☰
EIT逆问题中PINN与能量先验:从损失函数设计到训练崩溃排查
2026/10/1 17:48:51 网站建设 项目流程

简介:针对电阻抗断层扫描(EIT)图像重建中的物理信息神经网络训练难题,这套Python源码给出了一种基于能量先验的改进方案。资料面向从事科学计算、深度学习和医学成像研究的开发者,核心思路是在PINN框架中引入能量模型作为先验约束,配合score matching方法提升逆问题求解的稳定性与精度。压缩包共含35个文件、141KB,其中13个Python脚本覆盖网络结构、能量先验训练、正反向求解等环节,17个MATLAB脚本负责网格生成、边界条件处理及EIT仿真数据制作,代码目录按数据创建、正问题求解、逆问题求解和EBM先验模块划分,并附带Markdown说明文档与交互式HTML页面,便于理解整体架构和快速复现。目前已有168人学习下载。借助这份源码,可搭建EIT完整求解流水线,对比传统PINN与能量先验改进方案的差异,为后续研究或论文复现提供可直接修改的实验基础。

1. 先别急着训网络:电阻抗断层扫描为什么让 PINN 集体翻车

电阻抗断层扫描(EIT)的逆问题,本质上是从边界电压测量值反推内部电导率分布。这个问题的病态程度比图像超分辨率还高一个量级:边界上只有几十个电极的电压数据,内部却是成千上万个未知量。常规神经网络硬学这个映射,几乎必然陷入一个又一个局部极小值。物理信息神经网络(PINN)把 Maxwell 方程塞进损失函数,看似给了模型“物理约束”,但在 EIT 场景下,边界条件对内部解的约束极其薄弱,训练时 loss 曲线前几百轮看着在降,实际上网络正在走一条捷径:把电导率图抹成均匀背景,用电流场硬凑电极电压。

这正是基于能量的先验要解决的问题。它不是加一个正则化项那么简单,而是把“解的先验形状”编码进训练动力学里——让网络在最小化电压残差的同时,被能量泛函拉回平滑且有物理意义的区域。本文直接给你一套完整的 Python 源码级训练管线:EIT 正问题的仿真数据生成、PINN 损失函数组装、能量先验的两种实现方式(梯度惩罚和总变分)、以及训练过程中的权重调度和崩溃排查。适合已经在跑 PINN、被损失不收敛或反演结果空间振荡逼疯的从业者,也适合准备把 EIT 逆问题交给深度网络但还没选好正则化方案的人。

2. EIT 正反问题的损失函数设计:PINN 在这里容易学“坏”

这一章先把 EIT 的物理模型写清楚,再落到 PyTorch 代码上。只有把数学形式摆出来,后面能量先验往哪插、权重怎么调,才有据可依。

2.1 为什么要用物理信息神经网络做 EIT:边界电压到内部电导率的映射困境

传统 EIT 反投影算法(比如线性反投影、Newton-Raphson 迭代)的问题在于,它们对噪声极其敏感,相邻两次迭代的解可能差出几个数量级。深度学习方案把它们换成“端到端学习边界电压到电导率图的映射”,但训练这种纯数据驱动的映射需要海量样本,而真实 EIT 数据获取成本高:胸腔、脑部、工业管道场景都要搭建物理实验平台。

PINN 的曲线救国思路是:网络输出电导率分布 σ(x, y),电流场 u(x, y),然后用控制方程和边界条件构成损失。这样即使没有配对标签,只要有边界电压测量值,就能训练。听起来完美,但坑藏在偏微分方程的约束强度里。EIT 的正问题是椭圆型方程,在 Dirichlet 边界条件下解是唯一的;反过来,从边界测量反推内部系数,稳定性极差。PINN 训练时,网络在边界附近被损失函数强烈约束,内部区域几乎只有方程残差在“隔空指挥”,这是反演图像产生块状伪影和条纹的根本原因。

2.2 用 PyTorch 写 EIT-PINN 的损失函数:数据项、残差项与边界强制项

先落地最小可跑版本。控制方程取稳态电流场 ∇·(σ∇u) = 0,边界上有注入电流和测量电压两类条件。PINN 的总体损失分三项:电压残差(边界测量数据)、接电导率分布下的 PDE 残差、边界约束。下面这段代码在 PyTorch 下定义三部分 loss:

