☰
决策树模型实战:从分裂准则到网格搜索调参
2026/10/9 2:41:51 网站建设 项目流程

简介:决策树作为机器学习和数据挖掘中常用的监督学习算法,本压缩包系统收录其模型构建与实战源码,适合正在学习机器学习基础或希望掌握分类、回归建模的开发者。包内共28个文件,包含10个Python脚本、6个Jupyter Notebook、4个PDF讲义、3个TXT说明以及2个Excel数据集、2张PNG可视化图等,涵盖从原理讲解、代码实现到员工离职预测案例和K折交叉验证、网格搜索参数调优的完整流程,PyCharm与Notebook双格式便于直接运行。已有526人学习下载。通过研读源码,读者能理解决策树ID3、C4.5、CART的分裂与剪枝逻辑,掌握sklearn建模、评估及可视化方法,并借助案例数据完成从特征处理到模型优化的实战演练,为后续集成学习打下基础。

1. 决策树模型源码包:从离职预测案例看懂分类算法的完整链路

做员工离职预测这类HR分析时,很多人第一反应是上逻辑回归,但真要给业务讲清楚“为什么这个人会走”,决策树的可解释性比黑匣子式的模型好用太多。这份“机器学习与算法源代码5:决策树模型.zip”把决策树的原理代码、员工离职预测实战、K折交叉验证和GridSearch网格搜索调参整合在一条完整链路里,同时提供Jupyter Notebook和PyCharm两种格式,是少有的能把机器学习算法从理论一路跑到可视化的源码包。适合正在学决策树、想复现分类任务,或者准备在项目里做模型预研的从业者。

2. 决策树分裂准则:信息增益、信息增益率与基尼不纯度的选型逻辑

2.1 三个分裂准则:为什么CART成了sklearn的默认选择

决策树的核心问题只有一个:每一个节点该用哪个特征、按什么阈值去分裂。回答这个问题靠的就是分裂准则。源码包里5.1节专门讲了这一块,对应的PDF文档把ID3、C4.5、CART三者的公式推导写得很清楚,代码部分则用sklearn的DecisionTreeClassifier把“选特征”这件事封装成了一行调用。

ID3基于信息增益选特征,哪个特征让数据的信息熵下降最多就选哪个。它的毛病是偏好取值多的离散特征,比如员工ID这种唯一值特征,一拆下去每个分支只剩一个样本,熵直接归零,但这样的树毫无泛化能力。C4.5改用信息增益率,在信息增益基础上除以特征自身的熵,相当于对取值过多的特征做了惩罚,还顺带支持了连续特征。

CART用的是基尼不纯度(Gini impurity),它衡量的是“从节点里随机抽两个样本,它们的类别不一致的概率”。基尼不纯度越低,节点越纯。sklearn的DecisionTreeClassifier默认criterion='gini',就是这个原因:基尼计算比熵快,没有对数运算,分类和回归任务都能用同一套框架。实际项目里我一般不会纠结选哪个准则,默认gini,只有遇到特征取值极多且业务意义不明的离散变量时才切回entropy对比一下。

2.2 用源码包里的熵计算代码理解“不纯度在下降”

5.1的源码里有一段手写熵计算的实现,虽然短,但它是理解整个树构建过程的钥匙。信息熵公式是 -Σp·log₂(p),代码实现如下:

import numpy as np def entropy(y): # 统计每个类别出现的次数,再转成概率 _, counts = np.unique(y, return_counts=True) p = counts / counts.sum() # 信息熵 = -sum(p * log2(p)),纯度高时熵趋近于0 return -np.sum(p * np.log2(p))

这段逻辑的关键在于np.unique把标签列y去重并计数,然后除以总数得到每个类别的概率p。比如离职预测里标签是0和1,如果5000个样本里2500人离职,熵就是1,说明节点完全混沌;如果4500人没离职,熵会明显变小,说明节点已经比较纯了。决策树做的就是在每个节点找一个特征,让分裂后的加权熵最小,这就是“不纯度下降”的数值含义。

配套的还有信息增益计算函数,按特征取值把数据切到不同分支,加权求和子节点的熵,再用父节点熵减去它:

def info_gain(x, y, base_entropy): # x是某个特征的取值,y是标签 values = np.unique(x) weighted_entropy = 0 for v in values: subset_y = y[x == v] # 子节点熵按样本量加权 weighted_entropy += len(subset_y) / len(y) * entropy(subset_y) return base_entropy - weighted_entropy

这段代码的实用价值在于,当你想验证sklearn内部到底在做什么时,可以用它手算某个特征的信息增益,再和模型输出的feature_importances_对比。参数上需要注意len(subset_y)/len(y)这一项是样本量权重,不是等权平均,这保证了样本多的分支在评价中占更大比重。

