简介:面向希望系统掌握贝叶斯神经网络理论与实践的机器学习开发者,尤其适合具备一定深度学习基础、需要量化模型不确定性的读者。压缩包内共十二个文件,包含六个Python脚本、四个Jupyter Notebook交互式教程以及说明文档和示例数据,总大小约一百六十四KB,涵盖贝叶斯后向传播(BBB)回归与分类、MC dropout回归与分类等典型实现,并配有README和中文说明,便于按模块循序渐进学习。教程从贝叶斯线性回归延伸到BBB变分推断与MC dropout,逐步展示如何将概率建模融入神经网络,每个Notebook都配有清晰注释和可视化输出,可直接在PyTorch或TensorFlow环境下运行。已有八十七人学习使用,通过动手实践可直观理解变分推断、蒙特卡洛采样在神经网络中的应用,并可将代码迁移至小样本学习、鲁棒性分析或不确定性预测等真实场景。
1. 拆开贝叶斯神经网络教程 zip,先看到的是不确定性的价值
一个分类器在测试集上跑到 97% 准确率,面对它没见过的数据时,仍然会给出一个接近 1 的 softmax 置信度。这个置信度并不代表它真的“懂”,因为传统神经网络用最大似然训练,权重是固定的点估计,预测结果天然缺少对自身不确定性的表达。贝叶斯神经网络(BNN)把权重建模成概率分布,用变分推断或 MC Dropout 这类后验近似方法,把不确定性变成可直接输出的预测方差。这套《BayesNuronalNetworksTutorial》代码包正是围绕这一点展开,BBB(Bayes by Backprop)和 MC Dropout 各配一个回归和一个分类案例,代码统一落在 Python + PyTorch 上。解压 zip 后会看到 BayesNuronalNetworksTutorial-main 目录,里面有 .py 和 .ipynb 两套可执行文件,想直接跑实验的人用 .py 就够了,想一边写笔记一边改参数则打开 Notebook。适合刚接触概率深度学习的人,也适合已经用惯确定性模型、想给线上预测加上可靠性边界的工程师。
2. 拆解代码包:BBB 与 MC Dropout 两条实现路线
2.1 压缩包里的文件到底想讲什么
打开 zip 后,目录排列很清楚。文件名带bbb的是 Bayes by Backprop:1_bbb-regression.py、2_bbb-classification.py及同名.ipynb;带mcdropout的是 MC Dropout 方案:3_mcdropout-regreesion.py、4_mcdropout-classification.py及同名.ipynb。这里有个小坑,3_mcdropout-regreesion.py把 regression 拼成了 regreesion,搜索文件时别按正确拼写去找。bbb.py是 BBB 的核心层,utils.py负责数据生成和画图,README.md是实验说明。
| 文件 | 实现方法 | 任务类型 |
|---|---|---|
bbb.py | Bayes by Backprop 核心模块 | 定义贝叶斯线性层 |
utils.py | 数据集生成、评价辅助 | 回归/分类通用 |
1_bbb-regression.py | BBB | 回归 |
2_bbb-classification.py | BBB | 分类 |
3_mcdropout-regreesion.py | MC Dropout | 回归 |
4_mcdropout-classification.py | MC Dropout | 分类 |
另外压缩包内附了一个“如果解压失败请用ara软件解压.txt”,这通常出现在中文 zip 包场景,某些解压器对中文文件名编码处理不当,换用 ara 这类对编码兼容更好的工具即可,与代码逻辑无关。
2.2 bbb.py 中的重参数化采样
BBB 的核心思路是把权重 W 当成随机变量,先设定先验 p(W),教程里常见的是N(0, 0.1),再用变分分布 q(W|theta) 去逼近真实后验。q 通常建模为对角高斯,theta 就是均值 mu 和标准差 sigma。如果直接从 q 里采样 W,梯度无法反传,所以bbb.py必须使用重参数化:先采eps ~ N(0,1),再算W = mu + sigma * eps。这一行变换把随机性转移到了 eps 上,mu 和 sigma 就有了可导路径。
import torch import torch.nn as nn import torch.nn.functional as F class BayesLinear(nn.Module): def __init__(self, in_features, out_features, prior_std=0.1): super().__init__() self.in_features = in_features self.out_features = out_features self.prior_std = prior_std # mu 是后验均值,rho 经过 softplus 后得到标准差 self.mu = nn.Parameter(torch.randn(out_features, in_features) * 0.1) self.rho = nn.Parameter(torch.full((out_features, in_features), -3.0)) self.bias_mu = nn.Parameter(torch.zeros(out_features)) self.bias_rho = nn.Parameter(torch.full((out_features,), -3.0)) def reparameterize(self, mu, rho): sigma = torch.log1p(torch.exp(rho)) # softplus,保证 sigma > 0 eps = torch.randn_like(mu) return mu + sigma * eps def forward(self, x): weight = self.reparameterize(self.mu, self.rho) bias = self.reparameterize(self.bias_mu, self.bias_rho) return F.linear(x, weight, bias)rho 初始化为-3.0而不是 0,原因在于softplus(-3) ≈ 0.049,让模型在训练早期从一个较小的不确定度开始,避免随机权重噪声过大导致前向输出完全失真。如果你复现时发现预测曲线忽大忽小,优先检查是不是把 rho 直接当标准差用了,而没有过softplus。记住,网络里真正参与矩阵乘法的weight是采样后的值,每次 forward 都会重新采一份权重,这就是 BBB 与普通线性层的本质区别。
2.3 MC Dropout 近似贝叶斯平均
MC Dropout 的工程实现比 BBB 更轻量,甚至不需要改网络结构。常规 Dropout 在测试阶段会被关闭,MC Dropout 反其道而行,在预测阶段保持 Dropout 打开,用多次随机前向传播的结果做平均和方差估计。从贝叶斯角度看,这等价于对权重后验做了一次蒙特卡洛采样。
import numpy as np import torch def mc_dropout_predict(model, x, n_samples=100): model.train() # 保持 dropout 激活,这是关键 outputs = [] with torch.no_grad(): for _ in range(n_samples): outputs.append(model(x).cpu().numpy()) outputs = np.stack(outputs) return outputs.mean(axis=0), outputs.std(axis=0)在教程的3_mcdropout-regreesion.py里,模型通常是两层全连接中间夹一个Dropout(0.2),预测阶段反复调用这个函数,就能得到每个输入点的均值曲线和标准差带。调用model.train()看起来反直觉,但目的就是让 Dropout 继续生效;model.eval()会立刻退化成单次确定性输出。需要额外留意的是,如果模型里同时有 BatchNorm,直接设成 train 会影响 BN 的统计量,这种情况下需要单独关闭 BN 的更新,或者把 Dropout 层拆出来单独控制。
这两条路线分别代表了 BNN 的两种主流近似。BBB 通过显式的变分后验分布建模权重,训练成本高但表达能力强;MC Dropout 几乎零成本,直接从确定性网络改造而来,适合快速给现有模型加不确定性。教程代码把两者放在一起,本质上是让学习者亲手对比它们的边界。
3. 复现 1_bbb_regression.py:先验、变分后验与 ELBO 损失
3.1 从 utils.py 生成带异方差噪声的回归数据
回归任务的样例通常不会用干净的正弦曲线,否则不确定性没有发挥空间。用np.linspace(-1, 1, 80)生成自变量,再叠加一个与 x 相关的噪声项,让中段波动大于两端,这样的数据跑完后,可以直观看到方差带在不同区域的宽度差异。
import numpy as np np.random.seed(42) x = np.linspace(-1, 1, 80).reshape(-1, 1) y = x * np.sin(4 * x) + 0.15 * np.random.randn(*x.shape)这段数据把异方差性注入目标值。如果模型用普通 MSE 训练,最终只会得到一条拟合均值曲线,而无法表达“中间这段我不确定”的信息。BNN 输出的是后验预测分布,均值负责拟合,方差负责表达置信区间,所以这里的噪声幅度直接决定了最终可视化里不确定性带的下限。
3.2 用 ELBO 把负对数似然与 KL 散度拼进损失
BBB 的训练目标不是最小化 MSE,而是最大化证据下界(ELBO)。写成最小化形式就是:
L = E_q[ -log p(y|w,x) ] + KL(q(w) || p(w))
其中第一项是数据拟合项,第二项是让变分后验不要离先验太远。对回归问题,-log p(y|w,x)等价于高斯分布下的负对数似然,实现时直接用 MSE 代替。KL 散度在两个高斯分布之间有解析式,前提是知道后验 q 的标准差 sigma,也就是softplus(rho)。
基于上一章的BayesLinear,先给每个层加一个计算 KL 的方法,再组装出完整的回归网络。
def kl_loss(self, prior_std=None): if prior_std is None: prior_std = self.prior_std q_sigma = torch.log1p(torch.exp(self.rho)) prior_sigma = torch.full_like(q_sigma, prior_std) var_ratio = q_sigma ** 2 / prior_sigma ** 2 t1 = 2 * torch.log(prior_sigma / q_sigma) t2 = self.mu ** 2 / prior_std ** 2 t3 = var_ratio - 1 return 0.5 * (t1 + t2 + t3).sum()这里的 KL 计算直接作用在 mu 和 rho 上,不需要额外采样。q_sigma每次 forward 时都从当前rho解算出来,保证 KL 与重参数化采样使用的是同一组变分参数。注意t1里对多个维度做求和,而t2和t3也是按元素计算后求和,这样得到的是整层的标量 KL 值,可以直接累加进总损失。
接下来组装回归网络:
class BayesianRegressor(nn.Module): def __init__(self): super().__init__() self.fc1 = BayesLinear(1, 64, prior_std=0.1) self.fc2 = BayesLinear(64, 64, prior_std=0.1) self.out = BayesLinear(64, 1, prior_std=0.1) def forward(self, x): x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.out(x)每一层都是贝叶斯线性层,激活函数放在采样后的输出上。因为每次 forward 都会重新采样权重,所以同一个输入在不同迭代里得到的输出本身就带有随机性,这正是模型拟合后验分布的表现。
3.3 训练循环中的 KLD 缩放与超参数
训练时需要在每个 batch 里累积所有贝叶斯层的 KL,然后除以样本数。原因是 KL 是整批数据的近似先验惩罚项,而 NLL 是逐样本误差;如果不缩放,KL 项很容易盖过数据拟合,导致训练只优化先验而忽略样本信息。
model = BayesianRegressor() optimizer = torch.optim.Adam(model.parameters(), lr=0.01) n_epochs = 300 for epoch in range(n_epochs): optimizer.zero_grad() y_pred = model(x_tensor) nll = torch.mean((y_pred - y_tensor) ** 2) kld = 0.0 for layer in model.modules(): if isinstance(layer, BayesLinear): kld = kld + layer.kl_loss(prior_std=0.1) loss = nll + kld / x_tensor.shape[0] loss.backward() optimizer.step()这里把kld除以训练样本总数,是常见做法之一。另一种做法是除以 batch size,那样会让 KL 在 Batch 较小时偏大,训练更不稳定。教程代码通常会打印每个 epoch 的 loss,如果在 loss 下降过程中出现大幅抖动,优先怀疑 KL 权重偏大,把prior_std调大或降低学习率即可。
| 超参数 | 教程常用值 | 说明 |
|---|---|---|
prior_std | 0.1 | 先验标准差,控制后验向零收缩的程度 |
rho初始值 | -3.0 | 约等于初始标准差 0.05 |
| 学习率 | 0.001 到 0.01 | Adam 下建议从 0.01 开始 |
| KLD 缩放 | 除以样本总数 | 让 KL 与 NLL 处于同一数量级 |
| 采样次数 | 1 | 训练时每 batch 采一次权重即可 |
训练结束后,保留模型,用固定输入跑 50 次 forward,收集预测均值与标准差,就能画出带置信带的回归曲线。如果标准差带在数据稀疏区域明显变宽,说明模型成功学到了位置相关的不确定性,这是确定性网络做不到的。
4. 分类任务中的不确定性:两个分类案例的对照
4.1 分类 BNN 的似然函数与置信度校准
分类任务里的负对数似然通常是交叉熵,模型输出 logits,经 softmax 后得到类别概率。传统分类器的问题在于,softmax 概率并不是校准后的不确定性,即使不正确也会给出高置信度。BNN 的分类输出则是对多个概率分布的期望平均,可以在置信度上给出额外方差,从而告诉使用者结果是否可靠。
教程里2_bbb_classification.py和4_mcdropout-classification.py都是做同样的分类任务,但采样方式不同。前者是 BBB,后者是 MC Dropout,对比两个案例能清楚看到实现难度的差异。
4.2 从 bbb 分类代码里看后验采样
BBB 分类模型的结构与回归几乎一样,只是输出维度从 1 变为类别数。在预测阶段,需要多次经过模型,得到多个 logits,然后对 softmax 概率求平均。直接对 logits 求平均再 softmax 是不等价的,因为 softmax 不是线性变换。
def bbb_classify(model, x, n_samples=20): model.eval() # 关闭 dropout,只使用权重分布 logits_list = [] with torch.no_grad(): for _ in range(n_samples): logits_list.append(model(x)) logits = torch.stack(logits_list) probs = torch.softmax(logits, dim=-1).mean(dim=0) return probsn_samples 取 20 通常就够用,再增大收益有限,反而增加推理耗时。这里的model.eval()不会关闭权重采样,因为采样发生在BayesLinear.forward里,和 Dropout 无关。如果你在这个阶段发现每次输出几乎一致,可以检查是否把 rho 训练成了较小的值,导致后验分布退化到近似点估计。
4.3 MC Dropout 分类如何完成多次前向传播
MC Dropout 分类代码更简洁,甚至不需要改动网络结构。只需要在预测阶段让 dropout 保持打开,收集 softmax 概率,然后计算均值与标准差。
def predict_mc_classification(model, x, n_samples=50): model.train() probs = [] with torch.no_grad(): for _ in range(n_samples): logits = model(x) p = torch.softmax(logits, dim=-1) probs.append(p.cpu().numpy()) probs = np.stack(probs) return probs.mean(axis=0), probs.std(axis=0)PyTorch 的Dropout在训练模式下会自动对激活值做尺度缩放,因此不需要再额外乘1/(1-p)。与 BBB 分类相比,MC Dropout 预测时每次都共享同一套权重,只是随机遮掉部分神经元,得到的方差主要来自网络结构的随机性,而不是权重后验的随机性。这个差异决定了 MC Dropout 的不确定性刻画能力比 BBB 粗一些,但胜在实现成本极低。
4.4 两条路线的适用边界
| 对比维度 | BBB | MC Dropout |
|---|---|---|
| 随机性来源 | 权重采样 | 神经元随机失活 |
| 训练改动 | 需替换贝叶斯层,自定义 ELBO | 原网络基本不动 |
| 预测开销 | 多次 forward,采样权重复用低 | 多次 forward,权重固定 |
| 后验表达能力 | 对角高斯,可学习均值与方差 | 隐式近似,方差与 dropout 率绑定 |
| 调参敏感点 | rho 初始化、KL 权重 | dropout 率、采样次数 |
| 推荐场景 | 对不确定性精度要求高的离线分析 | 已上线模型快速加不确定性 |
做这个对照是想强调:教程把两个算法放进同一套任务,并不是让读者判断谁更好,而是让读者意识到,同样的预测方差背后对应了完全不同的概率解释。BBB 输出的方差可以对权重视角做合理近似,MC Dropout 输出的是随机前向的离散程度。两者都能用,但写论文或做模型风险分析时必须区分清楚。
5. 验证不确定性的校准度,才是 BNN 的临门一脚
5.1 用可靠性图检查模型是否过度自信
拿到 BNN 的预测概率,不能直接认为它天然可信。分类模型输出 0.8 的概率,意味着 100 个这样的样本里大约 80 个真阳性,这才是校准。用测试集算 reliability diagram 时,把预测概率分桶,每个桶内比较平均置信度与实际频率。
def reliability_curve(y_true, y_prob, n_bins=10): bins = np.linspace(0, 1, n_bins + 1) conf = [] acc = [] for i in range(n_bins): mask = (y_prob > bins[i]) & (y_prob <= bins[i + 1]) if mask.sum() > 0: conf.append(y_prob[mask].mean()) acc.append((y_true[mask] == 1).mean()) return conf, acc画图时如果大部分点落在对角线下方,说明模型过度自信;落在上方则是过度保守。BNN 训练完成后,可以先跑这个函数确认校准情况,而不是只看 AUC 或 accuracy。对于回归任务,则检查每个置信区间的覆盖率,例如 95% 标准差带是否真的包含了约 95% 的测试样本。
5.2 温度缩放与方差阈值调整
当可靠性图显示过度自信时,一个不需要重新训练模型的修复方式是温度缩放。先学一个温度参数 T,把 logits 除以 T 后再过 softmax,使得置信度分布整体变平。BNN 中同样适用,尤其适合 MC Dropout 这种训练时没有显式不确定性约束的方法。
def temperature_scale(logits, temperature): return torch.softmax(logits / temperature, dim=-1)温度 T 大于 1 时概率分布更平滑,T 小于 1 时更尖锐。用一个验证集搜索最小化 NLL 的 T 值即可。完成校准后,把多次预测的标准差当作风险信号,在线上系统里设置阈值,例如标准差超过 0.3 的样本转发给人工复审,这比单纯看概率阈值更能捕捉分布外输入。实际部署时,我通常把 BBB 和 MC Dropout 的预测方差都记录进日志,离线定期观察方差分布,一旦发现方差带上移,就应该考虑数据分布是否已经偏移。
本文还有配套的精品资源,点击获取