task2学习心得:

这次是学习​​MessagePassing​​​基类实现“消息传递”的运行流程,继承​​MessagePassing​​基类来构造自己的图神经网络类的规范,在学习中一定需要通过逐行代码调试,观察代码运行流程(如下)。还要多参考下刘知远老师的《图神经网络导论》。

​MessagePassing​​​基类的运行流程:
(1)初始化参数聚合函数​​​aggr​​​,消息传递流向​​flow​​​,传播维度​​node_dim​​​ (2)初始化自实现函数中用到的自定义参数​​__user_args__​​,​​__fused_user_args__​​ (3)基于​​Module​​基类,调用​​forward​​函数,用于数据或参数的初始化
(4)​​propagate​​函数:
——1.检查​​edge_index​​和​​size​​参数是否符合要求,并返回​​size​​ ——2.判断​​edge_index​​是否为​​SparseTensor​​,如果满足,则执行​​message_and_aggregate​​,再执行​​update​​方法
——3.如果不满足,就先执行​​message​​方法,再执行​​aggregate​​和​​update​​方法

PS:小菜鸡赶紧恶补dl知识:

【GNN】task2-消息传递图神经网络_神经网络

文章目录

一、学习步骤

为节点生成节点表征(Node Representation)是图计算任务成功的关键,因此要利用神经网络来学习节点表征。
消息传递范式:是一种聚合邻接节点信息来更新中心节点信息的范式,它将卷积算子推广到了不规则数据领域,实现了图与神经网络的连接
消息传递GNN:遵循消息传递范式的图神经网络

  • 首先我们将学习图神经网络生成节点表征的范式–消息传递(Message Passing)范式
  • 接着我们将初步分析PyG中的​​MessagePassing​​基类,通过继承此基类我们可以方便地构造一个图神经网络。
  • 然后我们以继承​​MessagePassing​​​基类的​​GCNConv​​​类为例,学习如何通过继承​​MessagePassing​​基类来构造图神经网络。
  • 再接着我们将对​​MessagePassing​​基类进行剖析。
  • 最后我们将学习在继承​​MessagePassing​​​基类的子类中覆写​​message(),aggreate(),message_and_aggreate()​​​和​​update()​​,这些方法的规范。

二、 消息传递范式介绍

下方图片展示了基于消息传递范式的聚合邻接节点信息来更新中心节点信息的过程

【GNN】task2-消息传递图神经网络_深度学习_02

  1. 图中黄色方框部分展示的是一次邻接节点信息传递到中心节点的过程:B节点的邻接节点(A,C)的信息经过变换聚合到B节点,接着B节点信息与邻接节点聚合信息一起经过变换得到B节点的新的节点信息。同时,分别如红色和绿色方框部分所示,遵循同样的过程,C、D节点的信息也被更新。实际上,同样的过程在所有节点上都进行了一遍,所有节点的信息都更新了一遍。
  2. 这样的“邻接节点信息传递到中心节点的过程”会进行多次。如图中蓝色方框部分所示,A节点的邻接节点(B,C,D)的已经发生过一次更新的节点信息,经过变换、聚合、再变换产生了A节点第二次更新的节点信息。多次更新后的节点信息就作为节点表征

图片来源于:​​Graph Neural Network • Introduction to Graph Neural Networks​

公式描述

消息传递图神经网络遵循上述的“聚合邻接节点信息来更新中心节点信息的过程”,来生成节点表征。用【GNN】task2-消息传递图神经网络_消息传递_03表示【GNN】task2-消息传递图神经网络_深度学习_04层中节点【GNN】task2-消息传递图神经网络_深度学习_05的节点表征,【GNN】task2-消息传递图神经网络_覆写_06 表示从节点【GNN】task2-消息传递图神经网络_图神经网络_07到节点【GNN】task2-消息传递图神经网络_深度学习_05的边的属性,消息传递图神经网络可以描述为
【GNN】task2-消息传递图神经网络_消息传递_09
其中【GNN】task2-消息传递图神经网络_覆写_10表示可微分的、具有排列不变性(函数输出结果与输入参数的排列无关)的函数。