2.3 dot_data.txt里能看到什么:DOT源文件与PNG渲染的关系

压缩包里出现了dot_data.txt、dot_data_new.txt、决策树模型.png、决策树模型_new.png,很多初学者搞不清它们的关系。树的text格式和png格式其实是同一条可视化管线的两个产物:先用tree.export_graphviz把模型导出为DOT语言的文本,再用graphviz把DOT文本渲染成图片。DOT是一种图描述语言,节点、边、标签都以纯文本形式存在。

dot_data.txt就是中间产物,它记录的是每个节点的分裂条件、样本数、基尼系数和类别分布。我拿到这种文件的第一反应是直接打开搜“max_depth”相关的层级关系,看树是否被剪枝过,而不是急着渲染图片。因为DOT文本里的节点深度一目了然,比看图更快发现问题。

graphviz插件的安装及使用说明在包里是一份PDF,里面覆盖了Windows下安装graphviz、配置PATH、校验是否安装成功这几个步骤。如果你在Jupyter里跑graphviz.Source(dot_data)时提示找不到可执行文件,大概率是系统PATH没配置好,这个坑我在第5章会展开写。

3. 员工离职预测实战:从xlsx读表到导出可视化决策树

3.1 读入“员工离职预测模型.xlsx”:数据预处理与拆分

这个案例用的是员工离职预测模型.xlsx,典型的HR分析数据集。特征一般包括满意度、考核分数、项目数量、平均工时、工龄、工资等级、部门、岗位等,标签是员工是否离职。源码里5.2.1的Python脚本对应Notebook里的实战步骤,核心代码从pandas读表开始:

import pandas as pd from sklearn.model_selection import train_test_split # 读取Excel数据 df = pd.read_excel('员工离职预测模型.xlsx', engine='openpyxl') # 假设最后一列是是否离职的标签,其余列为特征 X = df.iloc[:, :-1] y = df.iloc[:, -1] # 把类别型特征转成数值型 for col in X.select_dtypes(include=['object']).columns: X[col] = pd.factorize(X[col])[0] # 按7:3切分训练集和测试集,固定随机种子保证可复现 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y )

这里engine='openpyxl'是读xlsx的关键参数,pandas 1.2之后的版本默认引擎不再支持xlsx,不带这个参数很容易报错。pd.factorize把“部门”“岗位”这类字符串转成0、1、2这样的整数编码,对决策树来说足够了,不需要像线性模型那样做one-hot。train_test_split里的stratify=y是分层抽样,保证训练集和测试集里的离职比例与原始数据一致,分类任务里这是常规操作。

这段跑完之后要注意检查X_train里有没有缺失值,决策树虽然对缺失值有一定容忍度,但sklearn的实现并不会自动填充。一般做法是先看df.isnull().sum(),有缺失就按列均值或众数填充。

3.2 用graphviz导出可视化决策树:关键参数一次配齐

模型训练本身不复杂,默认的DecisionTreeClassifier一行就能拟合。真正容易卡住的是可视化导出环节,源码包的5.2.2脚本里完整写了导出和渲染流程:

from sklearn.tree import DecisionTreeClassifier, export_graphviz import graphviz # 训练决策树分类器,max_depth设为4,控制树的复杂度 clf = DecisionTreeClassifier( criterion='gini', max_depth=4, min_samples_split=10, random_state=42 ) clf.fit(X_train, y_train) # 导出DOT格式文本 dot_data = export_graphviz( clf, feature_names=X.columns, # 特征名,用于图中显示 class_names=['在职', '离职'], # 类别名 filled=True, # 节点按纯度着色 rounded=True, # 节点圆角 special_characters=True ) # 渲染并保存为png文件 graph = graphviz.Source(dot_data) graph.render('决策树模型_new', format='png')

export_graphviz里feature_names和class_names必须传对,否则图上显示的是x[0]、x[1]这种索引,业务方根本看不懂。filled=True让节点颜色随纯度渐变,纯度高的是浅色,纯度低的是深色,一眼能看出哪些分支还在“混着”。min_samples_split=10表示内部节点至少要有10个样本才允许继续分裂,这是最早生效的剪枝防线。

渲染完成后,源码包里出现的“决策树模型.png”和“决策树模型_new.png”就是graph.render的输出结果。render函数第一个参数是文件名前缀,format='png'控制输出格式,默认还会同时生成一个PDF文件,这就是result.pdf的来源。

3.3 读一棵训练好的树:根节点的分裂条件最有业务价值

很多人把决策树图渲染出来就完事了,其实读树才是这个案例最有价值的部分。根节点是第一个分裂点,它在全部特征里找到了区分度最高的那个。在员工离职场景里,根节点往往是满意度或工龄,这非常符合直觉:满意度低、工龄短的员工离职风险高。

