资讯动态

决策树分类从ID3到CART:算法原理、Python实现与调参

发布时间:2026/10/1 11:07:31 来源:尧图企业网站定制
简介这份决策树分类资源包围绕机器学习中ID3、C4.5、CART三种经典算法面向需要理解分类模型原理、动手复现算法或准备课程实验的初学者与开发者。资源共31个文件压缩包约1.36MB以Python脚本和实验报告为主体配合数据集文本与表格、13张png和10张jpg结果对比图、dot树结构可视化描述文件覆盖数据准备、算法实现、结果可视化的完整流程目录结构清晰便于按模块查阅。已有3572人学习浏览适合作为课堂配套或自学入门材料。内容明确区分三种算法的划分标准ID3选择信息增益最大C4.5先筛出信息增益高于平均水平的属性再取增益率最高CART则按基尼指数最小划分同时附有分类效果图、实验记录与可复用脚本可帮助读者快速对照差异、理解特征划分过程并在此基础上调整参数、复用代码开展自己的分类任务。1. 决策树分类不是老古董表格数据里它依然是最快见效的模型做信贷审批的同事问过我一个问题现在深度学习这么热为什么银行那套风控名单里还留着决策树我说你去看招聘笔试的机器学习八股ID3、C4.5、CART三个名字就是决策树分类的半部历史你再去翻周志华的《机器学习》第四章整章都在讲这一套。决策树把“收入、年龄、职业”切成一棵能读的树每个叶子给出结论于是银行敢拿它解释“为什么拒绝你的贷款”。下面从ID3的信息增益讲到CART的基尼指数把特征怎么选、剪枝怎么做、参数怎么调说透。刚接触机器学习的人能跟着代码跑通第一棵决策树正在做表格数据分类的人也能找到调参的边界。文章里所有代码都用Python写鸢尾花数据集可以直接跑玩具级别的数据也足够你把ID3的手写实现看明白。2. 从ID3到CART三个算法选型的核心差异与计算逻辑先交代一下这个领域的坐标系ID3、C4.5、CART都叫决策树分类但选择切分特征的逻辑完全不同。ID3选信息增益最大的特征C4.5改成信息增益率CART用基尼指数并且严格二叉。名字记不住没关系记住一条主线就够了这三兄弟解决的是同一个问题在每一个节点上选哪个特征、按什么阈值切能让下面两组样本“更纯”。2.1 信息熵与信息增益ID3为什么天然偏好取值多的特征讲ID3躲不开信息熵这个概念。一个集合里如果只有一类样本熵是0两类各占一半熵是1类别越乱熵越大。熵的公式不复杂对每个类别算概率p对p求-log2(p)再按p加权求和。你不需要手算很多次写代码时一个函数就搞定了但理解它才能理解后面所有变种。ID3的做法是假设当前节点有100个样本先算一次熵这叫分裂前的熵。然后拿来一个特征比如“学历”按它的每个取值把样本切开切完之后每一份样本再各自算熵按每份的样本占比加权求和得到分裂后的熵。两者相减就是信息增益。增益越大说明这个特征把“乱”降得越狠就越应该先拿它切。这样设计的缺陷在真实数据里立刻暴露如果数据里有一列是身份证号几乎每个取值只对应一个人切完每个小组都“纯”得不行信息增益直接拉满。ID3会无脑选它当根节点产出一棵几百层深、毫无泛化能力的树。这就是被反复问到的“ID3的多值偏好”也是C4.5出场的原因。2.2 信息增益率C4.5修正多值偏好的代价与边界C4.5的修正是给信息增益加了一个分母这个特征本身的熵术语叫分裂信息。特征取值越杂分母越大最后算出来的增益率被压得越低。身份证号这种特征信息增益再大除以一个巨大的分母之后也排不上号。但一个问题被按下去另一个问题会浮上来增益率会反过来偏好取值少的特征。比如性别只有两个值很容易拿到很高的增益率。C4.5的实际做法是在每一层先筛出信息增益比平均水平高的特征再从这些特征里挑增益率最高的把两个偏好互相压住。这个“先筛增益再选比率”的两步选择是机器学习期末复习和算法面试里最爱挖细节的地方。除了特征选择C4.5还顺手解决了两件事连续特征不再只能按离散值切而是先排序在相邻值的中点里找最优阈值缺失值则可以按样本比例把缺失样本分配到各个分支。这两点直接影响了后来CART和sklearn的实现所以别因为C4.5工程少见就跳过它。2.3 基尼指数与二叉树CART成为工业默认的三个原因CART把切分准则换成了基尼指数。基尼值的算法比熵更省事1减去每个类别概率的平方和。两类各一半时基尼值是0.5全是一类时基尼值是0。它不用算对数扫描分裂点的速度更快对大规模表格数据是实打实的收益。更关键的是CART要求每个节点只分两支这让树的实现和剪枝都简单了一大截。sklearn里的DecisionTreeClassifier、随机森林、XGBoost、LightGBM底层树模型基本都是CART的二分结构。随机森林和决策树的区别也在这里随机森林是一堆CART树加上特征随机采样再投票出结果不是重新发明了一种树。选型的判断其实很直接我一般这样定课程作业要复现经典论文、应付理论考试按ID3、C4.5、CART逐一实现注意它们分裂准则的差异实际项目要快速拿到可解释的分类结果直接用sklearn的CART也就是criteriongini数据里连续特征多、缺失值比例高优先C4.5的思路做预处理再回到CART训练。下面给一个三者的速查表方便你在动手前先锁一个目标算法分裂准则树结构连续特征缺失值工程常见程度ID3信息增益多叉不支持不支持教学为主C4.5信息增益率多叉排序找阈值按比例分配教学与学术复现CART基尼指数严格二叉排序找阈值需自行填充sklearn、RF、GBDT默认3. 用Python实现决策树分类器手动ID3与sklearn版的最小可运行代码原理再熟不动手跑一遍等于没学。这一章先手写一个ID3实现让你看清递归建树的全过程再用sklearn的DecisionTreeClassifier做一条能直接用的CART流水线。两个版本都能在Python里直接跑通。3.1 从零写ID3信息增益计算与递归建树的二十行核心教学实现默认特征都是离散取值y是0、1、2这样的整数标签。完整代码不长核心就三部分算熵、找最佳特征、按特征值递归切数据。import numpy as np def entropy(y): counts np.bincount(y) # 统计每个类别的样本数 probs counts[counts 0] / len(y) # 去掉没出现的类别转概率 return -np.sum(probs * np.log2(probs)) def best_feature(X, y, features): base entropy(y) best_gain, best_f -1.0, None for f in features: values np.unique(X[:, f]) cond 0.0 for v in values: idx X[:, f] v cond len(y[idx]) / len(y) * entropy(y[idx]) gain base - cond # 信息增益 分裂前熵 - 加权分裂后熵 if gain best_gain: best_gain, best_f gain, f return best_f, best_gain def build_tree(X, y, features): if len(np.unique(y)) 1: return int(y[0]) # 叶子节点所有样本同一类 if len(features) 0: return int(np.bincount(y).argmax()) # 特征用完投票决定 f, gain best_feature(X, y, features) if gain 1e-9: return int(np.bincount(y).argmax()) # 切不动了投票决定 tree {f: {}} rest [x for x in features if x ! f] for v in np.unique(X[:, f]): idx X[:, f] v tree[f][v] build_tree(X[idx], y[idx], rest) return tree这段代码里的entropy用np.bincount统计频次比用Counter循环快但要求y是非负整数。如果你的标签是字符串先做一次np.unique(y, return_inverseTrue)映射再传进来。best_feature里对每个特征算加权条件熵条件熵越小、信息增益越大就选它做当前节点。build_tree是标准的递归终止逻辑样本全同归类、特征用尽、增益太小都是停下来的条件。用一个玩具数据验证一下X np.array([ [0, 1, 0], [0, 1, 1], [1, 0, 1], [1, 1, 0], [0, 0, 0], ]) y np.array([0, 1, 1, 1, 0]) print(build_tree(X, y, [0, 1, 2]))这里三列分别代表“学历是否本科”“是否已婚”“收入是否大于1万”。从标签分布看第三列和标签的对应最干净大概率成为根节点你可以把某个样本改一改观察树结构怎么跟着变。输出是一棵嵌套字典键是特征序号值是“特征取值到子树”的映射顺着字典能逐步还原整棵树的生长过程。3.2 sklearn版决策树分类器鸢尾花实战与参数设置手写版适合理解原理真正干活时用sklearn就够了。以经典的鸢尾花分类为例用train_test_split划分数据集再建一棵深度为3的CART树。这个模板在很多机器学习的入门课和实验平台上都能见到是可以直接抄走的代码。from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier X, y load_iris(return_X_yTrue) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) clf DecisionTreeClassifier( criteriongini, max_depth3, min_samples_leaf4, random_state42, ) clf.fit(X_train, y_train) print(train acc:, round(clf.score(X_train, y_train), 3)) print(test acc:, round(clf.score(X_test, y_test), 3))criteriongini就是CART想模拟ID3的信息增益就把这个参数改成entropy注意那并不是C4.5的增益率别在面试里说混。max_depth3限制树最多长三层min_samples_leaf4要求每个叶子至少留4个样本这两个参数组合起来先让树不会太碎。我跑这段代码测试集准确率一般在0.95左右每一次略不同量级接近即可不用纠结复现完全一样的数字。这里有个细节sklearn默认的splitter是best数据量大时改成random能明显提速代价是精度略降。决策树对特征尺度不敏感鸢尾花这种量纲接近的数据可以直接喂真正的问题出在不同特征量纲差很多的时候我在第5章会展开。3.3 C4.5没有sklearn接口时工程上怎么补很多初学者翻遍sklearn文档也找不到criteriongain_ratio因为sklearn的DecisionTreeClassifier压根没实现C4.5。严格说sklearn里的entropy是ID3式信息增益CART式基尼是默认C4.5的增益率只能自己写。作业和期末里最常见的是自己实现增益率的计算。我一般建议在3.1代码的基础上加一个函数把信息增益除以分裂信息def split_info(X, y, f): values np.unique(X[:, f]) total 0.0 for v in values: p np.mean(X[:, f] v) if p 0: total - p * np.log2(p) return total def gain_ratio(base_entropy, cond_entropy, si): return (base_entropy - cond_entropy) / (si if si 0 else 1e-9)算出来增益率之后再按“先筛高于平均增益再选最大比率”的规则挑特征。工程上我的建议是别在sklearn里硬改分裂准则因为C4.5还涉及多叉树、缺失值分配、后剪枝重写自己维护一套的成本很高。直接先用CART用GridSearchCV调参逼近效果多数表格数据场景下两者差距并没有大到值得折腾。4. 剪枝与调参让决策树逼近真实曲线而不是背下训练集决策树本质上是分段常数函数每片叶子对应特征空间里一个小矩形矩形里所有样本给同一个预测值。树越深矩形切得越细当然能更细腻地逼近真实曲线但也会把噪声一起学进去。剪枝就是在“拟合得细”和“泛化得住”之间找平衡点。4.1 预剪枝参数怎么设max_depth、min_samples_split、min_samples_leaf预剪枝是在建树过程中提前设限sklearn里最常用的是三个参数。max_depth控制树的层数我见过不少项目把它设到3到7之间就够用min_samples_split控制内部节点至少要有多少样本才允许继续切太小的值等于没限制min_samples_leaf控制叶子至少有多少样本这是最有效的防过拟合旋钮通常从5开始试。让我用一个直观的实验说明参数怎么配合。还是鸢尾花那套数据只改max_depth观察训练分数和测试分数for depth in [1, 2, 3, 4, 5, 6, 8, 10, None]: model DecisionTreeClassifier(max_depthdepth, random_state0) model.fit(X_train, y_train) print( fdepth{str(depth):5}, train, round(model.score(X_train, y_train), 3), test, round(model.score(X_test, y_test), 3), )跑完你大概率会看到训练分数单调上升测试分数先升后降。这就是过拟合曲线的样子。选参数时不要选测试分数最高的那个深度选“测试分数进入平台期的最小深度”。比如深度3和深度4分数一样就选深度3少一层就是少一份方差。4.2 后剪枝成本复杂度剪枝与ccp_alpha的选值方法预剪枝是“边建边砍”后剪枝是“先把树长满再回过头来剪”。sklearn实现的是成本复杂度剪枝对应参数ccp_alpha。它的思想是给代价函数加惩罚总代价等于训练误差加alpha乘以叶子数。alpha越大叶子多的树越吃亏模型就会自动砍掉对误差贡献小的分支。用下面这段代码可以画出alpha的选择路径model DecisionTreeClassifier(random_state0) path model.cost_complexity_pruning_path(X_train, y_train) ccp_alphas, impurities path.ccp_alphas, path.impurities for alpha in ccp_alphas: pruned DecisionTreeClassifier(random_state0, ccp_alphaalpha) pruned.fit(X_train, y_train) print( falpha{alpha:.4f}, train, round(pruned.score(X_train, y_train), 3), test, round(pruned.score(X_test, y_test), 3), )运行结果里alpha太小树还是长满alpha太大树直接退化成根节点。常见做法是先用粗网格筛一个范围再在小范围里细看。alpha通常落在1e-4到1e-1之间数据量小就偏小。判断标准仍然是用验证集或交叉验证而不是训练集分数我一般取“测试分数开始下滑之前”的那个alpha宁小勿大。4.3 连续值与缺失值处理把C4.5的处理套路搬进CART实践C4.5处理连续特征的方法是先按特征值排序在相邻值的中间点里找最优阈值这个思路被CART继承下来sklearn底层就是这么干的。所以不用自己手工给连续特征分箱除非你有强业务理由直接把原始数值喂进去让树自己找阈值。缺失值处理上别把缺失完全甩给模型先看占比占比低于1%直接删行占比在1%到5%之间用中位数或众数填充占比超过5%类别特征把缺失单独变成一个类别连续特征则填充中位数再加一列“是否缺失”的标记。填充有个原则必须守住填充器只能在训练集上fit再用训练好的填充器transform测试集。很多人在测试集上重新fit导致验证结果虚高上线直接现原形。用sklearn的SimpleImputer可以干净地做到from sklearn.impute import SimpleImputer imp SimpleImputer(strategymedian) X_train imp.fit_transform(X_train) X_test imp.transform(X_test) # 只用transform不重新fit写完预处理再训练树模型会稳定很多。这一节要记的其实就一句话连续特征交给树自己二分缺失特征按占比分层处理填充边界守死训练和测试两套数据。5. 决策树实战的5个高频翻车点现象、原因与后悔药5.1 类别不平衡时准确率虚高正经业务一测就现形现象做一个“年收入是否超过5万”的二分类样本里低收入占85%模型不学任何规律、全猜“低收入”准确率都有85%。拿带class_weight和不带的模型对比准确率数字差不多但少数类几乎一个都抓不到。原因DecisionTreeClassifier默认优化的是整体准确率少数类样本少对损失的贡献也小树自然偏向多数类。解决给少数类加权重sklearn里class_weightbalanced按类别频率反比调权少数类权重自动变大。同时别只看准确率打印混淆矩阵看少数类的召回率和精确率from sklearn.metrics import confusion_matrix clf DecisionTreeClassifier(max_depth5, class_weightbalanced, random_state0) clf.fit(X_train, y_train) print(confusion_matrix(y_test, clf.predict(X_test)))提示加了class_weight后准确率往往略降这是正常的业务指标要从“抓得准”切到“想抓的人抓不抓得到”。5.2 特征量纲差距大树被单个大数值特征带偏现象假设你在做收入预测age范围20到70capital-gain范围0到99999。树的前两层几乎都被capital-gain占领age要到很深才出现。原因CART在连续特征上找分裂点时本质上是遍历所有可能的阈值取值范围更大的特征能提供更多候选阈值信息增益容易虚高。这跟ID3偏好取值多的离散特征是同一个毛病。解决先把明显“虚胖”的特征做业务化处理比如收入类特征做分箱或取对数。决策树不怕量纲怕的是某个特征“取值花样太多”。可以用pd.cut把数值列先归档import pandas as pd df[capital_gain_bin] pd.cut( df[capital_gain], bins[-1, 0, 1000, 5000, 100000], labelsFalse, )注意标准化对树模型不是必须的真正要做的是压制高基数连续特征的“竞选优势”。5.3 不剪枝的树在训练集上100%测试集立刻崩现象max_depth不设min_samples_leaf1训练集准确率100%一到验证集掉到0.8几画出预测曲线全是锯齿单个样本就能改变一片区域。原因树的叶子越多分段常数函数越“碎”边界跟着训练集的噪声走泛化能力下降。解决先用min_samples_leaf压制碎叶子再设max_depth兜底最后用第4.2节的ccp_alpha后剪枝收尾。如果还是过拟合就要怀疑特征里混了太多无用列而不是继续调深。5.4 类别特征用数字编码后切分位置完全没业务含义现象学历列被人为编码成0、1、2树里出现“学历小于1.5”这种分裂把本科和研究生归到一边。对业务方解释时根本讲不通因为0、1、2之间的距离本身没有含义。原因LabelEncoder给类别强加了一个不存在的顺序决策树的数值比较又把这种顺序当成真。类别标签只有“是/否”二元时还好取值超过两个就容易翻车。解决无序类别用one-hot编码sklearn的OneHotEncoder或pandas的get_dummies都行只有评分、等级这类真正的定序变量才保留整数编码from sklearn.preprocessing import OneHotEncoder encoder OneHotEncoder(handle_unknownignore, sparse_outputFalse) X_cat encoder.fit_transform(df[[education]])fit之后再拿同一个encoder去transform测试集和填充器的道理一样别在测试集上重新fit。5.5 random_state不固定同一份数据两次结果不一致现象同事用同一份代码跑出两个准确率调参时又发现参数没变但结果飘于是开始怀疑“玄学”。原因sklearn的树模型默认random_stateNone分裂点搜索和样本顺序里存在随机性train_test_split不固定种子数据划分每次都不同。这不是模型坏了是随机种子没上锁。解决把随机种子当成工程契约写进代码训练、切分、网格搜索全用同一个SEEDSEED 42 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_stateSEED ) clf DecisionTreeClassifier(random_stateSEED, **best_params)提示固定random_state不是为了让分数更好是为了让同事能复现你的实验也让网格搜索结果可比较。6. 验证树好不好特征重要性读法与树结构打印的实操套路6.1 特征重要性的两种读法别拿feature_importances_当圣旨sklearn给每棵树都算好了feature_importances_它统计的是每个特征对基尼下降的贡献数值越大代表越重要。但这份排名有个毛病高基数连续特征容易虚高和5.2节是同一个隐患的延伸。所以我会再用排列重要性对照一次把测试集某一列随机打乱看分数掉了多少掉得越多说明这列越不能缺。from sklearn.inspection import permutation_importance result permutation_importance(clf, X_test, y_test, n_repeats10, random_state42) for name, score in zip(feature_names, result.importances_mean): print(name, round(score, 4))两份排名对不上的时候我一般偏信排列重要性因为它直接衡量的是“打乱之后模型还能不能干活”。但最后拍板永远靠业务特征能不能被解释、采集成本高不高比排名第几更重要。这也是决策树在风控、医疗这些领域至今没被黑箱模型完全替代的原因。6.2 用export_text把树结构打出来验证分裂逻辑训练完的决策树如果不打开看和黑匣子没什么区别。sklearn的export_text能把树结构打印成缩进的文本特征名、阈值、叶子类别全都在里面。这是决策树分类器区别于深度学习模型最值钱的地方。from sklearn.tree import export_text print(export_text(clf, feature_namesfeature_names, decimals2))拿打印结果和业务对一遍根节点的特征和阈值如果根节点选了“贷款金额超过50万”而业务常识里这个阈值应该是30万那说明数据或者标签出了问题别急着上线。我自己的习惯是每次模型训练完必做两件事打印一次树结构对一遍业务口径再跑一次固定种子的交叉验证确认参数不是撞出来的。这两件事做完模型才敢交出去。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