StellarGraph库支持许多最先进的图形机器学习(ML)算法。在这本笔记本中,我们将训练一个模型来预测节点的类或标签,通常称为节点分类。我们还将使用得到的模型来计算每个节点的向量嵌入。
原文链接
gcn算法原理介绍
要完成这项任务,有两个必要的部分:
1.图:本笔记本使用了来自https://lings.soe.ucsc.edu/data的Cora数据集。该数据集由学术出版物作为节点,它们之间的引用作为链接:如果出版物A引用出版物B,那么图中有一条从A到B的边。节点被分为七个主题中的一个,我们的模型将学习预测这个主题。
2.一个算法:这个笔记本使用了一个图卷积网络(GCN)。GCN神经网络模型的核心是“图卷积”层。这一层类似于传统的密集层,通过图邻接矩阵增强,以使用关于节点连接的信息。这个算法在“了解你的邻居:图上的机器学习”中有更详细的讨论。
这本笔记本分为三个部分:
- 使用Pandas和scikit-learn进行数据准备:从CSV文件加载图表,进行一些基本的内省,并将其划分为ML的训练、测试和验证分割
- 使用StellarGraph创建GCN层和数据输入
- 使用TensorFlow Keras、Pandas和scikit-learn对模型进行训练和评估
import pandas as pd
import os
##第一步是导入我们需要的Python库。
##为了方便起见,我们以sg的名称导入stellargraph,类似于pandas经常被导入为pd
import stellargraph as sg
from stellargraph.mapper import FullBatchNodeGenerator
from stellargraph.layer import GCN
from tensorflow.keras import layers, optimizers, losses, metrics, Model
from sklearn import preprocessing, model_selection
from IPython.display import display, HTML
import matplotlib.pyplot as plt
#******************************************************************************************************8********数据准备*****************************************************************************************#
#*********************************************************************************1加载cora网络*****************#
##我们可以从datassets子模块(docs)中使用Cora加载器检索一个包含这个Cora数据集的StellarGraph图形对象。
##它还为我们提供了ground-truth节点主题类。这个函数是使用Pandas实现的,详见“从Pandas加载数据到StellarGraph”笔记本。
##(注:Cora是一个引用网络,它是一个有向图,但是,和这个图的大多数用户一样,我们忽略了边缘方向,将其视为无向的。)
dataset = sg.datasets.Cora()
display(HTML(dataset.description))
G, node_subjects = dataset.load()
##Cora数据集包含2708份科学出版物,分为七个类别之一。
##引文网络由5429个链接组成。数据集中的每个出版物都用0/1值的词向量描述,该词向量指示字典中是否存在相应的词。
##这本词典由1433个独特的单词组成。
##info方法可以帮助我们验证加载的图形是否符合描述:
print(G.info())
##<IPython.core.display.HTML object>
##StellarGraph: Undirected multigraph
## Nodes: 2708, Edges: 5429
## Node types:
## paper: [2708]
## Features: float32 vector, length 1433
## Edge types: paper-cites->paper
## Edge types:
## paper-cites->paper: [5429]
## Weights: all 1 (default)
## Features: none
##我们的目标是训练一个graph-ML模型来预测节点上的“subject”属性。
##这些subject是7个类别中的一个,有些类别比其他类别更常见:
node_subjects.value_counts().to_frame()
#***********************************************************************************2分割数据*****************#
##对于机器学习,我们希望取节点的一个子集进行训练,并使用其余的节点进行验证和测试。
##我们将使用scikit-learn的train_test_split函数(docs)来做到这一点。
##这里我们使用140个节点标签进行训练,500个节点标签进行验证,剩下的用于测试。
train_subjects, test_subjects = model_selection.train_test_split(
node_subjects, train_size=140, test_size=None, stratify=node_subjects
)
val_subjects, test_subjects = model_selection.train_test_split(
test_subjects, train_size=500, test_size=None, stratify=test_subjects
)
##注意,使用分层抽样得到以下计数:
train_subjects.value_counts().to_frame()
#*********************************************************************************3转换成数字数组************#
##对于我们的分类目标,我们将使用一个one-hot vector与模型的soft-max output进行比较。
##要完成这个转换,我们可以使用scikit-learn中的LabelBinarizer转换(文档)。
##另一个选择是pandas.Get_dummies函数(docs),
##但是scikit-learn转换允许我们稍后在笔记本中轻松地进行反向转换,以解释预测。
target_encoding = preprocessing.LabelBinarizer()
##CORA数据集包含与该出版物中找到的单词对应的属性w_x。
##如果一个词在发布中出现多次,则相关属性将被设置为1,否则将为0。
##这些数值属性已经自动包含在StellarGraph实例G中,因此我们不需要进行任何进一步的转换。
train_targets = target_encoding.fit_transform(train_subjects)
val_targets = target_encoding.transform(val_subjects)
test_targets = target_encoding.transform(test_subjects)
#*************************************************************************************************************创建GCN layes***********************************************************************************#
##向FullBatchNodeGenerator指定method-'gcn'参数意味着它将通过使用归一化图拉普拉斯矩阵来捕获图结构,
##生成适合gcn算法的数据。
generator = FullBatchNodeGenerator(G, method="gcn")
##生成器只编码产生模型输入所需的信息。
##使用一组节点和它们的真实标签调用flow方法(docs)会生成一个对象,该对象可用于在指定的那些节点和标签上训练模型。
##我们在上面创建了一个训练集,这就是我们在这里要用到的。
train_gen = generator.flow(train_subjects.index, train_targets)
##现在我们可以通过构建一个层栈来指定我们的机器学习模型。
##我们可以使用StellarGraph的GCN类(docs),它打包了图形卷积和退出层堆栈的创建。我们可以指定一些参数来控制它:
##layer_sizes:隐藏的GCN层的数量和它们的大小。在本例中,两个GCN层各有16个单元。
##activations:应用于每个GCN层的输出的激活。在本例中,两个层都使用RelU。
##dropout:各GCN层输入的dropout率。在这种情况下,是50%。
gcn = GCN(
layer_sizes=[16, 16], activations=["relu", "relu"], generator=generator, dropout=0.5
)
##为了创建Keras模型,我们现在通过GCN公开GCN模型的输入和输出张量,用于节点预测。in_out_tensors方法:
x_inp, x_out = gcn.in_out_tensors()
x_out
##<tf.Tensor 'gather_indices/Identity:0' shape=(1, None, 16) dtype=float32>
predictions = layers.Dense(units=train_targets.shape[1], activation="softmax")(x_out)
##x_out值是一个TensorFlow张量,为训练或预测时请求的节点保存16维向量。
##每个节点的class/subject的实际预测需要从这个向量计算。
##StellarGraph是使用Keras功能构建的,因此可以使用标准的Keras功能:使用softmax激活附加的密集层(每个类有一个单元)。
##这个激活函数确保每个输入节点的最终输出将是一个“概率”向量,其中每个值都在O和1之间,并且整个向量和为1。
##预测的类是具有最高值的元素。
#*****************************************************************************************************************训练和验证***********************************************************************************#
#*******************************************************************************1训练模型*****************#
#现在让我们创建一个实际的Keras模型输入张量x_inp和输出张量是来自最终密度层的预测。
#我们的任务是一个分类预测任务,因此一个分类交叉熵损失函数是合适的。
#即使我们使用StellarGraph进行graph ML,我们仍然使用传统的Keras预测值,所以我们可以直接使用Keras的损失函数。
model = Model(inputs=x_inp, outputs=predictions)
model.compile(
optimizer=optimizers.Adam(learning_rate=0.01),
loss=losses.categorical_crossentropy,
metrics=["acc"],
)
#在训练模型时,我们还希望跟踪它在验证集上的泛化性能,
#这意味着使用前面创建的FullBatchNodeGenerator创建另一个数据生成器。
val_gen = generator.flow(val_subjects.index, val_targets)
#如果验证精度停止提高,我们可以直接使用Keras提供的earlystopped功能(docs)来停止训练。
from tensorflow.keras.callbacks import EarlyStopping
es_callback = EarlyStopping(monitor="val_acc", patience=50, restore_best_weights=True)
#我们现在已经设置了模型层、训练数据、验证数据甚至训练回调,所以我们现在可以使用模型的fit方法(docs)来训练模型。
#与本节中的大多数内容一样,这都是内置在Keras中的。
history = model.fit(
train_gen,
epochs=200,
validation_data=val_gen,
verbose=2,
shuffle=False, # this should be False, since shuffling data means shuffling the whole graph
callbacks=[es_callback],
)
#一旦我们训练好模型,我们就可以使用plot_history函数查看行为损失函数和任何其他指标。
#在这种情况下,我们可以看到训练集和验证集的损失和准确性。
sg.utils.plot_history(history)
plt.show()
#作为我们评估的最后一部分,让我们对照测试集检查模型。
#我们再次使用上面的FullBatchNodeGenerator上的flow方法创建所需的数据,
#并可以使用模型的evaluate方法(docs)为训练过的模型计算度量值。
#正如预期的那样,模型在训练期间的验证集和这里的测试集上执行类似的操作。
test_gen = generator.flow(test_subjects.index, test_targets)
test_metrics = model.evaluate(test_gen)
print("\nTest Set Metrics:")
for name, val in zip(model.metrics_names, test_metrics):
print("\t{}: {:0.4f}".format(name, val))
#**************************************************************************************2预测*****************#
#现在让我们得到所有节点的预测。您现在可能已经习惯了它,但是我们使用FullBatchNodeGenerator创建所需的输入,
#然后使用模型的一个方法:predict (docs)。
#这次我们不为flow提供标签,而只提供节点,因为我们试图在不知道这些类的情况下预测它们。
all_nodes = node_subjects.index
all_gen = generator.flow(all_nodes)
all_predictions = model.predict(all_gen)
#这些预测将是softmax层的输出,
#因此为了获得最终的类别,我们将使用目标属性规范的inverse_transform方法将这些值转回原始类别。
#注意,对于全批处理方法,批大小为1,预测有形状(1,Nnodes, Nclasses),
#因此我们删除批处理维度,使用NumPy挤压方法获得形状(Nnodes, Nclasses)的预测。
node_predictions = target_encoding.inverse_transform(all_predictions.squeeze())
#让我们来看看训练模型后的几个预测:
df = pd.DataFrame({"Predicted": node_predictions, "True": node_subjects})
df.head(20)
#************************************************************************************************3结点嵌入*****************#
#我们可以将这些嵌入图形上的点可视化,用它们真实的主题标签着色。
#如果模型已经根据节点的类了解到关于节点的有用信息,我们期望在节点嵌入空间中看到良好的论文集群,同一subject的论文属于同一集群。
#为了创建计算节点嵌入的模型,我们使用与上面预测模型相同的输入张量(x_inp),只是将输出张量交换到GCN张量(x_out)而不是预测层。
#这些张量与我们在训练上面的预测时训练的相同的层和权值相连接,所以我们只使用这个模型来计算/“预测”节点嵌入向量。
#类似于对每个节点进行预测,我们将使用all_gen数据计算每个节点的嵌入。
embedding_model = Model(inputs=x_inp, outputs=x_out)
emb = embedding_model.predict(all_gen)
emb.shape
#(1, 2708, 16)
#最后一个GCN层的输出维数为16,这意味着每个embedding包含16个数字。
#直接绘制这个图需要一个16维的图,这对人类来说是很难想象的。
#相反,我们可以首先将这些向量投射到2个数字上,制作出可以在普通2D散点图上绘制的二维向量。
#对于这个降维任务,有许多工具,其中许多是由scikitlearn提供的。
#其中比较常见的两种是主成分分析(PCA)(线性)和t分布随机邻居嵌入(t-SNE或TSNE)(非线性)。
#t-SNE的速度较慢,但在绘图时通常能得到更好的结果。
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
transform = TSNE # or PCA
#注意,来自GCN模型的嵌入的批处理维数为1,因此我们将其squeeze得到一个Nnodes x Nemb的矩阵
X = emb.squeeze(0)
X.shape
#(2708,16)
#因此,我们已经准备好了高维嵌入并选择了降维变换,所以现在我们计算降维向量,作为新值的两列。
trans = transform(n_components=2)
X_reduced = trans.fit_transform(X)
X_reduced.shape
#(2708,2)
#x_reduced值包含每个节点的一对数字,其顺序与地面真相标签的node_subjects系列相同(因为all_gen就是这样创建的)。
#这就足够做一个带颜色的节点散点图了。
#我们可以让matplotlib通过将主题映射到整数O, 1,…, 6,使用Pandas对分类数据的支持。
#定性地说,这幅图显示了良好的聚类,其中单一颜色的节点大多被分组在一起。
fig, ax = plt.subplots(figsize=(7, 7))
ax.scatter(
X_reduced[:, 0],
X_reduced[:, 1],
c=node_subjects.astype("category").cat.codes,
cmap="jet",
alpha=0.7,
)
ax.set(
aspect="equal",
xlabel="$X_1$",
ylabel="$X_2$",
title=f"{transform.__name__} visualization of GCN embeddings for cora dataset",
)
plt.show()