Explanation, source code and interpretation of GCN source code comments
1. utils.py
import numpy as np
import pickle as pkl
import networkx as nx
import scipy.sparse as sp
from scipy.sparse.linalg.eigen.arpack import eigsh
import sys
def parse_index_file(filename):
"""Parse index file."""
index = []
for line in open(filename):
index.append(int(line.strip()))
return index
def sample_mask(idx, l):
"""Create mask."""
mask = np.zeros(l)
mask[idx] = 1
return np.array(mask, dtype=np.bool)
def load_data(dataset_str):
"""
Loads input data from gcn/data directory
ind.dataset_str.x => the feature vectors of the training instances as scipy.sparse.csr.csr_matrix object;
ind.dataset_str.tx => the feature vectors of the test instances as scipy.sparse.csr.csr_matrix object;
ind.dataset_str.allx => the feature vectors of both labeled and unlabeled training instances
(a superset of ind.dataset_str.x) as scipy.sparse.csr.csr_matrix object;
ind.dataset_str.y => the one-hot labels of the labeled training instances as numpy.ndarray object;
ind.dataset_str.ty => the one-hot labels of the test instances as numpy.ndarray object;
ind.dataset_str.ally => the labels for instances in ind.dataset_str.allx as numpy.ndarray object;
ind.dataset_str.graph => a dict in the format {index: [index_of_neighbor_nodes]} as collections.defaultdict object;
ind.dataset_str.test.index => the indices of test instances in graph, for the inductive setting as list object. 归纳这些test结点
All objects above must be saved using python pickle module.
:param dataset_str: Dataset name
:return: All data input files loaded (as well the training/test data).
"""
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:
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)
test_idx_reorder = parse_index_file(
"data/ind.{}.test.index".format(dataset_str))
test_idx_range = np.sort(test_idx_reorder)
if dataset_str == 'citeseer':
# Fix citeseer dataset (there are some isolated nodes in the graph)
# Find isolated nodes, add them as zero-vecs into the right position
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
features = sp.vstack((allx, tx)).tolil()
""" example
it works for numpy
arr = np.array([2, 3, 4, 5, 6])
idx_reorder = [4, 3, 2, 1, 0] # the disorder indices of arr
# we are going to order the arr in a sort indices
idx_range = np.sort(idx_reorder)
arr[idx_reorder] = arr[idx_range]
print(arr)
>>> [6 5 4 3 2]
----- lil matrix ----
feature = sp.lil_matrix(np.array([[2], [3], [4], [5], [6]]))
>>> (0, 0) 2
(1, 0) 3
(2, 0) 4
(3, 0) 5
(4, 0) 6
idx_reorder = [4, 3, 2, 1, 0] # the disorder indices of feature
# we are going to order the feature in a sort indices
idx_range = np.sort(idx_reorder)
feature[idx_reorder, :] = feature[idx_range, :] # [4,3,2,1,0] => [0,1,2,3,4] 原本 4 位置的值被0替换掉
>>> (0, 0) 6
(1, 0) 5
(2, 0) 4
(3, 0) 3
(4, 0) 2
"""
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, :]
idx_test = test_idx_range.tolist() # [1708,2707]
idx_train = range(len(y)) # [0,140)
idx_val = range(len(y), len(y)+500) #[140,640)
""" bool [True True True ... False False False]
train_mask = [0,140)为True 其余为False
val_mask = [140,640)为True 其余为False
test_mask = [1708, 2707]为True 其余为False
"""
train_mask = sample_mask(idx_train, labels.shape[0])
val_mask = sample_mask(idx_val, labels.shape[0])
test_mask = sample_mask(idx_test, labels.shape[0])
y_train = np.zeros(labels.shape)
y_val = np.zeros(labels.shape)
y_test = np.zeros(labels.shape)
""" 替换False的位置为0
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
mask = np.zeros(len(a))
idx = [1, 2]
mask[idx] = 1
mask = np.array(mask, dtype=np.bool)
a_ = np.zeros(a.shape)
a_[mask, :] = a[mask, :]
print(a_mask)
>>> [[0. 0. 0.]
[4. 5. 6.]
[7. 8. 9.]
[0. 0. 0.]]
"""
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
def sparse_to_tuple(sparse_mx):
"""Convert sparse matrix to tuple representation.
将稀疏矩阵转换为元组形式(行列坐标,值,shape)"""
def to_tuple(mx):
if not sp.isspmatrix_coo(mx): # 判断mx是否为coo_matrix
mx = mx.tocoo() # 返回稀疏矩阵coo_matrix形式
coords = np.vstack((mx.row, mx.col)).transpose() # 矩阵mx的行和列索引
values = mx.data # 矩阵mx的值
shape = mx.shape # 矩阵mx的形状
""" example:
a = [[1,0,2],
[0,0,3],
[5,0,0]]
mx = coo_matrix(a)
>>> (0, 0) 1
(0, 2) 2
(1, 2) 3
(2, 0) 5
mx.row => [0,0,1,2]
mx.col => [0,2,2,0]
np.vstack((mx.row,mx.col)).transpose()
>>> [[0 0]
[0 2]
[1 2]
[2 0]]
mx.data
>>> [1 2 3 5]
mx.shape
>>> (3, 3)
:return: tuple
>>> (array([[0, 0], [0, 2], [1, 2], [2, 0]], dtype=int32),
array([1, 2, 3, 5]), (3, 3))
"""
return coords, values, shape
if isinstance(sparse_mx, list): # 判断sparse_mx是否为list类型
for i in range(len(sparse_mx)):
sparse_mx[i] = to_tuple(sparse_mx[i])
else:
sparse_mx = to_tuple(sparse_mx)
return sparse_mx
# preprocess_features和normalize_adj的写法一样 都是用度矩阵来归一化
def preprocess_features(features):
"""Row-normalize feature matrix and convert to tuple representation"""
rowsum = np.array(features.sum(1)) # 对每一行求和 axis=1是按列 axis=0是行
""" example
a = np.array([[1, 2, 4], [4, 6, 3]])
np.sum(a, axis=1)
>>> [7, 13]
"""
r_inv = np.power(rowsum, -1).flatten()
r_inv[np.isinf(r_inv)] = 0.
r_mat_inv = sp.diags(r_inv) # 稀疏对角矩阵
# 这里的归一化类似于GCN里面的归一化 用矩阵的度来归一化 相当于就是除以这一行的和 妙!
features = r_mat_inv.dot(features)
return sparse_to_tuple(features)
def normalize_adj(adj):
"""Symmetrically normalize adjacency matrix.
对称归一化,对应论文中的公式."""
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)
return adj.dot(d_mat_inv_sqrt).transpose().dot(d_mat_inv_sqrt).tocoo()
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 construct_feed_dict(features, support, labels, labels_mask, placeholders):
"""Construct feed dictionary."""
""" example
dic=dict()
dic={'Name':'Liugx','age':20}
>>> {'Name': 'Liugx', 'age': 20}
dic.update({'sex':'man'}) 添加指定字典到dic中
>>> {'Name': 'Liugx', 'age': 20, 'sex': 'man'}
"""
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))})
feed_dict.update({placeholders['num_features_nonzero']: features[1].shape})
return feed_dict
def chebyshev_polynomials(adj, k):
"""Calculate Chebyshev polynomials up to order k. Return a list of sparse matrices (tuple representation)."""
print("Calculating Chebyshev polynomials up to order {}...".format(k))
adj_normalized = normalize_adj(adj)
laplacian = sp.eye(adj.shape[0]) - adj_normalized
largest_eigval, _ = eigsh(laplacian, 1, which='LM')
scaled_laplacian = (
2. / largest_eigval[0]) * laplacian - sp.eye(adj.shape[0])
t_k = list()
t_k.append(sp.eye(adj.shape[0]))
t_k.append(scaled_laplacian)
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
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)
2. The models.py
base class Model implements the build() method, which performs forward propagation. The specific propagation method is defined by the layer. The specific layers of each model are defined in the _build() method of the subclass.
from gcn.layers import *
from gcn.metrics import *
flags = tf.app.flags
FLAGS = flags.FLAGS
class Model(object):
def __init__(self, **kwargs):
allowed_kwargs = {'name', 'logging'}
for kwarg in kwargs.keys():
assert kwarg in allowed_kwargs, 'Invalid keyword argument: ' + kwarg
name = kwargs.get('name')
if not name:
name = self.__class__.__name__.lower()
self.name = name
logging = kwargs.get('logging', False)
self.logging = logging
self.vars = {}
self.placeholders = {}
self.layers = []
self.activations = []
self.inputs = None
self.outputs = None
self.loss = 0
self.accuracy = 0
self.optimizer = None
self.opt_op = None
def _build(self):
raise NotImplementedError
def build(self):
""" Wrapper for _build() """
with tf.variable_scope(self.name):
self._build()
# Build sequential layer model
self.activations.append(self.inputs) # 初始化第一个元素为inputs features
for layer in self.layers:
hidden = layer(self.activations[-1]) # 这个hidden即为对应最后一个input的输出
self.activations.append(hidden)
self.outputs = self.activations[-1]
# Store model variables for easy access
variables = tf.get_collection(
tf.GraphKeys.GLOBAL_VARIABLES, scope=self.name)
self.vars = {var.name: var for var in variables}
# Build metrics
self._loss()
self._accuracy()
self.opt_op = self.optimizer.minimize(self.loss)
def predict(self):
pass # 不做任何事情 用做占位语句 给后面的子类用的
def _loss(self):
raise NotImplementedError # 子类一定要实现这个方法 否则就会报错
def _accuracy(self):
raise NotImplementedError # 子类一定要实现这个方法 否则就会报错
def save(self, sess=None):
if not sess:
raise AttributeError("TensorFlow session not provided.")
saver = tf.train.Saver(self.vars)
save_path = saver.save(sess, "tmp/%s.ckpt" % self.name)
print("Model saved in file: %s" % save_path)
def load(self, sess=None):
if not sess:
raise AttributeError("TensorFlow session not provided.")
saver = tf.train.Saver(self.vars)
save_path = "tmp/%s.ckpt" % self.name
saver.restore(sess, save_path)
print("Model restored from file: %s" % save_path)
class MLP(Model):
def __init__(self, placeholders, input_dim, **kwargs):
super(MLP, self).__init__(**kwargs)
self.inputs = placeholders['features']
self.input_dim = input_dim
# self.input_dim = self.inputs.get_shape().as_list()[1] # To be supported in future Tensorflow versions
self.output_dim = placeholders['labels'].get_shape().as_list()[1]
self.placeholders = placeholders
self.optimizer = tf.train.AdamOptimizer(
learning_rate=FLAGS.learning_rate)
self.build()
def _loss(self):
# Weight decay loss
for var in self.layers[0].vars.values():
self.loss += FLAGS.weight_decay * tf.nn.l2_loss(var)
# Cross entropy error
self.loss += masked_softmax_cross_entropy(self.outputs, self.placeholders['labels'],
self.placeholders['labels_mask'])
def _accuracy(self):
self.accuracy = masked_accuracy(self.outputs, self.placeholders['labels'],
self.placeholders['labels_mask'])
def _build(self):
self.layers.append(Dense(input_dim=self.input_dim,
output_dim=FLAGS.hidden1,
placeholders=self.placeholders,
act=tf.nn.relu,
dropout=True,
sparse_inputs=True,
logging=self.logging))
self.layers.append(Dense(input_dim=FLAGS.hidden1,
output_dim=self.output_dim,
placeholders=self.placeholders,
act=lambda x: x,
dropout=True,
logging=self.logging))
def predict(self):
return tf.nn.softmax(self.outputs)
class GCN(Model):
def __init__(self, placeholders, input_dim, **kwargs):
super(GCN, self).__init__(**kwargs)
self.inputs = placeholders['features']
self.input_dim = input_dim
# self.input_dim = self.inputs.get_shape().as_list()[1] # To be supported in future Tensorflow versions
self.output_dim = placeholders['labels'].get_shape().as_list()[1]
self.placeholders = placeholders
self.optimizer = tf.train.AdamOptimizer(
learning_rate=FLAGS.learning_rate)
self.build()
def _loss(self):
# Weight decay loss
# l2损失加到总损失里面
for var in self.layers[0]
The article comes from the Internet and respects the author’s originality. If there is any infringement, please contact the administrator to delete it.