import torch import torch.nn as nn class EITPINNLoss(nn.Module): def __init__(self, sigma_net, u_net, electrode_pts, measured_voltages, interior_pts): super().__init__() self.sigma_net = sigma_net # 参数化电导率 σ(x,y) self.u_net = u_net # 参数化电势场 u(x,y) self.electrode_pts = electrode_pts # 电极坐标(N_e, 2) self.V_data = measured_voltages # 实测电极电压(N_e,) self.pts = interior_pts # 内部残差采样点(N_p, 2) def forward(self): # 内部采样点需要梯度,开启 requires_grad x = self.pts[:, 0].unsqueeze(1).requires_grad_(True) y = self.pts[:, 1].unsqueeze(1).requires_grad_(True) sigma = self.sigma_net(torch.cat([x, y], dim=1)) u = self.u_net(torch.cat([x, y], dim=1)) # 一阶导——链式法则在 autograd 下自动完成 u_x = torch.autograd.grad(u, x, grad_outputs=torch.ones_like(u), create_graph=True)[0] u_y = torch.autograd.grad(u, y, grad_outputs=torch.ones_like(u), create_graph=True)[0] # PDE 残差: div(sigma * grad(u)) = 0 flux_x = sigma * u_x flux_y = sigma * u_y div_flux = torch.autograd.grad(flux_x, x, grad_outputs=torch.ones_like(flux_x), create_graph=True)[0] + \ torch.autograd.grad(flux_y, y, grad_outputs=torch.ones_like(flux_y), create_graph=True)[0] pde_loss = torch.mean(div_flux ** 2) # 边界电压匹配 u_electrode = self.u_net(self.electrode_pts) data_loss = torch.mean((u_electrode - self.V_data) ** 2) # 边界电流条件(这里按电流注入电极处理,简化为零通量) # 完整实现需要按电极段面积分,先留占位 return data_loss + 1e-2 * pde_loss

关键细节在于torch.autograd.grad的create_graph=True,它保证二阶导链式法则的梯度图不中断,后面能量先验加进来时同样靠这个机制。代码里pde_loss的权重给了1e-2,这是血泪经验:EIT 的 PDE 残差项量纲和电压完全不同,PDE 残差数值天然小,一上来给同样权重,边界项会被淹没,网络直接放弃拟合测量电压。实际训练中我会先把pde_loss的权重调到1e-3甚至0,等边界电压误差降到一个稳定水平,再把物理项拉进来。

2.3 loss 权重不是玄学:先看量纲再看曲线

当前 EIT-PINN 的 loss 组成里,数据项单位是 V²,PDE 残差项单位是 A²/cm² 这类量纲换算后的值,两者相差几十个数量级。我见过不少人把三个项写成 1:1:1,训练两万步 loss 纹丝不动,就往“网络结构不够深”上找原因,其实根本没有把尺度拉开。

一组比较可靠的起步值是:数据项 1.0、PDE 残差项 1e-3~1e-4、边界通量项 1e-2。判断原则很简单:让前 200 个 epoch 里数据项占比超过总 loss 的 80%,先把边界拟合住。内部解有模糊性不可怕,那是后面能量先验的工作,不是这一步的目标。

3. 基于能量的先验:把空间振荡按进电导率图里

纯 PINN 训练出来的 EIT 反演结果,边界电压拟合得特别好,电导率图却像打翻的调色盘——相邻像素突变厉害、出现大量孤立亮斑。这正是病态逆问题的典型症状:解空间太大,网络在数据项压力下“乱找一个能匹配电压的电导率图”。能量先验在这里扮演的角色,是告诉网络“答案应该长什么样”。

3.1 为什么普通深度先验压不住高频伪影:局部极小值陷阱

有人会用“网络隐式正则化”来辩解:神经网络自带平滑偏好,不需要外部先验。这话在图像重建任务上部分成立,因为 CNN 的卷积核局部连接天然有结构先验。但 PINN 里我们用的是坐标输入的全连接网络,坐标到电导率的映射没有任何空间共享机制,隐式正则化几乎为零。Loss 平面里,“平滑电导率分布”和“剧烈振荡电导率分布”可能同时满足边界电压约束,网络在无先验引导时会随机选一个,有时甚至选择剧烈振荡的那个——因为剧烈振荡能提供更多自由度去拟合电压噪声。独立后续验证靠差分检验。

