知识图谱与 LLM 的实战应用——知识图谱机器学习导论

0 阅读38分钟

本章涵盖

  • 理解知识图谱上的机器学习
  • 探索图上常见的机器学习任务
  • 理解节点与关系表示的作用

构建知识图谱是开发智能系统中的关键一步。它使我们能够从多个不同的数据源中获取整体性知识,并以一种支持探索、导航以及更高级分析的方式加以表示。到目前为止,我们已经看到如何查询图并提取相关信息,如何沿着节点和关系类型进行导航,甚至如何提取统计信息来验证导入过程,并评估 KG 中所存储知识的“质量”。这些都是构建智能咨询系统(IAS)过程中的重要步骤。

在 IAS 中,“咨询”意味着提供用户无法自行抽取出来的洞见。例如,研究人员如何高效地在一个包含疾病、蛋白质、基因和化合物的大型 KG 中导航,从而识别潜在的药物再利用机会?又或者,临床医生如何把患者症状和 DNA 序列,与文献、临床试验和标准诊疗方案结合起来,以制定个性化治疗计划?类似这样的场景有很多。而其中大多数——如果不是全部的话——都需要使用以图中知识为输入的机器学习(ML)算法。

第 9 到第 12 章将深入探讨 KG 上的机器学习。我们会在之前那本书 [1] 的基础上,扩展新的算法和理论。在过去几年里,这些方法已经得到了改进(尤其是图神经网络,GNN),并且已经在图上进行了测试(例如与 LLM 相关的场景)。我们还会讨论一些新的库,这些库已经足够稳定、性能也足够好,可以在生产环境中被认真考虑。

9.1 为什么要在图上做机器学习?

我们首先要让你清楚理解:为什么在(知识)图上执行 ML 会有价值(无论是否结合 LLM),以及为什么这种方法有时是最优选择,甚至是唯一可行的选择。核心原因可以概括如下:

  • 数据表示 —— 现实世界应用所使用或产生的数据形式多种多样,从矩阵和张量到序列和时间序列 [2]。图提供了一种通用的数据表示方式;来自各种系统的很多类型的数据都可以被转换成图。
  • 问题建模 —— 大量问题都可以归结为图上的少数几类计算任务。例如,检测异常节点和为患者推荐药物,都可以概括为节点分类问题;而做推荐和识别交互,本质上则是关系预测问题。
  • 数据项依赖性 —— 传统 ML 算法假设数据项是独立同分布(i.i.d.)的。但在许多现实世界场景中,数据项之间天然存在连接,忽略这些关系往往会导致结果不完整,甚至错误。将数据以图的形式存储——因为图天然能够存储关系——并在这些图上执行计算任务,往往能得到更好的结果 [3]。

像 GNN 这样的 ML 技术与 LLM 结合,能够通过把 GNN 识别复杂结构模式的能力,与 LLM 的自然语言理解和生成能力结合起来,构建出强大的 IAS。例如,在个性化医疗中,GNN 可以处理患者相似性网络和生物通路图,而 LLM 则可以整合医学文献和临床指南,并用清晰的医学术语解释治疗方案的依据。这种 GNN–LLM 集成 使系统既能识别复杂模式,又能用符合上下文的语言解释这些模式,从而让最终用户能够理解并采取行动。

你总是希望使用最合适的工具,来实现最终目标并解决业务问题。ML 天生就是一个以问题为驱动的学科;因此,对于我们将要讨论的很多场景来说,图上的 ML 将会是你手中最好的一支箭。

9.2 图上的机器学习是什么?

经典 ML 算法可以按照不同方式进行分类。最常见的方法,是依据它们处理的数据类型以及所解决的任务,将其分为监督学习无监督学习

  • 监督算法中,数据是部分带标签的:对于某些数据项,输出是已知的,而目标是预测其余数据的标签。一个典型例子是垃圾邮件过滤器。学习器需要在训练数据集中为每个数据项(邮件)提供标签,例如“垃圾邮件”和“非垃圾邮件”(这些是关键信息),然后它学习如何根据这些标签对邮件进行分类。
  • 无监督算法中,数据是完全无标签的,目标是从中抽取洞见和模式,例如从一组数据点中找出聚类,或者从图中找出社区。

图上的 ML 并没有本质不同,但在这里,“监督/无监督”这样的分类方式并不一定是最有用的 [4],尽管它们仍然有效:例如,节点分类可以被看作一种监督任务(虽然也不完全是),而社区检测则是无监督的。相反,图上的 ML 任务通常被划分为以下两大类(见图 9.1):

  • 以节点为中心的任务(Node-focused tasks) —— 整个数据集被表示为一个图,节点和关系就是数据点。
  • 以图为中心的任务(Graph-focused tasks) —— 数据由一组图构成,每个数据点本身就是一整个图。

image.png

图 9.1 以节点为中心和以图为中心的任务,可以通过训练和预测阶段期望的输入与输出来表示。

在本节中,我们将依次介绍图数据上最重要、研究最充分的 ML 任务。我们会勾勒每一类中的核心任务,并重点关注那些最可能对你有价值的任务。

