Skip to content

第 23 章 · 四路横评:模型们其实差得不多 ​

这一章要解决什么:把前面 17 章学过的模型放到同一张桌子上比一次。 这是全书的一个转折点 —— 你会得到一个尴尬但重要的结论。

最容易踩的坑:四路用了不同的划分或不同的预处理 → 数字放一起比毫无意义。横评的前提是"同一份划分、同一套指标、同一套特征"。

本章你需要完成的代码:demos/ch23-model-comparison.py

23.1 横评的前提:三个"同一" ​

先记住这一条,否则后面的表全是废的:

横评 = 同数据 + 同划分 + 同指标 + 同特征。少一个,比较就失效。

真实输出:

text
四路横评:同一份数据、同一套指标、同一份划分
  训练 17,244 行 / 48 支设备
  测试 3,995 行 / 12 支设备
  两边设备有交集:False  ← 必须是 False
  提升模型:HistGB(替 XGBoost,300棵)

四路用的是同一份 tr / te 索引:

python
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
tr, te = next(gss.split(X, y, groups=groups))       # ← 只划分一次

Xtr, Xte, ytr, yte = X.iloc[tr], X.iloc[te], y.iloc[tr], y.iloc[te]

# 四个模型都用这同一份 Xtr/Xte
for name, model in MODELS:
    model.fit(Xtr, ytr)
    pred = model.predict(Xte)
    r2 = r2_score(yte, pred)          # ← 都用这同一套 yte

★ 为什么这件事是硬性要求

如果模型 A 用 random_state=42 划分、模型 B 用 random_state=7
   ↓
A 抽到 12 支"好预测"的设备,B 抽到 12 支"难预测"的
   ↓
A 的 R² 比 B 高 —— 但这个差距来自【抽签】,不是模型
   ↓
第 11 章已经量化过:换个 random_state,同一个模型的分差可达 0.036

★ 而这里的陷阱更隐蔽:因为"更好的模型"赢了一点, 你会自然地归因于"模型更强",而不会想到是划分的差异。

点开:一个真实项目里的横评事故

场景:团队要选模型,三个人各跑一个,交上来三份结果。

text
小王:随机森林     R² = 0.913
小李:XGBoost      R² = 0.928
小张:LSTM         R² = 0.941

结论:LSTM 最好,上 LSTM。

三个月后上线翻车。排查发现问题

人数据划分特征预处理
小王按时间切(正确)7 个在 Pipeline 里缩放
小李按时间切12 个(多加了 5 个)全表缩放(泄漏)
小张按行随机(泄漏!)7 个全表缩放(泄漏)

三个人的数字根本不是一件事:

小张的 0.941 —— 按行随机划分,同一设备的相邻窗口跨集 → 虚高
小李的 0.928 —— 多加了 5 个特征(可能有用),但缩放泄漏
小王的 0.913 —— 唯一干净的那个

★ 教训:

横评之前,先对齐"方法"再对齐"数字"。

一个可操作的检查清单(打印出来贴在工位上):

□ 四路用的是同一份划分吗?(同一组 tr/te 索引)
□ 四路用的是同一个 y 吗?(同一份测试标签)
□ 四路用的是同一套特征吗?(同一个 FEATS 列表)
□ 预处理都在 Pipeline 里吗?(防止逐路泄漏程度不同)
□ 都固定 random_state 了吗?
□ 指标是同一个函数吗?(别一个用 MAE 一个用 RMSE)

这六条里任意一条没对齐,那张对比表就不能用。

23.2 四路横评的结果 ​

真实输出:

text
  模型                                  MAE     RMSE       R²      训练        单行
  线性回归(+标准化)                      0.00968  0.01217  0.95053   0.01s    0.000ms
  HistGB(替 XGBoost,300棵)          0.00787  0.01034  0.96429  15.22s    0.063ms
  随机森林(200棵)                      0.00790  0.01023  0.96503   6.27s    0.022ms
  高斯过程 GPR(600行,仅cycle)           0.01768  0.02288  0.82518   0.03s    0.005ms

★ 先读前三个(这是一个公平的比较)

模型MAERMSER²训练耗时
线性回归(+标准化)0.009680.012170.950530.01 s
梯度提升(HistGB 300 棵)0.007870.010340.9642915.22 s
随机森林(200 棵)0.007900.010230.965036.27 s

