资讯动态

决策树算法ID3、C4.5、CART原理与Python实现详解

发布时间:2026/10/13 23:57:56 来源:尧图企业网站定制
1. 决策树算法为什么至今仍是特征工程的第一把刀很多人第一次接触机器学习都是从决策树开始的。它不像神经网络那样需要调参调到怀疑人生也不像支持向量机那样对核函数的选择极其敏感。决策树最大的魅力在于它把如果……那么……这种人类最自然的判断逻辑直接翻译成了可以计算的数学过程。你给它一批带标签的数据它就能自动帮你找出先看哪个特征、再看哪个特征的判断顺序。我最早在做一个用户流失预测的项目时手头有几十个特征逻辑回归跑出来效果一般换成梯度提升树之后效果提升明显。但当时我对底层的分裂逻辑其实是一知半解的只知道调包。后来因为要做一个需要向业务方解释为什么这个用户会被判定为高流失风险的需求我才回过头把CART、ID3、C4.5这三个经典算法从头到尾啃了一遍并且用Python从零实现。这个过程让我真正理解了为什么有的算法偏好取值多的特征有的算法要做剪枝有的算法只能处理离散特征。这篇文章就是那次系统梳理的产物。我会把ID3、C4.5、CART三个算法的核心原理讲透然后用Python从零实现它们的关键部分最后分享几个我在实际使用中踩过的坑和总结的经验。无论你是刚入门机器学习的学生还是想补一补底层原理的从业者这篇内容都能让你对决策树有一个从会用到懂为什么的跨越。需要提前说明的是这三个算法各有各的历史背景和适用场景没有绝对的优劣之分。ID3是基础C4.5是改进CART是工程化最成功的那个。理解它们的演进逻辑比死记公式重要得多。2. 从ID3的信息增益说起最直觉的分裂标准2.1 信息熵怎么用数学衡量混乱程度ID3算法的核心是信息增益而信息增益的基础是信息熵。熵这个概念来自信息论用来衡量一个系统的不确定性。放到决策树的场景里熵衡量的是当前数据集里标签的混乱程度。举个具体的例子。假设你现在有一个数据集里面只有两种标签流失和不流失。如果100个样本里50个流失、50个不流失那这个数据集非常混乱你随便猜一个正确率也就50%熵很高。如果95个不流失、5个流失那这个数据集相对纯净你全猜不流失就有95%的正确率熵很低。信息熵的计算公式是[ H(D) -\sum_{k1}^{K} p_k \log_2 p_k ]其中 ( p_k ) 是第k类样本在数据集D中出现的概率。用刚才的例子算一下50对50的时候( H -(0.5 \times \log_2 0.5 0.5 \times \log_2 0.5) 1 ) bit。95对5的时候( H -(0.95 \times \log_2 0.95 0.05 \times \log_2 0.05) \approx 0.286 ) bit。可以看到熵确实反映了混乱程度。注意这里的对数底数取2单位是比特。取自然对数或者以10为底也可以只是单位不同不影响比较大小。实际实现中通常用2或者ePython的numpy里都有现成的函数。2.2 信息增益的计算逻辑与一个完整算例有了熵信息增益就好理解了。信息增益 分裂前的熵 - 分裂后的加权平均熵。它衡量的是用某个特征把数据集切分之后不确定性减少了多少。我拿一个经典的打网球数据集来演示。假设有14天的天气记录特征包括 outlook天气、temperature温度、humidity湿度、windy是否有风标签是是否去打网球。原始数据集中9天打球、5天不打球所以分裂前的熵是[ H(D) -\frac{9}{14}\log_2\frac{9}{14} - \frac{5}{14}\log_2\frac{5}{14} \approx 0.940 ]现在用outlook这个特征来分裂。outlook有三个取值sunny、overcast、rainy。分裂后sunny组5个样本2个打球、3个不打熵约0.971overcast组4个样本全部打球熵为0rainy组5个样本3个打球、2个不打熵约0.971加权平均熵 (5/14)×0.971 (4/14)×0 (5/14)×0.971 ≈ 0.694。信息增益 0.940 - 0.694 0.246。同样的方法算出其他三个特征的信息增益选最大的那个作为分裂节点。这就是ID3的全部逻辑。2.3 ID3的致命偏好为什么取值多的特征占便宜ID3有一个被诟病很久的问题它天然偏好取值数量多的特征。极端情况下如果有一个特征是样本ID每个样本的ID都不同那用这个特征分裂后每个子节点只有一个样本熵全是0信息增益达到最大。但这显然没有任何泛化能力。这个问题的根源在于信息增益只考虑了不确定性减少的绝对量没有考虑分裂本身带来的复杂度代价。一个特征把数据切成100份每份都很纯信息增益当然大但这种分裂是没有意义的。我在实际项目中就遇到过类似的情况。当时有一个用户注册渠道的特征取值有几十个ID3跑出来第一层就选了这个特征但模型在测试集上表现很差。后来换成C4.5之后这个问题就缓解了很多。所以理解每个算法的偏好比会调包重要得多。3. C4.5的改进增益率与连续值处理3.1 增益率给分裂太多加上惩罚项C4.5针对ID3的偏好问题引入了增益率Gain Ratio的概念。增益率的计算方式是先算信息增益再除以一个叫分裂信息Split Information的量。分裂信息的公式是[ SplitInfo(D, A) -\sum_{v1}^{V} \frac{|D_v|}{|D|} \log_2 \frac{|D_v|}{|D|} ]其中V是特征A的取值个数( D_v ) 是取值为v的样本子集。这个量衡量的是把数据集分成V份这件事本身带来的不确定性。分得越多、越均匀分裂信息就越大。增益率 信息增益 / 分裂信息。还是用刚才的样本ID例子。如果每个样本ID都不同分裂信息就是 ( -\sum 1/14 \times \log_2(1/14) \log_2 14 \approx 3.807 )非常大。虽然信息增益也大但除以这个大的分裂信息之后增益率就被压下来了。不过C4.5并不是直接用增益率来选特征。因为增益率也有一个反向偏好它可能偏好取值少的特征。极端情况下如果某个特征只有一个取值分裂信息为0增益率会变成无穷大或者未定义。所以C4.5的实际做法是先从候选特征中选出信息增益高于平均水平的那些再从中选增益率最高的。这是一个两阶段的筛选策略。3.2 连续特征的处理二分法找最优切分点ID3只能处理离散特征这是它的一大局限。现实数据中连续特征太常见了比如年龄、收入、点击次数。C4.5通过二分法解决了这个问题。具体做法是对于某个连续特征先把所有样本按这个特征的值排序。然后考虑相邻两个值之间的中点作为候选切分点。比如特征值排序后是[10, 20, 30, 40]那候选切分点就是15、25、35。对每个候选切分点把数据分成小于等于切分点和大于切分点两组计算信息增益或增益率选最好的那个。这个过程的计算量是O(n log n)排序 O(n)遍历切分点对于中等规模的数据集完全可以接受。但如果有多个连续特征计算量会成倍增加。我在处理一个包含20多个连续特征的数据集时C4.5的单棵树训练时间明显比ID3长原因就在这里。实操心得如果连续特征特别多可以先做一轮分箱比如等频分箱或等距分箱把连续值转成离散值再跑C4.5。这样能大幅减少候选切分点的数量训练速度会快很多。代价是可能损失一些精度需要根据具体场景权衡。3.3 剪枝C4.5的另一个关键改进C4.5还引入了剪枝机制这是ID3完全没有的。决策树如果不剪枝很容易过拟合——树长得太深把训练数据里的噪声都学进去了。C4.5用的是悲观剪枝Pessimistic Error Pruning。它的思路是对于每个子树用训练数据上的错误率加上一个惩罚项来估计它在未知数据上的错误率。如果把这个子树替换成叶子节点后估计错误率没有变差就剪掉。具体来说对于一个叶子节点如果它覆盖了N个样本其中E个分类错误那错误率是E/N。悲观剪枝会把这个错误率上调比如加上0.5的惩罚连续修正变成(E0.5)/N。对于子树把所有叶子节点的错误数加起来再除以总样本数同样加上惩罚。如果子树的悲观错误率大于等于把它替换成单叶节点的悲观错误率就执行剪枝。这个方法的优点是只需要训练数据不需要单独的验证集。但它的惩罚系数是固定的对于不同规模的数据集可能不是最优的。我在实际使用中如果数据量足够更倾向于用验证集来做后剪枝效果会更稳定。4. CART工程化最成功的决策树4.1 基尼指数比熵更快的分裂标准CARTClassification and Regression Trees用的是基尼指数Gini Index来选择分裂特征。基尼指数的公式是[ Gini(D) 1 - \sum_{k1}^{K} p_k^2 ]还是用50对50的例子( Gini 1 - (0.5^2 0.5^2) 0.5 )。95对5的例子( Gini 1 - (0.95^2 0.05^2) 0.095 )。基尼指数和熵的性质很像都衡量混乱程度但基尼指数不需要算对数计算速度更快。在实际工程中这个速度差异在数据量大的时候非常明显。我做过一个对比测试在100万条数据上用基尼指数比用信息熵大约快20%到30%。CART每次分裂都是二分的即使特征有多个取值也会把它分成属于某个子集和不属于某个子集两组。对于有V个取值的特征CART会考虑所有可能的二分方式。这和ID3/C4.5的多路分裂不同CART生成的是一棵二叉树。4.2 CART如何处理回归问题CART是三个算法中唯一能直接处理回归问题的。对于回归问题CART用均方误差MSE来衡量分裂质量。具体来说对于每个候选切分点把数据分成两组分别计算两组的MSE然后加权求和。选MSE最小的那个切分点。叶子节点的预测值就是该节点内所有样本标签的均值。这个逻辑非常直观如果你要预测一个连续值用均值是最自然的估计。我在做一个房价预测的项目时用CART回归树跑了一版虽然单棵树的精度不如随机森林但它的可解释性非常好。你可以直接看到面积小于90平米且房龄大于10年的房子均价是多少这种规则对业务方来说非常友好。4.3 三个算法的核心差异对比维度ID3C4.5CART分裂标准信息增益增益率基尼指数分类/ MSE回归特征类型仅离散离散连续离散连续分裂方式多路分裂多路分裂二分剪枝无悲观剪枝代价复杂度剪枝缺失值处理不支持支持支持任务类型仅分类仅分类分类回归这张表是我自己在复习时整理的放在这里方便你快速对比。可以看到CART在工程化方面是最完善的这也是为什么现在主流的集成学习算法比如随机森林、梯度提升树底层用的都是CART而不是ID3或C4.5。5. 用Python从零实现三个算法的核心逻辑5.1 信息熵与基尼指数的实现先从最基础的计算函数开始。这部分代码我写得比较直白没有做太多优化目的是让逻辑清晰可读。import numpy as np from collections import Counter def entropy(y): 计算信息熵 counter Counter(y) total len(y) ent 0.0 for count in counter.values(): p count / total ent - p * np.log2(p) return ent def gini(y): 计算基尼指数 counter Counter(y) total len(y) gini_val 1.0 for count in counter.values(): p count / total gini_val - p ** 2 return gini_val def split_info(y, groups): 计算分裂信息用于C4.5的增益率 total len(y) si 0.0 for group in groups: p len(group) / total if p 0: si - p * np.log2(p) return si这几个函数是整个算法的基础。entropy和gini分别对应ID3/C4.5和CART的分裂标准split_info是C4.5特有的。注意split_info里加了一个if p 0的判断防止空组导致对数运算出错。5.2 ID3的信息增益计算与特征选择接下来实现ID3的特征选择逻辑。核心是遍历每个特征的每个取值计算加权平均熵然后得到信息增益。def information_gain(X, y, feature_idx): 计算某个特征的信息增益 total len(y) base_ent entropy(y) # 按特征取值分组 groups {} for i, val in enumerate(X[:, feature_idx]): if val not in groups: groups[val] [] groups[val].append(y[i]) # 计算加权平均熵 weighted_ent 0.0 for group in groups.values(): weighted_ent (len(group) / total) * entropy(group) return base_ent - weighted_ent def choose_best_feature_id3(X, y, feature_indices): ID3选择最佳分裂特征 best_gain -1 best_feature None for idx in feature_indices: gain information_gain(X, y, idx) if gain best_gain: best_gain gain best_feature idx return best_feature, best_gain这段代码的逻辑很直接对每个候选特征算信息增益选最大的。feature_indices参数用来控制当前节点还可以用哪些特征因为决策树在分裂后已经用过的特征通常不再重复使用对于离散特征而言。5.3 C4.5的增益率与连续值二分C4.5的实现需要在ID3的基础上加两个东西增益率计算和连续值处理。def gain_ratio(X, y, feature_idx): 计算增益率 total len(y) base_ent entropy(y) groups {} for i, val in enumerate(X[:, feature_idx]): if val not in groups: groups[val] [] groups[val].append(y[i]) weighted_ent 0.0 for group in groups.values(): weighted_ent (len(group) / total) * entropy(group) gain base_ent - weighted_ent si split_info(y, list(groups.values())) if si 0: return 0 return gain / si def choose_best_feature_c45(X, y, feature_indices, continuous_indices): C4.5选择最佳特征先按信息增益筛选再按增益率选 gains {} for idx in feature_indices: if idx in continuous_indices: # 连续特征找最优二分点 gain, threshold best_split_continuous(X, y, idx) gains[idx] (gain, threshold) else: gains[idx] (information_gain(X, y, idx), None) # 计算平均增益 avg_gain np.mean([g for g, _ in gains.values()]) # 在高于平均增益的特征中选增益率最高的 best_ratio -1 best_feature None best_threshold None for idx, (gain, threshold) in gains.items(): if gain avg_gain: if idx in continuous_indices: ratio gain / (split_info(y, [[y[i] for i in range(len(y)) if X[i, idx] threshold], [y[i] for i in range(len(y)) if X[i, idx] threshold]]) 1e-10) else: ratio gain_ratio(X, y, idx) if ratio best_ratio: best_ratio ratio best_feature idx best_threshold threshold return best_feature, best_threshold def best_split_continuous(X, y, feature_idx): 连续特征的最优二分点 values sorted(set(X[:, feature_idx])) if len(values) 1: return 0, None best_gain -1 best_threshold None base_ent entropy(y) for i in range(len(values) - 1): threshold (values[i] values[i1]) / 2 left_y [y[j] for j in range(len(y)) if X[j, feature_idx] threshold] right_y [y[j] for j in range(len(y)) if X[j, feature_idx] threshold] if len(left_y) 0 or len(right_y) 0: continue weighted_ent (len(left_y)/len(y)) * entropy(left_y) \ (len(right_y)/len(y)) * entropy(right_y) gain base_ent - weighted_ent if gain best_gain: best_gain gain best_threshold threshold return best_gain, best_threshold这段代码里best_split_continuous是连续值处理的核心。它遍历所有相邻值的中点作为候选切分点选信息增益最大的。注意我加了一个1e-10的小量防止除零错误这是实际编码中很容易忽略的细节。5.4 CART的递归建树与剪枝框架CART的实现我用一个类来组织这样更清晰。class CARTNode: def __init__(self, featureNone, thresholdNone, leftNone, rightNone, valueNone): self.feature feature self.threshold threshold self.left left self.right right self.value value # 叶子节点的预测值 class CART: def __init__(self, max_depth10, min_samples_split2, min_samples_leaf1, taskclassification): self.max_depth max_depth self.min_samples_split min_samples_split self.min_samples_leaf min_samples_leaf self.task task self.root None def _split(self, X, y, feature_idx, threshold): left_mask X[:, feature_idx] threshold right_mask ~left_mask return X[left_mask], y[left_mask], X[right_mask], y[right_mask] def _best_split(self, X, y): best_gain -1 best_feature None best_threshold None for feature_idx in range(X.shape[1]): values sorted(set(X[:, feature_idx])) for i in range(len(values) - 1): threshold (values[i] values[i1]) / 2 X_left, y_left, X_right, y_right self._split(X, y, feature_idx, threshold) if len(y_left) self.min_samples_leaf or len(y_right) self.min_samples_leaf: continue if self.task classification: gain gini(y) - (len(y_left)/len(y)) * gini(y_left) - \ (len(y_right)/len(y)) * gini(y_right) else: gain np.var(y) - (len(y_left)/len(y)) * np.var(y_left) - \ (len(y_right)/len(y)) * np.var(y_right) if gain best_gain: best_gain gain best_feature feature_idx best_threshold threshold return best_feature, best_threshold def _build(self, X, y, depth): if depth self.max_depth or len(y) self.min_samples_split or len(set(y)) 1: if self.task classification: return CARTNode(valueCounter(y).most_common(1)[0][0]) else: return CARTNode(valuenp.mean(y)) feature, threshold self._best_split(X, y) if feature is None: if self.task classification: return CARTNode(valueCounter(y).most_common(1)[0][0]) else: return CARTNode(valuenp.mean(y)) X_left, y_left, X_right, y_right self._split(X, y, feature, threshold) left self._build(X_left, y_left, depth 1) right self._build(X_right, y_right, depth 1) return CARTNode(featurefeature, thresholdthreshold, leftleft, rightright) def fit(self, X, y): self.root self._build(X, y, 0) def _predict_one(self, x, node): if node.value is not None: return node.value if x[node.feature] node.threshold: return self._predict_one(x, node.left) return self._predict_one(x, node.right) def predict(self, X): return [self._predict_one(x, self.root) for x in X]这个实现包含了CART的核心逻辑递归建树、基尼指数/MSE分裂、叶子节点预测。max_depth、min_samples_split、min_samples_leaf这三个参数是控制过拟合的关键实际使用时需要根据数据规模调整。实操心得min_samples_leaf这个参数我一般不会设得太小。如果设成1树很容易长出只包含一两个样本的叶子节点这些节点几乎肯定是过拟合的。我通常从5开始试如果数据量很大可以设到20甚至50。max_depth的话对于几万条数据10到15层通常够用了。6. 实际项目中的调参与避坑经验6.1 特征取值过多时ID3为什么会翻车前面提到过ID3偏好取值多的特征这里展开说一下我在实际项目中遇到的具体情况。当时有一个商品类目的特征取值有200多个。ID3在第一层就选了这个特征把数据切成了200多份每份的熵都很低。训练集上的准确率看起来不错但测试集上惨不忍睹。原因很简单200多个类目里很多类目只有几个样本。ID3把这些小类目单独分成一个节点相当于给每个小类目都做了一个专属规则但这些规则完全没有泛化能力。换成C4.5之后增益率的分裂信息惩罚项把这个特征压下去了模型选择了用户活跃度这个取值只有5个但区分度更高的特征。这个经历让我明白一个道理特征取值数量本身就是一个需要关注的维度。在做特征工程的时候对于取值特别多的类别特征要么做目标编码要么做频次筛选把出现次数少于某个阈值的类别合并成其他不要直接扔给ID3。6.2 连续特征分箱的粒度怎么定C4.5和CART都能处理连续特征但它们的处理方式不同。C4.5是找最优二分点CART也是二分但CART会考虑所有可能的二分组合对于多值离散特征。对于连续特征两者其实差不多。我在实际使用中如果连续特征的数量不多比如少于10个直接让算法自己找切分点就行。但如果连续特征很多我会先做一轮分箱。分箱的粒度怎么定我的经验是数据量小于1万条分10到20个箱数据量1万到10万条分20到50个箱数据量大于10万条分50到100个箱分箱方法我一般用等频分箱因为等距分箱在数据分布倾斜的时候会导致某些箱里样本极少。等频分箱保证每个箱里的样本数量大致相等分裂时更稳定。当然分箱会损失信息。如果追求极致精度可以不分箱让算法自己找最优切分点。但训练时间会显著增加。这是一个精度和效率的权衡需要根据具体场景决定。6.3 剪枝策略的选择预剪枝还是后剪枝剪枝分为预剪枝和后剪枝。预剪枝是在建树过程中就限制树的生长比如设置最大深度、最小样本分裂数。后剪枝是先让树长到最大然后再回头剪掉不必要的子树。预剪枝的优点是速度快缺点是可能欠拟合——因为它是贪心的可能在某个节点觉得不该分裂但实际上再往下分裂几层之后效果会很好。后剪枝的优点是效果通常更好缺点是计算量大需要先建一棵完整的树。我在实际项目中的做法是先用预剪枝快速跑一版看看大致的特征重要性和模型表现然后再用后剪枝精细调优。如果时间紧预剪枝的参数调好也能达到不错的效果。如果追求极致后剪枝是必须的。CART用的代价复杂度剪枝Cost-Complexity Pruning是后剪枝里比较经典的一种。它的核心思想是给树的复杂度加一个惩罚项定义一个代价函数 ( R_\alpha(T) R(T) \alpha |T| )其中 ( R(T) ) 是训练误差( |T| ) 是叶子节点数量( \alpha ) 是惩罚系数。通过调整 ( \alpha )可以得到一系列不同复杂度的子树然后用交叉验证选最好的那个。6.4 缺失值处理的几种实用方案ID3不支持缺失值C4.5和CART支持。C4.5的处理方式是对于缺失某个特征的样本把它同时分配到所有子节点但分配时带一个权重权重等于该子节点样本数占非缺失样本总数的比例。CART的处理方式是为缺失值单独分一个分支或者用替代分裂Surrogate Split。在实际项目中我的处理优先级是如果缺失比例很低小于5%直接删除缺失样本简单粗暴但有效。如果缺失比例中等5%到30%用均值、中位数或众数填充或者用模型预测填充。如果缺失比例很高大于30%考虑把这个特征直接删掉或者把是否缺失本身作为一个新特征。注意填充缺失值的时候一定要在训练集上计算填充值比如均值然后应用到测试集。如果直接在全部数据上计算均值再填充会造成数据泄露导致模型评估结果偏乐观。这个坑我踩过当时模型在交叉验证上表现很好上线后效果差了一大截排查了很久才发现是这个问题。7. 从单棵树到集成学习决策树的真正价值7.1 为什么随机森林和GBDT都选CART作为基学习器现在主流的集成学习算法无论是随机森林还是梯度提升树底层用的都是CART而不是ID3或C4.5。原因有几个第一CART的二分结构更适合集成。集成学习需要基学习器有一定的多样性CART的二分方式让每棵树的形态更加灵活。ID3和C4.5的多路分裂会导致树的结构比较宽集成时多样性不如CART。第二CART能处理回归问题。随机森林和GBDT都有回归版本CART是唯一能直接支持回归的。第三CART的工程实现更成熟。CART的代价复杂度剪枝、缺失值处理、连续值处理都有很成熟的工程方案而ID3和C4.5在这些方面要么不支持要么实现起来比较麻烦。我在实际项目中几乎不会单独使用一棵决策树。单棵树的方差太大换个训练集可能结果就差很多。但把几百棵CART树集成起来效果就非常稳定了。这也是为什么我说理解CART是理解集成学习的必经之路。7.2 特征重要性决策树给特征工程的反哺决策树有一个非常好的副产品特征重要性。对于单棵树一个特征的重要性可以通过它带来的不纯度减少量来衡量。具体来说遍历树的所有分裂节点把每个节点的不纯度减少量加权后累加到对应特征上最后归一化。这个特征重要性在特征工程中非常有用。我经常用它来做第一轮特征筛选先跑一棵CART树看看哪些特征重要性高哪些接近零。重要性接近零的特征可以直接删掉减少后续模型的计算量。但要注意决策树的特征重要性是有偏的。它偏好取值多的特征因为取值多的特征有更多机会被选为分裂节点。如果要用特征重要性做筛选最好用随机森林或者梯度提升树算出来的重要性它们通过集成和采样机制偏差会小很多。7.3 决策树的可解释性优势与局限决策树最大的优势是可解释性。你可以直接把树画出来看到每一条决策路径。这在需要向业务方解释模型决策的场景下非常有用。比如在风控场景中你可以告诉审核人员这个用户被拒绝是因为他的收入低于5000且负债率高于70%。这种解释是业务方能够理解和信任的。但决策树的可解释性也有局限。当树很深的时候路径会变得非常复杂人脑很难跟踪。而且单棵树的稳定性差训练数据稍微变一下树的结构可能就完全不同了。所以决策树的可解释性优势在浅层树深度小于5上最明显深层树的可解释性其实并不比神经网络好多少。我在实际项目中的做法是如果业务方需要解释我会单独训练一棵浅层CART树深度3到5用它来做决策解释。同时用随机森林或GBDT来做实际预测。两者结合既保证了预测精度又满足了可解释性需求。8. 几个容易被忽略的实现细节8.1 对数运算的底数选择与数值稳定性信息熵和分裂信息都涉及对数运算。底数选2还是e对分裂结果没有影响因为不同底数之间只差一个常数因子。但在实际编码中要注意数值稳定性。当某个类别的概率为0时( \log_2 0 ) 是负无穷。虽然理论上概率为0的项对熵的贡献是0因为 ( 0 \times \log 0 0 )但在代码里直接算会报错。所以我在entropy函数里用了Counter来统计只遍历出现过的类别天然避免了这个问题。另外如果数据集非常大概率值可能非常小对数运算的结果可能超出浮点数的精度范围。这种情况下可以考虑用np.log1p或者对概率做平滑处理。不过对于一般规模的数据集这个问题不突出。8.2 递归深度与栈溢出风险决策树的建树过程是递归的。如果树很深比如数据里有大量噪声树一直长到每个叶子只有一个样本递归深度可能达到几千层导致栈溢出。Python默认的递归深度限制是1000。如果数据量很大且不设深度限制很容易触发这个限制。解决方案有两个一是设置max_depth参数限制树的最大深度二是把递归改成迭代用栈来模拟递归过程。我一般用第一种方案因为设置max_depth本身就是防止过拟合的必要手段。如果确实需要很深的树可以把sys.setrecursionlimit调大但要小心内存消耗。8.3 类别特征的编码方式对分裂结果的影响决策树对类别特征的编码方式比较敏感。如果用独热编码One-Hot一个取值有10个的类别特征会变成10个二元特征。CART在分裂时会考虑这10个二元特征的所有二分组合计算量是2的10次方非常大。所以对于取值较多的类别特征我一般不用独热编码而是用标签编码Label Encoding或者目标编码Target Encoding。标签编码把每个类别映射成一个整数CART会基于这个整数的大小关系来找切分点。但这样会引入一个假设类别之间是有序的。如果类别本身没有顺序关系这个假设可能不合理。目标编码是用类别的目标均值来替换类别值这样既保留了类别信息又避免了引入虚假的顺序关系。但目标编码容易过拟合需要配合交叉验证或者平滑处理。我在实际使用中对于取值少于10个的类别特征用独热编码对于取值多于10个的用目标编码加平滑。9. 写在最后我踩过的那些坑回顾这几年在决策树上的实践有几个坑让我印象特别深。第一个坑是在训练集上计算填充值。前面提过这个错误会导致数据泄露模型评估结果虚高。后来我养成了一个习惯任何涉及全局统计量的操作均值、中位数、分位数都只在训练集上计算然后应用到测试集。第二个坑是忽略了特征取值数量的影响。早期我用ID3跑一个包含高基数类别特征的数据集模型在训练集上表现很好测试集上很差。后来换成C4.5并做了类别合并问题才解决。这个经历让我明白算法的选择不是随意的每个算法都有它的偏好和适用场景。第三个坑是剪枝参数设得太激进。有一次我把min_samples_leaf设成了50结果树只长了3层就停了模型严重欠拟合。后来我学会了先用默认参数跑一版看看树的深度和叶子节点数量再根据实际情况调整剪枝参数。第四个坑是没有做特征重要性分析。有一次我直接把所有特征扔给CART跑出来效果一般。后来看了特征重要性发现前10个重要特征贡献了90%以上的不纯度减少后面几十个特征几乎没用到。删掉那些无用特征后模型训练速度提升了一倍效果还略有提升。这些坑说到底都是对算法原理理解不够深入导致的。如果你能把ID3、C4.5、CART三个算法的分裂标准、剪枝策略、适用场景都搞清楚大部分坑都可以提前避开。希望这篇内容能帮你少走一些弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