简介这份资源面向想入门物理信息神经网络PINN的深度学习学习者与科研新手用单个PyTorch脚本求解一维热传导方程并输出温度场无需外部数据或旧版TensorFlow。包内共14个文件以py脚本、bat与sh一键运行脚本、md中文指南、txt日志及png可视化图为主另有npz预测数据压缩包约512KB。已有79人学习。核心代码将PDE残差、初始条件与边界条件加权写入损失函数配合自动微分实时计算偏导数训练后与解析解对比最大绝对偏差约1e-3、L2相对误差低于5e-4。配套文档拆解理论背景与代码逻辑给出参数调节建议和排错路径并对比原始论文实现差异帮助读者十分钟内完成环境部署到温度场可视化理解深度学习与经典数学物理融合的求解范式。1. 一个方程丢给神经网络它到底在算什么把 Burgers 方程、热传导方程这类偏微分方程PDE的残差直接当作损失函数让一个前馈神经网络去拟合解函数——这就是物理信息神经网络PINN最朴素的形态。标题里说的「只给一个方程」指的就是不提供任何数值解标签、不做网格剖分只把方程本身、初边值条件和一小撮配点坐标喂进去看网络能不能把温度场 u(x,t) 反演出来。这件事在 2019 年前后被系统化提出后成了科学机器学习里被复现最多的方向之一也是很多人第一次接触「神经网络解方程」的入口。它适合谁手上有明确的 PDE 形式、边界条件写得出来、但网格生成麻烦或者参数反演反复要重算的从业者。不适合谁方程本身有强激波、间断、或者需要极高精度比如 1e-8 量级的场景纯 PINN 目前还打不过成熟的有限差分/有限元。下面按「方程怎么变成损失 → 代码怎么搭 → 参数怎么调 → 哪里会翻车」这条线走一遍能直接抄的部分我都给全。2. 从方程到损失函数PINN 到底在优化什么2.1 把 PDE 残差写成可求导的损失项以一维热传导方程 u_t α·u_xx 为例网络输出是 u_θ(x,t)θ 是网络参数。核心操作是用自动微分算出 u_t 和 u_xx然后构造残差f u_t − α·u_xx理想情况下 f 在整个求解域内恒为 0。于是损失函数由三块拼成方程残差项、初值项、边界项。这三块的加权和就是网络唯一的学习信号没有任何标签数据。import torch import torch.nn as nn # 定义网络输入 (x, t)输出 u class PINN(nn.Module): def __init__(self, layers[2, 50, 50, 50, 1]): super().__init__() self.net nn.Sequential() for i in range(len(layers) - 1): self.net.add_module(flin{i}, nn.Linear(layers[i], layers[i1])) if i len(layers) - 2: self.net.add_module(ftanh{i}, nn.Tanh()) def forward(self, x, t): inp torch.cat([x, t], dim1) return self.net(inp) def pde_residual(model, x, t, alpha0.01): x.requires_grad_(True) t.requires_grad_(True) u model(x, t) # 一阶导对 t 求 u_t u_t torch.autograd.grad(u, t, grad_outputstorch.ones_like(u), create_graphTrue)[0] # 二阶导先对 x 求一阶再对 x 求二阶 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_t - alpha * u_xx逻辑说明create_graphTrue是关键它让梯度计算本身可导否则二阶导拿不到反向传播会断。requires_grad_(True)必须对输入坐标开不是对网络参数——网络参数默认就要梯度坐标是额外需要的。参数alpha是热扩散系数属于方程物理参数如果它本身待反演就把它设成nn.Parameter一起训练。2.2 三类损失项的采样与配平残差项在域内随机撒点collocation points初值项在 t0 的线上撒点边界项在 x0 和 xL 两条边上撒点。采样数量不是越多越好经验上域内点占大头边界点保证每个边界至少几十个。def compute_loss(model, x_col, t_col, x_ic, t_ic, u_ic, x_bc, t_bc, u_bc): # 方程残差损失 f pde_residual(model, x_col, t_col) loss_pde torch.mean(f ** 2) # 初值损失 u_pred_ic model(x_ic, t_ic) loss_ic torch.mean((u_pred_ic - u_ic) ** 2) # 边界损失 u_pred_bc model(x_bc, t_bc) loss_bc torch.mean((u_pred_bc - u_bc) ** 2) # 加权求和权重是调参重点 return loss_pde 100 * loss_ic 100 * loss_bc参数说明初值和边界的权重通常要比残差项大一到两个数量级因为约束点少、梯度信号弱。我一般从 10 起步看初值误差收敛情况再往上加。如果初值项一直压不下去说明权重太小或者初值采样点太稀。这里没有万能值得看方程刚性。2.3 为什么用 tanh 而不是 ReLU激活函数选 tanh 几乎是 PINN 的默认操作。原因是 ReLU 的二阶导恒为 0而 PDE 残差里普遍含二阶导用 ReLU 会让 u_xx 直接消失网络根本学不到扩散项。tanh 光滑、二阶可导代价是深层容易梯度消失所以层数一般控制在 4 到 8 层每层 32 到 128 个神经元。这个规模对一维问题够用二维温度场要适当加宽。3. 用 PyTorch 跑通一维温度场的最小闭环3.1 完整训练脚本与关键超参下面这段是可以直接跑的完整流程方程用 u_t 0.01·u_xx初值 u(x,0)sin(πx)边界 u(0,t)u(1,t)0解析解是 sin(πx)·exp(−0.01π²t)方便验证。import numpy as np import torch torch.manual_seed(0) model PINN([2, 64, 64, 64, 1]) optimizer torch.optim.Adam(model.parameters(), lr1e-3) # 采样域内 5000 点初值 200 点边界各 200 点 N_col, N_ic, N_bc 5000, 200, 200 x_col torch.rand(N_col, 1) t_col torch.rand(N_col, 1) x_ic torch.rand(N_ic, 1) t_ic torch.zeros(N_ic, 1) u_ic torch.sin(np.pi * x_ic) x_bc torch.cat([torch.zeros(N_bc, 1), torch.ones(N_bc, 1)]) t_bc torch.rand(2 * N_bc, 1) u_bc torch.zeros(2 * N_bc, 1) for epoch in range(20000): optimizer.zero_grad() loss compute_loss(model, x_col, t_col, x_ic, t_ic, u_ic, x_bc, t_bc, u_bc) loss.backward() optimizer.step() if epoch % 2000 0: print(fepoch {epoch}, loss {loss.item():.6e})逻辑说明Adam 先跑学习率 1e-3 是稳妥起点。20000 轮对一维问题通常够收敛到 1e-3 量级误差。如果 loss 卡在某个值不动先查初值权重再查采样点是否覆盖了梯度变化剧烈的区域。3.2 验证和解析解逐点对比训练完不能只看 loss必须拿解析解对一遍。做法是在规则网格上预测算最大绝对误差。model.eval() x_test torch.linspace(0, 1, 100).reshape(-1, 1) t_test torch.linspace(0, 1, 100).reshape(-1, 1) X, T torch.meshgrid(x_test.squeeze(), t_test.squeeze(), indexingij) u_pred model(X.reshape(-1, 1), T.reshape(-1, 1)).detach() u_true torch.sin(np.pi * X.reshape(-1, 1)) * torch.exp(-0.01 * np.pi**2 * T.reshape(-1, 1)) max_err (u_pred - u_true).abs().max().item() print(fmax abs error: {max_err:.4e})参数说明网格密度 100×100 足够看整体形态。如果最大误差出现在 t 接近 0 或边界附近说明初边值权重或采样需要调整如果误差在域内均匀偏大多半是网络容量不够或训练轮数不足。一维热传导跑得好能到 1e-3 以内Burgers 方程因为非线性对流项通常只能到 1e-2 量级。3.3 训练不动的三个自查点第一检查create_graph是否漏了漏了二阶导就是错的loss 会假装下降但解完全不对。第二检查初值权重如果初值项 loss 比残差项大两三个数量级还降不下来把权重乘 10。第三检查采样点域内点如果全挤在初始时刻附近后期温度场根本没约束。我一般会画一张配点分布图肉眼确认覆盖均匀再开训。4. 参数怎么设网络结构、采样与加权的实操边界4.1 网络深度与宽度的取舍一维问题 4 层 64 宽基本够。二维温度场比如 u_t α(u_xx u_yy)建议加到 6 层 128 宽输入维度变成 3x, y, t。再往上加层收益递减而且 tanh 深层梯度消失会让训练变慢。宽度比深度更值得加因为宽网络对高频成分拟合更好。如果解里有剧烈梯度可以考虑在输入前做位置编码把坐标映射到高维正弦基这是 NeRF 那套思路迁移过来的对 PINN 同样有效。4.2 配点数量与分布策略配点数量不是越多越好。一维 5000 点、二维 20000 点是常见起点。关键是分布均匀随机采样在解变化平缓的区域浪费算力在激波附近又不够。改进做法是残差自适应采样——每隔若干轮在残差大的位置补点。实现上可以算每个配点的 |f|按概率重采样。def adaptive_resample(model, x_col, t_col, n_new1000): f pde_residual(model, x_col, t_col).detach().abs().squeeze() prob f / f.sum() idx torch.multinomial(prob, n_new, replacementTrue) return x_col[idx], t_col[idx]参数说明n_new每次补的点数不宜超过总配点的 20%否则训练分布跳变太猛。补点后旧点保留形成累积采样。这个技巧对 Burgers 方程这种有陡峭梯度的场景提升明显能把误差从 1e-2 压到 5e-3 左右。4.3 损失权重的动态调整固定权重需要反复试。更省事的做法是用自适应权重每若干轮根据各项 loss 的量级自动调。简单策略是让每项对总梯度的贡献大致相当。def update_weights(loss_dict, weights, alpha0.1): # loss_dict: {pde:..., ic:..., bc:...} max_loss max(loss_dict.values()) for k in weights: target max_loss / (loss_dict[k] 1e-8) weights[k] (1 - alpha) * weights[k] alpha * target return weights参数说明alpha是平滑系数0.1 表示每轮只往目标挪一小步避免权重震荡。这个策略在多项 loss 量级差很多时特别有用省去手工试权重的血泪经验。注意权重更新要放在 optimizer.step() 之后且不要对权重求导。5. 避坑与排查PINN 训练里最常见的五类翻车5.1 现象loss 降到某个值就纹丝不动原因多半是初值或边界权重太小网络优先满足域内残差把初边值当噪声忽略了。也可能是配点里初值附近点太少梯度信号传不进去。 解决把初边值权重乘 10 再跑同时把初值采样点从 200 加到 500。如果还不动检查初值条件是否和边界条件在角点冲突比如 u(0,0) 同时被初值和边界约束成不同值这种矛盾会让 loss 卡死。5.2 现象训练 loss 很低但验证误差巨大原因典型过拟合到配点上或者残差计算有 bug。最常见的是create_graph漏写导致二阶导恒为 0网络学了个满足一阶关系的假解。 解决先拿一个已知解析解的方程比如上面的热传导做单元测试确认 max error 能到 1e-3。如果单元测试都过不了别急着上复杂方程先把自动微分链路查清楚。5.3 现象Burgers 方程解在激波附近振荡原因激波处梯度极大均匀配点根本不够而且 tanh 网络对间断拟合能力有限。 解决上自适应采样在残差大的区域补点同时把网络加宽到 128训练轮数加到 50000 以上。如果还振荡考虑把激波位置作为先验加进采样分布。纯 PINN 处理强激波本来就吃力必要时换有限体积法做对比。5.4 现象换一组随机种子结果差很多原因PINN 损失面非凸初始化敏感。配点随机采样也带来方差。 解决固定随机种子做调试但最终评估要跑 3 到 5 个种子取平均。如果方差超过一个数量级说明网络容量或采样策略不稳优先加宽网络、增加配点而不是调学习率。5.5 现象GPU 显存爆了原因配点数量太大或者二阶导计算图没释放。 解决配点分批喂每批 2000 到 5000 点。二阶导的create_graphTrue会保留计算图如果不需要对二阶导再求导可以在算完残差后 detach 掉中间量。另外torch.autograd.grad比backward更省显存优先用它。6. 进阶把方程参数也交给网络一起学PINN 真正好用的地方不是解正问题而是反问题——方程里某个参数未知只有稀疏观测让网络把参数和场一起反演出来。比如热传导里 α 未知只有几个温度传感器读数可以把 α 设成nn.Parameter在损失里加一项观测拟合。alpha nn.Parameter(torch.tensor(0.02)) # 初始猜测 optimizer torch.optim.Adam([ {params: model.parameters()}, {params: [alpha], lr: 1e-4} # 参数学习率调小 ], lr1e-3) def compute_loss_inverse(model, alpha, x_col, t_col, x_obs, t_obs, u_obs): f pde_residual(model, x_col, t_col, alphaalpha) loss_pde torch.mean(f ** 2) u_pred_obs model(x_obs, t_obs) loss_obs torch.mean((u_pred_obs - u_obs) ** 2) return loss_pde 100 * loss_obs参数说明alpha的学习率要比网络小一个数量级否则参数会跟着网络震荡。观测点不用多一维问题 10 到 20 个点就能把 α 反演到 1% 误差以内前提是观测位置覆盖了温度变化明显的区域。如果观测全挤在初始时刻α 和初值条件会耦合反演不唯一。验证反演结果时除了看 α 是否收敛到真值还要看残差场是否均匀。如果残差在观测点附近特别小、远处特别大说明网络在过拟合观测α 可能被带偏。这时候加观测噪声做鲁棒性测试或者增加残差项权重。我自己踩过最深的坑是早期用 ReLU 跑热传导loss 降得挺好看结果 u_xx 恒为 0网络学了个线性解验证误差 0.3 还浑然不觉。后来养成习惯任何新方程上手先用解析解做单元测试确认自动微分链路和损失权重都对了再换真实问题。这个习惯帮我省了至少两周的无效调参。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?