能量先验的核心思想直接来自变分正则化理论:给解定义一个能量泛函 E[σ],训练时在 loss 里加上它的加权项。常见的能量泛函有两种取向——衡量空间平滑性的梯度惩罚,和保留边缘的总变分。下文分别给出实现。

3.2 用 H1 能量先验改造损失:在梯度图上做惩罚

H1 先验把电导率梯度的大小平方积起来,惩罚剧烈的空间变化。它的作用是强制反演结果“尽量平滑”,适合电导率本身变化缓慢的工业成像场景,比如检测管道内液体分布、两层介质的界面模糊化等。实现时直接对 σ 网络求输入坐标的偏导,再算二阶范数:

def h1_energy_prior(sigma_net, pts): x = pts[:, 0].unsqueeze(1).requires_grad_(True) y = pts[:, 1].unsqueeze(1).requires_grad_(True) sigma = sigma_net(torch.cat([x, y], dim=1)) sigma_x = torch.autograd.grad(sigma, x, grad_outputs=torch.ones_like(sigma), create_graph=True)[0] sigma_y = torch.autograd.grad(sigma, y, grad_outputs=torch.ones_like(sigma), create_graph=True)[0] # 均匀介质电导率归一化:避免尺度影响权重 # 如果背景电导率是 0.1 S/m,这里需要先除以它 grad_norm_sq = sigma_x ** 2 + sigma_y ** 2 return torch.mean(grad_norm_sq)

参数核心是归一化:sigma如果直接用绝对电导率值,不同场景背景不同,先验权重的量纲跟着乱跳。常见做法是在数据预处理阶段把 σ 归一到 [0.5, 2.0] 区间,能量先验的值域就稳定了,权重系数可以从 1e-2~1e-1 起步。H1 先验有一个副作用:会把真正尖锐的边界(比如空气-水界面)抹成渐变过渡。如果你的目标本身是找出清晰的相界面,建议看下一节的 TV 先验。

3.3 先验权重怎么取:动态归一化与自适应加权策略

把 H1 项直接加到总 loss 里,权重 λ 是固定的——这有问题。训练初期边界电压残差很大,数据项占主导,先验项起不到作用;训练后期电压项已经压到 1e-4 级别,如果固定 λ 太大会把解拖向“过度平滑的均匀介质”,反而牺牲了分辨率。所以工程实现里我一般会把 λ 设计成基于梯度尺度做动态修正:

# 每 500 个 epoch 更新一次先验权重 if step % 500 == 0: with torch.no_grad(): prior_val = h1_energy_prior(sigma_net, interior_pts) data_val = data_loss_value # 让先验项占当前总 loss 的 10% 左右 lambda_prior = 0.1 * data_val / (prior_val + 1e-8)

这个做法的直观解释是:先验项的数值随着训练在变——训练越往后,电导率图越平滑,梯度模长越小,先验项分母变大,权重适当降低,避免把网络压成“什么都没了”的平庸解。注意这段代码必须在torch.no_grad()下计算,否则权重计算本身会记录梯度图,把整个计算图复杂度翻几倍,速度降至不能看。若干其他改进需要以差分形式完成。

3.4 另一种先验:总变分与自适应加权,保留边界的替代方案

总变分先验在图像反问题里尤其实用——它只罚梯度的大值,但对小梯度放行。数学形式是 |∇σ| 的积分,非光滑,反向传播时需要在 |∇σ| 接近于零处做 epsilon 平滑。这是代码上最容易踩坑的点:直接对 sqrt 求导,当梯度为零时数值变成 NaN。