★ 关键数字:

三个主流模型的 R² 极差 = 0.96503 - 0.95053 = 0.01449

从最朴素的线性回归,到 200 棵树的随机森林,R² 只涨了 0.0145。

而 MAE 从 0.00968 降到 0.00790 —— 相当于"平均误差从 0.97 个百分点降到 0.79 个百分点"。

这个幅度值不值 6 秒训练时间? 看你后面会看到的"卡点在哪"。

点开:这张表里最该注意的三件事

① 线性回归就已经 0.9505 —— 它几乎赢了 85%

从 0 到 0.9505:靠的是"加特征"(7 个特征 vs 1 个特征)
从 0.9505 到 0.9650:靠的是"换模型"(线性 → 树)
       ↑
   后者只贡献了 0.0145

对比第 13 章的数字:

单特征线性  : 0.82070
七特征线性  : 0.94253    ← 加特征涨了 0.12183
七特征森林  : 0.96503    ← 换模型涨了 0.02250

加特征带来的提升,是换模型带来的 5 倍以上。

② 随机森林和梯度提升几乎打平(0.96503 vs 0.96429),差距 0.0007

这推翻了一个常见的直觉:

常见直觉:"梯度提升是'更高级'的算法,应该更准"
    ↓
实测:两者只差 0.0007,而随机森林训练快 2.4 倍(6.27s vs 15.22s)
    ↓
结论:【在这个数据上,随机森林是更划算的选择】

★ 这引出一个重要的工程观念:

"更强的算法"不等于"更好的选择"。 当两个模型的精度只差 0.001 时,训练时间、维护成本、可解释性才是决定因素。

③ 训练耗时差异巨大,而它常被忽略

线性回归   :  0.01 s
随机森林   :  6.27 s      (627 倍)
梯度提升   : 15.22 s      (1522 倍)

在离线场景(每周跑一次报告)这无所谓; 在实时场景(每条数据都要预测)就完全不同 —— 不过要注意:

训练耗时 ≠ 预测耗时

看"单行预测"那一列:线性回归是 0.000ms(微秒级)、随机森林 0.022ms。 预测都很快 —— 部署时更关心的是这个。

★ 一个容易混淆的点:

指标什么时候重要
训练耗时离线批处理、需要频繁重训(每天/每小时)
单行预测耗时在线服务、实时决策(有延迟预算)
模型大小边缘设备部署(内存有限)

第 26 章选型时,这三个要分开考虑。

⚠️ 一个关于"耗时"的提醒:树模型的训练耗时在不同运行之间波动很大 (受并行调度和机器负载影响,同一设置可能差 2 倍以上)。 所以报耗时请报数量级("毫秒级 / 秒级 / 十秒级"),不要报"快 7.6 倍"这种精确倍数 —— 换个环境就不成立了。R² 不受这个影响,它是稳定的。

23.3 ★ 这一章真正的结论 ​

真实输出:

text
  ★ 三个主流模型(线性/提升/森林)的 R² 极差 = 0.01449
    → 换模型带来的收益只有 0.0145;这个量级说明【卡点不在模型】

★ 这是全书最重要的结论之一。

换个角度读这个数字:

数据里"可预测的部分"(R² 的上限)大约在 0.96 附近
   ↓
线性回归拿到了 0.9505(占上限的 98.7%)
随机森林拿到了 0.9650(占上限的 100%)
   ↓
从线性到森林,我们只挤出了 1.3% 的剩余空间
   ↓
剩下的 3.5%(1 - 0.965)去哪了?
   ↓
【是数据里的测量噪声 —— 任何模型都消不掉】

为什么剩下的部分消不掉

第 10 章讲过这份数据的误差模型:每个通道有"每支设备固定的零点偏置"

  • "每行独立的测量波动"。这两层误差都不是"特征 → 标签"的函数关系, 所以任何模型都无法从单行特征里把它们分离出来。

★ 这就是"卡点不在模型"的含义:

你换更强的模型(更深的树、更大的集成、甚至神经网络)
   ↓
能改善的只有那 1.3% 的空间
   ↓
而真正的水面下还有 3.5% 的噪声,和一大片"没被特征表达出来的信息"

真实输出里的另一句提醒:

text
  ⚠️ GPR 的 R² 明显低,但它是【只喂 600 行 + 只用一个特征】——
     它的卖点是 ±σ 置信带(第 24 章),不是精度。不能这样比。

