☰
PINN解1D亥姆霍兹方程:用物理信息神经网络替代网格仿真
2026/9/27 23:36:54 网站建设 项目流程

简介:基于MATLAB的物理信息神经网络(PINN)求解一维亥姆霍兹方程资源包,面向具备数值计算和深度学习基础的研究者与工程师,适用于声学、电磁学及量子力学中的频域波动问题。资源共10个.m文件,压缩包约5KB,代码量虽小但流程完整,涵盖参数向量化转换、网络结构构建、损失函数定义、参数初始化以及L-BFGS优化调用等关键环节。main.m作为程序入口整合各模块,buildNet.m搭建网络层与激活函数,modelLoss.m和objectiveFunction.m分别负责物理残差与边界条件惩罚项及整体优化目标,initializeHe.m等文件提供初始化策略,用户可直接运行并修改扩展。已有313人学习下载,适合希望快速上手PINN求解偏微分方程、对比传统数值离散方法的MATLAB用户。通过这套代码,可系统理解PINN的建模思路与工程实现细节,为后续求解复杂波动方程或拓展至高维问题奠定基础。

1. 从"网格依赖"到"无网格优化":PINN 解 1D 亥姆霍兹方程到底解决了什么

做声学仿真或电磁场计算的人,对 1D 亥姆霍兹方程都不陌生:它本质上是频域波动方程,描述声压或电场在单一方向上的分布。传统做法是用有限元或有限差分,把求解域切成网格,组装刚度矩阵,然后解一个大型稀疏线性方程组。这套流程成熟、稳定,但有一个绕不开的痛点:网格质量直接决定结果精度,而高频工况下网格要加密到让人肉疼的程度。更麻烦的是,一旦边界形状或材料参数变了,网格得重新划分,整套矩阵组装流程也跟着重来。

PINN(物理信息神经网络)的思路完全反过来——它不切网格,而是把一个满足方程和边界条件的函数用神经网络来表示。训练过程中,损失函数直接包含方程残差和边界条件残差,通过自动微分计算导数,把偏微分方程求解变成一个优化问题。这个做法天然适合 MATLAB 中的深度学习工具箱,因为你只需要定义网络结构、拼损失函数、调优化器,剩下的事交给计算图。

这篇笔记面向两类人:一类是有 MATLAB 基础、想用 PINN 做快速原型验证的工程师;另一类是正在调研"PINN 在频域问题里到底能不能打"的研究者。我会先从理论层面把损失函数的设计逻辑讲清楚,再给出一个可直接运行的 1D 亥姆霍兹方程求解代码,最后把我的踩坑经历和调参策略一并交代。这篇文章不是科普,是我实际跑通过一遍之后沉淀下来的完整落地方案。

2. 从强形式到弱形式:PINN 如何把亥姆霍兹方程变成损失函数

2.1 为什么选残差形式而不选变分形式

传统有限元处理亥姆霍兹方程,用的是弱形式:两边乘测试函数、分部积分、引入边界条件。PINN 不这么做,它直接把微分方程在配点上的残差当成损失项来最小化。这个选择有实际理由:弱形式需要积分计算,而 PINN 的损失函数在数学上可以写成一个期望值,用采样点求和来近似;如果写成残差平方和,连积分都不用算,直接对配点的导数做数值求值即可。

具体来说,考虑如下 1D 亥姆霍兹方程:

[ \frac{d^2 u}{dx^2} + k^2 u = f(x), \quad x \in [0, L] ]

边界条件取 Dirichlet 型:(u(0)=a, u(L)=b)。这里的 (k) 是波数,代表物理频率参数。PINN 的做法是把 (u(x)) 用神经网络 (u_\theta(x)) 代替,其中 (\theta) 是网络的权重和偏置。损失函数分成两部分:

[ L = \lambda_{PDE} \cdot \frac{1}{N_P} \sum_{i=1}^{N_P} \left| \frac{d^2 u_\theta(x_i)}{dx^2} + k^2 u_\theta(x_i) - f(x_i) \right|^2 + \lambda_{BC} \cdot \left( |u_\theta(0)-a|^2 + |u_\theta(L)-b|^2 \right) ]

