Datawhale GNN组队学习-Task 03

基于图神经网络的节点表征学习

经典论文笔记(GCN MoNet ChebNet GraphSAGE)

  1. Semi-Supervised Classification with Graph Convolutional Networks(GCN)

这篇文章提出了一种图卷积网络(GCN),该网络可以直接用于处理图结构数据。从本质上讲,GCN 是谱图卷积(spectral graph convolution) 的局部一阶近似(localized first-order approximation)。GCN的另一个特点在于模型可以在图的边数上线性伸缩,并且可以学习编码局部图结构和节点特征的隐藏层表示。

l 谱图卷积

图上的谱卷积可以定义为信号与滤波器()在傅里叶域的乘积:

其中,为归一化拉普拉斯的特征向量矩阵,为相应的特征值矩阵(对角矩阵),为的图傅里叶变换。在这里,可以将看作是特征向量的函数,即。

但是对于图谱卷积来说,其计算代价非常大。未解决这个问题,这篇文章采用了Hammond et al.(2011)的方法,即使用切比雪夫多项式的阶截断来近似:

关于此近似的具体讨论可以参考Hammond et al.(2011)的文章,这里不再赘述。

回到信号和滤波器卷积的定义,现在有了:

通过这一近似,可以发现,谱图卷积只是依赖于距离中心节点步之内的节点(即阶邻居)

l Layer-wise线性模型

该文将逐层卷积运算限制为,以解决极宽节点度分布图(如社会网络、引文网络、知识图谱和许多其他真实世界的图数据集)局部邻域结构的过拟合问题。

在GCN的线性模型中,文章定义,此时可以得到图谱卷积的一阶线性近似:

其中有两个自由参数和。用来约束参数个数之后,可以得到:

但是,当不停地重复该操作时,可能会引起梯度爆炸或梯度消失。为解决此问题,该文引入renormalization trick:,其中,。最后,当图中每个节点表示为一个大小为C的向量时,可作如下定义:

其中为参数矩阵,为相应的卷积结果。

经过以上推导,可以得出图卷积神经网络(单层)的最终形式:

参考文献:David K. Hammond, Pierre Vandergheynst, and Remi Gribonval. Wavelets on graphs via spectral graph theory. Applied and Computational Harmonic Analysis, 30(2):129–150, 2011.

  1. Convolutional Neural Networks on Graphs with Fast Localized Spectral Filtering(ChebNet)

基本的频域卷积网络要计算拉普拉斯矩阵所有的特征值和特征向量,计算成本非常高。这篇文章对图卷积神经网络定义并实现了快速局部化谱过滤器,从而提升了计算效率。

卷积神经网络在图上的应用并不是直接的卷积与池化操作,无论是在理论与实践方面,定义谱过滤器对评估与学习特征起到至关重要的作用。本文中提出了应用切比雪夫多项式(Chebyshev polynomials)来改进过滤器,已达到加速特征矩阵求解的目的。将参数化为一个多项式函数,该多项式函数可以从中递归计算,因为乘上一个稀疏的会将复杂度降级到。这样的多项式就是切比雪夫展开。

假设切比雪夫多项式的第k项是, 频域卷积核的计算方式如下:

其中,的作用是让特征向量矩阵归一化到之间。可以由切比雪夫多项式的定义得来。

这种方法有几个重要的优点。首先,它不需要拉普拉斯特征向量的显示计算。第二,由于拉普拉斯算子是一个局部算子,只影响顶点的1跳邻域,它的(K-1)次幂影响K跳邻域,因此得到的滤波器是局部的。

  1. Geometric deep learning on graphs and manifolds using mixture model CNNs(MoNet)

