PYG教程【五】链路预测
鏈路預測是網絡科學里面的一個經典任務,其目的是利用當前已獲取的網絡數據(包含結構信息和屬性信息)來預測網絡中會出現哪些新的連邊。
本文計劃利用networkx包中的網絡來進行鏈路預測,因為目前PyTorch Geometric包中封裝的網絡還不夠多,而很多網絡方便用networkx包生成或者處理。
環境配置
首先,安裝一個工具包,DeepSNAP。這個包提供了networkx到PyTorch Geometric的接口,可以方便地將networkx中的網絡轉換成PyTorch Geometric所要求的數據格式。DeepSNAP有兩種安裝方法:
第一種安裝方法:
pip install deepsnap第二種安裝方法:
$ git clone https://github.com/snap-stanford/deepsnap $ cd deepsnap $ pip install .鏈路預測
使用圖神經網絡進行鏈路預測包含以下基本步驟:
鏈路預測最開始是一個無監督學習任務,即根據已經看到的網絡結構(或者其他屬性信息)來推斷未知連邊是否存在,但是這樣的話就比較難以驗證。只有在動態網絡(或稱時序網絡)中才會有這樣的數據以供實驗驗證,可以用前一段時間的網絡結構來預測后一段時間的網絡結構。然而,很多網絡沒有時間信息,在這樣的網絡中如何驗證呢?
后來,學者提出了用有監督的方式來進行鏈路預測,也就是將其視為二分類任務,將網絡中存在的邊都視為正樣本(即正邊),不存在的連邊都當作負樣本(即負邊)。然后,將這些邊分為兩部分,一部分為訓練集,一部分為測試集。訓練集和測試集中都包含正邊和負邊,目的是在訓練集上訓練出一個模型能夠準確分類這兩種邊,然后再在測試集上驗證效果。
然而,大多數網絡都是稀疏的,也就是說存在邊的數量差不多是節點數量的幾倍左右,而網絡中不存在的邊的數量差不多是節點數量的平方(在無向網絡中,不存在邊的數量等于(n?1)n/2?m(n?1)n/2?m(n?1)n/2?m( n ? 1 ) n / 2 ? m (n-1)n/2-m(n?1)n/2?m(n?1)n/2?m(n?1)n/2?m(n?1)n/2?m,其中nnn為節點數,mmm 為邊數)。這樣不存邊的數量就遠遠大于存在邊的數量,在有監督學習中就意味著負樣本遠大于正樣本,類別極其不平衡。怎么解決這個問題呢?大家很自然地想到了負采樣,就是每次訓練的時候隨機抽取與正樣本等比例的負樣本,這樣就避免了類別不平衡。
訓練結束后,就可以用測試集中的正邊和負邊來驗證模型的效果了。
代碼
import networkx as nx from deepsnap.graph import Graph import torch import torch.nn.functional as F from sklearn.metrics import roc_auc_score from torch_geometric.utils import negative_sampling from torch_geometric.nn import GCNConv from torch_geometric.utils import train_test_split_edgesG = nx.karate_club_graph() data = Graph(G) # 將networkx中的graph對象轉化為torch_geometric的Data對象 data.num_features = 3 data.edge_attr = None# 構造節點特征矩陣(原網絡不存在節點特征) data.x = torch.ones((data.num_nodes, data.num_features), dtype=torch.float32)# 分割訓練邊集、驗證邊集(默認占比0.05)以及測試邊集(默認占比0.1) data = train_test_split_edges(data)# 構造一個簡單的圖卷積神經網絡(兩層),包含編碼(節點嵌入)、解碼(分數預測)等操作 class Net(torch.nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = GCNConv(data.num_features, 128)self.conv2 = GCNConv(128, 64)def encode(self):x = self.conv1(data.x, data.train_pos_edge_index)x = x.relu()return self.conv2(x, data.train_pos_edge_index)def decode(self, z, pos_edge_index, neg_edge_index):edge_index = torch.cat([pos_edge_index, neg_edge_index], dim=-1) # 將正樣本與負樣本拼接 shape:[2,272]logits = (z[edge_index[0]] * z[edge_index[1]]).sum(dim=-1)return logitsdef decode_all(self, z):prob_adj = z @ z.t()return (prob_adj > 0).nonzero(as_tuple=False).t()# 將模型和數據送入設備 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model, data = Net().to(device), data.to(device) # 指定優化器 optimizer = torch.optim.Adam(params=model.parameters(), lr=0.01)# 將訓練集中的正邊標簽設置為1,負邊標簽設置為0 def get_link_labels(pos_edge_index, neg_edge_index):E = pos_edge_index.size(1) + neg_edge_index.size(1)link_labels = torch.zeros(E, dtype=torch.float, device=device)link_labels[:pos_edge_index.size(1)] = 1.return link_labels# 訓練函數,每次訓練重新采樣負邊,計算模型損失,反向傳播誤差,更新模型參數 def train():model.train()neg_edge_index = negative_sampling(edge_index=data.train_pos_edge_index, num_nodes=data.num_nodes,num_neg_samples=data.train_pos_edge_index.size(1), # 負采樣數量根據正樣本force_undirected=True,) # 得到負采樣shape: [2,136]neg_edge_index = neg_edge_index.to(device)optimizer.zero_grad()z = model.encode() # 利用正樣本訓練學習得到每個節點的特征 shape:[34, 64]link_logits = model.decode(z, data.train_pos_edge_index, neg_edge_index) # [272] 利用正樣本和負樣本 按位相乘 求和 (z[edge_index[0]] * z[edge_index[1]]).sum(dim=-1)link_labels = get_link_labels(data.train_pos_edge_index, neg_edge_index) # [272] 前136個是1,后136個是0loss = F.binary_cross_entropy_with_logits(link_logits, link_labels) # binary_cross_entropy_with_logits會自動計算link_logits的sigmoidloss.backward()optimizer.step()return loss# 測試函數,評估模型在驗證集和測試集上的預測準確率 @torch.no_grad() def test():model.eval()perfs = []for prefix in ["val", "test"]:pos_edge_index = data[f'{prefix}_pos_edge_index']neg_edge_index = data[f'{prefix}_neg_edge_index']z = model.encode()link_logits = model.decode(z, pos_edge_index, neg_edge_index)link_probs = link_logits.sigmoid()link_labels = get_link_labels(pos_edge_index, neg_edge_index)perfs.append(roc_auc_score(link_labels.cpu(), link_probs.cpu()))return perfs# 訓練模型,每次訓練完,輸出模型在驗證集和測試集上的預測準確率 best_val_perf = test_perf = 0 for epoch in range(1, 11):train_loss = train()val_perf, tmp_test_perf = test()if val_perf > best_val_perf:best_val_perf = val_perftest_perf = tmp_test_perflog = 'Epoch: {:03d}, Loss: {:.4f}, Val: {:.4f}, Test: {:.4f}'print(log.format(epoch, train_loss, best_val_perf, test_perf))# 利用訓練好的模型計算網絡中剩余所有邊的分數 z = model.encode() final_edge_index = model.decode_all(z)首先查看原始數據信息:
data: Graph(G=[], club=[34], # 總共34個節點edge_label_index=[2, 156], # 總共156個邊name=[], node_label_index=[34], num_features=[1], test_neg_edge_index=[2, 7], # 測試集 負樣本鄰接矩陣test_pos_edge_index=[2, 7], # 測試集 正樣本鄰接矩陣train_neg_adj_mask=[34, 34], # 訓練集 負樣本鄰接矩陣train_pos_edge_index=[2, 136], # 訓練集 正樣本鄰接矩陣val_neg_edge_index=[2, 3], # 驗證集 負樣本鄰接矩陣val_pos_edge_index=[2, 3], # 驗證集 正樣本鄰接矩陣x=[34, 3] # 節點屬性 )輸出情況如下:
Epoch: 001, Loss: 0.8969, Val: 0.3333, Test: 0.9796 Epoch: 002, Loss: 0.6772, Val: 0.3333, Test: 0.9796 Epoch: 003, Loss: 0.6933, Val: 0.3333, Test: 0.9796 Epoch: 004, Loss: 0.7107, Val: 0.3333, Test: 0.9796 Epoch: 005, Loss: 0.6960, Val: 0.3333, Test: 0.9796 Epoch: 006, Loss: 0.6905, Val: 0.3333, Test: 0.9796 Epoch: 007, Loss: 0.6896, Val: 0.3333, Test: 0.9796 Epoch: 008, Loss: 0.6837, Val: 0.3333, Test: 0.9796 Epoch: 009, Loss: 0.6834, Val: 0.3333, Test: 0.9796 Epoch: 010, Loss: 0.6840, Val: 0.3333, Test: 0.9796訓練集中的負樣本是每次隨機采樣得到的(第51-55行),而驗證集和測試集中的負樣本邊則在第14行就已經固定了,所以結果中訓練集上的loss一直在變化,而驗證集和測試集上的AUC得分沒有變化,因為我們的數據量太小導致的。如果換成Cora數據集效果會好點。
更詳細的介紹,請看這篇文章。
總結
以上是生活随笔為你收集整理的PYG教程【五】链路预测的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有流产迹象该怎么保胎
- 下一篇: 两个tplink路由器串联怎么设置两个无