使用 HashGNN 进行节点分类

Open In Colab

本 Jupyter Notebook 托管在 Neo4j 图数据科学客户端 Github 仓库中,点击此处查看。

本 Notebook 展示了如何使用 graphdatascience 库来实现:

  • 使用便捷的数据加载器将包含 Movie(电影)、Actor(演员)和 Director(导演)节点的 IMDB 数据集直接导入 GDS

  • 配置一个带有 HashGNN 嵌入的节点分类流水线,用于预测 Movie 节点的流派

  • 对流水线进行自动调优训练并检查结果

  • 对缺少流派信息的电影节点进行预测

1. 前置条件

运行此 Notebook 需要一个安装了最新版本(2.5 或更高版本)Neo4j 图数据科学库 (GDS) 插件的 Neo4j 数据库服务器。我们建议使用带有 GDS 的 Neo4j Desktop 或 AuraDS。

# Install necessary Python dependencies
%pip install "graphdatascience>=1.6"

2. 设置

首先,我们要导入依赖项并建立 GDS 客户端与数据库的连接。

# Import our dependencies
import os

from graphdatascience import GraphDataScience
# Get Neo4j DB URI, credentials and name from environment if applicable
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://:7687")
NEO4J_AUTH = None
NEO4J_DB = os.environ.get("NEO4J_DB", "neo4j")
if os.environ.get("NEO4J_USER") and os.environ.get("NEO4J_PASSWORD"):
    NEO4J_AUTH = (
        os.environ.get("NEO4J_USER"),
        os.environ.get("NEO4J_PASSWORD"),
    )
gds = GraphDataScience(NEO4J_URI, auth=NEO4J_AUTH, database=NEO4J_DB)
from graphdatascience import ServerVersion

assert gds.server_version() >= ServerVersion(2, 5, 0)

3. 加载 IMDB 数据集

接下来,我们使用 graphdatascience内置 IMDB 加载器 将数据导入 GDS 服务器。这将生成一个包含 MovieActorDirector 节点的图,节点间通过 ACTED_INDIRECTED_IN 关系连接。

注意:在“真实场景”中,我们通常会将 Neo4j 数据库中的数据投影到 GDS,或者使用 gds.graph.construct 从客户端数据创建图。

# Run the loading to obtain a `Graph` object representing our GDS projection
G = gds.graph.load_imdb()

让我们检查一下图表内容。

print(f"Overview of G: {G}")
print(f"Node labels in G: {G.node_labels()}")
print(f"Relationship types in G: {G.relationship_types()}")

结果符合预期,不过我们注意到有些节点的标签是 UnclassifiedMovie。实际上,这些正是我们希望通过节点分类模型预测其流派的节点。让我们查看不同节点标签上的节点属性,以便更清晰地了解情况。

print(f"Node properties per label:\n{G.node_properties()}")

可以看到 Movie 节点具有 genre 属性,这意味着我们可以在稍后训练模型时使用这些节点。而 UnclassifiedMovie 节点如预期般没有 genre 属性,这正是我们要预测的目标。

此外,我们注意到所有节点都有一个 plot_keywords 属性。这是一个二元的“词袋”模型特征向量,代表了描述某个节点的 1256 个情节关键词中的哪些项。这些特征向量稍后将用作 HashGNN 节点嵌入算法的输入。

4. 配置训练流水线

现在我们已经加载并理解了要分析的数据,接下来可以探索如何对 UnclassifiedMovie 节点进行上述流派预测的工具了。

由于我们要预测节点的离散值属性,因此将使用 节点分类流水线

# Create an empty node classification pipeline
pipe, _ = gds.beta.pipeline.nodeClassification.create("genre-predictor-pipe")

为了将我们的准确率得分与该数据集上现有的最先进方法进行比较,我们希望使用与图神经网络 (GTN) 论文 (NIPS 论文链接) 中相同大小的测试集。我们将相应地配置流水线。

# Set the test set size to be 79.6 % of the entire set of `Movie` nodes
_ = pipe.configureSplit(testFraction=0.796)

请注意,如果我们使用更标准的训练/测试集划分(例如 80/20),通常会获得更好的模型。这在实际用例中通常是首选方案。

5. HashGNN 节点嵌入算法

作为训练流水线的最后一部分,将涉及一个机器学习训练算法。如果我们直接使用 plot_keywords 作为机器学习算法的特征输入,我们将无法利用图中现有的任何关系数据。由于关系很可能会通过更有价值的信息丰富我们的特征,我们将使用一种考虑关系的节点嵌入算法,并将其输出用作机器学习训练算法的输入。

在此案例中,我们将使用 GDS 2.3 中引入的 HashGNN 节点嵌入算法。与名称所暗示的相反,HashGNN 并非有监督的神经学习模型,而是一种无监督算法。其名称源于该算法的设计灵感来自于图神经网络 (GNN) —— 即在每个节点上进行交替的消息传递与转换。但与大多数 GNN 不同,它的转换不是通过神经变换实现的,而是通过局部敏感最小哈希 (Locality Sensitive Min-Hashing) 完成的。由于所使用的哈希函数是独立于输入数据随机选择的,因此无需进行训练。

我们将 plot_keywords 节点属性作为输入提供给 HashGNN,它将为每个节点输出经过关系消息传递增强后的新特征向量。由于 plot_keywords 向量本身已经是二元的,我们无需进行特征二值化

由于我们有多个节点标签和关系,我们通过设置 heterogeneous=True 来启用 HashGNN 的异构能力。值得注意的是,我们还通过显式指定 contextNodeLabels 来声明我们希望包含所有类型的节点,而不仅仅是我们要训练的 Movie 节点。

