课程链接:CS224W: Machine Learning with Graphs
课程视频:【课程】斯坦福 CS224W: 图机器学习 (2019 秋 | 英字)
目录
1. 前言
我们上节课讲到的一些node embedding的方法,node embedding的方法有两个关键的部分——encoder(编码器)和similarity function(相似函数计算)。然而,上节课提到的一些encoder的方法实质上都是shallow embedding,这些方法都有一些局限性:
- 需要 O ( ∣ V ∣ ) O(|V|) O(∣V∣)个参数,节点之间的参数无法共享,每个节点都有自己的embedding。
- 转换的过程是确定的,对于未知节点(没有出现过的节点),没有办法生成它们的embedding。
- 并不能很好地利用节点的特性。
这节课的内容基于图神经网络(Graph neural networks)来展开一些deep methods(deep graph encoders),即
E N C ( v ) = multiple layers of non-linear transformations of graph structure ENC(v)= \text{multiple layers of non-linear transformations of graph structure} ENC(v)=multiple layers of non-linear transformations of graph structure
这节课讲到的deep graph encoders可以和上节课讲到的similarity function结合起来使用。
我们前面也提到,现在的深度学习框架主要适用于规则的网格和序列,而相比于这些,网络通常是复杂的、无序的、动态的,这就导致很难直接将现在的深度学习框架直接应用于网络上。
但是,我们同样可以从现在的深度学习框架中得到启发。
1.1 From image to graph
CNN on image
我们可以从图像的卷积神经网络(关于卷积神经网络可以看【深度学习实战】从零开始深度学习(三):卷积神经网络与计算机视觉)的思想得到启发。
在卷积神经网络的卷积层,会有一个滑动滤波器沿着像素滑动,捕获图片信息。在网络中也可以这样,可以有一个“滑动滤波器”,对于节点来说,捕获节点接收到的其他邻居节点传来的消息
h
i
h_i
hi,最后进行求和,即
∑
i
W
i
h
i
\sum_i W_i h_i
∑iWihi。
对于上面的右图来说,可以直接将“滑动滤波器”应用到网络上,而对于下面这两个更加复杂的真实网络,怎样去应用CNN的思想呢?
1.2 一个简单的想法
一个最简单的想法,就是将“滑动滤波器”应用于邻接矩阵上,将滤波器得到的结果作为深度神经网络的输入,最后训练得到输出。
但是,这个想法存在很明显的不足:
(1)首先,神经网络的输入层包含 O ( N ) O(N) O(N)个参数,且随着网络规模的增加,参数会不断增加,使得神经网络的工作效率很低;
(2)其次,当网络的规模发生变化时(比如从6个节点的网络变成7个节点的网络),模型得重新训练;
(3)第三,网络节点的编号一发生变化,邻接矩阵就会发生变化,模型将不适用。
因此,我们今天这节课主要讲解下面这几个内容:
- Basics of deep learning for graphs
- Graph Convolutional Networks
- Graph Attention Networks (GAT)
- Practical tips and demos
2. Basics of deep learning for graphs
2.1 Graph Convolutional Networks 的计算图
假设有图$G:
- V V V是节点集合(vertex set)
- A A A是邻接矩阵
- X ∈ R m × ∣ V ∣ X \in \Bbb{R}^{m \times |V|} X∈Rm×∣V∣是节点的特征矩阵(a matrix of node features)。这里的节点特征,根据不同的网络有不同的定义——如在社交网络里面,节点特征就包括用户资料、用户年龄等;在生物网络里面,节点的特征就包括基因表达、基因序列等;如果节点没有特征,就可以用one-hot编码或者Vector of constant 1: [ 1 , 1 , … , 1 ] [1, 1, …, 1] [1,1,…,1]来表示节点的特征。
需要应用GCN,我们首先要定义GCN的计算图(感觉类似于CNN中的“滑动滤波器”),这个计算图不仅能保留图 G G G的结构,同时还能合并节点的相邻特性。从信息传播的角度来看,节点的邻居确定了节点的计算图(Node’s neighborhood defines a computation graph)。
那么,要得到这个计算图,我们需要学习的就是信息在图中如何传播,从而计算节点的特征(Learn how to propagate information across the graph to compute node features)。
一个想法——Aggregate Neighbours,核心思想在于基于局部网络连接来生成Node embeddings(Generate node embeddings based on local network neighborhoods)。如下面这个图:
例如图中节点A的embedding决定于其邻居节点 { B , C , D } \{B,C,D\} {B,C,D},而这些节点又受到它们各自的邻居节点的影响。图中的“黑箱”可以看成是整合其邻居节点信息的操作,它有一个很重要的属性——其操作应该是顺序(order invariant)无关的,如求和、求平均、求最大值这样的操作,可以采用神经网络来获取。这样顺序无关的聚合函数符合网络节点无序性的特征,当我们对网络节点进行重新编号时,我们的模型照样可以使用。
那么,对于每个节点来说,它的计算图就由其邻居节点的数量来决定——
模型的深度可以自己定义(Model can be of arbitrary depth):
- Nodes have embeddings at each layer
- Layer-0节点 u u u的embedding是其节点特征向量 x u x_u xu
- Layer-K embedding gets information from nodes that
are K hops away,例如Layer-1中B节点的embedding就由Layer-0中节点A的特征向量 x A x_A xA和节点C的特征向量 x C x_C xC经过神经网络计算得到。
那么,不同方法之间的区别在于“黑箱”里面到底选择怎样的计算函数。
最简单的一个方法是计算平均值——
2.2 模型——Deep Encoder
基于上述思想,我们可以给出Deep encoder的数学表达式:
首先,
h
v
k
h_v^k
hvk表示节点
v
v
v在第
k
k
k层的embedding。Layer-0节点
v
v
v的embedding是其节点特征向量
x
v
x_v
xv,因此
h
v
0
=
x
v
h_v^0=x_v
hv0=xv。节点
v
v
v在第
k
k
k层的embedding通过其
k
−
1
k-1
k−1层的邻居节点的embedding由神经网络计算得到。我们最后得到的节点的特征向量为最后一层,即第
K
K
K层的输出。
2.3 模型训练
上面给出了deep encoder的模型,我们需要训练的参数是 W k W_k Wk和 B k B_k Bk。和传统的机器学习一样,我们需要定义损失函数来帮助训练模型。
我们可以看到,如果参数 W k W_k Wk接近于零,那么 h v k ≈ σ ( B k h v k − 1 ) h_v^k \approx \sigma(B_k h_v^{k-1}) hvk≈σ(Bkhvk−1),那整个训练过程相当于就是在学习节点自身的特征,而没有考虑邻居节点(网络结构对它的影响)。如果参数 B k B_k Bk接近于零,那么 h v k ≈ σ ( W k ∑ u ∈ N ( v ) h u k − 1 ∣ N ( v ) ∣ ) h_v^k \approx \sigma(W_k \sum_{u \in N(v)} \frac {h_u^{k-1}}{|N(v)|}) hvk≈σ(Wk∑u∈N(v)∣N(v)∣huk−1),那么模型就忽略了节点自身的特征。因此,我们需要模型能够平衡“个人特色”和“周围连接”这两者之间的关系。可以根据具体的应用,选择损失函数,应用随机梯度下降法进行优化。
我们可以将上面的数学表达式等价地写成矩阵形式:
H ( l + 1 ) = σ ( H ( l ) W 0 ( l ) + A ~ H ( l ) W 1 ( l ) ) H^{(l+1)}=\sigma(H^{(l)} W_0^{(l)} + \tilde{A} H^{(l)} W_1^{(l)}) H(l+1)=σ(H(l)W0(l)+A~H(l)W1(l))
A ~ = D − 1 2 A D − 1 2 \tilde{A}=D^{-\frac{1}{2}} A D^{-\frac{1}{2}} A~=D−21AD−21
H ( l ) = [ h 1 ( l ) T , ⋯ , h N ( l ) T ] T H^{(l)}=[h_1^{(l)^T},\cdots,h_N^{(l)^T}]^T H(l)=[h1(l)T,⋯,hN(l)T]T
无监督训练
无监督训练的方法和上节课的思想一样——“Similar” nodes have similar embeddings,无监督训练只依靠节点自身的特征和图的结构。无监督训练的损失函数可以从上节课提到的任意一种方法得到——Random walks (node2vec, DeepWalk, struc2vec)、Graph factorization、Node proximity in the graph等等。
有监督训练
有监督训练的损失函数和具体的任务相关。比如节点分类任务,其损失函数如下:
2.4 模型回顾
步骤 | 案例 |
---|---|
(1) Define a neighborhood aggregation function | ![]() |
(2) Define a loss function on the embeddings | |
(3) Train on a set of nodes, i.e., a batch of compute graphs | ![]() |
(4) Generate embeddings for nodes as needed | ![]() |
可以看到,相较于1.2节提到的简单的想法,我们这一节提出的模型有很大的优势:
- 所有节点共享相同的聚合参数。模型参数与网络结构无关。
- 模型具有很强的推广性,对于新的图或者图中新的节点同样适用。
3. Graph Convolutional Networks and GraphSAGE
3.1 GraphSAGE Idea
前面提到的模型,在黑箱里面的操作是取平均值。GraphSAGE的想法是采用一个通用的aggregation函数来表示黑箱的运算,可以采用不同的方法聚合其邻居,然后再将其与自身特征拼接。——
h v k = σ ( [ A k ⋅ A G G ( { h u k − 1 , ∀ u ∈ N ( v ) } ) , B k h v k − 1 ] ) h_v^k=\sigma([A_k \cdot AGG(\{h_u^{k-1}, \forall u \in N(v)\}),B_kh_v^{k-1}]) hvk=σ([Ak⋅AGG({huk−1,∀u∈N(v)}),Bkhvk−1])
常用的聚合函数有以下几种:
聚合函数 | 表达式 |
---|---|
Mean | 计算邻居节点的加权平均:![]() |
Pool | 变换邻居向量并运用对称向量函数(式中的\gamma可以是element-wise mean或max)。 A G G = γ ( { Q h n k − 1 , ∀ u ∈ N ( v ) } ) AGG=\gamma(\{Qh_n^{k-1},\forall u \in N(v)\}) AGG=γ({Qhnk−1,∀u∈N(v)}) |
LSTM | Apply LSTM to reshuffled of neighbors. A G G = L S T M ( [ h u k − 1 , ∀ u ∈ π ( N ( v ) ) ] ) AGG=LSTM([h_u^{k-1},\forall u \in \pi(N(v))]) AGG=LSTM([huk−1,∀u∈π(N(v))]) |
3.2 小结
许多聚合可以通过(稀疏)矩阵操作更有效地执行
3.3 关于GNN的一些论文
主题 | 论文 |
---|---|
Tutorials and overviews | 1. Relational inductive biases and graph networks (Battaglia et al., 2018), 2. Representation learning on graphs: Methods and applications (Hamilton et al., 2017) |
Attention-based neighborhood aggregation | Graph attention networks (Hoshen, 2017; Velickovic et al., 2018; Liu et al., 2018) |
Embedding entire graphs | 1. Graph neural nets with edge embeddings (Battaglia et al., 2016; Gilmer et. al., 2017) 2. Embedding entire graphs (Duvenaud et al., 2015; Dai et al., 2016; Li et al., 2018) and graph pooling (Ying et al., 2018, Zhang et al., 2018) 3. Graph generation and relational inference (You et al., 2018; Kipf et al., 2018) 4. How powerful are graph neural networks (Xu et al., 2017) |
Embedding nodes | 1. Varying neighborhood: Jumping knowledge networks (Xu et al., 2018), GeniePath (Liu et al., 2018) 2. Position-aware GNN (You et al. 2019) |
Spectral approaches to graph neural networks | 1. Spectral graph CNN & ChebNet (Bruna et al., 2015; Defferrard et al., 2016) 2. Geometric deep learning (Bronstein et al., 2017; Monti et al., 2017) |
Other GNN techniques | 1. Pre-Training Graph Neural Networks for Generic Structural Feature Extraction (Hu et al., 2019) 2. GNNExplainer: Generating Explanations for Graph Neural Networks(Ying et al., 2019) |
4. Graph Attention Networks (GAT)
定义
a
v
u
a_{vu}
avu代表节点
u
u
u传给节点
v
v
v的重要程度。在我们前面提到的模型中:
a
v
u
=
1
/
∣
N
(
v
)
∣
a_{vu}=1/|N(v)|
avu=1/∣N(v)∣,
a
v
u
a_{vu}
avu根据图的结构定义,即节点
v
v
v的所有邻居节点
u
u
u传递的信息对
v
v
v来说是同样重要的。然而,对于一个节点来说,有些邻居节点更加重要,那么,可以采用注意力机制给不同的节点分配权重。
加入注意力机制的首要目标和任务就是指定图中每个节点的不同邻居的任意重要性。
Attention Mechanism 注意力机制
假设参数 a v u a_{vu} avu可以通过注意力函数(注意力机制) a a a计算得到,则注意力系数(attention coefficients) e v u e_{vu} evu可以通过两个节点之间的信息流得到(comptue across pairs of nodes based on their messages), e v u e_{vu} evu表示节点 u u u的信息对节点 v v v的重要性:
e v u = a ( W k h u k − 1 , W k h v k − 1 ) e_{vu}=a(W_k h_u^{k-1},W_k h_v^{k-1}) evu=a(Wkhuk−1,Wkhvk−1)
使用softmax函数对系数进行归一化,以便在不同的邻居节点间具有可比性。
a v u = e x p ( e u v ) ∑ k ∈ N ( v ) e x p ( e v k ) a_{vu}=\frac {exp(e_{uv})}{\sum_{k \in N(v)}exp(e_{vk})} avu=∑k∈N(v)exp(evk)exp(euv)
h v k = σ ( ∑ u ∈ N ( v ) a u v W k h u k − 1 ) h_v^k=\sigma(\sum_{u \in N(v)}a_{uv}W_kh_u^{k-1}) hvk=σ(u∈N(v)∑auvWkhuk−1)
那么,注意力机制 a a a应该怎么选择呢?可以蚕蛹一层简单的神经网络来作为注意力机制函数,在训练过程中,这层神经网络的相关参数(即注意力机制的相关参数)也会跟着模型一起训练。
下面是关于注意力机制的一些属性:
5. Hand-on Session
这一部分主要是一些在python上的实操。
5.1 熟悉一下pytorch
首先,先通过MNIST手写数字数据集熟悉一下pytorch。这一部分我之前也写过,详细的内容可以看我之前的文章——【深度学习实战】从零开始深度学习(二):多层全连接神经网络与MNIST手写数字分类。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import sklearn.metrics as metrics
'''
首先先熟悉一下pytorch的使用,以MNIST手写数据集作为案例
'''
BATCH_SIZE = 32
## 定义变换格式
transform = transforms.Compose([transforms.ToTensor()])
## 下载训练集
trainset = torchvision.datasets.MNIST(root = './data', train = True, download = True, transform = transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size = BATCH_SIZE, shuffle = True, num_workers = 0)
## 下载测试集
testset = torchvision.datasets.MNIST(root = './data', train = False, download = True, transform = transform)
testloader = torch.utils.data.DataLoader(testset, batch_size = BATCH_SIZE, shuffle = False, num_workers = 0)
## 建立模型
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
# 28x28x1 => 26x26x32
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3)
self.d1 = nn.Linear(26 * 26 * 32, 128)
self.d2 = nn.Linear(128, 10)
def forward(self, x):
# 32x1x28x28 => 32x32x26x26
x = self.conv1(x)
x = F.relu(x)
# flatten => 32 x (32*26*26)
x = x.flatten(start_dim = 1)
#x = x.view(32, -1)
# 32 x (32*26*26) => 32x128
x = self.d1(x)
x = F.relu(x)
# logits => 32x10
logits = self.d2(x)
out = F.softmax(logits, dim=1)
return out
## 定义参数
learning_rate = 0.001
num_epochs = 5
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = MyModel()
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
## 开始训练
for epoch in range(num_epochs):
train_running_loss = 0.0
train_acc = 0.0
## training step
for i, (images, labels) in enumerate(trainloader):
images = images.to(device)
labels = labels.to(device)
## forward + backprop + loss
logits = model(images)
loss = criterion(logits, labels)
optimizer.zero_grad()
loss.backward()
## update model params
optimizer.step()
train_running_loss += loss.detach().item()
train_acc += (torch.argmax(logits, 1).flatten() == labels).type(torch.float).mean().item()
print('Epoch: %d | Loss: %.4f | Train Accuracy: %.2f' %(epoch, train_running_loss / i, train_acc/i))
输出结果如下:
Epoch: 0 | Loss: 1.5569 | Train Accuracy: 0.91
Epoch: 1 | Loss: 1.4922 | Train Accuracy: 0.97
Epoch: 2 | Loss: 1.4833 | Train Accuracy: 0.98
Epoch: 3 | Loss: 1.4778 | Train Accuracy: 0.99
Epoch: 4 | Loss: 1.4751 | Train Accuracy: 0.99
最后,我们在测试集上进行应用——
test_acc = 0.0
for i, (images, labels) in enumerate(testloader, 0):
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
test_acc += (torch.argmax(outputs, 1).flatten() == labels).type(torch.float).mean().item()
preds = torch.argmax(outputs, 1).flatten().cpu().numpy()
print('Test Accuracy: %.2f'%(test_acc/i))
在测试集上的准确率达到了98%。
5.2 利用Pytorch开始GNN
5.2.1 Setup
安装PyTorch geometry(用于创建图形神经网络,官方文档)和TensorboardX(用于可视化训练进程)。
安装PyTorch geometry——用于创建图形神经网络
首先,我们先查一下我们的pytorch的版本。要求至少安装 PyTorch 1.2.0 版本:
python -c "import torch; print(torch.__version__)"
接着,查询对应pytorch安装的CUDA的版本:
python -c "import torch; print(torch.version.cuda)"
然后,安装Pytorch geometry的软件包。需要注意的是,这里的${CUDA}
是前面查询到的CUDA的版本(cpu, cu92, cu101, cu102)
,${TORCH}
是前面查到的pytorch的版本。(建议将pytorch升级到最新版本再进行安装)
pip install torch-scatter==latest+${CUDA} -f https://pytorch-geometric.com/whl/torch-${TORCH}.html
pip install torch-sparse==latest+${CUDA} -f https://pytorch-geometric.com/whl/torch-${TORCH}.html
pip install torch-cluster==latest+${CUDA} -f https://pytorch-geometric.com/whl/torch-${TORCH}.html
pip install torch-spline-conv==latest+${CUDA} -f https://pytorch-geometric.com/whl/torch-${TORCH}.html
pip install torch-geometric
比如我这里查到Pytorch的版本是1.5.1(按照官网的教程,pytorch版本为1.5.0或者1.5.1的按照1.5.0来安装),CUDA的版本是10.2,那么我的安装语句如下:
pip install torch-scatter==latest+cu102 -f https://pytorch-geometric.com/whl/torch-1.5.0.html
pip install torch-sparse==latest+cu102 -f https://pytorch-geometric.com/whl/torch-1.5.0.html
pip install torch-cluster==latest+cu102 -f https://pytorch-geometric.com/whl/torch-1.5.0.html
pip install torch-spline-conv==latest+cu102 -f https://pytorch-geometric.com/whl/torch-1.5.0.html
pip install torch-geometric
安装TensorboardX——用于可视化训练进程
pip install tensorboardX
然后import我们需要的包——
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch_geometric.nn as pyg_nn
import torch_geometric.utils as pyg_utils
import time
from datetime import datetime
import networkx as nx
import numpy as np
import torch
import torch.optim as optim
from torch_geometric.datasets import TUDataset
from torch_geometric.datasets import Planetoid
from torch_geometric.data import DataLoader
import torch_geometric.transforms as T
from tensorboardX import SummaryWriter
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
5.2.2 搭建模型
首先,GNNStack
是最基本的GNN模型,含有不同的卷积层,可以用于节点分类和图分类。
build_conv_model
根据不同的任务选择不同的卷积层类型。
对于节点相关的任务,采用图卷积神经网络模型,
使用GCNConv
实现。GCNConv的数学定义参考论文Semi-Supervised Classification with Graph Convolutional Networks:
X ′ = D ^ − 1 2 A ^ D ^ − 1 2 X Θ X'=\hat{D}^{-\frac{1}{2}} \hat{A} \hat{D}^{-\frac{1}{2}} X \Theta X′=D^−21A^D^−21XΘ
其中 A ^ = A + I \hat{A}=A+I A^=A+I,也就是经过自循环处理的邻接矩阵; D i i ^ = ∑ j = 0 A i j ^ \hat{D_{ii}}=\sum_{j=0} \hat{A_{ij}} Dii^=∑j=0Aij^; Θ \Theta Θ是滤波器参数的矩阵。
对于图相关的任务,选择图同构网络(graph isomorphism network)。图同构网络的数学定义来自论文How Powerful are Graph Neural Networks?,可以结合这篇博客(《Graph Neural Networks多强大?》阅读笔记)
理解这篇文章。可以使用pytorch geometirc提供的GINConv
方法实现。
class GNNStack(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim, task='node'):
super(GNNStack, self).__init__()
self.task = task
self.convs = nn.ModuleList()
self.convs.append(self.build_conv_model(input_dim, hidden_dim))
self.lns = nn.ModuleList()
self.lns.append(nn.LayerNorm(hidden_dim))
self.lns.append(nn.LayerNorm(hidden_dim))
for l in range(2):
self.convs.append(self.build_conv_model(hidden_dim, hidden_dim))
# post-message-passing
self.post_mp = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim), nn.Dropout(0.25),
nn.Linear(hidden_dim, output_dim))
if not (self.task == 'node' or self.task == 'graph'):
raise RuntimeError('Unknown task.')
self.dropout = 0.25
self.num_layers = 3
def build_conv_model(self, input_dim, hidden_dim):
# refer to pytorch geometric nn module for different implementation of GNNs.
if self.task == 'node':
return pyg_nn.GCNConv(input_dim, hidden_dim)
else:
return pyg_nn.GINConv(nn.Sequential(nn.Linear(input_dim, hidden_dim),
nn.ReLU(), nn.Linear(hidden_dim, hidden_dim)))
def forward(self, data):
x, edge_index, batch = data.x, data.edge_index, data.batch
if data.num_node_features == 0:
x = torch.ones(data.num_nodes, 1)
for i in range(self.num_layers):
x = self.convs[i](x, edge_index)
emb = x
x = F.relu(x)
x = F.dropout(x, p=self.dropout, training=self.training)
if not i == self.num_layers - 1:
x = self.lns[i](x)
if self.task == 'graph':
x = pyg_nn.global_mean_pool(x, batch)
x = self.post_mp(x)
return emb, F.log_softmax(x, dim=1)
def loss(self, pred, label):
return F.nll_loss(pred, label)
pyg_nn.GCNConv
和GINConv
实质上是MessagePassing
的实例,它们通过构建四种基本的方法——Message computation(信息计算)、Aggregation(信息聚合)、Update(更新)和Pooling(池化),定义了图卷积层的不同形式。下面是直接利用MessagePassing
来构建模型的一个示例。
我们通过下面这些模块来构建MessagePassing
模型:
- aggr=‘add’: The aggregation method to use (“add”, “mean” or “max”).
- propagate(): The initial call to start propagating messages. Takes in the edge indices and any other data to pass along (e.g. to update node embeddings).
- message(): Constructs messages to node i. Takes any argument which was initially passed to propagate().
- update(): Updates node embeddings. Takes in the output of aggregation as first argument and any argument which was initially passed to propagate().
class CustomConv(pyg_nn.MessagePassing):
def __init__(self, in_channels, out_channels):
super(CustomConv, self).__init__(aggr='add') # "Add" aggregation.
self.lin = nn.Linear(in_channels, out_channels)
self.lin_self = nn.Linear(in_channels, out_channels)
def forward(self, x, edge_index):
# x has shape [N, in_channels]
# edge_index has shape [2, E]
# Add self-loops to the adjacency matrix.
edge_index, _ = pyg_utils.remove_self_loops(edge_index)
# Transform node feature matrix.
self_x = self.lin_self(x)
#x = self.lin(x)
return self_x + self.propagate(edge_index, size=(x.size(0), x.size(0)), x=self.lin(x))
def message(self, x_i, x_j, edge_index, size):
# Compute messages
# x_j has shape [E, out_channels]
row, col = edge_index
deg = pyg_utils.degree(row, size[0], dtype=x_j.dtype)
deg_inv_sqrt = deg.pow(-0.5)
norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]
return x_j
def update(self, aggr_out):
# aggr_out has shape [N, out_channels]
return aggr_out
5.2.3 模型训练
模型训练
def train(dataset, task, epoches = 200):
if task == 'graph':
data_size = len(dataset)
loader = DataLoader(dataset[:int(data_size * 0.8)], batch_size=64, shuffle=True)
test_loader = DataLoader(dataset[int(data_size * 0.8):], batch_size=64, shuffle=True)
else:
test_loader = loader = DataLoader(dataset, batch_size=64, shuffle=True)
# build model
model = GNNStack(max(dataset.num_node_features, 1), 32, dataset.num_classes, task=task)
opt = optim.Adam(model.parameters(), lr=0.01)
# train
for epoch in range(epoches):
total_loss = 0
model.train()
for batch in loader:
#print(batch.train_mask, '----')
opt.zero_grad()
embedding, pred = model(batch)
label = batch.y
if task == 'node':
pred = pred[batch.train_mask]
label = label[batch.train_mask]
loss = model.loss(pred, label)
loss.backward()
opt.step()
total_loss += loss.item() * batch.num_graphs
total_loss /= len(loader.dataset)
if epoch % 10 == 0:
test_acc = test(test_loader, model)
print("Epoch {}. Loss: {:.4f}. Test accuracy: {:.4f}".format(
epoch, total_loss, test_acc))
return model
模型测试
def test(loader, model, is_validation=False):
model.eval()
correct = 0
for data in loader:
with torch.no_grad():
emb, pred = model(data)
pred = pred.argmax(dim=1)
label = data.y
if model.task == 'node':
mask = data.val_mask if is_validation else data.test_mask
# node classification: only evaluate on nodes in test set
pred = pred[mask]
label = data.y[mask]
correct += pred.eq(label).sum().item()
if model.task == 'graph':
total = len(loader.dataset)
else:
total = 0
for data in loader.dataset:
total += torch.sum(data.test_mask).item()
return correct / total
5.2.4 开始训练
首先是关于图的分类任务:
dataset = TUDataset(root='./data/ENZYMES', name='ENZYMES')
dataset = dataset.shuffle()
task = 'graph'
model = train(dataset, task)
接下来是关于节点的分类任务:
dataset = Planetoid(root='./data/cora', name='cora')
task = 'node'
model = train(dataset, task, 200)
节点分类任务的输出:
Epoch 0. Loss: 2.0109. Test accuracy: 0.1980
Epoch 10. Loss: 0.4109. Test accuracy: 0.7620
Epoch 20. Loss: 0.0203. Test accuracy: 0.7460
Epoch 30. Loss: 0.0165. Test accuracy: 0.7370
Epoch 40. Loss: 0.0334. Test accuracy: 0.7310
Epoch 50. Loss: 0.0448. Test accuracy: 0.7420
Epoch 60. Loss: 0.0272. Test accuracy: 0.7070
Epoch 70. Loss: 0.1022. Test accuracy: 0.7570
Epoch 80. Loss: 0.0353. Test accuracy: 0.7700
Epoch 90. Loss: 0.0571. Test accuracy: 0.7410
Epoch 100. Loss: 0.0019. Test accuracy: 0.7260
Epoch 110. Loss: 0.0009. Test accuracy: 0.7490
Epoch 120. Loss: 0.0017. Test accuracy: 0.7400
Epoch 130. Loss: 0.0204. Test accuracy: 0.7390
Epoch 140. Loss: 0.0005. Test accuracy: 0.7550
Epoch 150. Loss: 0.0329. Test accuracy: 0.7500
Epoch 160. Loss: 0.0011. Test accuracy: 0.7690
Epoch 170. Loss: 0.0027. Test accuracy: 0.7680
Epoch 180. Loss: 0.0003. Test accuracy: 0.7560
Epoch 190. Loss: 0.0012. Test accuracy: 0.7600
结果可视化:
color_list = ["red", "orange", "green", "blue", "purple", "brown"]
loader = DataLoader(dataset, batch_size=64, shuffle=True)
embs = []
colors = []
for batch in loader:
emb, pred = model(batch)
embs.append(emb)
colors += [color_list[y-1] for y in batch.y]
embs = torch.cat(embs, dim=0)
xs, ys = zip(*TSNE().fit_transform(embs.detach().numpy()))
plt.scatter(xs, ys, color=colors)
5.3 Learning unsupervised embeddings with graph autoencoders 有监督学习
GNNs很好地适应了其他神经方法的框架,可以作为自动编码器技术、预训练和多任务学习方法的一部分,等等。在这里,我们进一步探索神经网络表示的思想,通过建立一个图自动编码器学习这些表示在一个完全无监督的方式。与上一个示例相反,在训练这种表示时,我们没有使用给定的节点标签。相反,我们在低维空间中对网络中的节点进行编码,这样嵌入的节点可以被解码成原始网络的重构。我们在编码器中使用了图形卷积层。你可以再次使用这里的TensorBoardX来可视化训练的进程。
class Encoder(torch.nn.Module):
def __init__(self, in_channels, out_channels):
super(Encoder, self).__init__()
self.conv1 = pyg_nn.GCNConv(in_channels, 2 * out_channels, cached=True)
self.conv2 = pyg_nn.GCNConv(2 * out_channels, out_channels, cached=True)
def forward(self, x, edge_index):
x = F.relu(self.conv1(x, edge_index))
return self.conv2(x, edge_index)
def train(epoch):
model.train()
optimizer.zero_grad()
z = model.encode(x, train_pos_edge_index)
loss = model.recon_loss(z, train_pos_edge_index)
loss.backward()
optimizer.step()
writer.add_scalar("loss", loss.item(), epoch)
def test(pos_edge_index, neg_edge_index):
model.eval()
with torch.no_grad():
z = model.encode(x, train_pos_edge_index)
return model.test(z, pos_edge_index, neg_edge_index)
writer = SummaryWriter("./log/" + datetime.now().strftime("%Y%m%d-%H%M%S"))
dataset = Planetoid("/tmp/citeseer", "Citeseer", T.NormalizeFeatures())
data = dataset[0]
channels = 16
dev = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print('CUDA availability:', torch.cuda.is_available())
# encoder: written by us; decoder: default (inner product)
model = pyg_nn.GAE(Encoder(dataset.num_features, channels)).to(dev)
labels = data.y
data.train_mask = data.val_mask = data.test_mask = data.y = None
data = model.split_edges(data)
x, train_pos_edge_index = data.x.to(dev), data.train_pos_edge_index.to(dev)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(1, 201):
train(epoch)
auc, ap = test(data.test_pos_edge_index, data.test_neg_edge_index)
writer.add_scalar("AUC", auc, epoch)
writer.add_scalar("AP", ap, epoch)
if epoch % 10 == 0:
print('Epoch: {:03d}, AUC: {:.4f}, AP: {:.4f}'.format(epoch, auc, ap))