【转】节点预测与边预测任务实践
節點預測與邊預測任務實踐
引言
在此小節我們將利用PlanetoidPubMed數據集類,來實踐節點預測與邊預測任務。
注:邊預測任務實踐中的代碼來源于link_pred.py。
節點預測任務實踐
之前我們學習過由2層GATConv組成的圖神經網絡,現在我們重定義一個GAT圖神經網絡,使其能夠通過參數來定義GATConv的層數,以及每一層GATConv的out_channels。我們的圖神經網絡定義如下:
class GAT(torch.nn.Module):def __init__(self, num_features, hidden_channels_list, num_classes):super(GAT, self).__init__()torch.manual_seed(12345)hns = [num_features] + hidden_channels_listconv_list = []for idx in range(len(hidden_channels_list)):conv_list.append((GATConv(hns[idx], hns[idx+1]), 'x, edge_index -> x'))conv_list.append(ReLU(inplace=True),)self.convseq = Sequential('x, edge_index', conv_list)self.linear = Linear(hidden_channels_list[-1], num_classes)def forward(self, x, edge_index):x = self.convseq(x, edge_index)x = F.dropout(x, p=0.5, training=self.training)x = self.linear(x)return x由于我們的神經網絡由多個GATConv順序相連而構成,因此我們使用了torch_geometric.nn.Sequential容器,詳細內容可見于官方文檔。
我們通過hidden_channels_list參數來設置每一層GATConv的outchannel,所以hidden_channels_list長度即為GATConv的層數。通過修改hidden_channels_list,我們就可構造出不同的圖神經網絡。
完整的代碼可見于codes/node_classification.py。請小伙伴們自行完成代碼中圖神經網絡類的訓練、驗證和測試。
邊預測任務實踐
邊預測任務,目標是預測兩個節點之間是否存在邊。拿到一個圖數據集,我們有節點屬性x,邊端點edge_index。edge_index存儲的便是正樣本。為了構建邊預測任務,我們需要生成一些負樣本,即采樣一些不存在邊的節點對作為負樣本邊,正負樣本數量應平衡。此外要將樣本分為訓練集、驗證集和測試集三個集合。
PyG中為我們提供了現成的采樣負樣本邊的方法,train_test_split_edges(data, val_ratio=0.05, test_ratio=0.1),其
- 第一個參數為torch_geometric.data.Data對象,
- 第二參數為驗證集所占比例,
- 第三個參數為測試集所占比例。
該函數將自動地采樣得到負樣本,并將正負樣本分成訓練集、驗證集和測試集三個集合。它用train_pos_edge_index、train_neg_adj_mask、val_pos_edge_index、val_neg_edge_index、test_pos_edge_index和test_neg_edge_index,六個屬性取代edge_index屬性。
注意train_neg_adj_mask與其他屬性格式不同,其實該屬性在后面并沒有派上用場,后面我們仍然需要進行一次訓練集負樣本采樣。
下面我們使用Cora數據集作為例子,進行邊預測任務說明。
獲取數據集并進行分析
首先是獲取數據集并進行分析:
import os.path as ospfrom torch_geometric.utils import negative_sampling from torch_geometric.datasets import Planetoid import torch_geometric.transforms as T from torch_geometric.utils import train_test_split_edgesdataset = Planetoid('dataset', 'Cora', transform=T.NormalizeFeatures()) data = dataset[0] data.train_mask = data.val_mask = data.test_mask = data.y = None # 不再有用print(data.edge_index.shape) # torch.Size([2, 10556])data = train_test_split_edges(data)for key in data.keys:print(key, getattr(data, key).shape)# x torch.Size([2708, 1433]) # val_pos_edge_index torch.Size([2, 263]) # test_pos_edge_index torch.Size([2, 527]) # train_pos_edge_index torch.Size([2, 8976]) # train_neg_adj_mask torch.Size([2708, 2708]) # val_neg_edge_index torch.Size([2, 263]) # test_neg_edge_index torch.Size([2, 527]) # 263 + 527 + 8976 = 9766 != 10556 # 263 + 527 + 8976/2 = 5278 = 10556/2我們觀察到訓練集、驗證集和測試集中正樣本邊的數量之和不等于原始邊的數量。這是因為,現在所用的Cora圖是無向圖,在統計原始邊數量時,每一條邊的正向與反向各統計了一次,訓練集也包含邊的正向與反向,但驗證集與測試集都只包含了邊的一個方向。
為什么訓練集要包含邊的正向與反向,而驗證集與測試集都只包含了邊的一個方向? 這是因為,訓練集用于訓練,訓練時一條邊的兩個端點要互傳信息,只考慮一個方向的話,只能由一個端點傳信息給另一個端點,而驗證集與測試集的邊用于衡量檢驗邊預測的準確性,只需考慮一個方向的邊即可。
邊預測圖神經網絡的構造
接下來構造神經網絡:
import torch from torch_geometric.nn import GCNConvclass Net(torch.nn.Module):def __init__(self, in_channels, out_channels):super(Net, self).__init__()self.conv1 = GCNConv(in_channels, 128)self.conv2 = GCNConv(128, out_channels)def encode(self, x, edge_index):x = self.conv1(x, edge_index)x = x.relu()return self.conv2(x, edge_index)def decode(self, z, pos_edge_index, neg_edge_index):edge_index = torch.cat([pos_edge_index, neg_edge_index], dim=-1)return (z[edge_index[0]] * z[edge_index[1]]).sum(dim=-1)def decode_all(self, z):prob_adj = z @ z.t()return (prob_adj > 0).nonzero(as_tuple=False).t()用于做邊預測的神經網絡主要由兩部分組成:其一是編碼(encode),它與我們前面介紹的節點表征生成是一樣的;其二是解碼(decode),它根據邊兩端節點的表征生成邊為真的幾率(odds)。decode_all(self, z)用于推理(inference)階段,我們要對所有的節點對預測存在邊的幾率。
邊預測圖神經網絡的訓練
定義單個epoch的訓練過程
def get_link_labels(pos_edge_index, neg_edge_index):num_links = pos_edge_index.size(1) + neg_edge_index.size(1)link_labels = torch.zeros(num_links, dtype=torch.float)link_labels[:pos_edge_index.size(1)] = 1.return link_labelsdef train(data, model, optimizer):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))optimizer.zero_grad()z = model.encode(data.x, data.train_pos_edge_index)link_logits = model.decode(z, data.train_pos_edge_index, neg_edge_index)link_labels = get_link_labels(data.train_pos_edge_index, neg_edge_index).to(data.x.device)loss = F.binary_cross_entropy_with_logits(link_logits, link_labels)loss.backward()optimizer.step()return loss通常,存在邊的節點對的數量往往少于不存在邊的節點對的數量。我們在每一個epoch的訓練過程中,都進行一次訓練集負樣本采樣。采樣到的樣本數量與訓練集正樣本相同,但不同epoch中采樣到的樣本是不同的。這樣做,我們既能實現類別數量平衡,又能實現增加訓練集負樣本的多樣性。在負樣本采樣時,我們傳遞了train_pos_edge_index為參數,于是negative_sampling()函數只會在訓練集中不存在邊的節點對中采樣。get_link_labels()函數用于生成完整訓練集的標簽。
注:在訓練階段,我們應該只見訓練集,對驗證集與測試集都是不可見的。所以我們沒有使用所有的邊,而是只用了訓練集正樣本邊。
定義單個epoch驗證與測試過程
@torch.no_grad() def test(data, model):model.eval()z = model.encode(data.x, data.train_pos_edge_index)results = []for prefix in ['val', 'test']:pos_edge_index = data[f'{prefix}_pos_edge_index']neg_edge_index = data[f'{prefix}_neg_edge_index']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)results.append(roc_auc_score(link_labels.cpu(), link_probs.cpu()))return results注:在驗證與測試階段,我們也應該只見訓練集,對驗證集與測試集都是不可見的。所以在驗證與測試階段,我們依然只用訓練集正樣本邊。
運行完整的訓練、驗證與測試
def main():device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')dataset = 'Cora'path = osp.join(osp.dirname(osp.realpath(__file__)), '..', 'data', dataset)dataset = Planetoid(path, dataset, transform=T.NormalizeFeatures())data = dataset[0]ground_truth_edge_index = data.edge_index.to(device)data.train_mask = data.val_mask = data.test_mask = data.y = Nonedata = train_test_split_edges(data)data = data.to(device)model = Net(dataset.num_features, 64).to(device)optimizer = torch.optim.Adam(params=model.parameters(), lr=0.01)best_val_auc = test_auc = 0for epoch in range(1, 101):loss = train(data, model, optimizer)val_auc, tmp_test_auc = test(data, model)if val_auc > best_val_auc:best_val_auc = val_auctest_auc = tmp_test_aucprint(f'Epoch: {epoch:03d}, Loss: {loss:.4f}, Val: {val_auc:.4f}, 'f'Test: {test_auc:.4f}')z = model.encode(data.x, data.train_pos_edge_index)final_edge_index = model.decode_all(z)if __name__ == "__main__":main()完整的代碼可見于codes/edge_prediction.py。
結語
在完整的第6節內容中,我們學習了
- PyG中規定的使用數據的一般過程;
- InMemoryDataset基類;
- 一個簡化的InMemory數據集類;
- 一個InMemory數據集類實例,以及使用該數據集類時會發生的一些過程;
- 節點預測任務實踐;
- 邊預測任務實踐。
我們需要重點關注**InMemory數據集類的運行流程與其四個方法的定義的規范**,同時我們還應該重點關注邊預測任務中的數據集劃分,訓練集負樣本采樣,以及訓練、驗證與測試三個階段使用的邊。
作業
-
實踐問題一:嘗試使用PyG中的不同的網絡層去代替GCNConv,以及不同的層數和不同的out_channels,來實現節點分類任務。
-
實踐問題二:在邊預測任務中,嘗試用torch_geometric.nn.Sequential容器構造圖神經網絡。
-
思考問題三:如下方代碼所示,我們以data.train_pos_edge_index為實際參數來進行訓練集負樣本采樣,但這樣采樣得到的負樣本可能包含一些驗證集的正樣本與測試集的正樣本,即可能將真實的正樣本標記為負樣本,由此會產生沖突。但我們還是這么做,這是為什么?
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))
參考資料
- Sequential官網文檔:torch_geometric.nn.Sequential
- 邊預測任務實踐中的代碼來源于link_pred.py
參考答案
思考問題三:
問題:我們以data.train_pos_edge_index為實際參數來進行訓練集負樣本采樣,但這樣采樣得到的負樣本可能包含一些驗證集的正樣本與測試集的正樣本,即可能將真實的正樣本標記為負樣本,由此會產生沖突。但我們還是這么做,這是為什么?
解答:
首先我們討論如果使用edge_index為實際參數會怎么樣?如果以edge_index為實際參數,negative_sampling()函數采樣到的是真實的負樣本。以真實負樣本作為訓練集負樣本,訓練集負樣本就不會與驗證集正樣本有交集,也不會與測試集正樣本有交集。 理論上這種采樣方式產生的驗證集的評估結果和測試集的評估結果都會更好,實際也是如此。但我們不能采用這種訓練集負樣本采樣方式,這是為什么?
整個數據集的正負樣本邊可劃分為訓練集正樣本邊、驗證集正樣本邊、測試集正樣本邊和所有負樣本邊,共四個集合。在訓練邊預測圖神經網絡時,我們要輸入所有訓練集邊的節點。如果訓練集由訓練集正樣本邊和所有的負樣本邊組成,那么有極大的可能性,所有的節點都要輸入給圖神經網絡。一個節點只有在滿足以下的條件時,才一定不會在訓練階段被輸入給圖神經網絡:
當邊預測圖神經網絡能夠感知所有的節點時,它也就能夠感知所有的正負樣本邊。在訓練階段,我們給邊預測圖神經網絡輸入訓練集正樣本邊和所有真實負樣本邊,邊預測圖神經網絡就相當于知道了訓練集與驗證集的正樣本邊,因為沒出現在訓練集正樣本與所有真實負樣本里的樣本即為訓練集或驗證集的正樣本。 采用這種數據采樣方式采樣得到的數據集,用于神經網絡的訓練,訓練得到的神經網絡會在“現在整個數據集”上過擬合,于是就降低了對將來未知的數據的泛化能力。于是在訓練階段,我們不能知道所有負樣本邊,那么我們只能知道所有訓練集正樣本邊。
接著我們討論如果以data.train_pos_edge_index為實際參數來進行訓練集負樣本采樣結果會是怎么樣?以data.train_pos_edge_index為實際參數來進行訓練集負樣本采樣,也就是在非訓練集正樣本中采樣。非訓練集正樣本包含了所有的負樣本,和沒有出現在訓練集中的正樣本。雖然包含了沒有出現在訓練集中的正樣本,但其數量相對于所有的負樣本的數量要少得多。即便將真實的正樣本標記為負樣本會產生沖突,但這帶來影響相對較小。
綜上,我們要以data.train_pos_edge_index為實際參數來進行訓練集負樣本采樣,也就是我們要在非訓練集正樣本中采樣訓練集負樣本。
總結
以上是生活随笔為你收集整理的【转】节点预测与边预测任务实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 平板电脑不开机平板电脑不开机怎么办
- 下一篇: 有流产迹象该怎么保胎