GCN从理论到实践——基于PyTorch的图卷积网络层实现

devtools/2025/3/1 11:08:36/

Hi,大家好,我是半亩花海。图卷积网络(Graph Convolutional Network, GCN)是一种处理图结构数据的深度学习模型。它通过聚合邻居节点的信息来更新每个节点的特征表示,广泛应用于社交网络分析、推荐系统和生物信息学等领域。本实验通过实现一个简单的 GCN 层,展示了其核心思想,并通过具体代码示例说明了 GCN 层的工作原理。

目录

一、图卷积网络的含义

二、实验展示——基于PyTorch的图卷积网络GCN)层实现

(一)实验目标

(二)实验方法

(三)实验结果分析

(四)思考与总结

三、完整代码

四、参考文章


一、图卷积网络的含义

说起图卷积神经网络(Graph Convolutional networks, GCN),可以先探讨一下卷积神经网络(CNN),CNN 中的卷积本质上就是利用共享参数的过滤器,通过计算中心像素点以及相邻像素点的加权和来实现空间特征的提取。而 GCN 也是如此,类似于图像中的卷积处理,它依赖于节点间的消息传递方法,这意味着节点与其邻居点交换信息,并相互发送消息。

在看具体的数学表达式之前,我们可以试着直观地理解 GCN 是如何工作的,可分为以下两大步骤:

  • 第一步:每个节点创建一个特征向量,表示它要发送给所有邻居的消息。
  • 第二步:消息被发送到相邻节点,这样每个节点均会从其相邻节点接收一条消息。

下面的图可视化了以上两大步骤:

那么随后该如何组合节点、接收消息呢?

由于节点间消息的数量不同,需要一个适用于任意数量的操作,通常的方法是求和或取平均值。令 H^{(l)} 表示节点 以前的特征表示,H^{(l+1)} 为整合消息后的特征表示,GCN 层定义如下:

H^{(l+1)}=\sigma\left(\hat{D}^{-1 / 2} \hat{A} \hat{D}^{-1 / 2} H^{(l)} W^{(l)}\right)

W^{(l)} 是将输入特征转换为消息的权重参数。在邻接矩阵 A 的基础上,加上单位矩阵,以便每个节点也向自身发送消息,即:\hat{A}=A+I。最后,为了取平均值的运算,需要用到矩阵 \hat{D},这是一个对角矩阵,D_{i} 表示节点 i 的邻居数。\sigma 表示一个任意的激活函数,当然,不一定是 Sigmoid,事实上,在 GNN 中通常使用基于 ReLU 的激活函数

二、实验展示——基于PyTorch的图卷积网络GCN)层实现

(一)实验目标

  • 理解 GCN 层的基本原理
  • 实现一个简单的 GCN 层,并通过手动设置权重矩阵验证其计算过程
  • 分析输入节点特征与邻接矩阵如何影响输出特征

(二)实验方法

在 PyTorch 中实现 GCN 层时,我们可以灵活地利用张量进行运算,不必定义矩阵 \hat{D},只需将求和的消息除以之后的邻居数即可。此外,线性层便是以上的权重矩阵,同时可添加偏置(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_inc_out 分别表示输入特征和输出特征的维度。
  • self.projection 是 PyTorch 中的线性变换层,将输入特征从 c_in 维映射到 c_out 维。其公式为:

output=input \cdot weight^{T}+bias

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):
    • 权重矩阵为单位矩阵,表示不改变输入特征:(该单位矩阵的值 I = 1
    • 偏置为零向量

由于权重矩阵是单位矩阵,偏置为零,线性变换的公式简化为:

output=input \cdot weight^{T}+bias=input \cdot I+0=input

因此,线性变换后的节点特征与输入特征相同

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博客


http://www.ppmy.cn/devtools/163597.html

相关文章

记一次生成core_dump文件的调查记录

1. 启用 core dump 在默认情况下,Linux 可能不生成 core dump 文件。要启用它,需要调整 shell 的 core dump 限制: 查看当前限制,执行命令: ulimit -c如果返回值为 0,则表示禁用了 core dump。 启用 co…

快速列出MS Word中所有可用字体

Word中有很多字体,虽然在字体下拉列表中提供了字体的样例,但是并不全面,例如使用Batang字体的话,数字会显示成什么效果,就无法直观的看到。 打开Word应用程序,新建一个空白文档,按AltF11打开VBE…

汽车悬架系统技术演进:从被动到全主动的革新之路(主动悬架类型对比)

在汽车工业的百年发展史中,悬架系统始终是平衡车辆性能与舒适性的关键战场。随着消费者对驾乘体验要求的不断提升,传统被动悬架已难以满足中高端车型的需求,而半主动与全主动悬架技术的崛起,正在重塑行业格局。本文将深入解析三大…

C++和OpenGL实现3D游戏编程【连载23】——几何着色器和法线可视化

欢迎来到zhooyu的C++和OpenGL游戏专栏,专栏连载的所有精彩内容目录详见下边链接: 🔥C++和OpenGL实现3D游戏编程【总览】 1、本节实现的内容 上一节课,我们在Blend软件中导出经纬球模型时,遇到了经纬球法线导致我们在游戏中模型光照显示问题,我们在Blender软件中可以通过…

【运维工具】今天就聊APM

文章目录 什么是APM?**什么是 APM?****APM 的核心目标****APM 的主要功能****APM 的应用场景****APM 的分类****APM 的优势****总结** APM框架有哪些?一、开源 APM 框架1. **Pinpoint**2. **SkyWalking**3. **Zipkin**4. **Jaeger**5. **OpenTelemetry*…

【压力测试】

压力测试 一、背景与现状1、引言2. 压力测试与不可忽视的α3. 制度演变:从公募基金到理财产品4. 行业实践仍处于早期阶段5. 理财产品压力测试的优化路径 二、压力测试介绍1. 压力测试的定义2. 压力测试的步骤 一、背景与现状 1、引言 20世纪末,随着世界…

利用DeepSeek-Kimi打通Excel与PPT的链条,自动生成数据分析报告

通过DeepSeek在Excel生成结构化的数据分析报告,再借助Kimi的PPT助手将报告自动转换为专业的PPT演示文稿,从而实现从数据到展示的一站式解决方案。 案例数据 1.一键生成数据分析报告 在下载并安装“Excel矩阵”后,我们启用DeepSeek的右侧对话…

神经网络 - 激活函数(Sigmoid 型函数)

激活函数在神经元中非常重要的。为了增强网络的表示能力和学习能力,激活函数需要具备以下几点性质: (1) 连续并可导(允许少数点上不可导)的非线性函数。可导的激活函数可以直接利用数值优化的方法来学习网络参数. (2) 激活函数及其导函数要尽可能的简单&#xff0…