★ 这里有一个"如何读表格"的教训

GPR 的 R² 是 0.82518,看起来"最差"。但这个比较本身就是错的:

线性/森林/提升  :  17,244 行 训练,  7 个特征
GPR             :     600 行 训练,  1 个特征
                        ↑            ↑
                    少 28 倍      少 6 倍

★ 一条通用纪律:

看到"某模型明显更差"时,先问一句"它们是在同等条件下比的吗"。

同样"看起来更好"也要问。这是这本书反复训练的反应。

点开:那"卡点不在模型"之后,该怎么办?

这是个关键的分岔路口,走错方向会浪费几周。

横评结果:换模型只有 0.0145 的收益
   ↓
三个可能的方向:

方向 A:继续换/调模型
   → 期望收益:很小(上限就在 0.96 附近)
   → 代价:时间
   → 结论:【不推荐】

方向 B:加数据
   → 期望收益:需要先看学习曲线(第 21 章)
   → 第 21 章实测:验证线在 8,000 行后就平了 → 【加数据收益也很小】

方向 C:改特征
   → 期望收益:可能很大(因为"没被表达出来的信息"就在这)
   → 代价:需要领域知识 + 试错
   → 结论:【推荐】

★ 判据是第 21 章的学习曲线

验证线还在涨  →  选 B(加数据)
验证线已走平  →  选 C(改特征)
两者都不是    →  选 A(但收益有限)

第 21 章的实测结论:

text
      训练规模       训练 R²       验证 R²        差距
         2832      0.9992      0.9473   +0.0519
         5664      0.9990      0.9517   +0.0473
         8496      0.9988      0.9579   +0.0409
        11328      0.9986      0.9584   +0.0402
        14160      0.9985      0.9582   +0.0403
                                      ↑
                            从 8,496 行起就基本不动了

样本量翻 5 倍(2,832 → 14,160),验证 R² 只从 0.9473 涨到 0.9582(+0.011)。

所以:加数据的边际收益已经很低了。

★ 那"改特征"具体是什么意思?

不是"加更多同类的测量列",而是"构造出能表达【个体差异】和【变化趋势】的特征":

当前特征都是"这个窗口内的瞬时状态":
  cycle、平均电压、平均温升、内阻、库仑效率、衰减速率、温度标准差

缺的是"这支设备【和历史相比】怎么样":
  · 相对自身基线的偏移(这支设备现在 vs 它自己早期)
  · 变化率(最近 100 圈 vs 前 100 圈)
  · 与"同批次同龄设备"的比较

★ 这就是"特征工程",而它是下一本书的主题。

这本书在第 27 章会把接力棒交出去,并说明"为什么现在该做特征,而不是继续换模型"。

而你现在已经知道了判断依据:

① 横评:换模型只有 0.0145 的收益     ← 这一章
② 学习曲线:验证线走平,加数据没用    ← 第 21 章
③ 所以:剩下的空间在特征里            ← 下一本书

★ 一个可以带走的思维模式

遇到"效果不够好",不要问"换个什么模型",而要问:

"我现在离【数据的上限】还有多远?
 而那个上限,是由什么决定的?"

模型只决定"你离上限多近";上限本身由数据和特征决定。

23.4 四路横评的完整对照表 ​

除了分数,选型还要看"能不能给不确定性"这类能力:

text
  模型                  速度            可解释性            不确定性    适用场景
  线性 / 岭回归         快              高(系数即公式)    无          基线;可解释性要求高时首选
  随机森林              中              中(重要性)        无          表格数据稳健起点,几乎不用调参
  XGBoost / LightGBM    中              中低                无          精度优先;有缺失值也扛得住
  高斯过程 GPR          慢(O(n³))     低                  ★ 有 ±σ     样本少、要给置信区间时
  SVR / KNN             中              低                  无          小样本;特征必须缩放

★ 这张表的第四列是全书的伏笔:

前三路(线性/森林/提升):都不给不确定性
GPR                    :给 ±σ

这就是第 24~26 章的主题 —— "精度"不是唯一的竞争维度。

点开:为什么"可解释性"在真实项目里常常比精度更重要

一个真实的决策场景:

模型 A:R² = 0.965,随机森林
  你可以说:"预测值是 0.815。"
  工程师问:"为什么?"
  你:"……树太多了,说不清。"