def tv_prior(sigma_net, pts, eps=1e-6): x = pts[:, 0].unsqueeze(1).requires_grad_(True) y = pts[:, 1].unsqueeze(1).requires_grad_(True) sigma = sigma_net(torch.cat([x, y], dim=1)) sigma_x = torch.autograd.grad(sigma, x, grad_outputs=torch.ones_like(sigma), create_graph=True)[0] sigma_y = torch.autograd.grad(sigma, y, grad_outputs=torch.ones_like(sigma), create_graph=True)[0] grad_mag = torch.sqrt(sigma_x ** 2 + sigma_y ** 2 + eps ** 2) return torch.mean(grad_mag)

eps是这里的后悔药开关。设成 1e-4 以下会让梯度在某些像素上爆炸,设成 1e-1 以上又近似于 L2 平滑先验。用得多的工业 EIT 反演项目里,过渡带处理一般给 1e-3。

另外 TV 先验对梯度图会过度惩罚孤立噪声,如果电极数量少、测量噪声大,TV 出来的结果边缘是锋利,但也容易把噪声一起“切成”小块轮廓。这时候可以考虑各向异性版本的 TV(只在某个方向约束梯度),不过这已经超出标题范围,先不提。

4. 仿真到训练管线:在合成数据上完整跑通 EIT 反演

这一节直接给完整可跑的落地路径。你不需要先有物理实验台,用合成正问题数据就能验证整套方案的可行性。数据管线分四步:正问题求解生成电压样本、构建双分支网络、分阶段训练、崩溃诊断。其中崩溃诊断是 90% 从业者在深夜折磨的主战场。

4.1 用有限元生成 EIT 正问题的合成数据集

正问题指的是:给一个已知电导率分布,求解边界电压。这个方向用传统有限元做又快又稳。工程里常见的做法是把计算域剖分成三角形网格,组装刚度矩阵,施加电流边界条件后求解线性方程组。为了不让读者额外安装复杂 FEM 引擎,下面给出用 PyTorch 解稀疏线性系统的方式(同样能用 scipy.sparse 替换):

import numpy as np from scipy.sparse import lil_matrix from scipy.sparse.linalg import spsolve def solve_forward_eit(mesh, sigma_cell, current_pattern): """ mesh: 有限元网格,包含 vertices 和 elements sigma_cell: 每个单元的电导率值 current_pattern: 电极注入电流向量 """ n_vertices = mesh['vertices'].shape[0] K = lil_matrix((n_vertices, n_vertices)) for elem in mesh['elements']: idx = elem # 三个顶点索引 v0, v1, v2 = [mesh['vertices'][i] for i in idx] area = 0.5 * abs((v1[0] - v0[0]) * (v2[1] - v0[1]) - (v1[1] - v0[1]) * (v2[0] - v0[0])) grad_basis = np.array([ [v1[1] - v2[1], v0[1] - v2[1]], [v2[0] - v1[0], v0[0] - v2[0]], ]) / (2 * area) sigma_e = sigma_cell[idx].mean() K_local = sigma_e * area * (grad_basis @ grad_basis.T) for i in range(3): for j in range(3): K[idx[i], idx[j]] += K_local[i, j] K = K.tocsr() # 处理电极边界条件,通常每个电极强制给定电压/电流 u = np.zeros(n_vertices) # 这里简化为在一个电极注入,另一个电极接地 u[spsolve(K, current_pattern)] return u

这版代码没有处理完整的电极模型,但对合成训练数据来说够用了——我们要的不是精确模拟物理平台,而是生成“数学上一致的电压-电导率配对样本”。每个样本:随机生成若干个圆形异常区域叠加在背景电导率上,跑一次正问题得到边界电压向量,存下来。一个 32 电极的模型,生成 2000 个样本并不需要太多时间。

注意采样策略:异常区域的半径、位置、电导率相对背景的倍数,需要在合理范围均匀采样,因为反问题的病态性意味着某些组合天然难反演(例如异常体正好在两个电极的连线深处)。如果训练集里这种困难样本比例过高,网络会倾向于输出“平均解”而不是按输入电压变化。用批量预处理来平衡种类比例。

4.2 把 σ(x,y) 和 u(x,y) 拆成两个分支:联合训练时禁止梯度穿越

网络上同时需要两个输出:σ 和 u。常见做法是共享底层特征,然后分叉成两个头。但 EIT 场景下我建议完全拆开成两个独立网络——原因在于:u 场受边界影响巨大、变化剧烈,而 σ 场在真实物理中通常是分片常数。共享底层会让 σ 分支被 u 的高速变化拖累,训练损失降不下去。

