Hi,大家好,我是半亩花海。图卷积网络(Graph Convolutional Network, GCN)是一种处理图结构数据的深度学习模型。它通过聚合邻居节点的信息来更新每个节点的特征表示,广泛应用于社交网络分析、推荐系统和生物信息学等领域。本实验通过实现一个简单的 GCN 层,展示了其核心思想,并通过具体代码示例说明了 GCN 层的工作原理。
目录
一、图卷积网络的含义
二、实验展示——基于PyTorch的图卷积网络(GCN)层实现
(一)实验目标
(二)实验方法
(三)实验结果分析
(四)思考与总结
三、完整代码
四、参考文章
一、图卷积网络的含义
说起图卷积神经网络(Graph Convolutional networks, GCN),可以先探讨一下卷积神经网络(CNN),CNN 中的卷积本质上就是利用共享参数的过滤器,通过计算中心像素点以及相邻像素点的加权和来实现空间特征的提取。而 GCN 也是如此,类似于图像中的卷积处理,它依赖于节点间的消息传递方法,这意味着节点与其邻居点交换信息,并相互发送消息。
在看具体的数学表达式之前,我们可以试着直观地理解 GCN 是如何工作的,可分为以下两大步骤:
- 第一步:每个节点创建一个特征向量,表示它要发送给所有邻居的消息。
- 第二步:消息被发送到相邻节点,这样每个节点均会从其相邻节点接收一条消息。
下面的图可视化了以上两大步骤:
那么随后该如何组合节点、接收消息呢?
由于节点间消息的数量不同,需要一个适用于任意数量的操作,通常的方法是求和或取平均值。令 表示节点 以前的特征表示,
为整合消息后的特征表示,GCN 层定义如下:
是将输入特征转换为消息的权重参数。在邻接矩阵 A 的基础上,加上单位矩阵,以便每个节点也向自身发送消息,即:
。最后,为了取平均值的运算,需要用到矩阵
,这是一个对角矩阵,
表示节点
的邻居数。
表示一个任意的激活函数,当然,不一定是 Sigmoid,事实上,在 GNN 中通常使用基于 ReLU 的激活函数。
二、实验展示——基于PyTorch的图卷积网络(GCN)层实现
(一)实验目标
(二)实验方法
在 PyTorch 中实现 GCN 层时,我们可以灵活地利用张量进行运算,不必定义矩阵 ,只需将求和的消息除以之后的邻居数即可。此外,线性层便是以上的权重矩阵,同时可添加偏置(bias)。基于 PyTorch,定义GCN层的具体步骤如下所示。
1. 导入必要的库
import torch
import torch.nn as nn
torch:
PyTorch 深度学习框架的核心库,用于张量操作和自动求导。torch.nn:
提供了构建神经网络所需的模块和函数。
GCNLayer%EF%BC%89" name="2.%C2%A0%E5%AE%9A%E4%B9%89%E5%9B%BE%E5%8D%B7%E7%A7%AF%E5%B1%82%EF%BC%88GCNLayer%EF%BC%89">2. 定义图卷积层(GCNLayer)
class GCNLayer(nn.Module):def __init__(self, c_in, c_out):"""Inputs::param c_in: 输入特征维度:param c_out: 输出特征维度"""super().__init__()self.projection = nn.Linear(c_in, c_out) # 线性层
GCNLayer
继承自nn.Module
,是 PyTorch 中所有神经网络模块的基类。c_in
和c_out
分别表示输入特征和输出特征的维度。self.projection
是 PyTorch 中的线性变换层,将输入特征从c_in
维映射到c_out
维。其公式为:
3. 前向传播
def forward(self, node_feats, adj_matrix):"""输入::param node_feats: 节点特征表示,大小为 [batch_size, num_nodes, c_in]:param adj_matrix: 邻接矩阵,大小为 [batch_size, num_nodes, num_nodes]:return: 更新后的节点特征"""num_neighbors = adj_matrix.sum(dim=-1, keepdims=True) # 各节点的邻居数node_feats = self.projection(node_feats) # 将特征转化为消息# 各邻居节点消息求和并求平均node_feats = torch.bmm(adj_matrix, node_feats)node_feats = node_feats / num_neighborsreturn node_feats
- 输入参数:
node_feats:
表示每个节点的特征,形状为[batch_size, num_nodes, c_in]
。adj_matrix:
图的邻接矩阵,形状为[batch_size, num_nodes, num_nodes]
。
- 步骤解析:
- 计算邻居数量:
num_neighbors = adj_matrix.sum(dim=-1, keepdims=True)
计算每个节点的邻居数量(包括自身)。- 线性变换:
node_feats = self.projection(node_feats)
对节点特征进行线性变换。- 邻居信息聚合:
torch.bmm(adj_matrix, node_feats)
使用批量矩阵乘法(Batch Matrix Multiplication)将邻居节点的消息加权求和。- 归一化:
node_feats = node_feats / num_neighbors
将聚合结果按邻居数量归一化,得到每个节点的更新特征。
4. 实验数据准备
node_feats = torch.arange(8, dtype=torch.float32).view(1, 4, 2)
adj_matrix = torch.Tensor([[[1, 1, 0, 0],[1, 1, 1, 1],[0, 1, 1, 1],[0, 1, 1, 1]]])
print("节点特征:\n", node_feats)
print("添加自连接的邻接矩阵:\n", adj_matrix)
(1)节点特征
node_feats
是一个形状为[1, 4, 2]
的张量,表示一个批次中 4 个节点的特征,每个节点有 2 维特征。
节点特征:tensor([[[0., 1.],[2., 3.],[4., 5.],[6., 7.]]])
(2)邻接矩阵
adj_matrix
是一个形状为[1, 4, 4]
的张量,表示图的邻接矩阵。
添加自连接的邻接矩阵:tensor([[[1., 1., 0., 0.],[1., 1., 1., 1.],[0., 1., 1., 1.],[0., 1., 1., 1.]]])
- 邻接矩阵中的元素为 1 表示两个节点之间存在连接,0 表示无连接。
GCN%E5%B1%82%E5%B9%B6%E8%AE%BE%E7%BD%AE%E6%9D%83%E9%87%8D" name="5.%C2%A0%E5%88%9D%E5%A7%8B%E5%8C%96GCN%E5%B1%82%E5%B9%B6%E8%AE%BE%E7%BD%AE%E6%9D%83%E9%87%8D">5. 初始化GCN层并设置权重
layer = GCNLayer(c_in=2, c_out=2)
# 初始化权重矩阵
layer.projection.weight.data = torch.Tensor([[1., 0.], [0., 1.]])
layer.projection.bias.data = torch.Tensor([0., 0.])
- 创建一个
GCNLayer
实例,输入特征维度为 2,输出特征维度也为 2。 - 手动初始化权重矩阵和偏置(bia):
- 权重矩阵为单位矩阵,表示不改变输入特征:
(该单位矩阵的值
)
- 偏置为零向量:
- 权重矩阵为单位矩阵,表示不改变输入特征:
由于权重矩阵是单位矩阵,偏置为零,线性变换的公式简化为:
因此,线性变换后的节点特征与输入特征相同。
6. 前向传播并计算输出特征
# 将节点特征和添加自连接的邻接矩阵输入 GCN 层
with torch.no_grad():out_feats = layer(node_feats, adj_matrix)print("节点特征:\n", node_feats)
print("添加自连接的邻接矩阵:\n", adj_matrix)
print("节点输出特征:\n", out_feats)
- 使用
torch.no_grad()
关闭梯度计算,避免不必要的内存开销。 - 调用
layer(node_feats, adj_matrix)
进行前向传播,得到更新后的节点特征。 - 输出结果:
节点输出特征:tensor([[[1., 2.],[3., 4.],[4., 5.],[4., 5.]]])
(三)实验结果分析
1. 输入数据
(1)节点特征
节点特征是一个大小为 [1, 4, 2]
的张量,表示一个批次中有 4 个节点,每个节点有 2 维特征。具体值如下:
tensor([[[0., 1.],[2., 3.],[4., 5.],[6., 7.]]])
- 节点 0 的特征为:
[0., 1.]
- 节点 1 的特征为:
[2., 3.]
- 节点 2 的特征为:
[4., 5.]
- 节点 3 的特征为:
[6., 7.]
(2)邻接矩阵
邻接矩阵是一个大小为 [1, 4, 4]
的张量,表示 4 个节点之间的连接关系。具体值如下:
tensor([[[1., 1., 0., 0.],[1., 1., 1., 1.],[0., 1., 1., 1.],[0., 1., 1., 1.]]])
- 节点 0 的邻居为:节点 0 和节点 1。
- 节点 1 的邻居为:节点 0、节点 1、节点 2 和节点 3。
- 节点 2 的邻居为:节点 1、节点 2 和节点 3。
- 节点 3 的邻居为:节点 1、节点 2 和节点 3。
如何通过邻接矩阵来判断每个节点的邻居是什么?——看值为1的索引是多少,那么邻居便是多少。
[[1., 1., 0., 0.], # 节点0的邻居:值为1的列索引为[0, 1],即节点0和节点1。[1., 1., 1., 1.], # 节点1的邻居:值为1的列索引为[0, 1, 2, 3],即节点0、节点1、节点2和节点3。[0., 1., 1., 1.], # 节点2的邻居:值为1的列索引为[1, 2, 3],即节点1、节点2和节点3。[0., 1., 1., 1.]] # 节点3的邻居:值为1的列索引为[1, 2, 3],即节点1、节点2和节点3。
本实验中的图 G 的图示如下:
2. 输出特征分析
经GCN层的前向传播后,得到输出特征,其形状为 [1, 4, 2]
的张量,表示更新后的节点特征。
tensor([[[1., 2.],[3., 4.],[4., 5.],[4., 5.]]])
GCN 层通过邻接矩阵聚合邻居节点的消息。具体计算如下:对于每个节点,将其邻居节点的特征相加。再将聚合后的特征除以邻居数量,得到平均特征,即最终的输出特征。下面逐节点分析输出特征的计算过程:
(1)节点0的计算
- 邻居节点:节点0和节点1。
- 聚合特征:
[0., 1.] + [2., 3.] = [2., 4.]
- 邻居数量:2
- 平均特征:
[2., 4.] / 2 = [1., 2.]
(2)节点1的计算
- 邻居节点:节点0、节点1、节点2和节点3。
- 聚合特征:
[0., 1.] + [2., 3.] + [4., 5.] + [6., 7.] = [12., 16.]
- 邻居数量:4
- 平均特征:
[12., 16.] / 4 = [3., 4.]
(3)节点2的计算
- 邻居节点:节点1、节点2和节点3。
- 聚合特征:
[2., 3.] + [4., 5.] + [6., 7.] = [12., 15.]
- 邻居数量:3
- 平均特征:
[12., 15.] / 3 = [4., 5.]
(4)节点3的计算
- 邻居节点:节点1、节点2和节点3。
- 聚合特征:
[2., 3.] + [4., 5.] + [6., 7.] = [12., 15.]
- 邻居数量:3
- 平均特征:
[12., 15.] / 3 = [4., 5.]
通过上述分析可以看出,GCN 层的核心思想是通过聚合邻居节点的信息来更新每个节点的特征表示。具体来说:
- 线性变换 :首先对输入特征进行线性变换(本实验中权重矩阵为单位矩阵,因此特征未发生变化)。
- 邻居信息聚合 :通过邻接矩阵将邻居节点的特征加权求和。
- 归一化 :将聚合结果按邻居数量归一化,得到最终的节点特征。
(四)思考与总结
1. 思考
如上所见,第一个节点的输出值是其自身和第二个节点的平均值,其他节点同理。当然,在具体实践中,我们还希望允许节点之间的消息传递不仅仅局限于邻居节点,还可以通过应用多个 GCN 层来实现,而很多的 GNN 即是由多个 GCN 和非线性(如 ReLU)的组合构建而成,如下图所示:
通过以上 GCN 层的运算示例,发现一个问题,即节点 3 和 4 的输出相同,这是因为它们具有相同的相邻节点(包括自身)输入,再取均值,所得到的值便一样了。这在大部分情况下并不合理。
2. 总结
本实验通过实现一个简单的 GCN 层,展示了图卷积网络的核心思想——通过聚合邻居节点的信息来更新节点特征。通过手动设置权重矩阵和偏置,我们验证了 GCN 层的计算过程,并分析了输入特征与邻接矩阵对输出特征的影响。实验结果表明,GCN 层能够有效地捕捉图结构中的局部信息。
未来可以进一步扩展该实验:
- 引入非线性激活函数 :在 GCN 层中加入 ReLU 等非线性激活函数,增强模型的表达能力。
- 多层 GCN :堆叠多个 GCN 层,以捕获更高阶的邻居信息。
- 真实数据集实验 :在实际图数据集(如 Cora 或 Citeseer)上测试 GCN 模型的性能。
- 优化算法 :结合梯度下降等优化算法,训练 GCN 模型以完成特定任务(如节点分类或链接预测)。
通过这些扩展,可以更全面地理解图卷积网络的工作原理及其在实际问题中的应用价值。
三、完整代码
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
@Project : GNN/GCN
@File : gcn1.py
@IDE : PyCharm
@Author : 半亩花海
@Date : 2025/02/28 21:33
"""
import torch
import torch.nn as nnclass GCNLayer(nn.Module):def __init__(self, c_in, c_out):"""Inputs::param c_in: 输入特征:param c_out: 输出特征"""super().__init__()self.projection = nn.Linear(c_in, c_out); # 线性层def forward(self, node_feats, adj_matrix):"""输入:param node_feats: 节点特征表示,大小为[batch_size,num_nodes,c_in]:param adj_matrix: 邻接矩阵:[batch_size,num_nodes,num_nodes]:return:"""num_neighbors = adj_matrix.sum(dim=-1, keepdims=True) # 各节点的邻居数node_feats = self.projection(node_feats) # 将特征转化为消息# 各邻居节点消息求和并求平均node_feats = torch.bmm(adj_matrix, node_feats)node_feats = node_feats / num_neighborsreturn node_featsnode_feats = torch.arange(8, dtype=torch.float32).view(1, 4, 2)
adj_matrix = torch.Tensor([[[1, 1, 0, 0],[1, 1, 1, 1],[0, 1, 1, 1],[0, 1, 1, 1]]])
print("节点特征:\n", node_feats)
print("添加自连接的邻接矩阵:\n", adj_matrix)layer = GCNLayer(c_in=2, c_out=2)
# 初始化权重矩阵
layer.projection.weight.data = torch.Tensor([[1., 0.], [0., 1.]])
layer.projection.bias.data = torch.Tensor([0., 0.])# 将节点特征和添加自连接的邻接矩阵输入 GCN 层
with torch.no_grad():out_feats = layer(node_feats, adj_matrix)print("节点输出特征:\n", out_feats)
四、参考文章
[1] 实战-----基于 PyTorch 的 GNN 搭建_pytorch gnn-CSDN博客
[2] 图神经网络简单理解 — — 附带案例_图神经网络实例-CSDN博客
[3] 一文快速预览经典深度学习模型(二)——迁移学习、半监督学习、图神经网络(GNN)、联邦学习_迁移学习 图神经网络-CSDN博客