图神经网络实战:从理论到应用


图神经网络实战:从理论到应用

引言

图神经网络(Graph Neural Networks, GNN)是处理图结构数据的深度学习模型。从社交网络分析到分子性质预测,从推荐系统到知识图谱,GNN正在改变我们处理关系数据的方式。本文将深入讲解GNN的核心原理、主流算法和实际应用。

一、图数据结构基础

1.1 图的数学表示

G = (V, E) V = {v1, v2, ..., vn} # 节点集合 E = {(vi, vj)} # 边集合 # 邻接矩阵 A[i][j] = 1 if (vi, vj) ∈ E else 0 # 节点特征矩阵 X ∈ R^(n×d) # n个节点,每个节点d维特征

1.2 图的类型

  • 同构图(Homogeneous Graph):节点和边类型单一
  • 异构图(Heterogeneous Graph):多种节点和边类型
  • 有向图 vs 无向图
  • 动态图 vs 静态图

二、图卷积网络(GCN)

2.1 核心思想

节点特征聚合:每个节点通过聚合邻居节点信息来更新自己的表示。

2.2 GCN层公式

H^(l+1) = σ(D^(-1/2) A D^(-1/2) H^(l) W^(l)) H^(l): 第l层的节点特征矩阵 A: 邻接矩阵 D: 度矩阵 W^(l): 可学习权重矩阵 σ: 激活函数

2.3 代码实现

import torch import torch.nn as nn import torch.nn.functional as F class GCNLayer(nn.Module): def __init__(self, in_features, out_features): super(GCNLayer, self).__init__() self.weight = nn.Parameter(torch.FloatTensor(in_features, out_features)) self.reset_parameters() def reset_parameters(self): nn.init.kaiming_uniform_(self.weight) def forward(self, x, adj): # x: [N, in_features] # adj: [N, N] 邻接矩阵 aggregate = torch.mm(adj, x) # 聚合邻居信息 output = torch.mm(aggregate, self.weight) # 线性变换 return output class GCN(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim, num_layers): super(GCN, self).__init__() self.layers = nn.ModuleList() self.layers.append(GCNLayer(input_dim, hidden_dim)) for _ in range(num_layers - 2): self.layers.append(GCNLayer(hidden_dim, hidden_dim)) self.layers.append(GCNLayer(hidden_dim, output_dim)) def forward(self, x, adj): for i, layer in enumerate(self.layers): x = layer(x, adj) if i < len(self.layers) - 1: x = F.relu(x) return x # 使用示例 # 假设有10个节点,每个节点5维特征 num_nodes = 10 input_dim = 5 hidden_dim = 16 output_dim = 3 # 节点特征 x = torch.randn(num_nodes, input_dim) # 邻接矩阵(无向图) adj = torch.tensor([ [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 0, 1, 0, 0, 0, 0, 0, 0], [0, 1, 1, 0, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 0, 1, 1, 0, 0], [0, 0, 0, 0, 1, 1, 0, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 0, 1, 1], [0, 0, 0, 0, 0, 0, 1, 1, 0, 1], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0] ], dtype=torch.float) # 创建模型 model = GCN(input_dim, hidden_dim, output_dim, num_layers=3) # 前向传播 output = model(x, adj) print(output.shape) # torch.Size([10, 3])

三、图注意力网络(GAT)

3.1 注意力机制

不同邻居节点的重要性不同。

class GATLayer(nn.Module): def __init__(self, in_features, out_features, num_heads=4): super(GATLayer, self).__init__() self.num_heads = num_heads self.out_features = out_features # 每个头有自己的变换矩阵 self.W = nn.Parameter(torch.FloatTensor(num_heads, in_features, out_features)) self.a = nn.Parameter(torch.FloatTensor(num_heads, 2 * out_features, 1)) self.reset_parameters() def reset_parameters(self): nn.init.kaiming_uniform_(self.W) nn.init.kaiming_uniform_(self.a) def forward(self, x, adj): # x: [N, in_features] batch_size, N = x.size(0), x.size(1) out_features = self.out_features # 多头注意力 outputs = [] for head in range(self.num_heads): # 线性变换 h = torch.mm(x, self.W[head]) # [N, out_features] # 计算注意力分数 a_input = self._prepare_attentional_mechanism_input(h) e = torch.matmul(a_input, self.a[head]).squeeze(2) # [N, N] e = F.leaky_relu(e) # Mask掉不存在的边 zero_vec = -9e15 * torch.ones_like(e) attention = torch.where(adj > 0, e, zero_vec) attention = F.softmax(attention, dim=1) # 聚合邻居信息 h_prime = torch.matmul(attention, h) outputs.append(h_prime) # 拼接所有头 output = torch.cat(outputs, dim=1) return output def _prepare_attentional_mechanism_input(self, h): N = h.size(0) a_input = torch.cat([h.repeat(1, N).view(N * N, -1), h.repeat(N, 1)], dim=1) return a_input.view(N, N, -1)

