首先感謝datawhale 的課程內容:引用
GNN/Markdown版本/5-基于圖神經網絡的節點表征學習.md · Datawhale/team-learning-nlp - 碼云 - 開源中國 (gitee.com)
這個實驗做的很順利。
在節點預測任務中,我們擁有一個圖,圖上有很多節點,部分節點的預測標簽已知,部分節點的預測標簽未知。我們的任務是根據節點的屬性(可以是類別型、也可以是數值型)、邊的信息、邊的屬性(如果有的話)、已知的節點預測標簽,對未知標簽的節點做預測。
我們將以Cora?數據集為例子進行說明,Cora?是一個論文引用網絡,節點代表論文,如果兩篇論文存在引用關系,那么認為對應的兩個節點之間存在邊,每個節點由一個1433維的詞包特征向量描述。我們的任務是推斷每個文檔的類別(共7類)。
為了展現圖神經網絡的強大,我們通過節點分類任務來比較MLP和GCN, GAT(兩個知名度很高的圖神經網絡)三者的節點表征學習能力。此節內容安排為:
首先,我們要做一些準備工作,即獲取并分析數據集、構建一個方法用于分析節點表征的分布。
然后,我們考察MLP用于節點分類的表現,并分析基于MLP學習到的節點表征的分布。
接著,我們逐一介紹GCN, GAT這兩個圖神經網絡的理論、他們在節點分類任務中的表現以及它們學習到的節點表征的分布。
最后,我們比較三者在節點表征學習能力上的差異。
下載數據集
from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures
dataset = Planetoid(root='./tmp/cora', name='Cora', transform=NormalizeFeatures())
print()
print(f'Dataset: {dataset}:')
print('======================')
看一下數據的情況:
print(f'Number of graphs: {len(dataset)}')
print(f'Number of features: {dataset.num_features}')
print(f'Number of classes: {dataset.num_classes}')
data = dataset[0]? # Get the first graph object.
print()
print(data)
print('======================')
# Gather some statistics about the graph.
print(f'Number of nodes: {data.num_nodes}')
print(f'Number of edges: {data.num_edges}')
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}')
print(f'Number of training nodes: {data.train_mask.sum()}')
print(f'Training node label rate: {int(data.train_mask.sum()) / data.num_nodes:.2f}')
print(f'Contains isolated nodes: {data.contains_isolated_nodes()}')
print(f'Contains self-loops: {data.contains_self_loops()}')
print(f'Is undirected: {data.is_undirected()}')
可視化代碼:
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
def visualize(h, color):
? ? z = TSNE(n_components=2).fit_transform(out.detach().cpu().numpy())
? ? plt.figure(figsize=(10,10))
? ? plt.xticks([])
? ? plt.yticks([])
? ? plt.scatter(z[:, 0], z[:, 1], s=70, c=color, cmap="Set2")
? ? plt.show()
構建MLP模型:
import torch
from torch.nn import Linear
import torch.nn.functional as F
class MLP(torch.nn.Module):
? ? def __init__(self, hidden_channels):
? ? ? ? super(MLP, self).__init__()
? ? ? ? torch.manual_seed(12345)
? ? ? ? self.lin1 = Linear(dataset.num_features, hidden_channels)
? ? ? ? self.lin2 = Linear(hidden_channels, dataset.num_classes)
? ? def forward(self, x):
? ? ? ? x = self.lin1(x)
? ? ? ? x = x.relu()
? ? ? ? x = F.dropout(x, p=0.5, training=self.training)
? ? ? ? x = self.lin2(x)
? ? ? ? return x
model = MLP(hidden_channels=16)
print(model)
MLP 模型 訓練情況:
model = MLP(hidden_channels=16)
criterion = torch.nn.CrossEntropyLoss()? # Define loss criterion.
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)? # Define optimizer.
def train():
? ? ? model.train()
? ? ? optimizer.zero_grad()? # Clear gradients.
? ? ? out = model(data.x)? # Perform a single forward pass.
? ? ? loss = criterion(out[data.train_mask], data.y[data.train_mask])? # Compute the loss solely based on the training nodes.
? ? ? loss.backward()? # Derive gradients.
? ? ? optimizer.step()? # Update parameters based on gradients.
? ? ? return loss
for epoch in range(1, 201):
? ? loss = train()
? ? print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')
MLP結果:
做了兩次實驗,小的數據量,和大的數據量,越大的數量兩,精度越低,說明該模型不適合再大數據集上跑。
# GCN圖節點分類神經網絡
from torch_geometric.nn import GCNConv
class GCN(torch.nn.Module):
? ? def __init__(self, hidden_channels):
? ? ? ? super(GCN, self).__init__()
? ? ? ? torch.manual_seed(12345)
? ? ? ? self.conv1 = GCNConv(dataset.num_features, hidden_channels)
? ? ? ? self.conv2 = GCNConv(hidden_channels, dataset.num_classes)
? ? def forward(self, x, edge_index):
? ? ? ? x = self.conv1(x, edge_index)
? ? ? ? x = x.relu()
? ? ? ? x = F.dropout(x, p=0.5, training=self.training)
? ? ? ? x = self.conv2(x, edge_index)
? ? ? ? return x
model = GCN(hidden_channels=16)
print(model)
# 可視化未訓練過的模型輸出的節點表征
model = GCN(hidden_channels=16)
model.eval()
out = model(data.x, data.edge_index)
visualize(out, color=data.y)
# 訓練GCN圖節點分類器model = GCN(hidden_channels=16)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
criterion = torch.nn.CrossEntropyLoss()deftrain():? ? ? model.train()
? ? ? optimizer.zero_grad()? # Clear gradients.? ? ? out = model(data.x, data.edge_index)? # Perform a single forward pass.? ? ? loss = criterion(out[data.train_mask], data.y[data.train_mask])? # Compute the loss solely based on the training nodes.? ? ? loss.backward()? # Derive gradients.? ? ? optimizer.step()? # Update parameters based on gradients.? ? ? return lossfor epoch in range(1, 201):
? ? loss = train()
? ? print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')
# 測試GCN圖節點分類器deftest(): model.eval()
? ? ? out = model(data.x, data.edge_index)
? ? ? pred = out.argmax(dim=1)? # Use the class with highest probability.? ? ? test_correct = pred[data.test_mask] == data.y[data.test_mask]? # Check against ground-truth labels.? ? ? test_acc = int(test_correct.sum()) / int(data.test_mask.sum())? # Derive ratio of correct predictions.? ? ? return test_acc
test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')
# 可視化訓練過的GCN模型輸出的節點表征
# 可視化訓練過的GCN模型輸出的節點表征
model.eval()
out = model(data.x,data.edge_index)
visualize(out, color=data.y)
因為數據沒有下載全,所有,節點有限,呵呵
訓練前的
訓練后:
測試精度:
GAT
# 構造GAT節點分類神經網絡
import torch
from torch.nn import Linear
import torch.nn.functional as F
from torch_geometric.nn import GATConv
class GAT(torch.nn.Module):
? ? def __init__(self, hidden_channels):
? ? ? ? super(GAT, self).__init__()
? ? ? ? torch.manual_seed(12345)
? ? ? ? self.conv1 = GATConv(dataset.num_features, hidden_channels)
? ? ? ? self.conv2 = GATConv(hidden_channels, dataset.num_classes)
? ? def forward(self, x, edge_index):
? ? ? ? x = self.conv1(x, edge_index)
? ? ? ? x = x.relu()
? ? ? ? x = F.dropout(x, p=0.5, training=self.training)
? ? ? ? x = self.conv2(x, edge_index)
? ? ? ? return x
# 可視化未訓練過的GAT模型輸出的節點表征
model.eval()
out = model(data.x,data.edge_index)
visualize(out, color=data.y)
# 訓練GAT圖節點分類器
model = GAT(hidden_channels=16)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
criterion = torch.nn.CrossEntropyLoss()
deftrain():? ??
? model.train()
? ?optimizer.zero_grad()? # Clear gradients.??
? ? out = model(data.x, data.edge_index)? # Perform a single forward pass.? ??
? loss = criterion(out[data.train_mask], data.y[data.train_mask])? # Compute the loss solely based on the training nodes.? ??
? loss.backward()? # Derive gradients.? ?
?? optimizer.step()? # Update parameters based on gradients.? ? ?
?return lossfor epoch in range(1, 201):
? ? loss = train()
? ? print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')
可以看出GAT的效果相比GCN差一些。
結論:
在節點表征的學習中,MLP節點分類器只考慮了節點自身屬性,忽略了節
點之間的連接關系,它的結果是最差的;而GCN與GAT節點分類器,同時
考慮了節點自身屬性與周圍鄰居節點的屬性,它們的結果優于MLP節點分
類器。從中可以看出鄰居節點的信息對于節點分類任務的重要性。
基于圖神經網絡的節點表征的學習遵循消息傳遞范式:
在鄰居節點信息變換階段,GCN與GAT都對鄰居節點做歸一化和線性變
換(兩個操作不分前后);
在鄰居節點信息聚合階段都將變換后的鄰居節點信息做求和聚合;
在中心節點信息變換階段只是簡單返回鄰居節點信息聚合階段的聚合結
果。
GCN與GAT的區別在于鄰居節點信息聚合過程中的歸一化方法不同:
前者根據中心節點與鄰居節點的度計算歸一化系數,后者根據中心節點
與鄰居節點的相似度計算歸一化系數。
前者的歸一化方式依賴于圖的拓撲結構,不同節點其自身的度不同、其
鄰居的度也不同,在一些應用中可能會影響泛化能力。
后者的歸一化方式依賴于中心節點與鄰居節點的相似度,相似度是訓練
得到的,因此不受圖的拓撲結構的影響,在不同的任務中都會有較好的
泛化表現。