最近在做一个多输入单输出的回归任务,输入是几个现场采集量,输出是一个关注指标。一开始直接用全连接神经网络拟合,数据量一旦少下来,验证集误差就忽高忽低,换几套初始化结果也不稳定。后来把物理信息神经网络(PINN)那套“物理残差”加进损失函数,整个模型才真正稳住了。这篇就把我实际跑通的一套东西讲透:PINN 在多变量回归里到底解决了什么问题,多输入单输出的网络和损失函数怎么设计,以及一份可以直接改成自己数据的 Matlab 代码套路。
如果你正在做小样本预测、有物理规律约束的工业建模,或者在 Matlab 里不想再依赖黑箱拟合工具,这篇文章应该能省你不少试错时间。我会把网络结构、损失函数、训练细节、调参坑全部分开讲,最后再给一个可以直接替换数据跑起来的实现思路。
1. PINN 到底是把什么信息塞进了回归模型
1.1 一个具体场景:测量点少,但物理规律明确
先想一个场景:某个设备内部温度场需要预测,观测数据是不同位置、不同时间下的少量温度测点。输入是空间坐标 (x) 和时间 (t),输出是温度 (u)。这类问题典型的难点是测点不可能覆盖整个区域,纯神经网络拟合时,网络只学会了“把已知点背下来”,对没采样到的位置完全靠猜。
但温度场本身遵循热传导方程:
[ \frac{\partial u}{\partial t} = a \frac{\partial^2 u}{\partial x^2} ]
其中 (a) 是热扩散系数。这张“额外图纸”就是我们可以利用的信息。PINN 的做法很简单:让神经网络不仅要拟合观测数据,还要在大量随机点上满足这条偏微分方程。方程不满足的地方会变成损失惩罚,逼迫网络学会一个既符合数据、又符合物理规则的解。
这个思路可以套到很多场景。比如结构力学里的应力场、流体里的速度场、化学工程里的浓度分布,只要你知道背后那条偏微分方程或常微分方程,就能把纯数据回归升级成物理约束回归。多输入单输出只是输入维度变多一些,本质逻辑完全一样。
1.2 数据项和物理项如何分工
PINN 的损失函数通常写成:
[ L = \lambda_d L_{data} + \lambda_p L_{pde} + \lambda_b L_{bc} ]
三项分工很明确:
- (L_{data}):网络预测值和真实测点值的均方误差,保证模型不偏离观测数据。
- (L_{pde}):在随机采样点处计算控制方程的残差平方均值,保证模型满足物理规律。
- (L_{bc}):边界条件或初始条件的残差,保证解的唯一性。
为什么边界条件单独拿一项?因为一个偏微分方程可能有无数个解,如果只约束“方程成立”,网络完全可以选择一个满足方程但完全不符合边界情况的解。边界项把解的取值范围限制住,实际训练中很重要。
我见过很多人只在损失里写 (L_{data}) 和 (L_{pde}),结果预测曲线在数据点附近很准,一离开区域就发散。后来把边界采样点单独加进去,情况立刻好转。这不是可选项,是必选项。
2. 网络结构设计与损失函数拆解
2.1 多输入单输出的网络前向计算
PINN 的骨干网络不需要花哨,全连接网络就够用。多输入单输出的意思是:输入矩阵 (X) 有多列,比如 ([x, t]),输出是一维值 (u)。网络前向计算可以用很简单的结构:
输入层: [N, 2] 隐藏层1: 32个神经元, tanh 隐藏层2: 32个神经元, tanh 输出层: 1个神经元, 线性这里我强烈建议激活函数用tanh,不要一上来就选ReLU。原因是物理残差里要计算导数,ReLU的导数在 0 处不连续,二阶导基本全是零,网络很难拟合光滑物理场。tanh到处可导,平滑性足够,对 PINN 来说是最省心的选择。
层数不用太深。4 层左右足够解决大多数二维输入、单输出的回归问题。层数过多不仅增加计算量,还容易让物理残差梯度消失,训练更难稳定。
2.2 物理残差的具体计算方式
以热传导方程为例,物理残差在某个采样点 ((x_i, t_i)) 上定义为:
[ R_i = \frac{\partial u}{\partial t} - a \frac{\partial^2 u}{\partial x^2} ]
理想的解应当让 (R_i = 0)。实际训练中我们计算所有采样点上的 (R_i^2) 均值,作为 (L_{pde})。
这里要注意一个关键点:对网络输出求导时,不是用有限差分,而是用自动微分。在 Matlab 里就是dlgradient。这样的好处是导数计算与网络参数和输入完全一致,精度高,实现也干净。后面我会给一段可以直接跑的残差计算代码。
物理残差的尺度取决于方程里的系数。比如热扩散系数 (a) 很小,而空间的二阶导很大,两者相乘之后的残差尺度可能和dataLoss差很多。如果不对损失项做平衡,训练会被大尺度项带着走。这不是算法问题,是量纲没处理好。
2.3 损失权重为什么不能随便给
损失中的 (\lambda_d)、(\lambda_p)、(\lambda_b) 决定三项的优先级。我常用的策略是:
- 数据比较干净、噪声小时,(\lambda_d) 可以大一些,比如 10。
- 数据噪声较大时,物理约束反而能起到平滑作用,(\lambda_p) 可以保持 1 左右,甚至更大。
- 边界条件权重一般不低于数据权重,否则边界解稳定性差。
实际操作时,我会先让 (\lambda_d = 1),(\lambda_p = 0.1) 开始跑,然后观察两条损失曲线的下降速度。如果dataLoss降得飞快但pdeLoss几乎不动,就逐步增大 (\lambda_p)。如果pdeLoss下降但dataLoss反弹,说明物理项太强,往回调。
一个很实用的技巧:把dataLoss、pdeLoss、bcLoss分别打出来,而不是只看总损失。总损失下降可能是某一项在降、另一项在涨,光看总数根本发现不了。
3. Matlab 代码实现:一套可以直接改的套路
3.1 数据组织与归一化
Matlab 实现 PINN 的底层思路是:用dlarray保存输入和输出,用dlgradient计算自动微分,再用dlfeval包装损失函数。先处理数据:
X = [x(:), t(:)]; % 每个样本一行,两列输入 U = u(:); % 每个样本一行,一个输出 % 归一化非常重要,不然后面的自动微分容易因为量级差异导致训练不稳 X_mean = mean(X); X_std = std(X); X_norm = (X - X_mean) ./ X_std; U_mean = mean(U); U_std = std(U); U_norm = (U - U_mean) ./ U_std;归一化不是可选项。如果 (x) 的量级是 0 到 1 米,(t) 的量级是几千秒,网络输入的数值差异会很大,初始化权重稍有不慎就梯度爆炸。把输入输出都归一化到接近零均值、单位方差的范围,训练会稳定很多。
3.2 网络参数与前向传播
不用trainNetwork那种高层接口,直接定义网络参数更灵活。下面这段代码初始化一个两层隐藏层、每层 32 个神经元的全连接网络。
rng(1); hidden1 = 32; hidden2 = 32; params.fc1_W = dlarray(0.1 * randn(2, hidden1)); params.fc1_b = dlarray(zeros(hidden1, 1)); params.fc2_W = dlarray(0.1 * randn(hidden1, hidden2)); params.fc2_b = dlarray(zeros(hidden2, 1)); params.out_W = dlarray(0.1 * randn(hidden2, 1)); params.out_b = dlarray(zeros(1, 1));前向传播函数这样写:
function U_pred = forward(X, params) Z1 = tanh(X * params.fc1_W + params.fc1_b'); Z2 = tanh(Z1 * params.fc2_W + params.fc2_b'); U_pred = Z2 * params.out_W + params.out_b; end这段代码里,输入X的形状是[N, 2],权重矩阵的形状按照“输入维度 x 神经元数”设计,做了最直接的矩阵乘法。用这种方式,后面换成三个输入、四个输入都不需要改网络主结构,只要改fc1_W的第一维大小就行。
3.3 自动微分与物理残差函数
核心部分来了。损失函数里要做两件事:计算数据误差,计算物理残差。物理残差需要用到dlgradient。
function [loss, gradParams] = modelLoss(params, X, U, Xc) % 数据项 U_pred = forward(X, params); dataLoss = mean((U_pred - U).^2); % 物理项:在采样点 Xc 上计算热传导方程残差 U_c = forward(Xc, params); % 一阶导数,grads 第一列是 du/dx,第二列是 du/dt grads = dlgradient(sum(U_c), Xc); u_t = grads(:, 2); dUdx = grads(:, 1); % 二阶导 du/dx^2,这里用到高阶自动微分 grads2 = dlgradient(sum(dUdx), Xc); u_xx = grads2(:, 1); alpha = 0.1; % 热扩散系数,按你的实际物理过程修改 pdeLoss = mean((u_t - alpha .* u_xx).^2); % 损失权重 lambdaData = 1.0; lambdaPde = 0.1; loss = lambdaData * dataLoss + lambdaPde * pdeLoss; % 反向传播,计算所有网络参数梯度 gradParams = dlgradient(loss, params); end这里有一个高阶导数的问题需要在实践中注意。Matlab 从某个版本开始支持用dlgradient嵌套计算二阶导,但我碰到过不同版本行为不一致的情况。如果你的 Matlab 提示“不能对梯度结果再次求导”,有两个处理思路:
- 把物理方程换成只含一阶导数的对流方程,比如 (\partial u/\partial t + c \partial u/\partial x = 0),这样只需要计算一阶导数。
- 或者用有限差分近似二阶导,比如在 (x) 方向上取相邻采样点差分,但这样做会引入数值误差,采样点不足时精度损失很大。
最稳妥的方案是先用一阶方程验证整个训练闭环,等代码跑通后再换回二阶方程。这个方法我用了很多次,能省下大半调试时间。
3.4 训练循环与参数更新
训练循环用dlfeval套损失函数,再用adamupdate更新参数。每次迭代时,网络参数会被dlgradient自动追踪,所以更新前不需要手动算梯度。
lr = 1e-3; numIter = 1000; avgGrad = []; avgSqGrad = []; for iter = 1:numIter [loss, grads] = dlfeval(@modelLoss, params, X_norm, U_norm, Xc_norm); [params, avgGrad, avgSqGrad] = adamupdate(params, grads, ... avgGrad, avgSqGrad, iter, lr); if mod(iter, 100) == 0 fprintf('iter=%d, loss=%.3e\n', iter, extractdata(loss)); end end这里有个细节:Xc_norm是物理采样点,不能和训练数据点完全重合。物理采样点应该是从整个定义域里随机撒出来的,代表网络输入空间里所有可能出现的坐标。训练数据点只关心实际测量位置,物理采样点关心整个物理区域,两者是互补关系。
预测新点时,记得把输入归一化,再反归一化输出:
X_new = [xq(:), tq(:)]; X_new_norm = (X_new - X_mean) ./ X_std; U_pred_norm = forward(dlarray(X_new_norm), params); U_pred = extractdata(U_pred_norm) * U_std + U_mean;4. 调参细节与常见坑
4.1 学习率和迭代次数怎么配
PINN 训练比普通神经网络更敏感,因为损失函数里有导数项,梯度尺度经常不稳。我一般从 (1\times 10^{-3}) 开始试,如果总损失震荡剧烈,降到 (3\times 10^{-4}) 或 (1\times 10^{-4})。如果训练曲线平滑收敛,再尝试调大学习率加快速度。
迭代次数并不是越多越好。我见过很多人在 3000 次循环后测试误差不降反升,原因是物理残差被压到极低,网络开始过度拟合采样点分布。比较合理的做法是每 200 次迭代存一次检查点,最后用验证集挑模型,而不是拿最后一次迭代的结果。
一个偏门但有用的技巧:每 50 轮重新随机撒一批物理采样点。固定采样点会让网络慢慢记住这些点的位置,重新采样相当于给物理项加了一点随机扰动,能明显提升泛化性。代价是物理损失曲线会有一点抖动,这很正常。
4.2 物理采样点怎么撒
采样点数量和分布直接影响物理约束的强度。我的经验是:
- 如果训练数据点只有几百个,物理采样点至少 1000 到 5000 个。
- 采样区域要覆盖输入空间的整个有效范围,才能在预测时发挥约束作用。
- 边界处要多撒一些点,尤其是边界条件变化剧烈的区域。
可以用rand或sobolset生成低差异序列。低差异序列比完全随机采样更均匀,尤其在高维输入时能避免“扎堆”现象。如果你的输入维度到了 5 维以上,建议直接换用sobolset。
物理采样点是否需要响应变量?不需要。它只有输入坐标,没有观测值,损失项只要求方程残差为零。这是 PINN 最迷人的地方:你可以凭空造出无限多的“监督点”,它们不来自于测量,而来自于物理规律。
4.3 网络宽度和层数选择
我实际测试下来,很多 PINN 问题用 3 到 5 层全连接、每层 32 到 64 个神经元就够用。过深的网络不会带来明显提升,反而让反向传播路径变长,梯度消失概率增加。
如果你发现dataLoss降到很低但pdeLoss还是下不去,可以先加宽度而不是加深度。比如把每层 32 改成 64,比再加一层隐藏层更有效。宽度增加能提高网络对复杂函数的表达能力,同时保持前向路径较短,训练更稳定。
激活函数的坑也值得说:ReLU系列在物理点上的二阶导几乎处处为 0,用热传导方程做物理约束时会直接失效。tanh最省事。如果你遇到梯度饱和,可以试试swish或mish,但大部分场景tanh足够好。
4.4 排查清单速查表
| 现象 | 可能原因 | 排查顺序 |
|---|---|---|
| 损失值停在某个数值不变 | 学习率太低或太高 | 先看总损失曲线是否震荡 |
| dataLoss 降,pdeLoss 不降 | 物理权重太小、采样点太少 | 增大 (\lambda_p),增加采样点 |
| pdeLoss 降,dataLoss 反弹 | 物理项过强 | 降低 (\lambda_p) |
| 输出发散、出现 NaN | 学习率过大、归一化没做 | 先调低学习率,再检查输入输出归一化 |
| 预测在边界附近偏很多 | 边界采样点不足 | 单独增加边界损失项权重 |
| 训练稳定但泛化差 | 网络太宽、迭代过多 | 加验证集早停,重新随机采样 |
这张表基本覆盖了我初学 PINN 时遇到的 90% 问题。多数问题不是模型算法太复杂,而是损失项的尺度没平衡好。
5. 实测效果和一两个扩展思路
5.1 数据少、噪声大时的对比感受
我在模拟项目X里用过这个套路:输入是位置和时间,输出是温度,训练数据只有 300 个带噪声的测点。纯全连接网络验证集误差在 8% 左右,每次换初始化结果都不一样。加上热传导方程物理约束之后,验证集误差降到 2% 以内,稳定性也明显改善。
这个提升不是免费的。训练时间大概是原来的 1.5 到 2 倍,因为要额外计算导数项。但换来的是更强的泛化能力和物理一致性,在很多不允许“瞎预测”的场景里非常值。
另一个实际体会是:数据噪声大时,物理项反而像一个平滑正则器。单个测点可能被噪声推到奇怪位置,但物理方程不允许周围区域出现不合理突变,所以整体预测曲线更光滑。这种效果是普通L2正则给不了的,因为普通正则只知道让权重变小,不知道让解变得更“物理”。
5.2 把物理残差换到别的方程上
这套代码最值得保留的部分是损失函数结构。换成其他物理规律时,你只需要改modelLoss里的pdeLoss。
如果处理的是稳态热传导,方程是 (\frac{\partial^2 u}{\partial x^2}+\frac{\partial^2 u}{\partial y^2}=0),就把残差改成二阶导之和。
如果处理的是对流方程,残差就是 (\frac{\partial u}{\partial t}+c\frac{\partial u}{\partial x}),代码里只需要两个一阶导。
如果处理的是一组已知的约束条件,比如某个变量必须恒大于零,也可以类似地加一个惩罚项进损失函数。这其实就是把“领域知识”翻译成可微约束的过程。你把多少条物理规则翻译进去,模型就有多少条靠山。
最后说一个我自己的习惯:每次搭 PINN,我都先不急着调网络结构,找个时间把dataLoss、pdeLoss、bcLoss三条曲线画在同一个对数坐标图里,观察它们各自的下降速度。只要物理项和数据项的量级差在 3 个数量级以上,就先调损失权重,而不是调网络。这个习惯帮我省了大量调参时间,你也可以试试。