9.2.1 节点分类

假设你在运营一个拥有数百万用户的社交网络。在这些用户中,有相当一部分是机器人账号。检测这些机器人非常重要,因为它们可能违反平台服务条款、传播假新闻,或者成为营销活动中的无效目标。人工识别它们几乎不可行。理想情况下,你希望拥有一个模型,能够在只给定一小部分人工标注样本的前提下,把用户区分为机器人或真实用户。这就是一个典型的节点分类任务:对于图 (V) 中每一个未标注节点 (u),目标是在仅给定一个已知标签训练节点集合 (V_{train})(它只是 (V) 的一个很小子集)的情况下,预测该节点的标签 (y_u)。

节点分类的其他例子还包括:对蛋白质互作网络中的蛋白质功能进行分类 [4],以及基于超链接或引文图对文档主题进行分类 [5]。在实现 IAS 的过程中,它是一种非常强大的工具,有助于识别与决策。图 9.2 展示了节点分类的主要组成部分和阶段。

乍一看,节点分类似乎只是标准监督分类的一个直接变体,但它与传统监督分类有着明显不同。最重要的一点在于:图中的节点并不是独立同分布(i.i.d.)的。通常,在构建监督 ML 模型时,我们假设每个数据点在统计意义上都与其他所有数据点独立。如果事实并非如此,我们就需要澄清输入数据点之间的依赖关系。同样,我们还假设数据点服从相同分布,以确保模型能够有效泛化到未见过的数据点。但节点分类打破了这一 i.i.d. 假设。我们建模的,不再是一组 i.i.d. 数据点,而是一个由节点相互连接而成的网络。

image.png

图 9.2 节点分类的典型流程。像许多监督式 ML 任务一样,它有两个阶段:训练和预测(在这里,预测就对应于给未分类节点分类)。

不同类型的图会呈现出各种关系模式,这些模式都会挑战 i.i.d. 假设。在社交网络中,同质性(homophily) 就体现了这种互联性:节点通过它们的关系相互影响,并与邻居表现出相似的兴趣、属性和行为 [6]。这种对 i.i.d. 假设的违背意味着:有效的模型在做预测时,必须同时考虑节点特征和网络关系 [7]。在蛋白质互作网络中,结构等价性(structural equivalence) 则变得尤为关键——拥有相似邻域结构的节点,往往共享相似的功能属性 [8]。另一种模式是异质性(heterophily) ,即节点更倾向于连接到与自己特征不同的节点,这进一步说明现实世界数据往往不满足 i.i.d. 假设。这些原则都说明了:如果把节点当作独立数据点来处理,就无法捕获图中编码的丰富关系信息。成功的节点分类,必须同时建模节点属性及其复杂依赖关系。

节点分类到底是监督式还是无监督式?

许多研究者认为,节点分类属于半监督学习(semisupervised learning) [9],因为在训练节点分类模型时,我们通常能够访问整个图,包括所有未标注的节点(例如测试节点)。唯一缺失的是测试节点的标签。但即便如此,我们仍然可以利用这些测试节点的信息(例如它们在图中的邻域结构)来改善训练过程。这与通常的监督学习场景不同:在标准监督学习里,未标注的数据点在训练时往往是不可见的。

将带标签数据与无标签数据共同用于训练的模型,一般统称为半监督学习,因此,在节点分类任务中使用这个术语是可以理解的。不过需要注意的是,半监督学习的标准定义依赖于 i.i.d. 假设,而这一假设在节点分类中并不成立。我们至今仍然会在这个定义上感到纠结。这也说明了:图上的 ML 任务并不能轻易套进经典分类框架中。