这篇文章提出了混合模型网络(MoNet),一个将CNNs泛化到非欧几里德域(如图和流形)上的通用框架。其方法遵循了空间域方法的一般原理,并可以学习局部的、平稳的、组合的特定任务特征。这篇文章也表明了先前文献中提出的各种非欧几里德CNN方法(如GCNN、ACNN、GCN、DCNN都可以被视为本文提出框架的特例。

论文中用x表示一个图顶点,用y表示x的邻居节点,每个y关联一个伪坐标的D维向量。在这些坐标中,定义一个权重函数,该权重函数由一些可学习的参数参数化。因此,patch operator可以写成以下形式:

然后,将该形式泛化到非欧几里德域上的卷积空间:

该架构的关键是伪坐标u和权重函数的选择。表1显示,其他深度学习方法(包括欧氏域上的经典CNN,图上的DCN和DCNN,流形上的GCNN和ACNN)都可以通过适当定义u和获得。

该文不使用固定的手工权重函数,而是考虑参数可学习的参数核。权重函数如下:

其中和是可学习的和协方差矩阵和高斯核平均向量。进一步将协方差限制为对角线形式,可得到每个核的参数,以及patch operator的参数。

  1. Inductive Representation Learning on Large Graphs(GraphSAGE)

这篇论文提出了一个通用的框架,称为GraphSAGE(Graph SAmple and aggreGatE),用于归纳节点embedding。与基于矩阵分解的embedding方法不同,该文章利用节点特征(如文本属性、节点基本信息、节点的度)来学习一个能泛化到未知节点的embedding函数,即从transduction到induction。通过在学习算法中加入节点特征,可以同时学习每个节点邻域的拓扑结构以及节点特征在邻域中的分布。

本文训练了一组从节点的局部邻域学习聚合特征信息的聚合器函数(图1),每个聚合器函数从给定节点以外的不同跳数或搜索深度来聚合信息。

l Embedding生成(前向传播)算法

4-5行介绍了卷积层操作:聚合与节点相连的邻居(采样)层的embedding,得到第层邻居聚合特征,与节点的第层embedding拼接,并通过全连接层转换,得到节点在第层的embedding。

此外还给出了mini batch的训练方式:

l GraphSAGE参数学习

¡ 无监督学习:为了在完全无监督的情况下学习有用的预测表示,该文将基于图的损失函数应用于输出表示形式,并通过随机梯度下降调整权重矩阵和聚合器函数的参数。基于图形的损失函数促进附近节点具有相似的表示,同时使得不同节点的表示距离很大:

¡ 监督学习: 无监督损失函数的设定来学习节点embedding 可以供下游多个任务使用,若仅使用在特定某个任务上,则可以替代上述损失函数符合特定任务目标,如交叉熵

l 聚合器构造

由于graph是非欧结构的数据,一个节点的邻居数量不固定,也没有特定的顺序,所以聚合器需要满足对称性(即聚合结果与顺序无关)。该文提出了三种聚合器:平均聚合器、LSTM聚合器、pooling聚合器。

¡ 平均聚合器

所有的邻居节点的embedding加上中心节点的embedding取平均得到中心节点新的embedding,将以下公式替换算法1伪代码中第4、5行:

¡ LSTM聚合器

与平均聚合器相比,LSTMs具有更大的表达能力。然而,LSTMs不是固有对称的(即,它们不是置换不变的),因为它们以顺序方式处理它们的输入。该文通过简单地将LSTMs应用于节点邻居的随机排列,使LSTMs适应于在无序集上操作。

¡ Pooling聚合器

我们研究的pooling聚合器是对称的和可训练的。在这种pooling方法中,每个邻居的向量通过完全连接的神经网络独立地输送;在这种transformation之后,采用max-pooling操作来聚合邻域信息:

原则上,任何对称向量函数都可以用来代替以上的max算子(例如,元素平均值)。

准备工作

获取并分析数据集

from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures

dataset = Planetoid(root='data/Planetoid', name='Cora', transform=NormalizeFeatures())

print()
print(f'Dataset: {dataset}:')
print('======================')
print(f'Number of graphs: {len(dataset)}')
print(f'Number of features: {dataset.num_features}')
print(f'Number of classes: {dataset.num_classes}')

data = dataset[0]  # Get the first graph object.

print()
print(data)
print('======================')

# Gather some statistics about the graph.
print(f'Number of nodes: {data.num_nodes}')
print(f'Number of edges: {data.num_edges}')
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}')
print(f'Number of training nodes: {data.train_mask.sum()}')
print(f'Training node label rate: {int(data.train_mask.sum()) / data.num_nodes:.2f}')
print(f'Contains isolated nodes: {data.contains_isolated_nodes()}')
print(f'Contains self-loops: {data.contains_self_loops()}')
print(f'Is undirected: {data.is_undirected()}')

