Skip to content

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

这一章要解决什么:前面所有模型都只给一个数。GPR 给的是一个区间—— 这在"要不要让设备退役"这类决策里,比精度更值钱。

最容易踩的坑:为了让置信带"好看"而约束噪声上界。 本章会实测这个操作的后果:覆盖率从 92.6% 掉到 28.9%, 而 R² 几乎不变 —— 也就是说,它不会让模型更准,只会让模型更自信。

本章你需要完成的代码:demos/ch24-gaussian-process.py

24.1 先说清 GPR 的代价:O(n³) ​

真实输出:

text
=== ① 为什么必须抽子集:O(n³) 的代价 ===
  全表 21,239 行。GPR 的计算量随 n³ 增长:
    n =    600  →  相对 600 行的耗时 ≈      1.0 倍
    n =  2,000  →  相对 600 行的耗时 ≈     37.0 倍
    n =  5,000  →  相对 600 行的耗时 ≈    578.7 倍
    n = 20,000  →  相对 600 行的耗时 ≈  37037.0 倍

★ n³ 意味着什么

样本量翻 2 倍 → 耗时翻 8 倍
样本量翻 10 倍 → 耗时翻 1000 倍   ← 这就是上表的 3.7 万倍从哪来

这就是"必须抽子集"的原因 —— 不是"GPR 不好用",是它的数学性质决定了大样本不可行。

★ 一个对比记忆:

模型复杂度2 万行要多久
线性回归快秒级
随机森林中秒级
梯度提升中十秒级
GPRO(n³)不可行,必须抽样

所以 GPR 的定位很清楚:

样本少、且你需要不确定性的时候用它。 样本多的时候,它的代价无法接受。

点开:抽样有一个必须避开的坑

真实输出:

text
=== ② 抽样方式:随机抽 vs 偷懒取前 800 行 ===
  随机抽 800 行  :cycle 范围 50 ~ 1424
  取前 800 行    :cycle 范围 50 ~ 1292
  → 表格按 cell_id + cycle 排序,前 800 行只覆盖最早 1~2 支设备!
    用它们训练的 GPR 去预测其他设备 = 【在训练分布之外硬外推】。

⚠️ 注意:看起来两种抽样"差不多"(50~1424 vs 50~1292),但真相差得远。

为什么:

表格的排序是 cell_id 优先:
  C001 的 356 行
  C002 的 356 行
  C003 的 ...
      ↓
前 800 行 ≈ C001 全部 + C002 全部 + C003 的一小部分

所以"取前 800 行"实际是"只用 2~3 支设备训练" —— 而 cycle 范围看起来还有 50~1292,具有极强的欺骗性:

你以为:抽了 800 行,覆盖 cycle 50~1292,不错
实际上:只有 3 支设备,且是同一批次、同一工艺条件
      ↓
GPR 学到的是"这 3 支设备的曲线"
      ↓
拿它去预测另外 12 支设备 → 它连"设备之间有差异"这件事都不知道

★ 正确写法(本 demo 用的):

python
rng = np.random.default_rng(42)
idx = rng.choice(len(df), size=800, replace=False)     # ✅ 全局随机
idx = idx[~np.isin(idx, te)]                           # 去掉测试集里的(防泄漏)
Xg, yg = df[["cycle"]].iloc[idx], y.iloc[idx]

★ 一个通用的排查习惯

python
# 抽样后,先看"覆盖了几个实体",而不只是看"数值范围"
print(f"抽到 {len(Xg)} 行,覆盖 {df['cell_id'].iloc[idx].nunique()} 支设备")
print(f"cycle 范围 {Xg['cycle'].min()} ~ {Xg['cycle'].max()}")

只看数值范围会骗你;看"实体数"才看得见真相。

★ 这个坑的通用形式

任何"按行切片"的操作,在有分组结构的表上都可能悄悄丢掉实体:
  · df.head(n)          取前 n 行
  · df.iloc[:n]         同上
  · df.sample(n)        ✅ 这个安全(随机)
  · df[df["x"] > k]     可能恰好只留下某一支设备

所以在有分组结构的表上,抽样后要检查"实体覆盖率"。

24.2 正确做法:让 GPR 自己学出不确定度 ​

