AI图神经网络(GNN)完全教程
1. 图数据与图神经网络概述
现实世界中大量数据天然具有图结构:社交网络中的人际关系、分子中的化学键、推荐系统中的用户-物品交互、交通网络中的路段连接。传统的深度学习架构(CNN、RNN)擅长处理欧几里得空间数据(如序列、网格),但在非欧几里得的图结构数据上表现受限。
图神经网络(Graph Neural Network, GNN)通过在图的节点和边上定义神经网络操作,实现了对图结构数据的端到端学习。核心思想是消息传递(Message Passing):每个节点聚合其邻居节点的信息,不断更新自身的表示。
一个图 \(G = (V, E)\) 由节点集合 \(V\) 和边集合 \(E\) 组成。邻接矩阵 \(A \in \mathbb{R}^{N \times N}\) 描述节点之间的连接关系,节点特征矩阵 \(X \in \mathbb{R}^{N \times d}\) 描述每个节点的 \(d\) 维特征。
import torch
import torch.nn.functional as F
# 构建一个简单的图:4个节点,5条边
# 边的定义:(源节点, 目标节点)
edge_index = torch.tensor([
[0, 1, 1, 2, 3], # 源节点
[1, 0, 2, 1, 1] # 目标节点
], dtype=torch.long)
# 节点特征:4个节点,每个3维特征
x = torch.tensor([
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 1.0, 0.0]
], dtype=torch.float)
print(f"节点数: {x.size(0)}, 特征维度: {x.size(1)}")
print(f"边数: {edge_index.size(1)}")
2. 图卷积网络(GCN)原理与实现
图卷积网络(Graph Convolutional Network, GCN)由 Kipf 等人在 2017 年提出,是 GNN 最经典的基础模型。GCN 的核心公式为:
\(H^{(l+1)} = \sigma(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(l)} W^{(l)})\)
其中 \(\tilde{A} = A + I\) 是添加自环的邻接矩阵,\(\tilde{D}\) 是对应的度矩阵,\(W^{(l)}\) 是可学习的权重矩阵,\(\sigma\) 是激活函数。
这个公式的直觉是:每个节点的新特征 = 自身和邻居特征的加权平均(归一化后)再经过线性变换和激活函数。
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class GCNLayer(nn.Module):
"""单层图卷积"""
def __init__(self, in_features, out_features):
super().__init__()
self.weight = nn.Parameter(torch.FloatTensor(in_features, out_features))
self.bias = nn.Parameter(torch.FloatTensor(out_features))
nn.init.xavier_uniform_(self.weight)
nn.init.zeros_(self.bias)
def forward(self, x, adj):
# adj 已经是归一化后的邻接矩阵 (D^{-1/2} A D^{-1/2})
support = torch.mm(x, self.weight)
output = torch.spmm(adj, support) + self.bias
return output
def normalize_adjacency(adj):
"""计算归一化的邻接矩阵 D^{-1/2} (A+I) D^{-1/2}"""
adj = adj + torch.eye(adj.size(0)) # 添加自环
degree = adj.sum(dim=1)
d_inv_sqrt = torch.pow(degree, -0.5)
d_inv_sqrt[torch.isinf(d_inv_sqrt)] = 0.0
d_mat = torch.diag(d_inv_sqrt)
return torch.mm(torch.mm(d_mat, adj), d_mat)
class GCN(nn.Module):
"""两层GCN用于节点分类"""
def __init__(self, n_input, n_hidden, n_output):
super().__init__()
self.gc1 = GCNLayer(n_input, n_hidden)
self.gc2 = GCNLayer(n_hidden, n_output)
def forward(self, x, adj):
x = F.relu(self.gc1(x, adj))
x = F.dropout(x, training=self.training)
x = self.gc2(x, adj)
return F.log_softmax(x, dim=1)
# 构建示例图
adj = torch.tensor([
[0, 1, 0, 0],
[1, 0, 1, 1],
[0, 1, 0, 0],
[0, 1, 0, 0]
], dtype=torch.float)
norm_adj = normalize_adjacency(adj)
x = torch.randn(4, 16) # 4个节点,16维特征
model = GCN(n_input=16, n_hidden=32, n_output=3)
output = model(x, norm_adj)
print(f"输出形状: {output.shape}") # torch.Size([4, 3])
GCN 的局限性在于:它使用固定的归一化权重(基于节点度数),无法学习不同邻居的重要性差异。这引出了后续的图注意力网络。
3. 图注意力网络(GAT)
图注意力网络(Graph Attention Network, GAT)通过引入注意力机制,让每个节点自适应地学习其邻居的重要性权重。GAT 的注意力系数计算如下:
\(\alpha_{ij} = \frac{\exp(\text{LeakyReLU}(a^T [Wh_i \| Wh_j]))}{\sum_{k \in \mathcal{N}(i)} \exp(\text{LeakyReLU}(a^T [Wh_i \| Wh_k]))}\)
其中 \(\|\) 表示拼接操作,\(a\) 是可学习的注意力向量。
class GATLayer(nn.Module):
"""图注意力层"""
def __init__(self, in_features, out_features, n_heads=4, concat=True):
super().__init__()
self.n_heads = n_heads
self.concat = concat
self.out_features = out_features
self.W = nn.Parameter(torch.FloatTensor(in_features, out_features * n_heads))
self.a = nn.Parameter(torch.FloatTensor(2 * out_features, n_heads))
self.leaky_relu = nn.LeakyReLU(0.2)
nn.init.xavier_uniform_(self.W)
nn.init.xavier_uniform_(self.a)
def forward(self, x, adj):
N = x.size(0)
h = torch.mm(x, self.W).view(N, self.n_heads, self.out_features)
# 计算注意力系数
h_src = h.repeat(1, N, 1).view(N, N, self.n_heads, self.out_features)
h_dst = h.repeat(N, 1, 1).view(N, N, self.n_heads, self.out_features)
a_input = torch.cat([h_src, h_dst], dim=-1) # [N, N, heads, 2*out]
e = (a_input * self.a).sum(dim=-1) # [N, N, heads]
e = self.leaky_relu(e)
# 用邻接矩阵遮罩非邻居
mask = adj.unsqueeze(-1).expand_as(e)
e = e.masked_fill(mask == 0, float('-inf'))
attention = F.softmax(e, dim=1) # [N, N, heads]
# 聚合
h_prime = torch.einsum('ijh,jhf->ihf', attention, h)
if self.concat:
return h_prime.view(N, -1) # [N, heads * out_features]
else:
return h_prime.mean(dim=1) # [N, out_features]
class GAT(nn.Module):
"""多头注意力GAT"""
def __init__(self, n_input, n_hidden, n_output, n_heads=4):
super().__init__()
self.gat1 = GATLayer(n_input, n_hidden, n_heads, concat=True)
self.gat2 = GATLayer(n_hidden * n_heads, n_output, 1, concat=False)
def forward(self, x, adj):
x = F.elu(self.gat1(x, adj))
x = F.dropout(x, training=self.training)
x = self.gat2(x, adj)
return F.log_softmax(x, dim=1)
# 测试
model = GAT(n_input=16, n_hidden=8, n_output=3, n_heads=4)
output = model(x, adj)
print(f"GAT输出形状: {output.shape}") # torch.Size([4, 3])
GAT 的优势在于能够为不同的邻居分配不同的重要性权重,在异质性图(邻居标签差异大)上表现尤其出色。
4. GraphSAGE 与归纳学习
GCN 和 GAT 属于转导学习(Transductive Learning),需要在训练时看到完整的图结构。GraphSAGE(SAmple and aggrEGatE)提出了一种归纳学习(Inductive Learning)框架,可以泛化到训练时未见过的新节点。
GraphSAGE 的核心是采样 + 聚合策略:对每个节点,先采样固定数量的邻居,然后通过聚合函数(均值、LSTM、池化)生成邻居表示,最后与自身特征拼接后做变换。
class SAGELayer(nn.Module):
"""GraphSAGE聚合层"""
def __init__(self, in_features, out_features, aggr='mean'):
super().__init__()
self.aggr = aggr
self.linear = nn.Linear(in_features * 2, out_features)
def forward(self, x, neighbors):
"""
x: [N, in_features] 节点特征
neighbors: list of lists, 每个节点的邻居索引
"""
N = x.size(0)
neighbor_feats = []
for i in range(N):
if len(neighbors[i]) > 0:
neigh = x[neighbors[i]]
if self.aggr == 'mean':
agg = neigh.mean(dim=0)
elif self.aggr == 'max':
agg = neigh.max(dim=0)[0]
else:
agg = neigh.mean(dim=0)
else:
agg = torch.zeros(x.size(1))
neighbor_feats.append(agg)
neighbor_feats = torch.stack(neighbor_feats)
combined = torch.cat([x, neighbor_feats], dim=1)
return F.relu(self.linear(combined))
class GraphSAGE(nn.Module):
def __init__(self, n_input, n_hidden, n_output):
super().__init__()
self.sage1 = SAGELayer(n_input, n_hidden)
self.sage2 = SAGELayer(n_hidden, n_output)
def forward(self, x, neighbors):
x = F.relu(self.sage1(x, neighbors))
x = F.dropout(x, training=self.training)
x = self.sage2(x, neighbors)
return F.log_softmax(x, dim=1)
# 示例:定义邻居列表
neighbors = [
[1], # 节点0的邻居
[0, 2, 3], # 节点1的邻居
[1], # 节点2的邻居
[1] # 节点3的邻居
]
model = GraphSAGE(n_input=16, n_hidden=32, n_output=3)
output = model(x, neighbors)
print(f"GraphSAGE输出: {output.shape}")
GraphSAGE 特别适合大规模动态图场景,比如社交网络中新用户的推荐。
5. 消息传递神经网络(MPNN)
消息传递神经网络(Message Passing Neural Network, MPNN)是 Gilmer 等人在 2017 年提出的统一框架,将多种 GNN 变体统一为消息传递范式。MPNN 包含三个阶段:
- 消息计算(Message):\(m_{ij} = \phi(h_i, h_j, e_{ij})\)
- 聚合(Aggregate):\(m_i = \bigoplus_{j \in \mathcal{N}(i)} m_{ij}\)
- 更新(Update):\(h_i' = \psi(h_i, m_i)\)
class MPNNLayer(nn.Module):
"""消息传递神经网络层"""
def __init__(self, in_features, out_features, edge_features=0):
super().__init__()
msg_input = in_features * 2 + edge_features
self.message_fn = nn.Sequential(
nn.Linear(msg_input, out_features),
nn.ReLU(),
nn.Linear(out_features, out_features)
)
self.update_fn = nn.GRUCell(out_features, in_features)
self.aggr = 'add' # 聚合方式:add / mean / max
def forward(self, x, edge_index, edge_attr=None):
src, dst = edge_index
# 消息计算
if edge_attr is not None:
msg_input = torch.cat([x[src], x[dst], edge_attr], dim=1)
else:
msg_input = torch.cat([x[src], x[dst]], dim=1)
messages = self.message_fn(msg_input)
# 聚合
agg_messages = torch.zeros_like(x[:, :messages.size(1)])
agg_messages.index_add_(0, dst, messages)
# 更新
output = self.update_fn(agg_messages, x)
return output
# 测试带边特征的MPNN
edge_attr = torch.randn(5, 4) # 5条边,每条4维特征
mpnn = MPNNLayer(in_features=16, out_features=16, edge_features=4)
out = mpnn(x, edge_index, edge_attr)
print(f"MPNN输出: {out.shape}")
GCN、GAT、GraphSAGE 等都可以看作 MPNN 的特例,只是在消息函数、聚合方式和更新函数上有不同的设计选择。
6. 图 Transformer
图 Transformer 将 Transformer 的自注意力机制引入图结构数据。与 GAT 不同,图 Transformer 可以考虑节点之间的全局关系,而不局限于局部邻居。
关键创新在于位置编码(Positional Encoding):由于图没有天然的顺序,需要通过拉普拉斯特征向量、随机游走概率等方法为节点注入结构位置信息。
class GraphTransformerLayer(nn.Module):
def __init__(self, d_model, n_heads, dropout=0.1):
super().__init__()
self.attention = nn.MultiheadAttention(d_model, n_heads, dropout=dropout, batch_first=True)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_model * 4, d_model),
nn.Dropout(dropout)
)
def forward(self, x, attn_mask=None):
# x: [N, d_model] 或 [B, N, d_model]
if x.dim() == 2:
x = x.unsqueeze(0)
# 自注意力 + 残差
h = self.norm1(x)
h, _ = self.attention(h, h, h, attn_mask=attn_mask)
x = x + h
# FFN + 残差
x = x + self.ffn(self.norm2(x))
return x.squeeze(0)
def laplacian_positional_encoding(adj, k=8):
"""基于拉普拉斯矩阵的位置编码"""
# 计算归一化拉普拉斯矩阵
degree = adj.sum(dim=1)
D_inv_sqrt = torch.diag(torch.pow(degree, -0.5))
D_inv_sqrt[torch.isinf(D_inv_sqrt)] = 0
L = torch.eye(adj.size(0)) - D_inv_sqrt @ adj @ D_inv_sqrt
# 特征分解
eigenvalues, eigenvectors = torch.linalg.eigh(L)
# 取最小的k个非零特征值对应的特征向量
return eigenvectors[:, 1:k+1]
# 测试
pe = laplacian_positional_encoding(adj, k=8)
x_with_pe = torch.cat([x, pe], dim=1) # 拼接位置编码
gt_layer = GraphTransformerLayer(d_model=24, n_heads=4)
output = gt_layer(x_with_pe)
print(f"图Transformer输出: {output.shape}")
图 Transformer 在分子属性预测、蛋白质结构分析等任务上展现了强大的全局建模能力。
7. 知识图谱嵌入(TransE/RotatE)
知识图谱以三元组 \((h, r, t)\) 的形式存储事实(头实体、关系、尾实体)。知识图谱嵌入的目标是将实体和关系映射到低维向量空间,使得合理的三元组获得更高的得分。
TransE 的思想简洁而优雅:\(\mathbf{h} + \mathbf{r} \approx \mathbf{t}\)。即头实体向量加上关系向量应该接近尾实体向量。得分函数为 \(s(h, r, t) = -\|\mathbf{h} + \mathbf{r} - \mathbf{t}\|\)。
RotatE 进一步将关系建模为复数空间中的旋转:\(\mathbf{t} = \mathbf{h} \circ \mathbf{r}\),其中 \(\circ\) 是逐元素复数乘法。
class TransE(nn.Module):
def __init__(self, n_entities, n_relations, embedding_dim, margin=1.0):
super().__init__()
self.entity_emb = nn.Embedding(n_entities, embedding_dim)
self.relation_emb = nn.Embedding(n_relations, embedding_dim)
self.margin = margin
nn.init.xavier_uniform_(self.entity_emb.weight)
nn.init.xavier_uniform_(self.relation_emb.weight)
# 关系向量归一化
with torch.no_grad():
self.relation_emb.weight.data = F.normalize(
self.relation_emb.weight.data, p=2, dim=1
)
def score(self, h, r, t):
h_emb = self.entity_emb(h)
r_emb = self.relation_emb(r)
t_emb = self.entity_emb(t)
return -torch.norm(h_emb + r_emb - t_emb, p=2, dim=1)
def loss(self, pos_score, neg_score):
return F.relu(self.margin + neg_score - pos_score).mean()
class RotatE(nn.Module):
def __init__(self, n_entities, n_relations, embedding_dim, margin=1.0):
super().__init__()
self.entity_emb = nn.Embedding(n_entities, embedding_dim)
self.relation_emb = nn.Embedding(n_relations, embedding_dim // 2) # 复数维度
self.margin = margin
nn.init.xavier_uniform_(self.entity_emb.weight)
def score(self, h, r, t):
h_emb = self.entity_emb(h)
t_emb = self.entity_emb(t)
r_phase = self.relation_emb(r)
# 将关系转换为复数旋转
r_real = torch.cos(r_phase)
r_imag = torch.sin(r_phase)
# 拆分实体为实部和虚部
h_real, h_imag = h_emb.chunk(2, dim=1)
t_real, t_imag = t_emb.chunk(2, dim=1)
# 复数乘法:h * r
hr_real = h_real * r_real - h_imag * r_imag
hr_imag = h_real * r_imag + h_imag * r_real
# 距离
diff_real = hr_real - t_real
diff_imag = hr_imag - t_imag
score = -(diff_real ** 2 + diff_imag ** 2).sum(dim=1).sqrt()
return score
def loss(self, pos_score, neg_score):
return F.relu(self.margin + neg_score - pos_score).mean()
# 测试TransE
n_entities, n_relations, dim = 1000, 50, 128
transe = TransE(n_entities, n_relations, dim)
h = torch.randint(0, n_entities, (32,))
r = torch.randint(0, n_relations, (32,))
t = torch.randint(0, n_entities, (32,))
pos_score = transe.score(h, r, t)
neg_t = torch.randint(0, n_entities, (32,))
neg_score = transe.score(h, r, neg_t)
loss = transe.loss(pos_score, neg_score)
print(f"TransE loss: {loss.item():.4f}")
8. 图分类与图生成
图分类任务需要将整个图映射为一个固定大小的向量表示。常用方法包括:
- 全局池化(Global Pooling):对所有节点表示做 sum/mean/max 池化
- 层次化池化(Hierarchical Pooling):如 DiffPool,学习节点的软聚类分配
class GraphClassifier(nn.Module):
"""基于GCN和全局池化的图分类器"""
def __init__(self, n_input, n_hidden, n_classes, n_layers=3):
super().__init__()
self.layers = nn.ModuleList()
self.layers.append(GCNLayer(n_input, n_hidden))
for _ in range(n_layers - 2):
self.layers.append(GCNLayer(n_hidden, n_hidden))
self.layers.append(GCNLayer(n_hidden, n_hidden))
self.classifier = nn.Linear(n_hidden, n_classes)
def forward(self, x, adj, batch=None):
for layer in self.layers:
x = F.relu(layer(x, adj))
# 全局均值池化
if batch is not None:
# batch标记每个节点属于哪个图
from torch_scatter import scatter_mean
graph_emb = scatter_mean(x, batch, dim=0)
else:
graph_emb = x.mean(dim=0, keepdim=True)
return self.classifier(graph_emb)
# 图生成:使用VAE框架
class GraphVAE(nn.Module):
"""图变分自编码器(简化版)"""
def __init__(self, n_nodes, latent_dim):
super().__init__()
self.n_nodes = n_nodes
# 编码器:将邻接矩阵编码为潜在向量
self.encoder = nn.Sequential(
nn.Linear(n_nodes * n_nodes, 256),
nn.ReLU(),
nn.Linear(256, 128)
)
self.fc_mu = nn.Linear(128, latent_dim)
self.fc_logvar = nn.Linear(128, latent_dim)
# 解码器:从潜在向量生成邻接矩阵
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 128),
nn.ReLU(),
nn.Linear(128, n_nodes * n_nodes),
nn.Sigmoid()
)
def encode(self, adj_flat):
h = self.encoder(adj_flat)
return self.fc_mu(h), self.fc_logvar(h)
def reparameterize(self, mu, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
def decode(self, z):
return self.decoder(z).view(-1, self.n_nodes, self.n_nodes)
def forward(self, adj):
adj_flat = adj.view(-1)
mu, logvar = self.encode(adj_flat)
z = self.reparameterize(mu, logvar)
adj_recon = self.decode(z)
return adj_recon, mu, logvar
vae = GraphVAE(n_nodes=10, latent_dim=16)
adj_sample = torch.rand(10, 10)
adj_sample = (adj_sample + adj_sample.T) / 2 # 对称化
recon, mu, logvar = vae(adj_sample)
print(f"重建邻接矩阵: {recon.shape}")
9. 异构图与动态图
异构图(Heterogeneous Graph)包含多种类型的节点和边。例如学术网络中有论文(Paper)、作者(Author)、会议(Venue)三种节点,以及"撰写"、"发表于"等不同类型的关系。
class HeteroGNN(nn.Module):
"""异构图神经网络(简化版)"""
def __init__(self, metapaths, in_dim, hidden_dim, out_dim):
"""
metapaths: dict, {relation_type: (src_type, dst_type)}
"""
super().__init__()
self.relation_layers = nn.ModuleDict({
rel: GCNLayer(in_dim, hidden_dim) for rel in metapaths
})
self.attention = nn.Parameter(torch.FloatTensor(len(metapaths)))
nn.init.uniform_(self.attention)
self.output_layer = nn.Linear(hidden_dim, out_dim)
def forward(self, x, adj_dict):
"""
adj_dict: {relation_type: adjacency_matrix}
"""
h_list = []
for rel, adj in adj_dict.items():
h_list.append(F.relu(self.relation_layers[rel](x, adj)))
# 基于注意力的跨关系聚合
h_stack = torch.stack(h_list, dim=0) # [n_relations, N, hidden]
attn_weights = F.softmax(self.attention, dim=0)
h = (h_stack * attn_weights.view(-1, 1, 1)).sum(dim=0)
return self.output_layer(h)
# 动态图:时间感知的消息传递
class TemporalGNN(nn.Module):
"""时间感知的GNN"""
def __init__(self, in_dim, hidden_dim, time_dim=16):
super().__init__()
self.time_enc = nn.Linear(1, time_dim)
self.msg_fn = nn.Linear(in_dim * 2 + time_dim, hidden_dim)
self.update_fn = nn.GRUCell(hidden_dim, in_dim)
def forward(self, x, edge_index, timestamps):
src, dst = edge_index
time_feat = self.time_enc(timestamps.unsqueeze(1))
msg_input = torch.cat([x[src], x[dst], time_feat], dim=1)
messages = F.relu(self.msg_fn(msg_input))
# 聚合(使用时间衰减权重)
agg = torch.zeros(x.size(0), messages.size(1))
for i in range(len(src)):
agg[dst[i]] += messages[i]
return self.update_fn(agg, x)
10. 实战案例:社交网络分析与推荐
以社交网络中的用户兴趣预测为例,展示完整的 GNN 工作流程:
import torch
import torch.nn.functional as F
import torch.nn as nn
class SocialGNN(nn.Module):
"""社交网络GNN推荐模型"""
def __init__(self, n_users, n_items, n_topics, embed_dim=64, hidden_dim=128):
super().__init__()
self.user_emb = nn.Embedding(n_users, embed_dim)
self.item_emb = nn.Embedding(n_items, embed_dim)
# 用户-用户社交关系GNN
self.social_gcn1 = GCNLayer(embed_dim, hidden_dim)
self.social_gcn2 = GCNLayer(hidden_dim, embed_dim)
# 用户-物品交互GNN
self.interaction_gcn1 = GCNLayer(embed_dim, hidden_dim)
self.interaction_gcn2 = GCNLayer(hidden_dim, embed_dim)
# 主题预测头
self.topic_predictor = nn.Sequential(
nn.Linear(embed_dim * 2, hidden_dim),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(hidden_dim, n_topics)
)
def forward(self, user_ids, social_adj, interaction_adj):
# 社交关系编码
user_feat = self.user_emb.weight
social_h = F.relu(self.social_gcn1(user_feat, social_adj))
social_h = self.social_gcn2(social_h, social_adj)
# 交互关系编码(用户-物品二部图)
all_feat = torch.cat([user_feat, self.item_emb.weight], dim=0)
inter_h = F.relu(self.interaction_gcn1(all_feat, interaction_adj))
inter_h = self.interaction_gcn2(inter_h, interaction_adj)
# 融合两种表示
user_social = social_h[user_ids]
user_inter = inter_h[user_ids]
user_final = torch.cat([user_social, user_inter], dim=1)
return self.topic_predictor(user_final)
# 模拟数据
n_users, n_items, n_topics = 100, 50, 20
model = SocialGNN(n_users, n_items, n_topics)
# 模拟邻接矩阵
social_adj = torch.rand(n_users, n_users)
social_adj = ((social_adj + social_adj.T) / 2 > 0.8).float()
total_nodes = n_users + n_items
interaction_adj = torch.zeros(total_nodes, total_nodes)
# 随机添加一些交互边
for _ in range(200):
u = torch.randint(0, n_users, (1,)).item()
i = torch.randint(n_users, total_nodes, (1,)).item()
interaction_adj[u, i] = 1.0
interaction_adj[i, u] = 1.0
user_ids = torch.arange(10)
logits = model(user_ids, social_adj, interaction_adj)
print(f"用户兴趣预测: {logits.shape}") # [10, 20]
11. GNN 框架(PyG / DGL)
目前主流的 GNN 框架有两个:PyTorch Geometric(PyG) 和 DGL(Deep Graph Library)。
PyTorch Geometric(PyG)
PyG 是 PyTorch 生态下最流行的 GNN 库,提供了丰富的预定义层和数据加载器。
# PyG 风格的代码示例(需要安装 torch_geometric)
from torch_geometric.data import Data
from torch_geometric.nn import GCNConv, GATConv, SAGEConv
import torch
# 构建图数据
edge_index = torch.tensor([[0, 1, 1, 2], [1, 0, 2, 1]], dtype=torch.long)
x = torch.randn(3, 16)
y = torch.tensor([0, 1, 0])
data = Data(x=x, edge_index=edge_index, y=y)
class PyG_GCN(torch.nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels):
super().__init__()
self.conv1 = GCNConv(in_channels, hidden_channels)
self.conv2 = GCNConv(hidden_channels, out_channels)
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.conv1(x, edge_index).relu()
x = F.dropout(x, p=0.5, training=self.training)
x = self.conv2(x, edge_index)
return F.log_softmax(x, dim=1)
# 使用PyG的内置数据集
# from torch_geometric.datasets import Planetoid
# dataset = Planetoid(root='./data', name='Cora')
# model = PyG_GCN(dataset.num_features, 64, dataset.num_classes)
DGL(Deep Graph Library)
DGL 由 AWS 开发,在分布式训练和大规模图处理上有优势。
# DGL 风格的代码示例(需要安装 dgl)
import dgl
import torch
import torch.nn as nn
import torch.nn.functional as F
from dgl.nn import GraphConv
# 构建DGL图
g = dgl.graph(([0, 1, 1, 2], [1, 0, 2, 1]))
g = dgl.add_self_loop(g)
g.ndata['feat'] = torch.randn(3, 16)
class DGL_GCN(nn.Module):
def __init__(self, in_feats, h_feats, num_classes):
super().__init__()
self.conv1 = GraphConv(in_feats, h_feats)
self.conv2 = GraphConv(h_feats, num_classes)
def forward(self, g, inputs):
h = self.conv1(g, inputs)
h = F.relu(h)
h = self.conv2(g, h)
return h
# 框架对比:
# PyG: 更Pythonic,社区活跃,适合研究和中小规模图
# DGL: 后端灵活(支持PyTorch/TF/MXNet),大规模分布式,工业级应用
框架选择建议
| 特性 | PyG | DGL |
|---|---|---|
| 学习曲线 | 低,API 直觉 | 中等 |
| 模型库 | 丰富,覆盖前沿论文 | 丰富,工业示例多 |
| 大规模支持 | 一般 | 优秀(分布式) |
| 适合场景 | 学术研究、快速原型 | 工业部署、大规模图 |
选择建议:如果做研究或中小规模项目,优先用 PyG;如果需要处理十亿级节点的工业级图,选择 DGL。
以上覆盖了图神经网络从基础到进阶的核心内容。GNN 的发展仍在快速推进,图 Transformer、大规模预训练图模型(如 GraphGPT)、以及 GNN 与大语言模型的结合是当前的热门方向。建议动手实践 Cora、Citeseer 等经典数据集上的节点分类任务,建立对 GNN 的直觉理解后再逐步深入。