物理信息神经网络入门:用PINNs解偏微分方程实战与避坑指南
2026/9/16 1:07:21 网站建设 项目流程

先劝退一波:如果你是来找“调个包、跑个demo、发个paper”的轻松路径,那 PINNs 可能要让你失望了。这玩意儿不是典型的深度学习任务,它更像“数值分析 + 深度学习”的杂交体,训练起来既要有调神经网络的耐心,又要有分析偏微分方程的直觉。但这恰恰是它最有意思的地方——你不需要生成昂贵的仿真数据,靠物理定律本身就能训练网络,这种思路在科学计算和工程反演里相当有想象力。

这篇博文我就以“用 PINNs 解基本的偏微分方程”为切入点,带你从原理推导到代码实现,再到我踩过的坑,完整走一遍。我自己是从传统有限差分/有限元那边转过来的,所以文中我会频繁拿这两种方法和 PINNs 作对比,帮助有数值计算基础、但刚接触神经网络的读者更快定位。

给你一个速览:这篇内容适合两类人。第一类是做传统 CFD、固体力学、热传导的工程师/研究生,想看看神经网络能不能当一个新的 PDE 求解器;第二类是熟悉深度学习框架,但没碰过连续介质方程的人,想了解怎么把物理约束塞进训练目标。如果你两边都不熟,也没关系,我会从损失函数怎么设计、网络怎么选、数据集怎么组织开始讲,尽量做到每一步都有据可依。

1. PINNs 的基本原理:把物理方程变成神经网络的损失函数

1.1 从拟合数据到拟合物理规律

传统深度学习做的是“输入到输出”的映射拟合,比如输入图像、输出类别,本质是学习一个函数 f(x)=y,并且依赖大量标注好的样本。而 PINNs 换了个思路:我们用神经网络来表示偏微分方程的解 u(x),其中 x 是时空坐标,u 就是该坐标下的物理量(温度、速度、位移等),然后通过损失函数强制这个 u 满足物理规律。

这里的物理规律不是以标签形式出现的,而是以方程残差的形式出现的。什么意思?以最简单的热传导方程为例。

∂u/∂t - α·∂²u/∂x² = 0

假设神经网络输出的解是 u_net(x,t),我们知道精确解 u_true 必须满足上面的偏微分方程。那么只要把 u_net 代入这个方程,如果网络输出是正确解,方程左边就应该等于 0;如果不等于 0,这个差值就叫 PDE 残差,记作 r(x,t)。我们的目标就是通过优化让这个残差尽可能接近 0,这样在方程约束的整个时空域上,网络输出就逼近真实解了。

这就是 PINNs 的核心:不需要传统意义的数据集标签,而是把偏微分方程本身当成监督信号。这也是为什么它叫 Physics-Informed,物理信息直接嵌入到了损失函数里,而不是事后校正。

1.2 损失函数的三座大山

一个标准的 PINNs 损失函数通常由三部分组成:

L = L_IC + L_BC + L_PDE

L_PDE 是内部点上的物理方程残差,L_IC 是初始条件损失,L_BC 是边界条件损失。从权重比例的角度看,前两项和最后一项是相互制衡的——纯数据项约束边界和初值,纯物理项约束内部行为。

具体怎么算?我们下文以稳态泊松方程为例展开。这里先建立直觉:这个损失函数不是一次性样本全扔进去训练就完事,而是内部点在每个训练 step 都重新采样,边界面也要保证足够多的采样,否则训练出来的解在边界附近会失真。

1.3 自动微分才是幕后支撑

这里必须提一下自动微分(Automatic Differentiation,AD)。传统数值方法(有限差分、有限元)要计算导数,要么用网格离散逼近(有限差分),要么用弱形式积分(有限元)。而 PINNs 依赖深度学习框架的自动微分,直接对网络输入求导。

PyTorch 里一句torch.autograd.grad(outputs, inputs, grad_outputs)就能对输入坐标求导,高阶导数也只要连续嵌套即可。这个能力彻底改变了我们处理微分方程的方式——你不需要像有限差分那样担心网格步长截断误差,导数的精度直接由网络表达能力和训练充分程度决定。代价是什么?计算图的内存开销很大,尤其是二阶导数,这是后期性能优化的主要瓶颈。

2. 用逻辑推演挑方程:为什么要用一个有解析解的简单方程

2.1 教学案例的选择标准

