简介:这份资源围绕物理信息神经网络(PINN)求解微分方程展开,面向具备一定Python与深度学习基础、希望将神经网络用于科学计算的研究生、工程师及科研人员,帮助解决传统数值方法在复杂边界条件与高维问题上建模繁琐、网格依赖强等痛点。压缩包共22个文件,以17个ipynb交互式笔记本为主,辅以3个py源码模块、1个md说明文档和1张png示意图,整体约889KB,涵盖PDE、model、pinns geometry等核心代码与实验记录。内容涉及泊松方程、拉普拉斯方程、扩散方程、欧拉梁、洛伦兹系统及多种ODE/PDE边值问题,并包含Dirichlet、Neumann、Robin、周期边界等不同设定,还测试了Jacobian-Hessian方法在ODE系统与拉普拉斯方程中的表现。已有2976人学习下载,读者可据此快速复现PINN求解流程,理解损失构造、边界处理与网络训练细节,并在此基础上迁移到自身微分方程问题。
1. 从一条弹簧振子曲线说起:PINN 到底在算什么
你手上有一组物理实验数据,或者一条来自仿真软件的位移-时间曲线,背后明明对应着一个微分方程,但方程里的阻尼系数、刚度参数你并不完全清楚。传统做法是先用差分法估参数,再拿数值积分去拟合,中间误差一层层放大,最后曲线对不上,你还得回头怀疑是数据噪声还是模型结构错了。PINN(Physics-Informed Neural Network,物理信息网络)换了个思路:把微分方程本身当成损失函数的一部分,让神经网络在拟合观测数据的同时,被迫满足物理规律。它解决的不是“解一个已知方程”,而是“在数据稀疏、参数未知、边界条件复杂的情况下,把方程约束和数据约束揉进同一个优化目标里”。适合谁?手头有 Python 基础、懂一点自动微分、被正问题或反问题折磨过的工程人员和研究生。这一章不铺公式,先把这件事的边界讲清楚。
PINN 的核心竞争力在于它把“求解微分方程”转化成了“训练一个网络”。传统数值方法(有限差分、有限元)需要在空间和时间上划网格,网格越密精度越高,但维度一高就遭遇维数灾难。PINN 用神经网络做函数逼近器,输入是坐标(比如 x、t),输出是待求物理量(比如 u),它天然无网格,高维问题上不会因为网格爆炸而崩掉。更关键的是,自动微分让方程里的导数项可以直接对网络输出求导,不需要手动推导离散格式。这意味着你写残差的时候,几乎就是把方程原样抄进代码。
但别急着上头。PINN 不是万能替代品。它在高维、反问题、数据同化场景下有优势,但在需要极高精度、强激波、多尺度湍流的问题上,收敛慢、训练不稳定是常态。我见过太多人拿 PINN 去解一个一维热传导,结果发现有限差分两秒出结果,PINN 训了半小时还在震荡。所以第一件事:判断你的问题值不值得上 PINN。判断标准很简单——如果你的方程维度低、边界规则、精度要求高,传统方法更划算;如果你的观测数据少、方程参数未知、区域不规则,PINN 才真正开始发光。
这一章先立住三个认知:第一,PINN 的损失函数由数据项和物理残差项组成,两者权重需要调;第二,网络结构不是越深越好,激活函数的选择直接影响导数计算的质量;第三,训练过程本质是一个约束优化,不是普通回归。后面几章会围绕这三个认知,把代码、参数、坑一个个拆开。
2. 用 PyTorch 搭一个最小 PINN:从方程到可运行代码
2.1 选型理由:为什么是 PyTorch 而不是别的框架
做 PINN 的框架选择,主流是 PyTorch、TensorFlow 和 JAX。我一般推荐 PyTorch,原因很实际:自动微分接口直观,torch.autograd.grad对高阶导的支持稳定,调试时能直接打印中间张量,社区里 PINN 相关实现也最多。TensorFlow 的GradientTape也能做,但高阶导嵌套写起来啰嗦;JAX 的jit和vmap性能好,但函数式编程风格对新手不友好,调试报错信息也不够直观。如果你已经装了 Python,直接上 PyTorch 就行。
环境准备不复杂,但版本要对齐。Python 3.8 以上都可以,PyTorch 建议 1.10 以上,因为早期版本对create_graph=True的高阶导支持有坑。安装命令如下:
pip install torch torchvision numpy matplotlib如果你用 GPU,去 PyTorch 官网按 CUDA 版本选对应命令,别直接pip install torch装成 CPU 版,后面训练慢到怀疑人生。装完验证一下:
import torch print(torch.__version__) print(torch.cuda.is_available())如果第二行输出False而你有显卡,说明装错了,回去重装 CUDA 版本。
2.2 一个最小可运行例子:一维阻尼振子
我们拿一个经典问题开刀:阻尼谐振子。方程是
d²u/dt² + 2ζω₀ du/dt + ω₀² u = 0其中 ω₀ 是自然频率,ζ 是阻尼比。假设我们只知道初始条件 u(0)=1、u'(0)=0,以及若干稀疏观测点,想反推出 ζ 和 ω₀,同时让网络输出满足方程。这就是典型的 PINN 反问题。
先定义网络。PINN 的网络不需要太深,一般 3 到 5 层全连接就够,每层 32 到 128 个神经元。激活函数是关键:tanh是默认选择,因为它的二阶导光滑且不为零;ReLU的二阶导几乎处处为零,会导致物理残差里的二阶导项失效,千万别用。代码:
import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt class PINN(nn.Module): def __init__(self, layers=[1, 64, 64, 64, 1]): super().__init__() modules = [] for i in range(len(layers) - 1): modules.append(nn.Linear(layers[i], layers[i+1])) if i < len(layers) - 2: modules.append(nn.Tanh()) self.net = nn.Sequential(*modules) # 可训练物理参数,初始猜测 self.zeta = nn.Parameter(torch.tensor(0.1)) self.omega0 = nn.Parameter(torch.tensor(1.0)) def forward(self, t): return self.net(t)这里把 ζ 和 ω₀ 设成nn.Parameter,它们会随着训练自动更新,这就是反问题的实现方式。注意初始值别设得太离谱,否则残差一开始就爆炸,梯度直接 NaN。
接下来构造损失函数。物理残差项是:
def physics_residual(model, t): t.requires_grad_(True) u = model(t) du = torch.autograd.grad(u, t, grad_outputs=torch.ones_like(u), create_graph=True)[0] d2u = torch.autograd.grad(du, t, grad_outputs=torch.ones_like(du), create_graph=True)[0] residual = d2u + 2 * model.zeta * model.omega0 * du + model.omega0**2 * u return residualcreate_graph=True必须加,否则二阶导无法回传。这是新手最容易翻车的地方——不加这个参数,程序不报错,但物理残差的梯度传不回去,训练半天参数不动。
数据项损失用观测点:
t_obs = torch.tensor([[0.0], [1.0], [2.0], [3.0]], dtype=torch.float32) u_obs = torch.tensor([[1.0], [0.5], [-0.2], [-0.6]], dtype=torch.float32) def data_loss(model): u_pred = model(t_obs) return torch.mean((u_pred - u_obs)**2)物理残差在配点上采样,配点可以随机生成,也可以均匀撒点:
t_phys = torch.linspace(0, 5, 200).reshape(-1, 1) def total_loss(model, lambda_phys=1.0): res = physics_residual(model, t_phys) loss_p = torch.mean(res**2) loss_d = data_loss(model) return loss_d + lambda_phys * loss_plambda_phys是物理项权重,这个参数非常敏感。太小,网络只拟合数据,方程约束形同虚设;太大,数据被忽略,网络输出一个满足方程但偏离观测的解。我一般从 1.0 开始试,观察两项损失的数量级,如果物理项比数据项大两个数量级以上,就调小。
训练循环:
model = PINN() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(5000): optimizer.zero_grad() loss = total_loss(model) loss.backward() optimizer.step() if epoch % 500 == 0: print(f"Epoch {epoch}, Loss: {loss.item():.6f}, " f"zeta: {model.zeta.item():.4f}, omega0: {model.omega0.item():.4f}")跑完 5000 轮,如果一切正常,ζ 和 ω₀ 会收敛到接近真实值。你可以把真实值设成 ζ=0.15、ω₀=1.2 生成观测数据,看网络能不能反推出来。
2.3 参数怎么设:学习率、网络宽度、配点数量
学习率用 Adam 默认的 1e-3 起步,如果损失震荡就降到 1e-4。网络宽度 64 是甜点区,32 可能欠拟合,128 以上训练变慢且容易过拟合噪声。配点数量 200 到 1000 之间,太少物理约束不够,太多每轮计算量大。我一般先 200 个点跑通,再逐步加到 500。
还有一个隐藏参数:配点的分布。均匀撒点适合解光滑的问题,如果解在某个区域变化剧烈,配点要往那里加密。常见做法是用残差大小做自适应采样,但那是进阶技巧,先把均匀撒点跑通再说。
训练轮数不是越多越好。PINN 的损失下降曲线经常是阶梯状,平一段时间突然掉一截。如果你看到损失卡住不动,先别急着加轮数,检查学习率、权重和网络结构。有时候换个激活函数或者调一下lambda_phys,比多跑一万轮管用。
3. 把 PINN 用到反问题:参数识别与边界条件处理
3.1 反问题的损失设计:数据项和物理项怎么平衡
反问题和正问题的区别在于,正问题里方程参数已知,网络只负责逼近解;反问题里参数未知,网络要同时学解和学参数。这意味着损失函数里数据项的作用更重,因为参数是靠数据锚定的。如果数据项权重太低,参数会漂到一个满足方程但不符合观测的值上。
我的经验是,反问题里lambda_phys从 0.1 到 1.0 之间调,先让数据项主导,再慢慢加物理约束。具体操作:前 1000 轮只用数据损失训练,让网络先拟合观测;然后加入物理残差,继续训练。这种分阶段策略比一上来就联合训练稳定得多。
还有一个技巧:对参数加边界约束。比如 ζ 必须在 0 到 1 之间,ω₀ 必须为正。可以在损失里加惩罚项:
def param_penalty(model): penalty = 0.0 if model.zeta < 0 or model.zeta > 1: penalty += 100 * (model.zeta - 0.5)**2 if model.omega0 < 0: penalty += 100 * model.omega0**2 return penalty这不是必须的,但能防止参数跑飞。尤其是训练初期,梯度噪声大,参数可能瞬间跳到负值,没有惩罚项的话后面很难拉回来。
3.2 边界条件怎么进损失:硬约束与软约束
边界条件在 PINN 里有两种处理方式。软约束是把边界条件当成损失的一项,比如loss_bc = mean((u(0) - u0)**2),和物理残差、数据项一起优化。硬约束是改造网络输出,让它在边界上自动满足条件,比如令u(t) = u0 + t * net(t),这样 t=0 时 u 恒等于 u0。
软约束实现简单,但权重难调;硬约束不需要调边界权重,但网络结构要针对问题定制。我一般先用软约束跑通,如果边界误差降不下去,再考虑硬约束。对于周期边界条件,硬约束更优雅,比如用傅里叶特征或者周期激活函数。
一个常见的翻车场景:边界条件权重设得太大,网络为了满足边界牺牲了内部区域的拟合,结果解在中间区域完全偏离。解决办法是观察边界损失和内部残差的数量级,让它们保持在同一量级。如果边界损失比内部残差小两个数量级,说明边界约束太弱;反之则太强。
3.3 用观测数据反推阻尼系数:完整代码与结果解读
把前面的代码串起来,生成一组带噪声的观测数据,看 PINN 能不能反推出 ζ 和 ω₀。真实值设 ζ=0.15、ω₀=1.2,用数值积分生成 20 个观测点,加 2% 高斯噪声。训练 10000 轮,每 1000 轮打印参数。
结果解读要注意:参数收敛曲线不是单调的,可能先上升后下降,也可能在真实值附近震荡。判断收敛的标准不是参数完全不动,而是损失不再显著下降且参数波动小于 1%。如果参数一直在漂,说明数据量不够或者噪声太大,PINN 不是魔法,数据质量决定上限。
还有一个细节:观测点的分布影响参数可辨识性。如果观测点全集中在 t=0 附近,阻尼系数很难反推,因为阻尼效应在后期才明显。所以实验设计阶段就要考虑观测点覆盖整个时间区间。
4. 避坑与排查:PINN 训练中常见的五个翻车现场
4.1 损失不下降,梯度为 NaN
现象:训练几轮后损失变成 NaN,或者损失一直停在初始值附近不动。
原因:最常见的是学习率太大,导致梯度爆炸;其次是物理残差里出现了除零或者对数负数;还有可能是create_graph没加,二阶导梯度断链。
解决:先把学习率降到 1e-4 甚至 1e-5 试;检查方程里有没有1/u或者log(u)这种项,如果有,给 u 加一个极小值保护;确认torch.autograd.grad里create_graph=True和grad_outputs都写了。如果还不行,把物理残差的表达式打印出来,看哪一项数值异常。
4.2 物理项损失降了,但数据拟合很差
现象:物理残差很小,但网络输出和观测点对不上,参数也偏离真实值。
原因:lambda_phys太大,网络只顾满足方程,忽略了数据。或者配点太多,数据点太少,优化被物理项主导。
解决:调小lambda_phys,或者增加观测点数量。也可以分阶段训练,先数据后物理。检查两项损失的数量级,让它们在同一量级内。
4.3 参数反推结果不稳定,每次训练都不一样
现象:同样的数据,换一个随机种子,反推的参数差很多。
原因:PINN 的损失曲面非凸,初始化对结果影响大。数据噪声大或者观测点少也会导致参数不可辨识。
解决:多跑几次取平均,或者用集成方法。增加观测点,降低噪声。对参数加先验约束,缩小搜索范围。如果参数物理意义明确,可以固定一个、反推另一个,降低问题维度。
4.4 训练速度慢,GPU 利用率低
现象:每轮训练耗时很长,GPU 显存占用低,计算瓶颈在 CPU。
原因:配点数量太多,或者网络太大。也可能是数据在 CPU 和 GPU 之间频繁拷贝。
解决:减少配点数量,先用少量点跑通再增加。网络宽度降到 32 或 64。把所有张量提前移到 GPU 上,避免在训练循环里做.cuda()转换。用torch.compile或者混合精度训练加速,但注意混合精度可能影响二阶导精度。
4.5 边界条件满足但内部解震荡
现象:边界处误差很小,但解在内部区域出现非物理震荡。
原因:网络过拟合噪声,或者物理残差在内部区域约束不够。激活函数选择不当也会导致高频震荡。
解决:增加配点密度,尤其是在震荡区域。换用更光滑的激活函数,比如tanh换成sin或者GELU。加正则化项,比如对网络权重的 L2 惩罚。如果震荡来自数据噪声,先做数据平滑再训练。
5. 进阶技巧:自适应权重与课程学习让 PINN 真正收敛
5.1 自适应权重:让损失自己找平衡
固定lambda_phys的问题是,训练过程中数据项和物理项的下降速度不一样,前期数据项主导,后期物理项主导,固定权重很难兼顾。自适应权重的基本思路是:根据两项损失的梯度范数动态调整权重,让它们在反向传播时对网络参数的更新贡献相当。
一个简单实现是梯度归一化:
def adaptive_loss(model, t_phys, t_obs, u_obs, alpha=0.9): loss_d = data_loss(model, t_obs, u_obs) res = physics_residual(model, t_phys) loss_p = torch.mean(res**2) grad_d = torch.autograd.grad(loss_d, model.net[0].weight, retain_graph=True, allow_unused=True)[0] grad_p = torch.autograd.grad(loss_p, model.net[0].weight, retain_graph=True, allow_unused=True)[0] norm_d = grad_d.norm() if grad_d is not None else torch.tensor(1.0) norm_p = grad_p.norm() if grad_p is not None else torch.tensor(1.0) lambda_hat = norm_d / (norm_p + 1e-8) return loss_d + lambda_hat * loss_p这个权重每轮更新,alpha是平滑系数,防止权重剧烈波动。实际用的时候,lambda_hat可能变化几个数量级,建议加一个上限截断,比如最大 100。
5.2 课程学习:先易后难,分阶段训练
课程学习的思路是先把问题简化,让网络学一个粗略解,再逐步增加难度。具体操作:第一阶段只用数据损失,配点少、训练轮数少;第二阶段加入物理残差,配点增加;第三阶段加入边界条件和高阶导数约束。每个阶段的学习率递减。
这种策略在反问题上尤其有效,因为参数识别需要数据先锚定一个大致范围,再靠物理约束精调。如果一上来就联合训练,参数可能在早期就被物理项拉偏,后面很难纠正。
5.3 验证方法:怎么判断 PINN 解是对的
PINN 没有解析解可比时,验证靠三条:第一,看物理残差在配点之外的测试点上是否也小,如果只在训练配点上小,说明过拟合;第二,看观测点之外的预测是否符合物理直觉,比如能量是否单调衰减;第三,换一组初始条件重新训练,看参数反推结果是否一致。
我自己的习惯是,每次训练完都把残差分布画出来,看有没有局部区域残差异常大。如果有,说明那个区域配点不够或者解变化剧烈,需要加密配点。这个习惯帮我省了很多次重新训练的时间。
最后说一句血泪经验:PINN 的调参没有银弹,每个问题都要重新试。但只要你把损失数量级、梯度范数、残差分布这三个东西盯住,大部分问题都能定位到原因。希望帮到你。
本文还有配套的精品资源,点击获取