GCN代码分析学习

本文非原创,主要参考学习博文:

 

说明:本文是对论文“SEMI-SUPERVISED CLASSIFICATION WITH GRAPH CONVOLUTIONAL NETWORKS, ICLR 2017”中描述的GCN模型代码详细解读。

代码下载地址:https://github.com/tkipf/pygcn
论文下载地址:https://arxiv.org/abs/1609.02907
数据集下载地址:https://linqs-data.soe.ucsc.edu/public/lbc/cora.tgz


一、代码结构总览

  • layers:定义了模块如何计算卷积
  • models:定义了模型train
  • train:包含了模型训练信息
  • utils:定义了加载数据等工具性的函数

 


 二、数据集结构及内容

论文中所使用的数据集合是Cora数据集,总共有三部分构成:

  • cora.content:包含论文信息;

                                   该文件共2078行,每一行代表一篇论文(即2708篇文章)

                                   由论文编号(id)论文词向量(features)(1433维)和论文类别(labels)三个部分组成

  • cora.cites:包含各论文间的相互引用记录;

                               该文件总共5429行,每一行有两篇论文编号(id),表示右边的论文引用左边的论文。

  • README:对数据集内容的描述

 

该数据集总共有2708个样本,而且每个样本都为一篇论文。根据README可知,所有的论文被分为了7个类别,分别为:

  • 基于案列的论文
  • 基于遗传算法的论文
  • 基于神经网络的论文
  • 基于概率方法的论文
  • 基于强化学习的论文
  • 基于规则学习的论文
  • 理论描述类的论文

此外,为了区分论文的类别,使用一个1433维的词向量,对每一篇论文进行描述,该向量的每个元素都为一个词语是否在论文中出现,如果出现则为“1”,否则为“0”

 


 三、utils.py

1. 特征独热码处理:

 1 def load_data(path="../data/cora/", dataset="cora"):
 2 
 3     """Load citation network dataset (cora only for now)"""
 4     print('Loading {} dataset...'.format(dataset))
 5 
 6     # 首先将文件中的内容读出,以二维数组的形式存储
 7     idx_features_labels = np.genfromtxt("{}{}.content".format(path, dataset),
 8                                         dtype=np.dtype(str))
 9     # 以稀疏矩阵(采用CSR格式压缩)将数据中的特征存储