具有排列不变性的函数有,​​sum()​​​函数、​​mean()​​​函数和​​max()​​​函数。
【GNN】task2-消息传递图神经网络_图神经网络_11【GNN】task2-消息传递图神经网络_神经网络_12表示可微分的函数,如​​​MLPs​​​(多层感知器)。
此处内容来源于CREATING MESSAGE PASSING NETWORKS

3个注意

注(1):神经网络的生成节点表征的操作称为节点嵌入(Node Embedding),节点表征也可以称为节点嵌入。

在下文中,规定节点嵌入只代指神经网络生成节点表征的操作

注(2):未经过训练的图神经网络生成的节点表征还不是好的节点表征,好的节点表征可用于衡量节点之间的相似性。通过监督学习对图神经网络做很好的训练,图神经网络才可以生成好的节点表征。我们将在​​第5节​​介绍此部分内容。

注(3),节点表征与节点属性的区分:遵循被广泛使用的约定,此次学习我们也约定,节点属性data.x是节点的第0层节点表征,第【GNN】task2-消息传递图神经网络_神经网络_13层的节点表征经过一次的节点间信息传递产生第【GNN】task2-消息传递图神经网络_深度学习_14层的节点表征。不过,节点属性不单指​​data.x​​,广义上它就指节点的属性,如节点的度等。

三、MessagePassing基类初步分析

Pytorch Geometric(PyG)提供了​​MessagePassing​​​基类,它封装了“消息传递”的运行流程。通过继承​​MessagePassing​​基类,可以方便地构造消息传递图神经网络。

