拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

贝叶斯神经网络代码实战:PyTorch实现BBB与MC Dropout不确定性

贝叶斯神经网络代码实战:PyTorch实现BBB与MC Dropout不确定性

简介:这份贝叶斯神经网络教程代码包面向具备一定深度学习基础、希望掌握不确定性建模的开发者与研究人员,通过概率编程与神经网络结合的方式,解决小样本学习、模型校准及不确定性预测等场景中的参数不确定性问题。包内共12个文件,包含6个Python脚本与4个Jupyter Notebook,另有1个说明文档和1个Markdown说明,压缩包整体仅164KB,文件类型覆盖可直接运行的脚本与便于逐步演示的交互式笔记。代码覆盖贝叶斯线性回归、BBB分类、MC Dropout回归与分类等典型实现,分别对应变分推断与MCMC等核心方法,并涉及PyTorch/TensorFlow及概率编程工具的使用;教程从简单线性模型过渡到分类与回归任务,配有可视化对比,帮助理解先验设定、后验近似与不确定性输出。目前已有87人学习下载。学习者可对照Notebook逐步运行,结合理论说明快速上手贝叶斯神经网络的构建与应用,是一份高效的实操补充资料。

1. 贝叶斯神经网络代码教程:不是给模型换壳,是给预测加置信区间

同样一个分类任务,普通神经网络输出的是一个点估计:这张图有 87% 的概率是猫。贝叶斯神经网络(BNN)输出的是一整个分布:这张图是猫的概率在 70% 到 93% 之间波动,波动越大,说明模型对这张图越没底。这个差异在医学影像、自动驾驶这类高风险场景里是致命的——宁可不给结论,也不能给一个错的自信。这份BayesNuronalNetworksTutorial-main代码包,就是用 PyTorch 把 BNN 的两种主流实现——BBB(Bayes by Backprop)和 MC Dropout——完整跑通的教程代码,四个 notebook 覆盖回归和分类两类任务,适合已经会写普通 PyTorch 模型、想进一步量化不确定性的读者。和我一样,你大概率已经训练过几十个 CNN 模型,真正缺的不是理论书,而是一份能改、能跑、能看损失的代码。

2. 压缩包内容与复现环境:先摸清四个 notebook 和两个核心模块

2.1 文件清单:哪个文件对应哪种算法

解压后你会发现整个项目结构并不复杂,核心就一个bbb.py和一个utils.py,剩下的全是成对的.ipynb和.py。我按自己复现时的习惯给它们排了个序,先跑回归、再跑分类,理解曲线会平缓很多。

文件作用建议运行顺序
1_bbb-regression.ipynb / .pyBBB 做回归任务,最基础,适合入门1
2_bbb-classification.ipynb / .pyBBB 做分类任务,涉及 softmax 似然与 KL 项调参2
3_mcdropout-regreesion.ipynb / .pyMC Dropout 做回归,对比 BBB 的另一种近似路径3
4_mcdropout-classification.ipynb / .pyMC Dropout 做分类,推理阶段要跑多次前向4
bbb.pyBBB 核心模块,变分层、重参数化采样、ELBO 损失配合 1、2
utils.py数据加载、可视化辅助函数全部依赖
README.md项目说明与运行指引先读

这个顺序是我自己的习惯,不是强制要求。如果你对变分推断更熟,直接从4_mcdropout-classification.ipynb开始也没问题,但新手建议老老实实按上表跑。

2.2 环境配置:PyTorch 版本与依赖项

项目没有给出requirements.txt,这是它作为教程代码的一个小遗憾。我复现时用的是 Python 3.8 + PyTorch 1.12 + CUDA 11.3,跑下来的经验是:torch、torchvision、matplotlib、numpy、tqdm这五个装齐就够,不需要额外安装 Pyro 或 Stan——这个教程用的是纯 PyTorch 手写变分推断,反而更适合理解原理。

conda create -n bnn python=3.8 conda activate bnn pip install torch==1.12.1 torchvision==0.13.1 --index-url https://download.pytorch.org/whl/cu113 pip install matplotlib numpy tqdm jupyter

参数说明:PyTorch 1.12 是 2022 年的稳定版本,和这份代码里的 API 匹配度最高。如果你用 2.x 版本跑,大概率会遇到torch.distributions某些接口行为变化,后面避坑章节会细讲。

逻辑说明:先建独立环境,再装 PyTorch 全家桶,最后补齐可视化工具。没必要装最新版 PyTorch,教程代码不是生产项目,稳定复现比追求新版特性更重要。

2.3 跑通第一个回归任务的最小操作

jupyter notebook 1_bbb-regression.ipynb