10 
11     '''content file的每一行的格式为 : <paper_id> <word_attributes>+ <class_label>
12        分别对应 0, 1:-1, -1
13        feature为第二列到倒数第二列,labels为最后一列
14     '''
15     # feature - idx_features_labels[:, 1:-1]:论文词向量
16     # labels - idx_features_labels[:, -1]:论文类别
17     # idx_features_labels[:, 0]:论文编号
18     features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)
19     labels = encode_onehot(idx_features_labels[:, -1]) # 这里的label为onthot格式,如第一类代表[1,0,0,0,0,0,0]
20 
21     """根据引用文件,生成无向图"""
22 
23     # 将每篇文献的编号idx提取出来
24     idx = np.array(idx_features_labels[:, 0], dtype=np.int32)
25 
26     # 对文献的编号构建字典
27     # 由于文件中节点并非是按顺序排列的(打开看看就知道了),因此建立一个编号为0-(node_size-1)的哈希表idx_map,
28     # 哈希表中每一项为id: 索引值,即节点id(论文编号)对应的索引值
29     '''关于enumerate():例如,s = abcdefghij,则enumerate(s):
30        [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f'), (6, 'g'), (7, 'h'), (8, 'i'), (9, 'j')]
31     '''
32     idx_map = {j: i for i, j in enumerate(idx)}
33 
34     # 读取cite文件,以二维数组的形式存储
35     # edges_unordered为直接从边表文件中直接读取的结果,是一个(edge_num, 2)的数组,每一行表示一条边两个端点的idx
36     edges_unordered = np.genfromtxt("{}{}.cites".format(path, dataset),
37                                     dtype=np.int32)
38     # 生成图的边,(x,y)其中x、y都是为以文章编号为索引得到的值(也就是边对应的并非论文编号,而是字典中论文编号对应的索引值),此外,y中引入x的文献
39     edges = np.array(list(map(idx_map.get, edges_unordered.flatten())), # 在idx_map中以idx作为键查找得到对应节点的索引值,reshape成与edges_unordered形状一样的数组
40                      dtype=np.int32).reshape(edges_unordered.shape)
41 
42     # 生成邻接矩阵,生成的矩阵为稀疏矩阵,对应的行和列坐标分别为边的两个点,该步骤之后得到的是一个有向图
43     # 如51行所示,edges是np.array数据,其中np.array.shape[0]表示行数,np.array.shape[1]表示列数
44     # np.ones是生成全1的n维数组,第一个参数表示返回数组的大小
45     '''coo_matrix((data, (i, j)), [shape=(M, N)]) 有三个参数:
46        data[:] 原始矩阵中的数据;
47        i[:] 行的指示符号;例如元素为0则代表data中第一个数据在第0行;
48        j[:] 列的指示符号;例如元素为0则代表data中第一个数据在第0列;
49        综合上面三点,对data中的第一个数据,它在第i[]行,第j[]列;
50        最后的shape参数是告诉coo_matrix原始矩阵的形状,除了上述描述的有数据的行列,其他地方都按照shape的形式补0。'''
51     # 根据coo矩阵性质,这一段的作用就是,网络有多少条边,邻接矩阵就有多少个1,
52     # 所以先创建一个长度为edge_num的全1数组,每个1的填充位置就是一条边中两个端点的编号,
53     # 即edges[:, 0], edges[:, 1],矩阵的形状为(node_size, node_size)。
54     adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])),
55                         shape=(labels.shape[0], labels.shape[0]),
56                         dtype=np.float32)
57 
58     # 无向图的领接矩阵是对称的,因此需要将上面得到的矩阵转换为对称的矩阵,从而得到无向图的领接矩阵
59     '''论文中采用的办法和下面两个语句是等价的,仅仅是为了产生对称的矩阵
60        adj_2 = adj + adj.T.multiply(adj.T > adj)
61        adj_3 = adj + adj.T
62     '''
63     '''test01 = adj.T
64        test02 = adj.T > adj
65        test03 = adj.T.multiply(adj.T > adj)
66        test04 = adj.multiply(adj.T > adj)
67     '''
68     adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)
69 
70     # 定义特征,调用归一化函数(之后的定义)
71     features = normalize(features)
72 
73     # 进行归一化,对应于论文中的A^=(D~)^0.5 A~ (D~)^0.5,但是本代码实现的是A^=(D~)^-1 A~
74     # A^=I+A,其中eye()即为创建单位矩阵
75     # test = normalize(adj)
76     adj = normalize(adj + sp.eye(adj.shape[0])) # eye创建单位矩阵,第一个参数为行数,第二个为列数
77 
78     # 分别构建训练集、验证集、测试集,并创建特征矩阵、标签向量和邻接矩阵的tensor,用来做模型的输入
79     # range()函数内只有一个参数,则表示会产生从0开始计数的整数列表:如range(140)返回[0,1,2...139]
80     # range()中传入两个参数时,则将第一个参数做为起始位,第二个参数为结束位:range(200, 500)返回[200,201,202...499]
81     idx_train = range(140)
82     idx_val = range(200, 500)
83     idx_test = range(500, 1500)
84 
85     # 将特征转换为tensor
86     # *这一步做得必要性?
87     features = torch.FloatTensor(np.array(features.todense()))
88     labels = torch.LongTensor(np.where(labels)[1])
89     adj = sparse_mx_to_torch_sparse_tensor(adj)
90 
91     idx_train = torch.LongTensor(idx_train)
92     idx_val = torch.LongTensor(idx_val)
93     idx_test = torch.LongTensor(idx_test)
94 
95     return adj, features, labels, idx_train, idx_val, idx_test

 在很多的多分类问题中,特征的标签通常都是不连续的内容(如本文中特征是离散的字符串类型),为了便于后续的计算、处理,需要将所有的标签进行提取,并将标签映射到一个独热码向量中。