在我实际跑 PINNs 之前,很多人建议我直接上流体(Navier-Stokes)或波动方程。我全部劝退,原因是这类复杂方程一旦训练失败,你根本分不清是代码 bug、网络结构问题,还是方程本身的挑战性太高。

所以我强烈建议:第一次玩 PINNs,务必选一个已知精确解的简单方程,比如一维泊松方程。它的形式很简单、解是光滑函数、能找到解析解做对比,而且边界条件类型相对直观。

一维泊松方程标准形式:

d²u/dx² = f(x),x ∈ [0, 1]

这里 u 是待求解的物理场,f 是源项。我选一个非常温柔的例子:f(x) = -π²·sin(πx),精确解就是 u_exact = sin(πx),边界条件 u(0)=0,u(1)=0。

这个例子好在哪儿?首先解是光滑的,神经网络理论上没有“硬要拟合间断”的痛苦;其次解析解简单,方便每个 epoch 后直接算 L2 误差对比。等你把这个例子跑通,并且理解每个 loss 的含义,再上复杂方程才有底气。

2.2 为什么 PINNs 对简单问题反而不一定赢

这里我要泼一盆冷水:对于这种一维简单线性椭圆方程,传统有限元、有限差分在毫秒级就能达到高精度,PINNs 在速度和精度上都讨不到便宜。那为什么还要学?

因为 PINNs 的优势不在“标准正问题”,而在于三类场景:一是方程形式复杂或求解域不规则,传统网格生成极难;二是反问题(从部分观测数据反推方程系数);三是高维或参数化方程,一个网络要同时求解一族解。理解这一点,你就不会盲目迷信 PINNs,也会更清楚它适合干什么。

我的建议是:入门阶段就把 PINNs 当做一个“带物理约束的回归模型”来学,在简单问题上验证机制的可行性,但心里要清楚它离取代工业级求解器还早得很。

3. 数据集的构建与采样策略:PINNs 的数据集其实长这样

3.1 内部点、边界点、初始点

做传统深度学习时,数据集是固定的:图片读进来、预处理、分 batch。PINNs 则完全不同,数据集大多是“实时生成”的。每个训练 step 我们都会在计算域内用某种采样方法生成一组新的坐标点,这些点也就是训练样本。

这些样本分为三类:内部点(domain points)、边界点(boundary points)、初始点(initial points)。对于稳态问题,没有初始点,只有前两类。每次迭代,网络在这些点上计算输出,并把对应的 PDE 残差和边界残差反馈到损失函数中。

这里有个容易被忽略的问题:边界点是真实数据约束,而内部点完全依赖 PDE 残差。如果边界点采样过少,网络会“自由发挥”,导致边界附近解发生偏移。如果内部点太少,PDE 残差的约束不够,解在中间区域可能偏离。所以采样密度和分布策略非常重要。

3.2 采样策略:随机采样还是拉丁超立方?

第一版我偷懒,直接用torch.rand均匀随机采样内部点,60% 的训练时间在边界附近出现非物理振荡。后来换了拉丁超立方(Latin Hypercube Sampling,LHS),边界附近的振荡大幅减少。

原因很好理解:拉丁超立方能让采样点在每个维度上覆盖更均匀,不会出现随机聚团的问题。PyTorch 自带的torch.rand在高维空间会有比较明显的分布不均问题。这里提供一个简单的 LHS 实现逻辑:把每个维度划分为 N 个等概率区间,从每个区间随机取一个代表点,再随机配对组合成 N 个样本点。

如果你实在不想自己写,也可以用pyDOEsobol_seq这样的库。我个人的习惯是,一维问题直接用torch.linspace加少量抖动,效果就很稳;二维及以上才需要认真考虑 LHS 或 Sobol 序列。

3.3 不同点集是不是要分 batch?

我的做法是:内部点、边界点分别占据独立的 batch。每个 step 里同时采样出N_f个内部点和N_b个边界点,分别计算损失后再加权合并。这个设计的优势是可以灵活调整权重,比如初期边界 loss 占比大一点,让网络先学会边界条件,后期再把 PDE 残差权重提上去,逼迫网络内部满足物理方程。

这里有个所谓“学习率退火”(Learning Rate Annealing)的思想展露雏形:通过动态调整不同损失项的权重,让训练更稳定。后面在训练技巧小节我会仔细展开。

4. 实操:完整跑通一维泊松方程 PINNs

4.1 网络结构选型和初始化