真实输出:

text
=== ③ 正确做法:放开噪声上界,让 GPR 自己学出不确定度 ===
  核 = RBF(length_scale 固定 30) + WhiteKernel(噪声水平【不设死上界】)
  训练 640 行,耗时 0.42 s
  学到的噪声水平 = 0.15677
  测试集 R²(单特征 cycle)= 0.83000
  平均 |误差| = 0.0173   平均 ±2σ = 0.0442
  ★ 覆盖率:真值落在 ±2σ 内的比例 = 92.6%(理论约 95%)

  对照:同样 800 行的线性回归 R² = 0.81905

★ 先看那个覆盖率:92.6%

这是本章最重要的一个数字。它的含义:

GPR 说"这个预测是 0.9132 ± 0.0442"
   ↓
如果这句话是对的,那么真值应该约 95% 的时间落在这个区间里
   ↓
实测:92.6% —— 基本符合
   ↓
【所以这个区间是可信的】

★ 再看精度:R² = 0.83000,与线性回归的 0.81905 相差无几。

这告诉我们:

GPR 的卖点从来不是精度。 它和线性回归在同一个特征上精度接近 —— 区别是线性回归只能给你一个数,GPR 能给你一个区间。

★ WhiteKernel 是"不确定性"的来源

python
kernel = RBF(length_scale=30.0) + WhiteKernel(noise_level=...)
#        ↑ 解释"趋势"              ↑ 解释"逐点抖动"

两项分工:

项负责什么对应到现实
RBF平滑的趋势"健康度随循环下降"这条规律
WhiteKernel逐点独立噪声测量误差、单支设备的固有偏差

★ 而 noise_level = 0.157 这个数很大 —— 它说明"数据里的噪声占了相当大的比例"。

这就是第 10 章那个误差模型的数字体现:

每支设备固定的零点偏置(电压/内阻/温度各一个)+ 缓慢漂移
   ↓
对 GPR 来说,这些都无法从 cycle 这一列学到
   ↓
它们全部被归入 noise_level
   ↓
所以 noise_level 很大(0.157)

24.3 带宽怎么随 cycle 变化 ​

真实输出:

text
=== ④ ★ 带宽怎么随 cycle 变化(数据只到 1440 圈)===
     cycle        预测       ±2σ   说明
       200    0.9713    0.0440   数据区(样本密)
       600    0.9132    0.0439   数据区(样本密)
      1000    0.8578    0.0449   数据区(样本密)
      1200    0.8377    0.0457   数据区(样本密)
      1440    0.8409    0.0756   数据边界
      1500    0.9059    0.1160   ★ 外推区,没有样本
      1600    0.9088    0.1161   ★ 外推区,没有样本

  数据区内 ±2σ ≈ 0.0457
  数据边界 ±2σ ≈ 0.0756(开始张开)
  外推区   ±2σ ≈ 0.1161(是数据区内的 2.5 倍)

★ 注意外推区的"预测值"发生了什么

cycle = 1200  →  预测 0.8377
cycle = 1440  →  预测 0.8409
cycle = 1500  →  预测 0.9059   ← 反而【回升】了!
cycle = 1600  →  预测 0.9088   ← 继续回升

这是 GPR 的一个重要特性:

在没有数据的地方,GP 的均值会退回"先验"(通常是全局平均),而不是顺着趋势往外画。

对比第 07 章的线性回归:

线性回归高斯过程
cycle=1600 的预测0.5583(顺着直线一路往下)0.9088(退回全局平均附近)
有不确定性表达吗❌ 只有一个数✅ ±0.1161

★ 两个模型面对"没见过的区域",给出了完全不同的回答:

线性回归:0.5583          →  看起来是个明确答案,但毫无依据
高斯过程:0.9088 ± 0.1161  →  承认"我不知道",并告诉你不知道到什么程度

★ 这就是"诚实的模型"的样子

真实输出里那句总结很到位:

text
  ★ 这就是 GPR 的诚实之处:没数据的地方,它自己会把嘴张大。
  ★ 而线性回归在外推区会给你一个「看起来很确定」的数(第 07 章讲过)。
点开:为什么 GPR 在无数据处会退回平均?

