简介:本资源是一套面向高校研究生、计算数学与科学计算初学者的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不是黑匣子:用Python把微分方程“编译”进神经网络权重里,解得准、可解释、不依赖网格
你有没有试过用传统数值方法解一个带强非线性边界、多尺度耦合、甚至部分物理约束缺失的PDE?网格加密到内存爆掉,迭代十几次还发散,最后发现初始猜测差0.1%就让整个解崩成噪声——这太常见了。而这份基于PINN物理信息网络求解微分方程的方法(python),不是又一个调参玩具,它把控制方程、边界条件、初始条件直接作为损失函数的硬约束项,嵌进神经网络训练过程。这意味着:你不再需要先离散再求解,而是让网络在训练中“自发学会满足物理律”。它能跑通从一维ODE(如y′−y−y=2x)到二维Laplace方程在圆域上的解析解逼近,再到含Robin边界的Euler梁弯曲问题;所有案例都用纯PyTorch或DeepXDE实现,不依赖商业软件,代码即文档。适合正在做CFD后处理、结构响应建模、传热反演或教学演示的工程师与研究生——尤其当你手头只有少量测量点、没有完整初边值、或想绕过网格生成这个玄学环节时,PINN不是替代,而是给你多一条路。
2. 为什么选PINN而不是FEM/FDM?从控制方程到损失函数的三步“物理编码”
PINN的核心不是换了个模型,而是换了一种建模范式:把物理定律从“求解对象”变成“训练指南”。要真正复现这份资源里的15+个Notebook,必须先理解它如何把数学表达式翻译成可微分的计算图。下面以最典型的Poisson方程Dirichlet问题为例,拆解其物理编码逻辑。
2.1 控制方程 → 残差函数:把Δu = f(x)写成自动微分友好的形式
在Poisson_equation_Dirichlet.ipynb中,作者没有直接构造u(x,y)的显式解,而是定义了一个神经网络model(x, y)输出预测解û。关键一步是构建残差函数:
import torch import torch.nn as nn def residual(model, x, y): u = model(torch.cat([x, y], dim=1)) # [N, 1] u_x = torch.autograd.grad(u, x, grad_outputs=torch.ones_like(u), retain_graph=True, create_graph=True)[0] u_y = torch.autograd.grad(u, y, grad_outputs=torch.ones_like(u), retain_graph=True, create_graph=True)[0] u_xx = torch.autograd.grad(u_x, x, grad_outputs=torch.ones_like(u_x), retain_graph=True, create_graph=True)[0] u_yy = torch.autograd.grad(u_y, y, grad_outputs=torch.ones_like(u_y), retain_graph=True, create_graph=True)[0] f = 2.0 # 对应 Δu = 2 的源项 return u_xx + u_yy - f注意:这里没用任何符号计算库,全靠PyTorch的
torch.autograd.grad逐阶求导。create_graph=True确保高阶导数仍保留在计算图中,这是PINN能反向传播到PDE残差的关键。retain_graph=True防止多次求导时图被释放——漏掉任一flag都会导致RuntimeError: Trying to backward through the graph a second time。
该函数返回的是每个采样点上的PDE残差值,维度为[N, 1]。训练目标就是让这个残差在全部内部点上趋近于零。
2.2 边界条件 → 约束损失项:Dirichlet、Neumann、Robin统一为加权惩罚项
边界不是单独处理的模块,而是损失函数的一部分。在Poisson_equation_Dirichlet_Robin.ipynb中,你将看到三种边界混合出现:
- Dirichlet:
u(-1) = 0→loss_bc_dir = torch.mean((model(x_bdry_d) - 0.0)**2) - Neumann:
∂u/∂n = g(x)→ 需先算出法向导数,再构造loss_bc_neu = torch.mean((nabla_u @ n_vec - g)**2) - Robin:
αu + β∂u/∂n = h→ 直接代入loss_bc_rob = torch.mean((alpha * u_bdry_r + beta * nabla_u_r @ n_vec - h)**2)
这些损失项不会被归一化到同一量级,而是通过超参数lambda_dir,lambda_neu,lambda_rob加权:
loss = loss_pde + lambda_dir * loss_bc_dir + lambda_neu * loss_bc_neu + lambda_rob * loss_bc_rob提示:
lambda_*不是随便设的。实践中,若PDE残差量级是1e3,而Dirichlet误差是1e-2,不加权重会导致网络完全忽略边界。我一般先跑100步预热,统计各loss项的均值和标准差,再设lambda = median(loss_pde) / median(loss_bc)作初始值,后续再微调。
2.3 域采样策略 → 决定泛化能力的隐性超参:均匀、伪随机、边界强化三类采样器
PINN的精度不只取决于网络结构,更取决于“在哪算残差”。资源包中的data_domain.ipynb专门对比了三种采样方式:
| 采样类型 | 实现方式 | 适用场景 | 典型缺陷 |
|---|---|---|---|
| 均匀网格采样 | torch.linspace(-1,1,100)叉乘 | 初步验证、教学演示 | 边界附近点稀疏,Dirichlet约束弱 |
| Sobol伪随机序列 | from scipy.stats import qmc; sampler = qmc.Sobol(d=2) | 高维PDE、避免周期性伪影 | 需手动剔除重复点,否则梯度爆炸 |
| 边界强化采样 | 内部点80% + 边界点20%(且边界点按曲率加权) | 所有含强边界效应的问题(如Euler Beam) | 若未同步更新边界点集,训练中会漏掉新暴露的角点 |
在Euler Beam.ipynb中,作者对梁两端施加固定约束(u=0, u′=0),但仅靠端点几个点无法稳定训练。解决方案是在每轮迭代中动态生成一批靠近x=±1的点(如x_bdry = torch.cat([torch.rand(50,1)*0.05-1, torch.rand(50,1)*0.05+1])),并赋予更高权重。这不是hack,而是对物理本质的尊重:约束最强的地方,采样密度必须最高。
3. 从DeepXDE到纯PyTorch:两个技术栈的落地差异与选型依据
这份资源同时包含DeepXDE封装版(如DeepXDE Poisson_equation_Dirichlet.ipynb)和纯PyTorch手写版(如Δu = 2, x∈[-1,1], u(-1)=0, u_(1)=4.ipynb)。二者不是简单重复,而是面向不同阶段的工程需求。选错栈,轻则调试三天找不到梯度断点,重则误读物理含义。
3.1 DeepXDE:开箱即用的物理建模DSL,适合快速验证与教学
DeepXDE把PINN抽象成“几何定义→PDE描述→边界设定→训练配置”四步流程。以Laplace方程在单位圆盘上的求解为例(Laplace_equation_on_a_disk.ipynb):
import deepxde as dde import numpy as np # 1. 定义几何:单位圆盘 geom = dde.geometry.Disk([0, 0], 1) # 2. 定义PDE:Δu = 0 def pde(x, u): u_xx = dde.grad.hessian(u, x, i=0, j=0) u_yy = dde.grad.hessian(u, x, i=1, j=1) return u_xx + u_yy # 3. 定义边界条件:u = x^2 on boundary bc = dde.DirichletBC(geom, lambda x: x[:, 0:1]**2, lambda _, on_boundary: on_boundary) # 4. 构建数据集与训练 data = dde.data.PDE(geom, pde, bc, num_domain=2000, num_boundary=400) net = dde.maps.FNN([2] + [50]*4 + [1], "tanh", "Glorot normal") model = dde.Model(data, net) model.compile("adam", lr=1e-3) losshistory, train_state = model.train(iterations=20000)逻辑说明:DeepXDE隐藏了自动微分细节,
dde.grad.hessian自动处理二阶导;num_domain和num_boundary直接控制采样密度;compile自动选择优化器并绑定损失权重。它极大降低入门门槛,特别适合课程设计——学生只需改几行就能跑通Laplace、Poisson、Burgers方程。
但代价是:当你要修改残差定义(比如加入不确定度建模)、自定义采样分布(如按应力集中度采样)、或调试某一层梯度时,DeepXDE的封装层反而成了障碍。
3.2 纯PyTorch手写栈:掌控每一处梯度流,适合科研深挖与工业定制
所有以*.ipynb结尾但不含DeepXDE前缀的文件,都是纯PyTorch实现。以y′=sin(πx)cos(πx), x∈[-1,1], y(-1)=0.ipynb为例,其核心在于手动管理计算图生命周期:
def train_step(model, optimizer, x_int, x_bdry, y_bdry): optimizer.zero_grad() # 内部点:计算ODE残差 y_pred = model(x_int) dy_dx = torch.autograd.grad(y_pred, x_int, grad_outputs=torch.ones_like(y_pred), retain_graph=True, create_graph=True)[0] ode_residual = dy_dx - torch.sin(np.pi * x_int) * torch.cos(np.pi * x_int) # 边界点:Dirichlet损失 y_bdry_pred = model(x_bdry) bc_loss = torch.mean((y_bdry_pred - y_bdry)**2) # 总损失:PDE残差L2 + 边界损失 pde_loss = torch.mean(ode_residual**2) loss = pde_loss + 10.0 * bc_loss # 边界权重显式指定 loss.backward() # 此处梯度流完全透明 optimizer.step() return loss.item(), pde_loss.item(), bc_loss.item()参数说明:
retain_graph=True保证dy_dx计算后,x_int的计算图仍存在,以便后续对ode_residual再次求导(若需Hessian);create_graph=True使dy_dx本身也构成可微分子图——这是实现Jacobian-Hessian混合方法(见Method Testing Jacobian-Hessian methods for Laplace equation.ipynb)的基础。DeepXDE默认不开启此模式,除非显式设置jacobian=True。
这种写法让你能:
- 在
loss.backward()后插入print(model.layer1.weight.grad.norm())监控各层梯度爆炸; - 对特定残差项添加Huber Loss替代L2,抑制异常点干扰;
- 将
x_int替换为从真实传感器数据采样的点集,实现数据-物理混合驱动。
3.3 何时切换?一个血泪经验:从DeepXDE起步,但第3次失败就切PyTorch
我带过的某高校课题组曾用DeepXDE跑Lorenz系统(Lorenz system.ipynb),前两次训练loss卡在1e-2不动。他们检查了网络深度、学习率、采样数,都没问题。直到第三次,他们切到纯PyTorch版,加了梯度打印:
for name, param in model.named_parameters(): if param.grad is not None: print(f"{name}: {param.grad.norm():.4f}")发现layer3.weight梯度恒为0——原因是激活函数用了sigmoid,在输入>5时梯度饱和。DeepXDE默认tanh,但他们复制代码时手误写成sigmoid,而DeepXDE的报错信息只显示loss not decreasing,不指明梯度死区。这就是封装的双刃剑:省事,但也藏雷。
所以我的建议是:第一次跑通用DeepXDE;第二次调参用DeepXDE;第三次还没收敛,立刻切PyTorch,用梯度监控定位。
4. 避坑:15个Notebook踩过的7类典型问题与现场急救方案
PINN训练不像CNN那样“调好lr基本稳了”,它的失败往往静默而顽固:loss曲线平滑下降,但解在物理上完全错误。这份资源的15个Notebook覆盖了绝大多数翻车场景。以下是我在复现过程中记录的真实避坑清单,每条都按“现象→原因→解决”结构整理,拒绝模糊描述。
4.1 现象:PDE残差loss降到1e-5,但预测解在边界剧烈震荡
原因:边界点采样不足 + Dirichlet权重过低。例如在u(-1)=0, u(1)=4问题中,仅用2个端点,而lambda_dir=1,导致网络优先拟合内部PDE,牺牲边界精度。
解决:将边界点数从2增至50,并设lambda_dir = 100。更鲁棒的做法是使用BoundaryLossScheduler——每100步将lambda_dir乘以1.05,让边界约束渐进增强。
4.2 现象:训练初期loss突降至0,随后发散
原因:torch.autograd.grad中遗漏retain_graph=True,导致第二次求导时计算图已被释放,梯度变为None,优化器用0更新权重。
解决:在所有涉及多次torch.autograd.grad的函数中,强制添加retain_graph=True。可在训练循环开头加断言:assert x.requires_grad and u.requires_grad。
4.3 现象:Laplace方程解出现明显“棋盘格”伪影(checkerboard artifact)
原因:使用ReLU激活函数 + 均匀网格采样。ReLU的分段线性特性在规则网格上产生周期性误差模式。
解决:换用tanh或sine激活;或改用Sobol序列采样打破周期性;若必须用ReLU,则在损失中加入TV(Total Variation)正则项:loss_tv = torch.mean(torch.abs(u_xx) + torch.abs(u_yy))。
4.4 现象:Robin边界条件αu + β∂u/∂n = h始终不满足,残差恒定
原因:法向量n_vec计算错误。在圆域上,n_vec = [x, y]/sqrt(x²+y²),但若x,y为tensor且含0点,sqrt(0)导致NaN传播。
解决:法向量计算加epsilon保护:n_vec = torch.cat([x, y], dim=1) / (torch.norm(torch.cat([x, y], dim=1), dim=1, keepdim=True) + 1e-8)。
4.5 现象:Jacobian-Hessian方法(Jacobian-Hessian methods for ODE system.ipynb)训练极慢,单步耗时>10s
原因:create_graph=True开启后,Hessian计算复杂度为O(n³),而网络有50×50权重矩阵时,torch.autograd.grad会构建巨大中间图。
解决:改用torch.func.hessian(PyTorch 2.0+)或backpack库的BatchGrad;或对Hessian项使用有限差分近似:hess_approx = (grad(x+h) - grad(x-h)) / (2*h),h=1e-4。
4.6 现象:DeepXDE训练中num_boundary=400,但实际边界点仅200个
原因:DeepXDE的on_boundary函数返回True的点,可能因浮点误差被重复剔除。尤其在圆、椭圆等参数化边界上,geom.on_boundary(x)对x=[0.999999,0]可能返回False。
解决:自定义边界判断函数,放宽容差:lambda x, on_boundary: on_boundary or torch.norm(x, dim=1) > 0.99。
4.7 现象:PDAE_system.ipynb中代数约束g(u,v)=0始终不满足,残差>1e2
原因:PDAE中微分变量与代数变量未解耦,网络同时输出u和v,但损失中未对g(u,v)施加足够权重。
解决:采用分步训练:第一阶段固定v,只训u满足PDE;第二阶段冻结u,只训v满足g=0;第三阶段联合微调,并设lambda_algebraic = 1000。
5. 验证PINN解的物理可信度:三类不可跳过的检验手段与量化指标
跑出一个loss<1e-4的PINN模型只是起点,真正的工程价值在于:它是否给出物理上自洽、数值上收敛、可外推的解?这份资源虽未显式提供验证脚本,但所有Notebook的命名和结构已暗含验证逻辑。我将其提炼为三个必须执行的检验层,每层配可抄作业的代码或表格。
5.1 解析解比对层:用已知闭式解量化误差,拒绝“loss低=解好”的幻觉
PINN最大的陷阱是loss下降但解漂移。必须找有解析解的问题做基线测试。资源中-y″=π²sin(πx), -1<x<1, y(-1)=0, y(1)=0.ipynb的解析解是y_true = sin(πx)。验证不能只画图,要量化:
# 加载训练好的model和测试点 x_test = torch.linspace(-1, 1, 1000).reshape(-1, 1) y_pred = model(x_test).detach().numpy() y_true = np.sin(np.pi * x_test.numpy()) # 计算三类误差(必须全打) l2_error = np.sqrt(np.mean((y_pred - y_true)**2)) linf_error = np.max(np.abs(y_pred - y_true)) h1_semi_error = np.sqrt(np.mean((np.gradient(y_pred.flatten(), x_test.numpy().flatten()) - np.pi * np.cos(np.pi * x_test.numpy().flatten()))**2)) print(f"L2 error: {l2_error:.2e} | L∞ error: {linf_error:.2e} | H1-semi: {h1_semi_error:.2e}")为什么必须三项:
L2反映整体拟合质量;L∞暴露局部最大偏差(如边界震荡);H1-semi检验导数精度——对结构力学问题,位移导数即应变,误差超标意味着应力计算失效。
行业经验阈值:L2 < 1e-3且H1-semi < 5e-2才认为可用于工程估算。
5.2 物理守恒律检验层:用积分守恒量验证全局一致性,揪出“局部正确,全局荒谬”的解
很多PDE蕴含守恒律,如扩散方程∂u/∂t = ∇·(D∇u)的质量守恒:∫u dx应随时间衰减(若D>0)。即使无时间项,稳态PDE也有隐含守恒。以Δu = 2为例,由Green公式:∫∫_Ω Δu dxdy = ∫_∂Ω ∂u/∂n ds = 2*Area(Ω)。若Ω=[-1,1]²,右边应为8。
验证脚本(适用于任意2D矩形域):
def conservation_check(model, x_min, x_max, y_min, y_max, n=100): x = torch.linspace(x_min, x_max, n) y = torch.linspace(y_min, y_max, n) X, Y = torch.meshgrid(x, y, indexing='ij') pts = torch.stack([X.ravel(), Y.ravel()], dim=1) u = model(pts).reshape(n, n) # 数值积分:梯形法则 dx, dy = x[1]-x[0], y[1]-y[0] laplacian_integral = torch.sum((u[2:,1:-1] + u[:-2,1:-1] + u[1:-1,2:] + u[1:-1,:-2] - 4*u[1:-1,1:-1])) * dx * dy # 理论值 area = (x_max-x_min) * (y_max-y_min) theory = 2 * area print(f"Numerical ∫∫Δu: {laplacian_integral.item():.4f} | Theory: {theory:.4f} | Error: {abs(laplacian_integral.item()-theory):.4f}") conservation_check(model, -1, 1, -1, 1) # 应输出 error < 0.1提示:若误差>0.5,说明网络在角落或边界采样严重不足,或
lambda_dir过小。守恒律检验不依赖解析解,是PINN独有的“物理可信度探针”。
5.3 外推与扰动鲁棒性层:在训练域外和噪声下测试,区分“记忆”与“理解”
一个合格的PINN解必须能外推。资源中y′=sin(πx)cos(πx)只在[-1,1]训练,但可测试[-1.2,1.2]:
x_ext = torch.linspace(-1.2, 1.2, 200).reshape(-1, 1) y_ext_pred = model(x_ext).detach().numpy() y_ext_true = -0.5 * np.cos(np.pi * x_ext.numpy())**2 + 0.5 # 解析原函数 # 计算外推区间误差 ext_mask = (x_ext.numpy() < -1) | (x_ext.numpy() > 1) ext_l2 = np.sqrt(np.mean((y_ext_pred[ext_mask] - y_ext_true[ext_mask])**2)) print(f"Extrapolation L2 error: {ext_l2:.2e}")更严苛的是加噪声测试:对训练点x_int加N(0,0.01)噪声,重训模型,看L2_error是否上升<50%。若上升300%,说明模型过拟合采样点,而非学习物理律。
5.4 一份完整的验证报告模板(可直接填入你的实验记录)
| 检验类型 | 测试案例 | 指标 | 阈值 | 实测值 | 是否通过 | 备注 |
|---|---|---|---|---|---|---|
| 解析解比对 | -y″=π²sin(πx) | L2 error | <1e-3 | 8.2e-4 | ✅ | — |
| 解析解比对 | -y″=π²sin(πx) | H1-semi error | <5e-2 | 3.1e-2 | ✅ | — |
| 物理守恒 | Δu = 2on [-1,1]² | ∫∫Δu error | <0.5 | 0.23 | ✅ | — |
| 外推能力 | y′=sin(πx)cos(πx) | Extrapolation L2 | <5e-3 | 4.7e-3 | ✅ | [-1.2,-1)∪(1,1.2] |
| 噪声鲁棒 | y′=sin(πx)cos(πx) | L2 increase ratio | <2.0 | 1.42 | ✅ | σ=0.01 |
从那以后我每次跑完一个PINN模型,都强制走一遍这五项检验——不是为了交差,而是因为曾经在某个Euler Beam项目里,只看了loss就交付,结果客户用解算出的应力云图做疲劳分析,寿命预测偏差达300%。物理信息网络的“物理”二字,不是装饰,是底线。希望帮到你。
本文还有配套的精品资源,点击获取