节点分类还可以扩展到为单个节点分配多个标签。例如,图片托管平台 Flickr ([www.flickr.com)就使用图和 多标签节点分类 来刻画用户兴趣。除了托管照片之外,Flickr 还是一个在线社交社区,用户可以彼此关注,因此形成了一个基于用户连接的网络。此外,Flickr 用户还可以订阅兴趣小组,这些小组成员身份体现了用户兴趣,并可作为用户标签。由于用户可以订阅很多小组,因此每个用户都可能关联多个标签。图上的多标签节点分类问题,就可以帮助预测用户可能感兴趣但尚未订阅的潜在小组;Tang 和 Liu [10] 提供了与此类 Flickr 用户行为相关的数据集。

9.2.2 链接预测(又称关系预测)

链接预测(Link prediction) 是图 ML 中的一项基础任务,用于识别节点之间未来可能出现的连接。设想你拥有一个综合性的科研论文数据库,数据来源可能包括医疗健康领域(PubMed、MedRxiv)、COVID-19 研究(CORD-19)、广义科学研究(Web of Science),或者计算机科学(DBLP)。基于这些来源,我们可以构建一个合作作者图:作者是节点,而边表示他们至少共同发表过一篇论文。链接预测任务的目标,就是预测那些尚未合作过的作者之间,未来很可能出现的合作关系。

这种预测方法也可以自然扩展到其他关键领域,比如执法与安全应用。图 9.3 展示了典型链接预测任务的两个主要阶段。

image.png

图 9.3 典型的链接预测流程。它分为两个阶段。在训练阶段,图作为输入,模型被训练来判断某条边是否存在。在预测阶段,模型会针对每一对节点输出目标关系的预测结果,也就是该关系存在的概率。

在许多现实世界应用中,图并不是完整的,因为其中存在缺失边。这种不完整通常来自两类原因。第一,有些连接实际上存在,但未被观测、未被记录,或者在某些情况下,被网络中的关键参与者刻意隐藏。第二,很多图本身就是动态演化的;例如,在学术合作图中,一位作者总是可能通过撰写新论文而与其他作者建立新的合作关系。推断或预测这些缺失边,可以为许多 IAS 带来帮助,例如:好友推荐 [11, 12]、商品推荐、KG 补全 [13]、药物副作用预测 [14]、关系数据库中新事实的推断 [15]、蛋白质–蛋白质相互作用发现 [16]、犯罪情报分析 [17],等等。

链接预测任务的不同叫法

这项任务也常被称为 link prediction、graph completion、relational inference 等,具体名称会随应用领域而变化。通常,link prediction 更偏向于预测两个节点之间是否存在连接,而不关心关系类型(关系类型通常固定且预先设定)。相对地,relationship prediction 一般用于既要判断关系是否存在,也要判断关系类型的场景。在本书中,我们会将这些术语交替使用

和节点分类一样,链接预测通常也被视为一种半监督学习任务。尽管某些节点对之间的具体边可能缺失,但我们仍然可以利用已有连接的样本,以及丰富的节点层信息,去预测潜在关系。正是这种半监督特性,使我们能够同时利用图结构中的已知模式与节点属性,来推断可能缺失或未来会出现的连接。

9.2.3 聚类与社区检测

设想你获得了一份来自上一节所提到的任意数据源的科研论文目录,并基于合著关系生成了一个研究者协作图。在观察这个网络时,你不太可能看到一个密集到像“毛线球”一样、所有人都同等可能合作的图。相反,这个图更可能被划分为若干不同的节点簇,这些簇可能由研究方向、所属机构或地理因素等元素所决定。来自同一所大学的两个研究者之间发生合作的概率,通常高于身处异地的两位研究者;同样,两个处于同一研究领域的学者,也更可能引用彼此领域中的研究者,而不是无关领域的研究者。正是这种自然的聚类行为,构成了社区检测算法的理论基础:它们的目标是识别网络结构中这种内在的分组模式(见图 9.4)。

image.png

图 9.4 社区检测流程。输入是一个图,输出则是节点与群组之间的映射关系(图中用圈标出)。

社区检测只利用网络的拓扑结构和关系信息,就能挖掘出图中的潜在群组结构。这项任务在现实世界中有很多应用场景,例如识别基因交互网络中的功能模块 [18],或者检测金融交易网络中的欺诈用户群体 [19]。

图聚类最强大的应用之一,是图描述与图摘要。通过识别那些连接稠密的区域,聚类能够提供关于网络组织结构的一个更高层次视角。当面对那些无法直接可视化、也难以靠人工全面分析的大规模图时,这种能力尤其有价值,因为它实际上为复杂网络关系提供了一种结构化摘要。

社区检测任务的不同叫法

在网络分析中,community detectiongraph clustering 常常会被交替使用,但它们与传统聚类算法(如 K-meansDBSCAN)在本质上是不同的。

  • K-means 会把 (n) 个数据点划分为 (k) 个簇,其方法是反复将每个点分配给最近的簇中心(质心),并不断更新这些质心;其中 (k) 必须预先指定。
  • DBSCAN(density-based spatial clustering of applications with noise)则将那些彼此靠得很近(即邻近点很多)的点聚合在一起,同时把低密度区域中的点标记为离群点;它不需要预先指定簇的数量。

二者最关键的区别在于数据结构:传统聚类算法处理的是向量空间中彼此独立的数据点,而图聚类处理的是相互连接的数据,其中节点之间的关系对于决定群组归属至关重要。在本书中,我们将这两个术语交替使用,因为在图场景下,它们指向的是同一类任务。

图聚类通常是无监督的,不需要任何预先标注的信息来识别社区结构。不过,也有一些聚类方法(例如标签传播 [20])可以利用已有标签来指导社区划分,从而在图分析中架起监督学习与无监督学习之间的桥梁。

前面介绍的三个算法都属于以节点为中心的任务。接下来,我们来看一个以图为中心的任务

9.2.4 图分类

考虑这样一个任务:预测化学分子的毒性溶解度。这些性质并不是仅由单个原子决定的,而是由这些原子如何连接成分子共同决定的。我们可以把每个分子表示为一个图,其中原子是节点,化学键是边 [21]。这种图表示方式使我们能够应用图分类技术,同时预测多个分子性质。正如图 9.5 所示,图分类能够系统地分析原子组成和结构模式,从而根据诸如溶解度和毒性等属性,对先前未标注的分子进行分类。

image.png

图 9.5 图分类流程。和许多监督任务一样,它也有两个阶段:训练阶段会利用带有相关类别的不同图进行训练,以识别每个图的类别;预测阶段则由模型对一个图的类别作出预测。

与在单个图中为单个节点预测标签的节点分类不同,图分类处理的是包含多个相互独立图的数据集,其中每个图本身都表示一个完整样本(例如一个分子)。在图分类中,每个图都作为一个 i.i.d. 数据点,并且有自己的标签,例如“某分子是否有毒”。

图分类的目标,是利用一个带标签图的训练集,学习从整个图到其关联标签的映射。同样地,图层面的图聚类则是对传统无监督聚类的一种扩展,其目标不再是对节点分类,而是对整个图进行归类。这类图级任务的主要挑战在于:如何构造能够有效捕捉每个图内部结构以及其组成部分属性的特征。

图分类在现实世界中有很多应用,也可以用于 IAS。例如,是一类蛋白质,而蛋白质可以表示成图:氨基酸是节点,两个节点之间若距离小于某个阈值,就可建立一条边。经过训练之后,给定一个蛋白质,图分类算法就可以预测它是否是酶。另一个例子是恶意软件分类器:在这个场景中,任务是通过分析一个程序的语法结构与数据流的图表示,来建立一个分类模型,判断该程序是否具有恶意 [22]。

9.3 如何在图上做机器学习?

到这里,我们已经理解了:为什么图上的 ML 会发展成一个独立研究分支,以及这些算法能够解决哪些复杂问题。现在,我们来看看有哪些实现路径。总体上,解决方案可以沿着两个方向展开,如图 9.6 所示。

第一种方法,是使用专门为图设计的算法,例如集体分类(collective classification) [23]。这些专门算法会直接处理图结构,同时综合考虑节点特征和邻域关系来作出预测。
第二种方法,是先把图问题转换成传统 ML 任务:具体做法是先把图结构转化为特征向量。这样一来,我们就可以利用现有完整的 ML 算法生态,包括现代深度学习技术。此时,主要挑战就转移到了:如何定义能够捕捉节点、边,以及在图分类场景下整个图结构关键特征的合适表示。 image.png

图 9.6 基于图的分类方法(collective)与传统分类算法的对比

在本书这一部分中,我们将重点关注图上的特征工程。我们会先介绍手工特征提取方法,展示它们的细致之处,同时也展示它们的繁琐性;然后再逐步过渡到半自动化方法,最后深入讨论 GNN 作为自动特征学习的一种强大方案。GNN 擅长同时捕捉结构模式与节点属性,因此对于不完整 KG 来说尤其有价值。我们将进一步考察:GNN 如何构造有意义的向量嵌入,从而把图中的知识编码到这些表示中,并直接服务于下游 ML 任务,例如分类(比如节点分类)。

9.3.1 节点分类与链接预测

图 9.7 展示了节点分类和链接预测的高层训练流程。训练完成后,会得到一个预测模型:它是本阶段的输出,也是下一个阶段的输入。图 9.8 则展示了预测过程的步骤:它接收已有的节点和关系,并预测类别和缺失链接。

image.png

图 9.7 节点分类与链接预测的训练流程。一个关键步骤是节点与关系的特征化(featurization) 。当这个过程完成后,这些向量就可以被输入到经典算法中。节点分类和链接预测都可以被看作分类任务。

image.png

图 9.8 节点分类与链接预测的预测流程。特征化过程应与训练阶段保持一致。此前构建好的分类器模型会在这个阶段用于做出预测。

注意 训练时使用的特征化过程,必须与预测时使用的特征化过程保持一致。否则,预测阶段将无法正常工作。

现在我们用一个简单示例,把这些原则落到实践中。假设你希望对一个网络中的节点进行分类。为了学习方便,我们使用一个很小的图:著名的 Zachary Karate Club [24]。这个图记录了一个空手道俱乐部 34 名成员之间的关系,追踪的是成员在俱乐部之外两两之间的互动。后来,管理员 “John A” 与教练 “Mr. Hi”(均为化名)之间发生争执,导致俱乐部分裂成两个派系。

在俱乐部分裂之后,每位成员(对应一个节点)要么加入教练的新俱乐部(Mr. Hi),要么继续留在管理员原有的俱乐部(John A)。这个真实世界结果正好为我们提供了真实标签:每个节点都依据成员最终加入哪一方而被标注。我们的节点分类任务,就是仅利用分裂之前的友谊网络结构,来预测这些成员的俱乐部归属,从而说明网络模式如何预测社会行为。我们将用简单代码,完整演示图 9.7 和图 9.8 所描述的流程。

先来查看这个网络,再进行分析。为了这个示例,我们不会把任何内容存入 Neo4j。这个网络非常小,而且在很多网络分析工具中都内置可用,在 NetworkXnetworkx.org)中也可以直接获得;而这正是我们将要使用的工具。下面的代码清单展示了如何导入必要包并加载这个空手道俱乐部图。

