到物理約束的工程實踐)
簡介該資源為模塊化物理信息神經(jīng)網(wǎng)絡PINN的輕量實現(xiàn)代碼包面向具備一定深度學習基礎、希望快速上手PINN的研究者或工程師。代碼將數(shù)據(jù)準備、網(wǎng)絡定義、物理損失構建與訓練測試環(huán)節(jié)拆分為獨立模塊便于根據(jù)具體偏微分方程問題進行替換與擴展。壓縮包共25個文件以Python源碼為主10個py同時包含已訓練權重2個pth、模型結構圖3個png、項目配置xml以及說明文檔md/txt總大小僅2.07MB整體結構緊湊清晰。目前已有387人學習下載。通過該資源讀者可獲得FNN、CNN、ResFNN、ResCNN等多種網(wǎng)絡實現(xiàn)以及普通約束與hard約束兩種訓練變體配合測試腳本和預訓練權重可直接復現(xiàn)訓練效果理解物理損失對預測精度和泛化能力的影響適合作為PINN入門學習和二次開發(fā)的基礎模板。1. 模塊化 PINN 拆的不只是代碼是物理約束的邊界當你想把一個剛調(diào)通的一維 Burgers 方程換成二維穩(wěn)態(tài)滲流問題時最常見的尷尬是網(wǎng)絡接口要改、殘差函數(shù)要改、邊界 mask 散落在訓練循環(huán)里甚至損失權重都被硬編碼在三層函數(shù)之外。物理信息神經(jīng)網(wǎng)絡PINN在數(shù)學上只有一個目標——讓 PDE 殘差、邊界條件和觀測數(shù)據(jù)同時逼近零但在工程上它必須同時管理至少四類計算節(jié)點。模塊化 PINN 要解決的就是這個耦合問題。筆者見過不少從單一腳本起步的項目最后都倒在了“改一個方程參數(shù)要動半個文件”的維護性上。這文章不會帶你復現(xiàn)某篇論文而是給一個能長期演進、可替換、可測試的 PINN 代碼骨架適合已經(jīng)跑過最小示例、現(xiàn)在想提升復用度的研究人員和仿真工程師。2. 從損失函數(shù)反推模塊邊界方程、約束與網(wǎng)絡基元先看 PINN 的典型損失函數(shù)L λ_r L_r λ_b L_b λ_0 L_0 λ_d L_dL_r是控制方程殘差在所有 collocation 點上的均方值L_b是邊界殘差L_0是初值或參考狀態(tài)殘差L_d是稀疏觀測數(shù)據(jù)殘差。模塊化做的第一件事就是讓每個L都具有獨立的計算單元并且這些單元對外暴露統(tǒng)一的調(diào)用接口。模型神經(jīng)網(wǎng)絡、方程殘差算子和采樣策略分開之后損失權重只在最后裝配時參與而不是散落各處。我一般會把模塊邊界定在“是否會隨物理問題變化”這條線上。網(wǎng)絡骨架通常不隨方程變化PDE 算子一定變化邊界條件的變化頻率極高損失權重幾乎每個實驗都要調(diào)。因此至少需要抽象出網(wǎng)絡、方程、約束、采樣器四類對象。2.1 把 PDE 算子變成有狀態(tài)的模塊如果把彈性力學和流體方程寫進同一個訓練循環(huán)維護成本會立刻失控。常見做法是定義方程基類讓殘差計算只依賴坐標和網(wǎng)絡輸出不感知訓練狀態(tài)。比如下面這個 Burgers 方程的模塊化表達import torch import torch.nn as nn class Equation(nn.Module): 所有 PDE 方程殘差模塊的基類 def residual(self, model, coords): raise NotImplementedError class BurgersEquation(Equation): Burgers 方程: u_t u*u_x - nu*u_xx 0 def __init__(self, nu0.01): super().__init__() self.register_buffer(nu, torch.tensor(nu, dtypetorch.float32)) def residual(self, model, coords): x coords[..., 0:1] t coords[..., 1:2] x.requires_grad_(True) t.requires_grad_(True) u model(torch.cat([x, t], dim1)) u_t torch.autograd.grad(u, t, grad_outputstorch.ones_like(u), create_graphTrue)[0] u_x torch.autograd.grad(u, x, grad_outputstorch.ones_like(u), create_graphTrue)[0] u_xx torch.autograd.grad(u_x, x, grad_outputstorch.ones_like(u_x), create_graphTrue)[0] return u_t u * u_x - self.nu * u_xx這段代碼有兩個容易被忽略的參數(shù)register_buffer把一個非可訓練的常數(shù)掛到模塊上它會隨.to(device)自動遷移create_graphTrue保證了二階導數(shù)的計算圖被保留否則梯度在第一次反向傳播后就會斷掉。方程模塊對外只暴露residual()方法訓練循環(huán)不需要知道內(nèi)部求了幾階導。換方程時繼承Equation重寫residual()即可網(wǎng)絡和訓練代碼完全不用改動。2.2 采樣器與邊界掩碼要單獨成一等公民邊界條件最常被寫壞常見的壞味道是在訓練函數(shù)里用一堆if判斷當前 batch 是左邊界、右邊界還是初始條件。更好的做法是用獨立采樣器把“取點”和“算損失”解耦。采樣器只返回坐標和邊值約束模塊負責把采樣結果與網(wǎng)絡預測值組合成損失項。模塊對外接口更換頻率典型實現(xiàn)網(wǎng)絡基元forward(coords) - u低MLP Fourier Feature方程模塊residual(model, coords) - r高Burgers、NS、滲流邊界采樣器sample_boundary(n) - (coords, values)高左、右、下邊界約束裝配器loss(r, bound, init) - dict每實驗必調(diào)加權組合這樣的結構讓單測也更清晰。對方程模塊可以用解析解直接做梯度檢查對采樣器可以單獨驗證邊界點是否落在幾何邊界上。模塊化到這一步PINN 的代碼就不再是一個訓練腳本而是一套可以像積木一樣替換的求解器組件。3. 模塊化 PINN 最小代碼從殘差回歸到損失裝配理論拆完了下一步給出一個可以直接跑的模塊化 PINN 最小實現(xiàn)。目標問題用帶解析解的一維熱方程這樣后面驗證會更容易。設計上區(qū)分三類組件Sampler負責在時空調(diào)度域產(chǎn)生內(nèi)點和邊界點HeatEquation負責算殘差PINNTrainer只負責把 loss 裝配起來并驅動優(yōu)化器。3.1 最小可替換的組件骨架下面的代碼不依賴任何隱藏技巧模塊邊界與第 2 章完全一致。訓練循環(huán)中不會出現(xiàn)任何與具體方程有關的數(shù)學公式這就是“模塊化”最直接的驗證。import torch import torch.nn as nn class MLP(nn.Module): def __init__(self, in_dim2, hidden64, layers4): super().__init__() seq [nn.Linear(in_dim, hidden), nn.Tanh()] for _ in range(layers): seq [nn.Linear(hidden, hidden), nn.Tanh()] seq [nn.Linear(hidden, 1)] self.net nn.Sequential(*seq) def forward(self, coords): return self.net(coords) class HeatEquation(Equation): def residual(self, model, coords): x coords[..., 0:1].requires_grad_(True) t coords[..., 1:2].requires_grad_(True) u model(torch.cat([x, t], dim1)) u_t torch.autograd.grad(u, t, grad_outputstorch.ones_like(u), create_graphTrue)[0] u_xx torch.autograd.grad( torch.autograd.grad(u, x, grad_outputstorch.ones_like(u), create_graphTrue)[0], x, grad_outputstorch.ones_like(x), create_graphTrue)[0] return u_t - u_xx class Sampler: def __init__(self, x_domain(0, 1), t_domain(0, 1)): self.x0, self.x1 x_domain self.t0, self.t1 t_domain def interior(self, n): x (self.x1 - self.x0) * torch.rand(n, 1) self.x0 t (self.t1 - self.t0) * torch.rand(n, 1) self.t0 return torch.cat([x, t], dim1) def boundary(self, n): x0 torch.zeros(n, 1); x1 torch.ones(n, 1) t (self.t1 - self.t0) * torch.rand(n, 1) self.t0 left torch.cat([x0, t], dim1) right torch.cat([x1, t], dim1) return torch.cat([left, right], dim0) def initial(self, n): x (self.x1 - self.x0) * torch.rand(n, 1) self.x0 t torch.zeros(n, 1) return torch.cat([x, t], dim1)參數(shù)上hidden64、layers4對一維熱方程已經(jīng)很寬裕。如果方程帶高頻振蕩或非線性陡梯度通常要把hidden提高到 128 并減少單層寬度如果解域是二維矩形in_dim2依然成立坐標拼接維度不變。3.2 將 loss 組合提升為顯式裝配訓練循環(huán)的核心是將不同部分采樣點上的誤差計算收斂到三者當中。下面這段代碼把r、邊界b、初值ic作為三個獨立變量并通過weights字典傳入class PINNTrainer: def __init__(self, model, eq, sampler, weights): self.model model self.eq eq self.sampler sampler self.weights weights def loss_on(self, coords, eq_outputNone, targetNone): if target is None: res self.eq.residual(self.model, coords) return torch.mean(res ** 2) pred self.model(coords) return torch.mean((pred - target) ** 2) def step(self): coords_r self.sampler.interior(1024) coords_b self.sampler.boundary(128) coords_i self.sampler.initial(256) u_i torch.sin(np.pi * coords_i[:, 0:1]) loss_r self.loss_on(coords_r) loss_b self.loss_on(coords_b, targettorch.zeros_like( self.model(coords_b))) loss_i self.loss_on(coords_i, targetu_i) total (self.weights[r] * loss_r self.weights[b] * loss_b self.weights[ic] * loss_i) return total, {r: loss_r.item(), b: loss_b.item(), ic: loss_i.item()}這里每個loss_on都通過target參數(shù)區(qū)分是殘差約束還是數(shù)值約束。initial采樣器返回的coords_i中t0部分是固定的網(wǎng)絡預測值應當接近sin(pi * x)。這種寫法把損失計算變成了聲明式裝配要關閉某個約束時只需要把對應weights置零而不用注釋掉整段代碼。損失分量采樣點數(shù)量權重初始值權重調(diào)整經(jīng)驗r方程殘差10241.0收斂慢時提到 10 或 50b邊界條件128 點 × 2 邊界10.0小模量方程提高到 100ic初值條件25610.0時間外推時提高避免漂移權重初始值的含義是“優(yōu)先滿足邊界還是優(yōu)先滿足方程”。如果邊界和初值直接決定了解的唯一性應該先用大權重把它壓住方程殘差在訓練后期需要更長時間來收斂。矩陣上這份表格可以作為第一次實驗的起點但要記得在 2000 步后根據(jù) loss 分量變化再做二次調(diào)整。4. 訓練參數(shù)、收斂判斷與常見坑從學習率到損失權重模塊化代碼帶來的額外收益是你可以單獨觀察每一項損失分量的行為而不是干看一個大 scalar。接下來講實戰(zhàn)中三個最常見的問題方程殘差不降、邊界條件震蕩、以及自動微分帶來的顯存爆炸。4.1 殘差不降時先檢查平凡解陷阱最典型的失敗是網(wǎng)絡輸出全部為零時方程殘差為零、邊界損失為零、初值損失也接近零——模型直接輸出了一個零函數(shù)這在無源熱傳導問題中恰好是合法解。避免平凡解的常用辦法是給初值或邊界加一個偏置擾動的權重或者把網(wǎng)絡輸出的均值錨定在某個非零值上。下面的訓練循環(huán)里加入了一個簡單診斷def train_with_monitor(model, trainer, optimizer, steps5000): for step in range(steps): total, components trainer.step() optimizer.zero_grad() total.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 10.0) optimizer.step() if step % 500 0: with torch.no_grad(): pred_sample model(trainer.sampler.initial(100)) sample_std pred_sample.std().item() print(step, components, std, sample_std)clip_grad_norm_的10.0是一個相對保險的下界。PINN 的梯度里?;烊攵A導貢獻當std降到 1e-4 以下時基本可以斷定模型陷入了零解。這時可以暫時把weights[r]調(diào)低、先只讓初值邊界條件擬合出光滑形狀再逐步放開殘差權重。4.2 學習率調(diào)度與損失權重的相互影響PINN 的 loss landscape 通常不像分類網(wǎng)絡那么平滑Adam 的前 1000 步表現(xiàn)還可以后期容易在殘差與邊界條件之間拉鋸。常見處理是先用 1e-3 學習率跑 2000 步再用余弦退火把學習率衰減到 1e-5。但殘差項對學習率的敏感度遠高于邊界項因為二階導放大了高頻誤差。階段學習率損失權重策略0 - 2000 步1e-3邊界權重 10殘差 12000 - 5000 步1e-3 → 1e-4逐步把殘差權重提到 5-105000 步后1e-4 以下凍結邊界權重只降殘差若發(fā)現(xiàn)驗證誤差在中間平臺期不再變化問題往往不在學習率而在采樣點數(shù)量。把 interior 點的 batch size 從 1024 提到 4096讓每次梯度的近似噪聲變小這種改進的效果比調(diào)權重更明顯。模塊化后這一步只改采樣器不會觸碰方程代碼。4.3 邊界條件振蕩時的操作序列當邊界 loss 曲線上下跳動而殘差在平穩(wěn)下降時先用grad_norm排查邊界點上的梯度是否過大。比如一個窄脈沖邊界條件在邊界附近采樣密度不足會導致網(wǎng)絡暴力擬合到不光滑狀態(tài)。這時可以把邊界采樣器從均勻隨機改成邊界處加密配合clip_grad_norm_把梯度限制在 10 以內(nèi)。提示在boundary()里不要只返回邊界坐標和邊值還應該返回邊界采樣的時間點。后續(xù)做誤差歸因時必須知道是哪一段邊界造成 loss 波動。5. 用解析解和制造解給模塊化 PINN 做定量驗收訓練結束不等于實現(xiàn)正確。模塊化 PINN 最大的風險是每個模塊單獨驗證時都正確組裝后卻在某個邊界上不斷累積誤差。這里給一個具體的定量驗證方法用解析解評估整套裝配是否真的做到了物理約束。以一維熱方程為例解析解為u(x,t) exp(-pi^2 * t) * sin(pi * x)。把最終的網(wǎng)絡預測與解析解在均勻網(wǎng)格上對比計算相對 L2 誤差def relative_l2_error(model, x_grid, t_eval): x torch.linspace(0, 1, x_grid).reshape(-1, 1) t torch.full_like(x, t_eval) coords torch.cat([x, t], dim1) with torch.no_grad(): u_pred model(coords).reshape(-1) u_exact torch.exp(-torch.pi**2 * t_eval) * torch.sin(torch.pi * x).reshape(-1) err torch.sqrt(torch.sum((u_pred - u_exact) ** 2) / torch.sum(u_exact ** 2)) return err.item() for time_value in [0.0, 0.1, 0.5, 1.0]: err relative_l2_error(model, 200, time_value) print(ft{time_value:.2f}, rel. L2 {err:.3e})這個驗證代碼放在訓練腳本之外作為一個獨立模塊。當錯誤率達到 1e-2 量級時逐個隔離嫌疑模塊先固定網(wǎng)絡結構、去掉殘差項只擬合初值和邊界確認基礎插值沒有系統(tǒng)偏差再加入殘差項觀察誤差是否變化。若是誤差突然跳到 1e-1 以上多半是自動微分中的create_graph沒有在每次前向傳播時打開導致二階導被當作常數(shù)。更進一步的驗證是對同一套代碼用制造解Method of Manufactured Solutions改造方程模塊的右端項。比如人為設定精確解為sin(pi * x) * cos(pi * t)代入熱方程后會產(chǎn)生一個非零源項f(x,t)把它直接加到方程模塊的返回值中讓殘差公式變成u_t - u_xx - f 0。變動只發(fā)生在HeatEquation.residual()方法體內(nèi)采樣器和訓練循環(huán)完全不動。這既驗證了方程模塊的正確性也驗證了裝配器沒有在不知不覺中引入額外約束。把誤差繼續(xù)按時間切片打印你會看到誤差通常最先在初始時刻附近冒頭因為那里的初值條件約束住了剛啟動的重量分布。本文還有配套的精品資源點擊獲取