GCN-图卷积神经网络算法简单实现(含python代码)
本文是就實現(xiàn)GCN算法模型進(jìn)行的代碼介紹,上一篇文章是GCN算法的原理和模型介紹。
代碼中用到的Cora數(shù)據(jù)集:
鏈接:https://pan.baidu.com/s/1SbqIOtysKqHKZ7C50DM_eA?
提取碼:pfny?
文章目錄
目的
一、數(shù)據(jù)集介紹
二、實現(xiàn)過程講解
三、代碼實現(xiàn)和結(jié)果分析
1. 導(dǎo)入包
2. 數(shù)據(jù)準(zhǔn)備?
3.?圖卷積層定義
4. GCN圖卷積神經(jīng)網(wǎng)絡(luò)模型定義
5.?模型訓(xùn)練
5.1 超參數(shù)定義,包含學(xué)習(xí)率、正則化系數(shù)等。
5.2 定義模型:
5.3 定義訓(xùn)練和測試函數(shù),進(jìn)行訓(xùn)練
6. 可視化
目的
本次實驗的目的是將論文分類,通過模型訓(xùn)練,利用已經(jīng)分好類的訓(xùn)練集,將論文通過GCN算法分為7類。
一、數(shù)據(jù)集介紹
數(shù)據(jù)集我選用的是GCN常用的Cora數(shù)據(jù)集,實驗的目標(biāo)就是通過對構(gòu)造出來的兩層GCN模型進(jìn)行訓(xùn)練,實現(xiàn)對數(shù)據(jù)集樣本節(jié)點(diǎn)的分類
Cora數(shù)據(jù)集下載地址:https://linqs-data.soe.ucsc.edu/public/lbc/cora.tgz
個人不建議用python的dgl包中的Cora數(shù)據(jù),總是報錯。
Cora數(shù)據(jù)集由關(guān)于機(jī)器學(xué)習(xí)方面的論文組成。 這些論文分為以下七個類別之一:
1.基于案例
2.遺傳算法
3.神經(jīng)網(wǎng)絡(luò)
4.概率方法
5.強(qiáng)化學(xué)習(xí)
6.規(guī)則學(xué)習(xí)
7.理論
這些論文都是經(jīng)過篩選的,在最終的數(shù)據(jù)集中,每篇論文引用或被至少一篇其他論文引用。整個語料庫中有2708篇論文。
在詞干堵塞和去除詞尾后,只剩下1433個唯一的單詞。文檔頻率小于10的所有單詞都被刪除。
即Cora數(shù)據(jù)集包含2708個頂點(diǎn), 5429條邊,每個頂點(diǎn)包含1433個特征,共有7個類別。
并且Cora已經(jīng)把訓(xùn)練集和測試集的數(shù)據(jù)都劃分好了,直接按照文件名讀取數(shù)據(jù)即可,如
文件ind.cora.x => 訓(xùn)練實例的特征向量;ind.cora.y => 訓(xùn)練實例的標(biāo)簽,獨(dú)熱編碼
ind.cora.tx => 測試實例的特征向量;ind.cora.ty => 測試實例的標(biāo)簽,獨(dú)熱編碼
二、實現(xiàn)過程講解
結(jié)合我最后做的代碼實現(xiàn),給大家先舉一個引文網(wǎng)絡(luò)的簡單實例,方便大家了解處理過程。
其中每個節(jié)點(diǎn)代表一篇研究論文,同時邊代表的是引用關(guān)系。
我們在這里有一個預(yù)處理步驟。在這里我們不使用原始論文作為特征,而是將論文轉(zhuǎn)換成向量(通過使用NLP嵌入,例如tf-idf)。
假設(shè)我們使用average()函數(shù)(實際上GCN內(nèi)部的傳遞函數(shù)肯定不是平均值,這里只是方便理解)。我們將對所有的節(jié)點(diǎn)進(jìn)行同樣的獲取特征向量的操作。最后,我們將這些計算得到的平均值輸入到神經(jīng)網(wǎng)絡(luò)中。
讓我們考慮下綠色節(jié)點(diǎn)。首先,我們得到它的所有鄰居的特征值,包括自身節(jié)點(diǎn),接著取平均值。最后通過神經(jīng)網(wǎng)絡(luò)返回一個結(jié)果向量并將此作為最終結(jié)果。請注意,在GCN中,我們僅僅使用一個全連接層。在這個例子中,我們得到2維向量作為輸出(全連接層的2個節(jié)點(diǎn))。
全連接網(wǎng)絡(luò)的作用就是對上一層得到的向量做乘法,最終降低其維度,然后輸入到softmax層中得到對應(yīng)的每個類別的得分。
在實際操作中,我們肯定是使用比average函數(shù)更復(fù)雜的聚合函數(shù),也就是上面講的那個傳播函數(shù)。
我們還可以將更多的層疊加在一起,以獲得更深的GCN。其中每一層的輸出會被視為下一層的輸入。
2層GCN的例子:第一層的輸出是第二層的輸入。
那么兩層的GCN就可以在降維的同時,通過層間傳播的公式獲取到二階鄰居節(jié)點(diǎn)的特征:
?在節(jié)點(diǎn)分類問題中,實際上在輸入的鄰接矩陣和每個節(jié)點(diǎn)的特征中,既包含了節(jié)點(diǎn)間的聯(lián)系情況,也包含了節(jié)點(diǎn)自身的特征。
通過GCN的卷積層就可以實現(xiàn)降維,想要聚成幾類就降成幾維。
三、代碼實現(xiàn)和結(jié)果分析
1. 導(dǎo)入包
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. 數(shù)據(jù)準(zhǔn)備?
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數(shù)據(jù),包括數(shù)據(jù)下載,處理,加載等功能當(dāng)數(shù)據(jù)的緩存文件存在時,將使用緩存文件,否則將下載、進(jìn)行處理,并緩存到磁盤處理之后的數(shù)據(jù)可以通過屬性 .data 獲得,它將返回一個數(shù)據(jù)對象,包括如下幾部分:* x: 節(jié)點(diǎn)的特征,維度為 2708 * 1433,類型為 np.ndarray* y: 節(jié)點(diǎn)的標(biāo)簽,總共包括7個類別,類型為 np.ndarray* adjacency: 鄰接矩陣,維度為 2708 * 2708,類型為 scipy.sparse.coo.coo_matrix* train_mask: 訓(xùn)練集掩碼向量,維度為 2708,當(dāng)節(jié)點(diǎn)屬于訓(xùn)練集時,相應(yīng)位置為True,否則False* val_mask: 驗證集掩碼向量,維度為 2708,當(dāng)節(jié)點(diǎn)屬于驗證集時,相應(yīng)位置為True,否則False* test_mask: 測試集掩碼向量,維度為 2708,當(dāng)節(jié)點(diǎn)屬于測試集時,相應(yīng)位置為True,否則FalseArgs:-------data_root: string, optional存放數(shù)據(jù)的目錄,原始數(shù)據(jù)路徑: ../data/cora緩存數(shù)據(jù)路徑: {data_root}/ch5_cached.pklrebuild: boolean, optional是否需要重新構(gòu)建數(shù)據(jù)集,當(dāng)設(shè)為True時,如果存在緩存數(shù)據(jù)也會重建數(shù)據(jù)"""self.data_root = data_root #數(shù)據(jù)存放的路徑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數(shù)據(jù)對象,包括x, y, adjacency, train_mask, val_mask, test_mask"""return self._datadef process_data(self):"""處理數(shù)據(jù),得到節(jié)點(diǎn)特征和標(biāo)簽,鄰接矩陣,訓(xùn)練集、驗證集以及測試集引用自: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) #節(jié)點(diǎn)特征y = np.concatenate((ally, ty), axis=0).argmax(axis=1) #標(biāo)簽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) #訓(xùn)練集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""""構(gòu)建鄰接矩陣"""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):"""根據(jù)鄰接表創(chuàng)建鄰接矩陣"""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)# 去除重復(fù)的邊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):"""使用不同的方式讀取原始數(shù)據(jù)以進(jìn)一步處理"""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節(jié)點(diǎn)輸入特征的維度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_神經(jīng)網(wǎng)絡(luò)權(quán)重初始化,神經(jīng)網(wǎng)絡(luò)要優(yōu)化一個非常復(fù)雜的非線性模型,而且基本沒有全局最優(yōu)解,#初始化在其中扮演著非常重要的作用,尤其在沒有BN等技術(shù)的早期,它直接影響模型能否收斂。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圖卷積神經(jīng)網(wǎng)絡(luò)模型定義
有了數(shù)據(jù)和GCN層,就可以構(gòu)建模型進(jìn)行訓(xùn)練了。
定義一個兩層的GCN,其中輸入的維度為1433,隱藏層維度設(shè)為16,最后一層GCN將輸出維度變?yōu)轭悇e數(shù)7,激活函數(shù)使用的是ReLU。
?
5.?模型訓(xùn)練
5.1 超參數(shù)定義,包含學(xué)習(xí)率、正則化系數(shù)等。
LEARNING_RATE = 0.1 #學(xué)習(xí)率 學(xué)習(xí)率過小→ →→收斂過慢,學(xué)習(xí)率過大→ →→錯過局部最優(yōu); WEIGHT_DACAY = 5e-4 #正則化系數(shù) weight_dacay,解決過擬合問題 EPOCHS = 200 #完整遍歷訓(xùn)練集的次數(shù) DEVICE = "cuda" if torch.cuda.is_available() else "cpu" #指定設(shè)備,如果當(dāng)前顯卡忙于其他工作,可以設(shè)置為 DEVICE = "cpu",使用cpu運(yùn)行為什么要訓(xùn)練200輪呢,因為我們最開始是不知道邊的權(quán)重的,需要通過模型訓(xùn)練出來合適的權(quán)重,也就是公式中的W。
# 加載數(shù)據(jù),并轉(zhuǎn)換為torch.Tensor dataset = CoraData().data node_feature = dataset.x / dataset.x.sum(1, keepdims=True) # 歸一化數(shù)據(jù),使得每一行和為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) # 規(guī)范化鄰接矩陣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()函數(shù)計算交叉熵?fù)p失 optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DACAY)其中在定義模型時,還順手定義了criterion,即在訓(xùn)練過程中可以用nn.CrossEntropyLoss()函數(shù)計算交叉熵?fù)p失:
?
5.3 定義訓(xùn)練和測試函數(shù),進(jìn)行訓(xùn)練
# 訓(xùn)練主體函數(shù) def train():loss_history = []val_acc_history = []model.train()train_y = tensor_y[tensor_train_mask]for epoch in range(EPOCHS):# 共進(jìn)行200次訓(xùn)練logits = model(tensor_adjacency, tensor_x) # 前向傳播#其中l(wèi)ogits是模型輸出,tensor_adjacency, tensor_x分別是鄰接矩陣和節(jié)點(diǎn)特征。train_mask_logits = logits[tensor_train_mask] # 只選擇訓(xùn)練節(jié)點(diǎn)進(jìn)行監(jiān)督loss = criterion(train_mask_logits, train_y) # 計算損失值,目的是優(yōu)化模型,獲得更科學(xué)的權(quán)重Woptimizer.zero_grad()loss.backward() # 反向傳播計算參數(shù)的梯度optimizer.step() # 使用優(yōu)化方法進(jìn)行梯度更新train_acc, _, _ = test(tensor_train_mask) # 計算當(dāng)前模型訓(xùn)練集上的準(zhǔn)確率val_acc, _, _ = test(tensor_val_mask) # 計算當(dāng)前模型在驗證集上的準(zhǔn)確率# 記錄訓(xùn)練過程中損失值和準(zhǔn)確率的變化,用于畫圖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# 測試函數(shù) def test(mask):model.eval() # 表示將模型轉(zhuǎn)變?yōu)閑valuation(測試)模式,這樣就可以排除BN和Dropout對測試的干擾with torch.no_grad(): # 顯著減少顯存占用logits = model(tensor_adjacency, tensor_x) #(N,16)->(N,7) N節(jié)點(diǎn)數(shù)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()?
使用上述代碼進(jìn)行模型訓(xùn)練,可以看到如下代碼所示的日志輸出:
loss, val_acc = train() test_acc, test_logits, test_label = test(tensor_test_mask) print("Test accuarcy: ", test_acc.item())#item()返回的是一個浮點(diǎn)型數(shù)據(jù),測試集準(zhǔn)確率?
其中Epoch為訓(xùn)練輪數(shù);loss是損失值;TrainAcc訓(xùn)練集準(zhǔn)確率;ValAcc測試集上的準(zhǔn)確率;
?
6. 可視化
將損失值和驗證集準(zhǔn)確率的變化趨勢可視化:
損失函數(shù)用來測度模型的輸出值和真實因變量值之間的差異
def plot_loss_with_acc(loss_history, val_acc_history):fig = plt.figure()# 坐標(biāo)系ax1畫曲線1ax1 = fig.add_subplot(111) # 指的是將plot界面分成1行1列,此子圖占據(jù)從左到右從上到下的1位置ax1.plot(range(len(loss_history)), loss_history,c=np.array([255, 71, 90]) / 255.) # c為顏色plt.ylabel('Loss')# 坐標(biāo)系ax2畫曲線2ax2 = fig.add_subplot(111, sharex=ax1, frameon=False) # 其本質(zhì)就是添加坐標(biāo)系,設(shè)置共享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坐標(biāo)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)?
可以看到紅線代表的損失值隨著訓(xùn)練次數(shù)的增加越來越小,藍(lán)線代表的模型準(zhǔn)確率越來越高。
將最后一層得到的輸出進(jìn)行TSNE降維,(TSNE)t分布隨機(jī)鄰域嵌入 是一種用于探索高維數(shù)據(jù)的非線性降維算法。
它將多維數(shù)據(jù)映射到適合于人類觀察的兩個或多個維度。
得到如下圖所示的分類結(jié)果:
繪制測試數(shù)據(jù)的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()?
根據(jù)上述結(jié)果:我們通過圖卷積神經(jīng)網(wǎng)絡(luò)算法,可以成功將論文集劃分為較為鮮明的7類,這與論文集原本的種類劃分基本一致,效果還是較為可觀的。
總結(jié)
以上是生活随笔為你收集整理的GCN-图卷积神经网络算法简单实现(含python代码)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Chrome 广告屏蔽功能不影响浏览器性
- 下一篇: websocket python爬虫_p