GCN的Python实现与源码分析

目录

一、GCN基本介绍

二、GCN的Keras实现

Cora 数据集预处理

1. 将Cora节点的类别标签进行one-hot编码

2. 将Cora.cite文件转化为邻接矩阵形式

3. 将数据集划分成训练集、验证集、测试集

Keras构建模型训练

1. 模型构建

2. Keras模型训练

 


    嗨,这是新手小白的第一篇文章。 新手上路,如果有什么写得不恰当或者不正确的地方,还请各位大神多多指教!

    平时都是在CSDN上白嫖大神的文章,受益匪浅。今天之所以决定开始写自己的文章,主要是因为我的自控能力太弱鸡,想要学的东西总是坚持不了一周,所以想借着写公开文章的方式来自我监督,同时也可以把平时自己收集到的一些资料供大家分享。

    这篇文章介绍一下GCN。其实早就听闻GCN的大名,有关于GCN的介绍分析文章数不胜数,这篇文章就当做我个人的学习记录吧。

 

一、GCN基本介绍

    GCN层的传递规则如下:

H^{l+1} = \sigma(\hat D^{-1/2}\hat A \hat D^{-1/2} H^{l}W^{l})

其中\hat A = A + I_{N}I_N为shape为A的单位矩阵,\hat D为对角矩阵,且\hat D_{i,i} = \sum _{j=1}^n \hat A_{i,j}W^l为l层的所需要学习的权重参数。H^0 = F为节点的特征矩阵。 令X = \hat D^{-1/2} \hat A \hat D^{-1/2},事实上可将X看作是邻接矩阵A进行归一化后的结果,则H^{l+1} = \sigma (XH^lW^l), 则X H^{l}矩阵每一行i均是节点i的邻居节点以及节点i本身在l层特征的平均聚合。随后将XH^{l}乘上权重矩阵W并输入非线性激活函数\sigma完成一次卷积。

得到GCN的propagation规则后,下面利用Keras实现GCN在Cora数据集上的节点分类问题。

 

二、GCN的Keras实现

文章代码为

文章中实现的GCN为两层,第一层采用ReLU作为激活函数,第二层采用sofmax作为激活函数(用于分类问题)。故模型的计算如下:

Z = softmax(XReLU(XFW^0)W^1)

Cora 数据集预处理

1. 将Cora节点的类别标签进行one-hot编码

Cora数据集2700多个节点,节点包含七类文本标签,需要将这2700多个节点根据其文本化的类别标签进行one-hot编码用于多标签分类。

# 获取类别标签列表
id_feature_labels = np.genfromtxt("{}{}.content".format(path, dataset), dtype=np.str)

labels = id_feature_labels[:,-1]

# 获取标签类别
classes = set(labels)

# 构造每个文本标签对应的one-hot编码字典,其中np.identity(n)构造维度为nxn的单位方阵
classes_dict = {c: np.identity(len(classes))[i,:] for i, c in enumerate(classes)}

# 逐一从labels中提取元素,然后在classes_dict中查到对应的one-hot编码,利用map函数实现。
# map()接收一个函数,一个迭代对象作为输入,输出为迭代对象元素逐一经过函数处理后的结果。Python3 需要将map返回的元素使用list显示转化为list,否认返回的是一个可迭代对象。
np.array(list(map(classes_dict.get, labels)),dtype=np.int32)


2. 将Cora.cite文件转化为邻接矩阵形式

因为Cora.cite文件中的元素ID不是从0开始,且ID不连续,故需要利用idx_map实现

idx = np.array(idx_feature_labels[:,0], dtype=np.int32)

idx_map = {j:i for i,j in enumerate(idx)}

edges_unordered = np.genfromtxt("{}{}.cite".format(path, dataset), dtype=np.int32)

# 知识点 array.flatten()作用为将array转化为1维向量
edges = np.array(list(map(idx_map.get, edges_unordered.flatten())),dtype=np.int32).reshape(edges_unordered.shape)

