简介这份贝叶斯神经网络教程代码包面向具备一定深度学习基础、希望掌握不确定性建模的开发者与研究人员通过概率编程与神经网络结合的方式解决小样本学习、模型校准及不确定性预测等场景中的参数不确定性问题。包内共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 的两种主流实现——BBBBayes by Backprop和 MC Dropout——完整跑通的教程代码四个 notebook 覆盖回归和分类两类任务适合已经会写普通 PyTorch 模型、想进一步量化不确定性的读者。和我一样你大概率已经训练过几十个 CNN 模型真正缺的不是理论书而是一份能改、能跑、能看损失的代码。2. 压缩包内容与复现环境先摸清四个 notebook 和两个核心模块2.1 文件清单哪个文件对应哪种算法解压后你会发现整个项目结构并不复杂核心就一个bbb.py和一个utils.py剩下的全是成对的.ipynb和.py。我按自己复现时的习惯给它们排了个序先跑回归、再跑分类理解曲线会平缓很多。文件作用建议运行顺序1_bbb-regression.ipynb / .pyBBB 做回归任务最基础适合入门12_bbb-classification.ipynb / .pyBBB 做分类任务涉及 softmax 似然与 KL 项调参23_mcdropout-regreesion.ipynb / .pyMC Dropout 做回归对比 BBB 的另一种近似路径34_mcdropout-classification.ipynb / .pyMC Dropout 做分类推理阶段要跑多次前向4bbb.pyBBB 核心模块变分层、重参数化采样、ELBO 损失配合 1、2utils.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 python3.8 conda activate bnn pip install torch1.12.1 torchvision0.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 散度最小。这个优化目标就是 ELBOEvidence Lower Bound等价于最小化 KL(q(w)||p(w|D))。# bbb.py 中核心的变分层实现简化版 class BayesianLinear(nn.Module): def __init__(self, in_features, out_features, prior_var0.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_var0.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_samples5, kl_weight0.1): log_likelihood 0.0 kl_divergence 0.0 for _ in range(num_samples): # 每次前向采样一组新的权重 output model(x) log_likelihood -F.mse_loss(output, y, reductionsum) 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_samples5表示每个 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, dim1) 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_passes30): 保持 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, dim0) # 均值作为预测结果方差作为不确定性 mean predictions.mean(dim0) variance predictions.var(dim0) return mean, variance逻辑说明这个函数是 MC Dropout 推理的全部核心。注意model.train()这行——常规推理都会切eval()但 MC Dropout 必须保持训练模式让 Dropout 层继续随机丢弃。num_forward_passes30是采样次数次数越多不确定性估计越稳定但推理时间线性增长。参数说明num_forward_passes的经验取值MNIST 级别小网络 10~30 次复杂图像任务网络大、单次推理慢可以先跑 5 次看方差有没有区分度再逐步加到 30 次。另外 Dropout 比率 p 本身就是超参数p0.5 时不确定性最大p0.1 时模型几乎退化成确定性网络。4.2 回归与分类的不确定性计算差异回归任务的预测是连续值多次前向采样的方差直接就是模型对预测值的不确定性单位与目标值一致分类任务要分两部分看——预测均值是类别概率的均值方差反映的是类别间的置信程度。代码里的呈现方式# 分类任务多轮 softmax 概率取均值和方差 probs [] for _ in range(30): output torch.softmax(model(x), dim1) probs.append(output) probs torch.stack(probs) mean_prob probs.mean(dim0) # [batch, num_classes] aleatoric probs.var(dim0) # 偶然不确定性逻辑说明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_weight0.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_samples5次梯度也要回传 5 次等于 5 倍计算量还叠加了 KL 散度的 autograd 开销。MCDropout 训练时不额外采样速度正常。 解决复现阶段把 batch size 从默认值调大一倍或者num_samples从 5 降到 2损失精度换速度。如果只是验证代码能跑通num_samples1都可以。注意不是所有层都需要贝叶斯——只把最后两层改成 BBB 层前面的卷积层保持确定性这是实际工程里最常用的压缩方案。6. 把不确定性用起来概率校准与决策阈值验证6.1 用概率校准指标验证不确定性质量不确定性数值算出来了怎么证明它不是模型瞎给的答案是概率校准当模型说一个样本有 80% 概率是正类时这类样本的真实正类比例应该接近 80%。实现一个校准曲线就是按预测概率分桶统计每个桶内的真实频率def expected_calibration_error(probs, labels, num_bins10): 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」这个流程再也没被「模型看起来会了」骗过。希望这些细节帮到你少踩一点我当年翻过的车。本文还有配套的精品资源点击获取