打开 notebook 后从上到下执行,如果utils.py里的数据加载函数没有报错,说明环境配置成功。第一次跑通时你会在终端看到类似Epoch 100, Loss: -125.34的输出,这个 loss 是负的 ELBO,不是正数——ELBO 最大化对应损失函数最小化,所以 loss 是负值完全正常,这是新手的第一个认知门槛。

3. BBB 变分推断:从先验到后验的完整代码路径

3.1 为什么选择变分推断而不是 MCMC

贝叶斯神经网络的终极目标是算出后验分布 p(w|D),但神经网络参数量动辄百万级,精确后验不可解,必须做近似。MCMC 能给无偏估计,但收敛极慢,跑一个 MNIST 级别的网络要几天。变分推断的思路是找一个近似的分布 q(w),让它和真实后验的 KL 散度最小。这个优化目标就是 ELBO(Evidence Lower Bound),等价于最小化 KL(q(w)||p(w|D))。

# bbb.py 中核心的变分层实现(简化版) class BayesianLinear(nn.Module): def __init__(self, in_features, out_features, prior_var=0.05): super().__init__() # 权重参数的均值 mu 和方差 rho self.mu = nn.Parameter(torch.randn(in_features, out_features) * 0.1) self.rho = nn.Parameter(torch.randn(in_features, out_features) * 0.1) self.prior_var = prior_var def forward(self, x): # 用重参数化技巧采样权重:w = mu + sigma * epsilon sigma = torch.log1p(torch.exp(self.rho)) # softplus 保证方差为正 epsilon = torch.randn_like(self.mu) w = self.mu + sigma * epsilon return F.linear(x, w.T)

逻辑说明:传统线性层的权重是固定参数,这里把权重拆成mu和rho两部分。rho经过 softplus 变换后得到标准差 sigma,采样时用重参数化技巧w = mu + sigma * epsilon,这样梯度可以通过epsilon反传——这是 BBB 算法的核心操作,也叫 Bayes by Backprop。

参数说明:prior_var=0.05是先验方差,对应先验分布 N(0, 0.05),这个值决定 KL 项对最终 loss 的牵制强度。先验方差越大,KL 项越弱,模型自由度越高;越小,模型越保守,拟合能力下降。经验值是分类任务用 0.03~0.1,回归任务可以放宽到 0.1~0.5。

3.2 损失函数:ELBO 的两个组成部分

BBB 的训练 loss 由两部分构成:模型输出对训练数据的负对数似然(拟合项)加上 KL 散度项(正则项)。代码里通常这样写:

def elbo_loss(model, x, y, num_samples=5, kl_weight=0.1): log_likelihood = 0.0 kl_divergence = 0.0 for _ in range(num_samples): # 每次前向采样一组新的权重 output = model(x) log_likelihood += -F.mse_loss(output, y, reduction='sum') kl_divergence += model.kl_loss() # 计算 q(w) 和先验 p(w) 的 KL # 平均后组合 log_likelihood /= num_samples kl_divergence /= num_samples return kl_divergence * kl_weight - log_likelihood

逻辑说明:num_samples=5表示每个 batch 前向传播 5 次,每次都重新采样权重,取平均作为最终 log-likelihood。这是 BBB 训练和普通神经网络训练最大的速度差异来源——计算量翻了 5 倍。

参数说明:kl_weight控制正则强度,这是 BBB 调参的核心旋钮。我复现时发现分类任务里kl_weight小于 0.01 时 KL 项几乎不起作用,模型退化成普通的确定性网络;大于 1 时模型过度正则化,训练 loss 降不下去。0.05~0.2 是个相对安全的区间,建议从 0.1 开始调。

3.3 分类任务的似然函数切换

回归用的是 MSE 做似然假设(高斯误差),分类任务要把输出层换成 softmax,损失函数也要从F.mse_loss改成F.cross_entropy,同时输出的不确定性表达方式完全不同:

# 分类任务的 BBB 输出 output = model(x) # shape: [batch_size, num_classes] log_softmax = F.log_softmax(output, dim=1) log_likelihood += torch.sum(log_softmax * y_onehot)

逻辑说明:分类任务下模型输出的是每个类别的 log 概率,y_onehot是真实标签的 one-hot 向量。单次前向得到的是一个概率分布,多次前向取平均后,每个类别除了有均值概率,还能算方差——这就是分类任务的不确定性。需要注意的是,分类任务里同一个样本会跑 5 次前向,softmax 的输出 5 次求平均才是最终的预测概率,不能只取最后一次。

4. MC Dropout 与不确定性量化:同一套网络换一种推理方式

4.1 MC Dropout 的原理:训练时的偶然变成了推理时的必然

