资讯动态

决策树算法详解:从ID3、C4.5到CART的演进与实战选型

发布时间:2026/10/2 13:23:04 来源:尧图企业网站定制
先交代一个背景我之前在一家金融科技公司做风控模型的基线方案拿到一份客户流失预测的数据集特征是几十个常规业务指标加一个渠道编号。当时团队里有人直接用 sklearn 默认参数跑决策树发现模型在训练集上准确率接近 98%测试集只有 74%而且树的第一层竟然分裂的是“客户编号”这个特征。但凡你对决策树三种算法ID3、C4.5、CART的区别有点概念就不会犯这种错ID3 的信息增益天然偏好取值多的特征“客户编号”这种高基数列就是它的重灾区。很多人学决策树时只记住了“分类树”“信息增益”“Gini 系数”这几个名词但真正落地时容易卡在三个问题上三种算法到底差在哪为什么现在几乎没人用 ID3但 C4.5 也慢慢淡出主流sklearn 里只能选 CART那还需要了解前两个吗这篇文章我把这三个问题一次性讲透从公式推导、分裂逻辑到剪枝策略最后给一份实战选型建议。适合刚学机器学习的同学也适合那些用过决策树但一直没搞懂底层原理的工程朋友。1. 三种算法的核心思想与演进逻辑1.1 熵、条件熵、信息增益决策树的底层语言想理解 ID3、C4.5、CART 的区别先得把决策树的分裂逻辑搞清楚。整棵树的生成过程本质上是一个“递归划分特征空间”的过程每次从所有特征中挑一个最优特征把当前样本集切分成几个子集每个子集再递归地重复这个过程直到满足停止条件。“最优特征”怎么定义这就用到信息论里的概念。信息熵Entropy衡量的是一个集合的混乱程度公式是Entropy(S) -Σ p_i · log₂(p_i)其中 p_i 是第 i 类样本在集合 S 中占的比例。举一个生活化的例子一个抽屉里全是黑色袜子熵是 0因为没有任何不确定性如果抽屉里黑袜子和白袜子各占一半熵就是 1因为你随机抽一只袜子时“猜中颜色”的不确定性最大。熵越高代表这个集合越“不纯”。条件熵 Entropy(S|A) 表示“在已知特征 A 的取值后集合 S 还剩下的不确定性”。信息增益就是两者的差值Gain(S, A) Entropy(S) - Entropy(S|A)通俗地说用了特征 A 去做分裂后分类结果的不确定性下降了多少。下降越多说明这个特征对分类越有用。这就是 ID3 的核心逻辑。1.2 ID3用信息增益做分裂的第一代算法ID3 是 Ross Quinlan 在 1986 年提出的它是决策树算法的开山之作。核心思路只有一条每次分裂时计算每个特征的信息增益选择信息增益最大的特征作为当前节点的分裂特征然后递归建树。ID3 的优点很突出原理简单、计算代价小、生成的规则容易理解。在数据量不大、特征维度不高的场景下它能产出一棵可解释性很强的树。但它的毛病同样明显。第一只能处理离散型特征连续型数值特征需要提前分箱分箱的粒度直接影响建模效果第二对缺失值完全没有处理机制第三也是最致命的——信息增益天然偏好取值多的特征。比如一个“用户 ID”列每个样本取值都不同ID3 算出来它的信息增益往往最大因为把数据切到极致后每个子集只剩一条样本熵直接降为 0。这样选出来的特征完全没有泛化能力树也会变得非常庞大直接过拟合。1.3 C4.5针对 ID3 缺点的补丁合集1993 年还是 Quinlan在 ID3 的基础上提出了 C4.5。与其说它是一个新算法不如说它是 ID3 的“完整补丁包”针对之前的所有痛点逐一打了补丁。针对信息增益的偏置问题改用信息增益率Gain Ratio。信息增益率等于信息增益除以特征的固有值Split Info固有值衡量的是特征本身取值的混乱程度特征取值越多固有值越大惩罚也越重。这样“用户 ID”这类高基数特征的信息增益率会被压得很低。增加连续特征的处理能力。C4.5 会把连续特征按取值排序然后遍历所有相邻的中位点作为候选分裂阈值选增益最大的那个阈值进行二分裂本质上是一种离散化操作。增加缺失值处理机制。当某个样本的特征值缺失时C4.5 不会直接丢弃而是把它分配到所有子节点中并按权重比例参与后续分裂和预测。增加剪枝环节。C4.5 采用的是悲观剪枝Pessimistic Error Pruning用训练误差加上一个惩罚项来评估子树替换的价值避免生成过于复杂的树结构。C4.5 的这些补丁让它在很长一段时间内都是工业界分类任务的首选算法之一。但它的缺点也在实践中被逐渐放大连续特征分裂时要反复排序和扫描阈值在特征多、样本量大的场景下训练效率非常低而且生成的树是多叉树一个特征被用过一次就不会再用这一点限制了它在某些场景下的表现。1.4 CART从分类树到回归树CARTClassification And Regression Tree是 Breiman 等人在 1984 年提出的它和 ID3、C4.5 有一个本质区别CART 生成的是一棵二叉树每次分裂只把当前节点切成两份而且同一特征可以反复出现在不同层级的节点中。这棵树既可以做分类输出类别也可以做回归输出连续值。CART 分裂时不再使用信息增益或信息增益率而是使用基尼系数Gini Index。基尼系数衡量的是从集合中随机抽取两个样本其类别不一致的概率。公式是Gini(S) 1 - Σ p_i²Gini 值越小集合纯度越高。对每个特征、每个候选阈值CART 会计算分裂后的加权基尼系数选出让纯度提升最大的切分方式。CART 的另一个重要改进是剪枝策略。它使用代价复杂度剪枝Cost-Complexity Pruning对每个子树同时考虑“错误率”和“叶子节点个数”两个因素通过调节惩罚系数 α 生成一个子树序列再用交叉验证或独立验证集选出最优子树。这个思路后来被 sklearn 完整继承也就是ccp_alpha参数。三种算法的区别可以用下面这张表快速对照维度ID3C4.5CART提出时间198619931984分裂指标信息增益信息增益率Gini 系数树结构多叉树多叉树二叉树连续特征不支持需提前离散化支持排序后找最优阈值支持排序后找最优阈值缺失值处理不支持支持加权分配支持代理分裂剪枝策略基本不剪枝悲观剪枝代价复杂度剪枝CCP回归支持否否是应用状态淘汰学习为主较少见学习为主主流sklearn 默认实现2. 关键公式与选择逻辑的深入拆解2.1 信息增益的偏置为什么“客户编号”会排在第一位前面提到 ID3 偏好取值多的特征这里我展开算一遍。假设样本集 S 有 1000 条数据标签是二分类正负各 500S 的熵是Entropy(S) -0.5·log₂(0.5) - 0.5·log₂(0.5) 1现在有两个候选特征。 特征 A“性别”有 2 个取值按取值切分后每个子集的类别分布仍然是 500 对 500那么条件熵为 1信息增益为 0毫无区分能力。 特征 B“客户编号”有 1000 个取值每个取值只对应一条样本。切分后每个子集只有一个样本了子集的熵全是 0。条件熵算出来是 0信息增益为 1。从公式上看“客户编号”完美地把不确定性降到了 0ID3 当然会选它。但这里犯了两个错误一是把每个子集只有一个样本的数据当成“纯净”这其实是“极端过拟合”不是真正的分类纯度二是这种分裂完全没有泛化能力一旦测试集出现新的客户编号树根本不知道往哪个分支走。信息增益率正是针对这个漏洞设计出来的。2.2 信息增益率的修正固有值如何压制高基数特征信息增益率的分母是特征 A 的固有值Split Info也就是特征 A 本身的熵。公式是SplitInfo(S, A) -Σ (|S_i| / |S|) · log₂(|S_i| / |S|)这个分母的含义可以这样理解如果特征 A 的取值非常多且均匀分布那么 SplitInfo 会非常大信息增益率就被压得很低。拿上面的“客户编号”举例每个取值只对应 1 条样本那么 SplitInfo -1000 × (1/1000) × log₂(1/1000) ≈ 9.97。信息增益率为 1 / 9.97 ≈ 0.1。而“性别”特征如果完全不区分标签增益本来就接近 0增益率也不高。这样一来高基数特征的“虚假优势”就被有效遏制了。C4.5 在实际运行时还会加一个启发式规则先从所有特征中挑出信息增益高于平均水平的候选集合然后再从中选信息增益率最高的特征。这样做是为了防止“信息增益率”反过来偏好取值极少的特征比如只有 1 个取值的特征SnakeInfo 接近 0增益率容易被极端放大。2.3 Gini 系数的数学直觉它和熵到底差多少Gini 系数公式为 Gini 1 - Σp_i²我经常用“随机错分概率”来解释它从集合中随机抽出两个样本有放回它们类别不一样的概率。概率越大集合越不纯。对比熵 -Σp_i·log₂(p_i)两者的曲线走势高度相似都是“类别越均匀取值越大”而且在二分类场景下熵的最大值是 1Gini 的最大值是 0.5单调趋势几乎一致。Gini 系数的优势主要是计算量小不需要算对数只用平方和减法。在大规模数据上这个计算差异在分裂点扫描时会放大得非常明显所以 sklearn 默认用 Gini 而不是熵。但需要注意Gini 系数对类别数更不敏感它不像熵那样会对“类别数爆炸”给予额外惩罚。在小样本、类别不均衡的场景下两者选出的分裂特征可能不同我后面在实战部分会展开讲。2.4 实际项目中用熵还是用 Gini不要过度纠结这是老生常谈的问题我的经验是在绝大多数二分类任务上选择“熵”和“Gini”得到的结果差异非常小树结构可能略微不同但精度往往在零点几个百分点内波动。没必要为了那点误差去刻意选指标。但在两个场景下还是要注意一是数据集非常小几百条样本时熵作为分裂指标通常能生成稍“平衡”一些的树解释性更好二是树被用作集成学习的基学习器时比如随机森林或 GBDT用 Gini 可以让单棵树的训练速度更快尤其在特征维度高的情况下收益明显。3. 剪枝策略的差异模型好不好全看这一步3.1 预剪枝和后剪枝的基本直觉决策树如果不加约束会一直分裂到每个叶子节点都是纯的为止这时候树对训练数据是“背答案”不是“找规律”。剪枝的目的是砍掉那些对泛化能力没有帮助的分支本质上是一种正则化手段。剪枝分两类。预剪枝是在建树过程中边分裂边判断如果当前节点的分裂不能让验证集准确率提升就停止分裂把当前节点变成叶子。后剪枝则是先完整生成一棵树然后自底向上考察每个内部节点判断如果把它替换成叶节点验证集误差是否会下降如果会就执行剪枝。预剪枝的优势是效率高缺点是“只看眼前”。有时当前分裂对验证集没有直接提升但它能解放下一层的强区分能力这种分裂会被预剪枝提前扼杀。后剪枝更稳妥但计算量更大尤其对大型树来说遍历成本不低。3.2 ID3、C4.5、CART 在剪枝上的区别ID3 原始版本几乎没有剪枝机制文献里更多依赖“设定最大深度”“叶子节点最小样本数”这类手工规则。这就导致 ID3 树普遍偏胖、偏深对噪声敏感。C4.5 采用悲观剪枝。它的做法是计算每个叶子节点的训练误差并加一个惩罚项 0.5等价于认为每个叶子节点至少会错半条样本然后比较剪枝前后误差的加权情况如果剪枝后的误差估计更小就执行剪枝。这个方法的优点是可以在不依赖独立验证集的情况下完成剪枝但对小样本来说惩罚项偏小剪枝可能不彻底。CART 的代价复杂度剪枝最有系统性。它定义一个目标函数Ra(T) R(T) α · |T|其中 R(T) 是子树在训练数据上的误分类率|T| 是叶子节点数量α 是平衡系数。α 越大树越简单。CART 的做法是让 α 从 0 逐步增大生成一组嵌套的候选子树然后通过交叉验证选出误差最小的那棵。这种方法比 C4.5 的启发式更严谨也是 sklearn 里面ccp_alpha参数的原理来源。3.3 实战中的剪枝建议在 sklearn 中最常用的剪枝手法不是ccp_alpha而是设置max_depth、min_samples_split、min_samples_leaf这几个参数。我的习惯是先把max_depth限制在 3 到 6 之间min_samples_leaf设为样本量的 1% 到 5%这种方法在实际项目里比用复杂剪枝算法更直观、更可控也符合决策树“偏置小、方差大”的特点。如果追求更高精度可以先把树建得足够深再用ccp_alpha剪枝。具体做法是用 sklearn 的cost_complexity_pruning_path得到不同 α 值下的树信息然后用验证集挑选最优 α。这一段我建议你写个小脚本去跑因为最优 α 和样本量、特征噪声水平强相关没有固定值。4. 动手试一下用 Python 复现 CART 和简化版 ID34.1 用 sklearn 构建一棵 CART 分类树sklearn 里的DecisionTreeClassifier用criteriongini时默认就是一棵以 Gini 作为分裂指标的 CART 分类树。下面我以一个示例数据集演示建树、可视化和验证from sklearn.datasets import load_iris from sklearn.tree import DecisionTreeClassifier, plot_tree from sklearn.model_selection import train_test_split data load_iris() X_train, X_test, y_train, y_test train_test_split( data.data, data.target, test_size0.3, random_state42 ) clf DecisionTreeClassifier( criteriongini, max_depth3, min_samples_leaf5, random_state42 ) clf.fit(X_train, y_train) print(训练集准确率:, clf.score(X_train, y_train)) print(测试集准确率:, clf.score(X_test, y_test)) import matplotlib.pyplot as plt plt.figure(figsize(12, 8)) plot_tree(clf, filledTrue, feature_namesdata.feature_names, class_namesdata.target_names) plt.show()运行结果通常显示训练准确率超过 96%测试准确率在 90% 左右。把max_depth3改成一个较大的值或者不设置训练准确率会迅速逼近 100%但测试准确率可能反而下降。这一现象就是“树越深方差越大”的最直观体现。4.2 核心参数逐个拆解criteriongini或entropy对应 CART 的两种分裂指标。默认gini追求可解释性时可以试entropy做对比。max_depth树的最大深度。限制深度是最直接的预剪枝手段。min_samples_split节点分裂所需的最小样本数。默认 2数据噪声大时我会调到 10 到 20。min_samples_leaf叶子节点最少样本数。它比min_samples_split更硬核因为它直接决定了叶子的大小。对不均衡数据特别重要建议设置得大一点。max_features每次分裂时考虑的特征数量。在集成学习中常用单棵树一般不调整。ccp_alpha代价复杂度剪枝系数。用于后剪枝需要配合路径分析使用。4.3 从零实现一个简化版 ID3理解信息增益的计算过程我们直接写一个只依赖 NumPy 的简化版 ID3。逻辑很简单把“计算熵、条件熵、信息增益、选最优特征”四步封装一下然后用递归生成树。import numpy as np from collections import Counter def entropy(y): _, counts np.unique(y, return_countsTrue) p counts / counts.sum() return -np.sum(p * np.log2(p)) def info_gain(X, y, feature): 按 feature 列取值切分返回信息增益 total_entropy entropy(y) values, counts np.unique(X[:, feature], return_countsTrue) weighted_entropy 0.0 for v, cnt in zip(values, counts): subset_y y[X[:, feature] v] weighted_entropy (cnt / len(y)) * entropy(subset_y) return total_entropy - weighted_entropy def build_id3_tree(X, y, features, depth0, max_depth3): # 如果所有样本属于同一类别返回叶节点 if len(np.unique(y)) 1: return {leaf: y[0], samples: len(y)} # 如果特征用完或达到最大深度返回多数类别 if len(features) 0 or depth max_depth: majority Counter(y).most_common(1)[0][0] return {leaf: majority, samples: len(y)} # 计算每个特征的信息增益选择最大的 gains [(info_gain(X, y, f), f) for f in features] _, best_feature max(gains) tree {feature: best_feature, children: {}, samples: len(y)} remaining_features [f for f in features if f ! best_feature] for v in np.unique(X[:, best_feature]): mask X[:, best_feature] v tree[children][v] build_id3_tree( X[mask], y[mask], remaining_features, depth 1, max_depth ) return tree这个实现虽然远没有成熟库稳健但足够让初学者看清 ID3 的运作逻辑每层选信息增益最大的特征然后按特征取值分出子节点。由于它是多叉树特征用过了就不再使用这正是 ID3 和 CART 最直观的区别。你可以把代码跑一遍对比一下手写的 ID3 树和 sklearn 的 CART 树在结构上的差异。4.4 从输出树的形状反推算法差异看树的结构是最直观的学习方式。ID3 是多叉树第一次分裂选出的特征如果取值很多树会快速变宽C4.5 同样是多叉树但因为信息增益率抑制了高基数特征第一层更可能选中语义上有区分度的特征CART 永远是二叉树而且同一特征可以在多个层级重复出现。你在 sklearn 里看到的plot_tree输出永远是 CART 结构这是很多初学者容易忽略的一点。5. 实战选型三个真实场景的算法选择思路5.1 医疗诊断、金融风控可解释性第一用带深度限制的 CART 或 C4.5医疗诊断和信贷风控这两个领域模型必须要能解释“为什么给这位患者打高风险标签”“为什么拒绝这笔贷款”。决策树天然适合这种场景因为它的分裂条件就是可以翻译成自然语言的规则。在这些场景里我不推荐直接用全量 CART而应该给 CART 加上较强的预剪枝限制比如max_depth4加上min_samples_leaf30左右。深层原因在于医疗数据和信贷数据通常存在较大的噪声树太深会捕捉到一些和标签无因果关系的特征模式后期很难向业务方解释。C4.5 在理论上也适合这类场景因为它产出的多叉树在某些业务规则上更“整齐”。但现实是 sklearn 没有提供 C4.5 的完整实现需要借助第三方库或者自己封装工程成本偏高。我的建议是在 Python 生态里直接用高约束 CART 配合feature_importances_输出特征排序已经能满足大多数业务解释需求。5.2 回归问题只能选 CART别无他选ID3 和 C4.5 本质上是分类算法无法输出连续值。如果你的目标是预测房价、销量、温度这类连续值决策树这边唯一的原生选择就是 CART 回归树。使用方式是把DecisionTreeClassifier换成DecisionTreeRegressor分裂指标自动变成 MSE均方误差from sklearn.tree import DecisionTreeRegressor reg DecisionTreeRegressor( criterionsquared_error, max_depth5, min_samples_leaf10, random_state42 ) reg.fit(X_train, y_train)CART 回归树的分裂逻辑是让子节点的预测值通常是均值与真实值之间的平方误差最小。它的解释性和分类树一样好但需要注意CART 回归树在边界外的预测能力基本为零因为它只能输出训练数据范围内的分段常数。做预测外推时不要用单棵 CART考虑用线性模型或 GBDT 做兜底。5.3 大规模数据、集成学习无脑上 CART随机森林、GBDT、XGBoost、LightGBM 这些集成学习算法基学习器清一色都是 CART少数实现支持 DART 变体。原因有三第一CART 是二叉的分裂规则简单适合反复叠加第二CART 用 Gini 或 MSE 作为分裂指标计算成本低能够在海量特征上快速做分裂点扫描第三CART 天然支持回归和分类这让它成为集成框架里最通用的“零件”。如果你正在准备面试或做算法选型答辩建议不要再说“我用的是 ID3 或 C4.5”除非你只是用它做教学演示。工业界真正的单棵决策树应用几乎都是 CART其余两种更多是学习价值和技术史价值。6. 常见问题与避坑指南6.1 连续特征的处理细节CART 处理连续特征的流程是先把样本按特征值排序然后在每两个相邻取值之间尝试一个阈值计算切分后的加权 Gini 或 MSE从中选最优阈值。这里有个细节min_samples_leaf决定阈值扫描时两侧的子节点最少要保留多少样本如果设置得太小阈值可能选在异常点附近导致分裂不稳定。另外连续特征在树的层级间可以被重复使用。比如某个特征在根节点以阈值为 3.5 分裂在下一层可能又以 2.1 作为阈值分裂。这不是 bug是 CART 的设计特性它让树能逐步逼近非线性边界。6.2 类别型特征编码时的坑如果直接对类别特征做 LabelEncoder相当于给类别强加了顺序关系CART 可能学出“类别 A 比类别 B 更接近类别 C”这种无意义的关系。正确做法是使用 OneHotEncoder 或者 OrdinalEncoder 配合适当的预处理。不过 OneHot 之后特征维度膨胀树的训练速度会下降解释性也会变差建议先对高基数类别做频次编码。6.3 特征相关性高时树的结果不稳定决策树对特征之间的相关性非常敏感。如果两个特征高度相关树可能随机选择一个进行分裂导致同一份数据多次运行得到不同的树结构。这不是模型精度问题而是可解释性层面的不稳定。解决方案是结合特征重要性排序和业务理解只保留一组强相关特征中的一个。6.4 决策树与随机森林、GBDT 的关系单棵决策树是一个低偏差高方差的模型随机森林通过投票和样本抽样降低了方差GBDT 通过加法模型和梯度下降降低了偏差。所以在实际项目里单棵决策树通常作为基线模型或快速可解释性方案而追求精度时优先考虑集成模型。这也解释了为什么Dog决策树常被当成“toy model”而随机森林和 GBDT 才是工业界的常客。整体看下来ID3、C4.5、CART 三代算法之间的关系像是一个开源项目从粗糙到成熟的过程。我建议大家学习路线是先用代码把 ID3 手写一遍理解信息熵和递归分裂的本质再研究一遍 C4.5 对连续特征和增益偏置的补丁最后深入掌握 CART因为它是你真正在 sklearn 里点击fit时实际调用的算法。技术迭代的速度很快但这些底层的选择逻辑——为什么用这种分裂指标、为什么要剪枝、为什么处理缺失值——无论多少年都不会过时。

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

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

免费获取报价 →
↑