请参阅 HashGNN 文档 以了解有关此算法的更多信息。

# Add a HashGNN node property step to the pipeline
_ = pipe.addNodeProperty(
    "hashgnn",
    mutateProperty="embedding",
    iterations=4,
    heterogeneous=True,
    embeddingDensity=512,
    neighborInfluence=0.7,
    featureProperties=["plot_keywords"],
    randomSeed=41,
    contextNodeLabels=G.node_labels(),
)
# Set the embeddings vectors produced by HashGNN as feature input to our ML algorithm
_ = pipe.selectFeatures("embedding")

6. 设置自动调优

现在是为流水线的训练部分设置 机器学习算法 的时候了。

在此示例中,我们将添加逻辑回归和随机森林算法作为最终模型的候选算法。流水线将评估每个候选算法,并根据我们指定的指标选择最佳方案。

很难确定需要多少正则化才能避免模型在训练集上过拟合,因此我们将利用 GDS 的自动调优功能来辅助。自动调优算法将尝试正则化参数 penalty(针对逻辑回归)和 minSplitSize(针对随机森林)的多个值,并选出找到的最佳参数。

请参阅 GDS 手册以了解有关 自动调优逻辑回归随机森林 的更多信息。

# Add logistic regression as a candidate ML algorithm for the training
# Provide an interval for the `penalty` parameter to enable autotuning for it
_ = pipe.addLogisticRegression(penalty=(0.1, 1.0), maxEpochs=1000, patience=5, tolerance=0.0001, learningRate=0.01)
# Add random forest as a candidate ML algorithm for the training
# Provide an interval for the `minSplitSize` parameter to enable autotuning for it
_ = pipe.addRandomForest(minSplitSize=(2, 100), criterion="ENTROPY")

7. 训练流水线

配置已完成,我们现在准备启动流水线训练并查看结果。

在训练调用中,我们提供要训练的节点标签和属性,以及决定如何选择最佳模型候选者的指标。

# Call train on our pipeline object to run the entire training pipeline and produce a model
model, _ = pipe.train(
    G,
    modelName="genre-predictor-model",
    targetNodeLabels=["Movie"],
    targetProperty="genre",
    metrics=["F1_MACRO"],
    randomSeed=42,
)

让我们检查一下由训练流水线生成的模型。

print(f"Accuracy scores of trained model:\n{model.metrics()['F1_MACRO']}")
print(f"Winning ML algorithm candidate config:\n{model.best_parameters()}")

正如我们所见,自动调优找到的最佳机器学习算法配置是 penalty=0.159748 的逻辑回归。

此外,我们注意到测试集的 F1 分数为 0.59118347,这与文献中该数据集上的其他算法得分相比表现非常出色。更多内容请见下文的 结论 部分。

8. 进行新预测

现在,我们可以使用训练流水线生成的模型来预测 UnclassifiedMovie 节点的流派。

# Predict `genre` for `UnclassifiedMovie` nodes and stream the results
predictions = model.predict_stream(G, targetNodeLabels=["UnclassifiedMovie"], includePredictedProbabilities=True)

print(f"First predictions of unclassified movie nodes:\n{predictions.head()}")

在此示例中,我们将预测结果流式传输回客户端应用程序;但在实际操作中,我们也可以通过调用 model.predict_mutate 来修改由 G 表示的 GDS 图。

9. 清理

我们可以选择清理 GDS 状态,为其他任务释放内存。

# Drop the GDS graph represented by `G` from the GDS graph catalog
_ = G.drop()
# Drop the GDS training pipeline represented by `pipe` from the GDS pipeline catalog
_ = pipe.drop()
# Drop the GDS model represented by `model` from the GDS model catalog
_ = model.drop()

10. 结论

仅使用 GDS 库及其客户端,我们就能够利用复杂的 HashGNN 节点嵌入算法和逻辑回归来训练节点分类模型。通过自动调优过程,我们的逻辑回归配置在众多其他算法(如各种配置的随机森林)中被自动选为最佳候选者。我们仅用很少的代码就取得了非常好的分数。

尽管我们使用了 graphdatascience 库的便捷方法将 IMDB 数据集加载到 GDS 中,但很容易将其替换为类似 从 Neo4j 数据库进行投影 的方式,从而创建一个更真实的生产工作流。

10.1. 与其他方法的比较

如前所述,我们尝试模仿 NeurIPS 论文《图 Transformer 网络》(Graph Transformer Networks) 中的基准测试设置,以便与当前最先进的方法进行比较。与该论文的不同之处在于,他们拥有预定义的训练/测试集划分,而我们在训练流水线中随机均匀地生成了一个划分(大小相同)。不过,我们没有理由认为论文中的预定义划分不是随机均匀生成的。此外,他们使用了长度为 64 的浮点嵌入(64 * 32 = 2048 位),而我们使用 HashGNN 进行了 1256 位的嵌入。

他们观察到的分数如下:

算法 测试集 F1 分数 (%)

DeepWalk

32.08

metapath2vec

35.21

GCN

56.89

GAT

58.14

HAN

56.77

GTN

60.92

鉴于此,使用 HashGNN 和逻辑回归获得 59.11% 的测试集 F1 分数确实令人印象深刻。特别是考虑到:- 我们使用更少的位数来表示嵌入(1256 比 2048);- 与上述深度学习模型相比,我们在梯度下降中使用的训练参数显著减少;- HashGNN 是一种无监督算法;- HashGNN 运行速度快得多(即使没有 GPU),且内存占用更少。

10.2. 进一步学习

要深入了解本 Notebook 中涵盖的主题,请查看 GDS 手册的以下页面: