【机器学习】训练GNN图神经网络模型进行节点分类

打印 上一主题 下一主题

主题 512|帖子 512|积分 1538

1. 引言

1.1 图神经网络GNN概述

图神经网络(Graph Neural Network,GNN)是一种专门用于处置惩罚图结构数据的神经网络方法。它起源于2005年,当时Gori等人首次提出了GNN的概念,用于学习图中的节点特性以及它们之间的关系。随后,随着深度学习技术的快速发展,GNN得到了广泛的关注和研究。
1.1.1 GNN的核心算法思想

GNN的核心思想是通过迭代地聚合节点的邻居信息来更新节点的表示,从而捕获图的结构信息。这种聚合过程可以看作是一种特殊的图卷积操作,使得GNN可以或许有效地处置惩罚图数据,并提取出节点和边之间的关系特性。主流的GNN算法包罗图卷积神经网络(GCN)、图自编码器(Graph Autoencoder)、图生成网络(Graph Generative Network)等。
1.1.2 GNN的应用场景

GNN的应用场景非常广泛,包罗交际网络分析、推荐体系、生物信息学、交通流预测等。比方,在交际网络中,GNN可以用于分析用户之间的关系,预测用户的爱好和举动;在推荐体系中,GNN可以使用用户-物品交互图来提供个性化的推荐;在生物信息学中,GNN可以用于分析蛋白质-蛋白质相互作用网络,预测蛋白质的功能和性质。
1.1.3 GNN图神经网络面对的挑衅

尽管GNN在处置惩罚图数据方面取得了明显的希望,但仍面对一些挑衅。起首,大规模图数据的处置惩罚是一个难题,必要计划高效的GNN架构和训练算法来应对。其次,动态图数据的处置惩罚也是一个挑衅,由于图的结构和节点属性可能会随时间发生变革。别的,GNN在处置惩罚异构图(即节点和边具有差别类型和属性的图)时也必要进一步的研究。
未来,GNN的研究将继承关注提高模型的性能、扩展应用范围以及办理上述挑衅。随着深度学习技术的不停进步和计算本事的提升,GNN有望在更多领域发挥重要作用,并推动图数据分析和应用的进一步发展。
1.2 节点分类

节点分类(Node Classification)是图数据分析中的一个重要任务,其目标是根据图的结构信息和节点的属性特性来预测图中节点的类别标签。在图数据中,节点通常表示实体,而边则表示实体之间的关系。节点分类在很多领域都有广泛的应用,如交际网络分析、生物信息学、推荐体系等。
1.2.1 节点分类任务的步调

在图神经网络(Graph Neural Networks, GNNs)的框架下,节点分类通常通过以下步调实现:


  • 图数据表示:起首,必要将图数据转化为神经网络可以处置惩罚的表示形式。这通常包罗节点特性矩阵(用于描述节点的属性信息)和毗邻矩阵(用于描述节点之间的关系)。
  • 图神经网络层:然后,使用图神经网络层来聚合节点的邻居信息。这些层通过特定的图卷积操作或图注意力机制来更新节点的表示,从而捕获图的结构信息。差别的GNN架构(如Graph Convolutional Networks, GraphSAGE, Graph Attention Networks等)具有差别的聚合函数和更新规则。
  • 特性传播与聚合:在图神经网络中,特性传播是一个关键步调。通过迭代地聚合节点的邻居信息,每个节点的表示都会逐渐融合其局部邻域的信息。这种过程可以重复多次,以便捕获更广泛的图结构信息。
  • 节点分类:最后,将聚合后的节点表示输入到分类器(如全连接层、softmax层等)中进行分类。分类器会根据节点的表示预测其所属的类别标签。
节点分类任务中,GNNs的优势在于它们可以或许捕获图数据的复杂依靠关系,并使用这些信息来提高分类性能。与传统的机器学习方法相比,GNNs可以或许更好地处置惩罚图数据的非欧几里得结构,并考虑节点之间的连接关系。
1.2.2 节点分类面对的挑衅

然而,节点分类也面对一些挑衅。起首,当图的规模非常大时,GNNs的训练和推理过程可能会变得非常耗时。其次,对于动态图数据,GNNs必要可以或许处置惩罚节点和边的添加、删除以及属性变革等情况。别的,当图中的节点和边具有差别的类型和属性时,GNNs也必要具备处置惩罚异构图的本事。
预测未来,节点分类的研究将继承关注提高GNNs的性能、扩展应用范围以及办理上述挑衅。随着深度学习技术的不停进步和计算本事的提升,GNNs有望在更多领域发挥重要作用,并推动图数据分析和应用的进一步发展。
1.3 GNN模型用于节点分类

图神经网络(GNN)在节点分类任务中发挥着关键作用。节点分类的目标是根据图的结构信息和节点的属性特性来预测图中每个节点的类别标签。GNN模型通过迭代地更新节点的表示来捕获节点之间的相互作用和依靠关系,进而实现高效的节点分类。
1.3.1 GNN进行节点分类的原理

GNN模型的工作原理在于通过聚合节点的邻居信息来更新节点的表示。起首,每个节点的表示向量被初始化为其初始特性向量。然后,在信息传递阶段,GNN通过图卷积操作或图注意力机制来聚合节点的邻居节点的信息,并更新节点的表示。这一过程中,邻居节点的信息被加权求和或聚合,以反映它们对目标节点的影响。通过多次迭代信息传递和聚合邻居信息的过程,GNN可以或许渐渐更新节点的表示,使其包罗更丰富的图结构信息。
1.3.2 GNN的优势及应用

GNN模型在节点分类任务中的优势在于其可以或许捕获图数据中的复杂依靠关系和结构信息,这对于节点分类任务至关重要。同时,GNN模型具有灵活性和可扩展性,可以或许处置惩罚具有差别巨细和结构的图数据,并适应大规模图数据的处置惩罚需求。
GNN在节点分类任务中的应用广泛,涵盖了交际网络分析、生物信息学和推荐体系等领域。在交际网络中,GNN可以辨认用户所属的潜在角色或群体,并根据爱好对用户进行分类。在生物信息学中,GNN可以应用于分子结构的分类,预测分子的功能和性质。在推荐体系中,GNN可以处置惩罚用户-项目交互的图结构数据,预测用户对未知物品的喜好程度,并向用户推荐合适的项目。
随着深度学习技术的不停进步和计算本事的提升,GNN有望在更多领域发挥重要作用,并推动图数据分析和应用的进一步发展。
2. GNN模型实现节点分类的过程

很多在各种机器学习(ML)应用中的数据集,实在体之间存在结构关系,这些关系可以表示为图。这类应用包罗交际和通信网络分析、交通预测以及欺诈检测。图表示学习旨在构建和训练用于图数据集的模型,以便用于各种机器学习任务。
本文的例子展示了一个图神经网络(GNN)模型的简单实现。该模型用于Cora数据集上的节点预测任务,以根据论文的词汇和引用网络来预测论文的主题。
本文我们重新开始实现了一个图卷积层,以便更好地理解它们是怎样工作的。然而,也有很多基于TensorFlow的专用库提供了丰富的GNN API,比方Spectral、StellarGraph和GraphNets等。
2.1 设置

  1. import os
  2. import pandas as pd
  3. import numpy as np
  4. import networkx as nx
  5. import matplotlib.pyplot as plt
  6. import tensorflow as tf
  7. from tensorflow import keras
  8. from tensorflow.keras import layers
复制代码
2.2 准备下载

以下的段代码是用于下载息争压缩Cora数据集的Python脚本:

  • 下载数据集
    - 使用keras.utils.get_file函数来下载数据集。这个函数是Keras提供的,用于下载文件并保存到本地路径。
    - fname="cora.tgz": 指定下载文件的名称为cora.tgz。
    - origin="https://linqs-data.soe.ucsc.edu/public/lbc/cora.tgz": 指定数据集的URL来源,即数据集在互联网上的位置。
    - extract=True: 表示下载完成后自动解压缩文件。
  • 设置数据目录
    - data_dir = os.path.join(os.path.dirname(zip_file), "cora"): 这行代码设置了解压缩后数据的存放目录。
    - os.path.dirname(zip_file): 获取zip_file变量的目录路径,即下载文件的存放路径。
    - os.path.join(..., "cora"): 将下载文件的目录路径与"cora"字符串连接,形成完整的数据集目录路径。
代码的作用是确保Cora数据集被下载并解压缩到步伐可以访问的目录中。Cora数据集通常用于图神经网络的节点分类任务,包罗了论文的引用关系和内容信息。在机器学习或深度学习项目中,这样的数据准备步调是常见的,以便于后续的数据加载和处置惩罚。
  1. zip_file = keras.utils.get_file(
  2.     fname="cora.tgz",
  3.     origin="https://linqs-data.soe.ucsc.edu/public/lbc/cora.tgz",
  4.     extract=True,
  5. )
  6. data_dir = os.path.join(os.path.dirname(zip_file), "cora")
