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 和 MAX Pooling,把基础打牢。等你发现 Mean 聚合不足以区分邻居重要性时,再升级到 LSTM 或 Transformer 也不迟。