class TwoBranchNet(nn.Module): def __init__(self, hidden_dim=128, n_layers=4): super().__init__() layers = [] for i in range(n_layers): layers.append(nn.Linear(2 if i == 0 else hidden_dim, hidden_dim)) layers.append(nn.Tanh()) self.sigma_branch = nn.Sequential(*layers) # 独立分支 self.u_branch = nn.Sequential(*layers) # 独立分支 def forward(self, xy): # 两个分支分别输入同样的坐标 sigma = self.sigma_branch(xy) u = self.u_branch(xy) return sigma, u

使用 Tanh 激活函数是关键选择:ReLU 在坐标回归问题里会产生不可导的 kink 点,而 PINN 要求损失函数对坐标求高阶导,ReLU 的二阶导处处为零,直接废掉 PED 残差里的二阶项。这段网络看起来平淡,但训练技巧有一点与普通网络训练不同——两个分支的收敛速度天然不平衡,一般 u 分支收敛快得多。建议每 500 轮检查一次 σ 分支输出的图像,如果电导率图还是一片均匀背景色,立刻把 u 分支的学习率降为 σ 分支的 1/5,不要硬等。

4.3 分阶段调度:先拟合边界、再让先验介入、最后微调

把多任务损失做成固定权重是一种“静态死局”。训练早期 σ 还没成形,H1/TV 先验的梯度方向会把 σ 往平面推,导致边界电压误差永远停在高位。反过来,如果一直不叠加先验,又回到纯 PINN 病态解。工程上高可复现的方案是三段式调度:

  1. 阶段一(0~1000 epoch):只优化边界电压数据项,σ 分支几乎自由发展,u 分支优先收敛。这一步的目的是让网络先去理解电极电压和内部场的大致联系。
  2. 阶段二(1000~5000 epoch):加入 PDE 残差项和能量先验项,权重按 2.3 节给出的动态方案调整。此时边界电压已经不离谱,先验开始修正 σ 的空间结构,抹掉孤立斑点和块状伪影。
  3. 阶段三(5000 epoch 后):把能量先验权重衰减到原来的 40%,恢复数据项主导。目的是让边界电压的细微信号重新 “雕刻” σ 图的边界轮廓,避免先验过度把边缘磨平。

每个阶段的切换指标,不推荐按 epoch 硬设,而应该看数据项实际降幅:当数据项相比初始值下降超过两个量级时,才进入阶段二。这个规则比“跑到第 N 轮”可靠,因为不同数据规模、不同电极数下网络收敛速度完全不同。

4.4 检查训练崩溃:Loss 曲线教你读症状而不是瞎调

训练 EIT-PINN 时最折磨人的是 loss 曲线一切正常,但反演图像清晰度十分可疑。因此在每个阶段结束时,要额外诊断 σ 分支输出。下面给出三个高频现象和对应处理:

现象一:数据项在前 200 轮下跌后卡在某个值,Image 显示 σ 分支输出全部退回背景值。原因通常是电流注入幅度太小,边界电压数值在 1e-6 级别,网络觉得“全设成背景也能凑合”。解决:把电压数据归一化到 [0, 1],或者把电流注入幅度调大。

现象二:σ 图出现棋盘格噪声,且越训练越明显。这在坐标全连接网络里属于高频自由度滥用的典型表现。此时能量先验权重太小,建议把权重往上提一个数量级。如果 H1 提了还无效,务必检查是不是偏导代码把create_graph漏了,导致先验项实际并未参与反向传播。

现象三:训练到后期 loss 反而上涨。这里要看是哪一项在涨——如果是 PDE 残差项在涨,大概率是 σ 内部出现了跳变,使得二阶导在跳变处产生尖峰;如果是数据项在涨,说明训练步长过大,u 分支刚跨过一个陡峭的 loss 峡谷。把 Adam 的 learning_rate 调低一个数量级重启。

5. 训练翻车现场:EIT-PINN 的高频报错与排查方法