代码清单 9.1 创建并绘制空手道俱乐部网络

import networkx as nx
import matplotlib.pyplot as plt

G = nx.karate_club_graph()  #1
draw_and_save_graph_picture(G)

def draw_and_save_graph_picture(G):   #2
    set_club_colors(G)
    layout_position = nx.spring_layout(G, k=8 / math.sqrt(G.order()))
    colors = [n[1]['color'] for n in G.nodes(data=True)]
    nx.draw_networkx(G, pos=layout_position, node_color=colors)
    plt.axis('off')
    plt.savefig("Karate_Graph.svg", format="SVG", dpi=1000)
    plt.savefig("Karate_Graph.png", format="PNG", dpi=1000)
    plt.show()

def set_club_colors(G):  #3
    for node in G.nodes(data=True):
        color = '#00fff9'
        if node[1]['club'] == 'Mr. Hi':
            color = '#e6e6fa'
        node[1]['color'] = color

#1 加载空手道俱乐部图
#2 在屏幕上显示图,并将其保存为 PNG 和 SVG 格式
#3 为每个节点群组分配颜色

生成出来的网络如图 9.9 所示。节点有两种不同深浅的颜色,分别代表成员在分裂后最终加入的俱乐部。

image.png

图 9.9 代码清单 9.1 生成的空手道俱乐部图,节点颜色深浅表示成员最终加入的俱乐部