构造一个最简单的消息传递图神经网络类,我们只需定义​​message()​​​方法(【GNN】task2-消息传递图神经网络_神经网络_12​update()​​​方法(【GNN】task2-消息传递图神经网络_图神经网络_11,以及使用的消息聚合方案(​​aggr="add"​​​、​​aggr="mean"​​​或​​aggr="max"​​)。这一切是在以下方法的帮助下完成的:

  • ​MessagePassing(aggr="add", flow="source_to_target", node_dim=-2)​​(对象初始化方法):
  • ​aggr​​:定义要使用的聚合方案(“add”、"mean "或 “max”);
  • ​flow​​:定义消息传递的流向("source_to_target "或 “target_to_source”);
  • ​node_dim​​​:定义沿着哪个维度传播,默认值为​​-2​​​,也就是节点表征张量(Tensor)的哪一个维度是节点维度。节点表征张量​​x​​​形状为​​[num_nodes, num_features]​​​,其第0维度(也是第-2维度)是节点维度,其第1维度(也是第-1维度)是节点表征维度,所以我们可以设置​​node_dim=-2​​。
  • 注:​​MessagePassing(……)​​​等同于​​MessagePassing.__init__(……)​

1.propagate

  • ​MessagePassing.propagate(edge_index, size=None, **kwargs)​​:
  • 开始传递消息的起始调用,在此方法中​​message​​​、​​update​​等方法被调用。
  • 它以​​edge_index​​​(边的端点的索引)和​​flow​​(消息的流向)以及一些额外的数据为参数。
  • 请注意,​​propagate()​​​不局限于基于形状为​​[N, N]​​​的对称邻接矩阵进行“消息传递过程”。基于非对称的邻接矩阵进行消息传递(当图为二部图时),需要传递参数​​size=(N, M)​​。
  • 如果设置​​size=None​​,则认为邻接矩阵是对称的。

2.message

  • ​MessagePassing.message(...)​​:
  • 首先确定要给节点【GNN】task2-消息传递图神经网络_消息传递_17传递消息的边的集合:
  • 如果​​flow="source_to_target"​​​,则是【GNN】task2-消息传递图神经网络_消息传递_18的边的集合;
  • 如果​​flow="target_to_source"​​​,则是【GNN】task2-消息传递图神经网络_消息传递_19的边的集合。
  • 接着为各条边创建要传递给节点【GNN】task2-消息传递图神经网络_消息传递_17的消息,即实现【GNN】task2-消息传递图神经网络_深度学习_21函数。
  • ​MessagePassing.message(...)​​​方法可以接收传递给​​MessagePassing.propagate(edge_index, size=None, **kwargs)​​​方法的所有参数,我们在​​message()​​​方法的参数列表里定义要接收的参数,例如我们要接收​​x,y,z​​​参数,则我们应定义​​message(x,y,z)​​方法。
  • 传递给​​propagate()​​​方法的参数,如果是节点的属性的话,可以被拆分成属于中心节点的部分和属于邻接节点的部分,只需在变量名后面加上​​_i​​​或​​_j​​​。例如,我们自己定义的​​meassage​​​方法包含参数​​x_i​​​,那么首先​​propagate()​​​方法将节点表征拆分成中心节点表征和邻接节点表征,接着​​propagate()​​​方法调用​​message​​​方法并传递中心节点表征给参数​​x_i​​​。而如果我们自己定义的​​meassage​​​方法包含参数​​x_j​​​,那么​​propagate()​​​方法会传递邻接节点表征给参数​​x_j​​。
  • 我们用【GNN】task2-消息传递图神经网络_消息传递_17表示“消息传递”中的中心节点,用【GNN】task2-消息传递图神经网络_消息传递_23表示“消息传递”中的邻接节点。

3.aggregate

  • ​MessagePassing.aggregate(...)​​:
  • 将从源节点传递过来的消息聚合在目标节点上,一般可选的聚合方式有​​sum​​​,​​mean​​​和​​max​​。
  • ​MessagePassing.message_and_aggregate(...)​​:
  • 在一些场景里,邻接节点信息变换和邻接节点信息聚合这两项操作可以融合在一起,那么我们可以在此方法里定义这两项操作,从而让程序运行更加高效。

4.update

  • ​MessagePassing.update(aggr_out, ...)​​:
  • 为每个节点【GNN】task2-消息传递图神经网络_深度学习_24更新节点表征,即实现【GNN】task2-消息传递图神经网络_深度学习_25函数。此方法以​​​aggregate​​​方法的输出为第一个参数,并接收所有传递给​​propagate()​​方法的参数。

以上内容来源于​​The “MessagePassing” Base Class​​。

四、MessagePassing子类实例

我们以继承​​MessagePassing​​​基类的​​GCNConv​​​类为例,学习如何通过继承​​MessagePassing​​基类来实现一个简单的图神经网络。

GCNConv的数学定义为
【GNN】task2-消息传递图神经网络_覆写_26
其中,邻接节点的表征【GNN】task2-消息传递图神经网络_深度学习_27首先通过与权重矩阵【GNN】task2-消息传递图神经网络_消息传递_28相乘进行变换,然后按端点的度【GNN】task2-消息传递图神经网络_覆写_29进行归一化处理,最后进行求和。这个公式可以分为以下几个步骤:

  1. 向邻接矩阵添加自环边。
  2. 对节点表征做线性转换。
  3. 计算归一化系数。
  4. 归一化邻接节点的节点表征。
  5. 将相邻节点表征相加("求和 "聚合)。

步骤1-3通常是在消息传递发生之前计算的。
步骤4-5可以使用​​​MessagePassing​​基类轻松处理。该层的全部实现如下所示。

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', flow='source_to_target')
# "Add" aggregation (Step 5).
# flow='source_to_target' 表示消息从源节点传播到目标节点
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) *

​GCNConv​​​继承了​​MessagePassing​​​并以"求和"作为领域节点信息聚合方式。该层的所有逻辑都发生在其​​forward()​​方法中。

我们首先使用​​torch_geometric.utils.add_self_loops()​​函数向我们的边索引添加自循环边(步骤1),

通过调用​​torch.nn.Linear​​​实例对节点表征进行线性变换(步骤2)。
​​​propagate()​​​方法也在​​forward​​​方法中被调用,​​propagate()​​方法被调用后节点间的信息传递开始执行。

归一化系数是由每个节点的节点度得出的,它被转换为每条边的节点度。结果被保存在形状为​​[num_edges,]​​​的变量​​norm​​中(步骤3)。

在​​message()​​​方法中,我们需要通过​​norm​​​对邻接节点表征​​x_j​​进行归一化处理。

便掌握了创建一个仅包含一次“消息传递过程”的图神经网络的方法
如下方代码所示,我们可以很方便地初始化和调用它:

from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='/dataset/Cora', name='Cora')
data = dataset[0]