读树的时候我习惯按三条线走:第一看根节点分裂条件,这是全局最重要的特征;第二看每个叶子节点的样本量和类别占比,样本数特别少的叶子通常是过拟合信号;第三看有没有“某个特征在多个层级重复出现”,如果工龄出现在根节点又出现在第三层,说明这个特征对预测的贡献是分段式的。源码包的PDF文档末尾附了这几个解读维度,和图形化的树配合起来,可以直接输出给HR部门当结论用。

4. 参数调优:K折交叉验证与GridSearch网格搜索的完整流程

4.1 为什么默认参数容易过拟合:从剪枝算法视角看树深

不调参的决策树会把训练集学到极致,树的深度可以涨到二十多层,每个叶子节点只剩一两个样本。叶子节点sample=1意味着模型把训练数据里的噪声也背下来了。剪枝算法要解决的就是这个问题,常见的手段有两类:预剪枝在树生长时提前停止,后剪枝等树长完整再从下往上砍分支。

sklearn的DecisionTreeClassifier没有提供独立的剪枝函数,所有控制都在构造参数里。max_depth限制最大深度,min_samples_split限制分裂所需的最小样本数,min_samples_leaf限制叶子节点最小样本数,max_leaf_nodes限制叶子总数。这四个参数配合使用就是完整的预剪枝策略。

我一般先把max_depth设成3跑一个基线,再看测试集准确率,然后逐步加深。如果深度从3涨到5时测试集分数明显上升,涨到7时反而下降,说明过拟合点就在5到7之间。源码包5.3节的参数调优就是围绕这几个参数展开的,用网格搜索把“最优深度”这件事自动化。

4.2 K折交叉验证:为什么一份数据要拆成五份轮流验证

单次train_test_split的评估结果依赖随机划分,换一个random_state分数可能差好几个百分点。K折交叉验证把训练集均分成K份,每次拿K-1份训练、1份验证,轮流做K次,最终分数取平均。员工离职案例里样本量大概几千条,K=5是常用配置,既能保证每折训练集够大,又让验证分数足够稳定。

sklearn里K折不用自己写循环,GridSearchCV内部就把这事做了。cv=5的含义是:每一组参数组合都要训练5次,取平均分数作为这一组参数的最终评估值。配合GridSearchCV,调参流程变成“列出参数候选值,让框架自动跑完所有组合”。

4.3 GridSearchCV网格搜索:参数网格怎么设、结果怎么读

源码包的5.3节是参数调优的核心,代码把max_depth、min_samples_split、min_samples_leaf放进一个param_grid里暴力搜索:

from sklearn.model_selection import GridSearchCV # 定义候选参数组合 param_grid = { 'max_depth': [3, 5, 7, 9], 'min_samples_split': [2, 5, 10, 20], 'min_samples_leaf': [1, 2, 4, 8] } # 网格搜索:5折交叉验证,用roc_auc评估,多核并行 grid = GridSearchCV( DecisionTreeClassifier(random_state=42), param_grid, cv=5, scoring='roc_auc', n_jobs=-1 ) grid.fit(X_train, y_train) # 输出最优参数和对应的验证分数 print(grid.best_params_) print(grid.best_score_) # 用最优参数的模型在测试集上做最终评估 best_clf = grid.best_estimator_ test_score = best_clf.score(X_test, y_test) print('test accuracy:', test_score)

param_grid里三个键对应三组候选值,组合总数是4×4×4=64组,每组跑5折,等于要训练320次。decision tree本身训练很快,这个体量几秒到十几秒就能跑完,n_jobs=-1让所有CPU核心并行处理。scoring='roc_auc'比准确率更稳,因为员工离职数据里离职样本通常占比偏低,准确率容易被“全预测在职”这种偷懒策略拉高。

跑完之后不要只盯着best_params_,还要看cv_results_里不同参数组合的分数分布。如果最优参数的分数比倒数第二好不了多少,选更简单的模型更稳妥。源码包5.3.2的脚本里把cv_results_按分数排序输出,就是为了观察这种梯度变化。

5. 复现避坑记录:graphviz安装、中文乱码与版本兼容的五条踩坑

5.1 graphviz报ExecutableNotFound:Python包装了,系统程序没装

现象:在Jupyter里执行graphviz.Source(dot_data)时报错ExecutableNotFound: failed to execute ['dot'],但pip install graphviz明明已经成功了。

原因:pip安装的graphviz只是Python调用库,真正执行渲染的是graphviz官方的系统程序。两者是独立的,缺了后者就会出现这个错误。源码包里专门放了一份graphviz插件的安装及使用说明,就是因为在学员复现时这个坑出现频率最高。