复制代码
2.3 数据预处置惩罚

2.3.1 加载并可视化数据集

处置惩罚并可视化数据集代码是用来加载和处置惩罚Cora数据集中的引用信息:

  • 加载数据
    - 使用pd.read_csv函数从Pandas库来读取CSV文件。这个函数是用来读取CSV文件并将其转换成DataFrame对象的。
  • 指定文件路径
    - os.path.join(data_dir, "cora.cites"): 这行代码通过os.path.join函数来拼接数据集的底子目录data_dir和子目录"cora.cites",形成完整的文件路径。data_dir是在上一段代码中设置的Cora数据集的目录路径。
  • 设置分隔符和列名
    - sep="\t": 指定分隔符为制表符(\t),这意味着CSV文件中的列是以制表符分隔的。
    - header=None: 指定文件中没有头部信息,即列名不在文件的第一行中。
    - names=["target", "source"]: 指定列名,即使文件没有头部信息,这里明白了两列的名称分别为"target"和"source"。
  • 打印引用数据的形状
    - print("Citations shape:", citations.shape): 打印出DataFrame citations 的形状,即行数和列数。这通常用于查抄数据加载是否正确,以及理解数据集的规模。
引用数据集citations通常包罗了两列,一列是被引用论文的ID(target),另一列是引用它的论文的ID(source)。在图神经网络中,这些引用可以表示为图中的边,此中每条边连接了两个节点(论文),用于捕捉论文之间的相互关系。
  1. citations = pd.read_csv(
  2.     os.path.join(data_dir, "cora.cites"),
  3.     sep="\t",
  4.     header=None,
  5.     names=["target", "source"],
  6. )
  7. print("Citations shape:", citations.shape)
复制代码
2.3.2 展示数据框

下面的代码将展示引文(citations)数据框的一个样本。目标列包罗了由源列中的论文ID所引用的论文ID。
在Pandas中,citations.sample(frac=1).head()
这行代码执行了以下操作:

  • citations.sample(frac=1): 这是一个方法调用,作用是从citations DataFrame中随机抽取纪录。参数frac=1表示抽取全部的纪录(即100%的纪录)。如果不指定frac或者设置frac<1,则会按照给定的比例随机抽取纪录。
  • .head(): 这个方法调用返回采样效果的前n行,此中n默以为5。也就是说,它会返回一个DataFrame的前5行。这是查看DataFrame内容的一个快速方法,特别是当你不想查看整个数据集时。
综合来看,citations.sample(frac=1).head()
这行代码的作用是:从citations DataFrame中随机抽取全部纪录,然后返回这些纪录的前5行。这通常用于获取数据集的一个随机样本,以便进行快速查抄或展示数据的多样性。
请注意,每次执行这个操作时,由于是随机采样,返回的5行可能会差别。如果你渴望每次采样都得到雷同的效果,可以在调用sample方法时指定随机种子,比方citations.sample(frac=1, random_state=1).head()。
  1. citations.sample(frac=1).head()
复制代码
2.3.3 加载数据到Pandas DataFrame

现在,让我们将论文数据加载到Pandas DataFrame中。
起首创建了一个名为column_names的列表,它包罗了Cora数据集中论文内容文件的全部列名。第一列是paper_id,接下来的1433列用term_0到term_1432命名,代表论文中术语的存在与否,最后一列是subject,表示论文的主题。
然后使用pd.read_csv函数读取Cora数据集的cora.content文件,这个文件包罗了论文的内容信息。通过os.path.join函数构建文件的完整路径,sep参数指定了制表符作为字段的分隔符,header参数设置为None表示文件中没有提供列名的头部行,names参数用来指定前面创建的列名列表。
最后,使用print函数打印出加载后的papers DataFrame的形状,即它包罗的行数和列数,这有助于了解数据集的规模。
  1. column_names = ["paper_id"] + [f"term_{idx}" for idx in range(1433)] + ["subject"]
  2. papers = pd.read_csv(
  3.     os.path.join(data_dir, "cora.content"), sep="\t", header=None, names=column_names,
  4. )
  5. print("Papers shape:", papers.shape)
复制代码
  1. Papers shape: (2708, 1435)
复制代码
2.3.4 展示Pandas DataFrame

展示论文DataFrame的一个样本。该DataFrame包罗paper_id和subject列,以及1,433个二进制列,这些列表示相应术语是否存在于论文中。
  1. print(papers.sample(5).T)
复制代码
显示每个主题的论文数量
  1. print(papers.subject.value_counts())
复制代码
  1. Neural_Networks           818
  2. Probabilistic_Methods     426
  3. Genetic_Algorithms        418
  4. Theory                    351
  5. Case_Based                298
  6. Reinforcement_Learning    217
  7. Rule_Learning             180
  8. Name: subject, dtype: int64
复制代码
2.3.5 创建数据索引和转换

索引和转换的目标是对Cora数据集中的论文和引用信息进行预处置惩罚,使其适合用于图神经网络模型的训练。


  • 主题索引的创建:起首,代码提取subject列中全部独特的主题,并按字典次序对它们进行排序,生成一个有序列表class_values。
  • 主题到索引的映射:接着,通过罗列排序后的主题列表,创建一个字典class_idx,该字典将每个主题映射到一个唯一的索引,这个索引将用于后续模型训练中的主题表示。
  • 论文索引的创建:类似地,代码对paper_id列中的全部独特论文ID进行排序,并创建一个字典paper_idx,将排序后的论文ID映射到它们各自的索引。
  • 论文ID的转换:在papers数据集中,使用paper_idx字典将paper_id列中的论文ID转换为对应的数值索引。
  • 引用数据的转换:在citations数据集中,使用paper_idx字典将source(引用源)和target(引用目标)列中的论文ID转换为数值索引。
  • 主题的转换:最后,使用class_idx字典将papers数据集中的subject列中的主题名称转换为对应的数值索引。
通过这些步调,原始数据集中的文本标识符被转换为模型易于处置惩罚的数值形式,为后续的图神经网络模型训练做好了准备。这种转换有助于模型更高效地学习论文之间的引用关系以及它们对应的主题分类。
  1. class_values = sorted(papers["subject"].unique())
  2. class_idx = {name: id for id, name in enumerate(class_values)}
  3. paper_idx = {name: idx for idx, name in enumerate(sorted(papers["paper_id"].unique()))}
  4. papers["paper_id"] = papers["paper_id"].apply(lambda name: paper_idx[name])
  5. citations["source"] = citations["source"].apply(lambda name: paper_idx[name])
  6. citations["target"] = citations["target"].apply(lambda name: paper_idx[name])
  7. papers["subject"] = papers["subject"].apply(lambda value: class_idx[value])
复制代码
2.3.6 可视化引用图

代码用于可视化Cora数据集的引用网络,具体步调如下:


  • 设置图形巨细:使用plt.figure函数创建一个新的图形对象,并设置图形的巨细为10x10英寸,以确保有足够的空间来展示网络图。
  • 准备颜色列表:从papers DataFrame中提取subject列的全部唯一值,将其转换为列表colors。这个列表将用于为图中的节点着色,每个主题的论文将使用差别的颜色。
  • 生成图数据结构:使用networkx库的from_pandas_edgelist函数,从citations DataFrame中随机抽取1500条引用纪录来构建图cora_graph。这个图的节点代表论文,边代表论文之间的引用关系。
  • 筛选节点主题:从papers DataFrame中筛选出与cora_graph图中节点对应的论文主题列表subjects。这确保了图中的每个节点都能根据其主题着上正确的颜色。
  • 绘制网络图:使用networkx的draw_spring函数绘制图cora_graph,此中node_size参数设置为15,控制节点的巨细。node_color参数设置为subjects列表,这样每个节点就会根据其主题被着上差别的颜色。
通过这种方式,代码生成了一个视觉化的网络图,此中节点代表论文,节点之间的连线代表引用关系,节点的颜色表示论文的主题。这种可视化有助于直观地理解数据集中论文之间的相互关系及其主题分布。
  1. plt.figure(figsize=(10, 10))
  2. colors = papers["subject"].tolist()
  3. cora_graph = nx.from_pandas_edgelist(citations.sample(n=1500))
  4. subjects = list(papers[papers["paper_id"].isin(list(cora_graph.nodes))]["subject"])
  5. nx.draw_spring(cora_graph, node_size=15, node_color=subjects)
复制代码
图中的每个节点代表一篇论文,节点的颜色对应其主题。请注意,我们仅显示数据集中论文的一个样本。

2.3.7 数据分割

