机器学习:结点的实现,决策树代码实现(二)
文章目錄
- 楔子
- 定義變量:
- 定義方法
- 獲得劃分的feature
- 生成結點
- 停止條件及其處理
- fit()
- 生成樹剪枝
楔子
前面已經實現了各種信息量的計算,那么我們劃分的基本有了,那么我們需要使用這個基本來劃分,來生成決策樹,樹的基本單元是node,這里的node是一堆數據的集合+他們內在的方法。由于需要處理三種算法,我們最好能使用基類,該類應該至少包含:
1、選擇劃分的feature; 2、根據基準劃分數據生成新的結點; 3、判斷那些節點可以當成葉子,并歸類;定義變量:
node的變量是不是有點多,容易糊涂,我們需要抓住重點,首先是一個node,那么這個node有那些變量了,按照上述方法應該有如下三個變量:self.feature_dim(劃分的feature),self._children(生成的子節點),self.leafs(一顆子節點實際代表一顆子樹,這顆子樹下的所有葉子結點),當然還有數據集(self._x ,self._y),其他的變量根據需要慢慢添加。
# ============================================================================= # 實現完信息量的計算,下面就考慮決策樹的生成 # 決策樹是一顆x叉樹,需要數據結構node # 我們需要同時處理ID3,C4.5,CART所以需要建立一個基類 # 類的方法:1、根據離散特征劃分數據;2、根據連續特征劃分數據;3、根據當前數據判斷 # 屬于哪一個類別 # ============================================================================= import numpy as np from Cluster import Cluster class CvDNode:def __init__(self,tree=None,base=2,chaos=None,depth=0,parent=None,is_root=True,pre_feat="Root"):# 數據集的變量,都是numpy數組self._x = self._y = None# 記錄當前的log底和當前的不確定度self.base,self.chaos =base,chaos# 計算該節點的信息增益的方法self.criterion = None# 該節點所屬的類別self.category = None# 針對連續特征和CART,記錄當前結點的左右結點self.left_child = self.right_child =None# 該node的所有子節點和所有的葉子結點self._children,self.leafs = {},{}# 記錄樣本權重self.sample_weight =None# whether continuous記錄各個緯度的特征是否連續self.wc = None# 記錄該node為根的treeself.tree =tree# 如果傳入tree的話,初始化if tree is not None:# 數據預處理是由tree完成# 各個features是否連續也是由tree記錄self.wc = tree.whether_continuous# 這里的node變量是Tree中所記錄的所有node的列表tree.nodes.append(self)# 記錄該node劃分的相關信息:# 記錄劃分選取的featureself.feature_dim =None# 記錄劃分二分劃分的標準,針對連續特征和CARTself.tar = None# 記錄劃分標準的feature的featureValuesself.feats =[]# 記錄該結點的父節點和是否為根結點self.parent =parentself.is_root = is_root# 記錄結點的深度self._depth = depth# 記錄該節點的父節點的劃分標準featureself.pre_feat = pre_feat# 記錄該節點是否使用的CART算法self.is_cart =False# 記錄該node劃分標準feature是否是連續的self.is_continuous = False# 記錄該node是否已經被剪掉,后面剪枝會用到self.pruned = False# 用于剪枝時,刪除一個node對那些node有影響self.affected = False # 重載__getitem__運算符,支持[]運算符# 重載__getattribute__運算符,支持.運算符def __getitem__(self,item):if isinstance(item,str):return getattr(self,"_" + item)# 重載__lt__的方法,使node之間可以相互比較,less than,方便調試def __lt__(self,other):return self.pre_feat < other.pre_feat# 重載__str__和 __repr__為了方便調試def __str__(self):# 該節點的類別屬性if self.category is None:return "CvDNode ({}) ({} -> {})".format(self.depth,self.pre_feat,self.feature_dim)return "CvDNode ({}) ({} -> class:{})".format(self.depth,self.pre_feat,self.tree.label[self.category])# __repr__ 用于打印,面對程序員,__str__用于打印,面對用戶__repr__ = __str__ # ============================================================================= # 定義幾個property,Python內置的@property裝飾器就是負責把一個方法變成屬性調用的 # @property廣泛應用在類的定義中,可以讓調用者寫出簡短的代碼, # 同時保證對參數進行必要的檢查,這樣,程序運行時就減少了出錯的可能性。 # =============================================================================# 定義children屬性,主要區分開連續,CART和其余情況# 有了該屬性之后所有子節點都不需要區分情況了@propertydef children(self):return {"left" : self.left_child, "right" : self.right_child} if (self.is_cart or self.is_continuous) else self._children# 遞歸定義高度屬性# 葉子結點的高度為1,其余結點高度為子節點最高高度+1@propertydef height(self):if self.category is not None:return 1return 1 + max([_child.height if _child is not None else 0 for _child in self.children.values()])# 定義info_dic屬性(信息字典),記錄了該node的主要屬性# 在更新各個node的葉子結點時,葉子結點self.leafs的屬性就是該節點@propertydef info_dic(self):return {"chaos" : self.chaos,"y" :self._y}定義方法
大概回憶一下算法的大概處理流程:選擇劃分的features,遞歸的劃分生成子節點,滿足停止條件,形成葉子。上述就是整個fit的過程。在實現主要函數fit()前,我們需要定義好一些fit()會使用到的函數。
獲得劃分的feature
這個放在fit()函數里面,見fit()
生成結點
獲取了劃分的feature之后,就需要根據這標準來生成子樹,或者說子節點。
處理大概過程:我們需要生成一個新結點,需要知道他的數據樣本(通過mask找到),他對應的chaos,還有一些其他的輔助變量,形成新的結點或者子樹,然后遞歸處理這堆數據不斷的生成結點。這里面需要區分離散、連續和CART3種情況處理。
停止條件及其處理
什么時候停止生成子樹?就是什么時候我們形成葉子,兩種情況,見下述代碼。形成葉子之后的處理:已經判斷這堆數據可以當作葉子了,我們需要干什么:一是這對數據屬于哪一類(少數服從多數),二是更新當前結點的列祖列組的self.leafs,告訴列祖列組我是你們正宗的leaf。停止條件及其處理就是回溯法里面的限界函數,用于剪枝,實際上這個只是預剪枝。
# ============================================================================= # 定義生成算法的準備工作:定義停止生成的準則,定義停止后該node的行為 # =============================================================================# 停止的第一種情況:當特征緯度為0或者當前node的數據的不確定性小于閾值停止# 假如指定了樹的最大深度,那么當node的深度太深時也停止# 滿足停止條件返回True,否則返回Falsedef stop1(self,eps):if (self._x.shape[1] == 0 or (self.chaos is not None and self.chaos < eps)or (self.tree.max_depth is not None and self._depth >=self.tree.max_depth)):# 調用停止的方法self._handle_terminate()return Truereturn False# 當最大信息增益小于閾值時停止def stop2(self, max_gain, eps):if max_gain <= eps:# 調用停止的方法self._handle_terminate()return Truereturn False# 定義該node所屬類別的方法,假如特征已經選完了,將此事樣本中最多的類,作為該節點的類def get_category(self):return np.argmax(np.bincount(self._y))# 定義剪枝停止的處理方法,核心思想是:將node結點轉換為葉子結點def _handle_terminate(self):# 首先先生成該node的所屬類別self.category = self.get_category()# 然后一路回溯,更新該節點的所有祖先的葉子結點信息_parent =self.parentwhile _parent is not None:# id(self)獲取當前對象的內存地址_parent.leafs[id(self)] = self.info_dic_parent = _parent.parentfit()
下面就到了重點,這個是整個node處理的核心函數,實現前面提到的三個方法的處理流程,每次新結點的處理都是調用這個函數來實現:傳入數據集,計算信息量,得到劃分的feature,是否滿足停止條件,否生成子節點遞歸的處理。
# 挑選出最佳劃分的方法,要注意二分和多分的情況def fit(self, x, y, sample_weight, eps= 1e-8):self._x = np.atleast_2d(x)self._y = np.array(y)# 如果滿足第一種停止條件,則退出函數體if self.stop1(eps):return# 使用該node的數據實例化Cluster類以便計算各種信息量_cluster = Cluster(self._x, self._y, sample_weight, self.base)# 對于根節點,需要額外計算其數據的不確定性,第一次需要計算,# 其他時候已經傳入了chaosif self.is_root:if self.criterion == "gini":self.chaos = _cluster.gini()else:self.chaos = _cluster.ent()# 用于存最大增益_max_gain = 0# 用于存取最大增益的那個feature,不同的featureValue限制下的不確定度# 最后[featureValue] = 對應的不確定度_chaos_lst = []# 遍歷還能選擇的featuresfor feat in self.feats:# 如果是該維度是連續的,或者使用CART,則需要計算二分標準的featureValue# 的取值集合if self.wc[feat]:_samples = np.sort(self._x.T(feat))# 連續型的featureValue的取值集合_set = (_samples[:-1] + _samples[1:]) *0.5else:_set = self.tree.feature_sets[feat]# 連續型和CART,需要使用二分類的計算信息量if self.wc[feat] or self.is_cart:# 取一個featureValuefor tar in _set:_tmp_gain, _tmp_chaos_lst = _cluster.bin_info_gain(feat, tar, criterion=self.criterion,get_chaos_lst= True, continuous=self.wc[feat])if _tmp_gain > _max_gain:(_max_gain,_chaos_lst),_max_feature,_max_tar =(_tmp_gain, _tmp_chaos_lst), feat, tar# 離散數據使用一般的處理 else:_tmp_gain, _tmp_chaos_lst = _cluster.info_gain(feat, self.criterion, True, self.tree.feature_sets[feat])if _tmp_gain > _max_gain:(_max_gain,_chaos_lst),_max_feature =(_tmp_gain, _tmp_chaos_lst), feat# 當所有features里面最大的不確定都足夠小,退出函數體 if self.stop2(_max_gain, eps):return# 更新相關的屬性# 當前結點劃分的featureself.feature_dim = _max_feature# 二叉的處理if self.is_cart or self.wc[_max_feature]:self.tar = _max_tar# 根據劃分進行生成結點self._gen_children(_chaos_lst)# 這個是專門針對二叉的處理,是在生成樹到底之后回溯時# 生成樹同一層兩個結點都生成的話,看能不能剪枝# 實際這也是后剪枝的一種,但是二叉樹比較好處理,剪枝的策略也比較簡單# 只是簡單的去重,所以x叉樹沒有實施,后剪枝有更加高效的策略# 如果左右結點都是葉子結點且他們都屬于同一個分類,可以使用剪枝操作if (self.left_child.category is not None and self.left_child.category == self.right_child.category):self.prune()# 調用tree的方法,剪掉該節點的左右子樹# 從Tree的記錄所有Node的列表nodes中除去self.tree.reduce_nodes()else:# 離散情況的處理self._gen_children(_chaos_lst)# 上述的剪枝策略,只是同一父親的子結點去重,x叉樹不好實現。# 直接采用更加高效的策略后剪枝生成樹剪枝
得到一顆生成樹之后,這棵樹沒啥約束或者僅僅只靠信息增益約束,可能枝繁葉茂,為了使這棵樹在其他數據集上能取得較好的泛化性能,我們需要剪枝,刪除一些葉子結點。
在樹生成完畢或者局部生成完畢的剪枝稱之為后剪枝,上述fit()實現針對二叉樹的處理,左右結點生成完畢判斷兩個結點是否屬于同一類別剪枝,就是后剪枝的一種,實際后剪枝有一套全局規劃的策略,下次再講。
post-prune的實現:這種情況的剪枝生成樹已經生成完畢,剪枝相當于把下面所有的葉子看成一個葉子結點,那么需要把下面所有的葉子從列祖列宗的leafs譜里除名,然后把當前結點成了葉子加入到列祖列宗的leafs譜里,完了還需要標記自己和下面的結點為pruned已被剪掉,為什么自己也需要剪掉,因為葉子的信息已經在列祖列宗的leafs譜里。
至此node類的變量和方法基本實現完畢,為什么說基本呢,因為真正的后剪枝還沒將,他還需要在node類里添加一些方法。
總結
以上是生活随笔為你收集整理的机器学习:结点的实现,决策树代码实现(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 机器学习:信息熵,基尼系数,条件熵,条件
- 下一篇: 机器学习:决策树过拟合与剪枝,决策树代码