net = GCNConv(data.num_features, 64)
h_nodes = net(data.x, data.edge_index)
print(h_nodes.shape)

输出的结果为:

torch.Size([2708, 64])

通过串联多个这样的简单图神经网络,我们就可以构造复杂的图神经网络模型

以上主要内容来源于​​Implementing the GCN Layer​​。

附:卷积图神经网络(GCN)

GCN的定义

GCN 来源于论文“​​Semi-supervised Classification with Graph Convolutional Network​​​”,其数学定义为,
【GNN】task2-消息传递图神经网络_深度学习_30
其中【GNN】task2-消息传递图神经网络_神经网络_31表示插入自环的邻接矩阵(使得每一个节点都有一条边连接到自身),【GNN】task2-消息传递图神经网络_消息传递_32表示【GNN】task2-消息传递图神经网络_覆写_33的对角线度矩阵(对角线元素为对应节点的度,其余元素为0)。邻接矩阵可以包括不为【GNN】task2-消息传递图神经网络_神经网络_34的值,当邻接矩阵不为​​​{0,1}​​​值时,表示邻接矩阵存储的是边的权重。【GNN】task2-消息传递图神经网络_深度学习_35是对称归一化矩阵,它的节点式表述为:
【GNN】task2-消息传递图神经网络_神经网络_36
其中,【GNN】task2-消息传递图神经网络_深度学习_37【GNN】task2-消息传递图神经网络_图神经网络_38表示从源节点【GNN】task2-消息传递图神经网络_图神经网络_07到目标节点【GNN】task2-消息传递图神经网络_深度学习_05的边的对称归一化系数(默认值为1.0)。

PyG中GCNConv模块说明

​GCNConv​​构造函数接口:

GCNConv(in_channels: int, out_channels: int, improved: bool = False, cached: bool = False, add_self_loops: bool = True, normalize: bool = True, bias: bool = True, **kwargs)

其中:

  • ​in_channels​​:输入数据维度;
  • ​out_channels​​:输出数据维度;
  • ​improved​​​:如果为​​true​​​,【GNN】task2-消息传递图神经网络_覆写_41,其目的在于增强中心节点自身信息;
  • ​cached​​​:是否存储【GNN】task2-消息传递图神经网络_覆写_42的计算结果以便后续使用,这个参数只应在归纳学习(transductive learning)的场景中设置为​​​true​​(归纳学习可以简单理解为在训练、验证、测试、推理(inference)四个阶段都只使用一个数据集);
  • ​add_self_loops​​:是否在邻接矩阵中增加自环边;
  • ​normalize​​:是否添加自环边并在运行中计算对称归一化系数;
  • ​bias​​:是否包含偏置项。

详细内容请大家参阅​​GCNConv官方文档​​。

五、MessagePassing基类剖析

在​​__init__()​​​方法中,我们看到程序会检查子类是否实现了​​message_and_aggregate()​​​方法,并将检查结果赋值给​​fuse​​属性。

