最近我在做平板间对流传热问题的数值模拟绕了一圈还是觉得软PINN的搞法最灵活。所谓软物理信息神经网络soft physics-informed neural network软PINN简单说就是不让边界条件这类物理规则“硬性”规定网络结构而是把它们作为惩罚项揉进损失函数通过网络训练让输出逐步逼近满足偏微分方程和边界条件的解。这套方法用PyTorch实现前后代码量不大真正费劲的是模型设计、权重设置和训练调参。这篇文章从物理模型开始把二维稳态对流传热方程、软PINN损失构造、PyTorch训练细节以及我踩过的坑一次性讲清楚。适合对PINN有基础了解、正准备处理传热类问题的朋友。先交代这个案例的物理场景两块水平平板之间是二维稳态层流流动沿x方向。入口处流体温度已知上壁面给定温度下壁面绝热流体在流动过程中通过对流和导热不断换热最后在出口处形成充分发展的温度剖面。这就是一个典型的二维稳态对流传热问题控制方程是带对流项的能量方程边界上同时存在Dirichlet和Neumann条件。比起简单的一维热传导这个案例的对流项会让解出现方向性对PINN的训练稳定性考验更大。1. 从物理场景说起平板间对流传热问题到底在算啥1.1 控制方程与无量纲化在二维直角坐标下稳态不可压缩流动的能量方程为ρ c_p (u ∂T/∂x v ∂T/∂y) k (∂²T/∂x² ∂²T/∂y²) S对平板间的充分发展层流垂直方向速度v0流向速度u只取决于yu(y) 4 U_max (y/H)(1 - y/H)这是压力驱动层流流动经典的抛物线分布。把速度场代入后需要求解的就是一个含对流项的对流-扩散方程u(y) ∂T/∂x α (∂²T/∂x² ∂²T/∂y²)其中 α k/(ρ c_p) 是热扩散系数。这里要先说明一点如果流动还没有充分发展速度场本身由N-S方程控制那就得把动量方程和能量方程一起交给网络。这个案例先假设流动已经充分发展速度场已知单独求解温度场这也是很多入门项目选择的简化路径。后面第6节再说怎么扩展。数值计算一般喜欢无量纲化把物理量都压到O(1)量级。设X x/LY y/Hθ (T - T_in)/(T_w - T_in)速度写成 u U_max u*其中 u* 4Y(1-Y)。代入方程后整理得到Pe u* ∂θ/∂X ∂²θ/∂X² (L/H)² ∂²θ/∂Y²其中 Pe U_max L / α是以长度L为特征尺度的Péclet数。无量纲化的好处对PINN尤其重要后面会看到输入坐标和输出变量不在同一量级时梯度会失衡训练会非常痛苦。这套无量纲方程对应的边界条件在这个案例里是入口θ(0,Y)0上壁面θ(X,1)1下壁面∂θ/∂Y(X,0)0出口∂θ/∂X(1,Y)0。后面构建损失函数的依据就是这些式子。1.2 为什么这个案例适合用PINN来解很多人一听到PINN就觉得是要替代CFD其实不完全对。这个平板间对流传热问题用有限差分、有限体积都能算而且成熟求解器算得又快又准。那为什么还拿PINN练手因为PINN有几个传统方法不具备的切入点。第一它不需要结构化网格。不需要在计算域里生成节点网格只要在内部和边界上撒点就够了。边界条件复杂、几何要改改采样点就行。第二它直接以控制方程为约束天然适合做反问题。已知部分温度测量反推入口条件、热流边界只需要在损失函数里加一项数据拟合项正问题和反问题的代码几乎一样。第三软约束让边界条件的处理变得非常灵活。对硬约束来说每加一个边界条件就要重新设计网络输出层的变换形式而软约束只需要往损失里加一项权重代码结构完全不用动。这个单相流动的传热例子边界条件既有温度给定又有热流为零正好能体现出软约束的通用性。1.3 验证基准怎么来做PINN训练不能只看loss曲线你得有个参考解。对这个经典构型可以在同等参数下用有限差分法算一版“标准解”作为对照。注意这一步不是为了调试网络本身而是为了后面判断训练误差和误差空间分布。我的做法是先用一个简单的显式有限差分格式在均匀网格上迭代到收敛然后把温度场插值到随机采样点上用来计算预测的相对L2误差。这样训练过程中随时能看“真实误差”到底是多少。光看loss下降很容易被误导这个教训后面会详细说。2. 软PINN的核心设计思路把物理规则“软”进损失函数2.1 软约束和硬约束的本质区别PINN逼近似解和传统神经网络回归的区别在于输出不仅要符合数据还要符合物理方程。把物理约束加进去有两种风格。硬约束的做法是“构造一个天然满足边界的网络输出”。举个例子如果边界条件是T(0)a、T(1)b可以把网络输出写成T_net(x) a (b-a)x x(1-x) * N(x, param)这样无论N输出多少边界条件都自动满足不需要进损失函数。好处是边界永远精确满足坏处是每换一个边界条件就要重新推导输出变换复杂几何、混合边界条件、导数边界条件都很难构造。更麻烦的是这种乘性变换在边界附近会产生额外的梯度结构反而可能拖慢收敛。软约束走的是相反路线网络输出就是裸输出所有物理要求统一通过损失项体现。总损失写成L λ_pde L_pde λ_bc L_bc其中L_pde是内部点控制方程残差的均方误差L_bc是边界点残差的均方误差。网络训练时优化器在最小化两者的加权和。这就是“软PINN”的由来。对平板间传热这种边界既有值又有导数的混合问题软约束明显更省事。2.2 损失函数里的分量常见的PINN损失可以拆成几块PDE残差损失内部点满足方程 f_net(X,Y)0f_net就是无量纲能量方程左端减右端的残差。边界条件损失Dirichlet边界上预测值等于给定值Neumann边界上导数等于给定热流。这个案例里包括入口温度、壁面温度和绝热壁。数据损失如果有实验测量点或者加密数值解可以加一项MSE让网络输出逼近观测值。正问题没有数据时这一项可省略。正则项对网络权重做L2正则或对预测的二阶导数做额外惩罚防止过拟合和振荡。PINN里正则不常用但某些强对流问题里有助于稳定。用公式写就是这样L_pde (1/N_PDE) Σ |Pe u*(Y) ∂θ/∂X - ∂²θ/∂X² - (L/H)² ∂²θ/∂Y²|²L_boundary (1/N_bc) Σ [θ(0,Y)² (θ(X,1)-1)² (∂θ/∂Y(X,0))² (∂θ/∂X(1,Y))²]注意边界损失里每一项的权重可以不同。实际用的时候入口条件的重要性往往低于壁面温度条件因为入口只影响上游局部但入口如果不准误差会顺着流线往下游传播也不能掉以轻心。2.3 权重怎么定是软PINN最大的坑软约束听着简单真正做起来权重选择能让人崩溃。λ_pde太大会让网络只顾“内部方程”而忽略边界λ_bc太大则会让边界附近“很准”但内部残差极大甚至出现温度场被人工压成线性分布的现象。我最初的做法是固定权重λ_pde1.0λ_bc5.0跑起来勉强能收敛但边界附近有锯齿。后来改成按残差量级动态调整权重效果明显好一些。原理很简单监控各分量的损失数量级如果边界损失始终是PDE损失的100倍说明网络在拼命满足边界内部方程几乎没有被约束需要把边界权重往下调或者把PDE权重往上提。更自动化的方案有几种基于梯度范数的自适应权重、基于同方差不确定性的权重学习还有基于神经正切核的权重设置。对于这个二维传热问题我的经验是先用固定权重把“能训练”这件事跑通再引入自适应调整。不要一开始就上复杂方案否则连基线都崩了还不好排查。3. PyTorch从零搭建采样、网络、训练循环做一遍3.1 为什么选PyTorch模型怎么定义PyTorch做PINN最舒服的一点是自动微分和动态图。你要算 ∂θ/∂X、∂²θ/∂Y² 这类高阶导数不需要手推梯度公式torch.autograd.grad几行就能算出来。而且网络结构换来换去很方便从最简单全连接换到带残差的变体只改模型定义就行。网络输入是两个坐标输出是一个温度值。最基础的结构就是全连接网络import torch class SoftPINN(torch.nn.Module): def __init__(self, hidden64, depth4): super().__init__() layers [] for i in range(depth): dim_in 2 if i 0 else hidden layers.append(torch.nn.Linear(dim_in, hidden)) layers.append(torch.nn.Tanh()) layers.append(torch.nn.Linear(hidden, 1)) self.net torch.nn.Sequential(*layers) def forward(self, x, y): return self.net(torch.cat([x, y], dim1))这里为什么用tanh而不是ReLU因为要计算二阶导数ReLU的二阶导几乎处处为0网络输出很难满足光滑的扩散方程。tanh在隐层激活里是经典选择二阶导连续训练也稳定。Swish/SiLU也可以用但小算例里和tanh差别不大。归一化比网络结构更重要。输入坐标X∈[0,1]、Y∈[0,1]已经是无量纲量不存在问题。但如果直接用有量纲的米和毫米作为输入两个坐标数值差几个量级第一层线性变换的梯度会偏向大数值坐标收敛速度完全不一样。温度θ的范围也是[0,1]。输出层最后加不加Sigmoid都可以加了能让训练起步更稳但输出接近0和1时梯度平后期L-BFGS可能限制精度。我的建议是先不加Sigmoid用Xavier均匀初始化跑一轮没有发散就不用加。3.2 采样策略随机撒点和边界加密PINN的“训练集”是采样点。内部点用来计算PDE残差边界点用来计算边界残差。由于没有真实标签采样策略直接影响解的准确度这也算软PINN的特色。我的参考做法内部点用拉丁超立方采样或纯均匀随机采样数量取5000到10000。每个训练步重新采样让网络在每轮看到的点都不同相当于隐式正则化。边界点四条边各采300到500个点。入口、出口、上下壁都需要。关键加密速度剖面在壁面附近变化大温度梯度也大光靠均匀随机点会在壁面附近欠采样。可以按壁面法向做加密采样比如Y靠近1的区域多抽点能明显降低壁面附近误差。如果几何复杂用误差驱动采样残差自适应采样但二维规则域里均匀采样加边界加密已经够用。采样点数量也不是越大越好。每个step都要对内部点做二阶自动微分5000个点的计算量已经不小再加到几万点单步时间成倍增加收益却很低。3.3 损失计算和自动微分细节这个环节最容易踩坑。先看PDE残差的计算怎么写def pde_residual(model, xy, Pe, aspect): xy xy.clone().requires_grad_(True) x xy[:, 0:1] y xy[:, 1:2] theta model(x, y) theta_x torch.autograd.grad( theta, x, grad_outputstorch.ones_like(theta), create_graphTrue, retain_graphTrue )[0] theta_y torch.autograd.grad( theta, y, grad_outputstorch.ones_like(theta), create_graphTrue, retain_graphTrue )[0] theta_xx torch.autograd.grad( theta_x, x, grad_outputstorch.ones_like(theta_x), create_graphTrue, retain_graphTrue )[0] theta_yy torch.autograd.grad( theta_y, y, grad_outputstorch.ones_like(theta_y), create_graphTrue, retain_graphTrue )[0] # aspect (L/H)**2我在算例里用数值代替 u_star 4.0 * y * (1.0 - y) f Pe * u_star * theta_x - theta_xx - aspect * theta_yy return torch.mean(f ** 2)注意两个地方。第一torch.autograd.grad默认会释放计算图如果你在计算完PDE损失之后还要在同一批图上做边界损失或继续backward必须设retain_graphTrue。我见过很多报错都是因为图被释放又没开retain。第二输入坐标在forward之前必须requires_grad。最稳妥的方法是在函数开头clone一份并.requires_grad_(True)这样不会污染外部张量。如果直接用原始输入改requires_grad下一个step重新采样时会出问题。边界损失的计算相对简单对Dirichlet边界直接用预测值对Neumann边界需要算一阶导数。例如下壁面绝热theta_bottom model(x_bottom, y_bottom) theta_y_bottom torch.autograd.grad( theta_bottom, y_bottom, grad_outputstorch.ones_like(theta_bottom), create_graphTrue, retain_graphTrue )[0] bc_bottom torch.mean(theta_y_bottom ** 2)θ(X,1)-1 这一类则直接算MSE。3.4 训练循环Adam热身L-BFGS收尾PINN训练常用“两段式”先用Adam把损失快速压下来再用L-BFGS精修。Adam对超参数不敏感能在大范围找到合适的“盆地”L-BFGS利用二阶信息在PINN这种loss相对平滑的问题里收敛性很好而且不需要手调学习率。一个可用的训练循环骨架optimizer torch.optim.Adam(model.parameters(), lr1e-3) for step in range(2000): xy_pde sample_interior(5000) xy_bc sample_boundary(300) pde pde_residual(model, xy_pde, Pe, aspect) bc boundary_loss(model, xy_bc) loss w_pde * pde w_bc * bc optimizer.zero_grad() loss.backward() optimizer.step() if step % 200 0: print(step, loss.item(), pde.item(), bc.item())Adam阶段结束后切到L-BFGS。这一步有个重要经验L-BFGS阶段尽量用一组固定的、密度较高的采样点这样优化器面对的是一个确定性目标不会因为每步换点导致二阶信息混乱。我试过在L-BFGS里也随机重采样效果反而变差收敛不稳定。optimizer torch.optim.LBFGS(model.parameters(), lr0.2, max_iter1000) def closure(): optimizer.zero_grad() # 这里用固定采样点保持一致的目标面 pde pde_residual(model, xy_pde_fixed, Pe, aspect) bc boundary_loss(model, xy_bc_fixed) loss w_pde * pde w_bc * bc loss.backward() return loss optimizer.step(closure)3.5 判断收敛看各分量而不是只看总loss总loss是多项的加权和权重又可能不同所以总loss下降不代表各部分都好。我会在每一步打印pde_loss、bc_loss、预测温度的min/max每500步在验证网格上算一次相对L2误差。如果L2误差不降但总loss在降多半是某个损失分量没被照顾到可能是权重失衡。还有一个常用技巧是做一次“最终验证”把训练好的网络在均匀网格上预测全部温度然后和有限差分参考解逐点比较。这个误差不是用来调参的而是用来确认整个做法是否成立。4. 一个中等Péclet数算例从收敛曲线到温度场4.1 算例参数这里给一个可复现的算例配置。取无量纲参数Pe10纵横比L/H4那么代码里的 aspect (L/H)² 16。速度剖面用u*4Y(1-Y)计算域(X,Y) ∈ [0,1]×[0,1]。边界条件入口X0θ0上壁Y1θ1下壁Y0∂θ/∂Y0出口X1∂θ/∂X0网络结构是4层64神经元tanh内部采样点10000边界每边500Adam训练3000步之后L-BFGS在固定点上继续迭代。权重设为λ_pde1λ入口2λ壁面10λ出口1。这个配置是有讲究的壁面温度条件权重高是因为它决定了整个温度基准入口和出口权重稍低因为它们只影响局部过渡段。4.2 训练过程记录第一次跑的时候Adam在500步左右总loss就降到1e-4但单独看bc_loss还在1e-2。原因是初始权重里边界权重不够网络先“抹平”内部方程把输出快速压向一个能大致满足壁面的平滑场边界细节还没收敛。把壁面权重提高到10之后2000步时各部分loss降到1e-5量级。L-BFGS阶段总loss从1e-5附近降到1e-7。此时用固定参考点评估相对L2误差大约在0.5%左右。对于这个算例来说已经是一个可以接受的结果。4.3 温度场特征和误差分布预测温度场有几个特征可以作为结构验证入口附近温度沿流向变化缓慢上壁面附近形成热边界层温度从壁面的1向内部衰减随着X增大温度分布逐渐发展为近似充分发展的抛物线形剖面。下壁面绝热条件让底部温度梯度为0温度等值线在上壁面方向密集。和有限差分参考解对比误差最大出现在入口段的上壁面附近那里温度梯度大网络更难捕捉误差最小在充分发展段中心温度变化平缓。这个空间分布特征其实很好理解梯度大的地方对网络容量和采样密度要求更高。5. 常见问题与排查技巧实录5.1 损失一直在降解却明显不合理这是我遇到最多的现象总loss到1e-5画出来温度场却是“糊的”或者边界出现不自然的尖角。大概率不是网络问题而是损失里的某一项没真正衡量物理量。比如在对流主导的情况下PDE残差主要由∂θ/∂X主导二阶项很小网络可能把θ学成沿X几乎不变残差依然很小但这明显不符合物理。解决方法是把PDE残差里的每一项拆开看量级或者对预测解做流线剖面检查从入口到出口温度应当有变化。5.2 边界条件满足但内部方程残差大如果你发现边界loss收敛到很小但PDE loss居高不下先别急着加大PDE权重。检查一下采样点是否覆盖了边界层尤其是壁面法向梯度大的区域。均匀随机采样可能让靠近壁面20%的区域内几乎没有点PDE残差在那个区域根本没有被约束。解决办法是在壁面附近加密采样或者采用残差自适应采样策略重新分配权重。5.3 训练中途出现NaNNaN在PINN里基本可以锁定几个原因。一是学习率太大Adam在1e-3一般没事如果初始化不好2e-3也可能炸二是输出没有归一化θ往大数跑中间激活饱和梯度爆炸三是坐标输入尺度太大。无量纲化之后输入在[0,1]输出在[0,1]NaN风险大幅下降。如果还是NaN可以给网络梯度加裁剪或者把网络深度减小。5.4 自动微分报错“尝试二次反向传播”这个报错多数是因为在同一个step内多次调用autograd.grad而没有设置retain_graphTrue。很多人第一次写PINN都会撞到。规则很简单凡是你计划在最终loss.backward里用到、却提前用autograd.grad算出来过的计算片段一定要在autograd.grad调用里加retain_graphTrue。不想被这个坑反复折腾就在一次前向里把所有点的输出和导数算好最后统一构造loss。5.5 软权重怎么调都不收敛如果试了一圈权重还是不行考虑是不是方程尺度本身不均衡。Pe很大比如上千的时候对流项比扩散项大几个量级固定权重很难平衡。一个做法是把方程再缩放一次比如把X改成X/2把有效Pe降下来。另一个做法是引入梯度项自适应权重每个step统计各损失对参数的梯度范数把权重反比于梯度范数。原理很简单某项梯度太大就压低权重太小就抬高权重。症状优先排查常用对策总loss小但解不合理各分loss、剖面图拆解loss项检查采样覆盖边界准内部残差大采样密度、区域权重壁面加密区域加权训练出NaN数值尺度、学习率无量纲化梯度裁剪二次反向传播报错retain_graph配置一次前向统一backward权重调不动方程尺度、自适应权重替换方程尺度梯度范数自适应6. 一点扩展心得软PINN在这个问题上的边界拿这个平板间传热算例收个尾。我的体会是软PINN最大的价值不是和成熟CFD比精度而是提供了一个“约束即组装”的框架。对一个具体物理问题你只需要把方程残差和边界条件写进loss网络结构几乎不用变就能换边界、反演参数、耦合多个方程。这次做温度场时速度场是给定的后来我把动量方程也加了进去让网络同时输出u、v、p、T损失里就多了两个N-S残差项训练难度上升一个量级但代码只是多写几个残差函数而已。如果后续要扩展优先往两个方向走一是把软约束换成条件自适应约束例如把Dirichlet条件用输出变换强制满足能减轻权重负担二是用自适应采样函数让网络在训练中自动把更多点投到当前误差大的区域。这两个方向都能解决我在第5节提到的最核心问题权重和采样。最后分享一个最让我印象深刻的小经验在PINN里物理方程本身没有“单位”网络只认数值尺度。你费心做无量纲化不只是数学上好看更是为了让神经网络在数值上真的好训练。这个道理放到任何物理场问题里都成立。