正如图 9.7 和图 9.8 所示,第一步是为每个节点创建一个向量表示,它将在训练和预测期间作为分类算法的输入。第 10 章和第 11 章会详细介绍更复杂的嵌入技术;在这里,我们先从最简单的方式开始:用每个节点的度(degree) 来表示它,也就是它与其他节点之间连接的数量。这个特征刻画了节点在网络中的连接性。下面的代码清单计算了图中每个节点的度。

代码清单 9.2 用节点的度来表示每个节点

def compute_degree_embeddings(G):

    embeddings = np.array(list(dict(G.degree()).values()))   #1
    embeddings = [[i] for i in embeddings]   #2
    return embeddings

#1 计算每个节点的嵌入表示
#2 将列表中的每个元素转换成一个单值嵌入

练习

尽管这种技术是基础性的,但对我们的目标来说,仅使用节点的度,可能并不能在节点分类任务中提供太大帮助。作为练习,你可以尝试另外两个指标:Mr. Hi 邻居的度John A 邻居的度。它们能为算法提供更多信息,帮助它判断某个节点属于哪个群体。本章稍后会给出答案。

在 GNN 出现之前,Node2Vec [25] 是一种非常知名的自主表示学习技术,它完全基于网络结构来计算节点嵌入。下面的代码清单展示了如何使用 Node2Vec 算法来生成这种具备结构感知能力的节点嵌入。

代码清单 9.3 将 Node2Vec 用作嵌入技术

def compute_complex_embeddings(G):
    node2vec = Node2Vec(
             G, 
             dimensions=64, 
             walk_length=30, 
             num_walks=200, 
             workers=4, 
             seed=0)   #1

    model = node2vec.fit(
             window=10, 
             min_count=1, 
             batch_words=4, 
             seed=0)   #2

    embeddings = [model.wv.get_vector(i) for i in G.nodes]
    return embeddings

#1 Node2Vec 库构造函数会预先计算概率并生成随机游走
#2 计算嵌入表示

这段代码完成了以下工作:

  • 它初始化 Node2Vec,指定每个节点由一个 64 维向量表示。随后,算法会在图上执行 200 次随机游走,每次游走访问 30 个节点。为了提高效率,它使用 4 个并行进程,并通过随机种子保证结果可复现。
  • 模型使用受 Word2Vec 启发的参数进行训练:它会考虑每次游走序列前后各 10 个节点作为上下文,把所有节点都纳入训练(即使某些节点只被访问了一次),并以较小批次处理词项。
  • 它提取图中每个节点学习得到的嵌入,并返回一个向量表示列表,其中每个向量都捕捉了该节点在网络中的结构角色。这些嵌入可以作为下游 ML 任务(例如节点分类或链接预测)的输入特征。

代码清单 9.2 和 9.3 都是在整个图上计算嵌入,因此我们不需要在训练集和预测阶段分别单独计算。下面的代码清单展示了一个训练函数示例,它使用逻辑回归作为分类器。

代码清单 9.4 训练函数

def train(self, train_dataset):
       node_embeddings = train_dataset.embeddings.values.tolist()  #1
       node_labels = train_dataset.label.values.tolist()   #2

       self.scaler = StandardScaler().fit(node_embeddings)   #3
       scaled_embeddings = self.scaler.transform(node_embeddings)   #3

       clf = LogisticRegressionCV(
           random_state=0,
           solver='liblinear',
           multi_class='ovr',
           max_iter=1000)   #4
       self.model = clf.fit(scaled_embeddings, node_labels)  #5

