2026/11/2 14:44:18

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

月亮数据二分类实战:决策树与随机森林效果对比 简介适用于机器学习入门与实践的月亮数据预测项目资源聚焦决策树与随机森林两类经典算法在分类任务上的应用。资源面向希望掌握分类建模、特征处理与模型评估流程的开发者可通过项目源码直观对比两种算法的实现与效果。压缩包共13个文件体积1.23MB主要包含Python脚本decision_tree_classifier.py、random_forest_classifier.py、编译缓存pyc、项目配置xml以及说明文档doc结构清晰便于按需查看。目前已有874人学习下载适合作为算法学习或课程设计的参考。项目中包含完整的数据读取、预处理、训练与预测代码并配有问题说明文档能够帮助读者理解特征选择、模型调参和泛化能力验证等关键环节快速上手类似预测任务。1. 月亮数据预测为什么会成为决策树和随机森林的试金石做机器学习分类实验很多人在鸢尾花和波士顿房价上跑熟了流程一换到非线性的数据分布就发现决策树和随机森林的表现差距被彻底拉开。月亮数据预测(决策树和随机森林算法).zip这个项目标题本质上是在sklearn的make_moons合成数据集上对比单棵决策树和随机森林在二分类任务上的泛化能力、决策边界形态和参数敏感性。月亮数据的两个半月形类别互相交错线性分类器在它面前直接失效而决策树能硬切出锯齿状的边界随机森林则通过多棵树投票把边界磨得平滑。这个项目最大的价值在于用一份几十行的代码把单模型为什么容易过拟合、集成模型为什么更稳这件事直观地摆在了台面上。适合正在学机器学习分类算法、准备期末项目或者面试前想快速理解决策树与随机森林本质区别的开发者。2. 月亮数据集与树模型的内在关联先搞懂数据再动手2.1 make_moons 的数据结构噪声、重叠与可分离性make_moons是sklearn.datasets里内置的样本生成器它模拟的是两个半月形分布默认情况下每个类别 100 个样本总共 200 条数据特征维度只有 2 维。注意这里的预测并不是时间序列上的未来预测而是对新样本的类别预测——判断某个坐标点属于上半月还是下半月。数据生成的核心参数有三个n_samples控制样本总数noise控制在半月形曲线上的高斯噪声标准差random_state固定随机种子保证结果可复现。当noise0时两个半月形完全不相交任何稍微有点能力的分类器都能做到 100% 准确率当noise0.2时两个类别的边缘开始互相渗入出现了一小部分重叠区域这时单棵决策树就开始暴露出问题。从学习算法的角度看月亮数据集最特殊的地方在于它的决策边界不是线性的也不是简单的圆形或者矩形而是一条弯曲的带状区域。对于决策树来说它只能做轴平行切割也就是每次沿着某个特征的某个阈值把特征空间切成两个矩形区域。这意味着即使用很浅的树也能通过多段折线去逼近那条弯曲的半月形边界但逼近的方式是锯齿状的而且树的深度越深锯齿越细密越容易把个别噪声点也圈进自己的边界里。2.2 为什么是决策树和随机森林这对组合选决策树和随机森林做对照是因为它们构成了个体与整体的最佳教学样本。决策树分类器的学习过程是贪心的从根节点开始每次选择一个特征和一个阈值使得分裂后的子节点纯净度最高常用的纯度指标是基尼系数或信息熵。这种贪心策略在训练集上表现很好但对训练集中的噪声极为敏感稍微改几个样本点整棵树的形态可能就完全变了这就是高方差的表现。随机森林做的事情很简单用Bagging的思路训练多棵决策树每棵树只看到训练集的一个 Bootstrap 抽样有放回抽样并且在每次分裂时只随机抽取一部分特征供选择。在月亮数据集上由于总共只有 2 个特征每棵树分裂时往往只有 1 个特征可供选择这会让单棵树的表达能力受限但多棵树投票之后这种受限反而变成了优势——每棵树从不同的角度去切割空间最终的投票边界比任何单棵树都更接近真实的半月形分布。提示在只有 2 个特征的数据集上max_features1会让随机森林中的每棵树都退化成每次只按一个特征分裂的决策树这正是理解随机森林特征随机机制的绝佳实验环境。2.3 项目里通常包含哪些文件这类以.zip形式分发的项目解压后一般是一个标准的 Python 工程目录常见文件清单如下文件/目录作用类型data_generation.py或make_moons调用代码生成或加载月亮数据集Python 脚本decision_tree_model.py构建单棵决策树分类器并训练评估Python 脚本random_forest_model.py构建随机森林分类器并训练评估Python 脚本visualization.py绘制数据分布图和两类模型的决策边界Python 脚本requirements.txt记录numpy、pandas、scikit-learn、matplotlib版本文本文件README.md项目说明和运行步骤Markdown这些文件的划分方式不是硬性规定但绝大多数课程设计和入门项目都会遵循数据生成—单模型—集成模型—可视化这条主线。你在复现时不必拘泥于文件名核心流程跑通就行。3. 用决策树和随机森林在月亮数据上做分类完整可复现代码3.1 环境准备与数据生成三行代码拿到原始样本import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split # 生成月亮数据200个样本2个特征噪声0.2固定随机种子 X, y make_moons(n_samples200, noise0.2, random_state42) # 划分训练集和测试集70%训练30%测试 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42) print(f训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]}) print(f类别分布: 训练集 y0 有 {(y_train 0).sum()} 个, y1 有 {(y_train 1).sum()} 个)这里的noise0.2是月亮数据实验最常用的配置。如果你把noise调大到0.4两个半月形会严重重叠甚至人类肉眼都无法分辨类别边界这时任何模型的准确率都会大幅下降。random_state42是 sklearn 社区约定俗成的种子但不妨碍你换成其他数字只要保证生成数据和划分数据用的是同一个随机种子结果就是可复现的。在训练之前值得注意的是数据是否需要标准化。决策树和随机森林都是基于特征的切分点做分裂不依赖特征之间的欧氏距离所以不需要对特征做标准化或归一化。这与 SVM、KNN 等基于距离的算法有本质区别。月亮数据两个特征的取值范围都在 -1.5 到 2.5 之间量纲一致直接送入模型即可。3.2 训练单棵决策树分类器看它如何用折线逼近曲线from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, classification_report # 构建决策树分类器限制最大深度为5避免无限生长 dt_model DecisionTreeClassifier( criteriongini, # 基尼系数作为分裂标准 max_depth5, # 最大深度5层 min_samples_split4, # 内部节点至少4个样本才继续分裂 min_samples_leaf2, # 叶子节点至少2个样本 random_state42 ) # 训练单棵决策树 dt_model.fit(X_train, y_train) # 分别在训练集和测试集上评估 dt_train_pred dt_model.predict(X_train) dt_test_pred dt_model.predict(X_test) train_acc accuracy_score(y_train, dt_train_pred) test_acc accuracy_score(y_test, dt_test_pred) print(f决策树 - 训练集准确率: {train_acc:.4f}) print(f决策树 - 测试集准确率: {test_acc:.4f})max_depth5是为了给单棵树设置一个合理的天花板。如果你不设置任何限制决策树会在训练集上一直分裂下去直到所有叶子节点都是纯的这样训练集准确率冲到 100%但测试集准确率往往不到 80%。这就是决策树翻车最典型的方式。min_samples_split4和min_samples_leaf2是控制树生长的次要参数。前者限制了内部节点的最小样本量后者保证了叶子节点的最小样本量两个参数共同作用防止树在个别样本上过度细化。对于 140 个训练样本的小数据集这两个参数设成 25 之间是合理的区间。运行这段代码你通常会发现训练集准确率在 0.95 左右测试集准确率在 0.850.90 之间。训练集和测试集之间的准确率差距就是过拟合程度的直接体现。3.3 训练随机森林分类器多棵树投票带来什么改变from sklearn.ensemble import RandomForestClassifier # 构建随机森林分类器100棵树每棵树最大深度5 rf_model RandomForestClassifier( n_estimators100, # 森林中树的数量 max_depth5, # 每棵树的最大深度 min_samples_split4, # 与决策树保持一致 min_samples_leaf2, # 与决策树保持一致 max_featuressqrt, # 每次分裂随机抽取的特征数 bootstrapTrue, # 使用有放回抽样 random_state42, # 固定随机种子 n_jobs-1 # 使用所有CPU核心并行训练 ) # 训练随机森林 rf_model.fit(X_train, y_train) # 分别在训练集和测试集上评估 rf_train_pred rf_model.predict(X_train) rf_test_pred rf_model.predict(X_test) rf_train_acc accuracy_score(y_train, rf_train_pred) rf_test_acc accuracy_score(y_test, rf_test_pred) print(f随机森林 - 训练集准确率: {rf_train_acc:.4f}) print(f随机森林 - 测试集准确率: {rf_test_acc:.4f})在只有 2 个特征的数据集上max_featuressqrt实际上等价于max_features1也就是每棵树每次分裂只看 1 个特征。听起来很奇怪这岂不是让树变得更弱了是的随机森林的核心思想就是故意让每棵树变弱、变随机然后通过大量弱树投票获得一个强分类器。每棵树只看 1 个特征意味着不同的树会从不同的特征维度去尝试切分空间有些树主要沿 x 轴切有些树主要沿 y 轴切投票之后的边界形状就会比单棵树丰富得多。n_estimators100是一个性价比很高的默认值。当你画随机森林在验证集上的表现时通常 50 棵树左右准确率就趋于稳定100 棵树已经足够再增加到 500 棵在月亮数据上不会带来明显提升只会拖慢训练速度。bootstrapTrue是 Bagging 的标准配置每棵树用随机抽样得到的 63.2% 的数据训练剩下 36.8% 的数据作为袋外数据Out-of-Bag用来评估模型所以在小数据集上你甚至不用专门划分验证集直接看oob_score_就行。3.4 可视化对比画决策边界才是看懂模型的关键from mlxtend.plotting import plot_decision_regions # 创建一个 2x2 的画布分别展示原始数据和两个模型的决策边界 fig, axes plt.subplots(1, 3, figsize(15, 4)) # 第一幅图原始数据分布 axes[0].scatter(X[:, 0], X[:, 1], cy, cmapcoolwarm, s20, alpha0.7) axes[0].set_title(原始月亮数据分布) axes[0].set_xlabel(特征1) axes[0].set_ylabel(特征2) # 第二幅图决策树的决策边界 plot_decision_regions(X, y, clfdt_model, axaxes[1], legend2) axes[1].set_title(决策树 (max_depth5)) axes[1].set_xlabel(特征1) axes[1].set_ylabel(特征2) # 第三幅图随机森林的决策边界 plot_decision_regions(X, y, clfrf_model, axaxes[2], legend2) axes[2].set_title(随机森林 (100棵树)) axes[2].set_xlabel(特征1) axes[2].set_ylabel(特征2) plt.tight_layout() plt.show()如果你没有安装mlxtend可以退而求其次用matplotlib的contourf自己画在特征范围内生成一个密集的网格点让模型预测每个网格点的类别然后用contourf填充颜色。两种方式的视觉效果一样但mlxtend写起来更省事。这可能是整个项目里观感冲击最大的一步决策树画出来的决策边界是一块一块的矩形拼贴锯齿感极其明显随机森林画出来的边界则平滑很多而且误分类的噪声点不再是一刀切的牺牲品而是被局部投票覆盖。看这张图你对决策树高方差、随机森林低方差的体会比读十篇博客都深刻。4. 决策树如何逼近真实曲线参数调节与随机森林的配合策略4.1 max_depth 对决策树边界形态的决定性影响决策树逼近月亮数据真实边界的过程本质上是用多个轴平行切分去拼出一条折线。每一层max_depth增加可用的矩形区域数量就翻倍边界也就随之细化。深度为 1 时只有 2 个矩形深度为 2 时有 4 个深度为 3 时 8 个以此类推深度为d时最多有2^d个叶子节点。在月亮数据集上建议依次把max_depth设为2, 3, 5, 8, None记录每个配置下训练集和测试集的准确率。你会发现一个典型的过拟合曲线深度从 2 增加到 5 时测试准确率逐步上升深度超过 8 之后测试准确率开始波动甚至下降而训练准确率始终在往 100% 靠。这个拐点位置就是决策树在这个数据集上的最优复杂度的边界。实际操作中我不建议手动去逐个数地试深度直接用GridSearchCV或者validation_curve扫一遍max_depth的候选值列表就够了from sklearn.model_selection import validation_curve # 对决策树的不同深度进行验证曲线分析 param_range [2, 3, 4, 5, 6, 8, 10, 15] train_scores, test_scores validation_curve( DecisionTreeClassifier(random_state42), X_train, y_train, param_namemax_depth, param_rangeparam_range, cv5, scoringaccuracy ) # 计算每个深度下的平均准确率 train_mean train_scores.mean(axis1) test_mean test_scores.mean(axis1) for depth, tr, te in zip(param_range, train_mean, test_mean): print(fmax_depth{depth}, 训练集准确率{tr:.4f}, 交叉验证准确率{te:.4f})在 200 个样本的数据集上5 折交叉验证每轮只用 160 个样本训练、40 个样本验证这个规模对决策树来说完全够用。从输出里你能直观看到深度为 5 到 6 时交叉验证分数最高深度超过 10 之后交叉验证分数明显下滑。这就是决策树如何逼近真实曲线的边界——过浅拟合不足过深则把噪声也学进去了。提示validation_curve默认使用StratifiedKFold分层采样月亮数据两个类别样本数相等所以默认设置即可不需要手动调整折数。4.2 随机森林中 n_estimators 与 max_features 的互动随机森林在月亮数据上的表现由两个核心参数的配合决定树的数量n_estimators和每棵树的特征采样数max_features。我们前面提到这个数据只有 2 个特征max_featuressqrt会变成每轮只看 1 个特征。但如果你把max_features设为2或者None即全部特征每棵树的每次分裂就能同时看到两个特征可以选取更优的切分维度单棵树的表达能力变强了但树之间的相关性也增加了。这带来一个微妙的权衡max_features越大单棵树越强但随机森林整体的方差降低效果越弱max_features越小单棵树越弱但投票的多样性越强。对月亮数据这种特征数极少的数据集通常max_features1的效果优于max_features2因为前者让每棵树产生了完全不同的切分逻辑。n_estimators的调节相对粗暴且安全从 10 棵到 200 棵每次增加 10 棵计算测试集准确率并画曲线。这个曲线通常在 3070 棵之间出现拐点之后趋于平稳。在月亮数据上把树加到 500 棵以上不会明显提高准确率但训练时间线性增加完全没必要。4.3 从决策树到随机森林准确率差距的解读单独运行决策树和随机森林后你大概率会得到这样一组数字模型训练集准确率测试集准确率准确率差距决策树 (max_depth5)0.95710.88330.0738决策树 (max_depthNone)1.00000.81670.1833随机森林 (100棵, max_depth5)0.93570.91670.0190这个对比表几乎是教科书级别的结论随机森林的训练集准确率略低于决策树因为单棵树的训练数据是随机抽样的模型不会对任何一棵树完全记忆训练集但测试集准确率明显更高而且训练集与测试集的差距大幅缩小。这证明随机森林的成功不是靠更强的单棵表达能力而是靠降低方差。很多人第一次跑这个实验时会困惑为什么随机森林的训练集准确率不是 100%因为每棵树只用了约 63.2% 的训练样本而且分裂时还随机丢了一半特征单棵树没法把所有样本都学进边界里。但这恰恰是随机森林泛化能力更强的根源——在对训练集的记忆上刻意打折换来对新样本更稳定的判断。5. 决策树和随机森林在月亮数据上的四大避坑记录5.1 不设 max_depth 导致测试集准确率暴跌现象把DecisionTreeClassifier的参数全部留默认只固定random_state训练集准确率直接 100%但测试集准确率只有 0.750.82比限制深度的决策树低了 58 个百分点。原因sklearn中max_depth默认是None决策树会一直生长到所有叶子节点都纯净。月亮数据里有重叠区域这部分噪声点被树逐点记忆了下来导致决策边界扭曲到完全贴合训练集。解决显式设置max_depth5或max_depth6。如果不想手动试用GridSearchCV搜索max_depth的取值或者干脆用min_samples_leaf限制叶子节点的最小样本量比如min_samples_leaf5同样可以起到控制过拟合的效果。5.2 把 max_features 设成 sqrt 后随机森林反而不如决策树现象某次实验中随机森林的测试准确率比单棵决策树还低且训练准确率也只有 0.80 左右。原因在只有 2 个特征的月亮数据上max_featuressqrt即max_features1。但你之前可能不小心设置了max_leaf_nodes4、min_samples_leaf10之类的强限制每棵树本身被约束得几乎没有表达能力100 棵弱树投票后仍然是一个弱分类器准确率自然上不去。集成模型的前提是每棵树至少比随机猜测好泛化能力不足的树堆再多也没用。解决先保证单棵决策树的测试准确率在 0.85 以上再去调随机森林的采样参数。随机森林不是自动变强的魔法它只是把单棵树的方差抹平如果单棵树本身就欠拟合抹平之后仍然欠拟合。5.3 在 pipeline 里对特征做了标准化可视化边界却一团糟现象代码里先跑StandardScaler再送入决策树打印的准确率正常但用plot_decision_regions画图时边界位置和原始数据分布对不上看起来像坐标系错位。原因决策树虽然不需要特征缩放但一旦用了Pipeline对特征做了标准化模型训练和预测的都是在标准化后的空间里进行的。预测新样本时sklearn 的Pipeline会自动化标准化但plot_decision_regions内部是用原始特征坐标去调模型的predict方法绕过了 Pipeline 的转换步骤导致给模型喂进去的是未经标准化的数据。解决在月亮数据上直接省略标准化步骤不要在Pipeline里加StandardScaler。如果确实需要在其他多特征数据集上保留标准化画边界时改用手动方式先对网格点做scaler.transform再送入模型预测最后在原始坐标系里画contourf。5.4 固定 random_state 后结果仍然每次不一样现象决策树和随机森林都设了random_state42但每次运行代码准确率仍然在小范围内波动。原因train_test_split和make_moons也各自有random_state参数。如果生成数据时固定种子但划分时不固定每次跑出来的训练集和测试集都不相同模型看到的数据不一样准确率自然不同。常见的错误是只对模型固定种子忽略了数据生成和划分环节。解决在make_moons、train_test_split、DecisionTreeClassifier、RandomForestClassifier四处都固定random_state42。更保险的做法是在脚本顶部用np.random.seed(42)设定全局随机种子。随机森林内部还有bootstrap抽样的随机性同样受random_state控制鱼贯固定才叫真正可复现。6. 用袋外分数和特征重要度验证模型的真实泛化能力月亮数据只有 2 个特征但随机森林仍然能输出每个特征的重要度分数这个分数能帮你理解模型的决策逻辑。随机森林的特征重要度计算方式是对每一棵树在袋外数据上计算分裂前的准确率然后对某一列特征的取值做随机打乱再计算打乱后的准确率两者之差平均后就是该特征的重要度。注意在月亮数据上这个分数不会像高维数据那样出现某个特征明显压倒另一个特征因为两个半月形的区分同时依赖 x 和 y 两个方向。# 使用袋外分数评估随机森林 rf_model RandomForestClassifier( n_estimators100, max_depth5, max_featuressqrt, bootstrapTrue, oob_scoreTrue, # 开启袋外评估 random_state42 ) rf_model.fit(X_train, y_train) print(f袋外分数 (OOB Score): {rf_model.oob_score_:.4f}) print(f测试集准确率: {accuracy_score(y_test, rf_model.predict(X_test)):.4f}) print(f特征重要度: {rf_model.feature_importances_})一个值得注意的技巧是袋外分数和测试集准确率之间的差距。两者都是对模型泛化能力的估计袋外分数基于训练过程中未参与单棵树构建的样本测试集准确率基于全新的数据。在月亮数据这样的小规模数据集上如果袋外分数明显低于测试集准确率说明训练集划分时存在一定的偶然性测试集恰好比平均情况好预测。这是一个信号不要只看一次测试集准确率就下结论多固定几个不同的random_state值跑几遍取平均才可靠。在实际的项目交付里我通常还会做一个鲁棒性验证把make_moons的noise参数从 0.1 逐步增加到 0.4每档噪声下分别记录决策树和随机森林的测试准确率。随本文还有配套的精品资源点击获取