基于gcn的半监督分类代码整理1

一、说明

最近阅读了论文《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)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值