四、应用案例

4.1 引文网络分类

# Cora数据集:论文分类 from torch_geometric.datasets import Planetoid from torch_geometric.nn import GCNConv # 加载数据 dataset = Planetoid(root='/tmp/Cora', name='Cora') data = dataset[0] # GCN模型 class GCN(torch.nn.Module): def __init__(self): super().__init__() self.conv1 = GCNConv(dataset.num_features, 16) self.conv2 = GCNConv(16, dataset.num_classes) def forward(self, data): x, edge_index = data.x, data.edge_index x = self.conv1(x, edge_index) x = F.relu(x) x = F.dropout(x, training=self.training) x = self.conv2(x, edge_index) return F.log_softmax(x, dim=1) # 训练 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = GCN().to(device) data = data.to(device) optimizer = torch.optim.Adam(model.parameters(), lr=0.01) model.train() for epoch in range(200): optimizer.zero_grad() out = model(data) loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask]) loss.backward() optimizer.step() # 测试 model.eval() pred = model(data).argmax(dim=1) acc = (pred[data.test_mask] == data.y[data.test_mask]).float().mean() print(f'Accuracy: {acc:.4f}')

4.2 推荐系统

# 基于GNN的协同过滤 class GraphRecommender: def __init__(self, num_users, num_items, embedding_dim=64): self.num_users = num_users self.num_items = num_items self.embedding_dim = embedding_dim # 用户和物品的嵌入 self.user_embedding = nn.Embedding(num_users, embedding_dim) self.item_embedding = nn.Embedding(num_items, embedding_dim) # GCN层 self.gcn = GCN(embedding_dim, embedding_dim, embedding_dim) # 构建用户-物品交互图 self.build_graph() def build_graph(self): """构建用户-物品二部图""" # 边: (user_i, item_j) 表示用户i与物品j有交互 edges = [] for user_id in range(self.num_users): for item_id in self.user_interactions[user_id]: edges.append((user_id, self.num_users + item_id)) edges.append((self.num_users + item_id, user_id)) # 无向图 self.edge_index = torch.tensor(edges, dtype=torch.long).t() def forward(self, user_id): # 获取用户嵌入 user_emb = self.user_embedding(user_id) # GCN传播 node_embeddings = self.gcn(all_embeddings, self.edge_index) # 预测用户对所有物品的评分 user_emb = node_embeddings[user_id] item_embeddings = node_embeddings[self.num_users:] scores = torch.matmul(user_emb, item_embeddings.t()) return scores

4.3 分子性质预测

# 分子图:节点=原子,边=化学键 from rdkit import Chem def mol_to_graph(mol): """将分子转换为图结构""" # 节点特征(原子属性) node_features = [] for atom in mol.GetAtoms(): features = [ atom.GetAtomicNum(), # 原子序数 atom.GetDegree(), # 度 atom.GetFormalCharge(), # 形式电荷 atom.GetHybridization(), # 杂化 ] node_features.append(features) # 边(化学键) edge_indices = [] edge_features = [] for bond in mol.GetBonds(): i = bond.GetBeginAtomIdx() j = bond.GetEndAtomIdx() edge_indices.append([i, j]) edge_indices.append([j, i]) # 键类型(单键、双键、三键、芳香键) bond_type = bond.GetBondType() edge_features.append([bond_type]) edge_features.append([bond_type]) return { 'node_features': torch.tensor(node_features), 'edge_index': torch.tensor(edge_indices).t(), 'edge_features': torch.tensor(edge_features) } # 分子性质预测模型 class MoleculeGNN(nn.Module): def __init__(self, node_dim, edge_dim, hidden_dim, output_dim): super().__init__() self.conv1 = GCNConv(node_dim, hidden_dim) self.conv2 = GCNConv(hidden_dim, hidden_dim) self.fc = nn.Linear(hidden_dim, output_dim) def forward(self, graph): x, edge_index = graph['node_features'], graph['edge_index'] x = F.relu(self.conv1(x, edge_index)) x = F.relu(self.conv2(x, edge_index)) # 图级别预测(全局池化) x = torch.mean(x, dim=0) out = self.fc(x) return out