#1 node_embeddings 是一个矩阵,其中每一行都包含一个节点的向量表示(embedding)
#2 node_labels 是一个向量,包含与 node_embeddings 对齐的每个节点的类别标签
#3 对嵌入进行标准化,使其具有零均值和单位方差
#4 初始化一个带交叉验证的逻辑回归分类器,以便自动调参
#5 使用标准化后的嵌入及其对应标签训练分类器

这个训练过程引入了两个重要的 ML 概念:特征标准化逻辑回归

当我们处理多个尺度不同的节点特征时,特征标准化就非常重要。在这个例子中,节点度可能从个位数到上千不等,而其他指标又可能分布在完全不同的数值范围内。通过使用 StandardScaler,把所有特征都转换为零均值、单位方差,我们就能确保每个特征在模型决策中都按比例发挥作用,而不会因为原始数值范围不同而失衡。

为什么特征缩放很重要

ML 算法往往依赖数据点之间的欧几里得距离计算。如果不做恰当的缩放,数值范围更大的特征就会主导这些距离计算,而不管它是否真的更重要。比如,如果节点度的范围是 1 到 1000,而中心性指标的范围是 0 到 1,那么在基于距离的计算中,节点度就会完全压过中心性的影响。这会导致预测偏差和模型精度下降,因为模型会对“大尺度特征”过度敏感,而这种敏感性来自尺度,而不是来自其真正的预测能力。

尽管名字里带着 “regression”,逻辑回归 实际上是一种擅长二元预测任务的分类算法 [25]。在这里,它会估计一个节点属于某个特定类别的概率。该算法通过逻辑函数,把特征的线性组合映射到 0 到 1 之间的概率值,因此非常适合我们的节点分类任务。例如,在空手道俱乐部这个例子中,它会预测每个成员加入教练一方还是管理员一方的概率。

这一实现体现了我们方法的一个关键优势:通过将图数据借助嵌入与适当缩放转化为特征向量,我们就能使用那些久经验证的成熟 ML 算法。如下一个代码清单所示,后续的评估阶段会通过比较保留测试节点集上的预测标签与真实结果,来检验模型的准确性。

代码清单 9.5 进行预测并与真实值进行比较

def evaluate(self, test_dataset):
       test_embeddings = test_dataset.embeddings.values.tolist()   #1
       true_labels = test_dataset.label.values.tolist()   #2

       scaled_test_embeddings = self.scaler.transform(test_embeddings)   #3

       predicted_labels = self.model.predict(scaled_test_embeddings)   #4

       print("True labels:\t\t", true_labels)
       print("Predicted labels:\t", list(predicted_labels))

       # Calculate performance metrics
       metrics = precision_recall_fscore_support(true_labels, #5
       predicted_labels, average='weighted')  
       print('Precision:', metrics[0], 'Recall:', metrics[1], 'f-score:',  #5
       metrics[2])
       conf_matrix = confusion_matrix(true_labels, predicted_labels)   #6
       print("Confusion Matrix:\t", conf_matrix)

#1 test_embeddings 是一个矩阵,其中每一行都包含一个测试节点的向量表示
#2 true_labels 包含测试节点的真实类别标签,并与 test_embeddings 对齐
#3 使用在训练数据上拟合得到的同一个 scaler 对测试嵌入进行标准化
#4 使用训练好的模型预测每个测试节点的类别标签
#5 通过比较预测标签和真实标签来计算 precision、recall 和 F1-score
#6 生成混淆矩阵,以可视化不同类别上的预测准确性

这个函数可用于评估预测模型的质量。我们的示例网络很小,只有两个标签,但这段代码同样适用于更大的图和更多标签的情况。

到这里,我们已经具备了执行节点分类任务训练与评估所需的全部要素。代码清单 9.6 会把前面所有组件(函数)整合起来,并打印结果。若要切换不同的嵌入方式,只需要注释掉不想用的函数并启用想测试的那个即可。

代码清单 9.6 完整的节点分类流程

G = nx.karate_club_graph()
draw_and_save_graph_picture(G)

labels = np.asarray([G.nodes[i]['club'] != 'Mr. Hi' for i in G.nodes])
.astype(np.int64)   #1

#embeddings = compute_degree_embeddings(G)   #2
embeddings = compute_complex_embeddings(G)

df = pd.DataFrame({
        'nodeId': G.nodes,
        'embeddings': embeddings,
        'label': labels
})  #3

train, test = train_test_split(df, test_size=0.4, random_state=0)   #4

classifier = EvaluateEmbedding()
classifier.train(train)   #5
classifier.evaluate(test)   #5

#1 获取每个节点的标签,并根据其所属群体赋值为 0 或 1
#2 取消注释你想测试的嵌入方式
#3 创建一个 DataFrame,其中包含 nodeId、完整嵌入向量以及节点标签
#4 将 DataFrame 随机拆分成训练集和测试集
#5 调用函数来训练并评估模型

在使用两种不同嵌入技术运行代码后,我们可以看到如下结果。

代码清单 9.7 两种嵌入方式的结果

