《数据挖掘》第六次实验
基于Pytorch的图卷积网络GCN实例应用及详解
一、图卷积网络GCN定义
图卷积网络GCN实际上就是特征提取器,只不过GCN的数据对象是图。图的结构一般来说是十分不规则,可以看作是多维的一种数据。GCN精妙地设计了一种从图数据中提取特征的方法,从而让我们可以使用这些特征去对图数据进行节点分类(node classification)、图分类(graph classification)、边预测(link prediction)和获得图的嵌入表示(graph embedding),用途十分广泛。
二、GCN核心公式
H
l
+
1
=
σ
(
D
−
1
2
A
∧
D
−
1
2
H
l
W
l
)
{H^{l + 1}} = \sigma ({D^{ - \frac{1}{2}}}\mathop A\limits^ \wedge {D^{ - \frac{1}{2}}}{H^l}{W^l})
Hl+1=σ(D−21A∧D−21HlWl)
A
∧
=
A
+
I
\mathop A\limits^ \wedge = A + I
A∧=A+I
A
+
I
A+I
A+I,
I
I
I是单位矩阵,即对角线为1,其余全为0
D
∼
\mathop D\limits^ \sim
D∼是
A
∼
\mathop A\limits^ \sim
A∼的度矩阵,计算方法
D
∼
\mathop D\limits^ \sim
D∼ =
∑
A
∼
i
j
{\sum {\mathop A\limits^ \sim } _{ij}}
∑A∼ij
H
H
H是每一层的所有节点的特征向量矩阵,对于输入层的话,
H
(
0
)
{H^{(0)}}
H(0) 就等于
X
,
[
n
,
d
]
X,[n,d]
X,[n,d]维度
σ
\sigma
σ是非线性激活函数,如Relu
W
(
l
)
{W^{(l)}}
W(l) 表示的是当前层卷积变换的可训练的参数矩阵
三、官方代码
1、code
import torch
from torch_geometric.nn import MessagePassing
from torch_geometric.utils import add_self_loops, degree
class GCNConv(MessagePassing):
def __init__(self, in_channels, out_channels):
super(GCNConv, self).__init__(aggr='add') # "Add" aggregation (Step 5).
self.lin = torch.nn.Linear(in_channels, out_channels)
def forward(self, x, edge_index):
# x has shape [N, in_channels]
# edge_index has shape [2, E]
# Step 1: Add self-loops to the adjacency matrix.
edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
# Step 2: Linearly transform node feature matrix.
x = self.lin(x)
# Step 3: Compute normalization.
row, col = edge_index
deg = degree(col, x.size(0), dtype=x.dtype)
deg_inv_sqrt = deg.pow(-0.5)
norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]
# Step 4-5: Start propagating messages.
return self.propagate(edge_index, x=x, norm=norm)
def message(self, x_j, norm):
# x_j has shape [E, out_channels]
# Step 4: Normalize node features.
return norm.view(-1, 1) * x_j
2、解读
(1)初始化函数 init(self, in_channels, out_channels):
in_channels
是输入特征的维度。
out_channels
是输出特征的维度。
通过调用父类的初始化函数 super(GCNConv, self).__init__(aggr='add')
,指定信息聚合的方式为"add"(按元素相加)。
self.lin = torch.nn.Linear(in_channels, out_channels)
创建了一个线性变换层(linear transformation layer)并将其存储在 self.lin 变量中。
(2)前向传播函数 forward(self, x, edge_index):
x
是节点特征矩阵,形状为 [N, in_channels]
,其中 N
是节点的数量。
edge_index
是边索引矩阵,形状为 [2, E]
,其中 E
是边的数量。
第一步:给邻接矩阵添加自环。
通过调用 add_self_loops(edge_index, num_nodes=x.size(0))
,将自环边添加到 edge_index
中,确保每个节点都与自身相连。
第二步:线性变换节点特征。
通过 self.lin(x)
,将节点特征矩阵进行线性变换,将输入特征维度从in_channels
转换为 out_channels
。
第三步:计算归一化系数。
row, col = edge_index
:将边索引矩阵 edge_index
分解为两个分量,row
存储了边的起始节点索引,col
存储了边的终止节点索引。
deg = degree(col, x.size(0), dtype=x.dtype)
:通过调用 degree 函数计算每个节点的度(即与该节点相连的边的数量)
deg_inv_sqrt = deg.pow(-0.5)
:计算度的倒数的平方根,这是为了进行归一化处理。将度的每个元素取倒数并开平方根,得到每个节点的归一化系数。
最终,norm 存储了归一化系数,用于在消息传递过程中对节点特征进行归一化处理。这
row, col = edge_index
deg = degree(col, x.size(0), dtype=x.dtype)
deg_inv_sqrt = deg.pow(-0.5)
norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]
第四步和第五步:开始传递消息。通过调用 self.propagate(edge_index, x=x, norm=norm)
,传递消息并进行聚合操作。
(3)消息传递函数 message(self, x_j, norm):
x_j
是邻居节点的特征矩阵,形状为 [E, out_channels]
,其中 E
是边的数量。
step4:对节点特征进行归一化。通过将 norm.view(-1, 1)
乘以邻居节点特征x_j
,对节点特征进行归一化处理。
四、模型搭建
1、模型
模型根据自己需求搭建
使用GCN模型对Cora数据集进行节点分类任务的训练和测试。
本网络模型通过两个GCNConv层进行信息传递和特征更新,使用ReLU激活函数进行非线性变换,并通过log_softmax操作产生最终的节点分类输出。
# 定义GCN模型
class GCN(nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels):
super(GCN, self).__init__()
self.conv1 = GCNConv(in_channels, hidden_channels)
self.conv2 = GCNConv(hidden_channels, out_channels)
def forward(self, x, edge_index):
x = self.conv1(x, edge_index)
x = F.relu(x)
x = self.conv2(x, edge_index)
return F.log_softmax(x, dim=1)
2、完整代码
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import GCNConv
# 定义GCN模型
class GCN(nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels):
super(GCN, self).__init__()
self.conv1 = GCNConv(in_channels, hidden_channels)
self.conv2 = GCNConv(hidden_channels, out_channels)
def forward(self, x, edge_index):
x = self.conv1(x, edge_index)
x = F.relu(x)
x = self.conv2(x, edge_index)
return F.log_softmax(x, dim=1)
# 下载Cora数据集
dataset = Planetoid(root='data', name='Cora')
# 提取数据集中的节点特征和边索引
data = dataset[0]
x = data.x
edge_index = data.edge_index
# 创建GCN模型实例
model = GCN(dataset.num_features, 64, dataset.num_classes)
# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# 存储损失和准确率
losses = []
accuracies = []
# 训练循环
model.train()
for epoch in range(75):
optimizer.zero_grad()
out = model(x, edge_index)
loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask]) # 使用训练集的标签计算损失
loss.backward()
optimizer.step()
# 计算在训练集上的准确率
model.eval()
pred = out.argmax(dim=1)
train_acc = (pred[data.train_mask] == data.y[data.train_mask]).sum().item() / data.train_mask.sum().item()
losses.append(loss.item())
accuracies.append(train_acc)
# 打印当前epoch的损失和准确率
print(f"Epoch {epoch+1}: Loss: {loss.item():.4f}, Train Accuracy: {train_acc:.4f}")
# 在测试集上评估模型
model.eval()
out = model(x, edge_index)
pred = out.argmax(dim=1)
test_acc = (pred[data.test_mask] == data.y[data.test_mask]).sum().item() / data.test_mask.sum().item()
print(f"Test Accuracy: {test_acc:.4f}")
# 绘制损失和准确率随epoch变化的图表
plt.figure()
plt.plot(range(1, len(losses) + 1), losses, label='Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.figure()
plt.plot(range(1, len(accuracies) + 1), accuracies, label='Train Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.show()
3、实验结果