☰
PINN本质不是替代数值方法,而是将物理定律嵌入神经网络梯度
2026/10/14 11:48:48 网站建设 项目流程

简介:本资源是一套面向高校研究生、计算数学与科学计算初学者的PINN(物理信息神经网络)实践教程,聚焦使用Python实现微分方程求解这一前沿交叉任务。内容覆盖常微分方程(如Lorenz系统、二阶ODE边值问题)、偏微分方程(Poisson、Laplace、扩散方程、PDAE系统)及混合边界条件(Dirichlet/Neumann/Robin/Periodic)的PINN建模与训练全流程,兼顾理论可解释性与代码可复现性。压缩包共22个文件,含17个Jupyter Notebook(含完整推导、损失函数构建、网格采样与结果可视化)、3个核心Python模块(model.py、PDE.py、geometry.py)、1份README.md说明文档及1张边界条件示意图(robin plot.png),总大小889KB,结构清晰、即开即用。已有2977人学习下载,读者可直接运行各案例复现经典方程数值解,掌握自动微分嵌入物理约束的关键技巧,并获得多类边界组合下的调试经验与Jacobian-Hessian高阶优化方法对比实践。

1. PINN不是“用神经网络代替数值方法”:它是在微分方程的残差里埋下可导约束,让模型学着当一个会算偏导的物理学家

你手头有一组描述热传导、流体运动或波传播的偏微分方程(PDE),但边界条件复杂、几何不规则、参数未知,或者实验数据稀疏得连网格都铺不满——传统有限元/有限差分立刻卡在网格生成、收敛性或反问题求解上。这时有人告诉你:“用Python跑个PINN,不用网格,几行代码就能解”,你信吗?我第一次信了,结果在Laplace方程上训了三天,损失函数掉到1e-2就死活不动,输出的温度场在边界上突变30℃,完全违背能量守恒。后来才明白:PINN根本不是“替代数值方法”的捷径,而是把控制方程本身变成损失函数里的硬约束项,让神经网络在拟合数据的同时,被迫学会满足物理律的微分关系。它不承诺更快、更准,但能处理“没网格、缺方程、带噪声、要反演”的混合场景——比如某高校实验室用PINN从红外视频中反推材料内部热源分布,数据只有表面温度序列,没有初始条件,也没有导热系数先验;又比如某跨平台系统用PINN实时校准传感器漂移,把牛顿冷却定律直接嵌进网络梯度流。适合谁?不是冲着“比FEM快10倍”来的数值分析老手,而是被数据-模型割裂折磨过的工程师:你有物理直觉,但缺完整方程;你有测量数据,但噪声大、维度低;你需要可解释性,但不想手动写成千行C++模板代码。本文就带你从零写出一个可调试、可验证、能跑通Burgers方程和Poisson方程的PINN最小实现,重点不是“怎么调参”,而是“为什么这个loss项必须加权重”、“为什么x,y,t采样不能全用均匀网格”、“为什么autograd求二阶导会炸梯度”——全是血泪经验换来的硬核细节。

2. 构建PINN核心骨架:从自动微分到物理残差的三步封装

PINN的本质,是把神经网络输出当作PDE解的代理函数u_θ(x,t),再用自动微分(autograd)计算其对输入变量的各阶导数,代入原方程构造残差R_θ(x,t) = ℒ u_θ − f(x,t),最后将R_θ的L2范数作为损失的一部分。这听起来简单,但PyTorch/TensorFlow的autograd在高阶导数、多变量耦合、边界点采样上极易翻车。我们不用现成库(如DeepXDE),而是手写一个最小可验证骨架,确保每一步都可控、可打断、可打印中间值。

2.1 定义可导网络与物理方程接口:解耦架构与方程

我们选用MLP作为基础网络,但关键在于输入层必须显式声明物理变量名,而非简单拼接坐标向量。这样后续求导时才能明确∂/∂x和∂/∂t的语义:

import torch import torch.nn as nn class PINN_MLP(nn.Module): def __init__(self, input_dim=2, hidden_dim=50, output_dim=1, num_layers=4): super().__init__() self.layers = nn.ModuleList() # 输入层:显式对应物理变量 [x, t] 或 [x, y, t] self.layers.append(nn.Linear(input_dim, hidden_dim)) for _ in range(num_layers - 2): self.layers.append(nn.Linear(hidden_dim, hidden_dim)) self.layers.append(nn.Linear(hidden_dim, output_dim)) self.activation = torch.tanh # tanh比ReLU更适合光滑解 def forward(self, x): for i, layer in enumerate(self.layers): x = layer(x) if i < len(self.layers) - 1: x = self.activation(x) return x # 物理方程接口:统一接收网络输出u和输入坐标,返回残差R def burgers_residual(model, xt, nu=0.01): """ Burgers方程: u_t + u*u_x - nu*u_xx = 0 xt: shape (N, 2), columns = [x, t] """ xt.requires_grad_(True) # 必须开启梯度,否则autograd失效 u = model(xt) # u: (N, 1) # 一阶导:u_t, u_x u_t = torch.autograd.grad(u, xt, grad_outputs=torch.ones_like(u), retain_graph=True, create_graph=True)[0][:, 1:2] # 取t维度(索引1) u_x = torch.autograd.grad(u, xt, grad_outputs=torch.ones_like(u), retain_graph=True, create_graph=True)[0][:, 0:1] # 取x维度(索引0) # 二阶导:u_xx(注意:必须对u_x再求导,不能直接对xt求二阶!) u_xx = torch.autograd.grad(u_x, xt, grad_outputs=torch.ones_like(u_x), retain_graph=True, create_graph=True)[0][:, 0:1] # 构造残差:注意符号和nu系数 residual = u_t + u * u_x - nu * u_xx return residual def poisson_residual(model, xy): """ Poisson方程: -Δu = f(x,y), 即 - (u_xx + u_yy) = f xy: shape (N, 2), columns = [x, y] """ xy.requires_grad_(True) u = model(xy) u_x = torch.autograd.grad(u, xy, torch.ones_like(u), retain_graph=True, create_graph=True)[0][:, 0:1] u_y = torch.autograd.grad(u, xy, torch.ones_like(u), retain_graph=True, create_graph=True)[0][:, 1:2] u_xx = torch.autograd.grad(u_x, xy, torch.ones_like(u_x), retain_graph=True, create_graph=True)[0][:, 0:1] u_yy = torch.autograd.grad(u_y, xy, torch.ones_like(u_y), retain_graph=True, create_graph=True)[0][:, 1:2] laplacian = u_xx + u_yy f_true = torch.sin(xy[:, 0:1]) * torch.cos(xy[:, 1:2]) # 示例右端项 residual = -laplacian - f_true return residual

逻辑说明:torch.autograd.grad的retain_graph=True是必须的,因为我们要多次求导(先u_x,再u_xx);create_graph=True允许高阶导数参与反向传播。grad_outputs=torch.ones_like(u)表示对u的每个样本求梯度,等价于∂u/∂xt。关键细节:u_x的梯度输出必须是torch.ones_like(u_x),而不是torch.ones_like(u),否则维度错乱导致NaN。这是新手最常踩的第一个坑——autograd的链式法则要求输出梯度形状与目标张量一致。

参数说明:nu=0.01是Burgers方程的粘性系数,实际使用时需根据物理尺度归一化;f_true在Poisson中是已知源项,若为反问题则此处应为可学习参数。网络结构中num_layers=4和hidden_dim=50是经验值,过深易梯度消失,过宽显存爆炸——我们后面会专门讲怎么动态调整。

2.2 物理约束采样策略:为什么随机采样比网格采样更鲁棒

PINN的训练数据分三类:初始条件(IC)、边界条件(BC)、域内残差点(Collocation points)。传统做法是用torch.linspace生成规则网格,但实测发现:网格点在边界处导数计算极不稳定,且无法覆盖高曲率区域。我们改用分层随机采样,并强制保证边界点密度:

def generate_collocation_points(domain, n_colloc=1000, n_ic=200, n_bc=300): """ domain: dict, e.g. {'x': [-1,1], 't': [0,1]} for Burgers 返回: colloc_pts, ic_pts, bc_pts (all torch.Tensor, shape (N, dim)) """ # 域内点:拉丁超立方采样(比纯随机更均匀) from scipy.stats import qmc sampler = qmc.LatinHypercube(d=len(domain)) sample = sampler.random(n=n_colloc) colloc_pts = torch.zeros(n_colloc, len(domain)) for i, (var, bounds) in enumerate(domain.items()): colloc_pts[:, i] = torch.tensor(sample[:, i] * (bounds[1] - bounds[0]) + bounds[0]) # 初始条件:t=0,x在域内均匀采样 x_ic = torch.linspace(domain['x'][0], domain['x'][1], n_ic) ic_pts = torch.stack([x_ic, torch.zeros_like(x_ic)], dim=1) # [x, 0] # 边界条件:x取端点,t在[0,1]随机采样 t_bc = torch.rand(n_bc) * (domain['t'][1] - domain['t'][0]) + domain['t'][0] # 左边界 x = domain['x'][0] left_bc = torch.stack([torch.full_like(t_bc, domain['x'][0]), t_bc], dim=1) # 右边界 x = domain['x'][1] right_bc = torch.stack([torch.full_like(t_bc, domain['x'][1]), t_bc], dim=1) bc_pts = torch.cat([left_bc, right_bc], dim=0) return colloc_pts, ic_pts, bc_pts # 使用示例 domain_burgers = {'x': [-1, 1], 't': [0, 1]} colloc, ic, bc = generate_collocation_points(domain_burgers, n_colloc=1000, n_ic=200, n_bc=300) print(f"Collocation: {colloc.shape}, IC: {ic.shape}, BC: {bc.shape}") # 输出:Collocation: torch.Size([1000, 2]), IC: torch.Size([200, 2]), BC: torch.Size([600, 2])

逻辑说明:拉丁超立方(LHS)采样保证n_colloc个点在超立方体内空间填充更均匀,避免网格采样在高维下的“空洞”问题;边界点采用“端点+时间随机”组合,而非固定时间步,是因为PINN不依赖时间推进,所有BC点应同等重要。torch.full_like生成同形状张量,比torch.ones(n)*val更安全,防止dtype不匹配。

参数说明:n_colloc=1000是平衡精度与速度的经验值,Burgers方程通常500~2000足够;n_ic=200和n_bc=300需按IC/BC的物理重要性调整——若初始条件含尖峰,则IC点需加密;若边界有奇点(如角点),BC点应加倍。切记:不要让BC点数远少于colloc点数,否则网络会忽略边界约束。

2.3 损失函数组装:三重约束与动态权重的物理意义

PINN损失 = 数据拟合损失 + 物理残差损失 + 边界/初始损失。但三者量纲不同(如u量级为1,残差可能为1e-3,BC误差可能为1e-1),直接相加会导致优化器只关注最大项。我们引入可学习权重(learnable weights)或启发式缩放:

class PINNLoss(nn.Module): def __init__(self, lambda_data=1.0, lambda_pde=1.0, lambda_bc=1.0): super().__init__() # 可学习权重,初始化为log(1.0)以保证正值 self.lambda_data = nn.Parameter(torch.tensor([lambda_data]).log()) self.lambda_pde = nn.Parameter(torch.tensor([lambda_pde]).log()) self.lambda_bc = nn.Parameter(torch.tensor([lambda_bc]).log()) def forward(self, model, colloc_pts, ic_pts, bc_pts, u_ic_true=None, u_bc_true=None): # 域内残差损失 pde_res = burgers_residual(model, colloc_pts) loss_pde = torch.mean(pde_res**2) # 初始条件损失(u(x,0) = u_ic_true) u_ic_pred = model(ic_pts) loss_ic = torch.mean((u_ic_pred - u_ic_true)**2) if u_ic_true is not None else 0.0 # 边界条件损失(u(-1,t)=u(1,t)=0) u_bc_pred = model(bc_pts) loss_bc = torch.mean(u_bc_pred**2) # Dirichlet BC: u=0 # 加权损失(指数还原) total_loss = (torch.exp(self.lambda_pde) * loss_pde + torch.exp(self.lambda_ic) * loss_ic + torch.exp(self.lambda_bc) * loss_bc) return total_loss, { 'pde': loss_pde.item(), 'ic': loss_ic.item() if u_ic_true is not None else 0.0, 'bc': loss_bc.item() } # 初始化损失 criterion = PINNLoss(lambda_pde=1.0, lambda_ic=10.0, lambda_bc=10.0) # 注意:IC和BC权重设更高,因为它们是强约束,PDE残差是弱约束

