资讯动态

物理信息神经网络PINN入门:用PyTorch把微分方程变成损失函数

发布时间:2026/9/23 17:11:02 来源:尧图企业网站定制
简介这份基于PINN的微分方程求解方法Python资源包面向科学计算、深度学习和数值模拟方向的研究者与学生聚焦利用物理信息神经网络处理常/偏微分方程尤其适合复杂几何域或非线性问题。压缩包共26个文件以17个ipynb案例笔记为核心配有3个py源码脚本、4个zbak备份、1张结果示意图和1份md说明文档整体仅891KB结构清晰便于对照学习。案例覆盖Euler Beam、扩散方程、Poisson方程含Dirichlet、Robin、Neumann、Periodic边界、Laplace方程、PDAE系统、Lorenz系统等典型问题并涉及DeepXDE工具、Jacobian-Hessian方法测试、损失函数物理约束项构建、自动微分及模型训练思路。已有217人学习下载对希望快速上手PINN并复现多种微分方程场景的读者具有实用参考价值初学者可通过完整代码与笔记梳理解题思路进阶者也能比较不同边界条件与求解器组合下的实现差异。1. PINN 到底在解决什么把微分方程变成损失函数微分方程的数值解在过去很长一段时间里几乎等于“有限差分、有限元、谱方法”这三板斧而现在 PINNPhysics-Informed Neural Networks物理信息神经网络在流体、固体力学和热传导领域确实“杀疯了”。它最反直觉的一点是不搞网格剖分也不做时间步进而是把微分方程和边界条件直接塞进神经网络的损失函数里用自动微分求导数用反向传播做优化最终输出一个连续可导的近似解。这篇文章就用 Python 从零搭一个能跑通的最小 PINN把损失函数怎么设、采样点怎么布、参数怎么调、坑在哪一次说清。适合有 Python 基础、想往 AI for Science 方向走或者正被高维/反问题折磨的读者。2. 把微分方程变成损失函数PINN 的数学形式与设计动机2.1 从欧拉法和 ode45 到 PINN为什么要换一种求解思路如果你以前用欧拉法解过常微分方程应该对“步长”这两个字有深刻印象步长太大误差累积到不可接受步长太小迭代次数暴涨。用 MATLAB 的 ode45 或 SciPy 的 solve_ivp 写初值问题确实方便但那属于“时间步进”的思路——每一小步都依赖上一步的结果本质是串行的而且遇到边界条件复杂或不规则求解域时前处理就够喝一壶的。PINN 把问题换了个角度解 u(x) 被一个神经网络近似网络的输入是坐标输出是解的值。微分方程本身变成一个约束要求这个网络在求解域内部的每个点上满足方程边界条件变成另一个约束要求网络在边界上取到指定值。这样一来求解过程变成了一个优化问题优化的对象是神经网络的权重。这个转变带来的第一个好处是无网格。不需要生成网格只需要在求解域里撒点点的分布可以完全独立于几何形状第二个好处是天然支持反问题——如果你想知道方程里的某个未知参数把它也定义成一个可训练的变量放进损失函数优化完参数、优化完网络参数值也出来了。这是传统求解器很难直接做到的事。当然PINN 不是银弹。它的训练本质上是在做优化而高维问题的采样点数量会指数增长训练耗时并不比传统方法短。一个务实的判断标准是如果你的问题域很规整、阶数不高、只需要解一次继续用有限差分或 ode45 是更划算的如果要做反演、要处理复杂几何、要获得一个处处可导的代理模型PINN 才值得投入。2.2 损失函数三件套PDE 残差、边界条件、初始条件PINN 的损失函数长这样[ L w_r L_r w_{bc} L_{bc} w_{ic} L_{ic} ]L_r 是方程残差把网络输出代入微分方程左右两边不相等的程度。L_{bc} 是边界条件损失网络在边界上的输出与给定边界值的差距。L_{ic} 是初始条件损失时间相关问题里t0 时网络输出与初值的差距。以最简单的稳态热传导方程 ( -u(x) f(x) )一维泊松方程为例如果解析解是 ( u(x)\sin(\pi x) )那么 ( f(x)\pi^2 \sin(\pi x) )。网络输入 x输出 u_net(x)方程残差就是[ L_r \frac{1}{N_r} \sum_{i1}^{N_r} \left( -\frac{d^2 u_{net}(x_i)}{dx^2} - f(x_i) \right)^2 ]这里的二阶导数不是用数值差分算的而是通过 PyTorch 的自动微分对网络输出求梯度这一点是 PINN 能高效实现的关键。边界条件损失更直接[ L_{bc} \frac{1}{N_{bc}} \sum_{i1}^{N_{bc}} \left( u_{net}(x_i^{bc}) - u_{bc}(x_i^{bc}) \right)^2 ]如果边界条件是 ( u(0)0, u(1)0 )那只要在 x0 和 x1 上各取一个样本点算网络输出与 0 的均方误差即可。严格来说边界上每个点都要满足但实际训练中我们只采样有限个边界点来近似。三个损失项的权重选择非常重要也是 PINN 最“玄学”的地方之一。如果 w_r 太大网络会优先满足方程内部点边界可能完全跑偏如果 w_bc 太大网络会把解压成一个整体偏移来满足边界方程反而不满足。我的经验是先从 w_r1、w_bc10~50 起步跑起来以后看边界误差再调。采样点方面内部点用随机采样或者拉丁超立方采样边界点固定取边界上的离散点。常见做法是内部占大多数边界点至少 2 到 20 个具体看边界维度一维问题取 2 个端点就够二维问题每条边界均匀取几十个点。3. 用 PyTorch 跑通最小 PINN一维泊松方程的完整实现3.1 先搭网络与优化器3~4 层 tanh 全连接就够下面这个实现用 PyTorch 求解 ( -u(x)\pi^2\sin(\pi x) )定义域 ( x\in[0,1] )边界条件 ( u(0)u(1)0 )解析解是 ( u(x)\sin(\pi x) )。这是 PINN 的 Hello World代码量控制在 100 行以内。import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt torch.manual_seed(42) # 全连接网络输入坐标 x输出解 u(x) class PINN(nn.Module): def __init__(self, n_hidden4, n_neurons50): super().__init__() layers [nn.Linear(1, n_neurons), nn.Tanh()] for _ in range(n_hidden - 1): layers [nn.Linear(n_neurons, n_neurons), nn.Tanh()] layers [nn.Linear(n_neurons, 1)] self.net nn.Sequential(*layers) def forward(self, x): return self.net(x) model PINN(n_hidden4, n_neurons50) optimizer torch.optim.Adam(model.parameters(), lr1e-3)每层之间用 Tanh 激活而不是 ReLU。原因后面避坑章会专门说这里先记住一句话PINN 要求激活函数有二阶连续导数Tanh 是默认选择。n_hidden4 表示隐藏层有 4 层加上输入输出层总共 6 层n_neurons50 是每层宽度。这个规模对一维问题足够了过大的网络只会拖慢收敛。3.2 核心损失函数autograd.grad 求二阶导create_graph 必须打开这一节是全网 PINN 代码里最容易出错的地方没有之一。# 损失函数PDE 残差 边界条件 def loss_function(model, x_domain, x_bc): # 域内点计算二阶导数构造残差 x_domain x_domain.clone().requires_grad_(True) u model(x_domain) u_x torch.autograd.grad( u, x_domain, grad_outputstorch.ones_like(u), create_graphTrue )[0] u_xx torch.autograd.grad( u_x, x_domain, grad_outputstorch.ones_like(u_x), create_graphTrue )[0] # 方程 -u pi^2 * sin(pi*x)所以残差 -u - f f (torch.pi ** 2) * torch.sin(torch.pi * x_domain) loss_pde torch.mean((-u_xx - f) ** 2) # 边界点损失u(0)0, u(1)0 u_bc model(x_bc) loss_bc torch.mean(u_bc ** 2) return loss_pde 50.0 * loss_bc先解释create_graphTrueautograd.grad 默认只计算一阶梯度返回的梯度张量不会保留计算图。但我们后面要对 u_x 再求一次梯度得到 u_xx中间必须保留图结构这个参数不打开二阶导直接报错或者得到 None。再说grad_outputsu 的形状是 (N, 1) 而不是 (N,)PyTorch 求梯度时需要一个与 u 同形状的权重张量来逐元素相乘用 ones_like 即可数学上等价于对每个样本分别求梯度后堆叠。训练数据里x_domain 是域内采样点每次迭代重新随机采样一批x_bc 固定为 [0.0] 和 [1.0] 两个边界点。损失函数里 loss_pde 和 loss_bc 的数量级可能差很多这里给边界项乘了 50.0 的权重让边界条件在训练初期就优先被满足。3.3 训练循环与结果验证loss 下降不代表解对了# 训练循环边采样边训练 for epoch in range(5000): # 每次迭代重新采样域内点避免网络记住固定点 x_domain torch.rand(2000, 1) x_bc torch.tensor([[0.0], [1.0]]) optimizer.zero_grad() loss loss_function(model, x_domain, x_bc) loss.backward() optimizer.step() if epoch % 500 0: print(fepoch {epoch:5d}, loss {loss.item():.6e}) # 验证与解析解对比 x_test torch.linspace(0, 1, 200).view(-1, 1) u_pred model(x_test).detach().numpy().squeeze() u_exact np.sin(np.pi * x_test.numpy()).squeeze() max_err np.max(np.abs(u_pred - u_exact)) l2_rel_err np.linalg.norm(u_pred - u_exact) / np.linalg.norm(u_exact) print(fmax abs error {max_err:.4e}) print(frelative L2 error {l2_rel_err:.4e}) # 画图对比 plt.figure(figsize(6, 4)) plt.plot(x_test.numpy(), u_exact, b-, labelexact) plt.plot(x_test.numpy(), u_pred, r--, labelPINN) plt.legend() plt.xlabel(x) plt.ylabel(u(x)) plt.savefig(pinn_poisson.png, dpi150)打印的 loss 只反映训练点上的误差loss 降到 1e-5 不代表解就是对的——有可能边界是对的中间振荡得厉害有可能整体解被压扁了内部残差被边界权重盖过去了。所以验证必须用独立于训练数据的均匀网格算最大绝对误差和相对 L2 误差两大指标都达标才算真的跑通。一个小习惯把验证用的 x_test 固定下来每次改动模型后都跑同样的对比这样前后误差才有可比性。训练 5000 个 epoch 后这个例子的最大绝对误差应该能到 1e-3 甚至更低。如果到不了优先检查是不是学习率太大导致 loss 震荡再检查边界权重是否太小。4. 采样点、网络宽度、激活函数三个必调参数与验证方法4.1 采样策略均匀随机、拉丁超立方与自适应加密PINN 的训练数据不是固定的数据集而是由采样策略动态生成的。这一点和传统深度学习完全不同也是调参时最需要转变思路的地方。采样策略做法优点缺点适用场景均匀随机采样在定义域内按均匀分布随机取点实现简单每轮样本独立高维时点分布会抱团覆盖不匀低维和一维问题拉丁超立方采样把每维分成 N 等份每份各取一个点点分布更均匀方差小实现略复杂每次都要生成新样本二维以上问题自适应采样先训练一轮找残差大的区域加采提高局部精度有额外计算开销容易过拟合热点区解有局部剧烈变化的问题域内采样点的数量直接决定训练的迭代质量。一维问题 1000~4000 点每轮就够二维问题每个维度 50~100 个点的密度总点数在 2500~10000更高维的问题点数要按维度折中——盲目加点数会让每一轮的正向传播和反向传播变慢训练轮数被迫减少最终精度未必提升。边界采样点独立于域内点。一维问题每个边界取 1 个点就够二维问题每条边界均匀取 20~50 个点三维问题每条边界面取 500~2000 个点。边界点数量不求多但要求覆盖完整。我在实际项目中会用torch.rand做快速验证确认模型能收敛后再换成scipy.stats.qmc.LatinHypercube提高训练稳定性。前缀检验跑通比一上来追求最优采样策略更重要。4.2 网络宽度和深度的权衡不要一上来就上大模型PINN 不是越大越准。隐藏层太多或神经元太宽会导致训练初期的梯度更新方向混乱收敛速度反而下降。问题复杂度建议网络结构典型采样点数/轮备注一维常微分方程2~4 层 × 20~50 神经元1000~2000 点3000~5000 轮30 秒内可跑完二维稳态偏微分方程3~5 层 × 50~80 神经元每维 50 点左右需要 Adam L-BFGS 两段训练二维时间依赖 / 三维稳态4~6 层 × 80~100 神经元10000~50000 点建议加傅里叶特征映射复杂工程反问题6~8 层 × 100~150 神经元按需加密必须做多组随机种子对比一个常见误区是拿 NLP 那套“大模型”思路套 PINN。PINN 的核心约束是物理方程网络只是函数逼近器不需要海量参数去记忆数据。我在多个项目里的经验是先跑一个 3 层 × 50 的小网络看残差分布在哪里残差大的区域用自适应采样去加密比盲目加宽网络更有效。4.3 激活函数为什么首选 tanh二阶导数说了算激活函数的选择在 PINN 里非常关键因为损失函数里含有网络输出的二阶导数激活函数的光滑性直接决定导数是否存在、是否稳定。Tanh二阶导连续且非零对称于原点梯度传播稳定是 PINN 的默认选项。Sigmoid导数范围窄层数稍深梯度就会消失输出均值不为零不推荐。ReLU一阶导是阶跃函数二阶导在全域几乎是 0。当你计算残差 -u - f 时u 输出恒为 0网络完全没有信号去满足方程。SiLU/Swish光滑且二阶导存在实际效果接近 Tanh可以作为备选但不值得为它引入额外的调参变量。顺便说一句如果网络输出层加一个可学习的缩放参数比如self.output_scale nn.Parameter(torch.ones(1))可以让网络在训练初期更容易匹配解的量级。这个技巧在处理数值量级很大的方程时特别有用。4.4 验证精度的方法不要只看 loss要看解析解和数值解误差PINN 的 loss 下降曲线是一个参考但远远不够。因为 loss 是在采样点上算的采样点之间的区域网络可能振荡得很厉害。我常用的验证手段有三个第一最大绝对误差和相对 L2 误差在独立均匀网格上计算。第二把预测解和解析解画在一张图里肉眼检查形态是否一致——解的形状错了但误差很小的情况并不少见比如整体平移。第三看残差分布图把训练好的模型在加密网格上重新计算方程残差残差大的区域就是需要加采样点的地方。对于没有解析解的问题可以拿传统数值方法做对照比如用 SciPy 的 solve_bvp 求解同一边值问题把两种结果放在一起比较。这不会证明哪一个绝对正确但两套独立方法得出相近结果可信度会高很多。5. 避坑PINN 求解微分方程的 5 个常见翻车现场5.1 边界条件一直不满足损失降了解却跑偏了现象训练过程中 loss 在下降打印边界点上的 u(0) 和 u(1)发现它们没有逼近 0而是某个非零常数。画出来的解整体被抬高或压低形状接近正确但数值离谱。原因边界损失项在总损失里占比太低。当残差项的梯度量级远大于边界项时优化器会把精力几乎全部放在满足方程内部点上边界约束形同虚设。解决把边界权重 w_bc 从默认的 1.0 调到 10~100或者在每个 epoch 里打印边界点预测值来监控。权重不是越大越好过大会让边界收敛但内部残差卡住一般从 50 起步看两边误差平衡后再微调。5.2 二阶导恒为 0选了 ReLU 当激活函数现象损失函数一上来就不降训练后网络输出近似一个常数你检查 u_xx 发现它全是 0。原因ReLU 的二阶导在除零点外的区域恒为 0代入 PDE 残差时 -u - f 变成了 -f不包含任何网络输出信息梯度无信号。解决把激活函数全部换成 Tanh。这是 PINN 入门最常见的翻车现场建议在第一步就把 ReLU 排除出候选列表。Leaky ReLU 同样不行它的二阶导也是 0。5.3 过拟合采样点训练残差小验证误差大现象训练 loss 降到 1e-6但换一组随机种子重新采样训练得到的解完全不一样或者用加密网格评估时误差远大于训练点误差。原因每轮迭代用同一批固定点训练网络会把采样点之间的区域“跳过去”形成局部振荡。网络参数量越大越容易这样。解决每个 epoch 重新采样域内点。把x_domain torch.rand(2000, 1)放进训练循环内部而不是在循环外生成一次。另外可以减小网络宽度或者引入 L-BFGS 做精细收敛见下一章。这个现象和传统深度学习里的过拟合不同没有任何正则化方法比重新采样更直接。5.4 loss 震荡或出现 NaN学习率与梯度爆炸现象训练过程中 loss 在某个量级来回跳甚至突然变成 nan。通常是训练进行到某一步后一阶导或二阶导的计算结果溢出。原因学习率过大导致参数更新步长过大或者方程的解在局部区域变化剧烈梯度值非常大加上自动微分会放大这种爆炸。解决先停下来把学习率降到 1e-4 重跑。如果还出现 nan检查输入坐标是否需要归一化——不是所有问题都适合直接用原始坐标把 x 归一化到 [-1, 1] 区间能有效缓解。另外权重初始化用默认的 kaiming 就行不需要额外处理。5.5 高频解学不动神经网络天然偏向低频函数现象方程的解含有快速振荡成分比如多尺度问题PINN 训练很久后低频部分拟合得很好高频尖峰区域误差始终很大。原因这本质上是神经正切核NTK理论揭示的频谱偏差——神经网络对低频分量的学习速度远快于高频分量。PINN 对这个问题尤其敏感因为损失函数里的导数项会进一步放大高频误差。解决加傅里叶特征映射把输入坐标先通过一个随机高频映射再送入网络或者把问题分解成多个子域分别训练。这个问题的深度处理放在下一章的进阶技巧里。6. 从收敛到泛化L-BFGS 精修、傅里叶特征与贝叶斯 PINN 的进阶技巧6.1 Adam 粗训 L-BFGS 精修PINN 的标配组合Adam 在前 3000 个 epoch 表现得很好但后期收敛速度明显变慢loss 在某个平台期附近徘徊。原因在于 Adam 是自适应学习率的随机优化器它适合大范围搜索不适合精细收敛。常见做法是先用 Adam 训到 loss 平台期再换 L-BFGS 精修。L-BFGS 是拟牛顿方法利用梯度信息近似 Hessian 矩阵收敛速度快、精度高在 PINN 里几乎是标准收尾操作。from torch.optim import LBFGS # 先用 Adam 训练到 loss 不再明显下降然后: optimizer_lbfgs LBFGS(model.parameters(), lr1.0, max_iter2000) def closure(): optimizer_lbfgs.zero_grad() loss loss_function(model, x_domain, x_bc) loss.backward() return loss optimizer_lbfgs.step(closure)L-BFGS 的 closure 函数必须返回 loss 的标量值这是和 Adam 最大的用法差异。注意 lr 设为 1.0 是常规做法L-BFGS 会自动计算步长手动调小 lr 反而会拖慢收敛。迭代次数 max_iter 从 1000 起步如果 loss 仍在明显下降可以继续增加 500。我实际用下来L-BFGS 对初值很敏感——如果 Adam 阶段没有把网络训到一个合理的解附近L-BFGS 容易陷入局部极小。所以两阶段顺序不要颠倒。6.2 傅里叶特征映射让网络学到高频分量如果直接训练高频振荡方程效果不好给输入先加一层傅里叶特征映射是有效手段。做法很简单把输入坐标 x 通过 sin/cos 映射到高维空间再送入全连接网络。class FourierFeature(nn.Module): def __init__(self, in_dim1, mapping_dim64, scale10.0): super().__init__() # B 矩阵固定不参与训练 self.B torch.randn(in_dim, mapping_dim) * scale def forward(self, x): proj x self.B return torch.cat([torch.cos(2 * torch.pi * proj), torch.sin(2 * torch.pi * proj)], dim-1)scale 控制了高频成分的初始范围。scale 太小退化成近似线性映射效果等同于原始输入scale 太大网络学到的全是噪声。一般从 scale10 起步如果你的解含有很高频的振荡比如频率超过 10π可以逐步加大到 50 甚至 100。加了这个映射之后网络本身不要加深还是那 3~4 层就够了。这个技巧的核心机制是让网络的输入特征提前包含多尺度成分然后再用全连接层去组合这些特征。它对高频 PDE 问题非常有效但对平滑解反而可能降低精度不要无条件使用。先用无映射版本跑通确认是高频问题后再加。6.3 多目标损失的权重自适应贝叶斯 PINN 的一种理解贝叶斯 PINN 这个方向最近讨论很多对初学者来说它最实用的落点是把损失函数里的人工权重变成可学习的变量。贝叶斯视角下的多任务学习认为不同损失项的噪声方差不同梯度量级差异大手动调权重很难平衡不如让模型自己学。核心思路是用不确定性同方差不确定性自动加权[ L \frac{1}{2\sigma_r^2} L_r \frac{1}{2\sigma_{bc}^2} L_{bc} \log\sigma_r \log\sigma_{bc} ]其中 σ_r 和 σ_bc 是对数化的可学习参数。前半部分把权重变成 1/(2σ²)后半部分是正则项防止 σ 无限增大。这个做法不需要额外的监督数据只需在模型里加两个 Parameter 对象即可log_sigma_r torch.nn.Parameter(torch.tensor(0.0)) log_sigma_bc torch.nn.Parameter(torch.tensor(0.0)) # 在损失函数里返回加权之后的标量 loss (0.5 * torch.exp(-2 * log_sigma_r) * loss_pde 0.5 * torch.exp(-2 * log_sigma_bc) * loss_bc log_sigma_r log_sigma_bc)优化器要把这两个参数一起传给 Adam训练后打印 sigma 的最终值可以帮助你反过来理解当前问题哪一项更难满足。我自己用下来的体会是自适应权重能去掉“人工来回试权重”这一步但训练初期的稳定性比固定权重差最好先用固定权重粗训几百轮再打开自适应权重。PINN 这个方向光看懂原理和跑通 demo 之间隔着十来个坑。我现在的习惯是每换一个新方程先跑一个 50 宽、3 层的小网络同时打印内部残差、边界误差和解曲线三个面板确认整体误差量级后再决定要不要上傅里叶特征、L-BFGS 和自适应权重。这个习惯帮我避开了无数个误以为“已经收敛”的假象希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价