一、GraphEmbedding
1. DeepWalk
- DeepWalk在无向图上
- 采用随机游走得到一个节点序列
- d: embedding维度, γ \gamma γ: 迭代次数
2. LINE
- DeepWalk在无向图上, LINE在有向图上
- 适用于大规模的图上, 表示节点之间的结构信息
- 一阶: 局部的结构信息
- 二阶: 节点的邻居, 共享邻居的节点可能是相似的
- 一阶二阶embedding训练完成之后, 直接拼接组合成一个embedding
3. Node2vec
- 同质性: BFS
- 结构性: DFS
- 基于概率的随机游走
- node2vec计算过程
- p, q参数控制同质性和结构性的阈值
4. Struc2vec
- 之前的node embedding方式都是基于近邻关系, 但是有些节点没有近邻, 但也有相似的结构性
- 1.定义近邻关系
- 我们允许序列的点与另一序列的多个连续的点相对应
- 我们允许序列的点与另一序列的多个连续的点相对应
- 2.构建多层带权重图
- 3.顶点采样序列
- 4.使用skip-gram生成embedding
- 5.Struc2vec适用于节点分类中,其结构标识比邻居标识更重要是。采用Struc2vec效果好
5. SDNE (Structural Deep Network Embedding)
- Deepwalk,LINE,node2vec,struc2vec都使用了浅层的结构, SDNE采用多个非线性层来铺货node的embedding
6. 总结
- 1.DeepWalk:采用随机游走,形成序列,采用skip-gram方式生成节点embedding。
- 2.node2vec:不同的随机游走策略, 基于概率的随机游走,形成序列,类似skip-gram方式生成节点embedding。
- 3.LINE:捕获节点的一阶和二阶相似度,分别求解,再将一阶二阶拼接在一起,作为节点的embedding
- 4.struc2vec:对图的结构信息进行捕获,在其结构重要性大于邻居重要性时,有较好的效果。
- 5.SDNE:采用了多个非线性层的方式捕获一阶二阶的相似性。
二、消息传递范式
1. 步骤
- 邻接节点信息变换
- 邻接节点信息聚合到中心节点
- 聚合信息变换
2. 介绍
- 公式:
x i ( k ) = γ ( k ) ( x i ( k − 1 ) , □ j ∈ N ( i ) ϕ ( k ) ( x i ( k − 1 ) , x j ( k − 1 ) , e j , i ) ) x_i^{(k)} = \gamma^{(k)} (x_i^{(k-1)}, \square j \in N_{(i)} \phi^{(k)} (x_i^{(k-1)}, x_j^{(k-1)}, e_{j,i})) xi(k)=γ(k)(xi(k−1),□j∈N(i)ϕ(k)(xi(k−1),xj(k−1),ej,i)) - 过程:
- 1.图中黄色方框部分展示的是一次邻接节点信息传递到中心节点的过程:B节点的邻接节点(A,C)的信息经过变换后聚合到B节点,接着B节点信息与邻接节点聚合信息一起经过变换得到B节点的新的节点信息。同时,分别如红色和绿色方框部分所示,遵循同样的过程,C、D节点的信息也被更新。实际上,同样的过程在所有节点上都进行了一遍,所有节点的信息都更新了一遍。
- 2.这样的“邻接节点信息传递到中心节点的过程”会进行多次。如图中蓝色方框部分所示,A节点的邻接节点(B,C,D)的已经发生过一次更新的节点信息,经过变换、聚合、再变换产生了A节点第二次更新的节点信息。多次更新后的节点信息就作为节点表征。
3. PyG中的MessagePassing基类
- 它实现了消息传播的自动处理, 我们只需要定义函数
ϕ
\phi
ϕ,函数
γ
\gamma
γ, 以及消息聚合方案
- ϕ \phi ϕ: 即 message();
- γ \gamma γ: 即 update();
- 消息聚合方案: 即 aggr = “add”, “mean”, “max”
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__()
"""
- aggr: 定义要使用的聚合函数 ("add", "mean", "max")
- flow: 定义消息传递的流向 ("source_to_target", "target_to_source")
- node_dim: 定义沿着哪个轴线传播
"""
self.aggr = aggr
assert self.aggr in ['add', 'mean', 'max', None]
self.flow = flow
assert self.flow in ['source_to_target', 'target_to_source']
self.node_dim = node_dim
def propagate(self, edge_index: Adj, size: Size=None, **kwargs):
"""
- 开始传播消息的起始调用, 以edge_index(边的端点的索引)和flow(消息的流向)以及一些额外的数据为参数
- 不仅可以在形状为[N, N]的对称邻接矩阵中交换信息, 还可以通过传递size=(N, M)作为额外参数,
例如在二部图的形状为[N, M]的一般稀疏分配矩阵中交换信息
- 如果设置size=None, 则假定邻接矩阵是对称的
- 对于有两个独立的节点集合和索引集合的二部图, 并且每个集合都持有自己的信息, 可以传递一个元组参数,
即x=(x_N, x_M), 来标记信息的区分
"""
...
def message(self, x_j: Tensor) -> Tensor:
"""
- 如果flow="source_to_target", (j,i) ∈ ε的边的集合
- 如果flow="_to_source", (i,j) ∈ ε的边的集合
- 接着个为各条边创建要传递给节点i的消息, 即实现 ϕ 函数
- message函数接受最初传递给propagate(edge_index, size=None, **kwargs)函数的所有参数
- 传递给propagate()的张量可以映射到各自的节点i和j上, 只需在变量名后面加上 _i 或 _j。
把i称为消息传递的目标中心节点, j称为邻接节点
"""
return x_j
def aggregate(self, inputs: Tensor, index: Tensor,
ptr: Optional[Tensor] = None,
dim_size: Optional[int] = None) -> Tensor:
"""
- 将从源节点传递过来的消息聚合在目标节点上, 一般可选的聚合方式有sum, mean, max
"""
...
def message_and_aggregate(self, adj_t: SparseTensor) -> Tensor:
"""
- 邻接节点信息变换和邻接节点信息聚合这两项操作可以融合在一起, 程序运行更加高效
"""
raise NotImplementedError
def update(self, inputs: Tensor) -> Tensor:
"""
- 为每个节点 i ∈ V 更新节点表征, 即实现 γ 函数, 该函数以聚合函数的输出为第一个参数, 并接收所有传递给propagate()函数的参数
"""
return inputs
4. 继承MessagePassing类的GCNConv
- GCN的数学公式为
x i ( k ) = ∑ j ∈ N ( i ) ∪ { i } 1 d e g ( i ) ⋅ d e g ( j ) ⋅ ( Θ ⋅ x j ( k − 1 ) ) x_i^{(k)} = \sum\limits_{j \in N(i) \cup \{i\}} \frac{1}{\sqrt{deg(i)} \cdot \sqrt{deg(j)}} \cdot(\Theta \cdot x_j^{(k-1)}) xi(k)=j∈N(i)∪{i}∑deg(i)⋅deg(j)1⋅(Θ⋅xj(k−1)) - 1.向邻接矩阵添加自环边
- 2.线性转换节点特征矩阵
- 3.计算归一化系数
- 4.归一化 j j j中的节点特征
- 5.将相邻节点特征相加("求和"聚合)
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')
self.lin = torch.nn.Linear(in_channels, out_channels)
def forward(self, x, edge_index):
# step1: 添加自环边
edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
# step2: 线性转换
x = self.lin(x)
# step3: 归一化
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]
print("`forward` is called")
return self.propagate(edge_index, x=x, norm=norm)
# 重写message方法
def message(self, x_j, edge_index, norm):
print("`message` is called")
return norm.view(-1, 1) * x_j
# 重写propagate方法
def propagate(self, edge_index: Adj, size: Size=None, **kwargs):
print("`propagate` is called")
return super().propagate(edge_index=edge_index, size=size, **kwargs)
# 重写aggregate方法
def aggregate(self, inputs, index, ptr, dim_size):
print(self.aggr)
print("`aggregate` is called")
return super().aggregate(inputs, index, ptr=ptr, dim_size=dim_size)
# 重写message_and_aggregate
def message_and_aggregate(self, adj_t, x, norm):
print("`message_and_and_aggregate` is called")
return super().message_and_aggregate(adj_t, x, norm)
# 重写update方法
def update(self, inputs: Tensor) -> Tensor:
print("`update` is called")
return super().update(inputs)
from torch_geometric.datasets import Planetoid
dataset = Planetoid(root='./input/Cora', name='Cora')
data = dataset[0]
net = GCNConv(data.num_features, 64)
h_nodes = net(data.x, data.edge_index)
print(h_nodes.shape)
#`forward` is called
#`propagate` is called
#`message` is called
#add
#`aggregate` is called
#`update` is called
#torch.Size([2708, 64])
- 消息传递是从propagate()方法开始的
- propagate()首先检查edge_index是否为SparseTensor类型, 以及是否子类继承了message_and_aggregate()方法
- 如果是则执行message_and_aggregate()方法
- 否则依次执行 message(),aggregate(),update() 三个方法
5. 重写message方法
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: 添加自环边
edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
# Step 2: 线性转换
x = self.lin(x)
# Step 3: 归一化.
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='input/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])
三、作业
1.请总结MessagePassing基类的运行流程。
-
根据MessagePassing的公式可得:
x i ( k ) = γ ( k ) ( x i ( k − 1 ) , □ j ∈ N ( i ) ϕ ( k ) ( x i ( k − 1 ) , x j ( k − 1 ) , e j , i ) ) x_i^{(k)} = \gamma^{(k)} (x_i^{(k-1)}, \square j \in N_{(i)} \phi^{(k)} (x_i^{(k-1)}, x_j^{(k-1)}, e_{j,i})) xi(k)=γ(k)(xi(k−1),□j∈N(i)ϕ(k)(xi(k−1),xj(k−1),ej,i)) -
1.需要定义三个函数 γ \gamma γ 即update(), □ \square □ 即aggregate()聚合函数, ϕ \phi ϕ 即message()
-
2.定义MessagePassing类, 继承nn.Module, 实现forward函数
-
3.forward中调用消息传递的起始函数propagate(),
- propagate()需要检查edge_index,
- 判断是执行 message_and_aggregate() 还是 message(), aggregate()
-
4.message()负责定义节点消息传递的方向[‘source_to_target’, ‘target_to_source’]
-
5.aggregate()负责定义节点聚合使用的聚合方式 [‘max’, ‘add’, ‘sum’]
-
6.update()负责更新节点表征
2.请复现一个一层的图神经网络的构造,总结通过继承MessagePassing基类来构造自己的图神经网络类的规范。
- 定义GCNConv类, 继承MessagePassing
import torch.nn as nn
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, aggr="add"):
super(GCNConv, self).__init__(aggr=aggr)
self.lin = 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: 添加自环边
edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
# Step 2: 线性转换
x = self.lin(x)
# Step 3: 归一化
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: 开始消息传递, 调用propagate().
return self.propagate(edge_index, x=x, norm=norm)
def message(self, edge_index, x_j, norm):
# x_j has shape [E, out_channels]
# Step 4: 节点归一化
return norm.view(-1, 1) * x_j
- 定义GCN
import torch.nn.functional as F
class GCN(nn.Module):
# torch.nn.Module 是所有神经网络单元的基类
def __init__(self, feature, hidden, classes):
super(GCN, self).__init__() ###复制并使用Net的父类的初始化方法,即先运行nn.Module的初始化函数
self.conv1 = GCNConv(feature, hidden)
self.conv2 = GCNConv(hidden, classes)
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.conv1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, training=self.training)
x = self.conv2(x, edge_index)
return F.log_softmax(x, dim=1)
- 训练
dataset = Planetoid(root='input/Cora', name='Cora')
data = dataset[0].to(device)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = GCN(dataset.num_features, 16, dataset.num_classes).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)
loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
#测试
model.eval()
_, pred = model(data).max(dim=1)
correct = float(pred[data.test_mask].eq(data.y[data.test_mask]).sum().item())
acc = correct / data.test_mask.sum().item()
print('Accuracy: {:.4f}'.format(acc))
# Accuracy: 0.7930
四、 参考资料
- DataWhale开源学习资料:https://github.com/datawhalechina/team-learning-nlp/tree/master/GNN
- PyG官方文档MessgePassing:https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#torch_geometric.nn.conv.message_passing.MessagePassing