GP 的世界观:它假设"函数"是从一个高斯分布里抽出来的。 在没有观测数据的地方,这个分布就是先验分布 —— 通常以均值(0,标准化后)为中心。

有数据的地方:
  数据"拉住"了后验分布 → 预测贴近数据 → σ 小

没有数据的地方:
  没有东西拉住它 → 保持先验 → 预测回到均值 → σ 恢复到先验方差

★ 一个直觉类比

想象你在一张纸上画了一条曲线,但只在 x ∈ [50, 1440] 有描点。
   ↓
问:"x = 2000 处的 y 是多少?"
   ↓
最诚实的回答是:"我不知道" —— 而不是"顺着我画的趋势延伸"
   ↓
GPR 的回答方式:给出"全局平均 ± 很大的不确定度"

★ 但要注意:这只是一个"约定",不是"真理"

GPR 退回均值,是因为它假设"函数是平稳的"。 如果你的物理知识告诉你"这个量不可能回升",那 GPR 的退均值行为反而违背了物理。

所以 GPR 的外推结果也要用领域知识去审视:

GPR 说:cycle=1600 时健康度 0.9088 ± 0.1161
   ↓
物理上:健康度不可能回升(退化是不可逆的)
   ↓
结论:不是"GPR 错了",而是"GPR 的先验假设与物理不符"
      → 如果一定要外推,应该换一个带单调性约束的模型

★ 这个例子说明一件重要的事:

任何模型都有"假设"。GPR 的假设是"函数平稳"。 当假设与现实不符时,模型输出的置信区间也是错的 —— 哪怕它看起来比线性回归"诚实"。

没有任何模型能自动保证正确。这就是为什么你会需要这本书的前 23 章。

24.4 ★★ 一个真实的坑:置信带会骗人 ​

这一节是本章最重要的部分。 它来自我实际运行这个 demo 时发现的问题。

场景:为了让置信带"好看且有结构",一个很自然的想法是—— 约束噪声不要学得太大:

python
WhiteKernel(noise_level=1e-3, noise_level_bounds=(1e-8, 4e-3))
#                                              ↑
#                             "防止噪声被学得太大"

看起来是个好心的限制。实际效果呢?

真实输出:

text
                    噪声上界       学到的噪声      平均±2σ      平均|误差|      覆盖率       R²
                4e-3(设死)     0.00400     0.0072      0.0175    28.9%  0.82604
                 1e1(放开)     0.15677     0.0442      0.0173    92.6%  0.83000

★★ 读这张表

平均 |误差| = 0.0175

设死上界(4e-3):
  平均 ±2σ = 0.0072
       ↑
   区间半宽只有 0.0072,而平均误差是 0.0175
       ↓
   误差比区间还大 2.4 倍!
       ↓
   覆盖率 28.9% ← 号称 95% 的区间,只有不到三成真的覆盖了真值

放开上界(1e1):
  平均 ±2σ = 0.0442
       ↑
   区间半宽 0.0442 > 平均误差 0.0175
       ↓
   覆盖率 92.6% ← 基本符合理论值

★ 而 R² 几乎没变:0.82604 → 0.83000

这意味着一件很重要的事:

把噪声上界设死,不会让模型更准,只会让模型更自信。误差率丝毫未降,但区间被压窄了 6 倍。

我把这个操作叫「欺骗性优化」 —— 它只改变了"模型表达的确定性",没有改变"模型的实际能力"。

点开:为什么"带子变好看"反而是错的信号?

把两种设置下的带子并排看:

text
     cycle         【设死上界】±2σ         【放开上界】±2σ   位置
       200            0.0071            0.0440   数据区
       600            0.0071            0.0439   数据区
      1000            0.0073            0.0449   数据区
      1200            0.0075            0.0457   数据区
      1440            0.0473            0.0756   边界
      1500            0.1080            0.1160   外推
      1600            0.1081            0.1161   外推

★ 反直觉的地方

设死上界的带子:【形状好看】
   → 数据区内又窄又平(0.0071)
   → 出了边界才张开(0.1080),涨了 15 倍
   → 完全符合"离数据越远越不确定"的直觉

放开上界的带子:【几乎是等宽的】
   → 数据区内就有 0.0440
   → 边界也只涨到 0.1160,只涨了 2.6 倍
   → 看起来"没有结构"