class MessagePassing(torch.nn.Module):
def __init__(self, aggr: Optional[str] = "add", flow: str = "source_to_target", node_dim: int = -2):
super(MessagePassing, self).__init__()
# 此处省略n行代码
# Support for "fused" message passing.
self.fuse = self.inspector.implements('message_and_aggregate')
# 此处省略n行代码

“消息传递过程”是从​​propagate​​方法被调用开始执行的。

class MessagePassing(torch.nn.Module):
# 此处省略n行代码
def propagate(self, edge_index: Adj, size: Size = None, **kwargs):
# 此处省略n行代码
# Run "fused" message and aggregation (if applicable).
if (isinstance(edge_index, SparseTensor) and self.fuse and not self.__explain__):
coll_dict = self.__collect__(self.__fused_user_args__, edge_index, size, kwargs)

msg_aggr_kwargs = self.inspector.distribute('message_and_aggregate', coll_dict)
out = self.message_and_aggregate(edge_index, **msg_aggr_kwargs)

update_kwargs = self.inspector.distribute('update', coll_dict)
return self.update(out, **update_kwargs)
# Otherwise, run both functions in separation.
elif isinstance(edge_index, Tensor) or not self.fuse:
coll_dict = self.__collect__(self.__user_args__, edge_index, size, kwargs)

msg_kwargs = self.inspector.distribute('message', coll_dict)
out = self.message(**msg_kwargs)
# 此处省略n行代码
aggr_kwargs = self.inspector.distribute('aggregate', coll_dict)
out = self.aggregate(out, **aggr_kwargs)

update_kwargs = self.inspector.distribute('update', coll_dict)
return self.update(out, **update_kwargs)

参数简介:

  • ​edge_index​​​: 边端点索引,它可以是​​Tensor​​​类型或​​SparseTensor​​类型。
  • 当flow="source_to_target"时,节点​​edge_index[0]​​​的信息将被传递到节点​​edge_index[1]​​,
  • 当flow="target_to_source"时,节点​​edge_index[1]​​​的信息将被传递到节点​​edge_index[0]​
  • ​size​​: 邻接节点的数量与中心节点的数量。
  • 对于普通图,邻接节点的数量与中心节点的数量都是N,我们可以不给size传参数,即让size取值为默认值None。
  • 对于二部图,邻接节点的数量与中心节点的数量分别记为M, N,于是我们需要给size参数传一个元组​​(M, N)​​。
  • ​kwargs​​: 图其他属性或额外的数据。

​propagate()​​​方法首先检查​​edge_index​​​是否为​​SparseTensor​​​类型以及是否子类实现了​​message_and_aggregate()​​​方法,如是就执行子类的​​message_and_aggregate​​​方法;否则依次执行子类的​​message(),aggregate(),update()​​三个方法。

六、子类中重写方法

1.message方法的覆写

前面我们介绍了,传递给​​propagate()​​​方法的参数,如果是节点的属性的话,可以被拆分成属于中心节点的部分和属于邻接节点的部分,只需在变量名后面加上​​_i​​​或​​_j​​​。现在我们有一个额外的节点属性,节点的度​​deg​​​,我们希望​​meassge​​​方法还能接收中心节点的度,我们对前面​​GCNConv​​​的​​message​​​方法进行改造得到新的​​GCNConv​​类:

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', flow='source_to_target')
# "Add" aggregation (Step 5).
# flow='source_to_target' 表示消息从源节点传播到目标节点
# 定义线性变换
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, deg=deg.view((-1, 1)))

def message(self, x_j, norm, deg_i):
# x_j has shape [E, out_channels]
# deg_i has shape [E, 1]
# Step 4: Normalize node features.
return norm.view(-1, 1) * x_j * deg_i


from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='/dataset/Cora', name='Cora')
data = dataset[0]

net = GCNConv(data.num_features, 64)
h_nodes = net(data.x, data.edge_index)
print(h_nodes.shape)

输出结果为:

torch.Size([2708, 64])