得到相应的边集合,下面需要将边转化为邻接矩阵。代码中采用的是scipy.sparse中的稀疏矩阵。下面简单介绍一下scipy.sparse这个包。。还是直接看官方文档吧。

3. 将数据集划分成训练集、验证集、测试集

传统的机器学习的样本为(X,Y),需要将X,Y同时划分成对应的训练集、验证集、测试集,然后将训练集、验证集、测试集分别输入模型来进行训练、调超参、测试。

需要注意的是,GCN每次输入的是整个图的邻接矩阵,并且在最后一层得到的是所有节点的嵌入向量,而并非传统的机器学习方法,即一次仅输入部分样本进行训练。此时如何进行模型的训练?

此时需要用的Keras中模型训练的model.fit()中的sample_weight参数,其可以指定对应训练样本的可选 Numpy 权重数组,用于对损失函数进行加权(仅在训练期间)。 您可以传递与输入样本长度相同的平坦(1D)Numpy 数组(权重和样本之间的 1:1 映射)。

在这里的具体实现是,构造对应训练集、验证集、测试集对应的train_mask,val_mask, test_mask。构造方法为: 每个mask的长度为样本数,且train_mask中对应训练节点的元素值设置为1,其余值为0。

def get_splits(y,):
    idx_list = np.arange(len(y))

    idx_train = []
    label_count = {}
    for i, label in enumerate(y):
    # 传入的y是一个矩阵,所有节点标签的one-hot
    # np.argmax(list)返回list中最大值的下标
        label = np.argmax(label)
        if label_count.get(label, 0) < 20:
            idx_train.append(i)
    # 字典.get(key. default)返回字典key对应的value。若key不存在,返回default
            label_count[label] = label_count.get(label, 0) + 1

    idx_val_test = list(set(idx_list) - set(idx_train))
    idx_val = idx_val_test[0:500]
    idx_test = idx_val_test[500:1500]

    # y_train、y_val、y_test均与真个样本集y同shape
    # 因为训练阶段、验证阶段、测试阶段,模型输入均为图的邻接矩阵A,在最后一层GCN得到的
    # 所有节点的嵌入向量,为了方便最后的损失函数计算,需要输入整个样本集Y的shap。只是
    # 最后通过设置fit函数的sample_weight的参数值mask来计算真正的损失。

    y_train = np.zeros(y.shape, dtype=np.int32)
    y_val = np.zeros(y.shape, dtype=np.int32)
    y_test = np.zeros(y.shape, dtype=np.int32)

    # 通过列表的形式访问一系列元素,注意学习
    y_train[idx_train] = y[idx_train]
    y_val[idx_val] = y[idx_val]
    y_test[idx_test] = y[idx_test]
    train_mask = sample_mask(idx_train, y.shape[0]) # 说明y.shape =( N, C) N 为节点数,C为 
                                                      样本标签数目
    val_mask = sample_mask(idx_val, y.shape[0])
    test_mask = sample_mask(idx_test, y.shape[0])

    return y_train, y_val, y_test,train_mask, val_mask, test_mask

def sample_mask(idx, l):
    mask = np.zeros(l)
    mask[idx] = 1
    return np.array(mask, dtype=np.bool)

Keras构建模型训练

1. 模型构建

两层GCN的模型结构比较简单,但由于单层GCN的有两个输入,即归一化邻接矩阵X和当前层节点嵌入表达H^l,因此需要自定义layer。下面详细介绍Keras自定义层的方法及函数解析,作为Keras的学习。

自定义Keras 层需要实现如下三个方法:

build(input_shape):这是定义权重的方法,可训练的权应该在这里被加入列表self.trainable_weights中。
其他的属性还包括self.non_trainabe_weights(列表)和
self.updates(需要更新的形如(tensor, new_tensor)的tuple的列表)。
你可以参考BatchNormalization层的实现来学习如何使用上面两个属性。
这个方法必须设置self.built = True,可通过调用super([layer],self).build()实现

