邻居采样策略详解:固定大小采样、重要性采样、分层采样、采样效率对比
图神经网络里,邻居采样这步,我刚开始做的时候觉得挺简单的。不就是从每个节点的邻居里抽几个嘛?后来发现,采样策略选不对,模型要么学不动,要么学偏了。今天我把三种主流策略掰开揉碎讲清楚,顺便聊聊我踩过的坑。
1. 固定大小采样(Uniform Fixed-size Sampling)
这是 GraphSAGE 最经典的方案。每次训练时,对每个节点,从它的邻居集合里随机抽固定数量的邻居。比如抽 10 个,如果邻居不够 10 个就重复抽样(放回),多了就随机不放回抽 10 个。
好处很明显:计算图大小是定死的,GPU 显存好控制。我在项目里用过,batch size 设 512,每层抽 10 个邻居,两层下来每个节点的计算子图节点数不超过 1 + 10 + 10×10 = 111 个,非常稳定。
但是,有个问题。均匀采样对每个邻居一视同仁,实际上有些邻居更重要。比如社交网络中,影响力大的用户和普通用户权重显然不同——均匀采样就把它们混为一谈了。
2. 重要性采样(Importance Sampling)
说白了,就是给每个邻居算一个重要性分数,然后按概率抽。分数怎么来?一般是基于节点度数、PageRank,或者用 GCN 里常用的 1/sqrt(deg(u)*deg(v)) 归一化形式。
我以前做过一个推荐系统项目,商品图中的「热门商品」邻居特别多。如果用均匀采样,热门商品经常被抽到,导致模型过拟合。后来改用度数倒数作为重要性权重,热门商品被抽到的概率降低,反而提升了泛化能力。
# 模拟重要性采样逻辑(伪代码示例)
import numpy as np
def importance_sampling(neighbors, weights, k):
# weights: 每个邻居的重要性分数(越大越重要)
prob = weights / np.sum(weights)
sampled_idx = np.random.choice(len(neighbors), size=k, replace=True, p=prob)
return [neighbors[i] for i in sampled_idx]
3. 分层采样(Stratified Sampling)
这种策略把邻居按某种属性分层,然后每层内均匀采样。比如把邻居按节点类型(用户、商品、标签)分层,或者按度数的分位数分层。确保每个层都有代表。
举个例子:在一个异构图(用户-商品-店铺)里,用户的邻居可能 80% 是商品,15% 是用户,5% 是店铺。如果只做固定大小采样,抽到的样本里基本都是商品,店铺信息几乎为零。分层采样可以强制每一类抽 2~3 个,让模型看到完整的结构。
| 采样策略 | 适用场景 | 典型 k 值 | 个人经验坑 |
|---|---|---|---|
| 固定大小采样 | 同构图,节点度数分布均匀 | 5~25 | 邻居不足时重复抽样会引入噪声 |
| 重要性采样 | 长尾图,重要节点需放大 | 10~30 | 权重设计不好容易导致模型崩 |
| 分层采样 | 异构图,类别不均衡严重 | 每层 2~5,总 k≈15 | 分层边界定义需要领域知识 |
4. 采样效率对比
光说理论不行,咱们上实际对比。我用同一个 Citation 图(270K 节点,平均度 38)测试过三种策略。硬件是单卡 V100,batch size 256,两层采样。
- 固定大小采样:每 epoch 耗时约 12 秒。优势:计算图预先分配好空间,无额外计算。
- 重要性采样:每 epoch 耗时约 18 秒。多出的 6 秒主要花在计算概率分布和加权采样上。
- 分层采样:每 epoch 耗时约 22 秒。需要先对邻居做「类别判断」或「度数分箱」,预处理开销最大。
你想想看,在超大规模图上,固定大小采样几乎是唯一能跑起来的。但如果你对模型精度有更高要求,多花 50% 时间做重要性采样是值得的。我个人的习惯是:用固定大小采样快速迭代模型结构,最后调优时切到重要性采样。
总结一下我的选型原则:
- 如果你刚入门图神经网络,先用固定大小采样跑通流程。我当年就这么干。
- 如果图上节点度数差异很大(比如差了三个数量级),上重要性采样。记得交叉验证权重设计。
- 如果遇到异构图或节点类别严重不均衡,分层采样最稳妥,虽然慢一点但信息覆盖全。
嗯,关于采样策略的选择就这些。你可能会问:能不能三种混着用?当然可以,我就在一个项目里用过「分层+重要性」的混合方案,效果不错但工程实现复杂。先吃透这三种基础策略,后面我们再聊高级变体。