详解GCN的性质

前言

本文参考 – 深入浅出图神经网络 GNN 原理解析一书

GCN 的性质

GCN 与 CNN 的联系

1. 图像是一种特殊的图数据

在图像中如果将像素视作节点,将像素之间空间坐标的连线作为彼此之间的边,如此图像数据就变成了一种结构非常规则的图数据,CNN 中的卷积计算则是用来出来这类固定的 2D 栅格结构的图数据。相较之下,一般意义下的图数据,往往单个节点附近的邻域结构是千差万别的,数据之间的关系也较为复杂多样,GCN中的卷积计算则是用来处理更普遍的非结构化的图数据。

2.从网络连接方式来看,两者都是局部连接

在 GCN 中的计算方式是每次图卷积操作作用范围是当前节点的一节子图,对于 CNN 来说针对于不同大小的卷积核,计算范围是当前节点的固定大小的 n × n n \times n n×n 的栅格内的像素。这种节点下一层的特征计算只依赖于自身领域的方式,在网络连接上表现为一种局部连接的结构。

3.两者的卷积核的权重是处处共享的

与 CNN 一样,GCN 的卷积核也作用于全图的所有节点,在每个节点处的计算中权重参数是共享的,这样的处理方式大大减少了单层网络的参数量,可以有效避免过拟合的现象出现。

4.从模型层面看,感受域随着卷积层的增加而变大
对于 CNN 来说,随着层数增多,所包含的信息来源于更广泛的临近像素点,对于 GCN 来说,层数加深表明信息的来源于更高阶的子图结构。

GCN 对图结构数据进行端到端的学习

图数据中同时包含着两部分信息:属性信息与结构信息。属性信息描述了图中对象的固有性质;结构信息描述了对象之间的关联性质,这种关联所产生的结构不仅对图数据中节点的刻画具有很大的帮助作用,而且对全图的刻画也非常关键。一个优秀的针对图数据的学习系统,必须能够做到对属性信息和结构信息进行端到端的学习。

在此之前,介绍两种非常典型的图数据学习方式 – 基于手工特征基于随机游走

一般来说,图数据中的属性信息的处理是比较简单的,按照属性的类型进行相应的编码设计,然后将其拼接成一个表示节点属性的特征向量就可以了,但是结构信息蕴含在节点之间的关系中,是比较难处理的。上述的两种方法核心都在于如何处理图的结构信息上。

基于手工特征:该方法对于图数据的处理方式依赖于人工干预,具体来说,将图中节点的结构信息以一些图的统计特征进行代替,常见的有节点的度、节点的中心度、节点的 PageRank 值等,然后将这个代表节点结构信息的特征向量与代表节点属性信息的特征向量拼接在一起,送到下游任务中进行学习。这种方法的最大问题在于,表示结构信息的特征向量需要人为定义,因此很难确定这些统计特征是否对学习后面的任务有效。

基于随机游走
其基本思想是将图中节点所满足的关系与结构的性质映射到一个新的向量空间中,比如在图中距离更近的两个节点,在新的向量空间中的距离也更近。通过这样的优化目标即将图里面的数据转化成向量空间中的数据,这样处理起来更加方便。

该方法和基于手工特征的方法的思路一样,将代表节点的结构信息的特征向量和属性信息的向量进行拼接,然后进行下游的任务学习。所不同的是,其节点的结构信息是通过随机游走类方法进行学习的,并不依赖人为定义,因此相比之下会更加高效。

GCN 方法可以完成对结构信息和属性信息的端到端的学习。其核心在于计算公式上: L ~ s y m X W \mathbf{\widetilde{L}_{sym}XW} L symXW,这一计算过程可以分解成两步:

  • W X \mathbf{WX} WX 是对属性信息的仿射变换,学习了属性特征之间的交互模式。
  • L ~ s y m X W \mathbf{\widetilde{L}_{sym}XW} L symXW从空域来看是聚合邻居节点的过程,代表了对节点局部结构信息的编码。

在这里引入一个很重要的概念:图同构问题(graph isomorphism problem),即给定两个图,判断这两个图是否是完全等价的。具体描述为:假设有两个图 H = ( V H , E H ) H=(V_H,E_H) H=(VH,EH) 和图 G = ( V , E ) G=(V,E) G=(V,E) 子图 同构即从 H H H G G G 存在这样一个函数 f : V H → V f:V_H \rightarrow V f:VHV 并且 ( u , v ) ∈ E H (u,v)\in E_H (u,v)EH 使得 ( f ( u ) , f ( v ) ) ∈ E (f(u),f(v))\in E (f(u),f(v))E 同样成立, f f f 叫做子图同构的一个映射。