为什么放开上界后带子变平了:

放开上界 → GPR 学到「噪声本来就很大」(noise_level = 0.157)
   ↓
这个噪声项【在任何 cycle 上都一样大】
   ↓
它盖过了 RBF 那部分"离数据越远越不确定"的贡献
   ↓
所以带子被"撑平"了

★ 那哪个才是对的?覆盖率说了算:

设死上界  28.9%   → 错(严重低估不确定性)
放开上界  92.6%   → 对(接近理论 95%)

★ 这里有一个所有数据工作者都会遇到的诱惑

一张"形状好看"的图,比一张"平淡"的图更有说服力
   ↓
于是你会不自觉地调整参数,直到图变得好看
   ↓
而"图好看"和"模型正确"是两件不同的事

这个坑的危险在于:它是一步步滑进去的。

第 1 步:跑出带子,发现很宽 → "太宽了,不够锐利"
第 2 步:加个 noise_level 上界 → 带子变窄了 → "好多了!"
第 3 步:看到带子随 cycle 张开 → "这才是对的样子"
第 4 步:把这张图放进报告
   ↓
整个过程中,你从来没有检查过覆盖率
   ↓
而覆盖率是唯一能告诉你"这个区间对不对"的指标

★ 正确的流程:

python
# 训练完之后,第一件事不是画图,是验证覆盖率
mu, sd = gpr.predict(Xte, return_std=True)
resid = np.abs(y_te.to_numpy() - mu)
coverage = np.mean(resid <= 2 * sd)
print(f"±2σ 覆盖率 = {coverage:.1%}(理论约 95%)")

# 只有覆盖率合理,才去看图
if coverage < 0.85:
    print("⚠️ 覆盖率过低 → 模型低估了不确定性 → 检查噪声设置")
elif coverage > 0.99:
    print("⚠️ 覆盖率过高 → 模型高估了不确定性(区间过宽,失去作用)")

★ 覆盖率过高也是问题

如果覆盖率是 100%,说明区间宽到"包住一切"
   ↓
这种区间没有任何决策价值("明天温度在 -100~100℃ 之间")
   ↓
好的不确定度估计,应该是"不过窄、也不过宽"

★ 一句话总结

置信区间的正确性,要用覆盖率检验,不能用"看起来对不对"判断。

这个道理适用于所有给出区间的模型(分位数回归、贝叶斯模型、集成方差等)。

24.5 那 GPR 到底什么时候值得用? ​

把这一章的结论放在一起:

维度GPR 的表现
精度中等(R² 0.83,与线性回归相当)
速度差(O(n³),2 万行不可行)
可解释性低(核函数的含义不直观)
不确定性★ 唯一能给的

★ 所以它的适用场景很明确:

适合:
  ✅ 样本量小(几百到几千行)
  ✅ 决策需要"风险区间"而不只是一个数
  ✅ 需要知道"哪些预测不可信"(主动学习、实验设计)

不适合:
  ❌ 样本量大(O(n³) 不可行)
  ❌ 只要精度(有更便宜的选择)
  ❌ 需要解释"为什么"(核函数不直观)

★ 一句准确的表述(第 26 章选型时会用到)

GPR 不是"更准的模型",是"能表达不确定性的模型"。 它的价值在精度之外 —— 当决策的代价不对称时, "知道自己不确定"比"平均误差小 0.001"重要得多。

24.6 常见坑 ​

text
① 直接喂全量数据 → O(n³) 跑到超时或爆内存,必须抽子集
② 抽样时取"前 N 行" → 因为表按实体排序,实际只覆盖 1~3 支设备
③ 把噪声上界设死 → 置信带被压窄,覆盖率崩到 29%(本章实测)
④ 把置信带当"95% 保证" → 它只对【训练分布附近】成立,外推区不可信
⑤ 拿单特征 GPR 的 R²(0.83) 去和 7 特征森林(0.965) 比 → 条件不同,不可比
⑥ 只看图不看覆盖率 → 会选中那个"更好看但更错"的版本

★ ③ 和 ⑥ 是这一章的核心。 它们是一个模式的两个面:

