刚接触机器学习那阵子,我对决策树的认知就一句话:"一堆if-else,有啥好学"。直到自己做分类任务,随手调了sklearn的DecisionTreeClassifier拟合了一棵树,画出来才被上了一课——树深到离谱,分叉逻辑绕成一团,业务方看完直接摇头。后来把信息熵、基尼系数、信息增益这几个核心概念弄明白,再重新限制树的生长,效果立刻就不同了。决策树看着简单,但划分标准怎么选、树长多深、特征重要性数字是怎么算出来的,背后全是细节。这篇我按自己从"调包"到"看懂"的路径,把决策树(分类树)的原理和sklearn用法完整梳理一遍,尽量把公式讲成口诀,把代码写到能直接跑。
1. 决策树在做什么:一场嵌套式"连环提问"
1.1 从"买不买"的场景理解树结构
决策树的本质是一组嵌套的规则判断。假设要判断一个用户今天会不会下单,你会问:他有没有把商品加入购物车?加入了,大概率高意向;没加入,再看有没有领到优惠券;有券,可能也会买;没券,基本不买。这个过程画成图,就是一个树状结构:每个分叉点是一个"提问"(特征判断),每条路径是"这个问题的答案",最终落在叶子上的就是"会不会买"这个分类结论。
树的内部节点、分支、叶子节点,分别对应特征判断、判断结果、类别输出。和纯规则人工编写不同,决策树的这些"提问"不是人拍脑袋定的,而是算法自动从数据里学出来的,它根据某个量化指标选出"当前最有区分能力"的特征来作为节点,然后把数据切得更纯。
这正是决策树最大的价值——可解释性。训练结束后,你可以把整棵树直接导出成一条条业务规则,或者画成图给业务方看。相比神经网络那种"给个结果但说不清为什么"的黑盒,决策树几乎接近白盒,这也是它在风控、医疗、运营分析等需要交代决策理由的领域里一直没被淘汰的原因。
1.2 分类树与决策树家族里的"亲戚"
决策树有分类树和回归树之分。分类树预测的是离散标签,比如"买/不买"、"正常/逾期";回归树预测的是连续数值,比如"预计客单价"、"流失概率"这类。两者在划分逻辑和评估指标上略有区别,分类树通常用信息熵、基尼系数衡量纯度,回归树则用均方误差、绝对误差。sklearn里对应的模型分别是DecisionTreeClassifier和DecisionTreeRegressor,接口几乎一致,只是criterion参数默认值和可选值不同。
另外,决策树本身是个大家族。经典算法包括ID3——用信息增益选特征;C4.5——用信息增益率来纠正ID3的偏好;CART——用基尼系数做分类、用均方误差做回归。sklearn里实现的DecisionTreeClassifier严格来说是CART的优化版本,所以我们后面讨论的"信息增益"和"基尼系数"其实都在CART框架下运作,只是sklearn允许你把criterion从gini切换成entropy。
整体上,无论哪种算法,构建树的套路都差不多:从根节点开始,依据某种指标选一个最优特征做划分,划分后每个子节点重复这个过程,直到满足停止条件。这个过程是贪心的——每一步都选当前看起来最好的划分,不回头改前面的节点,所以决策树并不保证全局最优。这个特点后面会直接影响剪枝的必要性。
2. 信息熵与信息增益:量化"不确定性"的关键
2.1 信息熵:数据有多"混乱"的度量
先说信息熵。它衡量的是系统的不确定程度,可以理解为"这里面有多乱"。公式长这样:
H(D) = - Σ p_i * log2(p_i)
其中p_i是第i类样本在数据集D中的占比。如果集合里的样本全部是同一类,那熵就是0,因为完全确定、没有混乱。如果两类各占一半,比如正负样本各50%,那熵等于1,这个时候最不确定。如果类别更多、分布更均匀,熵会更大。常见实现里也有用自然对数ln的,只是单位从bit变成nat,结论一致,sklearn底层的实现细节会做处理,我们使用时不需要刻意区分。
我举一个直观的例子。一个箱子里全是红球,你随便摸一个肯定是红色,熵为0;换成红球黑球各一半,你猜中颜色的概率只有一半,这部分不确定性就是熵1。机器学习里"不确定性"不是一个抽象概念,而是可以直接用熵这个数来计算的。
为什么要引入熵?因为决策树需要一个指标来判断"这个特征划分得好不好"。好的划分应该让划分后的子集比划分前更纯、更确定,也就是熵更小。划分前后熵的差,就是信息增益,也就是"这次提问消除了多少不确定性"。
2.2 信息增益:一次提问的价值
信息增益定义为划分前的熵减去划分后的加权熵,也就是条件熵的差值:
InformationGain(D, a) = H(D) - Σ (|Dv| / |D|) * H(Dv)
其中a是某个特征,v是这个特征的取值,Dv是特征a取值v的样本子集。|Dv|/|D|是权重,因为样本多的子集影响更大。每轮划分时,对所有候选特征分别计算信息增益,选增益最大的那个作为当前节点,这就是最经典的ID3思路。
先看熵减小的方向:信息增益越大,说明这个特征把混合样本切得更纯。所以决策树的贪心规则就是"每次都挑信息增益最大的特征来提问"。形象一点说,每个特征都是一道"测试题",树在建树时会优先挑"最能淘汰错误答案的那道题"来考。
2.3 手算一次信息增益:把公式变成直觉
只看公式容易懵,我用一个微型数据集手动算一遍。假设我们要预测用户"是否下单",只有两个特征:是否浏览了详情页、是否领到了优惠券,标签为是否下单。模拟四条样本:
- 样本A:浏览=1,优惠券=1,下单=1
- 样本B:浏览=1,优惠券=0,下单=1
- 样本C:浏览=0,优惠券=1,下单=0
- 样本D:浏览=0,优惠券=0,下单=0
初始状态下,2个下单、2个未下单,概率各一半:
H(D) = -0.5 * log2(0.5) - 0.5 * log2(0.5) = 1
按"是否浏览过详情页"划分:浏览=1的子集有A、B,两个都下单,熵是0;浏览=0的子集有C、D,两个都没下单,熵也是0。加权后的条件熵:
H(D|浏览) = (2/4)*0 + (2/4)*0 = 0
信息增益为1 - 0 = 1,完美切分。
再按"是否领到优惠券"划分:优惠券=1的子集是A和C,一个下单、一个不下单,熵还是1;优惠券=0的子集是B和D,也是一个下单、一个不下单,熵也是1。加权条件熵:
H(D|优惠券) = (2/4)*1 + (2/4)*1 = 1
信息增益是1 - 1 = 0。
结论很清楚:第一棵树根节点的"提问"应该选"是否浏览过详情页",因为它的信息增益远大于"是否有优惠券"。这个手算过程,就是决策树在计算机内部不断重复的基础操作。
不过信息增益有个毛病——它天然偏向取值类别多的特征。假如加一个特征"用户ID",每人取值都不同,按它划分时每个子集只有一个样本,子集纯度极高,信息增益直接接近最大,但它显然不是一个有泛化能力的特征。C4.5为了解决这个问题,提出了信息增益率,用特征的固有值来"惩罚"多取值特征。sklearn里的DecisionTreeClassifier用的是类似CART的实现,即使你选entropy作为criterion,内部做的是二元切分,会更谨慎地处理多取值问题,但理解信息增益的偏好仍然很重要。
3. 基尼系数:更"省计算"的纯度指标
3.1 基尼系数怎么算
基尼系数是CART分类树的默认划分标准,公式比信息熵更简单:
Gini(D) = 1 - Σ p_i^2
它的直觉解释很有意思:从数据集里随机抽两个样本,这两个样本类别不一致的概率。全部同一类时,Gini为0;两类各一半时,Gini为0.5;类别越杂,这个值越大。
还是用刚才的四条样本。初始集合里两类各一半:
Gini(D) = 1 - (0.5^2 + 0.5^2) = 1 - 0.5 = 0.5
按"是否浏览过详情页"划分后,两个子集都纯,每个子集的Gini都是0,所以加权基尼:
Gini_index(浏览) = (2/4)*0 + (2/4)*0 = 0
按"是否有优惠券"划分后,每个子集都是一正一负:
Gini_index(优惠券) = (2/4)(1 - 0.5) + (2/4)(1 - 0.5) = 0.5
CART选划分特征的规则是"选划分后加权基尼指数最小的那个",也就是让子集尽可能纯。和信息增益"选最大"正好相反。形式上也在做同样的事情,只是衡量混乱度的尺子不同。
信息熵里有log运算,基尼系数只有平方和减法,计算成本更小。数据量大时,这个差距会被放大,所以sklearn默认criterion='gini'是合理的工程选择。
3.2 信息熵和基尼系数,到底选哪个
很多教程说"信息熵和基尼系数结论几乎一致,可以随便选",这话大体没错,但有一些细节值得注意。
它们的共同点是都衡量节点纯度,排序优先级绝大多数情况下一致。不同点在于:基尼系数对"类别占比变化"更敏感,有时候会偏向把样本分得更快的划分方式,而信息熵在优化方向上更平滑。另一个细节是,基尼系数在构造树时可能更容易把节点分裂成"纯得很快"的形状,树的结构两者通常不会完全一样,但泛化性能差别很小。我在多个数据集上的实测结果是:换criterion带来的性能差异,远小于换max_depth带来的差异。你要是拿不准,默认gini就行,不需要在这上面内耗。如果为了对比,可以两个都跑一遍,选交给GridSearchCV去搜就行。
顺便说一句,sklearn中criterion还有选项log_loss,本质是使用对数损失的改进实现,在部分数据上会比entropy略快,但理解上仍然可以归到"基于熵"的那类。
4. 特征重要程度的量化:树给你"排座次"的逻辑
4.1 不纯度减少累加法
拿到一棵训练好的树,sklearn会输出feature_importances_,每个特征一个分数,所有特征分数加起来等于1。这个数是怎么来的?核心思路叫"不纯度减少累加"。
每一次节点划分,都会让这个节点的不纯度下降。下降的量是划分前的节点不纯度,减去划分后左右子节点不纯度的加权和,再乘上这个节点的样本占比。公式类似:
重要性贡献 = (N_t / N) * ( impurity_t - (N_L / N_t) * impurity_L - (N_R / N_t) * impurity_R )
impurity就是你选的criterion对应的度量:如果是gini,就用基尼指数;如果是entropy,就用熵。N_t是当前节点样本数,N是总样本数。把某个特征在所有节点上的这类贡献全部累加起来,最后对所有特征做归一化,让总和为1,这就是feature_importances_。
为什么还要乘N_t / N?因为一个节点即使把一个很小的子集切得很纯,它对全局的影响也有限;而靠近根节点的划分动辄影响成千上万个样本,权重自然更高。这个加权逻辑就是"重要程度"与"影响了多少样本"挂钩的体现。
4.2 特征重要性的解读与常见误用
我用一个模拟场景跑完sklearn实际输出里会看到,重要程度高的特征通常集中在根节点和靠近根部的分裂点上。如果某个特征的取值基本是随机噪声,它可能从头到尾都没有被选中参与分裂,重要程度直接是0。这本身是有效信号,说明这个特征在当前数据下没有区分能力。
但有个坑必须提醒:特征重要性不等于因果重要性。它只能说明"在当前数据、当前树结构下,这个特征被用来做了多少关键划分",不能说明特征与目标之间有因果关系。还有两个经典偏差:
- 高基数特征更容易被选作分裂点,也容易获得更高的重要性,哪怕它与标签无关。
- 存在相关性强的特征时,重要程度可能在它们之间被分散,单看一个特征的重要性会低估整体信息量。
所以做特征筛选时,如果你打算直接用feature_importances_砍掉排序靠后的特征,建议再配合排列重要性(permutation importance)或者多次交叉验证来交叉验证,别一棵树跑完就拍板。
4.3 从一棵小树看重要性的直观感受
回到手算的例子:假如我们用"是否浏览过详情页"做根节点,一次分裂后两类就完全分开了,这棵树不需要再分裂。那么"是否浏览过详情页"的不纯度减少贡献就占据了全部重要程度,"是否有优惠券"因为从未参与划分,重要性就是0。在实际输出中你会看到类似[1.0, 0.0]这样的结果。
这个例子虽然极端,但很好说明了特征重要性的一种特点:它依赖树的具体生长路径。同一个特征在不同随机种子、不同数据子集下训练出的树上,重要性会有波动。所以多跑几棵树,或者用随机森林取平均,观察会更稳定。
5. sklearn实战:训练一棵可解释的分类树
5.1 构造演示数据与预处理要点
下面进入代码部分。为了让流程完整,我用模拟数据来演示。假设场景是"预测用户是否会下单",原始特征里同时有数值型、二分类型和多分类型,这样可以把编码处理一起过一遍。
import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier, plot_tree from sklearn.metrics import classification_report import matplotlib.pyplot as plt np.random.seed(42) n = 600 df = pd.DataFrame({ "浏览时长": np.random.rand(n) * 100, "是否加购": np.random.randint(0, 2, n), "是否领券": np.random.randint(0, 2, n), "设备类型": np.random.choice(["iOS", "Android", "PC"], n, p=[0.4, 0.35, 0.25]) }) # 模拟一个带噪声的真实规则:不是靠随机生成标签,而是构造出符合业务直觉的规律 score = 0 rule = ( ((df["浏览时长"] > 40) & (df["是否加购"] == 1)) | ((df["是否领券"] == 1) & (df["浏览时长"] > 25) & (df["设备类型"] != "PC")) | ((df["是否加购"] == 1) & (df["是否领券"] == 1)) ) df["是否下单"] = rule.astype(int) # 加点噪声:让4%的样本标签抖动,让任务不至于“可被完美拟合” noise_idx = np.random.choice(df.index, int(n * 0.04), replace=False) df.loc[noise_idx, "是否下单"] = 1 - df.loc[noise_idx, "是否下单"]注意一个细节:预处理在模型训练之前完成,千万不要在切分训练集和测试集之后才做编码,否则容易出现数据泄漏。决策树对特征尺度不敏感,所以数值特征不需要标准化,但对类别变量需要做编码。我用get_dummies把"设备类型"拆成多个0/1列,对"是否加购"和"是否领券"本身已经是0/1,可以直接用。
df = pd.get_dummies(df, columns=["设备类型"], drop_first=True) X = df.drop("是否下单", axis=1) y = df["是否下单"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )关于stratify=y,做分类任务时这是一个很容易被忽略但非常值得养成的习惯。特别是正负样本不平衡时,如果不分层抽样,训练集和测试集的类别比例很可能会不一致,导致评估结果失真。用了stratify后,scikit-learn会尽量让切分后的数据保持原始比例,至少能排除一个明显的干扰源。
5.2 DecisionTreeClassifier参数拆解
先看一个基础版本的训练代码:
clf = DecisionTreeClassifier( criterion="gini", max_depth=4, min_samples_leaf=5, random_state=42 ) clf.fit(X_train, y_train) print("训练集准确率:", clf.score(X_train, y_train).round(4)) print("测试集准确率:", clf.score(X_test, y_test).round(4)) print("特征重要性:", dict(zip(X.columns, clf.feature_importances_.round(4))))这里我按经验先做了两项约束:max_depth=4和min_samples_leaf=5。如果完全不加这两个参数,树会一直长到每个叶子都足够纯,训练集准确率会逼近100%,但泛化能力往往很差。max_depth限制树的层数,min_samples_leaf要求每个叶子至少5个样本,这两种预剪枝手段是最常用的。
再补充几个常见参数:
- splitter:默认best,即每个节点遍历所有特征和阈值取最优划分;改为random则用随机子集搜索,更快但结果更不稳定,一般建议保留默认。
- max_features:限制每次分裂考虑的特征数,类似随机森林的做法,能增加多样性但单棵树的解释性会打折。
- class_weight:数据不平衡时给少数类加权,比如"balanced",会按类别频率自动调整权重。
- min_samples_split:内部节点再分裂的最少样本数,和min_samples_leaf配合使用。区别在于前者管"分裂前",后者管"分裂后"。
- random_state:固定随机种子,否则同一份数据可能长出不同的树,调试时尤其要记得设。
我看到一些同学习惯只看"准确率"来决定调参方向,这个习惯容易走偏。分类问题里准确率会受类别分布影响,更全面的是查看分类报告:
y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred, digits=3))5.3 可视化与解释:让业务方也能看懂
训练完以后,最有价值的是把树画出来。最省事的是用plot_tree:
plt.figure(figsize=(16, 9)) plot_tree( clf, feature_names=X.columns, class_names=["未下单", "下单"], filled=True, rounded=True, fontsize=10 ) plt.show()filled=True会按类别占比给节点上色,一眼就能看到哪些节点纯度高、哪些节点仍在混杂。rounded=True让框变成圆角,输出更美观。如果你在的环境里plot_tree显示乱码,或者想以文本方式快速看规则,还有一个替代方案:用export_text。
from sklearn.tree import export_text print(export_text(clf, feature_names=list(X.columns), decimals=2))export_text输出的是一段缩进文本,非常像可读的规则,适合放在邮件或者文档里解释给业务方看。我第一次给业务方演示的时候就是画图加文本一起上,对方很快理解了"什么样的人会被判定为高意向用户"。
可视化最重要的作用,是让你第一时间发现树有没有长歪。比如根节点直接选了一个业务上明显无关的特征,那你需要回头检查特征编码或者数据预处理是否有问题。又比如树的前两层全在按某个特征反复切分,可能说明其他特征的信息几乎没被利用,这也是一种诊断信号。
6. 剪枝与过拟合:树长太野,怎么收回
6.1 决策树过拟合的典型表现
决策树是典型的高方差模型。给它足够的深度,它能把训练集里每个样本都"背下来",包括噪声和异常点。表现就是训练集准确率接近满分,测试集却明显掉下来。我在一次实际数据上做过一个对比:不限制深度的树在训练集上准确率接近99%,测试集却只有74%;限制max_depth=3之后,训练集降到91%,测试集反而升到81%。这就是过拟合最直观的证据。
为什么决策树特别容易过拟合?因为它的构建过程是贪心的,每个节点都追求当前数据上的最优纯度,这种"局部最优"在训练集上很有用,但到了新数据上,那些只为极少数样本量身定制的小分支就成了"死记硬背",没有泛化价值。
应对思路分两类:预剪枝和后剪枝。预剪枝是在建树过程中提前停止,比如限制深度、叶子最小样本数;后剪枝是让树先长满,再回过头剪掉一些对泛化没帮助的分枝。sklearn两种都支持。
6.2 预剪枝的调参顺序经验
预剪枝参数一多,新手容易乱试。我的建议是固定一个调参顺序,别一次全动。
第一步,先限制max_depth。从3开始,逐步往上加(3、4、5、6、8……),观察测试集准确率的变化。一般情况下,测试集准确率会先升后降,找到拐点那个深度作为基线。第二步,再调min_samples_leaf,从2、3、5、10里选。它比min_samples_split更容易直观理解:叶子样本数太少,就很可能是极端分支。第三步,如果不放心,再用GridSearchCV整体搜一遍,把criterion、max_depth、min_samples_leaf都放进参数网格里。
from sklearn.model_selection import GridSearchCV param_grid = { "criterion": ["gini", "entropy"], "max_depth": [3, 4, 5, 6, 8], "min_samples_leaf": [2, 3, 5, 8] } grid = GridSearchCV( DecisionTreeClassifier(random_state=42), param_grid, cv=5, scoring="accuracy", n_jobs=-1 ) grid.fit(X_train, y_train) print("最优参数:", grid.best_params_) print("最优交叉验证分数:", grid.best_score_.round(4))GridSearchCV里cv=5是五折交叉验证,n_jobs=-1表示用全部CPU核并行跑,能省不少时间。选择参数时除了看交叉验证分数,我还会再检查一下best_params_对应的模型在测试集上的表现,防止交叉验证结果和测试集有出入。另外,cv折数在数据集较小时可以适当加大,比如cv=10,但训练成本也会更高,自己权衡。
6.3 后剪枝:用ccp_alpha做代价复杂度剪枝
sklearn在较新版本里支持基于代价复杂度(cost complexity pruning)的后剪枝。原理是给树的复杂度一个惩罚项,ccp_alpha越大,惩罚越重,树被剪得越短。实际操作时,可以先拿到一棵完整树的完整路径:
path = clf.cost_complexity_pruning_path(X_train, y_train) ccp_alphas = path.ccp_alphas # 遍历不同的alpha,训练并记录树的信息 for ccp_alpha in ccp_alphas[::10]: clf_pruned = DecisionTreeClassifier( criterion="gini", max_depth=None, ccp_alpha=ccp_alpha, random_state=42 ) clf_pruned.fit(X_train, y_train) score_train = clf_pruned.score(X_train, y_train) score_test = clf_pruned.score(X_test, y_test) print(f"ccp_alpha={ccp_alpha:.6f}, train={score_train:.4f}, test={score_test:.4f}")同样可以结合GridSearchCV直接搜ccp_alpha。后剪枝的优势是不用预先限制深度,让树自由生长,再统一裁剪,但代价是训练时间更长,树很大时计算开销不可忽视。在实际项目里,我更倾向于先用预剪枝控制树的大小,再用ccp_alpha做一次微调,两者结合使用。如果数据集不大、可解释性要求很高,这条路径基本够用。
注意:ccp_alpha只在树完全生长后的路径上选择剪枝位置,如果同时设置max_depth,后者会先限制树的规模,再执行后剪枝。这两类剪枝并不冲突,但调参时容易混淆。我一般不会同时把max_depth和ccp_alpha都放进同一个搜索网格,而是分开做,避免难以定位瓶颈。
7. 常见问题与排查技巧实录
7.1 典型问题速查
我整理了一份自己在用决策树过程中踩过、也见人踩过的常见问题列表,可以直接当排查手册用。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 树特别深、训练集准确率极高但测试集掉得厉害 | 过拟合,缺乏有效剪枝 | 限制max_depth或min_samples_leaf,搜索最佳参数 |
| 特征重要程度全都是0或接近0 | 特征被编码错误留成了object类型;或者特征与标签无关 | 检查特征dtype,用get_dummies编码;尝试多跑几棵树均值 |
| 换criterion(gini/entropy)树结构变化很大 | 两者衡量纯度方式不同,正常现象 | 不需要强行压制,用交叉验证选一个即可 |
| 类别特征取值很多,根节点总是它 | 高基数特征偏向 | 考虑特征编码方式,或用增益率/限制max_features |
| 预测结果偏向多数类 | 数据不平衡且未处理 | 设置class_weight="balanced",或使用分层抽样后再调参 |
| 每次运行树都长得不一样 | 没有固定random_state | 设random_state=42,保留可复现性 |
7.2 独家避坑经验
第一,不要在模型训练前对决策树做特征标准化。决策树做的是阈值切分,不涉及距离计算,标准化不影响结果,反而增加无意义的计算量。看到有人对每一列做StandardScaler然后喂给DecisionTreeClassifier,其实完全可以省掉。
第二,plot_tree输出太乱时,先调小max_depth再看。一次性画出深度几十的树,图会糊成一团,谁也看不清。我通常先用max_depth=3画一棵小树,确认特征逻辑没问题后,再慢慢放深度限制。这不是偷懒,而是用可视化做诊断的必要步骤。
第三,不要把feature_importances_当作唯一特征筛选依据。之前一个项目里,一个真正有业务价值的特征因为和另一个高基数特征高度相关,重要性被稀释到0.02,差点在筛选时被砍掉。后来我用排列重要性验证,特征信息量其实还在。遇到这种情况,要么容忍部分共线性,要么做联合重要性分析,别单看一个数字。
第四,类别变量编码时机要注意。get_dummies之后特征列会变多,树的可解释性也会受到影响——比如"设备类型_PC"这样的列名,画图时还算清楚,但如果是文本规则导出,就需要额外做一步映射。如果类别特征是有序的,比如"低中高",可以考虑用OrdinalEncoder保留顺序信息,效果往往更好。
第五,决策树通常不是最终模型,而是理解数据的工具。我经常先用一棵浅层决策树做探索性分析,看哪些特征自动成了分裂点,然后再决定后续该用逻辑回归、随机森林还是梯度提升树。决策树能帮你快速定位"哪些特征在什么阈值上有区分度",这一步在特征工程阶段非常值钱。
8. 一点个人体会
如果让我用一个词形容决策树,我会选"诚实"。它不像很多模型那样给你一堆概率让你自己猜,而是把"到底为什么这么判"明明白白摆在那里。理解了信息熵、基尼系数、信息增益这些概念之后,你再去看随机森林、XGBoost这类集成模型时,会发现它们的底层逻辑其实都在跟"如何切分数据让子集更纯"较劲。树模型是机器学习里少有的、花时间研究原理就能直接指导调参方向的模型,这笔时间投入性价比很高。遇到具体工程问题时,我建议你先把一棵决策树的可视化打印出来多看几遍,远比盲目调参来得有效。