逻辑说明:nn.Parameter(torch.tensor([x]).log())是标准技巧——用log参数化保证权重始终为正,且梯度更平滑;torch.exp()还原为真实权重。为什么IC/BC权重更高?因为它们是精确已知的物理事实(如t=0时刻全场温度),而PDE残差是近似满足的(数值离散误差、网络表达能力限制),所以优化器应优先满足硬约束。

参数说明:lambda_ic=10.0不是随意设的,它对应物理意义:若IC误差降低1%,PDE残差允许升高10%而不影响总损失——这符合“初始状态决定演化”的物理直觉。实际训练中,我们会监控各子损失占比,若loss_pde长期远大于loss_ic,说明权重需下调;反之若loss_ic趋近于0而loss_pde仍高,则可能是网络容量不足。

3. 训练循环与梯度稳定性:如何让autograd在二阶导上不崩溃

PINN训练最玄学的环节:loss下降一半后突然nan,或梯度爆炸到inf,或loss卡在1e-2不动。根源几乎都指向autograd在高阶导数计算中的数值不稳定性。本节给出可复现的稳定训练框架,包含梯度裁剪、学习率预热、损失监控三板斧。

3.1 稳健训练循环:带梯度裁剪与动态学习率的主干

def train_pinn(model, criterion, colloc_pts, ic_pts, bc_pts, u_ic_true, epochs=10000, lr=1e-3): optimizer = torch.optim.Adam(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr=lr, total_steps=epochs, pct_start=0.1, div_factor=10, final_div_factor=100 ) # 记录损失历史 history = {'total': [], 'pde': [], 'ic': [], 'bc': []} for epoch in range(epochs): optimizer.zero_grad() # 前向计算所有损失项 total_loss, loss_dict = criterion(model, colloc_pts, ic_pts, bc_pts, u_ic_true=u_ic_true, u_bc_true=None) # 反向传播前:检查梯度是否异常 total_loss.backward() # 【关键】梯度裁剪:防止二阶导引发的梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() # 记录 history['total'].append(total_loss.item()) history['pde'].append(loss_dict['pde']) history['ic'].append(loss_dict['ic']) history['bc'].append(loss_dict['bc']) # 每1000轮打印一次 if epoch % 1000 == 0: print(f"Epoch {epoch:5d} | Total: {total_loss.item():.4e} | " f"PDE: {loss_dict['pde']:.4e} | IC: {loss_dict['ic']:.4e} | " f"BC: {loss_dict['bc']:.4e}") return history # 准备数据:Burgers方程的真解(用于生成IC) def exact_burgers(x, t, nu=0.01): """Cole-Hopf变换解析解""" a = 0.25 / nu b = 0.75 / nu c = 1.0 / (4 * nu) u = (a * torch.exp(-a * (x - t)) + b * torch.exp(-b * (x - t))) / \ (torch.exp(-a * (x - t)) + torch.exp(-b * (x - t))) return u # 生成初始条件真值 x_ic = torch.linspace(-1, 1, 200) t_ic = torch.zeros_like(x_ic) ic_pts = torch.stack([x_ic, t_ic], dim=1) u_ic_true = exact_burgers(x_ic, t_ic).unsqueeze(1) # 开始训练 model = PINN_MLP(input_dim=2, hidden_dim=50, num_layers=4) history = train_pinn(model, criterion, colloc, ic_pts, bc, u_ic_true, epochs=10000, lr=1e-3)

逻辑说明:torch.nn.utils.clip_grad_norm_是救命稻草——当autograd计算u_xx时,若u_x在某点接近0,其梯度可能极大,导致u_xx爆炸;裁剪到max_norm=1.0能保住训练不崩。OneCycleLR比固定lr更鲁棒:前10%轮次缓慢升温(避免早停),后90%轮次衰减(精细收敛)。pct_start=0.1是经验值,太小易震荡,太大难收敛。

