
简介GP-EnKF 是一份面向在线高斯过程回归学习与实现的 Python 代码资源对应 Fusion 2018 论文思路适合已具备高斯过程、卡尔曼滤波基础的研究生、算法工程师和科研人员用于理解并复现 EnKF 如何在线更新高斯过程先验、完成实时预测与不确定性估计。资源以 zip 压缩包形式提供大小约 22KB上游未给出有效文件总数与类型明细包体较精简重点应落在核心实现代码与论文相关示例上便于按模块阅读和二次改造。页面显示已有 307 人学习或下载。读者可从中获得 GP-EnKF 的预测—更新循环实现线索包括集合状态构建、观测融合、超参数与核函数设置等关键环节并参考环境科学、控制工程、信号处理等场景将在线数据融合、动态系统状态估计与概率预测方法迁移到自己的实验中。1. GP-EnKF 代码包拆解在线高斯过程回归为什么要把超参数塞进卡尔曼滤波传感器每 200 ms 吐一个点模型必须在这个窗口里把核超参数和预测分布一起更新完还不能把历史数据全留着——这是在线高斯过程回归最真实的工况。GP-EnKF 这份 Python 代码处理的就是这个场景它把 GP 的诱导点函数值和核超参数打包成一个状态向量用集合卡尔曼滤波做逐点的递推贝叶斯更新思路来自 FUSION 2018 那篇在线 GP 回归与学习的滤波工作。它更像一份可以直接投入使用的在线推断基础框架不需要每次进来新数据就重训也不用对边缘似然求梯度它并非 GP 库的又一层封装。适合三类人手里有实时回归任务、又必须要不确定度输出的工程师给 MPC、状态估计、软测量找在线代理模型的开发者以及读过高斯过程教材、但对「在线」始终没找到能跑起来的参考实现的同学。代码本身不长真正费时间的是几个参数和数值细节集合开多大、诱导点怎么摆、观测噪声估多少、协方差要不要膨胀。后面按这条线拆。2. 从批量 GP 到在线 GP把 O(N³) 的账算清再谈滤波2.1 批量 GP 卡在哪一步不是预测慢是每次都要重来标准 GP 回归的后验均值是 $\mu_* k_^\top (K\sigma_n^2 I)^{-1} y$预测方差是 $k_{**} - k_^\top (K\sigma_n^2 I)^{-1} k_*$。整套计算的核心是一次 Cholesky 分解复杂度 O(N³)。N2000 时在普通笔记本上大概几百毫秒量级具体数字跟 BLAS 后端和 CPU 关系很大我建议你自己测一遍——重点不在于绝对值而在于增长率数据翻倍时间大约涨 8 倍。真正致命的是超参数。边缘似然每评估一次就要一次 CholeskyL-BFGS 迭代几十步、每步还要算梯度梯度里同样含 $K^{-1}$一轮训练就是几十次 O(N³)。数据以 5 Hz 进来一小时 18000 条批量重训这条路径在实时系统里彻底走不通。更麻烦的是「重训」这件事本身的语义新数据进来重新优化超参数解会跳。上一秒预测方差还是 0.02下一秒变成 0.15下游的控制器会以为模型崩了。在线 GP 要解决的不只是计算量的问题还有解路径的连续性。2.2 三条主流在线化路线的取舍路线更新代价超参数能否在线学主要短板滑动窗口 定期重训O(N_w³)N_w 为窗口长度能但解会跳老数据全丢突变段信息断档稀疏 GP 增量 Cholesky每次 O(N·M)M 为诱导点数不能超参数冻结非平稳数据上性能持续劣化状态空间 / 滤波递推每次 O(n_ens·n_state²)能超参数进状态向量需要调集合大小与膨胀系数滑动窗口最省事代价是超参数在窗口切换时跳变增量 Cholesky把 $K\sigma^2 I$ 的分解做 rank-1 更新速度快但超参数被钉死一旦工况漂移模型不会自己调长度尺度只能靠人工定期重训——这就是「稀疏 GP 上线三个月后精度莫名下滑」这类问题的常见根源。滤波路线把 GP 的诱导点参数化写成状态空间模型用卡尔曼类方法递推。它的好处是把「学超参数」和「估后验」变成同一件事都是状态更新。代价是数值细节多尤其是协方差的秩和膨胀。2.3 为什么是 EnKF而不是 EKF 或 UKF观测算子 $h(\cdot)$ 里含核矩阵求逆而核矩阵又依赖状态里的长度尺度所以 $h$ 对状态是非线性的。用 EKF 要推 $\partial h/\partial \theta$也就是 $\partial (K_{zz}^{-1} k_*) / \partial \log \ell$链式展开下来一长串且每步都要算一次矩阵导数工程量与出错概率都高。UKF 不用雅可比但 sigma 点数量是 $2n1$状态维度 $n M2$ 时诱导点 40 个就是 84 个 sigma 点每步要算 84 次核矩阵分解成本不划算。EnKF 用随机采样的集合近似协方差只需前向计算 $h$天然导数-free集合大小固定在 80~100 就能工作跟状态维度基本解耦。这就是这份代码选 EnKF 的核心动机——工程上可维护而且超参数能跟着一起更新。2.4 先把批量基准跑通再谈在线任何在线实现都需要一个离线基准来对齐否则你没法判断在线版的偏差是算法问题还是实现 bug。下面这段是批量 GP 的最小实现后面所有在线结果都拿它对照。import numpy as np from scipy.linalg import cho_factor, cho_solve def rbf(A, B, ls, sf): 各向同性 RBF 核。A:(n,d) B:(m,d) - (n,m) d2 (A * A).sum(1)[:, None] (B * B).sum(1)[None, :] - 2.0 * A B.T d2 np.maximum(d2, 0.0) # 浮点误差可能给出 -1e-16 return sf ** 2 * np.exp(-0.5 * d2 / ls ** 2) def gp_batch(X, y, Xs, ls1.0, sf1.0, sn0.1): 批量 GP 后验预测返回均值与方差 n len(X) K rbf(X, X, ls, sf) (sn ** 2 1e-8) * np.eye(n) # 1e-8 是 jitter c cho_factor(K, lowerTrue) alpha cho_solve(c, y) # (Kσ²I)^{-1} y Ks rbf(Xs, X, ls, sf) # (m, n) mu Ks alpha v cho_solve(c, Ks.T) # (n, m) var sf ** 2 - np.einsum(ij,ji-i, Ks, v) # 逐点取对角 return mu, np.clip(var, 1e-12, None) # 数值上可能微负cho_factor返回的是下三角分解结果cho_solve复用同一份分解同时算均值和方差——这是批量 GP 唯一值得强调的性能细节别写成两次np.linalg.solve。jitter加在训练协方差对角上1e-8 起步如果你的长度尺度很小或诱导点很密这个值要往上调到 1e-6。拿这段代码在一段模拟数据上跑一遍、记下耗时你就有了对照组。我一般会把 N 从 200 拉到 3200 做一条耗时曲线确认它确实接近立方增长——这条曲线是后面判断在线版「省了多少」的唯一依据。3. 把 GP 后验塞进 EnKF状态向量、观测算子与集合更新3.1 状态向量怎么定诱导点值在前对数超参数在后GP 的诱导点参数化sparse pseudo-input 那一套把函数写成 $f(x) k(x,Z)K_{zz}^{-1}u$其中 $Z$ 是 $M$ 个诱导点位置$u$ 是这些位置上的函数值先验 $u \sim \mathcal{N}(0, K_{zz})$。这把一个无穷维的函数推断压缩成了 $M$ 维向量的推断而 $M$ 可以直接作为状态维度。状态向量定义为$$z [,u_1, \dots, u_M,\ \log \ell,\ \log \sigma_f,]^\top$$长度 $n_{state} M 2$。用对数参数化而不是原始值是因为长度尺度和幅值必须为正而 EnKF 的更新是线性叠加、不保号直接更新 $\ell$ 很容易跑出负值然后核矩阵集体失效。取对数之后状态可以自由取任意实数指数变换回去恒正。这是整个实现里最便宜也最关键的一个设计。$u$ 的先验直接从 $\mathcal{N}(0, K_{zz})$ 采样集合初始化时每个成员独立抽一次。超参数则围绕一个初值加小扰动扰动幅度取 0.05~0.1对数尺度太大集合一开始就散掉太小则初始协方差接近奇异、第一次更新几乎不动。提示诱导点位置 $Z$ 不放进状态向量。它们是「支撑点」不是待估量一旦允许它们动状态维度会随 $M \cdot d$ 暴涨且 $Z$ 变化会让 $K_{zz}$ 整体改变集合的一致性没法维持。实践里 $Z$ 固定只在离线阶段选。3.2 观测算子与扰动观测EnKF 里最容易被写错的一行观测算子 $h(z, x)$ 把状态映射到「在输入 $x$ 处的预测值」import numpy as np from scipy.linalg import cho_factor, cho_solve def h_member(u, log_ls, log_sf, xq, Zind, jitter1e-6): 单个集合成员的观测预测k(x*,Z) K_zz^{-1} u ls, sf np.exp(log_ls), np.exp(log_sf) Kzz rbf(Zind, Zind, ls, sf) jitter * np.eye(len(Zind)) c cho_factor(Kzz, lowerTrue) kx rbf(xq[None, :], Zind, ls, sf).ravel() # (M,) w cho_solve(c, kx) # K_zz^{-1} k(x*,Z) return float(w u) def H_ensemble(Zf, xq, Zind): 把整个集合映射成观测预测集合。Zf:(n_state, n_ens) - (n_ens,) M_ind len(Zind) return np.array([ h_member(Zf[:M_ind, i], Zf[M_ind, i], Zf[M_ind 1, i], xq, Zind) for i in range(Zf.shape[1]) ])注意h_member里每个成员用的是自己那份超参数去算 $K_{zz}$不是共享一份。这一点写错的话超参数就退化成「只影响输出尺度」的装饰学不出任何东西——因为集合里所有成员共用同一个核矩阵超参数只在最后乘 $u$ 时才起作用梯度信息全丢。分析步按标准 EnKF 写def enkf_analysis(Zf, y, Hf, R, rng, inflation1.02): Zf : (n_state, n_ens) 预报集合 y : float 当前观测 Hf : (n_ens,) 观测预测集合 R : float 观测噪声方差 n_ens Zf.shape[1] # 协方差膨胀先把集合往均值外侧推抵消有限的秩导致的信息丢失 mean Zf.mean(axis1, keepdimsTrue) Zf mean inflation * (Zf - mean) Hf Hf.mean() inflation * (Hf - Hf.mean()) dz Zf - Zf.mean(axis1, keepdimsTrue) # (n_state, n_ens) dh Hf - Hf.mean() # (n_ens,) Cyy dh dh / (n_ens - 1) R # 标量 Cxy dz dh / (n_ens - 1) # (n_state,) K Cxy / Cyy # 增益向量 # 每个成员一份独立的观测扰动方差必须等于 R D y rng.normal(0.0, np.sqrt(R), sizen_ens) Za Zf np.outer(K, D - Hf) return Za, KD那一行是整个 EnKF 最容易出问题的地方。观测扰动必须逐成员独立、方差严格等于 $R$。如果所有成员共用同一个扰动观测噪声的影响会被系统性抵消集合方差被低估跑几百步之后集合直接坍缩成一条线如果扰动方差写成 $R/2$ 或者忘了加模型会变得过度自信预测区间窄得离谱。协方差膨胀inflation默认 1.02是在补偿「集合大小有限 → 样本协方差秩只有 n_ens-1 → 低估真实不确定性」这个结构性偏差。数据集短、观测密集时可以开到 1.05开到 1.1 以上通常表现为预测方差虚高、均值抖动。3.3 分析步与预报步一个循环里的两次更新EnKF 的一步分两段。分析步吃进当前观测把集合往观测拉预报步把状态推进到下一个时刻。预报步里$u$ 是诱导点上的函数值属于模型的静态参数理论上不随时间变所以用持续性预报 $u_{k1} u_k$。超参数不一样工况在漂长度尺度应该缓慢跟着走用随机游走$$\log \theta_{k1} \log \theta_k \eta, \qquad \eta \sim \mathcal{N}(0, q^2)$$def forecast_step(Za, rng, rw1e-3): 持续性预报u 不动对数超参数加随机游走 Zf Za.copy() n_state Zf.shape[0] Zf[n_state - 2:, :] rng.normal(0.0, rw, size(2, Zf.shape[1])) return Zfrw这一项是整个实现里唯一调节「超参数适应速度」的旋钮量纲是每次更新的对数标准差。1e-3 意味着每步长度尺度变化约 0.1%1000 步累积约 3 倍——对小时级的缓慢漂移是合适的。数据是平稳的就把rw压到 1e-4 甚至 0让超参数几乎不动工况切换频繁比如设备启停时抬到 5e-3但代价是超参数会一直小幅抖动预测方差偏高。注意这里给超参数加了过程噪声却没给 $u$ 加。有些实现为了维持集合离散度也会给 $u$ 加一点我不建议——$u$ 的先验方差本来就由 $K_{zz}$ 决定人为加噪声等于篡改先验会让预测方差的校准彻底失真。3.4 和标准卡尔曼更新式的对照把两套公式并排放能看清 EnKF 到底「近似」掉了什么标准 KFEnKF 实现$P^- \mathbb{E}[(z-\bar z)(z-\bar z)^\top]$$\hat P \frac{1}{n_{ens}-1}\sum_i (z_i-\bar z)(z_i-\bar z)^\top$$K P^- H^\top (H P^- H^\top R)^{-1}$$K C_{xy}(C_{yy}R)^{-1}$由集合样本估计$\bar z^a \bar z^- K(y - H\bar z^-)$$z_i^a z_i^f K(y \varepsilon_i - h(z_i^f))$$P^a (I-KH)P^-$不再显式更新直接由更新后的集合重算关键差別在最后一行EnKF 不维护 $P$它维护集合本身协方差是从集合现算的。这带来两个直接后果——第一$K$ 的秩最多 $n_{ens}-1$状态维度超过这个数时增益必然是降秩的第二更新后的协方差自动包含非线性传播的效果不需要雅可比。所以 $n_{ens}$ 一般取 80~100而 $n_{state}$ 最好控制在 60 以内超了就上 localization 或者减诱导点。4. 诱导点的在线维护代码骨架、初始化与四个必调参数4.1 诱导点从哪来三种摆法及各自的失效场景诱导点的位置决定了模型能表达什么。常见的三种选法均匀网格。1D 或 2D 输入下最省事直接按分位数切。缺点是数据分布不均匀时密集区采样不足、稀疏区浪费预算。我一般先用它跑通流程再换。k-means 聚类中心。对训练输入跑一次 k-means取 $M$ 个中心。这是覆盖数据密度最稳妥的做法代价是每来一批新数据要重跑聚类——但只在离线阶段做一次在线阶段固定成本可以接受。注意 k-means 对量纲敏感先做标准化。在线增删。新数据点离最近诱导点的距离超过阈值 $\tau$ 时把它加进去预算超了就合并距离最近的一对。听起来很美实际有两个硬伤状态向量长度一变整个集合必须重新初始化前面学到的后验全丢合并操作让 $u$ 的语义发生跳变。绝大多数能稳定跑起来的实现都固定 $Z$把「覆盖不足」的问题交给长度尺度和更大的 $M$ 来吸收。下面这个初始化函数用的是 k-means 路线没有 sklearn 依赖时可以用分位数采样替代def init_inducing(X, M, rng, modequantile): 从训练输入里挑 M 个诱导点位置 if mode quantile: # 每个维度独立取分位数再组合。1D/2D 效果好高维退化为网格 qs np.linspace(0.05, 0.95, M) cols [np.quantile(X[:, j], qs) for j in range(X.shape[1])] Z np.stack(cols, axis1) if X.shape[1] 1: Z Z.reshape(-1, 1) return Z # 随机抽样版本从数据点里无放回抽 idx rng.choice(len(X), sizeM, replaceFalse) return X[idx].copy()分位数版本在 1D 下等价于等间距网格在 2D 下会退化成对角线上的一条线——这是它的硬伤2D 以上请老实上 k-means。高维输入$d5$时诱导点数量要按 $M \approx 10d$ 起步否则 $K_{zz}$ 条件数会很差。4.2 预测均值与方差的集合估计给定训练好的集合在查询点 $x_*$ 处的预测就是逐成员算一遍再统计def predict(Za, xq, Zind, sn0.1, ddof1): 集合预测返回均值、潜函数方差、含噪方差 M_ind len(Zind) n_ens Za.shape[1] vals np.array([ h_member(Za[:M_ind, i], Za[M_ind, i], Za[M_ind 1, i], xq, Zind) for i in range(n_ens) ]) mu vals.mean() var_f vals.var(ddofddof) # 潜函数方差 return mu, var_f, var_f sn ** 2 # 后者是含观测噪声的预测方差ddof1不能省。EnKF 里的样本协方差全部用无偏估计混用ddof0会让方差系统性偏小 $1/n_{ens}$跑长序列时这个偏差会累积成明显的过度自信。潜函数方差和含噪方差要分开返回。下游如果拿预测做采集函数或者置信区间判断用的是潜函数方差如果做的是传感器读数预测用含噪方差。这两个混用是最常见的接口层 bug——表现是预测区间看起来「刚好」但覆盖率测试只有 70%。4.3 在线主循环与参数表把上面几块拼起来就是完整的在线循环。注意初始化顺序先采 $u$再采超参数两者独立。def run_online(X, y, Zind, n_ens100, sn0.1, rw1e-3, inflation1.02, seed0, warmup30): rng np.random.default_rng(seed) M_ind len(Zind) n_state M_ind 2 # ---- 集合初始化 ---- Kzz0 rbf(Zind, Zind, 1.0, 1.0) 1e-6 * np.eye(M_ind) L np.linalg.cholesky(Kzz0) U L rng.normal(size(M_ind, n_ens)) # u ~ N(0, K_zz) log_ls np.log(1.0) rng.normal(0, 0.05, sizen_ens) log_sf np.log(1.0) rng.normal(0, 0.05, sizen_ens) Z np.vstack([U, log_ls[None, :], log_sf[None, :]]) hist [] for k in range(len(X)): # 预热期只用先验不做更新让集合先散开 if k warmup: Hf H_ensemble(Z, X[k], Zind) Z, K enkf_analysis(Z, y[k], Hf, sn ** 2, rng, inflation) hist.append(Z.copy()) Z forecast_step(Z, rng, rw) return Z, histwarmup这一段容易被忽略。前几十步如果不做更新、只走预报集合会按先验方差自然散开如果从第一步就开始更新初始集合的离散度被人为设得很小0.05 的对数扰动第一次更新几乎不动之后要很久才能恢复正常的离散度。我一般设warmup30数据量少于 100 条时干脆关掉在线更新直接当批量 GP 用。四个必须调的参数连同调错的症状参数典型值作用调错的典型症状n_ens80~100集合大小决定协方差秩太小 → 集合坍缩、方差趋零M_ind20~60诱导点个数太少 → 系统性欠拟合残差有结构sn传感器噪声量级观测噪声标准差太小 → 增益过大、跟着噪声跑rw1e-3~1e-2超参数随机游走步长太大 → 超参数发散预测方差虚高inflation1.01~1.05协方差膨胀系数缺省 → 长序列方差单调衰减sn最容易被随手填成 0.1。它的正确量级应该来自传感器手册或者一段静态数据的残差标准差。填大了模型反应迟钝填小了每次更新都当成真信号去追超参数会被噪声推得到处跑。5. 踩坑排查集合坍缩、负方差与超参漂移的五个现场5.1 集合坍缩预测方差一路掉到 1e-8现象跑了两三百步之后predict返回的var_f单调下降最后稳定在 1e-8 量级预测区间变成一条线但 RMSE 并没有同步变好。原因EnKF 的更新是收缩性的每次分析步都会把集合往观测方向压样本协方差单调减小。有限集合$n_{ens}100$下这个衰减比理论值快得多因为增益的秩只有 99一部分方向上的方差被永久抹掉。没有膨胀机制的话几百步之后就彻底坍缩。解决先确认inflation真的生效了——把它放在enkf_analysis内部、算协方差之前而不是在调用处乘。然后观察集合标准差的时间序列正常应该稳定在一个平台上小幅波动。如果 1.02 压不住往上试 1.05还压不住说明n_ens相对n_state太小加集合或者减诱导点。我遇到过一次 $n_{state}82$ 配 $n_{ens}50$ 的组合膨胀开到 1.08 才勉强稳住最后还是把诱导点砍到 30 才正常。5.2 负方差与 Cholesky 报错LinAlgError: Matrix is not positive definite现象某一步之后cho_factor抛异常或者预测方差出现 -1e-9 这样的负值被clip掉了。原因两个来源。一是Kzz jitter*I在超参数被更新到极小长度尺度时条件数爆炸1e-6 的 jitter 不够二是集合里出现了几乎重复的成员比如某些成员的 $u$ 被更新到同一值样本协方差奇异。解决jitter 改成自适应——先算np.linalg.cond(Kzz)超过 1e10 就把 jitter 从 1e-6 抬到 1e-4同时给 $\log \ell$ 加硬下界更新后np.clip(log_ls, np.log(0.01), np.log(100))。这个 clip 看着粗暴但它挡住的是「长度尺度跑到 1e-5 导致核矩阵退化成单位阵」这类灾难比事后调试便宜得多。负方差用np.clip(var, 0, None)兜底同时打日志——出现频率高说明模型已经不对了别当成正常现象。5.3 超参数漂移长度尺度单调爬到边界现象$\log \ell$ 的时间序列单调上升或下降最后贴着 clip 的边界不动模型退化成常数预测或者纯噪声拟合。原因rw太大加上sn估得太小。观测噪声小 → 每次更新的增益大 → 超参数被单个观测点猛推一把 → 随机游走又把它往同方向再推一点形成正反馈。解决先把sn按实际噪声标定再把rw砍一个数量级观察。另一个有效的做法是给超参数的随机游走加一个弱回归项往初始值拉相当于给超参数一个高斯先验def forecast_step_anchored(Za, rng, rw1e-3, anchor0.0, kappa1e-3): Zf Za.copy() n Zf.shape[0] Zf[n-2:, :] rng.normal(0.0, rw, size(2, Zf.shape[1])) Zf[n-2:, :] - kappa * (Zf[n-2:, :] - anchor) # 弱回归抑制漂移 return Zfkappa取rw的十分之一左右。它不改变稳态性能但能把发散的时间尺度从几百步拉到几千步。5.4 诱导点数超过集合数增益矩阵被悄悄降秩现象模型能跑不报错但拟合效果始终比同配置的批量 GP 差一截残差里能看到明显的系统性结构不是白噪声。原因$n_{ens} n_{state}$ 时样本协方差矩阵 $\hat P$ 的秩最多 $n_{ens}-1$增益 $K$ 把状态投影到了一个低维子空间。落在零空间里的方向永远得不到更新等价于那部分诱导点的函数值被冻结在先验上。$M100$、$n_{ens}100$ 就是典型的踩线配置。解决保持 $n_{ens} \ge 2 n_{state}$ 是安全的做不到就减 $M$。另一个办法是给协方差做局部化localization但 GP 的状态没有空间索引做不了标准的距离衰减局部化实践中更简单的是把诱导点分成几组、轮流更新或者干脆接受更大的集合。跑之前先打印一下n_state和n_ens这两个数看一眼就能避免大部分问题。5.5 数据顺序敏感同一批数据换个顺序结果差很多现象把同一段数据打乱顺序喂进去最终的 RMSE 差 20% 以上超参数收敛到完全不同的值。原因在线递推本身就是路径依赖的——EnKF 只保留当前集合不保留历史数据所以信息是有损的。顺序影响来自两方面观测噪声被假设为各向同性同方差而实际噪声可能有时变特性超参数的随机游走让早期数据的影响被逐渐遗忘这正是在线学习的本意但遗忘速率由rw决定。解决先接受它。路径依赖是在线 GP 的固有性质不是 bug。要减小它就降rw、加warmup、把inflation调小。要量化它就跑 3~5 个不同的打乱顺序看指标的标准差——如果标准差超过均值的 10%说明rw太大或者数据量对当前M来说不够。我一般会在评估报告里同时给「顺序 A」和「顺序 B」两组数而不是只报最好那一组。6. 收敛验证三条曲线判断在线 GP 是否真的在学在线模型最麻烦的地方是它永远不「训练完」你没法用最终 loss 判断好坏。我的做法是固定看三条曲线缺一条就不敢上线。第一条一步预测的标准化残差。定义 $e_k (y_k - \mu_k) / \sqrt{\sigma_k^2 \sigma_n^2}$正常应该近似标准正态——均值在 0 附近、标准差在 1 附近、无自相关。如果标准差到了 3说明模型过度自信sn太小或者inflation不够如果只有 0.3说明区间太宽sn太大或者rw太大。这条曲线比 RMSE 敏感得多RMSE 对区间宽度完全不敏感而在线 GP 的价值恰恰在区间上。第二条集合离散度与残差的一致性spread-skill。把窗口内所有时刻的集合标准差求平均再除以同期残差的 RMS比值应该落在 0.8~1.2。低于 0.5 就是坍缩的前兆高于 2 说明集合被人为撑得太大。这个指标能提前几百步预警 5.1 里的坍缩问题。第三条超参数轨迹。把 $\log \ell$ 和 $\log \sigma_f$ 的时间序列画出来。健康的轨迹是「快速调整一段 → 趋于平稳 → 小幅波动」如果一直在单调爬升说明 5.3 的漂移已经发生如果从第一步到最后一步几乎不动说明rw压得太死超参数实际上没在学退化成固定超参的稀疏 GP。有一个小技巧值得单独说用集合本身做交叉验证。集合里每个成员都可以看作后验的一个样本把第 $i$ 个成员单独拿出来算它在前 $k$ 步的预测 NLL取平均就得到一个不需要额外留出集的泛化估计。代价是 $n_{ens}$ 次前向计算但比重新跑一遍在线流程便宜得多。我在数据量紧张的场景下经常用它来做超参数的粗筛def loo_nll(Za, X, y, Zind, sn0.1): 用集合成员近似后验样本算平均 NLL n_ens Za.shape[1] nlls [] for i in range(n_ens): col Za[:, i:i1] # 单个成员当作一个后验样本 err 0.0 for k in range(len(X)): mu, var_f, var_y predict(col, X[k], Zind, sn) err 0.5 * np.log(2 * np.pi * var_y) 0.5 * (y[k] - mu) ** 2 / var_y nlls.append(err / len(X)) return float(np.mean(nlls)), float(np.std(nlls))返回的标准差本身也有信息量它反映后验的分散程度标准差大说明不同成员给出的解释差很多模型还没收敛。从那以后我每次上线一个新的在线 GP 模型都强制先跑一遍这三条曲线加一次 LOO-NLL哪怕指标看着漂亮也不跳。在线模型的失败几乎都是缓慢的、不报错的——它不会抛异常只会安静地变得过度自信然后在某个工况切换的瞬间给出一个窄得离谱的预测区间。这三条曲线就是那个后悔药。希望帮到你。本文还有配套的精品资源点击获取