训练误差最低的模型,往往不是测试误差最低的那个。 一次切分不算数,五折交叉验证看的是平均分和标准差两样。 先跑一个最简单的基准模型,是所有机器学习项目的标准起手式。
这一篇对应教材 17.1 与 17.2:选定最佳的演算法/模型、实作机器学习模型、实作程序。 第 16 章把概念讲完了,这一篇开始选:五个候选摆在桌上,拿数据说话。
一、这次用一份真数据
不再自造了,用 scikit-learn 自带的一份医学数据:442 条样本、10 个特征,
标签是「一年后病情进展指标」,最小值 25.0、最大值 346.0、平均 152.1。
十个特征的名字是 age、sex、bmi、bp、s1 到 s6。
这十个特征在数据集里已经标准化过(均值 0、方差 1),所以量级一致——这一点在第五节会用到。
标签是连续的数,所以这是回归任务。回归的评估最简单:算误差,越小越好。
换真数据有个好处:自造数据往往太干净。第 16 章那份房价数据,一条直线就能拟合到 3% 误差; 真实数据不会这么客气。
二、五个候选,同一份切分
| 模型 | 训练误差 | 测试误差 | 训练耗时 ms |
|---|---|---|---|
| 线性回归 | 52.9542 | 55.6518 | 1.4 |
| 岭回归 alpha=1 | 58.4505 | 56.9076 | 1.3 |
| 决策树 depth=4 | 46.3829 | 66.2915 | 1.5 |
| 随机森林 100 棵 | 21.8555 | 59.3601 | 272.1 |
| 梯度提升 100 棵 | 27.8986 | 60.5819 | 120.9 |
切的是 7 比 3:训练 309 条、测试 133 条。五个模型吃的是同一份切分,不然比出来的东西没有意义。 耗时那一列值得看一眼:随机森林 272 毫秒,线性回归 1.4 毫秒,差两百倍。 这份数据只有 442 条,差距还不算夸张;换成几十万条,这个差距会变成「能跑」和「跑不动」的区别。
三、两个「最低」不在同一行
训练误差最低的是随机森林(21.8555),测试误差最低的是线性回归(55.6518)。两个答案不一样。 随机森林把训练误差压到 21.86,测试误差却还有 59.36——这就是第 16 章那个过拟合,只是换了身衣服: 一百棵树把训练集记得很牢,换一批数据就露馅。 所以「哪个模型好」这个问题,不能拿训练误差回答。 这句话在第 16 章说过一遍,这里用真数据又验证了一遍。
还有一个细节:岭回归的训练误差(58.45)比线性回归(52.95)高,测试误差也高(56.91 对 55.65)。
加了正则反而更差,说明这份数据上正则的收益抵不过它带来的偏差。
这就是「调参」的真相:没有哪个参数是普遍更好的。alpha=1 在这里不划算,
换个数据集可能就很划算。
四、一次切分不算数
| 模型 | 五折平均 | 标准差 |
|---|---|---|
| 线性回归 | 54.6918 | 1.3745 |
| 岭回归 alpha=1 | 58.4492 | 2.0029 |
| 决策树 depth=4 | 64.3202 | 3.5177 |
| 随机森林 100 棵 | 57.7409 | 2.0355 |
| 梯度提升 100 棵 | 58.4502 | 2.1634 |
交叉验证做的是:把数据切成五份,每次拿四份训练、一份测试,转五圈取平均。 每一份数据都当过一次测试集,所以这个数比单次切分稳得多。 看标准差那一列:决策树的 3.52 最大,线性回归的 1.37 最小。 标准差大说明「换一批测试数据,结果就变」。 一个模型好不好,既要看平均分,也要看它稳不稳。两个模型平均分一样、一个标准差 0.5 一个 3.0, 选前者——因为你不知道上线那天会碰到哪一批数据。
五次具体数字也值得看:决策树从 58.33 到 67.68,跨了近十分。 这种模型放到线上,用户会觉得「今天怎么这么不准」。
五、标准化:同一个 alpha 不是一回事
| 做法 | 测试误差 |
|---|---|
直接喂 Ridge(alpha=1) |
56.9076 |
先 StandardScaler 再 fit |
55.7106 |
这份数据整体已经标准化过一次,但在这 309 条训练子集上再标一遍,结果还是变了——低了 2.10%。
原因是:alpha=1 在两种尺度下不是同一件事。正则惩罚的是系数的绝对值,尺度一变,系数也跟着变,
同样的 alpha 就成了不同的强度。
所以工程上的做法是:不管数据有没有标准化过,都套一个 StandardScaler。代价几乎为零,
收益是「换个数据集也不会翻车」。
顺带一条纪律:scaler 必须只在训练集上 fit。拿全量数据 fit 再切分,
测试集的均值方差就漏进了训练过程——这叫数据泄漏,评估结果会虚高,而且上线才发现。
用 make_pipeline 把两步串起来,能自动避开这个坑:fit 时它只在训练集上算,
predict 时用存下来的参数。
六、结果有点意外:简单模型全赢了
| 维度 | 线性回归 | 梯度提升 100 棵 |
|---|---|---|
| 单次测试误差 | 55.6518 | 60.5819 |
| 五折平均 | 54.6918 | 58.4502 |
| 五折标准差 | 1.3745 | 2.1634 |
| 训练耗时 ms | 1.4 | 120.9 |
四项里线性回归赢了四项:误差更低、更稳、快 86 倍。 最可能的解释是:这 10 个特征和标签的关系本来就以线性成分为主。 线性模型能到 55.65,说明大部分规律已经被它抓住了; 复杂的模型没有额外的东西可学,只多拟合了一点噪声。 这正是「先跑一个基准模型」的价值。如果一上来就上梯度提升, 你会看到它比决策树好(60.58 对 66.29),以为调得不错——实际上还不如一点四毫秒就能算完的线性回归。
教材 17.1 讲「选定最佳的演算法/模型」,选的标准就是这个: 在同一份切分、同一套指标下比,谁的数字好看选谁,不许换着指标比。 换着比有多容易出错:用训练误差比,会选随机森林;用测试误差比,会选线性回归; 用交叉验证比,还是线性回归。指标一换,结论就翻。
七、训好的模型怎么存
| 模型 | pickle 体积 | 加载耗时 |
|---|---|---|
| 梯度提升 100 棵 | 128.2 KB | 4.02 ms |
| 线性回归 | 0.6 KB | 0.33 ms |
体积差 208 倍。这个差会直接反映到镜像大小和冷启动时间上——第 15 章那些部署参数,根子在这儿。
读回来之后预测误差 60.5819,跟存之前一模一样。pickle 存的确实是完整的模型状态。
但 pickle 有个坑:它把类的路径也存进去了。用 scikit-learn 1.9 存的模型,
拿到装了 1.5 的机器上可能读不出来,或者读出来行为不对。
所以线上环境的 scikit-learn 版本要跟训练时钉成同一个——下一篇那行 == 就是为这个。
存整个模型还是只存权重?第 13 章算过:RandomForest 存整个 1058.5 KB、只存权重 210.9 KB。 这一课用 pickle 存整个,省事;要跨版本就存权重。
八、实作程序:五步,不许跳
| 步 | 做什么 | 产出 |
|---|---|---|
| 1 定任务 | 回归还是分类,标签是什么 | 一句话 |
| 2 备数据 | 切分、标准化 | 训练集与测试集 |
| 3 选模型 | 基准模型先跑,再上复杂的 | 一张对比表 |
| 4 评估 | 交叉验证看平均与标准差 | 一个数加减误差 |
| 5 存盘 | pickle,记下版本号 | 一个 .pkl 文件 |
教材 17.2.1 讲「实作程序」,顺序就是这五步。 跳步最常见的是从 1 直接跳到 3:没切分就开始调模型,调出来的数全是训练误差,没有参考价值。 第二步和第四步是最容易被省略的两步,也是最值钱的两步。模型本身有现成的库,切分和评估没有。 下一篇开始做第二件事:把这一整套流程包成一个别人能调的服务。先定契约,再动代码。
九、动手 Demo:选模型:五个候选同场比,再用交叉验证复核
这个 Demo 用 scikit-learn 自带的糖尿病数据,把五个模型放在同一份切分上比, 再用五折交叉验证复核一遍。 第三节那个「两个最低不在同一行」,是整篇的转折点。
cands = [
("线性回归", LinearRegression()),
("岭回归 alpha=1", Ridge(alpha=1.0)),
("决策树 depth=4", DecisionTreeRegressor(max_depth=4)),
("随机森林 100 棵", RandomForestRegressor(n_estimators=100)),
("梯度提升 100 棵", GradientBoostingRegressor()),
]
for name, mdl in cands:
mdl.fit(Xtr, ytr)
print(name, rmse(ytr, mdl.predict(Xtr)), rmse(yte, mdl.predict(Xte)))
实跑输出:
--------------------------------------------------------------------
选模型:五个候选同场比,再用交叉验证复核
--------------------------------------------------------------------
① 这次用一份真数据
--------------------------------------------------------------------
样本 442 条,特征 10 个
特征名 -> age、sex、bmi、bp、s1、s2、s3、s4、s5、s6
标签(一年后病情进展指标)-> 最小 25.0,最大 346.0,平均 152.1
! 这是 scikit-learn 自带的数据,不用联网。
442 条、10 个特征,十个特征都已经做过标准化(均值 0、方差 1),所以量级一致。
! 标签是连续的数,所以这是回归任务。三类任务里,回归的评估最简单:算误差,越小越好。
② 五个候选,同一份切分
--------------------------------------------------------------------
切 7 比 3 -> 训练 309 条,测试 133 条
| 模型 | 训练误差 | 测试误差 | 训练耗时 ms | 预测耗时 ms |
|---|---:|---:|---:|---:|
| 线性回归 | 52.9542 | 55.6518 | 1.4 | 0.45 |
| 岭回归 alpha=1 | 58.4505 | 56.9076 | 1.3 | 0.37 |
| 决策树 depth=4 | 46.3829 | 66.2915 | 1.5 | 0.46 |
| 随机森林 100 棵 | 21.8555 | 59.3601 | 272.1 | 29.88 |
| 梯度提升 100 棵 | 27.8986 | 60.5819 | 120.9 | 1.40 |
! 训练误差最低的是 随机森林 100 棵(21.8555),但测试误差最低的是 线性回归(55.6518)。
两个答案不一样。
! 随机森林把训练误差压到 21.8555,测试误差却还有 59.3601——这就是上一篇那个过拟合,
只是换了身衣服。一百棵树把训练集记得很牢,换一批数据就露馅。
! 但代价在耗时那一列:随机森林训练 272 毫秒,线性回归 1.4 毫秒。
! 这就是「选模型」真实的形状:不是找误差最小的那个,是在误差、耗时、可解释性、
维护成本之间挑一个。
③ 一次切分不算数:五折交叉验证
--------------------------------------------------------------------
| 模型 | 五折平均 | 标准差 | 五次分别是 |
|---|---:|---:|---|
| 线性回归 | 54.6918 | 1.3745 | 52.72/55.03/56.90/54.85/53.95 |
| 岭回归 alpha=1 | 58.4492 | 2.0029 | 57.50/59.58/60.14/54.94/60.09 |
| 决策树 depth=4 | 64.3202 | 3.5177 | 58.33/62.47/65.80/67.68/67.33 |
| 随机森林 100 棵 | 57.7409 | 2.0355 | 55.04/55.72/59.56/58.25/60.14 |
| 梯度提升 100 棵 | 58.4502 | 2.1634 | 56.69/55.20/60.02/59.33/61.00 |
! 交叉验证做的是:把数据切成五份,每次拿四份训练、一份测试,转五圈取平均。
每一份数据都当过一次测试集,所以这个数比单次切分稳得多。
! 看标准差那一列:决策树的 3.52 最大,线性回归 1.37 最小。
! 标准差大说明「换一批测试数据,结果就变」。一个模型好不好,既要看平均分,也要看它稳不稳。
④ 标准化对线性模型的影响
--------------------------------------------------------------------
直接喂 -> 测试误差 56.9076
先标准化 -> 测试误差 55.7106
两者差 1.1970(标准化这边低了 2.10%)
! 这份数据整体已经标准化过一次,但在这 309 条训练子集上再标一遍,结果还是变了。
因为 alpha=1 在两种尺度下不是同一件事:正则惩罚的是系数的绝对值,尺度一变,系数也跟着变,
同样的 alpha 就成了不同的强度。
! 所以工程上的做法是:不管数据有没有标准化过,都套一个 StandardScaler。
代价几乎为零,收益是「换个数据集也不会翻车」。
! 顺带一个纪律:StandardScaler 必须只在训练集上 fit。
拿全量数据 fit 再切分,测试集的均值方差就漏进了训练过程,这叫数据泄漏,评估结果会虚高。
⑤ 结果有点意外:简单模型全赢了
--------------------------------------------------------------------
| 维度 | 线性回归 | 梯度提升 100 棵 |
|---|---|---|
| 单次测试误差 | 55.6518 | 60.5819 |
| 五折平均 | 54.6918 | 58.4502 |
| 五折标准差 | 1.3745 | 2.1634 |
| 训练耗时 ms | 1.4 | 120.9 |
! 四项里线性回归赢了四项:误差更低、更稳、快 86 倍。
! 最可能的解释是:这 10 个特征和标签的关系本来就以线性成分为主。
线性模型能到 55.6518,说明大部分规律已经被它抓住了;
复杂的模型没有额外的东西可学,只多拟合了一点噪声。
! 这正是「先跑一个基准模型」的价值。
如果一上来就上梯度提升,你会看到它比决策树好(60.5819 对 66.2915),
以为调得不错——实际上还不如几毫秒就能算完的线性回归。
! 教材 17.1 讲「选定最佳的演算法/模型」,选的标准就是这个:在同一份切分、同一套指标下,
谁的数字好看选谁,不许换着指标比。
⑥ 训好的模型怎么存
--------------------------------------------------------------------
梯度提升 -> 128.2 KB,加载 4.02 ms
线性回归 -> 0.6 KB,加载 0.33 ms
读回来的梯度提升预测误差 60.5819(和存之前一样)
! 体积差了 208 倍。这个差会直接反映到镜像大小和冷启动时间上——第 15 章那些部署参数,根子在这儿。
! pickle 有个坑:它把类的路径也存进去了。
用 scikit-learn 1.9 存的模型,拿到装了 1.5 的机器上可能读不出来,或者读出来行为不对。
所以线上环境的 scikit-learn 版本要跟训练时钉成同一个。
! 存整个模型还是只存权重?第 13 章算过:RandomForest 存整个 1058.5 KB、只存权重 210.9 KB。
这一课用 pickle 存整个,省事;要跨版本就存权重。
第二节耗时那一列波动较大,每次跑都不一样;误差那几列是稳定的。 第三节两个「最低」分别是随机森林和线性回归,差得还挺远。 第六节体积差 208 倍,这个数会决定镜像多大。
30 秒实验
- 把
test_size改成 0.15 再跑一遍第二节,看五个模型的排序变了没有。 - 把随机森林的
n_estimators从 100 改成 500,看测试误差降不降、耗时涨多少。 - 想一想:为什么标准化的 scaler 只能 fit 训练集?
常见误区
- 拿训练误差选模型:一定选到最复杂的那个。
- 只切一次就下结论:换个
random_state排序就变。 - 不看标准差:平均分一样,稳定性可能差六倍。
- 直接上复杂模型:基准都没跑,不知道复杂模型是不是白忙。
- scaler 在全量数据上 fit:数据泄漏,评估虚高。
自测三问(点开对答案)
1. 为什么训练误差最低的模型不能选? —— 它会记住训练集,换一批数据就露馅;要看测试误差或交叉验证。 2. 交叉验证比单次切分好在哪? —— 每份数据都当过一次测试集,得到的是一组数,能看平均分和稳定性。 3. 为什么 scaler 只能 fit 训练集? —— 用全量数据 fit 会把测试集的均值方差漏进训练,评估结果虚高。模型选定了,下一篇把这套流程做成一个别人能调的接口,先从契约写起。
- ☐ (本篇完,下一篇 17.2 API 规格与开发准备)