输入的labels格式如下:

GCN代码分析学习

执行完该程序后,输出的独热码为:(独热码这个概念并不复杂,就是分类标记)

GCN代码分析学习

2. 数据载入及处理函数:

 1 def load_data(path="../data/cora/", dataset="cora"):
 2 
 3     """Load citation network dataset (cora only for now)"""
 4     print('Loading {} dataset...'.format(dataset))
 5 
 6     # 首先将文件中的内容读出,以二维数组的形式存储
 7     idx_features_labels = np.genfromtxt("{}{}.content".format(path, dataset),
 8                                         dtype=np.dtype(str))
 9     # 以稀疏矩阵(采用CSR格式压缩)将数据中的特征存储
10 
11     '''content file的每一行的格式为 : <paper_id> <word_attributes>+ <class_label>
12        分别对应 0, 1:-1, -1
13        feature为第二列到倒数第二列,labels为最后一列
14     '''
15     # feature - idx_features_labels[:, 1:-1]:论文词向量
16     # labels - idx_features_labels[:, -1]:论文类别
17     # idx_features_labels[:, 0]:论文编号
18     features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)
19     labels = encode_onehot(idx_features_labels[:, -1]) # 这里的label为onthot格式,如第一类代表[1,0,0,0,0,0,0]
20 
21     """根据引用文件,生成无向图"""
22 
23     # 将每篇文献的编号idx提取出来
24     idx = np.array(idx_features_labels[:, 0], dtype=np.int32)
25 
26     # 对文献的编号构建字典
27     # 由于文件中节点并非是按顺序排列的(打开看看就知道了),因此建立一个编号为0-(node_size-1)的哈希表idx_map,
28     # 哈希表中每一项为id: 索引值,即节点id(论文编号)对应的索引值
29     '''关于enumerate():例如,s = abcdefghij,则enumerate(s):
30        [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f'), (6, 'g'), (7, 'h'), (8, 'i'), (9, 'j')]
31     '''
32     idx_map = {j: i for i, j in enumerate(idx)}
33 
34     # 读取cite文件,以二维数组的形式存储
35     # edges_unordered为直接从边表文件中直接读取的结果,是一个(edge_num, 2)的数组,每一行表示一条边两个端点的idx
36     edges_unordered = np.genfromtxt("{}{}.cites".format(path, dataset),
37                                     dtype=np.int32)
38     # 生成图的边,(x,y)其中x、y都是为以文章编号为索引得到的值(也就是边对应的并非论文编号,而是字典中论文编号对应的索引值),此外,y中引入x的文献
39     edges = np.array(list(map(idx_map.get, edges_unordered.flatten())), # 在idx_map中以idx作为键查找得到对应节点的索引值,reshape成与edges_unordered形状一样的数组
40                      dtype=np.int32).reshape(edges_unordered.shape)
41 
42     # 生成邻接矩阵,生成的矩阵为稀疏矩阵,对应的行和列坐标分别为边的两个点,该步骤之后得到的是一个有向图
43     # 如51行所示,edges是np.array数据,其中np.array.shape[0]表示行数,np.array.shape[1]表示列数
44     # np.ones是生成全1的n维数组,第一个参数表示返回数组的大小
45     '''coo_matrix((data, (i, j)), [shape=(M, N)]) 有三个参数:
46        data[:] 原始矩阵中的数据;
47        i[:] 行的指示符号;例如元素为0则代表data中第一个数据在第0行;
48        j[:] 列的指示符号;例如元素为0则代表data中第一个数据在第0列;
49        综合上面三点,对data中的第一个数据,它在第i[]行,第j[]列;
50        最后的shape参数是告诉coo_matrix原始矩阵的形状,除了上述描述的有数据的行列,其他地方都按照shape的形式补0。'''
51     # 根据coo矩阵性质,这一段的作用就是,网络有多少条边,邻接矩阵就有多少个1,
52     # 所以先创建一个长度为edge_num的全1数组,每个1的填充位置就是一条边中两个端点的编号,
53     # 即edges[:, 0], edges[:, 1],矩阵的形状为(node_size, node_size)。
54     adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])),
55                         shape=(labels.shape[0], labels.shape[0]),
56                         dtype=np.float32)
57 
58     # 无向图的领接矩阵是对称的,因此需要将上面得到的矩阵转换为对称的矩阵,从而得到无向图的领接矩阵
59     '''论文中采用的办法和下面两个语句是等价的,仅仅是为了产生对称的矩阵
60        adj_2 = adj + adj.T.multiply(adj.T > adj)
61        adj_3 = adj + adj.T
62     '''
63     '''test01 = adj.T
64        test02 = adj.T > adj
65        test03 = adj.T.multiply(adj.T > adj)
66        test04 = adj.multiply(adj.T > adj)
67     '''
68     adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)
69 
70     # 定义特征,调用归一化函数(之后的定义)
71     features = normalize(features)
72 
73     # 进行归一化,对应于论文中的A^=(D~)^0.5 A~ (D~)^0.5,但是本代码实现的是A^=(D~)^-1 A~
74     # A^=I+A,其中eye()即为创建单位矩阵
75     # test = normalize(adj)
76     adj = normalize(adj + sp.eye(adj.shape[0])) # eye创建单位矩阵,第一个参数为行数,第二个为列数
77 
78     # 分别构建训练集、验证集、测试集,并创建特征矩阵、标签向量和邻接矩阵的tensor,用来做模型的输入
79     # range()函数内只有一个参数,则表示会产生从0开始计数的整数列表:如range(140)返回[0,1,2...139]
80     # range()中传入两个参数时,则将第一个参数做为起始位,第二个参数为结束位:range(200, 500)返回[200,201,202...499]
81     idx_train = range(140)
82     idx_val = range(200, 500)
83     idx_test = range(500, 1500)
84 
85     # 将特征转换为tensor
86     # *这一步做得必要性?
87     features = torch.FloatTensor(np.array(features.todense()))
88     labels = torch.LongTensor(np.where(labels)[1])
89     adj = sparse_mx_to_torch_sparse_tensor(adj)
90 
91     idx_train = torch.LongTensor(idx_train)
92     idx_val = torch.LongTensor(idx_val)
93     idx_test = torch.LongTensor(idx_test)
94 
95     return adj, features, labels, idx_train, idx_val, idx_test

 这一部分比较绕,在笔记本上梳理了一下,比较迷幻的就是第68行的“生成对称邻接矩阵”代码:

adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)

关于 adj.multiply(adj.T > adj) 不明白其意义所在,在Debug中显示为全0:

(前面的 adj + adj.T.multiply(adj.T > adj) 理解起来倒是比较直接)

test04 = adj.multiply(adj.T > adj)

GCN代码分析学习

3. 特征归一化函数:

 1 # 该函数需要传入特征矩阵作为参数。对于本文使用的cora的数据集来说,每一行是一个样本,每一个样本是1433个特征。
 2 # 归一化函数实现的方式:对传入特征矩阵的每一行分别求和,取到数后就是每一行非零元素归一化的值,然后与传入特征矩阵进行点乘。
 3 # 其调用在第77行:features = normalize(features)
 4 def normalize(mx):
 5     """Row-normalize sparse matrix"""
 6     rowsum = np.array(mx.sum(1)) # 得到一个(2708,1)的矩阵
 7     r_inv = np.power(rowsum, -1).flatten() # 得到(2708,)的元组
 8     # 在计算倒数的时候存在一个问题,如果原来的值为0,则其倒数为无穷大,因此需要对r_inv中无穷大的值进行修正,更改为0
 9     # np.isinf()函数测试元素是正无穷还是负无穷
10     r_inv[np.isinf(r_inv)] = 0.
11     # 归一化后的稀疏矩阵
12     r_mat_inv = sp.diags(r_inv)  # 构建对角元素为r_inv的对角矩阵
13     # 用对角矩阵与原始矩阵的点积起到标准化的作用,原始矩阵中每一行元素都会与对应的r_inv相乘,最终相当于除以了sum
14     mx = r_mat_inv.dot(mx)
15     return mx

 该函数需要传入特征矩阵作为参数。对于本文使用的cora的数据集来说,每一行是一个样本,每一个样本是1433个特征。