本章把我在多个 EIT-PINN 项目里碰到的坑统一列出来。每一条都按“现象 → 原因 → 解决”的路线给到可操作方案。这里没有“理论上的建议”,全是实际代码运行时的选择题。

5.1 autograd 报错“grad can be implicitly created only for scalar outputs”

现象:调用torch.autograd.grad时引擎直接抛异常,要求输出先做sum()。

原因:torch.autograd.grad的grad_outputs参数要求输出是标量对应的向量,而u_net输出的是每个采样点的 u 值向量。当u是形状 (N,1) 的向量时,内部自动处理时某些版本会在非标量输出上报错。

解决:

# 加一行显式求和,再对和求梯度 u_sum = u.sum() u_x = torch.autograd.grad(u_sum, x, grad_outputs=torch.ones_like(u_sum), create_graph=True)[0]

关键区别在于:这里把对向量的梯度换成了对各分量求和的梯度,数学上结果是完全一致的。省掉了 grad_outputs 的维度匹配问题,同时 autograd 计算的图更简单。

5.2 训练开始第一个 step 就出 NaN:大概率在初值或除零

现象:loss 打印出来直接是 nan,或者前几十步还算正常、某一步突然跳过 nan。

原因两类:边界电压包含零值(某个电极没测到信号),但归一化过程里除了它;或者 TV 先验里的sqrt当grad_mag极小时,梯度爆炸。另一类是初始化的锅——全连接网络的权重初始化为默认 uniform 分布,对高维输入和二阶导计算来说,初始化值过大导致前向和反向乘积溢出。

解决:

  • 电压归一化时对电压向量加1e-12偏移再除最大值;
  • 把网络每一层接上nn.utils.clip_grad_norm_,给梯度总范数设 10.0 上限;
  • 初始化改成 Xavier,且把最后一层线性层偏置设 0,权重乘 0.1 缩放。

5.3 生成合成数据时网格太粗导致网络“完美拟合错误解”

现象:训练切一切,边界电压 loss 降到极低,但是把 σ 图打印出来和真实分布完全对不上,形状完全错位。

原因:合成数据生成用的有限元网格太稀疏。正问题求解时每个单元上的 σ 假设为常数,如果异常体直径小于网格尺寸,异常体“看起来”不存在。PINN 看到的是“均匀介质下生成的电压”,自然反演不出异常体。

解决:

# 生成网格时检查最小边长 min_edge = compute_min_edge_length(mesh) anomaly_radius_min = 3 * min_edge # 半径至少三个网格边长

这条规则免费送你:异常体半径至少三倍网格边长,否则合成数据自欺欺人。进阶准则是:用两套不同密度的网格分别做正问题,电压结果差距超过 0.5% 时,粗网格不可用。

5.4 能量先验没起作用:看起来加上了,反演图像照旧

现象:loss 曲线里先验项确实在下降,但 σ 图仍然高频振荡。

原因:先验项只计算在“内部采样点”上,而那些点是用均匀分布随机生成的。均匀分布在二维平面上的特性是:对边界的覆盖极差。如果采样点恰好集中在中心区域,边缘附近的 σ 振荡完全不被先验惩罚;而 EIT 的图像特征恰恰经常出现在靠近电极的区域。

解决:生成采样点时使用 Farthest Point Sampling 或者直接叠加边界区域的密集采样:

# 内部均匀采样 + 边界附近高斯扰动采样 interior_pts = torch.rand(5000, 2) # 边界环形区域加密:半径 0.8~1.0 之间 theta = torch.rand(2000, 1) * 2 * np.pi radius = 0.8 + 0.2 * torch.rand(2000, 1) boundary_pts = torch.cat([radius * torch.cos(theta), radius * torch.sin(theta)], dim=1) all_pts = torch.cat([interior_pts, boundary_pts], dim=0)

5.5 两个分支任务收敛严重不平衡,σ 分支一直不动

现象:u 分支的 loss 已经到 1e-5,σ 分支输出始终是初始化的均匀色块,数据项不再下降。

原因:这个是 Adam 优化器的节奏问题。u 分支的梯度幅度大、更新方向稳定,它每月都在抢优化器的“注意力”;σ 分支的梯度幅度小、更新方向容易在病态区域被噪声淹没。对两个分支分别设置不同学习率是常用解法,但更省心的是给两个分支各自一个 Adam,让优化器状态互不干扰。

