使用 PyG 進行圖神經網絡的節點分類、鏈路預測和異常檢測
圖神經網絡 (Graph Neural Networks) 是一種針對圖結構數據 (如社交圖、網絡安全網絡或分子表示) 設計的機器學習算法。它在過去幾年裏發展迅速,被用於許多不同的應用程序。在這篇文章中我們將回顧 GNN 的基礎知識,然後使用 Pytorch Geometric 解決一些常見的主要問題,並討論一些算法和代碼的細節。
常見的圖神經網絡應用
GNN 可以用來解決各種與圖相關的機器學習問題:
-
節點的分類:預測節點的類別或標籤。例如,在網絡安全中檢測網絡中的欺詐實體可能是一個節點分類問題。
-
鏈接預測:預測節點之間是否存在潛在的鏈接 (邊)。例如,社交網絡服務根據網絡數據建議可能的朋友聯繫。
-
圖分類:將圖形本身劃分爲不同的類別。比如通過觀察一個化合物的圖結構來確定它是有毒的還是無毒的。
-
社區檢測:將節點劃分爲集羣。比如在社交圖中尋找不同的社區。
-
異常檢測:以無監督的方式在圖中查找離羣節點。如果沒有標籤,可以使用這種方法。
在這篇文章中,我們將回顧節點分類、鏈接預測和異常檢測的相關知識和用 Pytorch Geometric 代碼實現這三個算法。
圖卷積
圖神經網絡在過去的幾年裏發展迅速,並且有許多的變體。在這些 GNN 變體中圖卷積網絡可能是最流行和最基本的算法。在本節中我們將對其進行回顧和介紹。
圖卷積是一種基於圖結構提取 / 彙總節點信息的有效方法。它是卷積神經網絡卷積運算的一個變體,卷積神經網絡通常用於解決圖像問題。
在圖像中,像素在網格中按結構排序,卷積操作中的過濾器或卷積核 (權重矩陣) 以預先確定的步幅在圖像上滑動。像素的鄰域由過濾器大小決定(下圖中過濾器大小爲 3 x 3,藍色過濾器中的 8 個灰色像素爲鄰域),過濾器中的加權像素值被聚合爲單個值。卷積運算的輸出比輸入圖像的尺寸小,但對輸入圖像有更高層次的視圖,這對預測圖像問題很有用,比如圖像分類。
在圖中,節點以非結構化的方式排序,節點之間的鄰域大小不同。圖卷積取給定節點 (下圖中的紅色節點) 及其鄰居 (藍圈內的灰色節點) 的節點特徵的平均值,計算更新後的節點表示值。通過這種卷積操作,節點表示捕獲局部的圖信息。
下圖顯示了更多關於圖卷積操作的細節。鄰居節點 (藍色) 的節點特徵和目標節點 (紅色) 的節點特徵被平均。然後與權重向量 (W) 相乘,其輸出更新目標節點的節點特徵(更新後的節點值也稱爲節點嵌入)。
對於那些相關的節點,節點特徵使用度矩陣的逆進行歸一化,然後再聚合而不是簡單的平均(原始論文公式 8 中提出)
這個卷積操作中需要注意的一點是,圖卷積的數量決定了節點特徵被聚合到每個節點的步數。在下圖中,第一個卷積將藍色節點的特徵聚合到橙色節點中,第二個卷積將綠色節點的特徵合併到橙色節點中。
可以看到卷積的數量決定了聚合的節點特徵有多遠
在接下來的幾節中,我們實現 GCN。但是在深入研究它們之前,先熟悉一下將要使用的數據集。
Cora - 基準數據集
Cora 數據集是一個論文引用網絡數據,包含 2708 篇科學論文。圖中的每個節點代表一篇論文,如果一篇論文引用另一篇論文,則有節點間有一條邊相連。
我們使用 PyG (Pytorch Geometric) 來實現 GCN, GCN 是 GNN 的流行庫之一。Cora 數據集也可以使用 PyG 模塊加載:
from torch_geometric.datasets import Planetoid
dataset = Planetoid(root='/tmp/Cora', name='Cora')
graph = dataset[0]
Cora 數據集來源於 Pytorch Geometric 的 “Automating the Construction of Internet Portals with Machine Learning” 論文。
節點特徵和邊緣信息如下所示。節點特徵是 1433 個詞向量,表示每個出版物中的詞不存在 (0) 或存在 (1)。邊在鄰接列表中表示。
每個節點都是七個類別中的一個,這將就是分類的目標標籤
利用 NetworkX 庫可以實現圖數據的可視化。節點顏色代表不同的類。
import random
from torch_geometric.utils import to_networkx
import networkx as nx
def convert_to_networkx(graph, n_sample=None):
g = to_networkx(graph, node_attrs=["x"])
y = graph.y.numpy()
if n_sample is not None:
sampled_nodes = random.sample(g.nodes, n_sample)
g = g.subgraph(sampled_nodes)
y = y[sampled_nodes]
return g, y
def plot_graph(g, y):
plt.figure(figsize=(9, 7))
nx.draw_spring(g, node_size=30, arrows=False, node_color=y)
plt.show()
g, y = convert_to_networkx(graph, n_sample=1000)
plot_graph(g, y)
節點分類
對於節點分類問題,可以使用 PyG 中的 RandomNodeSplit 模塊將節點分爲 train、valid 和 test(我替換數據中的原始分割掩碼,因爲它的訓練集太小了)。
import torch_geometric.transforms as T
split = T.RandomNodeSplit(num_val=0.1, num_test=0.2)
graph = split(graph)
數據分割標記會被寫入到圖對象的掩碼屬性中 (參見下圖),而不是圖本身。這些掩碼用於訓練損失計算和模型評估,而圖卷積使用整個圖數據。
節點分類基線模型 (MLP)
在構建 GCN 之前,只使用節點特徵來訓練 MLP(多層感知器,即前饋神經網絡) 來創建一個基線性能。該模型忽略節點連接 (或圖結構),並試圖僅使用詞向量對節點標籤進行分類。模型類如下所示。它有兩個隱藏層 (Linear),帶有 ReLU 激活,後面是一個輸出層。
import torch.nn as nn
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(dataset.num_node_features, 64),
nn.ReLU(),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, dataset.num_classes)
)
def forward(self, data):
x = data.x # only using node features (x)
output = self.layers(x)
return output
我們用一個普通的 Pytorch 訓練 / 驗證流程來定義訓練和評估函數。
def train_node_classifier(model, graph, optimizer, criterion, n_epochs=200):
for epoch in range(1, n_epochs + 1):
model.train()
optimizer.zero_grad()
out = model(graph)
loss = criterion(out[graph.train_mask], graph.y[graph.train_mask])
loss.backward()
optimizer.step()
pred = out.argmax(dim=1)
acc = eval_node_classifier(model, graph, graph.val_mask)
if epoch % 10 == 0:
print(f'Epoch: {epoch:03d}, Train Loss: {loss:.3f}, Val Acc: {acc:.3f}')
return model
def eval_node_classifier(model, graph, mask):
model.eval()
pred = model(graph).argmax(dim=1)
correct = (pred[mask] == graph.y[mask]).sum()
acc = int(correct) / int(mask.sum())
return acc
mlp = MLP().to(device)
optimizer_mlp = torch.optim.Adam(mlp.parameters(), lr=0.01, weight_decay=5e-4)
criterion = nn.CrossEntropyLoss()
mlp = train_node_classifier(mlp, graph, optimizer_mlp, criterion, n_epochs=150)
test_acc = eval_node_classifier(mlp, graph, graph.test_mask)
print(f'Test Acc: {test_acc:.3f}')
該模型的測試精度爲 73.2%。
GCN 進行節點分類
接下來,我們將對 GCN 進行訓練並將其性能與 MLP 進行比較。這裏使用的是一個非常簡單的模型,有兩個圖卷積層和它們之間的 ReLU 激活。此設置與論文原文相同 (公式 9)。
from torch_geometric.nn import GCNConv
import torch.nn.functional as F
class GCN(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv1 = GCNConv(dataset.num_node_features, 16)
self.conv2 = GCNConv(16, dataset.num_classes)
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.conv1(x, edge_index)
x = F.relu(x)
output = self.conv2(x, edge_index)
return output
gcn = GCN().to(device)
optimizer_gcn = torch.optim.Adam(gcn.parameters(), lr=0.01, weight_decay=5e-4)
criterion = nn.CrossEntropyLoss()
gcn = train_node_classifier(gcn, graph, optimizer_gcn, criterion)
test_acc = eval_node_classifier(gcn, graph, graph.test_mask)
print(f'Test Acc: {test_acc:.3f}')
該模型的測試集準確度爲 88.0%。從 MLP 中獲得了大約 15% 的精度提高。
鏈接預測
鏈接預測比節點分類更復雜,因爲我們需要使用節點嵌入對邊緣進行預測。預測步驟大致如下:
-
編碼器通過處理具有兩個卷積層的圖來創建節點嵌入。
-
在原始圖上隨機添加負鏈接。這使得模型任務變爲對原始邊的正鏈接和新增邊的負鏈接進行二元分類。
-
解碼器使用節點嵌入對所有邊 (包括負鏈接) 進行鏈接預測(二元分類)。它從每條邊上的一對節點計算節點嵌入的點積。然後聚合整個嵌入維度的值,並在每條邊上創建一個表示邊存在概率的值。
該模型結構來源於變分圖自動編碼器中原始的鏈接預測實現。代碼改編自 PyG repo 中的代碼示例,並基於 Graph Auto-Encoders 實現
from sklearn.metrics import roc_auc_score
from torch_geometric.utils import negative_sampling
class Net(torch.nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels):
super().__init__()
self.conv1 = GCNConv(in_channels, hidden_channels)
self.conv2 = GCNConv(hidden_channels, out_channels)
def encode(self, x, edge_index):
x = self.conv1(x, edge_index).relu()
return self.conv2(x, edge_index)
def decode(self, z, edge_label_index):
return (z[edge_label_index[0]] * z[edge_label_index[1]]).sum(
dim=-1
) # product of a pair of nodes on each edge
def decode_all(self, z):
prob_adj = z @ z.t()
return (prob_adj > 0).nonzero(as_tuple=False).t()
def train_link_predictor(
model, train_data, val_data, optimizer, criterion, n_epochs=100
):
for epoch in range(1, n_epochs + 1):
model.train()
optimizer.zero_grad()
z = model.encode(train_data.x, train_data.edge_index)
# sampling training negatives for every training epoch
neg_edge_index = negative_sampling(
edge_index=train_data.edge_index, num_nodes=train_data.num_nodes,
num_neg_samples=train_data.edge_label_index.size(1), method='sparse')
edge_label_index = torch.cat(
[train_data.edge_label_index, neg_edge_index],
dim=-1,
)
edge_label = torch.cat([
train_data.edge_label,
train_data.edge_label.new_zeros(neg_edge_index.size(1))
], dim=0)
out = model.decode(z, edge_label_index).view(-1)
loss = criterion(out, edge_label)
loss.backward()
optimizer.step()
val_auc = eval_link_predictor(model, val_data)
if epoch % 10 == 0:
print(f"Epoch: {epoch:03d}, Train Loss: {loss:.3f}, Val AUC: {val_auc:.3f}")
return model
@torch.no_grad()
def eval_link_predictor(model, data):
model.eval()
z = model.encode(data.x, data.edge_index)
out = model.decode(z, data.edge_label_index).view(-1).sigmoid()
return roc_auc_score(data.edge_label.cpu().numpy(), out.cpu().numpy())
對於這個鏈接預測任務,我們希望將鏈接 / 邊隨機分割爲訓練數據、有效數據和測試數據。我們可以使用 PyG 的 RandomLinkSplit 模塊來做到這一點。
import torch_geometric.transforms as T
split = T.RandomLinkSplit(
num_val=0.05,
num_test=0.1,
is_undirected=True,
add_negative_train_samples=False,
neg_sampling_ratio=1.0,
)
train_data, val_data, test_data = split(graph)
輸出數據如下所示。
這個輸出數據有以下 3 點需要注意:
1、在 edge_index 上執行分割,這樣訓練和驗證分割不包括來自驗證和測試分割的邊 (即只有來自訓練分割的邊),而測試分割不包括來自測試分割的邊。這是因爲編碼器使用 edge_index 和 x 來創建節點嵌入,這種方式確保了在對驗證 / 測試數據進行預測時,節點嵌入上沒有目標泄漏。
2、向每個分割數據添加兩個新屬性 (edge_label 和 edge_label_index)。它們是與每次分割相對應的邊標籤和邊索引。Edge_label_index 將用於解碼器進行預測,edge_label 將用於模型評估。
3、向 val_data 和 test_data 添加與正鏈接相同數量的負鏈接 (neg_sampling_ratio=1.0)。它們被添加到 edge_label 和 edge_label_index 屬性中,但沒有添加到 edge_index 中,因爲我們不希望在編碼器(或節點嵌入創建) 上使用負鏈接。我們沒有向這裏的訓練集添加負鏈接(設置 add_negative_train_samples=False),因爲會在上面的 train_link_predictor 的訓練循環中添加它們。訓練過程中的這種隨機化應該會使模型更健壯。
下圖總結了如何對編碼器和解碼器執行邊緣分割 (每個階段使用彩色邊緣)。
我們現在可以用下面的代碼來訓練和評估模型。
model = Net(dataset.num_features, 128, 64).to(device)
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.01)
criterion = torch.nn.BCEWithLogitsLoss()
model = train_link_predictor(model, train_data, val_data, optimizer, criterion)
test_auc = eval_link_predictor(model, test_data)
print(f"Test: {test_auc:.3f}")
該模型的測試 AUC 爲 92.5%。
異常檢測
再次使用 Cora 數據集進行異常檢測任務,但它與前面的數據集略有不同: 我們需要合成注入異常值。數據集有兩種不同類型的異常值:
-
結構異常
-
密集連接的節點,而不是稀疏連接的規則節點
-
上下文的異常值
-
屬性與相鄰節點顯著不同的節點
對於這個異常檢測任務,需要使用的是 PyGOD 庫,它是建立在 PyG 之上的一個圖異常值檢測庫。可以通過 PyGOD 模塊加載已經進行了異常值注入的 Cora 數據集。
from pygod.utils import load_data
graph = load_data('inj_cora')
下面的代碼顯示了異常值類型分佈。
Counter(graph.y.tolist())
#Counter({0: 2570, 1: 68, 2: 68, 3: 2})
0: 正常,1: 僅上下文異常,2: 結構異常,3: 上下文和結構都異常
如果你對這些異常值是如何注入的感興趣,可以查看關於異常值生成器模塊的 PyGOD 文檔,該文檔解釋了操作細節。這裏我們需要注意的是標籤 y 將只用於模型評估,而不是用於訓練標籤,因爲我們正在訓練一個無監督的模型。
爲了檢測這些異常點,我們訓練了 DOMINANT(Deep Anomaly Detection on Attributed Networks) 模型。它是一個具有圖卷積層的自編碼器網絡,其重構誤差將是節點異常評分。該模型遵循以下步驟進行預測。
-
屬性網絡編碼器使用三個圖卷積層來處理輸入圖,從而創建節點嵌入。
-
結構重構解碼器利用學習到的節點嵌入重構原始圖邊 (即鄰接矩陣)。它從每個可能的節點對計算節點嵌入的點積,在每個節點對上創建一個表示邊緣存在的概率評分。
-
屬性重構解碼器使用獲得的節點嵌入重構原始節點屬性。它有一個圖卷積層來預測屬性值。
-
最後一步將上述兩種解碼器的重構誤差在每個節點上進行加權平均合併,合併後的誤差即爲最終的誤差 / 損失。這些最終的誤差也是節點的異常評分。
from pygod.models import DOMINANT
from sklearn.metrics import roc_auc_score, average_precision_score
def train_anomaly_detector(model, graph):
return model.fit(graph)
def eval_anomaly_detector(model, graph):
outlier_scores = model.decision_function(graph)
auc = roc_auc_score(graph.y.numpy(), outlier_scores)
ap = average_precision_score(graph.y.numpy(), outlier_scores)
print(f'AUC Score: {auc:.3f}')
print(f'AP Score: {ap:.3f}')
graph.y = graph.y.bool()
model = DOMINANT()
model = train_anomaly_detector(model, graph)
eval_anomaly_detector(model, graph)
模型的 AUC 爲 84.1%,平均精度爲 20.8%。這種差異很可能是由於目標不平衡造成的。由於這是一個無監督的模型,我們不可能期望得到一個非常精確的模型,但可以在原始論文中看到,它仍然優於任何其他流行的異常檢測算法。
引用
本文的源代碼可以在 colab 找到:
https://colab.research.google.com/drive/1Ksca_p4XrZjeN0A6jT5aYN6ARvwFVSbY?usp=sharing
引用如下:
-
Benjamin Sanchez-Lengeling et al., A Gentle Introduction to Graph Neural Networks
-
Ameya Daigavane et al., Understanding Convolutions on Graphs
-
Francesco Casalegno, Graph Convolutional Networks — Deep Learning on Graphs
-
Thomas N. Kipf, Max Welling, Semi-Supervised Classification with Graph Convolutional Networks (2017), ICLR 2017
-
Thomas N. Kipf, Max Welling, Variational Graph Auto-Encoders (2016)
-
Kaize Ding et al., Deep Anomaly Detection on Attributed Networks (2019)
-
Kay Liu et al., Benchmarking Node Outlier Detection on Graphs (2022)
作者:Tomonori Masui
轉載:Deephub Imba
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/9qIzmcidWeFhK3kStuT9Qg