需要注意的是:由于特征中有很多的内容是“0”,因此使用稀疏矩阵的方式进行存储,因此经过该函数归一化之后的函数,仍然为一个稀疏矩阵。

归一化函数实现的方式:对传入特征矩阵的每一行分别求和,取倒数后就是每一行非零元素归一化的值,然后与传入特征矩阵进行点乘

为了直观展示归一化过程,测试如下代码:

test = normalize(adj)

输入adj矩阵如下:(其中 (0, 8) 1.0 表示 第0行8列的值为1)

GCN代码分析学习

归一化后的输出结果test为:

GCN代码分析学习

可以看到,在adj矩阵中,由于第0行是[1, 1, 1, 1, 1],因此经过归一化后会变成[0.2, 0.2, 0.2, 0.2, 0.2]

4. 精度计算函数:

1 def accuracy(output, labels):
2     # 使用type_as(tesnor)将张量转换为给定类型的张量。
3     preds = output.max(1)[1].type_as(labels) # 将预测结果转换为和labels一致的类型
4     correct = preds.eq(labels).double()
5     correct = correct.sum()
6     return correct / len(labels)

 5. 稀疏矩阵转稀疏张量函数

 1 def sparse_mx_to_torch_sparse_tensor(sparse_mx):
 2     """Convert a scipy sparse matrix to a torch sparse tensor."""
 3 
 4     """numpy中的ndarray转化成pytorch中的tensor : torch.from_numpy()
 5        pytorch中的tensor转化成numpy中的ndarray : numpy()
 6     """
 7     sparse_mx = sparse_mx.tocoo().astype(np.float32)
 8     indices = torch.from_numpy(
 9         np.vstack((sparse_mx.row, sparse_mx.col)).astype(np.int64))
10     values = torch.from_numpy(sparse_mx.data)
11     shape = torch.Size(sparse_mx.shape)
12     return torch.sparse.FloatTensor(indices, values, shape)
13 
14     # 这一部分不理解可以去看看COO性稀疏矩阵的结构(?)

 


四、models.py

 1 class GCN(nn.Module):
 2     # nfeat:底层节点的参数,feature的个数;
 3     # nhid:隐层节点个数;
 4     # nclass:最终的分类数
 5     def __init__(self, nfeat, nhid, nclass, dropout):
 6         super(GCN, self).__init__() #  super()._init_()在利用父类里的对象构造函数
 7 
 8         self.gc1 = GraphConvolution(nfeat, nhid) # gc1输入尺寸nfeat,输出尺寸nhid
 9         self.gc2 = GraphConvolution(nhid, nclass) # gc2输入尺寸nhid,输出尺寸ncalss
10         self.dropout = dropout
11 
12     # 输入分别是特征x和邻接矩阵adj;
13     # 最后输出为输出层做log_softmax变换得到的结果
14     def forward(self, x, adj):
15         x = F.relu(self.gc1(x, adj)) # adj即公式Z=softmax(A~Relu(A~XW(0))W(1))中的A~
16         x = F.dropout(x, self.dropout, training=self.training) # x要dropout
17         x = self.gc2(x, adj)
18         return F.log_softmax(x, dim=1)

 

定义了一个图卷积神经网络,其有两个卷积层:

  • 卷积层1(gc1):输入的特征为nfeat,维度是2708;输出的特征为nhid,维度是16;
  • 卷积层2(gc2):输入的特征为nhid,维度是16;输出的特征为nclass,维度是7(即类别的结果)

forward是向前传播函数,最终得到网络向前传播的方式为:relu——dropout——gc2——softmax

关于dropout策略的理解:

在前向传播的时候,让某个神经元的激活值以一定的概率p停止工作,这样可以使模型泛化性更强,因为它不会太依赖某些局部的特征,如图所示:

GCN代码分析学习

 


  五、layers.py

 layers.py中主要定义了图数据实现卷积操作的层,类似于CNN中的卷积层,只是一个“层”而已。本节将分别通过属性定义、参数初始化、前向传播以及字符串表达四个方面对代码进一步解析。

1. 属性定义

GraphConvolution作为一个类,首先需要定义其相关属性。

主要定义了其输入特征in_feature输出特征out_feature两个输入,以及权重weight偏移向量bias两个参数,同时调用了其参数初始化的方法。

(参数初始化此处没有详细说明)

 1  # 初始化层:输入feature,输出feature,权重,偏移
 2     def __init__(self, in_features, out_features, bias=True):
 3         super(GraphConvolution, self).__init__()
 4         self.in_features = in_features
 5         self.out_features = out_features
 6 
 7         '''常见用法self.v = torch.nn.Parameter(torch.FloatTensor(hidden_size)):
 8            可以把该函数理解为类型转换函数,将一个不可训练的类型Tensor转换成可训练的类型parameter,并将parameter绑定至module中。
 9            因此经过类型转换这个self.v变成了模型的一部分,成为了模型中根据训练可以改动的参数了。
10            使用这个函数的目的也是希望某些变量在学习的过程中不断的修改其值以达到最优化。
11         '''
12         self.weight = Parameter(torch.FloatTensor(in_features, out_features)) # 由于weight是可以训练的,因此使用parameter定义
13         if bias:
14             self.bias = Parameter(torch.FloatTensor(out_features)) # 由于weight是可以训练的,因此使用parameter定义
15         else:
16             self.register_parameter('bias', None)
17         self.reset_parameters()

2. 参数初始化

为了让每次训练产生的初始参数尽可能的相同,从而便于实验结果的复现,可以设置固定的随机数生成种子。

1 # 初始化权重
2     def reset_parameters(self):
3         # size()函数主要是用来统计矩阵元素个数,或矩阵某一维上的元素个数的函数  size(1)为行
4         stdv = 1. / math.sqrt(self.weight.size(1)) # sqrt() 方法返回数字x的平方根。
5         # uniform() 方法将随机生成下一个实数,它在 [x, y] 范围内
6         self.weight.data.uniform_(-stdv, stdv)
7         if self.bias is not None:
8             self.bias.data.uniform_(-stdv, stdv)

3. 前馈计算

此处主要定义的是本层的前向传播,通常采用的是 A ∗ X ∗ W 的计算方法。由于 A 是一个sparse变量,因此其与 X 进行卷积的结果也是稀疏矩阵。

 1     '''前馈运算 即计算A~ * X * W(0)
 2        input(即X)与权重W相乘,然后adj(即A)矩阵与他们的积稀疏相乘
 3        直接输入与权重之间进行torch.mm操作,得到support,即XW
 4        support与adj进行torch.spmm操作,得到output,即AXW选择是否加bias
 5     '''
 6     def forward(self, input, adj):
 7         # torch.mm(a, b)是矩阵a和b矩阵相乘,torch.mul(a, b)是矩阵a和b对应位相乘,a和b的维度必须相等
 8         support = torch.mm(input, self.weight)
 9         # torch.spmm(a,b)是稀疏矩阵相乘
10         output = torch.spmm(adj, support)
11         if self.bias is not None:
12             return output + self.bias
13         else:
14             return output

4. 字符串表达

__repr()__ 方法是类的实例化对象用来做“自我介绍”的方法,默认情况下,它会返回当前对象的“类名+object at+内存地址”, 而如果对该方法进行重写,可以为其制作自定义的自我描述信息。

1 def __repr__(self):
2         return self.__class__.__name__ + ' (' \
3                + str(self.in_features) + ' -> ' \
4                + str(self.out_features) + ')'

 


 六、train.py

 train.py完成函数的训练步骤。