将数据集划分为分层的训练集和测试集
代码的目标是将Cora数据集分割为训练集和测试集,具体步调如下:


  • 初始化空列表:起首,初始化两个空列表train_data和test_data,用于存储训练集和测试集的数据。
  • 分组并随机选择:使用papers.groupby("subject")对数据集中的论文按主题进行分组。对于每个主题组,使用np.random.rand生成一个与该组论文数量雷同的随机数数组。通过比较这些随机数与0.5,决定每篇论文是否被选入训练集(小于或等于0.5的被选入训练集,别的的被选入测试集)。
  • 分配训练集和测试集:对于每个主题组,根据随机选择的效果,将论文分配到train_data和test_data列表中。
  • 合并数据:使用pd.concat函数将train_data和test_data列表中的全部分组数据合并成两个单独的DataFrame,分别代表整个训练集和测试集。
  • 随机打乱数据:使用.sample(frac=1)确保训练集和测试集中的论文是随机分布的,frac=1表示打乱全部数据。
  • 打印数据形状:最后,打印出训练集和测试集的形状,即它们各自的行数和列数,以验证数据集的规模。
通过这种方式,代码实现了数据集的分层抽样,确保了每个主题在训练集和测试集中都有代表性,同时也包管了数据的随机性,这对于训练和评估机器学习模型是非常重要的。
  1. train_data, test_data = [], []
  2. for _, group_data in papers.groupby("subject"):
  3.     # Select around 50% of the dataset for training.
  4.     random_selection = np.random.rand(len(group_data.index)) <= 0.5
  5.     train_data.append(group_data[random_selection])
  6.     test_data.append(group_data[~random_selection])
  7. train_data = pd.concat(train_data).sample(frac=1)
  8. test_data = pd.concat(test_data).sample(frac=1)
  9. print("Train data shape:", train_data.shape)
  10. print("Test data shape:", test_data.shape)
复制代码
2.4 训练和评估实验

2.4.1 定义超参数

设置用于训练图神经网络模型的超参数:


  • 隐藏单元数 (hidden_units): 定义了神经网络中隐藏层的单元数。在这个例子中,有两个隐藏层,每层都有32个单元。这些单元负责学习数据中的复杂特性。
  • 学习率 (learning_rate): 这是优化算法在每一步更新模型权重时使用的比例因子。这里设置为0.01,这是一个常用的起始学习率,用于控制模型训练过程中参数更新的速率。
  • dropout率 (dropout_rate): Dropout是一种正则化技术,用于防止神经网络过拟合。这里设置为0.5,意味着在训练过程中,每层的每个神经元有50%的概率被随机抛弃(即临时从网络中移除),这有助于网络学习更加鲁棒的特性。
  • 训练周期数 (num_epochs): 表示整个数据集将被用于训练模型的次数。这里设置为300,意味着每个样本将被用于训练模型300次。较高的周期数可以提高模型的训练质量,但也可能导致过拟合。
  • 批量巨细 (batch_size): 指定了每次训练迭代中使用的样本数量。这里设置为256,意味着每次迭代将随机选择256个样本来计算损失函数并更新模型权重。批量巨细会影响模型的收敛速率和训练稳固性。
这些超参数的选择对模型的性能和训练过程有重要影响。通常,这些值必要根据具体问题和数据集进行调整,以获得最佳的模型表现。
  1. hidden_units = [32, 32]
  2. learning_rate = 0.01
  3. dropout_rate = 0.5
  4. num_epochs = 300
  5. batch_size = 256
复制代码
2.4.2 编译和训练输入模型

定义了一个名为 run_experiment 的函数,用于编译和训练给定的图神经网络模型,并返回训练过程中的历史纪录。

  • 编译模型:使用model.compile方法设置模型的训练参数。
    - optimizer: 指定优化器为keras.optimizers.Adam,这是一个基于Adam算法的优化器,通常用于训练深度学习模型。
    - learning_rate: 传递前面定义的学习率,优化器将使用这个速率来调整模型的权重。
    - loss: 指定损失函数为keras.losses.SparseCategoricalCrossentropy,这是一个实用于多分类问题的损失函数,from_logits=True表示模型输出未颠末softmax转换的原始分数。
    - metrics: 指定训练过程中要监控的指标,这里使用了keras.metrics.SparseCategoricalAccuracy,它计算模型预测的准确率。
  • 创建早停法回调:使用keras.callbacks.EarlyStopping定义一个早停回调,以避免过拟合。
    - monitor="val_acc": 指定监控的指标为验证集上的准确率val_acc。
    - patience=50: 如果验证集上的准确率在50个周期内没有改善,则停止训练。
    - restore_best_weights=True: 在训练竣事时,恢复在验证集上获得最佳准确率时的模型权重。
  • 拟合模型:使用model.fit方法训练模型。
    - x=x_train和y=y_train: 分别指定训练数据的特性和标签。
    - epochs=num_epochs: 设置训练周期数。
    - batch_size=batch_size: 设置每个训练周期中使用的批量巨细。
    - validation_split=0.15: 指定从训练数据中划分出15%作为验证集。
    - callbacks=[early_stopping]: 指定在训练过程中使用的回调函数列表,这里使用了早停法回调。
  • 返回训练历史:训练完成后,函数返回包罗训练和验证过程中损失和准确率信息的历史纪录对象history。
这个函数封装了模型训练的整个过程,使得可以方便地对差别的模型设置进行实验,并通过早停法来优化训练过程。通太过析返回的history对象,可以了解模型在训练过程中的性能变革。
  1. def run_experiment(model, x_train, y_train):
  2.     # Compile the model.
  3.     model.compile(
  4.         optimizer=keras.optimizers.Adam(learning_rate),
  5.         loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
  6.         metrics=[keras.metrics.SparseCategoricalAccuracy(name="acc")],
  7.     )
  8.     # Create an early stopping callback.
  9.     early_stopping = keras.callbacks.EarlyStopping(
  10.         monitor="val_acc", patience=50, restore_best_weights=True
  11.     )
  12.     # Fit the model.
  13.     history = model.fit(
  14.         x=x_train,
  15.         y=y_train,
  16.         epochs=num_epochs,
  17.         batch_size=batch_size,
  18.         validation_split=0.15,
  19.         callbacks=[early_stopping],
  20.     )
  21.     return history
复制代码
定义了一个名为 display_learning_curves 的函数,它的作用是将模型训练过程中的损失和准确率绘制成图表,以便可视化训练进度和性能。

  • 创建图形和轴:使用 plt.subplots 创建一个图形 (fig) 和两个子图轴 (ax1, ax2)。这个图形有1行2列,每个子图的巨细为宽15英寸、高5英寸。
  • 绘制损失曲线
    - 在 ax1 上绘制训练损失 ("loss") 和验证损失 ("val_loss")。
    - 使用 ax1.plot 分别绘制训练和验证的损失历史。
  • 设置图例和标签
    - 使用 ax1.legend 添加图例,标明哪些线代表训练损失,哪些代表验证损失。
    - 使用 ax1.set_xlabel 和 ax1.set_ylabel 分别设置x轴和y轴的标签。
  • 绘制准确率曲线
    - 同样,在 ax2 上绘制训练准确率 ("acc") 和验证准确率 ("val_acc")。
    - 使用 ax2.plot 分别绘制训练和验证的准确率历史。
  • 设置准确率曲线的图例和标签:与损失曲线类似,设置图例和轴标签。
  • 显示图形:最后,使用 plt.show() 显示整个图形。
通过这个函数,可以直观地观察模型在各个周期(epochs)上的训练损失和准确率,以及它们在验证集上的表现。这有助于分析模型的训练动态,比如是否存在过拟合(训练损失持续下降而验证损失上升)或欠拟合(训练和验证损失都较高且没有明显下降)。同时,准确率曲线可以资助我们了解模型在训练和验证集上的预测性能。
  1. def display_learning_curves(history):
  2.     fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
  3.     ax1.plot(history.history["loss"])
  4.     ax1.plot(history.history["val_loss"])
  5.     ax1.legend(["train", "test"], loc="upper right")
  6.     ax1.set_xlabel("Epochs")
  7.     ax1.set_ylabel("Loss")
  8.     ax2.plot(history.history["acc"])
  9.     ax2.plot(history.history["val_acc"])
  10.     ax2.legend(["train", "test"], loc="upper right")
  11.     ax2.set_xlabel("Epochs")
  12.     ax2.set_ylabel("Accuracy")
  13.     plt.show()
复制代码
2.5 定义前馈神经网络(FFN)模块

定义了一个函数 create_ffn,用于创建一个前馈神经网络(Feed-Forward Neural Network,FFN)模块。这个模块是一个序列化的模型,可以作为图神经网络中的一部分,用于节点特性的转换和处置惩罚。

  • 初始化层列表:起首,创建一个空列表 fnn_layers,用于存储FFN中的各个层。
  • 循环添加层:使用 for 循环遍历 hidden_units 列表,这个列表包罗了FFN中每层的单元数(神经元数)。对于 hidden_units 中的每个单元数 units:
    - 添加一个批量归一化层(layers.BatchNormalization()),这有助于加速训练过程并提高模型稳固性。
    - 添加一个 dropout 层(layers.Dropout(dropout_rate)),这个层在训练时随机抛弃一些神经元的输出,以防止过拟合。
    - 添加一个全连接层(layers.Dense(units, activation=tf.nn.gelu)),此中 units 是该层的单元数,tf.nn.gelu 是激活函数,GELU(Gaussian Error Linear Unit)是一种常用的激活函数,它可以提供非线性特性。
  • 创建序列模型:使用 keras.Sequential 将列表 fnn_layers 中的全部层按次序组合成一个序列化模型。这个模型可以吸收输入数据,然后逐层传递,最终输出转换后的特性。
  • 设置模型名称:如果提供了 name 参数,将其用作序列模型的名称。
  • 返回模型:最后,函数返回创建的FFN模型。
