本文非原創,主要參考學習博文:
說明:本文是對論文“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格式如下:
執行完該程序后,輸出的獨熱碼為:(獨熱碼這個概念並不復雜,就是分類標記)
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)
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)
歸一化后的輸出結果test為:
可以看到,在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停止工作,這樣可以使模型泛化性更強,因為它不會太依賴某些局部的特征,如圖所示:
五、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() # 再測試