news 2026/10/17 1:16:24

决策树算法通俗指南:熵、基尼系数与调参实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
决策树算法通俗指南:熵、基尼系数与调参实战

1. 这个决策树到底在干什么

先别急着背公式,我们用一个特别接地气的场景把决策树讲明白。

假设你要给一家小公司做“是否给客户发优惠券”的自动决策。手上攒了一批历史客户数据,每个客户有几个特征:

  • 最近30天消费金额(连续值)
  • 是否会员(类别值)
  • 距上次访问天数(连续值)
  • 访问次数(连续值)

标签就一个:这人领了券之后有没有实际下单(是/否)。

你希望系统自动学出一套“判断规则”,比如:

  • 如果是会员,且近30天消费超过300元,就发券;
  • 如果不是会员,但访问次数超过5次,也发券;
  • 剩下的情况,不发券。

这套规则就是一棵树。树根是一个特征判断,比如“是否会员”,往左走和往右走分别进入不同分支,每个分支再继续判断下一个特征,直到最终落到一个叶子节点,叶子上存的就是“发券”或者“不发券”这样的结论。

这就是决策树(分类树)最直观的形态。它本质上是在做一件事:把样本空间按照特征取值不断切分,切到每个小区域里的样本类别足够“纯”为止。

“纯”这个概念是理解整棵树的钥匙。如果某个节点里100个样本全是“发券”,那这个节点就非常纯,不需要再分了。但如果50个发券、50个不发券,那这个节点就很“脏”,需要继续找特征把它切开。

把“纯”变成数字,就是各种指标存在的意义:信息熵、基尼系数、信息增益。后面我会一个个拆开讲。

2. 核心指标拆解:信息熵、基尼系数、信息增益到底在算什么东西

2.1 信息熵:混乱程度的数字度量

“熵”这个词听起来很高大上,其实就是描述“系统有多乱”的数值。

拿一枚硬币来说,如果它被做了手脚,永远朝上,那抛硬币的结果毫无悬念,信息量是0,因为这个结果你早知道了。如果它是正常硬币,正反面各50%,那结果你完全没法预判,信息量最大。信息熵衡量的就是“不确定性”的大小。

对于分类问题,一个数据集D里面一共有K个类别,第k个类别占比是p_k,那么这个数据集的信息熵定义如下:

Ent(D) = - Σ p_k * log2(p_k) (k从1到K)

看不懂公式没关系,记住三条:一个节点里如果只有一种类别,熵等于0,最纯;如果类别均匀分布,熵最大,最混乱;类别越多、分布越均匀,熵越大。

举个例子体会一下。假设节点A里有10个样本,全属于“是”,那么:

Ent(A) = -1 * log2(1) = 0

因为只有一个类别p=1,那一项是0,整个熵就是0,节点非常纯。

再看节点B,5个“是”5个“否”:

Ent(B) = -0.5 * log2(0.5) - 0.5 * log2(0.5) = 0.5 + 0.5 = 1.0

熵为1,很混乱,这个节点值得继续切分。

再看节点C,有3个类别,比例分别是0.5、0.3、0.2:

Ent(C) = -0.5*log2(0.5) - 0.3*log2(0.3) - 0.2*log2(0.2)

自己拿计算器按一下,结果大约1.49,比三类别均匀分布的1.585略低,因为分布偏了一点,确定性相对高了一点。

2.2 基尼系数:另一种“不纯度”度量

信息熵涉及对数运算,计算起来稍微费点劲。基尼系数提供了一种更轻量的替代方案,它不取对数,直接算“随机抽两个样本,它们类别不一样的概率”。

基尼系数的公式长这样:

Gini(D) = 1 - Σ p_k² (k从1到K)

还是看那三个例子。

节点A只有一类“是”:

Gini(A) = 1 - 1² = 0

节点B五五开:

Gini(B) = 1 - 0.5² - 0.5² = 0.5

节点C比例0.5、0.3、0.2:

Gini(C) = 1 - 0.25 - 0.09 - 0.04 = 0.62

数值越大代表越不纯,0代表完全纯。

基尼系数和信息熵高度相关,但计算量小得多。所以在实际工程里,尤其sklearn的CART树,默认用的就是基尼系数,一大原因就是算得快。

2.3 信息增益:选哪个特征来切最划算

