目录
一、GCN基本介绍
二、GCN的Keras实现
Cora 数据集预处理
1. 将Cora节点的类别标签进行one-hot编码
2. 将Cora.cite文件转化为邻接矩阵形式
3. 将数据集划分成训练集、验证集、测试集
Keras构建模型训练
1. 模型构建
2. Keras模型训练
嗨,这是新手小白的第一篇文章。 新手上路,如果有什么写得不恰当或者不正确的地方,还请各位大神多多指教!
平时都是在CSDN上白嫖大神的文章,受益匪浅。今天之所以决定开始写自己的文章,主要是因为我的自控能力太弱鸡,想要学的东西总是坚持不了一周,所以想借着写公开文章的方式来自我监督,同时也可以把平时自己收集到的一些资料供大家分享。
这篇文章介绍一下GCN。其实早就听闻GCN的大名,有关于GCN的介绍分析文章数不胜数,这篇文章就当做我个人的学习记录吧。
GCN层的传递规则如下:
其中, 为shape为A的单位矩阵,为对角矩阵,且,为l层的所需要学习的权重参数。为节点的特征矩阵。 令,事实上可将X看作是邻接矩阵A进行归一化后的结果,则, 则矩阵每一行i均是节点i的邻居节点以及节点i本身在l层特征的平均聚合。随后将乘上权重矩阵W并输入非线性激活函数完成一次卷积。
得到GCN的propagation规则后,下面利用Keras实现GCN在Cora数据集上的节点分类问题。
文章代码为
文章中实现的GCN为两层,第一层采用ReLU作为激活函数,第二层采用sofmax作为激活函数(用于分类问题)。故模型的计算如下:
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)
因为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这个包。。还是直接看官方文档吧。
传统的机器学习的样本为,需要将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)
两层GCN的模型结构比较简单,但由于单层GCN的有两个输入,即归一化邻接矩阵X和当前层节点嵌入表达,因此需要自定义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__()。
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
基本步骤:
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, )
model.compile(optimizer=Adam(0.01), loss='categorical_crossentropy',
weighted_metrics=['categorical_crossentropy', 'acc'])
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])
# 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))