1 图学习任务
我们简单回顾下,上一节我们介绍了,图的机器学习任务主要是以下三种:
- Node Level:节点级别
- Link Level:边级别
- Graph Level:图级别å 并且三部分难度依次是由浅入深的
2 传统ML流程
- 定义和设计节点/边/图的特征
- 对所有训练数据构造特征
- 训练ML模型 (1)随机森林 (2)支持向量机 (3)神经网络等
- 应用模型 给定一个新的节点、边、图,然后获取特征进行预测
我们总结下 基于Graph的机器学习相关概念和流程,首先明确下目标
目标:对一些对象集合进行预测,比如是分类或者回归任务 特征设计:
- 特征:
d-dimensional
向量 - 对象:Nodes,edges,或者是graps
- 目标函数:结合具体任务设计目标函数,如下图所示给定一个图G(V,E),其中V代表节点集合,E代表边集合,然后学习节点到向量空间R的映射函数,也就是我们要学习权重参数W
为了方便,我们下面的例子是基于无向图(undirected grpah)进行解释的。
3 节点级别的相关任务
基于图中带有标签的节点训练模型,然后预测未标注节点的标签,
在这里我们主要阐述下Node的四种特征:
- Node degree:节点的度
- Node centrality:节点的中度
- Clustering coefficient:相似性
- Graphlets:图元
节点的度
- kv代表是节点v与邻居节点相连边的个数
- 所有邻居节点都是相等的
如下图所示,A的度为1,B的度为2,C的度为3,D的度为4
节点的中心度 Node Centrality
- 节点的度只计算了相连节点的个数,但是没有评估节点的重要性
- 节点的中心度
考虑了节点在图中的重要程度
- 节点的中心度有很多种计算方式: (1) Engienvector centrality:特征向量中心性 (2) Betweenness centrality:中介中心性 (3) Closeness centrality:紧密中心性 (4) 其他方式
Eigenvector centrality:特征向量中心性
- 如果一个节点的邻居节点们
越重要,那么该节点
就越重要
- 我们将节点?的中心性建模为相邻节点的中心性之和:
其中
为一个正常数。
- 我们注意到上面的等式是通过递归的方式来计算度度中心性的,所有我们应该怎么求解
其中
为正常数,
为邻接矩阵,如果
,那么
- 我们可以看到度中心性是一个特征向量(eigenvector),根据非负矩阵定理(Perron-Frobenius Theorem)我们可以知道
是一个正值并且是唯一的,特征向量
当做度中心性。
通常来说,有许多不同的特征值
能使得一个特征方程有非零解存在。然而,考虑到特征向量中的所有项均为非负值,根据佩伦-弗罗贝尼乌斯定理,只有特征值最大时才能测量出想要的中心性。然后通过计算网络中的节点
其特征向量的相关分量
便能得出其对应的中心性的分数。
特征向量的定义只有一个公因子,因此各节点中心性的比例可以很好确定。为了确定一个绝对分数,必须将其中一个特征值标准化,例如所有节点评分之和为1或者节点数 n。幂次迭代是许多特征值算法中的一种,该算法可以用来寻找这种主导特征向量。此外,以上方法可以推广,使得矩阵A中每个元素可以是表示连接强度的实数,例如随机矩阵。---特征向量中心性wiki
这里其实涉及到比较多的线性代数的理论以及矩阵分析的算法,我特意查了一些文章帮大家去回顾和理解下这里涉及到的知识: (1) 线性代数之——特征值和特征向量 (2)非负矩阵之Perron-Frobenius定理 (3)非负矩阵 (4)干货 | 万字长文带你复习线性代数!
我们这里再通过文章谁是社会网络中最重要的人?解释特征向量中心性: 特征向量中心性的基本思想是,一个节点的中心性是相邻节点中心性的函数。也就是说,与你连接的人越重要,你也就越重要。
特征向量中心性和点度中心性不同,一个点度中心性高即拥有很多连接的节点特征向量中心性不一定高,因为所有的连接者有可能特征向量中心性很低。同理,特征向量中心性高并不意味着它的点度中心性高,它拥有很少但很重要的连接者也可以拥有高特征向量中心性。
考虑下面的图,以及相应的5x5的邻接矩阵(Adjacency Matrix),A。
邻接矩阵的含义是,如果两个节点没有直接连接,记为0,否则记为1。
现在考虑x,一个5x1的向量,向量的值对应图中的每个点。在这种情况下,我们计算的是每个点的点度中心性(degree centrality),即以点的连接数来衡量中心性的高低。
矩阵A乘以这个向量的结果是一个5x1的向量:
结果向量的第一个元素是用矩阵A的第一行去“获取”每一个与第一个点有连接的点的值(连接数,点度中心性),也就是第2个、第3个和第4个点的值,然后将它们加起来。
换句话说,邻接矩阵做的事情是将相邻节点的求和值重新分配给每个点。这样做的结果就是“扩散了”点度中心性。你的朋友的朋友越多,你的特征向量中心性就越高。
我们继续用矩阵A乘以结果向量。如何理解呢?实际上,我们允许这一中心性数值再次沿着图的边界“扩散”。我们会观察到两个方向上的扩散(点既给予也收获相邻节点)。我们猜测,这一过程最后会达到一个平衡,特定点收获的数量会和它给予相邻节点的数量取得平衡。既然我们仅仅是累加,数值会越来越大,但我们最终会到达一个点,各个节点在整体中的比例会保持稳定。
现在把所有点的数值构成的向量用更一般的形式表示:
我们认为,图中的点存在一个数值集合,对于它,用矩阵A去乘不会改变向量各个数值的相对大小。也就是说,它的数值会变大,但乘以的是同一个因子。用数学符号表示就是:
满足这一属性的向量就是矩阵M的特征向量。特征向量的元素就是图中每个点的特征向量中心性。
特征向量中心性的计算需要读者具备矩阵乘法和特征向量的知识,但不影响这里读者对特征向量中心性思想的理解,不再赘述。
Betweenness centrality:中介中心性 如果一个节点位于很多条其他节点的最短路径上,那么改节点比较重要,定义如下:
我们可以看一个下面的例子:
假设我们要计算D的中介中心性:
- 首先,我们计算节点D之外,所有节点对之间的最短路径有多少条,这里是1条(在5个节点中选择两个节点即节点对的个数)。
- 然后,我们再看所有这些最短路径中有多少条经过节点D,例如节点A要想找到节点E,必须经过节点D。经过节点D的最短路径有3条(A-C-D-E,B-D-E,C-D-E)。
- 最后,我们用经过节点D的最短路径除以所有节点对的最短路径总数,这个比率就是节点D的中介中心性。节点D的中介中心性是3/1=3。
4 PyTorch Geometric教程
官方文档:https://pytorch-geometric.readthedocs.io/en/latest/index.html
PyTorch Geometric(PyG)是PyTorch的扩展库。 它提供了有用的框架来开发图深度学习模型,包括各种图神经网络层和大量基准数据集。
如果我们暂时不了解某些概念,例如“ GCNConv”,请不要担心,之后我们将在以后的文章中介绍这些概念。
这个教程来自: https://colab.research.google.com/drive/1h3-vJGRVloF5zStxL5I0rSy4ZUPNsjy8?usp=sharing#scrollTo=ci-LpZWhRJoI
by Matthias Fey
大家可以参照colab教程直接运行
4.1 PyG安装
- 查看当前torch和cuda版本
!python -c "import torch; print(torch.__version__)"
输出:1.6.0
!python -c "import torch; print(torch.version.cuda)"
输出:10.1
- 安装gpu版本
# 安装GPU版本
!pip install --no-index torch-scatter -f https://pytorch-geometric.com/whl/torch-1.7.0 cu101.html
!pip install --no-index torch-sparse -f https://pytorch-geometric.com/whl/torch-1.7.0 cu101.html
!pip install --no-index torch-cluster -f https://pytorch-geometric.com/whl/torch-1.7.0 cu101.html
!pip install --no-index torch-spline-conv -f https://pytorch-geometric.com/whl/torch-1.7.0 cu101.html
!pip install torch-geometric
可能安装gpu版本比较麻烦,大家可以多参照下官网进行安装:
https://pytorch-geometric.readthedocs.io/en/latest/notes/installation.html
image
- 安装cuda版本
!pip install torch-scatter==latest cpu -f https://pytorch-geometric.com/whl/torch-1.6.0.html
!pip install torch-sparse==latest cpu -f https://pytorch-geometric.com/whl/torch-1.6.0.html
!pip install torch-cluster==latest cpu -f https://pytorch-geometric.com/whl/torch-1.6.0.html
!pip install torch-spline-conv==latest cpu -f https://pytorch-geometric.com/whl/torch-1.6.0.html
!pip install torch-geometric
4.2 PyG数据集
PyTorch Geometric 可以通过 torch_geometric.datasets
快速的获取默认的数据集:
https://pytorch-geometric.readthedocs.io/en/latest/modules/datasets.html
image
一共有20 种数据,比较丰富~
代码语言:javascript复制# 导入空手道俱乐部数据集
from torch_geometric.datasets import KarateClub
dataset = KarateClub()
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}')
输出:
代码语言:javascript复制Dataset: KarateClub():
======================
Number of graphs: 1
Number of features: 34
Number of classes: 4
初始化[KarateClub
](https://pytorch-geometric.readthedocs.io/en/latest/modules/datasets.html#torch_geometric.datasets.KarateClub) 数据集之后,我们首先可以检查其某些属性。
例如,我们可以看到该数据集恰好有一个Graph,并且该数据集中的每个节点都被分配了** 34维特征向量(唯一地描述了空手道俱乐部的成员)。
此外,该图正好包含 4个类别**,代表每个节点所属的社区。
现在让我们更详细地看一下这个Graph:
代码语言:javascript复制data = dataset[0] # 获取graph对象.
print(data)
print('==============================================================')
# 获取一些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()}')
输出如下:
代码语言:javascript复制Data(edge_index=[2, 156], train_mask=[34], x=[34, 34], y=[34])
==============================================================
Number of nodes: 34
Number of edges: 156
Average node degree: 4.59
Number of training nodes: 4
Training node label rate: 0.12
Contains isolated nodes: False
Contains self-loops: False
Is undirected: True
通过上面的信息我们可以发现,这个KarateClub网络图一共有34个节点,边的个数为1,每个节点度的平均数有4.59,是一个无向图等等。
4.3 Data对象
PyTorch Geometric中的每个图形都由单个[Data
](https://pytorch-geometric.readthedocs.io/en/latest/modules/data.html#torch_geometric.data.Data)对象表示,该对象包含所有 描述其图形表示的信息。
我们可以随时通过print(data)打印数据对象,以接收有关其属性及其形状的简短介绍:
Data(edge_index=[2, 156], x=[34, 34], y=[34], train_mask=[34])
我们可以看到这个data
对象拥有4个属性:
(1)“ edge_index”属性保存有关“图形连接性”(即)的信息,即每个边缘的源节点和目标节点索引的元组。
PyG进一步将
(2)节点特征称为x(向34个节点中的每个节点分配了34维度的特征向量),将
(3)节点标记*称为y(每个节点仅分配给一个类别)。
(4)还有一个名为“ train_mask”的附加属性,它描述了我们已经知道其社区归属的节点。
总体而言,我们只知道4个节点的基本标签(每个社区一个),任务是推断其余节点的社区分配。
数据对象还提供了一些“实用功能”来推断基础图的一些基本属性。 例如,我们可以轻松推断图中是否存在孤立的节点(* ie 任何节点都没有边),图是否包含自环(i.e.,
),或者图形是否是无向的( (i.e.*, for each edge
there also exists the edge
).
代码语言:javascript复制from IPython.display import Javascript # Restrict height of output cell.
display(Javascript('''google.colab.output.setIframeHeight(0, true, {maxHeight: 300})'''))
edge_index = data.edge_index
# 打印节点
print(edge_index.t())
输出:
代码语言:javascript复制tensor([[ 0, 1],
[ 0, 2],
[ 0, 3],
[ 0, 4],
[ 0, 5],
[ 0, 6],
[ 0, 7],
[ 0, 8],
[ 0, 10],
....
[33, 31],
[33, 32]])
通过打印“ edge_index”,我们可以进一步了解PyG如何在内部表示图形连接。 我们可以看到,对于每个边缘,“ edge_index”都包含两个节点索引的元组,其中第一个值描述源节点的节点索引,第二个值描述边缘目标节点的节点索引。
这种表示称为** COO格式(坐标格式)**,通常用于表示稀疏矩阵。 代替以密集表示形式保存邻接信息
,,PyG稀疏地表示图形,这是指仅保存
中的条目为非零的坐标/值。
最后,我们可以通过将图形转换为networkx
库格式来进一步可视化图形,该格式除了图形操作功能之外,还实现了强大的可视化工具。我们创建一个专门用于展示Graph的函数
%matplotlib inline
import torch
import networkx as nx
import matplotlib.pyplot as plt
# Visualization function for NX graph or PyTorch tensor
def visualize(h, color, epoch=None, loss=None):
plt.figure(figsize=(7,7))
plt.xticks([])
plt.yticks([])
if torch.is_tensor(h):
h = h.detach().cpu().numpy()
plt.scatter(h[:, 0], h[:, 1], s=140, c=color, cmap="Set2")
if epoch is not None and loss is not None:
plt.xlabel(f'Epoch: {epoch}, Loss: {loss.item():.4f}', fontsize=16)
else:
nx.draw_networkx(G, pos=nx.spring_layout(G, seed=42), with_labels=False,
node_color=color, cmap="Set2")
plt.show()
代码语言:javascript复制from torch_geometric.utils import to_networkx
G = to_networkx(data, to_undirected=True)
visualize(G, color=data.y)
image
4.4 实战:基于GCN的Graph节点分类
接下来将通过Pytorch实现一个最基本的GCN网络用语节点分类,基于带有标签的节点数据进行训练模型,然后预测未带有标签的数据
在了解了PyG的数据处理之后,是时候实现我们的第一个Graph神经网络了!我们在这里将使用最简单的GNN网络之一,即GCN层**([Kipf等人(2017)](https://arxiv.org/abs/1609.02907))用于Graph节点分类。
PyG通过[GCNConv
](https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#torch_geometric.nn.conv.GCNConv)来实现此层,可以通过传入 节点要素表示形式“ x”和COO图形连接性表示形式“ edge_index”。
这样,我们就可以通过在torch.nn.Module类中定义我们的网络架构来创建我们的第一个图形神经网络:
代码语言:javascript复制import torch
from torch.nn import Linear
from torch_geometric.nn import GCNConv
class GCN(torch.nn.Module):
def __init__(self):
super(GCN, self).__init__()
torch.manual_seed(12345)
self.conv1 = GCNConv(dataset.num_features, 4)
self.conv2 = GCNConv(4, 4)
self.conv3 = GCNConv(4, 2)
self.classifier = Linear(2, dataset.num_classes)
def forward(self, x, edge_index):
h = self.conv1(x, edge_index)
h = h.tanh()
h = self.conv2(h, edge_index)
h = h.tanh()
h = self.conv3(h, edge_index)
h = h.tanh() # Final GNN embedding space.
# Apply a final (linear) classifier.
out = self.classifier(h)
return out, h
model = GCN()
print(model)
网络结构输出如下:
代码语言:javascript复制GCN(
(conv1): GCNConv(34, 4)
(conv2): GCNConv(4, 4)
(conv3): GCNConv(4, 2)
(classifier): Linear(in_features=2, out_features=4, bias=True)
)
在这里,我们首先在__init__
中初始化所有构建块,并在“转发”中定义网络的计算流程。
我们首先定义并堆叠三层图卷积层,这对应于在每个节点周围(距离3个“跳”为止的所有节点)汇总3跳邻域信息。
另外,GCNConv
层将节点特征维数减小为
, i.e.,
.。每个[GCNConv]层都通过[tanh](https://pytorch.org/docs/stable/generation/torch.nn.Tanh.html?highlight=tanh#torch.nn.Tanh)非线性进行了增强。
之后,我们应用单个线性变换([torch.nn.Linear
](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html?highlight=linear#torch.nn。线性)),用作将我们的节点映射到4个类/社区中的1个的分类器。
我们返回最终分类器的输出以及GNN生成的最终节点嵌入。
我们继续通过GCN()
初始化最终模型,并打印我们的模型以产生所有使用过的子模块的概括。
获取隐藏层的表示:
代码语言:javascript复制model = GCN()
_, h = model(data.x, data.edge_index)
print(f'Embedding shape: {list(h.shape)}')
visualize(h, color=data.y)
输出:
代码语言:javascript复制Embedding shape: [34, 2]
image
在这里值得注意的是,即使在训练我们的模型权重之前,这个初始化的GCN网络也会产生节点的嵌入,并且这些嵌入与图的社区结构非常相似。尽管我们的模型的权重是“完全随机地初始化”的,并且到目前为止,我们还没有进行任何训练,但是相同颜色(社区)的节点已经在嵌入空间中紧密地聚集在一起。得出这样的结论,即GNN引入了强烈的节点偏差,从而导致在输入图中彼此靠近的节点具有相似的嵌入
。
但是,节点表示并不是很完美,我们可以看出来四种类别的节点还是混在一块了(对应途中是四种颜色),我们可以做得更好吗? 让我们看一个示例,该示例如何基于对图中4四种节点的社区分配(每个社区一个)的知识来训练我们的网络参数:
由于模型中的所有内容都是可区分的和参数化的,因此我们可以添加一些标签,训练模型并观察嵌入如何反应。 在这里,我们使用一种半监督学习程序:我们仅针对每个类训练一个节点,但是允许使用完整的输入图数据。
这个怎么理解呢,我们其实可以输出data.y
和data.train_mask
就明白了:
data.y
tensor([0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 1, 0, 0, 0, 2, 2, 1, 0, 2, 0, 2, 0, 2, 2,
3, 3, 2, 2, 3, 2, 2, 3, 2, 2])
代码语言:javascript复制data.train_mask
tensor([ True, False, False, False, True, False, False, False, True, False,
False, False, False, False, False, False, False, False, False, False,
False, False, False, False, True, False, False, False, False, False,
False, False, False, False])
大家可以看到相当于我们训练网络的时候只针对每一个类别下的token做优化,这样可以加速网络的训练和收敛。
训练我们的模型与任何其他PyTorch模型非常相似。
除了定义我们的网络架构之外,我们还定义了损失函数在这里[[CrossEntropyLoss]](https://pytorch.org/docs/stable/generate/torch.nn.CrossEntropyLoss.html)) 并初始化随机梯度优化器(此处为[Adam
](https://pytorch.org/docs/stable/optim.html?highlight=adam#torch.optim.Adam))。
之后,我们执行多轮优化,其中每一轮都包含一个正向和反向传递,以计算模型参数w.r.t的梯度。从前向通行证产生的损失。
如果您对PyTorch并不陌生,则该方案应该对您来说很熟悉。
否则,PyTorch文档会提供[有关如何在PyTorch中训练神经网络的很好的介绍](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#define-a-loss-function-and-optimizer )。
请注意,我们的半监督学习场景是通过以下行实现的:
代码语言:javascript复制loss = criterion(out[data.train_mask], data.y[data.train_mask])
在计算所有节点的节点嵌入时,我们“仅利用训练节点来计算loss” **。 在这里,这是通过过滤分类器“ out”和真实性标签“ data.y”的输出以仅包含“ train_mask”中的节点来实现的。
现在让我们开始训练,看看我们的节点嵌入随时间如何演变(最好是显式地运行代码,打印出模型的训练效果):
代码语言:javascript复制import time
from IPython.display import Javascript #画图.
display(Javascript('''google.colab.output.setIframeHeight(0, true, {maxHeight: 430})'''))
model = GCN()
criterion = torch.nn.CrossEntropyLoss() # 定义loss
optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # 优化器
def train(data):
optimizer.zero_grad() # 梯度清零.
out, h = model(data.x, data.edge_index) # GCN模型.
loss = criterion(out[data.train_mask], data.y[data.train_mask]) # 计算真实loss.
loss.backward() # 反向求导.
optimizer.step() # 参数更新.
return loss, h
for epoch in range(401):
loss, h = train(data)
# 每10个epochs打印Graph
if epoch % 10 == 0:
visualize(h, color=data.y, epoch=epoch, loss=loss)
time.sleep(0.3)
image
image
过了一段时间之后,我们可以看到GCN强大的效果:
image
image
可以看到,我们的3层GCN模型可以有效地社区发现并正确地对大多数节点进行分类。