简介:适用于机器学习入门与实践的月亮数据预测项目资源,聚焦决策树与随机森林两类经典算法在分类任务上的应用。资源面向希望掌握分类建模、特征处理与模型评估流程的开发者,可通过项目源码直观对比两种算法的实现与效果。压缩包共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固定随机种子保证结果可复现。当noise=0时,两个半月形完全不相交,任何稍微有点能力的分类器都能做到 100% 准确率;当noise=0.2时,两个类别的边缘开始互相渗入,出现了一小部分重叠区域,这时单棵决策树就开始暴露出问题。
从学习算法的角度看,月亮数据集最特殊的地方在于它的决策边界不是线性的,也不是简单的圆形或者矩形,而是一条弯曲的带状区域。对于决策树来说,它只能做轴平行切割,也就是每次沿着某个特征的某个阈值把特征空间切成两个矩形区域。这意味着即使用很浅的树,也能通过多段折线去逼近那条弯曲的半月形边界,但逼近的方式是锯齿状的,而且树的深度越深,锯齿越细密,越容易把个别噪声点也圈进自己的边界里。
2.2 为什么是决策树和随机森林这对组合
选决策树和随机森林做对照,是因为它们构成了"个体与整体"的最佳教学样本。决策树分类器的学习过程是贪心的:从根节点开始,每次选择一个特征和一个阈值,使得分裂后的子节点纯净度最高,常用的纯度指标是基尼系数或信息熵。这种贪心策略在训练集上表现很好,但对训练集中的噪声极为敏感,稍微改几个样本点,整棵树的形态可能就完全变了,这就是高方差的表现。
随机森林做的事情很简单:用Bagging的思路训练多棵决策树,每棵树只看到训练集的一个 Bootstrap 抽样(有放回抽样),并且在每次分裂时只随机抽取一部分特征供选择。在月亮数据集上,由于总共只有 2 个特征,每棵树分裂时往往只有 1 个特征可供选择,这会让单棵树的表达能力受限,但多棵树投票之后,这种受限反而变成了优势——每棵树从不同的角度去切割空间,最终的投票边界比任何单棵树都更接近真实的半月形分布。
提示:在只有 2 个特征的数据集上,
max_features=1会让随机森林中的每棵树都退化成"每次只按一个特征分裂的决策树",这正是理解随机森林"特征随机"机制的绝佳实验环境。
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_samples=200, noise=0.2, random_state=42) # 划分训练集和测试集:70%训练,30%测试 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) print(f"训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]}") print(f"类别分布: 训练集 y=0 有 {(y_train == 0).sum()} 个, y=1 有 {(y_train == 1).sum()} 个")这里的noise=0.2是月亮数据实验最常用的配置。如果你把noise调大到0.4,两个半月形会严重重叠,甚至人类肉眼都无法分辨类别边界,这时任何模型的准确率都会大幅下降。random_state=42是 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( criterion='gini', # 基尼系数作为分裂标准 max_depth=5, # 最大深度5层 min_samples_split=4, # 内部节点至少4个样本才继续分裂 min_samples_leaf=2, # 叶子节点至少2个样本 random_state=42 ) # 训练单棵决策树 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_depth=5是为了给单棵树设置一个合理的天花板。如果你不设置任何限制,决策树会在训练集上一直分裂下去,直到所有叶子节点都是纯的,这样训练集准确率冲到 100%,但测试集准确率往往不到 80%。这就是决策树翻车最典型的方式。
min_samples_split=4和min_samples_leaf=2是控制树生长的次要参数。前者限制了内部节点的最小样本量,后者保证了叶子节点的最小样本量,两个参数共同作用,防止树在个别样本上过度细化。对于 140 个训练样本的小数据集,这两个参数设成 2~5 之间是合理的区间。
运行这段代码,你通常会发现训练集准确率在 0.95 左右,测试集准确率在 0.85~0.90 之间。训练集和测试集之间的准确率差距,就是过拟合程度的直接体现。
3.3 训练随机森林分类器:多棵树投票带来什么改变
from sklearn.ensemble import RandomForestClassifier # 构建随机森林分类器:100棵树,每棵树最大深度5 rf_model = RandomForestClassifier( n_estimators=100, # 森林中树的数量 max_depth=5, # 每棵树的最大深度 min_samples_split=4, # 与决策树保持一致 min_samples_leaf=2, # 与决策树保持一致 max_features='sqrt', # 每次分裂随机抽取的特征数 bootstrap=True, # 使用有放回抽样 random_state=42, # 固定随机种子 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_features='sqrt'实际上等价于max_features=1,也就是每棵树每次分裂只看 1 个特征。听起来很奇怪:这岂不是让树变得更弱了?是的,随机森林的核心思想就是故意让每棵树变弱、变随机,然后通过大量弱树投票获得一个强分类器。每棵树只看 1 个特征,意味着不同的树会从不同的特征维度去尝试切分空间,有些树主要沿 x 轴切,有些树主要沿 y 轴切,投票之后的边界形状就会比单棵树丰富得多。
n_estimators=100是一个性价比很高的默认值。当你画随机森林在验证集上的表现时,通常 50 棵树左右准确率就趋于稳定,100 棵树已经足够,再增加到 500 棵在月亮数据上不会带来明显提升,只会拖慢训练速度。bootstrap=True是 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], c=y, cmap='coolwarm', s=20, alpha=0.7) axes[0].set_title("原始月亮数据分布") axes[0].set_xlabel("特征1") axes[0].set_ylabel("特征2") # 第二幅图:决策树的决策边界 plot_decision_regions(X, y, clf=dt_model, ax=axes[1], legend=2) axes[1].set_title("决策树 (max_depth=5)") axes[1].set_xlabel("特征1") axes[1].set_ylabel("特征2") # 第三幅图:随机森林的决策边界 plot_decision_regions(X, y, clf=rf_model, ax=axes[2], legend=2) 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_state=42), X_train, y_train, param_name="max_depth", param_range=param_range, cv=5, scoring="accuracy" ) # 计算每个深度下的平均准确率 train_mean = train_scores.mean(axis=1) test_mean = test_scores.mean(axis=1) for depth, tr, te in zip(param_range, train_mean, test_mean): print(f"max_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_features='sqrt'会变成每轮只看 1 个特征。但如果你把max_features设为2或者None(即全部特征),每棵树的每次分裂就能同时看到两个特征,可以选取更优的切分维度,单棵树的表达能力变强了,但树之间的相关性也增加了。
这带来一个微妙的权衡:max_features越大,单棵树越强,但随机森林整体的方差降低效果越弱;max_features越小,单棵树越弱,但投票的多样性越强。对月亮数据这种特征数极少的数据集,通常max_features=1的效果优于max_features=2,因为前者让每棵树产生了完全不同的切分逻辑。
n_estimators的调节相对粗暴且安全:从 10 棵到 200 棵,每次增加 10 棵,计算测试集准确率并画曲线。这个曲线通常在 30~70 棵之间出现拐点,之后趋于平稳。在月亮数据上把树加到 500 棵以上不会明显提高准确率,但训练时间线性增加,完全没必要。
4.3 从决策树到随机森林:准确率差距的解读
单独运行决策树和随机森林后,你大概率会得到这样一组数字:
| 模型 | 训练集准确率 | 测试集准确率 | 准确率差距 |
|---|---|---|---|
| 决策树 (max_depth=5) | 0.9571 | 0.8833 | 0.0738 |
| 决策树 (max_depth=None) | 1.0000 | 0.8167 | 0.1833 |
| 随机森林 (100棵, max_depth=5) | 0.9357 | 0.9167 | 0.0190 |
这个对比表几乎是教科书级别的结论:随机森林的训练集准确率略低于决策树,因为单棵树的训练数据是随机抽样的,模型不会对任何一棵树完全记忆训练集;但测试集准确率明显更高,而且训练集与测试集的差距大幅缩小。这证明随机森林的成功不是靠更强的单棵表达能力,而是靠降低方差。
很多人第一次跑这个实验时会困惑:为什么随机森林的训练集准确率不是 100%?因为每棵树只用了约 63.2% 的训练样本,而且分裂时还随机丢了一半特征,单棵树没法把所有样本都学进边界里。但这恰恰是随机森林泛化能力更强的根源——在对训练集的记忆上刻意打折,换来对新样本更稳定的判断。
5. 决策树和随机森林在月亮数据上的四大避坑记录
5.1 不设 max_depth 导致测试集准确率暴跌
现象:把DecisionTreeClassifier的参数全部留默认,只固定random_state,训练集准确率直接 100%,但测试集准确率只有 0.75~0.82,比限制深度的决策树低了 5~8 个百分点。
原因:sklearn中max_depth默认是None,决策树会一直生长到所有叶子节点都纯净。月亮数据里有重叠区域,这部分噪声点被树逐点"记忆"了下来,导致决策边界扭曲到完全贴合训练集。
解决:显式设置max_depth=5或max_depth=6。如果不想手动试,用GridSearchCV搜索max_depth的取值,或者干脆用min_samples_leaf限制叶子节点的最小样本量(比如min_samples_leaf=5),同样可以起到控制过拟合的效果。
5.2 把 max_features 设成 'sqrt' 后随机森林反而不如决策树
现象:某次实验中,随机森林的测试准确率比单棵决策树还低,且训练准确率也只有 0.80 左右。
原因:在只有 2 个特征的月亮数据上,max_features='sqrt'即max_features=1。但你之前可能不小心设置了max_leaf_nodes=4、min_samples_leaf=10之类的强限制,每棵树本身被约束得几乎没有表达能力,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_state=42,但每次运行代码,准确率仍然在小范围内波动。
原因:train_test_split和make_moons也各自有random_state参数。如果生成数据时固定种子但划分时不固定,每次跑出来的训练集和测试集都不相同,模型看到的数据不一样,准确率自然不同。常见的错误是只对模型固定种子,忽略了数据生成和划分环节。
解决:在make_moons、train_test_split、DecisionTreeClassifier、RandomForestClassifier四处都固定random_state=42。更保险的做法是在脚本顶部用np.random.seed(42)设定全局随机种子。随机森林内部还有bootstrap抽样的随机性,同样受random_state控制,鱼贯固定才叫真正可复现。
6. 用袋外分数和特征重要度验证模型的真实泛化能力
月亮数据只有 2 个特征,但随机森林仍然能输出每个特征的重要度分数,这个分数能帮你理解模型的决策逻辑。随机森林的特征重要度计算方式是:对每一棵树,在袋外数据上计算分裂前的准确率,然后对某一列特征的取值做随机打乱,再计算打乱后的准确率,两者之差平均后就是该特征的重要度。注意在月亮数据上,这个分数不会像高维数据那样出现某个特征明显压倒另一个特征,因为两个半月形的区分同时依赖 x 和 y 两个方向。
# 使用袋外分数评估随机森林 rf_model = RandomForestClassifier( n_estimators=100, max_depth=5, max_features='sqrt', bootstrap=True, oob_score=True, # 开启袋外评估 random_state=42 ) 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,每档噪声下分别记录决策树和随机森林的测试准确率。随
本文还有配套的精品资源,点击获取