call(x):这是定义层功能的方法,除非你希望你写的层支持masking,
否则你只需要关心call的第一个参数:输入张量

compute_output_shape(input_shape):如果你的层修改了输入数据的shape,
你应该在这里指定shape变化的方法,这个函数使得Keras可以做自动shape推断

还可以定义具有多个输入张量和多个输出张量的 Keras 层。 为此,你应该假设方法 build(input_shape)call(x) 和 compute_output_shape(input_shape) 的输入输出都是列表

from keras import backend as K
from keras.engine.topology import Layer

class MyLayer(Layer):

    def __init__(self, output_dim, **kwargs):
        self.output_dim = output_dim
        super(MyLayer, self).__init__(**kwargs)

    def build(self, input_shape):
        assert isinstance(input_shape, list)
        # 为该层创建一个可训练的权重
        self.kernel = self.add_weight(name='kernel',
                                      shape=(input_shape[0][1], self.output_dim),
                                      initializer='uniform',
                                      trainable=True)
        super(MyLayer, self).build(input_shape)  # 一定要在最后调用它

    def call(self, x):
        assert isinstance(x, list)
        a, b = x
        return [K.dot(a, self.kernel) + b, K.mean(b, axis=-1)]

    def compute_output_shape(self, input_shape):
        assert isinstance(input_shape, list)
        shape_a, shape_b = input_shape
        return [(shape_a[0], self.output_dim), shape_b[:-1]]

在Layer基类中包含两个特殊的方法,__init__()和__call__()。

  • __init__()是构造方法,当我们建立类对象时,首先调用该方法初始化类对象。
  • __call__()是可调用方法,一旦实现该方法,我们的类对象在某些行为上可以表现的和函数一
  • 样。可以直接通过类对象object()进行调用。下面举个例子。
    class myObject:
        def __init__(self,mydata):
            self._mydata = mydata
     
        def common_method(self):
            print(self._mydata)
     
        def __call__(self):
            print(self._mydata)
     
    obj = myObject(20)
    obj.common_method()
    obj()
     
    ##########  output  ##############
    20
    20

__call__()方法中有几个关键操作,调用build(),调用call(),调用compute_output_shape(),最后再利用node将该层和上一层链接起来(如何链接可以不用关心)。因此可以知道,我们需要实现上述三个方法。下面具体介绍每个方法的作用与设计。

# bulid 接受的参数为input_shapes是一个二维的张量,其值由该gcn层的输入值的shape确定
# 在本代码中,gcn的输入为上层特征矩阵H以及邻接矩阵A,即[H,A], 故input_shapes=[[H.shape],[A.shape]] 

    def build(self, input_shapes):

        assert len(input_shapes) == 2
        features_shape = input_shapes[0]

        input_dim = int(features_shape[-1])

        # 为该层创建一个可训练的权重
        self.kernel = self.add_weight(shape=(input_dim,
                                             self.units),
                                      initializer=glorot_uniform(
                                          seed=self.seed),
                                      regularizer=l2(self.l2_reg),
                                      name='kernel', )
        # 添加偏置向量
        if self.use_bias:
            self.bias = self.add_weight(shape=(self.units,),
                                        initializer=Zeros(),
                                        name='bias', )
        # 添加dropout
        self.dropout = Dropout(self.dropout_rate, seed=self.seed)

        self.built = True
# 这是定义层功能的方法, 即定义运算, inputs参数接受的当前层的输入
# 本代码为A和H。   
# 这里面的运算都是tensorflow张量,必须采用TensorFlow的运算方法 
def call(self, inputs, training=None, **kwargs):
        features, A = inputs
        # 此时添加dropout层,使得上层的部分神经细胞失效
        features = self.dropout(features, training=training)
        # 真正的运算操作:H = AHW
        output = tf.matmul(tf.sparse_tensor_dense_matmul(
            A, features), self.kernel)
        if self.bias:
            output += self.bias

        # 添加激活函数
        act = self.activation(output)
        
        # 这一句看不懂

        act._uses_learning_phase = features._uses_learning_phase

        # 返回计算结果
        return act

 

