简介:这套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_outputs=torch.ones_like(u), create_graph=True)[0] u_x = torch.autograd.grad(u, x_r, grad_outputs=torch.ones_like(u), create_graph=True)[0] u_xx = torch.autograd.grad(u_x, x_r, grad_outputs=torch.ones_like(u), create_graph=True)[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_graph=True必须带上,否则求出来的梯度没法继续被反向传播使用,这是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], dim=1))这里的layers列表默认是[2, 64, 64, 64, 1],含义是输入层2个神经元接收x和t,中间三层各64个神经元,输出层1个神经元输出u的值。输入拼接这步经常被新手忽略,如果没用torch.cat把x和t合并成两列,网络训练后损失降不下去,大概率就是维度没对齐。
激活函数选Tanh而不是ReLU,是PINN项目里一个值得记住的差异点。ReLU的分段线性会让二阶导在切换点附近变成0,PDE残差算出来会失真;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_graph=True告诉框架保留计算图,这样第二次求导才有图可以回溯;retain_graph在同时对x和t分别求导时要记得设置,不然第一次求导结束计算图被释放,第二次求导直接报错。一个常见习惯是把xt也纳入requires_grad,而不是对模型的输入预先设置requires_grad=True,后者有时候会被torch的求导机制静默忽略,导致求导结果全是None。
3. 把环境装明白:Python版本、依赖安装与训练脚本启动
3.1 推荐依赖清单
这套PINN代码本身就是Python工程,环境装对能省掉一大半问题。依赖其实没有太多花哨的东西,最核心的是PyTorch,因为自动微分完全建立在它之上。我按自己实际跑通过的组合列了一张表:
| 依赖库 | 推荐版本 | 作用 |
|---|---|---|
| Python | 3.8 - 3.10 | 运行环境,3.11也可以但个别旧版torch不支持 |
| PyTorch | 1.12 - 2.x | 网络定义、自动微分、优化器 |
| NumPy | 1.24+ | 初始条件生成、数据采样、误差计算 |
| Matplotlib | 3.7+ | 结果热力图与剖面曲线绘制 |
| tqdm | 4.65+ | 训练进度显示,方便观察每个epoch的loss |
不建议用最新版本的Python,比如3.12或3.13,常见的问题是torch对系统自带的高版本Python支持晚半拍,装torch时容易卡在编译环节。用3.10是当前最省心的选择,装完直接能跑。
3.2 安装步骤与VSCode下的运行准备
环境搭建的常规做法是用conda先建一个独立环境,避免和本来的Python环境打架。命令行依次执行这几步:
conda create -n pinn python=3.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(), lr=1e-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(f"epoch {epoch}, loss_total={loss.item():.2e}, " f"loss_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到1,t从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时取1,PINN初期肯定会震荡一下,因为神经网络天生不擅长拟合并段不光滑的函数。这时候可以先把初值项权重加大,比如从1提到5,让网络优先把初始时刻的形状学对,再慢慢让PDE残差项拉平曲线。这个“先初值、后残差”的调度策略在PINN里非常实用,两个阶段的权重比一般设在1比5到1比10之间。
4.2 二维泊松方程:源项与边界形状的处理
二维泊松方程是另一类高频使用场景,方程形式是u_xx + u_yy = f(x, y),边界条件可以是零狄利克雷。和热传导方程相比,变化点在于输入从两个变成三个坐标,网络输入层要从2改成3,PDE残差项要分别对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_outputs=torch.ones_like(u), create_graph=True)[0], x, grad_outputs=torch.ones_like(u), create_graph=True)[0] u_yy = torch.autograd.grad( torch.autograd.grad(u, y, grad_outputs=torch.ones_like(u), create_graph=True)[0], y, grad_outputs=torch.ones_like(u), create_graph=True)[0] return u_xx + u_yy - f(x, y)这段逻辑里二阶导是嵌套着求的,第一层求出一阶导u_x,第二层再对u_x求导得到u_xx,两次都要写create_graph=True。注意后半段直接用原来的表达式 f(x, y) 计算源项,整个残差张量的shape要保持一致,不然torch.mean会报维度错误。这里有一个容易模糊的地方:网络类输出层要改成两个输入的组合,而损失函数里用的是batch维上的坐标张量,两者维度要对齐。
二维问题最常见的现象是训练变慢。维度从2升到3,网络要拟合的空间范围大了不少,默认3000个残差采样点会有稀疏感。我一般把n_r调到8000到10000,然后配上学习率1e-3的Adam前期预热,效果立刻不一样。如果还想再稳一点,可以把每次采样的点按均匀网格生成,保证定义域内不漏区域,比纯随机采样的覆盖面好得多。
4.3 损失项权重与采样规模的调整参考
不同方程里三项损失的量级天然不同,统一用一个固定权重很容易出问题。代码包里一般会提供一组初始权重,但实际使用时要按照自己的方程调整。下面这张表是我在不同场景下跑下来比较稳的起点值:
| 问题类型 | 内部残差点数 | 边界点数 | 初值点数 | 损失权重比(内:边:初) |
|---|---|---|---|---|
| 一维热传导 u_t = 0.01 u_xx | 3000 | 200 | 400 | 1:1:1 |
| 热传导快速扩散 alpha=0.1 | 5000 | 300 | 500 | 1:2:5 |
| 二维泊松方程 | 10000 | 800 | - | 1:1:- |
| Burgers 方程(有激波) | 10000 | 400 | 1000 | 1: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_iter=1000, line_search_fn="strong_wolfe") def closure(): optimizer.zero_grad() loss = total_loss() loss.backward() return loss optimizer.step(closure)第二个技巧是目标区域加密采样,先跑一遍粗网格训练,把预测解和残差大的区域标出来,再在这些区域按更高密度补采样点重新训练一次。这个方法特别适合处理激波、边界层这种局部梯度大的物理过程。
第三个技巧是物理量守恒校验。解完热传导方程后,总能量随时间的变化趋势是否符合物理预期;解完泊松方程后,可以把解的梯度在边界上积分,对比源项的总量是否一致。只要这个校验通过,网络结果在一定误差范围内就是可用的。
从那以后我每跑一个PINN,都会强制自己先跑一遍带解析解的标准方程,确认相对L2误差做到目标量级,再往自己的方程上搬。与其盯着训练曲线看半天,不如先用这个验证流程定一套基准,后面的每个改动都有对照。希望帮到你。
本文还有配套的精品资源,点击获取