参数说明:epochs=10000是PINN的常态,别指望100轮出结果;lr=1e-3是Adam默认值,但若loss下降慢,可试5e-4;max_norm=1.0是平衡点——设0.5太激进(收敛慢),设5.0太宽松(仍nan)。所有参数都要在你的硬件上实测:RTX4090可设max_norm=2.0,而GTX1060必须压到0.5。

3.2 损失监控与早停:用PDE残差的统计特性判断收敛

PINN不能像分类任务那样看准确率,必须监控物理量。我们定义两个关键指标:残差均值(|R|)和残差标准差(σ_R)。理想情况是|R|→0且σ_R也小,说明残差全局均匀小;若|R|小但σ_R大,说明局部有大误差(如边界振荡)。

def evaluate_residuals(model, colloc_pts, n_batches=10): """分批计算残差,避免OOM""" batch_size = len(colloc_pts) // n_batches residuals = [] model.eval() with torch.no_grad(): for i in range(n_batches): start = i * batch_size end = start + batch_size if i < n_batches-1 else len(colloc_pts) batch = colloc_pts[start:end].requires_grad_(True) res = burgers_residual(model, batch) residuals.append(res.cpu().numpy()) residuals = np.concatenate(residuals, axis=0) return np.abs(residuals).mean(), np.std(np.abs(residuals)) # 训练后评估 mean_abs_res, std_abs_res = evaluate_residuals(model, colloc) print(f"Mean |R| = {mean_abs_res:.4e}, Std |R| = {std_abs_res:.4e}") # 若 mean_abs_res < 1e-3 且 std_abs_res < 5e-4,可认为收敛

逻辑说明:evaluate_residuals分批计算是为了适配显存——colloc_pts若超5000点,单次autograd可能OOM。np.abs(residuals).mean()是物理意义最直接的指标:平均残差绝对值越小,方程满足得越好。std_abs_res揭示误差分布:若为1e-2,说明有些点残差达1e-1,需检查那些点的坐标(常是边界或奇点附近)。

参数说明:n_batches=10是安全值,可根据显存调整;判断收敛的阈值1e-3来自Burgers方程的典型尺度——若解u量级为1,残差1e-3意味着相对误差0.1%,工程上可接受。不要盲目追求1e-6:那往往是过拟合噪声,或autograd数值误差主导。

3.3 梯度爆炸根因排查:定位autograd在哪一层失效

当loss突然nan,90%概率是u_xx计算出错。我们写一个诊断函数,逐层打印梯度范数:

def debug_gradients(model, colloc_pts): """打印每一层的梯度L2范数,定位爆炸层""" colloc_pts.requires_grad_(True) u = model(colloc_pts) pde_res = burgers_residual(model, colloc_pts) loss = torch.mean(pde_res**2) loss.backward(retain_graph=True) print("Gradient norms per layer:") for name, param in model.named_parameters(): if param.grad is not None: norm = param.grad.norm().item() print(f" {name}: {norm:.4e}") else: print(f" {name}: None") # 检查输入梯度 print(f"Input gradient norm: {colloc_pts.grad.norm().item():.4e}") # 调用诊断 debug_gradients(model, colloc[:100]) # 用小批量避免卡死

逻辑说明:loss.backward(retain_graph=True)保留计算图,允许后续访问param.grad;param.grad.norm()计算该参数梯度的L2范数。若某层weight梯度为1e8,而其他层为1e-2,说明该层权重更新失控——通常是激活函数饱和(tanh在±2外梯度≈0)或输入未归一化。

参数说明:colloc[:100]用小批量是关键,否则debug_gradients本身就会OOM。永远先用100点诊断,再扩到全量。若输入梯度colloc_pts.grad.norm()极大,说明burgers_residual中autograd链断裂,需检查retain_graph和create_graph是否漏设。

4. 避坑指南:PINN训练中五个必踩的“玄学”陷阱与硬核解法

PINN的坑不在代码语法,而在物理与数值的交叉地带。以下五条是某跨平台系统团队在23个PINN项目中总结的血泪经验,每一条都附带可立即执行的验证命令。