模型 B:R² = 0.950,线性回归
  你可以说:"预测值是 0.815 —— 主要是循环次数贡献了 -0.02,
          库仑效率贡献了 +0.01。"
  工程师:"那我如果改善哪个环节,能提升?"
  你:"按模型看,库仑效率的边际影响更大。"

★ 模型 B 的 R² 更低,但它能支撑决策。

什么场景需要可解释性:

场景为什么需要
要指导改进(该优化哪个环节?)需要知道"哪个因素影响大"
要说服别人(凭什么信你的预测?)需要一个能讲通的逻辑
要合规(为什么拒绝了这个申请?)法规要求给出理由
要排错(模型为什么错了?)需要定位是哪个特征导致的

什么场景不需要:

纯预测、不需要解释、没有监管要求
  例:天气预报的数值模式、推荐系统的排序分

★ 一个折中方案:用简单模型做解释,用复杂模型做预测

python
# 用随机森林预测(精度高)
rf = RandomForestRegressor(n_estimators=200, random_state=42).fit(Xtr, ytr)

# 用线性回归解释方向(系数可读)
lin = LinearRegression().fit(Xtr, ytr)
for name, coef in zip(FEATS, lin.coef_):
    print(f"{name:<30} {coef:>+10.5f}")

这就是"双模型策略" —— 一个负责准,一个负责讲。 第 26 章会再看到它(GPR + GBDT 两条腿走路也是同一思路)。

23.5 常见坑 ​

text
① 四路用了不同的划分 → 差距可能全部来自"抽签"(分差可达 0.036)
② 四路预处理泄漏程度不同(有的在 Pipeline 里,有的全表 fit)→ 不可比
③ 四路特征数不同却不说明 → 读者以为在比模型,其实在比特征
④ 用训练耗时评价"速度" → 部署时关心的是【单行预测耗时】和模型大小
⑤ 看到"某模型明显更差"就下结论 → 先问"它们是在同等条件下比的吗"(GPR 就是反例)
⑥ 横评完就结束 → 真正的结论是"卡点不在模型",要接着往特征走

23.6 完整示例与输出 ​

bash
cd code
python demos/ch23-model-comparison.py

真实输出:

text
四路横评:同一份数据、同一套指标、同一份划分
  训练 17,244 行 / 48 支设备
  测试 3,995 行 / 12 支设备
  两边设备有交集:False  ← 必须是 False
  提升模型:HistGB(替 XGBoost,300棵)

  模型                                  MAE     RMSE       R²      训练        单行
  线性回归(+标准化)                      0.00968  0.01217  0.95053   0.01s    0.000ms
  HistGB(替 XGBoost,300棵)          0.00787  0.01034  0.96429  15.22s    0.063ms
  随机森林(200棵)                      0.00790  0.01023  0.96503   6.27s    0.022ms
  高斯过程 GPR(600行,仅cycle)           0.01768  0.02288  0.82518   0.03s    0.005ms

  ★ 三个主流模型(线性/提升/森林)的 R² 极差 = 0.01449
    → 换模型带来的收益只有 0.0145;这个量级说明【卡点不在模型】
  ★ 最优:随机森林(200棵)(R² 0.96503)

本章练习 ​

练习 1:亲手做一次公平横评

在同一份按设备划分的数据上,比较三个模型:线性回归(+标准化)、随机森林(100 棵)、HistGB。

要求:

  1. 四路必须共用同一组 tr/te 索引;
  2. 打印 MAE / RMSE / R² / 训练耗时;
  3. 计算三个 R² 的极差,并写一句话结论。
参考答案
python
import time

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, r2_score, root_mean_squared_error
from sklearn.model_selection import GroupShuffleSplit
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv("data/battery/soh_features.csv")
FEATS = ["cycle", "mean_charge_voltage_v", "mean_temp_rise_c", "resistance_mohm",
         "coulombic_eff", "cap_fade_rate_ah_per_100", "temp_std_c"]
X, y, groups = df[FEATS], df["soh"], df["cell_id"]

# ① 只划分一次,三路共用
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
tr, te = next(gss.split(X, y, groups=groups))
Xtr, Xte, ytr, yte = X.iloc[tr], X.iloc[te], y.iloc[tr], y.iloc[te]
print(f"训练 {len(tr):,d} 行 / 测试 {len(te):,d} 行")

