图驱动的数据分析和机器学习-全-
图驱动的数据分析和机器学习(全)
原文:
zh.annas-archive.org/md5/37e2889ce1b45e2831a79a87364a5a5b译者:飞龙
前言
目标
本书的目标是向您介绍图数据结构、图分析技术和图机器学习工具。当您完成本书后,我们希望您能理解图分析如何应用于解决各种实际问题。我们希望您能够回答以下问题:图在这项任务中是否合适?我应该使用哪些工具和技术?我的数据中有哪些有意义的关系,如何用关系分析的术语来表达任务?
根据我们的经验,我们发现许多人很快掌握了图的一般概念和结构,但要“思考图”,即开发最佳方法将数据建模为图,然后将分析任务表达为图查询,需要更多的努力和经验。每一章都以其目标列表开头。这些目标分为三个一般领域:学习图分析和机器学习的概念;使用图分析解决特定问题;理解如何使用 GSQL 查询语言和 TigerGraph 图平台。
受众和先决条件
我们为任何对数据分析感兴趣并希望学习图分析的人设计了这本书。您不必是一位严肃的程序员或数据科学家,但对数据库和编程概念的一些了解肯定会帮助您跟随本书的讲解。当我们深入探讨一些图算法和机器学习技术时,我们会呈现一些涉及集合、求和和极限的数学方程。然而,这些方程式只是对我们用文字和图形解释的补充。
在用例章节中,我们将在 TigerGraph Cloud 平台上运行预写的 GSQL 代码。您只需一台计算机和互联网访问权限。如果您熟悉 SQL 数据库查询语言和任何主流编程语言,则您将能够理解大部分 GSQL 代码。如果不熟悉,您可以简单地按照书中的说明运行预写的用例示例,并跟随书中的评论。
方法和路线图
我们的目标是以真实数据分析需求为动机,而不是理论原则。我们总是尝试用尽可能简单的术语解释事物,使用日常概念而不是技术术语。
通过完整的示例引入了 GSQL 语言。在本书的早期部分,我们逐行描述了每行的目的和功能。我们还突出了语言结构、语法和语义,这些内容特别重要。想要全面学习 GSQL 语言的教程,您可以参考本书之外的其他资源。
本书分为三部分:第一部分:连接;第二部分:分析;第三部分:学习。每部分都包含两种类型的章节。第一种是概念章节,后面是关于 TigerGraph Cloud 和 GSQL 的两到三个用例章节。
| 章节 | 格式 | 标题 |
|---|---|---|
| 1 | 介绍 | 一切从连接开始 |
| 第一部分:连接 | ||
| 2 | 概念 | 连接和探索数据 |
| 3 | 用例,TigerGraph 介绍 | 看到您的客户和业务更清晰:360 图表 |
| 4 | 用例 | 研究初创投资 |
| 5 | 用例 | 检测欺诈和洗钱模式 |
| 第二部分:分析 | ||
| 6 | 概念 | 分析连接以获得更深入的洞察 |
| 7 | 用例 | 改善推荐和建议 |
| 8 | 用例 | 加强网络安全 |
| 9 | 用例 | 分析航空公司航班路线 |
| 第三部分:学习 | ||
| 10 | 概念 | 图驱动的机器学习方法 |
| 11 | 用例 | 实体解析再访 |
| 12 | 用例,机器学习工作台介绍 | 提升欺诈检测 |
本书中使用的约定
本书使用以下排版约定:
Italic
表示新术语、网址、电子邮件地址、文件名和文件扩展名。
Constant width
用于程序列表,以及在段落内引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
Constant width bold
表示顶点或边类型。
提示
此元素表示提示或建议。
注意
此元素表示一般注释。
警告
此元素表示警告或注意事项。
使用代码示例
本书在 GitHub 上有自己的存储库,网址为https://github.com/TigerGraph-DevLabs/Book-graph-powered-analytics。
本站点的初始内容将包括所有用例示例的副本。我们还将把该书的 GSQL 技巧汇集成一篇文档,作为入门指南。随着读者的反馈(我们希望听到您的意见!),我们将发布对常见问题的答复。我们还将添加额外或修改后的 GSQL 示例,或指出如何利用 TigerGraph 平台的新功能。
有关 TigerGraph 和 GSQL 语言的更多资源,请访问 TigerGraph 的主要网站(https://www.tigergraph.com)、其文档网站(https://docs.tigergraph.com)或其 YouTube 频道(https://www.youtube.com/@TigerGraph)。
您可以通过 gpaml.book@gmail.com 联系作者。
致谢
没有 TigerGraph 的市场副总裁 Gaurav Deshpande 的提议,这本书就不会存在,他建议我们写这本书并且相信我们能够写出来。他撰写了最初的提案和章节大纲;三部分结构也是他的创意。感谢 TigerGraph 的首席执行官兼创始人 Dr. Yu Xu,他支持我们的工作,并赋予我们在这个项目上的灵活性。Dr. Xu 也构想了 GraphStudio 及其 Starter Kits。Mingxi Wu 和 Alin Deutsch 开发了以高效图分析为目标的 GSQL 语言。
除了官方作者外,还有其他几位贡献了本书的内容。Tom Reeve 运用他的专业写作技巧和对图形概念的了解,帮助我们撰写 Chapter 2,当笔者困扰和拖延似乎是我们最大的敌人时。Emily McAuliffe 和 Amanda Morris 设计了本书的早期版本中的几个图表。我们需要一些数据科学家来审查我们关于机器学习的章节。我们求助于 Parker Erickson 和 Bill Shi,他们不仅是图形机器学习方面的专家,还开发了 TigerGraph ML Workbench。
我们要感谢 TigerGraph 的原始 GSQL 查询与解决方案专家 Xinyu Chang,他开发或监督开发了本书中许多使用案例起始工具包和图算法实现。Yiming Pan 也编写或优化了几个图算法和查询。本书的许多示例都基于他们为 TigerGraph 客户开发的设计。这些起始工具包中的模式、查询和输出显示与本书的英文段落一样重要。我们对这些起始工具包进行了几处改进,以适应本书。许多人帮助审查和标准化起始工具包:开发者关系负责人 Jon Herke 以及几位 TigerGraph 实习生:Abudula Aisikaer、Shreya Chaudhary、McKenzie Steenson 和 Kristine Zheng。负责 TigerGraph Cloud 和 GraphStudio 设计与开发的 Renchu Song 和 Duc Le 确保我们修订后的起始工具包已发布至产品中。
非常感谢 O’Reilly 的两位开发编辑。Nicole Taché 指引我们完成了两章的早期发布,并提供了深刻的评论、建议和鼓励。Gary O’Brien 在此基础上带领我们完成了整个项目,经历了风风雨雨。两位都是出色的编辑,与他们合作是一种荣幸。也感谢我们的制作编辑 Jonathon Owen 和副本编辑 Adam Lawrence。
Victor 感谢他的父母 George 和 Sylvia Lee,他们在他学术和非学术追求中的无私支持。他要感谢他的妻子 Susan Haddox,她始终支持他,容忍他深夜写作,陪他看各种《星际迷航》,并成为他如何既聪明又善良和幽默的榜样。
Kien 感谢他的母亲 My Linh Ly,她始终是他职业生涯中的灵感源泉和推动力。他也感谢他的妻子 Sammy Wai-lok Lee,她一直与他同在,为他的生活增添色彩,照顾他们的女儿 Liv Vy Ly Nguyen-Lee,在写作本书期间出生。
Alex 感谢他的父母,Chris 和 Becky Thomas,以及他的姐姐 Ari,在写作过程中作为讨论伙伴给予他们的支持和鼓励。特别感谢他的妻子 Gloria Zhang,她的无限力量、广博智慧和无穷的灵感能力。
第一章:连接至关重要
从一个极端观点来看,世界只能看作是连接,别无其他。我们把词典视为意义的存储库,但它仅通过其他词语来定义单词。我喜欢这样一个想法,即信息片段实际上仅由它所关联的内容以及它们之间的关系来定义。意义实际上没有太多其他内容。结构就是一切。
蒂姆·伯纳斯-李,《编织网络:万维网的原始设计与终极命运》(1999),第 14 页
20 世纪展示了我们通过电子表格和关系数据库能够取得的成就。表格数据占据主导地位。21 世纪已经向我们展示,这还不够。表格使我们的视角变得扁平,只显示二维连接。然而在现实世界中,事物之间相互关联和连接着无数其他事物,这些关系决定了现在和将来会发生的事情。要全面理解,我们需要建模这些连接。
个人电脑在 1970 年代问世,但直到它们找到了第一个杀手级应用程序才真正起飞:财务电子表格。在苹果 II 上的 VisiCalc,然后在 IBM PC 上的 Lotus 1-2-3,自动化了从写作和算术发明以来会计员们一直手工进行的繁琐且容易出错的计算工作:加总行和列的数字,然后可能执行更复杂的统计计算。
1970 年,E.F.科德发表了他关系数据库模型的开创性论文。在数据库的早期阶段,有几种模型在流传,包括网络数据库模型。科德的关系模型建立在每个人都能认同且易于编程的东西之上:表格。
此外,矩阵代数和许多统计方法也已经准备好与表格一起工作。物理学家和业务分析师都使用矩阵来定义和找到一切从核反应堆设计到供应链管理的最优解。表格适合并行处理;只需垂直或水平地划分工作负载。电子表格、关系数据库和矩阵代数:表格化方法似乎是解决所有问题的方案。
然后互联网发生了,一切都改变了。
连接改变一切。
Web 不仅仅是互联网。互联网始于 20 世纪 70 年代初,是一种数据连接网络,连接了美国的选定研究机构。由 CERN 研究员蒂姆·伯纳斯-李在 1989 年发明的万维网,是一组在互联网之上运行的技术,使得发布、访问和连接数据变得更加容易,以便人类消费和交互。浏览器、超链接和网址也是 Web 的标志。与 Web 同时发展的是,各国政府放松了对互联网的控制,允许私营公司扩展它。现在我们有数十亿个相互连接的网页,以全球范围连接人们、多媒体、事实和观点。拥有数据还不够,数据的结构如何也很重要。
什么是图形?
随着“网络”一词开始具有新的内涵,“图”一词也开始如此。对于大多数人来说,“图”与能够显示诸如股票价格随时间变化等内容的折线图是同义词。然而,数学家对这个词有另一种含义,随着网络和连接开始对商业世界变得重要,数学上的含义也开始显现。
图形是一种由顶点(或节点)和顶点之间的连接称为边的抽象数据结构。就是这样。图形是网络的概念,由这两种类型的元素构建而成。这种抽象允许我们研究一般的网络(或图形),发现它们的属性,并设计算法来解决一般任务。图论和图分析为组织提供了他们需要利用大量连接数据的工具。
在图 1-1 中,我们可以看到《星球大战》(1977 年)和《帝国反击战》(1980 年)中演员和导演之间关系的网络。这可以很容易地建模为一个图形,不同类型的边连接不同类型的顶点。演员和电影可以由一个acted_in顶点连接,电影和其他电影可以由一个is_sequel_of顶点连接,电影和导演可以有一个directed_by边连接。

图 1-1. 显示早期《星球大战》电影中一些关键人物和联系的图形
为什么图形很重要
互联网向我们展示,有时候我们通过拥有各种各样互联的数据,完成的事情比试图将所有数据合并到几个僵化的表格中要多。它还向我们展示了,连接本身就是一种信息。我们有无限数量的关系类型:父母-子女、购买者-产品、朋友-朋友等等。正如伯纳斯-李所观察到的,我们从连接中获得意义。当我们知道某人是父母时,我们可以推断出他们经历了某些生活经历,拥有某些关切。我们还可以根据父母和子女之间的关系做出合理的猜测。
然而,互联网仅仅突显了一个一直存在的真理:在表示数据和分析数据时,数据关系是重要的。图表比表格更能体现关系的信息内容。这种丰富的数据格式更擅长表示复杂信息,在分析时也能产生更有洞察力的结果。面向商业的数据分析师们欣赏看到关系以图形化形式展现的直观性,而数据科学家则发现,更丰富的内容能够产生更准确的机器学习模型。作为一个额外的好处,图数据库在处理涉及多层次连接搜索(或多个跳跃)的任务时,通常比关系型数据库运行得更快。
结构很重要
谷歌的创始人们认识到,互联网将变得太大,以至于任何人都无法完全掌握。我们需要工具来帮助我们搜索和推荐网页。谷歌早期成功的一个关键组成部分是 PageRank,这是一个将互联网建模为一组相互连接的页面的算法,基于页面之间的连接模式来决定哪些页面最具影响力或权威性。
多年来,搜索引擎在推断我们的查询内容上变得越来越好,能够准确了解我们真正想知道的内容和我们会觉得有用的信息。谷歌的一个工具是其知识图谱,这是一个从更广泛的互联网中收集的、相互连接的分类和标签化的事实和概念集合。在分析用户的查询时,谷歌不仅理解表面的单词,还理解其中的隐含类别和目标,然后在其知识图谱中搜索最匹配的事实,并以格式良好的侧边栏呈现它们。只有图形才具有足够的灵活性和表达力,才能使这个事实宇宙变得有意义。
社区很重要
Facebook 最初是面向大学生的社交应用程序;它已发展成为全球最大的在线社交网络。显而易见,Facebook 关心网络和图形。从每个用户的角度来看,有自己和一组朋友。虽然我们是单独行动的,但人们自然倾向于聚集成社区,这些社区像活体一样发展并具有影响力。社区对我们接收信息的方式和形成观点具有强大的影响力。企业利用社区行为来推广其产品。人们也利用社交网络来推广政治议程。检测这些社区对于理解社会动态至关重要,但在表格视图中是看不到这些社区的。
连接的模式很重要
相同的信息可以以表格形式或图形形式呈现,但图形形式显示了表格所掩盖的内容。想象一棵家族树。我们可以在表格中列出所有的父子关系,但表格可能会忽略跨越多个关系的重要模式:家庭、孙辈、堂兄弟姐妹。
更不明显的例子是财务交易的图形。金融机构和供应商寻找特定的交易模式,这些模式可能暗示可能存在的欺诈或洗钱活动。一个模式是大笔资金从一方转移到另一方,其中高比例的资金返回到原点:一个闭环。图 1-2 展示了这样的循环,从包含数百万笔交易的图数据库中提取,来自我们在 第五章 中的财务欺诈示例。其他模式可以是线性或 Y 形;一切皆有可能。模式取决于数据的性质和感兴趣的问题。

图 1-2. 图搜索结果显示了闭环中的交易序列(请查看此图的更大版本:oreil.ly/gpam0102)
模式不仅取决于形状,还取决于顶点类型和边缘类型。图 1-2 有两种顶点类型:账户和交易(是的,交易是顶点,而不是边缘)。如果我们选择,我们可以将人和账户分开为单独的实体。一个人可以与多个账户关联。像这样分解使我们能够分析一个人的行为,而不仅仅是一个账户。将重要概念建模为顶点类型使我们能够拥有更丰富的搜索分析能力,这将在后面的章节中看到。
边缘优于表连接
将顶点表示为表,将边表示为表是真的。关于图实际上有什么不同,以及我们为什么声称它在多跳操作中更快?首先,图不仅仅是可视化。我们为了人类方便而可视化数据,但计算机根本不需要这种视觉方面。
图的性能优势归结为在关系型数据库与图数据库中实际进行连接搜索和利用的机制。在关系型数据库中,直到运行查询之前,表之间没有链接。是的,如果您在一个表中声明并强制执行了外键以引用另一个表,那么您知道外键列的值将对应于其相关表中的主键值。这仅意味着两个不同的表存储重复数据,但您仍然必须寻找那些匹配记录。
考虑一个简单的跟踪客户购买的数据库。我们有三张表:Person(人员)、Item(物品)和 Purchases(购买),如图 1-3 所示。假设我们想知道 Person B 做过的所有购买。Purchases 表按日期组织,而不是按人员组织,所以看来我们需要扫描整个表才能找到 Person B 的购买记录。对于大型数据库,这非常低效。

图 1-3. Person-Purchases-Item 数据库的关系表结构
这是一个常见的问题,因此关系型数据库已经提出了解决方案:次要索引。就像参考书的索引可以告诉您某些关键主题出现的页码一样,表索引告诉您某些列值出现的行地址。图 1-4 草绘了 Purchases 表的 PID(人员 ID)和 IID(物品 ID)列的索引概念。现在我们知道了 Person B 的购买记录分别在表的第 4、6、8 和 10 行。然而,仍然存在一些权衡。创建索引及随数据库演化而维护索引需要时间和存储空间,而且与直接访问具有所需内容的数据行相比,仍然需要额外步骤访问索引。索引本身就是一张表。我们能多快地找到所有人中的 Person B 呢?
没有索引:
- 阅读 Purchases 表中的每一行(速度慢且不可扩展)。
有索引:
-
转到 Purchases 表的次要索引。
-
找到感兴趣的行(可能很快)。
-
使用索引。

图 1-4. Purchases 表的次要索引
图数据库或图分析平台消除了通过表搜索和构建索引来查找连接的问题:连接已经存在。
在图中,边缘直接指向其端点顶点。无需阅读表格,也无需构建额外的索引结构。虽然单个连接的速度差异可能不大,但当您需要在连接链中重复执行此操作并且需要连接多个数据记录时,例如整个表时,图可能会快上百倍。例如,假设我们想回答这个问题:“查找由购买了您刚刚购买的物品的人也购买的物品。” 图 1-5 正是这样显示的,其中“您”是人员 A:
-
人员 A 购买了物品 1。
-
人员 B、C 和 D 也购买了物品 1。
-
人员 B、C 和 D 也购买了物品 2、3、4 和 5。

图 1-5. Person-Purchase-Item 数据库的图结构
这是一个三跳查询,对于图来说相当简单。我们总共遍历了 9 个顶点和 11 条边来回答这个问题。
在基于表的系统中,这将需要三个表连接。良好的查询优化和索引将减少工作量,使其接近非常高效的图模型,但代价是在数据表和索引之间来回跳转,并执行索引查找。图不需要为此查询建立索引,因为连接已经内置并优化。
一个警示:完全的性能优势仅在“本地”图上实现,这些图是从头开始设计的。可以在表格数据库之上构建图系统。这种组合会像图一样运作,但性能不会像图那样高效。
图分析与机器学习
图结构化数据最大的好处也许在于它如何改进分析结果和性能。我们收集和存储数据有许多原因。有时我们只想精确地回忆一段特定的信息,就像它被记录下来的那样。例如,信用卡公司记录了每一笔交易。每个月,它会向您发送一份对账单,列出每一笔交易和付款。数据表对于这种简单的列表和求和就足够了。
当今企业需要利用数据不仅限于基本任务。他们需要发现并利用更多的收入机会,减少欺诈和浪费,降低风险。在数据中看到模式可以帮助满足所有这些需求。例如,你的信用卡消费模式如何随时间变化?他们是否能够将你与具有相似模式的其他人分类?企业如何利用社交网络关系来服务其利益,比如通过推荐促进业务或基于家庭关系预测行为?企业从多个来源获取客户信息。数据差异如拼写错误、允许字符的不同、姓名或地址变更以及客户有意使用不同的在线角色,这些差异使他们看起来像是多个不同的人。企业能否利用分析技术检测并整合这些记录?你是否在进行信用卡欺诈,或者有人盗用了你的卡号?
分析就是看见模式。模式是按照某种方式结构化的关系集合,这也正是图的本质。一个模式可以有结构化和数量化的两个方面,比如“每个家庭平均有 1.4 只宠物”。结构部分(定义家庭的住房关系,以及某些动物与家庭之间的关系)可以被编码为图模式查询。当然,图数据库和图分析平台也可以进行数量分析。本书的第二部分将帮助您理解和应用图分析。
增强型机器学习
机器学习是利用过去的数据来检测可能有助于预测未来活动的模式。由于图是表示、存储和分析模式的自然方式,因此图有助于我们做出更好的预测是合理的。
传统的监督式机器学习对数据进行了一些假设,简化了分析,并且与表格数据很好地配合。首先,我们假设每个数据点是孤立存在的:数据集中的每条记录在统计上是相互独立的。其次,我们假设在创建监督式机器学习模型时,数据点是同分布的。因此,我们认为每个样本来自相同的分布。这两个假设的概念被称为独立同分布(i.i.d.)。然而,现实生活中,并不总是符合独立同分布的现象。
要获得最准确的机器学习模型,我们需要考虑数据点之间的关系。例如,在建模社交网络时,与其他人有共同朋友的人更有可能互相联系。图允许我们明确利用数据点之间的关系,因为我们建模了这些关系而不仅仅是独立的节点。
图数据可以通过多种方式改进机器学习。一种方法是使用选定的图算法或其他图查询来评估数据点(顶点)的关系特性。例如,在交易图中,PageRank 分数帮助预测欺诈者。您可以利用这些基于图的特征丰富您现有的特征集,同时保留现有的模型训练方法。
与传统的机器学习方法不同,在特征工程阶段设计和手动选择特征,您可以通过学习图的结构自动生成特征。这种所谓的图表示学习减轻了对特征工程的依赖。它不像依赖分析师领域知识来设计有意义的特征,而是遵循数据驱动的方法。图表示学习有两种风格:嵌入和图神经网络。嵌入技术为每个数据点产生关联的向量。我们可以将这些嵌入向量传递到任何下游机器学习算法中,以包含它们在我们的预测任务中。图神经网络(GNNs)类似于传统神经网络,但在训练过程中考虑了图的连接。可以说,GNNs 做其他神经网络所做的事情,但具有更好的潜力。本书的第三部分专注于图增强机器学习。
章节总结
在本章中,我们学习到图是一个由顶点和连接这些顶点的边组成的抽象数据结构。图使我们能够将数据连接在一起,比关系数据库更好地发现模式和社区。边比表连接表现更好,因为边直接连接顶点到它们的端点,无需读取表格并构建额外的索引结构。
图分析非常强大,因为它高效地探索和识别数据中的模式。图分析可以提高分析性能,并发现其他方法未曾发现的内容。
最后,我们已经看到,图结构化数据帮助我们使用机器学习模型做出更好的预测。图允许我们明确地利用数据点之间的关系,使我们的模型更接近我们正在研究的自然现象。以这种方式建模关系将使我们能够学习图表示,从而自动从图中生成特征,而不是在特征工程阶段手工选择特征。
在下一章中,我们将扩展您对图概念和术语的理解,并帮助您开始通过图形形状的透镜看世界。
¹ “杀手级应用”,维基百科,最后更新于 2023 年 5 月 14 日,https://zh.wikipedia.org/wiki/%E6%9D%80%E6%89%8B%E7%BA%A7%E5%BA%94%E7%94%A8
第一部分:建立联系
第二章:连接和探索数据
在第一章中,我们展示了图分析和机器学习在人类和商业活动中的潜力,并且我们提出分三个阶段介绍细节:连接数据的力量,图分析的力量以及图机器学习的力量。在本章中,我们将深入探讨第一个阶段:连接数据的力量。
在我们深入探讨连接数据的强大之前,我们需要先打下一些基础。我们从介绍图数据模型的概念和术语开始。如果你已经熟悉图,你可能想略过这一部分,确保我们在术语上达成一致。除了图本身,我们还将涵盖图模式和图遍历的重要概念。遍历是我们在图中搜索数据和连接的方式。
在此过程中,我们还将讨论图与关系数据库之间的区别,以及如何使用图分析提出问题和解决问题,在关系数据库中是不可行的。
从对图的基础理解开始,我们进一步展示图的力量,通过展示图数据提供比表格数据更多洞察力和分析能力的六种方式。
完成本章后,您应该能够:
-
使用描述图的标准术语
-
理解图模式和图实例之间的区别
-
从头或从关系数据库模型创建基本图模型或模式
-
应用“遍历”这个隐喻来搜索和探索图数据
-
理解图数据赋予您知识和分析能力的六种方式
-
阐述实体解析问题,并展示图如何解决这个问题
图结构
在第一章中,我们向您介绍了图的基本概念。在本节中,我们将更深入地讨论。首先,我们将确立本书其余部分将使用的术语。然后,我们将更多地讨论图模式的概念,这是制定数据结构计划和意识的关键。
图论术语
假设您正在组织关于电影、演员和导演的数据。也许您在 Netflix 或其他流媒体服务公司工作,或者您只是一位影迷。
让我们从一个电影开始,星球大战:新希望,它的三位主要演员以及它的导演。如果你在关系数据库中构建这个,你可以在单个表中记录这些信息,但是表会迅速增长并变得难以管理。我们甚至如何记录一部电影的细节,50 名演员出演以及每位演员职业生涯的细节,都放在一个表中呢?
在设计关系数据库的最佳实践中,建议将演员、电影和导演分别放入单独的表中,但这也意味着要添加交叉引用表来处理演员与电影之间以及电影与导演之间的多对多关系。
因此,总共需要五个表来在关系数据库中表示这个例子,就像图 2-1 中所示。
将不同类型的内容分别存储在不同的表中是组织数据的正确答案,但是要查看一条记录如何与另一条记录相关联,我们必须重新连接数据。一个查询,比如询问哪些演员与哪些导演合作,将涉及在内存中构建一个临时表,称为连接表,其中包括您调用的所有表的所有可能组合的行,这些行满足查询的条件。连接表在内存和处理器时间方面都是昂贵的。

图 2-1. 一个简单电影数据库的关系表的图示
正如我们从图 2-2 中看到的那样,在这个连接表中有很多冗余数据。对于非常大或复杂的数据库,您希望考虑优化连接表的方式来组织数据和查询。

图 2-2. 从关系数据库查询创建的临时表,显示三位演员如何通过电影星球大战与乔治·卢卡斯相关联
然而,如果我们将其与图表方法进行比较,正如图 2-3 所示,一个事情立即变得清楚:表格和图表的区别在于,图表可以直接显示一个数据元素如何与另一个相关联。也就是说,数据点之间的关系已经构建到数据库中,不需要在运行时构建。因此,图表和关系数据库之间的一个关键区别是,在图数据库中,数据点之间的关系是显式的。

图 2-3. 显示关于星球大战的基本信息的图表
每个演员、电影和导演被称为节点或顶点(复数:vertices)。顶点代表事物,无论是物理的还是抽象的。在我们的例子中,图有五个顶点。顶点之间的连接称为边,描述了顶点之间的关系。边也被视为数据元素。这个图有四条边:三条用于演员展示他们如何与电影相关(acted_in),以及一条用于导演展示他们与电影的关系(directed_by)。在其最简单的形式中,图是顶点和边的集合。我们将使用通用术语对象来指代顶点或边。
使用这个图,我们可以回答一个基本问题:哪些演员与导演乔治·卢卡斯一起工作过?从乔治·卢卡斯开始,我们查看他执导的电影,包括星球大战,然后我们查看该电影中的演员,包括马克·哈米尔、凯丽·费雪和哈里森·福特。
区分边的方向可能是有用的,甚至是必要的。在图数据库中,边可以是有向或无向的。有向边具有特定的方向性,从源顶点到目标顶点。我们将有向边画成箭头。
通过添加有向边,我们还可以显示层次结构,即帝国反击战是星球大战的续集(图 2-4)。

图 2-4. 带有有向边的多电影图。这展示了我们如何通过添加额外的电影和制作人员逐步构建数据库。请注意有向边is_sequel_of,它提供了背景,显示帝国是星球大战的续集而不是反过来。
要对图进行更多有用的工作,我们需要为每个顶点或边添加更多细节,比如演员的出生日期或电影的流派。
本书描述了属性图。属性图是一种图,其中每个顶点和每条边都可以具有提供有关个别元素详细信息的属性。如果我们再次看关系数据库,属性就像表中的列。属性使得图变得真正有用。它们为数据添加了丰富性和上下文,这使我们能够开发更加细致的查询,提取我们所需的数据。图 2-5 展示了带有一些额外功能的星球大战图。

图 2-5. 带有属性的图
图形为我们建模属性提供了另一种选择。与其将电影类型作为电影的属性处理,我们可以将每种类型单独建模为一个独立的顶点。为什么这么做呢?当属性是分类的时候,我们预计会有许多其他顶点具有相同的属性值(例如,有许多科幻电影)。所有的科幻电影都将链接到Sci-fi顶点,这样就非常容易搜索它们或收集关于它们的统计信息,比如“最卖座的科幻电影是什么?”所有非科幻电影已经被过滤掉了。图结构不仅可以模拟您的核心数据,还可以充当搜索索引。
我们希望将属性建模为顶点的另一个原因是为了改善规范化或数据的丰富性。规范化是一种分解表以消除冗余和更新复杂性的方法。此外,将其分解为更多的顶点类型意味着我们有更多可以拥有属性的东西。
在我们的电影数据库示例中,我们可能希望创建一个名为Character的新类型的顶点,以便展示谁扮演了什么角色。图 2-6 展示了我们的星球大战图,增加了Character顶点。当然,有趣的是达斯·维达由两个人扮演:戴维·普罗斯(穿着装备)和詹姆斯·厄尔·琼斯(配音)。幸运的是,我们的数据库可以通过最小的修改来表示这个现实。

图 2-6. 带有Actor和Character类型的电影图。此模式的灵活性使我们能够轻松展示两位演员扮演同一角色。
我们可以用这个图做什么?嗯,它足够灵活,可以让我们添加几乎每个参与电影制作的人员——从导演和演员到化妆师、特效艺术家、主摄像师,甚至最佳助理。每个为电影做出贡献的人都可以用一个称为worked_on的边和一个称为role的边属性连接起来,其中可能包括director、actor、voice actor、camera operator、key grip等等。
如果我们扩展我们的数据库,包括成千上万部电影和所有参与其制作的人员,我们可以使用图算法回答像“某些导演最喜欢与哪些演员合作?”这样的问题。通过图数据库,您可以回答像“谁是科幻特效的专家?”或“某些导演最喜欢与哪些照明技术人员合作?”这样不那么明显的问题。对于销售图形软件或照明设备的公司来说,这些都是有趣的问题。
通过图数据库,您可以连接多个数据源,提取所需的顶点数据,并针对组合数据集运行查询。如果您可以访问用于各种电影项目的照明设备数据库,您可以将其连接到您的电影数据库,并使用图查询来询问哪些照明技术人员具有何种设备的经验。
表 2-1 总结了我们介绍的关键图术语。
表 2-1. 关键图术语表
| 术语 | 定义 |
|---|---|
| 图 | 用于表示连接数据并支持语义查询的顶点、边和属性的集合。 |
| 顶点^(a) | 用于表示对象或物体的图对象。复数形式:顶点。 |
| 边 | 连接两个顶点的图对象,通常用于表示两个对象或物体之间的关系。 |
| 属性 | 与顶点或边相关联的变量,通常用于描述它。 |
| 模式 | 包括顶点和边类型及相关属性的数据库计划,将定义数据的结构。 |
| 有向边 / 无向边 | 有向边表示一个具有明确语义方向的关系,从源顶点到目标顶点。无向边表示一个没有方向暗示的关系。 |
| ^(a) 另一个常用的替代名称是节点。这是个人偏好的问题。已经提出,下一代 ISO 标准的属性图查询语言将接受 VERTEX 或 NODE 的任一术语。 |
图模式
在前一节中,我们有意从一个非常简单的图开始,并通过添加更多的顶点、边和属性以及新的顶点和边类型来增加复杂性。要有效地对图进行建模和管理,特别是在商业环境中,规划数据类型和属性是至关重要的。
我们称这个计划为图模式,或图数据模型,类似于关系数据库的模式或实体-关系模型。它定义了我们的图将包含的顶点和边的类型,以及与这些对象相关联的属性。
你可以通过仅添加任意顶点和边来创建一个没有模式的图,但很快你会发现它很难处理,也很难理解。另外,例如,如果你想搜索数据中的所有电影,知道它们实际上都被称为“电影”,而不是“电影”或“电影”,将非常有帮助!
确定每种对象类型的标准属性集也是有帮助的。如果我们知道所有电影顶点都具有相同的核心属性,如标题、类型和发布日期,那么我们可以轻松自信地对这些属性进行分析。
图 2-7 展示了电影图数据库的可能模式。它系统地处理了随着数据库中电影增多而产生的几种数据复杂性。

图 2-7. 电影数据库的图模式
让我们来看看模式的特点:
-
Person顶点类型表示现实世界中的人物,如George Lucas。 -
Worked_on边类型连接一个Person到一个Movie。它有一个属性来描述人的角色:director、producer、actor、gaffer等。通过将角色作为属性,我们可以支持任意数量的角色,只需一个人员顶点类型和一个工作在电影上的边类型。如果一个人有多个角色,那么图可以有多个边。¹ 模式只显示每种类型的一个对象。 -
Character顶点类型与Person顶点类型分开。一个Person可能扮演多个Character(例如 Tyler Perry 在 Madea 系列电影中),或者多个Person可能扮演一个Character(例如 David Prowse、James Earl Jones 和 Sebastian Shaw 在 The Return of the Jedi 中扮演 Darth Vader)。 -
Movie顶点类型非常直观。 -
Is_sequel_of是一个有向边类型,告诉我们源头Movie是目标Movie的续集。 -
如前所述,我们选择将电影的
Genre建模为一个顶点类型,而不是作为属性,以便更容易通过流派对电影进行筛选和分析。
理解模式的关键在于拥有一致的对象类型集合,使得数据更易于解释。
遍历图
遍历图是搜索图和收集分析数据的基本隐喻。想象图像为一组互相连接的踏脚石路径,其中每个踏脚石代表一个顶点。有一个或多个代理人在访问图。要读取或写入一个顶点,代理人必须站在其踏脚石上。从那里,代理人可以跨越边缘到达相邻的石头/顶点。从其新位置,代理人可以再次迈出一步。记住:如果两个顶点直接连接,这意味着它们之间有关系,因此遍历是跟随关系链。
跳跃和距离
遍历一个边也称为进行一次跳跃。遍历图的类比是移动游戏板上的移动,就像 图 2-8 中所示的那样。图是一个奇特的游戏板,你遍历图就像你在游戏板上移动一样。

图 2-8. 遍历图就像在游戏板上移动
在许多棋盘游戏中,轮到你时,你要掷骰子来确定要走多少步或跳数。在其他游戏中,你可能要穿越棋盘,直到到达某种类型的空间。这与搜索特定顶点类型的图形遍历完全相同。
图跳跃和距离在其他现实世界的情况中也会出现。你可能听说过“六度分隔”的说法。这指的是相信每个美国人都通过至多六次关系连接到其他每个人。或者,如果你使用 LinkedIn 商业社交应用程序,你可能已经看到,当你查看一个人的个人资料时,LinkedIn 会告诉你他们是否直接与你连接(一跳),通过两跳或三跳。
在图数据库中进行搜索也是如何进行搜索的。有两种基本方法:要么在继续到下一级邻居顶点之前访问每个邻居顶点(广度优先搜索),要么在尝试备选路径之前,沿着单条连接链到达尽头(深度优先搜索)。我们将在第六章中详细介绍这些搜索类型。
广度和深度
有两种基本方法可以系统地遍历图形以进行搜索。广度优先搜索(BFS)意味着在继续到下一级邻居之前,访问您的每个直接邻居,下一级邻居,依此类推。具有并行处理能力的图数据库可以通过同时进行多个遍历来加速 BFS。
深度优先搜索(DFS)意味着在回溯尝试其他路径之前,沿着单条连接链尽可能远地进行。无论是 BFS 还是 DFS,最终都将访问每个顶点,除非因为找到了所需的内容而停止。
图模型
现在你知道了图和图模式是什么。但是如何设计出一个好的图模型呢?
首先要问自己这些问题:
-
我关心的主要对象或实体是什么?
-
我关心的关键关系是什么?
-
我想要过滤掉的实体的主要属性是什么?
模式选项与权衡
正如我们所看到的,良好的图模式设计以自然的方式表示数据和关系,使我们能够遍历顶点和边,就像它们是真实世界的对象一样。与任何一组真实世界的东西一样,我们可以以许多方式来组织我们的集合,以优化搜索和提取所需的内容。
在设计图数据库时,影响设计的两个考虑因素是我们输入数据的格式和我们的查询用例。正如我们将在本节中看到的那样,一个关键的权衡是我们是想要优化模式以节省内存,还是使查询运行更快。
顶点、边还是属性?
如果您将表格数据转换为图形,自然的做法似乎是将每个表转换为顶点类型,将每个表列转换为顶点属性。实际上,一列可以映射到顶点、边缘、顶点的属性或边缘的属性。
实体和抽象概念通常映射到顶点,您可以将它们视为名词,例如前面示例中的movie或actor。关系通常映射到边缘,您可以将它们视为动词,例如directs或acts。描述符类似于形容词和副词,根据上下文和查询用例,可以映射到顶点或边缘属性。
乍一看,将对象属性尽可能靠近对象存储(即作为属性)似乎会提供最优的解决方案。然而,考虑一个需要优化产品颜色搜索的用例。颜色通常是作为顶点属性找到的质量,但是搜索蓝色对象将需要查看每个顶点。
在图中,您可以通过定义一个称为color的顶点类型并通过无向边缘将color顶点和product顶点连接起来来创建搜索索引。然后,要查找所有blue对象,您只需从color顶点blue开始,并找到所有链接的product顶点。这可以加快查询性能,但折衷是增加了复杂性和内存使用。
边缘方向性
较早时我们介绍了边缘方向性的概念,并指出您可以在设计模式中将边缘类型定义为有向或无向。在本节中,我们将讨论每种类型的优缺点。我们还将讨论 TigerGraph 数据库中的混合选项。
这是如此有用,以至于您可能认为您可以一直使用它,但在计算的所有事物中,您在边缘类型选择时都会有优缺点。
无向边缘
连接任意两个定义类型的顶点,没有方向性暗示。它们易于在创建链接时使用,并且在任何方向上遍历也很容易。例如,如果用户和电子邮件地址都是顶点类型,您可以使用无向边缘找到某人的电子邮件,但也可以找到使用相同电子邮件地址的所有用户——这是使用有向边缘无法实现的。
无向边缘的折衷之处在于它不提供层次结构等上下文信息。例如,如果您有一个企业图,并且想找到母公司,您无法使用无向边缘,因为没有层次结构。在这种情况下,您需要使用有向边缘。
有向边缘
表示具有明确语义方向的关系,从源顶点到目标顶点。有向边的好处是它提供更多的上下文信息,很可能比无向边更高效地存储和处理。然而,需要权衡的是,如果需要,您无法向后追溯。
与逆向有向边配对
如果您定义两种有向边类型,分别表示两个方向,您可以同时享有方向语义的优点以及双向遍历的便利。例如,要实现一个家谱,您可以定义一个child_of边类型来向下遍历树,以及一个parent_of边类型来向上遍历树。然而,需要权衡的是,您需要维护两种边类型:每次插入或修改一个边时,您都需要插入或修改其配对边。TigerGraph 数据库通过允许您一起定义这两种类型,并编写处理这两种类型数据摄入作业来简化这一过程。
如您所见,您选择的边类型将受到您需要运行的查询类型以及操作开销(如内存、速度和编码)的影响平衡。
提示
如果源顶点和目标顶点类型不同,例如Person和Product,通常可以选择无向边,并让顶点类型提供方向上下文。只有当两个顶点类型相同且您关心方向时,您必须使用有向边。
边类型的粒度
您需要多少不同的边类型,并且如何优化您对边类型的使用?理论上,您可以只有一种边类型——无向边类型,它连接您模式中的每种顶点类型。其好处是简单性——只需要记住一种边类型!然而,需要权衡的是,您需要边属性的数量以获取上下文,并且查询性能可能会变慢。
在另一个极端,您可以为每种关系类型定义不同的边类型。例如,在社交网络中,您可以为coworker、friend、parent_of、child_of等关系分别定义不同的边类型。如果您只想查找一种关系类型,比如专业网络,这种遍历方式非常高效。然而,需要权衡的是,您需要定义新的边类型来表示新的关系类型,并且代码的抽象性会降低——也就是说,代码复杂性会增加。
建模交互事件
在许多应用程序中,我们希望跟踪实体之间的交互,例如一个财务交易,其中一个财务账户向另一个账户转移资金。您可能会考虑将交易(资金转移)表示为两个Account顶点之间的边。如果发生多次,会有多个边吗?虽然这看起来很容易构想(图 2-9),但在数学理论和现实世界的数据库领域中,并非如此简单。

图 2-9. 多个事件表示为多条边
在数学中,对于给定顶点对之间有多条边的情况,超越了普通图的定义,进入了多边和多重图的范畴。由于这种复杂性,并非所有的图数据库都支持这种情况,即使支持,也没有一个方便的方法来引用组中的特定边。另一种处理方法是将每个交互事件建模为一个顶点,并使用边将事件与参与者连接起来(图 2-10a)。将事件建模为顶点提供了最大的灵活性,可以将其链接到其他顶点并设计分析。第三种方法是在两个实体之间创建单条边,并将所有交易聚合到边属性中(图 2-10b)。

图 2-10. 多个事件的两种替代建模方法:(a) 事件作为顶点,和 (b) 带有包含事件发生列表的单条事件边
表 2-2 总结了每种方法的利弊。最简单的模型并不总是最佳选择,因为应用要求和数据库性能问题可能更加重要。
表 2-2. 比较多次交互建模选项
| 模型 | 利益 | 权衡 |
|---|---|---|
| 多条边 | 简单模型 | 数据库支持并非普遍 |
| 与相关顶点链接的顶点 | 基于顶点属性的过滤 分析社区和事件相似度的易用性
高级搜索树集成 | 使用更多内存 需要更多步骤来遍历 |
| 带有记录事件详细信息的单条边属性 | 使用更少的内存 减少用户之间遍历的步骤 | 交易搜索效率较低 更新/插入属性较慢 |
|---|
根据用例调整设计模式
假设您正在创建一个用于跟踪 IT 网络事件的图数据库。我们假设您需要以下顶点类型:event、server、IP、event type、user和device。但是您想要分析什么关系,需要什么边呢?设计将取决于您希望关注的内容,您的模式可以以事件为中心或以用户为中心。
对于以事件为中心的模式(图 2-11a),其关键优势在于所有相关数据距离event顶点只有一个跳数。这使得查找事件社区、找到处理特定类型事件最多的服务器以及查找被任何给定 IP 访问的服务器变得简单。然而,从用户的角度来看,用户距离device或IP顶点有两个跳数的权衡。

图 2-11. 安排相同顶点类型的两种选项:(a)以事件为中心,(b)以用户为中心
我们可以通过使我们的模式以用户为中心来解决这个问题,但这样会使事件与 IP 和服务器之间的距离增加到两个跳数,并且事件类型与设备、服务器和 IP 之间的距离增加到三个跳数(图 2-11b)。然而,这些缺点可能值得权衡,因为可以进行有用的以用户为中心的分析,例如查找与给定用户共享相同设备/IP/服务器的所有用户或者对被阻止的用户进行分析,试图预测谁应该被进一步阻止。
将表转换为图形
您并不总是从头开始创建图数据库。通常,您会获取已经存储在表中的数据,然后将数据移动或复制到图中。但是,您应该如何将数据重新组织成图形呢?
将数据从关系数据库迁移到图数据库是将表和列映射到图数据库架构的过程。为了将数据从关系数据库映射到图数据库,我们在列和图对象之间创建一一对应关系。表 2-3 展示了将银行交易数据从关系数据库映射到图数据库的简单示例。
表 2-3. 将关系数据库中的映射表映射到图数据库中的顶点、边和属性的示例
| 源:关系数据库 | 目标:图数据库 |
|---|---|
表:Customers—包括 customer_id, first_name, last_name, DOB 等多列 |
顶点类型:Customer—对应的属性为 customer_id, first_name, last_name, DOB |
表:Banks—列 bank_id, bank_name, routing_code, address |
顶点类型:Bank—属性 bank_name, routing_code, address |
表:Accounts—列 bank_id, customer_id |
顶点类型:Account—属性 bank_id, customer_id |
| 表:Transactions—列source_account、destination_account、amount | 顶点类型:Transaction—属性source_account、destination_account、amount 或
有向边:Transaction—属性source_account、destination_account、amount |
图模式如 图 2-12 所示。

图 2-12. 一个简单的银行数据库的图模式,交易作为单独的顶点
在创建数据模式时的一个关键决策是决定哪些列需要映射到它们自己的顶点。例如,人们通常是理解任何现实情况的关键因素——无论他们是客户、员工还是其他人——因此它们通常会映射到它们自己的顶点。
理论上,关系数据库中的每一列都可以成为模式中的一个顶点,但这是不必要的,并且很快变得难以管理。与构建关系数据库结构类似,优化图数据库涉及理解数据的真实结构及其使用意图。
在图数据库中,关系数据库的关键列变成顶点,而上下文或支持数据成为这些顶点的属性。边通常映射到外键和交叉引用表中。
一些图数据库具有工具,可以便于导入表和将外键映射到顶点和边的 ID。
与关系数据库一样,结构良好的图数据库消除了冗余或重复的数据。这不仅确保了计算资源的有效使用,而且更重要的是,通过确保数据在不同位置不以不同形式存在,确保了数据的一致性。
优化映射选择
简单地将列映射到顶点和顶点属性可能有效,但可能无法充分利用图中可用的连接丰富性,实际上通常需要根据不同的搜索用例调整映射选择。
例如,在联系人数据库的图数据库中,手机号码和电子邮件地址是个人的属性,通常作为该顶点的属性表示。
但是,如果您试图使用银行应用程序来检测欺诈,您可能希望将电子邮件地址和电话号码视为单独的顶点,因为它们对于链接人员和金融交易非常有用。
当来自多个表的信息映射到一个顶点或边类型时,这并不少见。当数据来自多个源时,每个源提供了同一现实世界实体的不同视角时,这种情况尤其常见。同样,一个表可以映射到多个顶点和边类型。
模型演变
大多数情况下,您的数据会随着时间的推移而发展,您将需要调整模式以考虑新的业务结构和外部因素。这就是为什么模式设计为灵活的:允许系统随时间适应,而无需从头开始。
例如,如果我们看看银行业,金融机构不断进入新市场,无论是通过地理扩展还是引入新类型的产品。
举个简单的例子,假设我们有一家一直在单一国家运营的银行。因此,其所有客户的居住国家是隐含的。然而,进入第二个国家将需要更新数据库以包括国家数据。可以为每个相关的顶点类型添加国家属性,或者创建一个名为country的新顶点类型,并为银行在每个国家的运营地创建顶点。
使用灵活的模式,可以通过添加新的顶点类型然后将顾客顶点链接到新的国家顶点来更新模式。
尽管这只是一个简单的例子,但它展示了建模数据可以是一个进化过程。您可以从一个初始模型开始,也许它与之前的关系数据库模型非常相似。在使用图形数据库一段时间后,您可能会发现一些模型更改可以更好地满足您的需求。两个常见的更改是将顶点属性转换为独立的顶点类型,以及添加额外的边类型。
适应不断变化的数据可以很简单。添加属性、顶点类型或边类型很容易。连接两个不同的数据集也很容易,只要我们知道它们之间的关系。我们可以添加边缘来连接相关实体,甚至可以合并来自代表同一现实世界实体的两个来源的实体。
图力量
我们已经看到如何构建图形,但需要回答的最重要的问题是为什么构建图形?有什么优势?图形可以为您做到其他数据结构无法做到的事情是什么?我们称之为图形技术的集合能力和优势为图形力量。
接下来是图力量的关键方面。我们谦逊地承认,这既不是完整的,也不是可能的最佳清单。我们怀疑其他人提出了更完整、数学上更精确的清单。然而,我们的目标不是提出理论,而是建立人类之间的联系:将 resonance 的想法分享给您,让您能够理解和体验图形力量。
连接点
图形构成了可操作的知识体系。
正如我们所见,连接点是图形力量在其最基本的层面上。无论是将演员和导演与电影联系起来,还是将金融交易与涉嫌欺诈者联系起来,图形都能让您描述一个实体与另一个实体之间的关系跨越多个跳跃。
图的力量来自于能够描述连接网络、检测模式并从这些模式中提取智能。虽然单个顶点可能不包含我们正在寻找的智能,但它们一起可能使我们能够发现在多个顶点之间关系的模式,从而揭示新信息。
有了这些知识,我们可以开始从数据中推断和预测,就像侦探在调查谋杀案时连接线索一样。
在每个侦探故事中,调查员收集了一系列事实、可能性、线索和怀疑。但这些孤立的碎片并非答案。侦探的魔力在于将这些碎片缝合成隐藏的真相。他们可能利用已知或可疑的连接模式来预测他们未被告知的关系。
当侦探解决了谜团,他们可以展示一系列或网络的连接,将嫌疑人与犯罪联系起来,包括手段、机会和动机。他们也可以表明,对于任何其他嫌疑人,不存在足够强大的连接序列。
那些侦探知道他们在进行图分析吗?可能不知道,但我们每天都在不同方面的工作、家庭或朋友网络中做这种工作。我们不断地连接各种线索,以理解人与人、人与物、人与思想等之间的联系。
图作为一种数据范式的力量在于,它密切地模拟了这一过程,使得图的使用更加直观。
360 度视角
360 度图形视图消除了盲点。
各种规模的组织都在抱怨它们的数据孤岛。每个部门都希望另一个在需要时提供其数据,同时却未意识到自身无法在同样的基础上开放。问题在于,商业流程及其支持的系统积极阻碍了数据的开放共享。
例如,两个部门可能使用两种不同的数据管理系统。尽管两者都可能将数据存储在关系型数据库中,但每个系统的数据架构对另一个系统来说却是如此陌生,以至于几乎没有希望将它们链接起来以实现共享。
如果你从微观的角度来看待问题,问题可能并不明显。例如,如果你正在为客户 X 编制记录,那么了解存储客户数据的两个系统的分析师将能够轻松地从两个系统中提取数据,手动合并或协调两条记录,并呈现客户报告。但是,当你需要重复这个过程十万次或百万次时,问题就来了。
只有通过全面、整合的方式共享数据,企业才能消除阻碍其全面了解的局限性。
“客户 360”这个术语描述了一种数据架构,将来自多个来源和领域的客户数据汇集到一个数据集中,从而使您能够全面和全面地了解每位客户。
使用关系数据库,最明显的解决方案是将这两个部门数据库合并为一个。许多企业尝试过大规模的数据整合项目,但通常以失败告终,因为虽然合并数据带来了显著的好处,但也需要进行权衡,这导致丢失上下文细微差别和功能性。让我们面对现实:通常有一个原因,为什么某个软件包的创建者选择以特定方式构建其数据模式,并试图迫使其符合另一个系统的模式或新的混合模式,将至少破坏其中一个系统。
图形允许您以自然直观的方式连接数据库,而不会干扰原始表。首先授予图形应用程序访问每个数据库的权限,然后创建一个图形模式,以逻辑方式链接每个数据库中的数据点。图形数据库映射数据点之间的关系并进行分析性的重活,使源数据库可以继续完成它们之前的工作。
如果你想看到你的全部环境,你需要一个可以俯瞰每个角度的视野——即 360 度。如果你想理解你的整体业务或操作环境,你需要跨越所有你知道的数据之间的数据关系。
这是我们将在第三章更深入地研究的内容,我们将展示一个涉及客户旅程的用例。
在前两点中,我们已经看到如何设置数据,现在在接下来的四点中,我们将探讨如何从中提取有意义的智能。
深入探索更多洞察力
在图形中深入搜索可以揭示大量连接信息。
20 世纪 60 年代,斯坦利·米尔格拉姆进行的“六度分离”实验表明,仅通过追踪个人联系(并知道目标人物在波士顿),在内布拉斯加州奥马哈随机选择的人可以通过不超过六个人际关系连接到神秘的目标人物。
自那时以来,更严格的实验表明,许多图形都是所谓的小世界图形,意味着源顶点可以在非常少的跳数内到达数百万甚至数十亿其他顶点。
在社交图形中不仅如此,在知识图形中也能够在很少的跳数内访问如此多的信息。访问这么多信息的能力,并理解这些事实之间的关系,无疑是一种超能力。
假设你有一个图,其中包含两种类型的顶点:人物和专业领域,就像 图 2-13 中的那样。该图展示了你认识谁以及你擅长什么。每个人的直接连接代表了他们头脑中的内容。

图 2-13. 展示了谁认识谁及其专业领域的图表
从这里,我们很容易看出 A 是两个专家,天文学和人类学,但通过询问 B 和 C 知道什么,再走一步,A 就可以接触到四个更多的专业。
现在,假设每个人都有 10 个专业领域和 100 个人际关系。考虑一下你的朋友的朋友能联系到多少人和多少专业领域。有 100 × 100 = 10,000 个人际关系,每个关系涉及 10 个专业领域。有可能独特的人数并不是 10,000 —— 你和你的朋友可能认识一些相同的人。尽管如此,在图中的每一步中,你都会接触到指数级增长的信息量。寻找答案?想进行分析?想要理解全局?四处打听一下,你会找到认识认识的人。
我们经常谈论“更深入地看”,但在图表中,这意味着一种特定的方式来搜索信息,并理解这些事实如何相关。更深入地看包括基于广度的搜索,考虑从当前位置可访问的内容。然后遍历到一些邻近顶点以获取深度,并查看从这些新位置可访问的内容。无论是进行欺诈调查还是优化决策,深入图表中查找可以揭示出否则未知的事实和关联。
正如我们在 “连接点” 中所看到的,单独一个关系可能并不引人注目,并且可能没有任何信息显示一个给定顶点中的坏意图,但是成千上万的顶点和边被综合考虑时,可以开始揭示新的见解,进而导致可操作的智能。
观察和发现模式
图表现出一个新的视角,揭示了易于解释的隐藏数据模式。
正如我们所看到的,图是一组顶点和边,但在顶点和关系的集合内部,我们可以开始检测到模式。
一个图模式是一小组连接的顶点和边,可以作为搜索具有类似配置的顶点和边组的模板。
最基本的图模式是数据三元组:顶点 → 边 → 顶点。数据三元组有时被认为是语义关系,因为它与语言的语法相关,并可以读作“主语 → 谓语 → 宾语”。例如,Bob → 拥有 → 船。
我们还可以使用图形模式描述我们心中的高级对象或关系。例如,根据模式,一个人可以与包含地址、电话和电子邮件等个人数据的多个顶点相关联。虽然它们是单独的顶点,但它们都与那个人相关。另一个例子是洗售,这是两个证券交易的组合:以亏损出售证券,然后在 30 天内购买相同或基本相似的证券。
图案有不同的形状。我们已经看过的最简单的图案是两个顶点之间的线性关系跨越一系列跳跃。另一个常见的模式是星形:许多边和顶点从一个中心顶点辐射出去。
图案可以是 Y 形的,当两个顶点汇合在第三个顶点上时,就会出现这种模式,这个第三个顶点与第四个顶点相关。我们还可以有圆形或递归模式等等。
与关系数据库相比,图形数据易于可视化,图形数据模式易于解释。
设计良好的图形为顶点和边类型命名,反映它们的含义。如果正确完成,您几乎可以查看一系列连接的顶点和边,并像读句子一样阅读这些名称。例如,请参考图 2-14,显示了购买人员的物品。

图 2-14. 购买物品 1 及其它商品的人
从左边开始,我们看到 A(您)购买了物品 1。向右移动,我们看到另一组人,B、C 和 D,他们也购买了物品 1。最后,我们看到这些人购买了更多物品。因此,我们可以说:“您购买了物品 1。其他购买物品 1 的人也购买了物品 2、3、4 和 5。”听起来熟悉吗?
进一步分析表明,物品 4 是最受欢迎的物品,三位购买者中的所有人都购买了该物品。物品 3 紧随其后(被两人购买),物品 2 和 5 最不受欢迎。有了这些信息,我们可以优化我们的推荐。
许多零售商使用图形分析进行推荐分析,通常还会更深入,根据其他客户属性(如性别、年龄、地点和时间)对购买进行分类。如果我们发现客户晚上更有可能购买奢侈品,早上更倾向于购买实用品,我们甚至可以根据每天的时间来做出推荐。
如果我们还分析购买顺序,我们可以得出关于客户的一些高度个人化的信息。一家大型零售商曾经通过关注 25 种产品的购买情况来准确判断哪些客户怀孕及预产期。然后,该零售商能够在客户孩子出生时发送定向的促销优惠。
匹配和合并
图形是最直观和高效的匹配和合并记录的数据结构。
正如我们之前讨论的,组织希望全方位了解他们的数据,但这背后的一个大障碍是数据的不明确性。数据不明确性的一个例子是有多个版本的客户数据,去重数据的挑战对许多组织而言是众所周知的。
重复有时是企业系统的繁殖导致的,这些系统将您的客户视图分散到许多数据库中。例如,如果您在几个数据库中拥有客户记录——例如 Salesforce(客户服务数据库)、订单处理系统和会计软件包——客户的视图将分散在这些系统中。
要创建客户的联合视图,您需要查询每个数据库并将每个客户的记录连接在一起。
然而,并不总是那么简单,因为客户可能在您的数据库中使用不同的参考 ID 进行注册。姓名可能拼写不同。个人信息(姓名、电话号码、电子邮件地址等)可能会更改。如何匹配正确的记录呢?
实体解析根据被假定为唯一的实体属性来匹配记录。在个人记录的情况下,这些属性可能是电子邮件地址和电话号码,但也可以是属性的聚合。例如,我们可以将姓名、出生日期和出生地作为唯一标识符,因为全世界任何两个人拥有相同这三样东西的几率有多大呢?
在关系数据库中进行实体解析是具有挑战性的,因为为了比较实体,您需要比较相似的内容。如果您只处理一个表,您可以说相似的列中的相似值表示匹配,从而将两个实体解析为一个实体,但在多个表中,列可能不匹配。您可能还需要构建复杂的表连接以在分析中包含交叉参考的数据。
相比之下,在图中进行实体解析则较为容易。相似的实体共享相似的邻域,这使得我们能够使用诸如余弦相似度和 Jaccard 相似度等相似性算法来解析它们。
在实体解析中,我们实际上做了两件事:
-
通过测量其属性和邻域的相似度来计算实体对的匹配可能性分数。
-
合并具有足够高匹配可能性分数的实体。
当涉及到合并记录时,我们有几个选项:
-
将记录 B 的数据复制到记录 A,重定向原本指向 B 的边使其指向 A,并删除 B。
-
在记录 A 和 B 之间创建一个名为
same_as的特殊链接。 -
创建一个名为 C 的新记录,将数据从 A 和 B 复制过来,重定向从 A 和 B 指向 C 的链接,最后创建
same_as边从顶点 C 指向顶点 A 和 B。
哪个更好?第二种更快执行,因为只涉及一步操作——添加一条边——但图查询同样可以执行第一种和第三种选项。就结果而言,哪种选项更好取决于您的搜索用例。例如,您是更看重数据的丰富性还是搜索效率?它还可能取决于您在数据库中期望进行的匹配和合并程度。
我们将在 第十一章 中通过一个实例演示和讨论实体解析。
加权和预测
具有加权关系的图让我们能够轻松地建模和分析复杂的成本结构。
正如我们所展示的,图是分析关系的强大工具,但需要考虑的一点是关系不一定是二进制的、开或关、黑或白。边代表顶点之间的关系,可以加权表示关系的强度,例如距离、成本或概率。
如果我们对边进行加权,路径分析就不仅仅是追踪节点之间的链接,还需要进行计算工作,例如聚合它们的值。
然而,加权边也使得图分析变得更加复杂。除了计算工作之外,在具有加权边的图中找到最短路径算法上也比在无权图中更为困难。即使你已经找到了一条到达某个顶点的加权路径,你也无法确定它是否是最短路径。可能存在另一条你尚未尝试的更长路径,但其总权重较低。
另一方面,边的加权并不总是会导致工作量显著增加。在 PageRank 算法中,该算法计算每个顶点对所有其他顶点的影响时,边的加权几乎没有影响,只是从引用邻居接收的影响被边的权重乘以,这对算法增加了最小的计算开销。
有许多问题可以通过边的加权来解决。例如,与地图相关的任何事物都适合边的加权。每条边可以有多个权重。考虑地图示例,这些权重可以包括常数权重,如距离和速度限制,以及变量权重,如当前旅行时间,以考虑交通状况。
我们可以利用航线和价格的图表来计算乘客的最佳行程,不仅基于他们的行程安排,还考虑他们的预算限制。他们是要找最快的行程,不管价格,还是愿意接受更长的行程,可能有更多停留,以换取更低的价格?在这两种情况下,你可能会使用相同的算法——最短路径——但会优先考虑不同的边权重。
有了正确的数据访问权限,我们甚至可以计算成功旅程的概率。例如,我们的航班准时起飞和到达的概率是多少?对于单程跳跃,我们可能接受航班晚点不超过一个小时的 80%的机会,但对于两程跳跃,第二程不晚点的概率为 85%,延误的组合风险为 68%。
同样地,我们可以看一下供应链模型,并问一下,我们完成产品的生产会不会严重延误?如果我们假设有六个步骤,每个步骤的可靠性为 99%,那么组合可靠性约为 94%。换句话说,有 6%的机会出现问题。我们可以对数百个互连过程建模,并使用最短路径算法找到满足一系列条件的“最安全”路线。
章节总结
在本章中,我们看了图结构以及如何使用图数据库将数据表示为一系列数据节点和链接。在图中,我们称这些为顶点和边,它们不仅能够以直观的方式表示数据——并更有效地查询数据——还可以使用强大的图函数和算法遍历数据并提取有意义的智能信息。
属性图是一种图形,其中每个顶点和边(我们统称为对象)都可以持有描述该对象的属性。边的一个属性是方向,我们讨论了在指示层次结构和顺序时不同有向边类型的优缺点。
我们看了什么是遍历图以及“跳跃”和“距离”的含义。遍历图有两种方法:广度优先搜索和深度优先搜索,每种方法都有其自身的优点和权衡。
我们讨论了使用图模式定义数据库结构的重要性,一致的对象类型集使您的数据更易于解释,并且它能够与现实世界紧密相关。
在设计过程中,特别是在搜索用例和将关系数据库的列映射到图数据库时,仔细考虑了不同的方法,这会影响查询时间和编码的复杂性。
实施图数据库的关键步骤之一是将关系数据库中的列映射到图中,因为图的一个常见用例是在不同数据库之间建立关系。您需要做出的决策之一是将哪些列映射到它们自己的对象,以及哪些包含为其他对象的属性。
随着时间的推移,我们审视了数据库的演变,以及为什么灵活的模式对确保数据库保持最新状态至关重要。
在设计数据库模式时,无论是关系型数据库还是图数据库,都需要权衡利弊,我们探讨了其中一些,包括是否将列映射为对象或将其作为对象的属性的选择。我们还考虑了边的方向性选择和边类型的粒度。
记录同一两个实体之间的多个事件和跟踪 IT 网络中的事件也需要权衡取舍。
最后,我们探讨了“图力量”的含义,包括首先为什么使用图?我们看了一些常见的用例,包括:
-
连接各种知识点:图如何形成可操作的知识体系
-
360 度视角:如何通过 360 度图视图消除盲点
-
深入寻找更多见解:深度图搜索如何揭示大量连接信息
-
观察和发现模式:图如何呈现新的视角,揭示易于解释的隐藏数据模式
-
匹配和合并:为什么图是匹配和合并记录的最直观和高效的数据结构
-
权衡和预测:带有加权关系的图如何轻松建模和分析复杂的成本结构
¹ 一些图数据库通过一个名为Worked_on的单一边来处理多个角色,其角色属性接受角色列表。
第三章:更好地了解您的客户和业务:360 图
本章将运用一些真实的用例来说明我们在上一章讨论的六个图形功能中的两个:“连接点”和“360 视图”。图提供的 360 视图帮助企业和机构更全面地看待他们的数据,从而实现更好的分析能力。在第一个用例中,我们建立一个客户 360(C360)图,使公司能够跟踪和理解售前客户旅程。在第二个案例中,我们建立了一个药物相互作用 360 图,以便研究人员可以开发更安全的药物疗法。
完成本章后,您应能够:
-
定义术语C360并解释其价值主张
-
知道如何在图中建模和分析客户旅程
-
知道如何使用图分析来计数和过滤属性和关系
-
使用 GraphStudio 设置和运行 TigerGraph Cloud Starter Kit
-
阅读并理解基本的 GSQL 查询
案例 1:追踪和分析客户旅程
没有销售就没有业务。销售,无论是面向消费者(B2C)还是其他企业(B2B),不仅成为一门艺术,也成为一门科学。企业分析他们与潜在客户(潜在客户)从开始到结束的每个阶段的互动,希望最终能够实现销售。根据 Gartner 的数据,2018 年全球客户关系管理(CRM)软件支出增长了 15.6%,达到 2020 年的 482 亿美元。¹ Salesforce 已经确立了作为 CRM 软件市场领导者的地位,市场份额约为 20%。²
将销售过程视为潜在客户在时间上的一系列事件体验是考虑的关键方式。某人如何何时参与企业及其商品?绘制与销售前景的互动称为追踪客户旅程。
客户旅程模型是销售和营销的重要工具。首先,它采用客户的视角,因为他们是最终决策者。其次,通过意识到客户可能需要通过阶段,企业可以制定出他们认为将会吸引并确保许多成功业务交易的吸引力旅程。第三,通过查看个体旅程,我们可以看到它们的进展如何,一个旅程是否停滞不前,变慢或改变方向。第四,通过分析收集的旅程集合,企业可以看到模式和趋势,并将其与其目标行为进行比较。用户是否实际上按照设计的旅程进行?特定的参与是否成功推动潜在客户向前发展?
需要一个有效且可扩展的数据系统,可以收集客户旅程中的混合数据类型,并支持对个体和聚合旅程的分析。
解决方案:客户 360 + 旅程图
CRM 系统似乎提供了解决方案,但它们并没有完全满足企业对客户旅程分析的需求。设计用于手动输入数据或数字化摄取数据,CRM 主要以表格形式记录和呈现数据。挑战在于不同类型的参与行为(观看视频、参加演示、下载试用软件)具有不同的特征。将这种混合数据存储在一个表中并不奏效,因此数据必须分布在多个表中。真正的挑战在于建模旅程的顺序。遵循顺序的唯一方法要么是通过一系列昂贵的表连接,要么是筛选与某个人相关的所有参与行为,然后按时间排序这些参与行为。
另一方面,使用图形,我们可以直接使用边轻松建模顺序,如在图 3-1 中所示。所有潜在客户的旅程可以存储在一个图形中。个别旅程将彼此相似并交叉,因为个人参加相同的事件或从事类似的活动。

图 3-1. 客户旅程:一般阶段和一个特定客户的旅程的图形显示
企业不仅希望绘制客户旅程,还希望使其更成功:提高客户满意度,增加以销售结束的旅程比例,增加销售价值,缩短旅程。为了做到这一点,企业需要了解每个客户及其决策的背景。这就是 360 度视图发挥作用的地方。360 度视图是我们在前一章中讨论的图形的独特力量之一。
客户 360(C360)是通过集成多个来源的数据创建的客户(或任何感兴趣的实体)的综合视图,正如在图 3-2 中建议的那样。像客户旅程一样,客户 360 非常适合图形和图形分析。图形可以支持一个顶点(客户)与其他实体之间无限数量的关系。这些实体不仅可以描述旅程(冷呼叫、网络研讨会、宣传册、产品演示或网站互动),还可以描述客户的背景(当前和过去的职务头衔、任期、雇主、地点、技能、兴趣和教育)。一个好的 360 数据库还将包括关于雇主和行业的信息(规模、倡议、新闻等)。

图 3-2. 连接以形成客户 360 图的全面视图的单个个体信息
结合 360°数据和旅程分析,企业能够清晰地看到销售过程中发生的情况,无论是个体还是整体水平,了解这些行动的背景,看到哪里需要改进,并评估销售改进所带来的影响。
我们提议的解决方案是开发一个数据模型,使得检查和分析客户旅程变得简单。数据模型还应该包括描述和与客户相关的数据,以生成客户 360 视图。该模型应支持关于客户旅程包含或不包含哪些事件以及这些事件的时间的查询。
实施 C360 + Journey Graph:GraphStudio 教程
我们在下文中提供的 C360 和客户旅程图实施方案作为 TigerGraph Cloud Starter Kit 提供。起始套件是实践演示,教你如何使用图分析来处理不同的使用案例。每个套件都有一个图模式、示例数据和查询。如果这是你第一次使用 TigerGraph Cloud,不用担心。我们将向你展示如何注册一个免费账户并部署一个免费的起始套件。或者,如果你已经在自己的机器上安装了 TigerGraph,我们将告诉你如何将起始套件导入到系统中。然后,我们将同时指导你设计 C360 图和总体上的 GraphStudio。
图 3-3 描述了设置起始套件的两条路径。在接下来的章节中,我们将首先告诉你如何创建 TigerGraph Cloud 账户。然后我们将指导你完成获取和加载起始套件的步骤,首先是针对 TigerGraph Cloud 用户,然后是针对 TigerGraph 自建用户。

图 3-3. 设置 TigerGraph Starter Kit
创建 TigerGraph Cloud 账户
如果这是你第一次使用 TigerGraph Cloud,你需要设置一个账户。这很简单并且免费:
-
在网页浏览器中,访问 tgcloud.io。
-
点击“注册”按钮并填写表单。注册表单可能要求你创建一个组织。一个组织可以包含和管理多个用户和多个数据库在同一个账户下。
-
当你提交表单后,TigerGraph Cloud 将要求你去你的电子邮件验证你的账户。你现在拥有了一个 TigerGraph Cloud 账户!
在下一节中,我们将告诉你如何创建 TigerGraph Cloud 数据库,并选择起始套件。
获取并安装客户 360 起始套件
我们将使用名为“客户 360 – 归因和参与图”的起始套件。如果你是 TigerGraph Cloud 用户,你可以作为新数据库部署的一部分获取起始套件。如果你在自己的计算机上运行 TigerGraph,你可以从TigerGraph 网站下载起始套件文件,然后将它们上传到你的 TigerGraph 实例中。
接下来的两个部分详细介绍了这两个选项的细节。
部署带有起始套件的云实例
登录 TigerGraph Cloud 时,第一个可见的页面是我的集群页面。集群是一个 TigerGraph 数据库部署,具有或不具有图架构或数据。单击创建集群按钮,这将带您进入创建集群页面(如图 3-4 所示)。

图 3-4. TigerGraph Cloud 创建集群菜单
然后按照以下步骤操作:
-
确认您集群的服务层级。在创建集群页面上,默认为免费层级。较大和更强大的集群会产生按小时计费。对于本书中的练习,免费大小应该足够。您可以在部署后升级集群,如果需要的话。
-
选择一个用例的起始套件。如果您想要的套件未显示,请单击“查看所有用例”以查看更多选择。在本例中,是“Customer 360 – Attribution and Engagement Graph”。对于免费层级,就是这样。几分钟后,您的数据库实例将准备就绪。
-
如果您决定创建付费层级实例,则需要做更多选择:云平台提供商、实例大小、区域、磁盘大小和集群配置。您可以使用所有这些默认值按照本教程操作。
-
一旦您的集群实例准备就绪,它将显示在我的集群页面上。单击其工具按钮。从显示的菜单中,选择 GraphStudio。
-
继续到“加载数据并安装起始套件的查询”。
另选项:将起始套件导入到您的 TigerGraph 实例中
如果您在自己的机器上安装了 TigerGraph 软件,请按照以下步骤获取起始套件:
-
查找 Customer 360—Attribution and Engagement Graph。
-
下载与您的 TigerGraph 平台版本对应的数据集和解决方案包。
-
启动您的 TigerGraph 实例。前往 GraphStudio 主页。
-
单击导入现有解决方案,并选择您下载的解决方案包。
-
继续到“加载数据并安装起始套件的查询”。
警告
导入 GraphStudio 解决方案将删除您现有的数据库。如果您希望保存当前设计,请执行 GraphStudio 导出解决方案,并按照TigerGraph 文档站点上的说明备份数据库。
加载数据并安装起始套件的查询
还需要三个额外步骤来完成起始套件的安装。如果您了解 GraphStudio,只想知道如何安装起始套件,请按照以下步骤操作:
-
前往设计模式页面。在左侧菜单中,从全局视图切换到起始套件的本地图视图。可能称为 MyGraph,或者可能具有像 AntiFraud 这样的自定义名称。
-
转到加载数据页面。等待大约五秒,直到顶部菜单左端的“加载数据”按钮变为活动状态。点击按钮,等待数据加载完成。您可以在右下角的时间轴显示中跟踪加载进度。
-
转到编写查询页面。在查询列表上方,点击“安装所有查询”按钮,并等待安装完成。
GraphStudio 概述
TigerGraph 的 GraphStudio 是一个完整的图解决方案开发工具包,涵盖了从开发图模型到运行查询的整个过程中的每个阶段。它被组织为一系列视图或页面,每个页面用于开发过程中的不同任务。
因为这是我们首次一起使用 GraphStudio,我们将通过所有五个阶段:设计架构、映射数据到图、加载数据、探索图和编写查询。在每个阶段,我们将解释页面的一般目的,并指导您完成我们正在使用的特定起始套件的详细信息。在未来的章节中,我们将跳过大部分的概括,只讨论起始套件的细节。
如果我们从空数据库开始,我们需要进行额外的设计工作,例如创建图模型。有了起始套件,您可以跳过大部分工作,直接探索和查询示例数据集。
注意
如何在 GraphStudio 中创建图模型只是 TigerGraph 在线文档中涵盖的众多主题之一,详细信息请访问docs.tigergraph.com。TigerGraph 官方 YouTube 频道也是教程的宝贵资源。
设计图架构
起始套件预装了基于 Salesforce 和类似 CRM 软件中常用数据对象的图模型。此起始套件中图的名称为MyGraph。启动 GraphStudio 时,您最初位于全局图级别。您还没有在特定图上工作。在 TigerGraph 数据库中,全局级别用于定义可能对所有用户和所有图表可用的数据类型。请参阅图 3-5 中标记为“全局类型”的部分。然后数据库可以托管一个或多个图。图可以包含本地类型,并且可以包含某些或所有全局类型。请参阅图中的 G1 和 G2。

图 3-5。TigerGraph 数据库中的全局类型、本地类型和图表
要处理图表,您需要选择图表,这将使您从全局级别移动到本地图表级别。要切换到本地图表,请点击左上角的圆形图标。会弹出一个下拉菜单,显示可用的图表,并允许您创建一个新图表。点击“MyGraph”(步骤 2)。在下面稍微低一点的地方,点击“设计架构”,确保我们从正确的位置开始。
现在您应该在主显示面板上看到一个类似于图 3-6 的图模型或模式。

图 3-6. CRM 数据的图模式(在oreil.ly/gpam0306查看更大的版本)
数据库的图模式定义了要存储的数据对象类型。如果图模式在视觉上呈现,那么每种数据类型只显示一次。此模式有 8 种顶点类型和 14 种边类型。
中心顶点类型是**联系人**,这是产品的潜在买家。然而,一个**联系人**不仅仅是任何潜在买家,这反映了一个代表公司购买 B2B 产品的人并非一时冲动的决定。相反,这个人通过购买过程的各个阶段进行过渡。我们称之为这个人在购买过程中的流程为客户旅程。
在数据库中,一个真实的人可能会出现多次。如果供应商进行了营销活动,那么对该活动作出反应的人将显示为营销活动成员顶点。此外,如果第三方,即**潜在客户来源**,提供了潜在买家的联系信息,则潜在买家将显示为一个**潜在客户**。一位**销售人员**与一个**潜在客户**进行接触,看看是否存在实际的销售可能性。如果存在,那么**潜在客户**的信息将复制到一个称为**联系人**的新顶点类型中。这个**联系人**及其来源的**潜在客户**代表同一个物理人,但处于客户旅程的不同阶段。
表 3-1 包含所有八种顶点类型的描述。在某些情况下,一个顶点类型的描述讨论它如何与另一个顶点类型相关。例如,一个账户是“一个**联系人所属的组织”。查看图 3-6,您可以看到**账户**和**联系人**之间的边类型称为属于**。图中还有其他 13 种边类型。这些边类型有描述性的名称,因此如果理解了顶点类型,您应该能够理解边的含义。
表 3-1. Salesforce Customer 360 图模型中的顶点类型
| 顶点类型 | 描述 |
|---|---|
**账户** |
联系人所属的组织 |
**营销活动** |
旨在产生潜在客户的营销举措 |
**营销活动成员** |
响应营销活动的一个角色 |
**联系人** |
现在与销售机会相关联的一个潜在客户 |
**行业** |
账户的业务部门 |
**潜在客户** |
一个有可能购买产品但尚未与一个**销售机会**相关联的人 |
**潜在客户来源** |
一个潜在客户了解产品的渠道 |
**销售机会** |
一个潜在的销售交易,以货币金额为特征 |
数据加载
在 TigerGraph 的入门套件中,数据已包含在内,但尚未加载到数据库中。要加载数据,请切换到加载数据页面(第 1 步,参见 图 3-7),等待几秒钟,直到主面板左上角的加载按钮变为活动状态,然后单击它(第 2 步)。您可以在右侧的实时图表中观察加载的进度(未显示)。在 TGCloud 的免费实例上加载 34K 个顶点和 105K 条边应该需要两分钟;在付费实例上速度更快。

图 3-7. 在入门套件中加载数据
查询与分析
我们将通过组合和执行 GSQL 中的查询来分析图并运行图算法,GSQL 是 TigerGraph 的图查询语言。在第一次部署新的入门套件时,您需要安装查询。切换到编写查询页面(第 1 步,参见 图 3-8)。然后点击查询列表右上角的“全部安装”图标(第 2 步)。

图 3-8. 安装查询
学习和使用 GSQL
本书中的 GSQL 示例旨在向您展示一些表达图数据库信息和分析查询技术的方法。虽然示例是用 GSQL 编写的,但您不必精通这门语言,我们也不会试图以严格的方式教授它。如果您了解基本的 SQL 并熟悉像 Python 这样的通用语言,我们相信您将能够稍加努力地理解我们对查询的解释。当我们想要强调 GSQL 语言的某一点时,我们会使用如此的提示框。
对于我们的 Customer 360 使用案例,我们将讨论三个查询:
客户互动子图
此查询生成一个子图,为我们提供客户旅程的全面视图。子图体现了客户与公司广告活动的互动。查询从类型为**Contact**的给定客户开始。然后,它收集客户与之互动过的**Account**、**Opportunity**和**CampaignMember**顶点。此外,对于每个**CampaignMember**元素,也选择了一个**Campaign**。最后,查询返回生成的客户互动子图。
客户旅程
此查询查找在某个时间段内与客户互动过的所有**CampaignMember**元素。查询从给定的**Contact**开始,并过滤出在开始时间和结束时间之间与**Contact**有过联系的所有**CampaignMember**元素。与第一个查询不同的是,我们不返回连接**Contact**和**CampaignMember**之间的子图。在这里,我们返回一个按**CampaignMember**顶点排序的列表。
相似联系人
此查询返回与给定**Contact**类似的联系人。如果给定的**Contact**成功转化为付费客户,此查询可以找到其他潜在的转化候选人。此查询在 GSQL 中实现了 Jaccard 相似度测量,用于计算给定**Contact**与共享相似**Campaign**的其他**Contact**顶点之间的相似性。然后返回相似度得分最高的联系人。
对于这三个查询中的每一个,我们将提供一个高层次的解释,在 TigerGraph 的 GraphStudio 中运行它们的指南,预期的结果以及对查询中一些 GSQL 代码的更详细的查看。
客户互动子图
customer_interactions查询需要一个参数:作为**Contact**自然人类型的客户。首先,我们选择所有属于给定**Contact**的**Account**身份。然后,我们找到与**Contact**连接的所有**Opportunity**顶点。此外,**Contact**顶点与一个或多个属于**Campaign**的**CampaignMember**有连接。图 3-9 说明了此查询发现的关系。

图 3-9. Contact 顶点和边缘
进行:通过从列表中选择查询名称(图 3-10 中的步骤 1)并点击代码面板上方的运行图标(步骤 2)来运行 GSQL 查询customer_interaction。此查询有一个输入参数:字段“Contact”让我们填入客户的姓名。如果您查看查询代码窗格(图 3-10),您将看到一个建议的Contact示例值的注释:Sam-Eisenberg。

图 3-10. 运行customer_interaction查询
提示
使用鼠标从代码窗口复制并粘贴值到查询参数输入框中。
输出将显示在查询编辑面板下方的结果面板中。初始时,图可能看起来很混乱。要清理外观,请点击输出右下角的“更改布局(force)”按钮,然后选择 force。然后输出应该类似于图 3-11。

图 3-11. 查询结果customer_interaction,输入为Sam-Eisenberg(请查看此图的大图版本 oreil.ly/gpam0311)
现在我们来看看 GSQL 查询cust_journey_subgraph的工作原理。请参考随后的代码块:
CREATE QUERY customer_interactions(VERTEX<Contact> customer) {
/*
Finds the Account, Opportunity, CampaignMembers, and Campaigns
connected to the given customer.
Sample input:
customer: Sam-Eisenberg
*/
SetAccum<EDGE> @@edges_to_display; // accumulator declaration(s) 
cust = { customer }; // make into a vertex set 
// Get the customer's connected Accounts 
accts = SELECT t FROM cust:s -(belongs_to>:e)- Account:t
ACCUM @@edges_to_display += e;
// Get the customer's connected Opportunities 
opps = SELECT t FROM cust:s -(Has_Role>:e)- Opportunity:t
ACCUM @@edges_to_display += e;
// Get the customer's connected CampaignMembers 
campMems = SELECT t FROM cust:s -(is_connected_to>:e)- CampaignMember:t
ACCUM @@edges_to_display += e;
// Get the Campaigns connects to those CampaignMembers 
campaigns = SELECT t FROM campMems:s -(is_part_of>:e)- Campaign:t
ACCUM @@edges_to_display += e;
// Print (display) the collected vertices and connecting edges. 
interactions = accts UNION opps UNION campMems UNION campaigns;
PRINT cust, interactions;
PRINT @@edges_to_display;
}
在第一行中,我们定义了查询的名称及其输入参数。为了查找客户子图,我们需要一个客户参数,类型为 **Contact** 的顶点。接下来我们声明了一些变量。在 处,我们定义了一个名为
@@edges_to_display 的边集变量。
GSQL 查询结构
一个 GSQL 查询是一个命名的可参数化过程。其主体是一系列 SELECT 语句,一次或多次遍历和分析图形。SELECT 语句的输入和输出是顶点集变量。整个查询的输出通过 PRINT 语句明确声明。
要开始遍历图形,我们需要定义一个包含我们的起始点或起始点的顶点集。在 处,我们创建了一个名为
cust 的顶点集,其中包含输入参数中的 **Contact**。然后在 处,我们使用一个
SELECT 语句开始收集客户的交互。子句 FROM cust:s -(belongs_to:e)- Account:t 意味着从 cust 沿着 **belongs_to** 边遍历到 **Account** 顶点。符号 :e 和 :t 分别为边和目标顶点定义了别名变量。ACCUM 子句像 FOREACH 一样作用于别名变量。实际上,符合 FROM 模式的每个 e 边都会被添加到 @@edges_to_display 累加器中。最后,初始子句 accts = SELECT t 意味着此语句返回一个名为 accts 的顶点集,其中包含别名顶点 t。
GSQL 累加器
累加器 是 GSQL 语言的一个独特特性,是具有特殊操作 accumulate 的数据对象,由 += 运算符表示。+= 的确切含义取决于累加器类型,但它总是用于接受额外的输入数据以更新累加器的外部值。累加器可以接受多个异步累积操作,因此它们非常适合并发/并行处理。@@ 前缀表示全局累加器。@ 前缀表示一组本地(也称为顶点附加)累加器。本地意味着查询中的每个顶点都有其自己独立的累加器实例。例如,@interact_size 是类型为 SumAccum<INT> 的本地累加器。SumAccum<INT> 通常用于计数。
在处我们执行类似操作,但在此处我们选择顾客在创建
**Opportunity**时的顶点和边缘。所选的顶点存储在变量Opps中;所选的边缘添加到@@edges_to_display中。接下来,在处,我们找到与顾客连接的
**CampaignMember**顶点,并再次用结果更新@@edges_to_display。然后,我们从前一步骤中选择的campaign_members开始(FROM campMems),并找到每个是**Campaign**一部分的**CampaignMember**的顶点和边缘,然后在处再次用结果更新
@@edges_to_display。在处,我们将步骤
、
、
和
中选择的顶点合并为一个名为
interactions的变量。最后,我们打印(输出)输入的客户、其互动以及它们的连接边缘(@@_edges_to_display)。当输出包含顶点或边缘时,GraphStudio 将在代码窗格下方的面板中以图形方式显示它们。输出窗格的菜单中有将输出格式化为 JSON 或表格的选项。
客户旅程
customer_journey查询显示了在给定时间段内与客户有关系的所有**CampaignMember**和**Account**顶点。在这里,我们不仅想看到客户的市场互动,还想看到活动的顺序。让我们看看 GSQL 的实现。
此 GSQL 查询使用四个参数:
CREATE QUERY customer_journey(VERTEX<Contact> customer,
SET<STRING> campaign_type_set, DATETIME start_time, DATETIME end_time) {
第一个参数是类型为**Contact**的顶点,表示我们感兴趣的客户。第二个参数是要包括的活动类型列表。如果留空将包括所有活动类型。第三和第四个参数是DATETIME类型,并且我们使用这些参数来确定查询应该执行的时间窗口。
接下来我们利用本地累加器来充当顶点类的实例变量。我们将为每个选择的**CampaignMember**添加三个字符串属性:
SumAccum<STRING> @cam_type, @cam_name, @cam_desc;
首先我们选择目标客户所属的**Account**:
start = { customer };
account = SELECT t FROM start -(belongs_to>)- Account:t;
然后我们选择所有与给定时间窗口内的客户连接的**CampaignMember**顶点:
campaign_members =
SELECT c
FROM start-(is_connected_to>)- CampaignMember:c
WHERE c.CreatedDate >= start_time
AND c.CreatedDate <= end_time;
接下来,我们检查每个这些**CampaignMembers**是否属于输入参数中指定的某个活动类型。为此,我们需要从每个**CampaignMember**到其**Campaign**进行遍历。在此过程中,我们从**Campaigns**复制一些信息:
CM =
SELECT c FROM campaign_members:c -(is_part_of>)- Campaign:t
WHERE campaign_type_set.size() == 0
OR t.Campaign_Type IN campaign_type_set
ACCUM c.@cam_type = t.Campaign_Type,
c.@cam_name = t.Name,
c.@cam_desc = t.Description
ORDER BY c.FirstRespondedDate;
ORDER BY子句最后按其生效日期对所选**CampaignMember**顶点进行排序。
查询开头的注释建议尝试一些输入。单击“运行查询”按钮,然后将建议的输入复制并粘贴到左侧的参数文本框中。对于 campaign_type_set,单击 + 符号以添加一个值到集合中。对于日期时间参数 start_time 和 end_time,请注意 GraphStudio 接受 YYYY-MM-DD 格式的值。如有必要,请向下滚动到达“运行查询”按钮。输出应包括 **Contact** Sam-Eisenberg、**Account** VRG-Payments 和七个 **CampaignMember** 元素。这些是 Sam 在给定时间段内的客户旅程的组成部分。
要查看旅程的时间顺序,请切换到 JSON 或表格视图输出模式。或者您可以运行 customer_journey_path 查询,其输出显示在 Figure 3-12 中。它与 customer_journey 查询相同,除了几行额外的 GSQL 代码,这些代码插入了从一个 **CampaignMember** 顶点到下一个顶点的有向边。这本书的早期部分的代码有点复杂,所以我们不会详细描述它的工作原理。还要注意,您需要运行 customer_journey_path 两次:一次用于创建路径边,再次用于查看它们。

图 3-12. Sam-Eisenberg 的 customer_journey_path 查询输出(在 oreil.ly/gpam0312 上查看更大的版本)
相似的客户
在我们实施相似性度量之前,我们需要首先确定要计算相似性的属性。在我们的情况下,我们希望根据参与类似市场活动集的客户来计算相似性。为了衡量这一点,我们使用 Jaccard 相似性。Jaccard 相似性不仅适用于图结构数据。它是一种衡量两个集合相似度的方法,基于属于一个集合的项同时属于另一个集合的数量,除以两个集合中出现的所有不同项的总数。在图的情况下,每个顶点都有一组邻居顶点。因此,基于图的 Jaccard 相似性衡量了一个顶点的邻居集与另一个顶点的邻居集之间的重叠程度。换句话说,共同邻居的数量相对于总邻居数是多少?
我们的情况稍微复杂一些,因为我们想要评估与活动关联的相似性;然而,活动与联系人之间相距两步,而不是直接连接。此外,我们允许用户按活动类型进行筛选以计数。
让我们浏览similar_contacts查询的 GSQL 代码。此查询接受三个参数。第一个参数source_customer是类型为 Contact 的顶点,代表我们要找到类似客户的客户。第二个参数是用户希望考虑的活动类型(字符串)集合。第三个参数是确定我们要返回多少个类似客户的整数值:
CREATE QUERY similar_contacts(VERTEX<Contact> source_customer,
SET<STRING> campaign_types, INT top_k = 5) {
我们从声明四个累加器开始。前三个是整数计数:输入客户的活动数(@@size_A),每个候选联系人的活动数(@size_B),以及它们共同的活动数(@size_intersection)。只有一个输入,所以@@size_A是全局累加器。其他三个是附加到顶点的本地累加器。我们还有一个FLOAT类型的本地累加器来存储计算出的相似度值:
SumAccum<INT> @@size_A, @size_B, @intersection_size;
SumAccum<FLOAT> @similarity;
然后我们使用outdegree()函数来获取@@size_A的值,指定边类型 is_connected_to:
A = SELECT s
FROM A:s
ACCUM @@set_size_A += s.outdegree("is_connected_to");
现在我们从source_customer跨越两个跳到首先到**CampaignMember**,然后到**Campaign**顶点。这对应于 Figure 3-13 中的步骤 1 和 2。注意用于检查活动类型的WHERE子句:
campaign_mem_set =
SELECT t
FROM A:s -(is_connected_to>:e)- CampaignMember:t;
campaign_set =
SELECT t
FROM campaign_mem_set:s -(is_part_of>:e)- Campaign:t
WHERE campaign_types.size() == 0 OR (t.Campaign_Type IN campaign_types);

图 3-13。选择计算 Jaccard 相似度分数的相似客户步骤概览
下一个阶段是分析的基于图的方法的一个很好的示例。此查询的任务是“查找所有具有与联系人 A 类似关系的联系人 B”。而不是搜索所有可能的 Contacts,然后比较它们的关系,我们去到 A 的相关实体,然后从那里向候选 Contacts 后退。这些是 Figure 3-13 中的步骤 3 和 4。推理是这种前进然后后退跨关系遍历会自动过滤掉没有共同点的候选者。如果我们聪明的话,在遍历时我们可以测量相似度的程度。
比较步骤 3 的 GSQL 代码与步骤 2 的代码。注意有向边的方向性指示符从 > 后缀变为 < 前缀:
rev_campaign_mem_set =
SELECT t
FROM campaign_set:s -(<is_part_of:e)- CampaignMember:t;
最后一跳更复杂,因为它包含 Jaccard 计算。跳跃本身如预期那样,有一个WHERE子句来排除回到我们的source_customer:
B = SELECT t
FROM rev_campaign_mem_set:s -(<is_connected_to:e)- Contact:t
WHERE t != source_customer
回想一下,ACCUM子句就像是在满足前面的FROM-WHERE子句的每条路径上迭代的FOREACH块。以下代码逐步计算 A 和 B 活动集之间的交集大小,并为此特定的 Contact 设置@size_B:
ACCUM t.@intersection_size += 1,
t.@size_B = t.outdegree("is_connected_to")
现在我们可以计算 Jaccard 相似性了。顾名思义,POST-ACCUM子句通常在ACCUM子句之后执行。关于POST-ACCUM有两条最重要的规则:1)它可以使用前面ACCUM子句的累加器结果,2)它只能使用顶点变量,不能使用边变量。我们使用了 Jaccard 相似性的一个标准公式。分母等于集合 A 和 B 中唯一项的数量。分子中的 1.0 是为了执行浮点运算而不是整数运算。
POST-ACCUM t.@similarity = t.@intersection_size*1.0/
(@@size_A + t.@size_B - t.@intersection_size)
最后,我们按照相似性得分从高到低排序,并只取top_k个结果进行打印:
ORDER BY t.@similarity DESC
LIMIT top_k;
PRINT @@size_A;
PRINT B[B.FirstName, B.LastName, B.@similarity, B.@size_B];
我们通过实现 Jaccard 相似性来展示一些图算法在 GSQL 中的易实现性,同时帮助您理解该方法,以便如果您想编写自己的查询和分析时可以使用。TigerGraph 提供了一个广泛的预写图数据科学算法库,我们将在本书的后面介绍。
案例 2:分析药物不良反应
在我们的第二个使用案例中,我们试图分析药物治疗的不良反应。
今天的医疗系统涵盖了全球数据量的 30%,其复合年增长率预计到 2025 年将达到 36%。³ 这些数据来源于美国食品药品监督管理局(FDA)和国家数据库医学协会等外部来源,以及来自健康保险公司的私有数据集。组织利用这些数据获取有价值的洞见,创建针对性的内容和参与活动,改善健康保险计划,并开发药物。开发更好的医疗疗法是我们此用例的重点。
在开发药物时,清楚地了解药物的组成、它们如何相互作用以及可能引起的副作用至关重要。因此,FDA 要求每家药品制造商监控其药物与其他药物的使用情况,并报告任何不良反应。
分析师和研究人员希望找出各种药物、使用这些药物的患者以及可能的副作用之间的关系。医生是否会给同一个邮政区域的人开同一种药物,或者他们的评估主要建立在去同一所大学的患者基础上?当患者报告对某种药物有不良反应时,其他患者可能也面临危险,因为他们的药物相互作用历史。如果没有了解这些药物相互作用发生的方式以及给予这些药物处方的对象,该领域的研究将变得困难,并且当药物和副作用之间的重要联系被忽视时,可能会对公共健康构成威胁。
解决方案:药物交互 360 图
日益增长的医疗数据量带来了在大规模结合外部和内部数据源并以有意义的方式展示这些数据的挑战。这个领域的应用需要一种方法,既能处理这么大量的数据,又能在各种数据源中找到隐藏的模式。
图数据库是发现和分析药物相互作用的理想数据平台。通过图数据库,我们可以形成关键实体的 360 视图,并连接这些关键实体以揭示患者之间的所有可能相关性,以及这些药物的制造商。
相比之下,关系数据库和 NoSQL 数据库将数据存储在单独的表中,并依赖于分析师的领域专业知识来选择要连接的表,每次连接都是一次昂贵的操作。发现相互作用和相关性仅限于分析师检查的特定情况,形成特定表连接序列。对于这种情况,表格结构不如图结构有利于科学发现。
实施
为了说明一个药物相互作用 360 图,我们将使用名为“Healthcare Graph(Drug Interaction/FAERS)”的 TigerGraph Cloud Starter Kit。要跟进,请查看之前的说明,了解如何部署 TigerGraph Cloud Starter Kit,加载数据并安装查询。
我们用于此用例的数据来自美国 FDA 公开可用的数据。它包括 FDA 不良事件报告系统(FAERS)的季度数据,包括药品的人口统计和行政信息,患者结果以及案例报告中的反应。FDA 将数据发布为七个表格。文档包括一个实体关系图,表明了一个双中心 360 图的可能性。然而,使用关系数据库技术调查这些数据将需要创建许多连接表。使用图数据库,我们可以更轻松地遍历这些关系。
图模式
为了提高数据的可见性和分析能力,我们建议将这七个表格转换为 10 种顶点类型和 10 种边类型。我们将**Drug**表格分成**Drug**和**DrugSequence**顶点类型,将Demographic表格分成**ReportedCase**、**Patient**和**PharmaCompany**表格。这些分割使我们能够根据需要调整焦点,并更容易地看到不同因素之间的相互作用。对于每个**ReportedCase**,我们可以找到关于患者、药品制造商、患者反应、报告来源、结果以及患者正在服用的各种药物的信息。对于每个**DrugSequence**,我们可以找到相关药品、指示以及患者的疗法。
表格 3-2 描述了 10 种顶点类型,而 图 3-14 显示了它们的连接方式。起始套件包含一个日历季度的数据。总计有 1.87M 个顶点和 3.35M 条边。
表格 3-2. 药品信息模型中的顶点类型
| 顶点类型 | 描述 | 实例数 |
|---|---|---|
**药品序列** |
一系列 **药品** 元素 |
689,312 |
**药品** |
作为 **药品序列** 一部分的药物 |
40,622 |
**适应症** |
可以用 **药品序列** 治疗的适应症(医疗状况) |
422,145 |
**治疗** |
使用 **药品序列** 的治疗方法 |
268,244 |
**报告病例** |
副作用的报告案例 | 211,414 |
**患者** |
报告案例的人 | 211,414 |
**结果** |
对 **报告病例** 进行评估后的结果 |
7 |
**报告来源** |
用于 **报告病例** 的来源类型 |
9 |
**反应** |
从 **报告病例** 得到的反应 |
9,791 |
**制药公司** |
生产 药品 的制药公司 |
7,740 |

图 3-14. 药品信息数据的图模式(在 oreil.ly/gpam0314 查看更大的版本)
查询和分析
药物相互作用起始套件提供了三个示例,用于药物相互作用分析。通过这些示例,勤奋的分析师可以看到如何构建其他查询,甚至更复杂的查询。
基于反应查找类似报告案例
寻找具有类似反应集的案例可以帮助理解根本原因。该查询从给定的报告案例开始,计算其与其他案例的相似性,基于患者反应的相似性。然后返回得分最高的类似案例。
公司报告的大多数药物
制药公司想知道他们的药物中哪些收到了最多的不良反应报告。政府监管机构也可能对此感兴趣。此查询为他们执行此计算。
最常见的顶级药物副作用
制药公司和监管机构想要知道报告的不仅仅是哪些药物,还想知道最常见的副作用是什么。该查询选择给定 **公司** 的最高 **药品** 类型,并计算该药物的每种 **反应** 被报告的次数。
寻找类似的报告案例
拥有相似特征的事物是相似的,但究竟如何度量?您必须决定哪些特征重要以及如何评估相似性的强度。在图中,实体的特征不仅包括其属性,还包括其关系。查看 图 3-14,您可以看到 **ReportedCase** 周围与其他六种顶点类型的关系,这些都是潜在的相似性因素。它还有一种边缘类型 **similarCaseTo**,可以在那里存储相似性计算的结果。
查询实现基于关系的相似度评分:jaccard_nbor_reaction。查询的第一个参数 source 是感兴趣的 **ReportedCase**。etype 参数指定要考虑的关系类型。top_k 参数确定查询返回的报告案例数,而 sampSize 如果每个 **Reaction** 实例有超过此阈值的相关案例则调用抽样。
一旦我们指定要考虑的特征,仍然需要应用一个用于测量相似性的公式。此查询使用 Jaccard 相似度,当属性是分类而不是数值时,这是最常用的测量方法。我们只知道反应是否发生,而不知其强度,因此数据是分类的。
CREATE QUERY jaccard_nbor_reaction(VERTEX source, STRING etype
="hasReactions", INT top_k=100, INT sampSize=100) FOR GRAPH faers {
//example: ReportedCase=100640876
/*
Calculates the Jaccard Similarity between a given vertex and every other
vertex. A simplified version of the generic purpose algorithm
jacccard_nbor_ss in the GSQL Graph Data Science Library
https://github.com/tigergraph/gsql-graph-algorithms
*/
SumAccum<INT> @intersection_size, @@set_size_A, @set_size_B;
SumAccum<FLOAT> @similarity;
SumAccum<INT> @@t_Size;
Start (ANY) = {source}; 
Start = SELECT s
FROM Start:s
ACCUM @@set_size_A += s.outdegree(etype); 
Neighbors = SELECT t 
FROM Start:s-(etype:e)-:t;
类似于我们第一个案例示例中的第一个查询,我们需要定义从哪些顶点开始遍历。我们在 处进行此操作。然后,为了计算 Jaccard 计算,我们需要源顶点的邻居集大小,我们通过应用
out_degree 函数并在 处指定
etype 来获取此信息。在 处,我们通过从
Start 到每个 etype 边缘的遍历来收集 **Neighbors**。
表达式 Start:s-(etype:e)-:t 在图中表示遍历模式,位于 。此特定模式意味着:
-
以集合成员
Start开始。 -
连接到类型为
etype的边缘。 -
通过该边缘,到达任何目标顶点。
该表达式还为模式的三个部分定义了三个别名:s、e 和 t。FROM 子句的结果是满足模式的元组集合 (s, e, t)。别名 t 表示目标顶点集的成员。这些别名是局部的,只能在此 SELECT 块内使用。它们与其他 SELECT 块中的别名无关。
在 处,我们选择其他顶点。我们通过检查
etype 是否为 reactionTo 来执行此操作;然后 Neighbors 将包括给定源 **ReportedCase** 的所有 **Reactions**。然后我们通过再次遍历 etype 边,从 Neighbors 构建一组 **ReportedCases**。如果邻居的出度大于 sampSize,我们只遍历连接边的一个样本。我们在 处排除源顶点的选择。
Others = SELECT t 
FROM Neighbors:s -(:e)- :t
SAMPLE sampSize EDGE when s.outdegree(etype) > sampSize
WHERE t != source 
ACCUM t.@intersection_size += 1,
t.@set_size_B = t.outdegree(etype)
POST-ACCUM t.@similarity = t.@intersection_size*1.0/ 
(@@set_size_A + t.@set_size_B - t.@intersection_size),
@@tSize += 1
ORDER BY t.@similarity DESC 
LIMIT top_k;
PRINT Others;
PRINT @@t_Size, Others.size();
提示
这种模式(遍历邻居,沿着相同的边类型返回遍历,排除起始顶点)是一种常见的技术,用于查找与起始实体有共同点的实体。这是基于图的协同过滤推荐的技术。
在 处,我们计算源顶点与每个
Others 成员之间的 Jaccard 相似性分数。给定两个集合 A 和 B,Jaccard(A, B) 定义为:
(A 和 B 的交集) / (A 的大小 + B 的大小 - A 和 B 的交集)
高效的 GSQL 实现略微复杂。我们不会逐行详细说明,但我们会指出两种范例:
-
在我们的情况下,集合由 A 和 B 的邻居组成。我们不是从集合 A 和 B 开始计算它们的交集,而是从 A 开始,然后进入其邻居,再进入它们的邻居。这样可以找到所有的 B 集合,使得 intersection(A, B) 不为空。
-
我们使用分布式处理同时对一组成员执行操作。在 GSQL 中,
ACCUM和POST-ACCUM子句是隐式的FOREACH循环,指定了对每个迭代集的操作。迭代顺序未指定。TigerGraph 计算引擎可以同时处理多个迭代。
ACCUM 子句在满足前面的 FROM/SAMPLE/WHERE 子句的连接顶点和边的每组模式元组上充当 FOREACH 循环的角色。在这个 SELECT 块中,s 表示 Neighbors 的成员,它是一个 **Reaction**,而 t 表示具有该 **Reaction** 的 **ReportedCase**。POST-ACCUM 子句是另一个 FOREACH 循环,但它只能在一个顶点别名上操作(例如 s 或 t)。
在 处,我们按照降序相似性分数对
Others 顶点进行排序,然后将集合修剪为仅包括 top_k 个顶点。最后,我们打印 Others 中的所有顶点以及 @@t_Size 的值。
通过使用建议的源案例 100640876 运行查询,然后以表格形式查看结果,我们发现三个具有完美相似度分数 1 的 **ReportedCase** 实例:103126041、101749293 和 102852841。然后还有一些其他的相似度分数为 0.5 的实例。
公司报告的最多的药物
most_reported_drugs_for_company查询有三个参数。 第一个参数company_name选择要查找其最多报道药物的公司。 第二个参数k确定我们希望返回多少种药物类型。 最后一个参数用于过滤具有给定role值的DrugSequence元素:
CREATE QUERY most_reported_drugs_for_company(
STRING company_name="PFIZER",INT k=5, STRING role="PS") {
// Possible values for role: PS, SS, I, C
// PS = primary suspect drug, SS = secondary suspect drug
// C = concomitant, I = interacting
// Keep count of how many times each drug is mentioned.
SumAccum<INT> @num_Cases;
反思“公司的最报道药物”。 我们可以推断出这个查询必须遍历**ReportedCase**、**Drug**和**PharmaCompany**顶点。 回顾一下图 3-14 看看这些顶点类型是如何连接的:
**药物 – DrugSequence – ReportedCase – 制药公司**
从**药物**到**制药公司**有三个跳跃;我们的查询将分三个阶段完成其工作。将 GSQL 组合为多阶段过程而不是多个单独的查询,可以使用累加器作为临时存储值,既提高性能又增强功能性。
首先,我们找到与输入参数中给定公司相关的所有**ReportedCase**顶点。 深入挖掘:我们构建一个包含所有**Company**顶点的顶点集,因为 GSQL 要求图遍历从一个顶点集开始。 然后,我们选择所有链接到**Company**顶点的**ReportedCase**顶点,只要该公司的名称与company_name参数匹配:
// 1\. Find all cases where the given pharma company is the 'mfr_sndr'
Company = {PharmaCompany.*};
Cases = SELECT c
FROM Company:s -(relatedTo:e)- ReportedCase:c
WHERE s.mfr_sndr == company_name;
然后,我们从上面收集的选定的ReportedCase顶点开始遍历到其关联的**DrugSequence**顶点。 然后,我们过滤**DrugSequence**集合,只包括其角色与查询的role参数匹配的那些:
// 2\. Find all drug sequences for the selected cases.
DrugSeqs = SELECT ds
FROM Cases:c -(hasSequences:e)- DrugSequence:ds
WHERE (role == "" OR ds.role_cod == role);
在代码的最后部分,我们将第二部分中选择的**DrugSequence**顶点与其相关的**Drug**顶点连接起来。 当然,我们不仅仅是查找药物。 我们统计了特定药物出现的案例数,然后按出现次数降序排序,并选择出现频率最高的k种药物:
// 3\. Count occurrences of each drug mentioned in each drug sequence.
TopDrugs = SELECT d
FROM DrugSeqs:ds -(hasDrugs:e)-> Drug:d
ACCUM d.@num_Cases += 1
ORDER BY d.@num_Cases DESC
LIMIT k;
PRINT TopDrugs;
}
使用默认输入值(company_name="PFIZER", k=5, role="PS")运行此查询,我们得到 Lyrica、Lipitor、Chantix、Celebrex 和 Viagra 这些药物。 查看 JSON 或表格输出,我们可以看到案例数分别为 2682、1259、1189、1022 和 847。
最受欢迎的药物的顶部副作用
查询top_side_effects_for_top_drugs返回给定**公司**最多报道的**药物**的顶部副作用. 与之前的查询类似,它还想找到公司的最多报道药物,但也额外工作来计算副作用。 其参数列表与most_reported_drugs_for_company相同,但这里k不仅指最多报道的药物,还指最频繁的副作用:
CREATE QUERY top_side_effects_for_top_drugs(STRING company_name="PFIZER",
INT k=5, STRING role="PS") FOR GRAPH faers SYNTAX v2 {
// Possible values for role: PS, SS, I, C
// PS = primary suspect drug, SS = secondary suspect drug
// C = concomitant, I = interacting
// Define a heap which sorts the reaction map (below) by count.
TYPEDEF TUPLE<STRING name, INT cnt> tally;
HeapAccum<tally>(k, cnt DESC) @top_Reactions;
// Keep count of how many times each reaction or drug is mentioned.
ListAccum<STRING> @reaction_List;
SumAccum<INT> @num_Cases;
MapAccum<STRING, INT> @reaction_Tally;
就像我们为上一个查询所做的那样,让我们看一下查询的名称和描述,以了解我们必须遍历哪些顶点和边类型。我们可以看到,我们需要包括**ReportedCase**、**Drug**和**PharmaCompany**,以及**Reaction**(副作用)。这建立了一个 Y 形图遍历模式:
Drug – DrugSequence – ReportedCase – PharmaCompany
\– Reaction
这个查询有五个阶段。这个查询的第 1、3 和 4 阶段与most_reported_drugs_for_company查询中的第 1、2 和 3 阶段相同或略有增强。
第 1 阶段与most_reported_drugs_for_company中的第 1 阶段相同——查找与作为输入参数给定的公司相关的所有**ReportedCase**顶点:
// 1\. Find all cases where the given pharma company is the 'mfr_sndr'
Company = {PharmaCompany.*};
Cases = SELECT c
FROM Company:s -(relatedTo:e)- ReportedCase:c
WHERE s.mfr_sndr == company_name;
第 2 阶段是新的:现在我们有了一组**ReportedCase**顶点,我们可以计算它们关联的Reactions。我们遍历所有**ReportedCase** **–** **Reaction**边,并将每个病例c的反应类型r.pt添加到附加到该病例c的字符串列表中:
// 2\. For each case, attach a list of its reactions.
Tally = SELECT r
FROM Cases:c -(hasReactions:e)- Reaction:r
ACCUM c.@reaction_List += r.pt;
在第三阶段,我们从第一阶段选择的**ReportedCase**顶点开始遍历它们关联的**DrugSequence**顶点。我们首先执行遍历,然后过滤**DrugSequence**集合,仅包括其角色与查询的role参数匹配的部分。之后,我们将附加到**ReportedCase**顶点的反应列表复制到其关联的**DrugSequences**。这最后一步是一种 GSQL 技术,用于将数据移动到我们需要的位置:
// 3\. Find all drug sequences for the selected cases, and transfer
// the reaction list to the drug sequence.
DrugSeqs = SELECT ds
FROM Cases:c -(hasSequences:e)- DrugSequence:ds
WHERE (role == "" OR ds.role_cod == role)
ACCUM ds.@reaction_List = c.@reaction_List;
在第 4 阶段,我们将第 2 阶段选择的**DrugSequence**顶点与其关联的**Drug**顶点连接起来。除了计算药物的病例数之外,我们还计算每个**Reaction**的发生次数:
// 4\. Count occurrences of each drug mentioned in each drug sequence.
// Also count the occurrences of each reaction.
TopDrugs = SELECT d
FROM DrugSeqs:ds -(hasDrugs:e)- Drug:d
ACCUM d.@num_Cases += 1,
FOREACH reaction in ds.@reaction_List DO
d.@reaction_Tally += (reaction -> 1)
END
ORDER BY d.@num_Cases DESC
LIMIT k;
最后,在第 5 阶段,我们只取前k个副作用。我们通过计数tally中的每个reaction,按降序排序,并返回前几个:
// 5\. Find only the Top K side effects for each selected Drug.
TopDrugs = SELECT d
FROM TopDrugs:d
ACCUM
FOREACH (reaction, cnt) IN d.@reaction_Tally DO
d.@top_Reactions += tally(reaction,cnt)
END
ORDER BY d.@num_Cases DESC;
PRINT TopDrugs[TopDrugs.prod_ai, TopDrugs.@num_Cases,
TopDrugs.@top_Reactions];
}
如果您使用默认输入运行此查询(与上一个查询相同),则视觉输出看起来相同。区别在于TopDrugs.@top_Reactions累加器。最好的方法是查看 JSON 输出。对于来自辉瑞的最多报告的药物 Lyrica,我们有以下数值:
"TopDrugs.@top_Reactions": [
{ "cnt": 459,"name": "Pain"},
{ "cnt": 373, "name": "Drug ineffective" },
{ "cnt": 167, "name": "Malaise" },
{ "cnt": 145, "name": "Feeling abnormal" },
{ "cnt": 145, "name": "Pain in extremity" }
],
章节总结
在本章中,我们深入研究了两个用例,以展示图形的强大之处,帮助用户更清晰、更全面地看到其数据中的关系。我们介绍了 TigerGraph Starter Kits——预先安装在 TigerGraph Cloud 实例上的演示数据库和查询,展示了各种不同用例的基础知识。我们演示了获取和安装 Customer 360 starter kit 的过程。同时,我们还演示了使用 GraphStudio 的前几个步骤。
我们还向您介绍了 GSQL,这是 TigerGraph 图数据库使用的类 SQL 过程化图查询语言。了解 SQL 和传统编程语言的读者应该可以轻松学习 GSQL。为了展示 GSQL 如何在我们的图分析中发挥作用,我们深入探讨了两个用例。在第一个用例中,我们定义了一个客户旅程,并描述了销售团队如何通过记录和分析客户旅程来获益。然后,我们展示了 Customer 360 图如何提供一个强大而灵活的集成客户数据的方式,然后可以将其表示为客户旅程。我们详细介绍了三个 GSQL 查询,这些查询用于探索和分析客户旅程。在第二个用例中,我们展示了如何使用 360 图显示用于医疗治疗的药物之间所有可能的交互和相关性。这样的分析对于检测并采取有害副作用非常重要。
¹ “CRM Market Share—Salesforce Bright Future in 2020,” Nix United,2020 年 2 月 19 日,https://nix-united.com/blog/crm-market-share-salesforce-bright-future-in-2020。
² “Market Share of CRM Leading Vendors Worldwide 2016–2020,” Statista,2022 年 6 月 13 日,https://www.statista.com/statistics/972598/crm-applications-vendors-market-share-worldwide。
³ “The Healthcare Data Explosion,” RBC Capital Markets,访问日期为 2023 年 5 月 21 日,https://www.rbccm.com/en/gib/healthcare/episode/the_healthcare_data_explosion。
第四章:研究初创投资
在本章中,我们将深入探讨初创投资领域。这个现实生活中的用例向我们展示了六种图谱力量中的三种如何帮助我们揭示高潜力的投资机会。第一种图谱力量,连接各种角色,让我们可以看到投资景观中各种角色是如何相互连接的。第二种图谱力量,深入观察,为投资者提供了一种方法,通过我们的分析包含有关这些角色的连接信息。第三种图谱力量,权衡和预测,使我们能够利用过去的资金事件和投资组合来预测未来投资的成功率。
完成本章后,您应该能够:
-
解释连接各种角色、深入观察和权衡预测如何解决搜索和分析需求
-
对初创投资机会进行建模和分析
-
遍历多跳关系以过滤更深层次的连接信息
-
阅读和理解更高级的 GSQL 查询
目标:寻找有前途的初创公司
投资初创公司是一种既令人兴奋又有利可图的财富积累方式。2020 年,投资者在美国初创公司中投入超过 1560 亿美元。这些初创公司产生了超过 2900 亿美元的流动性[¹]。然而,十分之九的初创公司将会失败,只有 40%的公司变得盈利,这让正确押注变得非常具有挑战性[²]。
初创公司从成立团队开始,团队成员仅有少数几人。随着时间的推移,随着初创公司经历不同的发展阶段,其产品得到改进,团队也在壮大。为了资助这些发展,初创公司需要来自投资者的资金。从投资的角度来看,识别哪家初创公司适合融资的一种方法是看初创团队及其组织的构成。在其组织中拥有正确的人员的初创公司通常有更高的成功机会。因此,由具有积极创业历程的创始人领导的初创公司,在其他公司中也更有可能取得成功。评估投资机会的另一种方式是观察初创公司的现有投资者。投资组合回报率高的投资者表明他们能够看到早期阶段初创公司的潜力,并帮助它们发展成为更有利可图的企业。
投资初创公司是一个风险高、评估复杂的过程,需要理解其试图进入的产品和市场,以及推动其发展的人员和组织。投资者需要了解这些方面之间的关系概况,以帮助支持对初创公司潜力的分析。
解决方案:一种初创投资图谱
用于支持投资评估的数据主要是非结构化的,因为它来自不同的来源。其中一个例子是 Crunchbase 数据集。该数据集包含有关投资轮次、创始人、公司、投资者和投资组合的信息。然而,该数据集是原始格式,这意味着数据没有结构化以回答我们对与初创企业相关的实体的问题。除非我们显式查询数据,否则关于初创企业及其当前状态的实体的数据对我们而言是隐藏的。使用图表,我们可以形成以我们要调查的目标初创企业为中心的模式,并查看其他实体对该初创企业的影响。
投资初创企业发生在一系列融资事件中,如图 4-1 所示。初创企业通常希望在每个后续融资阶段从更广泛的投资者组合中筹集更多资金。了解这些融资阶段的时间和顺序对验证成功的投资互动至关重要。通过搜索多跳事件链,图表可以提供投资网络的完整概述。通过这样做,我们可以通过不同的融资阶段连接天使投资者和风险投资家,并逐步展示他们投资组合的成功率。

图 4-1. 初创企业融资阶段和每个阶段的投资者类型
传统的关系型数据库查询为我们提供了事件的快照和单个时间点上每个实体的状态。然而,在评估投资组合时,我们需要理解投资者与他们投资的公司之间的关系以及这些关系如何发展。图表通过使用多跳查询展示投资组合作为一系列事件。我们还可以使用多跳执行复杂的搜索和过滤,比如“找到那些拥有来自顶级风投公司的董事会成员,并且曾在有成功退出的初创公司董事会上任职的公司”。
例如,我们想知道一个成功投资者的同事现在投资哪些初创企业。这种洞察力允许我们利用成功投资者基于他们过去的投资的专业知识和网络。通过多跳查询,首先选择一个或多个成功的投资者来实现这一目标。我们可能已经有一些人选,或者我们可以通过计算每个投资者的成功投资者数量来找到他们;这将是一次跳跃。第二次跳跃选择投资者所在的所有金融机构。第三次跳跃查询选择这些金融机构的同事,第四次跳跃选择这些同事参与的其他融资事件。
实施初创企业投资图和查询
TigerGraph 云为初创公司投资分析用例提供了一个入门套件。在本章的其余部分,我们将描述如何使用图模式对初创公司及其融资进行建模。然后我们将看看四种不同的图分析,这些分析可能帮助投资者选择有前途的初创公司。
Crunchbase 入门套件
使用您在第三章中创建的 TigerGraph 云账户来部署一个新的用例,并选择“企业知识图谱(Crunchbase)”。安装了这个入门套件之后,按照第三章的“加载数据和安装查询的入门套件”部分的步骤进行操作。
图模式
入门套件包含了 Crunchbase 在 2013 年收集的初创公司投资的实际数据。它具有超过 575K 个顶点和超过 664K 条边,包括 10 种顶点类型和 24 种边缘类型。图表 4-2 展示了该入门套件的图模式。我们可以立即看到,**公司**是一个作为枢纽的顶点类型,因为它连接到许多其他顶点类型。

图 4-2. 企业知识图谱(Crunchbase)的图模式(请查看更大版本的图表:oreil.ly/gpam0402)
此外,还有两种类型的自环。一个**公司**可以**收购**另一个**公司**,而一个**公司**也可以投资于另一个**公司**。另一方面,**人**类型的顶点没有自环,这意味着社交连接总是通过另一个顶点类型进行,例如**大学**、**金融机构**、**融资轮次**或**公司**。例如,如果一个**人**为一家公司工作,这种类型的关系将通过**work_for_company**边类型来表示。
在表格 4-1 中,我们描述了入门套件中的 10 种顶点类型。从描述中我们可以看到,**公司**顶点与许多其他顶点类型有潜在的关系。有些甚至有多个连接到**公司**的关系类型。例如,一个**人**可以投资于一个**公司**,但也可以为一个**公司**工作。
表格 4-1. Crunchbase 入门套件中的顶点类型
| 顶点类型 | 描述 |
|---|---|
**公司** |
一家公司 |
**融资轮次** |
一次公司投资或获得资金的事件 |
**人** |
为**公司**工作或投资的自然人 |
**大学** |
一个大学机构 |
**金融机构** |
对**公司**进行投资的金融机构 |
**资金** |
一笔金融投资 |
**办公室** |
一家**公司**的实体办公室 |
**IPO** |
**Company**的首次公开发行 |
**产品** |
**Company**的产品或服务 |
**里程碑** |
**Company**已完成的里程碑 |
查询和分析
让我们来看看企业知识图谱(Crunchbase)入门套件中的查询。此入门套件中有四个查询。每个查询都设计为回答潜在投资者或雇主可能提出的问题。
关键角色发现
此查询查找在给定**Company**及其母公司中担任关键角色的所有人。对于**Person**来说,关键角色定义为担任其工作的**Company**的创始人、CEO、CTO、董事或高管。
投资者成功退出
针对特定投资者,此查询找出在投资者投资后的若干年内成功退出的初创企业。成功退出是指公司进行了 IPO 或被其他公司收购。查询的可视输出是给定投资者的子图,其中包括其与所有**IPO**和收购**Company**元素的关系。投资者可以是任何类型为**Person**、**Financial_Org**或**Company**的元素。
基于董事会的顶尖初创公司
此查询基于当前董事会成员在顶级投资公司(**Financial_Org**)工作,并且此前还是具有成功退出记录的前一个初创企业的董事会成员的次数对初创公司进行排名。投资公司根据它们在过去N年中投资的金额排名。根据其成功退出数量对董事会成员进行评分。此外,该查询还会过滤输出超过某一融资轮次阶段的初创公司。
基于领导者的顶尖初创公司
此查询基于其创始人之一先前在另一家**Company**早期阶段工作的次数,该公司随后取得了成功退出的次数对初创公司进行排名。搜索结果被过滤以仅查看特定行业部门。
关键角色发现
key_role_discovery查询有两个参数。第一个参数,company_name,是我们要查找在其或父公司中担任关键角色的人的目标**Company**。第二个参数k确定我们从起始company_name开始搜索父公司的跳数。由于k跳参数,此查询与图模型非常自然地契合。图 4-3 展示了两个跳的部分图遍历。从公司 Com A 开始,我们可以找到与父公司 Com B 和两位关键人物 Ben 和 Adam 的连接。然后,我们查看 Com B 是否有关键人物或另一个母公司。
现在我们来介绍一下 GSQL 的实现方式。在您的入门套件中,请查找名为key_role_discovery的查询。选择它,以便您可以看到代码。
首先,我们声明一些累加器³,用于收集我们的输出对象@@output_vertices和@@output_edges。我们还声明visited来标记查询已经遇到的顶点,以避免重复计数或循环搜索。在这个数据集中,如果时间变量没有真实值,则设置为代码 0,即转换为 1970 年 1 月 1 日。我们将TNULL声明为这种情况的更具描述性的名称:
OrAccum @visited;
SetAccum<VERTEX> @@output_vertices;
SetAccum<EDGE> @@output_edges;
DATETIME TNULL = to_datetime("1970-01-01 00:00:00");

图 4-3. 查找在公司及其母公司中拥有关键角色的员工的图遍历模式
接下来,我们选择所有公司元素,其name属性与输入参数company_name匹配。函数lower(trim())删除任何前导或尾随空格,并将所有字母转换为小写,以便不区分大小写。每个名称匹配的顶点都添加到@@output_vertices集合中,并标记为@visited:
Linked_companies (ANY) = SELECT tgt
FROM Company:tgt
WHERE lower(trim(tgt.name)) == lower(trim(company_name))
ACCUM @@output_vertices += tgt
POST-ACCUM tgt.@visited = TRUE;
现在,我们开始一个WHILE循环,以查找高达k层深度的关键人物和母公司。在每次迭代中,我们选择所有具有invested_by_company、acquired_by或work_for_company边缘连接到公司或个人的Company元素。这是选择顶点和边缘描述性名称重要性的很好示例:
WHILE TRUE LIMIT k DO
Linked_companies = SELECT tgt
FROM Linked_companies:s
- ((invested_by_company> | acquired_by> | work_for_company):e)
- (Company | Person):tgt
此SELECT块还有更多内容。其WHERE子句对所选公司和个人执行额外的过滤。首先,为了确保我们在正确的方向上遍历公司到个人的边缘,我们要求源顶点(使用别名s)是公司。我们还要求在之前未访问过目标顶点(NOT tgt.@visited)。然后,如果边缘类型是work_for_company,则职位标题必须包含“founder”、“CEO”、“CTO”、“[b]oard [of] directors”或“[e]xecutive”:
WHERE s.type == "Company" AND tgt.@visited == FALSE AND
(e.type == "work_for_company" AND
(e.title LIKE "%founder%" OR e.title LIKE "%Founder%" OR
e.title LIKE "%CEO%" OR e.title LIKE "% ceo%" OR
e.title LIKE "%CTO%" OR e.title LIKE "% cto%" OR
((e.title LIKE "%oard%irectors%" OR e.title LIKE "%xecutive%")
AND datetime_diff(e.end_at, TNULL) == 0))
) OR
e.type != "work_for_company"
然后,我们将选定的顶点和边缘添加到我们的累加器@@output_vertices和@@output_edges中,并标记顶点为visited。
最后,我们以图形和 JSON 数据的形式显示所选公司和个人及其相互连接的边缘。行Results = {@@output_vertices}从SetAccum<VERTEX>创建一个顶点集。如果直接打印@@output_vertex,只会看到顶点的 ID。像Results这样打印顶点集将显示所有顶点的属性:
IF @@output_vertices.size() != 0 THEN
Results = {@@output_vertices}; // conversion to output more that just id
PRINT Results;
PRINT @@output_edges;
ELSE
PRINT "No parties with key relations to the company found within ", k,
" steps" AS msg;
GSQL: 打印顶点
为了效率起见,包含顶点的累加器仅存储它们的 ID。要打印顶点属性,请将累加器复制到常规顶点集中并打印顶点集。
在图 4-4 中,我们展示了company_name = LuckyCal and k = 3时的输出。虽然中心公司的名称缺失,但根据创始人列表,包括马克·扎克伯格,我们可以看到它是 Facebook。

图 4-4. 当company_name = LuckyCal和k = 3 时的关键角色发现(请在oreil.ly/gpam0404查看此图的更大版本)
投资者成功退出
查询investor_successful_exits查找给定投资者的成就,其中成就由导致 IPO 和收购的投资数量衡量。它接受三个参数。investor_name是我们想要了解成就的目标投资者的名称,而investor_type是投资者的类型,可以是**Company**、**Person**或**Financial_Org**。我们使用year来测试资金注入后是否很快发生了退出。我们可以通过以下图遍历模式来回答此查询,如图 4-5 所示。从选定的投资者顶点(investor_name)开始:
-
跳到投资者参与的融资轮次。
-
跳到这些轮次资助的公司。
-
跳到退出事件(
acquired_by或company_ipo边缘)。

图 4-5. 寻找具有成功退出的投资者的图遍历模式(请在oreil.ly/gpam0405查看此图的更大版本)
我们将向您展示investor_successful_exits查询的 GSQL 代码的关键部分。
我们首先声明几个变量。我们想展示从投资者到成功退出的路径。当我们通过图遍历时,@parent_vertex_set和@parent_edge_set充当面包屑。在访问每个新顶点时,我们使用它们记录我们到达那里的方式。在达到结尾后,我们使用这些累加器找到回溯的路径。在回溯过程中,我们将所有这些路径上的顶点和边缘收集到全局累加器@@result_vertex_set和@@result_edge_set中:
SetAccum<VERTEX> @parent_vertex_set;
SetAccum<EDGE> @parent_edge_set;
SetAccum<VERTEX> @@result_vertex_set;
SetAccum<EDGE> @@result_edge_set;
首先,我们使用CASE语句和investor_type参数来创建Start顶点集,以选择用户指定的投资者类型:
Start (ANY) = {};
CASE lower(trim(investor_type))
WHEN "person" THEN Start = {Person.*};
WHEN "company" THEN Start = {Company.*};
WHEN "financialorg" THEN Start = {Financial_Org.*};
END;
我们通过找到拥有investor_name的个体投资者来完成前期工作。如果投资者是**Person**,我们检查称为fullname的属性;否则,我们检查称为name的属性:
Investor (ANY) = SELECT inv
FROM Start:inv
WHERE ( inv.type == "Person"
AND lower(trim(inv.fullname)) == lower(trim(investor_name))
) OR lower(trim(inv.name)) == lower(trim(investor_name));
现在我们开始我们的图遍历。首先,我们选择所有与投资者相关联的**Funding_Rounds**。在每个选定的**Funding_Rounds**顶点,我们存储到达那里的顶点和边缘的标识。这一跳的目标顶点存储在名为Funding_rounds的变量中:
Funding_rounds = SELECT tgt
FROM Investor:s - ((investment_from_company | investment_from_person |
investment_from_financialORG):e) - Funding_Rounds:tgt
ACCUM
tgt.@parent_vertex_set += s,
tgt.@parent_edge_set += e;
现在我们从所选的融资轮次再跳到它们投资的公司。一个投资者可以在一家公司的多个融资轮次投资。例如,在图 4-6 中,我们看到 Ted Leonsis 在 Revolution Money 的 B 轮和 C 轮都有投资。投资者的成功应该从他们的第一笔投资开始评估。每个**Funding_Rounds**顶点将其funded_at参数值发送到MinAccum @min_invested_time,以记住给定的最小值:
Invested_companies = SELECT tgt
FROM Funding_rounds:s - ((company_funding_rounds):e) - Company:tgt
ACCUM
tgt.@parent_vertex_set += s,
tgt.@parent_edge_set += e,
tgt.@min_invested_time += s.funded_at;
最后,对于每家接受投资的公司,我们查看是否在所需的时间窗口内有成功的退出。company_ipo或acquired_by边缘表示退出。如果是 IPO,我们检查 IPO 日期(public_at属性)是否晚于投资日期,但不超过years值。如果是收购事件,则对acquired_at属性执行类似的检查:
IPO_acquired_companies = SELECT tgt
FROM Invested_companies:s - ((company_ipo | acquired_by>):e) -:tgt
ACCUM
tgt.@parent_vertex_set += s,
tgt.@parent_edge_set += e,
// See if IPO occurred within `years` after Investor's investment
IF (e.type == "company_ipo"
AND datetime_diff(tgt.public_at, s.@min_invested_time) > 0
AND datetime_diff(
tgt.public_at, s.@min_invested_time) <= years * SECS_PER_YR)
// See if Acquisition occurred within `years` of investment
OR (e.type == "acquired_by"
AND datetime_diff(e.acquired_at, s.@min_invested_time) > 0
AND datetime_diff(
e.acquired_at, s.@min_invested_time) <= years * SECS_PER_YR)
THEN @@result_vertex_set += tgt
END;

图 4-6。当investor_name = Ted Leonsis 和years = 3 时的投资者成功退出(请在oreil.ly/gpam0406查看更大的版本)
如果我们只想知道我们的投资者有多少成功退出,或者这些退出的公司详细信息,我们可能已经完成了。然而,通过图形方式展示从投资者→融资→公司→退出的路径是有趣的,就像在图 4-6 中那样。为了收集这些信息,我们从退出顶点向后遍历到投资者,使用我们之前设置的面包屑(@parent_vertex_set和@parent_edge_set):
Children = {@@result_vertex_set};
PRINT Children.size() as Num_Successful_Exits;
WHILE(Children.size() > 0) DO
Start = SELECT s
FROM Children:s
ACCUM
@@parents += s.@parent_vertex_set,
@@result_edge_set += s.@parent_edge_set;
@@result_vertex_set += @@parents;
Children = {@@parents};
@@parents.clear();
基于董事会的顶级创业公司
top_startups_based_on_board查询通过两种排名方式增加了一些复杂性:表现最佳的投资公司和这些投资公司中表现最佳的领导者。首先,它识别了近年来投资最多资金的**Financial_Org**实体。然后,我们根据这些组织中他们在创业公司**Company**董事会上的次数及其指导成功退出的情况来排名**Persons**。然后,我们显示任何目前拥有这些成功高管作为董事会成员的退出前公司**Companies**。
top_startups_based_on_board查询有四个输入参数:
k_orgs
我们希望在我们的选择范围内包括顶级金融机构的数量
num_persons
选择顶级董事会成员的数量
max_funding_round
将最终有希望的创业公司列表的过滤器排除掉那些在max_funding_round之后接受过投资的公司
past_n_years
设置由**Financial_Org**投资的时间窗口
我们可以根据以下步骤实现这个查询,其中大部分对应于图遍历;这些步骤在 图 4-7 中有详细说明:
-
计算过去 N 年中每个
**Financial_Org**进行了多少次**Funding_Rounds**投资 [Hop 1]. -
按照投资金额对
**Financial_Org**进行排名,并选取前k_orgs名。 -
找到在前 k 个
**Financial_Org**(来自步骤 2)工作的**Persons**[Hop 2]. -
找到这些
**Persons**(来自步骤 3)在哪些公司担任董事会成员 [Hop 3]. -
按照以下步骤排名
**Persons**(来自步骤 3),根据他们在成功退出之前在**Company**董事会上任职的次数进行排名 [Hop 4]. -
找到在成功退出之前有顶级董事会成员
**Person**的**Company**顶点(来自步骤 5)。通过资金轮次截止日期筛选这些公司 [Hop 5].
这个查询声明了几个累加器和其他变量来辅助进行这个计算。还有两个有趣的数据准备步骤。一个是将一些货币汇率存储在查找表中。另一个是制作一个包含所有资金轮次代码 @@allowed_funding_rounds 的列表,直到我们的 max_cutoff_round。

图 4-7. 基于成功的董事会成员从顶级金融机构中找到有前途的创业公司的图遍历模式
我们的第一个图遍历也是一个数据准备步骤。我们的 Crunchbase 图模式在边上存储了公司的 IPO 或收购日期。复制这些数据,使其也可用于公司本身:
Comp = SELECT c
FROM (Company):c - ((company_ipo|acquired_by>):e) - (IPO|Company):x
ACCUM
CASE WHEN
e.type == "company_ipo" AND datetime_diff(x.public_at, T0) != 0
THEN
c.@t_exit += x.public_at
END,
CASE WHEN
e.type == "acquired_by" AND datetime_diff(e.acquired_at,T0) != 0
THEN
c.@t_exit += e.acquired_at
END;
在下一步中,我们将 **Financial_Org** 顶点与它们在过去 n 年中的投资 **Funds** 进行连接,然后统计投资,并选取前 k 个组织。WHERE 子句根据所需时间范围进行过滤。为了选取前 k 个,GSQL 提供了 ORDER BY 和 LIMIT 子句,就像 SQL 中一样:
Top_orgs = SELECT org
FROM (Financial_Org):org - (financial_funds:e) - Funds:f
WHERE datetime_diff(END_2013, f.funded_at) <= past_n_years*SECS_PER_YR
ACCUM org.@amount +=
(f.raised_amount / @@currency2USD.get(f.raised_currency_code)),
f.@visited = TRUE
ORDER BY org.@amount DESC
LIMIT k_orgs;
高级 GSQL 用户有时会选择使用 HeapAccum 而不是 ORDER BY/LIMIT,因为对一个小堆进行排序比 ORDER BY 执行的全局排序消耗的计算机内存少。
接下来,我们选择在这些顶级金融机构(来自前一步骤的 Top_org 顶点集合)工作的所有员工(**Person** who **work_for_fOrg**):
Persons_at_top_orgs = SELECT p
FROM Top_orgs:o - (work_for_fOrg:e) - Person:p;
从这些 Persons_at_top_orgs 中,我们想要选择那些满足以下条件以帮助领导成功退出的人:
-
他们的职位包括“董事会”。
-
公司已经退出 (
c.@t_exit.size() != 0)。 -
该人有一个有效的工作开始日期(
datetime_diff(w.start_at, T0) != 0)。 -
公司的退出是在董事成员加入之后发生的。
下面的代码执行了这个选择:
Top_board_members = SELECT p
FROM Persons_at_top_orgs:p - (work_for_company:w) - Company:c
WHERE (w.title LIKE "%Board%" OR w.title LIKE "%board%")
AND c.@t_exit.size() != 0 AND datetime_diff(w.start_at, T0) != 0
AND datetime_diff(c.@t_exit.get(0), w.start_at) > 0
找到这些成功初创企业董事会成员后,我们建立了这些成功初创企业的列表(@@comp_set)。我们还记录每个这样的 **Company** 记录其关键董事成员 (c@board_set),并统计每个关键人物的成功退出次数 (p.@amount += 1)。最后,我们取最多产的董事成员(通过 ORDER BY 和 LIMIT):
ACCUM
@@comp_set += c,
c.@board_set += p,
p.@amount += 1
ORDER BY p.@amount DESC
LIMIT num_persons;
然后,我们找到所有具有 **Company** 实体且具有 top_board_member 的前退出公司:
Top_startups = SELECT c
FROM Top_board_members:s - (work_for_company:w) - Company:c
WHERE (w.title LIKE "%Board%" OR w.title LIKE "%board%")
AND w.start_at != T0
AND c.status == "operating" AND c.@t_exit.size() == 0;
最后,我们仅包括那些 **Funding_Rounds** 早期足够满足 max_cutoff_round 限制的前退出公司:
Top_early_startups = SELECT r
FROM Top_startups:s - (company_funding_rounds:e) - Funding_Rounds:r
ACCUM
s.@visited += TRUE,
IF @allowed_funding_rounds.contains(r.funding_round_code) THEN
r.@visited = TRUE
ELSE
s.@early += FALSE
END;
查询的其余部分用于从顶级董事成员追溯显示他们工作过的公司及其成功退出情况。
图 4-8 展示了当我们设置 k_orgs = 10, num_persons = 2, max_funding_round = b,并且 past_n_years = 10 时的结果。两位关键董事成员分别是吉姆·戈茨和吉姆·布雷尔,他们都在 Accel Partners 工作。戈茨有四次成功退出,而布雷尔有三次。推荐的初创企业是与戈茨或布雷尔相关联但尚未退出的公司:Nimble Storage、Ruckus Wireless、HubSpot、Booyah 和 Etsy。⁴

图 4-8. 基于董事会成员的顶级初创企业图输出(请在 oreil.ly/gpam0408 查看此图的更大版本)
基于领导者的顶级初创企业
在这个入门套件中的最后一个查询与前一个类似,只是不是寻找顶级董事会成员,而是寻找创始人。此查询包含三个参数。max_funding_round 是资金轮次截止时间,意味着我们只选择其投资轮次早于 max_funding_round 的初创企业。return_size 是我们要从查询中检索的顶级初创企业的数量,sector 是我们要筛选结果的行业领域。
图 4-9 说明了我们如何将此查询构建为一系列图遍历操作:
-
找到所有已经上市或被收购的公司 [Hop 1]。
-
找到在第 1 步 [Hop 2] 中对这些公司做出贡献的员工。
-
找到创始人也是第 2 步 [Hop 3] 中关键员工的初创企业。根据截止轮次和行业领域筛选初创企业。
-
找到创始人具有最成功连接的公司。

图 4-9. 根据成功创始人查找有潜力的初创企业的图遍历模式
此查询介绍了一些我们以前没有见过的数据结构:TUPIL和HeapAccum。GSQL 元组是由一组基本现有类型组成的用户定义数据类型。Company_Score元组由一个Company顶点和一个整数组成。HeapAccum管理一个按照其分数值排序的元组列表,最多可以包含return_size个公司:
TYPEDEF TUPLE<VERTEX<Company> company, INT score> Company_Score;
HeapAccum<Score_Results>(return_size, score DESC) @@top_companies_heap;
我们还定义了两个嵌套的MapAccums。映射就像是一个查找表。查看@@person_company_leave_date_map的结构定义,这意味着对于给定的人员,我们记录了该人员何时离开了给定的公司。对于@@person_company_employment_map,我们记录了Person和Company之间的就业关系:
// Declare map to store when an employee left which company
MapAccum<VERTEX<Person>,
MapAccum<VERTEX<Company>, DATETIME>> @@person_company_leave_date_map;
MapAccum<VERTEX<person>,
MapAccum<VERTEX<Company>, EDGE>> @@person_company_employment_map;
现在我们找出所有进行了 IPO 或被其他公司收购的公司。为了更清晰的代码,一个代码块找到了 IPO 公司,另一个专注于收购,然后我们合并这两组公司。对于 IPO,我们从IPO顶点遍历到Company顶点。我们检查 IPO 是否具有有效的public_at属性。一旦选择,我们将每个Company与回到IPO顶点的路径以及public_at日期标记起来。我们将该公司标记为不再处于创业阶段:
IPO_companies = SELECT c
FROM IPO:i - (company_ipo:e) - Company:c
//Filter out companies with null acquisition time (not yet acquired)
WHERE datetime_diff(i.public_at, TNULL) != 0
ACCUM
c.@parent_vertex_set += i,
c.@parent_edge_set += e,
c.@min_public_date = i.public_at,
c.@is_still_startup += FALSE;
类似的代码块找到了acquired_companies。边的类型不同(acquire而不是company_ipo),有效数据属性也不同(acquired_at而不是public_at)。
然后我们将这两个块的输出集合合并:
IPO_acquired_companies = IPO_companies UNION Acquired_companies;
接下来,我们选择所有曾在成功退出公司工作过的人员。对于每个这样的人员,我们将他们的相关信息存储到前面描述的嵌套映射中。注意使用->运算符指定映射的key -> value对:
Startup_employees = SELECT p
FROM IPO_acquired_companies:c - (work_for_company:e) - Person:p
WHERE datetime_diff(e.start_at, TNULL) != 0
AND datetime_diff(e.end_at, TNULL) != 0
AND datetime_diff(e.start_at, c.@min_public_date) < 0
ACCUM
@@person_company_employment_map += (p -> (c -> e)),
@@person_company_leave_date_map += (p -> (c -> e.end_at));
现在我们找出那些成功退出员工目前是创始人的初创公司,按行业进行筛选。在WHERE子句中执行初创公司状态和创始人状态的检查:
New_startups = SELECT c
FROM startup_employees :p - (work_for_company :e) - Company :c
WHERE c.@is_still_startup
AND c.@early_startup
AND c.status != "acquired"
AND c.status != "ipo"
AND e.title LIKE "%ounder%"
AND lower(trim(c.category_code)) == lower(trim(sector))
AND datetime_diff(e.start_at, TNULL) != 0
AND datetime_diff(e.end_at, TNULL) != 0
在选择这些初创公司之后,我们统计了创始人的过去成功经历:
ACCUM
// Tally the founder:past-success relationships per new company
FOREACH (past_company, leave_date)
IN @@person_company_leave_date_map.get(p) DO
IF datetime_diff(e.start_at, leave_date) > 0 THEN
p.@parent_edge_set +=
@@person_company_employment_map.get(p).get(past_company),
p.@company_list += past_company,
c.@parent_vertex_set += p,
c.@parent_edge_set += e,
c.@sum_ipo_acquire += 1
END
END
HAVING c.@sum_ipo_acquire > 0;
选择那些创始人与成功退出公司具有最多关系的公司。我们使用之前描述的HeapAccum来根据创始人成功退出的次数对公司进行排名:
Top_companies = SELECT c
FROM Startups_from_employees:c
ACCUM @@top_score_results_heap += Score_Results(c, c.@sum_ipo_acquire);
PRINT @@top_score_results_heap;
FOREACH item IN @@top_score_results_heap DO
@@output_vertex_set += item.company;
END;
图 4-10 显示了当输入参数为max_funding_round = c,return_size = 5,sector = software 时的结果。右侧列出了五个选定的初创公司。查看顶部第二家公司时,我们从右向左阅读:Packet Trap Networks 之所以被选中,是因为创始人 Steve Goodman 曾是 Lasso Logic 的创始人/CEO,后者被 SonicWALL 收购。

图 4-10. 基于领导者的顶级初创企业的图表输出(请查看此图的更大版本:oreil.ly/gpam0410)
章节总结
在本章中,我们看到如何利用图分析来回答重要问题,并获得关于初创企业投资的宝贵见解。查看 Crunchbase 数据的图模式,我们发现这些数据高度互连。在投资建议的情况下,我们经常将过去的表现作为可能未来结果的指标。因此,我们寻找一种模式(过去的成功)并看看是否存在重复这种模式的潜力。这种类型的模式搜索或相似性搜索是图分析的典型应用。
我们在本章讨论了四个查询,以识别可以帮助我们调查投资机会的模式。第一个查询识别出在公司内担任关键角色的所有人员。第二个查询标识了投资者成功退出的成功初创企业。第三个查询显示了拥有成功董事会成员的初创企业的排名。第四个查询显示了拥有成功创始人的初创企业的排名。每个查询都展示了如何利用多跳来增强我们的分析。
本章演示了几个 GSQL 语言的特性和技术,例如:
-
使用
WHILE循环深入搜索多个级别 -
使用布尔累加器标记访问过的顶点
-
在多步遍历期间,使用
parent_vertex和parent_edge标记顶点作为面包屑,以便稍后恢复路径 -
在
SELECT块中使用ORDER BY和LIMIT子句来查找排名靠前的顶点,类似于在 SQL 中选择排名靠前的记录
¹ Alex Wilhelm,“在 2020 年,风险投资公司每天向总部位于美国的初创企业投资 4.28 亿美元”,TechCrunch,2021 年 1 月 19 日,https://techcrunch.com/2021/01/19/in-2020-vcs-invested-428m-into-us-based-startups-every-day。
² Sandeep Babu,“初创企业统计数据——您需要了解的数字”,Small Business Trends,2023 年 3 月 28 日,https://smallbiztrends.com/2022/12/startup-statistics.html。
³ 累加器在第三章中有描述。
⁴ 我们正在分析 Crunchbase 的 2013 年数据。其中一些初创企业取得了成功,而其他一些则没有。
第五章:检测欺诈和洗钱模式
在本章中,我们面对严重的欺诈和洗钱问题。欺诈通常由一个或多个方参与作为一个多步骤的过程进行。有时,区分欺诈或洗钱活动与合法活动的唯一方法是检测活动的特征或异常模式。通过图模型化活动和关系,我们能够检测到这些模式,并检查它们的频率以便发现可疑活动。
完成本章后,您应能够:
-
描述多方协调活动在图模式方面的表现
-
使用多跳或迭代单跳图遍历执行深度搜索
-
描述双向搜索及其优势
-
理解时间戳的使用以找到时间顺序
目标:检测金融犯罪
金融机构负责通过经济基础设施防止犯罪资金流动。根据《金融行动特别工作组》(FATF)的数据,非法资金占全球 GDP 的 3.6%。¹ 一种众所周知的犯罪活动是洗钱,即通过非法手段赚取的资金的来源掩盖。根据 FATF 的数据,全球 GDP 的 2.7% 每年被洗钱。银行有法律义务调查客户的支付行为,并报告任何可疑活动。
其他类型的金融欺诈包括身份盗窃,即某人未经许可使用他人账户,以及庞氏骗局,其特征是资金从新投资者流向早期投资者,而并未真正进入外部企业。
银行已将各种应用程序和程序整合到其日常运营中,以识别和检测金融犯罪。总体而言,这些技术可以分为两个领域。
第一个调查领域,了解您的客户(KYC),研究客户概况。就像我们在第三章中看到的客户 360 应用案例一样,分析师需要进行客户尽职调查。这种客户风险评估可以在客户生命周期的多个阶段进行,比如在新客户接管(NCTO)期间或定期审查期间。
第二个调查领域,交易监控,主要关注通过银行交易识别犯罪行为。在这里,分析师试图识别发送方和受益方之间的异常支付模式。尽管这两个调查领域在银行运营和风险管理水平上经常重叠,但本章主要关注交易监控。
交易监控涉及对表现出可疑支付行为的实体进行彻底调查。分析员从被标记为可疑的实体开始这些调查,然后从那里探索高风险交互。因此,分析员不知道资金流动的完整图景,也没有看到被标记实体在整个资金路径中的可见性。为了获得这种可见性,他们必须逐步查询下一个支付交互,以构建支付网络的完整图景。因此,分析员需要一种帮助他们检索一组连续支付和参与其中支付方的方法。
解决方案:将金融犯罪建模为网络模式
传统交易监控依赖于基于规则的系统,其中客户行为与固定的风险指标进行检查。例如,这样的风险指标可能是,客户账户中收到了 15000 美元现金,并立即将这笔钱发送到多个第三方账户。这可能是正常的收入和支出活动,也可能是称为分层的洗钱技术的一部分。它表明一种可疑活动,因为它围绕着大量现金,并且那笔钱移动到多个第三方账户,使其更难追溯其来源。
依赖基于规则的风险指标存在两个主要问题。首先,分析师仍然需要对标记客户进行深入跟进调查,这涉及查询不同客户之间的连续支付。其次,由于从表格数据中提取深层模式的挑战,基于规则的风险指标在其复杂性方面存在限制。
当将这个问题建模成网络时,通过图数据模型直观地展示资金流动,更容易识别高风险模式。这样做能让我们看到资金在网络中的流动方式,以及参与这些支付交互的各方。这种图形方法解决了第一个问题,因为图形模式搜索将为分析员发现连续支付。它还解决了第二个问题,因为网络将揭示涉及方之间的所有关系,包括分析员没有明确查询的那些关系。
本书后面我们将看到,图机器学习可以更好地检测金融犯罪模式。
实施金融犯罪模式搜索
TigerGraph 提供了一个欺诈和洗钱检测的入门套件。请按照第三章的安装步骤安装入门套件。安装完成后,我们将使用入门套件设计我们的洗钱网络,并探索如何在该网络上检测可疑的支付交互。
欺诈和洗钱检测入门套件
使用 TigerGraph Cloud,在云中部署一个新的集群,并选择“欺诈和洗钱检测”作为用例。一旦安装了这个入门套件,按照章节“加载数据并安装入门套件的查询”中的步骤进行操作。
图模式
欺诈和洗钱检测入门套件包含超过 4.3M 个顶点和 7M 条边,具有四种顶点类型和五种边类型的模式。图 5-1 展示了此入门套件的图模式。

图 5-1. 欺诈和洗钱检测入门套件的图模式(在oreil.ly/gpam0501上查看更大的版本)
在表 5-1 中描述了四种顶点类型。**User**在支付交互中扮演着核心角色,可以接收和发送支付。**Transaction**是支付本身。**Device_Token**是指用于支付的设备的唯一 ID 号码,而**Payment_Instrument**则指支付所使用的工具类型。
表 5-1. 欺诈和洗钱检测入门套件中的顶点类型
| 顶点类型 | 描述 |
|---|---|
**User** |
参与支付的人 |
**Transaction** |
一笔支付 |
**Device_Token** |
用于执行**Transaction**的唯一 ID 号码 |
**Payment_Instrument** |
执行支付的工具 |
**User**和**Transaction**之间有两种关系类型。**User**可以接收交易,用**User_Receive_Transaction**表示,或者**User**可以发送交易,用**User_Transfer_Transaction**标记。**User**可以引用另一个**User**,这由**User_Refer_User**表示。边类型**User_to_Payment**连接了**User**和用于进行交易的**Payment_Instrument**(支票、现金、票据等)。最后,边类型**User_to_Device**将**User**连接到用于电子支付时使用的**Device_Token**。
查询与分析
此入门套件中包含的查询展示了如何利用图来帮助分析师检测高风险支付行为,以打击欺诈和洗钱活动。我们首先提供每个查询所查找的模式的高级描述,以及这如何与交易欺诈或洗钱相关联。然后我们深入研究其中的三个查询,以便更好地了解它们的实现方式。
圆检测
此查询检测资金在循环流动时。它选择形成时间序列的**Transaction**元素,从输入的**User**开始,然后返回到该**User**。如果回来的金额接近出去的金额,可能表明存在洗钱行为。
邀请用户行为
此查询寻找**User_Refer_User**行为的可疑模式,可能表明一个User正在与其他方合作以收集推荐奖金。它查看源User周围两跳内的推荐数以及这些用户进行的交易数。
多次交易
此查询展示了两个**User**元素网络之间的支付。从输入的**Transaction**开始,第一组是与发送方相关的**User**元素网络。第二组是来自接收方的**User**元素网络。查询可视化了两个网络及其之间的资金流动。
重复用户
此查询发现向相同接收者发送资金的**User**元素之间是否存在连接。它从接收资金的输入**User**开始,并选择所有向该输入**User**发送资金的其他**User**元素。然后,它使用**Device_Token**、**Payment_Instrument**和**User**检查这些发送者之间是否存在路径。
同一接收者发送者
此查询检测**User**是否使用假账户向自己发送资金。给定一个**Transaction**,如果接收者和发送者可以通过**Device_Token**和**Payment_Instrument**相互关联,则此查询返回真。
转账金额
此查询在给定的时间窗口内查找从与源**User**几跳相连的Users转出的总金额。虽然不直接可疑,但大量资金可能有助于建立反洗钱分层的案件。
现在我们更详细地查看了邀请用户行为、多次交易和圈子检测查询。
邀请用户行为
此模式假设一个User可以通过引荐许多新的User来赚取分层推荐奖金,用于电子支付服务。此查询包含了一个双跳遍历实现,如图 5-2 所示。我们从给定的input_user开始遍历。第一跳选择所有由此input_user邀请的User元素。然后,通过第二跳,收集所有一级受邀者邀请的User元素。然后我们聚合这些被邀请者的交易金额。如果直接转账金额很高,而来自二级被邀请者的聚合金额很低或为零,则input_user是一个欺诈的User。其背后的直觉是input_user有很多虚假的推荐,通过推荐奖金来推动自身进行大量交易。

图 5-2. 图遍历模式,用于检测执行活动以赚取推荐奖金的欺诈用户
首先,我们声明一些累加器变量来存储我们聚合的数据:
SumAccum<INT> @@num_invited_persons;
SumAccum<FLOAT> @@total_amount_sent;
SetAccum<EDGE> @@edges_to_display;
SumAccum @@num_invited_persons 计算二级受邀者的数量。SumAccum @@total_amount_sent 聚合了来自一级受邀者的所有交易金额。SumAccum @@edges_to_display 收集了输入User 和引荐User 之间的所有边(User_Ref_User),以便可视化系统知道如何显示它们。
然后,我们找到由源User引荐的一级受邀者。我们将Start User 和一个受邀者之间的每条边保存在@@display_edge_set中:
Start = {input_user};
First_invitees = SELECT t
FROM Start:s -(User_Refer_User>:e)- :t
ACCUM @@edges_to_display += e;
注意
在FROM子句中,我们不需要指定我们要针对哪种类型的顶点,因为边的类型(User_Refer_User)只允许一个类型的目标顶点(User)。
接下来,我们累加这些一级受邀者发送出去的金额。每个Transaction都有一个名为amount的属性:
Trans = SELECT t
FROM First_invitees:s -(User_Transfer_Transaction>:e)- :t
ACCUM
@@total_amount_sent += t.amount,
@@edges_to_display += e;
最后,我们得到了由一级受邀者引荐的额外受邀者:
Second_invitees = SELECT t
FROM First_invitees:s -(User_Refer_User>:e)- :t
WHERE t != input_user
ACCUM @@edges_to_display += e
POST-ACCUM (t) @@num_invited_persons += 1;
这个搜索看起来非常像第一跳,但有两个额外的步骤:
-
我们检查我们没有回到源
User。 -
我们计算二级受邀者的数量。
如果您用三个建议的输入用户(115637、25680893、22120362)运行算法,您会发现他们推荐了一个或少数用户,这些用户又没有推荐用户。查看 JSON 结果,您会看到总支付金额在 0 美元到 709 美元之间。
多次交易
分析人员认为,犯罪分子经常在两个网络之间转移资金。以下查询展示了这一直觉。给定任何输入事务,第一个网络由该事务的发送方的相关账户组成,第二个网络由接收方的相关账户组成。然后,我们查找这两个网络中所有方的支付活动。该查询通过图 5-3 中说明的执行流程组装这些网络,并查找它们之间的任何交互。

图 5-3. 从发送方和接收方找到事务网络的图遍历模式
我们首先通过遍历**User_Transfer_Transaction**或**User_Receive_Transaction**边类型来选择给定**Transaction**的发送方和接收方**User**元素:
Sender_receiver (ANY) = SELECT t
FROM Start:s
-((<User_Receive_Transaction|<User_Transfer_Transaction):e)- :t
注意
在FROM子句中,我们从**Transaction**(source_transaction)到**User**元素进行遍历,这是**User_Receive_Transaction**和**User_Transfer_Transaction**边的反向方向。这就是为什么方向箭头指向左侧并位于边类型名称的左侧。或者,如果这些边有定义反向边类型,我们可以使用它们的反向边代替(并使用向右的箭头)。
我们使用用例来确定**User**是**Transaction**的接收方还是发送方。如果一个**User**通过**User_Receive_Transaction**连接到一个**Transaction**,我们将@from_receiver设置为 true,并将该**User**添加到@@receiver_set中。在其他情况下,**User**是**Transaction**的发送方,因此我们将@from_sender设置为 true,并将此**User**添加到@@sender_set中:
CASE WHEN e.type == "User_Receive_Transaction" THEN
t.@from_receiver += TRUE,
@@receiver_set += t
ELSE
t.@from_sender += TRUE,
@@sender_set += t
现在我们知道了发送方和接收方,我们找到属于接收方或发送方的**User**元素。也就是说,我们遍历**User_to_Device**或**User_to_Payment**边,并将**User**元素添加到@@sender_set或@@receiver_set(如果它们在四个跳跃内存在(WHILE Start.size() > 0 LIMIT MAX_HOPS DO)。因为完成一笔交易需要两个跳跃(发送方 → 事务 → 接收方),四个跳跃等于两个交易的链:
WHILE Sender_receiver.size() > 0 LIMIT MAX_HOPS DO
Sender_receiver = SELECT t
FROM Sender_receiver:s -((User_to_Device|User_to_Payment):e)- :t
WHERE t.@from_receiver == FALSE AND t.@from_sender == FALSE
ACCUM
t.@from_receiver += s.@from_receiver,
t.@from_sender += s.@from_sender,
@@edges_to_display += e
POST-ACCUM
CASE WHEN t.type == "User" AND t.@from_sender == TRUE THEN
@@sender_set += t
WHEN t.@from_receiver == TRUE THEN
@@receiver_set += t
如果我们最终到达一个**User**顶点类型,并且该**User**是发送方,我们将该**User**添加到@@sender_set中。如果t.@from_receiver为 true,则**User**属于接收方,我们将该**User**添加到@@receiver_set中。
形成发送方和接收方组后,我们现在寻找除源事务之外连接发送方和接收方组的其他事务。首先,我们查找与接收方集合相邻的事务:
Receivers = {@@receiver_set};
Receivers = SELECT t
FROM Receivers:s
-((User_Receive_Transaction>|User_Transfer_Transaction>):e)- :t
….
然后,我们查找与发送方集合相邻的事务:
Senders = {@@sender_set};
Connecting_transactions = SELECT t
FROM Senders:
-((User_Receive_Transaction>|User_Transfer_Transaction>):e)- :t
WHERE t != input_transaction
ACCUM
t.@from_sender += s.@from_sender,
@@edges_to_display += e
HAVING t.@from_receiver AND t.@from_sender;
HAVING子句检查事务是否被视为接收组和发送组的一部分。
运行使用建议的任一交易 ID(32、33 或 37)的查询时,输出看起来像是一个连接的社区,因为除了输入交易之外,还有至少一笔交易将发送方社区与接收方社区连接在一起。尝试不同的输入 ID,输出很可能看起来像是两个独立的社区,仅通过输入交易连接在一起。
圆圈检测
洗钱的本质是在足够多的参与者之间转移资金,使得追踪其起源成为一个挑战。罪犯有几种路由模式来掩盖其非法资金的来源。一个流行的转移模式是通过各种中介最终返回到其中一个发件人的资金。在这种情况下,资金以循环模式传播。循环资金流动本身并不是犯罪。使其成为犯罪的是意图以及其中任何一个过渡本身是否属于欺诈行为。循环流动的特征——循环的大小、转移的资金金额、返回给发送者的资金百分比、交易之间的时间延迟以及多少个个别交易是不寻常的——也是有用的指标。
在图形中,我们可以比传统数据库更容易地检测到这种循环模式,因为我们可以从一个交易跳到下一个,直到一个交易到达发起者。正如我们在第二章中所解释的那样,图的跳跃在计算上比关系数据库中的表连接要便宜得多。
在图 5-4 中,我们看到这样的循环资金流动。在这个例子中,亚当是发起者,向本发送了$100。本发送了$60 给科尔,她向黛西发送了$40,后者又向亚当发送了$100。我们在这个例子中展示,本、科尔和黛西没有把他们收到的金额完全发送给链条中的下一个人。罪犯这样做是为了通过使起始金额分散到各种中间人中,增加另一层噪音,使得查明起始者及洗钱金额更加困难。

图 5-4. 示例循环资金流动
查询 circle_detection 查找从给定的User(source_id)开始的所有循环交易链,每个循环最多有一定数量的交易(max_transactions)。由于每个交易有两次跳跃(发送者 → 交易 → 接收者),因此每个循环可以有多达两次跳跃。要成为有效的循环,循环中的交易序列必须在时间上向前移动。例如,为了使这个循环有效:
source_id → txn_A → txn_B → txn_C → source_id
然后 txn_A.ts < txn_B.ts < txn_C.ts,其中 ts 是交易的时间戳。
由于有很多可能的路径需要检查,查询的实现采用了几种性能和过滤技术。第一种是双向搜索,从起点向前搜索同时从终点向后搜索。进行两个半长度的搜索比进行一个全长度的搜索要快。当两个搜索相交时,就找到了一条完整的路径。
第二种技术过滤掉不能满足向前时间旅行要求的路径。
Seed = {source_id};
Seed = SELECT src
FROM Seed:src - ((User_Transfer_Transaction>|User_Receive_Transaction>):e)
- Transaction:tgt
ACCUM
CASE WHEN
e.type == "User_Transfer_Transaction"
THEN
@@min_src_send_time += tgt.ts
ELSE
@@max_src_receive_time += tgt.ts
END
…
HAVING @@max_src_receive_time >= @@min_src_send_time;
从source_id开始,向前一步(**User_Transfer_Transaction**)和向后一步(**User_Receive_Transaction**)。找到source_id发送的任何交易的最早时间(@@min_src_send_time)和接收的最晚时间(@@max_src_receive_time)。检查确保@@max_src_receive_time >= @@min_src_send_time。这些全局限制也将用于稍后检查其他交易的合理性,这些交易是循环路径的候选者。
然后我们开始搜索的第一阶段。从source_id开始,向前走两步(相当于一次交易)。以图 5-4 为例,这将从 Adam 到 Ben。同时向后走两步(Adam 到 Daisy)。迭代这种步骤的组合,向前(或向后)移动,直到每个方向都绕过一个最大大小的圆圈的一半。表 5-2 显示了如果我们考虑图 5-4 的图表,则会遍历的路径。
表 5-2. 使用图 5-4 的图表的前向和后向路径
| 迭代 | 1 | 2 | 3 |
|---|---|---|---|
| 前向 | Adam→Ben | Ben→Cor | Cor→Daisy |
| 反向 | Adam→Daisy | Daisy→Cor | Cor→Ben |
下面的代码片段显示了前向遍历的一个简化版本的一次迭代。为简洁起见,省略了时间和步骤约束的检查:
Fwd_set = SELECT tgt
FROM Fwd_set:src - (User_Transfer_Transaction>:e) - Transaction:tgt
WHERE tgt.ts >= @@min_src_send_time
AND src.@min_fwd_dist < GSQL_INT_MAX
AND tgt.@min_fwd_dist == GSQL_INT_MAX
ACCUM tgt.@min_fwd_dist += src.@min_fwd_dist + 1
… // POST-ACCUM clause to check time and step constraints
;
Fwd_set = SELECT tgt
FROM Fwd_set:src - (<User_Receive_Transaction:e) - User:tgt
WHERE src.@min_fwd_dist < GSQL_INT_MAX
AND tgt.@min_fwd_dist == GSQL_INT_MAX
ACCUM tgt.@min_fwd_dist += src.@min_fwd_dist + 1
… // POST-ACCUM clause to check time and step constraints
HAVING tgt != source_id;
从表 5-2 可以看出,在第二次迭代后,前向路径和后向路径在一个共同点 Cor 相遇。我们有一个圆圈!但是等等。如果 Ben→Cor 的时间戳晚于 Cor→Daisy 的时间戳怎么办?如果是这样,那么它就不是一个有效的圆圈。
在查询的第二阶段,我们通过以下方式发现并验证循环路径。对于前向搜索,继续沿着在反向方向先前遍历过的路径前进,而且时间上是向前的。在我们的例子中,如果max_transactions = 2,因此第一阶段已经走到了 Ben→Cor,那么第二阶段可以继续到 Cor→Daisy,但只有因为我们在第一阶段已经遍历了 Daisy→Cor,并且时间戳继续增加:
Fwd_set = SELECT tgt
FROM Fwd_set:src - (User_Transfer_Transaction>:e) - Transaction:tgt
// tgt must have been touched in the reverse search above
WHERE tgt.@min_rev_dist < GSQL_INT_MAX
AND tgt.ts >= @@min_src_send_time
AND src.@min_fwd_dist < GSQL_INT_MAX
AND tgt.@min_fwd_dist == GSQL_INT_MAX
ACCUM tgt.@min_fwd_dist += src.@min_fwd_dist + 1
POST-ACCUM
CASE WHEN
tgt.@min_fwd_dist < GSQL_INT_MAX
AND tgt.@min_rev_dist < GSQL_INT_MAX
AND tgt.@min_fwd_dist + tgt.@min_rev_dist
<= 2 * STEP_HIGH_LIMIT
THEN
tgt.@is_valid = TRUE
END;
Fwd_set = SELECT tgt
FROM Fwd_set:src - (<User_Receive_Transaction:e) - User:tgt
//tgt must have been touched in the reverse search above
WHERE tgt.@min_rev_dist < GSQL_INT_MAX
AND src.@min_fwd_dist < GSQL_INT_MAX
AND tgt.@min_fwd_dist == GSQL_INT_MAX
ACCUM tgt.@min_fwd_dist += src.@min_fwd_dist + 1
POST-ACCUM
CASE WHEN
tgt.@min_fwd_dist < GSQL_INT_MAX
AND tgt.@min_rev_dist < GSQL_INT_MAX
AND tgt.@min_fwd_dist + tgt.@min_rev_dist
<= 2 * STEP_HIGH_LIMIT
THEN
tgt.@is_valid = TRUE
END
HAVING tgt != source_id;
在第 2 阶段后,我们找到了我们的圈子。有第 3 阶段遍历这些圈子,并标记顶点和边缘,以便它们可以被显示出来。图 5-5 和图 5-6 展示了来自圈检测的示例结果,最大圈子大小为四、五和六个交易。随着圈子大小限制的增加,找到的圈子也越多。

图 5-5. 当 source_id = 111 和 max_transactions 分别为 4 和 5 时的圈检测结果(请在 oreil.ly/gpam0505 查看更大的版本)
s
图 5-6. 当 source_id = 111 和 max_transactions = 6 时的圈检测结果(请在 oreil.ly/gpam0506 查看更大的版本)
章节总结
金融欺诈是大多数企业和所有金融机构必须面对的严重和昂贵的问题。我们需要更好和更快的技术来检测和阻止欺诈。我们展示了图数据建模和图查询是检测可能被忽视的可疑活动模式的强大方法。图建模使得能够轻松应对搜索模式的三个关键阶段:描述搜索、执行搜索和检查结果。在本书的后续部分,我们将展示图机器学习如何提供更复杂和准确的欺诈检测。
更具体地说,我们已经讨论了三个查询来检测和打击欺诈和洗钱。第一个查询展示了如何检测资金是否呈循环流动。第二个查询展示了如何通过图形在推荐计划中发现可疑的用户行为。第三个查询展示了两个人群网络之间的资金流动。第四个查询展示了如何找出将资金发送给同一人的人之间的连接。第五个查询检测是否有人使用假账户向自己发送资金。我们讨论的最后一个查询是关于检测向某人进行大量资金转移的情况。
在下一章中,我们将提供一种系统化的方法来分析图形。特别是,我们将深入探讨图形度量和图形算法的丰富世界。
¹ “什么是洗钱?” fatf-gafi,访问于 2023 年 5 月 22 日,https://www.fatf-gafi.org/en/pages/frequently-asked-questions.html#tabs-36503a8663-item-6ff811783c-tab。
第二部分:分析
第六章:分析连接以获得更深入的洞察
在前面的章节中,我们了解到将数据表示为图表使我们能够更深入、更广泛地查看数据,从而能够更准确和更有洞察力地回答问题。我们看了几个用例,以了解如何将数据建模为图表以及如何查询它。现在我们希望更系统地查看图分析。当我们说图分析时,我们指的是什么?我们可以使用哪些具体的技术来进行图分析?
完成本章后,您应能够:
-
定义图分析并描述其与一般数据分析的区别
-
理解图分析的要求和一些关键方法,包括广度优先搜索和并行处理
-
定义几种对分析有用的图算法类别
-
列出每个类别中的几种算法,并举例说明其在现实世界中的用途
理解图分析
让我们从一般定义数据分析开始。数据分析是对数据集进行有用观察并得出结论,以帮助人们理解数据的重要性。分析将数据转化为有用的见解。图分析也是如此,只不过数据的结构影响我们将检查哪些数据以及以何种顺序检查。连接是一种数据形式,连接驱动分析的进程。
图分析的另一个显著特点是它非常适合回答关于连接的问题。在表格数据集中,您可以询问客户 A 和客户 B 之间的最短连接链路是什么,但如果您的数据是图形式式的,您将更好地完成这种分析。我们可以总结我们的思考如下:
图分析是对连接数据进行观察和得出结论的过程。
分析要求
要对数据集作出观察,显然我们必须检查所有数据或相关子集,并且这将涉及某种形式的计算。如果我们的数据集包含某一年的所有销售交易,简单的分析可以是计算每个月的总销售额,然后查看销售趋势是上升、下降还是以更复杂的方式移动。如果数据组织成表格,那么我们可以想象扫描表格,逐行阅读。我们还需要一个地方来保存结果——每月销售额。事实上,当我们阅读每一行并将其销售额添加到一个月总额时,我们可能希望保持一个累计总数。
图分析有类似的要求:阅读所有相关数据,对每个数据点执行计算和决策,保存临时结果,并报告最终结果。图分析与表格分析的主要区别在于,图的连接不仅影响数据项的性质,还影响我们扫描数据的顺序。还有一些方法论或架构的选择可以使计算和内存存储更加高效。
图遍历方法
在图分析中,我们跟随连接从一个数据点到下一个数据点。用图作为行走路径网络的比喻,我们常说我们走或遍历图。起初,看起来你可能想要跟随一系列连接,就像一个人会走的方式。然而,当你看看你要完成的任务时,结果可能更合理的是依次探索当前位置的直接连接,然后再跟随连接的连接。跟随一系列连接称为深度优先搜索(DFS),在移动到下一个层级连接之前查看所有直接连接称为广度优先搜索(BFS)。我们在第二章中简要提到了这些。
以下工作流程解释了 BFS 和 DFS。区别在于处理工作的顺序,反映在Places_to_Explore列表中顶点的顺序:
-
将源顶点放入名为
Places_to_Explore的处理列表中。作为列表,它有一个顺序,从前到后。 -
从
Places_to_Explore列表的前端移除第一个顶点。如果该顶点已标记为Already_Visited,则跳过步骤 3 和 4。 -
执行你希望对每个顶点执行的任何工作,比如检查一个值是否匹配你的搜索查询。现在将顶点标记为
Already_Visited。 -
从当前顶点获取所有连接边的列表。如果是 BFS,则将该列表添加到
Places_to_Explore列表(队列)的末尾。如果是 DFS,则将该列表添加到Places_to_Explore列表(栈)的前面。 -
重复步骤 2 到 4,直到
Places_to_Explore列表为空为止。
使用 BFS 时,我们遵循一个“公平”的系统,即每遇到的新顶点都排到队列的尾部。因此,顶点逐级处理,从源顶点开始的一跳所有顶点,然后是两跳顶点,依此类推。使用 DFS 时,我们遵循一个“贪婪”的系统,处理源的一个子节点,然后将其邻居放在列表的前面而不是后面。¹ 这意味着在第三步中,一个幸运的距离源三跳的顶点将得到关注。在图 6-1 中,我们看到 BFS 和 DFS 的示例。最初,只有顶点 1 有一个数字。其他数字按访问顺序分配。

图 6-1. 广度优先搜索(BFS)与深度优先搜索(DFS)方法概述
当你希望尽可能接近地面真实最佳答案时,BFS 更为优越。最短路径算法使用 BFS。如果期望答案为多跳并且存在满足任务的许多路径,则 DFS 可能更合适。
如果我们打算探索整个图并且只有一个工作者来处理信息,那么 BFS 和 DFS 的效率大致相当。然而,如果有并行处理可用,则几乎每次都是 BFS 胜出。
并行处理
并行处理是能够同时执行两个或更多任务以减少延迟,即从开始到完成的总时间。要从并行处理中受益,整体任务需要能够分解成多个可以独立执行的子任务(“可并行化”),并且必须有多个处理器。此外,还需进行一些管理工作,以了解如何分割任务,然后将分开的结果合并成最终结果。
BFS 和并行处理很好地结合在一起。大多数使用 BFS 的图分析任务可以通过并行处理更有效地执行。想象一下,你想创建一个道路网络的详细地图。你有一队勘测员(多个处理器),他们都从点 A 开始。在每个路口,你分开你的队伍以更快地完成工作(BFS 与并行处理)。在软件中,你比物理世界更有优势。当一个处理器完成当前任务后,它可以跳到数据网络中任何需要进行下一个任务的地方。
聚合
在分析中的一个基本任务是聚合:获取一组值,对它们进行某些操作,并产生一个描述该集合的单一结果。最常见的聚合函数包括计数、求和和平均值。
考虑购买行为分析:在给定客户-购买-产品图中,找出在产品 X 购买后一周内购买的三种最常购买的与产品 X 同一产品家族的产品。以下是我们如何使用图分析解决这个问题:
-
从代表产品 X 的顶点开始。
-
沿着
购买边遍历,找到所有购买了产品 X 的客户。在每条这样的遍历路径上,记录购买日期。 -
从每个
客户那里,扫描他们其他购买边,查找从那次购买日期开始的一周时间窗口内符合条件的每个产品,并将其添加到一个全局数据结构中,允许添加新项目并更新这些项目的计数。 -
对全局计数进行排序,找到三种最受欢迎的后续购买。
让我们分析一下工作流程和所需的内容:
-
从单个顶点开始,我们使用 BFS 进行两次跳数,通过第一次跳数确定的日期范围过滤第二次跳数。BFS 需要记账(前面提到的
Places_to_Explore列表)。在之前的章节中,您已经看到了 GSQL 语言如何内置支持,使用SELECT-FROM-ACCUM语句并将一级遍历的结果保存为该语句的顶点集结果。 -
每个 BFS 的路径都需要临时跟踪自己的时间窗口。数学很简单:在给定时间戳上加七天。GSQL 提供了本地变量,可以执行临时保存数据以供后续分析的任务。
-
主要的聚合工作是收集后续购买,并找出其中最受欢迎的三个。我们需要一个全局的数据结构,每个处理代理都可以访问以添加新项。最简单的数据结构可能是一个列表,可以保存相同项目的重复实例。在查找后续购买完成后,我们需要遍历列表,看每个项目被提及的次数,并对计数进行排序以获取前三名。更复杂的方法是使用一个 映射,它保存数据对:productID:count。这将需要支持两个操作:插入新的 productID 和增加计数。如果我们想使用并行 BFS,我们需要支持并发插入和增加操作。获取最终计数后,我们需要排序以获取前三名。
GSQL 语言提供了对并行聚合的内置支持,使用称为累加器的对象。累加器可以是全局的或每个顶点的。累加器可以保存标量值,如总和或平均值,也可以保存集合,如列表、集合、映射或堆。对于查找最受欢迎的后续购买的示例,一个全局的 MapAccum 将满足大部分工作需求。
使用图算法进行分析
一些分析任务需要编写定制的数据库查询:所提出的问题在数据集和用例中是独特的。在其他情况下,分析问题是相当常见的。拥有一套标准分析工具库,可以根据特定数据集和任务进行调整,这将非常有用。对于图分析,我们有这样一套工具包;它是一个 图算法库 或 图数据科学库,有时也被称为。在本书的早期章节中,我们描述了图算法作为图查询的一种类型,使用了相似度算法,并提到了一些其他算法类型,如最短路径。直到本章,我们才有意保持它们的使用最少。
作为工具的图算法
首先,让我们定义术语算法。算法是一个明确的、逐步的、有限的指令集,用于执行特定的任务。把算法想象成一个精确的配方。当你有一个任务的算法时,你知道这个任务是可以完成的。对于一个称为确定性算法的子类,相同的输入总是产生相同的输出,无论是谁执行算法或何时执行。算法不仅仅是用于分析。例如,有一个算法用于存储您电子阅读器的颜色和字体大小偏好,以及一个伴随算法,每次启动阅读器时应用您的偏好。这些虽然不是真正的分析任务,但它们也是任务。
然而,当我们说图算法时,它不仅仅是指“关于图的算法”。首先,该术语通常意味着通用算法,这些算法设计用于处理整个类别的图,比如任何带有无向边的图,而不仅仅是具有特定模式和语义细节的图,例如银行交易图。其次,术语图算法通常指解决分析任务的解决方案。
通过专注于通用分析任务,图算法成为图分析的优秀工具。多年来,理论家和数据分析师已经确定了一些常见和通用的图分析任务,并开发了执行这些任务的算法。图算法库是一组精心设计的图算法,能够执行各种不同的任务。该库的收藏被制作以跨越广泛的有用功能,因此它是一个工具包。
就像木工或汽车维修等熟练工艺一样,数据分析需要训练和经验才能熟练使用工具。作为图算法用户,你需要学习每种算法可以执行的任务类型,它设计用于什么类型的材料(数据),如何正确使用它,以及何时不使用它。随着你的专业水平提高,你会更好地理解执行类似功能的算法之间的权衡。你还会看到使用算法的创新方法以及如何结合使用多个算法来执行比任何单一算法更复杂和精密的任务。
因为我们谈论的是软件和数据,数据分析师在使用锻造钢工具处理木材和金属材料的工匠之上有一个优势:我们的工具和材料非常易于塑形。作为算法用户,了解算法如何运作虽然有帮助但并非必须。类比而言,你不需要知道如何设计电压表就能测量电池的电压。然而,如果你想修改一个算法以更好地适应你的情况,那么至少在某种程度上了解该算法是必要的。
任何使用图算法的用户都需要注意一点:仅将算法应用于与您所需分析的语义相关的顶点和边。大多数现实世界的图包含多种类型的顶点和边,每种都有其自己的语义角色。例如,我们可能有 书籍 和 读者 的顶点以及 购买,阅读 和 评论 的边。虽然您可以在整个图上运行 PageRank,但结果可能毫无意义,因为将 书籍 和 读者 放在同一尺度上并没有意义。由于它们的通用性质,大多数图算法忽略语义类型。当忽略类型时,您的分析是否有意义是您需要决定的事情。
到目前为止,我们已经涵盖了许多重要的概念,所以让我们总结一下:
-
图分析利用数据连接来深入了解数据。
-
广度优先搜索、并行处理和聚合是有效图分析的关键要素。
-
图算法作为常见图分析任务的工具。
-
通过组合使用它们可以执行更复杂的任务。
-
使用图算法是一种工艺。你对工具和材料了解得越多,你的工艺水平就越高。
表 6-1 展示了本章介绍的数据分析和算法的关键术语。
表 6-1. 图分析和算法术语表
| 术语 | 定义 |
|---|---|
| 数据分析 | 使用统计方法分析数据以获得洞察 |
| 图分析 | 专注于分析图中实体之间关系的数据分析子集 |
| 算法 | 一组明确的、逐步的和有限的指令,用于执行特定任务 |
| 确定性算法 | 一类算法,其相同输入将始终产生相同结果 |
| 图算法 | 一类通用于图类和分析图结构的算法子集 |
| 遍历/穿越图 | 探索图中顶点和边的过程,按照特定顺序从当前位置进行 |
图算法类别
再考虑几个图分析任务的例子:
社区排名
在社交网络中,根据每位成员每周的平均新讨论次数对子社区进行排名。
相似患者档案
给定某患者的特定症状、个人背景和迄今为止的治疗,找到类似的患者,以便比较成功和挫折,从而实现更好的整体护理。
第一个任务假设我们有明确定义的社群。在某些情况下,我们希望社群不是通过标签定义,而是通过实际的社交行为定义。我们有图算法来基于连接和关系行为来找到社群。第二个任务假设我们有一种衡量相似度的方法。还有一类基于图的相似度算法。
本节概述了今天图分析中最常见的图算法和算法类别。我们将看到五类:
-
路径和树
-
中心性
-
社群
-
相似度
-
分类和预测
路径和树算法
最经典的基于图的任务之一是从一个顶点到另一个顶点找到最短路径。知道最短路径不仅有助于找到最佳的传递和通信路线,还有助于查看个人或流程是否紧密相关。这个人或组织是否与关注的方面密切相关?我们还可以使用最短路径分析来检查文档或其他产品的谱系或来源。
这个任务看起来可能很简单,但请考虑这个例子:假设你想联系一个著名但是很私人的人,比如基努·里维斯。这是一件私人事务,所以你只能通过个人联系,中间联系越少越好。你不知道你的熟人中谁可能认识这个人。因此,你询问所有人是否认识里维斯先生。没有人认识他,所以请他们问问他们的熟人?每个联系人都请他们的熟人检查,直到最终有人认识基努·里维斯个人。
这种连接-连接的过程正是我们在无权图中找到最短路径的方式。事实上,这就是广度优先搜索。你可以看到并行处理是合适的。你的所有熟人可以同时工作,检查他们的熟人。
图 6-2 给出了一个例子。在第一轮中,你(顶点 A)检查所有直接连接(B、C 和 D)。它们每个都被标记为 1. 在第二轮中,每个新访问的顶点检查它们的连接。B 有两个连接(A 和 E),但只有 E 是新的。C 有三个连接,但只有 F 是新的。D 没有未被访问过的连接。因此,E 和 F 是我们的新的“前沿”顶点,并被标记为 2. 在第三轮中,G 和 H 是我们的前沿顶点。事实上,H 就是基努·里维斯,所以我们完成了。请注意,存在两条路径:A-B-E-H 和 A-C-F-H。还请注意,虽然我们正在寻找通往基努·里维斯的路径,但我们也找到了通往中间顶点如 E 和 F 的路径。

图 6-2. 无权最短路径
要得到我们的答案需要多少计算工作?这取决于我们的目标离起点有多近。计算机科学家通常考虑最坏情况或平均情况的工作量。由于我们的算法找到中间顶点的路径,如果我们改变任务:从一个起点到每个目标的路径,我们应该能够得到我们的答案。实际上,我们可能需要遍历两次:最初,我们尝试 A→B,然后稍后 B→A 并发现我们已经到达了 A。我们需要标记每个顶点是否已访问,设置一个距离,稍后检查是否已访问。所以我们有一些活动与边的数量 (E) 成比例,一些与顶点的数量 (V) 成比例。因此,总工作量大致为 E + V。在标准符号中,我们说它是 O(E + V),读作“大 O E 加 V”。在一个连通图中,E 至少与 V 一样大,我们关心最大的因素,所以我们可以简化为 O(E)。
如果有些连接比其他连接更好怎么办?例如,从你家到商店可能有三个街区,但有些街区比其他街区长。这是加权图中的 最短路径 问题。当边被赋权值时,我们必须更加小心,因为步数更多的路径可能仍然是成本较低的路径。在 图 6-3 中,我们给边赋权值,并展示了修改后的搜索算法的前两轮,这个算法归功于计算机科学先驱艾兹格·迪克斯特拉。首先,我们初始化每个顶点的最佳路径长度。因为我们还不知道任何路径(除了从 A 到 A),所以每个距离最初都被设为无穷大。然后在第一轮中,我们从 A 遍历到每个邻居顶点。我们用穿过的边的实际长度加上该边从源顶点回到起点的距离来标记它们。例如,要到达 B,总距离是边 A-B 的权重加上从源点 A 返回起点的距离,即 distance(A,A) = 2 + 0 = 2。

图 6-3. 加权最短路径
在接下来的轮次中,我们从前沿顶点遍历到它们 所有 的邻居。这里我们看到与无权路径算法的差异。我们考虑从 D 到 C 的路径,即使 C 之前已经被访问过。我们看到路径 A-D-C 的总长度为 weight(A,D) + distance(D,C) = 2 + 1 = 3。这比路径 A-C = 4 的长度要小。C 将被标记为到目前为止找到的 最短 路径及其长度。在加权图中,即使找到了一条路径,我们可能需要继续搜索,看看是否有更多跳数但总权重更少的路径。因此,在加权图中找到最短路径比在无权图中需要更多的计算工作。
我们将考虑另一个路径任务,即最小生成树(MST)问题。在图论中,树是一组 N 个顶点和恰好 N−1 条连接这些顶点的边。一个有趣的副作用是树中将会有一条确切的路径从每个顶点到其他每个顶点。在加权图中,最小生成树是具有最小总边权重的树。MST 的一个用途是以最低总成本提供连接性,例如铺设最少的道路或配置最少的网络电缆。
解决 MST 问题有几种类似效率的算法。Prim 算法可能是最简单的描述。我们将以图 6-4 为例:
-
制作一个按权重排序的所有边的列表。
-
选择最小权重的边(C-D)。我们选择的每条边都成为我们树的一部分。
-
选择具有一个端点在部分树中的最轻的边:(A-D)。
-
重复步骤 3,直到我们总共有 N−1 条边。

图 6-4. 最小生成树
根据这些规则,下一个选择的边将是 A-B,B-E,E-F 和 F-H。然后我们有一个平局。E-G 和 H-G 的权重都是 3,所以可以使用任何一个来完成我们的树。
中心度算法
在图中哪个顶点位置最中心?这取决于我们如何定义中心度。TigerGraph 图数据科学(GDS)库有超过 10 种不同的中心度算法。
接近中心度根据顶点到图中每个其他顶点的平均距离评分。通常我们倒转这个平均距离,以便在距离更短时得到更高的分数。接近中心度对于希望选择最佳位置的组织至关重要,无论是零售店、政府办公室还是配送中心。如果他们希望最小化顾客或包裹需要行进的平均距离,他们就会使用接近中心度。我们如何测量距离?最短路径算法可以做到这一点。虽然有更有效的方法来测量平均距离,而不是计算每个单独的最短路径,但原则仍然成立:算法可以作为解决更复杂问题的基本组成部分。有针对有向图和加权图的接近中心度变体。
让我们计算图 6-4 中加权图的一些中心度。顶点 E 和 F 看起来可能靠近中心。对于 E,我们需要计算到 A、B、C、D、F、G 和 H 的最短路径距离。通过目测,我们可以看到距离为 4 + 2 + 5 + 6 + 2 + 3 + 4 = 26. 对于 F,我们需要计算到 A、B、C、D、E、G 和 H 的距离,距离分别为 6 + 4 + 3 + 4 + 2 + 5 + 2 = 26,所以它们平局。
谐度中心性是接近中心性的一个小变种。谐度中心性不是平均距离的倒数,而是距离的倒数的平均(或总和)。谐度中心性的一个优点是,它可以通过将它们的距离视为无穷来处理未连接的顶点,其倒数值简单地为零。这带来了选择算法时的一个关键点:您是否需要处理未连接的顶点?
介数中心性提出了一个不同的情境:假设您考虑图中所有顶点之间的最短路径,从每个顶点到其他每个顶点。如果存在多条最短路径(正如我们在图 6-2 中看到的),则考虑所有这些路径。哪个顶点位于最多的路径上?具有高介数的顶点不一定是目的地,但它将获得大量的通过流量。无论您是要找出最佳的加油站位置还是评估哪些网络路由器最为关键,介数都可能是一个关键的度量。再次看到,一个算法(最短路径)是另一个算法(介数)的基础构件。
也许让你惊讶的是,PageRank 可以被归类为中心性算法。PageRank 旨在找出互联网上最重要的网页。更准确地说,PageRank 衡量的是引用权威,即如果更多的页面指向某页面或者这些页面的权威性更高,则该页面的重要性会增加。另一种看待它的方式是随机冲浪者模型。
想象有人在互联网上冲浪。他们从一个随机页面开始。每分钟,冲浪者转到另一个页面。大多数时候,他们会点击页面上的链接转到另一页;每个链接被选择的概率相等。还有一小部分固定的概率不会跟随链接,而是直接转到另一个随机页面。经过很长时间后,随机冲浪者会在特定页面的概率是多少?这个概率就是该页面的 PageRank 分数。由于图的连接模式,被访问频率更高的页面被认为具有更高的中心性。PageRank 的数学魔力在于,排名不受开始随机行走的位置影响。请注意,PageRank 是针对有向图设计的,而我们迄今看到的大多数任务对有向或无向图都是合理的。
社区算法
图的另一个有意义的分析是理解顶点之间的隐式分组,基于它们如何相互连接。高水平的互动意味着高水平的影响力、韧性或信息传递,这对于理解和预测市场细分、欺诈者行为、群体韧性以及思想或生物传播的病毒性传播等方面非常有用。
定义社区的方法有多种,每种方法都对应一个或多个算法。我们可以根据加入社区所需的连接数量将它们排序。在谱的低端,当仅需要一个连接即可被视为社区的一部分时,我们称之为连通分量,或简称组件。在连接性的高端,当每个顶点都与每个其他社区成员直接连接时,这被称为完全子图。完全子图的顶点构成团。在这两个极端之间是 k 核心。k 核心是一个子图,其中每个顶点与 k 个或更多其他成员有直接连接。
注意
连通分量是 k 核心,其中 k = 1. 包含 c 个顶点的团是 k 核心,其中 k = (c –1),这是 k 的最大可能值。
图 6-5 展示了应用于同一图的这三类社区。在左图中,每个顶点都是三个连通分量中的一个成员。在中心图中,我们有两个 k = 2 的 k 核心,但有四个顶点被排除在外。在右图中,我们有两个小团;许多顶点不符合资格。

图 6-5. 通过连接密度分类的社区类型
所有这些定义都可以强制边的方向性;对于连通分量,我们甚至有相应的名称。如果边是无向的(或者我们忽略方向性),那么我们称之为弱连接组件(WCC)。如果边是有向的,并且每个顶点可以通过跟随有向路径到达每个其他顶点,那么它是强连接组件(SCC)。在图 6-6 中,我们为我们的示例图的边添加了方向性,并看到这如何排除一些顶点。

图 6-6. 弱连接组件和强连接组件
上述社区的定义都有严格的定义,但在一些现实应用中,需要更灵活的定义。我们希望社区相对连接良好。为了解决这个问题,网络科学家提出了一种称为模块度的度量方法,它考虑了相对密度,比较社区内的密度与社区间连接的密度。这就像看城市内街道的密度与城市之间道路密度一样。现在想象一下,城市边界未知;你只能看到道路。如果你提出一组城市边界,模块度将评估你在最大化“内部密集;外部不密集”目标方面的表现。模块度是一个评分系统,而不是一个算法。基于模块度的社区算法找到产生最高模块度分数的社区边界。
要测量模块度(Q),我们首先将顶点分成一组社区,使得每个顶点属于一个社区。然后,考虑每条边作为一个案例,我们计算一些总数和平均数:
Q = [落在一个社区内的实际边的分数]
减去[如果边是随机分布的话,期望的边的分数]
“期望”在统计学上有特定含义。如果你抛硬币很多次,你期望头和尾的概率是 50/50。模块度可以通过使用权重而不是简单计数来处理加权边:
Q = [落在一个社区内的边的平均权重]
减去[如果边是随机分布的话,期望的边的权重]
注意,平均数是在总边数上取的。从一个社区到另一个社区的边对分子没有贡献,但对分母有贡献。设计成这样是为了使跨社区的边影响你的平均数并降低模块度得分。
“随机分布”是什么意思呢?每个顶点 v 都有若干条连接到它的边:顶点的 度 定义为 d(v) = v 的边的总数(或总权重)。想象一下,这些 d(v) 条边中的每一条都随机选择一个目标顶点。顶点 v 的度越大,其随机边与特定目标顶点建立连接的可能性越大。顶点 v1 和 v2 之间的期望(即统计平均)连接数可以计算为:
其中 m 是图中总边数。对于数学倾向的人,模块度 Q 的完整公式如下:
其中 wt(i,j) 是顶点 i 和 j 之间边的权重,comm(i) 是顶点 i 的社区 ID, 是克罗内克 δ 函数,当 a 和 b 相等时等于 1,否则等于 0。
有多种算法试图有效地搜索能产生最高模块度的社区分配方案。图 6-7 展示了两种可能的社区分组,但可能的分组数量呈指数增长。因此,算法采取一些捷径和假设,以有效地找到一个非常好的答案,即使不是最佳答案。

图 6-8. 两人共享相同邻居
为了重申社区和相似性之间的区别:在图 6-8 中的所有五个实体都是称为社区的连通分量的一部分。然而,我们不会说这五个实体互相相似。这些关系所暗示的唯一相似性情况是人员 A 与人员 B 相似。
邻域相似性
很少能找到两个确切相同邻居的实体。我们希望有一种方法来衡量和排名邻域的相似度。排名邻域相似度的两种最常见的方法是 Jaccard 相似度和余弦相似度。其他一些方法包括重叠相似度和皮尔逊相似度。
Jaccard 相似度
Jaccard 相似度 用于衡量两个通用集合之间的相对重叠。假设您经营“梦想清单旅行顾问”,您希望根据顾客所访问的目的地来比较他们。Jaccard 相似度是您可以使用的一种好方法;两个集合将是被比较顾客所访问的目的地。为了在一般情况下阐述 Jaccard 相似度,假设这两个集合分别是 N(a),顶点 a 的邻域,和 N(b),顶点 b 的邻域。那么 a 和 b 的 Jaccard 相似度为:
最大可能的分数是 1,即如果 a 和 b 恰好有相同的邻居。最小分数是 0,如果它们没有共同的邻居。
考虑以下例子:三位旅行者 A、B 和 C,他们去过以下在 表 6-2 中显示的地方。
表 6-2. Jaccard 相似度示例的数据集^(a)
| 目的地 | A | B | C |
|---|---|---|---|
| 巴西亚马逊雨林 | ✔ | ✔ | |
| 美国大峡谷 | ✔ | ✔ | ✔ |
| 中国长城 | ✔ | ✔ | |
| 秘鲁马丘比丘 | ✔ | ✔ | |
| 法国巴黎 | ✔ | ✔ | |
| 埃及的金字塔 | ✔ | ||
| 肯尼亚的野生动物园 | ✔ | ||
| 印度泰姬陵 | ✔ | ||
| 澳大利亚乌鲁鲁 | ✔ | ||
| 意大利的威尼斯 | ✔ | ✔ | |
| ^(a) 在我们的小例子中使用表格可能暗示不需要图结构。我们假设您已经决定将数据组织为图。表格是解释 Jaccard 相似度的简单方法。 |
我们可以使用表中的数据来计算每对旅行者的 Jaccard 相似度:
-
A 和 B 有三个共同的目的地(亚马逊、大峡谷和马丘比丘)。他们共同去过九个目的地:jaccard(A, B) = 3/9 = 0.33。
-
B 和 C 只有一个共同的目的地(大峡谷)。他们共同去过十个目的地:jaccard(B, C) = 1/10 = 0.10。
-
A 和 C 有三个共同的目的地(大峡谷、巴黎和威尼斯)。他们共同去过七个目的地:jaccard(A, C) = 3/7 = 0.43。
在这三个人中,A 和 C 是最相似的。作为业主,您可能建议 C 参观 A 曾经去过的一些地方,例如亚马逊和马丘比丘。或者您可以尝试安排一次团体旅行,邀请他们两个去他们都没有去过的地方,比如澳大利亚的乌鲁鲁。
余弦相似度
余弦相似度测量两个数值特征序列的对齐程度。其名称来源于几何解释,其中数值序列是实体在空间中的坐标。图中网格上的数据点(另一种“图”类型)在图 6-9 中说明了这一解释。

图 6-9。数值数据向量的几何解释
点 A 代表一个实体,其特征向量为(2,0)。B 的特征向量为(3,1)。现在我们明白为什么把属性值列表称为“向量”了。A 和 B 的向量有些对齐。它们之间夹角的余弦值就是它们的相似度分数。如果两个向量指向完全相同的方向,它们之间的角度为 0;它们的余弦值为 1。cos(A,C)等于 0,因为 A 和 C 是垂直的;向量(2,0)和(0,2)没有共同之处。cos(A,D)等于–1,因为 A 和 D 指向相反的方向。因此,cos(x,y) = 1 表示两个完全相似的实体,等于 0 表示两个完全不相关的实体,等于–1 表示两个完全反相关的实体。
假设您对一组实体的几个类别或属性有评分。这些评分可以是产品的各个特性、员工、账户等的评级。让我们继续以 Bucket List Travel Advisors 为例。这一次,每位客户对他们喜欢的目的地进行了评分,评分范围是 1 到 10,因此我们有数值,不只是是或否,显示在表 6-3 中。
表 6-3。余弦相似度示例数据集
| 目的地 | A | B | C |
|---|---|---|---|
| 巴西亚马逊雨林 | 8 | ||
| 美国大峡谷 | 10 | 6 | 8 |
| 中国长城 | 5 | 8 | |
| 秘鲁马丘比丘 | 8 | 7 | |
| 法国巴黎 | 9 | 4 | |
| 埃及金字塔 | 7 | ||
| 肯尼亚的野生动物园 | 10 | ||
| 印度泰姬陵 | 10 | ||
| 澳大利亚乌鲁鲁 | 9 | ||
| 意大利威尼斯 | 7 | 10 |
这里是使用此表格计算旅行者对之间余弦相似度的步骤:
-
列出所有可能的邻居并定义列表的标准顺序,以便我们可以形成向量。我们将使用从亚马逊到威尼斯的自上而下顺序在表 6-3 中的顺序。
-
如果每个顶点有 D 个可能的邻居,这给了我们长度为 D 的向量。对于表 6-3,D = 10。向量中的每个元素是边的权重,如果该顶点是邻居,或者是空值分数,如果它不是邻居。
-
确定正确的空值分数是必要的,以确保您的相似性分数意味着您希望它们意味的内容。如果 0 表示某人绝对讨厌一个目的地,那么如果某人没有访问过目的地,赋予 0 是错误的。更好的方法是标准化分数。您可以通过实体(旅行者)、邻居/特征(目的地)或两者同时进行标准化。其思想是用默认分数替换空单元格。您可以将默认值设置为平均目的地,或者您可以将其设置为略低于平均值,因为不访问某个地方相当于对该地方的投票弱。为简单起见,我们不会标准化分数;我们将使用 6 作为默认评分。那么旅行者 A 的向量是Wt(A) = [8, 10, 5, 8, 9, 6, 6, 6, 6, 7]。
-
然后,应用余弦相似度:
Wt(a) 和 Wt(b) 分别是 a 和 b 的邻居连接权重向量。分子逐元素在向量中进行,将来自 a 的权重乘以来自 b 的权重,然后将这些乘积相加。权重越接近,得到的总和就越大。分母是一个缩放因子,向量 Wt(a) 的欧几里得长度乘以向量 Wt(b) 的长度。
让我们再看一个使用案例,评分电影的人,来比较 Jaccard 相似性和余弦相似性的工作方式。在图 6-10,我们有两个人,A 和 B,他们各自评价了三部电影。他们都评价了两部相同的电影,黑豹和冰雪奇缘。

图 6-10. 评分电影的人的相似性
如果我们只关心人们看过哪些电影,而不关心评分,那么 Jaccard 相似性就足够了,并且更容易计算。如果分数不可用或者关系不是数值的话,这也将是您的选择。Jaccard 相似性是(重叠的大小)/(总集大小)= 2 / 4 = 0.5。这看起来像是一个中等分数,但如果可能有数百或数千部可能看过的电影,那么这将是一个非常高的分数。
如果我们想考虑电影评分来看 A 和 B 的品味有多相似,我们应该使用余弦相似度。假设空分数为 0,那么 A 的邻居分数向量为[5, 4, 3, 0],B 的为[4, 0, 5, 5]。对于余弦相似度,分子为[5, 4, 3, 0] ⋅ [4, 0, 5, 5] = (5)(4)+(4)(0)+(3)(5)+(0)(5) = 35。分母 = 。最终结果为 0.60927。这似乎是一个合理不错的分数——并非强相似度,但远比随机匹配好得多。
提示
当感兴趣的特征是是/否或分类变量时,请使用 Jaccard 相似度。
当您有数值变量时,请使用余弦相似度。如果您两种都有,您可以使用余弦相似度,并将您的是/否变量视为值 1/0。
角色相似性
之前我们说过,如果两个顶点具有相似的属性、相似的关系和相似的邻居,那么它们应该是相似的。我们首先研究了邻居包含完全相同成员的情况,但让我们看看更一般的情况,即个体邻居不是相同的,只是相似的。
考虑一个家庭关系图。一个人,乔丹,有两个活着的父母,并且与一个出生在另一个国家的人结婚,他们共同有三个孩子。另一个人,金,也有两个活着的父母,并且与一个出生在另一个国家的人结婚,他们共同有四个孩子。乔丹和金在共同的邻居实体(父母、配偶或子女)方面没有任何共同点。孩子的数量相似但不完全相同。尽管如此,乔丹和金因为有相似的关系而相似。这被称为角色相似性。
此外,如果乔丹的配偶和金的配偶相似,那就更好了。如果乔丹的孩子和金的孩子在某种方式上(年龄、爱好等)相似,那就更好了。你明白了吧。这些可以不是人,而是产品、供应链或电力分布网络中的组件,或者金融交易。
如果两个实体对实体有相似的关系,并且这些实体本身有相似的角色,则它们具有相似的角色。
这是一个递归定义:如果它们的邻居相似,那么 A 和 B 就是相似的。它何时停止?我们可以确定两者相似的基本情况是什么?
SimRank
在他们的 2002 年论文中,Glen Jeh 和 Jennifer Widom 提出了 SimRank²,通过使得从 A 和 B 的等长路径同时到达同一个个体来衡量相似性。例如,如果 Jordan 和 Kim 共享一个祖父母,那就会增加它们的 SimRank 分数。SimRank 的正式定义是:
In(a) 和 In(b) 分别是顶点 a 和 b 的入邻居集合;u 是 In(a) 的成员;v 是 In(b) 的成员;C 是一个介于 0 和 1 之间的常数,用于控制邻居影响随距离从源顶点增加而减少的速率。较小的 C 值意味着更快的减少。SimRank 计算一个 N × N 的相似性分数数组,每对顶点都有一个。这与余弦相似度和 Jaccard 相似度不同,后者根据需要计算单个对的分数。需要计算完整的 SimRank 分数数组,因为 SimRank(a,b)的值取决于它们邻居对的 SimRank 分数(例如 SimRank(u,v)),而后者又取决于它们的邻居,依此类推。为了计算 SimRank,初始化数组使得如果 a = b,则 SimRank(a,b) = 1;否则为 0。然后,通过应用 SimRank 方程计算修订后的分数集,其中右侧的 SimRank 分数来自先前的迭代。请注意,这类似于 PageRank 的计算,不同之处在于我们有 N × N 的分数而不仅仅是 N 个分数。SimRank 有一些弱点。它仅在两个实体最终找到某个顶点相同时才找到相似性。这对某些情况来说太严格了。对于 Jordan 和 Kim 来说,除非我们的图包含它们的共同亲属,否则它将不起作用。
RoleSim
为了解决这些缺点,Ruoming Jin、Victor E. Lee 和 Hui Hong 在 2011 年引入了 RoleSim³。RoleSim 从这样一个(过于)估计开始,即任意两个实体之间的相似性是它们邻居规模的比率。RoleSim(Jordan, Kim)的初始估计将是 5/6。然后 RoleSim 利用它们邻居当前的估计相似性来进行下一轮的改进猜测。RoleSim 定义如下:
参数 类似于 SimRank 的 C。主要区别在于函数 M。M(a,b) 是 a 和 b 的邻域之间的 二分图匹配。这就像试图将乔丹的三个孩子与金的四个孩子配对。M(乔丹, 金) 将包含三对(还有一个孩子单身)。此外,有 24 种可能的匹配方式。为了计算方便(并非实际的社会动态),假设乔丹的最大孩子选择金的一个孩子;有四个选择。接下来,乔丹的下一个孩子从金的剩下三个孩子中选择,而第三个孩子可以从金的剩下两个孩子中选择。这会产生 (4)(3)(2) = 24 种可能性。方程中的最大项意味着我们选择产生三对配对的 RoleSim 分数总和最高的匹配。如果你把 RoleSim 视为兼容性分数,那么我们正在寻找使合作伙伴的总兼容性最高的配对组合。可以看出,这比 SimRank 更需要计算工作,但是得到的分数具有更好的性质:
-
不要求 a 和 b 的邻域必须相遇。
-
如果a和b的邻域“看起来”完全相同,因为它们具有相同的大小,并且每个邻居都可以成对地进行匹配,以便它们的邻域看起来相同,依此类推,则它们的 RoleSim 分数将是完美的 1。从数学上讲,这种相似性水平称为自同构等价。
分类和预测算法
不仅可以通过图算法计算描述性属性如中心性或相似性,它们还可以承担数据科学的预测方面。图分析中最受需求的用途之一是使用顶点分类方法预测特定交易是否存在欺诈行为。我们将通过查看几种与图相关的预测任务算法来结束本章:预测顶点的类别和预测关系的未来成功或当前存在。(参见 4)。我们常常将预测与猜测未来联系起来,但同样重要的应用是尝试预测关于当前世界的事实,承认我们的数据库并不知道所有事情。
人类通过不断进行实体分类来导航世界。每当我们遇到以前没有见过的东西时,我们的大脑会尝试将其归类为我们已知事物类别之一。我们甚至对人进行分类,这会影响我们如何看待他们。在我们的头脑中,我们有成千上万种物品类别,每种类别由一些关键特征定义。然后我们在潜意识中执行鸭子测试:如果它看起来像鸭子,游泳像鸭子,并且嘎嘎叫像鸭子,那么它就是鸭子。如果我们采取更量化的方法,并说我们试图在某些已知类别的属性与这个新项目之间找到最多匹配,我们刚刚描述了 Jaccard 相似性。
在图中执行分类和预测的自然方法是利用基于图的相似性。通过应用诸如余弦或 Jaccard 相似性的公式,计算待定顶点与其他顶点之间的相似性。如果我们先前进行了某种建模,这样我们就为每个类别有了代表性的示例,那么我们只需要将其与这些示例进行比较。训练机器学习模型是获取这些示例的一种方法。如果我们尚未执行此建模,我们仍然可以进行分类。我们只需要查看更多数据,以发现存在哪些类别及其特征。一种流行的方法是k-最近邻,简称 kNN。
这里是 kNN 的基本工作流程:
-
计算查询顶点与已知类别顶点之间的相似分数。如果一个项目的类别已知,我们称其为标记。如果有太多已标记的顶点,我们可以选择它们的随机样本。
-
选择 k 个最相似的顶点,其中 1 < k < N。
-
统计 k 个最近顶点中每个类别的出现次数。选择出现最多的类别。
例如,如果在与 Kim 最相似的 10 人中,有 7 人更喜欢星际迷航而不是星球大战,那么我们预测 Kim 更喜欢星际迷航。
并没有普遍理想的 k 值。太小的值对噪声过于敏感,而太大的值则忽略了接近性的重要性。一个启发式方法是。另一个方法是对一系列值执行预测,然后从众多预测中选择最频繁的预测。
在图 6-11 中,查询顶点位于中心,顶点与中心之间的距离表示该顶点与查询顶点之间的距离(逆相似度)。被阴影覆盖的顶点有已知类别。我们有两类:深色和浅色。如果 k = 6,那么这六个中有两个是深色的,其他四个是浅色的。因此,我们预测查询顶点的类别是浅色的。

图 6-11. 使用 kNN 对顶点进行分类
链接预测还可以利用基于图的相似性。这个任务通常比节点分类更复杂。首先,我们处理的是两个顶点(端点),而不是一个。此外,链接可能是有方向性的。对于一般情况,我们假设,“如果从与 A 相似的顶点到与 B 相似的顶点通常有类型为 L 的边,那么从顶点 A 到顶点 B 可能存在类型为 L 的边”。我们可以对我们的顶点应用余弦或杰卡德相似度,然后计算我们看到的类似顶点对之间边的频率。如果两个端点是相同类型的顶点,则任务更简单:我们只需要研究一种顶点类型。
不同类型的关系可能与不同类型的相似性相关。例如,假设我们试图预测友谊关系。友谊通常以同质性为特征:有很多共同点的人倾向于彼此喜欢。杰卡德相似度可能是适当的。如果同质性是一个重要特征,我们可以跳过寻找现有边例子的步骤,仅基于两个顶点足够相似来进行预测。对于其他类型的关系,例如相互认识或共同做生意,属于同一个社区的两个顶点可能是最终建立直接关系的一个良好预测因素。
除了杰卡德和余弦之外,在链接预测服务中测量顶点相似性的其他方式包括:
共同邻居
计算共同邻居的数量。这个数字高吗?
总邻居
计算共同邻居的数量,对于深度为 D 跳的邻域。这个数字高吗?
同一个社区
两个顶点已经在同一个社区里吗?
使用原始计数可能存在问题。足够多是多少?杰卡德相似度通过将共同邻居的计数除以它们中的不同邻居总数来标准化计数。另一种标准化方法是考虑每个共同邻居拥有多少邻居。假设 A 和 B 都与 C 为朋友。如果 C 只有三个朋友,那么你们两个与 C 的友谊对 C 来说非常重要。如果 C 有一千个朋友,那么他们共同拥有 C 并不那么重要。阿达米克和阿达尔提出了一种相似性指数⁵,它通过邻居的对数倒数来缩放每个共同邻居的贡献。邻居越少,对相似性的贡献越大:
S(A,B) = ∑_(u ∈ N(A) ∩ N(B) ) 1/(log N(u))
所有这些相似性算法都包含在 TigerGraph 的 GDS 库的拓扑相似性类别中。
章节总结
在本章中,我们阐述了图分析的定义,讨论了图分析的计算要求,并深入审视了图算法,这是图分析的工具集。图分析是对连接数据进行观察和得出结论,图分析可以帮助您对数据进行更深入的洞察,这是通过传统表格分析难以实现或不切实际的。
图算法是处理标准分析任务的便利工具。用于分析的图算法的主要类别包括最短路径、中心性、社区、相似性、节点分类和链接预测。与任何技艺一样,将图算法作为工具使用需要一些学习和实践,以知道如何最好地利用它们。
¹ DFS 优先级别类似于遗传称号的长子继承权,即长子有优先继承权。
² Glen Jeh 和 Jennifer Widom,“SimRank: 结构上下文相似性的度量”,KDD ’02: 第八届 ACM SIGKDD 国际知识发现和数据挖掘会议(2002 年 7 月):538–543,https://dl.acm.org/doi/10.1145/775047.775126。
³ Ruoming Jin, Victor E. Lee, 和 Hui Hong,“网络角色相似性的公理化排名”,KDD ’11: 第 17 届 ACM SIGKDD 国际知识发现和数据挖掘会议(2011 年 8 月):922–930,https://doi.org/10.1145/2020408.2020561。
⁴ 在图数据科学的学术文献中,这两个任务通常被称为节点分类和链接预测。
⁵ Lada A. Adamic 和 Eytan Adar,《Web 上的朋友与邻居》,社交网络 25, no. 3 (2003 年 7 月): 211–230,https://doi.org/10.1016/S0378-8733(03)00009-1.
第七章:更好的推荐和推荐
本章将展示如何使用图分析从网络中检索信息,以便更好地进行推荐和推荐,使用两个真实的用例。在第一个用例中,我们将建立一个患者和医疗专家之间的推荐网络。我们将看到如何确定哪些医生是最有影响力的,以及他们之间的相互关系如何形成社区。第二个用例涉及使用基于客户、上下文因素、产品和特征之间连接和亲和力的特征来构建更好的推荐引擎。在本章结束时,您应该能够:
-
了解图连接如何提供上下文
-
应用多种技术分析上下文,以便进行推荐和推荐
-
知道如何建模和分析推荐网络
-
知道如何使用图分析建模和分析推荐引擎
-
解释高 PageRank 分数的含义,使用转诊和权威的概念
案例 1:改善医疗转诊
今天的医疗保健行业已发展到包括许多专业和专家。这在许多领域推进了技术进步,并使患者有可能接受专家护理。当患者的情况超出一般执业医生提供的常规护理时,一般执业医生可能会将患者转诊给专家。可能会有后续的转诊到其他专家。在许多医疗系统中,患者无权在没有转诊的情况下看专家;需要一个医生对另一个医生的正式转诊,以管理医疗成本和效率。
了解推荐行为对医生、他们的患者、医疗服务提供机构和保险公司都很重要。希望扩展业务和客户基础的医疗专家必须建立和维护强大的推荐水平。根据 2020 年的市场研究,每位医生每年因未能提供转诊而流失的收入高达 90 万美元。¹ 在这方面,医疗从业者类似于律师、体育教练、家庭装饰师和许多其他部分依赖于推荐来建立业务的服务提供者。患者可能想知道推荐是否由于护理质量或某些经济因素。保险公司可以研究推荐数据,看看是否存在可疑的模式,这可能构成一种欺诈形式。提供者是否在医疗必要范围外引导转诊?
要回答这些问题,我们需要能够看到全局图景并分析多层次的医生之间进行推荐的情况,这可能是一系列的推荐链,甚至有时是一个循环。该行业讨论推荐网络的结构。这些提供者-患者推荐网络的结构非常适合进行图分析。
解决方案:形成和分析转诊图
转诊网络的目标是通过将患者发送到正确的专科实践中来确保患者的医疗质量,方法是通过透明且对所有利益相关者有效的患者、医生和医疗专家之间的简化沟通来实现。
了解转诊网络的动态对个体参与者以及像健康保险提供者这样希望全面管理网络的组织都是有价值的。对转诊网络的深入分析可以揭示系统中的低效。例如,医生可能会经常将具有特定症状的患者转诊给特定专家,而不知道该专家倾向于再次将这些患者转诊给另一专家。如今的医疗提供者忙于立即处理面前的患者问题,可能无法看到更全面的系统级视角。能够访问转诊网络分析的医生或管理员可以识别这些模式,并根据数据调整其转诊方案。
一般来说,医生将其患者转诊给其他医疗专家有三个原因:第一,寻求专家在诊断或治疗上的建议;第二,将专家添加到为患者提供服务的医疗提供者团队中;第三,当原始医生由于经验差距或其他个人因素不适合时,将患者转移。在转诊网络中,顶点代表医生、患者和医疗专家。医生向医疗专家转诊患者的行为表示为有向边。在适当的时间做出正确的转诊是提供优质和高效医疗的重要组成部分。我们可以分析生成的有向网络以识别重要的医生和专家。
实施医疗专家转诊网络
TigerGraph 提供了一个入门套件,用于建模医疗转诊网络。我们使用这个入门套件来探索和分析医疗专家、患者和医生的转诊网络。你可以按照 第三章 中的步骤进行安装。
医疗转诊网络入门套件
部署一个新的 TigerGraph 云实例,选择“医疗 - 转诊网络、中心和社区检测”作为入门套件。安装成功后,你可以按照 “加载数据和安装入门套件的查询” 小节在 第三章 中列出的步骤加载数据。
图谱模式
医疗转诊网络入门套件包括超过 11K 个顶点和超过 40K 条边。有五种不同的顶点类型和五种有向边类型。² 该入门套件的模式显示在图 7-1 中。

图 7-1. 医疗保健 - 转诊网络、枢纽和社区检测入门套件的图模式(请参阅该图的更大版本:oreil.ly/gpam0701)
我们在表 7-1 中描述了五种顶点类型。**处方者**是一个医生或护士从业者,执行医疗服务,然后提交与**患者**相关联的**索赔**。每个**处方者**都有一个**专科**和**亚专科**。
五种边类型中有四种非常直接;**转诊**边类型值得特别关注。**处方者**可能向另一**处方者**推荐,以便**患者**可以接受额外的护理。然而,如果您查看加载数据页面上的图统计表,您将看到没有转诊边缘!源数据没有指定任何转诊。我们将运行的一个查询将使用图中的数据来推断转诊。链接推断,也称为链接预测,是图分析提供的最有价值的能力之一。
表 7-1. 医疗转诊图模型中的顶点类型
| 顶点类型 | 描述 |
|---|---|
**处方者** |
能够诊断并开药的医疗从业者 |
**索赔** |
由处方者执行的可计费医疗服务的描述,与患者相关联 |
**患者** |
接受医疗服务的人 |
**专科** |
医疗实践的一个分支,专注于一类生物系统、障碍、治疗或患者 |
**亚专科** |
专科的一个子类别 |
查询和分析
医疗转诊网络入门套件包含众多查询。我们将专注于展示图分析技术如何提供医疗保健转诊网络行为洞察的四个查询。以下是这四个查询的简要描述。然后我们将深入探讨每个查询的工作细节。
获取共同的患者
给定两位医生,找出这些医生共同的所有患者。
推断转诊网络
源数据并未明确指定转诊。这项分析通过查找一个**患者**由一个**处方者**提交了一个**索赔**,然后由另一个不同的**处方者**在有限的时间内提交了另一个**索赔**来推断转诊。
查找有影响力的医生
易于看出哪些医生接收了最多的转诊,但哪些医生影响力最大?影响力比仅仅转诊数量更加微妙。有多种方法来定义影响力。本分析使用 PageRank 算法的影响力概念来找出最具影响力的处方医师。
查找转诊社群
将转诊图看作社交网络,我们能看到哪些社群?也就是说,哪些处方医师由于转诊关系而紧密联系?该分析不仅考虑一对一关系的存在,还考虑了如何将提供者组合紧密联系在一起。
获取共同的患者
查询get_common_patients以两个处方医师顶点作为输入参数,并找到每个具有这两位医生**声明**的**患者**。例如,如果我们使用建议的输入,即处方医师道格拉斯·托马斯和海伦·苏,图 7-2 说明了查询的输出。该查询不仅发现他们有五名共同患者,还显示了患者为何看这两位医生。请注意,我们不要求这反映转诊关系。一个人可能因为溃疡和骨折等不相关的原因看这两位医生。尽管如此,这些信息对几个原因都很有帮助。提供者可以比较共同患者的数量与他们的预期。他们还可以查看共同患者群体的整体特征。这些共同患者的人口统计学或健康状况是否有显著或不寻常的地方?是否存在间接转诊,即没有转诊边缘,但如果进行直接转诊,患者可能会得到更好的护理?

图 7-2。道格拉斯·托马斯和海伦·苏医生之间的常见患者(请查看该图更大的版本:oreil.ly/gpam0702)
get_common_patients查询实施了六个步骤。前四步找到共同患者,最后两步收集连接的顶点和边,以便我们可以显示连接性。在我们计算步骤时,参考这个图可能会有所帮助。
第一步是通过遍历submitted_by边类型收集与第一个处方医师相关联的所有声明。为了记住我们已经遍历过的顶点,我们对每个已访问的声明标记@visited为true:
claims1 = SELECT t
FROM Pre1:s -(<submitted_by:e)- Claim:t
ACCUM t.@visited += true;
在我们的例子中,如果道格拉斯·托马斯是处方医师 1,那么claims1将包括顶点 c10005、c0007、c0009、c10011 和 c10013。可能会包括更多。这些其他顶点将在后期阶段被过滤掉。
然后,在下一步中,我们为每个**Claim**找到链接的**Patient**元素。同样,我们使用@visited来标记我们使用的顶点。在这种情况下,这些是**Patient**顶点:
patients1 = SELECT t
FROM claims1:s -(associated>:e)- Patient:t
ACCUM t.@visited += true;
继续我们的例子,对于 Douglas Thomas,这一步会找到**Patient** p1003, p1004, p1005, p1006 和 p1007。可能会找到更多,但稍后会被过滤掉。
在第三步中,我们与第一步相同,但现在收集第二个**Prescriber**的**Claim**元素。这会找到图 7-2 下部分的六个索赔:
claims2 = SELECT t
FROM Pre2:s -(<submitted_by:e)- Claim:t
ACCUM t.@visited += true;
在第四步中,我们与第二步相同,但现在我们从第三步中找到的**Claim**元素开始遍历,并使用WHERE条件仅包括之前已访问过的**Patient**顶点。任何之前已访问过的**Patient**必定是第一个**Prescriber**的患者,因此我们知道这个**Patient**是共同患者。这是我们之前提到的过滤阶段:
common_patient = SELECT t
FROM claims2:s -(associated>:e)- Patient:t
WHERE t.@visited == true;
PRINT common_patients;s
在第五步中,我们从通用的**Patient**元素中选择每个**Claim**,并使用**associated**边类型来收集它们的边。我们将这些边存储在@@edges_to_display中。在最后一步中,我们会收集更多的边:
claims = SELECT t
FROM common_patients:s -(<associated:e)- Claim:t
WHERE t.@visited == true
ACCUM @@edges_to_display += e;
PRINT claims;
最后,我们收集在第五步中找到的**Claim**元素与两个**Prescriber**元素之间的所有边。我们将这些边存储在@@edges_to_display中并打印它们:
claims = SELECT s
FROM claims:s -(submitted_by>:e)- :t
ACCUM @@edges_to_display += e;
PRINT @@edges_to_display;
推断转诊网络
源数据没有明确包含referral边,因此我们创建一个查询来推断何时有转诊,然后将一个referral边插入图中。如果患者在时间点 A 访问了 Prescriber 1,然后在稍后的时间点 B 访问了 Prescriber 2,这可能是由于转诊。查询参数max_days设置了两次医生访问之间被视为转诊的最大天数上限,默认值为 30 天。
有几个原因可能导致这种时间顺序不是由转诊引起:
-
Prescriber 1 和 Prescriber 2 都在治疗患者相同病症的不同方面,但 Prescriber 1 没有建议患者去看 Prescriber 2。
-
对于 Prescriber 2 的访问与对 Prescriber 1 的访问无关。
要区分真实转诊和错误转诊需要比我们数据集中拥有的更多信息。
查询infer_all_referrals仅仅调用infer_referrals,每次对每个**Prescriber**顶点调用一次。infer_referrals才是真正的工作:
SumAccum<INT> @@num_referrals_created;
all_prescribers = SELECT s FROM Prescriber:s
ACCUM
@@num_referrals_created += infer_referrals(s, max_days);
PRINT @@num_referrals_created;
图 7-3 展示了在infer_referrals查询中进行图遍历的示例流程。从input_prescriber D1 到另一个**Prescriber** D2 需要四次跳转。这对应于图 7-3 中描述的四个SELECT语句。

图 7-3. infer_referrals 查询的图遍历示例
infer_referrals 查询接受两个输入参数:input_prescriber,类型为 **Prescriber** 的顶点;以及整数值 max_days。从 input_prescriber 开始,查询通过遍历边类型 submitted_by 选择与输入开药人相关的所有 **Claim** 元素。从那里,它通过选择边类型 **associated** 找到所有属于那个 **Claim** 集合的 **Patient** 顶点。在 Figure 7-3 中,这两个步骤对应于从 Adam(输入开药人)到 **Patient** Cor。
注意,患者 Cor 从同一个开药人(Adam)那里有多个索赔。索赔的日期是决定是否有转诊的关键因素,因此我们需要注意每个单独的日期。以下的 GSQL 代码片段展示了从 input_prescriber 到他们的患者的前两跳,包括收集每个患者索赔日期的 @date_list 累加器:
Start = {input_prescriber};
my_claims = SELECT t FROM Start:s -(<submitted_by:e)- :t
POST-ACCUM t.@visited = true;
my_patients = SELECT t FROM my_claims:s -(associated>:e)- :t
// A Patient may have multiple claims; save each date
ACCUM t.@date_list += s.rx_fill_date;
现在我们想要找出其他开药人为这些患者提出的索赔。以下代码中的 WHERE t.@visited == false 子句确保这些其他索赔与我们之前查看过的不同。然后我们比较此步骤中遇到的索赔日期与患者的 @date_list 中的索赔日期。如果时间差小于 max_days,我们将此新索赔标记为转诊(在查询的前面,我们将 max_days 转换为 max_seconds):
other_claims = SELECT t FROM my_patients:s -(<associated:e)- :t
WHERE t.@visited == false
ACCUM
FOREACH date IN s.@date_list DO
CASE WHEN datetime_diff(date, t.rx_fill_date)
BETWEEN 0 AND max_seconds THEN
t.@is_referred_claim = true
END
END
HAVING t.@is_referred_claim == true;
接下来,我们查找与标记索赔相关的 **Prescriber** 顶点,并使用 INSERT 语句创建边。出于信息目的,我们使用 @@num_referrals_created 累加器计算插入的边数。在 GSQL 中,图更新直到查询结束前才会提交。因此,在此查询中直接计算 **referral** 边的数量是行不通的。我们只能在随后的查询中执行这样的计数:
other_prescribers = SELECT t FROM other_claims:s -(submitted_by>:e)- :t
POST-ACCUM
INSERT INTO referral VALUES(input_prescriber, t, 1),
@@num_referrals_created += 1;
最后,我们使用 RETURN @@num_referrals_created 将数据发送回调用此查询的查询。调用者查询(infer_all_referrals)为每个 **Prescriber** 返回的每个返回值相加,以计算整个图中创建的边的总数。
GSQL 中的子查询
可以通过在头部使用 RETURNS (<data_type>) 并在查询末尾使用 RETURN <value> 来定义查询作为子查询。除了返回值之外,子查询还可以具有图修改的副作用(例如插入边)。子查询中的 PRINT 语句不会打印到控制台;一个替代方法是使用 PRINTLN 或 LOG 将内容写入文件。更多详细信息,请参阅 TigerGraph 的 GSQL 语言参考。
找出有影响力的医生
此查询查找最具影响力的专家。一个具有影响力的专家被视为权威。他们不仅接受了很多转诊,而且处理重要病例。如果他们突然从图中退出,影响将是显著的。相对影响力分析可以帮助医生了解他们的相对重要性,并看看是否可以提高它。医疗管理人员可以全面考虑,看看是否可以减少对个人的过度依赖,并为患者提供更平衡的护理,同时降低成本。流行病学家、制药公司和医疗设备供应商也可能想知道哪些医生最具影响力。
幸运的是,一个著名的图算法通过这种方式衡量影响力,考虑了传入边的数量和相对重要性。由 Google 创始人 Larry Page 和 Sergey Brin 开发的PageRank 算法根据指向它的其他页面数量和这些其他页面的等级来排名网页。我们可以类似地查看转诊网络中医生的影响力。如果医生接收的转诊更多,或者这些转诊者的影响力增加,医生的影响力就会增加。
TigerGraph 的 GDS 库中包含了PageRank算法(tg_pagerank)的实现。这个算法包含在这个起始套件中以方便使用,但将库算法安装到您的数据库实例中是一个简单的过程。与我们看到的其他为特定图架构和特定起始套件编写的 GSQL 算法不同,这个算法是通用的。它有 10 个输入参数(在表 7-2 中显示),但其中 8 个具有默认值。我们只需要设置前两个。在我们的案例中,我们设置v_type = **Prescriber**和e_type = **referral**。
Tip
在 GraphStudio 中,GSQL GDS 库可在新查询窗口中使用。选择一个图后,在左侧边栏中选择“编写查询”,然后点击绿色的⨁按钮将新查询添加到已安装和已保存查询列表中。在新弹出的窗口中点击“从库中选择”。将鼠标悬停在任何查询旁边的问号图标上,可以简要了解其目的。
表 7-2. tg_pagerank库算法的输入参数
| 参数 | 默认值 | 描述 |
|---|---|---|
v_type |
要使用的顶点类型的名称。 | |
e_type |
要使用的边类型的名称。 | |
max_change |
0.001 | 当中间 PageRank 分数稳定时(变化小于 0.001)停止迭代。 |
max_iter |
25 | 如果我们已经计算了这么多次的中间 PageRank 分数,则停止迭代。 |
damping |
0.85 | 相邻节点相对于随机移动(在未连接顶点之间)的相对重要性。对于稳定性来说,需要一些随机移动。 |
top_k |
100 | 在输出中打印的前 K 个分数的数量。 |
print_accum |
TRUE | 以 JSON 格式打印输出。 |
result_attr |
FALSE | 将结果存储为顶点属性。 |
file_path |
空字符串 | 将结果以表格格式写入此文件。 |
display_edges |
FALSE | 在输出中包含感兴趣的边缘。如果在 GraphStudio 上运行该算法,并选择此选项,会导致更好的视觉显示。 |
在e_type之后的三个参数是 PageRank 特有的。最后五个参数是通用的,并且出现在 GDS 库中许多或大多数算法中。它们处理查询输出在控制台打印和/或导出到文件的方式。result_attr是一种将 PageRank 结果存储为图的顶点属性的方式,以便稍后的查询可以在其自己的计算中使用该算法的结果。display_edges参数指定输出是否应包含有助于可视化结果的边。例如,如果get_common_patients查询要添加此参数,它将指定是否应执行步骤 5 和 6。
如果您使用v_type = **Prescriber**,e_type = **referral**和top_k = 5设置运行 PageRank,您应该得到以下输出:
[ {
"@@top_scores_heap": [
{"Vertex_ID": "pre16", "score": 2.72331},
{"Vertex_ID": "pre61", "score": 2.5498},
{"Vertex_ID": "pre58","score": 2.20136},
{"Vertex_ID": "pre52","score": 2.08101},
{"Vertex_ID": "pre69","score": 1.89883}
]
} ]
寻找推荐社区
此查询检测推荐网络中的社区。社区是一组高度相互连接的顶点,与图的其余部分之间的关系稀疏。在推荐网络中,社区通过医生、患者和医疗服务提供者之间的密集连接而产生,当它们相互作用时形成一个组。它们因为彼此之间的许多互动而形成一个群体。检测社区可以帮助医生识别他们在网络中推荐的传播,并为他们的患者做出更好的推荐。医疗管理人员可以调查社区以评估当地的医疗系统。
一种广受欢迎的检测社区的图算法是 Louvain 算法,我们在第六章中提到过。这种算法由洛汉大学的 Vincent Blondel 开发,通过优化社区内边的相对密度(称为模块)与模块外边的边来选择图社区。Louvain 方法的一个关键特点是它没有固定的输入参数来检测社区数量,这使得它在实际应用中具有优势,例如在那些初始时不知道要检测的社区数量的情况下。它从检测小模块开始,然后在能够提高模块性评分时将它们组合成较大的模块。
包含在健康护理-转诊网络,枢纽和社区检测入门套件中的 Louvain 算法(tg_louvain)。它有八个输入参数(在表 7-3 中显示)。顶点类型(Prescriber)和边类型(referral,在正向和反向方向上)已经硬编码到查询中。无需调整任何参数,尽管您可以尝试增加output_level。
表 7-3. tg_louvain库算法的输入参数
| 参数 | 默认值 | 描述 |
|---|---|---|
iter1 |
10 | 移动的最大迭代次数。 |
iter2 |
10 | 合并的最大迭代次数。 |
iter3 |
10 | 优化的最大迭代次数。 |
split |
10 | 数据批次数,以减少内存峰值消耗。split=1 一次处理整个图。split=10 将图分为 10 批次处理。 |
| output_level | 0 | 如果为 0:JSON 输出是有关社区的统计信息。如果为 1:还输出按群集大小索引的社区 ID。
如果为 2:还输出每个社区的成员资格。
print_accum |
TRUE | 以 JSON 格式打印输出。 |
|---|---|---|
result_attr |
FALSE | 将结果存储为顶点属性。 |
file_path |
空字符串 | 将结果以表格格式写入此文件。 |
使用默认设置运行查询。超过 11,000 个顶点,难以可视化结果,因此我们将检查 JSON 输出。JSON 输出的前三个部分如下所示。第一部分告诉我们算法将顶点分组为 17 个社区。第二部分显示最大的社区有 11,055 个成员。第二大的社区只有 10 名成员。第三部分显示id=0 的社区是具有 11,055 名成员的社区,68157440 号社区有 6 名成员,等等。由于这是一种启发式算法,可能会得到略有不同的社区大小和质量。社区 ID 可能会有所不同:
[
{"num_of_clusters": 17},
{"@@largest_clusters": [
{"csize": 11055,"number": 1},
{"csize": 10,"number": 1},
{"csize": 9,"number": 1},
{"csize": 8,"number": 4},
{"csize": 7,"number": 3},
{"csize": 6,"number": 2},
{"csize": 4,"number": 4},
{"csize": 3,"number": 1}
]
},
{
"@@cluster_sizes": {
"0": 11055,
"68157440": 6,
"68157441": 8,
"71303168": 9,
"71303169": 10,
"71303170": 8,
"72351746": 3,
"72351747": 7,
"73400320": 8,
"75497473": 4,
"77594625": 6,
"77594628": 8,
"79691776": 4,
"81788928": 4,
"84934657": 7,
"84934658": 4,
"88080385": 7
}
]
情况 2:个性化推荐
今天的消费者通常有太多选择。他们很难知道有什么可用并做出决定,而供应商也很难确保他们被注意到并满足消费者的需求。推荐引擎越来越重要,用于引导用户穿过这些产品的丛林。推荐引擎旨在防止用户信息过载,并为他们提供更个性化的信息,使解决方案的用户体验更高效。像亚马逊这样的在线零售商可能在同一类别中拥有数十万个单独的产品。在线零售商提供许多产品,因此受益于推荐引擎,因为它有助于购物者更快速、更轻松地找到感兴趣的产品。重复业务也来自于对个性化体验满意的客户,其他零售商没有提供这种体验。
传统的推荐引擎根据用户的历史行为和相似用户的行为向用户提供产品、内容或服务的建议。然而,这种方法存在一些问题。首先,新用户没有历史记录,因此我们无法在开始时向他们提供正确的建议。这被称为“冷启动问题”。其次,当仅查看用户的历史行为时,我们仅限于重复推荐同类型的内容。用户可能会错过供应商提供的其他产品、内容和服务。最后,制定高度个性化的建议不容易扩展,因为随着用户基数和细节级别的增长,要考虑的人数、产品和因素也会相应增长,随着时间的推移需要指数级别的更多比较。
为了解决这些问题,我们需要一个推荐引擎,能够几乎实时地维护和处理最新的信息,且不依赖批处理。另一个要求是速度快且能够扩展到数百万用户和产品。
解决方案:使用图进行基于多关系的推荐
应用程序向用户推荐产品,本质上是发现连接或相似性。购买产品是消费者和产品之间的一种连接,而消费者、产品及其特征之间也存在连接。如果两个人有相似的喜好,那么这种相似性就是一种连接,经常一起购买的产品也分享同样的连接。这些连接是用户与应用程序互动后产生的,推荐是分析这些连接的一种形式。总体而言,这些连接形成了一个图。通过将数据建模为图,我们可以直接在图结构数据上进行查询,而无需在批处理数据上进行大型连接操作。因此,图是表示这些连接的一种自然且灵活的方式。以这种方式组织关系使得添加、修改和删除数据变得简单,并使应用程序具有高可扩展性。
使用图进行推荐引擎的另一个好处是避免用户的冷启动问题。因为图模型是一个单一的互连系统,我们可以使用混合的推荐技术来填充初始用户体验:与新用户的人口统计信息相关联的内容;基于内容的、协同过滤的推荐;以及供应商促销。在图中,这些技术可以通过模式匹配和相似性评分来实现。此外,增加更多数据、建立更多关系和修订推荐方案都变得非常简单。
实施多关系推荐引擎
TigerGraph 提供了一个入门套件,展示了如何利用图分析生成客户产品推荐。可以通过按照第三章中的步骤安装这个入门套件。
推荐引擎 2.0 入门套件
部署一个新的 TigerGraph Cloud 实例,选择“推荐引擎 2.0(超个性化营销)”作为入门套件。按照第三章的“加载数据和安装查询入门套件”中的步骤加载数据。
图模式
图 7-4 展示了这个入门套件的图模式,包含六种顶点类型和七种边类型。

图 7-4. 推荐引擎 2.0 的图模式(查看此图的更大版本,请访问oreil.ly/gpam0704)
表 7-4 描述了顶点类型。**人口统计学**顶点指的是**顾客**的人口统计属性。**顾客**是我们网店中有账户的自然人。此外,每个**顾客**都有购买**产品**的**历史**。**特征**可以是**人口统计学**、**顾客**或**产品**顶点的特征。通过**上下文**,我们为我们的查询添加了一个具有时间约束或天气条件的上下文层。
表 7-4. 推荐引擎 2.0 图模型中的顶点类型
| 顶点类型 | 描述 |
|---|---|
**顾客** |
一个自然人 |
**人口统计学** |
一个**顾客**的人口统计属性 |
**历史** |
一个**顾客**的购买历史 |
**产品** |
一个产品 |
**特征** |
一个**顾客**的特征 |
**上下文** |
一个上下文约束 |
在我们简化的示例中,每个**上下文**顶点都有一个 ID 值,这是描述其特征的单词或短语。在真实世界的示例中,模式可能会将数据分类到不同类型的上下文中,例如位置、日期、天气等,但我们将它们全部 lumped 到一个属性中。
一些边类型具有权重属性。**产品特征**边类型有一个称为weight的属性。高weight意味着该特征是该产品的重要方面。**顾客特征**边类型具有一个称为affinity的属性。高亲和力意味着该特征与顾客的需求之间有很强的关联。我们可以使用这些权重来计算顾客对产品的偏好程度,即基于特征的推荐。一个标准的方法是将**产品特征**的weight乘以**顾客特_feature**的affinity。
这个入门套件中的图表故意设计得非常小,以便于跟踪计算。整个图表在图 7-5 中显示。从上到下,顶点类型分别为**顾客**、**人口统计学**、**特征**、**产品**和**上下文**。

图 7-5. 推荐图(请查看此图的更大版本:oreil.ly/gpam0705)
查询与分析
推荐引擎 2.0 入门套件包括三个查询,展示了如何使用图分析改进推荐引擎。它们允许我们选择考虑 上下文 和 特征 元素的排名前列产品。
按特征和上下文推荐
在考虑 weather 和 time_of_day 的情况下,为 source_customer 返回 top_k 产品。
根据客户和上下文推荐产品
在考虑天气和一天中的时间的情况下,向客户推荐评分最高的产品。
获取顶级人口统计数据
显示与其顾客之间的平均亲和力最高的人口统计数据,并显示与该人口统计数据相关的特征。
按特征和上下文推荐
针对一组上下文条件和一个客户,recommend_by_features_and_context 查询返回满足上下文条件且在其特征与客户偏好特征之间具有最强匹配的产品。此查询接受四个输入参数。第一个参数 source_customer 指定我们想要了解其顶级产品的 客户。然后,使用 weather 和 time_of_day 指定我们选择的 上下文,使用 top_k 设置我们想要返回的顶级产品数量。
查询从将顶点集 start 初始化为 source_customer 开始。接下来,计算用户想要应用的上下文过滤器数量。如果用户不希望按照输入因素(time_of_day 或 weather)进行过滤,则应将该输入参数留空。以下代码计算激活的上下文过滤器数量:
IF time_of_day != "" THEN min_filters = min_filters + 1; END;
IF weather != "" THEN min_filters = min_filters + 1; END;
接下来,我们找出满足我们上下文过滤器的产品。我们首先将 candidate_products 初始化为所有产品,并将 filtered_context 初始化为所有上下文。然后,如果 min_filters 不为零,则表示已设置了 上下文,因此我们执行第一个 SELECT 语句,将 filtered_context 缩小到仅匹配 time_of_day 或 weather 的元素:
IF min_filters != 0 THEN
filtered_context = SELECT c FROM Context:c
WHERE c.id == weather OR c.id == time_of_day; // filter for context
我们使用另一个 SELECT 语句来将 candidate_products 细化到仅链接到所有 filtered_context 的产品:
candidate_products = SELECT p
FROM filtered_context:c -(product_context:pc)- Product:p
ACCUM p.@filters += 1 // count # matching filter properties
HAVING p.@filters >= min_filters; // must match all context filters
ACCUM 子句计算每个产品进行了多少次上下文匹配。HAVING 子句将最终选择过滤为仅包括在所有指定上下文参数上匹配的产品。参考 图 7-5,如果设置参数 weather = "BW" 和 time_of_day = "T2",则 candidate_products 将为 {P004},即唯一连接到两个 filtered_context 顶点的产品。
现在我们可以计算客户和产品之间的总体推荐得分。我们使用两跳路径来查找给定的**Customer**和候选**Product**节点之间的连接,**Feature**作为中介。对于每个连接到感兴趣特征的选择产品,我们将**product_feature**权重乘以**customer_feature**亲和力,并加到累加器@max_score中。
添加所有相关特征的贡献来计算总分数是一个有效的方法,但这并不是我们实际正在做的。@max_score被定义为MaxAccum,因此它保留了给定的最高值。换句话说,我们寻找最重要的单一特征,并仅使用该特征进行评分。⁴ 然后我们使用类似 SQL 的ORDER BY和LIMIT子句与@max_score一起选择具有最高推荐得分的top_k产品:
recomm_products = SELECT p
FROM start:s -(customer_feature:cf)- Feature:f
-(product_feature:pf)- candidate_products:p
ACCUM p.@max_score += pf.weight*cf.affinity // compute score
ORDER BY p.@max_score DESC
LIMIT top_k;
假设我们设置weather = "GW"和time_of_day = "T2"。那么candidate_products将为{P004, P002},如图 7-6 中所示(因为"GW"连接到每个产品,它没有过滤效果)。如果source_customer = C002,则只有一条Customer-Feature-Product路径到candidate_products,权重为 23。

图 7-6. 推荐路径按特征和上下文分析(查看此图的更大版本,请访问oreil.ly/gpam0706)。
要查看分数,请切换到 JSON 输出。现在假设我们设置source_customer = C001,weather = GW,time_of_day = T``2,以及top_k = 2。想想从C001到GW和T``2在图 7-6 中可以看到的路径,从而导致以下推荐得分:
{
"attributes": {
"@max_score": 12,
},
"v_id": "P004",
"v_type": "Product"
},
{
"attributes": {
"@max_score": -3,
},
"v_id": "P002",
"v_type": "Product"
}
推荐产品根据客户和上下文。
针对一组上下文条件和一组客户,此查询返回满足上下文条件且其特征与客户偏好特征最匹配的产品。此查询的基本任务与我们的第一个查询非常相似,即按特征和上下文推荐,但存在一些有趣的差异。第一个差异是此查询接受任意上下文值列表(input_context_set),而不是询问一个天气条件和一个时间条件。第二个差异是它可以处理多个输入客户(input_customer_set),而不是单个客户。这两个变化使得它更加通用。第三个差异是它仅返回一个产品。这是因为此查询显示了导致推荐的**Customer**和**Context**节点的所有连接的视觉表示。如果选择了前 k 个产品,则视觉表示将不那么容易解释。
查询从定义元组类型和多个累加器开始。前四个定义是为了通过它们与客户的平均亲和力来排序产品。HeapAccum将自动排序给定给它的元组。在我们有了HeapAccum之后,我们将需要将其转换为顶点集以进行进一步处理:
TYPEDEF TUPLE<VERTEX<product> v, DOUBLE avg_score> Product_Score_Tuple;
AvgAccum @product_avg_score;
HeapAccum<Product_Score_Tuple>(1, avg_score DESC) @@top_product_heap;
SetAccum<VERTEX<product>> @@top_product_vertex_set;
提示
堆是从非常大的列表中获取前 k 个元素的更可伸缩的方法,因为堆具有固定且通常较小的大小。虽然ORDER BY和LIMIT在语法上更简单,但它们将构建一个临时表来排序所有元素。
最后七个累加器仅用于收集要显示的顶点和边缘。虽然我们选择为每种类型单独使用容器,但您可以将它们合并为一个顶点集和一个边集:
SetAccum<VERTEX<customer>> @@final_customer_vertex_set;
SetAccum<VERTEX<feature>> @@final_feature_vertex_set;
SetAccum<VERTEX<product>> @@final_product_vertex_set;
SetAccum<VERTEX<context>> @@final_context_vertex_set;
SetAccum<EDGE> @@final_context_feature_edge_set;
SetAccum<EDGE> @@final_product_feature_edge_set;
SetAccum<EDGE> @@final_product_context_edge_set;
我们首先选择与customer_vertex_set中的**Customer**顶点共享**Feature**的所有**Product**顶点,并且还链接到context_vertex_set中输入**Context**顶点之一的顶点。在像 GSQL 这样的图查询语言中,您通过搜索构成这些连接的路径来执行此选择。图 7-7 显示了之前在图 7-4 中看到的图模式,突出显示了搜索路径和指定的顶点集。

图 7-7. 选择由某些客户评价的特征的产品并满足某些上下文约束的图路径(在oreil.ly/gpam0707上查看此图的较大版本)
在 GSQL 中,有两种语法选项来描述此路径:作为由逗号分隔的三个一跳路径或作为一个多跳路径。虽然多跳路径通常更优雅,但单独的路径可以在需要性能调优时更好地控制查询的执行方式。请注意多跳FROM子句如何与图中突出显示的路径完全对应:
product_vertex_set = SELECT p
FROM customer_vertex_set:c -(customer_feature:cf)- feature:f
-(product_feature:pf)- product:p
-(product_context:pctx)- context_vertex_set:ctx
每个**Customer-Feature-Product**路径都有一个得分:cf.affinity * pf.weight。我们计算所有路径得分,并在AvgAccum累加器(@product_avg_scor``e)中累加它们,以获得每个产品的平均得分。然后,我们将每个产品及其得分插入HeapAccum(@@order_product_heap),对其进行排序。由于我们将堆的大小设置为 1,因此我们最终得到了得分最高的单个产品:
ACCUM p.@product_avg_score += (cf.affinity * pf.weight)
POST-ACCUM
@@order_product_heap += Order_Product_Tuple(p, p.@product_avg_score);
在一个SELECT语句中,我们执行了我们的推荐分析。
最终的SELECT语句的目的是可视化我们想在图表中显示的元素。我们沿用之前的路径,使用几乎相同的FROM子句,只有一个改变:我们只包括我们的顶级产品,而不是所有产品。我们从顶点类型中的@@final_customer_vertex_set、@@final_feature_vertex_set、@@final_product_vertex_set和@@final_context_vertex_set累加器中添加所有元素,然后打印这些累加器:
product_vertex_set = SELECT p
FROM customer_vertex_set:c -(customer_feature:cf)- Feature:f
-(product_feature:pf)- product_vertex_set:p
-(product_context:pctx)- context_vertex_set:ctx
ACCUM @@final_context_feature_edge_set += cf,
@@final_product_feature_edge_set += pf,
@@final_product_context_edge_set += pctx
POST-ACCUM @@final_customer_vertex_set += c
POST-ACCUM @@final_feature_vertex_set += f
POST-ACCUM @@final_product_vertex_set += p
POST-ACCUM @@final_context_vertex_set += ctx;
图 7-8 展示了输入客户为C002和C003,输入上下文为BW(恶劣天气)和T3(午餐)时的输出结果。决定此选择的权重显示在图中。

图 7-8. recomm_by_customer_and_context的示例查询结果(请参阅此图的更大版本:oreil.ly/gpam0708)
获取顶级人口统计数据
查询display_top_demographic找到具有其成员中最高平均亲和力分数的**Demographic**元素,并显示连接到此**Demographic**的**Customer**和**Feature**元素。这里的直觉是,具有高亲和力的**Demographic**应该有相似的成员,因此您应该能够更好地预测他们的偏好。这个查询的结构与按客户和上下文推荐产品的结构非常相似。我们根据其与连接的**Customer**元素之间基于特征的平均亲和力评分每个**Demographic**元素。这告诉我们,我们是否有一个强烈联系的**Demographic**组,还是一个仅仅松散联系的组。
与之前的查询不同,这个查询没有输入参数,因为它在没有给定**Context**的情况下计算整个数据集的顶级**Demographic**。我们从定义元组类型和累加器开始,以对**Demographic**顶点进行评分和排序:
TYPEDEF TUPLE<VERTEX<demographic> v, DOUBLE score> Top_Demographic_Tuple;
AvgAccum @demographic_avg_score;
HeapAccum<Demographic_Score_Tuple>(1, score DESC) @@top_product_heap;
SetAccum<VERTEX<Demographic>> @@top_demographic_vertex_set;
我们还定义了六个累加器来收集要显示的顶点和边。
我们使用SELECT – FROM来找到连接**Demographic**到**Customer**通过**Feature**的路径。图 7-9 说明了这种选择。

图 7-9. 连接Demographic到Customer通过Feature的图路径。在找到路径之后,我们通过乘以它们的边权重来对它们进行评分。(请参阅此图的更大版本:oreil.ly/gpam0709)
然后,我们使用ACCUM语句来计算每个人口统计元素的平均人口统计分数,方法是将cf.affinity的属性乘以df.affinity,并将该分数添加到@@demographic_avg_score中。我们为每个人口统计创建一个Top_Demographic_Tuple,并将其添加到@@top_product_heap中:
demographic_vertex_set = SELECT d
FROM Demographic:d -(demo_feature:df)- Feature:f
-(customer_feature:cf)- Customer:c
// Score each demographic by its avg affinity to customers
ACCUM d.@demographic_avg_score += (cf.affinity * df.affinity)
// Pick the top scoring demographic
POST-ACCUM @@top_product_heap += Demographic_Score_Tuple(
d, d.@demographic_avg_score);
将堆转换为简单顶点集需要几行代码。
WHILE (@@top_product_heap.size() > 0) DO
@@top_demographic_vertex_set += @@top_product_heap.pop().v;
END;
demographic_vertex_set = { @@top_demographic_vertex_set }; // top product
第二个SELECT语句的目标是显示找到的顶级人口统计及其连接的客户和特征元素。我们还希望直接显示连接客户到人口统计的边,因此这次FROM子句中的路径遍历稍微长一些。在这些遍历过程中,我们使用累加器将所有边存储在@@final_demo_customer_edge_set、@@final_demo_feature_edge_set和@@final_context_feature_edge_set中。通过POST-ACCUM,我们将所有访问过的顶点存储在@@final_demographic_vertex_set、@@final_customer_vertex_set和@@final_feature_vertex_set中。最后,我们使用这些变量来显示图形:
demographic_vertex_set = SELECT d
FROM demographic_vertex_set:d -(demo_customer:dc)- customer:c,
demographic_vertex_set:d -(demo_feature:df)- feature:f,
customer:c -(customer_feature:cf)- feature:f
ACCUM @@final_demo_customer_edge_set += dc,
@@final_demo_feature_edge_set += df,
@@final_context_feature_edge_set += cf
POST-ACCUM @@final_demographic_vertex_set += d
POST-ACCUM @@final_customer_vertex_set += c
POST-ACCUM @@final_feature_vertex_set += f;
图 7-10 展示了运行display_top_demographic的结果。由于与所有连接的客户元素的亲和路径的平均权重,选择了Old_Cruisers人口统计。由于这个样本数据集非常小,所以只有一个连接的客户。

图 7-10 的输出。显示display_top_demographic查询的结果(在oreil.ly/gpam0710查看更大版本)。
章节总结
在本章中,我们研究了使用图技术识别网络中重要顶点的方法。通过一个真实的用例,我们定义了一个医生、患者和专家的推荐网络,展示了分析其结构如何帮助医生更有效地进行适当的推荐。
我们还展示了如何利用上下文信息网络来改进客户推荐。我们提出的解决方案包括一个客户、人口统计、特征和产品网络。我们发现分析这些实体之间的连接使得推荐更加自然和可扩展。这种方法还帮助我们避免了使用传统数据库数据结构时经常遇到的冷启动问题。
在下一章中,我们将展示如何在网络安全领域中使用图分析技术。我们将展示如何检测和缓解针对防火墙和用于 DDoS 攻击的设备的网络攻击。
¹ “医疗中医生转诊网络的重要性”,JournoMed,2020 年 8 月 18 日,https://journomed.com/importance-of-physician-referral-network-in-healthcare。
² 实际上,在这个模式中,每条有向边还有一个对应的反向边,因此共有 10 种边类型。
³ 没有已知的算法能够在不消耗随数据规模呈指数增长的计算资源的情况下找到最优解。Louvain 算法以时间高效的方式有效地找到一个“好”的答案。
⁴ 通过将累加器类型从MaxAccum修改为SumAccum(总亲和力)或AvgAccum(平均亲和力),您可以轻松修改评分方案。
第八章:加强网络安全
在本章中,我们将描述图形分析如何加强网络安全系统。我们将展示图形分析如何识别报警的根本原因,检测防火墙的绕过,发现洪水攻击和足迹扫描等异常行为。我们还将展示图形如何找到与可疑 IP 地址的连接,这些地址可能是攻击的来源。完成本章后,您应该能够:
-
在网络安全领域应用图形概念的理解
-
构建图形查询以追踪微服务
-
构建图形查询以检测统计异常
网络攻击的成本
我们依赖技术,这些技术不断受到网络攻击的挑战,这些攻击旨在损害、破坏或恶意控制我们的 IT 基础设施或敏感数据。根据 2019 年 Ponemon 研究所的一项调查,66%的中小型企业在过去 12 个月内经历过网络攻击。¹ 这些网络攻击已经成为我们社会运作的日常威胁。例如,2016 年美国总统选举前,俄罗斯黑客协调攻击民主党成员,试图操控选举结果。根据美国国家安全局(NSA)的说法,与希拉里·克林顿竞选活动及其他民主党组织有关的 300 多人的电子邮件账户遭到攻击。² 这些攻击导致信息泄露,旨在损害克林顿的竞选活动。更近期的例子是 2022 年 2 月,一组黑客对全球最大的半导体公司之一 NVIDIA 进行了大规模网络攻击。攻击者泄露了公司的凭证和员工信息。肇事者声称他们可以访问超过 1TB 的公司数据,如果 NVIDIA 不满足他们的赎金要求,他们将会公开这些数据。
网络攻击可能会损害政府和商业组织以及个人,导致政治混乱和财务损失。根据 Ponemon 研究所和 IBM 对数据泄露成本的研究,全球单一数据泄露的平均成本为 386 万美元。美国在这一领域排名首位,单一泄露的平均成本为 864 万美元。最大的挑战之一是尽早发现这些损害。研究人员声称,当在不到 200 天内检测到数据泄露时,组织可以节省 112 万美元。然而,他们也指出,对恶意数据泄露进行检测和遏制的平均时间仍然长达 315 天。³ 数据泄露的平均成本预计在 2023 年将达到 500 万美元。⁴
理解攻击者的操作方式对于构建有效的网络安全系统至关重要。《通用攻击模式枚举和分类》(CAPEC)⁵ 项目提供了一套标准的网络攻击模式类别和描述,分析员、开发人员和 IT 架构师可以利用这些来增强防御能力。CAPEC 将这些攻击模式分为九类。例如,“滥用现有功能”是一种攻击模式,对手利用应用程序的功能来达到原本不被预期的恶意输出或耗尽资源以影响功能。攻击模式“收集和分析信息”侧重于获取、收集和窃取信息。该类别中使用的方法包括主动查询和被动观察。2016 年美国总统选举的案例属于这一类别。攻击模式“注入意外项目”包括专注于控制或干扰应用程序行为的攻击模式。这些攻击模式通过利用目标应用程序的输入来安装和执行恶意代码。
问题。
一个关键挑战是监控信息流,以便漏洞可见并快速报告攻击。假设一个微服务触发另一个微服务引发警报,那么系统必须能够支持深度链接分析以追溯根本原因,或者进行模式匹配以检测异常行为。随着存储、处理和修改的数据量不断增长,从中实时高效地管理和提取相关信息变得更加困难。
上述信息显示,网络安全攻击检测需要:
-
必须处理大量数据。
-
尽快识别威胁并触发警报。
-
必须帮助找出原始故障点。
-
是许多企业急需但尚未满足的紧迫且增长迅速的业务需求。
解决方案
网络安全旨在确保信息的保密性、完整性和可用性,并保护网络、设备和数据免受未经授权的访问或犯罪性使用。⁶ 图表是模拟数字系统和检测攻击的自然选择,因为互联网本身及其基础设施和设备构成了一个互连网络。攻击模式可以被分析为一系列事件的链条,或者是图中由个别过程组成的路径。一个过程可以是一个对象或不同对象之间的互动,这取决于我们想要模型化的内容。
通常情况下,大量攻击是由相对较少的肇事者完成的。DDoS 攻击即是如此。在图模型中,这反映为一个中心结构。基于图的网络安全系统可以寻找并分析意外的中心点。
在构建基于图的网络攻击防御系统时,我们必须考虑四个方面。首先,我们在组织内收集的数据本身就是一个网络,但我们必须将其过程、资产和操作建模为统一的实时图。其次,我们必须监控图中的关键操作和易受攻击的位置。因此,我们可以使用已知的攻击模式在我们的图中表达这些,并围绕它们构建我们的防御。第三,当实际攻击发生时,图可以帮助我们。它可以帮助识别攻击发生在图中的位置,并追溯攻击的上游和下游影响。最后,我们从组织中收集历史数据,并将其与来自第三方(如 McAfee 或 Norton 的匿名数据集)的数据合并,并将其馈入机器学习模型以预测未来的攻击。
网络安全系统必须能够集成多个数据源并实时处理它们。例如,我们需要集成服务信息,以了解在我们的操作中调用哪些微服务,或者服务器信息,以了解我们的应用程序部署在哪里,并查看我们的虚拟机的状态。另一个包括的常见数据源是用户信息,涉及权限和授权。图可以将这些多种数据类型集成为单一视图,其中服务、数据库和用户与互联的网络安全解决方案相链接。
实施网络安全图
使用 TigerGraph Cloud 的一个入门套件,我们将展示如何实施一个网络攻击检测系统。
网络安全威胁检测入门套件
使用 TigerGraph Cloud,部署一个新的云实例,并选择“网络安全威胁检测”作为入门套件。安装了这个入门套件后,按照第三章的“加载数据和安装查询入门套件”部分中列出的步骤加载数据。
图模式
网络安全威胁检测入门套件的模式有九种顶点类型,以Event作为中心顶点类型。总共有 1,325 个顶点和 2,692 条边。这个入门套件的模式如图 8-1 所示。

图 8-1. 网络安全威胁检测入门套件的图模式(在oreil.ly/gpam0801查看此图的更大版本)
六种事件类型包括:认证、防火墙、登录、请求、读取和写入。如果事件是由人或网络设备引起的,则可能与 **用户 ID**、**设备** 或 **IP** 地址相关联。**事件** 可能是涉及 **服务**、**服务器** 或 **资源** 的动作。**服务** 或 **服务器** 可能会报告 **警报**,而 **警报** 则具有 **警报类型**。这些顶点类型在 Table 8-1 中有描述。
Table 8-1. Cybersecurity Threat Detection 图模型中的顶点类型
| 顶点类型 | 描述 |
|---|---|
**事件** |
系统中动作触发的事件 |
**IP** |
参与 事件 的 IP 地址 |
**用户 ID** |
与 **事件** 相关的用户 ID |
**服务** |
执行 事件 动作的微服务 |
**警报** |
由 服务 在 服务器 上触发的警报 |
**警报类型** |
警报 的警报类型 |
**服务器** |
发生 **事件** 的服务器 |
**资源** |
在 事件 中使用的资源 |
**设备** |
事件 中使用的设备 |
一个 **事件** 可能与 **设备**、**IP** 或 **用户 ID** 相关联。这些关联由边类型 From_Device、Has_ip 和 user_event 表示。我们使用两种边方向来表示 **事件** 与 **服务** 之间的关系。如果我们想知道哪些 **服务** 属于一个 **事件**,我们使用 To_Service,或者如果我们想知道属于 **服务** 的 **事件**,我们使用 From_Service。类似地,我们可以找到属于 **事件** 的 **服务器** 元素及其相反关系,分别使用边 To_Server 和 From_Server。通过边 Output_To_Resource 和 Read_From_Resource,我们可以找出哪些 **事件** 是由 **资源** 触发的,以及哪些 **资源** 参与了 **事件**。我们使用 Service_Alert 和 Server_Alert 来指示哪些 **服务** 和 **服务器** 与报告的 **警报** 相关联。而要找出每个 **警报** 的 **警报类型**,我们使用边类型 Alert_Has_Type。
查询与分析
此起始套件中包含的查询展示了使用基于图的查询和分析可以检测到的几种不同的网络攻击模式示例。其中三种对应于 CAPEC 描述的攻击模式类别:功能绕过、足迹探测和指纹识别。
检测防火墙绕过
检测通过防火墙保护的资源但某种方式逃避防火墙的用户。
可疑 IP 检测
给定一个 IP 地址,查找与一定跳数内所有连接到被禁止 IP 地址的连接。
洪水检测
检测基于对服务的异常高请求数的异常情况。返回负责洪水事件的用户。
足迹检测
检测基于短时间内对服务端点的异常高调用次数的异常情况。返回负责足迹事件的用户。
追溯警报源头
追踪导致损坏文件警报的用户元素和 IP 地址。
一些查询会显示从感兴趣的事件或 IP 返回到相关实体的路径。其他查询通过查看表格结果最容易理解。
检测绕过防火墙的行为
功能绕过攻击是访问服务的一种方式,同时绕过意图提供系统保护的功能。[⁷]如果某一资源的访问受到防火墙的保护,那么每次读取访问都应该先成功通过防火墙事件,如图 8-2 中所示(从右向左读取)。firewall_bypass_detection 查询用于检测某些用户或 IP 地址如何逃避防火墙保护并读取受保护资源。它使用图遍历和集合代数将用户分成四个不同的类别。我们首先选择所有 **Resource** 元素,其中 Firewall_required == TRUE。从这些资源中,我们首先遍历到所有读取 **Event** 顶点。这对应于 8-2 图中显示的第一跳。

图 8-2. firewall_bypass_detection 查询的遍历路径和前两个用户集
然后我们识别四组用户。在这种情况下,我们将考虑 **UserID** 和 **IP** 地址都作为“用户”。第一组用户是所有与读取防火墙保护资源相关联的用户:
ip_userid = SELECT t // set of all users accessing the events
FROM read_events:s -((Has_IP|User_Event):e)- :t
ACCUM t.@read_time += s.Start_Date;
第二组是那些同时也与防火墙事件相关联的用户的子集:
ip_userid_firewall = SELECT s // set of all users accessing with firewall
FROM ip_userid:s -((Has_IP|User_Event):e)- :event
WHERE event.Event_Type == "firewall"
ACCUM s.@firewall_time += event.Start_Date;
第三组是那些仅进行读取而没有参与防火墙事件的用户。我们可以通过从第一组中减去第二组来获得此集合:
ip_userid_no_firewall = ip_userid MINUS ip_userid_firewall;
我们的第四组也是最后一组用户是那些既有读取事件又有防火墙事件(第二组),但读取事件发生在防火墙事件之前,这意味着防火墙被绕过:
ip_userid_bypass_firewall = SELECT s
FROM ip_userid_firewall:s
WHERE s.@read_time.size() > s.@firewall_time.size();
表视图是查看这些结果的最佳选项。您将在 IP_userID_no_firewall 类别中找到九个 ID,以及在 IP_userID_bypass_firewall 组中找到一个 IP。在实际用例中,用户可能会随着时间的推移拥有许多读取和防火墙事件,因此应使用时间戳、会话 ID 或其他机制来确定哪些事件属于同一会话。
可疑 IP 检测
虽然与不受欢迎的实体紧密连接并不意味着有错,但这是更深入调查的正当理由。suspicious_ip_detection查询检测与给定输入 IP 地址在一定跳数内连接的禁止 IP 地址。该查询使用WHILE循环和 GSQL 的ACCUM子句进行广度优先搜索,以有效地发现最短路径。它返回给定跳数内的禁止 IP 地址数量以及到这些禁止 IP 地址的最短路径数量。
查询有三个参数:input_ip是要调查的 IP 地址,depth是我们希望从input_ip出发前进的跳数(默认为 3),display_paths是一个布尔指示器,指定是否可视化到禁止 IP 地址的路径:
CREATE QUERY suspicious_ip_detection(VERTEX<IP> input_ip, INT depth=3,
BOOL diplay_paths=FALSE) {
实现遵循经典的广度优先非加权最短路径搜索方法。每个顶点都有两个累加器来跟踪从input_ip到自身的最短路径信息。我们初始化一个顶点集(称为start),其初始值为input_ip,并将其累加器@num_shortest_paths初始化为 1(因为从input_ip到自身有一条路径)。然后,使用一个WHILE循环,重复以下步骤。
首先,我们从start到所有未访问的邻居顶点。我们知道顶点t之前未被访问过,如果t.@num_shortest_paths == 0。
start = SELECT t // (1) Step to unvisited neighbors
FROM start:s -(:e)- :t
WHERE t.@num_shortest_paths == 0
其次,我们考虑最短路径。当我们到达以前未访问的邻居顶点t时,这必然是沿着最短路径进行的。因此,我们更新t到自身的最短路径计数(t.@num_shortest_paths)以及它的边集合以显示这些路径(t.@edge_list):
ACCUM // (2) record # shortest paths
t.@num_shortest_paths += s.@num_shortest_paths,
t.@edge_list += e,
t.@edge_list += s.@edge_list
第三,我们检查邻居是否被标记为禁止 IP。如果是,我们更新三个全局累加器。首先,我们将该邻居添加到@@nearby_banned_IPs中。其次,我们将到t的最短路径数(t.@num_shortest_paths)添加到@@num_paths_to_banned_IPs中。第三,我们将路径本身(t.@edge_list)附加到@@paths_to_banned_IPs中:
POST-ACCUM CASE WHEN t.banned == TRUE THEN
// (3) Track the banned IPs. Tally the paths to banned IPs
@@nearby_banned_IPs += t,
@@num_paths_to_banned_IPs += t.@num_shortest_paths,
@@paths_to_banned_IPs += t.@edge_list
然后我们更新顶点集start为目标集t。我们重复以上三个步骤,这次从上一轮新访问的顶点开始遍历。我们进行深度轮次以确保我们离输入 IP 地址移动那么多步,或者直到我们用完未访问的顶点(WHILE start.size() > 0 LIMIT depth DO)。
使用建议的输入 input_ip = 188.117.3.237 和 depth = 4,我们发现了 18 个可疑 IP 地址,如图 8-3 所示。增加深度将找到更多,但是距离增加可能会降低违规行为的可能性。

图 8-3. 与给定禁止 IP 地址相连接的 IP 地址,在深度为 4 内(请参见此图的更大版本oreil.ly/gpam0803)
洪水检测
接下来的两个查询专注于检测可能的网络攻击异常。Flooding攻击会向目标发送大量服务请求,试图使其超负荷。⁸ flooding_detection查询检测一个服务是否接收到比通常多得多的请求。这个查询是 GSQL 支持算法编程和累加器的良好示例,使得可以轻松计算统计数据,如平均值和标准偏差,并根据这些值执行过滤。
查询包括一个参数n_sigma,其中sigma表示标准偏差。默认值为 3.0,这意味着如果 IP 的登录事件数量超过平均值三个标准偏差,那么该 IP 被视为异常值。此参数使用户能够轻松调整警报阈值。
此查询的整体流程包括四个单一的跳跃,最终我们将计算每个 IP 的服务请求次数,并与所有其他 IP 的平均值进行比较,以确定它是否是异常值。图 8-4 展示了一个样本图表,以及该查询如何聚合事件。

图 8-4. 在flooding_detection查询中的累积
在第一个跳跃中,我们通过IP到Event的Has_IP边遍历选择每个 IP 地址的所有登录事件。子句WHERE event.Event_Type == "login"将选择过滤为仅包括登录事件。然后我们计算每个 IP 地址的登录次数:@count_map += (i->1)。该语句中的i->1表示我们针对每个 IP 地址i添加 1。在接下来的几个跳跃中,我们将从这一步开始传输或重新分组计数,以计算按用户、请求事件和服务计算的小计:
login_events = SELECT event
FROM IPs:i -(Has_IP)- :event
WHERE event.Event_Type == "login"
ACCUM event.@count_map += (i->1);
在第二个跳跃中,我们将登录事件数量与用户关联起来。这是通过在User_Events边上从login_events到User的遍历完成的。我们使用user.@count_map += le.@count_map来按用户对先前计算的事件进行分组。在图 8-4 中,我们看到 User1 从 IP1 登录了两次:
users = SELECT user
FROM login_events:le -(User_Event)- :user
ACCUM user.@count_map += le.@count_map;
在第三个跳跃中,我们链接到我们在第二个跳跃中找到的用户的请求事件。子句WHERE event.Event_Type == "request"检查我们是否仅包括请求事件。然后我们将我们之前的计数复制到请求事件中:
events = SELECT event
FROM users:u -(User_Event:e)- :event
WHERE event.Event_Type == "request"
ACCUM event.@count_map += u.@count_map;
在第四和最后一跳中,我们将上一跳的请求事件链接到Service元素。我们还累积我们正在进行的计数,并按Service进行分组。在图 8-4 中,我们看到 Service1 共有四个来自 IP1 的登录事件,两个通过 User1 和两个通过 User3。现在我们可以计算最终的统计数据,以确定 IP 是否触发了异常数量的请求。我们通过三个步骤来完成此操作。在第一步中,我们使用AvgAccum累加器轻松计算所有 IP 之间的平均计数:
FOREACH (ip,cnt) in s.@count_map DO
s.@mean += cnt // @mean is an AvgAccum
END,
在第二步中,我们计算标准差(使用第一次传递中的@mean):
FOREACH (ip,cnt) in s.@count_map DO
s.@stdev += pow(cnt - s.@mean, 2)
END,
s.@stdev = sqrt(s.@stdev/(s.@count_map.size()-1)),
最后,我们通过将每个登录次数与平均值和标准差进行比较来检查异常值。如果计数大于平均值加上n_sigma(我们在参数中设置的阈值)乘以标准差,那么该 IP 的登录行为就是异常的:
CASE WHEN s.@stdev != 0 THEN
// calculate the outlier
FOREACH (ip,cnt) in s.@count_map DO
CASE WHEN cnt-s.@mean > n_sigma*s.@stdev THEN
@@outlier_list += Result_Tuple(
ip,s,cnt,s.@mean,s.@stdev)
END
END
END
为了简单起见,我们没有包含每次登录和请求事件的时间方面。在实际情况中,登录事件必须先于其关联的请求事件发生,并且请求事件必须在足够短的时间窗口内发生,以被视为攻击。在这个例子中,我们保持简单,仅演示了如何通过整个网络获取事件触发器来计算各个统计数据。
运行带有n_sigma = 3 的查询,在表视图中两次列出 IP 216.61.220.149,一次用于服务 11 的高使用率,一次用于服务 12。
足迹检测
另一种类型的网络攻击是足迹侦测。Footprinting会在短时间内调用服务的许多端点,试图了解其配置、行为和漏洞。⁹ footprinting_detection查询展示了我们如何检测执行这些操作的用户。其实现与洪泛检测查询非常相似,因为它计算平均值、标准差和异常值来检测**Users**的异常行为。
此查询有三个参数:n_sigma设置确定异常值的阈值,而start_date和end_date确定我们希望检测足迹的时间窗口。
首先,我们选择所有类型为Events的“请求”,并且发生在start_date和end_date之间:
events = SELECT s
FROM events:s
WHERE s.Start_Date > start_date AND s.Start_Date < end_date
AND s.Event_Type == "request";
然后,我们记录每个用户的所有端点请求。我们通过从Events到User使用User_Event边类型进行这样做。我们将用户调用的每个端点都添加到event.@api_map中。因为这些映射附加到每个事件,每个事件将具有单个映射条目:
events = SELECT event
FROM events:event -(User_Event)- :user
ACCUM event.@api_map += (user -> event.Endpoint); // make map
然后,我们从Events到Service进行遍历,将端点请求按服务分组:
services = SELECT s
FROM events:ev -(To_Service)- :s
ACCUM s.@api_map += ev.@api_map
然后,我们计算异常值统计,与洪水检测查询中的操作类似。在洪水检测中,MapAccum的值类型为SumAccum<INT>,它已经是一个总和。在足迹检测中,MapAccum的值类型为SetAccum<STRING>:一个端点名称的集合。要计算平均值,我们需要知道每个集合中有多少个端点,因此使用cnt.size():
FOREACH (user,cnt) IN s.@api_map DO
s.@mean += cnt.size()
END,
标准差和异常值计算与洪水检测查询中的计算完全类似。使用默认输入值运行查询显示,用户 1 对服务 13 和 14 的使用异常高。
追踪警报来源
在我们的最后一个示例中,已经触发了一个警报,我们沿着图中的路径跟踪用户及其可能导致警报的 IP 地址。我们将要跟踪的路径在图 8-5 中有所说明。我们从最近的特定**Alert_Type**的**Alert**开始,然后向后追溯到其原因。从**Alert**开始,我们跟随到引发**Alert**的**Service**。如果**Alert_Type**是文件数据损坏,那么在文件读取**Event**期间会注意到**Alert**,因此我们追溯到那时。写入**Event**会导致损坏,因此我们追溯到那时。最后,我们追溯到执行该写入的**UserID**和**IP**。这个查询的整体流程是直线上的八次跳跃。在传统关系数据库中,这将是八次连接,速度非常慢。相比之下,通过直接遍历图模型,我们可以实时获取这些洞察。

图 8-5. 追踪警报来源的路径遍历
alert_source_tracing查询有三个输入参数:alert_type是要追踪的警报类型;num_days是从警报被触发时开始往回查看的天数;top_ip是查询将返回的 IP 地址的数量。
CREATE QUERY alert_source_tracking(STRING alert_type="Data Corrupted",
INT num_days=8, INT top_ip=20) {
两个MaxAccum累加器@latest_alert_time和@latest_read_time分别计算并记录了最近一次警报和导致警报的最近读取事件的时间。SumAccum<INT> @count统计每个用户或 IP 的写入事件数量,以便了解哪些 IP 地址最为频繁。ListAccum<EDGE> @path_list收集了从输入的alert_type到触发这些警报的IP顶点的路径所需的所有边。
我们将八次跳跃分组为四个两跳阶段。在第一阶段,我们获取给定alert_type的**Alerts**,然后追溯到受影响的**Service**。每个Alert的日期都添加到其**Service**的@latest_alert_time累加器中。因为它是MaxAccum,它自动保留最近的警报日期。到目前为止,我们遍历的两个跳的边缘被添加到我们遍历路径的端点的@path_list累加器中:
service = SELECT serv
FROM alert_types:s -(Alert_Has_Type:e1)- :alert
-(Service_Alert:e2)- :serv
ACCUM
serv.@latest_alert_time += alert.Alert_Date,
serv.@path_list += e1, serv.@path_list += e2;
在第二阶段,我们从这些服务回溯到被读取并触发警报的文件。我们再进行两次跳跃,从这些**Services**通过读取**Event**回到文件**Resource**。我们只考虑在警报前一天内合理触发警报的事件:
resource = SELECT res
FROM service:s -(From_Service:e1)- :event
-(Read_From_Resource:e2)- :res
WHERE datetime_diff(s.@latest_alert_time,event.Start_Date)
BETWEEN 0 AND 3600*24
AND event.Event_Type == "read"
在选择适当的文件读取事件之后,我们执行两项任务。首先,记录每个文件最近读取的时间:
res.@latest_read_time += event.Start_Date,
其次,我们将第一跳的部分路径传输到**Resource**顶点,并使用连接Service到Resource的边扩展路径:
res.@path_list += s.@path_list,
res.@path_list += e1, res.@path_list += e2;
在第三阶段,我们从这些文件追溯到写入这些文件的用户。这种双跳遍历在结构上与第二阶段的遍历类似。该阶段从**Resource**顶点到**User**顶点开始,使用Output_To_Resource和User_Event边。它接受那些在最新读取前num_days天内发生的写入事件。此外,我们增加user.@count来存储用户写入文件的次数,然后再次传输和扩展路径:
users = SELECT user
FROM resource:s -(Output_To_Resource:e1)- :event
-(User_Event:e2)- :user
WHERE datetime_diff(s.@latest_read_time, event.Start_Date)
BETWEEN 0 AND 3600*24*num_days
AND event.Event_Type == "write"
ACCUM user.@count += 1, // Tally writes per user
user.@path_list += s.@path_list,
user.@path_list += e1, user.@path_list += e2;
在查询的最后阶段,我们从用户顶点追溯回那些引发警报的 IP 地址。我们从**User**顶点开始遍历,并使用User_Event和Has_IP边进行双跳。我们使用累加器传输和扩展路径一次。路径现在的长度为 8,从**Alert_Type**到**IP**。我们还计算用户每个 IP 地址写入文件的次数。最后,我们按写入计数对 IP 地址列表进行排序,并仅获取top_ip地址返回:
login_IP = SELECT ip
FROM users:s -(User_Event:e1)- :event
-(Has_IP:e2)- :ip
WHERE event.Event_Type == "login"
ACCUM ip.@count += s.@count, // Tally user-writes per IP
ip.@path_list += s.@path_list,
ip.@path_list += e1, ip.@path_list += e2
ORDER BY ip.@count DESC
LIMIT top_ip;
使用默认输入值(alert_type = "Data Corrupted",num_days = 7,top_ip = 3),我们发现文件损坏的前三个 IP 来源分别为 31、18 和 11 个事件,表示用户在时间窗口内写入最终损坏的文件。路径的可视化显示解释了我们如何得出这些总数。
章节总结
在本章中,我们展示了图如何加强网络安全。网络攻击遵循模式,这些模式可以表示为图查询。我们展示了针对三种攻击类型的检测查询,一种风险评估查询,该查询测量与禁止 IP 的接近程度,以及源追踪查询,查看是谁以及什么导致了警报。
¹ Ponemon Institute,《2019 年中小型企业全球网络安全状况报告》,2019 年,https://www.cisco.com/c/dam/en/us/products/collateral/security/ponemon-report-smb.pdf。
² Mark Mazzetti 和 Katie Benner,《穆勒调查中起诉的 12 名俄罗斯特工》,纽约时报,2018 年 7 月 13 日,https://www.nytimes.com/2018/07/13/us/politics/mueller-indictment-russian-intelligence-hacking.html。
³ IBM Security,《2020 年数据泄露成本报告》,2020 年,https://www.ibm.com/downloads/cas/QMXVZX6R。
⁴ Acronis,《Acronis 网络保护运营中心报告:2022 年下半年的网络威胁——数据遭受攻击》,2022 年,https://dl.acronis.com/u/rc/White-Paper-Acronis-Cyber-Protect-Cloud-Cyberthreats-Report-Year-End-2022-EN-US-221212.pdf。
⁵ “CAPEC 列表版本 3.9”,CAPEC,最后更新于 2021 年 10 月 21 日,https://capec.mitre.org/data/index.html。
⁶ “什么是网络安全?”网络安全与基础设施安全局,2021 年 2 月 1 日,https://www.cisa.gov/news-events/news/what-cybersecurity。
⁷ “CAPEC-554:功能绕过”,CAPEC,最后更新于 2021 年 10 月 21 日,https://capec.mitre.org/data/definitions/554.html。
⁸ “CAPEC-125:洪水攻击”,CAPEC,最后更新于 2021 年 10 月 21 日,https://capec.mitre.org/data/definitions/125.html。
⁹ “CAPEC-169:足迹”,CAPEC,最后更新于 2021 年 10 月 21 日,https://capec.mitre.org/data/definitions/169.html。
第九章:分析航空公司航线
图算法是进行图分析的必要工具。虽然人们可以从教科书中学习算法本身,但从业者需要获得使用图算法库并将算法应用于真实用例的实践经验。本章将使用图算法来分析全球航线网络。我们将应用三类算法:最短路径、中心性和社区检测。
完成本章后,您应能够:
-
安装并运行 TigerGraph GDS 算法
-
设置算法的必需和可选参数
-
修改 GSQL 算法或其他查询,以制作定制版本
-
使用“探索图”功能显示选定的顶点和边,包括创建属性过滤器
-
理解最短路径、中心性和社区算法在路由网络中的应用
目标:分析航空公司航线
阿姆斯特丹的史基浦机场位于相对较小的荷兰国家。尽管荷兰只有 1700 万居民,但它最大的机场是 2021 年转运了超过 2500 万乘客和 160 万吨货物的顶级枢纽。(1)要达到这样的壮举,像史基浦这样的机场面临着为数千次航班安排上百架飞机的挑战。史基浦机场在 2019 年新冠疫情爆发前几乎有 5 亿次航空交通运输。机场是一个时间敏感的业务,它在复杂的后勤约束条件下运作,比如为每条航线提供飞行连通性。机场的目标是通过以最节约成本的方式安排这些航线,最大化总利润。
一旦航空公司建立了他们的航班时间表,乘客就需要选择对他们最有意义的航线。对于一些航线,乘客可以选择多种选项。乘客可能希望选择最少连接的航线,或者他们可能希望选择最短的航线。今天,乘客可以使用在线搜索工具,这些工具可以整合每次航班的利弊,比如最短航线。就像 PageRank 算法是 Google 网络搜索实用程序的起点一样,最短路径算法是航空公司航班搜索工具的核心。一些乘客寻求对航班网络的进一步分析。那些经常飞往各种目的地的人,比如销售人员和顾问,可能想知道哪些机场是最佳枢纽。
一些行业可能会从寻找连接的机场社区中受益,尤其是那些与整体机场网络连接较少的地方。例如,野生动物摄影旅行社可能有兴趣销售偏远地区的套餐旅游。如果该地区与大型枢纽的连接较少,那么它可能更为偏远,因此更受摄影师欢迎,他们希望走"少有人走的路"。理想情况下,该偏远地区将拥有自己的本地航线社区,以便于前往不同的目的地。另一个例子可能是一家航空公司顾问寻找未开发的航线。图表明相对于世界其他地方而言,哪些社区相对孤立,可能是提出开通新航线建议的起点。
解决方案:航班路由网络上的图算法
航班流量形成了一个由航班连接的机场网络。因此,图分析是可视化和分析路线及其对机场业务影响的一种自然方式。我们可以使用有向图来结合每个航班的出发地和目的地,并使用边属性来包括距离、时间或碳排放等成本。仅通过构建图形,我们可以轻松地进行基本观察,如一个机场的进出航班数量。然而,通过使用图算法,我们可以进行更复杂的分析,如识别最有影响力的机场和最经济的路径。
我们可以使用边属性定义我们用例的最有效路径。例如,如果我们寻找最短的路径,我们可以将航程距离作为属性包含进去。在其他情况下,如果我们寻找最便宜的航班,我们可以包含每个机场之间的航班价格,或者如果我们有兴趣找到最可持续的飞行选项,则可以在每个边属性中包含二氧化碳排放。
实施机场和航班路由分析器
现在我们展示一些这些图算法的实际操作,使用另一个 TigerGraph Cloud 入门套件。
图算法入门套件
使用 TigerGraph Cloud,部署一个新的云数据库实例,并选择“图算法 - 中心性算法”作为用例。一旦安装了这个入门套件,按照第三章中“加载数据和安装查询的入门套件”一节中列出的步骤加载数据。
图模式和数据集
此数据集代表约 2014 年从 OpenFlights.org 获取的实际机场和航班路线。只有一种顶点类型,**Airport**,带有 ID、名称、城市、国家、IATA 代码、³ 纬度和经度等属性。还包括一个称为分数的附加属性,作为存储算法结果的通用占位符。例如,如果我们在图上运行 PageRank,那么会为每个顶点生成一个 PageRank 分数。我们可以将这些值存储在这个属性中。共有 7,935 个机场的数据。拥有单一顶点类型使得这个图适合使用标准化的图算法进行直接分析,其中大多数算法假设图具有单一顶点类型。
两种边类型,**flight_route** 和 **flight_to**,来自同一来源文件 routes.dat,这是一个计划的商业服务航班列表,从一个机场到另一个机场。该表中每条航线表示一个航空公司提供的两个城市之间的直达服务,不考虑频率。**flight_to** 边是有向的。**flight_route** 边是无向的,意味着这些城市之间有直达服务,但忽略方向。共有 19,268 条 **flight_route** 边和 37,606 条 **flight_to** 边,几乎是 **flight_route** 边的两倍,表明直达服务通常是双向的。图 9-1 中的模式的简单性提醒我们模式复杂性(这里是一个顶点类型和两种边类型)与数据复杂性(大约 8,000 个顶点和 57,000 条边)之间的差异是一个好的例子。

图 9-1. 航班路线数据集的模式(请在 oreil.ly/gpam0901 查看此图的较大版本)
安装 GDS 库中的算法
图算法的美妙之处之一是它们具有标准定义,并且可以在满足其条件的任何图上运行。例如,对于非加权边的最短路径算法应该在任何图上工作。截至 2023 年 5 月,TigerGraph GDS(图数据科学)库包含超过 55 种算法,可在 GitHub 上获取。⁴ 为了让用户在便利性和性能之间做选择,它们以两种格式提供:无模式和模板。
无模式算法被编写为开源的 GSQL 查询,其中顶点类型、边类型和相关的顶点或边的属性是运行时参数。它们只需安装一次,然后就可以用于任何图。模板算法以 proto-GSQL 编写。用户不需要执行显式安装操作。模板算法使用 CALL 而不是 INSTALL 和 RUN 运行。如果 CALL 语句指定了之前未使用过的模式详细信息(顶点类型、边类型和属性),则数据库会安装带有这些模式详细信息硬编码的优化版本的模板查询。然后 CALL 运行这个特定于模式的算法。如果 CALL 语句使用了之前已使用过的模式详细信息,则数据库跳过安装并直接运行已安装的算法。表 9-1 对比了这两种类型。
表 9-1. 比较无模式和模板算法
| 无模式算法 | 模板算法 | |
|---|---|---|
| ** | 安装 | 一次 |
| 运行时性能 | 较慢,可能使用更多内存 | 优化:更快速,内存占用更少 |
| 命令 | INSTALL、RUN |
CALL |
| 用户定制 | 简单,就像 GSQL 查询一样 | 间接,由于模板的性质 |
我们将使用无模式算法。按照以下步骤安装我们将用于分析飞行路线网络的无模式算法:
-
在 GraphStudio 的写查询页面上,查找并点击 GSQL 查询窗格底部的添加新查询按钮(一个带有 + 符号的黑色圆圈)。
-
一个面板将弹出。
-
点击从库中选择。
将出现一个包含算法类别列表的新面板。点击 Centrality 右侧的箭头。选择 Betweenness Centrality 和 Closeness Centrality 旁边的框。还选择 Community → Connected Components 和 Path → Shortest Path。然后点击 INSTALL。安装过程需要几分钟。
查询和分析
这个入门套件涵盖了三类图算法,它们都提供关于航线的有用答案和分析。此外,还有一些实用查询,可以帮助准备数据以检查各个记录。
实用查询
计算飞行距离
- 原始数据集没有告诉我们旅行距离。此查询使用纬度和经度计算两个机场之间直接航班的长度。⁵
搜索顶点
- 数据集中并非每个机场都有 IATA 代码。为了识别一个机场,我们可能需要根据城市或机场名称进行搜索。此查询提供了一个通用的顶点搜索功能。
路径算法
许多旅行者希望找到连接最少、行驶距离最短或成本最低的路线。最短路径算法将找到从一个顶点到另一个顶点的路线,其中段数最少。加权最短路径算法处理每个边具有数值权重的数据,该权重可以代表实际因素,如时间、距离或金钱。它找到从一个顶点到另一个顶点的路径,其总权重最小。
中心性算法
该套件使用接近度中心性和介数中心性以两种不同的方式对机场的路由重要性进行排名。
社区检测算法
在路由网络中,有更多的路由选项可供在一个社区内旅行,而在社区之间旅行则较少。我们运行强连通分量算法,以查看它对全球航空路线网络的揭示。
计算路线长度
使用此起始套件的第一步是运行calculate_route_length查询。该查询根据机场的纬度和经度计算每条路线的长度,使用 haversine 公式考虑地球的曲率。每条路线的弧长存储在每个顶点的名为miles的属性中。
运行查询时,您需要指定边类型。请分别使用边类型**flight_to**和**flight_route**运行查询两次。若您在 GSQL 命令行 shell 中运行此查询,命令和输出将如下所示:
RUN QUERY calculate_route_length("flight_to", True)
[
{
"@@dontChangeList": [],
"@@numChanged": 37606
}
]
RUN QUERY calculate_route_length("flight_route", True)
[
{
"@@dontChangeList": [],
"@@numChanged": 38535
}
]
测量和分析中心性
哪些机场有最多的中转航班?哪些机场对于希望方便地旅行到任何地方的个人或公司来说是最佳基地?如果这是您想了解的内容,请使用中心性算法。中心性是根据顶点在网络中的相对位置来衡量其重要性的指标。有几种方式来定义中心性;截至 2022 年 10 月,TigerGraph GDS 算法库共有 12 种。我们将尝试两种,即接近度和介数,比较它们的定义和结果。
顶点v的接近度中心性分数是从v到图中其他顶点的最短路径距离的倒数。例如,在一个四顶点图中,如果v到另外三个顶点的最短路径距离分别为 1、1 和 2,则接近度(v) = 1/(1+1+2) = 0.25。在航空路线网络中,机场的高接近度中心性分数意味着它具有大量直达和一站式路线到其他机场。小型地区机场通常因直接可达目的地数量较少而具有较低的接近度中心性。
在查询选择窗格中,单击 tg_closeness_cent 算法查询。表 9-2 列出了 TigerGraph GDS 算法的全部参数集。这些参数中的许多是 TigerGraph GDS 算法的标准功能,因此我们将花一些时间在这里进行回顾。
表 9-2. tg_closeness_cent 算法的参数
| 参数 | 描述 | 默认值 |
|---|---|---|
SET<STRING> v_type |
要使用的顶点类型 | (空字符串集合) |
SET<STRING> e_type |
要使用的边类型 | (空字符串集合) |
SET<STRING> rev_e_type |
要使用的反向边类型 | (空字符串集合) |
INT max_hops |
如果 >=0,则只查看距离每个顶点这么远的内容 | 10 |
INT top_k |
仅输出这么多分数(分数始终按从高到低排序) | 100 |
BOOL wf |
是否对多组件图使用瓦特斯曼-福斯特标准化 | True |
BOOL print_results |
如果为真,则将 JSON 输出到标准输出 | True |
STRING result_attr |
如果不为空,则将中心性值存储为浮点格式到此顶点属性 | (空字符串) |
STRING file_path |
如果不为空,则以 CSV 格式将输出写入此文件 | (空字符串) |
BOOL display_edges |
如果为真,则在 JSON 输出中包含图的边缘,以便可以显示完整的图形 | False |
前三个参数(v_type、e_type、rev_e_type)指定算法应在图的哪些顶点和边上运行。一些算法设计用于有向边,而一些则用于无向边。重要的是查阅文档⁶,了解首选或允许的边类型。图中可能存在反向边,也可能不存在;这是模式设计者的选择。
接下来的三个参数(max_hops、top_k、wf)是专门针对 closeness_centrality 的,尽管 max_hops 和 top_k 在几个其他算法中也出现过。对于接近中心性,距离超过 max_hops 限制的顶点将不会被考虑在平均距离计算中。top_k 适用于产生可以视为排名的结果的算法。wf 参数允许在由不同大小的断开子图组成的图中标准化分数的修改测度。
这四个参数(print_results、result_attr、file_path、display_edges)是用户指定结果传递方式的标准参数。默认情况下,将 JSON 文本流式传输到标准输出。
使用 v_type = Airport、e_type = flight_to 和 rev_e_type = reverse_flight_to 运行 tg_closeness_cent。其他参数可以保持默认值。
结果显示,前 10 名机场是 FRA、CDG、LHR、DXB、AMS、LAX、JFK、YYZ、IST 和 ORD,对应法兰克福、巴黎戴高乐、伦敦希思罗、多莫杰多沃、阿姆斯特丹、洛杉矶、纽约肯尼迪、多伦多、伊斯坦布尔和芝加哥奥黑尔。这些机场被广泛认可为世界上最繁忙和最重要的枢纽机场,因此结果看起来合乎逻辑。
我们接下来计算介数中心性。一个顶点的介数中心性定义为通过它的最短路径数量除以图的总最短路径数量。高介数的一个例子是巴拿马运河。它是连接大西洋港口和太平洋港口之间海上旅行的一部分,因此它的介数中心性很高,尽管巴拿马本身很少作为旅程的起点或终点。加油站通常也放置在介数中心性高的交叉路口。虽然它们通常不是旅程的起点或终点,但对于加油站而言,沿着许多旅行者的路线是有用的,每个旅行者的起点和终点路线不同。
接下来,使用与tg_closeness_cent相同的参数设置运行tg_betweenness_cent。由于介数中心性考虑从任何地方到任何地方的路径,所以运行时间较长,而接近中心性只考虑一个中心顶点。
提示
所有算法并非同质。查看您的算法库文档,了解运行算法所需的预期时间和资源。
对于介数中心性,前 10 名机场是多莫杰多沃、北京、芝加哥奥黑尔、伊斯坦布尔、波哥大、丹佛、亚特兰大、马尼拉、布宜诺斯艾利斯和达拉斯-沃斯堡。这些结果相当不同,也许令人惊讶。请记住,介数中心性高分给像巴拿马运河这样的瓶颈或门户。我们可以推测波哥大和布宜诺斯艾利斯对南美地区的区域机场非常重要。马尼拉可能对菲律宾和东南亚发挥类似作用。
此外,标准介数算法认为所有最短路径同等重要,因此从洛杉矶到纽约的路径和从 Sale (Australia)到 QGQ (Attu, Greenland)的路径同样重要。我们没有考虑乘客数量或航班数。如果我们有这些数据,我们将修改算法以计算加权介数分数。由于 TigerGraph GDS 算法是用 GSQL 编写的,因此可以由 GSQL 用户修改。在下一节中,我们将尝试自定义算法。
查找最短路径
首先,我们将使用无权边的最短路径算法;这将告诉我们哪些航线中转最少。然后,我们将运行带有正边权重的最短路径算法,这将告诉我们哪些航线最少飞行里程。当然,可能存在多条具有相同路径长度的路径。一些算法找到一条最短路径,而一些找到所有最短路径。在加权图中,要确保算法找到绝对最短路径,必须遍历图中的每条边。因此,图库中的最短路径算法是一源到所有目标的类型。
我们需要知道源顶点的 ID。不幸的是,选择一个所有人都知道的机场 ID 系统很困难。城市名称是不够的,因为一些城市有多个机场,而许多旅行者知道像 LAX 这样的 IATA 代码,但这个数据库包括没有 IATA 代码的小型机场。OpenFlights.org 的数据表使用他们自己设计的数字 ID。例如,LAX 的 ID 是 3484。为了妥协,我们的图数据库将 IATA 代码与 OpenFlights.org 的 ID 连接起来,因此我们对 LAX 的 ID 是 LAX-3484。
为了帮助用户找到机场的 ID,起始套件包含一个名为 _search_for_vertex 的查询。它有三个参数:顶点类型,该顶点类型的属性名称以及您要查找的值。该查询返回所有匹配顶点的 ID 和名称。例如,如果我们想找到克利夫兰主要机场的 ID,我们将运行以下查询:
RUN QUERY _search_for_vertex("Airport","city","Cleveland")
我们找到了三个匹配的机场。克利夫兰霍普金斯国际机场是主要的一个,所以让我们使用 CLE-3486。在 GraphStudio 的表视图中查看,您将看到类似 Table 9-3 的输出。
表 9-3. 克利夫兰机场的搜索结果
| v_id | v_type | Result.id | Result.name |
|---|---|---|---|
| HDI-8793 | 机场 | HDI-8793 | 哈德威克场 |
| CLE-3486 | 机场 | CLE-3486 | 克利夫兰霍普金斯国际机场 |
| BKL-8544 | 机场 | BKL-8544 | 伯克湖滨机场 |
现在用以下参数运行最短路径算法 tg_shortest_ss_no_wt:
source = CLE-3486
v_type = Airport
e_type = flight_to
查看 JSON 或表格输出。这是大量数据(记住我们有将近 8000 个机场),而且许多机场 ID 都不熟悉。Table 9-4 展示了最短路径之一。尽管数据可能对数据库是正确和有用的,但对人类不太友好。
表 9-4. 从 CLE 到其他机场的最短路径示例
| v_id | v_type | ResultSet.@min_dis | ResultSet.@path_list |
|---|
| ZQZ-10940 | 机场 | 4 | [ “CLE-3486”,
“YYZ-193”,
“TPE-2276”,
“SJW-6347”,
“ZQZ-10940”
] |
在下一节中,我们将修改算法以提供更可读的输出,并且仅显示距离源顶点一定距离的结果,使用边权重。
修改 GSQL 算法以自定义输出
让我们对输出进行两次更改以进行定制。首先,在 GSQL 代码窗口上方的菜单栏中点击“另存为”图标。将复制的算法命名为tg_shortest_ss_modified。我们只输出不超过三跳的路径,并将城市名称作为输出的另一个字段添加进去。我们在四个地方进行编辑:
-
找到行:
ListAccum<VERTEX> @path_list;在其下插入另一行:
ListAccum<STRING> @city_list;这定义了用于保存城市列表的数据结构。
-
大约在其下 10 行处,找到行:
s.@path_list = s;在其上插入这一行:
s.@city_list = s.city,重要的是在上方插入此行,以获得正确的标点符号。也就是说,我们希望得到以下结果:
s.@city_list = s.city, // Added s.@path_list = s; -
找到另一行更新
@path_list的行:t.@path_list = s.@path_list + [t],在其下插入另一行:
t.@city_list = s.@city_list + [t.city], -
找到接近末尾打印路径的行:
PRINT ResultSet[ResultSet.@min_dis, ResultSet.@path_list];修改并扩展它,使其变成:
PRINT ResultSet[ResultSet.@min_dis, ResultSet.@path_list, ResultSet.@city_list] WHERE ResultSet.@path_list.size() <= 3;
保存并安装此算法。使用与之前相同的输入参数运行它。现在你应该看到一些可识别的路径,例如表 9-5 中的示例。
表 9-5。最短路径示例,包含城市名称的添加
| v_id | v_type | ResultSet.@city_list | ResultSet.@min_dis | ResultSet.@path_list |
|---|
| LAR-5746 | 机场 | [ “克利夫兰”,
“丹佛”,
“拉勒米”
] | 2 | [ “CLE-3486”,
“DEN-3751”,
“LAR-5746”
] |
| BQK-5725 | 机场 | [ “克利夫兰”,
“亚特兰大”,
“布伦瑞克”
] | 2 | [ “CLE-3486”,
“ATL-3682”,
“BQK-5725”
] |
现在让我们运行正权重的最短路径算法。这个版本更适合希望最小化行程距离的旅行者。如果我们的权重是 CO[2]排放而不是英里数,那么我们将会最小化碳排放。
虽然起始套件可能已经有一个算法,但让我们去 GitHub 代码库中的算法库查找最新版本。在网络浏览器中,前往https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms。从那里,深入几个类别和子类别——路径 → 最短路径 → 加权 → 正向 → 回溯——最终找到tg_shortest_ss_pos_wt_tb.gsql。
如果你在 GraphStudio 中还没有tg_shortest_ss_pos_wt_tb查询,请在 GraphStudio 查询选择窗格中点击“创建查询”(+符号)按钮。命名新查询为tg_shortest_ss_pos_wt_tb。从 GitHub 代码库中复制tg_shortest_ss_pos_wt_tb.gsql中的文本,并将其用于替换 GraphStudio 中现有查询文本。保存并安装查询。使用以下设置运行算法:
Vertex_id = CLE-3486
v_type = Airport
e_type = flight_to
wt_attr = miles
wt_type = INT
output_limit = 10000
有人可能认为里程最少与连接最少会有很好的相关性,但在结果中,你会看到一些有 8 甚至 10 个跳数的路径。为了缩小结果范围,让我们对算法进行一次修改:只显示总行程距离小于 3,000 的路径(表示总行程距离短于三千英里)。这将限制结果在北美地区。
查找打印输出的行:
PRINT tmp[tmp.@min_path_heap.top().cost as cost, tmp.@path_list as p];
添加 WHERE 子句(并移动分号),将它们改为:
PRINT tmp[tmp.@min_path_heap.top().cost as cost, tmp.@path_list as p]
WHERE tmp.@min_path_heap.top().cost < 3000;
这些较短的结果仍然有一些长路径。例如,可以通过五个跳数从克利夫兰到 ZKE(安大略省卡舍丘安小镇,仅可通过机场和季节性冰路到达),经过 YYZ(多伦多)、YTS(蒂米斯)、YMO(穆森尼)、和 YFA(阿尔巴尼堡),全程只有 820 英里。这条五跳路径是一条相当直通的北上路径。由于北部城镇非常小且航空服务有限,它需要多次跳转。
查找并分析社区
我们预计全球机场网络将高度互联,但世界上一些地区仅由较小的地区机场提供服务。社区检测算法会指出这些地点吗?
在有向图中,强连通分量(SCC)是指最大的顶点集合,使得组件中的每个顶点都可以到达组件中的任何其他顶点。在航空网络中,如果航空公司在两个机场之间提供双向直达服务,则满足 SCC 要求很容易。在一些需求较少的地区,直达服务不是双向的。这就是我们可能会找到一个断点的地方,将图分隔为单独的 SCC。
使用以下参数设置运行 tg_scc:
v_type_set = Airport
e_type_set = flight_to
rev_e_type_set = reverse_flight_to
top_k_dist = 100
print_limit = 10000
result_attr = score
top_k_dist 确定输出多少个社区,而 output_limit 确定输出多少个单独的顶点。每个顶点的社区 ID 将存储在名为 score 的顶点属性中。
将输出切换为表格结果。有两个表需要显示。在 @@cluster_dist_heap 表中(见 表 9-6),我们得知最大的社区包括 3,354 个机场。然后有一个包括 10 个机场的社区,一个包括 8 个机场,三个包括 4 个机场,三个包括 2 个机场,以及 4,545 个单独的机场。
表 9-6. 机场社区大小和计数
| csize | num |
|---|---|
| 3,354 | 1 |
| 10 | 1 |
| 8 | 1 |
| 4 | 3 |
| 2 | 3 |
| 1 | 4,545 |
输出还包括所有顶点及其社区 ID 的完整列表。具有相同社区 ID 的所有顶点属于同一社区。扫描八千个顶点的列表不方便,因此让我们对算法进行另一次修改以获得更友好的输出。
使用“另存为”创建算法查询的副本,称为 tg_scc_modified。进行以下三个编辑:
-
在包含
Accum声明的顶部部分,添加以下声明:MapAccum<INT, ListAccum<VERTEX>> @@cluster_member_map;这种数据结构将记录属于每个社区的顶点列表。
-
接近结尾处,在输出结果部分和几个
clear()语句之后,找到以下块:v_all = SELECT s FROM v_all:s POST-ACCUM @@cluster_size_map += (s.@sum_cid -> 1);在
POST-ACCUM子句中插入一行额外的代码,并在此后添加五行的FOREACH块:v_all = SELECT s FROM v_all:s POST-ACCUM @@cluster_member_map += (s.@sum_cid -> s), //added @@cluster_size_map += (s.@sum_cid -> 1); FOREACH (cid, member_list) IN @@cluster_member_map DO IF member_list.size() == 1 OR member_list.size() > 50 THEN @@cluster_member_map.remove(cid); END; END;POST-ACCUM块中的代码构建成员列表,并且FOREACH块删除我们感兴趣的列表中过小或过大的列表。 -
找到以下行:
PRINT @@cluster_dist_heap;在此之后添加以下行:
PRINT @@cluster_member_map; // added
保存并安装tg_scc_modified算法查询。使用以下设置运行。这次,我们将排除个别顶点的列表:
v_type_set = Airport
e_type_set = flight_to
rev_e_type_set = reverse_flight_to
top_k_dist = 100
print_limit = 10000
print_results = false
result_attr = score
查看@@cluster_member_map的表视图结果。结果显示一个包含四名成员的社区 1048630:[“AKB-7195”, “DUT-3860”, “KQA-6134”, “IKO-7196”],以及一个包含八名成员的社区 3145861:[“CXH-5500”, “LKE-6457”, “WSX-8173”, “RCE-8170”, “FBS-8174”, “LPS-6136”, “YWH-4106”, “DHB-9540”]。你看到的社区 ID 值可能有所不同,但成员组合应保持一致。
让我们来可视化一个包含 10 名成员及其航班连接的社区。注意社区 ID:1048774。前往探索图表页面。
你应该在搜索顶点(放大镜图标)工作界面上。按照以下步骤进行:
-
在搜索配置窗格中,紧挨着
**机场**,点击筛选器图标以打开“添加属性筛选器”弹出窗口。 -
在条件下拉菜单中,选择 Expression1 == Expression2。
-
对于 Expression1,设置 Operand = 属性,属性名称 = score。
-
对于 Expression2,设置 Operand = 实数,值 = 1048774。现在你的窗口应该看起来像 图 9-2。
-
点击添加。
-
回到搜索配置窗格,确保选择顶点按钮旁边的顶点数至少为 10。
-
点击选择顶点按钮。

图 9-2。添加筛选器以选择属于社区 1048774 的顶点
在探索模式垂直菜单中,点击第二个图标,“从顶点扩展”。在边类型列表中,取消选中**航班路线**,以便只包括**航班至**边。确保每个顶点的边数至少为 10。点击扩展按钮。
现在你应该看到顶点之间有边。为了整理显示,点击图表显示窗格右下角的布局模式按钮。选择 force。你现在应该看到一个星形图案,如 图 9-3。我们希望了解更多关于这些顶点的信息,因此点击顶部菜单中的设置(齿轮形状)按钮。选择**机场**顶点类型后,勾选城市和国家的框,并点击底部的应用按钮。这会在图表视图中添加城市和国家的标签。
我们可以看到,这个航班网络局限于南太平洋的新喀里多尼亚群岛。其枢纽是首都努美阿。此外,当我们执行“从顶点扩展”的步骤时,如果有任何飞往新喀里多尼亚以外机场的航班,它们会显示出来。显然,我们的数据库中没有这样的航班。这可能不符合现实。OpenFlights.org 数据集可能缺少一些航班,但它仍然证明是一个用来揭示事实和见解的有趣工具。

图 9-3. 覆盖新喀里多尼亚的孤立航线社区(请在oreil.ly/gpam0903上查看该图的更大版本)
章节总结
在本章中,我们探讨了如何使用图算法分析航空网络的方法。我们使用了 GDS 库中包含的内置 GSQL 算法来进行路径查找、中心性计算和社区检测操作。此外,我们修改了现有的查询算法来过滤数据,并为我们提供了更易读和有用的结果。最后,我们进一步探索了 GraphStudio 中的探索图窗口的功能,仅需几秒钟就创建了一个视觉上吸引人的图表,并具有易于阅读的标签。
¹ “我们最重要的交通和运输数据”,斯希普霍尔,访问时间 2023 年 5 月 24 日,https://www.schiphol.nl/en/schiphol-group/page/traffic-review。
² “航空公司、机场和航线数据”,OpenFlights.org,访问时间 2023 年 5 月 24 日,https://openflights.org/data.html。
³ 国际航空运输协会为每个主要机场分配一个三个字母的代码,如 AMS 代表阿姆斯特丹斯希普霍尔机场,CDG 代表巴黎戴高乐机场。顶点属性ID是仅在此数据集中使用的内部编号。
⁴ “tigergraph / gsql-graph-algorithms”,GitHub,访问时间 2023 年 5 月 24 日,https://github.com/tigergraph/gsql-graph-algorithms。
⁵ 在现实生活中,航班通常会选择稍微绕道以利用风流或避开受限制的空域。
⁶ “TigerGraph 图数据科学库”,TigerGraph,访问时间 2023 年 5 月 24 日,https://docs.tigergraph.com/graph-ml/current/intro。
第三部分:学习
第十章:基于图的机器学习方法
完成本章后,你将能够:
-
列出图数据和分析如何改进机器学习的三种基本方式
-
指出哪些图算法在无监督学习中已被证明是有价值的
-
提取图特征以丰富你的训练数据,用于监督机器学习。
-
描述神经网络如何扩展到图上进行学习
-
提供使用案例和示例,以说明基于图的机器学习。
-
选择适合你的基于图的机器学习类型
现在我们开始书中的第三个主题:学习。也就是说,我们要认真对待机器学习的核心:模型训练。图 10-1 展示了一个简单的机器学习流程的各个阶段。在本书的第一部分中,我们探讨了连接主题,这符合流程的前两个阶段:数据获取和数据准备。图数据库使得从多个来源提取数据到一个连接的数据库中,并执行实体解析变得更加容易。

图 10-1. 机器学习流程
在本章中,我们将展示图如何增强流程的核心阶段:特征提取和至关重要的模型训练。特征简单地说就是数据实体的特性或属性,比如一个人的年龄或一件衣服的颜色。图提供了一整套基于实体如何与其他实体连接的特征,这些独特的面向图的特征增强了机器学习的原始材料,使其能够建立更好的模型。
本章包括四个部分。前三个部分分别描述了图如何增强机器学习的不同方式。首先,我们将从使用图算法进行无监督学习开始,因为这与我们在第二部分讨论的技术类似。其次,我们将转向用于监督和无监督机器学习的图特征提取。第三,我们以直接在图上进行模型训练结束,包括聚类、嵌入和神经网络的技术。第四部分回顾了各种方法,以便比较它们,并帮助您决定哪些方法能够满足您的需求。
使用图算法进行无监督学习
无监督学习是监督学习和强化学习的姊妹,它们共同构成机器学习的三大分支。如果你希望你的 AI 系统学会如何执行任务,按照你的类别分类事物,或者进行预测,你会选择使用监督学习和/或强化学习。然而,无监督学习有一个巨大的优势,即自给自足且准备就绪。不像监督学习,有些情况下你不需要已经知道正确答案。不像强化学习,你不必在通过试错学习时耐心和宽容。无监督学习只是获取你拥有的数据并报告其所学到的内容。
一个无监督学习算法可以查看你的客户和销售网络,并识别你实际的市场细分,这可能不符合年龄和收入的简单观念。无监督学习算法可以通过从你的数据而不是你的先入之见中确定“正常”来指出异常或远离正常的客户行为。例如,异常值可以指出哪些客户可能会流失(即停止使用你的产品或服务)。
我们将从图数据中学习的第一种方式是应用图算法来发现数据的模式或特征。在第六章中,我们详细介绍了五种算法类别。在本节中,我们将讨论哪些算法适合无监督学习。我们还将介绍另一个图分析任务:频繁模式挖掘。
通过相似性和社区结构学习
在第六章中介绍的五种算法类别中,最后一种——分类和预测——通常被数据科学家认为属于机器学习领域。特别是分类通常是监督学习。预测有各种各样的形式。正如我们之前指出的那样,这两个任务都依赖于某种度量相似性的方法。因此,相似性算法是机器学习的关键工具之一。
如果你找到所有具有高 Jaccard 相似性的顶点,可能感觉不像在做机器学习。你可以再进一步:找到的相似顶点数量是比你预期的要高还是低?你可以根据顶点通常具有的连接数以及两个随机顶点有共同邻居的可能性来建立你的期望。这些特征可以告诉你关于图表及其所代表的现实世界事物的重要信息。例如,假设一个大公司在图表中映射出其员工和各种与工作相关的数据。经理可以搜索是否有其他具有与其当前团队相似工作资格的员工。结果对于员工交叉培训、弹性和工作力量中的冗余有何影响?
当一个图的结构由许多个体玩家而不是中央规划决定时,它的社区结构不是事先已知的;我们必须分析图来发现结构。结构反映了实体及其相互关系,因此学习结构告诉我们一些关于实体及其动态的信息。基于模块性的算法如 Louvain 和 Leiden 是自学习的良好示例:通过查看图的自身连接相对密度来确定社区成员资格。递归定义的 SimRank 和 RoleSim 度量也符合无监督学习的自学习特征。那么 PageRank 难道不也是无监督学习的一种形式吗?
这些算法也非常有价值。许多金融机构发现,将中心性和社区算法应用于交易图表中有助于更好地识别金融犯罪。
查找频繁模式
正如本书所述,图表非常棒,因为它们可以轻松发现和分析基于连接的多重连接模式。在第二部分:分析中,我们讨论了查找特定模式,并且在本章稍后将回到这个话题。在无监督学习的背景下,这是目标:
发现所有频繁发生的模式。
计算机科学家称这为频繁子图挖掘任务,因为连接的模式只是一个子图。这个任务特别适用于理解自然行为和结构,比如消费者行为、社会结构、生物结构甚至软件代码结构。然而,它也提出了一个更为困难的问题。“任何和所有”大图中的模式意味着要检查大量可能的出现。阈值参数 T 是挽救的一线希望。要被视为频繁,模式必须至少出现 T 次。选择一个好的 T 值很重要。我们希望它足够高,以过滤掉小的、不重要的模式—我们过滤得越多,我们需要做的总体工作就越少—但不要过高以至于排除有趣的模式。选择一个好的阈值可以成为一个机器学习任务。
有许多高级方法尝试加快频繁子图挖掘的速度,但基本方法是从一边的模式开始,保留至少出现 T 次的模式,然后尝试连接这些模式以形成更大的模式:
-
根据它们的类型和端点顶点的类型对所有边进行分组。例如,
Shopper-(bought)-Product是一个模式。 -
计算每个模式出现的次数。
-
保留所有频繁模式(至少有 T 个成员)并且丢弃其余模式。例如,我们保留
Shopper-(lives_in)-Florida但是排除Shopper-(lives_in)-Guam因为它不频繁。 -
考虑每对具有兼容顶点类型的组(例如,组 1 和 2 都有一个
Shopper顶点),并查看组 1 中有多少个单独顶点也在组 2 中。将这些单独的小模式合并以形成更大模式的新组。例如,我们合并在频繁模式Shopper-(bought)-Blender中相同人物也在频繁模式Shopper-(lives_in)-Florida中的情况。 -
重复步骤 2 和 3(过滤频率)针对这些新形成的大模式。
-
使用扩展的模式集重复步骤 4。
-
当没有新的频繁模式建立时停止。
在计数中存在一个复杂性(步骤 2)。这个复杂性是同构性,也就是说,相同的顶点和边集如何以多种方式适配模板模式。考虑模式 A-(friend_of)-B。如果 Jordan 是 Kim 的朋友,这暗示 Kim 也是 Jordan 的朋友,这是一个实例还是两个实例?现在假设模式是“找到朋友 A 和 B 的成对,他们都和第三个人 C 是朋友”。这形成一个三角形。假设 Jordan,Kim 和 Logan 形成一个友谊三角形。我们可以有六种可能的方式将 Jordan,Kim 和 Logan 分配给变量 A,B 和 C。您需要事先决定是否应将这些类型的对称模式分开计数,然后确保您的计数方法是正确的。
图算法可以对图数据进行无监督机器学习。本节的关键收获如下:
-
几种类别的图算法符合无监督学习的自学习理念:相似性、社区检测、中心性、预测以及频繁模式挖掘。
-
无监督学习的好处在于提供见解,而无需事先分类。无监督学习还可以根据数据自身的上下文进行观察。
提取图特征
在前一节中,我们展示了如何使用图算法进行无监督机器学习。在大多数示例中,我们分析整个图以发现一些特征,例如社区或频繁模式。
在本节中,您将了解图如何提供额外和有价值的特征,以描述和帮助您理解您的数据。图特征是基于图中连接模式的特征。特征可以是局部的——归因于单个顶点或边的邻域——或全局的——涉及整个图或子图。在大多数情况下,我们对顶点特征感兴趣:顶点周围邻域的特征。这是因为顶点通常代表我们想要用机器学习建模的现实世界实体。
当一个实体(现实世界物体的一个实例)有多个特征,并且我们按照标准顺序排列这些特征时,我们称之为特征向量。本节我们将讨论的一些方法提供单个特征,其他方法则生成整套特征。您可以将一个顶点的实体属性(那些不基于连接的属性)与从本节讨论的一个或多个方法中获得的图特征连接起来,以制作更长、更丰富的特征向量。我们还将看一种特殊的特征向量称为嵌入,它总结了顶点的整个邻域。
这些特征可以直接提供见解,但它们最强大的用途之一是丰富监督机器学习的训练数据。特征提取是机器学习管道中的关键阶段之一(请参考图 10-1)。对于图形数据来说,这尤为重要,因为传统的机器学习技术设计用于向量,而不是图形。因此,在机器学习管道中,特征提取也是将图形转换为不同表示的地方。
在接下来的章节中,我们将讨论三个关键主题:领域无关特征、领域相关特征以及图嵌入的激动人心的发展。
领域无关特征
如果图特征对您来说是新的,理解它们的最佳方法是查看适用于任何图的简单示例。因为这些特征可以用于我们建模的任何类型的数据,我们称它们为领域无关。考虑图中的图 10-2。我们看到一个友谊网络,并计算一些简单的领域无关图特征的出现次数。

图 10-2. 带有有向友谊边的图(请在 oreil.ly/gpam1002 查看此图的更大版本)
表 10-1 展示了四个选定顶点(亚历克斯、查斯、菲奥娜、贾斯汀)和四个选定特征的结果。
表 10-1. 来自图图 10-2 的领域无关特征示例
| 入邻居数量 | 出邻居数量 | 两个前向跳跃内的顶点数量 | 三角形数量(忽略方向) | |
|---|---|---|---|---|
| 亚历克斯 | 0 | 2 (鲍勃、菲奥娜) | 6 (B、C、F、G、I、J) | 0 |
| 查斯 | 1 (鲍勃) | 2 (达蒙、埃迪) | 2 (D、E) | 1 (查斯、达蒙、埃迪) |
| 菲奥娜 | 2 (亚历克斯、艾维) | 3 (乔治、艾维、贾斯汀) | 4 (G、I、J、H) | 1 (菲奥娜、乔治、艾维) |
| 贾斯汀 | 1 (菲奥娜) | 0 | 0 | 0 |
您可以通过查看超过一到两个跳点的示例,考虑顶点或边的通用权重属性,并通过计算更复杂的方式(计算平均值、最大值或其他函数)轻松生成更多特征。因为这些是领域无关的特征,我们不考虑“人”或“朋友”的含义。我们可以将对象类型更改为“计算机”和“发送数据至”。但是,如果存在许多具有非常不同含义的边类型,则领域无关特征可能不适合您。
图元
另一种提取与领域无关特征的选择是使用图元。¹ 图元 是小型子图模式,已经系统地定义,以包括每个可能的配置,直到最大顶点数。图 10-3 展示了所有最多五个顶点(或节点)的 72 种图元。请注意,图中显示了两种类型的标识符:形状 ID(G0、G1、G2 等)和图元 ID(1、2、3 等)。形状 G1 包含两种不同的图元:当参考顶点位于三顶点链的末端时是图元 1,当参考顶点位于中间时是图元 2。
统计围绕给定顶点的每个图案图形的出现次数,提供了一个标准化的特征向量,可以与任何图中的其他顶点进行比较。这种通用签名让您可以基于其邻域结构对实体进行聚类和分类,适用于诸如预测一个国家的世界贸易动态²或动态社交网络如 Facebook 的链接预测³等应用。

图 10-3. 最多五个顶点(或节点)的图形图案⁴(请在 oreil.ly/gpam1003 查看此图的较大版本)
图形图案的一个关键规则是它们是感兴趣图中一组顶点的诱导子图。诱导意味着它们包括选定顶点集之间的所有边缘。该规则使得每个特定的顶点集最多匹配一个图形图案模式。
例如,在 图 10-2 中考虑 Fiona、George、Howard 和 Ivy 这四人。如果有的话,它们匹配哪种形状和图形图案?它是形状 G7,因为这四人形成一个带有一个交叉连接的矩形。它们不匹配形状 G5,即方形,因为 George 和 Ivy 之间有交叉连接。当我们谈论那个交叉连接时,请仔细查看形状 G7 的两个图形图案,图形图案 12 和 13. 图形图案 13 的源节点位于交叉连接的一端,正如 George 和 Ivy 所在的位置。这意味着图形图案 13 是它们的图形图案之一。Fiona 和 Howard 位于方形的另外两个角落,它们没有交叉连接。因此,它们在其图形图案合集中具有图形图案 12。
显然,我们最初讨论的特征之间存在一些重叠(例如邻居数量)和图形图案之间。假设顶点 A 有三个邻居 B、C 和 D,如 图 10-4 所示。然而,我们不知道任何其他连接。关于顶点 A 的图形图案我们了解到什么?
-
它展示了图形图案 0 模式三次。计算其出现次数非常重要。
-
现在考虑包含三个顶点的子图。我们可以定义包含 A 的三种不同子图:(A, B, C), (A, B, D), 和 (A, C, D)。这些三元组中的每一个都满足图形图案 2 或 3. 如果不知道 B、C 和 D 之间的连接(图中虚线边),我们就无法更加具体地判断。
-
考虑所有四个顶点,我们可能会说它们匹配图形图案 7. 由于 B、C 和 D 之间可能存在其他连接,实际上可能是不同的图形图案。是哪一个?如果有一个外围连接,则为图形图案 11;如果有两个连接,则为图形图案 13;如果有所有三种可能的连接,则为图形图案 14。

图 10-4. 直接邻居和图形图案的影响
图图特征的优点在于它们是彻底和有条理的。检查所有大小为五节点的图图相当于考虑源顶点四跳邻域的所有细节。您可以运行自动化的图图计数器,而不必花费时间和金钱来设计定制特征提取。图图的缺点在于它们可能需要大量的计算工作,而且可能更有成效地专注于更有选择性的领域相关特征。我们很快将介绍这些特征类型。
图算法
这里有第三种提取领域无关图特征的选项:图算法!特别是在第二部分讨论过的中心性和排名算法,因为它们系统地查看每个顶点周围的所有内容,并为每个顶点产生一个评分。图 10-5 和 图 10-6 分别展示了早期展示的图的 PageRank 和接近中心度⁵分数。例如,Alex 的 PageRank 分数为 0.15,而 Eddie 的 PageRank 分数为 1. 这告诉我们,Eddie 比 Alex 受到同行更多的重视。Eddie 的排名不仅取决于连接的数量,还取决于边的方向。像 Eddie 一样有两个连接且位于图形粗略的“C”形末端的 Howard,只有 0.49983 的 PageRank 分数,因为一条边进入,另一条边出去。

图 10-5. 友谊图的 PageRank 分数(请在oreil.ly/gpam1005上查看此图的更大版本)
图 10-6 中的接近中心度分数讲述了一个完全不同的故事。Alex 因为位于“C”形中心而获得了 0.47368 的最高分。Damon 和 Howard 的分数接近或接近底部——分别为 0.11111 和 0.22222,因为他们位于“C”形的末端。

图 10-6. 友谊图的接近中心度分数(请在oreil.ly/gpam1006上查看此图的更大版本)
领域无关特征提取的主要优势在于其普遍性:通用提取工具可以事先设计和优化,并可立即应用于任何数据。然而,其未导向的方法可能使其成为一种粗糙的工具。
领域无关特征提取有两个主要缺点。因为它不关注考虑的边和顶点类型,它可能会将形状相同但含义完全不同的出现物组合在一起。第二个缺点是它可能会浪费资源计算和分类没有实际重要性或逻辑意义的特征。根据您的用例,您可能希望专注于更有选择性的领域相关特征集。
领域相关特征
少量领域知识可以大大提升您的特征提取智能和效率。
在提取领域相关特征时,首先要注意图中的顶点类型和边类型。查看图的模式显示非常有帮助。一些模式将信息分层地分解为图路径,例如City-(IN)-State-(IN)-Country或Day-(IN)-Month-(IN)-Year。这是按位置或日期对数据进行图导向索引和预分组的方式。这在南韩 COVID-19 接触追踪数据的图模型中有所体现⁶,如 Figure 10-7 所示。虽然城市到国家和日到年各自都是两跳路径,但这些路径只是基线信息,不具有像Patient-(INFECTED_BY)-Patient-(INFECTED_BY)-Patient这样的两跳路径的重要性。
您可以看到当混合边类型时,图形途径方法和其他领域无关方法可能会提供令人困惑的结果。一个简单的解决方案是通过在查找特征时仅考虑某些顶点类型和边类型来采取领域半独立方法。例如,如果要查找图形模式,您可能希望忽略**Month**顶点及其连接边。您可能仍然关心患者的出生年份和他们旅行的确切日期,但不需要图告诉您每年包含 12 个月。

图 10-7。南韩 COVID-19 接触追踪数据的图模式(查看此图的更大版本:oreil.ly/gpam1007)
使用这种顶点和边类型的认知,您可以优化一些与领域无关的搜索。例如,尽管可以在任何图上运行 PageRank,但只有当所有边具有相同或相似的含义时,得分才有意义。在整个 COVID-19 接触追踪图上运行 PageRank 是没有意义的,因为我们无法将所有不同的顶点类型和边类型排在同一尺度上。然而,仅考虑**Patient**顶点和**INFECTED_BY**边是有意义的。然后,PageRank 将告诉您在引起感染方面谁是最有影响力的患者:可以说是零号患者。
在这种情况下,您还希望应用您对领域的理解来考虑具有两个或更多特定类型边缘的小模式,这些边缘表明某种意义。对于这种 COVID-19 接触追踪方案,最重要的事实是感染状态(InfectionCase)、谁(Patient)、在哪里(City 和 TravelEvent)以及何时(Day_)。连接这些路径是重要的。一个可能的特征是“患者 P 在 2020 年 3 月的旅行事件数”。一个更具体的特征是“在 2020 年 3 月,与患者 P 相同城市的感染患者数量”。这第二个特征是我们在第二部分提出的问题类型:分析。您将在 TigerGraph Cloud Starter Kit for COVID-19 中找到基于顶点和边缘类型特定 PageRank 和领域相关模式查询的示例。
让我们停顿一分钟,思考一下您提取这些特征的即时目标。您是否希望这些特征直接指出可操作的情况,还是正在构建一个供机器学习系统使用的模式集合?机器学习系统可以确定哪些特征重要,程度如何,以及以什么组合。如果是后者,这也是我们本章的重点,那么您无需构建过于复杂的特征。相反,专注于基础特征。尝试包含一些提供数字的特征(例如,多少个旅行事件)或在几种可能性之间进行选择的特征(例如,最常访问的城市)。
为了更多地激发使用基于图的特征的灵感,这里有一些现实世界系统中使用的领域相关特征的例子,以帮助检测金融欺诈:
-
贷款申请人与已知欺诈者之间有多少条最短路径,最大路径长度为上限(因为非常长的路径代表风险微乎其微)?
-
贷款申请人的邮寄地址、电子邮件地址或电话号码已被不同名称的申请人使用了多少次?
-
特定信用卡在过去 10 分钟内产生了多少次消费?
虽然很容易看出任何这些指标的高值更可能涉及金融不端行为,但我们的目标是选择正确的特征和正确的阈值。对欺诈的正确测试可以减少假阴性(错过真实欺诈案例)和假阳性(将一个本不是欺诈的情况标记为欺诈)。假阳性有双重伤害。它们伤害企业,因为它们拒绝了一个诚实的商业交易,也伤害了被不公正地标记为骗子的客户。
图嵌入:一个全新的世界
我们最后一种特征提取的方法是图嵌入,这是最近研究和讨论的热门话题。一些权威人士可能觉得我们将图嵌入分类为一种特征提取有些不寻常。图嵌入不是一种降维吗?它不是表示学习吗?它不是一种机器学习本身吗?这些说法都是正确的。让我们首先定义一下图嵌入。
嵌入 是将一个拓扑对象表示为特定系统中的表示,使得我们关心的属性被保持(或者被很好地近似)。最后一部分,保持我们关心的属性,正是我们使用嵌入的核心原因。选择适当的嵌入使得我们更方便地看到我们想要看到的内容。
这里有几个例子来帮助说明嵌入的含义:
-
地球是一个球体,但我们在平面纸上印制世界地图。地球在纸上的表示就是一种嵌入。地球有几种不同的标准表示或嵌入作为地图。图 10-8 展示了一些例子。
-
在 2010 年代末之前,当有人提到“图嵌入”时,他们可能指的是像地球这样的东西。要表示图中所有的连接而不让边相交,通常需要三个或更多维度。每当你在一个平面上看到一个图时,它都是一个嵌入,如图 10-9 所示。此外,除非你的数据指定了顶点的位置,否则即使是三维表示也是一个嵌入,因为它是关于顶点放置的特定选择。从理论上讲,实际上需要 n − 1 维度来表示具有 n 个顶点的图。
-
在自然语言处理(NLP)中,词嵌入是给定单词的一系列分数(即特征向量)。对于个别分数没有自然解释,但是机器学习程序设置这些分数,以便在训练文档中经常在一起出现的单词具有相似的嵌入。例如,“机器”和“学习”可能具有相似的嵌入。词嵌入对人类使用并不方便,但对需要计算机方式理解单词相似性和分组的计算机程序非常方便。

图 10-8. 地球表面的三种嵌入到二维空间的表示⁷

图 10-9. 一些图可以在二维空间中嵌入,而不会有交叉边。

图 10-10. 词嵌入
近年来,图嵌入(graph embedding)已经获得了新的含义,类似于词嵌入(word embedding)。我们计算一个或多个特征向量来近似图的邻域结构。实际上,当人们说“图嵌入”时,他们通常指的是顶点嵌入:计算图中每个顶点的特征向量。顶点的嵌入告诉我们有关它如何连接到其他顶点的信息。然后,我们可以使用顶点嵌入的集合来近似表示整个图,不再需要考虑边。还有方法将整个图汇总为一个嵌入,这对比较不同图很有用。在本书中,我们将重点讨论顶点嵌入。
Figure 10-11 展示了图例子(a)及其部分顶点嵌入(b)的示例。每个顶点的嵌入(32 个数字的系列)描述了其邻域的结构,而不直接提到任何邻居。

图 10-11. (a) Karate 俱乐部图⁸ 和 (b) 其中两个顶点的 64 元素嵌入
让我们回到分类图嵌入的问题。图嵌入给我们的是一组特征向量。对于一个拥有百万个顶点的图,一个典型的嵌入向量可能只有几百个元素,远少于一百万维的上限。因此,图嵌入代表了一种降维的形式。如果我们使用图嵌入来获取特征向量,它们也是一种特征提取的形式。正如我们将看到的,生成嵌入的方法符合机器学习的定义,因此它们也是表示学习的一种形式。
任何特征向量都能够作为嵌入吗?这取决于你选择的特征是否告诉了你想知道的信息。由于它们系统地分解了邻域关系,图小片段是我们即将研究的学习嵌入的最接近模型。
基于随机游走的嵌入
图嵌入中最著名的方法之一是使用随机游走来获取围绕每个顶点 v 的邻域的统计样本。随机游走是在图 G 中连接的跳跃序列。游走从某个顶点 v 开始。然后选择 v 的一个随机邻居并移动到那里。它重复选择随机邻居直到被告知停止。在无偏的游走中,选择任何一条出边的概率都是相等的。
随机游走非常适合,因为它们易于操作且能够高效地收集大量信息。我们之前看到的所有特征提取方法都要求遵循如何遍历图的精确规则;图小片段特别严格,因为它们具有非常精确的定义和彼此的区别。随机游走则毫不在意,随便走。
对于图 10-12 中的示例图,在 Figure 10-12 中假设我们从顶点 A 开始随机行走。有三分之一的概率我们会下一步到达顶点 B、C 或 D 中的一个。如果你从顶点 E 开始行走,则下一步将百分百地到达顶点 B。有随机行走规则的变化,可能停留在原地,反转上一步,跳回起点或跳转到随机顶点。

图 10-12 普通图,适合悠闲行走
每次行走可以记录为顶点列表,按访问顺序排列。A-D-H-G-C 是可能的一次行走。你可以将每次行走看作是一个签名。这些签名告诉我们什么呢?假设行走 W1 从顶点 5 开始,然后到达 2. 行走 W2 从顶点 9 开始,然后也到达 2. 现在它们都在 2. 从这里开始,它们在剩余行走中的概率完全相同。这些单独的行走不太可能相同,但是如果有一个“典型行走”的概念,通过对多次行走进行采样平均,那么,是的,顶点 5 和 9 的签名会相似。这一切都因为 5 和 9 共享了邻居 2. 此外,顶点 2 本身的“典型行走”也会相似,只是偏移了一个步骤。
结果表明,这些随机行走以与 SimRank 和 RoleSim 类似的方式收集邻域信息。不同之处在于,那些角色相似性算法考虑了 所有 路径(通过考虑所有邻居),这在计算上是昂贵的。让我们看看两种基于随机行走的图嵌入算法,它们使用了完全不同的计算方法,其中一种是从神经网络中借鉴过来的。
DeepWalk
DeepWalk 算法⁹ 对图中每个顶点收集长度为 λ 的 k 个随机行走。如果你了解 word2vec 算法¹⁰,其余的就简单了。将每个顶点看作一个词,每次行走看作一句话。选择窗口宽度 w 用于 skip-grams,以及长度 d 用于嵌入。你将得到每个顶点长度为 d 的嵌入(潜在特征向量)。DeepWalk 的作者发现,在他们的测试图中,设置行走计数 k = 30,行走长度 λ = 40,窗口宽度 w = 10,嵌入长度 d = 64 效果很好。你的结果可能会有所不同。图 10-13 (a) 展示了一个从顶点 C 开始长度为 16 的随机行走示例,即距起点 15 步或跳跃。当我们解释 skip-grams 时,这些阴影将会被解释。

图 10-13 (a) 随机行走向量及其对应的 skip-gram
我们假设您不了解 word2vec,因此我们将提供一个高层次的解释,足以让您理解正在发生的事情。这是概念模型。实际的算法通过许多统计技巧加快了工作速度。首先,我们构建了一个简单的神经网络,有一个隐藏层,如图 10-14 所示。输入层接受长度为n的向量,其中n = 顶点数。隐藏层的长度为d,嵌入长度,因为它将学习嵌入向量。输出层的长度也为n。

图 10-14. DeepWalk 的神经网络
每个顶点都需要在输入层和输出层中分配一个位置。例如,顶点 A 位于位置 1,顶点 B 位于位置 2,依此类推。在层与层之间有两个n × d连接的网格,从一个层的每个元素到下一个层的每个元素。每条边最初具有随机权重,但我们将逐渐调整第一个网格的权重。
从一个起始顶点开始进行一次行走。在输入时,我们使用one-hot 编码表示顶点。与起始顶点对应的向量元素设置为 1;所有其他元素设置为 0。我们将训练这个神经网络来预测给定输入顶点的邻域。
将第一个网格中的权重应用于我们的 one-hot 输入,我们得到隐藏层中的加权顶点。这是输入顶点嵌入的当前猜测。将隐藏层中的值乘以第二个网格的权重以获得输出层的值。现在你有一个具有随机权重的长度为n的向量。
我们将比较这个输出向量与步行的 skip-gram 表示。这是我们使用窗口参数w的地方。对于图中的每个顶点,在输入顶点v的前后w步内出现的次数。我们将跳过标准化过程,但您的最终 skip-gram 向量表达了在这次行走中每个n个顶点靠近顶点v的相对可能性。现在我们将解释图 10-13 的结果。顶点 C 是随机行走的起点;我们用深色标记了每次踏上顶点 C 的情况。浅色显示了距离顶点 Cw = 2 步的每一步。然后,我们在(b)中形成 skip-gram,通过计算在阴影区域内每个顶点被踏足的次数。例如,顶点 G 被踏足了两次,因此 skip-gram 在 G 的位置有 2。这是在一个小图上的长步行,因此大多数顶点都在窗口内被踏足。对于大图上的短步行,大多数值将为 0。
我们的输出向量应该是这个 skip-gram 的预测。比较两个向量中每个位置的值,如果输出向量的值高于 skip-gram 的值,则减少输入网格中相应的权重。如果值较低,则提高相应的权重。
你已经处理了一次遍历。重复这个过程,对每个顶点进行第二次遍历,直到你调整了k × n次权重。完成了!第一个n × d网格的权重是您n向量的长度-d嵌入。第二个网格呢?奇怪的是,我们从来不打算直接使用输出向量,所以我们没有费心调整它的权重。
下面是如何解释和使用顶点嵌入的方法:
-
对于神经网络来说,通常无法指出潜在特征向量中各个元素的明确现实世界含义。
-
基于我们如何训练网络,我们可以从 skip-gram 逆推,这些 skip-gram 表示顶点周围的邻居:具有相似邻域的顶点应该具有相似的嵌入。
-
如果您记得之前关于两条路径偏移一个步骤的例子,请注意,这两条路径的 skip-gram 应该非常相似。因此,彼此接近的顶点应该具有相似的嵌入。
对 DeepWalk 的一个批评是它的均匀随机游走过于随机。特别是在从源顶点远离之前,可能会漫步到足够接近源的邻域的样本。解决这个问题的一种方法是通过包括重置漫步的概率,神奇地传送回源顶点,然后再次进行随机步骤,如Zhou, Wu, and Tan所示。这被称为“带重启的随机游走”。
Node2vec
随机游走的一个有趣扩展是node2vec。它使用与 DeepWalk 相同的 skip-gram 训练过程,但它给用户两个调整参数来控制游走的方向:走得更远(深度)、走到侧面(广度)、或者后退一步。远和后退看起来很明显,但侧面到底意味着什么呢?
假设我们从图中的顶点 A 开始,图示见图 10-15。它的邻居是顶点 B、C 和 D。由于我们刚开始,选择任何一个都会向前移动。让我们去顶点 C。在第二步中,我们可以从顶点 C 的邻居中选择:A、B、F、G 或 H。

图 10-15. 说明带有内存的偏向随机游走在 node2vec 中使用
如果我们记得上一步中的选择,我们可以将当前的邻居分成三组:后向、侧向和前向。我们还为每个连接的边分配一个权重,代表选择该边的未标准化概率。
后向
在我们的例子中:顶点 A。边的权重 = 1/p。
侧面
这些是在上一步中可用的顶点,并且在本步中也可用。它们代表了访问先前所在位置的不同邻居的第二次机会。在我们的示例中:顶点 B。边权重=1。
正向
所有不返回或侧向的顶点。在我们的示例中:顶点 F、G 和 H。边权重=1/q。
如果我们设置p = q = 1,则所有选择的概率相等,因此我们回到了一个无偏的随机游走。如果p < 1,则返回比侧向更有可能。如果q < 1,则每个前向(深度)选项比侧向(广度)更有可能。返回还使步行保持在家附近(例如,类似于广度优先搜索),因为如果您向后走,然后随机向前走,您正在尝试前一邻域中的不同选项。
调整步行路径使得 node2vec 比 DeepWalk 更加灵活,这在许多情况下产生了更好的模型,但是以更高的计算成本为代价。
除了随机游走方法之外,还有几种其他的图嵌入技术,各有优缺点:矩阵因子分解、边重建、图核函数和生成模型。FastRP 和 NodePiece 展示了在现实世界中效率和准确性之间有一个有前途的平衡。虽然已经有些过时,但是蔡宏运、郑卫华和张晓川的图嵌入综述:问题、技术和应用的全面调查 提供了一个详尽的概述,并附有几个易于理解的表格,比较了不同技术的特点。伊利亚·马卡罗夫、德米特里·基谢列夫、尼基塔·尼基金斯基和洛夫罗·苏贝尔最近进行了一项关于图嵌入的调查。
图可以提供额外和有价值的特征来描述和理解您的数据。本节的主要收获如下:
-
图特征是基于图中连接模式的特征。
-
图小片和诸如中心性等的图算法为任何图提供了领域独立的特征。
-
应用一些领域知识来指导您的特征提取将产生更有意义的特征。
-
机器学习可以生成顶点嵌入,这些嵌入使用紧凑的特征向量编码顶点的相似性和接近性。
-
随机漫步是采样顶点邻域的简单方法。
图神经网络
在流行媒体中,如果没有使用神经网络,那就不是 AI;如果没有使用深度学习,那就不是机器学习。神经网络最初是为了模拟人类大脑的工作原理而设计的,但它们已经发展到可以处理计算机和数学的能力。主流模型假定您的输入数据是一个矩阵或张量;目前尚不清楚如何呈现和训练具有相互连接顶点的神经网络。但是有基于图的神经网络吗?是的!
图神经网络(GNN)是传统神经网络加入图形特性的变体。就像神经网络有几种变体一样,GNN 也有几种变体。将图本身包含到神经网络中的最简单方式是通过卷积。
图卷积网络
在数学中,卷积 是指两个函数以特定方式相互作用时对结果的影响。它通常用于模拟一个函数描述主要行为,而另一个函数描述次要效应的情况。例如,在图像处理中,卷积考虑到相邻像素以改善边界识别并添加人工模糊。在音频处理中,卷积用于分析和合成房间混响效果。卷积神经网络(CNN)是在训练过程中包含卷积的神经网络。例如,你可以用 CNN 进行人脸识别。CNN 会系统地考虑相邻像素,在分析数字图像时是至关重要的职责。
图卷积网络(GCN)是在学习过程中使用图遍历作为卷积函数的神经网络。虽然早期有一些相关工作,但第一个将图卷积的本质提炼为简单而强大的神经网络模型是由 Thomas Kipf 和 Max Welling 于 2017 年提出的“使用图卷积网络进行半监督分类”。
对于图形,我们希望每个顶点的嵌入包含关于与其他顶点关系的信息。我们可以使用卷积原理来实现这一点。图 10-16 显示了一个简单的卷积函数,顶部是一个通用模型,底部是一个更具体的例子。

图 10-16. 使用顶点邻居的卷积
在图中的部分(a),主要函数是 Features(v):给定顶点 v,输出其特征向量。卷积将 v 的特征与所有邻居 u1, u2,...,uN* 的特征结合起来。如果特征是数值的,简单的卷积操作可以是加法。结果是 v 的新卷积特征。在部分(b),我们设定 v = D,来自图 10-15。顶点 D 有两个邻居,A 和 H。在求和特征向量之后,我们再插入一步:除以主顶点的度。顶点 D 有 2 个邻居,所以我们除以 2。这样可以使输出值规范化,以防止它们不断增大。(是的,严格来说我们应该除以 deg(v) + 1,但更简单的版本似乎已经足够好了。)
让我们做一个快速的例子:
features0 = [3, 1 ,4, 1]
features0 = [5, 9, 2, 6]
features0 = [5, 3, 5, 8]
features1 = [6.5, 6.5, 5.5, 7.5]
通过让邻居分享其特征值,这个简单的卷积函数执行选择性信息共享:它决定了什么是被分享的(特征)以及由谁来分享(邻居)。使用这种卷积函数的神经网络往往会按照以下准则演变:
-
共享许多邻居的顶点往往会相似。
-
共享相同初始特征值的顶点往往会相似。
这些特性让人联想到随机游走图嵌入。
如何将这种卷积操作集成到神经网络中?请看图 10-17。

图 10-17. 两层图卷积网络
这个两层网络从左向右流动。输入是图中所有顶点的特征向量。如果特征向量水平排列,顶点垂直堆叠,则得到一个n × f矩阵,其中f是特征数。接下来,我们应用基于邻接矩阵的卷积。然后我们应用一组随机化权重(类似于随机游走图嵌入网络中所做的),将特征合并并减少到大小为h1的嵌入。通常*h1 < f。在将值存储到嵌入矩阵之前,我们应用一个激活函数(圆圈中的方块“S”表示),它作为滤波器/放大器。低值被推低,高值被推高。激活函数在大多数神经网络中都有使用。
因为这是一个两层网络,我们重复相同的步骤。唯一的区别在于这个嵌入可能具有不同的大小,通常h2 ≤ h1,以及这个权重网格具有不同的随机权重集。如果这是最后一层,那么它被视为输出层,并输出结果。通过有两层,每个顶点的输出嵌入考虑了两个跳之内的邻居。您可以添加更多层来考虑更深的邻居。通常两到三层提供最佳结果。层数过多时,每个顶点邻域的半径变得如此大,以至于即使与不相关顶点的邻域也有显著的重叠。
我们的例子展示了如何在无监督学习模式下使用 GCN。我们没有提供训练数据或目标函数;我们只是将顶点的特征与其邻居的特征合并。令人惊讶的是,无监督、未经训练的 GCN 也能产生有用的结果。GCN 的作者们尝试了一个三层未经训练的 GCN,使用了著名的 Karate Club 数据集。他们将输出层的嵌入长度设置为二,这样他们可以将这两个值解释为坐标点。绘制时,输出数据点显示出与 Zachary 的 Karate Club 中已知社区匹配的社区聚类。
GCN 架构足够通用,可用于无监督、监督、半监督甚至强化学习。GCN 和普通前馈神经网络唯一的区别在于添加向量特征与邻居特征聚合的步骤。图 10-18 展示了神经网络如何调整权重的通用模型。GCN 中的图卷积仅影响标有前向传播层的块。所有其他部分(输入值、目标值、权重调整等)决定了你正在进行的学习类型。也就是说,学习类型独立于你使用图卷积决定。

图 10-18. 神经网络中响应式学习的通用模型
注意力神经网络使用更高级的反馈和调整形式。这本书的范围之外详细介绍,但图注意力神经网络(GATs)可以调整每个邻居的权重(即聚焦注意力)并将它们相加进行卷积。也就是说,GAT 执行加权求和而不是邻居特征的简单求和,并且 GAT 会自我训练以学习最佳权重。当应用于相同的基准测试时,GAT 稍微优于 GCN。
GraphSAGE
基本 GCN 模型的一个限制是它仅对顶点加邻居特征做简单平均。我们似乎希望对这种卷积进行更多控制和调整。此外,不同顶点的邻居数量大幅变化可能导致训练困难。为了解决这一限制,William Hamilton、Rex Ying 和 Jure Leskovec 在他们的论文“大规模图上的归纳表征学习”中于 2017 年提出了 GraphSAGE。像 GCN 一样,这种技术也结合了邻居的信息,但做法有所不同。为了标准化邻居的学习,GraphSAGE 从每个顶点中采样固定数量的邻居。图 10-21 展示了 GraphSAGE 的块图,包括采样的邻居。

图 10-21. GraphSAGE 的块图
在 GraphSAGE 中,邻居的特征根据选择的聚合函数进行组合。这个函数可以是加法,就像 GCN 中一样。可以使用任何无序的聚合函数;长短期记忆(LSTM)在随机顺序和最大池化中工作良好。与 GCN 不同,源顶点不包含在聚合中;相反,聚合特征向量和源顶点的特征向量被串联起来形成双倍长度的向量。然后我们应用一组权重来混合这些特征,应用激活函数,并存储为顶点下一层的表示。这一系列集合构成神经网络中的一层,以及每个顶点一跳内信息的收集。GraphSAGE 网络有 k 层,每层都有自己的权重集合。GraphSAGE 提出了一个损失函数,如果附近的顶点具有相似的嵌入,就奖励它们,并且如果它们具有不相似的嵌入,则奖励远距离的顶点。
与 GCN 一样,在整个图上训练,你可以只用顶点及其邻域的一部分来训练 Graph SAGE。GraphSAGE 的聚合函数使用等大小的邻域样本,这意味着输入的排列方式并不重要。这种排列自由度使你可以使用一个样本进行训练,然后使用不同的样本进行测试或部署。因为它基于这些泛化的图邻域属性构建模型,GraphSAGE 执行 归纳学习。也就是说,该模型可以用来预测原始数据集中不存在的新顶点。相比之下,GCN 直接使用邻接矩阵,这强制它使用特定顺序排列的完整图。使用完整数据进行训练,并仅针对该数据学习模型是 传导学习。
在你的特定图中,从样本学习是否有效取决于你的图的结构和特征是否遵循全局趋势,这样一个随机子图看起来就像另一个大小相似的子图。例如,森林的一部分可能看起来很像另一部分森林。举个图的例子,假设你有一个包含客户与销售团队、网站和活动的所有互动、他们的购买以及你能获取的所有其他个人资料信息的客户 360 图。去年的客户根据其购买总金额和频率进行评级。合理地预期,如果你使用 GraphSAGE 和去年的图来预测客户评级,它应该能够很好地预测今年客户的评级。Table 10-2 总结了我们提出的 GCN 和 GraphSAGE 之间所有的相似性和差异。
Table 10-2. GCN 和 GraphSAGE 特性比较
| GCN | GraphSAGE | |
|---|---|---|
| 聚合邻居 | 所有 | n 个邻居样本 |
| 聚合函数 | 平均 | 几个选项 |
| 聚合顶点与邻居? | 与其他聚合 | 与其他连接 |
| 需要学习权重吗? | 对于无监督的转导模型不需要 | 对于归纳模型需要 |
| 监督? | 是 | 是 |
| 自监督? | 经过修改 | 经过修改 |
| 可以对顶点样本进行训练吗? | 否 | 是 |
基于图的神经网络使图在机器学习中变得主流。本节的关键收获如下:
-
图卷积神经网络在学习过程中通过平均每个顶点的邻居的特征向量与自己的特征进行增强,从而提升了基本神经网络的能力。
-
GraphSAGE 对基本的 GCN 进行了两个关键改进:顶点和邻域采样,并在学习过程中保持向量特征与其邻居的特征分离。
-
GCN 以转导方式学习(仅使用全数据学习该数据的信息),而 GraphSAGE 以归纳方式学习(使用数据样本学习可以应用于其他数据样本的模型)。
-
神经网络和图增强的模块化特性意味着 GCN 和 GraphSAGE 的思想可以转移到许多其他类型的神经网络中。
比较图机器学习方法
本章介绍了许多从图数据中学习的不同方法,但只是皮毛。我们的目标不是提供详尽的调查,而是提供一个框架,供您继续成长。我们概述了图驱动机器学习的主要类别和技术,描述了它们的特征和区别,并提供了简单的示例来说明它们的运作方式。简要回顾这些技术是值得的。我们的目标不仅是总结,还包括为您选择正确的技术提供指导,帮助您从连接的数据中学习。
机器学习任务的用例
表 10-3 汇集了每个主要学习任务的用例示例。这些是您可能在任何数据上执行的基本数据挖掘和机器学习任务,但这些示例对图数据特别相关。
表 10-3. 图数据学习任务的用例
| 任务 | 用例示例 |
|---|---|
| 社区检测 | 描绘社交网络 |
| 发现一个金融犯罪网络 | |
| 发现生物生态系统或化学反应网络 | |
| 发现意外依赖组件或过程的网络,例如软件流程或法律法规 | |
| 相似性 | 物理接近的抽象,距离的倒数 |
| 聚类、分类和链接预测的先决条件 | |
| 实体解析:找到两个在线身份,可能指的是同一个现实世界的人 | |
| 产品推荐或建议行动 | |
| 识别在不同但类似网络中执行相同角色的人员 | |
| 发现未知模式 | 识别你的网站或应用中最常见的“客户旅程” |
| 一旦识别出当前的模式,就开始注意变化 | |
| 链接预测 | 预测某人未来的购买或购买意愿 |
| 预测企业或个人关系的存在,即使这些关系未记录在数据中 | |
| 特征提取 | 通过图特征丰富你的客户数据,从而使得你的机器学习训练在分类和建模客户方面更加成功 |
| 嵌入 | 将大量特征转换为更紧凑的集合,以进行更高效的计算 |
| 在没有设计特定特征提取查询的情况下,全面捕获邻居的特征签名 | |
| 分类(预测类别) | 根据过去欺诈案例创建用于识别新欺诈案例的模型 |
| 预测未来疫苗接种者的分类结果,基于过去患者的测试结果 | |
| 回归(预测数值) | 基于过去参与者的结果预测减重效果 |
一旦确定您想要执行的任务类型,请考虑可用的基于图的学习技术,它们提供的内容以及它们的主要优势和差异。
模式发现和特征提取方法
表 10-4 列出了我们在本章和第六章中遇到的图算法和特征提取方法。
表格 10-4. 图中的模式发现和特征提取方法
| 任务 | 基于图的学习方法 | 评论 |
|---|---|---|
| 社区检测 | 连通分量 | 一个连接到社区就足够了 |
| k-核 | 至少与其他社区成员有 k 个连接 | |
| 模块性优化(例如,Louvain) | 社区内连接的密度相对较高 | |
| 相似性 | Jaccard 邻域相似性 | 计算共同关系的数量,适用于非数值数据 |
| 余弦邻域相似性 | 比较数值或加权关系向量 | |
| 角色相似性 | 递归地定义为具有相似邻居 | |
| 发现未知模式 | 频繁模式挖掘 | 从小模式开始构建到大模式 |
| 领域无关特征提取 | 图子结构 | 所有可能的邻域配置的系统列表 |
| 页面排名 | 排名基于同类型顶点之间的入度和排名 | |
| 接近中心性 | 接近度 = 到任何其他顶点的平均距离 | |
| 中介中心性 | 顶点在任意两个顶点之间的最短路径上出现的频率;计算速度较慢 | |
| 领域相关特征提取 | 搜索与您领域相关的模式 | 需要具有领域知识的人的定制努力 |
| 降维和嵌入 | DeepWalk | 如果向量具有类似的随机游走,则嵌入将类似,考虑接近性和角色;比 SimRank 更高效 |
| node2vec | 具有随机游走方向调整的 DeepWalk,用于更高的调整 |
图神经网络:总结和用途
本章介绍的图神经网络不仅在许多情况下直接有用,而且还是向更高级数据科学家展示如何在训练中包含图连接的模板。关键在于卷积步骤,该步骤考虑了相邻顶点的特征。所展示的所有 GNN 方法都可以用于无监督或监督学习。表 10-5 比较了图神经网络方法。
表 10-5. 三种图神经网络类型的总结
| 名称 | 描述 | 用途 |
|---|---|---|
| 图卷积网络(GCN) | 卷积:邻居特征的平均 | 特定图上的聚类或分类 |
| GraphSAGE | 卷积:邻居特征样本的平均 | 在样本图上学习代表性模型,除了聚类或分类 |
| 图注意力神经网络(GAT) | 卷积:邻居特征的加权平均 | 聚类、分类和模型学习;通过学习卷积权重增加调整和复杂性 |
章节总结
图及基于图的算法对机器学习流程的几个阶段有贡献:数据获取、数据探索和模式发现、数据准备、特征提取、降维和模型训练。正如数据科学家所知,没有金票,没有一种单一的技术可以解决所有问题。相反,你努力获取工具箱中的工具,发展使用这些工具的技能,并了解何时使用它们。
¹ Graphlets 最早由 Nataša Pržulj, Derek G. Corneil, 和 Igor Jurisi 在“建模相互作用组:无标度还是几何?”生物信息学 20, no. 18 (2004 年 12 月): 3508–3515 中提出, https://doi.org/10.1093/bioinformatics/bth436.
² Anida Sarajlić, Noël Malod-Dognin, Ömer Nebil Yaveroğlu, and Nataša Pržulj,“基于图元的有向网络特征化”,科学报告 6 (2016), https://www.nature.com/articles/srep35098.
³ Mahmudur Rahman 和 Mohammad Al Hasan,“使用图小结进行动态网络中的链接预测”,收录于《数据库中的机器学习与知识发现》第一部分,由 Paolo Frasconi、Niels Landwehr、Giuseppe Manco 和 Jilles Vreeken 编辑(意大利瑞瓦德尔加达:欧洲会议 ECML PKDD,2016 年),394–409 页。
⁴ Tijana Milenković 和 Nataša Pržulj,“通过图小结度量揭示生物网络功能”,癌症信息学 6 卷,第 10 期(2008 年 4 月),https://www.researchgate.net/publication/26510215_Przulj_N_Uncovering_Biological_Network_Function_via_Graphlet_Degree_Signatures。
⁵ 这些算法首次出现在第二部分:“分析”。
⁶ “[NeurIPS 2020] COVID-19 的数据科学 (DS4C),” Kaggle,访问于 2023 年 5 月 25 日,https://www.kaggle.com/datasets/kimjihoo/coronavirusdataset。
⁷ 来自 John P. Snyder 和 Philip M. Voxland 的《地图投影集锦》,第二版(美国地质调查专业论文 1453 号,1994 年),https://pubs.usgs.gov/pp/1453/report.pdf。
⁸ 这种可视化的分区来自于“Zachary's Karate Club”,维基百科,2017 年 4 月 5 日,https://en.wikipedia.org/wiki/File:Zachary%27s_karate_club.png。
⁹ 参见 Perozzi、Al-Rfou 和 Skiena 的文章 https://dl.acm.org/doi/abs/10.1145/2623330.2623732。
¹⁰ 参见 Mikolov、Sutskever、Chen、Corrado 和 Dean 的文章 https://arxiv.org/abs/1310.4546。
第十一章:实体解析再访
本章以流媒体视频服务的实体解析为例,介绍了无监督机器学习和图算法。完成本章后,您将能够:
-
列举适用于实体解析的无监督学习图算法类别
-
列出评估实体相似性的三种不同方法
-
理解参数化权重如何使实体解析成为监督学习任务
-
解释简单的 GSQL
FROM子句,并对ACCUM语义有一般了解 -
设置和运行 TigerGraph Cloud 入门套件,使用 GraphStudio
问题:识别现实世界用户及其喜好
流媒体视频点播(SVoD)市场规模庞大。全球市场规模的准确估算并不容易,但最保守的估计可能是 2020 年约 500 亿美元¹,而年增长率在未来五年左右将介于 11%²到 21%³之间。电影制片厂、电视网络、通讯网络和科技巨头一直在进行合并并重塑自己,希望成为娱乐消费的新首选格式的领导者:即时数字娱乐,适用于任何视频设备。
要成功,SVoD 提供商需要有内容来吸引和保留数百万订阅者。传统视频技术(电影院和广播电视)限制提供商每次只能在一个场所或广播区域播放一个节目。观众的选择非常有限,而提供商则选择能够吸引大众的内容。VHS 磁带和 DVD 的家庭视频引入了个性化。任何个人设备上的无线数字视频点播把权力交到了消费者手中。
提供商不再需要迎合大众。相反,成功的关键是微分割:为每个人提供一些内容。SVoD 巨头正在汇集大量现有内容的目录,同时在新内容上投入数十亿美元。这么多选择量产生了几个数据管理问题。有了这么多节目可供选择,用户很难浏览。提供商必须对内容进行分类、对用户进行分类,然后向用户推荐节目。良好的推荐可以提高观众收视率和满意度。
尽管预测客户的兴趣已经足够困难,但流媒体视频行业还需要克服一个多方面的实体解析问题。实体解析,您可能记得,是识别数据集中指向同一现实世界实体的两个或多个实体,然后将它们链接或合并在一起的任务。在今天的市场上,流媒体视频提供商面临至少三个实体解析挑战。首先,每个用户可能有多个不同的授权方案,每种设备都有一个。其次,企业合并很常见,需要合并组成公司的数据库。例如,Disney+结合了 Disney、Pixar、Marvel 和 National Geographic Studios 的目录。Max 汇集了 HBO、Warner Bros.、DC Comics 和 Discovery。第三,SVoD 提供商可能与另一家公司进行促销、附属或合作关系:客户可能因为是某些其他服务 B 的客户而能够访问流媒体服务 A。例如,Verizon 互联网服务的客户可以免费使用 Disney+、Hulu 和 ESPN+服务。
解决方案:基于图的实体解析
在我们设计解决方案之前,让我们从清晰地陈述我们想要解决的问题开始。
问题陈述
每个现实世界的用户可能有多个数字身份。我们的目标是发现这些数字身份之间的隐藏联系,然后将它们链接或合并在一起。通过这样做,我们将能够将所有信息连接起来,形成用户的更完整的画像。特别是,我们将知道一个人观看过哪些视频,从而更好地理解他们的个人喜好并提供更好的推荐。
现在我们已经明确了问题陈述,让我们考虑一个潜在的解决方案:实体解析。实体解析分为两部分:确定哪些实体可能相同,然后解析实体。让我们依次看看每个部分。
学习哪些实体是相同的
如果我们有幸拥有显示实际上是相同实体的示例的训练数据,我们可以使用监督学习来训练机器学习模型。在这种情况下,我们没有训练数据。相反,我们将依赖数据本身的特征,查看相似性和社区以执行无监督学习。
要做好这项工作,我们希望积累一些领域知识。一个人有多个在线身份的情况是什么,数据中有什么线索?以下是一些人可能创建多个账户的原因:
-
用户创建第二个账户是因为他们忘记了或忘记了如何访问第一个账户。
-
用户在两个不同的流媒体服务中都有账户,并且这些公司进入了合作或合并关系。
-
一个人可能有意设立多个不同的身份,也许是为了利用多个会员奖励,或者为了分开他们的行为配置文件(例如,在不同账户上观看不同类型的视频)。个人信息可能会有很大差异,但设备 ID 可能是相同的。
每当同一人在不同时刻创建两个不同账户时,由于琐碎或无害的原因,某些细节可能会有所不同。人们可能决定使用昵称,选择缩写城市或街道名称,误输入,有多个电话号码和电子邮件地址可供选择,但他们出于无特定原因做出了不同选择。随着时间的推移,地址、电话号码、设备 ID 甚至用户姓名可能会发生更大的变化。
尽管有多种情况导致一个人拥有多个在线身份,但我们似乎可以将我们的数据分析集中在只有两种模式上。在第一种模式中,大部分个人信息将相同或类似,但可能会有少量属性不同。即使两个属性不同,它们可能仍然相关。例如使用昵称或地址拼写错误。在第二种模式中,大部分信息不同,但一个或多个关键部分仍然相同,例如家庭电话号码或生日,以及行为线索(例如喜欢什么类型的视频以及观看它们的时间)。这些线索可能表明两个身份属于同一人。
要构建我们的解决方案,我们需要使用一些相似度算法,以及社区检测或聚类算法将相似的实体分组在一起。
解决实体
一旦我们使用适当的算法识别出我们认为相同的一组实体,我们将如何处理?我们希望更新数据库以反映这些新知识。有两种可能的方法可以实现这一点:将组合并为一个实体,或以一种特殊方式链接这些实体,以便每当我们查看组中的一个成员时,我们将立即看到其他相关的身份。
当考虑到一些在线身份信息不正确时,合并实体是有意义的,因此我们希望消除它们。例如,假设客户因姓名拼写错误或忘记已有账户而拥有两个在线账户。业务所有者和客户都希望消除一个账户,并将所有记录(购买历史、游戏得分等)合并到一个账户中。要确定删除哪个账户需要更多的具体案例知识,超出了我们的示例范围。
或者,可以简单地将实体链接在一起。具体来说,利用图中的两种实体类型:一个表示数字身份,另一个表示真实世界实体。解析后,数据库将显示一个真实世界实体与其各个数字身份之间有边,如图 11-1 所示。

图 11-1. 解析后与真实世界实体链接的数字实体
实施基于图的实体解析
我们将介绍的基于图的实体解析的实现可作为 TigerGraph 云起始套件使用。与往常一样,我们将专注于使用 GraphStudio 可视化界面。所有必要的操作也可以从命令行界面执行。
数据库内实体解析起始套件
使用 TigerGraph Cloud,部署一个新的云实例,并选择“数据库内机器学习用于大数据实体解析”作为用例。一旦安装了这个起始套件,按照第三章中“加载数据并安装查询的起始套件”一节中列出的步骤加载数据。
图模式
查看图模式,如图 11-2 所示,您可以看到账户、用户和视频是中心顶点,从它们辐射出多条边。其他顶点代表用户的个人信息和视频的特征。我们想比较不同用户的个人信息。遵循面向图形的分析的良好实践,如果我们想查看两个或更多实体是否共享某个特征(例如电子邮件地址),我们将该特征建模为一个顶点,而不是作为顶点的属性。

图 11-2. 视频客户账户的图模式(在oreil.ly/gpam1102上查看更大版本的此图)
表 11-1 简要解释了图模型中每个顶点类型。虽然起始套件的数据包含大量有关视频的数据,但我们在本次练习中不会专注于视频本身。我们将专注于账户的实体解析。
表 11-1. 图模型中的顶点类型
| 顶点类型 | 描述 |
|---|---|
账户 |
一个 SVoD 用户的账户,一个数字身份 |
用户 |
一个真实世界的人。一个用户可以链接到多个账户 |
IP,电子邮件,姓,电话,地址,设备 |
账户的关键属性,表示为顶点以便链接共享共同属性的账户/用户 |
视频 |
由 SVoD 提供的视频标题 |
关键词,类型 |
视频的属性 |
Video_Play_Event |
特定Account观看特定Video的时间和持续时间。 |
Weight |
相似性模型参数 |
查询和分析
对于我们的实体解析用例,我们有一个需要三个或更多查询的三阶段计划:
-
初始化:对于每个
Account顶点,创建一个User顶点并将它们链接起来。Accounts是在线身份,Users代表现实世界的人物。我们从每个Account都是一个真实人物的假设开始。 -
相似性检测:应用一个或多个相似性算法来衡量
User顶点之间的相似性。如果我们认为一对足够相似,那么我们会创建一个连接它们的链接,使用图 11-2 中显示的SameAs边类型。 -
合并:查找链接的
User顶点的连通分量。选择其中一个作为主要顶点。将其他社区成员的所有边转移到主要顶点。删除其他社区顶点。
出于我们在讨论合并时将解释的原因,您可能需要重复步骤 2 和 3,直到相似性检测步骤不再创建任何新的连接。
我们将为我们的用例展示两种不同的实体解析方法。第一种方法使用 Jaccard 相似度(详见第六章)来计算相邻顶点的精确匹配,并将每个相邻顶点视为同等重要。合并将使用简单的连通分量算法。第二种方法更为高级,建议一种处理属性值的精确和近似匹配的方法,并包括权重以调整关系的相对重要性。近似匹配是处理小错误或使用缩写名称的一种良好方法。
方法 1:Jaccard 相似度
对于每个阶段,我们将提供一个高层次解释,操作 TigerGraph 的 GraphStudio 指南,期望的结果描述以及对查询中某些 GSQL 代码的更详细查看。
初始化
在我们的模型中,Account是数字身份,User是真实人物。原始数据库仅包含Accounts。初始化步骤创建一个与每个Account关联的唯一临时User。对于从Account到属性顶点(Email、Phone等)的每条边,我们创建一个从User到相同属性顶点集的对应边。图 11-3 显示了一个示例。左侧的三个顶点及连接它们的两条边属于原始数据。初始化步骤创建了User顶点和三条虚线边。因此,每个User从其Account开始具有相同的属性邻域。

图 11-3. 在初始化步骤中创建的用户顶点和边
执行 GSQL 查询initialize_users。
此查询没有输入参数,因此将立即运行,无需用户额外步骤。以下代码块显示了initialize_users的前 20 行。开头的注释列出了包括的六种属性顶点类型:
CREATE QUERY initialize_users() FOR GRAPH Entity_Resolution SYNTAX v2 {
// Create a User vertex for each Account, plus edges to connect attributes
// (IP, Email, Device, Phone, Last_Name, Address) of the Account to the User
// Initialize each account with a user
Accounts = SELECT s FROM Account:s
WHERE s.outdegree("Has_Account")==0
ACCUM
INSERT INTO User VALUES(s.id),
INSERT INTO Has_Account VALUES(s.id, s);
// Connect the User to all the attributes of their account
IPs = SELECT attr FROM Accounts:s -(Has_IP:e)- IP:attr
ACCUM
INSERT INTO User_IP VALUES(s.id, attr);
Emails = SELECT attr FROM Accounts:s -(Has_Email:e)- Email:attr
ACCUM
INSERT INTO User_Email VALUES(s.id, attr);
// Remaining code omitted for brevity
}
在第一个SELECT块中,对于每个尚未具有相邻User的Account,我们使用INSERT语句创建一个User顶点,并创建一个Has_Account边连接此User到Account。别名s表示一个Account;我们为新的User赋予与其对应的Account相同的 ID:s.id。
下一个块处理IP属性顶点:如果从Account到IP顶点存在一个Has_IP边,则插入一条从相应User顶点到同一IP顶点的边。本节的最后一个块类似地处理Email属性顶点。出于简洁起见,剩余四种属性类型(Device、Phone、Last_Name和Address)的代码已被省略。
相似度检测
Jaccard 相似度计算两个实体共有多少属性,除以它们之间的总属性数。每个属性比较都有是/否答案;失误也不可小觑。图 11-4 展示了一个例子,其中用户 A 和用户 B 各自有三个属性;其中两个匹配(Email 65 和 Device 87)。因此,A 和 B 共有两个属性。

图 11-4. Jaccard 相似度示例
他们总共有四个不同的属性(Email 65、Device 87、Phone 23 和 Phone 99);因此,Jaccard 相似度为 2/4 = 0.5。
执行带有默认参数值的connect_jaccard_sim查询。
此查询为每对顶点计算此相似度分数。如果分数达到或超过给定阈值,则创建一个Same_As边连接这两个User。默认阈值为 0.5,但您可以调高或调低。Jaccard 分数的范围从 0 到 1。图 11-5 展示了使用 Jaccard 相似度和 0.5 阈值连接User顶点 1、2、3、4 和 5 的情况。对于这五个顶点,我们发现的社区大小从一个单独的顶点(User 3)到三个顶点(Users 1 和 2)不等。

图 11-5. 使用 Jaccard 相似度和阈值 0.5 连接User顶点 1、2、3、4 和 5(详见此图的大版本:oreil.ly/gpam1105)

图 11-6. 在探索图页面上选择顶点

图 11-7. 从探索图页面上的顶点扩展
我们将会讨论connect_jaccard_sim的几部分 GSQL 代码,以解释它的工作原理。在下面的代码片段中,我们统计了每对Users之间共同属性的数量,使用单个SELECT语句。该语句使用模式匹配描述了如何连接两个这样的Users,然后使用累加器来计算出现的次数:
Others = SELECT B FROM
Start:A -()- (IP|Email|Phone|Last_Name|Address|Device):n -()- User:B
WHERE B != A
ACCUM
A.@intersection += (B -> 1), // tally each path A->B,
@@path_count += 1; // count the total number of paths
注意
GSQL 的FROM子句描述了一个从顶点到顶点经由边的从左到右路径。符合要求的每个顶点和边序列形成结果临时“表”的一行,传递给ACCUM和POST-ACCUM子句进行进一步处理。
这个FROM子句呈现了一个两跳图路径模式来搜索:
FROM User:A -()- (IP|Email|Phone|Last_Name|Address|Device):n
-()- User:B
子句的组成部分如下:
-
User:A表示从一个User顶点开始,别名为A。 -
-()-表示通过任何边类型。 -
(IP|Email|Phone|Last_Name|Address|Device):n表示到达这六种顶点类型之一,别名为n。 -
-()-表示通过另一种类型的边。 -
User:B表示到一个User顶点,别名为B。
WHERE B != A确保我们跳过 A = B 的情况。下一行宣布了ACCUM子句的开始。在ACCUM子句内部,(``A.@intersection += (B -> 1), // tally each path A->B``)是 GSQL 支持并行处理和聚合的一个很好的例子:对于从 A 到 B 的每条路径,附加一个(键→值)记录附加到 A。记录是(B,+=1)。也就是说,如果这是将 B 与 A 关联的第一条记录,则将值设置为 1。对于 B 是 A 目标的每个额外记录,则将值增加 1。因此,我们正在计算从 A 到 B 的连接次数,通过六种指定的边类型之一。这一行以逗号结尾,所以下一行@@path_count += 1仍然是ACCUM子句的一部分。为了记账目的,@@path_count`计算我们找到了多少这样的路径。
让我们看看另一个代码块——Jaccard 相似度的最终计算,并在Users之间创建连接:
Result = SELECT A FROM User:A
ACCUM FOREACH (B, overlap) IN A.@intersection DO
FLOAT score = overlap*1.0/(@@deg.get(A) + @@deg.get(B) - overlap),
IF score > threshold THEN
INSERT INTO EDGE SameAs VALUES (A, B, score), // FOR Entity Res
@@insert_count += 1,
IF score != 1 THEN
@@jaccard_heap += SimilarityTuple(A,B,score)
END
END
END;
这个SELECT块执行以下操作:
-
对于每个
UserA,迭代其类似UsersB 的记录集合,以及 B 的共同邻居数,别名为overlap。 -
对于每对(A,B),使用
overlap以及 A 和 B 的合格邻居数((``@@deg.get(A)和`@@deg.get(B)``))计算 Jaccard 分数,这些先前计算过。 -
如果分数大于阈值,在 A 和 B 之间插入一个
SameAs边。 -
@@insert_count和@@jaccard_heap用于报告统计数据。
合并
在我们的第三个和最后一个阶段,我们合并了在前一步中创建的User顶点的连接社区。对于每个社区,我们将选择一个顶点作为生存者或领导者。剩余成员将被删除;从一个Account到非领导者的所有边将被重定向指向领导User。
运行merge_connected_users查询。阈值参数的值应始终与connect_jaccard_sim使用的值相同。
查看 JSON 输出。注意它是否说converged = TRUE或FALSE。图 11-8 显示了帐户 1、2、3、4 和 5 的用户社区。每个用户社区已经减少到一个单一的User(真实人物)。每个这些Users链接到一个或多个Accounts(数字身份)。我们已经实现了实体解析。

图 11-8。使用 Jaccard 相似度实现的实体解析(在oreil.ly/gpam1108可以查看此图的大图版本)
merge_connected_users算法有三个阶段:
-
在每个组件中,选择一个主要的
User。 -
在每个组件中,将其他
Users的属性连接重定向到主要的User。 -
删除不是主要
User的Users和所有Same_As边。
让我们更仔细地看一下 GSQL 代码。对于每组由Same_As边连接的相似Users,我们将选择具有最小 ID 值的那个作为主要的User。我们使用名为@min_user_id的MinAccum来比较顶点的内部 ID 值。每当您向MinAccum输入新值时,它保留其当前值和新输入值的较小值。我们首先将每个顶点的@min_user_id初始化为其自身:
Updated_users = SELECT s FROM Users:s
POST-ACCUM s.@min_user_id = s;
然后,我们对以下过程进行迭代:对于每对连接的Users s → t(FROM子句),顶点t更新@min_user_id,以较小的两个顶点内部 ID 为准(ACCUM子句):
WHILE (Updated_users.size() > 0) DO
Updated_users = SELECT t
FROM Updated_users:s -(SameAs:e)- User:t
// Propagate the internal IDs from source to target vertex
ACCUM t.@min_user_id += s.@min_user_id // t gets the lesser of t & s ids
HAVING t.@min_user_id != t.@min_user_id' // tick' means accum's previous val
;
iteration = iteration + 1;
END;
HAVING子句是一个筛选器,用于决定是否将特定的t包括在Updated_users输出集中。注意行末的撇号(');这是对累加器t.@min_user_id的修饰符。它表示“在执行ACCUM子句之前的累加器值”。对于像这样的循环过程,此语法允许我们将先前的值与当前值进行比较。如果t.@min_user_id的值与其先前的值相同,则t不包括在Updated_users中。当没有顶点更改其@min_user_id时,我们可以退出WHILE循环。
看起来通过三个步骤——初始化、连接相似实体和合并连接实体——应该足够了。然而,合并可能会创建一种情况,其中产生新的相似性。请查看图 11-9,该图描述了在将用户 2 和用户 602 合并为用户 302 后用户 302 的属性连接。账户 2、302 和 602 保持分开,因此您可以看到每个账户如何贡献一些属性。因为用户 302 拥有比以前更多的属性,所以现在可能与一些其他用户(可能是新合并的用户)更相似。因此,我们应该运行另一轮相似性连接和合并。重复这些步骤,直到不再出现新的相似性。

图 11-9. 实体解析后具有更多属性的用户(请查看此图的大尺寸版本,网址为 oreil.ly/gpam1109)
作为提醒,这是使用 Jaccard 相似度进行简单实体解析的查询顺序:
-
运行
initialize_users. -
运行
connect_jaccard_sim. -
运行
merge_connected_users. -
重复步骤 2 和 3,直到
merge_connected_users的输出显示converged = TRUE。
重置
完成后,或随时,您可能希望将数据库恢复到其原始状态。如果要重新运行实体解析过程,您需要这样做。查询 util_delete_users 将删除所有User顶点及其所有相关边。请注意,您需要将输入参数 are_you_sure 从 FALSE 更改为 TRUE。这是为了安全起见而进行的手动操作。
警告
删除批量顶点(util_delete_users)或创建批量顶点(initialize_users)可能需要几秒钟才能生效,即使查询显示已完成。请转到“加载数据”页面,检查User顶点和User相关边的实时统计信息,以查看创建或删除是否已完成。
方法 2: 计分精确和近似匹配
前一节展示了一个基于图的实体解析技术,简单易行,但对于实际应用来说过于基础。它依赖于属性值的精确匹配,而我们需要允许几乎相同的值,这些值可能来自无意和有意的拼写变化。我们也希望某些属性比其他属性更重要。例如,如果你有出生日期信息,你可能会对这个属性的精确匹配要求严格。尽管个人可以移动并拥有多个电话号码和电子邮件地址,但他们只能有一个出生日期。在这一节中,我们将引入权重来调整不同属性的相对重要性。我们还将提供一种字符串值的近似匹配技术。
注意
如果您已经使用起始工具包运行了方法 1,请务必进行重置。 (参见方法 1 结尾的 “重置” 部分。)
初始化
我们仍然使用相同的图模型,其中User顶点代表真实人物,Account顶点代表数字账户。因此,我们仍然使用initialize_users查询来设置一组初始User顶点。
我们将查询util_set_weights添加为另一个初始化步骤。此查询接受六个属性(IP、Email、Phone、Address、Last_Name和Device)的权重并将其存储。如果这是关系数据库,我们将把这些权重存储在表中。由于这是一个图形数据库,我们将把它们存储在一个顶点中。我们只需要一个顶点,因为一个顶点可以有多个属性。然而,我们要更加精致。我们将使用映射类型属性,其中将有六个键 → 值条目。这使我们可以像查找表一样使用映射:告诉我键的名称(属性名称),我会告诉你值(权重)。
如果我们有一些地面真相训练数据(例如,知道哪些帐户真正属于同一个用户),我们可以使用机器学习来学习哪些属性权重值在预测两个帐户是否属于同一真实人物方面表现良好。由于我们没有任何训练数据,设置最佳权重的工作留给用户的经验和判断。
执行:运行initialize_users。检查 Load Data 页面上的图统计信息,确保所有 901 个User顶点和相关边已经创建。运行util_set_weights。这个查询的输入参数是六个属性的权重。默认包含了权重,但如果您愿意,可以进行更改。如果想要查看结果,运行util_print_vertices。
计分加权精确匹配
我们将在两个阶段进行相似性比较和链接。在第一阶段中,我们仍然检查精确匹配,因为精确匹配比近似匹配更有价值;然而,这些连接将被加权。在第二阶段中,我们将检查我们具有字母值的两个属性Last_Name和Address的近似匹配。
在加权精确匹配中,我们在Users之间创建加权连接,其中更高的权重表示更强的相似性。连接的净权重是每对共享相同Users的属性贡献之和。图 11-10 展示了加权匹配计算。在初始化阶段早期,您已经为每个感兴趣的属性建立了权重。在图中,我们使用wt_email和wt_phone来表示与匹配Email和Phone属性相关的权重。

图 11-10. 加权匹配的两阶段计算
加权匹配计算分为两步。在第 1 步中,我们寻找从Users到Attributes的连接,并在每个User的连接上记录权重。User A 和 User B 都连接到 Email 65,因此 Email 65 记录了A:wt_email和B:wt_email。每个User的权重需要分别记录。Phone 99 也连接到 Users A 和 B,因此它记录了类似的信息。
在步骤 2 中,我们寻找相同的连接,但是在另一个方向上,以Users作为目的地。 Email 65 和 Phone 99 都与 User A 相连接。 User A 从步骤 1 中汇总了他们的记录。注意,其中一些记录是指向 User A 的。User A 忽略了这些记录,因为它对与自身的连接不感兴趣!在这个例子中,最终记录了B:(wt_email + wt_phone)。我们使用这个值在 Users A 和 B 之间创建一个加权的Same_As边缘。你可以看到 User B 对于 User A 有着等效的信息。
执行connect_weighted_match查询。
图 11-11 展示了connect_weighted_match生成的一个社区。这个特定社区包含了用户/帐户 5。图中还显示了与两个属性Address和Last_Name的连接。其他属性如Email在评分中使用但未显示,以避免混乱。

图 11-11。精确加权匹配后包含帐户 5 的用户社区(在 oreil.ly/gpam1111 上查看此图的更大版本)
Same_As边缘的厚度显示了连接的强度。屏幕底部 Users 505 和 805 之间的连接最为强大。事实上,我们可以看到七个成员中最大社区中的三个用户子社区:
-
顶部的 Users 5、105 和 205。Users 5 和 105 之间的连接略微更强,原因未显示。他们三人有相同的姓氏。他们有类似的地址。
-
在中间的 Users 305 和 405。他们的姓和地址不同,因此某些未显示的属性必定是他们相似性的原因。
-
底部的 Users 505 和 805。他们共享相同的姓和地址,以及其他属性。
评分近似匹配
我们可以在图 11-11 中看到一些Users具有类似的姓名(Ellsworth 与 Ellesworth)和相似的地址(Eagle Creek Center 与 Eagle Crest Ctr)。一个只查找完全匹配的评分系统对这些近似情况不予考虑。实体解析系统理想情况下能够评估两个文本字符串的相似度并为其分配一个分数。它们是否仅有一个字母不同,例如 Ellsworth 和 Ellesworth?是否有字母位置颠倒,例如 Center 和 Cneter?计算机科学家喜欢考虑两个文本字符串之间的编辑距离:需要多少单字母值或位置变换来将字符串 X 转换为字符串 Y?
我们将使用 Jaro-Winkler(JW)相似度⁴来衡量两个字符串之间的相似度,这是 Jaro 相似度的改进版本。给定两个字符串s1 和s2,它们具有m个匹配字符和t个转换步骤,它们的 Jaro 相似度定义为:
.
如果字符串完全相同,则m = |s1| = |s2|,而t = 0,因此方程简化为(1 + 1 + 1)/3 = 1. 另一方面,如果没有共同的字母,则分数为 0. JW 相似度从 Jaro 相似度出发,并在每个字符串的开头完全匹配时额外奖励。
两个属性值的净相似度分数是它们的 JW 相似度乘以属性类型的权重。例如,如果属性的权重为 0.5,并且 JW 相似度分数为 0.9,则净分数为 0.5 × 0.9 = 0.45。
执行score_similar_attributes查询。
score_similar_attributes查询考虑已经通过Same_As边缘连接的User对。它计算了Last_Name和Address属性的加权 JW 相似性,并将这些分数添加到现有的相似性分数中。我们选择了Last_Name和Address,因为它们是字母顺序而不是数字顺序。这是一个应用决策而不是技术决策。图 11-12 显示了在添加近似匹配分数后的结果。

图 11-12. 包括帐户 5 在精确和近似加权匹配后的用户社区(在oreil.ly/gpam1112可以查看这个图的更大版本)
比较图 11-11 和图 11-12,我们注意到以下变化:
-
Users 1、105 和 205 之间的联系因为他们有相似的地址而加强了。
-
用户 305 因为姓氏相似而与上述三人更紧密地联系在一起。
-
305 和 405 之间的联系由于他们有相似的地址而加强了。
-
用户 405 由于名字 Hunter 与 Brunke 有一些字母相同,因此与用户 505 和 805 更紧密地连接在一起。这个影响可能被认为是 JW 相似性度量不像人工评估者那样慎重的副作用。
比较两个字符串是一个通用的函数,不需要图遍历,因此我们将其实现为 GSQL 中的简单字符串函数。由于它还不是 GSQL 语言的内置功能,我们利用了 GSQL 接受用户提供的 C++函数作为用户定义函数(UDF)的能力。本启动套件包含了jaroDistance(s1, s2)和jaroWinklerDistance(s1, s2)的 UDF。您可以在 GSQL 查询中的任何地方调用它们,就像调用内置字符串函数一样。当然,任何其他字符串比较函数都可以在此处实现,以取代 JW。
以下代码片段显示了我们如何对Address特征执行近似匹配和评分:
connected_users = SELECT A
// Find all linked users, plus each user's address
FROM Connected_users:A -(SameAs:e)- User:B,
User:A -()- Address:A_addr,
User:B -()- Address:B_addr
WHERE A.id < B.id // filter so we don't count both (A,B) & (B,A)
ACCUM @@addr_match += 1,
// If addresses aren't identical compute JaroWinkler * weight
IF do_address AND A_addr.val != B_addr.val THEN
FLOAT sim = jaroWinklerDistance(A_addr.id,B_addr.id) * addr_wt,
@@sim_score += (A -> (B -> sim)),
@@string_pairs += String_pair(A_addr.id, B_addr.id),
IF sim != 0 THEN @@addr_update += 1 END
END
第一个FROM子句中的行是合取路径模式的示例,即由多个单独模式组成的复合模式,用逗号分隔。逗号的作用类似于布尔 AND。这个合取模式意味着“找到与用户 A 连接的用户 B,找到与 A 连接的Address,并找到与 B 连接的Address”。接下来的WHERE子句过滤了 A = B 的情况,并防止对一对(A,B)进行两次处理。
IF 语句过滤掉 A 和 B 不同但地址相同的情况。如果它们的地址相同,那么在运行 connect_weighted_match 时我们已经给予了它们完全的信用。然后我们使用 jaroWinklerDistance 函数和 Address 的权重计算加权得分,将分数存储在一个 FLOAT 变量 sim 中,该变量暂时存储在查找表中。IF 语句中的最后两行仅用于记录我们的活动,以便在最后输出时提供信息。
合并相似实体。
在方法 1 中,我们有一个简单的方案来决定是否合并两个实体:如果它们的 Jaccard 分数大于某个阈值,则创建一个 Same_As 边缘。决定合并所有具有 Same_As 边缘的内容。现在我们希望采用更细致的方法。我们的评分具有可调整的权重,并且 Same_As 边缘记录了我们的评分。我们可以使用另一个阈值分数来决定要合并哪些 Users。
我们只需要对 merge_connected_users 进行两个小改动,以让用户设置一个阈值:
-
将
merge_connected_users的一个副本保存为名为merge_similar_users的新查询。 -
在查询标题中添加一个阈值参数:
CREATE QUERY merge_similar_users(FLOAT threshold=1.0, BOOL verbose=FALSE) -
在查找连接的
Users的SELECT块中,添加一个WHERE子句来检查Same_As边缘的相似性值:WHILE (Updated_users.size() > 0) DO IF verbose THEN PRINT iteration, Updated_users.size(); END; Updated_users = SELECT t FROM Updated_users:s -(SameAs:e)- User:t WHERE e.similarity > threshold // Propagate the internal IDs from source to target vertex ACCUM t.@min_user_id += s.@min_user_id // t gets the lesser of t & s ids HAVING t.@min_user_id != t.@min_user_id' // accum' is accum's previous val ; iteration = iteration + 1; END;
运行 merge_similar_users。选择一个阈值,并查看您是否得到了预期的结果。
对于在 图 11-12 中显示的社区,图 11-13 展示了三种不同的合并结果,阈值分别为 1.0、2.5 和 3.0。

图 11-13. 使用不同阈值级别进行实体解析(请查看该图的更大版本:oreil.ly/gpam1113)。
这就是我们第二种更细致的实体解析方法的总结。
回顾一下,这里是使用加权精确和近似匹配进行实体解析时运行的查询序列:
-
运行
initialize_users。 -
运行
util_set_weights。 -
运行
connect_weighed_match。 -
运行
score_similar_attributes。 -
运行
merge_similar_users。 -
重复步骤 3、4 和 5,直到
merge_similar_users的输出显示converged = TRUE。
章节总结
在本章中,我们看到了如何使用图算法和其他图技术进行复杂的实体解析。相似性算法和连接组件算法起着关键作用。我们考虑了几种评估两个实体相似性的方案:Jaccard 相似性、精确匹配的加权和以及用于比较文本字符串的 Jaro-Winkler 相似性。
这些方法如果有训练数据,可以轻松扩展到监督学习。有许多模型参数可以学习,以提高实体解析的准确性:每个属性的得分权重用于精确匹配,调整近似匹配的评分以及合并相似 Users 的阈值。
在 GSQL 查询中,我们看到了 FROM 子句通过表示路径或模式来选择图中的数据。我们还看到了 ACCUM 子句的示例以及累加器被用来计算和存储信息,例如顶点之间的共同邻居、一个计数、累积分数,甚至是逐步演变的 ID 值,标记顶点作为特定社区的成员。
本章向我们展示了基于图的机器学习如何提高企业看清数据背后的真相的能力。在下一章中,我们将把图机器学习应用到其中一个最受欢迎和重要的用例之一:欺诈检测。
¹ “2030 年视频流媒体市场规模预计达到 4168.4 亿美元”,Grand View Research,2023 年 3 月,https://www.grandviewresearch.com/press-release/global-video-streaming-market。
² “视频流媒体(SVoD)- 全球”,Statista,2023 年 5 月 26 日访问,https://www.statista.com/outlook/dmo/digital-media/video-on-demand/video-streaming-svod/worldwide。
³ “2030 年视频流媒体市场规模预计达到 4168.4 亿美元”,Grand View Research。
⁴ 还存在更高级的相似性算法,能够融合字符串之间的语义相似性以及编辑距离。在本例中,为了速度和简单说明,我们使用了一个相对简单的算法。
第十二章:改进诈骗检测
在早期的一章中,我们通过设计图查询来处理诈骗检测问题,这些查询寻找某些行为模式,这些模式可能是可疑的。本章将应用机器学习方法来改进诈骗检测。机器学习可以通过异常检测或者通过训练软件识别基于已知诈骗案例的方法来帮助我们。无论哪种情况,图结构化数据都是感知异常(异常)或提供数据特征(用于构建预测模型)的宝贵资产。没有绝对完美的方法,但机器学习通常可以检测到人类会忽略的模式和异常。传统方法只遵循专家规定的规则。通过在图上应用机器学习,我们可以检测到数据中未明确标记为诈骗案例的模式,这使其更适应于变化中的诈骗策略。
完成本章后,您应能够:
-
部署并使用 TigerGraph 机器学习工作台
-
使用基于图的特征来丰富数据集的特征向量,然后将带有图特征和不带图特征的模型精度进行比较。
-
为节点预测准备数据并训练图神经网络——在本例中是诈骗预测
目标:改进诈骗检测
诈骗是为了个人利益而使用欺骗手段。诈骗者可能会破坏系统及其用户,但最终目的是为了个人利益。欺诈活动的例子包括身份盗窃、虚假或夸大的保险索赔以及洗钱。诈骗检测是一组活动,旨在防止诈骗者成功地进行此类活动。在许多情况下,诈骗者希望从他们的努力中获取金钱。因此,诈骗检测是金融机构中常见的实践,但在持有有价值资产和财产的组织中,如保险、医疗、政府和主要零售组织中也很普遍。
诈骗是一个重大的商业风险,越来越难以应对。根据 LexisNexis 的一项研究,对于美国电子商务和零售部门的公司,每 1 美元的诈骗成本为公司带来 3.75 美元的损失,这是自 2019 年以来增加了 19.8%。[¹] 这些诈骗成本来自于因身份欺诈而产生的欺诈交易,其中包括对被盗身份或个人信息的误用。由于欺诈者可以操作的渠道越来越多,诈骗检测变得更加具有挑战性。例如,一个令人担忧的趋势是,通过移动智能手机进行的诈骗成本激增。在 COVID-19 大流行期间,消费者被推动进行更多的数字交易。许多这类交易依赖于智能手机,这为诈骗者提供了欺骗人们的新途径。
加密货币是欺诈者流行的交易媒介。与由政府或央行发行和监管的货币不同,加密货币是在账户持有人之间传递的数字资产,通常使用开放和分布式账本。这项技术让每个人都能参与交易,而无需通过中央机构进行身份识别,使洗钱、诈骗和盗窃更具吸引力。2021 年,犯罪分子窃取了价值 140 亿美元的加密货币,与 2020 年相比,与加密货币有关的犯罪案件增加了 79%。²
解决方案:利用关系建立更智能的模型
如果能够收集更多有关参与方和活动的事实,并将它们联系在一起,欺诈行为就能被发现。例如,假设我们发现异常的交易行为,比如在短时间内在账户之间转移大量资金。统计上,这种行为的一定比例是由于欺诈。然而,如果这些账户与中央机构制裁的实体有关联,那么欺诈案的可能性就变得更大。换句话说,在孤立使用交易数据时,我们只能看到案件的有限方面,但当我们将这些数据连接到另一个识别受制裁实体的数据集时,就可以考虑参与方与受制裁实体之间的路径长度。利用不同数据集之间的关系比各部分之和更重要。
图是发现这些关系和模式的绝佳方式。在早前的章节中,我们看到了如何使用 GSQL 查询来检测感兴趣的特定模式。然而,依赖调查员事先了解这些模式是有限的。一个更强大的方法是利用机器学习确定哪些模式表明欺诈。
大多数机器学习方法都分析向量或矩阵。每个向量是一个数字特征或特征列表,表示某种类型实体(如个人)。机器学习方法寻找这些特征之间的模式。数据科学家为系统提供了一组实际欺诈(和非欺诈)案例的代表样本,供机器学习系统分析。机器学习系统的任务是提取一个模型,表明:“当你有这些特征数值时,很可能发生欺诈。”
这种方法在打击欺诈行为方面是一个强大的工具,但并非完美。一个局限性是模型只能像提供的训练数据那样好。如果我们的特征向量只描述实体的直接特征,那么我们就会错过可能有价值的更深层次的基于图的关系。通过结合可通过图形分析获得的更深入见解,我们可以丰富输入或训练数据,从而产生更准确的机器学习模型。
在下面的实例中,我们将使用 TigerGraph 机器学习 Workbench 来帮助我们自动提取图特征,以丰富训练数据,并运行图神经网络(GNN)。
使用 TigerGraph 机器学习 Workbench
对于此集中练习,重点是机器学习,我们将使用 TigerGraph 机器学习 Workbench,简称 ML Workbench。基于开源的面向 Python 数据科学家的 JupyterLab IDE,并包含 TigerGraph 的 Python 库 pyTigerGraph,ML Workbench 可以简化开发包含图数据的机器学习流程。
设置 ML Workbench
首先,我们将在 TigerGraph 云服务上获取 ML Workbench 的一个实例,然后将其连接到数据库实例。
创建 TigerGraph 云 ML Bundle
设置 ML Workbench 的最简单方法是部署 TigerGraph 云 ML Bundle,它将 ML Workbench 添加为可用于 TigerGraph 云数据库实例的工具之一:
-
使用 ML Bundle 需要支付少量费用,因此您需要在您的帐户上设置付款信息。
-
在 TigerGraph 云帐户的集群屏幕上,单击创建集群按钮。
-
在创建集群页面的顶部,选择右侧的 ML Bundle 选项。
-
选择一个实例大小。此练习中可用的最小大小即可。
-
我们将使用内置于 ML Workbench 中的数据集和查询,因此您在此处选择的用例并不重要。完成设置其他选项,然后在页面底部单击创建集群。
集群需要几分钟来配置。
创建并复制数据库凭据
ML Workbench 包括一系列强大的示例 Jupyter 笔记本,使用 pyTigerGraph 下载数据集并在您的集群中创建图表。但在此之前,它必须首先使用您提供的凭据访问 TigerGraph 数据库:
-
您应该仍然在 TigerGraph 云的集群页面上。对于您刚创建的集群,请单击访问管理。
-
单击数据库访问选项卡,然后单击添加数据库用户。
-
输入用户名和密码。请务必记住这两者,因为稍后在 ML Workbench 中会用到它们。
-
转到数据库访问旁边的角色管理选项卡。
-
选择您的新用户旁边的复选框,将角色设置为全局设计师,然后单击保存。
-
转到详细信息选项卡。复制域名,以 i.tgcloud.io 结尾。
将 ML Workbench 连接到您的图数据库
-
转到此数据库实例的 GraphStudio。
-
在右上角,单击工具菜单图标(一个 3 × 3 网格图标),然后选择 ML Workbench。
-
在工作台打开后,在左侧面板中找到 config.json 并双击进行编辑。
-
用您复制的域名值替换
host的 URL 值。结果值仍应以 https:// 开头,并以 i.tgcloud.io 结尾。 -
更改
username和password的值为您创建的新用户的用户名和密码。
虽然这个过程涉及几个步骤,但一旦您习惯了 TigerGraph Cloud 和 ML Workbench 的界面,将会变得很容易通过一个新的数据库用户为 ML Workbench 授予访问您的集群的权限。
使用 ML Workbench 和 Jupyter Notes
双击 ML Workbench 左侧面板的 README.md,如 图 12-1 所示,以获取有关 pyTigerGraph 和 ML Workbench 组件的一般结构和功能的概述。

图 12-1. ML Workbench 和 README 文件
您刚刚完成了设置部分。滚动到学习部分。在这里,您将看到教程和示例笔记本的列表,用于入门、图算法、GNN 和端到端应用程序。
此部分剩余内容介绍 Datasets.ipynb 笔记本,供不熟悉 Jupyter 的用户参考。如果您熟悉 Jupyter,您仍应快速浏览以验证数据库连接是否正常。
打开 Datasets.ipynb 笔记本,位于 Basics 文件夹中。这个文件是一个 Jupyter 笔记本,结合了 Python 代码片段和解释性评论。Python 代码块被编号为 [1]、[2] 等等。左侧的厚蓝条突出显示要执行的下一部分。单击顶部命令菜单上的右箭头将执行下一个代码块:
-
点击箭头直到块 [1] 下载数据集 开始运行。
当它正在运行时,方括号中的数字将变成一个星号 (*)。当完成时,星号将再次变成数字。注意任何输出中的信息或错误消息。
注
运行 ML Workbench 笔记本时,请确保您的数据库处于活动状态(非暂停状态)。如果它被暂停了,那么当您尝试运行一个代码块时,方括号将包含一个空格 [ ] 而不是一个星号 [*]。
-
运行接下来的三个 Python 代码块:创建连接、导入数据和可视化模式。
如果您在创建连接时遇到问题,则可能没有正确设置 config.json 文件。导入数据步骤将需要几秒钟的时间。最后一步应该通过显示一个简单模式的图像来结束,其中包括 Paper 顶点和 Cite 边。回顾代码块,我们看到我们使用了两个 pyTigerGraph 库(datasets 和 visualization)以及一些类和方法:TigerGraphConnection.ingestDataset 和 visualization.drawSchema。
笔记本的其余部分导入另一个数据集,IMDB。这两个数据集被一些其他笔记本使用。
绘制模式和数据集
对于我们的图机器学习示例,现在我们将转向 applications 文件夹内的 fraud_detection 笔记本。这里使用的数据是以太坊平台上的交易数据;以太币是按市值计算第二大的加密货币。这些交易形成一个图形,其中顶点是平台上的钱包(即账户),边是账户之间的交易。数据集包含 32,168 个顶点和 84,088 条从发送账户到接收账户的有向边。数据集源自梁晨等人在“以太坊交易网络中的网络钓鱼骗局检测”[³] 中的研究,可从 XBlock[⁴] 获取。有向边告诉我们资金如何流动,这对于任何金融分析(包括欺诈检测)都非常重要。
每个账户顶点都有一个 is_fraud 参数。数据集中有 1,165 个被标记为欺诈的账户。这些账户被报告为参与网络钓鱼骗局的账户,这是加密货币社区中最常见的欺诈形式之一。
在加密经济中,典型的网络钓鱼骗局是指攻击者建立一个网站,承诺以小额投资获得巨大回报,通常宣称受害者早早参与了一个会带来巨大收益的计划。然而,这些承诺从未兑现,最初的投资永远丧失。
由于这些骗子在短时间内接受许多小额交易,然后将资金以较大笔移至其他账户,他们的交易活动通常与典型合法加密货币用户的活动模式不匹配。数据集中的顶点具有七个参数,详细说明见表 12-1,对应陈等人描述的特征。
重要的是,加载数据集时,这些图形特征实际上并不包含在内。数据集只包含顶点(账户)的 ID 和 is_fraud 标志,以及边(交易)上的金额和时间戳。在教程演示过程中,我们使用给定的信息生成图形特征。
表 12-1. 以太坊交易数据集的基于图形的特征
| 特征 | 描述 |
|---|---|
| FT1 | 入度,或账户顶点的入向交易数 |
| FT2 | 出度,或账户顶点的外向交易数 |
| FT3 | 度数,或涉及账户的总交易数 |
| FT4 | 入度,即所有入向交易的总金额 |
| FT5 | 出度,即所有外向交易的总金额 |
| FT6 | 强度,或涉及账户的所有交易的总金额 |
| FT7 | 邻居数量 |
| FT8 | 反向交易频率:账户第一笔和最后一笔交易之间的时间间隔除以 FT3 |
欺诈账户往往具有特征 4、5 和 6 的较高值,而特征 8 的较小值。网络钓鱼攻击者通过许多较小的交易总体上窃取了大量资金。
FT7(邻居数量)不同于 FT3(交易数量),因为一个邻居可能负责多个交易。当我们将数据加载到 TigerGraph 时,我们将一对账户之间的所有交易合并为单个边缘,因此我们不使用 FT7。尽管如此简化,我们仍然取得了良好的结果,很快将进行演示。在类似数据集中的其他特征可能会进一步改善性能。虽然银行用于欺诈检测的确切图特征既依赖于数据又是商业机密,但普遍认为像 PageRank 这样的中心性算法和像 Louvain 这样的社区检测算法通常是有帮助的。
虽然这些指标提供了对网络钓鱼攻击行为的快速直观查看,但传统的机器学习方法和 GNN 能够找出所有特征之间更精确的关系,以区分用于网络钓鱼和合法用途的账户。在本章中,我们将比较这两种方法的方法和结果。
检查fraud_detection笔记本中的第一个代码块,确保它具有您在config.json中设置的相同连接和凭据信息。运行fraud_detection笔记本的数据库准备步骤,以创建图模式并加载数据。
图特征工程
您现在应该在标题为“图特征工程”的部分。正如笔记本上所说,我们使用一个名为 pyTigerGraph 的featurizer对象来生成特征:两个来自内置算法(PageRank 和介数中心性)的特征,以及两个来自我们自己的 GSQL 查询。Featurizer 提供了一个高级简化的过程,用于生成和存储基于图的特征。GDS 库中的算法对 Texturizer 自动可用;用户只需指定一些参数。
我们称我们的对象为f。运行代码块 4 以创建它:
[4] : f = conn.gds.featurizer()
在 PageRank 部分,我们使用预安装的 Featurizer 算法集中包含的tg_pagerank算法。PageRank 衡量了图中顶点的影响力。如果一个顶点被许多其他顶点指向,而这些顶点本身又被许多顶点指向,它将获得高的 PageRank 分数。每种算法都使用一组输入参数。您可以查看TigerGraph GDS Library的文档,了解特定算法的参数。在 PageRank 代码块中,我们指定一个 Python 字典,包含要传递给 PageRank 的参数及其值。由于此图架构非常简单,顶点和边类型的选择由系统决定。我们将排名值存储在每个顶点的pagerank属性下,然后返回排名值最高的五个顶点。还有一个类似的代码块生成介数中心性作为顶点特征。介数中心性是一个较慢的算法,请耐心等待。
接下来,我们基于其度(FT3 在特征图表中)和金额(FT6,也称为强度)计算交易的特征。这些使用可以在笔记本的GraphML/applications/fraud_detection/gsql文件夹中找到的自定义查询。运行“Degree Features”和“Amount Features”下的代码块。每个代码块大约需要 10 到 20 秒。
查看查询以检查您对 GSQL 的理解。amounts查询为图中的每个顶点设置四个顶点属性:最小接收量、总接收量、最小发送量和总发送量。degrees查询更简单,只检查接收到的交易数量(入度)和发送到其他顶点的数量(出度)。
现在,我们在每个顶点上有一组与图相关的特征,包括它们是否为欺诈账户的真实情况,我们可以使用传统的监督学习方法尝试预测欺诈。
运行下一个代码块以获取快速 RP⁵嵌入。FastRP是一种基于随机投影(RP)原理进行降维的顶点嵌入算法。对于像这样的相对小数据集,它提供了非常好的性能,而且资源成本合理。
运行 Check Labels 块以检查数据集中欺诈和正常账户的数量。您应该得到关于标记账户的以下统计信息:
Fraud accounts: 1165 (3.62%%)
Normal accounts: 31003 (96.38%%)
在 Train/Test Split 代码块中,我们使用vertexSplitter函数将顶点分为 80%的训练数据和 20%的验证数据。笔记本中包含的vertexSplitter函数为每个顶点分配两个布尔特征is_training和is_validation,然后随机分配true或false值以创建 80-20 的分割。
接下来,我们创建两个顶点加载器,将所有顶点批量加载到机器学习服务器上。我们传递一个包含的属性列表;这些属性都是我们最近几步创建的,但不包括 is_fraud 标签。我们显示每组中的前五个顶点,以确保它们已正确加载。
使用图特征训练传统模型
现在我们准备训练我们的欺诈检测模型。我们将使用 XGBoost,这是一种用于表格数据的流行分类算法。我们从 xgboost 库导入 XGBClassifier 类,并创建一个名为 tree_model 的分类器实例,如“创建 xgboost 模型”代码块所示。
接下来,我们使用三组不同的特征训练 XGBoost 模型,以便比较它们的结果。对于每种情况,我们创建一个列表,其中包含所选的图特征,但不包括 is_fraud。然后我们使用 tree_model.fit() 来说,“使用我们的训练数据的特征,尝试预测属性 is_fraud。”训练每个模型后,另一个代码块使用 pyTigerGraph 中的 Accuracy、BinaryPrecision 和 BinaryRecall 模块评估每个模型。
运行第一个案例,仅使用非图特征。您的模型应该达到大约 75%的准确率,12%的精确度和 100%的召回率。请记住,大约 3.6%的交易是欺诈的。100%的召回率意味着我们的模型将捕捉到所有真实的欺诈案例。由于总体准确率为 75%,这意味着模型错误地将大约 25%的正常账户分类为欺诈者。
运行下一个案例,现在包括 PageRank 和介数中心性。您应该看到准确率和精确度提高了几个百分点,而召回率降至约 98%。最后,运行第三个案例,将 FastRP 嵌入添加到特征集中。您应该看到准确率和精确度显著提高。图 12-2 比较了三个案例的预测性能。

图 12-2. 带有图特征和图嵌入的预测性能(请查看此图的更大版本:oreil.ly/gpam1202)
运行下一个单元格,在“解释模型”部分下创建一个类似图 12-3 的图表,显示 Case 2 的特征重要性,包括图算法,但不包括我们的训练中的图嵌入。注意,pagerank 是预测欺诈的第二重要特征,仅次于 send_amount。

图 12-3. XGBoost 模型中图算法的特征重要性(请查看此图的更大版本:oreil.ly/gpam1203)
接下来,在“解释模型”下方运行后续单元,以查看嵌入式特征的重要性。在这里,嵌入式的所有维度被汇总为一个特征重要性分数。图 12-4 显示,嵌入式对模型的性能贡献很大。

图 12-4. 使用 FastRP 嵌入式的 XGBoost 模型特征重要性(请查看此图的更大版本:oreil.ly/gpam1204)
使用图神经网络
在下一节中,我们将设置一个图神经网络,试图更精确地预测欺诈账户。
在 GNN 部分的第一个块中,我们设置了一些超参数。我们已经选择了产生高度准确结果的良好超参数值。然而,微调超参数是机器学习的一门艺术,因此在完成本节后,请回到此阶段并尝试调整这些值。
就像在上一节中一样,我们设置了两个加载器来将数据加载到两个大块中:训练和验证。然而,这些加载器使用的是 neighborLoader 方法,而不是 vertexLoader。在 GNN 中,每个顶点都受其相邻顶点的影响。因此,当我们加载数据时,我们不仅加载单个顶点,还加载围绕每个顶点中心的邻域。正如您所看到的,这些加载器的语法大致等同于 xgboost 部分的加载器,尽管这些加载器还包含一些超参数。
现在我们的数据已经准备好了,我们可以创建和训练 GNN。此笔记本使用了 pyTorch Geometric GDS 库,它提供了几个 GNN 模型。ML 工作台足够灵活,可以使用其中任何一个;它还假定了 DGL 和 TensorFlow 图机器学习库。一些更常见的模型已经内置到 pyTigerGraph 中,以便更方便地使用。
我们将使用图注意力网络(在 pyTorch Geometric 库中的 GAT),它结合了注意力模型的细粒度建模和图邻域卷积。我们将网络运行 10 个时期。
这比非图形 XGBoost 模型稍微花费更多时间。部分原因是神经网络与决策树模型相比的复杂性。在免费的云实例层中,每个时期大约需要五秒钟。企业级数据集通常在更强大的硬件上运行,利用 GPU 大大加快了进程,远远超过使用 CPU 的可能性。
然而,额外的等待是值得的。当最后一个时期完成时,请查看返回的值:我们达到了超过 90% 的准确率!
运行下面的几个单元格,可以实时查看随时间的训练情况。在解释模型部分,我们随机选择一个可疑顶点来查看其网络情况。由于我们没有完全准确的模型,这个单元格有时可能显示连接很少或没有连接的顶点。然而,如果多次运行这个单元格,你可能会看到更接近图 12-5 的顶点邻域。

图 12-5. 针对顶点 311 的预测的视觉解释(请在 oreil.ly/gpam1205 查看更大版本的图像)
在这种情况下,该顶点从其他顶点接收大量交易,然后向更多顶点进行大额交易。如果这确实是一个诈骗者,他们可能会接收许多支付,然后将资金转移到另一组他们也能访问的账户。
运行最后的代码块后,我们得到了另一个关于特征重要性的图表,类似于 图 12-6。我们可以看到,在我们更准确的 GNN 模型中,重要的特征往往与 XGBoost 模型中被识别为重要的特征不同。在这里,与欺诈检测相关的特征中,PageRank 并不像收到的金额、发送的金额和收到的交易数那样重要。然而,请记住,GNN 模型的邻域卷积已经考虑了关系的影响,因此像 PageRank 这样的图特征可能是多余的。

图 12-6. GNN 模型预测顶点 311 欺诈特征的重要性(请在 oreil.ly/gpam1206 查看更大版本的图像)
最后,运行最后的代码块后,我们得到了 图 12-7,显示了三个模型的综合性能。在这里,我们看到我们的 GNN 在准确率和精度上甚至比 XGBoost 和嵌入都要好。GNN 达到的召回率比其他两个模型低。然而,它仍然很好,特别是与没有嵌入的 XGBoost 相比。

图 12-7. XGBoost 对比 XGBoost + FastRP 对比 GNN 的性能(请在 oreil.ly/gpam1207 查看更大版本的图像)
章节总结
在本章中,我们研究了一个特定的机器学习问题,并比较了三种增强图方法来解决它。我们的平台是 TigerGraph ML Workbench,包括可在 TigerGraph Cloud 实例上使用的示例笔记本和数据集。
我们首先使用了传统的决策树机器学习库 XGBoost,通过包含 PageRank 和度等基于图的特征的数据,将以太坊交易数据集分类为普通账户和涉嫌欺诈的账户。在这里,图数据,图分析甚至图机器学习都为数据准备阶段做出了贡献。
接着,我们使用了 GNN 进行相同的预测,在训练阶段考虑了图关系,结果比 XGBoost 能实现的模型具有更高的精度。
与您建立联系
我们希望我们的书能与您建立联系,无论是信息的形式,洞察力,甚至一些灵感。我们热爱图形和图形分析,因此我们希望这种热爱能够显现出来。我们的使命是帮助您将数据视为连接实体,从连接数据的视角学习数据搜索和分析,并通过使用案例起始套件开始开发解决方案以完成您自己的任务。
从我们作为数据分析师,作家和教育工作者的经验来看,我们知道并非一切在第一次就能讲清楚。通过使用 TigerGraph 的入门套件或其他教程进行一些实际操作是帮助您串联思路并亲自了解下一步可能是什么的最佳方式。此外,这个领域仍在快速增长和发展,正如 TigerGraph 产品一样。我们将在https://github.com/TigerGraph-DevLabs/Book-graph-powered-analytics上发布书籍、补充材料和常见问题的更正和更新。
我们很乐意收到您的反馈。您可以通过 gpaml.book@gmail.com 联系我们。
谢谢您,祝您探索愉快!
¹ “发现欺诈的真正成本”,LexisNexis,访问于 2023 年 5 月 29 日,https://risk.lexisnexis.com/insights-resources/research/us-ca-true-cost-of-fraud-study。
² 麦肯齐·希加洛斯,“加密骗子在 2021 年创下纪录,窃取了 140 亿美元”,CNBC,2022 年 1 月 6 日,https://www.cnbc.com/2022/01/06/crypto-scammers-took-a-record-14-billion-in-2021-chainalysis.html。
³ 李昂·陈等,“以太坊交易网络中的网络钓鱼检测”,《ACM 互联网技术期刊》第 21 卷,第 1 期(2021 年 2 月):1–16,doi: 10.1145/3398071。
⁴ 李昂·陈等,“以太坊网络中的网络钓鱼交易”,XBlock,访问于 2023 年 5 月 29 日,https://xblock.pro/#/dataset/13。
⁵ 由于图机器学习是一个快速发展的领域,笔记本中的算法可能在您获取它时已经更新。


浙公网安备 33010602011771号