譯者:朱先忠
引言
深度學習為對非結構化數據進行預測開闢了一個全新的可能性世界。如今,人們常用卷積神經網絡(CNN)處理圖像數據,而採用遞歸神經網絡(RNN)來處理文本數據,等等。
在過去幾年中,又出現了一類新的令人興奮的神經網絡:圖神經網絡(Graph Neural Networks,簡稱“GNN”)。顧名思義,這個網絡類型專注於處理圖數據。
在這篇文章中,您將學習圖神經網絡如何工作的基礎知識,以及如何使用Pytorch Geometric(PyG)庫和Open Graph Benchmark(OGB)庫並通過Python編程實現這樣一個圖神經網絡。
注意,您可以在我的Github和Kaggle網站上找到本文提供的示例工程源碼。
普通GNN的工作原理
隨著圖卷積網絡(GCN)[見參考文獻1]的引入,GNN開始流行起來,該網絡將CNN中的一些概念借用到了圖世界。這種網絡的主要思想,也稱為消息傳遞框架(Message-Passing Framework),多年來成為該領域的黃金標準。我們將在本文中探討這一概念。
消息傳遞框架指出,對於圖中的每個節點,我們將做兩件事:
聚合來自其鄰節點的信息
使用來自其上一層及其鄰節點聚合的信息更新當前節點信息
上圖中顯示了消息傳遞框架的工作原理。在GCN之後開發的許多架構側重於定義聚合和更新數據的最佳方式。
PyG和OGB簡介
PyG是Pytorch庫的擴展,它允許我們使用研究中已經建立的層快速實現新的圖神經網絡架構。
OGB[見參考文獻2]是作為提高該領域研究質量的一種方式開發的,因為它提供了可使用的策劃圖,也是評估給定架構結果的標準方式,從而使提案之間的比較更加公平。
於是,我們可以將這兩個庫一起使用,一方面可以更容易地提出一個架構,另一方面也不必擔心數據獲取和評估機制的問題。
實現一個GNN項目
首先,讓我們安裝示例工程必需的庫。請注意,您必須首先安裝PyTorch:
複製pip install ogbpip install torch_geometric1.2.
現在,讓我們導入所需的方法和庫:
複製import osimport torchimport torch.nn.functional as Ffrom tqdm import tqdmfrom torch_geometric.loader import NeighborLoaderfrom torch.optim.lr_scheduler import ReduceLROnPlateaufrom torch_geometric.nn import MessagePassing, SAGEConvfrom ogb.nodeproppred import Evaluator, PygNodePropPredDataset1.2.3.4.5.6.7.
接下來,第一步是從OGB下載數據集。我們將使用ogbn-arxiv網絡,其中每個節點都是arxiv網站上的計算機科學論文,每個有向邊表示一篇論文引用了另一篇論文。我們的任務是:將每個節點分類為一個論文類別。
下載過程非常簡單:
複製target_dataset = "ogbn-arxiv"#我們將把ogbn-arxiv下載到當前示例工程的"networks"文件夾下dataset = PygNodePropPredDataset(name=target_dataset, root="networks")1.2.
其中,dataset變量是一個名為PygNodePropPredDataset的類的實例,該類特定於OGB庫。要將該數據集作為可在Pyrotch Geometric上使用的數據類進行訪問,我們只需執行以下操作:
複製data = dataset[0]1.
如果我們通過調試跟蹤看一下這個變量,我們會看到如下結果:
複製Data(num_nodes=169343, edge_index=[2, 1166243], x=[169343, 128], node_year=[169343, 1], y=[169343, 1])1.
至此,我們已經準備好了節點數目、鄰接列表、網絡的特徵向量、每個節點的年份信息,並確定下目標標籤。
另外,ogbn-arxiv網絡已經配備好了分別用於訓練、驗證和測試的分割數據子集。這是OGB提供的一種提高該網絡研究再現性和質量的好方法。我們可以通過以下方式提取:
複製split_idx = dataset.get_idx_split() train_idx = split_idx["train"]valid_idx = split_idx["valid"]test_idx = split_idx["test"]1.2.3.4.5.
現在,我們將定義兩個在訓練期間使用的數據加載器。第一個將僅加載訓練集中的節點,第二個將加載網絡上的所有節點。
我們將使用Pytorch Geometric庫中的鄰節點加載函數NeighborLoader。該數據加載器為每個節點採樣給定數量的鄰節點。這是一種避免具有數千個節點的節點的RAM和計算時間癱瘓的方法。在本教程中,我們將在訓練加載程序上每個節點使用30個鄰節點。
複製train_loader = NeighborLoader(data, input_nodes=train_idx, shuffle=True, num_workers=os.cpu_count() - 2, batch_size=1024, num_neighbors=[30] * 2)total_loader = NeighborLoader(data, input_nodes=None, num_neighbors=[-1], batch_size=4096, shuffle=False, num_workers=os.cpu_count() - 2)1.2.3.4.5.
注意,我們把訓練數據加載器中的數據以隨機方式打亂次序,但沒有打亂總加載器中數據的次序。此外,訓練加載程序的鄰節點數定義為網絡每層的數量。因為我們將在這裡使用兩層網絡,所以我們將其設置為兩個值為30的列表。
現在是時候創建我們的GNN架構了。對於任何熟悉Pytorch的人來說,這應該都是平常的事情。
我們將使用SAGE圖層。這些層是在一篇很好的論文[見參考文獻3]中定義的,該論文非常細緻地介紹了鄰節點採樣的思想。幸運的是,Pytorch Geometric 庫已經為我們實現了這一層。
因此,與每個PyTorch架構一樣,我們必須定義一個包含我們將要使用的層的類:
複製class SAGE(torch.nn.Module): def __init__(self, in_channels, hidden_channels, out_channels, n_layers=2): super(SAGE, self).__init__() self.n_layers = n_layers self.layers = torch.nn.ModuleList() self.layers_bn = torch.nn.ModuleList() if n_layers == 1: self.layers.append(SAGEConv(in_channels, out_channels, normalize=False)) elif n_layers == 2: self.layers.append(SAGEConv(in_channels, hidden_channels, normalize=False)) self.layers_bn.append(torch.nn.BatchNorm1d(hidden_channels)) self.layers.append(SAGEConv(hidden_channels, out_channels, normalize=False)) else: self.layers.append(SAGEConv(in_channels, hidden_channels, normalize=False)) self.layers_bn.append(torch.nn.BatchNorm1d(hidden_channels)) for _ in range(n_layers - 2): self.layers.append(SAGEConv(hidden_channels, hidden_channels, normalize=False)) self.layers_bn.append(torch.nn.BatchNorm1d(hidden_channels)) self.layers.append(SAGEConv(hidden_channels, out_channels, normalize=False)) for layer in self.layers: layer.reset_parameters() def forward(self, x, edge_index): if len(self.layers) > 1: looper = self.layers[:-1] else: looper = self.layers for i, layer in enumerate(looper): x = layer(x, edge_index) try: x = self.layers_bn[i](x) except Exception as e: abs(1) finally: x = F.relu(x) x = F.dropout(x, p=0.5, training=self.training) if len(self.layers) > 1: x = self.layers[-1](x, edge_index) return F.log_softmax(x, dim=-1), torch.var(x) def inference(self, total_loader, device): xs = [] var_ = [] for batch in total_loader: out, var = self.forward(batch.x.to(device), batch.edge_index.to(device)) out = out[:batch.batch_size] xs.append(out.cpu()) var_.append(var.item()) out_all = torch.cat(xs, dim=0) return out_all, var_1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.
讓我們一步一步地將上述代碼分開解釋:
我們必須定義網絡的in_channels數量,這個值代表數據集中的特徵數。out_channels代表我們試圖預測的類別的總數。隱藏通道參數idden_channels是一個我們可以定義的值,表示隱藏單元的數量。
我們可以設置網絡的層數。對於每個隱藏層,我們添加一個批量歸一化層,然後重置每個層的參數。
forward方法運行正向過程的單個迭代。期間,獲得特徵向量和鄰接列表,並將其傳遞給SAGE層,然後將結果傳遞給批量歸一化層。此外,我們還應用ReLU非線性和衰減層進行正則化。
最後,推理方法(inference)將為數據集中的每個節點生成預測。我們將使用它進行驗證。
現在,讓我們定義模型的一些參數:
複製device = torch.device("cuda" if torch.cuda.is_available() else "cpu")model = SAGE(data.x.shape[1], 256, dataset.num_classes, n_layers=2)model.to(device)epochs = 100optimizer = torch.optim.Adam(model.parameters(), lr=0.03)scheduler = ReduceLROnPlateau(optimizer, "max", patience=7)1.2.3.4.5.
現在,我們可以開始測試了,以驗證我們的所有預測:
複製def test(model, device): evaluator = Evaluator(name=target_dataset) model.eval() out, var = model.inference(total_loader, device) y_true = data.y.cpu() y_pred = out.argmax(dim=-1, keepdim=True) train_acc = evaluator.eval({ "y_true": y_true[split_idx["train"]], "y_pred": y_pred[split_idx["train"]], })["acc"] val_acc = evaluator.eval({ "y_true": y_true[split_idx["valid"]], "y_pred": y_pred[split_idx["valid"]], })["acc"] test_acc = evaluator.eval({ "y_true": y_true[split_idx["test"]], "y_pred": y_pred[split_idx["test"]], })["acc"]return train_acc, val_acc, test_acc, torch.mean(torch.Tensor(var))1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
在這個函數中,我們從OGB庫中實例化一個驗證器類Validator。這個類將負責驗證我們之前檢索到的每個分割的模型。這樣,我們將看到每個世代上的訓練、驗證和測試集的得分值。
最後,讓我們創建我們的訓練循環:
複製for epoch in range(1, epochs): model.train() pbar = tqdm(total=train_idx.size(0)) pbar.set_description(f"Epoch {epoch:02d}") total_loss = total_correct = 0 for batch in train_loader: batch_size = batch.batch_size optimizer.zero_grad() out, _ = model(batch.x.to(device), batch.edge_index.to(device)) out = out[:batch_size] batch_y = batch.y[:batch_size].to(device) batch_y = torch.reshape(batch_y, (-1,)) loss = F.nll_loss(out, batch_y) loss.backward() optimizer.step() total_loss += float(loss) total_correct += int(out.argmax(dim=-1).eq(batch_y).sum()) pbar.update(batch.batch_size) pbar.close() loss = total_loss / len(train_loader) approx_acc = total_correct / train_idx.size(0) train_acc, val_acc, test_acc, var = test(model, device) print(f"Train: {train_acc:.4f}, Val: {val_acc:.4f}, Test: {test_acc:.4f}, Var: {var:.4f}")1.2.3.4.5.6.7.8.9.10.11.12.13.14.
這個循環將訓練我們的GNN的100個世代,如果我們的驗證得分連續7個世代沒有增長的話,它將提前停止訓練。
結論
總之,GNN是一類有趣的神經網絡。今天,人們已經開發出了一些現成的工具來幫助我們開發這種解決方案。正如您在本文中所見到的,藉助Pytorch Geometric和OGB這兩個庫就可以輕鬆實現某些類型的圖的GNN設計。