RESULTS WITH MORE SIMPLE EMBEDDINGS USING DEGREE

Gold:     [0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0]
Predicted:     [1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0]

Precision: 0.44642857142857145 
Recall: 0.42857142857142855 
f-score: 0.42857142857142855

Confusion Matrix:     [[3 3]
                      [5 3]]   #1


RESULTS WITH MORE COMPLEX EMBEDDINGS USING NODE2VEC
Gold:     [0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0]
Predicted:     [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0]

Precision: 0.6530612244897959 
Recall: 0.6428571428571429 
f-score: 0.6446886446886447

Confusion Matrix:     [[4 2]
                      [3 5]]   #2

#1 第一次运行的混淆矩阵:3 个真负例(左上)、3 个假正例(右上)、5 个假负例(左下)、3 个真正例(右下)
#2 第二次运行的混淆矩阵:4 个真负例、2 个假正例、3 个假负例、5 个真正例

这两次运行结果都显示出性能相对较差且不稳定。多次重复执行这些实验,会发现结果波动很大,F1 值在 30% 到 70% 之间大幅起伏。由于逻辑回归本身是一个成熟且经过验证的算法,这种不稳定性说明问题并不在分类方法,而在于我们的特征工程方式。结果的高方差表明:仅仅依赖节点度的简单表示,无法捕捉执行可靠节点分类所需的复杂网络模式。即便使用 Node2Vec,也同样如此。

现在,我们利用同质性(homophily) 来改进特征工程。与其只使用节点度——它仅仅统计总连接数——不如基于这些连接的社会上下文,为节点构建一个更丰富的表示。对于每个节点,我们构建一个三维向量,包含:

  • 总度数(total degree,总连接数)
  • Mr. Hi degree(连接到教练阵营的数量)
  • officer degree(连接到管理员阵营的数量)

这种方法如下一段代码所示,它利用了同质性假设:一个节点所属的群体,很可能会受到其邻居所属群体的影响。

代码清单 9.8 基于三种度数计算特征表示

def compute_specific_degree_embeddings(G):
    clubs = nx.get_node_attributes(G, "club")
    mr_hi_degree =  #1
        [[clubs[c] for c in G.neighbors(i)].count('Mr. Hi') for i in
        G.nodes()]
    officer_degree = #2
        [[clubs[c] for c in G.neighbors(i)].count('Officer') 
        for i in G.nodes()]
    degree = list(dict(G.degree()).values())  #3
    embeddings =   #4
        [[degree[i], mr_hi_degree[i], officer_degree[i]] for i in G.nodes]
    return embeddings

#1 计算 mr_hi_degree,只统计属于 Mr. Hi 阵营的邻居
#2 计算 officer_degree,只统计属于 Officer 阵营的邻居
#3 用传统方式计算 degree,统计所有邻居
#4 把这三个值组合成每个节点的单个向量表示

现在我们已经有了新的函数,只需把代码清单 9.6 中的嵌入函数替换为这个新函数即可。结果如下所示。

代码清单 9.9 使用三种度数组成的向量得到的结果

Gold:    [0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0]
Predicted:    [0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1]

Precision: 0.9365079365079365 
Recall: 0.9285714285714286 
f-score: 0.9274255156608098

Confusion Matrix:   [[5 1]
                     [0 8]]

结果明显好了很多。你可以多次运行,它们会非常稳定,而且始终接近 100%。

尽管这个实验很简单,但它展示了在图上使用 ML 构建高效 IAS 的几个原则:

  • 特征工程至关重要。 我们如何表示节点和关系,会从根本上影响图 ML 任务的成败;很多时候,融入领域知识的特征,会比像度中心性这种简单指标表现更好。
  • 自主嵌入需要精心调参。 像 Node2Vec 这样的方法虽然很强大,但并不保证天然能给出最优结果。其参数必须经过仔细设置,才能避免生成过于同质化的节点表示。作为练习,你可以尝试将 walk_lengthnum_walks 设置得更低一些,再运行实验。
  • 领域理解很重要。 对底层图动态和网络属性(例如同质性)的理解,可以指导我们设计出更好的特征工程策略,而这些策略通常比通用方法更有效。

这些洞见将成为后续章节中进一步探索更复杂图表示学习方法的基础。

9.3.2 图分类

前面在节点分类和链接预测中讨论的特征工程方法,在图分类中也是类似的。只不过,这里大多数算法不再为节点提取特征,而是为整个图计算或提取一组特征——当然,输入本身也变成了多个图。图 9.10 展示了这一高层流程。

image.png

图 9.10 图分类的高层训练过程。输出是一个分类器模型,它已经基于不同图的已知类别完成训练。

当模型训练完成之后,它会与未分类的图一起作为输入,进入预测阶段。图 9.11 展示了这一预测过程中的关键步骤和参与对象。

image.png

图 9.11 图分类的高层预测过程。输出是为未标注图分配的类别。