由于该文件主要完成对上述函数的调用,因此只是在程序中进行详细的注释,不在分函数进行介绍。

  1 # 在 Python2 中导入未来的支持的语言特征中division (精确除法),
  2 # 即from __future__ import division:若在程序中没有导入该特征,
  3 # / 操作符执行的只能是整除,也就是取整数,只有当导入division(精确算法)以后,
  4 # / 执行的才是精确算法。
  5 from __future__ import division
  6 # 在开头加上from __future__ import print_function这句之后,即使在python2.X,
  7 # 使用print就得像python3.X那样加括号使用。
  8 # 注意:python2.X中print不需要括号,而在python3.X中则需要。
  9 from __future__ import print_function
 10 
 11 import time
 12 import argparse
 13 import numpy as np
 14 
 15 import torch
 16 import torch.nn.functional as F
 17 import torch.optim as optim
 18 
 19 from pygcn.utils import load_data, accuracy
 20 from pygcn.models import GCN
 21 
 22 '''训练设置
 23 '''
 24 parser = argparse.ArgumentParser()
 25 parser.add_argument('--no-cuda', action='store_true', default=False,
 26                     help='Disables CUDA training.')
 27 parser.add_argument('--fastmode', action='store_true', default=False,
 28                     help='Validate during training pass.')
 29 parser.add_argument('--seed', type=int, default=42, help='Random seed.')
 30 parser.add_argument('--epochs', type=int, default=200, # 训练回合200次
 31                     help='Number of epochs to train.')
 32 parser.add_argument('--lr', type=float, default=0.01, # 设置初始学习率(learning rate)
 33                     help='Initial learning rate.')
 34 parser.add_argument('--weight_decay', type=float, default=5e-4, # 定义权重衰减
 35                     help='Weight decay (L2 loss on parameters).')
 36 parser.add_argument('--hidden', type=int, default=16, # 隐藏单元设置为16
 37                     help='Number of hidden units.')
 38 parser.add_argument('--dropout', type=float, default=0.5, # dropout设置
 39                     help='Dropout rate (1 - keep probability).')
 40 
 41 args = parser.parse_args()
 42 # 如果程序不禁止使用gpu且当前主机的gpu可用,arg.cuda就为True
 43 args.cuda = not args.no_cuda and torch.cuda.is_available()
 44 
 45 # 指定生成随机数的种子,从而每次生成的随机数都是相同的,通过设定随机数种子的好处是,使模型初始化的可学习参数相同,从而使每次的运行结果可以复现
 46 np.random.seed(args.seed)
 47 torch.manual_seed(args.seed)
 48 if args.cuda:
 49     torch.cuda.manual_seed(args.seed)
 50 
 51 '''开始训练
 52 '''
 53 
 54 # 载入数据
 55 adj, features, labels, idx_train, idx_val, idx_test = load_data()
 56 
 57 # Model and optimizer
 58 # 函数来自于models.py
 59 model = GCN(nfeat=features.shape[1], # 特征维度,number of features
 60             nhid=args.hidden,
 61             nclass=labels.max().item() + 1,
 62             dropout=args.dropout)
 63 optimizer = optim.Adam(model.parameters(),
 64                        lr=args.lr, weight_decay=args.weight_decay)
 65 
 66 # 如果可以使用GPU,数据写入cuda,便于后续加速
 67 # .cuda()会分配到显存里(如果gpu可用)
 68 if args.cuda:
 69     model.cuda()
 70     features = features.cuda()
 71     adj = adj.cuda()
 72     labels = labels.cuda()
 73     idx_train = idx_train.cuda()
 74     idx_val = idx_val.cuda()
 75     idx_test = idx_test.cuda()
 76 
 77 
 78 def train(epoch):
 79     # 返回当前时间
 80     t = time.time()
 81     # 将模型转为训练模式,并将优化器梯度置零
 82     model.train()
 83     # optimizer.zero_grad()意思是把梯度置零,即把loss关于weight的导数变成0;pytorch中每一轮batch需要设置optimizer.zero_grad
 84     optimizer.zero_grad()
 85 
 86     '''由于在算output时已经使用了log_softmax,这里使用的损失函数是NLLloss,如果之前没有加入log运算,
 87        这里则应使用CrossEntropyLoss
 88        损失函数NLLLoss() 的输入是一个对数概率向量和一个目标标签. 它不会为我们计算对数概率,
 89        适合最后一层是log_softmax()的网络. 损失函数 CrossEntropyLoss() 与 NLLLoss() 类似,
 90        唯一的不同是它为我们去做 softmax.可以理解为:CrossEntropyLoss()=log_softmax() + NLLLoss()
 91        理论上,对于单标签多分类问题,直接经过softmax求出概率分布,然后把这个概率分布用crossentropy做一个似然估计误差。
 92        但是softmax求出来的概率分布,每一个概率都是(0,1)的,这就会导致有些概率过小,导致下溢。 考虑到这个概率分布总归是
 93        要经过crossentropy的,而crossentropy的计算是把概率分布外面套一个-log 来似然
 94        那么直接在计算概率分布的时候加上log,把概率从(0,1)变为(-∞,0),这样就防止中间会有下溢出。 
 95        所以log_softmax本质上就是将本来应该由crossentropy做的取log工作提到预测概率分布来,跳过了中间的存储步骤,防止中间数值会有下溢出,使得数据更加稳定。 
 96        正是由于把log这一步从计算误差提到前面的步骤中,所以用log_softmax之后,下游的计算误差的function就应该变成NLLLoss
 97        (NLLloss没有取log这一步,而是直接将输入取反,然后计算其和label的乘积,求和平均)
 98     '''
 99     # 计算输出时,对所有的节点都进行计算(调用了models.py中的forward即前馈函数)
