4. 聚合函数深度解析:四种主流方法

图注意力网络之前,先得把聚合函数吃透。我这些年调过的 GraphSAGE 模型不下几十个,踩坑最多的就是这里。四种聚合方式,各有脾气,选对了事半功倍,选错了……嗯,模型怎么训都上不去。

4.1 Mean 聚合 —— 最简单也最稳定

Mean 聚合就是取邻居特征的算术平均。说白了,把你所有邻居的特征向量加在一起,除以邻居数量。代码实现也就几行:

def mean_aggregate(neighbor_features):
    # neighbor_features: [num_neighbors, feat_dim]
    return torch.mean(neighbor_features, dim=0)

我个人习惯把 Mean 聚合当作基准模型的首选。为什么?因为它对邻居数量不敏感,不管你有 3 个邻居还是 300 个,算出来的向量尺度是稳定的。

适用场景:图中节点度分布比较均匀,或者你刚起步做实验想快速验证。Mean 聚合收敛快,梯度稳定,适合做 ablation study 的 baseline。

但要注意,Mean 聚合会把所有邻居"一视同仁"。重要邻居和次要邻居权重一样,这在异质图中会丢失信息。我在处理电商用户-商品图时就遇到过这个问题:用户购买过的商品和浏览过的商品,影响力完全不同,Mean 聚合却把它们混在了一起。

4.2 Pooling 聚合 —— MAX vs MEAN 的对决

Pooling 聚合比 Mean 多了个"变换+压缩"的步骤。它对每个邻居特征先过一个全连接层,再在邻居维度上做 MAX 或 MEAN。

def pooling_aggregate(neighbor_features, W, b):
    # 先变换
    transformed = torch.matmul(neighbor_features, W) + b
    # 再池化
    # MAX pooling
    pooled_max = torch.max(transformed, dim=0)[0]
    # 或者 MEAN pooling
    pooled_mean = torch.mean(transformed, dim=0)
    return pooled_max  # 或 pooled_mean

MAX Pooling 更关注"最显著"的邻居特征。我在社交网络分析中发现,MAX 能捕捉到那些"一鸣惊人"的邻居——比如某个好友突然爆火,MAX Pooling 会放大这个信号。

MEAN Pooling 则更温和,类似加了一层非线性后再平均。它结合了 Mean 的稳定性和全连接层的表达能力。

避坑指南:我曾经在稠密图上用 MAX Pooling,结果梯度爆炸了。原因是邻居太多(上万个),MAX 操作每次都挑最大值,导致梯度方差极大。建议在邻居数超过 500 时谨慎使用 MAX 模式。

4.3 LSTM 聚合 —— 序列的力量

LSTM 聚合很有意思。它把邻居看作一个序列,用 LSTM 模型来编码。但有个问题:邻居天然没有顺序。

def lstm_aggregate(neighbor_features, lstm_model):
    # 先随机打乱邻居顺序
    shuffled = neighbor_features[torch.randperm(len(neighbor_features))]
    # 再送入 LSTM
    lstm_out, (h_n, c_n) = lstm_model(shuffled.unsqueeze(1))
    return h_n.squeeze(0)

你想想看,图结构本身不是序列,强行排序会不会引入偏差?会的。我记得有一次在引文网络数据集上做实验,同样的数据,换一次随机种子,LSTM 聚合的结果就差了好几个点。后来我查了论文才知道,LSTM 聚合需要在训练时多次随机打乱邻居顺序,让模型学到顺序不变性。

关键经验:LSTM 聚合适合邻居之间有隐式依赖的场景。比如论文引用网络,一篇论文引用的文献list本身就有时间顺序,这时 LSTM 能捕获到"早期文献打基础、近期文献追热点"这种演变模式。

代价呢?计算量是四者中最大的。batch 里每个节点都要跑一遍 LSTM,邻居数稍大,显存直接爆炸。我建议只用在邻居数小于 50 的小图上。

4.4 Transformer 聚合 —— 注意力机制的降维打击

到了 Transformer 聚合,整个玩法就升级了。它用自注意力机制来聚合邻居,每个邻居的权重由它与目标节点以及其他邻居的相似度共同决定。

def transformer_aggregate(target_feat, neighbor_feats, W_q, W_k, W_v):
    # target_feat: [feat_dim]
    # neighbor_feats: [num_neighbors, feat_dim]
    q = torch.matmul(target_feat, W_q).unsqueeze(0)  # [1, feat_dim]
    k = torch.matmul(neighbor_feats, W_k)             # [N, feat_dim]
    v = torch.matmul(neighbor_feats, W_v)             # [N, feat_dim]
    attn_scores = torch.matmul(q, k.T) / sqrt(feat_dim)
    attn_weights = torch.softmax(attn_scores, dim=1)
    out = torch.matmul(attn_weights, v)
    return out.squeeze(0)

我建议用 Transformer 聚合的场景是:异质性强、邻居类型差异大的图。比方说知识图谱,一个实体可能同时连接"人物"、"地点"、"事件"三种邻居,自注意力能自动给它们分配不同权重。

优势对比:Mean 聚合给所有邻居 1/N 的固定权重;LSTM 聚合依赖随机排序;Transformer 聚合的权重由数据本身学习得到,理论上信息损失最小。

但说实话,Transformer 聚合的落地难度最大。显存开销跟邻居数的平方成正比,工业界大图基本扛不住。我做过实验:邻居数 1000 时,单层 Transformer 聚合的显存消耗是 Mean 聚合的 40 倍。

4.5 四大聚合函数终极对比

聚合方式 表达能力 计算开销 邻居顺序敏感性 适用图规模
Mean 聚合 低 低 (O(N)) 不敏感 超大图(百万级节点)
MAX Pooling 中 低 (O(N)) 不敏感 中大图
MEAN Pooling 中 低 (O(N)) 不敏感 中大图
LSTM 聚合 高 高 (O(N)) 高度敏感(需 shuffle) 小图(邻居 < 50)
Transformer 聚合 最高 极高 (O(N²)) 不敏感 小图(邻居 < 200)

我的经验是:工业级项目先用 Mean 或 MAX Pooling 跑通全流程,再根据业务场景决定是否上 LSTM 或 Transformer。90% 的情况下,你把 Pooling 的 hidden size 调大一倍,效果就逼近 Transformer 了,成本却低得多。

一句话总结:选聚合函数就是选"表达力 vs 效率"的平衡点。没有银弹,只有权衡。

GraphSAGE 四种聚合函数流程对比 邻居特征集合 Mean 聚合 Pooling 聚合 LSTM 聚合 Transformer 聚合 聚合结果(节点嵌入) 说明: 四种聚合函数接收相同的邻居特征集合,通过不同的策略生成节点嵌入。 Mean 计算平均;Pooling 先变换后极值/平均;LSTM 依赖序列顺序;Transformer 利用自注意力分配权重。

这四种聚合方式,从简单到复杂,从高效到高表达,覆盖了 GraphSAGE 家族的全部变种。我个人建议:如果你刚开始接触图神经网络,先死磕 Mean 和 MAX Pooling,把基础打牢。等你发现 Mean 聚合不足以区分邻居重要性时,再升级到 LSTM 或 Transformer 也不迟。