有了“不纯度”的度量方式,我们就能计算“切完之后纯度提升多少”。这个提升量就叫信息增益。

假设父节点有D个样本,信息熵是Ent(D)。如果用特征A来切,A有V个取值,会把原数据集分成V个子集,第v个子集记作D_v,那么切完之后的总熵就是每个子集熵的加权平均:

Ent_after = Σ (|D_v| / |D|) * Ent(D_v)

信息增益gain就是“切之前”减去“切之后”:

Gain(D, A) = Ent(D) - Ent_after

信息增益越大,说明用这个特征切完,混乱度下降得越多,也就是“越划算”。

决策树在每个节点要做的事,就是遍历所有特征、所有可能的切分点,找出那个让信息增益最大的特征和切分点,然后切下去。

这里有个非常经典的例子,就是某经典的“好瓜”数据集。先算整个数据集的熵,然后分别假设用“色泽”“根蒂”“敲声”切分,分别算出加权熵,最后发现“根蒂”的信息增益最大,于是树根用“根蒂”来切。这个例子几乎所有教科书都讲过,我不重复,你只需要抓住刚才那个计算思路。

2.4 信息增益率与CART的贪心策略

有人会觉得信息增益这么好,直接用不就行了?但信息增益有个坑:它偏爱取值比较多的特征。

比如给每个样本分配一个唯一的编号ID,取值有几百个,用这个特征去切,每个子集里只有一个样本,纯度直接拉满,信息增益巨大。但这明显是过度拟合,学到的规则毫无泛化能力。

因此有了信息增益率,它在信息增益基础上除以一个“固有值”,惩罚那些取值数多的特征。这就是ID3和C4.5算法的重要区别:

  • ID3用信息增益选特征;
  • C4.5用信息增益率选特征;
  • CART分类树用基尼系数选特征。

sklearn里的DecisionTreeClassifier默认实现的是CART,所以它用的是基尼系数。这一点你在看文档时要注意,很多人拿ID3的理论去套sklearn,结果发现对不上,就是这个原因。

3. 特征重要程度的量化:模型自己有“话语权”

决策树有个特别实用的副产品:特征重要程度(feature_importances_)。

很多场景下,我们不光想要一个预测模型,还想知道“到底哪个因素对结果影响最大”。比如银行风控,想知道收入、负债、征信哪个更关键;医疗场景,想知道哪个检查指标对诊断贡献大。

决策树的特征重要性不是靠人为拍脑袋定义的,而是从训练过程中自动算出来的,思路非常朴素。

核心逻辑:每个节点在切分时,都会让数据更纯。纯度提升的幅度越大,说明用来切分的那个特征功劳越大。把这个贡献累计起来,归一化,就得到每个特征的重要性得分。

sklearn里的具体计算方式稍微复杂一点,它用的是“节点纯度下降的加权平均”,权重是该节点样本数占总样本数的比例。

importance(feature) = Σ (该特征作为切分特征的所有节点的 样本占比 * 不纯度下降量)

得到每个特征的重要性之后,sklearn会做一个归一化,让所有特征的重要性之和等于1。

注意几个容易踩的坑:

第一,特征重要性不等于因果性。它只反映这个特征在当前数据分布下对切分的贡献,不代表它和标签有因果关系。

第二,存在相关性较强的特征时,重要性会在它们之间“摊薄”。比如“消费金额”和“消费次数”高度相关,模型可能随机选用其中一个来切分,导致两个特征的重要性都偏低,而不是某一个接近真实水平。这种情况非常常见,解读时要小心。

第三,特征重要性和模型在测试集上的泛化能力不是一回事。一个在训练集上重要性很高的特征,也可能过拟合到噪声上。

我通常会在做完特征重要性分析后,做一个简单的“特征剔除验证”。把重要性排名靠后的特征删掉再重新训练,看看测试集精度掉不掉。如果掉得不多,说明那些特征确实是冗余的;如果掉得很明显,说明重要性排序可能被相关性干扰了,需要进一步分析。

4. 决策树的完整建模流程:从原始数据到一棵可用分类树

4.1 第零步:先想清楚数据长什么样

建模之前,先理清两件基本的事:特征类型和缺失情况。

