Graphsage Note


layout: post title: GAT categories: MachineLearning tags: —

代码地址:https://github.com/williamleif/GraphSAGE

数据集介绍

以protein-protein interaction(PPI)数据集为例。

该数据集共有24张图,其中20张作为训练,2张作为验证,2张作为测试,每张图对应不同的人体组织,该数据是为了从系统的角度研究疾病分子机制、发现新药靶点等等。平均每张图有2372个结点,每个结点特征长度为50,其中包含位置基因集,基序集和免疫学特征。基因本体集作为labels(总共121个),labels不是one-hot编码,所以这是一个多分类问题。

代码中的数据集并不完整,包含3张训练图,1张验证图,1张测试图,总共包含14755个节点,每个节点的特征为50。

example_data 目录下有该数据集,文件说明如下:

toy-ppi-G.json 描述图结构 每个节点有一个val和test属性表示属于验证集还是测试集。 toy-ppi-id_map.json 对图节点进行编码 toy-ppi-class_map.json 每个节点的类别label,一个节点可能属于多个类 toy-ppi-feats.npy 预训练好的节点embedding,顺序和id_map一致。 toy-ppi-walks.txt 图上的随机游走序列,每行有两个节点,表示从第一个节点游走到第二个节点。只用在无监督任务中。

日志

–base_log_dir 指定了日志目录,默认是当期目录。有监督模型输出F1得分,无监督模型输出节点的embedding,存储在npy格式文件val.npy 中,同时,使用val.txt 指定embedding的顺序,一行一个节点id。

运行

有两个shell文件:

example_supervised.sh 和example_unsupervised.sh

无监督版本学习图中节点的embedding,有监督是对节点进行分类?

以example_unsupervised.sh 为例:

python -m graphsage.unsupervised_train --train_prefix ./example_data/toy-ppi --model graphsage_mean --max_total_steps 1000 --validate_iter 10

该命令中的train_prefix做了修改,原始代码里面是./example_data/ppi

unsupervised_train.py

load_data

加载数据,数据文件在上面简单说过了。 id_map.json是一个map结构,key是节点在图中的编号, value是新的编号 class_map.json也是一个map结构,key是节点在图中的编号,value是label向量。

加载完各个文件之后做了预处理。

1.对于图中的所有边,如果边的起点或者终点在验证集或者测试集中,则把边的train_removed属性置为True,表示不能用于训练。否则,置为False。

2.如果需要则对特征进行归一化

使用训练集中的所有节点特征计算均值方差,然后对图中所有节点进行Z-Score归一化。

图中的节点都被会被标注为属于训练集、验证集还是测试集。 图中的节点的特征向量会被归一化。

train

首先,构建以边为单位的minbatch。

minbatch的构建是在EdgeMinibatchIterator类中实现的。

下面介绍该类。

首先,构建训练集的临接矩阵和出度列表,测试集的临接矩阵

两个数据集的临接矩阵都包含了图中所有的节点。

下面介绍训练集的临接矩阵的构造过程

临接矩阵的维度是[图中节点数量+1, 最大出度],使用节点数量作为临接矩阵的初始值。

行加1是为啥?

每个节点采样的邻居数量为”最大出度”

使用节点数量作为邻居矩阵的初始值是因为啥

节点的出度都初始为0。

遍历所有节点, 如果节点不在训练集中则跳过该节点, 否则,遍历该节点的所有邻居, 如果该邻居不在训练集中,则跳过该邻居; 否则,将该邻居加入到当前节点的邻居列表中; 将该节点的出度设置为邻居列表的长度。 假设该节点符合条件的邻居数为N 如果N为0,则跳过该节点; 如果N大于最大出度,则进行无放回采样,保证邻居数等于最大出度 如果小于最大出度,则进行有放回采样,保证邻居数等于最大出度 最后,将该节点的采样到的邻居填充到临接矩阵中。

最后,返回临接矩阵和出度列表。