我们可以看到,Cora图拥有2,708个节点和10,556条边,平均节点度为3.9。我们仅使用140个有真实标签的节点(每类20个)用于训练。有标签的节点的比例只占到5%。进一步我们可以看到,这个图是无向图,不存在孤立的节点(即每个文档至少有一个引文)。数据转换在将数据输入到神经网络之前修改数据,这一功能可用于实现数据规范化或数据增强。在此例子中,我们使用NormalizeFeatures,进行节点特征归一化,使各节点特征总和为1。其他数据转换方法请参阅torch-geometric-transforms。

可视化节点表征分布的方法

import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

def visualize(h, color):
    z = TSNE(n_components=2).fit_transform(out.detach().cpu().numpy())
    plt.figure(figsize=(10,10))
    plt.xticks([])
    plt.yticks([])

    plt.scatter(z[:, 0], z[:, 1], s=70, c=color, cmap="Set2")
    plt.show()

为了实现节点表征分布的可视化,我们先利用TSNE将高维节点表征嵌入到二维平面空间,然后在二维平面空间画出节点。

MLP在图节点分类中的应用

理论上,我们应该能够仅根据文件的内容,即它的词包特征表示来推断文件的类别,而无需考虑文件之间的任何关系信息。让我们通过构建一个简单的MLP来验证这一点,该网络只对输入节点的特征进行操作,它在所有节点之间共享权重。

MLP图节点分类器:

import torch
from torch.nn import Linear
import torch.nn.functional as F

class MLP(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(MLP, self).__init__()
        torch.manual_seed(12345)
        self.lin1 = Linear(dataset.num_features, hidden_channels)
        self.lin2 = Linear(hidden_channels, dataset.num_classes)

    def forward(self, x):
        x = self.lin1(x)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.lin2(x)
        return x

model = MLP(hidden_channels=16)
print(model)

我们的MLP由两个线程层、一个ReLU非线性层和一个dropout操作。第一线程层将1433维的特征向量嵌入(embedding)到低维空间中(hidden_channels=16),第二个线性层将节点表征嵌入到类别空间中(num_classes=7)。

我们利用交叉熵损失Adam优化器来训练这个简单的MLP网络。

model = MLP(hidden_channels=16)
criterion = torch.nn.CrossEntropyLoss()  # Define loss criterion.
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)  # Define optimizer.

def train():
    model.train()
    optimizer.zero_grad()  # Clear gradients.
    out = model(data.x)  # Perform a single forward pass.
    loss = criterion(out[data.train_mask], data.y[data.train_mask])  # Compute the loss solely based on the training nodes.
    loss.backward()  # Derive gradients.
    optimizer.step()  # Update parameters based on gradients.
    return loss

for epoch in range(1, 201):
    loss = train()
    print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')

训练完模型后,我们可以进行测试,以检验这个简单的MLP模型在未知标签的节点上表现如何。

def test():
    model.eval()
    out = model(data.x)
    pred = out.argmax(dim=1)  # Use the class with highest probability.
    test_correct = pred[data.test_mask] == data.y[data.test_mask]  # Check against ground-truth labels.
    test_acc = int(test_correct.sum()) / int(data.test_mask.sum())  # Derive ratio of correct predictions.
    return test_acc

