图神经网络(GNN)
1. 图的基本概念
通常使用G = (V, E)来表示图,其中V表示节点的集合、E表示边的集合。对于两个相邻节点u , v 使用e=(u,v)表示这两个节点之间的边。两个节点之间边既可能是有向,也可能无向。若有向,则称之有向图(Directed Graph), 反之,称之为无向图(Undirected Graph)。
2. 图的表示
在图神经网络中,常见的表示方法有邻接矩阵、度矩阵、拉普拉斯矩阵等。
3. 经典的图神经网络模型
GCN: Graph Convolution Networks(图卷积网络)
GCN是一种在图中结合拓扑结构和顶点属性信息学习顶点的embedding表示的方法。然而GCN要求在一个确定的图中去学习顶点的embedding,无法直接泛化到在训练过程没有出现过的顶点,即属于一种直推式(transductive)的学习。
*
GraphSAGE:Graph Sample and aggregate (图采样和聚合)
GraphSAGE则是一种能够利用顶点的属性信息高效产生未知顶点embedding的一种归纳式(inductive)学习的框架。其核心思想是通过学习一个对邻居顶点进行聚合表示的函数来产生目标顶点的embedding向量。
*
GAT:Graph Attention Networks(图注意力网络)
为了解决GNN聚合邻居节点的时候没有考虑到不同的邻居节点重要性不同的问题,GAT借鉴了Transformer
的idea,引入masked self-attention
机制,在计算图中的每个节点的表示的时候,会根据邻居节点特征的不同来为其分配不同的权值。
4.GCN: Graph Convolution Networks(图卷积网络)
python
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.data import Data
from torch_geometric.nn import GCNConv
# 1. 创建数据并标准化
# 节点特征 (4个节点,每个节点3维特征)
x = torch.tensor([
[1.0, 0.2, 0.5], # 节点0
[0.4, 0.9, 0.1], # 节点1
[0.8, 0.3, 0.6], # 节点2
[0.1, 0.7, 0.4] # 节点3
], dtype=torch.float)
edge_index = torch.tensor([
[0, 1, 1, 2, 2, 3], # 源节点
[1, 0, 2, 1, 3, 2] # 目标节点
], dtype=torch.long)
y = torch.tensor([12.5, 18.3, 22.1, 15.4], dtype=torch.float).view(-1, 1)
# 数据标准化
x_mean, x_std = x.mean(dim=0), x.std(dim=0)
y_mean, y_std = y.mean(), y.std()
data = Data(
x=(x - x_mean) / x_std,
edge_index=edge_index,
y=(y - y_mean) / y_std,
train_mask=torch.tensor([True, True, False, False]), # 前两个节点训练
test_mask=torch.tensor([False, False, True, True]) # 后两个节点测试
)
# 2. 增强模型
class EnhancedGCNRegressor(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = GCNConv(3, 32)
self.conv2 = GCNConv(32, 16)
self.conv3 = GCNConv(16, 8)
self.linear = nn.Linear(8, 1)
self.dropout = 0.2
def forward(self, x, edge_index):
x = F.relu(self.conv1(x, edge_index))
x = F.dropout(x, self.dropout, training=self.training)
x = F.relu(self.conv2(x, edge_index))
x = F.relu(self.conv3(x, edge_index))
return self.linear(x)
# 3. 初始化训练组件
model = EnhancedGCNRegressor()
# 修改优化器参数(添加权重衰减防止过拟合)
optimizer = torch.optim.Adam(model.parameters(), lr=0.005, weight_decay=1e-4)
# 添加学习率调度器
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10)
loss_fn = nn.MSELoss()
# 4. 训练循环(显示还原后的预测值)
for epoch in range(1, 501):
model.train()
optimizer.zero_grad()
pred = model(data.x, data.edge_index)
loss = loss_fn(pred[data.train_mask], data.y[data.train_mask])
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪
optimizer.step()
scheduler.step(loss)
if epoch % 50 == 0:
model.eval()
with torch.no_grad():
test_pred = model(data.x, data.edge_index)
test_loss = loss_fn(test_pred[data.test_mask], data.y[data.test_mask])
# 还原标准化数据
denorm_pred = test_pred * y_std + y_mean
denorm_true = data.y * y_std + y_mean
mae = F.l1_loss(denorm_pred[data.test_mask], denorm_true[data.test_mask])
print(f'Epoch {epoch:03d} | Loss: {loss.item():.4f} | Test MAE: {mae.item():.2f}')
# 5. 最终预测(反标准化后)
model.eval()
with torch.no_grad():
predictions = model(data.x, data.edge_index) * y_std + y_mean
print("\n预测结果对比:")
for i in range(len(y)):
print(f"节点{i}: 预测 {predictions[i].item():.2f} | 真实 {y[i].item():.2f}")
结果展示
python
Epoch 50 | Loss: 0.2362 | Test MAE: 3.51
Epoch 100 | Loss: 0.0774 | Test MAE: 3.37
Epoch 150 | Loss: 0.0309 | Test MAE: 3.37
Epoch 200 | Loss: 0.0005 | Test MAE: 3.37
Epoch 250 | Loss: 0.1027 | Test MAE: 3.37
Epoch 300 | Loss: 0.0019 | Test MAE: 3.37
Epoch 350 | Loss: 0.0130 | Test MAE: 3.37
Epoch 400 | Loss: 0.0133 | Test MAE: 3.37
Epoch 450 | Loss: 0.0268 | Test MAE: 3.37
Epoch 500 | Loss: 0.0010 | Test MAE: 3.37
预测结果对比:
节点0: 预测 12.39 | 真实 12.50
节点1: 预测 18.26 | 真实 18.30
节点2: 预测 19.15 | 真实 22.10
节点3: 预测 19.18 | 真实 15.40
5. gcn+lstm