MODELS = [
    ("线性回归(+标准化)", Pipeline([("s", StandardScaler()), ("m", LinearRegression())])),
    ("随机森林(100棵)", RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)),
    ("HistGB", HistGradientBoostingRegressor(max_iter=300, learning_rate=0.05,
                                             max_depth=4, random_state=42,
                                             early_stopping=False)),
]

print(f"\n{'模型':<22}{'MAE':>10}{'RMSE':>10}{'R²':>10}{'训练':>9}")
r2s = []
for name, model in MODELS:
    t0 = time.time()
    model.fit(Xtr, ytr)
    fit_s = time.time() - t0
    pred = model.predict(Xte)
    r2 = r2_score(yte, pred)
    r2s.append(r2)
    print(f"{name:<22}{mean_absolute_error(yte, pred):>10.5f}"
          f"{root_mean_squared_error(yte, pred):>10.5f}{r2:>10.5f}{fit_s:>8.2f}s")

print(f"\n三路 R² 极差 = {max(r2s) - min(r2s):.5f}")

期望输出:

text
训练 17,244 行 / 测试 3,995 行

模型                           MAE      RMSE        R²       训练
线性回归(+标准化)               0.00968   0.01217   0.95053     0.01s
随机森林(100棵)                 0.00791   0.01026   0.96487     2.54s
HistGB                       0.00787   0.01034   0.96429    11.26s

三路 R² 极差 = 0.01434

③ 一句话结论

从最朴素的线性回归到 100 棵随机森林,R² 只从 0.95053 涨到 0.96487(极差 0.0143)。 这说明在这份数据上,【换模型】能挤出的空间已经很小了 —— 真正卡住效果的不是模型,而是特征与数据本身的噪声上限。

★ 三个观察

观察一:线性回归就已经吃掉了大部分可预测性

线性回归 0.95053  /  最好 0.96487  =  98.5%
        ↑
  最朴素的模型拿到了 98.5% 的成绩

观察二:随机森林 100 棵 vs 200 棵,收益已经很小

100 棵:0.96487
200 棵:0.96503      ← 翻倍棵树只涨 0.0002

这是第 17 章讲过的"饱和"现象。

观察三:耗时差异很大,但这是【训练】耗时

如果关心的是"上线后每条数据预测要多久",
就要单独测 predict 的耗时(本 demo 也打印了那一列)

★ 这个练习最重要的收获

不是"我学会了横评",而是"我看到了换模型的天花板"。

这个认知会让你在真实项目里少走很多弯路 —— 当有人建议"换个更高级的模型试试"时,你可以回答:

"我们先测一下换模型的收益上限。 如果横评的极差只有 0.01,那说明问题不在模型。"

练习 2:找出"横评无效"的三个错误

下面三段代码各有一处让横评失效的错误,请指出并改正:

python
# 版本 A
r2_lr = r2_score(yte, Pipeline([("s", StandardScaler()),
                                ("m", LinearRegression())]).fit(Xtr, ytr).predict(Xte))
r2_rf = r2_score(yte, RandomForestRegressor(random_state=42).fit(X, y).predict(X))
python
# 版本 B
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42)
print(f"线性 {r2_score(yte, LinearRegression().fit(Xtr, ytr).predict(Xte)):.5f}")
print(f"森林 {r2_score(yte, RandomForestRegressor(random_state=42).fit(Xtr, ytr).predict(Xte)):.5f}")
python
# 版本 C
scores = {}
scores["线性"] = r2_score(yte, LinearRegression().fit(X, y).predict(Xte))
scores["森林"] = r2_score(yte, RandomForestRegressor(random_state=42).fit(Xtr, ytr).predict(Xte))
参考答案

版本 A 的错误:用整表训练 + 在训练集上评估

python
r2_rf = r2_score(yte, RandomForestRegressor(...).fit(X, y).predict(X))
#                     ↑                ↑        ↑
#              用的是测试标签    用【全量数据】训练   在【全量数据】上预测

问题有两层:

① 训练用了 X(含测试集)→ 泄漏
② 预测也是 X,而 yte 只对应测试部分 → 形状/语义完全对不上

改正:

python
r2_rf = r2_score(yte, RandomForestRegressor(random_state=42).fit(Xtr, ytr).predict(Xte))

版本 B 的错误:按行随机划分