五、高级技巧

5.1 图池化(Graph Pooling)

将图级别信息压缩为向量表示。

# 全局平均池化 def global_mean_pool(x, batch): """x: [N, F], batch: [N] 指示每个节点属于哪个图""" batch_size = batch.max().item() + 1 out = torch.zeros(batch_size, x.size(1)) for i in range(batch_size): out[i] = x[batch == i].mean(dim=0) return out # 最大池化 def global_max_pool(x, batch): batch_size = batch.max().item() + 1 out = torch.zeros(batch_size, x.size(1)) for i in range(batch_size): out[i] = x[batch == i].max(dim=0)[0] return out # 注意力池化 class AttentionPooling(nn.Module): def __init__(self, input_dim): super().__init__() self.attention = nn.Sequential( nn.Linear(input_dim, input_dim // 2), nn.Tanh(), nn.Linear(input_dim // 2, 1) ) def forward(self, x, batch): attention_scores = self.attention(x) attention_scores = F.softmax(attention_scores, dim=0) batch_size = batch.max().item() + 1 out = torch.zeros(batch_size, x.size(1)) for i in range(batch_size): mask = (batch == i) out[i] = (x[mask] * attention_scores[mask]).sum(dim=0) return out

5.2 邻居采样

处理大规模图的技巧。

from torch_geometric.loader import NeighborSampler # 邻居采样 train_loader = NeighborSampler( data.edge_index, size=[10, 10], # 每层采样10个邻居 num_hops=2, # 2层GNN batch_size=1024, shuffle=True ) for batch_size, n_id, adjs in train_loader: # n_id: 采样的节点ID # adjs: 每层的邻接信息 x = data.x[n_id] for i, (edge_index, _, size) in enumerate(adjs): x = gnn_layers[i](x, edge_index) loss = criterion(x, data.y[n_id[:batch_size]])

六、GNN框架推荐

PyTorch Geometric

from torch_geometric.nn import GATConv, SAGEConv # GAT模型 class GAT(torch.nn.Module): def __init__(self): super().__init__() self.conv1 = GATConv(dataset.num_features, 8, heads=8, dropout=0.6) self.conv2 = GATConv(8 * 8, dataset.num_classes, heads=1, concat=False, dropout=0.6) def forward(self, data): x, edge_index = data.x, data.edge_index x = F.dropout(x, p=0.6, training=self.training) x = self.conv1(x, edge_index) x = F.elu(x) x = F.dropout(x, p=0.6, training=self.training) x = self.conv2(x, edge_index) return F.log_softmax(x, dim=1)

DGL (Deep Graph Library)

import dgl import dgl.nn.pytorch as dglnn class DGL_GCN(nn.Module): def __init__(self, in_feats, hidden_size, num_classes): super().__init__() self.conv1 = dglnn.GraphConv(in_feats, hidden_size) self.conv2 = dglnn.GraphConv(hidden_size, num_classes) def forward(self, g, features): x = F.relu(self.conv1(g, features)) x = self.conv2(g, x) return x # 创建DGL图 g = dgl.graph((edge_index[0], edge_index[1])) g.ndata['feat'] = features # 训练 logits = model(g, g.ndata['feat']) loss = F.cross_entropy(logits[train_mask], labels[train_mask])

总结

图神经网络是处理关系数据的强大工具:

  1. 入门路径:GCN → GAT → GraphSAGE
  2. 应用领域:社交网络、生物信息、推荐系统、知识图谱
  3. 实战建议
    • 从PyTorch Geometric开始
    • 先在小图上验证想法
    • 大图使用邻居采样
  4. 进阶方向:异构图、动态图、自监督学习

扩展资源

  • 经典论文:《Semi-Supervised Classification with Graph Convolutional Networks》
  • 书籍:《Graph Representation Learning》
  • 课程:斯坦福CS224W(Machine Learning with Graphs)
  • 库:PyTorch Geometric、DGL、DeepMind Jraph

作者与出处
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天学者_XYT5OR的小龙虾 转发
评论区 (0)
U