机器学习:决策树过拟合与剪枝,决策树代码实现(三)
文章目錄
- 楔子
- 變量
- 方法
- 數據預處理
- 剪枝
- 獲取待剪集:
- 針對ID3,C4.5的剪枝
- 損失函數的設計
- 基于該損失函數的算法描述
- 基于該損失函數的代碼實現
- 針對CART的剪枝
- 損失函數的設計
- 基于該損失函數的算法描述
- 基于該損失函數的代碼實現
- 獲得p顆生成樹
- 選取最優生成樹
- 整個流程處理fit():
楔子
上次講到:至此node類的變量和方法基本實現完畢,為什么說基本呢,因為真正的后剪枝還沒講,他還需要在node類里添加一些方法。這一次來講一下后剪枝。
首先,后剪枝是對整個生成樹操作,我們給整個樹的操作定義一個基類,定義一個新類就涉及到:變量和方法
Tree 結構需要做到如下幾點:
定義好需要在各個 Node 上調用的“全局變量” 做好數據預處理的工作、保證傳給 Node 的數據是合乎要求的 對各個 Node 進行合適的封裝,做到:生成決策樹時能夠正確地調用它們的生成算法進行后剪枝時能夠正確地調用它們的局部剪枝函數 定義預測函數和評估函數以供用戶調用變量
首先,我們思考一下,我們整體考慮生成樹,并對樹進行操作,我們需要操作哪些對象:
1、我們需要剪枝,就需要對結點操作,在這里我們不好每次都遍歷樹一遍,我們把所有的node存下來專門處理,self.nodes = []
2、每個node都有一個可選features的列表,但是選中某個feature之后,遍歷featureValue時,在node里面沒有變量定義,在全局變量里面定義一個,所有features的featureValue的變量:self.feature_sets;同樣的道理各個特征的維度是否連續也是如此:self.whether_continuous
3、剪枝屬于全局的操作,變量也應該是全局的:限制樹的深度:self.max_depth;CART種需要處理p顆生成樹:self.roots
4、還有一個最終要的變量,就是樹的根:self.root
方法
數據預處理
自動判斷哪些features為連續的;初始化樹的全局變量
def feed_data(self, x, continuous_rate = 0.2):# continuous_rate用于判斷該維度是否是連續的# 利用set獲取各個維度的特征可能取值self.feature_sets = [set(dimension) for dimension in x.T]data_len, data_dim = x.shape# 判斷是否連續self.whether_continuous = np.array([len(feat) >= continuous_rate*data_len for feat in self.feature_sets])# 根節點可選的劃分特征維度self.root.feats = [i for i in range(x.shape[1])]# 把self.root.feed_tree(self)最后一行我們對根節點調用了feed_tree方法,該方法會做以下三件事:
讓決策樹中所有的 Node 記錄一下它們所屬的 Tree 結構 將自己記錄在 Tree 中記錄所有 Node 的列表nodes里 根據 Tree 的相應屬性更新記錄連續特征的列表 # 栽樹,會做三件事# 決策樹所有node記錄他們屬于哪一顆樹# 把所有結點保存到self.tree.nodes# 更新每一個結點的特征是否連續的列表def feed_tree(self, tree):self.tree = treeself.tree.nodes.append(self)self.wc = tree.whether_continuousfor child in self.children.values():if child is not None:child.feed_tree(tree)剪枝
剪枝時,需要獲取所有的非葉子結點,為待剪集,從底層像高層一層一層的剪枝。
獲取待剪集:
# ============================================================================= # # 定義Prune # 因為是后剪枝是針對全局的考慮,要決定那些結點需要剪枝,然后再調用結點的剪枝 # =============================================================================# 獲取每一層的結點self.layers:[depth,node_lst] = nodedef _update_layers(self):self.layers = [[] for _ in range(self.root.height)]self.root.update_layers()# Util# 獲取以當前結點為根的樹的每一層結點列表def update_layers(self):self.tree.layers[self._depth].append(self)for node in sorted(self.children):node = self.children[node]if node is not None:node.update_layers()針對ID3,C4.5的剪枝
損失函數的設計
# 新的損失函數,當未剪枝時損失,已剪枝或者葉子的損失def cost(self, pruned=False):if not pruned:return sum([leaf["chaos"] * len(leaf["y"]) for leaf in self.leafs.values()])return self.chaos * len(self._y)# node.cost() + self.prune_alpha * len(node.leafs)基于該損失函數的算法描述
基于該損失函數的代碼實現
# 離散數據的剪枝函數def _prune(self):# 獲取生成樹每一層的結點,每一層結點按照其劃分feature順序排列self._update_layers()# 用于保存所有的非葉子結點,為待剪枝結點,保存順序前面的靠近底部,后面的靠近根部tmp_nodes = []append = tmp_nodes.appendfor node_lst in self.layers[::-1]:for node in node_lst[::-1]:if node.category is None:append(node)# 剪枝的新損失函數 = 各個葉子不確定度*葉子樣本數量加權和 + alpha*葉子個數# old為剪枝前的損失函數,所有的待剪枝結點的剪枝前的損失函數old = np.array([node.cost() + self.prune_alpha * len(node.leafs) for node in tmp_nodes])# 假如進行剪枝后,當前結點變成葉子,損失函數 = 當前結點的不確定度*樣本個數 + alpha*1new = np.array([node.cost(pruned=True) + self.prune_alpha for node in tmp_nodes])# 根據這個得到待剪枝的結點maskmask = old >= newwhile True:# 剪到根時退出if self.root.height == 1:break# 獲取最深的待剪枝的結點,從下往上的剪枝,取的是第一個True,前面都是靠近底部的結點p = np.argmax(mask) # type: int# 判斷一下是否是可剪枝的,每次剪枝之后,會影響上層的結點,可能Ture變成了False,# 最后一次時里面,里面可能全部都是False if mask[p]:# 對這個結點剪枝,該做的操作在結點里面都操作了,里面還有一項操作# 就是剪枝該結點,會對那些結點有影響,就是他的祖宗們,已標記node.affectetmp_nodes[p].prune()# 遍歷所有的待剪枝結點,挑出被當前結點影響的結點for i, node in enumerate(tmp_nodes):if node.affected:# 更新那些結點的損失函數old[i] = node.cost() + self.prune_alpha * len(node.leafs)# 再次判斷是否需要被剪枝,new是不會變的他只和樣本有關mask[i] = old[i] >= new[i]# 重置一下,以免下次也更新他了node.affected = False# 把標記為已剪枝的結點從待剪枝結點列表刪除,當前結點也是標記為已剪枝的# 他已經變成葉子結點,葉子結點是不在待剪枝列表的for i in range(len(tmp_nodes) - 1, -1, -1):if tmp_nodes[i].pruned:tmp_nodes.pop(i)old = np.delete(old, i)new = np.delete(new, i)mask = np.delete(mask, i)# 假如待剪枝列表沒有可剪枝的也退出else:break# 剪枝完畢之后,新的生成樹,更新一下,這棵樹的nodes列表,把前面刪除的葉子都刪除掉# 前后的剪枝函數主要處理的是leafs,沒有處理nodes,所以最后處理一下。self.reduce_nodes()針對CART的剪枝
損失函數的設計
這個的設計思想是,隨著懲罰因子alpha從0到大不斷增加,結點被一個一個剪掉,每剪掉一顆都是一棵樹保存起來,最后只剩下root,形成了p棵樹,求p棵樹里面的最優樹。
每一個結點都有一個alpha的閾值,超過了這個閾值,該節點就可以被剪掉。
閾值的實現:
基于該損失函數的算法描述
基于該損失函數的代碼實現
獲得p顆生成樹
# CART的剪枝處理def _cart_prune(self):# 初始化整顆樹的self.tree值,這棵樹的每個結點屬于哪棵樹self.root.cut_tree()# 獲取待剪枝的結點列表,也就是非葉子結點tmp_nodes = [node for node in self.nodes if node.category is None]# 計算這些候選集的閾值thresholds = np.array([node.get_threshold() for node in tmp_nodes])while True:# 理論上我們需要記錄p棵樹,然后在p顆樹里找最好的那棵樹,# 因此我們需要深度copy原始樹,在此基本上剪枝,每次形成不同的樹root_copy = deepcopy(self.root)# self.roots用于記錄產生的p棵樹,先把原始樹存進來self.roots.append(root_copy)# 出口,只剩根結點了,p棵樹產生完畢if self.root.height == 1:break# 取閾值最低的結點,那個結點第一個被剪p = np.argmin(thresholds) # type: int# 下面的處理和離散處理一致tmp_nodes[p].prune()# 剪掉之后,看哪些結點受影響了,更新受影響的結點for i, node in enumerate(tmp_nodes):if node.affected:# 對于受影響的結點,更新一下閾值thresholds[i] = node.get_threshold()node.affected = Falsepop = tmp_nodes.popfor i in range(len(tmp_nodes) - 1, -1, -1):if tmp_nodes[i].pruned:pop(i)thresholds = np.delete(thresholds, i)self.reduce_nodes()選取最優生成樹
# 定義選擇那個樹最優的標準,使用加權正確率作為交叉驗證的標準def acc(self, y, y_pred, weights):if weights is not None:return np.sum((np.array(y) == np.array(y_pred))*weights) /len(y)return np.sum(np.array(y) == np.array(y_pred)) /len(y)# 后剪枝是通過比較每棵樹在驗證集上的表現來找出最優樹def prune(self, x_cv, y_cv, weights):if self.root.is_cart:if x_cv is not None and y_cv is not None:self._cart_prune()# 選出最優的子樹arg = np.argmax([self.acc(y_cv, tree.predict(x_cv), weights) for tree in self.roots]) # type: inttar_root = self.roots[arg]self.nodes = []# 更新一下樹的相關信息,所屬tree,所有的nodestar_root.feed_tree(self)# 把指針給rootself.root = tar_rootelse:self._prune()整個流程處理fit():
方法都有了下面就開始整個操作流程:準備數據,數據預處理,生成樹,剪枝
# =============================================================================# 參數alpha和剪枝有關;cv_rate用于控制交叉驗證集大小;train_only是否進行數據集切分def fit(self,x,y,alpha= None, sample_weight= None, eps= 12-8, cv_rate= 0.2, train_only= False):# 數值化類別向量_dic = {c:i for i,c in enumerate(set(y))}# 將y數值化y = np.array([_dic[yy] for yy in y])# 保存ID-->class映射,這樣才可以反向找回去self.label_dic = {value:key for key,value in _dic.items()}# 如果x為非數值的,也需要數值化x = np.array(x)# 根據特征個數給出alphaself.prune_alpha = alpha if alpha is not None else x.shape[1]/2# 劃分數據集if not train_only and self.root.is_cart:# 利用下標實現各種切分_train_num = int(len(x)*(1-cv_rate))# 相當于打亂了順序_indices = np.random.permutation(np.arange(len(x)))_train_indices = _indices[:_train_num]_test_indices = _indices[_train_num:]# 針對樣本權重的處理if sample_weight is not None:# 切分后的樣本權重需要做歸一化處理_train_weight = sample_weight[_train_indices]_test_weight = sample_weight[_test_indices]# 歸一化_train_weight /= np.sum(_train_weight)_test_weight /= np.sum(_test_weight)else:_train_weight = _test_weight = Nonex_train, y_train = x[_train_indices],y[_train_indices]x_cv, y_cv = x[_test_indices],y[_test_indices]else:x_train, y_train, _train_weight = x, y, sample_weightx_cv = y_cv = _test_weight = None# 數據預處理 self.feed_data(x_train)# 調用根節點的生成算法self.root.fit(x_train, y_train, _train_weight, eps)# 調用對node的剪枝算法的封裝self.prune(x_cv, y_cv, _test_weight)# 定義刪除結點方法,從后往前刪除,這樣就可以使用popdef reduce_nodes(self):for i in range(len(self.nodes)-1, -1, -1):if self.nodes[i].pruned:self.nodes.pop(i) 與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的机器学习:决策树过拟合与剪枝,决策树代码实现(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 机器学习:结点的实现,决策树代码实现(二
- 下一篇: 机器学习:集成学习(ensemble),