第一项是方程残差,逼迫网络在内部配点上满足原方程;第二项是边界残差,逼迫网络在端点满足边界条件。(\lambda_{PDE}) 和 (\lambda_{BC}) 是权重系数,用来平衡两项的量级差异。这是 PINN 里最重要的超参数之一,我后面会专门讲。

2.2 为什么边界条件权重不能随便设

理论上,只要权重大于零,优化最终都能收敛。但实践中边界权重大小直接决定收敛速度和精度。原因很简单:方程残差是一个分布式的约束,分布在所有内部点上;而边界残差只约束两个点。如果边界损失太小,网络会优先把内部残差压到很小,但边界处的值可能偏离很远;反过来,如果边界损失太大,内部方程可能被牺牲,物理正确性没有保障。

在我的实验中,一个典型的翻车现象是:边界残差降到了 (10^{-6}) 以下,看着很漂亮,但画出解曲线后,中间区域的波动形态跟解析解明显对不上。这说明 PDE 残差和边界残差之间的平衡被打破了。常见做法是先固定 (\lambda_{PDE}=1),然后按边界项的数量和内部配点数目的比值来缩放 (\lambda_{BC})。1D 情况下边界只有两个点,我会把 (\lambda_{BC}) 设在 1 到 10 之间,具体数值要通过少量试跑来确定。

另外要提一点:PINN 求解亥姆霍兹方程和求解扩散方程的一个显著区别是,亥姆霍兹方程的解带有振荡特性。波数 (k) 越大,解的空间振荡越快,网络需要越深越宽才能拟合出这种快速变化。换句话说,PINN 对亥姆霍兹方程的能力边界受制于网络容量和配点密度的双重限制,不是所有频率都能轻松突破的。

3. 用 MATLAB 搭建最小可复现的 PINN:从网络定义到训练循环

3.1 MATLAB 中的深度学习工具箱怎么选网络结构

MATLAB 从 R2019b 之后推出了一整套用于自定义训练循环的 API,包括dlnetwork、dlarray、extractdata等。对于 PINN 这种需要在损失函数里计算二阶导数的场景,我们不走trainNetwork这条路,因为那个接口不允许自定义损失函数。正确做法是用dlnetwork定义网络,然后在dlfeval里调用自定义函数,用dlgradient计算梯度。

网络结构方面,我测试过两种方案:全连接网络和带残差连接的全连接网络。对于 1D 亥姆霍兹方程,全连接网络已经足够,只要能保证足够的宽度和深度即可。为了处理二阶导数,激活函数必须至少二阶可导,所以 ReLU 这类分段线性激活函数不能用。我建议用tanh,它在整个定义域上无限光滑,且导数计算稳定。下面是搭建加训练的关键代码:

% 定义网络:输入维度1(坐标x),输出维度1(解u),3个隐藏层,每层64个神经元 layers = [ featureInputLayer(1, 'Normalization', 'none') fullyConnectedLayer(64) tanhLayer fullyConnectedLayer(64) tanhLayer fullyConnectedLayer(64) tanhLayer fullyConnectedLayer(1)]; net = dlnetwork(layers);

这段代码的核心是featureInputLayer(1)定义单个坐标输入,fullyConnectedLayer加tanhLayer反复堆叠,最后输出一个标量。选择 64 个神经元和 3 个隐藏层,是我在波数 (k) 不超过 20 的情况下测试出来的合理配置,既能拟合振荡解,又不会让训练太慢。如果你的波数更大,建议把隐藏层加到 4 到 5 层,宽度加到 128。

3.2 训练循环:ADAM 起步,L-BFGS 收尾

损失函数需要计算网络输出对输入的二阶导数。MATLAB 提供dlgradient能自动完成这件事,不需要你手推链式法则。关键点在于,dlgradient只能在dlfeval的上下文里调用,而且必须在一个自定义函数内部完成前向传播和梯度计算。下面是核心的损失函数定义:

function [loss, grads] = modelLoss(net, x, x0, u0, xL, uL, k) % 前向传播:预测内部点和边界点的解 u = forward(net, x); % 内部点预测值 u0_pred = forward(net, x0); % 左边界预测值 uL_pred = forward(net, xL); % 右边界预测值 % 计算一阶导数和二阶导数 du = dlgradient(u, x); % 一阶导数 d2u = dlgradient(du, x); % 二阶导数 % PDE残差:亥姆霍兹方程 d2u/dx2 + k^2*u = 0(这里取齐次形式) fPDE = d2u + k^2 * u; lossPDE = mean(fPDE.^2, 'all'); % 边界残差 lossBC = mean((u0_pred - u0).^2, 'all') + mean((uL_pred - uL).^2, 'all'); % 总损失,lambda_BC 设为 5 loss = lossPDE + 5 * lossBC; % 计算网络参数梯度 grads = dlgradient(loss, net.Learnables); % 为了后续绘图,把数值取出来 loss = extractdata(loss); end

这个函数的关键数学逻辑在fPDE = d2u + k^2 * u这一行,它就是把偏微分方程的左边直接用网络输出的导数表示出来。如果方程有源项,比如右边不是零而是 (f(x)),就在这里做一个减法。训练时不需要extractdata特别早,因为dlgradient需要在dlarray类型上操作,但最后取出来是为了方便打印 loss 值。

接下来是训练循环本身。这里有一个值得注意的细节:PINN 训练通常先用 ADAM 做几百轮粗调,再用 L-BFGS 精修。原因在于,ADAM 的收敛虽然稳定且不容易发散,但到最后阶段往往在某个精度附近徘徊;L-BFGS 作为二阶优化方法,可以在接近解的区域快速收敛,精度高一个量级。不过,MATLAB 的dlfeval配合 L-BFGS 有一点坑:你需要手动把net.Learnables拼成一个向量,然后让优化器在这个向量上迭代。下面给出 ADAM 阶段的训练代码:

