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

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:提供了构建神经网络所需的模块和函数。

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_neighbors
    return 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 表示无连接。

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 nn


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);  # 线性层

    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_neighbors
        return node_feats


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)

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/979491.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

给虚拟机配置IP

虚拟机IP这里一共有三个地方要设置,具体说明如下: (1)配置vm虚拟机网段 如果不进行设置,每次启动机器时都可能是随机的IP,不方便我们后续操作。具体操作是:点击编辑→虚拟网络编辑器 选择VMne…

【免费】YOLO[笑容]目标检测全过程(yolo环境配置+labelimg数据集标注+目标检测训练测试)

一、yolo环境配置 这篇帖子是我试过的,非常全,很详细【cudaanacondapytorchyolo(ultralytics)】 yolo环境配置 二、labelimg数据集标注 可以参考下面的帖子,不过可能会出现闪退的问题,安装我的流程来吧 2.1 labelimg安装 label…

mapbox基础,使用geojson加载heatmap热力图层

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性1.3 ☘️heatmap热力图层样式二、🍀使用geojs…

Python 课堂点名桌面小程序

一、场景分析 闲来无事,老婆说叫我开发一个课堂点名桌面小程序,给她在课堂随机点名学生问问题。 人生苦短,那就用 Python 给她写一个吧。 二、依赖安装 因为要用到 excel,所以安装两个依赖: pip install openpyxl…

蓝桥杯 路径之谜

路径之谜 题目描述 小明冒充 XX 星球的骑士,进入了一个奇怪的城堡。 城堡里边什么都没有,只有方形石头铺成的地面。 假设城堡地面是 nnnn 个方格。如下图所示。 按习俗,骑士要从西北角走到东南角。可以横向或纵向移动,但不能斜着走…

在鸿蒙HarmonyOS手机上安装hap应用

一、下载工具 安装hap包需要用到小工具 。 二、解压到目录后,进入该文件夹,打开命令行,如下图 三、将下载好的hap包放入刚才解压的文件夹内(假设hap包文件名为app.hap) 四、连接好手机和电脑,手机需要打…

Android APK组成编译打包流程详解

Android APK(Android Package)是 Android 应用的安装包文件,其组成和打包流程涉及多个步骤和文件结构。以下是详细的说明: 一、APK 的组成 APK 是一个 ZIP 格式的压缩包,包含应用运行所需的所有文件。解压后主要包含以…

自然语言处理:词频-逆文档频率

介绍 大家好,博主又来给大家分享知识了。本来博主计划完成稠密向量表示的内容分享后,就开启自然语言处理中文本表示的讲解。可在整理分享资料的时候,博主发现还有个知识点,必须得单独拎出来好好说道说道。 这就是TF-IDF&#xf…

esp8266 rtos sdk开发环境搭建

1. 安装必要的工具 1.1 安装 Git Git 用于从远程仓库克隆代码,你可以从Git 官方网站下载 Windows 版本的安装程序。安装过程中可保持默认设置,安装完成后,在命令提示符(CMD)或 PowerShell 中输入git --version&#…

pytest下放pytest.ini文件就导致报错:ERROR: file or directory not found: #

pytest下放pytest.ini文件就导致报错:ERROR: file or directory not found: # 如下: 项目文件目录如下: pytest.ini文件内容: [pytest] addopts -v -s --alluredir ./allure-results # 自动添加的命令行参数:# -…

Blender调整最佳渲染清晰度

1.渲染采样调高 512 2.根据需要 开启AO ,开启辉光 , 开启 屏幕空间反射 3.调高分辨率 4096x4096 100% 分辨率是清晰度的关键 , 分辨率不高 , 你其他参数调再高都没用 4.世界环境开启体积散射 , 可以增强氛围感 5.三点打光法 放在模型和相机45夹角上 白模 白模带线条 成品

Vllm进行Qwen2-vl部署(包含单卡多卡部署及爬虫请求)

1.简介 阿里云于今年9月宣布开源第二代视觉语言模型Qwen2-VL,包括 2B、7B、72B三个尺寸及其量化版本模型。Qwen2-VL具备完整图像、多语言的理解能力,性能强劲。 相比上代模型,Qwen2-VL 的基础性能全面提升,可以读懂不同分辨率和…

xr-frame 3D Marker识别,扬州古牌坊 3D识别技术稳定调研

目录 识别物体规范 3D Marker 识别目标文件 map 生成 生成任务状态解析 服务耗时: 对传入的视频有如下要求: 对传入的视频建议: 识别物体规范 为提高Marker质量,保证算法识别效果,可参考Marker规范文档 Marker规…

Windows环境下SuperMapGIS 11i 使用达梦数据库

1. 环境介绍: 1.1. 操作系统: windows server 2019 1.2. GIS 软件: 1.2.1. GIS 桌面 supermap-idesktopx-11.3.0-windows-x64-bin 下载链接:SuperMap技术资源中心|为您提供全面的在线技术服务 安装教程:绿色版&…

redis的下载和安装详解

一、下载redis安装包 进入redis官网查看当前稳定版本: https://redis.io/download/发现此时的稳定版本是6.2.4, 此时可以去这个网站下载6.2.4稳定版本的tar包。 暂时不考虑不在windows上使用redis,那样将无法发挥redis的性能 二、上传tar…

Prometheus + Grafana 监控

Prometheus Grafana 监控 官网介绍:Prometheus 是一个开源系统 监控和警报工具包最初由 SoundCloud 构建。自 2012 年成立以来,许多 公司和组织已经采用了 Prometheus,并且该项目具有非常 活跃的开发人员和用户社区。它现在是一个独立的开源…

使用Semantic Kernel:对DeepSeek添加自定义插件

SemanticKernel介绍 Semantic Kernel是一个SDK,它将OpenAI、Azure OpenAI等大型语言模型与C#、Python和Java等传统编程语言集成在一起。Semantic Kernel通过允许您定义插件来实现这一点。 为什么需要添加插件? 大语言模型虽然具有强大的自然语言理解和…

Grok3使用体验与模型版本对比分析

文章目录 Grok的功能DeepSearch思考功能绘画功能Grok 3的独特功能 Grok 3的版本和特点与其他AI模型的比较 最新新闻:Grok3被誉为“地球上最聪明的AI” 最近,xAI公司正式发布了Grok3,并宣称其在多项基准测试中展现了惊艳的表现。据官方消息&am…

QT——c++界面编程库

非界面编程 QT编译的时候,依赖于 .pro 配置文件: SOURCES: 所有需要参与编译的 .cpp 源文件 HEADERS:所有需要参与编译的.h 头文件 QT:所有需要参与编译的 QT函数库 .pro文件一旦修改,注意需要键盘按 ctrls 才能加载最新的配置文…

第十四届蓝桥杯大赛软件赛国赛C/C++大学C组

A 【跑步计划——日期问题】-CSDN博客 B 【残缺的数字】-CSDN博客 C 题目 代码 #include <bits/stdc.h> using namespace std;void change(int &x) {int sum 0, t x;while(t){sum t % 10;t / 10;}x - sum; } int main() {int n;cin >> n;int ans 0;…