从图同构的定义来看,需要考虑的是算法对图结构信息的分辨能力。一个经典的算法是 Weisfeiler-Lehman (WL算法),这是一个迭代式的算法,算法流程如下:

对于图上的任意一个节点 v i v_i vi
1.获取邻居节点 v j ∈ N ( v i ) v_j \in N(v_i) vjN(vi) 的标签 h j h_j hj
2.更新 v i v_i vi 的标签 h i ← h a s h ( ∑ v j h j ) h_i \leftarrow hash(\sum_{v_j}h_j) hihash(vjhj),其中 h a s h ( ⋅ ) hash(\cdot) hash() 是一个单射函数(一对一映射)。

上述 WL 算法过程与节点层面的 GCN 计算过程基本一致:迭代式地聚合邻居节点特征,从而更新当前的节点特征。从这个角度看,GCN 近似于一种带参的、支持自动微分的 WL 算法。GCN 的衍生模型 GIN 就是从 WL 算法出发提出了一种新型的 GCN 框架,其在判断图同构问题上的性能几乎等价于 WL 算法。

总之,在 GCN 模型中,通过堆叠图卷积层,上述属性信息的编码学习与结构信息的编码学习被不断的交替进行,如是完成对图数据中更加复杂的模式学习

GCN 是一个低通滤波器

在图的半监督学习任务中,通常会在相应的损失函数里面增加一个正则项,该正则项需要保证相邻节点之间的类别信息趋于一致,一般情况下,我们选用拉普拉斯矩阵的二次型作为正则约束:
L = L 0 + L r e g , L r e g = ∑ e i j ∈ E A i j ∣ ∣ f ( x i ) − f ( x j ) ∣ ∣ 2 = f ( X ) T L f ( X ) (1) \mathcal{L=L_0+L_{reg},L_{reg}}=\sum_{e_{ij}\in E} A_{ij}||f(\mathbf{x}_i)-f(\mathbf{x}_j)||^2=f(\mathbf{X})^T\mathbf{L}f(\mathbf{X})\tag{1} L=L0+Lreg,Lreg=eijEAijf(xi)f(xj)2=f(X)TLf(X)(1)
其中 L , L 0 , L r e g \mathcal{L,L_0,L_{reg}} L,L0,Lreg 分别代表模型的总损失,监督损失和正则项。从学习的目标来看,这样的正则项使得相邻结点的分类标签尽量一致,这种物以类聚的先验知识可以指导我们更加高效的对未标记的数据进行学习。从图信号的角度来说,该正则化项也表示图信号的总变差,减小该项表示期望经过模型之后的图信号更加平滑,从频域上来看,相当于对图信号做了低通滤波的处理。

GCN 的核心计算过程为 L ~ X W \mathbf{\widetilde{L}XW} L XW,体现图滤波的地方在于左乘了一个对称归一化形式的拉普拉斯矩阵 L ~ \mathbf{\widetilde{L}} L .要确定是否为低通滤波,就必须去研究 L ~ \mathbf{\widetilde{L}} L 对应的频率响应函数 p ( λ ) p(\lambda) p(λ) 的性质

L ~ s y m = D ~ − 1 2 A ~ D ~ − 1 2 = D ~ − 1 2 ( D ~ − L ) D ~ − 1 2 = I − D ~ − 1 2 L D ~ − 1 2 = I − L ~ s (2) \mathbf{\widetilde{L}_{sym}=\widetilde{D}^{-\frac{1}{2}}\widetilde{A}\widetilde{D}^{-\frac{1}{2}}=\widetilde{D}^{-\frac{1}{2}}(\widetilde{D}-L)\widetilde{D}^{-\frac{1}{2}}=I-\widetilde{D}^{-\frac{1}{2}}L\widetilde{D}^{-\frac{1}{2}}=I-\widetilde{L}_s}\tag{2} L sym=D 21A D 21=D 21(D L)D 21=ID 21LD 21=IL s(2)

由于 L ~ s \mathbf{\widetilde{L}_s} L s 可以被正交对角化,可以设 L ~ s = V Λ ~ V T \mathbf{\widetilde{L}_s=V\widetilde{\Lambda}V^T} L s=VΛ VT, λ ~ i \widetilde{\lambda}_i λ i L ~ s \mathbf{\widetilde{L}_s} L s 的特征值,可以证明 λ ~ i ∈ [ 0 , 2 ) \widetilde{\lambda}_i \in [0,2) λ i[0,2)

