图神经网络GraphSAGE代码详解

图神经网络GraphSAGE代码详解

1. 前言

最近在学习图神经网络相关知识,对于直推式的图神经网络,训练代价昂贵,这篇文章主要是介绍一个基于归纳学习的框架GraphSAGE的代码,旨在训练一个聚合函数,为看不见的节点(新的节点)生成嵌入。因为自己也是小白,写这篇文章的目的也是为了增强自己对该算法的理解和记忆,由于下载下来的代码没有注释,我会尽可能的加上足够清晰的注释,方便大家阅读,如有错误,望神仙网友给予批评指正!!!

2. 代码下载

该代码是从github上下载而来,使用pytorch框架的一个简易版的GraphSAGE算法,适合小白入手学习。
代码下载链接:https://pan.baidu.com/s/1WW0mkHXupl6kkyyzOG9pBA
提取码:v06v

3. 数据集分析

代码中提供了两种数据集,cora数据集和pubmed数据集,主要针对cora数据集进行分析。
Cora数据集中样本是机器学习论文,论文被分为7类:

  1. Case_Based
  2. Genetic_Algorithms
  3. Neural_Networks
  4. Probabilistic_Methods
  5. Reinforcement_Learning
  6. Rule_Learning
  7. Theory

数据集共有2708篇论文,分为两个文件:

  1. cora.cites
  2. cora.content

第一个文件cora.cites文件格式:

<paper_id> <word_attributes>+ <class_label>
<paper_id> :论文的ID(或者说图中节点的ID编号)
<word_attributes>:节点的特征向量(0-1编码)
<class_label>:节点类别

第一个文件cora.cites文件格式:

<ID of cited paper> <ID of citing paper>
<ID of cited paper>:被引用的论文ID
<ID of citing paper>:引用论文的ID
我们可以把它看作是图中两个节点ID的边

4. 代码分析

主要有三个代码文件:aggregators.py、encoders.py、model.py
aggregators.py:用于聚合邻居节点的特征,返回的就是聚合后的邻居节点的特征。
encoders.py:根据aggregators得到的邻居节点特征执行卷积操作
model.py:代码的主文件,加载数据集以及训练代码等操作

4. 1 model.py

首先是一个用于创建GraphSage的SupervisedGraphSage类。


