PINNs的参数反演——从“正问题”到“逆问题”的工程改造

文章目录

  • PINNs的参数反演------从"正问题"到"逆问题"的工程改造
    • 一、从正问题到逆问题的最小改动:把未知常数设为nn.Parameter
      • [1.1 正问题的代码模板](#1.1 正问题的代码模板)
      • [1.2 逆问题最小改动:k变成nn.Parameter](#1.2 逆问题最小改动:k变成nn.Parameter)
      • [1.3 训练循环的改动](#1.3 训练循环的改动)
    • 二、数据项的引入:观测点的loss怎么写、权重怎么设
      • [2.1 数据项的位置决定了它"管什么"](#2.1 数据项的位置决定了它“管什么”)
      • [2.2 数据项权重的设定](#2.2 数据项权重的设定)
      • [2.3 数据自洽性:一个被忽视的工程前提](#2.3 数据自洽性:一个被忽视的工程前提)
      • [2.4 尺度锚定:为什么 k(x) 会整体偏低 30%](#2.4 尺度锚定:为什么 k(x) 会整体偏低 30%)
    • [三、空间变化参数场的处理:用子网络表示 k ( x ) k(x) k(x)](#三、空间变化参数场的处理:用子网络表示 k ( x ) k(x) k(x))
      • [3.1 全局常数 vs 空间变化场](#3.1 全局常数 vs 空间变化场)
      • [3.2 计算图拼接:梯度如何从PDE残差反传到参数网络](#3.2 计算图拼接:梯度如何从PDE残差反传到参数网络)
      • [3.3 训练不稳定的工程对策](#3.3 训练不稳定的工程对策)
    • 四、一维稳态Darcy流的完整代码:正问题到逆问题逐行改造
      • [4.1 问题设定](#4.1 问题设定)
      • [4.2 完整逆问题代码](#4.2 完整逆问题代码)
      • [4.3 正问题版本到逆问题版本的改动清单](#4.3 正问题版本到逆问题版本的改动清单)
    • 五、调试记录:从37%到12%的四轮迭代
      • [5.1 第 1 轮:k 被压成常数线](#5.1 第 1 轮:k 被压成常数线)
      • [5.2 第 2 轮:调参无效](#5.2 第 2 轮:调参无效)
      • [5.3 第 3 轮:用有限差分生成自洽数据](#5.3 第 3 轮:用有限差分生成自洽数据)
      • [5.4 第 4 轮:加 flux 观测锚定尺度](#5.4 第 4 轮:加 flux 观测锚定尺度)
      • [5.5 关于 L r e g L_{reg} Lreg 上升的解释](#5.5 关于 L r e g L_{reg} Lreg 上升的解释)
    • [六、反演精度的评估:相对误差 vs 训练epoch](#六、反演精度的评估:相对误差 vs 训练epoch)
      • [6.1 三个阶段的收敛特征](#6.1 三个阶段的收敛特征)
      • [6.2 反演误差收敛曲线](#6.2 反演误差收敛曲线)
    • 七、与MCMC结合的思路:PINN做前向代理,MCMC做后验采样
      • [7.1 PINN+MCMC的两阶段框架](#7.1 PINN+MCMC的两阶段框架)
      • [7.2 什么时候值得上MCMC](#7.2 什么时候值得上MCMC)
    • 八、工程落地的七个关键经验
    • 参考资料

PINNs的参数反演------从"正问题"到"逆问题"的工程改造

代码仓库GitHub - PINN-Inverse-Darcy

如果你在做的是热传导方程,把本文的Darcy方程 − ∇ ⋅ ( k ∇ h ) = f -\nabla \cdot (k \nabla h) = f −∇⋅(k∇h)=f 换成 − ∇ ⋅ ( λ ∇ T ) = q -\nabla \cdot (\lambda \nabla T) = q −∇⋅(λ∇T)=q,未知参数从渗透率 k k k 换成热导率 λ \lambda λ 即可。如果你在做的是弹性力学问题,把未知参数换成杨氏模量 E ( x ) E(x) E(x) 即可。

做PINNs正问题的时候,方程里的系数是已知的------你只需要让网络拟合解 u ( x , t ) u(x,t) u(x,t)。但做反问题的时候,方程里有一项是未知的,你要从稀疏的观测数据中把它反推出来。这两件事在代码上只差几行,但在计算图结构上,差的是整条梯度回传路径。

这篇以"把正问题代码改造成逆问题代码"为主线,把每一处需要动的地方都标出来。正问题用一维稳态Darcy流:

− d d x ( k ( x ) d h d x ) = f ( x ) , x ∈ 0 , 1 -\frac{d}{dx}\left(k(x) \frac{dh}{dx}\right) = f(x), \quad x \in 0, 1 −dxd(k(x)dxdh)=f(x),x∈0,1

其中 h ( x ) h(x) h(x) 是水头, k ( x ) k(x) k(x) 是渗透率, f ( x ) f(x) f(x) 是源汇项。正问题是已知 k ( x ) k(x) k(x) 求 h ( x ) h(x) h(x),逆问题是已知 h ( x ) h(x) h(x) 的部分观测值求 k ( x ) k(x) k(x)。

这篇的独特之处在于,它不仅给出"一次跑通"的代码,还完整记录从 37% 到 12% 的四轮调试过程。 大多数PINN反演教程的失败,不是算法问题,而是数据不自洽尺度不确定性这两个被忽视的工程前提。

一、从正问题到逆问题的最小改动:把未知常数设为nn.Parameter

1.1 正问题的代码模板

先看正问题的完整实现------这是改造的起点:

python 复制代码
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

class DarcyPINN(nn.Module):
    """一维稳态Darcy流正问题PINN
    方程: -d/dx(k * dh/dx) = f(x)
    """
    def __init__(self, layers=[1, 64, 64, 64, 1]):
        super().__init__()
        self.layers = nn.ModuleList()
        for i in range(len(layers) - 1):
            self.layers.append(nn.Linear(layers[i], layers[i+1]))
        # 自适应斜率,加速收敛
        self.alpha = nn.Parameter(torch.ones(1) * 1.0)

    def forward(self, x):
        """输入: x (N, 1); 输出: h (N, 1)"""
        inputs = x
        for layer in self.layers[:-1]:
            inputs = layer(inputs)
            inputs = torch.tanh(self.alpha * inputs)
        return self.layers[-1](inputs)

# 正问题: 渗透率k是已知常数
K_TRUE = 1.0  # 均匀渗透率

def compute_pde_residual(model, x, k=K_TRUE):
    """计算PDE残差: f = -d/dx(k * dh/dx) - source"""
    x.requires_grad_(True)
    h = model(x)

    # 一阶导: dh/dx
    h_x = torch.autograd.grad(h, x, torch.ones_like(h), create_graph=True)[0]
    # k * dh/dx
    k_h_x = k * h_x
    # 二阶导: d/dx(k * dh/dx)
    k_h_xx = torch.autograd.grad(k_h_x, x, torch.ones_like(k_h_x), create_graph=True)[0]

    # 源汇项 (假设常数)
    source = torch.ones_like(x) * 1.0
    f = -k_h_xx - source
    return f

正问题的代码很直接:k 是一个常数,直接乘在 h_x 上就行。

1.2 逆问题最小改动:k变成nn.Parameter

现在假设我们不知道 k k k 是多少,但有一些 h ( x ) h(x) h(x) 的观测数据。逆问题的第一步改造极其简单:

python 复制代码
class InverseDarcyPINN(nn.Module):
    """一维稳态Darcy流逆问题PINN
    方程: -d/dx(k * dh/dx) = f(x)
    未知: 渗透率k (全局常数)
    """
    def __init__(self, layers=[1, 64, 64, 64, 1]):
        super().__init__()
        self.net = DarcyPINN(layers)
        # 关键改动: k从常数变成可训练参数
        # 用log参数化保证k始终为正 (渗透率必须大于0)
        self.log_k = nn.Parameter(torch.tensor(0.0))

    @property
    def k(self):
        """可训练的渗透率,通过exp保证正值"""
        return torch.exp(self.log_k)

    def forward(self, x):
        return self.net(x)

def compute_pde_residual_inverse(model, x):
    """逆问题的PDE残差: k来自模型参数"""
    x.requires_grad_(True)
    h = model(x)

    h_x = torch.autograd.grad(h, x, torch.ones_like(h), create_graph=True)[0]
    # 这里用model.k而不是外部传入的k
    k_h_x = model.k * h_x
    k_h_xx = torch.autograd.grad(k_h_x, x, torch.ones_like(k_h_x), create_graph=True)[0]

    source = torch.ones_like(x) * 1.0
    f = -k_h_xx - source
    return f

为什么用 log_k 而不是直接 k = nn.Parameter(...) 因为渗透率必须是正数。如果直接优化 k k k,优化器可能把 k k k 推向负值,导致PDE残差的物理意义消失。用 k = e θ k = e^{\theta} k=eθ 参数化后,无论 θ \theta θ 取什么值, k k k 永远是正的。这在工程上是一个简单但极其重要的细节------很多PINN反问题的失败不是因为算法不对,而是因为参数化方式不对。

1.3 训练循环的改动

正问题的训练循环只用PDE残差。逆问题需要增加数据项

python 复制代码
def train_inverse(model, x_col, x_obs, h_obs, n_epochs=20000, lr=1e-3):
    """
    x_col: 域内配点
    x_obs, h_obs: 观测点和观测值 (稀疏、可能带噪声)
    """
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    losses = {'pde': [], 'data': [], 'k': []}

    for epoch in range(n_epochs):
        optimizer.zero_grad()

        # 1. PDE残差损失
        f = compute_pde_residual_inverse(model, x_col)
        loss_pde = torch.mean(f ** 2)

        # 2. 数据拟合损失 (新增项)
        h_pred = model(x_obs)
        loss_data = torch.mean((h_pred - h_obs) ** 2)

        # 3. 复合损失
        # 为什么数据项权重取10?
        # 观测数据通常比PDE配点少得多,如果不加权,数据项会被PDE残差淹没
        loss = loss_pde + 10.0 * loss_data

        loss.backward()
        optimizer.step()

        losses['pde'].append(loss_pde.item())
        losses['data'].append(loss_data.item())
        losses['k'].append(model.k.item())

        if epoch % 2000 == 0:
            print(f"Epoch {epoch:5d} | PDE: {loss_pde.item():.2e} | "
                  f"Data: {loss_data.item():.2e} | k: {model.k.item():.4f}")

    return losses

二、数据项的引入:观测点的loss怎么写、权重怎么设

2.1 数据项的位置决定了它"管什么"

数据项 L d a t a \mathcal{L}_{data} Ldata 是一个约束------它告诉网络"在这些点上,你的输出必须和观测值一致"。但它和PDE残差的约束角色不同:

损失项 约束对象 约束范围 对反演的影响
PDE残差 方程结构 全域配点 保证解满足物理规律
数据项 观测值 稀疏观测点 锚定解的具体形状,反推系数
边界条件 边界值 边界 保证解的物理合理性

逆问题的本质矛盾是:PDE残差和未知系数之间存在"共谋关系" 。如果只优化PDE残差,网络可以找到一组 ( h , k ) (h, k) (h,k) 使得残差为零,但这组 k k k 可能是完全错误的。数据项的作用就是打破这种共谋。

2.2 数据项权重的设定

数据项权重的设定是逆问题中最棘手的问题之一。推荐一个自适应方案:

python 复制代码
class AdaptiveDataWeight:
    """自适应数据项权重
    核心思想: 让数据项和PDE残差的梯度范数保持均衡
    """
    def __init__(self, model, alpha=0.1):
        self.model = model
        self.alpha = alpha  # 权重更新步长
        self.lambda_data = 10.0  # 初始值

    def update(self, loss_pde, loss_data, optimizer):
        """根据梯度范数比值调整权重"""
        # 计算PDE损失对网络参数的梯度范数
        grad_pde = torch.autograd.grad(
            loss_pde, self.model.parameters(),
            retain_graph=True, allow_unused=True
        )
        gn_pde = sum(g.norm()**2 for g in grad_pde if g is not None) ** 0.5

        # 计算数据损失对网络参数的梯度范数
        grad_data = torch.autograd.grad(
            loss_data, self.model.parameters(),
            retain_graph=True, allow_unused=True
        )
        gn_data = sum(g.norm()**2 for g in grad_data if g is not None) ** 0.5

        # 让数据项的梯度范数逼近PDE项的梯度范数
        ratio = (gn_data / (gn_pde + 1e-8)).detach()
        self.lambda_data = self.lambda_data * (1 + self.alpha * (ratio - 1))
        self.lambda_data = max(self.lambda_data, 0.1)  # 下限保护

        return self.lambda_data

为什么用梯度范数而非损失值来调权重? 因为损失值的大小不能反映该项对参数更新的实际贡献。一个损失值很小但梯度很大的项,可能在优化中占主导地位。梯度范数才真正决定了优化器会把参数往哪个方向推。

2.3 数据自洽性:一个被忽视的工程前提

逆问题的输入是观测数据 h o b s h_{obs} hobs。如果 h o b s h_{obs} hobs 是由一个和真实 k ( x ) k(x) k(x) 无关的解析函数 生成的,那么 PINN 会发现:不存在任何 k ( x ) k(x) k(x) 能同时满足 PDE 和观测数据。此时 PINN 只能找"最小二乘意义下的折衷解"------通常是一条常数线。

正确的做法 :先给定 k true ( x ) k_\text{true}(x) ktrue(x),用有限差分(或有限元)求解正问题,得到 h true ( x ) h_\text{true}(x) htrue(x),再从 h true h_\text{true} htrue 采样观测点。

python 复制代码
def solve_forward_fd(k_func, f_value, x_grid, h_left=1.0, h_right=0.0):
    """
    用二阶有限差分解正问题 -d/dx(k dh/dx) = f
    """
    n = len(x_grid)
    dx = x_grid[1] - x_grid[0]

    # 计算单元中心处的k值
    k_half = k_func(0.5 * (x_grid[:-1] + x_grid[1:]))

    # 组装三对角系统 A h = b
    A = np.zeros((n, n))
    b = np.full(n, f_value * dx * dx)

    for i in range(1, n - 1):
        kL = k_half[i - 1]
        kR = k_half[i]
        A[i, i - 1] = -kL
        A[i, i]     =  kL + kR
        A[i, i + 1] = -kR

    # Dirichlet边界条件
    A[0, 0]   = 1.0;  b[0]   = h_left
    A[-1, -1] = 1.0;  b[-1]  = h_right

    return np.linalg.solve(A, b)

用 401 个点的细网格求解后,再从 h true h_\text{true} htrue 中插值采样观测点:

python 复制代码
x_fine = np.linspace(0.0, 1.0, 401)
k_fine = reference_k(x_fine)
h_fine = solve_forward_fd(reference_k, cfg.source, x_fine)

# 从FD解中采样观测点
x_obs = np.linspace(0.05, 0.95, cfg.n_obs)
h_obs = np.interp(x_obs, x_fine, h_fine)
h_obs += cfg.obs_noise * rng.standard_normal(cfg.n_obs)

2.4 尺度锚定:为什么 k(x) 会整体偏低 30%

即便数据自洽,反演仍可能系统性低估 k ( x ) k(x) k(x)。原因是 Darcy 方程只约束 k k k 和 h ′ ′ h'' h′′ 的乘积:

k ( x ) ⋅ h ′ ′ ( x ) ≈ − f ( x ) k(x) \cdot h''(x) \approx -f(x) k(x)⋅h′′(x)≈−f(x)

如果 PINN 把 k k k 整体缩小 30%,同时把 h h h 的曲率放大 30%,PDE 残差几乎不变,数据拟合也不会变差。更糟的是,Dirichlet 边界条件 h ( 0 ) = 1 , h ( 1 ) = 0 h(0)=1, h(1)=0 h(0)=1,h(1)=0 只约束了 h h h 的值,不约束 h h h 的梯度 ,所以 flux − k h ′ -k h' −kh′ 的量级是自由的。

这在反问题理论里叫做零空间(null space) :存在一族 ( k , h ) (k, h) (k,h) 组合满足所有约束,但 k k k 的绝对量级不同。

解法:加 flux 观测。 flux q ( x ) = − k ( x ) h ′ ( x ) q(x) = -k(x) h'(x) q(x)=−k(x)h′(x) 直接耦合了 k k k 和 h ′ h' h′,锚定它的量级就锚定了 k k k。

python 复制代码
# 从FD解中计算flux并采样
h_prime_fine = np.gradient(h_fine, x_fine)
q_fine = -k_fine * h_prime_fine

x_flux = np.linspace(0.1, 0.9, cfg.n_flux)
q_obs = np.interp(x_flux, x_fine, q_fine)
q_obs += cfg.obs_noise * rng.standard_normal(cfg.n_flux)

对应的损失项:

L f l u x = 1 N q ∑ i = 1 N q ∣ − k ( x i ) ∂ h ∂ x ∣ x i − q o b s , i ∣ 2 \mathcal{L}{flux} = \frac{1}{N_q}\sum{i=1}^{N_q} \left| -k(x_i) \frac{\partial h}{\partial x}\bigg|{x_i} - q{obs,i} \right|^2 Lflux=Nq1i=1∑Nq −k(xi)∂x∂h xi−qobs,i 2

python 复制代码
# flux loss: q_pred = -k * dh/dx
x_flux_req = x_flux.clone().requires_grad_(True)
h_flux = model.forward_h(x_flux_req)
k_flux = model.forward_k(x_flux_req)
h_x_flux = torch.autograd.grad(
    h_flux, x_flux_req,
    torch.ones_like(h_flux), create_graph=True
)[0]
q_pred = -k_flux * h_x_flux
loss_flux = torch.mean((q_pred - q_obs) ** 2)

只需要 3 个 flux 观测点 ,就能把 k k k 的尺度从"自由"变成"锚定"。

观测类型 数量 约束对象 对反演的作用
h h h 观测 30 水头值 约束 k k k 的低频形状
q q q 观测 3 − k ⋅ d h / d x -k \cdot dh/dx −k⋅dh/dx 锚定 k k k 的绝对尺度

三、空间变化参数场的处理:用子网络表示 k ( x ) k(x) k(x)

3.1 全局常数 vs 空间变化场

如果未知渗透率是一个全局常数,问题到此就结束了。但现实中的非均质介质渗透率是空间变化的------比如不同岩层的渗透率差异可能达到几个数量级。

这时,k = nn.Parameter 就不够了。你需要把 k ( x ) k(x) k(x) 也用一个子网络表示:

python 复制代码
class SpatiallyVaryingDarcyPINN(nn.Module):
    """一维Darcy流逆问题: 渗透率k(x)是空间变化的
    方程: -d/dx(k(x) * dh/dx) = f(x)
    未知: k(x) 是一个场
    """
    def __init__(self, layers_main=[1, 64, 64, 64, 1],
                 layers_k=[1, 32, 32, 32, 1]):
        super().__init__()
        # 主网络: 拟合水头h(x)
        self.net_h = DarcyPINN(layers_main)
        # 参数网络: 拟合渗透率场k(x)
        # 注意: 参数网络通常比主网络小
        # 因为k(x)通常比h(x)更光滑
        self.net_k = nn.Sequential(
            nn.Linear(1, 32),
            nn.Tanh(),
            nn.Linear(32, 32),
            nn.Tanh(),
            nn.Linear(32, 32),
            nn.Tanh(),
            nn.Linear(32, 1),
        )

    def forward_h(self, x):
        return self.net_h(x)

    def forward_k(self, x):
        """参数网络的输出通过softplus保证正值"""
        # 为什么用softplus而不是exp?
        # exp在输入较大时容易溢出
        # softplus(x) = log(1+exp(x)) 平滑且数值稳定
        raw = self.net_k(x)
        return torch.nn.functional.softplus(raw) + 0.1  # 保证k>0.1

3.2 计算图拼接:梯度如何从PDE残差反传到参数网络

这是逆问题与正问题最本质的差异。在正问题中, k k k 是常数,不参与梯度计算。在逆问题中, k ( x ) k(x) k(x) 来自参数网络,PDE残差的梯度需要通过 k k k 反传到参数网络的权重。

python 复制代码
def compute_pde_residual_spatial(model, x):
    """
    空间变化k(x)的PDE残差
    关键: k(x)需要通过自动微分计算dk/dx
    """
    x.requires_grad_(True)

    # 水头
    h = model.forward_h(x)
    # 渗透率场 (注意: 也需要对x求导!)
    k = model.forward_k(x)

    # 一阶导
    h_x = torch.autograd.grad(h, x, torch.ones_like(h), create_graph=True)[0]
    k_x = torch.autograd.grad(k, x, torch.ones_like(k), create_graph=True)[0]

    # k * dh/dx
    k_h_x = k * h_x
    # d/dx(k * dh/dx) = dk/dx * dh/dx + k * d²h/dx²
    k_h_xx = torch.autograd.grad(k_h_x, x, torch.ones_like(k_h_x), create_graph=True)[0]

    source = torch.ones_like(x) * 1.0
    f = -k_h_xx - source
    return f

这里有一个关键的计算图问题k_x = torch.autograd.grad(k, x, ...) 这一步是正问题中不存在 的。在正问题中 k k k 是常数, d k / d x = 0 dk/dx = 0 dk/dx=0,二阶导退化为 k ⋅ d 2 h / d x 2 k \cdot d^2h/dx^2 k⋅d2h/dx2。在逆问题中, k ( x ) k(x) k(x) 是网络输出, d k / d x dk/dx dk/dx 不为零,PDE残差中多了一项 d k / d x ⋅ d h / d x dk/dx \cdot dh/dx dk/dx⋅dh/dx。

这条梯度路径的稳定性是独立于正问题的新问题。具体来说:

  • 参数网络 k ( x ) k(x) k(x) 的梯度会通过 d k / d x dk/dx dk/dx 影响PDE残差
  • 如果 k ( x ) k(x) k(x) 在训练初期输出剧烈波动, d k / d x dk/dx dk/dx 会非常大,导致PDE残差爆炸
  • 反过来,PDE残差的梯度又会通过 k ( x ) k(x) k(x) 反传到参数网络,可能让 k ( x ) k(x) k(x) 更加不稳定

3.3 训练不稳定的工程对策

python 复制代码
def train_spatial_inverse(model, x_col, x_obs, h_obs, n_epochs=30000):
    """空间变化k(x)的逆问题训练"""
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
    scheduler = torch.optim.lr_scheduler.StepLR(
        optimizer, step_size=10000, gamma=0.5
    )

    for epoch in range(n_epochs):
        optimizer.zero_grad()

        # PDE残差 (包含dk/dx项)
        f = compute_pde_residual_spatial(model, x_col)
        loss_pde = torch.mean(f ** 2)

        # 数据损失
        h_pred = model.forward_h(x_obs)
        loss_data = torch.mean((h_pred - h_obs) ** 2)

        # 正则化: 惩罚k(x)的剧烈波动
        # 为什么需要这个?因为dk/dx过大是训练不稳定的根源
        k_col = model.forward_k(x_col)
        k_x = torch.autograd.grad(
            k_col, x_col, torch.ones_like(k_col),
            create_graph=True
        )[0]
        loss_reg = torch.mean(k_x ** 2) * 1e-6  # 权重极小

        loss = loss_pde + 10.0 * loss_data + loss_reg

        loss.backward()
        # 梯度裁剪: 防止dk/dx的梯度爆炸
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        scheduler.step()

        if epoch % 2000 == 0:
            with torch.no_grad():
                k_pred = model.forward_k(x_col)
                print(f"Epoch {epoch:5d} | PDE: {loss_pde.item():.2e} | "
                      f"Data: {loss_data.item():.2e} | "
                      f"k range: [{k_pred.min().item():.3f}, {k_pred.max().item():.3f}]")

四、一维稳态Darcy流的完整代码:正问题到逆问题逐行改造

4.1 问题设定

我们考虑一维稳态Darcy流:

− d d x ( k ( x ) d h d x ) = f , x ∈ 0 , 1 -\frac{d}{dx}\left(k(x) \frac{dh}{dx}\right) = f, \quad x \in 0, 1 −dxd(k(x)dxdh)=f,x∈0,1

边界条件: h ( 0 ) = 1 h(0) = 1 h(0)=1, h ( 1 ) = 0 h(1) = 0 h(1)=0。

正问题 : k ( x ) = 1 + 0.5 sin ⁡ ( 2 π x ) k(x) = 1 + 0.5\sin(2\pi x) k(x)=1+0.5sin(2πx),用有限差分求解 h ( x ) h(x) h(x)。

逆问题 :只知道 h ( x ) h(x) h(x) 在 30 个点上的观测值(带 0.5% 噪声)和 3 个 flux 观测值,反推 k ( x ) k(x) k(x)。

参考解生成 :用 401 点有限差分网格求解正问题,得到 h true h_\text{true} htrue,再从它采样观测点和 flux 观测。

4.2 完整逆问题代码

python 复制代码
# ============ 1. 配置 ============
class Config:
    source = 1.0
    n_hidden_h = 64
    n_hidden_k = 64
    n_layers_h = 4
    n_layers_k = 3

    n_col = 200
    n_obs = 30              # 水头观测点数
    n_flux = 3              # flux 观测点数
    obs_noise = 0.005

    epochs = 20000
    lr = 1e-3
    lambda_data = 100.0     # 数据项权重
    lambda_flux = 50.0      # flux 项权重
    lambda_reg = 1e-6       # 正则项权重(要足够小)

    k_min = 0.3
    k_max = 1.8
    seed = 42


# ============ 2. 数据生成(含有限差分正解) ============
def generate_data(cfg):
    rng = np.random.default_rng(cfg.seed)

    # (a) 有限差分求解正问题
    x_fine = np.linspace(0.0, 1.0, 401)
    k_fine = reference_k(x_fine)
    h_fine = solve_forward_fd(reference_k, cfg.source, x_fine)

    # (b) 从正解中采样水头观测
    x_obs = np.linspace(0.05, 0.95, cfg.n_obs)
    h_obs = np.interp(x_obs, x_fine, h_fine)
    h_obs += cfg.obs_noise * rng.standard_normal(cfg.n_obs)

    # (c) 从正解中采样flux观测
    h_prime_fine = np.gradient(h_fine, x_fine)
    q_fine = -k_fine * h_prime_fine
    x_flux = np.linspace(0.1, 0.9, cfg.n_flux)
    q_obs = np.interp(x_flux, x_fine, q_fine)
    q_obs += cfg.obs_noise * rng.standard_normal(cfg.n_flux)

    # (d) 配点与测试网格
    x_col = np.linspace(0.0, 1.0, cfg.n_col)
    x_test = np.linspace(0.0, 1.0, 400)

    to_tensor = lambda a: torch.tensor(a, dtype=torch.float32).unsqueeze(1)

    return {
        'x_col':  to_tensor(x_col),
        'x_obs':  to_tensor(x_obs),
        'h_obs':  to_tensor(h_obs),
        'x_flux': to_tensor(x_flux),
        'q_obs':  to_tensor(q_obs),
        'x_test': to_tensor(x_test),
        'x_fine': x_fine,
        'h_fine': h_fine,
        'k_fine': k_fine,
    }


# ============ 3. 训练循环(含flux损失) ============
def train(model, data, cfg, device='cpu'):
    model.to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=cfg.lr)
    scheduler = torch.optim.lr_scheduler.StepLR(
        optimizer, step_size=max(1, cfg.epochs // 3), gamma=0.5
    )

    # 注意: history必须包含'flux'键
    history = {
        'pde': [], 'data': [], 'reg': [], 'flux': [], 'total': [],
        'k_err': [], 'k_err_epoch': [],
    }

    x_col = data['x_col'].to(device)
    x_obs = data['x_obs'].to(device)
    h_obs = data['h_obs'].to(device)
    x_flux = data['x_flux'].to(device)
    q_obs = data['q_obs'].to(device)

    for epoch in range(cfg.epochs):
        model.train()
        optimizer.zero_grad()

        # (a) PDE残差损失
        f = pde_residual(model, x_col, cfg.source)
        loss_pde = torch.mean(f ** 2)

        # (b) 水头数据损失
        h_pred = model.forward_h(x_obs)
        loss_data = torch.mean((h_pred - h_obs) ** 2)

        # (c) flux损失: q_pred = -k * dh/dx
        x_flux_req = x_flux.clone().requires_grad_(True)
        h_flux = model.forward_h(x_flux_req)
        k_flux = model.forward_k(x_flux_req)
        h_x_flux = torch.autograd.grad(
            h_flux, x_flux_req,
            torch.ones_like(h_flux), create_graph=True
        )[0]
        q_pred = -k_flux * h_x_flux
        loss_flux = torch.mean((q_pred - q_obs) ** 2)

        # (d) 正则化: 惩罚dk/dx的剧烈波动
        x_reg = x_col.clone().requires_grad_(True)
        k_reg = model.forward_k(x_reg)
        k_x = torch.autograd.grad(
            k_reg, x_reg, torch.ones_like(k_reg), create_graph=True
        )[0]
        loss_reg = torch.mean(k_x ** 2)

        # (e) 加权总损失
        loss = (loss_pde
                + cfg.lambda_data * loss_data
                + cfg.lambda_flux * loss_flux
                + cfg.lambda_reg * loss_reg)

        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        scheduler.step()

        history['pde'].append(loss_pde.item())
        history['data'].append(loss_data.item())
        history['flux'].append(loss_flux.item())
        history['reg'].append(loss_reg.item())
        history['total'].append(loss.item())

        if epoch % max(1, cfg.epochs // 10) == 0:
            print(f"  [{epoch:5d}/{cfg.epochs}] "
                  f"pde={loss_pde.item():.3e}  "
                  f"data={loss_data.item():.3e}  "
                  f"flux={loss_flux.item():.3e}  "
                  f"reg={loss_reg.item():.3e}")

    return history

训练损失曲线。蓝色为 PDE 残差,红色为数据拟合,绿色为正则项,黑色为总损失。

4.3 正问题版本到逆问题版本的改动清单

改动位置 正问题代码 逆问题代码 为什么改
参数定义 k = 1.0 (常数) self.log_k = nn.Parameter(...) 让 k k k 参与梯度优化
参数网络 新增 self.net_k 表示空间变化的 k ( x ) k(x) k(x)
PDE残差 只有 d 2 h / d x 2 d^2h/dx^2 d2h/dx2 增加 d k / d x ⋅ d h / d x dk/dx \cdot dh/dx dk/dx⋅dh/dx 项 k k k 不再是常数
数据损失 新增 loss_data 用观测数据锚定反演
Flux损失 新增 loss_flux 锚定 k k k 的绝对尺度
正则化 新增 loss_reg(权重极小) 抑制 k ( x ) k(x) k(x) 的剧烈波动
数据自洽 不涉及 用有限差分生成 h true h_\text{true} htrue 保证反演问题有解
梯度裁剪 通常不需要 clip_grad_norm_(max_norm=1.0) 防止 d k / d x dk/dx dk/dx 梯度爆炸

五、调试记录:从37%到12%的四轮迭代

这一节记录从"第一次跑不通"到"反演精度达标"的完整过程。四轮迭代的对比数据如下:

轮次 关键改动 k k k 相对误差 典型现象
第 1 轮 初始版本(解析 h h h + 强正则) 37.3% k ( x ) k(x) k(x) 被压成一条常数线
第 2 轮 调参: λ r e g \lambda_{reg} λreg 降低、观测点从10增到30 39.9% 形状错乱,调参反而变差
第 3 轮 用有限差分生成自洽数据 34.5% 形状对了,但整体偏低 30%
第 4 轮 加 3 个 flux 观测锚定尺度 12.1% 形状和量级都对了

5.1 第 1 轮:k 被压成常数线

初始版本用解析函数构造 h true h_\text{true} htrue:

python 复制代码
def reference_h(x):
    return 1.0 - x + 0.15 * np.sin(np.pi * x)

它和 k true ( x ) = 1 + 0.5 sin ⁡ ( 2 π x ) k_\text{true}(x) = 1 + 0.5\sin(2\pi x) ktrue(x)=1+0.5sin(2πx) 没有物理关系 。PINN 发现不存在任何 k ( x ) k(x) k(x) 能同时满足 PDE 和观测数据,最后只能收敛到一个常数 k k k 使残差最小。

症状 : k ( x ) k(x) k(x) 图像是一条近乎水平的线,相对误差 37%。

5.2 第 2 轮:调参无效

盲目地调 λ r e g \lambda_{reg} λreg、增加观测点,结果 k k k 误差反而升到 39.9%。

结论 :当数据本身不自洽时,调参没有任何意义。问题不在超参,在数据。

5.3 第 3 轮:用有限差分生成自洽数据

用 401 点的二阶有限差分求解正问题:

python 复制代码
h_fine = solve_forward_fd(reference_k, cfg.source, x_fine)

再从 h fine h_\text{fine} hfine 中插值采样观测点。修复后 k k k 的形状正确了------峰值在 x ≈ 0.25 x \approx 0.25 x≈0.25、谷值在 x ≈ 0.75 x \approx 0.75 x≈0.75,相位完全匹配

但出现了新问题 : k ( x ) k(x) k(x) 整体偏低约 30%。峰值只有 1.1(真实 1.5),谷值只有 0.4(真实 0.5)。

5.4 第 4 轮:加 flux 观测锚定尺度

k k k 整体偏低的根源是尺度不确定性 。Dirichlet 边界条件只约束 h h h 的值,不约束 h ′ h' h′,所以 − k h ′ -k h' −kh′ 的量级是自由的。

加 3 个 flux 观测 q = − k ⋅ d h / d x q = -k \cdot dh/dx q=−k⋅dh/dx,直接锚定 k k k 的绝对量级。

修复后结果

指标 修复前 修复后
h h h 相对误差 0.22% 0.25%
k k k 相对误差 34.5% 12.1%
k k k 峰值 1.1(偏低) 接近 1.5
k k k 谷值 0.4(偏低) 接近 0.5

左图为水头 h ( x ) h(x) h(x) 预测值与观测点对比;右图为渗透率 k ( x ) k(x) k(x) 反演结果与真实值对比,相对误差 12.1%。


5.5 关于 L r e g L_{reg} Lreg 上升的解释

训练日志里 reg 从 2.5 × 10 − 3 2.5\times 10^{-3} 2.5×10−3 升到 3.0 3.0 3.0,看起来异常,实际上正常:

  • L r e g = mean ( ( d k / d x ) 2 ) L_{reg} = \text{mean}((dk/dx)^2) Lreg=mean((dk/dx)2), k k k 在 0.3 , 1.8 0.3, 1.8 0.3,1.8 区间变化, d k / d x dk/dx dk/dx 量级约 1--3
  • 训练后期 k k k 的形状变丰富后, d k / d x dk/dx dk/dx 自然增大
  • 但因 λ r e g = 10 − 6 \lambda_{reg} = 10^{-6} λreg=10−6,它对总损失的实际贡献只有 3 × 10 − 6 3 \times 10^{-6} 3×10−6,几乎可以忽略
  • 这说明正则项已经弱到不再干扰 k k k 的形状,是好事,不需要调

六、反演精度的评估:相对误差 vs 训练epoch

反演精度的评估需要区分两个阶段:

6.1 三个阶段的收敛特征

阶段 Epoch范围 特征 反演精度
预热期 0-5000 主网络还在拟合h(x)的大致形状,k(x)基本不变 相对误差 > 50%
耦合期 5000-15000 h(x)和k(x)开始相互影响,反演结果波动 相对误差 10%-50%
收敛期 15000-30000 h(x)和k(x)趋于稳定,反演误差缓慢下降 相对误差 < 15%
python 复制代码
def evaluate_inversion(model, x_test, k_true_func):
    """评估反演精度"""
    with torch.no_grad():
        k_pred = model.forward_k(x_test).numpy().flatten()
    k_true = k_true_func(x_test.numpy().flatten())

    # 相对L2误差
    rel_error = np.linalg.norm(k_pred - k_true) / np.linalg.norm(k_true)

    # 逐点误差
    point_error = np.abs(k_pred - k_true) / np.abs(k_true)

    return {
        'rel_l2': rel_error,
        'max_point_error': point_error.max(),
        'mean_point_error': point_error.mean(),
        'k_pred': k_pred,
        'k_true': k_true,
    }

6.2 反演误差收敛曲线

python 复制代码
def plot_convergence(history, outdir):
    """绘制反演误差收敛曲线"""
    epochs = np.array(history['k_err_epoch'])
    k_err = np.array(history['k_err']) * 100.0

    fig, ax = plt.subplots(figsize=(10, 5))
    ax.semilogy(epochs, k_err, 'b-', lw=2,
                label='Relative error of k(x)')
    ax.axhline(y=5, color='r', linestyle='--', lw=1.5,
               label='5% threshold')
    ax.set_xlabel('Epoch')
    ax.set_ylabel('Relative error (%)')
    ax.set_title('Inversion Error Convergence', fontsize=13, pad=10)
    ax.legend()
    ax.grid(True, alpha=0.3)

    path = os.path.join(outdir, 'inversion_convergence.png')
    plt.savefig(path, bbox_inches='tight')
    plt.close()
    print(f'  saved: {path}')

反演k(x) 的相对误差随训练 epoch 变化。预热期误差高且波动,耦合期快速下降,收敛期趋于平稳。

注意一个重要的工程事实 :如果你在训练过程中观察 model.k.item() 并绘制曲线,你会发现反演值在耦合期(epoch 5000-15000)会有明显的波动。这不是bug------这是h(x)和k(x)在相互调整。工程上建议在耦合期之后才开始记录反演结果。

七、与MCMC结合的思路:PINN做前向代理,MCMC做后验采样

PINN的反演结果是点估计------它给出一个最可能的k值,但不告诉你这个值的不确定性有多大。如果观测数据很少或者噪声很大,点估计的置信度就很低。

MCMC解决的就是这个问题:它给出参数的后验分布,而不只是一个点。

7.1 PINN+MCMC的两阶段框架

#mermaid-svg-FbDYPdTycjyR0Rfh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FbDYPdTycjyR0Rfh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FbDYPdTycjyR0Rfh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FbDYPdTycjyR0Rfh .error-icon{fill:#552222;}#mermaid-svg-FbDYPdTycjyR0Rfh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FbDYPdTycjyR0Rfh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FbDYPdTycjyR0Rfh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FbDYPdTycjyR0Rfh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FbDYPdTycjyR0Rfh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FbDYPdTycjyR0Rfh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FbDYPdTycjyR0Rfh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FbDYPdTycjyR0Rfh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FbDYPdTycjyR0Rfh .marker.cross{stroke:#333333;}#mermaid-svg-FbDYPdTycjyR0Rfh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FbDYPdTycjyR0Rfh p{margin:0;}#mermaid-svg-FbDYPdTycjyR0Rfh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FbDYPdTycjyR0Rfh .cluster-label text{fill:#333;}#mermaid-svg-FbDYPdTycjyR0Rfh .cluster-label span{color:#333;}#mermaid-svg-FbDYPdTycjyR0Rfh .cluster-label span p{background-color:transparent;}#mermaid-svg-FbDYPdTycjyR0Rfh .label text,#mermaid-svg-FbDYPdTycjyR0Rfh span{fill:#333;color:#333;}#mermaid-svg-FbDYPdTycjyR0Rfh .node rect,#mermaid-svg-FbDYPdTycjyR0Rfh .node circle,#mermaid-svg-FbDYPdTycjyR0Rfh .node ellipse,#mermaid-svg-FbDYPdTycjyR0Rfh .node polygon,#mermaid-svg-FbDYPdTycjyR0Rfh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FbDYPdTycjyR0Rfh .rough-node .label text,#mermaid-svg-FbDYPdTycjyR0Rfh .node .label text,#mermaid-svg-FbDYPdTycjyR0Rfh .image-shape .label,#mermaid-svg-FbDYPdTycjyR0Rfh .icon-shape .label{text-anchor:middle;}#mermaid-svg-FbDYPdTycjyR0Rfh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FbDYPdTycjyR0Rfh .rough-node .label,#mermaid-svg-FbDYPdTycjyR0Rfh .node .label,#mermaid-svg-FbDYPdTycjyR0Rfh .image-shape .label,#mermaid-svg-FbDYPdTycjyR0Rfh .icon-shape .label{text-align:center;}#mermaid-svg-FbDYPdTycjyR0Rfh .node.clickable{cursor:pointer;}#mermaid-svg-FbDYPdTycjyR0Rfh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FbDYPdTycjyR0Rfh .arrowheadPath{fill:#333333;}#mermaid-svg-FbDYPdTycjyR0Rfh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FbDYPdTycjyR0Rfh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FbDYPdTycjyR0Rfh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FbDYPdTycjyR0Rfh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FbDYPdTycjyR0Rfh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FbDYPdTycjyR0Rfh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FbDYPdTycjyR0Rfh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FbDYPdTycjyR0Rfh .cluster text{fill:#333;}#mermaid-svg-FbDYPdTycjyR0Rfh .cluster span{color:#333;}#mermaid-svg-FbDYPdTycjyR0Rfh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FbDYPdTycjyR0Rfh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FbDYPdTycjyR0Rfh rect.text{fill:none;stroke-width:0;}#mermaid-svg-FbDYPdTycjyR0Rfh .icon-shape,#mermaid-svg-FbDYPdTycjyR0Rfh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FbDYPdTycjyR0Rfh .icon-shape p,#mermaid-svg-FbDYPdTycjyR0Rfh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FbDYPdTycjyR0Rfh .icon-shape .label rect,#mermaid-svg-FbDYPdTycjyR0Rfh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FbDYPdTycjyR0Rfh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FbDYPdTycjyR0Rfh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FbDYPdTycjyR0Rfh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 观测数据
PINN训练
前向代理模型
MCMC采样
后验分布
参数置信区间

第一阶段 :用PINN做前向代理。PINN训练完成后,model.forward_k(x) 可以快速预测任意输入下的k(x),作为MCMC的"快速评估器"。

第二阶段:用MCMC做后验采样。MCMC在参数空间中采样,每次采样调用PINN代理快速评估似然函数。

python 复制代码
def mcmc_sampling(pinn_model, x_obs, h_obs, n_samples=5000, burn_in=1000):
    """
    简化的MCMC采样器
    在k(x)的参数空间中采样
    """
    # 提取PINN参数网络的权重作为MCMC的采样空间
    k_params = list(pinn_model.net_k.parameters())
    n_params = sum(p.numel() for p in k_params)

    # 初始参数
    current_params = torch.cat([p.flatten().detach() for p in k_params])

    samples = []
    accept_count = 0

    for i in range(n_samples + burn_in):
        # 提议新参数 (随机游走)
        proposal = current_params + 0.01 * torch.randn_like(current_params)

        # 用PINN代理快速评估
        # (实际实现需要把proposal写回网络)
        log_likelihood = compute_log_likelihood(pinn_model, proposal, x_obs, h_obs)
        log_prior = -0.5 * torch.sum(proposal ** 2) / 0.01  # 高斯先验

        # Metropolis-Hastings接受准则
        log_alpha = log_likelihood + log_prior
        if torch.log(torch.rand(1)) < log_alpha:
            current_params = proposal
            accept_count += 1

        if i >= burn_in:
            samples.append(current_params.clone())

    accept_rate = accept_count / (n_samples + burn_in)
    print(f"MCMC acceptance rate: {accept_rate:.3f}")

    return torch.stack(samples)

7.2 什么时候值得上MCMC

场景 是否需要MCMC 理由
观测数据充足、噪声小 不需要 PINN点估计已经足够准确
观测数据稀疏 需要 需要量化反演的不确定性
存在多解风险 需要 MCMC可以探索多个后验峰
需要参数置信区间 需要 工程决策需要不确定性量化
计算资源有限 谨慎 MCMC需要大量前向评估

一个实用的建议:先用PINN做反演得到点估计,如果点估计的精度已经满足需求(相对误差<15%),就不需要上MCMC。只有当点估计的置信度不够,或者需要明确的不确定性量化时,再引入MCMC。

八、工程落地的七个关键经验

经验 具体做法 原因
数据必须自洽 用有限差分/有限元先求解正问题,从正解中采样观测 解析构造的 h true h_\text{true} htrue 与 k true k_\text{true} ktrue 无物理关系,PINN 会收敛到平凡解
加 flux 观测锚定尺度 3 个 flux 观测点即可锚定 k k k 的绝对量级 Dirichlet 边界条件不约束 h ′ h' h′, k k k 的尺度是自由的
参数化保证物理约束 渗透率用 softplus 或 tanh ⁡ \tanh tanh 有界参数化 防止优化器产生非物理的负值
参数网络比主网络小 参数网络通常 2--3 层,主网络 4--5 层 k ( x ) k(x) k(x) 通常比 h ( x ) h(x) h(x) 更光滑,不需要太多容量
正则项权重必须小 λ r e g \lambda_{reg} λreg 取 10 − 5 10^{-5} 10−5 或 10 − 6 10^{-6} 10−6 正则项权重过大会把 k ( x ) k(x) k(x) 压向常数
梯度裁剪 clip_grad_norm_(max_norm=1.0) 防止 d k / d x dk/dx dk/dx 的梯度爆炸
分阶段观察反演结果 耦合期(epoch 5000--15000)之后才开始记录 预热期的反演值无意义

思考题:你的反演场景中,未知参数是全局常数还是空间变化的场?如果是空间变化的场,参数网络的结构和正则化强度你是如何选择的?欢迎在评论区分享你的经验。

参考资料

  1. Deguchi S, Asai M. Reliable and efficient inverse analysis using physics-informed neural networks with normalized distance functions and adaptive weight tuning. Machine Learning: Science and Technology, 2025, 6(4): 045055.
  2. GitHub - ShotaDeguchi/PINN_HardBC_DynNorm: https://github.com/ShotaDeguchi/PINN_HardBC_DynNorm
  3. Aziz M A, et al. Self-adaptive physics-informed neural network for forward and inverse problems in heterogeneous porous flow. arXiv:2512.14610, 2025.
  4. gws-PINNs与MCMC联合框架:攻克时空跳跃系数反演难题. CSDN, 2026-05-24.
  5. PINA Tutorial: Resolution of an inverse problem. GitHub - mathLab/PINA.
  6. MATLAB Deep Learning. Inverse Problems using Physics-Informed Neural Networks (PINNs). GitHub.
  7. 张升, 兰鹏, 苏晶晶, 熊海斌. 基于PINNs算法的地下水渗流模型求解及参数反演. 岩土工程学报, 2023, 45(2): 376-383.
  8. 一维Darcy流参数反演的PINN实现:从数据自洽到尺度锚定. CSDN, 2026-09-16.
相关推荐
joinwell521 小时前
没收到回复,再点一次会启动两个 AI 吗?
人工智能·后端
hzxxxz1 小时前
2000份投稿里的共性-工具建设可复用的4条原则
人工智能
Bolt1 小时前
Agent: 将 harness 工程升级到认知工程
人工智能·架构·agent
桃西西呀1 小时前
限速 30 和限速 80 只差一个数字,卷积网络怎么分得清?
人工智能·深度学习·llm
Behaviour1 小时前
Sam Altman 预热本周重磅产品发布,或为 GPT-6 Sol
人工智能·chatgpt·aigc·openai·vibecoding
宣宣猪的小花园.1 小时前
【机器学习】过拟合与泛化:模型为什么会“刷题很强、实战失灵”
人工智能·算法·机器学习
7177771 小时前
有没有国产 GitLab?Gitee 与极狐 GitLab 等主流替代方案对比
人工智能·gitee
幂律智能1 小时前
幂律副总裁李融受邀参加新加坡 TechLaw.Fest 圆桌讨论,分享中国法律科技创新实践
人工智能
Zentceh1 小时前
0.001Lux是什么概念
图像处理·人工智能·科技·计算机视觉·车载系统·无人机·智能硬件