很多人一上来就堆多层 MLP,我建议三到四层全连接就够,然后多做对比实验。以我最终定型的结构为例:输入维度 1(就是坐标 x),隐藏层 4 层,每层 50 个神经元,激活函数用 tanh。输出维度 1(解 u)。

为什么用 tanh?因为 PINNs 的损失函数里有高阶导数,tanh 是光滑无限可微的,而且它的二阶导数也比较稳定。ReLU 这类分段线性激活函数虽然收敛快,但二阶导恒等于 0,用在二阶 PDE 上等于直接把 PDE 残差打没了,完全不可用。

初始化上我踩过一个坑:默认的 Xavier 初始化在 PINNs 上容易导致初始残差过大,梯度一上来就爆炸。我的做法是在第一层加一个线性缩放,把输入映射到 [-1,1],然后直接用默认初始化就行。实际上对于 tanh,Xavier 初始化 + 坐标归一化到 [-1,1] 配合得比较好。

4.2 损失函数详拆:PDE 残差具体怎么写

这一小节是整个 PINNs 的门面,我直接上伪代码的数学表达式。假设网络输出 u_net(x),我们要构造三部分损失。

L_PDE = 1/N_f · Σ (d²u_net/dx² - f(x))²

L_BC = 1/N_b · Σ [(u_net(x=0) - 0)² + (u_net(x=1) - 0)²]

L_total = L_PDE + w_BC · L_BC

实际操作中,d²u_net/dx² 是通过自动微分实现的。这里我提供核心 PyTorch 代码片段:

import torch import torch.nn as nn class PINN(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Linear(1, 50), nn.Tanh(), nn.Linear(50, 50), nn.Tanh(), nn.Linear(50, 50), nn.Tanh(), nn.Linear(50, 50), nn.Tanh(), nn.Linear(50, 1) ) def forward(self, x): return self.net(x) def pde_residual(model, x_f): x_f = x_f.clone().requires_grad_(True).float() u = model(x_f) # 一阶导 u_x = torch.autograd.grad(u, x_f, grad_outputs=torch.ones_like(u), create_graph=True)[0] # 二阶导 u_xx = torch.autograd.grad(u_x, x_f, grad_outputs=torch.ones_like(u_x), create_graph=True)[0] # 源项 f(x) = -pi^2 * sin(pi * x) f = -torch.pi**2 * torch.sin(torch.pi * x_f) residual = u_xx - f return (residual**2).mean()

这一段代码是 PINNs 的核心中的核心。注意create_graph=True这一行,因为我们要对二阶导继续反向传播优化网络参数,必须保留计算图。

4.3 训练循环与完整可复现配置

下面我给出一个完整可跑的简单循环框架,从数据采样到参数更新,尽量避免多余的封装,方便你看清楚每一步在干什么:

import torch.optim as optim model = PINN() optimizer = optim.Adam(model.parameters(), lr=1e-3) # 固定的边界点 x_boundary = torch.tensor([[0.0], [1.0]], dtype=torch.float32) u_boundary = torch.tensor([[0.0], [0.0]], dtype=torch.float32) # 内部点采样数量 N_f = 2000 N_b = 50 for epoch in range(5000): # 每次迭代重新采样内部点 x_f = torch.rand(N_f, 1) * 1.0 # [0,1] # 边界损失 u_b_pred = model(x_boundary) loss_bc = ((u_b_pred - u_boundary)**2).mean() # PDE 残差损失 loss_pde = pde_residual(model, x_f) # 加权合并 loss = loss_pde + 50.0 * loss_bc optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 500 == 0: print(f'Epoch {epoch}, Loss_PDE: {loss_pde.item():.6f}, Loss_BC: {loss_bc.item():.6f}')

训练结束后,你可以在 [0,1] 上均匀取点,像下面这样对比解析解:

with torch.no_grad(): x_test = torch.linspace(0, 1, 200).reshape(-1, 1) u_pred = model(x_test).reshape(-1) u_exact = torch.sin(torch.pi * x_test).reshape(-1) l2_error = torch.norm(u_pred - u_exact) / torch.norm(u_exact) print(f'相对 L2 误差: {l2_error:.4f}')

我实测下来,训练 3000 步左右相对 L2 误差可以降到 1e-3 量级,5000 步后能稳定在 5e-4 上下。注意这只是一个一维光滑问题,这个误差水平并不算惊艳——但作为机制验证,足够了。

4.4 选择边界权重的直觉

上面代码里我把边界 loss 权重设成了 50。为什么是 50 而不是 1 或 1000?