这个 create_ffn 函数提供了一个灵活的方式来构建FFN模块,可以根据差别的需求调整隐藏单元的数量和dropout率,以适应差别的图神经网络架构。通过这种方式,可以轻松地在GNN模型中实现复杂的特性转换和处置惩罚逻辑。
  1. def create_ffn(hidden_units, dropout_rate, name=None):
  2.     fnn_layers = []
  3.     for units in hidden_units:
  4.         fnn_layers.append(layers.BatchNormalization())
  5.         fnn_layers.append(layers.Dropout(dropout_rate))
  6.         fnn_layers.append(layers.Dense(units, activation=tf.nn.gelu))
  7.     return keras.Sequential(fnn_layers, name=name)
复制代码
2.6 构建基准神经网络模型

代码是数据预处置惩罚的一部分,主要负责从Cora数据集中提取特性和目标,并将它们转换为适合模型训练和测试的格式。

  • 特性名称提取:feature_names = list(set(papers.columns) - {"paper_id", "subject"}): 这行代码起首从papers DataFrame中获取全部列名,然后使用聚集操作去除"paper_id"和"subject"这两列,由于它们不是特性列。效果是一个包罗全部特性列名的列表feature_names。
  • 计算特性数量:num_features = len(feature_names): 通过获取feature_names列表的长度,计算出数据集中特性的总数。
  • 计算类别数量:num_classes = len(class_idx): 通过获取前面创建的class_idx字典的长度,计算出数据集中类别的总数。这个字典将每个主题映射到一个唯一的索引。
  • 创建训练和测试特性数组
    - x_train = train_data[feature_names].to_numpy(): 从train_data DataFrame中选取feature_names列表包罗的列,然后将这些特性转换为NumPy数组x_train,用于模型训练。
    - x_test = test_data[feature_names].to_numpy(): 同上,从test_data DataFrame中选取特性列并转换为NumPy数组x_test,用于模型测试。
  • 创建训练和测试目标数组
    - y_train = train_data["subject"]: 从train_data DataFrame中选取"subject"列,这是训练数据的目标(即论文的主题),并将其用作模型训练的目标数组。
    - y_test = test_data["subject"]: 同上,从test_data DataFrame中选取"subject"列,作为模型测试的目标数组。
通过这些步调,代码准备好了用于训练和测试图神经网络模型的数据。x_train和x_test包罗了输入特性,而y_train和y_test包罗了对应的目标标签。这种数据格式是大多数机器学习模型训练所必须的。
  1. feature_names = list(set(papers.columns) - {"paper_id", "subject"})
  2. num_features = len(feature_names)
  3. num_classes = len(class_idx)
  4. # Create train and test features as a numpy array.
  5. x_train = train_data[feature_names].to_numpy()
  6. x_test = test_data[feature_names].to_numpy()
  7. # Create train and test targets as a numpy array.
  8. y_train = train_data["subject"]
  9. y_test = test_data["subject"]
复制代码
2.6.1 定义基准分类器

我们添加了五个带有跳跃连接的前馈网络(FFN)块,以便生成一个基准模型,该模型大抵具有与稍后要构建的图神经网络(GNN)模型雷同数量的参数。
定义了一个函数 create_baseline_model 用于创建一个基线模型,这个模型是一个多层前馈神经网络(FFN),用于作为图神经网络(GNN)模型的对比基准。然后,代码实例化了这个基线模型并打印了其结构摘要。以下是详细步调:

  • 定义基线模型函数:吸收参数:隐藏层单元数 hidden_units,类别数 num_classes,以及可选的dropout率 dropout_rate。
  • 创建输入层:使用 layers.Input 定义模型的输入层,其形状由 num_features 决定。
  • 创建第一个FFN块:调用之前定义的 create_ffn 函数创建第一个FFN块,命名为 ffn_block1。
  • 循环创建额外的FFN块和跳跃连接
    - 使用 for 循环创建额外的4个FFN块,每个块后面跟一个跳跃连接(skip connection)。
    - 每个FFN块通过 create_ffn 函数创建,并以 f"ffn_block{block_idx + 2}" 命名。
    - 使用 layers.Add 创建跳跃连接,将前一个FFN块的输出与当前FFN块的输出相加。
  • 计算 logits:使用 layers.Dense 创建一个全连接层,其单元数等于类别数 num_classes,用于计算每个类别的原始分数(logits)。
  • 创建Keras模型:将输入层和最后的logits层包装成一个 keras.Model 对象,命名为 “baseline”。
  • 实例化基线模型:调用 create_baseline_model 函数并传入相应的参数来创建基线模型实例。
  • 打印模型摘要:使用 baseline_model.summary() 打印模型的层级结构和参数数量。
这个基线模型通过堆叠多个FFN块并使用跳跃连接来学习数据的表示,它将作为后续图神经网络模型性能的对比基准。通过比较基线模型和GNN模型的性能,可以评估GNN模型使用图结构信息的本事。
  1. def create_baseline_model(hidden_units, num_classes, dropout_rate=0.2):
  2.     inputs = layers.Input(shape=(num_features,), name="input_features")
  3.     x = create_ffn(hidden_units, dropout_rate, name=f"ffn_block1")(inputs)
  4.     for block_idx in range(4):
  5.         # Create an FFN block.
  6.         x1 = create_ffn(hidden_units, dropout_rate, name=f"ffn_block{block_idx + 2}")(x)
  7.         # Add skip connection.
  8.         x = layers.Add(name=f"skip_connection{block_idx + 2}")([x, x1])
  9.     # Compute logits.
  10.     logits = layers.Dense(num_classes, name="logits")(x)
  11.     # Create the model.
  12.     return keras.Model(inputs=inputs, outputs=logits, name="baseline")
  13. baseline_model = create_baseline_model(hidden_units, num_classes, dropout_rate)
  14. baseline_model.summary()
复制代码
2.6.2 训练基准分类器

代码执行了基线模型的训练实验,并纪录了训练过程中的详细统计信息。

  • 调用训练函数:使用run_experiment函数来训练baseline_model,这是之前定义的基线模型。
  • 训练数据:将训练特性x_train和训练目标y_train作为参数传递给run_experiment函数。这些数据是从Cora数据集的论文特性和主题标签中提取并预处置惩罚得到的。
  • 编译模型:在run_experiment函数内部,模型起首被编译,设置好优化器(使用定义的学习率)、损失函数(希罕分类交织熵),以及评估指标(希罕分类准确率)。
  • 设置早停法:创建一个早停回调,用于监控验证集上的准确率。如果在50个周期内验证准确率没有提升,则停止训练过程,并恢复到最佳权重状态。
  • 训练模型:模型使用指定的周期数、批量巨细和验证分割比例进行训练。训练过程中会应用早停法和其他回调函数。
  • 纪录训练历史:训练过程竣事后,run_experiment函数返回一个history对象,此中包罗了训练和验证过程中的损失值和准确率等信息。
  • 输出效果:通过打印history对象,可以查看模型在每个周期竣事时的损失和准确率,以及早停法是否触发和在哪个周期触发的。
这个步调是机器学习流程中模型训练的关键环节,它为后续的模型评估和调优提供了必要的信息和数据。通太过析history对象,可以对模型的训练效果有一个直观的了解。
  1. history = run_experiment(baseline_model, x_train, y_train)
  2. display_learning_curves(history)
复制代码

2.6.3 评估基准模型

现在我们使用测试数据拆分来评估基准模型。

  • 模型评估:使用baseline_model.evaluate方法对模型进行评估。这个方法将返回模型在指定数据上的损失值和评估指标值。
  • 测试数据:传入测试集的特性x_test和目标y_test作为评估的输入数据。
  • 设置参数:verbose=0:设置为0表示在评估过程中不打印进度信息,使输出更加轻便。
  • 获取测试准确率:baseline_model.evaluate方法返回两个值,第一个是损失值,第二个是准确率。这里使用下划线_来忽略损失值,只获取准确率。
  • 计算百分比:将准确率乘以100,将其转换为百分比形式,以便于直观理解模型的性能。
  • 四舍五入:使用round函数将准确率的百分比值四舍五入到小数点后两位,以提供更准确的展示。
  • 打印效果:最后,使用print函数输出测试准确率的最终效果,并格式化为百分比形式。