若一个数据可以被拆分成属于中心节点的部分和属于邻接节点的部分,其形状必须是​​[num_nodes, *]​​​,因此在上方代码的第​​29​​​行,我们执行了​​deg.view((-1, 1))​​​操作,使得数据形状为​​[num_nodes, 1]​​​,然后才将数据传给​​propagate()​​方法。

2.aggregate方法的覆写

在前面的例子的基础上,我们增加如下的​​aggregate​​​方法。通过观察运行结果我们可以看到,我们覆写的​​aggregate​​​方法被调用,同时在​​super(GCNConv, self).__init__(aggr='add')​​​中传递给​​aggr​​​参数的值被存储到了​​self.aggr​​属性中。

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', flow='source_to_target')
# "Add" aggregation (Step 5).
# flow='source_to_target' 表示消息从源节点传播到目标节点
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, deg=deg.view((-1, 1)))

def message(self, x_j, norm, deg_i):
# x_j has shape [E, out_channels]
# deg_i has shape [E, 1]
# Step 4: Normalize node features.
return norm.view(-1, 1) * x_j * deg_i

def aggregate(self, inputs, index, ptr, dim_size):
print('self.aggr:', self.aggr)
print("`aggregate` is called")
return super().aggregate(inputs, index, ptr=ptr, dim_size=dim_size)


from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='/dataset/Cora', name='Cora')
data = dataset[0]

net = GCNConv(data.num_features, 64)
h_nodes = net(data.x, data.edge_index)
print(h_nodes.shape)

输出的结果为:

self.aggr: add
`aggregate` is called
torch.Size([2708, 64])

3.message_and_aggregate方法的覆写

在一些案例中,“消息传递”与“消息聚合”可以融合在一起。对于这种情况,我们可以覆写​​message_and_aggregate​​​方法,在​​message_and_aggregate​​方法中一块实现“消息传递”与“消息聚合”,这样能使程序的运行更加高效。

import torch
from torch_geometric.nn import MessagePassing
from torch_geometric.utils import add_self_loops, degree
from torch_sparse import SparseTensor

class GCNConv(MessagePassing):
def __init__(self, in_channels, out_channels):
super(GCNConv, self).__init__(aggr='add', flow='source_to_target')
# "Add" aggregation (Step 5).
# flow='source_to_target' 表示消息从源节点传播到目标节点
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.
adjmat = SparseTensor(row=edge_index[0], col=edge_index[1], value=torch.ones(edge_index.shape[1]))
# 此处传的不再是edge_idex,而是SparseTensor类型的Adjancency Matrix
return self.propagate(adjmat, x=x, norm=norm, deg=deg.view((-1, 1)))

def message(self, x_j, norm, deg_i):
# x_j has shape [E, out_channels]
# deg_i has shape [E, 1]
# Step 4: Normalize node features.
return norm.view(-1, 1) * x_j * deg_i

def aggregate(self, inputs, index, ptr, dim_size):
print('self.aggr:', self.aggr)
print("`aggregate` is called")
return super().aggregate(inputs, index, ptr=ptr, dim_size=dim_size)

def message_and_aggregate(self, adj_t, x, norm):
print('`message_and_aggregate` is called')
# 没有实现真实的消息传递与消息聚合的操作

from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='/dataset/Cora', name='Cora')
data = dataset[0]

net = GCNConv(data.num_features, 64)
h_nodes = net(data.x, data.edge_index)
# print(h_nodes.shape)

输出结果为:

`message_and_aggregate` is

运行程序后我们可以看到,虽然我们同时覆写了​​message​​​方法和​​aggregate​​​方法,然而只有​​message_and_aggregate​​方法被执行。

4.update方法的覆写

from torch_geometric.datasets import Planetoid
import torch
from torch_geometric.nn import MessagePassing
from torch_geometric.utils import add_self_loops, degree
from torch_sparse import SparseTensor


