开发技巧

Kaggle Learn 是 Kaggle 的免费微课程

Kaggle Learn 是 Kaggle 的免费微课程。《Intro to Machine Learning》的主线是:划分训练/验证集 → 决策树 → 调 maxleafnodes 理解欠拟合与过拟合 → 随机森林。 这篇在 scikit-learn 自带的离线数据集(糖尿病进展,442 行)上把这条主线完整跑一遍,脚本打印每个叶子数下的训练和验证误差,

BINBHO · 图文教程

Kaggle Learn 是 Kaggle 的免费微课程。《Intro to Machine Learning》的主线是:划分训练/验证集 → 决策树 → 调 max_leaf_nodes 理解欠拟合与过拟合 → 随机森林。 这篇在 scikit-learn 自带的离线数据集(糖尿病进展,442 行)上把这条主线完整跑一遍,脚本打印每个叶子数下的训练和验证误差,最后再用交叉验证解决“换个随机种子结论就变”的问题。

先看清成品

  • 真实运行:基线 MAE 61.6,最好的决策树 51.2,随机森林 48.0;不限叶子数的树训练误差 0、验证误差 62.5,比瞎猜还差。
  • 发现单次划分选出的最佳叶子数随 random_state 变(10、5、5),改用 5 折交叉验证选出 5。
  • 3 项 pytest 通过。

合规提醒:Kaggle Learn 的课程和练习 Notebook 免费在线学习,内容版权归 Kaggle;本文不复制课程内容,只把课程讲的方法用别的数据重做一遍。用 Kaggle 数据集前看它的许可证;比赛数据只能按比赛规则使用,下载前要在网页上接受规则。一人一个账号,不共享账号或 API Token,不用多个账号绕过提交次数限制。

运行说明:本文代码全部在 box 上真实运行(2026-10-21,UTC+8),用的是 scikit-learn 1.9.1 自带数据或我自己生成的合成数据,没有从 Kaggle 下载任何数据。

做完之后你会得到什么?

成果 说明
tree_vs_forest.py 扫叶子数 + 随机森林 + 交叉验证
test_tree.py 3 项测试

需要准备哪些环境?

项目 说明
Python 3.9 以上,pip install pandas scikit-learn pytest(我用的是 scikit-learn 1.9.1)

关键事实

事实 来源
课程内容:模型如何工作、基本数据探索、第一个模型、模型验证、欠拟合与过拟合、随机森林等 Kaggle Learn 课程页
load_diabetes:442 个样本、10 个特征,目标是一年后的病情进展量化指标;随 scikit-learn 安装,无需联网 scikit-learn 文档
MAE(平均绝对误差):预测值与真实值之差的绝对值的平均 —

文件结构

88-kaggle-learn-A-src/
├── tree_vs_forest.py
└── test_tree.py

第 1 步:脚本

"""Kaggle Learn《Intro to Machine Learning》的流程,换成 scikit-learn 自带的离线数据集(糖尿病进展,442 行):
划分训练/验证 → 决策树 → 扫 max_leaf_nodes 找欠拟合/过拟合的平衡点 → 随机森林对比。"""
import sys
import pandas as pd
from sklearn.datasets import load_diabetes
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor


def load():
    d = load_diabetes(as_frame=True)
    return d.data, d.target


def mae_for(leaves, X_tr, X_va, y_tr, y_va, seed=0):
    m = DecisionTreeRegressor(max_leaf_nodes=leaves, random_state=seed).fit(X_tr, y_tr)
    return mean_absolute_error(y_va, m.predict(X_va)), mean_absolute_error(y_tr, m.predict(X_tr))


def main(seed=1):
    X, y = load()
    print(f"数据:{X.shape[0]} 行 × {X.shape[1]} 列;目标是一年后的病情进展指标,均值 {y.mean():.1f}")
    X_tr, X_va, y_tr, y_va = train_test_split(X, y, random_state=seed)
    base = mean_absolute_error(y_va, [y_tr.mean()] * len(y_va))
    print(f"基线(全猜训练集均值)验证 MAE:{base:.1f}")
    rows = []
    for leaves in (2, 5, 10, 25, 50, 100, None):
        va, tr = mae_for(leaves, X_tr, X_va, y_tr, y_va)
        rows.append((leaves, tr, va))
        print(f"max_leaf_nodes={str(leaves):>4}  训练 MAE {tr:6.1f}  验证 MAE {va:6.1f}")
    best = min(rows, key=lambda r: r[2])
    print(f"最好的叶子数:{best[0]}(验证 MAE {best[2]:.1f})")
    rf = RandomForestRegressor(random_state=seed).fit(X_tr, y_tr)
    rf_mae = mean_absolute_error(y_va, rf.predict(X_va))
    print(f"随机森林(默认参数)验证 MAE:{rf_mae:.1f}")
    return base, best, rf_mae


def cv_choose(cands=(2, 5, 10, 25, 50), folds=5):
    """单次划分选出的叶子数随 random_state 变(实测 10 和 5 来回跳),改用 5 折交叉验证选。"""
    from sklearn.model_selection import cross_val_score
    X, y = load()
    res = {}
    for leaves in cands:
        s = -cross_val_score(DecisionTreeRegressor(max_leaf_nodes=leaves, random_state=0), X, y,
                             cv=folds, scoring="neg_mean_absolute_error")
        res[leaves] = (s.mean(), s.std())
        print(f"max_leaf_nodes={leaves:>3}  {folds} 折 MAE {s.mean():.1f} ± {s.std():.1f}")
    best = min(res, key=lambda k: res[k][0])
    print(f"交叉验证选出:{best}")
    return best, res


