一、说明
最近阅读了论文《Semi-Supervised Classification with Graph Convolutional Network》,也看了相关的代码,以下是笔记和代码逻辑的整理。
本节主要说明了数据预处理部分,建模在第二节
这个使用tensorflow写的,但是我主要用Pytorch,后续看看怎么修改成Pytorch的
二、代码
这是项目结构,data里面包含了24个2进制的数据,然后有6个python文件
data里的数据如下:
1、_init_.py文件
from __future__ import print_function
from __future__ import division
是Python 2 和 Python 3 之间的兼容性处理
2、inits.py文件
此处写了4种初始化权重的方法
(1)使用均匀分布初始化权重,设置scale=0.05,使用tf.random.uniform()函数在[-0.05,0.05]范围内生成均匀分布的随机数组,使用tf.Variable()构造name变量的初始值。
def uniform(shape, scale=0.05, name=None):
initial = tf.random.uniform(shape, minval=-scale, maxval=scale, dtype=tf.float32)
return tf.Variable(initial, name=name)
(2)使用Glorot & Bengio (AISTATS 2010)论文中的方法生成scale,再使用均匀分布初始化权重。生成scale的公式在代码里,先除后开方。
def glorot(shape, name=None):
init_range = np.sqrt(6.0/(shape[0]+shape[1]))
initial = tf.random.uniform(shape, minval=-init_range, maxval=init_range, dtype=tf.float32)
return tf.Variable(initial, name=name)
(3)全0初始化
def zeros(shape, name=None):
initial = tf.zeros(shape, dtype=tf.float32)
return tf.Variable(initial, name=name)
(4)全1初始化
def ones(shape, name=None):
"""All ones."""
initial = tf.ones(shape, dtype=tf.float32)
return tf.Variable(initial, name=name)
3、train.py文件
我喜欢先看这个文件,然后调用了哪个函数再详细说明,所以这一节可能会有点混乱,但愿我的逻辑是不混乱的。
(1)设置随机数种子
seed = 123
np.random.seed(seed)
tf.random.set_seed(seed)
(2)定义一个解析器,我改成了tensorflow2.X的形式。里面定义了数据集、模型、lr、 epochs等等,调用时之间用args.dataset,即cora。
parser = argparse.ArgumentParser(description='Training parameters for the model.')
parser.add_argument('--dataset', type=str, default='cora', help='Dataset string.')
parser.add_argument('--model', type=str, default='gcn', help='Model string.')
parser.add_argument('--learning_rate', type=float, default=0.01, help='Initial learning rate.')
parser.add_argument('--epochs', type=int, default=200, help='Number of epochs to train.')
parser.add_argument('--hidden1', type=int, default=16, help='Number of units in hidden layer 1.')
parser.add_argument('--dropout', type=float, default=0.5, help='Dropout rate (1 - keep probability).')
parser.add_argument('--weight_decay', type=float, default=5e-4, help='Weight for L2 loss on embedding matrix.')
parser.add_argument('--early_stopping', type=int, default=10, help='Tolerance for early stopping (# of epochs).')
parser.add_argument('--max_degree', type=int, default=3, help='Maximum Chebyshev polynomial degree.')
args = parser.parse_args()
(3)加载数据
adj, features, y_train, y_val, y_test, train_mask, val_mask, test_mask = load_data(args.dataset)
此处调用了utils.py文件的load_data()函数:
首先定义names = ['x', 'y', 'tx', 'ty', 'allx', 'ally', 'graph'],观察/data/ind.coea.前缀的有8个文件,此处先读取7个,并使用pkl.load()解码这些二进制文件,存到objetcs中,然后分别赋值给x y tx ty allx ally graph。
此处说明以下这几个数据:
x是140×1433的稀疏矩阵,打印第一行是(0,19) 1.0 (0,81),1.0 ... (0,1247),1.0,意思是第1行第20、82、1248列上的数是1,其余位上是0,这样表示节省空间
y是140×7的多维数组numpy.ndarry,每行是类似[1 0 0 1 0 0 0]
tx是1000×1433的稀疏矩阵,类似x
ty是1000×7的numpy.ndarry
allx是1708×7的稀疏矩阵
ally是1708×7的numpy.ndarry
graph是图,即字典类型defaultdict
names = ['x', 'y', 'tx', 'ty', 'allx', 'ally', 'graph']
objects = []
for i in range(len(names)):
with open("data/ind.{}.{}".format(dataset_str, names[i]), 'rb') as f:
# 先检查python版本是否大于3.0,大于则指定latin1解码
if sys.version_info > (3, 0):
objects.append(pkl.load(f, encoding='latin1'))
else:
objects.append(pkl.load(f))
x, y, tx, ty, allx, ally, graph = tuple(objects)
然后读取data/ind.cora.test.index,并排序
test_idx_reorder是1708~2707的乱序数组
test_idx_range是1708~2707的有序排列数组
test_idx_reorder = parse_index_file("data/ind.{}.test.index".format(dataset_str))
test_idx_range = np.sort(test_idx_reorder)
如果要读取的是citeseer的8个文件,需要对其额外进行修复,因为图中有一些孤立的节点,找到这些节点,然后把它们作为zero-vecs添加到正确位置。test_idx_range_full是min~max的数字,
创建行数是len(test_idx_range_full)、列数是x的列数的稀疏矩阵tx_extended,然后将tx复制到tx_entended的test_idx_range-min(test_idx_range)行中,再赋值给tx
创建行数是len(test_idx_range_full)、列数是y的列数的稀疏矩阵ty_extended,然后将ty复制到ty_entended的test_idx_range-min(test_idx_range)行中,再赋值给ty
if dataset_str == 'citeseer':
# 修复citeseer数据集(图中有一些孤立的节点)
# 找到孤立的节点,将它们作为zero-vecs添加到正确的位置
test_idx_range_full = range(min(test_idx_reorder), max(test_idx_reorder)+1)
tx_extended = sp.lil_matrix((len(test_idx_range_full), x.shape[1]))
tx_extended[test_idx_range-min(test_idx_range), :] = tx
tx = tx_extended
ty_extended = np.zeros((len(test_idx_range_full), y.shape[1]))
ty_extended[test_idx_range-min(test_idx_range), :] = ty
ty = ty_extended
然后生成feature、adj和labels,features是将allx和tx垂直堆叠,然后用test_idx_range的有序顺序来替换掉features里原来test_idx_reorder的数据。labels是将ally和ty垂直堆叠,然后用有序替换无序。将graph从字典生成邻接矩阵,这应该是个无向图。
# 将allx和tx垂直堆叠,再转换为列表格式的稀疏矩阵(List of Lists, LIL)
# lil格式:非零元素按行存储在一个列表中,再存储在一个外部列表中
features = sp.vstack((allx, tx)).tolil()
# 举例test_idx_range=[124],test_idx_reorder=[214]
# 就是用第1 2 4行的数据 分别替换掉2 1 4行的数据
features[test_idx_reorder, :] = features[test_idx_range, :]
adj = nx.adjacency_matrix(nx.from_dict_of_lists(graph))
labels = np.vstack((ally, ty))
labels[test_idx_reorder, :] = labels[test_idx_range, :]
然后生成y_train, y_val, y_test, train_mask, val_mask, test_mask:
idx_test是[1708, 2708)的列表,idx_train是[0,140)的列表,idx_val是[140, 640)的列表;然后分别生成mask,只有对应位置上是true,其余位置都是false;然后分别读取labels里的各行。
# idx_test是测试集数据在原始数据集中的索引
idx_test = test_idx_range.tolist()
idx_train = range(0, len(y))
idx_val = range(len(y), len(y)+500)
# [0,140)是true
train_mask = sample_mask(idx_train, labels.shape[0])
# [140,640)是true
val_mask = sample_mask(idx_val, labels.shape[0])
# [1708,2707)是true
test_mask = sample_mask(idx_test, labels.shape[0])
# 2708×7
y_train = np.zeros(labels.shape)
y_val = np.zeros(labels.shape)
y_test = np.zeros(labels.shape)
y_train[train_mask, :] = labels[train_mask, :]
y_val[val_mask, :] = labels[val_mask, :]
y_test[test_mask, :] = labels[test_mask, :]
return adj, features, y_train, y_val, y_test, train_mask, val_mask, test_mask
然后继续看train.py,对feaures执行行归一化,返回元组类型
def preprocess_features(features):
"""对特征矩阵进行 行规范化 并转换为元组表示"""
# 对features的列求和,求得每行的和看看看看
rowsum = np.array(features.sum(1))
r_inv = np.power(rowsum, -1).flatten()
r_inv[np.isinf(r_inv)] = 0.
r_mat_inv = sp.diags(r_inv)
# 实现行归一化:使得每行的和加起来是1,数字之间的比例不变
features = r_mat_inv.dot(features)
return sparse_to_tuple(features)
# 行归一化
features = preprocess_features(features)
然后选择模型,提供了三种gcn、gcn_cheby和dense模型,gcn_cheby就是论文中添加chebyshev多项式的gcn模型,danse就是MLP模型
if args.model == 'gcn':
support = [preprocess_adj(adj)]
num_supports = 1
model_func = GCN
elif args.model == 'gcn_cheby':
support = chebyshev_polynomials(adj, args.max_degree)
num_supports = 1 + args.max_degree
model_func = GCN
elif args.model == 'dense':
support = [preprocess_adj(adj)] # Not used
num_supports = 1
model_func = MLP
else:
raise ValueError('Invalid argument for model: ' + str(args.model))
关于普通gcn模型:support = [preprocess_adj(adj)]是指对adj执行对称归一化。
sp.eye(adj,shape[0])是指创建一个行数×行数的单位矩阵(只在对角线上有1),然后与adj相加成为新的adj。adj首先按行求和,求得每个节点的度,然后对度求逆平方根,将无穷值设为0,生成对角矩阵,然后adj乘对角矩阵、转置、乘对角矩阵,转成coo格式。
def preprocess_adj(adj):
"""Preprocessing of adjacency matrix for simple GCN model and conversion to tuple representation."""
adj_normalized = normalize_adj(adj + sp.eye(adj.shape[0]))
return sparse_to_tuple(adj_normalized)
def normalize_adj(adj):
"""对称归一化 邻接矩阵"""
# coo:只存储非零元素的位置和值,减少内存
adj = sp.coo_matrix(adj)
# 按行求和,求得的是每个节点的度数
rowsum = np.array(adj.sum(1))
d_inv_sqrt = np.power(rowsum, -0.5).flatten() # 对度数求逆平方根
d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0.
d_mat_inv_sqrt = sp.diags(d_inv_sqrt) # 对角矩阵
# 先用原始adj乘对角矩阵,然后转置,再乘对角矩阵,再转换成coo格式
return adj.dot(d_mat_inv_sqrt).transpose().dot(d_mat_inv_sqrt).tocoo()
关于gcn_cheby,是论文中的公式5,计算adj的chebyshev的多项式直到k阶
def chebyshev_polynomials(adj, k):
"""计算给定邻接矩阵adj的Chebyshev多项式,直到k阶"""
print("Calculating Chebyshev polynomials up to order {}...".format(k))
adj_normalized = normalize_adj(adj) # 对称归一化
# 计算图的拉普拉斯矩阵
laplacian = sp.eye(adj.shape[0]) - adj_normalized
# 找到矩阵的最大特征值,缩放拉普拉斯矩阵,使得特征值在[-1,1]之间
largest_eigval, _ = eigsh(laplacian, 1, which='LM')
scaled_laplacian = (2. / largest_eigval[0]) * laplacian - sp.eye(adj.shape[0])
# 初始化Chebyshev多项式,存储阶数
#第0阶是单位矩阵,第1阶是缩放后的拉普拉斯矩阵
t_k = list()
t_k.append(sp.eye(adj.shape[0]))
t_k.append(scaled_laplacian)
# chebushev函数递推式
def chebyshev_recurrence(t_k_minus_one, t_k_minus_two, scaled_lap):
s_lap = sp.csr_matrix(scaled_lap, copy=True)
return 2 * s_lap.dot(t_k_minus_one) - t_k_minus_two
# 从第三阶开始计算chebyshev多项式,直到k阶
for i in range(2, k+1):
t_k.append(chebyshev_recurrence(t_k[-1], t_k[-2], scaled_laplacian))
return sparse_to_tuple(t_k)
关于MLP模型,但是感觉没有用,应该是用来做对比的
创建占位符placeholders,这里是刚创建还没填数据
# 字典,包含了多个TensorFlow占位符
# 占位符是在 TensorFlow 计算图中 用于表示待输入数据的节点,在运行图时动态地提供数据
placeholders = {
'support': [tf.sparse.SparseTensor(tf.float32) for _ in range(num_supports)],
'features': tf.sparse.SparseTensor(tf.float32, shape=tf.constant(features[2], dtype=tf.int64)),
'labels': tf.keras.Input(tf.float32, shape=(None, y_train.shape[1])),
'labels_mask': tf.keras.Input(tf.int32),
'dropout': placeholder_with_default(0., shape=()),
'num_features_nonzero': tf.keras.Input(tf.int32) # helper variable for sparse dropout
}
下面是两个函数,一个调用另外一个,下面训练的代码里会调用evaluate()函数。
construct_feed_dict
函数负责构建 feed dictionary,即往placeholders里面填数据。而 evaluate
函数负责使用这个 feed dictionary 运行模型并返回评估结果,包括loss、accuracy和运行时间。
def evaluate(features, support, labels, mask, placeholders):
# 使用feed dictionary运行模型并返回评估结果
t_test = time.time()
feed_dict_val = construct_feed_dict(features, support, labels, mask, placeholders)
with tf.compat.v1.Session() as sess:
# 初始化全局变量
outs_val = sess.run([model.loss, model.accuracy], feed_dict=feed_dict_val)
return outs_val[0], outs_val[1], (time.time() - t_test)
def construct_feed_dict(features, support, labels, labels_mask, placeholders):
"""创建feed dictionary."""
feed_dict = dict()
feed_dict.update({placeholders['labels']: labels})
feed_dict.update({placeholders['labels_mask']: labels_mask})
feed_dict.update({placeholders['features']: features})
feed_dict.update({placeholders['support'][i]: support[i] for i in range(len(support))})
# features[1].shape是features中第二个元素的维度,是(49216,)
feed_dict.update({placeholders['num_features_nonzero']: features[1].shape})
return feed_dict
下面是重点部分,建立模型,我放到下一节了
三、笔记
创建稀疏矩阵的方法 sp.lil_matrix(行数, 列数)
import scipy.sparse as sp
from scipy.sparse.linalg import eigsh
tx_extended = sp.lil_matrix((len(test_idx_range_full), x.shape[1]))
将两个稀疏矩阵(多维数组)垂直堆叠:
features = sp.vstack((allx, tx)).tolil()
将图结构graph(字典)转换为邻接矩阵adj
# networkx用于创建、操作和研究复杂网络
import networkx as nx
adj = nx.adjacency_matrix(nx.from_dict_of_lists(graph))
对稀疏矩阵features执行行归一化
def preprocess_features(features):
"""对特征矩阵进行 行规范化 并转换为元组表示"""
# 对features的列求和,求得每行的和看看看看
rowsum = np.array(features.sum(1))
r_inv = np.power(rowsum, -1).flatten()
r_inv[np.isinf(r_inv)] = 0.
r_mat_inv = sp.diags(r_inv)
# 实现行归一化:使得每行的和加起来是1,数字之间的比例不变
features = r_mat_inv.dot(features)
return sparse_to_tuple(features)
生成对角矩阵的方法
import scipy.sparse as sp
d_mat_inv_sqrt = sp.diags(d_inv_sqrt)