机器学习学习笔记(五):决策树,CART 原理、泰坦尼克生存预测与剪枝

发布时间:2026/10/7 8:36:51
机器学习学习笔记(五):决策树,CART 原理、泰坦尼克生存预测与剪枝 系列第五篇。决策树是可解释性最好的模型每一层都是一个是/否问题也是集成学习随机森林、GBDT、XGBoost的基石下一篇就要用它。这篇的重点有两个CART 树的分裂逻辑基尼系数以及泰坦尼克案例里被很多人忽略的数据预处理和调参细节。本篇看完要回答的四个面试题决策树怎么选分裂特征基尼系数是什么CART 树和 ID3、C4.5 有什么区别决策树过拟合怎么办预剪枝和后剪枝的区别回归树是怎么输出连续值的一、决策树一串是/否问题决策树是一种树形结构内部节点是一个特征上的判断分支是判断结果的输出叶子节点是最终的分类结果。课件里的例子是女孩相亲第一个问题问年龄年龄 特定值直接不见第二个问长相第三个问收入最后问是否公务员。每个问题都在缩小范围走到底就得出结论。值得注意的是问题的先后顺序她把年龄放在第一个问。先问哪个特征本质上是在问哪个特征最能把我关心的人群分开这正是决策树构建的核心问题。决策树的建立过程分三步特征选择选取分类能力强的特征决策树生成根据选择的特征递归生成树剪枝决策树容易过拟合用剪枝缓解二、CART 树所有主流树模型的老祖宗CART 全称 Classification and Regression Tree分类与回归树规则可以概括成一句话不停做二分选择题一层一层筛选数据分得越纯越好。2.1 为什么是 CART前辈的局限在 CART 之前有 ID3 和 C4.5 两代算法它们有三个问题局限说明功能单一只支持分类无法做回归容易过拟合学了太多训练数据的细节噪声分裂规则不统一时而二分时而多分计算逻辑不规范CART 的解法是全部统一既能分类又能回归强制二分自带剪枝机制。2.2 核心规则一每次只做二分无论选哪个特征划分都只能把数据分成两组。类别特征色泽不允许一次分出青绿、乌黑、浅白三组只能问是青绿吗或是乌黑吗数值特征重量只能在数轴上切一刀“大于 5 斤和小于等于 5 斤”2.3 核心规则二选分得最纯的特征划分的目标是让分开后的两组数据内部尽可能一致。一堆里绝大多数是好瓜、另一堆绝大多数是坏瓜说明这个特征有用。衡量纯度的指标是基尼系数Gini(D)1−∑k1Kpk2Gini(D) 1 - \sum_{k1}^{K} p_k^2Gini(D)1−k1∑K​pk2​其中pkp_kpk​是第 k 类样本的占比。基尼系数越小数据越纯。两类数据的极端情况很好算五五开时 Gini 0.5最混乱全是同一类时 Gini 0最纯。用泰坦尼克数据的根节点算一下712 个训练样本442 人遇难、270 人生存。Gini1−(442712)2−(270712)20.471Gini 1 - \left(\frac{442}{712}\right)^2 - \left(\frac{270}{712}\right)^2 0.471Gini1−(712442​)2−(712270​)20.471接近 0.5说明根节点处人群几乎对半开非常混乱。2.4 核心规则三逐层递归分裂对每个候选特征都尝试二分计算划分后两组的加权基尼系数选让纯度提升最多的那个。以泰坦尼克的第一层为例实测数据下一节会讲怎么来的按是否男性划分后男性组 455 人377 遇难88 生存Gini 0.307女性组 247 人65 遇难182 生存Gini 0.388。加权平均455×0.307247×0.3887120.331\frac{455 \times 0.307 247 \times 0.388}{712} 0.331712455×0.307247×0.388​0.331从 0.471 降到 0.331纯度明显提升所以根节点选了这个特征。分出两组后在每组内部重复同样的操作直到叶子足够纯或达到停止条件。2.5 核心规则四回归问题怎么办分类树的叶子输出类别叶子中多数类回归树的叶子输出数值叶子中所有样本目标值的均值。分裂逻辑不变只是纯度的衡量从基尼系数换成了平方误差让切出来的两堆各自内部的房价或其他目标值差异尽量小。2.6 优缺点优点缺点简单易懂决策过程清晰可见可解释性强容易过拟合参数不当时单棵树非常明显分类回归都能做功能全面结果不稳定数据微小扰动可能让整棵树结构剧变强制二分训练和预测速度快贪心策略每步只找局部最优不保证全局最优自带剪枝也是随机森林、GBDT、XGBoost 的基础模块缺点里的不稳定和贪心不是背书用的第四节实测会亲眼看到数据只动一点最优深度和准确率就跟着跳。三、泰坦尼克生存预测重点在数据预处理案例背景1912 年泰坦尼克号沉没2224 人中 1502 人遇难。救生艇不够但妇女、儿童和上流社会更容易生存。我们要用乘客数据预测谁能幸存。数据集 891 行用三个特征Pclass船票等级 1/2/3、Age年龄、Sex性别标签是Survived0 遇难1 生存。这个案例的重点不是决策树本身API 一行就完而是中间的数据预处理一共三件事缺失值处理Age列有 177 个缺失用均值填充类别特征转数值Sex是字符串模型不认识用pd.get_dummies做 one-hot 编码展开成Sex_female和Sex_male两列 0/1数据集划分8 : 2 切分训练集和测试集3.1 第一课不限深度的树长什么样先按课件原样训练一棵不做任何限制的决策树importpandasaspdfromsklearn.model_selectionimporttrain_test_splitfromsklearn.treeimportDecisionTreeClassifierfromsklearn.metricsimportclassification_report titan_dfpd.read_csv(./data/titanic/train.csv)xtitan_df[[Pclass,Age,Sex]]ytitan_df[Survived]x[Age]x[Age].fillna(x[Age].mean())xpd.get_dummies(x)x_train,x_test,y_train,y_testtrain_test_split(x,y,test_size0.20,random_state33)estimatorDecisionTreeClassifier(random_state33)estimator.fit(x_train,y_train)结果指标数值训练集准确率0.8750测试集准确率0.8492树的深度16叶子数115712 个训练样本被切成 115 个叶子很多叶子里只剩一两个人。这就是过拟合的形态树把训练集里大量偶然细节都当成了规则。测试集准确率看起来还行但训练集和测试集的差距、以及夸张的叶子数都说明它学了不该学的东西。3.2 第二课限深之后发生了什么限制max_depth从 2 到 10 逐个试每组都固定random_state33max_depth训练集测试集20.79780.782130.80760.815640.81040.815650.82300.804560.82720.810170.84410.843680.85390.849290.85530.8547100.86100.8268两个观察训练集准确率随深度单调上升测试集不是。这就是欠拟合/过拟合的完整曲线浅了欠拟合深度 2 只有 0.78深了过拟合深度 10 反而掉到 0.8268测试集分数从深度 9 到 10 直接掉了近 3 个百分点这就是第二节说的结果不稳定树的分裂路径对数据和深度极其敏感3.3 第三课我差点犯的数据泄漏错误看到上表很自然的反应是那选测试集分数最高的深度 9 就完事了。错这是数据泄漏。测试集的作用是最终评估不是选参数。用测试集选超参数等于让模型提前看过考试题再挑答案选出来的分数是虚高的深度 9 的 0.8547 就有这个成分。正确做法是在训练集内部做交叉验证来选参数fromsklearn.model_selectionimportcross_val_scorefordepthinrange(2,11):mDecisionTreeClassifier(max_depthdepth,random_state33)scorescross_val_score(m,x_train,y_train,cv5)print(fmax_depth{depth}5折交叉验证{scores.mean():.4f})max_depth5 折交叉验证20.782330.797840.787950.778260.776770.785280.783890.7852100.7852交叉验证选出的最优深度是30.7978而不是偷看测试集得到的 9。它的真实测试集成绩bestDecisionTreeClassifier(max_depth3,random_state33)best.fit(x_train,y_train)y_predbest.predict(x_test)print(classification_report(y_test,y_pred,target_names[died,survived]))指标数值测试集准确率0.8156树的深度3叶子数8precision recall f1-score support died 0.82 0.88 0.85 107 survived 0.80 0.72 0.76 72 accuracy 0.82 179诚实地说交叉验证选出的模型0.8156在这一次测试上确实低于作弊选出的 0.8547。但这不矛盾0.8547 是提前看过考题挑出来的数字换一批测试集大概率没这么好0.8156 才是模型真实泛化能力的合理估计。深度 3 的树只有 8 个叶子在可解释性和泛化之间取得了好平衡面试里讲清楚这个取舍是加分项。3.4 这棵树到底学到了什么用plot_tree画出深度 3 的树完整图见项目仓库这里说结论根节点问的第一个问题是是否男性Sex_female 0.5。算法在所有特征里自动把性别排在了第一优先级男性分支第二个问题问年龄Age 5.5男孩里 5 岁以下优先获救女性分支第二个问题问船票等级Pclass 2.5一二等舱的女性优先获救妇女儿童优先 舱位差异这个历史叙述被数据完整复现了。看特征重要性更直接特征重要性Sex_female0.6621Pclass0.2431Age0.0947Sex_male0.0000Sex_male的重要性是 0one-hot 编码产生了两列完全互斥的特征一列是 1 另一列必是 0树只需要用其中一列就够另一列的信息量为零。one-hot 后出现某个哑变量重要性为 0 是正常现象不是 bug。四、回归树实战加州房价课件的回归树案例还在用波士顿房价但load_boston从 sklearn 1.2 起已被移除线性回归篇讲过的老坑这里同样换fetch_california_housing。回归树 API 是DecisionTreeRegressor流程和分类树一致fromsklearn.datasetsimportfetch_california_housingfromsklearn.model_selectionimporttrain_test_splitfromsklearn.preprocessingimportStandardScalerfromsklearn.treeimportDecisionTreeRegressorfromsklearn.metricsimportmean_squared_error,mean_absolute_errorimportnumpyasnp datafetch_california_housing()x_train,x_test,y_train,y_testtrain_test_split(data.data,data.target,random_state22)scalerStandardScaler()x_trainscaler.fit_transform(x_train)x_testscaler.transform(x_test)mDecisionTreeRegressor(max_depth8,random_state22)m.fit(x_train,y_train)y_predm.predict(x_test)msemean_squared_error(y_test,y_pred)print(np.sqrt(mse),mean_absolute_error(y_test,y_pred))深度 8 时MSE 0.4196RMSE 0.6478MAE 0.4525。对比线性回归篇正规方程的 MSE 0.4918单棵回归树在这个数据上比线性回归还准特征关系有非线性成分树能拟合出来。更有价值的是深度对照实验max_depth训练集 MSE测试集 MSE40.55810.539560.44550.4464100.22760.4142不限0.00000.5274不限深度时训练集 MSE 正好是 0树把每个训练样本都分到了独享的叶子里“预测退化成背答案”。测试集 MSE 随之从 0.4142深度 10 的最优附近恶化到 0.5274。训练误差 0 永远不是好事它是过拟合最极端的样子。另外注意回归树对特征做了标准化但严格说树模型对单调缩放不敏感分裂只看相对大小这里做标准化是沿用流程习惯不影响结论。五、剪枝给树减负剪枝是一种防止决策树过拟合的正则化方法思路是把子树删掉、用叶子节点替换牺牲一点训练集准确率换泛化能力。两种方式预剪枝边生成边判断。每个节点分裂前先估计如果这次划分不能带来泛化性能提升就停止划分、直接标记为叶节点。上文用max_depth限深就是最常用的预剪枝手段。后剪枝先让树完整生成再自底向上考察每个非叶节点把某节点对应的子树替换为叶节点后如果泛化性能提升通常用验证集判断就剪掉。优点缺点预剪枝分支少训练和测试的时间开销显著降低有些分支当前看着没用、后续分裂可能显著提升性能提前停有欠拟合风险后剪枝保留更多分支欠拟合风险小泛化性能通常优于预剪枝先生成再遍历训练开销大sklearn 的DecisionTreeClassifier里max_depth、min_samples_split节点再划分所需最小样本数、min_samples_leaf叶子节点最少样本数这些都是预剪枝参数后剪枝可用ccp_alpha代价复杂度剪枝实现本文实验以预剪枝为主。课件里那道多选题值得记一下剪枝防过拟合、分预剪枝和后剪枝、预剪枝是边生成边验证、后剪枝是构建完再遍历四个说法全对。六、易错点用测试集调超参数就是数据泄漏3.3 节的完整教训。调参只能用训练集内部的交叉验证测试集只在最终评估时碰一次one-hot 之前必须先处理缺失值如果先get_dummies再填充缺失值会被当成一个独立类别处理逻辑上就错了。本案例顺序是先fillna再get_dummiespandas 的inplaceTrue链式赋值在新版本会报 FutureWarning 甚至失效课件里的x[Age].fillna(..., inplaceTrue)写法已经过时推荐x[Age] x[Age].fillna(...)本文代码已用新写法不限深度的决策树在训练集上 MSE 可以是 0看到训练误差为 0 不要以为是模型很强先看测试集classification_report的参数顺序classification_report(y_test, y_pred)真实值在前预测值在后。传反了报告照样能生成但精确率和召回率就全部错位这个 bug 很隐蔽树模型不需要标准化对单调缩放不敏感但需要做类别特征编码标准化在树模型流程里是可做可不做在 KNN、线性回归流程里是必须做七、知识清单决策树结构根节点、内部节点、分支、叶子节点建树三步特征选择、生成、剪枝CART 五条核心规则强制二分、选最纯分裂、逐层递归、叶子输出类别 / 均值、剪枝基尼系数Gini1−∑pk2Gini 1 - \sum p_k^2Gini1−∑pk2​越小越纯五五开 0.5纯一类 0ID3 / C4.5 局限不能回归、易过拟合、分裂规则不统一预剪枝参数max_depth、min_samples_split、min_samples_leaf后剪枝ccp_alpha回归树 APIDecisionTreeRegressor评估用 MSE / RMSE / MAE调参纪律交叉验证选参测试集只做最终评估八、面试高频问答Q1决策树如何选择分裂特征计算每个候选特征划分后的不纯度分类用基尼系数或信息熵回归用平方误差选使不纯度下降最多的特征和分裂点。CART 强制每次二分逐层递归直到满足停止条件。Q2CART 和 ID3、C4.5 的区别ID3 用信息增益、只能处理离散特征、不能回归C4.5 用信息增益比、能处理连续特征、仍不能回归CART 用基尼系数分类或平方误差回归、强制二分、分类回归通吃且是随机森林和 GBDT 的基础模块。Q3决策树过拟合怎么解决剪枝预剪枝限制生长如 max_depth、min_samples_leaf后剪枝先生成再自底向上裁剪、集成学习把单棵不稳定大树换成多棵小树的随机森林下一篇的主题、以及增加训练数据。Q4预剪枝和后剪枝的区别与取舍预剪枝边生成边判断省时间但可能提前砍掉后期有价值的分支有欠拟合风险后剪枝先生成完整树再自底向上考察效果通常更好但训练开销大。工程上 sklearn 默认走预剪枝参数追求性能时配合交叉验证调参。