简介:这份PDF综述面向数学、力学与深度学习交叉领域的研究者及研究生,系统梳理了基于深度学习的偏微分方程智能求解方法。内容以是否求解单一问题为判定依据,将方法划分为神经算子与类物理信息神经网络(PINN)两大类:前者针对具有相同数学特征的PDE问题,从数据驱动与物理约束两个角度展开分析;后者面向单一问题,介绍基于数据优化、模型优化和领域知识优化的三种改进思路,并探讨基于传统离散方程与无网格非离散的物理驱动求解方案。资源包为单个PDF文件,约2.64MB,内容涵盖技术路线总结、现有研究不足与未来方向建议,并附有智能求解程序发展现状。已有207人学习,适合希望快速把握PDE智能求解研究脉络、明确选题方向或补充方法对比的读者参考。
1. 神经算子加类物理信息网络:一条把 PDE 求解从“逐题重训”里捞出来的路
做仿真的人大概都经历过这种憋屈:同一个偏微分方程,换个边界条件、换个初始场、换个几何区域,传统数值求解器就得从头再算一遍,网格剖分、时间步进、迭代收敛,一套流程走下来,算力烧掉一大截。更别提那些要做参数扫描、反问题、实时控制的场景,动辄几百上千次重复求解,纯靠传统方法根本扛不住。这两年“基于神经算子与类物理信息神经网络智能求解”这个方向被反复提起,本质上就是在回答一个问题:能不能学一个解算子,让它对一整族 PDE 都管用,而不是只对某一个实例管用。
神经算子(Neural Operator)干的就是这件事——它不学“某个方程在某组参数下的解”,而是学“从参数到解的映射”本身。类物理信息神经网络(类 PINN)则是在损失函数里塞进物理约束,让网络在数据稀疏甚至无标签的情况下也能逼近真解。两者结合,一个负责泛化到不同参数、不同分辨率,一个负责把物理规律当正则项压住解空间。适合谁?做流体、传热、电磁、结构仿真的工程师,做反问题或实时孪生系统的算法同学,以及想从传统数值方法往 AI for Science 迁移的开发者。下面我按“先立住概念、再跑通最小例子、再讲参数和坑”的顺序,把这条路拆开讲。
2. 神经算子与类 PINN 到底在学什么:从函数到函数的映射
2.1 传统 PINN 的边界:为什么换个参数就得重训
传统 PINN 的做法是:用一个神经网络 (u_\theta(x,t)) 去逼近某个具体 PDE 的解,损失函数由三部分组成——方程残差、边界条件残差、初始条件残差。训练完之后,这个网络只对当前这组参数、这个几何、这个边界条件有效。你改一个入口速度,或者把圆管换成方管,网络输出立刻失效,必须重新训练。
这就是“逐题重训”的根源:PINN 学的是解函数 (u(x,t)),而不是解算子 (\mathcal{G}: a \mapsto u)。对于需要大量重复求解的场景,比如优化设计、不确定性量化、实时控制,逐题重训的时间成本完全不可接受。我见过一个传热反问题的例子,用传统 PINN 做参数扫描,单次训练 20 分钟,扫 50 组参数就是十几个小时,还没算调参的时间。
2.2 神经算子:一次学习,多实例泛化
神经算子的核心思想是把映射关系从“有限维向量到有限维向量”提升到“函数空间到函数空间”。输入是一个函数(比如初始条件、材料系数场、边界条件),输出也是一个函数(解场)。DeepONet 用分支网络编码输入函数、主干网络编码查询坐标,两者点积得到解;FNO(傅里叶神经算子)则在频域里做线性变换,利用傅里叶模态截断实现分辨率无关的泛化。
关键区别在于:训练一次之后,同一个算子网络可以对同一族 PDE 的不同实例直接推理,不需要重新训练。比如训练时见过 100 组不同的入口速度,推理时给一个没见过的速度,网络能直接输出对应的流场。这就是“智能求解”里“智能”二字的落脚点——不是替你做一次计算,而是替你做一族计算。
2.3 类物理信息神经网络:把物理约束当正则项
“类物理信息”这个说法,指的是不完全依赖标注数据,而是把 PDE 残差、守恒律、对称性等物理先验嵌进损失函数。和传统 PINN 的区别在于,这里的物理约束是加在算子学习框架上的,而不是单独训练一个解网络。
常见做法是:算子网络输出解场后,用自动微分计算方程残差,和边界条件残差一起构成物理损失;同时如果有少量数值解或实验数据,再加一个数据损失。总损失是两者的加权和。这样做的价值在于:纯数据驱动的算子网络在训练数据覆盖不到的区域容易发散,物理约束相当于给解空间加了一个“护栏”,让外推更稳。
提示:物理损失的权重不是越大越好。权重过大,网络会优先满足方程残差而忽略数据,导致解偏离真实观测;权重过小,物理约束形同虚设。一般从 1e-3 到 1e-1 之间调,具体看方程量纲。
3. 用 DeepONet 加物理损失跑通一维 Burgers 方程的最小实现
3.1 问题设定与数据生成
一维 Burgers 方程是算子学习里的“Hello World”:
[ u_t + u u_x = \nu u_{xx}, \quad x \in [0,1], \quad t \in [0,1] ]
初始条件 (u(x,0) = a(x)) 作为算子输入,输出是 (u(x,t))。黏度 (\nu) 固定为 0.01。用数值求解器生成 200 组不同初始条件的解作为训练数据,每组在 64 个空间点、50 个时间点上采样。
import numpy as np from scipy.integrate import solve_ivp # 生成一维 Burgers 方程的数值解 def burgers_rhs(t, u, nu, dx): u = u.reshape(-1) dudx = np.gradient(u, dx) d2udx2 = np.gradient(dudx, dx) return -u * dudx + nu * d2udx2 def generate_data(n_samples=200, nx=64, nt=50, nu=0.01): x = np.linspace(0, 1, nx) dx = x[1] - x[0] t_eval = np.linspace(0, 1, nt) data = [] for _ in range(n_samples): # 随机初始条件:几个正弦波的叠加 a = np.zeros(nx) for k in range(1, 4): a += np.random.uniform(-1, 1) * np.sin(2 * np.pi * k * x) sol = solve_ivp(burgers_rhs, [0, 1], a, t_eval=t_eval, args=(nu, dx), method='RK45') data.append(sol.y.T) # shape: (nt, nx) return np.array(data), x, t_eval这段代码用solve_ivp做时间推进,空间导数用中心差分。n_samples控制训练集大小,nx和nt是空间和时间采样点数。初始条件用 1 到 3 个正弦波的随机叠加,保证多样性。生成的数据形状是(n_samples, nt, nx),后面要拆成输入函数和输出函数。
3.2 DeepONet 网络结构与物理损失
DeepONet 分两支:分支网络吃初始条件 (a(x)),主干网络吃查询坐标 ((x,t)),输出是两者的点积。
import torch import torch.nn as nn class DeepONet(nn.Module): def __init__(self, branch_in=64, trunk_in=2, hidden=128, p=64): super().__init__() self.branch = nn.Sequential( nn.Linear(branch_in, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), nn.Linear(hidden, p) ) self.trunk = nn.Sequential( nn.Linear(trunk_in, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), nn.Linear(hidden, p) ) self.bias = nn.Parameter(torch.zeros(1)) def forward(self, a, xt): # a: (batch, nx), xt: (batch, n_points, 2) b = self.branch(a) # (batch, p) t = self.trunk(xt) # (batch, n_points, p) out = torch.einsum('bp,bnp->bn', b, t) + self.bias return out分支网络输入维度是空间点数 64,主干网络输入是 ((x,t)) 二维坐标。p是潜在维度,一般取 64 到 128。forward里用einsum做点积,输出形状是(batch, n_points)。
物理损失的计算需要自动微分:
def physics_loss(model, a, xt, nu): xt.requires_grad_(True) u = model(a, xt) # 对 xt 求导得到 u_t, u_x, u_xx grads = torch.autograd.grad(u.sum(), xt, create_graph=True)[0] u_t = grads[..., 1] u_x = grads[..., 0] u_xx = torch.autograd.grad(u_x.sum(), xt, create_graph=True)[0][..., 0] residual = u_t + u * u_x - nu * u_xx return torch.mean(residual ** 2)这里xt的最后一维是 ((x,t)),所以grads[..., 0]是 (u_x),grads[..., 1]是 (u_t)。u_xx通过对 (u_x) 再求一次导得到。残差的均方值就是物理损失。注意create_graph=True必须开,否则二阶导没法回传。
3.3 训练循环与关键参数
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = DeepONet().to(device) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=2000, gamma=0.5) data, x, t = generate_data() a_train = torch.tensor(data[:, 0, :], dtype=torch.float32).to(device) u_train = torch.tensor(data, dtype=torch.float32).to(device) # 构造查询坐标网格 X, T = np.meshgrid(x, t) xt_grid = torch.tensor(np.stack([X.ravel(), T.ravel()], axis=-1), dtype=torch.float32).to(device) xt_batch = xt_grid.unsqueeze(0).repeat(a_train.shape[0], 1, 1) lambda_phys = 1e-2 # 物理损失权重 for epoch in range(5000): model.train() optimizer.zero_grad() u_pred = model(a_train, xt_batch).reshape(-1, len(t), len(x)) loss_data = torch.mean((u_pred - u_train) ** 2) loss_phys = physics_loss(model, a_train, xt_batch, nu=0.01) loss = loss_data + lambda_phys * loss_phys loss.backward() optimizer.step() scheduler.step() if epoch % 500 == 0: print(f"Epoch {epoch}, data loss {loss_data.item():.6f}, " f"phys loss {loss_phys.item():.6f}")lambda_phys是物理损失权重,这里取 1e-2。学习率 1e-3,每 2000 步衰减一半。训练 5000 步在单卡上大概十几分钟。u_predreshape 成(batch, nt, nx)是为了和数据损失对齐。物理损失在每个 batch 上单独算,注意xt_batch的requires_grad要在physics_loss里重新设置,因为repeat之后梯度状态会丢。
注意:如果显存不够,把
n_samples降到 50,或者把xt_batch分块计算物理损失。物理损失的计算图比数据损失大得多,是显存的主要消耗方。
4. 参数怎么设、模型怎么选:FNO 与 DeepONet 的取舍
4.1 分辨率泛化:FNO 的频域截断优势
FNO 的核心是在频域做线性变换,只保留前 (k) 个傅里叶模态。这个设计让它天然具备分辨率无关性:训练时用 64 个空间点,推理时用 128 个点,网络结构不用改,直接跑。DeepONet 的分支网络输入维度固定,换分辨率就得改网络或做插值。
如果你的场景需要跨分辨率泛化,比如训练用粗网格、推理用细网格,FNO 更合适。但 FNO 对非周期边界条件的处理比较麻烦,需要额外做 padding 或加边界网络。DeepONet 对边界条件的适应性更好,因为主干网络可以直接吃边界坐标。
4.2 物理损失权重与训练稳定性
物理损失权重 (\lambda_{\text{phys}}) 的调节是这类方法里最“玄学”的部分。我的经验是:先只用数据损失训练 500 步,让网络大致拟合数据分布,再逐步把物理损失权重从 0 加到目标值。直接上大权重容易导致训练初期梯度爆炸。
另一个技巧是对物理损失做归一化。不同方程的残差量纲差异很大,Burgers 方程的残差可能在 1e-2 量级,而 Navier-Stokes 的残差可能在 1e2 量级。归一化方式可以是除以残差的滑动平均值,或者除以方程各项的典型量级。
4.3 训练数据量与算子泛化边界
算子网络的泛化能力高度依赖训练数据的多样性。如果训练集的初始条件都是低频正弦波,推理时给一个高频或局部突变的初始条件,网络大概率翻车。我一般会做两件事:一是用随机场生成器(比如高斯随机场)生成多样化的输入函数;二是在训练集里故意留出一些“极端”样本做验证,比如最大振幅、最陡梯度的情况。
数据量方面,一维问题 200 组通常够用,二维问题至少 500 组起步,三维问题目前还是难点,数据需求和算力都上了一个数量级。
5. 避坑与排查:那些训练日志不会告诉你的翻车现场
5.1 物理损失不降反升:检查自动微分的坐标顺序
现象:训练日志里数据损失正常下降,物理损失从某个 epoch 开始震荡上升。原因:xt的最后一维坐标顺序搞反了,把 ((x,t)) 写成了 ((t,x)),导致 (u_t) 和 (u_x) 互换,残差计算完全错误。解决:打印grads[..., 0]和grads[..., 1]的数值范围,和数值解的 (u_x)、(u_t) 做对比,确认坐标顺序。
5.2 推理时输出全为零:检查分支网络的输入归一化
现象:训练损失降到很低,但推理时给新初始条件,输出几乎全零。原因:分支网络的输入没有做归一化,训练集初始条件的均值和方差与推理时差异大,网络饱和到零输出。解决:对分支网络输入做标准化,减均值除标准差,推理时用训练集的统计量做同样的变换。
5.3 显存溢出:物理损失的计算图太大
现象:训练几个 batch 后显存爆掉。原因:物理损失对每个样本的每个查询点都做二阶自动微分,计算图规模是数据损失的几倍。解决:把物理损失的 batch 分块,每次只算一部分样本的物理损失,累积梯度后再更新。或者降低查询点数量,物理损失不需要和所有数据点对齐。
5.4 边界条件不满足:物理损失里漏了边界项
现象:解在区域内部拟合得很好,但边界处误差大。原因:物理损失只包含了方程残差,没有加边界条件残差。解决:在损失函数里显式加边界点的残差项,边界点的采样密度可以比内部点高一些,比如边界点占 20%。
5.5 训练后期 loss 震荡:学习率衰减太慢
现象:loss 降到一定程度后开始上下震荡,不收敛。原因:学习率在后期太大,优化器在最优解附近跳来跳去。解决:用余弦退火或阶梯衰减,把学习率降到 1e-5 量级。如果还震荡,检查物理损失权重是否过大,适当降低。
6. 进阶技巧:用课程学习把物理损失权重从 0 调到目标值
课程学习(Curriculum Learning)在这类方法里特别好用。核心思路是:先让网络在“简单”任务上收敛,再逐步增加难度。具体到物理损失权重,可以设计一个从 0 到目标值的线性或余弦调度:
def get_lambda_phys(epoch, warmup=1000, target=1e-2): if epoch < warmup: return target * epoch / warmup return target前 1000 步让数据损失主导,网络先拟合数据分布;之后物理损失权重线性增加到目标值,物理约束逐步介入。这样训练稳定性比固定权重好很多,尤其对于强非线性方程。
另一个技巧是“残差自适应采样”:在物理损失大的区域多采查询点,物理损失小的区域少采。实现方式是每训练 N 步,用当前网络算一遍残差场,按残差大小做加权采样。这个做法对激波、边界层这类局部高梯度区域特别有效。
验证方法上,我习惯留一组“外推测试集”:训练时用的初始条件振幅范围是 ([-1,1]),测试时用 ([-1.5,1.5]) 的样本。如果外推误差比内插误差大一个数量级以内,说明物理约束起了作用;如果大两个数量级以上,说明网络在死记硬背训练数据,物理损失权重需要调大。
最后说个血泪教训:别一上来就怼二维 Navier-Stokes。先用一维 Burgers 把整个流程跑通,确认数据生成、网络结构、物理损失、训练调度都没问题,再往高维推。我见过太多人直接上二维,调了两周还在和显存、梯度爆炸搏斗,最后连一维都没跑通。从最小可复现的例子开始,每一步都验证,比什么都重要。希望帮到你。
本文还有配套的精品资源,点击获取