python
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42)
#                     ↑ 没有 groups=

问题:

同一支设备的相邻窗口一个进训练、一个进测试
   ↓
模型"背邻居"就能答对
   ↓
两个模型的分数都虚高,而且【虚高程度可能不同】
   (线性模型背不了那么细,树可以 → 差距被夸大)
   ↓
结论完全不可信

改正:

python
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
tr, te = next(gss.split(X, y, groups=df["cell_id"]))
Xtr, Xte, ytr, yte = X.iloc[tr], X.iloc[te], y.iloc[tr], y.iloc[te]

版本 C 的错误:线性模型用全量训练,森林用训练集 —— 不公平

python
scores["线性"] = r2_score(yte, LinearRegression().fit(X, y).predict(Xte))
#                                                  ↑ 全量(含测试集)
scores["森林"] = r2_score(yte, RandomForestRegressor(...).fit(Xtr, ytr).predict(Xte))
#                                                       ↑ 只用训练集

问题:

线性模型见过测试数据 → 它的分数被抬高了
   ↓
如果你因此得出"线性模型和森林差不多"的结论 → 错
   (真实差距应该更大,因为线性模型本来更弱)

★ 这是最隐蔽的一种"不公平":

因为泄漏的模型分数更高,你会误以为"它更强"。

改正:三路统一用 Xtr。

★ 三个版本的共同教训

版本错误后果
A训练用全量 + 预测用全量完全错位
B按行随机划分分数虚高,且虚高程度因模型而异
C各路训练集不同不公平,且偏向泄漏更严重的那路

一个总结性的判断法则:

横评时,除了"模型"这一个变量,其他所有条件都必须完全相同。

一个可执行的检查:把四路的训练代码写成循环,而不是四份复制粘贴。

python
# ✅ 用循环,从结构上保证公平
for name, model in MODELS:
    model.fit(Xtr, ytr)                  # ← 只有 Xtr/ytr,不可能写错
    r2 = r2_score(yte, model.predict(Xte))

复制粘贴四份代码,就是在邀请自己写出版本 C 那种错误。

练习 3:给三个场景选模型

用第 23.4 节的对照表(速度 / 可解释性 / 不确定性)为下面三个场景各选一个模型, 并说明取舍的理由:

  1. 离线周报:每周一算一次全批设备的健康度分布,给质量部门看;
  2. 产线实时看板:每来一组传感器数据就要在 100 ms 内刷新 SOH;
  3. 退役决策支持:工程师要决定"这批设备该不该换",需要一个可信的风险区间。
参考答案
场景推荐理由
1 离线周报梯度提升 / 随机森林精度优先;一周跑一次,几分钟无所谓;没有延迟要求
2 实时看板线性回归 / 小树模型单行预测耗时优先(线性 0.000ms vs 森林 0.022ms);精度损失很小(0.9505 vs 0.9650)
3 退役决策高斯过程 GPR需要 ±σ;精度虽低,但能触发"人工复检",比一个自信的错数字安全

★ 逐个展开

场景 1:离线周报 → 精度优先

约束:无延迟要求,一周一次
优化目标:精度
    ↓
选 R² 最高的(本数据上是随机森林 0.96503 或 HistGB 0.96429)
    ↓
可以用全量数据训练(不需要考虑推理延迟)

★ 但要注意一个细节:

"每周算一次全批分布" → 用【全量数据】训练是可以的(这不是评估,是生产)
但如果你要报"模型的预测精度" → 那次评估必须用留出的测试集

这两个用途不要混(第 11 章讲过:调参看 CV,报成绩看留出集)。

场景 2:实时看板 → 速度优先

约束:100 ms 内必须出结果
    ↓
先算一下预算:
  线性回归  0.000 ms  ← 完全没问题
  随机森林  0.022 ms  ← 也没问题!
  GPR       O(n³)     ← 取决于训练集大小,可能超预算

⚠️ 注意:这个数据上"速度"其实不是瓶颈 —— 所有模型的单行预测都在 1 ms 以内。

所以真实的选择逻辑会变成:

如果所有模型都满足延迟要求 → 那"速度"这个维度就不构成约束
    ↓
于是回归到"精度优先" → 选随机森林
    ↓
【而不是机械地"实时场景就要选线性模型"】

★ 这是一个很重要的思维修正:

约束只有在"真的构成约束"时才有意义。先量化,再取舍 —— 不要凭场景标签做决定。

场景 3:退役决策 → 不确定性优先

约束:决策错误代价高(提前退役浪费,推迟退役有风险)
    ↓
"0.815" 这个数字本身不够 —— 你需要知道"这个 0.815 有多可信"
    ↓
GPR 给出:"0.808 ± 0.109" → 区间跨过 0.80 的退役线 → 【触发人工复检】
    ↓
随机森林给出:"0.815"     → 你只能凭感觉决定

★ 这就是"精度不是唯一指标"的具体体现

随机森林 R² 0.96503  vs  GPR R² 0.82518
        ↑                      ↑
   精度赢了 0.14           但它不会说"我不确定"
        ↓
在【需要承担决策后果】的场景里,后者常常更值得推荐

★ 三段建议的写法(练习要求"给业务方看")

① 离线周报:
   "推荐用梯度提升模型。它的平均误差约 0.8 个百分点,
    一周跑一次,多花几分钟不影响交付。"
    ← 只说业务关心的(误差多大、多快),不说 R² / HistGB

② 实时看板:
   "推荐用线性回归。预测延迟在微秒级,远低于 100 ms 要求;
    平均误差 0.97 个百分点,比最复杂的模型只差 0.18 个百分点。
    这个精度差在实时监控场景里可以接受,换来的是极低的延迟和运维成本。"
    ← 量化了"付出了什么、换来了什么"

③ 退役决策:
   "推荐用高斯过程模型。它除了给出预测值,还会给出不确定度:
    例如'0.81 ± 0.11'表示这次预测不够确定,建议人工复检。
    单纯追求精度的模型只会给出一个数字,无法支撑'是否要换'这类决策。"
    ← 说清了"为什么精度不是唯一标准"

★ 三条建议的共同点

① 都说了【数字】(误差、延迟、区间)
② 都说了【取舍】(付出了什么、换来了什么)
③ 都【没有出现】R²、HistGB、O(n³) 这类术语

这是"面向业务方"写作的标准。

第 26 章会让你再写一次,那时你会用上四维(精度×速度×可解释性×不确定性)。

面向工程实践:横评是"决策",不是"排名" ​

① 横评的产出不是一张排名表,而是一个决策依据

❌ 错误的产出:"随机森林最好(R² 0.96503),就它了。"
   → 没有回答"为什么不是别的",也没有回答"差多少算差"

✅ 正确的产出:
   "三个主流模型的 R² 极差 0.0145,说明换模型的空间已经不大。
    随机森林在精度(0.96503)、训练速度(6.27s)、维护成本上综合最优;
    GPR 精度更低但提供 ±σ,留给需要风险区间的场景。"
   → 说明了差距量级、选择理由、以及"不选别的"的原因

② 一个实用的"横评报告"模板

## 横评设置
- 数据:<什么数据,多少行,多少实体>
- 划分:<按什么划分,训练/测试各多少>
- 指标:<哪几个,为什么选它们>
- 共同条件:同一份 tr/te 索引、同一套特征、预处理都在 Pipeline 里

## 结果
<table: 模型 | MAE | RMSE | R² | 训练耗时 | 单行预测>

## 结论
1. 极差:<最好的 - 最差的> → 说明<卡点在模型还是别处>
2. 选定:<哪个>,理由:<精度/速度/不确定性哪个是主导约束>
3. 未选:<其他>,理由:<为什么不选>

## 局限
- <这份数据是合成的 / 样本量 / 有没有覆盖某个子群>
- <结论的适用范围>

★ 第 ④ 节"局限"最容易被省略,但最重要:

没有局限说明的结论 = 无法判断它什么时候会失效

③ 最后一次强调这一章的核心

横评的结果通常是:换模型的收益有限
   ↓
这不是"横评没用",恰恰相反 ——
   ↓
它帮你【排除】了一个方向,让你把精力投到真正有收益的地方(特征)

★ 在工程上,"排除"和"发现"一样有价值:

"我们试了四路模型,收益上限 0.015" 
   → 这个结论能阻止团队在未来三个月里继续换模型
   → 它的价值等于省下了三个月的工期

"做了实验,得到否定结果"不是失败,是有效信息。

这本书教你做实验,也教你从否定结果里读出方向。


下一章:第 24 章 · 高斯过程回归:会给"置信区间"的模型