③ 是"怎么错的"
⑥ 是"为什么没被发现"(因为只看图)

24.7 完整示例与输出 ​

bash
cd code
python demos/ch24-gaussian-process.py

真实输出(节选):

text
=== ③ 正确做法:放开噪声上界,让 GPR 自己学出不确定度 ===
  学到的噪声水平 = 0.15677
  测试集 R²(单特征 cycle)= 0.83000
  平均 |误差| = 0.0173   平均 ±2σ = 0.0442
  ★ 覆盖率:真值落在 ±2σ 内的比例 = 92.6%(理论约 95%)

=== ④ ★ 带宽怎么随 cycle 变化 ===
     cycle        预测       ±2σ   说明
       200    0.9713    0.0440   数据区(样本密)
      1200    0.8377    0.0457   数据区(样本密)
      1440    0.8409    0.0756   数据边界
      1600    0.9088    0.1161   ★ 外推区,没有样本

=== ⑤ ⚠️ ★★ 一个真实的坑:把噪声上界设死,置信带会骗你 ===
                    噪声上界       学到的噪声      平均±2σ      平均|误差|      覆盖率       R²
                4e-3(设死)     0.00400     0.0072      0.0175    28.9%  0.82604
                 1e1(放开)     0.15677     0.0442      0.0173    92.6%  0.83000

本章练习 ​

练习 1:用覆盖率检验置信区间

训练一个 GPR(RBF + WhiteKernel,噪声上界设为 1e1),然后:

  1. 在测试集上算出 ±1σ、±2σ、±3σ 的覆盖率;
  2. 和理论值(正态分布下 68.3% / 95.4% / 99.7%)对比;
  3. 解释偏差可能的原因。
参考答案
python
import numpy as np
import pandas as pd
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF, WhiteKernel
from sklearn.model_selection import GroupShuffleSplit

df = pd.read_csv("data/battery/soh_features.csv")
y = df["soh"]
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
tr, te = next(gss.split(df[["cycle"]], y, groups=df["cell_id"]))
tr, te = np.sort(tr), np.sort(te)

rng = np.random.default_rng(42)
idx = rng.choice(tr, size=800, replace=False)
Xg, yg = df[["cycle"]].iloc[idx], y.iloc[idx]

kernel = (RBF(length_scale=30.0, length_scale_bounds="fixed")
          + WhiteKernel(noise_level=1e-3, noise_level_bounds=(1e-8, 1e1)))
gpr = GaussianProcessRegressor(kernel=kernel, normalize_y=True, random_state=42).fit(Xg, yg)

mu, sd = gpr.predict(df[["cycle"]].iloc[te], return_std=True)
resid = np.abs(y.iloc[te].to_numpy() - mu)

print(f"{'区间':>8}{'理论覆盖率':>12}{'实测覆盖率':>12}")
for k, theory in ((1, 0.6827), (2, 0.9545), (3, 0.9973)):
    got = np.mean(resid <= k * sd)
    print(f"{'±' + str(k) + 'σ':>8}{theory:>12.1%}{got:>12.1%}")

期望输出:

text
      区间      理论覆盖率      实测覆盖率
      ±1σ       68.3%       66.2%
      ±2σ       95.4%       92.6%
      ±3σ       99.7%       99.1%

★ ② 对比结果

三个区间的实测覆盖率都略低于理论值,但偏差很小:

±1σ:66.2% vs 68.3%   (低 2.1 个百分点)
±2σ:92.6% vs 95.4%   (低 2.8 个百分点)
±3σ:99.1% vs 99.7%   (低 0.6 个百分点)

这个程度的偏差是"可用"的 —— 区间的量级是对的。

★ ③ 偏差可能的原因

原因说明
模型假设不完全成立GP 假设"函数从高斯过程抽样",真实数据未必满足
特征不足只用 cycle 一个特征 → 忽略了其他导致差异的因素 → 残差有结构
测试集是"新设备"训练时没见过这些设备 → 存在分布偏移
有限样本800 行训练,超参数(噪声水平)估计本身有误差

★ 一个重要的判据

覆盖率偏差在 ±5 个百分点以内 → 区间可用
偏差很大(比如 ±2σ 只有 30%)→ 区间严重错误,不能用