class GCNConv(MessagePassing):
def __init__(self, in_channels, out_channels):
super(GCNConv, self).__init__(aggr='add', flow='source_to_target')
# "Add" aggregation (Step 5).
# flow='source_to_target' 表示消息从源节点传播到目标节点
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.
adjmat = SparseTensor(row=edge_index[0], col=edge_index[1], value=torch.ones(edge_index.shape[1]))
# 此处传的不再是edge_idex,而是SparseTensor类型的Adjancency Matrix
return self.propagate(adjmat, x=x, norm=norm, deg=deg.view((-1, 1)))

def message(self, x_j, norm, deg_i):
# x_j has shape [E, out_channels]
# deg_i has shape [E, 1]
# Step 4: Normalize node features.
return norm.view(-1, 1) * x_j * deg_i

def aggregate(self, inputs, index, ptr, dim_size):
print('self.aggr:', self.aggr)
print("`aggregate` is called")
return super().aggregate(inputs, index, ptr=ptr, dim_size=dim_size)

def message_and_aggregate(self, adj_t, x, norm):
print('`message_and_aggregate` is called')
# 没有实现真实的消息传递与消息聚合的操作

def update(self, inputs, deg):
print(deg)
return inputs


dataset = Planetoid(root='/dataset/Cora', name='Cora')
data = dataset[0]

net = GCNConv(data.num_features, 64)
h_nodes = net(data.x, data.edge_index)
# print(h_nodes.shape)

输出结果为:

`message_and_aggregate` is called
tensor([[4.],
[4.],
[6.],
...,
[2.],
[5.],
[5.]])

​update​​​方法接收聚合的输出作为第一个参数,此外还可以接收传递给​​propagate​​​方法的任何参数。在上方的代码中,我们覆写的​​update​​​方法接收了聚合的输出作为第一个参数,此外接收了传递给​​propagate​​​的​​deg​​参数。

七、总结

消息传递范式是一种聚合邻接节点信息来更新中心节点信息的范式,它将卷积算子推广到了不规则数据领域,实现了图与神经网络的连接。
该范式包含这样三个步骤:
(1)邻接节点信息变换、
(2)邻接节点信息聚合到中心节点、
(3)聚合信息变换

因为简单且强大的特性,消息传递范式现被人们广泛地使用。基于此范式,我们可以定义聚合邻接节点信息来生成中心节点表征的图神经网络。在PyG中,​​​MessagePassing​​基类是所有基于消息传递范式的图神经网络的基类,它大大地方便了我们对图神经网络的构建。

通过此节内容的学习,我们可以了解​​MessagePassing​​​基类实现“消息传递”的运行流程,可以掌握继承​​MessagePassing​​基类来构造自己的图神经网络类的规范。

作业

第一题:总结MessagePassing基类的运行流程

(1)初始化参数聚合函数​​aggr​​​,消息传递流向​​flow​​​,传播维度​​node_dim​​​ (2)初始化自实现函数中用到的自定义参数​​__user_args__​​,​​__fused_user_args__​​ (3)基于​​Module​​基类,调用​​forward​​函数,用于数据或参数的初始化
(4)​​propagate​​函数:
——1.检查​​edge_index​​和​​size​​参数是否符合要求,并返回​​size​​ ——2.判断​​edge_index​​是否为​​SparseTensor​​,如果满足,则执行​​message_and_aggregate​​,再执行​​update​​方法
——3.如果不满足,就先执行​​message​​方法,再执行​​aggregate​​和​​update​​方法

第二题:复现一个一层的图神经网络的构造

总结通过继承​​MessagePassing​​​基类来构造自己的图神经网络类的规范。
自定义一层图神经网络的数学公式:
【GNN】task2-消息传递图神经网络_消息传递_43

1.构造图神经网络

import torch
from torch.nn import functional as F
from torch_geometric.nn import MessagePassing
from torch_geometric.datasets import Planetoid