100     output = model(features, adj)
101     # 损失函数,仅对训练集的节点进行计算,即:优化对训练数据集进行
102     loss_train = F.nll_loss(output[idx_train], labels[idx_train])
103     # 计算准确率
104     acc_train = accuracy(output[idx_train], labels[idx_train])
105     # 反向求导  Back Propagation
106     loss_train.backward()
107     # 更新所有的参数
108     optimizer.step()
109     # 通过计算训练集损失和反向传播及优化,带标签的label信息就可以smooth到整个图上(label information is smoothed over the graph)
110 
111     # 通过model.eval()转为测试模式,之后计算输出,并单独对测试集计算损失函数和准确率。
112     if not args.fastmode:
113         # Evaluate validation set performance separately,
114         # deactivates dropout during validation run.
115         # eval() 函数用来执行一个字符串表达式,并返回表达式的值
116         model.eval()
117         output = model(features, adj)
118 
119     # 测试集的损失函数
120     loss_val = F.nll_loss(output[idx_val], labels[idx_val])
121     acc_val = accuracy(output[idx_val], labels[idx_val])
122 
123     print('Epoch: {:04d}'.format(epoch+1),
124           'loss_train: {:.4f}'.format(loss_train.item()),
125           'acc_train: {:.4f}'.format(acc_train.item()),
126           'loss_val: {:.4f}'.format(loss_val.item()),
127           'acc_val: {:.4f}'.format(acc_val.item()),
128           'time: {:.4f}s'.format(time.time() - t))
129 
130 # 定义测试函数,相当于对已有的模型在测试集上运行对应的loss与accuracy
131 def test():
132     model.eval()
133     output = model(features, adj)
134     loss_test = F.nll_loss(output[idx_test], labels[idx_test])
135     acc_test = accuracy(output[idx_test], labels[idx_test])
136     print("Test set results:",
137           "loss= {:.4f}".format(loss_test.item()),
138           "accuracy= {:.4f}".format(acc_test.item()))
139 
140 
141 # Train model
142 # 逐个epoch进行train,最后test
143 t_total = time.time()
144 for epoch in range(args.epochs):
145     train(epoch) # 先训练
146 print("Optimization Finished!")
147 print("Total time elapsed: {:.4f}s".format(time.time() - t_total))
148 
149 # Testing
150 test() # 再测试
上一篇:pytorch 官网教程 [神经网络] 笔记


下一篇:macOS10.15 系统docker配置加速镜像