PINN求解PDE:完整Python代码实战与避坑指南

发布时间:2026/10/2 22:20:41
PINN求解PDE:完整Python代码实战与避坑指南 简介这套Python代码面向求解偏微分方程的科研与教学需求基于物理信息神经网络PINNs实现适合计算机、电子信息工程、数学等专业学生完成课程设计、期末大作业或毕业设计。代码采用参数化编程参数可方便更改注释明细初学者也能快速上手。压缩包共48个文件以19个py源码为主辅以pyc缓存、gif演示、训练参数、说明文档等整体体积仅3.6MB轻量实用。内容覆盖一维Burgers方程、热传导方程、浅水波方程以及二维浅水波方程等多个典型PDE案例并附赠可直接运行的数据。已有1755人学习运行后可直观观察PINN如何将物理约束融入损失函数、通过神经网络逼近PDE解对理解AI与科学计算融合极具价值。1. PINN为什么值得动手一个能直接跑的PDE求解Python代码包PINN最近在微分方程求解圈子里热度很高连带着“PINN杀疯了”的讨论也越来越多。物理信息神经网络的做法和传统数值方法完全不同把PDE残差、初始条件、边界条件直接写进损失函数用自动微分代替差分离散省掉画网格和构造离散格式这两件最麻烦的事。这份资源就是一套完整可跑的Python代码覆盖网络定义、损失函数、训练循环、结果可视化几个核心模块适合刚接触PINN的学生快速复现也适合工程师拿自己的方程改一改先看PINN在这类问题上到底靠不靠谱。我拆过不少类似代码包这套的结构算是比较规整的下面从原理到踩坑挨个展开。2. 把物理定律写进损失函数PINN的数学结构与代码对应2.1 从PDE到损失残差项、初值项、边值项PINN求解PDE的基本思路是先用神经网络表示解的近似函数再把这个近似函数代进PDE让网络输出尽量满足方程本身、初始时刻的条件和边界上的约束。拟合程度用均方误差衡量总损失拆成三项权重分别写在前面。以经典的一维热传导方程为例控制方程是u_t alpha * u_xx初始条件是u(x,0)g(x)边界条件是u(0,t)u(1,t)0。PINN的做法是构造神经网络输出u_net(x,t)然后让它在三组点上同时最小化误差内部残差点要满足方程两边接近相等初始时刻的点要满足给的初值函数边界点要满足零边界条件。代码里通常用一个函数把三项损失加起来下面这段就是最常用的PDE残差写法def pde_loss(model, x_r, t_r, alpha): 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_xx torch.autograd.grad(u_x, x_r, grad_outputstorch.ones_like(u), create_graphTrue)[0] residual u_t - alpha * u_xx return torch.mean(residual ** 2)这段代码里最关键的是autograd.grad的使用方式。u_t是u对时间t的一阶偏导u_x是u对空间x的一阶偏导u_xx则是对u_x再做一次求导得到的二阶偏导。create_graphTrue必须带上否则求出来的梯度没法继续被反向传播使用这是PINN里最常见的翻车点。alpha是热扩散系数替换成你要求解的具体方程参数就行。整体损失函数再往下拼初值项把x在定义域内采样t固定为0计算网络输出和初值函数g(x)的均方误差边界项把t在整个时间区间采样x固定为0和1计算输出与0的均方误差。三项损失相加得到总损失训练目标就是让总损失最小化。2.2 网络结构与激活函数选择PINN里的网络结构不需要花哨多层全连接网络配合双曲正切激活函数就是最稳定的组合。代码包里一般定义成独立的类方便换方程时直接复用class PINN(nn.Module): def __init__(self, layers[2, 64, 64, 64, 1]): super().__init__() net [] for i in range(len(layers) - 2): net.append(nn.Linear(layers[i], layers[i 1])) net.append(nn.Tanh()) net.append(nn.Linear(layers[-2], layers[-1])) self.net nn.Sequential(*net) def forward(self, x, t): return self.net(torch.cat([x, t], dim1))这里的layers列表默认是[2, 64, 64, 64, 1]含义是输入层2个神经元接收x和t中间三层各64个神经元输出层1个神经元输出u的值。输入拼接这步经常被新手忽略如果没用torch.cat把x和t合并成两列网络训练后损失降不下去大概率就是维度没对齐。激活函数选Tanh而不是ReLU是PINN项目里一个值得记住的差异点。ReLU的分段线性会让二阶导在切换点附近变成0PDE残差算出来会失真Tanh无穷阶可导能保证二阶偏导有稳定的值。网络层数和宽度方面层数一般来说三层左右够用宽度64到128都可以接受。如果你的方程解高频振荡比较厉害优先加宽度而不是叠层数能在同样算力下更快收敛。2.3 自动微分torch.autograd.grad 的参数细节PINN整个训练过程的核心依赖是torch.autograd.grad而不是常见的loss.backward()。这两者的定位不一样backward()是把梯度传到模型参数上autograd.grad是手动对某个中间变量求导得到的是导数表达式本身可以继续参与后续运算。PINN的残差计算需要多次求导所以必须走autograd.grad这条路。autograd.grad里有三个参数需要注意。grad_outputs要传一个与u形状相同的全1张量否则高阶求导会报梯度不匹配的错误create_graphTrue告诉框架保留计算图这样第二次求导才有图可以回溯retain_graph在同时对x和t分别求导时要记得设置不然第一次求导结束计算图被释放第二次求导直接报错。一个常见习惯是把xt也纳入requires_grad而不是对模型的输入预先设置requires_gradTrue后者有时候会被torch的求导机制静默忽略导致求导结果全是None。3. 把环境装明白Python版本、依赖安装与训练脚本启动3.1 推荐依赖清单这套PINN代码本身就是Python工程环境装对能省掉一大半问题。依赖其实没有太多花哨的东西最核心的是PyTorch因为自动微分完全建立在它之上。我按自己实际跑通过的组合列了一张表依赖库推荐版本作用Python3.8 - 3.10运行环境3.11也可以但个别旧版torch不支持PyTorch1.12 - 2.x网络定义、自动微分、优化器NumPy1.24初始条件生成、数据采样、误差计算Matplotlib3.7结果热力图与剖面曲线绘制tqdm4.65训练进度显示方便观察每个epoch的loss不建议用最新版本的Python比如3.12或3.13常见的问题是torch对系统自带的高版本Python支持晚半拍装torch时容易卡在编译环节。用3.10是当前最省心的选择装完直接能跑。3.2 安装步骤与VSCode下的运行准备环境搭建的常规做法是用conda先建一个独立环境避免和本来的Python环境打架。命令行依次执行这几步conda create -n pinn python3.10 conda activate pinn pip install torch numpy matplotlib tqdm cd pinn_code python train.py如果机器上有NVIDIA显卡可以先看一眼CUDA版本再装对应的torch没有显卡就用CPU版本解一维热传导这类小规模问题CPU完全扛得住。VSCode的话装好Python扩展后记得在右下角选择解释器切到刚才创建好的pinn环境不然F5运行时会用错Python。代码诊断插件建议装Pylance能直接在编辑器里标出未定义变量和类型错误排查autograd.grad参数写错的情况会快很多。跑训练脚本的时候第一轮可以先不急着调参让它用默认配置跑完一个完整的训练周期确认程序能正常输出loss曲线和最终预测图。代码包里的train.py通常已经包含采样点生成、损失函数组装、模型初始化这几步不需要额外改文件路径直接python train.py就能起来。3.3 跑通示例训练循环骨架与采样逻辑训练循环是PINN代码里第二个容易翻车的地方。不少初学者以为和普通图像分类一样写一个标准优化循环就行实际上PINN有几个特殊点采样点每轮要么重新随机、要么固定好后再加一层“数据增强”优化器一般先用Adam后期切换到L-BFGS。下面这段是代码包里常见的核心循环片段optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(5000): x_r (torch.rand(n_r, 1) * (x_max - x_min) x_min).requires_grad_(True) t_r (torch.rand(n_r, 1) * (t_max - t_min) t_min).requires_grad_(True) loss_r pde_loss(model, x_r, t_r, alpha) x_b_left torch.zeros(n_b, 1) t_b_left torch.rand(n_b, 1) * t_max u_b_left model(x_b_left, t_b_left) loss_b torch.mean(u_b_left ** 2) x_i torch.rand(n_i, 1) * (x_max - x_min) x_min u_i model(x_i, torch.zeros_like(x_i)) loss_i torch.mean((u_i - g(x_i)) ** 2) loss loss_b loss_i loss_r optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 500 0: print(fepoch {epoch}, loss_total{loss.item():.2e}, floss_r{loss_r.item():.2e}, loss_b{loss_b.item():.2e}, loss_i{loss_i.item():.2e})几个参数写在这里n_r是内部残差采样点数代码包里默认是3000到5000个n_b是边界采样点数建议100到200个就够n_i是初始条件采样点数200到500个。x_max、x_min、t_max、t_min是定义域范围比如x从0到1t从0到2。采样点每个epoch重新生成好处是避免网络只在固定的点上记答案能促进泛化。训练时如果loss曲线的三个分量差距不大说明权重分配基本合理如果某一个分量长期压不下去参考后面第五章的应对方法。4. 换方程换边界热传导与泊松方程的调参实战4.1 一维热传导方程自定义扩散系数与初值条件拿到代码包先跑热传导方程是最能建立手感的方式。代码包里默认的alpha是0.01初值函数是sin(pi * x)边界为零。把这个参数改掉就能验证PINN是否能在不同物理参数下保持稳定。比如想要模拟传热更快的过程把alpha调成0.1解的衰减速度会明显变快网络需要更多内部采样点才能抓住解在时间-空间平面的变化趋势。改初始条件也是同样的套路。把g(x)换成分段函数比如在x小于0.5时取0、大于0.5时取1PINN初期肯定会震荡一下因为神经网络天生不擅长拟合并段不光滑的函数。这时候可以先把初值项权重加大比如从1提到5让网络优先把初始时刻的形状学对再慢慢让PDE残差项拉平曲线。这个“先初值、后残差”的调度策略在PINN里非常实用两个阶段的权重比一般设在1比5到1比10之间。4.2 二维泊松方程源项与边界形状的处理二维泊松方程是另一类高频使用场景方程形式是u_xx u_yy f(x, y)边界条件可以是零狄利克雷。和热传导方程相比变化点在于输入从两个变成三个坐标网络输入层要从2改成3PDE残差项要分别对x和y求二阶导再相加。代码包里的网络类输入维度改一下就能复用。坐标为(x, y)取0到1的单位方块。源项f(x,y) 2 * pi^2 * sin(pi * x) * sin(pi * y)时方程有精确解sin(pix) * sin(piy)是拿来验证PINN实现是否正确的好标定对象。残差计算代码相应改为def poisson_residual(model, x, y): u model(x, y) u_xx torch.autograd.grad( torch.autograd.grad(u, x, grad_outputstorch.ones_like(u), create_graphTrue)[0], x, grad_outputstorch.ones_like(u), create_graphTrue)[0] u_yy torch.autograd.grad( torch.autograd.grad(u, y, grad_outputstorch.ones_like(u), create_graphTrue)[0], y, grad_outputstorch.ones_like(u), create_graphTrue)[0] return u_xx u_yy - f(x, y)这段逻辑里二阶导是嵌套着求的第一层求出一阶导u_x第二层再对u_x求导得到u_xx两次都要写create_graphTrue。注意后半段直接用原来的表达式 f(x, y) 计算源项整个残差张量的shape要保持一致不然torch.mean会报维度错误。这里有一个容易模糊的地方网络类输出层要改成两个输入的组合而损失函数里用的是batch维上的坐标张量两者维度要对齐。二维问题最常见的现象是训练变慢。维度从2升到3网络要拟合的空间范围大了不少默认3000个残差采样点会有稀疏感。我一般把n_r调到8000到10000然后配上学习率1e-3的Adam前期预热效果立刻不一样。如果还想再稳一点可以把每次采样的点按均匀网格生成保证定义域内不漏区域比纯随机采样的覆盖面好得多。4.3 损失项权重与采样规模的调整参考不同方程里三项损失的量级天然不同统一用一个固定权重很容易出问题。代码包里一般会提供一组初始权重但实际使用时要按照自己的方程调整。下面这张表是我在不同场景下跑下来比较稳的起点值问题类型内部残差点数边界点数初值点数损失权重比内:边:初一维热传导 u_t 0.01 u_xx30002004001:1:1热传导快速扩散 alpha0.150003005001:2:5二维泊松方程10000800-1:1:-Burgers 方程有激波1000040010001:2:3权重比的含义是总损失计算时三个分量的乘数比如1:2:3表示loss 1loss_r 2loss_b 3*loss_i。初始条件和边界条件给定一个前期就提高权值模型的置信度会更高。损失项的权重不用追求极致的精度相差一个数量级内问题都不大真正要注意的还是输入尺度一致性这一点放到第五章细讲。5. PINN避坑指南收敛停滞、边界穿透与数据尺度问题5.1 现象总Loss降不动预测曲线形状还是不对Loss卡在某个数值高低波动训练几千轮都不下降是PINN里最常见的劝退现场。我见过有人以为是网络结构问题把层数加到10层结果更差。实际排查时先看三个分量如果PDE残差项一直压不下去多半是内部采样点覆盖不均匀网络学到的区域不完整某些局部解的特征没有被采样点暴露出来。解决方式有两个。一是把纯随机采样改成网格采样加随机扰动保证每个子区域都有代表点二是观察是否有尖峰区域比如激波和边界层附近这些地方得在局部加密采样。用均匀分布再加一层torch.rand的抖动保留随机性的同时不会让某个区域成为空白。5.2 现象边界附近预测值明显越界预测曲线穿出去了PINN的边界条件是通过软约束实现的不是硬编码约束所以边界项权重不够时预测值在边界附近会悄悄漂移。特别是方程本身带大梯度的时候整条曲线在边缘处穿出边界是常事。检查边界项当前的值如果边界损失比其他项大两个数量级以上那就是边界项在总损失里弱势。解决的办法是把边界项的权重往上提同时增加边界采样点密度。边界点的数量不需要太多但位置要卡得准最好在边界上均匀采样几百个点配合权重从1提到10。还有一个后备思路如果方程允许直接在网络输出层乘一个距离边界距离的函数让边界约束从软约束变成硬约束。这个技巧在矩形定义域上很好用。5.3 现象训练初期Loss震荡剧烈曲线上下乱跳刚开始训练时loss值大到飞起是正常现象但一直震荡不停就不正常了。最常见的原因是学习率设置过高Adam虽然适应性强但初始学习率1e-2对PINN来说往往会过冲导致loss在最优值附近来回跳。另一个原因是激活函数输入太大进入Tanh饱和区梯度消失后训练完全停摆。我一般把初始学习率控制在1e-3然后用余弦退火或每隔2000轮降一半。如果震荡出现在初始条件附近可以把初值项权重加大如果所有项都在震荡优先怀疑学习率先把lr降到1e-4跑2000轮看看趋势。5.4 现象三个Loss项量级差出几十倍小的那项形同虚设这是把不同物理量的方程放进PINN时最容易踩的坑。比如热传导方程的残差项量级在1e-2左右边界损失只有1e-4两者直接相加时小的那项对梯度几乎没贡献。本质原因是输入坐标没有归一化或者方程中系数alpha过小导致残差天然偏小。最省心的解决方式是先把坐标归一化到[-1, 1]的区间再让网络输出的值也限制在同一个量级。输入归一化做完之后残差项和边界项的量级会明显靠近这时再配合给每项单独乘权重就比较容易控制收敛方向。判断是否需要对某一项额外放大的标准是训练后期查看三项各自的loss值如果最小的一个比最大的小两个数量级以上就得优先调整它而不是总盯着总loss。6. 把结果验证到能上会误差计算与多组对比小技巧6.1 相对L2误差的计算脚本结果跑完PINN不等于任务结束下一步是务必验证数值结果和参考解相差多少。工程上最常用的指标是相对L2误差把预测解和解在网格点上做差除以解的L2范数。代码包里可以自己加一个评估函数下面是具体的实现def relative_l2_error(model, x_grid, t_grid, u_ref): u_pred model(x_grid, t_grid).detach().cpu().numpy() x_grid_np x_grid.detach().cpu().numpy() # 这里直接计算网格上的真实解 u_exact u_ref(x_grid_np) l2_error np.linalg.norm(u_pred - u_exact) / np.linalg.norm(u_exact) return l2_error判断模型可用的经验标准相对L2误差在1e-2量级算合格1e-3量级算不错1e-4以上就相当理想了。达不到1e-2水平时先别急着调网络结构回去看采样密度、权重分配和归一化这三个方向。6.2 让PINN结果更稳的三个小技巧第一个技巧是两段式训练先用Adam跑几千轮把loss粗降到一个平台然后切到L-BFGS精调收尾。L-BFGS是拟牛顿法能利用loss曲面的二阶信息做精细化搜索和Adam配合使用之后我几乎每一次都能把loss再压低一个数量级。切换的时机是看Adam的loss连续几百轮不再明显变化此时用一个optimizer切换语句即可optimizer torch.optim.LBFGS(model.parameters(), max_iter1000, line_search_fnstrong_wolfe) def closure(): optimizer.zero_grad() loss total_loss() loss.backward() return loss optimizer.step(closure)第二个技巧是目标区域加密采样先跑一遍粗网格训练把预测解和残差大的区域标出来再在这些区域按更高密度补采样点重新训练一次。这个方法特别适合处理激波、边界层这种局部梯度大的物理过程。第三个技巧是物理量守恒校验。解完热传导方程后总能量随时间的变化趋势是否符合物理预期解完泊松方程后可以把解的梯度在边界上积分对比源项的总量是否一致。只要这个校验通过网络结果在一定误差范围内就是可用的。从那以后我每跑一个PINN都会强制自己先跑一遍带解析解的标准方程确认相对L2误差做到目标量级再往自己的方程上搬。与其盯着训练曲线看半天不如先用这个验证流程定一套基准后面的每个改动都有对照。希望帮到你。本文还有配套的精品资源点击获取