Deeponet/cnn_branch_test2.py

484 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import torch
from torch.autograd import Function
# import modules
from dataclasses import dataclass
from tqdm.auto import tqdm
import numpy as np
from getdata import GetDataset
# deep learning modules
import scipy.sparse as sp
from scipy.sparse.linalg import spilu
from scipy.io import loadmat
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import datetime
import pandas as pd
# Plot modules
import matplotlib.pyplot as plt
from scipy.interpolate import griddata
from pathlib import Path
from scipy.io import savemat
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
class ILUApply(Function):
@staticmethod
def forward(ctx, r_torch, ilu):
"""
r_torch: torch complex tensor, shape (Mi,)
ilu: fixed SciPy spilu object
"""
ctx.ilu = ilu
r_np = r_torch.detach().cpu().numpy()
z_np = ilu.solve(r_np) # z = M^{-1} r
z = torch.from_numpy(z_np).to(r_torch.device).to(r_torch.dtype)
return z
@staticmethod
def backward(ctx, grad_out):
"""
grad_out: dL/dz
complex: grad_r = M^{-H} grad_out
"""
ilu = ctx.ilu
g_np = grad_out.detach().cpu().numpy()
gr_np = ilu.solve(g_np, trans='H') # conjugate-transpose solve
grad_r = torch.from_numpy(gr_np).to(grad_out.device).to(grad_out.dtype)
return grad_r, None
@dataclass
class PINNConfig:
# 训练(仅 PDE 残差时建议 batch_size 较小如 16~32避免 branch 输出塌缩)
epochs: int = 1_000
batch_size: int = 32
learning_rate: float = 1e-2
step_size: int = 500 # StepLR: 每 step_size 轮衰减一次
gamma: float = 0.95 # StepLR 衰减系数
max_grad_norm: float = 1.0 # 梯度裁剪,稳定训练
print_every: int = 1 # 每多少轮打印一次
save_every: int = 1000 # 每多少轮保存一次 checkpoint
# 数据n 为样本数目,如 deepOnet_data_A1_100 表示 100 个样本)
matpath: str = "deepOnet_data_A1_1558_8_2"
# 保存
save_dir: str = "./model_save"
results_dir: str = "./results"
load_file_name: str = "model_A1_size_1558_2"
save_file_name: str = "model_A1_size_1558_2"
# 设备/精度
device: str = "cuda" if torch.cuda.is_available() else "cpu"
dtype: torch.dtype = torch.float64 # 和 .mat 的 float64 对齐double
num_workers: int = 10
pin_memory: bool = True
class Modified_MLP_Block(nn.Module):
def __init__(self, input_dim, hidden_channel, output_dim, hidden_size=6):
super(Modified_MLP_Block, self).__init__()
self.activation = nn.Tanh()
self.encodeU = nn.Linear(input_dim, hidden_channel)
self.encodeV = nn.Linear(input_dim, hidden_channel)
self.In = nn.Linear(input_dim, hidden_channel)
self.hidden_layers = nn.ModuleList([
nn.Linear(hidden_channel, hidden_channel) for _ in range(hidden_size)
])
self.out = nn.Linear(hidden_channel, output_dim)
self._init_weights()
def _init_weights(self):
torch.manual_seed(123)
gain = nn.init.calculate_gain('tanh')
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight, gain=gain)
nn.init.zeros_(m.bias)
def forward(self, x):
U = self.activation(self.encodeU(x))
V = self.activation(self.encodeV(x))
Hidden = self.activation(self.In(x))
for layer in self.hidden_layers:
Z = self.activation(layer(Hidden))
Hidden = (1 - Z) * U + Z * V
x = self.out(Hidden)
return x
def _branch_norm2d(channels):
"""Branch 内使用 InstanceNorm2d按样本、按通道独立归一化不跨样本混合避免输出塌缩。"""
return nn.InstanceNorm2d(channels)
def add_spatial_coord_channels(epsilon_data):
"""
在 ε 图像上拼接空间坐标通道,使 CNN 能区分「同形不同位」的介质。
epsilon_data: (B, 1, H, W) -> 返回 (B, 3, H, W),通道为 [ε, x_norm, y_norm],归一化到 [0,1]。
"""
B, _, H, W = epsilon_data.shape
device, dtype = epsilon_data.device, epsilon_data.dtype
j = torch.linspace(0, 1, W, device=device, dtype=dtype).view(1, 1, 1, W).expand(B, 1, H, W)
i = torch.linspace(0, 1, H, device=device, dtype=dtype).view(1, 1, H, 1).expand(B, 1, H, W)
return torch.cat([epsilon_data, j, i], dim=1)
class CNN_Branch_Residual(nn.Module):
"""带残差连接的 CNN 分支Branch 用 InstanceNorm2d输入含 ε + 空间坐标通道以区分同形不同位)。"""
def __init__(self, in_channels=3, num_classes=128):
super(CNN_Branch_Residual, self).__init__()
# 初始卷积层
self.initial = nn.Sequential(
nn.Conv2d(in_channels, 32, kernel_size=3, padding=1, bias=False),
_branch_norm2d(32),
nn.ReLU(inplace=True),
nn.Conv2d(32, 32, kernel_size=3, padding=1, bias=False),
_branch_norm2d(32),
nn.ReLU(inplace=True)
)
# 残差块
self.res_block1 = ResidualBlock(32, 64, stride=2, norm_layer=_branch_norm2d)
self.res_block2 = ResidualBlock(64, 128, stride=2, norm_layer=_branch_norm2d)
# 全局平均池化
self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1))
# 全连接层
self.fc = nn.Linear(128, num_classes)
def forward(self, x):
x = self.initial(x)
x = self.res_block1(x)
x = self.res_block2(x)
x = self.global_avg_pool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
class ResidualBlock(nn.Module):
"""残差块(支持 norm_layerBranch 中传入 GroupNorm"""
def __init__(self, in_channels, out_channels, stride=1, norm_layer=None):
super(ResidualBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = norm_layer(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = norm_layer(out_channels)
self.relu = nn.ReLU(inplace=True)
# 下采样连接
self.downsample = None
if stride != 1 or in_channels != out_channels:
self.downsample = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1,
stride=stride, bias=False),
norm_layer(out_channels)
)
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class DeepONet(nn.Module):
def __init__(self, branch_input_dim, trunk_input_dim, hidden_channel, output_dim):
super(DeepONet, self).__init__()
self.output_dim = output_dim
# branch 输入为 ε + 2 个空间坐标通道,共 3 通道,便于区分同形不同位的介质
self.branch_net = CNN_Branch_Residual(in_channels=3, num_classes=output_dim)
self.trunk_net = Modified_MLP_Block(trunk_input_dim, hidden_channel, output_dim)
def forward(self, branch_input, trunk_input):
branch_input = add_spatial_coord_channels(branch_input)
branch_out = self.branch_net(branch_input)
trunk_out = self.trunk_net(trunk_input)
B1, B2 = branch_out[:, :self.output_dim//2], branch_out[:, self.output_dim//2:]
T1, T2 = trunk_out[:, :, :self.output_dim//2], trunk_out[:, :, self.output_dim//2:]
#print("B1 shape:", B1.shape, "B2 shape:", B2.shape)
#print("T1 shape:", T1.shape, "T2 shape:", T2.shape)
s_re = torch.einsum('bi,bni->bn', B1, T1) #实部
s_im = torch.einsum('bi,bni->bn', B2, T2)
return s_re, s_im
class PINN_maxwell():
def __init__(self, model, config: PINNConfig):
self.cfg = config
self.device = torch.device(self.cfg.device)
self.model = model.to(self.device, dtype=self.cfg.dtype)
self.batch_size = self.cfg.batch_size
self.learning_rate = self.cfg.learning_rate
self.matpath = self.cfg.matpath
self.loss_fn = nn.MSELoss()
self.optimizer = optim.Adam(self.model.parameters(), lr=self.learning_rate)
self.scheduler = optim.lr_scheduler.StepLR(self.optimizer, step_size=self.cfg.step_size, gamma=self.cfg.gamma)
self.losses = []
self.lamda = []
self.save_file_name = self.cfg.save_file_name
self.load_file_name = self.cfg.load_file_name
self.save_dir = Path(self.cfg.save_dir)
self.train_set, self.test_set = self.load_dataset()
self.train_loader = DataLoader(self.train_set, self.cfg.batch_size, shuffle=True)
self.test_loader = DataLoader(self.test_set, batch_size=len(self.test_set), shuffle=False)
self.ilu_cache = {}
self._build_ilu_cache()
def load_model(self):
self.model.load_state_dict(torch.load(self.save_dir / f'{self.load_file_name}.pth', map_location=self.device, weights_only=True))
def E_function(self, epsilon_data, coord_data):
epsilon_data = epsilon_data.to(self.device)
coord_data = coord_data.to(self.device)
return self.model(epsilon_data, coord_data)
def get_data_loss(self, epsilon_data, coord_data, E_true):
E_re_pred, E_im_pred = self.E_function(epsilon_data, coord_data)
E_re_true = E_true[:,:, 0]
E_im_true = E_true[:,:, 1]
data_loss = self.loss_fn(E_re_pred, E_re_true) + self.loss_fn(E_im_pred, E_im_true)
return data_loss
def get_fem_loss(self, indices, epsilon_data, coord_data, E_true, Ai, Aj, Av, b, coord_len):
"""indices: (B,) 每个样本的全局索引,用于从 ilu_cache 取对应的 ILU。"""
Ere_pred, Eim_pred = self.E_function(epsilon_data, coord_data)
E = torch.complex(Ere_pred, Eim_pred) # shape: (B, Mmax)
B, Mmax = E.shape
Mi = coord_len.squeeze(-1).long().to(self.device) # shape: (B,)
arangeM = torch.arange(Mmax, device=self.device) # (Mmax,)
mask_x = arangeM[None, :] < Mi[:, None] # (B, Mmax)
x_flat = E[mask_x] # (sum Mi,)
b_flat = b.to(self.device)[mask_x].to(x_flat.dtype) # (sum Mi,)
sumMi = int(Mi.sum().item())
offsets = torch.cumsum(
torch.cat([torch.zeros(1, device=self.device, dtype=torch.long), Mi[:-1]]),
dim=0
) # (B,)
Ai = Ai.to(self.device).long()
Aj = Aj.to(self.device).long()
Av = Av.to(self.device).to(x_flat.dtype)
mask_nnz = (Ai > 0) & (Aj > 0) # (B, Kmax)padding 位置为 0
rows = (Ai - 1 + offsets.unsqueeze(1)).masked_select(mask_nnz)
cols = (Aj - 1 + offsets.unsqueeze(1)).masked_select(mask_nnz)
vals = Av.masked_select(mask_nnz)
y = torch.zeros(sumMi, dtype=x_flat.dtype, device=self.device)
y.scatter_add_(0, rows, vals * x_flat.index_select(0, cols))
r = y - b_flat
# 按样本用缓存的 ILU 计算 z每个样本一个 ILU
z_parts = []
for i in range(B):
start = int(offsets[i].item())
m = int(Mi[i].item())
r_i = r[start : start + m]
ilu = self.ilu_cache[int(indices[i].item())]
z_i = ILUApply.apply(r_i, ilu)
z_parts.append(z_i)
z = torch.cat(z_parts, dim=0)
loss = (z.abs() ** 2).mean()
return loss
@torch.no_grad()
def test_E_loss(self):
self.model.eval()
total_loss = 0.0
for indices, epsilon_data, coord_data, E_true, Ai, Aj, Av, b, coord_len in self.test_loader:
indices = indices.to(self.device)
epsilon_data = epsilon_data.to(self.device)
coord_data = coord_data.to(self.device)
loss = self.get_fem_loss(indices, epsilon_data, coord_data, E_true, Ai, Aj, Av, b, coord_len)
total_loss += loss.item()
self.model.train()
return total_loss / len(self.test_loader)
def train(self, epochs, print_every=100, save_every=10000):
self.losses.append(['epoch', 'fem_loss', 'test_loss'])
start_time = datetime.datetime.now()
for epoch in tqdm(range(epochs), desc='Training'):
self.model.train()
total_loss = 0.0
fem_loss = 0.0
for indices, epsilon_data, coord_data, E_true, Ai, Aj, Av, b, coord_len in self.train_loader:
indices = indices.to(self.device)
epsilon_data = epsilon_data.to(self.device)
coord_data = coord_data.to(self.device)
self.optimizer.zero_grad()
fem_loss = self.get_fem_loss(indices, epsilon_data, coord_data, E_true, Ai, Aj, Av, b, coord_len)
loss = fem_loss
loss.backward()
if self.cfg.max_grad_norm > 0:
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.cfg.max_grad_norm)
self.optimizer.step()
total_loss += loss.item()
avg_total_loss = total_loss / len(self.train_loader)
avg_test_loss = self.test_E_loss()
self.losses.append([epoch, avg_total_loss, avg_test_loss])
self.scheduler.step()
if epoch % print_every == 0:
print(f'Epoch {epoch}, Total Loss: {avg_total_loss}, test Loss {avg_test_loss}')
if (epoch + 1) % save_every == 0:
ckpt_path = self.save_dir / f'{self.save_file_name}_epoch{epoch + 1}.pth'
torch.save(self.model.state_dict(), ckpt_path)
torch.save(self.model.state_dict(), self.save_file_name)
print("Current learning rate:", self.optimizer.param_groups[0]['lr'])
print("Training Time:", (datetime.datetime.now() - start_time).total_seconds(), "s")
def plot_loss(self):
data = np.array(self.losses[1:])
epochs = data[:, 0]
train_loss = data[:, 1]
test_loss = data[:, 2]
plt.figure(figsize=(10, 6))
plt.title('Training/Test Loss')
plt.semilogy(epochs, train_loss, label='train_loss')
plt.semilogy(epochs, test_loss, label='test_loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid()
save_path = 'results/loss_plot2_1.png'
plt.savefig(save_path)
plt.show()
def load_dataset(self):
"""从 MATLAB 划分好的 .mat 文件加载数据90% 训练 10% 测试由 mat 内已分好。"""
data_set = loadmat(self.matpath)
Epsilon_train = data_set['Eplison_train']
X_train = data_set['X_train']
Ez_train = data_set['Ez_train']
Epsilon_test = data_set['Eplison_test']
X_test = data_set['X_test']
Ez_test = data_set['Ez_test']
coord_len_train = data_set['coord_len_train']
coord_len_test = data_set['coord_len_test']
Ai_train, Aj_train = data_set['Ai_train'], data_set['Aj_train']
Ai_test, Aj_test = data_set['Ai_test'], data_set['Aj_test']
Av_train, Av_test = data_set['Av_train'], data_set['Av_test']
b_train, b_test = data_set['b_train'], data_set['b_test']
n_train, n_test = len(Epsilon_train), len(Epsilon_test)
print(f"Train samples: {n_train}, Test samples: {n_test}")
print(f"Train shapes: ε {Epsilon_train.shape}, X {X_train.shape}, Ez {Ez_train.shape}")
Train_dataset = GetDataset(
Epsilon_train, X_train, Ez_train,
Ai_train, Aj_train, Av_train, b_train, coord_len_train,
index_offset=0
)
Test_dataset = GetDataset(
Epsilon_test, X_test, Ez_test,
Ai_test, Aj_test, Av_test, b_test, coord_len_test,
index_offset=n_train
)
return Train_dataset, Test_dataset
def _build_ilu_cache(self):
"""训练开始前为每个样本的稀疏矩阵 A 预计算 ILU 并缓存,训练过程中 A 不变,只算一次。"""
for idx in tqdm(range(len(self.train_set)), desc="Building ILU cache (train)"):
ai = self.train_set.Ai[idx].numpy()
aj = self.train_set.Aj[idx].numpy()
av = self.train_set.Av[idx].numpy()
Mi = int(self.train_set.coord_len[idx].item())
mask = (ai > 0) & (aj > 0)
rows = (ai[mask] - 1).astype(np.int64)
cols = (aj[mask] - 1).astype(np.int64)
vals = av[mask]
A = sp.coo_matrix((vals, (rows, cols)), shape=(Mi, Mi)).tocsc()
self.ilu_cache[idx] = spilu(A)
for idx in tqdm(range(len(self.test_set)), desc="Building ILU cache (test)"):
ai = self.test_set.Ai[idx].numpy()
aj = self.test_set.Aj[idx].numpy()
av = self.test_set.Av[idx].numpy()
Mi = int(self.test_set.coord_len[idx].item())
mask = (ai > 0) & (aj > 0)
rows = (ai[mask] - 1).astype(np.int64)
cols = (aj[mask] - 1).astype(np.int64)
vals = av[mask]
A = sp.coo_matrix((vals, (rows, cols)), shape=(Mi, Mi)).tocsc()
self.ilu_cache[len(self.train_set) + idx] = spilu(A)
print(f"ILU cache built: {len(self.ilu_cache)} samples.")
def saveE_pred(self):
for indices, epsilon_data, coord_data, *_ in self.train_loader:
epsilon_data = epsilon_data.to(self.device)
coord_data = coord_data.to(self.device)
E_real, E_imag = self.E_function(epsilon_data, coord_data)
E_pred = torch.complex(E_real, E_imag)
E_pred_np = E_pred.detach().cpu().numpy()
savemat('E_train_pred_size_1588.mat', {"E_pred": E_pred_np})
for indices, epsilon_data, coord_data, *_ in self.test_loader:
epsilon_data = epsilon_data.to(self.device)
coord_data = coord_data.to(self.device)
E_real, E_imag = self.E_function(epsilon_data, coord_data)
E_pred = torch.complex(E_real, E_imag)
E_pred_np = E_pred.detach().cpu().numpy()
savemat('E_test_pred_size_1588.mat', {"E_pred": E_pred_np})
if __name__ == "__main__":
cfg = PINNConfig(
)
model = DeepONet(branch_input_dim=1, trunk_input_dim=2, hidden_channel=128, output_dim=64)
model = model.double()
pinn = PINN_maxwell(model, cfg)
# pinn.load_model()
#pinn.test_fem_loss()
pinn.train(epochs=cfg.epochs, print_every=cfg.print_every, save_every=cfg.save_every)
pinn.plot_loss()
# pinn.saveE_pred()