ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PINN代码实战:从Burgers方程到自建PDE模型的完整拆解

PINN代码实战:从Burgers方程到自建PDE模型的完整拆解 简介面向深度学习与物理模拟学习者的物理信息神经网络PINNPython代码合集系统对应哔哩哔哩《PINN》课程第30讲。资源整合了第18次课至第29次课的14个.py脚本覆盖热扩散、波传播等经典物理场景并分别给出硬约束与软约束的边界处理实现代码分段清晰、注释完整便于对照课程逐讲练习和复习可帮助学习者快速上手PINN建模、训练与结果分析加深对损失函数和物理约束的理解。另附3个GIF动态图直观呈现heat_diffusion与wave_propagation等模拟的演化过程让网络输出与物理过程的对应关系一目了然便于观察物理场随时间的动态变化。整个压缩包仅2.6MB、共17个文件轻量便携适合工程师、研究人员和学生下载使用。目前已有61人学习配合原课程视频讲解与讨论区可系统掌握PINN在物理现象模拟中的应用细节与算法实现。1. 拿到“30讲配套代码”先别急着跑先搞懂这套PINN代码在帮你解决什么一个准备入坑 PINN 的人最容易卡死的地方不是理解物理约束而是下载了配套代码不知道从哪个文件开始跑。这套“PINN 哔哩哔哩课程·30讲配套代码汇总”里代码按讲次散落在几十个文件夹里真正动手时面对的是一个黑匣子模型定义在哪、损失函数在哪、跑完怎么把解画出来全靠自己翻。这篇笔记专门解决这个问题——先按训练骨架、方程定义和输出可视化把整套代码拆开再给出一条可以照着敲的最小训练命令最后把训练过程里最常见的几类翻车现场和验证方法讲透。适合用 PINN 做论文复现、课题预研或者想把自己的偏微分方程交给神经网络去求解的人。2. PINN课程30讲代码的整体结构先看哪几个文件、怎么把散装讲次拼起来2.1 从代码汇总里找主线训练脚本、模型定义、方程参数三件套我拿到这类按讲次打包的代码汇总第一件事不是看视频而是先把所有 .py 文件按文件名过一遍。30讲听起来很多但绝大多数文件只是“换个方程、换组边界条件”的变体骨架是同一套。常见的目录组织方式是这样PINN-course-code/ ├── 01_intro/ # 第一次课自动微分与简单函数拟合 │ └── train.py ├── 02_burgers/ # Burgers 方程完整案例 │ ├── model.py │ ├── pde_residual.py │ └── train.py ├── 03_heat/ # 热传导方程 ├── 04_wave/ # 波动方程 ├── shared/ │ ├── utils.py # 网格生成、归一化、结果保存 │ └── plotting.py └── environment.yml如果你手里的包没有这么整齐也不需要慌。不管目录怎么散一个能跑的 PINN 项目一定包含三个文件训练脚本、模型定义、残差/损失函数定义。训练脚本负责组装采样点和训练循环模型定义就是那个把坐标映射到解的神经网络残差函数是这个项目真正区别于普通深度学习代码的地方——它把偏微分方程写成了可微的损失项。找主线时先定位三个关键函数forward、compute_residual或者带pde、loss字样的函数、train。找到之后按“输入坐标 → 输出预测 → 自动求导得到导数 → 拼出 PDE 残差 → 加边界初值损失”的顺序读一遍整个课件的脉络就出来了。这个做法也适用于其他语言写的 PINN 代码MATLAB 里对应找predict、modelLoss和主循环即可。2.2 示例代码讲解一个最小可跑的 PINN 骨架先分清前向与损失30讲里前几讲通常会从最简单的“用网络拟合一个函数”讲起后面才进入方程求解。这里给一个最小骨架对应的是“用 PINN 求解单个稳态方程”的公共部分后面所有讲次的代码基本都是在这个骨架上改import torch import torch.nn as nn class PINN(nn.Module): def __init__(self, in_dim2, hidden64, layers4): super().__init__() self.net nn.Sequential() for i in range(layers): in_f in_dim if i 0 else hidden self.net.append(nn.Linear(in_f, hidden)) self.net.append(nn.Tanh()) self.net.append(nn.Linear(hidden, 1)) # 输出解 u def forward(self, x, t): # 输入是空间坐标和时间形状都是 [N, 1] xt torch.cat([x, t], dim1) return self.net(xt)这个模型的输出是标量解 (u(x,t))没有显式的物理层物理约束全部通过损失函数织进去。训练循环里的大头不是反向传播而是“怎样用自动微分把方程残差算出来”这也是和普通神经网络训练最不一样的地方def pde_loss(net, x, t, nu0.01): x.requires_grad_(True) t.requires_grad_(True) u net(x, t) 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] # Burgers 方程u_t u*u_x - nu*u_xx 0 residual u_t u * u_x - nu * u_xx return torch.mean(residual ** 2)逻辑说明x和t是模型输入requires_grad_(True)是为后续求一阶、二阶导做准备create_graphTrue是因为残差还要再对网络参数求导不能把求导图断开。residual的物理含义是“当前网络输出距离满足方程还差多少”对残差做平方均值就成了损失。这套写法在 30 讲里反复出现只是中间的算子项会随方程变化。参数说明hidden是隐藏层宽度layers是隐藏层个数。经典 Burgers 题目用 4 层 × 64 神经元足够换复杂方程时优先加深到 6 层不要一上来加宽度。nu是方程里的黏性系数如果你的问题没有这个参数删掉对应项即可。2.3 把30讲散落代码整理成统一入口用脚本收敛到一条命令不同讲次的代码往往写着不同的训练入口有的要传数据文件有的直接内置初值。我会在整理代码时做一个“统一入口”脚本把每个讲次包装成一个可复用的接口这样后面复现、改方程都只需要改一处# run_case.py import importlib CASES { 02_burgers: (train_burgers, config_burgers), 03_heat: (train_heat, config_heat), } def main(name: str): if name not in CASES: raise ValueError(funknown case: {name}) train_mod, config_mod CASES[name] config importlib.import_module(fcases.{config_mod}).config trainer importlib.import_module(fcases.{train_mod}).get_trainer(config) trainer.run() if __name__ __main__: main(sys.argv[1])这段代码只做“导入-配置-运行”三件事不改任何训练逻辑。好处是把 30 讲原来分散的启动方式收敛成一条命令python run_case.py 02_burgers后面跑新方程时只需要往CASES里加一行不用为一个新讲次复制整个工程。代码整理的另一个重点是统一config结构。很多讲次里的超参数散落在脚本各处我一般统一成字典config { domain: {x: [0.0, 1.0], t: [0.0, 1.0]}, n_collocation: 10000, n_boundary: 200, n_initial: 200, epochs: 20000, lr: 1e-3, loss_weights: {pde: 1.0, bc: 10.0, ic: 10.0}, }这里的loss_weights是 PINN 最值得调的参数后面第 4 章会专门展开。整理完入口和配置后“30讲代码”就从一堆零散脚本变成了一个有目录、有配置、可替换方程的实验仓库。3. 跑通最小训练命令在本地把第一讲案例完整搭起来3.1 最小训练命令从配置到权重一次跑完大部分讲次会给一个可运行的入口但缺失依赖、路径写错是常见问题。最小可跑的训练命令通常是这种形式python train_burgers.py --config configs/burgers.yaml --epochs 20000 --device cuda如果代码里没有--config这种参数说明配置写在文件头部的全局变量里直接改头部变量即可。我这里推荐先跑经典 Burgers 方程案例因为它是课程里最标准的算例网络结构、采样策略、损失权重都有默认值跑通了再换其他方程。运行前先确认环境中已经有 torch、numpy、matplotlib。缺少依赖时不要一次装很多先跑一遍看报错缺什么补什么。终端里出现两种输出就算正常一是每 100 轮打印一次的 loss 数值二是训练结束后生成的solution.png或.npy文件。如果 2000 轮以内 loss 没有任何下降不用怀疑直接跳到第 5 章排查。3.2 训练循环里的四个必看细节学习率、采样点、归一化、权重初始化PINN 的“代码诊断”不像分类任务看准确率主要看训练循环里这几件事做得对不对。这里是我常用的训练循环写法optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20000) for epoch in range(epochs): # 1. 每个 epoch 重新采样配点避免网络记住固定点 x_colloc, t_colloc sample_domain(10000) x_bc, t_bc, u_bc sample_boundary(200) x_ic, t_ic, u_ic sample_initial(200) loss_pde pde_loss(model, x_colloc, t_colloc) loss_bc boundary_loss(model, x_bc, t_bc, u_bc) loss_ic initial_loss(model, x_ic, t_ic, u_ic) loss 1.0 * loss_pde 10.0 * loss_bc 10.0 * loss_ic optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() if epoch % 500 0: print(fepoch{epoch} pde{loss_pde.item():.4e} fbc{loss_bc.item():.4e} ic{loss_ic.item():.4e})这里有四个细节决定成败。第一配点每轮重新采样让网络在整段时空域上被持续约束而不是只背下固定点坐标。第二边界和初值损失的权重设为内部的 10 倍这是多数算例的默认经验能有效压低边界漂移。第三输入坐标最好先归一化到 [-1, 1] 或 [0, 1]否则二阶导在自动求导时会被放大或缩小很多训练发散是常事。第四学习率配合余弦退火从 1e-3 降到 1e-5PINN 的损失曲面比较陡固定学习率容易在一个平台期反复横跳。这四个细节在课程代码里可能不是最优值但把“玄学”变成可控项的关键就在这。换方程时优先调loss_weights和学习率换网络结构时才调层数和宽度顺序不能反。3.3 如果要在 MATLAB 里搭建 PINN与 PyTorch 代码的对应关系网上经常有人问“MATLAB 怎么搭建 PINN”课程 30 讲如果主推 Python你在 MATLAB 里搭的时候依然可以用同样的思路只是自动求导接口变成了dlgradient。核心步骤是三步定义网络、写损失函数、用dlgradient求残差和梯度。这是一段对应代码function [loss, grad] modelLoss(net, x, t) % 网络输出 u u forward(net, x, t); % 对 x 求一阶导和二阶导 u_x dlgradient(u, x); u_xx dlgradient(u_x, x); u_t dlgradient(u, t); % PDE 残差u_t u*u_x - nu*u_xx 0 residual u_t u .* u_x - 0.01 .* u_xx; loss mean(residual.^2, all); % 对网络参数求梯度 grad dlgradient(loss, net.Learnables); end与 PyTorch 的对应关系我整理成一张表任务PyTorchMATLAB网络前向net(x, t)返回[N,1]forward(net, x, t)返回dlarray一阶导数torch.autograd.graddlgradient二阶导数对一阶导再求一次grad同样对一阶导再求组装残差直接在张量上做运算注意. *逐元素乘参数更新optimizer.step()sgdmupdate/adamupdateMATLAB 版踩坑主要在数据格式输入要用dlarray包一层且坐标默认形状是[特征数, 样本数]和 PyTorch 的[样本数, 特征数]相反。如果你习惯了 PyTorch 写法在 MATLAB 里最容易在forward之后维度对不上报错。4. 把课程示例改成自己的方程替换PDE、边界与初值的三个修改点4.1 修改点一方程残差函数改的就是物理约束本身把课程里的 Burgers 方程换成你自己的方程第一步不是改网络结构而是改残差函数。比如把 Burgers 方程换成二维对流扩散方程[ \frac{\partial u}{\partial t} a\cdot\nabla u \nu\nabla^2 u f(x,t) ]残差函数改成这样def convection_diffusion_residual(net, x, y, t, a_x, a_y, nu, source): x.requires_grad_(True) y.requires_grad_(True) t.requires_grad_(True) u net(x, y, t) u_t grad(u, t) u_x grad(u, x) u_y grad(u, y) u_xx grad(u_x, x) u_yy grad(u_y, y) # 对流项 扩散项 源项 residual u_t a_x * u_x a_y * u_y - nu * (u_xx u_yy) - source(x, y, t) return torch.mean(residual ** 2)这里的source如果是依赖坐标的源项函数比如热源 (f\sin(\pi x)\cos(\pi y))直接传给source(x, y, t)即可。换方程时最容易漏的是“符号”每一项的符号错一个网络就会努力去拟合一个根本不存在的解而且表面上看 loss 也能降下来因为它在用网络容量硬凑残差。参数说明a_x、a_y是对流速度分量nu是扩散系数。如果你的方程没有对流项删掉a_x * u_x和a_y * u_y没有源项删掉source。改完之后先做一项 sanity check把网络输出固定成一个常数残差应该等于-f在采样点上的值如果对不上说明残差函数里丢了项。4.2 修改点二边界与初值采样损失权重怎么设换方程后边界条件往往不是简单的常数值。代码里常见的是把边界约束直接写成均方误差def boundary_loss(net, x_bc, t_bc, u_bc): u_pred net(x_bc, t_bc) return torch.mean((u_pred - u_bc) ** 2) def initial_loss(net, x_ic, t_ic, u_ic): u_pred net(x_ic, t_ic) return torch.mean((u_pred - u_ic) ** 2)采样策略有讲究。边界点不能只采样均匀网格推荐在边界附近加密初值点则要在初始时刻t0附近整段空间上采样否则模型会“遗忘”初始条件训练后期完全被内部残差带跑。损失权重的经验是bc和ic的权重比 PDE 残差高一个数量级但也不要超过 100 倍权重过大会让网络优先满足边界内部解完全失真。我一般从{pde: 1.0, bc: 10.0, ic: 10.0}起步再根据训练曲线微调。如果边界条件是周期性边界或 Neumann 边界boundary_loss里不能直接用u_pred - u_bc要改成预测值在边界上的导数约束。课程代码里通常会在注释里标明“Neumann”字样换到这类边界时记得连损失函数一起换只改数据不改损失是常见翻车点。4.3 修改点三网络输出与坐标缩放3个必调参数换方程后网络不收敛多半不是网络太浅而是输入输出尺度没处理好。PINN 对尺度极其敏感因为残差里混合了不同阶的导数坐标范围差一个量级二阶导就可能差两个量级。我一般必调这三个参数config { coord_scale: 2.0, # 把物理坐标映射到 [-1, 1] output_scale: 1.0, # 解的幅值缩放大幅值问题用 init_std: 0.01, # 最后一层权重初始化标准差 }归一化在数据进入网络前做def normalize(x, x_min, x_max): return 2.0 * (x - x_min) / (x_max - x_min) - 1.0output_scale适用于解的量级本身很大比如位移量级到 1e3的问题这时在网络输出后乘上output_scale让网络先在 O(1) 尺度上学习。最后一个必调参数是最后一层初始化的标准差很多 PINN 翻车是因为最后一层权重初始太大导致初始预测在边界处离真实解太远惩罚项一开始就爆表。常见做法是把最后一层初始化标准差压到 0.01这是课程代码里不会讲但非常关键的一个点。这三个参数按顺序调先归一化输入再调输出尺度最后看情况改初始化。大多数单位不匹配的问题都能在这三步里解决。5. PINN训练常见问题与排查从“loss不动”到“全是零解”的四条避坑记录5.1 现象loss 在前 2000 轮几乎不降或者某一步突然跳到 NaN这是 PINN 新手遇到最多的一个现场。原因通常有两个一是学习率过大导致残差在高阶导数的放大下直接振荡二是坐标没有归一化输入范围超过网络激活函数的敏感区间梯度在反向传播时消失或爆炸。解决路径先把学习率降到 3e-4 重跑。如果还跳 NaN检查坐标是否做了归一化再检查残差函数里有没有出现torch.sqrt或torch.abs这类不可导操作。一般改完这三处loss 会重新回到平滑下降。另外训练到一半 loss 突然变成inf多半是学习率配合余弦退火时在后期步长仍然过大建议把T_max改成epochs而不是默认值。5.2 现象训练结束输出全是 0 或者一条平直线这个“零解”问题最容易误判成网络结构不行。统计上看原因是初始条件和边界条件约束太强PDE 残差约束太弱网络发现“输出全 0”就能把边界和初值损失压到很小——因为你的边界值恰好是 0或者源项太小。解决方法是重新平衡损失权重把pde的权重从 1.0 提到 10.0同时把边界初值的权重从 10.0 降到 1.0。另一个办法是检查源项source(x, y, t)是否真的在采样点上有非零值有些源项写成了0那网络学出来的“全零解”实际上是残差最小解系统没有给它足够的驱动力去产生非零结构。5.3 现象训练耗时翻倍显存也涨了一大截一个很容易忽略的坑是训练循环里反复调用网络前向导致自动求导图叠加。比如计算u_xx时如果代码里写的是u_x grad(net(x, t), x)然后又调一次net(x, t)网络前向图就被重复构建了一次。正确的姿势是只调用一次net把u存下来所有导数都从这同一个u出发。检查方式是看torch.autograd.grad调用的第一个参数是不是同一个张量。另外create_graphTrue是必需的但用完之后记得把x的requires_grad关掉再进入下一轮否则图会越积越大。5.4 现象换一个方程就发散或者同一套代码跑三次结果差很多这通常不是代码 bug而是训练配置对问题太敏感。PINN 的损失曲面里有很多局部极小初始权重不同会有明显差异。我一般固定一个种子先跑通再换三个种子看稳定性。如果换种子之后解完全不一样优先怀疑配点采样数量不够把n_collocation从 10000 提到 20000 通常能压住方差。如果换方程后从第一步就发散回到第 4.3 节检查输入归一化和输出缩放新方程的坐标范围和解幅值很可能和课程示例差了一个量级。5.5 现象脚本启动直接失败报错里带“启动失败代码2”这类报错大多是环境问题而不是模型问题。常见原因是工作目录切换后相对路径失效或者 conda 环境里缺少某个安装包。解决办法确认当前目录在工程根目录确认依赖已经装全如果命令行是直接在 Windows 终端里双击运行的改成先在终端conda activate对应环境再执行。代码层面也可以把配置文件的路径改成基于当前文件位置的绝对路径避免从其他目录启动时找不到权重文件。6. 验证一个PINN模型是否真的可信误差场、残差场和“看不见的测试点”6.1 不在训练时空范围里抽点用全空间网格算误差场训练集是随机配点验证时就要故意避开这些点。常见做法是把空间均匀切成 200×200 网格再叠加最优拉丁超立方采样生成一套独立测试集计算预测解与解析解或高精度参考解逐点的相对误差。这里给出我常用的误差场计算代码def compute_error_field(net, x_test, t_test, u_ref): u_pred net(x_test, t_test).detach() abs_err torch.abs(u_pred - u_ref) rel_err abs_err / (torch.abs(u_ref) 1e-8) return abs_err, rel_err这套代码只做前向计算不参与训练能真实反映模型在全空间的行为。误差场的分布比单个 loss 更能说明问题如果误差集中在边界层或激波附近说明配点密度不足如果误差均匀分布且量级偏大说明网络容量不够该加深了。6.2 画出PDE残差场而不是只看损失曲线只看 loss 曲线很容易被骗因为总损失是三项的加权和单项残差可能在训练后期不再下降却被权重压住。我一般把训练结束后模型在固定网格上的 PDE 残差画成热力图残差集中在某个狭窄区域就是典型的“局部过拟合”。可视化代码def plot_residual_field(net, x_grid, t_grid, pde_fn): residual pde_fn(net, x_grid, t_grid).detach() plt.contourf(..., residual, levels50, cmapjet)如果残差场里出现明显的条纹或环形结构说明当前配点策略没有覆盖这些区域需要在这些位置加密采样。如果残差场整体平滑且量级与训练损失相当这个模型的可信度才算真正站住了。6.3 把“损失下降”包装成可复现脚本形成每次训练后必做的检查项我会把验证步骤写成一个固定脚本verify.py每次训练完自动执行。检查项包括检查项通过标准异常时处理训练 loss 曲线最后 1000 轮稳定不上升回看学习率与权重误差场最大相对误差 5%问题相关加配点或加层PDE 残差场无局部尖峰局部加密采样多个随机种子结果解结构一致检查配点数量边界处预测误差边界误差小于内部误差提高边界权重这套验证流程跑完才能回答“这个 PINN 能不能用”这个问题。我自己的习惯是每次训练完先画残差场再写结论看过的翻车案例里loss 好看但误差场一塌糊涂的情况太多了。先确认误差位置再决定改网络还是改采样这条路径比盲目调参可靠得多。希望这套排查和验证流程能帮你在 PINN 的路上少走几步弯路。本文还有配套的精品资源点击获取
返回列表