GraphSAGE 原理:采样邻居与归纳式学习

咱们正式开始啃 GraphSAGE。说实话,我第一次看论文标题的时候还在想:这名字又是 Graph 又是 SAGE,听起来像什么魔法棒?后来做项目调了几个模型才发现,它最大的魔法就藏在两个词里——采样邻居和归纳式学习。

1. 采样邻居:为什么不全拿?

全图邻居计算在工业图上有多贵?我刚开始做推荐系统时,有个用户节点的度超过 10 万。如果每个节点都去聚合所有邻居,一张 1080Ti 显卡瞬间爆显存。所以 GraphSAGE 的做法很粗暴——固定采样。它给每个目标节点随机抽 1 跳、2 跳的邻居,数量固定,比如第一跳 10 个,第二跳 25 个。

为什么要固定大小?你想想看,如果采样数不一致,批处理的时候没法拼成张量。图神经网络的高效训练全靠矩阵运算,形状不对全白搭。我个人习惯把采样数设成 第一跳 5~10,第二跳 15~25,这样既覆盖主要邻居,又不会把内存撑爆。

避坑:我曾经在一个电商图谱上把采样数直接设成 50+50,结果训练时每个 batch 的邻居张量超过 1GB,卡死。后来改成 10+20,效果好多了。采样数不是越大越好,要跟你的显存和 batch size 匹配。

采样的具体逻辑很简单:对每个节点,如果不带替换抽样,邻居不够就补重复;如果带替换抽满。代码实现就像这样:

def sample_neighbors(nodes, adj_list, sample_size):
    sampled = []
    for node in nodes:
        neighbors = adj_list[node]
        if len(neighbors) < sample_size:
            # 重复采样
            chosen = random.choices(neighbors, k=sample_size)
        else:
            chosen = random.sample(neighbors, sample_size)
        sampled.append(chosen)
    return torch.tensor(sampled)

这里有个小细节:采样要在每轮训练时重新做,不是只采样一次。随机性本身就是一种正则化,能防止过拟合。我在一个文档分类项目里试过固定邻居集,AUC 掉了 2%,换成每 epoch 重新采样就回来了。

2. 归纳式学习:从训练集到新节点

传统 GCN 是直推式(Transductive)——训练时整个图都得拿到,新节点出现就要重新训。GraphSAGE 换了个玩法:学一个函数,这个函数输入节点特征和邻居特征,输出节点嵌入。新节点来了,直接拿它的邻居跑一遍函数就行。

说白了,GraphSAGE 学的是「如何聚合」,而不是背「每个节点长什么样」。这带来的好处很明显——

特性 直推式 (GCN) 归纳式 (GraphSAGE)
泛化到新节点 需要重新训练 直接推理
图结构依赖 强依赖全图 仅依赖局部邻居
大规模部署 困难,全图拉不下来 容易,可拆分小 batch
适合场景 静态小图 动态大规模图

我记得之前在社交网络推荐场景里,每天新增上千万用户。如果每次新用户进来都要重训整个模型,那别想上线了。用 GraphSAGE 的归纳式学习,新用户来了直接跑一次聚合函数,嵌入秒出,爽得不行。

3. 聚合函数:一个核心模块

采样完邻居之后,怎么把一堆邻居特征变成一条表示?GraphSAGE 给了三种选择:

  • Mean:邻居特征取平均。简单粗暴,适合对称关系。
  • LSTM:把邻居打乱顺序丢进 LSTM。我试过在小数据集上效果略好,但训练慢不少。
  • Pooling:对每个邻居特征过一层 MLP,再逐元素最大池化。这是我个人用得最多的,效果稳定,速度还快。

如何选?我在项目里一般这么判断:

  • 如果邻居节点特征噪声大,Mean 比较容易把噪声平均掉,适合。
  • 如果想捕捉邻居的不同模式,用 Pooling 能通过 MLP 做变换,再取最大值保留最强信号。
  • 除非数据量不大且对顺序有隐含意义(比如时间序列),否则慎用 LSTM——它计算量太大。
小技巧:实际训练时可以把 Mean 和 Pooling 的embed_dim设成相同,然后做特征拼接。我的经验是这会带来 3~5% 的提升,代价只是增加一倍参数。

好,我们把采样、聚合、归纳式学习串起来,就是下面这张图的核心流程:

GraphSAGE 采样 + 聚合流程 原始节点 v 采样邻居 N₁, N₂, N₃, ... 固定 K 个 聚合函数 Mean / LSTM / Pooling 更新表示 h_v^(k) = σ(W · concat) 归纳式:同一函数适用于新节点

这张图从左到右:先锁定目标节点 v,从它的邻居池里固定抽 K 个;然后把这些邻居的特征喂进聚合函数;最后把聚合结果跟节点自身特征做拼接,过一层非线性,得到更新后的表示。整个过程不依赖全局图,新节点来了照做不误——这就是「归纳」的实质。

核心总结:GraphSAGE 通过「固定大小采样」解决了大规模图的邻居爆炸问题,再用「聚合函数」学出一个可跨节点共享的归纳规则。这两招让它成了工业界做动态图、超大图时的首选地基。