2026/11/2 14:44:18

月亮数据二分类实战:决策树与随机森林的对比与调参

月亮数据二分类实战:决策树与随机森林的对比与调参 简介一份围绕“月亮数据预测”的机器学习项目压缩包内含决策树与随机森林两种算法的完整实现适合有基础Python经验的开发者和数据科学爱好者学习分类建模与预测实战也可作为教学演示或毕业设计参考。解压后共13个文件主要包括3个Python脚本决策树分类器与随机森林分类器、2个.pyc编译缓存、5个XML配置及工程文件、1份DOC说明文档和1个.gitignore其中DOC文档对两个算法的原理、适用场景和项目结构做了系统梳理包体约1.23MB体积小巧便于下载与复现实验。目前平台已有874人学习下载该项目从数据收集、缺失值处理、特征编码与缩放、特征选择到模型训练、交叉验证和超参数调整完整覆盖标准机器学习流程源码中可见基于sklearn的DecisionTreeClassifier与RandomForestClassifier调用并包含对比评估模型性能的关键代码。通过运行脚本读者能直观对比单棵决策树与随机森林的预测效果理解集成学习降低过拟合、提升泛化能力的原理同时掌握树模型在真实数据上的调试技巧是一份兼顾理论说明与代码实现的实用参考资料。1. 一份作业包里的月亮数据预测决策树与随机森林的完整复现拿到这个 homework5 压缩包解压后是月亮数据预测项目的完整源码三个 Python 脚本加一篇 doc 说明。这是一个基于 scikit-learn 的经典二分类实验用 make_moons 生成两个半月形交错的数据分布然后分别用决策树分类器和随机森林分类器来学习它的类别边界。项目标题写的是月亮数据预测但这里的月亮不是天文观测数据也不需要预测月相而是 sklearn 专门用来演示非线性分类的模拟数据集预测目标是每个样本的类别标签是 0 还是 1。适合正在补机器学习作业的同学也适合想快速对比这两个算法效果的从业者。代码不长跑通不难真正卡人的是参数到底在调什么、过拟合出在哪个环节。2. 先立住选型理由为什么月亮数据需要同时上决策树和随机森林2.1 月亮数据的分布特征非线性边界决定了树模型的上限先看数据长什么样。make_moons 生成两个半圆形点簇一个朝上一个朝下交错在一起像两弯相对的月牙。每个样本只有两个特征即平面上的横纵坐标标签是 0 或 1正好对应两个月亮。from sklearn.datasets import make_moons X, y make_moons(n_samples1000, noise0.2, random_state42) print(X.shape, y.shape) # (1000, 2) (1000,) import matplotlib.pyplot as plt plt.scatter(X[:, 0], X[:, 1], cy, cmapbwr, s10) plt.show()n_samples 是生成样本总数我习惯取 1000样本太少决策树学不到完整的边界太多又会放大尾部噪声。noise 是加到坐标上的高斯噪声标准差0.2 意味着两类在交界处已经有一部分重叠这是刻意保留的难度。random_state 固定成 42保证每次生成的数据完全一致。打印出来的 X 形状是 (1000, 2)每个样本两个特征y 是 (1000,)取值只有 0 和 1。把数据散点图画出来后能直观看到用一条直线永远切不开这两类这正是树模型这类非线性算法的用武之地。为什么树模型适合这里因为决策树的本质是用一系列特征 1 是否大于某个阈值特征 2 是否小于某个阈值的判断逐层切分空间每刀平行于坐标轴多刀组合就能拼出任意形状的分类边界。月亮数据的两个类别犬牙交错决策树用递归划分去逼近真实曲线划分粒度越细边界越贴合当然也越容易把噪声一起学进去这是后话。理解这一点就理解了后面所有调参动作的出发点。2.2 决策树分类器的划分准则基尼不纯度与信息增益的取舍决策树在每个内部节点都要回答一个问题用哪个特征的哪个阈值去切能让左右两个子节点更纯。sklearn 里默认用基尼不纯度 Gini 来衡量公式是 Gini 1 - Σ p_i²p_i 是节点中第 i 类的样本占比。节点里只有一类时基尼为 0两类各占一半时基尼为 0.5。划分时算法会遍历每个特征的所有可能阈值挑出使切分后两个子节点基尼加权和最小的那个阈值。信息增益是另一种常用准则它用熵 H -Σ p_i log(p_i) 代替基尼。信息增益 父节点熵 - 子节点熵的加权和增益越大说明这次划分带来的信息量越多。sklearn 里通过参数 criterion 切换默认是 gini改成 entropy 也能用。实际项目里两者在月亮数据这类均衡二分类上差异极小我一般不会花太多时间纠结选哪个把精力留给决策树学多深的问题上。决策树一个容易忽略的点是它天生就是分段常数逼近每次划分给一片区域打一个固定的类别标签划分次数越多台阶越密曲线拟合越精细。月亮数据本身是平滑曲线边界决策树想逼近它就得不断加深而加深的代价是每个叶子装的样本变少单棵树的方差被拉大换到新数据上很容易翻车。2.3 随机森林算法原理Bagging 与随机特征子集如何压住方差随机森林的逻辑是三个臭皮匠顶个诸葛亮。它先做 Bagging即对原始训练集做有放回抽样生成多份与原数据集等大的样本子集每份子集里大约有 63.2% 的原始样本剩下的被重复抽到或漏掉。然后用每份子集训练一棵不剪枝或浅剪枝的决策树预测时所有树投票得票多的类获胜。除了样本层面的随机随机森林在做节点划分时还会随机挑一部分特征作为候选。这个参数叫 max_features分类任务默认是 sqrt(n_features)月亮数据只有两个特征所以每棵树每次划分基本就是在两个特征里挑一个随机性主要体现在样本上。这样设计的目的很明确让树与树之间尽量不吵架如果每棵树都长得一样集成再多棵也只是把同一份错误重复 n 遍。随机性制造了个体差异投票再把这些差异中的噪声互相抵消方差自然降下来。需要留意的是随机森林的随机是双层的样本随机加特征随机这也是它和单纯 Bagging 的本质区别。如果要在回归场景用同思路换成 RandomForestRegressor 即可投票逻辑变成取平均。去年我在一个工业项目里用随机森林回归算法做传感器数值预测原理和这里完全一致只是评估指标从准确率换成均方误差。2.4 决策树和随机森林在月亮数据上的区别可解释性与泛化能力决策树和随机森林在月亮数据上的差异可以用一句话概括单棵树容易学过头森林靠平均把过头的地方拉回来。决策树训练集准确率经常冲到 100%但放到含噪声的测试集上马上回落随机森林单棵树同样会过拟合但多数投票抹平了单棵树的极端判断整体泛化明显更稳。对比维度决策树 DecisionTreeClassifier随机森林 RandomForestClassifier模型结构单棵树多棵树集成训练样本全部样本每棵树有放回抽样特征选择每次考虑全部特征每次随机选部分特征方差表现高对数据扰动敏感低稳定性好可解释性强可直接可视化树结构较弱只能看特征重要性训练成本低随 n_estimators 线性增长实际跑月亮数据时还有个明显现象决策树可视化出来是一棵可以从根一路走到叶子的完整树人能直接读懂规则比如特征 1 小于 -0.1 且特征 2 大于 0.2 就判为类别 1。随机森林上百棵树没法逐棵看只能退而求其次看特征重要性排序。这是选型时的一个真实权衡要想向业务方解释清楚单棵树更友好要想上线稳随机森林更省心。项目里两个都写正好覆盖了这两个维度。3. 把源码跑通数据划分、模型训练与评估三步走3.1 数据准备与训练集划分moon.py 里最前面做的事通常是生成数据然后切分训练集和测试集。这里要注意make_moons 生成的是完整样本集切分时哪怕只混入一点顺序问题后面对比两个算法就会不公平。from sklearn.model_selection import train_test_split # 先用 7:3 划分训练集和测试集固定 random_state 保证可复现 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) print(X_train.shape, X_test.shape)train_test_split 的关键参数是 test_size 和 stratify。test_size0.3 表示用三成数据做测试月亮数据 1000 个样本训练集 700 个测试集 300 个这个比例足够看出模型差异。stratifyy 是很多初学者会漏掉的参数它的作用是让划分后训练集和测试集的类别比例和原始数据一致月亮数据两类本就均衡不设问题不大但养成习惯总没错等遇到 9:1 的不均衡数据时这一个参数能救不少事。random_state 固定后每次跑出来的划分结果完全相同两个算法面对的是同一份训练集和测试集对比才公平。3.2 决策树分类器训练从根节点到叶子的切分逻辑下面这段是把决策树分类器在月亮数据上跑起来的最小代码。项目里的 decision_tree_classifier.py 核心逻辑基本如此只是多了画图和打印的部分。from sklearn.tree import DecisionTreeClassifier # 限制最大深度为 5叶子最少 5 个样本防止单棵树过分生长 dt DecisionTreeClassifier( criteriongini, max_depth5, min_samples_leaf5, random_state42 ) dt.fit(X_train, y_train) train_acc dt.score(X_train, y_train) test_acc dt.score(X_test, y_test) print(f决策树 训练集准确率: {train_acc:.4f}, 测试集准确率: {test_acc:.4f})max_depth5 是这棵树的增长上限树最多做 5 层二分。为什么要限制因为不限制时树会一直切到每个叶子只剩一个样本训练集准确率 100%但测试集大概率掉回 90% 以下这就是典型的过拟合。min_samples_leaf5 是另一道保险强制每个叶子至少装 5 个样本等于逼着树在局部做少数服从多数而不是记住单个点。random_state 固定同样是为了结果可复现决策树对数据顺序和随机性敏感不固定的话每次跑出来的树都不一样。跑出来的结果在 noise0.2 的数据上决策树测试集准确率一般在 87% 到 90% 之间训练集则接近 95%。这个差距就是树的方差在作怪叶子越深训练集就越记得清晰但测试集一换就露馅。理解这个现象比记住具体分数重要。3.3 随机森林分类器训练多棵树投票的集成效果随机森林的代码结构和决策树几乎一样区别只在类名和参数。项目里的 random_forest_classifier.py 写的就是这个过程。from sklearn.ensemble import RandomForestClassifier # 100 棵树集成每棵树同样限制深度和叶子大小 rf RandomForestClassifier( n_estimators100, max_depth5, min_samples_leaf5, bootstrapTrue, random_state42 ) rf.fit(X_train, y_train) train_acc rf.score(X_train, y_train) test_acc rf.score(X_test, y_test) print(f随机森林 训练集准确率: {train_acc:.4f}, 测试集准确率: {test_acc:.4f})n_estimators100 是森林里树的数量这个值在月亮数据规模下已经能看出稳定效果。bootstrapTrue 开启有放回抽样这是 Bagging 的核心开关如果关掉每棵树都用全量样本只会得到一批高度相似的树集成效果大打折扣。max_depth 和 min_samples_leaf 这里和单棵树保持一致是为了控制单棵树的复杂度让对比更有说服力同样的单棵树能力通过投票机制能提升多少。实测在相同数据上随机森林的测试集准确率通常能到 91% 到 93%比单棵决策树高两到三个百分点而且训练准确率和测试准确率的差距明显缩小。这说明随机森林的多数投票确实把单棵树的判断误差摊薄了。如果目标只是拿到一个更好的分数把 max_depth 放开到 None、只留 min_samples_leaf 限制随机森林还能再涨一点代价是训练时间变长和模型变大。3.4 用分类报告评估准确率之外的精确率、召回率与 F1准确率只能说明整体判断对了几成在月亮数据这种均衡二分类里信息有限。更细的做法是输出混淆矩阵和分类报告看两个类别各自的表现。from sklearn.metrics import classification_report, confusion_matrix # 生成预测结果输出分类报告和混淆矩阵 y_pred rf.predict(X_test) print(classification_report(y_test, y_pred, target_names[类别0, 类别1])) print(confusion_matrix(y_test, y_pred))classification_report 会给出每一类的 precision查准率、recall召回率和 F1。对月亮数据来说类别 0 和类别 1 的样本数几乎一样所以这几个指标和准确率差不太多但它们的价值在于暴露模型偏科。比如某次结果显示类别 0 的 recall 只有 0.85而类别 1 是 0.93说明模型把一批本该是类别 0 的样本判成了类别 1边界区域切得偏了。confusion_matrix 输出一个 2x2 矩阵对角线是正确分类副对角线是错误分类哪一类被欺负了一眼就能看出来。评估环节做扎实后面调参才有方向否则只看一个总分数很容易被模型的表现骗过去。4. 避坑记录跑月亮数据时最容易翻车的五个细节4.1 决策树不限制深度训练集 100%测试集直接打回原形现象DecisionTreeClassifier 不设 max_depth也不设 min_samples_leaf训练集准确率 1.0测试集准确率只有 0.87训练和测试差十多个百分点。原因树会一直生长到每个叶子只包含一个样本等于把训练数据背了下来而不是学到边界规律。月亮数据的 noise 让部分样本标签天生带噪声树把噪声点也当成规则记了下来。解决给 max_depth 设一个合理上限月亮数据经验值 3 到 6 就够同时设 min_samples_leaf5强制叶子至少有 5 个样本支撑。改完测试集分数通常会从 0.87 涨到 0.89 以上训练集降到 0.93 附近训练和测试之间的沟壑明显收窄。4.2 noise 参数设 0模型漂亮得像满分换数据立刻失效现象make_moons 里 noise0决策树和随机森林测试集准确率都能到 0.99 以上图形上两个月亮边界干干净净给人这套模型天下无敌的错觉。原因noise0 时两个类别完全没有重叠边界清晰可辨任何有深度的树模型都能轻松学出完美分界线。这个分数是数据本身给的不是模型能力的证明。解决把 noise 调到 0.2 或 0.3 再跑一遍。noise0.3 时两类在交界处大面积混在一起测试集准确率会掉到 0.85 以下这时才看得出哪个算法更抗噪。我拿到这类实验项目的习惯是先把噪声参数全部列出来用两种数据各训一遍只拿 noise0 的结果下结论是典型的自欺欺人。4.3 random_state 不固定同一个脚本每次跑出不同分数现象连续运行三次同一个 .py 文件决策树测试集分数分别是 0.873、0.891、0.866浮动超过两个百分点没法判断是算法问题还是运气问题。原因make_moons 生成数据、train_test_split 切分、决策树训练三步各有随机性每一步不固定种子整个实验就是随机游走。随机森林内部还有样本抽样和特征抽样的随机性浮动更大。解决在 make_moons、train_test_split、DecisionTreeClassifier、RandomForestClassifier 四处全部固定 random_state42。这样就锁定了数据生成、切分和训练全过程任何分数差异都只能来自算法本身。固定之后如果还想再观察模型稳定性可以换几个不同的 random_state 各跑一遍看波动范围但那是稳定性分析不是重新调参。4.4 n_estimators 调到 2000分数没涨多少训练时间翻几倍现象把随机森林的 n_estimators 从 100 调大到 2000测试集准确率从 0.92 涨到 0.925几乎可以忽略不计训练时间却从 0.2 秒涨到 4 秒模型文件也大了十几倍。原因随机森林的收益服从边际递减规律树的数量超过某个阈值后新加入的树提供的增量信息越来越少因为它们的训练子集不断撞车。月亮数据只有 1000 个样本、2 个特征100 到 300 棵树足够覆盖全部有效变化。解决我从经验上把 n_estimators 的合理区间定在 100 到 300 之间调参时先固定 200 跑通全流程最后有时间再用网格搜索微调。如果想监控最佳树数量可以在随机森林里开 oob_scoreTrue它用袋外样本估算每棵树的效果能辅助判断当前树数是否已经饱和。4.5 只看准确率边界样本被分错几个总分看不出来现象随机森林准确率 0.92 看起来不错但画出决策边界后发现交界处是一片碎花区域错分样本全都集中在那两个月亮交错的地方边界附近几乎没有有效区分能力。原因准确率对所有样本一视同仁边界附近的样本占比小被分错十来个在总分上只浮动一个百分点不到根本不显眼。解决补上 classification_report 看类别明细用 confusion_matrix 看错分分布最直观的还是把决策边界画出来。月亮数据只有两个特征画边界几乎零成本。准确率负责一个总览结论分类报告和决策边界负责解释错在哪、为什么错三者配合才是一份完整的模型评估。5. 验证与调优让模型在月亮数据上逼近真实边界的四种手段5.1 用 K-Fold 交叉验证替换单次切分单次 train_test_split 的结果受切分点影响很大一次 0.92 不代表平均也是 0.92。更稳妥的做法是 5 折交叉验证把数据切成 5 份轮流拿 4 份训练、1 份验证最后得到 5 个分数取平均。from sklearn.model_selection import cross_val_score # 对随机森林做 5 折交叉验证输出每一折的分数 scores cross_val_score(rf, X, y, cv5, scoringaccuracy) print(每折分数:, scores) print(平均准确率: {:.4f} ± {:.4f}.format(scores.mean(), scores.std()))cross_val_score 返回一个长度为 5 的数组每个元素是一折的准确率。这里传 X 和 y 而不是 X_train 和 y_train是因为交叉验证内部会自己做切分把整个数据集交进去即可。cv5 是折数月亮数据 1000 个样本每折验证集 200 个样本量足够分数波动小。输出里的标准差代表模型在不同数据子集上的稳定性标准差越小说明模型泛化越均匀这个指标有时候比平均分更能说明问题。5.2 用 GridSearchCV 网格搜索关键超参数网格搜索的价值在于把拍脑袋调参变成让数据筛选。比如想知道 max_depth 取多少、min_samples_leaf 取多少直接把候选值列成字典交给 GridSearchCV它会穷举所有组合并返回最优结果。from sklearn.model_selection import GridSearchCV # 定义候选参数网格对随机森林做网格搜索 param_grid { n_estimators: [100, 200], max_depth: [3, 5, None], min_samples_leaf: [1, 3, 5] } grid GridSearchCV( RandomForestClassifier(random_state42), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid.fit(X_train, y_train) print(最优参数:, grid.best_params_) print(最优分数: {:.4f}.format(grid.best_score_))param_grid 里每个键对应随机森林的一个超参数值是待测候选列表。3 个参数一共 2×3×318 种组合每种组合都做 5 折交叉验证总共要训练 90 轮。n_jobs-1 表示用满所有 CPU 核心月亮数据规模小几秒钟就能跑完。bias 在 scoringaccuracy 上会优先选准确率最高的组合但要注意网格搜索的分数是验证集上的平均分不等于测试集分数最后还要用最优参数在测试集上确认一次。实测在月亮数据上最优组合通常是 max_depth5、min_samples_leaf3、n_estimators200。max_depthNone不限制深度在这种小数据集上并不稳因为噪声会让树长到难以控制的程度min_samples_leaf 太小时单棵树照样过拟合。5.3 画出决策边界直观判断分类器学到的形状调参是否有效画一次决策边界比看十个数字都清楚。月亮数据只有两个特征可以构造一个覆盖整个特征空间的网格让模型对每个网格点做预测再把预测结果渲染成背景色。import numpy as np # 生成覆盖特征范围的网格点让模型逐个预测 x_min, x_max X[:, 0].min() - 0.5, X[:, 0].max() 0.5 y_min, y_max X[:, 1].min() - 0.5, X[:, 1].max() 0.5 xx, yy np.meshgrid( np.linspace(x_min, x_max, 300), np.linspace(y_min, y_max, 300) ) Z dt.predict(np.c_[xx.ravel(), yy.ravel()]) Z Z.reshape(xx.shape) plt.contourf(xx, yy, Z, alpha0.3, cmapbwr) plt.scatter(X_test[:, 0], X_test[:, 1], cy_test, cmapbwr, s10, edgecolork) plt.title(Decision Tree Boundary) plt.show()np.linspace 在特征范围外各扩了 0.5确保边界外也有背景色可看。np.c_ 把两个坐标列拼成 (90000, 2) 的矩阵一次性交给 model.predict。reshape 成 300x300 的二维网格后contourf 画出彩色等高区域测试集样本散点叠加在上面。决策树的分界是一组与坐标轴平行的折线随机森林的则相对平滑这就是分段常数逼近和集成平均的光滑效果差异。看到边界呈锯齿状或出现孤立小色块基本可以断定那部分是过拟合噪声直接调 max_depth 和 min_samples_leaf 就能改善。5.4 特征重要性排序两个特征在两类模型中的权重差异决策树和随机森林都能输出 feature_importances_数值代表每个特征对划分的贡献占比和为 1。月亮数据只有两个特征比较结果一目了然但这一步的意义在于建立特征重要性是模型视角而非真实因果关系的意识。# 打印随机森林的特征重要性 print(随机森林特征重要性:, rf.feature_importances_) print(决策树特征重要性:, dt.feature_importances_)在月亮数据上两个特征的重要性通常会比较接近因为两个维度对判断类别都有价值但每次跑会略有不同。如果某个特征的重要性长期接近 1说明模型基本只靠它划分另一个特征是冗余或噪声。真实业务里这个指标用来做特征筛选重要性接近 0 的特征可以直接丢弃减少数据维度。要注意的是特征重要性衡量的是模型在多大程度上依赖该特征做划分不代表该特征与标签的因果关系两次实验里重要性排序发生变化是正常现象不必过度解读。6. 从训练到落地的最后一步模型保存、加载与对新样本的预测训练出了满意的随机森林模型只剩最后一步怎么把它存下来留着对新数据做预测。直接用 pickle 或 joblib 把模型对象序列化即可下次加载就是一个现成的预测器。这里我习惯用 joblib它对 numpy 数组的内部格式处理得比 pickle 更稳妥大模型序列化速度也更快。import joblib # 保存训练好的随机森林模型到本地文件 joblib.dump(rf, moon_rf_model.joblib) # 模拟一个新样本输入两个特征的坐标值 new_sample [[0.1, 0.3], [-0.4, 0.1]] # 加载模型并做预测同时输出置信概率 loaded_rf joblib.load(moon_rf_model.joblib) pred loaded_rf.predict(new_sample) prob loaded_rf.predict_proba(new_sample) print(预测类别:, pred) print(属于类别0/类别1的概率:, prob)predict 返回类别硬标签predict_proba 返回每个类别的概率两行输出配合就能看出模型对每个样本的把握程度。比如 new_sample 里第一个样本预测为类别 1概率是 [0.18, 0.82]说明模型相当有底气如果概率是 [0.48, 0.52]说明样本落在决策边界附近这个结果需要谨慎对待在真实场景里我会给这类低置信度样本单独打一个待人工复核的标记。保存模型这一步做完整个项目才真正闭环离线训练完毕在线加载预测中间不再依赖任何训练数据。一个重要细节保存前固定好的特征顺序就是新样本必须遵守的特征顺序joblib 模型不会记住特征名。如果真实项目里特征的先后顺序变了预测结果就会错乱。我一般会同步保存一份特征列表随模型一起加载核对。月亮数据只有 2 个特征还好工业项目里有几十个特征时这个核对动作是上线前的强制流程。从那以后我每次拿到训练好的分类模型无论数据多么简单都会强制走一遍固定随机种子、画决策边界、joblib 保存、加载回测预测概率这四个动作才敢把模型交出去。这份月亮数据预测的作业包正是练习这套完整流程的最低成本素材跑通一遍决策树和随机森林的底层差异、过拟合的形态、调参的方向就都有了体感。希望帮到你。本文还有配套的精品资源点击获取