如果你最近在折腾物理信息神经网络,或者手里正好有传热类的偏微分方程需要求解,那这个“平板间二维稳态对流传热方程的软物理信息神经网络实现”很值得仔细过一遍。这个项目解决的是一个很具体的问题:在两个平行平板之间的通道里,流体沿板面方向流动并伴随热量交换,需要把温度场 T(x,y) 求出来。而它的特别之处在于,整个求解过程不生成网格、不做有限差分或有限体积离散,直接把偏微分方程、边界条件作为惩罚项塞进神经网络的损失函数里,用自动微分去逼近温度场。这就是所谓“软物理信息神经网络(Soft-PINN)”的核心思路。本文把这个项目的背景、设计思路、Python 实现细节和我在实际复现中踩过的坑完整拆开讲一遍,适合刚入门 PINN 的开发者,也适合想把这个方法迁移到其他热流问题上的研究者参考。
1. 项目背景与核心问题拆解
1.1 平板间稳态对流传热到底在算什么
平板间对流传热在工程里很常见,比如换热器里的板式通道、电子设备散热风道、平板式太阳能集热器内部流动,本质都是流体在上下两个平行壁面之间流动,壁面有温度差或者热流密度,流体一边流动一边和壁面换热。稳态情况下,温度场不随时间变化,控制方程是带对流项的能量方程:
u(x,y)·∂T/∂x + v(x,y)·∂T/∂y = α·(∂²T/∂x² + ∂²T/∂y²)
其中 u、v 是流场速度分量,α 是热扩散系数。如果是充分发展的层流,V 方向速度常常近似为零,u 沿 y 方向呈抛物线分布,方程会继续简化成:
u(y)·∂T/∂x = α·∂²T/∂x² + α·∂²T/∂y²
这属于典型的二维稳态对流-扩散问题。传统做法是用 CFD 工具画网格,在网格上用有限体积法或有限差分法迭代求解。但这个项目选了一条完全不同的路:用神经网络直接拟合 T(x,y) 这个函数,再把控制方程残差作为约束加进去,让网络自己去“学习”满足物理规律的温度场。
适合做这个方向的场景很明确:要么你不想花大量时间在网格划分上,要么你解的区域边界形状比较奇怪,要么你后续要做参数反演、快速预测之类的任务。PINN 的思路天然适合这些情况,因为它不依赖网格,只需要在计算域内和边界上撒点。
1.2 为什么强调“软”物理信息约束
在 PINN 里,物理约束有两种常见加法:硬约束和软约束。硬约束的思路是改造网络输出结构,让输出本身精确满足某些边界条件,比如通过输出变换把边界值直接构造进去,模型再怎么训练都不会违反边界。这种方式听起来很完美,但实现起来限制很多,尤其是边界条件复杂、多个边界类型混合的时候,输出变换很难构造,还容易让网络的可表达性变差。
软约束则是把物理残差当作损失函数的一项。PDE 残差、边界残差、数据残差各自算一个均方误差,再加权求和,优化器通过最小化总损失让解逼近真实温度场。这个项目的标题明确点是“软”物理信息神经网络,也就是说它走的是后一条路,把密码方程“软性”地编入训练目标,而不是硬编码进网络结构。
软约束的最大好处是灵活。边界条件想换就换,想加一个数据拟合项也无所谓,本质上就是多目标优化问题里的一个目标项。它不需要针对特定边界条件重新设计网络结构,写代码的时候更通用。代价是训练时要反复调节各项损失的权重,否则 PDE 残差和边界残差之间会“打架”,训练容易不稳定。这一点后面代码部分会具体讲。
2. 方案设计与核心机制
2.1 神经网络结构与输入输出设计
这个项目用的是一个很常规的全连接前馈网络,输入是坐标 (x, y),输出是温度 T。网络结构不需要太复杂,常见的配置是 2 个输入神经元、3 到 5 个隐藏层、每层 32 到 64 个神经元、输出 1 个神经元。我复现时用的是一组稳定可靠的配置:4 个隐藏层,每层 40 个神经元。
激活函数选型是个关键点。传热方程里有二阶偏导,这要求激活函数至少二阶可导,而且二阶导数不能恒等于零。如果选 ReLU,它的一阶导是阶跃函数,二阶导几乎处处为零,PDE 残差直接退化成对流项只依赖一阶导,导热项完全失效,所以 ReLU 在这里不能用。常见的做法是用 tanh,输出平滑、导数有界,训练初期不容易出现梯度爆炸。后来我也试过 Swish 和正弦激活,在某些问题上收敛更快,但默认配置还是 tanh 最稳。
这里有个容易被忽略的预处理:坐标必须归一化。如果 x 范围是 [0, 1]、y 范围是 [0, 0.02],直接喂给网络,y 轴方向的信息会被网络当成“不太重要”的特征,梯度更新也很难平衡。我习惯把 x、y 都线性缩放到 [-1, 1] 区间,温度也做一个无量纲化处理,比如 T* = (T - T_cold) / (T_hot - T_cold)。缩放之后,PDE 残差各项的数值量级会接近,训练收敛明显更快。
2.2 损失函数四件套配置
Soft-PINN 的损失函数通常由三部分组成:
总损失 = λ_pde × L_pde + λ_bc × L_bc + λ_data × L_data
L_pde 是 PDE 残差的均方误差。做法是随机撒一批内部配点,计算网络输出对输入的各阶偏导数,代入控制方程左边减右边,求平方和的均值。L_bc 是边界条件的均方误差,包括入口温度、壁面温度或者壁面热流。L_data 是如果有实验测量点或者高精度数值解参考点,就把网络输出和真实值的误差加进来,这个项目里一般作为可选项。
权重 λ_pde、λ_bc、λ_data 是训练里最需要花心思的地方。PDE 残差的量级和边界残差的量级经常差很多。比如温度归一化后是 0 到 1,边界残差天然就是 0 到 1 的量级;而 PDE 残差是温度的二阶导乘以扩散系数再减去对流项,可能只有 1e-3 甚至更小。如果直接用等权相加,边界条件会被优化得很好,但内部物理规律完全没学到。所以权重不是随便拍的,而是根据各项Loss的初始量级反推,目标是让各项在总损失里的贡献大致同量级。我一般先跑 50 到 200 轮,打印各分项损失值,再设 λ。
边界条件处理还要注意一个细节:边界配点数量不能太少。每条边界至少 50 到 100 个点,四个边界加起来要有几百个点,否则网络容易“钻空子”,边界上某些区域完全没被约束,算出来的温度场在边界局部明显失真。
2.3 配点采样策略与训练流程
配点采样是 Soft-PINN 容易被低估的关键环节。固定点训练有一个明显问题:网络会记住这些点的位置,导致在点之间区域出现振荡。更好的做法是每个训练轮次重新随机采样内部点。也就是说,每轮迭代都从均匀分布或拉丁超立方采样里重新生成一组内部配点,这样网络在每个 epoch 面对的都是“新”样本,泛化能力会强很多。这个项目里我采用的是每轮重新采样的策略,内部点 1000 个,边界点每条边 100 个。
训练流程一般分两阶段。第一阶段用 Adam 优化器,学习率从 1e-3 开始,训练 2 万到 5 万轮,把损失压到一个相对稳定的平台。第二阶段切换成 L-BFGS 优化器,利用它对高精度解的强局部搜索能力,再优化几千轮,往往能把 PDE 残差再降低一到两个数量级。这种两阶段策略在 PINN 社区里几乎是标配,比单纯用 Adam 死磕更高效。
3. Python 代码实现与关键环节
3.1 环境准备与依赖
建议直接用 PyTorch,自动微分这一块最顺手。TensorFlow 也能做,但 PyTorch 的torch.autograd.grad接口在处理高阶导数时更直观。需要安装的库很少,核心就torch和numpy,绘图用matplotlib。我用的 PyTorch 2.x 版本,CPU 也能跑这个规模的例子,只是训练稍慢,GPU 会快很多。
import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt3.2 构建 Soft-PINN 模型
网络结构用 PyTorch 的nn.Module封装,代码不长,但有几个地方值得注意:每一层都接tanh激活,最后一层输出不加激活函数,因为温度可以是负值,不需要限制。
class SoftPINN(nn.Module): def __init__(self, layers=[2, 40, 40, 40, 40, 1]): super().__init__() self.layers = nn.ModuleList() for i in range(len(layers) - 1): self.layers.append(nn.Linear(layers[i], layers[i+1])) self.activation = nn.Tanh() def forward(self, x, y): x_in = torch.hstack([x, y]) for layer in self.layers[:-1]: x_in = self.activation(layer(x_in)) out = self.layers[-1](x_in) return out输入坐标要hstack拼成一个两列的张量,网络才能同时看到 x 和 y。输出层不加激活函数,是为了避免把输出限制在某个区间,给网络更大的表达自由度。
3.3 PDE 残差的计算
计算 PDE 残差是整个项目最核心的代码块。核心是自动微分求一阶和二阶偏导,用torch.autograd.grad而不是loss.backward(),这样不会污染网络的梯度状态。二阶导需要在一阶导的基础上再求一次,所以第一次求导时create_graph=True必须打开。
def pde_residual(net, x, y, alpha, U_max, H): x.requires_grad_(True) y.requires_grad_(True) T = net(x, y) dT_dx = torch.autograd.grad(T, x, grad_outputs=torch.ones_like(T), create_graph=True)[0] dT_dy = torch.autograd.grad(T, y, grad_outputs=torch.ones_like(T), create_graph=True)[0] d2T_dx2 = torch.autograd.grad(dT_dx, x, grad_outputs=torch.ones_like(dT_dx), create_graph=True)[0] d2T_dy2 = torch.autograd.grad(dT_dy, y, grad_outputs=torch.ones_like(dT_dy), create_graph=True)[0] # 充分发展层流抛物线速度分布,v=0 u = 4.0 * U_max * y * (H - y) / (H * H) residual = u * dT_dx - alpha * (d2T_dx2 + d2T_dy2) return residual这里我直接使用了简化的稳态能量方程,速度剖面采用了平板通道充分发展层流的抛物线分布。如果你的流场不是这种形式,把u替换成已知速度场的表达式即可。边界条件也类似,写一个函数:把边界配点上的预测值和给定值做差,求均方误差。
3.4 训练循环与权重调整
训练循环是标准的 PyTorch 套路,但要时刻记住:内部配点每轮都重新采样,边界配点可以固定也可以重新采样。我把边界点固定,内部点每轮重新生成,这样既保证边界约束稳定,又能让内点覆盖更均匀。
def train_soft_pinn(net, optimizer, n_iter, alpha, U_max, H): # 边界配点固定一次 x_left = torch.zeros(100, 1) y_left = torch.linspace(0, H, 100).reshape(-1, 1) T_left = torch.full_like(x_left, T_hot) # 其他边界同类处理,此处省略具体赋值 for it in range(n_iter): # 内部配点 x_int = torch.rand(1000, 1) * L y_int = torch.rand(1000, 1) * H # 边界损失 + PDE损失 loss_pde = torch.mean(pde_residual(net, x_int, y_int, alpha, U_max, H) ** 2) T_pred = net(x_left, y_left) loss_bc = torch.mean((T_pred - T_left) ** 2) # 加权组合 loss = 1.0 * loss_pde + 10.0 * loss_bc optimizer.zero_grad() loss.backward() optimizer.step() if it % 1000 == 0: print(f"iter={it}, loss_pde={loss_pde.item():.3e}, " f"loss_bc={loss_bc.item():.3e}")注意我这里损失权重暂时是λ_pde=1、λ_bc=10,这只是一个起始值。实际操作中要先跑一小段,观察两个分项损失的量级,再反过来调整权重。如果loss_pde一直比loss_bc大好几个数量级,说明边界约束太弱,或者温度尺度归一化没做好。
L-BFGS 的阶段在 PyTorch 里不能简单地循环optimizer.step()了事,需要把整个损失计算封装成一个闭包函数传给optimizer.step(closure)。不过整体代码结构和 Adam 大同小异,关键是设置line_search_fn='strong_wolfe',能明显提升收敛稳定性。
3.5 后处理与结果验证
训练结束后,把计算域划分成密集网格,网格点上直接前向传播得到温度场预测值,用contourf或者pcolormesh画等温线图。
x_plot = np.linspace(0, L, 200) y_plot = np.linspace(0, H, 80) xx, yy = np.meshgrid(x_plot, y_plot) x_tensor = torch.tensor(xx.ravel(), dtype=torch.float32).reshape(-1, 1) y_tensor = torch.tensor(yy.ravel(), dtype=torch.float32).reshape(-1, 1) with torch.no_grad(): T_pred = net(x_tensor, y_tensor).numpy().reshape(xx.shape) plt.contourf(xx, yy, T_pred, levels=50, cmap='jet') plt.colorbar() plt.show()验证阶段不要只看 Loss 曲线,一定要和参考解做对比。如果拿不到解析解,就用成熟的 CFD 工具在同一工况下跑一个高精度网格解作为基准,然后计算预测温度和基准解的最大绝对误差、平均绝对误差。我复现时经常发现 Loss 已经压到 1e-5 了,但温度场局部还是有异常尖峰,这说明网络在个别区域过拟合了采样点,此时需要检查采样密度和网络层数。
4. 实操中踩过的坑与避坑技巧
4.1 激活函数选错,二阶导等于零
这是新手最容易踩的坑。最开始我用 LeakyReLU,一阶导是常数,二阶导全为零,PDE 残差里导热项直接消失,网络只能靠对流项拟合,温度场完全对不上。换成 tanh 或正弦激活后,二阶导才有意义。这里建议大家检查自己的激活函数二阶导是否恒为零,不要只看网络能不能收敛,还要看 PDE 残差里的每一项是不是真的被计算到了。
4.2 边界损失权重失衡,内部物理规律学不到
我试过一组参数:边界损失和 PDE 损失等权叠加,训练结束后边界温度非常准确,但内部温度分布是完全错误的,等温线扭曲得离谱。原因就是 PDE 残差量级远小于边界残差,被边界损失完全掩盖了。解决办法不是盲目调权重,而是先把温度归一化、坐标归一化做完,再看残差量级。如果边界残差已经到 1e-2,PDE 残差还在 1e-1,那权重比还说得过去;如果差了三个数量级,就要考虑是不是热扩散系数 α 太小,导致导热项在整个方程里没存在感。这时候可以适当把 α 从方程里提取出来,或者重新归一化。
4.3 内部配点固定不变,出现记忆效应
如果内部配点固定不变,训练到后期 Loss 看似很低,但换一组测试点去算温度,误差立刻变大。这是典型的过拟合到配点位置,网络记住了这些点的值,而不是真正学到了函数关系。我现在的习惯是每 500 轮重新采样一次内部点,甚至在小型问题上每轮都采样。采样次数增加后,训练时间没涨多少,但泛化效果明显改善。
4.4 Adam 后不切 L-BFGS,精度卡在平台
Soft-PINN 训练到中后期,Adam 的收敛速度会变得非常慢,Loss 在一个平台上来回震荡,怎么降都降不下去。这个项目里我最早只用 Adam 跑 5 万轮,PDE 残差停在 1e-4 左右。后来改成 Adam 先跑 2 万轮,再切 L-BFGS 跑 3000 轮,残差直接降到 1e-6。L-BFGS 是一种拟牛顿方法,善于利用损失函数的曲率信息,在 PINN 这类优化目标接近收敛时表现非常突出。切换时注意要把所有张量从自动求导状态整理干净,否则会报RuntimeError。
4.5 高频分量拟合不动,温度场过平滑
传统 tanh 网络对低频信号拟合很好,对局部高频温度变化通常无能为力。平板间对流传热如果在入口区域温度剧烈变化,网络给出的结果往往是“磨平”的,过渡带被拉宽。这个问题的根源是神经网络的光谱偏差现象,也可以说是“谱偏差”:网络天然先学低频、后学高频。解决办法有两种:一是加 Fourier 特征映射,把坐标输入先做正弦余弦变换再送进网络;二是加大内部点采样密度,并让采样点偏向温度梯度大的区域。项目里我采用后者,在近壁面处加密采样,效果比较明显。
5. 常见问题排查速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| Loss 始终不降 | 激活函数不可二阶导或网络初始化不当 | 换 tanh 或正弦激活;缩小初始权重方差 |
| 边界准确但内部不对 | 边界损失权重过高或 PDE 残差量级过小 | 打印分项损失,反推权重;归一化温度、坐标 |
| 训练后期 Loss 震荡 | Adam 学习率过大 | 降低学习率到 1e-4 或切 L-BFGS 精调 |
| 换测试点误差大 | 内部配点固定导致过拟合 | 每轮重新采样内部点 |
| 温度场局部尖峰 | 采样点不足或网络容量过大 | 加密内部点;减小隐藏层宽度或增加正则化 |
| 二阶导计算出 NaN | 自动微分create_graph未开启 | 确保一阶导create_graph=True |
| 入口区域温度过平滑 | 高频分量拟合不了 | 加入 Fourier 特征映射或加密高梯度区采样 |
这个表基本覆盖了用 Soft-PINN 解传热方程时最常遇到的七类问题。每次训练不收敛,先按这个表逐条排查,比盲目调参有效率得多。
6. 实操心得与一点扩展建议
这个项目我前前后后复现了好几遍,最有感触的是:Soft-PINN 的“软”字既是优点,也是麻烦。优点在于灵活,换任何边界条件都不需要动网络结构;麻烦在于权重调节环节太依赖经验,参数稍微给错,训练就直接跑偏。现在我自己做类似问题时,会在一开始就把所有量纲约化干净,把温度、坐标、速度全部无量纲化,这样不仅损失权重好调,不同工况之间的模型迁移也更方便。
还有一个很实用的小技巧:训练前先不急着跑完整模型,把内部配点数量压到 100 个、训练轮数压到 500 轮,快速跑一遍看看三个分项损失的初始量级和下降趋势。如果初始残差比例就不对,直接停下来调权重,等比例合理了再上完整规模。这样能节省大量试错时间。
再往后扩展的话,这个思路完全可以延伸到非稳态问题,加入时间 t 作为第三个输入,同时把时间导数项加到 PDE 残差里。也可以用来做参数反演,把热扩散系数 α 设成未知参数,让网络一边拟合温度场一边反推物性参数。这些方向都是在现有这套编码框架上做小改动,不需要推翻重写。对真正想拿 PINN 解决工程传热问题的人来说,先把稳态、二维、软约束这个基础版本吃透,后面很多路都会顺很多。