决策树本身对连续特征和离散特征都能处理。CART树处理连续特征的方式是排序后找切分点。比如“消费金额”有1000个不同取值,CART会先排序,然后尝试每两个相邻值的中点作为切分点,分别算基尼系数下降,选最优的那一个。

离散特征的情况稍微不同。sklearn的DecisionTreeClassififForCategorical不直接支持字符串类别,需要事先编码成数值。但要注意,如果你用LabelEncoder把“红黄蓝”编码成0、1、2,CART会把这些数字当作有序数值来处理,这不一定符合实际语义。更稳妥的做法,是先用一个“决策树专用”的编码思路:把类别变成0/1哑变量,或者干脆不追求“类别ID”,而是用“类别分布合理映射”的方式处理。

实际工程中我比较推荐用pandas的get_dummies生成哑变量,或者用sklearn的OneHotEncoder。虽然会让特征维度变大,但决策树本身对维度增长不敏感,而且避免了错误的有序假设。

缺失值方面,sklearn的CART实现不会丢弃缺失样本,而是会把缺失样本按照“走到当前节点的样本在左右子节点的分布比例”分配到左右。所以数据里的缺失值不用提前大量填充。不明说可能会被很多从其他模型转过来的同学忽略,以为必须先填充才能建模。

4.2 第一步:划分训练集与测试集

核心目的是验证泛化能力。我习惯先做分层划分,确保训练集和测试集里正负样本比例大致一致,尤其在样本不平衡的时候,简单随机划分可能导致测试集里一类样本极少,评估指标失真。

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y )

刻意加上stratify参数,就是保持分布一致。

4.3 第二步:先跑一个最朴素的基线模型

很多人上来就调参。我的建议是先不调,把默认参数的决策树跑一遍,当作基线。sklearn默认配置能直接跑通,得到一组初始精度。

from sklearn.tree import DecisionTreeClassifier clf = DecisionTreeClassifier(random_state=42) clf.fit(X_train, y_train) train_acc = clf.score(X_train, y_train) test_acc = clf.score(X_test, y_test) print('训练集准确率:', train_acc) print('测试集准确率:', test_acc)

如果这个基线的训练集准确率是0.98,测试集准确率是0.82,眼前就出现一个典型信号:过拟合。因为默认参数下树几乎会无限生长,把训练样本全部记住,必然过拟合。接下来所有调参工作,基本都是围绕“如何限制树的复杂度”展开。

4.4 第三步:理解决策树的核心超参数

有几个参数你必须逐个弄清楚,否则调参就是乱撞。

max_depth是最直观的。限制树的最大深度。深度越大,模型越复杂。一般从3到10开始尝试,深度3的树规则很容易解释,像“如果A且B则C”这种,展示给业务方毫无压力。

min_samples_split控制“一个内部节点继续分裂所需的最少样本数”。默认值是2,只要节点样本数不小于2就可以继续切,这很容易切得很细。把它调到10、20、50,能显著降低过拟合。

min_samples_leaf控制“叶子节点必须至少包含的样本数”。这个参数非常关键,它强制每个叶子不能太小。建议同时调整。比如设置min_samples_leaf=20,每个决策规则都至少基于20个样本,统计意义更扎实,噪声影响会小很多。

max_features控制每次切分时考虑多少个特征。默认是全部特征都考虑,即None。设成sqrt或log2可以引入随机性,降低树之间的相关性,对之后的随机森林尤其重要。

ccp_alpha是最小代价复杂度剪枝参数。sklearn提供了基于代价复杂度剪枝的方法,可以用它来做后剪枝。很多人不知道这个参数,其实它比手动限制深度更精细。通过调整ccp_alpha,可以让树先长满,再逐步砍掉那些“性价比不高的分支”。这是一个很好的整体剪枝思路,可以把这个参数和max_depth分开理解:max_depth是事前限制生长,ccp_alpha是事后修剪。

4.5 第四步:用网格搜索做调参,并观察重要参数趋势

我不会从头到尾手动试几百种组合,那太低效了。经典做法是先用GridSearchCV粗调,再用曲线观察规律。

先固定max_depth,试一组step:

from sklearn.model_selection import GridSearchCV param_grid = { 'max_depth': [3, 5, 7, 9, 11], 'min_samples_leaf': [1, 5, 10, 20], 'min_samples_split': [2, 10, 20], } grid = GridSearchCV( DecisionTreeClassifier(random_state=42), param_grid, cv=5, scoring='accuracy' ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)