我做过一组对比实验,权重=1 时边界误差一直降不下去,解在两端明显上翘;权重=1000 时边界倒是准了,但内部 PDE 残差长期偏高,解的形状整体偏平。权重=50 左右,边界和内部能保持相对较好的平衡。

这背后有个朴素道理:边界点数量远少于内部点(50 vs 2000),如果不加权,边界约束的信息量被内部点淹没了。你可以把每个边界点看成“一票顶几十票”的硬约束,这正是物理先验的强引导作用。

5. 训练细节与优化技巧:残差修正与损失平衡

5.1 训练初期怎么快速收敛

用 Adam + 固定学习率 1e-3 是最省心的组合,但训练后期会碰到一个问题:loss 降到一个平台后就非常缓慢了。这时候我一般会在 3000 步左右切换到 L-BFGS。

L-BFGS 是个拟牛顿法优化器,在 PINNs 这种“小规模数据集、高精度要求”的任务上意外地好用。它不需要手动调学习率,而且能利用二阶信息,收敛速度比 Adam 后期快很多。我第一次用 L-BFGS 直接把 L2 误差从 5e-4 拉到 1e-5 量级,相当惊艳。

具体切换方式也很简单,PyTorch 中直接用optim.LBFGS(model.parameters(), lr=1),然后需要包一个closure函数:

def closure(): optimizer.zero_grad() u_b_pred = model(x_boundary) loss_bc = ((u_b_pred - u_boundary)**2).mean() loss_pde = pde_residual(model, x_f) loss = loss_pde + 50.0 * loss_bc loss.backward() return loss optimizer.step(closure)

但 L-BFGS 对采样点比较敏感,如果你每个 step 都重新采样内部点,L-BFGS 的梯度估计会不平稳,所以用 L-BFGS 时我建议先把内部点固定住,跑几十步稳定后再重新采样。

5.2 “残差修正”到底改的是什么

这里我想专门聊一下“PINNs 残差修正”这个热词。很多人以为残差修正是指训练过程中调整网络权重,其实更常见的意思有两层。

第一层是损失函数层面的残差修正:比如给 PDE 残差配一个自适应权重,让训练早期侧重边界,中后期侧重内部。这就是我之前提到的学习率退火思路的雏形,可以用梯度统计信息自动调整每个损失项的权重。

第二层是预测结果层面的修正:训练完一个 PINNs 后,在部分高置信点上做一个后处理校准,比如用少量已知解的数据点(如边界或实验测点)做一个浅层修正网络。这在某些反问题场景非常实用,因为 PINNs 训练出的解在局部区域可能有偏差,用测量数据修正比重新训练整个网络快得多。

如果你是在做工程应用而不是论文复现,我强烈建议在“全网训练”之后加第二个轻量修正阶段。我自己在一次热传导反问题中,只用了 20 个实测测温点做残差修正,反演精度就提升了一个数量级。这比盲目堆网络层数和训练步数划算得多。

5.3 到底要不要分阶段训练?

我试过两种策略:一是从一开始就让边界损失和 PDE 损失同步优化,二是分段训练:先只训边界和初始条件,固定住后再训 PDE 残差。

实测结论:分段训练在复杂问题上有用,在简单问题上反而拖慢收敛。原因是简单问题的边界和 PDE 两者相容性很好,同步优化不会打架。但复杂问题(比如流体)里,网络如果先拟合了边界的某个形状,再被 PDE 残差强行拉回,就会来回振荡,训练特别不稳定。

所以我的建议是:先用同步训练跑 100 轮看 loss 曲线,如果发现边界 loss 和 PDE loss 交替上升(像跷跷板一样),再切换成分段训练。

6. PINNs 的常见翻车现场与排错指南

6.1 问题一:loss 下降但解明显错误

这种“假收敛”我遇到太多次了。loss 可能在千分之一量级,但画出来的解曲线完全不是想象的样子。原因往往是这三点:内部点采样太少、网络容量不足、或微分计算有误。

优先检查自动微分。比如我早期写高阶导时忘了加create_graph=True,梯度传不回去,loss 看起来在下降,实际上 PDE 残差根本没回传到网络参数,纯属表面功夫。这个 bug 不打印每个分项 loss 根本发现不了。

另一个排查手段是把 PDE 残差的分布打印出来,画个直方图。如果残差集中在某个区域特别大,那大概率是采样密度不够或网络在局部欠拟合。

