简介一套覆盖物理信息神经网络PINN离散时间识别、离散时间推理、连续时间识别、连续时间推理方法的Python源码包内含4个独立代码每个方法均配有相应数据与结果文件适合研究科学计算、偏微分方程反演及时间序列建模的开发者与科研人员。包内共559个文件压缩包约476MB包含13个py源码脚本、503个txt日志与说明文本、12个csv误差对比表、7个mat实验数据文件以及pdf和eps格式的结果图、yml配置文件等文件类型覆盖源码、数据、图表与配置组织较完整便于分模块对照查阅。目前已有1546人学习或下载关注度较高。借由源码、数据与误差表可快速复现四类方法在离散与连续时间场景下的识别和推理全过程并利用结果图观察误差演化与参数影响为后续调整网络结构、损失函数及训练策略提供了直观的定量依据和实验基础。1. PINN不是网络架构而是损失函数设计思路PINN物理信息神经网络这些年出镜率很高但第一次接触的人容易把它想成一种新的深度学习骨架。实际上它没有特殊层、没有特殊激活函数核心是损失函数设计把偏微分方程的残差项直接塞进训练目标里让网络不仅拟合观测数据还满足物理规律。这套资源一共四个 python 代码分别对应离散时间识别、离散时间推理、连续时间识别、连续时间推理覆盖了 PINN 最常见的前向求解与参数反演场景。如果你手里有稀疏的时空观测数据想知道背后 PDE 的未知参数或者想在没有完整观测的情况下重建整个场那这套代码就是最直接的参照物。适合已经在跑深度学习、但对物理约束如何落进训练循环还缺一次完整实操的人。2. 离散时间识别把 Runge-Kutta 写进网络反演方程参数2.1 为什么离散时间方向要单独做一版代码连续时间 PINN 的做法是直接把空间坐标和时间坐标拼起来作为网络输入用自动微分求 u_t、u_xx 这些偏导。思路直观但有个实际问题当数据只有几帧离散快照、时间间隔又比较大时让网络自己学习跨大步长的时间演化很容易发散。离散时间方法换个思路把 Runge-Kutta 这类时间积分格式直接嵌进网络结构拿相邻几帧快照做约束网络要学的实质上是「从 t_n 到 t_{n1} 的状态转移」。识别identification和推理inference在这里是两种诉求。推理是正问题已知方程结构和参数求给定初始边界条件下的解场识别是反问题方程结构已知、参数未知要用观测数据把参数反推出来。离散时间识别之所以是四个方向里最常被复现的是因为它把时间步进和参数反演耦合在一起RK 积分器的系数同时参与网络输出和残差计算参数能不能反准直接反映在误差表里那几列 lambda 值上。2.2 RK 离散的最小实现与参数设定以 Burgers 方程 u_t u·u_x − ν·u_xx 0 为例ν 是待识别参数。常见做法是构造一个以 (x, t) 为输入的网络输出 u再把 ν 设成 nn.Parameter让优化器同时更新网络权重和 ν。四阶 Runge-Kutta 的格式要先备好它把一步时间推进拆成四个 stagestageca 系数组合k100k21/2a21 1/2k31/2a31 0, a32 1/2k41a41 0, a42 0, a43 1每个 stage 对应一个中间时刻离散时间 PINN 会在这个时刻上计算 PDE 残差让网络输出满足 RK 积分关系。下面是最小可运行的网络和参数声明import torch import torch.nn as nn class BurgersPINN(nn.Module): def __init__(self, n_hidden4, n_width50): super().__init__() layers [nn.Linear(2, n_width)] for _ in range(n_hidden - 1): layers.append(nn.Linear(n_width, n_width)) layers.append(nn.Linear(n_width, 1)) self.net nn.Sequential(*layers) # nu 作为可训练参数这里就是“识别”的关键 self.nu nn.Parameter(torch.tensor(0.01), requires_gradTrue) def forward(self, x, t): xt torch.cat([x, t], dim1) return self.net(xt) model BurgersPINN() opt torch.optim.Adam([ {params: model.net.parameters(), lr: 1e-3}, {params: [model.nu], lr: 1e-3}, ])逻辑上要注意两件事。第一ν 不能只被当作常数写死在残差公式里必须放进参数组参与反向传播否则梯度根本流不到它身上优化器只会空转。第二学习率拆成两个组是常见的稳健做法因为网络权重和未知参数的量纲、梯度量级可能差很多绑在一起用一个 lr 经常导致 ν 震荡。参数设定上隐层 4 层、每层 50 个神经元是 PINN 论文里的常见配置激活函数默认 tanh因为它对自动微分二阶导更友好ReLU 在求二阶导时会直接退化成 0。训练循环里离散时间识别要比连续时间多一步把 t_n 和 t_{n1} 两帧数据同时喂给网络中间用 RK stage 生成若干辅助时间点在这些点上计算残差并累加进 loss。代码骨架如下def rk4_stages(x, t_n, dt, model): # 四阶 RK 的四个时间偏移 c [0.0, 0.5, 0.5, 1.0] stages [] for ci in c: t_mid t_n ci * dt u_mid model(x, t_mid) # 这里用当前 nu 计算 PDE 残差后续累加到 loss stages.append((t_mid, u_mid)) return stages这里的核心是把 RK 系数当成配点生成器每个 stage 时刻都参与残差计算但最终数据损失只约束 t_n 和 t_{n1} 两帧。你会发现网络没有显式存储时间演化它是靠「所有中间时刻都满足同一个 PDE」这个约束把时间连续性学出来的。dt 的取值很敏感我一般取观测时间间隔的一半作为初始值然后看误差表里的残差列是否持续下降再逐步加大。2.3 从 error_lambda 表判断参数反演是否收敛资源里带着几个 CSV 文件error_table_1.csv、error_table_2.csv 和 error_lambda_1_table_1.csv、error_lambda_2_table_2.csv这些就是训练过程里导出的误差快照。命名规律很清晰带 lambda 的专门记录未知参数 ν 的估计误差不带 lambda 的记录整体解的误差。实际读的时候至少关注三列迭代轮数、PDE 残差 loss、lambda 相对误差。判断收敛不要单看 loss 数值。PINN 训练里 loss 降到很低但参数反错了的情况非常常见因为你可能把数据损失权重调得太低网络学会了「假装满足物理」却牺牲了观测匹配。我一般会同时盯 lambda 相对误差那条曲线它应该先快速下降然后进入小幅波动。如果 lambda 曲线一直在单一方向漂移说明学习率太大或者数据快照太少属于典型的欠约束。3. 连续时间推理自动微分替代 PDE 残差计算3.1 网络输入 (x, t) 到高阶导数的完整链路连续时间方向的代码是四个里最容易理解的版本也是首次接触 PINN 的人最该先跑通的。网络输入是 (x, t)输出是 uPDE 里的时间导数和空间导数全部交给 torch.autograd.grad 去算。这个过程看着简单坑全在细节里第一次求导是 u 对 t 和 u 对 x第二次求导是 u_x 再对 x 求一次两次都必须要 create_graphTrue否则计算图被释放后续的损失回传会直接报错。def pde_residual(model, x, t, nu): x x.clone().requires_grad_(True) t t.clone().requires_grad_(True) u model(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), create_graphTrue)[0] # Burgers 方程残差理想情况下应为 0 return u_t u * u_x - nu * u_xx这段代码里最容易被忽略的是requires_grad_(True)必须作用在输入张量上而不是等 model 输出后再临时设置。如果输入 x、t 本身没有开启梯度自动微分返回的梯度就是 None训练循环会在 loss.backward() 时报一个莫名其妙的 TypeError这种报错最容易让人误判成网络结构问题。nu 在推理场景是固定常数不需要梯度但代码里通常保留成参数方便你在同一套骨架上切换识别模式。3.2 数据点、边界点、内部配点的配置顺序连续时间 PINN 的 loss 由三个部分叠加观测数据损失、边界条件损失、PDE 残差损失。数据点来自稀疏传感器或仿真快照边界点在计算域边缘采样内部配点是随机撒在 (x, t) 域里的点专门用来计算 PDE 残差。这三个来源的采样数量配比直接决定训练成败。常见做法是数据点保持原始观测量边界点采样 100200 个内部配点采样 10000 个左右。配点数量不是越多越好太多会导致单轮迭代变慢太少则约束不足解会偏向纯数据拟合。我一般用拉丁超立方采样替代纯随机均匀采样这样能在同样点数下让配点覆盖更均匀尤其当计算域时间跨度大、数据集中在某一小段时纯随机采样很容易让长时间段出现约束空洞。def sample_interior(n_points, x_range, t_range): x torch.rand(n_points, 1) * (x_range[1] - x_range[0]) x_range[0] t torch.rand(n_points, 1) * (t_range[1] - t_range[0]) t_range[0] return x, t边界点和内部配点分开采样后loss 计算也要分开写。最常见的问题是把所有点混在一起算一个均方误差结果边界约束被海量内部配点稀释边界条件永远学不精确。我的习惯是每个 loss 分量单独乘一个权重系数初始值分别设为数据 1.0、边界 1.0、残差 0.01再根据训练初期的 loss 比例做调整。3.3 识别与推理在损失函数上的三个差异如果你把四个代码都跑一遍会发现识别和推理的损失函数长得很像但有三处关键差异。第一PDE 残差里的 ν 是否可训练。推理时 ν 是常量残差计算只给网络提供物理约束识别时 ν 是一个 nn.Parameter优化器会更新它。第二loss 记录方式不同。推理模式只需要记录解误差和残差识别模式额外记录 ν 的估计值和真实值的相对误差也就是 error_lambda 系列文件存在的原因。第三收敛判据不同。推理模式看解场误差识别模式必须同时看解误差和参数误差而且参数误差往往滞后于解误差收敛需要多训几轮才能稳定。我见过不少人在识别模式里一直盯着 loss 曲线看到它降到 1e-5 就认为成功结果一查 error_lambda 表ν 的估计值偏了 30%。这就是没理解第三点差异解场拟合得好不代表参数反演得准这是反问题的固有特性必须靠专门的误差表来盯。4. 四套代码的训练骨架优化器、初始化和误差记录4.1 网络宽度、层数与激活函数的常见选型四个方向代码共享同一套网络骨架差别只在损失函数和时间离散方式。默认参数是 4 层隐层、每层 50 个神经元、tanh 激活这套配置我在多个方程上都验证过收敛速度和稳定性比较平衡。想优先提速可以压到 3 层 30 宽想处理更复杂的耦合方程可以加到 6 层 100 宽但要注意宽度增加后 tanh 的梯度饱和问题会更明显训练初期 loss 可能长时间不动。def init_net(n_hidden4, n_width50): layers [nn.Linear(2, n_width)] for _ in range(n_hidden - 1): layers.append(nn.Linear(n_width, n_width)) layers.append(nn.Linear(n_width, 1)) net nn.Sequential(*layers) for m in net: if isinstance(m, nn.Linear): nn.init.xavier_normal_(m.weight) nn.init.zeros_(m.bias) return net激活函数方面tanh 是 PINN 的默认选择因为它各阶导数都连续且不会像 ReLU 那样让二阶导恒等于 0。GELU 有时在高频问题上表现更好但稳定性差一些我一般只在 tanh 收敛困难时才换。初始化用 Xavier 系列、偏置归零这是避免前几轮 loss 变成 NaN 的最有效手段。4.2 Adam L-BFGS 双阶段为什么先粗后精PINN 的训练有个明显规律用纯 Adam 训loss 降到一定程度就进入平台期梯度方向在小幅振荡里来回摇摆这时候换成 L-BFGS通常还能再降一两个数量级。原因在于 L-BFGS 利用了二阶曲率信息在损失函数光滑的区域能精准地收敛到局部极小点。常见做法是 Adam 先跑 1000020000 轮把解从随机初始化拉到一个合理区域再切 L-BFGS 跑到收敛。# 阶段一Adam 粗训 opt torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(20000): loss compute_loss(...) opt.zero_grad() loss.backward() opt.step() # 阶段二L-BFGS 精修 opt_lbfgs torch.optim.LBFGS(model.parameters(), lr1.0, max_iter2000) def closure(): opt_lbfgs.zero_grad() loss compute_loss(...) loss.backward() return loss opt_lbfgs.step(closure)切换优化器有一个必须注意的点L-BFGS 对学习率极其敏感默认 lr1.0 在 PINN 场景下经常直接让 loss 炸掉。我一般从 0.1 起步如果前几次迭代 loss 不降反升就继续调低。另一个坑是 closure 函数不能遗漏zero_grad()否则梯度会跨迭代累积L-BFGS 的线搜索条件被破坏表现为 loss 忽高忽低。4.3 批量读取误差表快速对比四个方向的收敛轨迹资源里的误差表文件分布在四个代码目录下命名区分了不同的 lambda 权重设置。读表时直接用 pandas一次性把所有表都读进来对比import pandas as pd import glob for f in glob.glob(**/error*.csv, recursiveTrue): df pd.read_csv(f) print(f, df.shape, df.columns.tolist()) print(df.head(3))读表不是看下就行要做两件事。第一确认列名里有没有空值CSV 导出时如果遇到 NaN说明那一步训练可能崩溃过。第二对比同一轮数下不同表的残差值筛选出最稳定的参数组合。四套代码对应四个不同的方法分支误差表其实是你判断哪个分支值得继续调参的第一手依据。文件名的 lambda_1、lambda_2 分别代表不同的正则权重或损失权重配置对照实验时不要混用。5. 避坑PINN 训练里的五个翻车现场现象一loss 降到 1e-5画出来的解却是一条直线。原因PDE 残差损失权重太大数据损失占比太小网络找到的「最优解」是让残差为零的平凡常数解这个解确实满足 PDE但完全不符合观测数据。解决把损失函数各个分量打印出来看比例确保数据项和边界项加起来不低于残差项同时把残差权重从 0.01 往下调或者用带权重的 MAE 替代 MSE 观测损失。现象二识别模式里 error_lambda 表的数值前期下降很好某一步突然跳成 NaN。原因ν 在更新过程中变成负值而残差公式里 ν 参与了除法或开方运算数值失去意义。解决对 ν 做 softplus 约束保证它始终为正或者在 optimizer.step() 之后手动 clamp 一下例如model.nu.data.clamp_(min1e-6)。这是参数反演最典型的数值事故不是网络结构问题。现象三连续时间推理模式里u_t 的梯度为 Noneloss.backward() 直接报错。原因输入张量 x、t 没有在传给网络前设置 requires_grad_(True)或者 forward 里做了原地操作破坏了计算图。解决在残差函数开头用x.clone().requires_grad_(True)重建叶子张量并确认所有自动微分调用都传了create_graphTrue。现象四训练集误差很低测试集误差却高一个数量级。原因内部配点采样范围没有覆盖完整计算域尤其是时间轴只采了前半段网络在没配点的区域自由发挥自然泛化失败。解决用拉丁超立方重新采样并在训练过程中每 5000 轮重新撒一次配点避免网络过拟合固定配点位置。现象五L-BFGS 切换后 loss 不降反升连续几步出现 inf。原因L-BFGS 的 lr 设置太激进或者 closure 里漏了zero_grad()导致线搜索失败。解决降低 lr 到 0.1 以下closure 函数严格按照前面代码三分支来写不要省略清零步骤如果仍然不稳定退回 Adam 训练几千轮再切换确保进入 L-BFGS 时解已经比较光滑。6. 验证技巧先用合成解自测再读误差表下结论拿到这套代码别急着往自己的数据上套。我会先做一步合成解验证自己设定一个解析解比如 u sin(x − t)代入 Burgers 方程算出对应的 ν然后用这个 ν 生成合成观测数据再把数据喂给识别代码。如果反演出的 ν 能回到设定值误差表里的相对误差最终小于 1%说明代码链路完整、参数设置合理这时候才有信心换到真实数据。def synthetic_check(model, x, t, true_nu): # 用解析解生成数据并计算识别误差 with torch.no_grad(): # 假设解析解 u sin(x - t) u_true torch.sin(x - t) # 训练结束后读取模型中的 nu pred_nu model.nu.item() rel_err abs(pred_nu - true_nu) / true_nu return rel_err验证通过后再回到误差表看整体趋势。注意 error_lambda 系列表的收敛速度通常慢于 error_table 系列因为参数反演需要网络先拟合出合理的解场才能从解场里反推参数。我习惯把两个表画在同一张图里横轴是迭代数纵轴取对数观察两条曲线是否同步下降。如果解误差已经平稳但参数误差还在漂说明需要增加数据快照或加大残差权重。这套代码给我最大的教训是PINN 的调参没有银弹四个方向各有脾气。离散时间方向对 dt 敏感连续时间方向对配点分布敏感识别方向对参数初始化敏感。从那以后我每次跑新数据都强制走一遍合成解自测、误差表对比、再换数据的流程能省下大量反复试错的时间。希望帮到你。本文还有配套的精品资源点击获取