1. 程式人生 > 其它 >GNN實驗(一)

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 1050679

cora.content:

​ 31336 (0 1 0......0) Neural_Networks

中間1433維,帶1的表示包含那個位置的語料,Neural_Networks 即為label

  1. 標籤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])
    
  2. 鄰接矩陣建立和處理

    論文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 ndarry
torch.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多的正確率