AI图神经网络(GNN)完全教程

教程简介

本教程全面讲解图神经网络(GNN)的核心原理与实战应用,涵盖GCN/GAT/GraphSAGE经典模型、消息传递神经网络、图Transformer、知识图谱嵌入(TransE/RotatE)、图分类与图生成、异构图与动态图等核心内容,通过社交网络分析案例帮助开发者掌握GNN技术。

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 包含三个阶段:

  1. 消息计算(Message):\(m_{ij} = \phi(h_i, h_j, e_{ij})\)
  2. 聚合(Aggregate):\(m_i = \bigoplus_{j \in \mathcal{N}(i)} m_{ij}\)
  3. 更新(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 的直觉理解后再逐步深入。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录