% 采样配置点:内部点1000个,边界点各1个 numInternal = 1000; x = dlarray(linspace(0, 1, numInternal)', 'BC'); x0 = dlarray(0, 'BC'); xL = dlarray(1, 'BC'); u0 = 0; uL = 0; % 齐次Dirichlet边界 k = 10; % 波数 % 初始化ADAM优化器参数 trainParams = struct(); trainParams.learnRate = 0.001; trainParams.gradientDecayFactor = 0.9; trainParams.squaredGradientDecayFactor = 0.999; trainParams.epsilon = 1e-8; averageGrad = []; averageSqGrad = []; % 训练1500轮 numEpochs = 1500; for iter = 1:numEpochs [loss, grads] = dlfeval(@modelLoss, net, x, x0, u0, xL, uL, k); [net, averageGrad, averageSqGrad] = adamupdate(net, grads, averageGrad, averageSqGrad, iter, trainParams); % 每100轮打印一次loss if mod(iter, 100) == 0 fprintf('Iter %d, Loss: %.6e\n', iter, loss); end end

这里adamupdate是 MATLAB 深度学习工具箱自带函数,不需要手动实现动量项。配置点的数量numInternal = 1000在 1D 问题里已经足够,因为求解域被 1000 个点覆盖,平均间距只有千分之一,足以分辨波数 10 对应的振荡波长。如果你把波数调到 30 以上,配点密度就得相应增加,至少 2000 到 3000 个。

3.3 用解析解做验证:算完必须做的一件事

光看 loss 降到多少没有意义,因为 loss 值只代表你在加权意义上同时满足了 PDE 残差和边界残差,不代表解本身是物理正确的。对于 1D 齐次亥姆霍兹方程,解析解可以写出来,形式是:

[ u(x) = A\sin(kx) + B\cos(kx) ]

通过边界条件 (u(0)=0, u(1)=0) 可以推出 (B=0),且 (k) 必须满足 (\sin(k)=0),也就是说 (k) 必须是 (\pi) 的整数倍。为了让解析解存在且匹配零边界条件,下面验证代码中把波数设为 (k=\pi),这时候解析解是 (\sin(\pi x))。对比代码:

% 解析解:k = pi时,u(x) = sin(pi * x) x_plot = linspace(0, 1, 200)'; u_exact = sin(pi * x_plot); u_pred = extractdata(forward(net, dlarray(x_plot, 'BC'))); % 最大绝对误差 err = max(abs(u_pred - u_exact)); fprintf('Max error: %.6e\n', err); % 绘图对比 figure; plot(x_plot, u_exact, 'k-', 'LineWidth', 1.5); hold on; plot(x_plot, u_pred, 'r--', 'LineWidth', 1.5); legend('Exact: sin(pi*x)', 'PINN prediction'); xlabel('x'); ylabel('u(x)'); grid on;

这段代码里,我把网络在 200 个均匀点上的预测值和解析解做了一次性的最大绝对误差评估。如果你的输出曲线能跟解析解重合,且最大误差在 (10^{-3}) 量级以下,恭喜你,PINN 的训练是成功的。测完这一步再谈优化和扩展才有意义。

4. PINN 解亥姆霍兹方程的避坑指南:5 个踩过的具体问题

4.1 激活函数选 ReLU,二阶导数恒为零,训练直接下不去

痛点现象:训练开始时 loss 值非常大,但迭代几百轮之后 loss 不再下降,而且画出预测曲线是一条直线。

原因分析:ReLU 激活函数的一阶导数在正区间是常数 1,负区间是常数 0,二阶导数处处为 0(除了不可导点)。而亥姆霍兹方程的 PDE 残差里包含二阶导数项 (d^2u/dx^2),这个项直接归零,导致网络无法通过优化来拟合方程的振荡行为。PINN 的物理机制被激活函数切断了。

解决办法:改用二阶可导的光滑激活函数。深度学习中可用的选项有tanh、sin、sigmoid。在亥姆霍兹方程这种振荡问题里,sin激活函数反而有天然优势,因为它的导数仍然是cos,再来一次还是sin,不会产生梯度消失。但我建议优先用tanh,原因在于它的输出有界且训练稳定,后续调起来不容易出幺蛾子。

4.2 边界条件权重太小,方程对了但边界不闭合

痛点现象:PDE 残差降得很低,边界残差在 (10^{-2}) 量级徘徊不下去,最终预测曲线两端跟边界值差一截。

原因分析:我最初把 (\lambda_{PDE}) 和 (\lambda_{BC}) 都设成 1,想着"平等对待"。但在优化过程中,PDE 残差有 1000 个配点做支撑,梯度贡献大;边界残差只有 2 个点,在总损失中占比太小,梯度被淹没。优化器认为降低 PDE 残差的"性价比"更高,于是边界被牺牲了。

解决办法:把边界权重调大,我最终使用 (\lambda_{BC}=5) 到 10 之间,能有明显改善。另外一个更工程化的技巧是使用边界条件硬编码:对于 1D 问题,你可以把网络输出改写成 (u_\theta(x) = g(x) + (x-x_0)(x-x_L)\cdot N_\theta(x)),这样无论如何网络输出在边界上都自动满足零边界条件,根本不用加边界损失项。不过这种方式在边界条件不是零时稍微绕一点,需要构造一个额外的插值函数。

4.3 波数加大之后怎么调都不收敛

痛点现象:把波数从 (\pi) 调到 (8\pi),同样的网络结构和配点数,loss 怎么降都降不到合理范围,预测曲线振荡保数不对,幅值也偏。

原因分析:波数增大意味着解的振荡频率变快,网络需要更高的频率表达能力。标准的全连接网络偏向于学习低频分量,对高频分量天然钝感,这是所谓的"谱偏置"。另一个限制是配点间距:1000 个点均匀分布在 0 到 1 之间,对于波长 (2\pi/(8\pi)=0.25) 来说,每个波长只有 4 个点,网络根本没有足够的信息来分辨振荡。

解决办法:双管齐下。把配点加到 3000 以上;同时把网络从 3 层 64 宽改成 5 层 128 宽。如果还不行,就引入 Fourier 特征映射,把输入 (x) 映射成 (\sin(2\pi f x)) 和 (\cos(2\pi f x)) 的组合,让网络在输入层就具备高频信息。MATLAB 实现这个很简单,就是在featureInputLayer前手动构造扩展特征:

% Fourier特征映射:把原始坐标映射到多尺度正弦/余弦特征 freqs = [1, 2, 4, 8, 16]; % 频率集合,波数越大频率越高 x_feat = [sin(2*pi*x.*freqs), cos(2*pi*x.*freqs)];

这段代码的逻辑是:把原来的单一坐标输入变成 10 维特征向量,网络需要从这 10 个特征里学习解的模式。Fourier 特征映射是解决 PINN 高频问题最有效的杀手锏之一,代价是输入维度变高、训练时间增加。

4.4 训练前期 loss 下降很猛,后期陷入平台期

痛点现象:前 500 轮 loss 从 (10^0) 降到 (10^{-3}),然后 1000 轮过去几乎不动,无论怎么调学习率都突破不了。

原因分析:这是 ADAM 优化器的通病,它对参数的更新量级有自适应性,越到收敛后期,有效步长越小。加上 PINN 损失函数是一个多目标优化(PDE 残差和边界残差),两个目标在接近最优解时可能存在冲突,ADAM 找不到一个能同时再下降两个目标的方向。

解决办法:换优化器。2023 年之后的 MATLAB 版本已经通过dlfeval和fmincon或者fminunc提供了和 L-BFGS 对接的路径。你只需要把网络的可学习参数提取成一个向量,在fminunc的迭代中不断更新网络参数即可。这个切换通常能把最终误差再压一个量级。

4.5 随机种子不同结果差异大,同一套代码跑两次精度波动明显

痛点现象:同样的代码,同样的超参数,只是重启 MATLAB 或者换台电脑,最终误差在 (10^{-3}) 到 (10^{-5}) 之间随机波动。

原因分析:网络初始化的随机权重决定了损失曲面的起点,进而影响优化收敛到的局部最优。PINN 的损失函数面是非凸的,存在大量局部极小值点,不同初始化会走向不同结果。这是这类方法的固有特性,无法彻底消除。

解决办法:工程上采用多轮独立实验取最优的策略。写一个外层循环,用不同的rng种子初始化网络,训练 5 次,取最终误差最小的模型作为输出。每一轮的训练时间在 1D 问题上通常只有几十秒,所以多跑几轮完全可接受。这是做 PINN 必加的"后悔药",没有它就不要向别人汇报你的收敛结果。

5. 进阶验证与技巧:把 PINN 当 PDE 求解器的最后一公里

PINN 的优势不是单点解算,而是一次训练,多参数复用。以亥姆霍兹方程为例,如果波数 (k) 是可变参数,你完全可以把 (k) 也作为网络输入,让网络学习"不同波数下的解族"。这样当工程中需要在多个频率下评估响应时,不需要重新训练,一次前向传递就能得到任意 (k) 对应的解。这个思路在 MATLAB 中实现起来不算复杂——把网络输入层改为 2 个神经元,一个接收坐标 (x),一个接收波数 (k),训练时在配点上同时随机采样这两者。代价是网络容量要加大,否则网络学不出"参数的连续变化"这一类映射。

验证方面,除了和解析解对比最大误差之外,我还会做一步残差诊断:训练结束后,重新在一个更密的网格上计算 PDE 残差分布。这个步骤能帮你发现"解曲线看着对但局部波动有隐藏误差"的问题。具体做法是把预测解代入方程左边,然后画残差关于 (x) 的分布图,如果残差在某些区间明显隆起,说明那个区域的配点不够或者网络表达能力不足,需要返工。

我的习惯是:每调整一次网络结构或权重参数,就固定随机种子,先用小规模训练快速验证可行性,再放大到完整配置。这种方法避免了许多盲目的长时间训练。最后提醒一句,PINN 不是一个银弹,1D 问题它做得很漂亮,但到了 2D/3D 或者强对流问题,收敛难度会指数级上升,别抱着"换汤不换药"的预期直接套用。

如果你打算在自己的项目里认真用 PINN,我建议把这份代码框架保存成一个模板,把modelLoss的 PDE 部分替换成你自己的方程,然后严格按"先 ADAM 粗调,再 L-BFGS 精修"的流程走,这会节省你大量摸索时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询