MC Dropout 的思路比 BBB 简单得多:训练时网络里有 Dropout 层随机丢弃神经元,推理时把 Dropout 保持打开,多跑几次前向传播,把结果当成从近似的后验分布中采样。Gal 在 2016 年证明了带 Dropout 的 DNN 等价于深度高斯过程的变分近似——这意味着你现有的任何一个带 Dropout 的网络,不用改结构、不用改训练方式,就能变成贝叶斯网络。

def mc_dropout_predict(model, x, num_forward_passes=30): """保持 Dropout 开启状态,多次前向采样 """ predictions = [] model.train() # 关键:不能用 eval(),否则 dropout 被关闭 with torch.no_grad(): for _ in range(num_forward_passes): pred = model(x) predictions.append(pred) predictions = torch.stack(predictions, dim=0) # 均值作为预测结果,方差作为不确定性 mean = predictions.mean(dim=0) variance = predictions.var(dim=0) return mean, variance

逻辑说明:这个函数是 MC Dropout 推理的全部核心。注意model.train()这行——常规推理都会切eval(),但 MC Dropout 必须保持训练模式,让 Dropout 层继续随机丢弃。num_forward_passes=30是采样次数,次数越多不确定性估计越稳定,但推理时间线性增长。

参数说明:num_forward_passes的经验取值:MNIST 级别小网络 10~30 次,复杂图像任务网络大、单次推理慢,可以先跑 5 次看方差有没有区分度,再逐步加到 30 次。另外 Dropout 比率 p 本身就是超参数:p=0.5 时不确定性最大,p=0.1 时模型几乎退化成确定性网络。

4.2 回归与分类的不确定性计算差异

回归任务的预测是连续值,多次前向采样的方差直接就是模型对预测值的不确定性,单位与目标值一致;分类任务要分两部分看——预测均值是类别概率的均值,方差反映的是类别间的置信程度。代码里的呈现方式:

# 分类任务:多轮 softmax 概率取均值和方差 probs = [] for _ in range(30): output = torch.softmax(model(x), dim=1) probs.append(output) probs = torch.stack(probs) mean_prob = probs.mean(dim=0) # [batch, num_classes] aleatoric = probs.var(dim=0) # 偶然不确定性

逻辑说明:aleatoric是偶然不确定性,代表数据本身的噪声;如果你还想看模型对参数的不确定性(认知不确定性),可以比较不同 dropout mask 下预测结果的离散程度。实际应用时可以只输出mean_prob和var两个量,前者做决策,后者做置信过滤。

4.3 和 BBB 的对比:怎么选

维度BBBMC Dropout
理论基础变分推断,显式逼近后验隐式近似,等价于深度高斯过程
参数量双倍(mu + sigma)与原网络相同
训练开销每个 batch 多次前向,约 5 倍与普通训练相同
推理开销多次前向采样多次前向采样
适用场景从头训练、需要显式先验已有训练好的模型、快速部署

实操判断:如果你手里有一个已经训练好的普通神经网络,想快速加不确定性能力,MC Dropout 是零门槛的改法——把model.eval()改成model.train(),预测循环包一层即可。如果你从零设计新模型,且对先验有明确要求(比如知道权重应该落在某个小区间),选 BBB。复现教程时两边都跑一遍,感受一下两个算法的不确定性差异,这个是代码包最有价值的练习。

5. 复现避坑:五个最容易翻车的细节

5.1 解压报错:ZIP 包在 Windows 下提示文件损坏或乱码

现象:双击 zip 文件用系统自带解压工具解压,报「无法完成操作」或解压后 notebook 文件名乱码。 原因:打包时用了非 Windows 默认的压缩编码,Windows 资源管理器自带解压器对 zip 的 UTF-8 标记识别不完整,尤其包含中文文件名时更容易翻车。 解决:换第三方解压工具。项目内附的 txt 提示说的「如果解压失败请用 ara 软件解压」指的就是这类兼容性问题。我自己的经验是装一个 Bandizip 或 7-Zip,右键「解压到当前文件夹」,基本都能正常出来。另外一个细节:解压后先看文件夹内的README.md和utils.py文件名是否完整,有乱码就说明编码没解对。

5.2 KL 权重没调,模型输出「假不确定性」

现象:BBB 训练结束后,不同样本的方差几乎一模一样,模型退化成了确定性网络,不确定性输出没有区分度。 原因:kl_weight太小或太大,见 3.2 节。KL 项太弱时,模型完全被数据拟合主导,后验塌缩成接近点估计;KL 项太强时,所有样本都被先验拉向同一个中心,同样没有区分度。 解决:用 3.2 节的损失函数,固定kl_weight=0.1跑 50 个 epoch 后画一下测试集每个样本的方差分布。如果方差集中在很小的区间(最大值和最小值相差不到 1 个数量级),就把kl_weight从 0.1 往上(0.3、0.5)或往下(0.03、0.01)各试一轮,选方差区分度最大的那个。这是调参的玄学环节,没有公式,只能手动搜。