Build()方法:

我们知道,当我们定义网络层的时候,需要用到一些张量(tensor)来对我们的输入进行操作。比如权重信息Weights,偏差Biases。其实一个网络本身就可以理解为这些张量的集合。keras是如何在我们给定input以及output_dim的情况下定义这些张量的呢?这里主要就是build()方法的功劳了。build函数就是为该网络定义一层相应的张量的集合。在Layer类中有两个成员变量,分别是trainable_weights和non_trainable_weights,分别是指可以训练的参数的集合和不可训练的参数的集合。这两个参数都是list。在build中建立的张量通过add_weight()方法加入到上面两个张量集合中,进而建立网络层。需要注意的是,一个网络层的参数是固定的,我们不能重复添加,因此,build()方法最多只能调用一次。如何保证每个layer的build()最多调用一次???这是通过self._built变量来控制的。如果built变量为False,那么build()将会在__call__()中被调用,否则build()方法将不再会被调用。build()在被调用之后,built会被赋值为True,防止以后build()被重复调用。这在__call__()方法中有体现。所以我们如果没有重新写__call__()方法,那么我们不用担心build()方法会被多次调用。但是如果重新写了__call__()方法,一定要注意在build()调用之后,将built置为True。【TIP:build只接受input一个参数,所以如果需要用到output_shape,可以在__init__()中将output_shape赋值给一个成员变量,这样就可以在build中直接使用output_shape的值了】。举个简单例子,实现功能为output=tanh(input*W+B)网络层。我们首先定义build()函数。这里用到的参数分别是W和B。假设输出的大小为output_dim,且已经在__init__()中已经初始化了。
 

def build(self,input_shape):
    # input_shape为(batch_size,input_dim)
    # output_dim为self.output_dim
    input_dim = input_shape[1]
    
    #添加权重W,W的形状为(input_dim,output_dim)
    self.W = self.add_Weight(name='simple_layer',
                            shape=(input_dim,self.output_dim),
                            initializer=self.kernel_initializer,
                            trainable='True')
    #添加偏差信息B,bias的形状为(output_dim,)
    if self.use_bias:
        self.B = self.add_weight(
            shape=(self.output_dim, ),
            initializer=self.bias_initializer,
            name='bias',)
    else:
        self.B = None
    # 没有重写__call__()函数,下面这一句可以不写,不过keras官方好像推荐写上。
    self.built = True

2. Keras模型训练

基本步骤:

  1. 构建模型 
     model = GCN(A.shape[-1], feature_dim, 16, y_train.shape[1],  dropout_rate=0.5, l2_reg=2.5e-4,feature_less=FEATURE_LESS, )
  2. compile模型 
    model.compile(optimizer=Adam(0.01), loss='categorical_crossentropy',
                      weighted_metrics=['categorical_crossentropy', 'acc'])
  3. fit模型 
    
    
    val_data = (model_input, y_val, val_mask)
    mc_callback = ModelCheckpoint('./best_model.h5',
                                      monitor='val_weighted_categorical_crossentropy',
                                      save_best_only=True,
                                      save_weights_only=True)
    # 设定验证集,保存在验证集中表现最好的模型参数
    
     model.fit(model_input, y_train, sample_weight=train_mask, validation_data=val_data,
                  batch_size=A.shape[0], epochs=NB_EPOCH, shuffle=False, verbose=2, callbacks=[mc_callback])
  4. 模型测试 
     # test
        model.load_weights('./best_model.h5')
        eval_results = model.evaluate(
            model_input, y_test, sample_weight=test_mask, batch_size=A.shape[0])
        print('Done.\n'
              'Test loss: {}\n'
              'Test weighted_loss: {}\n'
              'Test accuracy: {}'.format(*eval_results))

     

 

你可能感兴趣的:(gcn,gcn)