注意,邻接矩阵和出度列表都包含了图中所有的节点, 只不过 只有训练集中的节点才真实记录了采样到的邻居节点,测试集、验证集中的节点和虽然在训练集中,但是邻居都不在训练集中的节点的邻居全部是初始化值,即节点数量。这个值超出了节点下标,所以可以认为是没有邻居。 出度列表中也只有训练集中的节点真实记录了出度值,其余都为初始值0.

测试集构造临接矩阵的逻辑和训练集基本相同,只不过不再限制节点和邻居边都在训练集中,即节点和边都可以是训练集中出现过的。

测试集中不再计算节点的出度(原因是啥)

第二步,构造训练集的边集合和验证集的边集合。

这一部分好几个地方没明白。。。

random_context 表示是否使用random walk生成的边集合作为训练集,如果为false,则使用图中的边进行训练。

n2v_retrain 表示是否用来在node2vec模型中学习新的节点的embedding。

如果n2v_retrain为false,则去掉满足如下条件的边: 1.边的起点或者终点不在图的节点集合中; 2.起点或者终点的出度为0,且(起点不在测试集中或者在验证集中)和(终点不在测试集中或者在验证集中)(这个是为啥?)

将满足这两种条件的边去掉,剩余的边作为训练集中的边。 将train_removed标记为true的边作为验证集中的边,即起点或者终点不在训练集中的边都作为验证集的边。

