如果您在这里并且没有跳过本节,那么,我们假设您是一个完整的初学者,您可能需要非常仔细地阅读所有内容。我们可以将图形定义为以有组织的方式表示数据的图片。让我们深入研究应用图论。图(有向或无向)由一组由 V 表示的顶点(或节点)和一组由 E 表示的边组成。让我们看一下图表。
在上图中,我们有:
V = { A , B , C , D , E , F , G } V = \{A, B, C, D, E, F, G\} V={A,B,C,D,E,F,G}
E = { ( A , B ) , ( B , C ) , ( C , E ) , ( B , D ) , ( E , F ) , ( D , E ) , ( B , E ) , ( G , E ) } E = \{(A,B), (B,C), (C,E), (B,D), (E,F), (D,E), (B,E), (G,E)\} E={(A,B),(B,C),(C,E),(B,D),(E,F),(D,E),(B,E),(G,E)}
在所有这些边之上,它们已经指定了相应的权重。这些权重可以表示不同的数量。例如,如果我们将这些节点视为不同的城市,则边缘可以是这些城市之间的距离。
因此,让我们进入真正的交易。环顾四周,我们可以观察到,大多数现实世界的数据集都以图形或网络的形式出现:社交网络、蛋白质交互网络、万维网等。这使得在图形上学习成为一个非常有趣的问题,可以解决大量特定于领域的任务,从而为我们带来有见地的信息。
但是,为什么这些图学习问题不能通过像CNN这样的传统机器学习/深度学习算法来解决呢?为什么需要建立一个全新的网络类别?
首先需要了解像卷积神经网络(CNN)这样的一类模型是如何工作的。CNN真的很强大,它们有能力学习非常高维的数据。假设您有一张 512*512 像素的图片。这里的维度约为 100 万。对于 10 个样本,空间变为 10 ^{1,000,000},CNN 已被证明在如此艰巨的任务设置下工作得非常好!
但是,有一个问题!这些数据样本,如图像,视频,音频等,其中CNN模型主要使用,都具有特定的组成性,这是我们在使用CNN之前做出的强烈假设之一。
因此,CNN基本上提取组合特征并将其提供给分类器。
GCN 的一个可能的应用是Facebook的朋友预测算法。考虑三个人A,B和C。假设A是B的朋友,B是C的朋友。您可能还以每个人的特征形式提供了一些代表性信息,例如,A可能喜欢Liam Neeson 主演的电影,并且一般来说C是类型惊悚片的粉丝,现在您必须预测A是否是C的朋友。
Facebook链接预测,用于使用社交网络推荐朋友
顾名思义,图卷积网络(GCN)借鉴了卷积神经网络为非欧几里得数据域重新定义它们的想法。通常用于图像识别的常规卷积神经网络捕获图像的每个像素的周围信息。与图像等欧几里得数据类似,这里的卷积框架旨在捕获非欧几里得空间(如图节点)的邻域信息。
GCN基本上是一个在图形上运行的神经网络。它将采用图形作为输入,并提供一些(我们将看到确切的)有意义的输出。
GCN 有两种不同的样式:
注意:频谱方法具有所有样品的图形结构相同的局限性,即同质结构。但这是一个硬约束,因为大多数现实世界的图形数据对于不同的样本(即异构结构)具有不同的结构和大小。空间方法与图结构无关。
首先,让我们针对朋友预测问题解决这个问题,然后我们将推广该方法。
问题描述: 你会得到N个人,还有一个图表,如果他们是朋友,两个人之间有一个边。你需要预测两个人将来是否会成为朋友。
这里的人 ( 1 , 2 ) (1,2) (1,2) 是朋友, 相识的 ( 2 , 3 ) , ( 3 , 4 ) , ( 4 , 1 ) , ( 5 , 6 ) , ( 6 , 8 ) , ( 8 , 7 ) , ( 7 , 6 ) (2,3), (3,4), (4,1), (5,6), (6,8), (8,7), (7,6) (2,3),(3,4),(4,1),(5,6),(6,8),(8,7),(7,6) 两两之间也是朋友.
现在,我们有兴趣找出一对给定的人将来是否有可能成为朋友。假设我们感兴趣的对是 ( 1 , 3 ) (1,3) (1,3), 现在由于他们有2个共同的朋友,我们可以轻声暗示他们有机会成为朋友,而节点 ( 1 , 5 ) (1,5) (1,5) 没有共同的朋友,所以他们不太可能成为朋友。
让我们再举一个例子:这里的 ( 1 , 11 ) (1,11) (1,11)比 ( 3 , 11 ) (3, 11) (3,11)更有可能成为朋友。
现在,人们可以提出的问题是 "如何实施和实现这一结果?GCN 以类似于CNN的方式实现它。在 CNN 中,我们对原始图像应用滤镜,以获得下一层中的表示形式。同样,在GCN中,我们应用一个过滤器来创建下一个图层表示。
从数学上讲,我们可以定义如下:
H i = f ( H i − 1 , A ) H^{i} = f(H^{i-1}, A) Hi=f(Hi−1,A)
一个非常简单的 f f f的例子可能是
f ( H i , A ) = σ ( A H i W i ) f(H^{i}, A) = σ(AH^{i}W^{i}) f(Hi,A)=σ(AHiWi)
这里:
在每个图层上,这些要素将使用传播规则 f f f进行聚合以形成下一层的要素。通过这种方式,要素在每个连续的层上变得越来越抽象。
是的,就是这样,我们已经有一些功能可以在图形之间传播信息,这些信息可以以半监督的方式进行训练。使用GCN层,每个节点(每行)的制图表达现在为其相邻要素的总和!换句话说,该图层将每个节点表示为其邻域的聚合。
但是,等等,有这么简单吗?
我请您在这里停顿片刻,认真思考一下我们刚刚定义的函数。
这是对的吗?
STOP
…
…
…
这有点像!但这并不是我们想要的。如果您无法解决问题,请不要担心。让我们看看这个函数可能导致的 ‘问题’(是的,不止一个问题)到底是什么:
1. 新节点特征 H i H^{i} Hi不是其先前表示的函数 : 您可能已经注意到,节点的聚合表示只是其相邻函数的函数,不包括其自己的特征。如果不加以处理,这可能会导致节点标识丢失,从而使特征表示变得毫无用处。我们可以通过添加自循环轻松解决此问题,即在同一节点上开始和结束的边缘,这样节点将成为自己的邻居。从数学上讲,自循环只是可以通过添加节点标识来表示。
2. 节点的度数导致值在整个图形中不对称地缩放 : 简单来说,具有大量相邻节点(较高度)的节点将从相邻节点以邻域聚合的形式获得更多的输入,因此将具有较大的值,反之亦然,对于具有较小值的度数的节点。这可能会导致在训练网络期间出现问题。为了解决这个问题,我们将使用规范化,即以一种使值处于相同尺度的方式减少所有值。规范化 A A A 使得所有行的总和为 1,即 D − 1 A D^{−1}A D−1A,其中 D D D 是对角节点度矩阵,从而摆脱了这个问题。乘以 D − 1 A D^{−1}A D−1A 现在对应于取相邻节点特征的平均值。根据作者的说法,在观察了经验结果之后,他们建议"在实践中,当我们使用对称归一化时,动力学变得更加有趣,即 D ^ − 1 2 A ^ D ^ − 1 2 \hat{D}^{-\frac{1}{2}}\hat{A}\hat{D}^{-\frac{1}{2}} D^−21A^D^−21(因为这不再仅仅是相邻节点的平均值)。
在解决了上述两个问题之后,新的传播函数是:f ( H ( l ) , A ) = σ ( D ^ − 1 2 A ^ D ^ − 1 2 H ( l ) W ( l ) ) f(H^{(l)}, A) = \sigma\left( \hat{D}^{-\frac{1}{2}}\hat{A}\hat{D}^{-\frac{1}{2}}H^{(l)}W^{(l)}\right) f(H(l),A)=σ(D^−21A^D^−21H(l)W(l))
这里
在本教程中,我们将在"Zachary空手道俱乐部网络"上对GCN进行培训。我们将使用 Thomas Kipf & Max Welling 在论文中提出的 ‘半监督图学习模型’ 。
在1970年至1972年期间,韦恩·W·扎卡里(Wayne W. Zachary)观察了属于当地空手道俱乐部的人。他将这些人表示为图表中的节点。并在一对人之间增加边,如果他们彼此互动。结果是下图所示。
在研究期间发生了一个有趣的事件。管理员"John A"和教练"Mr. Hi"(化名)之间发生了冲突,导致俱乐部一分为二。一半的成员围绕着Hi先生成立了一个新的俱乐部;来自另一部分的成员找到了新的教练或放弃了空手道。
使用他之前找到的图表,他试图预测哪个成员将进入哪一半。令人惊讶的是,他能够预测所有成员的决定,除了与Hi先生而不是John A. Zachary一起去的节点9之外,Zachary使用了最大流量-最小Ford-Fulkerson算法。我们今天将使用不同的算法,因此不需要了解Ford-Fulkerson算法。
在这篇文章中,我们将使用PyTorch和Matplotlib。
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import imageio
首先,我们将创建 GCNConv 类,该类将用作 Layer 创建类。此类的每个实例都将获取邻接矩阵作为输入,并将输出 Net 类将使用的 ‘RELU(A_hat * X * W)’。
class GCNConv(nn.Module):
def __init__(self, A, in_channels, out_channels):
super(GCNConv, self).__init__()
self.A_hat = A+torch.eye(A.size(0))
self.D = torch.diag(torch.sum(A,1))
self.D = self.D.inverse().sqrt()
self.A_hat = torch.mm(torch.mm(self.D, self.A_hat), self.D)
self.W = nn.Parameter(torch.rand(in_channels,out_channels))
def forward(self, X):
out = torch.relu(torch.mm(torch.mm(self.A_hat, X), self.W))
return out
Net 类将组合多个 Conv 层。
class Net(torch.nn.Module):
def __init__(self,A, nfeat, nhid, nout):
super(Net, self).__init__()
self.conv1 = GCNConv(A,nfeat, nhid)
self.conv2 = GCNConv(A,nhid, nout)
def forward(self,X):
H = self.conv1(X)
H2 = self.conv2(H)
return H2
‘A’ 是邻接矩阵
# 'A' is the adjacency matrix, it contains 1 at a position (i,j)
# if there is a edge between the node i and node j.
A = torch.Tensor([[0,1,1,1,1,1,1,1,1,0,1,1,1,1,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0],
[1,0,1,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0,1,0,0,0],
[1,1,0,1,0,0,0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0],
[1,1,1,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1],
[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,1,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1],
[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1,1],
[0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,1,1],
[0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,1,0,0,1,0,1,0,1,1,0,0,0,0,0,1,1,1,0,1],
[0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,1,0,0,1,1,1,0,1,1,0,0,1,1,1,1,1,1,1,0]
])
在这个例子中,我们有 admin(节点 1) 和 instructor(节点 34) 的标签,所以只有这两个包含类 label(0 和 1),所有其他的都设置为 -1,这意味着这些节点的预测值将在计算损失函数时被忽略。
target=torch.tensor([0,-1,-1,-1, -1, -1, -1, -1,-1,-1,-1,-1, -1, -1, -1, -1,-1,-1,-1,-1, -1, -1, -1, -1,-1,-1,-1,-1, -1, -1, -1, -1,-1,1])
X 是特征矩阵。由于我们没有每个节点的任何功能,因此我们将只使用与节点索引相对应的one-hot编码。
X=torch.eye(A.size(0))
在这里,我们正在创建一个网络,其中 10 个 features 位于隐藏层,2 个 features 位于输出层中。
T=Net(A,X.size(0), 10, 2)
criterion = torch.nn.CrossEntropyLoss(ignore_index=-1)
optimizer = optim.SGD(T.parameters(), lr=0.01, momentum=0.9)
loss=criterion(T(X),target)
# Plot animation using celluloid
fig = plt.figure()
camera = Camera(fig)
for i in range(200):
optimizer.zero_grad()
loss=criterion(T(X), target)
loss.backward()
optimizer.step()
l=(T(X));
plt.scatter(l.detach().numpy()[:,0],l.detach().numpy()[:,1],
c=[0, 0, 0, 0 ,0 ,0 ,0, 0, 1, 1, 0 ,0, 0, 0, 1 ,1 ,0 ,0
,1, 0, 1, 0 ,1 ,1, 1, 1, 1 ,1 ,1, 1, 1, 1, 1, 1 ])
for i in range(l.shape[0]):
text_plot = plt.text(l[i,0], l[i,1], str(i+1))
camera.snap()
if i%20==0:
print("Cross Entropy Loss: =", loss.item())
animation = camera.animate(blit=False, interval=150)
animation.save('./train_karate_animation.mp4', writer='ffmpeg', fps=60)
HTML(animation.to_html5_video())
正如你在上面看到的,它把数据分为两类,并且接近实际的预测。
我们还使用这个伟大的库PyTorch Geometric (PyG) 和一个超级活跃的 Matthias Fey实现了GCN。PyG是专门为PyTorch爱好者构建的,他们需要一种简单,快速和简单的方法来在各种图形表示学习论文上实现和测试他们的工作。
您可以在此处找到GCN_PyG Notebook ,其中包含在引文网络(Cora 数据集)上训练的 graph_nets 的实现。
另外,还记得我要求你记住一件事吗?要回答这个问题,请阅读这个惊人的博客,该博客试图了解GCN是否真的像他们声称的那样强大. How powerful are Graph Convolutions?