同样可以清楚看出:对节点、关系和图的特征提取是整个过程中的关键,因为它们构成了训练和预测的输入。最终输出的质量——包括准确率和整体性能——在很大程度上都取决于这些特征的质量,以及下游算法参数是否被恰当调优。第 10 章以及第 4 部分后续的大部分内容,都将聚焦于这一任务,以及如何有效利用这个过程的结果。

9.3.3 图聚类

并不是所有图上的 ML 任务都需要前面几节所展示的那些步骤。例如,在图聚类的场景中,输入就是整个图(或者某个子图),而算法直接利用节点和关系来抽取社区(见图 9.12)。

image.png

图 9.12 图聚类的高层流程。这是一个纯图算法方法的示例,不需要中间把图转换成向量表示或其他格式。

对于这个任务来说,不需要特征提取,因为图算法直接使用节点之间的关系、关系方向以及关系权重来划分图。某些算法,例如弱连通分量(WCC) ,采用固定机制,只考虑彼此独立的子图。其他方法,如 Louvain标签传播(label propagation) ,则会优化某些目标,比如模块度(modularity) 。我们在第 4 章已经讨论过 Louvain;这里快速介绍一下标签传播。

标签传播算法(LPA) [20] 是一种在网络中检测社区的快速方法。它通过在网络中传播标签,最终把拥有相同标签的节点视为属于同一社区。由于其内部带有随机性,LPA 并不能保证输出结果完全一致;因此,在同一个网络上多次运行它,可能会得到略有不同的社区划分。

为了展示这一过程,我们继续使用同一个空手道俱乐部图,并利用下面这段代码在其上运行 Louvain。

代码清单 9.10 在空手道俱乐部网络上运行 Louvain

import math
import time
import networkx as nx
import matplotlib.pyplot as plt

def set_club_colors(G):
    for node in G.nodes(data=True):
        color = '#00fff9'
        if node[1]['club'] == 'Mr. Hi':
            color = '#e6e6fa'
        node[1]['color'] = color

def draw_and_save_graph_picture(G, i=0):
    set_club_colors(G)
    layout_position = nx.spring_layout(G, k=8 / math.sqrt(G.order()))
    colors = [n[1]['color'] for n in G.nodes(data=True)]
    nx.draw_networkx(G, pos=layout_position, node_color=colors)
    plt.axis('off')
    plt.savefig("Karate_Graph_" + str(i) + ".svg", format="SVG", dpi=1000)
    plt.savefig("Karate_Graph_" + str(i) + ".png", format="PNG", dpi=1000)
    plt.show()

if __name__ == '__main__':
    start = time.time()
    G = nx.karate_club_graph()
    draw_and_save_graph_picture(G)
    communities = nx.community.louvain_communities(G, seed=123)   #1
    i = 1
    for community in communities:  #2
        subGraph = G.subgraph(community)
        draw_and_save_graph_picture(subGraph, i)
        i += 1

    end = time.time() - start
    print("Time to complete:", end)

#1 使用 Louvain 计算社区
#2 为每个社区分别绘制图

这段代码识别出了 4 个社区,如图 9.13 所示。我们立刻可以看到,这个算法在识别“内部连接紧密、对外连接松散”的节点群体方面做得非常好。每个社区几乎都具有很强的同质性:其中的节点都属于同一个群体(从节点颜色即可看出)。只有一个例外,也就是第 8 号节点,它所在的社区中其他节点全部来自另一个群体。也许这个人真的站错队了!

image.png

图 9.13 社区检测算法应用于空手道俱乐部网络后的结果。算法识别出了 4 个社区。除一个例外外,其余社区中的成员都来自同一群体。有一个“离群”节点,但它与另一群体中的人连接得很紧密。

这个任务与我们前面介绍的任务相比,有两个显著区别。
第一,它不需要表示变换。整个图直接作为输入,算法直接与节点和关系交互。
第二,它没有训练阶段。这是一个无监督任务,它直接接收图并生成输出,而不需要任何标签样本等监督信号。

小结

  • 图上的机器学习天然适合处理相互连接的数据,能够提供通用的数据表示方式,并且通过少量计算任务就能对复杂问题建模。
  • 与假设数据点独立同分布的传统 ML 不同,基于图的方法会利用节点之间的连接和依赖关系,更真实地反映现实世界中的关系与模式。
  • 图上的 ML 任务主要分为两类:以节点为中心的任务(如节点分类和链接预测),即在单个图上执行任务;以及以图为中心的任务(如图分类),即把每个图都视为一个独立的数据点。
  • 节点分类和链接预测通常是半监督任务,它们会同时利用有标签和无标签数据,以及图的结构信息。社区检测通常是无监督的,并直接在图结构上操作。
  • 特征工程在这些任务中起着至关重要的作用。
  • 节点嵌入可以通过多种方式生成,从手工特征工程,到 Node2Vec 这样的自动化方法。但自主嵌入同样需要精心调参,以避免生成过于同质化的表示。
  • 图 ML 的成功,往往取决于能否在自动特征学习、领域知识注入以及针对具体任务选择合适算法之间,找到恰当平衡。