如果n2v_retrain 为true,分为两种情况: fixed_n2v 表示是否在当前节点集合基础上重新训练node2vec。 1.fixed_n2v也为true,表示在已有节点基础上重新训练n2v模型。 此时,去掉终点在测试集或者验证集中的边(?这里是为啥

2.如果为false,表示在新节点上学习embedding,此时,训练集和验证集中的边不再做任何限制,都使用所有的边。

第三步,对邻居进行采样,并构建模型

对邻居采样在UniformNeighborSampler类中实现,该类对batch中的每个节点采样指定数量的邻居。

无监督的Graphsage模型在SampleAndAggregate中实现。

identity_dim 表示是否使用节点id作为特征。 在静态图中,可以使用节点id表示特征,即每个节点id都有一个embedding。此时,训练可能会变慢,但是效果会有提升。在动态图中,由于有新节点,所以此时就不能使用节点id作为特征了。

在模型中,inputs1表示边的起点,inputs2表示边的终点。

如果features为空,则只使用节点id作为特征。如果features不为空,则使用features作为节点特征。如果identity_dim也大于0,那么将节点id的embedding也加入到特征中。

构建模型

无监督学习中,边的终点是label,负采样的节点是负样本,负样本数量为neg_sample_size。

第一步,给batch中的每条边的起点和终点采样邻居节点。

采样信息都记录在layer_infos中的SAGEInfo对象中。

以边的起点为例: 第一跳的输入是batch中的边的起点,记录到samples中。 support_size 记录每个起始节点累计采样到的邻居节点数量。 起点节点的support_size为1. 要采样的邻居数量为layer_infos的最后一层中的num_samples。 把采样到的邻居也加入到samples中,同时,support_size 变为上一层的采样数量(1)该层的采样数量。 第二跳的输入是第一跳采样到的邻居。 然后对这些邻居节点的邻居进行采样,采样邻居数量为layer_infos的倒数第二层的num_samples。 support_size 变为上一跳的采样数量该跳的采样数量。

最后,返回每一跳采样到的节点(samples)和每个输入节点在每一跳采样的邻居数量

假设有两层,第一层的邻居数量为25, 第二层邻居数量为10

那么第一次在采样时,每个节点采样10个邻居,得到节点数为10batch_size. 第二次在采样时,每个节点采样25个邻居,得到节点数为2510*batch_size.

为什么要倒着来呢?

第二步,对采样到的邻居节点信息进行聚合。

还是上面的例子。 特征维度为50.

第一层聚合

输入:[batch50, 10batch50, 1025batch50] aggregator的维度是[50, 128],输入维度50,生成128

第一跳: 输入为[batch50,10batch*50]

邻居被reshape为[batch,10,50] 假设是MeanAggregator 邻居特征矩阵的维度为[batch_size, 10,50],那么, 先对邻居求均值,得到聚合后的均值向量[batch_size,50].

然后分别对邻居的聚合向量和自身的特征向量进行线性映射。 Wsself + Wnneigh

最后,将两部分结果 concat或者相加 作为第一跳的聚合结果,如果是concat,输出维度为[batch_size,256]

第二跳

输入为[10batch50,1025batch*50]

邻居reshape为[10*batch,25,50]

先对邻居求均值,得到聚合后的均值向量[10*batch_size,50]

然后分别对邻居的聚合向量和自身的特征向量进行线性映射。 Wsself + Wnneigh

最后,将两部分结果concat 或者相加作为第二跳的聚合结果,如果是concat,输出维度为[10*batch_size,256]

第二层: aggregator 的维度是[256,128],

第一跳: 输入是[batch256,10batch*256]

聚合过程和第一层一样,得到的输出维度[batch_size,256]

这也是起始节点最终聚合后的结果。

分别得到边的起点的表示和终点的表示(维度和起点相同)。

最后,对采样的负样本同样进行负采样和聚合,得到负样本的表示。

这样,起点、终点和负样本都得到表示了。

最后,计算损失函数。

函数的输入就是起点、终点和负样本的表示。

默认是交叉熵损失:

 def _xent_loss(self, inputs1, inputs2, neg_samples, hard_neg_samples=None):
        aff = self.affinity(inputs1, inputs2) #维度为[batch_size] #计算input1和input2的内积
        neg_aff = self.neg_cost(inputs1, neg_samples, hard_neg_samples)
        #维度为[batch_size ,20],每个样本都和20个负样本计算内积作为相似度
        true_xent = tf.nn.sigmoid_cross_entropy_with_logits(
                labels=tf.ones_like(aff), logits=aff) #正样本的交叉熵
        negative_xent = tf.nn.sigmoid_cross_entropy_with_logits( #负样本的交叉熵
                labels=tf.zeros_like(neg_aff), logits=neg_aff)
        loss = tf.reduce_sum(true_xent) + self.neg_sample_weights * tf.reduce_sum(negative_xent)
        return loss

affinity计算起点和终点的内积作为正样本得分,neg_cost计算起点和负样本的内积作为负样本得分。

然后分别计算正样本的损失和负样本的损失,最后对两部分进行加权求和,得到最终的loss。

到这里模型结构就搭建完了。

下面就要开始训练了。

每个epoch 刚开始先shuffle 训练集的边和节点,并且重置batch_num=0

在每一步,从训练集的边中按顺序取一个batch,并把batch中的边的起点feed给batch1,终点feed给batch2,起点和终点 就是模型的输入了。有了输入就可以计算损失了。

并且,每隔一定的step就在验证集上选择一个batch进行验证。

迭代,直至训练集中所有边都处理过了。

  def next_minibatch_feed_dict(self):
        start_idx = self.batch_num * self.batch_size
        self.batch_num += 1
        end_idx = min(start_idx + self.batch_size, len(self.train_edges))
        batch_edges = self.train_edges[start_idx : end_idx]
        return self.batch_feed_dict(batch_edges)

     def batch_feed_dict(self, batch_edges):
        batch1 = []
        batch2 = []
        for node1, node2 in batch_edges:
            batch1.append(self.id2idx[node1])
            batch2.append(self.id2idx[node2])

        feed_dict = dict()
        feed_dict.update({self.placeholders['batch_size'] : len(batch_edges)})
        feed_dict.update({self.placeholders['batch1']: batch1})
        feed_dict.update({self.placeholders['batch2']: batch2})

监督学习脚本是一个节点分类任务,节点的label 在数据中应该有。

参考: https://blog.csdn.net/yyl424525/article/details/102966617

宁雨 /
Published under (CC) BY-NC-SA in categories tagged with
comments powered by Disqus