test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')

正如我们所看到的,我们的MLP表现相当糟糕,只有大约59%的测试准确性。
为什么MLP没有表现得更好呢?其中一个重要原因是,用于训练此神经网络的有标签节点数量过少,此神经网络被过拟合,它对未见过的节点泛化性很差。

GCN及其在图节点分类任务中的应用

GCN的定义

GCN 神经网络层来源于论文“Semi-supervised Classification with Graph Convolutional Network”,其数学定义为,

X′=D^−1/2A^D^−1/2XΘ,

其中A^=A+I表示插入自环的邻接矩阵,D^ii=∑j=0A^ij表示其对角线度矩阵。邻接矩阵可以包括不为1的值,当邻接矩阵不为{0,1}值时,表示邻接矩阵存储的是边的权重。D^−1/2A^D^−1/2对称归一化矩阵。

它的节点式表述为:

xi′=Θ∑j∈N(v)∪{i}ej,id^jd^ixj

其中,d^i=1+∑j∈N(i)ej,i,ej,i表示从源节点j到目标节点i的边的对称归一化系数(默认值为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,A^=A+2I,其目的在于增强中心节点自身信息;
  • cached :是否存储D^−1/2A^D^−1/2的计算结果以便后续使用,这个参数只应在归纳学习(transductive learning)的景中设置为true
  • add_self_loops :是否在邻接矩阵中增加自环边;
  • normalize :是否添加自环边并在运行中计算对称归一化系数;
  • bias :是否包含偏置项。

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

基于GCN图神经网络的图节点分类

通过将torch.nn.Linear layers 替换为PyG的GNN Conv Layers,我们可以轻松地将MLP模型转化为GNN模型。在下方的例子中,我们将MLP例子中的linear层替换为GCNConv层。

from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(GCN, self).__init__()
        torch.manual_seed(12345)
        self.conv1 = GCNConv(dataset.num_features, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, dataset.num_classes)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index)
        return x

model = GCN(hidden_channels=16)
print(model)

现在先让我们可视化我们的未训练的GCN网络的节点表征。

model = GCN(hidden_channels=16)
model.eval()

out = model(data.x, data.edge_index)
visualize(out, color=data.y)

经过visualize函数的处理,7维特征的节点被嵌入到2维的平面上。我们可以惊喜地发现存在同类节点聚集一起的情况。

"images/image-20210526214252415.png" 未创建成功!请点击以创建。

现在让我们训练GCN节点分类器:

model = GCN(hidden_channels=16)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
criterion = torch.nn.CrossEntropyLoss()

def train():
      model.train()
      optimizer.zero_grad()  # Clear gradients.
      out = model(data.x, data.edge_index)  # Perform a single forward pass.
      loss = criterion(out[data.train_mask], data.y[data.train_mask])  # Compute the loss solely based on the training nodes.
      loss.backward()  # Derive gradients.
      optimizer.step()  # Update parameters based on gradients.
      return loss

for epoch in range(1, 201):
    loss = train()
    print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')

在训练过程结束后,我们检测GCN节点分类器在测试集上的准确性:

def test():
      model.eval()
      out = model(data.x, data.edge_index)
      pred = out.argmax(dim=1)  # Use the class with highest probability.
      test_correct = pred[data.test_mask] == data.y[data.test_mask]  # Check against ground-truth labels.
      test_acc = int(test_correct.sum()) / int(data.test_mask.sum())  # Derive ratio of correct predictions.
      return test_acc

test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')

通过简单地将线性层替换成GCN层,我们可以达到81.4%的测试准确率!与前面的仅获得59%的测试准确率的MLP分类器相比,现在的分类器准确性要高得多。这表明节点的邻接信息在取得更好的准确率方面起着关键作用。

最后我们还可以通过可视化我们训练过的模型输出的节点表征来再次验证这一点,现在同类节点的聚集在一起的情况更加明显了。