其中,节点为0,1,2,3,4,边分别是0->2,1->2,2->3,2->4,0、1表示气源,2表示中部节点,3、4表示用户
python
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.data import Data, Dataset
from torch_geometric.loader import DataLoader
from torch_geometric.nn import GCNConv
from sklearn.preprocessing import StandardScaler
import numpy as np
# 配置参数
class Config:
seq_len = 3 # 历史时间窗口
batch_size = 8
hidden_dim = 32
epochs = 200
lr = 0.005
train_ratio = 0.8
nodes = 5 # 节点总数
config = Config()
# 生成模拟数据(带趋势项和周期性的时间序列)
def generate_flow(base, noise=0.2, trend=0.1, period=7):
t = np.arange(days)
return base * (1 + trend * t / days) + np.sin(2 * np.pi * t / period) * 0.5 + np.random.normal(0, noise, days)
days = 30
np.random.seed(42)
flow0 = generate_flow(10) # 气源0
flow1 = generate_flow(8) # 气源1
flow3 = 0.6 * (flow0 + flow1) + generate_flow(0, 0.3) # 用户3
flow4 = 0.4 * (flow0 + flow1) + generate_flow(0, 0.2) # 用户4
# print(f"flow0:{flow0}")
# print(f"flow1:{flow1}")
# print(f"flow3:{flow3}")
# print(f"flow4:{flow4}")
# 数据标准化
scaler = StandardScaler()
node_features = np.stack([flow0, flow1, np.zeros(days), flow3, flow4]).T # (30,5)
scaled_features = scaler.fit_transform(node_features) # 标准化
# 图结构定义
edge_index = torch.tensor([[0, 1, 2, 2],
[2, 2, 3, 4]], dtype=torch.long)
# 创建PyG Dataset
class GasDataset(Dataset):
def __init__(self, sequences):
super().__init__()
self.sequences = sequences
def len(self):
return len(self.sequences)
def get(self, idx):
return self.sequences[idx]
# 构建时序数据集
def create_sequences(data, edge_index):
sequences = []
for i in range(len(data) - config.seq_len):
# 节点特征:[seq_len, num_nodes]
seq_features = torch.FloatTensor(data[i:i + config.seq_len]) # (3,5)
# 目标值:下一时刻的用户流量 (1,2)
target = torch.FloatTensor(data[i + config.seq_len][2:4]).unsqueeze(0) # 调整形状
# 创建Data对象
data_obj = Data(
x=seq_features.T, # GCN需要形状为 [num_nodes, num_features]
edge_index=edge_index,
y=target # 形状调整为(1,2)
)
sequences.append(data_obj)
return sequences
# 创建数据集
sequences = create_sequences(scaled_features, edge_index)
train_size = int(len(sequences) * config.train_ratio)
train_data = GasDataset(sequences[:train_size])
val_data = GasDataset(sequences[train_size:])
# 创建数据加载器
train_loader = DataLoader(train_data, batch_size=config.batch_size, shuffle=True)
val_loader = DataLoader(val_data, batch_size=config.batch_size)
# 定义模型
class GasFlowModel(nn.Module):
def __init__(self):
super().__init__()
# 空间特征提取
self.conv1 = GCNConv(config.seq_len, config.hidden_dim) # 输入特征=时间窗口长度
self.conv2 = GCNConv(config.hidden_dim, 16)
# 时序特征提取
self.lstm = nn.LSTM(
input_size=16 * config.nodes, # 所有节点特征拼接
hidden_size=32,
batch_first=True
)
# 预测层
self.fc = nn.Sequential(
nn.Linear(32, 16),
nn.ReLU(),
nn.Linear(16, 2))
def forward(self, data):
# 输入数据格式
x = data.x # [batch_size*num_nodes, seq_len]
# 计算batch_size(处理单个样本的情况)
if hasattr(data, 'batch') and data.batch is not None:
batch_size = data.batch.max().item() + 1
else:
batch_size = 1 # 单个样本
# 空间特征提取
x = self.conv1(x, data.edge_index) # [batch*num_nodes, hidden_dim]
x = F.relu(x)
x = self.conv2(x, data.edge_index) # [batch*num_nodes, 16]
# 重构为时序数据 [batch, seq_len, features]
x = x.view(batch_size, config.nodes, 16).permute(0, 2, 1) # [batch, 16, nodes]
x = x.reshape(batch_size, -1).unsqueeze(1) # [batch, 1, 16*nodes]
# 时序特征提取
lstm_out, (hn, _) = self.lstm(x) # hn形状 [1, batch, 32]
return self.fc(hn[-1]) # [batch, 2]
model = GasFlowModel()
optimizer = torch.optim.Adam(model.parameters(), lr=config.lr)
criterion = nn.MSELoss()
# 训练循环
best_val_loss = float('inf')
for epoch in range(config.epochs):
# 训练
model.train()
train_loss = 0
for batch in train_loader:
optimizer.zero_grad()
pred = model(batch)
loss = criterion(pred, batch.y)
loss.backward()
optimizer.step()
train_loss += loss.item()
# 验证
model.eval()
val_loss = 0
with torch.no_grad():
for batch in val_loader:
pred = model(batch)
val_loss += criterion(pred, batch.y).item()
avg_train_loss = train_loss / len(train_loader)
avg_val_loss = val_loss / len(val_loader)
# 早停机制
if avg_val_loss < best_val_loss:
best_val_loss = avg_val_loss
torch.save(model.state_dict(), 'best_model.pth')
if epoch % 20 == 0:
print(f'Epoch {epoch:03d} | Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f}')
# 预测第31天
# model.load_state_dict(torch.load('best_model.pth'))
# torch.save(model.state_dict(), 'best_model.pth')
model.eval()
# 构造输入序列
last_seq = scaled_features[-config.seq_len:] # 最后3天数据
input_seq = last_seq # 最后3天数据
# 新一天的气源数据(示例)
# new_day = np.array([[9.5, 8.8, 0, 0, 0]]) # 气源0=9.5, 气源1=8.8
# new_day_scaled = scaler.transform(new_day)[0]
#
# # 更新序列
# full_sequence = np.vstack([scaled_features, new_day_scaled])
# input_seq = full_sequence[-config.seq_len - 1:-1] # 取最后3天(排除最新一天)
# 创建预测数据
pred_data = Data(
x=torch.FloatTensor(input_seq.T), # [5,3] 符合模型输入格式
edge_index=edge_index,
y=torch.zeros(1, 2) # 调整为(1,2)保持一致性
)
# 执行预测
with torch.no_grad():
prediction = model(pred_data)
# 反标准化
dummy = np.zeros((1, config.nodes))
dummy[0, 3:5] = prediction.numpy()
denorm_pred = scaler.inverse_transform(dummy)
print(f'\n预测结果:用户3={denorm_pred[0, 3]:.2f}, 用户4={denorm_pred[0, 4]:.2f}')