1. 先从直觉说起:决策树到底在做什么
很多人一听到“决策树”三个字,第一反应是“又是机器学习里一个高大上的算法”,第二反应是“肯定要推一堆数学公式”。说实话,我刚开始学的时候也是这样想的,直到我真正把它用到业务里之后,才发现一个真相:决策树可能是所有机器学习算法里最贴近人类思考方式的一个,它本质上就是“一连串 if-else 规则的集合”。
什么叫 if-else 规则?举个例子,你在网上买衣服,判断一件衣服值不值得买,你可能会先看价格,超过预算直接放弃;价格合适再看尺码,没有自己的码数也放弃;尺码合适再看颜色和款式,都满意就下单。这个过程就是一个决策树:每一步都问一个问题,根据答案走向不同的分支,最终到达一个结论。决策树做的,就是把这个人类纯直觉的判断过程,转化为一套可以自动化执行的规则体系。
从算法层面来说,决策树的训练过程就是回答三个问题:
第一,每一步应该用哪个特征来划分数据?第二,这个特征应该按什么阈值来切?第三,什么时候停下来,生成一个叶子节点?
这三个问题看起来简单,但背后涉及信息熵、信息增益、基尼系数这些概念。而这些概念指向的终极目标只有一个:找到一种划分方式,让数据被切分之后,每个子集内部的样本类别越来越纯。纯的意思就是,同一堆数据里尽量都是同一个种类的东西。
谁适合把决策树学透?我觉得所有刚开始接触机器学习的人,都应该先把它作为第一个真正动手去拆解的算法。原因也很直接:它不像神经网络那样是端到端的黑盒,它的每一步决策你都能解释清楚,每个节点的含义你都能说出来。而且从实操角度来看,决策树是后面很多高级算法的地基,像随机森林、GBDT、XGBoost、LightGBM,内部核心组件都是一棵棵决策树。搞不懂单棵树,后面玩集成学习基本等于空中楼台。
这篇文章,我打算从决策树的原理推导、特征选择逻辑、剪枝策略、回归场景、与随机森林的关系、以及一份可以直接跑的 Python 代码案例,完整地拆一遍。你不需要有很高深的数学基础,能用 python 写基本循环就行,跟着一步步来,一定会比你在书上看十遍公式管用得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 特征选择的核心逻辑:从信息熵到信息增益
2.1 信息熵:衡量数据混乱程度的尺子
要理解决策树怎么选特征,得先理解一个概念:信息熵。
信息熵这个概念来自信息论,天才人物香农在1948年提出了它,用来量化一份数据的“不确定性”。打个比方,一个袋子里全部装的是红球,你伸手进去摸一个球,不用看就知道是红球,没有任何不确定性,这时候熵是0。如果袋子里红球和白球各占一半,你猜对颜色就没那么容易了,这时候不确定性就高,熵也就大。
信息熵的计算公式是:
code复制H(D) = -∑(pk * log2(pk))
其中 pk 表示第 k 类样本在数据集 D 中占的比例。我们通常用 log2 来计算,单位是比特(bit)。熵值越小,说明数据越纯;熵值越大,说明数据越混乱。
举个例子。一个数据集里有 10 个样本,其中 5 个是猫,5 个是狗。那么:
code复制H(D) = -(5/10 * log2(5/10) + 5/10 * log2(5/10))
= -(0.5 * (-1) + 0.5 * (-1))
= 1
这个数据集的熵就是 1 bit。如果 10 个样本里 9 个是猫、1 个是狗,那么:
code复制H(D) = -(9/10 * log2(9/10) + 1/10 * log2(1/10))
≈ -(0.9 * (-0.152) + 0.1 * (-3.322))
≈ 0.469
可以看到,数据越“偏”,熵越小,不确定性越低。这就是熵的直观含义。
2.2 信息增益:选择特征的评判标准
现在我们知道了怎么度量一份数据的混乱程度,下一步就是怎么评判一个特征好不好。
理想的特征是:我用了这个特征把数据划分之后,各个子集的熵之和(按样本量加权平均)显著变小了。这个“熵的减少量”就叫信息增益。
信息增益的计算公式是:
code复制Gain(D, A) = H(D) - ∑( |Dv| / |D| * H(Dv) )
其中 A 是我们要评估的特征,v 表示特征 A 的每一个取值,Dv 是按照特征 A=v 划分出来的子集。
我拿一个经典场景来说:根据天气条件判断要不要出门打球。训练数据有14个样本,特征包括天气(晴、阴、雨)、温度(高、中、低)、湿度(高、正常)、风(有、无),标签是“打球”和“不打球”。
数据是这样的:
| 序号 | 天气 | 温度 | 湿度 | 风 | 打球 |
|---|---|---|---|---|---|
| 1 | 晴 | 高 | 高 | 无 | 否 |
| 2 | 晴 | 高 | 高 | 有 | 否 |
| 3 | 阴 | 高 | 高 | 无 | 是 |
| 4 | 雨 | 中 | 高 | 无 | 是 |
| 5 | 雨 | 低 | 正常 | 无 | 是 |
| 6 | 雨 | 低 | 正常 | 有 | 否 |
| 7 | 阴 | 低 | 正常 | 有 | 是 |
| 8 | 晴 | 中 | 高 | 无 | 否 |
| 9 | 晴 | 低 | 正常 | 无 | 是 |
| 10 | 雨 | 中 | 正常 | 无 | 是 |
| 11 | 晴 | 中 | 正常 | 有 | 是 |
| 12 | 阴 | 中 | 高 | 有 | 是 |
| 13 | 阴 | 高 | 正常 | 无 | 是 |
| 14 | 雨 | 中 | 高 | 有 | 否 |
14个样本里,打球的有9个,不打球的有5个。总体熵为:
code复制H(D) = -(9/14 * log2(9/14) + 5/14 * log2(5/14))
≈ -(0.643 * (-0.637) + 0.357 * (-1.485))
≈ 0.940
好,现在我们计算一下“天气”这个特征的信息增益。天气有晴、阴、雨三种取值:
-
晴:5个样本(编号1、2、8、9、11),其中打球2个,不打球3个。这个子集的熵是:
code复制H(晴) = -(2/5 * log2(2/5) + 3/5 * log2(3/5)) ≈ 0.971 -
阴:4个样本(编号3、7、12、13),全部打球。熵为 0。
-
雨:5个样本(编号4、5、6、10、14),打球3个,不打球2个。子集熵为:
code复制H(雨) = -(3/5 * log2(3/5) + 2/5 * log2(2/5)) ≈ 0.971
那么天气这个特征划分之后,加权平均熵为:
code复制(5/14)*0.971 + (4/14)*0 + (5/14)*0.971 ≈ 0.347 + 0 + 0.347 = 0.694
信息增益就是:
code复制Gain(D, 天气) = 0.940 - 0.694 = 0.246
同样方式可以算出其他特征的信息增益,哪个特征的信息增益最大,决策树就把哪个特征放在当前节点作为分裂依据。这就是 ID3 算法的核心思想。
2.3 信息增益的坑与改进
信息增益好用,但有一个非常明显的缺点:它天然偏好取值数目较多的特征。
怎么理解?假设我们加一个“编号”特征,每个样本的编号都不一样,从1到14。如果我们用编号来划分数据,每个分支下面只有1个样本,每个子集的熵都是0,加权平均熵也是0,信息增益会直接逼近整体熵0.94,数值上最大。但这样的划分毫无意义,因为它只是“背答案”,没有任何泛化能力。
这个问题的本质,是信息增益没有惩罚特征的取值数量。解决方式也很经典,C4.5 算法引入了信息增益率(Gain Ratio):
code复制GainRatio(D, A) = Gain(D, A) / IV(A)
其中 IV(A) 是特征 A 的固有值(Intrinsic Value),按特征 A 的取值分布来计算熵:
code复制IV(A) = -∑( |Dv| / |D| * log2(|Dv| / |D|) )
特征A的取值越多、分布越均匀,IV(A) 就越大,这样就能把“编号”这类极端特征的信息增益拉低。
而 CART 算法(分类与回归树)则换了一个指标——基尼系数(Gini Index),它不需要算 log,计算效率更高。基尼系数的计算公式是:
code复制Gini(D) = 1 - ∑(pk^2)
基尼系数衡量的是从数据集中随机抽取两个样本,其类别不一致的概率。Gini 越小,数据越纯。CART 在特征划分时,选择使得划分后基尼指数最小的特征作为最优划分特征。
这三个算法的演进,本质上就是特征评估标准从“偏科”走向“平衡”的过程:ID3 用信息增益但偏好多值特征;C4.5 用信息增益率做修正;CART 用基尼系数换计算效率并支持回归任务。现在业界最常用的是 CART,因为 sklearn 里的 DecisionTreeClassifier 默认用的就是 CART 和基尼系数。
3. 从根到叶:决策树的完整构建与剪枝策略
3.1 训练一棵树的完整流程
决策树的训练过程,是一个自顶向下的递归过程。我用一个步骤流程来说明:
第一步,从根节点开始,把全部训练数据放在根节点上。
第二步,遍历所有候选特征,按照当前节点的划分标准(信息增益、信息增益率或基尼系数)计算每个特征的得分。
第三步,选择得分最高的特征作为当前节点的分裂特征,并根据特征的取值或阈值生成子节点,把数据分配到各个子节点中。
第四步,对每个子节点重复第二步和第三步,直到满足某个停止条件。
什么时候停止递归?最常见的是这几种情况:
- 当前节点包含的样本都属于同一类别,不需要再分了;
- 当前节点没有可用特征可以继续划分了;
- 当前节点的样本数量小于预设的最小样本数(比如 min_samples_leaf);
- 树的深度已经达到预设的最大深度(max_depth)。
这个流程听起来很简单,但真正落地的时候你会遇到一个很现实的问题:如果不加任何限制地让树一直长下去,它能把每个训练样本都分得清清楚楚,连噪声都背下来。这种状态叫过拟合。
我自己第一次跑决策树的时候就踩过这个坑。当时拿了一个包含几千条样本的营销数据集训练模型,不加任何限制跑了一遍,训练集准确率直接99.5%,看着非常漂亮。结果拿到测试集上一验证,准确率掉到68%。这个落差让我意识到,决策树必须先解决过拟合问题。
3.2 剪枝:对抗过拟合最有效的手段
解决决策树过拟合最常用的方法就是剪枝。剪枝分为两种:预剪枝和后剪枝。
预剪枝是在树的构建过程中提前停止分裂。常用的预剪枝手段包括:
- 限制树的最大深度 max_depth;
- 限制内部节点再划分所需的最小样本数 min_samples_split;
- 限制叶子节点的最小样本数 min_samples_leaf;
- 限制分裂带来的信息增益最小值 min_impurity_decrease。
限制树的最大深度是最直观的做法。深度过大,意味着划分次数过多,模型过度细化了训练数据的细节。我的经验是,在实际业务中,树深度在3到8之间往往就已经有很好的效果了,超过10层之后泛化能力很容易下降。
后剪枝则是先让树完全长成,然后自底向上对非叶子节点进行考察,判断把该节点替换成叶子节点是否会提升模型在验证集上的表现。如果替换后表现更好或持平,就剪掉这个分支。经典的 C4.5 算法里使用的悲观剪枝法,就是后剪枝的一种具体实现。
预剪枝和后剪枝各有优劣。预剪枝训练效率高,但有可能因为过早停止而漏掉好的划分;后剪枝效果好一些,但计算开销更大,尤其是在数据量大的时候。在实际项目中,我个人通常先用预剪枝快速跑通模型,再通过交叉验证或网格搜索来调参。会不会用到后剪枝?说实话,在 sklearn 里没有内置的后剪枝实现,所以绝大多数实际项目依赖的都是预剪枝参数组合。
另外,关于决策树为什么容易过拟合,我想多说两句。决策树是一种特殊的分类器,它是完全基于样本空间划分的非参数方法。非参数模型的主要特征就是模型复杂度会随着训练数据的增加而增长,不像线性模型那样有一个固定的复杂度上界。所以,如果不加限制,决策树可以无限细分直到每个叶子节点只包含一个样本,这就必然导致过拟合。这也是为什么在工程实践中,纯决策树反而不如加了一堆限制条件的决策树好用。
4. 从分类到回归:决策树如何处理连续值任务
4.1 决策树回归的原理
很多初学者以为决策树只能做分类,其实决策树在回归场景下同样是一把好手,也就是决策树回归。CART 本身就同时支持分类和回归。
分类树划分节点时用的是基尼系数,回归树用的则是均方误差(MSE)或者平均绝对误差(MAE)。思路是一致的:对于划分后的每一个子集,误差要尽可能小。
假设我们用一张房子价格表来举例。数据集有三个特征:面积、房龄、到地铁站距离,标签是总价。回归树的每个叶子节点不是“是/否”这样的类别,而是一个数值预测值。这个值通常取该叶子节点内所有样本标签的平均值。
那么怎么判断一个划分好不好?对每个候选划分点,我们计算划分后左右两个子节点的 MSE,把它作为划分质量评估标准:
code复制总MSE = (左子节点样本数 / 总样本数) * MSE(左子节点) + (右子节点样本数 / 总样本数) * MSE(右子节点)
我们要找的,就是让这个总 MSE 最小的划分方式。
拿一个具体的小案例来说明。现在我们有 5 个训练样本:
| 面积(平米) | 总价(万元) |
|---|---|
| 50 | 150 |
| 60 | 170 |
| 70 | 200 |
| 80 | 230 |
| 90 | 260 |
目标是回归出价格。对于“面积”这个特征,候选切分点就是相邻两个样本取值的中间值,比如55、65、75、85。我们计算每个切分点的总 MSE:
- 切分点55:左子节点包含面积50,预测值150;右子节点面积60-90,预测值均值(170+200+230+260)/4=215。总MSE = (1/5)(0) + (4/5)(((170-215)^2+(200-215)^2+(230-215)^2+(260-215)^2)/4) = (4/5)(((2025+225+225+2025))/4) = (4/5)(4500/4) = 900。
- 切分点65:左子节点面积50、60,预测值160;右子节点面积70-90,预测值230。总MSE = (2/5)(((150-160)^2+(170-160)^2)/2) + (3/5)(((200-230)^2+(230-230)^2+(260-230)^2)/3) = (2/5)*50 + (3/5)*300 = 20+180 = 200。
- 切分点75:左子节点50、60、70,预测值约173.3;右子节点80、90,预测值245。总MSE = (3/5)(((150-173.3)^2+(170-173.3)^2+(200-173.3)^2)/3) + (2/5)(((230-245)^2+(260-245)^2)/2) = (3/5)*((543.3+11.1+711.1)/3) + (2/5)*175 ≈ (3/5)*421.7 + 70 ≈ 253 + 70 = 323。
- 切分点85:左子节点50-80,预测值190;右子节点90,预测值260。总MSE = (4/5)*(((150-190)^2+(170-190)^2+(200-190)^2+(230-190)^2)/4) + (1/5)0 = (4/5)((1600+400+100+1600)/4) + 0 = (4/5)*925 = 740。
比较下来,切分点65 的 MSE 最小,所以回归树根节点会选择“面积≤65”作为第一次划分的依据。这就是回归树做连续值预测的完整逻辑。
4.2 连续特征的切分方式与多值特征处理
上面的例子其实也展示了决策树处理连续特征的一种通用思路:把所有可能的取值排序,然后计算相邻取值的中位数,把中位数作为候选切分点,逐个评估。这个思路非常朴素,但计算量会随着样本量的增大而增大。所以 sklearn 在底层实现时做了优化,会按特征值排序后批量计算边界点,而不是真的把所有样本都单独切一遍。
那遇到多值特征怎么办?比如“颜色”有红、黄、蓝三种取值,CART 是怎么处理的?
CART 有一个重要特点:它是一棵二叉树。也就是说,即使是分类特征,每个节点也只能分成两支。如果是二分类特征,天然分成两支;如果是多分类特征,CART 会尝试把所有取值组合成“左集合”和“右集合”两类,然后选择最优的组合方式。比如“颜色”可以切分为“红”和“非红”,或者“红、黄”和“蓝”,以此类推。
这一点和 ID3、C4.5 不同,它们支持多叉树,每个特征取值对应一个分支。多叉树的优点是每个特征只用一次,树比较宽;缺点是容易导致数据切分过快,每个叶子节点样本量少,过拟合风险反而增加。二叉树在这方面反而更稳健,这也是 CART 在业界成为默认选择的一个重要原因。
说句题外话,处理连续特征时还有一个常见的坑:特征的量纲不影响决策树的切分结果。很多刚入门的同学会习惯性地对特征做标准化,用 MinMaxScaler 或者 StandardScaler。其实对决策树来说,标准化完全没必要,因为树模型根本不关心特征的绝对数值大小,只关心排序和相对位置。这一点和 SVM、逻辑回归完全不同。你要是把特征标准化了,唯一的变化就是切分阈值也跟着变了,但划分逻辑完全一致,模型的精度不会有任何提升。
5. 单棵树的局限与集成:决策树和随机森林为什么是天生一对
5.1 随机森林的核心机制
上一节我讲到,单棵决策树很容易过拟合,而且对训练数据中的噪声极其敏感。训练数据一有小的波动,整棵树的结构就可能完全不一样,这种现象叫高方差。
怎么降低方差?最简单的思路是:多训练几棵树,让它们一起投票。这就是随机森林(Random Forest)的核心思想,它属于集成学习中的 Bagging 方法。
随机森林的训练流程可以概括为四步:
第一步,从原始训练集中用 Bootstrap(自助采样)的方法随机抽取 n 个样本子集。Bootstrap 的意思是每次抽一个样本记录后放回,这样抽出的 n 个子集与原始数据集大小相同,但数据内容会有所差异,大约有 63.2% 的原始样本会被抽到,剩下的作为袋外数据(Out-of-Bag)。
第二步,对每个样本子集,训练一棵决策树。但这个决策树的训练过程和普通决策树不同:在每次节点分裂的时候,不是从所有特征里选出最优特征,而是先从全部特征里随机挑选一个特征子集(一般取特征总数的平方根左右),再从这个子集里选出最优划分特征。这个操作叫特征随机采样,也是“随机森林”中“随机”二字的双重含义之一。
第三步,所有树都训练完之后,如果是分类任务,每棵树的预测结果进行投票,取票数最多的类别作为最终输出;如果是回归任务,把所有树的预测值取平均。
第四步,用袋外数据进行模型评估,无需单独的验证集。
随机森林相比单棵决策树,最大的提升就是大幅降低了过拟合风险。单棵树可能对训练数据中的噪声敏感,但很多棵树平均下来,个别树的错误就会被抵消掉。这有点像公司决策,一个人的判断可能很偏激,但一群人的平均判断往往更接近真相。
5.2 决策树和随机森林怎么选
很多同学会纠结:项目里到底该用决策树还是随机森林?我的建议很直白:
如果你的任务是快速理解数据规律、需要向非技术同事解释模型的决策逻辑,那就用单棵决策树。树小、规则清晰、可视化方便,一张图就能说明白。
如果你的任务是追求预测准确率、数据量足够大、特征之间有复杂交互,那就直接上随机森林。它不需要太精细的调参,默认参数下往往就能取得不错的精度,而且天然支持特征重要性评估,对数据预处理要求也比较低。
从我的实际经验来看,随机森林相比单棵树,准确率通常能提升5到10个百分点,甚至更多。但代价是牺牲了可解释性——一棵包含上百棵树的森林,已经无法用“可视化决策路径”的方式直观解释了。
还有一个值得注意的细节:随机森林的基学习器是决策树,所以第2节、第3节讲的关于特征选择、剪枝的知识,在随机森林里依然适用。只不过随机森林在做特征子集采样时已经有了一定的随机性,所以在调参时,树的深度通常可以适当调深一些,因为集成机制本身能分担一部分过拟合风险。
从算法演进的角度来看,从单颗决策树到随机森林,体现了机器学习里一个非常重要的思想:弱学习器通过学习器之间的相互配合,可以变成一个强学习器。理解了这一点,再去看 GBDT、XGBoost、LightGBM 这些进阶的梯度提升模型,就会发现它们和随机森林的区别只在于“树之间是如何协作的”:随机森林是并行地各练各的、最后投票;梯度提升是串行地一棵树接着一棵树,每棵树都拟合前一棵树的残差。
6. 实操案例:用 Python 从零训练一棵可解释的决策树
6.1 环境准备与数据集选择
讲了这么多原理,终究要落到代码上。这里我用 Python 的 scikit-learn 库来演示一个完整案例。
首先确认环境,需要安装以下库:
bash复制pip install scikit-learn pandas matplotlib
我选用的数据集是 scikit-learn 内置的鸢尾花数据集(iris)。它包含了 150 条样本,每条样本有 4 个特征:花萼长度、花萼宽度、花瓣长度、花瓣宽度,标签是鸢尾花品种(山鸢尾、变色鸢尾、维吉尼亚鸢尾)。这个数据集虽然很老,但它是最适合新手理解决策树分类过程的数据集。
顺便说一句,很多初学者喜欢一上来就找复杂数据集练手,其实完全没有必要。决策树这种算法,核心在于理解分裂逻辑和可视化呈现,简单数据反而能让你把注意力集中在“树是怎么长出来的”上。
6.2 核心代码与参数解读
下面的代码会经历“加载数据 → 切分训练集测试集 → 训练模型 → 可视化决策树 → 预测评估”的完整流程:
python复制import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree
import matplotlib.pyplot as plt
# 1. 加载数据
iris = load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target
# 2. 切分训练集和测试集,注意随机种子要固定,保证结果可复现
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42
)
# 3. 训练决策树模型
clf = DecisionTreeClassifier(
criterion="gini", # 划分标准,默认基尼系数,也可以换成'entropy'
max_depth=3, # 限制最大深度,防止过拟合
min_samples_split=5, # 内部节点至少需要5个样本才能继续划分
min_samples_leaf=2, # 叶子节点至少要有2个样本
random_state=42
)
clf.fit(X_train, y_train)
# 4. 可视化决策树
plt.figure(figsize=(20, 10))
plot_tree(
clf,
feature_names=iris.feature_names,
class_names=iris.target_names,
filled=True,
rounded=True,
fontsize=10
)
plt.savefig("decision_tree.png", dpi=200, bbox_inches="tight")
plt.show()
# 5. 预测与评估
y_pred = clf.predict(X_test)
acc = (y_pred == y_test).mean()
print(f"测试集准确率: {acc:.4f}")
# 6. 输出特征重要性
feature_importance = pd.Series(clf.feature_importances_, index=iris.feature_names).sort_values(ascending=False)
print("特征重要性排序:")
print(feature_importance)
代码跑完之后,你会得到一张非常直观的树形图:根节点是“花瓣长度 ≤ 2.45cm”,左边分支直接判定为山鸢尾,右边继续按“花瓣宽度”往下分。
这里我重点说几个参数为什么这样设:
max_depth 设为 3,是因为这个数据集只有150条样本,4个特征,深度3已经足够把三类花区分得比较清楚了。如果数据量更大、特征更多,深度可以适当放宽到5到8,但一般不建议超过10。
min_samples_split 和 min_samples_leaf 的作用是限制最小样本量,防止树在数据稀疏时继续分裂。这两个参数搭配 max_depth 使用,能够有效减少过拟合。
criterion 选 gini 还是 entropy?我个人的经验是,两者的差异在实际效果上往往非常小,很多项目直接用默认的 gini。gini 计算更快,entropy 需要算 log,所以我一般选 gini。
特征重要性这个数组是决策树给我们的一个很有价值的“副产品”。它统计了每个特征在所有节点分裂中对降低不纯度的贡献程度。你在实际做特征工程时,可以先跑一棵决策树看特征重要性排序,筛掉那些贡献几乎为零的特征,再进入后续建模流程,有点像一个快速探路的侦察兵。
跑完这个例子之后,建议你做一个练习:把 max_depth 调成 1、2、5、10,分别看训练集和测试集准确率的变化。你会发现,深度太小欠拟合、深度太大过拟合,这个曲线走到哪里是甜点,才是你对决策树是否真正理解的证明。
7. 常见问题与排查技巧实录
7.1 过拟合问题怎么定位
决策树项目里最常被问到的就是“为什么我的模型训练集表现很好,测试集一塌糊涂?”我刚才已经提到了,这就是过拟合,但真正要解决它,不能只靠一个 max_depth 参数。
我的排查套路是分三步走:
第一步,观察训练集和测试集的准确率差距。如果差距在5个百分点以内,属于正常范围;如果超过10个百分点,基本可以认定过拟合了。
第二步,逐一尝试调整预剪枝参数。通常把 max_depth 从当前值逐步调小,或者把 min_samples_leaf 调大(从1调到5、10甚至20),观察测试集准确率的抖动。注意,不是调得越小越好,要找到泛化误差最低的那个点。
第三步,如果预剪枝调完还是没有明显改善,可以考虑换模型,直接上随机森林。随机森林因为有多棵树平均,天然具备抗过拟合能力,很多情况下一步到位。
另外,要不要用网格搜索 GridSearchCV 来调参?我的建议是,如果你的数据量不大,完全可以在小范围内手动试几个组合,不必一上来就套网格搜索。网格搜索在参数空间大的时候计算开销不小,而且调出来的参数往往过拟合验证集。数据量大的时候才值得用随机搜索或者贝叶斯优化来替代网格搜索。
7.2 类别不平衡时怎么处理
现实中很多分类任务都会遇到类别不平衡的问题,比如欺诈检测里欺诈样本可能只占1%。如果直接用决策树去训练,模型会非常偏向于多数类,因为分裂的目标是降低整体不纯度,少数类对整体贡献太小,很容易被忽略。
处理这个问题有几个常用的方法:
第一种,设置 class_weight 参数为 balanced。sklearn 的决策树支持这个参数,它会根据类别频率自动调整权重,少数类的错分代价会被放大,这样树就会更关注少数类样本。
第二种,对少数类做上采样,对多数类做下采样。经典的 SMOTE 算法就是用来生成少数类合成样本的。
第三种,评估指标不要只用准确率,在类别不平衡的场景下用精确率、召回率、F1-score 更合理。决策树输出的是概率,你还可以通过调整预测阈值来在精确率和召回率之间做取舍。
7.3 特征工程方面的高频问题
决策树虽然对特征预处理的宽容度很高,但在实际项目中还是会遇到三类问题:
一个问题:类别特征要不要独热编码?如果你用的是 sklearn 的 DecisionTreeClassifier,它本身不支持类别特征,所以类别特征必须编码。但独热编码之后,数万个稀疏列会让树变得非常慢。更优的方案是用 LabelEncoder 或者 OrdinalEncoder 做标签编码,因为决策树的切分逻辑是数值比较,标签编码不引入维度爆炸的问题。在这一点上,树模型和线性模型有本质区别。
另一个问题:特征之间高度相关会不会影响模型?决策树每次只选一个特征进行分裂,所以特征之间的多重共线性对树模型的预测能力影响不大,但会干扰特征重要性的解释。假如两个特征强相关,树可能只用其中一个,另一个的重要性被低估,给人造成“某个特征不重要”的错觉。你在看特征重要性报告时,要注意这个坑。
最后一个问题,也是我经常强调的:要不要删掉 ID 类特征?在第2节提到信息增益率时,我说过 ID 类特征会把树带偏。在实际数据集中,身份证号、用户ID、订单号这类特征,哪怕在 CART 的基尼系数标准下,也可能因为取值众多而获得虚高的划分优先级。在建模之前,一定要把这类标识型的特征剔除掉。
7.4 训练与预测速度优化的实战心得
最后一个部分聊聊性能。决策树本身训练速度很快,但在大数据集上,如果不加处理也有一些优化空间。
首先是数据规模和 sklearn 的兼容问题。当样本量达到百万级别时,sklearn 的单机决策树训练时间会明显拉长,内存开销也大。这个时候要么换用稀疏表示方法处理特征,要么直接上 XGBoost、LightGBM 这类专为大数据优化的梯度提升框架。它们内部实现了直方图算法和分位点近似,在大规模数据上的训练速度比 sklearn 默认实现快一个数量级。
其次是预测阶段的速度优化。树模型的预测复杂度取决于树的深度,每层一次比较,所以限制深度不仅能防止过拟合,还能提升单次预测速度。如果模型部署在低延迟的服务上,限制 max_depth 到5以内是一个很值得做的优化。
最后提一句模型大小的问题。如果你把树训练得很深,每棵树的节点数会非常多,模型文件也会变得很大。这个在嵌入到APP或者边缘设备时影响尤为明显。我做过一个项目,单棵树的深度逼近30层,模型文件超过200MB,后来通过限制深度、合并相近叶子节点的方式压缩到了20MB以内,推理速度提升了将近3倍。
写在最后
这篇文章从决策树的底层原理讲到代码实操,再从单棵树延伸到随机森林,覆盖了分类、回归、剪枝、调参、坑点排查这些核心环节。说到底,决策树不是一个“高不可攀”的算法,它就是一套让数据自己长成规则的系统,朴素、直观、极其工程化。
我个人在实际操作中最深的体会是:不要急着套模型,先把你手中的数据交给一棵不加限制的决策树跑一遍,打印出那棵完整的树,认认真真看一遍每个节点的拆分逻辑。这一步带给你的洞察,远超过所有自动调参工具给出来的那份干巴巴的准确率报告。你往往会发现一些意料之外的特征交互,也会发现一些你原本以为很重要、实际上却被忽略掉的特征。
如果你在学决策树的过程中卡住了,多半是卡在数学公式和代码之间缺了一座桥。这座桥就是我上面写的那些手算过程和参数解释。把它摘下来收藏,等你真正开始处理自己的数据时,再翻出来对照,一定会有更多收获。
