4. 配点采样密度不足导致的残差振荡

问题现象与判据

当 PINN 的整体损失曲线看起来平稳下降,但把物理残差 r(x) = N[u](x) - f(x) 按空间位置画出来时,出现局部高频振荡(相邻配点间残差正负跳变、幅值远高于周边),而全局残差均值并不大,这通常不是网络容量或损失权重的问题,而是配点采样密度不足:稀疏区域里网络缺少约束,残差在配点之间自由“摆动”,只在配点处被压小。

判据可以量化为:把求解域划分为若干等宽区间,统计每个区间的配点数 n_i 与残差均方根 RMS_i。若存在区间满足 RMS_i > 3 × median(RMS) 且 n_i < 0.5 × 平均配点数,即判定为“稀疏—振荡”耦合区,需要加密。

残差分布 vs 配点密度(同一求解域 x∈[0,1]) 物理残差 r(x) 局部高频振荡 平滑 平滑 配点密度(每 0.05 区间配点数) 稀疏区:n_i 明显偏低 → 残差振荡 密集区:残差被有效压制 判据:RMS_i > 3×median(RMS) 且 n_i < 0.5×平均配点数 → 判定为稀疏—振荡耦合区

定位稀疏区:残差—密度联合诊断

不要只看总损失。把训练后的模型在细网格上评估残差,同时统计训练配点的空间分布,两者叠加即可定位问题区。下面给出可直接运行的诊断脚本(PyTorch,示例为一维 Poisson 问题 -u'' = f,u(0)=u(1)=0,真解 u=sin(πx))。

import torch, numpy as np

torch.manual_seed(0)
# 示例:初始配点——均匀采样 200 个,故意在 [0.35,0.65] 留稀疏
x_col = torch.rand(200, 1)
mask = (x_col > 0.35) & (x_col < 0.65)
x_col = x_col[~mask]                      # 稀疏区几乎无配点
x_bc  = torch.tensor([[0.0], [1.0]])

def residual(model, x):
    x = x.clone().requires_grad_(True)
    u = model(x)
    du  = torch.autograd.grad(u.sum(), x, create_graph=True)[0]
    d2u = torch.autograd.grad(du.sum(), x, create_graph=True)[0]
    f = (np.pi**2) * torch.sin(np.pi * x)   # 真解代入的源项
    return d2u + f                          # -u'' - f = 0 的残差

def diagnose(model, bins=20):
    grid = torch.linspace(0, 1, 2001).view(-1, 1)
    r = residual(model, grid).detach().numpy().ravel()
    edges = np.linspace(0, 1, bins + 1)
    idx = np.clip(np.digitize(grid.numpy().ravel(), edges) - 1, 0, bins - 1)
    rms = np.array([np.sqrt((r[idx == i]**2).mean()) for i in range(bins)])
    n   = np.array([(idx == i).sum() for i in range(bins)])
    # 训练配点密度
    xc = x_col.numpy().ravel()
    n_col = np.histogram(xc, bins=edges)[0]
    return edges, rms, n_col

# 训练后调用
# edges, rms, n_col = diagnose(model)
# sparse = (rms > 3*np.median(rms)) & (n_col < 0.5*n_col.mean())
# print("稀疏—振荡区:", edges[:-1][sparse], edges[1:][sparse])

若输出显示稀疏区集中在 [0.35, 0.65] 且该段 RMS 显著高于中位数,即确认采样密度不足是振荡主因,而非网络或权重问题。

自适应加密采样实验

验证思路:保持网络结构、优化器、损失权重完全不变,仅对稀疏区加密配点,再训练同样轮数,对比残差分布是否变平滑。加密策略采用“残差驱动 + 密度补偿”:在残差 RMS 高的区间按比例追加配点。

def adaptive_refine(x_col, edges, rms, n_col, extra=300):
    # 每个区间的追加权重:残差高、密度低 → 权重大
    w = rms / (n_col + 1e-6)
    w = w / w.sum()
    counts = np.random.multinomial(extra, w)
    new_pts = []
    for i, c in enumerate(counts):
        if c > 0:
            lo, hi = edges[i], edges[i+1]
            new_pts.append(lo + (hi - lo) * torch.rand(c, 1))
    return torch.cat([x_col] + new_pts, dim=0)

# 实验流程
# 1) 基线:用原始 x_col 训练 N 轮,记录 diagnose 结果
# 2) 加密:x_col2 = adaptive_refine(x_col, edges, rms, n_col, extra=300)
# 3) 用 x_col2 重新训练同结构模型 N 轮
# 4) 对比两组的 rms 曲线与 max(rms)/median(rms) 比值

判断标准:加密后 max(RMS)/median(RMS) 应从基线的大于 3 降到接近 1.5 以内,且稀疏区残差曲线不再出现相邻点正负跳变。若加密后振荡仍在,则需转向检查网络容量或损失权重,而非继续加配点。

加密时保持总配点数可控(如从 200 增至 500),避免一次性把全域均匀加密——那会掩盖“稀疏区”这一诊断结论,也无法证明是密度而非总量在起作用。
残差计算必须用 create_graph=True 的二阶导,且源项 f 要与方程符号一致;符号写反会让残差整体偏移,误判为振荡。