【图神经网络】GraphSAGE 无监督训练源码剖析

2023-11-21 21:50

本文主要是介绍【图神经网络】GraphSAGE 无监督训练源码剖析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

概述

本教程主要介绍pytorch_geometric库examples下的graph_sage_unsup.py的源码剖析,主要的关键技术点,包括:

  • 如何实现随机采样的?
  • SAGEConv是如何训练的?

关键问题1,随机采样和采样方向的问题(有向图)

首先要理解的是,采样的过程和特征聚合的过程是相反的,采样的过程,比如,如下图所示,先采样A节点的一阶邻域节点,再根据一阶采样得到的节点进行二阶采样,是一个从左到右的采样过程,而在特征聚合消息传递的时候是先从二阶节点开始聚合,逐步收敛到目标节点A的过程(在关键问题2的训练阶段会谈到这个问题),是一个从右到左的过程。

同时,一个最重要的问题在于,如果图是有向图,那么采样函数是沿着节点A的出边采样,还是沿着入边采样呢?

以上图为例,如果采样是按照节点2的出边进行采样,那么N(2) = {3,5},上面说过消息传递是反向的,所以消息传递的方向是aggr(3,5) = 2,这显然与有向图的指向相反,通常情况下,有向图的指向应该代表了信息流动的方向,所以NeighborSampler.sample函数的采样并不是按照上述所说进行采样的,而是按照节点2的入边采样的,即N(2) = {0,1},这样在消息传递的时候,就是由aggr(0,1) = 2,正好跟有向图的方向一致

代码验证如下:

import torch
import os.path as osp
import torch.nn as nn
import torch.nn.functional as F
from torch_cluster import random_walk
from sklearn.linear_model import LogisticRegressionimport torch_geometric.transforms as T
from torch_geometric.nn import SAGEConv
from torch_geometric.datasets import Planetoid
from torch_geometric.data import NeighborSampler as RawNeighborSampler
from torch_geometric.data import Dataclass NeighborSampler(RawNeighborSampler):def sample(self, batch):batch = torch.tensor(batch)print("batch = ",batch)return super(NeighborSampler, self).sample(batch)edge_index = torch.tensor([[0,1,2,2,5,3],[2,2,5,3,4,4]], dtype=torch.long)
x = torch.tensor([[-1,0,1], [0,-1,1], [1,0,-1],[1,0,-1],[1,0,-1],[-1,0,1],[-1,0,1]], dtype=torch.float)
data = Data(x=x, edge_index=edge_index)train_loader = NeighborSampler(data.edge_index, sizes=[2], batch_size=1,shuffle=True, num_nodes=data.num_nodes)for batch_size,n_id,adjs in train_loader:print("n_id:",n_id)print("adjs:",adjs)batch =  tensor([2])
n_id: tensor([2, 1, 0])
adjs: EdgeIndex(edge_index=tensor([[1, 2],[0, 0]]), e_id=tensor([1, 0]), size=(3, 1))batch =  tensor([4])
n_id: tensor([4, 5, 3])
adjs: EdgeIndex(edge_index=tensor([[1, 2],[0, 0]]), e_id=tensor([4, 5]), size=(3, 1))batch =  tensor([5])
n_id: tensor([5, 2])
adjs: EdgeIndex(edge_index=tensor([[1],[0]]), e_id=tensor([2]), size=(2, 1))batch =  tensor([1])
n_id: tensor([1])
adjs: EdgeIndex(edge_index=tensor([], size=(2, 0), dtype=torch.int64), e_id=tensor([], dtype=torch.int64), size=(1, 1))batch =  tensor([0])
n_id: tensor([0])
adjs: EdgeIndex(edge_index=tensor([], size=(2, 0), dtype=torch.int64), e_id=tensor([], dtype=torch.int64), size=(1, 1))batch =  tensor([6])
n_id: tensor([6])
adjs: EdgeIndex(edge_index=tensor([], size=(2, 0), dtype=torch.int64), e_id=tensor([], dtype=torch.int64), size=(1, 1))batch =  tensor([3])
n_id: tensor([3, 2])
adjs: EdgeIndex(edge_index=tensor([[1],[0]]), e_id=tensor([3]), size=(2, 1))

再者,graphsage_conv要想能够进行无监督训练,还需要构建正负样本,对于图上一批minibatch节点,其邻域节点就是作为其正样本,与该节点不连接的样本点作为负样本,为此源码中构建了一个随机采样函数NeighborSampler,看一下这个函数的实现:

from torch_geometric.data import NeighborSampler as RawNeighborSamplerclass NeighborSampler(RawNeighborSampler):def sample(self, batch): # sample 1batch = torch.tensor(batch)row, col, _ = self.adj_t.coo()# For each node in `batch`, we sample a direct neighbor (as positive# example) and a random node (as negative example):pos_batch = random_walk(row, col, batch, walk_length=1,coalesced=False)[:, 1]neg_batch = torch.randint(0, self.adj_t.size(1), (batch.numel(), ),dtype=torch.long)batch = torch.cat([batch, pos_batch, neg_batch], dim=0)return super(NeighborSampler, self).sample(batch) # sample 2

第一阶段,假设batch样本量为256(即256为一批),pos_batch,通过random_walk,随机游走长度为1,进行采样得到,而neg_batch,是随机在图上进行采样获得,其shape均为256,最后的torch.cat([batch, pos_batch, neg_batch], dim=0)为256*3=768,作为源点;

第二阶段,调用sample函数进行采样,代码中,一阶和二阶中每个节点均从邻居中(如上面所示,会沿着节点入边进行采样)采样10个点,最后形成n_id包含了二阶采样的所有节点索引id,而adjs为列表,存储着一阶和二阶的子图对应的邻接矩阵。

其中,adjs列表元素由EdgeIndex(edge_index, e_id, size)构成:

  • edge_index:每一阶的子图矩阵,source->target形式;
  • e_id:子图边在全graph中的原始索引id;
  • size:tuple形式,(K阶节点数,K-1阶节点数)

举个例子,例如我们的源点batch有768个节点,经过一阶采样后,一阶节点数为1826,二阶采样后节点数为2412,即采样过程中满足:768->1826->2412的变化,而在训练消息传递聚合的时候,是按照2412->1826->768的变化进行训练的。

def sample(self, batch):if not isinstance(batch, Tensor):batch = torch.tensor(batch)batch_size: int = len(batch)adjs = []n_id = batchfor size in self.sizes:adj_t, n_id = self.adj_t.sample_adj(n_id, size, replace=False)e_id = adj_t.storage.value()size = adj_t.sparse_sizes()[::-1]if self.__val__ is not None:adj_t.set_value_(self.__val__[e_id], layout='coo')if self.is_sparse_tensor:adjs.append(Adj(adj_t, e_id, size))else:row, col, _ = adj_t.coo()edge_index = torch.stack([col, row], dim=0)adjs.append(EdgeIndex(edge_index, e_id, size))adjs = adjs[0] if len(adjs) == 1 else adjs[::-1]out = (batch_size, n_id, adjs)out = self.transform(*out) if self.transform is not None else outreturn out

train_loader = NeighborSampler(data.edge_index, sizes=[10, 10], batch_size=256,shuffle=True, num_nodes=data.num_nodes)

注意:需要明白这里面存在两种物理含义上的采样

sample 1,得到的pos_batch和neg_batch是为了用于计算loss所用,即:

pos\_batch = z_v,neg\_batch = z_{vn}

sample 2,是图上的K阶采样,是用于特征聚合学习所用,即下图中的3-7行

关键问题2,sage_conv如何训练

正如上述所说,采样的时候是由向图方向反向实现K阶采样的,这样在训练的时候,特征聚合(消息传递)的过程是沿着图的方向进行的,即从第二层向源点进行消息传递。其中,n_id表示每个batch样本中二阶采样点的索引id(包含节点自身)。整个前向传播的out,分为三个部分,out,pos_out,neg_out,根据论文中的loss计算方式,用于反向传播:

def train():model.train()total_loss = 0for batch_size, n_id, adjs in train_loader:# `adjs` holds a list of `(edge_index, e_id, size)` tuples.adjs = [adj.to(device) for adj in adjs]optimizer.zero_grad()out = model(x[n_id], adjs)out, pos_out, neg_out = out.split(out.size(0) // 3, dim=0)pos_loss = F.logsigmoid((out * pos_out).sum(-1)).mean()neg_loss = F.logsigmoid(-(out * neg_out).sum(-1)).mean()loss = -pos_loss - neg_lossloss.backward()optimizer.step()total_loss += float(loss) * out.size(0)return total_loss / data.num_nodes

下面看一下model整个前向传播的计算。定义的SAGE模型如下,forward的传播过程如下:二阶->一阶->目标节点。