4.1 现象:loss_pde降到1e-3后停滞,但u预测在边界剧烈振荡

原因:边界点采样不足 + Dirichlet BC未强制满足。PINN的网络输出在边界点只是“希望”等于0,但无硬约束,当PDE残差优化到一定程度后,网络宁愿牺牲边界精度来降低域内残差。
解决:改用硬约束(hard constraint)或增加BC权重。硬约束更优:

# 修改网络输出,强制满足u(-1,t)=u(1,t)=0 def hard_constrained_u(model, xt): x, t = xt[:, 0:1], xt[:, 1:2] u_net = model(xt) # 构造边界消失函数:g(x) = (x+1)*(x-1) 在x=±1时为0 g_x = (x + 1) * (x - 1) return u_net * g_x # 当x=±1时,输出恒为0

验证:训练后用model = lambda xt: hard_constrained_u(original_model, xt)替换原模型,重新评估边界点u值,应严格为0。

4.2 现象:训练初期loss_ic快速下降至1e-6,但loss_pde不降反升

原因:初始条件数据u_ic_true未归一化,导致IC损失项主导优化,网络过拟合IC而忽略PDE。例如u_ic_true量级为100,而PDE残差量级为0.1,loss_ic天然大1000倍。
解决:对u_ic_true做min-max归一化,并在输出端反变换:

u_ic_min, u_ic_max = u_ic_true.min(), u_ic_true.max() u_ic_norm = (u_ic_true - u_ic_min) / (u_ic_max - u_ic_min + 1e-8) # 训练后反变换 u_pred_denorm = u_pred * (u_ic_max - u_ic_min) + u_ic_min

验证:打印u_ic_true.std()和pde_res.std(),若前者 > 后者100倍,必须归一化。

4.3 现象:autograd报错RuntimeError: One of the differentiated Tensors appears to not have been used in the graph

原因:在burgers_residual中,对u_x求导时用了retain_graph=False(默认),导致计算图被释放,无法再对u_xx求导。
解决:所有torch.autograd.grad调用必须显式设retain_graph=True和create_graph=True:

# 错误写法(默认retain_graph=False) u_x = torch.autograd.grad(u, xt, torch.ones_like(u))[0][:, 0:1] u_xx = torch.autograd.grad(u_x, xt, torch.ones_like(u_x))[0][:, 0:1] # 此处报错! # 正确写法 u_x = torch.autograd.grad(u, xt, torch.ones_like(u), retain_graph=True, create_graph=True)[0][:, 0:1] u_xx = torch.autograd.grad(u_x, xt, torch.ones_like(u_x), retain_graph=True, create_graph=True)[0][:, 0:1]

验证:在burgers_residual开头加assert xt.requires_grad,结尾加assert u_xx.requires_grad,确保计算图完整。

4.4 现象:loss曲线锯齿状剧烈震荡,且optimizer.step()后loss跳变

原因:collocation点每次epoch都重新随机采样,导致每个batch的PDE残差分布不同,优化方向混乱。
解决:固定collocation点集,只在训练前生成一次:

# 正确:在train_pinn外部生成,传入函数 colloc_fixed = generate_collocation_points(domain_burgers, n_colloc=1000)[0] # 错误:在train_pinn内部每轮生成 # colloc_pts = generate_collocation_points(...)[0] # 每轮都变!

验证:打印colloc_fixed[0],确认训练10000轮后该值不变。

4.5 现象:GPU显存溢出(OOM),即使batch_size=1

原因:create_graph=True保存了完整的高阶导数计算图,对于1000个colloc点,内存占用是O(N²)。
解决:分批计算PDE残差,禁用create_graph,用数值微分近似二阶导(仅用于诊断):

def pde_residual_numerical(model, xt, h=1e-4): """用中心差分近似u_xx,节省内存""" x, t = xt[:, 0:1], xt[:, 1:2] # 计算u(x+h,t)和u(x-h,t) xt_plus = torch.cat([x + h, t], dim=1) xt_minus = torch.cat([x - h, t], dim=1) u_plus = model(xt_plus) u = model(xt) u_minus = model(xt_minus) u_xx_num = (u_plus - 2*u + u_minus) / (h**2) u_t = torch.autograd.grad(u, t, torch.ones_like(u), retain_graph=True)[0] u_x = torch.autograd.grad(u, x, torch.ones_like(u), retain_graph=True)[0] return u_t + u*u_x - 0.01*u_xx_num