解决:去graphviz官网下载Windows安装包,安装时勾选“Add Graphviz to System PATH”,装完后重启终端。如果PATH没生效,可以在代码里手动指定路径:

import os os.environ["PATH"] += os.pathsep + 'C:/Program Files/Graphviz/bin/'

这行代码要放在import graphviz之前,路径以实际安装目录为准。

5.2 导出的决策树图片中文全变方块

现象:export_graphviz里class_names=['在职', '离职'],渲染出来的PNG中所有中文都显示成方框。

原因:graphviz默认字体不支持中文字符,DOT文本里的中文标签没有可用的字形去渲染。

解决:在export_graphviz里加fontname参数指定中文字体,Windows下用SimHei或Microsoft YaHei:

dot_data = export_graphviz( clf, feature_names=X.columns, class_names=['在职', '离职'], filled=True, fontname='Microsoft YaHei' )

我一般还会配合设置graphviz的全局字体环境变量,但fontname参数是覆盖最直接的。如果你用的是Mac,改成‘PingFang SC’。

5.3 pandas.read_excel读xlsx报错:引擎不兼容

现象:执行pd.read_excel('员工离职预测模型.xlsx')抛异常,提示Install xlrd >= 2.0.1 or use engine='openpyxl'。

原因:xlrd库升级到2.0之后不再支持xlsx格式,只保留xls。而pandas在没有显式指定engine时,会优先尝试xlrd。源码包里这份数据恰好是xlsx,直接踩中版本断层。

解决:两种方案任选,我习惯在read_excel显式指定引擎:

df = pd.read_excel('员工离职预测模型.xlsx', engine='openpyxl')

同时确保环境里装了openpyxl,pip install openpyxl。如果你自己造测试数据时另存为xls格式,则不需要改引擎。

5.4 GridSearchCV跑得极慢:参数组合数比想象中大得多

现象:param_grid里设了5个参数,每个给5个候选值,跑了几分钟还在fit,笔记本风扇狂转。

原因:组合数是5的5次方等于3125组,乘以cv=5折就是15625次训练。决策树虽然单次训练快,但上万次累计起来照样要等很久。源码包里的网格只放了3个参数,每个不超过4个值,就是避免组合爆炸。

解决:先粗后细两轮搜索。第一轮用大步长覆盖范围,比如max_depth设[3, 5, 7, 9],第二轮在最优值附近加密,比如第一轮选到5,第二轮改成[4, 5, 6]。另外把n_jobs设置成-1并行化,但要注意CPU核数,核少的时候并行提升有限。

5.5 测试集分数和网格搜索分数对不上:数据提前泄露

现象:GridSearchCV交叉验证分数是0.92,在测试集上复算只有0.78,差距大到不合理。

原因:最常见的情况是预处理在train_test_split之前整体做了,比如对全量数据做缺失值填充或标准化,导致验证折里混入了训练折的数据分布信息。某些特征如员工ID在案例里没删干净,树会直接拿它当分裂特征,训练时几乎完美,测试时失效。

解决:先拆分再预处理,所有fit操作只允许在训练集上执行。检查X里有没有唯一值特征,有就drop掉。验证方法是对照clf.feature_importances_,排名第一的特征如果是员工编号这类无业务含义的字段,基本就是泄露了。

6. 把剪枝和特征重要性当成模型验证的最后一环

网格搜索找到最优参数后,我还会再做一步:用feature_importances_确认模型依据的特征在业务上说得通。决策树的特征重要性是每个特征在分裂时带来的不纯度下降量加权求和,归一化后的结果可以直接输出成一份排序表:

import pandas as pd # best_clf是网格搜索得到的最优模型 importance = pd.Series( best_clf.feature_importances_, index=X_train.columns, name='importance' ) importance.sort_values(ascending=False).head(10)

在员工离职案例里,排名前三的特征通常是满意度、工龄和平均每月工时。如果模型把某个无关紧要的字段排到第一,我不会直接采信,而是回头查数据,判断是泄露还是偶然。除此之外,我还会用apply方法取个别样本的叶子节点路径,解释单条预测的依据:

# 取测试集第一条样本,返回它落到的叶子节点ID leaf_id = best_clf.apply(X_test.iloc[[0]]) print(leaf_id)

这个叶子节点ID对应决策树图里的一个具体节点,顺着根节点到该叶子的路径,就能说出“这条样本预测离职是因为满意度低于0.3且工龄不满2年”。这是决策树区别于其他机器学习算法最实用的地方:每一跳都能变成人话。从那以后,我做决策树项目都强制把这两步走一遍——先看特征重要性排序是否合理,再用apply抽三条样本验证路径解释,多花五分钟,省掉给业务方解释黑匣子的两小时。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询