通过这段代码,我们可以得到基线模型在未见过的数据上的表现,这是一个重要的指标,用来评估模型的泛化本事。如果准确率较高,说明模型在新数据上也能做出准确的预测;如果准确率较低,则可能必要进一步调整模型或使用更多的训练数据。
  1. test_accuracy = baseline_model.evaluate(x=x_test, y=y_test, verbose=0)
  2. print(f"Test accuracy: {round(test_accuracy * 100, 2)}%")
复制代码
2.7 查抄基准模型的预测本事

我们通过随机生成关于单词出现概率的二进制词向量来创建新的数据实例。
2.7.1 定义函数

定义了两个函数,用于生成随机实例并展示模型对这些实例的分类概率预测。

  • generate_random_instances 函数
    - 作用:生成指定数量的随机二进制实例,这些实例可以代表论文的单词出现概率。
    - 参数 num_instances:必要生成的实例数量。
    - token_probability:使用训练集特性x_train的每列(即每个单词)的均匀值来计算单词出现的概率。
    - 循环:对于每个要生成的实例,使用np.random.uniform生成一个与token_probability长度雷同的随机数组,然后通过比较这些随机数与相应单词出现概率来创建一个二进制实例。如果随机数小于或等于单词出现概率,则该单词在实例中的值为1,否则为0。
    - 返回值:将全部生成的实例作为一个NumPy数组返回。
  • display_class_probabilities 函数
    - 作用:展示模型对每个实例的分类概率预测。
    - 参数 probabilities:模型预测的每个实例的分类概率数组。
    - 循环:遍历每个实例及其预测概率,class_idx 用于遍历每个类别的概率。
    - 输出:打印每个实例的索引和每个类别的预测概率(转换为百分比并四舍五入到小数点后两位)。
这两个函数通常用于模型的解释性分析,资助理解模型怎样对输入实例进行分类预测。通过生成随机实例,可以模拟模型在面对未知或随机数据时的举动。展示分类概率可以资助分析模型预测的置信度和多样性。
  1. def generate_random_instances(num_instances):
  2.     token_probability = x_train.mean(axis=0)
  3.     instances = []
  4.     for _ in range(num_instances):
  5.         probabilities = np.random.uniform(size=len(token_probability))
  6.         instance = (probabilities <= token_probability).astype(int)
  7.         instances.append(instance)
  8.     return np.array(instances)
  9. def display_class_probabilities(probabilities):
  10.     for instance_idx, probs in enumerate(probabilities):
  11.         print(f"Instance {instance_idx + 1}:")
  12.         for class_idx, prob in enumerate(probs):
  13.             print(f"- {class_values[class_idx]}: {round(prob * 100, 2)}%")
复制代码
2.7.2 实行预测

调用2.7.1 定义的函数和模型来生成随机实例,获取模型对这些实例的预测,并展示预测效果的分类概率。以下是详细步调:

  • 生成随机实例: 调用generate_random_instances函数,并传入num_classes作为参数,以生成与类别数量雷同数量的随机实例。这些实例是根据训练数据中单词出现的均匀概率随机生成的二进制向量。
  • 模型预测:使用baseline_model.predict方法对生成的随机实例进行预测。这将返回模型对于每个实例的原始输出(logits)。
  • 转换为概率: 将模型的原始输出(logits)通过softmax函数转换为概率。这里使用tf.convert_to_tensor将logits转换为Tensor,然后调用keras.activations.softmax来应用softmax函数。最后,使用numpy方法将概率转换为NumPy数组。
  • 展示分类概率:调用display_class_probabilities函数,传入上一步得到的分类概率数组。这个函数将遍历每个实例,并打印出每个实例属于每个类别的概率。
  • 输出效果:对于每个随机生成的实例,将按照类别索引次序打印出每个类别的预测概率,概率值会被四舍五入到小数点后两位,并以百分比的形式展示。
通过这个过程,可以获得模型对于随机数据的响应,这有助于理解模型的泛化本事和预测举动。由于这些实例是随机生成的,它们并不代表实际的论文内容,但可以作为模型输出的一个参考。
  1. new_instances = generate_random_instances(num_classes)
  2. logits = baseline_model.predict(new_instances)
  3. probabilities = keras.activations.softmax(tf.convert_to_tensor(logits)).numpy()
  4. display_class_probabilities(probabilities)
复制代码
  1. Instance 1:
  2. - Case_Based: 13.02%
  3. - Genetic_Algorithms: 6.89%
  4. - Neural_Networks: 23.32%
  5. - Probabilistic_Methods: 47.89%
  6. - Reinforcement_Learning: 2.66%
  7. - Rule_Learning: 1.18%
  8. - Theory: 5.03%
  9. Instance 2:
  10. - Case_Based: 1.64%
  11. - Genetic_Algorithms: 59.74%
  12. - Neural_Networks: 27.13%
  13. - Probabilistic_Methods: 9.02%
  14. - Reinforcement_Learning: 1.05%
  15. - Rule_Learning: 0.12%
  16. - Theory: 1.31%
  17. Instance 3:
  18. - Case_Based: 1.35%
  19. - Genetic_Algorithms: 77.41%
  20. - Neural_Networks: 9.56%
  21. - Probabilistic_Methods: 7.89%
  22. - Reinforcement_Learning: 0.42%
  23. - Rule_Learning: 0.46%
  24. - Theory: 2.92%
  25. Instance 4:
  26. - Case_Based: 0.43%
  27. - Genetic_Algorithms: 3.87%
  28. - Neural_Networks: 92.88%
  29. - Probabilistic_Methods: 0.97%
  30. - Reinforcement_Learning: 0.56%
  31. - Rule_Learning: 0.09%
  32. - Theory: 1.2%
  33. Instance 5:
  34. - Case_Based: 0.11%
  35. - Genetic_Algorithms: 0.17%
  36. - Neural_Networks: 10.26%
  37. - Probabilistic_Methods: 0.5%
  38. - Reinforcement_Learning: 0.35%
  39. - Rule_Learning: 0.63%
  40. - Theory: 87.97%
  41. Instance 6:
  42. - Case_Based: 0.98%
  43. - Genetic_Algorithms: 23.37%
  44. - Neural_Networks: 70.76%
  45. - Probabilistic_Methods: 1.12%
  46. - Reinforcement_Learning: 2.23%
  47. - Rule_Learning: 0.21%
  48. - Theory: 1.33%
  49. Instance 7:
  50. - Case_Based: 0.64%
  51. - Genetic_Algorithms: 2.42%
  52. - Neural_Networks: 27.19%
  53. - Probabilistic_Methods: 14.07%
  54. - Reinforcement_Learning: 1.62%
  55. - Rule_Learning: 9.35%
  56. - Theory: 44.7%
复制代码
2.8 构建图神经网络模型

2.8.1 准备数据

将图数据准备并加载到模型中进行训练是图神经网络(GNN)模型中最具挑衅性的部分,这通常由专业库以差别的方式办理。在这个例子中,我们展示了一个简单的方法来准备和使用图数据,这种方法实用于你的数据集由单个完全可以在内存中加载的图组成的情况。
图数据由graph_info元组表示,它包罗以下三个元素:

  • node_features:这是一个[num_nodes,num_features]的NumPy数组,包罗了节点特性。在这个数据集中,节点是论文,而node_features是每个论文的单词存在二进制向量。
  • edges:这是一个[num_edges, 2]的NumPy数组(注意,这里应该是[num_edges,2],而不是[num_edges, num_edges]),表示节点之间链接的希罕毗邻矩阵。在这个例子中,链接是论文之间的引用。
  • edge_weights(可选):这是一个[num_edges]的NumPy数组,包罗了边的权重,这些权重量化了图中节点之间的关系。在这个例子中,论文引用之间没有权重。
以下代码实现图神经网络模型中图数据的准备工作:

  • 创建边数组:使用citations DataFrame中的"source"和"target"列创建一个希罕毗邻矩阵,表示图中的边。.to_numpy().T将DataFrame转换为NumPy数组,并转置它,以得到形状为[2, num_edges]的数组,此中num_edges是边的数量。
  • 创建边权重数组:使用tf.ones创建一个形状为[num_edges]的数组,并将全部元素初始化为1。这个数组edge_weights表示每条边的权重,在本例中全部边的权重雷同,没有特定的权重信息。
  • 创建节点特性数组:起首对papers DataFrame按照"paper_id"列进行排序,然后选择特性列(feature_names),将这些特性转换为NumPy数组。使用tf.cast将这个NumPy数组转换为tf.dtypes.float32类型的Tensor,以适配TensorFlow模型的输入要求。效果是一个形状为[num_nodes, num_features]的数组node_features,此中num_nodes是节点的数量,num_features是每个节点的特性数量。
  • 创建图信息元组:将node_features、edges和edge_weights打包成一个元组graph_info,这个元组将作为图神经网络模型的输入,提供图的结构和节点特性信息。
  • 打印形状信息:打印出边数组和节点特性数组的形状,以验证它们是否符合预期的尺寸。
