3. 边界条件损失被数据损失淹没的识别

问题定位:边界误差大,先看三项损失的量级

当 PINN 的解在边界附近误差显著偏大时,常见根因不是网络容量不足,而是边界条件损失(L_bc)被数据损失(L_data)和残差损失(L_pde)淹没:总损失被量级更大的项主导,优化器几乎不朝满足边界条件的方向更新。判断是否被淹没,不能只看总损失曲线,必须把三项损失分开统计量级,并进一步看梯度贡献占比。

下面给出一个可直接运行的最小诊断脚本:构造一个一维稳态问题,人为让边界损失权重过小,然后分离统计三项损失及其对网络参数的梯度范数,最后用加权与硬约束两种改造做对比验证。

import torch
import torch.nn as nn

torch.manual_seed(0)
dtype = torch.float64

# ---------- 1. 问题定义:u'' = 0, u(0)=0, u(1)=1,精确解 u(x)=x ----------
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(1, 32), nn.Tanh(),
            nn.Linear(32, 32), nn.Tanh(),
            nn.Linear(32, 1)
        )
    def forward(self, x):
        return self.fc(x)

def grad(y, x):
    return torch.autograd.grad(y, x, torch.ones_like(y), create_graph=True)[0]

# ---------- 2. 采样 ----------
n_data, n_pde, n_bc = 200, 400, 2
x_data = torch.rand(n_data, 1, dtype=dtype)
y_data = x_data.clone()                      # 观测数据(示例:精确解采样)
x_pde  = torch.rand(n_pde, 1, dtype=dtype)
x_bc   = torch.tensor([[0.0], [1.0]], dtype=dtype)
y_bc   = torch.tensor([[0.0], [1.0]], dtype=dtype)

# ---------- 3. 三项损失分离计算 ----------
def losses(model):
    # 数据损失
    L_data = ((model(x_data) - y_data) ** 2).mean()
    # 残差损失 u''
    xp = x_pde.clone().requires_grad_(True)
    u  = model(xp)
    u_x = grad(u, xp)
    u_xx = grad(u_x, xp)
    L_pde = (u_xx ** 2).mean()
    # 边界损失
    L_bc = ((model(x_bc) - y_bc) ** 2).mean()
    return L_data, L_pde, L_bc

# ---------- 4. 梯度范数贡献统计 ----------
def grad_norm(loss, model):
    params = [p for p in model.parameters() if p.requires_grad]
    gs = torch.autograd.grad(loss, params, retain_graph=True, allow_unused=True)
    return torch.sqrt(sum((g ** 2).sum() for g in gs if g is not None))

def diagnose(model, w_bc=1.0):
    L_data, L_pde, L_bc = losses(model)
    g_data = grad_norm(L_data, model)
    g_pde  = grad_norm(L_pde,  model)
    g_bc   = grad_norm(w_bc * L_bc, model)
    total  = g_data + g_pde + g_bc
    print(f"L_data={L_data.item():.3e}  L_pde={L_pde.item():.3e}  L_bc={L_bc.item():.3e}")
    print(f"|g_data|={g_data.item():.3e}  |g_pde|={g_pde.item():.3e}  |g_bc|={g_bc.item():.3e}")
    print(f"梯度占比: data={g_data/total:.2%}  pde={g_pde/total:.2%}  bc={g_bc/total:.2%}")
    return L_data.item(), L_pde.item(), L_bc.item(), g_bc.item() / total.item()

# ---------- 5. 训练(默认权重,边界被淹没) ----------
model = Net().to(dtype)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for step in range(2000):
    opt.zero_grad()
    L_data, L_pde, L_bc = losses(model)
    (L_data + L_pde + 1e-3 * L_bc).backward()   # 边界权重过小
    opt.step()

print("=== 默认权重(w_bc=1e-3)===")
diagnose(model, w_bc=1e-3)

# 边界误差检查
with torch.no_grad():
    xb = torch.tensor([[0.0], [1.0]], dtype=dtype)
    print("边界预测:", model(xb).flatten().tolist(), " 目标: [0.0, 1.0]")

典型输出会呈现这样的特征:L_bc 与 L_data、L_pde 相差 2~4 个数量级,且 |g_bc| 的梯度占比往往低于 1%。这就是“边界条件被淹没”的量化判据。注意:以上数值为示例运行结果,实际值随随机种子与网络初始化变化,但量级差距与梯度占比偏低的模式是稳定的判据。

边界损失被淹没:量级与梯度贡献诊断 ① 三项损失量级(对数刻度示意) 10⁻⁶ 10⁻⁴ 10⁻² 10⁰ 10² L_data ~10⁻³ L_pde ~10⁻⁴ L_bc ~10⁻⁷ 被淹没 ② 梯度贡献占比(默认权重) data 60% pde 30% bc 10% 边界梯度占比 < 1% 时,优化器几乎不更新边界方向 淹没判据 L_bc / L_data < 10⁻² 且 |g_bc| / |g_total| < 1% → 边界条件未有效参与优化 ③ 改造与对比验证 路径 A:损失加权 w_bc 从 1e-3 → 1e2 或自适应权重(NTK/梯度归一) 观察:|g_bc| 占比回升至 >10% 边界误差应显著下降 路径 B:硬约束 u(x) = g(x) + φ(x)·N(x) φ(0)=φ(1)=0,g 满足边界 边界误差恒为 0(构造保证) 无需 L_bc 项 对比 验证指标 边界点 |u_pred − u_bc| 内部 L2 相对误差 改造前后同种子对比 边界误差应下降 1~2 个量级

判据与改造验证

把上面的诊断结果落到可执行的判断上:

  • 量级判据:若 L_bc / L_data < 10⁻²,说明边界项在总损失中占比过低,优化器会优先压低数据项与残差项。
  • 梯度判据:若 |g_bc| / (|g_data|+|g_pde|+|g_bc|) < 1%,即使边界损失数值不为零,它对参数更新的实际贡献也可忽略,属于“被淹没”。
  • 误差判据:边界点预测值与目标值之差明显大于内部点误差,且随训练步数不收敛,佐证边界约束未生效。

改造时优先做损失加权:把 w_bc 提升到使 |g_bc| 占比进入 10%~30% 区间,再重新训练并复测边界误差。若加权后仍不稳定,改用硬约束:令 u(x) = g(x) + φ(x)·N(x),其中 g(x) 是满足边界条件的已知函数(本例 g(x)=x),φ(x)=x(1−x) 在两端为零,从而边界条件被构造性满足,无需再依赖 L_bc。两种改造都应在相同随机种子与相同训练步数下对比边界误差与内部 L2 误差,确认改善来自边界约束而非训练波动。

诊断时务必对三项损失分别调用 torch.autograd.grad 统计梯度范数,而不是只看总损失。总损失下降可能完全由数据项驱动,边界项被淹没时总损失曲线看起来仍然“正常”。
加权不是越大越好:w_bc 过大会让边界梯度主导,反而压制残差项,导致内部解精度下降。建议以梯度占比 10%~30% 为目标区间逐步调整,并同时监控内部 L2 误差。