1. 损失曲线不下降的初步定位

先分清“优化不动”还是“权重失衡”

PINN 的总损失通常是 L = L_data + λ·L_pde(有时再加边界项)。当曲线长期平坦时,最常见的两类原因截然不同:一类是优化本身失效(学习率过大导致震荡、过小导致停滞、梯度爆炸/消失),另一类是两项损失量级相差几个数量级,导致梯度被其中一项主导,另一项几乎不参与更新。定位的关键不是盯着总损失,而是把两项损失、两项梯度范数和学习率放在同一时间轴上对比。

损失不降的初步定位:三项观测 → 两类归因 观测层(每 N 步记录一次) ① 两项损失量级 L_data 与 λ·L_pde 的比值 ② 两项梯度范数 ‖∇θ L_data‖ 与 ‖∇θ λL_pde‖ ③ 学习率与更新量 lr 及参数相对变化幅度 判读层(对照阈值) 优化问题 两项梯度范数同量级但都很小 或 lr 过大 → 损失震荡/发散 或 lr 过小 → 更新量≈0 权重失衡 两项损失量级差 ≥ 2~3 个数量级 小项梯度被大项淹没 总损失几乎只反映大项 可验证的调整动作 优化问题 → 学习率扫描(如 1e-4 / 1e-3 / 1e-2),观察损失是否出现下降段;必要时加梯度裁剪 权重失衡 → 按量级比调 λ(如 λ ← λ·L_data/L_pde),或改用自适应权重;调整后复测两项梯度范数是否接近

用一段可运行的诊断代码把三项观测打出来

下面是一个最小可复现的诊断脚本:一个两层 tanh 网络拟合 u(x)=sin(πx),PDE 残差用 u''+π²u(真解使其为 0)。脚本在每个记录点分别计算数据损失、物理残差损失、两者对参数的梯度范数,并打印学习率与参数更新量,用于判断属于哪一类问题。

import torch, math

torch.manual_seed(0)
dtype = torch.float64

# 网络
net = torch.nn.Sequential(
    torch.nn.Linear(1, 32), torch.nn.Tanh(),
    torch.nn.Linear(32, 32), torch.nn.Tanh(),
    torch.nn.Linear(32, 1)
).to(dtype)

# 数据点(监督项)
x_data = torch.linspace(0, 1, 20, dtype=dtype).unsqueeze(1)
y_data = torch.sin(math.pi * x_data)

# 配点(物理残差项)
x_pde = torch.linspace(0, 1, 200, dtype=dtype).unsqueeze(1).requires_grad_(True)

lam = 1.0          # 物理项权重
lr  = 1e-3
opt = torch.optim.Adam(net.parameters(), lr=lr)

def pde_residual(x):
    u = net(x)
    du  = torch.autograd.grad(u, x, torch.ones_like(u), create_graph=True)[0]
    d2u = torch.autograd.grad(du, x, torch.ones_like(du), create_graph=True)[0]
    return d2u + (math.pi ** 2) * u

def grad_norm(loss):
    gs = torch.autograd.grad(loss, [p for p in net.parameters() if p.requires_grad],
                             retain_graph=True, allow_unused=True)
    return torch.sqrt(sum((g.pow(2).sum() for g in gs if g is not None)) + 1e-30)

for step in range(1, 2001):
    opt.zero_grad()
    L_data = ((net(x_data) - y_data) ** 2).mean()
    r = pde_residual(x_pde)
    L_pde = (r ** 2).mean()
    L = L_data + lam * L_pde

    g_data = grad_norm(L_data)
    g_pde  = grad_norm(lam * L_pde)

    L.backward()
    opt.step()

    if step % 200 == 0:
        ratio = (L_data / (lam * L_pde + 1e-30)).item()
        print(f"step {step:5d} | L_data={L_data.item():.3e} "
              f"| lam*L_pde={(lam*L_pde).item():.3e} | 比值={ratio:.2e} "
              f"| g_data={g_data.item():.3e} | g_pde={g_pde.item():.3e} "
              f"| lr={lr:.1e}")

如何读这些数字

  • 看比值:若 L_data / (λ·L_pde) 长期大于 100 或小于 0.01,说明两项量级严重失衡,总损失曲线基本只反映大项,小项“看起来没降”其实是没被优化。
  • 看梯度范数:若 g_data 与 g_pde 相差 2 个数量级以上,即使损失比值尚可,参数更新也主要被大梯度项主导,属于权重失衡的另一种表现。
  • 看学习率:若两项梯度范数同量级但都极小(如 1e-8),且参数更新量接近 0,则是学习率过小或已进入平坦区;若损失出现明显震荡或 NaN,则是学习率过大。
  • 看更新量:记录 ‖Δθ‖/‖θ‖,若长期低于 1e-6,说明优化几乎停滞,应优先排查学习率而非权重。
判断顺序建议:先看学习率与更新量排除“完全不动”,再看两项梯度范数判断谁在主导,最后用损失比值确认量级差。三者一致时结论最可靠。

可验证的调整方案

若判定为优化问题:做一次学习率扫描(如 1e-4、1e-3、1e-2 各跑 500 步),观察是否出现损失下降段;若大 lr 震荡,加入梯度裁剪 torch.nn.utils.clip_grad_norm_(net.parameters(), 1.0) 后复测。

若判定为权重失衡:按量级比调整 λ,例如初始 L_data=1e-2、L_pde=1e2,则令 λ ← λ·L_data/L_pde = 1e-4,使两项量级接近;调整后复测两项梯度范数是否落在同一数量级。也可改用自适应权重(如 GradNorm 思路:按梯度范数比动态更新 λ)。

观测现象归因验证动作通过判据
两项梯度范数同量级但均 < 1e-7,更新量≈0学习率过小/平坦区lr 放大 10 倍重跑损失出现持续下降段
损失震荡或 NaN学习率过大lr 缩小 10 倍 + 梯度裁剪曲线平滑下降
损失比值 > 100 或 < 0.01权重失衡按量级比调 λ 后复测两项梯度范数同量级
g_pde 比 g_data 小 2 个数量级以上物理项被淹没增大 λ 或改用自适应权重g_pde 提升至同量级
调整 λ 后必须重新观察两项梯度范数,而不是只看总损失是否下降——总损失下降可能只是大项继续优化,小项仍未参与训练。