FemGIL/model.py

467 lines
16 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
import torch.nn as nn
from torch_geometric.nn import MessagePassing, TopKPooling
# 从全局配置文件导入迭代次数配置
from config import N_ITER
# ==========================================
# 6. 网络维度配置导入
# ==========================================
# 从全局配置导入网络维度设置
from config import (
NETWORK_USE_CUSTOM_DIMS,
NETWORK_BASE_DIM,
NETWORK_CUSTOM_DIMS,
NETWORK_POOL_RATIOS
)
# ==========================================
# 0. BatchNorm 辅助函数
# ==========================================
def get_batch_norm(num_features):
return nn.BatchNorm1d(num_features)
# ==========================================
# 1. 物理感知图卷积层
# ==========================================
class EMFullComplexLayer(MessagePassing):
def __init__(self, in_feats, out_feats):
super(EMFullComplexLayer, self).__init__()
self.aggr = 'mean'
total_in_dim = in_feats + in_feats
self.lin_fusion = nn.Linear(total_in_dim, out_feats)
def forward(self, x, edge_index):
return self.propagate(edge_index, x=x)
def message(self, x_j):
return x_j
def update(self, aggs, x):
combined = torch.cat([x, aggs], dim=1)
fused_msg = self.lin_fusion(combined)
return fused_msg
# ==========================================
# 2. 修改后的 GCN 模块
# ==========================================
class GCN(nn.Module):
def __init__(self, in_feats, out_feats):
super(GCN, self).__init__()
self.conv = EMFullComplexLayer(in_feats, out_feats)
self.bn = get_batch_norm(out_feats)
self.gelu = nn.GELU()
def forward(self, x, edge_index):
x = self.conv(x, edge_index)
x = self.bn(x)
x = self.gelu(x)
return x
class FFTFeatureLayer(nn.Module):
"""
对特征维度进行 FFT
"""
def __init__(self, in_feats, out_feats):
super(FFTFeatureLayer, self).__init__()
# 线性投影调整维度
self.lin_in = nn.Linear(in_feats, out_feats)
# 可学习的频域滤波器 (复数权重)
# RFFT 后,频域维度为 (out_feats // 2) + 1
self.freq_dim = out_feats // 2 + 1
# 初始化复数权重
# shape: [freq_dim] - 对每个频率分量进行缩放
self.complex_weight = nn.Parameter(
torch.randn(self.freq_dim, 2, dtype=torch.float32) * 0.02
)
def forward(self, x):
# x: [N, in_feats] -> [N, out_feats]
x = self.lin_in(x)
# 修复cuFFT在float16模式下只支持维度大小为2的幂次
# 如果输入是float16且维度不是2的幂次需要转换为float32
original_dtype = x.dtype
converted_to_float32 = False
if x.dtype == torch.float16:
# 检查最后一个维度是否为2的幂次
last_dim = x.size(-1)
is_power_of_two = (last_dim & (last_dim - 1)) == 0 and last_dim != 0
if not is_power_of_two:
# 转换为float32以避免cuFFT限制
x = x.to(torch.float32)
converted_to_float32 = True
x_fft = torch.fft.rfft(x, dim=-1)
# 2. 频域滤波 / 混合
weight = torch.view_as_complex(self.complex_weight)
# 广播乘法: [N, freq_dim] * [freq_dim]
x_fft = x_fft * weight
# 3. 傅里叶逆变换 (Complex -> Real)
x_out = torch.fft.irfft(x_fft, n=x.size(-1), dim=-1)
# 如果原始是float16且被转换过转换回float16保持精度一致性
if original_dtype == torch.float16 and converted_to_float32:
x_out = x_out.to(original_dtype)
return x_out
# ==========================================
# 2.5 [核心修改] FFM 增强的输入层
# ==========================================
class FFMEncodingLayer(nn.Module):
"""
使用傅里叶特征映射 (FFM) 替换传统的线性输入层。
逻辑:
1. 输入 features (如坐标或物理量)
2. 映射 -> sin(2*pi*B*x), cos(2*pi*B*x)
3. 线性投影融合
4. 物理图卷积 (EMFullComplexLayer)
"""
def __init__(self, in_feats, out_feats, sigma=1.0, mapping_size=None):
super(FFMEncodingLayer, self).__init__()
# 1. 配置 FFM 参数
self.input_dim = in_feats
# 如果未指定 mapping_size默认设为 out_feats 的一半因为sin/cos会翻倍
# 或者设为一个固定的高维空间,例如 128 或 256
self.mapping_size = mapping_size if mapping_size is not None else max(out_feats, 64)
self.sigma = sigma
# 2. 初始化高斯随机矩阵 B (不可学习,类似位置编码)
# shape: [in_feats, mapping_size]
self.B = nn.Parameter(
torch.randn(self.input_dim, self.mapping_size) * self.sigma,
requires_grad=False
)
# FFM 后的维度 = mapping_size * 2 (sin + cos)
ffm_out_dim = self.mapping_size * 2
# 3. 特征融合层:将高维 FFM 特征投影回目标维度
self.feature_projection = nn.Linear(ffm_out_dim, out_feats)
# 4. 空间混合:保留图卷积能力
self.conv = EMFullComplexLayer(out_feats, out_feats)
# 5. 激活与归一化
self.bn = get_batch_norm(out_feats)
self.gelu = nn.GELU()
def input_mapping(self, x):
# x: [N, input_dim]
# 投影: (2 * pi * x) @ B
# 结果 shape: [N, mapping_size]
x_proj = (2.0 * torch.pi * x) @ self.B
# 拼接 sin 和 cos -> [N, mapping_size * 2]
return torch.cat([torch.cos(x_proj), torch.sin(x_proj)], dim=-1)
def forward(self, x, edge_index):
# 1. FFM 映射 (提升频率感知能力)
x_ffm = self.input_mapping(x)
# 2. 线性投影 (融合特征)
x_embed = self.feature_projection(x_ffm)
# 3. 图卷积 (聚合邻居信息)
x_out = self.conv(x_embed, edge_index)
# 4. 后处理
x_out = self.bn(x_out)
x_out = self.gelu(x_out)
return x_out
# ==========================================
# 2.6 新增:混合谱图卷积层 (替代原 GCN)
# ==========================================
class SpectralGCN(nn.Module):
"""
混合层:结合 局部图卷积(GCN) 和 频域特征变换(FFT)
Out = GCN(x, edge) + FFT(x)
"""
def __init__(self, in_feats, out_feats):
super(SpectralGCN, self).__init__()
# 分支1物理感知图卷积 (处理局部连接)
self.spatial_conv = EMFullComplexLayer(in_feats, out_feats)
# 分支2FFT 频域层 (处理全局/频域特征)
self.spectral_conv = FFTFeatureLayer(in_feats, out_feats)
# 融合门控 (可学习的加权系数)
self.alpha = nn.Parameter(torch.tensor(0.5))
self.bn = get_batch_norm(out_feats)
self.gelu = nn.GELU()
def forward(self, x, edge_index):
# 1. 空间路径:消息传递
x_spatial = self.spatial_conv(x, edge_index)
# 2. 频谱路径FFT 变换 (不依赖 edge_index纯节点特征变换)
x_spectral = self.spectral_conv(x)
# 3. 残差融合
# 使用 sigmoid 确保融合比例在 0-1 之间,或者直接相加
# 这里使用加权求和,兼顾空间和频域信息
x_out = x_spatial + self.alpha * x_spectral
# 4. 激活与归一化
x_out = self.bn(x_out)
x_out = self.gelu(x_out)
return x_out
# ==========================================
# 2.7 FFM处理
# ==========================================
class ffm_process(nn.Module):
def __init__(self, in_feats, out_feats, ffm_sigma=1.0):
super(ffm_process, self).__init__()
self.B = nn.Parameter(torch.randn(in_feats, out_feats) * ffm_sigma, requires_grad=False)
def forward(self, x):
x_proj = (2.0 * torch.pi * x) @ self.B
return torch.cat([torch.cos(x_proj), torch.sin(x_proj)], dim=-1)
# ==========================================
# 3. Graph U-Nets 组件 (Checkpoint稳定版)
# ==========================================
class gPool(nn.Module):
"""Graph Pooling layer - 简化版,不特殊处理背景场"""
def __init__(self, in_dim, ratio):
super(gPool, self).__init__()
self.ratio = ratio
self.proj = nn.Linear(in_dim, 1) # 评分投影
self.sigmoid = nn.Sigmoid()
def forward(self, x, edge_index):
N, C = x.size()
# 计算节点重要性得分(使用所有特征)
scores = self.proj(x) # [N, 1]
scores = self.sigmoid(scores).squeeze() # [N]
# 确定保留的节点数量
k = max(1, int(self.ratio * N))
# 选择top-k节点
values, idx = torch.topk(scores, k)
# 门控机制:保留的节点特征按重要性加权
x_pool = x[idx] * values.view(-1, 1) # [k, C]
# 筛选保留节点之间的边
row, col = edge_index
row_mask = torch.zeros(N, dtype=torch.bool, device=x.device)
col_mask = torch.zeros(N, dtype=torch.bool, device=x.device)
row_mask[idx] = True
col_mask[idx] = True
edge_mask = row_mask[row] & col_mask[col]
edge_index_pool = edge_index[:, edge_mask]
# 重新映射节点索引到0-k范围
idx_map = -torch.ones(N, dtype=torch.long, device=x.device)
idx_map[idx] = torch.arange(k, device=x.device)
edge_index_pool[0] = idx_map[edge_index_pool[0]]
edge_index_pool[1] = idx_map[edge_index_pool[1]]
return x_pool, edge_index_pool, idx
class gUnpool(nn.Module):
"""Graph Unpooling layer (inverse of Pool)"""
def __init__(self):
super(gUnpool, self).__init__()
def forward(self, x_pool, x_skip, idx):
# idx 对应 TopKPooling 返回的 perm (保留节点的索引)
N = x_skip.size(0)
C = x_pool.size(1)
# 恢复原始图结构大小
x_unpool = torch.zeros(N, C, device=x_pool.device, dtype=x_skip.dtype)
if idx is not None and len(idx) > 0:
# 填充保留节点的特征
# PyG TopKPooling 的 perm 保证了 idx < N
# 确保 x_pool 的行数与 idx 长度一致
if x_pool.size(0) == len(idx):
x_unpool[idx] = x_pool
else:
# 理论上不应发生,除非维度对不上
valid_len = min(x_pool.size(0), len(idx))
x_unpool[idx[:valid_len]] = x_pool[:valid_len]
# 跳跃连接(残差连接)
x_unpool = x_unpool + x_skip
return x_unpool
# ==========================================
# 4. 辅助模块
# ==========================================
class StackedLinearBlock(nn.Module):
def __init__(self, in_feats, out_feats, dropout):
super(StackedLinearBlock, self).__init__()
self.fc = nn.Linear(in_feats, out_feats)
self.bn = get_batch_norm(out_feats)
self.gelu = nn.GELU()
self.dropout = nn.Dropout(dropout)
self.reset_parameters()
def reset_parameters(self):
nn.init.kaiming_uniform_(self.fc.weight, a=1.0)
if self.fc.bias is not None:
nn.init.zeros_(self.fc.bias)
def forward(self, x):
x = self.fc(x)
x = self.bn(x)
x = self.gelu(x)
x = self.dropout(x)
return x
class FinalLinear(nn.Module):
def __init__(self, in_feats, out_feats):
super(FinalLinear, self).__init__()
self.fc = nn.Linear(in_feats, out_feats)
self.reset_parameters()
def reset_parameters(self):
nn.init.kaiming_uniform_(self.fc.weight, a=1.0)
if self.fc.bias is not None:
nn.init.zeros_(self.fc.bias)
def forward(self, x):
x = self.fc(x)
return x
# ==========================================
# 5. BuildGCN 网络构建 (最接近原始版本)
# ==========================================
class BuildGCN(nn.Module):
def __init__(self, input_feats, output_feats, base_dim=None, custom_dims=None):
super(BuildGCN, self).__init__()
# 优先级:函数参数 > 全局配置 > 默认值
if custom_dims is not None:
# 函数参数指定的自定义维度(最高优先级)
dims = custom_dims
elif base_dim is not None:
# 函数参数指定的基础维度
dims = [base_dim, base_dim * 2, base_dim * 4]
elif NETWORK_USE_CUSTOM_DIMS:
# 全局配置的自定义维度
dims = NETWORK_CUSTOM_DIMS
else:
# 全局配置的基础维度自动计算
dims = [NETWORK_BASE_DIM, NETWORK_BASE_DIM * 2, NETWORK_BASE_DIM * 4]
# 使用全局配置的池化比例
pool_ratios = NETWORK_POOL_RATIOS
# ========== 编码器路径 ==========
self.gcn1 = SpectralGCN(input_feats, dims[0])
self.pool1 = TopKPooling(dims[0], pool_ratios[0])
self.gcn2 = GCN(dims[0], dims[1])
self.pool2 = TopKPooling(dims[1], pool_ratios[1])
self.gcn3 = GCN(dims[1], dims[2])
# ========== 瓶颈层 ==========
self.bottom_gcn = GCN(dims[2], dims[2])
# ========== 解码器路径 ==========
self.dec_gcn1 = GCN(dims[2], dims[1])
self.unpool1 = gUnpool()
self.dec_gcn2 = GCN(dims[1], dims[0])
self.unpool2 = gUnpool()
self.dec_gcn3 = GCN(dims[0], dims[0])
# ========== 输出层 ==========
self.lin_stacked = StackedLinearBlock(dims[0], dims[0], dropout=0.5)
self.final_linear = FinalLinear(dims[0], output_feats)
def forward(self, ndata, edata, batch=None):
x, edge_index = ndata, edata
if batch is None:
batch = torch.zeros(x.size(0), dtype=torch.long, device=x.device)
# === 编码器路径 ===
x1 = self.gcn1(x, edge_index)
# 第1次池化 (TopKPooling)
# 返回值: x, edge_index, edge_attr, batch, perm, score
x2, edge_index2, _, batch2, idx1, _ = self.pool1(x1, edge_index, batch=batch)
x2 = self.gcn2(x2, edge_index2)
# 第2次池化 (TopKPooling)
x3, edge_index3, _, batch3, idx2, _ = self.pool2(x2, edge_index2, batch=batch2)
x3 = self.gcn3(x3, edge_index3)
# === 瓶颈层 ===
x_bottom = self.bottom_gcn(x3, edge_index3)
# === 解码器路径 ===
# 注意:这里继续使用 edge_index3和原逻辑一致
x_up1 = self.dec_gcn1(x_bottom, edge_index3)
# 反池化:需要传入 idx (即 perm) 来恢复到上一层的大小 (x2 的大小)
x_up1 = self.unpool1(x_up1, x2, idx2)
# 注意:这里使用 edge_index2
x_up2 = self.dec_gcn2(x_up1, edge_index2)
# 反池化:恢复到输入层大小 (x1 的大小)
x_up2 = self.unpool2(x_up2, x1, idx1)
x_out = self.dec_gcn3(x_up2, edge_index)
# === 输出层 ===
x_out = self.lin_stacked(x_out)
x_out = self.final_linear(x_out)
return x_out
# ==========================================
# 7. BuildGCNList (保持不变)
# ==========================================
class BuildGCNList(nn.Module):
def __init__(self, input_feats, output_feats, n_iter=N_ITER, base_dim=None, custom_dims=None):
super(BuildGCNList, self).__init__()
self.n_iter = n_iter
self.input_feats = input_feats
self.output_feats = output_feats
self.networks = nn.ModuleList()
for i in range(n_iter):
network = BuildGCN(input_feats, output_feats, base_dim=base_dim, custom_dims=custom_dims)
self.networks.append(network)
setattr(self, f'iter_{i}', network)
print(f"已创建 {n_iter} 个集成Graph U-Nets的BuildGCN网络")
def forward(self, ndata, edata, batch=None, iter_idx=None):
if iter_idx is not None:
if iter_idx < 0 or iter_idx >= self.n_iter:
raise ValueError(f"iter_idx必须在[0, {self.n_iter-1}]范围内")
return self.networks[iter_idx](ndata, edata, batch)
else:
outputs = []
for i in range(self.n_iter):
output = self.networks[i](ndata, edata, batch)
outputs.append(output)
return outputs
def get_network(self, iter_idx):
return self.networks[iter_idx]
def __len__(self):
return self.n_iter