GCN-图卷积神经网络算法简单实现(含python代码)
本文是就實現GCN算法模型進行的代碼介紹,上一篇文章是GCN算法的原理和模型介紹。
代碼中用到的Cora數據集:
鏈接:https://pan.baidu.com/s/1SbqIOtysKqHKZ7C50DM_eA?
 提取碼:pfny?
文章目錄
目的
一、數據集介紹
二、實現過程講解
三、代碼實現和結果分析
1. 導入包
2. 數據準備?
3.?圖卷積層定義
4. GCN圖卷積神經網絡模型定義
5.?模型訓練
5.1 超參數定義,包含學習率、正則化系數等。
5.2 定義模型:
5.3 定義訓練和測試函數,進行訓練
6. 可視化
目的
本次實驗的目的是將論文分類,通過模型訓練,利用已經分好類的訓練集,將論文通過GCN算法分為7類。
一、數據集介紹
數據集我選用的是GCN常用的Cora數據集,實驗的目標就是通過對構造出來的兩層GCN模型進行訓練,實現對數據集樣本節點的分類
Cora數據集下載地址:https://linqs-data.soe.ucsc.edu/public/lbc/cora.tgz
個人不建議用python的dgl包中的Cora數據,總是報錯。
Cora數據集由關于機器學習方面的論文組成。 這些論文分為以下七個類別之一:
1.基于案例
2.遺傳算法
3.神經網絡
4.概率方法
5.強化學習
6.規則學習
7.理論
這些論文都是經過篩選的,在最終的數據集中,每篇論文引用或被至少一篇其他論文引用。整個語料庫中有2708篇論文。
在詞干堵塞和去除詞尾后,只剩下1433個唯一的單詞。文檔頻率小于10的所有單詞都被刪除。
即Cora數據集包含2708個頂點, 5429條邊,每個頂點包含1433個特征,共有7個類別。
并且Cora已經把訓練集和測試集的數據都劃分好了,直接按照文件名讀取數據即可,如
文件ind.cora.x => 訓練實例的特征向量;ind.cora.y => 訓練實例的標簽,獨熱編碼
ind.cora.tx => 測試實例的特征向量;ind.cora.ty => 測試實例的標簽,獨熱編碼
二、實現過程講解
結合我最后做的代碼實現,給大家先舉一個引文網絡的簡單實例,方便大家了解處理過程。
其中每個節點代表一篇研究論文,同時邊代表的是引用關系。
我們在這里有一個預處理步驟。在這里我們不使用原始論文作為特征,而是將論文轉換成向量(通過使用NLP嵌入,例如tf-idf)。
假設我們使用average()函數(實際上GCN內部的傳遞函數肯定不是平均值,這里只是方便理解)。我們將對所有的節點進行同樣的獲取特征向量的操作。最后,我們將這些計算得到的平均值輸入到神經網絡中。
讓我們考慮下綠色節點。首先,我們得到它的所有鄰居的特征值,包括自身節點,接著取平均值。最后通過神經網絡返回一個結果向量并將此作為最終結果。請注意,在GCN中,我們僅僅使用一個全連接層。在這個例子中,我們得到2維向量作為輸出(全連接層的2個節點)。
全連接網絡的作用就是對上一層得到的向量做乘法,最終降低其維度,然后輸入到softmax層中得到對應的每個類別的得分。
在實際操作中,我們肯定是使用比average函數更復雜的聚合函數,也就是上面講的那個傳播函數。
我們還可以將更多的層疊加在一起,以獲得更深的GCN。其中每一層的輸出會被視為下一層的輸入。
2層GCN的例子:第一層的輸出是第二層的輸入。
那么兩層的GCN就可以在降維的同時,通過層間傳播的公式獲取到二階鄰居節點的特征:
?在節點分類問題中,實際上在輸入的鄰接矩陣和每個節點的特征中,既包含了節點間的聯系情況,也包含了節點自身的特征。
通過GCN的卷積層就可以實現降維,想要聚成幾類就降成幾維。
三、代碼實現和結果分析
1. 導入包
import itertools import os import os.path as osp import pickle import urllib from collections import namedtuple import warnings warnings.filterwarnings("ignore") import numpy as np import scipy.sparse as sp import torch import torch.nn as nn import torch.nn.functional as F import torch.nn.init as init import torch.optim as optim import matplotlib.pyplot as plt %matplotlib inline2. 數據準備?
Data = namedtuple('Data', ['x', 'y', 'adjacency','train_mask', 'val_mask', 'test_mask'])def tensor_from_numpy(x, device):return torch.from_numpy(x).to(device)class CoraData(object):filenames = ["ind.cora.{}".format(name) for name in['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']]def __init__(self, data_root="./data", rebuild=False):"""Cora數據,包括數據下載,處理,加載等功能當數據的緩存文件存在時,將使用緩存文件,否則將下載、進行處理,并緩存到磁盤處理之后的數據可以通過屬性 .data 獲得,它將返回一個數據對象,包括如下幾部分:* x: 節點的特征,維度為 2708 * 1433,類型為 np.ndarray* y: 節點的標簽,總共包括7個類別,類型為 np.ndarray* adjacency: 鄰接矩陣,維度為 2708 * 2708,類型為 scipy.sparse.coo.coo_matrix* train_mask: 訓練集掩碼向量,維度為 2708,當節點屬于訓練集時,相應位置為True,否則False* val_mask: 驗證集掩碼向量,維度為 2708,當節點屬于驗證集時,相應位置為True,否則False* test_mask: 測試集掩碼向量,維度為 2708,當節點屬于測試集時,相應位置為True,否則FalseArgs:-------data_root: string, optional存放數據的目錄,原始數據路徑: ../data/cora緩存數據路徑: {data_root}/ch5_cached.pklrebuild: boolean, optional是否需要重新構建數據集,當設為True時,如果存在緩存數據也會重建數據"""self.data_root = data_root #數據存放的路徑save_file = osp.join(self.data_root, "ch5_cached.pkl")if osp.exists(save_file) and not rebuild:print("Using Cached file: {}".format(save_file))self._data = pickle.load(open(save_file, "rb"))else:self._data = self.process_data()with open(save_file, "wb") as f:pickle.dump(self.data, f)print("Cached file: {}".format(save_file))@propertydef data(self):"""返回Data數據對象,包括x, y, adjacency, train_mask, val_mask, test_mask"""return self._datadef process_data(self):"""處理數據,得到節點特征和標簽,鄰接矩陣,訓練集、驗證集以及測試集引用自:https://github.com/rusty1s/pytorch_geometric"""print("Process data ...")_, tx, allx, y, ty, ally, graph, test_index = [self.read_data(osp.join(self.data_root, name)) for name in self.filenames]train_index = np.arange(y.shape[0])val_index = np.arange(y.shape[0], y.shape[0] + 500)sorted_test_index = sorted(test_index)x = np.concatenate((allx, tx), axis=0) #節點特征y = np.concatenate((ally, ty), axis=0).argmax(axis=1) #標簽x[test_index] = x[sorted_test_index]y[test_index] = y[sorted_test_index]num_nodes = x.shape[0]train_mask = np.zeros(num_nodes, dtype=np.bool) #訓練集val_mask = np.zeros(num_nodes, dtype=np.bool) #驗證集test_mask = np.zeros(num_nodes, dtype=np.bool) #測試集train_mask[train_index] = Trueval_mask[val_index] = Truetest_mask[test_index] = True""""構建鄰接矩陣"""adjacency = self.build_adjacency(graph)print("Node's feature shape: ", x.shape)print("Node's label shape: ", y.shape)print("Adjacency's shape: ", adjacency.shape)print("Number of training nodes: ", train_mask.sum())print("Number of validation nodes: ", val_mask.sum())print("Number of test nodes: ", test_mask.sum())return Data(x=x, y=y, adjacency=adjacency,train_mask=train_mask, val_mask=val_mask, test_mask=test_mask)@staticmethoddef build_adjacency(adj_dict):"""根據鄰接表創建鄰接矩陣"""edge_index = []num_nodes = len(adj_dict)for src, dst in adj_dict.items():edge_index.extend([src, v] for v in dst)edge_index.extend([v, src] for v in dst)# 去除重復的邊edge_index = list(k for k, _ in itertools.groupby(sorted(edge_index)))edge_index = np.asarray(edge_index)adjacency = sp.coo_matrix((np.ones(len(edge_index)), (edge_index[:, 0], edge_index[:, 1])),shape=(num_nodes, num_nodes), dtype="float32")return adjacency@staticmethoddef read_data(path):"""使用不同的方式讀取原始數據以進一步處理"""name = osp.basename(path)if name == "ind.cora.test.index":out = np.genfromtxt(path, dtype="int64")return outelse:out = pickle.load(open(path, "rb"), encoding="latin1")out = out.toarray() if hasattr(out, "toarray") else outreturn out@staticmethoddef normalization(adjacency):"""計算 H=D^-0.5 * (A+I) * D^-0.5"""adjacency += sp.eye(adjacency.shape[0]) # 增加自連接degree = np.array(adjacency.sum(1))d_hat = sp.diags(np.power(degree, -0.5).flatten())return d_hat.dot(adjacency).dot(d_hat).tocoo()3.?圖卷積層定義
class GraphConvolution(nn.Module):def __init__(self, input_dim, output_dim, use_bias=True):"""圖卷積:H*X*\thetaArgs:----------input_dim: int節點輸入特征的維度output_dim: int輸出特征維度use_bias : bool, optional是否使用偏置"""super(GraphConvolution, self).__init__()self.input_dim = input_dimself.output_dim = output_dimself.use_bias = use_biasself.weight = nn.Parameter(torch.Tensor(input_dim, output_dim))if self.use_bias:self.bias = nn.Parameter(torch.Tensor(output_dim))else:self.register_parameter('bias', None)self.reset_parameters() #初始化wdef reset_parameters(self):init.kaiming_uniform_(self.weight) #init.kaiming_uniform_神經網絡權重初始化,神經網絡要優化一個非常復雜的非線性模型,而且基本沒有全局最優解,#初始化在其中扮演著非常重要的作用,尤其在沒有BN等技術的早期,它直接影響模型能否收斂。if self.use_bias:init.zeros_(self.bias)def forward(self, adjacency, input_feature):"""鄰接矩陣是稀疏矩陣,因此在計算時使用稀疏矩陣乘法Args: -------adjacency: torch.sparse.FloatTensor鄰接矩陣input_feature: torch.Tensor輸入特征"""support = torch.mm(input_feature, self.weight)output = torch.sparse.mm(adjacency, support)if self.use_bias:output += self.biasreturn outputdef __repr__(self):return self.__class__.__name__ + ' (' \+ str(self.input_dim) + ' -> ' \+ str(self.output_dim) + ')'4. GCN圖卷積神經網絡模型定義
有了數據和GCN層,就可以構建模型進行訓練了。
 定義一個兩層的GCN,其中輸入的維度為1433,隱藏層維度設為16,最后一層GCN將輸出維度變為類別數7,激活函數使用的是ReLU。
