赞
踩
首先让我们来看一下原文是怎么说的图卷积网络 |托马斯·基普夫 |谷歌 DeepMind (tkipf.github.io)
众所周知,深度学习一直都是被几大经典模型所垄断,如CNN、RNN等等,它们无论再CV还是NLP领域都取得了优异的效果,那这个GCN是怎么跑出来的?是因为我们发现了很多CNN、RNN无法解决或者效果不好的问题——图结构的数据。(不熟悉cnn和rnn的可以看我以前的文章卷积神经网络(CNN)-CSDN博客,循环神经网络(RNN)-CSDN博客)
例如:城市交通的每个路口上的传感器所记录的数据;化学分子结构;人际关系网;推荐系统中每个人构成的图等。并不是说以上的神经网络处理不了图这种类型的数据,只是在处理图这种数据上存在欠缺,图数据有一个很明显的特征,相邻或相近的节点存在一定的空间依赖关系,这种关系传统神经网络很难捕获,为此,图神经网络应运而出。
我们已经知道如何用图来表示信息了,接下来我们需要知道要使用这些图数据来做什么类型的预测任务。当前基于图实现的预测主要分为以下几类:
节点分类:给定一个带有标签的图,任务是对未标记节点进行分类。例如,在社交网络中,节点可以表示用户,而标签可以表示用户的兴趣或属性。节点分类任务可以用于预测用户的兴趣、社区检测等。
图分类:给定一个图,任务是将整个图分为不同的类别。例如,在化学分子的分类中,图可以表示分子结构,而任务是将分子归类为不同的化合物类型。
链接预测:给定一个图,任务是预测两个节点之间是否存在边或连接。例如,在社交网络中,链接预测可以用于推荐新的社交关系或预测两个用户之间是否可能建立联系。
图生成:给定一些已知节点和边的信息,任务是生成一个新的图结构。例如,在蛋白质结构预测中,可以使用已知的蛋白质结构信息生成新的蛋白质结构。
异常检测:给定一个图,任务是检测其中的异常节点或异常边。例如,在金融交易网络中,可以使用图结构来检测潜在的欺诈行为或异常的交易模式。
推荐系统:使用图结构来建模用户和项目之间的关系,从而进行个性化推荐。例如,在电影推荐系统中,图可以表示用户和电影之间的关联,通过分析用户的社交网络或相似用户之间的交互,进行电影推荐。
除了上述任务,图结构数据还广泛应用于社交网络分析、知识图谱、交通网络优化、蛋白质相互作用预测等领域。
乍一看是不是很吓人 ,这个是具有一阶滤波器的多层图卷积网络 (GCN),接下来让我们一起来慢慢分析
假设有一批图数据,其中有N个节点(node),每个节点都有自己的特征,假设特征一共有D个,我们设这些节点的特征组成一个N×D维的矩阵X,然后各个节点之间的关系也会形成一个N×N维的矩阵A,也称为邻接矩阵(adjacency matrix)。X和A便是我们模型的输入。
GCN算法的步骤简单可总结为:
1、初始化:为每个节点分配初始特征表示。
2、邻居聚合:对于每个节点,将其自身特征与邻居节点的特征进行加权平均或拼接,得到聚合后的特征。
3、特征转换:对聚合后的特征进行线性变换,以充分利用特征之间的关系。
4、非线性激活:应用非线性激活函数,如ReLU,将线性变换后的特征映射到非线性空间。
5、循环迭代:重复进行邻居聚合、特征转换和非线性激活的步骤,直到达到所需的网络层数或收敛条件。对于每个节点,我们在聚类时从它的所有邻居节点处获取其特征信息,当然也包括它自身的特征。
GNN的输入一般是所有节点的起始特征向量和表示节点间连接关系的邻接矩阵。聚合操作和节点特征更新是图神经网络的核心操作。以下图为例,我们首先考虑A节点,聚合操作会将当前节点A的邻居节点信息乘以系数聚合起来加到节点A身上作为A节点信息的补足信息,当作一次节点A的特征更新。对图中的每个节点进行聚合操作,更新所有图节点的特征。通过不断交换邻域信息来更新节点特征,直到达到稳定均衡,此时所有节点的特征向量都包含了其邻居节点的信息,然后我们就能利用这些信息来进行聚类、节点分类、链接预测等等。不同的GNN有不同的聚合函数,可根据任务的不同自由选择,这里举的只是一个很简单的例子,重点是要明白我们需要用到什么信息需要做什么操作,做这些操作的目的是什么。一次图节点聚合操作和权重学习,可以理解为一层神经网络,后面再重复进行聚合、加权,就是多层迭代了。一般GNN只要3~5层就可以学习到足够的信息,所以训练GNN对算力要求很低。注意, GNN 不会更新和改变输入图(Input Graph)的结构和连通性,所以我们可以用与输入图相同的邻接矩阵和相同数量的特征向量来描述 GNN 的输出图(Output Graph),输出的图只是更新了每个节点的特征。
那就来举了例子吧
我们需要理解A矩阵和X矩阵相乘的含义,因为它们的相乘代表了GCN的聚合操作。以下图为例,观察邻接矩阵的第一行,我们看到节点 A 与 E 有一个连接。相乘之后,结果矩阵的第一行是 E 的特征向量。同样,结果矩阵的第二行是 D 和 E 的特征向量之和。通过将A和X相乘,我们可以得到所有邻居向量的和。
但是如果按照这样简单聚合的话就会出现问题
- 我们没有包含节点本身的特征。结果矩阵也应该包含节点本身的特征。
- 我们需要取平均值或者邻居的特征向量的加权平均值,而不是 sum() 函数把这些信息全部加起来。原因是在使用 sum() 函数时,度数高的节点可能会对度数低的节点的更新有很大的影响,而实际上如果一个度数低的节点在特征更新的过程中加入了全部的度数高的节点信息,这是不合理的。此外,神经网络对输入数据的规模很敏感。因此,我们需要对这些向量进行归一化以消除潜在问题。
这两个问题我们这样来解释:我们想通过聚类,估计出一个人的工资水平,先假设:一个人所有朋友的工资平均值等于那个人的工资,利用社交网络(graph图)中的关联信息(edge边),我们可以得到节点的有效信息。 当前节点可以用相邻节点推断出来。我们先考虑用相邻节点的加和来代表当前节点:
如果图中的边没有权重,Xj是每个节点的特征值, Aij也就是节点间的关系只能为0或1。当一个节点不是i的邻居节点时,Aij就是0。
那么我们将Aij写成描述节点间关系的邻接矩阵A,就能得到:
但是没有权重显然不合适,因为假如我和马云见过一面,相互仅是认识,但是马化腾和马云是铁哥们,那么将我与马云间联系的权重和马化腾与马云间联系的权重都设为1就是不合理的,所以在处理问题时,Aij一定可以为0到1的任意值,这样才可以科学的计算我的工资水平。
并且只考虑朋友的工资水平也是不科学的,比如有的人会拍领导马屁,那么他的工资就会比没有拍马屁技能的同事高。
所以要想准确评估他的工资,还要加上他自身的一些特征Xi。在矩阵里就是加上自连接矩阵I:
截止目前,我们已经结合节点特征,将朋友们的工资科学地进行了求和,那么接下来就需要取平均值了
因为可能某些技术高薪大佬,朋友不多但是个个有钱,而某些人善于交际,狐朋狗友就很多,虽然狐朋狗友的工资低,但是也架不住多
所以需要求平均,求平均的公式:
分母是节点个数,分子就是节点特征,然后进行求和。
到目前为止,我们就通过求平均,得到了一个人的工资水平,当然简单的求平均肯定是不完善的;
假如我的朋友只有一个大佬,那么我和大佬的关系网络如果用平均算法的话就等同了,所以直接把B的特征赋给A肯定是不合适的
那么GCN的传播公式就能够解决上面两个问题
先让我们来解释一下这个让人头大的公式,这个公式中:
我们现在得到的就是加权平均值,我们这里做的是对度数低的节点增加权重,减少度数高的节点的影响。这种加权平均的想法是,我们假设低度节点对其邻居产生更大的影响,而高度节点产生的影响较小,因为它们将影响分散在太多的邻居身上。到这一步,归一化已经算完成了
GCN是一个多层的图卷积神经网络,每一个卷积层仅处理一阶邻域信息,通过叠加若干卷积层可以实现多阶邻域的信息传递。大致的基本结构可以描述如下:
输入层:GCN的输入是图数据,包括邻接矩阵(表示节点之间的连接关系)和节点特征矩阵(表示每个节点的特征向量)。
Graph Convolutional Layer(图卷积层):GCN包含多个图卷积层,每一层都对节点特征进行卷积操作。在图卷积层中,每个节点的特征向量与其邻居节点的特征向量进行聚合和组合。
聚合邻居特征:通过邻接矩阵将每个节点的邻居节点特征进行聚合,可以使用矩阵乘法实现,即将邻接矩阵与节点特征矩阵相乘。
特征变换和更新:对聚合得到的邻居特征进行线性变换,通常使用全连接层(如线性变换)来处理。然后,应用非线性激活函数(如ReLU)对特征进行激活,以增加模型的非线性能力。
聚合结果更新:将特征变换后的结果与节点自身的特征进行融合,可以使用加法或连接操作。这样,每个节点都能够利用自身特征和邻居节点特征的信息。
输出层:最后一个图卷积层的输出可以被用于节点分类、图分类或其他图相关任务。对于节点分类问题,通常在输出层使用 softmax 函数将每个节点的特征向量映射为类别概率分布。
GCN模型的训练过程通常使用反向传播算法和梯度下降优化方法来最小化损失函数,以使模型能够逐渐学习到节点的表示和分类准则。
在原论文中我们会发现作者做了一个实验,使用一个俱乐部会员的关系网络,使用随机初始化的GCN进行特征提取,得到各个node的embedding,然后可视化:
可以发现,在原数据中同类别的node,经过GCN的提取出的embedding,已经在空间上自动聚类了。而这种聚类结果,可以和DeepWalk、node2vec这种经过复杂训练得到的node embedding的效果媲美了。作者接着给每一类的node,提供仅仅一个标注样本,然后去训练,得到的可视化效果如下:
一次聚合更新操作代表GCN的一层。层数是节点特征可以行进的最远距离。
例如,使用 1 层 GCN,每个节点只能从其邻居那里获取信息。收集信息过程独立进行,所有节点同时进行。当在第一层之上堆叠另一层时,我们重复收集信息过程,但这一次,邻居已经有了关于他们自己邻居的信息(来自上一步)。因此,根据我们认为节点应该从网络中获取信息的程度,我们可以为设置相应的层数。但一般通过 6-7 层,我们几乎可以得到了整个图,这使得聚合变得不那么有意义。因为节点每更新一次,感受野就变大一些,如果网络太深,那么每个节点就会受无关节点的影响,效果反而下降。
GCN就是在平均法的基础上,加入了针对每个节点度的归一化。
GCN的每一层通过邻接矩阵A和特征矩阵H(l)相乘得到每个顶点邻居特征的汇总,然后再乘上一个参数矩阵W(l),加上激活函数σ做一次非线性变换得到聚合邻接顶点特征的矩阵H(l+1)。
之所以邻接矩阵A要加上一个单位矩阵I,是因为我们希望在进行信息传播的时候顶点自身的特征信息也得到保留。 而对邻居矩阵A波浪进行归一化操作D^(-1/2)A*D是为了信息传递的过程中保持特征矩阵H的原有分布,防止一些度数高的顶点和度数低的顶点在特征分布上产生较大的差异。
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- import torch.optim as optim
- import matplotlib.pyplot as plt
-
- # 定义一个图卷积层类
- class GraphConvolution(nn.Module):
- def __init__(self, input_dim, output_dim):
- """
- 初始化图卷积层
-
- 参数:
- input_dim (int): 输入特征的维度
- output_dim (int): 输出特征的维度
- """
- super(GraphConvolution, self).__init__()
- # 定义一个线性层,用于特征变换
- self.linear = nn.Linear(input_dim, output_dim)
-
- def forward(self, adjacency_matrix, input_features):
- """
- 前向传播
-
- 参数:
- adjacency_matrix (torch.Tensor): 邻接矩阵
- input_features (torch.Tensor): 输入特征矩阵
-
- 返回值:
- torch.Tensor: 输出特征矩阵
- """
- # 使用邻接矩阵和输入特征进行矩阵乘法得到支持矩阵
- support = torch.matmul(adjacency_matrix, input_features)
- # 将支持矩阵通过线性层进行特征变换
- output = self.linear(support)
- return output
-
- # 定义一个GCN模型类
- class GCN(nn.Module):
- def __init__(self, input_dim, hidden_dim, output_dim):
- """
- 初始化GCN模型
-
- 参数:
- input_dim (int): 输入特征的维度
- hidden_dim (int): 隐藏层特征的维度
- output_dim (int): 输出特征的维度
- """
- super(GCN, self).__init__()
- # 定义第一层图卷积层
- self.gc1 = GraphConvolution(input_dim, hidden_dim)
- # 定义第二层图卷积层
- self.gc2 = GraphConvolution(hidden_dim, output_dim)
-
- def forward(self, adjacency_matrix, input_features):
- """
- 前向传播
-
- 参数:
- adjacency_matrix (torch.Tensor): 邻接矩阵
- input_features (torch.Tensor): 输入特征矩阵
-
- 返回值:
- torch.Tensor: 输出特征矩阵
- """
- # 使用第一层图卷积层进行特征变换,并使用ReLU激活函数进行非线性变换
- hidden = F.relu(self.gc1(adjacency_matrix, input_features))
- # 使用第二层图卷积层进行特征变换
- output = self.gc2(adjacency_matrix, hidden)
- return output
-
- # 构建一个简单的图数据
- adjacency_matrix = torch.tensor([[0, 1, 1, 0],
- [1, 0, 0, 1],
- [1, 0, 0, 1],
- [0, 1, 1, 0]], dtype=torch.float32)
-
- # 定义输入特征矩阵,矩阵的每一行表示一个节点的特征
- input_features = torch.tensor([[1, 2], [3, 4], [5, 6], [7, 8]], dtype=torch.float32)
-
- # 定义标签,表示每个节点的类别
- labels = torch.tensor([0, 1, 1, 0], dtype=torch.long)
-
- # 创建GCN模型并定义损失函数和优化器
- model = GCN(input_dim=2, hidden_dim=16, output_dim=2)
- criterion = nn.CrossEntropyLoss() # 使用交叉熵损失函数
- optimizer = optim.Adam(model.parameters(), lr=0.01) # 使用Adam优化器,学习率为0.01
-
- # 训练模型
- epochs = 100 # 定义训练的轮数
- losses = [] # 存储每轮的损失值
- for epoch in range(epochs):
- # 在每轮开始前清空梯度
- optimizer.zero_grad()
-
- # 进行前向传播,得到模型的输出
- outputs = model(adjacency_matrix, input_features)
-
- # 计算损失值
- loss = criterion(outputs, labels)
-
- # 进行反向传播,计算梯度
- loss.backward()
-
- # 更新模型参数
- optimizer.step()
-
- # 将当前轮次的损失值存储起来
- losses.append(loss.item())
-
- # 每10轮输出一次训练进度和损失值
- if (epoch + 1) % 10 == 0:
- print("Epoch [{}/{}], Loss: {:.4f}".format(epoch + 1, epochs, loss.item()))
-
- # 可视化训练过程中的损失值
- plt.plot(range(epochs), losses)
- plt.xlabel('Epochs')
- plt.ylabel('Loss')
- plt.title('Training Loss')
- plt.show()
-
- # 对新样本进行预测
- # 创建一个新的输入特征矩阵
- new_input_features = torch.tensor([[1, 2], [3, 4], [5, 6], [7, 8]], dtype=torch.float32)
-
- # 在不计算梯度的情况下进行预测
- with torch.no_grad():
- # 通过模型对新样本进行预测,获取输出概率矩阵
- outputs = model(adjacency_matrix, new_input_features)
-
- # 取输出概率矩阵每行中最大值的索引,作为预测类别
- predicted_labels = torch.argmax(outputs, dim=1)
-
- # 打印预测的标签
- print("Predicted Labels:", predicted_labels)
可视化结果如下:
这只是一个最基础的进行节点分类的例子,可用来了解GCN具体的实现过程
本文仅作为学习笔记使用,并无其他用处
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。