跑完之后,把最佳参数填入模型,再看测试集。

再补充一个技巧,试着画出不同max_depth下训练集和测试集准确率的变化曲线。通常你会看到,训练集准确率一路走高,但测试集准确率先升后降。那个转折点附近就是合适的深度。如果转折点出现在深度3,说明数据本身信号不强,不要强行加深。

调参的核心思路不是“让测试集分数最高”,而是“让训练集和测试集之间的差距最小”。这句话值得反复品。如果你发现训练集0.9、测试集0.85,可能还有提升空间,瓶颈在特征工程。但如果你发现训练集0.99、测试集0.82,那首要问题是过拟合,再多特征也没用。

4.6 第五步:可视化决策树,把规则讲给业务听

决策树最大的卖点之一就是可解释性。sklearn提供了把树导出为文本或图片的方式。

先用文本方式快速浏览规则结构:

from sklearn.tree import export_text r = export_text(clf, feature_names=list(X.columns)) print(r)

输出会像一段缩进的if-else规则序列:

|--- 消费金额 <= 300.50 | |--- 是否会员 <= 0.50 | | |--- class: 不发券 | |--- 是否会员 > 0.50 | | |--- class: 发券 |--- 消费金额 > 300.50 | |--- 访问次数 <= 5.50 | | |--- class: 发券 | |--- 访问次数 > 5.50 | | |--- class: 发券

这可以直接丢到演示文档里,业务方一看就懂。

如果想输出成图片,用graphviz配合plot_tree更直观:

from sklearn.tree import plot_tree import matplotlib.pyplot as plt plt.figure(figsize=(20, 12)) plot_tree( clf, filled=True, feature_names=X.columns, class_names=['不发券', '发券'], rounded=True ) plt.show()

图中每个节点都会显示样本数、类别分布和不纯度。调色板颜色越纯,代表该节点类别分布越单一。这一张图就能把模型逻辑讲清楚,特别适合在汇报场合用。

这里有一个非常关键的注意事项,别忽略:深度很大的树画出来基本没法看。所以可视化前,我会单独训练一棵max_depth=3的浅树来展示,而不是把调好的复杂树直接画出来。可解释性展示和最佳性能模型可以不是同一个模型,这一点要明确记录在文档里,以免同事问“这棵树和线上模型为什么不一样”。

5. 用代码走一遍完整流程:从造数据到拿到特征重要性

下面给出一份完整可运行的实践代码,数据集用sklearn自带的鸢尾花数据做演示,逻辑上完全即插即用。

from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier import pandas as pd iris = load_iris() X = pd.DataFrame(iris.data, columns=iris.feature_names) y = iris.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y ) clf = DecisionTreeClassifier( max_depth=4, min_samples_leaf=4, min_samples_split=8, random_state=42 ) clf.fit(X_train, y_train) train_acc = clf.score(X_train, y_train) test_acc = clf.score(X_test, y_test) print(f'训练集准确率: {train_acc:.4f}') print(f'测试集准确率: {test_acc:.4f}')

接着输出特征重要性:

importance = pd.Series(clf.feature_importances_, index=X.columns).sort_values(ascending=False) print(importance)

以鸢尾花数据来看,通常“花瓣宽度”和“花瓣长度”重要性最高,花萼相关两个特征重要性很低。这是符合直觉的,因为鸢尾花分类的核心差异就在花瓣尺寸比例上。

特征重要性的可视化也更直观:

import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) importance.plot(kind='bar') plt.title('特征重要程度') plt.tight_layout() plt.show()

这里的数值已经归一化,总和为1,可以直接用于汇报。

6. 常见问题与排查技巧实录

6.1 为什么我的树训练集准确率极高,测试集一塌糊涂

这是决策树最经典的问题,不是bug,而是模型结构太复杂。默认参数下树几乎不设防,会一路生长到把所有训练样本都分对。解决办法围绕限制复杂度展开:减少max_depth、增大min_samples_leaf、增大min_samples_split,或者使用ccp_alpha做剪枝。

不要一下子把max_depth从默认直接切成2,那样可能直接从过拟合跳到欠拟合。先试3、5、7,观察测试集变化趋势,再逐步收敛。