5.3 MC Dropout 推理时不确定性为零

现象:改了mc_dropout_predict函数,但输出的variance全是数值接近 0 的张量。 原因:推理用了model.eval()。PyTorch 的 eval 模式会把 Dropout 层关掉,所有神经元都参与计算,输出自然没有随机性。这个问题隐藏很深,因为模型不会报错,loss 正常、预测正常,就是方差没了。 解决:运行mc_dropout_predict前先打印一层 Dropout 模块的training属性——model.dropout.training输出True才是对的。更稳妥的写法是用model.apply把除了 Dropout 之外的所有层切到 eval,但新手最常见做法就是直接model.train()整模型前向。

5.4 PyTorch 2.x 下的 distribution 接口变化

现象:按 README 跑1_bbb-regression.ipynb,报错TypeError: rsample() got an unexpected keyword argument 'sample_shape'或AttributeError: module 'torch.distributions' has no attribute 'Normal'。 原因:PyTorch 版本差异,教程代码大概率基于 1.x 写的,2.x 对torch.distributions内部实现改过一轮。 解决:两个选择。一是按 2.1 节装 PyTorch 1.12 的 conda 环境,稳定复现;二是在 2.x 下把代码里的dist.Normal(mu, sigma).rsample()改成dist.Normal(mu, sigma).rsample(torch.Size([1]))再取下标[0]。我建议选第一个,教程复现的目标是理解原理,不是和生产环境对齐。

5.5 训练速度慢到无法忍受

现象:单卡 1080Ti 跑2_bbb-classification.ipynb,一个 epoch 要 3 分钟以上。 原因:BBB 每个 batch 前向传播num_samples=5次,梯度也要回传 5 次,等于 5 倍计算量,还叠加了 KL 散度的 autograd 开销。MCDropout 训练时不额外采样,速度正常。 解决:复现阶段把 batch size 从默认值调大一倍,或者num_samples从 5 降到 2,损失精度换速度。如果只是验证代码能跑通,num_samples=1都可以。注意:不是所有层都需要贝叶斯——只把最后两层改成 BBB 层,前面的卷积层保持确定性,这是实际工程里最常用的压缩方案。

6. 把不确定性用起来:概率校准与决策阈值验证

6.1 用概率校准指标验证不确定性质量

不确定性数值算出来了,怎么证明它不是模型瞎给的?答案是概率校准:当模型说一个样本有 80% 概率是正类时,这类样本的真实正类比例应该接近 80%。实现一个校准曲线就是按预测概率分桶,统计每个桶内的真实频率:

def expected_calibration_error(probs, labels, num_bins=10): """ECE 校准误差:预测置信度和真实准确率的偏差 """ bin_boundaries = torch.linspace(0, 1, num_bins + 1) ece = 0.0 for i in range(num_bins): mask = (probs >= bin_boundaries[i]) & (probs < bin_boundaries[i + 1]) if mask.sum() == 0: continue avg_conf = probs[mask].mean() # 预测置信度均值 avg_acc = (labels[mask].float() == 1).float().mean() # 真实频率 ece += mask.float().sum() * (avg_conf - avg_acc).abs() return ece / probs.shape[0]

逻辑说明:probs是模型对某个类的预测概率,labels是对应的二值标签。把概率按 0~1 等分成 10 个桶,比较每个桶内「预测平均置信度」和「真实正样本比例」的偏差。ECE 越小,校准越好,理想值是 0。用这个指标对比普通网络和 BNN,你会发现 BNN 通常在校准性上更有优势。

6.2 把不确定性变成决策规则:置信度阈值过滤

实际工程里,不确定性最直接的价值就是做「拒绝决策」——把模型没把握的样本过滤出来转人工。做法是把 6.1 节算出的每个样本的方差排序,取前 20% 高不确定样本做掩码,观察去掉这些样本后准确率是否提升:

python eval_with_uncertainty.py --drop_threshold 0.8

如果去掉高不确定样本后准确率从 85% 提升到 92%,说明不确定性输出是有意义的,阈值调参方向也正确。这一步是很多项目真正落地 BNN 的地方,也是教程里没写但最实用的延伸。从那以后我每次跑完 BNN,都会强制走一遍「算 ECE、画校准散点、调kl_weight或num_forward_passes」这个流程,再也没被「模型看起来会了」骗过。希望这些细节帮到你,少踩一点我当年翻过的车。

本文还有配套的精品资源,点击获取

返回列表