
简介这份资源面向具备一定Python与深度学习基础、希望用物理信息神经网络PINN求解微分方程的学习者与研究者提供一套可运行的代码实现与案例集合。包内共27个文件以17个ipynb交互式笔记本为核心辅以3个py模块脚本、4个zbak备份文件及png图示、md说明等压缩包约1.02MB覆盖模型定义、几何域构建与PDE残差计算等模块。案例涉及欧拉梁、扩散方程、泊松方程多种边界条件、拉普拉斯方程、洛伦兹系统及ODE方程组等并包含Jacobian-Hessian方法等进阶内容便于读者对照复现、理解损失函数构造与自动微分流程。目前已有98人学习下载适合作为入门PINN求解微分方程的实践参考。1. 用 Python 把微分方程交给神经网络PINN 到底在算什么如果你做过数值求解大概率写过有限差分或者调用过scipy.integrate.solve_ivp网格一密、维度一高计算量就压得人喘不过气。PINNPhysics-Informed Neural Network物理信息神经网络换了个思路不去网格上离散微分算子而是用一个神经网络去逼近解函数把方程残差直接塞进损失函数里当约束。换句话说网络不光要拟合数据还得“遵守物理定律”。这件事在 Python 里落地并不玄学核心就是自动微分加损失加权。它适合谁适合手头有微分方程、又拿不到足够观测数据、还想把方程本身当先验的从业者。这篇笔记就按“理论立住、代码能跑、参数能调、坑能避”的顺序把 PINN 求解微分方程这条路走一遍。2. 从方程到损失函数PINN 的数学骨架与最小可跑实现PINN 的出发点很朴素假设未知解 $u(x,t)$ 可以用一个神经网络 $u_\theta(x,t)$ 表示其中 $\theta$ 是网络参数。既然 $u_\theta$ 是可微的那就可以用自动微分算出 $u_t$、$u_x$、$u_{xx}$ 这些导数项代回原方程得到残差。训练目标就是让残差在求解域内尽量小同时满足初值、边值条件。整个过程不需要网格只需要在域内采样一批“配点”collocation points。2.1 把 PDE 残差写成可微损失以最经典的一维热方程为例$$u_t \alpha u_{xx}, \quad x \in [-1, 1], \quad t \in [0, 1]$$初值 $u(x,0) -\sin(\pi x)$边界 $u(-1,t)u(1,t)0$。解析解是 $u(x,t) -e^{-\pi^2 \alpha t}\sin(\pi x)$方便我们后面验证精度。PINN 的损失通常由三部分组成方程残差损失 $L_r$在内部配点上计算 $u_t - \alpha u_{xx}$ 的均方值初值损失 $L_{ic}$在 $t0$ 上匹配给定初值边界损失 $L_{bc}$在 $x\pm 1$ 上匹配边界条件总损失 $L L_r \lambda_{ic} L_{ic} \lambda_{bc} L_{bc}$。这里的 $\lambda$ 是权重后面会专门讲怎么调。2.2 用 PyTorch 搭一个最小 PINN下面这段代码可以直接跑依赖只有torch和numpy。网络用 4 层全连接、每层 32 个神经元、tanh激活。为什么用tanh而不是ReLU因为ReLU的二阶导几乎处处为零算 $u_{xx}$ 会直接翻车这是新手最容易踩的坑之一。import torch import torch.nn as nn import numpy as np # 定义网络输入 (x, t)输出 u class PINN(nn.Module): def __init__(self, width32, depth4): super().__init__() layers [nn.Linear(2, width), nn.Tanh()] for _ in range(depth - 1): layers [nn.Linear(width, width), nn.Tanh()] layers.append(nn.Linear(width, 1)) self.net nn.Sequential(*layers) def forward(self, x, t): # 拼接输入形状 [N, 2] inp torch.cat([x, t], dim1) return self.net(inp) # 物理参数 alpha 0.1 def compute_loss(model, x_r, t_r, x_ic, t_ic, u_ic, x_bc, t_bc): # 内部配点需要计算 u_t 和 u_xx x_r.requires_grad_(True) t_r.requires_grad_(True) u model(x_r, t_r) # 一阶导 u_t torch.autograd.grad(u, t_r, grad_outputstorch.ones_like(u), create_graphTrue)[0] u_x torch.autograd.grad(u, x_r, grad_outputstorch.ones_like(u), create_graphTrue)[0] # 二阶导对 u_x 再求一次 x 的导 u_xx torch.autograd.grad(u_x, x_r, grad_outputstorch.ones_like(u_x), create_graphTrue)[0] # 方程残差 residual u_t - alpha * u_xx loss_r torch.mean(residual ** 2) # 初值损失 u_ic_pred model(x_ic, t_ic) loss_ic torch.mean((u_ic_pred - u_ic) ** 2) # 边界损失边界上 u 应为 0 u_bc_pred model(x_bc, t_bc) loss_bc torch.mean(u_bc_pred ** 2) return loss_r, loss_ic, loss_bc逻辑说明create_graphTrue是关键它让一阶导的计算图保留下来才能继续对 $u_x$ 求 $x$ 的导得到 $u_{xx}$。如果漏掉这个参数二阶导会报错或者返回None。参数上alpha是热扩散系数改它相当于改方程本身width和depth控制网络容量太小欠拟合太大训练慢且容易过拟合配点噪声。2.3 采样配点与训练循环配点采样有两种常见做法均匀网格采样和随机采样。均匀采样在简单问题上收敛更稳随机采样泛化稍好但方差大。我一般先用均匀采样把流程跑通再换随机看鲁棒性。def sample_points(n_r1000, n_ic100, n_bc100): # 内部配点均匀撒在 [-1,1] x [0,1] x_r torch.rand(n_r, 1) * 2 - 1 t_r torch.rand(n_r, 1) # 初值点t0x 均匀 x_ic torch.linspace(-1, 1, n_ic).reshape(-1, 1) t_ic torch.zeros_like(x_ic) u_ic -torch.sin(np.pi * x_ic) # 边界点x-1 和 x1t 均匀 t_bc torch.rand(n_bc, 1) x_bc torch.cat([-torch.ones(n_bc // 2, 1), torch.ones(n_bc // 2, 1)], dim0) t_bc torch.cat([t_bc[:n_bc // 2], t_bc[:n_bc // 2]], dim0) return x_r, t_r, x_ic, t_ic, u_ic, x_bc, t_bc model PINN() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(5000): x_r, t_r, x_ic, t_ic, u_ic, x_bc, t_bc sample_points() loss_r, loss_ic, loss_bc compute_loss(model, x_r, t_r, x_ic, t_ic, u_ic, x_bc, t_bc) loss loss_r 10 * loss_ic 10 * loss_bc optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 500 0: print(fepoch {epoch}, loss_r{loss_r.item():.2e}, floss_ic{loss_ic.item():.2e}, loss_bc{loss_bc.item():.2e})参数说明n_r1000是内部配点数太少残差约束不够太多每步计算变慢lr1e-3是 Adam 的常见起点如果损失震荡就降到1e-4权重10是初值和边界的放大系数因为这两项通常比残差小一两个数量级不放大容易被忽略。训练 5000 步后残差损失一般能降到1e-4量级此时用网格验证最大误差通常在1e-2到1e-3之间。3. 参数怎么设网络结构、权重与优化器的实操边界PINN 的调参不像传统数值方法那样有明确的稳定性条件它更像训练一个带物理约束的回归模型。下面把几个真正影响收敛的参数拆开讲。3.1 网络宽度、深度与激活函数的选择宽度和深度决定了解的表达能力。对于一维热方程这种光滑解width32, depth4已经够用如果是 Burgers 方程或者带激波的解宽度要加到 64 甚至 128深度 5 到 6 层。激活函数方面tanh是默认选择因为它的高阶导连续且非零。sin激活SIREN在周期性问题上有优势但需要专门的初始化否则训练初期梯度会爆炸。ReLU系列直接排除原因前面说过二阶导为零会让 $u_{xx}$ 恒等于零方程残差退化成 $u_t0$网络学不到任何扩散行为。一个经验判断如果训练 2000 步后残差损失还在1e-2以上不降先加宽网络再加配点最后才考虑换激活函数。3.2 损失权重的三种调法损失权重是 PINN 最玄学的地方。残差、初值、边界三项量级不同直接相加会让网络偏向量级大的那一项。常见做法有三种方法做法适用场景固定权重人工设lambda_ic10, lambda_bc10快速验证、简单方程自适应权重按各项梯度的范数动态调整多尺度、刚性方程学习权重把 log 权重设为可训练参数不确定量级时固定权重最省事但遇到刚性方程容易翻车。自适应权重的思路是每一步算三项损失对网络参数的梯度范数然后让权重反比于梯度范数这样量级小的项不会被淹没。学习权重则是把 $\log \lambda$ 作为nn.Parameter交给优化器代价是多了几个超参收敛更慢。我一般先用固定权重跑通如果发现初值损失降得比残差快很多说明残差被压制了这时候把lambda_ic和lambda_bc调小或者上自适应。3.3 优化器与学习率调度Adam 是 PINN 的标配学习率从1e-3开始。训练到后期损失下降变慢时切到 L-BFGS 做精细收敛这是很多论文里的标准操作。L-BFGS 在 PyTorch 里可以直接用torch.optim.LBFGS但它需要把整个训练集一次性传入配点数不能太大否则内存吃紧。学习率调度上StepLR每 1000 步乘 0.5 比较稳CosineAnnealing在配点随机采样时表现更好。注意不要在训练初期就降学习率否则网络还没找到大致解的形状就被锁死在小区域里。scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size1000, gamma0.5) # 在每个 epoch 末尾调用 scheduler.step()参数说明step_size1000表示每 1000 个 epoch 调整一次gamma0.5是衰减系数。如果损失曲线出现阶梯状下降说明这个调度在起作用如果一直平缓可以适当减小step_size。4. 避坑与排查PINN 训练不收敛的五个血泪现场PINN 的坑大多集中在自动微分、损失平衡和采样策略上。下面五条是我和身边同行反复遇到的按“现象 → 原因 → 解决”写清楚。4.1 二阶导算出来全是零现象残差损失从一开始就不降打印 $u_{xx}$ 发现全是 0 或者极小值。原因激活函数用了ReLU或LeakyReLU它们的二阶导在大部分区域为零或者求二阶导时漏了create_graphTrue导致计算图断裂。解决换成tanh或sin检查torch.autograd.grad的调用确保一阶导的create_graphTrue并且对一阶导结果再求导时grad_outputs形状匹配。4.2 初值条件被网络完全忽略现象训练结束后$t0$ 处的预测和给定初值差得很远但残差损失很小。原因初值损失权重太小或者初值采样点太少。网络发现只要满足方程残差就能降低总损失于是把初值约束当耳旁风。解决把lambda_ic调大一个数量级同时增加初值采样点密度。更稳的做法是每隔几百步单独监控初值损失如果它比残差损失大两个数量级以上就手动放大权重。4.3 边界处出现剧烈振荡现象解在边界附近上下抖动最大误差出现在 $x\pm 1$ 附近。原因边界采样点太少或者边界损失权重不足。另外如果边界条件是非齐次的比如 $u(1,t)1$网络在边界附近需要快速变化tanh网络可能表达不够。解决增加边界采样点把边界损失权重提高如果边界条件复杂考虑对输出做硬约束比如令 $u_\theta g(x,t) h(x,t) \cdot \text{NN}(x,t)$其中 $g$ 满足边界条件$h$ 在边界为零。这样边界条件天然满足不用再靠损失去逼。4.4 训练后期损失突然爆炸现象前几千步损失平稳下降突然某一批配点导致损失飙到NaN。原因配点随机采样时某些点落在了解变化剧烈的区域残差梯度极大加上学习率没降一步更新就把参数推飞了。解决开启梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)或者在损失爆炸前切到更小的学习率。另外配点采样可以改用拉丁超立方采样避免点聚集在局部。4.5 换方程后完全无法收敛现象同样的网络和超参热方程能跑换成 Burgers 方程或者 Allen-Cahn 方程就完全不收敛。原因不同方程的刚性不同解的特征尺度差异大。Burgers 方程在低粘性下会出现激波需要更密的配点和更大的网络容量Allen-Cahn 方程有双势阱解在正负之间跳变tanh网络需要更深的层数。解决先做量纲分析把方程归一化到 $O(1)$ 尺度然后逐步增加网络容量和配点数每次只改一个参数观察损失曲线。不要一次性把所有参数都调大否则出了问题不知道是哪个起的作用。5. 进阶技巧用自适应配点与硬约束把精度再提一个量级前面跑通的是固定配点加软约束的版本精度通常在1e-2到1e-3。如果要做参数反演或者长时间积分这个精度不够。下面两个技巧能把误差压到1e-4以下代价是实现复杂度上升。5.1 残差自适应配点固定配点的问题是解变化平缓的区域配点浪费变化剧烈的区域配点不够。自适应配点的思路是每隔若干步重新采样优先在残差大的位置撒点。实现上可以用残差作为概率密度做重要性采样。def adaptive_sample(model, n_candidates10000, n_select1000): # 先撒大量候选点 x_c torch.rand(n_candidates, 1) * 2 - 1 t_c torch.rand(n_candidates, 1) x_c.requires_grad_(True) t_c.requires_grad_(True) u model(x_c, t_c) u_t torch.autograd.grad(u, t_c, grad_outputstorch.ones_like(u), create_graphFalse)[0] u_x torch.autograd.grad(u, x_c, grad_outputstorch.ones_like(u), create_graphFalse)[0] u_xx torch.autograd.grad(u_x, x_c, grad_outputstorch.ones_like(u_x), create_graphFalse)[0] residual (u_t - alpha * u_xx).detach().abs().flatten() # 按残差概率采样残差大的点被选中概率高 prob residual / residual.sum() idx torch.multinomial(prob, n_select, replacementFalse) return x_c[idx].detach(), t_c[idx].detach()逻辑说明先撒 10000 个候选点算残差绝对值归一化成概率再用multinomial抽 1000 个。这样残差大的区域会被反复选中网络被迫在那里降低误差。参数上n_candidates越大采样越准但每步开销越大n_select是实际参与训练的配点数和固定采样保持一致即可。注意这里求导时create_graphFalse因为只需要残差数值不需要反向传播。5.2 硬约束输出层软约束靠损失去逼边界硬约束直接把边界条件写进网络输出。以边界 $u(-1,t)u(1,t)0$ 为例令$$u_\theta(x,t) (1 - x^2) \cdot \text{NN}(x,t)$$这样无论NN输出什么$x\pm 1$ 处 $u_\theta$ 恒为零。初值也可以类似处理但初值通常依赖 $t$硬约束构造更复杂一般只对边界做硬约束。class HardConstraintPINN(nn.Module): def __init__(self, width32, depth4): super().__init__() layers [nn.Linear(2, width), nn.Tanh()] for _ in range(depth - 1): layers [nn.Linear(width, width), nn.Tanh()] layers.append(nn.Linear(width, 1)) self.net nn.Sequential(*layers) def forward(self, x, t): inp torch.cat([x, t], dim1) raw self.net(inp) # 硬约束边界 x±1 处输出为零 return (1 - x ** 2) * raw用了硬约束后边界损失那一项可以直接从总损失里去掉训练更稳边界误差能降到机器精度附近。代价是网络输出被强制塑形如果真实解在边界附近变化剧烈(1-x^2)因子可能引入额外偏差这时候需要加宽网络来补偿。5.3 验证精度的方法训练完不能只看损失要用独立网格算最大误差。下面这段代码生成验证网格并计算与解析解的偏差model.eval() x_test torch.linspace(-1, 1, 200).reshape(-1, 1) t_test torch.linspace(0, 1, 100).reshape(-1, 1) X, T torch.meshgrid(x_test.flatten(), t_test.flatten(), indexingij) u_pred model(X.reshape(-1, 1), T.reshape(-1, 1)).reshape(200, 100) u_true -torch.exp(-np.pi ** 2 * alpha * T) * torch.sin(np.pi * X) max_err (u_pred - u_true).abs().max().item() print(fmax error: {max_err:.2e})参数说明验证网格要比训练配点密200x100 是常用规模。如果最大误差出现在边界附近说明硬约束没生效或者边界采样不够如果出现在初值附近说明初值权重还需要调。我一般会把这个误差和残差损失一起看两者量级差太多就说明某一项被压制了。最后说个习惯每次换方程先把alpha或者粘性系数设成容易收敛的值跑通再逐步调到目标值。直接上刚性参数十有八九会看到损失曲线一路平着不降然后开始怀疑人生。希望帮到你。本文还有配套的精品资源点击获取