超大图上的节点表征学习
传统方法的问题
- 随着图神经网络层数增加,计算成本呈指数增长;
- 保存整个图的信息和每一层每个节点的表征到内存(显存)而消耗巨大内存(显存)空间;
- 无需保存整个图信息和每一层每个节点表征的方法,可能会损失预测精度或者对内存利用率提高不明显。
Cluster-GCN方法
论文提出的方法
- 利用图节点聚类算法将一个图的节点划分为c个簇,每一次选择几个簇的节点和这些节点对应的边构成一个子图,然后对子图做训练。
- 由于是利用图节点聚类算法将节点划分为多个簇,所以簇内边的数量要比簇间边的数量多得多,所以可以提高表征利用率,并提高图神经网络的训练效率。
- 每一次随机选择多个簇来组成一个batch,这样不会丢失簇间的边,同时也不会有batch内类别分布偏差过大的问题。
- 基于小图进行训练,不会消耗很多内存空间,于是我们可以训练更深的神经网络,进而可以达到更高的精度。
-
传统方法:
空间复杂度:同时计算所有节点的表征以及训练集中所有节点的损失产生的梯度。需要巨大的计算开销和内存开销。
时间复杂度:神经网络在每个epoch中只更新一次,所以训练需要更多的epoch才能达到收敛。 -
mini-batch SGD方法:
空间复杂度:采用mini-batch SGD的方式训练,可以提高图神经网络的训练速度并减少内存(显存)需求。
时间复杂度:尽管在epoches数量相同的情况下,采用SGD方式进行训练,收敛速度可以更快,但此种训练方式会引入额外的时间开销,这使得相比于全梯度下降的训练方式,此种训练方式每个epoch的时间开销要大得多。 -
邻域扩展 ——> 节点表征的利用率
-
简单的Cluster-GCN方法:
将节点划分为c个簇,对应c个子图;接下来我们用块对角线邻接矩阵 A ˉ \bar{A} Aˉ去近似邻接矩阵 A A A(这样做的好处是,完整的损失函数可以根据batch分解成多个部分之和);
时间空间复杂度
-
随机分区:
应对的问题:图被分割后,一些边被移除,性能可能因此会受到影响。图聚类算法倾向于将相似的节点聚集在一起。因此,单个簇中节点的类别分布可能与原始数据集不同,导致对梯度的估计有偏差。
为了解决上述问题,Cluster-GCN论文提出了一种随机多簇方法,此方法首先将图划分为 p p p个簇, V 1 , ⋯ , V p \mathcal{V}_{1}, \cdots, \mathcal{V}_{p} V1,⋯,Vp, p p p是一个较大的值,在构建一个batch时,不是只使用一个簇,而是使用随机选择的 q q q个簇,表示为 t 1 , … , t q t_{1}, \ldots, t_{q} t1,…,tq,得到的batch包含节点 { V t 1 ∪ ⋯ ∪ V t q } \left\{\mathcal{V}_{t_{1}} \cup \cdots \cup \mathcal{V}_{t_{q}}\right\} {Vt1∪⋯∪Vtq} 、簇内边 { A i i ∣ i ∈ t 1 , … , t q } \left\{A_{i i} \mid i \in t_{1}, \ldots, t_{q}\right\} {Aii∣i∈t1,…,tq}和簇间边 { A i j ∣ i , j ∈ t 1 , … , t q } \left\{A_{i j} \mid i, j \in t_{1}, \ldots, t_{q}\right\} {Aij∣i,j∈t1,…,tq}。
此方法的好处有,1)不会丢失簇间的边,2)不会有很大的batch内类别分布的偏差,3)以及不同的epoch使用的batch不同,这可以降低梯度估计的偏差。
-
整体算法流程
Cluster-GCN实践
# 数据集分析
from torch_geometric.datasets import Reddit
from torch_geometric.data import ClusterData, ClusterLoader, NeighborSampler
dataset = Reddit('../dataset/Reddit')
data = dataset[0]
print(dataset.num_classes)
print(data.num_nodes)
print(data.num_edges)
print(data.num_features)
# 41
# 232965
# 114615873
# 602
# 图节点聚类与数据加载器生成
cluster_data = ClusterData(data, num_parts=1500, recursive=False, save_dir=dataset.processed_dir)
train_loader = ClusterLoader(cluster_data, batch_size=20, shuffle=True, num_workers=12)
subgraph_loader = NeighborSampler(data.edge_index, sizes=[-1], batch_size=1024, shuffle=False, num_workers=12)
# 图神经网络的构建
class Net(torch.nn.Module):
def __init__(self, in_channels, out_channels):
super(Net, self).__init__()
self.convs = ModuleList(
[SAGEConv(in_channels, 128),
SAGEConv(128, out_channels)])
def forward(self, x, edge_index):
for i, conv in enumerate(self.convs):
x = conv(x, edge_index)
if i != len(self.convs) - 1:
x = F.relu(x)
x = F.dropout(x, p=0.5, training=self.training)
return F.log_softmax(x, dim=-1)
def inference(self, x_all):
pbar = tqdm(total=x_all.size(0) * len(self.convs))
pbar.set_description('Evaluating')
# Compute representations of nodes layer by layer, using *all*
# available edges. This leads to faster computation in contrast to
# immediately computing the final representations of each batch.
for i, conv in enumerate(self.convs):
xs = []
for batch_size, n_id, adj in subgraph_loader:
edge_index, _, size = adj.to(device)
x = x_all[n_id].to(device)
x_target = x[:size[1]]
x = conv((x, x_target), edge_index)
if i != len(self.convs) - 1:
x = F.relu(x)
xs.append(x.cpu())
pbar.update(batch_size)
x_all = torch.cat(xs, dim=0)
pbar.close()
return x_all
# 训练、验证与测试
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = Net(dataset.num_features, dataset.num_classes).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)
def train():
model.train()
total_loss = total_nodes = 0
for batch in train_loader:
batch = batch.to(device)
optimizer.zero_grad()
out = model(batch.x, batch.edge_index)
loss = F.nll_loss(out[batch.train_mask], batch.y[batch.train_mask])
loss.backward()
optimizer.step()
nodes = batch.train_mask.sum().item()
total_loss += loss.item() * nodes
total_nodes += nodes
return total_loss / total_nodes
@torch.no_grad()
def test(): # Inference should be performed on the full graph.
model.eval()
out = model.inference(data.x)
y_pred = out.argmax(dim=-1)
accs = []
for mask in [data.train_mask, data.val_mask, data.test_mask]:
correct = y_pred[mask].eq(data.y[mask]).sum().item()
accs.append(correct / mask.sum().item())
return accs
for epoch in range(1, 31):
loss = train()
if epoch % 5 == 0:
train_acc, val_acc, test_acc = test()
print(f'Epoch: {epoch:02d}, Loss: {loss:.4f}, Train: {train_acc:.4f}, '
f'Val: {val_acc:.4f}, test: {test_acc:.4f}')
else:
print(f'Epoch: {epoch:02d}, Loss: {loss:.4f}')
参考资料:
https://github.com/datawhalechina/team-learning-nlp/tree