图神经网络实战——利用节点回归预测网络流量
- 0. 前言
- 1. 数据集分析
- 2. 实现 GCN 模型执行节点回归
- 3. 模型测试
- 相关链接
0. 前言
在机器学习中,回归指的是对连续值的预测。通常与分类形成鲜明对比,分类的目标是找到正确的类别(即离散值,而非连续值)。在图数据中,分类和回归分别对应于节点分类和节点回归。在本节中,我们将尝试预测每个节点的连续值,而非分类变量。
1. 数据集分析
为了利用节点回归预测网络流量,在本节中,我们将使用 Wikipedia Network
数据集,Wikipedia Network
数据集由 Rozemberckzi
等人于 2019
年引入。它由三个页面网络组成:chameleons
(包含 2277
个节点和 31421
条边)、crocodiles
(包含 11631
个节点和 170918
条边)和 squirrels
(包含 5201
个节点和 198493
条边)。在这些数据集中,节点代表文章,边代表文章之间的相互链接,节点特征反映了文章中包含的特定词语,我们的目标是预测 2018
年 12
月的平均流量的对数。
在本节中,我们将在 chameleon
数据集上应用图卷积网络 (Graph Convolutional Network, GCN) 来预测网络流量。
(1) 导入 WikipediaNetwork
并下载 chameleon
数据集,应用转换函数 RandomNodeSplit()
随机创建一个评估掩码和一个测试掩码:
from torch_geometric.datasets import WikipediaNetwork
import torch_geometric.transforms as T
dataset = WikipediaNetwork(root=".", name="chameleon", transform = T.RandomNodeSplit(num_val=200, num_test=500))
data = dataset[0]
(2) 打印该数据集的相关信息:
# Print information about the dataset
print(f'Dataset: {dataset}')
print('-------------------')
print(f'Number of graphs: {len(dataset)}')
print(f'Number of nodes: {data.x.shape[0]}')
print(f'Number of unique features: {dataset.num_features}')
print(f'Number of classes: {dataset.num_classes}')
# Print information about the graph
print(f'\nGraph:')
print('------')
print(f'Edges are directed: {data.is_directed()}')
print(f'Graph has isolated nodes: {data.has_isolated_nodes()}')
print(f'Graph has loops: {data.has_self_loops()}')
输出结果如下所示:
Dataset: WikipediaNetwork()
-------------------
Number of graphs: 1
Number of nodes: 2277
Number of unique features: 2325
Number of classes: 5
Graph:
------
Edges are directed: True
Graph has isolated nodes: False
Graph has loops: True
(3) 在以上输出中,可以看出数据集中有五个类别。但是,我们需要执行的是节点回归任务,而不是分类。实际上,这五个类别正是我们想要预测的连续值的分段函数,但这些标签并不满足我们的需求,因此必须手动进行修改。首先,下载 wikipedia.zip 文件,解压缩后导入 pandas
并使用它加载目标值:
import pandas as pd
df = pd.read_csv('wikipedia/chameleon/musae_chameleon_target.csv')
(4) 使用 np.log10()
对目标值应用对数函数,因为我们的目标是预测月平均流量的对数:
values = np.log10(df['target'])
(5) 将 data.y
重新定义为上一步中获取的连续值张量。需要注意的是,为了便于演示,我们在本例中未对这些值进行归一化处理(通常是获取优秀模型的标准预处理步骤):
data.y = torch.tensor(values)
print(data.y)
tensor([2.2330, 3.9079, 3.9329, ..., 1.9956, 4.3598, 2.4409],
dtype=torch.float64)
(6) 将节点度的分布进行可视化:
from torch_geometric.utils import degree
from collections import Counter
# Get list of degrees for each node
degrees = degree(data.edge_index[0]).numpy()
# Count the number of nodes for each degree
numbers = Counter(degrees)
# Bar plot
fig, ax = plt.subplots()
ax.set_xlabel('Node degree')
ax.set_ylabel('Number of nodes')
plt.bar(numbers.keys(), numbers.values())
plt.show()
与 Cora 和 Facebook Page-Page 数据集相比,该分布的尾部较短,但形状相似,大多数节点只有一个或几个邻居,但其中一些节点作为 "枢纽"节点可以连接 80
多个节点。
(7) 在节点回归的任务中,节点度的分布并不是唯一需要检查的分布类型,目标值的分布同样重要。事实上,非正态分布(如节点度)往往更难预测,可以使用 Seaborn
库绘制目标值,并将其与 scipy.stats.norm
提供的正态分布进行比较:
import seaborn as sns
from scipy.stats import norm
df['target'] = values
fig = sns.distplot(df['target'], fit=norm)
plt.show()
可以看到该分布不完全是正态分布,也不像节点度分布那样近似指数分布,因此模型有机会能够很好地预测这些值。
2. 实现 GCN 模型执行节点回归
接下来,使用 PyTorch Geometric
实现图卷积网络 (Graph Convolutional Network, GCN) 架构用于执行节点回归任务。
(1) 定义 GCN
类和 __init__()
初始化方法,使用三个神经元数量递减的 GCNConv
层。这种编码器架构能够迫使模型选择最相关的特征来预测目标值,并添加了一个线性层,令预测输出不局限于 -1
和 `1 之间:
class GCN(torch.nn.Module):
"""Graph Convolutional Network"""
def __init__(self, dim_in, dim_h, dim_out):
super().__init__()
self.gcn1 = GCNConv(dim_in, dim_h*4)
self.gcn2 = GCNConv(dim_h*4, dim_h*2)
self.gcn3 = GCNConv(dim_h*2, dim_h)
self.linear = torch.nn.Linear(dim_h, dim_out)
(2) 在 forward()
方法中使用 GCNConv
层和 nn.Linear
层,但不再需要使用 log_softmax
函数,因为模型的目标并不是预测类别:
def forward(self, x, edge_index):
h = self.gcn1(x, edge_index)
h = torch.relu(h)
h = F.dropout(h, p=0.5, training=self.training)
h = self.gcn2(h, edge_index)
h = torch.relu(h)
h = F.dropout(h, p=0.5, training=self.training)
h = self.gcn3(h, edge_index)
h = torch.relu(h)
h = self.linear(h)
return h
(3) 在 fit()
方法中使用 F.mse_loss()
函数替代分类任务中使用的交叉熵损失,使用均方差 (Mean Squared Error
, MSE
) 作为模型性能的评价指标,MSE
定义如下:
M
S
E
=
1
N
∑
i
=
1
N
(
y
i
−
y
^
i
)
2
MSE=\frac 1N\sum_{i=1}^N(y_i-\hat y_i)^2
MSE=N1i=1∑N(yi−y^i)2
完整的 fit()
方法代码如下:
def fit(self, data, epochs):
optimizer = torch.optim.Adam(self.parameters(),
lr=0.02,
weight_decay=5e-4)
self.train()
for epoch in range(epochs+1):
optimizer.zero_grad()
out = self(data.x, data.edge_index)
loss = F.mse_loss(out.squeeze()[data.train_mask], data.y[data.train_mask].float())
loss.backward()
optimizer.step()
if epoch % 20 == 0:
val_loss = F.mse_loss(out.squeeze()[data.val_mask], data.y[data.val_mask])
print(f"Epoch {epoch:>3} | Train Loss: {loss:.5f} | Val Loss: {val_loss:.5f}")
(4) 在 test()
方法中同样包含 MSE
:
def test(self, data):
self.eval()
out = self(data.x, data.edge_index)
return F.mse_loss(out.squeeze()[data.test_mask], data.y[data.test_mask].float())
(5) 实例化 GCN
模型,模型包含 128
个隐藏维度、 1
个输出维度(目标值),并训练 200
个 epoch
:
# Create the Vanilla GNN model
gcn = GCN(dataset.num_features, 128, 1)
print(gcn)
# Train
gcn.fit(data, epochs=200)
3. 模型测试
(1) 模型训练完成后进行测试,获得在测试集上的 MSE
:
# Test
loss = gcn.test(data)
print(f'\nGCN test loss: {loss:.5f}\n')
# GCN test loss: 0.80326
MSE
损失本身并不是最合适指标,可以使用以下两个指标得到更有意义的结果:
RMSE
:衡量误差的平均值:
R M S E = M S E = 1 N ∑ i = 1 N ( y i − y ^ i ) 2 RMSE=\sqrt {MSE}=\sqrt {\frac 1N\sum_{i=1}^N(y_i-\hat y_i)^2} RMSE=MSE=N1i=1∑N(yi−y^i)2- 平均绝对误差 (
Mean Absolute Error
,MAE
):预测值和实际值之间的平均绝对差值:
M A E = 1 N ∑ i = 1 N ∣ y i − y ^ i ∣ MAE=\frac 1N\sum_{i=1}^N |y_i-\hat y_i| MAE=N1i=1∑N∣yi−y^i∣
接下来,使用 Python
实现上述两种模型评价指标。
(2) 可以直接从 scikit-learn
库中导入 MSE
和 MAE
:
from sklearn.metrics import mean_squared_error, mean_absolute_error
(3) 使用 .detach().numpy()
将模型预测值的 PyTorch
张量转换为 NumPy
数组:
out = gcn(data.x, data.edge_index)
y_pred = out.squeeze()[data.test_mask].detach().numpy()
mse = mean_squared_error(data.y[data.test_mask], y_pred)
mae = mean_absolute_error(data.y[data.test_mask], y_pred)
(4) 使用 scikit-learn
库函数计算 MSE
和 MAE
,使用 np.sqrt()
计算 MSE
的平方根得到 RMSE
:
print('=' * 43)
print(f'MSE = {mse:.4f} | RMSE = {np.sqrt(mse):.4f} | MAE = {mae:.4f}')
print('=' * 43)
'''
===========================================
MSE = 0.8033 | RMSE = 0.8962 | MAE = 0.7409
===========================================
'''
不同指标可以用于比较不同的模型。为了直观可视化模型性能,可使用散点图,其中横轴代表预测值,纵轴代表实际值,在 Seaborn
库中可以使用函数 regplot()
实现这种可视化:
fig = sns.regplot(x=data.y[data.test_mask].numpy(), y=y_pred)
fig.set(xlabel='Ground truth', ylabel='Predicted values')
plt.show()
虽然我们没有使用基线模型,但仍然可以看出模型可以得到不错的预测结果,因为离群值很少。尽管数据集很小,但足以说明 GCN
在多种应用中都能发挥作用。如果我们想改进模型性能,可以调整超参数并进行误差分析,以了解异常值的来源。
相关链接
图神经网络实战(1)——图神经网络(Graph Neural Networks, GNN)基础
图神经网络实战(2)——图论基础
图神经网络实战(3)——基于DeepWalk创建节点表示
图神经网络实战(4)——基于Node2Vec改进嵌入质量
图神经网络实战(5)——常用图数据集
图神经网络实战(6)——使用PyTorch构建图神经网络
图神经网络实战(7)——图卷积网络(Graph Convolutional Network, GCN)详解与实现