if __name__ == "__main__":
    if sys.argv[1:] == ["cv"]:
        cv_choose()
    else:
        main(int(sys.argv[1]) if len(sys.argv) > 1 else 1)

第 2 步:测试

import tree_vs_forest as t

def test_beats_baseline_and_forest_reasonable():
    base, best, rf = t.main(1)
    assert best[2] < base and rf < base

def test_unlimited_tree_overfits():
    from sklearn.model_selection import train_test_split
    X, y = t.load(); X_tr, X_va, y_tr, y_va = train_test_split(X, y, random_state=1)
    va, tr = t.mae_for(None, X_tr, X_va, y_tr, y_va)
    assert tr == 0 and va > tr + 30

def test_cv_returns_candidate():
    best, res = t.cv_choose(cands=(2, 10), folds=3)
    assert best in (2, 10) and set(res) == {2, 10}

第 3 步:运行

# 2026-10-21 21:55 UTC+8
$ python tree_vs_forest.py
数据:442 行 × 10 列;目标是一年后的病情进展指标,均值 152.1
基线(全猜训练集均值)验证 MAE:61.6
max_leaf_nodes=   2  训练 MAE   53.7  验证 MAE   56.0
max_leaf_nodes=   5  训练 MAE   42.6  验证 MAE   51.9
max_leaf_nodes=  10  训练 MAE   38.5  验证 MAE   51.2
max_leaf_nodes=  25  训练 MAE   31.3  验证 MAE   53.8
max_leaf_nodes=  50  训练 MAE   24.3  验证 MAE   57.1
max_leaf_nodes= 100  训练 MAE   12.8  验证 MAE   60.0
max_leaf_nodes=None  训练 MAE    0.0  验证 MAE   62.5
最好的叶子数:10(验证 MAE 51.2)
随机森林(默认参数)验证 MAE:48.0

$ python tree_vs_forest.py 2 | tail -3
max_leaf_nodes=None  训练 MAE    0.0  验证 MAE   68.9
最好的叶子数:5(验证 MAE 48.9)
随机森林(默认参数)验证 MAE:48.4
$ python tree_vs_forest.py 3 | tail -3
max_leaf_nodes=None  训练 MAE    0.0  验证 MAE   63.6
最好的叶子数:5(验证 MAE 46.9)
随机森林(默认参数)验证 MAE:46.1

# 21:56 UTC+8
$ python tree_vs_forest.py cv
max_leaf_nodes=  5  5 折 MAE 50.2 ± 2.6
max_leaf_nodes= 10  5 折 MAE 51.4 ± 1.9
max_leaf_nodes= 25  5 折 MAE 52.6 ± 2.6
max_leaf_nodes= 50  5 折 MAE 56.0 ± 3.8
交叉验证选出:5
$ python -m pytest -q
3 passed

怎么检查结果?

  • 训练 MAE 随叶子数增加一直下降,到不限叶子时为 0;验证 MAE 先降后升——这就是过拟合的典型曲线。
  • 每个模型都要和“全猜均值”的基线比,不然不知道模型到底有没有用。
  • 随机森林不调参就比调过的单棵树好,这也是课程最后一节的结论。

调试示例

1. 换一个随机种子,“最好的叶子数”就变了。 random_state=1 时选出 10,=2 和 =3 时选出 5。442 行数据只划出约 110 行做验证,一次划分的误差波动(几个 MAE)和叶子数 5 与 10 之间的差距(0.7)差不多大,所以单次划分的结论不可靠。改用 5 折交叉验证:5 个叶子 50.2 ± 2.6,10 个叶子 51.4 ± 1.9,选 5。注意两者的差距仍在标准差之内,严格说它们差不多好。交叉验证在 Kaggle Learn 的《Intermediate Machine Learning》里讲。

2. 我把新函数加在了 if __name__ 后面。 第一次运行 python tree_vs_forest.py cv 报 NameError: name 'cv_choose' is not defined:我用追加的方式把 cv_choose 写到了文件末尾,而入口代码在它前面,运行到入口时函数还没定义。pytest 却全部通过,因为测试是 import 模块,整个文件都执行完了才调用函数。把入口代码移到文件最后后正常。所以除了测试,命令行入口也要实际跑一次。

常见问题

现象 原因 处理
验证误差比基线还差 树太深,过拟合 限制 max_leaf_nodes
每次结果不同 没固定 random_state 固定种子;用交叉验证
训练误差为 0 模型记住了训练集 只看验证误差
数据太少 小数据集波动大 用交叉验证

模板规则

  • 一定要有验证集或交叉验证。
  • 和基线比较。
  • 小数据用交叉验证选参数。
  • 固定随机种子便于复现。

项目资源

用途 地址
Kaggle Learn https://www.kaggle.com/learn
Kaggle 使用条款 https://www.kaggle.com/terms
scikit-learn 常见陷阱(官方) https://scikit-learn.org/stable/common_pitfalls.html
Intro to Machine Learning(Kaggle Learn) https://www.kaggle.com/learn/intro-to-machine-learning
load_diabetes(scikit-learn) https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_diabetes.html

先建立“训练误差和验证误差要分开看”的直觉,后面的课程都建立在这上面。