?
5.?模型訓練
5.1 超參數定義,包含學習率、正則化系數等。
LEARNING_RATE = 0.1 #學習率 學習率過小→ →→收斂過慢,學習率過大→ →→錯過局部最優; WEIGHT_DACAY = 5e-4 #正則化系數 weight_dacay,解決過擬合問題 EPOCHS = 200 #完整遍歷訓練集的次數 DEVICE = "cuda" if torch.cuda.is_available() else "cpu" #指定設備,如果當前顯卡忙于其他工作,可以設置為 DEVICE = "cpu",使用cpu運行為什么要訓練200輪呢,因為我們最開始是不知道邊的權重的,需要通過模型訓練出來合適的權重,也就是公式中的W。
# 加載數據,并轉換為torch.Tensor dataset = CoraData().data node_feature = dataset.x / dataset.x.sum(1, keepdims=True) # 歸一化數據,使得每一行和為1 tensor_x = tensor_from_numpy(node_feature, DEVICE) tensor_y = tensor_from_numpy(dataset.y, DEVICE) tensor_train_mask = tensor_from_numpy(dataset.train_mask, DEVICE) tensor_val_mask = tensor_from_numpy(dataset.val_mask, DEVICE) tensor_test_mask = tensor_from_numpy(dataset.test_mask, DEVICE) normalize_adjacency = CoraData.normalization(dataset.adjacency) # 規范化鄰接矩陣num_nodes, input_dim = node_feature.shape indices = torch.from_numpy(np.asarray([normalize_adjacency.row, normalize_adjacency.col]).astype('int64')).long() values = torch.from_numpy(normalize_adjacency.data.astype(np.float32)) tensor_adjacency = torch.sparse.FloatTensor(indices, values, (num_nodes, num_nodes)).to(DEVICE)5.2 定義模型:
# 模型定義:Model, Loss, Optimizer model = GcnNet(input_dim).to(DEVICE) criterion = nn.CrossEntropyLoss().to(DEVICE) #nn.CrossEntropyLoss()函數計算交叉熵損失 optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DACAY)其中在定義模型時,還順手定義了criterion,即在訓練過程中可以用nn.CrossEntropyLoss()函數計算交叉熵損失:
?
5.3 定義訓練和測試函數,進行訓練
# 訓練主體函數 def train():loss_history = []val_acc_history = []model.train()train_y = tensor_y[tensor_train_mask]for epoch in range(EPOCHS):# 共進行200次訓練logits = model(tensor_adjacency, tensor_x) # 前向傳播#其中logits是模型輸出,tensor_adjacency, tensor_x分別是鄰接矩陣和節點特征。train_mask_logits = logits[tensor_train_mask] # 只選擇訓練節點進行監督loss = criterion(train_mask_logits, train_y) # 計算損失值,目的是優化模型,獲得更科學的權重Woptimizer.zero_grad()loss.backward() # 反向傳播計算參數的梯度optimizer.step() # 使用優化方法進行梯度更新train_acc, _, _ = test(tensor_train_mask) # 計算當前模型訓練集上的準確率val_acc, _, _ = test(tensor_val_mask) # 計算當前模型在驗證集上的準確率# 記錄訓練過程中損失值和準確率的變化,用于畫圖loss_history.append(loss.item())val_acc_history.append(val_acc.item())print("Epoch {:03d}: Loss {:.4f}, TrainAcc {:.4}, ValAcc {:.4f}".format(epoch, loss.item(), train_acc.item(), val_acc.item()))return loss_history, val_acc_history# 測試函數 def test(mask):model.eval() # 表示將模型轉變為evaluation(測試)模式,這樣就可以排除BN和Dropout對測試的干擾with torch.no_grad(): # 顯著減少顯存占用logits = model(tensor_adjacency, tensor_x) #(N,16)->(N,7) N節點數test_mask_logits = logits[mask] # 矩陣形狀和mask一樣predict_y = test_mask_logits.max(1)[1] # 返回每一行的最大值中索引(返回最大元素在各行的列索引)accuarcy = torch.eq(predict_y, tensor_y[mask]).float().mean()return accuarcy, test_mask_logits.cpu().numpy(), tensor_y[mask].cpu().numpy()?
使用上述代碼進行模型訓練,可以看到如下代碼所示的日志輸出:
loss, val_acc = train() test_acc, test_logits, test_label = test(tensor_test_mask) print("Test accuarcy: ", test_acc.item())#item()返回的是一個浮點型數據,測試集準確率?
其中Epoch為訓練輪數;loss是損失值;TrainAcc訓練集準確率;ValAcc測試集上的準確率;
?
6. 可視化
將損失值和驗證集準確率的變化趨勢可視化:
損失函數用來測度模型的輸出值和真實因變量值之間的差異
def plot_loss_with_acc(loss_history, val_acc_history):fig = plt.figure()# 坐標系ax1畫曲線1ax1 = fig.add_subplot(111) # 指的是將plot界面分成1行1列,此子圖占據從左到右從上到下的1位置ax1.plot(range(len(loss_history)), loss_history,c=np.array([255, 71, 90]) / 255.) # c為顏色plt.ylabel('Loss')# 坐標系ax2畫曲線2ax2 = fig.add_subplot(111, sharex=ax1, frameon=False) # 其本質就是添加坐標系,設置共享ax1的x軸,ax2背景透明ax2.plot(range(len(val_acc_history)), val_acc_history,c=np.array([79, 179, 255]) / 255.)ax2.yaxis.tick_right() # 開啟右邊的y坐標ax2.yaxis.set_label_position("right")plt.ylabel('ValAcc')plt.xlabel('Epoch')plt.title('Training Loss & Validation Accuracy')plt.show()plot_loss_with_acc(loss, val_acc)?
可以看到紅線代表的損失值隨著訓練次數的增加越來越小,藍線代表的模型準確率越來越高。
將最后一層得到的輸出進行TSNE降維,(TSNE)t分布隨機鄰域嵌入 是一種用于探索高維數據的非線性降維算法。
它將多維數據映射到適合于人類觀察的兩個或多個維度。
得到如下圖所示的分類結果:
繪制測試數據的TSNE降維圖:
from sklearn.manifold import TSNE tsne = TSNE() out = tsne.fit_transform(test_logits) fig = plt.figure() for i in range(7):indices = test_label == ix, y = out[indices].Tplt.scatter(x, y, label=str(i)) plt.legend()?
根據上述結果:我們通過圖卷積神經網絡算法,可以成功將論文集劃分為較為鮮明的7類,這與論文集原本的種類劃分基本一致,效果還是較為可觀的。
總結
以上是生活随笔為你收集整理的GCN-图卷积神经网络算法简单实现(含python代码)的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: Chrome 广告屏蔽功能不影响浏览器性
 - 下一篇: [G+smo][Test]gsView