ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

决策树原理与sklearn实战:从熵、剪枝到随机森林

决策树原理与sklearn实战:从熵、剪枝到随机森林 决策树这个模型说起来很有意思。好像每一个学机器学习的人接触的第一个分类模型基本都是它教科书把它放在最前面各种入门教程也用它开场。但真正把它用明白、用出价值的人其实不多。很多人学完只会调sklearn的DecisionTreeClassifier跑通一个鸢尾花案例就觉得完事了换个数据集立马不会分析更别说解释清楚这棵树为什么这么长。这篇文章我想把决策树从原理到实战再到调参和进阶一次性说透。不管你是刚入门想搞懂熵和信息增益的学生还是已经在用sklearn做数据挖掘、但总被过拟合坑到的开发都能从里面找到能直接拿去用的东西。内容会覆盖决策树如何逐步逼近真实数据曲线、CART 的分裂机制、sklearn里的完整代码实操、以及决策树和随机森林的本质区别。文章偏长但读完你应该能独立完成一棵树的训练、评估、可视化和调优。1. 为什么我们还在讨论决策树一份嵌套 if-else 的自我修养很多人觉得决策树太简单了不就是一堆 if-else 嵌套吗这话对了一半。决策树确实是 if-else 的堆叠但它之所以能在深度学习时代依然占有一席之地靠的是这套简单规则背后的可解释性和对数据分布的强大逼近能力。1.1 从业务场景理解树的本质想象一个实际场景你在银行做客户流失预警业务同事跑过来问你到底什么样的人会流失你给他扔一个神经网络准确率 92%他只会一脸茫然。但如果你给他一棵决策树根节点写着最近 30 天登录次数 ≤ 3左边分支是历史购买次数 ≥ 5他立刻就能看懂哦高价值但近期不活跃的老客户需要重点召回。这就是决策树不可替代的价值。它把你做预测的规则显式地暴露给使用者而不是藏在几千维的矩阵里。对金融风控、医疗诊断、运营分析这类强监管、强解释诉求的领域这个特性比模型精度还要重要。我做过不少实际项目最终交付给业务方的模型不一定是精度最高的那个但往往决策树或者基于树的集成模型因为它讲得清楚。1.2 一棵树到底如何逼近真实曲线热词里有一条决策树如何逼近真实曲线这个问题问得非常好。要理解它得先接受一个前提决策树本质上是一个分段常数函数。它对特征空间做的切分是平行于坐标轴的矩形区域每个叶子节点对应一个区域区域里的样本预测值全部取相同的结果。所以一棵树的预测曲面长得像乐高积木拼出来的台阶而不是神经网络那种光滑的流形。举个例子数据真实分布是一个正弦曲线你拿一棵深度为 2 的决策树去拟合它最多只能把特征轴切成 4 段每段取一个平均值那拟合出来的就是四段水平的折线和真实曲线差距很大。但当你把深度加到 10切成 1024 段这段折线就已经非常贴近正弦波了。这就是用分段常数去逼近真实曲线的直观过程。这里要注意树的逼近能力和神经网络的逼近有本质区别。神经网络用连续变换去拟合决策树用离散切分去逼近。这意味着决策树更擅长处理有明显阈值效应的数据比如年龄超过 60 岁是个关键拐点这种场景而面对平滑的线性趋势它需要消耗大量深度去模拟效率不高。这也是为什么在部分数据集上树模型的表现会输给线性模型或神经网络。1.3 决策树的完整组成部分一棵树可以拆成三块看根节点第一次分裂的位置决定了整棵树最核心的划分依据内部节点中间的分裂规则每个节点包含一个特征和一个阈值叶子节点最终输出的预测结果。分类问题里是类别或概率回归问题里是数值从数据角度看训练的整个过程其实就是从根节点出发每一次都找一个特征和阈值把当前节点的样本分成左右两堆让分完之后的两堆纯度最高。然后对子节点重复这个过程直到满足停止条件。这个找最优切分的过程是整棵树的灵魂。2. 拆分规则的底层逻辑熵、信息增益与 Gini 系数的直觉与计算上一节说的是为什么要切分这一节聊聊怎么切——也就是分裂时的评分标准。sklearn里criterion参数就控制这个默认是gini也可以改成entropy。理解它们的数学含义你才能在不同场景下做出正确选择而不是跟着默认参数走。2.1 信息熵衡量一个集合有多乱熵这个概念来自信息论香农老爷子给的公式是[ H(S) -\sum_{i1}^{c} p_i \log_2(p_i) ]其中 (p_i) 是第 (i) 个类别在集合里占的比例(c) 是类别总数。熵越大集合越混乱熵等于 0说明这个集合只有一个类别纯得不能再纯。我给你算一个具体例子。假设有 14 个样本8 个正类、6 个负类那这个集合的熵就是[ H -\frac{8}{14}\log_2\frac{8}{14} - \frac{6}{14}\log_2\frac{6}{14} ]把分数换成小数算一下第一项(-0.571 \times (-0.807) 0.461)第二项(-0.429 \times (-1.222) 0.524)加起来(H 0.985)。这个值大概说明了集合接近一半一半不确定性最大。2.2 信息增益切分前后熵降了多少决策树选择特征看的是哪个特征能让切分后的加权熵最小——等价于让信息增益最大。信息增益就是[ \text{Gain} H(\text{父}) - \sum_{j} \frac{|S_j|}{|S|} H(S_j) ]还用上面那 14 个样本的例子。假设候选特征是是否有编程基础把样本分成两组有基础组 8 人6 正 2 负无基础组 6 人2 正 4 负。先算有基础组8 人6 正 2 负的熵[ H_{\text{有}} -\frac{6}{8}\log_2\frac{6}{8} - \frac{2}{8}\log_2\frac{2}{8} -(0.75 \times -0.415) - (0.25 \times -2) 0.311 0.5 0.811 ]无基础组6 人2 正 4 负的熵[ H_{\text{无}} -\frac{2}{6}\log_2\frac{2}{6} - \frac{4}{6}\log_2\frac{4}{6} -(0.333 \times -1.585) - (0.667 \times -0.415) 0.528 0.277 0.805 ]加权总熵[ H_{\text{加权}} \frac{8}{14} \times 0.811 \frac{6}{14} \times 0.805 0.463 0.345 0.808 ]信息增益[ 0.985 - 0.808 0.177 ]假设另一个候选特征最近 7 天是否登录过算出来的信息增益只有 0.02那算法显然会优先选是否有编程基础做根节点的分裂特征。从直觉上说这个特征一次切分就让数据的混乱程度显著下降了。2.3 Gini 系数让计算更简单一点Gini 系数的公式比熵简单[ G 1 - \sum_{i1}^{c} p_i^2 ]还是 14 个样本、8 正 6 负那个集合[ G 1 - \left(\frac{8}{14}\right)^2 - \left(\frac{6}{14}\right)^2 1 - 0.3265 - 0.1837 0.4898 ]分类是否切分干净看 Gini 是否下降。sklearn默认用 Gini一个重要原因是它不含对数运算计算更快。在大规模数据上这个速度差异会被放大所以 CART 算法的标准实现基本都用 Gini。但两者在大多数数据集上的表现差异很小你不用太纠结选哪个。指标计算复杂度对类别分布变化的敏感度取值范围sklearn 对应取值信息熵高含对数更敏感[0, 1]entropyGini 系数低纯乘方较钝感[0, 0.5]gini2.4 连续特征怎么切分现实里的特征很少是有/无这种二值变量更多是年龄 25、收入 8000这种连续值。决策树处理连续特征的办法是把所有样本在该特征上的取值排序然后依次尝试相邻两个值的中点作为切分点。比如年龄有 18、25、34、40、52 这几个值就尝试切在 21.5、29.5、37、46算哪个切分点信息增益最大就用哪个。这个过程的计算量不小所以如果数据集特别大且特征很多决策树的训练会比较慢。这也是后来随机森林通过每次只随机挑选一部分特征来降低计算压力的原因之一后面第五章会详细讲。3. 手写两份可运行的 sklearn 案例鸢尾花分类与收入预测原理讲再多不跑代码都是空的。这一节给两个完整案例一个是最经典的鸢尾花分类一个是偏实际业务的收入预测覆盖了热词里提到的两个sklearn实战场景。两份代码都可以直接复制运行我尽量在注释里写清每个关键参数选择的理由。3.1 案例一鸢尾花分类理解树的构建与可视化鸢尾花数据集只有 150 条样本、4 个特征、3 个类别非常适合做第一个实验。代码如下from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, classification_report # 加载数据 iris load_iris() X, y iris.data, iris.target # 切分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 构建决策树 # - criterionentropy用信息增益选择特征方便对照理解 # - max_depth3先限制深度观察结构防止直接过拟合 # - random_state42固定随机性保证结果可复现 clf DecisionTreeClassifier( criterionentropy, max_depth3, random_state42 ) clf.fit(X_train, y_train) # 预测与评估 y_pred clf.predict(X_test) print(f准确率: {accuracy_score(y_test, y_pred):.4f}) print(classification_report(y_test, y_pred, target_namesiris.target_names))跑完之后你大概会看到准确率在 0.9 以上。关键要看的是树的结构。这里推荐两种可视化方式。第一种是纯文本输出最省事from sklearn.tree import export_text text_tree export_text(clf, feature_namesiris.feature_names) print(text_tree)输出大概长这样每行就是一个节点的分裂规则|--- petal width (cm) 0.80 | |--- class: 0 |--- petal width (cm) 0.80 | |--- petal width (cm) 1.75 | | |--- petal length (cm) 4.95 | | | |--- class: 1 | | |--- petal length (cm) 4.95 | | | |--- class: 2第二种是画成真正的树形图import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize(14, 8)) plot_tree( clf, feature_namesiris.feature_names, class_namesiris.target_names, filledTrue, # 节点按类别填充颜色 roundedTrue, # 圆角矩形更好看 fontsize12 ) plt.show()我强烈建议你把max_depth从 3 依次调成 5、8、None分别打印树的结构。你会直观地看到深度加深时节点越来越多训练集精度不断上升但测试集精度先升后降——过拟合的出现过程就这么清晰地展现在眼前。3.2 案例二收入预测处理偏真实业务的数据热词里有头歌决策树进行收入预测-sklearn版我估计你是在做教学平台的练习。为了让你能完全复现我用模拟数据来演示数据生成方式如下5 个特征年龄、受教育年限、周工作时长、职业等级、地区编号标签是年收入是否超过 5 万。注意这是演示用的模拟数据和 UCI Adult 真实收入数据不一样但代码处理流程完全可迁移。from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, precision_score, recall_score from sklearn.pipeline import Pipeline # 生成模拟数据2000个样本5个特征2分类 X, y make_classification( n_samples2000, n_features5, n_informative4, # 4个特征携带有效信息 n_redundant0, n_classes2, weights[0.7, 0.3], # 模拟类别不平衡70% 低于5万30% 高于5万 random_state42 ) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 用Pipeline封装树模型方便后续替换其他算法 model Pipeline(steps[ (tree, DecisionTreeClassifier( criteriongini, max_depth5, min_samples_leaf5, class_weightbalanced, # 处理不平衡自动调整类别权重 random_state42 )) ]) model.fit(X_train, y_train) y_pred model.predict(X_test) print(f准确率: {accuracy_score(y_test, y_pred):.4f}) print(f精确率: {precision_score(y_test, y_pred):.4f}) print(f召回率: {recall_score(y_test, y_pred):.4f})这里我要特别强调几个坑第一收入划分是个不平衡问题多数人收入低于某个阈值。如果你只看accuracy_score哪怕模型把所有人都预测成低于 5 万准确率也有 70%看起来不低但毫无意义。所以要同时看精确率和召回率。class_weightbalanced会帮你把少数类的权重调上来是处理这类问题的第一选择。第二树的训练不需要特征归一化。决策树的分裂逻辑是找阈值比大小每个特征单独处理不受量纲影响。这和 SVM、逻辑回归完全不同。所以你没必要对收入、年龄做标准化做了也白做还可能会让节点可视化变得不直观。第三用random_state固定结果。树的生成有一定随机性尤其涉及特征采样时。不固定随机种子你每次跑出来的树都不一样提交作业或复现实验时会非常痛苦。永远记住设置random_state42这不是玄学是工程习惯。3.3 把训练好的树导出成业务可用的规则这是我觉得决策树最值钱的能力直接导出规则交付给业务团队落地。用export_text生成的规则文本稍作整理就是一条条可执行的风控规则。你也可以用graphviz把树导出为图片放进 PPT 或文档里做汇报。Graphviz 的配置用起来会有点绕给你一个稳定的操作流程from sklearn.tree import export_graphviz import graphviz dot_data export_graphviz( clf, out_fileNone, feature_namesiris.feature_names, class_namesiris.target_names, filledTrue, roundedTrue, special_charactersTrue ) graph graphviz.Source(dot_data) graph.render(decision_tree) # 生成 decision_tree.pdf如果你在 MATLAB 编程环境里跑平台会提示你安装 Graphviz 本体和 Python 包两者缺一不可。装完之后如果还报错找不到dot命令多半是系统 PATH 没配置好把 Graphviz 的bin目录手动加进去就行。这个坑我在多个机器上都踩过给你提前打个预防针。4. 剪枝与过拟合真实项目中决策树最容易翻车的三个细节决策树有个很要命的特性它有能力记住每一个训练样本。只要不加限制树会一直分裂直到每个叶子节点只剩一个样本训练集精度直接拉满 100%但测试集一塌糊涂。这是树模型最经典的过拟合问题。下面三个坑是我在项目里实际遇到的每一个都对应一个具体的控制参数。4.1 不限制深度训练集 100%测试集直接崩盘我最早做流失预测的时候偷懒没设max_depth直接DecisionTreeClassifier()一把梭。训练集准确率 99.8%我当时还挺高兴。结果测试集一测掉到 71%直接被业务方问得哑口无言。原因很简单。树在没有深度限制的情况下会不断切分直到每个叶子都纯为止。这个过程中它把训练集里的噪声、异常点、随机波动全部当成规律记下来了。真实数据里的规律没多少噪声倒是被学了个遍测试集当然崩。解决办法就是预剪枝。sklearn里最核心的三个参数max_depth树的最大深度建议从 3 开始试每次加 2直到测试集精度不再提升min_samples_split内部节点再分裂所需的最少样本数默认 2实际建议设 10 以上min_samples_leaf叶子节点最少样本数默认 1建议设 5 以上我最常用的调参组合是max_depth7, min_samples_leaf5, min_samples_split10在大部分表格型数据上表现都不错。当然你最好还是用GridSearchCV做正式搜索后面会讲。4.2 类别不平衡光调准确率等于自欺欺人决策树在类别不平衡的数据上会倾向于把叶子节点预测成多数类。因为它的分裂目标是总体纯度最高当 95% 的样本都是负类时一棵什么都不分的树纯度就已经很高了树根本懒得去学习少数类的特征。这个问题的解决方案我在收入预测案例里提过就是class_weightbalanced。它会根据类别频率自动放大少数类的权重让模型把少数类误分类的代价变高。如果你想要更精细的控制可以传一个字典比如{0: 1.0, 1: 5.0}表示少数类的重要性是多数类的 5 倍。这个权重怎么设取决于你对漏报和误报的容忍度需要和业务方聊清楚再做决定。评估指标也需要换。在类别不平衡场景下accuracy是骗人的你应该关注Precision精确率预测为正类的样本里真正为正类的比例Recall召回率真实为正类的样本里被成功找出来的比例F1-score两者的调和平均实际项目里通常要在召回率和精确率之间做权衡。比如做流失预警宁可每天多打扰几个实际上没流失的客户也不想漏掉一个高价值流失客户那你就把少数类的权重调大牺牲一点精度换召回。4.3 后剪枝sklearn 里被人忽略的 ccp_alpha预剪枝靠的是设置树的生长上限属于事前控制。还有一个思路是先把树长得足够深再回头把不重要的分支剪掉这就是后剪枝。理论上看后剪枝往往比预剪枝效果更好因为它允许树先看到全局结构再做局部删除不会因为某一步切分暂时没增益就错过后面的好结构。sklearn从 0.22 版本开始支持基于成本复杂度剪枝的ccp_alpha参数。它的思路是给树的复杂度叶子节点数量加一个惩罚项叶子越多、惩罚越大。ccp_alpha就是这个惩罚的强度系数。具体操作是先拿到不同 alpha 值对应的树再从中选测试集表现最好的那棵import numpy as np from sklearn.tree import DecisionTreeClassifier # 先构建一棵不剪枝的树 clf DecisionTreeClassifier(random_state42) clf.fit(X_train, y_train) # 获取剪枝路径alpha 从小到大对应的树复杂度递减 path clf.cost_complexity_pruning_path(X_train, y_train) ccp_alphas path.ccp_alphas best_clf None best_score 0 for alpha in ccp_alphas: clf_temp DecisionTreeClassifier(random_state42, ccp_alphaalpha) clf_temp.fit(X_train, y_train) score clf_temp.score(X_test, y_test) if score best_score: best_score score best_clf clf_temp print(f最优 alpha: {best_clf.ccp_alpha:.6f}测试集分数: {best_score:.4f})注意当ccp_alpha特别大时树会被减到只剩根节点所以要在中间找到一个甜点。你可以把每个 alpha 对应的深度和分数打印出来自己观察权衡曲线。5. 单棵树的局限性在哪里随机森林与决策树的定位差异热词里有一条随机森林和决策树区别这是每个学树模型的人都会问的问题。我从一个实践者的角度把两者的关系讲清楚。5.1 单棵树的三个天花板决策树单打独斗有几个结构性缺陷是调参解决不了的第一高方差。树的生成过程非常不稳定训练集稍微变一点根节点的特征选择就可能完全变掉整棵树的结构就跟着天翻地覆。这导致单棵树的预测波动很大换个数据切片性能能差好几个点。第二贪婪搜索的局部最优。决策树在每个节点只考虑当前最优的特征和切分点不会去规划未来三步。这种局部贪心策略在不少数据集上会陷入次优解但你也没办法因为全局最优的决策树搜索是一个 NP 难问题穷举根本不可行。第三对轴平行边界敏感。树的切分永远垂直于特征轴如果真实决策边界是斜的对角线树需要用很多层去近似效率很低。这也就是为什么很多高维稀疏数据上线性模型反而更好用。5.2 随机森林如何补上这些短板随机森林的思路很直接既然单棵树容易偏科、不稳定那我就种很多棵树每棵用不同的样本子集、不同的特征子集去训练最后投票取众数。这就是 BaggingBootstrap Aggregating的思想。它主要做了两件事样本随机每棵树做有放回抽样相当于每棵树都换了套教材特征随机每次分裂只随机挑一部分特征来评估保证树与树之间不会长成一个样两重随机性叠加之后单棵树的高方差被平均掉了。方差降低的同时偏差不会显著增加所以整体效果往往比单棵树好一大截。这也是为什么随机森林在实际比赛中很少缺席的原因。还有个挺反直觉的事随机森林里的单棵树甚至不需要训练得特别好。只要每棵树都有点预测能力、彼此之间误差不相关组合起来就能形成一个强模型。这个三个臭皮匠顶个诸葛亮的机制是集成学习的核心魔力。5.3 什么时候用决策树什么时候切换随机森林维度决策树随机森林可解释性极高单条规则可直接追溯较低集成结果难以解释训练速度快慢树的数量多预测精度中等易过拟合高稳定性好调参难度简单几个深度参数就够稍复杂还要调树的数量和特征数对特征缩放不敏感不敏感适用场景规则提取、业务解释、风控白名单精度优先、数据量大、科研竞赛我用决策树最多的场景是探索性分析。拿到一份新数据先跑一棵深度 3 的树打印出export_text看看前几个分裂特征是什么等于让模型帮我做了一次快速特征筛选。业务方问起来我还能指着树图告诉他们你看第一层就是账户余额说明这是最硬的分水岭。一旦确认了方向、进入正式建模阶段我基本会切换到随机森林或者梯度提升树GBDT。不是说决策树不好而是在大多数表格数据上单棵树的精度天花板确实偏低。你愿意牺牲一部分可解释性换取更高的准确率时集成模型是更务实的方案。5.4 我的最终建议决策树真正厉害的地方不是它自己多能打而是它作为算法家族的基础理解了树的分裂逻辑随机森林、GBDT、XGBoost、LightGBM 对你来说都只是换汤不换药——核心还是那个找特征、找阈值、分两堆的过程。差别无非是在怎么抽样、怎么组合、怎么加速上做文章。如果你现在正在和一棵过拟合的树搏斗我的建议是先不要急着换算法把max_depth从 3 到 15 扫一遍同时盯着训练集和测试集的曲线找到那个训练还在涨、测试开始跌的拐点那就是你这份数据对应的最优复杂度。然后试试min_samples_leaf5和class_weightbalanced大部分问题在单棵树上就已经能解决七七八八。万一还不行再请随机森林出场也不迟。这个从简单到复杂的推进路径能帮你避免一上来就陷入集成模型的调参泥潭。
返回列表