model.eval()

out = model(data.x, data.edge_index)
visualize(out, color=data.y)

"images/image-20210526214436051.png" 未创建成功!请点击以创建。

GAT及其在图节点分类任务中的应用

GAT的定义

图注意网络(GAT)来源于论文 Graph Attention Networks。其数学定义为,

xi′=αi,iΘxi+∑j∈N(i)αi,jΘxj,

其中注意力系数αi,j的计算方法为,

αi,j=exp⁡(LeakyReLU(a⊤[Θxi∥Θxj]))∑k∈N(i)∪{i}exp⁡(LeakyReLU(a⊤[Θxi∥Θxk])).

PyG中GATConv 模块说明

GATConv构造函数接口:

GATConv(in_channels: Union[int, Tuple[int, int]], out_channels: int, heads: int = 1, concat: bool = True, negative_slope: float = 0.2, dropout: float = 0.0, add_self_loops: bool = True, bias: bool = True, **kwargs)

其中:

  • in_channels :输入数据维度;
  • out_channels :输出数据维度;
  • heads :在GATConv使用多少个注意力模型(Number of multi-head-attentions);
  • concat :如为true,不同注意力模型得到的节点表征被拼接到一起(表征维度翻倍),否则对不同注意力模型得到的节点表征求均值;

详细内容请大家参阅GATConv官方文档

基于GAT图神经网络的图节点分类

这一次,我们将MLP例子中的linear层替换为GATConv层,来实现基于GAT的图节点分类神经网络。

import torch
from torch.nn import Linear
import torch.nn.functional as F

from torch_geometric.nn import GATConv

class GAT(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(GAT, self).__init__()
        torch.manual_seed(12345)
        self.conv1 = GATConv(dataset.num_features, hidden_channels)
        self.conv2 = GATConv(hidden_channels, dataset.num_classes)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index)
        return x

基于GAT图神经网络的训练和测试,与基于GCN图神经网络的训练和测试相同,此处不再赘述。

结语

在节点表征的学习中,MLP节点分类器只考虑了节点自身属性,忽略了节点之间的连接关系,它的结果是最差的;而GCN与GAT节点分类器,同时考虑了节点自身属性与周围邻居节点的属性,它们的结果优于MLP节点分类器。从中可以看出邻居节点的信息对于节点分类任务的重要性

基于图神经网络的节点表征的学习遵循消息传递范式

  • 在邻居节点信息变换阶段,GCN与GAT都对邻居节点做归一化和线性变换(两个操作不分前后);
  • 在邻居节点信息聚合阶段都将变换后的邻居节点信息做求和聚合;
  • 在中心节点信息变换阶段只是简单返回邻居节点信息聚合阶段的聚合结果。

GCN与GAT的区别在于邻居节点信息聚合过程中的归一化方法不同

  • 前者根据中心节点与邻居节点的度计算归一化系数,后者根据中心节点与邻居节点的相似度计算归一化系数。
  • 前者的归一化方式依赖于图的拓扑结构,不同节点其自身的度不同、其邻居的度也不同,在一些应用中可能会影响泛化能力。
  • 后者的归一化方式依赖于中心节点与邻居节点的相似度,相似度是训练得到的,因此不受图的拓扑结构的影响,在不同的任务中都会有较好的泛化表现。

参考文献

  • Datawhale GNN教程:https://github.com/kwyyangfan/team-learning-nlp/tree/master/GNN
  • PyG中内置的数据转换方法:torch-geometric-transforms
  • 一个可视化高纬数据的工具:t-distributed Stochastic Neighbor Embedding
  • 提出GCN的论文:Semi-supervised Classification with Graph Convolutional Network
  • GCNConv官方文档:torch_geometric.nn.conv.GCNConv
  • 提出GAT的论文: Graph Attention Networks

你可能感兴趣的:(GNN理论与实践,pytorch,神经网络)