通过这些步调,代码准备好了图神经网络模型所需的图数据结构,包罗节点特性、边的连接信息以及边的权重,这些信息将被用于后续的图神经网络模型训练和推理。
  1. # Create an edges array (sparse adjacency matrix) of shape [2, num_edges].
  2. edges = citations[["source", "target"]].to_numpy().T
  3. # Create an edge weights array of ones.
  4. edge_weights = tf.ones(shape=edges.shape[1])
  5. # Create a node features array of shape [num_nodes, num_features].
  6. node_features = tf.cast(
  7.     papers.sort_values("paper_id")[feature_names].to_numpy(), dtype=tf.dtypes.float32
  8. )
  9. # Create graph info tuple with node_features, edges, and edge_weights.
  10. graph_info = (node_features, edges, edge_weights)
  11. print("Edges shape:", edges.shape)
  12. print("Nodes shape:", node_features.shape)
复制代码
2.8.2 实现图卷积层

我们将图卷积模块实现为一个Keras层。我们的GraphConvLayer执行以下步调:
准备:输入节点表示通过全连接网络(FFN)进行处置惩罚以生成消息。你可以通过仅对表示应用线性变换来简化处置惩罚过程。
聚合:使用与edge_weights相关的排列稳定池化操作(如求和、均值和最大值)来聚合每个节点的邻居的消息,为每个节点准备一个单一的聚合消息。比方,可以使用tf.math.unsorted_segment_sum API 来聚合邻居消息。
更新:node_representations和aggregated_messages(两者都是[num_nodes, representation_dim]的形状)被组合并处置惩罚以生成节点表示的新状态(节点嵌入)。如果combination_type是gru,那么node_repesentations和aggregated_messages会被堆叠成一个序列,然后通过一个GRU层进行处置惩罚。否则,node_repesentations和aggregated_messages会被相加或连接,然后通过一个FFN进行处置惩罚。
所实现的技术借鉴了图卷积网络(Graph Convolutional Networks)、GraphSage、图同构网络(Graph Isomorphism Network)、简单图网络(Simple Graph Networks)和门控图序列神经网络(Gated Graph Sequence Neural Networks)的思想。另外两种未涵盖的关键技术是图注意力网络(Graph Attention Networks)和消息传递神经网络(Message Passing Neural Networks)。
图卷积层GraphConvLayer的实现包罗以下几个关键部分:

  • 创建GRU模型:create_gru函数用于创建一个具有门控循环单元(GRU)的模型,这个模型将用于后续的节点更新操作。输入参数hidden_units和dropout_rate分别定义了GRU层的巨细和dropout率。
  • 定义图卷积层: GraphConvLayer类继承自layers.Layer,是一个图卷积操作的实现。
  • 初始化层参数: 在__init__方法中,定义了层的参数,包罗隐藏单元数hidden_units,dropout率dropout_rate,聚合类型aggregation_type,组合类型combination_type,以及是否进行归一化normalize。
  • 准备节点表示:prepare方法使用一个前馈神经网络(通过create_ffn函数创建)来准备节点的消息。
  • 聚合邻居消息:aggregate方法根据节点的索引和邻居的消息来聚合信息,支持求和、均匀和最大值聚合。
  • 更新节点嵌入: update方法根据聚合的消息和节点当前的表示来更新节点的嵌入。如果组合类型是"gru",则使用GRU层;否则,使用前馈网络。
  • 层的前向传播: call方法是层的前向传播实现,它吸收节点表示、边和边权重作为输入,并返回更新后的节点嵌入。
  • 层的调用: 当调用GraphConvLayer实例时,将执行call方法,处置惩罚输入数据以产生节点嵌入。
这个GraphConvLayer的计划结合了前馈网络和GRU单元来更新节点表示,使其可以或许捕捉图中的局部结构和节点间的关系。通过这种方式,图神经网络可以学习到节点的嵌入表示,这些表示可以用于各种下游任务,比方节点分类或图分类。
  1. def create_gru(hidden_units, dropout_rate):
  2.     inputs = keras.layers.Input(shape=(2, hidden_units[0]))
  3.     x = inputs
  4.     for units in hidden_units:
  5.       x = layers.GRU(
  6.           units=units,
  7.           activation="tanh",
  8.           recurrent_activation="sigmoid",
  9.           return_sequences=True,
  10.           dropout=dropout_rate,
  11.           return_state=False,
  12.           recurrent_dropout=dropout_rate,
  13.       )(x)
  14.     return keras.Model(inputs=inputs, outputs=x)
  15. class GraphConvLayer(layers.Layer):
  16.     def __init__(
  17.         self,
  18.         hidden_units,
  19.         dropout_rate=0.2,
  20.         aggregation_type="mean",
  21.         combination_type="concat",
  22.         normalize=False,
  23.         *args,
  24.         **kwargs,
  25.     ):
  26.         super().__init__(*args, **kwargs)
  27.         self.aggregation_type = aggregation_type
  28.         self.combination_type = combination_type
  29.         self.normalize = normalize
  30.         self.ffn_prepare = create_ffn(hidden_units, dropout_rate)
  31.         if self.combination_type == "gru":
  32.             self.update_fn = create_gru(hidden_units, dropout_rate)
  33.     else:
  34.             self.update_fn = create_ffn(hidden_units, dropout_rate)
  35.     def prepare(self, node_repesentations, weights=None):
  36.         # node_repesentations shape is [num_edges, embedding_dim].
  37.         messages = self.ffn_prepare(node_repesentations)
  38.         if weights is not None:
  39.             messages = messages * tf.expand_dims(weights, -1)
  40.         return messages
  41.     def aggregate(self, node_indices, neighbour_messages, node_repesentations):
  42.         # node_indices shape is [num_edges].
  43.         # neighbour_messages shape: [num_edges, representation_dim].
  44.         # node_repesentations shape is [num_nodes, representation_dim]
  45.         num_nodes = node_repesentations.shape[0]
  46.         if self.aggregation_type == "sum":
  47.             aggregated_message = tf.math.unsorted_segment_sum(
  48.                 neighbour_messages, node_indices, num_segments=num_nodes
  49.             )
  50.         elif self.aggregation_type == "mean":
  51.             aggregated_message = tf.math.unsorted_segment_mean(
  52.                 neighbour_messages, node_indices, num_segments=num_nodes
  53.             )
  54.         elif self.aggregation_type == "max":
  55.             aggregated_message = tf.math.unsorted_segment_max(
  56.                 neighbour_messages, node_indices, num_segments=num_nodes
  57.             )
  58.         else:
  59.             raise ValueError(f"Invalid aggregation type: {self.aggregation_type}.")
  60.         return aggregated_message
  61.     def update(self, node_repesentations, aggregated_messages):
  62.         # node_repesentations shape is [num_nodes, representation_dim].
  63.         # aggregated_messages shape is [num_nodes, representation_dim].
  64.         if self.combination_type == "gru":
  65.             # Create a sequence of two elements for the GRU layer.
  66.             h = tf.stack([node_repesentations, aggregated_messages], axis=1)
  67.         elif self.combination_type == "concat":
  68.             # Concatenate the node_repesentations and aggregated_messages.
  69.             h = tf.concat([node_repesentations, aggregated_messages], axis=1)
  70.         elif self.combination_type == "add":
  71.             # Add node_repesentations and aggregated_messages.
  72.             h = node_repesentations + aggregated_messages
  73.         else:
  74.             raise ValueError(f"Invalid combination type: {self.combination_type}.")
  75.         # Apply the processing function.
  76.         node_embeddings = self.update_fn(h)
  77.         if self.combination_type == "gru":
  78.             node_embeddings = tf.unstack(node_embeddings, axis=1)[-1]
  79.         if self.normalize:
  80.             node_embeddings = tf.nn.l2_normalize(node_embeddings, axis=-1)
  81.         return node_embeddings
  82.     def call(self, inputs):
  83.         """Process the inputs to produce the node_embeddings.
  84.         inputs: a tuple of three elements: node_repesentations, edges, edge_weights.
  85.         Returns: node_embeddings of shape [num_nodes, representation_dim].
  86.         """
  87.         node_repesentations, edges, edge_weights = inputs
  88.         # Get node_indices (source) and neighbour_indices (target) from edges.
  89.         node_indices, neighbour_indices = edges[0], edges[1]
  90.         # neighbour_repesentations shape is [num_edges, representation_dim].
  91.         neighbour_repesentations = tf.gather(node_repesentations, neighbour_indices)
  92.         # Prepare the messages of the neighbours.
  93.         neighbour_messages = self.prepare(neighbour_repesentations, edge_weights)
  94.         # Aggregate the neighbour messages.
  95.         aggregated_messages = self.aggregate(
  96.             node_indices, neighbour_messages, node_repesentations
  97.         )
  98.         # Update the node embedding with the neighbour messages.
  99.         return self.update(node_repesentations, aggregated_messages)