基于此,上述公式变为:
L ~ s y m = I − V Λ ~ V T = V ( I − Λ ~ ) V T (3) \mathbf{\widetilde{L}_{sym}=I-V\widetilde{\Lambda}V^T=V(I-\widetilde{\Lambda})V^T}\tag{3} L sym=IVΛ VT=V(IΛ )VT(3)

显然其频率响应函数为 p ( λ ) = 1 − λ ~ i ∈ ( − 1 , 1 ] p(\lambda)=1-\widetilde{\lambda}_i \in (-1,1] p(λ)=1λ i(1,1],该函数是一个线性收缩的函数,因此能起到对图信号进行低通滤波的作用。如果将信号矩阵 X \mathbf{X} X 不断左乘 K K K L ~ s y m \mathbf{\widetilde{L}_{sym}} L sym,则对应频率响应函数为 ( 1 − λ ~ i ) K (1-\widetilde{\lambda}_i)^K (1λ i)K

通过实验数据可知,随着 K K K 的增大,频率响应函数在低频段上有着更强的缩放效果,因此是一种更强效应的低通滤波器。这种堆叠式的滤波操作,在一定程度上解释了多层 GCN 模型对于信号的平滑能力,因此为了更好的突出这种能力、减少模型的参数两,很多论文中将多层 GCN 退化成 σ ( L ~ s y m X W ) \sigma(\mathbf{\widetilde{L}_{sym}XW}) σ(L symXW)

在paper:Revisiting Graph Neural Networks: All We Have is Low-Pass Filters 中,作者论证了一个关于图数据的假设 – 输入数据的特征信号包括低频信号与高频信号,低频信号包含着对任务学习更有效的信息。

作者在 Cora、Citeseer、Pubmed 数据集上做了实验,这三个数据集都是论文引用网络,节点是论文,边是论文之间的引用关系。作者设计了一个实验,通过低通滤波截掉数据中的高频信息,然后使用剩下的低频信息进行分类学习,具体过程如下:

  • 对数据集的 L ~ s \mathbf{\widetilde{L}_{s}} L s 进行正交对角化,得到傅里叶基 V \mathbf{V} V
  • 对输入的信号矩阵增加高斯噪声 X ← X + N ( 0 , σ 2 ) \mathbf{X} \leftarrow \mathbf{X}+\mathcal{N(0,\sigma^2)} XX+N(0,σ2),其中 σ = ( 0 , 0.01 , 0.05 ) \sigma=(0,0.01,0.05) σ=(0,0.01,0.05)
  • 计算输入的信号矩阵在前 k k k 个最小频率上的傅里叶变换系数 X ~ k = ( V [ : , : k ] ) T D ~ 1 2 X \mathbf{\widetilde{X}_k=(V[:,:k])^T\widetilde{D}^{\frac{1}{2}}X} X k=(V[:,:k])TD 21X
  • 利用傅里叶逆变换重构信号 X k = D ~ − 1 2 V [ : , : k ] X ~ k \mathbf{X_k=\widetilde{D}^{-\frac{1}{2}}V[:,:k]\widetilde{X}_k} Xk=D 21V[:,:k]X k
  • 将重构后的信号送到一个两层的 MLP 网络进行分别类学习,并记录准确率。

作者通过实验证明:最高的分类准确率集中在仅使用最小的前20%的频段恢复信号的实验中,增加高频信息参与信号重构,模型分类效果会下降。同时,增加高斯噪声会造成分类准确率下降,这种效应随着重构所用的频率分量的比例的增加而增强,这说明了使用低通滤波对数据进行去噪的有效性。

总结一下:对数据有效品频率成分的分析可以指导我们发现数据内在规律,从而更好i的设计符合特定需求的滤波器,让 GCN 对于任务的高效学习做到有的放矢

GCN 相当于对输入信号做了一个低通滤波的操作,这会使信号变得更加平滑,这也是 GCN 模型的一个内在趋势,这会导致信号越来越趋同,丧失了节点特征的多样性。