本练习的偏差(-2.8 个百分点)属于"可用"。

★ 反过来,如果覆盖率是 100% 呢?

那说明区间宽到"一定能包住" ——
  例:"明天温度在 -100~100℃ 之间"
  虽然"正确",但【没有决策价值】

★ 好的不确定度估计 = 不过窄、也不过宽:

python
# 一个更完整的检验
coverage = np.mean(resid <= 2 * sd)
width = (2 * sd).mean()
print(f"±2σ 覆盖率 {coverage:.1%}   平均区间宽度 {width:.4f}")

# 判读:
# 覆盖率太高 + 宽度很大  → 区间过于保守(没用)
# 覆盖率太低 + 宽度很小  → 区间过于自信(危险,本章的坑)
# 覆盖率接近 95%         → 合适

★ 与本章 24.4 节的坑呼应

设死噪声上界那一版:
  ±2σ 平均宽度 0.0072(很窄,看起来"很确定")
  覆盖率 28.9%(严重不可信)
   → 属于"过于自信"这一类,最危险
练习 2:亲手复现"置信带骗人"这个 bug

用同样的 800 行训练两个 GPR:一个把 noise_level_bounds 上界设为 4e-3,一个设为 1e1。 对比它们的:学到的噪声水平、平均 ±2σ、平均绝对误差、±2σ 覆盖率、R²。

要求:用一张表把五个数字列出来,并解释为什么 R² 几乎不变而覆盖率差这么多。

参考答案
python
import numpy as np
import pandas as pd
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF, WhiteKernel
from sklearn.metrics import r2_score
from sklearn.model_selection import GroupShuffleSplit

df = pd.read_csv("data/battery/soh_features.csv")
y = df["soh"]
tr, te = next(GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
              .split(df[["cycle"]], y, groups=df["cell_id"]))
tr, te = np.sort(tr), np.sort(te)
rng = np.random.default_rng(42)
idx = rng.choice(tr, size=800, replace=False)
Xg, yg = df[["cycle"]].iloc[idx], y.iloc[idx]
Xte, yte = df[["cycle"]].iloc[te], y.iloc[te]

print(f"{'噪声上界':>10}{'学到的噪声':>12}{'平均±2σ':>11}{'平均|误差|':>12}"
      f"{'覆盖率':>9}{'R²':>9}")
for ub in (4e-3, 1e1):
    k = (RBF(length_scale=30.0, length_scale_bounds="fixed")
         + WhiteKernel(noise_level=1e-3, noise_level_bounds=(1e-8, ub)))
    g = GaussianProcessRegressor(kernel=k, normalize_y=True, random_state=42).fit(Xg, yg)
    mu, sd = g.predict(Xte, return_std=True)
    e = np.abs(yte.to_numpy() - mu)
    print(f"{ub:>10.0e}{g.kernel_.k2.noise_level:>12.5f}{(2 * sd).mean():>11.4f}"
          f"{e.mean():>12.4f}{(e <= 2 * sd).mean():>9.1%}{r2_score(yte, mu):>9.5f}")

期望输出:

text
  噪声上界       学到的噪声      平均±2σ      平均|误差|      覆盖率       R²
     4e-03     0.00400     0.0072      0.0175    28.9%  0.82604
     1e+01     0.15677     0.0442      0.0173    92.6%  0.83000

★ 为什么 R² 几乎不变,而覆盖率差这么多?

这是本练习的核心问题。答案在于"R² 和区间宽度测量的是两件不同的事"。

R² 只关心【均值预测得准不准】
   mu = gpr.predict(Xte)          ← 只用均值,不用 sd
   R² = r2_score(yte, mu)
   ↓
改噪声上界【几乎不改变均值】:
   因为白噪声项是"零均值的随机抖动",它不贡献趋势
   ↓
所以 R² 几乎不变(0.82604 → 0.83000)

覆盖率关心【区间能不能包住真值】
   coverage = mean(|y - mu| <= 2 * sd)     ← 依赖 sd
   ↓
改噪声上界直接改变 sd:
   noise_level 被卡在 0.004 上限 → sd 被限制在 sqrt(0.004) ≈ 0.063(标准化后)
   ↓
所以覆盖率天差地别(28.9% → 92.6%)

★ 一个精确的类比

这就像天气预报:

预报 A:"明天 25℃",并说"误差不超过 ±0.5℃"
预报 B:"明天 25℃",并说"误差不超过 ±3℃"

两者的"预报值"完全一样(都是 25℃)
   ↓
如果这是"点预测准确度"的比较 → 打平
   ↓
但 A 的那句"±0.5℃"是在撒谎(实际误差经常超过)
   ↓
【它们的区别全在"那句关于不确定性的声明"上】

★ 三个可以带走的认知

① 点预测指标(R²、MAE、RMSE)对"不确定性质量"完全不敏感

所以:报 R² 不能说明你的区间是对的。
     检验区间必须用覆盖率。

② 模型可以"既准又错"

R² = 0.826 是"算准了"
覆盖率 28.9% 是"说错了自己有多准"
   ↓
两者同时成立,互不矛盾

③ 而"说错自己有多准"在决策场景里是致命的

工程师看到 "0.9132 ± 0.0072"
   ↓
他认为:"这个预测非常确定,直接按它决定是否退役"
   ↓
但真值可能偏离 0.0175(是他以为的 2.4 倍)
   ↓
→ 决策建立在错误的信心上

★ 一条可操作的纪律

python
# 任何给出区间的模型,训练后第一件事:
mu, sd = model.predict(X_test, return_std=True)
resid = np.abs(y_test.to_numpy() - mu)
print(f"±2σ 覆盖率 = {np.mean(resid <= 2 * sd):.1%}(应接近 95%)")
print(f"平均区间宽度 = {(2 * sd).mean():.4f}")

# 覆盖率 < 85% 或 > 99% 都要查原因
练习 3:给业务方解释"为什么要用 GPR"

假设你要说服一个产线主管用 GPR 而不是随机森林。写出你的论证,要求:

  1. 用数字说明两者的差异(R² 和"能不能给区间");
  2. 举一个"随机森林会误导决策、GPR 不会"的具体场景;
  3. 不要出现"高斯过程""核函数"这类术语。
参考答案

参考论证(口语化、面向业务):

我们有两条路可选,我说一下差别。

第一条路:模型给你一个数字,比如"这支设备健康度 0.815"。 它的平均误差在 0.8 个百分点左右,比另一条路准确一些。

第二条路:模型除了给数字,还告诉你"这个数字我有多大把握"。 比如"健康度 0.808,上下浮动 0.109"。它的平均误差大一点, 约 1.7 个百分点。

关键在于:

如果按第一条路的 0.815,我们很难判断该不该换 —— 因为它离 0.80 的 退役线只差 0.015,而模型的误差本身就有 0.008 到 0.017。 这个数字的精度不足以支撑"换不换"的决定,但我们不知道它不够。

第二条路会直接告诉你:"这个预测的浮动范围是 ±0.109" —— 也就是说真值可能在 0.70 到 0.92 之间,完全跨过了退役线。 这时候正确的动作不是"按 0.808 决定",而是"安排人工复检"。

所以第二条路的价值不在于"算得准",而在于它能主动告诉你"这次我没把握"。 对"该不该换设备"这类决定,知道哪里没把握比平均准 0.9 个百分点更重要 —— 因为前者的错误会让工程师对一个不可靠的数字产生信任。

★ 这段论证用到的技巧

技巧例子
说数字,不说术语"平均误差 0.8 个百分点"而不是"MAE 0.0079"
建立对比两条路并排,各说优劣
给具体场景"0.815 离 0.80 只差 0.015,而误差是 0.017"
落到行动"正确的动作是人工复检,而不是按数字决定"
收回术语全程没有出现"高斯过程""σ""核函数"

★ 一个可以量化"为什么需要区间"的方法

python
import numpy as np

# 假设退役线 0.80,模型预测 0.815
pred, sigma = 0.815, 0.055      # GPR 给出的 ±2σ 的一半
threshold = 0.80

# 点预测:只有一个数
print(f"点预测 {pred} → 高于退役线 {pred - threshold:.3f} → '看着安全'")

# 区间:真值可能落在哪
lo, hi = pred - 2 * sigma, pred + 2 * sigma
print(f"±2σ 区间 [{lo:.3f}, {hi:.3f}]")
print(f"区间跨过退役线吗:{lo < threshold < hi}")
# 区间 [0.705, 0.925] 跨过 0.80 → 这个设备【可能已经该退役了】

★ 输出的含义

text
点预测 0.815 → 高于退役线 0.015 → '看着安全'
±2σ 区间 [0.705, 0.925]
区间跨过退役线吗:True

同一个预测值,两种读法给出相反的结论:

只看点预测:0.815 > 0.80 → 安全,继续用
看区间    :[0.705, 0.925] 跨过 0.80 → 【不确定,需要复检】

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

一个更进一步的量化:把"区间跨线"做成一个筛选规则

python
def triage(pred, sigma, eol=0.80, k=2.0):
    """按区间是否跨过退役线来分诊。"""
    lo, hi = pred - k * sigma, pred + k * sigma
    if hi < eol:
        return "确定该退役"
    if lo > eol:
        return "确定可继续"
    return "不确定 → 人工复检"

for pred, sigma in ((0.72, 0.02), (0.88, 0.02), (0.815, 0.055)):
    print(f"预测 {pred} ±{sigma}: {triage(pred, sigma)}")

输出:

text
预测 0.72 ±0.02: 确定该退役
预测 0.88 ±0.02: 确定可继续
预测 0.815 ±0.055: 不确定 → 人工复检

★ 这个 triage 函数就是"用不确定性驱动行动"的最小实现。

它比"按预测值排序、取最低的 10% 退役"要安全得多 —— 因为后者会把"高不确定度"和"确定低"混在一起处理。

这也是第 26 章选型时会用到的核心论点。

面向工程实践:不确定性是"可运营"的资产 ​

在真实项目里,"知道模型哪里不确定"能直接变成业务流程。

① 三种把不确定性变成行动的模式

模式触发条件动作
人工复检区间跨过决策阈值安排人去看一眼
主动采样σ 最大的样本优先对它加密测量(实验设计)
降级使用σ 超过某个值改用更保守的策略(如按最坏情况)

② 一个完整的例子

python
def decide(pred, sigma, eol=0.80):
    """把预测与不确定度一起转成决策。"""
    lo, hi = pred - 2 * sigma, pred + 2 * sigma

    if hi < eol:
        return "退役", f"区间 [{lo:.3f},{hi:.3f}] 整体低于 {eol}"
    if lo > eol:
        return "继续", f"区间 [{lo:.3f},{hi:.3f}] 整体高于 {eol}"

    # 区间跨线:分两种情况
    if (hi - lo) > 0.10:                       # 区间太宽 → 信息不足
        return "加密采样", f"区间宽 {(hi - lo):.3f},先补测再判"
    return "人工复检", f"区间 [{lo:.3f},{hi:.3f}] 跨过 {eol}"

★ 这个函数的价值:

它把"模型输出"变成了"行动建议"
   ↓
而且包含了"信息不足"这个第三种状态(加密采样)
   ↓
这是只给点预测的模型做不到的

③ 为什么说它是"资产"

只给点预测的模型:
   产出 = 一个数字 → 需要人来判断怎么用

给出不确定度的模型:
   产出 = 数字 + 置信度 → 可以直接接业务规则
   ↓
可以自动化、可以排队、可以分优先级
   ↓
【这就是从"模型"到"系统"的一步】

④ 但记住本章的教训

这个资产有一个前提:区间必须是可信的。

区间可信(覆盖率 92.6%)→ 上面的所有自动化都成立
区间不可信(覆盖率 28.9%)→ 自动化会把错误放大:
   模型说"确定可以继续",于是系统自动跳过复检
       ↓
   而实际上它并不确定
       ↓
   问题设备被漏掉了 —— 【自动化把错误变成了规模化的错误】

★ 所以本章最后一句话是:

在把不确定性接进业务流程之前,先验证它的覆盖率。 一个错误的"我很确定",比"我不确定"危险得多。

这也是为什么第 26 章(选型)会把"不确定性"和"它的可信度"分开看 —— 一个能给出区间的模型,只有区间可信,这个能力才算数。


下一章:第 25 章 · 置信带什么时候张开:数据内 vs 外推