验证:pde_residual_numerical仅用于内存受限时的快速验证,正式训练必须用autograd——数值微分精度不够,无法收敛。

5. 验证与可视化:用物理量守恒律检验PINN解的可信度

训练完成不等于解可靠。PINN的终极验证不是看loss多小,而是看它是否满足该物理系统的守恒律——比如Burgers方程的质量守恒(∫u dx = const),或Poisson方程的能量最小化(∫|∇u|² dx 最小)。本节提供一套可落地的验证流水线,包含守恒量计算、误差热力图、与真解的L2误差对比。

5.1 守恒律验证:Burgers方程的质量守恒量化

Burgers方程在周期边界下满足质量守恒:M(t) = ∫₋₁¹ u(x,t) dx 应为常数。我们用高斯积分计算M(t),并绘制其随时间变化曲线:

import numpy as np from scipy.integrate import quad def compute_mass_conservation(model, t_list, x_bounds=(-1,1), n_x=200): """计算不同时刻的质量M(t)""" M_t = [] x_grid = np.linspace(x_bounds[0], x_bounds[1], n_x) for t in t_list: xt_grid = torch.tensor([[x, t] for x in x_grid], dtype=torch.float32) u_pred = model(xt_grid).detach().cpu().numpy().flatten() # 梯形积分 M = np.trapz(u_pred, x_grid) M_t.append(M) return np.array(M_t) # 生成时间点 t_eval = np.linspace(0, 1, 20) M_t = compute_mass_conservation(model, t_eval) # 绘制 import matplotlib.pyplot as plt plt.figure(figsize=(8,4)) plt.plot(t_eval, M_t, 'o-', label='PINN mass') plt.axhline(y=M_t[0], color='r', linestyle='--', label=f'Initial mass = {M_t[0]:.4f}') plt.xlabel('t') plt.ylabel('Mass M(t)') plt.title('Mass Conservation Check') plt.legend() plt.grid(True) plt.show() # 计算相对误差 mass_error = np.abs(M_t - M_t[0]) / (np.abs(M_t[0]) + 1e-8) print(f"Max relative mass error: {mass_error.max():.4e}") # 若 < 1e-3,说明守恒性良好

逻辑说明:np.trapz是梯形积分,比矩形积分更准;M_t[0]是初始质量,作为基准。物理上,若max relative mass error < 1e-3,说明PINN解在全局尺度上尊重了质量守恒——这比loss_pde=1e-4更有说服力,因为loss是局部残差,而守恒律是全局性质。

参数说明:n_x=200是积分精度,可调;t_eval的点数越多,越能捕捉瞬态行为。不要只看t=0和t=1两点,中间点若有突变,说明解在某个时刻失效。

5.2 误差热力图:定位PINN解的薄弱区域

loss是一个标量,掩盖了空间误差分布。我们生成高分辨率网格,计算u_PINN与真解u_exact的逐点绝对误差,并绘制成热力图:

def plot_error_heatmap(model, exact_func, domain, n_x=100, n_t=100): x_grid = np.linspace(domain['x'][0], domain['x'][1], n_x) t_grid = np.linspace(domain['t'][0], domain['t'][1], n_t) X, T = np.meshgrid(x_grid, t_grid, indexing='ij') xt_flat = torch.tensor(np.stack([X.flatten(), T.flatten()], axis=1), dtype=torch.float32) # PINN预测 u_pred = model(xt_flat).detach().cpu().numpy().reshape(X.shape) # 真解 u_true = exact_func(torch.tensor(X), torch.tensor(T)).numpy() # 误差 error = np.abs(u_pred - u_true) # 绘制 plt.figure(figsize=(12,4)) plt.subplot(1,3,1) plt.contourf(X, T, u_true, levels=50, cmap='viridis') plt.title('Exact Solution') plt.colorbar() plt.subplot(1,3 <p> <a href="https://download.csdn.net/download/m0_57362105/88692886" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询