class MyGNN(MessagePassing):
"""
.. math::
\mathbf{x}^{\prime}_i = \mathbf{x}_i \cdot \mathbf{\Theta}_1 +
\sum_{j \in \mathcal{N}(i)} e_{j,i} \cdot
(\mathbf{\Theta}_2 \mathbf{x}_i - \mathbf{\Theta}_3 \mathbf{x}_j)
"""

def __init__(self, in_channels, out_channels, device):
super(MyGNN, self).__init__(aggr='add')
self.in_channels = in_channels
self.out_channels = out_channels

self.lin1 = torch.nn.Linear(in_channels, out_channels).to(device)
self.lin2 = torch.nn.Linear(in_channels, out_channels).to(device)
self.lin3 = torch.nn.Linear(in_channels, out_channels).to(device)

def forward(self, x, edge_index):
a = self.lin1(x)
b = self.lin2(x)
out = self.propagate(edge_index, a=a, b=b)
return self.lin3(x) + out

def message(self, a_i, b_j):
out = a_i - b_j
return out

def __repr__(self):
return '{}({}, {})'.format(self.__class__.__name__, self.in_channels,
self.out_channels)

2.训练过程

device = torch.device('cuda:0')

dataset = Planetoid(root='dataset/Cora', name='Cora')
model = MyGNN(in_channels=dataset.num_features, out_channels=dataset.num_classes, device=device)
print(model)

data = dataset[0].to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)

model.train()
for epoch in range(200):
optimizer.zero_grad()
out = model(data.x, data.edge_index).to(device)
pred = out.argmax(dim=1)
accuracy = int((pred[data.test_mask] == data.y[data.test_mask]).sum()) / data.test_mask.sum()
loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()

if epoch % 10 == 0:
print("Train Epoch: {:3} Accuracy: {:.2f}%".format(epoch, accuracy.item() * 100.0))

3.输出结果

MyGNN(1433, 7)
Train Epoch: 0 Accuracy: 11.80%
Train Epoch: 10 Accuracy: 57.80%
Train Epoch: 20 Accuracy: 59.00%
Train Epoch: 30 Accuracy: 59.40%
Train Epoch: 40 Accuracy: 59.20%
Train Epoch: 50 Accuracy: 59.10%
Train Epoch: 60 Accuracy: 59.20%
Train Epoch: 70 Accuracy: 59.50%
Train Epoch: 80 Accuracy: 59.70%
Train Epoch: 90 Accuracy: 59.80%
Train Epoch: 100 Accuracy: 59.90%
Train Epoch: 110 Accuracy: 59.90%
Train Epoch: 120 Accuracy: 59.90%
Train Epoch: 130 Accuracy: 59.90%
Train Epoch: 140 Accuracy: 59.90%
Train Epoch: 150 Accuracy: 59.80%
Train Epoch: 160 Accuracy: 59.90%
Train Epoch: 170 Accuracy: 60.00%
Train Epoch: 180 Accuracy: 59.90%
Train Epoch: 190 Accuracy: 59.90%

reference

mark

  • 图神经网络2021顶会论文(先mark下:​​javascript:void(0)​​)
  • GNN入门的极佳综述,追着这篇综述可以看完2018年前所有具代表性的论文,所需理论都在上面那篇里面
    Wu, Zonghan, et al. A comprehensive survey on graph neural networks. IEEE Transactions on Neural Networks and Learning Systems (2020).
  • 从函数空间和积分变换角度解释GCN并提出FastGCN,最优雅最好玩的一篇
    Chen, Jie, Tengfei Ma, and Cao Xiao. Fastgcn: fast learning with graph convolutional networks via importance sampling. arXiv preprint arXiv:1801.10247 (2018).
  • 利用双曲空间做节点嵌入
    Chami, Ines, et al. Hyperbolic graph convolutional neural networks. Advances in neural information processing systems. 2019.
  • 做GNN的绝对躲不开Leskovec大佬的俄音英语