复制代码
2.8.3 实现图神经网络节点分类器

图神经网络(GNN)分类模型遵循图神经网络计划空间的方法,如下所述:

  • 使用全连接网络(FFN)对节点特性进行预处置惩罚,以生成初始节点表示。
  • 将一个或多个图卷积层(带有跳跃连接)应用于节点表示,以生成节点嵌入。
  • 使用全连接网络(FFN)对节点嵌入进行后处置惩罚,以生成最终的节点嵌入。
  • 将节点嵌入输入到Softmax层中以预测节点类别。
每个添加的图卷积层都会捕获来自更高级别邻居的信息。然而,添加过多的图卷积层可能会导致过平滑,即模型为全部节点生成相似的嵌入。
请注意,传递给Keras模型构造函数的graph_info,并作为Keras模型对象的属性使用,而不是训练或预测的输入数据。模型将接受一批节点索引(node_indices),这些索引用于从graph_info中查找节点特性和邻居。
起首义一个名为 GNNNodeClassifier 的类,它是一个基于图神经网络(GNN)的节点分类器,使用 TensorFlow 框架。

  • class GNNNodeClassifier(tf.keras.Model): 定义了一个继承自 tf.keras.Model 的类,这意味着它是一个模型类,可以用于构建和训练深度学习模型。
  • __init__ 是类的构造函数,用于初始化模型的参数:
    - graph_info: 包罗图信息的元组,包罗节点特性、边和边权重。
    - num_classes: 类别数,用于最终分类的输出。
    - hidden_units: 隐藏层单元数,用于定义模型中隐藏层的巨细。
    - aggregation_type: 聚合类型,定义了怎样聚合邻居节点的特性。
    - combination_type: 组合类型,定义了怎样将节点自身特性和邻居特性组合。
    - dropout_rate: 抛弃率,用于正则化以防止过拟合。
    - normalize: 是否对边权重进行归一化处置惩罚。
  • super().__init__(*args, **kwargs) 调用父类的构造函数。
  • node_features, edges, edge_weights = graph_info 从 graph_info 中解包出节点特性、边和边权重。
  • 如果 edge_weights 未提供,则将其设置为与边数量雷同的1的数组。
  • 将 edge_weights 归一化,使得全部边权重的和为1。
  • self.preprocess 创建一个前处置惩罚层,用于对节点特性进行预处置惩罚。
  • self.conv1 和 self.conv2 分别创建了两个图卷积层,这些层是 GNN 的核心,用于学习节点的表示。
  • self.postprocess 创建一个后处置惩罚层,用于对颠末图卷积层后的节点特性进行进一步处置惩罚。
  • self.compute_logits 是一个密集层,用于将节点的嵌入转换为类别的对数几率(logits)。
  • def call(self, input_node_indices): 定义了模型的调用函数,它接受输入节点的索引,并返回这些节点的分类效果:
    - x = self.preprocess(self.node_features) 对节点特性进行预处置惩罚。
    - x1 = self.conv1(...) 应用第一个图卷积层,并使用残差连接。
    - x2 = self.conv2(...) 应用第二个图卷积层,并使用残差连接。
    - x = self.postprocess(x) 对节点嵌入进行后处置惩罚。
    - node_embeddings = tf.gather(x, input_node_indices) 根据输入的节点索引提取对应的节点嵌入。
    - return self.compute_logits(node_embeddings) 计算并返回分类的 logits。
这个模型通过图卷积层来学习节点的表示,并通过残差连接来资助梯度流动,防止深层网络中的梯度消散问题。最终,模型通过一个密集层来输出每个节点属于各个类别的对数几率。
  1. class GNNNodeClassifier(tf.keras.Model):
  2.     def __init__(
  3.         self,
  4.         graph_info,
  5.         num_classes,
  6.         hidden_units,
  7.         aggregation_type="sum",
  8.         combination_type="concat",
  9.         dropout_rate=0.2,
  10.         normalize=True,
  11.         *args,
  12.         **kwargs,
  13.     ):
  14.         super().__init__(*args, **kwargs)
  15.         # Unpack graph_info to three elements: node_features, edges, and edge_weight.
  16.         node_features, edges, edge_weights = graph_info
  17.         self.node_features = node_features
  18.         self.edges = edges
  19.         self.edge_weights = edge_weights
  20.         # Set edge_weights to ones if not provided.
  21.         if self.edge_weights is None:
  22.             self.edge_weights = tf.ones(shape=edges.shape[1])
  23.         # Scale edge_weights to sum to 1.
  24.         self.edge_weights = self.edge_weights / tf.math.reduce_sum(self.edge_weights)
  25.         # Create a process layer.
  26.         self.preprocess = create_ffn(hidden_units, dropout_rate, name="preprocess")
  27.         # Create the first GraphConv layer.
  28.         self.conv1 = GraphConvLayer(
  29.             hidden_units,
  30.             dropout_rate,
  31.             aggregation_type,
  32.             combination_type,
  33.             normalize,
  34.             name="graph_conv1",
  35.         )
  36.         # Create the second GraphConv layer.
  37.         self.conv2 = GraphConvLayer(
  38.             hidden_units,
  39.             dropout_rate,
  40.             aggregation_type,
  41.             combination_type,
  42.             normalize,
  43.             name="graph_conv2",
  44.         )
  45.         # Create a postprocess layer.
  46.         self.postprocess = create_ffn(hidden_units, dropout_rate, name="postprocess")
  47.         # Create a compute logits layer.
  48.         self.compute_logits = layers.Dense(units=num_classes, name="logits")
  49.     def call(self, input_node_indices):
  50.         # Preprocess the node_features to produce node representations.
  51.         x = self.preprocess(self.node_features)
  52.         # Apply the first graph conv layer.
  53.         x1 = self.conv1((x, self.edges, self.edge_weights))
  54.         # Skip connection.
  55.         x = x1 + x
  56.         # Apply the second graph conv layer.
  57.         x2 = self.conv2((x, self.edges, self.edge_weights))
  58.         # Skip connection.
  59.         x = x2 + x
  60.         # Postprocess node embedding.
  61.         x = self.postprocess(x)
  62.         # Fetch node embeddings for the input node_indices.
  63.         node_embeddings = tf.gather(x, input_node_indices)
  64.         # Compute logits
  65.         return self.compute_logits(node_embeddings)
复制代码
2.8.4 测试GNN

测试一下GNN(图神经网络)模型的实例化和调用。请注意,如果你提供了N个节点索引作为输入,那么输出将是一个形状为[N, num_classes]的张量,这与图的巨细无关。
起首创建了一个 GNNNodeClassifier 类的实例,然后打印了模型的输出形状,并显示了模型的摘要。

  • gnn_model = GNNNodeClassifier(...) 创建了 GNNNodeClassifier 的实例,此中:
    - graph_info 是一个包罗图信息的元组,它包罗节点特性、边和边权重。
    - num_classes 是分类任务中的类别数。
    - hidden_units 是模型中隐藏层的巨细。
    - dropout_rate 是模型中使用的抛弃率。
    - name="gnn_model" 给模型实例指定了一个名称。
  • print("GNN output shape:", gnn_model([1, 10, 100])) 打印了模型对输入节点索引 [1, 10, 100] 的输出形状。这里的 [1, 10, 100] 表示我们想要模型预测这三个节点的类别。由于 num_classes 定义了类别的数量,输出形状将是 (3, num_classes),此中3是输入节点索引的数量。
  • gnn_model.summary() 打印了模型的摘要,这包罗了模型的层、每层的参数数量、激活函数等信息。这有助于理解模型的结构和复杂性。
  1. gnn_model = GNNNodeClassifier(
  2.     graph_info=graph_info,
  3.     num_classes=num_classes,
  4.     hidden_units=hidden_units,
  5.     dropout_rate=dropout_rate,
  6.     name="gnn_model",
  7. )
  8. print("GNN output shape:", gnn_model([1, 10, 100]))
  9. gnn_model.summary()
复制代码
2.8.5 训练GNN模型

训练GNN模型时,我们使用了标准的监督交织熵损失来训练模型。但是,我们可以为生成的节点嵌入添加一个自监督损失项,以确保图中相邻的节点具有相似的表示,而远离的节点具有不相似的表示。
调用定义的 GNNNodeClassifier 模型来进行训练,并纪录训练过程:

  • x_train = train_data.paper_id.to_numpy():这行代码将训练数据集中的 paper_id 列转换为 NumPy 数组。train_data 可能是一个 pandas DataFrame 或类似的数据结构,此中包罗了训练数据。paper_id 列可能包罗了论文的唯一标识符,这些标识符将作为模型的输入。
  • history = run_experiment(gnn_model, x_train, y_train):这行代码调用了一个名为 run_experiment 的函数,该函数吸收三个参数:
    - gnn_model:之前创建的 GNNNodeClassifier 模型实例。
    - x_train:训练数据的特性输入,这里是转换为 NumPy 数组的 paper_id。
    - y_train:训练数据的目标输出,这是模型必要预测的值,可能是论文的类别标签。