class SAGE(nn.Module):def __init__(self, in_channels, hidden_channels, num_layers):super(SAGE, self).__init__()self.num_layers = num_layersself.convs = nn.ModuleList()for i in range(num_layers):in_channels = in_channels if i == 0 else hidden_channelsself.convs.append(SAGEConv(in_channels, hidden_channels))def forward(self, x, adjs):for i, (edge_index, _, size) in enumerate(adjs):x_target = x[:size[1]]  # Target nodes are always placed first.x = self.convs[i]((x, x_target), edge_index)if i != self.num_layers - 1:x = x.relu()x = F.dropout(x, p=0.5, training=self.training)return xdef full_forward(self, x, edge_index):for i, conv in enumerate(self.convs):x = conv(x, edge_index)if i != self.num_layers - 1:x = x.relu()x = F.dropout(x, p=0.5, training=self.training)return x

torch_geometric源码SAGEConv的定义,重点关注其forward函数,其计算过程是按照如下公式进行计算的:

根据上述公式,结合下面的源码可以看出,self.line_l和self.line_r分别对应这个公式中的

self.line\_l = W_2*mean_{j=N(i)}X_j

self.line\_r = W_1*X_i

class SAGEConv(MessagePassing):r"""The GraphSAGE operator from the `"Inductive Representation Learning onLarge Graphs" <https://arxiv.org/abs/1706.02216>`_ paper.. math::\mathbf{x}^{\prime}_i = \mathbf{W}_1 \mathbf{x}_i + \mathbf{W}_2 \cdot\mathrm{mean}_{j \in \mathcal{N(i)}} \mathbf{x}_jArgs:in_channels (int or tuple): Size of each input sample. A tuplecorresponds to the sizes of source and target dimensionalities.out_channels (int): Size of each output sample.normalize (bool, optional): If set to :obj:`True`, output featureswill be :math:`\ell_2`-normalized, *i.e.*,:math:`\frac{\mathbf{x}^{\prime}_i}{\| \mathbf{x}^{\prime}_i \|_2}`.(default: :obj:`False`)root_weight (bool, optional): If set to :obj:`False`, the layer willnot add transformed root node features to the output.(default: :obj:`True`)bias (bool, optional): If set to :obj:`False`, the layer will not learnan additive bias. (default: :obj:`True`)**kwargs (optional): Additional arguments of:class:`torch_geometric.nn.conv.MessagePassing`."""def __init__(self, in_channels: Union[int, Tuple[int, int]],out_channels: int, normalize: bool = False,root_weight: bool = True,bias: bool = True, **kwargs):  # yapf: disablekwargs.setdefault('aggr', 'mean')super(SAGEConv, self).__init__(**kwargs)self.in_channels = in_channelsself.out_channels = out_channelsself.normalize = normalizeself.root_weight = root_weightif isinstance(in_channels, int):in_channels = (in_channels, in_channels)self.lin_l = Linear(in_channels[0], out_channels, bias=bias)if self.root_weight:self.lin_r = Linear(in_channels[1], out_channels, bias=False)self.reset_parameters()def reset_parameters(self):self.lin_l.reset_parameters()if self.root_weight:self.lin_r.reset_parameters()def forward(self, x: Union[Tensor, OptPairTensor], edge_index: Adj,size: Size = None) -> Tensor:""""""if isinstance(x, Tensor):x: OptPairTensor = (x, x)# propagate_type: (x: OptPairTensor)out = self.propagate(edge_index, x=x, size=size)out = self.lin_l(out)x_r = x[1]if self.root_weight and x_r is not None:out += self.lin_r(x_r)if self.normalize:out = F.normalize(out, p=2., dim=-1)return out

参考链接

论文链接:https://arxiv.org/abs/1706.02216

github链接:https://github.com/rusty1s/pytorch_geometric/blob/master/examples/graph_sage_unsup.py

https://github.com/rusty1s/pytorch_geometric/issues/2816

官方文档:https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html?highlight=SAGEConv#torch_geometric.nn.conv.SAGEConv.forward

知乎链接:https://www.zhihu.com/people/zhang-kang-5-20/posts

这篇关于【图神经网络】GraphSAGE 无监督训练源码剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/405326

相关文章

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

图神经网络模型介绍(1)

我们将图神经网络分为基于谱域的模型和基于空域的模型,并按照发展顺序详解每个类别中的重要模型。 1.1基于谱域的图神经网络         谱域上的图卷积在图学习迈向深度学习的发展历程中起到了关键的作用。本节主要介绍三个具有代表性的谱域图神经网络:谱图卷积网络、切比雪夫网络和图卷积网络。 (1)谱图卷积网络 卷积定理:函数卷积的傅里叶变换是函数傅里叶变换的乘积,即F{f*g}

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页:https://tangyuan96.github.io/minigpt_3d_project_page/ 代码:https://github.com/TangYuan96/MiniGPT-3D 论文:https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA,被ACM MM2024接收,只拥有47.8M的可训练参数,在一张RTX