数据科学的图算法-全-

数据科学的图算法(全)

原文:Graph Algorithms for Data Science_

译者:飞龙

协议:CC BY-NC-SA 4.0

前置内容

前言

当你阅读这本书时,我希望你对关系的力量和关联信息的威力感到惊讶,就像我 15 年前在波罗的海的一次极客游轮上第一次遇见 Neo4j 的创始人之一 Emil Eifrém 时一样。十年后,当 Tomaž和我第一次在伦敦见面时,发生了一次同样鼓舞人心且影响深远的事件。他在 Neo4j 社区中已经活跃了一段时间。在那次会面之后,他的贡献急剧增加,最初帮助测试和记录图数据科学库的前身,同时成为与图、NLP 及其实际应用相关的数据科学主题的多产作者(bratanic-tomaz.medium.com)。在我们被 Manning 联系讨论创作一本关于图分析的书——你现在手中的这本书——的时候,Tomaž肯定已经发表了数百篇文章。Tomaž显然是成为这本书作者的合适人选,并且他做得非常出色,将他的经验、教育写作风格和现实世界案例提炼成一本见解独到且引人入胜的书。这本书是一次探索连接数据隐藏深度的旅程,使用图算法和新的机器学习技术——如节点嵌入——以及图机器学习,如链接预测和节点分类,其中许多现在在向量搜索或基于转换器的 GPT 等大型语言模型等领域得到应用。

我经常说,在现实世界中,根本不存在孤立的事物;一切事物都是相互联系的——人物、事件、设备、内容与产品、艺术、历史、政治、市场、生物途径以及气候临界点,从最小的亚原子粒子(关系量子动力学)到宇宙中最大的结构(星系途径)。人类通过添加信息技术、互联网、社交网络、移动计算、物联网以及机器学习模型的广泛应用,加速了这些联系的数量和密度。我们的生活依赖于所有这些网络的正常运作,即使我们大多数时候对此浑然不觉。一个人如何理解所有这些明显和隐藏的关系,它们为所有单个数据点提供了上下文和意义?当然,你可以查询已知模式的模式,但未知未知的事物怎么办?这正是图分析和基于图的机器学习技术大放异彩的地方。它们帮助你找到所需的洞察力。我们从中心性或聚类算法开始,如 PageRank 或 Louvain,这些算法可以用于无监督学习关于你数据中元素的结构和重要性。我最喜欢的例子仍然是 Andrew Beveridge 的《权力的游戏》网络,他在《权力的游戏》书籍的自然语言处理文本中使用了角色的空间接近性来确定重要性、群体和依赖关系。这些算法实现了与人类阅读书籍时发现的结果惊人地相似的结果。使用这些算法的结果作为你的机器学习模型中的特征向量,已经提高了你预测的准确性,因为它们在结构和行为上捕捉了你的实体的上下文。但你甚至可以更进一步,基于图操作显式地计算节点的嵌入。这个领域中的第一个算法之一是 node2vec,它使用了从起点(一个与 PageRank 概念上相似的方法)出发的随机游走路径上的 word2vec 嵌入。现在,我们已经取得了长足的进步,知识图谱嵌入使用图作为输入和输出,可以真正利用连接数据的丰富性。在当前的机器学习论文和架构中,你经常会看到关于图结构和算法的提及,因此这现在是一种基础技术。Tomaž将带你开始学习之旅,从数据建模、摄取和查询开始;到图算法的第一个应用;再到使用自然语言处理从文本中提取知识图谱;最后,在机器学习训练应用中利用节点和图的嵌入。享受这次通往图灵顿启示的旅程,我希望你最终会成为一个图迷,就像我们所有人一样。

Michael Hunger,Neo4j 用户创新高级总监

前言

大约七年前,我在职业道路上转向软件开发。仿佛宇宙为我安排了计划,我在第一份开发工作中被温柔地推向了图(graphs)。我可能是少数几个能声称 Cypher 查询语言是他们第一次接触并开始使用的语言的人,甚至在 SQL 或任何脚本语言(如 Python)之前。当时我的老板 Kristjan Pećanac 预见到了图,尤其是带标签属性的图,是未来的趋势。

当时,市场上并没有很多本地的图数据库,所以 Neo4j 看起来是一个明确的选择。我对图和 Neo4j 了解得越多,就越喜欢它们。然而,有一件事相当明显。尽管我可以用图做很多酷的事情,但文档可以做得更好。我开始写博客,展示人们可以用图做哪些令人瞩目的事情,并省去人们在网上和源代码中搜索如何实现各种功能和工作流程的努力。此外,我把博客当作一个代码库,我可以将其用于我的项目并复制其中的代码。

快进五年:在发表了超过 70 篇博客文章之后,我写了一篇关于结合自然语言处理和图的博客。这可能是我迄今为止最好的博客,有趣的是,我在摘要中写道,如果我有机会写一本书,那篇文章将成为其中的一章。生活是幸运巧合的结合。Michael Hunger 阅读了我的 NLP 博客,并问我是否认真考虑过写一本书。我半开玩笑地回答说,写一本书可能是个好主意,并且有助于我在职业生涯中取得进步。Michael 认真对待了这件事,我们下个月与 Manning 见面。剩下的就是历史,你面前的这本书是我为了使图和图数据科学更容易学习、理解和在你的项目中实现而进行旅程的结果。

致谢

起初,我没有意识到写一本书需要多少工作量。在写完这本书之后,我对任何出版过书籍的作者都产生了相当大的尊重。幸运的是,我周围有伟大的人,他们通过他们的想法、评论和反馈帮助改进了这本书。

首先,我要感谢 Manning 出版社的开发编辑 Dustin Archibald,他帮助我成为一名更好的作家,并引导我接触和了解许多使一本好书更加出色的概念。也要感谢我的项目编辑 Deirdre Hiam,我的校对编辑 Christian Berk,我的校对员 Katie Tennant,以及我的审稿编辑 Aleksandar Dragosavljević。我还要感谢以下这些人在不同程度上提供了他们的想法并帮助审稿:Ljubica Lazarevic,Gopi Krishna Phani Dathar,David Allen,Charles Tapley Hoyt,Pere-Lluís Huguet Cabot,Amy Hodler,Vlad Batushkov,Jaglan Gaurav,Megan Tomlin,Al Krinker,Andrea Paciolla,Atilla Ozgur,Avinash Tiwari,Carl Yu,Chris Allan,Clair Sullivan,Daivid Morgan,Dinesh Ghanta,Hakan Lofquist,Ian Long,Ioannis Atsonios,Jan Pieter Herweijer,Karthik Rajan,Katie Roberts,Lokesh Kumar,Marcin Sȩk,Mark Needham,Mike Fowler,Ninoslav Cerkez,Pethuru Raj,Philip Patterson,Prasad Seemakurthi,Richard Tobias,Sergio Govoni,Simone Sguazza,Subhash Talluri,Sumit Pal,Syed Nouman Hasany,Thomas Joseph Heiman,Tim Wooldridge,Tom Kelly,Viron Dadala,以及 Yogesh Kulkarni。我还要感谢 Jerry Kuch 和 Arturo Geigel,他们的宝贵技术评论是无价的。Arturo 是一位来自波多黎各的独立研究员。他在 Nova Southeastern 大学获得了计算机科学博士学位,因发明神经木马而闻名,目前从事机器学习、图论和技术分析的研究。

关于这本书

《数据科学中的图算法》这本书是为了帮助你将图分析工具包融入你的分析工作流程而编写的。这本书背后的想法是,从一个以前从未听说过图的人开始,引导他们了解他们的第一个图模型和图聚合,最终到达更高级的图和机器学习工作流程,如节点分类和链接预测。

谁应该阅读这本书

《数据科学中的图算法》旨在帮助那些希望通过结合图算法来探索数据点之间关系的数据分析师和开发者扩展他们的数据分析工具集。这本书非常适合那些对 Python 和机器学习概念(如分类模型)有基本理解,并渴望增强他们的数据分析能力的人。凭借其结构化的方法,这本书适合广泛的读者,帮助初级分析师在图算法方面打下坚实的基础,同时也为经验丰富的分析师提供新的视角和高级技术,从而拓宽他们的数据科学能力。

这本书的组织结构

本书分为 3 个部分,共 12 章。第一部分介绍了图,并引导你完成一个图建模任务:

  • 第一章介绍了图的概念以及如何识别图形问题。它还介绍了你将在整本书中学到的图算法类型。

  • 第二章首先介绍了你可以用来描述图的基图形术语。然后,它继续介绍一个带标签属性图模型,并引导你完成一个图建模任务。

第二部分介绍了 Cypher 查询语言和常用的图算法:

  • 第三章涵盖了基本的 Cypher 查询语言语法和子句。它还演示了如何从 CSV 文件中导入图。

  • 第四章引导你进行探索性图分析。你将学习如何使用 Cypher 查询语言检索、过滤和聚合数据。

  • 第五章演示了如何使用 Cypher 查询语言和图算法来表征图。它还展示了如何使用 PageRank 算法找到最重要的节点。

  • 第六章说明了如何将数据点之间的间接关系转换为直接关系,这可以用作图算法的输入。此外,它还介绍了某些图算法的加权变体,如节点度和 PageRank。

  • 第七章展示了如何投影一个共现网络,其中一对节点之间的共同邻居数量定义了它们之间的相似程度。

  • 第八章演示了如何使用各种特征和指标来表征网络中的节点角色。在章节的后面部分,你将学习如何构建一个 k-最近邻图,并找到具有相似角色的节点社区。

第三部分涵盖了更高级的图机器学习工作流程,例如节点分类和链接预测:

  • 第九章介绍了节点嵌入模型,并引导你完成一个节点分类任务。

  • 第十章引导你进行链接预测任务,你将使用 Cypher 查询语言提取相关特征,并使用它们来训练一个链接预测模型。

  • 第十一章涵盖了简单图与复杂图中链接预测的区别,并介绍了知识图谱嵌入模型,这些模型可以用于预测复杂网络中的链接。

  • 第十二章展示了如何使用自然语言处理技术,如命名实体识别和关系提取,来构建图。

总体来说,前两章介绍了基本的图理论和术语,同时讨论了将在第 3-8 章中使用的 Twitter 图模型。第三章和第四章旨在使你熟悉 Cypher 查询语言。接下来的章节被设计为单独的分析员作业,在需要时介绍相关的图算法。

关于代码

本书包含许多源代码示例,既有编号列表,也有与普通文本并行的代码。在这两种情况下,源代码都使用固定宽度字体如这样来格式化,以将其与普通文本区分开来。有时代码也会被加粗以突出显示与章节中先前步骤相比已更改的代码,例如当新功能添加到现有代码行时。在许多情况下,原始源代码已被重新格式化;我们添加了换行并重新整理缩进以适应书中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括行续续标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中删除。代码注释伴随着许多列表,突出显示重要概念。

第三章至第八章的源代码仅作为本书的一部分提供,而第九章至第十二章的源代码则以 Jupyter 笔记本的形式提供在此 GitHub 仓库中:github.com/tomasonjo/graphs-network-science

liveBook 讨论论坛

购买《数据科学中的图算法》包括免费访问 liveBook,Manning 的在线阅读平台。使用 liveBook 的独特讨论功能,您可以在全球范围内或针对特定章节或段落附加评论。为自己做笔记、提问和回答技术问题以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问livebook.manning.com/book/graph-algorithms-for-data-science/discussion。您还可以在livebook.manning.com/discussion了解更多关于 Manning 论坛和行为准则的信息。

Manning 对我们读者的承诺是提供一个场所,在那里个人读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量承诺的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向他提出一些挑战性的问题,以免他的兴趣转移!只要本书有售,论坛和先前讨论的存档将从出版社的网站提供。

关于作者

托马什·布拉塔尼奇是一位内心深处热爱网络科学的科学家,他的工作领域在图和机器学习交叉点上。他已经将这些图技术应用于各个领域的项目,包括欺诈检测、生物医学、面向商业的分析和推荐。

关于封面插图

《数据科学中的图算法》封面上的图像是“利马女人”或“利马女性”,取自雅克·格拉塞·德·圣索沃尔的作品集,该作品集于 1797 年出版。每一幅插图都是手工精细绘制和着色的。

在那些日子里,仅凭人们的服饰就能轻易识别他们居住的地方以及他们的职业或社会地位。曼宁通过基于几个世纪前丰富多样的地域文化的书封面,庆祝计算机行业的创新精神和主动性,这些文化通过如这一系列图片的收藏得以重现。

第一部分 图算法简介

你是否曾对构成互联网的复杂路由之间的联系感到惊奇,是否曾使用地图应用在陌生的城市中导航,或者通过推荐引擎发现隐藏的宝藏?如果是这样,你已经体验到了图算法和数据科学在实际中的应用力量。图无处不在,构成了塑造我们世界的复杂联系和关系,从社交网络和互联网到生物系统和交通网络。在过去的几年里,图数据科学领域以指数级增长,这得益于大型数据集的日益可用和计算能力的快速进步。这些应用的核心在于建模、分析和挖掘数据以揭示隐藏的模式并获得有价值的见解的艺术。本质上,图数据科学专注于探索数据点之间的关系和交互,而不是单独研究它们,因为这种方法允许更深入地理解更大网络中每个元素在上下文和重要性。本书的第一部分将带你进入图算法和数据科学的迷人世界,为你提供利用其潜力的基础知识,以转变你的数据驱动项目。第一章概述了识别图相关问题的方法,并提供了全书将使用的图算法的分类。第二章介绍了图术语,然后直接进入实践,通过一个实际示例展示如何进行图建模。

1 图和网络科学:简介

本章涵盖

  • 介绍图建模和可视化

  • 通过关系理解数据

  • 识别图形化问题

如果您曾经进行过任何分析,您可能已经使用过数据表的表示,如 Excel 电子表格或 SQL 数据库。此外,如果您处理大量文档,您可能已经使用过 parquet 格式或类似 JSON 的对象来表示数据。

图 1.1 展示了订单的表格和 JSON 文档表示。例如,表格包含有关各种日期和产品的四个订单的信息。表格表示可以有效地执行聚合,如汇总总收入或计算新客户的数量。同样,文档结构可以很好地用于存储大量数据。在图 1.1 中,使用 JSON 对象存储有关在线订单的信息,例如订单状态、送货地址等。然而,为表格或文档设计的数据分析工具通常忽略了数据点之间的关系。

01-01

图 1.1 表和文档数据表示

有时,数据点之间的关系会被明确指定,例如一个人和他们与朋友的关系;而在其他情况下,关系可能是间接或隐含的,例如当数据点之间存在可以计算的相关性时。无论关系是明确还是隐含的,它们都包含了可以显著提高分析输出的数据点的额外上下文。

图 1.2 展示了一个 的小实例,它由五个通过五个 关系 连接的 节点 组成。节点或顶点可以用来表示各种现实世界实体,如人、公司或城市;然而,您也可以使用节点来表示各种概念、思想和单词。

01-02

图 1.2 数据的图形表示

在大多数可视化中,节点被描绘为圆形。关系或边用于表示节点之间的各种连接,例如人与人之间的友谊或软件架构中的依赖关系。在本书中,当我们讨论图的基本元素时,我们将使用术语 节点关系

图可以用来表示和探索大量高度连接的数据。例如,图数据库在这些场景中表现出色,因为它们天生就是为了处理实体之间复杂和多变的关系而设计的。在图论中,节点和关系构成了模型的核心,重点在于那些能够实现丰富、连接查询的连接。与关系数据库不同,图数据库执行这些查询时无需进行昂贵的连接或递归操作。这使得你可以高效地回答像“谁是朋友的朋友的朋友?”这样的问题,而不会遭受在关系模型中通常遇到的计算和空间复杂性的困扰。此外,图数据库具有灵活的模式,可以适应现实世界中数据中的变化和异常,为不断变化的企业需求提供更敏捷的解决方案。因此,从传统的关系范式转向图数据库可以消除不必要的复杂性,提高性能,并使我们能够更直观地理解我们的数据和它们之间的关系。

虽然图数据库在许多行业中的有效性是显而易见的,但它们的用途并不仅限于社交平台或电子商务领域。它们在开创性的领域,如太空探索和研究,同样可以证明其重要性。为了说明这一点,可以考虑世界上最有名的太空组织之一:NASA。NASA 的经验教训数据库(llis.nasa.gov/)包含了多年来积累的数百万条记录,包括该组织如何应对困难项目及其产生的建议。这些记录对于规划未来的 NASA 项目以及其他政府和私营组织的项目都非常有价值。此外,数据库还包括关于记录教训的人员、提交教训的中心以及讨论的主题的元数据。图 1.3 中展示的这样一个 NASA 教训的示例,可以通过以下 NASA 网页访问(llis.nasa.gov/lesson/31901)。

01-03

图 1.3 来自 NASA 经验教训数据库的一个示例记录

虽然这些记录包含大量的知识,但数据库已经增长到巨大的规模,这使得快速定位与特定情况相关的记录变得越来越困难(Marr,2017;CKO 新闻团队,2017)。最初,他们的搜索方法使用关键词信息和访问频率,这意味着被频繁查看的记录会在搜索结果中排名更高。然而,仅仅因为一个记录被频繁查看,并不意味着它包含针对特定用例的相关信息。而且,尽管关键词搜索可以显著缩小结果范围,但它仍然可能让工程师面对多达 1,000 条记录来检查相关信息。

大约在 2015 年,一位名叫大卫·梅萨(Dr. David Meza)的先生开始了一项使命,旨在寻找并开发一种更好的方法来在经验教训数据库中查找和开发相关信息。大卫在研究过程中遇到了基于图的技术和工具。有趣的是,他意识到图可视化可以使信息更加直观,更容易看到模式和观察事物在经验教训数据库中的连接方式。首先,他必须将数据建模为图。图 1.4 展示了可以用来表示经验教训记录的图模型。

01-04

图 1.4 表示经验教训记录的图模型

一条经验教训被描述为图中的一个节点。此外,大多数经验教训的元数据,如主题、类别和作者,都作为单独的节点表示。然后,节点通过关系连接起来;例如,主题与经验教训和类别通过相应的关联连接。正如你所见,图模型和可视化的力量来自于观察事物是如何连接的。

假设你对关于阀门污染类别的经验教训感兴趣,该类别在图 1.5 中作为节点表示。为了检查阀门污染类别,你可以遍历从类别到经验教训节点的关联,以访问该类别的经验教训。

01-05

图 1.5 经验教训之间的连接揭示了相关信息。

基于图的可视化有一个优点,就是它们允许你超越单个记录的视角,而是观察经验教训是如何连接的。这种更广阔的视角通常能让你注意到之前未曾考虑过的模式。此外,它使得搜索相关信息更加直观,因为你可以遍历经验教训之间的连接来找到你想要的信息。

通过关系检查数据可能产生的一个意想不到的后果是,你可能会看到之前未曾想到的连接。例如,尽管图中类别之间没有显式的关系,但你可能会看到通过传统分析方法可能被忽视的隐含关系。比如说,你在查看图 1.5 中的图可视化时,可能会看到单个经验教训可以属于多个类别,因此,类别可能存在重叠。基于类别的重叠,你可以计算类别对之间的相关性,并将它们表示为关系,如图 1.6 所示。

01-06

图 1.6 推断节点之间的关系

有趣的是,大卫·梅萨博士提到,他无法理解在这个例子中,阀门污染如何最终与电池火灾风险相关联。在阅读了相关课程后,他了解到以前电池存在泄漏问题。此外,电池放置得足够靠近阀门,以至于会污染它们。

这只是你可能如何发现课程之间的意外联系并深入学习的例子。知识始终存在于记录中;然而,你可能因为只关注特定的记录而错过了更大的图景。使用基于图的建模和可视化方法可以帮助你发现新的见解,这些见解在事后可能很明显,但如果你不考虑数据点周围的环境和模式,你可能会错过这些见解。

1.1 通过关系理解数据

虽然图形可视化对于揭示新的见解非常出色,但当数据点数量太大而无法正确可视化时,它们可能会失去价值。在处理大量数据时,你可以使用图算法来提供见解或确定图中的有趣部分,这些部分可以通过可视化进一步探索。例如,Facebook 在其平台上拥有超过十亿用户;虽然你可以通过图形可视化探索你或他人的周围环境,但要掌握图的整体结构或找到任何突出的用户却很困难。在这些情况下,你可以使用各种图算法来检查整体结构,找到高度连接的用户群,或者你可以专注于识别对信息流有重大影响的图中的重要节点。

最后,你可以使用关于节点位置或角色的信息来提高你的机器学习模型的预测能力。考虑一下像 Facebook 或 Twitter(为了本文的目的,我使用“Twitter”这个名字,而不是“X”)这样的社交平台。通过检查用户的联系来考察用户可能会提供有价值的见解;通过检查一个人的联系,你可能会了解到他们的朋友、家人和榜样。因此,节点代表平台上的用户,关系描述了他们关注的内容,就像你在 Twitter 上看到的那样。图 1.7 中的图相对较小,以便适合可视化,但你可以假设你正在处理数百万——甚至数十亿——用户。然而,即使是这样一个小图,如果没有使用图算法正确准备图可视化,提取任何有意义的信息也很困难。例如,假设你想评估图 1.7 中图的社区结构。

01-07

图 1.7 示例社交平台图

在应用了社区检测(也称为聚类)算法后,评估图的社区结构相对轻松。例如,您可以在图 1.8 中看到,有两个节点是断开的,并且与主图隔离。

01-08

图 1.8 评估图的社区结构

此外,很容易观察到中心图中的节点形成了多个社区。社区被定义为节点之间紧密相连的群体,类似于您可能想象的一群朋友。朋友们形成一个大部分时间都在一起玩的朋友圈;然而,群体中的一些人偶尔才会与其他人互动。例如,假设一个群组成员每周去攀岩一次。他们在攀岩活动中与其他人互动,但不会向其他群组成员介绍他们,也不会邀请他们参加自己的活动。您可能应用到朋友圈的逻辑同样适用于图中的社区。社区检测算法有助于识别图中的群体,这些群体可以用作内容或友情推荐或作为营销工具来识别和针对各种用户群体。可能存在您想要识别图中最重要的节点的情况。一种称为中心性算法的图算法子集旨在识别图中最重要或中心节点。节点重要性有几种不同的定义。在图 1.9 的视觉化中,重要性被定义为对网络信息流的影响。例如,图中的某些节点是不同社区之间唯一的连接——您可能知道他们是社交媒体影响者。由于所有社区之间的信息都通过它们流动,它们对信息流产生了很大影响。它们可以阻止信息到达其他人,或者通过战略性地选择分享信息的位置或传播虚假信息来操纵一个或另一个社区。

01-09

图 1.9 识别有影响力的节点

识别重要节点可以用来制定通过网络传播信息以进行营销的有效策略。另一个相关的策略是找到处于有利位置的节点,这些节点有更多机会完成特定任务。

最后,你可以将有关社区结构和影响力的信息作为机器学习工作流程的一部分。机器学习正成为在当今商业环境中保持竞争力的突出解决方案。采用机器学习的强大动机是完成那些用传统计算方法无法实现的任务和取得成果。如图 1.10 所示,基于图的机器学习方法可以通过探索连接来编码数据点的额外上下文,从而提高模型的准确性。

01-10

图 1.10 基于图的机器学习

节点分类和回归可以被视为一个传统的分类工作流程。唯一的区别是,你包括了描述数据点在网络中位置或角色的各种特征。同样,你可以提取不同的度量标准,这些度量标准编码了数据点在网络中的角色或位置,并将其用于聚类工作流程。此外,还有多个商业场景,你希望使用链接预测工作流程来预测未来的链接。

1.2 如何识别图形问题

现在,你可能正在想,“我不运营一个社交网络平台!那么图能为我提供什么?”虽然任何用例都可以被建模为图,但有一组特定的场景特别适合基于图的分析。

1.2.1 自引用关系

第一个场景涉及同一类型实体之间的自引用关系。在关系数据库中,自引用关系发生在同一表内的数据点之间。自引用关系可以建模为图中的一个节点和关系类型。图 1.11 提供了三种类型图的可视化。

01-11

图 1.11 具有自引用关系的图

图 1.11 中的三个图都由一个节点和一种关系组成。例如,社交图包含 User 节点和 FRIEND 关系,而服务依赖图包含 Service 节点和 DEPENDS_ON 关系。上一节提到了图算法在社交媒体平台上的所有应用。图算法的一个优点是它们是领域无关的,这意味着节点代表的是人、服务还是其他实体或概念无关紧要。因此,你可以在其他领域应用相同的图算法,例如在银行交易网络中,欺诈检测是一个常见的任务。或者,你可以在服务依赖图上应用相同的图算法来评估漏洞如何在图中传播,或者如何防御恶意攻击。有许多场景,其中具有自引用关系的图会非常有用:

  • 优化供应链

  • 欺诈检测

  • 社交媒体分析

  • 管理复杂的电信网络

  • 网络安全

1.2.2 路径查找网络

另一个相当常见的图场景是发现实体或地点之间的路径或路线。你们中大多数人可能都使用过导航系统来找到旅行中的最佳路线。图 1.12 提供了比利时和荷兰城市之间交通网络的可视化。

01-12

图 1.12 交通网络

城市被表示为节点,而交通方式被表示为城市之间的关系。例如,你可以骑自行车从安特卫普到鹿特丹大约需要 330 分钟,或者乘坐火车从海牙到阿姆斯特丹需要 37 分钟。

如前所述,你可以使用交通网络根据你的要求计算最佳路线。路线可以根据不同的因素进行优化:时间、距离或成本。你还可以分析整个网络,并尝试根据网络结构预测交通拥堵模式,或者找到如果发生事故等事件会破坏整个网络的关键位置。在关系型数据库中,你必须假设必须连接的关系顺序,以找到两个实体之间的可用路径。在图 1.12 的例子中,你有三种关系选项可以选择穿越:你可以通过道路、铁路或自行车网络从一个城市到另一个城市旅行。

你可能会遇到的传统数据库的另一个问题是,你事先不知道必须穿越多少关系才能从节点 A 到达节点 B。事先不知道确切需要穿越哪些和多少关系可能会导致复杂且计算成本高昂的查询。因此,明智的做法是将你的数据视为图,因为它将帮助你减轻这两个问题。

在以下场景中,找到最佳路线可能是一种有用的策略:

  • 物流和路线规划

  • 基础设施管理

  • 寻找最佳路径以建立新的联系

  • 支付路由

1.2.3 二分图

图的另一个引人入胜的用例是检查间接或隐藏的关系。在前两个场景中,图只有一个节点类型。另一方面,二分图包含两种节点类型和一种关系类型。图 1.13 展示了零售图和文章图。

01-13

图 1.13 二分图

例如,一个零售图有用户商品节点通过购买关系连接。二分图非常适合用于识别隐藏或间接关系。虽然客户之间没有直接关系,但你可以通过比较他们的购买模式来找到相似客户。本质上,你可以使用这种方法定义客户群体。这类图也常用于协同过滤推荐系统,其中你寻找相似客户或共同购买的产品。还有许多其他场景中,零售图非常有用,比如 Netflix 上的电影推荐或 Spotify 上的歌曲推荐。实际上,任何涉及个人对物品进行评分、购买或投票的场景都可以建模为零售图。

文章图在图 1.5 中简要提及,该图涵盖了 NASA 的学习经验数据库。再次强调,文章或主题之间没有直接联系;然而,通过使用各种数据科学技术,你可以计算文章的相似性或主题的相关性,这些都可以用于改进搜索或推荐。作为另一个例子,考虑应用商店的评论或议会成员对法律和决议的投票。例如,你可以研究议会成员投票的相似性,并将他们的记录与他们的政党关联进行比较。

1.2.4 复杂网络

最后一个例子是一个具有许多不同实体之间连接的复杂网络;生物医学图就是这种情况之一。图 1.14 中展示的图包含各种实体,如基因、通路、化合物和疾病。此外,图中还存在着 24 种不同类型的关系。

01-14

图 1.14 生物医学图(来源:Himmelstein 等人。根据 CC BY 4.0 许可)

解释医疗实体及其关系的所有医疗术语可能是一整本书的主题。然而,你可以观察到生物医学概念高度互联,因此非常适合图模型。生物医学图常用于表示现有知识并预测新的治疗方法、副作用等。

有趣的是,如果你在产品公司工作,你也可以构建一个高度复杂的图,表示关于你客户的全部信息。通常,你拥有关于客户的多种信息来源,如网页分析、商店信息、产品模式和 CRM 数据,你可以将这些信息结合起来创建一个完整的客户表示。然后,你可以使用这个数据配置文件来支持你公司中的分析或各种预测模型。

摘要

  • 在你的数据模型中使用关系给你的数据提供了所需的上下文,以提供更好的分析或预测结果。

  • 关系可以是显式定义的或推断的,就像各种相关性一样。

  • 图由节点组成——代表实体如人、组织、地点或生物医学概念——以及关系,这些关系描述了事物是如何连接、相关、组装的等等。

  • 表格或文档格式中的信息可以被建模并转换为图。

  • 图可视化有助于你发现如果你单独查看数据点时可能错过的模式。

  • 社区检测算法可以提供关于图结构的洞察,并找到不连接或高度连接的组。

  • 中心性算法用于识别图中最重要的、关键的或最具影响力的节点。

  • 图算法的输出可以用作机器学习工作流程的输入,在这些工作流程中,你可以预测新的链接或对数据点进行分类。

  • 自引用图,如只包含单个节点和关系类型的社交图,非常适合基于图的分析。

  • 寻径是核心图问题,即在特定约束条件下识别最优路径。

  • 通常,图被用来识别数据点之间隐藏的、隐含的或间接的关系,这些关系可能没有被明确提及或显而易见。

  • 图模型非常适合模拟复杂、高度连接的数据,例如生物医学图。

2 表示网络结构:设计你的第一个图模型

本章涵盖

  • 介绍掌握图算法和数据科学的学习路径

  • 熟悉基本的图术语

  • 标签属性图模型架构设计

  • 从推文中提取信息

图 2.1 展示了我是如何设想成为一名多才多艺且经验丰富的图数据实践者和科学家的学习路径。这本书将带你沿着这条激动人心的道路前进。

02-01

图 2.1 掌握图算法和数据科学的学习路径

图 2.1 中展示的路径采用自下而上的方法,你将首先学习如何将你领域的数据描述为图,包括建模和构建图。接下来,你将学习如何识别、检索和聚合各种图模式。一旦基础知识掌握,你将深入研究描述性图分析,这有助于你理解图当前的状态。在最后几章中,你将学习如何结合所有之前的课程并将其应用于预测图中的新模式。

学习路径包括四个主要里程碑:

  • 图建模和构建

  • 图查询语言

  • 图算法和推断网络

  • 图机器学习

第一步是识别图形化问题,并将你的数据表示为图。我在上一章已经提到了如何识别适合基于图的方法的使用案例。在本章中,你将首先学习如何描述特定的图结构以及它在不同的数据集中是如何变化的。

第一个里程碑是理解如何进行图建模并将数据集导入图数据库。虽然不同的图数据库使用不同的底层图模型,但我认为最好专注于一个并努力掌握它;否则,我可能需要用几章的篇幅来介绍图模型之间的差异。由于我希望快速进入实际应用和分析,你将学习如何仅作为标签属性图(LPG)模型来建模和导入数据。

与 LPG 模型交互的最广泛采用的图查询语言是Cypher 查询语言。Cypher 查询语言由 Neo4j 开发,并被 Amazon Neptune、AgensGraph、Katana Graph、Memgraph、RedisGraph 和 SAP HANA 采用(opencypher.org/projects/)。Cypher 查询语言可以用来从 LPG 数据库中读取数据,以及导入和转换数据。在第三章中,你将学习 Cypher 的基础知识,这将使你能够构建你的第一个图。你的第一个图将代表 Twitter 社交网络,它将在第四章中用来教你如何使用 Cypher 查询语言进行探索性数据分析。了解并理解 Cypher 查询语言的语法将帮助你达到掌握图算法和数据科学的第二个里程碑,届时你将能够识别和检索图模式,遍历连接,聚合数据,并执行探索性数据分析。

第三个里程碑是介绍和理解图算法。在本书的这一部分,你将学习如何使用图算法来描述图的当前状态并提取有价值的见解。图算法有几个类别。例如,你可以使用中心性算法来识别最重要的或最有影响力的节点。另一方面,你可以使用社区检测或聚类算法来识别高度相互连接的节点组。关于图算法的一个有趣的事实是,大多数都有预定的最佳输入图形状。在实践中,你不会调整图算法以适应你的数据,而是将你的数据转换为提供正确的算法输入。你会发现,大多数中心性和社区检测算法都是设计用来接受一个包含单个节点和关系类型的图的;然而,你将经常处理包含多个节点或关系类型的图。因此,你需要学习帮助你将各种图转换为图算法期望的形状的技术。

这种想法不仅限于图算法,对于大多数传统的机器学习和数据科学算法也是正确的。数据科学和机器学习从业者知道,特征工程或数据处理代表了分析或机器学习工作流程中的大部分工作量——处理图和图算法也不例外。在执行算法之前必须进行数据转换也是我推荐使用图数据库存储数据的原因之一。大多数图数据库都有图查询语言或其他内置工具,可以帮助你以最佳方式执行所需的数据转换。第五章、第六章和第七章致力于教你各种技术,教你如何转换或推断最适合你想要执行的图算法的图结构。

我故意将图算法和图机器学习分为第三和第四个里程碑。从理论上讲,一些图算法也可以被称为图机器学习工具。本书中使用的术语图机器学习指的是预测或分类缺失值或未来行为的流程。另一方面,术语图算法用于描述描述性分析,如社区检测和中心性算法。

图机器学习背后的工作流程与传统机器学习工作流程非常相似,其中你训练一个模型来帮助你解决指定的任务。传统方法与基于图的方法之间的主要区别在于如何构建机器学习模型特征。一般来说,从图中提取机器学习模型特征有两种方法。第一种方法是采取手动方法,定义相关和预测特征。例如,你可以使用 Cypher 查询语言来描述特征或使用社区检测或中心性图算法的输出。第二种方法是利用嵌入模型,这些模型“自动”将网络结构编码为可以输入到机器学习模型中的向量。在第八章到第十一章中,你将学习如何从下游机器学习工作流程中提取和整合图特征。

作为额外收获,你将在最后一章学习如何使用自然语言处理(NLP)技术构建一个图。具体来说,你将使用命名实体识别(NER)和关系抽取(RE)工具从文本中提取相关信息,并将输出存储为图。让我们迈出掌握图算法和数据科学道路的第一步。

2.1 图术语

我们将从学习一些基本的图理论术语开始我们的旅程。本节旨在教你描述你手头上的图的所需语言。

2.1.1 有向图与无向图

图 2.2(A)是一个有向图的例子,其中关系方向起着重要作用。有向图的真实世界例子是 Twitter,用户可以关注其他用户,而这些用户不一定关注他们。在这个例子中,马克的帖子将出现在简和英格丽德的动态中。另一方面,马克将看不到简或英格丽德的任何帖子,因为他没有关注他们。图 2.2(B)显示了一个无向图,其中关系方向不是必要的。一个无向双向关系表示两个实体之间的连接,可以在两个方向上遍历。这个例子可以用来表示 Facebook,其中只有当双方都同意时,才存在友谊关系。

02-02

图 2.2(A)表示一个有向图;(B)表示一个无向图。

每个无向图都可以表示为有向图的观点是一个关键概念,它将在后续内容中变得相关。你可以简单地用两个方向相反的有向链接来替换每个无向连接。

在图算法的上下文中,无向关系允许双向遍历,而有向关系只允许单向遍历。例如,在图 2.3(A)中,你可以遍历从马克到简的无向连接并返回。要在有向图中模拟此功能,你需要创建两个指向相反方向的关系。在图 2.3(B)中,双向有向关系允许你从马克到简并返回。这个概念对于理解图算法的输入至关重要。

02-03

图 2.3 (A)表示一个无向图;(B)表示无向图的等价有向图。

注意 在图算法的上下文中,通常不允许在相反方向上遍历有向关系。然而,在图数据库中,Cypher 查询语言的灵活性允许遍历有向关系的两个方向,使其成为导航复杂关系数据结构的多功能工具。

2.1.2 加权图与无权图

在无权图中,如图 2.4(A)所示,所有关系都具有相同的强度或关联的穿越成本。在无权图中不存在更强或更弱的关系的概念。另一方面,图 2.4(B)展示了一个加权图,其中穿越关系的强度或成本被存储为属性(权重必须是数字)。在这个例子中,简和英格丽德的连接比英格丽德和马克的关系更强。根据领域不同,有时更高的权重值更好,而有时更小的权重值更受欢迎。在图 2.4(B)的例子中,较高的权重更受欢迎,因为它表明了更强的友谊关系。另一方面,在交通网络中,较小的权重值更受欢迎,这是使用加权图的典型应用,你正在寻找一对节点之间的最短加权路径。

02-04

图 2.4 (A)表示一个无权图;(B)表示一个加权图。

2.1.3 二部图与单部图

单部图描述的是由单个类型或类别的节点组成的图。在图 2.5(A)中,你可以观察到只包含代表个人的单个类别的节点的单部图。如前所述,大多数图算法都是设计为以单部图为输入的。图 2.5(B)包含代表个人和代表一个组织的两个节点。由于存在两组节点,你可以将此图描述为二部图。此外,在二部图中,关系总是从一组节点开始,并在另一组节点结束。

02-05

图 2.5(A)表示一个单部图;(B)表示一个二部图。

在图 2.5(B)的示例中,只有从人到组织的关联关系——而不是人与人之间或组织与组织之间的关联。二部图在现实世界中很常见;许多用户-项目交互可以表示为图。例如,在 Netflix 上,用户可以对电影进行评分。用户和电影都表示为节点,评分被描述为一种关系。另一个例子是亚马逊市场,在这里,客户可以购买各种商品。在这里,客户和产品都表示为节点,关系表示客户购买了哪些商品。

2.1.4 多重图与简单图

简单图,如图 2.6(A)所示,在每个方向上只允许一对节点之间有一个关系。有许多用例中,你希望在一对节点之间允许多个关系。在这些情况下,你将处理多重图。图 2.6(B)显示了多重图的一个例子。

02-06

图 2.6(A)表示一个简单图;(B)表示一个多重图。

2.1.5 完全图

一个完全图是一个图中每个节点都与其他所有节点相连的图(见图 2.7)。

02-07

图 2.7 完全图

2.2 网络表示

在你学习更多关于 LPG 模型之前,你还将查看简单网络的文本表示。当你想要通过文本快速传达网络结构时,网络文本表示非常有用。我们将借鉴 Cypher 查询语言的语法。Cypher 的语法提供了使用 ASCII 艺术语法匹配图中节点和关系模式的一种视觉方式。它描述节点和关系的语法也是未来图查询语言(GQL;www.gqlstandards.org/home)的基础,该语言旨在统一图模式查询语言,就像 SQL 对关系数据库所做的那样。Cypher 中节点表示的一个例子如下所示:

(:Person {name:"Thomas"})

在 Cypher 中描述节点时,用括号包围节点——例如,(node)。冒号用于描述节点类型或标签。在前面的例子中,节点标签被定义为Person。节点也可以有属性,这些属性以节点括号内的键值对表示。花括号内有一个键值对{name:"Thomas"},它表示节点的名称属性。

Cypher 中的关系被方括号包围:

-[:FRIEND{since:2016}]->

与节点类似,您可以使用冒号来描述关系的类型。关系也可以在花括号内定义为键值对属性。一个关系不能在没有存在源节点和目标节点的情况下独立存在。例如,您可以使用以下语法指定一个简单的友谊网络:

(:Person {name:"Thomas"})-[:FRIEND {since:2016}]->(:Person {name:"Elaine"})

此 Cypher 语法描述了 Thomas 和 Elaine 之间的友谊关系,可以如图 2.8 所示的网络可视化。

02-08

图 2.8 Thomas 和 Elaine 之间的示例友谊网络

Thomas 和 Elaine 是自 2016 年以来一直是朋友的人。如果您仔细观察,您可以在文本表示的末尾观察到关系的方向指示符。有了它,您可以区分有向和无向关系。如果您想将友谊关系描述为无向的,您只需省略关系方向指示符即可:

-[:FRIEND{since:"2016"}]-

我需要在这里做一个小的声明。许多图数据库不支持直接存储无向关系。然而,Cypher 查询语言支持无向查询,这意味着在查询运行时忽略关系的方向。您将在下一节中学习如何存储无向关系。

练习 2.1

尝试使用 Cypher 语法表示您与雇主之间的关系。此图模式中存在两种类型的节点:一个人和一个组织或企业。您还可以根据需要添加额外的节点或关系属性。

我可以使用以下 Cypher 语法描述我与 Manning Publications 的关系:

(:Person {name:"Tomaz"})-[:WRITES_FOR {since:2020}]->(:Organization {name:"Manning Publications"})

2.2.1 标签属性图模型

标签属性图模型(LPG)是图数据库使用的图模型之一。如前所述,我们将在这本书中仅使用 LPG 模型,因为重点是教授您如何解决和分析实际问题——而不是比较不同的图模型。在前一节中,我简要介绍了 LPG 结构,其中我介绍了 Cypher 文本表示。

节点有一种特殊的属性类型,称为标签,用于表示你领域中的节点角色。例如,你可以使用标签来分类节点是否代表一个人或一个组织。节点和关系都可以存储为键值对属性,例如人的名字作为节点属性或起始日期作为之前 Cypher 文本表示中的关系属性。

重要的是要注意,LPG 模型中的所有关系都是有向的,并且分配了单个类型。关系类型赋予它语义意义。到目前为止,使用的例子使用了FRIENDWRITES_FOR作为关系类型。有趣的是,使用WRITES_FOR关系,连接的方向对于准确描述语义至关重要:

(:Person {name:"Tomaz"})<-[:WRITES_FOR {since:2020}]-(:Organization {name:"Manning Publications"})

如果我反转连接方向,我与 Manning Publications 的关系的语义将显著不同。另一方面,有些场景中关系方向并不那么重要:

(:Person {name:"Thomas"})<-[:FRIEND {since:2016}]-(:Person {name:"Elaine"})

在这个例子中,我反转了关系方向,语义并没有那么不同,因为友谊通常是双向的。我这是什么意思呢?当我与某人成为朋友时,这通常意味着他们也是我的朋友。双向关系也可以被视为无向的。我无法代表所有图数据库,但具体来说对于 Neo4j:当存储具有双向或无向语义的关系,如友谊时,只需要在两个人之间创建一个单一直接连接,因为这种关系也隐含了相反方向上的连接。使用 Cypher 查询语言,你可以忽略你想要遍历的关系的方向。你将通过实际示例了解更多关于这一点。

为了演示一个简单的 LPG 图模型,让我们尝试将以下信息建模为一个图:

  • 托马斯 40 岁。

  • 托马斯是埃莱恩的朋友。

  • 托马斯是一个人。

  • 埃莱恩是一个人。

在图 2.9 的例子中,我们使用节点标签来对节点进行分类,将年龄信息作为内部节点属性存储。由于友谊信息表明了两个人之间的现实世界关系,我们在我们的图中也将它建模为关系。

02-09

图 2.9 表示示例数据的标签属性图模型

假设你想要在图中添加关于他们的电话号码、社会保险号码和地址的信息。通常,你会将像社会保险号码这样的字面值作为节点属性存储;然而,在某些领域,你可能希望将像社会保险号码或电话号码这样的信息表示为单独的节点。一个典型的例子是欺诈检测场景,其中你感兴趣的是检查共享相同地址、社会保险号码或电话号码的客户(图 2.10)。

02-10

图 2.10 表示欺诈调查领域的标记属性图模型

图模型取决于你的任务,并且使用 LPG 模型,你可以将字面值既表示为内部节点属性(键值对)也可以表示为单独的节点。同样,你也可以选择将标签表示为单独的节点。一个经典的例子是使用 LPG 模型描述类层次结构。在图 2.11 中,图模式被修改以支持类层次结构的表示,这可以用于生物领域,例如。

02-11

图 2.11 表示类层次结构领域的标记属性图模型

如前所述,图模型取决于你试图解决的问题。在标记属性图中,抽象级别是节点、关系、标签和属性。

在这本书中,你将使用 LPG 图数据库作为图分析的真相来源。如果你想在进一步的图分析中使用其他图模型作为真相来源,那完全没问题。然而,在这本书中,我不会详细介绍如何构建其他图模型以最佳地适应你的图分析任务。

2.3 设计你的第一个标记属性图模型

现在,想象一个场景,客户要求你进行 Twitter 的网络分析。客户将提供所有相关数据;你的任务是代表这些数据为图,并通过执行网络分析获得各种见解。你将使用 LPG 模型来表示 Twitter。图建模的一般方法是从你想回答的问题开始,逆向工作。不幸的是,在这个任务中并没有提出具体的问题。作为网络科学家,你的任务是尽你所能,找到尽可能多的见解。你可以从尝试描述你手头的领域开始。在 Twitter 的例子中,最基本的规定如下

  • 用户可以关注其他用户(关注者网络)。

  • 用户可以发布推文(用户-推文网络)。

  • 用户可以从其他用户那里转发帖子(转发网络)。

在本节中,你将学习如何开发一个 LPG 图模型。

2.3.1 跟随者网络

在 Twitter 上,你有选择关注其他用户的选项。通过关注用户,你正在订阅他们的活动,并表明你希望在自己的信息流中看到他们的推文。关注者网络的规定如下:用户可以关注其他用户

练习 2.2

作为一项练习,尝试设计跟随者图模型。一般来说,你希望将实体表示为节点。你也可以借鉴一些英语语法中的逻辑。句子的主语和宾语通常表示为节点,而动词描述了它们之间的关系。形容词可以翻译为属性。在设计图模型时,要考虑关系的方向是否具有语义价值。

设计图模型没有正确或完美的方式,但某些模型可以适用于特定场景。当你设计图模型时,尝试回答以下问题:

  • 有多少种不同的节点类型存在?

  • 这些节点有什么样的属性?

  • 你会使用哪个属性来存储节点的唯一标识符?

  • 有哪些类型的关系存在?

  • 单一的关系类型是否足够准确地描述你的领域?

  • 关系的方向是否具有任何语义价值?

  • 属性是如何确定或量化关系的?

在关注网络规范中,句子的主语和宾语都是用户,可以表示为节点。关系可以用来表示规范句子的动词。在这里,将关注交互表示为两个用户之间的关系是有意义的。图 2.12 中的单个关系模式可以用 Cypher 的模式语法如下表示:

(:User{id:"Vanessa", registeredAt:"2019-03-05"})-[:FOLLOWS{since:"2020-01-01"}]->(:User{id:"Thomas", registeredAt:"2011-03-05"})

02-12

图 2.12 Twitter 关注网络模型

图 2.12 中的每个节点都附有标签User。节点标签用于对表示用户的节点进行分类。由于只有一个节点标签,你正在处理一个单部分网络。对于网络中的所有节点,有一个好的做法是有一个唯一标识符来区分节点。在 Twitter 上,你可以使用任何用户的 Twitter 昵称作为他们的唯一标识符。数据还包含用户的注册日期,你可以将其存储为节点的registeredAt属性。

在图 2.12 中,你可以观察到 Kim 关注了 Vanessa,但 Vanessa 没有关注 Kim。换句话说,Kim 会在她的动态中看到 Vanessa 的推文,而 Vanessa 将不会看到来自 Kim 的任何内容。你可以得出结论,关系的方向具有语义价值,因此你正在处理一个有向网络。FOLLOWS关系没有分配任何强度概念,这意味着 Twitter 关注网络是无权重的。然而,你知道关系的创建时间。关于关系创建日期的信息可以存储为关系属性。

在继续之前,你可以检查使用此图模型可以回答的问题或洞察。在检查社交网络时,你可能想要尝试识别影响者。评估影响者的一个简单指标是用户的直接关注者数量。一个用户拥有的关注者越多,他们的推文传播得越广;然而,你的关注者是否也有影响力则很重要。例如,一个财富 500 强 CEO 的关注可能比你的邻居更有影响力。

很可能,用于评估节点传递性影响的著名图算法是 PageRank。传递性关系是两个节点之间的间接关系,例如,节点 A 连接到节点 B,节点 B 连接到节点 C。在这个例子中,节点 A 与节点 C 没有直接关系,但通过节点 B 存在间接关系,这表明它们是传递性连接的。假设一位财富 500 强公司的 CEO 比你的邻居有更多的联系。在这种情况下,如果你有一位财富 500 强公司的 CEO 关注你,而不是你的邻居关注你,你将获得更多的传递性关系,从而获得更大的网络影响力。

社交网络中常用的一种分析类型是推断社区结构。在过去的几年里,使用图进行预测分析变得越来越流行。例如,有一句流行的话是:一个人的平均水平等于他/她最亲近的五个朋友。你可以利用这个假设,尝试根据用户关注的用户来预测一个人的属性。由于关注者的图包含了节点和关系的时间成分,你也可以检查网络随时间的变化,并利用这些信息来预测它未来的增长。

2.3.2 用户推文网络

推文是分享 Twitter 上内容的初级方式。用户推文网络的简单描述如下:一个用户可以发布一条推文。

练习 2.3

要进入设计图模型的流程,尝试开发一个描述先前规范的图模型。从长远来看,如果你拿一块白板或一本素描本,画一些基本的图模型,这将对你有益。

如果你再次尝试根据规范开发一个图模型,你可能会得到以下图模式:

(:User)-[:PUBLISH]->(:Tweet)

为了更紧凑的文本表示模型,您通常可以省略节点和关系属性。存在两种类型的节点:用户和推文。您需要添加的是它们之间的关系,以指示推文的作者。一个好的做法是尽可能具体地描述您的图模型。例如,您也可以使用更通用的标签来表示推文,如Post。使用更通用的标签,如果在路上被要求添加有关来自 Facebook、LinkedIn 或 Stack Overflow 的用户的信息,可能会遇到问题。每个社交媒体平台都是独特的,并生成不同的数据。例如,推文具有与 Facebook 帖子、LinkedIn 更新或 Stack Overflow 问题不同的属性。这意味着Post节点可能有多个可选属性和关系,其中许多在特定类型的帖子中可能未被使用,导致潜在的复杂性和低效。因此,对于不同类型的帖子,拥有特定的节点标签通常是有益的。这样,图数据库的建模更接近于它处理的具体领域数据,从而允许更高效和简单的查询。如果您遵循将图模型尽可能接近领域的指南,您最终会得到如图 2.13 所示的UserTweet节点类型。

02-13

图 2.13 用户-推文网络

在上一个练习中,您已经为用户建立了一个唯一的标识符。现在,您可以做类似的事情,并假设在创建推文时为每个推文创建了一个唯一的 ID。推文的唯一标识符、创建时间和文本存储为Tweet节点的属性。由于存在两个不同的节点标签,您正在处理一个二分网络。在这个例子中,关系的方向并不很重要。您可以反转关系方向,并将关系类型更改为更合适的类型,如PUBLISHED_BY。两种方法都是正确的;您需要选择一种并坚持下去。我的偏好是使用主动语态定义关系类型,在这个例子中是PUBLISH。您不需要担心查询性能,因为在原生图数据库中,遍历关系的反方向没有惩罚。

图 2.14 右侧的示例展示了在开发图模型时应避免的情况。当反向关系不增加任何语义价值时,在标记属性图数据库中避免将其添加到模型中是一种良好的做法。您也不必担心从Tweet节点到User节点的转换。使用 Cypher 查询语言语法,您可以遍历任何反向关系,或者您可以完全忽略方向。

02-14

图 2.14 展示了两种图模型的比较,其中左侧模型使用单一的关系类型来捕捉所有相关信息,而右侧模型则使用相反方向上的冗余关系类型。

可能还会出现的一个问题是,为什么将推文的创建日期存储为节点属性而不是关系属性。这是一个合理的问题,而且像往常一样,这主要取决于你处理的领域。在 Twitter 宇宙中,一条推文只能有一个作者。拥有单一作者意味着一条推文将恰好有一个指向它的PUBLISH关系;因此,推文只创建一次。当你做出这样的决定时,你应该始终包括你想要在这个图模型上执行的查询类型。如果你考虑最基本的使用案例,即你想按天计算推文数量,将创建日期存储为推文节点属性而不是PUBLISH关系会更简单。你将在第三章中了解更多关于图模式查询语言的工作方式。

喜欢推文的用户将是一个例子,在这种情况下,将创建日期存储为关系属性更有意义。虽然只有单一推文作者,但许多用户可以喜欢特定的推文。为了捕捉特定用户点赞的创建时间,将创建时间信息作为关系属性存储是合理的,如图 2.15 所示。如果你想要将关于点赞的时间信息以日期数组的格式存储在Tweet节点上,你将失去关于在那个时间点赞的用户的信息。

02-15

图 2.15 展示了 Twitter 帖子LIKE关系的一个例子,在这种情况下,将创建日期作为关系属性存储是有意义的。

从网络科学的角度来看,用户和推文之间只有PUBLISH关系并不那么有趣。因为每个推文只有一个指向它的关系,所以推文之间没有重叠或相似之处可以尝试分析。然而,如果你在模型中添加了LIKES关系,你可以分析哪些用户喜欢相同或相似的内容,并基于他们喜欢的内容创建用户段。

2.3.3 重新推文网络

剩下的唯一任务规范是定义一个用于重新推文的图模型。当用户对推文有强烈反应时,他们可能想要与他们的关注者分享以扩大其影响力。在这种情况下,他们可以选择重新推文原始推文。用户可以选择喜欢重新推文,而这些点赞不计入原始推文。任务规范定义如下:用户可以从其他用户那里重新推文帖子。

这个规范比前两个要复杂一些。你无法只是提取句子的主语和宾语,然后使用它们来描述节点。用户-推文网络已经定义,因此你可以在该基础上扩展并添加图模型中的转发。你可以选择几种图模型变体。一个简单的选项是在用户和原始推文之间添加RETWEET关系。

描述此模式的 Cypher 语法看起来是这样的:

(:User)-[:PUBLISH]->(:Tweet)<-[:RETWEETS]-(:User)

在这里,你正在使用用户和原始推文之间的RETWEET关系。这个图模式并不将转发视为实际的推文。这既不是好也不是坏——这完全取决于你的用例以及你希望通过分析实现的目标。然而,这种方法确实存在一个小问题。在 Twitter 上,转发也可能有与转发相关联的点赞,而不是与原始帖子相关联。使用 LPG 模型,你无法创建指向另一个关系的引用关系。使用图 2.16 中的图模型,你将失去将点赞附加到转发的功能。在本章的后面部分,你将从转发文本中提取信息,例如标签和提及。在那里,你将再次面临无法将标签和提及信息附加到转发关系的问题。如果 Twitter 域不允许单独的点赞(这些点赞不计入总数),那么将转发作为一个关系是有意义的。不幸的是,在处理 Twitter 域时情况并非如此。为了解决这个问题,你可以将转发视为一个单独的推文,该推文引用了原始推文。这样,你保持了图的一致性,同时仍然能够在以后添加额外的信息到转发中。

02-16

图 2.16 展示了转发网络图模型,其中定义了原始推文和转发该推文的用户之间的RETWEET关系

你仍然可以轻松地区分推文和转发。目前,不必担心底层数据和如何检索它。你将在下一章中了解更多关于 Twitter 数据源的信息。在 Twitter 上转发推文时,用户可以选择性地添加他们的评论。在这个例子中,我们跳过了这个场景,因为它不是规范的一部分;然而,我们将在下一章中探讨如何建模引用推文互动。转发有一个出向的RETWEETS关系,原始推文只能有一个入向的RETWEETS关系。将转发存储为单独节点的图模型(如图 2.17 所示)也允许你在分析过程中根据需要添加其他关系。再次强调,你想要评估如何使用这个图来提取洞察。记住,用户通常在强烈反应于推文内容并希望与他们的关注者分享时转发推文。你可以计算转发次数并尝试识别最受欢迎的推文主题或其作者。你也可以根据转发模式推断用户之间新的直接关系。将间接图模式转换为直接关系是网络分析中常见的中间步骤。在转发网络的情况下,你可以根据用户转发其他用户的频率推断用户之间的直接关系。

02-17

图 2.17 转发网络的图模型,其中转发被视为引用原始推文的推文

图 2.17 演示了 Vanessa 刚刚转发了 Kim 的一条推文的情况。如果你假设转发总是积极的互动,你可能会认为 Vanessa 积极推广 Kim 的推文并扩大其影响力。这种间接的放大模式可以转化为 Kim 和 Vanessa 之间的直接关系,如图 2.18 所示。

02-18

图 2.18 从间接的转发关系模式中推断新的网络

你可以使用文本表示来展示你采用了以下图模式:

(:User)-[:PUBLISH]->(:Tweet)<-[:RETWEETS]-(:User)

然后,你可以将这种间接关系模式转换为用户之间的直接关系:

(:User)-[:AMPLIFY]->(:User)

通过将间接模式转换为直接关系,你正在创建一个新的推断网络。推断关系的类型取决于你的领域用例。在这里,我选择了AMPLIFY类型,因为转发放大了原始推文的传播范围。新的推断AMPLIFY关系是单向的,因为关系的方向具有语义价值。它也是加权的,因为你可以通过计算转发的数量来量化AMPLIFY关系的强度。如果一个用户单次转发另一个用户的帖子,那么AMPLIFY关系的权重是1。然而,如果一个用户经常转发另一个用户的帖子,那么权重将等于转发的数量。你可以在这个推断网络中再次搜索影响者,或者尝试找到通过推广他们的内容来积极支持彼此的用户社区。

2.3.4 表示图模式

图形方法在数据建模中的美妙之处在于,你总是可以将新信息连接到现有图上。现在,你可以将迄今为止的所有图模型决策组合成一个单一的图模型。图 2.19 可视化了根据你对如何建模用户、他们的关注者、推文和转发的决策所遵循的合并数据。

02-19

图 2.19 表示 Twitter 网络的标记属性图,其中用户可以相互关注,也可以发布和转发帖子

近年来,人们提出了表示 LPG 图模式的方法。目前还没有官方标准来展示 LPG 图模式,但我们将现在讨论可能最常见的方法。

02-20

图 2.20 Twitter 社交网络图模型表示

每个节点类型或标签都表示为一个单独的节点。当前 Twitter 网络表示中存在两种不同的节点标签。你可以将它们描述为图模式表示中的两个节点,如图 2.20 所示。然后,将节点属性添加到代表每个标签的节点上。你可以选择性地添加节点属性的示例值,但我更喜欢添加它们的数据类型作为值。目前,还没有达成共识的方式来可视化节点属性是否为可选的,或者是否用作唯一标识符。具有相同标签的节点之间的关系表示为自环。自环是具有相同起始和结束节点的关联。在图 2.20 的右侧,存在两个自环。FOLLOWS关系从起始并指向具有User类型的节点。此外,RETWEETS关系从起始并指向具有类型Tweet的节点。不幸的是,再次,没有达成共识的方式来表示关系方向是否具有语义价值(即应被视为有向或无向)或不是。目前,你必须阅读随图模式可视化一起提供的详细说明。

2.4 从文本中提取知识

你已经学习了如何根据数据的图特征构建图模型。接下来,你将学习如何从文本中提取相关信息并将它们纳入你的知识图谱模型。为了了解你可以从推文内容中提取哪些信息,让我们看看一个示例推文。

02-21

图 2.21 一个包含提及、链接和标签的示例推文

图 2.21 中的推文包含几个元素,以最大化其覆盖范围和互动性。例如,#KnowledgeGraph、#NamedEntityLinking、#Wikipedia、#NLP、#DataScience 和#Graphs 等标签是用于对内容进行分类并使其对感兴趣这些特定主题的用户可发现,从而增加其在 Twitter 平台上的可见性。链接,如 Medium 文章的 URL,提供了直接访问相关内容的途径,允许关注者深入探索该主题。提及,由@符号后跟 Twitter 用户名(例如,@tb_tomaz 和@CatedraBOB)表示,用于直接指向或引用特定个人或组织,从而在 Twitter 社区中创造对话并促进参与。

2.4.1 链接

你可以从推文内容中提取的第一条信息是推文中包含的任何链接。从文本中处理任何链接应该相当直接。

练习 2.4

现在你已经有一些开发标记属性图(LPG)模型的经验了,你认为在 Twitter 图模型中添加链接信息最好的方法是什么?你已经在模型中定义了推文;现在你只需要将提取的链接信息添加到它们上面。

我可以想到两种选择。你可以将 URL 存储为推文节点属性,或者将其存储为单独的节点,并在链接和推文之间添加一个关系(图 2.22)。

02-22

图 2.22 从推文中提取链接的两种存储选项

你认为哪种方法更好?在这两种方法中,你都有选择存储一个或多个链接的选项,因为你可以使用字符串列表作为节点属性。真正的答案取决于你的查询。你想要在分析中将具有相同链接的推文分组吗?如果你将链接作为节点属性存储,那么找到具有相同链接的推文将更加计算密集,因为你需要比较所有推文对之间的列表中的所有元素。你可以通过将链接作为单独的节点来避免这种情况。通过使用单独的节点来表示链接,你只需要遍历两个关系就可以从一个推文到达另一个推文,甚至到达具有相同链接的所有推文。如果你对将具有相同链接的节点分组感兴趣,这种方法将具有更好的可扩展性。

图模型考虑因素

当考虑是否要将信息存储为单独的节点或节点属性时,重要的是要检查这些值是否标准化。以推文中链接的例子来看。当离散值未标准化时,将信息存储为单独的节点是没有意义的。使用单独的节点来存储信息的整个目的就是为了在查询运行时允许更快的遍历。当值未标准化时,你将失去在居住在同一城市的人之间快速遍历的能力。一个经验法则是,一个现实世界的实体或概念应该作为图中的一个单独节点来表示。在下面的图中,Medium 网站被表示为三个不同的节点,如果你试图查找具有相同链接的推文,这将返回无效信息。虽然将链接信息作为节点属性并不能解决查找具有相同链接的推文的问题,但至少你不会在图中将一个现实世界的实体表示为多个节点。解决方案是清理并标准化链接信息,以便你可以将链接作为单独的节点来建模,或者将链接信息作为节点属性存储,以避免在图中将单个实体表示为多个节点。

另一个需要考虑的话题是信息的具体性。例如,假设信息非常不具体,并且从信息的角度来看没有增加多少价值,比如用户的性别。在这种情况下,最好将此信息作为节点属性存储。一个原因是你避免了拥有可以连接到图的大部分部分的节点。以性别为例,你可以有连接到图中几乎一半用户的节点。连接到图的大部分部分的节点被称为超级节点,你通常希望避免在图中使用它们,因为它们由于附着的众多关系而可能阻碍查询性能。

02-22-UN01

一个链接未标准化的示例图

2.4.2 标签

你可以从推文内容中提取的其他信息是标签。人们在推文中使用井号(#)符号在相关关键词或短语之前来对它们进行分类。一条推文可以包含许多标签;将它们作为单独的节点存储并将推文连接到它们是有意义的,如图 2.23 所示。

02-23

图 2.23 推文标签的图模型

一个重要的考虑因素是,你想要避免使用像HAS这样的通用关系类型,你可以在许多场景中使用它。你希望你的关系类型具有意义(例如,如果你从一个推文遍历LINKS_TO关系,你将始终到达一个Link节点)。同样,如果你从一个推文遍历HAS_TAG关系,你将始终到达一个Tag节点。重要的是要记住,你不想最终得到一个可以导致许多不同节点类型的关系类型。使用通用关系类型可能会阻碍图模型的表达能力,并负面对查询处理时间产生影响。

避免使用通用关系类型

假设你正在处理一个使用各种节点之间的通用HAS关系的 Twitter 图模型。例如,假设你想检索所有推文的标签。引擎必须扩展Tweet节点的所有出向HAS关系,并检查关系的末端节点是否为Tag节点。另一方面,如果你使用特定的HAS_TAG关系将标签连接到推文,你将避免遍历不指向Tag节点的关系。此外,引擎不需要检查目标节点类型,因为可以保证HAS_TAG关系指向Tag节点。因此,使用通用关系类型可能会导致查询性能变慢,因为数据库需要搜索更多的关系来找到与查询匹配的图模式。

02-23-UN02

具有通用HAS关系的图示例

使用通用关系类型也可能阻碍图的清晰性和可维护性。在以下图例中,使用HAS关系来表示

推文的作者以及其标签和链接。使用通用关系类型在查询或修改图时可能会导致混淆或错误。相反,定义描述关系本质的特定关系类型可以使图更容易理解和操作,从而有助于确保图随着时间的推移保持一致和正确,因为这种模式更不容易出错。

图 2.24 中的图是一个由推文和标签节点组成的二分网络。在处理二分网络时,将其投影到单边网络是一个常见的分析工作流程步骤。例如,如果一对推文共享相同的标签,你可以假设它们以某种方式相连。这个过程与你在转发网络中看到的过程类似,其中你将间接的图模式转换为直接关系。标签和推文的二分网络转换为单边网络的过程在图 2.24 中进行了可视化。

02-24

图 2.24 单边网络投影的 Twitter 标签

你可以观察到,你总是可以选择将二分网络投影到两种类型的节点上。在一个包含推文及其标签的二分网络中,你可以将其投影为推文或标签的单分网络。新推断关系命名取决于领域。我添加了推文之间的SIMILAR关系,因为我假设具有更多重叠标签的推文可以被认为是更相似的。另一方面,如果标签经常共同出现,它们也可以被认为是相似的。用网络的文本表示来说,你可以将以下间接图模式转换为:

(:Tweet)-[:HAS_TAG]->(:Tag)<-[:HAS_TAG]-(:Tweet)

你可以将其转换为更直接的图模式:

(:Tweet)-[:SIMILAR]-(:Tweet)

你可能已经注意到,网络分析的一个常见方法是将复杂的图模式简化为只有一种类型节点和关系的网络。这是因为大多数经典图算法,如中心性或社区检测算法,都是设计为以单分网络作为输入的。使用单分投影,关系的方向通常不包含任何语义价值。如果推文 A 与推文 B 相似,那么推文 B 也与推文 A 相似。你可以通过计算两个推文共有的标签数量来量化它们之间相似性的强度。因此,大多数推断的相似性网络都是无向和加权的,就像这个例子一样。另一方面,基于转发的先前推断的放大网络也是有向和加权的。你可以分析推文的推断相似性网络,试图找到发布相似内容的用户。请注意,你也可以结合图的其他部分的信息来推断一个新的单分网络。

2.4.3 提及

用户可以通过使用提及符号(@)在其推文中提及其他用户。提及可以理解为邀请评论或召唤,而在其他时候,它可以用作通知用户查看特定内容。你已经在图模式中定义了用户,因此将推文连接到被提及的用户是有意义的,如图 2.25 所示。

02-25

图 2.25 推文提及的图模型

就像标签网络一样,提及网络也是一个经典的二分网络。因此,你可以将其投影为用户或推文的单分网络(图 2.26)。例如,你可以分析哪些用户经常被共同提及,并尝试检查推断的共同提及网络的用户社区结构。你还可以检查被提及的人是否与推文互动。你还可以将提及信息与其他图中的信息结合,并检查被提及的用户在推文中出现的最常见标签。你可以使用的一种策略是将提及信息整合到关注者推荐中。

02-26

图 2.26 Twitter 提及网络折叠或单分投影

2.4.4 最终 Twitter 社交网络模式

设计图模型模式是一个迭代的过程。你逐渐向图模型添加了额外的信息,它逐渐变得更加丰富。当向图模型添加新数据时,建议设计一个自描述的图模式。使用自描述的图模型,你可以避免创建一个用于其他人了解图存储的信息以及如何查询它的模式手册的额外工作。最后,图模式可能会根据你将要执行的查询而改变,因为你可能想要优化特定查询的性能。如果你将迄今为止所做的所有图模型设计考虑因素放入一个单独的可视化中,你可以观察到以下图结构。

图 2.27 显示了 Twitter 网络的一个示例,其中存在四种不同的节点类型或标签。在最终的 Twitter 图模型中,有用户、推文、标签和链接节点。在这个过程中,你也介绍了六种不同类型的关系。如果你总结一下,你可以用图 2.28 中显示的图模式来表示这个示例网络。

02-27

图 2.27 从文本中提取知识后的 Twitter 社交网络

你只想将那些将被实例化的推断关系添加到图模式中。推断关系和相似性网络是基于你在网络分析期间可能做出的假设创建的。你还不知道哪些推断关系将被实例化,所以现在将它们排除在图模式之外是有意义的。最终的图模式表示可以在图 2.28 中查看。

02-28

图 2.28 最终 Twitter 社交网络图模式表示

你可能已经注意到,网络分析中的一个常见主题是将间接的图模式和关系转换为直接的。使用具有专用图模式查询语言的图数据库可以使你更容易实例化这些网络转换。例如,你可以将推文之间的转发链接转换为用户之间的直接放大关系。另一个常见的场景是将二分网络转换为单分网络。单分投影被使用,因为大多数图算法都是设计用来在具有单一类型节点和关系的网络上工作的。在下一章中,你将学习 Cypher 的基础知识以及如何根据本章中推导出的图模型导入网络。

摘要

  • 标签属性图模型由一组节点、关系、节点标签和属性表示。

  • 无向关系可以从两个方向进行遍历。

  • 无向图可以表示为有向图,其中每个关系都被两个指向相反方向的关系所取代。

  • 关系可以被加权,其中权重表示遍历关系的强度或成本。

  • 单分图只包含一个节点和一种关系类型。

  • 二分图包含两组节点,并且同一组内的节点之间不存在任何关系。

  • 多重图是一种允许单个节点对之间存在多个关系的图类型。

  • 完全图是一种节点与其他所有节点都相连的图。

  • Cypher 语法使用括号来表示节点:(:Node)

  • Cypher 中的关系用方括号表示:()-[:RELATIONSHIP]-()

  • 关系不能存在或表示,如果没有其相邻节点。

  • 根据领域,字面值可以表示为一个单独的节点或在标签属性图模型中的一个节点属性。

  • 当关系方向不增加任何语义价值时,在单方向添加关系是一种好的做法,并在使用标签属性图数据库时避免在相反方向重复添加关系。

  • 图建模是一个迭代的过程。

  • 图模型可以用图模型架构可视化来表示。

  • 从非结构化文本中提取结构化信息对大多数数据分析来说非常有价值。

  • 将间接图模式简化为直接关系(单分图投影)是网络分析中常见的步骤。

第二部分 网络分析

现在你已经掌握了图术语和建模的基本知识,你准备好学习如何使用图查询语言和图算法来进行社交网络分析,并揭示传统分析方法可能错过的隐藏或间接模式。第三章和第四章旨在向你介绍 Cypher 查询语言,并展示如何将其用于探索性图分析。在第五章中,你将学习如何通过检查 Twitter 关注者的社交图来描述网络。你将查看节点度分布,并使用图算法,如弱连接组件算法和局部聚类系数算法,然后使用 PageRank 算法识别图中最重要的节点。图分析的一个重要部分涉及将图转换为与图算法期望的结构相一致。因此,你将在第六章学习如何将间接关系转换为直接关系,在第七章构建和分析共现网络,在第八章手动定义和提取节点角色特征,并使用它们来生成最近邻图。

3 使用 Cypher 查询语言的第一步

本章涵盖

  • 介绍 Cypher 查询语言语法

  • 使用 Cypher 创建节点和关系

  • 从数据库中匹配和检索数据

  • 移除属性、删除节点和关系

  • 将 CSV 导入图数据库的最佳实践

到目前为止,你已经学习了一些图论知识以及如何处理带标签的属性图建模过程。现在,你将开始学习如何通过实际案例进行网络分析。为了跟随本书中的示例,你需要设置 Neo4j 开发环境。如果你在设置过程中需要一些帮助,请参阅附录。

本章将介绍 Cypher 查询语言子句以及将数据导入图数据库的最佳实践。首先,我将简要回顾如何使用 Cypher 查询语言语法以文本格式表示网络。如果你已经熟悉 Cypher 查询语法,你可以跳过本章的大部分内容,只需按照最后一节所示导入数据即可。记住,从上一章中,Cypher 语法使用括号来封装节点的表示。如何使用 Cypher 语法描述节点的一个快速提醒如图 3.1 所示。

03-01

图 3.1 使用 Cypher 查询语言语法表示具有标签和属性的节点

在这个例子中,我使用标签 Person 描述了一个节点。节点的标签总是由一个冒号引导。节点属性是括号内封装的关键值对。示例节点只有一个属性,键为 name,值为 Thomas。在 Cypher 中,你还可以在节点的开始处添加一个变量,该变量用作对特定节点的引用。你可以为引用变量选择任何名称;在这个例子中,我选择了引用变量 thomas。节点变量允许你在 Cypher 语句中稍后引用此特定节点,并且它仅在单个 Cypher 语句的上下文中有效。你可以使用节点变量来访问其属性和标签;在表达式中使用它;或创建与给定节点相关的新模式。在 Cypher 中,关系用方括号表示。

关系只能存在于它相邻于源节点和目标节点时。当你用 Cypher 描述一个关系时,你总是需要包括相邻的节点。每个关系都有一个单一的类型;就像节点标签一样,关系类型也是由冒号引导的。图 3.2 中的示例描述了一个具有 FRIEND 类型的关系。关系属性像节点属性一样描述,并且每个关系都可以分配一个变量(在这个例子中,f),该变量可以在 Cypher 语句中稍后用来引用给定的连接。

03-02

图 3.2 使用 Cypher 查询语言语法表示具有类型和属性的节点

注意:在 Neo4j 中,每个关系都存储为有向的。然而,在执行基于存储图上的 Cypher 查询或图算法时,您可以忽略关系的方向。在 Neo4j 中,将无向关系存储为单个有向关系是一种常见的做法。在执行 Cypher 查询时,您可以忽略关系的方向,将其视为无向的。虽然这在 Neo4j 中是一个相关方面,但一开始可能难以理解,因此在本书中,我将向您展示如何区分存储图和查询图的实用示例。

3.1 Cypher 查询语言子句

带着如何在 Cypher 中描述节点和关系的知识,我们现在将开始讨论 Cypher 子句。为了跟上示例,您需要准备好一个可工作的 Neo4j 环境。我建议您使用 Neo4j 浏览器来执行 Cypher 查询。再次提醒,如果您需要设置 Neo4j 环境和访问 Neo4j 浏览器的帮助,我建议您查阅附录。

3.1.1 CREATE 子句

您将首先学习如何使用 Cypher 创建数据。CREATE子句用于在图数据库中存储节点和关系。虽然 Cypher 查询语言子句不区分大小写,但为了便于阅读,建议使用大写字母编写 Cypher 子句。有关 Cypher 风格指南的更多信息,请参阅此 Neo4j 网页:neo4j.com/developer/cypher/style-guide/。您可以使用 Cypher 语法描述节点和关系模式来存储任何图模式。

您将首先在图中创建一个节点。以下列表中的语句创建了一个带有Person标签和单个节点属性的节点。

列表 3.1 存储带有标签Person和属性name(值为Satish)的节点的 Cypher 查询

CREATE (p:Person{name:'Satish'})

每次执行列表 3.1 中的查询,数据库中都会创建一个新的节点。CREATE子句不会检查图数据库中的现有数据;它盲目地遵循命令在数据库中创建新的模式。

如果您想检索创建的图元素并在 Neo4j 浏览器中可视化它们,可以使用RETURN子句。Cypher 查询中只能有一个RETURN子句,并且只能作为查询的最后一个子句。像往常一样,有一些例外情况,您可以在查询中包含多个RETURN子句。这些例外是并集和子查询,您将在以后学习到。

以下列表中的 Cypher 语句首先创建了一个带有Person标签和name属性的节点。

列表 3.2 创建节点并使用RETURN子句检索其信息的 Cypher 查询

CREATE (p:Person{name:'Frodo'})
RETURN p

创建的节点使用 p 变量进行引用。要从数据库中获取创建节点的信息,你可以使用 RETURN 子句并定义你想要检索的变量。在这个例子中,你想要检索 p 变量。

列表 3.2 中的 Cypher 语句在 Neo4j 浏览器中产生了如图 3.3 所示的输出。这个可视化展示了由列表 3.2 中的 Cypher 语句返回的单个节点。此外,Neo4j 浏览器还提供了一些可视化选项,如节点颜色和大小选项,以及标题定义。更多关于 Neo4j 浏览器样式的信息可以在浏览器手册中找到(mng.bz/j1ge)。

03-03

图 3.3 列表 3.2 中 Cypher 语句的输出

练习 3.1

为了练习创建图,在图中创建一个新的带有标签 Person 的节点,并设置两个节点属性。第一个节点属性包含你的姓名信息,第二个节点属性包含你的年龄信息。

你可以在单个 Cypher 语句中使用多个 CREATE 子句。通过使用节点和关系的变量,你可以在后续的 CREATE 查询中修改和连接它们。

下面的列表中的 Cypher 语句演示了如何创建两个节点及其之间的关系。

列表 3.3 存储两个节点及其之间关系的 Cypher 查询

CREATE (elaine:Person{name:'Elaine'}), (michael:Person {name: 'Michael'})
CREATE (elaine)-[f:FRIEND]->(michael)
RETURN *

在第一个 CREATE 子句中创建了两个节点,在第二个 CREATE 子句中添加了它们之间的关系。虽然你可以将这两个 CREATE 子句合并为一个子句,但作为一个最佳实践,建议分别创建节点和关系。

你可能已经注意到列表 3.3 中的 Cypher 语句在 RETURN 子句中使用了 * 通配符运算符。通配符运算符 * 将返回作用域内的所有变量。

练习 3.2

尝试创建两个节点,一个代表你自己,另一个代表你工作的组织。你可能希望使用不同的节点标签来描述一个人和一个组织。然后,在同一个 Cypher 语句中,创建一个连接你和你的雇主的关系。你可以尝试添加一个关系属性来表示你开始当前职位的日期。

记住,你只能在 Neo4j 图数据库中存储有向关系。让我们看看当你尝试在以下列表中创建无向关系时会发生什么。

列表 3.4 尝试在数据库中存储无向关系并失败的 Cypher 查询

CREATE (elaine:Person{name:'Elaine'}), (michael:Person {name: 'Michael'})
CREATE (elaine)-[:FRIEND]-(michael)
RETURN *

列表 3.4 中的 Cypher 语句失败,因为数据库中只能存储有向关系。虽然关系方向箭头在查询中似乎只是很小的一部分,但它对查询的行为有非常大的影响。

初学者中另一个常见的误解是他们忘记引用变量仅在相同的 Cypher 语句中可见。如前所述,CREATE 语句在向图中插入新数据之前不执行数据库查找。以下 Cypher 语句乍一看似乎是好的,但实际上相当糟糕。

列表 3.5 将两个没有标签且之间有关系的空节点存储到数据库中的 Cypher 查询

CREATE (ankit)-[f:FRIEND]->(elaine)
RETURN *

你能推断出原因吗?因为 CREATE 语句不执行数据库查找,并且在 Cypher 查询之间没有变量引用可见性,它只会创建我们描述的新模式。因此,列表 3.5 中的 Cypher 语句在两个没有标签且没有属性的节点之间创建了一个 FRIEND 关系。你必须非常小心,避免这些类型的情况。在标记属性图模型中,没有你想在数据库中存储没有任何标签的节点的情况。至少,你可以在每个节点上添加一个通用的 Node 标签。

注意:标记属性图模型非常灵活,允许你创建没有标签或属性的节点。然而,你应该始终努力至少为你在数据库中存储的每个节点添加一个标签。有标签的节点将有助于模型的可读性以及查询执行性能。我可以安全地说,如果你的图中没有标签的节点,那么你的模型或导入过程可能存在问题。

练习 3.3

本练习的目标是创建三个节点,并在它们之间建立两个关系。这些节点应代表你目前居住的城市、国家和大陆。在它们之间添加一个城市和国家之间的关系,以及国家与大陆之间的关系。花几分钟时间决定你想要使用的关系类型和关系的方向。

3.1.2 MATCH 子句

你可以使用 MATCH 子句在数据库中搜索现有的图模式。Cypher 是一种声明式查询语言,这意味着你只需要指定你感兴趣的模式,然后让查询引擎负责如何从数据库中检索这些模式。在上一节中,你至少创建了三个带有 Person 标签和不同 name 属性值的节点。如果你想找到一个具有特定 name 属性值的 Person 节点,你可以使用以下查询。

列表 3.6 查询并检索具有标签 Personname 属性值为 Satish 的任何节点的 Cypher 语句

MATCH (p:Person {name:'Satish'})
RETURN p

在你编写第一个 Cypher 子句之前,首先学习如何使用 Cypher 描述节点和关系模式是至关重要的。当你知道如何描述图模式时,你可以使用MATCH子句从数据库中检索它。列表 3.6 中的查询使用内联图模式匹配。内联模式匹配使用 Cypher 模式语法来描述具有其标签和属性的节点或关系模式。与内联模式匹配相反的是使用WHERE子句来描述图模式。

列表 3.7 使用WHERE子句搜索和检索任何标签为Personname属性值为Satish的节点的 Cypher 语句

MATCH (p)
WHERE p:Person AND p.name = 'Satish'
RETURN p

列表 3.7 中的查询将产生与列表 3.6 中查询完全相同的查询计划和结果。内联语法只是提高可读性的语法糖。使用WHERE子句,你描述了你想要检索一个标签为Personname属性值为Satish的节点。我个人的偏好是使用内联图模式描述节点标签,并在WHERE子句中提供额外的匹配过滤器。

列表 3.8 使用内联图模式匹配与WHERE子句结合来描述图模式的 Cypher 语句

MATCH (p:Person)
WHERE p.name = 'Satish' OR p.name = 'Elaine'
RETURN p.name AS person

列表 3.8 中的 Cypher 语句引入了AS运算符。使用AS运算符,你可以命名或别命名一个变量引用,这允许你产生更易读的查询输出。

练习 3.4

作为练习,尝试从数据库中检索所有标签为Person的节点。你可以使用内联图模式描述,或者你可以使用WHERE子句。在RETURN子句中,只返回节点的name属性。使用AS运算符将name属性别名化,以产生更易读的列名。

你可以在一个序列中始终有多个MATCH子句;然而,WHERE子句仅适用于前面的MATCH子句。如果你在序列中使用多个MATCH子句,确保在每个MATCH子句之后(如果需要的话)附加一个WHERE子句。

列表 3.9 使用内联图模式匹配与WHERE子句结合来描述图模式的 Cypher 语句

MATCH (satish:Person)
WHERE satish.name = 'Satish'
MATCH (elaine:Person)
WHERE elaine.name = 'Elaine'
RETURN *

注意:WHERE子句只能存在于它跟随WITHMATCHOPTIONAL MATCH子句之后。当你连续有多个MATCHWITH子句时,确保在每个子句之后(如果需要的话)附加WHERE子句。即使你在多个MATCH语句之后只使用一个WHERE子句,有时你可能会得到相同的结果,但查询性能很可能会更差。

MATCH子句通常用于在数据库中查找现有的节点或关系,然后使用CREATEMERGE子句插入附加数据。例如,你可以使用MATCH子句来查找标签为Person且名字为ElaineSatish的节点,并在它们之间创建一个新的关系。

列表 3.10 在数据库中查找两个节点并创建它们之间新的 FRIEND 关系的 Cypher 查询

MATCH (from:Person), (to:Person)
WHERE from.name = 'Satish' AND to.name = 'Elaine'
CREATE (from)-[f:FRIEND]->(to)
RETURN *

列表 3.10 中的语句将 MATCHCREATE 子句组合起来,在数据库中的现有节点之间创建一个新的关系。

练习 3.5

如果您还没有创建一个以您的名字作为 name 节点属性的 Person 节点,请首先创建它。接下来,在单独的查询中,使用 MATCH 子句查找具有您名字和 ElainePerson 节点,Elaine 也需要存在于您的数据库中,并在它们之间创建一个新的 FRIENDS 关系。您可以添加任何您认为合适的额外关系属性。

使用 MATCH 子句的一个关键概念是认识到,如果查询中的单个 MATCH 子句在数据库中没有找到任何与提供的模式匹配的数据,查询将返回没有结果。如果您使用单个 MATCH 子句从数据库中检索一个不存在的图模式,您将得到没有结果。

列表 3.11 在数据库中匹配一个不存在的图模式的 Cypher 查询

MATCH (org:Organization)
WHERE org.name = 'Acme Inc'
RETURN *

当您尝试从数据库中检索一个不存在的图模式时,您将得到没有结果,这是直观的。不那么直观的是,当您在查询中按顺序有多个 MATCH 子句时,如果只有一个 MATCH 子句尝试从数据库中检索一个不存在的模式,整个查询将返回没有结果。

列表 3.12 在数据库中匹配现有和非现有图模式的 Cypher 查询

MATCH (p:Person)
WHERE p.name = 'Satish'
MATCH (b:Book)
WHERE org.title = 'Catcher in the rye'
RETURN *

列表 3.12 中的查询首先尝试在数据库中找到一个具有 name 属性 SatishPerson 节点。您已经执行了查询的这一部分,所以您知道这个模式存在于数据库中。第二个 MATCH 子句尝试从数据库中检索一个不存在的模式。如果查询中的任何一个 MATCH 子句都没有从数据库中检索到任何模式,查询的结果将为空。

可选的匹配子句

如果您不希望查询在单个 MATCH 子句在数据库中找不到现有图模式时停止,您可以使用 OPTIONAL MATCH 子句。如果数据库中没有找到匹配的模式,OPTIONAL MATCH 子句将返回 null 值,而不是返回没有结果,其行为类似于 SQL 中的 OUTER JOIN。您可以使用 OPTIONAL MATCH 子句重写列表 3.12 中的查询,以期望和处理一个不存在的 Organization 模式。

列表 3.13 在数据库中匹配现有和非现有图模式的 Cypher 语句

MATCH (p:Person)
WHERE p.name = 'Satish'
OPTIONAL MATCH (b:Book)
WHERE b.title = 'Catcher in the rye'
RETURN *

通过使用列表 3.13 中所示 OPTIONAL MATCH 子句,当没有找到图模式时,语句不会返回空结果。相反,对于不存在的图模式返回 null 值,而匹配的模式仍然被检索。

OPTIONAL MATCH 也可以用来检索存在的节点关系。

列表 3.14 匹配数据库中现有和非现有图模式的 Cypher 语句

MATCH (p:Person)
WHERE p.name = 'Satish'
OPTIONAL MATCH (p)-[f:FRIEND]->(f1)
RETURN *

列表 3.14 中的 Cypher 语句首先匹配具有 name 属性值为 SatishPerson 节点。然后,它使用 OPTIONAL MATCH 来匹配任何出度的 FRIENDS 关系。如果找到 FRIENDS 关系,则返回关系和原始节点。然而,如果没有找到 FRIENDS 关系,列表 3.14 中的 Cypher 语句仍然返回具有 name 属性值 SatishPerson 节点。

练习 3.6

确定代表您自己的节点在数据库中是否有任何 FRIENDS 关系。首先使用 MATCH 子句匹配名为您的 Person 节点。接下来,使用 OPTIONAL MATCH 来评估节点是否有任何附加的 FRIENDS 关系。最后,使用 RETURN 子句返回指定的图模式。

3.1.3 WITH 子句

使用 WITH 子句,您可以在将结果传递到 Cypher 查询的下一部分之前,作为中间步骤来操作数据。在将中间数据操作传递到下一部分之前,Cypher 语句中的中间数据操作可以是一个或多个以下操作之一:

  • 过滤结果

  • 选择结果

  • 聚合结果

  • 分页结果

  • 限制结果

例如,您可以使用 WITH 子句与 LIMIT 结合来限制行数。

列表 3.15 使用 WITH 子句限制行数的 Cypher 语句

MATCH (p:Person)
WITH p LIMIT 1
RETURN *

您还可以使用 WITH 子句来定义和计算中间变量。

列表 3.16 使用 WITH 子句计算新变量的 Cypher 语句

CREATE (p:Person {name: "Johann", born: 1988})
WITH p.name AS name, 2023 - p.born AS age
RETURN name, age

列表 3.16 中的 Cypher 语句首先创建一个具有 nameborn 属性的 Person 节点。您可以使用中间的 WITH 子句根据约翰出生的年份计算年龄。此外,您还可以选择任何变量并将它们别名为,如列表 3.16 中的 name 属性示例。

如果您愿意,您还可以使用 WITH 子句与 WHERE 子句结合来根据现有或新定义的变量过滤中间结果。

列表 3.17 使用 WITH 子句和 WHERE 子句过滤结果的 Cypher 语句

MATCH (p:Person)
WITH p.name AS name, 2023 - p.born AS age
WHERE age > 12
RETURN name, age

列表 3.17 中的 Cypher 语句在 WITH 子句中引入了计算出的 age 变量。您可以使用紧随 WITH 子句之后的 WHERE 子句根据现有或新定义的变量来过滤结果。

当聚合数据时,WITH 子句也非常有用。在第四章中,我们将更深入地讨论数据聚合。

3.1.4 SET 子句

使用 SET 子句来更新节点标签以及节点和关系的属性。SET 子句通常与 MATCH 子句结合使用,以更新现有节点或关系属性。

列表 3.18 使用 SET 子句更新现有节点属性的 Cypher 语句

MATCH (t:Person)
WHERE t.name = 'Satish'
SET t.interest = 'Gardening',
    t.hungry = True

对于使用映射数据结构更改或突变多个属性,SET 子句也有特殊的语法。映射 数据结构起源于 Java,与 Python 中的字典或 JavaScript 中的 JSON 对象相同。

列表 3.19 使用 SET 子句结合映射数据结构更新多个节点属性的 Cypher 语句

MATCH (e:Person)
WHERE e.name = 'Elaine'
SET e += {hungry: false, pet: 'dog'}

注意,如果将 SET 子句的 += 运算符替换为仅 =,则仅用映射中提供的那些属性覆盖所有现有属性。

练习 3.7

到现在为止,数据库中可能已经有一个名为你的名字的 Person 节点。使用 SET 子句添加额外的节点属性,例如你最喜欢的食物或你宠物的名字。

使用 SET 子句,你还可以向节点添加额外的标签。

列表 3.20 向现有节点添加二级标签的 Cypher 语句

MATCH (p:Person)
WHERE p.name = 'Satish'
SET p:Student

当你想为节点添加标签以实现更快和更轻松的检索时,多个节点标签是有帮助的。在列表 3.20 的例子中,你向 Satish 节点添加了 Student 标签,在接下来的练习中,你将向代表你的节点添加一个适当的标签,例如 StudentEmployee 或其他。在使用多个节点标签时,一个好的指导原则是节点标签应该是语义正交的。语义正交 指的是节点标签不持有相同或相似的意义,并且彼此之间没有任何关系。二级节点标签用于将节点分组到不同的桶中,以便每个子集都易于访问。请注意,单个节点可以有多个二级标签;例如,一个人可以同时被雇佣和上大学。在这种情况下,你可以向它分配 StudentEmployee 标签。然而,出于建模和性能的原因,你应该避免向单个节点添加超过几个节点标签。

在我的工作中,我也注意到在以下场景中使用多个标签是有帮助的:你预先计算一些值,并根据这些值分配额外的节点标签。例如,如果你在一个营销漏斗中与客户合作,你可以根据其漏斗阶段向节点添加二级标签。图 3.4 显示了三个 Person 节点;然而,所有节点都有一个二级节点标签。如果你在你的网站上有一个人的图,你可以使用二级节点标签来标记他们在营销漏斗中的位置。例如,已经完成购买的可以标记为二级 Customer 标签。

03-04

图 3.4 使用多个节点标签来分配客户漏斗阶段

练习 3.8

匹配代表你的 Person 节点(即,它具有你的名字作为 name 属性值)。然后,向它添加一个二级 Reader 标签。

3.1.5 REMOVE 子句

REMOVE 子句是 SET 子句的反义。它用于删除节点标签和节点以及关系的属性。删除节点属性也可以理解为将其值设置为 null。如果您想从名为 SatishPerson 节点中删除 hungry 属性,您可以执行以下 Cypher 查询。

列表 3.21 从数据库中现有节点中删除节点属性的 Cypher 语句

MATCH (t:Person)
WHERE t.name = 'Satish'
REMOVE t.hungry

使用 REMOVE 子句,您还可以从现有节点中删除标签。

列表 3.22 从数据库中现有节点中删除节点标签的 Cypher 语句

MATCH (t:Person)
WHERE t.name = 'Satish'
REMOVE t:Author

3.1.6 DELETE 子句

DELETE 子句用于在数据库中删除节点和关系。您可以使用以下 Cypher 查询首先检查您的图数据库的内容(如果图非常小)。

列表 3.23 检索数据库中所有节点和关系的 Cypher 语句

MATCH (n)
OPTIONAL MATCH (n)-[r]->(m)
RETURN n, r, m

如果您在 Neo4j 浏览器中运行列表 3.23 中的查询,您应该得到一个类似于图 3.5 的图可视化。

03-05

图 3.5 数据库中当前存储的图的视觉表示

目前数据库中只有少数节点和关系,确切数量取决于您完成了多少练习。如果您没有完成任何练习,这没关系;然而,您至少需要执行列表 3.3、3.6 和 3.10 中的 Cypher 语句,以将相关数据填充到数据库中。首先,您将删除具有 name 属性 SatishElainePerson 节点之间的关系。要执行图模式删除,您必须首先使用 MATCH 子句找到图模式,然后使用 DELETE 子句将其从数据库中删除。

列表 3.24 删除具有 name 属性 SatishElainePerson 节点之间关系的 Cypher 语句

MATCH (n:Person)-[r]->(m:Person)
WHERE n.name = 'Satish' AND m.name = 'Elaine'
DELETE r

在列表 3.24 中,MATCH 子句首先匹配从代表 Satish 的 Person 节点指向代表 Elaine 的节点的任何关系。请注意,您在 MATCH 子句中没有定义任何关系类型。当您在图模式描述中省略关系类型时,MATCH 子句将在描述的节点之间搜索任何类型的关系。同样,您也可以从数据库中删除节点,如下面的列表所示。

列表 3.25 从数据库中删除单个节点的 Cypher 查询

MATCH (n:Person)
WHERE n.name = 'Satish'
DELETE n

现在,如果您尝试从数据库中删除 Elaine 节点,会发生什么?

列表 3.26 从数据库中删除单个节点的 Cypher 查询

MATCH (n:Person)
WHERE n.name = 'Elaine'
DELETE n

您可能会想知道为什么您可以删除代表 Satish 的节点但不能删除代表 Elaine 的节点。幸运的是,图 3.6 中显示的错误信息非常详细:您不能删除仍然有其他节点关系的节点。

03-06

图 3.6 当您想要删除具有与其他节点现有关系的节点时发生的错误

DETACH DELETE子句

由于删除具有现有关系的节点是一个常见的操作,Cypher 查询语言提供了一个DETACH DELETE子句,它首先删除节点上所有附加的关系,然后删除节点本身。你可以尝试使用DETACH DELETE子句删除代表 Elaine 的节点。

列表 3.27 使用DETACH DELETE子句从数据库中删除单个节点及其所有关系的 Cypher 语句

MATCH (n:Person)
WHERE n.name = 'Elaine'
DETACH DELETE n

列表 3.27 中的 Cypher 语句删除了节点附加的关系以及节点本身。

练习 3.9

尝试删除代表你自己或代表名为MichaelPerson节点的节点。如果给定的节点仍然存在现有关系,你必须使用DETACH DELETE子句先删除关系,然后删除节点。

当你在图形数据库中玩耍时可能会用到的一个 Cypher 语句是删除数据库中的所有节点和关系,希望这不是在生产环境中使用。

列表 3.28 删除数据库中所有节点和关系的 Cypher 语句

MATCH (n)
DETACH DELETE n

列表 3.28 中的查询将首先使用MATCH子句在数据库中查找所有节点。由于你未在节点描述中包含任何节点标签,查询引擎将返回数据库中的所有节点。使用DETACH DELETE子句,你指示查询引擎首先删除节点上所有附加的关系,然后删除节点本身。一旦语句执行完毕,你应该会得到一个空数据库。

3.1.7 MERGE子句

在本节中,我将假设你从一个空数据库开始。如果你数据库中仍有数据存储,请运行列表 3.28 中的查询。

MERGE子句可以理解为MATCHCREATE子句的组合使用。使用MERGE子句,你指示查询引擎首先尝试匹配给定的图模式,如果不存在,则创建以下列表中显示的模式。

列表 3.29 使用MERGE子句确保数据库中存在名为AliciaPerson节点的 Cypher 查询

MERGE (a:Person {name:'Alicia'})

MERGE子句仅支持内联图模式描述,不能与WHERE子句结合使用。列表 3.29 中的语句确保数据库中存在具有name属性AliciaPerson节点。你可以多次运行此查询,数据库中始终只有一个名为AliciaPerson节点。可以多次运行且始终输出相同结果的语句也称为幂等语句。当你将数据导入图数据库时,建议使用MERGE子句而不是CREATE子句。使用MERGE子句,你不必担心后续的节点去重,并且可以多次运行查询而不会破坏你的数据库结构。你认为如果我们尝试使用MERGE子句来描述一个名为AliciaPerson节点以及额外的节点属性位置会发生什么?让我们在下面的列表中检查。

列表 3.30 Cypher 查询合并具有两个节点属性的单一节点

MERGE (t:Person {name:'Alicia', location:'Chicago'})

练习 3.10

尝试匹配和检索数据库中所有具有Person标签和name属性值为Alicia的节点。

通过完成练习 3.10,你可以快速观察到在图 3.7 所示的图形可视化中存在两个节点,标签为Person,并且具有name属性Alicia

03-07

图 3.7 数据库中所有 Alicia 节点的可视化

数据库中有两个具有相同name属性的Person节点。我们刚才不是讨论了MERGE子句是幂等的吗?记住,MERGE子句首先尝试匹配现有的图模式,如果不存在,它才创建完整的给定图模式。当你执行列表 3.30 中的查询以合并具有两个节点属性的Person节点时,查询引擎首先搜索给定的模式。在那个时刻,数据库中不存在具有name属性Alicialocation属性ChicagoPerson节点。遵循MERGE逻辑,然后它创建了一个具有这两个属性的新Person节点。

在设计图模型时,最佳实践是为每个节点标签定义一个唯一标识符。唯一标识符包括为图中的每个节点定义一个唯一的属性值。例如,如果你假设Person节点的name属性是唯一的,你可以使用以下MERGE子句来导入Person节点。

列表 3.31 Cypher 查询在唯一标识符属性上合并节点,然后向节点添加额外的属性

MERGE (t:Person{name:"Amy"})
ON CREATE SET t.location = "Taj Mahal", t.createdAt = datetime()
ON MATCH SET t.updatedAt = datetime()

MERGE子句后面可以跟一个可选的ON CREATE SETON MATCH SET。在MERGE子句中,你使用节点的唯一标识符来合并节点。如果在查询过程中创建了节点,你可以使用ON CREATE SET子句定义应该设置的额外节点属性。相反,如果具有Person标签和name属性Amy的节点已经在数据库中存在,那么将调用ON MATCH SET子句。

列表 3.31 中的 Cypher 语句将首先合并一个具有name属性AmyPerson节点。如果数据库中不存在具有Person标签和name属性Amy的节点,则MERGE子句将创建一个节点并调用ON CREATE SET子句来设置节点上的locationcreatedAt属性。假设提到的节点已经在数据库中存在。在这种情况下,查询引擎不会创建任何新节点,它只会更新updatedAt节点属性,如ON MATCH SET子句中所述,将当前时间作为时间。Cypher 中的datetime()函数返回当前时间。

非常频繁地,你的导入查询看起来会像以下列表。

列表 3.32:合并两个节点并在它们之间合并关系的 Cypher 查询

MERGE (j:Person{name:"Jane"})
MERGE (s:Person{name:"Samay"})
MERGE (j)-[:FRIEND]->(s)

当将数据导入 Neo4j 时,最频繁使用的 Cypher 结构是首先分别合并节点,然后合并它们之间的任何关系。使用MERGE子句,你不必担心数据重复或多次查询执行。列表 3.32 中的语句确保数据库中有三个图模式:两个描述 Jane 和 Samay 的节点以及它们之间存在的FRIEND关系。你可以多次运行此查询,输出始终相同。

MERGE子句还支持合并无向关系。你可以在MERGE子句中省略关系方向的事实可能会有些令人困惑。在章节的开头,我提到你只能在 Neo4j 数据库中存储有向关系。让我们看看如果你运行以下查询会发生什么。

列表 3.33:合并两个节点并在它们之间合并无向关系的 Cypher 查询

MERGE (j:Person{name:"Alex"})
MERGE (s:Person{name:"Andrea"})
MERGE (j)-[f:FRIEND]-(s)
RETURN *

你可以观察到创建了两个新的 Person 节点。当你使用 MERGE 子句描述无向关系时,查询引擎首先尝试匹配关系,而忽略方向。实际上,它在两个方向上搜索关系。如果在任何方向上节点之间没有关系,它就会创建一个新的具有任意方向的关系。能够在 MERGE 子句中描述无向关系,使我们能够更方便地导入无向网络。如果你假设 FRIEND 关系是无向的——这意味着如果 Alex 和 Andrea 是朋友,那么 Andrea 也和 Alex 是朋友——那么你应该只存储他们之间的一条有向关系,并在执行图算法或查询时将其视为无向。你将在接下来的章节中了解更多关于这种方法的信息。现在,你只需要知道,在 MERGE 子句中描述无向关系是可能的。

注意:在创建或导入数据到 Neo4j 时,通常希望将 Cypher 语句拆分为多个 MERGE 子句,并分别合并节点和关系以提高性能。在合并节点时,最佳做法是在 MERGE 子句中仅包含节点的唯一标识符属性,并使用 ON MATCH SETON CREATE SET 子句添加额外的节点属性。

处理关系略有不同。如果两个节点之间最多只有一个类型的关系,例如在 FRIEND 示例中,那么不要在 MERGE 子句中包含任何关系属性。相反,使用 ON CREATE SETON MATCH SET 子句来设置任何关系属性。然而,如果你的图模型在节点对之间包含多个相同类型的关系,那么在 MERGE 语句中仅使用关系的唯一标识符属性,并设置任何附加属性,如之前讨论的那样。

3.2 使用 Cypher 导入 CSV 文件

你已经学习了基本的 Cypher 子句,这将帮助你开始。现在,你将学习如何从外部源导入数据。图数据库的两种常见输入数据结构是 CSV 和 JSON 格式。在本章中,你将学习如何导入类似 CSV 的数据结构。有趣的是,处理 CSV 文件和从关系型数据库导入数据几乎相同。在这两种情况下,你都在处理一个希望有命名字段的表。在本节中,你将定义唯一约束并将 Twitter 数据集导入到 Neo4j 图数据库中。

3.2.1 清理数据库

在继续之前,你需要清空数据库,因为你不想保留之前示例中的随机节点。

列表 3.34 删除数据库中所有节点和关系的 Cypher 查询

MATCH (n)
DETACH DELETE n

3.2.2 Twitter 图模型

在上一章中,你经历了一个图模型设计过程,并开发了图 3.8 所示的图模型。没有数据限制;你只是假设你可以获取任何相关数据。就像生活中的任何事物一样,你不可能总是得到你想要的,但可能会有一些之前未考虑到的额外数据。

03-08

图 3.8 初始 Twitter 图模型,你将导入

你将首先导入用户之间的关注网络。在初始图模型中,FOLLOWS关系有一个since属性。不幸的是,Twitter API 不提供FOLLOWS关系的创建日期,所以你将不得不从模型中移除它。有关 Twitter API FOLLOWS端点的更多信息,请参阅参考(mng.bz/W1PW)。

如果你后来发现一些假设并不成立,那么初始的图模型设计和提供的数据之间可能会有差异。这就是为什么图模型过程是迭代的。你开始时有一些假设,随着你获得更多信息,相应地改变图模型。另一方面,一个推文只能有一个作者的假设最终被证明是有效的。你没有考虑到一个用户也可以对某个推文进行回复,而不仅仅是转发。图模型已经更新,通过添加IN_REPLY_TO关系来支持存储推文是对另一个推文进行回复的信息。还应注意仅转发帖子与对转发添加评论之间的区别。Twitter 界面允许你通过引用推文(图 3.9)来对转发添加评论。

03-09

图 3.9 通过引用推文添加对转发的评论。

由于添加评论与仅转发帖子具有不同的语义,因此通过使用不同的关系类型来区分这两种情况是很重要的,如图 3.10 所示。

03-10

图 3.10 通过不同的关系类型区分转发和引用

通过明确区分转发和引用,你将更容易在图表中找到引用内容并分析它们的回应。例如,你可以使用自然语言处理技术来检测评论的情感,并检查哪些推文或用户最有可能收到带有积极或消极情感的评论。不幸的是,当我考虑将它们包含在我们的图表导入中时,我在抓取过程中没有获取任何引用推文,所以我们将跳过导入和分析它们。在初始导入中,你也将忽略推文的标签、提及和链接。

3.2.3 唯一约束

Neo4j 图形数据库模型被认为是无模式的,这意味着你可以在不定义图形模式模型的情况下添加任何类型的节点和关系。然而,你可以在你的图形模型中添加一些约束来确保数据完整性。在我的图形之旅中,我迄今为止只使用了唯一节点属性约束。使用唯一节点属性约束有两个好处。第一个好处是它确保给定节点属性值对于具有特定标签的所有节点都是唯一的。对于初学者来说,这个特性很有用,因为它会通知你并阻止可能破坏数据完整性的导入查询。定义唯一节点约束的另一个好处是它会自动在指定的节点属性上创建索引。通过在指定的节点属性上创建索引,你可以优化导入和分析 Cypher 查询的性能。你可以将唯一约束视为类似于关系数据库中的主键的概念,尽管不同之处在于 Neo4j 的唯一约束允许空值。

对于初始导入,你将定义两个唯一的节点约束。一个唯一约束将确保数据库中只能有一个具有特定id属性的User节点。第二个唯一约束保证具有Tweet标签的节点的id属性对于每个节点都是唯一的。

列表 3.35:定义两个唯一节点约束的 Cypher 查询

CREATE CONSTRAINT IF NOT EXISTS FOR (u:User) REQUIRE u.id IS UNIQUE;
CREATE CONSTRAINT IF NOT EXISTS FOR (p:Tweet) REQUIRE p.id IS UNIQUE;

3.2.4 LOAD CSV子句

Cypher 查询语言有一个LOAD CSV子句,它允许你打开并从 CSV 文件中检索信息。LOAD CSV子句可以检索本地 CSV 文件以及来自互联网的 CSV 文件。能够从互联网上检索 CSV 文件非常有用,因为你不必首先将 CSV 文件下载到你的本地计算机。我已经将所有相关的 CSV 文件存储在 GitHub 上(github.com/tomasonjo/graphs-network-science),以便更容易访问。LOAD CSV子句可以加载包含或不包含标题的 CSV 文件。如果存在标题,CSV 文件的每一行都将作为可以用于后续查询的映射数据结构可用。相反,当没有标题时,行将作为列表可用。LOAD CSV子句还可以与FIELDTERMINATOR子句结合使用,以设置自定义的分隔符,例如,当你处理制表符分隔值格式时。

要从特定的 CSV 文件中检索信息,你可以使用以下查询。

列表 3.36:从 CSV 文件中检索并显示信息的 Cypher 查询

LOAD CSV WITH HEADERS FROM "https:/ /bit.ly/39JYakC" AS row
WITH row
LIMIT 5
RETURN row

CSV 文件必须是公开可访问的,因为LOAD CSV子句不支持任何授权支持。列表 3.36 中的语句也使用了LIMIT子句。正如提到的,LIMIT子句用于限制你想要检索的结果数量。

重要的是要注意,LOAD CSV子句将所有值作为字符串返回,并且不尝试识别数据类型。您必须在 Cypher 导入语句中将值转换为正确的数据类型。

3.2.5 导入 Twitter 社交网络

我已经准备了五个包含以下信息的 CSV 文件:

  • 用户信息

  • 关注者网络

  • 关于推文及其作者的信息

  • 关于帖子与用户之间的MENTIONS关系信息

  • 关于帖子之间的RETWEETS关系信息

  • 关于帖子之间的IN_REPLY_TO关系信息

将图导入拆分为多个语句是一个好习惯;我可能已经准备了一个包含所有相关信息的单个 CSV 文件。然而,将导入拆分为多个语句以提高可读性和导入性能更有意义。如果您处理的是包含数百万个节点的图,那么建议拆分节点和关系的导入。在这种情况下,您只处理数千个节点,因此不必过多担心查询优化。我的经验法则是尽可能按节点标签和关系类型拆分导入查询。

首先,您将导入用户信息到 Neo4j 中。正如所述,所有数据都在 GitHub 上公开可用,因此不需要下载任何文件。用户信息的 CSV 结构如表 3.1 所示。

表 3.1 用户 CSV 结构

id name username createdAt
333011425 ADEYEMO ADEKUNLE King ADEYEMOADEKUNL2 2011-07-10T20:36:58
1355257214529892352 Wajdi Alkayal WajdiAlkayal 2021-01-29T20:51:28
171172327 NLP Excellence excellenceNLP 2021-01-29T20:51:28

您可以使用以下 Cypher 语句将用户信息导入 Neo4j 数据库。

列表 3.37 从 CSV 文件导入用户信息的 Cypher 查询

LOAD CSV WITH HEADERS FROM "https:/ /bit.ly/39JYakC" AS row
MERGE (u:User {id:row.id})
ON CREATE SET u.name = row.name,
              u.username = row.username,
              u.registeredAt = datetime(row.createdAt)

您本可以使用CREATE子句来导入用户信息。如果您从一个空数据库开始,并且信任我已经准备了一个没有重复的 CSV 文件,那么结果将是相同的。

在处理现实世界的数据集时,您通常无法承担假设数据是干净的这个奢侈。因此,编写可以处理重复或其他异常并且也是幂等的 Cypher 语句是有意义的。在列表 3.37 中的 Cypher 语句中,LOAD CSV子句首先从 GitHub 仓库中获取 CSV 信息。然后LOAD CSV遍历文件中的每一行并执行随后的 Cypher 语句。在这个例子中,它对 CSV 文件中的每一行执行MERGE子句与ON CREATE SET的组合。

练习 3.11

从数据库中检索五个随机用户以检查结果并验证用户导入过程是否正确。

目前,数据库中只有没有任何关系的节点;您将继续导入用户之间的FOLLOWS关系。包含关注者信息的 CSV 文件的结构如表 3.2 所示。

表 3.2 关注者 CSV 结构

source target
14847675 1355257214529892352
1342812984234680320 1355257214529892352
1398820162732793859 1355257214529892352

关注者的 CSV 文件只有两列。源列描述起始节点 ID,目标列描述关注者关系的终点节点 ID。在处理较大的 CSV 文件时,您可以使用IN TRANSACTIONS子句与 Cypher 子查询结合,将导入分成几个事务,其中x代表批量大小。将导入分成几个事务可以避免在执行大型导入时耗尽内存的烦恼。关注者的 CSV 文件有近 25,000 行。默认情况下,IN TRANSACTIONS子句将每 1,000 行分割一个事务。您应该使用事务批量子句与 Cypher 子查询结合,有效地将其分成 25 个事务。由于某种原因,在 Neo4j 浏览器中使用事务批量时需要预先添加:auto。在其他情况下(例如,使用 Neo4j Python 驱动程序导入数据时),您不需要预先添加:auto运算符,如下面的列表所示。

列表 3.38 从 CSV 文件导入关注者网络的 Cypher 查询

:auto LOAD CSV WITH HEADERS FROM "https:/ /bit.ly/3n08lEL" AS row    ❶
CALL {                                                              ❷
  WITH row                                                          ❸
  MATCH (s:User {id:row.source})
    MATCH (t:User {id:row.target})                                  ❹
  MERGE (s)-[:FOLLOWS]->(t)                                         ❺
} IN TRANSACTIONS                                                   ❻

❶ 预先添加:auto运算符并使用 LOAD CSV 子句,如常操作

❷ 使用 CALL 子句启动 Cypher 子查询

❸ 要使用封装查询中的任何变量,您需要使用 WITH 子句显式导入它们。

❹ 匹配起始用户和源用户

❺ 合并源用户和目标用户之间的关系

IN TRANSACTIONS子句表示子查询应每 1,000 行批量成单个事务。

列表 3.38 中的语句首先从 GitHub 上的 CSV 文件检索信息。要定义导入应分成多个事务,请使用CALL {}子句定义一个带有附加IN TRANSACTIONS子句的 Cypher 子查询。在封装查询中使用的任何变量都必须显式定义并使用WITH子句导入。Cypher 子查询中的查询步骤将为 CSV 文件中的每一行执行。对于每一行,它将匹配源和目标User节点。在这里,您假设所有User节点已经存在于数据库中。记住,如果MATCH子句找不到模式,它将跳过特定行的查询的其余部分执行。

在列表 3.38 中的语句中,例如,如果MATCH子句未找到源节点,Cypher 查询将跳过创建FOLLOWS关系。你可以通过使用MERGE子句而不是MATCH子句来识别源和目标User节点来避免这种限制。然而,有一个缺点:列表 3.38 中由MERGE子句创建的任何节点都只有id属性,没有其他信息,因为它们来自包含用户信息的 CSV 文件中缺失。

一旦确定了源节点和目标节点,查询将合并它们之间的FOLLOWS关系。使用MERGE子句,你确保数据库中从源到目标User节点的FOLLOWS关系恰好只有一个。这样,你就不必担心输入 CSV 文件中的重复或多次运行查询。另一个关键考虑因素是 Twitter 域中FOLLOWS关系的方向具有语义值。例如,如果用户 A 关注用户 B,这并不意味着用户 B 也关注用户 A。因此,你需要在MERGE子句中添加关系方向指示符,以确保正确导入关系。

注意:当你使用MATCH子句在 Cypher 导入查询中识别节点时,请注意,在导入过程中不会创建任何额外的节点。因此,在导入过程中也会跳过数据库中不存在的节点之间的关系。

练习 3.12

从数据库中检索五个FOLLOWS关系以验证导入过程。

接下来,你将导入推文及其作者。推文的 CSV 结构如表 3.3 所示。

表 3.3 Twitter 帖子 CSV 结构

id text createdAt author
12345 示例文本 2021-06-01T08:53:22 134281298
1399649667567 图数据科学很酷! 2021-06-01T08:53:18 54353345
13996423457567 探索社交网络 2021-06-01T08:45:23 4324323

Tweets CSV 文件的id列描述了将用作唯一标识符的 Twitter 帖子 ID。该文件还包括文本、创建日期以及作者 ID。CSV 文件中有 12,000 行,因此你将再次使用CALL {}子句与IN TRANSACTIONS子句结合用于批量处理。

列表 3.39 从 CSV 文件导入推文的 Cypher 查询

:auto LOAD CSV WITH HEADERS FROM "https:/ /bit.ly/3y3ODyc" AS row
CALL {                                                 ❶
  WITH row                                             ❷
  MATCH (a:User{id:row.author})                        ❸
  MERGE (p:Tweet{id:row.id})                           ❹
  ON CREATE SET p.text = row.text,
                p.createdAt = datetime(row.createdAt)
    MERGE (a)-[:PUBLISH]->(p)                          ❺
} IN TRANSACTIONS                                      ❻

❶ 使用 CALL 子句开始 Cypher 子查询

❷ 在 WITH 子句中显式导入行变量

❸ 匹配作者用户节点

❹ 合并推文节点并设置其他属性

❺ 合并用户与推文之间的关系

❻ 使用IN TRANSACTIONS子句来指示事务批量处理

如果你仔细观察,你会发现列表 3.39 中的查询结构与列表 3.38 中的查询相似。当你将任何关系导入数据库时,你很可能会匹配或合并源节点和目标节点,然后将它们连接起来。在第一步中,查询匹配User节点。接下来,你使用MERGE子句创建Tweet节点。尽管所有Tweet节点都需要创建,因为数据库中事先没有这些节点,但我仍然喜欢使用MERGE子句来保持查询的幂等性。这对用户来说是最好的体验,并且是一种优秀的实践。最后,创建用户和推文之间的关系。

练习 3.13

为了验证导入过程,检索三个随机Tweet节点的text属性。

现在你已经将推文和用户导入数据库,你可以导入推文和用户之间的MENTIONS关系。MENTIONS关系表示用户在其帖子中标记了另一个用户。包含此信息的 CSV 文件的结构如表 3.4 所示。

表 3.4 Mentions CSV 结构

post user
333011425 134281298
1355257214529892352 54353345
171172327 4324323

导入查询相对简单。首先,使用MATCH子句识别推文和提到的用户,然后MERGE它们之间的关系。

列表 3.40 从 CSV 文件导入MENTIONS关系的 Cypher 查询

LOAD CSV WITH HEADERS FROM "https:/ /bit.ly/3tINZ6D" AS row
MATCH (t:Tweet {id:row.post})    ❶
MATCH (u:User {id:row.user})     ❷
MERGE (t)-[:MENTIONS]->(u);      ❸

❶ 匹配推文节点

❷ 匹配提到的用户节点

❸ 合并推文与用户之间的关系

在最后两个导入查询中,你将导入额外的RETWEETSIN_REPLY_OF关系。这两个 CSV 文件的结构相同,如表 3.5 所示。

表 3.5 Retweets 和 In Reply To CSV 文件结构

source target
14847675 1355257214529892352
1342812984234680320 1355257214529892352
1398820162732793859 1355257214529892352

首先导入RETWEETS关系,如列表 3.41 所示。

列表 3.41 从 CSV 文件导入推文关系的 Cypher 查询

LOAD CSV WITH HEADERS FROM "https:/ /bit.ly/3QyDrRl" AS row
MATCH (source:Tweet {id:row.source})    ❶
MATCH (target:Tweet {id:row.target})    ❷
MERGE (source)-[:RETWEETS]->(target);   ❸

❶ 匹配源推文

❷ 匹配目标推文

❸ 合并源推文和目标推文之间的推文关系

向数据库添加关系的查询结构主要识别源节点和目标节点,然后在两者之间添加连接。在确定是否使用MATCHMERGE来识别节点时必须特别小心。如果你使用MATCH子句,则不会创建任何新节点,因此在导入过程中将忽略任何尚未在数据库中同时包含源节点和目标节点的关联。另一方面,如果你使用MERGE子句,可能会得到只有id但没有text属性或甚至没有与之关联的作者的Tweet节点。使用MERGE子句添加关系,你可以确保源节点和目标节点之间恰好有一个该类型的关系,无论底层数据中的连接出现多少次或导入查询运行多少次。有选项可以更改查询以导入一对节点之间超过一个关系类型,同时仍然使用MERGE子句。

练习 3.14

匹配数据库中一对推文之间的五个RETWEETS关系。然后,检查原始推文和被转发推文的内容。

最后的导入语句导入了IN_REPLY_TO关系,如下所示。它的结构几乎与导入RETWEETS关系相同:只是关系类型发生了变化。

列表 3.42 从 CSV 文件导入IN_REPLY_TO关系的 Cypher 查询

LOAD CSV WITH HEADERS FROM "https:/ /bit.ly/3b9Wgdx" AS row
MATCH (source:Tweet {id:row.source})       ❶
MATCH (target:Tweet {id:row.target})       ❷
MERGE (source)-[:IN_REPLY_TO]->(target);   ❸

❶ 匹配源推文

❷ 匹配目标推文

❸ 合并源推文和目标推文之间的回复关系

恭喜你,你已经将初始 Twitter 图导入 Neo4j!Neo4j 有一个特殊的程序,允许你检查和可视化数据库中存储的图模式。

列表 3.43 模式内省程序

CALL db.schema.visualization()

如果你运行 Neo4j 浏览器中指定的 3.43 列表中的模式内省程序,你应该会得到图 3.11 所示的方案可视化。

03-11

图 3.11 基于存储图生成的图模式可视化

如果你得到完全相同的图模式可视化,你就可以学习更多关于分析 Cypher 查询和图算法的知识,这些内容我们在下一章中会介绍。如果生成的模式不相同,请重新运行所有导入查询。

3.3 练习解决方案

练习 3.1 的解决方案如下。

列表 3.44 创建一个带有Person标签和nameage属性的节点的 Cypher 语句

CREATE (p:Person{name:'Tomaz', age: 34})

练习 3.2 的解决方案如下。

列表 3.45 创建代表员工及其雇主的两个节点,然后在这两个节点之间创建一个EMPLOYED_BY关系的 Cypher 语句

CREATE (p:Person{name:'Tomaz'}), (o:Organization {name: "Acme Inc"})
CREATE (p)-[e:EMPLOYED_BY]->(o)
RETURN *

练习 3.3 的解决方案如下。

列表 3.46 创建代表你居住的城市、国家和大陆的三个节点,然后使用IS_IN关系将它们连接起来

CREATE (city:City {name: "Grosuplje"}),
       (country: Country {name: "Slovenia"}),
       (continent: Continent {name: "Europe"})
CREATE (city)-[i1:IS_IN]->(country)
CREATE (country)-[u2:IS_IN]->(continent)
RETURN *

使用不同的节点和关系类型是可以的,因为练习没有指定你应该使用任何标签或类型。

练习 3.4 的解决方案如下。

列表 3.47 检索所有 Person 节点的 name 属性

MATCH (p:Person)
RETURN p.name AS name

练习 3.5 的解决方案如下。

列表 3.48 在代表你自己的节点和 Elaine 之间创建 FRIENDS 关系

MATCH (t:Person {name:'Tomaz'}), (e:Person {name:'Elaine'})
CREATE (t)-[f:FRIENDS]->(e)
RETURN *

练习 3.6 的解决方案如下。

列表 3.49 使用 OPTIONAL MATCH 来识别是否有任何连接到代表你自己的节点的 FRIENDS 关系

MATCH (t:Person {name:'Tomaz'})
OPTIONAL MATCH (t)-[f:FRIENDS]->(e)
RETURN *

练习 3.7 的解决方案如下。

列表 3.50 使用 SET 子句添加节点属性

MATCH (t:Person{name:'Tomaz'})
SET t.favouriteFood = 'Steak'
RETURN t

练习 3.8 的解决方案如下。

列表 3.51 使用 SET 子句添加一个二级 Reader 标签

MATCH (t:Person{name:'Tomaz'})
SET t:Reader
RETURN t

练习 3.9 的解决方案如下。

列表 3.52 删除代表你自己的节点

MATCH (t:Person{name:'Tomaz'})
DETACH DELETE t

练习 3.10 的解决方案如下。

列表 3.53 检索所有带有标签 Person 和名称属性 Alicia 的节点

MATCH (n:Person)
WHERE n.name = 'Alicia'
RETURN n

练习 3.11 的解决方案如下。

列表 3.54 检索五个 User 节点

MATCH (u:User)
WITH u
LIMIT 5
RETURN u

练习 3.12 的解决方案如下。

列表 3.55 检索五个 FOLLOWS 关系

MATCH (u:User)-[f:FOLLOWS]->(u2:User)
WITH u, f, u2
LIMIT 5
RETURN *

练习 3.13 的解决方案如下。

列表 3.56 检索三个随机 Tweet 节点的 text 属性

MATCH (t:Tweet)
WITH t.text AS text
LIMIT 3
RETURN text

练习 3.14 的解决方案如下。

列表 3.57 比较原始推文及其转发的文本

MATCH (retweet:Tweet)-[:RETWEETS]->(original:Tweet)
WITH original.text AS originalText, retweet.text AS retweetText
LIMIT 3
RETURN text

摘要

  • Cypher 语法使用圆括号,(),来封装一个节点。

  • Cypher 语法使用方括号,[],来封装一个关系。

  • 一个关系不能独立存在,需要用相邻节点来描述。

  • 在 Neo4j 中,所有关系都存储为有向的,尽管在查询时你可以忽略方向。

  • 不建议创建任何没有节点标签的节点。

  • 使用 CREATE 子句来创建数据。

  • MATCH 子句用于识别数据库中的现有模式。

  • 可以将 WHERE 子句与 MATCHWITH 子句结合使用,以指定各种过滤器。

  • 如果 MATCH 子句找不到指定的图模式,整个 Cypher 语句将返回无结果。

  • 当你不确定数据库中是否存在图模式时,可以使用 OPTIONAL MATCH,但你仍然想在查询输出中返回其他变量。

  • WITH 子句可以用于在 Cypher 语句中过滤、聚合、选择、分页或限制中间行。

  • SET 子句用于添加节点属性和标签。

  • REMOVE 子句用于删除节点属性和标签。

  • DELETE 子句用于删除节点和关系。

  • 如果你想要删除具有现有关系模式的节点,你需要使用 DETACH DELETE 子句。

  • MERGE 子句是 MATCHCREATE 子句的组合,确保指定的图模式存在于数据库中。

  • MERGE 子句常用于数据导入,因为它允许进行幂等查询和自动去重。

  • 在 Neo4j 中,您可以定义唯一约束,以确保特定节点标签的指定节点属性具有唯一值。

  • 建议分别将节点和关系导入数据库,以提高性能和提升数据质量。

  • 您可以使用db.schema.visualization()过程轻松评估现有数据的图模式。

4 探索性图分析

本章节涵盖

  • 使用 Cypher 查询语言探索图

  • 使用 Cypher 查询语言聚合数据

  • 使用存在子查询根据图模式进行过滤

  • 使用计数子查询统计图模式

  • 在使用单个 Cypher 语句中的多个子句时处理查询基数

本章节将教会你如何使用 Cypher 查询语言对导入的 Twitter 社交网络进行探索性数据分析。想象你正在担任社交媒体顾问,并希望尽可能多地发现洞察。正如任何分析一样,你首先从探索性数据分析开始,以了解你正在处理的数据概览。

我将展示我是如何收集数据的,以便你了解在本章中你将工作的数据。导入的 Twitter 社交网络是通过官方 Twitter API 检索的。我抓取了属于NLPKnowledge graph主题的推文。当时,我有关于被提及或发布推文的推文和用户的信息。接下来,我抓取了图中用户的其他元数据,例如他们的注册日期和关注关系。导入的 Twitter 子图中的所有用户都发布过推文或被提及过。我没有包括所有关注者,因为这会导致图爆炸,最终网络将包含几百万用户。

探索性图分析的一部分是统计网络中节点和关系的数量。对于 Twitter 网络,了解创建推文的时序也非常重要。在本章中,你将学习如何使用 Cypher 查询语言聚合基于时间的信息。作为探索性分析的最后一部分,你将检查数据集中的一些有趣节点,例如发布最多推文或被提及最多的用户。

现在我们已经概述了本章你将学习的内容,让我们从一些实际例子开始。要跟随本章中的例子,你必须有一个 Neo4j 数据库实例,并且已经按照第三章所述导入 Twitter 网络数据。

4.1 探索 Twitter 网络

探索性分析的目标是了解数据集,并教你 Cypher 查询语法,这将允许你聚合和过滤数据。我建议你使用 Neo4j 浏览器环境,它可以用来开发 Cypher 查询,并以表格和图形可视化的形式返回查询结果。

如果你打开 Neo4j 浏览器并选择右上角的数据库标签,它将显示一个简单的报告,指示数据库中节点和关系的数量和类型,如图 4.1 所示。

04-01

图 4.1 Neo4j 浏览器数据库报告

在第三章中展示的 Twitter 数据上传之后,数据库中应该总共有 15,654 个节点。正如你所知,节点被标记为 TweetUser。此外,还有 58,369 个连接分布在四种关系类型中。左侧工具栏中的节点标签和关系类型都是可点击的。例如,如果你点击 FOLLOWS 关系类型,工具将生成一个返回 25 个 FOLLOWS 关系样本的 Cypher 语句。

列表 4.1 生成的用于可视化 25 个 FOLLOWS 关系的 Cypher 查询

MATCH p=()-[r :FOLLOWS]->()
RETURN p LIMIT 25;

列表 4.1 中生成的语句返回结果作为 路径 数据对象。一系列连接的节点和关系可以用路径数据类型表示。Cypher 查询语法允许通过变量名引用路径,类似于节点和关系变量。

通过执行生成的 Cypher 语句,你应该得到与图 4.2 中所示类似的可视化。

04-02

图 4.2 关注者网络的一个子图

练习 4.1

作为练习,生成一个 Cypher 语句来可视化样本 RETWEETS 关系。你可以在左侧工具栏中点击关系类型,或者可以使用列表 4.1 中的语句作为模板并相应地更改关系类型。

4.2 使用 Cypher 查询语言聚合数据

聚合和计数数据点是所有数据分析的基础。在图分析中,你将首先学习如何计数各种图模式,如节点和关系的数量。如果你已经对关系数据库和 SQL 查询语言有一些经验,你会发现 Cypher 在聚合数据时遵循类似的语法。你将学习的第一个聚合函数是 count() 函数,它用于计数 MATCH 子句生成的行或值。

要计算数据库中节点的数量,你可以执行以下 Cypher 语句。

列表 4.2 计算节点数量

MATCH (n)                           ❶
RETURN count(n) AS numberOfNodes;   ❷

❶ 匹配数据库中的所有节点

❷ 返回节点的总数

图中总共有 15,654 个节点。count() 函数有两种变体:

  • count(*)—返回由 Cypher 语句生成的行数

  • count(variable or expression)—返回由表达式生成的非空值的数量

为了测试 count() 函数的两个变体,你需要计算用户节点的数量以及它们 registeredAt 属性的非空值。

列表 4.3 计算用户节点数量及其 registeredAt 属性的非空值

MATCH (u:User)
RETURN count(*) AS numberOfRows,
       count(u.registeredAt) AS numberOfUsersWithRegisteredAtDate

表 4.1 显示了列表 4.3 的结果。

表 4.1 列表 4.3 中 Cypher 语句的结果

numberOfRows numberOfUsersWithRegisteredAtDate
3,594 3,518

图中有 3,594 个User节点,但其中只有 3,518 个具有非空的registeredAt属性。图中缺少 76 个用户的注册日期信息。在准备数据集摘要时,你通常将缺失值的数量以比率而不是绝对数的形式呈现。在 Neo4j 中,你必须小心,因为当你用整数除以整数时,结果也将是整数。为了避免这个问题,你可以将其中一个变量转换为浮点数据类型。

执行以下 Cypher 语句以评估User节点registeredAt属性非空值的比率。

列表 4.4 计算registeredAt节点属性非空值的比率

MATCH (u:User)                                                            ❶
WITH count(*) AS numberOfRows,
     count(u.registeredAt) AS usersWithRegisteredAtDate                   ❷
RETURN toFloat(usersWithRegisteredAtDate) / numberOfRows * 100 AS result  ❸

❶ 匹配数据库中的所有用户

❷ 计算所有用户数量以及非空registeredAt属性的数量

❸ 将其中一个变量转换为浮点型以避免除以两个整数

你会看到 97.88%的用户具有非空的registeredAt属性值。如果你在列表 4.4 中忘记将usersWithRegisteredAtDatenumberOfRows转换为浮点型,结果将是 0。

注意:在 Neo4j 中,当你将一个整数值除以另一个整数值时,结果也将是整型数据。如果你想结果为浮点型,例如在列表 4.4 中的比率示例,你需要使用toFloat()函数将其中一个变量转换为浮点型。

练习 4.2

计算缺失值的比率,即Tweet节点createdAt节点属性的百分比,结果应该是非空值的比率除以推文的计数。

练习 4.2 的正确答案是Tweet节点的createdAt属性没有缺失值。

就像你可能从其他数据项目中习惯的那样,你通常更频繁地按特定值聚合或计数值。对于熟悉 SQL 聚合的人来说,你可以在GROUP BY语句中定义分组键。分组键是用于将值分组到聚合函数中的非聚合表达式。Cypher 中的聚合与 SQL 中的聚合不同。在 Cypher 中,你不需要显式指定分组键。一旦在 Cypher 语句中使用任何聚合函数,WITHRETURN子句中的所有非聚合列都成为分组键。使用 Cypher 查询语言时,分组键是隐式定义的,因为你不需要在聚合函数之后显式添加GROUP BY语句。

假设你想要按节点标签分组计算节点数量。提取节点标签的函数是labels()。你只需要提供labels()函数作为分组键,并配合一个将计算节点数量的聚合函数。

列表 4.5 按标签计数节点

MATCH (n)
RETURN labels(n) AS labels,
       count(n) AS count;

表 4.2 显示了每个标签的节点数量。

表 4.2 按标签计数节点

labels count
["用户"] 3594
["推文"] 12060

目前,图中只有两种类型的节点。你可以观察到列表 4.5 中的 Cypher 语句返回了节点标签列表。列表数据类型表示你可以在单个节点上拥有多个标签。在网络分析中,当你想要通过标记相关节点子集来加速后续查询时,分配二级节点标签是有帮助的。如前所述,你可能注意到缺少GROUP BY语句。在 Cypher 中,你不需要显式指定分组键。一旦使用聚合函数,所有非聚合结果列都成为分组键。

注意:使用 Cypher 查询语言,分组键是隐式定义的,这意味着在WITHRETURN子句中的所有非聚合列自动成为分组键。

练习 4.3

按关系类型计数关系数量。要按类型分组计数关系数量,你首先需要使用 Cypher 语法描述一个关系模式。请注意,没有相邻节点的描述,你不能描述一个关系。因为你对计数所有关系类型感兴趣,所以在 Cypher 模式中你不能指定任何节点标签或关系类型。在语句的最后部分,你使用type()函数提取关系类型,并将其与count()聚合函数结合作为分组键。

练习 4.3 的解决方案生成了表 4.3 所示的输出。

表 4.3 按类型分组的关系的计数

relationshipType count
PUBLISH 12,060
FOLLOWS 24,888
RETWEETS 8,619
MENTIONS 12,379
IN_REPLY_TO 423

PUBLISH关系的数量与Tweet节点数量相同。在 Twitter 社交网络中,一个TWEET恰好有一个作者,由PUBLISH关系表示。

有趣的是,在 12,060 条推文中,有 8,619 条是转发,423 条是回复。只有大约 30%的推文是原创内容。这并不罕见;例如,Joshua Hawthorne 等人(2013 年)的研究表明,尽管普通用户每条推文的转发数量不高,但更受欢迎的账户获得了大量的转发。此外,当研究人员检查前美国总统的推文时(Minot 等人,2021 年),转发的数量至少比通常高一个数量级。因此,由于重要账户的推文转发量很大,平台上转发的数量比原创推文多。

有一点令人惊讶的是,提及的数量比推文多。我没有手动解析提及的信息,因为这些信息是由官方 Twitter API 自动提供的。我还注意到,当用户转发其他用户的推文时,他们会被自动提及。

练习 4.4

检查转发的文本并与原始推文的文本进行比较。使用 LIMIT 子句将结果数量限制为 1。

练习 4.4 的解决方案生成了表 4.4 中所示的输出。

表 4.4 推文和转发文本的单个比较

retweetText originalText
RT @Eli_Krumova: 5 Best Practices: Writing Clean & Professional #SQL #Code https://t.co/Y4DepLfOOn v/ @SourabhSKatoch #DataScience #AI #ML... 5 Best Practices: Writing Clean & Professional #SQL #Code https://t.co/Y4DepLfOOn v/ @SourabhSKatoch #DataScience #AI #ML #MachineLearning #IoT #IIoT #IoTPL #Python #RStats #Cloud #CyberSecurity #Serverless #RPA #NLP #programming #coding #100DaysOfCode #DEVCommunity #CodeNewbie https://t.co/ma03V8btZBhttps://t.co/TOnwwHgaHQ

一个立即明显的事情是,转发文本被截断到固定长度,并且并不总是包含原始推文的完整文本。另一个更微妙的不同之处在于,转发文本以 RT 开头,后面跟着原始作者的昵称。看起来 Twitter 自动在转发中添加原始用户的昵称,并将其视为提及——我之前并不知道这种情况。在深入研究图算法之前,始终进行探索性图分析是一个好的实践,以发现此类异常。

练习 4.5

对于那些更倾向于视觉的人来说,尝试可视化一个图模式,其中用户从另一个用户那里转发了帖子。包括原始和被转发推文的 MENTION 关系。遵循以下提示来帮助你构建所需的 Cypher 语句:

  • 匹配一个描述转发、原始推文及其作者的图模式。

  • 使用 WITH 子句与 LIMIT 子句结合,将结果限制为单个描述的模式。

  • 分别匹配原始和被转发推文的 MENTION 关系。

  • 在 Neo4j 浏览器中可视化网络最简单的方法是返回一个或多个路径对象。

这个练习稍微复杂一些,所以请一步一步地构建最终的 Cypher 语句。你可以在每一步之后检查结果,以确保你正确地描述了所需的图模式。练习 4.5 的解决方案在 Neo4j 浏览器中产生了图 4.3 所示的网络可视化。

04-03

图 4.3 默认情况下,转发也会提及原始推文作者。

4.2.1 时间聚合

与任何数据集一样,了解数据点的时序至关重要。UserTweet 节点都包含 datetime 属性。首先,你将评估推文的时间窗口。你可以使用 min()max() 函数对 datetime 属性进行操作,以获取最早和最后日期值。

列表 4.6 获取推文的最早和最后创建日期值

MATCH (n:Tweet)
RETURN min(n.createdAt) AS earliestDate, max(n.createdAt) as lastDate

数据集中的第一条推文是在 2016 年 8 月 12 日,最后一条是在 2021 年 6 月 1 日。第一条和最后一条推文之间有五年的时间跨度。虽然这个信息不错,但并不非常具有描述性。为了更好地感受时间窗口,你将计算按年份分布的推文分布。

在 Cypher 中,日期时间属性的行为像一个对象。你可以使用以下 Cypher 语法来访问日期时间属性,如年份和月份。

列表 4.7 提取日期时间属性

MATCH (t:Tweet)
WITH t LIMIT 1
RETURN t.createdAt.year AS year,
       t.createdAt.month AS month,
       t.createdAt.day AS day,
       t.createdAt.epochSeconds AS epochSeconds;

表 4.5 显示了此样本推文的日期时间属性。

表 4.5 样本推文的日期时间属性

year month day epochSeconds
2021 6 1 1622537602

你可以使用日期时间属性与聚合函数结合使用。

练习 4.6

计算按创建年份分组的推文分布。记住,Cypher 使用隐式分组键聚合,所以你只需要在RETURN语句中添加year列和count运算符。

练习 4.6 的解决方案在表 4.6 中产生输出。

表 4.6 按创建日期分布的推文

year count
2021 12029
2020 19
2019 6
2018 3
2016 3

即使第一条推文和最后一条推文之间的时间窗口是五年,几乎所有推文都是在 2021 年创建的。让我们进一步深入分析。

练习 4.7

结合使用MATCH子句和WHERE子句来选择所有在 2021 年创建的推文。你可以像过滤其他节点属性一样过滤日期时间属性。在下一步中,计算推文按创建月份的分布。使用创建年份和月份作为分组键。

到现在为止,你可能已经养成了在RETURNWITH子句中添加分组键作为非聚合值的习惯。此外,你只需要小心使用WHERE子句来匹配仅在 2021 年创建的推文。

练习 4.7 的解决方案在表 4.7 中产生输出。

表 4.7 按创建日期分布的推文

year month count
2021 6 2695
2021 5 8507
2021 4 376
2021 3 432
2021 2 8
2021 1 11

大约 93%(12,060 条中的 11,202 条)的推文是在 2021 年 5 月和 6 月创建的。

练习 4.8

在你继续本章的其余部分之前,我想向你提出一个挑战。你能准备一个 Cypher 语句,返回创建推文数量最高的前四天吗?虽然你没有看到这个具体的例子,但你已经有了一些使用构建此 Cypher 语句所需的所有子句的经验。以下是一些应该有帮助的提示:

  • 首先匹配所有推文。

  • 使用创建年份、月份和日期作为分组键,以及count()聚合。

  • 使用ORDER BY子句按计数降序排序结果。

  • 使用LIMIT子句仅返回前四天。

练习 4.8 旨在测试你对 Cypher 中的隐式分组聚合的理解,并使用之前讨论的一些子句。请花几分钟时间尝试自己解决它。我建议你在查询的每一步之后返回结果,以确定你是否走上了正确的道路。

练习 4.8 的解决方案在表 4.8 中产生输出。

表 4.8 按创建日期分布的推文

year month day count
2021 5 31 6185
2021 6 1 2695
2021 5 30 1847
2021 5 28 62

有趣的是,你从一个五年的时间窗口开始,通过逐步深入挖掘,最终将其缩小到只有四天。数据集中大部分推文都是在 5 月 30 日和 6 月 1 日之间创建的。这些信息将帮助你评估在这个时间框架内推文和提及的数量。

4.3 过滤图模式

现在,你将更深入地研究提及的网络。你已经知道有 12,379 个MENTIONS关系,但现在,你想要确定被提及的不同用户数量。你将如何构造 Cypher 语句来检索被提及的不同用户数量?作为一个初学者,我的第一个想法是使用以下语句。

列表 4.8 计算被提及的用户出现的次数

MATCH (u:User)<-[:MENTIONS]-(:Tweet)
RETURN count(u) AS countOfMentionedUsers;

初看,列表 4.8 中的语句似乎是有效的。你匹配了被提及的用户,然后返回了用户的计数。但是count()函数并不计算不同用户的数量;它计算用户变量非空的出现次数。你实际上计算了包含User节点从Tweet节点起源的MENTIONS关系的图模式的数量,即 12,379。计算被提及的不同用户数量的方法之一是使用distinct前缀。以下列表中显示的distinct前缀用于计算引用变量或表达式的唯一值的数量。

列表 4.9 计算被提及的不同用户数量

MATCH (u:User)<-[:MENTIONS]-(:Tweet)
RETURN count(u) AS numberOfOccurences,
       count(distinct u) AS numberOfDistinctUsers;

表 4.9 显示了列表 4.9 的结果。

表 4.9 推文中提及的不同用户出现的次数和计数

numberOfOccurences numberOfDistinctUsers
12,379 1,632

总共有 1,632 个不同的用户至少被提及一次。在进行任何查询聚合时,您还必须考虑到查询的基数以及您实际在计算什么。"基数" 是操作输入流中的行或记录数。Cypher 对每行输入执行特定操作。在列表 4.9 中,MATCH 子句产生了 12,379 行。这些行随后被用作 count 操作的输入。使用 count(u) 操作符,您计算 u 引用变量的非空值数量。由于 MATCH 子句将为 u 变量产生无空值,因此 count(u) 操作的结果是 12,379。

注意:查询的基数也会影响其性能。您希望将基数保持在尽可能低,以实现最佳执行速度。在 Neo4j 中,您可以在 Cypher 语句前加上 PROFILE 子句来比较查询的性能。有关 PROFILE 子句的更多信息,请参阅文档:http://mng.bz/84pD。

您无需展开所有 MENTIONS 关系以获取至少在一条推文中被提及的用户列表。使用 WHERE 子句中的 存在性子查询,您可以在图模式上过滤。存在性子查询可用于确定指定的模式在图中至少存在一次。您可以将其视为与图模式结合的 WHERE 子句的扩展或升级,其中您可以在子查询中引入新的引用变量,甚至使用其他子句,如 MATCH。子查询以大括号 {} 开头和结尾。您可以使用外部查询中的任何变量来描述图模式;然而,在子查询中引入的任何新变量都不会传递到主查询中。

列表 4.10 计算推文中被提及的不同用户数量

MATCH (n:User)
WHERE EXISTS { (n)<-[:MENTIONS]-() }
RETURN count(n) AS numberOfDistinctUsers;

列表 4.10 中的 Cypher 语句产生的不同用户数量与列表 4.9 中的查询结果相同,并且性能也更好。列表 4.10 中使用的语法对于在网络上找到至少属于一个描述的图模式的节点非常有用。在这个例子中,您不在乎一个用户被提及一次还是数百次——您只想匹配至少被提及一次的不同用户。

注意 存在性子查询通常性能更好,因为它们不需要展开所有关系。例如,列表 4.9 中的 Cypher 语句必须展开 12,379 个MENTIONS关系,然后使用distinct运算符来检索不同User节点的计数。另一方面,列表 4.10 中指定的存在性子查询不需要展开所有MENTIONS关系,因为只要存在一个MENTIONS关系,表达式就是真实的。如果一个User节点有 100 个MENTIONS关系,列表 4.9 中的 Cypher 语句将展开所有 100 个关系,而列表 4.10 中指定的存在性子查询将满足找到第一个MENTIONS关系,因此将忽略其他 99 个。

练习 4.9

计算至少发布过一条推文的独立用户数量。

在否定图模式时,将存在性子查询与图模式结合使用也非常有帮助。你可以匹配前一个例子中的整个模式,并使用distinct来获取正确的计数。然而,当你想要否定一个图模式时,你无法在MATCH子句中使用它。因此,否定图模式是使用WHERE子句中的存在性子查询来否定图模式的典型例子。

在以下列表中显示的示例中,你将计算被提及但自己没有发布任何推文的独立用户数量。你必须否定出度的PUBLISH关系来过滤出没有任何推文的用户。

列表 4.11 计算被提及但尚未发布任何推文的用户数量

MATCH (u:User)
WHERE EXISTS { (u)<-[:MENTIONS]-() } AND
  NOT EXISTS { (u)-[:PUBLISH]->() }
RETURN count(*) AS countOfDistinctUsers

在我们的数据集中,大约一半的不同用户(1,632 个中的 809 个)在推文中被提及,但他们自己没有发布任何推文。如列表 4.11 所示,你可以轻松地组合多个图模式谓词来过滤出符合描述的图模式的节点。

如前所述,你还可以在存在性子查询中引入新的引用变量。例如,如果你想计算推文中被提及的用户数量并折扣转发模式中的提及,你需要引入一个新的引用变量。在下面的列表中,你将使用存在性子查询来计算推文中被提及的用户数量并忽略转发提及模式。

列表 4.12 使用存在性子查询计算被提及在推文中的用户数量,并折扣转发提及模式

MATCH (u:User)<-[:MENTIONS]-(tweet:Tweet)                                  ❶
WHERE NOT EXISTS {
  (original)<-[:PUBLISH]-(u)<-[:MENTIONS]-(tweet)-[:RETWEETS]->(original)
}                                                                          ❷
RETURN count(distinct u) AS countOfUsers                                   ❸

❶ 使用 MATCH 子句来识别用户被推文提及的模式

❷ 使用存在性查询来否定由于转发模式而存在的 MENTION 关系的图模式

❸ 使用 distinct 运算符来返回描述的图模式中不同用户的数量

你需要在列表 4.12 中使用存在子查询才能引入引用变量original。对original的引用是必要的,因为你只想折扣那些属于转发模式的特定MENTION关系。

列表 4.12 中语句的结果是 1,206。因此,大约 26%(1,632 中的 426)被提及的用户只有进入的MENTION关系,因为他们的帖子被转发。有趣的是,如果你不考虑转发,大约 33%(3,594 中的 1,206)的所有用户在推文中被提及。如果你完成了 4.9 练习,你应该知道大约 75%(3,594 中的 2,764)的所有用户至少发布过一条推文。

练习 4.10

找出转发独特推文最多的前五个用户。为了使您更容易,我已经准备了一个需要填写的模板 Cypher 语句,如表 4.10 所示。

列表 4.13 练习 4.10 的模板查询

MATCH (n:User)-[:PUBLISH]->(t:Tweet)              ❶
_Fill in the WHERE_
WITH n, count(*) AS numberOfRetweets              ❷
_Fill in the ORDER BY_
RETURN n.username AS user, numberOfRetweets       ❸
_Fill in the LIMIT_

❶ 使用 WHERE 子句与图模式的组合来过滤被转发的推文。被转发的推文有一个进入的 RETWEETS 关系。

❷ 使用 ORDER BY 子句按 numberOfRetweets 降序排序

❸ 使用 LIMIT 返回仅前五个用户

通过解决练习 4.10,你应该得到表 4.10 中所示的结果。

表 4.10 转发次数最多的前五个用户

user numberOfRetweets
"IainLJBrown" 754
"SuzanneC0leman" 314
"Eli_Krumova" 31
"Paula_Piccard" 31
"Analytics_699" 26

IainLJBrown 似乎到目前为止转发推文最多。第二位,有 354 条被转发的推文,是 SuzanneC0leman。你可能认为这些用户是影响者,因为他们发布很多,但他们的粉丝也大量转发他们的帖子。之后,转发次数的数量级下降到只有 Eli_Krumova 和 Paula_Piccard 的 31 条转发推文。

4.4 计数子查询

接下来,我将向您展示如何方便地使用 Cypher 计数图模式。尽管你已经知道如何计数和过滤图模式,但在计数各种图模式时,有一个简单但高效的语法需要记住。例如,如果你想获取提及最多的前五个用户,可以使用以下 Cypher 语句。

列表 4.14 检索提及最多的前五个用户

MATCH (u:User)<-[:MENTIONS]-(:Tweet)
WITH u, count(*) AS mentions
ORDER BY mentions DESC LIMIT 5
RETURN u.username AS user, mentions

列表 4.14 中的陈述没有问题;然而,你经常会在一个查询中执行多个聚合操作。在查询中执行多个聚合操作时,你必须非常注意查询基数(查询中的中间行数)。在计数一个节点有多少关系时,不增加基数的一个简单但非常有效的语法是使用count {}运算符并描述你想要计数的所需图模式,如下面的列表所示。

列表 4.15 通过不增加主查询基数,方便地检索提及最多的前五个用户

MATCH (u:User)
WITH u, count { (u)<-[:MENTIONS]-() } AS mentions
ORDER BY mentions DESC LIMIT 5
RETURN u.username AS user, mentions

你可能已经注意到,Cypher 语法中的一个常见主题是将图模式用大括号括起来,并根据你的用例在前面添加一个所需的 Cypher 子句,如countEXISTSCALL。列表 4.15 中 Cypher 语句的结果显示在表 4.11 中。

表 4.11 提及最多的前五个用户

user mentions
"IainLJBrown" 3646
"SuzanneC0leman" 673
"Analytics_699" 476
"Paula_Piccard" 460
"Eli_Krumova" 283

到目前为止,被提及最多的用户是 IainLJBrown。如果你像我一样,你可能想知道这些被提及者的分布情况。这个用户是否经常被转发,提及他的帖子是否经常被转发,或者人们只是喜欢提及他?从练习 4.10 的结果中,你已经知道他有 754 篇被转发的帖子。

4.5 连续多个聚合

如前所述,在连续执行多个聚合操作时,必须注意中间基数。例如,假设你有连续的两个MATCH子句。

列表 4.16 多个MATCH子句如何影响查询基数

MATCH (u:User)
MATCH (t:Tweet)
RETURN count(*) AS numberOfRows,
       count(u) AS countOfUsers,
       count(t) AS countOfTweets

表 4.12 展示了在连续执行多个MATCH子句而不进行任何中间步骤时,查询基数如何爆炸。

表 4.12 不减少基数的情况下连续多个聚合

numberOfRows countOfUsers countOfTweets
43,343,640 43,343,640 43,343,640

你已经知道这个结果完全没有意义。首先,用户和推文的数量是相同的,你肯定没有 4300 万个节点在图中。那么为什么你会得到这些结果?每个MATCHOPTIONAL MATCH会产生一定数量的行。任何后续的MATCHOPTIONAL MATCH子句都将根据前一个MATCH子句产生的行数执行多次。列表 4.16 中的第一个MATCH产生了 3,594 行。然后第二个MATCH会针对每个产生的行分别执行。实际上,第二个MATCH将会执行 3,594 次。我们的图中共有 12,060 条推文,所以如果你找到 12,060 和 3,594 的乘积,你将得到 4300 万行。

如何避免这个问题?在这个例子中,你可以在第二个MATCH子句之前将基数减少到 1,这样第二个MATCH子句就只会执行一次。你可以使用任何聚合函数来减少基数。假设你想要计算图中用户和推文的数量。在这种情况下,你可以在第一个MATCH子句之后使用count()函数来减少基数。

列表 4.17 在多个MATCH子句之间减少基数

MATCH (u:User)
WITH count(u) AS countOfUsers
MATCH (t:Tweet)
RETURN count(*) AS numberOfRows, countOfUsers, count(t) AS countOfTweets  ❶

❶ 在执行后续的MATCH子句之前将基数减少到 1

表 4.13 展示了如何通过执行聚合操作以减少查询的中间基数来产生有效结果。

表 4.13 按顺序进行多次聚合,同时减少中间基数

numberOfRows countOfUsers countOfUsers
12,060 3,594 12,060

通过在第一次 MATCH 之后减少中间基数到 1,您可以确保任何后续的 MATCH 子句只会执行一次。这将有助于您提高查询性能并获得准确的结果。另一个有助于您控制基数的方法是使用如列表 4.15 所述的 count {} 操作符。

练习 4.11

计算用户 IainLJBrown 的提及分布。提及可以有以下三种形式:

  • 某人转发 IainLJBrown 的帖子

  • 某人发布了一条提及 IainLJBrown 的原创推文

  • 某人转发提及 IainLJBrown 的帖子

确保在每条 MATCHOPTIONAL MATCH 子句之后减少基数。因为您事先不知道提及 IainLJBrown 的内容是否属于所有三个类别,我建议您在计算提及分布时使用 OPTIONAL MATCH

练习 4.11 的解答如下。

列表 4.18 计算用户 IainLJBrown 的提及分布

MATCH (u:User)
WHERE u.username = "IainLJBrown"                                  ❶
OPTIONAL MATCH (u)-[:PUBLISH]->(rt)<-[:RETWEETS]-()
WITH u, count(rt) AS numberOfRetweets                             ❷
OPTIONAL MATCH (u)<-[:MENTIONS]-(t)
WHERE NOT (t)-[:RETWEETS]->()
WITH u, numberOfRetweets, count(t) AS mentionsInOriginalTweets    ❸
OPTIONAL MATCH (u)<-[:MENTIONS]-(ort)
WHERE (ort)-[:RETWEETS]->() AND NOT (ort)-[:RETWEETS]->()<-[:PUBLISH]-(u)
WITH u, numberOfRetweets, mentionsInOriginalTweets,
    count(ort) AS mentionsInRetweets
RETURN u.username AS user, numberOfRetweets,
       mentionsInOriginalTweets, mentionsInRetweets               ❹

❶ 识别用户

❷ 计算他们帖子收到的转发数量

❸ 计算原始帖子中的提及数量

❹ 计算转发中的提及数量,并排除作者帖子的转发

表 4.14 显示了 IainLJBrown 的结果分布。

表 4.14 IainLJBrown 的提及分布

user numberOfRetweets mentionsInOriginalTweets mentionsInRetweets
"IainLJBrown" 3643 2 1

您必须在每个 OPTIONAL MATCH 子句之后直接使用 count() 操作符,而不仅仅是最后,这是一个非常重要的细节。这样,您在每个 OPTIONAL MATCH 子句之后将中间基数减少到 1,并且您的计数不会爆炸。您还有其他几种方法可以得到这个结果,所以如果您的查询略有不同但产生相同的结果,那么一切都很好。几乎所有关于用户 IainLJBrown 的提及都来自他们的帖子被转发。他们只被提及在两篇原始推文中,其中一篇很可能被转发了一次。如果您考虑练习 4.10 的信息,您知道他的 754 篇帖子被转发了 3,643 次。在这个 Twitter 子图中,他绝对可以被视为一个有影响力的人。

练习 4.12

获取发布最多推文或转发的 top 五个用户。使用 count {} 操作符。

恭喜!通过完成所有练习,您已经对 Cypher 聚合和过滤有了相当的了解。

4.6 练习解答

练习 4.1 的解答如下。

列表 4.19 生成的 Cypher 查询,用于可视化 25 条 RETWEETS 关系

MATCH p=()-[r:RETWEETS]->()
RETURN p LIMIT 25;

练习 4.2 的解答如下。

列表 4.20 计算推文 createdAt 节点属性的 non-null 值比率

MATCH (u:Tweet)
WITH count(*) AS numberOfRows,
count(u.createdAt) AS tweetsWithCreatedAtDate
RETURN toFloat(tweetsWithCreatedAtDate) / numberOfRows * 100 AS result

练习 4.3 的解决方案如下。

列表 4.21 按类型分组计算关系的数量

MATCH ()-[r]->()
RETURN type(r) AS relationshipType, count(r) AS countOfRels

练习 4.4 的解决方案如下。

列表 4.22 比较转发推文和原始推文的文本属性

MATCH (rt:Tweet)-[:RETWEETS]->(t:Tweet)
RETURN rt.text AS retweetText, t.text AS originalText
LIMIT 1

练习 4.5 的解决方案如下。

列表 4.23 可视化一个用户从另一个用户转发帖子的单个图模式

MATCH p=(:User)-[:PUBLISH]->(rt:Tweet)-[:RETWEETS]->
➥ (t:Tweet)<-[:PUBLISH]-(:User)
WITH p, rt, t LIMIT 1
MATCH prt=(rt)-[:MENTIONS]->()
MATCH pt=(t)-[:MENTIONS]->()
RETURN p,pt,prt

练习 4.6 的解决方案如下。

列表 4.24 通过年份计算推文的分布

MATCH (t:Tweet)
RETURN t.createdAt.year AS year, count(*) AS count
ORDER BY year DESC

练习 4.7 的解决方案如下。

列表 4.25 通过月份计算 2021 年创建的推文的分布

MATCH (t:Tweet)
WHERE t.createdAt.year = 2021
RETURN t.createdAt.year AS year,
       t.createdAt.month AS month,
       count(*) as count
ORDER BY year DESC, month DESC

练习 4.8 的解决方案如下。

列表 4.26 通过创建推文的数量确定前四天

MATCH (t:Tweet)
WITH t.createdAt.year AS year,
     t.createdAt.month AS month,
     t.createdAt.day AS day,
     count(*) AS count
ORDER BY count DESC
RETURN year, month, day, count LIMIT 4

练习 4.9 的解决方案如下。

列表 4.27 计算至少发布了一条推文的独特用户的数量

MATCH (u:User)
WHERE EXISTS { (u)-[:PUBLISH]->() }
RETURN count(*) AS countOfUsers

练习 4.10 的解决方案如下。

列表 4.28 找到转发最多独特推文的顶级五名用户

MATCH (n:User)-[:PUBLISH]->(t:Tweet)
WHERE EXISTS { (t)<-[:RETWEETS]-() }
WITH n, count(*) AS numberOfRetweets
ORDER BY numberOfRetweets DESC
RETURN n.username AS user, numberOfRetweets
LIMIT 5

练习 4.11 的解决方案如下。

列表 4.29 计算用户 IainLJBrown 的提及分布

MATCH (u:User)
WHERE u.username = "IainLJBrown"                                   ❶
OPTIONAL MATCH (u)-[:PUBLISH]->(rt)<-[:RETWEETS]-()
WITH u, count(rt) AS numberOfRetweets                              ❷
OPTIONAL MATCH (u)<-[:MENTIONS]-(t)
WHERE NOT (t)-[:RETWEETS]->()
WITH u, numberOfRetweets, count(t) AS mentionsInOriginalTweets     ❸
OPTIONAL MATCH (u)<-[:MENTIONS]-(ort)
WHERE (ort)-[:RETWEETS]->() AND NOT (ort)-[:RETWEETS]->()<-[:PUBLISH]-(u)
WITH u, numberOfRetweets, mentionsInOriginalTweets,
➥ count(ort) AS mentionsInRetweets
RETURN u.username AS user, numberOfRetweets,
       mentionsInOriginalTweets, mentionsInRetweets                ❹

❶ 识别用户

❷ 统计他们帖子收到的转发次数

❸ 统计原始帖子中的提及次数

❹ 统计在转发中提及的次数,并排除作者帖子的转发

练习 4.12 的解决方案如下。

列表 4.30 获取发布最多推文或转发的顶级五名用户

MATCH (u:User)
RETURN u.username AS username,
       count{ (u)-[:PUBLISH]->() } AS countOfTweets
ORDER BY countOfTweets DESC
LIMIT 5

摘要

  • 路径数据对象包含一系列连接的节点和关系。

  • Cypher 聚合使用隐式分组键。

  • 一旦使用聚合函数,所有非聚合列都成为分组键。

  • 存在性子查询可以帮助您通过图模式高效地过滤。

  • 存在性子查询在您想要否定图模式时特别有用。

  • 基数是操作输入流中行或记录的数量。

  • 日期时间对象有多个属性,您可以使用它们来检索年、月、日或纪元信息。

  • 可以使用distinct运算符来计算不同模式、节点、关系或属性的数目。

  • 将一个整数除以一个整数将返回一个整数。因此您需要使用toFloat函数将其中一个值转换为浮点数,以避免返回整数。

  • 可以使用count运算符来计算非空属性或表达式的数量。

  • 当连续执行多个子句或聚合函数时,您必须注意中间查询的基数。

  • 计数子查询在您想要计数图模式而不影响主查询基数时很有用。

  • 您可以在任何 Cypher 语句前加上PROFILE子句,通过检查总数据库击打来评估其性能。

5 社交网络分析简介

本章涵盖

  • 展示随机和无尺度网络度分布

  • 使用度量来表征网络

  • 介绍 Neo4j 图数据科学库

  • 使用原生投影来投影内存中的图

  • 检查图的社区结构

  • 使用 PageRank 在网络上查找影响者

社交网络分析是使用图理论和算法研究网络结构和节点角色的过程。最早的网络科学作家之一是匈牙利作家弗里吉斯·卡林蒂(Frigyes Karinthy)。他最重要的作品之一是短篇小说“Láncszemek”,在其中他描述了尽管我们认为世界很广阔,但实际上它非常小。这个故事最初是用匈牙利语写的,但亚当·马卡伊(Adam Makkai)准备了一个英文翻译(mng.bz/E9ER)。这个短篇小说描述了一个今天被称为小世界概念的想法。为了证明他的观点,他展示了他在 1929 年如何将自己与他视角之外的人联系起来。在卡林蒂的例子中,他展示了作为布达佩斯人的他如何与一个美国福特工厂的工人联系起来。福特工厂的工人认识他的经理,而那位经理可能认识亨利·福特。亨利·福特可能认识一个在匈牙利的工业家,而这个工业家可能是卡林蒂的一个朋友的朋友。这样,他证明了福特公司的工人可能只隔四到五个握手就能与布达佩斯的作家联系起来。多年来,小世界概念被重新命名并普及为六度分隔,源自约翰·古尔(John Guare)的戏剧六度分隔

在 20 世纪 50 年代和 60 年代,保罗·埃尔德什(Paul Erdös)和阿夫拉姆·雷尼(Alfréd Rényi)开始研究用于描述网络的术语。在他们的 1959 年论文中,他们开始研究大型网络的行为。大型网络看起来如此复杂,以至于人们可能会认为它们是随机的。即使在社交网络中,也很难预测谁与谁相连。他们认为这些网络必须是随机的,因为人们可能会随机遇到其他人,或者分子可能会随机相互作用。

任何网络的表征一个基本方面是查看节点度分布。简单来说,节点度是每个节点拥有的链接数量。在随机网络中,度分布将遵循高斯分布,如图 5.1 所示。

05-01

图 5.1 随机网络度分布

大多数节点大致具有相同数量的链接。不会有很多非常受欢迎的节点,但也不会有很多孤立的节点。然而,高斯分布通常用于独立观测。另一方面,一个图由高度相互连接的观测组成,这些观测不是独立的。结果发现,几乎没有任何现实世界的网络遵循随机网络度分布。这个主张背后的想法是网络有深刻的组织原则。大约在同一时间,Google 开发了其著名的图算法 PageRank(Brin & Page,1998 年),Albert Barabási 和他的同事检查了网络的架构(Albert et al. 1999 年)。网络由网页和指向其他网站的 URL 链接组成。这本质上是一个网络,其中节点代表网页,关系代表它们的 URL 链接。假设是,网络将变成一个随机网络,因为任何人都可以发布网页并选择他们想要链接的网站。他们发现,网络的度分布遵循不同的模式,如图 5.2 所示。

05-02

图 5.2 无标度网络度分布

这与预期的度分布非常不同。在互联网上,绝大多数网页很少被访问,甚至从未被访问;它们可能只有一个或两个链接指向它们。然后,有一些网页,如 Google、Amazon、Yahoo 和其他,有数亿个链接指向它们。这样的网络极其不均匀,今天被称为无标度网络。后来发现,大多数现实世界的网络都是无标度网络,其中一些大型枢纽节点连接了许多微小的节点。为什么会这样呢?事实是网络有深刻的组织原则。例如,考虑一群人。谁更有可能建立新的联系:只有几个朋友的个人,还是已经有很多朋友的个人?结果是,已经有很多朋友的个人更有可能建立新的关系。一个简单的解释是,他们更有可能被邀请参加更多活动,在这些活动中,他们可以与新人交往,因为他们现有的联系数量更多。此外,他们更有可能通过现有的联系人介绍给新人。这种网络组织原则被称为优先连接模型,这是 Barabási 和 Albert(1999 年)提出的术语。

传统分析工具通常被设计用于处理遵循高斯分布的独立观测数据。然而,这些工具在分析密集且不均匀连接的数据时可能会遇到麻烦。因此,设计了一套图分析工具来处理高度连接且遵循幂律分布的数据。

5.1 关注者网络

大多数图算法都是设计用于单部分网络的。如果你还记得,单部分网络包含一种类型的节点和关系。一个典型的例子是友谊网络,其中你只有人和他们的友谊关系。另一个经常提到的例子是网络网络,其中你处理网页和连接它们的超链接。即使处理多部分网络,也常见使用各种技术推断或投影单部分网络。下一章将更多地关注推断单部分网络。

在这里,你将在 Twitter 跟随者网络上执行你的第一个图算法。尽管 Twitter 社交图包含多种节点类型和关系,但你可以将你的图分析集中在特定的子图上。跟随者网络是单部分的,因为它只包含User节点和FOLLOWS关系。我选择它是因为你目前不需要处理单部分投影。

一个用户可以关注另一个用户,但他们不一定回关。这意味着你正在处理一个有向网络。此外,关系没有任何属性或属性可以量化它们的强度,这意味着你正在处理一个无权网络。

首先,你将学习如何从连通性和链接密度方面来描述跟随者网络。例如,斯坦福网络分析平台(SNAP)仓库(snap.stanford.edu/index.xhtml)包含各种图数据集。如果你打开 Pokec 社交网络(snap.stanford.edu/data/soc-Pokec.xhtml)数据集网页,你可以观察到网络的一些特征与数据本身一起给出:

  • 节点数

  • 关系数

  • 最大的弱连接组件中的节点数

  • 最大的强连接组件中的节点数

  • 平均局部聚类系数

为了描述网络,你将使用 Cypher 查询语言和 Neo4j 图数据科学库中的图算法。具体来说,在本章中,你将学习如何使用一些社区检测中心性图算法。社区检测算法将用于描述网络并找到紧密连接的用户组。在网络的背景下,社区指的是一个密集连接的节点组,其成员与其他网络中的社区相比具有较少的连接。例如,考虑一个友谊网络(图 5.3)。

05-03

图 5.3 带有轮廓社区的友谊网络

图 5.3 可视化了一个八人的网络及其友谊联系。社区用圆圈标出。你可以观察到,在节点之间紧密相连的地方形成了社区。例如,图 5.3 中有三个社区;在右侧,弗罗多、山姆维兹和杰克形成一个社区。他们彼此之间都有联系,就像你预期的一群朋友一样。尽管杰克和甘尼许有联系,但他们不属于同一个社区,因为甘尼许与杰克组中的其他朋友没有任何联系。

如果从现实生活中的朋友圈的角度来考虑,这就有意义了。想象一下,你有一群喜欢一起去徒步或玩桌游的朋友。我们可以称他们为你的朋友圈。现在,尽管你可能在工作中交到朋友,但这并不意味着这个工作朋友自动成为你朋友圈的一部分。而且,很可能这个工作朋友也有一个他们一起参与爱好活动的独立朋友圈。只有当你的朋友圈和你的工作朋友的朋友圈一起,例如一起玩桌游或去徒步时,你才能考虑这两个朋友圈合并,形成一个紧密相连的单个朋友圈。在这种情况下,你将和你工作上的朋友属于同一个朋友圈,你们现在也会在空闲时间一起出去玩。

社区检测技术可以用来检测用户的不同部分,发现有共同兴趣的人,或在社区内推荐新的联系。假设你从社交网络中退一步。在这种情况下,你可以使用社区检测算法将具有相似角色的蛋白质分组(Pinkert 等人,2010 年)或根据处方数据识别医生的专业领域(Shirazi 等人,2020 年)。社区检测算法的另一个应用是检查科学合作网络的网络结构(Newman,2001 年;Piedra 等人,2017 年)。

现在,让我们思考一下什么使一个节点对网络有影响力。有几个程序可以确定一个节点是否具有影响力。用于识别图中重要或具有影响力的节点的度量标准被称为中心性度量,而计算它们的算法被称为中心性算法。例如,确定一个节点重要性的最基本度量是度中心性,它简单地计算一个节点有多少关系。关系的数量越多,节点在网络中的影响力就越大。另一个节点重要性的例子是检查一个节点对网络中信息流的影响量(图 5.4)。

05-04

图 5.4 展示了具有节点大小对应其信息流影响程度的友谊网络

图 5.4 显示了与图 5.3 相同的网络。唯一的区别是,现在可视化中的节点大小对应于其对信息流的影响力。假设信息只能通过友谊关系流通。在这种情况下,Ganesh 是网络中最重要的节点,因为他连接了所有三个社区。Ganesh 可以被看作是社区之间信息守门人,允许他选择想要传递的数据以及何时传递。还有一点需要注意,如果 Ganesh 从网络中移除,它将分成三个部分。其他两个关键节点是 Ljubica 和 Jack,他们连接他们的社区与网络的其余部分。

在本章的最后部分,你将利用中心性算法来寻找最重要或最具影响力的用户。节点影响力度量有多种变体。为了计算网络中信息流的影响力,你可以使用介数中心性。如前所述,它在社交网络分析中有多种应用,但也可以用来预测道路网络中的拥堵(Kirkley 等人,2018 年)。最著名的节点中心性算法可能是PageRank,它被开发出来用于对网站进行排名,并使用排名信息来产生更好的搜索结果(Brin & Page,1998 年)。PageRank 算法的美丽之处在于它可以应用于其他领域。例如,它已经被用来根据引用来对研究论文作者进行排名(Ying Ding 等人,2010 年)。我还发现了一个例子,其中 PageRank 被用来评估 YouTube 上的用户声誉(Hanm Yo-Sub 等人,2009 年)。最后,它还可以用来分析蛋白质相互作用网络(Iván & Grolmusz,2011 年)。

现在,我们将完成一些实际例子,以学习如何利用 Cypher 查询语言和图算法来描述和评估 Twitter 关注者网络的社区结构,并随后识别最具影响力的节点。为了跟随本章中的示例,你必须拥有一个 Neo4j 数据库实例,并已按照第三章所述导入 Twitter 网络数据。

5.1.1 节点度分布

网络的一个基本特征是节点度分布。在有向网络中,你可以将度分布分为入度出度分布。节点入度计算进入关系的数量,而出度计算每个节点的输出连接数量。

首先,你将检查关注者网络的出度分布。如果你想在 Neo4j 浏览器中快速评估任何分布,你可以使用来自 Awesome Procedures on Cypher(APOC)库的apoc.agg.statistics函数。

注意:APOC 库包含约 450 个过程和函数,可以帮助你完成各种任务,从数据集成到批处理等。虽然它不是自动与 Neo4j 集成,但我建议你在所有 Neo4j 项目中都包含它。你可以查看官方文档,了解它所提供的所有过程,网址为 neo4j.com/docs/apoc/current/

apoc.agg.statistics 函数返回统计值,如平均值、最大值和给定值的百分位数。由于你只计算每个节点的连接数,因此你可以通过使用 count {} 函数简化查询。

列表 5.1 使用 apoc.agg.statistics 函数评估节点出度分布

MATCH (u:User)
WITH u, count{ (u)-[:FOLLOWS]->() } AS outDegree
RETURN apoc.agg.statistics(outDegree)

表 5.1 显示了最终的分布。

表 5.1 随机网络出度分布

total 3,594
min 0
minNonZero 1.0
max 143
mean 6.924874791318865
0.5 2
0.99 57
0.75 8
0.9 21
0.95 32
stdev 11.94885358058576

分布中有 3,594 个样本或节点。用户节点平均有大约七个出度关系。0.5 键代表第 50 个百分位数,0.9 键代表第 90 个百分位数,依此类推。虽然出度关系的平均值接近 7,但中位数仅为 2,这表明 50% 的节点有两个或更少的出度连接。大约 10% 的用户有超过 21 个出度连接。

如果你更倾向于视觉化,你可以像我一樣,在你的可视化库中绘制出度分布的直方图。图 5.5 展示了一个示例。

05-05

图 5.5 使用 seaborn 直方图可视化的出度分布图

可视化图表超出了本章的范围,因此我不会详细介绍我是如何生成图 5.5 的。然而,第九章和第十章的 Jupyter 笔记本中包含使用 Python seaborn 库绘制直方图的代码。

有趣的是,即使是 Twitter 网络的一个小子图也遵循幂律分布,这是现实世界网络的典型特征。为了使图表易于阅读,我已将箱范围限制在只有出度为 60 或更少的节点。超过 1,000 个节点没有出度连接,大多数节点的链接数少于 10。你之前观察到最高的出度为 143,只有 5% 的节点的出度大于 32。

练习 5.1

获取出度最高的前五个用户。使用 count {} 操作符。

现在,你将重复相同的步骤来评估入度分布。首先,你将使用 apoc.agg.statistics 函数在 Neo4j 浏览器中评估入度分布,如列表 5.2 所示。

列表 5.2 使用 apoc.agg.statistics 函数评估节点出度分布

MATCH (u:User)
WITH u, count{ (u)<-[:FOLLOWS]-() } AS inDegree
RETURN apoc.agg.statistics(inDegree)

表 5.2 显示了最终的分布。

表 5.2 关注者网络的出度分布

total 3594
min 0
minNonZero 1.0
max 540
mean 6.924874791318865
0.5 0
0.99 112
0.75 4
0.9 16
0.95 35
stdev 22.7640611678852

立刻吸引我注意的就是出度和入度的平均值是相同的。这可能对你来说是有道理的,因为节点和关系的总数是相同的,所以平均值应该相同。超过一半的用户没有入站连接。虽然我已经抓取了所有用户的 Twitter API 关注者关系,但我只包括在 12,000 条抓取推文中发布或被提及的用户之间的关系。看起来大约一半的用户在这个子图中没有任何关注者。有一个异常值有 540 个入站关系(关注者数量),这意味着七分之一的用户关注他们。

再次,我将使用 seaborn 库展示入度分布(图 5.6)。尽管我没有旨在获取节点入度和出度的幂律分布,但大多数现实世界的网络往往表现出这种分布。然而,当处理图的小子集时,节点度分布高度依赖于用于检索数据集的采样方法。随着数据样本的增长,其节点度分布将越来越接近整体图度分布(图 5.6)。

05-06

图 5.6 使用 seaborn 直方图可视化的入度分布图

练习 5.2

获取具有最高入度(关注者数量)的前五名用户。使用count {}运算符。

练习 5.2 的解决方案生成了表 5.3 所示的输出。

表 5.3 具有最高入度的前五名用户

user inDegree
"elonmusk" 540
"AndrewYNg" 301
"NASA" 267
"OpenAI" 265
"GoogleAI" 264

具有最高入度的用户非常有趣。Elon Musk 位居榜首。看起来他在科技社区很受欢迎,或者至少在给定的 Twitter 子图中是这样。第二名是 Andrew Ng。如果你接触过任何机器学习,你可能听说过他,因为他是最著名的机器学习讲师之一。

练习 5.3

记住,我只包括发布推文或被提及的用户。选择具有最高入度的前五名用户之一,并检查他们发布的推文或被提及的推文。

练习 5.3 的解决方案 Cypher 语句显示在列表 5.3 中。

列表 5.3 检查 NASA 的提及和发布的帖子

MATCH (u:User)
WHERE u.username = "NASA"
OPTIONAL MATCH m=(u)<-[:MENTIONS]-()
OPTIONAL MATCH p=(u)-[:PUBLISH]->()
RETURN m,p

我选择探索 NASA 的 Twitter 账户。请注意,我使用了OPTIONAL MATCH,因为我事先不知道 NASA 是否发布了一条推文并被提及。再次强调,你可以使用 Cypher 语句的几个变体来产生相同的结果,所以如果你得到了正确的结果但使用了稍微不同的 Cypher 语句,请不要担心。列表 5.3 中的 Cypher 语句将在 Neo4j 浏览器中产生图 5.7 所示的可视化。在我们的数据集中,NASA 发布了一条推文,并被提及在两条其他推文中。

05-07

图 5.7 发布或提及 NASA 的推文网络可视化

5.2 Neo4j 图数据科学库简介

在继续进行网络特征化之前,你应该熟悉 Neo4j 图数据科学库(GDS)。GDS 是 Neo4j 的一个插件,它包含超过 50 个图算法,从社区检测和中心性到节点嵌入算法和链接预测管道,以及更多。你可以在官方文档中找到所有可用图算法的概述(neo4j.com/docs/graph-data-science/current/)。

GDS 库中的图算法是在一个与数据库中存储的图结构分开的投影内存图结构上执行的(图 5.8)。要使用 GDS 库执行图算法,你必须首先投影一个内存图。投影图完全存储在内存中,使用优化的数据结构以实现可扩展和并行图算法执行。你可以使用原生投影Cypher 投影来创建一个投影内存图。

05-08

图 5.8 图数据科学库工作流程

原生投影在选择或过滤你想要投影的特定子图方面有限制,因为你只能根据节点标签和关系类型进行过滤。然而,它是推荐的方式来投影一个图,因为它由于直接从 Neo4j 存储中读取数据而具有高性能。

创建内存图的第二种可用选项是 Cypher 投影。使用它,你将获得 Cypher 查询语言的全部灵活性来选择或过滤任何你想要投影的特定子图。当然,Cypher 投影有一个缺点,即它比原生投影慢,通常只推荐用于项目的实验或探索阶段。

由于在处理大型图时,内存图投影可能成本较高,因此 GDS 库还提供了一种 图目录 功能。当您想要在同一个投影图上执行多个图算法时,图目录就派上用场了。您不需要为每个算法执行单独创建内存图,而是一次创建一个内存图,然后在该图上执行多个图算法。投影图可以通过其名称在执行图算法时访问,因此术语 命名图 就与存储在图目录中的投影图相关联。一旦创建了内存图,您就可以在其上执行图算法。

每个算法都有四种执行模式,具体取决于用例:

  • stream—以记录流的形式返回结果,但不存储结果。

  • stats—返回结果的摘要统计信息,但不存储结果。

  • mutate—将结果写回投影的内存图。此模式只能与存储在图目录中的命名图结合使用。当您想将一个图算法的输出用作另一个图算法的输入时,这非常有用。

  • write—将结果写回 Neo4j 数据库图。

5.2.1 图目录和本地投影

让我们从回顾一些涵盖网络特征化和 GDS 语法及算法用例的示例开始。首先,您需要投影一个内存图。您将使用本地投影来创建一个包含 User 节点和 FOLLOWS 关系的内存图。本地投影语法如下所示。

列表 5.4 在图目录中创建命名图的本地投影语法

CALL gds.graph.project(
    graphName,
    nodeProjection,
    relationshipProjection,
    optional configuration
)

使用 CALL 子句与过程名称结合执行 GDS 程序。使用本地投影将命名图存储在图目录中的过程称为 gds.graph.project()。它包含三个必填参数和一个可选参数。第一个参数用于命名图,在执行图算法时将通过该名称访问。第二个参数,称为 nodeProjection,定义了您想要投影的节点子集。同样,relationshipProjection 参数指定在创建内存图时应考虑哪些关系。需要注意的是,如果相邻节点在 nodeProjection 参数中未描述,则关系将在投影期间被跳过。在 GDS 术语中,关系的前一个节点称为 源节点,后一个节点称为 目标节点

要投影关注者网络,你只需要包括User节点和FOLLOWS关系,无需额外配置。列表 5.5 中的 Cypher 语句使用原生投影来存储内存中的图。第一个参数指定其名称,当执行图算法时将使用该名称来访问它。第二个参数定义了你想包含在投影中的节点。当你只想投影一种类型的节点时,你可以将所需的节点标签定义为字符串。同样,当你只想在第三个参数中投影一种类型的关系时,你可以指定类型为字符串。

列表 5.5 将内存中的由User节点和FOLLOWS关系组成的图进行投影

CALL gds.graph.project('follower-network', 'User', 'FOLLOWS')

在后面的章节中,你将了解更多关于原生投影以及如何创建由多个节点标签和关系类型组成的内存图。GDS 库还支持投影节点和关系属性,这在处理加权网络时非常有用。

5.3 网络特征

接下来,你将使用 GDS 库来表征 Twitter 关注者网络。这些网络表征度量提供了对网络拓扑的全面视图,有助于理解整体结构、信息流效率以及网络中节点之间的互连程度。

5.3.1 弱连通分量算法

你将要执行的第一个图算法是弱连通分量(WCC)算法。WCC 是图内的一组节点,如果忽略关系的方向,则该组内的所有节点之间都存在路径。WCC 可以被认为是一个“岛屿”,无法从其他图组件中到达。虽然该算法识别了节点集的连通集,但其输出可以帮助你评估整体图的连通性程度。

以 WCC 算法开始任何图分析是有益的,因为它提供了对图结构的概述,包括连通性和孤立部分。通过识别这些不同的部分,你可以将进一步的分析集中在最相关的组件上,从而简化计算成本。了解连通性和整体图的连通性为更详细的调查奠定了基础,例如社区检测或中心性分析。

WCC 算法可能是一种图算法,应该在任何图分析的第一步执行,以评估图的连通性。图 5.9 显示了两个 WCC。一个组件包含 John、Alicia 和 Amulya,另一个包含 OpenAI、Google AI、NASA 和 Andrew Ng。

05-09

图 5.9 两个 WCC 的网络可视化

如果忽略关系方向,单个 WCC 内的节点可以到达所有其他节点。例如,John 可以到达 Alicia,即使关系方向相反。实际上,可以说关系被当作无向处理。算法将同一社区内的所有节点视为如果它们之间存在路径,则可以相互到达,而不考虑关系的方向。

你将使用write模式执行 WCC 算法。如前所述,write模式将结果存储回 Neo4j 数据库,同时也提供了算法结果的摘要统计信息。GDS 中图算法过程的语法在以下列表中显示。

列表 5.6 图算法过程语法

CALL gds.<algorithm>.<mode>(namedGraph, {optional configuration})

当使用算法的write模式时,你需要提供强制性的writeProperty参数,该参数指定了算法结果将存储到的节点属性名称。执行 WCC 算法的过程是gds.wcc

你可以使用以下列表中的 Cypher 语句在跟随者网络上执行 WCC 算法并将结果存储到 Neo4j。

列表 5.7 在跟随者网络中执行 WCC 算法并将结果存储为followerWcc节点属性

CALL gds.wcc.write('follower-network', {writeProperty:'followerWcc'})
YIELD componentCount, componentDistribution

表 5.4 显示了结果统计信息。

表 5.4 在跟随者网络上执行 WCC 算法的摘要统计信息

componentCount componentDistribution
547

算法的write模式将结果存储到 Neo4j 数据库,并提供表 5.4 中所示的摘要统计信息。包含算法结果的节点属性标识了节点所属的组件 ID。

跟随者网络中有 547 个不连通的组件,其中最大的包含 2,997 个成员。大多数现实世界的网络都有一个包含网络中大多数节点的单个连通组件和一些不连通的边缘组件。由于你正在分析的只是更大网络的一部分,组件数量较多并不罕见,因为许多缺失的用户和关系如果包含在内,本应连接网络的各个部分。我发现了一项关于 Twitter 网络的(Myers 等人,2014)分析,其中作者分析了 2012 年 Twitter 网络的快照,包含 1.75 亿用户和 200 亿个关注关系。分析显示,大约 93%的用户属于最大的 WCC。另一项分析检查了 Facebook 图(Ugander 等人,2011),他们发现最大的 WCC 包含所有节点的 99%以上。

p90结果,或组件大小的第 90 百分位数,其值为 1,表示 90%的组件只有一个成员。当一个组件只有一个成员时,这意味着该节点没有关系。

练习 5.4

计算五个最大 WCC 的成员数量。组件 ID 存储在User节点的followerWcc属性下。使用followerWcc属性作为分组键,结合count()函数来按组件计算成员数量。

练习 5.4 的解决方案产生了表 5.5 所示的输出。

表 5.5 五个最大 WCC 的成员数量

componentId countOfMembers
0 2997
1293 5
1049 3
269 3
335 3

您可以观察到存在一个单独的组件,它包含网络中 85%的节点。组件 ID 不是确定的,这意味着您可以为组件 ID 得到不同的值。然而,组件成员分布应该是相同的。

第二大的组件只包含五个成员。您可以使用以下列表中的 Cypher 语句在 Neo4j 浏览器中可视化第二大的组件(也请参阅图 5.10)。如果您有不同的组件 ID,请确保在WHERE子句中更改组件 ID。

05-10

图 5.10 关注网络中第二大的 WCC 的网络可视化

列表 5.8 检索没有传出FOLLOWS关系的User节点

MATCH p=(u:User)-[:FOLLOWS]->()
WHERE u.followerWcc = 1293
RETURN p

我已经检查了图 5.10 中显示的用户名在 Twitter 上的情况,他们似乎是新加坡和京都大学的几位教授。他们相互关注,但与我们小型 Twitter 快照中的其他网络没有连接。

练习 5.5

确定只包含一个成员的 WCC 数量。记住,如果一个 WCC 只包含一个成员,这实际上意味着该节点没有传入或传出的关系。与其使用followerWcc属性来计数这些组件,您可以直接过滤没有FOLLOWS关系的User节点并计数。计数将与只有一个成员的 WCC 数量相同。

5.3.2 强连通分量算法

强连通分量(SCC)是有向图中所有节点之间都存在路径的子图。WCC 算法和 SCC 算法之间的唯一区别是 SCC 算法考虑关系方向。因此,SCC 算法仅适用于有向图。如果忽略关系方向,那么您正在处理 WCC。

图 5.11 显示了四个 SCC。第一个组件包含 NASA、Andrew Ng 和 Google AI。您会注意到,尽管 OpenAI 可以到达第一个组件中的所有节点,但 Google AI 到 OpenAI 的路径是不可能的,因为 SCC 算法不会忽略关系方向。同样,第三组件中的 Amulya 可以通过 Alicia 和 John 到达,但从 Amulya 到 John 或 Alicia 的定向路径不存在。

05-11

图 5.11 四个 SCC 的网络可视化

SCC 算法在有向路径和可达性起重要作用时很有用。例如,想象一个道路网络,其中节点代表交叉口,关系代表道路连接。例如,许多大城市中心有很多单向道路连接。使用 SCC 算法,你可以评估关闭一个或多个道路连接的后果以及它会如何影响城市内地点的可达性。

图 5.12 可视化了一个玩具道路网络,其中节点代表交叉口,它们之间的关系代表道路连接。例如,节点 A 和 B 两个方向都相连,而 B 和 E 之间的链接是单向的。使用 SCC 算法,你可以评估如果关闭特定的道路,节点的可达性。例如,如果由于维护而关闭交叉口 D 和 C 之间的道路,就无法从网络的右侧到达左侧。具体来说,你无法从节点 D、E、F 或 G 到达节点 A、B 或 C。因此,节点 A、B 和 C 将形成第一个 SCC,而 D、E、F 和 G 将形成第二个。

05-12

图 5.12 一个道路网络中的 SCC 用例示例

在 Twitter 的背景下,SCC 可以应用于识别更小的紧密连接的节点组。一篇研究论文(Swati 等人,2016)声称你可以使用 SCC 算法来识别用于更精确营销定位的用户组。另一篇文章(Efstathiades,2016)使用 SCC 算法建议用户只跟随受欢迎的用户,而不与其他不受欢迎的用户建立太多联系。结果是 SCC 数量随时间增加。

再次,你将使用算法的 write 模式将结果存储回 Neo4j 数据库。

列表 5.9 在跟随者网络上执行 SCC 算法并将结果存储为 followerScc 节点属性

CALL gds.scc.write('follower-network', {writeProperty:'followerScc'})
YIELD componentCount, componentDistribution

表 5.6 显示了生成的统计信息。

表 5.6 在跟随者网络上执行 SCC 算法后的摘要统计信息

componentCount componentDistribution
2704 (cf

如预期的那样,SCC 的数量高于 WCC 的数量。有 2,704 个 SCC,最大的一个包含 796 个成员。

练习 5.6

计算五个最大 SCC 的成员数量。组件 ID 存储在 User 节点的 followerScc 属性下。

练习 5.6 的解决方案生成了表 5.7 所示的输出。

表 5.7 五个最大 SCC 的成员数量

componentId countOfMembers
0 796
380 20
407 7
36 6
212 4

类似于 WCC 算法,社区 ID 不是确定的。你可能会得到不同的社区 ID,但应该得到相同的计数。

练习 5.7

在 Neo4j 浏览器中可视化第二大 SCC。一个节点可以与其他 SCC 中的节点建立关系,因此你必须应用一个过滤器来确保所有节点都在第二大 SCC 中。

练习 5.4 的解决方案在 Neo4j 浏览器中显示了图 5.13 所示的网络可视化。你可以观察到这个社区非常紧密,因为组内节点之间存在许多连接。根据用户名判断,他们似乎都来自同一个地区。不幸的是,我不是语言专家,所以我不知道是哪个地区。

05-13

图 5.13 是跟随网络中第二大强连通分量(SCC)的网络可视化。

5.3.3 局部聚类系数

局部聚类系数(LCC)是一个衡量特定节点的邻居之间连接程度或接近程度的指标。LCC(图 5.14)衡量的是节点两个邻居之间连接的平均概率。因此,LCC 的值介于 0 到 1 之间。LCC 值为 0 表示相邻节点之间没有连接。另一方面,LCC 值为 1 表示邻居的网络形成一个完全图,其中所有邻居都相互连接。

05-14

图 5.14 无向图的 LCC 值

在无向图上,LCC 更容易理解。例如,在图 5.14 中,Stu 有三个邻居。当这些邻居中的任何一个都没有与其他邻居建立连接时,LCC 值为 0。因此,图 5.14 左边的例子中,Stu 的 LCC 值为 0。在中间的例子中,Stu 的 LCC 值为 1/3 或 0.33。Stu 有三个邻居,所以从组合学的角度来看,他们之间有三种可能的关系。在图 5.14 中间的例子中,Stu 的邻居之间只有一个连接;因此,Stu 的 LCC 值为 1/3。右侧的例子中,Stu 的邻居之间有两个连接;因此,Stu 的 LCC 值为 2/3。如果 Jack 和 Amy 之间再建立另一个关系,那么 Stu 节点的所有邻居就会形成一个完全图,这将改变 Stu 的 LCC 值到 1。

LCC 算法提供了一个指标来评估节点邻居之间的连接强度。你可以通过将相邻节点之间现有链接的数量除以相邻节点之间可能链接的数量来计算单个节点的 LCC 值。你还可以使用图 5.15 中的公式来计算有向图上的 LCC。

对于有向图,第一个区别是,如果一个节点至少有一个连接到它,那么它就有相邻节点。尽管 Stu 在图 5.15 中有四个连接,但它们只有三个不同的邻居。一个节点的邻居可以有一个指向原始节点的入站或出站连接,或者两者都有。在有向图中,每一对邻居之间可以有最多两个关系,所以三个邻居之间的可能连接总数是六。再次强调,你只需要计算邻居之间的现有连接数量,并将其除以可能连接的数量。

05-15

图 5.15 有向图的 LCC 值

不幸的是,GDS 库仅支持无向图的 LCC 算法。然而,由于有向 LCC 仅计算相邻节点及其链接的数量,你可以很容易地仅使用 Cypher 查询语言实现该算法。使用以下 Cypher 语句计算每个节点的有向 LCC 值,并将结果存储在lcc节点属性下。

列表 5.10 在有向跟随者网络中计算 LCC

MATCH (u:User)                                                           ❶
OPTIONAL MATCH (u)-[:FOLLOWS]-(n)
WITH u,count(distinct n) AS neighbors_count                              ❷
OPTIONAL MATCH (u)-[:FOLLOWS]-()-[r:FOLLOWS]-()-[:FOLLOWS]-(u)
WITH u, neighbors_count, count(distinct r) AS existing_links             ❸
WITH u,
     CASE WHEN neighbors_count < 2 THEN 0 ELSE
       toFloat(existing_links) / (neighbors_count * (neighbors_count - 1))
       END AS lcc                                                        ❹
SET u.lcc = lcc                                                          ❺

❶ 匹配所有用户节点

❷ 计算它们不同邻居的数量

❸ 计算邻居之间不同链接的数量

❹ 计算 LCC 值

❺ 在 lcc 节点属性下存储 LCC 值

你应该已经熟悉列表 5.10 中的大多数 Cypher 语法。你首先匹配数据库中的所有用户。接下来,计算不同邻居的数量。由于一些User节点没有任何FOLLOWS关系,你必须使用OPTIONAL MATCH子句。使用MATCH子句会减少基数并有效地过滤掉所有没有任何FOLLOWS关系的User节点。如果你还记得 WCCs 示例,大约有 500 个User节点没有任何FOLLOWS关系。另一个小细节是,OPTIONAL MATCH中的 Cypher 模式不提供关系方向。你想要计算不同邻居的数量,无论它们是否有指向原始节点的入站、出站或两者都有的关系。由于一些邻居可以与原始User节点同时有入站和出站连接,你需要在count()函数中使用distinct前缀来获取正确的结果。在计算 LCC 之前,唯一缺少的变量是邻居之间现有链接的数量。同样,你应该使用OPTIONAL MATCH子句,因为一些邻居可能有零连接,你不想过滤掉这些连接。我真的很喜欢 Cypher 语法的表达性,可以定义将计算相邻节点之间链接数量的图模式:

OPTIONAL MATCH (u)-[:FOLLOWS]-()-[r:FOLLOWS]-()-[:FOLLOWS]-(u)

您可以观察到在这个模式中我使用了引用变量u两次。实际上,这个图模式描述了u节点参与的所有三角形。

05-16

图 5.16 用于识别三角形的可视化 Cypher 模式

如图 5.16 所示,根据 Cypher 语法描述,两个节点都必须与节点u相邻。您只对计算邻居之间的r关系感兴趣,因此您结合count()函数和distinct前缀来获取邻居之间现有链接的数量。同样,正如之前一样,指定图模式中的FOLLOWS关系没有指定方向,因为您想考虑所有可能的关系方向变体。最后,您可以使用 LCC 算法公式来计算每个节点的 LCC 值。

05-16-UN01

上述方程可以用来计算有向 LCC 值。您需要计算现有链接的数量,并将其除以邻居之间可能连接的数量,即邻居数量乘以邻居数量减 1。当节点邻居数量为 0 或 1 时,该公式不适用,因为您最终会除以 0。根据定义,少于 2 个邻居的节点的 LCC 值是未定义的。然而,我遇到过一些实现,它们在少于 2 个邻居的节点上使用 0 代替未定义,这也是我在这个例子中决定使用的。我引入了CASE语句来自动为少于 2 个邻居的节点分配 LCC 值为 0。如果您对 SQL 查询语言有一些经验,您会注意到CASE语句在 Cypher 中是相同的。无论如何,CASE子句的 Cypher 语法如下:

CASE WHEN predicate THEN x ELSE y END

断言值应该是一个布尔值。然后,如果布尔值为真,则选择x值;如果断言为假,则选择y值。

最后,您需要将计算出的 LCC 值存储在User节点的lcc属性下。现在 LCC 值已经存储在数据库中,您可以继续计算平均 LCC。

列表 5.11 计算平均 LCC

MATCH (u:User)
RETURN avg(u.lcc) AS average_lcc

平均 LCC 为 0.06。这相当接近 0。如此小的 LCC 值的一个原因是,我们只有 Twitter 网络的微小快照,因此关于关注者的信息有限。对更大范围的 Twitter 网络的研究(Myers 等人,2014 年)表明,平均 LCC 值更接近于 0.15 到 0.20 之间。此外,似乎 Twitter 用户比 Facebook(Ugander 等人,2011 年)上的用户联系得更松散。这很合理,因为人们通常在 Facebook 上与他们的朋友和家人联系,这是一个联系更紧密的用户群体。另一方面,一项研究(Efstathiades 等人,2016 年)表明,Twitter 用户更喜欢关注精英用户或有影响力的人,而不是像经常与家人、现实生活中的朋友或邻居那样联系。

5.4 识别中心节点

在本章的最后部分,你将学习如何识别最中心的节点。用于识别最中心节点的图算法组被称为中心性算法,其中 PageRank 是最著名的一个。

5.4.1 PageRank 算法

PageRank 是由拉里·佩奇和谢尔盖·布林(1999 年)设计的,并帮助谷歌搜索成为今天的模样。PageRank 衡量节点的传递或方向性影响力。例如,节点度通过仅考虑其直接邻居来量化节点的影响力或重要性。相比之下,PageRank 还考虑了跨越多个跳转的图中其他节点的间接关系。以我们的 Twitter 子图为例,例如,如果埃隆·马斯克或安德鲁·吴关注你,你获得的影响力比如果我关注你时更多。PageRank 评估特定节点拥有的关注者数量以及这些关注者的影响力。

PageRank 最初是为排名网页重要性而开发的。该算法将每个关系视为一种影响力投票(图 5.17)。我喜欢这样想,如果一个节点指向另一个节点,它本质上表明另一个节点很重要或有影响力。

05-17

图 5.17 PageRank 将每个关系视为一种影响力投票。

你可以想象,通过有向关系,投票如何在网络中流动。每个节点初始化时,其分数等于节点总数的倒数。然后,它通过其出链传递其排名。每条关系中传递的影响力等于节点影响力除以出链数量。第一次迭代后,节点的排名等于来自其他节点的输入分数之和。然后,算法重复此过程,直到收敛或达到预定义的迭代次数(图 5.18)。

05-18

图 5.18 基于网络流简化的 PageRank 计算

然而,基于网络流的简化 PageRank 计算存在一个关键缺陷。图 5.17 中的节点 D 没有出链。没有任何出链的节点也被称为死胡同。死胡同的存在会导致网络中某些或所有节点的 PageRank 分数下降到零,因为它实际上将排名分数泄露出了网络。

PageRank 算法引入了teleportation能力以避免排名泄露。Teleportation 引入了跳转到随机节点的小概率,而不是跟随出链。在探索网页的上下文中,想象一个冲浪者穿越互联网。他们可能会跟随网页到网页的出链,或者感到无聊并跳转到随机页面。定义冲浪者跟随出链概率的常数称为阻尼因子。因此,他们跳转到随机页面的概率是 1 减去阻尼因子。阻尼因子的典型值是 0.85,这意味着冲浪者大约有 15%的时间会跳转到随机页面。

使用标准的 PageRank 算法,随机跳转到网络中任意节点的概率在所有节点之间均匀分布,这意味着一个无聊的冲浪者有同等的机会跳转到图中的任何节点,包括他们当前正在访问的节点。在这种情况下,中间节点在每次迭代后都会将出链的排名相加,如图 5.18 所示,并添加一个冲浪者随机跳转到该节点的概率。 teleportation 能力解决了死胡同节点泄露整个网络 PageRank 分数的情况,这实际上会导致所有节点的排名值为零。你可以使用以下 Cypher 语句在关注者网络上执行 PageRank。

列表 5.12 在关注者网络上执行 PageRank

CALL gds.pageRank.write('follower-network',
  {writeProperty:'followerPageRank'})

列表 5.12 中的 Cypher 语句在投影关注者网络上执行 PageRank 算法,并将结果存储回数据库作为User节点的followerPageRank属性。

练习 5.8

获取具有最高 PageRank 分数的前五个用户。PageRank 分数存储在followerPageRank节点属性下。

练习 5.8 的解决方案产生了表 5.8 所示的输出。

表 5.8 按 PageRank 分数排序的最重要节点

username followerPageRank
"elonmusk" 20.381862706745217
"NASA" 8.653231888111382
"wmktech" 6.937989377788902
"Twitter" 6.937989377788902
"Wajdialkayal1" 6.551413750286345

伊隆·马斯克是我们 Twitter 子图中最有影响力的用户。有趣的是,安德鲁·吴、谷歌 AI 和 OpenAI 都曾在前五名,但在使用 PageRank 分数时失去了位置。记住,PageRank 评估的是进入连接的数量以及链接背后的节点的影响力。有时,具有高 PageRank 分数的节点只有少量有影响力的连接。你可以使用以下 Cypher 语句检查每个用户的顶级关注者,如表 5.8 所示。

列表 5.13 检查排名最高的用户的顶级关注者

MATCH (u:User)<-[:FOLLOWS]-(f)
WHERE u.username IN
  ["elonmusk", "NASA", "wmktech", "Twitter", "Wajdialkayal1"]  ❶
WITH u,f
ORDER BY f.followerPageRank DESC                               ❷
RETURN u.username AS user,
       round(u.followerPageRank, 2) AS pagerankScore,
       collect(f.username)[..5] AS topFiveFollowers            ❸
ORDER BY pagerankScore DESC

❶ 匹配使用 IN 子句的用户组

❷ 按关注者 PageRank 分数降序排列中间结果

❸ 按原始用户分组收集前五个关注者

列表 5.13 中的 Cypher 语句首先通过username属性匹配排名前五的用户。您可以使用IN运算符指定可能值的列表,而不是使用多个OR谓词。然后,您使用WITH语句按关注者的 PageRank 分数对结果进行排序。最后,您使用collect()函数生成按 PageRank 分数排序的关注者有序列表。collect()函数保持输入数据的顺序。因为您首先在WITH语句中按关注者的 PageRank 分数对结果进行排序,所以collect()函数的列表结果将包含按 PageRank 分数排序的关注者列表。通过列表切片实现了每个用户只返回前五名关注者。如果您曾经进行过任何编程或 SQL 分析,您可能已经遇到过列表或数组切片。Cypher 中的列表切片语法如下:

array[from..to]

方括号语法将从起始索引from提取数组元素,直到(但不包括)结束索引to。Cypher 有一个round()函数,允许您指定将任何数字四舍五入到指定的精度或小数点。列表 5.13 中 Cypher 语句的结果显示在表 5.9 中。

表 5.9 按 PageRank 分数排序的最重要节点

用户 PageRank 分数 前五名关注者
"elonmusk" 20.38 ["fchollet", "TheCuriousLuke", "DrLiMengYAN1", "douwekiela", "threadreaderapp"]
"NASA" 8.65 ["BIBBI02374449", "Lucian2drei", "NYTScience", "CmccClimate", "abhibisht89"]
"wmktech" 6.94 ["Wajdialkayal1", "alkayal_wajdi", "Websystemer", "AlkayalWajdi", "SwissCognitive"]
"Twitter" 6.66 ["Lucian2drei", "Chuck_Moeller", "Omkar_Raii", "SportsCenter", "philipvollet"]
"Wajdialkayal1" 6.55 ["wmktech", "Websystemer", "taylorwfarley", "RiM2ww", "saye2018"]

我原本希望 Elon 或 NASA 会出现在前关注者中,但不幸的是,他们在我们子图中没有关注任何人。Elon 和 NASA 缺乏关注者关系并不令人惊讶,因为他们总共关注了约 500 个用户,但他们自己却有超过 2 亿的关注者。例如,如果 Elon 或 NASA 关注了一个用户,他们的 PageRank 分数将自动很高,因为他们将有一个最具影响力的节点在关注他们。一个现实生活中的类比可能是以下这样:想象你刚刚搬到瑞典,除了国家的总统外,你认识的人都不认识。尽管你只有一个联系,但这个联系非常有影响力,这会自动给你在网络中带来很多影响力。

表 5.9 中唯一令人兴奋的跟随模式是用户 wmktech 和 Wajdialkayal1 相互关注。他们两人都很有影响力,而且通过相互关注,彼此也增加了对方的重要性。

5.4.2 个性化 PageRank 算法

Neo4j GDS 库也支持个性化 PageRank变体。在 PageRank 定义中,网络冲浪者可能会感到无聊并随机跳转到其他节点。通过个性化 PageRank 算法,你可以定义当网络冲浪者感到无聊时应该跳转到哪些节点。可以说,通过定义冲浪者倾向于跳转的sourceNodes,你实际上是通过特定节点或多个节点的视角来检查节点的影响。

在本例中,你将使用个性化 PageRank 算法的stream模式。stream模式将算法的结果作为记录流返回。个性化 PageRank 算法的语法几乎与 PageRank 算法相同,只是你还提供了sourceNodes参数。

列表 5.14:从 2016 年注册的用户视角运行个性化 PageRank 算法

MATCH (u:User)
WHERE u.registeredAt.year = 2016
WITH collect(u) AS sourceNodes                             ❶
CALL gds.pageRank.stream('follower-network', {sourceNodes: sourceNodes})
YIELD nodeId, score                                        ❷
RETURN gds.util.asNode(nodeId).username AS user, score
ORDER BY score DESC
LIMIT 5;                                                   ❸

❶ 匹配用于个性化 PageRank 算法的源节点

❷ 执行个性化 PageRank 算法

❸ 使用 gds.util.asNode 函数通过其内部 ID 匹配特定节点

首先,你需要使用MATCH子句后跟collect()函数来生成所有 2016 年注册用户的列表。然后,你可以将收集到的用户作为sourceNode参数输入。通过定义sourceNode参数,你是在指示程序执行个性化 PageRank 算法,并在 teleport 时使用提供的节点作为重启节点。PageRank 算法的stream模式输出两列:nodeIdscorenodeId代表 Neo4j 的内部节点 ID,数据库会自动为数据库中的每个节点生成。你可以使用gds.util.asNode()函数将nodeId值映射到实际的节点实例。score列代表特定节点的 PageRank 分数。

从 2016 年注册的用户角度来看,最重要的用户是埃隆·马斯克,其次是安德鲁·吴、伊恩·古德费洛、Hugging Face 和 NASA。虽然埃隆·马斯克和 NASA 也出现在整体 PageRank 分数排名前五的用户中,但你也可以观察到,当从 2016 年注册的用户视角来确定 PageRank 分数时,安德鲁·吴、伊恩·古德费洛和 Hugging Face 的账户获得了重要性。

如果你运行个性化 PageRank 并使用没有出向连接的节点作为sourceNodes参数会发生什么?以下列表显示了此过程。

列表 5.15:从 NASA 视角运行个性化 PageRank 算法

MATCH (u:User)
WHERE u.username = "NASA"
WITH collect(u) AS sourceNodes
CALL gds.pageRank.stream('follower-network', {sourceNodes: sourceNodes})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).username AS user, score
ORDER BY score DESC
LIMIT 3;

表 5.10 显示了本例的 PageRank 分数。

表 5.10:按 PageRank 分数排序的最重要节点

user score
"NASA" 0.15000000000000002
"ServerlessFan" 0
"dr_sr_simpson" 0

由于 sourceNodes 参数中指定的节点没有出链,PageRank 算法将不断在选定的源节点上重新启动,这反过来又会导致所有其他节点的 PageRank 评分为 0。当使用默认设置运行 PageRank 时,所有 PageRank 评分为 0.15 的节点都没有任何进入关系。它们只通过网络浏览者随机跳转到它们而获得重要性,但它们没有来自其他节点的显著投票。在个性化 PageRank 变体中,你也可以指定在无聊时跳转到哪些节点,这意味着一些节点甚至不会获得 0.15 的 PageRank 评分,因为浏览者不会随机跳转到它们。

练习 5.9

执行个性化 PageRank 算法,并使用 2019 年注册的 User 节点作为 sourceNodes 参数。

5.4.3 删除命名图

恭喜!你现在已经完成了你的第一次网络分析。在你完成计划的图算法执行序列后,建议删除投影图以节省内存。你可以通过使用 gds.graph.drop() 程序来释放内存中的图。

列表 5.16 从内存中释放 follower-network 图

CALL gds.graph.drop('follower-network')

在下一章中,你将学习如何根据间接关系推断单部分网络。你将运行本章中学到的许多图算法,以巩固执行它们和理解它们结果的能力。

5.5 练习解答

练习 5.1 的解答如下。

列表 5.17 获取具有最高五个出度的用户

MATCH (u:User)
RETURN u.username AS user,
       count { (u)-[:FOLLOWS]->() } AS outDegree
ORDER BY outDegree DESC
LIMIT 5

练习 5.2 的解答如下。

列表 5.18 获取具有最高五个入度的用户

MATCH (u:User)
RETURN u.username AS user,
       count{ (u)<-[:FOLLOWS]-() } AS inDegree
ORDER BY inDegree DESC
LIMIT 5

练习 5.3 的解答如下。

列表 5.19 检查 NASA 的提及和发布的帖子

MATCH (u:User)
WHERE u.username = "NASA"
OPTIONAL MATCH m=(u)<-[:MENTIONS]-()
OPTIONAL MATCH p=(u)-[:PUBLISH]->()
RETURN m,p

练习 5.4 的解答如下。

列表 5.20 计算五个最大 WCC 的用户数量

MATCH (u:User)
WITH u.followerWcc AS componentId, count(*) AS countOfMembers
ORDER BY countOfMembers DESC
RETURN componentId, countOfMembers
LIMIT 5

练习 5.5 的解答如下。

列表 5.21 计算仅包含一个成员的 WCC 数量

MATCH (u:User)
WHERE NOT EXISTS {(u)-[:FOLLOWS]-()}
RETURN count(*) AS countOfComponents

练习 5.6 的解答如下。

列表 5.22 计算五个最大 SCC 的成员数量

MATCH (u:User)
WITH u.followerScc AS componentId, count(*) AS countOfMembers
ORDER BY countOfMembers DESC
RETURN componentId, countOfMembers
LIMIT 5

练习 5.7 的解答如下。

列表 5.23 在 Neo4j 浏览器中可视化第二大 SCC

MATCH p=(u1:User)-[:FOLLOWS]->(u2:User)
WHERE u1.followerScc = 380 AND u2.followerScc = 380
RETURN p

确保在 WHERE 子句中根据需要更正 followerScc 值。

练习 5.8 的解答如下。

列表 5.24 获取具有最高 PageRank 评分的前五名用户

MATCH (u:User)
RETURN u.username AS username, u.followerPageRank AS followerPageRank
ORDER BY followerPageRank DESC
LIMIT 5

练习 5.9 的解答如下。

列表 5.25 执行个性化 PageRank 算法,并使用 2019 年注册的 User 节点作为 sourceNodes 参数

MATCH (u:User)
WHERE u.registeredAt.year = 2019
WITH collect(u) AS sourceNodes
CALL gds.pageRank.stream('follower-network', {sourceNodes: sourceNodes})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).username AS user, score
ORDER BY score DESC
LIMIT 5

摘要

  • 实际网络遵循节点度数的幂律分布。

  • 对于有向网络,节点度数可以分为入度,即进入连接的数量,和出度,即出链的数量。

  • 图数据科学(GDS)库使用投影的内存图来执行图算法。

  • 本地投影是内存中图投影性能更好的变体。

  • 图算法的stream模式以记录流的形式返回结果,并不存储结果。

  • stats模式返回算法的摘要统计信息,并不存储结果。

  • 图算法的mutate模式将算法的结果存储回投影的内存图。

  • write模式将算法的结果存储到数据库中。

  • 弱连通分量(WCC)算法用于识别网络中的不连通部分或岛屿。

  • WCC 算法通常在分析早期使用,以评估图的连通性和结构。

  • 强连通分量(SCC)是有向图中所有节点之间都存在路径的子图。

  • 本地聚类系数(LCC)检查一个节点的邻居是如何紧密相连的。

  • 可以使用CASE子句来指定通用条件表达式。

  • PageRank 将每个关系视为一个影响力投票,可以用来找到图中最有影响力的节点。

  • PageRank 有一个阻尼因子参数,指定随机网络冲浪者应该多频繁地跟随出站链接,而不是跳转到随机节点。

  • 当使用默认的阻尼因子参数 0.85 运行 PageRank 算法时,没有传入关系的节点将具有 0.15 的 PageRank 得分。

  • 使用个性化 PageRank 变体,您可以指定随机游走者应该跳转到的节点,这为您提供了一个从特定视角看网络的方式。

  • 分析完成后,请确保使用gds.graph.drop过程从内存中释放投影图。

6 投影单部分网络

本章涵盖

  • 将间接图模式转换为直接关系

  • 使用 Cypher 投影将内存图投影出来

  • 展示自环

  • 介绍加权变体的度数和 PageRank 中心性算法

在上一章中,你对 Twitter 关注者网络进行了网络分析。决定从关注者网络开始是直截了当的。大多数图算法都是设计在单部分网络中执行的,其中只有一个节点和关系类型。然而,Twitter 社交网络模式包含多种节点类型和关系。而不是调整图算法以支持多部分网络(多个节点和关系类型),一般的方法是首先将单部分网络(单个节点和关系类型)投影出来。我在第二章中简要提到了这个概念,其中我介绍了一些如何在 Twitter 社交网络上进行单部分投影的选项。

假设你想分析内容在 Twitter 上的传播方式以及哪些用户是最有影响力的内容创作者。用户可以以推文的形式发布内容。然而,为了让推文触及更广泛的受众并可能成为病毒式传播,它必须被其他用户与他们的受众分享。在 Twitter 上,将他人的内容与你的受众分享称为转发。因此,为了了解哪些是最有影响力的内容创作者,你需要检查网络的转发行为。许多研究已经使用转发网络发表,从 2015 年关于欧洲议会的网络分析(Cherepnalkoski & Mozetič)到 COVID-19 大流行期间 Twitter 上与科学和健康相关的转发集群(Durazzi 等人,2021)。Twitter 上最有影响力的内容创作者可以定义为发布最多被转发的内容的用户。然而,你可以更进一步,假设不仅转发数量很重要,转发的人也很重要。例如,像 Andrew Ng 这样的用户,在机器学习领域享有盛誉,分享你的内容与一个刚刚加入平台且没有受众的人做同样的事情,这之间有很大的区别。因此,Andrew Ng 的转发给你的内容带来的影响力比一个没有受众的用户要大得多。

通常,中心性算法非常适合确定网络中最重要或最有影响力的节点。比如说,你决定使用 PageRank 算法,因为你已经在上一章中听说过它。PageRank 算法也是一个很好的选择,因为它既考虑了进入关系的数量,也考虑了这些关系来自哪些节点,以此来计算网络中一个节点的影响力。为了确定如何利用 PageRank 来识别最有影响力的内容创作者,你必须考虑你用来表示 Twitter 社交网络中转发模式的图模型。

在您的 Twitter 社交网络模式中,用户可以发布推文,如图 6.1 右侧所示。然而,当另一个用户重发一条推文时,会创建一个新的Tweet节点,并带有指向原始推文的RETWEETS关系。如果您要在图 6.1 的网络中执行 PageRank 算法,哪个节点最重要?请记住,PageRank 将每个关系视为信任或影响力的投票。然后,这种影响力在整个网络中流动。请花一两分钟思考一下,根据 PageRank 算法,图 6.1 网络中的哪个节点最重要。对我来说,从最不具影响力的节点开始,逐步向上到最重要的节点更容易。

06-01

图 6.1 表示重发模式图模型

图 6.2 可视化了重发模式,节点大小根据其 PageRank 分数进行缩放。节点越大,其 PageRank 分数越高。用户节点没有进入关系,因此它们的 PageRank 分数最低。原始推文和重发都有进入关系,因此它们比用户更重要。重发有一个进入关系,原始推文有两个进入连接。在 PageRank 算法中,计算 PageRank 分数时考虑了进入链接的数量以及链接到特定节点的节点的重要性。因此,并不总是说具有更多进入链接的节点会有更高的分数。然而,在图 6.2 的重发网络中,原始推文从其作者以及重发和重发的作者那里获得影响力。另一方面,重发节点只从重发的作者那里获得影响力,这意味着重发将比原始推文不那么重要。

06-02

图 6.2 表示节点大小代表其 PageRank 分数的重发模式

如图 6.2 所示,影响力从用户流向推文,并且可选地通过重发关系流向原始内容。理论上,重发次数最多的内容将具有最高的 PageRank 分数。有一些例外,因为推文和重发的数量随着用户发布的推文和重发数量的增加而降低其重要性;然而,这仍然是一个很好的近似,可以确定哪些推文被转发得最多。然后,您可以汇总用户推文的重要性,并识别最有影响力的用户。不幸的是,这种方法忽略了你定义有影响力内容创作者的一个主要项目。由于用户没有任何进入关系,所有用户的 PageRank 分数是相同的,算法无法区分重要用户和不重要用户。因此,PageRank 分数并不能反映有影响力的用户是否转发过内容。

有趣的是,在数据分析中,更常见的是根据算法调整数据集,而不是相反。为了确定哪些用户基于转发行为最具影响力,你必须以某种方式模拟用户之间的影响力流动。结果证明,在用户和推文都存在的网络中,将网络简化为只考虑用户的单部分网络是模拟用户之间影响力流动的最佳方式。由于单部分网络只包含一种类型的节点,你需要以某种方式排除 Tweet 节点,同时保留关于转发的信息。如果一个用户转发另一个用户的帖子,他们会增加或放大原始推文的覆盖范围,从而也增加了原始推文作者的覆盖范围。你可以用一个直接关系来表示用户如何放大其他用户的 内容覆盖范围。你想要给这个新的 推断关系 起什么名字取决于你的领域和用例。在这个例子中,我将这个新的关系类型命名为 AMPLIFY,因为它用来表示用户通过转发如何相互放大对方的覆盖范围。推断关系 这个术语意味着这种关系在数据中并没有明确定义,而是基于某些假设推断或创建的。

图 6.3 展示了将两个 User 节点之间跨越三个关系的图模式转换为两个节点之间直接链接的概念。在这个可视化的左侧是存储在数据库中的转发模式。然而,由于你想根据转发模式使用 PageRank 算法评估用户的影响力,你需要将两个用户之间的间接路径转换为直接关系,如图 6.3 的右侧所示。影响力将通过直接关系在用户之间流动,以模拟转发行为。因此,当在单部分网络中执行时,PageRank 算法将考虑转发次数以及谁转发了内容,在最终得分中都会考虑,其中转发模式被模拟为直接关系。

06-03

图 6.3 将间接转发模式转换为直接的 AMPLIFY 关系

你可以用 Cypher 语句轻松描述这种网络转换。

列表 6.1 描述将间接转发模式转换为直接 AMPLIFY 关系的翻译

MATCH (s:User)-[:PUBLISH]->()-[:RETWEETS]->()<-[:PUBLISH]-(t:User)
CREATE (s)-[:AMPLIFY]->(t);

在将所有转发模式转换为直接关系之后,你最终会得到一个只包含 User 节点和 AMPLIFY 关系的单部分网络(图 6.4)。

图 6.4 可视化了一个投影或推断的单部分网络子图,它代表了基于转发模式的User节点和AMPLIFY关系,构建了基于转发模式。由于一个用户可以从其他用户那里多次转发帖子,你可以将计数存储为关系属性。因此,你可以将图 6.4 中的网络描述为有向和加权的。许多研究人员(Evkoski 等,2020;Evkoski 等,2021;Priyanta & Prayana Trisna,2019)已经使用了表示只有用户节点和表示用户之间转发的关系的简化单部分网络的概念。

06-04

图 6.4 投影的单部分网络,包括User节点和AMPLIFY关系

基于假设转发意味着用户喜欢原始推文的内文,推断出的放大网络可以用来检查产生最佳(最具可分享性)内容的用户。我想象如果你想要表达对推文内容的反对意见,你会引用推文并描述你对推文的争议。你定义说,引用和转发应该存储在原始图模式的不同关系类型下。然而,由于数据集中没有引用,你可以假设所有转发都是积极的,这意味着用户同意原始推文的内容。

你将通过实际示例了解更多关于推断单部分网络的详细信息和注意事项。为了跟随本章的练习,你需要将 Twitter 网络导入到 Neo4j 数据库中,如第三章所述。

6.1 将间接多跳路径转换为直接关系

你将首先将多跳转发关系转换为直接的AMPLIFY关系(图 6.5)。使用 Neo4j 图数据科学(GDS),你可以采取两种不同的方法来完成这个任务。

06-05

图 6.5 在投影图中将间接多跳路径转换为直接关系的两种选项

要使用原生投影投影单部分转发网络,你必须首先在 Neo4j 数据库中实例化它。原生投影在图加载期间不提供自定义网络转换。另一方面,你可以使用 Cypher 投影将一个虚拟图加载到内存中。在这个上下文中,虚拟图是一个不在数据库中存储,仅在投影时构建的图。能够在不将自定义转换存储在数据库中的情况下进行投影是一个很好的特性,它让你可以探索各种图投影并在保持图数据库清洁的同时分析它们。Cypher 投影具有 Cypher 查询语言的全部表达能力,用于选择、过滤和转换要投影的图。在下面的子节中,你将学习如何使用 Cypher 投影来避免在数据库中存储单部分转发网络。

6.1.1 Cypher 投影

如下所示,Cypher 投影是一种更灵活和表达性的方法来投影内存图。如您从该功能的名称中推断出的那样,您可以使用 Cypher 语句来定义您想要加载到内存图中的节点和关系。Cypher 投影函数称为 gds.graph.project,有三个强制参数和两个可选参数。

列表 6.2 Cypher 投影语法

MATCH (sourceNode)-[relationship]->(targetNode)  ❶
RETURN gds.graph.project(
   'graph',                                      ❷
   sourceNode,                                   ❸
   targetNode,                                   ❹
   {dataConfig},                                 ❺
   {configuration}                               ❻
) YIELD
    graphName,
    nodeCount,
    relationshipCount

❶ 匹配所需图模式的 Cypher 语句

❷ 投影图名称

❸ 关系的源节点

❹ 关系的目标节点

❺ 可选的属性和类型配置映射

❻ 定义无向关系的可选参数映射

您可以将 Cypher 投影视为使用 Cypher 语句描述投影图的连接关系,其中每个连接都由其源节点和目标节点定义。首先,您必须使用 Cypher 语法匹配您想要投影的关系的源节点和目标节点。如前所述,您可以匹配数据库中的现有关系或定义数据库中未实体化的虚拟连接。一旦指定了所需关系的源节点和目标节点,您就可以在 WITHRETURN 子句中使用 gds.graph.project 函数来投影图,而不是必须使用 CALL 子句。gds.graph.project 函数的第一个参数用于定义投影内存图的名称。另一方面,第二个和第三个参数描述了投影关系的源节点和目标节点。第四个参数是可选的,用于指定节点和关系属性及其标签或类型(如果需要)。通过定义节点标签和关系类型,您可以在算法运行时有效地过滤它们。

注意 Cypher 投影是描述您投影的图的一种更灵活和表达性的方法。本质上,它是一种使用节点和关系列表定义投影图的方式。由于关系列表是使用 Cypher 语句定义的,您可以利用 Cypher 查询语言的全部表达性来过滤或转换所需的图投影,而无需首先在数据库中将其实体化。然而,这种方法有一个缺点,即 Cypher 投影的性能不如原生投影。由于其性能较差,Cypher 投影不建议用于大型图或生产阶段。

接下来,您将使用 Cypher 投影来将重推放大网络加载为内存图。在使用 Cypher 投影加载内存图并执行图算法之前,您必须准备关系 Cypher 语句。

练习 6.1

MATCH语句中描述重推模式。在source列下返回重推其他用户的用户,在target列下返回被重推的用户。由于一个用户可以多次重推另一个用户,因此计算重推次数并在weight列下返回重推次数。只返回结果的前五行。

现在您可以将练习 6.1 中使用的 Cypher 语句作为gds.graph.project函数的输入,以使用 Cypher 投影在内存中生成图。

列表 6.3 使用 Cypher 投影将放大重推网络加载为内存图

MATCH (source:User)-[:PUBLISH]->()-[:RETWEETS]->()<-[:PUBLISH]-(target:User)
WITH source, target, count(*) AS weight      ❶
WITH gds.graph.project(
    'amplify',
    source,
    target,                                  ❷
   {relationshipProperties:{weight:weight}}
) AS g
RETURN g.graphName AS graph,
       g.nodeCount AS nodes,
       g.relationshipCount AS rels   

❶ 匹配重推模式并计算一对用户之间的重推次数

❷ 使用 Cypher 投影在内存中生成图

列表 6.3 中的 Cypher 语句首先匹配重推模式。重推用户指定为source节点,而被重推的用户描述为target节点。接下来,您使用count()函数计算一对用户之间的重推次数。现在关系列表已准备就绪,您可以使用gds.graph.project函数进行投影。在这个例子中,您提供了sourcetarget节点以及数据配置参数中指定的关系属性weight。请检查文档以获取可能的配置键的完整列表:mng.bz/GyJO。Cypher 投影函数将返回表 6.1 所示的输出。

表 6.1 Cypher 投影函数输出

graphName nodeCount relationshipCount
"amplify" 1828 2719

在投影的内存amplify图中,有 1,828 个节点和 2,719 个关系。重推放大网络可以被描述为一个有向、加权的网络。

6.2 重推网络特征描述

接下来,您将对重推放大网络进行简短的网络分析。这次分析旨在加强您执行图算法的经验。

6.2.1 度中心性

首先,您将评估推断网络的节点度分布。在前一章中,您使用了一个普通的 Cypher 语句来计算和可视化节点度分布。在这里,重推放大网络没有在数据库中实现,因此您没有使用普通 Cypher 语句来计算节点度分布的选项。相反,您可以使用 GDS 度中心性算法gds.degree来评估节点度分布

您可以使用算法的stats模式来检查节点度分布,如下所示。默认情况下,gds.degree中心性计算的是出度。请记住,出度是一个节点拥有的 outgoing 关系的计数,而入度是 incoming 链接的计数。

列表 6.4 评估推断重推放大网络的出度分布

CALL gds.degree.stats('amplify')
YIELD centralityDistribution

表 6.2 显示了结果分布。

表 6.2 重推放大网络的出度分布

度量
p99 18.00011444091797
min 0.0
max 146.00096893310547
mean 1.4874205599728507
p90 2.0000076293945312
p50 1.0
p999 61.00023651123047
p95 4.000022888183594
p75 1.0

在网络中,一个节点平均有大约 1.5 个出度关系。pX 值表示百分位数;例如,p75 表示 1.0 的 75 个百分位数,这意味着 75% 的节点有 1 或 0 个出度关系。你可以推断出推断的重推网络是稀疏的。

练习 6.2

使用度中心性算法计算并返回重推放大网络中出度最高的前五个节点。使用算法的 stream 模式来流式传输结果,而不将其存储在数据库或投影图中。gds.degree 算法的 stream 模式输出 nodeIdscore 列。使用 gds.util.asNode 将节点 ID 映射到节点实例,并检索前五个节点的 username 属性。

练习 6.2 的解决方案生成了表 6.3 中所示的结果。

表 6.3 重推放大网络中按出度排名前五的用户

user score
"textsla" 146.0
"godfrey_G_" 61.0
"iPythonistaBot" 48.0
"Beka "Bexx" Modebade" 36.0
"chidambara09" 33.0

用户 textsla 从 146 个不同的用户那里转发了帖子。如果这个列表上的大多数用户都为特定的标签设置了自动重推,我并不会感到惊讶。由于你处理的是一个加权网络,你也可以评估加权出度分布。大多数 GDS 库图算法通过使用 relationshipWeightProperty 配置参数支持算法的加权变体。你可以使用以下 Cypher 语句评估重推放大网络的加权出度分布。

列表 6.5 评估推断重推放大网络的加权出度分布

CALL gds.degree.stats('amplify', {relationshipWeightProperty:'weight'})
YIELD centralityDistribution

表 6.4 显示了结果分布。

表 6.4 重推放大网络的加权出度分布

度量
p99 65.00048065185547
min 0.0
max 2,006.0078048706055
mean 4.715000173456038
p90 3.0000076293945312
p50 1.0
p999 670.0038986206055
p95 7.000022888183594
p75 1.0

尽管平均加权出度为 4.7,但第 75 个百分位数仅为 1,第 90 个百分位数仅上升到 3。这似乎表明有一些异常值提高了整个群体的平均值。例如,一个用户有 2,006 次重推。鉴于这个数据集中大多数重推发生在三天窗口内,我敢猜测最高重推用户可能设置了一些重推自动化。

练习 6.3

使用度中心性算法计算并返回在转发放大网络中具有最高加权出度的前五个节点。解决方案几乎与练习 6.2 相同,只是你包括了relationshipWeightProperty参数来计算加权出度。

出度节点度可以帮助你评估和识别通过网络传播或分发内容最多的用户。另一方面,你可以使用入度分布来识别产生最多可分享(也许最好?)内容的用户。节点度中心性算法有一个orientation参数,允许你评估入度、出度或两者的组合。orientation参数有三个可能的输入:

  • NATURAL—评估出度(出关系数量)

  • REVERSE—评估入度(入关系数量)

  • UNDIRECTED—评估入度和出度的总和

因此,你可以通过将orientation参数设置为REVERSE来评估入度分布。

列表 6.6 返回具有最高入度的前五个用户

CALL gds.degree.stats('amplify', {orientation:'REVERSE'})
YIELD centralityDistribution

表 6.5 显示了结果分布。

表 6.5 转发放大网络的入度分布

度量
p99 29.00011444091797
min 0.0
max 117.00048065185547
mean 1.4874205599728507
p90 3.0000076293945312
p50 0.0
p999 96.00048065185547
p95 7.000022888183594
p75 1.0

超过 50%的用户甚至没有被转发过一次。从某种意义上说,转发内容的用户少于转发用户是有道理的。如果你还记得上一章,入度和出度的平均值总是相同的,因为连接数和用户数保持不变,只有关系方向相反。有趣的是,在这个例子中,第 75 百分位数和第 90 百分位数是相同的。这似乎表明入度分布比出度分布更偏向于顶部。这表明几个内容创作者持续产生被转发的内容。也许这意味着生产高质量的内容,但我们需要进一步调查。也许,只有他们的标签游戏很强大。

练习 6.4

使用度中心性算法计算并返回在转发放大网络中具有最高入度的前五个节点。使用算法的stream模式来流式传输结果,而不将其存储在数据库或投影图中。

练习 6.4 的解决方案产生了表 6.6 所示的结果。

表 6.6 转发放大网络中按入度排名前五的用户

user score
"Paula_Piccard" 117.0
"IainLJBrown" 96.0
"Eli_Krumova" 90.0
"Analytics_699" 69.0
"annargrs" 65.0

共有 117 个不同的用户重推了 Paula_Piccard。第一名和第五名之间的差距不如出度分布大。您可能会认为他们产生相关且高质量的内容,因为他们经常被重推。为了获得更准确的结果,您可能需要抓取更多带有相关标签的推文。

练习 6.5

使用度中心性算法的 stats 模式评估加权入度分布。如果您需要一些帮助,可以查看加权出度的示例,并包括 orientation 参数。之后,使用算法的 stream 模式来识别具有最高五个加权入度的用户。

现在,您将执行第五章中学到的弱连接组件(WCC)算法,以巩固您的知识。

6.2.2 弱连接组件

WCC 算法应该是几乎所有网络分析的一部分。通过它,您可以评估网络的连接程度并识别断开连接的组件。

练习 6.6

使用 gds.wcc.stats 程序评估弱连接组件大小的分布。

表 6.7 显示了 WCC 算法产生的统计数据。

表 6.7 在重推放大网络上执行 WCC 算法的摘要统计数据

componentCount componentDistribution
207

最大的组件由 1,082 个成员组成,约占重推放大网络中总用户的 60%。如前所述,大多数现实世界的网络都有一个包含大多数网络节点的单个超级组件,旁边有几个较小的组件。对我来说奇怪的是,最小尺寸的组件只包含一个成员。使用 Cypher 投影,您已经过滤了重推或被重推的用户。我的第一个想法是,不应该有只包含一个成员的组件。由于这是一个意外的结果,值得探索。您可以使用以下 Cypher 语句来检查具有单个成员的样本组件。

列表 6.7 使用单个成员检查样本组件

CALL gds.wcc.stream('amplify')
YIELD nodeId, componentId                              ❶
WITH componentId, collect(nodeId) AS componentMembers,
     count(*) AS componentSize                         ❷
WHERE componentSize = 1                                ❸
WITH componentMembers[0] AS id
LIMIT 3                                                ❹
MATCH p=(n)-[:PUBLISH]->()-[:RETWEETS]-()              ❺
WHERE id(n)=id
RETURN p

❶ 执行 WCC 算法的流模式

❷ 按组件 ID 分组收集节点

❸ 过滤只包含单个成员的组件

❹ 从列表中提取三个样本组件的节点 ID

❺ 匹配他们的重推

列表 6.7 中的 Cypher 语句首先在转推放大网络上执行 WCC 算法的stream模式。WCC 算法的stream模式输出nodeId列,表示节点的内部节点 ID,以及componentId列,它描述节点属于哪个组件。在 Cypher 语句的下一步中,你通过componentId进行聚合,以计算组件大小并收集其成员的节点 ID。之后,你使用WHERE子句过滤出只有一个成员的组件。由于单个成员组件的componentMembers列表中应该只有一个元素,你可以很容易地使用方括号语法结合其索引位置提取唯一的节点 ID。为了不让结果可视化过于繁杂,你将只检查具有单个成员的三个组件。最后,你需要匹配三个特定节点 ID 的转推模式。列表 6.7 中的 Cypher 语句将在 Neo4j 浏览器中产生图 6.6 所示的可视化。

06-06

图 6.6 包含单个成员的组件,其中用户转发了自己的推文

在 Twitter 上,用户也可以转推自己的帖子。在图论中,自环是与相同起始和结束节点的关系。如前所述,WCC 算法有助于识别网络的连接程度以及识别各种意外的模式。

6.3 识别最有影响力的内容创作者

本章任务的主要目标是识别 Twitter 社交网络数据集可用子集中的最有影响力的内容创作者。你将通过以下两个步骤来确定最重要的内容创作者:

  1. 在投影过程中忽略自环。

  2. 执行 PageRank 算法的加权变体。

6.3.1 排除自环

我认为在执行加权 PageRank 算法之前排除网络中的所有自环是有意义的。自环可以被解释为节点声明它是有影响力的。我认为转发自己的推文不应该增加你在网络中的影响力。不幸的是,没有一键排除自环的魔法按钮,所以你必须使用 Cypher 投影在内存中投影另一个图。

练习 6.7

使用 Cypher 投影将转推放大网络加载到内存中并排除所有自环。本质上,你只需要更改关系 Cypher 语句以过滤出起始和结束节点相同的边。将新的投影图命名为amplify-noselfloops

6.3.2 加权 PageRank 变体

接下来,你将执行加权 PageRank 算法以识别潜在的内容影响者。记住,PageRank 算法考虑了指向节点的连接数量和节点的重要性。你不仅分析哪个用户拥有最多的转发,还在评估网络中哪些其他有影响力的节点也转发了它们。

加权版本的 PageRank 算法在计算节点重要性时也考虑了关系权重。在未加权的 PageRank 版本中,节点的重要性在它的邻居之间平均分配。另一方面,在加权版本中,每个邻居都获得与关系权重相关的重要性份额(图 6.7)。

06-07

图 6.7 单次迭代中加权与无加权 PageRank 计算的差异

如前所述,加权版本的 PageRank 算法在计算影响如何在网络中传播时考虑了关系权重。图 6.7 可视化了一个由三个节点组成的简单网络。加权版本和无加权版本之间的区别在于节点 A 如何传播其影响。在无加权版本中,节点 B 和 C 从节点 A 那里获得相等的重要性份额。在加权网络中,节点 A 到 C 的关系权重为 1,节点 A 到 B 的连接值为 2。在加权 PageRank 算法的每一次迭代中,节点 B 将接收到节点 A 三分之二的影响,而节点 C 将只接收到三分之一。使用加权 PageRank 算法计算影响份额的方程只是将关系权重除以所有输出关系权重的总和。

现在,你可以在没有自环的转发放大网络上执行加权 PageRank 算法。同样,与度中心性一样,你只需要包含relationshipWeightProperty参数来执行算法的加权版本。你必须完成练习 6.7,然后才能执行以下 Cypher 语句。

列表 6.8 在没有自环的转发放大网络上执行 PageRank 算法

CALL gds.pageRank.stream('amplify-noselfloops',
  {relationshipWeightProperty:'weight'})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).username AS user, score
ORDER BY score DESC
LIMIT 5

表 6.8 显示了按 PageRank 分数排序的该示例的前五名用户。

表 6.8 无自环的转发放大网络中按加权 PageRank 分数排序的前五名用户

用户 分数
"Paula_Piccard" 8.270755243786214
"annargrs" 7.836125000000006
"psb_dc" 7.478576023152348
"IainLJBrown" 7.457764370226901
"Eli_Krumova" 6.95963977383344

按照加权 PageRank 排名前五的用户列表与按入度排名前五的用户列表相似。当然,你正在分析的 Twitter 子图相对较小。虽然你还没有分析推文主题或标签,但我提到我已经通过关注自然语言处理(NLP)和知识图谱主题来抓取数据集。因此,如果你对 NLP 或知识图谱主题更新感兴趣,表 6.8 中的用户可能是 Twitter 上值得关注的对象。此外,作为一种营销策略,你可以尝试联系这些用户,看看他们是否愿意分享你的内容。

6.3.3 删除投影的内存图

在分析完成后,记得释放投影的内存图以释放内存,以便进行其他分析。目前,你应该有两个图已加载到内存中。以下 Cypher 语句将删除所有当前投影图。

列表 6.9 从内存中释放所有投影图

CALL gds.graph.list() YIELD graphName           ❶
CALL gds.graph.drop(graphName) YIELD nodeCount
RETURN 'dropped ' + graphName AS result         ❷

❶ 列出所有投影图

❷ 从内存中释放每个投影图

6.4 练习解答

练习 6.1 的解答如下。

列表 6.10 计算用户之间转发模式的出现次数

MATCH (source:User)-[:PUBLISH]->()-[:RETWEETS]->()<-[:PUBLISH]-(target:User)
RETURN source, target, count(*) AS weight
LIMIT 5

练习 6.2 的解答如下。

列表 6.11 返回具有最高五个加权出度的用户

CALL gds.degree.stream('amplify')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).username AS user, score
ORDER BY score DESC
LIMIT 5

练习 6.3 的解答如下。

列表 6.12 返回具有最高五个加权出度的用户

CALL gds.degree.stream('amplify',
  {relationshipWeightProperty:'weight'})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).username AS user, score
ORDER BY score DESC
LIMIT 5

练习 6.4 的解答如下。

列表 6.13 返回具有最高五个加权入度的用户

CALL gds.degree.stream('amplify', {orientation:'REVERSE'})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).username as user, score
ORDER BY score DESC
LIMIT 5

练习 6.5 的解答如下。

列表 6.14 评估加权入度分布

CALL gds.degree.stats('amplify', {orientation:'REVERSE',
  relationshipWeightProperty:'weight'})
YIELD centralityDistribution

列表 6.15 返回具有最高五个加权入度的用户

CALL gds.degree.stream('amplify', {orientation:'REVERSE',
  relationshipWeightProperty:'weight'})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).username as user, score
ORDER BY score DESC
LIMIT 5

练习 6.6 的解答如下。

列表 6.16 评估弱连通组件的数量和大小

CALL gds.wcc.stats('amplify')
YIELD componentCount, componentDistribution

练习 6.7 的解答如下。

列表 6.17 使用 Cypher 投影将放大转发网络作为内存图加载,并排除自环

MATCH (source:User)-[:PUBLISH]->()-[:RETWEETS]->()<-[:PUBLISH]-(target:User)
WHERE NOT source = target
WITH source, target, count(*) AS weight
WITH gds.graph.project(
    'amplify-noselfloops',
    source,
    target,
    {relationshipProperties:{weight:weight}}
) AS g
RETURN g.graphName AS graph,
       g.nodeCount AS nodes,
       g.relationshipCount AS rels

摘要

  • 推断单连通网络是图分析中常见的步骤。

  • Cypher 投影可用于投影虚拟图(数据库中不存在的关联)。

  • Cypher 投影是一种更灵活但性能较低的内存图投影选项。

  • Cypher 投影接受一个定义节点的 Cypher 语句,以及另一个指定应投影的关系的 Cypher 语句。

  • 使用 Cypher 投影定义节点时,保留的列有 idlabels

  • 使用 Cypher 投影定义与关系时,保留的列有 sourcetargettype

  • GDS 库使用两个方向相反的定向关系来表示无向关系。

  • 在内存图投影期间,你可以更改关系方向或将它视为无向。

  • 可以使用gds.degree.stats过程来评估度中心性分布。

  • PageRank 的加权变体在计算影响力如何在图中传播时考虑了关系权重。

  • 通过定义relationshipWeightProperty参数,你可以使用度中心性和 PageRank 算法的加权变体。

  • 可以使用nodeLabelsrelationshipTypes参数来仅考虑投影图的子集作为图算法的输入。

  • 自环是一种具有相同起始节点和结束节点的关系。

7 基于二部网络推断共现网络

本章涵盖

  • 使用 Cypher 查询语言从推文中提取标签

  • 计算 Jaccard 相似系数

  • 使用 Jaccard 相似系数构建和分析单部网络

  • 使用标签传播算法评估网络的社区结构

  • 使用 PageRank 在社区内找到最重要的节点

在上一章中,你学习了如何将自定义图模式转换为直接关系,以便将它们用作 PageRank 等图算法的输入。在本章中,你将专注于二部网络以及如何将它们投影到单部网络。让我们先快速回顾一下二部网络。

二部网络包含两个集合或类型的节点。例如,图 7.1 显示了左侧的推文二部网络和右侧的标签。正如你所观察到的,关系总是从一个类型的节点指向另一个类型的节点。推文或标签之间没有直接连接。

07-01

图 7.1 推文和标签的二部网络

假设你在一个处理自然语言处理(NLP)和知识图谱的公司担任市场分析角色。你的老板决定改进发布在 Twitter 上的内容标签策略可能是有价值的。你被分配了识别相关标签以尽可能好地定位公司理想客户的任务。由于你迄今为止使用的 Twitter 数据集包含关于 NLP 和知识图谱的推文,你可以分析数据集中的标签以确定公司应该定位哪些标签。

你的第一个想法可能是使用 PageRank 算法来识别最重要的标签。记住,像 PageRank 这样的图算法期望输入单部网络或至少是影响力可以贯穿连接的网络。对于二部网络,一种类型的节点只有入度关系,另一种类型的节点只有出度连接。如果你要对推文和标签的二部网络示例执行 PageRank,你认为哪些节点会排在最前面?

由于推文没有任何入度关系,它们的 PageRank 分数将等于网络冲浪者随机访问它们的概率。默认情况下,阻尼因子为 0.85,没有入度连接的节点的 PageRank 分数为 0.15。另一方面,标签只有入度关系。影响力从推文流向标签,但不会进一步流动,因为没有从标签出发的出度连接。在实践中,标签的 PageRank 排名将等于它们入度关系的数量。然而,由于不同的分数计算技术,PageRank 和入度的实际值会有所不同。

记住,目标是确定数据集中最重要的和相关的标签。如果标签重要性的定义仅仅是它们的频率,那么使用Tag节点的入度指标就足够了。

然而,数据集中的某些标签可能并不与营销目标相关。仅通过查看最常提到的标签,你可能会完全错过其他相关的标签,因为你不知道数据集是否偏向于特定主题。此外,战略性地组合标签可以通过针对更广泛的受众而不是仅使用最常提到的标签来显著提高内容的覆盖范围。因此,你的任务是找到重要的标签,以及可以组合使用以增加病毒性的其他标签。用于识别常用标签社区的一种常用技术是检查标签对在推文中共现的频率。通过分析标签对在相同推文中共现的频率,你构建了一个共现网络。术语“共现网络”指的是一种网络构建方法,它分析文本中各种实体之间的关系。在推文及其标签的情况下,你可以使用共现网络方法来分析推文文本中出现的标签之间的关系。

图 7.2 显示了医学文章中关键词的共现网络。共现网络是通过使用定义共现的一组标准将文本中的实体对连接起来构建的。共现的定义可能因场景而异。在这个例子中,共现被定义为两个关键词出现在同一篇文章中。一对关键词在同一篇文章中出现的次数越多,它们之间的联系就越强。类似的技巧被用来分析围绕 COVID-19 研究的科学文献(Al-Zaman,2021;Andersen 等人,2020)。此外,研究人员还使用生物医学文献共现网络来预测新的链接(Kastrin 等人,2014)。

07-02

图 7.2 医学关键词共现网络

然而,你不仅限于分析给定文本中关键词的共现。在《权力的游戏》分析(Beveridge 等,2018)中,Andrew Beveridge 通过共现实体网络的角度普及了分析书籍的方法。《权力的游戏》书籍分析包括两个步骤。首先,Beveridge 确定了书中的所有角色。在下一步中,他定义了一对角色如果在彼此 15 个单词之内出现,则它们之间存在共现事件。在这种情况下,共现事件可以理解为角色之间的互动。在我之前的工作中,我使用这种技术构建了《哈利·波特与魔法石》书中角色之间的共现或互动网络。

只需看图 7.3,你就可以评估《哈利·波特》第一本书中的主要角色,并评估它们之间的互动。节点大小基于节点度,所以节点连接越多,节点大小就越大。哈利·波特与其他角色互动最多是合情合理的,因为这本书的叙述主要从他的视角来写。

07-03

图 7.3 《哈利·波特》第一本书中字符共现网络

共现分析的前两个例子展示了从文本中提取关键词或实体,然后通过任意共现事件连接的情景。我准备的第三个例子是可视化各种菜肴或食谱中成分的共现情况。再次强调,图 7.4 中的节点大小基于其连接数。在左侧社区的中心,你可以观察到鸡蛋、面粉、糖和牛奶。我想象这些成分的菜肴大多属于面包、煎饼或甜点。出于某种原因,花生酱也频繁出现在这个社区中。在右侧,你可以观察到主菜类型的成分,包括洋葱、番茄酱、土豆和肉类。有趣的是,一篇研究论文(Cooper,2020)使用成分共现网络分析美国的包装食品。另一篇研究论文(Kular 等,2011)使用成分共现网络来研究菜系与各种文化之间的关系。

07-04

图 7.4 菜肴中成分的共现网络

现在让我们回到识别最优化策略的任务,结合使用知识图谱和 NLP 兴趣来找到能够触达用户的各种标签。你可以从一个假设开始,即如果两个标签在同一个推文中同时出现,它们可能在某种程度上相关或属于相似的主题。基于这个假设,共现网络方法会将可能具有相同或相似整体主题的标签连接起来。共现网络还可以帮助你产生可以一起使用的各种标签组合。

图 7.5 展示了基于原始推文和标签的二分网络构建标签共现网络的过程。在可视化的左侧是原始的二分网络。一条推文可以包含多个标签。例如,推文 A 包含#Growth 和#Startup 标签。在这个例子中,共现被定义为在同一个推文中同时出现的标签对。因此,在右侧的可视化中,即共现网络可视化的部分,#Finance 和#Startup 标签之间存在关系,因为它们出现在同一个推文中。如果你看推文 D,你会注意到它有三个标签:#Data、#ML 和#NLP。由于这些标签在同一个推文中共现,因此所有三个标签之间都有关系,如图 7.5 的右侧所示。

07-05

图 7.5 基于标签在同一个推文中是否共现构建标签共现网络

这项数据转换可以用以下 Cypher 语句来描述。

列表 7.1 使用 Cypher 语句描述构建标签共现网络的过程

MATCH (s:Tag)<-[:HAS_TAG]-(:Tweet)-[:HAS_TAG]->(t:Tag)
CREATE (s)-[:CO_OCCURRENCE]->(t);

标签用于索引或定义推文的主题。通过分析标签的共现网络,你可以了解哪些标签经常重叠,并可能属于相同的整体主题。构建 Twitter 标签共现网络已在多项研究中使用。例如,你可以分析标签如何帮助推动病毒式传播。一项研究(Pervin,2015)得出结论,当标签与相似标签共现时,其流行度会上升。另一方面,如果标签与不相似的标签一起出现,其流行度会下降。像 Twitter 这样的社交网络在传播有关当前社会运动或事件的信息方面变得越来越重要。研究人员已经确定了可以用来帮助从不同的社交圈中获得可见性的标签组合策略,从而增加某些社会运动的病毒性(Wang 等,2016;Gleason,2013)。另一篇论文研究了标签共现连接与它们的语义相似性之间的差异(Türker & Sulak,2018)。最后,一篇令人兴奋的文章(Vitale,2018)使用标签共现网络提供有关吸烟习惯的新信息和趋势。

为了在知识图谱和自然语言领域内找到最相关的标签组,你必须首先确定哪些标签经常一起使用,并且有可能形成一个针对特定用户群体的主题。在这里,你回到了之前的假设。如果两个标签在同一个推文中频繁共现,你可以假设它们是相关的,并属于同一主题。为了找到形成主题的标签社区或聚类,你可以利用社区检测算法,如标签传播算法(LPA)。LPA 是一种评估网络社区结构的算法。互联网上的大部分文献都将 LPA 介绍为一种半监督算法,其中你可以为网络中某些节点的初始社区输入。然而,在这里,你将使用 LPA 的无监督变体,因为你不会提供任何初始社区。LPA 的无监督变体工作如下。首先,它为每个节点分配一个唯一的社区标签。然后,它遍历网络,并将每个节点的标签更新为其大多数邻居所拥有的标签。这种迭代背后的想法是,单个社区标签可以迅速在紧密连接的节点组中占据主导地位。一旦 LPA 达到收敛,算法停止,结果节点标签代表它们的社区。

社区代表节点之间紧密连接的群体,群体之间则通过稀疏的链接相连。例如,图 7.6 展示了两个由标签组成的群体或社区。左侧的社区包含#Finance、#Growth 和#Startup 标签。你可以将左侧社区分配为一个更偏向商业化的主题。在图 7.6 的另一侧,右侧的社区由#ML、#Data 和#NLP 标签组成。同样,你可以尝试推断右侧社区的整体主题。在这个例子中,类似于计算机科学或数据科学导向的主题可能比较合适。

07-06

图 7.6 在标签共现网络中识别社区

在你确定了形成主题的标签社区之后,你可以使用 PageRank 算法来找到社区中最中心的节点。记住,我们假设一对标签之间的共现意味着它们在某种程度上是相关或相似的。如果你要在每个社区分别执行 PageRank 算法,你会识别出社区中最中心的节点。你可以假设社区中最中心的节点是其代表,因为 PageRank 算法将每条关系视为一次投票。在标签共现网络示例中,这是一次相似度或相关性的投票。所以,与社区中所有其他标签最相似的标签将排名最高,你可以将其解释为社区代表。为了跟随本章的练习,你需要将 Twitter 网络导入到第三章中描述的 Neo4j 数据库中。

7.1 从推文中提取标签

在您能够推断出标签共现网络之前,您首先必须从推文内容中提取标签。您只会从非转发推文中提取标签,因为转发推文与原始推文具有相同的标签,这只会扭曲结果。提取标签的过程,如列表 7.2 所示,是基本的文本处理。您通过空格或换行符分割推文文本,以创建一个单词列表。然后,您需要过滤掉以哈希符号(#)开头的单词。一旦完成这两个步骤,您就成功从推文中提取了标签,并且可以将它们存储在数据库中。

列表 7.2 从非转发推文中提取标签

MATCH (t:Tweet)
WHERE NOT EXISTS { (t)-[:RETWEETS]->() }                ❶
WITH t, replace(t.text, '\n', ' ') AS cleanText         ❷
WITH t, split(cleanText, ' ') AS tokens                 ❸
WITH t, [el IN tokens WHERE el STARTS WITH "#" |
            toLower(replace(el,",",""))] AS hashtags    ❹
WHERE size(hashtags) > 0                                 ❺
RETURN hashtags LIMIT 5

❶ 匹配所有非转发推文

❷ 将所有换行符替换为空格

❸ 使用空格字符分割每条推文的内容以提取单词

❹ 使用列表推导式过滤以哈希字符开头的单词,并从标签中删除逗号

❺ 过滤至少包含一个标签的推文

表 7.1 显示了提取的标签。

表 7.1 提取的标签

标签
["#心态", "#自然语言处理", "#冥想", "#心算", "#生物共振", "#心智教练", "#焦虑", "#催眠", "#心理健康"]
["#sql", "#代码", "#数据科学", "#人工智能", "#机器学习", "#机器学习", "#物联网", "#IIoT", "#物联网平台", "#python", "#rstats", "#云", "#网络安全"]
["#教练", "#自然语言处理", "#进步而非完美", "#成长心态", "#训练你的大脑", "#先养育自己"]
["#acl2021nlp"]
["#medium", "#机器学习", "#自然语言处理", "#深度学习", "#自动纠正", "#拼写检查", "#人工智能", "#tds", "#python"]

有趣的是,NLP 标签在技术社区中指的是自然语言处理,而个人发展社区则将 NLP 作为神经语言程序学的缩写。虽然我旨在抓取关于自然语言处理和知识图谱主题的推文,但似乎数据集中也有一些自我提升的推文。数据集中有更多不同的主题并不是问题。这使得您的分析更有趣,因为您将了解更多计算机科学导向主题背后的驱动标签,并且您可以稍后自行分析自我帮助主题。

大多数营销平台允许您指定关键词或标签,并排除不需要的标签。由于您作为营销分析师的目标是在 Twitter 上设计定位策略,您也可以准备一个要排除的标签列表。该公司从事 NLP 业务,因此您会想定位到#nlp标签。您已经了解到#nlp标签在自我帮助主题中也非常流行,由于自我帮助主题与公司无关,排除属于自我帮助主题的标签是有意义的。

列表 7.2 中的 Cypher 语句首先匹配所有没有出向RETWEETS关系的推文。为了为每条推文创建一个单词列表,你使用replacesplit函数的组合。首先,你使用replace函数将所有换行符替换为空白字符。replace函数的语法如下。

列表 7.3 replace函数语法

replace(string, search, replace)

replace是大多数脚本语言和查询语言中的一个基本函数,因此我希望它不需要任何额外的解释。同样,split也是一个非常基本的函数,其语法如下。

列表 7.4 split函数语法

split(string, delimiter)

split函数的输入是一个字符串和一个分隔符字符,输出是一个元素列表;再次强调,这是大多数(如果不是所有)编程语言中可用的一项基本功能。最后要做的就是过滤以#字符开头的单词。你可以使用以下列表推导式语法来从单词列表中过滤掉标签。乍一看,似乎列表推导式函数从 Python 语法中汲取了一些灵感。

列表 7.5 列表推导式语法

[element in list WHERE predicate | element]

列表推导式语法被方括号包围。使用element IN list语法来定义一个变量以引用列表中的元素。与 Python 不同,元素操作和转换可以直接在管道|字符之后定义,而不是直接在变量赋值中。你在列表推导式语法的元素转换部分去除了逗号并将文本转换为小写,以避免区分#NLP 和#nlp 标签。你也可以使用WHERE子句来过滤列表中的项目。

最后,你使用size()函数来过滤至少包含一个标签的推文。size()函数返回列表中的项目数。在前一章中,你学习了如何以优化的方式使用size()函数来访问节点度,但它也可以用来计算列表的长度。

在继续进行共现分析之前,你将提取标签并将其存储在数据库中。每次你在数据库中添加一个新的节点标签时,建议识别节点的唯一属性并定义唯一约束。对于标签,每个节点应代表一个单独的标签,因此你可以在Tag节点的id属性上简单地定义唯一约束。

列表 7.6 在id属性上为Tag节点定义唯一约束

CREATE CONSTRAINT IF NOT EXISTS FOR (t:Tag) REQUIRE t.id IS UNIQUE;

最后,你可以执行以下 Cypher 语句来提取并存储标签到数据库中。

列表 7.7 提取标签并存储到数据库中

MATCH (t:Tweet)
WHERE NOT EXISTS { (t)-[:RETWEETS]->() }
WITH t, replace(t.text, '\n', ' ') AS cleanText
WITH t, split(cleanText, ' ') AS tokens
WITH t, [el IN tokens WHERE el STARTS WITH "#" |
            toLower(replace(el, ",", " "))] AS hashtags
WHERE size(hashtags) > 0
UNWIND hashtags AS tag_id       ❶
MERGE (tag:Tag {id: tag_id})
MERGE (t)-[:HAS_TAG]->(tag)

❶ 使用 UNWIND 子句将元素列表转换为单独的行

列表 7.7 中的 Cypher 语句引入了 UNWIND 子句。UNWIND 子句用于将元素列表转换为行,类似于各种脚本语言中的 FOR 循环。本质上,您会遍历列表中的每个元素,在这种情况下,合并一个 Tag 节点并将其连接到 Tweet 节点。UNWIND 子句始终跟随着 AS 操作符,以将生成的行中的元素值分配给引用变量。以下 Cypher 语句演示了 UNWIND 子句的简单用法。

列表 7.8 UNWIND 子句语法

UNWIND [1, 2, 3] AS i
RETURN i

表 7.2 显示了作为行显示的元素。

表 7.2 将元素列表转换为行的 UNWIND 子句

i
1
2
3

练习 7.1

标签现在已存储并连接到数据库中的 Tweet 节点。在跳转到共现分析之前,调查哪些标签出现在最多的推文和转发中。记住,您只在数据库中存储了原始推文的标签(不是转发)。因此,首先匹配出现标签的原始推文。接下来,计算这些推文被转发的次数,然后返回原始推文和转发总计数之和最高的前五个标签。由于并非所有推文都被转发,使用 OPTIONAL MATCH 来计算转发次数。

表 7.3 显示了最受欢迎的标签。

表 7.3 推文和转发中最受欢迎的标签

hashtag originalTweetsCount retweetCount
#nlp 1,848 7,532
#ai 1,554 7,169
#machinelearning 1,474 7,007
#datascience 1,455 6,736
#bigdata 1,358 6,577

最受欢迎的标签是 #nlp、#ai、#machinelearning 和 #datascience。根据转发计数,它们必须在同一推文中频繁共同出现,因为总共有 12,000 条推文和转发。接下来,您将进行标签分析的共现部分。

7.2 构建共现网络

您可以使用 Cypher 查询语言来评估哪些标签最频繁地共同出现。

练习 7.2

评估哪些标签最频繁地共同出现。使用 MATCH 子句定义一个图模式,其中两个标签出现在同一推文中,然后使用 count() 函数计算一对标签共同出现的推文数量。仅返回最常见的五个共同出现的标签对。

练习 7.2 的解决方案如下。

列表 7.9 检查最常见的五个共同出现的标签对

MATCH (h1:Tag)<-[:HAS_TAG]-()-[:HAS_TAG]->(h2:Tag)
WHERE id(h1) < id(h2)                              ❶
WITH h1,h2,count(*) AS cooccurrences
ORDER BY cooccurrences DESC LIMIT 5
RETURN h1.id AS tag1, h2.id AS tag2, cooccurrences 

❶ 从结果中删除重复项

表 7.4 显示了最常见的共同出现的标签对。

表 7.4 最常见的五个共同出现的标签对

tag1 tag2 cooccurrences
#ai #nlp 1,507
#machinelearning #nlp 1,428
#datascience #nlp 1,410
#ai #machinelearning 1,410
#datascience #ai 1,405

练习 7.2 没有提到理想情况下你应该从输出中移除重复项,因为你还没有学到如何做到这一点。由于每个标签号都将作为h1变量以及h2变量出现,结果将包含重复项。使用id(h1) < id(h2)来去重结果是最常见的策略,我在实践中也经常看到。

你可以使用类似的 Cypher 语句使用 Cypher 投影来投影共现网络。基于列表 7.9 中的 Cypher 语句生成的共现网络将类似于图 7.1。

图 7.7 展示了样本标签共现网络,其中关系权重代表共现次数。你可能会想知道为什么每个节点对之间有两个指向相反方向的关系。如果#NLP 与标签#AI 共现,这直接意味着标签#AI 也与标签#NLP 共现。在图论中,你可以认为CO_OCCUR关系是无向的,因为连接的方向并不重要。然而,图数据科学(GDS)库没有无向关系的概念。无向关系背后的一个关键概念是它允许双向遍历。你可以在有向网络中通过将单个无向连接转换为两个指向相反方向的定向链接来复制这种功能。

07-07

图 7.7 样本加权无向共现网络

注意:GDS 库没有无向关系的概念。当在 GDS 中处理无向网络时,你将网络中的每个关系表示为两个指向相反方向的定向关系。在节点相似度算法示例中,算法的输出是一个无向网络,其中每个无向关系都表示为两个定向关系,如图 7.8 所示。GDS 库还允许在投影时间将单个关系转换为两个指向相反方向的链接。

07-08

图 7.8 表示为两个指向相反方向的定向关系的单一无向关系

7.2.1 Jaccard 相似系数

虽然使用共现次数作为关系权重没有问题,但更常见的方法是使用Jaccard 相似系数来评估节点之间的相似度。Jaccard 相似系数很容易理解,因为它只涉及将两个集合的交集除以并集。

图 7.9 可视化两个篮子,其中每个篮子包含一组产品。例如,篮子 A 包括沙发、扬声器、手机和电视,而篮子 B 包含手机、电视和耳机。如果你想计算两个篮子之间的 Jaccard 相似系数,你首先计算两个产品集合的交集和并集。两个篮子都包含手机和电视,这是两个集合的交集。在两个篮子中分散着五种不同的产品,这是两个集合的并集。要计算 Jaccard 相似系数,你只需将两个集合的交集(2)除以并集(5),结果为 0.4。Jaccard 相似系数的附加好处是它提供了一个可以用来评估的指标——在这个例子中,基于产品评估两个篮子的相似程度。

07-09

图 7.9 两个具有重叠产品的篮子的示例

Jaccard 相似系数的范围是 0 到 1。当两个集合的成员之间没有交集时,Jaccard 相似系数等于 0。例如,假设篮子 A 包含三明治和果汁,而篮子 B 包含电视。篮子 A 和 B 之间没有项目的交集,这进而表明两个篮子之间的 Jaccard 相似系数为 0。另一方面,具有相同成员的两个集合的 Jaccard 相似系数为 1。当两个集合的 Jaccard 相似系数为 1 时,这意味着两个集合具有相同数量的成员,且两个集合中的成员相同。在这个例子中,篮子 A 和 B 都包含三明治和果汁。然而,如果你在任一篮子中添加或删除任何项目,Jaccard 相似系数将不再为 1。使用 Jaccard 相似系数评估标签重叠的过程如图 7.10 所示。

07-10

图 7.10 使用 Jaccard 相似系数检查标签重叠

在图论背景下,Jaccard 相似系数算法的典型输入是由两种类型或集合的节点组成的二分网络。使用 Jaccard 相似系数算法背后的想法是基于二分输入图投影一个单分图。图 7.10 显示了将包含推文和标签的网络转换为基于它们共同拥有的推文数量形成的单分标签网络的转换过程。这个过程如下:

  1. 对于每个标签,你首先收集包含它的推文集合。

  2. 在下一步中,你遍历每一对标签,通过将两个集合的交集除以它们的并集来计算 Jaccard 相似系数。

  3. 最后,你可以选择将一对节点之间的相似系数以关系的形式存储。

推断关系的语义取决于领域。在标签示例中,你可以选择CO_OCCUR类型的关系。在篮子示例中,推断的关系可能具有SIMILAR类型。

Jaccard 相似度系数是一种对称相似度度量。如果节点 A 与节点 B 相似,这直接意味着节点 B 与节点 A 相似。就像前面的 Cypher 投影示例一样,生成的共现或相似度网络将是无向的。此外,你还可以将节点之间的 Jaccard 相似度系数存储为关系属性。

7.2.2 节点相似度算法

由于 Jaccard 相似度系数可以用来评估一对节点之间的相似程度,因此 GDS 开发者决定将其命名为节点相似度算法。节点相似度算法通过 Jaccard 相似度系数或重叠系数,根据节点的邻居来比较节点集合。一个典型的输入是由两种类型节点组成的二分网络。正在比较的是具有输出关系的节点,而它们的输出邻居被用来构建比较集。图 7.11 显示了用户和音乐类型的简单网络。

07-11

图 7.11 节点相似度算法如何构建比较集并评估相似度

LISTENS关系是从用户指向音乐类型的。在这种情况下,节点相似度算法将根据用户的输出邻居(即他们正在收听的音乐类型)来比较用户。意识到节点相似度算法正在比较哪些节点对于正确执行算法至关重要。在我们的 Twitter 社交网络中,HAS_TAG关系是从Tweet节点指向Tag节点。如果你避免反转关系方向,你实际上会根据它们有多少共同标签来比较推文。GDS 库允许在投影期间反转关系方向,因此你不需要转换底层存储的图。当你想在投影期间转换关系方向时,你需要使用配置映射语法来描述投影关系,如下所示。

列表 7.10 描述关系类型及其方向的配置映射

{ALIAS_OF_TYPE: {type:'RELATIONSHIP_TYPE',
                 orientation: 'NATURAL',
                 properties:['property1','property2']}

而不是简单地用字符串指定关系,你需要构建一个关系配置映射。ALIAS_OF_TYPE键指定了投影关系将在内存图中以哪个名称可用。别名不需要与数据库中存储的关系类型相同。每个别名键都有一个值,该值由一个映射描述,说明哪些关系类型应该被投影,它们的方向以及可选属性。你可以使用orientation键来操作和转换关系方向。它有三个可能的值:

  • NATURAL—每个关系以与在数据库中存储相同的方式被投影。

  • REVERSE—在图投影过程中,每个关系都被反转。

  • UNDIRECTED—每个关系在自然方向和反向方向都被投影。

使用orientation配置,你可以选择按原样投影关系、反转其方向,或将其视为无向的。如前所述,为了将关系视为无向的,引擎只需在相反方向上复制该关系。

接下来进行标签共现任务,你需要投影TweetTag节点,并包括反向方向的HAS_TAG关系。

列表 7.11 投影TweetTag节点并包含反向的HAS_TAG关系

CALL gds.graph.project(
    'tags',                                                        ❶
   ['Tweet', 'Tag'],                                               ❷
   {REVERSED_HAS_TAG: {orientation:'REVERSE', type:'HAS_TAG'}});   ❸

❶ 投影的图名称

❷ 使用列表语法投影多个节点标签

❸ 投影反向的 HAS_TAG 关系

列表 7.11 介绍了两种新的原生投影语法选项。首先,你可以使用列表指定要投影的多个节点标签。在列表 7.11 中,你描述了要投影的TweetTag节点。其次,你使用了配置映射语法来描述投影的关系。投影的关系将在REVERSED_HAS_TAG别名下可用,并包含反向关系方向的HAS_TAG连接。

现在你已经将推文和标签的网络投影出来,你可以使用节点相似度算法来推断一个单部分网络。你可以通过节点相似度算法的topKsimilarityCutoff参数来影响推断网络的密集程度或稀疏程度。similarityCutoff参数定义了被视为相似的一对节点之间的 Jaccard 相似系数的阈值值。例如,如果similarityCutoff是 0.5,那么只有 Jaccard 相似度分数为 0.5 或更高的节点对之间的关系将被考虑。另一方面,topK参数指定了每个节点相似关系的数量限制。由于你可以直接通过topKsimilarityCutoff参数来影响应该存储多少关系,因此你相应地描述了推断的共现网络的稀疏程度或密集程度。

定义标签共现网络的稀疏程度或密集程度将直接关联到形成主题的标签社区的范围。例如,如果你使用topK值为 1,每个节点将只有一个指向其最相似邻居的出度关系。然而,如果你将topK值增加到 2,每个节点将有两个出度关系,指定了哪两个节点是最相似的。

图 7.12 展示了使用不同topK值时推断出的共现网络的比较。如前所述,节点相似度算法的输入通常是一个二分网络。在这个例子中,您有一个由推文和标签组成的二分网络。节点相似度算法将根据它们共同出现的推文数量来评估标签之间的相似度。一旦计算出标签对之间的 Jaccard 相似度系数,算法将输出标签之间的关系。您可以在图 7.12 的左侧观察到topK值为 1 的共现网络。使用topK值为 1,每个标签将有一个单一的输出关系,表示其最相似的标签。图 7.12 左侧的共现网络有八个节点和八个关系。例如,#data 标签与#datascience 标签最相似。尽管我之前提到 Jaccard 相似度系数是一个对称的相似度度量,但#datascience 标签并没有与#data 标签的逆向关系。为什么是这样呢?原因是,一旦您对节点相似度算法应用topK过滤器,您就会失去所有关系都是对称的保证。如果您将topK值设置为推断出的共现网络中的节点数量,所有关系都将是对称的。

07-12

图 7.12 不同topK值对结果共现网络的密度影响比较

回到您的场景,一旦创建了共现网络,想法是使用标签传播算法等算法来找到紧密连接的标签社区。一个社区被定义为密集互联的节点群,这些节点群可能与其他群体有较稀疏的连接。当使用较低的topK值时,推断出的共现网络中的连接会更少。因此,社区的大小会变小,因为密集互联的节点会更少。由于社区会更小,整个网络中会有更多的社区。您定义了每个标签社区将被视为一个单一主题。因此,通过调整节点相似度算法的topK值,您实际上是在影响结果标签社区的大小。标签社区越大,结果主题就越广泛。当标签社区较大时,您可能会产生一个更粗粒度的定位策略。另一方面,使用较小的topK值将有助于您找到较小的标签社区,从而制定更窄的营销策略。

在图 7.12 中,你可以观察到当使用 topK 值为 1 时,社区检测算法在结果共现网络中识别出两个社区。一个社区包括 #ai、#datascience、#data 和 #blockchain 标签,而另一个社区包含 #ml、#graph、#network 和 #algorithm。当你使用更高的 topK 值时,结果共现网络将更加互联互通,因此社区检测算法将识别出更大的社区。在你的用例中,更大的标签社区将导致更广泛的话题。在图 7.12 的右侧,你可以观察到使用 topK 值为 2 产生了一个更密集连接的共现网络。由于节点连接更加密集,社区检测算法识别出更大且数量更少的社区。在图 7.12 中,当使用更高的 topK 值为 2 时,算法仅识别出一个标签社区。

定义 topK 参数和 similarityCutoff 参数是科学与艺术的结合,并且取决于你的用例。默认情况下,topK 参数值为 10,similarityCutoff 为 1E-42,略高于零。你可以使用节点相似度算法的 stats 模式评估默认参数值下推断网络的密集程度。

列表 7.12 使用默认参数评估 Jaccard 系数的分布

CALL gds.nodeSimilarity.stats('tags', {similarityMetric: 'JACCARD'})
YIELD nodesCompared, similarityPairs, similarityDistribution

表 7.5 展示了结果分布。

表 7.5 使用默认参数的节点相似度算法的相似度分布

nodesCompared similarityPairs similarityDistribution
2,093 13,402

使用默认参数执行节点相似度算法将在 2,093 个标签之间创建 13,402 个关系。这些关系的平均相似度得分为 0.49。请注意,此分布摘要不包括所有节点对之间的相似度得分,而仅包括节点的 top 10 个相似邻居的得分,因为这是默认的 topK 值。有趣的是,中位数值 (p50) 接近平均相似度值,大约 25% 的关系具有最大可能的相似度得分 1。当相似度得分为 1 时,一对标签总是在一条推文中一起出现。你可以使用 similarityCutoff 参数排除相似度得分低于阈值的标签对之间的关系。

列表 7.13 使用 similarityCutoff 参数定义相似度得分阈值

CALL gds.nodeSimilarity.stats('tags',
  {similarityMetric: 'JACCARD', similarityCutoff:0.33})
YIELD nodesCompared, similarityPairs, similarityDistribution

表 7.6 展示了结果分布。

表 7.6 使用 similarityCutoff 参数的节点相似度算法的相似度分布

nodesCompared similarityPairs similarityDistribution
2,093 7,733

你可以观察到,将similarityCutoff值设置为 0.33,只会创建 7,733 个关系,而不是默认参数值下的 13,402 个。第 25 百分位数是 0.5,有趣的是,中位数已经是最大分数 1.0。结果网络的平均节点度数大约为 4。根据相似度分布,关系将在非常相似或高度共现的标签对之间创建,因为中位数已经是 1.0。

练习 7.3

使用节点相似度算法的stats模式测试topKsimilarityCutoff参数的各种组合,并评估它们值的变化如何影响推断网络的密度。

很遗憾,没有明确的解决方案来定义topKsimilarityCutoff参数。这让我想起了金发女孩困境;它们必须恰到好处。如果你推断出一个过于密集的图,进一步分析推断出的网络可能不会产生有价值的见解。同样,如果你推断出一个过于稀疏的图,也是如此。作为初学者,建议你尝试各种参数配置并检查下游结果。在掌握底层数据结构和配置值如何影响结果之后,你可以应用自动超参数优化方法。

在标签共现示例中,你将使用similarityCutoff值为 0.25 和topK值为 50。由于你将在推断出的共现网络上执行其他图算法,你将使用节点相似度算法的mutate模式。mutate模式将推断出的网络存储到内存图中,这允许你将节点相似度算法的结果作为其他图算法的输入,如下面的列表所示。

列表 7.14 将标签共现网络转换为内存图

CALL gds.nodeSimilarity.mutate('tags',
  {topK:50, similarityCutoff:0.25,
    mutateRelationshipType:'CO_OCCURRENCE',
    mutateProperty:'score',
    similarityMetric: 'JACCARD'})
YIELD nodesCompared, relationshipsWritten

推断出的标签共现网络包含 2,093 个节点和 9,992 个关系。

7.3 标签共现网络的特性

在继续到社区检测部分之前,你需要巩固使用图算法来表征网络的知识。现在你正在处理一个单部分图,你可以应用与前面章节相同的算法来表征网络。

7.3.1 节点度中心性

你可以使用节点度算法进一步评估推断出的共现网络的节点度分布。问题是现在投影的 tags 图包含 TweetTag 节点以及 REVERSE_HAS_TAGCO_OCCURRENCE 关系。你可以在算法执行时使用 nodeLabelsrelationshipTypes 参数来过滤算法应考虑的节点或关系。

列表 7.15 评估标签共现网络的节点度分布

CALL gds.degree.stats('tags',
  {nodeLabels:['Tag'], relationshipTypes:['CO_OCCURRENCE']})
YIELD centralityDistribution

表 7.7 显示了结果分布。

表 7.7 标签共现网络的节点度分布

p99 21.00011444091797
min 0.0
max 40.00023651123047
mean 5.351917056393738
p90 13.000053405761719
p50 3.0000076293945312
p999 29.00011444091797
p95 17.00011444091797
p75 8.000053405761719

nodeLabelsrelationshipTypes 参数都期望输入一个列表。在算法执行时过滤节点和关系的能力是一个方便的特性,它允许你分析投影图的各个部分或分析一个新推断出的网络。

标签共现网络的平均节点度为 5.3。一些标签没有 CO_OCCURRENCE 关系,而至少有一个标签经常与 40 个其他标签共现。topK 参数值为 50 对结果网络没有影响,因为最高度数仅为 41。

7.3.2 弱连通分量

弱连通分量(WCC)算法无需过多介绍,因为它已在之前的章节中介绍过。然而,你应该完成本节中的两个练习,以巩固你对执行和解释 WCC 算法结果的知识。

练习 7.4

在标签共现网络上执行 WCC 算法,并将结果存储到数据库中的节点属性 tcWcc 中。提供 nodeLabelsrelationshipTypes 参数,以便算法只考虑投影图的所需子集。使用算法的 write 模式将结果存储到数据库中。

表 7.8 显示了结果统计。

表 7.8 在标签共现网络上执行 WCC 算法的结果汇总统计

componentCount componentDistribution
469

WCC 算法的 write 模式也提供了结果的高级总结,类似于 stats 模式。标签共现网络中有 469 个组件,其中最大的包含 491 个成员,约占整个网络的 25%。你可以想象你正在处理一个非常不连通的网络,因为大多数组件的成员数在 10 个或以下。

练习 7.5

识别有多少组件的成员数不超过 10 人。首先,你需要根据它们的tcWcc属性计算每个组件的成员数。在第一次聚合后,你需要应用过滤器并忽略成员数超过 10 人的组件。在最后一步,你只需再次使用count函数来计算过滤后的组件数量。

通过完成 7.5 练习,你可以观察到 467 个组件中有 445 个组件成员数不超过 10 人。推断出的网络如此不连通的一个原因是你正在处理 Twitter 社交网络的一个非常小的子集。我认为添加更多数据将有助于连接一些组件。另一方面,像#meditation 或#selfhelp 这样的标签可能永远不会频繁地与 AI 或机器学习一起出现,即使偶尔发生,它们也不会达到相似度阈值,从而在它们之间创建共现关系。

7.4 使用标签传播算法进行社区检测

到目前为止,你只学习了如何使用 WCC 和强连通组件(SCC)算法来评估社区结构。在本章的最后部分,你将学习如何使用标签传播算法(LPA)来找到标签的非重叠社区。社区和组件之间的区别是什么?使用 WCC 算法时,组件由在图中忽略关系方向时可以相互到达的节点组成。另一方面,社区被定义为由密集连接的节点组成的组,这些节点可能与其他组有较稀疏的连接。图 7.13 显示了一个只包含单个 WCC 的网络。

07-13

图 7.13 示例:网络社区结构的可视化

当你在网络上运行社区检测算法,如 LPA 时,该算法将识别高度连接的节点组。图 7.13 中有三个社区。例如,左侧有一个成员高度连接的社区。同样,在可视化的右侧还有一个由密集连接的节点组成的社区,而其中一个节点也与中心社区有连接。你可以使用以下 Cypher 语句执行标签传播算法的mutate模式。

列表 7.16 在标签共现网络上执行标签传播算法并将结果存储到内存图

CALL gds.labelPropagation.mutate('tags',
  {nodeLabels:['Tag'], relationshipTypes: ['CO_OCCURRENCE'],
   mutateProperty:'community'})
YIELD communityCount, communityDistribution;

如你所见,大多数图算法遵循相同的语法,这使得尝试各种图算法变得容易。再次强调,你必须使用nodeLabelsrelationshipTypes参数来选择标签共现网络。

注意:标签传播算法是一个非确定性算法,这意味着它有可能产生不同的结果,即使多次应用于同一数据集。这种固有的非确定性源于算法在决定节点更新标签顺序时的随机性。因此,这种非确定性可能导致不同结果之间的差异。

如果您想使用 Cypher 评估结果,您需要将内存中图的突变community属性存储到 Neo4j 存储图中。您可以使用gds.graph.writeNodeProperties过程将内存中图的节点属性存储到数据库中。运行以下 Cypher 语句将内存中图的突变community属性存储到数据库中。

列表 7.17 将内存中图节点的突变属性写入数据库

CALL gds.graph.writeNodeProperties('tags', ['community'])
YIELD propertiesWritten

算法结果现在作为Tag节点的community属性可用。在下面的列表中,您将检查五个最大的社区并检查其中的一些成员。

列表 7.18 检查标签的五大社区

MATCH (t:Tag)
RETURN t.community AS community,
       count(*) AS communitySize,
       collect(t.id)[..7] AS exampleMembers
ORDER BY communitySize DESC
LIMIT 5

表 7.9 显示了标签的结果社区。

表 7.9 标签的五大最大社区

community communitySize exampleMembers
15,809 43 ["#mentalism", "#respect", "#special-needs", "#mondayvibes", "#goals", "#mindset", "#anxiety"]
15,828 42 ["#auto_tagging", "#data_entry", "#itrules", "#writingcommunity", "#feg", "#crypto", "#tsa"]
17,537 35 ["#programming", "#ml", "#iiot", "#iotpl", "#rstats", "#cybersecurity", "#serverless"]
16,093 34 ["#nlpimpulse", "#iserlohn", "#zoom", "#selbstbild", "#selbstwert", "#spiegelbild", "#werte"]
16,271 31 ["#artproject", "#nft", "#art", "#nfts", "#oculusquest", "#gaming", "#xrhub"]

标签的最大的社区有 43 个成员。从表面上看,最大的社区的整体主题集中在心理健康和个人成长上。起初,我没有预料到数据集中会有这类推文,但现在我知道 NLP 可以指代自然语言处理神经语言编程。第三和第四大的社区围绕着计算机科学和软件开发。另一方面,第五大的社区似乎围绕着非同质化代币(NFTs)以及有趣的是,还提到了 VR 主题,如 Oculus Quest。您还可以取消成员数量以及行数的限制,以进一步分析社区结构。

练习 7.6

识别包含以下标签的社区:

  • #nlp是成员之一

  • #graph是成员之一

表 7.10 显示了结果社区。

表 7.10 包含#nlp 或#graph 成员的社区

15,699 ["#graphdatabases", "#hcm", "#peopleanalytics", "#hranalytics", "#graphdatascience", "#twitch", "#graph", "#neo4j"]
17,533 ["#datascience", "#ai", "#machinelearning", "#iot", "#python", "#nlp", "#100daysofcode", "#deeplearning", "#artificialintelligence", "#bigdata", "#robots"]

练习 7.6 的结果为你提供了可以用来为公司制定营销策略的标签推荐。例如,假设你想要针对自然语言处理社区。在这种情况下,你应该尝试将 #nlp 标签与其他相关标签(如 #datascience、#deeplearning 或 #machinelearning)结合起来,以触及更广泛的受众。另一方面,你应该排除不相关的标签主题,例如本例中的自我帮助领域。你还可以探索其他社区并寻找可能对你公司相关的其他标签。

7.5 使用 PageRank 识别社区代表

有时你会从标签传播算法中得到更大的标签社区。在营销策略示例中,你可能想识别社区中几个最中心的标签,以便集中精力,因为在你内容中使用 50 个或更多的标签可能没有意义。

你可以运行 PageRank 算法来找到社区的代表。要使用 PageRank 算法找到代表,你需要分别对每个社区执行它。不幸的是,你无法在算法执行时根据突变节点属性进行过滤。但你可以使用以下列表中显示的 图过滤 功能,它允许你通过指定节点和关系过滤器来过滤现有的内存图。

列表 7.19 子图投影语法

CALL gds.graph.filter(
  graphName: String, -> name of the new projected graph
  fromGraphName: String, -> name of the existing projected graph
  nodeFilter: String, -> predicate used to filter nodes
  relationshipFilter: String -> predicate used to filter relationships
)

你可以使用 nodeFilter 参数根据节点属性或标签来过滤节点。同样,你也可以使用 relationshipFilter 参数根据其属性和类型来过滤关系。过滤谓词是针对节点或关系的 Cypher 谓词。过滤谓词始终需要评估为 truefalse。谓词内的变量名不是任意选择的。节点谓词必须引用变量 n,而关系谓词必须引用变量 r

你使用标签传播算法的 mutate 模式的原因是现在你可以使用突变属性进行子图投影。如果你直接使用 write 模式,标签传播算法的结果将不会在内存图中可用,因此你无法根据它们进行过滤。

例如,包含 #ml 标签的社区有 35 个成员。你不必手动评估组内哪些成员最重要,可以使用 PageRank 算法来识别该组的代表。以下 Cypher 语句投影了一个只包含 #ml 标签作为成员的标签社区子图。

列表 7.20 投影只包含最大标签社区子图

MATCH (h:Tag {id:"#ml"})
WITH h.community AS communityId
CALL gds.graph.filter(
  'ml-community',
  'tags',
  'n.community = $communityId',                  ❶
 '*',                                            ❷
{ parameters: { communityId: communityId } })    ❸
YIELD graphName, fromGraphName, nodeCount, relationshipCount
RETURN graphName, fromGraphName, nodeCount, relationshipCount

❶ 过滤特定社区的节点

❷ 当你不想应用任何过滤器时,可以使用通配符运算符。

❸ 在从外部操作到子图投影中使用的任何参数都需要通过参数配置值传递。

第五个参数是配置映射,其中你可以定义在子图投影过程之前计算的任何参数。在列表 7.20 中的 Cypher 语句中,你首先匹配 #ml 标签节点并检索其 community 属性。然后,community 属性作为参数传递给子图投影过程。最后,你可以在新投影的 ml-community 内存图上执行 PageRank 算法以识别其代表。

列表 7.21 使用 PageRank 算法识别最大社区的代表

CALL gds.pageRank.stream('ml-community')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).id AS tag, score
ORDER BY score DESC
LIMIT 5

表 7.11 显示了生成的社区。

表 7.11 最大的标签社区的前五个代表

标签 得分
#serverless 1.9529458990718058
#iiot 1.8179378938658664
#usa? 1.6916560123489326
#coders 1.4975137459724255
#frenchtech 1.4975137459724253

有趣的是,与 #ml 同社区的标签中,最中心的标签是 #serverless、#iiot 和出人意料的是 #usa。可能是在抓取推文的时间窗口内,美国发生了某些物联网(IoT)事件。另一方面,#ml 与关于无服务器或 IoT 技术的推文共现并不令人惊讶。你也必须意识到你正在处理一个很小的推文样本。如果你增加了分析中的推文数量,结果可能会改变,因为你不会那么依赖于大多数推文在创建的三天窗口期间发生的事情。由于社区检测算法的结果是非确定性的,你可能会得到与表 7.11 不同的结果。

练习 7.7

找到其他社区的代表。你需要使用子图投影功能来过滤相关节点,然后使用 PageRank 算法找到其代表。

恭喜!你已经学会了如何推断共现网络并分析其社区结构。分析标签共现网络中最关键的一步是定义节点相似性算法的 topKsimilarityCutoff 参数。正如所讨论的,topKsimilarityCutoff 参数将直接影响推断出的共现网络的密度。因此,共现网络的密度将与识别出的社区的大小相关,在标签共现的例子中意味着生成的主题将有多广泛。我建议你测试这两个参数的各种配置,并检查它们如何影响生成的标签社区。

7.5.1 丢弃投影的内存图

在分析完成后,记得释放投影的内存图以释放内存供其他分析使用。以下 Cypher 语句将删除所有当前投影图。

列表 7.22 从内存中释放所有投影图

CALL gds.graph.list() YIELD graphName          ❶
CALL gds.graph.drop(graphName) YIELD nodeCount
RETURN 'dropped ' + graphName                  ❷

❶ 列出所有投影图

❷ 从内存中释放每个投影图

7.6 练习题解答

练习题 7.1 的解答如下。

列表 7.23 通过合并推文和转发计数之和检索前五个热门标签

MATCH (h:Tag)<-[:HAS_TAG]-(t:Tweet)
OPTIONAL MATCH (t)<-[r:RETWEETS]-()
RETURN h.id AS hashtag,
       count(distinct t) AS originalTweetsCount,
       count(r) AS retweetCount
ORDER BY retweetCount + originalTweetsCount DESC
LIMIT 5

练习题 7.2 的解答如下。

列表 7.24 检查前五个最常共现的标签对

MATCH (h1:Tag)<-[:HAS_TAG]-()-[:HAS_TAG]->(h2:Tag)        ❶
WHERE id(h1) < id(h2)
WITH h1,h2,count(*) AS cooccurrences
ORDER BY cooccurrences DESC LIMIT 5
RETURN h1.id AS tag1, h2.id AS tag2, cooccurrences

❶ 从结果中移除重复项

练习题 7.4 的解答如下。

列表 7.25 在标签共现网络上执行 WCC 算法并将结果存储到数据库中

CALL gds.wcc.write('tags',
  {writeProperty:'tcWcc',
   nodeLabels: ['Tag'], relationshipTypes: ['CO_OCCURRENCE']})
YIELD componentCount, componentDistribution;

练习题 7.5 的解答如下。

列表 7.26 识别有多少个组件有 10 个或更少的成员

MATCH (t:Tag)
WITH t.tcWcc AS componentId, count(*) AS componentSize
WHERE componentSize <= 10
RETURN count(*) AS count

练习题 7.6 的解答如下。

列表 7.27 识别与 #nlp 标签处于同一社区中的成员

MATCH (t:Tag)
WHERE t.id IN ['#nlp', '#graph']
WITH distinct t.community AS target_community
MATCH (o:Tag)
WHERE o.community = target_community
RETURN target_community, collect(o.id) as members

摘要

  • 推断单连通网络是图分析中常见的步骤。

  • Jaccard 相似系数可以通过将两个集合的交集除以并集来计算。

  • GDS 库使用两个指向相反方向的有向关系来表示无向关系。

  • 在 GDS 中,可以使用 gds.nodeSimilarity 算法计算 Jaccard 相似系数。

  • 节点相似度算法的 similarityCutoff 参数用于定义一对节点之间相似性的阈值值,仍被视为相似。

  • 节点相似度算法的 similarityCutoff 参数用于指定每个节点相似关系的数量限制。

  • 节点相似度算法的 stats 模式可以用来评估推断相似度网络的密度。

  • Cypher 实现了基本的文本函数,如 replacesplit 函数。

  • Cypher 语法提供了一种列表推导语法,这对于过滤或转换列表中的元素非常有用。

  • UNWIND 子句用于将元素列表转换为行,类似于各种脚本语言中的 for 循环。

  • 在内存图投影期间,您可以更改关系方向或将它视为无向的。

  • GDS 库使用两个指向相反方向的有向关系来表示无向关系。

  • 参数 nodeLabelsrelationshipTypes 可以用来考虑仅将投影图的一个子集作为图算法的输入。

  • 标签传播算法用于评估网络的社区结构。

  • 标签传播算法是非确定性的,这意味着算法的输出可能在不同的执行之间有所不同。

  • 社区代表节点之间紧密连接的密集组,组与组之间的链接较为稀疏。

  • 您可以使用图过滤功能来投影一个包含现有内存图中子集的新图。

  • PageRank 可以用来在共现网络中找到社区的代表。

8 构建最近邻相似性网络

本章涵盖

  • 手动提取节点特征

  • 展示网络基元和图小类

  • 介绍中介中心性和接近中心性

  • 基于成对余弦相似度构建单部分网络

  • 使用社区检测算法完成用户细分任务

本章将描述基于节点属性或特征的相似性网络的构建。类似于典型的机器学习预处理工作流程,每个数据点或节点都表示为一个向量。在机器学习环境中,向量是一组一个或多个数值的列表。处理图时,通常有两种方法可以用来描述一个节点为向量。你可以手动生成一组描述节点的特征,或者可以使用各种图算法生成表示网络中节点的向量。在本章中,你将手动创建节点的表示,以描述它们在网络中的角色,然后使用这些表示来构建一个推断的相似性网络。

图 8.1 展示了从关注者网络中提取节点特征的过程。描述一个节点为向量的方法有多种。在本章中,你将手动识别和提取用于构建相似性网络的相关特征。之后,你将根据提取的特征评估节点之间的相似度。评估两个向量之间相似度的最常见指标是余弦相似度。余弦相似度定义为两个向量之间角度的余弦值。你将计算节点对之间的余弦相似度,并存储被认为相似的节点之间的关系。与上一章类似,你将定义创建关系的相似度阈值。请注意,在原始网络中连接的节点在推断的相似性网络中不一定连接。

08-01

图 8.1 基于节点表示构建相似性网络的过程

余弦相似度定义为两个向量之间角度的余弦值,如图 8.2 所示。这个度量范围在-1 到 1 之间。当一对向量具有相同方向时,意味着向量之间的角度为 0,余弦相似度为 1。另一方面,当两个向量方向相反时,余弦相似度为-1。在实践中,当两个向量的余弦相似度接近 1 时,你认为这两个向量是相似的。

想象你是一名 Twitter 的分析师。你的上司给你分配了一个任务,即识别平台上的用户类型,但没有告诉你具体要寻找什么或如何分组用户。数据集中有几个特征可以用来描述一个用户。例如,你知道他们发推文或转发推文时使用哪些标签或标签组。你也很清楚他们关注或提及的人。

08-02

图 8.2 余弦相似度是通过测量两个向量之间角度的余弦值来衡量的。

此外,你还有一些关于用户或推文创建时间的时序信息。与所有手动特征工程一样,你首先必须决定你将使用哪些指标或特征来描述一个节点。由于你只有一小部分来自小时间窗口的推文,分析用户是否由于不再发布或转发而变得不活跃是没有意义的。另一方面,探索有助于你根据内容创作者与主要仅转发的人来划分用户的特征可能很有趣。例如,你可以将推文总数和转发与所有推文之间的比率作为前两个特征。

另一个有趣的指标可能是用户转发所需平均时间。你可以假设,如果平均转发时间是最低的,那么你很可能在处理一个机器人。另一个可以帮助你识别机器人的指标是检查多个用户是否在相似时间发布相同内容。由于你有关于关注者的信息,探索一些包含节点在关注者网络中位置和角色的指标可能值得考虑。你将学习如何描述节点的直接邻域以及调查它在整个网络中的作用。节点的角色是对它在网络中所扮演部分的主观解释。例如,你可以使用介数中心性算法来评估哪些节点充当不同社区或网络部分之间的桥梁。同样,你可以使用接近中心性来评估节点在网络中与其他所有节点的接近程度。假设大多数,如果不是所有,关于 Twitter 的信息都是通过关注者关系传播的,你可以识别出由于在网络中的位置,可以最快通过网络传播信息的节点。

在特征提取过程之后,你将根据用户特征向量之间的成对余弦相似度构建用户之间的相似性网络。然后,你将使用前一章中介绍的标签传播算法等社区检测算法来识别用户的各个部分。由于关系连接了相似节点,社区检测算法将识别在推断的相似性网络中紧密互联的节点组。识别出的用户组可以解释为基于手动提取特征的用户细分。在商业环境中,根据特定特征对个体进行分组的工作被称为用户细分,而在技术环境中,它被称为无监督聚类社区检测

图 8.3 展示了在推断相似性网络上使用社区检测算法的过程,以识别可以解释为段的用户组。推断相似性网络的密度将直接与社区的大小相关。您不能使用这种方法预先定义您想要识别的段或聚类的数量;然而,您可以通过调整推断相似性网络的密度来影响聚类的大小。

08-03

图 8.3 使用社区检测算法识别用户段

几篇研究论文(例如,Tinati & Carr,2012 年;Beguerisse-Díaz 等人,2014 年)专注于在 Twitter 网络上定义用户角色。尽管提取的特征因论文而异,并且可以使用多个社区检测或聚类技术将用户分组,但基本思想似乎是始终如一的。第一部分涉及识别和提取描述用户的有关特征。特征提取是手动完成的,允许分析师解释所有特征及其相关性。例如,如果您使用一个将节点自动转换为向量的模型,那么很难解释这些向量代表什么。最后,研究人员然后使用各种社区检测或聚类技术将用户分组到段中。

您可以使用构建最近邻图并评估其社区结构的方法,在许多其他领域识别特定的组或聚类。例如,您可以使用这种技术对用户进行细分以创建个性化服务(Voulodimos 等人,2011 年)或聚类客户以改善市场预测和规划研究(Kashwan & Velu,2013 年)。您还可以使用类似的方法通过检查其摘要中特定句子结构的相关性来聚类研究论文(Fukuda & Tomiura,2018 年)。虽然特征提取在不同的分析中可能看起来非常不同,从使用简单的统计到提取网络特征或甚至文档嵌入,分析输入始终是一个表示每个数据点的向量。接下来,有大量的算法可以根据数据点的向量表示对数据进行分组,我并不在这里争论哪个最好以及为什么。相反,我想给你一个使用基于图的方法进行无监督聚类的例子,其中最终聚类的数量或社区不是预先定义的。为了跟随本章的练习,您需要将 Twitter 网络导入到第三章中描述的 Neo4j 数据库中。

8.1 特征提取

如前所述,用户细分过程的第一步是特征提取。每个节点特征都将存储为其属性。首先,您将使用您的 Cypher 知识提取每个用户的推文数量以及转发与推文的比率。

练习 8.1

计算每个用户的推文数量,并将其存储为tweetCount属性。确保包括那些发布了零条推文的用户。此外,计算每个用户的转发与推文之间的比率。具体来说,将转发的数量除以转发和推文的总和,并将其存储为retweetRatio属性。当一个用户没有转发或推文时,使用默认值零。您可以使用一个或两个 Cypher 语句来计算这两个特征,您觉得哪个更容易。

接下来,您将评估用户在推文发布后平均需要多长时间转发推文的分布。您可以假设如果一个用户几乎立即转发很多推文,那么它很可能是机器人。以下列表介绍了duration.between()函数,该函数用于计算两个日期时间之间的持续时间。

列表 8.1 评估转发和创建推文日期之间的平均持续时间分布

MATCH (u:User)-[:PUBLISH]-(retweet)-[:RETWEETS]->(tweet)
WITH u, toInteger(duration.between(                             ❶
  tweet.createdAt, retweet.createdAt).minutes) AS retweetDelay
WITH u, avg(retweetDelay) AS averageRetweetDelay                ❷
RETURN apoc.agg.statistics(averageRetweetDelay,
  [0.05, 0.10, 0.25, 0.5, 0.9]) AS result

❶ 使用duration.between()函数计算两个日期时间之间的持续时间

❷ 按用户分组计算平均转发延迟

duration时间类型表现得像一个对象,并且有多个方法来提取年、天、分钟等持续时间。您可以在文档中查看所有可用的方法:mng.bz/NVnd

列表 8.1 中 Cypher 语句的结果显示在表 8.1 中。

表 8.1 每个用户转发和原始推文之间的平均时间分布

total 1,385
min 0.0
minNonZero 0.05769228935241699
0.1 2.583343267440796
max 1,439
0.05 1.0000073909759521
mean 372.21522092560997
0.25 22.00012183189392
0.5 206.00097632408142
0.9 1,057.0078122615814
stdev 410.56837279615803

您可以观察到,您只有 1,385 个用户的信息,略少于所有用户的 40%。5%的用户在 1 分钟内转发,10%的用户在 2.5 分钟内转发。您可以使用转发和平均转发时间来识别机器人。您可以观察到,否则,平均转发时间约为 6 小时,这对于一个不是一直在查看 Twitter 动态的正常人来说是有意义的。

练习 8.2

计算每个用户之间推文和转发的时间间隔(以分钟为单位)的平均值,并将其存储为timeToRetweet属性。对于从未转发过的用户(有缺失值),使用 372 分钟的平均值。

另一个可能表明是机器人的特征是多个用户是否发布相同的内容。

练习 8.3

检查内容相同但不是转发的推文对。内容可在text属性中找到。此外,忽略单个作者发布相同内容的多个推文的情况。

通过解决练习 8.3,你可以观察到只有五条推文内容完全相同。由于这个特征只出现在 5 个用户中,每 1000 个用户中只有 1 个,所以你会忽略它。

8.1.1 模式和图子

接下来,你将专注于编码用户在跟随网络中的角色。具有相似角色的节点在网络上不必相邻。例如,你可以这样说,拥有大量追随者的用户在产生某些类型的内容中扮演着角色。可能有多个拥有大量追随者的用户,他们不必相互跟随或在网络中接近,但他们仍然扮演着相似的角色。在这个例子中,你实际上只检查了一个节点的直接邻域。你可以通过计算节点的图子来编码一个节点的局部邻域。图子是一个节点在由k个节点组成的明显连通子图中的位置。你可能已经熟悉了两节点图子,尽管你可能从未听说过这个名字。

图 8.4 显示了所有两节点有向图子。一个两节点有向图子由两个节点组成,并具有有向关系。两个节点之间有三种可能的有向关系变体。当你计算图子时,你实际上是在计算一个节点在那个图模式中出现的次数。

08-04

图 8.4 两节点图子

图 8.5 展示了节点 A 的两节点图子计数可视化表示。如图 8.4 所示,位于位置 0 的图子有一个出向连接。所以如果你想计算节点 A 在位置零的图子,你计算出向连接的数量,在图 8.5 的例子中是 2。同样,通过评估其入度,你可以计算节点 A 在位置 1 的图子。最后,在有向图中,两个节点之间可以有两个方向的关系,如图 8.5 的右侧所示。在某些社交网络中,当两个用户相互关注时,可以认为他们是朋友。在图 8.5 中,只有节点 A 和 D 相互关注。

08-05

图 8.5 计算两节点图子

练习 8.4

计算跟随网络中所有用户的入度和出度,并将结果存储在inDegreeoutDegree属性下。此外,计算每个用户存在的“朋友”(图子二)模式数量,并将输出存储为friendCount属性。

接下来,你将研究三节点图子,并计算其中一些来编码一个节点的局部邻域。图 8.6 显示了所有 30 种有向三节点图子的变体。在 Cypher 中进行所有这些计算将是一个很好的练习;然而,你将只计算图 8.6 中可视化的三个图子。

08-06

图 8.6 三节点图子

你可能还会注意到图 8.6 不仅显示了基序数量,还显示了图子数量。这两者之间有什么区别?一个 基序 是一个明显连接的子图,而一个 图子 描述了节点在基序中的位置。例如,如果你看基序 1,你可以观察到它由三个节点和两个关系组成。使用基序,你只计算这种模式在网络中出现的频率。另一方面,你可以观察到在这个基序 1 中,节点位置有三种选择;因此,有三种图子可用。基序用于表征网络结构(Kim 等人,2011),而图子在你想要描述节点的局部邻域时非常有用(Pržulj 等人,2004)。

虽然你可能不会经常遇到人们只专注于计算图子或基序计数的情况,但不容忽视它们的的重要性。实际上,许多算法将它们作为推导单个节点(Rossi 等人,2017)甚至整个图(Dutta 等人,2020;Gorrec 等人,2022)表示的关键组成部分。

练习 8.5

为跟随网络中的每个用户计算图 8.6 中显示的图子 5、8 和 11,并将它们存储为节点属性。将图子 5 存储在 graphlet5 节点属性下,依此类推。我建议你为每个图子计算使用单独的 Cypher 语句。

8.1.2 介数中心性

你已经使用图子来编码节点的局部邻域;然而,你还没有提取任何描述用户在全局网络中位置的特性。你将首先执行介数中心性算法来提取一个描述用户作为不同社区之间桥梁频率的特性。介数中心性 算法假设所有信息都沿着节点之间的最短路径传输。一个节点越频繁地位于这些最短路径上,它的介数中心性排名就越高。

图 8.7 可视化了一个超级英雄网络,其中关系出现在出现在同一漫画书中的角色之间。节点的大小和角色名称的大小都是使用介数中心性计算的。介数中心性排名越高,节点和标题的大小就越大。你可以观察到连接不同社区的角色节点是最大的。例如,美国队长位于网络的中心,在中央社区和其他所有社区之间充当桥梁。另一个介数中心性的优秀例子是野兽角色,他是图 8.7 中中央和底部社区之间的唯一联系。如果他被从网络中移除,网络将分裂成两个单独的部分。因此,野兽角色充当了底部社区和整个网络之间的桥梁。充当桥梁也使节点对两个社区之间的信息流有影响力。

08-07

图 8.7 示例:中间中心性排名的可视化(来源:Sanhueza。许可协议为 CC BY 3.0)

在对关注者网络执行中间中心性算法之前,你必须投影一个内存图,如图 8.2 所示。你将使用相同的投影图来执行图算法,然后构建最近邻图。因此,你还需要在投影中包含所有之前计算过的节点特征。完成 8.1 至 8.5 的练习是执行以下 Cypher 语句以投影内存图的必要条件。

列表 8.2 投影描述关注者网络并包含所有预计算节点特征的内存图

CALL gds.graph.project('knnExample','User', 'FOLLOWS',
 {nodeProperties:['tweetCount', 'retweetRatio', 'timeToRetweet', 'inDegree',
  'outDegree', 'friendCount', 'graphlet5', 'graphlet8', 'graphlet11']})

现在,你可以执行中间中心性算法。你将使用mutate模式将结果存储回以下列表中的投影图中。

列表 8.3 修改中间中心性算法

CALL gds.betweenness.mutate('knnExample', {mutateProperty:'betweenness'})

8.1.3 接近中心性

接近中心性是一种衡量指标,表示一个节点在网络中与其他所有节点的接近程度。算法首先计算从节点到网络中所有其他节点的最短路径。一旦计算出最短路径,算法将计算到所有其他节点的距离之和。默认情况下,它返回距离之和的倒数,因此分数越高意味着节点的接近中心性排名越高。可以将接近性解释为尽可能快地到达所有其他节点的潜在能力。

图 8.8 显示了与图 8.7 相同的漫威网络。不同之处在于,这里节点和标题的大小是通过接近中心性算法而不是中间中心性算法计算的。你可以观察到最大的节点位于网络的中心,这是有道理的,因为它们可以最快地到达所有其他节点。另一方面,网络外围的角色具有最小的接近中心性排名。美国队长处于如此有利的地位,他在中心性的两个类别中都领先。另一方面,例如,钢铁侠在中间中心性排名上落后于蜘蛛侠和野兽。然而,当查看接近中心性排名时,他由于在网络中的中心位置而领先于他们。

08-08

图 8.8 示例:网络中节点接近中心性排名的可视化

接近中心性的第一个变体在无连接图中可能不可靠。记住,该算法试图找到图中所有其他节点的最短路径。如果使用原始变体在无连接图中,最短路径可能不存在,因此,从节点到所有最短路径的总和可能是无限的。在实践中,存在几种处理无连接图的接近中心性公式的变体。在这个例子中,你将使用 Wasserman 和 Faust 公式的变体(Wasserman & Faust, 1994)。你可以使用以下 Cypher 语句执行接近中心性算法的 mutate 模式。

列表 8.4 修改接近中心性算法

CALL gds.closeness.mutate('knnExample',
  {mutateProperty:'closeness', useWassermanFaust: true})

8.2 构建最近邻图

你已经通过手动提取特征完成了用户细分过程的第一个步骤。第二个步骤是将用户分组或聚类成细分市场。如前所述,有几种不同的方法可以根据向量表示聚类数据点。在这里,你将基于向量表示之间的成对余弦相似性构建最近邻图。由于评估许多数据点之间的余弦相似性是一个相对频繁的过程,一些算法实现会进行智能搜索并避免比较所有数据点对,因为那样扩展性不好。Neo4j 图数据科学(GDS)库实现了一个基于余弦相似性度量的高效相似性搜索。在构建最近邻图之前,探索特征之间的分布和相关性是明智的。

8.2.1 评估特征

为了利用 Cypher 的完整表达性和灵活性来分析特征,你必须将投影到内存图中的 closenessbetweenness 属性存储回存储的图中。你可以使用 gds.graph.writeNodeProperties 将修改后的属性存储回数据库。

列表 8.5 将修改后的属性存储到数据库中

CALL gds.graph.writeNodeProperties('knnExample',
  ['betweenness', 'closeness'])

你将首先检查哪些节点特征相关性最高。GDS 库提供了一个 gds.similarity.pearson(vector1, vector2) 函数,用于计算两个向量之间的相关性。你将比较所有特征对并识别相关性最高的那些。许多聚类技术受特征共线性影响,这可能导致结果偏差。特征共线性 是一种现象,其中一个特征与另一个特征高度相关。你可以使用以下 Cypher 语句来识别最相关的特征。

列表 8.6 识别最频繁相关的特征对

WITH ['tweetCount', 'retweetRatio', 'timeToRetweet', 'friendCount',
      'inDegree', 'outDegree', 'graphlet5', 'graphlet8',
      'graphlet11', 'closeness', 'betweenness'] AS features
MATCH (u:User)
UNWIND features as feature1
UNWIND features as feature2                                      ❶
WITH feature1,
     feature2,
     collect(u[feature1]) as vector1,
     collect(u[feature2]) as vector2

WHERE feature1 < feature2                                        ❷
RETURN feature1,
       feature2,
       gds.similarity.pearson(vector1, vector2) AS correlation   ❸
ORDER BY correlation DESC LIMIT 5

❶ 使用两个 UNWIND 语句比较每个特征与其他所有特征

❷ 避免比较特征与其自身并移除重复项

❸ 计算相关性

表 8.2 显示了结果对。

表 8.2 特征的前五对相关性

feature1 feature2 correlation
friendCount graphlet5 0.8173954540589915
graphlet8 outDegree 0.7867637411832583
graphlet11 graphlet5 0.7795975711131173
friendCount graphlet11 0.6578582639591071
betweenness friendCount 0.6370096424048863

看起来,一些特征高度相关。例如,friendCountgraphlet5graphlet11betweenness 特征高度相关。此外,graphlet8 变量与出度相关。为了消除一些高度相关的特征对,您将在分割过程中忽略 friendCountgraphlet8graphlet5 特征。

接下来,您将快速评估剩余特征的分布。您可以使用以下 Cypher 语句来计算基本的分布统计信息。

列表 8.7 将 hashtag 共现网络转换为内存图

WITH ['tweetCount', 'retweetRatio', 'timeToRetweet','inDegree',
      'outDegree', 'graphlet11', 'closeness', 'betweenness'] AS features
MATCH (u:User)
UNWIND features as feature
WITH feature,
     apoc.agg.statistics(u[feature],
                        [0.5,0.75,0.9,0.95,0.99]) as stats
RETURN feature,
       round(stats.min,2) as min,
       round(stats.max,2) as max,
       round(stats.mean,2) as mean,
       round(stats.stdev,2) as stdev,
       round(stats.`0.5`,2) as p50,
       round(stats.`0.75`,2) as p75,
       round(stats.`0.9`,2) as p90,
       round(stats.`0.95`,2) as p95,
       round(stats.`0.99`,2) as p99

表 8.3 显示了结果分布。

表 8.3 特征分布

feature min max mean stdev p50 p75 p90 p95 p99
"tweetCount" 0.0 754.0 0.96 13.74 0.0 1.0 1.0 2.0 6.0
"retweetRatio" 0.0 1.0 0.37 0.48 0.0 1.0 1.0 1.0 1.0
"timeToRetweet" 0.0 1439.0 372.08 254.87 372.0 372.0 613.0 944.0 1353.01
"inDegree" 0.0 540.0 6.92 22.76 0.0 4.0 16.0 35.0 112.0
"outDegree" 0.0 143.0 6.92 11.95 2.0 8.0 21.0 32.0 57.0
"graphlet11" 0.0 75.0 0.2 1.88 0.0 0.0 0.0 0.0 4.0
"closeness" 0.0 0.25 0.04 0.06 0.0 0.11 0.13 0.14 0.15
"betweenness" 0.0 199788.66 2385.97 10885.16 0.0 17.57 4075.66 11850.37 48312.5

有趣的是,我首先注意到的是,超过 95% 的用户 graphlet11 计数为 0。有些人可能会说,由于方差低,可以删除 graphlet11 特征;然而,在这个例子中,您将保留它。接近中心性分数的范围从 0.0 到 0.25,平均值为 0.04。另一方面,中介中心性没有归一化,因此分数要高得多,其范围从 0.0 到近 200,000。

在机器学习的背景下,归一化指的是将数据中特征或变量的范围进行转换的过程。它是许多机器学习算法中的关键预处理步骤。归一化通过将特征或变量的尺度转换为标准范围(通常在 0 到 1 或 -1 到 1 之间)来使特征更具有可比性。虽然如果您使用余弦相似度指标来推断相似性网络,则不需要对特征进行归一化,但如果您使用任何其他指标,则必须小心。例如,使用欧几里得距离(即两点之间的距离),归一化肯定会影响结果。

8.2.2 推断相似性网络

您已经预处理并评估了节点特征。一切准备就绪,可以继续用户细分过程。要运行社区检测算法并识别用户细分,您必须首先基于用户向量之间的成对余弦相似性度量推断一个相似性网络。Neo4j GDS 库提供了使用 gds.knn 算法的有效余弦相似性搜索。gds.knn 算法用于构建最近邻相似性图,不应与更主流的 kNN 分类或回归模型混淆。

同样,与上一章类似,您可以通过 topKsimilarityCutoff 参数影响推断出的相似性网络的密集或稀疏程度。在本例中,如果您推断出一个更密集的网络,生成的社区将更大;因此,用户细分过程将输出更少的细分或用户组。另一方面,如果您推断出一个更稀疏的相似性网络,细分将更细粒度。定义 topKsimilarityCutoff 参数没有对错之分——它始终取决于您的任务。在本例中,您将使用 topK 值为 65,并将 similarityCutoff 保持为默认值。

由于您需要在 gds.knn 算法的输出上执行社区检测算法,您将使用 mutate 模式将结果存储到投影图中。gds.knn 算法通过 topKsimilarityCutoff 参数定义的相似性阈值创建了用户之间新的关系。运行以下 Cypher 语句以执行 gds.knn 算法的 mutate 模式。

列表 8.8 将用户相似性网络转换为内存图

CALL gds.knn.mutate('knnExample', {
   nodeProperties:['tweetCount', 'retweetRatio', 'timeToRetweet','inDegree',
      'outDegree', 'graphlet11', 'closeness', 'betweenness'],
   mutateRelationshipType:'SIMILAR',
   mutateProperty:'score',
   topK:65
})

8.3 使用社区检测算法进行用户细分

用户细分过程的最后一步是执行社区检测算法以识别用户组或细分。到目前为止,您已经使用标签传播算法评估了网络的社区结构。在本章中,您将改用 Louvain 算法。Louvain 算法有一个与标签传播算法相同的任务,即将密集连接的节点分组或社区化。然而,它使用略微不同的底层数学来实现这一点。如果您对数学感兴趣,可以阅读提出 Louvain 算法的文章(Blondel et al., 2008)。运行以下 Cypher 语句将 Louvain 算法的输出 mutate 到投影内存图中。

列表 8.9 将转换后的属性 userSegmentation 存储到数据库

CALL gds.louvain.mutate('knnExample',
  {relationshipTypes:['SIMILAR'], mutateProperty:'userSegmentation'})

communityCount 输出中,有 22 个已识别的社区。

注意:与标签传播类似,Louvain 方法不是确定性的。因此,由于算法的随机性质,每次运行都可能得到不同的结果。

要进一步调查,您必须将转换后的 userSegmentation 属性存储起来,以便使用 Cypher 分析细分。

列表 8.10 将标签共现网络转换为内存图

CALL gds.graph.writeNodeProperties('knnExample', ['userSegmentation'])

最后,您可以评估用户细分结果。使用以下 Cypher 语句来评估五个最大用户细分平均特征值。

列表 8.11 评估用户细分结果

MATCH (u:User)
RETURN u.userSegmentation as community,
       count(*) AS memberCount,
       round(avg(u.tweetCount), 2) AS tweetCount,
       round(avg(u.retweetRatio), 2) AS retweetRatio,
       round(avg(u.timeToRetweet), 2) AS timeToRetweet,
       round(avg(u.inDegree), 2) AS inDegree,
       round(avg(u.outDegree), 2) AS outDegree,
       round(avg(u.graphlet11), 2) AS graphlet11,
       round(avg(u.betweenness), 2) AS betweenness,
       round(avg(u.closeness), 2) AS closeness
ORDER BY memberCount DESC
LIMIT 5

表 8.4 显示了结果用户细分。

表 8.4 用户细分结果

community memberCount tweetCount retweetRatio timeToRetweet inDegree outDegree graphlet11 betweenness closeness
270 217 3.5 0.007 385.3 15.28 19.12 1.09 10283.99 0.08
84 197 1.0 0.001 375.43 0.0 0.0 0.0 0.0 0.0
725 179 0.92 0.2 376.72 19.13 7.82 0.78 4692.19 0.12
737 156 0.0 0.0 372.0 0.0 0.03 0.0 0.0 0.0
381 145 0.0 1.0 35.68 2.5 4.43 0.55 845.37 0.04

最大的细分包含 217 名成员,他们的平均推文数为 3.5 条。由于他们的转发率仅为 0.07,所以他们几乎不做转发。另一方面,他们平均有 15 个关注者,并关注了 19 个其他用户。根据他们较高的中介中心性得分,他们充当了不同社区之间的桥梁。另一方面,第四大社区似乎包含不活跃和孤立的用户,至少从我们的数据集角度来看。他们没有推文或转发,也没有关注任何人或拥有任何关注者。

您可以从列表 8.11 中的 Cypher 语句中移除LIMIT子句以评估所有 22 个细分。此外,您还可以尝试不同的topKsimilarityCutoff值来评估gds.knn算法的值如何影响用户细分。

练习 8.6

在第七章中,您使用了 PageRank 算法来识别标签社区的代表。应用相同的技巧来识别细分代表。首先,使用图过滤过程仅过滤出最大细分中的用户。之后,在新过滤的投影上使用 PageRank 算法来识别其代表。

恭喜!您已经学会了如何手动提取节点特征,并在 k 近邻图和社区检测算法的帮助下,基于这些特征完成用户细分过程。

8.4 练习题解答

练习 8.1 的解答如下。

列表 8.12 计算每个用户的推文计数和转发率

MATCH (u:User)
OPTIONAL MATCH (u)-[:PUBLISH]->(tweet)
WHERE NOT EXISTS { (tweet)-[:RETWEETS]->() }
WITH u, count(tweet) AS tweetCount
OPTIONAL MATCH (u)-[:PUBLISH]->(retweet)
WHERE EXISTS { (retweet)-[:RETWEETS]->() }
WITH u, tweetCount, count(retweet) AS retweetCount
WITH u, tweetCount,
  CASE WHEN tweetCount + retweetCount = 0 THEN 0
    ELSE toFloat(retweetCount) / (tweetCount + retweetCount)
      END AS retweetRatio
SET u.tweetCount = tweetCount,
    u.retweetRatio = retweetRatio

练习 8.2 的解答如下。

列表 8.13 计算每个用户的平均转发时间和存储它

MATCH (u:User)
OPTIONAL MATCH (u)-[:PUBLISH]-(retweet)-[:RETWEETS]->(tweet)
WITH u, toInteger(duration.between(
  tweet.createdAt, retweet.createdAt).minutes) AS retweetDelay
WITH u, avg(retweetDelay) AS averageRetweetDelay
SET u.timeToRetweet = coalesce(averageRetweetDelay, 372)

练习 8.3 的解答如下。

列表 8.14 检查具有相同内容但不是转发的推文对

MATCH (t1:Tweet), (t2:Tweet)
WHERE NOT EXISTS { (t1)-[:RETWEETS]->() }
  AND NOT EXISTS { (t2)-[:RETWEETS]->() }
  AND id(t1) < id(t2)
  AND NOT EXISTS { (t1)<-[:PUBLISH]-()-[:PUBLISH]->(t2) }
  AND t1.text = t2.text
RETURN t1, t2 LIMIT 5

练习 8.4 的解答如下。

列表 8.15 计算两个节点的图元并将它们存储为节点属性

MATCH (u:User)
WITH u,
     count{ (u)<-[:FOLLOWS]-() } AS inDegree,
     count{ (u)-[:FOLLOWS]->() } AS outDegree,
     count{ (u)-[:FOLLOWS]->()-[:FOLLOWS]->(u) } AS friendCount
SET u.inDegree = inDegree,
    u.outDegree = outDegree,
    u.friendCount = friendCount

练习 8.5 的解答如下。

列表 8.16 计算并存储图元的计数 5

MATCH (u:User)
OPTIONAL MATCH p=(u)-[:FOLLOWS]->()-[:FOLLOWS]->()-[:FOLLOWS]->(u)
WITH u, count(p) AS graphlet5
SET u.graphlet5 = graphlet5

列表 8.17 计算并存储图元的计数 8

MATCH (u:User)
OPTIONAL MATCH p=(u)-[:FOLLOWS]->()-[:FOLLOWS]->()<-[:FOLLOWS]-(u)
WITH u, count(p) AS graphlet8
SET u.graphlet8 = graphlet8

列表 8.18 计算和存储图元的计数 11

MATCH (u:User)
OPTIONAL MATCH (u)-[:FOLLOWS]->(other1)-[:FOLLOWS]->(other2)-[:FOLLOWS]->(u),
               (u)<-[:FOLLOWS]-(other1)<-[:FOLLOWS]-(other2)<-[:FOLLOWS]-(u)
WHERE id(other1) < id(other2)
WITH u, count(other1) AS graphlet11
SET u.graphlet11 = graphlet11;

练习 8.6 的解决方案如下。

列表 8.19 过滤只包含最大标签社区子图

CALL gds.graph.filter('largestSegment', 'knnExample',
 'n.userSegmentation=270', '*')

列表 8.20 使用 PageRank 算法识别社区的代表

CALL gds.pageRank.stream('largestSegment',
  {relationshipTypes:['SIMILAR'], relationshipWeightProperty:'score'})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).username AS user, score
ORDER BY score DESC
LIMIT 5

摘要

  • 一个节点在网络中的作用可以用各种局部邻域和全局特征来描述。

  • 在网络中具有相似角色的节点不需要在网络中靠近。

  • 模式被用来描述网络结构。

  • 图元被用来编码一个节点的直接邻域。

  • 介数中心性用于识别在各个社区之间充当桥梁作用的节点。

  • 亲近中心性识别那些能够最快与其他所有节点共享信息的节点。

  • 通过评估其中一个向量相似度度量来构建最近邻图。

  • 最常用的向量相似度度量是余弦相似度。

  • 余弦相似度是通过两个向量之间角度的余弦值来衡量的。

  • 特征共线性是一种现象,当某个特征与另一个特征高度相关时发生。

  • 可以使用gds.graph.writeNodeProperties过程将内存图中的节点属性存储到数据库中。

  • 洛文算法与标签传播非常相似,但使用不同的底层数学。

  • 洛文算法是非确定性的,这意味着它在每次执行中可能会产生不同的结果。

  • 可以使用 PageRank 算法来找到推断相似网络中的代表性节点。

第三部分 图机器学习

在探索图数据科学的激动人心的旅程中,你已经见证了图的力量,了解了图算法,并发现了如何在各种场景中使用它们。现在,是时候在此基础上建立并深入预测分析领域了,在这里你将学习如何通过训练机器学习模型来预测缺失的节点属性和未来的关系。在第九章,你的旅程从节点嵌入的世界开始。在这里,你将学习如何将节点表示为向量,同时允许表示保留网络信息。节点的向量表示将随后用于构建节点分类模型。进入第十章,你将接触到链接预测,这是一个在众多领域中的关键任务,从社交网络分析到推荐系统。你将学习如何计算网络特征,这些特征将被用于训练和评估链接预测模型。第十一章介绍了知识图谱补全技术,这是一个在异构图上执行的链接预测任务。你将深入研究知识图谱嵌入,这些嵌入用于捕捉异构图复杂的结构。最后,作为额外内容,第十二章将指导你应用自然语言处理技术,如命名实体识别和关系抽取,来构建图。

9 节点嵌入和分类

本章节涵盖

  • 介绍节点嵌入模型

  • 展示归纳模型和归纳模型之间的区别

  • 检查结构角色和基于同质性的嵌入之间的差异

  • 介绍 node2vec 算法

  • 在下游机器学习任务中使用 node2vec 嵌入

在上一章中,你使用向量来表示网络中的每个节点。这些向量是基于你认为重要的特征手工制作的。在本章中,你将学习如何使用节点嵌入模型自动生成节点表示向量。节点嵌入模型属于降维类别。

特征工程和降维的一个例子是体质指数(BMI)。BMI 通常用于定义肥胖。为了精确地描述肥胖,你可以查看一个人的身高和体重,并测量他们的体脂百分比、肌肉含量和腰围。在这种情况下,你需要处理五个输入特征来预测肥胖。而不是在做出观察之前必须测量所有五个特征,医生们提出了 BMI。

图 9.1 可视化了一个用于评估人体类型的 BMI 秤。例如,如果 BMI 为 35 或更高,BMI 秤会将该人视为极度肥胖。BMI 是通过将一个人的体重(千克)除以他们的身高(平方米)来计算的,并且是对体脂的粗略估计。与使用五个输入特征相比,单个嵌入特征是期望输出的良好表示。这是一个良好的近似,但绝不是肥胖的完美描述符。例如,一个橄榄球运动员根据 BMI 会被认为是肥胖的,但他们可能肌肉比脂肪多。嵌入模型在保持与给定问题强相关性的同时降低了输入特征的维度。使用嵌入模型的额外好处是,你可以收集更少的数据来训练和验证模型。在 BMI 的情况下,你可以通过仅比较身高和体重比来避免可能昂贵的测量。

09-01

图 9.1 体质指数图表

每个图都可以表示为一个邻接矩阵。邻接矩阵是一个方阵,其中的元素表示节点对是否连接。这样的矩阵可以被视为网络的高维表示

图 9.2 可视化了一个包含四个节点 A、B、C 和 D 的邻接矩阵。邻接矩阵中的每个元素表示节点对是否连接。例如,C 列和 D 行的元素值为 1,表示图中存在节点 C 和 D 之间的关系。如果矩阵中元素的值为 0,则表示节点对之间不存在关系。

09-02

图 9.2 邻接矩阵

现在,想象一下你有一个包含一百万个节点的图。在一个邻接矩阵中,每个节点都会在矩阵中以一行表示,该行包含一百万个元素。换句话说,每个节点可以用一个包含一百万个元素的向量来描述。因此,邻接矩阵被视为一个高维网络表示,因为它随着图中节点数量的增加而增长。

假设你想训练一个机器学习模型,并且以某种方式使用网络结构信息作为输入特征。比如说,你使用邻接矩阵作为输入。这种方法有几个问题:

  • 输入特征太多。

  • 机器学习模型依赖于图的大小。

  • 过拟合可能成为一个问题。

如果你从图中添加或删除一个节点,邻接矩阵的大小会改变,你的模型就不再有效,因为输入特征的数量不同。使用邻接矩阵作为模型输入也可能导致过拟合。在实践中,你通常希望嵌入节点的局部表示来比较具有相似邻域拓扑结构的节点,而不是使用节点之间所有的关系作为特征输入。节点嵌入技术试图通过学习任何给定网络的低维节点表示来解决这些问题。学习到的节点表示或嵌入应该自动编码网络结构,以便嵌入空间中的相似性近似网络中的相似性。一个关键信息是,节点表示是通过学习得到的,而不是手动工程化的。在 BMI 示例中,医生通过手动公式降低了维度。节点嵌入技术旨在通过将嵌入过程视为一个独立的机器学习步骤来消除繁琐的手动特征工程,并提供最佳的节点表示。节点嵌入步骤是一个无监督过程,因为它学习表示节点而不使用标记的训练数据。节点嵌入模型使用基于深度学习和非线性降维的技术来实现这一点(Hamilton 等,2018)。

图 9.3 可视化了节点嵌入过程。节点嵌入模型将图的高维表示作为输入,并输出低维表示。在具有一百万个节点的图示例中,每个节点可以用包含一百万个元素的向量表示。假设你在该图上执行节点嵌入模型。对于大多数节点嵌入模型,你可以定义嵌入维度。嵌入维度是描述节点的嵌入矩阵中的元素数量。例如,你可以将嵌入维度设置为 256。在这种情况下,每个节点将用包含 256 个元素的向量来描述。将元素数量从一百万减少到 256 非常有益,因为它允许你使用低维向量有效地描述网络拓扑或图中节点的位置。低维向量可以用于下游机器学习工作流程,或者它们可以用于使用最近邻图算法推断相似性网络。

09-03

图 9.3 邻接矩阵

9.1 节点嵌入模型

节点嵌入模型旨在生成节点的低维表示,同时保留网络结构信息。这些低维表示可以随后用作各种机器学习任务的输入特征,例如节点分类、链接预测和社区检测,从而简化计算复杂度并可能提高模型的性能。

9.1.1 同质化与结构角色方法比较

“网络结构信息”究竟是什么意思?一种常见的方法是在嵌入空间中表示节点,使得图中相邻的节点在嵌入空间中彼此靠近。

图 9.4 展示了所谓的基于社区的节点嵌入方法。图中相邻的节点在嵌入空间中也彼此靠近。因此,属于同一社区的节点在嵌入空间中应该彼此靠近。这种方法是在节点同质化假设下设计的,即连接的节点在下游机器学习工作流程中往往相似或有相似的标签。

09-04

图 9.4 同质化方法进行节点嵌入

例如,您和您的朋友可能有着相似的兴趣。假设您想要预测一个朋友的新兴趣。在这种情况下,您可以使用基于同质性的节点嵌入模型来编码他们在友谊网络中的位置,并基于训练示例训练一个监督模型来预测或推荐他们的兴趣。如果一个人及其朋友具有相似兴趣的假设是有效的,那么训练好的模型应该表现相对较好。在这个例子中,您可以使用的节点嵌入算法之一是快速随机投影(FastRP)算法(Chen 等人,2019)。另一种方法是编码节点在嵌入空间中,使得具有相似网络角色的节点在嵌入空间中靠近。

在上一章中,您简要地介绍了节点角色。图 9.5 展示了节点嵌入过程,其中节点根据其网络结构角色在嵌入空间中编码得较为接近。在图 9.5 中,节点 D 和 F 都充当两个社区之间的桥梁。可以假设它们具有相似的网络角色,因此它们在嵌入空间中编码得较为接近。您可以使用结构角色嵌入方法来分析合作作者中研究者的角色。例如,您可以使用结构角色嵌入方法来分析合作作者网络中研究者的角色,或者,也许可以确定互联网网络中路由器的角色。例如,角色提取(RolX)算法(Henderson 等人,2012)是一种节点嵌入算法,它将具有网络结构角色的节点在嵌入空间中编码得较为接近。

09-05

图 9.5 节点嵌入的结构角色方法

您想使用哪种节点嵌入模型的设计取决于您需要完成的下游任务。一些算法,如 node2vec(Grover & Leskovec,2016),也可以产生两种嵌入设计的组合作为输出。

9.1.2 归纳与演绎嵌入模型

一些节点嵌入模型存在一个显著的局限性。在机器学习工作流程中使用节点嵌入模型的典型过程包括计算嵌入并将它们输入到分类机器学习模型中,例如。然而,归纳演绎节点嵌入模型之间的区别在于它们在训练过程中编码新未见节点的能力。

当处理一个归纳节点嵌入算法时,你无法计算在初始嵌入计算期间未看到的节点的嵌入。你可以将归纳模型视为在初始计算期间创建一个词汇表,其中词汇表的关键字代表一个节点,其值代表嵌入。如果一个节点在初始计算期间未被看到,它就不会出现在词汇表中;因此,你不能简单地检索新未见节点的嵌入。如果你想计算新节点的嵌入,你必须计算整个图的嵌入,这意味着所有先前观察到的节点以及新节点。由于现有节点的嵌入可能会发生变化,你还必须重新训练分类模型。

另一方面,归纳节点嵌入模型可以在初始计算期间计算未见节点的嵌入。例如,你可以基于节点嵌入的初始计算训练一个模型。当引入新节点时,你可以计算新节点的嵌入,而无需重新计算整个图的嵌入。同样,你也不必为每个新节点重新训练分类模型。在处理增长或多个分离的图时,对先前未见节点进行编码是一个巨大的优势。例如,你可以在单个图上训练一个分类模型,然后使用它来预测不同分离图的节点标签。要了解更多关于归纳模型的信息,我建议阅读 GraphSAGE 模型(Hamilton 等人,2017 年)。

9.2 节点分类任务

现在,是时候开始一个实际例子了。想象一下,你正在 Twitch 作为数据科学家工作。Twitch 是一个直播平台,它使任何人都能向全世界直播他们的内容。此外,其他用户可以通过聊天界面与直播者互动。

每天都有新用户加入平台,他们决定开始直播。你的经理希望你识别新流的语言。由于该平台是全球性的,直播者可能使用大约 30 到 50 种语言。假设由于某种原因,将音频转换为文本并运行语言检测算法是不可行的。其中一个原因可能是,许多 Twitch 上的直播者玩电子游戏,因此,电子游戏的声音可能会扭曲语言检测。

你还能用其他什么方法来预测新主播的语言呢?你拥有关于在特定流中聊天的用户的信息。可以假设用户主要使用单一语言聊天。因此,如果一个用户在两个流中聊天,那么这两个流很可能使用相同的语言。例如,如果一个用户在日语文流中聊天,然后切换流并与新主播通过聊天互动,那么新的流很可能也是日语。对于英语来说,可能会有一些例外,因为大部分情况下,互联网上的人至少对英语有基本的了解。记住,这只是一个需要验证的假设。

图 9.6 可视化了解析网络信息以预测新主播语言的过程。原始数据具有(:User)-[:CHATTED]→ (:Stream)二分图的结构。过程的第一步是将单分图投影出来,其中节点代表流,关系代表它们共享的观众。投影的单分图模式可以用以下 Cypher 语句表示:(:Stream)-[:SHARED_AUDIENCE]-(:Stream)。单分图是无向的,所以如果流 A 与流 B 共享观众,那么自动意味着流 B 也共享观众与流 A。此外,你还可以将主播之间共享观众的数量作为关系权重。假设提取原始数据并将其转换为单分图可以由你团队中的数据工程师完成。数据工程师可以采用与你在第七章中学到的方法类似的方法来投影单分图。你现在的工作是训练一个预测模型并评估其结果。

09-06

图 9.6 预测新流语言的过程

这个想法是准备一个 Jupyter 笔记本,每天使用一次来预测新主播的语言。记住,如果流在共享观众网络中很接近,它们很可能使用相同的语言。因此,你将使用一个基于同质性的方法在嵌入空间中对节点进行编码的节点嵌入模型。最简单且最广泛使用的节点嵌入模型之一是 node2vec,你将在本例中使用它。一旦计算出节点嵌入,你将使用它们来训练一个基于已知语言训练示例的随机森林分类模型。在过程的最后一步,你将使用标准的分类报告和混淆矩阵来评估预测结果。

为了跟随示例,您需要一个准备好的 Jupyter 笔记本环境和访问 Neo4j 数据库的权限。在开始本章之前,数据库应该是空的。您将使用 scikit-learn Python 库来分割数据、训练模型并评估结果,所以请确保您已经安装了它。本章中所有代码的笔记本也可在 GitHub 上找到(github.com/tomasonjo/graphs-network-science)。

9.2.1 定义与 Neo4j 数据库的连接

首先,打开一个新的 Jupyter 笔记本,或者从上一段中的 GitHub 链接下载已填充的笔记本。为了跟随代码示例,您需要安装以下三个 Python 库:pandas、scikit-learn 和 matplotlib。您可以使用 pip 或 Conda 包管理器安装所有这三个库。

  • Neo4j

  • pandas

  • scikit-learn

您可以使用 pip 或 Conda 包管理器安装所有三个库。

首先,您需要定义与 Neo4j 数据库的连接。

列表 9.1 定义与 Neo4j 的连接

from neo4j import GraphDatabase

url = 'bolt://localhost:7687'
username = 'neo4j'
password = 'letmein'

driver = GraphDatabase.driver(url, auth=(username, password))

列表 9.1 从neo4j库中导入GraphDatabase对象。为了与 Neo4j 数据库建立连接,您需要填写并可选地更改凭据。一旦定义了凭据,就将它们传递给GraphDatabase对象的driver方法。驱动程序允许您在会话中执行任意的 Cypher 语句。

接下来,您将定义一个函数,该函数接受一个 Cypher 语句作为参数,并将结果作为 pandas 数据框返回,如下所示。pandas 数据框是一个方便的数据结构,可以用于过滤、转换或轻松与其他 Python 库集成。

列表 9.2 定义一个执行任意 Cypher 语句并返回 pandas 数据框的函数

def run_query(query):
    with driver.session() as session:
        result = session.run(query)
        return result.to_df()

9.2.2 导入 Twitch 数据集

环境准备就绪后,您可以回到指定的任务。记住,您的团队中的数据工程师足够友好,提取了有关流和聊天者的信息,并执行了单部分投影。他们准备了包含相关信息的两个 CSV 文件。第一个 CSV 文件包含网络中节点信息,如表 9.1 所示。

表 9.1 节点 CSV 结构

id language
129004176 en
50597026 fr
102845970 ko

节点 CSV 包含有关流 ID 及其语言的信息。在本例中,您拥有所有流的语言信息,以便评估测试数据的分类模型准确率。定义节点唯一属性上的唯一约束以加快导入速度是良好的实践。您将首先定义Stream节点id属性上的唯一约束。

列表 9.3 在Stream节点上定义约束

run_query("""
CREATE CONSTRAINT IF NOT EXISTS FOR (s:Stream) REQUIRE s.id IS UNIQUE;
""")

由于你在一个 Python 环境中工作,你需要通过定义在列表 9.2 中的run_query函数执行 Cypher 语句。该函数以 pandas dataframe 格式返回输出。然而,你对此 Cypher 语句的结果不感兴趣,因此不需要将输出分配给新变量。

现在,你可以继续导入有关 Twitch 流及其语言的信息。CSV 文件可在 GitHub 上找到,因此你可以使用LOAD CSV子句检索并导入 CSV 信息到数据库,如下所示。

列表 9.4 导入节点

run_query("""
LOAD CSV WITH HEADERS FROM "https://bit.ly/3JjgKgZ" AS row
MERGE (s:Stream {id: row.id})
SET s.language = row.language
""")

关系 CSV 文件包含有关流之间共享受众及其数量的信息,如表 9.2 所示。

表 9.2 关系 CSV 结构

source target weight
129004176 26490481 524
26490481 213749122 54
129004176 125387632 4591

关系 CSV 包含三个列。sourcetarget列包含具有共享受众的流 ID,而weight列表示在两个流中聊天的共享用户数量。你可以使用以下 Cypher 语句导入关系信息。

列表 9.5 导入关系

run_query("""
LOAD CSV WITH HEADERS FROM "https://bit.ly/3S9Uyd8" AS row
CALL{
    WITH row
    MATCH (s:Stream {id:row.source})
    MATCH (t:Stream {id:row.target})
    MERGE (s)-[r:SHARED_AUDIENCE]->(t)
    SET r.weight = toInteger(row.weight)
} IN TRANSACTIONS
""")

练习 9.1

检查有多少(如果有)Stream节点没有进入或出去关系。

幸运的是,数据集中没有孤立节点。一个孤立节点是指没有进入或出去关系的节点。在从数据集中提取节点特征时,始终要特别注意孤立节点。例如,如果有一些没有关系的Stream节点,那将是一个缺失数据的情况。如果你等待几天,希望有人会在他们的流中聊天,你就可以为该流创建新的关系,这样它就不会再是孤立的。另一方面,孤立的Stream节点可以是任何语言。由于大多数节点嵌入算法对孤立节点进行相同的编码,包含孤立节点会向你的分类模型引入噪声。因此,你希望从训练和测试数据集中排除所有孤立节点。

另一方面,如果你正在处理孤立节点,并且关系没有缺失,这意味着未来不会形成新的关系,你可以将孤立节点包含在你的工作流程中。例如,想象一下,如果你要根据一个人的网络角色和位置预测其净资产。假设一个人没有关系,因此没有网络影响力。在这种情况下,对孤立节点进行编码可以为预测净资产的机器学习模型提供关键信号。始终记住,大多数节点嵌入模型对孤立节点进行相同的编码。所以如果孤立节点都属于单个类别,那么考虑它们是有意义的。然而,如果孤立节点属于各种类别,那么从模型中移除它们以消除噪声是有意义的。

9.3 节点 2vec 算法

现在图已经构建好了,你的任务是编码节点到嵌入空间中,以便能够根据节点的网络位置训练基于语言的预测模型。正如之前提到的,你将使用节点 2vec 算法(Grover & Leskovec, 2016)来完成这个任务。节点 2vec 算法是归纳性的,并且可以被微调以捕捉同质化或基于角色的嵌入。

9.3.1 word2vec 算法

节点 2vec 算法深受 word2vec(Mikolov et al., 2013)skip-gram 模型的影响。因此,为了正确理解节点 2vec,你必须首先了解 word2vec 算法是如何工作的。Word2vec 是一个浅层、两层的神经网络,经过训练以重建词的语用上下文。word2vec 模型的目标是给定一个文本语料库产生词表示(向量)。词表示在嵌入空间中定位,使得在文本语料库中共享相同上下文的词在嵌入空间中彼此靠近。在 word2vec 的背景下使用了两种主要模型:

  • 连续词袋(CBOW)

  • Skip-gram 模型

Node2vec 受到 skip-gram 模型的影响,因此你将跳过 CBOW 实现说明。skip-gram 模型预测给定词的上下文。上下文定义为与输入项相邻的词。

图 9.7 展示了在 skip-gram 模型中如何收集训练词对。记住,skip-gram 模型的目标是预测上下文词或与目标词经常共现的词。算法通过将特定词与其相邻词结合为训练对,为文本语料库中的每个词创建训练对。例如,在图 9.7 的第三行中,你可以观察到单词grey被突出显示并定义为目标词。算法通过观察其相邻或邻近词来收集训练样本,代表该词出现的上下文。在这个例子中,当构建训练对样本时,考虑了突出词左右两侧的两个词。以目标词为中心的上下文窗口中词的最大距离定义为窗口大小。然后,训练对被输入到一个浅层、两层神经网络中。

09-07

图 9.7 预测新流语言的过程

图 9.8 展示了 word2vec 神经网络的架构。如果你从未见过或使用过神经网络,请不要担心。你只需要知道,在训练这个神经网络的过程中,输入是一个表示输入词的独热编码向量,输出也是一个表示上下文词的独热编码向量。

09-08

图 9.8 Word2vec 浅层神经网络架构

大多数机器学习模型不能直接处理分类值。因此,通常使用一热编码将分类值转换为数值。例如,您可以看到图 9.9 中所有不同的类别通过一热编码过程转换成了列。图 9.9 中只有三个不同的类别,所以一热编码输出中有三个列。然后,您可以看到类别“蓝色”在“蓝色”列下编码为 1,在其他所有列下编码为 0。本质上,类别“蓝色”的数值表示为[1,0,0]。同样,类别“黄色”的数值表示为[0,0,1]。如您所观察到的,一热编码向量将在它们所属类别的列下有一个单独的 1,而向量的其他元素都是 0。因此,一热编码技术确保了所有类别之间的欧几里得距离是相同的。虽然这是一个简单的技术,但它非常流行,因为它允许将分类值简单地转换为数值,这些数值然后可以被输入到机器学习模型中。

09-09

图 9.9 一热编码技术将分类值转换为数值

跳字模型训练步骤完成后,输出层的神经元代表一个词与目标词关联的概率。Word2vec 使用了一个技巧,我们并不关心神经网络的输出向量,而是希望学习隐藏层的权重。隐藏层的权重实际上是我们要学习的词嵌入。隐藏层中的神经元数量将决定嵌入维度或词汇表中每个词所表示的向量的大小。请注意,神经网络不考虑上下文词的偏移,因此它不会区分输入的直接相邻上下文词和上下文窗口中更远的上下文词,甚至不会区分上下文词在目标词之前还是之后。因此,窗口大小参数对词嵌入的结果有重大影响。例如,一项研究(Levy,2014)发现,较大的上下文窗口大小倾向于捕获更多的主题或领域信息。相比之下,较小的窗口倾向于捕获更多关于词本身的信息(例如,哪些其他词在功能上是相似的)。

9.3.2 随机游走

那么,word2vec 与节点嵌入有什么关系呢?node2vec 算法在底层使用 skip-gram 模型;然而,由于你在一个图中不是处理文本语料库,你怎么定义训练数据呢?答案是相当巧妙的。Node2vec 使用random walks从给定的网络中生成“句子”语料库。比喻来说,随机游走可以想象为一个醉酒的人遍历图。当然,你永远无法确定一个醉酒的人下一步会去哪里,但有一点是确定的。遍历图的醉酒的人只能跳到相邻的节点。

node2vec 算法使用随机游走来生成句子,这些句子可以用作 word2vec 模型的输入。在图 9.10 中,随机游走从节点 A 开始,通过节点 C、B 和 F 遍历到节点 H。随机游走的长度是任意决定的,并且可以用walk length参数来改变。随机游走中的每个节点被视为句子中的一个单词,句子的长度由游走长度参数定义。随机游走从图中的所有节点开始,以确保捕获句子中的所有节点。然后,这些句子被传递到 word2vec skip-gram 模型作为训练示例。这就是 node2vec 算法的核心。

09-10

图 9.10 使用随机游走来生成句子

然而,node2vec 算法实现了二阶偏置随机游走。一阶随机游走的一步只依赖于其当前状态。

想象一下,你 somehow wound up at node A in figure 9.11. 因为一阶随机游走只看它的当前状态,算法不知道它之前在哪个节点。因此,返回到之前节点或任何其他节点的概率是相等的。计算概率没有复杂的数学概念。节点 A 有四个邻居,所以遍历到任何一个的概率是 25%(1/4)。

09-11

图 9.11 首阶随机游走

假设你的图是带权重的,这意味着每个关系都有一个属性来存储其权重。在这种情况下,这些权重将包含在遍历概率的计算中。

在带权重的图中,遍历特定连接的概率是其权重除以所有相邻权重之和。例如,在图 9.12 中从节点 A 遍历到节点 E 的概率是 2 除以 8(25%),从节点 A 遍历到节点 D 的概率是 37.5%。

09-12

图 9.12 首阶加权随机游走

另一方面,二阶游走考虑了当前状态以及之前的状态。简单来说,当算法计算遍历概率时,它也会考虑之前一步的位置。

在图 9.13 中,游走刚刚从节点 D 游走到节点 A 的上一步,现在正在评估其下一步移动。回溯游走并立即重新访问游走中节点的可能性由回退参数 p 控制。如果回退参数 p 的值较低,那么重新访问节点 D 的可能性更高,使随机游走更接近游走的起始节点。相反,将参数 p 设置为高值确保重新访问节点 D 的可能性较低,并避免在采样中的两跳冗余。参数 p 的更高值也鼓励适度的图探索。

09-13

图 9.13 二阶随机游走

inOut 参数 q 允许遍历计算区分内向和向外节点。将参数 q 设置为高值(q > 1)会使随机游走偏向于移动到上一步节点附近的节点。查看图 9.13,如果你将参数 q 设置为高值,从节点 A 出发的随机游走会更偏向于节点 B。这样的游走可以获得相对于游走起始节点的底层图的一个局部视图,并近似广度优先搜索。相反,如果 q 的值较低(q < 1),游走更倾向于访问距离节点 D 更远的节点。在图 9.13 中,节点 C 和 E 更远,因为它们不是上一步节点邻居。这种策略鼓励向外探索,并近似深度优先搜索。

node2vec 算法的作者声称,近似深度优先搜索将产生更多基于社区或同质性的节点嵌入。另一方面,随机游走的广度优先搜索策略鼓励结构角色嵌入。

9.3.3 计算节点 2vec 嵌入

现在你已经对节点嵌入和 node2vec 算法有了理论上的理解,你将在一个实际例子中使用它。正如之前提到的,作为 Twitch 的数据科学家,你的任务是根据不同流之间的共享观众或聊天者来预测新主播的语言。图已经构建完成,你只需要执行 node2vec 算法并训练一个分类模型。一如既往,你首先必须投影一个内存图。关系代表流之间的共享观众。当流 A 与流 B 共享观众时,这直接意味着流 B 也与流 A 共享观众。因此,你可以将这些关系视为无向的。此外,你知道一对流之间共享了多少用户,你可以将这个关系表示为关系权重。执行以下查询以投影流之间共享观众的无向加权网络。

列表 9.6 在内存中投影流及其共享观众关系的内存图

run_query("""
CALL gds.graph.project("twitch", "Stream",
  {SHARED_AUDIENCE: {orientation: "UNDIRECTED", properties:["weight"]}})
""")

列表 9.6 中的 Cypher 语句投影了一个名为twitch的内存图。为了将关系视为无向的,你必须将orientation参数值设置为UNDIRECTED。关系properties参数可以用来定义要包含在投影中的关系属性。

最后,你可以执行 node2vec 算法。你可以调整多个参数以获得最佳结果。然而,超参数优化不在此章的范围内。你将使用 8 的embeddingDimension参数值,这意味着每个节点将用一个包含八个元素的向量来表示。接下来,你将定义inOutFactor参数为 0.5,这会鼓励更多的深度优先搜索遍历,并产生基于同质性的嵌入。在这个例子中,你对节点的结构角色不感兴趣,只想编码它们在图中的接近程度。所有其他参数都将保留默认值。执行以下 Cypher 语句以执行 node2vec 算法并将结果存储到数据库中。

列表 9.7 计算 node2vec 嵌入并将它们存储到数据库中

data = run_query("""
CALL gds.beta.node2vec.write('twitch',
  {embeddingDimension:8, relationshipWeightProperty:'weight',
   inOutFactor:0.5, writeProperty:'node2vec'})
""")

9.3.4 评估节点嵌入

在你训练语言分类模型之前,你将评估嵌入结果。你将首先检查存在关系的节点对的嵌入的余弦和欧几里得距离。余弦和欧几里得距离分布可以用 Cypher 计算,然后使用 seaborn 库进行可视化。

列表 9.8 评估连接节点的嵌入的余弦和欧几里得距离

import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams["figure.figsize"] = [16, 9]

df = run_query("""
MATCH (c1:Stream)-[:SHARED_AUDIENCE]->(c2:Stream)
RETURN gds.similarity.euclideanDistance(
    c1.node2vec, c2.node2vec) AS distance, 'Euclidean' as metric
UNION
MATCH (c1:Stream)-[:SHARED_AUDIENCE]->(c2:Stream)
RETURN gds.similarity.cosine(
    c1.node2vec, c2.node2vec) AS distance, 'cosine' as metric
"""
)

sns.displot(
    data=df,
    x="distance",
    col="metric",
    common_bins=False,
    facet_kws=dict(sharex=False),
    height=7,
)

列表 9.8 中的代码生成了图 9.14 中的可视化,它显示了存在关系的节点对之间的嵌入的余弦和欧几里得距离分布。对于欧几里得距离,值越低,节点在嵌入空间中的相似度或接近度越高。你可以观察到分布的顶部略低于 1。大多数节点基于欧几里得距离非常相似;然而,也有一些节点对,它们的距离略大。另一方面,对于余弦相似度,当值接近 1 时,两个节点在嵌入空间中非常接近。同样,大多数存在关系的节点对具有接近 1 的余弦相似度。那么当一对节点之间存在关系但它们的嵌入的余弦相似度,例如,小于 0.5 时会发生什么?使用以下代码,你可以调查基于它们的组合度值,连接节点对之间余弦相似度的依赖性。

09-14

图 9.14 存在关系的节点对之间嵌入的余弦和欧几里得距离分布

列表 9.9 评估余弦相似度对组合节点度值的依赖性

df = run_query("""
MATCH (c1:Stream)-[:SHARED_AUDIENCE]->(c2:Stream)
WITH c1, c2, gds.similarity.cosine(
        c1.node2vec, c2.node2vec) AS cosineSimilarity,
     count{ (c1)-[:SHARED_AUDIENCE]-() } AS degree1,
     count{ (c2)-[:SHARED_AUDIENCE]-() } AS degree2
RETURN round(cosineSimilarity,1) AS cosineSimilarity,
       avg(degree1 + degree2) AS avgDegree
ORDER BY cosineSimilarity
"""
)

sns.barplot(data=df, x="cosineSimilarity", y="avgDegree", color="blue")

列表 9.9 中的代码生成了图 9.15 中的可视化,你可以清楚地看到,一个节点拥有的连接越多,平均来说,它与其邻居的相似度就越低。这在某种程度上是有道理的。想象一下,如果你只有一个朋友,你可能在几个重要的方面几乎与他们完全相同。然而,当你有 100 个朋友时,你不可能与他们都完全相同。你可以从每个朋友那里挑选一些你共有的属性,但要做到与所有朋友都完全相同实际上是不可能的,除非他们也都是相同的。

09-15

图 9.15 基于组合节点度值的连接节点平均余弦相似度的分布

你还指定了关系权重来计算 node2vec 嵌入。关系权重越高,随机游走越倾向于遍历它。你可以通过以下代码检查连接节点的余弦相似度如何依赖于关系权重。

列表 9.10 评估连接节点余弦相似度对关系权重的依赖

df = run_query("""
MATCH (c1:Stream)-[r:SHARED_AUDIENCE]->(c2:Stream)
WITH c1, c2, gds.similarity.cosine(
     c1.node2vec, c2.node2vec) AS cosineSimilarity,
     r.weight AS weight
RETURN round(cosineSimilarity,1) AS cosineSimilarity,
       avg(weight) AS avgWeight
ORDER BY cosineSimilarity
"""
)
sns.barplot(data=df, x="cosineSimilarity", y="avgWeight", color="blue")

列表 9.10 中的代码生成了图 9.16 中的可视化,它显示了连接节点对之间的欧几里得和余弦相似度的分布。

09-16

图 9.16 基于组合节点度值的连接节点平均余弦相似度的分布

再次,你可以清楚地观察到连接节点的余弦相似度与关系权重的依赖关系。关系权重越高,随机游走越有可能遍历它。因此,当一对节点在随机游走中频繁地紧密出现时,它们的嵌入越有可能更相似。当关系权重较低时,随机游走会倾向于不遍历它。因此,你可以观察到一些示例,即使一对节点之间存在关系,它们的嵌入也可能完全不相似。当一对节点的嵌入的余弦距离接近零时,人们可能会假设这对节点没有连接。然而,这可能是随机游走倾向于避免遍历这两个节点之间关系的一种偏见。

9.3.5 训练分类模型

在本章的最后部分,你将训练一个分类模型来预测新流式的语言。首先,你必须从数据库中检索相关数据并进行必要的预处理。

列表 9.11 为分类训练检索和预处理相关数据

data = run_query("""
MATCH (s:Stream)
RETURN s.id AS streamId, s.language AS language, s.node2vec AS embedding
"""
)
data['output'] = pd.factorize(data['language'])[0]    ❶

❶ 将要表示为整数的语言进行编码

列表 9.11 中的代码首先从数据库中检索数据。一个简单的 Cypher 语句返回流 ID、语言和节点嵌入。由于语言以字符串形式表示,你需要将它们映射或编码为整数。你可以使用 pd.factorize 方法轻松地将分类值,如语言,编码为整数。在此步骤之后,数据框的结构应如图 9.3 所示。

表 9.3 Pandas 数据框结构

streamId language embedding output
129004176 en [-0.952458918094635,...] 0
50597026 fr [-0.25458356738090515,...] 1
102845970 ko [-1.3528306484222412,... ] 2

在表 9.3 中,你可以观察到 pd.factorize 方法将英语语言编码为 0。法语语言映射为 1,依此类推。

embedding 列包含表示每个数据点的向量或列表。因此,分类模型的输入将是 embedding 模型,你将训练它来预测 output 列下的整数。在这个例子中,你将使用 scikit-learn 库中的随机森林分类器。与所有机器学习训练一样,你必须将你的数据分割成训练集和测试集。你将使用 train_test_split 来生成数据集的训练和测试部分。执行以下代码以训练一个随机森林分类模型来预测新流的语言。

列表 9.12 基于数据集的训练部分分割数据集并训练随机森林模型分类器

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier

X = data['embedding'].to_list()
y = data['output'].to_list()

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2,
  random_state=0)

rfc = RandomForestClassifier()
rfc.fit(X_train, y_train)

9.3.6 评估预测

在本章中,你将做的最后一件事是在测试数据上评估模型。你将从检查分类报告开始。分类报告用于衡量机器学习模型预测的质量。执行以下代码以生成分类报告。

列表 9.13 生成分类报告

from sklearn.metrics import classification_report

y_pred = rfc.predict(X_test)
print(classification_report(y_test,y_pred))

列表 9.13 中的代码生成了图 9.17 中的报告,你可以观察到你正在处理一个不平衡的数据集,因为英语有 384 个测试数据点,而法语流只有 54 个示例。此外,编号 9 下映射的语言是意大利语,只有 19 个测试数据点。在处理不平衡数据集时,检查 F1 分数是有意义的。F1 分数和加权 F1 分数都是 0.91,这是一个很好的结果。假设聊天者通常在共享相同语言的流中进行聊天的假设是有效的。

09-17

图 9.17 分类报告

最后,你将生成混淆矩阵。混淆矩阵可以帮助你评估数据点的实际与预测类别。执行以下代码以可视化混淆矩阵。

列表 9.14 计算每个用户的推文计数和转发率

from sklearn.metrics import ConfusionMatrixDisplay

ConfusionMatrixDisplay.from_predictions(y_test, y_pred,
  normalize="true", cmap="Greys")

列表 9.14 中的代码生成了图 9.18 中的可视化。记住,英语语言映射到 0。通过检查图 9.18 中的混淆矩阵,你可以观察到模型仅在英语和其他语言之间发生错误分类。例如,模型从未将韩语错误分类为葡萄牙语。这很有道理,因为英语是互联网上最常用的语言,所以除了他们的母语外,许多人至少会说一点英语。

09-18

图 9.18 混淆矩阵

练习题 9.2

尝试不同的 node2vec 算法配置,以观察它如何影响连接节点嵌入之间的余弦距离和分类模型的准确性。你可以移除关系权重参数来观察 node2vec 算法的无权重版本的行为,或者微调 embeddingDimensioninOutFactorreturnFactor 参数。查看官方文档 (mng.bz/lVXo) 以获取 node2vec 参数的完整列表。

恭喜!你已经成功训练了第一个基于 node2vec 嵌入的节点分类模型。记住,由于 node2vec 是一个归纳模型,当图中添加新节点时,你应该重新训练模型。因此,像 node2vec 这样的模型通常用于每天运行一次的批量处理管道中。

9.4 练习题解答

练习题 9.1 的解答如下。

列表 9.15 计算没有传入或传出关系的 Stream 节点

MATCH (n:Stream)
WHERE NOT EXISTS {(n)--()}
RETURN count(*) AS result

摘要

  • 节点嵌入模型使用降维技术来生成任意大小的节点表示。

  • 节点嵌入模型可以根据网络中的结构角色编码节点,或者可以采用更基于同质性的设计。

  • 一些节点嵌入模型是归纳性的,这意味着它们不能为训练期间未看到的节点生成嵌入。

  • node2vec 算法受到 word2vec 跳字模型(skip-gram model)的启发。

  • node2vec 算法使用随机游走来生成句子,然后将其输入到 skip-gram 模型中。

  • 二阶随机游走在计算下一个遍历可能性时会考虑随机游走的上一步。

  • Node2vec 可以微调以生成基于节点结构角色或同质性的嵌入。

  • 嵌入维度参数定义了表示节点的向量的大小。

  • 使用节点嵌入模型时,请特别注意孤立节点。大多数节点嵌入模型对孤立节点进行相同的编码,因此请确保它们符合你的任务要求。

  • 节点分类是一个基于节点网络特征预测节点属性或标签的任务。

10 链接预测

本章节涵盖

  • 讨论链接预测工作流程

  • 介绍链接预测数据集划分

  • 基于节点对构建链接预测特征

  • 训练和评估监督链接预测分类模型

大多数现实世界的网络都是动态的,并且随时间演变。以人们的友谊网络为例。人们的朋友会随时间变化。他们可能会遇到新的人或停止与某些人交往。你可能会认为在友谊网络中新的连接是随机形成的;然而,事实证明,大多数现实世界的网络都有一个深刻的组织原则。围绕链接预测的研究集中在识别和理解各种网络演变机制,并将它们应用于预测未来链接。

图 10.1 展示了一个小规模的人际网络,其中关系代表友谊。实线表示现有连接。正如之前提到的,友谊网络会随时间演变,人们会形成新的连接。直观地,你可能会认为图 10.1 中的 Luke 和 Rajiv 比 Jose 和 Alice 更有可能形成未来的连接,用虚线表示,因为他们在网络中更接近。不出所料,一对个体之间的共同朋友数量是他们未来是否可能相遇的好指标。简单来说,两个个体在网络中越接近,形成未来链接的概率就越高。预测网络中的未来链接是链接预测领域的主要目标。准确预测未来链接可以用于推荐系统,或者可以用来更好地理解网络的组织原则。想象一个由用户和产品组成的二分网络,其中连接代表他们购买的产品。在这个例子中,链接预测技术对于预测未来的用户-产品关系至关重要,这允许你实施个性化的产品推荐。

10-01

图 10.1 链接预测

另一方面,有些网络不一定随时间演变,但我们对其连接的了解有限。一个这样的例子是药物和疾病的生物网络。一种药物通常只能治疗有限种类的疾病。必须进行临床试验以确定药物是否可以治疗任何新的疾病;然而,临床试验成本非常高。药物再利用的另一个问题是,可以进行的药物和疾病组合临床试验数量庞大。链接预测技术可用于识别网络中的缺失链接。预测网络中缺失链接的过程可以被视为链接完成

图 10.2 中可视化的生物医学网络由药物和疾病组成。实线表示哪些疾病可以使用药物。例如,阿司匹林可以用来治疗头痛、川崎病和冠状动脉疾病。另一方面,赖诺普利被用来治疗冠状动脉疾病和高血压。由于阿司匹林和赖诺普利都可以用来治疗冠状动脉疾病,因此探索赖诺普利是否可以治疗阿司匹林不能治疗的疾病是有意义的。再次强调,你只是在观察药物之间共同邻居的数量来基于预测。请注意,这是一个药物再利用场景的简化版本。在现实世界中,会考虑关于人类基因、通路和其他生物过程的大量信息。此外,你还需要在过程中包括生物医学领域的专家,以帮助构建图、评估结果等。

10-02

图 10.2 链接补全

10.1 链接预测工作流程

如前所述,大多数网络都遵循各种组织原则,无论是故意还是无意,你都可以在分析中使用这些原则来预测一对节点之间新链接的概率。例如,在一个社交网络中,你可能会假设如果两个人的年龄相似,他们更有可能成为朋友。然而,仅仅关注个人特征可能是不满意的,因为这种方法可能会丢失关于两个人之间关系的大量信息。两个人可能年龄相似,但他们并不在同一个社交群体中混,所以他们成为朋友的可能性较低。另一方面,如果两个人有很多共同的朋友,他们更有可能相遇并成为朋友。此外,如果一个人有很多朋友,他们形成新连接的可能性比朋友少的人要高。

当预测是否将建立新的连接时,你永远不会孤立地检查单个节点,而是检查图中的节点对。因此,链接预测的关键步骤是设计能够编码节点对的特征。

你可以采取几种方法来编码一对节点。我将它们分为三类,如图 10.3 所示。首先,你可以组合任何节点属性。例如,我在社交网络示例中使用了年龄差异。你也可以取节点度数的乘积或节点嵌入的余弦相似度。然而,你也可以简单地连接嵌入或任何其他属性,如果这可能会表现得更好。基于距离的度量标准是你在链接预测中可以使用的另一组特征。这一组的典型代表是节点对之间最短路径的长度。本质上,你计算从一个节点到另一个节点所需的跳数,并将其用作特征。其基本思想是,节点在网络中越接近,未来它们之间形成链接的可能性就越大。第三组特征专注于评估两个节点之间的邻域重叠。例如,共同朋友的数量越高,这对在未来某时相遇的可能性就越大。你也可以计算 Jaccard 相似度指数,它将简单地是共同朋友计数的归一化版本。还有其他度量标准,它们编码了局部邻域的重叠,你将在本章后面学到。

10-03

图 10.3 编码节点对

图 10.3 中展示的三种类别及其示例并不提供可能特征的完整列表。还有其他方法可以将度量标准组合成链接预测特征。然而,图 10.3 中展示的组别涵盖了你在链接预测任务中会遇到的大多数生成特征的方法。

预测链接的一种简单方法是在节点对之间计算链接预测度量,并简单地选取任意数量的它们作为未来可能发生的链接。另一方面,你也可以决定使用这些链接预测度量来训练分类模型。训练分类模型的一个附加价值是它可以学会识别你在采用无监督方法时可能错过的模式。例如,模型可以识别某些链接预测度量比其他度量更重要。此外,特定的度量组合可能对结果有显著的预测能力。如果只考虑单个相似度度量的最高分数,这些细节可能会被忽视。在本章中,你将学习如何计算几个链接预测度量,并基于这些度量训练和评估分类模型。

现在,假设你仍然在 Twitch 担任数据科学家。你被分配了寻找改进频道推荐方法的任务。到目前为止,你已经使用了基于不同频道之间共享观众群体的推荐系统。如果两个频道之间存在显著的观众重叠,你可以使用这些信息向用户提供推荐。作为用户,你将在“此频道用户也观看”部分看到这些推荐。你该如何尝试改进这些推荐呢?一个想法是预测哪些频道将在未来共享观众。然后,你可以将这些预测作为推荐提供给用户。这种方法可能会提高整体推荐的准确性,因为你会推荐既有观众重叠的频道,也有未来观众重叠概率高的频道。

图 10.4 显示了 Twitch 频道或流量的网络,其中关系表示观众重叠。实线代表现有的共享观众重叠,可以用来填充推荐系统的一部分。作为数据科学家,你可以使用现有关系的信息来预测未来的观众重叠。然后,可以使用未来的观众重叠预测来驱动你的推荐引擎。

10-04

图 10.4 链接预测流程

有趣的是,使用未来链接预测作为推荐,反过来可以被视为自我实现的预测。类似的方法已经被用来推荐电影(Lakshmi & Bhavani, 2021)、产品(Darke et al., 2017),甚至医学概念之间的链接(Kastrin et al., 2014)。链接预测还可以与其他技术结合使用,构建混合推荐引擎(Çano & Morisio, 2017)。你将如何着手训练一个分类模型,用于预测频道之间未来观众的重叠?

链接预测分类模型的训练过程的高级概述在图 10.5 中展示。第一步是将关系分为三个不同的集合。一个集合用于生成网络特征,而其他两个集合用于训练和评估分类模型。如果你使用相同的关系来同时生成网络特征和训练分类模型,可能会遇到数据泄露问题。数据泄露发生在你的训练数据包含关于输出的信息,但在模型用于预测时,类似的数据将不可用。数据泄露通常会导致模型在训练和可能评估期间表现良好,但不幸的是,它对新预测的表现不佳。如果你在使用任何图特征与链接预测模型,你必须格外小心,以防止任何特征泄露。特征泄露指的是一个特征包含与输出相同或可比较的信息。例如,想象你正在使用两个节点之间的基于距离的最短路径特征。通过使用相同的关系来生成特征和训练模型,模型会简单地学会根据网络距离为 1 的所有节点对进行分类或预测连接。换句话说,当一对节点的网络距离为 1 时,这对节点之间存在关系。因此,网络距离特征和分类输出将包含相同的信息,导致特征泄露。本质上,你可以将特征泄露视为在训练期间偷看结果而对模型评估作弊。为了避免特征和数据泄露,你需要使用一个关系集合来计算网络特征,并使用另一个关系集合为训练和评估模型提供监督分类示例。

10-05

图 10.5 构建链接预测模型以推荐 Twitch 频道

一旦完成数据集的拆分,你需要计算用于训练模型的链接预测指标。如前所述,你可以计算网络距离,计算共同邻居的数量,或者简单地聚合节点属性。你需要计算网络中链接的正例和负例的特征。最后,使用这些链接预测特征来训练一个分类模型,以预测链接在未来是否可能发生。鉴于数据集被拆分为训练和测试关系集,你可以在数据集的测试集上评估你的模型。一旦确定分类模型的表现足够好,你就可以在生产中使用它为平台用户生成推荐。

为了完成本章的练习,你需要将 Twitch 网络导入到第九章中描述的 Neo4j 数据库中。本章中所有代码示例的 Jupyter 笔记本可在以下 GitHub 网页上找到:mng.bz/Y1JA

10.2 数据集分割

你需要相应地分割数据集以评估训练好的分类模型。如果你的模型没有使用基于图的特征,如基于距离或基于邻居的度量,那么你可以遵循传统的测试/训练数据分割。假设你正在尝试根据人们的年龄和教育程度预测他们之间新的链接。由于这两个特征都是节点属性,而不是基于图的特征,你可以简单地取现有关系的 80%作为训练集,并在剩余的 20%的关系上评估你的模型。显然,你需要添加一些负例,因为否则模型无法学会区分两种输出,并可能产生不准确的预测。生成负例不是问题,因为有许多节点对没有连接。在实践中,正链接示例的规模与图中节点的数量成线性关系,而负例的规模成二次关系。这可能导致相当大的类别不平衡问题;然而,在链接预测过程中,通常需要从负分类样本中子采样到与正样本大致相同的数量。你将在下一节中了解更多关于负例子采样的内容。

一旦你添加了任何基于图的特征来捕捉图中节点之间的相似性或接近度,你就需要非常注意数据泄露问题。记住,当一个特征包含与输出变量相同或可比较的信息,但在进行预测时不可用,你就已经将数据泄露引入了工作流程。最明显的例子是图中节点之间的网络距离。如果网络特征和训练示例是在同一组关系上计算的,那么模型将简单地学习到只有一步之遥的节点之间存在关系。然而,网络中没有任何链接的节点对将不会被分类为可能形成连接,因为它们都不是一步之遥。即使基于第九章中介绍的亲社会性原则的节点嵌入也可能存在问题,如果你没有进行适当的数据集分割。总的来说,大多数基于图的特征可能会引入一些数据泄露问题。通常将关系分为三组以避免数据泄露问题:

  • 用于生成特征的关联集

  • 用于训练模型的关联集

  • 用于评估模型的关联集

使用单独的关系集来生成网络特征,然后进行分类训练和评估,你可以避免基于图的特征泄漏问题。因此,计算出的网络特征将不会与输出变量有相同或非常相似的信息。以网络距离为例,监督分类示例将具有至少 2 的最小网络距离。正负分类示例都可以有 2 的网络距离。反过来,网络距离特征与输出变量不相同,你也就防止了任何特征泄漏。

在进行数据集分割方面有许多选择。在本节中,你将了解基于时间随机的数据集分割技术。

10.2.1 基于时间的分割

理论上,链接预测是一种基于过去连接来预测未来连接的技术。如果你知道链接的创建时间,你可以根据时间成分产生数据集分割。

图 10.6 中的原始网络在 2020 年至 2021 年之间创建了关系。在这个简单的图示例中,你可以使用 2020 年的关系来生成网络特征。特征集中应该有相当数量的关系,因为你不希望网络过于分散或节点过于孤立。特征集中关系太少可能会产生较差的网络特征,这反过来可能无法预测未来的链接。图 10.6 中 2021 年创建的关系用于构建测试集和训练集。例如,你可以使用 80%的新关系作为训练集,剩余的 20%用于评估模型。记住,用于生成网络特征的关系不应用于训练集或测试集。如果你计划对分类模型进行任何超参数优化,你也可以选择从 2021 年创建的新关系中引入一个验证集。

10-06

图 10.6 链接预测任务中关系分割的基于时间方法

如果可能的话,使用基于时间的分割,因为它准确地模拟了预测未来链接的场景。在图 10.6 的示例中,你使用 2020 年的现有关系知识来预测未来(2021 年)。额外的优势是,模型应该学会捕捉网络的潜在组织机制,因为基于时间的分割遵循网络演变,这反过来应该提供更好的预测。不幸的是,共享观众数据的 Twitch 数据集没有关系的时间成分,所以你将不得不求助于另一种方法。

10.2.2 随机分割

随机分割类似于基于时间分割,因为你需要生成训练集和测试集以及关系的特征集。由于没有时间信息可用,你无法区分过去和未来的关系。相反,你随机从图中选取关系子集作为训练集和测试集,用于训练和评估分类模型。随机分割对于链接补全任务也很有用,在这种任务中,你预测网络中的缺失链接,且没有时间组件可用。

你可以看到图 10.7 几乎与图 10.6 完全相同。唯一的区别在于如何选择哪些关系属于哪个集合。基于时间的方法,你可以根据时间属性选择关系所属的集合。然而,由于没有时间信息可用,你需要采取随机方法。因此,你随机选择关系属于哪个集合。

10-07

图 10.7 链接预测任务的关系分割随机方法

注意:在链接预测数据集分割中的关键概念是,为了避免数据泄露,用于计算网络特征所使用的链接应与用于训练和评估分类模型的监督链接样本不同。特征泄露发生在特征包含与输出变量相同或可比较信息的情况下。例如,如果你避免引入一个单独的特征集来计算网络特征,如网络距离,分类模型会学习到一对节点在一跳或遍历距离内形成或拥有链接的概率为 100%。因此,训练集和测试集上的模型准确率会达到 100%,因为网络距离包含与输出变量相同的信息。然而,模型在预测缺失或未来链接方面会非常糟糕。如果你计划进行任何超参数优化,可以选择引入验证集。

现在,你将对 Twitch 共享观众网络进行链接预测的随机分割。你需要确保 Neo4j 环境运行正常,并在第九章中定义的 Twitch 数据集已加载。接下来,你需要打开一个 Jupyter 笔记本来定义与 Neo4j 数据库的连接。

列表 10.1 定义与 Neo4j 的连接

from neo4j import GraphDatabase

url = "bolt://localhost:7687"
username = "neo4j"
password = "letmein"

# Connect to Neo4j
driver = GraphDatabase.driver(url, auth=(username, password))

def run_query(query, params={}):
  with driver.session() as session:
    result = session.run(query, params)
    return result.to_df()

列表 10.1 中的代码定义了与 Neo4j 数据库的连接以及用于执行任何 Cypher 语句的 run_query 函数。根据需要更改 urlusernamepassword 变量。

练习 10.1

计算 Twitch 共享观众网络中的关系数量。

数据集中有 131,427 个关系。你将首先构建关系特征集。记住,特征集需要尽可能大,因为你希望保留尽可能多的连接网络,而不至于有太多孤立节点或断开的部分。在这种情况下,你可以在特征集中使用 90%的所有关系,为你留下大约 13,000 个正样本用于训练集和测试集。为了构建特征集,你将创建具有FEATURE_REL类型的新关系。Cypher 提供了一个rand()函数,它返回一个介于 0 到 1 之间的随机浮点数,并遵循近似均匀分布。为了选择关系特征集的随机子集,你将在 Cypher 语句中使用rand()函数。以下 Cypher 语句大约选取了 90%的现有SHARED_AUDIENCE关系,并在这些节点对之间创建了一个新的FEATURE_REL连接。

列表 10.2 构建关系特征集

run_query("""
MATCH (s1:Stream)-[:SHARED_AUDIENCE]->(s2:Stream)
WITH s1, s2
WHERE rand() <= 0.9
MERGE (s1)-[:FEATURE_REL]->(s2);
""")

列表 10.2 中的代码选择并创建了一个随机、非确定性的关系集。请确保只运行一次,因为否则关系分割将不可接受。如果你出于任何原因多次运行了查询,只需删除FEATURE_REL关系并重新运行列表 10.2 中的 Cypher 语句。请注意,由于使用了rand()函数,在重新运行查询时,你可能会得到FEATURE_REL关系的不同计数。

练习 10.2

现在,你将选择训练集和测试集中的关系。你将首先为分类模型生成正样本。正样本是在存在SHARED_AUDIENCE关系但不存在FEATURE_REL关系的节点对之间的关系。

匹配存在SHARED_AUDIENCE但不存在FEATURE_REL关系的节点对。接下来,使用MERGE子句在这些节点对之间创建具有TEST_TRAIN类型的新关系。最后,返回新创建的关系数。

我得到了 13,082 个创建的关系的结果。你可能得到一个不同的数字,但应该在大约 13,000 的同一范围内。你已经为分类模型准备好了正样本。现在,是时候选择一些不存在关系的负样本了。

10.2.3 负样本

当训练一个像链接预测模型这样的二元分类器时,你应该在训练集和测试集中包含正例和负例。没有负例,模型无法学会区分两种输出,可能会产生不准确的预测。

现实世界图的一个共同特征是它们是稀疏的。想象一下互联网上的任何大型社交平台。你可能在平台上拥有数百或数千个朋友;然而,平台上可能有数百万甚至数十亿用户。这意味着你只有十亿中可能的关系中的一千种。在机器学习环境中,每个用户可能有几千个正面示例,以及可能大约有十亿的负面示例。如果你使用了所有的负面示例,你将不得不处理相当大的类别不平衡,因为关系的正面示例与节点的数量成线性关系,而负面示例与节点的数量成平方关系。大多数机器学习模型在每一类样本数量大致相同的情况下表现最佳。然而,如果数据集严重不平衡,那么你可能会通过每次都预测多数类来获得很高的准确率。在链接预测中,如果你预测任何一对节点之间不存在链接,你可能会在大多数情况下获得大约 99% 的准确率。因此,对少数类进行错误分类的可能性很高,从而导致分类模型性能不佳。因此,在大多数链接预测工作流程中,通常会子采样负面示例,并使用大约相同数量的正面和负面样本。

练习 10.3

在这个练习中,你将选择节点对来构建分类模型的负面示例。你必须选择与练习 10.2 中产生的正面样本数量大致相同的负面样本。你可以使用 13,082 个正面示例的数量,或者你在练习 10.2 中得到的数量。负面示例应该以确保在特征、训练或测试集中节点对之间不存在关系的方式产生。

首先,匹配一对节点,其中它们之间不存在 SHARED_AUDIENCE 关系。接下来,确保你匹配了两个不同的节点,并将避免遇到源节点和目标节点都相同的情况。过滤掉随机生成的值高于 0.9 (rand() > 0.9) 的节点对,以保证负面示例在一定程度上是随机的,并且代表整个图。一旦正确匹配了节点对,使用 LIMIT 子句将负面示例的数量限制在大约 13,000 个左右。最后,使用 NEGATIVE_TEST _TRAIN 类型在选定的节点对之间创建关系。

10.3 网络特征工程

现在,你将生成网络特征,以捕捉网络中节点对之间的接近度或相似度。想法是,给定网络度量,节点对越接近或越相似,它们形成未来连接的可能性就越大。然后,将使用未来的连接为 Twitch 用户提供更好的推荐。

您可能已经注意到,训练集和测试集在TEST_TRAINNEGATIVE_TEST_TRAIN关系类型下被合并在一起。由于您需要为训练集和测试集都计算链接预测特征,目前没有必要区分这两者。记住,训练集和测试集的所有基于图的特征都将严格基于关系特征集进行计算,以防止任何数据泄露。

再次强调,您可以选择使用学习到的或手动定义的特征。例如,您可以使用 node2vec 算法计算节点嵌入,然后使用节点对之间的嵌入余弦相似度作为分类模型的特征。然而,由于您将使用归纳节点嵌入来计算链接预测特征,因此每当图中新添加一个节点时,您都需要重新训练分类模型。虽然这在某些场景中可能是令人满意的,但您可能不希望每次新流媒体主播出现在平台上时都重新训练模型。幸运的是,关于链接预测特征的研究已经做了很多,您可以从中借鉴一些关于特征工程的想法。从简单且不复杂的特征开始选择并评估其性能是合理的。如果需要,您以后总是可以使用更复杂的技术,比如归纳节点嵌入。在继续生成特征的代码示例之前,您必须完成 10.2 和 10.3 的练习。

10.3.1 网络距离

您将计算的第一个特征是网络距离。网络距离是通过找到一对节点之间的最短路径,然后计算最短路径中的关系数量来计算的。

图 10.8 展示了计算节点 A 和 E 之间网络距离的过程。在第一步,您需要计算这对节点之间的最短路径。在处理无权网络时,最短路径表示穿越最少关系从一节点到另一节点的路径。在图 10.8 的例子中,您必须穿越两个关系才能从节点 A 到达节点 E。换句话说,节点 A 和 E 之间的网络距离是 2。

10-08

图 10.8 计算节点 A 和 E 之间的网络距离

网络距离背后的思想是,网络中两个节点越接近,它们形成未来连接的可能性就越大。例如,想象你正在处理社交网络中的链接预测。训练集或测试集中一对人员的网络距离永远不会是 1,因为这意味着你没有正确执行关系分割。然而,如果网络距离是 2,这意味着这对人员至少有一个共同的朋友。如果距离大于 2,那么这两个人没有共同的朋友,并且不太可能形成未来的连接。理论上,网络距离越高,未来连接的可能性就越小。在你的用例中,两个流在网络中越接近,未来出现显著观众重叠的可能性就越大。

使用 Cypher 查询语言,您可以使用shortestPath()函数找到最短无权路径。在无权路径中,每个关系的遍历具有相同的成本,因此两个节点之间的最短路径将始终是它们之间路径中总关系的数量。shortestPath()函数期望输入一个 Cypher 模式,该模式定义了源节点和目标节点以及路径中可选的允许关系类型。对于更高级的用例,您还可以定义路径中遍历的最小或最大次数或关系数。以下 Cypher 语句查找 Surya 和 Jim 之间的最短路径。

列表 10.3 使用 Cypher 查找最短无权路径

MATCH (source:Person {name:"Surya"}),
      (target:Person {name:"Jim"})                                   ❶
MATCH p = shortestPath((source)-[:FRIEND|COWORKER*1..10]->(target))  ❷
RETURN p

❶ 定义最短路径的源节点和目标节点

❷ 在 p 参考变量下确定源节点和目标节点之间的最短路径

列表 10.3 的第一部分是一个简单的MATCH子句,用于确定源节点和目标节点。接下来,您需要使用 Cypher 语法定义最短路径约束。列表 10.3 中定义的最短路径约束的图模式如下所示。

列表 10.4 定义最短路径约束的图模式

(source)-[:FRIEND|COWORKER*]->(target)

列表 10.4 中的 Cypher 语法定义了sourcetarget节点之间的最短路径。最短路径的一个约束是它只能遍历FRIENDCOWORKER关系。该函数忽略所有其他关系类型。请注意,关系方向也是至关重要的。在列表 10.4 的示例中,最短路径算法只能遍历路径中的出向关系。最后,您需要添加*符号以允许算法遍历多个关系。如果缺少*符号,最短路径约束之一将是算法只能遍历单个关系。

现在,你将为关系(列表 10.5)的训练集和测试集中的所有节点对计算网络距离。节点对的测试和训练集被标记为TEST_TRAINNEGATIVE_TEST_TRAIN关系类型。然后,你必须找到两个集合中所有节点对之间的最短路径。在最后一步,你将使用length()函数计算最短路径的长度,这相当于关系的数量。

列表 10.5 计算训练集和测试集中节点对之间的网络距离

run_query("""
MATCH (s1)-[r:TEST_TRAIN|NEGATIVE_TEST_TRAIN]->(s2)    ❶
MATCH p = shortestPath((s1)-[:FEATURE_REL*]-(s2))      ❷
WITH r, length(p) AS networkDistance                   ❸
SET r.networkDistance = networkDistance                ❹
""")

❶ 匹配所有通过 TEST_TRAIN 或 NEGATIVE_TEST_TRAIN 关系连接的节点对

❷ 识别节点对之间的最短路径。最短路径仅限于允许通过 FEATURE_REL 关系进行遍历,以防止数据泄露。

❸ 使用 length()函数计算最短路径的长度

❹ 将网络距离结果存储为关系属性

你可能会注意到,列表 10.5 中最短路径图模式定义中没有方向指示符。因此,最短路径算法也可以沿着相反的方向遍历关系,有效地将关系视为无向的。

10.3.2 优先连接

在链接预测中使用的另一个流行指标是优先连接。优先连接是现实世界中网络的一个基本组织原则,其中具有更多关系的节点更有可能建立新的关系。在社会网络示例中,朋友更多的人更有可能建立新的联系。他们可能会被邀请参加更多的社交活动或被介绍给更多的人,因为他们的朋友众多。优先连接模型最初由巴拉巴西和艾伯特(1999 年)描述。

图 10.9 显示了两个位于中心的Stream节点,它们的节点度数相对较大。优先连接机制假设已经与许多其他流共享大量受众的流更有可能形成未来的连接。因此,根据图 10.9 中的虚线,可以假设这两个中心的Stream节点很可能有共享的受众重叠。

10-09

图 10.9 节点度数较高的节点更有可能形成新的连接。

要计算节点对之间的优先连接指标,你需要将它们的节点度数相乘。本质上,你取第一个节点的节点度数并乘以第二个节点的节点度数。当节点对具有高优先连接指标时,节点更有可能在将来形成连接。

练习 10.4

计算训练集和测试集中节点对的优先连接度指标。类似于网络距离度量的计算,你首先通过匹配与TEST_TRAINNEGATIVE_TEST_TRAIN关系连接的节点对。接下来,计算两个节点的节点度。确保将传入和传出的关系都计入节点度,并且只计算FEATURE_REL关系。最后,将两个节点度相乘,并将结果存储在关系的preferentialAttachment属性下。

10.3.3 常见邻居

你接下来要计算的链接预测特征指标是共同邻居指标。共同邻居指标背后的直觉很简单。两个节点共有的邻居越多,未来形成链接的可能性就越高。在社会网络的背景下,两个人共有的朋友越多,他们未来相遇或被介绍的可能性就越大。

记住,由于关系分割,训练集或测试集中的节点对之间没有直接连接。然而,许多节点可能有几个共同的朋友,如图 10.10 所示。想象一下,图 10.10 中的所有节点代表 Twitch 直播。如果直播 A 与直播 B 的观众有重叠,而直播 B 与直播 C 有重叠,那么未来直播 A 和 C 之间很可能有观众重叠。此外,两个直播之间的共同邻居数量越多,未来建立链接的概率就越高。为了在链接预测模型中使用共同邻居度量,你需要计算训练集和测试集中所有节点对的共同邻居数量。

10-10

图 10.10 节点对之间的共同邻居

练习 10.5

计算训练集和测试集中节点对的共同邻居度指标。你与前一次类似,通过匹配与TEST_TRAINNEGATIVE_TEST_TRAIN关系连接的节点对。然后,你需要计算匹配节点对之间的不同共同邻居数量。确保使用OPTIONAL MATCH子句包括没有共同邻居的节点对的结果。最后,将节点对之间的共同邻居数量存储在关系的commonNeighbor属性下。

10.3.4 Adamic-Adar 指数

Adamic-Adar 指数是 Adamic 和 Adar(2003)首次描述的链接预测指标。Adamic-Adar 指数背后的想法是,一对节点之间共有的节点度越小,它们未来形成连接的可能性就越大。再次想象你正在处理一个社交网络。一对人有共同的一个朋友。如果这个共同朋友有 1,000 个其他朋友,他们不太可能介绍这对特定的人,如果他们总共只有两个朋友的话。

在图 10.11 的示例 A 中,节点 A 和 B 有两个共同邻居或朋友。共同的邻居是节点 C 和 D。节点 C 和 D 总共各有 1,000 个朋友。由于节点 A 和 B 的共同邻居本身有一个广泛的社交圈,因此共同邻居中的任何一个不太可能将节点 A 和 B 介绍给对方。另一方面,在图 10.11 的示例 B 中,节点 A 和 B 的共同邻居总共只有两个朋友。本质上,节点 C 和 D 只与节点 A 和 B 是朋友。因此,由于共同朋友的社交圈较小,例如,节点 A 和 B 更有可能被邀请参加节点 C 或 D 可能举办的社交活动。类似的逻辑也可以应用于 Twitch 重叠网络。

10-11

图 10.11 Adamic-Adar 指数的直觉

Adamic-Adar 指数是使用图 10.12 中所示的方程计算的。

10-12

图 10.12 Adamic-Adar 指数公式

如果你不理解图 10.12 中的所有符号,请不要担心。Adamic-Adar 指数定义为节点对共享的共同邻居的逆对数节点度数之和。以下是 Adamic-Adar 指数计算的主要步骤:

  1. 首先找出节点xy的所有共同邻居。

  2. 计算所有共同邻居的节点度数。

  3. 求所有共同邻居节点度数逆对数的总和。

以下 Cypher 语句计算训练集和测试集中节点对的 Adamic-Adar 指数。

列表 10.6 计算训练集和测试集中节点对的 Adamic-Adar 指数

run_query("""
MATCH (s1:Stream)-[r:TEST_TRAIN|NEGATIVE_TEST_TRAIN]->(s2:Stream)
OPTIONAL MATCH (s1)-[:FEATURE_REL]-(neighbor)-[:FEATURE_REL]-(s2)
WITH r, collect(distinct neighbor) AS commonNeighbors             ❶
UNWIND commonNeighbors AS cn
WITH r, count{ (cn)-[:FEATURE_REL]-() } AS neighborDegree         ❷
WITH r, sum(1 / log(neighborDegree)) AS adamicAdar                ❸
SET r.adamicAdar = adamicAdar;                                    ❹
""")

❶ 识别节点 s1 和 s2 的所有共同邻居

❷ 计算每个共同邻居的节点度数,包括传入和传出关系

❸ 计算逆对数度数的总和

❹ 将结果存储为关系属性

10.3.5 共同邻居的聚类系数

你将要计算的最后一个链接预测是共同邻居的聚类系数。聚类系数衡量特定节点的邻居的连通性,其值介于 0 到 1 之间。0 的值表示相邻节点之间没有连接。另一方面,1 的值表示邻居的网络形成一个完全图,其中所有邻居都是连接的。

共同邻居的聚类系数是链接预测的一种变体,其中你只计算特定节点对共同邻居的连通性。研究人员已经证明(Wu 等,2015 年),共同邻居的聚类系数可以提高链接预测模型的准确性。

要计算一对节点之间共同邻居的局部聚类系数,你需要确定共同邻居的数量以及共同邻居之间的链接数量。一旦你有了这些数字,你只需要将邻居之间现有链接的数量除以潜在连接的数量。如果所有邻居都连接在一起,邻居之间的潜在连接数量等于链接数量。以下 Cypher 语句计算共同邻居的聚类系数并将结果存储为关系属性。

列表 10.7 计算训练和测试集中节点对之间共同邻居的聚类系数

run_query("""
MATCH (s1:Stream)-[r:TEST_TRAIN|NEGATIVE_TEST_TRAIN]->(s2:Stream)
OPTIONAL MATCH (s1)-[:FEATURE_REL]-(neighbor)-[:FEATURE_REL]-(s2)
WITH r, collect(distinct neighbor) AS commonNeighbors,
        count(distinct neighbor) AS commonNeighborCount             ❶
OPTIONAL MATCH (x)-[cr:FEATURE_REL]->(y)                            ❷
WHERE x IN commonNeighbors AND y IN commonNeighbors
WITH r, commonNeighborCount, count(cr) AS commonNeighborRels
WITH r, CASE WHEN commonNeighborCount < 2 THEN 0 ELSE               ❸
  toFloat(commonNeighborRels) / (commonNeighborCount *
                 (commonNeighborCount - 1) / 2) END as clusteringCoefficient
SET r.clusteringCoefficient = clusteringCoefficient                 ❹
“””)

❶ 识别并计算一对节点之间共同邻居的数量

❷ 识别共同邻居之间所有现有关系

❸ 通过将现有关系数量除以潜在关系数量来计算聚类系数

❹ 将结果存储为关系属性

你可能已经注意到,在本节的示例中,你在查询时将特征集中的关系视为无向的。列表 10.7 中的 Cypher 语句也不例外。最初,你在识别共同邻居时忽略关系方向。由于关系被视为无向的,潜在连接的数量也比有向网络少 50%。因此,列表 10.7 的第二行最后将潜在关系数量除以 2。

10.4 链接预测分类模型

剩下的唯一任务是训练和评估链接预测模型。链接预测 是一个二元分类问题,其中你预测一个链接是否可能在将来形成。你将基于你为关系训练和测试集计算的特征来训练一个随机森林分类模型以解决链接预测任务。在这里使用随机森林分类模型是因为它对特征缩放和共线性问题相对稳健。然而,你也可以选择其他分类模型,如逻辑回归或支持向量机。

使用以下 Cypher 语句从数据库中检索链接预测特征和输出。

列表 10.8 检索链接预测特征和分类输出

data = run_query("""
MATCH (s1)-[r:TEST_TRAIN|NEGATIVE_TEST_TRAIN]->(s2)
WITH r.networkDistance AS networkDistance,
     r.preferentialAttachment AS preferentialAttachment,
     r.commonNeighbor AS commonNeighbor,
     r.adamicAdar AS adamicAdar,
     r.clusteringCoefficient AS clusteringCoefficient,
     CASE WHEN r:TEST_TRAIN THEN 1 ELSE 0 END as output
RETURN networkDistance, preferentialAttachment, commonNeighbor,
       adamicAdar, clusteringCoefficient, output
""")

列表 10.8 中的 Cypher 语句检索存储在 TEST_TRAINNEGATIVE_TEST_TRAIN 关系上的特征。列表 10.8 的结果中的最后一列是 output 列,用于区分正负分类示例。正例用 TEST_TRAIN 关系类型标记,表示为 1,而负例用 NEGATIVE_TEST_TRAIN 标记,表示为 0。

检查相关特征的分布是建议的,就像任何其他机器学习任务一样。pandas 数据框有一个 describe() 方法,用于计算列中值的分布。

列表 10.9 定义与 Neo4j 的连接

data.describe()

图 10.13 展示了链接预测特征的分布。有趣的是,网络距离特征的范围从 2 到 4;然而,它主要是 2,因为平均网络距离仅为 2.055。此外,由于它的方差较低,它可能不是这个例子中最可预测的特征。优先连接的范围很广,从 0 到近 3,000,000。记住,优先连接是通过乘以一对节点中两个节点的度数来计算的。优先连接为 0 的唯一方式是某些节点没有连接。虽然所有节点在原始网络中都有关系,但在特征集中,由于数据拆分,某些连接可能缺失。有趣的是,聚类系数相对较高,平均来看。

10-13

图 10.13 链接预测特征分布

10.4.1 缺失值

总共有 26,164 个训练和测试样本。然而,图 10.13 也表明 networkDistanceadamicAdar 列中存在一些缺失值。例如,在 networkDistance 特征下只有 26,102 个非空值。网络距离未定义,因为这两个节点不在同一个组件中。因此,它们之间不存在路径。正如之前提到的,网络中的孤立节点可能是导致网络距离值缺失的主要原因。你可以用最大距离值 4 来填充缺失值。记住,节点对之间的网络距离越高,它们之间形成链接的可能性就越小,至少在理论上是这样的。所以,如果一对节点不在同一个组件中,在这个例子中就是网络距离为空,你想要选择一个表示显著网络距离的值来填充缺失值。因此,你可能会决定选择数据集中网络距离的最大值(4)来填充缺失值。

另一列存在缺失值的列是 adamicAdar,这种情况可能发生在一对节点没有共同邻居时。你可以用大约 8 的平均 Adamic-Adar 值来填充 adamicAdar 列的缺失值。

列表 10.10 填充缺失值

data['networkDistance'].fillna(4, inplace=True)
data['adamicAdar'].fillna(8.097444, inplace=True)

10.4.2 训练模型

在完成所有预处理步骤后,你可以继续训练链接预测模型。data 数据框包含关系训练集和测试集。因此,你将首先使用 scikit-learn 库中的 train_test_split 函数来分割测试集和训练集。你将使用 80% 的样本作为训练示例,剩余的 20% 用于评估模型。如果你计划对分类模型进行超参数优化,你也可以生成一个验证集。然而,优化分类模型本身超出了本书的范围,所以你将跳过创建验证集。在数据集分割后,你将训练样本输入到随机森林模型中,该模型将学习预测未来是否存在链接的可能性。

列表 10.11 分割训练集和测试集并训练链接预测模型

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier

X = data.drop("output", axis=1)
y = data["output"].to_list()

X_train, X_test, y_train, y_test = train_test_split(
  X, y, test_size=0.2, random_state=0)
rfc = RandomForestClassifier()
rfc.fit(X_train, y_train)

列表 10.11 中的代码首先定义了特征和目标列。output 列用作目标,而所有其他列都用作模型特征。接下来,你使用 train_test_split 函数进行测试/训练分割。最后,你实例化一个随机森林模型,并基于训练样本进行学习。

10.4.3 评估模型

与所有机器学习任务一样,你应该使用测试集来评估你的链接预测模型的表现。在以下列表中,你将使用内置的 scikit-learn 函数生成一个分类报告。

列表 10.12 生成分类报告

from sklearn.metrics import classification_report

y_pred = rfc.predict(X_test)
print(classification_report(y_test, y_pred))

列表 10.12 中的代码生成了图 10.14 所示的分类报告,可以用来评估模型。

10-14

图 10.14 链接预测模型的分类报告

恭喜!你已经训练了一个准确率为 92% 的链接预测模型。准确率是一个很好的指标,因为负样本和正样本的比例是均衡的。

最后,你可以评估训练好的链接预测模型的特征重要性。以下代码将生成一个按特征重要性降序排列的有序数据框。

列表 10.13 评估特征重要性

def feature_importance(columns, classifier):
    features = list(zip(columns, classifier.feature_importances_))
    sorted_features = sorted(features, key = lambda x: x[1]*-1)

    keys = [value[0] for value in sorted_features]
    values = [value[1] for value in sorted_features]
    return pd.DataFrame(data={'feature': keys, 'value': values})

feature_importance(X.columns, rfc)

表 10.15 显示,网络距离是最不重要的特征,差距很大。这在某种程度上是可以预料的,因为网络距离特征的方差较低。有趣的是,最相关的特征是 Adamic-Adar 指数,其次是共同邻居和优先连接特征。请注意,由于本章开头使用随机数据集分割,你可能会得到略微不同的结果。

10-15

图 10.15 特征重要性

10.5 练习题解答

练习 10.1 的解答如下。

列表 10.14 计算关系数量

run_query("""
MATCH (n)-[:SHARED_AUDIENCE]->()
RETURN count(*) AS result
""")

练习 10.2 的解答如下。

列表 10.15 构建测试集和训练集的正例

# Create test/train rel
# Take the remaining 10%
train_test_size = run_query("""
MATCH (s1)-[:SHARED_AUDIENCE]->(s2)
WHERE NOT EXISTS {(s1)-[:FEATURE_REL]->(s2)}
MERGE (s1)-[r:TEST_TRAIN]->(s2)
RETURN count(r) AS result;
""")
print(train_test_size)

练习 10.3 的解答如下。

列表 10.16 构建测试集和训练集的负例

# Create negative test/train pairs
run_query("""
MATCH (s1:Stream),(s2:Stream)
WHERE NOT EXISTS {(s1)-[:SHARED_AUDIENCE]-(s2)}
      AND s1 < s2
      AND rand() > 0.9
WITH s1,s2
LIMIT 13082
MERGE (s1)-[:NEGATIVE_TEST_TRAIN]->(s2);
""")

练习 10.4 的解决方案如下。

列表 10.17 计算训练集和测试集中节点对的偏好连接特征

run_query("""
MATCH (s1:Stream)-[r:TEST_TRAIN|NEGATIVE_TEST_TRAIN]->(s2)
WITH r, count{ (s1)-[:FEATURE_REL]-() } *
        count{ (s2)-[:FEATURE_REL]-() } AS preferentialAttachment
SET r.preferentialAttachment = preferentialAttachment
""")

练习 10.5 的解决方案如下。

列表 10.18 计算训练集和测试集中节点对的共同邻居特征

run_query("""
MATCH (s1:Stream)-[r:TEST_TRAIN|NEGATIVE_TEST_TRAIN]->(s2)
OPTIONAL MATCH (s1)-[:FEATURE_REL]-(neighbor)-[:FEATURE_REL]-(s2)
WITH r, count(distinct neighbor) AS commonNeighbor
SET r.commonNeighbor = commonNeighbor
""")

摘要

  • 链接预测是预测网络中未来或缺失链接的任务。

  • 链接预测模型常用于推荐系统中。

  • 链接预测特征旨在编码节点对之间的相似性或距离。

  • 链接预测特征可以通过聚合节点属性、评估网络距离或检查局部或全局邻域重叠来构建。

  • 如果你使用归纳节点嵌入在链接预测工作流程中,你无法在训练期间为新的未见节点生成节点嵌入,因此无法预测训练期间未出现的节点的未来链接。

  • 当一个特征包含与输出变量相同或可比较的信息时,就会发生特征泄漏。如果你使用相同的关系来生成网络特征以及训练和评估分类模型,可能会遇到泄漏问题。因此,有必要将数据集分为特征、训练和测试集。如果你计划执行任何超参数优化,也可以引入验证集。

  • 特征集用于计算网络特征,而测试集和训练集提供分类样本以训练和评估模型。如果你计划实现任何超参数优化技术,你也可以引入验证集。

  • 在训练期间使用所有负例会导致相当大的类别不平衡。因此,通常会对负例进行子采样,并使用大约相同数量的正例和负例。

  • 网络距离编码了节点对在网络中的接近程度。理论表明,节点对越接近,它们形成未来链接的可能性就越大。

  • 偏好连接原则很好地捕捉了大多数现实世界网络如何演变。其基本思想是“富者愈富”。因此,具有更多现有链接的节点在未来更有可能形成新的链接。

  • 本地邻域重叠特征可以简单到两个节点共有的邻居数量,也可以高级到 Adamic-Adar 指数,该指数假设一对节点之间共有邻居的度数越小,这对节点未来形成连接的可能性就越大。

11 知识图谱补全

本章涵盖

  • 介绍异构图

  • 解释知识图谱嵌入

  • 描述知识图谱补全工作流程

  • 检查知识图谱补全结果

第十章介绍了链接预测和补全技术。链接预测和补全之间的区别在于前者是一个预测未来链接的工作流程,而后者是处理预测缺失链接。然而,在实践中,链接预测和补全的工作流程非常相似。没有明确提到的是,第十章中使用的链接预测特征没有区分不同的节点或关系类型。例如,共同邻居的数量不会区分不同的关系或节点类型。因此,第十章中使用的链接预测特征最适合单部分或同质图。单部分或同质图由单个节点和关系类型组成。图 11.1 中的可视化展示了一个由单个节点类型Stream和单个关系类型SHARED_AUDIENCE组成的同质图。

11-01

图 11.1 由Stream节点和SHARED_AUDIENCE关系组成的同质图

假设你在一家大型制药公司担任数据科学家。你被分配了一个任务,即预测公司生产的现有药物的新用例。为现有批准药物识别新用例的策略被称为药物再利用。药物再利用的最古老例子是乙酰水杨酸,更广为人知的是阿司匹林。它最初在 1899 年被用作镇痛剂来缓解疼痛。后来,它被重新定位为抗血小板聚集药物。抗血小板药物可以降低血凝块形成的可能性(Vane,1971)。阿司匹林后来再次被重新定位,因为研究表明,每天服用阿司匹林可以帮助预防癌症的发展,尤其是结直肠癌(Rüschoff 等人,1998;Rothwell 等人,2011)。尽管药物再利用具有潜力,但必须记住,它可能是一个漫长的过程,需要多年时间才能通过成本高昂且耗时的临床试验来获得药物的批准,而且这并不像数据科学家预测一个新的连接并立即将产品推向生产那样简单。

作为一名数据科学家,你很可能没有生物医学背景,因此不能根据领域专业知识手动挑选新的潜在用例。你的选择有哪些?你可以将药物和疾病之间的已知联系建模为双部分图。

图 11.2 显示了批准的药物和疾病的双部分网络。这些关系表示药物在治疗条件方面的现有应用。例如,阿司匹林可以用来治疗头痛、川崎病、冠状动脉疾病和高血压。

11-02

图 11.2 现有药物和已知治疗的二分网络

你可以通过首先确定相似药物来创建药物再利用工作流程。药物的相似性通常基于它们的化学结构和它们治疗的疾病重叠(Luo 等,2016 年)。一旦确定了相似药物,你可以使用这些信息来预测或推荐现有药物的新应用。

图 11.3 中所示的药物再利用工作流程有两个步骤:

  1. 识别相似关系

  2. 基于药物相似性推荐新药应用

11-03

图 11.3 基于药物相似性预测新药应用

第一步是确定相似药物。对于这个练习,一个想法可能是,两种药物治疗的常见疾病越多,它们之间的药物相似性就越高。你可以采取几种方法来推断相似关系。你可以使用第七章中描述的 Jaccard 相似系数来计算药物相似性。另一个想法是使用第九章中介绍的 node2vec 等节点嵌入模型来计算节点嵌入,并使用节点嵌入的余弦相似度来比较药物。最后,你也可以借鉴第十章中描述的一些链接预测特征来计算药物相似性。使用任何提到的方法,你都会在药物对之间创建某种形式的得分相似关系。

在第二步中,你可以根据计算出的相似关系推荐新的药物应用。在图 11.3 的例子中,阿司匹林和依普利酮被标记为相似药物。因此,你可以通过检查阿司匹林等药物治疗的疾病来预测依普利酮的潜在应用。在这个例子中,你可能会预测依普利酮可能被用于治疗川崎病和头痛。

注意:记住,链接预测工作流程仅建议评估新应用的优先级,而领域专家随后决定并可能进行临床试验以确定新的药物应用。

描述的药物再利用工作流程是有效的;然而,采用这种方法,你会忽略很多现有的生物医学知识。关于基因、生物过程、解剖学以及其他生物医学信息的大量数据,你可以将其纳入你的图中,并因此纳入药物再利用分析中。

医学研究人员多年来积累了大量知识。你可以从许多官方医学数据库中借用信息来构建生物医学图。例如,图 11.4 中的图架构包含多种类型的节点,从药物到疾病,再到基因和副作用,以及其他许多内容。此外,还存在多种类型的关系。有时,特定类型的节点之间可能存在多种类型的关系。在图 11.4 中,你可以观察到一种药物可以上调或下调一个基因。

11-04

图 11.4 复杂生物医学图的示例架构

复杂生物医学图是异构图的例子,其中存在多种节点和关系类型。在药物再利用工作流程中,你可以使用生物医学图中的所有可用信息来预测新的 TREATS 关系。然而,由于图架构更为复杂,它需要与我们所讨论的不同特征工程方法。如果你倾向于执行类似于第十章中描述的手动特征工程工作流程,你需要找到一种方法来编码各种节点和关系类型。例如,第十章中使用的共同邻居数量并不能区分不同的节点和关系类型。一种疾病可以上调或下调一个基因,而你希望以某种方式将它们区分开来。因此,手动特征工程可能会很繁琐且劳动密集,同时需要领域专业知识。虽然节点嵌入算法(如 node2vec 算法)消除了手动特征工程的需求,但它们并未设计用于区分不同的节点和关系类型。幸运的是,你不是第一个遇到这个问题的人。避免手动特征工程同时拥有能够区分不同节点和关系类型的模型的方法是使用 知识图谱嵌入 模型。与节点嵌入模型不同,知识图谱嵌入模型在嵌入空间中编码节点以及关系。在嵌入空间中编码关系的额外好处是,嵌入模型可以学习区分不同的关系类型。

11.1 知识图谱嵌入模型

正如所述,节点嵌入与知识图谱嵌入模型之间的关键区别在于后者不仅嵌入节点,还嵌入关系。在深入理论之前,你需要熟悉知识图谱嵌入术语。

11.1.1 三元组

知识图谱嵌入模型使用 三元组 来描述图。一个三元组由两个节点组成,称为 头节点(h)和 尾节点(t),以及一个带标签的有向关系(r)。

图 11.5 展示了左侧一个样本图的视觉化和右侧相同图的三角表示。一个三角由两个节点组成,一个头节点(h)和一个尾节点(t),以及一个有向标签关系(r)。头节点是关系的源节点或起始节点,而目标节点或结束节点被标记为尾节点。在图 11.5 的例子中,Ana 被认为是头节点,而巴黎是尾节点。知识图嵌入背后的理念是支持异构图并区分不同类型的关系。因此,三角中的关系标签定义了其类型。图 11.5 中的关系标签是LIVES_IN.

11-05

图 11.5 三角表示

练习 11.1

构造两个三角来定义你的位置。第一个三角应包含关于你居住的城市的信息,而第二个三角应将你的城市与其所属的国家连接起来。选择你认为最合适的标签关系。

注意:三角被定义为区分不同关系类型或标签。然而,没有对节点标签的明确定义。因此,知识图嵌入模型没有明确区分不同的节点类型。

11.1.2 TransE

TransE(Bordes 等人,2013 年)是最早且最直观的知识图嵌入模型之一。TransE 方法的目标是为图中的所有节点和关系计算低维向量表示,也称为嵌入。TransE 方法常用于展示知识图嵌入,因为它易于说明且相对容易计算。

图 11.6 展示了在嵌入空间中编码节点和关系的概念。TransE 方法背后的关键思想是在嵌入空间中编码节点和关系,使得头节点的嵌入加上关系的嵌入应该接近尾节点的嵌入。在图 11.6 中,你可以观察到头节点的嵌入加上关系的嵌入恰好等于尾节点的嵌入。

11-06

图 11.6 TransE 编码直觉

TransE 方法试图生成嵌入,使得对于训练集中的每个三角,它最小化头节点和关系嵌入与尾节点嵌入之间的距离。这种优化分数可以写成h + r t,如图 11.7 所示。另一方面,如果头节点和尾节点之间不存在关系,那么头节点和关系嵌入的加和不应接近尾节点(h + r != t)。你可以在原始文章(Bordes 等人,2013 年)中了解更多关于数学实现的细节。

11-07

图 11.7 TransE 优化指标

11.1.3 TransE 局限性

虽然 TransE 的实现简单直观,但它有一些缺点。你将使用三类关系来评估 TransE 方法。

关系的第一类是 对称 关系。三元数据结构不允许无向关系;然而,一类关系可以被视为无向的。在知识图谱嵌入模型领域,无向关系被称为 对称 关系。一些对称关系的例子包括

Tomaž, SIBLING, Blaž
Blaž, SIBLING, Tomaž

如果 Tomaž 是 Blaž 的兄弟姐妹,那么 Blaž 也是 Tomaž 的兄弟姐妹。这个简单的事实无法回避。问题是,TransE 是否可以编码对称关系?

TransE 方法为每种关系类型生成一个向量表示。因此,图 11.8 中的 SIBLING 向量表示在两个实例中方向相同。问题是,相同类型的关系的向量表示不能指向相反方向。一个 SIBLING 向量从头节点指向尾节点。然而,第二个 SIBLING 向量从第二个节点开始,方向与第一个 SIBLING 向量相同。因此,第二个 SIBLING 向量不能也不能指向第一个节点。因此,从理论上看,TransE 不支持对称关系。

11-08

图 11.8 使用 TransE 编码对称关系

你将评估的第二类关系是 组合 关系。组合关系的一个例子如下:

John, MOTHER, Alicia
Alicia, SPOUSE, Michael
John, FATHER, Michael

组合关系可以通过组合两个或多个关系来构建。在图 11.9 的例子中,FATHER 关系可以通过添加 MOTHERSPOUSE 关系来组合。你可以观察到,可以调整关系向量以适应这种图模式。因此,TransE 方法支持组合关系。

11-09

图 11.9 使用 TransE 编码组合关系

最后一类关系是 1 到 N 的关系。本质上,这种情况发生在节点与多个其他节点有相同关系时。1 到 N 关系的例子包括

Surya, FRIEND, Rajiv
Surya, FRIEND, Jane

TransE 能够编码 Surya 与 Jane 和 Rajiv 都是朋友的关系的唯一方式是 Jane 和 Rajiv 的向量表示相等。Jane 和 Rajiv 有相同的向量表示在逻辑上是不合理的,因为它们在图中是不同的实体,因此应该有不同的嵌入。另一个解决方案将是 FRIEND 关系向量有不同的方向,如图 11.10 所示。然而,TransE 方法仅实现给定关系类型的单个向量表示。因此,TransE 方法不支持 1 到 N 的关系。

11-10

图 11.10 使用 TransE 编码 1 到 N 的关系

11.2 知识图谱补全

现在你已经掌握了知识图谱嵌入的理论背景,你可以继续进行预测现有药物新应用的任务。想象一下,你在一家大型制药公司工作,该公司生产阿司匹林。阿司匹林是一种大规模生产的药物,因此,一个新的应用可能会带来大量的收入。这个想法是利用现有的生物医学知识来预测新的应用。你已经确定,最佳的行动方案是应用知识图谱补全技术来寻找新的潜在药物应用,也称为药物再利用。知识图谱补全可以被视为多类链接预测,其中你预测新的链接及其类型。你将训练一个知识图谱嵌入模型来编码生物医学图中的节点和关系,然后使用这些嵌入来识别阿司匹林的新潜在应用。

药物再利用工作流程如图 11.11 所示。整个流程的基础是一个丰富而复杂的生物医学知识图谱,其中包含现有的药物;它们的疗法;以及其他生物医学实体,如基因和通路。由于你为一家大型公司工作,公司里的其他优秀人士已经绘制并构建了所需的生物医学图。接下来,你需要将生物医学图输入到知识图谱嵌入模型中。由于你不需要执行任何图转换或操作,你可以完全跳过使用图数据库。虽然多个 Python 库都提供了知识图谱嵌入模型,但我更喜欢 PyKEEN(Ali 等人,2021),因为它简单易用。此外,PyKEEN 实现了超过 40 种不同的知识图谱嵌入模型,并提供了开箱即用的超参数优化支持。最后,你将使用 PyKEEN 内置的方法来预测阿司匹林的新应用。你需要安装 PyKEEN 和 pandas 库,如列表 11.11 所示,以跟随代码示例。

11-11

图 11.11 药物再利用工作流程

列表 11.1 安装 PyKEEN

pip install pykeen==1.9.0 pandas

所有代码都作为 Jupyter 笔记本提供(mng.bz/zXNQ)。

11.2.1 Hetionet

你的同事已经准备了一个 Hetionet 数据集的子集(Himmelstein 等人,2017)供使用。原始 Hetionet 数据集包含 47,031 个节点(11 种类型)和 2,250,197 个关系(24 种类型)。

Hetionet 数据集的图架构在图 11.12 中展示。图中包含各种实体,如基因、通路、化合物和疾病。此外,图中还有 24 种不同类型的关系。解释所有医疗实体及其关系的医学术语可能需要一本书的篇幅。对于药物重新定位工作流程来说,最重要的关系是 TREATS 关系,它从 Compound 节点开始,以 Disease 节点结束。本质上,TREATS 关系封装了现有的批准药物治疗方法。你将使用知识图谱补全技术来预测从阿司匹林或乙酰水杨酸节点起源的新 TREATS 关系。

11-12

图 11.12 Hetionet 架构(来源:Himmelstein 等人。根据 CC BY 4.0 许可)

在本例中,你将使用 Hetionet 数据集的一个子集。该子集的架构如图 11.13 所示。

11-13

图 11.13 将用于药物重新定位工作流程的 Hetionet 子集的图架构

图 11.13 展示了你在药物重新定位工作流程中将使用的 Hetionet 数据集的一个子集。给定的子集包含 22,634 个节点(3 种类型)和 561,716 条关系(12 种类型)。图中包含了在 TREATS 关系下可以找到的现有批准的药物治疗方法,以及一些关于化合物和疾病如何与基因相互作用的附加信息。基因也可以与其他基因相互作用。Hetionet 数据集的子集可在 GitHub 上找到( mng.bz/ddww),其结构如表 11.1 所示。

表 11.1 Hetionet 关系 CSV 文件的结构

source_name source_label target_name target_label type
SERPINF2 基因 KLK13 基因 相互作用
SERPINF2 基因 SSR1 基因 相互作用
SERPINF2 基因 TGM2 基因 相互作用
SERPINF2 基因 UBC 基因 相互作用
SERPINF2 基因 SERPINB12 基因 相互作用

你将使用 pandas 库从 GitHub 加载 CSV 文件,如列表 11.13 所示,首先导入 pandas 库。然后,它使用内置的 read_csv 方法从 GitHub 加载 Hetionet 数据集。

列表 11.2 将 Hetionet 子集作为 pandas 数据框加载

import pandas as pd

data = pd.read_csv(
    "https://bit.ly/3X2qp1r"
)

11.2.2 数据集拆分

与所有机器学习工作流程一样,你需要执行测试-训练数据集拆分。你可以将图结构作为三元组列表输入 PyKEEN,如下所示。记住,三元组数据对象由头、标签和尾元素组成。

列表 11.3 向 PyKEEN 输入三元组

from pykeen.triples import TriplesFactory

tf = TriplesFactory.from_labeled_triples(
    data[["source_name", "type", "target_name"]].values,
)

TriplesFactory是 PyKEEN 类,用于存储用于训练和评估模型的三元组。列表 11.3 中的代码使用from_labeled_triples方法从 pandas dataframe 输入三元组列表。data dataframe 包含有关节点标签的附加信息,您需要过滤掉这些信息。因此,列表 11.3 中的代码指定使用source_nametypetarget_name列作为三元组。现在,三元组已加载到 PyKEEN 中,您可以使用以下代码进行数据集分割。

列表 11.4 将数据集分割为训练集、测试集和验证集

training, testing, validation = tf.split([0.8, 0.1, 0.1], random_state=0)

数据集的分割是通过split方法进行的,如列表 11.4 所示。虽然主要目标是预测新的treats关系,但包括所有数据分割的数据集包含所有可用的关系,例如interactsupregulates等,以便尽可能地为模型提供相关信息。该方法接受一个包含三个值的数组作为参数,该参数定义了训练集、测试集和验证集的比例。第一个值定义了训练集的比例,第二个值表示测试集的比例,最后一个数字指定了验证集的大小。第三个值可以省略,因为它可以从前两个值中计算得出。

11.2.3 训练 PairRE 模型

虽然 TransE 模型对于知识图嵌入模型的入门非常出色,但它有其局限性。例如,一种药物可以用来治疗多种疾病。然而,如 TransE 介绍中提到的,TransE 方法无法编码 1-to-N 关系,这使得它不适合生物医学知识图。因此,您将使用一个更晚且更好的模型,称为PairRE(Chao et al., 2020)。PairRE 能够编码对称性、组合性和 1-to-N 关系,这使得它非常适合用于生物医学知识图。如果您对数学实现的细节感兴趣,请阅读arxiv.org/abs/2011.03798上的文章。以下代码基于您提供的 Hetionet 数据集的子集训练 PairRE 模型。

列表 11.5 训练 PairRE 模型

from pykeen.pipeline import pipeline

result = pipeline(
    training=training,
    testing=testing,
    validation=validation,
    model="PairRE",          ❶
    stopper="early",         ❷
    epochs=100,
   random_seed=0,            ❸
)

❶ 指定 PairRE 模型

❷ 定义停止策略

❸ 设置随机种子以确保可重复性。

PairRE 模型可以通过单个函数进行训练,如列表 11.5 所示。训练、测试和验证集通过单独的参数加载。您可以使用model参数选择模型;您可以选择超过 40 种模型。请检查文档(mng.bz/rj2y)以获取可用模型的完整列表。stopper参数的early值默认情况下每 10 个 epoch 评估模型一次。使用带有early值的stopper选项,如果模型精度在额外的 epoch 中没有提高,训练管道将停止训练。最后,random_seed参数用于确保结果的可重复性。完整的可用管道参数列表可在官方文档(mng.bz/0K86)中找到。

注意:训练可以在 CPU 或 GPU 设备上执行。然而,如果您有可用的 GPU 设备,训练将更快。如果您没有本地 GPU 可用,您始终可以尝试使用免费云环境,例如 Google Colab。

11.2.4 药物应用预测

在训练好 PairRE 模型后,您可以预测乙酰水杨酸(即阿司匹林)的新应用。PyKEEN 库提供了一个predict_target函数,允许您输入三元组的头和关系,并输出尾节点的预测。在您的示例中,您将acetylsalicylic acid作为头,将treats作为关系元素输入,如下所示。最可能的tail节点输出以 pandas 数据框结构给出。

列表 11.6 预测乙酰水杨酸的新用例

from pykeen.predict import predict_target

pred = predict_target(
    result.model,
    head="Acetylsalicylic acid",
    relation="treats",
    triples_factory=result.training,
).df
pred_filtered = pred.filter_triples(result.training)
print(pred_filtered.head())

表 11.2 显示了预测出的结果用例。

表 11.2 乙酰水杨酸的前五项预测

tail_id tail_label score
19,912 系统性红斑狼疮 −9.228726
19,827 乳腺癌 −9.510363
19,913 系统性硬化症 −9.543921
19,887 胰腺癌 −9.711681
19,919 1 型糖尿病 −9.731101

分数值接近零的预测更可能。您的模型预测阿司匹林可能被用于治疗系统性红斑狼疮、系统性硬化症和某些类型的癌症。这些预测可用于推荐特定药物用例的临床试验。临床试验必须精心规划,因为它们耗时很长且成本极高(Schlander 等,2021 年)。因此,尽可能产生准确的建议至关重要,因为临床试验的成本可能超过十亿美元。

练习 11.2

使用predict_target函数预测咖啡因的潜在新应用。

11.2.5 解释预测

预测完成后,您可以在医学文献中搜索支持或反驳的研究。再次强调,在过程中包括领域专家的重要性不容忽视,因为他们对解释结果和相关的医学文献都起着关键作用。例如,如果您搜索阿司匹林和胰腺癌的组合,您可能会找到一些可能验证您预测的文章(Sun 等人,2019 年)。鉴于 Hetionet 文章是在 2017 年发表的,它可能不包含 2019 年的新医学信息。Hetionet 是一个逐渐老化的资源,仅限于不到 200 种疾病。在实践中,制药和其他公司使用各种大规模部署的文本挖掘系统从各种医学研究文章和试验中提取知识,以保持其生物医学图更新至所有最新可用信息(Bachman 等人,2022 年)。

您的预测有支持证据表明,使用知识图嵌入模型进行知识图补全的方法可以产生很好的结果。假设您没有找到支持您预测的文献。在这种情况下,您可以将现有的生物医学连接展示给领域专家,让他们决定这些连接是否有价值。尽管您不需要图数据库进行药物再利用工作流程,但它仍然可以很好地解释预测。幸运的是,您在大型制药公司的同事已经为您解决了这个问题,或者更确切地说,Hetionet 的作者已经通过只读 Neo4j 浏览器界面使其可用。Hetionet 浏览器界面可在 neo4j.het.io/browser/ 找到。

以下 Cypher 查询将可视化乙酰水杨酸与胰腺癌之间距离不超过三个跳的第一条 25 条路径。

列表 11.7 预测乙酰水杨酸的新潜在用途

MATCH (c:Compound {name:"Acetylsalicylic acid"}),
      (d:Disease {name:"pancreatic cancer"})
MATCH p=(c)-[* ..3]-(d)
RETURN p LIMIT 25

列表 11.7 中的 Cypher 语句生成了图 11.14 中的可视化。在图的左侧,您将看到 乙酰水杨酸,而 前列腺癌 在右侧。乙酰水杨酸可用于缓解骨关节炎和痛风。有趣的是,骨关节炎与类似前列腺癌的基因相关。无论如何,领域专家可以评估现有连接并形成自己的观点。乙酰水杨酸与胰腺癌之间有 1,716 条不同的路径,长度不超过三个跳。因此,很难在单个图像中可视化所有这些路径,领域专家可以根据节点或关系类型优先考虑连接。

11-14

图 11.14 乙酰水杨酸与胰腺癌之间的现有连接

练习 11.3

可视化水杨酸和自闭症之间最多三跳的 25 条路径。使用现有的 Neo4j 版本的 Hetionet 图,该图可通过 Neo4j 浏览器在neo4j.het.io/browser/访问。

11.3 练习的解决方案

练习 11.1 的一个可能解决方案如下。

Tomaz, LIVES_IN, Ljubljana
Ljubljana, PART_OF, Slovenia

练习 11.2 的解决方案如下。

列表 11.8 预测咖啡因的新用途

pred_df = get_prediction_df(
    result.model,
    head_label="Caffeine",
    relation_label="treats",
    remove_known=True,
    triples_factory=result.training,
)
pred_df.head()

练习 11.3 的解决方案如下。

列表 11.9 可视化水杨酸和自闭症之间最多三跳的 25 条路径

MATCH (c:Compound {name:"Acetylsalicylic acid"}),
      (d:Disease {name:"autistic disorder"})
MATCH p=(c)-[* ..3]-(d)
RETURN p LIMIT 25

摘要

  • 异构图或多部分图由多种节点和关系类型组成。两个实体类型之间也可能存在多种关系类型。

  • 三元数据对象用于表示存在多种关系类型的有向图。

  • 三元数据对象由头元素、关系元素和尾元素组成。

  • 知识图谱嵌入模型在嵌入空间中编码节点和关系,这与仅编码节点的节点嵌入模型不同。

  • 知识图谱嵌入模型试图以这种方式计算嵌入,即对于每个现有的三元组,头节点和关系嵌入的总和接近尾节点的嵌入。

  • 如果知识图谱嵌入模型能够编码对称性、逆关系、复合关系和 1 对 N 关系,则从理论角度评估知识图谱嵌入模型。

  • PairRE 模型可以编码所有四种关系类别(对称性、逆关系、复合关系和 1 对 N 关系)。

  • 可以将知识图谱补全视为一个多类链接预测问题,其中你正在预测新的链接及其类型。

  • 在药物再利用的工作流程中,预测必须由领域专家评估,然后通过临床试验才能获得批准。知识图谱补全仅用于优先考虑最有可能的候选者。

12 使用自然语言处理技术构建图

本章涵盖

  • 信息提取管道

  • 共指消解

  • 命名实体识别和链接

  • 关系提取

  • 开发一个信息提取管道

互联网上可用的基于文本的信息量令人震惊。很难想象每天发布的社交媒体帖子、博客和新闻文章的数量。然而,尽管信息丰富,其中很大一部分仍然是未结构化的,难以从中提取有价值的见解。这就是自然语言处理(NLP)发挥作用的地方。NLP 是一个快速发展的领域,近年来受到了越来越多的关注,特别是在变压器模型(Vaswani,2017)以及最近引入的 GPT-3(Brown 等人,2020)和 GPT-4 模型(OpenAI,2023)之后。NLP 的一个特别重要的领域是信息提取领域,该领域专注于从非结构化文本中提取结构化信息。

在图 12.1 中提供的示例中,文本描述了公司的创立,并包含诸如公司名称、创始人姓名和创立日期等信息。信息提取过程将涉及从文本中识别和提取这些信息。信息提取管道的输出将是一组三元组。每个三元组代表一条信息,由头、关系和尾对象组成。三元组对象定义与上一章相同,其中你实现了知识图谱补全管道。在图 12.1 的示例中,提取的信息可能用以下三元组表示:

  • 史蒂夫·乔布斯创立了苹果

  • 史蒂夫·沃兹尼亚克创立了苹果

  • 苹果成立于1976 年 4 月 1 日

12-01

图 12.1 从文本中提取结构化信息并用于构建图

基于给定的三元组创建一个图是一个简单的过程。每个三元组都可以用来构建一个图,其中三元组的头是起始节点,关系代表关系类型,尾是结束节点。对于一些特殊情况,你可能希望将三元组作为节点属性存储在标记属性图模型中。例如,你可以将苹果的创立日期作为节点属性存储。通过连接这些节点和关系,你可以创建一个表示提取信息的图。这个图可以用于各种目的,例如数据分析、知识表示和信息检索。

假设你正在一家风险投资公司担任数据科学家。风险投资公司需要了解该领域的最新发展。鉴于每天产生的新闻量巨大,且涉及多种语言,手动阅读每一篇文章几乎是不可能的;因此,你被分配去开发一个系统,该系统能够自动阅读新闻并提取有关新成立公司、合并和收购的信息。在这种情况下,你提出了开发一个信息提取管道来实现这一目标的想法。这个管道将从文本中提取相关数据并将其存储为图。

这种方法将使你能够轻松地识别新成立、合并或被收购的公司的模式。此外,你将能够轻松地获取这些事件的相关背景信息。例如,你可以检索谁创立了这些公司,它们是在何时创立的,创始人是否知名,等等。处理足够的数据后,这种方法还将允许探索图中各种实体的历史或发展进程。因此,当图中出现特定类型的新关系时,你可以创建一个通知系统。此外,该图还将允许你探索参与各种企业事件的实体的背景和历史。这将使风险投资公司能够了解该领域的最新发展,并做出明智的投资决策。你提出了图 12.2 所示的信息提取管道的设计。

12-02

图 12.2 信息提取管道设计

图 12.2 中提出的拟议信息提取管道是一个多步骤过程,用于从非结构化文本中提取结构化信息。该管道由几个自然语言处理(NLP)技术组成,例如核心 ference 解析命名实体识别(NER)和关系提取(RE):

  • 核心 ference 解析模型识别所有指向同一实体的引用。在实践中,核心 ference 解析通常表示为将代词替换为引用实体的任务,尽管这可能比这更复杂。例如,在文本He lives in Bahamas中,代词He将被替换为引用实体Jon

  • 接下来,该管道使用命名实体识别(NER)模型在文本中识别实体和概念。在图 12.2 中,该模型识别了JonW3CBahamas作为实体。NER 模型还可以训练以检测实体类型,如Person(人物)、Organization(组织)和Location(地点)。

  • 管道中的最后一个 NLP 技术是关系提取。它用于提取识别出的实体之间的各种连接。在这个例子中,该管道将提取以下两个三元组:

    • Jon, WORKS_AT, W3C

    • Jon, RESIDES, Bahamas

  • 使用图数据库存储 NLP 过程中提取的实体之间的关系,可以轻松地存储、查询和可视化信息。此外,使用图数据库允许进行更复杂的查询,例如找到实体之间的模式和联系,这在使用传统数据库结构时可能很难执行。由于你有一些图机器学习的经验,你也可以在节点分类或链接预测工作流程中使用该图。

12.1 核实指代

管道中的第一步是一个核实指代模型。核实指代是指识别文本中所有指向同一实体的表达式的任务。

在图 12.3 中的示例的第一句话中,Googlethey 指向同一实体。第二句话以对 公司 的引用开始。有趣的是,公司 的引用有点含糊不清,因为没有其他信息时,它可能指的是 GoogleYouTube。这是核实指代模型必须解决的更具挑战性的问题的一个例子。然而,如果你知道 Eric Schmidt 是 Google 的首席执行官,那么将 公司 的引用解析为 Google 就会相对容易。有时,你处理的是复数代词或引用,例如在 两家公司 的情况下。因此,核实指代模型的理想输出将是同时指向 GoogleYouTube 的引用。

12-03

图 12.3 核实指代

核实指代可以采用两种主要方法:

  • 基于规则

  • 神经网络

基于规则的核实指代(Haghighi & Klein, 2009; Raghunathan et al., 2010)涉及使用一组预定义的规则和启发式方法来识别和链接文本中的核实指代表达式。这些规则可以基于句法、语义和上下文信息,例如表达式的性别或数的一致性,文本中表达式的距离,或某些关键词的存在。然而,当应用于实际应用时,基于规则的系统存在一些局限性,因为几乎不可能为每种可能的变体定义规则。另一方面,神经核实指代(Clark & Manning, 2016; Lee et al., 2017)涉及训练一个神经网络来识别和链接文本中的核实指代表达式。神经网络通过识别数据中的模式和关系来学习识别核实指代表达式,而不是依赖于预定义的规则。

核实指代是大多数自然语言处理(NLP)管道的一个关键组件。它被用作各种 NLP 工作流程的预处理步骤,从信息提取到文本摘要和问答任务(Singh, 2018)。

12.2 命名实体识别

命名实体识别(NER)是一项任务,涉及将文本中的命名实体识别和分类到预定义的类别中。命名实体和类别在很大程度上取决于你正在处理的领域。例如,在一家风险投资公司,你可能想在文本中识别人、组织和地点。然而,如果你在一家生物医学公司,重点将更多地放在基因、蛋白质、疾病、药物等上。在大多数情况下,NER 模型都是针对特定领域进行微调的。在提出的信息提取管道中,你希望识别人、组织、地点和日期。

在图 12.4 的示例中,NER 模型将实体 GoogleYouTube 识别为组织,将 Eric Schmidt 识别为个人。提取的命名实体使用 displaCy 进行可视化(spacy.io/universe/project/displacy)。

12-04

图 12.4 命名实体识别

12.2.1 实体链接

通常,区分文本中提到的实体可能具有挑战性。例如,只有一个名为 Google 的组织,但可能有多个名为 Eric Schmidt 的人。为了解决区分命名实体的问题,已经引入了如实体链接等技术。实体链接是将命名实体与其在知识库中的对应 ID 相连接的任务。目标知识库取决于用例领域。例如,实体链接在生物医学领域很受欢迎,它用于将文本中识别的基因、药物和其他实体连接到各种生物医学知识库。另一方面,为了区分人和组织的一般目的,像 WikipediaDBpedia 这样的知识库最常被使用。

图 12.5 展示了将实体链接到 Wikipedia 的一个示例。正如你所见,Wikipedia 网址被用作实体的唯一标识符。有趣的是,即使在 Wikipedia 上,也有多个关于 Eric Schmidt 的条目(en.wikipedia.org/wiki/Eric_Schmidt_(disambiguation))。当存在具有相同名称的多个条目时,选择正确的实体是一个具有挑战性的任务。实体的缩写和别名是在执行实体链接时面临的其他挑战之一。

12-05

图 12.5 命名实体链接

最近提出的许多实体链接模型都是基于深度学习方法。一些模型被称为所谓的端到端实体链接模型,它们在单一步骤中执行命名实体识别和链接(Brank 等人,2017 年)。其他模型仅执行实体链接,并需要提供与文本一起识别的命名实体(Barba 等人,2022 年)。

12.3 关系抽取

如其名所示,关系提取模型从文本中提取实体之间的关系。为风险投资公司提出的信息提取管道应专注于提取公司和人员之间的关系。关系提取模型最频繁的输出是一个三元组列表。你已经在上一章中使用了三元组。如其名所示,三元组包含三个元素,指定起始节点、结束节点和关系类型。

从文本中提取关系有几种方法。你可以采用基于规则的方法,通过使用一组预定义的启发式规则来提取关系。提取关系的基于规则的一个例子是定义基于词性标注和依存句法分析的规则。

图 12.6 可视化了词性标注和依存句法分析的结果。例如,你可以观察到每个单词都是根据其在英语中的功能进行标注的。这些标签表示该词是名词、形容词、动词等。另一方面,依存句法分析计算单词之间的依存关系。例如,“acquiring”是一个动词。该动词指的是宾语“YouTube”,而主语指向“they”。在这种情况下,“they”指的是“Google”。因此,可以定义一个简单的规则,该规则仅取动词的主语和宾语,并基于此构建三元组。然而,在实践中,规则可以变得更加复杂。我发现基于规则的方法在生物医学(Rindflesch & Fiszman, 2003; Rindflesch et al., 2011)等领域被使用。

12-06

图 12.6 词性标注和依存标注

你还可以采取另一种方法,即基于训练数据训练一个深度学习模型来识别和提取关系。基于神经网络方法提取关系的研究有很多,因为它具有出色的提取关系和实现卓越性能的能力。有模型被训练来检测单个句子中的关系(Zhou et al., 2016; Han et al., 2019),以及文档级关系提取(Yao et al., 2019)。最终,你希望得到的模型能够从图 12.5 的示例中提取以下关系。

列表 12.1 从图 12.5 的示例中提取的三元组

Google, ACQUIRED, YouTube
Eric Schmidt, CEO, Google

12.4 信息提取管道的实现

现在你已经对信息提取工作流程有了理论上的了解,你将开发一个管道来识别组织和人员以及他们之间的相关关系。你将使用现有的 NLP 模型进行初始管道实现,因此你不需要担心训练任何定制的 NLP 模型。该管道将使用以下文本进行评估。

列表 12.2 用于评估信息提取管道的文本

Apple Inc was founded on April 1, 1976, by Steve Jobs, Steve Wozniak, and Ronald Wayne as a partnership. They started the company in California. The company's first product was the Apple I, a computer designed and hand-built entirely by Wozniak. To finance the company's creation, Jobs sold his Volkswagen Bus, and Wozniak sold his HP-65 calculator. Wozniak debuted the first prototype Apple I at the Homebrew Computer Club in July 1976\. The Apple I was sold as a motherboard with CPU, RAM, and basic textual-video chips—a base kit concept which would not yet be marketed as a complete personal computer. It went on sale soon after debut for US$666.66 (equivalent to $3,175 in 2021). Wozniak later said he was unaware of the coincidental mark of the beast in the number 666, and that he came up with the price because he liked "repeating digits". Later in 2013 Apple acquired AlgoTrim. Then in 2015 the company also bought Semetric, which is in music analytics category.

列表 12.2 中的文本是从维基百科 (en.wikipedia.org/wiki/Apple_Inc.) 复制的,并包含有关苹果公司、其创始人及其首款产品的信息。我在结尾添加了两句话,提到了各种收购,以便您可以评估模型在处理公司收购信息方面的表现。此文本将用于评估核心词解析模型以及关系提取过程。所提出的信息提取管道的实现可在 GitHub 上的 Jupyter notebook 中找到 (mng.bz/KeJ0)。

12.4.1 SpaCy

您将使用 SpaCy 来开发信息提取管道。SpaCy 是一个免费的开源 Python 库,用于 NLP 工作流程。SpaCy 的优点是它提供了一个面向初学者的界面,可以执行各种 NLP 任务,并且也可以在生产环境中使用。

以下 Python 代码安装所需的 Python 库。

列表 12.3 安装所需的 Python 库

pip install spacy==3.4.4 coreferee==1.3.1 transformers==4.25.1

列表 12.3 中的代码安装了 SpaCy 以及 corefereetransformers 库。coreferee 库 (github.com/explosion/coreferee) 是一个 SpaCy 插件,它提供了一个核心词解析模型。列表 12.3 中的代码安装了 transformers 库,该库将用于关系提取。接下来,您需要使用以下 Python 代码安装预训练模型 en_core_web_lgcoreferee

列表 12.4 下载 NLP 模型

python -m spacy download en_core_web_lg
python -m coreferee install en

12.4.2 核心词解析

如前所述,您将使用 coreferee 插件来执行核心词解析。您可以使用以下代码在 SpaCy 中加载核心词解析模型。

列表 12.5 加载核心词解析模型

import spacy, coreferee

coref_nlp = spacy.load('en_core_web_lg')
coref_nlp.add_pipe('coreferee')

coreferee 模型不提供直接解析核心词文本的方法。因此,您需要定义一个函数,使用核心词解析方法来识别引用,然后在文本中将引用替换为被引用实体。

列表 12.6 使用核心词解析模型解析文本引用

def coref_text(text):
    coref_doc = coref_nlp(text)
    resolved_text = ""

    for token in coref_doc:
        repres = coref_doc._.coref_chains.resolve(token)
        if repres:
            resolved_text += " " + " and ".join(
                [
                    t.text
                    if t.ent_type_ == ""
                    else [e.text for e in coref_doc.ents if t in e][0]
                    for t in repres
                ]
            )
        else:
            resolved_text += " " + token.text

    return resolved_text

ctext = coref_text(text)
print(ctext)

列表 12.6 中的 coref_text 函数解析实体核心词并在文本中相应地替换它们。不深入细节,代码检查解析的标记 repres 是否是命名实体的部分,如果是,则用命名实体的文本替换引用,而不仅仅是标记的文本。

列表 12.6 中的代码生成了以下输出。

列表 12.7 核心词解析文本

Apple Inc was founded on April 1 , 1976 , by Steve Jobs , Steve Wozniak , and Ronald Wayne as a partnership .Steve Jobs and Steve Wozniak and Ronald Wayne started the Apple Inc in California . The Apple Inc 's first product was the Apple I , a computer designed and hand - built entirely by Steve Wozniak . To finance the Apple Inc 's creation , Jobs sold Ronald Wayne Volkswagen Bus , and Steve Wozniak sold Ronald Wayne HP-65 calculator . Steve Wozniak debuted the first prototype Apple I at the Homebrew Computer Club in July 1976 . The Apple I was sold as a motherboard with CPU , RAM , and basic textual - video chips—a base kit concept which would not yet be marketed as a complete personal computer . Apple went on sale soon after debut for US$ 666.66 ( equivalent to $ 3,175 in 2021 ) . Wozniak later said Wozniak was unaware of the coincidental mark of the beast in the number 666 , and that Wozniak came up with the price because Wozniak liked " repeating digits " . Later in 2013 Apple Inc acquired AlgoTrim . Then in 2015 the AlgoTrim also bought Semetric , which is in music  analytics category .

从列表 12.7 的输出初看,核心指代模型做得相当不错。例如,你可能注意到第二行中的 They 被替换成了 Steve Jobs, Steve Wozniak 和 Ronald Wayne,而 company 被替换成了 Apple Inc。然而,该模型并不完美。在第四行,模型将两个 his 的实例都替换成了 Ronald Wayne。结果显然是错误的,因为 Jobs 卖掉了他的大众巴士,而不是 Ronald Wayne 的。此外,你可能会注意到,解析后的文本并没有考虑正确的所有格名词和冠词,因为根据核心指代模型输出生成语法正确的文本本身就是一个大问题。同样,列表 12.7 的最后一行关于哪家公司购买了 Semetric 的描述有些含糊不清。模型可以在 Apple Inc. 和 AlgoTrim 之间选择。虽然你可能知道 Apple Inc. 收购了 Semetric,但核心指代模型选择了 AlgoTrim。

12.4.3 端到端关系抽取

你将在管道中的关系抽取步骤中使用 REBEL 模型(Huguet Cabot & Navigli, 2021)。REBEL 模型是一个端到端的关系抽取模型,这意味着它在一个模型中同时检测实体和关系。该模型在各种数据集上表现出最先进的性能,如其在 GitHub 仓库中所示(github.com/Babelscape/rebel)。

注意:大型语言模型,如 OpenAI 的 GPT-4(2023),在自然语言处理领域带来了颠覆性的进步。它们理解类似人类文本的能力为信息提取领域提供了显著的机会。通过在多样化的互联网文本上训练的模型,它们可以筛选大量数据,理解上下文,并提取相关细节,使信息提取管道更加易于访问。它们的泛化能力使它们能够应用于各种领域,从学术研究到商业分析,从非结构化数据中提取有价值的见解(Li et al., 2023; Wei et al., 2023)。

该模型可以直接使用 transformers 库或作为 SpaCy 组件使用。你将使用 SpaCy 变体。与可以使用 pip 安装的 coreferee 模型不同,你必须从仓库中复制 SpaCy 组件定义(mng.bz/VR8G)。SpaCy 组件定义太长,无法包含在本书中;然而,你可以复制代码而不做任何更改。SpaCy 组件定义也包含在配套的 Jupyter 笔记本中。

接下来,你使用以下代码在 SpaCy 中加载 REBEL 模型。

列表 12.8 加载 REBEL 模型

nlp = spacy.load("en_core_web_lg")

nlp.add_pipe(
    "rebel",
    after="senter",
    config={
        "device": -1,  # Number of the GPU, -1 if want to use CPU
        "model_name": "Babelscape/rebel-large",
    },
)

一旦模型加载完成,提取关系的代码就非常简单了。

列表 12.9 从文本中提取关系

doc = nlp(ctext)
for value, rel_dict in doc._.rel.items():
    print(rel_dict)

列表 12.9 中的 Python 代码处理ctext,REBEL 组件的结果可以通过doc._.rel.items()方法检索。列表 12.9 中的代码产生以下输出。

列表 12.10 关系提取步骤的结果

{'relation': 'founded by', 'head_span': Apple Inc, 'tail_span': Steve Jobs}
{'relation': 'founded by', 'head_span': Apple Inc,
➥ 'tail_span': Steve Wozniak}
{'relation': 'founded by', 'head_span': Apple Inc,
➥ 'tail_span': Ronald Wayne}
{'relation': 'employer', 'head_span': Steve Jobs, 'tail_span': Apple Inc}
{'relation': 'employer', 'head_span': Steve Wozniak, 'tail_span': Apple Inc}
{'relation': 'employer', 'head_span': Ronald Wayne, 'tail_span': Apple Inc}
{'relation': 'manufacturer', 'head_span': Apple Inc, 'tail_span': Apple Inc}
{'relation': 'member of', 'head_span': Steve Wozniak,
➥ 'tail_span': Homebrew Computer Club}
{'relation': 'founded by', 'head_span': Homebrew Computer Club,
➥ 'tail_span': Steve Wozniak}
{'relation': 'has part', 'head_span': motherboard, 'tail_span': CPU}
{'relation': 'has part', 'head_span': motherboard, 'tail_span': RAM}
{'relation': 'part of', 'head_span': CPU, 'tail_span': motherboard}
{'relation': 'part of', 'head_span': RAM, 'tail_span': motherboard}
{'relation': 'said to be the same as', 'head_span': mark of the beast,
➥ 'tail_span': 666.66}
{'relation': 'said to be the same as', 'head_span': 666.66
➥, 'tail_span': mark of the beast}
{'relation': 'parent organization', 'head_span': AlgoTrim,
➥ 'tail_span': Apple Inc}
{'relation': 'subsidiary', 'head_span': AlgoTrim, 'tail_span': Semetric}
{'relation': 'parent organization', 'head_span': Semetric,
➥ 'tail_span': AlgoTrim}

列表 12.10 中提取的关系看起来令人满意。REBEL 模型确定苹果公司是由史蒂夫·乔布斯、史蒂夫·沃兹尼亚克和罗纳德·韦恩共同创立的。反过来,它解析出他们也是苹果公司的雇员。不幸的是,该模型可以产生幻觉一些结果。幻觉是指无法在文本中找到的结果。例如,该模型确定史蒂夫·沃兹尼亚克创立了 Homebrew 计算机俱乐部。虽然这个事实是真实的,但无法通过文本进行验证;因此,该模型产生了这个事实的幻觉。收购被识别为parent organizationsubsidiary关系。该模型从文本中提取了这两种关系。最后一个关系是不正确的;这不是 REBEL 模型的问题,而是核心词消歧步骤的问题。

练习 12.1

使用列表 12.10 中提取的关系在 Neo4j 中构建一个图。由于您没有关于节点类型的信息,您可以使用通用的Entity标签为所有节点。

图 12.7 展示了信息提取管道输出的可视化表示。实体用节点表示,而关系用关系表示。

12-07

图 12.7 信息提取管道结果

练习 12.2

搜索关于商业基础或收购的新闻文章,然后通过实现的信息提取管道进行处理。您可以尝试包括或排除核心词消歧步骤,并评估它对输出的影响。

12.4.4 实体链接

我们信息提取过程的最后一步是将实体与其对应的 WikiData ID 进行链接。作为维基百科生态系统的一部分,WikiData 充当所有维基百科项目结构化数据的存储库。为了更好地了解可用的数据,您可以访问专门介绍史蒂夫·乔布斯(www.wikidata.org/wiki/Q19837)的 WikiData 页面。史蒂夫·乔布斯的 WikiData ID 位于 URL 末尾,为 Q19837。对于初始版本,将不会有任何高级实体消歧。您将直接使用 WikiData API 搜索实体,并从列表中选择第一个结果。稍后,您可以实现更高级的实体链接,包括使用句子上下文进行实体消歧(Barba 等,2022 年)。

列表 12.11 中的代码在 WikiData 上搜索给定的实体,并返回列表中第一个实体的 URL 和描述。

列表 12.11 定义一个在 WikiData 上搜索实体的函数

import requests

def retrieve_wiki_id(item):
  try:
    url = "https://www.wikidata.org/w/api.php?action=wbsearchentities"
    params = f"&search={item}&language=en&format=json"
    data = requests.get(url + params).json()
    return {
      "id": data["search"][0]["url"],
      "description": data["search"][0]["display"]["description"]["value"],
    }
  except Exception as e:
    return None

你现在可以使用列表 12.11 中定义的函数将信息提取管道中的实体与 WikiData ID 进行匹配。

列表 12.12 基本的实体链接工作流程

entities = set()
for value, rel_dict in doc._.rel.items():
    entities.update([rel_dict["head_span"], rel_dict["tail_span"]])

for entity in entities:
    wiki_data = retrieve_wiki_id(entity)
    print(entity, wiki_data)

列表 12.12 中的代码输出了以下结果。

列表 12.13 实体链接结果

CPU {'id': '//www.wikidata.org/wiki/Q5300', 'description': 'electronic
➥ circuitry within a computer that carries out the instructions of a
➥ computer program by performing the basic arithmetic, logical, control
➥ and input/output (I/O) operations specified by the instructions and
➥ coordinates the other components'}
motherboard {'id': '//www.wikidata.org/wiki/Q4321', 'description': 'main
➥ printed circuit board (PCB) for a computing device'}
Steve Jobs {'id': '//www.wikidata.org/wiki/Q19837', 'description':
➥ 'American entrepreneur; co-founder of Apple Inc. (1955-2011)'}
Steve Wozniak {'id': '//www.wikidata.org/wiki/Q483382', 'description':
➥ 'American computer pioneer, inventor, computer engineer and
➥ programmer; co-founder of Apple Inc.'}
AlgoTrim None
Ronald Wayne {'id': '//www.wikidata.org/wiki/Q332591', 'description':
➥ 'co-founder of Apple Inc.'}
Apple I {'id': '//www.wikidata.org/wiki/Q18981', 'description': 'computer
➥ built by the Apple Computer Company'}
Semetric None
mark of the beast {'id': '//www.wikidata.org/wiki/Q6770514', 'description':
➥ 'album by Manilla Road'}
666.66 {'id': '//www.wikidata.org/wiki/Q2778183', 'description': 'album
➥ by Noir Désir'}
RAM {'id': '//www.wikidata.org/wiki/Q5295', 'description': 'form of
➥ computer data storage'}
Apple Inc {'id': '//www.wikidata.org/wiki/Q312', 'description': 'American
➥ multinational technology company'}
Homebrew Computer Club {'id': '//www.wikidata.org/wiki/Q1195191',
➥ 'description': "computer hobbyist users' group in California"}

总体而言,列表 12.13 中的实体链接结果令人满意。幸运的是,没有出现歧义性的实体引用,例如,没有提到两个同名的人,你需要确定指的是哪一个。例如,想象一下文本中提到了埃里克·施密特。你需要确定这是美国商人埃里克·施密特还是为 2001 年北达科他州战斗雄狮队效力的美国足球运动员埃里克·施密特。此外,需要注意的是,并非所有实体都能链接到 WikiData。例如,AlgoTrim 和 Semetric 在 WikiData 知识库中没有条目。有趣的是,兽的印记666.66都链接到了代表专辑的实体。兽的印记666.66的链接结果是完全忽略文本上下文的结果。信息提取的过程可能极其有价值,但也同样具有挑战性,需要精确的技术来实现可靠的结果。

12.4.5 外部数据丰富

虽然实施的 WikiData 链接方法在实体消歧方面提供的帮助有限,但它为外部数据丰富提供了选择。WikiData 提供了大量的信息。例如,WikiData 提供了关于苹果公司(Apple Inc.)的各种数据,从董事会成员到子公司等更多内容(www.wikidata.org/wiki/Q312)。由于 REBEL 模型不提供实体类型,你可以使用 WikiData 来获取链接的实体类型。然而,从 WikiData 检索信息超出了本书的范围,因为它需要一些基本的 SPARQL 知识。尽管如此,我还是想给你一些提示,告诉你如何使用各种外部数据源丰富你的风险投资公司图。

恭喜!你已经成功实现了信息提取管道,从非结构化文本中提取关于组织和人物的信息。

12.5 练习解答

练习 12.1 的解答如下。

列表 12.14 将提取的关联导入 Neo4j 数据库的 Cypher 语句

WITH [
  {relation:'founded by',head_span:'Apple Inc',tail_span:'Steve Jobs'},
  {relation:'founded by',head_span:'Apple Inc',tail_span:'Steve Wozniak'},
  {relation:'founded by',head_span:'Apple Inc',tail_span:'Ronald Wayne'},
  {relation:'employer',head_span:'Steve Jobs',tail_span:'Apple Inc'},
  {relation:'employer',head_span:'Steve Wozniak',tail_span:'Apple Inc'},
  {relation:'employer',head_span:'Ronald Wayne',tail_span: 'Apple Inc'},
  {relation:'manufacturer',head_span:'Apple Inc',tail_span:'Apple Inc'},
  {relation:'member of',head_span:'Steve Wozniak',
     tail_span: 'Homebrew Computer Club'},
  {relation: 'founded by', head_span: 'Homebrew Computer Club',
     tail_span: 'Steve Wozniak'},
  {relation: 'has part', head_span: 'motherboard', tail_span: 'CPU'},
  {relation: 'has part', head_span: 'motherboard', tail_span: 'RAM'},
  {relation: 'part of', head_span: 'CPU', tail_span: 'motherboard'},
  {relation: 'part of', head_span: 'RAM', tail_span: 'motherboard'},
  {relation: 'said to be the same as',head_span: 'mark of the beast',
    tail_span: '666.66'},
  {relation: 'said to be the same as',head_span: '666.66',
    tail_span: 'mark of the beast'},
  {relation:'parent organization',head_span:'AlgoTrim',
    tail_span:'Apple Inc'},
  {relation:'subsidiary',head_span:'AlgoTrim',tail_span:'Semetric'},
  {relation: 'parent organization', head_span: 'Semetric',
    tail_span: 'AlgoTrim'}] AS data
UNWIND data AS row
MERGE(head: Entity {id: row.head_span})
MERGE(tail: Entity {id: row.tail_span})
WITH head, tail, row.relation AS relationType
CALL apoc.merge.relationship(head, relationType, {}, {}, tail) YIELD rel
RETURN distinct 'done'

简单的 Cypher 语法不支持导入具有动态类型的关联。你可以使用如列表 12.14 所示的apoc.merge.relationship过程来避免必须单独导入每种关系类型。你可以在文档中了解更多关于该过程的信息(mng.bz/xjrX)。

摘要

  • 信息提取是一种自然语言任务,它从非结构化文本中提取结构化信息。

  • 共指消解用于识别文本中所有对同一实体的引用。

  • 命名实体识别在文本中识别各种实体及其类型。

  • 实体链接是将实体链接到目标知识库(如 WikiData、MeSH 等)的过程。

  • 实体链接过程为您提供了额外的外部数据丰富选项,因为您可以识别具有唯一 ID 的实体。

  • 实体链接模型使用句子或段落上下文来消除实体歧义。

  • 关系抽取模型提取实体之间的关系。

  • 关系抽取模型有两种类型:基于规则和深度学习方法。

  • 命名实体识别和关系抽取模型高度依赖于用例领域。

  • 关系抽取模型生成一个三元组列表,这些三元组可以用来构建图。

  • 三元组可以用来构建信息的图表示。

  • 类似于 REBEL 的端到端关系抽取模型可以在单步中执行命名实体识别和关系抽取。

  • 将实体映射到目标知识库(如 WikiData)允许您使用这些知识库中的信息来丰富您的图。

附录。Neo4j 环境

在这本书中,您将通过使用 Neo4j 的实际示例学习图理论和算法。我选择 Neo4j 是因为我有超过五年的使用经验,用于构建和分析图。

Neo4j 是一个本机图数据库,从头开始构建以存储、查询和操作图数据。它使用 Java 实现,可以通过 Cypher 查询语言从其他语言编写的软件中访问,通过事务性 HTTP 端点或二进制 Bolt 协议。在 Neo4j 中,数据以节点和关系的形式存储,它们在数据库中都是一等公民。节点代表实体,如人或企业,关系代表这些实体之间的连接。节点和关系可以具有属性,这些属性是键值对,提供了关于节点和关系的额外信息。

Neo4j 设计为高度可扩展。它使用灵活的索引系统来高效地查询和处理数据,并支持原子性、一致性、隔离性和持久性(ACID)事务,以确保数据一致性。它还内置了一个查询语言,称为 Cypher,该语言旨在易于表达且易于使用,用于查询和操作图数据。

使用 Neo4j 的另一个好处是它有两个实用的插件,您将使用它们:

  • The Awesome Procedures on Cypher (APOC) plugin—这是一个为 Neo4j 提供的程序、函数和插件库,它提供了广泛的功能,包括数据导入/导出、数据转换和处理、日期时间区间处理、地理空间处理、文本处理等。

  • The Graph Data Science (GDS) plugin—这是一套为 Neo4j 设计的图算法和程序,允许用户对其图数据进行高级分析。GDS 提供了常见图算法(如最短路径、PageRank 和社区检测)的高效、并行实现。此外,该插件还包括节点嵌入算法和机器学习工作流程,支持节点分类和链接预测工作流程。

A.1 Cypher 查询语言

Cypher 是一种用于图数据库的声明式查询语言,用于检索和操作存储在图数据库中的数据。Cypher 查询使用简单、易于阅读的语法编写。以下是一个简单的 Cypher 查询示例,它使用 ASCII 艺术风格的图表来展示所查询的关系。

列表 A.1 一个示例 Cypher 语句

MATCH (a:Person)-[:FOLLOWS]->(b:Person)
WHERE a.name = "Alice"
RETURN b.name

openCypher 倡议是 Neo4j 与几个其他组织之间的合作,旨在推广 Cypher 查询语言作为处理图数据的标准。openCypher 倡议的目的是创建一种通用的语言,可以用来查询任何图数据库,无论其底层技术如何。为了实现这一目标,openCypher 倡议正在将 Cypher 语言规范和相关资源以开源许可证的形式提供,并鼓励各种组织开发 Cypher 实现。到目前为止,Cypher 查询语言已被 Amazon、Agens Graph、Katana Graph、Memgraph、RedisGraph 和 SAP HANA(openCypher 实施者小组;opencypher.org/projects/)采用。

此外,还有一个官方的 ISO 项目提议一个统一的图查询语言(GQL)来与图数据库交互(GQL 标准委员会)。GQL 旨在建立在 SQL 的基础上,并整合现有图查询语言中的成熟想法,包括 Cypher。这使得学习 Cypher 成为与图数据库交互的绝佳起点,因为它已经与许多数据库集成,并将成为官方 ISO 图查询语言的一部分。有关更多信息,请参阅 GQL 的图模式匹配提案(Deutsch 等人,2022 年)。

A.2 Neo4j 安装

有几种不同的选项来设置您的 Neo4j 环境:

  • Neo4j Desktop

  • Neo4j Docker

  • Neo4j Aura

如果您是 Neo4j 的新用户,我建议您使用 Neo4j 桌面版。

A.2.1 Neo4j 桌面版安装

Neo4j 桌面版是一个本地的 Neo4j 图数据库管理应用程序。它允许您通过几个点击创建数据库实例并安装官方插件。如果您决定使用 Neo4j 桌面版,请按照以下步骤成功启动一个安装了 APOC 和 GDS 插件的 Neo4j 数据库实例:

  1. 从官方网站下载 Neo4j 桌面版应用程序(neo4j.com/download;图 A.1)。

    A-01

    图 A.1 下载 Neo4j 桌面版。

  2. 在您的计算机上安装 Neo4j 桌面版应用程序,然后打开它。

  3. 完成注册步骤。您可以在下载应用程序时分配的软件密钥,或者通过点击“稍后注册”(图 A.2)跳过此步骤。

    A-02

    图 A.2 输入您的个人信息,或跳过注册步骤。

  4. 电影数据库管理系统(DBMS)在 Neo4j 桌面版首次执行时自动启动。如果正在运行,请停止电影 DBMS(图 A.3)。

    A-03

    图 A.3 停止默认的电影数据库管理系统数据库。

  5. 添加一个新的本地数据库管理系统(图 A.4)。

    A-04

    图 A.4 添加本地数据库管理系统。

  6. 输入数据库管理系统名称和密码的任何值。请确保选择版本 5.12.0 或更高版本(图 A.5)。

    A-05

    图 A.5 定义 DBMS 密码和版本。

  7. 通过选择 DBMS 来安装 APOC 和 GDS 插件,这将在右侧面板中打开详细信息、插件和升级选项卡。选择插件选项卡,然后安装 APOC 和 GDS 插件(图 A.6)。

    A-06

    图 A.6 安装 APOC 和 GDS 插件。

  8. 启动数据库(图 A.7)。

    A-07

    图 A.7 启动数据库。

  9. 打开 Neo4j 浏览器(图 A.8)。

    A-08

    图 A.8 打开 Neo4j 浏览器。

  10. 通过在 Cypher 编辑器中输入来执行 Cypher 查询。对于较长的 Cypher 语句,可以使用全屏编辑器选项(图 A.9)。

    A-09

    图 A.9 Neo4j 浏览器中的 Cypher 查询编辑器

A.2.2 Neo4j Docker 安装

如果你选择了 Neo4j Docker 安装,你需要在命令提示符中运行以下列表中的命令。

列表 A.2 启动 Neo4j Docker

docker run \
  -p 7474:7474 -p 7687:7687 \
  -d \
  -v $HOME/neo4j/data:/data \
  -e NEO4J_AUTH=neo4j/pleaseletmein \
  -e 'NEO4J_PLUGINS=["apoc", "graph-data-science"]' \
    neo4j:5.12.0

此命令在后台启动 docker 化的 Neo4j。通过定义NEO4J_PLUGINS环境变量,APOC 和 GDS 插件会自动添加。将data卷挂载以持久化数据库文件是一个好习惯。数据库用户名和密码由NEO4J_AUTH变量指定。

在执行列表 A.2 中的命令后,在你的网页浏览器中访问 http://localhost:7474。输入由NEO4J_AUTH变量指定的密码。示例中的密码是pleaseletmein

A.2.3 Neo4j Aura

Neo4j Aura 是 Neo4j 数据库的托管云实例。不幸的是,免费版本不提供 GDS 库。如果你想使用云托管的 Neo4j Aura 来跟随本书中的示例,你需要使用 AuraDS 版本,它提供了对 GDS 算法的支持。你可以在 Neo4j 的官方网站上找到更多信息:neo4j.com/cloud/platform/aura-graph-database/

A.3 Neo4j 浏览器配置

Neo4j 浏览器有一个面向初学者的功能,可以可视化结果节点之间所有的关系,即使这些关系不是查询结果的一部分。为了避免混淆,请取消选中如图 A.10 所示的连接结果节点功能。

A-10

图 A.10 在 Neo4j 浏览器中取消选中连接结果节点。

参考文献

Adamic, L. A., & Adar, E. (2003). 网络上的朋友和邻居. 社会网络, 25, 211-230. Al-Zaman, S. (2021). 2019 年 12 月至 2020 年 6 月间发表的 COVID-19 相关文献的计量学和共现分析. 科学编辑. 8, 57-63. https://doi.org/10.6087/kcse.230

Albert, R., Jeong, H. & Barabási, A. L. (1999). 互联网的直径. 自然, 401, 130-131. https://doi.org/10.1038/43601

Ali, M., Berrendorf, M., Hoyt, C., Vermue, L., Sharifzadeh, S., Tresp, V., & Lehmann, J. (2021). PyKEEN 1.0: 一个用于训练和评估知识图谱嵌入的 Python 库. 机器学习研究杂志, 22(82), 1-6.

Andersen, N. et al. (2020). 新兴的 COVID-19 研究:动态和定期更新的科学图谱和分析. BMC 医学信息学与决策制定, 20(1), 309. 30. https://doi.org/10.1186/s12911-020-01321-9

Bachman, J., Gyori, B., & Sorger, P. (2022). 从文本挖掘和精选数据库中大规模自动组装分子机制. bioRxiv. Barabási, A. L., & Albert, R. (1999). 随机网络中的尺度涌现. 科学, 286(5439), 509-512.

Barba, E., Procopio, L., & Navigli, R. (2022). ExtEnD: 提取式实体消歧. 第 60 届计算语言学协会年会论文集,爱尔兰, 1 (长篇论文), 2478-2488. 计算语言学协会.

Beguerisse-Díaz, Mariano et al. (2014). 有向网络中的兴趣社区和流动角色:英国骚乱的 Twitter 网络. 皇家学会接口杂志, 11(101), 20140940. https://doi.org/10.1098/rsif.2014.0940

Beveridge, A. & Chemers, M. M. (2018). 权力的游戏:网络化的和谐与分形戏剧学. 泰勒弗朗西斯.

Blondel, V. D., Guillaume, J.-L., Lambiotte, R., & Lefebvre, E. (2008). 大规模网络中社区的快速展开. 统计力学:理论与实验杂志, (10), P10008.

Bordes, A., Usunier, N., Garcia-Duran, A., Weston, J., & Yakhnenko, O. (2013). 用于建模多关系数据的嵌入翻译. 神经信息处理系统进展, 26.

Brank, J. , Leban, G., & Grobelnik, M. (2017). 使用相关的维基百科概念标注文档. 斯洛文尼亚数据挖掘和数据仓库会议(SiKDD),斯洛文尼亚卢布尔雅那.

Brin, S., & Page, L. (1998). The anatomy of a large-scale hypertextual web search engine. 计算机网络与 ISDN 系统, 30(1-7), 107-117.

Brown, T. B. , Mann, B., Ryder, N., Subbiah, M., Kaplan, J., Dhariwal, P., Neelakantan, A., Pranav Shyam, Girish Sastry, Amanda Askell, Sandhini Agarwal, Ariel Herbert-Voss, Gretchen Krueger, Tom Henighan, Rewon Child, Aditya Ramesh, Daniel M. Ziegler, Jeffrey Wu, Clemens Winter, ... Dario Amodei. (2020). 语言模型是少样本学习者. arXiv.

Çano, E.,& Morisio, M. (2017)。混合推荐系统:系统文献综述。智能数据分析21(6),1487-1524。

Chao, L.,He, J.,Wang, T.,& Chu, W. (2020)。PairRE:通过配对关系向量进行知识图谱嵌入

Chen, H.,Sultan, S.,Tian, Y.,Chen, M.,& Skiena, S. (2019)。通过非常稀疏随机投影实现快速准确的网络嵌入

Cherepnalkoski, D.,& Mozetic, I. (2015)。欧洲议会的重推网络分析。第 11 届信号-图像技术及基于互联网的系统国际会议(SITIS),350-357,https://doi.org/10.1109/SITIS.2015.8

CKO 新闻编辑。 (2017)。NASA 专家可视化经验教训。APPEL。https://appel.nasa.gov/2017/08/29/nasa-expert-visualizes-lessons-learned/

Clark, K.,& Manning, C. (2016)。深度强化学习用于提及排名共指模型。Cooper, K. M. (2020)。在美国销售的包装食品的成分共现网络。食品成分与分析杂志86,103391。

Darke, E.,Zhuang, Z.,& Wang, Z. (2017)。将链接预测应用于亚马逊产品的推荐系统

Dhingra, S. 等人. (2016). 在社交网络图中寻找强连通分量。国际计算机应用杂志136,1-5。

Ding, Y.,Yan, E.,Frazho, A.,& Caverlee J. (2010)。PageRank 用于共引网络中的作者排名

Durazzi, F.,Müller, M.,Salathé, M. 等人. (2021)。在 COVID-19 大流行期间,与科学和健康相关的 Twitter 用户的集群变得更加孤立。科学报告11,19655。https://doi.org/10.1038/s41598-021-99301-0

Dutta, A.,Riba, P.,Lladós, J. 等人. (2020)。用于基于图的模式识别的分层随机图嵌入。神经计算与应用32,11579-11596。https://doi.org/10.1007/s00521-019-04642-7

Efstathiades, H. 等人. (2016). 在线社交网络演变:重新审视 Twitter 图。2016 年 IEEE 国际大数据会议(大数据),626-635。

Erdös, P.,& Rényi, A. (1959). 关于随机图。数学出版物6,290-297。

Evkoski, B. & Mozeticˇ, I.,Ljubešic´, N.,& Kralj Novak, P. (2020)。2018-2020 年斯洛文尼亚重推网络。SiKDD

Evkoski, B. & Mozeticˇ, I.,Ljubešic´, N.,& Kralj Novak, P. (2021)。重推网络中的社区演变。PLOS ONE16。e0256175。https://doi.org/10.1371/journal.pone.0256175

Fukuda, S.,& Tomiura, Y. (2018)。基于句子角色的研究论文聚类。ICADL 海报论文集。汉密尔顿,新西兰。怀卡托大学。

Gleason, B. (2013). #占领华尔街:在 Twitter 上探索关于社会运动的非正式学习。美国行为科学家57(7),966-982。https://doi.org/10.1177/0002764213479372

Grover, A.,& Leskovec, J. (2016)。node2vec:网络的缩放特征学习

Gábor I., Vince G. (2011). 当网络遇到细胞:使用个性化 PageRank 分析蛋白质相互作用网络。生物信息学27(3),405-407 页。https://doi.org/10.1093/bioinformatics/btq680

Haghighi, A., & Klein, D. (2009). 基于丰富句法和语义特征的单个核心 ference 解析。自然语言处理实证方法会议论文集,1152-1161 页。计算语言学协会。

Hamilton, W. L. , Ying, R., & Leskovec, J. (2017). 大型图上的归纳表示学习。CoRR。abs/1706.02216。

Hamilton, W. L., Ying, R., & Leskovec, J. (2018). 图上的表示学习:方法和应用

Han, X., Gao, T., Yao, Y., Ye, D., Liu, Z., & Sun, M. (2019). OpenNRE:一个开放且可扩展的神经关系提取工具包。EMNLP-IJCNLP 系统演示会议论文集,第 169-174 页。

Han, Y.-S., Kim, L., & Cha, J.-W. (2009). YouTube 上用户声誉的评估。在线社区与社会计算国际会议,346-353 页。https://doi.org/10.1007/978-3-642-02774-1_38

Hawthorne, J., Houston, J. B., & Mckinney, M. (2013). 通过实时推文总统初选辩论探索新的政治对话。社会科学计算机评论31,552-562 页。10.1177/0894439313490643

Henderson, K., Gallagher, B., Eliassi-Rad, T., Tong, H., Basu, S., Akoglu, L., Koutra, D., Faloutsos, C., & Li, L. (2012). RolX: 结构角色提取与挖掘在大图中的应用。第 18 届 ACM SIGKDD 国际知识发现与数据挖掘会议论文集,1231-1239 页。计算机协会。

Himmelstein, D., Lizee, A., Hessler, C., Brueggeman, L., Chen, S., Hadley, D., Green, A., Khankhanian, P., & Baranzini, S. (2017). 系统集成生物医学知识,优先考虑药物再利用。eLife6,e26726。

Huguet Cabot, P. L., & Navigli, R. (2021). REBEL:通过端到端语言生成进行关系提取。计算语言学协会:EMNLP 2021 发现,2370-2381 页。计算语言学协会。

Kashwan, K. R., & Velu, C. (2013). 使用聚类和数据挖掘技术进行客户细分。计算机理论工程国际期刊5,856-861 页。https://doi.org/10.7763/IJCTE.2013.V5.811

Kastrin, A., Rindflesch, T., & Hristovski, D. (2014). 在 MeSH 共现网络中的链接预测:初步结果。健康技术与信息学研究205,579-583 页。

Kim, T.-H., Kim, J., Heslop-Harrison, P., & Cho, K.-H. (2011). 基于王国特定网络基元的进化设计原则和功能特征。生物信息学27(2),245-251 页。https://doi.org/10.1093/bioinformatics/btq633

Kirkley, A., Barbosa, H., Barthelemy, M., & Ghoshal, G. (2018). 从街道网络中的中介中心性到随机平面图的结构不变量。自然通讯, 9, 2501. https://doi.org/10.1038/s41467-018-04978-z

Kular, D. K. , Menezes, R., & Ribeiro, E. (2011). 使用网络分析来理解烹饪与文化的关联。_IEEE 网络科学研讨会, 38-45. https://doi.org/10.1109/NSW.2011.6004656

Lakshmi, T., & Bhavani, S. (2021). 链接预测方法在推荐系统中的应用. le Gorrec, L., Knight, P. A., & Caen, A. (2022). 使用小图群学习网络嵌入。社会网络分析与挖掘, 12, 20. https://doi.org/10.1007/s13278-021-00846-9

Lee, K., He, L., Lewis, M., & Zettlemoyer, L. (2017). 端到端神经核心词消解.

Levy, Y. (2014). 基于依存关系的词嵌入。第 52 届计算语言学年会论文集, 2 (短论文), 302-308. 计算语言学协会。

Li, B., Fang, G., Yang, Y., Wang, Q., Ye, W., Zhao, W., & Zhang, S. (2023). 评估 ChatGPT 的信息提取能力:对性能、可解释性、校准和忠实度的评估

Luo, H., Wang, J., Li, M., Luo, J., Peng, X., Wu, F. X., & Pan, Y. (2016). 基于综合相似度测量和 Bi-Random walk 算法的药物重定位。生物信息学, 32(17), 2664-2671.

Marr, B. (2017). 大数据:为什么 NASA 现在可以可视化其经验教训. Forbes. https://www.forbes.com/sites/bernardmarr/2017/02/22/big-data-why-nasa-can-now-visualize-its-lessons-learned/?sh=7f39b78b2003

Mikolov, T., Chen, K., Corrado, G., Dean, J. (2013). 在向量空间中高效估计词表示. arXiv:1301.3781

Minot J. R., Arnold M. V., Alshaabi T., Danforth C. M., & Dodds P. S. (2021). 比较总统:探索公众在推特上对奥巴马和特朗普的参与度。PLOS ONE, 16(4), e0248880. https://doi.org/10.1371/journal.pone.024888

Myers, S., Sharma, A., Gupta, P., & Lin, J. (2014). 信息网络还是社交网络?推特关注图的架构。第 23 届国际万维网会议论文集, 493-498. 计算机协会。

Newman, M. E. (2001). 科学合作网络的结构。美国国家科学院院刊, 98(2), 404-409. https://doi.org/10.1073/pnas.021544898. PMID: 11149952; PMCID: PMC14598.

Nicola, V. (2018). 推特标签共现网络研究及其知识发现应用 (v1.0.0). Zenodo. https://doi.org/10.5281/zenodo.1289254

OpenAI. (2023). GPT-4 技术报告.

Pervin, F. (2015). 推特上的标签流行度:分析多个标签的共现。在 G. Meiselwitz, 社会计算与社会媒体 (第 169-182 页). Springer 国际出版社。

Piedra, N., Chicaiza, J., Lopez-Vargas, J., & Tovar, E. (2017). 从开放知识源中发现潜在的协作网络.

Pinkert, S., Schultz, J., & Reichardt J. (2010). 蛋白质相互作用网络——不仅仅是模块。计算生物学杂志, 6(1), e1000659.

Priyanta, S., & Prayana Trisna, I. N. (2019). 使用 PageRank 识别主题发行者的 Twitter 社交网络分析。高级计算机科学与应用国际杂志, 10. https://doi.org/10.14569/IJACSA.2019.0100113.

Pržulj, N., Corneil, D. G., Jurisica, I. (2004). 模型化相互作用组:无标度还是几何?生物信息学, 20(18), 3508-3515. https://doi.org/10.1093/bioinformatics/bth436

Raghunathan, K., Lee, H., Rangarajan, S., Chambers, N., Surdeanu, M., Jurafsky, D., & Manning, C. (2010). 核心词消解的多遍筛法。2010 年自然语言处理经验方法会议论文集, 492-501. 计算语言学协会.

Rindflesch, T. C., Kilicoglu, H., Fiszman, M., Rosemblat, G., & Shin, D. (2011). 语义 MEDLINE:一种高级生物医学信息管理应用。信息服务与利用, 31(1), 15-21.

Rindflesch, T. C, & Fiszman, M. (2003). 领域知识与语言结构在自然语言处理中的相互作用:在生物医学文本中解释上位词命题。生物医学信息学杂志, 36(6), 462-477. https://doi.org/10.1016/j.jbi.2003.11.003

Rossi, R. A., Zhou, R., & Ahmed, N. K. (2017). 图深度特征学习.

Rothwell, Peter M. et al. (2011). 每日阿司匹林对长期癌症死亡风险的影响:分析随机试验的个体患者数据。柳叶刀, 377(9759), 31-41. https://doi.org/10.1016/S0140-6736(10)62110-1

Rüschoff, J. et al. (1998). 阿司匹林通过遗传选择抑制与遗传非息肉性结直肠癌相关的突变表型。美国国家科学院院刊, 95(19), 11301-11306. https://doi.org/10.1073/pnas.95.19.11301

Sanhueza, C. (2017, January). 漫威宇宙社交网络,版本 1. Kaggle. https://www.kaggle.com/datasets/csanhueza/the-marvel-universe-social-network

Schlander, M., Hernandez-Villafuerte, K., Cheng, C. Y. et al. (2021). 研究和开发新药的成本是多少?一项系统综述和评估。药理学经济学, 39, 1243-1269. https://doi.org/10.1007/s40273-021-01065-y

Shirazi, S., Albadvi, A., Akhondzadeh, E., Farzadfar, F., & Teimourpour B. (2020). 社区检测的新应用:识别医生的真实专业。国际医学信息学杂志, 140, 104161.

Singh, S. (2018). 信息提取的自然语言处理.

Sun, J., Li, Y., Liu, L., Jiang, Z., & Liu, G. (2019). 阿司匹林使用与胰腺癌风险:观察性研究的系统评价。医学98(51),e18033。https://doi.org/10.1097/MD.0000000000018033. PMID: 31860953; PMCID: PMC6940047。

Tinati, R., Carr, L., Hall, W., & Bentwood, J. (2012). 在 Twitter 中识别传播者角色。10.1145/2187980.2188256。

Türker, I.lker, & Sulak, E. (2018). 通过共现和语义链接对 Twitter 中的标签进行多层网络分析。国际现代物理 B 卷,32(04),1850029。

Ugander, J., Karrer, B., Backstrom, L., & Marlow, C. (2011). Facebook 社交图谱的结构。arXiv。

Vane, J. (1971). 阿司匹林类药物作用的机制:前列腺素合成的抑制。自然新生物学,231,232-235 页。https://doi.org/10.1038/newbio231232a0

Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, L., & Polosukhin, I. (2017). 注意即所需。CoRR. abs/1706.03762.

Voulodimos, A., Doulamis, A., Patrikakis, C., Sardis, E., & Karamolegkos, P. (2011). 使用聚类算法创建用户组以提供个性化上下文感知服务。https://doi.org/10.1145/2072627.2072637

Wang, R., Liu, W. and Gao, S. (2016). 网络社会运动中的标签和信息病毒性:检查标签共现模式。在线信息评论40(7),850-866 页。https://doi.org/10.1108/OIR-12-2015-0378

Wasserman, S., & Faust, K. (1994). 社会网络分析:方法和应用。剑桥大学出版社。https://doi.org/10.1017/CBO9780511815478

Wei, X., Cui, X., Cheng, N., Wang, X., Zhang, X., Huang, S., Xie, P., Xu, J., Chen, Y., Zhang, M., Jiang, Y., & Han, W. (2023). 通过与 ChatGPT 聊天进行零样本信息提取。

Wu, Z., Lin, Y., Wang, J., & Gregory, S. (2015). 基于节点聚类系数的链接预测效率。CoRR. abs/1510.07819。

Yao, Y., Ye, D., Li, P., Han, X., Lin, Y., Liu, Z., Liu, Z., Huang, L., Zhou, J., & Sun, M. (2019). DocRED:一个大规模文档级关系抽取数据集。第 57 届计算语言学协会年会论文集,764-777 页。计算语言学协会。

Zhou, P., Shi, W., Tian, J., Qi, Z., Li, B., Hao, H., & Xu, B. (2016). 基于注意力的双向长短期记忆网络用于关系分类。第 54 届计算语言学协会年会论文集,2(短论文),207-212 页。计算语言学协会。

posted @ 2025-11-23 09:27  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报