GCN(Graph Convolutional Networks,图卷积网络)是一种用于图像分类、节点分类等图像处理任务的深度学习模型。下面我将介绍如何使用PyTorch实现GCN。 首先,我们需要安装PyTorch和其它必要的库。可以使用以下命令安装: ``` pip install torch torchvision pip install numpy scipy scikit-learn ``` 接下来,我们需要定义一个GCN模型。以下是一个简单的实现: ```python import torch import torch.nn as nn import torch.nn.functional as F class GCN(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super(GCN, self).__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, output_dim) def forward(self, x, adj): x = F.relu(self.fc1(torch.mm(adj, x))) x = self.fc2(torch.mm(adj, x)) return F.log_softmax(x, dim=1) ``` 在上面的代码中,我们定义了一个三层的GCN模型。`input_dim`是输入特征的维度,`hidden_dim`是隐藏层的维度,`output_dim`是输出层的维度。`nn.Linear`是一个线性层,`F.relu`是一个激活函数,`F.log_softmax`是一个softmax函数。 接下来,我们需要定义一个训练函数。以下是一个简单的实现: ```python def train(model, optimizer, criterion, features, adj, labels, idx_train): model.train() optimizer.zero_grad() output = model(features, adj) loss = criterion(output[idx_train], labels[idx_train]) loss.backward() optimizer.step() return model, optimizer, loss.item() ``` 在上面的代码中,我们定义了一个训练函数。`features`是输入特征,`adj`是邻接矩阵,`labels`是标签,`idx_train`是训练样本的索引。我们使用`model.train()`将模型切换到训练模式,然后使用`optimizer.zero_grad()`将梯度清零,使用`model(features, adj)`计算输出,使用`criterion(output[idx_train], labels[idx_train])`计算损失,使用`loss.backward()`计算梯度,使用`optimizer.step()`更新参数。 接下来,我们需要定义一个测试函数。以下是一个简单的实现: ```python def test(model, features, adj, labels, idx_test): model.eval() output = model(features, adj) _, preds = torch.max(output, dim=1) correct = torch.sum(preds[idx_test] == labels[idx_test]) acc = correct.item() / len(idx_test) return acc ``` 在上面的代码中,我们定义了一个测试函数。`features`是输入特征,`adj`是邻接矩阵,`labels`是标签,`idx_test`是测试样本的索引。我们使用`model.eval()`将模型切换到测试模式,然后使用`model(features, adj)`计算输出,使用`torch.max`计算最大值,使用`torch.sum`计算正确的预测数量,使用`acc = correct.item() / len(idx_test)`计算准确率。 以下是一个完整的GCN模型的实现: ```python import torch import torch.nn as nn import torch.nn.functional as F class GCN(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super(GCN, self).__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, output_dim) def forward(self, x, adj): x = F.relu(self.fc1(torch.mm(adj, x))) x = self.fc2(torch.mm(adj, x)) return F.log_softmax(x, dim=1) def train(model, optimizer, criterion, features, adj, labels, idx_train): model.train() optimizer.zero_grad() output = model(features, adj) loss = criterion(output[idx_train], labels[idx_train]) loss.backward() optimizer.step() return model, optimizer, loss.item() def test(model, features, adj, labels, idx_test): model.eval() output = model(features, adj) _, preds = torch.max(output, dim=1) correct = torch.sum(preds[idx_test] == labels[idx_test]) acc = correct.item() / len(idx_test) return acc ``` 接下来,我们需要加载数据。以下是一个简单的实现: ```python import numpy as np def load_data(): adj = np.array([[0, 1, 1, 0], [1, 0, 1, 0], [1, 1, 0, 1], [0, 0, 1, 0]]) features = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1], [1, 0, 0]]) labels = np.array([0, 1, 2, 0]) idx_train = np.array([0, 1, 2]) idx_test = np.array([3]) return adj, features, labels, idx_train, idx_test ``` 在上面的代码中,我们定义了一个简单的图。`adj`是邻接矩阵,`features`是输入特征,`labels`是标签,`idx_train`是训练样本的索引,`idx_test`是测试样本的索引。 接下来,我们需要训练模型。以下是一个简单的实现: ```python import torch.optim as optim adj, features, labels, idx_train, idx_test = load_data() model = GCN(input_dim=features.shape[1], hidden_dim=16, output_dim=labels.max()+1) optimizer = optim.Adam(model.parameters(), lr=0.01) criterion = nn.CrossEntropyLoss() for epoch in range(100): model, optimizer, loss = train(model, optimizer, criterion, features, adj, labels, idx_train) acc = test(model, features, adj, labels, idx_test) print('Epoch: {:03d}, Loss: {:.4f}, Acc: {:.4f}'.format(epoch, loss, acc)) ``` 在上面的代码中,我们定义了一个优化器和一个损失函数。然后,我们使用一个循环来训练模型,并使用`test`函数测试模型。我们使用`print`函数打印训练和测试的损失和准确率。 以上就是使用PyTorch实现GCN的一些基本步骤。当然,这只是一个简单的实现,实际中还有很多需要改进的地方。希望能对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值