尧图精选

决策树算法原理与Python实战:从特征选择到剪枝调参

🕒 发布时间:2026/9/12 15:40:49 📁 来源:尧图网络
最近后台不少人在问决策树的东西有人是因为机器学习期末复习有人是刚入门Python想做点能出结果的项目还有人是在调模型时发现树模型tuning起来比神经网络直观太多。不管你是哪一类决策树这玩意儿都值得搞明白。它既是机器学习里最接近人类思维方式的那一类算法又是随机森林、XGBoost、LightGBM这些硬核模型的底层地基花一晚上把它吃透后面看集成学习基本就是降维打击。这篇就围绕“Python 机器学习 决策树”这三个关键点展开从原理到实操、从分类到回归、从调参到可视化一条线全捋清楚。代码都是用 scikit-learn 写的版本 1.2 以上都能跑环境配好了直接抄作业。1. 决策树的核心逻辑它在干什么决策树这东西打个比方就是你是一个特别爱问问题的人面对一条数据你挨个问“特征A是不是大于某个值”“特征B是不是等于某个类别”每问一次就把数据分成两拨问到最后分出来的每一拨里面样本类别足够纯就停下来了。这一连串问题形成一个树状结构就是决策树。所以说决策树本质上一套if-else 规则的有序集合只不过这套规则不是人拍脑袋写的而是算法根据数据自动找出来的。它属于监督学习既能做分类也能做回归还能输出特征重要性这个特性在实际项目中太香了。和神经网络比决策树有很明显的定位差异神经网络适合处理图像、语音、文本这类非结构化数据靠大量矩阵运算和梯度下降去拟合特征决策树更适合处理表格型数据特征含义明确、数据量中等或偏小的时候训练快、可解释性强而且不需要做特征标准化。另一个和神经网络的关键区别是决策树不用梯度下降。它找分裂点靠的是“纯度”指标比如信息增益、基尼系数这个我们后面专门讲。你在热搜里看到“机器学习中的梯度”这个词大概率就是学神经网络或者线性模型时遇到的但在决策树里没有梯度这个概念不是说树模型学不会而是它的学习方式根本不用对损失函数求导。决策树本身是弱学习器单棵树容易过拟合但它最牛的地方在于能作为集成学习的基模型。随机森林是把很多棵在随机样本和随机特征上训练的树放在一起投票XGBoost 和 LightGBM 是用梯度提升的方式串行训练一棵棵树去拟合残差。这些模型强不强很大程度取决于你对单棵决策树的理解深不深。内含一个常被问到的点树模型假设样本独立同分布吗严格地说ID3、C4.5、CART 这些经典决策树算法没有像线性回归那样明确地声明独立同分布假设但训练数据如果分布漂移或者样本之间有强关联树的结构会被带偏。比如你用第一季度的数据训练拿去预测第二季度的行为如果用户习惯季节性变化明显准确率一定会下降。所以实操中还是要尽量保证训练集和预测集来自同一个分布这个比模型选型更关键别指望算法自动帮你抹平数据分布的差异。2. Python 环境准备与依赖安装在写代码之前先把环境那块踩过的坑都填了别等代码跑起来报错再回来看这里。Python 版本建议 3.9 以上3.8 也能跑 sklearn但新版一些 API 会有细微变化没必要给自己找麻烦。Windows 直接去官网下安装包安装勾选 “Add Python to PATH”macOS 用 brew 或者官网 pkg 都可以Linux 发行版风格各异但核心就一条装完在终端敲python --version能正常输出版本号就算成功。千万别装完就忘了 pip后面装库全靠它。依赖库按需装就行不用一上来就装深度学习全家桶。跑今天这份代码最小集合就四个pip install numpy pandas matplotlib scikit-learn如果下载速度慢可以用国内镜像源。比如清华的 PyPI 镜像加一个-i参数即可pip install numpy pandas matplotlib scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple可视化决策树这一步如果你用的 sklearn 版本比较老0.21 之前需要额外装 graphviz 并配置系统路径这在 Windows 上相当折磨人。新版 sklearn 内置了plot_tree基于 matplotlib 画图省掉了 Graphviz 这个外部依赖。所以我强烈建议升级到新版本再动手能少踩一个天坑。IDE 我常用 VSCode配置非常简单装好 Python 插件在项目根目录创建.venv虚拟环境然后CtrlShiftP选择解释器指向这个虚拟环境。终端里激活之后装依赖、跑脚本干干净净不会污染全局环境。实验室或者公司里搭了专门的机器学习服务器那更好Jupyter Notebook 或者 JupyterLab 一开远程内核一挂代码丢上去就能跑模型训练和数据探索都非常顺手。3. 决策树怎么选特征从信息熵到基尼系数决策树最核心的环节是选择每次分裂用哪个特征、在哪个阈值切。这个选择不是随机的而是用一个“纯度”指标来打分让分完之后的数据变得尽可能纯。下面把这几个指标掰开揉碎讲清楚。3.1 信息熵衡量“混乱程度”信息熵的概念来自信息论它衡量的是一个系统的混乱程度。公式长这样[ H(S) -\sum_{i1}^{k} p_i \log_2(p_i) ]其中 (p_i) 表示第 (i) 类样本在数据集 (S) 中占的比例(k) 是类别数。熵越大说明数据越乱熵越小说明数据越有序。举一个直观的例子一个袋子里所有球都是红色的那么任意抽一个球它必然是红色完全没有不确定性熵是 0。如果红球和蓝球各一半那么你猜下一次抽到什么颜色猜中的概率只有 50%不确定性最大熵就是 1。如果四个颜色各占四分之一那不确定性更大熵是 2。在决策树里我们希望分裂之后每个子节点的熵尽可能低也就是说每个子节点里的样本尽可能属于同一个类别这样规则才有意义。3.2 信息增益用熵的下降幅度来选特征有了熵接下来定义信息增益它表示的是一次分裂之后熵下降了多少[ Gain(S, A) H(S) - \sum_{v \in Values(A)} \frac{|S_v|}{|S|} H(S_v) ]ID3 算法就是靠最大化信息增益来选择分裂特征的。它会遍历所有候选特征计算每一个特征分裂后的信息增益选增益最大的那个作为当前节点的分裂特征。举个例子假设有一个数据集目标是判断“要不要出门打球”特征有两个“天气”晴、阴、雨和“风力”大、中、小。总样本里“去”和“不去”各占一半所以总熵是 1。按“天气”分裂晴天的时候5 个样本里 4 个去、1 个不去阴天全部去雨天全部不去。算出来加权熵很小信息增益很大。按“风力”分裂风力大的时候去和不去各有 2 个风力中的时候 3 个去 2 个不去风力小的时候 4 个去 1 个不去。加权熵明显比按天气分更大信息增益更小。所以这一层就应该选“天气”来分裂。这个计算过程初学者容易算错尤其是多个特征、多类别的时候我建议第一次代入的时候手推一遍用 Excel 列公式也行有了手感之后写代码就顺了。3.3 基尼系数与CART树为什么实际项目中更常用ID3 用信息增益但它有一个毛病偏向选择取值多的特征。比如特征“编号”每条数据都不同ID3 会直接把它选为最佳分裂特征因为按编号分裂后每个子节点只有一个样本熵直接降到 0。但这是个无意义的规则过拟合严重。后来 C4.5 改用信息增益率来纠偏但在 scikit-learn 里我们最常碰到的是 CART 树它用的是基尼系数。公式[ Gini(S) 1 - \sum_{i1}^{k} p_i^2 ]基尼系数的思路是从数据集里随机抽两个样本它们类别不一致的概率。这个值越小说明纯度越高。CART 树在分裂时选择基尼系数下降最多的特征和切分点。scikit-learn 中的DecisionTreeClassifier默认用的 criterion 就是gini你也可以改成entropy。很多人在两者之间纠结实际项目中差异没那么大gini 计算稍快一点entropy 对某些数据集会稍微更平滑。快手建议是默认 gini遇到效果不理想时花几分钟把两个都试一遍选验证集上表现好的即可不用太痴迷理论上的优劣。CART 树还有一个特点它生成的是二叉树不管特征是离散的还是连续的每个节点只会分出两个分支。连续特征靠阈值的比较来切分离散特征则通过“是否属于某个子集”来分成两拨。这是和其他决策树变体比如多叉树很不一样的地方。4. 用 sklearn 实现分类决策树从数据到模型环境准备好了原理也心里有数了现在开始写代码。用 scikit-learn 里的红酒数据集来做演示这是内置数据集没有下载成本非常适合学习。4.1 数据准备与划分from sklearn.datasets import load_wine from sklearn.model_selection import train_test_split wine load_wine() X wine.data y wine.target X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(f训练集样本数: {X_train.shape[0]}) print(f测试集样本数: {X_test.shape[0]}) print(f特征维度: {X_train.shape[1]})这里用了stratifyy意思是保证训练集和测试集里三类红酒的比例和原始数据一致。分类任务里这个参数强烈建议加上尤其是类别不平衡的时候不加真的会翻车。4.2 训练决策树分类器from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, classification_report clf DecisionTreeClassifier( max_depth3, # 限制树的深度防止过拟合 min_samples_split5, # 内部节点至少要有5个样本才继续分裂 min_samples_leaf2, # 叶子节点至少要有2个样本 criteriongini, 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_nameswine.target_names))这是决策树最基本的姿势。三个关键参数我先解释一下后面调参部分还会再展开max_depth是整棵树的深度限制。深度过大模型会把训练集里的噪声也学进去表现在测试集上就是准确率下降min_samples_split决定一个节点至少要有多少个样本才允许继续分裂。这个值设大一点树的分支更少模型更保守min_samples_leaf规定叶子节点的最小样本数防止分出一个只含一个样本的纯叶子这种叶子基本就是记答案没有泛化能力。这三个参数放到一起就是在控制模型的复杂度和泛化能力之间的平衡也就是所谓“奥卡姆剃刀”在效果差不多的情况下树越简单越好。4.3 特征重要性分析训练完模型之后能直接拿到每个特征的重要性分数这是树模型非常实用的一个特性for name, importance in zip(wine.feature_names, clf.feature_importances_): print(f{name}: {importance:.4f})输出示例alcohol: 0.3341 malic_acid: 0.0000 ash: 0.0000 alcalinity_of_ash: 0.0521 magnesium: 0.0000 ...这里的数值含义是每个特征在整棵树分裂过程中带来的纯度提升总和做了归一化。数值越大说明这个特征对分类越重要。如果某个特征的重要性是 0说明它从头到尾没有被用来分裂过可以考虑从特征列表里删掉。实际项目中我会拿这个作为特征筛选的第一个依据比盲目用相关性矩阵更贴合模型本身的行为。4.4 决策树可视化这个环节对理解模型非常有用。sklearn 1.2 以后内置的plot_tree可以直接在 matplotlib 里画图import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize(20, 10)) plot_tree( clf, feature_nameswine.feature_names, class_nameswine.target_names, filledTrue, roundedTrue, fontsize10 ) plt.savefig(decision_tree.png, dpi150, bbox_inchestight) plt.show()filledTrue会给每个节点按类别上色颜色越深说明该节点中某一类的占比越高。读图的时候重点看根节点的分裂特征和阈值再看深度为 2 的节点和深度为 3 的节点之间的区别。如果你发现有些分支在很浅的地方就完全区分开一个类别说明这个特征在这个数据集上区分度极其高。还有一个细节如果你想知道每条样本到底走了哪条路径可以看clf.decision_path(X)它返回一个稀疏矩阵记录每个样本经过了哪些节点这对查异常样本非常有用。5. 决策树回归用树来拟合连续数值决策树不仅能分类还能做回归。核心区别在于叶子节点的输出从“类别”变成了“一个数值”而这个数值就是落入该叶子节点的所有训练样本目标值的平均值。决策树回归的公式化描述是给定训练集树的每个叶子节点对应一个预测值 (c_m)回归决策树的预测函数为[ f(x) \sum_{m1}^{M} c_m I(x \in R_m) ]其中 (R_m) 是第 (m) 个叶子节点对应的特征空间区域(I(\cdot)) 是指示函数表示样本 (x) 是否落在这个区域内。和分类树不同回归树分裂时用的不是基尼系数而是均方误差MSE。每次分裂时算法会找一个特征和阈值把当前节点分成两部分让分裂后的总均方误差最小。在 sklearn 中对应DecisionTreeRegressor。5.1 回归树实操用 sklearn 内置的糖尿病数据集演示from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error, r2_score # 加载糖尿病数据集 from sklearn.datasets import load_diabetes diabetes load_diabetes() X_train, X_test, y_train, y_test train_test_split( diabetes.data, diabetes.target, test_size0.2, random_state42 ) reg DecisionTreeRegressor( max_depth4, min_samples_leaf4, random_state42 ) reg.fit(X_train, y_train) y_pred reg.predict(X_test) print(fMSE: {mean_squared_error(y_test, y_pred):.2f}) print(fR2: {r2_score(y_test, y_pred):.4f})回归树在连续值预测上有一个天然短板它的预测输出是阶梯状的不是一个平滑的曲线。这是因为它本质上是用多个局部区域的均值来做预测。数据关系平滑且复杂时单棵回归树往往表现不如线性回归或集成模型。所以在实际项目中我很少单独用一棵回归树更多是把它当作随机森林回归或者梯度提升回归的基学习器来使用。但单独理解一棵回归树依然很重要因为它能帮你建立对树模型行为的基本直觉。5.2 回归树可视化与解析和分类树一样回归树也可以画图plt.figure(figsize(20, 10)) plot_tree(reg, feature_namesdiabetes.feature_names, filledTrue, roundedTrue, fontsize10) plt.savefig(regression_tree.png, dpi150, bbox_inchestight) plt.show()注意叶子节点的 value 是一个具体数值比如 152.3这就是该叶子覆盖样本的均值。如果一个叶子节点里只有几十个样本那这个预测值方差会很大泛化能力弱。所以回归树中min_samples_leaf往往要设得大一些尤其是在数据量小的时候否则回归结果容易抖动。再看看特征重要性for name, importance in zip(diabetes.feature_names, reg.feature_importances_): if importance 0: print(f{name}: {importance:.4f})这样可以快速定位哪些指标对目标变量的影响最大。比如在糖尿病数据集中通常会看到bmi的重要性很高这和医学常识是吻合的。6. 剪枝策略与过拟合控制新手最容易犯的错误就是看到训练集准确率接近 100% 就开心得不行结果放到测试集上直接崩盘这就是过拟合。对决策树来说过拟合的最典型表现就是树长得太深、叶子太多把训练集中的噪声和异常点都记成了规则。解决过拟合有两种思路预剪枝和后剪枝。预剪枝是在树生长过程中提前停止分裂靠的就是前面提过的那些超参数max_depth、min_samples_split、min_samples_leaf、max_features、max_leaf_nodes。这是 sklearn 中常用的操作方式不用等树长完再回头改。后剪枝是先让树完整生长然后自底向上把一些不重要的子树替换成叶子节点。sklearn 中提供了cost_complexity_pruning路径基于复杂度参数ccp_alpha来剪枝。这个参数是正则化强度值越大剪枝越狠树越简单。下面是一个完整的成本复杂度剪枝操作流程import numpy as np # 训练一棵不剪枝的树获取不同 alpha 值对应的树 path clf.cost_complexity_pruning_path(X_train, y_train) ccp_alphas path.ccp_alphas # 对每个 alpha 训练一棵树看树深度和准确率变化 train_scores [] test_scores [] depths [] for alpha in ccp_alphas: tree DecisionTreeClassifier(random_state42, ccp_alphaalpha) tree.fit(X_train, y_train) train_scores.append(tree.score(X_train, y_train)) test_scores.append(tree.score(X_test, y_test)) depths.append(tree.get_depth())然后用 matplotlib 画一条测试集准确率随 alpha 变化的曲线找最佳点。这种方法比手动瞎试参数更科学也是面试中常问的知识点。实际项目里我通常先设一个较大的max_depth比如 8 到 10然后用GridSearchCV去搜索最优参数组合。直接限制max_depth3虽然安全但可能会让树欠拟合丢失数据中的重要模式。更稳的做法是from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 5, 7, 9], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 2, 4], criterion: [gini, entropy] } grid_search GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid_search.fit(X_train, y_train) print(f最优参数: {grid_search.best_params_}) print(f最优交叉验证得分: {grid_search.best_score_:.4f})这里有个细节交叉验证cv5是在训练集内部切五份轮流拿一份做验证剩下的做训练。这样做出来的得分才不容易被单次随机划分带来的波动所影响比仅靠单次训练测试划分来确定超参数要靠谱得多。水果店老板挑苹果也不能只看苹果屁股那一面亮不亮要多翻几个角度观察道理是一样的。7. 常见问题与排查技巧实录这部分是我自己踩过的坑和带新人时最常被问到的点整理成速查表形式方便你遇到问题时直接定位。问题原因解决方案训练集准确率 100%测试集很低过拟合树太深调小max_depth增大min_samples_leaf或使用ccp_alpha剪枝某个特征重要性为 0该特征从未被选中用于分裂考虑是否删除该特征或检查特征是否存在大量缺失值plot_tree报错找不到 graphviz使用了老版本 sklearn升级 sklearn 到 1.2直接用plot_tree或手动安装 graphviz 并配置 PATH运行代码报ValueError: Unknown label type标签数据不是 sklearn 支持的格式检查是否标签是字符串类型但没有转换为数值用LabelEncoder处理模型对输入顺序敏感特征缩放导致分裂点不同决策树对特征缩放不敏感如果表现差异大说明是随机性影响设置random_state固定结果树在数据稍有变化时结构大变高方差问题单棵树对训练集很敏感使用随机森林或梯度提升等集成模型或者减小深度预测值总是取到均值附近回归树叶子节点样本数太少或太多调整min_samples_leaf使每个叶子的均值更稳定几个实用经验第一连续特征切分点。决策树对连续特征的处理是排序后找相邻点中点作为候选切分点。这就意味着数据中有极端离群值时切分点附近往往只有极少数样本此时可以适当增大min_samples_leaf避免出现“为了一个点单独开一条分支”的情况。第二特征缺失问题。sklearn 的决策树不支持直接处理缺失值和某些算法不一样。实际项目中遇到缺失值先做填充均值、中位数、众数或者直接删除缺失比例过高的特征。如果你需要对缺失值更鲁棒可以考虑 XGBoost 或 LightGBM它们原生支持缺失值处理。第三类别特征编码。分类特征不能直接传给 sklearn 的决策树需要用 OneHotEncoder 或者 OrdinalEncoder 转换。但要注意对有序类别特征用 OneHotEncoder 会丢失顺序信息对无序类别特征用 OrdinalEncoder 又会强加不存在的顺序关系。具体怎么选取决于特征背后业务语义。第四树模型不需要做特征标准化或者归一化。因为树的分裂是基于排序比较阈值的特征数值的大小范围不影响分裂结果。这和 SVM、逻辑回归、KNN 很不一样那些模型对特征尺度敏感必须先标准化。所以当你用决策树时可以省掉这步省出来的时间多调调参数更划算。第五样本量太小的时候决策树效果非常不稳定。我曾在一份只有 200 条样本的数据上跑决策树稍微换一下random_state测试集准确率能在 0.65 到 0.85 之间跳。这种时候别急着调参数先去想办法搞更多数据或者用交叉验证看均值单次划分的结果真的不能说明问题。8. 从单棵树到集成模型你的下一步单棵决策树说到底是一个高方差模型意思是你换一批训练数据树的形状可能差别很大预测结果跟着抖。解决高方差问题里最有效的方式之一就是集成学习。随机森林的思路非常朴素用有放回抽样从训练集里抽出多份子样本每份子样本训练一棵树同时每次分裂时随机挑选一部分特征做候选。这样一来每棵树长得都不太一样最后分类用投票回归用平均把“三个臭皮匠顶个诸葛亮”这句话落到了实处。在 sklearn 中从单棵树切到随机森林几乎零成本from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators100, max_depth5, min_samples_leaf2, random_state42 ) rf.fit(X_train, y_train) print(f随机森林测试集准确率: {rf.score(X_test, y_test):.4f})如果追求更高精度可以尝试梯度提升树比如GradientBoostingClassifier或者直接上手 XGBoost、LightGBM、CatBoost。这些都是基于决策树的模型超参数里依然有max_depth、min_samples_leaf这些熟悉面孔换句话说今天学的这些概念到集成学习时代全部能复用完全可以无缝衔接过去。我自己在项目里的习惯是先跑一棵决策树看它的特征重要性和可视化结构对数据有个直观认知。然后立刻上随机森林设一个适中的树数量和剪枝参数作为 baseline。最后如果有性能要求再切换到 XGBoost 或 LightGBM 做精细调参。决策树那个可视化环节在整个流程里起到的是“侦察兵”的作用它能帮你提前发现数据中的异常、特征中的冗余、甚至标注中的错误这些信息比准确率数字本身更有价值。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →