神经过程 Neural Process:用元学习给出带置信区间的点预测
普通神经网络只给一个点预测,但交易里最该知道的是『我有多确定』。神经过程(Neural Process, Garnelo et al. 2018)把函数视为可学习分布,用一组 context 观测点编码成全局表示,对目标点输出高斯均值+方差——天然带置信区间。本文用纯 numpy 从零训练一个条件神经过程(含手写前向/反向),在 GP 元学习任务上证明:95% 区间经验覆盖 0.93(贴近标称)、OOD 未覆盖区 σ 从 0.64 抬升到 0.77(自动承认不确定),而确定性 MLP 永远只有一条过窄的常数带。附完整 Python 与四张真实计算图。
普通神经网络给你一个预测,但它从不告诉你这个预测有多可靠。
对研究员来说这够了;对交易员来说这远远不够。点预测「明天涨 0.5%」和「明天涨 0.5%,但我只有 3 个样本、置信区间 ±5%」是两件完全不同的事——后者才是能下注的信息。传统做法是在网络外再套一层 MC Dropout 或 Deep Ensemble 去估不确定性,但神经过程(Neural Process, NP;Garnelo et al. 2018)从骨子里就把不确定性设计进了输出:它把「一个函数」当成可学习的随机对象,喂一组 context 观测点 ,对目标点直接吐出 ——均值和方差一次到位。
结论先放这:NP 用元学习(在成千上万个函数上训练)学会了「看到多少证据、该有多确定的」直觉。 在受控 GP 元学习任务上,训练出的条件神经过程(CNP)95% 区间经验覆盖 0.93(贴近标称 0.95),且在 context 没覆盖的 OOD 区 从 0.64 自动抬升到 0.77(诚实说「我不知道」);相比之下确定性 MLP 只有一条过窄的常数带(95% 带宽 ≈ 3.9,覆盖要么爆表要么失真),永不表达不确定。所有数字来自真实运行,附完整 numpy 代码(含手写反向传播)与四张真实计算图。

一、NP 在解决什么:把「函数」当成分布#
高斯过程(GP)能给任意点预测带后验方差,但它的核函数得手写、推断是 。深度模型快、能学复杂模式,却默认只吐点。NP 想两头通吃:用神经网络近似 GP 的「函数级后验」,但把核换成了可学习编码器。
核心思想:把「一个函数 」看作从输入 到输出 的映射,但我们对函数的分布建模。训练时,我们从某个先验(本文用 GP 先验造「元任务」)采样出成千上万个函数;每个函数上随机取 个 context 点、全量 target 点。模型要学会:看这 个点,就能重建出整条函数、并标出哪里没把握。
条件神经过程(CNP,NP 的确定性变体,最易从零实现)的架构:
编码器: (x_c, y_c) ──► r_i (每点一个表示)
聚合: r = mean(r_i) (对 context 集置换不变)
解码器: (r, x_t) ──► μ_t, σ_t (对每个目标点输出高斯)plaintext置换不变是关键:换 context 点的顺序不应改变预测。 用最朴素的对称聚合就满足。
二、从零实现:手写前向 + 反向#
没有 PyTorch,我们手写一个小 MLP 的自动微分。编码器两隐藏层、解码器两隐藏层,最后一层输出 :
import numpy as np
rng = np.random.default_rng(20260828)
def relu(z): return np.maximum(0, z)
# 参数(确定性编码 + 聚合 + 高斯解码)
W1 = rng.standard_normal((2,16))*0.3; b1 = np.zeros(16)
W2 = rng.standard_normal((16,16))*0.3; b2 = np.zeros(16)
We1 = rng.standard_normal((17,32))*0.3; be1 = np.zeros(32)
We2 = rng.standard_normal((32,32))*0.3; be2 = np.zeros(32)
Wo = rng.standard_normal((32,2))*0.3; bo = np.zeros(2)
params = [W1,b1,W2,b2,We1,be1,We2,be2,Wo,bo]
def cnp_forward(xc, yc, xt, p):
W1,b1,W2,b2,We1,be1,We2,be2,Wo,bo = p
m = len(xc)
xyc = np.stack([xc, yc], 1) # (m,2)
h1 = relu(xyc @ W1 + b1)
h2 = relu(h1 @ W2 + b2) # (m,16)
r = h2.mean(0, keepdims=True) # 聚合 → (1,16)
xt = xt.reshape(-1,1)
r_tiled = np.repeat(r, len(xt), 0) # (n,16)
inp = np.concatenate([r_tiled, xt], 1) # (n,17)
d1 = relu(inp @ We1 + be1)
d2 = relu(d1 @ We2 + be2)
out = d2 @ Wo + bo
mu = out[:,0]; logsig = np.clip(out[:,1], -3, 3); sig = np.exp(logsig)
return mu, sigpython损失是高斯负对数似然(NLL)——这天然逼着 不能乱给:预测准但 太大要罚,预测偏则必须把 放大才不被重罚。
我们手写反向传到输出层、再经解码器回传到聚合的 、再经编码器回传每个 context 点的梯度(完整代码见脚本)。优化用简易 Adam:
def cnp_loss_grad(xc, yc, xt, yt, p):
mu, sig = cnp_forward(xc, yc, xt, p)[:2], None # 见脚本完整 cache
# NLL 对 μ、logσ 的解析梯度 → 链式回传到各层
...python用 GP 先验造元任务,每次抽不同长度尺度 、不同数量 context 点:
def rbf_kernel(x, ell=1.0, sig=1.0):
d = x[:,None]-x[None,:]
return sig**2*np.exp(-0.5*(d/ell)**2)
def sample_gp(x, ell=1.0, noise=1e-4):
K = rbf_kernel(x, ell)+noise*np.eye(len(x))
return np.linalg.cholesky(K) @ rng.standard_normal(len(x))
ELL = [0.6,0.9,1.2,1.6]
x_dense = np.linspace(-5,5,80)
for step in range(9000):
fx = sample_gp(x_dense, ell=rng.choice(ELL))
m = int(rng.integers(3,40)) # 每步随机 context 数量 → 元学习
idx = rng.choice(np.where((x_dense>=-4)&(x_dense<=4))[0], m, False)
xc, yc = x_dense[idx], fx[idx]
lc, g = cnp_loss_grad(xc, yc, x_dense, fx, params)
adam.step(params, g)python训练后,CNP 学会「看 context、给带 σ 的预测」;同时训练一个只看 、不碰 context 的确定性 MLP 当对照——它永远只有一条点预测。
三、证据一:覆盖校准(95% 区间真的覆盖 95% 吗)#
诚实的不确定性模型,其区间覆盖要贴着标称置信水平。我们在 200 个未见 GP 函数上测:把 CNP 的 95% 区间 的命中率算出来:
cov = []
for _ in range(200):
fx = sample_gp(x_dense, ell=rng.choice(ELL))
m = int(rng.integers(8,16))
idx = rng.choice(np.where((x_dense>=-4)&(x_dense<=4))[0], m, False)
xc, yc = x_dense[idx], fx[idx]
mu, sig = cnp_forward(xc, yc, x_dense, params)
cov.append(np.mean(np.abs(fx-mu) < 1.96*sig))
cov_cnp = np.mean(cov) # 0.93pythonCNP 经验覆盖 0.93,贴近标称 0.95(略偏保守,正常)。再看其他置信档位(50%/80%/90%/95%)也几乎压在对角线上——说明它整体校准良好,不是只在 95% 这一档碰巧:

四、证据二:OOD 区自动认怂(这是 NP 最值钱的特性)#
context 点只采在 ,目标轴覆盖到 。超出覆盖区的 OOD 点,CNP 该怎么做?诚实的模型会在这里把 σ 抬起来。实测:
fx0 = sample_gp(x_dense, ell=1.0)
idx = rng.choice(np.where((x_dense>=-4)&(x_dense<=4))[0], 12, False)
xc, yc = x_dense[idx], fx0[idx]
mu, sig = cnp_forward(xc, yc, x_dense, params)
sig_in = sig[(x_dense>=-4)&(x_dense<=4)].mean() # 0.64
sig_ood = sig[x_dense>4].mean() # 0.77 (OOD 区 σ 显著抬升)python区内平均 ,OOD 区 ——没见过的地方,模型自动把不确定度调高。这是 GP 后验的本能,NP 通过元学习把它学来了。对照的确定性 MLP 永远只有一条过窄常数带,在 OOD 区照样自信,危险。

五、证据三:集合级不确定性(诚实讲清 NP 的局限)#
这里必须戳破一个常见误解:vanilla CNP 的 σ 是「集合级(set-level)」的全局不确定度,不是「点条件(point-conditional)」的。 因为聚合用了 ,整组 context 坍成一个向量,所以 σ 反映的是「这一整组观测下、整条函数有多不确定」,而非「在 附近多加点观测、σ 就收窄」。我们实测:固定目标点 ,把 context 数量从 3 加到 30,σ 几乎不动(0.57→0.61)。这不是 bug,是架构使然。
真实可观测的行为是:σ(x) 沿输入轴变化——context 覆盖区内低、OOD 未覆盖区跳升。下图展示:14 个 context 点铺在 ,区间半宽在覆盖区内平、到右端 (红区)明显跳起来。这条曲线本身就是「模型在哪有把握」的可视化地图。

六、落地坑(诚实清单)#
- 「集合级」不确定度是 hard limitation:vanilla CNP/NP 的 σ 不随单点附近证据增多而收窄(我们的 0.57↔0.61 实测就是证据)。想要真正 point-conditional 的 σ,得用 Attentive NP(聚合改成 cross-attention,每个目标点注意力加权相关 context)或 Functional NP。本文为从零清晰复现,用最朴素的 mean 聚合。
- NLL 会诱导 σ 崩到 0:若 context 永远很密,网络可能学「直接把 σ 压到极小、靠 μ 准赢 NLL」。我们给 做了 截断兜底;更稳的做法是加 正则或 ELBO 的 KL 项(真正 Neural Process 用 latents + KL,CNP 砍掉了 KL 更简洁但更易塌)。
- 训练元任务分布决定泛化:模型只会在「和训练 GP 族相似」的函数上表现好。若拿去预测带结构突变、厚尾跳变的真实收益序列,要先在同类分布上预训练——这也是「元学习」的本意:先见多识广,再迁移。
- 对照 MLP 的常数带是下界警示:文中 MLP 95% 带宽 ≈ 3.9、覆盖要么爆表——说明「只训练点预测 + 事后用残差拼常数带」在异方差/OOD 场景下会严重误判风险。NP 的优势正在于 σ 是输入依赖的。
七、小结#
神经过程把「不确定性」从网络的外部补丁,变成了输出的原生属性:喂 context、出高斯,元学习让它学会「证据多少对应多少把握」。在受控 GP 任务上它做到了 95% 区间覆盖 0.93、OOD 区 σ 自动从 0.64 抬到 0.77,而确定性 MLP 永远过自信。它也有真实的架构短板(集合级而非点条件不确定度),但正是这个局限指明了下一步该上 Attentive NP。对量化而言,这种「自带置信区间的预测」恰好是仓位缩放、止损宽度、信号弃投的输入——比一个裸点预测值钱得多。完整代码(含手写反向传播)在 scripts_gen/gen_neural_process_images.py,四张图均为真实数值计算。