6.2 为什么准确率很高,但业务方觉得规则不合理

一个可能原因是数据不平衡。比如“不发券”占90%,“发券”占10%,模型只需要全预测“不发券”就能拿到90%准确率,但业务价值为0。

此时准确率不是合适的指标,换用精准率、召回率、F1更合理。sklearn里可以用classification_report快速查看:

from sklearn.metrics import classification_report y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred))

另外,再用class_weight参数给少数类加权,改写成:

clf = DecisionTreeClassifier( class_weight='balanced', random_state=42 )

这会让少数类样本在计算不纯度时获得更高权重,树会更关注它们。

6.3 为什么特征重要性总和不是100%

看具体输出。sklearn的feature_importances_归一化之后总和是1,如果是百分比展示,应该是100%。如果发现不是,多半是有人直接用了feature_importances_与样本量相乘的中间值,而不是归一化后的最终输出。按上一节代码直接取clf.feature_importances_即可。

6.4 为什么数值型特征和类别型特征混杂时,特征重要性会失真

这属于高频问题。数值型特征天然的可切分点多,类别型特征如果用独热编码后每个“哑变量”的可切分点只有1个。这种情况下,数值特征更容易被选中去切分,重要性偏高。

这不代表数值特征真的更重要。处理思路是:如果类别特征本身是强信号,模型依然会选它;如果多个相关性高的特征并存,重要性会被摊薄。我在实际项目里的做法是,除了看重要性,还会做一次“单特征建模对比”,逐个特征单独跑一颗浅层决策树,看谁单独也能取得较好精度。这个信息往往比全局重要性更直观。

6.5 树的可解释性如何进一步落地

很多团队要求模型能解释“为什么给这个客户发券”。决策树天然适合这种要求。把每个客户按照树的分支走一遍,就能拿到完整的if-else路径。

sklearn里可以直接用decision_path获取每个样本经过的节点:

path = clf.decision_path(X_test)

配合feature和threshold信息,就能拼出该样本的预测理由。这在金融、医疗等对解释性要求较高的场景非常有用。

7. 从单棵树到集成模型:只懂决策树是不够的

决策树算法的上限并不高,单棵树非常容易过拟合,抗噪声能力也差。工业界真正大量使用的往往是它的升级形式:随机森林、梯度提升树、XGBoost、LightGBM。这些集成模型本质上是把很多棵决策树组合起来,用群体的智慧弥补单棵树的脆弱。

随机森林的思路是:多棵决策树,每棵树用随机抽样的样本和随机抽样的特征来训练,最后投票决定结果。树与树之间越不相关,集成效果越好,这就是为什么max_features参数在随机森林里如此重要。

梯度提升树的思路不同:逐棵树拟合前面所有树的“残差”,一步步逼近正确结果。它对单棵树的深度限制更严格,一般深度3、5就够,重点是数量多、学习率小。

理解了决策树的各项指标,其实你已经掌握了集成模型的底层逻辑。随机森林的feature_importances_就是对所有树的特征重要性做平均,梯度提升树的feature_importances_同样是汇总各树贡献。这些都不难迁移。

所以我建议,如果你业务中稳定、可解释性要求高、数据量不大,直接用决策树没问题。如果追求高精度,数据量大,果断转向随机森林或梯度提升树。但无论哪种,先吃透决策树永远不亏,因为后面所有复杂模型的一砖一瓦,都是这里的基本原理。

说到底,决策树就像一套朴素的分叉问询系统。你今天纠结的熵、基尼,其实就是给“纯度变化”配了一把尺子。尺子拿稳了,后面学随机森林、XGBoost,都会轻松很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 1:15:42

SCENRED场景缩减实战:GAMS把10000个随机场景降到300个

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:15:21

基于MP2642GR-Z与MK24FN256VDC12的两串电池主动均衡方案设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:15:13

Windows 深度学习环境搭建:Anaconda3 + TensorFlow + PyCharm 避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:14:38

储能电池双串被动均衡方案:MP2642GR-Z与PIC18LF45K40实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:13:40

双节锂电池主动均衡设计:MP2642+PIC18LF4680硬件闭环方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:13:18

u-boot配置命令与Makefile机制深度解析:从make xxx_config到编译链接

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华