简介这份基于PINN物理信息网络求解微分方程的Python学习包聚焦深度学习与科学计算的交叉应用适合想要掌握PINN建模思路、熟悉常见微分方程数值解法的开发者、研究生及高年级本科生。包内共22个文件包括17个可直接运行的Jupyter Notebook案例、3个Python核心脚本、1份Markdown说明文档和1张结果可视化图整体压缩包仅889KB轻量易下载。案例覆盖欧拉-伯努利梁、扩散方程、拉普拉斯方程、泊松方程、常微分方程组、洛伦兹系统等典型问题从一维到多维从定解条件到方程系统均有涉及配套脚本按照PINN流程拆分为几何区域处理、PDE方程定义、网络模型搭建等模块结构清晰便于二次开发。截至目前已有2973人学习下载适合用于课程项目、论文复现或作为从零搭建PINN架构的参考帮助读者理解损失函数设计、训练策略与误差评估等关键环节。1. 为什么PINN不用网格也能解微分方程一个做流体仿真的朋友去年和我抱怨说他在一个带复杂边界的模型上画网格画了两周网格质量始终不达标求解器算出来的压力场在拐角处全是伪振荡。这个场景里传统数值方法的核心瓶颈不是求解器而是网格生成——而PINN物理信息网络Physics-Informed Neural Network给出的解法是干脆不要网格。用神经网络直接拟合微分方程的解把方程本身塞进损失函数里当作软约束这就是PINN在2024年“杀疯了”的原因。本质上PINN解决的是两类人的问题一类是工程师手里有成熟方程但边界形状不规矩网格成本高于求解成本另一类是科研人员方程形式明确但参数不确定想用少量观测数据把方程和反演参数一起解出来。本文用Python和PyTorch从零搭一套可运行的PINN流程覆盖损失函数设计、训练调参、误差验证和工程化技巧适合已经熟悉Python语法但没碰过PINN的读者也适合想摆脱“玄学调参”的从业者。方向定在PINN而不是深度算子网络是因为PINN对微分方程约束的利用最直接、最容易被读代码的人理解。2. PINN的核心机制网络输出、自动求导与残差损失2.1 物理信息网络的损失函数结构数据、方程与初边值三项加权PINN的做法可以用一句话概括用一个全连接神经网络 $\hat{u}(x,t;\theta)$ 作为微分方程解 $u(x,t)$ 的替代模型神经网络的输入是自变量空间坐标、时间输出是待求物理场。网络的结构通常是多层感知机激活函数在回归问题里几乎默认选双曲正切因为解的导数要进损失函数而tanh的任意阶导数都存在且光滑ReLU的一阶导是分段常数二阶导直接为零没法用于二阶方程的残差计算。方程约束怎么进入网络把 $\hat{u}$ 代入原方程左右两边会产生一个差值这个差值就是方程残差。以Burgers方程 $u_t u u_x - \nu u_{xx} 0$ 为例残差定义为$$r(x,t) \hat{u}_t \hat{u},\hat{u}x - \nu,\hat{u}{xx}$$如果网络输出的解是精确的残差处处为零。所以训练目标就是让残差在求解域内尽量接近零这就是“物理信息”的含义——不是把方程结果当标签而是把方程本身当监督信号。完整损失函数由三部分组成方程残差损失、边界条件损失、初值条件损失有观测数据时再加第四项数据损失$$L L_r \lambda_b L_b \lambda_i L_i \lambda_d L_d$$其中 $L_r$ 是在求解域内随机采样的残差点的平均平方残差$L_b$ 和 $L_i$ 分别约束边界和初值$\lambda$ 是权重。这里有个新手上路最容易犯的错只盯着残差损失训不设初边值权重网络确实能让残差变小但解会整体漂移因为方程只约束“导数关系”不约束“绝对位置”。合理的做法是先单独跑几个小epoch观察各项loss的量级再反推权重让三项在初始阶段处于同一个数量级这个技巧在后面调参章节展开。2.2 自动微分PINN区别于差分法的核心差异PINN里所有导数都不是用有限差分算的而是用PyTorch的autograd自动求导。原因有两个第一自动微分基于链式法则逐层回传精度到机器精度不像差分法有截断误差第二求解域内任意一点都能直接求导不需要网格拓扑。用PyTorch对网络输出求二阶导的标准写法是import torch def compute_derivatives(model, x, t): x x.clone().requires_grad_(True) t t.clone().requires_grad_(True) u model(torch.cat([x, t], dim1)) u_t torch.autograd.grad(u, t, grad_outputstorch.ones_like(u), create_graphTrue)[0] u_x torch.autograd.grad(u, x, grad_outputstorch.ones_like(u), create_graphTrue)[0] u_xx torch.autograd.grad(u_x, x, grad_outputstorch.ones_like(u_x), create_graphTrue)[0] return u, u_t, u_x, u_xx逻辑上分三层第一层对输入x和t显式声明requires_grad_(True)只有这样后续对它们求导才有轨迹第二层计算u关于t和x的一阶导时必须传create_graphTrue因为后续还要对一阶导再求导不开启这个标志计算图会在第一次求导后被释放第三层二阶导是对u_x再求一次x方向的梯度不是对u直接求二次梯度思路是在一阶导结果上继续走链式法则。这里有一个能省很多内存的参数设置求u_t和u_x时如果二者互相独立后算的那个不需要保留之前的计算图可以把retain_graph设为False默认就是False但上面代码中二者用了同一个计算图第二次调用autograd.grad时前一个图的梯度已经被释放所以不会冲突。实际训练时如果显存吃紧把x和t分开传网络、分别求导是更省显存的做法代价是网络前向计算两次。对比维度PINN传统有限差分/有限元解的表达形式连续函数任意点可直接取值离散节点值插值得到中间点网格依赖不需要网格只需采样点强依赖网格质量导数计算自动微分机器精度差分近似有截断误差边界条件处理以软约束加入损失函数直接融入线性方程组高维扩展性维度增加不影响网络结构网格数量随维度爆炸表格最后一行是PINN在高维问题上的卖点但代价是训练时间通常远高于传统单次求解。实际工程里PINN的定位不是完全替代求解器而是做代理模型或者反问题参数辨识这个定位直接影响后面选网络和调参的策略。2.3 最小网络该多大隐藏层宽度与深度的起点PINN网络设计没有公式可套但从业者一般从3到5个隐藏层、每层50到100个神经元起步。层数太浅函数表达能力不够残差损失降不下去层数太深梯度消失和训练不稳定接踵而至。选60×3或80×4这种规模就够处理一维和二维问题。输入输出的归一化是网络能不能训动的先决条件空间和时间坐标全部缩放到[-1, 1]区间物理量的取值如果跨越多个数量级也先做归一化再进网络否则高阶导数的梯度很容易溢出为NaN。3. 用PyTorch实现PINNBurgers方程的最小可复现代码3.1 问题设定与采样策略残差点、边界点、初值点的生成逻辑用Burgers方程做测试案例是PINN领域的事实标准因为它同时包含一阶非线性项和二阶粘性项能暴露大多数训练问题。方程写在区间 $x \in [-1, 1]$、$t \in [0, 1]$ 上取粘性系数 $\nu 0.01 / \pi$初值 $u(0,x) -\sin(\pi x)$边界条件采用周期性边界 $u(t,-1) u(t,1)$。采样策略分三块残差点在 $x$-$t$ 平面内用均匀随机采样采样数一般取10000到20000个初值点和边界点各取1000到2000个边值点在两条竖边上随机撒初值点在 $t0$ 这条横边上随机撒。边界采样数不必多但覆盖要均匀因为边界条件本质是一条约束线残差是面约束。所有采样点每一轮都重新生成会比固定一批好原因是重新采样让网络无法“记住”特定点这属于最简单的退化抗性处理后面章节里讲到自适应采样时再深化。3.2 完整训练代码Adam粗训加L-BFGS精修import torch import torch.nn as nn import numpy as np class PINN(nn.Module): def __init__(self, layers[2, 50, 50, 50, 1]): super().__init__() self.activation nn.Tanh() self.linears nn.ModuleList() for i in range(len(layers) - 1): self.linears.append(nn.Linear(layers[i], layers[i 1])) if i len(layers) - 2: nn.init.xavier_uniform_(self.linears[-1].weight) def forward(self, x): for linear in self.linears[:-1]: x self.activation(linear(x)) return self.linears[-1](x) def burgers_residual(model, x_r, t_r, nu): u, u_t, u_x, u_xx compute_derivatives(model, x_r, t_r) return u_t u * u_x - nu * u_xx def train_pinn(num_iters15000): torch.manual_seed(42) model PINN() optimizer torch.optim.Adam(model.parameters(), lr1e-3) nu 0.01 / np.pi for it in range(num_iters): # 每轮重新采样 x_r torch.rand(10000, 1) * 2 - 1 # [-1, 1] t_r torch.rand(10000, 1) # [0, 1] x_b (torch.rand(1000, 1) * 2 - 1) * 0 torch.tensor(1.0) t_b torch.rand(1000, 1) x_i torch.rand(1000, 1) * 2 - 1 t_i torch.zeros_like(x_i) # 三类损失 loss_r torch.mean(burgers_residual(model, x_r, t_r, nu) ** 2) u_left model(torch.cat([torch.full_like(t_b, -1.0), t_b], dim1)) u_right model(torch.cat([torch.full_like(t_b, 1.0), t_b], dim1)) loss_b torch.mean((u_left - u_right) ** 2) u_ic model(torch.cat([x_i, t_i], dim1)) loss_i torch.mean((u_ic torch.sin(np.pi * x_i)) ** 2) loss loss_r loss_b loss_i optimizer.zero_grad() loss.backward() optimizer.step() if it % 1000 0: print(fiter {it:6d} | loss_r {loss_r.item():.3e} f| loss_b {loss_b.item():.3e} | loss_i {loss_i.item():.3e}) # L-BFGS精修 optimizer_l torch.optim.LBFGS(model.parameters(), lr1.0, max_iter50) def closure(): optimizer_l.zero_grad() x_r torch.rand(10000, 1) * 2 - 1 t_r torch.rand(10000, 1) loss_r torch.mean(burgers_residual(model, x_r, t_r, nu) ** 2) loss loss_r loss.backward() return loss optimizer_l.step(closure) return model参数说明网络层结构[2, 50, 50, 50, 1]表示输入2维x和t、三个隐藏层每层50个神经元、输出1维。Adam阶段学习率1e-3是PINN的常用起点低于1e-4收敛过慢高于1e-2残差损失极易发散。L-BFGS阶段用max_iter50做精修它的优势是二阶近似在损失曲面较平滑时能比Adam多降一个数量级的残差但它对batch的随机性很敏感所以closure里重采样的点每步都在变实际中更稳妥的做法是用固定一批验证点来跑L-BFGS。周期性边界条件通过比较左右两端网络输出实现没有直接让网络结构保证周期性所以边界损失权重如果太小边界处会出现可见的不连续。3.3 训练过程怎么看三项loss各自的行为特征正常训练时观察打印输出loss_r的前1000轮会从10的零次方级别迅速降到10的负三次方这是网络先拟合方程的大尺度平滑结构loss_b和loss_i在前5000轮会来回抖动因为Adam的学习率对这几个小样本分支偏大。如果loss_r降得很快但loss_b长期不降说明边界采样的梯度被残差点的梯度淹没了解法在下一章讲权重分配。如果loss在训练中打印出NaN优先检查nu的取值和坐标归一化Burgers方程里nu的数值是0.003183而u的典型量级是0到1方程里u * u_x和nu * u_xx的量级如果差到三个数量级以上二阶导项在反向传播时梯度特别容易爆炸。这类问题的处理方式是给方程各项分别开损失而不是把残差统一平方这一招在工程代码里经常用得着。4. PINN不收敛的10个调试方向权重、采样与训练策略PINN这个领域有个反直觉的现象损失函数一路降到10的负五次方画出来的解却和真实解对不上。原因是PINN的损失曲面高度非凸网络完全可能出现“方程成立、边界也成立但解是另一个分支”的局部最优。把调试方向按经验从高概率到低概率排列能省下大量无头绪试错的时间。4.1 从学习率到激活函数前5个常规检查项学习率先降后升。PINN的初始损失值通常很大残差项的量级在10到100之间这个区间用Adam的默认学习率1e-3是合理的。如果print出的loss_r在5000轮后还在缓慢爬升把学习率降到3e-4同时把每个维度的梯度打印出来看是否有个别梯度过大。激活函数换tanh。换成ReLU之后二阶导恒为零方程残差直接变成“0 0 - 0”类型的问题网络无法感知方程约束这是一个必踩的坑想保ReLU系的非线性又想要光滑导数可以用SiLU即swish但它对初始化更敏感收敛不如tanh稳定。权重初始化。nn.init.xavier_uniform_是配合tanh的默认选择换成Kaiming初始化会让tanh层输出方差过大前向传播时接近饱和区梯度消失来得更快。坐标归一化。输入范围超过[-1, 1]越远tanh的饱和区越早被激活一阶导和二阶导同时趋近于零残差损失传不回梯度。把时间除以最大值、空间减去中点再除以半宽是几乎所有PINN工程的标配。去掉BatchNorm或LayerNorm。标准化层在批量样本之间引入依赖而PINN的“样本”是独立采样点标准化后的输出分布随batch扰动训练曲线会出现周期性的尖峰。4.2 损失权重失衡用梯度幅度做动态调节初边值损失和残差损失在数量级上的竞争是PINN不收敛的头号原因。残差点在10000个量级初边值点只有1000个所以残差梯度天然占主导。一个工程级的解决思路是用梯度范数做动态权重每500轮计算一次三类损失对网络参数的梯度范数如果某类损失的梯度范数比其他类小两个数量级就把它的权重乘以2。一个简化实现def update_weights(model, loss_r, loss_b, loss_i, weights, lr0.1): grads [] for loss in [loss_r, loss_b, loss_i]: grads.append(sum(p.grad.norm().item() ** 2 for p in model.parameters() if p.grad is not None) ** 0.5) mean_grad sum(grads) / len(grads) for i in range(3): if grads[i] 0.1 * mean_grad: weights[i] * (1 lr) return weights逻辑说明每个loss分支都保留计算图、单独反向传播后再取梯度的范数梯度范数小说明这个约束对网络的影响弱权重放大后该项在总损失里占比上升。注意这里没有做归一化权重可能越涨越多所以实际用时要带上限或者把三个权重归一化到和为固定值否则训练后期权重失衡会反过来压垮残差损失。动态权重的前提是三个loss分支能分别反向传播实现时不要让三个loss相加后统一backward应按分支顺序分别backward(retain_graphTrue)或者用register_hook截取各层梯度这与第一节里求二阶导的create_graph同理都是控制计算图生命周期的问题。4.3 采样密度不均残差点的分布比数量更关键均匀随机采样在稳态问题里够用但输运类方程的解会在特定区域出现陡峭梯度比如激波。如果残差点在陡峭区域的密度不够网络会用光滑函数强行拟合突变残差损失看似不大局部误差却很高。常见做法是先用粗训练找出残差绝对值大的点然后把新一批采样点按残差大小加权生成def resample_with_residual(model, n_samples, bounds, nu): 根据当前残差分布生成密度不均匀的采样点 n_probe n_samples * 5 x_probe torch.rand(n_probe, 1) * (bounds[1] - bounds[0]) bounds[0] t_probe torch.rand(n_probe, 1) with torch.no_grad(): r burgers_residual(model, x_probe, t_probe, nu).abs().squeeze() p r / r.sum() idx torch.multinomial(p, n_samples, replacementTrue) return x_probe[idx], t_probe[idx]说明先密集撒一批探测点计算每个点的残差绝对值按残差分布做带权重的随机抽样残差大的区域自然被多采。这个过程每2000轮触发一次等价于把网络注意力动态集中到还没学会的区域。代价是有偏采样会让损失值整体偏大所以对比收敛进度时要用固定测试集上的误差不能看训练loss本身。症状可能原因首选调整全部loss不降学习率过高或输入未归一化降到3e-4检查坐标范围loss_r降但loss_b不动边界权重太小将边界损失权重乘10loss降到1e-5但解错初边值约束失效或局部最优恢复初边值权重重采样训练早期出现NaNnu等系数未归一化方程系数整体缩放10000轮后loss_r缓慢下降损失曲面平坦换L-BFGS精修预测解有锯齿状波动网络过参数化或采样点太少减宽度提高残差采样数边界区域误差远大于内部边界硬约束缺失引入边界硬约束见第6章更换种子后结果差异大初始化敏感多跑几个种子取中位结果训练曲线周期性尖峰每轮重新采样导致的噪声增大batch到20000或固定采样点4.4 优化器组合与训练长度什么时候该认输PINN训练有一个不成文的经验Adam训到loss_r不再下降后的前5000轮是“性价比最高”的阶段后面每降一个数量级要翻倍的时间。如果Adam 30000轮后loss_r还在10的负四次方以上瓶颈往往不是优化器而是网络容量或采样分布。此时先扩网络宽从50加到80再不行检查方程系数是否做了归一化。标题是“使用Python”实操里换TensorFlow或DeepXDE这类专业库不在本文范围但了解深水区后用封装库能减少大量模板代码前提是理解本文的底层原理。5. 用解析解验证PINN精度误差度量与诊断方法PINN没有“训练集”和“测试集”的概念模型在整个求解域上都是连续函数所以验证策略也完全是另一套。面对一个没有解析解的方程新手很容易陷入“loss已经够小”的自我欺骗。面对真实问题的替代方案不是没有你可能没被你考虑到的可行的替代方案。5.1 用Poisson方程做解析验证相对L2误差的计算在动手处理工程方程之前先用一个带解析解的问题校准整个流程。以Poisson方程 $-u_{xx} f(x)$$x \in [0, 1]$取 $f(x) \pi^2 \sin(\pi x)$边界条件 $u(0) u(1) 0$解析解为 $u(x) \sin(\pi x)$。把网络训练完成后在测试网格上计算预测值与解析解的相对L2误差def relative_l2_error(model, x_test, u_exact): 相对L2误差衡量预测整体与真解的偏离 u_pred model(x_test).squeeze() u_exact u_exact.squeeze() return torch.norm(u_pred - u_exact, p2) / torch.norm(u_exact, p2) x_test torch.linspace(0, 1, 1000).reshape(-1, 1) u_exact torch.sin(np.pi * x_test) err relative_l2_error(model, x_test, u_exact) print(f相对L2误差: {err.item():.3e})相对L2误差是把两个解的差向量的模除以解析解的模消除了解本身量级带来的偏差。一个调好的PINN在这个问题上应当达到1e-3量级的相对L2误差如果误差在1e-1量级说明边界条件权重偏低或者网络训练轮数不足。这个方法在PINN的验证协议中的地位相当于机器学习里的留出验证集之所以不用交叉验证是因为PINN的每次训练动辄几万步交叉验证的成本不切实际。5.2 误差诊断残差分布比单一误差值信息量大解析解能拿到的时候可以画出误差随空间的分布看它是否集中在边界、初始时刻或者某个特殊区域。常见的诊断结论误差集中在边界附近说明边界损失权重不够误差在某一小片连续区域过高说明该区域残差采样密度不足误差分布呈现高频振荡说明网络宽度过大过拟合了噪声。对于没有解析解的实际问题改用事后验证方程残差在各处的绝对值分布是否接近均匀初边值条件的误差落在哪个数量级5.3 四个症状对照表这个loss曲线还能不能继续训把验证结果和训练loss曲线的形态放在一起看比只看单一数值有效。loss_r曲线在20000轮后仍然以近似线性的斜率缓慢下降说明训练还没充分继续加大轮数有意义loss_r曲线已经水平但没有达到预期精度优先检查初边值权重而非加轮数loss_r快速下降后出现反弹多半是采样点重采样后的分布发生剧烈变化把重采样周期加大或者改用确定性采样训练末期loss_i突然变大这是Adam在平坦区域处的震荡引起的用L-BFGS收尾可以解决。6. 四个进阶技巧让PINN快速落地到实际方程6.1 坐标归一化先于一切PINN对输入量纲极其敏感写出第一个残差函数之前先把坐标线性映射到[-1, 1]区间时间同理。一个简单写法是x_norm (x - x_mid) / (x_span / 2)。这一步能避免大部分NaN问题也是让L-BFGS阶段稳定收敛的前提。6.2 从软约束升级到硬约束边界条件直接写进网络结构前面已经把边界条件作为损失项加入了训练其本质是软约束网络能逼近边界值但未必精确满足。对于边界条件简单的问题可以直接修改网络输出形式让边界条件从结构上被自动满足。例如一维Dirichlet边界 $u(0)A$、$u(1)B$把网络输出 $\hat{u}$ 替换为def hard_constraint_output(x, net_out): 线性插值满足两个端点网络只修正内部形状 u_boundary (1 - x) * A x * B return u_boundary x * (1 - x) * net_out这个处理的好处是边界点上的误差被强制清零训练可以把原本分给边界损失的计算资源全部用于残差损失。代价是边界形式越复杂构造约束函数的难度越高在矩形域上还可以用乘积形式处理多个边界在不规则边界上建议仍用软约束两者之间存在一个“结构复杂度”和“训练效率”的取舍。6.3 用固定测试集记录真实误差PINN的训练损失不能直接对比因为每次采样都是随机点集损失值包含采样噪声。工程上习惯单独生成一组固定的大规模测试点比如10000个均匀网格点每5000轮在这组固定点上计算一次相对L2误差或残差平均值记录成日志。固定测试集上的误差曲线比训练损失的收敛曲线能更早暴露出过拟合也能在更换随机种子后保持结果可对比。6.4 训练交给自适应采样去纠偏真实工程里没有时间手动微调权重把4.3节的重采样逻辑封装成每N轮自动运行一次的函数后可以直接放到训练循环里一个PINN能否落地差别常常不在网络结构而在残差点的选择策略这一点贯穿PINN从学术原型到工业使用的全过程。本文还有配套的精品资源点击获取