简介:这份资源面向机器学习初学者与数据挖掘实践者,聚焦决策树这一经典监督学习模型,帮助读者从原理到代码完整掌握分类与回归任务中的树模型实现。压缩包共28个文件,约2.43MB,包含10个py脚本、6个ipynb笔记本、4份pdf讲义、3个txt说明及xlsx数据集与png可视化图,兼顾PyCharm与Jupyter两种运行方式。内容覆盖ID3、C4.5、CART三大算法的特征选择、节点分裂与剪枝策略,并配有员工离职预测案例实战,以及K折交叉验证与GridSearch网格搜索的参数调优环节,还涉及graphviz插件安装与决策树可视化。已有526人学习下载,读者可借助源码理解信息增益、基尼不纯度等准则,动手搭建预测模型并完成评估,适合作为课程作业、项目练手与面试复习的参考素材。
1. 决策树模型源码包:从一份 zip 到能跑通的第一棵树
很多人拿到「机器学习与算法源代码5:决策树模型.zip」这类压缩包,第一反应是解压、找 main、直接运行,然后被一堆相对路径、缺失依赖和编码报错劝退。这个标题背后其实是一套很典型的机器学习入门工程:用决策树把一份带标签的表格数据训练成可解释的分类或回归模型,并附上可复现的源代码。它解决的不是「算法有多玄」的问题,而是「我能不能在本地把这棵树完整跑起来,并且知道每个参数在干什么」。适合刚接触机器学习、正在做期末复习或课程设计的人,也适合想拿一份干净源码对照《机器学习》周志华那类教材补实现细节的工程师。下面按「先立住原理、再动手复现、最后排坑」的顺序拆开讲,代码以 Python 从零实现和 scikit-learn 两条线并行,方便你对照理解。
2. 决策树到底在切什么:信息增益、基尼指数与剪枝的选型逻辑
2.1 一棵树的分裂标准:从熵到基尼指数
决策树的核心动作只有一个:在每个节点上选一个特征和一个阈值,把当前数据集切成两份,让切完之后的子集「更纯」。衡量纯度最常用的两个指标是信息熵和基尼指数。信息熵来自信息论,取值越大表示越混乱;基尼指数可以理解为从数据集中随机抽两个样本、类别不一致的概率,计算量比熵小,因为不用算对数。
信息增益就是父节点熵减去子节点熵的加权和,ID3 用它选特征;C4.5 用信息增益率修正了「取值多的特征占便宜」的问题;CART 用基尼指数,并且只做二叉分裂。你在源码包里看到的criterion参数,本质就是在选这套标准。分类任务常用gini或entropy,回归任务用squared_error(老版本叫mse)。选哪个不是玄学:数据量大、追求速度用 gini;类别不平衡、想更细致区分用 entropy,但差距通常不大。
2.2 预剪枝和后剪枝:什么时候该让树停下来
不限制深度的决策树一定能把训练集拟合到接近零误差,代价是每个叶子只剩一两个样本,测试集上直接翻车。控制过拟合有两条路:预剪枝在生长过程中就设限,比如max_depth、min_samples_split、min_samples_leaf、max_leaf_nodes;后剪枝先让树长满,再自底向上把收益不大的子树替换成叶子,代价复杂度剪枝(CCP)就是代表,scikit-learn 里对应ccp_alpha。
实操里我一般先用预剪枝把max_depth卡在 3 到 10 之间快速看效果,再用ccp_alpha做一轮后剪枝微调。原因是预剪枝快、可控,但可能欠拟合;后剪枝效果通常更好,但计算更贵。源码包里如果只实现了 ID3 而没有剪枝,那它更适合教学,不适合直接上生产数据。
2.3 连续值、缺失值和多分类:源码里最容易偷懒的三处
教学版决策树经常只处理离散特征、无缺失、二分类,这三处恰好是真实数据里最常见的。连续值处理的标准做法是排序后取相邻值中点作为候选切分点,选增益最大的那个;缺失值可以用权重分配法(C4.5 的做法),把样本按权重同时分到所有子节点,或者简单点直接填充;多分类只要把熵和基尼的计算从二类推广到 K 类即可,公式不变,只是求和范围变大。
看一份决策树源码值不值得细读,就看它有没有认真处理这三点。只支持离散二分类的,当教学demo看;支持连续值和剪枝的,才有对照价值。
3. 把 zip 跑起来:环境、数据加载与第一棵树的训练
3.1 环境准备与依赖确认
拿到源码包先别急着运行,第一步是确认 Python 版本和依赖。多数这类源码包基于 Python 3.8 以上,依赖 numpy、pandas、scikit-learn,画图可能用 matplotlib。建议单独建虚拟环境,避免和系统包冲突。
# 创建并激活虚拟环境 python -m venv dt_env source dt_env/bin/activate # Windows 用 dt_env\Scripts\activate # 安装核心依赖,版本按源码包 requirements 为准 pip install numpy pandas scikit-learn matplotlib逻辑说明:虚拟环境把这份源码的依赖和系统隔离,出问题直接删掉重建,不用收拾残局。参数说明:python -m venv后跟环境目录名,激活命令因操作系统而异。如果源码包里有requirements.txt,优先用pip install -r requirements.txt,版本冲突时再逐个降级。
3.2 数据加载与特征处理
决策树对特征缩放不敏感,不需要标准化,但类别特征要编码,缺失值要处理。下面这段是通用的加载和检查流程。
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split # 读取数据,注意编码,中文数据常见 gbk 或 utf-8 df = pd.read_csv("data.csv", encoding="utf-8") # 基本检查:形状、缺失、类别分布 print(df.shape) print(df.isnull().sum()) print(df.iloc[:, -1].value_counts()) # 假设最后一列是标签 # 类别特征独热编码,标签单独取出 X = pd.get_dummies(df.drop(columns=["label"])) y = df["label"] # 分层切分,保证训练测试集类别比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )逻辑说明:先看数据长什么样再动手,缺失和类别分布直接决定后面要不要填充、要不要用分层切分。参数说明:encoding按实际文件编码改,读进来乱码就换gbk;test_size=0.2是常见比例,数据少可以到 0.3;stratify=y在类别不平衡时必加,否则测试集可能缺某一类;random_state固定后结果可复现,调参时别改。
3.3 训练第一棵树并看关键参数
先用 scikit-learn 跑通基线,再对照源码包里的手写实现,理解每个参数的作用。
from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, classification_report # 初始化决策树,先给一组保守参数 clf = DecisionTreeClassifier( criterion="gini", # 分裂标准 max_depth=5, # 最大深度,防过拟合第一道闸 min_samples_split=10, # 节点样本数少于该值不再分裂 min_samples_leaf=5, # 叶子最少样本数 random_state=42 ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print("准确率:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred))逻辑说明:先给一组偏保守的参数拿到基线,再逐步放开看效果变化。参数说明:max_depth从 3 开始试,每加 2 看一次测试集;min_samples_split和min_samples_leaf越大树越简单,太小等于没限制;criterion分类用 gini 或 entropy,回归换成squared_error。如果源码包是手写实现,重点看它怎么递归选最优特征、怎么判断停止条件,这两处对应上面的参数。
3.4 可视化:把树画出来才算真看懂
决策树最大的优势是可解释,不画出来等于浪费。
from sklearn.tree import export_text, plot_tree import matplotlib.pyplot as plt # 文本形式,适合快速看规则 print(export_text(clf, feature_names=list(X.columns))) # 图形形式,适合放进报告 plt.figure(figsize=(16, 8)) plot_tree(clf, feature_names=list(X.columns), class_names=[str(c) for c in clf.classes_], filled=True, rounded=True, fontsize=9) plt.savefig("tree.png", dpi=150, bbox_inches="tight")逻辑说明:export_text输出 if-else 规则,方便直接抄进业务逻辑;plot_tree出图看结构。参数说明:filled=True按类别上色,颜色越深纯度越高;fontsize调小避免文字重叠;dpi提高导出清晰度。看树时重点看根节点用了哪个特征、深度是否过深、叶子样本数是否太少。
4. 参数怎么调:决策树的关键参数与调参顺序
4.1 必调参数清单与影响方向
| 参数 | 作用 | 调大后果 | 建议范围 |
|---|---|---|---|
| max_depth | 树最大深度 | 过拟合 | 3~15 |
| min_samples_split | 分裂所需最小样本数 | 欠拟合 | 2~50 |
| min_samples_leaf | 叶子最小样本数 | 欠拟合 | 1~30 |
| max_features | 每次分裂考虑的特征数 | 随机性增强 | sqrt / log2 / None |
| ccp_alpha | 后剪枝强度 | 树变小 | 0~0.1 试 |
| class_weight | 类别权重 | 偏向少数类 | balanced |
调参顺序我一般按影响从大到小:先max_depth,再min_samples_leaf,然后min_samples_split,最后ccp_alpha。max_features在单棵树里影响不大,主要是给随机森林用的。
4.2 用交叉验证代替单次切分
单次 train_test_split 的结果波动大,尤其数据量小的时候。用交叉验证看均值和方差更稳。
from sklearn.model_selection import GridSearchCV param_grid = { "max_depth": [3, 5, 7, 10, None], "min_samples_leaf": [1, 5, 10, 20], "criterion": ["gini", "entropy"] } grid = GridSearchCV( DecisionTreeClassifier(random_state=42), param_grid, cv=5, # 5 折交叉验证 scoring="f1_weighted", # 类别不平衡时比 accuracy 稳 n_jobs=-1 ) grid.fit(X_train, y_train) print("最优参数:", grid.best_params_) print("最优得分:", grid.best_score_)逻辑说明:网格搜索把参数组合逐个用交叉验证评估,选平均得分最高的。参数说明:cv=5是常用折数,数据少用 10;scoring按任务选,分类看 f1 或 roc_auc,回归看 neg_mean_squared_error;n_jobs=-1用满 CPU。注意max_depth=None配合交叉验证时可能很慢,先小范围试。
4.3 后剪枝:用 ccp_alpha 做一轮精修
预剪枝调完后,用代价复杂度剪枝再压一轮,往往能再降一点过拟合。
path = clf.cost_complexity_pruning_path(X_train, y_train) alphas = path.ccp_alphas # 对每个 alpha 训练一棵树,看测试集表现 scores = [] for a in alphas: t = DecisionTreeClassifier(random_state=42, ccp_alpha=a) t.fit(X_train, y_train) scores.append(t.score(X_test, y_test)) best_alpha = alphas[scores.index(max(scores))] print("最优 ccp_alpha:", best_alpha)逻辑说明:cost_complexity_pruning_path给出候选 alpha 序列,逐个训练选测试集最好的。参数说明:alpha 越大剪得越狠,树越小;alpha 为 0 等于不剪。注意这里用测试集选 alpha 有轻微信息泄露,严谨做法是嵌套交叉验证,但快速实验可以接受。
5. 避坑与排查:决策树源码包最常见的五个翻车点
5.1 现象:训练集准确率 100%,测试集只有 60%
原因:树长得太深,每个叶子记住个别样本,典型过拟合。解决:先设max_depth或min_samples_leaf,再用ccp_alpha后剪枝,同时检查特征里有没有 ID 类唯一标识列,有就删掉。
5.2 现象:源码包运行报 FileNotFoundError 或路径错误
原因:源码里写死了绝对路径,或者数据文件相对路径和你的运行目录不一致。解决:统一用os.path.dirname(__file__)拼相对路径,或者把数据文件放到和脚本同级目录,运行时确认当前工作目录。
5.3 现象:中文标签或特征读进来乱码
原因:文件编码和read_csv的encoding参数不匹配。解决:先试utf-8,乱码换gbk或gb18030;实在不确定用chardet检测。写文件时同样指定encoding="utf-8-sig",避免 Excel 打开乱码。
5.4 现象:类别不平衡,准确率虚高但少数类全错
原因:多数类占比高,模型全预测多数类也能拿高准确率。解决:看classification_report的 recall 和 f1,别只看 accuracy;设class_weight="balanced",或者对少数类过采样;评估指标换成 f1 或 auc。
5.5 现象:手写源码和 sklearn 结果对不上
原因:分裂标准实现细节不同,比如连续值切分点取法、平局时选哪个特征、停止条件差异。解决:先在小数据集上逐步打印每层分裂的特征和阈值,和 sklearn 的export_text对照;确认双方用的是同一个 criterion 和同样的停止条件。对不上不一定是 bug,可能是实现约定不同。
6. 从单棵树到集成:把这份源码用出更高价值的一个技巧
单棵决策树方差大、容易过拟合,真正在比赛和生产里能打的是基于它的集成方法。你手里这份决策树源码,最大的价值不是直接拿去预测,而是当作理解随机森林和梯度提升树的跳板。随机森林用 bagging 思路训练多棵树再投票,把方差降下来;梯度提升树用 boosting 思路,每棵新树拟合前面模型的残差,把偏差降下来。两者的基学习器都是决策树,你把单棵树的参数和剪枝搞明白,调集成模型时就知道每个参数在动什么。
一个具体技巧:用feature_importances_做特征筛选,再喂给集成模型。决策树给出的特征重要性虽然对高基数特征有偏好,但作为第一轮粗筛足够快。
import pandas as pd from sklearn.ensemble import RandomForestClassifier # 用单棵树的重要性做初筛 importances = pd.Series(clf.feature_importances_, index=X.columns) selected = importances[importances > 0.01].index.tolist() print("保留特征数:", len(selected)) # 用筛选后的特征训练随机森林 rf = RandomForestClassifier( n_estimators=200, # 树的数量,越多越稳但越慢 max_depth=8, min_samples_leaf=3, random_state=42, n_jobs=-1 ) rf.fit(X_train[selected], y_train) print("随机森林准确率:", rf.score(X_test[selected], y_test))逻辑说明:先用单棵树的重要性去掉几乎不贡献的特征,减少噪声和维度,再训练随机森林。参数说明:n_estimators从 100 起,到 500 后收益递减;max_depth在集成里可以比单棵树深一些,因为投票会抵消部分过拟合;n_jobs=-1并行训练。注意特征重要性阈值别设太高,0.01 只是粗筛,筛完最好再用交叉验证确认效果没掉。
验证方法上,我习惯做三件事:一是对比筛选前后交叉验证得分,掉了就说明筛过头;二是看随机森林的 OOB 得分(oob_score=True),它相当于免费的验证集;三是把最终模型的特征重要性和单棵树对比,如果排序差异很大,说明特征间有较强相关,需要进一步处理。
我自己踩过最深的一个坑,是早期拿到一份决策树源码,没看停止条件就直接套到一份几万行的数据上,结果树长到几千个节点,训练跑了十几分钟,测试集一塌糊涂。后来养成习惯:任何树模型先卡深度、先看数据分布、先跑小样本。源码包是起点不是终点,把它当教材逐行读一遍,比直接调库收获大得多。希望帮到你。
本文还有配套的精品资源,点击获取