1. 一维热传导方程的后验不确定性量化
任务目标
构建一维热传导方程 \(u_t = \alpha u_{xx}\) 的贝叶斯PINN(B-PINN)模型。使用变分推断近似网络权重的后验分布,从而获得温度场预测的均值与方差,并通过与精确解对比评估不确定性校准质量。
问题设定与精确解
求解域:\(x \in [0, 1]\),\(t \in [0, 1]\),热扩散系数 \(\alpha = 0.4\)。边界与初始条件由精确解 \(u_{exact}(x,t) = e^{-t}\sin(\pi x)\) 导出(该函数满足方程与齐次边界条件)。训练数据从精确解中采样并添加高斯噪声(标准差 \(\sigma_n = 0.05\)),用于模拟带噪声的观测。
依赖与环境
需要:Python 3.9+,torch,matplotlib(可选,用于绘图)。安装命令:pip install torch matplotlib。本实现使用PyTorch的AutoGrad计算PINN残差,使用torch.distributions.Normal构建变分后验。
核心实现
以下代码完整实现了B-PINN:网络权重服从高斯变分分布,通过最小化负ELBO(均方误差损失 + KL散度)训练。训练后,用后验均值作为预测均值,后验方差传播得到预测方差。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.distributions import Normal
import numpy as np
torch.manual_seed(42)
np.random.seed(42)
# ---------- 精确解与数据生成 ----------
alpha = 0.4
def u_exact(x, t):
return torch.exp(-t) * torch.sin(np.pi * x)
# 训练点:边界 + 内部 + 初始条件
N_b = 40 # 边界点(每边)
N_i = 60 # 内部配点(用于残差)
N_u = 80 # 带噪声的观测点
# 观测数据(从精确解采样加噪声)
x_u = torch.rand(N_u, 1) * 0.8 + 0.1 # 避开边界附近
t_u = torch.rand(N_u, 1) * 0.9 + 0.05
u_obs = u_exact(x_u, t_u) + 0.05 * torch.randn(N_u, 1)
# 边界与初始配点(用于硬约束或软约束,这里用软约束)
# 左边界 x=0, 右边界 x=1
x_b = torch.cat([torch.zeros(N_b, 1), torch.ones(N_b, 1)])
t_b = torch.rand(2 * N_b, 1)
u_b = u_exact(x_b, t_b) # 边界精确值(已知为0,但这里用精确值做软约束)
# 初始条件 t=0
x_ic = torch.rand(N_i, 1)
t_ic = torch.zeros(N_i, 1)
u_ic = u_exact(x_ic, t_ic)
# 内部配点(用于PDE残差)
x_r = torch.rand(N_i, 1)
t_r = torch.rand(N_i, 1)
# ---------- 贝叶斯PINN网络 ----------
class BayesianPINN(nn.Module):
def __init__(self, n_input=2, n_hidden=50, n_output=1):
super().__init__()
self.n_hidden = n_hidden
# 变分参数:权重和偏置的均值与log标准差
self.fc1_mean = nn.Linear(n_input, n_hidden)
self.fc1_logstd = nn.Linear(n_input, n_hidden)
self.fc2_mean = nn.Linear(n_hidden, n_hidden)
self.fc2_logstd = nn.Linear(n_hidden, n_hidden)
self.out_mean = nn.Linear(n_hidden, n_output)
self.out_logstd = nn.Linear(n_hidden, n_output)
self._init_weights()
def _init_weights(self):
for m in [self.fc1_mean, self.fc2_mean, self.out_mean]:
nn.init.xavier_uniform_(m.weight)
nn.init.zeros_(m.bias)
for m in [self.fc1_logstd, self.fc2_logstd, self.out_logstd]:
nn.init.zeros_(m.weight)
nn.init.constant_(m.bias, -3) # 初始小标准差
def forward(self, x, t, n_samples=1):
"""返回预测均值与标准差(形状: [n_samples, batch, 1])"""
xt = torch.cat([x, t], dim=1)
means = []
stds = []
for _ in range(n_samples):
# 采样权重
w1 = self.fc1_mean.weight + torch.exp(self.fc1_logstd.weight) * torch.randn_like(self.fc1_mean.weight)
b1 = self.fc1_mean.bias + torch.exp(self.fc1_logstd.bias) * torch.randn_like(self.fc1_mean.bias)
w2 = self.fc2_mean.weight + torch.exp(self.fc2_logstd.weight) * torch.randn_like(self.fc2_mean.weight)
b2 = self.fc2_mean.bias + torch.exp(self.fc2_logstd.bias) * torch.randn_like(self.fc2_mean.bias)
wo = self.out_mean.weight + torch.exp(self.out_logstd.weight) * torch.randn_like(self.out_mean.weight)
bo = self.out_mean.bias + torch.exp(self.out_logstd.bias) * torch.randn_like(self.out_mean.bias)
h = torch.tanh(xt @ w1.T + b1)
h = torch.tanh(h @ w2.T + b2)
pred = h @ wo.T + bo
means.append(pred)
# 预测标准差(从输出层方差传播,简化处理)
# 这里用输出层权重的方差近似预测不确定性
out_var = torch.sum(torch.exp(2 * self.out_logstd.weight), dim=1) + torch.exp(2 * self.out_logstd.bias)
stds.append(torch.sqrt(out_var).expand_as(pred))
return torch.stack(means), torch.stack(stds)
# ---------- 损失函数(负ELBO) ----------
def kl_divergence(model):
"""计算变分后验与标准正态先验的KL散度(近似)"""
kl = 0
for layer_mean, layer_logstd in [(model.fc1_mean, model.fc1_logstd),
(model.fc2_mean, model.fc2_logstd),
(model.out_mean, model.out_logstd)]:
mu = layer_mean.weight
logstd = layer_logstd.weight
kl += torch.sum(logstd + 0.5 * (mu**2 + torch.exp(2*logstd)) - 0.5 - logstd)
# 偏置
mu_b = layer_mean.bias
logstd_b = layer_logstd.bias
kl += torch.sum(logstd_b + 0.5 * (mu_b**2 + torch.exp(2*logstd_b)) - 0.5 - logstd_b)
return kl
def pde_residual(model, x, t):
"""计算PINN残差 u_t - alpha*u_xx"""
x.requires_grad_(True)
t.requires_grad_(True)
# 使用后验均值网络(前向一次)
xt = torch.cat([x, t], dim=1)
h = torch.tanh(xt @ model.fc1_mean.weight.T + model.fc1_mean.bias)
h = torch.tanh(h @ model.fc2_mean.weight.T + model.fc2_mean.bias)
u = h @ model.out_mean.weight.T + model.out_mean.bias
u_t = torch.autograd.grad(u, t, grad_outputs=torch.ones_like(u), create_graph=True)[0]
u_x = torch.autograd.grad(u, x, grad_outputs=torch.ones_like(u), create_graph=True)[0]
u_xx = torch.autograd.grad(u_x, x, grad_outputs=torch.ones_like(u_x), create_graph=True)[0]
return u_t - alpha * u_xx
# ---------- 训练 ----------
model = BayesianPINN()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
n_epochs = 3000
kl_weight = 1e-4 # KL权重(需调参)
for epoch in range(n_epochs):
optimizer.zero_grad()
# 数据损失(观测点)
pred_mean, pred_std = model(x_u, t_u, n_samples=1)
data_loss = torch.mean((pred_mean[0] - u_obs)**2) / (2 * 0.05**2)
# 边界与初始条件损失
pred_b, _ = model(x_b, t_b, n_samples=1)
loss_b = torch.mean((pred_b[0] - u_b)**2)
pred_ic, _ = model(x_ic, t_ic, n_samples=1)
loss_ic = torch.mean((pred_ic[0] - u_ic)**2)
# PDE残差损失
res = pde_residual(model, x_r, t_r)
loss_pde = torch.mean(res**2)
# KL散度
kl = kl_divergence(model)
total_loss = data_loss + loss_b + loss_ic + loss_pde + kl_weight * kl
total_loss.backward()
optimizer.step()
if epoch % 500 == 0:
print(f'Epoch {epoch}, Loss: {total_loss.item():.4f}')
# ---------- 验证与不确定性校准 ----------
# 测试网格
x_test = torch.linspace(0.1, 0.9, 20).view(-1, 1)
t_test = torch.linspace(0.1, 0.9, 20).view(-1, 1)
X, T = torch.meshgrid(x_test.squeeze(), t_test.squeeze(), indexing='ij')
x_flat = X.reshape(-1, 1)
t_flat = T.reshape(-1, 1)
# 预测(多次采样估计均值和方差)
n_mc = 50
pred_means = []
pred_stds = []
with torch.no_grad():
for _ in range(n_mc):
mean, std = model(x_flat, t_flat, n_samples=1)
pred_means.append(mean[0])
pred_stds.append(std[0])
pred_mean = torch.stack(pred_means).mean(dim=0)
pred_std = torch.stack(pred_means).std(dim=0) # 认知不确定性
# 与精确解对比
u_true = u_exact(x_flat, t_flat)
error = (pred_mean - u_true).abs()
print(f'\n平均误差: {error.mean().item():.4f}')
print(f'预测标准差均值: {pred_std.mean().item():.4f}')
# 校准评估:计算Z-score(应接近标准正态)
z_score = (u_true - pred_mean) / (pred_std + 1e-6)
within_1sigma = (z_score.abs() < 1).float().mean().item()
within_2sigma = (z_score.abs() < 2).float().mean().item()
print(f'Z-score在1σ内比例: {within_1sigma:.3f} (理想≈0.683)')
print(f'Z-score在2σ内比例: {within_2sigma:.3f} (理想≈0.954)')
运行与验证
直接运行上述脚本即可完成训练与评估。训练约需1-2分钟(CPU)。判定标准:
- 训练损失持续下降并收敛;
- 平均误差(MAE)小于0.05(与噪声水平相当);
- Z-score在1σ内的比例接近0.683,在2σ内接近0.954,说明不确定性校准良好。
关键说明
本实现采用均值网络计算PDE残差(忽略权重不确定性对残差的影响),这是B-PINN的常见简化处理。预测标准差由输出层权重方差传播得到,反映了认知不确定性。若需更精确的不确定性传播,可对残差也进行多次采样,但会显著增加计算量。KL权重需根据数据噪声调整,本案例取1e-4。