```python
class SupervisedGraphSage(nn.Module):

    def __init__(self, num_classes, enc):
        super(SupervisedGraphSage, self).__init__()
        self.enc = enc
        self.xent = nn.CrossEntropyLoss()
		# num_classes:节点类别数量
		# enc:执行卷积操作的encoder类,embed_dim是输出维度的大小
        self.weight = nn.Parameter(torch.FloatTensor(num_classes, enc.embed_dim))
        init.xavier_uniform(self.weight)

    def forward(self, nodes):
        embeds = self.enc(nodes)
        scores = self.weight.mm(embeds)
        return scores.t()

    def loss(self, nodes, labels):
        scores = self.forward(nodes)
        return self.xent(scores, labels.squeeze())

上面的代码比较简单,下面看一下加载数据集的代码模块。

def load_cora():
    num_nodes = 2708   #节点数量
    num_feats = 1433   #节点特征维度
    #创建一个特征矩阵,维度大小[2708,1433]
    feat_data = np.zeros((num_nodes, num_feats))
    #创建一个类别矩阵,相当于y_train的意思,维度大小[2708,1]
    labels = np.empty((num_nodes,1), dtype=np.int64)
    #节点的map映射 节点ID -> 索引
    node_map = {}
    #节点的列别标签映射 节点类别 -> 节点的类别对应的index
    label_map = {}
    with open("D:\workspace\pythonSpace\pythonProject\\nlp\graphNetwork\graphsage-simple-master\cora\cora.content") as fp:
        for i,line in enumerate(fp):
        	'''把文件中的每一行读出来,info含有三部分:
        	info[0]:节点的编号ID
        	info[1:-1]:节点对应的特征 -> 1433维
        	info[-1]:节点对应的类别
        	'''
            info = line.strip().split()
            #把加载进来的节点特征维度赋给特征矩阵feat_data
            feat_data[i,:] = list(map(float, info[1:-1]))
            #构造节点编号映射,{节点编号:索引号}
            node_map[info[0]] = i
            if not info[-1] in label_map:
            #构造节点类别映射,len(label_map)的值域[0,6],对应七种论文的类别。
                label_map[info[-1]] = len(label_map)
            #把该论文类别对应的标签[0,6]存储在labels列表中
            labels[i] = label_map[info[-1]]
	#创建一个空的邻接矩阵集合
    adj_lists = defaultdict(set)
    with open("D:\workspace\pythonSpace\pythonProject\\nlp\graphNetwork\graphsage-simple-master\cora\cora.cites") as fp:
        for i,line in enumerate(fp):
        	'''
        	info有两部分组成
        	info[0]:被引用论文的ID
        	info[1]:引用论文的ID        	'''
            info = line.strip().split()
            #拿到这两个节点对应的索引
            paper1 = node_map[info[0]]
            paper2 = node_map[info[1]]
            #在邻接矩阵中互相添加相邻节点ID
            adj_lists[paper1].add(paper2)
            adj_lists[paper2].add(paper1)
    return feat_data, labels, adj_lists

下面看一下训练函数

def run_cora():
	#设置随机种子
    np.random.seed(1)
    random.seed(1)
    num_nodes = 2708
    #加载数据集
    feat_data, labels, adj_lists = load_cora()
    #随机生成[2708,1433]维度的特征向量
    features = nn.Embedding(2708, 1433)
    #使用特征维度[2708,1433]的feat_data数据替换features中的特征向量值
    features.weight = nn.Parameter(torch.FloatTensor(feat_data), requires_grad=False)
   # features.cuda()
	#创建一个两层的graphsage网络,MeanAggregator和Encoder下面会单独解释
    agg1 = MeanAggregator(features, cuda=True)
    enc1 = Encoder(features, 1433, 128, adj_lists, agg1, gcn=True, cuda=False)
    agg2 = MeanAggregator(lambda nodes : enc1(nodes).t(), cuda=False)
    enc2 = Encoder(lambda nodes : enc1(nodes).t(), enc1.embed_dim, 128, adj_lists, agg2,
            base_model=enc1, gcn=True, cuda=False)
    #设置两层网络各自的邻居节点采样数
    enc1.num_samples = 5
    enc2.num_samples = 5
	
    graphsage = SupervisedGraphSage(7, enc2)
#    graphsage.cuda()
	#打乱节点的顺序,生成一个乱序的1-2708的列表
    rand_indices = np.random.permutation(num_nodes)
    #划分测试集和训练集
    test = rand_indices[:1000]
    val = rand_indices[1000:1500]
    train = list(rand_indices[1500:])
	#创建优化器对象
    optimizer = torch.optim.SGD(filter(lambda p : p.requires_grad, graphsage.parameters()), lr=0.7)
    times = []
    #迭代循环训练
    for batch in range(100):
    	#挑选256个节点为一个批次进行训练
        batch_nodes = train[:256]
        #打乱训练集节点的顺序,方便下一次训练选取节点
        random.shuffle(train)
        start_time = time.time()
        #梯度清零
        optimizer.zero_grad()
        #计算损失
        loss = graphsage.loss(batch_nodes, 
                Variable(torch.LongTensor(labels[np.array(batch_nodes)])))
        #执行反向传播
        loss.backward()
        #更新参数
        optimizer.step()
        end_time = time.time()
        times.append(end_time-start_time)
        #打印每一轮的损失值
        print( batch, loss.data)
	#查看验证集
    val_output = graphsage.forward(val) 
    print( "Validation F1:", f1_score(labels[val], val_output.data.numpy().argmax(axis=1), average="micro"))
    print ("Average batch time:", np.mean(times))

4. 2 aggregators.py

下面我们看一下GraphSAGE如何获得邻居节点的特征。

class MeanAggregator(nn.Module):
    """
    Aggregates a node's embeddings using mean of neighbors' embeddings
    """
    def __init__(self, features, cuda=False, gcn=False): 
        """
        Initializes the aggregator for a specific graph.

        features -- function mapping LongTensor of node ids to FloatTensor of feature values.
        cuda -- whether to use GPU
        gcn --- whether to perform concatenation GraphSAGE-style, or add self-loops GCN-style
        """

        super(MeanAggregator, self).__init__()

        self.features = features
        self.cuda = cuda
        #是否使用GCN模式的均值聚合方式,详细请看GraphSAGE的聚合方式。
        self.gcn = gcn
        
    def forward(self, nodes, to_neighs, num_sample=10):
        """
        nodes --- 一个批次的节点编号
        to_neighs --- 每个节点对应的邻居节点编号集合
        num_sample --- 每个节点对邻居的采样数量
        """
        # Local pointers to functions (speed hack)
        _set = set
        if not num_sample is None:
            _sample = random.sample
            #如果邻居节点数目大于num_sample,就随机选取num_sample个节点,否则选取仅有的邻居节点编号即可。
            samp_neighs = [_set(_sample(to_neigh, 
                            num_sample,
                            )) if len(to_neigh) >= num_sample else to_neigh for to_neigh in to_neighs]
        else:
            samp_neighs = to_neighs

        if self.gcn:
        	#聚合邻居节点信息时加上自己本身节点的信息
            samp_neighs = [samp_neigh + set([nodes[i]]) for i, samp_neigh in enumerate(samp_neighs)]
        #把一个批次内的所有节点的邻居节点编号聚集在一块并去重
        unique_nodes_list = list(set.union(*samp_neighs))
        #为所有的邻居节点建立一个索引映射
        unique_nodes = {n:i for i,n in enumerate(unique_nodes_list)}
        #创建mask的目的,其实就是为了创建一个邻接矩阵
        #该临界矩阵的维度为[一个批次的节点数,一个批次内所有邻居节点的总数目]
        mask = Variable(torch.zeros(len(samp_neighs), len(unique_nodes)))
        #所有邻居节点的列索引
        column_indices = [unique_nodes[n] for samp_neigh in samp_neighs for n in samp_neigh]   
        #所有邻居节点的行索引
        row_indices = [i for i in range(len(samp_neighs)) for j in range(len(samp_neighs[i]))]
        #将对应行列索引的节点值赋为1,就构成了邻接矩阵
        mask[row_indices, column_indices] = 1
        if self.cuda:
            mask = mask.cuda()
        #每个节点对应的邻居节点的数据
        num_neigh = mask.sum(1, keepdim=True)
        #除以对应的邻居节点个数,求均值
        mask = mask.div(num_neigh)
        # if self.cuda:
        #     embed_matrix = self.features(torch.LongTensor(unique_nodes_list).cuda())
        # else:
        #得到unique_nodes_list列表中各个邻居节点的特征
        #embed_matrix 的维度[一个批次内所有邻居节点的总数目,1433]
        embed_matrix = self.features(torch.LongTensor(unique_nodes_list))
        #用邻接矩阵乘上所有邻居节点的特征矩阵,就得到了聚合邻居节点后的各个节点的特征矩阵
        to_feats = mask.mm(embed_matrix)
        return to_feats

4. 3 encoders.py

得到聚合了邻居节点的特征向量之后,执行卷积的操作如下:

class Encoder(nn.Module):
    """
    Encodes a node's using 'convolutional' GraphSage approach
    """
    def __init__(self, features, feature_dim, 
            embed_dim, adj_lists, aggregator,
            num_sample=10,
            base_model=None, gcn=False, cuda=False, 
            feature_transform=False): 
        super(Encoder, self).__init__()
		#特征矩阵信息
        self.features = features
        #特征矩阵的向量维度 
        self.feat_dim = feature_dim
        #每个节点对应的邻居节点的编码集合,例如[1:{2,3,4},2:{1,5,6}]
        self.adj_lists = adj_lists
        self.aggregator = aggregator
        self.num_sample = num_sample
        if base_model != None:
            self.base_model = base_model

        self.gcn = gcn
        #输出向量的维度
        self.embed_dim = embed_dim
        self.cuda = cuda
        self.aggregator.cuda = cuda
        #执行卷积的参数矩阵,如果不使用GCN模式,需要执行一个concat拼接操作,所以向量维度为2倍的feat_dim
        self.weight = nn.Parameter(
                torch.FloatTensor(embed_dim, self.feat_dim if self.gcn else 2 * self.feat_dim))
        init.xavier_uniform(self.weight)

    def forward(self, nodes):
        """
        Generates embeddings for a batch of nodes.

        nodes     -- list of nodes
        """
        #获得聚合了邻居节点后的节点特征信息
        neigh_feats = self.aggregator.forward(nodes, [self.adj_lists[int(node)] for node in nodes], 
                self.num_sample)
        if not self.gcn:
            if self.cuda:
                self_feats = self.features(torch.LongTensor(nodes).cuda())
            else:
            	#获得这一个批次的节点本身的特征信息
                self_feats = self.features(torch.LongTensor(nodes))
            #将节点本身的特征信息和邻居节点的特征信息拼接一起
            combined = torch.cat([self_feats, neigh_feats], dim=1)
        else:
        	#使用GCN的均值聚合方式,直接使用聚合了本身信息的邻居节点信息即可
            combined = neigh_feats
        #线性转换后再经过一个relu激活函数,得到最终的聚合结果
        combined = F.relu(self.weight.mm(combined.t()))
        return combined

5 总结

以上就是实现了均值MeanAggregator的GraphSAGE的算法,我尽可能多的为每一行代码加上了注释,如有错误,望批评指正。
除了上面的均值聚合方式,还有LSTM、池化聚合方式,还有无监督的GraphSAGE训练方式,如果有机会,争取在后面学习之后再写一篇博文分享出来。

上一篇:赫夫曼编码解压缩


下一篇:【CCF CSP】【AC】201912-2:回收站选址(C++版)(只用到了结构体和数组)