6.2 问题二:边界附近出现“振铃”现象

边界附近的非物理振荡,尤其是用随机采样时特别明显。解决方式我刚才提过,换 LHS 或者 Sobol 序列。还有一种思路是加大边界点数量,甚至直接把边界点重复采样几次强行强化。

另外一个容易被忽略的设置是激活函数。如果你换成了 Swish 或 GELU,它们虽然有平滑性,但某些情况下二阶导的传播不稳定,振铃更严重。我目前用下来,tanh 在大多数二阶 PDE 问题上是最稳妥的选择。

6.3 问题三:训练后期 loss 平台期过长

这个问题的常见原因有学习率过低、优化器退化为随机梯度噪声主导、以及采样点更新太频繁。我的典型做法是:先固定采样点上 L-BFGS 精调,如果还卡住,就检查梯度范数是不是出现梯度消失。

PINNs 有个特有现象:由于残差项涉及高阶导数,网络越深,梯度在反向传播时就越容易消失。我试过一个 8 层网络,不仅在性能上没有提升,反而训练到后期几乎学不动。所以如果你遇到平台期,不一定要加深网络,可以尝试增加宽度、换优化器、或调整采样密度。

6.4 常见问题速查表

现象可能原因解决措施
loss 下降但解不对微分计算有误 / 采样不足打印分项 loss,加大内部点数量
边界附近振荡随机采样分布不均改 LHS 或 Sobol 序列
边界 loss 居高不下边界权重太小增大边界损失权重或边界点数量
PDE loss 降不下去网络容量不足 / 学习率过大加宽度、换 L-BFGS
训练后期平台期优化器效率低切到 L-BFGS 固定采样点精调
梯度消失网络过深减少层数、增加单层宽度

7. 如何进阶:PINNs 的适用边界与扩展方向

7.1 什么时候别用 PINNs

我必须把这个说清楚:如果你只需要在常规几何、简单边界条件下解一次 PDE,用传统有限元/有限差分/谱方法,又快又准。我的习惯是“能用传统方法解决的问题,绝对不上 PINNs”。这不是因为 PINNs 不行,而是成本和收益不成正比。

PINNs 真正的价值在于反问题、参数化问题、高维问题,以及正问题里边界非常复杂或存在未知物理机制的场景。在这些场景里,你没法轻易生成网格或格式良好的矩阵,但神经网络天然擅长处理“端到端”的非结构化映射。

7.2 PINNs 后续可以怎么扩展

如果你已经跑通本文这个最小案例,下一步可以从几个方向扩展。

第一个方向是把方程换成含时间项的非线性 PDE,比如 Burgers 方程或 KdV 方程,此时需要额外采样初始点,并且输入维度变成 (x,t),采样策略也要设计成时空联合采样,不同时间层需要覆盖完整空间域。

第二个方向是做反问题,比如通过部分观测点反推扩散系数 α。做法很简单:把 α 设为一个可训练参数,训练时不仅优化网络权重,还优化 α,loss 里再加上观测数据项。

第三个方向是参数化 PDE,即网络输入除了 (x,t) 还加入参数 μ,训练一次网络,就能预测不同参数下的解。这个方向相当实用,相当于在离线阶段做一次昂贵训练,在线阶段做实时推理。

我个人的建议是:先把本文的一维案例反复吃透,尤其是残差计算、采样策略、损失权重这三块,再去碰上述扩展方向。很多人在复杂模型上翻车,追根溯源还是这三个基本功没扎实。

7.3 学习 PINNs 的长期心态

说实话,PINNs 目前还不是一个“开箱即用”的工业级工具,论文里漂亮的结果背后往往藏着大量调参和计算资源。但你别因此就否定它的价值。任何一个新范式在早期都是这样的:概念亮眼,工程欠打磨。

我在实际项目里更多是把 PINNs 当作一个“物理先验增强的回归器”来用——不是去和有限元抢精度,而是用来做快速探索、参数反演、以及在传统方法难以建模的地方提供一个可微的替代方案。带着这个定位去学,你会少很多焦虑。

最后分享一个我自己的习惯:每解锁一个新的 PINNs 变体(比如加入注意力机制、用 CNN 处理高维输入、或者做多保真度融合),我都会先回到这个一维泊松方程上做 sanity check。基础模型永远是你排查复杂问题时的标尺。别小看这个“入门级玩具”,它能在你后面被各种论文坑得焦头烂额时,帮你找回直觉。

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

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

立即咨询