GNN實驗(一)
GNN實驗
實驗一
論文:《Semi-Supervised Classification with Graph Convolutional Networks》
程式碼:https://github.com/tkipf/pygcn
資料集:Cora(主要利用論文之間的相互引用關係,預測論文的分類)
注意:之所以叫做半監督分類任務(Semi-Supervised Classification),這個半監督意思是,訓練的時候使用了未標記的資料,在這篇論文中未標記的資料的使用,體現在鄰接矩陣的使用上,從load_data函式的具體實現可以知道剛開始就構建了所有資料的鄰接矩陣,既有有label的也有希望test的(遮住label的)
程式碼講解
整體的程式碼結構
layers.py:定義了圖卷積層
models.py:模型的整體架構
train.py:資料集的載入、訓練、測試
utils.py:accuracy測試、載入資料函式封裝、其它
程式碼根據如下公式進行組織
\(Z=f(X,A)=softmax(\hat A ReLU(\hat AXW^0)W^1)\)
# nfeat : 輸入的維度 # nhid : 隱藏層的維度 # nclass: 預測的論文類別數 # x : 輸入 # adj : 經過處理的鄰接矩陣 gc1 = GraphConvolution(nfeat, nhid) gc2 = GraphConvolution(nhid, nclass) def forward(self, x, adj): x = F.relu(self.gc1(x, adj)) # 有一個小細節,如果要dropout生效,必須新增training=self.training x = F.dropout(x, self.dropout, training=self.training) x = self.gc2(x, adj) return F.log_softmax(x, dim=1)
圖卷積層的定義
def forward(self, input, adj):
# X * W^0
support = torch.mm(input, self.weight)
# A * X * W^0
output = torch.spmm(adj, support)#稀疏矩陣相乘
# 是否新增偏置
if self.bias is not None:
return output + self.bias
else:
return output
資料預處理
cora資料集由論文組成
cora.cites: 包含論文之間的引用關係
cora.content:包含論文的id,論文中包含的詞彙,論文的類別
for example:
cora.cites:
35 1033
35 103482
35 103515
35 1050679cora.content:
31336 (0 1 0......0) Neural_Networks
中間1433維,帶1的表示包含那個位置的語料,Neural_Networks 即為label
-
標籤one-hot編碼
def encode_onehot(labels): # 獲取論文標籤的類別集合,用set可以快速獲取 # 注意:標籤是中文的,不是直接給的數字,需要處理成數字 classes = set(labels) classes_dict = {c: np.identity(len(classes))[i, :] for i, c in enumerate(classes)} labels_onehot = np.array(list(map(classes_dict.get, labels)), dtype=np.int32) return labels_onehot # 提取原始資料的最後一行,也就是類別 labels = encode_onehot(idx_features_labels[:, -1]) labels = torch.LongTensor(np.where(labels)[1])
-
鄰接矩陣建立和處理
論文ID不是從0開始,於是重新將它編號
idx = np.array(idx_features_labels[:, 0], dtype=np.int32)# 提取index idx_map = {j: i for i, j in enumerate(idx)}# 從0開始編號
將cora.cites檔案中的論文ID替換
# 獲取邊 edges_unordered = np.genfromtxt("{}{}.cites".format(path, dataset),dtype=np.int32) # 重新標號,flatten方法使得資料格式能夠用map函式處理 edges =np.array(list(map(idx_map.get,edges_unordered.flatten())),dtype=np.int32).reshape(edges_unordered.shape)
準備工作完成,可以構造鄰接矩陣了
''' 引數說明: coo_matrix(data,(row,col),shape) np.ones(edges.shape[0]) -------> 邊的數量為edges.shape[0],鄰接矩陣中有邊的位置填充為1 (edges[:, 0], edges[:, 1]) ------> (row,col) ''' # 此處作為稀疏矩陣儲存,佔的空間少一點 adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), shape=(labels.shape[0], labels.shape[0]), dtype=np.float32) # 根據其它博主的說法,下面的語句和adj = adj + adj.T.multiply(adj.T > adj) 意思和作用是一樣的,可能作者在實現的時候沒考慮到? adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)
根據以下公式,對鄰接矩陣進行處理,也就是文中提到的renormalization trick
\(I_N+D^{-\frac{1}{2}}AD^{-\frac{1}{2}} -----> \tilde D^{-\frac{1}{2}}\tilde A\tilde D^{-\frac{1}{2}}\)
其中\(I_N\)是單位矩陣,\(\tilde A = A + I_N,\tilde D_{ii} = \sum_j\tilde A_{ij}\)
def normalize(mx): """Row-normalize sparse matrix""" # 將每一行求和 rowsum = np.array(mx.sum(1)) # 將每一行的和作為分母 r_inv = np.power(rowsum, -1).flatten() # 0的倒數為無窮大,因此需要剔除為0 r_inv[np.isinf(r_inv)] = 0. # 對角線矩陣,對角線上的元素是上面的r_inv r_mat_inv = sp.diags(r_inv) # 矩陣點乘,也就是除以r_inv mx = r_mat_inv.dot(mx) return mx # 在原先的鄰接矩陣上對角線填充為1,相當於一個自環操作 # 然後標準化就可以了 # 為什麼不乘D?因為直接矩陣內部歸一化和這個操作是等價的(沒試驗過,可以自行進行計算驗證) adj = normalize(adj + sp.eye(adj.shape[0]))
訓練
補充:
torch.max()[0]
, 只返回最大值的每個數troch.max()[1]
, 只返回最大值的每個索引torch.max()[1].data
只返回variable中的資料部分(去掉Variable containing:)torch.max()[1].data.numpy()
把資料轉化成numpy ndarrytorch.max()[1].data.numpy().squeeze()
把資料條目中維度為1 的刪除掉
def accuracy(output, labels):
preds = output.max(1)[1].type_as(labels)
correct = preds.eq(labels).double()
correct = correct.sum()
return correct / len(labels)
model.train()
optimizer.zero_grad()
output = model(features, adj)
loss_train = F.nll_loss(output[idx_train], labels[idx_train])# 全稱為the negative log likelihood loss
acc_train = accuracy(output[idx_train], labels[idx_train])
loss_train.backward()
optimizer.step()
訓練結果
Epoch: 0190 loss_train: 0.4485 acc_train: 0.9143 loss_val: 0.7083 acc_val: 0.8067 time: 0.0070s
Epoch: 0191 loss_train: 0.4087 acc_train: 0.9286 loss_val: 0.7086 acc_val: 0.8067 time: 0.0120s
Epoch: 0192 loss_train: 0.4215 acc_train: 0.9357 loss_val: 0.7085 acc_val: 0.8100 time: 0.0080s
Epoch: 0193 loss_train: 0.4282 acc_train: 0.9643 loss_val: 0.7078 acc_val: 0.8100 time: 0.0080s
Epoch: 0194 loss_train: 0.4115 acc_train: 0.9214 loss_val: 0.7078 acc_val: 0.8133 time: 0.0060s
Epoch: 0195 loss_train: 0.4394 acc_train: 0.9357 loss_val: 0.7080 acc_val: 0.8100 time: 0.0060s
Epoch: 0196 loss_train: 0.4254 acc_train: 0.9214 loss_val: 0.7080 acc_val: 0.8100 time: 0.0070s
Epoch: 0197 loss_train: 0.4243 acc_train: 0.9286 loss_val: 0.7076 acc_val: 0.8067 time: 0.0060s
Epoch: 0198 loss_train: 0.3971 acc_train: 0.9286 loss_val: 0.7070 acc_val: 0.8067 time: 0.0100s
Epoch: 0199 loss_train: 0.4467 acc_train: 0.9357 loss_val: 0.7059 acc_val: 0.8133 time: 0.0060s
Epoch: 0200 loss_train: 0.4267 acc_train: 0.9214 loss_val: 0.7042 acc_val: 0.8133 time: 0.0060s
Test set results: loss= 0.7397 accuracy= 0.8410
能夠達到論文中80多的正確率