run_experiment 函数执行以下操作:
- 使用 x_train 和 y_train 来训练 gnn_model。
- 纪录训练过程中的关键指标,如损失值和准确率,并将这些信息存储在 history 对象中。
- 可能还包罗验证过程,使用验证集来评估模型在未见过的数据上的表现。
history 对象通常包罗了模型训练过程中的详细统计信息,可以用于后续的分析,比如绘制训练和验证损失曲线,评估模型性能等。
  1. x_train = train_data.paper_id.to_numpy()
  2. history = run_experiment(gnn_model, x_train, y_train)
  3. display_learning_curves(history)
复制代码

在测试数据集上评估GNN模型。效果可能会因训练样本的差别而有所变革,但GNN模型在测试准确率方面总是优于基线模型。
  1. x_test = test_data.paper_id.to_numpy()
  2. _, test_accuracy = gnn_model.evaluate(x=x_test, y=y_test, verbose=0)
  3. print(f"Test accuracy: {round(test_accuracy * 100, 2)}%")
复制代码
  1. Test accuracy: 80.19%
复制代码
2.9 查抄GNN模型的预测本事

让我们将新实例作为节点添加到node_features中,并为它们生成与现有节点之间的链接(引用)。
2.9.1 添加节点


  • 添加新节点:起首,代码通过将 new_instances 特性追加到现有的 node_features 数组中,来将新的实例(比方新论文)作为节点添加到图中。这增加了图中的节点数量。
  • 生成新节点索引:接着,代码生成了一组新的节点索引 new_node_indices,这些索引将用于引用新添加的节点。索引的数量由 num_classes 决定,这里假设 num_classes 与新实例的数量相称。
  • 创建新边:然后,代码通过遍历按主题分组的论文聚集,为每个新节点创建引用边。对于每个主题:

    • 选择当前主题下的5篇论文作为被引用的节点。
    • 选择恣意主题下的2篇论文作为被引用的节点。
    • 将这两组被引用的节点索引合并。
    • 对于每个新节点(即新论文),创建指向这些被引用节点的边。

  • 更新边数组:最后,将新创建的边作为数组 new_citations 存储,并将其转置以匹配边的格式。然后,将这些新边追加到现有的边数组 edges 中,从而扩展了图的边矩阵。
这个过程模拟了新论文的引用举动,通过将新论文作为节点添加到图中,并将它们与现有论文通过边连接起来,来更新图的结构。这种更新对于图神经网络模型来说是重要的,由于它允许模型学习新论文的特性,并根据其与现有论文的关系进行分类。
  1. # First we add the N new_instances as nodes to the graph
  2. # by appending the new_instance to node_features.
  3. num_nodes = node_features.shape[0]
  4. new_node_features = np.concatenate([node_features, new_instances])
  5. # Second we add the M edges (citations) from each new node to a set
  6. # of existing nodes in a particular subject
  7. new_node_indices = [i + num_nodes for i in range(num_classes)]
  8. new_citations = []
  9. for subject_idx, group in papers.groupby("subject"):
  10.     subject_papers = list(group.paper_id)
  11.     # Select random x papers specific subject.
  12.     selected_paper_indices1 = np.random.choice(subject_papers, 5)
  13.     # Select random y papers from any subject (where y < x).
  14.     selected_paper_indices2 = np.random.choice(list(papers.paper_id), 2)
  15.     # Merge the selected paper indices.
  16.     selected_paper_indices = np.concatenate(
  17.         [selected_paper_indices1, selected_paper_indices2], axis=0
  18.     )
  19.     # Create edges between a citing paper idx and the selected cited papers.
  20.     citing_paper_indx = new_node_indices[subject_idx]
  21.     for cited_paper_idx in selected_paper_indices:
  22.         new_citations.append([citing_paper_indx, cited_paper_idx])
  23. new_citations = np.array(new_citations).T
  24. new_edges = np.concatenate([edges, new_citations], axis=1)
复制代码
2.9.2 更新节点特性

现在让我们更新GNN模型中的node_features和边。
更新图神经网络模型 gnn_model 的内部状态,并使用更新后的模型来预测新节点的类别概率:

  • 打印原始的 node_features 和 edges 的形状:
    - gnn_model.node_features 包罗了图的节点特性。
    - gnn_model.edges 包罗了图的边信息。
  • 更新 gnn_model 的属性:
    - gnn_model.node_features = new_node_features:将之前通过添加新实例得到的新节点特性赋值给模型。
    - gnn_model.edges = new_edges:将之前通过添加新边得到的新边信息赋值给模型。
    - gnn_model.edge_weights = tf.ones(shape=new_edges.shape[1]):为新边创建权重,这里简单地为每个边分配权重1,这可能必要根据实际情况进行调整。
  • 打印更新后的 node_features 和 edges 的形状,以确认更新是否乐成。
  • 使用模型预测新节点的类别:logits = gnn_model.predict(tf.convert_to_tensor(new_node_indices)):调用模型的 predict 方法来获取新节点的 logits。new_node_indices 是新节点的索引数组。
  • 将 logits 转换为概率: probabilities = keras.activations.softmax(tf.convert_to_tensor(logits)).numpy():使用 softmax 函数将 logits 转换为概率分布。这里起首将 logits 转换为 TensorFlow 张量,然后应用 softmax 函数,最后将效果转换回 NumPy 数组。
  • 显示新节点的类别概率: display_class_probabilities(probabilities):这是一个假设存在的函数,用于显示或打印新节点的类别概率。这个函数的具体实现没有给出,但我们可以假设它以某种方式格式化并输出概率信息。
  1. print("Original node_features shape:", gnn_model.node_features.shape)
  2. print("Original edges shape:", gnn_model.edges.shape)
  3. gnn_model.node_features = new_node_features
  4. gnn_model.edges = new_edges
  5. gnn_model.edge_weights = tf.ones(shape=new_edges.shape[1])
  6. print("New node_features shape:", gnn_model.node_features.shape)
  7. print("New edges shape:", gnn_model.edges.shape)
  8. logits = gnn_model.predict(tf.convert_to_tensor(new_node_indices))
  9. probabilities = keras.activations.softmax(tf.convert_to_tensor(logits)).numpy()
  10. display_class_probabilities(probabilities)
复制代码
3. 总结

本文气密深入探索了怎样使用Keras和图神经网络(GNN)库(如Spektral)来构建和训练一个模型,以处置惩罚引文网络数据。这种网络结构的数据常见于学术文献领域,此中节点代表论文,而边则代表论文之间的引用关系。
3.1 数据集与预处置惩罚

起首,我们使用了Cora数据集,这是一个广泛使用的引文网络数据集,包罗了2708篇科学论文,每篇论文都被标记了所属的研究领域。为了将这些数据转化为模型可以处置惩罚的形式,我们进行了数据预处置惩罚。具体来说,我们为每个文档提取了特性向量(如使用TF-IDF或Word2Vec等方法),这些特性向量构成了图中的节点特性。同时,我们构建了毗邻矩阵,此中每个元素表示两个文档之间是否存在引用关系,从而形成了图的边。
3.2 模型构建

接下来,我们构建了一个图神经网络(GNN)模型来处置惩罚这些数据。模型的核心是图神经网络层(如GraphConv层),这些层可以或许聚合节点的邻居信息。在每一次迭代中,图神经网络层都会将节点的特性与其邻居的特性进行聚合,从而生成新的节点表示。通过堆叠多个图神经网络层,我们可以捕获图中更广泛的上下文信息。
在聚合了足够的节点信息后,我们使用全连接层将节点表示映射到最终的输出类别。具体来说,我们为每个节点分配了一个类别标签(即论文所属的研究领域),并通过全连接层将这些节点表示转换为类别概率分布。
3.3 训练与评估

在训练过程中,我们采用了适当的优化器(如Adam)和损失函数(如交织熵损失)。我们将数据集划分为训练集、验证集和测试集,以便在训练过程中评估模型的性能。在每个训练迭代中,我们使用训练集来更新模型的参数,并使用验证集来评估模型的性能。如果模型在验证集上的性能有所提高,则保存当前的模型参数作为最佳参数。
训练完成后,我们在测试集上评估了模型的性能。我们使用准确率、准确率、召回率和F1分数等指标来全面评估模型的性能。这些指标可以资助我们了解模型在未见过的数据上的表现,并与其他模型进行比较。
3.4 预测

本文详细展示了怎样使用Keras和图神经网络库来处置惩罚引文网络数据。这个例子不仅让我们深入了解了GNN的工作原理和应用场景,还为我们进一步研究和应用GNN提供了宝贵的履历。在未来,我们可以实验使用更复杂的GNN结构、更先辈的优化算法和更丰富的特性表示来提高模型的性能,并探索GNN在其他领域的应用潜力。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

拉不拉稀肚拉稀

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表