u_optimizer = torch.optim.Adam(u_net.parameters(), lr=1e-3) sigma_optimizer = torch.optim.Adam(sigma_net.parameters(), lr=1e-3) # 每轮交替更新,且每 500 轮检查一次 loss.backward() if data_loss > 2e-4: u_optimizer.step() sigma_optimizer.step()

参数上,我一般让 u 分支的 lr 是 σ 分支的 1 到 2 倍之间浮动,通过观察 u 分支收敛停滞与否来切换。另外一个经验值:σ 分支的梯度幅度如果均值低于 1e-6,考虑把网络最后一层权重初始化放大 10 倍,促使前向输出更明显的变化。

6. 验证这套方法的两个关键实验:L-curve 和误差可视化

训练完了别急着上报告,先做一个确定性能边界的验证流程。这里把两种在 EIT 社区被普遍认可的方法用代码落地:L-curve 定先验权重、解码 τ 分割图的残差可视化。

6.1 用 L-curve 选先验权重:踩过最强简单 CR 的 Dia 套路

L-curve 的思路是画先验项对数据项的帕累托曲线——权重小时先验项大但数据项小,权重太大则反之。曲线曲率最大的位置对应最优权重的经验挑选。实现路径:固定权重集合从 1e-3 到 1e1 对数间隔取 10 个值,各训一短程模型,记录数据项 loss 和先验项 loss,再在 log-log 坐标下列出点集:

import matplotlib.pyplot as plt weights = [1e-3, 3e-3, 1e-2, 3e-2, 1e-1, 3e-1, 1.0, 3.0, 10.0] data_losses, prior_losses = [], [] for lam in weights: # 重训一遍相同配置的模型,记录最终两项 loss model = build_model() d_loss, p_loss = train_with_prior(model, lam, epochs=600) data_losses.append(d_loss) prior_losses.append(p_loss) # 绘图 plt.loglog(prior_losses, data_losses, 'o-') plt.xlabel('Energy Prior Loss') plt.ylabel('Data Loss (V^2)') plt.title('L-curve for EIT-PINN hyperparameter selection')

曲率最大的转折点即为推荐权重。这个办法“老派但可靠”,尤其对你在线上和实验团队 battle 超高分辨率时特别有力度,因为它把“我调了先验权重”变成一条曲线供审阅。注意一个陷阱:每个权重都要从同一个随机种子开始训,否则网络初始化的随机性会把 L-curve 点阵搞得上下跳动,找不出干净转折点。

6.2 残差图判断反演质量:边界贴合度高不等于内部解正确

PINN 训练完成后的常规迷惑行为是只看电极电压误差。这个指标在病态反问题里几乎无意义,原因在第二章讲过——不同电导率分布完全可以产生非常接近的边界电压。验证时必须做两件事:

第一,把 σ 反演结果重新代入正问题求解器,生成新的边界电压,与初始的实测/合成电压做逐电极差值。第二,把这个差值画在电极序号坐标上,观察是否有单个电极突然偏差过大(表明 PINN 在网络边界处产生了局部过拟合)。在代码里,正问题复用 4.1 节的 FEM 函数。差值向量如果所有电极都在 1% 以内,训练合格;如果某一个或连续几个电极偏大,大概率是这两个电极之间的边界区域 σ 估计错误。

另外推荐在合成数据上多跑几十个随机生成的异常体样本,统计反演电导率图的归一化均方误差和结构相似度指数。EIT 这种病态问题,单样本的视觉美感不可靠,统计分布才是判断方法可复现性的依据。我自己保留的习惯是:每次训练完毕输出一张组合图——左边真实 σ 图、中间反演 σ 图、右边残差热力图。残差图中心区域的亮斑如果持续出现在多个样本的同一位置,那多半是网络结构偏差而不是数据偶然性。

这个方案的落地路径到这里基本完整了。能量先验的选择、权重调度、采样策略和验证协议,每个环节都有明确的参数和诊断标志位,你照跑一轮大概就能看到基线结果。希望帮到你。

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

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

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

立即咨询