图驱动的机器学习-全-
图驱动的机器学习(全)
原文:Graph-Powered Machine Learning
译者:飞龙
前置内容
前言
技术世界对机器学习充满热议。每天我们都被其应用和进步的文章所轰炸。但在从业者中,一场安静的革命正在酝酿,这场革命将图置于机器学习的核心。
亚历山德罗在近十年的实践之后撰写了这本书,正值图论与机器学习的交汇点。如果亚历山德罗为网络巨头之一工作,提炼一支由博士组成的军队的知识,这将是一本有趣的书,但对于我们大多数人来说,它只能满足我们的好奇心,而不是作为一本实用的指南。幸运的是,尽管亚历山德罗拥有博士学位,但他从事企业领域的工作,对企业构建的系统有着深刻的同理心和了解。这本书反映了这一点:亚历山德罗能够有效地解决软件工程师和数据专业人士在构建非超大规模网络巨头之外的当代系统时必须克服的实践设计和实施挑战。
图增强机器学习展示了图对于机器学习未来的重要性。它不仅表明图提供了为当代机器学习管道提供动力的优越手段,而且还表明图是组织、分析和处理数据以进行机器学习的自然方式。本书提供了一次丰富的、精心策划的图机器学习之旅,每个主题都基于亚历山德罗的深厚经验和长期实践者的轻松、精致的自信。
本书循序渐进,提供了一个整体框架来推理机器学习并将其集成到我们的数据系统中。紧接着,它提供了一种实用的方法来处理推荐,涵盖了各种方法,如协同过滤、基于内容和会话的推荐以及混合风格。亚历山德罗指出了在最先进的技术中缺乏可解释性的问题,并表明这不是图方法的问题。然后,他继续处理欺诈检测,涉及邻近性和社交网络分析等概念,我们在犯罪网络背景下重新学习了“物以类聚”的格言。最后,本书处理了知识图谱:图技术消费文档并从中提取关联知识的能力,消除术语歧义,处理模糊查询术语。主题范围广泛,但信息质量始终优秀。
在整本书中,亚历山德罗温和地引导读者,从基础知识到高级概念逐步构建。通过示例和配套代码,注重实践的读者能够快速运行示例,并据此为自己的需求进行适配。你将带着各种实用的工具完成这本书,如果你愿意,甚至可能有一些指甲下的污垢。你将准备好提取图特征,让你的现有模型在今天表现更好,你也将准备好明天原生地与图数据库一起工作。我保证这将是一次美妙的旅程。
——吉姆·韦伯博士,Neo4j 首席科学家
前言
2012 年的夏天是我能记得的意大利南部最热的一个夏天。我和我的妻子正在等待我们第一个儿子的出生,他很快就要出生了,所以我们几乎没有机会外出或在阿普利亚那令人惊叹的新鲜、干净的水中享受任何清凉。在这种条件下,你可以因为 DIY(在我这个情况下不是)而变得疯狂,或者你可以用一些具有挑战性的东西来保持你的大脑忙碌。因为我不是 Sudoku 的超级粉丝,所以我开始了一个夜间和周末的项目:尝试构建一个通用的推荐引擎,它可以服务于多个范围和场景,从小型简单的到复杂和结构化的用户-项目交互数据集,最终包括相关的上下文信息。
这是我生活中图数据库强力进入的时刻。这种灵活的数据模型不仅允许我以相同的方式存储用户的购买信息,还包括所有相关的信息(后来正式定义为上下文信息)以及生成的推荐模型。当时,Neo4j 1.x 版本刚刚发布。尽管它没有现在拥有的 Cypher 或其他高级查询机制,但它足够稳定,让我选择它作为我项目的主要图数据库。采用图数据库帮助我解开了项目的难题,四个月后,我发布了 reco4j 的 alpha 版本:历史上第一个图驱动推荐引擎!
这是一场真正的、充满激情的爱情故事的开始。在接下来的三年里,我一直在自己尝试,试图在这里和那里推销 reco4j 的想法(说实话,并不那么成功),直到我和 GraphAware 的首席执行官 Michal Bachman 通了一次电话。几天后,我飞往伦敦,签署了我的合同,成为这家小型咨询公司的第六位员工,该公司帮助公司在他们的图项目中取得成功。最终,图数据库成为了我的存在理由(当然,在我两个孩子之后)。
之后,图生态系统发生了很大的变化。更多的公司开始采用图作为它们的核心技术,为客户提供先进的服务或解决内部问题。在 GraphAware,我在那里成长为首席科学家,公司规模显著增长,我有机会帮助公司利用图来构建新的服务和改进现有的服务。图不仅能够解决经典问题——从基本的搜索功能到推荐引擎,从欺诈检测到信息检索——而且也成为了改进和增强机器学习项目的突出技术。网络科学和图算法为对自然连接数据和无连接数据进行不同类型的分析提供了新的工具。
在多年的咨询生涯中,我与数据科学家和数据工程师交谈,发现了很多可以通过使用图模型或图算法解决的问题。这种向人们展示一种不同方式来处理机器学习项目的经历,使我写下了你手中的这本书。图模型并不声称能解决所有问题,但它们可以成为你武器库中的另一件利器。这本书是你自己的爱情故事开始的地方。
致谢
这本书的发布历时超过三年。它需要大量的工作——肯定比我最初脑海中这个疯狂想法出现时想象的要多得多。与此同时,这已经成为我迄今为止职业生涯中最激动人心的经历。(是的,我正在计划写第二本书。)我喜欢制作这本书,但这是一段漫长的旅程。这就是为什么我想感谢很多人在过程中帮助我的原因。
首先,我要感谢我的家人。我的妻子奥罗拉在我所有漫长的夜晚和早起的早晨里不得不独自入睡,我的孩子们很少看到我的脸,除了在无数周末的笔记本电脑屏幕上。感谢你的理解和无条件的爱。
接下来,我想感谢我在 Manning 出版社的开发编辑 Dustin Archibald。感谢你与我合作,并教会我所有关于写作的知识。我特别感谢你在每个月都晚交的情况下仍然如此耐心。你对这本书质量的承诺使得它对每一位读者来说都变得更好。还有许多其他在 Manning 出版社的同事在出版的每个阶段都与我合作,我要向他们表示衷心的感谢。你们的团队是一个伟大且运转良好的机器,我很享受与每一位同事的合作。
向所有审稿人致谢:Alex Ott、Alex Lucas、Amlan Chatterjee、Angelo Simone Scotto、Arnaud Castelltort、Arno Bastenhof、Dave Bechberger、Erik Sapper、Helen Mary Labao Barrameda、Joel Neely、Jose San Leandro Armendáriz、Kalyan Reddy、Kelvin Rawls、Koushik Vikram、Lawrence Nderu、Manish Jain、Odysseas Pentakalos、Richard Vaughan、Robert Diana、Rohit Mishra、Tom Heiman、Tomaz Bratanic、Venkata Marrapu 和 Vishwesh Ravi Shrimali;你们的建议帮助使这本书变得更好。
最后但同样重要的是,如果没有 GraphAware,尤其是没有 Michal,这本书将不会存在——这不仅因为他雇佣了我,并允许我在这样一个令人惊叹的公司中成长,而且还因为当我在喝啤酒时告诉他我在考虑写一本书时,他说:“我认为你应该这么做!”这本书也离不开 Chris 和 Luanne,他们从第一天起就是我最忠实的粉丝;离不开 KK,他总是在关键时刻用正确的话来鼓励我并激励我;离不开 Claudia,她帮助我审阅图像;以及离不开我那些了不起的同事,我们进行了最有意思和最具挑战性的讨论。你们所有人让这本书成为可能!
关于这本书
图增强机器学习是一本关于在机器学习应用中有效使用图的实用指南,展示了在图中扮演关键角色的完整解决方案的构建所有阶段。它侧重于与图相关的技术、算法和设计模式。基于我在构建复杂机器学习应用的经验,本书提出许多配方,其中图是美味产品的主要成分,供您的客户使用。在整个机器学习项目的生命周期中,这些方法可以在多个方面发挥作用,例如更有效地管理数据源、实现更好的算法、存储预测模型以便更快地访问,以及以更有效的方式可视化结果以进行进一步分析。
谁应该阅读这本书?
这本书适合你吗?如果你是一位数据科学家或数据工程师从业者,它可以帮助你完成或开始你的学习路径。如果你是一位需要启动或推动新机器学习项目的经理,它可以帮助你向团队提出不同的视角。如果你是一位对探索图的力量感兴趣的资深开发者,它可以帮助你发现关于图的新视角,不仅作为一种数据库,而且作为一种使 AI 成为可能的使能技术。
这本书不是关于机器学习技术的一般性汇编;它专注于与图相关的技术、算法和设计模式,这是本书的重点主题。具体来说,本书专注于图方法如何帮助您开发和交付更好的机器学习项目。图模型技术被详细地介绍,并描述了多个基于图的算法。最复杂的概念通过具体场景进行说明,并设计了具体的应用。
这本书旨在成为一本实用的指南,帮助你将一个可工作的应用程序安装到生产环境中。因此,它描述了优化技术和启发式方法,以帮助你处理真实数据、真实问题和真实用户。本书不仅讨论了玩具示例,还讨论了来自现实世界用例的端到端应用程序,并提出了一些处理具体问题的建议。
如果这些场景激起了你的兴趣,这本书绝对是适合你的。
本书是如何组织的
本书共 12 章,分为四个部分。第一部分介绍了本书的主要主题,从通用的机器学习和图概念开始,然后转向结合这些概念的优势:
-
第一章介绍了机器学习和图,并涵盖了理解以下章节所需的基本概念。
-
第二章列出了与机器学习输入相关的主要挑战,并讨论了如何通过使用图模型和图数据库来处理它们。它还介绍了图数据库的主要特性。
-
第三章详细描述了图在机器学习工作流程中的作用,并描述了一个用于大规模图处理的系统。
第二部分讨论了几个实际案例,在这些案例中,图支持了机器学习项目的发展并改善了最终结果,特别是关注推荐:
-
第四章介绍了最常见的推荐技术,并描述了如何为其中之一设计合适的图模型:基于内容的推荐引擎。它详细说明了如何将现有的(非图)数据集导入图模型,并实现工作的基于内容的推荐引擎。
-
第五章描述了如何为协作过滤方法设计合适的图模型,以及如何实现完全工作的协作过滤推荐引擎。
-
第六章介绍了基于会话的推荐算法并描述了一个能够捕捉用户会话数据的图模型。它说明了如何将样本数据集导入设计的模型,并在其上实现一个真实的推荐引擎。
-
第七章引导读者通过实现一个考虑用户上下文的推荐引擎。它描述了上下文感知推荐引擎的图模型,并展示了如何将现有数据集导入图模型。此外,本章还说明了如何在一个单一引擎中结合多种推荐方法。
第三部分解决欺诈检测问题:
-
第八章介绍了欺诈检测并描述了不同领域中的不同类型欺诈。它还指出了在建模数据以更快、更轻松地揭示欺诈方面图的作用,以及一些用于简单图模型以对抗欺诈的技术和算法。
-
第九章转向基于异常检测的更高级的欺诈对抗算法。它展示了如何使用图来存储和分析交易的 k-NN,并识别异常交易。
-
第十章介绍了如何使用社会网络分析(SNA)来分类欺诈者和欺诈风险。它列出了基于 SNA 的欺诈分析的不同图算法,并展示了如何从数据中推导出合适的图。
第四部分涵盖了自然语言处理(NLP):
-
第十一章介绍了与基于图的 NLP 相关的概念。特别是,它描述了一种简单的方法,通过提取非结构化数据的隐藏结构,将文本分解并存储在图中。
-
第十二章介绍了知识图谱,详细说明了如何从文本中提取实体和关系,并创建知识图谱。它列出了与知识图谱一起使用的后处理技术,例如语义网络构建和自动主题提取。
虽然从头到尾阅读这本书可以最大化学习效果,但并不需要这样做。它可以作为参考,随时应对新挑战。对于这个领域的初学者,我建议从前三章开始,以清晰地理解关键概念,然后跳转到感兴趣的具体主题章节。如果你对某个特定主题或应用感兴趣,最好从你感兴趣部分的第一章开始:第四章为推荐,第八章为欺诈检测,第十一章为自然语言处理。如果你是图和机器学习的专家,并且只寻找建议,你可以阅读你感兴趣的章节。每个章节都建议阅读其他章节中的材料。
关于代码
这本书包含了许多源代码示例,无论是编号列表还是与普通文本内联,源代码都以固定宽度字体格式化,如下所示,以将其与普通文本区分开来。
在许多情况下,原始源代码已被重新格式化;我们添加了换行并重新整理了缩进,以适应书中的可用页面空间。在某些情况下,即使这样也不够,列表中还包括了行续接标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常已从列表中删除。许多列表都有代码注释,突出显示重要概念。
本书中的示例源代码可以从出版商的网站www.manning.com/books/graph-powered-machine-learning下载,或者你可以从我的 Git 仓库github.com/alenegro81/gpml克隆它。
liveBook 讨论论坛
购买 Graph-Powered Machine Learning 包括免费访问由 Manning Publications 运营的私人网络论坛,您可以在那里对本书发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛并订阅它,请将您的网络浏览器指向livebook.manning.com/book/graph-powered-machine-learning。您还可以在livebook.manning.com/discussion了解更多关于 Manning 的论坛和行为准则。
Manning 对我们读者的承诺是提供一个场所,在那里个人读者之间,以及读者和作者之间可以进行有意义的对话。这不是对作者参与特定数量承诺的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您向他提出挑战性问题,以免他的兴趣转移!只要本书有售,论坛和先前讨论的存档将从出版社的网站提供访问。
在线资源
需要更多想法和支持吗?
GraphAware 网站(www.graphaware.com)总是充满了与使用图解决问题相关的最新博客文章、视频、播客和案例研究。
Neo4j 网站(neo4j.com)也不断发布关于图的新材料,展示如何在多个环境中正确使用图数据库。
如果您在安装、查询或管理您的 Neo4j 实例或其他图数据库时遇到问题,Stack Overflow(stackoverflow.com)总是寻找您问题解决方案的最佳资源。
关于作者
首先,我对计算机科学和数据研究充满极大的热情。我专长于自然语言处理、推荐引擎、欺诈检测和图辅助搜索。
在学术上追求计算机工程并在该领域担任各种职务后,我继续攻读跨学科科学与技术博士学位。随着我对图数据库的兴趣达到顶峰,我创立了一家名为 Reco4 的公司,旨在支持一个名为 reco4j 的开源项目——第一个基于图数据源的推荐框架。
现在,我是 GraphAware 的首席科学家,我们所有人都在追求成为图技术领域的首选品牌的目标。我们有 LinkedIn、世界经济论坛、欧洲航天局和美国银行等客户,我们专注于帮助客户通过将数据转化为可搜索的、可理解的和可操作的知识来获得竞争优势。在过去的几年里,我致力于领导 Hume(我们的知识图谱平台)的开发,并在世界各地的各种会议上发表演讲。
关于封面插图
《图神经网络机器学习》封面上的图像是从《罗马塔兰泰拉舞者,手摇铃鼓和曼陀林演奏者》(1865)中提取的。艺术家是安东·罗马科(1832-1889),一位在旅行中爱上意大利的奥地利 19 世纪画家,1857 年移居罗马,开始绘画意大利的日常生活片段。这幅画展示了一位塔兰泰拉舞者,她在跳舞的同时还在敲击手摇铃鼓(与一位演奏曼陀林的男子一起)。这幅图像被选为对作者自己在意大利南部的起源的致敬,特别是在南阿普利亚,那里这种舞蹈仍然是一种活的传统。塔兰泰拉是一种传统舞蹈,表演者模拟了被当地常见的狼蛛——被称为“塔兰图拉”(不要与今天普遍所知的塔兰图拉混淆)咬后的情况。这种蜘蛛被普遍认为毒性极强,其咬伤会导致一种被称为塔兰图拉病的歇斯底里症状。这种舞蹈的原始形式是无序且流畅的,以模拟这种状态。其他人认为,舞蹈的发明是因为连续的运动,通过引起大量出汗,可以帮助将蜘蛛的毒液从体内排出。
无论这种舞蹈的真正起源是什么,其音乐和节奏都是令人愉悦且无法抗拒的。今天仍然非常常见当地人表演这种舞蹈。
第一部分 引言
我们被图所包围。Facebook、LinkedIn 和 Twitter 是最著名的社交网络示例——即人的图。尽管我们并不认为它们是这样的,但其他类型的图也存在:如电力网络、地铁等。
图是强大的结构,不仅用于表示连接信息,还支持多种类型的分析。它们简单的数据模型,由节点和关系等两个基本概念组成,足够灵活以存储复杂信息。如果你也在节点和关系中存储属性,那么可以表示任何大小的一切。
此外,在图中,每个节点和每条关系都是分析的一个接入点,从接入点出发,可以以无限的方式导航其余部分,这提供了多种接入模式和分析潜力。
另一方面,机器学习提供了构建现实表示和提供预测的工具和技术。推荐是一个很好的例子;算法根据用户交互的内容,能够预测他们可能感兴趣的内容。欺诈检测是另一个例子,它通过分析之前的交易(合法或不合法)来创建一个模型,该模型能够以良好的近似度识别新的交易是否为欺诈。
机器学习算法的性能,无论是准确性还是速度,几乎直接受到我们表示训练数据和存储预测模型的方式的影响。算法预测的质量与训练数据集的质量相当。如果我们希望对预测达到合理的信任水平,数据清洗和特征选择等任务是强制性的。系统提供预测的速度会影响整个产品的可用性。假设一个在线零售商的推荐算法在 3 分钟内产生推荐。到那时,用户可能已经在另一个页面,或者更糟糕的是,在竞争对手的网站上。
图可以通过它们最擅长的任务来支持机器学习:以易于理解和访问的方式表示数据。图使所有必要的流程更快、更准确、更有效。此外,图算法是机器学习实践者的强大工具。图社区检测算法可以帮助识别人群,页面排名可以揭示文本中最相关的关键词,等等。
如果你没有完全理解引言中介绍的一些术语和概念,本书的第一部分将为你提供进一步阅读所需的所有知识。它将图和机器学习的基本概念作为单一、独立的实体,以及强大的二元组合介绍。祝您阅读愉快!
1 机器学习与图:简介
本章涵盖
-
机器学习简介
-
图的简介
-
图在机器学习应用中的作用
机器学习是人工智能的核心分支:它是计算机科学中研究计算机程序如何从数据中学习的领域。这个术语是在 1959 年提出的,当时 IBM 的计算机科学家亚瑟·塞缪尔(Arthur Samuel)编写了第一个玩跳棋的计算机程序[塞缪尔,1959]。他心中有一个明确的想法:
编程计算机从经验中学习最终应该消除大部分这种详细的编程工作。
塞缪尔通过为每个棋盘位置分配基于固定公式的分数来编写他的初始程序。这个程序工作得相当好,但在第二种方法中,他让程序与自己执行数千场比赛,并使用结果来细化棋盘评分。最终,程序达到了人类玩家的水平,机器学习迈出了第一步。
一个实体——如人、动物、算法或通用计算机代理¹——如果在观察世界之后能够提高其在未来任务上的表现,那么它就是在学习。换句话说,学习是将经验转化为专业知识或知识的过程[沙莱夫-舒瓦茨和本-大卫,2014]。学习算法使用代表经验的训练数据作为输入,并创建专业知识作为输出。这种输出可以是计算机程序、复杂的预测模型或内部变量的调整。性能的定义取决于特定的算法或要实现的目标;一般来说,我们认为它是预测与特定需求匹配的程度。
让我们用一个例子来描述学习过程。考虑实现电子邮件的垃圾邮件过滤器。一个纯粹的编程解决方案是编写一个程序来记忆由人类用户标记为垃圾邮件的所有电子邮件。当一封新电子邮件到达时,伪代理将在之前的垃圾邮件中搜索相似匹配,如果找到任何匹配项,新电子邮件将被重定向到垃圾文件夹。否则,电子邮件将未经修改地通过过滤器。
这种方法可能有效,在某些情况下可能有用。然而,它不是一个学习过程,因为它缺少学习的一个重要方面:概括的能力,将个别例子转化为更广泛模型的能力。在这个特定用例中,这意味着即使它们与之前标记的电子邮件不同,也能标记未见过的电子邮件。这个过程也被称为归纳推理或归纳推理。² 为了概括,算法应该扫描训练数据,提取一组在电子邮件消息中出现可指示垃圾邮件的单词。然后,对于一封新的电子邮件,代理会检查是否有可疑的单词出现,并据此预测其标签。
如果你是一位经验丰富的开发者,你可能想知道:“为什么我要编写一个学习如何编程的程序,当我可以指示计算机执行当前任务时?”以垃圾邮件过滤器为例,可以编写一个程序来检查某些单词的出现,如果这些单词存在,则将电子邮件分类为垃圾邮件。但这种方法有三个主要缺点:
-
开发者无法预见到所有可能的情况。在垃圾邮件过滤用例中,不能事先预测出可能用于垃圾邮件的所有单词。
-
开发者无法预见到所有随时间的变化。在垃圾邮件中,可能会使用新词,或者采用一些技术来避免容易被识别,例如在字符之间添加连字符或空格。
-
有时候,开发者无法编写程序来完成这项任务。例如,识别朋友的 faces 对于人类来说是一个简单的任务,但如果没有使用机器学习,编写软件来完成这个任务是不可能的。
因此,当你面对新的问题或任务,希望用计算机程序来解决时,以下问题可以帮助你决定是否使用机器学习:
-
这个特定任务是否过于复杂而无法编程?
-
在任务的生命周期中是否需要任何形式的适应性?
任何机器学习任务的一个关键方面是构建知识所依赖的训练数据。从错误的数据开始会导致错误的结果,无论潜在的性能或学习算法的质量如何。
这本书的目的是帮助数据科学家和数据工程师从两个角度来接近机器学习过程:学习算法和数据。在这两个视角中,我们将使用图(我现在将其介绍为一组节点及其相互连接的关系)作为一个有价值的心理和技术模型。许多基于以图表示的数据的学习算法可以提供高效的预测模型,而其他算法可以通过使用以图表示的数据或在工作流程中使用图算法来改进。图的使用还提供了许多其他好处:图形是表示过程输入知识的有价值存储模型,管理训练数据,以及存储预测模型的输出,提供多种快速访问它的方式。这本书将引导读者通过整个机器学习项目生命周期,逐步展示所有可能有用和可靠的图案例。
但图表并不是所有机器学习项目的万能药。在流分析中,需要处理数据流以揭示短期异常,以图表形式存储数据可能毫无用处。此外,其他算法需要的数据格式可能无法适应图表,无论是在训练期间还是模型存储和访问时。本书使读者能够判断在过程中使用图表是否会成为优势或负担。
1.1 机器学习项目生命周期
机器学习项目既是一个人类过程,也是一个软件项目。它涉及大量人员、大量沟通、大量工作和一系列混合技能,并且需要一个明确的方法才能有效。我们将通过定义一个清晰的步骤和组件的流程来开始我们的漫长旅程,这些步骤和组件将在整本书中使用。这里提出的心理模型,即许多可能模型之一,将帮助您更好地理解图表在成功机器学习项目开发和部署中的作用。
提供机器学习解决方案是一个复杂的过程,它需要的不仅仅是选择正确的算法(s)。这些项目包括与[Sculley, 2015]相关的众多任务
-
选择数据源
-
收集数据
-
理解数据
-
清洗和转换数据
-
处理数据以创建机器学习模型
-
评估结果
-
部署
部署后,有必要监控应用程序并对其进行调整。整个过程涉及多个工具、大量数据和不同的人员。
数据挖掘项目中最常用的过程之一是跨行业标准数据挖掘流程,或称 CRISP-DM [Wirth 和 Hipp, 2000]。尽管 CRISP-DM 模型是为数据挖掘设计的,但它也可以应用于通用的机器学习项目。CRISP-DM 的关键特性使其成为基础工作流程模型的有吸引力的部分包括
-
它不是专有的。
-
它是应用、行业和工具中立的。
-
它明确地从应用和技术两个角度来审视数据分析过程。
这种方法可用于项目规划和管理、沟通以及文档编制。
CRISP-DM 参考模型概述了机器学习项目生命周期。这个框架或心理模型有助于在从数据角度接近机器学习项目之前,从算法角度出发,并为定义清晰的流程提供基准。图 1.1 展示了过程的六个阶段。值得注意的是,数据是这个过程的核心。
观察图 1.1,我们看到阶段的顺序是流动的。箭头仅表示阶段之间最重要的和最频繁的依赖关系;在特定项目中,每个阶段的成果决定了下一个要执行的阶段或阶段的具体任务。

图 1.1 CRISP-DM 过程的六个阶段
外圈符号表示过程的循环性质,解决方案部署后并不意味着结束。后续的机器学习过程不仅可以从先前过程的经验中受益(Linoff 和 Berry [2011] 的良性循环),还可以从先前过程的结果中受益。让我们更详细地概述每个阶段。
1.1.1 业务理解
第一阶段需要定义机器学习项目的目标。这些目标通常用一般术语表达:增加收入、改善客户体验、获得更好的定制搜索结果、销售更多产品等等。为了将这些高级问题定义转换为机器学习项目的具体需求和约束,有必要了解业务和领域。
机器学习项目是软件项目,在这个阶段,学习语言和领域概念也很重要。这种知识不仅有助于数据科学家在后续阶段与内部团队之间的沟通,还能提高文档和结果展示的质量。
这个阶段的结果是
-
对领域和业务视角有清晰的理解
-
定义目标、需求和约束
-
将这些知识转换为机器学习问题定义
-
设计一个初步且合理的项目计划,旨在实现目标
第一轮的目标不应该过于宽泛,因为这一轮需要大量与将机器学习过程注入现有基础设施相关的工作。同时,在设计第一轮时,也要考虑到未来的扩展。
1.1.2 数据理解
数据理解阶段首先从询问数据源开始,并从每个数据源收集一些数据,然后继续以下活动:
-
熟悉数据。
-
识别数据质量问题。
-
获得对数据的初步洞察。
-
检测有趣的子集,以形成关于隐藏信息的假设。
数据理解需要领域和业务理解。此外,查看数据有助于建立对领域和业务视角的理解,这就是为什么这个阶段和前一个阶段之间存在反馈循环。
这个阶段的结果是
-
对可用的数据源有清晰的理解
-
对不同类型的数据及其内容(或至少对机器学习目标的所有重要部分)有清晰的理解
-
设计架构以获取或提取这些数据并将其输入到机器学习工作流程的下一步
1.1.3 数据准备
这一阶段涵盖了从多个来源收集数据并将其组织成模型阶段算法所需的具体结构的所有活动。数据准备任务包括记录和属性选择、特征工程、数据合并、数据清理、构建新属性和现有数据的丰富。正如之前指出的,数据的质量对下一阶段最终结果的影响巨大,因此这一阶段至关重要。
这一阶段的结果是
-
使用适当的设计技术定义一个或多个数据结构
-
为喂养机器学习算法训练数据而定义的清晰数据管道
-
一套合并、清理和丰富数据的程序
这一阶段另一个结果是确定在等待处理期间将存储这些数据的数据库管理系统。
为了完整性,在进一步处理之前,并不总是需要有一个明确的数据存储来持久化数据。在处理阶段之前,可以提取数据并转换它。然而,这样的中间步骤在性能、数据质量以及进一步的可扩展性方面有很多优点。
1.1.4 模型
模型阶段是机器学习发生的地方。选择并应用不同的算法,并将它们的参数校准到最佳值。算法被用来构建一系列预测模型,当评估阶段完成后,从中选择最佳的模型进行部署。一个有趣的现象是,一些算法产生预测模型,而另一些则不产生。³
这一阶段的结果是
-
下一个阶段要测试的算法集
-
相关的预测模型(如果适用)
数据准备和建模之间存在紧密的联系,因为在建模过程中,你经常发现数据问题并获得构建新数据点的想法。此外,一些技术需要特定的数据格式。
1.1.5 评估
在机器学习项目的这个阶段,你已经构建了一个或多个看起来质量很高的预测模型。在模型可以部署之前,重要的是要彻底评估它,并回顾构建模型的步骤,以确保你确信它正确地实现了在过程开始时定义的业务目标。
这种评估是以正式的方式进行,例如将可用的数据分成训练集(80%)和测试集(20%)。另一个主要目标是确定是否已经充分考虑了任何重要的业务问题。
这一阶段的结果是
-
一组允许测量性能的值。(成功的具体衡量标准取决于算法类型和范围。)
-
对是否实现业务目标进行彻底评估。
-
在生产环境中使用解决方案的授权。
1.1.6 部署
由于机器学习模型是为了在组织中发挥某种作用而构建的,因此模型的创建通常不是项目的终点。根据需求,部署阶段可能只是生成报告那么简单,也可能像发布一个为最终用户提供服务的完整基础设施那么复杂。在许多情况下,客户——而不是数据科学家——执行部署步骤。无论如何,重要的是事先了解需要执行哪些操作才能利用创建的模型。
此阶段的结果如下:
-
一个或多个包含预测模型结果的报告
-
用于预测未来和辅助决策的预测模型本身
-
为最终用户提供特定服务的基础设施
当项目处于生产状态时,需要持续对其进行监控(例如评估性能)。
1.2 机器学习挑战
机器学习项目有一些固有的挑战,使得它们难以完成。本节总结了在接近新的机器学习项目时需要考虑的主要方面。
1.2.1 真实来源
CRISP-DM 模型通过从数据的角度描述生命周期,将数据置于机器学习过程的核心。训练数据代表任何洞察都可以从中提取,任何预测都可以做出的真实来源。管理训练数据需要大量的努力。正如华盛顿大学计算机科学教授 Jeffrey Heer 所说:“将算法发送到原始数据并让洞察浮现出来是一个绝对的神话。”作为一个案例,据估计,数据科学家将高达 80% 的时间用于数据准备 [Lohr, 2014]。
在讨论算法的细节之前,我经常使用以下陈述来将重点转移到数据上:
即使是最优秀的机器学习算法在错误的数据上也会产生错误的结果。
Banko 和 Brill [2001] 以及 Halevy、Norvig 和 Pereira [2009] 的开创性论文指出,对于复杂问题,数据往往比算法更重要。这两篇文章都考虑了自然语言处理,但这个概念可以推广到机器学习的一般性 [Sculley, 2015]。
图 1.2,来自 Banko 和 Brill [2001]的研究,展示了针对一组学习者的学习曲线,考虑了每个学习者在不同大小的训练数据(高达 10 亿单词)上的平均性能。这里使用的具体算法并不重要;关键点是,如图像所示,在训练阶段增加可用数据量提高了所有学习者的性能。这些结果表明,重新考虑在语料库开发与算法开发之间分配时间和金钱是值得的。从另一个角度来看,作为一名数据科学家,你可以专注于垂直维度——寻找更好的算法——但图表显示,在水平方向上还有更多的改进空间——收集更多数据。作为证据,图 1.2 中表现最差的算法在拥有 1000 万个元素时比表现最好的算法在拥有 100 万个元素时表现要好得多。

图 1.2 混淆集去歧义的学习曲线
从多个来源收集数据不仅允许你访问大量数据,还可以提高数据质量,解决稀疏性、拼写错误、正确性等问题。从各种来源收集数据不是问题;我们生活在大数据时代,网络、传感器、智能手机、企业数据库和公开数据源提供了大量的数字数据。但如果将不同数据集组合起来有价值,也会出现问题。来自不同来源的数据格式不同。在学习者分析之前,数据必须被清理、合并并规范化为算法可以理解的统一和同质化的模式。此外,对于许多问题,获取额外的训练数据都有非零成本,对于监督学习,这种成本可能很高。
由于这些原因,数据代表了机器学习过程中的第一个重大挑战。数据问题可以总结为四个类别:
-
数据量不足—机器学习需要大量的训练数据才能正常工作。即使是简单的用例,也需要数千个示例,而对于深度学习或非线性算法等复杂问题,你可能需要数百万个示例。
-
数据质量差—数据来源总是充满了错误、异常值和噪声。差的数据质量会直接影响机器学习过程的结果质量,因为许多算法难以丢弃错误(不正确、无关或无关)的值,并在这种混乱中检测到潜在的规律。
-
非代表性数据—机器学习是一个归纳过程:模型从其观察到的内容进行推断,并且不太可能支持训练数据中未包含的边缘情况。此外,如果训练数据太嘈杂或仅与可能情况的子集相关,学习者可能会产生偏差或过度拟合训练数据,并且无法推广到所有可能的情况。这对于基于实例和基于模型的机器学习算法都是正确的。
-
无关特征—如果数据包含一组良好的相关特征而不是太多无关特征,算法将以正确的方式学习。尽管选择更多特征通常是一种有用的策略,目的是提高模型的准确性,但更多并不总是更好。使用更多特征将使学习者能够找到从特征到目标的更详细的映射,这增加了模型计算将过度拟合数据的可能性。特征选择和特征提取是数据准备阶段两项重要的任务。
为了克服这些问题,数据科学家必须从多个来源收集和合并数据,对其进行清理,并通过使用外部来源来丰富它。(此外,数据往往是为了某个特定目的而准备的,但在过程中,你可能会发现一些新东西,而预期的目的会发生变化。)这些任务并不简单;它们不仅需要大量的专业技能,还需要一个数据管理平台,以便以方便的方式执行更改。
与训练示例质量相关的问题决定了机器学习项目基础设施的一组数据管理约束和要求。这些问题可以总结如下:
-
管理大数据—从多个数据源收集数据并将其合并到一个统一的事实来源将生成一个庞大的数据集,正如之前所指出的,增加(质量)数据的数量将提高学习过程的质量。第二章考虑了大数据平台的特点,并展示了图如何在这种巨兽的驯服中发挥突出作用。
-
设计灵活的架构—尝试创建一个架构模型,它能够将多个异构架构合并到一个统一且同质化的数据结构中,以满足信息和导航需求。架构应根据机器学习项目目的的变化轻松演进。第四章介绍了多种数据模型架构和最佳实践,用于对几种场景进行数据建模。
-
开发高效的访问模式—快速的数据读取可以提高训练过程的性能,从处理时间来看。特征提取、过滤、清理、合并以及其他训练数据的预处理任务将受益于使用提供多种灵活访问模式的数据平台。
1.2.2 性能
性能在机器学习中是一个复杂的话题,因为它可以与多个因素相关:
-
预测准确性,可以通过使用不同的性能指标来评估。回归问题的一个典型性能指标是均方根误差(RMSE),它衡量系统在预测中犯错的均方差⁴。换句话说,它查看测试数据集中所有样本的估计值与已知值之间的差异,并计算平均值。我在本书后面的章节中介绍了其他测量性能的技术,当讨论不同的算法时。准确性取决于几个因素,例如用于训练模型的数据量、数据的质量以及所选的算法。如 1.2.1 节所述,数据在保证适当准确度水平方面起着主要作用。
-
训练性能,指的是计算模型所需的时间。要处理的数据量以及所使用的算法类型决定了处理时间和预测模型所需的存储空间。显然,这个问题更多地影响那些在训练阶段产生模型的算法。例如,基于实例的学习者⁵的性能问题会在处理过程的后期出现,比如在预测阶段。在批量学习中,由于要处理的数据量较大(与在线学习方法相比,在线学习方法是从较少的数据量中增量学习算法),训练时间通常较长。虽然在在线学习中,要处理的数据量较小,但处理速度会影响系统与最新数据的同步能力,这直接影响到预测的准确性。
-
预测性能,指的是提供预测所需的时间响应。机器学习项目的输出可能是一个静态的一次性报告,以帮助管理者做出战略决策,或者是一个面向最终用户的在线服务。在前一种情况下,完成预测阶段和计算模型所需的时间不是主要问题,只要在合理的时间内完成工作(即,不是几年)。在后一种情况下,预测速度确实很重要,因为它会影响用户体验和预测的有效性。假设你正在开发一个推荐引擎,该引擎根据用户的兴趣推荐与用户当前查看的产品相似的产品。用户的导航速度相当快,这意味着在用户继续浏览下一个项目之前,需要在短时间内进行大量的预测;只有几毫秒的时间可以建议一些有用的东西。在这种情况下,预测速度是成功的关键。
这些因素可以转化为对机器学习项目的多个要求,例如在训练期间快速访问数据源、高数据质量、高效的模型访问模式以加速预测等。在这种情况下,图可以提供适当的存储机制,用于源数据和模型数据,减少读取数据所需的访问时间,并提供多种算法技术来提高预测的准确性。
1.2.3 存储模型
在基于模型的学习者方法中,训练阶段的输出是一个用于进行预测的模型。此模型需要时间来计算,并且必须存储在持久化层中,以避免每次系统重启时重新计算。
模型的结构直接相关于所使用的特定算法或算法类。例如包括
-
使用最近邻方法的推荐引擎的项目到项目相似性
-
表达元素如何分组到簇中的项目到簇映射
两个模型的尺寸差异极大。考虑一个包含 100 个项目的系统。作为初步尝试,项目到项目的相似性需要存储 100 x 100 个条目。利用优化,这个数字可以减少到只考虑前 k 个相似项,在这种情况下,模型将需要 100 x k 个条目。相比之下,项目到簇映射只需要 100 个条目;因此,存储模型在内存或磁盘上的空间可能是巨大的或相对适中。此外,如前所述,模型访问/查询时间会影响预测阶段的整体性能。因此,模型存储管理是机器学习中的一个重大挑战。
1.2.4 实时
机器学习越来越多地被用于向用户提供实时服务。例子涵盖了从简单的响应用户最后点击的推荐引擎到被指示不要伤害过街行人的自动驾驶汽车的全范围。尽管这两个例子中失败的结果差异很大,但在两种情况下,学习者对来自环境的新刺激做出快速(或适当及时)反应的能力对于最终结果的质量是基本的。
考虑一个为匿名用户提供实时推荐的推荐引擎。这种匿名性(用户未注册或登录)意味着没有长期的历史交互记录——只有通过使用 cookies 提供的短期、基于会话的信息。这是一个复杂的任务,涉及多个方面,并影响机器学习项目的多个阶段。所采取的方法可能因使用的学习者而异,但目标可以描述如下:
-
快速学习。在线学习者应该能够在新数据可用时立即更新模型。这种能力将减少事件或通用反馈(如导航点击或与搜索会话的交互以及模型的更新)之间的时间差距。模型与最新事件越一致,就越能满足用户的当前需求。
-
快速预测。当模型更新时,预测应该很快——最多几毫秒——因为用户可能已经离开当前页面,甚至可能迅速改变他们的观点。
这两个目标都需要能够快速对齐模型的算法,以及提供快速记忆和高效访问模式的存储机制(在内存中、在磁盘上或两者的组合版本)。
1.3 图
如本章引言所述,图提供了可以极大地支持机器学习项目的模型和算法。尽管图是一个简单的概念,但了解如何表示它以及如何使用其周围的主要概念是很重要的。本节介绍了图世界的关键元素。如果您已经掌握了这些概念,可以跳过这部分内容。
1.3.1 什么是图?
图是一个简单且相当古老的数学概念:由一组顶点(或节点/点)和边(或关系/线)组成的数据结构,可以用来模拟一组对象之间的关系。传说懒惰的莱昂哈德·欧拉于 1736 年首次开始谈论图。当访问普鲁士的柯尼斯堡(现俄罗斯加里宁格勒)时,欧拉不想在城市里花费太多时间散步,该城市位于普雷格尔河两侧,包括两个通过七座桥梁连接到彼此和城市两个大陆部分的两个大岛。欧拉将问题形式化为计划一次穿过所有这些桥梁一次且仅一次的散步。他证明了这样做是不可能的,这导致了图和图论的发明确立 [Euler, 1736]。因此,他留在了家里。图 1.3 显示了柯尼斯堡的一张旧地图和欧拉用来证明其论文的图的表示。


图 1.3 带领图论发明的柯尼斯堡桥梁
更正式地说,一个图是一个对 G = (V, E),其中 V 是顶点的集合 V = {V[i], i = 1,n},E 是 V 上的边的集合,E[ij] = {(V[i], V[j]), V[i] ∊ V, V[j] ∊ V}。E ⊆ [V]²;因此,E 的元素是 V 的两个元素子集 [Diestel, 2017]。
表示图的最简单方法是为每个顶点画一个点或一个小圆圈,如果它们形成一个边,则通过一条线连接两个这样的顶点。这种更正式的描述如图 1.4 所示。

图 1.4 在 V = {1, 2, 3, 4, 5} 上的无向图,其边集 E = {(1,2), (1,5), (2,5), (2,4), (4,3)}
图可以是有向的或无向的,这取决于是否在边上定义了遍历方向。在有向图中,边 E[ij] 可以从 Vi 遍历到 Vj,但不能反向遍历;V[i] 被称为尾或起始节点,V[j] 被称为头或终止节点。在无向图中,两个方向的边遍历都是有效的。图 1.4 表示一个无向图,图 1.5 表示一个有向图。

图 1.5 在 V = {1, ..., 5} 上的有向图,其边集 E = {(1,2), (2,5), (5,1), (2,4), (3,4)}
箭头表示关系的方向。默认情况下,图中的边是无权重的;因此,相应的图被称为无权图。当给边分配一个权重——一个用于传达某些意义的数值时,该图被称为加权图。图 1.6 显示了与图 1.4 和图 1.5 相同的图,并为每条边分配了权重。

图 1.6 一个无向加权图(a)和一个有向加权图(b)
如果图 G 中的两个顶点 x 和 y 通过边 {x,y} 相连,则称这两个顶点为相邻或邻居。连接它们的边 E[ij] 被称为在两个顶点 V[i] 和 V[j] 上关联。如果两个不同的边 e 和 f 有一个共同的顶点,则称它们为相邻。如果 G 的所有顶点都是成对相邻的,则 G 是完全图。图 1.7 显示了一个完全图,其中每个顶点都与所有其他顶点相连。

图 1.7 每个顶点都与所有其他顶点相连的完全图
图中的顶点的一个重要属性是其度,定义为与该顶点相连的边的总数,这也等于该顶点的邻居数量。例如,在图 1.4 的无向图中,顶点 2 的度数为 3(它有顶点 1、4 和 5 作为邻居);顶点 1(邻居是 2、5)、4(邻居是 2、3)和 5(邻居是 1、2)的度数为 2,而顶点 3 的度数为 1(仅与顶点 4 相连)。
在有向图中,顶点 V[i]的度数分为顶点的入度,定义为以 V[i]为终端节点(箭头的头部)的边的数量,以及顶点的出度,即以 V[i]为起始节点的边的数量。在图 1.5 的有向图中,顶点 1 和 5 的入度和出度都是 1(它们各自有两个关系,一个进入和一个出去),顶点 2 的入度是 1,出度是 2(从 1 进入一个关系,向 4 和 5 各出去两个关系),顶点 4 的入度是 2,出度是 0(从 2 和 3 进入两个关系),顶点 3 的出度是 1,入度是 0(向 4 出去一个关系)。
图的平均度数计算如下,

其中 N 是图中顶点的数量。
具有性质,即序列中的每对连续顶点都通过一条边连接的顶点序列称为路径。没有重复顶点的路径称为简单路径。环是首尾顶点相同的路径。在图 1.4 中,[1,2,4]、[1,2,4,3]、[1,5,2,4,3]等都是路径;特别是顶点序列[1,2,5]的路径代表一个环。
1.3.2 图作为网络模型
图对于表示事物在简单或复杂结构中如何物理上或逻辑上相互连接非常有用。当我们给边和顶点分配名称和意义时,这样的图就变成了所谓的网络。在这些情况下,图是描述网络的数学模型,而网络是对象之间的一系列关系,这些对象可能包括人、组织、国家、Google 搜索中找到的项目、脑细胞或电力变压器。这种多样性展示了图及其简单结构(这也意味着它们需要很少的磁盘存储空间)的巨大力量,这些结构可以用来模拟⁶复杂系统。
让我们通过一个例子来探讨这个概念。假设我们有一个如图 1.8 所示的图。

图 1.8 一个非平凡的通用图
这个在数学定义上纯粹的图可以根据边的类型和顶点的类型来模拟几种类型的网络:
-
如果顶点是人们,而每条边代表人类之间任何类型的关系(友谊、家庭成员、同事),则称为社交网络。
-
如果顶点是信息结构,如网页、文档或论文,而边代表逻辑连接,如超链接、引用或交叉引用,则称为信息网络。
-
如果顶点是能够中继消息的计算机或其他设备,而边代表可以传输消息的直接链接,则称为通信网络。
-
一种 交通网络,如果顶点是城市,而边代表使用航班、火车或道路的直接连接
这一小组示例展示了如何通过为边和顶点分配不同的语义,同一个图可以表示多个网络。图 1.9 展示了不同类型的网络。
观察图 1.9,我们可以发现图形的另一个有趣特征:它们具有很强的沟通能力。图形能够以清晰的方式展示信息,这也是为什么它们经常被用作信息地图。将数据表示为网络并使用图算法,可以
-
寻找复杂模式
-
使其可见以便进行进一步调查和解读




图 1.9 从左上角顺时针方向:共现网络⁷、1974 年的 ARPA 网络⁸、伦敦地铁网络⁹和电网¹⁰

在这个方向上,许多有趣的例子也可以在组织顾问 Valdis Krebs 的博客文章中找到,¹² 他专注于社交网络应用。他的工作中包含了将图增强的机器学习与人类思维相结合的例子,通过图可视化来实现。在这里,我们考虑其中一个最著名的例子。
图 1.11 中的数据来自 Amazon.com,代表了 2008 年美国购买的前政治书籍列表 [Krebs, 2012]。Krebs 将网络分析原理应用于数据,创建了一个与那年总统选举相关的书籍地图。如果两本书经常被同一顾客购买,则它们之间有链接。这些书籍被称为也购买对(在那个顾客购买了这本书也购买了那本书)。

图 1.11 2008 年美国政治书籍网络图(Krebs, 2012)
有三个不同且不重叠的集群:
-
左上角的奥巴马书籍集群
-
中间的民主党(蓝色)集群
-
右下角的共和党(红色)集群
2008 年,美国政治气候高度两极分化。这一事实在 Amazon 的政治书籍数据中得到了反映,图 1.11 显示了保守派和自由派选民之间的深刻分歧。红色和蓝色书籍之间没有联系或中间人;每个集群与其他集群完全不同。如前所述,有一个单独的阅读总统候选人巴拉克·奥巴马传记的人的集群,但他们显然对阅读或购买其他政治书籍不感兴趣。
四年后,在 2012 年,同样的分析产生了一个看起来实质上不同的网络(图 1.12)。这个网络显示了许多作为集群之间桥梁的书籍。此外,潜在的选民似乎在阅读关于两位主要候选人的书籍。结果是,一个更复杂的网络,没有孤立的集群。

图 1.12 2012 年美国政治书籍网络图 [Krebs, 2012]
政治书籍网络示例引入了网络的一个重要方面。如果图是一个纯粹数学概念,存在于它自己的柏拉图世界中,那么网络,作为某些具体系统或生态系统的抽象,会受到力量的影响,这些力量作用于它们,改变它们的结构。我们将这些力量称为周围环境:存在于网络顶点和边之外的因素,但仍然会影响网络结构随时间演化的方式。这种环境的性质和力量的类型是特定于网络类型的。例如,在社会网络中,每个人都有一个独特的个人特征集合,两个人之间的特征相似性和兼容性会影响链接的创建或删除 [Easley and Kleinberg, 2010]。
社会网络结构的基本观念之一是同质性(源自希腊语,意为对相同事物的喜爱):社会网络中的链接倾向于连接相似的人。更正式地说,如果两个人的特征在比例上大于他们所来自的群体或他们所属的网络中的预期比例,他们更有可能被连接[Verbrugge, 1977]。反之亦然:如果两个人是连接的,他们更有可能拥有共同的特征或属性。因此,我们的 Facebook(例如)朋友并不像随机抽样的人群,他们在种族、种族和地理维度上通常与我们相似;他们在年龄、职业、兴趣、信仰和观点上往往与我们相似。这一观察已有悠久的历史,其起源远在马克·扎克伯格写下第一行代码之前。这一基本思想可以在柏拉图(“相似性产生友谊”)和亚里士多德(人们“爱那些与自己相似的人”)的著作中找到,以及民间命题如“物以类聚”中。同质性原则也适用于群体、组织、国家或社会单位的任何方面。
理解网络周围的上下文以及作用于网络的相关力量,以多种方式帮助机器学习任务:
-
网络是既受欢迎又不受欢迎的流动的渠道。营销人员总是在尝试接触和说服人们。如果能够找到一种方法来启动雪球滚动,个人接触是最有效的。这个概念是所谓病毒式营销的基础。
-
理解这些力量可以使我们预测网络随时间如何演变,并使数据科学家能够积极应对这些变化或将其用于特定的商业目的。
-
社会学和心理学学科的研究结果表明,一个人的社交网络在决定他们的品味、偏好和活动方面具有相关性。这些信息对于构建推荐引擎很有用。与推荐引擎相关的一个问题是冷启动问题:由于没有他们的历史,你无法为新用户预测任何事情。社交网络和同质性原则可以用来根据连接用户的品味进行推荐。
1.4 图在机器学习中的作用
图被用来表征感兴趣对象之间的交互,用于建模简单和复杂的网络,或一般地用于表示现实世界问题。因为它们基于严格但简单的形式化,所以在许多科学领域中被使用,从计算机科学到历史科学。我们不应该对它们在机器学习中作为强大的工具被广泛使用感到惊讶,这个工具可以启发直觉并推动许多有用的功能。基于图的机器学习随着时间的推移变得越来越普遍,超越了众多传统技术。
许多不同规模的公司都在使用这种方法为他们的客户提供更高级的机器学习功能。一个突出的例子是谷歌,它正在使用基于图的机器学习作为其 Expander 平台的核心。这项技术背后支撑着许多你可能每天都在使用的谷歌产品和服务,例如 Gmail 收件箱中的提醒或 Google Photos 中的最新图像识别系统。¹⁴
构建一个图驱动机器学习平台有许多好处,因为图不仅可以作为克服先前描述的挑战的有价值工具,还可以提供没有图支持无法实现的高级功能。
图 1.13 展示了机器学习和图之间的主要接触点,考虑了不同任务的目标。

图 1.13 图驱动机器学习思维导图
这个思维导图可以立即直观地展示图在机器学习全景中的角色。在图 1.13 中,图特征被分为三个主要区域:
-
数据管理——这个区域包含图提供的帮助机器学习项目处理数据的特性。
-
数据分析——这个区域包含对学习和预测有用的图特征和算法。
-
数据可视化——这个区域突出了图作为视觉工具的实用性,它帮助人们通过使用人脑来沟通、与数据互动并发现洞察。
模式还显示了基于图的技术与 CRISP-DM 模型阶段之间的映射。
1.4.1 数据管理
图允许学习系统探索更多你的数据,更快地访问它,并轻松地进行清理和丰富。传统的学习系统在研究者准备的单个表格上训练,而图原生系统可以访问的不仅仅是这个表格。
图驱动数据管理功能包括
-
连接的真实数据源——图允许你将多个数据源合并成一个单一、统一、连接的数据集,为训练阶段做好准备。通过减少数据稀疏性、增加可用数据量以及简化数据管理,这一特性带来了巨大的优势。
-
知识图谱——基于前面的想法,知识图谱提供了一个统一的数据结构,不仅可以将数据源合并,还可以将预测模型、手动提供的数据和外部知识来源合并。结果数据是机器可用的,可以在训练、预测或可视化过程中使用。
-
快速数据访问——表格提供与行和列过滤器相关的单一访问模式。另一方面,图提供了对同一数据集的多个访问点。通过减少需要访问的数据量到特定需求的基本最小值,这一特性提高了性能。
-
数据丰富—除了使扩展现有数据与外部源变得容易外,图的无模式性质和图数据库内提供的访问模式有助于数据清理和合并。
-
特征选择—在数据集中识别相关特征是几个机器学习任务(如分类)的关键。通过提供快速访问数据和多种查询模式,图加速了特征识别和提取。
在 CRISP-DM 模型的“数据理解”和“数据准备”阶段,连接的真相来源和知识图是宝贵的辅助工具,而快速数据访问、数据丰富和特征选择在数据准备阶段很有用。
1.4.2 数据分析
图可以用来建模和分析实体之间的关系以及它们的属性。这一方面为图驱动的机器学习带来了额外的信息维度,可以用于预测和分类。图提供的模式灵活性还允许不同的模型在同一个数据集中共存。
图像驱动的数据分析功能包括
-
图算法—几种类型的图算法,如聚类、页面排名和链接分析算法,对于在数据中识别洞察和分析目的很有用。此外,它们可以用作更复杂分析过程中的第一个数据预处理步骤。
-
图加速机器学习—前面讨论的图驱动的特征提取是图如何加速或提高学习系统质量的一个例子。图可以帮助在训练阶段之前或期间过滤、清理、丰富和合并数据。
-
网络动态—对网络周围环境和相关作用力的认识不仅使你能够理解网络动态,而且还可以利用它们来提高预测质量。
-
混合模型—在同一个图中可以共存多个模型,利用灵活且快速的访问模式,只要它们在预测阶段可以合并。这一特性提高了最终精度。此外,同一个模型有时可以用不同的方式使用。
-
快速模型访问—实时使用需要快速预测,这意味着需要一个尽可能快就能访问的模型。图提供了这些范围所需的正确模式。
图算法、图加速机器学习和网络动态主要涉及建模阶段,因为它们比其他特性更多地与学习过程相关。部署阶段利用混合模型和快速模型访问方法,因为它们在预测阶段运行。
1.4.3 数据可视化
图表具有强大的沟通能力,它们能够以人类大脑易于理解的方式同时展示多种类型的信息。这一特性在机器学习项目中非常重要,无论是用于分享结果、分析它们,还是帮助人们导航数据。
图表驱动的数据可视化特性包括
-
数据导航—网络通过突出元素之间的连接来显示数据。它们既可以作为帮助人们正确导航数据的辅助工具,也可以作为强大的调查工具。
-
人类大脑分析—以图表的形式展示数据通过结合人类大脑的力量和机器学习的力量,释放了机器学习的潜力,实现了高效、高级和复杂的数据处理和模式识别。
-
改进沟通—图表—特别是属性图—是“白板友好”的,这意味着它们在数据库中存储的方式与在白板上表示的方式概念上是一致的。这一特性缩小了复杂模型的技术细节与将其传达给领域专家或利益相关者之间的差距。有效的沟通提高了最终结果的质量,因为它减少了领域理解、业务目标、项目需求和限制方面的问题。
在业务和数据理解阶段,改进沟通尤为重要,而数据导航和人类大脑分析主要与评估阶段相关。
1.5 书籍心智模型
本章中展示的心智图帮助您轻松地可视化图表在机器学习项目中的位置。这并不意味着在所有项目中,您都会使用图表来完成那里列出的所有事情。在本书的几个示例中,图表被用来克服一些问题或提高性能(无论是质量还是数量);拥有一个帮助您通过简单的图像了解图表在特定情况下作用的思维模型将是有用的。
下一个架构将图表驱动的机器学习心智图中的关键特征(接触点)组织成机器学习工作流程的四个主要任务:
-
管理数据源,指的是收集、合并、清理和准备训练数据集以供学习阶段的所有任务
-
学习,涉及将机器学习算法应用于训练数据集
-
存储和访问模型,包括存储预测模型的方法以及提供预测的访问模式
-
可视化,指的是数据可视化的方式,以支持分析
这些要点总结在图 1.14 中展示的心智图中。

图 1.14 描述机器学习项目四个阶段的思维模型
此图将在书中反复出现。该方案将帮助您立即了解当前讨论在项目工作流程中的位置。
这种心智模型从过程角度展示了机器学习项目,这是确定你在生命周期中处于何种位置的最佳方式。但同时也从更广泛、以任务为导向的角度思考项目也是有用的。
摘要
-
机器学习的目标是开发计算机程序,这些程序可以从样本数据中自主获得经验,将其转化为专业知识,而无需明确编程。
-
机器学习项目不仅是一个软件项目,也是一个涉及众多不同技能的人的复杂的人类过程。它需要一种明确和系统的方法才能成功。CRISP-DM 提供了正式的项目生命周期来推动这样的项目,帮助实现正确的结果。
-
任何机器学习项目必须应对的挑战主要与数据管理相关——无论是训练数据集还是预测模型——以及学习算法的性能。
-
图是简单的数学概念,可以用来模拟和分析复杂网络。网络外的周围环境对其产生影响,决定了其如何演变。
-
图和网络可以从数据管理、数据分析、数据可视化三个维度提升机器学习项目的能力。
参考文献
[Banko and Brill, 2001] Banko, Michele, and Eric Brill. “Scaling to Very Very Large Corpora for Natural Language Disambiguation.” Proceedings of the 39th Annual Meeting on Association for Computational Linguistics (2001): 26-33.
[Diestel, 2017] Diestel, Reinhard. Graph Theory. 5th ed. New York: Springer, 2017.
[Easley and Kleinberg, 2010] Easley, David, and Jon Kleinberg. Networks, Crowds, and Markets: Reasoning About a Highly Connected World. Cambridge, UK: Cambridge University Press, 2010.
[Euler, 1736] Euler, Leonhard. “Solutio Problematis ad Geometriam Situs Pertinentis.” Comment. Acad. Sci. U. Petrop 8 (1736): 128-40.
[Halevy, Norvig, and Pereria, 2009] Halevy, Alon, Peter Norvig, and Fernando Pereira. “The Unreasonable Effectiveness of Data.” Intelligent Systems, IEEE 24:2 (2009): 8-12.
[Krebs, 2012] Krebs, Valdis. “Political Book Networks.” TNT: The Network Thinkers, October 2012. www.thenetworkthinkers.com/2012/10/2012-political-book-network.html.
[Linoff and Berry, 2011] Linoff, Gordon S., and Michael J. A. Berry. Data Mining Techniques: For Marketing, Sales, and Customer Relationship Management. 3rd ed. Hoboken, NJ: Wiley, 2011.
[Lohr, 2014] Lohr, Steve. “For Big-Data Scientists, ‘Janitor Work’ Is Key Hurdle to Insights.” New York Times, August 15, 2014. mng.bz/K4wn.
[Mihalcea 等人,2011] Mihalcea, Rada,和 Dragomir Radev. 基于图的自然语言处理和信息检索。英国剑桥:剑桥大学出版社,2011 年。
[Russell 和 Norvig, 2009] Russell, Stuart J.,和 Peter Norvig. 人工智能:现代方法。第 3 版。新泽西州上萨德尔河:皮尔逊,2009 年。
[Samuel, 1959] Samuel, Arthur L. “使用国际象棋游戏进行机器学习的一些研究。” IBM 研究与发展杂志 3:3(1959 年 7 月):210-229.
[Sculley, 2015] Sculley, D.,Gary Holt,Daniel Golovin,Eugene Davydov,Todd Phillips,Dietmar Ebner,Vinay Chaudhary,Michael Young,Jean-Francois Crespo,和 Dan Dennison. 2015. “机器学习系统中的隐藏技术债务。” 第 28 届国际神经网络信息处理系统会议——第 2 卷(NIPS15)。麻省理工学院出版社,美国剑桥,马萨诸塞州,2503-2511.
[Shalev-Shwartz 和 Ben-David, 2014] Shalev-Shwartz, Shai,和 Shai Ben-David. 理解机器学习:从理论到算法。英国剑桥:剑桥大学出版社,2014 年。
[Verbrugge, 1977] Verbrugge, Lois M. “成年友谊选择的结构。” 社会力量 56(1977 年):576-597.
[Wirth 和 Hipp, 2000] Wirth, R., 和 J. Hipp. “CRISP-DM:数据挖掘的标准流程模型。” 第四届国际知识发现和数据挖掘实际应用会议论文集(2000 年):29-39.
^(1.)根据 Russell 和 Norvig [2009] 的说法,一个智能体是某种能够行动的东西。(“智能体”一词来源于拉丁语“agere”,意为“做。”)所有计算机程序都会做某些事情,但计算机智能体被期望做更多:自主操作、感知其环境、在长时间内持续存在、适应变化,并创造和追求目标。
^(2.)根据斯坦福哲学百科全书网站(plato.stanford.edu/entries/logic-inductive)的说法,在归纳逻辑中,前提应在一定程度上支持结论。相比之下,在演绎推理中,前提逻辑上蕴含结论。因此(尽管有些人不同意这个定义),归纳有时被定义为从具体观察中推导出一般原则的过程。
^(3.)附录 A(关于机器学习分类)包含了一些创建预测模型的算法示例。
^(4.)“标准差”是一个衡量组内成员与组平均值的差异程度的度量。
^(5.)如果您对实例化算法和批量学习等概念不熟悉,请参阅附录 A,它涵盖了机器学习分类。
^(6.)在此语境中,动词“模型”是指以简化的方式表示一个系统或现象。建模还旨在以计算机系统易于处理的方式表示数据。
(7.)Higuchi Koichi—KH Coder 的共现屏幕截图 (en.wikipedia.org/wiki/Co-occurrence_network).
(8.)Yngvar—1974 年 9 月 ARPANET 的符号表示 (en.wikipedia.org/wiki/ARPANET).
(9.)伦敦交通局提供 (mng.bz/G6wN).
(10.)Paul Cuffe—高压输电系统的网络图,显示了不同电压级别之间的互联 (en.wikipedia.org/wiki/Electrical_grid).
(11.)panamapapers.icij.org.
(12.)www.thenetworkthinkers.com.
(13.)数学柏拉图主义 (mng.bz/zG2Z) 是一种形而上学观点,认为存在抽象的数学对象,其存在独立于我们以及我们的语言、思想和实践。
(14.)mng.bz/0rzz
2 图形数据工程
-
与机器学习输入相关的主要大数据挑战
-
如何使用图模型和图数据库处理大数据分析
-
图形数据库的形状和特征
第一章强调了数据在机器学习项目中的关键作用。正如我们所见,在大量高质量数据上训练学习算法比微调或替换算法本身更能提高模型的准确性。在一次关于大数据的访谈[Coyle, 2016]中,发明了现在广泛使用的亚马逊项目到项目协同过滤算法的 Greg Linden 回答说:
大数据是亚马逊推荐工作得如此之好的原因。大数据是调整搜索并帮助我们找到所需内容的原因。大数据是使网络和移动智能化的原因。
在过去几年里,我们在各个领域(信息技术、工业、医疗保健、物联网(IoT)等)产生的数据量呈指数级增长。回到 2013 年,IBM 估计每天有 2500 亿字节的数据被创造——这意味着世界上 90%的数据在过去两年内已经被创造出来[Johnson, 2013]。这些数据来自各个地方:用于收集气候信息的传感器、社交媒体网站上的帖子、数字图片和视频、购买交易记录以及手机 GPS 信号,仅举几例。这些数据是大数据。图 2.1 展示了每分钟从知名应用程序或平台产生的当前数据量的统计数据[Domo, 2020]。

图 2.1 2020 年每分钟产生的数据(由 Domo, Inc.提供)
过去十年发生的巨大变化既不是互联网用户数量的激增,也不是像数据传感器这样的新系统的创造。这种变化是由对数据作为知识来源重要性的认识增强所引发的。对了解用户、客户、各种企业和组织的强烈愿望产生了新的数据收集、聚集和处理需求。这种愿望导致了数据科学家收集分析数据方式的变化。几年前,他们不得不在一些奇怪格式的非组织化和脏乱的数据孤岛中寻找数据。现在,随着公司发现了他们产生的数据中隐藏的价值,数据科学家领导了数据生成和收集的努力。
例如,一个旅游网站曾经可能只为简单的推荐引擎收集星级评分,而现在它使用每个用户提供的评论中的信息力量作为更详细的知识来源。这个过程产生了一种良性循环(图 2.2),能够在每个循环中收集更多数据。

图 2.2 数据收集循环
这种前所未有的数据源可用性使得机器学习从业者能够访问大量以多种形式存在的数据。但尽管找到和访问这些数据相对容易,存储和管理它们可能完全不同。特别是对于机器学习过程,有必要识别和提取与观察现象相关的特征,即可测量的属性或特征。选择信息丰富、区分度高和独立的特征是创建有效学习算法的关键步骤,因为这些特征定义了算法训练阶段的输入结构,并决定了预测的准确性。特征列表的要求根据算法类别而变化,但一般来说,更准确的数据会产生更好的模型。如果你能运行一个考虑 300 个因素而不是 6 个因素的预测,你就能更好地预测需求。然而,随着因素的增多,你也会面临过拟合的风险。
分析大数据的系统生命周期由一系列步骤组成,这些步骤从从多个数据源收集数据开始。需要专门的工具和框架将来自不同源的数据摄入到定义的大数据存储中。数据存储在特定的存储解决方案中(如分布式文件系统或非关系型数据库),这些解决方案旨在可扩展。更正式地说,完成这些任务所需的步骤可以总结如下:
-
收集. 从多个数据源收集和汇总数据。
-
存储. 数据以适当的方式存储在单个(或偶尔超过一个)易于访问的数据存储中,以便为下一阶段做好准备。
-
清洁. 数据通过使用统一和一致的架构进行合并、清洗,并在可能的情况下进行归一化。
-
访问. 数据可用。提供多种视图或访问模式,以简化并加快用于训练目的的数据集的访问。
本章重点介绍这四个步骤中的最后三个:存储、清洗和访问数据。本章描述了大数据的主要特征,并讨论了处理大数据的方法。详细介绍了基于图模型和图数据库的特定方法,并提供了最佳实践。
2.1 处理大数据
为了驯服这个巨兽——定义大数据分析平台的需求——我们需要了解它。让我们考虑使大数据成为“大”的基本特征。
在 2001 年(是的,超过 20 年前!),META Group 的分析师道格·兰尼(Doug Laney)发表了一篇题为“3D 数据管理:控制数据量、速度和多样性”的研究报告[Laney, 2001]。尽管这个术语本身并没有出现在兰尼的报告里,但十年后,“三个 V”——量、速度和多样性——已经成为大数据普遍接受的三维定义维度。
之后,又增加了一个维度:“真实性”,指的是数据集或数据源的质量、准确性或真实性。随着新的、不可信和未经验证的数据源的出现,例如 Web 2.0 中的用户生成内容,收集到的信息的可靠性和质量成为了一个大问题,导致真实性作为大数据平台的一个有价值的、重要的维度被普遍接受。图 2.3 回顾了“四个‘V’”的主要方面,这些方面将在以下章节中更详细地描述。

图 2.3 大数据的四个“V”
几年来,随着“大数据”这个术语开始受到越来越多的关注并变得流行,分析师和技术记者不断地在维度的列表中添加更多的“V”。截至最后一次检查,数量达到了 42 个[Shafer, 2017];随着时间的推移,无疑还会增加更多。
我们将重点关注原始的三个“V”加上真实性,因为它们仍然是普遍接受的。这些维度在整本书中用于强调图模型和数据库在管理大量数据中的作用。
2.1.1 量
从“量”(图 2.4)中获得的收益——处理大量信息的能力——是大数据对机器学习的主要吸引力。拥有更多和更好的数据胜过拥有更好的模型。在大量数据的情况下,简单的数学运算可以非常有效。

图 2.4 大数据量
假设你希望使用一种机器学习方法,该方法使用各种电子健康记录(EHR)数据来对治疗不同症状所需的类型进行实时预测。患者健康数据量呈指数增长,这意味着遗留的 EHR 数据量也在急剧增加。根据 Health Data Archiver [2018]:
来自 EMC 和研究公司 IDC 的一份报告提出了一些富有创意的方式来可视化健康信息的激增,预计健康数据将每年增长 48%。报告将 2013 年的医疗数据量定为 153 艾字节。按照预测的增长率,这个数字将在 2020 年增加到 2,314 艾字节。
如前所述,现代 IT、工业、医疗保健、物联网和其他系统生成数据的量呈指数级增长。这种增长一方面是由于数据存储和处理架构成本的降低,另一方面是由于从数据中提取有价值见解的能力(这些能力创造了新的需求),这些见解可以改善业务流程、效率和提供给最终用户或客户的服务的质量。尽管没有固定阈值来定义数据的量是否为“大数据”,但通常这个术语表示的数据规模是“难以使用传统的数据库系统和数据处理架构存储、管理和处理” [Bahga 和 Madisetti, 2016]。
试图持续收集和分析这些大数据已成为 IT 领域的主要挑战之一。解决这个挑战的方案主要分为两大类:
-
可扩展存储—扩展存储通常指的是添加更多机器并将(读取、写入或两者)负载分布到这些机器上。这个过程被称为水平扩展。通过查询或访问机制也可以实现可扩展性,这些机制提供了对整个数据存储子集的多个访问点,无需通过过滤器或索引查找来遍历整个数据集。原生图数据库属于第二组,将在 2.3.4 节中讨论。
-
可扩展处理—处理水平扩展不仅意味着有多个机器并行执行任务;它还需要分布式查询方法、网络中有效通信的协议、编排、监控以及分布式处理的具体范式(如分而治之、迭代和管道)。
在 CRISP-DM 生命周期(图 2.5)的数据理解和数据准备阶段,有必要确定数据源以及每个阶段的大小和结构,以设计模型并确定将使用的数据库管理系统(DBMS)。

图 2.5 CRISP-DM 模型中的数据理解和数据准备
图形在这些阶段可以提供有价值的支持,帮助解决与数据量相关的问题。基于图模型的数据可以从多个数据源存储在一个单一、高度连接和同质化的真相来源中,提供多种快速访问模式。具体来说,在大数据平台上,图形可以通过扮演两个角色来帮助解决数据量问题:
-
主要数据源—在这种情况下,图包含所有最低粒度的数据。学习算法直接访问图以执行其分析。从这个意义上说,根据分析类型,大数据的正确图数据库必须暴露
-
-
一种索引结构(常见于其他 SQL 和 NoSQL 数据库)以支持随机访问
-
一种仅访问图的一小部分的访问模式,消除了复杂索引查找或数据库扫描的需求
-
-
物化视图——在这种情况下,图表示主数据集的子集或其中数据的聚合版本,对于分析、可视化和结果沟通很有用。视图可以是分析过程的输入或输出,图提供的全局和本地访问模式也为此提供了宝贵的帮助。
2.2 节通过展示两个示例场景及其相关实现来阐述这些相反的方法。
2.1.2 速度
速度(图 2.6)指的是数据生成、积累或处理的快速程度。例如,在一小时内接收和处理 1,000 个搜索请求,与在几秒钟内接收和处理相同数量的请求是不同的。一些应用程序对数据分析有严格的时间限制,包括股票交易、在线欺诈检测和实时应用等。数据速度的重要性遵循与数据量类似的模式。以前仅限于特定行业段的问题现在正在更广泛的背景下出现。

图 2.6 大数据中的速度
假设你正在开发自动驾驶汽车。每辆应该自动驾驶的汽车都可以访问许多传感器,例如摄像头、雷达、声纳、GPS 和激光雷达。¹ 每个传感器每秒都会生成大量数据,如表 2.1 [Nelson, 2016]所述。
表 2.1 自驾驶汽车传感器每秒生成数据
| 传感器 | 每秒生成数据量 |
|---|---|
| 摄像头 | ~20-40 MB/s |
| 雷达 | ~10-100 KB/s |
| 声纳 | ~10-100 KB/s |
| GPS | ~50 KB/s |
| 激光雷达 | ~10-70 MB/s |
在这种场景下,你设计的系统不仅应该能够快速处理这些数据,还应该尽可能快地生成预测,以避免例如撞到正在过马路的行人。
但 incoming data 的速度并不是唯一的问题:例如,可以通过将快速移动的数据流式传输到批量存储中进行后续批量处理来解决这个问题。速度的重要性在于 反馈循环(图 2.7)的整体速度,它涉及从输入到决策的数据处理:
*通过反馈循环,系统通过监控预测的有效性并在需要时重新训练来持续学习。监控和使用产生的反馈是机器学习的核心。²

图 2.7 反馈循环的一个示例
一则 IBM 的广告指出,如果你所依赖的只是五分钟前的交通快照,你就不会过马路。这个例子说明了有时你无法等待报告运行或 Hadoop 作业完成。换句话说,“反馈循环越紧密,竞争优势越大” [Wilder-James, 2012]。理想情况下,实时机器学习平台应该能够即时分析数据,就像数据生成时一样。
随着时间的推移,针对大数据管理的一些机器学习架构最佳实践已经出现。Lambda 架构 [Marz and Warren, 2015],这是一种用于构建实时数据密集型系统的架构模式,是这些实践之一;我们将在 2.2.1 节中查看基于图实现的例子。
能够应对速度的数据基础设施必须提供对必要数据的快速访问,这可能只是整个数据的一部分。假设你打算实现一个针对度假房屋的实时推荐引擎。学习算法使用用户的最新点击和搜索记录来推荐房屋。在这种情况下,引擎不需要访问整个数据集;它检查最后 N 次点击或在过去 X 时间段内的点击,并做出预测。一个本地的图数据库(在第 2.3.4 节中描述)维护每个节点的关联列表。从最后一次点击开始——拥有适当的图模型——引擎可以沿着每个节点的关联回溯。这个例子是图提供的高性能访问数据集小部分的一个说明,以服务于速度。
2.1.3 多样性
多样性(图 2.8)与被分析的数据的不同类型和性质有关。数据在格式、结构和大小上有所不同;很少以完美有序和准备处理的形式出现。由于它来自不同的和多样化的来源,大数据以多种形状(结构化、非结构化或半结构化)和格式出现;它可以包括文本数据、图像、视频、传感器数据等等。任何大数据平台都需要足够灵活,能够处理这种多样性,特别是考虑到数据的潜在不可预测的演变。

图 2.8 大数据多样性
假设你想要组织一个公司所有分散在多个数据孤岛中的知识,为洞察引擎创建一个知识库。³ 数据可能以不同的格式存在,从结构良好的关系型数据库到公司提供的产品或服务的非结构化用户评论,从 PDF 文档到社交网络数据。为了被机器学习平台处理,数据需要以统一的方式组织、存储和管理。
尽管关系型数据库广受欢迎且熟悉,但它们不再是大数据平台的首选目的地。某些数据类型比其他数据类型更适合某些类型的数据库。例如,社交网络关系本质上就是图,因此非常适合存储在像 Neo4j(附录 B 中描述)这样的图数据库中,这使得对连接数据的操作变得简单高效。此外,由于图在管理连接数据方面的多功能性,其他类别的数据也适合图模型。正如 Edd Wilder-James [2012]所说:
即使没有根本性的数据类型不匹配,关系型数据库的一个缺点是其模式的静态性质。在一个敏捷、探索性的环境中,计算的成果会随着更多信号的检测和提取而演变。半结构化的 NoSQL 数据库满足了这种灵活性的需求:它们提供了足够的数据结构来组织数据,但在存储之前并不需要数据的精确模式。
作为一种 NoSQL 数据库,图数据库也不例外。基于节点和边的简单模型在数据表示方面提供了极大的灵活性。此外,在设计过程中后期可以出现新的节点和边类型,而不会影响先前定义的模型,这使得图具有高度的扩展性。
2.1.4 真实性
真实性(图 2.9)与收集到的数据的质量和/或可信度相关。只有当数据是正确的、有意义的和准确的时,数据驱动型应用才能从大数据中获益。

图 2.9 大数据中的真实性
假设你想通过使用评论来为旅游网站创建一个推荐引擎。这种类型的引擎是推荐领域的新趋势,因为评论包含的信息比旧式的星级评分要多得多。问题在于这种评论的真实性:
对于在线零售商来说,打击虚假评论行业的战争现在是业务的重要组成部分。今天,当评论提交给 TripAdvisor 时,它会通过一个跟踪系统,该系统检查数百个不同的属性,从基本的数据点,如评论者的 IP 地址,到更详细的信息,如提交评论所使用的设备的屏幕分辨率。⁴
如第一章所述,训练数据集中质量和数量的组合直接影响推断出的模型质量。错误的数据可能会影响整个处理流程。产生的预测结果肯定会受到影响。
数据很少完全准确和完整,这就是为什么需要进行清理过程的原因。这项任务可以通过图方法完成。具体来说,图访问模式使得根据元素之间的关系发现问题变得容易。此外,通过将多个来源合并到一个单一的真实来源中,可以合并信息,减少数据稀疏性。
2.2 大数据平台中的图
与大数据打交道是一项复杂的工作。机器学习平台需要访问数据以提取洞察力并向最终用户提供预测服务。表 2.2 总结了与四个“V”相关的挑战,将每个挑战与存储、管理、访问和分析数据的要求配对。
表 2.2 大数据挑战
| 大数据对比 | 挑战 |
|---|---|
| 体积 | 数据库应该能够存储大量数据,或者定义的模型应该能够通过聚合压缩数据,以便可以快速访问,同时模型可以执行所有必需的分析。 |
| 速度 | 数据通常以高速度到达,这需要队列来解耦数据摄取与存储机制。摄取速率和存储速率必须得到适当的平衡,以便数据能够快速转换和存储,从而防止队列中元素的积累。 |
| 多样性 | 数据库模式需要足够灵活,能够同时存储多种类型的信息,使用一个模型可以存储所有当前的数据类别以及项目后期可能出现的任何类别。 |
| 真实性 | 设计的模型和选择的数据库应该允许轻松、快速地导航数据,并识别不正确、无效或不受欢迎的数据。应该有一种方法来清理数据,允许去除噪声。简化数据(以对抗数据稀疏性)的任务也应得到支持。 |
处理这些挑战的方法可以分为两类:方法论(或设计)和技术。为了从两者中获取最佳效果,您应该将它们结合起来,和谐地使用。
方法论方法包括涉及架构、算法、存储模式和清理方法的所有设计决策;我们将在本节中查看这些方法,并与一些具体的、具体的情况相关联。
技术方法包括与所使用的数据库管理系统相关的设计方面,采用的集群配置,以及提供解决方案的可靠性。这些方面将在第 2.3 节中考虑。
这里提供了两个场景,以展示图在作为机器学习项目管道中管理大数据的价值以及从中提取洞察力。虽然这两个场景都与方法论方法相关,但它们也突出了将在本章后面讨论的技术方法的一些方面。
2.2.1 图在大数据中的价值
为了看到图在大数据全景中提供的价值,我们将探索一个复杂的用例,该用例需要处理大量数据才能有效。在这种情况下,图可以处理问题复杂性,提供一套完整的特征来存储、处理和分析数据。考虑以下场景:
你是一名警察。你如何通过使用从每个手机发送到(或接收自)所有可达基站的连续监测信号收集的蜂窝基站数据来追踪嫌疑人?
一篇由 Eagle、Quinn 和 Clauset 于 2009 年发表的有趣文章,探讨了使用从手机与可达的每个基站之间交换的连续监测信号收集的蜂窝基站数据的问题(图 2.10)。我们示例场景的目标是利用此类监测数据创建一个预测模型,该模型能够识别与主题生活相关的位置聚类,并根据主题的当前位置预测和预测后续移动。

图 2.10 手机与蜂窝基站通信
此方法的相关之处在于,它使用图作为从蜂窝基站获取的数据的折叠和组织方式,并创建了一个基于图的物化视图,以展示主题的移动。该图通过识别位置聚类的图算法进行分析。这些位置在分析管道中的下一个算法中使用,以构建位置预测模型。在本书的这一阶段,我不会描述算法的细节,因为场景的目的是展示如何使用图模型作为在复杂问题中预处理和组织数据的宝贵方法。图压缩信息,使其适合完成的分析。
对于此场景的心理模型,使用图模型来存储和管理数据源,在处理管道中使用图算法,并使用图来存储中间模型(图 2.11)。

图 2.11 与此场景相关的心理地图区域
今天使用的每部移动电话都能持续访问有关附近蜂窝基站的信息。研究这些数据流可以为用户的移动和行为提供有价值的见解。获取连续蜂窝基站数据的方法包括
-
在手机本身上安装日志应用程序以捕获连续数据流
-
使用(当可用时)来自蜂窝基站的原始连续数据
此示例使用连续数据聚合过程来合并特定手机的数据,因此我们将使用此场景中的第一个用例来简化描述。
每个受试者的电话记录在 30 秒间隔内记录最近的四个塔——信号最强的塔。一旦收集到这些数据,就可以表示为一个蜂窝塔网络(CTN),其中节点是唯一的蜂窝塔。如果两个节点在同一个记录中同时出现,则它们之间存在一条边,每条边的权重根据这对节点在所有记录中共同出现的总时间来计算。为每个受试者生成一个 CTN,包括受试者电话在监控期间记录的所有塔[Eagle, Quinn, and Clauset, 2009]。图 2.12 显示了单个受试者产生的图例。

图 2.12 单个受试者的 CTN 图表示
一个节点的强度是通过求和其所有边的权重来确定的。总边权重最高的节点识别出最常靠近受试者电话的塔。因此,高权重节点的组应该对应于受试者花费大量时间的地点。基于这个想法,可以通过不同的聚类算法将图分割成簇。(我们将在本书的第二部分开始讨论这些算法。)这种聚类过程的结果将类似于图 2.13。

图 2.13 CTN 的聚类视图
这些簇代表受试者花费大量时间的区域——可能是家、办公室、健身房或商店。跨越簇的关系,连接一个簇中的一个节点到另一个簇中的一个节点,代表从一个区域到另一个区域的过渡,例如早上从家到办公室,下午反向行程。
识别出的塔簇可以被转换为动态模型的状态。给定一个受试者访问的地点序列,可以学习他们的行为模式,并计算他们将来移动到各种地点的概率。(可以为此目的使用不同的技术,但这些技术超出了本次讨论的范围。)当预测模型计算出来后,可以使用它来预测受试者的未来移动,考虑到从同一数据源揭示的最后位置。
这种场景说明了方法论方法,其中使用图模型作为数据的有力视图,可以用作某些机器学习算法的输入或分析师的可视化工具。此外,因为图模型是无模式的,历史数据的聚合版本(通常数据量很大,表示受试者在每个位置花费的时间等)和实时值(受试者的电话当前能够到达的蜂窝塔,这表明他们现在的位置)可以存在于同一个模型中。
从具体模型中,可以抽象出一个更通用的方法。问题和过程可以概括如下:
-
存在大量以事件形式的数据(来自蜂窝基站或手机的监控数据)。
-
数据分布在多个数据源(每个蜂窝基站或手机)。
-
需要将数据聚合并组织成一种简化后续处理和分析(CTN)的格式。
-
从第一个聚合格式中,创建了某些视图(聚类算法和位置预测模型)。
-
同时,还需要存储一些最近事件的实时视图,以便快速对这些事件做出反应。
此数据流的一些重要和相关的方面会影响机器学习项目的架构:
-
手机或蜂窝基站记录的事件是原始的、不可变的和真实的。它们不会因为分析而改变;它们只是发生。如果一个手机向蜂窝基站发送了 ping 请求,这个事件不会因为不同的分析目的而改变。有必要一次性以原始格式存储这些事件。
-
在此数据上创建了多个视图,作为分析使用的算法的函数(聚合是一个例子),并且它们可以根据分析使用的算法而改变。
-
视图构建过程通常在数据集的整个集合上操作,这个过程可能需要时间,尤其是在处理大量数据时,如我们的具体用例。处理数据所需的时间在当前事件视图和先前事件视图之间产生了差距。
-
要有一个实时的数据视图,有必要填补这个差距。实时视图需要一个读取事件并向视图追加信息的流处理过程。
这里描述的架构问题通过 Nathan Marz 和 James Warren 在他们所著的《大数据:Lambda 架构》一书中介绍的一种特定类型的架构来解决,该架构“提供了一种通用的方法,可以在任意数据集上实现任意函数,并以低延迟返回函数的结果” [Marz and Warren, 2015]。Lambda 架构的核心概念是将大数据系统构建为三个层级的系列:批处理、服务和速度。架构方案在图 2.14 中展示。

图 2.14 Lambda 架构 [Marz and Warren, 2015]
每一层都满足一组需求,并建立在其他层提供的功能之上。一切始于查询 = 函数(所有数据)的等式。在我们的示例场景中,查询是“获取用户花费大量时间的地点。”
理想情况下,我们可以即时运行查询以获取结果。但由于需要处理的数据量以及要访问的数据源的分布式特性,这种方法通常不可行。此外,这将消耗大量资源,成本不合理,且耗时较长。它也不适合任何实时监控和预测系统。
最明显的替代方法是预先计算查询函数的结果或加速最终查询结果的中间值。让我们称预先计算的查询结果,无论是最终结果还是中间结果,为批量视图。我们不是即时计算查询,而是从这个视图中计算结果,这个视图需要以提供快速访问的方式存储。然后,前面的方程式如下拆分:
批量视图 = 函数(所有数据)
查询 = 函数(批量视图)
所有原始格式的数据都存储在批量层中。该层还负责原始数据的访问、批量视图的计算和提取。生成的视图存储在服务层,其中它们以适当的方式索引并在查询期间访问(图 2.15)。

图 2.15 批量层过程
在蜂窝基站场景中,第一个批量视图是 CTN,它是一个图。在此视图上操作的函数是图聚类算法,它产生另一个视图:聚类网络。图 2.16 显示了批量层生成作为批量视图的主题图。这些视图如图 2.12 所示,为每个监控对象进行计算。

图 2.16 批量层生成图视图
图 2.16 中的主要区别在于,批量视图和随后的实时视图都被建模为图。这种建模决策在几个场景中具有优势,在这些场景中,图表示不仅使我们能够更快地响应用户查询,而且也便于分析。在考虑的具体场景中,CTN 被创建为一个对所有收集到的原始蜂窝数据的函数,并支持聚类算法。
当批量层完成预计算批量视图时,服务层会进行更新。由于预计算需要时间,因此最新到达架构入口点的新数据在批量视图中没有得到表示。速度层通过提供一些较新数据的视图来解决此问题,填补了批量层和最新传入数据之间的差距。这些视图可以与服务层中的结构相同,也可以具有不同的结构并服务于不同的目的。
两个层之间的一大区别是,速度层只关注最近的数据,而批量层关注所有数据。在蜂窝基站场景中,实时视图提供了关于主体最后位置和位置之间转换的信息。
Lambda 架构是技术无关的;可以使用不同的方法来实现它。您使用的具体技术可能会根据要求而变化,但 Lambda 架构定义了选择这些技术并将它们连接起来以满足您要求的一致方法。
本节中提出的场景展示了如何将图模型作为服务层和速度层的一部分来使用。最终架构在图 2.17 中表示。

图 2.17 基于图的 Lambda 架构
这种新的 Lambda 架构子类型可以被定义为一种基于图的 Lambda 架构。在这种情况下,速度层仅跟踪每个主题最后已知的位置,该位置基于他们手机最后到达的蜂窝基站。然后,这些信息与聚类和预测模型结合使用,以预测主题未来将在何处。
图 2.17 架构中的主数据集可以通过任何数据库管理系统或可以存储大量数据的简单数据存储来存储。最常用的数据存储是 HDFS、Cassandra 和类似的 NoSQL 数据库管理系统。
在考虑的具体场景中,在以图的形式提取数据之前,需要合并数据。HDFS 提供了一个基于文件系统存储的基本访问机制,而 Cassandra 提供了更灵活的访问模式。存储图视图需要图数据库。此类数据库的通用特性和特定工具在 2.3 节中介绍。
基于图的 Lambda 架构和此处描述的场景是图在机器学习大数据分析中发挥重要作用的例子之一。在多个场景中,包括以下场景,可以使用相同的架构:
-
分析银行交易以检测欺诈
-
分析 Web 农场中的服务器日志以识别网络攻击
-
分析电话数据以识别人群社区
2.2.2 图对于主数据管理很有价值
在 2.2.1 节中,我们看到了如何使用图在主数据集(批量层)或实时(速度层)表示的部分数据上创建视图。在那个方法中,事务数据驻留在其他地方:在主数据集中。当您可以在服务层存储的聚合数据上查询和执行分析时,此选项很有用。
然而,其他类型的分析不能在数据的汇总版本上执行。这些算法需要更详细的信息才能有效;它们需要访问数据的细粒度版本来完成其工作。这种分析还可以使用图模型作为表示连接和利用数据洞察的一种方式。理解数据之间的连接并从中提取意义提供了经典基于图的分析方法所不具备的能力。在这种情况下,图是知识的主要来源,它模型化了一个单一连接的真实来源。这个概念带我们来到了我们的第二个示例场景:
您希望为银行创建一个简单但有效的欺诈检测平台。
银行和信用卡公司每年因欺诈而损失数十亿美元。传统的欺诈检测方法,如基于规则的,在最大限度地减少这些损失方面发挥着重要作用。但欺诈者不断开发越来越复杂的手段来逃避发现,使得基于规则的欺诈检测方法变得脆弱且很快过时。我们将重点关注一种特定的欺诈类型:信用卡盗窃。犯罪分子可以通过多种方式窃取信用卡数据,包括在自动取款机上安装的蓝牙数据读取设备、黑客的大规模入侵或通过收银员或餐厅工作人员的结账线刷卡的设备。甚至有权访问您的卡的人也可以在一张纸上偷偷记下相关信息[Villedieu, n.d.]。为了揭露这种欺诈,有必要确定“漏洞”的来源:信用卡窃贼及其活动地点。通过将信用卡交易表示为图,我们可以寻找共同点并追踪诈骗的起源点。与其他查看数据的方式不同,图是设计用来表达相关性的。图数据库可以揭示使用传统表示方法(如表格)难以检测到的模式。
假设表 2.3 中的交易数据库包含了一部分有争议交易的用户的交易数据。
表 2.3 用户交易⁵
| 用户标识符 | 时间戳 | 金额 | 商户 | 有效性 |
|---|---|---|---|---|
| 用户 A | 2018 年 2 月 1 日 | $250 | Hilton Barcelona | 无争议 |
| 用户 A | 2018 年 2 月 2 日 | $220 | AT&T | 无争议 |
| 用户 A | 2018 年 3 月 12 日 | $15 | Burger King New York | 无争议 |
| 用户 A | 2018 年 3 月 14 日 | $100 | Whole Foods | 有争议 |
| 用户 B | 2018 年 4 月 12 日 | $20 | AT&T | 无争议 |
| 用户 B | 2018 年 4 月 13 日 | $20 | Hard Rock | 无争议 |
| 用户 B | 2018 年 4 月 14 日 | $8 | Burger King New York | 无争议 |
| 用户 B | 2018 年 4 月 20 日 | $8 | Starbucks | 有争议 |
| 用户 C | 2018 年 3 月 5 日 | $15 | Whole Foods | 无争议 |
| 用户 C | 2018 年 5 月 5 日 | $15 | Burger King New York | 无争议 |
| 用户 C | 2018 年 5 月 12 日 | $15 | Starbucks | 有争议 |
从这个交易数据集开始,让我们定义一个图模型。每笔交易涉及两个节点:一个人(客户或用户)和一个商人。节点通过交易本身相互连接。每笔交易都有一个日期和状态:无争议表示合法交易,争议表示报告的欺诈交易。图 2.18 将数据表示为图。
在这个图中,红色连接是争议交易;其他的是常规(无争议)交易。生成的图很大,但图的维度不会影响必须执行的分析类型。从这样的数据集开始,检测欺诈来源的分析步骤是
-
过滤欺诈交易。识别攻击中涉及的人和卡。
-
找出欺诈的源头。搜索欺诈开始之前的所有交易。
-
隔离小偷。识别一些共同模式,例如共同的商人,这可能是欺诈的起源。

图 2.18 信用卡欺诈检测的示例图模型
根据图 2.18 中的示例图,欺诈交易和受影响的人列在表 2.4 中。
表 2.4 争议交易
| 用户标识符 | 时间戳 | 金额 | 商人 | 有效性 |
|---|---|---|---|---|
| 用户 A | 14/03/2018 | $100 | Whole Foods | 争议 |
| 用户 B | 20/04/2018 | $8 | 星巴克 | 争议 |
| 用户 C | 12/05/2018 | $15 | 星巴克 | 争议 |
所有这些交易发生在不同的月份。现在,对于每个用户,让我们考虑在争议交易日期之前执行的交易,以及相关的商人。结果如表 2.5 所示。
表 2.5 争议交易之前的所有交易
| 用户标识符 | 时间戳 | 金额 | 商人 | 有效性 |
|---|---|---|---|---|
| 用户 A | 01/02/2018 | $250 | 希尔顿巴塞罗那 | 无争议 |
| 用户 A | 02/02/2018 | $220 | AT&T | 无争议 |
| 用户 A | 12/03/2018 | $15 | 汉堡王纽约 | 无争议 |
| 用户 B | 12/04/2018 | $20 | AT&T | 无争议 |
| 用户 B | 13/04/2018 | $20 | 硬石 | 无争议 |
| 用户 B | 14/04/2018 | $8 | 汉堡王纽约 | 无争议 |
| 用户 C | 03/05/2018 | $15 | Whole Foods | 无争议 |
| 用户 C | 05/05/2018 | $15 | 汉堡王纽约 | 无争议 |
让我们将交易按商店分组。结果列在表 2.6 中。
表 2.6 聚合交易
| 商人 | 数量 | 用户 |
|---|---|---|
| 汉堡王纽约 | 3 | [用户 A, 用户 B, 用户 C] |
| AT&T | 2 | [用户 A, 用户 B] |
| Whole Foods | 1 | [用户 A] |
| 硬石 | 1 | [用户 B] |
| 希尔顿巴塞罗那 | 1 | [用户 A] |
从这张表中可以清楚地看出,小偷在汉堡王餐厅进行操作,因为这是所有用户都有的唯一商人,并且因为每次欺诈都是在那里交易之后开始的。
从这个结果开始,分析可以进一步深入,使用图形来寻找其他类型的模式,并将结果转换为一种下降行动,防止从识别的源头进行任何进一步的交易,直到进一步的调查完成。
在这个场景中,图被用来存储分析所使用的单一真实来源,数据可以以图形的形式进行可视化,以便进行进一步的分析和调查。相关的心智模型如图 2.19 所示。

图 2.19 与此场景相关的我们心智地图的区域
这个例子是对揭示欺诈方法的简化,但它展示了使用图数据库进行此类分析的一些优点。这些优点可以总结如下:
-
多个数据源,如地理或 GPS 信息、社交网络数据、用户个人资料、家庭数据等,可以合并到一个单一的真实来源中。
-
可以用外部知识来源(商店位置、人们的地址等)或上下文信息(新商店、其他投诉等)来扩展现有数据,这些信息可以用以提高分析。
-
相同的数据模型可以支持多种分析技术(例如,揭露欺诈团伙⁶)。
-
数据可以以图形的形式可视化,以加快手动分析的速度。
-
分析可以扩展到多个交互级别,考虑到多个跳数。
-
该结构简化了合并和清理操作,这得益于图形模型提供的灵活访问模式。
在欺诈分析场景中,图代表了合并、清理和扩展数据的主要知识来源,分析是在其上进行的,并且任何决策都是基于它做出的。与第 2.2.1 节中描述的基于图形的 Lambda 架构不同,在这里,图扮演了主数据集的角色,是主数据管理(MDM)的基础——识别、清理、存储和管理数据[Robinson 等人,2015]。MDM 的关键关注点包括
-
随着组织结构的变化、企业的合并和业务规则的演变,管理变化。
-
结合新的数据来源
-
用外部数据源补充现有数据
-
满足报告、合规性和商业智能消费者的需求
-
随着其值和模式的变化,版本化数据。
MDM 不是数据仓库(DW)的替代品或现代版本,尽管这两种实践有很多共同之处。DW 与历史数据的存储有关,而 MDM 处理当前数据。MDM 解决方案包含公司内所有业务实体的当前和完整信息。DW 仅包含用于某种静态分析的历史数据。当正确实施时,MDM 具有许多优势,可以概括如下:
-
简化人员与部门之间的数据共享
-
促进在多个系统架构、平台和应用中的计算
-
消除数据中的不一致性和重复
-
减少在搜索信息时产生的无谓挫折
-
简化业务流程
-
提高组织内的沟通效率
此外,当适当的 MDM 解决方案到位时,可以更多地相信系统提供的数据分析,从而提高基于该数据的决策的信心。在这种情况下,图数据库“并不提供完整的 MDM 解决方案;然而,它们非常适合于层次结构、主数据元数据和主数据模型的建模、存储和查询。这些模型包括类型定义、约束、实体之间的关系以及模型与底层源系统之间的映射” [Robinson 等人,2015]。
基于图的 MDM 具有以下优势:
-
灵活性—捕获的数据可以轻松更改,以包括额外的属性和对象。
-
可扩展性—该模型允许主数据模型随着业务需求的变化而快速演变。
-
搜索能力—每个节点、每个关系以及它们的所有相关属性都是搜索入口点。
-
索引能力—图数据库自然由关系和节点索引,与关系型数据相比提供更快的访问速度。
基于图的 MDM 解决方案解决了不同类型的功能。在欺诈检测场景中,以及本书的其余部分,它被视为分析/机器学习平台的一部分,作为主要数据源,并由生成的模型扩展,代表了基于图 Lambda 架构的替代方法。
图可以表示存储用于训练目的的数据的 MDM 系统的一个有趣场景是推荐引擎。在这种情况下,图可以存储包含用户与项目之间交互历史的用户-项目矩阵。我们将在第三章中更详细地考虑这个场景。
2.3 图数据库
第 2.2 节介绍了一些使用图的机器学习方法,并展示了如何将图作为数据存储和访问模型来增强预测分析的具体示例。为了以最佳方式使用这些模型,您必须能够以与您在数据流或算法中思考它们相同的方式存储、操作和访问图。为了完成这项任务,您需要一个图数据库作为存储引擎。图数据库允许您存储和操作实体(也称为节点)以及这些实体之间的连接(也称为关系)。
本节描述了与图管理相关的技术方面。这种视角在机器学习项目的生命周期中是相关的,在此期间您必须操作、存储和访问真实数据。此外,在大多数情况下,您将处理大数据,因此您必须考虑可伸缩性问题。分片(在多个服务器上水平分割数据)和复制(为了高可用性和可伸缩性目的在多个服务器上复制数据)在此背景下介绍。
许多图数据库都可用,但并非所有都是原生(从头开始为图构建的);相反,它们在非图存储模型之上提供了一个图“视图”。这种非原生方法会导致存储和查询时的性能问题。另一方面,适当的原生图数据库使用图模型来存储和处理数据,使得图操作简单、直观且高效。关键差异在第 2.3.4 节中突出显示。
在许多情况下,我可以说几乎总是如此,因为您至少需要一个节点标识,可能还需要为节点和关系添加一些属性。换句话说,有必要将节点分组在同一类别中。这些“特征”显著提高了图数据库的表达能力和建模能力。满足此类需求的标签属性图在第 2.3.5 节中介绍。
尽管本书中展示的所有理论、示例和用例都是完全技术无关的,但我们将使用 Neo4j(附录 B 中介绍)作为参考数据库平台。Neo4j 不仅是最少数几个提供高性能的适当图数据库之一,而且还有一个功能强大且直观的查询语言,称为 Cypher。⁷
2.3.1 图数据库管理
在机器学习项目中使用图需要您存储、访问、查询和管理这些图中的每一个。所有这些任务都属于图数据库管理的范畴,如图 2.20 所示。

图 2.20 图数据库管理任务
此图显示了可以将图数据管理任务分为的三个主要区域:
-
图建模—从一般意义上讲,图模型是数据库系统背后的基本抽象——用于建模现实世界实体及其之间关系的概念工具。图结构的一个主要特征是建模非结构化数据的简单性。在图模型中,模式与数据之间的分离不如在经典的关系模型中那么显著[Angles and Gutierrez, 2017]。同时,图模型是灵活和可扩展的。在图模型中,现实或问题的同一方面可以以多种方式映射。不同的模型可以从不同的角度解决不同的问题,因此定义正确的模型需要努力和经验。幸运的是,图的“无模式”特性意味着模型,即使在项目的早期阶段定义的模型,也可以相对容易地进行更改。另一方面,当你使用其他类型的 NoSQL 数据库或关系数据库时,模型的变化可能需要完全重新导入。在大数据中,这种操作可能从金钱、时间和努力的角度来看是一项昂贵的任务。此外,模型设计会影响在图上执行的所有查询和分析的性能。因此,建模是数据管理的一个关键方面。本章前面已描述了一些模型示例,在接下来的章节中,我们将探讨几个更多的用例,并考虑相关模型的优缺点。
-
图存储—当模型定义完成后,数据必须存储在一个持久层中。图数据库管理系统(Graph DBMSs)专门设计用于管理类似图的数据,遵循数据库系统的通用原则:持久数据存储、内存使用、缓存、物理/逻辑数据独立性、查询语言、数据完整性和一致性等。此外,图数据库供应商还需要负责与可扩展性、可靠性和性能相关的所有方面,例如备份、恢复、横向和纵向可扩展性以及数据冗余。在本节中,我们将讨论图数据库管理系统(Graph DBMSs)的关键概念——特别是那些影响模型以及在处理过程中访问数据的方式的概念。
-
图处理—这些任务涉及用于处理和分析图的框架(例如工具、查询语言和算法)。有时,处理图需要使用多台机器以提高性能。一些处理功能,如查询语言和某些图访问模式,在图数据库管理系统(Graph DBMS)本身中可用;其他则作为算法或外部平台提供,这些平台必须建立在图和图数据库管理系统之上。
图处理是一个广泛的话题,相关任务可以大致分为几个类别。Özsu [2015] 提出了一种有趣的图处理分类方法,该方法沿三个维度组织:图动态性、算法类型和工作负载类型(见图 2.21)。

图 2.21 属性图处理分类法 [Özsu, 2015]
图处理这一复杂主题将在整本书中讨论。将介绍不同的算法,并将它们与特定的实际用例或应用实例相对应,这些实例展示了它们在提取洞察力方面的有用性。
2.3.2 分片
从纯粹的数据存储角度来看,大数据应用面临的主要四个 V 挑战是
-
数据量——涉及的数据量如此之大,以至于难以存储在单台机器上。
-
速度—单台机器只能服务有限数量的并发用户。
尽管垂直扩展——如增加计算、存储和内存资源——可以作为一种临时解决方案来处理大量数据并提高多个并发用户的响应时间,但数据最终会变得太大,无法存储在单个节点上,用户的数量也会太高,无法由单台机器处理。
在 NoSQL 数据库中,一种常见的扩展技术是分片,其中大型数据集被分割,子集被分布到不同服务器上的多个分片中。这些分片或子集通常在多个服务器上复制,以提高可靠性和性能。分片策略决定了哪些数据分区应该发送到哪些分片。分片可以通过各种策略实现,这些策略可以由应用程序或数据存储系统本身管理。
对于面向聚合的数据模型,如键/值、列族和文档数据库[Fowler and Sadalage, 2012],在这些模型中,表达概念之间关系的方式是将它们聚合在单个数据条目中,使用值或文档,这种解决方案是合理的。在这些类型的存储中,用于检索任何项目的键是已知且稳定的,查找机制快速且可预测,因此将想要存储或检索数据的客户端定向到适当的分片是直接的[Webber, 2011]。
另一方面,图数据模型高度关系导向。每个节点都可以与任何其他节点相关联,因此图没有可预测的查找。它还具有高度可变的结构:在只有少量新链接和新节点的情况下,连接结构可以发生很大变化。在这些情况下,对图数据库进行分片并不简单[Webber, 2011]。一个可能的解决方案是将相关的节点和相关的边放置在同一位置。这种解决方案将提高图遍历性能,但同一数据库分片上有太多连接的节点会使它负载过重,因为大量数据将位于同一分片上,导致其不平衡。图 2.22 和 2.23 说明了这些概念。
图 2.22 展示了在导航一个图时可能需要多次跨越分片边界。这种跨分片遍历非常昂贵,因为它需要许多网络跳转,导致查询时间显著增加。在这种情况下,与所有操作都在同一分片上的情况相比,性能会迅速下降。

图 2.22 不同分片间的关系遍历
在图 2.23 中,为了克服这个问题,相关的节点被存储在同一分片上。图遍历更快,但分片间的负载极不平衡。此外,由于图具有动态特性,图及其访问模式在运行时可能会迅速且不可预测地发生变化,这使得在实际中实施这种解决方案不太方便。
考虑到这些挑战,一般来说,有三种技术可以用于扩展图数据库:

图 2.23 单个分片(分片 2)的过载
-
应用级分片——在这种情况下,数据的分片是在应用层面通过使用领域特定知识完成的。对于一个全球业务,与北美相关的节点可以创建在一个服务器上,而与亚洲相关的节点可以位于另一个服务器上。这种应用级分片需要理解节点存储在物理上不同的数据库中。分片也可以基于必须对数据进行的不同类型分析或图处理。在这种情况下,每个分片包含执行算法所需的所有数据,并且一些节点可以在分片之间进行复制。图 2.24 描述了应用级分片。
-
增加 RAM 或使用缓存分片——可以垂直扩展服务器,添加更多 RAM,使整个数据库适合内存。这种解决方案使图遍历非常快,但对于大型数据库来说既不合理也不可行。在这种情况下,可以采用一种称为缓存分片的技术,以保持高性能,同时数据集的大小远超过主内存空间。缓存分片在传统意义上不是分片,因为我们期望完整的数据集存在于每个数据库实例上。为了实现缓存共享,我们将每个数据库实例承担的工作量进行分区,以增加给定请求命中热缓存的可能性。(在像 Neo4j 这样的图数据库中,热缓存性能非常高。)
-
复制——可以通过添加更多(相同的)数据库副本来实现数据库的扩展,这些副本作为只读访问的跟随者。当您将相对较高数量的只读跟随者数据库实例与少量领导者数据库实例配对时,可以实现高度的扩展性。这种技术在第 2.3.3 节中描述。其他技术有其优缺点,这里不予讨论。

图 2.24 应用级分片隔离
在某些情况下,分片比其他情况更有效。考虑本章前面讨论的两个场景:
-
在蜂窝基站监控示例中,为每个监控对象创建一个图,因此机器学习模型会产生多个独立的图,这些图将单独访问。在这种情况下,应用级分片是一个简单的任务,因为所有图都是隔离的。为了推广,在基于图的 Lambda 架构场景中,在同一个数据集上创建了多个图视图,我们可以将视图存储在多个数据库实例中,因为它们以独立的方式访问。
-
在第二个用例(欺诈检测)中,分片会很棘手,因为在理论上,所有节点都可以连接。可以应用一些启发式方法来减少跨分片遍历,或将频繁访问的节点保持在同一分片上,但图不能像前一个用例那样分成多个独立的图。在这种情况下,另一种选择是使用复制来扩展读取性能并加快分析时间。
2.3.3 复制
如第 2.3.2 节所述,在图数据库中,分片是一个困难的任务。处理速度和可用性的一个有效替代方案是复制。数据复制包括在多个不同的计算机上维护数据的多个副本,这些副本被称为副本。复制有几个目的 [Özsu and Valduriez, 2011]:
-
系统可用性——通过使数据项可以从多个站点访问,复制从分布式数据库管理系统中移除了单点故障。即使某些集群节点关闭,数据也应保持可用。
-
性能——复制使我们能够通过将数据定位在其访问点附近来减少延迟。
-
可扩展性——复制允许系统在地理上和访问请求的数量上增长,同时保持可接受的响应时间。
-
应用需求——作为它们操作规范的一部分,应用程序可能需要维护数据的多个副本。
数据复制具有明显的优势,但保持不同副本同步是一个挑战。在定义复制协议时,数据库更新首先在哪里执行是一个基本的设计决策。这些技术可以按以下方式描述:
-
集中式如果它们首先在主副本上执行更新。集中式技术可以根据系统中的所有数据项只有一个主数据库副本被进一步识别为单主节点,或者当每个数据项或数据项集可以有一个单独的主副本时,被识别为主副本。
-
分布式如果它们允许对任何副本进行更新。
由于图的高度连通性,实现集中式主副本协议或分布式协议是一项困难的任务,这对性能和数据一致性有严重影响。(在图中,数据项可以是节点或关系;根据定义,关系连接到两个其他数据项——节点,而节点可能通过多个关系与其他节点连接。)因此,我们将重点关注具有单个主节点的集中式方法,也称为主/从复制。
在这种方法中,一个节点被指定为数据的主授权源,被称为主节点、领导者或主副本。这个节点通常负责处理对该数据的任何更新。即使从节点接受写入操作,这些操作也必须通过主节点执行(见图 2.25)。如果大部分数据访问都是读取操作,主/从复制非常有用。通过添加更多从节点并将所有读取请求路由到从节点,可以实现水平扩展。主/从复制还提供了读取弹性:如果主节点失败,从节点仍然可以处理请求 [Fowler and Sadalage, 2012]。

图 2.25 基于 master/slave 协议的复制
大多数主/从协议的实现允许从节点在当前主节点不可用时投票选择不同的主节点。这种方法增加了架构堆栈的可用性和可靠性。具体来说,在一个机器学习项目中,复制允许在训练或预测阶段将读取负载分散到所有节点。
2.3.4 原生与非原生图数据库
本书描述了多种方法,通过这些方法图可以增强机器学习项目。为了从图模型中获得最大的优势,需要一个合适的图数据库管理系统来存储、访问和处理图。尽管模型本身在多个图数据库实现中相对一致,但在不同的数据库引擎中编码和表示图的方法有很多。一个旨在处理整个计算堆栈上的图工作负载的数据库管理系统,从查询语言到数据库管理引擎和文件系统,从集群到备份和监控,被称为 原生图数据库 [Webber, 2018]。原生图数据库被设计为以理解并支持图的方式使用文件系统,这意味着它们不仅对图工作负载性能高,而且安全。更详细地说,原生图数据库管理系统具有一种称为 无索引邻接 的属性,这意味着每个节点都维护对其相邻节点的直接引用。邻接表表示是表示稀疏图最常见的方法之一。
从形式上讲,这种表示图 G = (V, E) 的方法由一个邻接表数组 Adj 组成,每个顶点 V 中都有一个列表。对于 V 中的每个顶点 u,邻接表 Adj[u] 包含所有与 u 相邻的顶点 v,其中在 E 中存在边 E[uv]。换句话说,Adj[u] 包含 G 中与 u 相邻的所有顶点 [Cormen 等人,2009]。
图 2.26(b) 是图 2.26(a) 中无向图的邻接表表示。例如,顶点 1 有两个邻居,2 和 5,因此 Adj[1] 是列表 [2,5]。顶点 2 有三个邻居,1、4 和 5,因此 Adj[2] 是 [1,4,5]。其他列表以相同的方式创建。这里值得注意的是,由于关系没有顺序,列表中也没有特定的顺序;因此,Adj[1] 也可以是 [2,5] 或 [5, 2]。

图 2.26 一个无向图(a)及其作为邻接表的表示(b)
同样,图 2.27(b) 是图 2.27(a) 中有向图的邻接表表示。这种列表被表示为链表,其中每个条目都包含对下一个条目的引用。在节点 1 的邻接表中,第一个元素是节点 2,对下一个元素的引用是节点 5 的元素。这种方法是存储邻接表最常见的方法之一,因为它使得添加和删除元素变得高效。在这种情况下,我们只考虑出边关系,但我们可以用相同的方法处理入边关系;重要的是要选择一个方向并在创建邻接表的过程中保持一致性。在这里,顶点 1 只有一个出边关系,与顶点 2 相连,因此 Adj[1] 将是 [2]。顶点 2 有两个出边关系,与 4 和 5 相连,因此 Adj[2] 是 [4,5]。顶点 4 没有出边关系,因此 Adj[4] 是空的 ([])。

图 2.27 一个有向图(a)及其相关的邻接表表示(b)
如果 G 是一个有向图,所有邻接表长度的总和是|E|。因为每条边只能沿一个方向遍历,所以 E[uv]只会出现在 Adj[u]中。如果 G 是一个无向图,所有邻接表长度的总和是 2 × |E|,因为如果 E[uv]是一个无向边,E[uv]将出现在 Adj[u]和 Adj[v]中。无向或有向图的邻接表表示所需的内存与|V| + |E|成正比。
通过在 Adj[u]中存储边 E[uv]的权重 w,邻接表可以很容易地适应表示加权图。邻接表表示法也可以类似地修改以支持许多其他图变体。在这种表示中,每个节点都充当其他附近节点的微观索引,这比使用全局索引便宜得多。在这样的数据库中,跨关系的遍历具有恒定的成本,与图的大小无关。此外,查询时间与图的总大小无关;相反,它们与搜索的图量成正比。
另一种选择是非原生图数据库。这个组中的数据库系统可以分为两类:
-
在现有不同数据结构(如键/值、关系型、文档型或基于列的存储)之上叠加图 API 的那些
-
声称多模型语义的那些,即一个系统据说可以支持几种数据模型
非原生图引擎针对的是替代存储模型,如列式、关系型、文档型或键/值数据,因此在处理图时,数据库管理系统必须执行昂贵的转换,从数据库的主模型转换到图模型。实施者可以通过极端的反规范化来尝试优化这些转换,但这种方法通常会导致查询图时的高延迟。换句话说,非原生图数据库在现实情况下永远不会像原生图数据库那样高效,简单的理由是转换过程是必需的。
理解图是如何存储的有助于你为其定义一个更好的模型,其中图数据库的“原生”特性至关重要。这一关注点也与本书的哲学思想相关:
在一个成功的机器学习项目中,每个方面都与向最终用户提供高效和性能良好的服务相关,其中“高效”和“性能良好”不仅意味着“准确”,还意味着“按时交付”。
例如,一个网站上的高度准确的推荐,如果它在 30 秒内送达,那么它将是无用的,因为到那时,用户可能已经去了别处。
有时,这些方面被认为次要。一个常见的误解是,非本地图技术已经足够好。为了更好地理解在机器学习项目中数据库引擎对图的原生支持的价值,让我们考虑一个例子:
您必须实现一个供应链管理系统,该系统分析整个链以预测未来的库存问题或发现网络中的瓶颈。
供应链管理专业协会将供应链管理定义为“涵盖所有涉及采购、转换以及所有物流管理活动的计划和管理工作。”⁸ 供应链可以自然地建模为图,如图 2.28 所示。

图 2.28 供应链网络
假设现在您想使用基于全局索引的关系数据库或任何其他 NoSQL 数据库来存储供应链网络模型。供应链中元素之间的关系如图 2.29 所示。

图 2.29 存储供应链网络的表格模型
如图中所示,这些索引为每次遍历添加了一层间接层,从而增加了计算成本。要找到生产完成后 B 型成品将运往何处,我们首先必须执行索引查找,成本为 O(log n),⁹,然后获取链中下一个节点的列表。这种方法对于偶尔或浅层查找可能是可接受的,但当我们反转遍历方向(例如,找到创建 C 型成品所需的中间步骤)时,它很快就会变得难以忍受地昂贵。
假设现在原材料 A 被污染或不再可用,我们需要找到受此问题影响的链中所有产品或商店。我们必须执行多次索引查找,每次查找针对可能位于原材料和商店之间链中的每个节点,这使得成本变得更为沉重。而找到 B 型成品将运往何处的成本为 O(log n),要遍历 m 步网络,索引方法的成本为 O(m log n)。在一个具有无索引邻接关系的本地图数据库中,双向连接实际上是预先计算并存储在数据库中的关系,如图 2.30 所示。

图 2.30 存储供应链的基于图模型
在这种表示中,当你有第一个节点时,遍历关系的成本为 O(1),它直接指向下一个节点。执行之前所需的相同遍历现在只需 O(m)。不仅图引擎更快,而且成本仅与跳数(m)有关,而不是与关系的总数(n)有关。
假设现在你需要识别供应链中的瓶颈。在网络上发现瓶颈的一种常见方法是 中介中心性,它是一种基于计算节点之间最短路径的图中心性(重要性)度量。每个节点的中介中心性是通过该节点经过的最短路径的数量。在这种情况下,索引查找的成本——O(log n)将对计算性能产生重大影响。
总结一下,原生图架构提供了许多优势,使其在管理图模型方面通常优于非原生方法。我们可以将这些优势总结如下:
-
“分钟到毫秒”的性能—原生图数据库处理连接数据查询的速度远快于非原生图数据库。即使在普通的硬件上,原生图数据库也可以轻松处理每秒数百万次图上节点的遍历,以及每秒数千次的事务性写入 [Webber, 2017]。
-
读效率—原生图数据库可以在没有复杂模式设计和查询优化的情况下,通过无索引邻接实现常数时间遍历。直观的属性图模型消除了创建任何额外且通常复杂的应用程序逻辑来处理连接的需求 [Webber, 2017]。
-
磁盘空间优化—为了提高非原生图性能,可以非规范化索引或创建新索引,或者两者结合,这将影响存储相同信息所需的空间量。
-
写效率—索引非规范化也会对写性能产生影响,因为所有这些额外的索引结构也需要更新。
2.3.5 标签属性图
用于表示复杂网络的图需要存储比简单的节点和关系列表更多的信息。幸运的是,这种简单的结构可以很容易地扩展到一个更丰富的模型,该模型包含以 属性 形式存在的附加信息。此外,有必要将节点分组到类中,并为不同类型的关系分配不同的类型。图数据库管理系统提供商引入了 标签属性图模型,将一组属性与图结构(节点和关系)关联,并为节点和关系添加类或类型。这种数据模型允许具有任何数据库管理系统典型查询功能的更复杂的查询集,例如投影、过滤、分组和计数。
根据 openCypher 项目,¹⁰ 标签属性图被定义为“一个有向、顶点标记、边标记的多重图,¹¹ 其中边具有自己的标识。”在属性图中,我们用 节点 来表示顶点,用 关系 来表示边。
属性图具有以下属性(以下以平台无关的方式定义):
-
图由一组实体组成。实体代表一个节点或一个关系。
-
每个实体都有一个标识符,它在整个图中唯一地标识它。
-
每个关系都有一个方向、一个名称,该名称标识关系的类型、一个起始节点和一个结束节点。
-
实体可以有一组属性,这些属性通常表示为键/值对。
-
节点可以被标记为一个或多个标签,这些标签将节点分组并指示它们在数据集中的角色。
属性图仍然是一个图,但通信能力比以前更强。在图 2.31 中,你可以轻松地看到,亚历山德罗·佩尔森为 GraphAware 公司工作,正如米哈尔和克里斯托夫一样。name 是节点 Person 的属性,而 start_date 和 role 是关系 WORKS_FOR 的属性。每个 Person 的国籍是通过使用关系 HAS_NATIONALITY 存储的,该关系将 Person 连接到一个具有存储国家名称属性的 Country 节点。
对于关系数据库,定义图模型有一些最佳实践或风格规则。节点的标签应该是单数,例如,因为它们代表一个特定的实体,而关系的名称应该反映方向。
显然,表示同一组概念的方法有很多种。例如,在图 2.31 中的模型中,国籍可以存储为 Person 节点的属性。根据访问模式和底层图数据库管理系统(DBMS)的具体需求,模式可能会发生显著变化。在本书的第二部分,我们将看到许多表示数据的方法,每种方法都有特定的范围并满足特定目标应用的需求。

图 2.31 属性图
摘要
本章描述了与机器学习应用数据管理相关的一些挑战,并讨论了图模型如何帮助解决这些挑战。本章通过使用具体场景和相关图解决方案的描述来具体说明。你学到了以下内容:
-
如何处理大数据的四个 V:量(volume)、速度(velocity)、多样性(variety)和真实性(veracity)。四 V 模型描述了机器学习项目在数据规模、新数据生成的速度、数据展现出的异构结构和数据来源的不确定性等方面面临的多个关键问题。
-
如何设计架构以处理大量训练数据。预测分析和机器学习通常在训练过程中需要大量数据才能有效。拥有更多数据胜过拥有更好的模型。
-
如何设计一个使用图来存储数据视图的适当 Lambda 架构。在基于图的 Lambda 架构中,图模型用于存储和访问批量或实时视图。这些视图代表预先计算且易于查询的主数据集视图,其中包含原始格式的原始数据。
-
如何规划您的 MDM 平台。MDM 是指识别、清理、存储以及(最重要的是)管理数据的过程。在此背景下,图模型在数据模型中提供了更多的灵活性和可扩展性,同时具备搜索和索引功能。
-
如何选择适合应用程序需求的复制模式。复制允许您在图数据集群的多个节点之间分配分析负载。
-
原生图数据库的优势是什么。与非原生图数据库相比,原生图数据库管理系统更可取,因为它们将模型(我们表示数据的方式)与底层数据引擎一一映射。这种匹配允许更好的性能。
参考文献
[Angles 和 Gutierrez, 2017] Angles, Renzo, 和 Claudio Gutierrez. “图数据管理简介。” arXiv, 2017 年 12 月 29 日。arxiv.org/pdf/1801.00036.pdf.
[Bahga 和 Madisetti, 2016] Bahga, Arshdeep 和 Madisetti, Vijay K. 大数据科学与分析:动手实践方法。VPT, 2016。
[Corbo 等人,2017] Corbo, Jacomo, Carlo Giovine, 和 Chris Wigley. “在金融机构打击欺诈中应用分析。” McKinsey & Company, 2017 年 4 月。mng.bz/xGZq.
[Cormen 等人,2009] Cormen, Thomas H., Charles E. Leiserson, Ronald L. Rivest, 和 Clifford Stein. 算法导论。第 3 版。波士顿,MA: MIT Press, 2009。
[Coyle, 2016] Coyle, Peadar. “数据科学家访谈:Greg Linden。” 2016 年 10 月 12 日。mng.bz/l2Zz.
[Domo, 2020] Domo. “数据永不眠 8.0。” 2020。www.domo.com/learn/data-never-sleeps-8.
[Eagle, Quinn 和 Clauset, 2009] Eagle, Nathan, John A. Quinn, 和 Aaron Clauset. “连续蜂窝塔数据分析方法。” 第 7 届国际普适计算会议论文集 (2009): 342-353.
[Fowler 和 Sadalage, 2012] Fowler, Martin, 和 Pramod J. Sadalage. NoSQL 精粹:多语言持久性的新兴世界简明指南。Upper Saddle River, NJ: Addison-Wesley Professional, 2012。
[Gatner, 2017] Gartner. “主数据管理(MDM)。” 2017。mng.bz/rmZE.
[Health Data Archiver, 2018] Health Data Archiver (2018). “健康数据量激增,传统数据归档在增加。” 2018 年 8 月 3 日。mng.bz/dmWz.
[Johnson, 2013] Johnson, Ralph. “每天产生 2500 亿字节的数据。CPG 和零售如何管理它?” IBM 消费品行业博客,2013 年 4 月 24 日。
[Jürgensen, 2016] Jürgensen, Knut. “主数据管理(MDM):是帮助还是阻碍?” Redgate Hub,2016 年 5 月 16 日。mng.bz/ZY9j。
[Laney, 2001] Laney, Douglas. “3D 数据管理:控制数据量、速度和多样性。” META Group,2001 年 2 月 6 日。mng.bz/BKzq。
[Marz 和 Warren, 2015] Marz, Nathan, 和 James Warren. 大数据。纽约州舍尔特岛:Manning,2015 年。
[Nelson, 2016] Nelson, Patrick. “一辆自动驾驶汽车每天将使用 4,000 GB 的数据。” 网络世界,2016 年 12 月 7 日。mng.bz/Paw2。
[Özsu, 2015] Özsu, M. Tamer. “图数据管理和分析概述。” ADC 博士学校,2015 年 6 月 4 日。hkbutube.lib.hkbu.edu.hk/st/display.php?bibno= b3789774。
[Özsu 和 Valduriez, 2011] Özsu, M. Tamer, 和 Patrick Valduriez. 分布式数据库系统原理。第 3 版。纽约:Springer,2011 年。
[Parkin, 2018] Parkin, Simon. “对虚假评论的永无止境的战争。” 《纽约客》,2018 年 5 月 31 日。mng.bz/1A5Z。
[Puget 和 Thomas, 2016] Puget, Jean Francois, 和 Rob Thomas. “机器学习的实用指南:理解、区分和应用。” IBM 社区,2016 年 8 月 16 日。
[Robinson 等人,2015] Robinson, Ian, Jim Webber, 和 Emil Eifrem. 图数据库。第 2 版。加利福尼亚州塞巴斯蒂波尔:O’Reilly,2015 年。
[Rund, 2017] Rund, Ben. “关于图数据库在主数据管理(MDM)中的优点、缺点和炒作。” TDWI,2017 年 3 月 14 日。mng.bz/VG9r。
[Shafer, 2017] Shafer, Tom. “大数据和数据科学的 42 个特征。” Elder Research,2017 年 4 月 1 日。www.elderresearch.com/blog/42-v-of-big-data。
[Shannon, 2017] Shannon, Sarah. “更新至 2018 年:大数据的五大特征:它们如何帮助您的业务?” XSI,2017 年 2 月 15 日。
[Villedieu, n.d.] Villedieu, Jean. “GraphGist:信用卡欺诈检测。” Neo4j。mng.bz/A1GE。
[Webber, 2011] Webber, Jim. “关于图数据库分片。” World Wide Webber,2011 年 2 月 16 日。
[Webber, 2017] Webber, Jim. “原生图数据库的动机。” 2017 年 5 月 17 日。mng.bz/2z6N。
[Webber, 2018] Webber, Jim. “并非所有图数据库都是相同的:为什么您需要一个原生图数据库。” 数据库趋势与应用,2018 年 3 月 7 日。mng.bz/RKwn。
[Wilder-James, 2012] Wilder-James, Edd. “什么是大数据?大数据领域的介绍。” www.oreilly.com/ideas/what-is-big-data。
[Woodie, 2016] Woodie, Alex. “Neo4j 将图数据库的节点限制推至超过一千万亿个。” Datanami,2016 年 4 月 26 日。mng.bz/Jvwp。
(1.) “激光雷达的工作原理与雷达类似,但它不是发送无线电波,而是发射红外光脉冲——即人眼看不见的激光——并测量它们击中附近物体后返回所需的时间。”来源:mng.bz/jBZ9。
(2.) Puget 和 Thomas [2016]。
(3.) “洞察引擎应用相关性方法来描述、发现、组织和分析数据。这使得现有或合成信息能够主动或交互式地传递,并在数字工作者、客户或利益相关者的及时业务时刻传递。”(来源:mng.bz/WrwX)
(4.) Parkin [2018]。
(5.) 这里使用的商家名称作为示例,以使用例更加具体。
(6.) 根据《法律词典》(thelawdictionary.org/fraud-ring),“欺诈团伙”是指“一个专注于欺诈人们的组织。伪造、虚假陈述、窃取身份、伪造支票和货币都是欺诈行为。”
(7.) www.opencypher.org。
(8.) mng.bz/8WXg
(9.) 大 O 符号“在计算机科学中用于描述算法的性能或复杂度。大 O 特别描述了最坏的情况,可以用来描述算法所需的执行时间或使用的空间(例如在内存或磁盘上)。”(来源和示例:mng.bz/8WXg)
(10.) mng.bz/N8wX
(11.) 自环,也称为 sloops,是指源节点和目标节点相同的边。
机器学习应用中的 3 个图表
本章涵盖
-
图表在机器学习工作流程中的作用
-
如何正确存储训练数据和生成的模型
-
机器学习的图算法
-
使用图可视化进行数据分析
在本章中,我们将更详细地探讨图表和机器学习如何结合在一起,帮助为最终用户、数据分析师和商业人士提供更好的服务。第一章和第二章介绍了机器学习的一般概念,例如
-
构成通用机器学习项目的不同阶段(特别是 CRISP-DM 模型的六个阶段:业务理解、数据理解、数据准备、建模、评估和部署)
-
数据质量和数量对于创建一个有价值且有意义、能够提供准确预测的模型的重要性
-
如何通过使用图数据模型来处理大量数据(大数据)
在这里,我们将看到如何利用图模型作为表示数据的手段,使其易于访问和分析,以及如何使用基于图理论的机器学习算法的“智能”。我想以一张图像(图 3.1)作为本章的开端,这张图像代表了将来自多个来源的原始数据转换为不仅仅是简单知识或洞察力的过程:智慧。

图 3.1 由 David Somerville 绘制,基于 Hugh McLeod 的原始作品¹
我们被数据淹没。数据无处不在。新闻、博客文章、电子邮件、聊天和社交媒体只是围绕我们的多个数据生成源的几个例子。此外,在撰写本文时,我们正处于物联网爆炸的中间:今天甚至我的洗衣机都会给我发送数据,提醒我我的裤子已经洗好了,我的汽车也知道我应该停车休息喝杯咖啡。
数据本身是无用的;单独来看,它不提供任何价值。为了理解数据,我们必须与之互动并组织它。这个过程产生了信息。将信息转化为知识,揭示信息项之间的关系——这是一种质量变化——需要进一步的努力。这个转换过程连接了点,使之前无关的信息获得意义、重要性和逻辑语义。从知识中产生洞察力和智慧,这不仅相关,而且提供指导,可以转化为行动:生产更好的产品,让用户更快乐,降低生产成本,提供更好的服务等等。这个行动就是数据的真正价值所在,在漫长的转换路径的终点——而机器学习提供了从数据中提炼价值的必要智能。
图 3.1 在一定程度上代表了本章第一部分的学习路径:
-
从一个或多个来源收集数据和信息。这些数据是训练数据,任何学习都将在此基础上发生,并以图的形式管理(第 3.2 节)。
-
当数据以知识和适当的图的形式组织时,机器学习算法可以在其上提取和构建洞察和智慧(第 3.3 节)。
-
作为在知识上训练机器学习算法的结果而创建的预测模型被存储回图中(第 3.4 节),使得推断出的智慧永久可用。
-
最后,可视化(第 3.5 节)以人类大脑容易理解的方式展示数据,使得推导出的知识、洞察和智慧易于获取。
此路径遵循第一章和第二章中使用的相同心智模型,以突出和整理图在机器学习项目中作为有价值的帮助的多种方式(图 3.2)。

图 3.2 图驱动机器学习的心智模型
我们将从这个心智模型的开始部分开始,深入探讨,展示许多使用图特征来提高机器学习项目的技术和方法。
3.1 机器学习工作流程中的图
第一章中描述的 CRISP-DM 模型(Wirth 和 Hipp,2000)允许我们定义一个通用的机器学习工作流程,为了我们的讨论,可以将它分解为以下宏观步骤:
-
选择数据源,收集数据,并准备数据。
-
训练模型(学习阶段)。
-
提供预测。
一些学习算法没有模型。² 基于实例的算法没有单独的学习阶段;它们在预测阶段使用训练数据集中的条目。尽管在这种情况下图方法可以是一个有效的支持,但我们不会在我们的分析中考虑这些算法。
此外,很多时候,数据需要以多种形状可视化,以达到分析的目的。因此,可视化在作为完成机器学习工作流程的最终步骤中扮演着重要的角色,允许进一步的调查。
这个工作流程描述与图 3.2 中的心智模型相匹配,你将在本章(以及本书)的整个过程中看到它,以帮助你弄清楚每个步骤的位置。在这样的工作流程中,从操作、任务和数据流的角度来看图的作用是很重要的。图 3.3 说明了数据如何从数据源通过学习过程流向最终用户,以可视化的形式或预测的形式。

图 3.3 机器学习工作流程中图的作用
过程通常从可用的数据开始。不同的数据源将具有不同的模式、结构和内容。通常,用于机器学习应用的数据可以归类为大数据。(我们在第二章讨论了与大数据一起工作。)在学习过程开始之前,必须对这些数据进行组织和管理。图模型通过创建一个连接良好且组织有序的真相来源来帮助数据管理。从原始数据形状到图的转换可以通过多种技术完成,这些技术可以分为两组:
-
图建模——通过建模模式将数据转换为某种图表示。信息是相同的,只是格式不同,或者数据被聚合以使其更适合输入学习过程。
-
图构建——从可用的数据开始创建一个新的图。生成的图包含比之前更多的信息。
在此准备之后,数据以良好的结构格式存储,为下一阶段:学习过程。数据的图表示不仅支持基于图的算法;它可以喂养多种类型的算法。具体来说,图表示对于以下任务很有帮助:
-
特征选择——查询关系数据库或从 NoSQL 数据库中的值中提取一个键是一项复杂的任务。图易于查询,可以合并来自多个来源的数据,因此通过图方法找到和提取用于训练的变量列表变得简单。
-
数据过滤——对象之间易于导航的关系使得在训练阶段之前过滤掉无用数据变得容易,这加快了模型构建过程。我们将在 3.2.4 节中看到一个例子,我们将考虑推荐场景。
-
数据准备——图使得清理数据变得容易,去除虚假条目,并合并来自多个来源的数据。
-
数据丰富——通过图,可以轻松地用外部知识来源(如语义网络、本体和分类法)扩展数据,或者将建模阶段的结果循环回来构建更大的知识库。
-
数据格式化——很容易以所需的任何格式导出数据:向量、文档等。
在两种场景(基于图或非基于图的算法)中,结果可能是一个非常适合图表示的模型;在这种情况下,它可以存储回图中,或者以二进制或专有格式存储。
当预测模型允许时,将模型存储回图中,您就有机会在图上执行预测(更或更复杂)查询。此外,图提供了从不同角度和不同范围访问相同模型的方式。在第 3.2.4 节中描述的推荐是这种方法的潜在能力的例子。
最后,图模型可以用来以图格式可视化数据,这在沟通能力方面通常是一个很大的优势。图易于在白板上展示,因此这些可视化也可以在机器学习项目的早期阶段改善业务所有者和数据科学家之间的沟通。
这里描述的所有阶段和步骤并非都是强制性的;根据机器学习工作流程的需求,可能只有一些是有帮助或必要的。后面的章节将展示一系列具体的示例场景。对于每个场景,图的作用都得到了清晰的说明。
3.2 管理数据源
正如我们所见,图在编码信息方面极为有用,以图格式呈现的数据也越来越多。在机器学习的许多领域——包括自然语言处理、计算机视觉和推荐系统——图被用来模拟孤立数据项(用户、物品、事件等)之间的局部关系,并从局部信息构建全局结构 [赵和席尔瓦,2016]。将数据表示为图通常是处理机器学习或数据挖掘应用中产生的问题的必要步骤(有时也只是一种期望)。特别是,当我们想要将基于图的机器学习方法应用于数据集时,这一点变得至关重要。图 3.4 突出了在机器学习项目中管理数据源相关的图的作用。

图 3.4 心智模型中的数据源管理
将结构化或非结构化数据转换为图表示可以以无损的方式进行,但这种无损表示并不总是对学习算法的目的来说是必要的(或理想的)。有时,更好的模型是数据的聚合视图。例如,如果你正在模拟两个人之间的电话通话,你可以决定为每次通话在这两个实体(打电话者和接收者)之间建立关系,或者你可以有一个聚合所有通话的单个关系。你可以通过以下两种方式从输入数据集中构建图:
-
通过设计一个表示数据的图模型
-
通过使用一些方便的图形成标准
在第一种情况下,图模型是数据集中本身或多个数据集中可用的相同信息的替代表示。图中的节点和关系仅仅是原始来源中可用数据的表示(可能是聚合的或非聚合的)。此外,在这种情况下,图充当一个连接的数据源,合并来自多个异构源的数据,在过程结束时作为单一可信的真实数据源。有多个方法、技术和最佳实践用于应用这种模型转换并在图格式中表示数据,我们将在多个场景中讨论其中一些。其他数据模型模式在本书的其余部分中介绍。
在第二种场景中——使用一些方便的图形成标准——数据项存储在图中(通常作为节点),通过某种边构建机制创建图。假设在你的图中,每个节点代表一些文本,例如一个句子或整个文档。这些条目是孤立条目。除非它们被明确连接(例如通过论文中的引用),否则节点之间没有关系。在机器学习中,文本通常表示为向量,每个条目包含文本中单词或特征的权重。可以通过使用向量之间的相似度或差异值来创建(构建)边。从无关信息开始创建一个新的图。在这种情况下,生成的图包含比原始数据集更多的信息。这些附加信息由几个成分组成,其中最重要的是数据关系的结构或拓扑信息。
这两个过程的结果是一个图,它表示输入数据,并成为相关机器学习算法的训练数据集。在某些情况下,这些算法本身就是图算法,因此它们需要数据的图表示;在其他情况下,图是访问相同数据的更好方式。这里描述的示例和场景代表了这两种情况。
图 3.5 显示了将数据转换为图表示(或作为图创建)过程中的必要步骤。

图 3.5 将数据转换为图表示的过程
让我们更详细地看看每个步骤:
-
确定数据来源。确定用于算法训练目的的数据,以及可以从哪些来源提取此类数据。这一步骤对应于机器学习项目的第二阶段,在定义目标之后(CRISP-DM 数据模型的数据准备阶段)。
-
分析可用数据。分析每个可用的数据源,并从质量和数量方面评估其内容。为了从训练阶段获得良好的结果,拥有大量高质量的数据至关重要。
-
设计图数据模型。这一步是双重的。根据具体的分析需求,你必须
-
识别从数据源中提取的有意义的信息。
-
设计一个特定的图模型,考虑到可用的数据、访问模式和可扩展性。
-
-
定义数据流。设计摄入过程的架构(称为 ETL 过程),该过程使用设计的模式从多个源提取、转换和加载数据到图数据库。
-
将数据导入图。启动第 4 步中定义的 ETL 过程。通常,第 3 步、第 4 步和第 5 步是迭代的,直到你得到正确的模型和正确的 ETL 过程。
-
执行导入后任务。在开始分析之前,图中的数据可能需要进行一些预处理。这些任务包括
-
数据清理—删除或纠正不完整或不正确的数据。
-
数据丰富—通过外部知识源或从数据本身提取的知识扩展现有数据源。后一种情况属于图创建。
-
数据合并—因为数据来自多个源,数据集中的相关元素可以合并为一个单一元素,或者可以通过新的关系连接起来。
-
第 5 步和第 6 步可以颠倒或混合。在某些情况下,数据在摄入之前可能需要经过一个清理过程。
数据的新表示提供了几个优势,我们将通过多个场景和多个模型来探讨这些优势。你将首次看到其中一些场景,但其他场景已在第一章和第二章中介绍,并在整本书中进一步扩展。对于每个提出的场景,我将描述背景和目的——定义正确模型和理解使用图方法存储和管理数据以及将图作为分析下一步输入的价值的关键方面。
从第二部分开始,本书详细介绍了使用图模型表示不同数据集的技术。本章通过示例场景突出了使用图管理用于训练预测模型的数据的主要优势。
3.2.1 监控一个主体
假设你再次是一名警察。你希望通过使用从每部手机发送到(或接收自)它能到达的所有塔的持续监控信号收集到的蜂窝塔数据来尝试追踪嫌疑人并预测他们的未来行动。使用图模型和图聚类算法,可以将蜂窝塔数据结构化,以简单、清晰的方式表示一个主体的位置和移动。然后你可以创建一个预测模型。³
在此场景中,目标是监控一个对象并创建一个预测模型,该模型能够识别与对象生活相关的位置聚类,并且能够根据对象的当前位置和最后移动情况预测和预测后续移动 [Eagle, Quinn, and Clauset, 2009]。在此场景中,数据是手机与移动基站之间交互产生的移动基站数据,如图 3.6 所示。

图 3.6 手机与移动基站通信
为了进行此类分析,可以从基站或受监控对象的手机中收集数据。通过必要的权限,从移动基站收集数据很容易,但需要进行大量的清理(去除无关的号码)和合并(来自多个移动基站的数据)。从手机收集数据需要黑客技术,这并不总是可能的,但此数据是干净的并且已经合并。在他们的论文中,Eagle, Quinn 和 Clauset 考虑了第二种数据源,我们在这里也这样做,但结果和考虑与数据来源无关。
假设手机将以表 3.1 所示的格式提供数据。
表 3.1 单个手机与四个基站(按 ID 标识)在各个时间戳下提供的数据示例
| 手机标识符 | 时间戳 | 移动基站 1 | 移动基站 2 | 移动基站 3 | 移动基站 4 |
|---|---|---|---|---|---|
| 562d6873b0fe | 1530713007 | eca5b35d | f7106f86 | 1d00f5fb | 665332d8 |
| 562d6873b0fe | 1530716500 | f7106f86 | 1d00f5fb | 2a434006 | eca5b35d |
| 562d6873b0fe | 1530799402 | f7106f86 | eca5b35d | 2a434006 | 1d00f5fb |
| 562d6873b0fe | 1531317805 | 1d00f5fb | 665332d8 | f7106f86 | eca5b35d |
| 562d6873b0fe | 1533391403 | 2a434006 | 665332d8 | eca5b35d | 1d00f5fb |
为了简化起见,此表表示单个手机提供的数据。(手机标识符始终相同。)手机以 30 秒的间隔记录四个信号最强的基站。
分析需要我们识别出被监控主题花费时间的位置。手机上可用的蜂窝基站数据本身无法提供这些信息,因为它只包含四个信号强度最高的基站的标识符。但是,从这些数据开始,通过数据的一个图表示和一个图算法,可以识别出关键位置。因此,这些数据可以建模为一个表示蜂窝基站网络(CTN)的图。正如你从第二章回忆的那样,这个图中的每个节点都是一个独特的蜂窝基站;任何两个节点之间都存在边,如果它们在同一个记录中同时出现,则每条边都会根据这对节点在所有记录中同时出现的总时间分配一个权重。为该主题生成一个 CTN,显示了在监控期间由他们的手机记录下的每个基站。结果看起来像图 3.7。

图 3.7 单个主题的 CTN 图表示
将图聚类算法应用于此图,以识别主题花费大量时间的主要位置(在办公室、在家、在超市、在教堂等)。分析的结果看起来像图 3.8,其中识别并隔离了多个聚类。

图 3.8 CTN 的聚类视图
这个场景展示了图模型在表示分析特定目的的数据方面是多么地适应良好。通过使用社区检测算法进行基于图的分析,我们可以轻松地识别出主题花费大量时间的地方——如果使用其他表示或分析方法,这项任务将变得困难,甚至不可能。
这里描述的图建模技术说明了图构建技术。生成的图可以概括为共现图。节点代表实体(在这种情况下,是蜂窝基站),关系表示两个实体属于一个共同的集合或组。(在 CTN 中,集合是表格中的一行,表示在特定时间点,手机可以到达两个基站。)这项技术是一种强大的技术,在许多算法和机器学习应用中作为分析前的数据预处理步骤使用。这种类型的图构建技术通常用于与文本分析相关的应用;我们将在 3.3.2 节中看到一个例子。
3.2.2 检测欺诈
假设你想要为银行创建一个欺诈检测平台,该平台可以揭示信用卡盗窃的源头。交易的一个图表示可以帮助你识别,甚至通过视觉识别,盗窃的位置。⁴
在这种情况下,可用的数据是信用卡交易,包括关于日期(时间戳)、金额、商家以及交易是否争议的详细信息。当某人的信用卡详情被盗时,真实操作与非法或欺诈操作会混入他们的交易历史中。分析的目标是识别欺诈开始的地方——盗窃发生的商店。该商店的交易将是真实的,但在此之后发生的任何交易可能是欺诈性的。可用的数据看起来像表 3.2。
表 3.2 用户交易子集
| 用户标识 | 时间戳 | 金额 | 商家 | 有效性 |
|---|---|---|---|---|
| 用户 A | 2018 年 2 月 1 日 | $250 | Hilton Barcelona | 未争议 |
| 用户 A | 2018 年 2 月 2 日 | $220 | AT&T | 未争议 |
| 用户 A | 2018 年 3 月 12 日 | $15 | Burger King New York | 未争议 |
| 用户 A | 2018 年 3 月 14 日 | $100 | Whole Foods | 争议 |
| 用户 B | 2018 年 4 月 12 日 | $20 | AT&T | 未争议 |
| 用户 B | 2018 年 4 月 13 日 | $20 | Hard Rock Cafe | 未争议 |
| 用户 B | 2018 年 4 月 14 日 | $8 | Burger King New York | 未争议 |
| 用户 B | 2018 年 4 月 20 日 | $8 | Starbucks | 争议 |
| 用户 C | 2018 年 5 月 3 日 | $15 | Whole Foods | 争议 |
| 用户 C | 2018 年 5 月 5 日 | $15 | Burger King New York | 未争议 |
| 用户 C | 2018 年 5 月 12 日 | $15 | Starbucks | 争议 |
我们的目的是识别一些常见的模式,揭示用户开始对交易提出争议的点,这将帮助我们定位到卡片详情被盗的场所。这种分析可以通过使用交易的图表示来进行。表 3.2 中的数据可以建模成交易图,如图 3.9 所示。

图 3.9 用于信用卡欺诈检测的交易图
如第二章所述,从这种交易表示开始,使用图查询,我们可以确定盗窃发生在 Burger King。 (得出这一结论的步骤在第 2.2.2 节中描述,此处不再重复。)
交易图使我们能够轻松地识别卡片窃贼的活动区域。在这种情况下,分析是在 ETL 阶段之后的图上进行的;不需要其他中间转换。这种数据表示方式使得在长长的交易列表中快速识别行为模式并发现问题所在成为可能。
图 3.9 所示的交易图可以表示任何涉及两个实体的活动。通常,这些图用于以非聚合方式建模货币交易,这意味着每个单独的操作都可以与图的一个特定部分相关联。在大多数情况下,在生成的图中,每个交易以以下两种方式之一表示:
-
作为涉及交易的两个实体之间的有向边。如果用户 A 在商店 B 进行购买,这一事件被转换为一个有向边,起点是用户 A,终点是商店 B。在这种情况下,所有与购买相关的详细信息,如日期和金额,都存储为边的属性(图 3.10 (a))。
-
作为包含关于事件所有相关信息的一个节点,并通过边与相关节点相连。在购买的情况下,交易本身被建模为一个节点;然后它连接到购买的源和目的地(图 3.10 (b))。

图 3.10 交易建模示例
第一种方法通常在事件相关的信息量较小或分析目的更倾向于简单模型时使用。第二种方法通常在事件本身包含有价值的信息,这些信息可以与其他信息项相关联,或者事件涉及超过两个项目时更受欢迎。
交易图在欺诈检测分析和所有机器学习项目中都很常见,在这些项目中,每个事件都包含相关信息,如果事件被汇总,这些信息将会丢失。
3.2.3 在供应链中识别风险
假设你必须实施一个风险管理系统,该系统可以识别或预测供应链中的可能风险。供应链网络(SCN)是表示供应链元素及其交互的一种常见方式,以图形的形式。这种表示方式,加上适当的图分析算法,使得在整个供应链中快速发现问题变得容易且快速。
这种场景近年来变得越来越相关,原因有很多,包括以下方面:
-
随着全球经济的发展,任何供应链都可以具有全球性,因此供应链管理面临的问题正变得越来越复杂。
-
位于供应链末端的客户越来越关注他们所购买产品的来源。
管理中断风险和使供应链更加透明是任何供应链中的强制性任务。供应链本质上脆弱,面临着各种威胁,包括自然灾害和攻击,以及原材料污染、交货延误和劳动力短缺[Kleindorfer 和 Saad,2005]。此外,由于链的各个部分复杂且相互关联,单个成员的正常运营——以及整个链的效率运营——通常依赖于其他组件的正常运营。供应链的成员包括供应商、制造商、分销商、客户等等。所有成员相互依赖,通过物质、信息和金融流进行合作,但它们也是独立实体,可能在多个公司提供相同的服务。因此,在这种场景下可用的数据将分布在具有不同结构的多个公司中。基于此类形状数据的任何分析都是一个复杂任务;从多个成员那里收集所需信息并对其进行组织需要大量努力。
这里分析的目的在于发现链中的元素,如果这些元素受到损害,可能会破坏整个网络(或其大部分)或严重影响正常行为。图模型可以通过不同的网络分析算法支持此类分析任务。我们将在第 3.3 节中讨论算法的细节;在这里,我们将重点介绍可以用于从多个可用来源构建图表示的图构建技术。
可以使用以下方法在图中表示供应链:
-
供应链的每个成员都由一个节点表示。成员可以是原材料供应商(一级供应商)、二级供应商、中间分销商、转换过程、大公司中的组织单位、最终零售商等等。图的粒度与所需的风险评估相关。
-
图中的每个关系都代表链中两个成员之间的依赖关系。这些关系可能包括从供应商到中间分销商的运输、两个加工步骤之间的依赖关系、最终零售商的交货等等。
-
对于每个节点,可以关联一些时间数据,这些数据可以存储历史信息以及预测信息。
网络结构也可能随时间而演变。图模型可以设计为跟踪这些变化,但这样的设计会使本例目的过于复杂。我们的示例图模型如图 3.11 所示。

图 3.11 供应链网络
该模型代表了一种收集数据并以有机和均匀方式组织数据的重要方法,并为风险管理所需的分析类型提供了合适的表示。允许我们执行分析以揭示链中高风险元素的算法在 3.3.1 节中讨论。
3.2.4 推荐项目
假设你想要在电子商务商店中向用户推荐项目,使用你关于先前交互(点击、购买、评分等)的数据。图可以帮助你以加快访问速度的方式存储用户-项目数据集,并且在图中存储预测模型不仅便于预测,还允许你平滑地合并多个模型。
在机器学习中,图的最常见用例之一是推荐。我在 2012 年编写了第一个基于 Neo4j 构建的推荐引擎。这就是我的图学职业生涯的开始,也是为什么这个特定主题对我来说如此亲近。在这本书中,我们将通过多个示例发现使用图构建多模型推荐引擎的巨大优势,但在这里,我们将从一个简单的例子开始,考虑最基本实现。
可以使用多种方法来提供推荐。在这个特定例子中,选择的方法是基于一种称为协同过滤的技术。协同推荐方法的主要思想是利用现有用户社区过去的行为或意见信息来预测系统当前用户最可能喜欢或感兴趣的项目 [Jannach et al., 2010]。纯协同方法将任何类型的给定用户-项目交互矩阵(查看、过去购买、评分等)作为输入,并产生以下类型的输出:
-
一个(数值)预测,表示当前用户可能会喜欢或不喜欢某个特定项目的可能性(相关度得分)
-
根据预测值(从最有可能到最不可能)为用户提供的推荐项目的前 n 个项目的有序列表
相关度是通过一个基于用户反馈估计的效用函数 f 来衡量的 [Frolov and Oseledets, 2016]。更正式地说,相关度函数可以定义为
f: 用户 × 项目 → 相关度得分
其中 用户 是所有用户的集合,项目 是所有项目的集合。此函数可以用于计算所有无信息元素的关联得分。基于预测的数据可以直接由用户(通过评分、点赞/不喜欢等)或通过观察用户的行为(页面点击、购买等)隐式收集。可用的信息类型决定了可以用于构建推荐的技术类型。如果可以获取关于用户(配置文件属性、偏好)和项目(内在属性)的信息,则可以实现基于内容的推荐方法。如果只有隐式反馈可用,则需要采用协同过滤方法。
在预测所有未见过(或未购买)项目的相关性得分后,我们可以对它们进行排序,并向用户展示前 n 个项目,从而进行推荐。像往常一样,我们从可用的数据开始讨论。在这种情况下,数据源看起来像表 3.3(其中 1 表示低评分,5 表示用户对项目的评价很高)。
表 3.3 一个示例用户-项目数据集,以矩阵形式表示
| 用户 | 项目 1 | 项目 2 | 项目 3 | 项目 4 | 项目 5 |
|---|---|---|---|---|---|
| Bob | - | 3 | - | 4 | ? |
| 用户 2 | 3 | 5 | - | - | 5 |
| 用户 3 | - | - | 4 | 4 | - |
| 用户 4 | 2 | - | 4 | - | 3 |
| 用户 5 | - | 3 | - | 5 | 4 |
| 用户 6 | - | - | 5 | 4 | - |
| 用户 7 | 5 | 4 | - | - | 5 |
| 用户 8 | - | - | 3 | 4 | 5 |
这个表格是一个经典的用户-项目矩阵,包含了用户和项目之间的交互(在这个例子中,是评分)。带有符号-的单元格表示用户没有购买或评分该项目。在我们考虑的电子商务场景中,可能会有大量的用户和项目,因此生成的表格可能相当稀疏;每个用户只会购买一小部分可用的项目,因此生成的矩阵将有很多空单元格。在我们的示例表中,我们想要预测的用户 Bob 对未见过或未购买的项目 5 的兴趣。
从可用的数据(以描述的形状)和协同过滤的基本思想出发,存在多种实现这种预测的方法。为了本场景的目的,在本书的这一部分,我们将考虑基于项目的算法。基于项目的算法的主要思想是通过使用项目之间的相似度来计算预测。因此,我们将逐列考虑表格,每一列描述一个元素向量(称为评分向量),其中-符号被 0 值替换。让我们检查我们的用户-项目数据集,并为 Bob 预测第 5 个项目的评分。首先,我们比较其他所有项目的评分向量,寻找与项目 5 相似的项目。现在基于项目的推荐想法是查看 Bob 对这些相似项目的评分。基于项目的算法计算这些其他评分的加权平均值,并使用这个平均值来预测用户 Bob 对项目 5 的评分。
为了计算项目之间的相似度,我们必须定义一个相似度度量。余弦相似度是项目推荐方法中的标准度量:它通过计算两个向量之间角度的余弦值来确定两个向量之间的相似度[Jannach et al., 2010]。在机器学习应用中,这个度量通常用于比较两个表示为术语向量的文本文档;我们将在本书中经常使用它。
计算两个向量之间角度的余弦值以及因此两个项目 a 和 b 之间的相似度的公式如下:

·符号表示两个向量的点积。|
|是向量的欧几里得长度,定义为向量与其自身点积的平方根。
图 3.12 展示了 2D 空间中余弦距离的表示。

图 3.12 2D 空间中的余弦距离表示
为了进一步解释公式,让我们考虑第 5 个项目的余弦相似度,该项目的评分向量为[0, 5, 0, 3, 4, 0, 5, 5],以及第 1 个项目的向量[0, 3, 0, 2, 0, 0, 5, 0]。计算如下:

分子是两个向量之间的点积,由两个数字序列对应项的乘积之和计算得出。分母是两个向量的欧几里得长度的乘积。欧几里得距离是多维空间中两点之间的距离。图 3.13 在 2D 空间中说明了这一概念。

图 3.13 2D 空间中的欧几里得距离
公式如下:

欧几里得长度是向量从空间原点(在我们的情况下是向量[0,0,0,0,0,0,0,0])的欧几里得距离:

相似度值介于 0 和 1 之间,其中 1 表示最强的相似度。现在考虑我们必须为数据库中的每一对物品计算这种相似度,因此如果我们有 100 万种产品,我们需要计算 1M × 1M 个相似度值。我们可以将这个数字减半,因为相似度是可交换的——cos(a,b) = cos(b,a),但我们仍然有很多计算要做。在这种情况下,图可以成为加速推荐机器学习算法的有价值助手。用户-物品数据集可以很容易地转换为如图 3.14 所示的图。

图 3.14 表示用户-物品数据集的二分图
在这种图表示中,所有用户都在左侧,所有物品都在右侧。关系仅从一个子集的节点到另一个子集的节点;同一集合的节点之间没有关系。这个图是一个二分图或双图的例子。更正式地说,一个双图是一种特殊的图,其顶点(节点)可以被分为两个不相交且独立的集合 U 和 V,使得每条边都连接 U 中的一个顶点到 V 中的一个顶点,反之亦然。顶点集 U 和 V 通常被称为图的“部分” [Diestel, 2017]。
如何通过二分图表示减少我们必须执行的相似度计算数量?为了理解这个概念,有必要更好地理解余弦相似度(尽管原理可以扩展到更广泛的相似度函数集)。余弦相似度度量两个 n 维向量之间的角度,因此当两个向量是正交的(垂直)时,它们的余弦相似度为 0。在我们的例子中,这种情况发生在两个物品之间没有重叠用户(对两个物品都进行评分的用户)时。在这种情况下,分数的分子将为 0。我们可以计算由向量[3, 5, 0, 0, 3, 0, 4, 0]描述的物品 2 和由向量[0, 0, 4, 4, 0, 5, 0, 3]描述的物品 3 之间的距离,如下所示:

在这种情况下,相似度值将为 0(图 3.15)。在稀疏的用户-物品数据集中,正交性的概率相当高,因此无用的计算数量相应地很高。使用图表示法,使用简单的查询很容易找到至少有一个评分用户的所有物品。然后,只需在当前物品和重叠物品之间计算相似度,从而大大减少所需的计算数量。在原生图引擎中,搜索重叠物品的查询速度很快。

图 3.15 两个不重叠的项目。这些项目没有共同的评分用户,因此它们之间的余弦相似度为 0。
另一种方法是将二分图分成簇,并仅计算属于同一簇的项目之间的距离。
在本节所描述的场景中,图模型通过减少计算项目相似性所需的时间来帮助提高性能,从而提高推荐的效果。在 3.4 节中,我们将看到如何从这一图模型出发,存储相似性计算的结果以实现快速推荐。此外,余弦相似度将被用作图构建的技术。
3.3 算法
3.2 节描述了图模型在表示用于学习阶段的学习数据中的作用。这种对真相来源的表示具有多种优势,如前所述,无论学习算法是否基于图。
本节再次使用多个场景,描述了一些使用图算法实现项目目标的机器学习技术。我们将考虑两种方法:
-
作为主要学习算法的图算法
-
图算法作为更复杂算法管道中的促进者

图 3.16 心理模型中的算法
图 3.16 突出了图算法在机器学习项目中的作用。
在本书的其余部分,详细描述了一个完整的算法目录,并提供了实现示例。在本章中,目的是突出图算法在向最终用户提供预测中的作用。与传统的相比,这些技术提供了解决机器学习用例提出的挑战性问题的替代和新颖的方法。在这里(以及本书的其余部分),重点是展示如何在现实世界应用中使用这些技术,但我们也会考虑引入的方法的设计,这些方法在性能和计算复杂度方面是互补或有益的。
3.3.1 识别供应链中的风险
供应链风险管理主要旨在确定供应链对中断的敏感性,也称为供应链脆弱性 [Kleindorfer and Saad, 2005]。评估供应链生态系统的脆弱性具有挑战性,因为它不能直接观察或测量。单个节点的故障或过载可能导致整个网络发生级联故障。因此,供应链系统内部将发生严重损害。因此,脆弱性分析必须考虑整个网络,评估每个节点中断的影响。这种方法需要识别出比其他节点更能代表网络关键元素的节点。如果将供应链表示为网络,如图 3.17 所示,可以应用多种图算法来识别使其面临更大脆弱性的节点。
分析的目的是确定网络中最重要或中心节点。这种类型的分析将揭示供应链感兴趣的节点——最可能成为攻击目标以及需要最多保护的节点,因为任何对这些节点的中断都将严重影响整个供应链及其正常运行能力。例如,在图 3.17 中,原材料 B(B)的供应问题(中断可能发生在供应商端或连接处)将影响整个链条,因为它位于所有商店的路径上。

图 3.17 供应链网络
重要性有许多可能的定义;因此,可以用于网络的许多中心性度量。我们将考虑其中两种,不仅因为它们对供应链的具体场景有用,而且因为它们是本书后面多个示例中使用的强大技术:
-
PageRank——该算法通过计算一个节点到其他节点的边数和质量,对节点的重要性进行粗略估计。由谷歌创始人发明用于其搜索引擎的 PageRank 模型所实现的基本思想是投票或推荐。当一个节点通过边连接到另一个节点时,它基本上是在为该节点投票。一个节点收到的投票越多,它就越重要——但“投票者”的重要性也很重要。因此,与节点关联的分数是根据为其投下的投票以及投下这些投票的节点的分数来计算的。
-
中间中心性——该算法通过考虑节点在连接其他节点之间的最短路径上出现的频率来衡量节点的重要性。它适用于网络理论中的广泛问题。例如,在供应链网络中,具有更高中间中心性的节点将拥有对网络的更多控制权,因为更多的商品将通过该节点。
图 3.18 说明了这两种算法。

图 3.18 PageRank(a)和中间中心性(b)示例
在供应链脆弱性用例中,两种算法都可以用来确定供应链网络中最有趣的节点,但出发点不同:
-
中间中心性使我们能够通过它们控制通过的产品来识别在供应链中可能具有相当影响力的节点。具有最高中心性的节点也是那些从供应链网络中移除将最严重干扰产品流动的节点,因为它们位于产品采取的最大路径数量上。假设在供应链中,一家公司是所有产品基本组件的唯一供应商,或者一家公司作为特定产品的唯一分销商运营。在这两种情况下,该组件或产品的最大路径数量都通过它们,任何对这些节点的严重干扰都会影响整个供应链。
-
PageRank使我们能够识别节点,根据它们连接的节点的相对重要性,这些节点在网络中具有高价值。在这种情况下,破坏一个重要节点可能只会影响网络的一小部分,但破坏仍然可能是重大的。假设在供应链中,一个转换过程将产品转换为仅适合供应链中最大的最终客户之一的形式。在这种情况下,通过该过程的路径不多,因此节点的中间中心性相当低,但节点的价值很高,因为破坏它会影响链中的重要元素。
如这些示例所示,图算法为供应链网络提供了一种强大的分析机制。这种方法可以推广到许多类似场景,例如通信网络、社交网络、生物网络和恐怖主义网络。
3.3.2 在文档中查找关键词
假设你想自动识别一组最能描述文档或整个语料库的术语。使用基于图排序的模型,你可以通过无监督学习方法找到文本中最相关的话语或短语。
公司通常需要管理和处理大量数据,无论是为了向最终用户提供服务还是为了内部流程。其中大部分数据以文本形式存在。由于文本数据的非结构化特性,访问和分析这一庞大的知识来源可能是一项具有挑战性和复杂性的任务。关键词可以帮助识别主要概念,从而有效地访问大量文档。关键词提取还可以用于为文档集合构建自动索引、构建特定领域的词典或执行文本分类或摘要任务 [Negro 等人,2017]。
可以使用多种技术从语料库中提取关键词列表。最简单的方法是使用相对频率标准(识别出现频率最高的术语)来选择文档中的重要关键词——但这种方法缺乏复杂性,通常会导致较差的结果。另一种方法涉及使用监督学习方法,其中系统被训练根据词汇和句法特征来识别文本中的关键词——但需要大量的标记数据(手动提取相关关键词的文本)来训练一个足够准确以产生良好结果的模型。
图可以成为解决这类复杂问题的秘密武器,通过使用数据图的表示和图算法(如 PageRank)以无监督的方式从文本中提取关键词或句子。TextRank [Mihalcea and Tarau, 2004] 是一种基于图的排名模型,可用于此类文本处理。
在这种情况下,我们需要构建一个表示文本并使用有意义的关联将单词或其他文本实体相互连接的图。根据目的,提取的文本单元——可以是用于摘要的关键词、短语或整个句子——可以作为图中的节点添加。同样,最终范围定义了用于连接节点的关系的类型(词汇或语义关系、上下文重叠等)。无论添加到图中的元素类型和特征如何,将 TextRank 应用于自然语言文本的过程包括以下步骤 [Mihalcea and Tarau, 2004]:
-
确定与当前任务相关的文本单元,并将它们作为节点添加到图中。
-
确定连接文本单元的关系。节点之间的边可以是有向的或无向的,可以是加权的或无权的。
-
迭代基于图的排名算法,直到收敛或达到最大迭代次数。
-
根据最终得分对节点进行排序,使用这些得分进行排名/选择决策,并最终将两个或多个文本单元合并为一个(短语)关键词。
因此,节点是从文本中提取的一个或多个词汇单元的序列,它们是将被排名的元素。任何可以在两个词汇单元之间定义的关系都是可以在节点之间添加的潜在有用连接(边)。对于关键词提取,识别关系最有效的方法之一是共现。在这种情况下,如果两个节点都出现在最多 N 个单词(N-gram)的窗口内,则两个节点是连接的,N 通常在 2 到 10 之间。这种情况是使用共现图的一个例子(可能是最常见的例子之一);图 3.19 显示了结果的一个示例。此外,还可以使用句法过滤器来选择特定词性的词汇单元(例如,仅名词、动词和/或形容词)。

图 3.19 TextRank 创建的共现图
当图构建完成后,可以在其上运行 TextRank 算法以识别最重要的节点。图中的每个节点最初被分配一个值为 1 的值,算法运行直到收敛到给定的阈值以下(通常为 20 到 30 次迭代,阈值为 0.0001)。为每个节点确定最终分数后,节点按分数降序排序,并对前 T 个节点(通常为 5 到 20 个)进行后处理。在此后处理过程中,文本中连续出现且都相关的单词合并成一个关键词。
这种无监督的基于图的算法所达到的准确性与任何监督算法相当 [Mihalcea and Tarau, 2004]。这一结果表明,使用图方法,可以避免监督算法在提供预标记数据时所需的相当大的努力,例如本节所描述的任务。
3.3.3 监控一个主题
让我们继续讨论如何通过使用蜂窝基站数据来监控主题的运动。在本章前面,我们讨论了如何将分布在不同塔或多个手机上并以表格格式存储的数据转换为同质图,称为 CTN(如图 3.20 所示)。正如第二章所述,图中具有最高总边权重的节点对应于最常被主题的手机看到的塔 [Eagle, Quinn, and Clauset, 2009]。

图 3.20 单个主题的 CTN 图表示
本章前面描述的图构建是一个初步任务,用于使用允许我们识别塔群组的图聚类算法。这里的逻辑是,通过大量加权边相互连接,并通过较少加权边与其他节点连接的一组节点应该对应于监控主题花费大量时间的位置。图聚类是一种无监督学习方法,旨在根据图中的边结构将图中的节点分组,使得每个簇内部应该有许多边,而簇之间的边相对较少 [Schaeffer, 2007]。为此目的存在多种技术和算法,并在本书的其余部分进行了广泛讨论。
当图被组织成多个子图以识别位置时,下一步是使用这些信息构建一个预测模型,该模型能够根据当前位置指示主题可能去往的下一个位置。之前识别出的塔群可以作为动态模型的状态。⁵ 给定一个主题访问的位置序列,算法学习主题行为中的模式,并能够计算主题在未来移动到不同位置的概率。这里用于建模的算法 [Eagle, Quinn, and Clauset, 2009] 是一个动态贝叶斯网络,它在与更简单的方法(马尔可夫链)一起在 3.4.2 节中介绍。
在前一种场景中,应用图算法(TextRank)是主要且唯一必要的操作,而在这里,由于问题更加复杂,图算法被用作更细致的学习流程的一部分,以创建一个高级预测模型。
3.4 存储和访问机器学习模型
工作流程的第三步涉及将预测结果交付给最终用户。学习阶段的输出是一个包含推理过程结果的模型,并允许我们对未见实例进行预测。该模型必须存储在永久存储或内存中,以便在需要新的预测时可以访问。我们可以访问模型的速度会影响预测性能。这一时间因素对于机器学习项目成功定义至关重要。如果最终模型的准确性很高,但预测需要很长时间,系统将无法正确完成任务。
图 3.21 总结了图如何贡献这一阶段。

图 3.21 在心智模型中存储和访问模型
考虑电子商务网站的推荐场景。用户正在寻找某物,但没有具体的产品购买想法,因此他们从文本搜索开始导航,然后在结果列表中这里点击那里,浏览几个选项。在这个时候,系统开始根据导航路径和点击推荐项目给用户。所有这些都在瞬间完成:在有相当的网络条件下,用户可以快速导航,每 5 到 10 秒或更短的时间内从一个页面跳转到下一个页面。因此,如果推荐过程需要 10 秒或更长时间,那就毫无意义。
这个例子展示了拥有一个能够快速提供预测的系统的重要性。在这种情况下,提供快速访问模型是成功的关键,而且,图可以再次发挥重要作用。本节通过一些解释性场景,探讨了使用图存储预测模型并提供快速访问的方法。
3.4.1 推荐项目
基于物品(或基于用户)的协同过滤方法在学习阶段的结果是一个包含用户-物品数据集中每对物品之间相似度的物品-物品矩阵。生成的矩阵看起来像表 3.4。
表 3.4 相似度矩阵
| 物品 1 | 物品 2 | 物品 3 | 物品 4 | 物品 5 | |
|---|---|---|---|---|---|
| 物品 1 | 1 | 0.26 | 0.84 | 0 | 0.25 |
| 物品 2 | 0.26 | 1 | 0 | 0.62 | 0.25 |
| 物品 3 | 0.84 | 0 | 1 | 0.37 | 0.66 |
| 物品 4 | 0 | 0.62 | 0.37 | 1 | 0.57 |
| 物品 5 | 0.25 | 0.25 | 0.66 | 0.57 | 1 |
确定了物品之间的相似性后,我们可以通过计算与物品 5 相似的物品的 Bob 的评分的加权总和来预测 Bob 对物品 5 的评分。形式上,我们可以预测用户 u 对产品 p 的评分如下[Jannach 等人,2010]:

在这个公式中,分子包含 Bob 对目标产品评分的每个产品的相似值与他对该产品的评分的乘积之和。分母包含 Bob 对目标产品评分的所有物品的相似值之和。
让我们只考虑表 3.5 中显示的用户-物品数据集的行(技术上,是用户-物品矩阵的一个切片)。
表 3.5 用户 Bob 的用户-物品切片
| 用户 | 物品 1 | 物品 2 | 物品 3 | 物品 4 | 物品 5 |
|---|---|---|---|---|---|
| Bob | - | 3 | - | 4 | ? |
上述公式将如下所示:

表 3.4 中的物品-物品相似度矩阵可以轻松存储在图中。从为存储用户-物品矩阵而创建的二部图开始,存储这个矩阵就是添加新的关系,这些关系将物品连接到其他物品(因此图将不再是二部图)。关系的权重是相似度的值,介于 0(在这种情况下,不存储任何关系)和 1 之间。生成的图看起来像图 3.22。

图 3.22 存储在原始二部图中的相似度距离模型
在此图中,为了减少连接节点的弧的数量,表示了项目之间的双向关系;在现实中,它们是两种不同的关系。此外,由于关系的数量是 N x N,从阅读和写作的角度来看,存储所有关系可能相当困难。典型的方法是只为每个节点存储前 K 个关系。当为每个项目计算所有相似性后,它们按降序排列,从最相似到最不相似,只存储前 K 个,因为在预测计算过程中,只使用前 K 个。以这种方式存储数据时,计算用户最感兴趣的项目只需在图中跳跃几次。根据公式,考虑了目标用户评价的所有项目(在我们的案例中,项目 2 和 4 与用户 Bob 相连),然后对每个项目,计算与目标项目(项目 5)的相似度。用于计算预测的信息是本地化的,因此使用所提出的图模型进行预测速度快。无需进行长时间的数据查找。
此外,在预测过程中,可以存储更多类型的关系,并同时导航它们,以基于多个相似性度量提供组合预测。这些预测可以基于除了纯协同过滤之外的方法。我们将在本书的第三部分讨论计算相似度或距离(从不同角度的相同概念)的其他技术。
3.4.2 监控一个主题
在主题监控场景中,在识别代表主题花费大量时间的地点的塔群之后,算法继续通过学习主题的行为模式。然后我们可以使用动态模型,如动态贝叶斯网络,来构建主题位置的预测模型。
贝叶斯网络 是一个有向图,其中每个节点都标注了定量概率信息(例如 50% 或 0.5,70% 或 0.7)。贝叶斯网络(又称 概率图模型 或 信念网络)代表了概率理论和图理论相结合的混合体,其中变量之间的依赖关系以图形方式表示。该图不仅有助于用户理解哪些变量影响哪些其他变量,而且还能有效地计算可能用于推理和学习的边缘概率和条件概率。完整的规范如下 [Russell and Norvig, 2009]:
每个节点对应一个随机变量。这些变量可能是可观察的数量、潜在变量、未知参数或假设。
边代表条件依赖。如果从节点 X 到节点 Y 有边,则称 X 为 Y 的父节点。该图没有有向循环(因此是一个有向无环图,或 DAG)。没有连接的节点(在贝叶斯网络中变量之间没有路径)代表条件独立的变量。
每个节点 Xi 都有一个条件概率分布 P(Xi, Parents(Xi)),它量化了父节点对节点的影响。换句话说,每个节点都与一个概率函数相关联,该函数接受节点父变量的一组特定值作为输入,并给出节点所代表变量的概率,或适用的概率分布。
为了使这个讨论更清晰,考虑一个简单的例子 [Russell and Norvig, 2009]:
你在家里安装了一个新的防盗报警器。它相当可靠地检测到盗窃,但有时也会对轻微的地震做出反应……你还有两个邻居,约翰和玛丽,他们承诺在听到警报时给你打电话。约翰几乎每次听到警报都会打电话,但有时会把电话铃声误认为是警报,然后也会打电话。另一方面,玛丽喜欢很响的音乐,经常完全错过警报。根据谁打电话或没打电话的证据,我们想要估计盗窃的概率。
与此例相关的贝叶斯网络如图 3.23 所示。盗窃和地震直接影响报警器发出警报的概率,这由连接顶部 Burglary 和 Earthquake 节点到 Alarm 节点的有向边表示。在底部,你可以看到约翰或玛丽是否打电话只取决于警报(由连接 Alarm 到 JohnCalls 和 MaryCalls 节点的边表示)。他们没有直接感知盗窃或注意到轻微的地震,并且在打电话之前没有进行协商。
在图 3.23 中,每个节点附近的表格是条件分布,表示为条件概率表(CPT)。条件分布是子群体的概率分布。CPT 中的每一行包含每个节点值的条件概率,给定父节点值的可能组合。P(B)代表发生盗窃的概率,例如,P(E)代表发生地震的概率。这些分布很简单,因为它们不依赖于任何其他事件。P(J),约翰打电话的概率,和 P(M),玛丽打电话的概率,依赖于警报。JohnCalls 的条件概率表表明,如果警报响起,约翰打电话的概率是 90%,而当他没有响起警报时(记住,约翰可能会把电话铃声误认为是警报)打电话的概率是 5%。Alarm 节点的 CPT 稍微复杂一些,它依赖于 Burglary 和 Earthquake 节点。在这里,当盗窃和地震同时发生时,P(A)(警报响起的概率)是 95%,但在盗窃与地震不同时发生的情况下是 94%,在没有盗窃的情况下发生地震时是 29%。误报很少见,概率为 0.1%。

图 3.23 一个典型的贝叶斯网络,显示了拓扑结构和条件概率表
动态贝叶斯网络(DBN)是一种特殊的贝叶斯网络,它关联相邻时间步的变量。回到我们的主题监控场景,用于执行位置预测的最简单的 DBN 版本是马尔可夫链。图 3.24 中显示的例子是一个纯图,是贝叶斯网络更一般图表示的特殊情况。在这种情况下,节点代表 t 点的状态(在我们的例子中,是主题的位置),关系的权重代表 t+1 时间步状态转换的概率。
在图 3.24 中,如果某人在时间 t 处于家中,他们最有可能留在家里(45%)。他们移动到办公室的概率是 25%;他们去市场的可能性是 20%,他们去学校的可能性是 10%(可能是送孩子)。这个例子是观察到的模型表示。从这个模型开始,计算 t 步后的位置概率是在节点之间进行路径导航,其中每个节点可以出现多次。

图 3.24 一个简单的马尔可夫链。(最可能的移动用红色表示。)
这种方法可以进一步扩展。Eagle、Quinn 和 Clauset [2009] 注意到,实践中人们的移动模式取决于一天中的时间和一周中的某一天(例如周六晚上与周一早晨)。因此,他们基于上下文马尔可夫链(CMC)创建了一个扩展模型,其中主题位于某个位置的概率也取决于一天中的小时和一周中的某一天(这些代表上下文)。CMC 在此处不做详细描述,但图 3.25 展示了其背后的基本思想。

图 3.25 两个上下文值的一个简单上下文马尔可夫链:C = {中午,周末}(a),和 C = {早晨,工作日}(b)
考虑到一天中的时间,定义为“上午”、“下午”、“傍晚”或“夜间”,以及一周中的某一天,分为“工作日”或“周末”,来创建上下文。图 3.25 中的图表表示学习每个上下文的最大似然参数后的结果马尔可夫链。图 3.25(a)显示了周末中午的马尔可夫链,因此孩子们没有学校,办公室也没有工作。图 3.25(b)显示了工作日早晨相关的马尔可夫链。这样的图表使我们能够通过在图上简单查询,预测主题最有可能去的地方。
马尔可夫链、CMC 以及更一般地(动态)贝叶斯网络是适用于许多用例的预测模型。此处使用主题监控场景进行说明,但此类模型在许多类型的用户建模和尤其是在网络分析中积极用于预测用户意图。
3.5 可视化
机器学习的主要目标之一是理解数据并为最终用户提供某种预测能力(尽管如本章开头所述,数据分析通常旨在从原始数据源中提取知识、洞察力,最终是智慧,而预测只是可能用途的一小部分)。在本学习路径中,数据可视化起着关键作用,因为它使我们能够从不同的角度访问和分析数据。在我们对机器学习工作流程的心理地图(图 3.26)中,可视化位于流程的末尾,因为在对数据进行初步处理之后可视化数据要比直接可视化原始数据好得多,但数据可视化可以在工作流程的任何一点发生。

图 3.26 心理模型中的可视化
在这种情况下,图表方法再次扮演了基础角色。数据分析中的一个日益增长的趋势是将链接数据视为网络。网络分析师不仅关注数据的属性,还关注数据中的连接和结果结构。如果图表是组织数据以更好地理解和分析其中关系的有用方式,那么可视化有助于揭示这种组织,进一步简化理解。结合这两种方法有助于数据科学家理解他们拥有的数据。此外,成功的可视化在简单性上具有欺骗性,一眼就能为观众提供新的洞察和理解。
为什么将数据可视化,特别是以图表的形式,使其更容易分析?有几个原因:
-
人类天生是视觉生物。我们的眼睛是我们最强大的感官接收器,通过信息可视化展示数据可以最大限度地发挥我们的感知能力[Perer, 2010]。
-
今天,许多数据集太大,无法在没有便于处理和交互的计算工具的情况下进行检查。数据可视化结合了计算机的力量和人类大脑的力量,利用我们的模式识别能力,使我们能够高效且复杂地解释数据。如果我们能以图表的形式看到数据,就更容易发现模式、异常值和缺口[Krebs, 2016; Perer, 2010]。
-
图表模型揭示了可能隐藏在其他数据视图(如表格和文档)中的关系,并帮助我们挑选出重要细节[Lanum, 2016]。
另一方面,选择有效的可视化可能是一个挑战,因为不同的形式有不同的优点和缺点[Perer, 2010]:
并非所有信息可视化都能突出分析师任务中重要的模式、缺口和异常值,而且并非所有信息可视化“强迫我们注意到我们从未预料到看到的东西”[Tukey, 1977]。
此外,将大数据可视化需要投入大量的精力来进行筛选、组织和在屏幕上展示。尽管面临所有这些挑战,图表视图仍然对众多领域的科研人员具有吸引力。
社会网络分析师 Valdis Krebs 在其工作中展示了使用图表示法揭示人类行为洞察力的良好例子。Krebs 工作的一个有趣方面是,他能够从任何类型的来源(旧文档、报纸、数据库或网页)获取数据;将其转换为图表示法;进行一些网络分析;然后使用他自己的软件 InFlow 可视化结果。然后他分析图表并得出一些结论。一个例子,我们在第一章中看到的是他对 2003 年之前两次美国总统选举在 Amazon.com 上购买政治书籍的分析(图 3.27)。

图 3.27 2003 年之前两次美国总统选举的政治书籍网络 [Krebs, 2016]
亚马逊提供了可以用来创建共现网络(我们在一些示例场景中之前看到过的一种图)的购买摘要数据。当一位顾客购买了这两本书时,两本书就连接在一起。购买这两件商品的顾客越多,它们之间的关联就越强,在“购买此商品的顾客还购买了”列表中出现的关联商品就越高。因此,通过使用亚马逊数据,可以生成一个网络,该网络可以提供对客户偏好和购买行为的深刻见解。正如 Krebs 所说:“通过一点数据挖掘和一些数据可视化,我们可以深入了解亚马逊顾客的习惯和选择——也就是说,我们可以了解一群人,而无需了解他们的个人选择。”
在图 3.27 中,可以识别出两个不同的政治集群:一个红色集群(在印刷版中为灰色),代表那些阅读右倾书籍的人;一个蓝色集群(在印刷版中为黑色),代表那些阅读左倾书籍的人。只有一本书将这两个集群连接在一起;具有讽刺意味的是,这本书的名字叫出了什么问题。这个图可视化提供了强有力的证据,证明了 2008 年政治选举期间美国公民是多么的极化。但是,对于机器学习算法来说,这个“证据”并不那么明显,因为它需要大量的上下文信息,而这些信息对于人类大脑来说更容易提供:书籍的政治倾向或作者的倾向,正在进行的政治选举的情况等等。
3.6 剩余:深度学习和图神经网络
机器学习是一个广泛且不断发展的领域。它如此庞大,以至于没有任何一本书能涵盖这些实践可能完成的全部任务和可能性。这本书也不例外。我们有意省略了很多话题。其中,有一个话题至少值得提及,因为在写作的时候,它在研究和应用中都显得非常突出:深度学习。让我简要介绍一下,以便您对它有一个高层次的理解,了解它在机器学习领域的位置,以及它如何应用于图。
如我们所见,并且我们将在整本书中讨论,机器学习算法的性能(从准确性的角度来说)在很大程度上取决于数据的质量以及它的表示方式。在表示中包含的每一项信息,以及在训练和预测过程中使用的信息,都被定义为特征。特征的例子包括用户购买的商品列表,用户花费时间的地方,以及文本中的标记。机器学习过程将这些特征作为输入,并推断出一个能够将这种输入映射到潜在输出的模型。在推荐的情况下,这个过程会考虑用户购买或评价的商品,并试图预测他们可能感兴趣的内容。在主题监控的情况下,考虑到之前的地点,算法预测用户在接下来的几小时或几天内可能在哪里。本书解释了如何使用图数据模型来表示这些特征,以及如何简化或改进映射。
不幸的是,对于许多任务来说,确定用于训练模型的特征提取并不是那么简单。假设你需要编写一个算法来识别图片中的面孔。一个人有一对眼睛,一些颜色的头发(不是所有头发),一个鼻子,一个嘴巴等等。用语言描述面孔很简单,但我们如何用像素来描述眼睛的形状呢?
解决这种表示问题的可能方法之一是使用机器学习来发现不仅是从表示到输出的映射,还包括表示本身。这种方法被称为表示学习 [Goodfellow 等人,2016]。这种表示通常比手工挑选的特征有更好的性能。此外,因为机器能够从更简单的数据(例如,只有图像或一些文本)中学习表示,所以它能够以减少人力投入的方式快速适应新的任务。对于机器可能只需要几分钟或几天就能完成的事情,对于人类可能需要数十年的研究。
深度学习通过引入用其他更简单的表示表达表示来解决表示学习问题 [Goodfellow 等人,2016]。在深度学习中,机器在基础更简单概念之上构建多个越来越复杂的层次。人的图像概念可以通过结合角点和轮廓来表示,而这些角点和轮廓又可以用边缘来定义。图 3.28 描述了经典机器学习和深度学习子领域之间的差异。

图 3.28 基于规则的算法、经典机器学习和深度学习之间的差异(受 Goodfellow 等人,2016 年启发)
在前面的例子中,我们提到了图像、文本等。这些数据类型被定义为欧几里得类型,因为它们可以在多维空间中表示。如果我们想在图上使用这种方法,比如识别社交网络中的节点是否为机器人,或者预测表示疾病的两个节点之间链接(以及因此产生的相关性)的形成?图 3.29 显示了图像和文本在欧几里得空间中的表示与难以在这样多维空间中表示的图。 (图下方左侧,TF-IDF 代表词频-逆文档频率。)

图 3.29 图像和文本的欧几里得表示与难以在多维空间中表示的图
具有多个节点类型和不同类型关系的图远非欧几里得空间。这项任务正是图神经网络(GNNs)的用武之地。GNNs是基于深度学习的方法,在图域上执行复杂任务,如节点分类(例如机器人示例)、链接预测(例如疾病示例)等。由于其令人信服的性能,GNN 已成为广泛应用的图分析方法。
图 3.30 展示了一个通用的编码函数,该函数能够将 d 维向量中的节点(或关系)进行转换。这个函数通常用作嵌入技术。当图元素已经迁移到欧几里得空间时,我们可以使用图像和文本的经典机器学习技术。这种表示学习质量影响后续任务的质量和准确性。

图 3.30 通用编码器示例
GNNs 能够生成依赖于图结构以及我们拥有的任何特征信息的节点表示。这些特征可能是节点的属性、关系类型和关系属性。这就是为什么 GNNs 能够推动最终任务达到更好的结果。这些嵌入代表了节点分类、链接预测和图分类等任务的输入。
这些概念更加复杂,需要更广泛地理解机器学习(特别是深度学习)和图,这就是为什么我更喜欢将这些主题留在这本书之外。新的技术,如深度学习和 GNNs,并没有使本书中介绍的内容失效;它们是建立在这些页面上的原则之上的,这些原则代表了事实真相。我确实认为这里介绍的概念为读者提供了一个理解更近期方法的思维模型,使他们能够评估何时使用每种类型。
概述
本章介绍了机器学习项目中图的综合应用案例。在本章中,你学习了
-
如何使用图和图模型来管理数据。设计合适的图模型允许将多个数据源合并为一个单一、连接良好且组织有序的真相来源。这种方法不仅因为它创建了一个单一的知识库——知识图谱,可以供多个项目共享,而且还因为它可以以适合要执行的分析方式组织数据。
-
如何使用图算法处理数据。图算法支持广泛的分析,可以单独使用或作为更复杂和细致的分析管道的一部分。
-
如何设计一个图,该图存储训练得到的预测模型,以简化预测阶段的数据访问并提高速度。
-
如何将数据以图形的形式可视化。数据可视化是预测分析的关键方面。图可以作为建模数据的模式,以便分析师以高效和有效的方式可视化;人脑可以完成剩余的部分。
-
深度学习和图神经网络是什么。
参考文献
[Diestel, 2017] Diestel, Reinhard. 《图论》。第 5 版。纽约:Springer,2017 年。
[Eagle, Quinn, and Clauset, 2009] Eagle, Nathan,John A. Quinn 和 Aaron Clauset. “连续蜂窝基站数据分析的方法。” 第 7 届国际普适计算会议论文集(2009 年):342-353.
[Frolov and Oseledets, 2016] Frolov, Evgeny 和 Ivan Oseledets. “张量方法和推荐引擎。” GroundAI,2016 年 3 月 18 日。
[Goodfellow et al., 2016] Goodfellow, Ian,Yoshua Bengio 和 Aaron Courville. 《深度学习》。麻省理工学院出版社。2016 年。
[Jannach et al., 2010] Jannach, Dietmar,Markus Zanker,Alexander Felfernig 和 Gerhard Friedrich. 《推荐系统:入门》。英国剑桥:剑桥大学出版社,2010 年。
[Kleindorfer and Saad, 2005] Kleindorfer, Paul R. 和 Germaine H. Saad. “供应链中断风险的应对策略.” 生产与运营管理 14:1 (2005): 53-68.
[Krebs, 2016] Krebs, Valdis. 政治选择. T N T: 网络思想家,2016 年 1 月。www.thenetworkthinkers.com.
[Lanum, 2016] Lanum, Corey L. 《可视化图数据》。纽约:Manning,2016 年。
[Mihalcea and Tarau, 2004] Mihalcea, Rada 和 Paul Tarau. “TextRank:将秩序带入文本。” 第 2004 年实证自然语言处理会议论文集(2004 年):404-411.
[Negro et al., 2017] Negro, Alessandro,Vlasta Kus,Miro Marchi 和 Christophe Willemsen. “使用图进行高效的未监督关键词提取。” GraphAware,2017 年 10 月 3 日。mng.bz/w0Z7.
[Perer, 2010] Perer, Adam. “在社交网络可视化混乱中寻找美丽的洞察力。” 见《美丽可视化》,由 Julie Steele 和 Noah Iliinsky 编著。加利福尼亚州塞巴斯蒂波利斯:O’Reilly,2010 年。157-173.
[Russel and Norvig, 2009] Russell, Stuart J. 和 Peter Norvig. 人工智能:现代方法. 第 3 版. 新泽西州上萨德尔河:Pearson,2009.
[Schaeffer, 2007] Schaeffer, Satu Elisa. “调查:图聚类。”计算机科学评论 1:1 (2007): 27-64.
[Tukey, 1977] Tukey, John W. 探索性数据分析. 麻省雷丁:Addison-Wesley,1977.
[Wirth and Hipp, 2000] Wirth, R. 和 J. Hipp. “CRISP-DM:数据挖掘的标准流程模型。”第四国际知识发现和数据挖掘实际应用会议论文集(2000):29-39.
[Zhao and Silva, 2016] Zhao, Liang 和 Thiago Christiano Silva. 复杂网络中的机器学习. 纽约:Springer,2016.
^(1.)www.smrvl.com.
^(2.)详细信息请见附录 A。
^(3.)这个场景在第二章中出于不同的目的被引入。在这里,它被扩展并分割成构成我们心智模型的多项任务。请允许我为这种(但必要的)小重复表示歉意。
^(4.)与前例相同的原因,本章中从第二章有一些重复。在本章中,场景描述得更加详细。
^(5.)使用动态模型来表示或描述随时间变化状态的系统。
第二部分建议
表示是机器学习和计算机科学中最为复杂和引人入胜的任务之一。华盛顿大学的计算机科学教授佩德罗·多明戈斯(Pedro Domingos)发表了一篇名为[Domingos, 2012]的文章,在其中他将机器学习分解为三个主要组成部分:表示、评估和优化。表示具体影响机器学习项目生命周期的三个核心方面:
-
在将其作为输入传递给学习过程之前,表示训练数据集的正式语言(或模式)
-
学习过程的结果——预测模型——的存储方式
-
在预测阶段,如何访问训练数据和预测模型以进行预测
所有这些方面都受到用于从训练数据集中的观察示例中推断泛化的学习算法的影响,并影响整体性能,包括预测准确性和训练和预测性能(速度)。
从第二部分开始,本书专注于数据建模:用于表示训练数据集和推断模型(学习过程的结果)的正式结构,以便计算机程序(学习代理¹)可以处理和访问它,为最终用户提供预测或分析。因此,考虑了两种不同的模型:
-
描述模型是为了服务于特定学习目的而创建的现实的简化表示(训练数据集)。这种简化基于对特定目的相关或不相关的某些假设,有时也基于可用信息的约束。
-
预测模型是估计感兴趣未知值的公式:目标。这个公式可以是数学的,是对数据结构(例如数据库或图)的查询,是一个逻辑陈述,如规则,或者这些的组合。它以高效格式表示训练数据集上的学习过程结果,并用于执行实际预测。
本书剩余部分阐述了用于数据建模的基于图的技术,这些技术服务于这两个目的。在某些情况下(当学习算法是图算法时),使用图作为数据模型的决定是必要的。在其他情况下,图方法比使用表格或其他替代方案更优。
第二章和第三章在较高层次上介绍了一些建模示例,例如使用蜂窝塔网络来监控对象、共现图来查找关键词,以及二分图来在推荐引擎中表示用户-物品数据集。这些示例展示了在这些特定场景中,图方法在建模目的上的优势。在本书的第二部分,我们将深入探讨。通过使用三个不同的宏观目标——推荐、欺诈分析和文本挖掘——本部分和下一部分的内容将更详细地介绍一系列建模技术和最佳实践,用于表示训练数据集、预测模型和访问模式。尽管如此,重点仍然在于图建模技术和预测算法。主要目的是为您提供心理工具,将预测技术的输入或输出表示为图,展示图方法固有的价值。将展示一些示例场景,并在可能和适当的情况下,将这些设计技术——包括必要的扩展和考虑——投影到其他相关场景中。
从现在到本书的结尾,章节也将具有实践性。将介绍并详细讨论真实数据集、代码片段和查询。对于每个示例场景,将选择数据集,设计并导入模型,创建并访问预测模型以获取预测。对于查询,我们将使用一种标准的图查询语言,Cypher (www.opencypher.org)。这种类似 SQL 的语言最初是专为 Neo4j 数据库设计的专有查询机制,但自 2015 年以来,它已成为一个开放标准。此后,其他公司(如 SAP HANA 和 Databricks)和项目(如 Apache Spark 和 RedisGraph)已将其作为图数据库的查询语言采用。
本部分不需要对 Cypher 语言有任何先前的了解,并且在整个书中,所有查询都进行了详细描述和注释。如果您想了解更多关于它的信息,我建议您查看官方文档以及一些详细描述该主题的书籍[Robinson 等人,2015,以及 Vukotic 等人,2014]。此外,为了更好地理解本部分和下一部分的内容,我建议您安装和配置 Neo4j 并运行查询。这为您提供了学习新查询语言、玩转图和更好地在脑海中固定这里提出的概念的机会。附录 B 提供了对 Neo4j 和 Cypher 的快速介绍,并解释了为什么本书使用 Neo4j 作为参考图数据库。安装指南可在那里以及 Neo4j 开发者网站上找到(neo4j.com/docs/operations-manual)。查询和代码示例与当时可用的最新版本 4.x 进行了测试。
本部分章节专注于将数据建模应用于推荐引擎。由于这个主题是一个常见的与图相关的机器学习主题,因此介绍了多种技术,并进行了详细的阐述。
术语推荐系统(RS)指的是所有通过使用他们可以收集到的关于相关用户和项目的知识,向特定用户推荐可能感兴趣的项目[Ricci 等人,2015]。这些建议可能与各种决策过程相关,例如购买什么产品、听什么音乐或看什么电影。在这种情况下,项目是一个通用术语,用于识别系统向用户推荐的内容。推荐系统通常专注于特定类型或类别的项目,例如要购买的书、要阅读的新闻文章或要预订的酒店。整体设计和用于生成推荐的技术都是为了提供对该特定类型项目有用的和相关的建议。有时,可以使用从一类项目中收集到的信息来提供其他类型项目的推荐。例如,购买西装的人可能也对商业书籍或昂贵的手机感兴趣。
虽然推荐系统(RS)的主要目的是帮助公司卖出更多商品,但它们从用户的角度来看也有很多优点。用户不断被选择所淹没:要阅读的新闻、要购买的产品、要看的节目等等。不同提供商提供的项目集合正在迅速增长,用户无法再对所有这些进行筛选。在这种情况下,推荐引擎提供了一个定制的体验,帮助人们更快地找到他们正在寻找的或可能对他们感兴趣的东西。结果是,由于他们在短时间内获得了相关的结果,用户的满意度将会更高。
越来越多的服务提供商正在利用这一套工具和技术 [Ricci 等人,2015],原因包括以下几点:
-
增加销售的商品数量—帮助提供商卖出比不提供任何推荐时更多的商品可能是 RS 最重要的功能。这一目标得以实现,因为推荐的商品很可能会满足用户的需求和愿望。在大多数环境中,由于有大量商品(新闻文章、书籍、手表等)可供选择,用户经常被大量信息淹没,以至于他们找不到他们想要的东西,结果是他们的会话没有以具体行动结束。在这种情况下,RS 在精炼用户需求和期望方面提供了一个有效的帮助。
-
销售更多样化的商品—RS 可以完成的另一个重要功能是允许用户选择在没有精确推荐的情况下可能难以找到的商品。例如,在旅游 RS 中,服务提供商可能希望推广可能对特定用户在该地区感兴趣的地方,而不仅仅是最受欢迎的地方。通过提供定制建议,提供商大大降低了广告可能不适合该用户口味的地方的风险。通过向用户推荐或广告不太受欢迎(在不太知名的意义上)的地方,RS 可以提高他们在该地区的整体体验质量,并允许新地方被发现并变得流行。
-
提高用户满意度—一个设计得当的 RS 可以改善用户对应用的使用体验。有多少次,在浏览像亚马逊这样的在线书店时,你看到推荐并想,“哇,那本书肯定很有趣”?如果用户发现推荐有趣、相关,并且由设计良好的前端温和地提出,他们就会享受使用系统。有效、准确的推荐和可用界面的杀手级组合将提高用户对系统的主观评价,并且他们很可能会再次回来。因此,这种方法增加了系统的使用率、可用于模型构建的数据、推荐的质量和用户满意度。
-
提高用户忠诚度—网站和其他以客户为中心的应用通过识别回头客并将他们视为尊贵的访客来欣赏和鼓励忠诚度。对于 RS(除了一些例外情况)来说,跟踪回头用户是一个常见的要求,因为算法会使用用户在之前互动中获得的信息,例如他们对项目的评分,来在用户下一次访问过程中做出推荐。因此,用户与网站或应用互动的频率越高,用户的模型就越精细;他们偏好的表示就发展起来,推荐系统的输出效果也得到提高。
-
更好地理解用户的需求——一个正确实施的 RS 的另一个重要副作用是它为用户的偏好创建了一个模型,这些偏好可以是明确收集的,也可以由系统本身预测。服务提供商可以重新使用由此产生的新知识来实现其他目标,例如改善物品的库存或生产管理。在旅游领域,目的地管理机构可以选择向新客户群体宣传特定地区,或使用通过分析 RS 收集的数据(用户的交易)得出的特定类型的促销信息。
在设计 RS 时必须考虑这些方面,因为它们不仅影响系统收集、存储和处理数据的方式,还影响其用于预测的方式。由于这里列出的某些原因(如果不是全部原因),数据图表示和基于图的分析可以通过简化数据管理、挖掘、通信和交付发挥重要作用。这些方面在本部分中得到强调。
值得注意的是,我们谈论的是个性化推荐。换句话说,每个用户都会根据他们的口味收到不同的推荐列表,这些口味是基于之前的互动或通过不同方法收集的信息推断出来的[Jannach 等人,2010]。提供个性化推荐需要系统了解每个用户和每个物品的一些(或很多)信息。因此,RS 必须开发和维护一个包含用户偏好数据的用户模型(或用户配置文件),以及包含物品特征或其他细节的项目模型(或项目配置文件)。
用户和项目模型的创建是每个推荐系统的核心。然而,这种信息的收集、建模和利用方式取决于特定的推荐技术和相关的学习算法。根据构建模型所使用的信息类型以及预测用户兴趣和提供预测的方法,可以实现不同类型的推荐系统。
本部分探讨了四种主要的推荐技术。这些技术只是可用的解决方案中的一部分样本,但我选择它们是因为它们涵盖了广泛的机会和建模示例。这四种方法包括
-
基于内容的推荐(第四章)——推荐引擎使用项目描述(手动创建或自动提取)和用户配置文件,这些配置文件将重要性分配给不同的特征。它学会找到与用户过去喜欢(互动过)的内容相似的项目。一个典型的例子是新闻推荐引擎,它将用户之前阅读的文章与最新的文章进行比较,以找到在内容上相似的项目。
-
协同过滤(第五章)——协同推荐背后的基本思想是,如果用户在过去有相同的兴趣——例如购买类似书籍或观看类似电影,那么他们未来也会有相同的行为。这种方法的最著名例子是亚马逊的推荐系统,它使用用户与物品的交互历史来向用户提供推荐。
-
基于会话的推荐(第六章)——推荐引擎根据会话数据(如会话点击和点击物品的描述)进行预测。当用户资料和过去活动的细节不可用时,基于会话的方法很有用。它使用有关当前用户交互的信息,并将其与其他用户的先前交互相匹配。一个例子是提供酒店、别墅和公寓详情的旅游网站;用户通常在过程结束时才登录,那时是预订的时候。在这种情况下,没有关于用户的历史信息。
-
上下文感知推荐(第七章)——推荐引擎通过适应用户的特定上下文来生成相关的推荐 [Adomavicius et al., 2011]。上下文信息可能包括位置、时间或公司(用户与谁在一起)。许多移动应用程序使用上下文信息(位置、天气、时间等)来细化提供给用户的推荐。
这个列表并不全面,也不是唯一可用的分类。从某些角度来看,基于会话和上下文感知的推荐可能被认为是协同过滤的子类别,这取决于实现它们的算法类型。然而,这个列表反映了本书中描述方法的方式。
每种方法都有其优缺点。混合推荐系统结合方法来克服这些问题,并向用户提供更好的推荐。混合推荐方法在第七章中也有讨论。
参考文献
[Domingos, 2012] Domingos, Pedro. “关于机器学习的一些有用知识。” 《ACM 通讯》 55:10 (2012): 78-87. doi: dx.doi.org/10.1145/ 2347736.2347755
Cypher 查询语言参考,版本 9. mng.bz/zGza.
[Robinson et al., 2015],Robinson, Ian, Jim Webber, 和 Emil Eifrem. 图数据库. 2nd ed. Sebastopol, CA: O’Reilly, 2015.
[Vukotic et al., 2014] Vukotic, Aleksa, Dominic Fox, Jonas Partner, Nicki Watt, 和 Tareq Abedrabbo. Neo4j 实战. Shelter Island, NY: Manning, 2014.
[Ricci et al., 2015] Ricci, Lior Rokach, 和 Bracha Shapira. 推荐系统手册. 2nd ed. New York: Springer, 2015.
[Jannach 等人,2010] Jannach, Dietmar, Markus Zanker, Alexander Felfernig, 和 Gerhard Friedrich. 推荐系统:导论. 英国剑桥:剑桥大学出版社,2010. doi: dx.doi.org/10.1017/CBO9780511763113
[Adomavicius 等人,2011] Adomavicius, Gediminas, Bamshad Mobasher, Francesco Ricci, 和 Alexander Tuzhilin. “上下文感知推荐系统.” 人工智能杂志 32:3 (2011): 67-80, 2011. DOI: doi.org/10.1609/aimag.v32i3.2364
^(1.)如第一章所述,如果一个代理在观察世界之后能够提高其在未来任务上的表现,则认为该代理正在学习。
4 基于内容的推荐
本章涵盖
-
为基于内容的推荐引擎设计合适的图模型
-
将现有的(非图)数据集导入设计的图模型
-
实现工作的基于内容的推荐引擎
假设你想为你的本地视频租赁店构建一个电影推荐系统。老式的 Blockbuster 风格的租赁店大多已被新的流媒体平台如 Netflix(mng.bz/0rBx)所取代,但还有一些仍然存在。在我的镇上就有一个。在我上大学的时候(很久以前),我经常和哥哥每个星期天去那里租一些动作电影。(记住这个偏好;它以后会很有用!)这里的一个重要事实是,这个场景本质上与更复杂的在线推荐系统有很多共同之处,包括以下内容:
-
一个小型的用户群体—用户或客户数量相当少。大多数推荐引擎,如我们稍后将要讨论的,需要大量的活跃用户(从交互次数的角度来看,如查看、点击或购买)才能有效。
-
一组精心挑选的项目—每个项目(在这种情况下,是一部电影)可以有很多相关的细节。对于电影,这些细节可能包括剧情描述、关键词、类型和演员。在其他场景中,这些细节并不总是可用,例如,当项目只有一个标识符时。
-
了解用户偏好—店主或店员几乎知道几乎所有客户的偏好,即使他们只租赁过几部电影或游戏。
在我们讨论技术细节和算法之前,花一点时间思考一下实体店铺。想想店主或店员为了成功都做了些什么。他们通过分析客户的先前租赁习惯和记住与他们交谈的内容来努力了解他们的客户。他们试图为每位客户创建一个档案,包含他们的品味(恐怖和动作电影而不是浪漫喜剧)、习惯(通常在周末或工作日租赁)、项目偏好(电影而不是电子游戏)等详细信息。他们随着时间的推移收集信息来建立这个档案,并使用他们创建的心理模型以有效的方式欢迎每位客户,建议可能对他们感兴趣的东西,或者当商店有吸引人的新电影时,也许会给他们发一条消息。
现在考虑一个虚拟店员,它欢迎网站的访客,建议他们租借电影或游戏,或者当有新商品上架可能引起兴趣时发送电子邮件。前面描述的条件排除了某些推荐方法,因为它们需要更多的数据。在我们考虑的情况中(无论是真实还是简化的虚拟商店),一个有价值的解决方案是基于内容的推荐系统(CBRS)。CBRSs 依赖于项目描述(内容)来构建项目表示(或项目档案)和用户档案,以建议与目标用户过去喜欢的项目相似的项目。(这类推荐系统也被称为语义感知 CBRSs。)这种方法允许系统在可用的数据量相当小的情况下提供推荐(即,有限数量的用户、项目或交互)。
生成基于内容的推荐的基本过程包括匹配目标用户档案中的属性,其中建模了偏好和兴趣,与项目的属性,以找到与用户过去喜欢的项目相似的项目。结果是相关性分数,它预测目标用户对那些项目的兴趣水平。通常,用于描述项目的属性是从与该项目相关的元数据中提取的特征或与项目相关的文本特征——描述、评论、关键词等。这些内容丰富的项目本身就包含大量信息,可用于比较或根据用户与之交互的项目列表推断用户的兴趣。因此,CBRSs 不需要大量数据就能有效。
图 4.1 显示了 CBRS 的高级架构,这是许多可能架构之一,也是本节中使用的架构。

图 4.1 CBRS 的高级架构
此图将推荐过程分解为三个主要组成部分:
-
项目分析器——此组件的主要目的是分析项目,提取或识别相关特征,并以适合后续处理步骤的形式表示项目。它从一个或多个信息源中获取项目内容(如书籍或产品描述的内容)和元信息(如书籍的作者、电影中的演员或电影类型),并将它们转换为用于后续提供推荐的项模型。在本节中描述的方法中,这种转换产生图模型,可以是不同类型的。这种图表示用于向推荐过程提供数据。
-
用户资料构建器—此过程收集代表用户偏好的数据并推断用户资料。此信息集可能包括通过询问用户关于他们的兴趣收集到的显式用户偏好或通过观察和存储用户行为收集到的隐式反馈。结果是模型——具体来说,是表示用户对某些项目、项目特征或两者的兴趣的图模型。在图 4.1 所示的架构中,项目资料(在项目分析阶段创建)和用户资料(在此阶段创建)在同一个数据库中汇聚。此外,由于这两个过程都返回图模型,它们的输出可以组合成一个单一的、易于访问的图模型,用作下一阶段的输入。
-
推荐引擎—此模块通过匹配用户兴趣与项目特征来利用用户资料和项目表示,建议相关项目。在这个阶段,你构建一个预测模型并使用它为每个用户创建每个项目的相关性得分。这个得分用于对项目进行排序和排序,以向用户推荐。一些推荐算法预先计算相关值,例如项目相似度,以使预测阶段更快。在本方法中,这些新值被存储回图中,从而通过从项目资料中推断出的其他数据丰富了图。
在 4.1 节中,每个模块都进行了更详细的描述。具体来说,我描述了如何使用图模型来表示项目分析和资料构建阶段输出的项目和用户资料。这种方法简化了推荐阶段。
就像本章的其余部分以及从现在开始的大多数书籍一样,将展示真实示例,使用公开可用的数据集和数据源。MovieLens 数据集(grouplens.org/datasets/movielens)包含真实用户提供的电影评分,是推荐引擎测试的标准数据集。然而,这个数据集并没有包含很多关于电影的信息,并且基于内容的推荐器需要内容才能工作。这就是为什么在我们的示例中,它与来自互联网电影数据库(IMDb)的数据结合使用,例如故事梗概、关键词、类型、演员、导演和编剧。
4.1 表示项目特征
在基于内容的推荐方法中,一个项目可以通过一组特征来表示。特征(也称为属性或属性)是该项目的具有重要性或相关性的特征。在简单的情况下,这些特征很容易发现、提取或收集。在电影推荐示例中,每部电影都可以通过使用以下特征来描述
-
类型或类别(恐怖、动作、卡通、戏剧等)
-
故事梗概
-
演员
-
手动(或自动)分配给电影的标签或关键词
-
制作年份
-
导演
-
编剧
-
制作人
考虑表 4.1 中提供的信息(来源:IMDb)。
表 4.1 电影相关数据示例
| 标题 | 类型 | 导演 | 编剧 | 演员 |
|---|---|---|---|---|
| 低俗小说 | 动作,犯罪,惊悚 | 昆汀·塔伦蒂诺 | 昆汀·塔伦蒂诺,罗杰·阿维里 | 约翰·特拉沃尔塔,塞缪尔·杰克逊,布鲁斯·威利斯,乌玛·瑟曼 |
| 惩罚者(2004) | 动作,冒险,犯罪,剧情,惊悚 | 乔纳森·亨谢利 | 乔纳森·亨谢利,迈克尔·弗朗斯 | 托马斯·简,约翰·特拉沃尔塔,萨曼莎·玛瑟斯 |
| 杀戮比尔:第一卷 | 动作,犯罪,惊悚 | 昆汀·塔伦蒂诺 | 昆汀·塔伦蒂诺,乌玛·瑟曼 | 乌玛·瑟曼,刘玉玲,薇薇卡·A·福克斯 |
这些特征通常被定义为 元信息,因为它们实际上不是项目的内 容。不幸的是,有一些项目的类别,找到或识别特征并不容易,例如文档集合、电子邮件消息、新闻文章和图像。
文本型项目通常没有现成的特征集。尽管如此,它们的内容可以通过识别描述它们的特征集来表示。一种常见的方法是识别表征主题的单词。存在不同的技术来完成这项任务,其中一些在 12.4.2 节中有所描述;结果是特征列表(关键词、标签、相关单词),这些特征描述了项目的内 容。这些特征可以用来以与这里元信息相同的方式表示文本型项目,因此从现在开始描述的方法可以在元信息特征易于访问或需要从内容中提取特征时应用。从图像中提取标签或特征超出了本书的范围,但一旦提取了这些特征,方法与本章讨论的方法完全相同。
虽然在图中表示这样的特征列表——更确切地说,是一个 属性图²(#pgfId-1011891)——是直 接的,但在设计项目模型时,你应该考虑一些建模最佳实践。以一个简化的例子来说,考虑表 4.1 中电影的图模型,如图 4.2 所示,以及它们相关的特征。

图 4.2 基于图的项目基本表示
在这个图中,使用了可能的最简单表示方式,包括相关的属性列表。对于每个项目,创建一个单独的节点,并将特征建模为节点的属性。列表 4.1 展示了用于创建三个电影的 Cypher 查询。(逐个运行查询)。请参考附录 B 获取关于 Neo4j 的基本信息、安装指南以及 Cypher 的快速介绍。你将在本书的其余部分学习到其他内容。
列表 4.1 创建电影表示基本模型的查询
CREATE (p:Movie { ❶
title: 'Pulp Fiction', ❷
actors: ['John Travolta', 'Samuel Jackson', 'Bruce Willis', 'Uma Thurman'],
director: 'Quentin Tarantino',
genres: ['Action', 'Crime', 'Thriller'],
writers: ['Quentin Tarantino', 'Roger Avary'],
year: 1994 ❸
}) ❹
CREATE (t:Movie {
title: 'The Punisher',
actors: ['Thomas Jane', 'John Travolta', 'Samantha Mathis'],
director: 'Jonathan Hensleigh',
genres: ['Action', 'Adventure', 'Crime', 'Drama', 'Thriller'],
writers: ['Jonathan Hensleigh', 'Michael France'],
year: 2004
})
CREATE (k:Movie {
title: 'Kill Bill: Volume 1',
actors: ['Uma Thurman', 'Lucy Liu', 'Vivica A. Fox'],
director: 'Quentin Tarantino',
genres: ['Action', 'Crime', 'Thriller'],
writers: ['Quentin Tarantino', 'Uma Thurman'],
year: 2003
})
❶ 每个 CREATE 语句创建一个带有 Movie 标签的新节点。
❷ 大括号定义了节点的键/值属性列表,从标题开始。
❸ 属性可以是不同类型:字符串、数组、整数、双精度浮点数等。
❹ 括号定义了创建的节点实例的边界。
在 Cypher 查询中,CREATE 允许你创建一个新的节点(或关系)。括号定义了创建的节点实例的边界,在这些情况下由 p、t 和 k 标识,并且每个新节点都被分配了一个特定的标签,即 Movie。标签指定了节点的类型或节点在图中所扮演的角色。使用标签不是强制性的,但它是组织图中节点的一种常见且有用的做法(并且比为每个节点分配类型属性更高效)。标签有点像旧式关系数据库中的表,标识节点类别,但在属性图数据库中,对属性列表没有约束(如关系模型中的列那样)。每个节点,无论分配给它什么标签,都可以包含任何一组属性或甚至没有任何属性。此外,一个节点可以有多个标签。属性图数据库的这两个特性——对属性列表没有约束和多个标签——使得结果模型非常灵活。最后,在花括号内指定了一组以逗号分隔的属性。
单节点设计方法的优势在于节点与具有所有相关属性的项目之间的一对一映射。通过有效的索引配置,通过特征值检索电影非常快。例如,检索由 Quentin Tarantino 执导的所有电影的 Cypher 查询看起来如下所示。
列表 4.2 查询以搜索由 Quentin Tarantino 执导的所有电影
MATCH (m:Movie) ❶
WHERE m.director = 'Quentin Tarantino' ❷
RETURN m ❸
❶ MATCH 子句定义了要匹配的图模式:在这种情况下是带有标签 Movie 的节点。
❷ WHERE 子句定义了过滤条件。
❸ RETURN 子句指定了要返回的元素列表。
在这个查询中,MATCH 子句用于定义要匹配的图模式。在这里,我们正在寻找所有的 Movie 节点。WHERE 子句是 MATCH 的一部分,并添加了约束——过滤器,就像关系 SQL 中的那样。在这个例子中,查询是通过导演的名字进行过滤的。RETURN 子句指定了要返回的内容。图 4.3 显示了从 Neo4j 浏览器运行此查询的结果。

图 4.3 Neo4j 浏览器对简单模型的查询结果
简单模型存在多个缺点,包括以下内容:
-
数据重复—在每个属性中,数据都是重复的。例如,导演的名字在所有由同一导演执导的电影中都是重复的,对于作者、类型等也是如此。数据重复在数据库所需的磁盘空间和数据一致性(我们如何知道“Q. Tarantino”和“Quentin Tarantino”是否相同?)方面是一个问题,并且它使得更改变得困难。
-
易出错性—尤其是在数据导入期间,此简单模型容易受到诸如拼写错误和属性名称等问题的影响。如果数据在每个节点中是孤立的,这些错误很难识别。
-
难以扩展/丰富—如果在模型的生命周期中需要扩展,例如分组流派以改进搜索能力或提供语义分析,这些功能很难提供。
-
导航复杂性—任何访问或搜索都是基于值比较,或者更糟糕的是,基于字符串比较。这种模型没有使用图的真实力量,图能够有效地导航关系和节点。
为了更好地理解为什么这种模型在导航和访问模式方面表现不佳,假设你想查询“在同一部电影中共同工作的演员”。此类查询可以写成如图表 4.3 所示。
列表 4.3 查询以找到共同工作的演员(简单模型)。
MATCH (m:Movie) ❶
WITH m.actors as actors ❷
UNWIND actors as actor ❸
MATCH (n:Movie)
WHERE actor IN n.actors ❹
WITH actor, n.actors as otherActors, n.title as title ❺
UNWIND otherActors as otherActor ❻
WITH actor, otherActor, title ❼
WHERE actor <> otherActor ❽
RETURN actor, otherActor, title
ORDER BY actor ❾
❶ 搜索所有电影。
❷ 将演员列表传递到下一步。
❸ 此 UNWIND 将演员列表转换为多行。
❹ 此第二个 MATCH 与 WHERE 过滤器搜索每个演员参演的所有电影。
❺ 将演员列表、该演员参演的每部电影中的演员列表(同电影中的合演者)以及电影标题传递。
❻ 将其他演员的列表转换为多行。
❼ 将演员对和共同参演的电影的标题传递。
❽ 过滤掉演员与自己配对的配对。
❾ 按对中第一个演员的姓名排序结果。
此查询的工作方式如下:
-
第一个 MATCH 搜索所有电影。
-
WITH 用于将结果传递到下一步。第一个 WITH 仅传递演员列表。
-
使用 UNWIND,你可以将任何列表转换回单独的行。每部电影中的演员列表被转换成一系列演员。
-
对于每个演员,下一个 MATCH 与 WHERE 条件一起找到他们参演的所有电影。
-
第二个 WITH 传递本次迭代中考虑的演员、他们参演的每部电影中的演员列表以及电影标题。
-
第二个 UNWIND 将其他演员的列表转换,并将演员-其他演员对以及他们共同参演的电影的标题一起传递。
-
最后的 WHERE 过滤器过滤掉演员与自己配对的配对。
-
查询返回每对中的姓名以及他们共同参演的电影的标题。
-
结果按 ORDER BY 子句排序,即按对中第一个演员的姓名排序。
在此类查询中,所有比较都是基于字符串匹配,因此如果存在拼写错误或不同的格式(例如,“U. Thurman”而不是“Uma Thurman”),结果将是不正确或不完整的。图 4.4 显示了在我们创建的图数据库上运行此查询的结果。

图 4.4 使用我们创建的示例数据库查询的结果。
一种更高级的表示物品的模型,对于这些特定目的来说更加有用和强大,它将重复出现的属性作为节点暴露出来。在这个模型中,每个实体,如演员、导演或类型,都有自己的表示——自己的节点。这些实体之间的关系由图中的边表示。边也可以包含一些属性来进一步描述关系。图 4.5 显示了新模型在电影场景中的样子。

图 4.5 基于图的高级物品表示
在高级模型中,新节点出现以表示每个特征值,特征类型由标签如类型、演员、导演和编剧指定。某些节点可以有多个标签,因为它们可以在同一部电影或不同电影中扮演多个角色。每个节点都有一些属性来描述它,例如演员和导演的名字以及类型的类型。现在电影只有标题属性,因为这个属性是针对项目本身的特定属性;没有理由提取它并将其表示为单独的节点。创建此新图模型的电影示例的查询如下所示。³
列表 4.4 创建电影表示的高级模型的查询
CREATE CONSTRAINT ON (a:Movie) ASSERT a.title IS UNIQUE; ❶
CREATE CONSTRAINT ON (a:Genre) ASSERT a.genre IS UNIQUE;
CREATE CONSTRAINT ON (a:Person) ASSERT a.name IS UNIQUE;
CREATE (pulp:Movie {title: 'Pulp Fiction'}) ❷
FOREACH (director IN ['Quentin Tarantino'] ❸
| MERGE (p:Person {name: director}) SET p:Director MERGE (p)-[:DIRECTED]->
➥ (pulp)) ❹
FOREACH (actor IN ['John Travolta', 'Samuel L. Jackson', 'Bruce Willis',
➥ 'Uma Thurman']
| MERGE (p:Person {name: actor}) SET p:Actor MERGE (p)-[:ACTS_IN]->(pulp))
FOREACH (writer IN ['Quentin Tarantino', 'Roger Avary']
| MERGE (p:Person {name: writer}) SET p:Writer MERGE (p)-[:WROTE]->(pulp))
FOREACH (genre IN ['Action', 'Crime', 'Thriller']
| MERGE (g:Genre {genre: genre}) MERGE (pulp)-[:HAS]->(g))
CREATE (punisher:Movie {title: 'The Punisher'})
FOREACH (director IN ['Jonathan Hensleigh']
| MERGE (p:Person {name: director}) SET p:Director MERGE (p)-[:DIRECTED]->
➥ (punisher))
FOREACH (actor IN ['Thomas Jane', 'John Travolta', 'Samantha Mathis']
| MERGE (p:Person {name: actor}) SET p:Actor MERGE (p)-[:ACTS_IN]->
➥ (punisher))
FOREACH (writer IN ['Jonathan Hensleigh', 'Michael France']
| MERGE (p:Person {name: writer}) SET p:Writer MERGE (p)-[:WROTE]->
➥ (punisher))
FOREACH (genre IN ['Action', 'Adventure', 'Crime', 'Drama', 'Thriller']
| MERGE (g:Genre {genre: genre}) MERGE (punisher)-[:HAS]->(g))
CREATE (bill:Movie {title: 'Kill Bill: Volume 1'})
FOREACH (director IN ['Quentin Tarantino']
| MERGE (p:Person {name: director}) SET p:Director MERGE (p)-[:DIRECTED]->
➥ (bill))
FOREACH (actor IN ['Uma Thurman', 'Lucy Liu', 'Vivica A. Fox']
| MERGE (p:Person {name: actor}) SET p:Actor MERGE (p)-[:ACTS_IN]->(bill))
FOREACH (writer IN ['Quentin Tarantino', 'Uma Thurman']
| MERGE (p:Person {name: writer}) SET p:Writer MERGE (p)-[:WROTE]->(bill))
FOREACH (genre IN ['Action', 'Crime', 'Thriller']
| MERGE (g:Genre {genre: genre}) MERGE (bill)-[:HAS]->(g))
❶ 这些语句中的每一个都在数据库中创建一个唯一约束。
❷ 每个 CREATE 语句都只使用标题属性创建电影。
❸ FOREACH 循环遍历一个列表并对每个元素执行 MERGE 操作。
❹ MERGE 首先检查节点是否已经存在,在这种情况下使用导演名字的唯一性;如果不存在,则创建该节点。
虽然图数据库通常被称为无模式,但在 Neo4j 中可以在数据库中定义一些约束。在这种情况下,前三个查询分别创建三个约束,分别针对电影标题的唯一性、类型的值和人物的名字,从而防止例如同一个人(演员、导演或编剧)在数据库中多次出现。如前所述,在新模型中,想法是数据库中只有一个节点代表一个单一实体。这些约束有助于强制执行这种建模决策。
在创建约束之后,CREATE 子句(在示例中重复三次,每次针对一部电影)像以前一样工作,以创建每个新的电影,并将标题作为属性。然后 FOREACH 子句分别遍历导演、演员、编剧和类型,对于每个元素,它们搜索与电影节点连接的节点,如果需要则创建新的节点。在演员、编剧和导演的情况下,通过 MERGE 子句创建一个具有标签 Person 的通用节点。MERGE 确保提供的模式存在于图中,要么通过重用与提供的谓词匹配的现有节点和关系,要么通过创建新的节点和关系。在这种情况下,SET 子句根据需要为节点分配一个新的特定标签。FOREACH 中的 MERGE 检查(并在必要时创建)人与电影之间的关系。对于类型也采用类似的方法。整体结果如图 4.6 所示。
模型技巧
你可以为同一个节点使用多个标签。在这种情况下,这种方法既有用又必要,因为在模型中,我们希望每个人都能被独特地表示,无论他们在电影中扮演什么角色(演员、编剧或导演)。因此,我们选择使用 MERGE 而不是 CREATE,并为所有人使用一个共同的标签。同时,图模型为每个人扮演的每个角色分配一个特定的标签。一旦分配,标签就会分配给节点,这样运行查询如“找到所有制片人...”将会更容易和更高效。

图 4.6 三部电影的高级基于图的项表示
新的描述性模型不仅解决了之前描述的所有问题,还提供了多个优势:
-
无数据重复—将每个相关实体(人物、类型等)映射到特定的节点可以防止数据重复。同一个实体可以扮演不同的角色并具有不同的关系。(乌玛·瑟曼不仅在《杀死比尔:卷一》中是女演员,还是编剧之一。)此外,对于每个项目,可以存储一个包含替代形式或别名的列表(“Q. Tarantino”,“Tarantino 导演”,“Quentin Tarantino”)。这种方法有助于搜索并防止同一概念在多个节点中表示。
-
错误容错性—防止数据重复保证了更好的对值错误的容错性。与之前模型不同,在之前的模型中,由于拼写错误作为属性分布在所有节点之间,很难被发现,这里信息集中在隔离且非重复的实体中,使得错误容易识别。
-
易于扩展/丰富—可以使用共同的标签或创建一个新的节点并将其连接到相关节点来对实体进行分组。这种方法可以提高查询性能或风格。我们可以在一个共同的戏剧节点下连接多个类型,如犯罪和惊悚。
-
易于导航——每个节点甚至每个关系都可以作为导航的入口点(演员、类型、导演等等),而在先前的模式中,唯一的入口点是节点中的特征。这种方法使得对数据的访问模式更加多样化和高效。
再次考虑“在同一部电影中共同工作的演员”的查询。在新模型中,构建此查询要容易得多,如下列所示。
列表 4.5 查询以找到所有共同工作的演员(高级模型)
MATCH (actor:Actor)-[:ACTS_IN]->(movie:Movie)<-[:ACTS_IN]-(otherActor:Actor)❶
WHERE actor <> otherActor ❷
RETURN actor.name as actor, otherActor.name as otherActor,
movie.title as title
ORDER BY actor
❶ 在这种情况下,MATCH 子句指定了一个更复杂的图模式。
❷ 标识对被移除。
列表 4.5 产生的结果与列表 4.4 完全相同,但它更简单、更清晰,甚至更快——显然是更好地使用了 MATCH 子句。在这里,查询描述的不是单个节点,而是我们正在寻找的整个图模式;我们正在寻找两位共同出演过同一部电影的电影演员,WHERE 子句过滤掉了原始演员。结果如图 4.7 所示。

图 4.7 列表 4.5 的样本数据库创建结果
值得注意的是,这里没有字符串比较。此外,查询要简单得多,在一个更大的数据库中,它将执行得更快。如果您还记得我们关于原生图数据库(第 2.3.4 节)的讨论以及 Neo4j 如何实现节点关系的无邻接索引(附录 B),它将比列表 4.3 中必要的字符串索引查找要快得多。
我们已经设计了我们用于表示项目的最终图模型。在一个真实的机器学习项目中,下一步将是创建数据库,从一个或多个来源导入数据。如本节开头所述,MovieLens 数据集被选为测试数据集。您可以从 GroupLens(grouplens.org/datasets/movielens)下载该数据集。代码仓库包含了在正确的目录下下载的说明和步骤,以及设置代码正确运行的流程。根据您愿意等待多长时间来查看第一个图数据库,您可以选择合适的数据集大小。(如果您不耐烦,请选择最小的。)数据集只包含关于每部电影的一点点信息,例如标题和一系列类型,但它还包含一个指向 IMDb 的引用,在那里可以访问有关电影的各类详细信息:剧情、导演、演员、编剧等等。这些数据正是我们所需要的。
列表 4.6 和 4.7 包含了从 MovieLens 数据集中读取数据、在图中存储第一个节点以及使用 IMDb 上可用的信息来丰富它们的 Python 代码。(您应该整理您的数据库,但这不是强制性的。)
列表 4.6 从 MovieLens 导入基本电影信息
def import_movies(self, file):
with open(file, 'r+') as in_file:
reader = csv.reader(in_file, delimiter=',') ❶
next(reader, None)
with self._driver.session() as session: ❷
self.executeNoException(session,
"CREATE CONSTRAINT ON (a:Movie) ASSERT a.movieId IS UNIQUE; ")❸
self.executeNoException(session,
"CREATE CONSTRAINT ON (a:Genre) ASSERT a.genre IS UNIQUE; ") ❸
tx = session.begin_transaction() ❹
i = 0;
j = 0;
for row in reader:
try:
if row:
movie_id = strip(row[0])
title = strip(row[1])
genres = strip(row[2])
query = """ ❺
CREATE (movie:Movie {movieId: $movieId,
➥ title: $title})
with movie
UNWIND $genres as genre
MERGE (g:Genre {genre: genre})
MERGE (movie)-[:HAS]->(g)
"""
tx.run(query, {"movieId": movie_id, "title": title,
➥ "genres": genres.split("|")})
i += 1
j += 1
if i == 1000: ❻
tx.commit()
print(j, "lines processed")
i = 0
tx = session.begin_transaction()
except Exception as e:
print(e, row, reader.line_num)
tx.commit()
print(j, "lines processed")
❶ 从 CSV 文件(movies.csv)读取值
❷ 启动一个新的会话连接到 Neo4j
❸ 为保证人和类型的唯一性创建约束。函数 executeNoException 包装了如果约束已存在时生成的异常。
❹ 开始一个新的事务,这将允许数据库操作的原子性(全部进入或全部退出)
❺ 创建电影和类型(MERGE 防止多次创建相同的类型)并将它们连接起来
❻ 高级技巧:为了避免在最后进行大量提交,此检查确保每处理 1,000 行数据就向数据库提交一次。
列表 4.7 使用 IMDb 上可用的详细信息丰富数据库
def import_movie_details(self, file):
with open(file, 'r+') as in_file:
reader = csv.reader(in_file, delimiter=',')
next(reader, None)
with self._driver.session() as session:
self.executeNoException(session, "CREATE CONSTRAINT ON (a:Person)
➥ ASSERT a.name IS UNIQUE;") ❶
tx = session.begin_transaction()
i = 0;
j = 0;
for row in reader:
try:
if row:
movie_id = strip(row[0])
imdb_id = strip(row[1])
movie = self._ia.get_movie(imdb_id) ❷
self.process_movie_info(movie_info=movie, tx=tx,
➥ movie_id=movie_id) ❸
i += 1
j += 1
if i == 10:
tx.commit()
print(j, "lines processed")
i = 0
tx = session.begin_transaction()
except Exception as e:
print(e, row, reader.line_num)
tx.commit()
print(j, "lines processed")
def process_movie_info(self, movie_info, tx, movie_id):
query = """ ❹
MATCH (movie:Movie {movieId: $movieId} )
SET movie.plot = $plot
FOREACH (director IN $directors | MERGE (d:Person {name: director})
➥ SET d:Director MERGE (d)-[:DIRECTED]->(movie))
FOREACH (actor IN $actors | MERGE (d:Person {name: actor}) SET
➥ d:Actor MERGE (d)-[:ACTS_IN]->(movie))
FOREACH (producer IN $producers | MERGE (d:Person {name: producer})
➥ SET d:Producer MERGE (d)-[:PRODUCED]->(movie))
FOREACH (writer IN $writers | MERGE (d:Person {name: writer}) SET
➥ d:Writer MERGE (d)-[:WROTE]->(movie))
FOREACH (genre IN $genres | MERGE (g:Genre {genre: genre}) MERGE
➥ (movie)-[:HAS]->(g))
"""
directors = []
for director in movie_info['directors']:
if 'name' in director.data:
directors.append(director['name'])
genres = ''
if 'genres' in movie_info:
genres = movie_info['genres'
]
actors = []
for actor in movie_info['cast']:
if 'name' in actor.data:
actors.append(actor['name'])
writers = []
for writer in movie_info['writers']:
if 'name' in writer.data:
writers.append(writer['name'])
producers = []
for producer in movie_info['producers']:
producers.append(producer['name'])
plot = '' ❺
if 'plot outline' in movie_info: ❺
plot = movie_info['plot outline'] ❺
tx.run(query, {"movieId": movie_id, "directors": directors,
➥ "genres": genres, "actors": actors, "plot": plot,
"writers": writers, "producers": producers})
❶ 创建一个新的约束以使人物唯一
❷ 从 IMDb 获取电影详情
❸ 处理来自 IMDb 的信息并将其存储在图中
❹ 与列表 4.4 相同,但电影已经存在
❺ 从电影信息中提取剧情值以在节点上创建剧情属性
这段代码过于简化,需要花费很长时间才能完成,因为访问和解析 IMDb 页面需要时间。在本书的代码仓库中,除了完整的代码实现外,还有一个并行版本的函数 import_movie_details,其中创建了多个线程同时下载和处理多个 IMDb 页面。完成后,生成的图具有图 4.6 所描述的结构。
练习
在新创建的数据库中玩耍,并编写查询来完成以下操作:
-
搜索在相同电影中工作的演员对。
TIP 使用列表 4.3,但在查询末尾添加 LIMIT 50;否则,查询将产生大量结果。
-
统计每个演员出演了多少部电影。
-
获取一部电影(通过 movieId),并列出所有特性。
在本场景中,项目(电影)被正确建模并存储在真实的图数据库中。在第 4.2 节中,我们将对用户进行建模。
4.2 用户建模
在 CBRS 中,存在多种方法用于收集和建模用户配置文件。所选的设计模型将根据偏好的收集方式(隐式或显式)以及过滤策略或推荐方法的类型而有所不同。收集用户偏好的直接方法是询问用户。用户可能对特定类型或关键词、特定演员或导演感兴趣。
从高层次的角度来看,用户配置文件和定义的模型的目的在于帮助推荐引擎为每个项目或项目特性分配一个分数。这个分数有助于按从高到低的顺序对建议给特定用户的项目进行排序。因此,推荐系统属于机器学习领域中的“学习排序”领域。
我们可以通过为用户添加节点并将它们连接到感兴趣的特性来向正在设计的模型添加偏好或兴趣。生成的模式将类似于图 4.8。

图 4.8 具有用户兴趣指向元信息的图模型
用于建模用户偏好的图模型扩展了之前为物品描述的模型,为每个用户添加了一个新节点,并将其连接到用户感兴趣的特征。
建模笔记
为物品设计的先进模型更适合这种场景,因为特征是图中的节点,因此可以通过边连接到用户——与更简单的模型相比,这种模型的另一个优点是,建模兴趣会困难得多,也更痛苦。
或者,系统可以明确要求用户对某些物品进行评分。最佳方法是选择那些能帮助我们最广泛地了解用户品味的物品。生成的图模型看起来像图 4.9。

图 4.9 带有用户显式物品评分的图模型
在这种情况下,用户节点连接到电影。评分作为属性存储在边上。这些方法被称为显式,因为系统要求用户表达自己的品味和偏好。
在另一端,另一种方法是通过对每个用户与物品的交互进行隐式地推断用户的兴趣、品味和偏好。例如,如果我买了大豆奶,那么我可能对类似的产品,如大豆酸奶,感兴趣。在这种情况下,大豆是相关特征。同样,如果一个用户观看了《指环王》三部曲的第一集,那么他们很可能对其他两集或同一奇幻动作类型的其他电影感兴趣。生成的模型看起来像图 4.10。这个模型与图 4.9 中的模型相同;唯一的区别是图 4.10 中的系统收集并存储用户行为数据,以隐式地推断用户的兴趣。

图 4.10 带有用户-物品交互的图模型
值得注意的是,当系统建模用户和物品之间的关系时,无论是否收集用户兴趣的信息是隐式还是显式,都可以通过不同的方法推断用户对特定物品特征的兴趣。从图 4.9 和 4.10 所示图中,列表 4.8 中的 Cypher 查询计算用户和特征之间新的关系,并通过在图中创建新的边来物化它们(存储为新关系以提高访问性能)。
列表 4.8 查询计算用户和物品特征之间的关系^(4)
MATCH (user:User)-[:WATCHED|RATED]->(movie:Movie)-
➥ [:ACTS_IN|WROTE|DIRECTED|PRODUCED|HAS]-(feature) ❶
WITH user, feature, count(feature) as occurrences ❷
WHERE occurrences > 2 ❸
MERGE (user)-[:INTERESTED_IN]->(feature) ❹
❶ | 允许你在 MATCH 模式中指定多个关系类型。
❷ WITH 子句聚合用户和特征,计算出现的次数。
❸ 这个 WHERE 子句允许你只考虑用户观看过的至少三部电影中出现的特征。
❹ 创建关系。使用 MERGE 而不是 CREATE 可以防止同一对节点之间有多个关系。
这个查询搜索所有表示用户观看或评分的所有电影的图模式((u:User)-[:WATCHED|RATED]-> (m:Movie))。它识别出用户和特征。
(movie:Movie)-[:ACTS_IN|WROTE|DIRECTED|PRODUCED|HAS]-(feature)
对于每个用户-特征对,WITH 的输出还表明用户观看具有该特定特征的电影的频率(该特征可能是演员、导演、类型等)。WHERE 子句过滤掉出现次数少于三次的所有特征,以保留最相关的特征,避免在图中填充无用的关系。最后,MERGE 子句创建关系,防止在相同的节点对之间存储多个关系(如果使用 CREATE 会发生这种情况)。生成的模型看起来像图 4.11。

图 4.11 推断关系 INTERESTED_IN 后的图模型
图 4.11 中展示的模型包含以下关系之间的
-
用户和项目。(在建模示例中,我们使用了显式的观看关系,但同样适用于显式的评分。)
-
用户和特征。
第二种类型是从第一种类型开始计算的,使用一个简单的查询。这个例子展示了起始模型的另一种可能的扩展。在这种情况下,不是使用外部知识源,而是从图中本身推断新的信息。在这个特定的情况下,使用图查询来提炼知识并将其转换为新的关系,以实现更好的导航。
MovieLens 数据集包含基于用户评分的显式用户-项目对。(这些配对被认为是显式的,因为用户决定对项目进行评分。)在列表 4.9 中,使用评分来构建一个图,如图 4.10 所示,唯一的区别是 WATCHED 被 RATED 替换,因为它代表用户明确评分的内容。该函数从 CSV 文件中读取,创建用户,并将它们连接到他们评分的电影。
列表 4.9 从 MovieLens 导入用户-项目对
def import_user_item(self, file):
with open(file, 'r+') as in_file:
reader = csv.reader(in_file, delimiter=',')
next(reader, None)
with self._driver.session() as session:
self.executeNoException(session, "CREATE CONSTRAINT ON (u:User)
➥ ASSERT u.userId IS UNIQUE") ❶
tx = session.begin_transaction()
i = 0;
for row in reader:
try:
if row:
user_id = strip(row[0])
movie_id = strip(row[1])
rating = strip(row[2])
timestamp = strip(row[3])
query = """ ❷
MATCH (movie:Movie {movieId: $movieId})
MERGE (user:User {userId: $userId})
MERGE (user)-[:RATED {rating: $rating,
➥ timestamp: $timestamp}]->(movie)
"""
tx.run(query, {"movieId":movie_id, "userId": user_id,
➥ "rating":rating, "timestamp": timestamp})
i += 1
if i == 1000:
tx.commit()
i = 0
tx = session.begin_transaction()
except Exception as e:
print(e, row, reader.line_num)
tx.commit()
❶ 创建约束以保证用户唯一性
❷ 查询通过 movieId 搜索电影,如果不存在则创建用户,并将它们连接起来。
到目前为止,我们设计的图模型能够正确地表示项目和用户,并且能够适应多种变化或扩展,例如语义分析和隐式或显式信息。我们创建并填充了一个真实的图数据库,使用的是通过结合 MovieLens 数据集和 IMDb 信息获得的数据。
练习
在数据库中玩耍,并编写查询来完成以下操作:
-
获取一个用户(通过 userId),并列出该用户感兴趣的所有特征。
-
寻找具有共同兴趣的用户对。
第 4.3 节讨论了如何使用此模型在考虑的电影租赁场景中向最终用户提供推荐。
4.3 提供推荐
在推荐阶段,CBRS 使用用户档案来匹配用户与最有可能引起他们兴趣的物品。根据可用信息和为用户和物品定义的模型,可以为此目的使用不同的算法或技术。从之前描述的模型开始,本节描述了预测用户兴趣和提供推荐的几种技术,按复杂性和准确性的增加顺序呈现。
第一种方法基于图 4.12 中展示的模型,其中明确要求用户指出他们对特征的兴趣,或者从用户与物品的交互中推断出兴趣。

图 4.12 用户兴趣指向元信息的图模型
当适用时,这种方法是:
-
物品通过一个与物品相关的特征列表来表示,例如标签、关键词、类型和演员。这些特征可能是由用户手动(标签)或专业人士(关键词)创建的,或者通过某些提取过程自动生成。
-
用户档案通过将用户连接到他们感兴趣的特征来表示。这些连接以二进制形式描述:喜欢(在图中,用用户和特征之间的边表示)和不喜欢/未知(表示为用户和特征之间没有边)。当没有关于用户兴趣的明确信息时,可以从其他来源(明确或隐式)推断兴趣,如前所述,使用图查询。
这种方法对于电影租赁等场景非常相关,其中元信息可用且更好地描述了物品本身。在这个场景中的整个推荐过程可以总结如图 4.13 所示。

图 4.13 基于内容推荐器中第一个场景的推荐过程
这张高级图表突出了整个过程可以基于图来构建。这种方法不需要复杂或花哨的算法来提供推荐。有了合适的图模型,一个简单的查询就能完成任务。数据已经包含了足够的信息,图结构有助于计算分数并返回给用户排序后的列表,无需预先构建任何模型:描述和预测模型重叠。这种纯图基方法简单但有很多优点:
-
它产生良好的结果。考虑到这种方法所需的努力有限,推荐的品质相当高。
-
它很简单。它不需要复杂的计算或复杂的代码,在提供推荐之前读取和预处理数据。如果数据在图中建模得当,如前所述,就可以实时执行查询并响应用户。
-
可扩展性. 图可以包含其他信息,这些信息对于根据其他数据源或上下文信息细化结果可能很有用。查询可以轻松地更改以考虑新的方面。
通过像列表 4.10 中的查询这样的方式完成提供推荐的任务。
列表 4.10 为用户提供推荐查询
MATCH (user:User)-[i:INTERESTED_IN]->(feature)-[]-(movie:Movie) ❶
WHERE user.userId = "<user Id>" AND NOT exists((user)-[]->(movie)) ❷
RETURN movie.title, count(i) as occurrences
ORDER BY occurrences desc ❸
❶ 从一个用户开始,MATCH 子句搜索所有对该用户感兴趣的电影。
❷ NOT EXISTS() 过滤出用户已经观看或评分的所有电影。
❸ 逆序排序有助于将共享给选定用户的电影推到顶部。
这个查询从用户(WHERE 子句指定一个字符串形式的 userId)开始,识别用户感兴趣的所有特征,并找到包含这些特征的 所有电影。对于每部电影,查询计算重叠特征的数量,并根据这个值对电影进行排序:重叠特征的数量越多,项目可能对用户感兴趣的可能性就越高。
这种方法可以应用于我们之前创建的数据库。MovieLens 数据集包含用户和项目之间的连接,但没有用户与其感兴趣的特征之间的关系;这些特征在数据集中不可用。我们通过使用 IMDb 作为电影特征的知识来源,并应用列表 4.8,来丰富数据集,从而可以计算用户和项目特征之间缺失的关系。使用代码和查询来玩转图数据库并提供推荐。它可能不会很快,但会正常工作。图 4.14 显示了在导入的数据库上运行列表 4.7 的结果。值得注意的是,这里显示的示例中用户 598 已经对 Shrek 和 Shrek 2 进行了评分。

图 4.14 在导入的 MovieLens 数据库上运行列表 4.10 的结果
在本章和本书的后续部分,描述了提高性能的不同技术和方法;在这里,重点是不同的图建模技术和设计选项。
练习
将列表 4.10 重写为仅考虑特定类型或特定年份的电影。
小贴士:通过使用 EXISTS 添加条件到 WHERE 子句。
这种方法效果良好且简单,但通过一点努力,它可以得到极大的改进。第二种方法通过考虑两个主要方面来扩展前一种方法,这两个方面可以改进:
-
在用户配置文件中,对项目特征的兴趣由一个布尔值表示。这个值是 二进制 的,仅表示用户对特征感兴趣。它不赋予这种关系任何权重。
-
计算用户配置文件和项目之间的重叠特征是不够的。我们需要一个函数来计算用户兴趣和项目之间的相似性或共同点。
关于第一点,正如本书中经常提到的,模型是现实的表示,现实是我们在模拟一个可能对某些特征比对其他特征更感兴趣的(例如喜欢动作电影但喜欢杰森·斯坦森的电影)。这些信息可以提高推荐的质量。
关于第二点,与其计算重叠特征的数量,不如通过测量用户资料与项目特征之间的相似度来找到特定用户感兴趣的项目——越接近越好。这种方法需要
-
一个函数,用于测量相似度
-
一个共同表示,以便可以测量项目和用户资料的相似度
选定的函数定义了项目和个人资料所需的表示形式。有多种函数可供选择。其中最精确的函数之一是余弦相似度,在第三章中介绍:

与大多数常见的相似度函数一样,此函数要求每个项目和每个用户资料被投影到共同的向量空间模型 (VSM)中,这意味着每个元素都必须由一个固定维度的向量表示。在这种情况下,整个推荐过程可以总结为图 4.15 中的高级图。

图 4.15 基于内容的推荐器第二场景的推荐过程
与先前的方法相比,在这种情况下,在推荐过程之前有一个中间步骤将项目投影,并将用户资料投影到 VSM 中。为了描述将项目和用户资料转换为 VSM 中的表示的过程,让我们考虑我们的电影推荐场景。假设我们的电影数据集如图 4.16 所示。

图 4.16 电影高级模型
考虑到元信息,如类型和导演,每个项目都可以表示为一个向量。(我们可以使用所有可用的元信息,但下一个表会太大。)在这种情况下,每个向量的维度由类型和导演的所有可能值的列表定义。表 4.2 显示了我们手动创建的简单数据集中这些向量的样子。
表 4.2 将项目转换为向量
| 动作 | 剧情 | 犯罪 | 惊悚 | 冒险 | 昆汀·塔伦蒂诺 | 乔纳森·亨斯莱 | |
|---|---|---|---|---|---|---|---|
| 低俗小说 | 1 | 0 | 1 | 1 | 0 | 1 | 0 |
| 惩罚者 | 1 | 1 | 1 | 1 | 1 | 0 | 1 |
| 杀死比尔:第一卷 | 1 | 0 | 1 | 1 | 0 | 1 | 0 |
这些向量是布尔向量,因为值只能是 0,表示不存在,或者 1,表示存在。代表三部电影的向量是
向量(低俗小说) = [1, 0, 1, 1, 0, 1, 0]
向量(惩罚者) = [1, 1, 1, 1, 1, 0, 1]
向量(杀死比尔:第一卷) = [1, 0, 1, 1, 0, 1, 0]
这些二进制向量可以通过图 4.16 中的图模型通过以下查询提取。
列表 4.11 提取电影的布尔向量
MATCH (feature) ❶
WHERE "Genre" in labels(feature) OR "Director" in labels(feature) ❶
WITH feature
ORDER BY id(feature)
MATCH (movie:Movie) ❷
WHERE movie.title STARTS WITH "Pulp Fiction" ❷
OPTIONAL MATCH (movie)-[r:DIRECTED|HAS]-(feature) ❸
RETURN CASE WHEN r IS null THEN 0 ELSE 1 END as Value, ❹
CASE WHEN feature.genre IS null THEN feature.name ELSE feature.genre END as
➥ Feature ❺
❶ 使用 labels 函数获取分配给节点的标签列表,搜索所有类型为导演或类型的特征。
❷ 搜索电影《低俗小说》。使用 STARTS WITH 比精确字符串比较更可取,因为电影通常在标题中有年份。
❸ 可选的 MATCH 允许我们考虑所有特征,即使它们与所选电影无关。
❹ 如果不存在关系,则此 CASE 子句返回 0,否则返回 1。
❺ 这个 CASE 子句返回导演或类型的名称。
查询首先寻找代表类型或导演的所有节点,并按节点标识符顺序返回它们。顺序很重要,因为在每个向量中,特定的类型或导演必须在相同的位置表示。然后查询通过标题查找特定电影,并使用可选的 MATCH 检查电影是否与特征相关联。与 MATCH 不同,MATCH 会过滤掉不匹配的元素,而可选的 MATCH 如果不存在关系则返回 null。在 RETURN 中,第一个 CASE 子句如果不存在关系则返回 0,否则返回 1;第二个返回导演或类型的名称。图 4.17 显示了针对从 MovieLens 导入的数据库运行的查询结果。
如图 4.17 中的截图所示,实际向量很大,因为有很多可能的维度。尽管这种完整表示可以通过这里讨论的实现来管理,但第五章介绍了一种表示这样长向量的更好方法。

图 4.17 运行列表 4.11 在 MovieLens 数据集上的结果
添加索引
在 MovieLens 数据库上运行此查询可能需要很长时间。时间花费在过滤条件上,即 movie.title 以"Pulp Fiction"开头。添加索引可以大大提高性能。运行以下命令后,再尝试查询:
CREATE INDEX ON :Movie(title)
这不是快得多吗?
可以将这种向量方法推广到各种特征,包括具有数值的特征,例如我们电影场景中的平均评分。⁵在向量表示中,相关组件包含这些特征的精确值。在我们的例子中,三部电影的向量表示如下:
《低俗小说》向量 = [1, 0, 1, 1, 0, 1, 0, 4]
《惩罚者》向量 = [1, 1, 1, 1, 1, 0, 1, 3.5]
《杀死比尔:卷一》向量 = [1, 0, 1, 1, 0, 1, 0, 3.9]
最后一个元素代表平均评分。向量中的一些分量是布尔值,而其他分量是实数值或整数值,这并不重要 [Ullman and Rajaraman, 2011]。仍然可以计算向量之间的余弦距离,尽管如果我们这样做,我们应该考虑对非布尔分量进行适当的缩放,以便它们既不主导计算,也不无关紧要。为此,我们将值乘以一个缩放因子:
向量(低俗小说) = [1, 0, 1, 1, 0, 1, 0, 4α]
向量(惩罚者) = [1, 1, 1, 1, 1, 0, 13.5α]
向量(杀死比尔:第一卷) = [1, 0, 1, 1, 0, 1, 0, 3.9α]
在这种表示中,如果α设置为 1,平均评分将主导相似度的值;如果设置为 0.5,效果将减半。缩放因子可以针对每个数值特征不同,并取决于该特征在结果相似度中的权重。
在手头有了物品的正确向量表示后,我们需要将用户配置文件投影到相同的 VSM 中,这意味着我们需要创建具有与物品向量相同的分量和顺序的向量,这些向量描述了用户的偏好。如第 4.2.2 节所述,在基于内容的案例中,有关用户偏好或喜好的信息可以是用户-物品对或用户-特征对。这两对都可以隐式或显式地收集。因为向量空间以特征值作为维度,投影的第一步是将用户-物品矩阵迁移到用户-特征空间(除非它已经可用)。可以使用不同的技术进行此转换,包括通过计算用户之前喜欢列表中每个特征的出现次数进行聚合⁶。此选项对于布尔值效果良好;另一种选项是计算数值特征的平均值。在电影场景中,每个用户配置文件可以表示如表 4.3 所示。
表 4.3 与电影相同的向量空间表示的用户配置文件
| 动作 | 剧情 | 犯罪 | 惊悚 | 冒险 | 昆汀·塔伦蒂诺 | 约翰·亨谢利 | 总计 | |
|---|---|---|---|---|---|---|---|---|
| 用户 A | 3 | 1 | 4 | 5 | 1 | 3 | 1 | 9 |
| 用户 B | 0 | 10 | 1 | 2 | 3 | 0 | 1 | 15 |
| 用户 C | 1 | 0 | 3 | 1 | 0 | 1 | 0 | 5 |
每个单元格表示用户观看具有该特定特征的电影数量。例如,用户 A 观看了三部昆汀·塔伦蒂诺执导的电影,但用户 B 没有观看他执导的任何电影。表中还包含一个新列,表示每个用户观看的电影总数;此值将在创建用于归一化值的向量时很有用。
这些用户-特征对及其相关计数很容易从我们迄今为止用于表示用户-项目交互的图模型中获得。为了简化下一步,我建议通过在图中正确存储这些值来实际化这些值。在一个属性图数据库中,表示用户对特定项目特征的兴趣程度的权重可以通过用户和特征之间的关系属性来建模。修改之前用于推断用户和特征之间关系的列表 4.8,可以提取此信息,创建新的关系,并将这些权重添加到边中。新的查询如下所示。
列表 4.12 查询提取用户和特征之间的加权关系
MATCH (user:User)-[:WATCHED|RATED]->(m:Movie)-
➥ [:ACTS_IN|WROTE|DIRECTED|PRODUCED|HAS]-(feature)
WITH user, feature, count(feature) as occurrence
WHERE occurrence > 2
MERGE (user)-[r:INTERESTED_IN]->(feature)
SET r.weight = occurrence ❶
❶ SET 在 INTERESTED_IN 关系上添加或修改权重属性。
在这个版本中,发生情况被存储为 INTERESTED_IN 关系上的一个属性,而在列表 4.8 中,它仅被用作过滤器。图 4.18 显示了结果模型。

图 4.18 推断 INTERESTED_IN 关系后的图模型
仅凭表中的数字可能导致用户配置文件向量和项目向量之间相似性的错误计算。它们必须被归一化以更好地表示用户对特定特征的真正兴趣。例如,如果一个用户观看了 50 部电影,其中只有 5 部是剧情片,我们可能会得出结论,该用户对这个类型不如一个在总共 10 部电影中观看了 3 部剧情片的用户感兴趣,尽管第一个用户观看了更多类型的电影。
如果我们将表 4.3 中的每个值与用户观看的电影总数进行归一化,我们会看到第一个用户对剧情类型的兴趣为 0.1,而第二个为 0.6。表 4.4 显示了归一化的用户配置文件。
表 4.4 表 4.3 的归一化版本
| 动作 | 剧情 | 犯罪 | 惊悚 | 冒险 | 昆汀·塔伦蒂诺 | 约翰·亨谢利 | |
|---|---|---|---|---|---|---|---|
| 用户 A | 0.33 | 0.11 | 0.44 | 0.55 | 0.11 | 0.33 | 0.11 |
| 用户 B | 0 | 0.66 | 0.06 | 0.13 | 0.2 | 0 | 0.06 |
| 用户 C | 0.2 | 0 | 0.6 | 0.2 | 0 | 0.2 | 0 |
建模技巧
我不建议将归一化过程的结果作为图中的权重存储,因为这些结果受到用户观看电影总数的影响。存储这些值将需要我们每次用户观看一部新电影时都重新计算每个权重。如果我们只将计数作为权重存储,当用户观看一部新电影时,只需更新受影响的功能。例如,如果用户观看了一部冒险电影,那么只需更新该类型的计数。
在显式场景中,可以通过要求用户为一系列可能的项目特征分配评分来收集此权重信息。相关值可以存储在用户和特征之间的边上的权重属性上。
在这个过程结束时,我们以共同和可比的方式表示了项目和用户档案。我们可以通过计算用户档案向量表示与每部尚未观看的电影之间的相似度来完成每个用户的推荐任务,从高到低排序,并返回前 N 个,其中 N 可以是 1、10 或应用所需的任何数字。在这种情况下,推荐任务需要复杂的操作,这些操作不能通过查询完成,因为它们需要复杂的计算、循环、转换等。
列表 4.13 展示了如何以图 4.18 所示的方式存储数据时提供推荐。完整的代码可在代码存储库中的 ch04/recommendation/content_based_recommendation_second_approach.py 找到。
列表 4.13 使用第二种方法提供推荐的方法
def recommendTo(self, userId, k): ❶
user_VSM = self.get_user_vector(userId)
movies_VSM = self.get_movie_vectors(userId)
top_k = self.compute_top_k (user_VSM, movies_VSM, k);
return top_k
def compute_top_k(self, user, movies, k): ❷
dtype = [ ('movieId', 'U10'),('value', 'f4')]
knn_values = np.array([], dtype=dtype)
for other_movie in movies:
value = cosine_similarity([user], [movies[other_movie]]) ❸
if value > 0:
knn_values = np.concatenate((knn_values, np.array([(other_movie,
➥ value)], dtype=dtype)))
knn_values = np.sort(knn_values, kind='mergesort', order='value' )[::-1]
return np.array_split(knn_values, [k])[0]
def get_user_vector(self, user_id): ❹
query = """
MATCH p=(user:User)-[:WATCHED|RATED]->(movie)
WHERE user.userId = $userId
with count(p) as total
MATCH (feature:Feature)
WITH feature, total
ORDER BY id(feature) ❺
MATCH (user:User)
WHERE user.userId = {userId}
OPTIONAL MATCH (user)-[r:INTERESTED_IN]-(feature)
WITH CASE WHEN r IS null THEN 0 ELSE (r.weight*1.0f)/(total*1.0f) END as value
RETURN collect(value) as vector
"""
user_VSM = None
with self._driver.session() as session:
tx = session.begin_transaction()
vector = tx.run(query, {"userId": user_id})
user_VSM = vector.single()[0]
print(len(user_VSM))
return user_VSM;
def get_movie_vectors(self, user_id): ❻
list_of_moview_query = """ ❼
MATCH (movie:Movie)-[r:DIRECTED|HAS]-(feature)<-
➥ [i:INTERESTED_IN]-(user:User {userId: $userId})
WHERE NOT EXISTS((user)-[]->(movie)) AND EXISTS((user)-[]->
➥ (feature))
WITH movie, count(i) as featuresCount
WHERE featuresCount > 5
RETURN movie.movieId as movieId
"""
query = """ ❽
MATCH (feature:Feature)
WITH feature
ORDER BY id(feature)
MATCH (movie:Movie)
WHERE movie.movieId = {movieId}
OPTIONAL MATCH (movie)-[r:DIRECTED|HAS]-(feature)
WITH CASE WHEN r IS null THEN 0 ELSE 1 END as value
RETURN collect(value) as vector;
"""
movies_VSM = {}
with self._driver.session() as session:
tx = session.begin_transaction()
i = 0
for movie in tx.run(list_of_moview_query, {"userId": user_id}):
movie_id = movie["movieId"];
vector = tx.run(query, {"movieId": movie_id})
movies_VSM[movie_id] = vector.single()[0]
i += 1
if i % 100 == 0:
print(i, "lines processed")
print(i, "lines processed")
print(len(movies_VSM))
return movies_VSM
❶ 这个函数提供推荐。
❷ 这个函数计算用户档案向量和电影向量之间的相似度,并返回与用户档案最匹配的前 k 部电影。
❸ 我们使用 scikit-learn 提供的 cosine_similarity 函数。
❹ 这个函数创建用户档案;注意它如何通过单个查询提供并映射到向量。
❺ 排序至关重要,因为它允许我们拥有可比的向量。
❻ 这个函数提供了电影向量。
❼ 这个查询只为用户获取相关且尚未观看的电影,这加快了处理过程。
❽ 这个查询创建电影向量。
如果你为用户 598(与上一个场景中的用户相同)运行此代码,你会看到推荐的电影列表与上一个案例的结果没有太大不同,但就预测准确性而言,这些新结果应该更好。多亏了图表,可以轻松找到至少与用户档案有五个共同特征的电影。
还要注意,这个推荐过程需要一段时间才能产生结果。不要担心;这里的目的是以最简单的方式展示概念,书中稍后讨论了各种优化技术。如果你感兴趣,在代码存储库中你可以找到一个使用不同方法创建向量和计算相似度的优化版本的代码。
练习
考虑到 4.13 列表中的代码,
-
将代码重写为使用不同的相似度函数,例如皮尔逊相关系数(
libguides.library.kent.edu/SPSS/PearsonCorr),而不是余弦相似度。小贴士:搜索 Python 实现,并用余弦相似度函数替换。
-
查看代码存储库中的优化实现,并找出新向量是如何创建的。现在它有多快?第五章介绍了稀疏向量的概念。
我们将要考虑的基于内容的第三种推荐方法可以描述为“推荐与用户过去喜欢的项目相似的项目” [Jannach et al., 2010]。这种方法在一般情况下效果良好,并且当可以计算项目之间的相关相似度,但难以或不适当地以相同方式表示用户档案时,它是唯一的选择。
考虑我们的训练数据集,如图 4.9 和图 4.10 所示。用户偏好是通过将用户与项目连接而不是与项目的元信息连接来建模的。当每个项目的元信息不可用、有限或不相关时,这种方法可能是必要的,因此无法(或没有必要)提取关于用户对某些特征兴趣的数据。尽管如此,与每个项目相关的内容或内容描述以某种方式是可用的;否则,基于内容的方法将不适用。即使元信息可用,这种第三种方法在推荐准确性方面也大大优于前一种方法。这种被称为基于相似度的检索的技术,由于以下几个原因,是一种有价值的解决方案:
-
它在第三章中介绍。在这里,使用不同的项目表示来计算相似度。
-
相似度很容易存储在图中作为项目之间的关系。这个例子代表了一个完美的图建模用例,通过导航相似度关系可以提供快速推荐。
-
这是最常见且最强大的 CBRSs(内容基础推荐系统)方法之一。
-
它足够灵活和通用,可以在许多场景中使用,无论每个项目可用的数据/信息类型如何。
在这种场景中的整个推荐过程可以总结为图 4.19 中的高级示意图。

图 4.19 基于内容推荐器的第三种方法的推荐过程
值得注意的是,这与第五章中描述的协同过滤方法的最大区别在于,项目之间的相似度仅通过使用与项目相关的数据进行计算,无论是什么。用户-项目交互仅在推荐阶段使用。
根据图 4.19,在这个场景中需要三个关键元素:
-
用户档案——用户档案通过建模用户与项目之间的交互来表示,例如评分、购买或观看。在图中,这些交互表示为用户与项目之间的关系。
-
项目表示/描述——为了计算项目之间的相似度,有必要以可测量的方式表示每个项目。如何进行取决于用于测量相似度的函数选择。
-
相似度函数—我们需要一个函数,给定两个项目表示,计算它们之间的相似度。我们在第三章中描述了将余弦相似度指标应用于协同过滤的简化示例。在这里,更详细地描述了不同的技术,应用于基于内容的推荐。
与第二种方法一样,列出的前两个元素是严格相关的,因为每个相似度公式都需要特定的项目表示。相反,根据每个项目的可用数据,某些函数可以应用,而其他则不能。
一个典型的相似度指标,适用于多值特性,是 Dice 系数 [Dice, 1945]。它的工作原理如下。每个项目 I[i]由一组特征 features(I[i]) 描述——例如,一组关键词。Dice 系数衡量项目 I[i]和 I[j]之间的相似度如下

在这个公式中,关键词返回描述项目的关键词列表。在分子中,公式计算重叠/交集的关键词数量,并将结果乘以 2。在分母中,它计算每个项目中的关键词数量。这是一个简单的公式,其中关键词可以被任何东西替换——在我们的电影示例中,类型、演员等等(见图 4.20)。当计算相似度时,它们可以存储回图中,如图 4.21 所示。



图 4.21 将相似度存储回图中
没有必要存储每对节点之间的邻居关系(尽管需要计算所有这些关系)。通常,只存储少量。你可以定义一个最小相似度阈值,或者你可以定义一个 k 值,只保留最相似的 k 个项目。因此,这种方法中描述的方法被称为 k-最近邻 (k-*NN**) 方法,无论选择的相似度函数如何。
k-NN 方法在许多机器学习任务中使用,从推荐到分类。它们在数据类型方面具有灵活性,可以应用于文本数据以及结构化数据。在推荐中,k-NN 方法的优势在于相对简单易实现——尽管我们需要考虑计算相似度所需的时间(我们将在第 6.3 节和第 9.2 节中解决这个问题)——并且能够快速适应数据集的最新变化。
这些技术在整本书中都发挥着重要作用,不仅因为它们在机器学习领域的广泛应用,还因为它们很好地适应了图空间——作为一个在机器学习任务中有多种用途的常见模式,其中许多在本书中都有介绍。本部分的其余章节讨论了最近邻方法在推荐任务中的优势。在第九章中,类似的方法应用于欺诈检测。
在所有这些应用中,图提供了一个合适的数据模型来存储 k 个最相关的邻居。这样的图被称为 k-最近邻图(或网络),k-NN 技术在许多场景中用作网络形成方法。
从形式上看,这个图是在 v[i]和 v[j]之间创建一条边,如果 v[j]是 v[i]的 k 个最相似元素之一。k-NN 网络通常是一个有向网络,因为 v[j]可以是 v[i]的 k 个最近邻之一,但反之则不成立。(节点 v[j]可能有一组不同的邻居。)在预测或分析阶段会访问 k-NN 网络。
再看看图 4.21。当为每个相关节点确定了 k 个最相似的节点(例如推荐引擎中的项目或反欺诈系统中的交易)时,可以使用适当的关系类型将它们连接起来。相对相似度值存储在关系属性上。在推荐阶段使用生成的图。
Dice 系数很简单,但由于它使用少量信息来计算相似度,因此生成的推荐质量较差。计算项目之间相似度的一个更强大的方法是基于余弦相似度。项目可以精确地表示为第二种方法中的那样。区别在于,不是在用户配置文件和项目之间计算余弦相似度,而是余弦函数计算项目之间的相似度。这种相似度是对每一对项目进行计算的;然后,每个项目的 top k 匹配作为相似关系存储在图中。考虑表 4.5 中列出的相似度。
表 4.5 电影之间的余弦相似度
| 低俗小说 | 惩罚者 | 杀戮比尔:第一卷 | |
|---|---|---|---|
| 低俗小说 | 1 | 0.612 | 1 |
| 惩罚者 | 0.612 | 1 | 0.612 |
| 杀戮比尔:第一卷 | 1 | 0.612 | 1 |
表的内容可以像图 4.21 所示的那样存储在图中。需要注意的是,与第一种和第二种方法不同,其中推荐过程使用原始数据,这里的推荐过程需要一个中间步骤:这个 k-NN 计算和存储。在这种情况下,描述性模型和预测模型不匹配。
列表 4.14 显示了一个用于计算 k-NN 并将这些数据存储回图中的 Python 脚本。它适用于我们从 MovieLens 数据集导入的图数据库。
列表 4.14 创建 k-NN 网络的代码
def compute_and_store_similarity(self): ❶
movies_VSM = self.get_movie_vectors()
for movie in movies_VSM:
knn = self.compute_knn(movie, movies_VSM.copy(), 10);
self.store_knn(movie, knn)
def get_movie_vectors(self): ❷
list_of_moview_query = """
MATCH (movie:Movie)
RETURN movie.movieId as movieId
"""
query = """
MATCH (feature:Feature)
WITH feature
ORDER BY id(feature)
MATCH (movie:Movie)
WHERE movie.movieId = $movieId
OPTIONAL MATCH (movie)-[r:DIRECTED|HAS]-(feature)
WITH CASE WHEN r IS null THEN 0 ELSE 1 END as value
RETURN collect(value) as vector;
"""
movies_VSM = {}
with self._driver.session() as session:
tx = session.begin_transaction()
i = 0
for movie in tx.run(list_of_moview_query):
movie_id = movie["movieId"];
vector = tx.run(query, {"movieId": movie_id})
movies_VSM[movie_id] = vector.single()[0]
i += 1
if i % 100 == 0:
print(i, "lines processed")
print(i, "lines processed")
print(len(movies_VSM))
return movies_VSM
def compute_knn(self, movie, movies, k): ❸
dtype = [ ('movieId', 'U10'),('value', 'f4')]
knn_values = np.array([], dtype=dtype)
for other_movie in movies:
if other_movie != movie:
value = cosine_similarity([movies[movie]], [movies[other_movie]])❹
if value > 0:
knn_values = np.concatenate((knn_values,
➥ np.array([(other_movie, value)], dtype=dtype)))
knn_values = np.sort(knn_values, kind='mergesort', order='value' )[::-1]
return np.array_split(knn_values, k)[0]
def store_knn(self, movie, knn): ❺
with self._driver.session() as session:
tx = session.begin_transaction()
test = {a : b.item() for a,b in knn}
clean_query = """MATCH (movie:Movie)-[s:SIMILAR_TO]-()
WHERE movie.movieId = $movieId
DELETE s
"""
query = """
MATCH (movie:Movie)
WHERE movie.movieId = $movieId
UNWIND keys($knn) as otherMovieId
MATCH (other:Movie)
WHERE other.movieId = otherMovieId
MERGE (movie)-[:SIMILAR_TO {weight: $knn[otherMovieId]}]-(other)
"""
tx.run(clean_query, {"movieId": movie}) ❻
tx.run(query, {"movieId": movie, "knn": test})
tx.commit()
❶ 执行所有电影任务的整体函数
❷ 此函数将 VSM 中的每部电影进行投影。
❸ 此函数计算每部电影的 k-NN。
❹ 在这里,它使用了 scikit 中可用的 cosine_similarity。
❺ 此函数将 k-NN 存储在图数据库中。
❻ 在存储新的相似性之前删除旧的
此代码可能需要一段时间才能完成。在这里,我展示了基本思想;在第 6.3 节和第 9.2 节中,我讨论了真实项目中的一些优化技术。此外,我想提到 Neo4j 提供了一个名为 Graph Data Science Library⁷(GDS)的数据科学插件,其中包含许多相似性算法。如果您使用 Neo4j,我建议使用这个库。前面的代码更通用,可以在任何情况下使用。
练习
当通过列表 4.14 中的代码计算了 k-NN 后,编写一个查询来完成以下操作:
-
获取一部电影(通过 movieId),并获取最相似的 10 个物品列表。
-
搜索最相似的 10 对物品。
在此第三种推荐流程的下一步,如图 4.19 所示,包括生成推荐,我们通过利用 k-NN 网络和用户对物品的隐式/显式偏好来实现这一点。目标是预测那些尚未看到/购买/点击的、可能对用户感兴趣的物品。
这个任务可以通过不同的方式完成。在最简单的方法[Allan, 1998]中,对于用户>u 尚未看到的物品 d 的预测基于一个投票机制,考虑与物品>d 最相似的 k 个物品(在我们的场景中是电影)。如果用户 u 观看了或评价了这些最相似的 k=5 个物品中的 4 个,例如,系统可能会猜测用户也会喜欢 d 的机会相对较高。
另一种更精确的方法是受协同过滤的启发,特别是受基于物品的协同过滤推荐[Sarwar et al., 2001, and Deshpande and Karypis, 2004]。这种方法涉及通过考虑目标物品与其他用户之前互动过的物品的所有相似性之和来预测用户对特定物品的兴趣:

这里,Items(u) 返回用户已与之互动的所有物品(喜欢、观看、购买、点击)。返回的值可用于对所有尚未看到的物品进行排名,并将前 k 个推荐给用户。以下列表实现了最后一步:为这种第三种场景提供推荐。
列表 4.15 获取用户物品排名列表的代码
def recommendTo(self, user_id, k): ❶
dtype = [('movieId', 'U10'), ('value', 'f4')]
top_movies = np.array([], dtype=dtype)
query = """ ❷
MATCH (user:User)
WHERE user.userId = $userId
WITH user
MATCH (targetMovie:Movie)
WHERE NOT EXISTS((user)-[]->(targetMovie))
WITH targetMovie, user
MATCH (user:User)-[]->(movie:Movie)-[r:SIMILAR_TO]->(targetMovie)
RETURN targetMovie.movieId as movieId, sum(r.weight)/count(r) as
➥ relevance
order by relevance desc
LIMIT %s
"""
with self._driver.session() as session:
tx = session.begin_transaction()
for result in tx.run(query % (k), {"userId": user_id}):
top_movies = np.concatenate((top_movies, np.array([(result["movieId"], result["relevance"])], dtype=dtype)))
return top_movies
❶ 此函数向用户提供推荐。
❷ 此查询返回推荐;它需要之前构建的模型。
当你运行这段代码时,你会注意到它运行得很快。当模型创建后,提供推荐只需几毫秒。
还可以使用其他方法,但它们超出了本章和本书的范围。这里的主要目的是展示,当你为物品、用户及其交互定义了适当的模型后,你可以使用多种方法来提供推荐,而无需更改定义的基本图模型。
练习
重新编写计算物品之间相似度的方法,使用与余弦相似度不同的函数,例如 Jaccard 指数(mng.bz/qePA)、Dice 系数或欧几里得距离(mng.bz/7jmm)。
4.4 图方法的优势
在本章中,我们讨论了如何使用图和图模型创建 CBRS,用于存储在推荐过程的不同步骤中作为输入和输出的不同类型的信息。特别是,基于图的内容推荐方法的主要方面和优势是
-
有意义的信息必须作为唯一的节点实体存储在图中,以便这些实体可以在物品和用户之间共享。
-
当元信息可用且有意义时,将用户-物品数据转换为用户-特征数据是一项简单的任务;你需要一个查询来计算和实现它。
-
从同一个图模型中可以提取物品和用户配置文件的好几个向量表示。轻松提取多种类型的向量可以改善特征选择,因为它减少了尝试不同方法所需的努力。
-
使用不同的函数计算不同的相似度值,并将它们组合使用是可能的。
-
代码展示了在不同模型之间切换或甚至将它们结合起来的简便性,前提是它们由适当的图模型描述。
最大的优势是信息图表示的灵活性,它使得相同的数据模型可以通过小的调整服务于许多用例和场景。此外,所有场景都可以存在于同一个数据库中,这使数据科学家和数据工程师免于处理相同信息的多个表示。以下章节中描述的所有推荐方法都共享这些优势。
摘要
本章向您介绍了基于图的建模技术。在本主题的第一章中,我们专注于推荐引擎,探讨了如何建模用于训练的数据源,如何存储生成的模型,以及如何访问它以进行预测。
在本章中,你学习了
-
如何设计用于用户-物品以及用户-特征数据集的图模型
-
如何将数据从原始格式导入到您设计的图模型中
-
如何将用户配置文件和物品数据以及元数据投影到向量空间模型中
-
如何通过余弦相似度和其他函数计算用户和项目配置文件之间的相似度以及项目对之间的相似度
-
如何在图模型中存储项目相似度
-
如何查询生成的模型以执行预测和推荐
-
如何从头到尾设计和实现一个由图驱动的推荐引擎,使用不同复杂度的方法
-
k-NN 和 k-NN 网络在机器学习(一般)和基于图机器学习中的作用
参考文献
[Allan, 1998] Allan, James. “主题检测与跟踪试点研究最终报告。” DARPA 广播新闻转录和理解研讨会论文集 (1998): 194-218.
[Deshpande and Karypis, 2004] Deshpande, Mukund, 和 George Karypis. “基于项目的 Top-N推荐算法。” ACM 信息系统交易 22:1 (2004): 143-177. DOI: mng.bz/jB6x.
[Dice, 1945] Dice, Lee Raymond. “物种之间生态关联量的度量。” 生态学 26:3 (1945): 297-302. DOI: mng.bz/9N8l. JSTOR 1932409.
[Jannach et al., 2010] Jannach, Dietmar, Markus Zanker, Alexander Felfernig, 和 Gerhard Friedrich. 推荐系统:导论. 英国剑桥:剑桥大学出版社,2010 年. DOI: mng.bz/K4dK.
[Sarwar et al., 2001] Sarwar, Badrul, George Karypis, Joseph Konstan, 和 John Riedl. “基于项目的协同过滤推荐算法。” 第 10 届国际万维网会议论文集 (2001): 285-295. mng.bz/Wrm0.
[Ullman and Rajaraman, 2011], Ullman, Jeffrey David, 和 Anand Rajaraman. 大规模数据集挖掘. 纽约:剑桥大学出版社,2011 年。
^(1)IMDb (https://www.imdb.com) 是一个包含与电影、电视节目、家庭录像、视频游戏和互联网流相关的信息的在线数据库,包括演员和制作团队、剧情简介、趣味知识和粉丝评论和评分的详细信息。
^(2)第二章中引入的属性图将数据组织为节点、关系和属性(存储在节点或关系上的数据)。
^(3)请使用 MATCH (n) DETACH DELETE n.整理您的数据库。
^(4)此查询只能在用户评分导入完成后执行,如列表 4.9 所示。
^(5)平均评分不是一个有价值的特征,但在我们的例子中它将起到作用。
^(6)点赞在这里意味着用户和项目之间的任何互动:观看、评分等。
^(7)neo4j.com/product/graph-data-science-library/.
5 协同过滤
本章涵盖
-
为协同过滤方法设计合适的图模型
-
将现有的(非图)数据集导入为图模型设计
-
实现工作协同过滤推荐引擎
第四章中描述的基于内容的(也称为内容过滤或认知)推荐方法为用户和项目创建档案以描述它们。这些档案允许系统将用户与相关项目匹配。基于内容方法的一般原则是识别用户对获得正面反馈(如好评、购买、点击)的项目共有的特征,然后向该用户推荐具有这些特征的新项目。基于内容策略需要收集可能不易获得、难以收集或与内容直接相关的信息。
内容过滤的替代方案仅依赖于过去用户的行为,例如之前的交易或项目评分,或现有用户社区的意见,以预测用户最可能喜欢或感兴趣的项目,而无需根据项目特征为项目和用户创建显式的档案。这种方法被称为协同过滤,是由 Tapestry 的开发者(Goldberg 等人,1992 年)提出的,是第一个推荐系统。协同过滤分析用户之间的关系和项目之间的相互依赖性,以预测新的用户-项目关联。图 5.1 表示了考虑输入和输出的协同过滤推荐者的心智模型。

图 5.1 协同过滤心智模型
协同过滤的一个主要吸引力是它不受领域限制,并且不需要任何关于项目的详细信息。它可以应用于广泛的用例和场景,并且可以使用内容过滤难以描述的数据方面来解决问题。
尽管基于协同过滤的方法通常比基于内容的技术更准确,但由于其无法为新项目或用户或当有限交互数据可用时提供合理的(从准确性角度)推荐,因此协同过滤受到所谓的冷启动问题的影响。尽管如此,确实存在一些机制,通过使用不同的算法(如第 5.5 节中讨论的图方法)或其他知识来源(如社交网络)来减轻冷启动问题的影响。
协同过滤技术通常分为两种主要方法或领域:
- 基于记忆的—基于记忆的方法假设如果用户喜欢电影《拯救大兵瑞恩》,他们可能喜欢类似的电影,例如战争电影、斯皮尔伯格的电影和汤姆·汉克斯的电影 [Koren 等人,2009]。为了预测特定用户对《拯救大兵瑞恩》的评分,我们会寻找这位用户评价过的与这部电影最接近的邻居电影。或者,算法可以根据他们观看的电影集合寻找类似用户,并推荐当前用户尚未观看的内容。在这些方法中,存储的 User-Item 数据集直接用于预测物品的评分。
这些方法也被称为 邻域方法,因为它们集中在计算物品或用户之间的关系。物品导向的方法基于同一用户对邻近物品的评分来预测用户对物品的偏好。一个物品的邻居是那些在相同用户评分下倾向于获得相似评分的其他物品。相比之下,用户导向的方法识别出有共同观点的用户,他们可以互补彼此的评分。换句话说,在这种情况下,推荐过程包括寻找与当前用户相似的其他用户(对物品有相似的评分或购买了相同的东西),并推荐这些用户互动过的物品(评分、购买或点击)。
- 基于模型的方法—这些方法为用户和物品创建模型,通过一系列因素或特征以及这些特征对每个物品和每个用户的权重来描述他们的行为。在电影示例中,发现的因素可能衡量明显的维度,如类型(喜剧、剧情、动作)或对儿童的态度;不太明确的维度,如人物发展的深度或古怪性;或不可解释的维度。对于用户来说,每个因素都表达了用户对在相应因素上得分高的电影的喜爱程度。在这些方法中,原始数据(User-Item 数据集)首先在离线状态下进行处理,使用评分或之前的购买信息来创建这个预测模型。在运行时,在推荐过程中,只需要预先计算或学习到的模型来做出预测。潜在因素模型是这类方法中最常见的途径。它们试图通过在从评分模式中推断出的 20 到 100 个因素上对物品和用户进行特征化来解释评分。从某种意义上说,这些因素构成了计算机化的替代品,替代了在基于内容的推荐系统中遇到的人类创建的特征。
注意:尽管最近的调查表明,在预测评分的任务中,基于模型的方法在性能上优于基于邻域的方法 [Koren,2008,以及 Takács 等人,2007],但一个理解正在出现,即仅仅好的预测精度并不能保证用户获得有效和满意的体验 [Herlocker 等人,2004]。
如第二部分引言所述,公司实施推荐引擎的一些主要原因是为了提高用户满意度和忠诚度,以及销售更多样化的商品。此外,向用户推荐他们最喜欢的导演执导的电影,如果用户之前不知道这部电影,则构成一种新颖的推荐,但用户可能自己会发现这部电影[Ning 等人,2015 年]。
此例展示了另一个被认定为在用户对推荐系统欣赏中扮演重要角色的相关因素:意外发现[Herlocker 等人,2004 年,以及 Sarwar 等人,2001 年]。意外发现通过帮助用户找到他们可能否则不会发现的有趣项目,扩展了新颖性的概念。这一方面的推荐增加了用户满意度,并帮助公司销售更多样化的商品。
这个例子说明了基于模型的方法在表征具有潜在因素的用户偏好方面的优势。在电影推荐系统中,这些方法可能确定某个用户是既搞笑又浪漫电影的粉丝,而无需定义“搞笑”和“浪漫”这些概念。因此,该系统将能够向用户推荐他们可能未曾知晓的浪漫喜剧。在预测准确度方面,这种方法可以提供最佳结果,但系统可能难以推荐一个与这种高级类型(如恐怖电影的搞笑模仿)不太吻合的电影。
另一方面,基于邻域的方法捕捉数据中的局部关联。因此,基于这种类型的方法的电影推荐系统可能会推荐一部与用户通常口味截然不同的电影,或者如果他们的一个最近邻(用户)给出了强烈的评分,可能会推荐一部不太知名的电影(如保留剧目电影)。这种推荐可能不会像浪漫喜剧那样保证成功,但它可能帮助用户发现一个新类型,或者一个新喜爱的演员或导演。基于邻域的方法具有许多优点,如下所述:
-
简单性——基于邻域的方法直观且相对容易实现。在其最简单形式中,只需要调整一个参数(用于预测中使用的邻居数量)。另一方面,基于模型的方法通常使用矩阵分解技术,这些技术通过优化算法实现以找到近似最优解。这些优化技术,如随机梯度下降¹(SGD)和交替最小二乘²(ALS),具有许多必须仔细调整的超参数,以避免陷入局部最小值。
-
可解释性——这些方法为计算出的预测提供了简洁直观的解释。在基于项目的推荐中,可以展示给用户邻近项目的列表以及用户对这些项目的评分,作为推荐的解释。这种解释可以帮助用户更好地理解推荐过程以及相关性是如何计算的,从而增加用户对推荐系统(及其提供平台)的信任。它还可以作为用户在选择在推荐中给予更大重要性的邻居时的交互式系统的基础 [Bell et al., 2007]。
-
效率——基于邻域的系统的一个显著优点是它们的效率。与大多数基于模型的系统相比,它们需要的昂贵训练阶段更少,这些阶段在大规模商业应用中需要频繁进行。这些系统可能需要在离线步骤中预先计算最近邻,但通常比基于模型的方法的模型训练便宜得多。此外,当有新信息可用时,可以识别出需要重新计算的小部分模型。这些特性有助于提供几乎瞬时的推荐。此外,存储这些最近邻需要很少的内存,使得这种方法可以扩展到拥有数百万用户和项目的应用。
-
稳定性——基于这种方法的推荐系统另一个有用的特性是,它们受到用户、项目和评分增加的影响很小,这在大型商业应用中通常观察到。当计算了项目相似度后,基于项目的系统可以轻松地向新用户推荐,而无需重新训练。此外,当为新项目输入少量评分时,只需计算此项目与系统中已有的项目之间的相似度。
-
基于图的——另一个与本书主题相关的重要优势是,原始数据集和最近邻模型可以轻松地映射到图模型中。由此产生的图在模型更新或预测期间的数据局部导航、可解释性和访问效率方面提供了巨大优势。它还有助于解决第 5.5 节中描述的冷启动问题。
由于这些优势,本章主要关注这类协同过滤推荐系统。尽管基于邻居的方法因其优势而受到欢迎,但它们也普遍存在覆盖范围有限的问题,导致某些商品从未被推荐。此外,该类传统方法也普遍对评分稀疏性和系统只有少量评分或对新用户和商品没有评分时的冷启动问题敏感。在第 5.5 节中,我将讨论缓解或解决冷启动问题的技术。最后,没有单一的推荐引擎方法可以适用于所有情况,这就是为什么在可投入生产的推荐引擎系统中通常使用混合方法。这些方法在第七章中进行了描述。
5.1 协同过滤推荐
在本节中,你将学习如何设计图模型及其算法,以实现一个用于电子商务网站的推荐系统,该系统采用协同过滤方法,温和地向用户推荐可能对他们感兴趣的商品。
该网站可能销售许多类型的商品,例如书籍、电脑、手表和服装。关于每个商品的详细信息未经精心整理;有些商品只有标题、一张或多张图片以及一小段无用的描述。供应商没有提供大量信息,由于商品数量和供应商数量众多,因此不可能有一个内部团队大规模地处理这项任务。³ 在网站上,用户只有在注册并登录后才能购买商品。多亏了 cookies,用户一旦到达网站就可以自动登录。关于每个用户的信息跟踪很少——只有支付和运输所必需的信息。
在这种情况下,类似于第四章中描述的内容方法不适用。通常,没有可用于创建用户或商品档案的有用数据。尽管如此,用户的活跃度是会被跟踪的。大多数时候用户都是登录状态,可以收集和存储用户与商品之间的大量互动(购买、点击、搜索和评分)。协同过滤方法使用这些数据向用户提供推荐。利用图模型的优势,整个推荐过程可以用图 5.2 中的高级架构进行总结。

图 5.2:由图驱动的协同过滤推荐系统
此图展示了协同过滤推荐引擎的主要元素和任务,该引擎使用图作为用户-项目数据集和最近邻网络的表示。假设用户约翰从电子商务网站购买了两种书籍:关于机器学习和图的科技书籍。作为第一步,用户-项目矩阵被转换为一个二分图。在这种情况下,代表约翰的节点将与代表购买的两本书的两个节点相连。生成的图被用作输入,通过计算用户、项目或两者的相似性来创建最近邻网络,具体取决于所使用的推荐算法。因此,约翰与对图和机器学习感兴趣且购买了相同书籍集的用户相似。这两本书也与被同一组用户购买的其它书籍相似,可能是在同一主题下。然后,将每个元素(用户或项目)的前 k 个最近邻存储回原始图,该图通过这些新的用户、项目或两者之间的关系得到丰富。相似度值作为关系的属性存储,并为其分配权重。这个权重值越高,通过这种关系用户之间的相似度就越高。此时为用户提供推荐是一个使用他们之前交互和最近邻网络的问题。这个任务可以通过简单的图匹配查询来完成。然后,将推荐列表提供给用户。
进一步的交互可以被循环回系统中以更新模型。这个任务可以利用图模型提供的局部性;新的交互只会影响最近邻网络的一小部分。如果一个用户观看了一部新电影,将很容易找到受影响的电影(所有至少与受影响的用户共享一个用户的电影),并计算它们的新的相似度。这种方法还允许推荐引擎更容易地适应口味的变化。基于关系遍历的图导航也有助于这些更新以及推荐阶段。
考虑到高级架构,它也充当一个心理模型,以下章节将详细描述如何创建和导航一个二分图,如何计算最近邻网络,以及如何提供推荐。在这个场景中,使用一个具体的电子商务数据集作为示例。
5.2 创建用户-项目数据集的二分图
在第四章讨论的内容导向方法中,物品和用户都有大量可用信息,这些信息对于创建个人资料非常有用。我们使用图模型来表示这些个人资料,将每个物品与其特征连接,并将每个用户与其感兴趣的特征连接。甚至最近的邻域网络也是仅用这些信息构建的。另一方面,协同过滤方法依赖于用户和物品之间不同类型交互的数据。这类信息通常被称为用户-物品数据集。第三章中描述了一个此类数据集的例子;在这里,讨论得到了扩展和细化。图 5.3 突出了在推荐过程中从用户-物品数据集中创建二分图的过程。

图 5.3 推荐过程中的二分图创建
表 5.1 展示了示例数据集。物品是不同用户购买的书。
表 5.1 电子商务场景的示例用户-物品数据集
| Fluent Python | Machine Learning: A Probabilistic Perspective | Graph Analysis and Visualization | Bayesian Reasoning | Fraud Analytics | Deep Learning | |
|---|---|---|---|---|---|---|
| 用户 A | 1 | 1 | 1 | 1 | 1 | 1 |
| 用户 B | 0 | 1 | 0 | 0 | 0 | 1 |
| 用户 C | 1 | 0 | 0 | 0 | 0 | 0 |
| 用户 D | 0 | 1 | 0 | 1 | 0 | 1 |
此表只包含关于一种类型交互的数据:购买。在不同的场景中,包括电子商务网站,有多种类型的交互可用(查看、点击、评分等),并且可以在推荐过程中使用。多模态推荐系统[da Costa and Manzato, 2014]结合多种交互类型来提供推荐。其中最好的方法之一是为每种交互类型创建一个独立的推荐系统,然后将这些交互在混合推荐系统中结合。因为这里的重点是协同过滤的数据建模和算法,我们将关注单一类型,但扩展到更多类型的讨论将在第 7.2 节中展开。
用户-物品数据集(在现实生活中将比这里展示的样本大得多)代表了协同过滤中推荐过程的输入。这些初始数据很容易获得。以下是一些例子:
-
在线商家会记录哪些客户购买了哪些产品,有时还会记录他们是否喜欢这些产品。
-
超市连锁店通常通过使用奖励卡来记录其常客的购买记录。
人们对于某些零售商销售的产品等事物的偏好可以用推荐网络中的图来表示[Newman, 2010],并在推荐系统中使用。推荐网络的基本和最常见表示是二分图或双图。我们在第三章中讨论了这些图。图 5.4 展示了一个简单的、通用的二分图,其中节点是类型 U 或 V。

图 5.4 一个通用的二分图
让我们将这个简单的概念应用到我们的场景中,以及使用协同过滤方法的推荐系统中。在推荐网络中,一种顶点类型代表产品(或一般的项目),另一种类型代表用户。边连接用户与他们互动的项目(例如购买或喜欢)。也可以在边上表示强度或权重,以指示一个人购买某个项目的频率或他们有多喜欢它 [Newman, 2010]。
使用这个模型,可以将二分图中的用户-项目数据集转换。从我们表 5.1 中的简单电子商务网站数据集,可以创建图 5.5 中的图。

图 5.5 表示表 5.1 的二分图
尽管二分图可以表示整个推荐网络,但通常处理仅一种类型顶点之间的直接连接既方便又有用。从一个二分图中,可以推断出相同类型节点之间的连接,创建一个单模式投影。对于每个二分图可以生成两个投影。第一个投影连接 U 节点(用户),第二个连接 V 节点(书籍)。图 5.6 显示了从图 5.5 中的二分图计算出的两个投影。

图 5.6 图 5.5 中的图的两个可能的投影
二分图的投影显示了做出相同购买的用户之间的关系,即使只有一次,以及被相同用户购买的书籍之间的关系,即使只有一次。这种单模式投影通常很有用且被广泛采用,但它的构建隐藏了原始二分图中的大量信息,因此在表示方面在某种程度上较弱。在项目和用户的情况下,它没有显示有多少用户购买了这两件商品。为了解决这个问题,可以在关系上添加一个属性,其值捕获这些信息,使这种投影加权。这种投影是一个共现网络(在第三章中描述),其来源是二分图。在用户-项目矩阵中,如果两个项目至少在一个用户的偏好中同时出现,它们在 V 模式投影中连接。
双边图的加权和无权投影以一种方式表示数据,这使得我们比使用原始格式更容易进行某些分析。因此,投影在推荐系统中被广泛使用 [Grujic´, 2008]。通常,在这些网络上执行图聚类分析(在第三部分讨论),以揭示通常一起购买的项目组,或者在另一个投影中,根据对某些特定项目集合的偏好来识别客户细分。投影也是强大的可视化技术,可以揭示在原始格式中难以发现或识别的模式。
回顾一下,用户-项目数据集的图模型,特别是双边图表示,的优势是
-
它以紧凑直观的方式表示数据。用户-项目数据集在本质上稀疏;通常,它有大量的用户和大量的项目,而用户只与可用项目的一小部分进行交互。表示这种交互的矩阵将包含大量的零和很少的有用值。图只包含表示有意义信息的关联。
-
从双边图派生出的投影信息丰富,允许进行不同类型的分析,包括图形分析(在可视化后)和通过算法(例如图聚类)。如客户细分和项目聚类等分析可以为本节讨论的经典推荐算法提供有价值的支持。
-
可以通过建模具有相同顶点集但使用不同边的多个双边图来扩展表示。这种扩展有助于我们表示作为多模态推荐引擎输入的数据。这些引擎使用多种类型的交互来提供更好的推荐。
-
如第 5.3 节所述,当计算最近邻网络时,它可以存储,共享双边图的用户和项目节点。这种方法通过提供一个包含用户偏好和最近邻网络的单个数据结构来简化推荐阶段。
现在你已经了解了描述用户-项目数据集的图模型,让我们来实际操作,创建一个真实的数据库。以下列表,类似于用于基于内容场景的列表,从 Retail Rocket⁴数据集(retailrocket.net)导入数据,将用户-项目矩阵转换为双边图。
列表 5.1 导入用户-项目数据集的代码
def import_user_item(self, file):
with open(file, 'r+') as in_file:
reader = csv.reader(in_file, delimiter=',')
next(reader, None)
with self._driver.session() as session:
self.executeNoException(session, "CREATE CONSTRAINT ON (u:User)
➥ ASSERT u.userId IS UNIQUE") ❶
self.executeNoException(session, "CREATE CONSTRAINT ON (u:Item)
➥ ASSERT u.itemId IS UNIQUE") ❶
tx = session.begin_transaction()
i = 0
j = 0
query = """ ❷
MERGE (item:Item {itemId: $itemId})
MERGE (user:User {userId: $userId})
CREATE (user)-[:PURCHASES{ timestamp: $timestamp}]->(item)
"""
for row in reader:
try:
if row:
timestamp = strip(row[0])
user_id = strip(row[1])
event_type = strip(row[2])
item_id = strip(row[3])
if event_type == "transaction": ❸
tx.run(query, {"itemId":item_id, "userId":
➥ user_id, "timestamp": timestamp})
i += 1
j += 1
if i == 1000:
tx.commit()
print(j, "lines processed")
i = 0
tx = session.begin_transaction()
except Exception as e:
print(e, row, reader.line_num)
tx.commit()
print(j, "lines processed")
❶ 创建约束以防止重复。每个用户和项目必须是唯一的
❷ 查询使用 MERGE 在用户或项目不存在时创建它们。存储购买关系的 CREATE 存储每个购买的关联。
❸ 数据集包含多种事件类型(查看、添加到购物车等)。我们在这里只考虑完成的交易或实际购买。
导入完成后,这可能需要几秒钟,可以运行以下查询并可视化图形的一部分:
MATCH p=()-[r:PURCHASES]->() RETURN p LIMIT 25
从二分模型创建的图代表下一阶段的入口点,如图 5.2 中的过程心理模型所述。第 5.3 节描述了如何计算和存储最近邻网络。
练习
使用新创建的数据库,运行查询以执行以下操作:
-
寻找最佳卖家(购买次数最多的项目)。
-
寻找最佳买家(购买次数最多的用户)。
-
寻找重复购买(同一用户购买多次的项目)。
5.3 计算最近邻网络
推荐过程中的下一个任务是计算元素之间的相似性——用户或项目或两者——并构建最近邻网络。图 5.7 突出了在推荐过程中构建最近邻网络作为二分图的丰富化。

图 5.7 在推荐过程中计算最近邻网络
如前所述,基于内存的协同过滤推荐有两种可能的方法:
-
基于项目——相似性是基于与项目交互的用户计算的。
-
基于用户——相似性是基于用户与之交互的项目列表计算的。
重要的一点是,与基于内容的案例不同,在邻域方法中,用于计算相似性的信息仅与用户-项目数据集中可用的交互有关。每个项目和每个用户只能通过 ID(或一个没有任何相关属性与标识符不同的节点)来识别。这种方法的优势在于,即使没有关于项目和用户的详细信息,也可以创建模型。尽管如此,相似性计算的程序与基于内容的相同:
-
识别/选择一个允许您计算图中同质元素之间距离的相似性函数(例如,用户或项目)。
-
以适合所选相似性函数的方式表示每个元素。
-
计算相似性,并将它们存储在图中。
图模型具有极大的灵活性,使我们能够轻松地提取用户或项目在各种函数选择下的各种表示。为了简单起见,并且因为它最适合我们的示例场景,我们再次使用余弦相似度。⁵ 不同之处在于如何在向量空间模型(VSM)中提取用于计算的向量。考虑一个简单的二分图,它表示一个更大的用户-项目数据集的简化版本,如图 5.8 所示。

图 5.8 表示缩小的用户-项目数据集的二分图
根据我们需要计算的相似度(用户之间的或项目之间的),可以从这个图中提取出两种向量表示。在这种情况下,向量创建过程与基于内容的近似方法基本相同。在那个例子中,为每个项目创建一个向量,考虑可能描述它的特征集。在这个例子中,对于每个项目,我们考虑对它感兴趣的用户集,同时考虑用户过去喜欢的项目。以下表格显示了如何完成这项任务。根据我们需要计算的相似度(用户之间的或项目之间的),可以从这个图中提取出两种向量表示。在这种情况下,向量创建过程与基于内容的近似方法基本相同。在这个例子中,对于每个项目,我们考虑对它感兴趣的用户集,同时考虑用户过去喜欢的项目。表 5.2 和 5.3 显示了如何完成这项任务。
表 5.2 图 5.8 中的用户向量表
| 流畅的 Python | 概率视角下的机器学习 | 图分析及可视化 | 贝叶斯推理 | 欺诈分析 | 深度学习 | |
|---|---|---|---|---|---|---|
| 用户 A | 1 | 1 | 1 | 1 | 1 | 1 |
| 用户 B | 0 | 1 | 0 | 0 | 0 | 1 |
| 用户 C | 1 | 0 | 0 | 0 | 0 | 0 |
| 用户 D | 0 | 1 | 0 | 1 | 0 | 1 |
在表 5.2 中,每一行代表一个用户,每一列代表一个项目(一本书)。在向量创建过程中,列的顺序必须始终相同;否则,两个向量无法比较。在这个例子中,向量是二进制或布尔型的:我们不是考虑用户和项目之间关系的值,只是它们存在的事实。这样的值可以模拟用户分配给书籍的评分、用户点击产品的次数等等,并在图模型中作为关系的属性表示。迁移到非二进制表示需要将 1 替换为用户和项目之间关系的实际权重值。
在 4.1 节中,我们看到了在相同的向量构造中混合二进制值与实数(整数、浮点数和双精度浮点数)是可能的。当我们需要为每个项目创建一个表示更多关系类型的向量时,这种方法很有用,例如在多模态推荐中。为了简化,这里没有考虑这种情况。
从表 5.2 中,我们可以提取出 VSM 中用户的以下压缩表示:
向量(用户 A) = [1, 1, 1, 1, 1, 1]
向量(用户 B) = [0, 1, 0, 0, 0, 1]
向量(用户 C) = [1, 0, 0, 0, 0, 0]
向量(用户 D) = [0, 1, 0, 1, 0, 1]
表 5.3 显示了项目向量。
表 5.3 项目向量表
| 用户 A | 用户 B | 用户 C | 用户 D | |
|---|---|---|---|---|
| 流畅的 Python | 1 | 0 | 1 | 0 |
| 概率视角下的机器学习 | 1 | 1 | 0 | 1 |
| 图分析与可视化 | 1 | 0 | 0 | 0 |
| 贝叶斯推理 | 1 | 0 | 0 | 1 |
| 欺诈分析 | 1 | 0 | 0 | 0 |
| 深度学习 | 1 | 1 | 0 | 1 |
此表采用与项目相同的方法。在这种情况下,每一行是一个项目,每一列代表不同的用户。在这里,列的顺序也很重要。结果向量表示看起来像这样:
向量(流畅的 Python) = [1, 0, 1, 0]
向量(机器学习) = [1, 1, 0, 1]
向量(图分析) = [1, 0, 0, 0]
向量(贝叶斯推理) = [1, 0, 0, 1]
向量(欺诈分析) = [1, 0, 0, 0]
向量(深度学习) = [1, 1, 0, 1]
在这两种情况下,都可以通过查询轻松地从图数据库中提取这些向量。这个例子再次表明,图不仅是一个适当的数据表示模型,可以以易于访问和导航的格式保持复杂数据,而且还提供了灵活性,可以以适合不同学习过程的数据格式导出数据。类似的模型可以同时为基于内容和邻域的方法提供数据,而且这些方法甚至可以在同一图中共存。而我们只是刚开始揭示图的力量!
在我们检查向量查询本身之前,让我们快速看一下一些可以帮助您在数据提取和处理方式上获得显著提升的考虑因素。以内存高效的方式表示向量和有效地访问它们的值是重要的机器学习任务。向量表示的最佳方法根据向量的性质及其使用方式而有所不同。在基于内容的场景中,项目向量的创建依赖于少量可能的向量维度。向量的稀疏性或密集性是最重要的考虑因素。图 5.9 显示了每种类型向量的示例。

图 5.9 密集和稀疏向量的示例(带有随机值)
如果您还记得我们关于基于预定义特征创建电影向量的讨论,可能的特征数量相对有限。考虑到所有可能的演员和导演以及所有类型等,特征的数量不会超过 10,000 或 20,000。使用文本创建向量也是如此。在这种情况下,向量的尺寸由语言词汇量或语言中使用的单词数量定义。尽管与向量的维度性相比非零值的数量较小,但总体而言,向量是小的。
与其大小相比,具有相对较多非零值的向量被称为密集向量。这样的向量可以通过存储在双精度浮点数组中的实现来表示。向量索引直接对应于数组索引。密集向量的优势在于速度:由于是数组支持的,因此快速访问和更新其任何值。
在光谱的另一端,一个典型的电子商务网站包含超过 100 万件商品,并且(对他们来说理想的情况)有数量相同级别的用户。每个用户,即使是患有强迫性在线购物症候群的用户(像我一样对书籍),也只能购买整体可能商品的一小部分。另一方面,每个商品——即使是畅销书——也只被相对较少的用户购买,因此相应向量中的非零值总数始终很小。这种总非零值百分比小的向量被称为稀疏向量。使用数组表示稀疏向量不仅浪费内存,而且使任何计算都变得昂贵。
可以用不同的方式表示稀疏向量以优化内存并简化操作。在 Java 中,Apache Mahout 提供了一些最常见的方法,⁶是一个用于创建可扩展高性能机器学习应用的分布式线性代数框架。对于相似度计算,我使用了一种不同的稀疏向量表示方法,该方法仍然使用浮点数或双精度浮点数数组作为基本数据结构。我首选的稀疏向量实现结构在图 5.10 中描述。

图 5.10 稀疏向量表示
图 5.10 展示了稀疏向量如何以紧凑的格式表示。第一个元素包含原始数组中非零值的数量。我们将它定义为 N。第二部分,从位置 1 开始(记住向量的索引从 0 开始)到位置 N 结束,包含非零值的索引。最后一部分,从 N+1 开始,一直延续到向量的末尾,包含实际的值。
这种表示方法的优势在于,使用单个小型数组,可以表示一个长而复杂的稀疏向量。这个数组需要的内存最小,并且可以轻松地作为节点的属性存储。当你需要处理大量数据时,这种优势是非常大的。我的个人 Java 实现可以在代码仓库的 ch05/java 目录中找到。
为了使本章内容保持一致,列表 5.2 包含了 Python 中稀疏向量的表示。在这种编程语言中,稀疏向量可以表示为一个字典,其中键是向量中元素的索引,值是有效的元素值。在这种情况下,非零元素的数量是字典的大小,使得表示足够简单。相关的代码可以在代码仓库的 util/sparse_vector.py 文件中找到。
列表 5.2 Python 中的稀疏向量实现
def convert_sparse_vector(numbers): ❶
vector_dict = {}
for k, c in enumerate(numbers): ❷
if c: ❸
vector_dict[k] = c
return vector_dict
if __name__ == '__main__':
print(convert_sparse_vector([1, 0, 0, 1, 0, 0])) #{0: 1, 3: 1}
print(convert_sparse_vector([1, 1, 0, 0, 0, 0])) #{0: 1, 1: 1}
print(convert_sparse_vector([1, 1, 0, 0, 0, 1])) #{0: 1, 1: 1, 5: 1}
❶ 从向量创建字典(稀疏向量)的函数
❷ 遍历向量,获取位置和值
❸ 检查值是否不为空或 0
对于使用稀疏向量而不是密集向量,没有真正的阈值需要考虑。通常,我更喜欢使用稀疏向量,因为它们优化了相似度计算。从现在起,本书中的大多数示例都将使用稀疏向量。
现在我们已经拥有了提取用户和项目向量以及计算它们之间相似度所需的所有元素。提取用户和项目每个向量非零元素的查询在接下来的两个列表中显示。
注意:在这些查询中,我们使用节点 ID 作为索引,但任何整数或长值都可以工作。我们可以使用任何识别项目的数字 ID,例如,如 itemId。
列表 5.3 查询提取用户稀疏向量
MATCH (u:User {userId: "121688"})-[:PURCHASES]->(i:Item)
return id(i) as index, 1 as value
order by index
列表 5.4 查询提取项目稀疏向量
MATCH (i:Item {itemId: "37029"})<-[:PURCHASES]-(u:User)
RETURN id(u) as index, 1 as value
ORDER BY index
在 MATCH 子句中,使用 PURCHASES 关系找到用户购买的所有项目或购买项目的所有用户。RETURN 子句提取结果向量的非零元素。因为没有值表示关系的权重,所以使用二进制方法,默认情况下,每个值分配为 1。
练习
将前面的查询更改为使用 itemId 和 userId 而不是节点 ID。
提示:考虑到查询存储为字符串,尽管它们是整数,并且记住我们需要一个数值作为索引。
下一步是计算和存储相似度。对于每个用户或项目,我们必须执行以下操作:
-
计算与其他所有元素(均匀地:每个用户与其他用户以及每个项目与其他项目)的相似度。
-
按降序排列相似度。
-
仅保留前 k 个,其中 k 的值是预定义的。或者,设置一个阈值或最小相似度值,并仅保留高于该值的相似度。
-
将前 k 个相似元素存储在图中作为用户或项目之间的新关系。
图 5.11 展示了推荐过程中的“计算最近邻”块,总结了前面的步骤序列。

图 5.11 计算最近邻的细节
以下列表显示了此任务的 Python 实现,使用稀疏向量表示。代码位于 ch05/recommendation/collaborative_filtering/recommender.py 文件中。
列表 5.5 相似度计算
label = "User" ❶
property = "userId" ❶
sparse_vector_query = """ ❷
MATCH (u:User {userId: $id})-[:PURCHASES]->(i:Item)
return id(i) as index, 1.0 as value
order by index
"""
def compute_and_store_KNN(self, size: int) -> None: ❸
print("fetching vectors")
vectors = self.get_vectors()
print(f"computing KNN for {len(vectors)} vectors")
for i, (key, vector) in enumerate(vectors.items()): ❹
# index only vectors
vector = sorted(vector.keys())
knn = FixedHeap(size)
for (other_key, other_vector) in vectors.items():
if key != other_key:
# index only vectors
other_vector = sorted(other_vector.keys())
score = cosine_similarity(vector, other_vector)
if score > 0:
knn.push(score, {"secondNode": other_key, "similarity":
➥ score})
self.store_KNN(key, knn.items())
if (i % 1000 == 0) and i > 0:
print(f"{i} vectors processed...")
print("KNN computation done")
def get_vectors(self) -> Dict: ❺
with self._driver.session() as session:
tx = session.begin_transaction()
ids = self.get_elements(tx)
vectors = {id_: self.get_sparse_vector(tx, id_) for id_ in ids}
return vectors
def get_elements(self, tx) -> List[str]: ❻
query = f"MATCH (u:{self.label}) RETURN u.{self.property} as id"
result = tx.run(query).value()
return result
def get_sparse_vector(self, tx: Transaction, current_id: str) -> Dict[int,
➥ float]: ❼
params = {"id": current_id}
result = tx.run(self.sparse_vector_query, params)
return dict(result.values())
def store_KNN(self, key: str, sims: List[Dict]) -> None: ❽
deleteQuery = f""" ❾
MATCH (n:{self.label})-[s:SIMILARITY]->()
WHERE n.{self.property} = $id
DELETE s"""
query = f""" ❿
MATCH (n:{self.label})
WHERE n.{self.property} = $id
UNWIND $sims as sim
MATCH (o:{self.label})
WHERE o.{self.property} = sim.secondNode
CREATE (n)-[s:SIMILARITY {{ value: toFloat(sim.similarity) }}]->
➥ (o)"""
with self._driver.session() as session:
tx = session.begin_transaction()
params = {
"id": key,
"sims": sims}
tx.run(deleteQuery, params)
tx.run(query, params)
tx.commit()
❶ 为基于用户的相似度设置一些变量:节点的标签和具有元素 id 的属性。项目的变量在代码库中。
❷ 查询提取用户购买作为向量。每行返回的第一个元素是 itemId;第二个固定为 1,因为我们只对购买事件感兴趣,而不是比率或用户购买项目的次数。参见列表 5.3。
❸ 计算并存储所有用户相似度的入口点函数。更改变量,您将得到相同的项目结果。
❹ 遍历所有用户的稀疏向量字典,计算所有相似性,为每个节点保留 k 个最高值,并调用存储 k-NN 的函数
❺ 创建一个字典的函数,其中键是 userId,值是用户的稀疏向量
❻ 通过查询数据库返回元素(用户或物品)ID 的列表的函数
❼ 通过查询图数据库返回指定用户(或物品)的相关稀疏向量的函数
❽ 将 k-NN 存储到数据库中的函数
❾ 查询以删除节点的所有相似性
❿ 一次性存储所有相似性的查询
如前述代码所示,相似度计算需要执行 N × N 次计算,其中 N 是 |U| 或 |V|。当 N 较大时,此操作可能需要一段时间。
图 5.12 显示了当关系存储在图中时,结果图模型的外观。因为图较小,所以 k 值设置为 2。

图 5.12 协同过滤的最终图模型
图 5.12 中的图不再是二分图,因为现在存在同一分区中元素之间的关系。但我们可以通过仅考虑节点和关系的子集来拥有同一图的多个子图,因此可以将图分为三个高度相关的子图:
-
以 U 和 I(U 代表所有用户,I 代表所有物品)为节点,并且只有购买关系的子图是我们之前提到的二分图。
-
以 U 为节点和相似性为关系的子图是 U 的最近邻网络(U 的 k-NN)。
-
以 I 为节点和相似性为关系的子图是 I 的最近邻网络(I 的 k-NN)。
在 5.4 节中,这些图被用作最后任务的输入,提供推荐,但重要的是要注意,这些图已经包含了很多信息。这个过程从二分图中提炼出新的知识,并以可以服务于除推荐之外多种目的的方式存储它。
-
物品聚类—通过在物品的最近邻网络上应用一些图聚类算法,可以识别(例如)通常一起购买的产品组或被同一组用户观看的电影。
-
用户分割—相同的聚类算法可以应用于用户的最近邻网络,结果将是一个用户组(分割),他们通常购买相同的产品或观看相同的电影。
-
寻找相似产品—物品的最近邻网络本身很有用。如果一个用户正在查看特定的产品,通过查询图,可以基于相似性关系显示一系列相似产品,并且这个操作将会很快。
图形方法不仅允许我们通过将信息存储在灵活的数据结构中来混合信息,而且还为我们提供了各种访问数据、揭示模式、提取知识和在统一数据环境中进行分析的机会。
建模技巧
本节和第四章中相应的部分描述了将不同类型的数据转换为图的技术。这些技术通常被称为图构建技术。在基于内容的情况下,数据由元数据和内容定义,例如演员、类型,甚至是电影的情节,而在协同过滤的情况下,数据是用户-物品矩阵。在这两种情况下,一个相似度函数和适当的数据转换为向量空间模型使我们能够创建一个比原始数据版本具有更多知识和更强沟通能力的图。
在机器学习的许多领域,图被用来模拟数据元素之间的局部关系,并从局部信息构建全局结构 [Silva and Zhao, 2016]。构建图有时是处理机器学习或数据挖掘应用中产生的问题所必需的,而在其他时候,它有助于管理数据。重要的是要注意,从原始数据到图数据表示的转换始终可以以无损的方式进行。反之则不然。因此,这里描述的技术和用例代表了如何执行这些图转换的具体示例;这些示例不仅适用于所描述的场景,而且也适用于许多实际用例。玩转你拥有的数据,并尝试将其转换为图。
到目前为止,我们一直使用余弦相似度作为计算相似性的基本函数。已经提出了其他度量标准——例如调整后的余弦相似度、斯皮尔曼秩相关系数、均方差异和皮尔逊系数——并且通常作为该函数的替代方案。具体来说,皮尔逊系数在基于用户的推荐中优于其他度量 [Herlocker et al., 1999],而余弦相似度在基于物品的推荐中表现最佳。
在本书的这一部分,重点是数据建模,因此余弦相似度是参考函数。在本书的后面部分,当适用时,将描述其他解决方案。
练习
查询通过运行计算相似度的代码获得的图,以找到以下内容:
-
给定一个物品,相似物品列表
-
给定一个用户,相似用户列表
-
相似度的最高值(搜索数据库以了解原因)
5.4 提供推荐
推荐过程的最终任务是,在必要时或有价值时,向用户提供建议列表。图 5.13 突出了推荐过程中的推荐提供。

图 5.13 推荐过程的最后一步
在我们的场景中,当用户在电子商务网站上导航时,我们希望在某些框中提供针对该用户的定制化推荐。从高层次来看,推荐过程会产生以下类型的输出:
-
相关性分数——数值预测,表示当前用户对某个项目的喜好或厌恶程度。
-
推荐——一个包含 N 个推荐项目的列表。当然,这样的前 N 个列表不应包含当前用户已经购买的项目,除非这些购买是重复购买。
在协同过滤的邻域方法中,第一个输出——相关性分数——可以通过查看用户的最近邻网络(基于用户的方法)或项目的最近邻网络(基于项目的方法)来产生。让我们更详细地看看这两种方法。
基于用户的方法的基本思想是,给定当前用户作为输入,表示交互数据库的二元图和最近邻网络,对于用户尚未看到或购买的产品 p,基于同行用户(该用户最近邻网络中的用户)对 p 的评分进行预测。考虑图 5.14 中的图。

图 5.14 基于用户方法的协同过滤基本思想
亚历山德罗购买了左侧的四个产品。基于用户的方法的想法是找到也购买了这些产品的相似用户,然后找到他们购买但亚历山德罗尚未购买的其他产品。这类方法的潜在假设[Jannach, 等人,2010]是
-
如果两个用户在过去有相似的味道,他们将来也会有相似的味道。
-
用户的偏好随时间保持稳定和一致。
一些方法减轻了这些假设,但它们超出了我们考虑的场景的范围。
让我们将用文字和图像表达的想法转换为计算机可以理解的公式。可能有两种情况发生。在一种情况下,用户和项目之间的交互(点击、购买和查看)没有权重。这种情况称为二元或布尔模型。在第二种情况下,交互有权重(如评分)。这种情况的公式是

这个公式预测用户 a 会对项目 p 分配的评分。KNN(a)代表用户 a 的 k 个最近邻,r[b,p]代表用户 b 对产品 p 分配的评分。如果用户 b 没有对项目 p 进行评分,则该评分可以是 0。这个公式的某些变体存在,但它们超出了本章的范围。
布尔情况稍微复杂一些,因为在文献中,没有一种方法被公认为最佳方法。以下公式 [Sarwar et al., 2000] 在我看来是最合理的一个,并且在电子商务网站上得到了广泛应用:

在这里,如果用户 b 购买了产品 p,则 r[b,p]将为 1,否则为 0,因此求和将返回有多少最近邻购买了产品 p。这个值将用 a 的最近邻数量(|KNN(a)|的值)进行归一化。
以下列表包含了一个使用之前创建的最近邻网络的 Python 示例实现。代码位于 ch05/recommendation/collaborative_filtering/recommender.py 文件中。
列表 5.6 通过基于用户的方法提供推荐
score_query = """ ❶
MATCH (user:User)-[:SIMILARITY]->(otherUser:User)
WHERE user.userId = $userId
WITH otherUser, count(otherUser) as size
MATCH (otherUser)-[r:PURCHASES]->(target:Target)
WHERE target.itemId = $itemId
return (+1.0/size)*count(r) as score
"""
def get_recommendations(self, user_id: str, size: int) -> List[int]: ❷
not_seen_yet_items = self.get_not_seen_yet_items(user_id)
recommendations = FixedHeap(size)
for item in not_seen_yet_items:
score = self.get_score(user_id, item)
recommendations.push(score, item)
return recommendations.items()
def get_not_seen_yet_items(self, user_id: str) -> List[int]: ❸
query = """
MATCH (user:User {userId:$userId})
WITH user
MATCH (item:Item)
WHERE NOT EXISTS((user)-[:PURCHASES]->(item))
return item.itemId
"""
with self._driver.session() as session:
tx = session.begin_transaction()
params = {"userId": user_id}
result = tx.run(query, params).value()
return result
def get_score(self, user_id: str, item_id: str) -> float: ❹
with self._driver.session() as session:
tx = session.begin_transaction()
params = {"userId": user_id, "itemId": item_id}
result = tx.run(self.score_query, params)
result = result.value() + [0.0]
return result[0]
❶ 基于用户之间相似性对项目评分的查询
❷ 提供推荐的函数
❸ 提供尚未查看(在本例中为尚未购买)项目列表的函数
❹ 通过使用简单查询预测用户对项目的评分的函数。查询已在变量中指定。
尽管基于用户的方法在不同领域已经成功应用,但当涉及到需要处理数百万用户和数百万目录项目的大型电子商务网站时,仍然存在一些严重的挑战。特别是,由于需要扫描大量潜在邻居,即使使用图方法,实际上也无法实时计算预测。因此,大型电子商务网站通常使用不同的技术。基于项目的推荐是这些技术之一,因为它们允许在大型评分矩阵中对推荐进行实时计算 [Sarwar et al., 2001]。
基于项目的算法的主要思想是通过使用项目之间的相似性来计算预测,而不是用户。让我们通过一个具体的例子来使这个想法更清晰。考虑图 5.15 中的图数据库,并假设我们需要预测用户 Alessandro 对项目 5 的评分。

图 5.15 基于项目的协同过滤基本思想
首先,我们比较其他项目的评分向量,寻找与项目 5 评分相似的项(即与项目 5 相似)。在示例中,我们看到项目 5 的评分[3, 5, 4, 1, 0]与项目 1 的评分[3, 4, 3, 1, 5]相似(两个相同,两个相差 1,0 是因为 Alessandro 没有对项目 5 进行评分),并且与项目 4[3, 3, 5, 2, 4]有部分相似性(一个相同,三个相差 1)。基于项目的推荐想法是查看 Alessandro 对这些相似项目的评分。他给项目 1 评了 5 分,给项目 4 评了 4 分。基于项目的算法计算这些其他评分的加权平均值,并预测项目 5 的评分在 4 到 5 之间。
再次,为了将这些词转换成具体的公式以供计算机程序使用,我们需要考虑两种用例:带有显式评分和带有简单布尔值,表示用户是否与项目互动(购买、点击等)。在布尔值的情况下,目标不是预测评分(例如 0-5),而是预测项目 5 对 Alessandro 的兴趣程度,范围从 0 到 1(0 = 不感兴趣,1 = 最有可能感兴趣)。
如果原始用户-项目数据集包含评分值,预测数据集中尚未看到的产品的评分的公式是 [Sarwar et al., 2001]。

这个公式可以根据你决定如何导航数据来重写为不同的形式。在我们的情况下,
-
q ∈ ratedItem(a) 考虑用户 a 评过的所有产品。
-
对于每个 q,它乘以三个值:
-
-
q 与目标产品 p 之间的相似性。
-
用户对 q 的评分。
-
|KNN(q) ∩ {p}|,如果 p 是 q 的最近邻集合中的元素,则为 1,否则为 0。可以考虑只考虑 q 的最近邻,而不是所有相似性。
-
-
分母将值标准化,使其不超过评分的最大值。
考虑图 5.15 中的数据。这三个项目之间的相似性是
-
项目 1-项目 4:0.943
-
项目 1-项目 5:0.759
-
项目 4-项目 5:0.811
因为只有三个项目,所以我们考虑了最近邻中的所有相似性;因此,对于我们来说 |KNN(q) ∩ {p}| 总是 1。用户 Alessandro 给项目 1 评了 5 星,给项目 4 评了 4 星。预测 Alessandro 对项目 5 的兴趣/评分的公式是

与基于用户的方法一样,对于布尔情况,没有接受的标准方法来计算分数,所以我将展示我最喜欢的其中一种 [Deshpande and Karypis, 2004]:

在这种情况下,我们没有评分,所以公式更简单。此外,它不返回预测,而是一个通用的分数。这样的分数可以通过不同的方式归一化,以方便比较。
以下列表展示了基于物品的邻域方法的示例实现。代码位于 ch05/recommendation/collaborative_filtering/recommender.py 文件中。
列表 5.7 使用基于物品的方法提供推荐
score_query = """ ❶
MATCH (user:User)-[:PURCHASES]->(item:Item)-[r:SIMILARITY]->(target:Item)
WHERE user.userId = $userId AND target.itemId = $itemId
return sum(r.value) as score
"""
def get_recommendations(self, user_id: str, size: int) -> List[int]:
[... See Listing 5.6 ...]
❶ 从列表 5.6 中唯一需要更改的是 score_query 参数。在这里,该值是通过考虑先前用户购买与目标物品之间的相似性来计算的。
练习
运行前面列表中的代码(可在本书的代码仓库中找到),更改用户并观察推荐列表如何变化。然后使用查询检查图数据库,看看建议的物品是否与用户之前购买的物品一致。
5.5 处理冷启动问题
在我结束关于协同过滤的这一章之前,讨论一个问题对于影响协同过滤推荐系统非常重要:数据稀疏性。在现实世界的电子商务应用中,用户-物品矩阵往往很稀疏,因为客户通常只购买了目录中很小一部分的产品(或为产品提供了评分)。对于没有或很少互动的新用户或新物品,这个问题更为严重。这个问题被称为冷启动问题,它进一步说明了解决数据稀疏性问题的重要性。
通常,与冷启动问题相关的挑战是在相对较少的信息可用时计算良好的预测。处理这个问题的直接选择之一是利用关于用户的额外信息,例如性别、年龄、教育、兴趣或任何其他可以帮助分类用户的数据。其他方法也存在,例如创建混合推荐系统,将多种方法合并到单个预测机制中(第七章)。
这些方法不再是纯粹的合作式,关于如何获取额外信息以及如何结合不同分类器的新问题也随之产生。尽管如此,为了达到合作方法所需的临界用户数量,这些技术在新的推荐服务启动阶段可能有所帮助。
在过去几年中提出的各种方法中,基于图的方法 [Huang et al., 2004] 通过过去的交易和反馈探索消费者之间的传递性关联(相似性)。这种方法的主要思想是利用当消费者共享物品时客户口味假设的传递性。用户的偏好由物品及其与物品的互动来表示。
以下示例说明了在推荐系统中探索传递性关联的想法。假设我们有一个类似于表 5.4 中的简单用户-物品数据集。
表 5.4 用户-物品数据集样本
| 物品 1 | 物品 2 | 物品 3 | 物品 4 | |
|---|---|---|---|---|
| 用户 1 | 0 | 1 | 0 | 1 |
| 用户 2 | 0 | 1 | 1 | 1 |
| 用户 3 | 1 | 0 | 1 | 0 |
到这本书的这一部分,你应该能够轻松地将这个表格表示为二分图。结果应该看起来像图 5.16。

图 5.16 表 5.4 中用户-项目数据集的图表示
提出的方法使用这种表示方法来解决数据稀疏性问题。此外,可以给每条边分配一个权重,例如其对应评分的值。
假设推荐系统需要为用户 1 推荐项目。当我们使用标准的协同过滤方法时,用户 2 将被视为用户 1 的同伴,因为这两个用户都购买了项目 2 和项目 4。由于用户 2 也是用户 1 最近的邻居,并且也购买了或喜欢了项目 3,因此项目 3 将被推荐给用户 1。在用户 1 和用户 3 之间找不到强烈的相似性。
在传递关联方法中,推荐方法可以通过计算项目节点和用户节点之间的关联,在基于图的模型中轻松实现。推荐是由确定用户和项目之间的路径来确定的。标准的协同过滤方法,包括基于用户和基于项目的两种方法,只考虑长度等于 3 的路径。(作为提醒,路径长度是通过考虑路径中的边来计算的。)
考虑我们的小例子。在图 5.16 中的二分图中,用户 1 和项目 3 之间的关联由所有长度为 3 的连接用户 1 和项目 3 的路径确定。从图中很容易看出,有两条路径连接用户 1 和项目 3:用户 1-项目 2-用户 2-项目 3 和用户 1-项目 4-用户 2-项目 3。图 5.17 突出了图 5.16 中所有长度为 3 的路径。

图 5.17 用户 1 和项目 3 之间的长度为 3 的路径
这种强烈的关联导致将项目 3 推荐给用户 1。连接项目节点和用户节点的唯一路径数量越多,这两个节点之间的关联就越强。
由于稀疏评分数据库中此类长度为 3 的路径数量很少,因此想法是也要考虑更长的路径——所谓的间接关联——来计算推荐。在基于图的模型中,将先前的方法扩展以探索和包含传递关联是直截了当的。
通过考虑长度超过 3 的路径,模型可以探索传递关联。例如,有两条长度为 5 的路径连接用户 1 和项目 1:用户 1-项目 2-用户 2-项目 3-用户 3-项目 1 和用户 1-项目 4-用户 2-项目 3-用户 3-项目 1。图 5.18 突出了图 5.16 中所有长度为 5 的路径。

图 5.18 用户 1 和项目 1 之间的长度为 5 的路径
因此,在考虑传递关联时,可以将项目 1 推荐给用户 1。
即使采用这种方法,也需要定义一个评分机制来对推荐列表进行排序。在这种情况下,推荐是基于用户节点和项目节点对之间的关联进行的。给定一个用户节点 User t 和一个项目节点 Item j,它们之间的关联,即 score(User t, Item j),定义为连接 User t 和 Item j 的所有独特路径的权重之和。
在这个公式中,只有长度小于或等于最大定义长度 M 的路径将被考虑。限制 M 是推荐系统设计者可以控制的参数。值得注意的是,M 必须是奇数,因为传递性关联在二分图中表示。

在这个公式中,pathsBetween(user,item,M) 返回所有长度为 x 且 x ≤ M 的从 用户 到 项目 的路径。路径的权重计算为 α^x,其中 α 是介于 0 和 1 之间的常数,确保较长的路径具有较小的影响。
α 的特定值可以根据底层应用领域的特性由系统设计者确定。在那些传递性关联可以作为消费者兴趣强预测的应用中,α 应取接近 1 的值,而在传递性关联很少传达信息的应用中,α 应取接近 0 的值。
让我们通过图 5.16 中的示例来说明这个计算。当 M 设置为 3(如标准协同过滤中所示)时,score(User 1, Item 3) = 0.5³ + 0.5³ = 0.25,而 score(User 1, Item 1) = 0。当 M 为 5 时,score(User 1, Item 3) = 0.5³ + 0.5³ = 0.25,而 score(User 1, Item 1) = 0.5⁵ + 0.5⁵ = 0.0625。对于消费者 User 1,上述分数计算会针对数据集中的所有项目重复进行。与之前的案例一样,项目会根据这个分数按降序排序。然后,将排序列表中的前 k 个项目(排除 User 1 过去购买的项目)推荐给 User 1。这种方法需要我们注意以下几个原因:
-
即使信息量很少,它也能生成高质量的推荐。
-
它使用本章迄今为止使用的相同的用户-项目数据集。
-
它纯粹基于图,并使用我们在示例中讨论的相同二分图模型。
此外,与标准基于用户和基于项目的算法进行比较表明,基于间接关系的建议技术可以显著提高推荐质量,特别是在评分矩阵稀疏的情况下。对于新用户,该算法与标准协同过滤技术相比,性能有可测量的提升。然而,当评分矩阵达到一定密度时,与标准算法相比,推荐质量可能会下降。
本节中描述的方法使用基于路径的相似性来计算推荐。其他基于图的方案使用更复杂的图算法。
5.6 图方法的优势
本章重点介绍了使用图和图模型创建协同过滤推荐引擎。特别是,它探讨了适合于图表示和基于图的数据导航的邻域方法。使用邻域方法实现的基于图的合作过滤推荐引擎的主要方面和优势如下:
-
用户-物品数据集可以很容易地表示为一个二分图,其中每个用户-物品对的权重被表示为关系的可选属性。
-
用户-物品数据集的二分图表示不仅具有只存储相关信息的优势——避免像矩阵表示那样存储无用的零,从而节省内存,而且还能在创建模型时通过仅关注可能相关的邻居来加速访问。
-
从同一个图模型中提取物品和用户的几个向量表示是可能的。
-
结果模型,由用户或物品或两者的相似性组成,可以自然地表示为连接用户和物品的新关系。结果的新图是最近邻(k-NN)网络。
-
基于相似性计算的算法,用于创建 k-NN 网络,代表了图构建中最强大和最广泛采用的技术之一。结果的网络不仅在推荐过程中易于导航,而且包含从现有的用户-物品数据集中提取的新知识,可用于从其他角度分析数据,例如物品聚类、客户细分等。
-
一种广泛采用的技术,用于解决数据稀疏问题和冷启动问题,基于图表示、导航和处理。图导航方法(如前面描述的路径查找示例)和图算法(如 PageRank)被应用于填补一些空白,并创建一个更密集的用户-物品数据集表示。
-
即使在这种情况下,也可以在单个图中混合多个推荐算法,并结合多种方法的力量来提供推荐。
摘要
本章继续我们关于数据建模的讨论,特别是通过介绍实现推荐引擎最常见的技术之一:协同过滤方法。
在本章中,你学习了
-
如何将用户-物品数据集以二分图的形式建模,以及如何将其投影到两个相关图中
-
如何仅使用与用户-物品交互相关的信息来计算用户和物品之间的相似性,而不是使用关于用户和物品的静态信息
-
如何在 k-NN 模型中存储这样的相似性
-
如何使用这样的相似性通过基于用户和基于物品的方法以及考虑二元和非二元值来向用户提供一个推荐列表
-
稀疏向量的优势是什么,何时使用它,以及如何实现它
-
如何使用基于图的技术来解决数据稀疏性问题,特别是冷启动问题
参考文献
[Bell 等人,2007] Bell, Yehuda Koren, 和 Chris Volinsky. “在多个尺度上建模关系以提高大型推荐系统的准确性。” 第 13 届 ACM SIGKDD 国际知识发现和数据挖掘会议论文集 (2007): 95-104.
[Börner,2010] Börner, Katy. 科学图谱:可视化我们所知. 剑桥,MA: MIT Press, 2010.
[da Costa 和 Manzato,2014] da Costa, Arthur F., 和 Marcelo Garcia Manzato. “推荐系统中的多模态交互:一种集成方法。” 巴西智能系统会议论文集 (2014): 67-72.
[Deshpande 和 Karypis,2004] Deshpande, Mukund, 和 George Karypis. “基于物品的 Top-N 推荐算法。” ACM 信息系统交易 22:1 (2004): 143-177. DOI: dx.doi.org/10.1145/963770.963776.
[Diestel,2008] Diestel, Reinhard. 图论(研究生数学教材). 第 5 版. 柏林: Springer, 2008.
[Go 等人,2007] Go, Kwang-Il Goh, Michael E. Cusick, David Valle, Barton Childs, Marc Vidal, 和 Albert-László Barabási. “人类疾病网络。” PNAS 104:21 (2007): 8685-8690. DOI: doi.org/10.1073/pnas.0701361104.
[Goldberg 等人,1992] Goldberg, David, David Nichols, Brian M. Oki, 和 Douglas Terry. “使用协同过滤编织信息锦缎。” ACM 通讯 35:12 (1992): 61-70. DOI: doi.acm.org/10.1145/138859.138867.
[Grujic´,2008] Grujic´, Jelena. “电影推荐网络作为二部图。” 第 8 届国际计算科学会议(第二部分)论文集 (2008): 576-583.
[Herlocker 等人,1999] Herlocker, Joseph A., Konstan, Loren G., Borchers, Al, 和 Riedl, John. “执行协同过滤的算法框架。” 第 22 届国际 ACM SIGIR 信息检索研究与发展年度会议论文集 (1999): 230-237. DOI: dx.doi.org/10.1145/312624.312682.
[Herlocker 等人,2004] Herlocker, Joseph A., Konstan, Loren G., Terveen, Loren G., 和 Riedl, John T. “评估协同过滤推荐系统。” ACM 信息系统交易 22:1 (2004): 5-53. DOI: dx.doi.org/10.1145/963770.963772.
[黄等,2004] 黄,赞,辛钦·陈,和丹尼尔·曾。“将关联检索技术应用于缓解协同过滤中的稀疏性问题。”ACM 信息系统交易 22:1(2004 年):116-142 页。DOI: dx.doi.org/10.1145/963770.963775.
[詹纳奇等,2010] 詹纳奇,迪特马尔,马克斯·赞克尔,亚历山大·费尔费尼格,和格尔哈德·弗里德里希。《推荐系统:导论》。剑桥,英国:剑桥大学出版社,2010 年。DOI: dx.doi.org/10.1017/CBO9780511763113.
[科伦,2008] 科伦,耶胡达。“因子分解与邻域相遇:一个多角度的协同过滤模型。”第十四届 ACM SIGKDD 国际知识发现和数据挖掘会议论文集(2008 年):426-434 页。DOI: doi.org/10.1145/1401890.1401944.
[科伦等,2009] 科伦,耶胡达,罗伯特·贝尔,和克里斯·沃尔辛斯基。“推荐系统中的矩阵分解技术。”计算机 42:8(2009 年):30-37 页。DOI: dx.doi.org/10.1109/MC.2009.263.
[纽曼,2010] 纽曼,马克。《网络:导论》。牛津,英国:牛津大学出版社,2010 年。
[宁等,2015] 宁,夏,克里斯蒂安·德罗西耶,和乔治·卡里皮斯。“基于邻域的推荐方法综述。”见《推荐系统手册》,由弗朗西斯科·里奇,利奥尔·罗卡奇,和布拉查·沙皮拉编辑,第 37-76 页。纽约:斯普林格,2015 年。DOI: doi.org/10.1007/978-1-4899-7637-6.
[萨瓦尔等,2000] 萨瓦尔,巴杜鲁,乔治·卡里皮斯,约瑟夫·康斯坦,和约翰·里德尔。“电子商务推荐算法分析。”第二届 ACM 电子商务会议论文集(2000 年):158-167 页。DOI= dx.doi.org/10.1145/352871.352887.
[萨瓦尔等,2001] 萨瓦尔,巴杜鲁,乔治·卡里皮斯,约瑟夫·康斯坦,和约翰·里德尔。“基于物品的协同过滤推荐算法。”第十届国际万维网会议论文集(2001 年)285-295 页。DOI: doi.org/10.1145/371920.372071.
[席尔瓦和赵,2016] 席尔瓦,蒂亚戈·C,和李昂·赵。《复杂网络中的机器学习》。纽约:斯普林格,2016 年。
[塔卡奇等,2007] 塔卡奇,加博尔,伊斯特万·皮拉斯齐,博蒂安·内梅特,和多米诺斯·蒂克。“重力推荐系统的主要组件。”SIGKDD 探索通讯 9:2(2007 年):80-83 页。DOI: doi.org/10.1145/1345448.1345466.
^(1.)随机梯度下降是机器学习中常见的优化技术。它试图通过迭代方法最小化目标函数(具体来说,是一个可微分的目标函数)。它被称为随机,因为样本是随机选择(或打乱顺序)的,而不是作为一个单一组(如标准梯度下降)或按照它们在训练集中出现的顺序。在协同过滤中,它用于矩阵分解。
^(2.)交替最小二乘法是另一种易于并行化的优化技术。
^(3.)在实际项目中,你可能会遇到这些类型的问题。内容策划问题在电子商务网站上很常见,我在很多我跟踪的项目中都见过这种情况。
^(4.)数据可通过 Kaggle 在mng.bz/8W8P获取。
^(5.)到这一点,你应该已经熟悉了公式,它总是与之前描述的所有用例中的公式相同,所以这里不再重复。如果你需要复习,请参阅第 4.2.3 节。
^(6.)例如,请参阅mng.bz/EVjJ或mng.bz/N8jD。
6 会话推荐
本章涵盖了
-
通过使用会话数据实现推荐系统
-
为基于会话的推荐引擎设计图模型
-
将现有数据集导入图模型
第四章和第五章介绍了实现推荐引擎的两种最常见方法:基于内容和协同过滤。每种方法的优点都得到了强调,但在讨论中也出现了几个缺点。值得注意的是,这些技术需要关于用户的信息,而这些信息并不总是可用。本章介绍了一种在难以或无法获取用户互动历史或其他用户细节时有用的推荐方法。在这种情况下,应用经典方法不会产生好的结果。
6.1 会话方法
假设你想为在线旅游网站构建一个推荐引擎。该网站提供住宿预订,但在流程的早期阶段不需要登录或注册。使用基于会话的推荐引擎,即使在用户信息很少的情况下,也可以提供推荐。
这种场景在预订住宿的网站上很常见。在这类网站上,以及在许多其他现实生活中的推荐场景中,用户通常不会登录或注册,直到选择过程的最后。只有在他们选择了住宿之后,用户才会登录或注册以完成预订。
传统的推荐方法依赖于从购买历史、显式评分或其他过去互动(如查看和评论)创建的用户资料。它们使用这些长期偏好模型来确定要向用户展示的项目。更具体地说,基于内容的推荐方法根据项目与用户资料中存在的项目的相似性推荐项目,而协同过滤方法基于具有相似资料的用户的选项进行预测。在这两种情况下,都假设可用的用户资料是信息丰富的。
然而,在许多推荐场景中,例如这里所描述的,由于大部分用户是首次访问者或未登录,因此他们没有可用的长期用户模型。在这种情况下,用户基本上是匿名的,所以我们之前考虑的推荐系统无法提供准确的结果。尽管有其他用户识别方法,如 cookies 和指纹技术,但由于它们的相对低可靠性和隐私问题,这些方法的适用性有限 [Tuan and Phuong, 2017]。此外,创建一个信息丰富的个人资料需要用户在过去与系统有足够的互动。
然而,提供能够捕捉当前用户兴趣和需求的有效推荐是提供高质量服务、提高用户满意度和吸引用户再次访问的关键。因此,必须根据其他类型的信息确定合适的推荐。尽管可用数据不是经典格式,但并非一切都已失去;可以使用用户与网站或应用的最新互动作为推荐的基础。在这种情况下,匿名、独特用户与系统之间的交互可以组织成会话。仅依赖于用户在当前会话中的行为并适应其推荐的推荐技术被称为基于会话的推荐方法 [Quadrana 等人,2018]。图 6.1 描述了基于会话的推荐引擎的关键元素及其关系。

图 6.1 基于会话的推荐心智模型
会话是在给定时间段内发生的一系列交互。它可能跨越一天、几天、几周甚至几个月。会话通常有一个时间限制的目标,例如今晚找餐厅吃饭,听某种风格或情绪的音乐,或寻找下一个假期的地点。图 6.2 展示了用户在找到合适的地点之前如何改变主意。

图 6.2 用户搜索假日地点
根据用户会话中可用的信息,推荐系统应该为用户创建一个模型并做出预测。会话数据具有许多重要特征:
-
会话点击和导航本质上是顺序的。点击顺序以及导航路径可能包含关于用户意图的信息。
-
观看的项目通常具有诸如名称、类别和描述等元数据,这些信息提供了关于用户品味和他们在寻找什么的线索。
-
会话在时间和范围上都是有限的。一个会话有一个特定的目标,通常在目标达成时结束:为商务旅行租酒店,为浪漫约会找餐厅,等等。会话与特定项目(如最终预订的酒店或餐厅)相关的内在信息能力。
当会话推荐的使用既实用又合理时,会话推荐引擎可以提供高质量的推荐,以高精度预测用户的最终目标,缩短导航路径和满足用户特定需求所需的时间。
问题解决了吗?我们可以轻松提供推荐,即使用户是匿名的。太棒了!
不幸的是,情况并不那么简单。处理会话的问题在于,并不总是容易识别会话何时开始和结束(任务完成时或会话不再相关)。想想你自己的经历。你在工作休息期间有多少次开始考虑你可能在下一个假期去哪里?你开始查看你梦想中的某个地点的酒店;然后你必须回去工作。你可能几天或几周后回到任务,也许会有不同的想法想去哪里。对于一个系统来说,理解何时可以认为会话已经结束或不再相关是一个困难的任务。幸运的是,一些特定领域的最佳实践可以应用于识别搜索会话的结束,例如考虑不活跃的天数或酒店的成功预订。
本章的示例场景说明了实现基于会话的推荐引擎的一些方法。这些方法有助于你处理用户-物品交互数据不可用的情况。
在文献中,顺序推荐问题(以及因此的基于会话的推荐)通常被实现为预测下一个用户行为的任务。从算法的角度来看,早期的预测方法基于顺序模式挖掘技术。后来,基于马尔可夫模型更复杂的方法被提出并成功应用于该问题。最近,基于人工神经网络的深度学习方法的运用被探索作为另一种解决方案。循环神经网络(RNNs),¹ 能够从顺序排列的数据中学习模型,对于这个问题是一个自然的选择,最近文献中报道了此类算法预测精度的显著提高 [Devooght and Bersini, 2017; Hidasi and Karatzoglou, 2018; Hidasi et al., 2016 a, b; Tan et al., 2016]。
Ludewig 和 Jannach [2018] 以及之前的工作 [Verstrepen and Goethals, 2014; Jannach and Ludewig, 2017 a; Kamehkhosh et al., 2017] 展示的一些结果表明,在计算和概念上,基于最近邻的方法通常会导致与基于深度学习模型的当前技术相当准确,甚至更好的预测。存在不同的最近邻方案,并且它们都与基于图的数据建模和推荐方法很好地匹配。因此,本节重点介绍这些方法。图 6.3 描述了该场景的推荐过程。

图 6.3 一个基于图的会话推荐系统示意图
6.2 节描述了如何将会话数据建模为图的形式。6.3 节说明了构建预测模型和提供推荐的各种技术。
6.2 事件链和会话图
会话数据可以根据学习算法的类型和可用数据的性质以多种方式建模。首先,考虑一些基于会话的推荐引擎的期望属性[Tuan and Phuong, 2017],这些属性与训练数据建模的方式相关:
-
数据表示应该能够模拟点击流中的顺序模式。最流行的方法之一,也是我们场景中选定的方法之一,是项目到项目的 k-NN 方法。这种方法基于项目的共现进行推荐,但忽略了点击的顺序。为了部分解决这个问题,我们可以引入时间衰减或相关性窗口,只考虑较长事件序列中的一小部分。
-
该方法应提供一种简单的方式来表示和组合项目 ID 与元数据。通常,一个项目与不同类型的特征相关联。例如,一个产品可能有一个 ID、一个名称和一个描述,它通常属于一个或多个类别(有时组织在类别层次结构中)。有一个通用的方式来表示不同的特征类型,并共同建模它们的交互,考虑到它们之间的关系和依赖性会更为方便。
-
用户兴趣和目标在导航过程中会演变。一步一步地,用户会专注于更具体的目标;他们的想法随着每一次点击而变得更加清晰。因此,在选择过程开始时点击的项目与后来点击的项目相比,相关性较低。时间必须得到适当的建模,以便为较新的项目分配比旧项目更多的价值。
图 6.4 中的图模型表示会话数据,考虑了所需的属性。

图 6.4 会话推荐方案的会话图
此模型满足所有要求,并支持不同类型的推荐算法。让我们更详细地描述它:
-
模型中的主要实体(节点)是会话、用户、项目和点击。这些元素代表了整个导航路径的步骤。
-
用户可以分为两类:匿名用户和注册/登录用户。这两种类型的用户都可以连接到一个会话,甚至可以同时连接,因为匿名用户必须登录或注册才能完成预订或购买。匿名用户和相关的注册用户之间的关系会被追踪,因此可以记录下用户在历史记录中点击的所有项目。此类信息对于在简单的用户-项目数据集上进行更传统的协同过滤方法非常有用。这些信息可能与发送定制电子邮件或其他类型的营销活动(如线下营销活动)相关。
-
会话是点击聚合器。所有点击都发生在与它们连接的特定会话中。每个会话属于一个用户。会话可以包含一些上下文信息,如开始时间、结束时间、位置和设备。这样的上下文信息对于提高推荐质量可能是相关的。
-
项目是一个广泛的概念,代表多个感兴趣元素,如页面、产品、搜索项和搜索查询。每个项目都包含一些描述特定特征的属性。与基于内容的方法类似,一些属性被建模为节点,然后连接到项目上;其他(特定于项目的,如 ID)被建模为项目节点的属性。
-
一个点击将一个会话与一个项目连接起来。它还包含一些信息,如时间和位置。点击流定义了一条路径,并包含极其有价值的信息。它不仅代表了一个导航路径,还代表了一个心理路径。在导航过程中,用户细化搜索,明确他们的目标并应用一些过滤器。这些信息很有价值,必须有效地进行建模。在图 6.4 所示的模型中,导航是通过使用 NEXT 关系来存储的,该关系将每个点击与下一个点击连接起来。出于性能考虑,还存储了一个与会话的连接。
-
用户在导航过程中细化他们的思考和目标;第一个点击的相关性不如最后一个点击。因此,在模型中(至少在概念上)模拟相关性衰减是很重要的。在学习和预测期间考虑时间因素有多种选择。考虑其中两种:
-
-
时间衰减—对较老的点击分配较低的权重。如果最后一个点击的权重为(相关性为)1.0,那么两个小时的点击可能的相关性为 0.8 或 0.5。根据算法应该多快忘记过去事件,可以定义不同的衰减函数,如线性或指数。
-
相关性窗口—一个或多个仅包含最后 N 个(N 可配置)点击的滑动窗口,限制了在模型训练或预测期间考虑的数据量。
-
-
一个会话可以持续很长时间,包含大量的点击。可以定义一个阈值,以时间或点击次数来指定,以考虑仅相关的点击并丢弃其他点击。这种方法有助于减少数据库的大小,确保只存储相关信息,并保证长期的高性能。
-
最后一条相关信息是会话如何结束的——是否以购买或离开结束。在图表示法中,通过将最后一个项目标记为特定的标签 AddToCartItem 来模拟这一信息。最终决策不仅代表有价值的信息,因为它使我们能够识别一个成功的会话,而且,一些方法仅计算每个项目与标记为 AddToCartItem 的项目之间的距离。
值得注意的是,这里定义的模型不仅对基于会话的推荐目的有用,而且代表了一种建模任何顺序事件以促进进一步分析的方法。
既然我们已经定义了用于建模事件链的图,让我们考虑一个真实示例,我们将使用为我们的图数据库定义的模式导入它。我们将使用的数据集是在 ACM RecSys 2015 挑战赛² 的背景下提供的,包含六个月的记录点击序列(项目浏览和购买)。数据集由两个文件组成:
-
yoochoose-clicks.dat—点击事件。文件中的每个记录/行具有以下字段:
-
-
会话 ID—会话的 ID。一个会话可能包含一个或多个点击。
-
时间戳—点击发生的时间。
-
项目 ID—项目的唯一标识符。
-
类别—项目的类别。
-
-
yoochoose-buys.dat—购买事件。文件中的每个记录/行具有以下字段:
-
-
会话 ID—会话的 ID。一个会话可能包含一个或多个购买事件。
-
时间戳—购买发生的时间。
-
项目 ID—项目的唯一标识符。
-
价格—项目的价格。
-
数量—购买此项目的单位数量。
-
在 yoochoose-buys.dat 文件中,会话 ID 总是存在于 yoochoose-clicks.dat 文件中;具有相同会话 ID 的记录一起形成用户在会话期间点击事件的序列。会话可能很短(几分钟)或很长(几小时),可能包含一个点击或数百个点击。一切取决于用户的活动。以下列表显示了根据迄今为止设计的模型创建会话数据的代码。
列表 6.1 从 yoochoose 文件导入会话数据
def import_session_data(self, file):
with self._driver.session() as session:
self.executeNoException(session,
"CREATE CONSTRAINT ON (s:Session) ASSERT s.sessionId IS UNIQUE") ❶
self.executeNoException(session,
"CREATE CONSTRAINT ON (i:Item) ASSERT i.itemId IS UNIQUE") ❶
query = """
MERGE (session:Session {sessionId: $sessionId}) ❷
MERGE (item:Item {itemId: $itemId, category: $category})
CREATE (click:Click {timestamp: $timestamp}) ❸
CREATE (session)-[:CONTAINS]->(click)
CREATE (click)-[:IS_RELATED_TO]->(item)
"""
dtype = {"sessionID": np.int64, "itemID": np.int64, "category":
➥ np.object} ❹
j = 0;
for chunk in pd.read_csv(file,
header=0,
dtype=dtype,
names=['sessionID', 'timestamp', 'itemID',
➥ 'category'],
parse_dates=['timestamp'],
chunksize=10**6): ❺
df = chunk
tx = session.begin_transaction()
i = 0;
for row in df.itertuples(): ❻
try:
timestamp = row.timestamp
session_id = row.sessionID
category = strip(row.category)
item_id = row.itemID
tx.run(query, {"sessionId": session_id, "itemId": item_id,
➥ "timestamp": str(timestamp), "category": category})
i += 1
j += 1
if i == 10000:
tx.commit()
print(j, "lines processed")
i = 0
tx = session.begin_transaction()
except Exception as e:
print(e, row)
tx.commit()
print(j, "lines processed")
print(j, "lines processed")
❶ 创建约束以保证会话和项目的唯一性
❷ 检查会话和项目是否存在;否则,创建它们
❸ 创建点击以及项目、点击和会话之间的所有关系
❹ 定义导入 CSV 文件的类型(在 Pandas 中有助于防止类型转换问题)
❺ 以 10⁶ 行为单位读取 CSV 文件并分批提交以加快处理速度
❻ 遍历行并运行查询,为每个会话传递参数以创建新的点击
CSV 文件中的每一行都包含特定项目的点击。MERGE 子句允许我们只创建一次会话和项目;然后点击节点将会话连接到项目。以下列表向已结束购买的现有会话添加购买信息。
列表 6.2 从 yoochoose 文件导入购买数据
def import_buys_data(self, file):
with self._driver.session() as session:
query = """
MATCH (session:Session {sessionId: $sessionId}) ❶
MATCH (item:Item {itemId: $itemId})
CREATE (buy:Buy:Click {timestamp: $timestamp}) ❷
CREATE (session)-[:CONTAINS]->(buy)
CREATE (buy)-[:IS_RELATED_TO]->(item)
"""
dtype = {"sessionID": np.int64, "itemID": np.int64, "price":
➥ np.float, "quantity": np.int} ❸
j = 0;
for chunk in pd.read_csv(file,
header=0,
dtype=dtype,
names=['sessionID', 'timestamp', 'itemID',
➥ 'price', 'quantity'],
parse_dates=['timestamp'],
chunksize=10**6): ❹
df = chunk
tx = session.begin_transaction()
i = 0;
for row in df.itertuples(): ❺
try:
timestamp = row.timestamp
session_id = row.sessionID
item_id = row.itemID
tx.run(query, {"sessionId": session_id, "itemId":
➥ item_id, "timestamp": str(timestamp)})
i += 1
j += 1
if i == 10000:
tx.commit()
print(j, "lines processed")
i = 0
tx = session.begin_transaction()
except Exception as e:
print(e, row)
tx.commit()
print(j, "lines processed")
print(j, "lines processed")
❶ 搜索会话和项目
❷ 创建购买(一种特殊的点击)以及项目、购买和会话之间的所有关系
❸ 定义导入 CSV 文件的类型(在 Pandas 中有助于防止类型转换问题)
❹ 以 10⁶ 行为单位读取 CSV 文件以加快处理速度
❺ 遍历行并运行查询,为每个会话传递参数以创建新的点击
虽然这段代码是正确的,但运行速度较慢。它易于理解且线性,这就是为什么在这里被优先选择,但它可能需要运行数小时。在本章的代码仓库中,你可以找到一个性能更好但也更复杂的不同版本。
无论你运行哪个版本,结果都将相同。以下查询允许你可视化导入的结果,考虑单个会话(ID 为 140837 的那个会话)。
列表 6.3 查询以显示与特定会话相关的子图
MATCH p=(s:Session {sessionId: 140837})-[:CONTAINS]->(c)
MATCH q=(c)-[:IS_RELATED_TO]->(item:Item)
return p,q
练习
尝试使用新的数据库,并编写查询以找到以下内容:
-
被点击最多的 10 个项目
-
购买量最多的 10 个商品(它们匹配吗?)
-
最长的会话(它是否包含购买?)
6.3 提供推荐
第 6.2 节设计的模型足够灵活,可以服务于不同的推荐方法。正如本章引言中所述,基于会话的推荐引擎在许多场景中都很有用。这个主题已经被广泛研究,已经提出了各种各样的解决方案,以提供尽可能好的推荐。
第一种自然的方法是使用协同过滤方法,具体来说,是使用 k 近邻方法,通过使用会话-项目数据代替用户-项目矩阵。尽管如此,由于事件链的顺序性质,一些研究人员提出了使用 RNN 或卷积神经网络(CNNs)³来揭示会话中的顺序模式,并使用它们来提供推荐[Jannach and Ludewig, 2017 a; Quadrana et al., 2018; Tuan and Phuong, 2017]。与基本忽略实际动作顺序的协同过滤方法相比,RNN 和 CNN 考虑了整体的导航路径,并且可以模拟会话点击的顺序模式。尽管已经证明这些方法可以提供高质量的推荐,但它们的实现相当复杂,并且需要大量数据进行适当的训练。此外,如果正确实现,k-NN 方法在效率和品质上都可以优于深度学习方法[Jannach and Ludewig, 2017 a; Ludewig and Jannach, 2018]。
由于这些原因,在本节中,我们将考虑不同的 k-NN 方法来解决基于会话推荐引擎中建议前-N 个项目的问题。多年来,已经提出了多种实现此类任务的方案。对我们场景和本节目的最相关的是[Ludewig and Jannach, 2018]。
-
基于物品的 k-NN(Item-KNN)—这种方法,由 Hidasi 等人[2016 a]提出,仅考虑给定会话(当前会话)中的最后一个元素,并推荐那些在其他会话中与其共现最相似的物品。如果用户当前正在查看比利亚尔巴的别墅,系统将建议其他在用户查看比利亚尔巴的同一别墅时也频繁出现的别墅。
-
基于会话的 k-NN(SKNN)—这种方法不是只考虑当前会话中的最后一个事件,而是将整个当前会话(或其重要部分,仅考虑最新的 N 次点击)与训练数据中的过去会话进行比较,以确定要推荐的物品[Bonnin 和 Jannach, 2014; Hariri 等人,2012; Lerche 等人,2016]。如果一个用户在比利亚尔巴、巴塞罗那和马德里看过别墅,算法将搜索类似会话(因为它们包含或多或少相同的条目)并计算建议物品的分数。
第二种方法比第一种方法更复杂一些,但它的准确度与更复杂的实现(如 RNNs 和 CNNs)相当,同时训练模型所需的数据更少。然而,基于物品的方法也能提供有价值的特征,因此以下章节将考虑这两种方法。
6.3.1 基于物品的 k-NN
假设你正在浏览鞋子。如果系统根据其他用户在到达与你相同的鞋子之前和之后查看的内容向你展示类似鞋子,这会有用吗?基于物品的k-NN 方法使用会话数据来计算成对物品之间的相似性。整体方法与基于物品的协同过滤案例相同;唯一的区别是,不是使用用户-物品矩阵,而是使用会话-物品数据。与经典方法一样,第一步是将物品表示为向量空间模型(VSM),其中每个元素对应一个会话,如果物品出现在会话中,则设置为 1。两个物品的相似度可以通过使用余弦相似度度量来确定,例如,邻居数量k隐式地由期望的推荐列表长度定义。整个过程在图 6.5 中展示。

图 6.5 使用 Item-KNN 的基于会话的推荐方案
从概念上讲,该方法实现了一种“购买了 . . . 的客户也购买了”的形式。使用余弦相似度指标使该方法对流行度偏差的敏感性降低。尽管项目到项目的方案相对简单,但它们在实践中被广泛使用,有时被认为是一个强大的基线 [Davidson 等人,2010;Linden 等人,2003]。值得注意的是,在这种情况下可以使用项目元数据信息,即使与会话数据结合使用,也可以计算项目之间的相似度。当您有一个没有历史记录的新项目时,这种技术非常有用。
在实现方面,所有相似度值都可以在训练过程中预先计算并排序,以确保在推荐时的快速响应。需要根据时间(每 x 小时)或会话量(每 x 次新的会话点击)进行更新。
考虑一个具有平凡数据示例,并逐步跟踪整个过程。假设我们有表 6.1 中描述的五个会话。
表 6.1 会话示例
| 会话编号 | 会话内容(项目 ID 的有序列表) |
|---|---|
| 1 | [项目 12, 项目 23, 项目 7, 项目 562, 项目 346, 项目 85] |
| 2 | [项目 23, 项目 65, 项目 12, 项目 3, 项目 9, 项目 248] |
| 3 | [项目 248, 项目 12, 项目 7, 项目 9, 项目 346] |
| 4 | [项目 85, 项目 65, 项目 248, 项目 12, 项目 346, 项目 9] |
| 5 | [项目 346, 项目 7, 项目 9, 项目 3, 项目 12] |
目标是在会话 5 中向用户推荐一些内容。当前项目是 12。第一步包括计算此项目与所有尚未查看的项目之间的距离。表 6.2(VSM 表示)有助于轻松提取项目的向量表示,正如我们在第五章中看到的。
表 6.2 表 6.1 的会话以 VSM 表示
| 会话编号 | 3 | 7 | 9 | 12 | 23 | 65 | 85 | 248 | 346 | 562 |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 |
| 2 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 0 |
| 3 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
| 4 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 0 |
| 5 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
重要的是要注意点击顺序已丢失。以下代码片段为您执行所有必要的计算。使用它比手动计算要容易,并且它引入了 sklearn⁴,这是一个强大的机器学习库。
列表 6.4 计算项目 12 与所有尚未查看的项目之间的相似度
from sklearn.metrics.pairwise import cosine_similarity
#Vector representation of the items
item3 = [0,1,0,0,1]
item7 = [1,0,1,0,1]
item9 = [0,1,1,1,1]
item12 = [1,1,1,1,1]
item23 = [1,1,0,0,0]
item65 = [0,1,0,1,0]
item85 = [1,0,0,1,0]
item248 = [0,1,1,1,0]
item346 = [1,0,1,1,1]
item562 = [1,0,0,0,0]
# Compute and print relevant similarities
print(cosine_similarity([item12], [item23])) # 0.63245553
print(cosine_similarity([item12], [item65])) # 0.63245553
print(cosine_similarity([item12], [item85])) # 0.63245553
print(cosine_similarity([item12], [item248])) # 0.77459667
print(cosine_similarity([item12], [item562])) # 0.4472136
在此情况下,最相似的项目是项目 248。其他项目具有相同的相似度得分或甚至更高,例如项目 9 和 346,但根据我们的推荐策略,我们决定避免展示用户已经看到的项目。
现在过程已经清楚,让我们从平凡示例转移到我们的真实数据库。我们将过程分为两部分。第一部分将预先计算项目之间的相似度并存储最接近的 k 个邻居,第二部分将提供推荐。列表 6.5 显示了相似度预计算的代码。
列表 6.5 从图中提取项目向量;计算和存储相似度
def compute_and_store_similarity(self): ❶
items_VSM = self.get_item_vectors()
for item in items_VSM:
knn = self.compute_knn(item, items_VSM.copy(), 20);
self.store_knn(item, knn)
def get_item_vectors(self): ❷
list_of_items_query = """
MATCH (item:Item) ❸
RETURN item.itemId as itemId
"""
query = """ ❹
MATCH (item:Item)<-[:IS_RELATED_TO]-(click:Click)<-
➥ [:CONTAINS]-(session:Session)
WHERE item.itemId = $itemId
WITH session
ORDER BY id(session)
RETURN collect(distinct id(session)) as vector;
"""
items_VSM_sparse = {}
with self._driver.session() as session:
i = 0
for item in session.run(list_of_items_query):
item_id = item["itemId"];
vector = session.run(query, {"itemId": item_id})
items_VSM_sparse[item_id] = vector.single()[0] ❺
i += 1
if i % 100 == 0:
print(i, "rows processed")
print(i, " rows processed")
return items_VSM_sparse
def compute_knn(self, item, items, k): ❻
dtype = [ ('itemId', 'U10'),('value', 'f4')]
knn_values = np.array([], dtype=dtype)
for other_item in items:
if other_item != item:
value = cosine_similarity(items[item], items[other_item]) ❼
if value > 0:
knn_values = np.concatenate((knn_values,
➥ np.array([(other_item, value)], dtype=dtype)))
knn_values = np.sort(knn_values, kind='mergesort', order='value' )[::-1]❽
return np.split(knn_values, [k])[0]
def store_knn(self, item, knn): ❾
with self._driver.session() as session:
tx = session.begin_transaction()
knnMap = {a : b.item() for a,b in knn}
clean_query = """ ❿
MATCH (item:Item)-[s:SIMILAR_TO]->()
WHERE item.itemId = $itemId
DELETE s
"""
query = """ ⓫
MATCH (item:Item)
WHERE item.itemId = $itemId
UNWIND keys($knn) as otherItemId
MATCH (other:Item)
WHERE other.itemId = otherItemId
MERGE (item)-[:SIMILAR_TO {weight: $knn[otherItemId]}]->(other)
"""
tx.run(clean_query, {"itemId": item})
tx.run(query, {"itemId": item, "knn": knnMap})
tx.commit()
❶ 处理所有项目的入口点
❷ 搜索项目并为每个项目创建向量
❸ 获取项目列表的查询
❹ 根据所属会话提取每个项目的向量
❺ 处理所有项目的入口点
❻ 对于每个项目,计算所有其他项目中的前 k 个最近邻
❼ 计算两个稀疏向量之间的余弦相似度
❽ 根据相似度值对邻居进行排序
❾ 存储模型(k-NN)
❿ 清理节点的旧模型
⓫ 将新模型存储为当前项目与最相似的前 k 个项目的相似关系 SIMILAR_TO
注意,这段代码与我们用于基于项目的协同过滤方法中的代码相似。这样的代码需要定期执行以保持相似度值最新。
在继续之前,我想提到一个问题,你可能会遇到。尽管它是形式上正确的,但前面列表中的代码将需要一段时间(实际上是很长一段时间)才能完成。这个问题在处理最近邻时是常见的。到目前为止,我们一直在处理小数据集,所以这不是一个问题,但正如我一开始承诺的,这本书旨在成为一本具体的书,帮助你解决真正的机器学习问题,而不是简单的例子。计算最近邻需要你计算 N × N 的相似度,当你有很多项目时,这个过程可能需要很长时间。此外,考虑到你偶尔还需要更新最近邻网络以保持其与用户最新点击的一致性,前面的代码在可生产的项目中并不实用。
你可以使用不同的技术来解决此问题。如果你的目标是计算每一对的相似度,你无法做任何事情来减少工作量,尽管并行处理可以减少经过的时间。你可以使用用于大规模数据处理的分析引擎,如 Apache Spark⁵或 Apache Flink。⁶
另一种方法是只考虑 k-最近邻的近似版本。通常,我们只想找到最相似的成对或所有相似度高于某个下限的成对。在这种情况下,我们只需关注可能相似的成对,而不必调查每一对。存在不同的算法用于计算 k-NN 的近似版本。其中之一,如图 6.6 所示,被称为局部敏感哈希(LSH)或近邻搜索 [Ullman and Rajaraman, 2011]。

图 6.6 解释 LSH 的心智模型
LSH 的一种通用方法是对项目进行多次哈希,使得相似的项目更有可能被哈希到同一个桶中,而不相似的项目则不太可能。然后我们考虑在任何时候被哈希到同一个桶中的任何一对作为候选对,并且只检查候选对以确定相似度。希望大多数不相似的对永远不会被哈希到同一个桶中,因此永远不会被检查。那些确实被哈希到同一个桶中的不相似对是假阳性;我们希望这些对只占所有对的一小部分。我们也希望大多数真正相似的对至少会哈希到同一个桶中一次。那些没有这样做的是假阴性,我们希望它们只占真正相似对的一小部分。使用这种技术的实现可以在代码仓库中找到,作为列表 6.5 的高级版本。
当每个项目的 k-NN 已经被预先计算后,推荐过程就是一个简单的查询,如下所示。
列表 6.6 使用项目 KNN 方法提供推荐的查询
MATCH (i:Item)-[r:SIMILAR_TO]->(oi:Item)
WHERE i.itemId = $itemId
RETURN oi.itemId as itemId, r.weight as score
ORDER BY score desc
LIMIT %s
这个查询在我的笔记本电脑上完成只需要 1 毫秒,因为所有内容都是预先计算的,导航相似度图非常快。
练习
在数据库中执行以下操作:
-
找到最接近的 10 个项目。
-
前一个查询的结果将显示,由于近似,很多项目的相似度值接近 1 甚至更高。导航这部分图,看看原因。
-
搜索最佳卖家项目和它们的邻居。它们也是最佳卖家吗?
通过查询,你会注意到导航图是多么简单。尝试自己从图中获取更多见解。
6.3.2 基于会话的 k-NN
与之前的方法相比,基于会话的方法的关键区别在于,它是计算会话之间的相似度,而不是项目之间的相似度。这些相似度以 k-NN 的形式存储在图中,用于评估项目并向用户返回推荐。整体方法在图 6.7 中描述。

图 6.7 使用 SKNN 的基于会话的推荐方案
在这个例子中,元数据,如标题、描述或特征列表,对于所选算法来说并不相关。这种方法另一个有趣的特点是它不需要太多信息就能有效。更详细地说,给定一个会话 s,推荐过程的结构如下:
-
通过应用合适的会话相似度度量,如项目空间上的二进制向量上的 Jaccard 指数或余弦相似度,计算 k 个最相似的过去会话(邻居)。根据 Quadrana [2017]的研究,二进制余弦相似度度量能带来最佳结果。此外,正如 Jannach 和 Ludewig [2017 a]所展示的,使用 k = 500 作为考虑的邻居数量,对于许多数据集来说,能带来良好的性能结果。
-
给定当前会话 s,其邻居 k 和相似度值,计算每个项目的分数,对它们进行排序,并返回前 N 个。
可以使用不同的公式作为评分函数。一个导致良好结果[邦宁和贾纳奇,2014]的函数是

其中
-
KNN(s)是 s 的 k 近邻网络。
-
sim(s,n)表示会话 s 和 n 之间的余弦相似度。
-
1n 是一个函数,如果会话 n 包含目标项目 i,则返回 1,否则返回 0。(此函数允许我们仅考虑包含目标项目的会话,过滤掉其他会话。)
为了理解公式和整体过程,请再次考虑表 6.1 中的示例会话及其 VSM 表示,为了您的方便,再次在表 6.3 中展示。
表 6.3 表 6.1 的会话以 VSM 表示
| 会话编号 | 3 | 7 | 9 | 12 | 23 | 65 | 85 | 248 | 346 | 562 |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 |
| 2 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 0 |
| 3 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
| 4 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 0 |
| 5 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
在这种情况下,我们感兴趣的是会话向量,我们可以通过逐行读取来提取它们。在下面的列表中,我们计算会话 5(我们的目标)与其他所有会话之间的距离。
列表 6.7 计算项目 12 与所有尚未查看的项目之间的相似度
from sklearn.metrics.pairwise import cosine_similarity
session1 = [0,1,0,1,1,0,1,0,1,1]
session2 = [1,0,1,1,1,1,0,1,0,0]
session3 = [0,1,1,1,0,0,0,1,1,0]
session4 = [0,0,1,1,0,1,1,1,1,0]
session5 = [1,1,1,1,0,0,0,0,1,0]
print(cosine_similarity([session5], [session1])) #0.54772256
print(cosine_similarity([session5], [session2])) #0.54772256
print(cosine_similarity([session5], [session3])) #0.8
print(cosine_similarity([session5], [session4])) #0.54772256
现在我们可以按照以下方式计算所有尚未查看项目的分数:
score(项目 23,会话 5) = 0.547 × 0 + 0.547 × 1 + 0.8 × 1 + 0.547 × 1 = 1.894
score(项目 65,会话 5) = 0.547 × 1 + 0.547 × 1 + 0.8 × 0 + 0.547 × 0 = 1.094
score(项目 85,会话 5) = 0.547 × 0 + 0.547 × 1 + 0.8 × 0 + 0.547 × 1 = 1.094
score(项目 248,会话 5) = 0.547 × 0 + 0.547 × 1 + 0.8 × 1 + 0.547 × 1 = 1.894
score(项目 562,会话 5) = 0.547 × 1 + 0.547 × 0 + 0.8 × 0 + 0.547 × 0 = 0.547
在这种情况下,最高分数由项目 23 和 248 获得。
在推荐阶段,时间限制意味着确定当前会话与数百万个过去会话之间的相似度,然后计算每个尚未查看项目的分数是不切实际的。可以实施各种方法来优化和加速此过程,其中许多方法可以使用图模型。我们在这里将考虑两种方法。
第一次优化
这种技术通过以下方式使用图模型提供优化:
-
k-NN 预计算——与上一个案例一样,可以预先计算(并保持更新)存储为会话之间关系的 k 近邻。后台进程可以根据某些标准,如时间或数量,更新这些关系。
-
后过滤——对于每个项目,我们已与其中出现该项目的所有会话建立了关系。这些关系可以用来过滤掉 k-NN(s)中不包含该项目的所有会话。
考虑为此场景设计的图模型,如图 6.4 所示,以下列表显示了如何预先计算所有会话之间的相似度。
列表 6.8 在图中计算和存储每个会话的 k-NN
def compute_and_store_similarity(self): ❶
sessions_VSM = self.get_session_vectors()
for session in sessions_VSM:
knn = self.compute_knn(session, sessions_VSM.copy(), 20);
self.store_knn(session, knn)
def compute_knn(self, session, sessions, k): ❷
dtype = [ ('itemId', 'U10'),('value', 'f4')]
knn_values = np.array([], dtype=dtype)
for other_session in sessions:
if other_session != session:
value = cosine_similarity(sessions[session],
➥ sessions[other_session])
if value > 0:
knn_values = np.concatenate((knn_values,
➥ np.array([(other_session, value)], dtype=dtype)))
knn_values = np.sort(knn_values, kind='mergesort', order='value' )[::-1]
return np.split(knn_values, [k])[0]
def get_session_vectors(self): ❸
list_of_sessions_query = """
MATCH (session:Session)
RETURN session.sessionId as sessionId
"""
query = """
MATCH (item:Item)<-[:IS_RELATED_TO]-(click:Click)<-
➥ [:CONTAINS]-(session:Session)
WHERE session.sessionId = $sessionId
WITH item
ORDER BY id(item)
RETURN collect(distinct id(item)) as vector;
"""
sessions_VSM_sparse = {}
with self._driver.session() as session:
i = 0
for result in session.run(list_of_sessions_query):
session_id = result["sessionId"];
vector = session.run(query, {"sessionId": session_id})
sessions_VSM_sparse[session_id] = vector.single()[0]
i += 1
if i % 100 == 0:
print(i, "rows processed")
break
print(i, " rows processed")
print(len(sessions_VSM_sparse))
return sessions_VSM_sparse
def store_knn(self, session_id, knn):
with self._driver.session() as session:
tx = session.begin_transaction()
knnMap = {a : b.item() for a,b in knn}
clean_query = """ ❹
MATCH (session:Session)-[s:SIMILAR_TO]->()
WHERE session.sessionId = $sessionId
DELETE s
"""
query = """ ❺
MATCH (session:Session)
WHERE session.sessionId = $sessionId
UNWIND keys($knn) as otherSessionId
MATCH (other:Session)
WHERE other.sessionId = toInt(otherSessionId)
MERGE (session)-[:SIMILAR_TO {weight: $knn[otherSessionId]}]->
➥ (other)
"""
tx.run(clean_query, {"sessionId": session_id})
tx.run(query, {"sessionId": session_id, "knn": knnMap})
tx.commit()
❶ 计算并存储所有会话的 k-NN 模型的入口点函数
❷ 计算相似度的函数
❸ 搜索会话并为每个基于点击的项目创建相关向量
❹ 清理当前会话现有模型的查询
❺ 创建新模型的查询
后过滤,它将帮助我们仅考虑包含当前项目的 k-NN 中的会话,可以通过以下查询实现。
列表 6.9 实现后过滤的查询
MATCH (target:Session)-[r:SIMILAR_TO]->(otherSession:Session)-[:CONTAINS]->
➥ (:Click)-[:IS_RELATED_TO]->(item:Item)
WHERE target.sessionId = 12547 AND item.itemId = 214828987
RETURN DISTINCT otherSession.sessionId
从这个查询开始,可以将其推广,以便我们可以使用定义的得分函数生成推荐列表。
列表 6.10 使用此优化进行推荐的推荐过程
def recommend_to(self, session_id, k):
top_items = []
query = """
MATCH (target:Session)-[r:SIMILAR_TO]->(d:Session)-[:CONTAINS]->
➥ (:Click)-[:IS_RELATED_TO]->(item:Item)
WHERE target.sessionId = $sessionId
WITH DISTINCT item.itemId as itemId, r
RETURN itemId, sum(r.weight) as score
ORDER BY score desc
LIMIT %s
"""
with self._driver.session() as session:
tx = session.begin_transaction()
for result in tx.run(query % (k), {"sessionId": session_id}):
top_items.append((result["itemId"], result["score"]))
top_items.sort(key=lambda x: -x[1])
return top_items
到目前为止,一切顺利,但如果使用样本数据集运行整个流程,将需要一段时间,并且需要大量的内存来计算 k-NN。计算完成后,推荐过程只需要几毫秒。通常,在代码仓库中,你可以找到一个高级版本,它使用一个名为 Annoy7(近似最近邻,是的)的 Python 库作为 LSH 的替代品。但是,对于包含 850,000 个会话的数据集,它仍然需要很长时间才能完成。
为了解决这些问题,我将提出第二种优化方法,该方法利用了数据存储的图方式。
第二种优化
这种技术以下列方式优化了过程:
-
k-NN 元素采样—从可能的邻居集中,通过随机选择或使用启发式方法提取 M 个会话的子样本。最有效的启发式方法之一是关注最近的会话,如果可用的话;关注最近趋势(会话)已被证明对电子商务中的推荐有效[Jannach 和 Ludewig,2017 b],并且比考虑所有过去会话时得到的结果更好。从 M 个会话集中,提取当前会话 s 的 k 个最近邻居:k-NN(s)。
-
得分计算预过滤—从 k-NN(s)中出现的会话中提取推荐项目集 R,仅考虑这些项目。最后,算法使用之前描述的公式计算 R 集中项目的得分。
k-NN(s) 的变化很大,因为会话是持续创建的。因此,每个会话的 k-NN(s) 并不以图中的关系存储(它们是实时计算的),但在采样和预过滤过程中使用图表。第二次优化允许高效地进行相似度计算和最终预测。在 Jannach 和 Ludewig [2017 a] 报道的实验中,例如,只需要考虑几百万个现有会话中的前 1,000 个最新会话,就可以获得高质量的结果。
6.4 图形方法的优点
在本章中,我们讨论了如何创建一个使用最近邻方法的基于会话的推荐引擎。所提出的解决方案非常适合数据图表示,它为加快推荐过程提供了正确的索引结构。此外,不同的优化可以使用图方法来优化计算和准确性。具体来说,基于图的方法在实现最近邻方法的基于会话的推荐引擎中的主要方面和优势是
-
事件链,如会话中点击的顺序,在图模型中容易表示。
-
图表使得按顺序访问事件变得容易,可以专注于最近的事件,同时丢弃或降低旧事件和会话的优先级,从而简化删除策略的实施。
-
图表提供了必要的灵活性,可以添加与某些算法(如 CNNs)相关的项目元数据 [Tuan and Phuong, 2017]。
-
在推荐过程中,特别是对于本章中描述的算法,图表提供了一个自然的索引结构,使我们能够更快地访问相关信息。在非图方法中,通常需要创建索引和其他缓存数据结构以加快推荐过程。图表为算法提供了所需的所有数据访问模式,减少了其他工具的需求。
摘要
本章介绍了基于会话的推荐引擎。各种数据模型展示了图表的灵活性如何满足在训练数据和模型存储方面的许多需求。在本章中,你学习了
-
当用户大多匿名时,如何使用基于会话的方法实现推荐引擎
-
如何对基于会话的方法进行训练数据和模型建模
-
如何使用不同的方法通过会话数据提供推荐
-
如何通过使用不同的技术(如 LSH)优化 k-NN 计算
参考文献
[Bonnin and Jannach, 2014] Bonnin, Geoffray, and Dietmar Jannach. “Automated Generation of Music Playlists: Survey and Experiments.” ACM Computing Surveys 47:2 (2014): Article 26.
[Davidson et al., 2010] Davidson, James, Benjamin Liebald, Junning Liu, Palash Nandy, Taylor Van Vleet, Ullas Gargi, Sujoy Gupta, Yu He, Mike Lambert, Blake Livingston, 和 Dasarathi Sampath. “YouTube 视频推荐系统。” 第 4 届 ACM 推荐系统会议论文集 (2010): 293-296.
[Devooght and Bersini, 2017] Devooght, Robin, 和 Hugues Bersini. “基于循环神经网络的长期和短期推荐。” 第 25 届用户建模、自适应和个性化会议论文集 (2017): 13-21.
[Goodfellow et al., 2016] Goodfellow, Ian, Yoshua Bengio, 和 Aaron Courville. 2016. 深度学习。麻省理工学院出版社。
[Hariri et al., 2012] Hariri, Negar, Bamshad Mobasher, 和 Robin Burke. “基于潜在主题序列模式的上下文感知音乐推荐。” 第 6 届 ACM 推荐系统会议论文集 (2012): 131-138.
[Hidasi and Karatzoglou, 2018] Hidasi, Balázs, 和 Alexandros Karatzoglou. “基于 Top-k 增益的会话推荐循环神经网络。” 第 27 届 ACM 国际信息与知识管理会议论文集 (2018): 843-852.
[Hidasi et al., 2016 a] Hidasi, Balázs, Alexandros Karatzoglou, Linas Baltrunas, 和 Domonkos Tikk. “基于循环神经网络的会话推荐。” 第 4 届国际学习表示会议论文集 (2016).
[Hidasi et al., 2016 b] Hidasi, Balázs, Massimo Quadrana, Alexandros Karatzoglou, 和 Domonkos Tikk. “用于特征丰富会话推荐的并行循环神经网络架构。” 第 10 届 ACM 推荐系统会议论文集 (2016): 241-248.
[Jannach and Ludewig, 2017 a] Jannach, Dietmar, 和 Malte Ludewig. “循环神经网络与邻域在会话推荐中的应用。” 第 11 届 ACM 推荐系统会议论文集 (2017): 306-310.
[Jannach and Ludewig, 2017 b] Jannach, Dietmar, 和 Malte Ludewig. “从日志数据中确定成功推荐的特性:一个案例研究。” 应用计算研讨会论文集 (2017): 1643-1648.
[Kamehkhosh et al., 2017] Kamehkhosh, Iman, Dietmar Jannach, 和 Malte Ludewig. “基于频繁模式技术和深度学习方法的会话推荐比较。” 第 1 届推荐系统时间推理研讨会论文集 (2017): 50-56.
[Lerche et al., 2016] Lerche, Lukas, Dietmar Jannach, 和 Malte Ludewig. “电子商务推荐中提醒的价值。” 第 24 届用户建模、自适应和个性化会议论文集 (2016): 27-35.
[Linden et al., 2003] Linden, Greg, Brent Smith, 和 Jeremy York. “Amazon.com 推荐:物品到物品的协同过滤。” IEEE 互联网计算 7:1 (2003): 76-80.
[Ludewig and Jannach, 2018] Ludewig, Malte, and Dietmar Jannach. “基于会话的推荐算法评估。” arXiv 预印本 arXiv:1803.09587(2018 年)。
[Quadrana, 2017] Quadrana, Massimo. “序列感知推荐系统的算法。” 博士学位论文,米兰理工大学(2017 年)。
[Quadrana et al., 2017] Quadrana, Massimo, Alexandros Karatzoglou, Balázs Hidasi, and Paolo Cremonesi. “使用分层循环神经网络进行基于会话的推荐个性化。” 第 11 届 ACM 推荐系统会议论文集(2017 年):130-137。
[Quadrana et al., 2018] Quadrana, Massimo, Paolo Cremonesi, and Dietmar Jannach. “序列感知推荐系统。” ACM 计算评论 51:4(2018 年):文章 66。
[Tan et al., 2016] Tan, Yong Kiam, Xinxing Xu, and Yong Liu. “用于基于会话推荐的改进循环神经网络。” 第 1 届深度学习推荐系统研讨会论文集(2016 年):17-22。
[Tuan and Phuong, 2017] Tuan, Trinh Xuan, and Tu Minh Phuong. “具有内容特征的基于会话的推荐的三维卷积网络。” 第 11 届 ACM 推荐系统会议论文集(2017 年):138-146。
[Ullman and Rajaraman, 2011] Ullman, Jeffrey David, and Anand Rajaraman. 大规模数据集挖掘. 英国剑桥:剑桥大学出版社,2011 年。
[Verstrepen and Goethals, 2014] Verstrepen, Koen, and Bart Goethals. “统一最近邻协同过滤。” 第 8 届 ACM 推荐系统会议论文集(2014 年):177-184。
^(1.)循环神经网络(RNNs)是具有非线性动力学的分布式实值隐藏状态模型。在每个时间步,RNN 的隐藏状态是从序列中的当前输入和前一步的隐藏状态计算得出的。然后,隐藏状态用于预测序列中下一个项目的概率。循环反馈机制在 RNN 的隐藏状态中记住每个过去数据样本的影响,克服了马尔可夫链的基本限制。因此,RNN 非常适合模拟用户动作序列中的复杂动态[Quadrana et al., 2017]。
^(2.)recsys.acm.org/recsys15/challenge。
^(3.)卷积神经网络(CNNs)是专门用于处理具有已知网格拓扑结构的数据的神经网络,例如用户-物品矩阵或可以建模为一维矩阵的时间序列数据。名称“卷积”来自神经网络中使用的数学线性运算,卷积,它至少在它们的层中替代了通用的矩阵乘法[Goodfellow et al., 2016]。
^(4.)scikit-learn.org。
^(5.)spark.apache.org。
^(6.)flink.apache.org。
^(7.)github.com/spotify/annoy。
7 上下文感知和混合推荐
本章涵盖
-
实现考虑用户上下文的推荐引擎
-
为上下文感知推荐引擎设计图模型
-
将现有数据集导入图模型
-
结合多种推荐方法
本章介绍了推荐场景中另一个先前方法忽略的变量:上下文。用户表达欲望、偏好或需求的具体条件对其行为和期望有强烈的影响。在推荐过程中考虑用户上下文存在不同的技术。我们将在本章中介绍主要的技术。
此外,为了完成我们对推荐引擎模型和算法的概述,我们将看到如何使用一种混合方法,该方法结合了迄今为止所展示的不同类型系统。这种方法将使我们能够创建一个独特且强大的推荐生态系统,能够克服每种单独推荐方法的所有问题、局限性和缺点。
7.1 基于上下文的方法
假设你想实现一个提供影院观影推荐的电影移动应用;我们将称之为 Reco4.me。通过使用上下文感知技术,你将能够在推荐过程中考虑环境信息,例如,建议用户当前位置附近的影院正在上映的电影。
让我们通过一个具体的例子进一步细化场景。假设你在伦敦,你想要在附近的影院找一部电影来看。你拿出手机,打开 Reco4.me 应用,希望得到一些好的推荐。你期望什么样的推荐?你希望了解你附近影院正在上映的电影。理想情况下,你希望得到适合你偏好的推荐。我不知道你,但对我来说,上下文会改变我的偏好。当我独自在家时,我喜欢看动作或奇幻电影。当我和孩子在一起时,我更喜欢看卡通或家庭电影。当我和我妻子在一起时,“我们”更喜欢看浪漫喜剧或爱情喜剧。应用应该考虑这种环境信息,并提供适合用户当前上下文的准确推荐。
这个例子展示了在推荐系统中考虑环境信息是多么重要,因为它可能对用户行为和需求产生微妙但强大的影响。因此,考虑环境信息可以显著影响推荐的质量,将可能在某些情况下有用的建议转化为在其他情况下无用的建议。这种情况不仅适用于这里描述的场景,也适用于许多其他场景。例如,想想你如何使用像亚马逊这样的电子商务网站。你可能用它为自己买一本书,为你的未婚夫买一份礼物,或者为你的孩子买一个玩具。你有一个单独的账户,但你的行为和偏好是由你在浏览网站时特定的需求所驱动的。所以,尽管在寻找为你儿子买滑板的推荐时看到可能对你感兴趣的书是有用的,但得到基于你之前为孩子购买的礼物的当前需求的建议会更有效。
传统的推荐系统,如第四章和第五章中讨论的基于内容和协同过滤方法的系统,往往使用相当简单的用户模型。例如,基于用户的协同过滤模型将用户简单地视为项目评分的向量。随着对用户偏好的观察越来越多,用户模型得到扩展,并使用用户偏好的完整集合来生成推荐或做出预测。因此,这种方法忽略了“情境行为”的概念[Suchman, 1987]——即用户在特定情境或特定范围内与系统交互的事实,以及同一情境中项目偏好可能与另一情境不同。在许多应用领域,一个与环境无关的表示可能会失去预测能力,因为来自多个情境的有用信息被汇总。
更正式地说,用户与项目之间的交互表现出多方面的性质。用户偏好通常不是固定的,并且可能随着特定情况而变化。回到 Reco4.me 应用的例子,可能的上下文信息的简化模式在图 7.1 中展示。

图 7.1 Reco4.me 应用的环境信息
这个例子是可能考虑的环境信息类型的一个小子集。环境可能包括一年中的季节或星期几,用户正在使用的电子设备类型,用户的心情——几乎任何东西[Bazire 和 Brézillon, 2005; Doerfel 等,2016]。也值得提到的是,环境信息是由系统对特定条件下发生动作或交互的具体情况所知道或可以推测的内容来定义的。
在基于内容和协同过滤方法中,推荐问题被定义为预测问题,其中,给定一个用户配置文件(以不同的方式定义)和一个目标项目,推荐系统的任务是预测用户对该项目的评分或兴趣,反映用户对项目的偏好程度。具体来说,推荐系统试图估计一个评分函数:
f: 用户 × 项目 → 评分
这样的函数将用户-项目对映射到一个有序的分数值集合。请注意,f 可以被视为用户-项目对的一般用途(或偏好)度量。所有用户-项目对的评分都是未知的,因此必须推断出来,这就是我们为什么谈论预测的原因。当收集到一组初始评分(隐式或显式地)后,推荐系统试图估计尚未被用户评分的项目评分值。从现在起,我们将这些传统的推荐系统称为二维(2D),因为它们在推荐过程中只考虑用户和项目维度作为输入。
与此相反,上下文感知推荐系统试图结合或使用额外的环境证据(超出用户和项目信息)来估计用户对未见项目的偏好。当此类上下文证据可以作为推荐系统输入的一部分时,评分函数可以被视为多维的。在这个公式中,上下文代表一组进一步界定用户-项目对被分配特定评分条件的因素:
f: 用户 × 项目 × 上下文[1] × 上下文[2] × … × 上下文[n] → 评分
这个扩展模型的潜在假设是,用户对项目的偏好不仅取决于项目本身,还取决于考虑项目时的上下文。
上下文信息代表一组显式变量,它们在底层领域(时间、位置、环境、设备、场合等)中建模上下文因素。无论上下文如何表示,上下文感知推荐者必须能够获取与用户活动(如购买或评分项目)相对应的上下文信息。在这种上下文感知推荐系统中,此类信息有两个方面的目的:
-
这是学习和建模过程的一部分(例如,用于发现规则、细分用户或构建回归模型)。
-
对于给定的目标用户和目标项目,系统必须能够识别特定上下文变量的值,作为用户与系统持续互动的一部分。这些信息用于确保在考虑上下文的情况下提供正确的推荐。
上下文信息可以通过多种方式获得,无论是显式还是隐式。显式上下文信息可能来自用户本身或来自设计用于测量特定物理或环境信息的传感器 [Frolov and Oseledets, 2016]。然而,在某些情况下,上下文信息必须从其他观察数据中推导或推断出来。以下是一些例子:
-
显式—应用程序可能要求寻找餐厅推荐的人指定他们是否在约会或与同事一起参加商务晚餐。
-
显式/隐式—如果餐厅推荐器是一个移动应用,可以通过设备的 GPS 和其他传感器获得关于位置、时间和天气条件等额外上下文信息。
-
隐式—一个电子商务系统可能尝试使用之前学习到的用户行为模型来区分(例如)用户是否可能为其配偶购买礼物或与工作相关的书籍。
隐式推断上下文信息的方法通常需要从历史数据中构建预测模型 [Palmisano et al., 2008]。
图 7.2 展示了上下文感知推荐引擎的心理模型。用户的动作——系统的输入——被上下文化并转换为图;然后可以开始构建模型和提供推荐的过程。

图 7.2 由图驱动的上下文感知推荐系统
7.1.1 表示上下文信息
在基于内容和协同过滤的方法中,用户-项目交互(购买、点击、查看、评分、观看等)被表示为一个二维矩阵,我们将其定义为用户 x 项目(U x I)数据集。这样的矩阵可以很容易地表示为一个二分图,其中一个顶点集代表用户,另一个顶点集代表项目。交互通过事件的用户(事件的主题)和项目(事件的客体)之间的关系来建模。
在上下文感知推荐系统中,每个交互事件都带来了更多的信息。它不仅由用户和项目描述,还包括所有环境信息,这些信息上下文化了特定行为。如果一个用户在晚上与孩子们在家看电影,上下文信息包括
-
时间—晚上,工作日
-
公司—儿童
-
位置—家
这个例子仅是描述“观看”这一事件的有关信息的子集。其他信息可能包括使用的设备、用户的情绪、观看者的年龄或场合(约会之夜、派对或孩子的睡前电影)。一些变量可能是离散的(具有定义好的值集的上下文信息,如设备和位置),而其他变量是连续的(如年龄这样的数值)。在后一种情况下,通常最好以某种方式对变量进行离散化。在年龄的情况下,你可能会有不同的年龄段——例如 0-5 岁、6-14 岁、15-21 岁、22-50 岁以及 50 岁以上——这取决于推荐引擎的具体要求。
代表推荐过程输入的结果数据集不能再表示为一个简单的二维矩阵。它需要一个 N 维矩阵,其中两个维度是用户和项目,其余维度代表上下文。在考虑的例子中,数据集将是一个五维矩阵:
dataset = 用户 × 项目 × 位置 × 公司 × 时间
每个交互或事件不能仅通过两个元素及其关系来简单描述。在最佳情况下,当所有上下文信息都可用时,还需要另外三个元素,因此我们不能使用简单的二元关系在二分图中表示一个事件。要表示五个顶点之间的关系,我们需要一个超图。在数学中,超图是图的推广,其中一条边可以连接任意数量的顶点。然而,在大多数图数据库(包括 Neo4j)中,无法表示 n 顶点的关系。
解决方案是将事件实体化为节点,并将每个事件节点与描述该事件的全部元素或维度相连接。结果将类似于图 7.3。

图 7.3 表示事件上下信息的 n 分图
由于我们拥有用户、项目、位置信息、时间信息和公司信息以及事件,新的图表示是一个 6 分图。此图表示了推荐过程中下一步的输入,如图 7.2 所示。
从二维表示过渡到 n 维表示(在我们的例子中 n=5)使得数据稀疏性成为一个更大的问题。很难找到在完全相同情况下发生的大量事件。当我们拥有详细上下文信息(n 的值更高)时,这个问题会加剧,但可以通过在上下文信息中引入层次结构来缓解。图 7.4 展示了考虑我们特定场景的一些上下文信息的可能层次结构示例——以图的形式表示。这些层次结构被定义为分类法。

图 7.4 用户、项目和时间的分类法
这些分类法将在推荐阶段使用,以解决稀疏性问题,并使我们能够在没有太多关于当前用户特定上下文信息的情况下提供推荐。
在本节中,我们将使用 DePaulMovie 数据集¹ [Zheng et al., 2015],该数据集包含从学生调查中收集的数据。它包含关于 97 个用户和 79 部电影的评分数据,这些评分在不同的上下文中进行(时间、地点和同伴)。这样的数据集完美符合我们的需求,并且经常用于进行上下文感知推荐系统的比较 [Ilarri et al., 2018]。
首先,让我们导入为这个示例选择的数据集 DePaulMovie 的数据。请使用一个全新的数据库运行代码;你可以清理它² 或者决定使用不同的数据库,并保留你在前几章中创建的数据库以进行进一步实验。
列表 7.1 从 DePaulMovie 数据集导入数据
def import_event_data(self, file): ❶
with self._driver.session() as session:
self.executeNoException(session, "CREATE CONSTRAINT ON (u:User)
➥ ASSERT u.userId IS UNIQUE") #B
self.executeNoException(session, "CREATE CONSTRAINT ON (i:Item)
➥ ASSERT i.itemId IS UNIQUE") #B
self.executeNoException(session, "CREATE CONSTRAINT ON (t:Time)
➥ ASSERT t.value IS UNIQUE") #B
self.executeNoException(session, "CREATE CONSTRAINT ON
➥ (l:Location) ASSERT l.value IS UNIQUE") ❷
self.executeNoException(session, "CREATE CONSTRAINT ON
➥ (c:Companion) ASSERT c.value IS UNIQUE") ❷
j = 0;
with open(file, 'r+') as in_file:
reader = csv.reader(in_file, delimiter=',')
next(reader, None)
tx = session.begin_transaction()
i = 0;
query = """ ❸
MERGE (user:User {userId: $userId})
MERGE (time:Time {value: $time})
MERGE (location:Location {value: $location})
MERGE (companion:Companion {value: $companion})
MERGE (item:Item {itemId: $itemId})
CREATE (event:Event {rating:$rating})
CREATE (event)-[:EVENT_USER]->(user)
CREATE (event)-[:EVENT_ITEM]->(item)
CREATE (event)-[:EVENT_LOCATION]->(location)
CREATE (event)-[:EVENT_COMPANION]->(companion)
CREATE (event)-[:EVENT_TIME]->(time)
"""
for row in reader:
try:
if row:
user_id = row[0]
item_id = strip(row[1])
rating = strip(row[2])
time = strip(row[3])
location = strip(row[4])
companion = strip(row[5])
tx.run(query, {"userId": user_id, "time": time,
➥ "location": location, "companion": companion,
➥ "itemId": item_id, "rating": rating})
i += 1
j += 1
if i == 1000:
tx.commit()
print(j, "lines processed")
i = 0
tx = session.begin_transaction()
except Exception as e:
print(e, row)
tx.commit()
print(j, "lines processed")
print(j, "lines processed")
❶ 从 CSV 文件导入数据的入口点
❷ 创建数据库中的约束以防止重复并加快访问速度的查询
❸ 一次性创建事件并将它们与相关维度连接的查询
在代码仓库的完整版本中,你会注意到我还导入了一些关于电影的信息。这些信息将有助于了解结果,也适用于以下练习。
练习
在导入数据后,尝试操作图数据库。以下是一些可以尝试的事情:
-
寻找最频繁的上下文信息——例如,观看电影的最频繁时间。
-
寻找最活跃的用户,并检查他们上下文信息的变异性。
-
尝试添加一些分类法,看看先前查询的结果是否会有所改变。
-
搜索在周内普遍观看的电影或类型,以及周末更常观看的电影。
7.1.2 提供推荐
经典推荐系统通过使用对用户偏好的有限了解(即用户对某些项目子集的偏好)来提供推荐,这些系统的输入数据通常是基于形式为 <用户,项目,评分> 的记录。如前几章所述,推荐过程通常使用 U x I 矩阵来创建模型,并仅基于用户交互和偏好提供推荐。
相比之下,上下文感知推荐系统通常处理形式为 <用户, 项目, 上下文 1, 上下文 2, ..., 评分> 的数据记录,其中每个记录不仅包括特定用户对某个项目的喜好程度,还包括用户与项目交互时所处的条件上下文信息(context1 = 星期六,context2 = 妻子,等等)。这种“丰富”的信息被用于构建模型。此外,用户当前上下文的信息可以在推荐过程的各个阶段被使用,从而产生几种上下文感知推荐系统的方法。从算法的角度来看,绝大多数上下文感知推荐方法都执行以下操作:
-
以形式 U × I × C[1] × C[2] × ... × C[n] 的上下文化(扩展)用户 × 项目数据集作为输入,其中 C[i] 是一个额外的上下文维度。
-
为每个用户 u 根据用户的当前上下文生成一个上下文推荐列表 i[1],i[2],i[3],...
根据上下文信息、当前用户和当前项目在推荐过程中的使用情况,上下文感知推荐系统可以采取图 7.5 中所示的三种形式之一。

图 7.5 上下文感知推荐系统的三种形式
三种类型的上下文感知推荐系统是 [Ilarri et al., 2018]
-
上下文预过滤(或推荐输入的上下文化)—在这个范例中,当前上下文 c 的信息仅用于选择相关数据集,并且通过在所选数据上使用任何传统的二维推荐系统来预测评分。为了提高效率,必须预先计算几个模型,考虑到上下文最可能的组合。
-
上下文后过滤(或推荐输出的上下文化)—在这个范例中,上下文信息最初被忽略,并且使用任何传统的二维推荐系统在整个数据集上预测评分。然后,使用上下文信息调整(上下文化)生成的推荐集,针对每个用户。只构建了一个模型,因此更容易管理,并且上下文信息仅在推荐阶段被使用。
-
上下文建模(或推荐函数的上下文化)—在这个范例中,上下文信息直接作为模型构建的一部分被用于建模技术。
以下几节将更详细地描述这三种范例,并突出每种范例中图方法的作用(尤其是前两种)。
上下文预过滤
如图 7.6 所示,上下文预过滤方法使用上下文信息来选择最相关的用户 × 项目矩阵,并从这些矩阵中创建模型;然后通过推断的模型生成推荐。

图 7.6 上下文预过滤
当提取用户 × 项目数据集时,可以使用文献中提出的任何众多传统推荐技术(如第四章和第五章中讨论的方法)来构建模型并提供推荐。这种技术代表了上下文感知推荐引擎第一种方法的最大优势之一。
注意,预过滤方法与在机器学习和数据挖掘中基于最相关上下文信息组合构建多个局部模型的任务相关。而不是使用所有可用的评级来构建全局评级估计模型,预过滤方法(在实际场景中是预先构建的)构建了一个局部评级估计模型,该模型仅使用与用户指定的推荐标准(如周六或工作日)相关的评级。
在这种方法中,上下文 c 实际上充当了一个过滤器,用于选择相关的评级数据。以下是一个电影推荐系统上下文数据过滤器的示例:如果某人想在周六看电影,则只使用周六的评级数据来推荐电影。提取相关数据集、构建模型和提供推荐当然需要时间,尤其是如果数据集很大。因此,预先计算了多个版本,使用最相关的上下文信息组合。
在图方法中,考虑图 7.3 中所示模型,执行此类预过滤包括通过运行如下查询来选择相关事件。
列表 7.2 基于相关上下文信息过滤事件
MATCH (event:Event)-[:EVENT_ITEM]->(item:Item)
MATCH (event)-[:EVENT_USER]->(user:User)
MATCH (event)-[:EVENT_TIME]->(time:Time)
MATCH (event)-[:EVENT_LOCATION]->(location:Location)
MATCH (event)-[:EVENT_COMPANION]->(companion:Companion)
WHERE time.value = "Weekday"
AND location.value = "Home"
AND companion.value = "Alone"
RETURN user.userId, item.itemId, event.rating
在这个查询中,我们只考虑在家庭中仅在工作日发生的事件。输出是我们多维矩阵的一个切片。如果我们想获取 <周末,电影院,伙伴> 的用户 × 项目矩阵,查询将如下所示。
列表 7.3 基于不同上下文信息过滤事件
MATCH (event:Event)-[:EVENT_ITEM]->(item:Item)
MATCH (event)-[:EVENT_USER]->(user:User)
MATCH (event)-[:EVENT_TIME]->(time:Time)
MATCH (event)-[:EVENT_LOCATION]->(location:Location)
MATCH (event)-[:EVENT_COMPANION]->(companion:Companion)
WHERE time.value = "Weekend"
AND location.value = "Cinema"
AND companion.value = "Partner"
RETURN user.userId, item.itemId, event.rating
结果矩阵将不同。
当然,没有必要指定所有上下文信息。某些维度可以被忽略。例如,我们可能有一个包含 <电影院,伙伴> 的上下文,其中时间维度可能是不相关的。在这种情况下,查询看起来如下。
列表 7.4 仅考虑两个上下文信息项过滤事件
MATCH (event:Event)-[:EVENT_ITEM]->(item:Item)
MATCH (event)-[:EVENT_USER]->(user:User)
MATCH (event)-[:EVENT_LOCATION]->(location:Location)
MATCH (event)-[:EVENT_COMPANION]->(companion:Companion)
WHERE location.value = "Cinema"
AND companion.value = "Partner"
RETURN user.userId, item.itemId, event.rating
图模型非常灵活。如前所述,数据过滤后,任何经典方法都可以应用于构建模型并提供推荐。假设我们想使用协同方法——具体来说,是最近邻方法。我们必须计算项目、用户或两者的相似度。结果相似度可以存储为项目与/或用户之间简单的关系,但预过滤条件的信息将会丢失。可以在关系中添加一个属性来跟踪用于计算它们的信息来源,但这很难查询;更重要的是,这种方法没有使用图的能力来加速通过节点和关系的导航。
在这种情况下,最佳建模选择是使用节点来具体化相似度,并将它们连接到用于计算它们的相应上下文信息:预过滤条件。结果图模型将类似于图 7.7。

图 7.7 计算后相似节点图模型
在推荐过程中,此模型易于导航。我们可以为每一组上下文信息分配一个 ID,以便查询更简单;这个 ID 不是强制的,但它很有帮助,因为它允许更快、更简单的访问。我们可以通过以下查询获取特定上下文的 k-NN。³
列表 7.5 获取特定上下文信息的 k-NN 的查询
MATCH p=(n:Similarity)-->(i)
WHERE n.contextId = 1 ❶
RETURN p
limit 50
❶ 将 ID 分配给特定的上下文信息集合,使我们能够通过上下文 ID 进行查询。
以下列表允许您创建这样的图模型。
列表 7.6 预过滤方法中计算和存储相似度的代码
def compute_and_store_similarity(self, contexts): ❶
for context in contexts:
items_VSM = self.get_item_vectors(context)
for item in items_VSM:
knn = self.compute_knn(item, items_VSM.copy(), 20); ❷
self.store_knn(item, knn, context)
def get_item_vectors(self, context): ❸
list_of_items_query = """
MATCH (item:Item)
RETURN item.itemId as itemId
"""
context_info = context[1].copy()
match_query = """
MATCH (event:Event)-[:EVENT_ITEM]->(item:Item)
MATCH (event)-[:EVENT_USER]->(user:User)
"""
where_query = """
WHERE item.itemId = $itemId
"""
if "location" in context_info: ❹
match_query += "MATCH (event)-[:EVENT_LOCATION]->(location:Location) "
where_query += "AND location.value = $location "
if "time" in context_info:
match_query += "MATCH (event)-[:EVENT_TIME]->(time:Time) "
where_query += "AND time.value = $time "
if "companion" in context_info:
match_query += "MATCH (event)-[:EVENT_COMPANION]->
➥ (companion:Companion) "
where_query += "AND companion.value = $companion "
return_query = """
WITH user.userId as userId, event.rating as rating
ORDER BY id(user)
RETURN collect(distinct userId) as vector
"""
query = match_query + where_query + return_query
items_VSM_sparse = {}
with self._driver.session() as session:
i = 0
for item in session.run(list_of_items_query):
item_id = item["itemId"];
context_info["itemId"] = item_id
vector = session.run(query, context_info)
items_VSM_sparse[item_id] = vector.single()[0]
i += 1
if i % 100 == 0:
print(i, "rows processed")
print(i, "rows processed")
print(len(items_VSM_sparse))
return items_VSM_sparse
def store_knn(self, item, knn, context):
context_id = context[0]
params = context[1].copy()
with self._driver.session() as session:
tx = session.begin_transaction()
knnMap = {a: b for a, b in knn}
clean_query = """ ❺
MATCH (s:Similarity)-[:RELATED_TO_SOURCE_ITEM]->(item:Item)
WHERE item.itemId = $itemId AND s.contextId = $contextId
DETACH DELETE s
"""
query = """ ❻
MATCH (item:Item)
WHERE item.itemId = $itemId
UNWIND keys($knn) as otherItemId
MATCH (other:Item)
WHERE other.itemId = otherItemId
CREATE (similarity:Similarity {weight: $knn[otherItemId],
➥ contextId: $contextId})
MERGE (item)<-[:RELATED_TO_SOURCE_ITEM]-(similarity)
MERGE (other)<-[:RELATED_TO_DEST_ITEM ]-(similarity)
"""
if "location" in params: ❼
query += "WITH similarity MATCH (location:Location
➥ {value: $location}) "
query += "MERGE (location)<-[:RELATED_TO]-(similarity) "
if "time" in params:
query += "WITH similarity MATCH (time:Time {value: $time}) "
query += "MERGE (time)<-[:RELATED_TO]-(similarity) "
if "companion" in params:
query += "WITH similarity MATCH (companion:Companion
➥ {value: $companion}) "
query += "MERGE (companion)<-[:RELATED_TO]-(similarity) "
tx.run(clean_query, {"itemId": item, "contextId": context_id})
params["itemId"] = item
params["contextId"] = context_id
params["knn"] = knnMap
tx.run(query, params)
tx.commit()
def compute_knn(self, item, items, k):
knn_values = []
for other_item in items:
if other_item != item:
value = cosine_similarity(items[item], items[other_item])
if value > 0:
knn_values.append((other_item, value))
knn_values.sort(key=lambda x: -x[1])
return knn_values[:k]
❶ 预过滤中计算相似度的入口点。上下文参数指定上下文信息。此函数必须多次运行,以处理多个上下文信息的组合。
❷ 计算相似度。余弦函数与第 4、5、6 章多次使用的是同一个。
❸ 根据相关上下文信息预过滤数据集,并返回带有相关稀疏向量的常规项目列表
❹ 根据上下文信息更改查询的 if 语句
❺ 清理先前存储模型的查询
❻ 创建新的相似度节点并将它们连接到相关项目和上下文的查询
❼ 根据过滤条件修改查询的 if 语句
如前所述,确切上下文可能过于狭窄。例如,考虑周六与伴侣在电影院看电影的情况——或者更正式地,c = <Partner, Cinema, Saturday>。使用这个确切上下文作为数据过滤查询可能有问题,因为可能没有足够的数据来进行准确的评分预测。为了解决这个问题,Adomavicius 和 Tuzhilin [2005]建议通过聚合较窄的上下文细节来泛化过滤条件,这些细节可能并不重要。这些泛化是我们之前讨论的分类法,其中一些例子在图 7.4 中展示过。例如,周六可以成为周末,而周一到周五被认为是工作日。不仅容易在图中表示这样的层次结构或聚合,而且还可以查询它们。在过滤数据时使用更广泛的概念可以提供更好的结果。
当考虑预滤波方法时,重要的是确定它生成的局部(特定于某些上下文信息)模型是否优于传统的 2D 技术的全局模型,该模型忽略了与上下文维度相关的所有信息。在这种情况下,可能最好使用上下文预滤波来推荐周末在电影院观看的电影,而使用传统的 2D 技术(忽略上下文信息)来推荐按需在家观看的电影。在这种情况下计算未知评分时的权衡是在以下方面:
-
使用更具体(在上下文信息较窄的意义上)但相关的数据(预滤波)
-
使用所有可用的数据(传统的 2D 推荐)
没有简单的规则可以帮助我们在这两种计算之间做出选择;哪种方法更成功取决于许多因素,例如上下文信息类型、应用领域、用户行为以及可用数据的数量和稀疏性。因此,预滤波推荐方法在某些上下文中可能优于传统的 2D 推荐技术,但在其他上下文中则不然。基于这一观察,Adomavicius 和 Tuzhilin [2005]提出,在没有进行过滤的情况下,将上下文预滤波与传统的 2D 技术相结合。
上下文后滤波
如图 7.8 所示,上下文后滤波方法在模型生成过程中忽略了上下文信息。

图 7.8 上下文后滤波
此外,无论上下文如何,都会计算所有候选项目的排序列表。后滤波方法在后续阶段使用上下文信息来调整每个用户的推荐列表。对前 N 个项目的调整可以有两种方式:
-
过滤掉给定上下文中不相关的推荐
-
调整列表中推荐的排序
在我们的电影推荐应用 Reco4.me 中,如果用户在周末只看喜剧,推荐系统可能会过滤掉周末观看推荐列表中的所有非喜剧,或者通过降低它们的评分来惩罚它们。
哪种方法更可取将取决于应用。Panniello 等人[2009]对精确(即非泛化)预过滤方法与他们称为 Weight 和 Filter 的后过滤方法进行了实验比较,使用了几个现实世界的电子商务数据集。他们的结果显示,Weight 后过滤方法优于精确预过滤方法,而后者又优于 Filter 方法。然而,根据您的应用,结果可能会有所不同。
过滤或调整排名的方法可以分为基于启发式或基于模型。启发式后过滤方法侧重于在给定上下文中(例如,在电影院观看周六电影时喜欢的演员)为给定用户找到共同的项目特征(属性),然后使用这些属性来调整推荐。这种方法需要存储每个项目的元数据并搜索用户偏好的共同模式。
在图模型中,表示项目元数据是直接的,前几章(特别是对于基于内容的策略)已经介绍了多种建模技术。将这些模型与用户×项目×上下文图表示混合是一个简单的练习;图 7.9 展示了可能的结果。

图 7.9 表示事件上下文信息以及项目属性的 n 分图
DePaulMovie 数据集包含每部电影的 IMDb ID 引用,因此我们可以重用第四章中实现的代码来从 IMDb 获取和添加信息。该代码在本书的仓库中的文件 import_depaulmovie.py 中展示。
导入后,可以使用以下查询来根据上下文信息计算用户的共同点。请注意,这里显示的查询专注于特定用户以证明概念。查询仅考虑所有可能组合中的两个上下文:<Cinema, Partner>和<Home, Alone>。我们将从<Cinema, Partner>上下文(列表 7.7)开始。
列表 7.7 查询获取上下文<Cinema, Partner>的用户资料
MATCH (user:User)<-[:EVENT_USER]-(event:Event)
MATCH (event)-[:EVENT_ITEM]->(item:Item)-[]-(feature:Feature)
MATCH (event)-[:EVENT_LOCATION]->(location:Location)
MATCH (event)-[:EVENT_COMPANION]->(companion:Companion)
WHERE user.userId = "1032"
AND location.value = "Cinema"
AND companion.value = "Partner"
RETURN CASE 'Genre' IN labels(feature)
WHEN true THEN feature.genre
ELSE feature.name END AS feature, count(event) as occurrence
ORDER BY occurrence desc
列表 7.7 的结果显示在图 7.10 中。

图 7.10 列表 7.7 的结果
从结果来看,当这位用户与伴侣在电影院看电影时,用户更喜欢喜剧、浪漫、戏剧和动作电影。喜欢的演员/导演遵循相同的逻辑。现在让我们看看<Home, Alone>上下文(列表 7.8)。
列表 7.8 查询获取上下文<Home, Alone>的用户偏好/资料
MATCH (user:User)<-[:EVENT_USER]-(event:Event)
MATCH (event)-[:EVENT_ITEM]->(item:Item)-[]-(feature:Feature)
MATCH (event)-[:EVENT_LOCATION]->(location:Location)
MATCH (event)-[:EVENT_COMPANION]->(companion:Companion)
WHERE user.userId = "1032"
AND location.value = "Home"
AND companion.value = "Alone"
RETURN CASE 'Genre' IN labels(feature)
WHEN true THEN feature.genre
ELSE feature.name END AS feature, count(event) as occurrence
ORDER BY occurrence desc
本查询的结果显示在图 7.11 中。

图 7.11 列表 7.8 的结果
这个用户和之前的是同一个吗?这里的结果不同,显示了上下文在用户偏好中扮演的角色。以这种方式获得的结果可以用于后过滤或微调传统协同过滤方法的结果。基于上下文的偏好可以预先计算并存储回我们的图模型中。结果将类似于图 7.12。

图 7.12 上下文化用户偏好的图模型
图 7.12 中模型中展示的节点和关系类型可以通过运行以下查询来创建。
列表 7.9 创建用户偏好的查询
MERGE (userPreference:UserPreference {userId: "1032", location:"Home",
➥ companion: "Alone"})
WITH userPreference
MATCH (user:User)<-[:EVENT_USER]-(event:Event)
MATCH (event)-[:EVENT_LOCATION]->(location:Location)
MATCH (event)-[:EVENT_COMPANION]->(companion:Companion)
WHERE user.userId = userPreference.userId
AND location.value = userPreference.location
AND companion.value = userPreference.companion
WITH userPreference, user, collect(distinct event) as events
MERGE (userPreference)<-[:HAS_PREFERENCE]-(user)
WITH userPreference, events, size(events) as size
UNWIND events as event
MATCH (event)-[:EVENT_ITEM]->(item:Item)-[]-(feature:Feature)
WITH feature, userPreference, 1.0f*count(event)/(1.0f*size) as
➥ preferenceValue
MERGE (userPreference)-[:RELATED_TO {value: preferenceValue}]->(feature)
值得注意的是,此查询使用属性来表示用户偏好的上下文信息。这个例子与图 7.12 中所示的模型设计略有不同,但它是一个有效的选项。在接下来的练习中,你将被邀请创建一个与模型完全匹配的等效查询。
练习
以列表 7.9 为基础,创建以下查询:
-
不同上下文下的相同查询
-
使用关系代替属性来指定上下文的等效查询
-
仅针对演员的查询
-
仅针对导演的查询
-
仅针对编剧的查询
-
仅针对类型的查询
在推荐过程中,我们可以使用有关用户偏好的信息来确定如何调整第一个方法获得的结果。获取此信息的查询很简单,如列表 7.10 所示。
列表 7.10 获取特征提升因素的查询
MATCH (user:User)-[:HAS_PREFERENCE]->(userPreference:UserPreference)-
➥ [r:RELATED_TO]->(feature:Feature)
WHERE user.userId = "1032"
AND userPreference.location = "Home"
AND userPreference.companion = "Alone"
RETURN CASE 'Genre' IN labels(feature)
WHEN true THEN feature.genre
ELSE feature.name END AS feature, r.value
此查询返回的值可以用作在经典方法中获取第一个通用推荐列表之后作为提升因素的值。与后过滤的启发式方法相比,基于模型的方法是另一种选择。在这里,我们构建预测模型,这些模型计算用户在特定情境下选择某种类型项目的概率(例如,独自在家时选择特定类型的电影的可能性),然后使用这个概率来调整推荐。计算概率的算法超出了本章的范围,但一旦计算出来,它们可以像图 7.12 中所示的那样存储在图模型中。
需要注意的是,与上下文预过滤的情况一样,上下文后过滤方法的最大优势是它允许使用任何传统的推荐技术。
上下文建模
第三种上下文感知推荐系统是基于上下文建模的。这种方法如图 7.13 所示,在模型创建过程中直接使用上下文信息,从而产生了真正多维的推荐函数,这些函数代表预测模型(如决策树和回归)或结合上下文信息(除用户和项目数据外)的启发式计算。

图 7.13 上下文建模
在过去几年中,基于各种启发式方法和预测建模技术的推荐算法大量开发。其中一些技术可以被认为是将二维扩展到多维推荐设置中的扩展。Frolov 和 Oseledets[2016]展示了如何将用户 × 项目 × 上下文数据集表示为多维矩阵或张量。⁴这样一个张量可以如图 7.14 所示表示,其中每个事件代表一个元素,上下文、用户和项目代表维度。在这种表示中,对张量的某些操作,如切片,可以通过简单的查询轻松完成。

图 7.14 图模型中的张量表示(作为一个多部分图)
其他研究人员已经使用纯图方法来解决上下文建模的任务,将上下文感知推荐视为一个搜索问题,即在所谓的上下文图中找到对用户感兴趣的项目[Wu 等人,2015]。之前提出的创建图的方法在这种情况下将不起作用,因为模型设计不同。相反,图是按照以下方式创建的。给定上下文图 G = {V, E},顶点和边被定义为如下:
-
顶点集 V 被划分为几个不同的集合,例如用户集 U、项目集 I、属性集 A 和上下文集 C。C 代表节点中上下文信息的组合。《家庭,独自,工作日》是一个节点,例如。节点 A 代表用户或项目的静态特征或属性——与上下文信息不同,这些信息不会因不同的评分而改变。
-
边集 E 由笛卡尔积的现有连接组成:V × V。具有不同类型的边具有不同的语义。U × A 将用户及其属性(用户兴趣)连接起来,U × I 将用户与其交互过的项目连接起来(旧的 User × Item 数据集),U × C 将用户与上下文连接起来。存储社交网络信息的子矩阵 U × U 可能存在也可能不存在。
上下文图 G 可以表示为一个邻接矩阵,其中所有子矩阵都配置为对称的(例如,UI^T 是 UI 的转换矩阵),如表 7.1 所示。
表 7.1 用户-项目交互的邻接矩阵表示
| 用户 | 项目 | 上下文 | 属性 | |
|---|---|---|---|---|
| 用户 | UU | UI | UC | UA |
| 项目 | UIT | 0 | IC | IA |
| 上下文 | UCT | ICT | 0 | 0 |
| 属性 | UAT | IAT | 0 | 0 |
结果图显示在图 7.15 中。

图 7.15 上下文图的表示
为了避免过多的细节,使用随机游走方法(特别是个性化 PageRank 或带重启的 PageRank 算法)来计算图中节点的相关性。推荐过程使用这些相关性分数来估计未看到的项 i 被用户 u 访问的可能性。有关详细描述,请参阅 Wu 等人[2015]。
优缺点
对于所讨论的基于上下文的推荐的三种技术,每种都有其优势和劣势:
-
预过滤
-
-
优点—这种方法不仅易于实现,还允许你使用任何传统的推荐技术。如果用户当前上下文的相关数据可用,它可以提供相当准确的结果。
-
缺点—数据稀疏性问题在这里很常见,因为对于某些上下文,可能没有足够的数据来进行准确的推荐。此外,为了性能,这种方法要求你预先构建大量模型并保持它们全部更新。
-
-
后过滤
-
-
优点—这种方法甚至更容易实现:你使用传统的技术(如协同过滤)来生成推荐,然后对结果应用过滤器。
-
缺点—后过滤会过滤掉或减少与用户当前上下文不相关的元素的评分。预测准确性几乎与上下文无关,并且主要与传统方法一致。数据稀疏性在这里也是一个问题,正如传统方法中那样;可能所有生成的元素都与当前上下文无关。
-
-
上下文建模
-
-
优点—这一类的方法是最新的,往往也是最准确的。先前方法的主要缺点是上下文没有紧密集成到推荐算法中,这阻止了你充分利用各种用户-项目组合和上下文值之间的关系。上下文建模从一开始就考虑上下文,使得可以创建精确的模型,并使用用户、项目和上下文信息进行查询。
-
缺点—大多数可用的上下文建模方法实现起来都很复杂,创建和更新模型需要大量的计算能力。
-
技术的选择取决于权衡这些优缺点。更具体地说,选择取决于可用数据的类型和数量、新数据的频率以及模型应与当前数据多么紧密地一致。
7.1.3 图方法的优势
在本节中,我们讨论了创建上下文感知推荐引擎的不同方法:预过滤、后过滤和上下文建模。这里介绍的所有方法和算法都可以使用 User × Item × Contexts 数据集的图表示,这简化了访问和导航这些复杂数据。具体来说,基于图的方法在上下文感知推荐系统中的主要方面和优势包括
-
表示任何此类系统输入的 User × Item × Contexts 多维矩阵可以通过一个实体化交互事件的图来表示。这种数据模型加速了过滤阶段,并防止了数据稀疏性问题,这在当前场景中可能是个问题。
-
一个合适的图模型可以存储上下文预过滤的多模型结果。具体来说,在预过滤的最近邻方法中,这会导致项目或用户之间不同的相似性集合,图可以通过实体化相似性节点来存储多个模型的结果。
-
在推荐阶段,图访问模式简化了基于当前用户和当前上下文选择相关数据的过程。
-
在上下文建模方法中,图提供了一种适合存储张量、简化某些操作的方法。此外,一些特定的方法不仅使用数据(如前所述的上下文图)的图表示,还使用图算法,如随机游走和 PageRank 来构建模型并提供推荐。
7.2 混合推荐引擎
本书讨论的推荐方法利用不同的信息来源和遵循不同的范式来做出推荐。尽管它们基于接收者的假设兴趣产生个性化的结果,但在不同的应用领域中,它们的成功程度各不相同。协同过滤利用用户模型中的一种特定类型的信息(项目评分)来推导推荐,而基于内容的推荐方法则依赖于产品特征和文本描述以及用户配置文件。基于会话的方法使用匿名用户的点击流,而上下文感知方法则使用上下文信息以及项目评分来根据用户的当前需求微调推荐。
这些方法各有优缺点(在本章和前几章中详细说明),包括处理数据稀疏性和冷启动问题的能力,以及获取和处理内容或上下文所需的工作量。
图 7.16 概述了一个推荐系统作为黑盒,它将输入数据转换为输出项的排序列表。潜在的输入,基于这里讨论的方法,包括用户模型和上下文信息,以及会话数据和项目数据;其他输入,由其他推荐模型所需,也可以包括在内。然而,没有任何基本方法能够充分利用所有这些输入。因此,构建结合不同算法和模型优势的混合系统,以克服上述一些缺点和问题,已成为最近研究的目标。

图 7.16 混合推荐系统作为黑盒
混合推荐系统是结合多个算法或推荐组件的技术实现。Burke 的[2002]分类法区分了七种混合策略。从更一般的角度来看,这七个变体可以抽象为三种基本设计:
-
单体—这种混合化设计在一个算法实现中结合了多种推荐策略的方面。几个推荐者几乎贡献了虚拟的,因为混合使用了针对另一个推荐算法的特定输入数据,或者输入数据通过一种技术增强,实际上被另一种技术利用。特征组合和特征增强策略可以归为此类。特征组合使用多种多样的输入数据。它可以结合协作特征,如用户的喜好和不喜欢,与目录项目的特征内容。特征增强应用复杂的转换步骤。贡献的推荐系统通过预处理其知识源来增强实际推荐系统的特征空间。见图 7.17 a。
-
并行化—这种方法需要至少两个独立的推荐系统实现,随后将它们组合在一起(见图 7.17 b)。并行化混合推荐系统相互独立运行,并产生单独的推荐列表。在随后的混合化步骤中,它们的输出被组合成最终的推荐集。根据 Burke 的分类法,加权、混合和切换策略要求推荐组件并行工作。
-
流水线—在这种情况下,多个推荐系统以流水线架构(见图 7.17 c)连接在一起。一个推荐系统的输出成为后续一个推荐系统的输入。可选地,后续的推荐系统组件也可能使用原始输入数据的一部分。Burke 定义的级联和元级混合架构就是此类架构的例子。

图 7.17 混合设计技术
在本章中,我们将重点关注并行化混合技术,它允许多个推荐系统并行运行,每个系统使用自己的输入并产生自己的输出模型。结果模型必须存储在某个地方,以便在推荐阶段可以轻松访问和混合或合并。在这种情况下,图提供了
-
一种适合存储在单一、同质和连通的数据源中,以满足每个推荐系统所需的所有不同信息集的表示
-
一种用于存储训练过程结果的模型,以便可以并行查询,然后根据混合策略合并
7.2.1 多模型,单图
让我们更详细地看看并行化混合方法(图 7.18)。假设你有两种类型的推荐系统需要混合:一种是基于内容的,如第四章中描述的,另一种是基于协作的,如第五章中描述的。这种情况很常见:通常很有用将这两种类型的推荐系统合并,因为每种都可以解决另一种的问题。基于内容的方案可以缓解数据缺失时出现的冷启动问题,例如在新的用户、项目或新平台的情况下,而基于协作过滤的方法则提供更准确的结果,并且在没有用户和项目信息或元数据的情况下也能工作。

图 7.18 并行化方法
作为并行化混合推荐系统输入的图,该系统使用这两种类型的推荐器作为输入,看起来像图 7.19。

图 7.19 示例图模型,结合了协作过滤和基于内容的方案
在这种情况下,重要的是要注意两个推荐系统如何以不同的方式使用评分连接。在协作过滤方法中,它用于创建用户-项目数据集;在基于内容的方法中,它用于访问用户感兴趣的特征。当模型被计算后,它们可以存储回图中,如图 7.20 所示。

图 7.20 在同一图中混合多个推荐模型
7.2.2 提供推荐
现在我们已经构建了模型并将它们存储在图中,我们可以结合它们的输出以获得一个独特的推荐项目列表(有时是多个列表)以推荐给用户。如前所述,并行化混合设计并排使用多个推荐器,并使用特定的混合机制来聚合它们的输出。混合机制定义了向用户提供推荐的战略。根据 Burke 的[2002]分类,可以应用三种主要策略:混合、加权以及切换。对于多个推荐列表,如多数投票方案等附加组合策略也可能适用。
混合
混合混合策略在用户界面级别结合不同推荐系统的结果。不同技术的结果一起呈现;因此,用户 u 的推荐结果是一系列项目列表。
每个推荐器的得分最高的项目并排显示给用户,通常指定每个项目的标准。有时在混合方法中,需要某种类型的冲突解决来防止多个列表中重叠过多。
加权
一种加权混合策略通过计算两个或多个推荐系统得分的加权总和来结合它们的推荐。图 7.21 是一个图形模型,展示了其工作原理。

图 7.21 使用图形模型解释加权方法
因此,给定 n 个不同的推荐函数 scorek 及其相关的相对权重β[k],最终得分将根据以下公式计算

其中 n 是需要混合的推荐器输出的数量。此外,项目得分需要限制在所有推荐器中相同的范围内,并且所有β[k]的总和必须为 1。这种方法简单直接,因此是结合不同推荐技术预测能力的加权方式的一种流行策略。
值得注意的是,β[k]的值可以是动态的,在推荐系统的一生中发生变化,例如在早期由于缺乏足够的信息而无法有效应用协同过滤时,优先考虑基于内容的推荐方法,然后在收集到更多数据后逐渐增加其权重。此外,这些值可以针对每个用户动态变化,将较高的β[k]值分配给基于内容的推荐,直到系统收集到足够的数据,使得协同过滤方法变得有效。可以应用不同的技术来评估如何设置并演变权重的值。
切换
切换 混合推荐系统需要一个预言机来决定在特定情况下应使用哪个推荐器,这取决于用户配置文件和/或推荐结果的质量。图 7.22 是一个描述其工作原理的图形模型。

图 7.22 通过图形模型解释的切换方法
这样的评估可以按照以下方式进行,

其中 k 由切换条件确定。为了克服冷启动问题,基于内容和协作的切换混合推荐系统可以最初基于内容进行推荐,直到有足够的评分数据可用。当协同过滤组件可以提供具有足够置信度的推荐时,推荐策略可以切换。在极端情况下,可以实施动态权重调整作为切换混合。除了动态选择的单个推荐器外,所有其他推荐器的权重都设置为 0,而单个剩余推荐器的输出被分配一个权重为 1。
7.2.3 图方法的优势
在本节中,我们讨论了如何创建一个混合推荐引擎,重点关注并行化混合方法:混合、加权以及切换。这里提出的所有方法都可以利用数据图表示,无论是训练还是生成的模型。基于图混合方法的主要方面和优势是
-
各种信息集可以在相同的数据结构中共存,这使得更容易满足混合推荐系统的数据管理需求。
-
每个推荐器产生的独立模型可以一起存储,并在推荐阶段轻松访问。
摘要
本章介绍了使用上下文信息实现推荐引擎的最新高级技术,并展示了如何结合不同的方法以产生更大的效果。各种数据模型说明了图在满足不同训练数据和模型存储需求方面的有用性和灵活性。在本章中,你学习了
-
如何通过在模型和相关图模型中嵌入上下文信息来提高推荐质量
-
如何使用图来为上下文感知设计方法提供数据:预/后过滤和上下文建模
-
如何在一个推荐引擎中结合多个算法
-
如何在一个大图中混合不同的训练数据集和模型
参考文献
[Adomavicius and Tuzhilin, 2005] Adomavicius, Gediminus, 和 Alexander Tuzhilin. “迈向下一代推荐系统:对现有技术和可能扩展的综述。” IEEE 知识和数据工程杂志 17(6): 734-749.
[巴齐尔和布雷松,2005] 巴齐尔,玛丽,帕特里克·布雷松。 “在使用上下文之前理解上下文。” 第 5 届国际和跨学科建模与使用上下文会议论文集 (2005): 29-40。
[伯克,2002] 伯克,罗宾。 “混合推荐系统:调查和实验。” 用户建模与用户自适应交互 12:4 (2002): 331-370。
[多费尔等,2016] 多费尔,斯蒂芬,罗伯特·雅施克,格德·斯特姆。 “在社交书签系统中推荐基准测试中核心的作用。” ACM 智能系统与技术研究 7:3 (2016): 文章 40。
[弗罗洛夫和奥谢列茨,2016] 弗罗洛夫,叶夫根尼,伊万·奥谢列茨。 “张量方法和推荐系统。” arXiv 预印本 arXiv:1603.06038 (2016)。
[伊拉里等,2018] 伊拉里,塞戈里奥,拉奎尔·特里略-拉多,拉蒙·赫尔莫索。 “面向上下文感知推荐系统的数据集:当前上下文和可能的方向。” IEEE 第 34 届国际数据工程研讨会论文集 (2018)。
[帕尔米萨诺等,2008] 帕尔米萨诺,科西莫,亚历山大·图齐林,米歇尔·戈戈利奥内。 “在个性化应用中使用上下文来改进客户预测建模。” 知识数据工程杂志 20:11 (2008): 1535-1549。
[帕尼内洛等,2009] 帕尼内洛,翁贝托,亚历山大·图齐林,米歇尔·戈戈利奥内,科西莫·帕尔米萨诺,安托·佩多内。 “在上下文感知推荐系统中,预先过滤和后过滤方法的实验比较。” 第 3 届 ACM 推荐系统会议论文集 (2009): 265-268。
[萨奇曼,1987] 萨奇曼,露西。 计划和情境行动。 英国剑桥:剑桥大学出版社,1987。
[吴等,2015] 吴,浩,岳坤,刘晓欣,裴一建,李波。 “基于图上下文建模和后过滤的上下文感知推荐。” 国际分布式传感器网络杂志 - 关于大数据和知识提取的网络安全物理系统特刊 (2015): 文章 16。
[郑等,2015] 郑勇,班沙德·莫巴舍,罗宾·伯克。 “CARSKit:一个基于 Java 的上下文感知推荐引擎。” 第 15 届 IEEE 国际数据挖掘会议(ICDM)研讨会论文集 (2015): 1668-1671。
^(1.)mng.bz/D1jE.
^(2.)为了清理现有的数据库,你可以运行 MATCH (n) DETACH DELETE n,但这可能需要更长的时间。另一个选择是停止数据库并清除数据目录。
^(3.)在代码完成创建 k-NN 之后可以运行查询。在这里,目的是展示如何查询模型。
^(4.)一个 矩阵 是一个二维数字网格。一个 张量 是矩阵概念的推广,可以具有任意数量的维度:0(一个单独的数字)、1(一个向量)、2(一个传统矩阵)、3(一个数字立方体)或更多。这些高维结构难以可视化。张量的维度被称为其 秩(也称为 阶 或 度)。
第三部分 打击欺诈
欺诈与人类历史一样悠久,可以采取无数种形式。根据普华永道(PwC)2020 年全球经济犯罪和欺诈调查(mng.bz/l2ny),全球 47%的组织都曾遭受欺诈(而剩余的 53%中许多可能并未意识到,因此这个数字几乎肯定更高),总估计损失为 420 亿美元。欧洲央行报告(mng.bz/BK8J)称,每年欺诈卡交易的总价值达到 18 亿欧元(20 亿美元)。
打击欺诈,以及更普遍地检测数据中的异常,是一项至关重要的任务,它在多个领域,如金融、安全、医疗保健和执法等领域产生了巨大影响。最近,它引起了机器学习从业者的广泛关注。在此之前,大多数领域的公司使用基于人类和固定规则的分析混合方法,而欺诈检测正变得越来越自动化,机器学习在其中扮演着关键角色。
书的第二部分专注于一个特定的机器学习任务:推荐。此类任务的主要目标是收集关于用户偏好的数据,这些偏好可能是隐式或显式表达的;创建一个或多个模型;并执行预测以增加用户满意度和商业收入。正如我们所见,有各种技术和方法可用,但它们都有一个共同的目标——即向用户推荐可能感兴趣的东西。这种对最终用户的关注为推荐平台的建设以及相关基础设施的定义,以及算法的类型带来了一些限制。考虑因素,如考虑实时、最新的偏好、预测准确性和减少对用户体验的影响,推动了 CRISP-DM 过程模型中每个步骤的决策过程。我们已经探索了许多这些限制,特别是发现图在解决此类任务的一些主要挑战中发挥着关键作用。
打击欺诈从不同的角度开始。它有不同的目标,分析采取不同的方法。打击欺诈的分析平台与推荐平台之间的主要区别之一与最终利益相关者有关。在推荐用例中,目标是最终用户:那些在零售网站或酒店预订网站上导航的人。在打击欺诈用例中,真正的利益相关者是公司的分析师,他们必须以最快的方式发现问题并识别模式以防止同样的欺诈再次发生。
本书本部分(特别是第八章和第九章)讨论的一些欺诈检测技术借鉴了更通用的异常或离群值检测领域。在此背景下,异常或离群值指的是与其它数据点显著不同的数据点。在欺诈的背景下,我们将看到这种行为(如交易)如何与个人的常规行为相背离,这可能是欺诈活动的指标。我们不会用通用术语进行推理,而是解决具体问题。尽管如此,这些章节中描述的方法、算法和技巧可以在更广泛的场景中应用。除了在金融环境中揭示可疑或异常行为外,异常检测对于发现医疗领域中的罕见事件(如罕见疾病爆发或副作用)至关重要,在医疗诊断中具有至关重要的应用。异常检测的另一个应用是数据清洗,即作为预处理步骤从数据中移除错误值或噪声,以便学习更准确的数据模型。
第十章采用了一种不同的欺诈检测方法,使用社交网络分析技术来分析欺诈如何影响人们的行为,以及欺诈者如何利用社交网络中的其他人来实现他们的欺诈目标。
8 种基于图的数据欺诈检测基本方法
本章涵盖
-
不同领域欺诈类型的介绍
-
图在建模数据以更快、更轻松地揭示欺诈中的作用
-
使用简单的图模型来对抗欺诈
根据 Van Vlasselaer 等人[2017]的研究:
欺诈是一种罕见、经过深思熟虑、随时间演变、精心组织和难以察觉的犯罪,以许多不同类型和形式出现。
这个定义突出了与开发欺诈检测系统相关的挑战的六个欺诈特征:
-
罕见——在几乎所有类型的欺诈和各个领域,只有极小部分可用的数据与(或被认为与)欺诈相关。检测欺诈很困难,从历史案例中学习也是如此(在训练阶段)。
-
经过深思熟虑——欺诈是经过仔细考虑的计划,不是偶然发生的。
-
精心组织——欺诈通常是有组织的犯罪。欺诈者通常以大型团队的形式运作,具有明确的角色。此外,某些类型的欺诈,如洗钱,涉及复杂的结构,可能需要数年才能建立。
-
难以察觉的隐藏——欺诈者投入大量精力隐藏欺诈本身,并应用技术使其识别变得困难。
-
以多种形式出现——欺诈者在各个领域采用广泛的技术和方法。许多经济活动都容易受到欺诈的影响。
-
随时间演变——欺诈者使用的技巧随着时间的推移而演变,以应对用于对抗他们的欺诈检测方法,双方都在试图领先一步。需要不断开发新的检测方法来应对新的欺诈类型。
因此,对抗欺诈对于机器学习从业者来说是一项具有挑战性的任务,但与此同时,它也非常吸引人。这些考虑也意味着本部分中介绍的技巧、约束和算法与第二部分中介绍的相比有许多不同的特点。
为什么在关于图的书中包含欺诈检测这一主题?Akoglu 等人[2014]认为,图对于异常检测是“至关重要且必要的”,原因如下:
-
数据的相互依存性——数据对象通常是相关的,并表现出依赖性。在各种各样的数据集和场景中,数据实例之间存在强大的关系,包括生物数据(如蛋白质-蛋白质相互作用网络)、金融数据(如信用卡交易)、零售网络和社会网络。
-
强大的表示能力——图通过关系(链接或边)自然地表示相关对象(节点)之间的相互依存性,这些关系捕捉了长距离相关性。此外,图表示允许通过结合节点和边的属性或类型来表示丰富的数据集。
-
问题域的关系性——异常通常具有关系性。欺诈往往是由一组相关主体在紧密合作下实施的。另一个例子,与欺诈无关,可以在系统监控领域找到:一台机器的故障可能导致依赖它的其他机器出现故障,或者由于环境条件,它可能成为其他物理上接近的机器故障概率增加的指标。
-
稳健的机制——图是具有对抗鲁棒性的,因为它们可以提供整个网络的全球视角。欺诈者或其他对手可能能够更改或伪造某些行为线索,例如登录时间或 IP 地址,但他们很可能无法消除或隐藏图揭示的数据中连接的所有迹象。当数据以图结构组织时,检查和导航变得更加容易。
在打击欺诈的背景下,图可视化可以在欺诈检测和欺诈调查中发挥关键作用。交易系统越自治,揭露欺诈就越困难,这就是图可视化的用武之地。它为专家提供了通过快速查看可视化并评估单个交易或许多交易的机会,使他们能够发现可疑行为并进一步调查。
8.1 欺诈预防和检测
打击欺诈有一些独特的特点,这些特点因领域而异,包括数据的类型、数据的维度和多样性,以及欺诈者的最终目标。尽管如此,我们可以识别出两个主要组成部分,它们是任何有效打击欺诈策略的必要部分 [Bolton and Hand, 2002]:
-
欺诈预防指的是可以采取的措施来预防或减少欺诈,例如在纸币上使用荧光纤维、层压金属条和全息图;银行卡的个人识别码;信用卡交易的互联网安全系统;移动电话的 SIM 卡和指纹传感器;以及计算机系统和电话银行账户的密码。这些方法在易受攻击性、有效性、成本和/或对客户的便利性方面都有缺点。需要在利弊之间找到权衡。
-
欺诈检测指的是识别或发现欺诈的能力。当欺诈预防失败时,它就会发挥作用,但由于这种情况并不总是显而易见,因此必须始终使用欺诈检测措施。我们可以尽最大努力预防信用卡欺诈,但如果卡的详细信息被盗,我们需要能够尽快检测到欺诈性使用。
值得注意的是,为了防止欺诈而采取的预防措施会导致欺诈者调整他们使用的策略,这反过来又会影响检测欺诈所使用的策略的有效性。所实施的欺诈类型是动态的,因此现行的欺诈减少系统也应该是动态的。尽管欺诈检测和预防是互补的,但必须将它们作为一个整体来考虑,而不是作为独立且无关的系统。
令人惊讶的是,欺诈检测最常见的方法是经典的基于专家的方法,它依赖于欺诈分析师的经验、领域知识、直觉和个人技能。这些方法几乎完全是手动和基于人类的。专家对可疑案件进行手动调查,通常由其他人(如客户投诉被收取未发起的交易费用)引起。
这种分析的结果可能是发现欺诈者正在使用的一种新的欺诈机制。当发现这种机制时,它会被进一步分析和调查,以扩展现有的欺诈检测和预防机制,这通常是通过基于规则的引擎实现的。这个引擎由一系列规则组成,通常以 if-then 语句的形式,应用于每个交易或一系列交易,并在匹配时触发警报或信号。图 8.1 展示了这种方法的思维模型。

图 8.1 基于规则的引擎思维模型
可以定义一个简单但有效的信用卡欺诈检测规则集如下:¹
如果
-
上一次交易金额不到$15
-
上一次交易发生时间不到 2 小时前
-
当前交易金额超过$500
那么
-
将状态设置为拒绝
-
在小额交易后设置通知为大额
尽管这种基于专家的方法在许多领域都有用且仍然很常见,但它有几个缺点:
-
基于规则的引擎通常很复杂,因此建造成本很高,因为它们需要欺诈专家的高级手动输入。
-
这种复杂性使得它们难以维护和管理。
-
规则必须保持最新,因为欺诈者不断演变他们的方法,并提出新的方法。一旦他们发现了欺诈减少系统背后的规则,他们就会改变行为以避免被识别。
-
在大多数情况下,这些系统需要进一步的人工跟进、分析和调查。
这种方法的最大缺点是,因为它涉及到大量的人类干预,包括专家输入、分析、评估和监控,它过度依赖难以共享和维护的个人贡献。欺诈检测系统的有效性与特定知识的人的可用性有关。那么当这些人休假或退休时会发生什么呢?
近年来,一种以数据驱动、统计和机器学习为基础的欺诈检测方法正在兴起。与之前提到的以及以下原因一样,自动化的方法比纯粹基于人类的方法更可取:
-
精确度——一个自主的系统可以处理大量数据,揭示人类无法识别的欺诈模式。
-
运营效率——想想信用卡发行商每天、每分钟和每秒钟需要处理的交易数量。在正常运营所需的时间限制内,让人类实时检查所有交易是不可能的,但计算机可以轻松处理这项任务。此外,基于机器的方法可以通过预筛选、分析每一笔交易/操作,然后将最相关或可疑的交易发送给人类进行进一步调查来支持基于人类的方法。
-
成本效率——如前所述,基于专家的欺诈检测系统难以实施和维护。更自动化、数据驱动且因此更有效的方法更受欢迎。
-
适应效率——一些自主的数据驱动方法是无监督的(将在这些章节中突出显示)。这一方面不仅使它们在运营和成本上效率高,而且还能随着时间的推移适应欺诈行为和欺诈者行为的演变特征。
为了更好地说明欺诈检测背后的基本思想,让我们考虑一个来自 Fawcett 和 Provost [1997]的简单示例,如表 8.1 所示。
表 8.1 电信异常检测示例
| 通话序列 | 日期和时间 | 星期 | 持续时间 | 起始地 | 目的地 | 欺诈 |
|---|---|---|---|---|---|---|
| 1 | 2019-01-01 10:05:01 | Mon | 13 mins | 布鲁克林,纽约 | 斯坦福,康涅狄格州 | |
| 2 | 2019-01-05 14:53:10 | Fri | 5 mins | 布鲁克林,纽约 | 格林威治,康涅狄格州 | |
| 3 | 2019-01-08 09:42:15 | Mon | 3 mins | 布朗克斯,纽约 | 怀特普莱恩斯,纽约 | |
| 4 | 2019-01-08 15:01:34 | Mon | 9 mins | 布鲁克林,纽约 | 布鲁克林,纽约 | |
| 5 | 2019-01-09 15:06:54 | Tue | 5 mins | 曼哈顿,纽约 | 斯坦福,康涅狄格州 | |
| 6 | 2019-01-09 16:28:20 | Tue | 53 sec | 布鲁克林,纽约 | 布鲁克林,纽约 | |
| 7 | 2019-01-10 01:45:29 | Wed | 35 sec | 波士顿,马萨诸塞州 | 切尔西,马萨诸塞州 | True |
| 8 | 2019-01-10 01:46:35 | Wed | 34 sec | 波士顿,马萨诸塞州 | 杨克斯,纽约 | True |
| 9 | 2019-01-10 01:50:54 | Wed | 39 sec | 波士顿,马萨诸塞州 | 切尔西,马萨诸塞州 | True |
| 10 | 2019-01-10 11:23:20 | Wed | 24 sec | 怀特普莱恩斯,纽约 | 康格斯,纽约 | |
| 11 | 2019-01-11 22:00:58 | Thu | 37 sec | 波士顿,马萨诸塞州 | 东波士顿,马萨诸塞州 | True |
| 12 | 2019-01-11 22:04:00 | Thu | 37 sec | 波士顿,马萨诸塞州 | 东波士顿,马萨诸塞州 | True |
骗子电话被标记为欺诈。这些电话是由客户标记的,因为他们抱怨这些电话是他们没有打的。
如果你仔细查看表格,你会注意到这些电话有一些特征,使它们与真实号码所有者打出的电话不同。特别是,它们更短,发生在夜间,源和目的地异常。基于异常检测的欺诈检测方法在这里非常有价值,尤其是与专家系统技术(手动方法)相比:它允许自动检测大量欺诈案例,因为这些案例与从历史示例中明显可见的正常行为不同。
与欺诈检测相关的一大挑战是数据管理。这些问题在书中多次提及,与大数据相关;包括数据量、速度、多样性和真实性。回想一下书中第一部分使用的图表,这里重复展示在图 8.2 中。

图 8.2 大数据的四个 V(Volume, Velocity, Variety, Veracity)也适用于欺诈检测的上下文。
在这个案例中,还有一些额外的挑战需要处理,例如流数据和数据复杂性。用于欺诈检测过程的数据集内容丰富且复杂,包括用户人口统计信息、兴趣和角色,以及关系类型。整合这些额外的信息源使得数据表示变得复杂。因此,能够扩展到大型数据集、随时间变化更新其估计并有效整合所有可用和有用数据源的方法对于异常检测至关重要。
图数据模型和分析提供了一套处理此类复杂性的宝贵工具。模式灵活性允许来自各种数据源的数据被存储,并提供多种易于扩展的访问模式,即使数据集很大也是如此。图辅助或基于图的算法使得发现、分析和调查事件之间的关系变得更加容易。在欺诈检测的上下文中,图提供的优势在 8.2 节中得到了强调,并在本章的剩余部分以及第九章和第十章中进一步探讨。
因为打击欺诈就像任何一场所有各方都试图使用他们能找到的任何可能的技术来取得优势的战斗一样,分析师必须使用他们可用的所有工具。重要的是要认识到这些技术不是相互排斥的:它们可以组合在一个综合系统中,以便更容易、更快、更有效地达到目标。
8.2 图在打击欺诈中的作用
图形为捕捉相互依赖的数据对象之间的长期相关性提供了强大的建模和分析工具,这使得它们非常适合反欺诈场景。我们的银行账户和信用卡交易遵循与我们行为、居住地、喜欢购买的东西等相关联的逻辑。此外,欺诈很少孤立发生:任何欺诈背后都有一套涉及准备的计划,这通常需要多个欺诈者的合作。
为了更好地理解图形在数据表示(以及我们稍后将看到的分析)方面的价值,让我们考虑一个简单的例子。看看表 8.2 中信用卡欺诈系统的交易数据源。
表 8.2 信用卡交易数据示例
| 信用卡 | 商户 | 商户类别 | 国家 | 金额 | 日期 | 接受 | 欺诈 |
|---|---|---|---|---|---|---|---|
| 77777783427 | 207005 | 服装店 | 美国 | 120.00$ | 2019-01-11 00:12:01 | TRUE | FALSE |
| 47559798454 | 105930 | 加油站 | 意大利 | 50.00€ | 2019-03-12 08:01:30 | TRUE | FALSE |
| 25548837225 | 105930 | 加油站 | 意大利 | 20.56€ | 2019-04-23 10:10:20 | TRUE | FALSE |
| 18560530742 | 11525 | 餐厅 | 比利时 | 50.00€ | 2019-05-01 15:00:12 | TRUE | FALSE |
| 37960598819 | 323158 | 在线商店 | 美国 | 300.00$ | 2019-05-02 01:00:00 | TRUE | TRUE |
| 16307358365 | 11525 | 餐厅 | 比利时 | 40.00€ | 2019-05-03 20:45:00 | TRUE | FALSE |
在这个表中,每一行代表两个行为者之间的资金转移:信用卡持卡人和商户。第五笔交易在最右侧列中被标记为欺诈,因为卡主对此提出了投诉。
在这种表示中,数据存储为行列表,难以捕捉持卡人和商户之间的关系。现实生活中的数据源包含数十亿笔交易,这使得手动提取相关性以及有用的见解变得不可能。图 8.3 展示了这种数据在图模型中的可能表示。

图 8.3 信用卡交易数据的图形表示
在这个图中,圆圈代表信用卡,正方形代表商家。连接信用卡和商家的关系代表交易。粗线代表欺诈交易。在这个表示中,很明显信用卡 Y 被盗,商家 1 行为可疑(处理大量欺诈交易)。这个简单的例子清楚地说明了图如何提供一种强大的工具,使原本在原始格式中隐藏且难以识别的信息更加明显,更容易解释和理解。检查图的视觉表示可以是预处理阶段的一个宝贵部分:它使分析师熟悉数据,并可以迅速得出初步发现和见解。此外,数据简单易查询和分析;我们将在 8.3 节中看到具体的例子。然后,在后处理阶段,图可以提供一种有用的表示,用于验证获得的结果并理解其背后的原因。
这些方法在 8.3 节中有所介绍。其目的是强调图表示的实用性和灵活性;一个简单的表示可以服务于不同的目的。
之前的例子是转换数据的一种可能方式——具体来说,是将交易数据转换为图模型。一种截然相反的方法是,将每一笔交易表示为一个节点,并根据某种逻辑连接这些节点。最常见的方法是基于相似性。以下例子将这种方法解释得更清楚。考虑我们之前看到的通话数据,再次在表 8.3 中展示。
表 8.3 电信异常检测示例
| 通话序列 | 日期和时间 | 星期 | 持续时间 | 起点 | 目的地 | 欺诈 |
|---|---|---|---|---|---|---|
| 1 | 2019-01-01 10:05:01 | Mon | 13 mins | Brooklyn, NY | Stamford, CT | False |
| 2 | 2019-01-05 14:53:10 | Fri | 5 mins | Brooklyn, NY | Greenwich, CT | False |
| 3 | 2019-01-08 09:42:15 | Mon | 3 mins | Bronx, NY | White Plains, NY | False |
| 4 | 2019-01-08 15:01:34 | Mon | 9 mins | Brooklyn, NY | Brooklyn, NY | False |
| 5 | 2019-01-09 15:06:54 | Tue | 5 mins | Manhattan, NY | Stamford, CT | False |
| 6 | 2019-01-09 16:28:20 | Tue | 53 sec | Brooklyn, NY | Brooklyn, NY | False |
| 7 | 2019-01-10 01:45:29 | Wed | 35 sec | Boston, MA | Chelsea, MA | True |
| 8 | 2019-01-10 01:46:35 | Wed | 34 sec | Boston, MA | Yonkers, NY | True |
| 9 | 2019-01-10 01:50:54 | Wed | 39 sec | Boston, MA | Chelsea, MA | True |
| 10 | 2019-01-10 11:23:20 | Wed | 24 sec | White Plains, NY | Congers, NY | False |
| 11 | 2019-01-11 22:00:58 | Thu | 37 sec | Boston, MA | East Boston, MA | True |
| 12 | 2019-01-11 22:04:00 | Thu | 37 sec | Boston, MA | East Boston, MA | True |
使用前几章讨论的图构建技术,可以将此类表格数据转换为图表示。在生成的图模型中,如图 8.4 所示,每个调用表示为一个节点,所有相关细节都显示为相关属性。

图 8.4 从表 8.3 中的数据创建的第一个图
使用属性和相关值,我们可以将每个节点表示为一个向量。表 8.4 详细展示了这个过程。
表 8.4 将通话数据转换为向量的示例表
| 序列 | 星期 | 持续时间(分钟) | 来源(城市/区域) |
|---|---|---|---|
| 星期一 | 星期二 | 星期三 | |
| 1 | 1 | 0 | 0 |
| 2 | 0 | 0 | 0 |
| 3 | 1 | 0 | 0 |
| 4 | 1 | 0 | 0 |
| 5 | 0 | 1 | 0 |
| 6 | 0 | 1 | 0 |
| 7 | 0 | 0 | 1 |
| 8 | 0 | 0 | 1 |
| 9 | 0 | 0 | 1 |
| 10 | 0 | 0 | 1 |
| 11 | 0 | 0 | 0 |
| 12 | 0 | 0 | 0 |
每个属性(原始表格的每一列)都已被分解为可能的值,就像我们为文本生成基于内容的推荐那样。对于标量属性(在这种情况下,持续时间),我们定义了多个范围。每个节点的这种向量表示用于计算相似度,例如使用余弦相似度。以下列表展示了如何使用 scikit-learn 进行计算。
列表 8.1 计算向量之间相似性的代码
from sklearn.metrics.pairwise import cosine_similarity
call_01 = [1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0]
call_02 = [0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0]
call_03 = [1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0]
call_04 = [1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0]
call_05 = [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0]
call_06 = [0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]
call_07_fraud = [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0]
call_08_fraud = [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0]
call_09_fraud = [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0]
call_10 = [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1]
call_11_fraud = [0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0]
call_12_fraud = [0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0]
calls = {'call_01': call_01,
'call_02': call_02,
'call_03': call_03,
'call_04': call_04,
'call_05': call_05,
'call_06': call_06,
'call_07_fraud': call_07_fraud,
'call_08_fraud': call_08_fraud,
'call_09_fraud': call_09_fraud,
'call_10': call_10,
'call_11_fraud': call_11_fraud,
'call_12_fraud': call_12_fraud}
print("....")
processed = []
for i in list(calls):
for j in list(calls):
if {'source': j, 'dest': i} not in processed and i != j:
print("similarity between", i, j, cosine_similarity([calls[i]],
➥ [calls[j]]))
processed += [{'source': j, 'dest': i}]
processed += [{'source': i, 'dest': j}]
输出如下所示,为了简洁起见,省略了尾随零。
列表 8.2 列表 8.1 的输出
similarity between call_01 call_02 [[0.66666667]]
similarity between call_01 call_03 [[0.33333333]]
similarity between call_01 call_04 [[1.]]
similarity between call_01 call_05 [[0.33333333]]
similarity between call_01 call_06 [[0.33333333]]
similarity between call_02 call_04 [[0.66666667]]
similarity between call_02 call_05 [[0.33333333]]
similarity between call_02 call_06 [[0.33333333]]
similarity between call_03 call_04 [[0.33333333]]
similarity between call_04 call_05 [[0.33333333]]
similarity between call_04 call_06 [[0.33333333]]
similarity between call_05 call_06 [[0.33333333]]
similarity between call_06 call_07_fraud [[0.33333333]]
similarity between call_06 call_08_fraud [[0.33333333]]
similarity between call_06 call_09_fraud [[0.33333333]]
similarity between call_06 call_10 [[0.33333333]]
similarity between call_06 call_11_fraud [[0.33333333]]
similarity between call_06 call_12_fraud [[0.33333333]]
similarity between call_07_fraud call_08_fraud [[1.]]
similarity between call_07_fraud call_09_fraud [[1.]]
similarity between call_07_fraud call_10 [[0.66666667]]
similarity between call_07_fraud call_11_fraud [[0.66666667]]
similarity between call_07_fraud call_12_fraud [[0.66666667]]
similarity between call_08_fraud call_09_fraud [[1.]]
similarity between call_08_fraud call_10 [[0.66666667]]
similarity between call_08_fraud call_11_fraud [[0.66666667]]
similarity between call_08_fraud call_12_fraud [[0.66666667]]
similarity between call_09_fraud call_10 [[0.66666667]]
similarity between call_09_fraud call_11_fraud [[0.66666667]]
similarity between call_09_fraud call_12_fraud [[0.66666667]]
similarity between call_10 call_11_fraud [[0.33333333]]
similarity between call_10 call_12_fraud [[0.33333333]]
similarity between call_11_fraud call_12_fraud [[1.]]
图 8.5 显示了如果我们定义相似度阈值为 0.5 并将关系存储在之前的图中,结果将如何。

图 8.5 通过图构建技术获得的通话图
观察节点之间的距离,我们发现欺诈电话比正常电话更接近其他欺诈电话。在这个时候,可以使用图聚类算法,如社区检测,将欺诈电话分组,然后使用这种模型根据分类模型创建的组来分类新的电话是否为欺诈,考虑它们与这些组之间的距离。这些方法在第 8.3 节中更详细地介绍,与之前章节中介绍的 k-NN 方法相似。这种反复出现的“模式”再次表明,基于图的技术——在这种情况下,基于最近邻方法的图构建——是通用的,可以服务于不同的分析目的。您可以在需要分析具有类似于表 8.3 的结构的数据时应用这种思维模式。
在进行欺诈分析时,一个基本问题就是检测模型是否可能从社交网络上的复杂网络分析中受益[Baesens 等人,2015]。换句话说,人与人之间的关系在欺诈中是否扮演着重要角色,欺诈是否在网络上具有传染性?欺诈者是否在社交网络中随机分布,或者可观察到的效应表明欺诈是一种社会现象?也就是说,欺诈者是否倾向于聚集在一起?正如之前所述,欺诈很少由单个个体实施;复杂和高级的方案需要许多人的合作,欺诈者通常利用他们的社交网络(朋友、同事等)来进行欺诈。因此,社交网络分析有助于发现欺诈者群体或组织。
欺诈者可能因为参加相同的活动/事件、涉及相同的犯罪、使用相同的资源集,或者有时是同一个人(例如在身份盗窃的情况下)而相互关联。第十章探讨了社交网络分析作为欺诈检测的辅助技术。
这里介绍的概念是表示数据为图以执行欺诈检测的几种可能方式之一。更高级的方法在第九章中介绍。
8.3 热身:基本方法
让我们从考虑一些简单易懂且易于实施的方法开始我们的欺诈之战。尽管这些技术很简单,但它们在提到的情况下允许有效的分析,并且很好地适应图空间。
8.3.1 寻找信用卡欺诈的源头
假设你希望通过识别信用卡盗贼来打击信用卡欺诈。这个场景是欺诈检测示例中的经典案例,尤其是在图空间中。在这种情况下,用户(信用卡所有者)使用他们的卡进行多次购买,在某个时刻,他们的信用卡详细信息被盗。欺诈者使用被盗的凭证购买商品或在不同账户之间转账。他们通常会通过以低廉的价格购买商品来测试卡详情,然后在消失之前进行几次大额购买。这种欺诈的架构总结在图 8.6 中。

图 8.6 信用卡欺诈架构
值得注意的是,信用卡详细信息被盗的地方可能是一个实体店或电子商务网站。在后一种情况下,网络攻击通常会打开一个漏洞,并复制所有注册用户的信用卡详细信息。
本场景的目标是在多个用户的一系列交易中确定信用卡详细信息被盗的点。我借鉴了这种方法以及第 8.3.2 节中的方法,来源于 Max DeMarzi 撰写的几篇博客文章。²我喜欢这些想法,因为它们既有效又简单。此外,它们展示了如何通过使用正确的图数据模型,在传统数据库中看似困难的分析只需要在图上进行几个查询。第九章中的例子更为复杂,但正如本节标题所暗示的,本节中的例子将有助于作为热身。
仅考虑特定用户及其交易,很难发现盗贼行动的点。那次交易被接受且有效;问题(欺诈交易)发生在之后。幸运的是,如果我们考虑大量用户的交易序列,我们可以识别共性——商店或电子商务网站——并开始更深入的调查。图 8.7 说明了这个概念。

图 8.7 结合多个信用卡交易数据以识别共性
所有经历过信用卡问题的用户都在同一个加油站使用过他们的卡。很可能盗窃就发生在那里。此外,很明显,这些卡通过小额购买来验证详情。因此,设计一个能够使这些事实明显并促使进一步调查的图模型非常重要。
我们如何使用图模型来发现欺诈交易的起源?让我们从本书中我总是建议的可用数据开始,这些数据可能看起来像表 8.5。
表 8.5 信用卡交易示例
| 信用卡 | 商家 | 商家类别 | 国家 | 金额 | 日期 | 接受 | 欺诈 |
|---|---|---|---|---|---|---|---|
| 77777783427 | 207005 | 衣服店 | 美国 | 120.00 美元 | 2019-01-11 00:12:01 | 是 | 否 |
| 47559798454 | 105930 | Gas Station | ITA | 50.00€ | 2019-03-12 08:01:30 | TRUE | FALSE |
| 25548837225 | 105930 | Gas Station | ITA | 20.56€ | 2019-04-23 10:10:20 | TRUE | FALSE |
| 18560530742 | 11525 | Restaurant | BEL | 50.00€ | 2019-05-01 15:00:12 | TRUE | FALSE |
| 37960598819 | 323158 | Online Shop | USA | 300.00$ | 2019-05-02 01:00:00 | TRUE | TRUE |
| 16307358365 | 11525 | Restaurant | BEL | 40.00€ | 2019-05-03 20:45:00 | TRUE | FALSE |
这些数据是信用卡持卡人执行的交易样本(足以说明概念)。交易根据用户是否对那些购买提出投诉而标记为欺诈或不欺诈,因此大多数情况下这些信息是可用且准确的。我们希望从表中捕获的关键元素是
-
用户进行的交易,包括金额和日期等详细信息
-
信用卡标识符
-
进行购买的商家的标识符
-
关于商家类型的信息(虽然不是必需的,但对简化我们的讨论很有用)
为此场景设计图模型的第一次尝试如图 8.8 所示。

图 8.8 我们交易的第一个基本模型
架构肯定是正确的。缺少的是每个用户的交易顺序。如图 8.7 所示,我们需要考虑交易的顺序,将它们与其他用户关联起来,以找到信用卡详细信息被盗的点。我们可以通过使用时间戳信息来对交易进行排序,但这种方法会使导航变得困难,并且难以找到共同的模式。因此,下一步建模步骤是添加交易之间的关系,以显式存储它们之间的顺序。扩展模型如图 8.9 所示。此模型考虑了每个用户的交易序列。

图 8.9 基本模型的扩展
现在我们有了我们的模型,我们可以通过检查它是否可以回答我们的问题来验证它:信用卡详细信息是在哪里被盗的?为了跟随这个例子,你需要一个样本数据集。在代码仓库中有一个 Cypher 文件(ch08/queries/simple_fraud_dataset.cypher),其中包含一个查询,用于创建一个简单的数据集,你可以用于此目的。在第九章中,你将看到如何创建和导入更复杂的数据集,但为了简单起见,我们将从一个小的开始,找到过去一周内所有关于欺诈交易的投诉,并查看这些用户在过去两周内进行了哪些其他交易。我们可以使用 HAS_NEXT 关系链向后导航并找到所有交易。
列表 8.3 可视化导致欺诈的交易
MATCH p = (fraud:Fraudulent)<-[:HAS_NEXT*]-(tx:Transaction)
WHERE fraud.date > datetime() - duration('P7D') AND
NONE (tx IN nodes(p) WHERE COALESCE(tx.date, datetime()) <= datetime() -
➥ duration('P14D'))
RETURN p
此查询仅显示导致已知欺诈的交易序列。结果看起来像图 8.10。³

图 8.10 最大的节点是欺诈交易。
下一步是识别这些序列中的共同元素(模式)。为此,我们可以稍微扩展查询以找到这些交易发生的商店,并计算每个商店出现的频率。
列表 8.4 欺诈交易链中前五名常见商家
MATCH p = (fraud:Fraudulent)<-[:HAS_NEXT*]->(tx)
WHERE fraud.date > datetime() - duration('P7D')
AND NONE (tx IN nodes(p)
WHERE COALESCE(tx.date, datetime()) <= datetime() -
➥ duration('P14D'))
WITH nodes(p) AS transactions
UNWIND transactions AS tx
WITH DISTINCT tx
MATCH (tx)-[:AT_MERCHANT]->(merchant)
RETURN merchant.name, COUNT(*) AS txCount
ORDER BY txCount DESC
LIMIT 5
结果显示在图 8.11 中。

图 8.11 查询 8.2 的结果
这些结果还不错,但它们并不是我们想要的;一些众所周知、每个人都购买商品的在线商店出现在列表中。我们需要小心:不加批判地看待这些结果,我们可能会倾向于说欺诈者正在亚马逊上操作,但由于该网站的安全措施,这种情况极不可能发生。相反,我们需要关注我们选择的交易集中不寻常的常见商店,因为它们更有可能与欺诈有关。
我们可以采取不同的方法来实现这个目标。例如,我们可以将这些结果与全球交易列表中的顶级商家进行比较,并将两个列表中都出现在顶部的所有商店标记为“不相关”。相反,我们将使用更复杂、更有趣(以及更强大)的方法。我们需要考虑这两组交易:
-
包含所有交易数据的全局集,而不仅仅是成为欺诈受害者的人的交易。这个数据集是我们的背景集。我们将获取该集中商家的出现频率统计信息,并在我们的公式中使用这些信息。
-
投诉欺诈性收费的人的最近交易集。这个数据集是我们的前景集,代表我们的真实目标。
考虑到这两组交易,我们的目标可以明确地表述如下:在前景数据集中找到与背景数据集相比不寻常地常见的商家。这种方法用于搜索引擎⁵中揭示搜索结果中的显著术语,我们将借用相关的公式⁶。

通过将此公式应用于我们之前获得的商家列表,我们可以揭示在欺诈受害者集的交易中比平常更频繁出现的商家。这个公式很简单,因此我们可以将查询转换为使用它。
列表 8.5 使用显著性分数查询以揭示欺诈点
MATCH (t:Transaction)
WITH count(t) as txCount ❶
MATCH p = (fraud:Fraudulent)<-[:HAS_NEXT*]-(tx)
WHERE fraud.date > datetime() - duration('P7D')
AND NONE (tx IN nodes(p)
WHERE COALESCE(tx.date, datetime()) <= datetime() -
➥ duration('P14D'))
WITH txCount, count(distinct tx) as txForegoundCount ❷
MATCH p = (fraud:Fraudulent)<-[:HAS_NEXT*]-(tx)
WHERE fraud.date > datetime() - duration('P7D')
AND NONE (tx IN nodes(p)
WHERE COALESCE(tx.date, datetime()) <= datetime() -
➥ duration('P14D'))
WITH txCount, nodes(p) AS nodes, txForegoundCount ❸
UNWIND nodes AS tx
WITH DISTINCT txCount, txForegoundCount, tx
MATCH (tx)-[:AT_MERCHANT]->(merchant)
WITH merchant, txCount, 1.0f*COUNT(tx)/txForegoundCount AS
➥ foregroundPercentage ❹
MATCH (t:Transaction)-[:AT_MERCHANT]->(merchant)
with merchant, 1.0f*count(t)/txCount as backgroundPercentage,
➥ foregroundPercentage ❺
RETURN merchant.name, backgroundPercentage, foregroundPercentage,
➥ (foregroundPercentage*foregroundPercentage/backgroundPercentage) -
➥ foregroundPercentage as score ❻
ORDER BY score DESC
LIMIT 5
❶ 计算数据库中的总交易数量。我们也可以考虑过去 14 天的总交易数量,因为我们在分析中考虑了这一范围,但在这个小型数据库中这并不重要。这个值用于计算背景百分比。
❷ 计算属于欺诈受害者(欺诈链)的交易总数。这个值用于计算前景百分比。
❸ 获取欺诈链中的节点列表
❹ 对于欺诈链中的每个商家,计算其前景百分比(它们在我们前景集中出现的频率)
❺ 计算背景百分比
❻ 计算得分
查询结果如图 8.12 所示。
结果不同且意义重大。从这个列表中,很明显可以看出欺诈者在哪里操作并窃取信用卡详细信息。

图 8.12 查询结果
当我们有与欺诈有关的相关商家的列表时,我们可以使用这些信息来获取一个名单,包括过去 14 天内在该地点使用的任何信用卡,以及可能处于风险中的人或信用卡。
列表 8.6 获取潜在受害者的查询
MATCH (merchant:Merchant {name:"Gas Station"})<-[:AT_MERCHANT]-(tx)<-
➥ [:MAKES]-(user)
WHERE tx.date > datetime() - duration('P14D')
RETURN user.name
当我们确定了有风险的信用卡后,可以将其锁定并通知持卡人,或者可以更密切地监控它们,以检查是否出现其他信号,例如与其他商家的小额交易来测试卡片。
需要注意的是,如果交易数量很高,这里提出的方法将无法扩展。特别是,你可能会有很多与某些商家相关的交易,使得他们的相关节点密集。可以应用一些技术来减轻这些问题,例如将密集节点拆分为多个节点,创建时间特定的关系(例如 AT_MERCHANT_ON_2019_08_19),等等。在第九章中,我们将考虑可以扩展的技术。
然而,首先值得提及另一种场景,在这种场景中,图模型提供了一个简单、强大且高效的表示,可以识别特定类型的欺诈。即使在这种情况下,分析也可以通过在定义的图模型上使用查询来完成。
8.3.2 识别欺诈团伙
我们将考虑的特定欺诈方案的步骤总结如下列表:
-
在金融机构(为实施欺诈而创建的真实账户)中创建了大量的合成客户账户。
-
很长时间以来,这些账户表现得像普通客户一样。
-
随着时间的推移,他们要求更高的信用额度,并按时偿还,这使得他们能够在银行中获得信誉和信任。
-
实际上,资金是在同一组账户之间转移,通过多个和不同的跳转来避免被识别。
-
在某个时候,他们都要求他们能得到的最大信用额度,取出钱后消失。
这种特定的欺诈类型被称为欺诈团伙,因为账户、它们的详细信息以及资金围绕同一组人形成一个圈。在洗钱方面,这种类似的技术被用于更大规模。在这种情况下,资金从一个账户转移到另一个账户,通常在不同的国家,以清洗来自非法来源的资金,避免缴纳税款,或资助恐怖分子或其他非法组织。让我们看看我们如何应对这种类型的欺诈,首先从定义场景开始。
假设你希望尽快识别欺诈团伙的形成,在欺诈行为实施之前。目标是揭示团伙的形成,以免为时已晚。为了理解团伙的形成,考虑表 8.6 中的样本账户持有人详情。
表 8.6 账户持有人详情
| 账户 ID | 用户名 | 邮箱 | 电话号码 | 全名 | 地址 |
|---|---|---|---|---|---|
| 49295987202 | alenegro | mpd7xg@tim.it | 580-548-1149 | 希尔达·J·沃马克 | 奥克拉荷马州恩尼德 4093 科迪岭路 |
| 45322860293 | jimjam | jam@mail.com | 504-262-8173 | 梅根·S·布鲁巴格 | 奥克拉荷马州恩尼德 4093 科迪岭路 |
| 45059804875 | drwho | mpd7xg@tim.it | 504-262-8173 | 约翰·V·丹尼尔森 | 威斯康星州霍普山 4985 玫瑰大道 |
| 41098759500 | robbob | bob@google.com | 352-588-9221 | 罗伯特·C·安图内斯 | 佛罗里达州圣安东尼奥 2041 贝格尔大道 |
与账户相关的产品列在表 8.7 中。(关系数据库将更复杂;此示例仅用于说明。)
表 8.7 监控账户所属的产品
| 账户 ID | 产品类型 | 产品 ID |
|---|---|---|
| 49295987202 | 信用卡 | 793922 |
| 49295987202 | 银行账户 | 896857 |
| 49295987202 | 贷款 | 885398 |
| 45322860293 | 信用卡 | 482513 |
| 45322860293 | 银行账户 | 305693 |
| 45059804875 | 信用卡 | 631264 |
| 45059804875 | 银行账户 | 171215 |
| 45059804875 | 贷款 | 432775 |
| 41098759500 | 银行账户 | 377703 |
| 41098759500 | 贷款 | 859916 |
如果我要求你以图的形式对这些表格进行建模,到这本书的这一部分你应该能够做到,你很可能会得到类似于图 8.13 的东西。

图 8.13 银行账户持有人详情的第一图模型
这个模型是完美的,特别是考虑到到目前为止讨论的指南,但为了识别团伙,我们需要尽可能多地爆炸信息到单独的节点中。使用这种方法,你可能会创建一个如图 8.14 所示的图。

图 8.14 第二个模型,将关键用户属性转换为节点。(仅可视化相关节点。)
显然,这些银行账户有些奇怪;图表清楚地表明它们之间联系紧密。数据库当然很小,所以链接很容易被发现,但有一个机制允许我们检查任何大小的图表并搜索连接组件。如果一个图中的任何一对节点都通过路径连接(如果它是定向的,则称为强连接),则认为该图是连接的 [Diestel, 2017]。在任何图(包括未连接的图)中,都可能存在一组称为最大连接子图的连接组件。
幸运的是,我们不需要自己在 Neo4j 中实现这样的算法,因为它已经被实现并作为插件提供。⁷ 要尝试它,你需要一个简单的图;创建一个图的 Cypher 查询可以在 ch08/queries/simple_ring_fraud.cypher 中找到。⁸ 执行查询,并在 Neo4j 中安装插件。⁹ 到那时,你可以执行以下查询。第一个查询将属性分区分配给每个用户,指定用户属于哪个簇。
列表 8.7 识别环并将用户分配给它们的查询
CALL gds.wcc.write(
{nodeQuery: "MATCH (p:User) RETURN id(p) as id",
relationshipQuery: "MATCH (p1:User)--()--(p2:User) RETURN id(p1) as source,
➥ id(p2) as target",
writeProperty: "partition"}
)
YIELD
componentCount,
createMillis,
computeMillis,
writeMillis
下一个查询显示了每个簇的组成部分。
列表 8.8 查询以可视化每个用户属于哪个簇
MATCH (n:User)
RETURN n.partition, COUNT(*) AS members, COLLECT(n.name) AS names
ORDER BY members DESC
结果显示在图 8.15 中。

图 8.15 两个先前查询的结果
这些查询显示的正是我们已知的内容:Hilda、Megan 和 John 形成一个簇,因为他们通过一个或多个属性(如电子邮件地址、电话号码和邮寄地址)连接在一起。
看起来 Rob 已经出局了,但现实情况略有不同:Rob 是这个组织的头目。(为了这个场景的目的,我们可以假设我们是从其他来源知道这个信息的。)但他不使用与其他所有人共有的任何信息。聪明人!我们如何在环中捕捉他的关系?
如前所述,图的一个优点是我们可以使用图来合并来自不同数据源的数据。银行账户和信用卡为用户提供连接在线银行服务的可能性。这个系统捕捉了关于用户浏览器、IP 地址、设备等很多我们未使用的细节。让我们只考虑 IP 信息。表 8.8 显示了这些信息可能的样子。
表 8.8 账户连接信息
| 账户 ID | IP | 日期 |
|---|---|---|
| 41098759500 | 166.184.50.48 | 2020-01-21 |
| 45059804875 | 166.184.50.48 | 2020-01-21 |
| 41098759500 | 208.125.140.154 | 2020-01-19 |
| 45059804875 | 74.248.71.164 | 2020-01-17 |
| 45322860293 | 208.125.140.154 | 2020-01-19 |
如果我们将这个数据源添加到我们的图中,我们得到图 8.16 的结果。

图 8.16 第三种捕捉用户连接到在线银行平台 IP 地址信息的模型
图 8.16 说明了我们的论点:Rob 在操纵一切。他作为不同账户持有人从同一地点连接。让我们通过使用 ch08/queries/simple_ring_fraud_IP.cypher 中可用的查询来扩展我们的数据库。重新运行列表 8.7 中的查询,我们得到图 8.17 的结果。

图 8.17 合并 IP 信息后的结果
从这些结果来看,显然 Rob 与其他所有账户都有很好的联系。现在,人类分析师可以进行更深入的调查,关闭欺诈账户,并将账户背后的个人报告给当局,禁止他们在该国开设任何其他银行账户。问题解决。
值得注意的是,环检测方法可以用于金融欺诈以外的其他环境,例如用于识别网站上的同一用户的多个账户。这个问题在多种场景中都很常见,如下所示:
-
被禁止的用户试图获取新账户
-
在一个拍卖网站上,同一用户使用多个账户出价以增加物品的价格
-
在一个扑克室里,同一用户在同一桌面上与其他玩家使用多个账户进行游戏
-
在像亚马逊这样的市场上,商家支付人们或其他公司发布虚假评论以增加其产品可信度的行为
在所有这些情况下,找到环状结构有助于我们发现潜在问题或发现可疑行为,并正确地应对它们。
8.3.3 图方法的优势
本节介绍了一些简单而强大的欺诈检测技术。所提出的场景非常适合图模型:正如你所看到的,通过适当的模型和查询,我们可以找到关于我们数据的洞察性信息。如果这些关系没有明确表示,这些细节在一个大型数据库中很难找到。具体来说,在信用卡欺诈的情况下,通过图查询,我们能够识别出一组交易中不寻常的常见模式,这些交易最终导致了欺诈。在环状示例中,图显示了人与人、账户之间的联系,这在关系型数据库中是无法捕捉到的。
摘要
本章介绍了与欺诈以及更广泛的异常检测相关的基本概念。介绍了不同的方法,其中图的作用对于提供高质量的分析结果和可扩展到实际生产就绪解决方案的基础设施至关重要。更详细地说,你学习了
-
欺诈类型以及如何处理最关键的欺诈
-
如何设计一个能够识别用户信用卡详细信息可能被盗位置的图模型
-
如何根据提供的个人详细信息以及用户用于访问在线银行系统的多个接触点收集的信息来识别银行账户中的环
参考文献
[Akoglu et al., 2014] Akoglu, Leman, Hanghang Tong, and Danai Koutra. “基于图的异常检测和描述:综述。” arXiv 预印本 arXiv:1404.4679 (2014)。
[Baesens et al., 2015] Baesens, Bart, Veronique Van Vlasselaer, and Wouter Verbeke. 使用描述性、预测性和社交网络技术的欺诈分析:数据科学在欺诈检测中的应用指南。Hoboken, NJ: Wiley, 2015。
[Bolton 和 Hand,2002] Bolton, Richard J., 和 David J. Hand. “统计欺诈检测:综述.” 统计科学 17:3 (2002): 235-255.
[Diestel,2017] Diestel, Reinhard. 图论. 纽约:Springer,2017.
[Fawcett 和 Provost,1997] Fawcett, Tom, 和 Foster Provost. “自适应欺诈检测.” 数据挖掘与知识发现 1:3 (1997): 291-316.
[Van Vlasselaer 等人,2017] Van Vlasselaer, Véronique, Tina Eliassi-Rad, Leman Akoglu, Monique Snoeck, 和 Bart Baesens. “GOTCHA! 基于网络的社保欺诈检测.” 管理科学 63:9 (2017): 3090-3110.
^(1.)受 mng.bz/dmYQ 启发。
^(2.)参见 mng.bz/rmMX 和 mng.bz/VG65.
^(3.)如果在查询后未在 Neo4j 浏览器中看到值,请在查询下方选择标签 Transaction;然后选择金额作为要可视化的属性。
^(4.)www.elastic.co/blog/significant-terms-aggregation
^(5.)mng.bz/A17W
^(6.)mng.bz/ZYvZ
^(7.)github.com/neo4j/graph-data-science
^(8.)使用常规的 MATCH (n) DETACH DELETE n 命令整理数据库。
^(9.)附录 B 包含安装和配置插件的说明。
9 基于邻近度的算法
本章涵盖
-
基于异常检测的高级算法对抗欺诈
-
使用图存储和分析交易的 k-NN
-
识别异常交易
第八章通过展示基于识别数据中显式关系的两种方法介绍了欺诈检测技术。在第一种情况下,每笔交易将持卡人与使用卡的商家连接起来。在第二种情况下,银行或信用卡账户通过所有者的个人或访问详情(电话号码、地址、IP 等)连接起来。但在大多数情况下,这种关系并不明显,在这些情况下,我们需要做更多的工作来推断或发现数据项之间的连接或关系,以检测和对抗欺诈。
本章探讨了用于对抗欺诈的高级算法,这些算法借鉴了异常检测理论,能够在看似独立的大型交易数据集中识别异常项。正如我在第八章中提到的,异常检测是数据挖掘的一个分支,涉及在数据集中发现罕见事件或异常值。当你分析大型且复杂的数据集时,确定数据中的突出点至少与了解其一般结构一样重要和有趣。
为了解决异常检测问题,已经开发了许多技术和算法 [Akoglu et al. 2014],主要关注在非结构化的多维数据点集合中检测异常——即每个数据点都可以用一个向量表示的数据集。这些技术将数据对象视为位于多维空间中的独立点,但现实情况是,在许多场景中,它们可能表现出应考虑在异常检测过程中的相互依赖性。在广泛的学科领域——如物理学、生物学、社会科学和信息系统——数据实例实际上本质上相关。正如我们所见,图提供了一种强大的工具,可以有效地捕捉相互依赖数据对象之间的长期相关性。
本章继续我们关于在异常检测领域使用图来对抗欺诈的研究。首先,我们将使用图构建技术来创建一个图;然后,我们将分析这个图以揭示异常交易。这里使用的算法并不新颖,但示例清晰地展示了图如何帮助我们更好地可视化和导航数据,简化分析过程。
9.1 基于邻近度的算法:简介
假设你想要识别可疑的信用卡交易以避免客户被收取未经授权的付款。在分析运营数据时,你需要识别偏离正常用户行为的交易并将它们标记为可能欺诈。
在这个阶段,场景应该是清晰的:你被分配的任务是在信用卡操作列表中识别欺诈或至少异常的交易。目标是浏览数据并编制一份需要进一步检查的交易清单。相反,当请求新的交易时,系统应该评估是否接受或拒绝它。值得注意的是,在这种情况下,可用的数据与我们在第八章中考虑的场景不同。在这里,我们有一个巨大的特征集,每个交易都有很多特征,而之前我们只有几个。这种场景更现实,因为通常信用卡公司会收集每个交易的大量信息,以便尽可能准确地将其分类为欺诈。
即使目的在这里也有所不同。之前,我们试图确定用户信用卡详细信息被盗或购买发生的地点,以测试被盗凭证。在这种情况下,我们的目标是识别异常交易。当系统将交易标记为可疑时,卡片将被锁定,直到进行进一步的调查和分析。
显然,这种场景需要不同的方法。可用的数据是以一系列交易的形式存在的,通过一系列属性来描述,如卡 ID、时间、金额、位置、商户 ID 以及信用卡系统收集的其他信息。关于个人用户和商户的信息很少或没有。此外,与早期示例相比,数据量很大。
由于数据量很大且缺乏明确的关系,我们在第八章中探讨的技术不能在这里应用。相反,本章介绍了一种新的欺诈检测技术,它使用来自异常检测领域的定义良好的方法:基于邻近度的方法。我们还将使用图表示来建模和导航数据,以提高分析和性能。你会发现用于从数据建模图的方法是熟悉的,因为它使用了本书前面讨论的一些图构建技术。本章展示了这种方法的灵活性以及它们如何适应多个上下文和用例。我将快速介绍这些选项,然后将其中最合适的一个应用到我们的场景中。
基于邻近度的技术将数据点定义为异常值,当它与其他数据点距离异常遥远时。更复杂地说,它的局部性(或邻近度)是稀疏的。不同的算法使用不同的机制来定义数据点的邻近度。这些方法在细节上有所不同,但它们背后的概念足够相似,值得将它们统一为几个组。用于定义异常分析中邻近度的最常见方法是由 Aggarwal 在 2016 年提出的。
-
基于聚类的—使用最合适的技巧将数据点划分为聚类,考虑到元素如何表示以及算法应该有多精确。通过使用数据点在任意聚类中的非成员资格、与其他聚类的距离、最近聚类的尺寸或这些因素的组合来计算异常值得分。点属于聚类或应被视为异常值。
-
基于密度的—为每个数据点定义一个局部区域(可能基于网格位置),并使用该区域中其他点的数量来定义一个局部密度值。此值可以转换为异常值得分,得分较高的元素被认为是异常值。基于密度的异常值检测方法的基本假设是,非异常对象周围的局部密度与其邻居周围的局部密度相似,而异常对象周围的局部密度与其邻居周围的局部密度显著不同。与基于聚类的方 法划分数据点不同,基于密度的方法划分数据空间。
-
基于距离的—对于每个数据点,计算 k-近邻(k-NN)网络(是的,就是之前用于推荐的同一个网络)。通过使用数据点到其 k-近邻的距离来计算异常值得分;具有最大 k-NN 距离的数据点被标记为异常值。基于距离的算法通常比这里介绍的其他方法表现更好,因为它们具有更高的粒度。在基于聚类和密度的方法中,在异常值分析之前,通过划分点或数据空间来聚合数据,并将单个数据点与这些分布进行比较以进行分析。在基于距离的方法中,异常值得分基于原始数据点到 k-NN 的距离。这种更高的粒度通常伴随着显著的计算成本,但可以通过使用图和一些其他技术来减轻这种成本。
所有这些技术都是基于某种邻近度(或相似度,或距离)的概念,并且密切相关。主要区别在于定义此距离的详细程度。
我们将重点关注基于距离的机制,因为与其它方法相比,它们更准确。此外,它们不仅适用于图空间,还适用于我迄今为止介绍的技术。尽管本章重点讨论多维数值数据,如信用卡交易,但这些方法已被推广到许多其他领域,例如分类数据、文本数据、时间序列数据和序列数据。
本章的另一个要点是使用 k-NN 网络进行不同于推荐的任务,这展示了该技术在分类目的上的强大能力。在本书的早期,我们探讨了 k-NN 网络在解决不同问题时的不同用途和内在灵活性。本章完成了概述。
9.2 基于距离的方法
为了说明基于距离的异常值检测方法的使用,我们将使用 Kaggle 上可用的匿名信用卡交易数据集。¹ 该数据集包含 2013 年 9 月两日内欧洲持卡人进行的信用卡交易。总共有 284,807 笔交易,其中 492 笔是欺诈交易。该数据集高度不平衡:正类(欺诈)占所有交易的 0.172%。数据集仅包含数值输入变量,这些变量是经过统计转换以使其不相关,以更好地适应我们的场景。由于保密问题,Kaggle 无法提供原始特征和更多关于数据的背景信息。唯一未转换的特征是时间和金额。时间特征包含每个交易与数据集中第一个交易之间的秒数。金额特征是交易金额。类别特征是响应变量;在欺诈的情况下取值为 1,否则为 0。图 9.1 中的模式显示了使用基于距离的方法在这样一份交易列表中识别异常值所需的高级工作流程。

图 9.1 由图驱动的基于距离的方法
第一部分由两个子任务(提取数据和将其存储为图中的节点)组成,这是一种经典的图构建技术。我们在书中已经多次使用它,所以到现在你应该已经很熟悉了。这一步创建了一个如图 9.2 所示的图。

图 9.2 图中存储的交易
让我们更仔细地看看这里涉及的内容。
9.2.1 将交易存储为图
我们首先从原始格式 CSV 中摄取我们的数据,作为图中的节点。你可以在代码仓库的 ch09/import/creditcard 目录下找到导入数据的 Python 脚本和所需依赖。这一步开始了将交易存储为图中的节点的过程,如图 9.3 所示。

图 9.3 我们在心理模型中的位置
下一个列表,来自代码仓库,展示了如何从 Kaggle 摄取数据集并将交易数据转换为向量。
列表 9.1 导入交易的代码
def import_transactions(self, directory):
transactions = pd.read_csv(os.path.join(directory, "creditcard.csv")) ❶
for k in range(50): ❷
writing_thread = threading.Thread(target = self.write_transaction)
writing_thread.daemon = True
writing_thread.start()
j = 0;
for index, row in transactions.iterrows(): ❸
j += 1
transaction = {
'transactionId': j,
'isFraud': row['Class'],
'transactionDt': row['Time'],
'transactionAmt': row['Amount']}
vector = self.normalize(row, ['Time', 'Class']) ❹
transaction['vector'] = vector;
self._transactions.put(transaction); ❺
if j % 10000 == 0:
print(j, "lines processed")
print(j, "lines processed")
self._transactions.join() ❻
print("Done")
def write_transaction(self): ❼
query = """
WITH $row as map
CREATE (transaction:Transaction {transactionId: map.transactionId})
SET transaction += map
"""
i = 0
while True:
row = self._transactions.get()
with self._driver.session() as session:
try:
session.run(query, {"row": row})
i += 1
if i % 2000 == 0:
with self._print_lock:
print(i, "lines processed on one thread")
except Exception as e:
print(e, row)
self._transactions.task_done()
❶ 使用 pandas 从交易.csv 文件中读取
❷ 启动 50 个写入线程。在这种情况下,因为每个交易节点是独立的,所以不会出现任何并发问题。
❸ 遍历文件,创建参数映射
❹ 通过删除无用列并将离散/非数值数据转换为数字(对于这个特定数据集不是必需的,因为数据已经标准化)来创建交易向量。请参阅代码存储库以获取完整实现。
❺ 将交易对象添加到存储元素的队列中
❻ 这个连接等待所有元素被处理。
❼ 通过将每个交易作为数据库中的一个节点存储来处理队列元素
列表中的代码应该很容易理解;它与我们在其他示例中所做的是相似的。唯一相关的区别是,每个节点的向量不需要后来计算,因为它已经在提供的数据中可用。代码提取一些数据(如时间、是否欺诈等)并将其作为交易节点的专用属性。其余的输入数据被放在相同的节点中,作为向量属性,包含浮点向量,这样我们就不需要自己计算它了。
警告:重要的是指出,使用多个线程写入 Neo4j 的使用。这种方法是常见的,但请注意:只有在查询不会引起任何冲突的情况下(换句话说,当查询作用于不同的、不重叠的图部分时),才能安全地使用。这种“隔离”将防止任何与序列化相关的问题,或者更糟糕的是,死锁。第一种问题可以通过延迟重试来解决,但第二种问题并不那么简单,需要更新操作以写入的方式,即使它们并行运行,也会按相同的顺序锁定节点。
在数据摄取结束时,这应该只需要几秒钟,你的数据库将包含 284,807 个节点和没有关系。
练习
在摄取数据后,在 Neo4j(通过 Neo4j 浏览器)上运行一些查询,以查看摄取是否成功。这里有一些建议:
-
计算被标记为“Transaction”的交易数量。这个数字符合你的预期吗?
-
计算被标记为欺诈的交易数量。这个数字符合你的预期吗?
-
从节点获取 10 个向量。你能找到每个向量的维度吗?
-
获取按金额排序的前 10 笔交易。
9.2.2 创建 k 最近邻图
第二步(图 9.4)是通过使用表示它们之间距离的关系连接节点来创建 k-NN 图。

图 9.4 我们在心理模型中的位置
在本书中之前构建的推荐系统中,我们使用了相似度的概念,因为我们正在寻找相似的项目或用户。在这里,我们正在考虑距离,因为我们正在寻找远离合法交易的异常值。这种变化并不大,因为距离通过以下简单的公式与相似度相关联:²
距离 = 1 – 相似度
公式表明,当两个数据点完全相同的时候,它们的相似度是最高可能的:1。因此,它们的距离是 0。另一方面,如果它们完全不同(如果你喜欢,可以认为是垂直的),相似度是 0。在这种情况下,距离将是最高可能的:1。
当 k-NN 图已经计算并存储后,就可以找到异常值——在我们的案例中,是欺诈交易。基于距离的邻近方法通常以分数的形式产生输出,这些分数代表预测一个数据点(在这种情况下,是一个事务)是异常值的概率。通过设置一个阈值,我们可以做出二元决策:每当分数高于固定的阈值时,该交易就被认为是异常值。
到目前为止,一切顺利,但正如我们在本书前面探索的场景中一样,魔鬼在于细节。计算 k-NN 图是计算密集型的,需要 O(N²)的时间。幸运的是,一些技术可以使它更快;在适当的时候,我们将看到其中的一些。
现在我们已经将数据库中的所有事务作为一个节点集,我们必须创建 k-NN 图。为此,我们必须使用我们创建的向量属性计算每个节点与其他所有节点的距离。这些距离是有序的,并且只有前 k 个距离被存储为图中的关系。每个关系还包含距离值作为其权重属性。这个过程的结果如图 9.5 所示。

图 9.5 使用向量信息获得的 k-NN 图
如前所述,正如我们在推荐场景中看到的那样,计算 k-NN 图是一项繁琐的任务,因为它需要计算每对事务之间的距离或相似度。这就是为什么当有大量数据项(事务、呼叫等)时,基于距离的方法使用得较少。创建 k-NN 图并将其持久化为真实图(前一个图的扩展)在访问和分析数据方面有很多优点,但在计算相似度时并不会有所帮助。我想强调这一点,因为正如我在本书开头所说,本书的目标不仅仅是解决玩具示例。数据科学家和数据工程师必须解决需要考虑时间、磁盘/内存空间和结果质量的真实问题。
在第六章中,我们探索了几种技术——局部敏感哈希(LSH)和 Spotify 的 Annoy³——来计算一个近似最近邻(ANN)图,并取得了良好的结果。现在我们将探索另一种比这两种技术表现更好的有趣 ANN 方法。为了清晰起见,由于数据的大小和特定场景对结果质量的要求,其他方法在我们使用它们的场景中表现良好。在欺诈检测用例中,我们需要处理大量的密集向量,近似的质量将影响欺诈检测结果的质量。我建议在这种情况下采用不同的、更复杂的方法来处理 ANN。
与精确的 k-NN 搜索相比,后者搜索其他元素以找到 k 个最近的元素,ANN 搜索通过允许少量错误来放宽条件。它在包含用于搜索的特定向量的元素最接近的子集中进行搜索。
ANN 搜索在过去几十年中一直是一个热门话题,并为许多应用提供了基本支持,从通用数据挖掘到推荐,从数据库索引到信息检索。对于稀疏离散数据(如文档),最近邻搜索可以在高级索引结构(如倒排索引[Manning et al., 2008] ⁴)上有效地进行。对于密集连续向量,如我们欺诈用例中的向量,已经提出了各种解决方案,包括基于树结构的方法、基于哈希的方法(如第六章中描述的 LSH 方法)、基于量化的方法、基于图的方法等。为了减少索引复杂性,已经提出了基于近似传统图的方法。最近,这些方法在百万规模数据集上展示了革命性的性能[Fu et al., 2019]。这些方法不仅因为它们基于图,因此与本书相关,而且因为从性能的角度来看,它们代表了当前的最佳水平。⁵ 不深入细节,我可以描述这些方法背后的基本思想分为两大步:
-
创建基于图的索引结构。在这个阶段,创建一个邻近图(或邻居图)。在这个图中,节点是多维空间中的向量,节点之间的边是通过使用基于特定距离函数的逻辑和导航标准创建的。如果根据使用的距离函数,两个节点可能是邻居,则它们之间连接。
-
搜索邻居。给定一个特定的向量或搜索查询,根据算法的不同,通过不同的方式在邻近图中导航,以找到输入向量的最近邻居候选。
我选择了分层可导航小世界(HNSW)实现[Malkov and Yashunin, 2016]作为此场景的 ANN 方法。根据 Erik Bernhardsson 网站和 Aumüller 等人[2018]提供的基准,它是其中最好的之一,Python 包装器⁶的使用非常简单。以下列表展示了如何计算和存储 k-NN 图。
列表 9.2 计算和存储距离
def compute_and_store_distances(self, k, exact, distance_function,
➥ relationship_name): ❶
start = time.time()
data, data_labels = self.get_transaction_vectors() ❷
print("Time to get vectors:", time.time() - start)
start = time.time()
if exact: ❸
ann_labels, ann_distances = self.compute_knn(data, data_labels, k,
➥ distance_formula)
else:
ann_labels, ann_distances = self.compute_ann(data, data_labels, k,
➥ distance_formula)
print("Time to compute nearest neighbor:", time.time() - start)
start = time.time()
self.store_ann(data_labels, ann_labels, ann_distances, relationship_name)
print("Time to store nn:", time.time() - start)
def store_ann(self, data_labels, ann_labels, ann_distances, label): ❹
clean_query = """
MATCH (transaction:Transaction)-[s:{}]->()
WHERE transaction.transactionId = $transactionId
DELETE s
""".format(label)
query = """
MATCH (transaction:Transaction)
WHERE transaction.transactionId = $transactionId
UNWIND keys($knn) as otherSessionId
MATCH (other:Transaction)
WHERE other.transactionId = toInteger(otherSessionId) and
➥ other.transactionId <> {transactionId}
MERGE (transaction)-[:{} {{weight: $knn[otherSessionId]}}]->(other)
""".format(label)
with self._driver.session() as session:
i = 0;
for label in data_labels:
ann_labels_array = ann_labels[i]
ann_distances_array = ann_distances[i]
i += 1
knnMap = {}
j = 0
for ann_label in ann_labels_array:
value = np.float(ann_distances_array[j]);
knnMap[str(ann_label)] = value
j += 1
tx = session.begin_transaction()
tx.run(clean_query, {"transactionId": label})
tx.run(query, {"transactionId": label, "knn": knnMap})
tx.commit()
if i % 1000 == 0:
print(i, "transactions processed")
❶ 用于根据参数集计算和存储距离的函数
❷ 创建在距离计算期间使用的向量的函数
❸ 在精确和近似 k-NN 之间切换
❹ 存储 k-NN 图的函数
此代码已实现,允许创建 ANN 和 k-NN 图。信用卡数据集足够小,可以在 k-NN 方法中管理,因此为了测试不同的解决方案,我使用了列表 9.2 中的代码,使用了不同的距离函数和两种方法:近似和精确。表 9.1 包含了函数的简要描述。
表 9.1 测试期间使用的不同距离函数
| 名称 | 公式 |
|---|---|
| 平方 L2 | L2(也称为欧几里得)距离是欧几里得空间中两点之间的直线距离。平方 L2(L2 的平方形式)在估计统计模型的参数中具有核心重要性,在其中它被用于最小二乘法,这是一种标准的回归分析方法。 |
| 马氏距离⁷ | 马氏距离是一个点到分布的距离,而不是两个不同点之间的距离。它实际上是欧几里得距离的多变量等效。它在多元异常检测、高度不平衡数据集的分类、单类分类以及更多不常见用例中具有出色的应用。 |
列表 9.3 和 9.4 详细展示了近似和精确最近邻计算的实施方式。切换是通过在列表 9.2 中的 compute_and_store_distances 函数中设置精确参数来实现的。
列表 9.3 计算近似最近邻的函数
def compute_ann(self, data, data_labels, k, distance_function):
dim = len(data[0])
num_elements = len(data_labels)
p = hnswlib.Index(space=distance_function, dim=dim) ❶
p.init_index(max_elements=num_elements, ef_construction=400, M=200) ❷
p.add_items(data, data_labels)
p.set_ef(200) ❸
labels, distances = p.knn_query(data, k = k) ❹
return labels, distances
❶ 声明索引
❷ 初始化索引。应事先知道元素的最大数量,以便在要加载的数据上计算索引。
❸ 设置查询时间精度/速度权衡,定义 ef 参数,该参数应始终大于 k 且小于 num_elements。ef 指的是最近邻动态列表的大小(在搜索期间使用)。更高的 ef 导致搜索更精确但更慢。
❹ 查询数据集,k 个最近元素(返回 2 个 numpy 数组)
列表 9.4 计算精确最近邻的函数
def compute_knn(self, data, data_labels, k, distance_function):
pre_processed_data = [np.array(item) for item in data]
nbrs = NearestNeighbors(n_neighbors=k, algorithm='brute', metric=
➥ distance_function, n_jobs=-1).fit(pre_processed_data)
knn_distances, knn_labels = nbrs.kneighbors(pre_processed_data)
distances = knn_distances
labels = [[data_labels[element] for element in item] for item in
➥ knn_labels]
return labels, distances
在执行列表 9.2 中的代码(完整代码可在代码仓库中找到)并使用表 9.2 中的参数后,你将得到你的近似最近邻图。
表 9.2 创建近似最近邻图所使用的参数值
| 参数 | 值 |
|---|---|
| 精确 | False |
| k | 25 |
| 距离函数 | L2 |
| 关系名称 | DISTANT_FROM |
你可以从 Neo4j 浏览器运行一个简单的查询来检查新的图数据库。
列表 9.5 可视化 KNN(近似)图的一部分
MATCH p=(:Transaction)-[:DISTANT_FROM]-(:Transaction)
RETURN p
LIMIT 100
查询的结果将类似于图 9.6,这是从 Neo4j 浏览器中截取的屏幕截图。

图 9.6 列表 9.5 的结果
在继续之前,分析近似最近邻图和精确最近邻图之间的差异是值得的。通过在列表 9.2 中运行 Python 脚本⁸,并使用表 9.3 中的参数,你可以得到一个精确最近邻图。
表 9.3 创建精确最近邻图所使用的参数值
| 参数 | 值 |
|---|---|
| 精确 | True |
| k | 25 |
| 距离函数 | L2 |
| 关系名称 | DISTANT_FROM_EXACT |
执行脚本的结果是一个包含近似和精确最近邻图的图。(近似图是使用之前的运行计算的。)现在比较这些图不再比对其运行简单查询更复杂。以下列表显示了比较图的查询。
列表 9.6 比较最近邻图
MATCH (t:Transaction) with t
MATCH (t)-[r:DISTANT_FROM]->(ot:Transaction)
WITH t, r.weight as weight, ot.transactionId as otherTransactionId
ORDER BY t.transactionId, r.weight
WITH t, collect(otherTransactionId) as approximateNearestNeighbors
MATCH (t)-[r: DISTANT_FROM_EXACT]->(ot:Transaction)
WITH t.transactionId as transactionId, approximateNearestNeighbors, r.weight
➥ as weight, ot.transactionId as otherTransactionId
ORDER BY t.transactionId, r.weight
WITH transactionId, collect(otherTransactionId) = approximateNearestNeighbors
➥ as areNeighborsSimilar
WHERE areNeighborsSimilar = true
RETURN count(*)
如果你不想等待创建第二个图并亲自进行实验,我可以透露这个查询将返回超过 230,000(总共有 284,807 笔交易)的计数,这意味着准确率高于 80%。这个结果是近似最近邻计算产生的邻居列表与精确列表匹配的百分比。如果你检查剩余的 20%左右,你会发现差异很小。不是最小的是计算精确最近邻图所需的时间,这需要 N × N 的距离计算。ANN 方法(特别是 HNSW 提供的基于图的 ANN 实现)不仅在准确性方面表现良好,而且显著减少了计算最近邻图所需的时间。因此,这里提出的方法可以在具有真实数据集的生产环境中使用。
9.2.3 识别欺诈交易
最后一步(图 9.7)是识别欺诈交易。基于距离的方法——特别是我们在此处详细讨论的最近邻方法——如果一个观察值(在我们的场景中是一个交易)的邻居很远,那么它被认为是一个异常值,或者可能的欺诈。因此,可以通过使用节点与其最近邻的距离来计算异常度量的异常分数:分数越高,异常的可能性就越高。

图 9.7 我们在心理模型中的位置
存在多种方式来衡量每个观察值的分数,这些分数基于其 k-NN。这种评分机制的两种简单变体如下:
-
精确 k 近邻分数——任何数据观察值的异常值分数等于其与第 k 个最近邻的距离。因此,如果 k = 5,且 ID 为 32 的交易的 k-NN 为 [45: 0.3, 34: 0.45, 67:0.6, 50: 0.75, 21: 0.8],则第二个最近邻的分数是 0.45(列表中第二个节点的距离),第三个最近邻的分数将是 0.6,依此类推。
-
平均 k 近邻分数——任何观察值的异常值分数等于其到其 k 个最近邻的平均距离。因此,在前面的例子中,ID 为 32 的交易的分数将是 0.58((0.3 + 0.45 + 0.6 + 0.75 + 0.8) / 5)。
一般而言——相信我,这个事实也让我感到惊讶——如果我们知道正确的 k 值,查看精确的 k-NN 分数往往比平均 k-NN 分数给出更好的结果。然而,在无监督问题如异常值检测中,我们无法知道任何特定算法应使用的正确 k 值,分析师可能会使用一系列的 k 值。此外,当数据集中包含具有不同密度的多个簇时,定义一个单一的理想 k 值是一个复杂(如果不是不可能)的任务。
在这个阶段,因为 k-NN 图已存储在我们的图数据库中,计算分数和评估此处提出的方法的性能只需运行几个查询。重点应放在识别可能欺诈的交易并将它们传递给分析师进行进一步验证。
在开始更深入的分析之前,让我们评估整个数据集中节点与其 k 个最近邻的平均距离的分布情况。再次强调,图形表示——特别是,使用 APOC⁹ 插件的 Neo4j——可以极大地帮助。使用以下查询,可以导出一个包含交易和每个交易与其 k 个最近邻的平均距离的 .csv 文件。查询展示了如何为被识别为可能和不太可能欺诈的交易分别执行此操作。
列表 9.7 创建欺诈交易的平均距离 .csv 文件
CALL apoc.export.csv.query('MATCH (t:Transaction)-[r:DISTANT_FROM]->
➥ (other:Transaction)
WHERE t.isFraud = 1
RETURN t.transactionId as transactionId, avg(r.weight) as weight',
➥ 'fraud.csv',
{batchSize:1000, delim: '\t', quotes: false, format: 'plain', header:true})
列表 9.8 创建非欺诈交易的平均距离 .csv 文件
CALL apoc.export.csv.query('MATCH (t:Transaction)-[r: DISTANT_FROM]->
➥ (other:Transaction)
WHERE t.isFraud = 0
RETURN t.transactionId as transactionId, avg(r.weight) as weight',
➥ 'noFraud.csv',
{batchSize:1000, delim: '\t', quotes: false, format: 'plain', header:true})
结果已使用 Microsoft Excel(文件位于代码仓库的 ch09/analysis/analysis.xlsx 目录下)进行分析,并在图 9.8 中展示。条形图比较了两个数据集(欺诈交易与非欺诈交易)的平均距离分布。一般来说,我们预计欺诈交易的平均距离将高于非欺诈交易。在图中,欺诈交易应位于右侧(距离更大)且左侧(距离更小)的值更高。

图 9.8 比较欺诈和非欺诈交易的平均距离分布图
在这种情况下,现实符合我们的预期:表示非欺诈交易的虚线柱状图在左侧更高,表示欺诈交易的斜线条纹柱状图在右侧更高。直到平均距离约为 20 时,非欺诈柱状图高于欺诈柱状图,从下一个值(54.6)开始,趋势逆转。虚线柱状图的峰值在约 7 处,我们有 33%的值(即大约 284,000 笔合法交易中有三分之一的交易的平均 k-NN 距离在 2.72 到 7.39 之间)。大约 78%的这些交易的平均分数在 2.72 到 20.09 之间。斜线条纹柱状图的峰值在 403 处,我们可以看到 72%的欺诈交易的平均 k-NN 距离在 54 到 403 之间。这个结果意味着非欺诈交易与最近邻的平均距离在统计上低于欺诈交易与最近邻的平均距离。
现在我们确信我们使用的距离度量标准表现良好,并且数据的行为符合我们的预期,让我们继续进行更深入的分析。为此目的提出的第一个查询是按 k 最近邻的最小距离对交易进行排序,这意味着在 k-NN 分数机制中 k=1。
列表 9.9 按最小距离计算潜在的欺诈交易
MATCH (t:Transaction)-[r:DISTANT_FROM]->(:Transaction)
WITH t, min(r.weight) as score, t.isFraud as fraud
ORDER BY score DESC
LIMIT 1000
WHERE fraud = 1
RETURN COUNT(distinct t)
此查询返回 118 个计数,¹⁰,这意味着在按降序分数排序的前 1000 笔交易中,有 11.8%是欺诈的。这个值可能看起来很低,但实际上并不是。回想一下,在总共 284,807 笔交易的数据集中有 492 笔欺诈交易,这意味着只有 0.17%的交易是欺诈的。这个结果加强了我们这样的直觉:交易与其最近邻的距离越远,交易是欺诈的可能性就越高。这个距离是从列表 9.9 开始定义的分数,以及相关的描述中的分数。它将以不同的方式计算,但在所有情况下,分数越高,交易是欺诈的可能性就越高。以下是一个更通用的查询,你可以更改 k 的值,考虑你喜欢的最近邻列表中的任何元素。
列表 9.10 按四邻域距离计算潜在的欺诈交易
MATCH (t:Transaction)-[r:DISTANT_FROM]->(:Transaction)
WITH t.transactionId as transactionId, r.weight as weight, t.isFraud as fraud
ORDER BY transactionId ASC, weight
WITH transactionId, fraud, collect(weight)[3] as score ❶
ORDER BY score DESC
LIMIT 1000
WHERE fraud = 1
RETURN COUNT(distinct transactionId)
❶ 3 可以是小于 k 的任何值。(collect 生成的向量从 0 开始,所以 k=1 是 0 处的元素,k=2 是 1 处的元素,依此类推。)
在这个查询中,我们将向量位置固定为 3(这意味着我们的 KNN 中的第四个元素,因为 collect 创建了一个基于 0 的向量)。因此,在这个查询中,我们根据每笔交易到其自身 KNN 中第四个元素的距离来分配分数。提醒一下,我们将 k(要考虑的最近邻数量)设置为 25(见表格 9.2 和 9.3);这个值可以是小于 25 的任何值。
在这种情况下,查询返回 111,这意味着结果列表中有 11.1% 的交易是欺诈的。这个结果略低于之前,但仍然远高于 0.17%——这是整个数据集中欺诈交易的比例。
这些查询生成的结果很好。为了证明,运行以下查询,它生成一个随机分数。
列表 9.11 生成 1,000 笔交易的随机列表
MATCH (t:Transaction)-[r:DISTANT_FROM]->(:Transaction)
WITH t, rand() as score, t.isFraud as fraud
ORDER BY score desc
LIMIT 1000
WHERE fraud = 1
RETURN COUNT(distinct t)
查询为每笔交易分配一个随机分数,无论其与 KNN 中任何元素的距离如何。由于交易列表是随机生成的,这个查询的结果可能会有所不同,但你应该发现查询通常返回 2 或 3,这相当于 0.2% 到 0.3%。这个结果与整体数据集中欺诈交易分布相一致,即 0.17%。
这个实验设定了我们的基线。让我们尝试更多的实验。使用以下查询,我们通过使用所有 k 个最近邻的平均距离来分配分数,而不是像之前那样选择一个邻居的距离。
列表 9.12 使用平均距离计算交易的分数
MATCH (t:Transaction)-[r:DISTANT_FROM]->(:Transaction)
WITH t, avg(r.weight) as score, t.isFraud as fraud
ORDER BY score DESC
LIMIT 1000
WHERE fraud = 1
RETURN COUNT(distinct t)
结果是 86——比列表 9.9 的结果更差,在列表 9.9 中,用于排序的分数是基于到 k 个最近邻的最小距离,在我们的测试中是 118。这个结果是预期的,因为我之前提到,对于大多数数据集来说,查看精确 k-NN 分数通常比近似 k-NN 给出更好的结果。尽管如此,这种技术比随机选择交易表现得要好得多。
图可以帮助进行深入分析,我们可以利用这个机会来发现更多关于可用选项的见解。一个有趣的可能是通过使用精确 k-NN 图来测试列表 9.9 和列表 9.12。以下列表展示了如何进行。
列表 9.13 使用精确 k-NN 图和最小距离计算分数
MATCH (t:Transaction)-[r:DISTANT_FROM_EXACT]->(:Transaction)
WITH t, min(r.weight) as score, t.isFraud as fraud
ORDER BY score DESC
LIMIT 1000
WHERE fraud = 1
RETURN COUNT(distinct t)
列表 9.14 使用精确 k-NN 图和平均距离计算分数
MATCH (t:Transaction)-[r: DISTANT_FROM_EXACT]->(:Transaction)
WITH t, avg(r.weight) as score, t.isFraud as fraud
ORDER BY score DESC
LIMIT 1000
WHERE fraud = 1
RETURN COUNT(distinct t)
这些查询分别返回 81 和 72,因此精确方法的结果比近似方法更差,这可能是因为 HNSW 提供的基于图的大致近似去除了数据中的部分噪声。
练习
尝试调整参数值(k、精确值、距离函数和关系名称)并执行不同的查询,看看你是否能获得比这里展示的更好的结果。我自己也进行了这个实验,并发现了一些有趣的结果。例如,以下查询考虑了第 24 个最近邻的得分(k = 23),使用精确方法和 Mahalanobis 距离度量。
列表 9.15 使用 Mahalanobis 精确 24-NN 距离计算得分
MATCH (t:Transaction)-[r:DISTANT_FROM_EXACT_MAHALANOBIS]->(:Transaction)
WITH t.transactionId as transactionId, r.weight as weight, t.isFraud as fraud
ORDER BY transactionId ASC, weight ASC
WITH transactionId, fraud, collect(weight)[23] as weight
ORDER BY weight DESC
LIMIT 1000
WHERE fraud = 1
RETURN COUNT(distinct transactionId)
查询返回 147,这意味着前 14.7%的结果可能是欺诈交易。这个例子再次表明,在考虑的数据库中,k-最近邻方法比平均距离得分表现更好,至少在当前的 k 值(25)和当前的距离度量(Mahalanobis)下是这样。我使用表 9.4 中的参数进行了另一个有趣的测试,以查看增加 k 值会有什么影响。
表 9.4 用于计算精确 400-NN 的 Mahalanobis 参数值
| 参数 | 值 |
|---|---|
| 精确值 | True |
| k | 400 |
| 距离函数 | MAHALANOBIS |
| 关系名称 | DISTANT_FROM_EXACT_400_MAHALANOBIS |
运行 Python 脚本会花费更长的时间,因为需要额外的时间来在图中存储 k-NN,但现在我们可以考虑每个交易的 400 个最近邻,而不是 25 个。以下查询使用每个交易的 400 个邻居列表的平均得分来生成列表。
列表 9.16 使用 Mahalanobis(k = 400)计算平均距离得分
MATCH (t:Transaction)-[r:DISTANT_FROM_EXACT_400_MAHALANOBIS]->(:Transaction)
WITH t.transactionId as transactionId, avg(r.weight) as score, t.isFraud as
➥ fraud
ORDER BY score desc
LIMIT 1000
WHERE fraud = 1
RETURN COUNT(distinct transactionId)
结果是 183,这意味着前 18.3%的结果被分类为欺诈——这无疑是迄今为止最好的结果!我还测试了 L2 和近似最近邻方法,结果是 2。这个例子展示了参数如何影响你分析的结果。
你可能认为这些结果并不那么好;你希望看到更高的精确度,大约 80%,这是完全合理的。最初我也有同样的想法。但是,重要的是要考虑数据集的不平衡性。我们有超过 280,000 笔交易,其中只有 492 笔是欺诈的。在这些条件下,我们的查询表现不会更好。然而,我们可以使数据集更加平衡,使得欺诈交易的数量与合法交易的数量相同。这种方法使我们能够更好地评估迄今为止使用的评分机制的质量。以下查询通过随机选择所有可用的合法交易中的 492 笔(欺诈交易的数量)并添加到数据集中,创建了一个平衡的数据集。数据集是通过标记数据集中的所有交易来创建的。
列表 9.17 生成与欺诈集大小相同的随机交易集
MATCH (t:Transaction)
WHERE t.isFraud = 0
WITH t, rand() as rand
ORDER BY rand
LIMIT 492
SET t:Test1
列表 9.18 将欺诈交易分配到相同的测试集
MATCH (t:Transaction)
WHERE t.isFraud = 1
SET t:Test1
之前的查询可以重复运行以创建多个测试数据集(将 Test1 改为 Test2、Test3 等)。每种情况的结果都是一个大小为 984 的同质数据集,欺诈交易和合法交易的数目平衡。现在,使用类似的查询,让我们看看使用平均距离如何帮助我们捕捉到欺诈交易。
列表 9.19 为小数据集分配分数
MATCH (t:Test1)-[r:DISTANT_FROM]->(:Test1)
WITH t.transactionId as transactionId, avg(r.weight) as score, t.isFraud as
➥ fraud
ORDER BY score desc
LIMIT 100
WHERE fraud = 1
RETURN COUNT(distinct transactionId)
结果是 100。按分数降序排列的前 100 笔交易是欺诈交易。
所有这些证据——图 9.8 的图表、按平均和其他指标排序的前 1,000 笔交易的测试,以及这个在减少但平衡的数据集上的最后测试——展示了基于距离的方法如何能够识别欺诈交易。请记住,所谓的合法交易是指尚未证明为欺诈的交易,因此结果的质量可能比这里展示的还要高。
练习
我邀请您调整参数以找到适合您数据集的最佳配置。具体来说,距离函数和 k 参数的大小会影响最终结果。从这个意义上说,图方法将使您的分析工作变得愉快且简单。您可以存储和比较解决方案,就像我们在本节中做的那样。在同一个图中玩多个 k-NN 是令人兴奋的——至少对我来说是这样的!
这种基于分数的方法可以轻松地转换为产生二进制输出。在这种情况下,我们不再有一个表示交易欺诈可能性的分数,而是一个二进制输出,表示“欺诈”或“非欺诈”。将基于分数的机制转换为二进制机制是一个简单的任务。为此目的,已经提出了两种主要方法
-
基于分数阈值的距离异常值 [Knorr 和 Ng,1998]——一个观察(在我们的情况下,是一笔交易)是异常值,如果至少有 f 部分对象在完整数据集中位于距离 β 以上的位置。如果数据集有 100 笔交易,f = 0.1(10%)且 β = 0.5,那么一笔交易是异常值,如果至少有 10 笔其他交易的距离高于 0.5。请注意,参数 f 在实际中相当于使用 k-NN 分数中的 k 参数。我们可以通过设置 k = ⌈ N × (1 − f) ⌉ 来使用确切的第 k 个最近邻距离,而不是使用分数 f。在这种情况下,我们可以将条件重新表述如下:数据集中的观察值是异常值,如果其确切的第 k 个最近邻距离至少是 β。参见列表 9.20。
-
基于排名阈值的距离异常值 [Ramaswamy 等人,2000]——这个第二个定义是基于 top-r 阈值而不是分数绝对值的阈值。这正是我们在先前的查询中所做的,我们将阈值设置为 1,000。观察值(在我们的案例中是交易)按精确 k-NN 距离或平均 k-NN 距离的降序排列。这些 top-r 数据点被报告为异常值。因此,阈值是在距离排名而不是距离值上。参见列表 9.21。
值得注意的是,这两种变体都与基于距离的方法相关。唯一改变的是计算异常值的分数或排名的方式:基础邻域图仍然是计算的基础图。之前使用的查询可以很容易地调整,以返回基于排名阈值的异常值,如下所示。
列表 9.20 基于排名阈值的查询
MATCH (t:Transaction)-[r:SIMILAR_TO_EXACT_400_MAHALANOBIS]->(:Transaction)
WITH t.transactionId as transactionId, avg(r.weight) as score, t.isFraud as
➥ fraud
ORDER BY score desc
LIMIT 100
WHERE fraud = 1
RETURN transactionId
在这个查询中,阈值(r 值)被设置为 100,但它可以是任何您喜欢的值。关于基于分数阈值的异常值,之前的查询可以按以下方式调整。
列表 9.21 基于分数阈值的查询
MATCH (t:Transaction)-[r:DISTANT_FROM_EXACT]->(:Transaction)
WITH t.transactionId as transactionId, r.weight as weight, t.isFraud as fraud
ORDER BY transactionId ASC, weight ASC
WITH transactionId, fraud, collect(weight)[23] as weight
WHERE weight > 10
WITH transactionId, fraud, weight
ORDER BY weight DESC
LIMIT 100
WHERE fraud = 1
RETURN transactionId
如前所述,k-NN(精确或近似)图允许您在几乎不费力和最小磁盘空间的情况下,在同一个数据库上使用多种方法。
9.2.4 图方法的优势
本节介绍了用于欺诈检测的最强大的基于邻近度的技术之一。我们看到了单个图模型如何能够为多个算法/方法提供有效的支持。特别是,图使我们能够做到以下几点:
-
通过提供对每个 k 近邻的直接访问来正确索引 k-NN 图
-
通过使用不同的距离函数和关系类型存储多个 k-NN 图,精确和近似,使比较和分析变得简单
-
探索和评估多种分数机制(多亏了查询和访问机制提供的灵活性),以便能够识别最佳方案并将其用于进一步分析
-
使用标签标记不同的节点集,并使用它们来计算创建的模型的准确性
基于邻近度的技术是异常检测中确立的技术,本章中的例子展示了如何将这些经典技术与图结合,使它们更加强大且易于使用。
摘要
本章介绍了一种用于欺诈检测和分析的高级技术。图构建技术和异常检测的结合突出了图模型在支持数据调查和深度分析中的价值。介绍了不同的方法,其中图的作用对于提供高质量的分析结果以及一个能够扩展到实际生产就绪解决方案的基础设施至关重要。您学习了
-
如何从事务中构建 k-NN 图,以及如何将这些技术应用于新领域中的不同任务。
-
如何在基于距离的异常值检测方法中使用图模型
-
如何同时使用多个距离度量以及多种方法,并通过不同的关系将结果合并到单个图中
-
如何通过不同的查询识别图中的异常节点
本章中的示例和代码展示了从数据导入到分析最终结果的端到端方法,使用来自异常值分析理论的算法。
参考文献
[Aggarwal, 2016] Aggarwal, Charu C. Outlier Analysis. New York: Springer, 2016.
[Akoglu et al., 2014] Akoglu, Leman, Hanghang Tong, and Danai Koutra. “Graph-Based Anomaly Detection and Description: A Survey.” arXiv preprint arXiv:1404.4679 (2014).
[Aumüller et al., 2018] Aumüller, Martin, Erik Bernhardsson, and Alexander Faithfull. “ANN-Benchmarks: A Benchmarking Tool for Approximate Nearest Neighbor Algorithms.” arXiv preprint arXiv:1807.05614 (2018).
[Fu et al., 2019] Fu, Cong, Chao Xiang, Changxu Wang, and Deng Cai. “Fast Approximate Nearest Neighbor Search with the Navigating Spreading-out Graph.” Proceedings of the VLDB Endowment (2019): 461-474.
[Knorr and Ng, 1998] Knorr, Edwin M., and Raymond T. Ng. “Algorithms for Mining Distance-Based Outliers in Large Datasets.” Proceedings of the 24th International Conference on Very Large Data Bases (1998): 392-403.
[Malkov and Yashunin, 2016] Malkov, Yu. A., and D. A. Yashunin. “Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs.” arXiv preprint arXiv:1603.09320 (2016).
[Manning et al., 2008] Manning, C. D., P. Raghavan, H. Schutze, et al. Introduction to Information Retrieval. Cambridge, UK: Cambridge University Press, 2008.
[Ramaswamy et al., 2000] Ramaswamy, Sridhar, Rajeev Rastogi, and Kyuseok Shim. “Efficient Algorithms for Mining Outliers from Large Data Sets.” ACM SIGMOD Record 29:2 (2000), 427-438.
^ (1.)www.kaggle.com/mlg-ulb/creditcardfraud.
^ (2.)可以将公式用作相似度值在[0, 1]范围内的情况。否则,必须通过例如使用相似度/max(相似度值)来归一化相似度值。
^ (3.)github.com/spotify/annoy.
^ (4.)倒排索引数据结构是所有典型搜索引擎索引算法的核心组件。一个倒排索引包括所有出现在任何文档中的唯一单词列表,以及对于每个单词,一个列出它出现的文档列表。这种数据结构也是信息检索过程中最常用的一个。
^ (5.)mng.bz/veDM.
^ (6.)github.com/nmslib/hnswlib.
(7.)一篇关于马氏距离重要性和功能性的有趣文章可在mng.bz/4MEV找到。
(8.)根据你运行脚本的硬件,完成可能需要很长时间。
(9.)mng.bz/Q26j。附录 B 展示了如何安装和配置它。
(10.)结果可能会根据在人工神经网络中使用的方法略有不同。
10 社交网络分析对抗欺诈
本章涵盖
-
使用社交网络分析(SNA)对欺诈者和欺诈风险进行分类
-
描述基于社交网络分析(SNA)的欺诈分析的不同图算法
-
使用真实的图数据库执行适当的社交网络分析
在本章中,你将了解从不同角度应对欺诈的技术。第八章和第九章中介绍的欺诈斗争技术使用不同的图构建方法,根据交易本身和/或用户账户中可用的信息创建网络。在第八章中,我们通过使用交易信息创建了一个连接用户和商家的图,并探讨了基于重叠信息连接节点(例如,使用相同电子邮件地址的两个账户)。在第九章中,你学习了如何通过计算成对观察之间的距离(每个观察都已被转换为节点)并存储前 k 个关系来构建一个图(k-NN 图)。
在本章中,我们将考虑以下情况:一个图——具体来说,是一个社交网络——要么隐含地,要么明确地存在于我们为欺诈分析收集的数据中。正如你在本书的第一部分所学,如果一个图中的节点是人,而边表示人与人之间的关系(友谊、家庭、工作联系等),那么这个图被认为是社交网络。这样的网络可以从现有的显式社交网络中导入,例如 Facebook、LinkedIn 和 Twitter,或者可以从内部数据中创建。例如,电信提供商拥有大量的交易数据库,其中记录了他们客户之间的通话。假设关系更强的人比相对陌生的人更频繁地互相通话,那么可以使用此类信息来创建社交网络,并根据通话的频率和/或持续时间分配人与人之间联系的力量。其他可以推断出社交网络的例子包括互联网基础设施提供商收集的数据(来自同一 IP 地址的人连接)、银行(人们之间周期性的重复交易)、零售组织(一个人向另一个人的地址发送送货)以及在线游戏行业(人们从同一地址或同一团队定期玩游戏)。
无论社会网络是隐式创建还是显式创建,问题在于我们的欺诈检测模型是否可能从社会网络分析(SNA)或复杂网络分析(CNA)中受益。换句话说,人与人之间的关系在欺诈中是否扮演着重要角色,欺诈是否是网络中的传染效应?欺诈者是否在社会网络中随机分布,或者是否存在可观察的效果表明社会联系在欺诈尝试中发挥作用?我们应该假设某人是否会犯欺诈取决于他们所联系的人吗?如果约翰·史密斯有五个是欺诈者的朋友,你会怎么说他 [Baesens et al., 2015]?目标是确定从社会网络中提取的哪些类型的非结构化网络信息(与 SNA 和 CNA 相关的技术)可以转化为有用的和有意义的主题特征(在我们的案例中是欺诈者),以及如何转化。
社会网络分析(SNA)——以及作为提高欺诈检测系统质量工具的链接分析——在欺诈斗争软件行业中是一个热门话题。Gartner 集团欺诈分析师 Avivah Litan [2012]建议采用一种五层方法¹进行欺诈检测和预防,如图 10.1 所示。

图 10.1 Gartner 提出的欺诈检测五层方法
在这个模型中,每个层级代表一种特定的客户活动和行为类型:
-
Level 1 以端点为中心,包括用户身份验证、设备和地理位置信息。它包括为不同的客户访问渠道添加一组身份验证选项。对于低风险场景,所有银行都提供双因素身份验证,例如硬件或软件 ID 与个人识别码(PIN)的组合,或者用户 ID 和密码。对于高风险场景,三因素身份验证(添加来自第三类身份凭证,如生物识别)更安全,但也不太方便。带外身份验证,它要求为身份验证和访问使用单独的信息通道,被广泛接受为防止中间人攻击的防御措施。这些控制听起来很基础,但许多机构即使在这一点上监控也很薄弱 [Barta and Stewart, 2008]。
-
Level 2 以导航为中心,这意味着在特定会话期间分析客户行为是否存在异常。分析包括对在线客户和账户活动的实时、动态捕获。这些信息用于构建客户档案,以确定对这位客户而言什么是正常或异常,从而提供对客户/账户的更全面视角,并为实时决策奠定基础。
-
第三级 是以渠道为中心的。它侧重于分析账户活动的异常情况。想法是拥有一个端到端的企业平台,可以针对特定渠道,并在渠道之间提供可扩展性。在银行环境中,欺诈预防系统可能会专注于自动清算所(ACH)交易。如果客户通常每月只进行一到两次此类交易,那么一天内发生 30 笔 ACH 交易将被视为异常。
-
第四级 是跨产品和跨渠道;它包括监控实体在账户、产品和渠道之间的行为。最初看似合法的交易,当与其他区域的活动相关联时,可能会显得可疑。银行可以监控通过电话银行、在线、支票和移动设备进行的信用卡账户支付。这种广泛的交易监控方法(而不是像以渠道为中心的方法那样只关注来自单一来源的交易子集)很重要,因为它提供了对主体行为的全局视角。欺诈者会通过小额或零额交易尝试进行大量系统测试,这些测试可能会被抽样遗漏。
-
第五级 是关于实体链接分析,或评估不同用户或交易之间的联系。在这一级别,分析超越了交易和客户视角,以分析相关实体网络(如共享人口统计数据或交易模式的客户,或人们之间的一般关系)内的活动和关系。实体链接分析通过识别仅在跨相关账户或实体查看时才显得可疑的行为模式,或通过发现与可疑账户、实体或个人相关的网络,以确定案件是否仅限于孤立的个人,还是属于犯罪阴谋的一部分,从而帮助检测和预防欺诈。在汽车保险行业,欺诈检测系统可能通过识别可疑模式或重叠,如一个人在一个案例中是投保人,在另一个案例中是乘客,或投保人和索赔人有相同的电话号码,或反复使用相同的汽车修理厂和医疗专业人员,来揭露安排的交通事故或虚假索赔。
在这个框架中,第五级——最先进的一级——专注于使用图分析作为对抗欺诈的有力工具。第八章中提出的场景,其中使用图根据提供的信息在人们之间建立联系,也属于这一类别。在这种情况下,网络分析通过人口统计属性(如地址、电话号码、雇主、账户所有权、IP 地址和设备 ID)将实体连接起来。
本章重点介绍社会网络分析(SNA),它涉及探索用户之间的显式或隐式联系,作为进行欺诈检测和预防的基础。我们将考虑从可用数据中构建社会网络的各种技术,以及识别欺诈者或评估人们成为欺诈受害者风险的技术。
10.1 社会网络分析概念
从一般意义上来说,我们可以认为网络效应是强大的。这个说法通常被认为是正确的,并且得到了大量具体证据的证实。人们倾向于与他们认为在某些方面与自己相似的其他人交往[Newman, 2010],例如种族、宗教、政治派别、爱好、社会经济地位或性格。因此,了解网络中某些节点(如“诚实”和“不诚实”)的某些特征,将给我们一个相当大的机会来猜测其他节点的特征[Koutra et al., 2011]。
这个概念用“同质性”这个术语来表达,它归结为“物以类聚”的表达方式。友谊大多基于相似的兴趣、起源或经历——包括,可能地,共同倾向于进行欺诈。关系决定了哪些人会受到谁的影响,以及他们之间信息交流的程度。
重要的是要注意,并非所有的社交网络都是同质性的。有不同方式来正式计算一个网络有多少同质性,但总的来说,我们可以这样说:如果一个网络中的节点 X(欺诈者、纽约客、医生等)与其他带有标签 X 的节点有更大的连接程度,那么这个网络就是同质性的。在欺诈检测的背景下,
如果一个网络是同质性的,那么欺诈节点与其他欺诈节点有显著更多的连接,因此,合法节点与其他合法节点有显著更多的连接[Baesens et al., 2015]。
更具体地说,假设 l 是网络中合法节点的比例,计算为合法节点数除以总节点数,f 是网络中欺诈节点的比例,计算为欺诈节点数除以总节点数。在一个纯粹随机的网络中——在这种网络中,边是在节点之间随机创建的,没有任何内部或外部影响——两个不同标签节点之间连接的预期概率可以表示为 2 × l × f。这些边被称为交叉标签边。一个网络是同质性的,如果交叉标签边的比例(r),即交叉标签边数与总边数的比率,显著低于预期的概率 2 × l × f。考虑图 10.2 所示的社交网络。

图 10.2 社会网络的一个玩具示例
黑色节点是欺诈者,白色节点代表合法的人。该网络由 11 个节点组成,其中 7 个是合法的,4 个是欺诈的。在这个例子中,l 的分数是 7/11,f 的分数是 4/11。在随机网络中,交叉标记边的比例应该是
2 × l × f = 2 × 7/11 × 4/11
2 × 7/11 × 4/11 = 0.462
该网络有 4 个交叉标记的边,5 个欺诈节点之间的边,以及 9 个合法节点之间的边,总共有 18 条边。因此,r(观察到的交叉标记边的比例)的值为 4/18(0.222),这显著小于(实际上,小于随机网络预期值的一半)。因此,我们可以说图 10.2 中的网络是同质的。用公式来说,如果一个网络是同质的,那么
H: 2 × 1 × f ≈ r
在市场营销中,同质性的概念经常被用来评估个人如何相互影响,以及确定哪些人可能对营销激励措施做出反应,并应该成为目标。例如,如果亚历山德罗的所有朋友都使用电信运营商 X,那么亚历山德罗很可能与同一家运营商签订合同。
在欺诈的背景下,同样的推理也是适用的。我们定义同质网络为一个网络,其中欺诈者更有可能与其他欺诈者相连,而合法的人更有可能与其他合法的人相连。图 10.3 展示了这样一个网络可能的样子。
灰色节点代表欺诈者,白色节点代表合法的人。很容易看出灰色节点聚集成了群体;这个网络有一个大子图和一个小子图,包含三个节点。在欺诈分析的情况下,这样的一群欺诈者被称为欺诈网络。合法节点也聚集在一起。然而,正如你所看到的,网络并不是完全同质的:欺诈节点不仅与其他欺诈节点相连,还与合法节点相连。通过观察这个网络,我们可以识别出那些有很高可能性成为欺诈尝试受害者的合法节点,因为它们是与欺诈网络相连的节点。
练习
使用前面给出的公式来确定图 10.3 中的网络是否是同质的。到目前为止,我们一直在观察社交网络的快照,但社交网络本质上是动态的,并且迅速演变。为了进行适当的分析,我们的技术必须考虑这个维度——也就是说,我们必须考虑网络随时间的变化。网络中同时出现几个欺诈节点可能表明新欺诈网络的产生;包含许多欺诈节点的子图可能表明更成熟的系统。防止新网络的成长和现有网络的扩张是欺诈检测模型中需要解决的重要挑战。

图 10.3 欺诈网络
从前面的讨论中可以看出,社会网络分析在打击欺诈方面的相关性是明显的。使用适当的公式,我们能够识别社会网络是否表现出同质行为。在这个阶段,我们可以探索我们可用的 SNA 和 CNA 方法。
假设我们拥有或可以创建一个社会网络,我们验证它是一个同质网络,并有一些关于真实或潜在欺诈者的信息。我们如何使用 SNA 来提高我们的欺诈检测能力?
欺诈者通常因为参加相同的活动和事件、参与相同的犯罪、使用相同的资源集合或是一个人(如通过共享的个人信息的证据)而相互联系。基于关联的罪责方法结合可用的弱信号以推导出更强的信号,并在许多环境中被广泛用于异常检测和分类(如会计欺诈、网络安全、信用卡欺诈等)。因此,我们可以寻找欺诈者通过使用社会结构交换如何进行欺诈的知识。我们可以使用两种类型的方法和技术:
-
基于分数——我们逐个分析社会网络节点,使用考虑直接邻居和节点在网络中角色的指标(例如,有多少最短路径通过它)为每个节点分配分数。
-
基于聚类——考虑关系,我们将社会网络分割成多个节点集群(社区),试图从这些群体中推导出一些知识。
我们将首先检查基于分数的方法。
10.2 基于分数的方法
基于分数的社会网络分析(SNA)方法使用指标来衡量社会环境对感兴趣节点的影響。这些指标被分配给每个节点,可以是单独的,也可以是组合的,并进行分析以确定未标记的节点是否可能存在欺诈。得到的分数表示一个人(由社会网络中的节点表示)可能成为欺诈目标或可能成为欺诈者的概率或风险。如图 10.4 所示,将一组指标分配给每个节点的这种想法被称为特征化。

图 10.4 特征化
可以从网络中提取多种类型的指标,其中一些比其他指标更相关,这取决于具体情况。我们使用的网络分析技术可以分为三大类[Baesens et al., 2015]:
-
邻域指标——邻域指标基于节点的直接连接来表征节点,考虑节点周围的 n 阶邻域(由距离该节点 n 跳的节点组成)。由于可扩展性问题,检测模型通常仅整合从节点及其直接联系人推导出的特征,称为节点的一阶邻域。
-
中心性度量—中心性度量旨在量化节点(在此,社交网络中的个人)的重要性 [Boccaletti 等人,2006]。这些度量通常基于子图或整个网络结构。
-
集体推理算法—给定一个已知欺诈节点的网络,集体推理算法试图确定这些节点对网络中未标记节点的影响。目标是计算一个节点被暴露于欺诈并因此受到影响(使得该节点所标识的人更有可能成为欺诈者或欺诈的受害者)的概率。
为了说明这些方法中的每一个,我们将使用一个真实的社会网络和 Neo4j 中可用的图算法 ²。要深入了解这些算法,请参阅 Mark Needham 和 Amy Hodler 所著的 Graph Algorithms: Practical Examples in Apache Spark and Neo4j(O’Reilly,2019)。
由于(显然的原因)没有公开可用的欺诈者社交网络,我们将使用 GitHub 社交网络³进行分析。GitHub 是一个由 GitHub 开发者组成的大型社交网络,于 2019 年 6 月从公共 API 中收集 [Rozemberczki 等人,2019]。节点是那些至少标记了 10 个 GitHub 仓库(在界面中点击星号按钮以保存或“收藏”它们)的开发者,边是相互关注的关联。节点根据职位或描述进行标记,并标记为 Web 开发者或机器学习从业者。这里使用的度量、它们的计算方式以及产生的分数是有效且适用于真实场景的。
为了本章的目的,我们需要一个两个用户类别相互作用的社交网络。我们将把机器学习从业者视为目标类别(扮演欺诈者的角色)。这样,讨论和结果可以直接应用于欺诈者社交网络。
由于维度,我们不需要为导入数据集创建 Python 脚本;我们可以使用 Neo4j 中的 LOAD CSV 功能。首先,我们需要按照以下查询导入节点(在将数据集移动到 Neo4j 安装目录的导入目录之后)。
列表 10.1 创建约束
CREATE CONSTRAINT ON (g:GitHubUser) ASSERT (g.id) IS UNIQUE;
列表 10.2 导入节点 4
USING PERIODIC COMMIT 1000⁴
LOAD CSV WITH HEADERS FROM 'file:///git_web_ml/musae_git_target.csv' AS row
CREATE (user:GitHubUser {id: row.id, name: row.name, machine_learning:
➥ toInteger(row.ml_target)})
接下来,我们必须导入关系。
列表 10.3 导入关系
USING PERIODIC COMMIT 1000
LOAD CSV WITH HEADERS FROM 'file:///git_web_ml/musae_git_edges.csv' AS row
MATCH (userA:GitHubUser {id: row.id_1})
MATCH (userB:GitHubUser {id: row.id_2})
CREATE (userA)-[:FOLLOWS]->(userB)a
几秒钟(或几分钟,取决于你电脑的功率),图数据库就准备好了。正如我之前已经提到好几次的,图的一个优点是它们允许我们通过视觉检查来洞察数据。因此,在使用图之前,我们应该用特定的标签(GitHub 用户)标记节点——WebDeveloper 和 MLDeveloper——这样我们就可以通过视觉来识别它们。以下查询用于此目的,使我们能够轻松识别节点,并简化以下分析。
列表 10.4 标记 Web 开发者
MATCH (web:GitHubUser {machine_learning: 0})
SET web:WebDeveloper
列表 10.5 标记机器学习开发者
MATCH (ml:GitHubUser {machine_learning: 1})
SET ml:MLDeveloper
结果图如图 10.5 所示。蓝色节点代表网页开发者,红色节点代表机器学习开发者。

图 10.5 导入后的图数据库
在这里使用标签,而不是机器学习属性的值,将提高后续查询的结果,在这些查询中,我们必须区分机器学习开发者和网页开发者。
选定的社交网络似乎具有我们需要的特征:机器学习开发者(我们的目标)在图中相对较少,并且通常与类似开发者相连。现在我们可以计算我们需要的度量。
10.2.1 邻域度量
邻域度量使用节点的直接连接和 n 跳邻域来表征节点本身。我们可以考虑不同的度量,但在欺诈的背景下,为了本章的目的,我们将考虑度数、三角形和局部聚类系数。
节点的度数正式定义为节点拥有的连接数。度数本身不考虑关系的方向,但相关的入度和出度度量则考虑。入度计算进入的连接数,出度计算出去的连接数。节点的度数总结了节点有多少邻居。在欺诈检测的背景下,区分节点拥有的欺诈邻居和合法邻居的数量也是有用的。我们可以将这些度量称为欺诈度数和合法度数。为了澄清,考虑一个带有和不带有关系方向的简单社交网络(图 10.6)。在这个图中,红色节点(D 和 G)是欺诈的。


图 10.6 两个示例图,一个无向(左)和一个有向(右),包含两个欺诈节点(D 和 G)
左侧的无向图告诉我们节点 B 的度数为 8,这是网络中最大的度数。如果我们考虑方向,我们可以看到节点 B 的入度为 6,出度为 3。
我们还可以分析图中欺诈节点和合法节点之间的联系。节点 A 和 B 各自有 2 个欺诈度(G 和 D 都与它们相连),因此是受欺诈影响最大的节点。每个节点的度数列在表 10.1 中。
表 10.1 图 10.6 中节点的度数(考虑到 D 和 G 是欺诈的)
| 节点 | 度数 | 入度 | 出度 | 欺诈度数 | 合法度数 |
|---|---|---|---|---|---|
| A | 8 | 7 | 1 | 2 | 6 |
| B | 9 | 6 | 3 | 3(节点 G 计算了两次) | 6 |
| C | 2 | 0 | 2 | 0 | 0 |
| D | 3 | 1 | 2 | 0 | 3 |
| E | 1 | 1 | 0 | 1 | 0 |
| F | 2 | 1 | 1 | 0 | 2 |
| G | 3 | 1 | 2 | 0 | 3 |
| H | 1 | 0 | 1 | 0 | 1 |
| I | 1 | 0 | 1 | 0 | 1 |
| L | 2 | 0 | 2 | 0 | 2 |
| O | 2 | 0 | 2 | 0 | 2 |
让我们通过查询来计算真实网络的相同指标。以下列表显示了如何计算节点的度(这里,GitHub 用户 amueller)。
列表 10.6 计算节点的度
MATCH (m:MLDeveloper {name: "amueller"})-[:FOLLOWS]-(o:GitHubUser)
return count(distinct o) as degree
注意,我们在计算度时没有使用带箭头的-[:FOLLOWS]-(<- 或 ->),因为我们不关心关系的方向。在有向网络中,我们可以区分入度和出度。入度指定了多少个节点指向感兴趣的靶点,而出度描述了可以从感兴趣的靶点到达的节点数量。以下查询显示了如何计算这些指标。
列表 10.7 计算节点的入度
MATCH (m:MLDeveloper {name: "amueller"})<-[:FOLLOWS]-(o:GitHubUser)
return count(distinct o) as indegree
列表 10.8 计算节点的出度
MATCH (m:MLDeveloper {name: "amueller"})-[:FOLLOWS]->(o:GitHubUser)
return count(distinct o) as outdegree
我们可以通过以下查询轻松地计算一个节点的欺诈度(在我们的数据集中,与 MLDeveloper 的关联)和合法度(在我们的数据集中,与 WebDeveloper 的关联)。在(在我们的网络中,意义略有不同,但概念是相同的)。
列表 10.9 计算节点的欺诈(MLDeveloper)度
MATCH (m:MLDeveloper {name: "amueller"})-[:FOLLOWS]-(o:MLDeveloper)
return count(distinct o) as degree
列表 10.10 计算节点的合法(WebDeveloper)度
MATCH (m:MLDeveloper {name: "amueller"})-[:FOLLOWS]-(o:WebDeveloper)
return count(distinct o) as degree
对于这个用户,欺诈度(MLDeveloper)为 305,合法度(WebDeveloper)为 173。这个结果并不令人惊讶:GitHub 用户 amueller 是《Python 机器学习入门:数据科学家指南》(O’Reilly,2016)的作者之一,因此很明显,他是一名机器学习开发者,我们预计他与其他机器学习开发者的联系会比与网络开发者的联系更多。我们可以通过以下查询轻松地检查这个规则是否普遍适用。
列表 10.11 计算所有机器学习开发者的度
MATCH (ow:WebDeveloper)-[:FOLLOWS]-(m:MLDeveloper)-[:FOLLOWS]-
➥ (om:MLDeveloper)
WITH m.name as name,
count(distinct om) as mlDegree,
count(distinct ow) as webDegree
RETURN name, mlDegree, webDegree, mlDegree + webDegree as degree
ORDER BY degree desc
结果表明,对于网络中的所有机器学习开发者也是如此。因此,似乎数据集表现出我们描述的欺诈社交网络的特征。
网络的度分布描述了网络中度的概率分布。在现实生活中的网络中,它通常遵循幂律:也就是说,许多节点只与其他少数节点相连,而网络中只有少数节点与其他许多节点相连。下一个查询允许我们计算节点度的分布。
列表 10.12 计算节点度的分布
MATCH (m:GitHubUser)-[:FOLLOWS]-(om:GitHubUser)
RETURN m.name as user, count(distinct om) as degree
结果可以下载为 CSV 文件并可视化,如第九章所述。图 10.7 显示了分布将看起来是什么样子。(Excel 文件可在代码存储库的 ch10/analysis/DegreeAnalysis.xlsx 中找到。)

图 10.7 示例网络中的度分布
如您所见,大多数节点的度小于 100。大多数值介于 7 和 20 之间。
练习
通过调整列表 10.6 到 10.11 来探索你创建的图数据库,将它们改为考虑网页开发者而不是机器学习开发者。为这些实验识别一些有趣的网页开发者。
我们将要考虑的第二个邻域度量是三角形数量:由三个节点组成的完全连接子图的数量。图 10.8 展示了完全连接子图或三角形的例子。

图 10.8 三角形的例子
观察三角形是研究紧密相连的人群群体影响力的方法之一。它与三角闭合原则相关,这是一种建立新联系[拉波波特,1953]的机制。在社会网络中,这可以表达为这样的观点:如果两个人有一个共同的朋友,那么这两个人将来成为朋友的可能性会增加。这个术语来源于这样一个事实,如果我们有一个像图 10.9 那样的简单网络,D-B 边就具有闭合这个三角形第三边的效果(如图 10.8 所示)。

图 10.9 一个三角形的一边是开放的简单网络
如果我们在两个不同时间点观察一个社交网络,我们通常会发现在较晚的时间点,由于这种三角形闭合操作,形成了大量新的边,因为具有共同邻居的人之间建立了联系[艾斯利和克莱因伯格,2010]。
属于一个群体的节点会受到该群体其他成员的影响。在社会网络中,这种影响扩展到群体的其他成员的信仰、兴趣、观点等。因此,在欺诈者网络中,如果一个节点是许多三角形的组成部分,而其他两个节点是欺诈的,那么这个节点有很高的可能性会受到欺诈本身的影响(即参与欺诈或成为欺诈的受害者)。为了计算这种风险,我们可以区分合法三角形和欺诈三角形。如果三角形中的另外两个节点都是欺诈的,我们说这个三角形是欺诈的。如果两个节点都是合法的,我们说这个三角形是合法的。如果只有其中一个节点是欺诈的,这个三角形就是半欺诈的。考虑图 10.10 中的玩具网络。

图 10.10 示例欺诈者网络
表 10.2 列出了网络中每个至少属于一个三角形的节点的三角形度量。
表 10.2 图 10.10 中网络的三角形度量
| 节点 | 三角形 | 欺诈三角形 | 合法三角形 | 半欺诈三角形 |
|---|---|---|---|---|
| A | 6 | 1 | 3 | 2 |
| B | 6 | 1 | 3 | 2 |
| C | 1 | 0 | 1 | 0 |
| D | 3 | 0 | 1 | 2 |
| G | 3 | 0 | 1 | 2 |
| L | 1 | 0 | 1 | 0 |
| O | 1 | 0 | 1 | 0 |
Neo4j 在其图数据科学库中提供了一个简单的计算图或特定节点相关所有三角形的程序。附录 B 解释了如何安装和配置它。以下查询演示了如何操作。
列表 10.13 计算节点的三角形
CALL gds.triangleCount.stream({
nodeProjection: 'GitHubUser',
relationshipProjection: {
FOLLOWS: {
type: 'FOLLOWS',
orientation: 'UNDIRECTED'
}
},
concurrency:4
})
YIELD nodeId, triangleCount
RETURN gds.util.asNode(nodeId).name AS name,
gds.util.asNode(nodeId).machine_learning as ml_user, triangleCount
ORDER BY triangleCount DESC
三元闭合在社会网络中的基本作用激发了简单社会网络度量的制定,以捕捉其普遍性。其中一种度量是局部聚类系数。节点 A 的聚类系数定义为随机选择 A 的两个朋友成为彼此朋友的可能性。换句话说,A 的朋友对之间通过边的连接比例。让我们通过再次考虑我们的玩具网络(图 10.11)来更清楚地说明这一点。

图 10.11 示例图
节点 A 的聚类系数为 2/6,或 0.333,因为在六个可能的朋友对(B-D,B-C,B-F,C-D,C-F 和 D-F)中,C-B 和 D-B 之间存在边。一般来说,节点的聚类系数范围从 0(当节点的朋友之间没有一个是朋友时)到 1(当节点的所有朋友都是朋友时),并且节点周围的三元闭合作用越强,聚类系数往往会越高。在欺诈场景中,这个度量很重要,因为如果一个节点的局部聚类系数与网络的平均聚类系数相比较低,那么这个节点连接到属于独立群体的人。这种异常情况可能揭示潜在的欺诈风险,因为欺诈者正在加入人群以实施犯罪。确定节点局部聚类系数的公式看起来像这样:

以下查询允许您计算我们网络中每个节点的聚类系数,并将它们存储为节点的属性。
列表 10.14 计算聚类系数
CALL gds.localClusteringCoefficient.write ({
nodeProjection: 'GitHubUser',
relationshipProjection: {
FOLLOWS: {
type: 'FOLLOWS',
orientation: 'UNDIRECTED'
}
},
concurrency:4,
writeProperty:'clusteringCoefficient'
})
YIELD createMillis, computeMillis, writeMillis, nodeCount,
➥ averageClusteringCoefficient
RETURN createMillis, computeMillis, writeMillis, nodeCount,
➥ averageClusteringCoefficient
当您运行此查询并查看结果时,您会发现平均聚类系数较低:大约为 0.167。
练习
探索图,检查一些系数为 0 或 1 的节点的三角形。您可以使用查询来找到一两个这样的节点,然后使用 Neo4j 浏览器来导航它们。作为一个建议,我会创建查询以避免只有一两个追随者的平凡节点。
10.2.2 中心性度量
中心性度量揭示了图或子图中的中心节点——即可能对其他节点有强烈影响的节点——通过为每个节点分配一个分数来表示它在通过图进行通信中的中心程度[Fouss 等人,2016]。在社会网络中,中心性度量帮助我们回答以下问题:
-
在一个社区中,哪个节点最具代表性?
-
给定节点在通过网络信息流动方面有多重要?
-
哪些节点是最外围的?
这些指标在反欺诈场景中很有用;它们可以通过识别欺诈网络中的关键行动者来防止未来欺诈活动的扩展,该行动者可能作为中心节点并大量参与有关欺诈组织的沟通。与只考虑节点直接连接的邻里指标相比,这些类型的指标考虑了整个图(或其大部分)。因此,它们考虑了人在整个网络中的作用,而不仅仅是他们在自我网络中的作用。⁵
我们将考虑的指标包括最短路径、接近中心性和中介中心性。为了演示目的,就像邻里指标一样,我们将使用 GitHub 网络作为我们假设的欺诈者社交网络的代理。
最短路径是从目标节点到达节点所需的最小跳数(测地距离)。在计算这些距离时,算法可以考虑到每条关系的权重,这在您使用此指标寻找从一地到另一地的最佳路线时特别有用。在其他场景中,例如在我们的场景中,每条关系都有相同的权重:1。在这种情况下,最短路径是通过计算您需要通过多少节点(或关系)从源节点到达目标节点来确定的。图 10.12 展示了示例,识别了从节点 A 到节点 G、I 和 L 的最短路径。

图 10.12 最短路径示例
在欺诈背景下,潜在目标与欺诈者之间的测地距离可以揭示他们成为受害者的风险。合法节点与欺诈节点越近,它成为目标的可能性就越大。了解欺诈节点与合法节点之间存在多少路径也很有趣:路径越多,欺诈影响最终到达目标节点的可能性就越高。换句话说,如果一个欺诈节点在一个潜在目标的直接子图(邻域)中,它被影响的可能性就更大。相反,考虑这些距离可以揭示特定欺诈者的欺诈潜力:那个人直接或间接地接触到不同受害者有多容易。假设在我们的样本网络中,某些节点已被识别为欺诈,如图 10.13 所示。

图 10.13 欺诈用例中的最短路径示例。深色节点是欺诈节点。
通过查看此图,我们可以提取表 10.3 中的信息来分析网络中合法节点的风险水平。为了计算风险,我们将考虑到欺诈节点的最短路径长度(列 2)以及该节点与任何欺诈节点之间的 n 跳路径——换句话说,在 n 跳内我们可以从该节点到达多少个欺诈节点。(如果通过不同的路径可达,则同一欺诈节点会被计算多次。)
表 10.3 到欺诈节点的测地距离摘要
| 节点 | 测地路径 | 1 跳路径 | 2 跳路径 | 3 跳路径 |
|---|---|---|---|---|
| A | 1 | 1 | 2 | 5 |
| B | 1 | 2 | 2 | 7 |
| E | 1 | 2 | 4 | 7 |
| F | 1 | 2 | 5 | 9 |
| G | 1 | 1 | 4 | 7 |
| I | 1 | 1 | 3 | 8 |
| L | 1 | 1 | 4 | 7 |
许多算法和技术可以计算两个节点之间的最短路径,但这个主题超出了本书的范围。Neo4j 提供了许多开箱即用的解决方案,并且各种库提供了针对此类知名任务的超级优化算法。以下查询显示了如何使用 Neo4j 图算法库计算导入数据库的最短路径。
列表 10.15 查找两个节点之间的最短路径
MATCH (start:MLDeveloper {name: "amueller"})
MATCH (end:MLDeveloper {name: "git-kale"})
CALL gds.beta.shortestPath.dijkstra.stream({
nodeProjection: 'MLDeveloper',
relationshipProjection: {
FOLLOWS: {
type: 'FOLLOWS',
orientation: 'UNDIRECTED'
}
},
sourceNode: id(start),
targetNode: id(end)
})
YIELD index, nodeIds, costs
WITH index,
[nodeId IN nodeIds | gds.util.asNode(nodeId).name] AS nodeNames,
costs
ORDER BY index
LIMIT 1
UNWIND range(0, size(nodeNames) - 1) as idx
RETURN nodeNames[idx] as name, costs[idx] as cost
本查询的结果显示在图 10.14 中。成本是从起始节点 (amueller) 到特定节点所需跳数的数量。

图 10.14 列表 10.15 的结果
接近中心性 是通过考虑连接节点的所有最短路径长度来确定的。形式上,它是节点从网络中所有可达节点到该节点的平均距离的倒数。网络 G 中具有 n 个节点的节点 a 的接近中心性 cca 计算如下

其中
-
d(b,a) 是节点 a 和节点 b 之间的测地距离。
-
reachable(a,G) 是从节点 a 可达的 G 的子图。
-
|reachable(a,G)| 是 a 可达的节点数。
分母是节点 a 从 a 可达的所有节点的平均距离。高接近中心性值表示该节点可以轻松地到达网络中的许多其他节点,因此对其他节点有很强的影响。我们可以这样理解中心性的逻辑:如果一个人不是中心人物(具有低接近中心性值),他们必须依赖他人通过网络为他们传递消息或到达其他人。相反,一个中心人物(具有高接近中心性值)可以轻松地在网络中传播消息或直接到达其他人,而无需(或很少)中间人稀释他们的影响力。
在欺诈用例中,如果一个(子)图中的欺诈节点具有高接近中心性值,欺诈可能很容易通过(子)网络传播并污染其他节点。能够独立地接触到大量其他节点扩展了该节点的覆盖范围。此外,接近中心性在作为衡量一个人在社会网络中独立性的指标之外还有用。研究人员还将这个指标与一个人在网络中轻松获取信息的能力、权力和影响力联系起来。所有这些方面都可能在一个欺诈者的欺诈能力中发挥关键作用。
我们不必担心自己计算接近中心性值,因为 Neo4j 和其他库提供了对该任务的支持。以下查询显示了如何计算整个图的接近中心性并将值存储在每个节点中,以供进一步分析。
列表 10.16 计算和存储接近中心性
CALL gds.alpha.closeness.write({
nodeProjection: 'GitHubUser',
relationshipProjection: 'FOLLOWS',
writeProperty: 'closeness'
}) YIELD nodes, writeProperty
我们可以通过接近中心性对节点进行排序,以找到具有最高值的机器学习开发者。
列表 10.17 获取具有最高接近中心性的前 20 名 ML 开发者
MATCH (user:MLDeveloper)
RETURN user.name, user.closeness
ORDER BY user.closeness desc
LIMIT 20
这个查询的结果显示在图 10.15 中。

图 10.15 列表 10.17 的结果
看到结果,你会注意到一些用户是杰出的用户,他们拥有很多粉丝,并且正在从事高度相关的开源项目。但在我们的数据集中,一些顶级用户可能不会被考虑为有影响力。第一个是 WillemJan,他只有很少的粉丝,但关注了 33,000 个 GitHub 用户,这使得他在接近中心性堆栈中名列前茅。这个例子表明,接近中心性受到你知道多少人(或者你关注了多少人)的影响。
中介中心性衡量一个节点在网络中连接任何两个节点最短路径的程度。这个指标可以解释为通过这个节点传递的信息量的一个指标。具有高中介中心性的节点可能连接社区(网络中的子图)。考虑图 10.16 中显示的例子。
在这个图中,中心节点 B 连接了几乎独立的子图。值得注意的是,该节点具有低接近中心性值;它直接连接到图中很少的节点,要到达其他节点,它必须通过长路径。但它具有高中介中心性值,因为它位于最多的路径中。
这个例子有助于理解这两个中心性概念之间的区别。而接近中心性衡量一个人知道多少人或他们能在几步之内接触到多少人,而中介中心性则考虑一个人在整个网络中的位置。在通信网络中,中介中心性衡量一个节点对通信流量的潜在控制程度。
这个例子让我们了解到在分析社交网络以打击欺诈时这些指标的价值。如果图 10.16 中的节点 B 被一个社区感染了欺诈,欺诈可以轻易地传播到其他社区。为了防止这种污染发生,一个选择是将这个节点从网络中移除。

图 10.16 具有三个几乎不同的集群通过一个中心节点连接的示例图
介数中心性是多个领域中的一个强大度量。也许一个更直观的例子是疾病的传播。在一个大型人群中,具有高介数中心性的节点有很大可能性将疾病从不同的社区传播给很多人。通过识别和隔离这些节点,可以减少疾病爆发的范围。同样,在恐怖主义网络中,具有最高介数中心性的节点是传递信息、资金、武器等跨多个细胞或集群的关键人物。通过识别他们,可以识别更多的集群,通过阻断他们,我们可以影响整个网络运作的能力。
计算节点 a 的介数中心性的公式考虑了每对节点通过 a 的最短路径的百分比,并将所有这些百分比相加。数学上,

where
-
gjk 是从 j 到 k 通过 a 的最短路径的数量。
-
g[jk] 是从 j 到 k 的总最短路径数。
与接近度中心性一样,这个度量在 Neo4j 和许多其他库中得到了全面的支持。在我们的数据库中,我们可以通过以下查询轻松计算介数中心性。
列表 10.18 计算节点的介数中心性
CALL gds.betweenness.write({
nodeProjection: 'GitHubUser',
relationshipProjection: 'FOLLOWS',
writeProperty: 'betweenness'
}) YIELD nodePropertiesWritten, minimumScore, maximumScore, scoreSum
然后,我们可以根据介数中心性对节点进行排序,以找到具有最高值的机器学习开发者。
列表 10.19 获取具有最高介数中心性的前 20 个 ML 开发者
MATCH (user:MLDeveloper)
RETURN user.name, user.betweenness
ORDER BY user.betweenness desc
LIMIT 20
这个查询的结果要重要得多。几乎列表中的所有人都是高度相关的:他们是著名书籍的作者,或者他们在机器学习领域贡献了很多开源项目。在这种情况下,介数中心性比接近度中心性更好地捕捉了人们在网络中的重要性。它代表了一个人在整个网络中的影响力,而不仅仅是他们的朋友圈。
练习
在先前的查询中,我们只考虑了机器学习开发者来分析他们的影响力。运行查询以搜索顶级网络开发者,考虑介数和接近度中心性,然后查看他们在 GitHub 上的个人资料。
10.2.3 集体推理算法
在本章所考虑的场景中,一种集体推理过程通过考虑节点之间可以相互影响的事实以及这种推理效果传播的比例与节点的重要性成正比,来推断一个节点可能受到欺诈的影响——要么成为欺诈者,要么成为欺诈的受害者。我们将重点关注 PageRank,这是一种强大的集体推理算法,我们在本书早期已经看到其应用。
PageRank [Page et al., 1998]——谷歌著名搜索引擎算法排名网页的基础——可能是今天最流行的声望计算技术。它为图中的每个节点 j 分配一个声望分数。直观地讲,如果一个节点连接到很多本身很重要的节点,那么这个节点的声望分数必须更高。当然,在欺诈的情况下,这种“声望”也可以从另一个角度来理解。如果一个节点连接到很多声望很高的欺诈者(在我们的半标记网络中被标记为这样的知名欺诈者),那么这个节点卷入欺诈(无论是作为实施者还是受害者)的可能性很高。
我们的起点是一个半标记网络,其中包含一些标记为合法和欺诈的节点以及许多未标记的节点。图 10.17 代表我们的玩具欺诈者网络。观察这个图,如果我们假设 G 和 D 是欺诈者,我们能推断出节点 A 是欺诈者或欺诈受害者的概率是多少?

图 10.17 一个简单的有向欺诈网络,其中 D 和 G 是欺诈者
让我们从 PageRank 最初被引入的场景开始分析,忽略标签,将所有节点视为平等。现在假设节点是网页,并且一个冲浪者只能通过查看他们当前正在查看的页面上的链接来浏览页面。图显示网页 A 有七个进入链接。一个正在查看网页 B 的冲浪者接下来访问网页 A 的概率是 1/3,即 33%,因为网页 B 有三个指向其他网页的链接,其中一个是网页 A。同样,如果一个冲浪者正在查看网页 C 或 D,那么在这两种情况下,他们接下来访问网页 A 的概率都是 50%。一个网页被访问的概率被称为该网页的页面排名。为了确定网页 A 的页面排名,我们需要知道网页 B、C、D、F、G、L 和 O(链接到 A 的七个页面)的页面排名。
这个过程是集体推理:一个网页的排名取决于链接到它的其他网页的排名,其中一个网页排名的变化可能会以级联的方式影响所有其他网页的排名。具体来说,主要思想是重要的网页(出现在搜索结果顶部)来自其他重要网页的链接很多。以下是针对任何给定网页进行页面排名的基本方法:
-
指向该页面的网页的排名
-
从该页面链接出的页面的出度
节点的初始页面排名值设置为随机漫游者从该节点开始导航的概率——即,1/<页面数量>。然后它迭代直到满足某些停止标准,通常是在每次迭代的排名变化变得微不足道或达到定义的最大迭代次数时。图 10.18 通过一个具体示例总结了该算法。

图 10.18 PageRank 初始化和迭代
在每次迭代 i 中,我们可以说节点 A 的页面排名值 PR(i,A) 是这样计算的

其中
-
PR(i -1,n) 是节点 n 在前一次迭代的页面排名值。
-
NA 是节点 A 的所有邻居。
PR(0,n) 是每个节点的初始值,表示随机漫游者从该节点开始导航的概率。当然,随机漫游者只通过在当前查看的网页上随机跟随链接来访问页面这一假设并不现实。漫游者的行为更加随机:他们可能不会跟随网页上的链接,而是随机访问其他页面。因此,一个更复杂的 PageRank 算法必须包括随机漫游者模型,该模型假设漫游者经常感到无聊并随意跳转到另一个网页。
假设 α 是漫游者跟随当前查看的网页上的链接的概率,而 (1 - α) 是漫游者访问随机其他网页的概率,一个更高级的公式是

其中 (1 - α) 是 重启概率,e[A] 是网页 A 的 重启值,通常在所有网页中均匀分布。
从这个高级版本开始,Page 等人通过个性化搜索扩展了 PageRank 算法。这种个性化是通过将重启值从所有节点的均匀分布更改为针对用户搜索兴趣量身定制的版本来实现的。页面 X 的更高重启值对应于用户对该页面的更高兴趣。
这个最后的版本非常适合我们的最终用例:推断欺诈者对未标记节点的影响(这意味着我们不知道该节点所标识的人是否是欺诈者或欺诈的受害者)。在这种情况下,PageRank 算法可以被视为通过标记网络传播节点影响。 (在我们的案例中,我们知道一些节点是欺诈的。)我们通过以下方式在网络中注入欺诈:

在 PageRank 计算结束时,排名最高的节点是最受欺诈影响的节点。我们可以将这些想法应用到我们的图数据库中。有很多个性化的 PageRank 实现,Neo4j 也有自己的实现。以下查询计算了所有用户的个性化 PageRank,将机器学习开发者作为起始值(在此指定为源节点)。
列表 10.20 为机器学习开发者计算个性化 PageRank
MATCH (mlUser:MLDeveloper)
with collect(mlUser) as mlUsers #A
CALL gds.pageRank.write({
nodeProjection: 'GitHubUser', #B
relationshipProjection: 'FOLLOWS',
maxIterations: 20,
dampingFactor: 0.85,
sourceNodes: mlUsers,
writeConcurrency: 4,
writeProperty: 'pagerank'
})
YIELD ranIterations, didConverge
RETURN ranIterations, didConverge
#A Computing the list of starting nodes, since we would like to consider the
➥ effect of this nodes on the network.
#B The page rank is computed on all the GitHubUsers
在这个查询中,dampingFactor 是我们的α,sourceNodes 是用于重启向量的节点。当分数计算完成后,我们可以使用以下查询按 PageRank 值从高到低对网络开发者进行排序。
列表 10.21 按 PageRank 排序以找到受影响最严重的用户
MATCH (user:GitHubUser)
RETURN user.name, user.pagerank, labels(user)
ORDER BY user.pagerank desc
LIMIT 20
之前查询返回的列表顶部被最有可能受到机器学习开发者影响的网络开发者占据。如果您想进一步调查,可以使用以下查询来计算特定机器学习开发者关注者的 PageRank 值总和。
列表 10.22 计算开发者的 PageRank 总和
MATCH (user:GitHubUser {name: "dalinhuang99"})<-[:FOLLOWS]-(follower)
WITH user, follower, follower.machine_learning as machine_learning,
CASE follower.machine_learning = 0
WHEN true THEN 0
WHEN false THEN follower.pagerank
END as mlpagerank,
CASE follower.machine_learning = 1
WHEN true THEN 0
WHEN false THEN follower.pagerank
END as webpagerank
RETURN user.pagerank, count(follower), sum(machine_learning),
➥ sum(mlpagerank), sum(webpagerank)
这个查询的结果显示,这个用户有 1,000 名机器学习开发者关注他们,总共有超过 7,000 名关注者,但这些开发者的 PageRank 值总和高于网络开发者的排名总和。这个结果表明,机器学习开发者对这位用户的影响大于网络开发者,证明了 PageRank 算法可以用来确定网络对主题的影响。回到我们的欺诈场景,那么,PageRank 值可以衡量一个节点(人)受到欺诈(参与其中或被作为受害者)影响的概率。
练习
再次运行最后两个查询,将机器学习开发者和网络开发者互换,并探索结果。
10.3 基于聚类的算法
社交网络是通过网络中的链接揭示人与人之间关系的强大工具。在第 10.2 节中,我们的分析侧重于逐个节点地提取或计算特征,在大多数情况下考虑直接连接的邻近节点或通过每个节点的路径。在这种类型的分析中,每个节点都是单独考虑的,关注它在网络中的作用及其紧密连接的集合。这些结果指标是有用的,但要全面理解社交网络对节点(或相反,节点对网络的影响),重要的是将节点视为节点组的一部分,而不是单独考虑。由于网络的动态性,可以合理地认为,行为相同或持有相同观点或信念的社区(网络内的节点组)对网络的影响可能比单个人更大,在信息交换方面具有更大的影响力。在欺诈用例中,一组共同工作的欺诈者可能比单独行动的欺诈者具有更大的影响力。
在图论中,一个社区是网络中的一个子图或簇,其节点之间的连接比它们与网络中任何其他随机子图中的节点之间的连接更强、更密集。社区的所有成员都可以通过同一社区的其他成员轻松地到达(连通性)。同时,我们期望属于社区的节点与其他社区成员的链接概率高于不属于同一社区的节点(局部密度)。因此,我们可以更正式地将社区定义为网络中的一个局部密集连接子图 [Barabási and Pósfai, 2016]。例如,图 10.19 中的图可以被划分为三个社区,在每个社区中,社区内的节点之间有许多连接,而社区外则没有或很少。

图 10.19 一个示例图,已聚类
欺诈者通常以小组的形式合作,分享、强化和提出关于如何实施欺诈的互补想法。此外,成为欺诈者群体的一员会影响人们的行为,增加他们参与欺诈的风险(工作场所的同伴压力)。在这种情况下,社区挖掘旨在识别网络中的欺诈者群体,以确定欺诈比图的其他部分更可能发生的子图。这些信息有助于检测隐藏的欺诈结构。考虑到人们如果受到整个社区的影响而不是单个欺诈者的诱惑,更有可能进行欺诈,因此,这也是识别可能被卷入欺诈活动的人的有用方法,从而遏制欺诈群体的增长 [Baesens et al., 2015]。
社区是识别常见行为模式的有力机制,即使节点在经典社交网络中没有连接。图 10.20 是一个二部图,其中的节点是商店和信用卡。商店通过欺诈交易与信用卡相连。

图 10.20 一个由欺诈交易连接的商店和信用卡的二部图
如第九章所述,欺诈者倾向于坚持相同的行为模式。他们可能会在相同的商店反复使用被盗的信用卡,可能是因为那些商店的员工参与了欺诈,或者可能是因为之前的尝试在那里是成功的。我们可以将先前的二部图投影到一个商店图中,如果两个节点在二部表示中连接到相同的信用卡节点,则它们之间相连。结果将类似于图 10.21。

图 10.21 向二部图添加投影关系
很容易看出,一些可疑的商店社区通常被欺诈者使用(图 10.22)。发现这样的社区表明,一些商店比其他商店更容易成为欺诈的受害者,这是一个信号,表明这些商店可能自己参与了欺诈,或者可能需要加强他们的安全措施。

图 10.22 二部图投影中的聚类
在信用卡欺诈中,一个已知的模式是被盗的卡在许多商店进行小额交易。在这种情况下,社区挖掘可以立即揭示哪些商店经常与同一被盗信用卡相关——这是在单个商店层面实施欺诈检测实践所无法揭示的。在这些商店中的多个商店使用一张卡,或者在其中的某些商店多次使用,可以用作相关信号,表明该卡(详情)已被盗。在许多应用和环境中,通过社会网络分析发现这样的社区可以帮助检测欺诈结构或遏制欺诈团伙。
社区挖掘的一个常见技术是图分区,也称为节点聚类。目的是通过优化社区内和社区间边的比率,将图分割成预定的若干个集群。可以使用不同的算法来确定分割图的最佳方式。其中,我想提到两种相反的方法:
-
自上而下或分裂——这些方法也称为分区或分割,从一个所有节点都被视为一个单一集群的初始情况开始。这个集群通过迭代分割成小块,试图最小化集群间的连通性,直到达到一个稳定点,在这个点上无法获得显著的改进。
-
自下而上或聚类——这些方法以相反的方式进行操作:它们首先将每个节点视为一个独立的簇,然后递归地尝试将最相似或高度互联的节点合并到簇中。
对于我们的目的,我们将考虑第二种方法,这种方法也适用于检测密集(高度互联)的区域。具体来说,下一个示例中使用的算法是 Blondel 等人[2008]引入的 Louvain 方法。这种方法通常用于检测大型网络中的社区。根据 Neo4j 文档⁷
它最大化每个社区的模块度得分,其中模块度量化了将节点分配给社区的质量。这意味着评估社区内节点之间的连接密度与它们在随机网络中连接密度的差异。Louvain 算法是一种层次聚类算法,它递归地将社区合并成一个节点,并在凝聚图上执行模块度聚类。
为了完整性,通常使用的实现是 Lu 等人[2015]引入的并行版本,该版本引入了一些启发式方法来打破内部顺序屏障。
您不需要担心此算法的实现,除非您特别想自己实现它,因为 Neo4j 和其他库中都提供了此算法。以下查询将在我们的示例网络上执行 Louvain 社区检测。
列表 10.23 在 GitHub 图上执行 Louvain 社区检测
CALL gds.louvain.write({
nodeProjection: 'GitHubUser',
relationshipProjection: {
FOLLOWS: {
type: 'FOLLOWS',
orientation: 'undirected',
aggregation: 'NONE'
}
},
writeProperty: 'community'
}) YIELD nodePropertiesWritten, communityCount, modularity
RETURN nodePropertiesWritten, communityCount, modularity
与前述案例一样,算法将在每个节点中存储一个新的属性,称为社区,其中包含节点所属社区的 ID。以下查询将给出 Louvain 如何将我们的网络分割成社区的想法。
列表 10.24 获取前 5 个 Louvain 社区
MATCH (g:GitHubUser)
RETURN g.community,
count(g) as communitySize,
sum(g.machine_learning) as mlDevCount
ORDER BY communitySize desc
LIMIT 5
结果将类似于图 10.23。

图 10.23 顶级五 Louvain 社区。(由于某些随机初始化,结果可能略有不同。)
看看前两个社区中机器学习开发者的数量。第一个群体显然是一个网络开发者社区(只有 373 名成员是机器学习开发者),第二个群体显然是一个机器学习开发者社区。算法似乎很好地识别了具有相似兴趣的人群组。
10.4 图的优势
在这种情况下,我们无法讨论使用图相对于其他方法的优势,因为图——特别是社交网络——是本章描述的方法的核心元素。但我们可以回顾使用社交网络(因此,使用图方法)进行欺诈检测和预防的优势:
-
社交网络为探索和调查欺诈提供了极好的知识来源。
-
图是表示社交网络的最佳方式。
-
使用不同的算法,社会网络分析(SNA)提供了一系列关于欺诈和欺诈者影响范围和影响力的见解。
-
图算法是提取图见解和进行社交网络深度分析的优秀工具集,使我们能够识别网络中的关键人物、人群或节点。
摘要
尽管关于这个主题还可以说很多,但本章完成了我们对使用图方法进行欺诈检测技术的概述。在这里,我们关注的是社交网络,特别是社会网络分析(SNA)在分析网络中欺诈者行为和影响中的作用。在本章中,你学习了
-
如何从不同类型的数据源创建社交网络
-
如何使用 SNA 方法探索网络中欺诈和欺诈者影响力的网络
-
如何将一组与网络结构相关的特征分配给每个节点
-
如何使用各种图算法从网络中提取见解(找到关键影响者,计算聚类系数,计数三角形等)
-
如何将节点分组到社区中
值得注意的是,在本章中,我们仅使用 Neo4j 和查询就完成了所有分析,使用可用的插件而没有编写任何代码。这一事实证明了图方法和现有库的强大功能和灵活性。
参考文献
[Baesens et al., 2015] Baesens, Bart, Veronique Van Vlasselaer, 和 Wouter Verbeke. 使用描述性、预测性和社会网络技术进行欺诈分析:欺诈检测的数据科学指南. 新泽西州霍布肯:Wiley 和 SAS 商业系列,2015 年。
[Barabási and Pósfai, 2016] Barabási, Albert-László, 和 Mártin Pósfai. 网络科学. 剑桥,英国:剑桥大学出版社,2016 年。
[Barta and Stewart, 2008] Barta, Dan, 和 David Stewart. 2008. “欺诈检测与预防的分层方法:使用网络分析提高调查员效率.” SAS 结论论文。
[Blondel et al., 2008] Blondel, Vincent D., Jean-Loup Guillaume, Renaud Lambiotte, 和 Etienne Lefebvre. “大型网络中社区的快速展开.” 统计力学:理论与实验杂志 (2008): P10008.
[Boccaletti et al., 2006] Boccaletti, Stefano, Vito Latora, Yamir Moreno, Mario Chavez, 和 Dong-Uk Hwang. “复杂网络:结构与动力学.” 物理报告 424:4-5 (2006): 175-308.
[Easley and Kleinberg, 2010] Easley, David, 和 Jon Kleinberg. 网络、群体与市场:高度连接世界的推理. 纽约:剑桥大学出版社,2010 年。
[Fouss et al., 2016] Fouss, François, Marco Saerens, 和 Masashi Shimbo. 2016. 网络数据与链接分析算法和模型. 纽约,剑桥大学出版社,2016 年。
[库特拉等,2011] 库特拉,丹尼,泰-尤·凯,U. 康,杜恩·霍恩格(波洛)赵,辛格-库欧·肯尼思·赵,和克里斯托斯·法洛特索斯。 “统一关联归咎方法:定理和快速算法。” 欧洲机器学习与知识发现数据库会议论文集 (2011): 245-260.
[利坦,2012] 利坦,阿维瓦。 “企业欺诈和滥用管理市场中的谁是谁,什么是何。” Gartner 资源 ID G00230076 (2012)。
[卢等,2015] 卢,浩,马哈特什·哈拉帕纳瓦,和安南特·卡利扬拉曼。 “可扩展社区检测的并行启发式方法。” 并行计算 47: 19-37.
[尼德汉姆和霍德勒,2019] 尼德汉姆,马克,和艾米·E·霍德勒。 图算法:Apache Spark 和 Neo4j 中的实用示例。 塞巴斯蒂波利斯,CA:奥雷利媒体,2019。
[纽曼,2010] 纽曼,马克。 网络:导论。 纽约:牛津大学出版社,2010。
[佩奇等,1998] 佩奇,拉里,谢尔盖·布林,拉杰夫·莫特瓦尼,和特里·温格拉德。 “PageRank 引文排名:为网络带来秩序。” 斯坦福信息实验室技术报告。
[拉波波特,1953] 拉波波特,阿纳托尔。 “通过具有社会结构偏差的群体的信息传播 I:传递性的假设。” 数学生物物理学通报 15:4 (1953): 523-533.
[罗兹门奇克等,2019] 罗兹门奇克,本德克,卡尔·艾伦,和里克·萨卡尔。 “多尺度属性节点嵌入。” arXiv 预印本 arXiv:1909.13021 (2019).
(1.)www.gartner.com/en/documents/1912615.
(2.)github.com/neo4j/graph-data-science.
(3.)snap.stanford.edu/data/github-social.html.
(4.)如果你使用 Neo4j 桌面版或社区版,你需要在 USING(此处和下一个查询)之前加上:auto,因为 Neo4j 桌面版使用显式事务处理器,这会导致与“周期性提交”语句冲突。使用:auto,你可以强制 Neo4J 桌面版使用自动提交选项而不是显式事务处理器。下一个查询也适用。
(5.)自我网络是由一个节点(一个人,自我)和所有与之直接相连的其他节点(人)组成的社会网络。
(6.)回顾第五章,我们可以推断出二分图中相同类型节点之间的联系,从而创建一个投影。
(7.)mng.bz/XYy6.
第四部分:用图表驯服文本
文本,文本,还有更多的文本!我们被文本数据所包围。世界上大部分的知识都是通过使用自然语言中的文本来存储和共享的。这自从人类历史开始以来就是如此,当时我们开始用不同的语言分享知识——最初是通过声音,后来,为了使其永久化,通过书写。
自然语言是我们与其他人类互动的主要方式。我们从婴儿时期就开始学习它,然而理解语言是机器可以做的最复杂的任务之一。尽管如此,计算机科学家、数据科学家和机器学习从业者已经努力使机器能够处理文本数据,提供使用文本为最终用户提供高级功能的复杂解决方案。
在本书的最后两个章节中,我们将关注这个有趣研究领域的以下方面:
-
自然语言处理(NLP)——NLP 的目标是处理自然语言(英语、意大利语、法语等)并将其转换为机器可以理解和处理的数据结构。它提取文本的隐藏结构,识别所有元素及其之间的联系。这个过程使机器在一定程度上能够“理解”人类语言。
-
知识表示、组织和管理——自然语言处理技术为我们提供了利用我们可用的巨大数据量并将其转换为有用知识源的基础。这个任务或任务集的输出必须被适当地存储和组织,以便可以轻松访问和导航。无论在什么背景下(如聊天机器人或问答系统,或管理所有企业数据的公司),知识管理都是处理文本数据(文档、语音等)的关键要素,因为它允许人们以有效的方式访问信息。
这两个任务紧密相连,并且不断相互作用,两者都是巨大的主题。在第十一章和第十二章中,我们将看到如何将自然语言处理与图模型和算法相结合,从文本中提取有意义的信息,并以多种方式存储,使其可以被多个进程访问。具体来说,我们将强调图表在帮助从业者处理复杂机器学习任务中的作用和实用性。
图表提供了一种足够灵活的数据模型,可以支持组织、访问和处理文本数据所需的大部分步骤。在这些章节中,因为我们已经接近书末,我们将这一概念发挥到极致:生成的图表将是迄今为止我们讨论过的最复杂的图表,也是最强大的。将你的知识存储在一个包含巨大知识库的单个连接的真实来源中,将使你能够完成大量的任务。
这个主题对我来说非常重要:我是 GraphAware Hume(graphaware.com)的创作者之一,并且长期担任该产品的负责人。GraphAware Hume 是一个由图技术驱动的洞察力引擎,它允许公司从多个数据源收集所有企业数据——无论是结构化还是非结构化数据——并将其首先转换为知识图谱,然后转化为可操作的洞察。
11 基于图的自然语言处理
本章涵盖
-
一种将文本分解并存储在图中的简单方法
-
如何通过自然语言处理提取非结构化数据的隐藏结构
-
一种用于驯服文本的高级图模型
让我们从考虑最常见的一些应用开始,这些应用处理自然语言(以不同格式)为最终用户提供服务。你可能每天都在使用它们,可能甚至没有意识到它们的复杂性和实用性。
第四章处理文本以实现一个推荐引擎,该引擎使用与项目相关的信息,例如产品的描述或电影情节。在这种情况下,这些数据被用来比较项目或用户配置文件,找出用户或项目之间的共同点(特别是相似性),并利用这些共同点向当前用户推荐可能感兴趣的内容。图 11.1 展示了第四章中摘取的内容推荐引擎的高级结构。

图 11.1 如第四章所述的内容推荐引擎
项目分析器和用户配置文件构建器处理文本,以便在推荐阶段使其可用。他们的分析结果以易于在模型生成和推荐过程中访问和查询的方式存储。
近年来,搜索引擎可能一直是处理文本的最关键类型的应用,在用户寻找某物时提供相关结果。想想谷歌和雅虎!如果没有像这两个搜索引擎这样的工具,互联网可能就不会是现在这样;将没有一种有效的方法来发现新内容或访问互联网提供的巨大资源宝库,结果可能是它永远无法发展到现在的规模。搜索是我们用来与各种内容来源(新闻网站、零售网站、数据库等)互动的最常见技术。它帮助我们以直观和有效的方式获取相关数据。图 11.2 展示了搜索引擎的一个简化架构[Turnbull 和 Berryman,2016]。

图 11.2 搜索引擎的一个过于简化的架构
搜索引擎与文档存储库相连。它以这种方式索引存储库中的文档,即用户的查询将快速执行,结果将准确无误。
当一个应用收到一个自然语言问题并提供回复或以某种方式与用户互动时,会出现一个更复杂的场景。我们大多数人都有使用数字语音助手的经验。你以“Siri . . . ,”“Okay Google . . . ,”或“Alexa . . . ,”开始一句话,并要求助手执行一些简单任务,比如“找到最近的餐厅”,“播放一首浪漫的歌曲”,或“告诉我天气预报”。由于对这项新技术感到兴奋,你可能已经尝试得更多,要求它执行更复杂的任务,并尝试不同格式的不同问题。很可能会因为代理无法处理超出预定义技能集的查询而感到沮丧。
这些应用正在尽力而为,但他们试图实现的目标是困难和复杂的。如图 11.3 所示,回答甚至一个简单的问题也需要完成一系列的任务。

图 11.3 对话式代理执行的任务集示例
这些任务在表 11.1 中有详细描述。
表 11.1 对话式代理执行的任务
| 任务/组件 | 描述 |
|---|---|
| 自动语音识别 (ASR) | ASR 从用户那里接收音频(语音)输入,并输出代表查询的转录单词字符串。 |
| 自然语言理解 (NLU) | NLU 组件的目标是从用户的表述中提取三件事。第一项任务是领域分类。用户(例如)是在谈论预订航班、设置闹钟还是处理日历?这种 1-of-n 分类任务对于只关注,比如说,日历管理的单领域系统是不必要的,但多领域对话系统是现代标准。第二项任务是用户意图识别。用户试图完成什么一般任务或目标?任务可能是查找电影、显示航班或删除日历预约。最后,实体提取涉及从用户的表述中提取用户意图系统应理解的具体概念。这些实体用于具体化意图:他们想在哪里预订航班?他们在日历中寻找哪个事件? |
| 搜索答案 | 这一步是整个过程的精髓。系统在接收到领域、意图和实体后,访问某些知识库并确定可能的答案集。然后它必须对答案进行排序并将它们作为输入提供给后续步骤。在某些情况下,这个过程意味着在文档中查找段落;在另一些情况下,意味着检索将用于生成适当答案的信息。在此背景下,知识库可以由来自多个数据源的结构化和非结构化数据创建。在对话代理中,此组件还会考虑用户先前的问题,以缩小当前问题的上下文。 |
| 自然语言生成 (NLG) | (可选)接下来,NLG 组件生成对用户的响应文本。在信息状态架构中,NLG 的任务通常分为两个阶段:内容规划(说什么)和句子实现(如何说)。此组件是可选的。在大多数情况下,句子直接从知识库中的现有文档中提取。 |
| 文本转语音 (TTS) | TTS 将答案转换为用户可以听到的音频,而不是阅读。 |
由于此场景的复杂性,它代表了机器学习中最激动人心和最活跃的研究领域之一。在未来,我们只需使用我们的声音就能与周围的所有设备进行交互,但到目前为止,这种交互仍然是一个梦想。
尽管推荐代理、搜索引擎和对话代理似乎不同,但它们有共同的关键方面。基本要求可以总结如下:
-
使用文本构建知识库。
-
-
推荐引擎使用项目描述来创建推荐模型(例如,识别项目之间的相似性)。
-
搜索引擎通过索引对文档进行预处理。
-
聊天机器人和对话代理使用非结构化数据(文档和先前问题)来创建知识库。
-
-
适当的知识表示存储了应用范围内所需的所有信息,并提供对其的高效访问。
-
与用户的交互,这在大多数情况下是通过自然语言进行的。
以下章节和第十二章处理这些关键元素,提出不同的基于图的方法来完成这些任务或解决一些相关问题。
11.1 基本方法:存储和访问单词序列
如常,让我们从一个基本的图模型开始,以说明基于图的自然语言处理的高级概念和主要问题。在本章的后续部分,从第 11.2 节开始,我们将讨论更高级的技术和模型。
值得注意的是,每个图模型都应该根据应用的目的来设计。尽管这些模型很简单,但在这个案例中设计的模型非常适合本节所述场景的范围。这些模型将适当地发挥作用,而不会过于复杂,展示了图建模的一个关键方面:从简单开始,并在必要时通过引入新的概念和复杂性来适应。话虽如此,现在是时候开始我们的旅程了。
假设你想实现一个支持消息编写、在输入时建议下一个单词的工具。此外,假设你希望这个工具能够从你或特定的文档集中学习。这样的工具不仅可以帮助提供消息编写辅助,还可以支持拼写检查、提取常用短语、总结等功能。
第一步是将输入拆分为单词。在西方语言中,最简单的方法是使用空格。(其他语言,如中文,需要不同的方法。)当它们被提取出来时,这些单词必须以某种方式存储,以跟踪它们在原始文本中的顺序。这里概述的基本方法受到了 Michael Hunger 的一篇博客文章¹的启发。一个合适的图模型将类似于图 11.4(使用示例短语“你将不能”)。

图 11.4 应用到短语“你将不能”的基本方案
在这个方案中,单词本身被认为是唯一的,但单词之间的关系则不是,因此如果某些单词在其他句子中也被使用,它们不会被复制;相反,会创建新的关系。如果我们也处理短语“你将能够”,生成的图将类似于图 11.5。

图 11.5 我们对短语“你将能够”应用的方案
由于新的输入创建了新的关系,但没有创建新的单词,因为我们已经存储了所有这些单词。这种模型和我们在每个句子中保持单词唯一并创建新关系的方法既有优点也有缺点,我们将对其进行探讨和分析。在某些情况下,这个模型可能很有用,但在其他情况下则不然。这个例子将说明,在你的项目发展中,你可以改变对解决方案某些方面的看法。你的需求可能会变化,需要你开发一个更适合它们的新的方案。图对于这个目的很有帮助,因为它们是灵活的数据结构,可以根据你项目的变化约束和要求进行演变。以下 Cypher 查询显示了如何处理文本并获得预期的图数据库。
列表 11.1 使用空格拆分句子并存储它
WITH split("You will not be able to send new mail until you upgrade your
➥ email."," ") as words
UNWIND range(0, size(words)-2) as idx
MERGE (w1:Word {value:words[idx]})
MERGE (w2:Word {value:words[idx+1]})
CREATE (w1)-[:NEXT]->(w2);
在这个查询中,开头的 WITH 子句为下一个查询语句提供了数据。注意,split() 函数根据空白字符定义了分词过程,这被指定为第二个参数中的分隔符。range() 函数创建一个数字范围,在这种情况下是从 0 到句子的大小——通过在文本上使用 size() 函数获得——减去 2。UNWIND 子句将范围集合转换为具有索引值的结果行,以获取正确的单词。MERGE 子句,如通常一样,帮助我们避免创建相同的节点(在这种情况下是相同的单词)两次。最后,我们使用 CREATE 来存储两个连续单词之间的关系。作为旁注,为了使 MERGE 高效工作,你希望在图中创建一个约束,如下面的查询所示。
列表 11.2 为单词的值创建唯一约束
CREATE CONSTRAINT ON (w:Word) ASSERT w.value IS UNIQUE;
如果你想要探索这个第一个图,下一个查询将显示路径。
列表 11.3 返回图的路径
MATCH p=()-[r:NEXT]->() RETURN p
结果将看起来像图 11.6。

图 11.6 处理句子“在你升级电子邮件之前,你将无法发送新邮件”后的结果图
到目前为止,一切顺利——这个结果正是我们预期的。但是,模型可以改进。如前所述,同一个单词可以在不同的句子中后面跟多个其他单词。通过遵循所描述的模型,列表 11.1 由于最后的 CREATE 子句,在相同的两个单词之间产生了多个关系。如果我们摄入一个新句子,如“他说比尔·克林顿和希拉里·克林顿送他们的孩子去私立学校是可以的”,生成的图将看起来像图 11.7。

图 11.7 也处理“他说比尔·克林顿和希拉里·克林顿送他们的孩子去私立学校是可以的”后的结果图
注意,我们在 to 和 send 之间有多个关系。这个结果是正确的,并且在某些情况下肯定是有用的,但在这个案例中,我们想要优先考虑最有可能与当前单词相邻的单词。这个架构要求我们计算当前单词出现在每个单词组合中的关系数量。
我们可以通过在两个单词之间的关系上添加权重并使其在连接相同的两个单词时唯一来修改我们的图模型。结果架构如图 11.8 所示。

图 11.8 在关系上具有权重属性的新架构模型
新架构通过在连接单词的关系上添加权重属性来消除了多个关系的需要。这种架构的变化需要在查询中做小的修改。
列表 11.4 存储词对频率
WITH split("You will not be able to send new mail until you upgrade your
➥ email."," ") as words
UNWIND range(0,size(words)-2) as idx
MERGE (w1:Word {value:words[idx]})
MERGE (w2:Word {value:words[idx+1]})
MERGE (w1)-[r:NEXT]->(w2)
ON CREATE SET r.weight = 1
ON MATCH SET r.weight = r.weight + 1;
注意,最后的 CREATE 已经被 MERGE 替换,它要么在创建时(ON CREATE)创建,要么在匹配时(ON MATCH)更新 NEXT 关系上的权重属性。
练习
尝试使用之前的句子运行新的查询,并检查生成的图。请记住先清理您的数据库。² 检查 to 和 send 之间关系的权重。
拥有一个合适的模型在手,我们现在可以接近我们的原始问题。因为我们现在很难用我们个人的信息来训练一个模型,所以让我们使用包含一些文本的通用数据集。我们将使用手动注释子语料库(MASC)数据集,³ 这是一个从开放美国国家语料库(OANC)中抽取的包含 50 万个单词的书面文本和转录语音的平衡子集。表 11.2 显示了数据集中的一些文档示例。由于空间原因,我只复制了关键列。
表 11.2 MASC 数据集的样本项目
| 文件名 | 内容 |
|---|---|
| [MASC]/data/written/110CYL200.txt | 这需要一些时间和努力,但在 Goodwill 的帮助下,Jerry 能够与检察官办公室协商出一个付款计划,找到住处,并进行更彻底的求职。 |
| [MASC]/data/written/111348.txt | 我得到了上面的那份文件作为我的份额,为了隐藏这种钱,这对我来说成了问题,所以在我这里与联合国合作的英国联系人(他的办公室享有某些豁免权)的帮助下,我能够将包裹安全地送到一个完全远离麻烦的地方。 |
| [MASC]/data/written/111364.txt | 我非常清楚这封信可能会让你感到惊讶,我是 Dagmar,一个决定将我所拥有的一切捐给教堂、清真寺或你所在社区的任何慈善组织的垂死之人,通过你的帮助,因为在这里我的社区里我无法做到这一点,原因我稍后会向你解释。 |
| [MASC]/data/written/111371.txt | 想象一下,从你自己的家庭工作室中向北美和其他地区的数百万人们提供你的机会和产品的感觉。 |
我们将只使用文件 masc_sentences.tsv 中可用的制表符分隔的句子数据集。对于以下查询,请将此文件复制到您的 Neo4j 安装中的导入目录。为了导入文件中的所有句子并将它们分解,您需要运行以下查询(请记住先清理您的数据库)。
列表 11.5 导入 MASC 数据集并处理其内容
:auto USING PERIODIC COMMIT 500
LOAD CSV FROM "file:///masc_sentences.tsv" AS line
FIELDTERMINATOR '\t'
WITH line[6] as sentence
WITH split(sentence, " ") as words
FOREACH ( idx IN range(0,size(words)-2) |
MERGE (w1:Word {value:words[idx]})
MERGE (w2:Word {value:words[idx+1]})
MERGE (w1)-[r:NEXT]->(w2)
ON CREATE SET r.weight = 1
ON MATCH SET r.weight = r.weight + 1)
高级技巧 为了遍历句子中的单词,这个查询将 UNWIND 替换为 FOREACH(使用相同的索引范围)。UNWIND 子句将范围集合转换为结果行,在这种情况下返回大量数据。相反,FOREACH 执行 MERGE 操作而不返回任何内容。这个子句简化了执行并显著提高了性能。
让我们快速查看数据库。我们可以通过以下查询搜索最常见的 10 个单词组合。
列表 11.6 查找最常见的 10 个词对
MATCH (w1:Word)-[r:NEXT]->(w2)
RETURN w1.value as first, w2.value as second, r.weight as frequency
ORDER by r.weight DESC
LIMIT 10
结果如图 11.9 所示。

图 11.9 MASC 数据集中最常见的 10 个单词对
现在我们已经拥有了满足我们最初要求的所有组件:实现一个支持消息编写并建议下一个单词的工具。想法是取当前单词并查询我们的新图,以找到三个最可能的后继单词,使用关系上的权重。
列表 11.7 建议最可能单词的查询
MATCH (w:Word {value: "how"})-[e:NEXT]->(w2:Word)
RETURN w2.value as next, e.weight as frequency
ORDER BY frequency desc
LIMIT 3
这个查询将给我们展示在图 11.10 中的结果。

图 11.10 接下来最可能跟随“how”的单词
显然,对于以how作为最后一个单词的用户,建议的前三个最佳单词是to、the和much。不错!
练习
在数据库中尝试其他单词的结果。它们对您有意义吗?
如您从目前获得的结果中看到,结果相当不错,但我们还可以做得更好。我们不仅考虑最后一个单词,还可以考虑前两个甚至三个单词。这种方法将给我们提供提高建议质量的机会,但我们需要稍微改变数据库的结构。
再次强调,改变主意并随着解决方案的开发而细化数据库模型并没有什么不好。这种情况很常见:你心中有一个想法;相应地设计你的模型;然后意识到通过小小的改变,你可以得到更好或更快的成果,所以你改变模型并测试。你应该始终遵循这个过程;你不应该认为你的模型是最终的。在这方面,图提供了你需要的一切灵活性。在某些情况下,你甚至可以在不重新摄入所有内容的情况下调整你的模型。
让我们通过考虑用户写的最后两个(或三个)单词而不是一个单词来尝试这种模型细化。通过考虑两个(或三个)前一个单词来提高推荐质量的想法可以形式化为以下内容:
-
取用户最后写的两个(或三个)单词。
-
在数据库中搜索所有包含这些单词以相同顺序出现的句子。
-
找出下一个可能出现的单词。
-
将单词分组并计算每个单词的频率。
-
按频率(降序)排序。
-
推荐前三名。
这个过程在图 11.11 中得到了说明。

图 11.11 提高下一个单词推荐质量的模式
新模型应该允许我们重建句子,以便我们可以确定在哪些句子中单词以我们想要的特定顺序出现。当前模型无法做到这一点,因为它合并了句子,只更新了权重。原始信息丢失了。
到目前为止,我们有一些选择。一个选择是移除 Word 上的唯一约束,并在每次出现时复制所有单词(以及关系),但这种解决方案需要大量的磁盘空间,而不会增加任何具体的价值。我们可以通过使用图 11.12 中的模型做得更好。

图 11.12 我们模式的第三个版本
此模型保持了单词的唯一性,但通过在关系上添加一个 ID 来创建每个句子特有的关系。这样,通过通过 sentenceId 过滤关系,就有可能重建原始句子。这种方法比复制单词使用的磁盘空间更少,并且获得的结果将完全相同。因此,让我们清理我们的数据库,并使用新的模型重新加载。清理数据库的查询如下。
列表 11.8 使用 APOC 的 iterate 程序清理数据库
CALL apoc.periodic.iterate(
"MATCH (p:Word) RETURN p",
"DETACH DELETE p", {batchSize:1000, parallel:true})
在这种情况下,最好使用 apoc.periodic.iterate,因为数据库相当大;在单个事务中删除它可能需要一段时间,并且事务可能会失败。APOC 插件中的 iterate() 函数允许您将大提交拆分成更小的提交,并且操作可以并行执行,这将更快。当图数据库为空时,我们可以重新导入并处理文本。
列表 11.9 使用句子标识符的新导入查询
:auto USING PERIODIC COMMIT 100
LOAD CSV FROM "file:///masc_sentences.tsv" AS line
FIELDTERMINATOR '\t'
WITH line[6] as sentence, line[2] as sentenceId
WITH split(sentence," ") as words, sentenceId
FOREACH ( idx IN range(0,size(words)-2) |
MERGE (w1:Word {value:words[idx]})
MERGE (w2:Word {value:words[idx+1]})
CREATE (w1)-[r:NEXT {sentence: sentenceId}]->(w2))
如您所见,用于创建单词之间关系的 MERGE 已被第一个示例中的 CREATE 替换。但在这个情况下,一个新的属性,即句子,包含了一个句子标识符。如果您现在查看图,您会看到几乎每个节点都有很多关系进出。另一方面,现在您可以使用以下查询来根据当前和前面的单词建议下一个单词。
列表 11.10 考虑最后两个单词来建议下一个单词的查询
MATCH (w2:Word {value: "know"})-[r:NEXT]->(w3:Word {value: "how"})-[e:NEXT]->
➥ (w4:Word)
WHERE r.sentence = e.sentence
RETURN w4.value as next, count(DISTINCT r) as frequency
ORDER BY frequency desc
LIMIT 3
要根据最后三个单词建议一个单词,您可以使用以下查询。
列表 11.11 考虑最后三个单词来建议下一个单词的查询
MATCH (w1:Word {value: "you"})-[a:NEXT]->(w2:Word {value: "know"})-[r:NEXT]->
➥ (w3:Word {value: "how"})-[e:NEXT]->(w4:Word)
WHERE a.sentence = r.sentence AND r.sentence = e.sentence
RETURN w4.value as next, count(DISTINCT r) as frequency
ORDER BY frequency desc
LIMIT 3
如预期的那样,建议的质量要高得多,但代价是数据库更大、更复杂。最后两个查询在数据库中搜索特定的模式。在这种情况下,Cypher 语言帮助您在较高层次上定义您正在寻找的图模式;引擎将返回所有匹配该模式的节点和关系。
值得注意的是,我们最后定义的最后一个模式有一个微小的缺点。单词节点是唯一的,所以如果你有数百万个句子,这个模式将创建超级节点——即有数百万个关系进入、出去或两者都有的节点。在大多数情况下,这些密集节点代表了所谓的停用词:在大多数文本中频繁出现的词语,如冠词(a,the)、代词(he,she)和助动词(do,does,will,should)。如前所述,这样的密集节点在查询执行期间可能会成为一个问题,因为遍历它们所需的时间很高。对于本场景和本节中提出的解决方案,这种情况不会是一个大问题,但在第 11.2 节中,我们将探讨如何正确识别和处理这些词语。
11.1.1 图方法的优势
第一个场景展示了如何将文本表示为图的形式。句子被分割成简单的标记,这些标记由节点表示,而单词的顺序则由关系来维持。
尽管其简单性,最终设计的图模型完美地实现了我们心中的目的。你也看到了如何使你的模型适应新的需求或满足新的约束。
最后几个查询展示了如何在图中搜索特定的模式——这是通过像 Cypher 这样的适当图查询语言提供的图的一个极其强大的功能。使用其他方法,例如关系数据库,表达相同的概念将会复杂得多。简单性、灵活性(适应变化的能力)和强大:这些图特性在这个简单场景中表现得非常明显。
11.2 自然语言处理和图
在第 11.1 节中讨论的基本方法有很多局限性,其中一些我们在讨论过程中已经提到了。它很好地服务于建议下一个词基于用户先前输入的预期目的,但它不适合需要详细分析和理解文本的高级场景,例如本章引言中提到的对话代理、聊天机器人和高级推荐引擎。前例中未考虑的一些方面包括
-
词语没有被归一化到其基本形式(例如去除复数或考虑动词的变位的基本形式)。
-
我们没有考虑词语之间的依赖关系(例如形容词和名词之间的联系)。
-
一些词语放在一起更有意义,因为它们代表了一个单一实体。(例如,Alessandro Negro 是一个人,应该被视为一个单独的标记。)
-
停用词没有被正确识别和(最终)移除以防止密集节点。给出了一些例子,但基于语言和领域,有可用的列表。
-
仅使用空白字符进行分割通常是不够的。(例如,考虑像 can’t 这样的词以及可能附加在句子最后一个词上的所有类型的标点符号。)
本节描述了需要高级 NLP 任务的高级场景。它探讨了分解和正确分析文本数据的各种技术和工具,以及存储此类分析结果的图模型。这一阶段代表了在之上可以完成更高级任务的基本步骤。
文本通常被认为是无结构数据,但自由文本有很多结构。困难在于,大部分这种结构都不是显式的,这使得在文本中搜索或分析信息变得困难 [Grishman, 2015]。NLP 使用来自计算机科学、人工智能和语言学的概念来分析自然语言,目的是从文本中提取有意义的和有用的信息。
信息提取(IE**) 是理解文本和构建复杂、引人入胜的机器学习应用过程中的第一步。它可以描述为分析文本、分解它,并识别其中语义定义的实体和关系的过程,目的是使文本的语义结构明确。这种分析的结果可以记录在数据库中以便查询和推理,或者用于进一步分析。
IE 是一个多步骤的过程,涉及几个分析组件的组合,以从文本中提取最有价值的信息,使其可用于进一步处理。以下是一些主要任务,其中一些我们将在本节剩余部分详细讨论:
-
分词、词性标注(PoS)、词干提取/词形还原和停用词去除
-
命名实体识别(NER)
-
实体关系提取(ERE)
-
语法分析
-
共指消解
-
语义分析
这个列表并不是详尽的——其他任务也可以被认为是核心信息提取过程的一部分——但这些项目是最常见的,而且就信息量、结构和知识而言,我认为它们是最有价值且最有用的。每个 IE 任务的结果都必须被适当组织和存储,以便它们对其他过程和分析有用,或者可以查询。用于存储这些结果的模型至关重要,因为它会影响后续操作的性能。
在本节中,我们将探讨图如何为驯服文本提供一个完美的数据模型,因为它们允许我们组织文本中的结构和信息,以便它们可以立即用于查询、分析或提取其他过程所需的特征集。对于每个任务,都提出了一个图模型来正确存储结果。所提出的模型将从第一个任务持续增长到最后一个任务,并且生成的图将包含从文本中提炼出的所有可能的知识,并以统一的数据结构呈现。
为了简化描述并使其更具体,我将通过描述一个需要该技术的真实场景来描述这些任务中的几个。这个过程将是逐步的:在每一个阶段,新的信息将被添加到图中,到最后,我们将拥有处理过的完整语料库,结构化、可访问,并准备好进行下一步。让我们开始我们的旅程!
假设你想要将文本分解为其主要元素(最终将它们归一化到基本形式),获取文本中每个实体的角色,删除无用的单词,并以便于导航和查询搜索目的或进一步分析的方式存储结果。
这种场景相当常见,因为几乎所有信息提取(IE)过程的第一个步骤都是将内容分解成小的、可用的文本块,这些文本块被称为标记。这个过程被称为分词。通常,标记代表单个单词,但正如你很快就会看到的,构成一个小而可用的块可以针对特定应用。如第 11.1 节所述,对英语文本进行分词的最简单方法是基于空白(如空格和换行符)的出现来分割字符串,正如我们的简单分词器所做的那样。使用这种方法对以下句子进行处理
我简直不敢相信自己的眼睛,当我看到我最喜欢的球队赢得 2019-2020 赛季的奖杯时。
得到以下列表:
[“我”, “couldn’t”, “believe”, “my”, “eyes”, “when”, “I”, “saw”, “my”, “favorite”, “team”, “winning”, “the”, “2019-2020”, “cup.”]
这种方法正是我们之前使用的方法,但显然,这还不够。为了获得更好的分词效果,我们需要处理诸如标点符号、缩写、电子邮件地址、URL 和数字等问题。如果我们应用一个更复杂的分词方法,该方法使用诸如字母、数字和空白等标记类,输出应该类似于以下这样:
[“我”, “couldn”, “’”, “t”, “believe”, “my”, “eyes”, “when”, “I”, “saw”, “my”, “favorite”, “team”, “winning”, “the”, “2019”, “-”, “2020”, “cup”, “.”]
太好了!
在这个例子中,我们考虑了一个单独的句子,但在许多情况下,首先将文档分解成句子是相关的。在英语中,我们可以通过考虑诸如句号和问号之类的标点符号来完成这项任务。分词和句子分割受到几个因素的影响,其中两个最关键的因素是
-
语言—不同的语言有不同的规则。这些规则可以显著影响你执行甚至像分词这样简单的任务的方式。例如,中文中没有短语之间的空格,所以像英语中那样基于空格分割可能不起作用。
-
领域—某些领域有特定的元素,这些元素具有特定的结构。例如,在化学领域,考虑像 3-(furan-2-yl)-[1,2,4]triazolo[3,4-b][1,3,4]thiadiazole 这样的分子名称,以及在家庭装修零售领域的大小,如 60 in. X 32 in.。
即使使用更复杂的方法,单独的分词在大多数情况下也不够。当你有了标记列表时,你可以应用多种技术来获得更好的表示。最常见的技巧 [Farris 等人,2013] 是
-
大小写更改—这项任务涉及将标记的大小写更改为通用大小写,以便标记是一致的。然而,这个过程通常比将所有内容转换为小写要复杂得多:一些标记应该首字母大写,因为它们出现在句子的开头,而其他标记应该大写,因为它们是专有名词(如人名或地点名)。适当的案例更改会考虑这些因素。请注意,这项任务是特定于语言的;例如,在阿拉伯语中,没有小写或大写。
-
停用词去除—这项任务会过滤掉诸如 the、and 和 a 这样的常见词汇。这类经常出现的词汇通常对不依赖句子结构的应用程序价值不大(注意,我说的是 little 价值,而不是 no 价值)。这个停用词列表也是特定于应用程序的。如果你在处理像这样一本书,你可能想过滤掉那些对内容价值最小但出现频率较高的其他词汇,例如 chapter、section 和 figure。
-
扩展—一些标记可以通过添加同义词或扩展标记流中的缩写和缩写词来进一步扩展或澄清。这项任务可以使应用程序能够处理来自用户的替代输入。
-
词性标注—这项任务的目的在于识别一个词的词性——比如它是名词、动词还是形容词等。这项任务非常有价值,因为它在后续处理过程中被用来提高结果的质量。词性标注可以帮助确定文档中的重要关键词,例如(我们将在本节后面看到),或者支持正确的首字母大写(例如,Will 作为专有名词与 will 作为情态动词的区别)。
-
词形还原 和 词干提取——假设你想要在一组文档中搜索动词 take。简单的字符串匹配将不起作用,因为正如你所知,这样的动词可以以多种形式出现,如 take、took、taken、taking 和 takes。这些形式被称为 表面形式。动词是相同的,但它根据它在文本中扮演的角色和其他句法规则以不同的方式进行屈折。词形还原和词干提取是允许我们将单词还原到其根或基本形式的任务,例如通过将 take 的所有表面形式转换为它们的根形式。词形还原和词干提取之间的区别在于产生单词根形式的方法以及产生的单词。一般来说,词干提取使用语法规则,而词形还原使用基于字典的方法。因此,词干提取更快但准确性较低;词形还原较慢但更精确。
关于进一步阅读,我推荐本章末尾参考文献部分提到的这些主题的优秀实用书籍 [Lane 等人,2019;Farris 等人,2013]。这些书籍广泛涵盖了此处概述的步骤,并提供了具体的示例。
图 11.13 展示了前面列表中描述的一些任务应用于简单句子的情况。

图 11.13 对示例句子应用分词、停用词去除和词形还原
许多任务似乎都涉及信息提取,但现实是(除非你想实施大量自定义),各种编程语言的许多软件和库可以为你执行所有这些任务。以下示例演示了用于此类目的最常用的库之一:spaCy⁵ Python 库。
列表 11.12 使用 spaCy 进行基本文本处理
import spacy
class BasicNLP(object):
def __init__(self, language):
spacy.prefer_gpu() ❶
def tokenize(self, text):
nlp = spacy.load("en_core_web_sm") ❷
doc = nlp(text) ❸
i = 1
for sentence in doc.sents: ❹
print("-------- Sentence ", i, "-----------")
i += 1
for token in sentence: ❺
print(token.idx, "-", token.text, "-", token.lemma_) ❻
if __name__ == '__main__':
basic_nlp = BasicNLP(language="en")
basic_nlp.tokenize("Marie Curie received the Nobel Prize in Physics in
➥ 1903\. She became the first woman to win the prize.")
❶ 在可能的情况下优先使用 GPU(因为它更快)
❷ 加载英语语言模型
❸ 处理文本
❹ 遍历句子
❺ 遍历标记
❻ 打印索引(标记起始位置)、文本本身和词形还原版本
此列表⁶ 实现了一个基本示例,打印出分词的结果,如下所示:
-------- Sentence 1 -----------
0 - Marie - Marie - NNP
6 - Curie - Curie - NNP
12 - received - receive - VBD
21 - the - the - DT
25 - Nobel - Nobel - NNP
31 - Prize - Prize - NNP
37 - in - in - IN
40 - Physics - Physics - NNP
47 - in - in - IN
50 - 1903 - 1903 - CD
54 - . - . - .
-------- Sentence 2 -----------
56 - She - -PRON- - PRP
60 - became - become - VBD
67 - the - the - DT
71 - first - first - JJ
77 - woman - woman - NN
83 - to - to - TO
86 - win - win - VB
90 - the - the - DT
94 - prize - prize - NN
99 - . - . - .
我们应该如何在图模型中存储这个第一步的结果?通常情况下,没有唯一的正确答案;答案取决于你打算如何使用它。我将要介绍的是我们在 GraphAware Hume 中使用的方案,它已经证明足够灵活,可以覆盖大量场景而不会遇到任何特别困难。唯一的问题是它相当冗长,从某种意义上说,它存储了有时并不需要的大量数据。正如你将看到的,它提供了一个起点;你可以修剪一些部分或添加其他部分。
首先提出的方案是满足大量场景和需求所需的最小方案。此模型以下方面对于许多应用和使用至关重要:
-
句子节点—主文本被分割成句子,这是大多数文本分析用例(如摘要和相似度计算)中的关键元素。
-
标签出现节点—这些节点包含有关标签在文本中如何出现的信息,例如起始和结束位置、实际值和词元(词性值)。
-
HAS_NEXT 关系—标签出现节点之间存在类型为 HAS_NEXT 的关系,其范围与第 11.1 节中相同。通过这种方式,这个新架构结合并大量扩展了之前生成的架构,以便可以使用这个新模型解决之前的场景。
图 11.14 展示了架构。其中的注释应该能帮助你正确阅读,即使它可能看起来很复杂。

图 11.14 处理文本的正确第一个架构
通过添加标签节点来表示词元的归一化版本,可以略微改进此架构。这些节点是唯一的;它们只存储一次,包含此类标签的所有句子都通过标签出现节点指向它们。
由于前面提到的原因,常见单词可以生成密集节点。为了减轻这个问题,只有非停用词被存储为标签节点。结果模型将类似于图 11.15。

图 11.15 带有标签节点的扩展架构
当你想使用一些关键术语作为入口点来访问图数据库时,标签节点简化了导航。例如,你可以通过在标签节点上直接进行查询并使用关系来访问标签出现节点来实现这个任务。因为这些节点对我们来说不是关键,所以我们将忽略它们在架构、示例和练习中的内容,以使图更容易阅读,但请记住它们作为特定访问模式的选择。
拥有这个新模型后,我们可以扩展我们的文本处理代码并将其存储在图中。下面的列表比列表 11.12 更为复杂,它通过使用图 11.11 和 11.12 中描述的模型将文本转换为图。
列表 11.13 创建文本的第一个图
def tokenize_and_store(self, text, text_id, storeTag):
docs = self.nlp.pipe([text], disable=["ner"]) ❶
for doc in docs: ❷
annotated_text = self.create_annotated_text(doc, text_id)
i = 1
for sentence in doc.sents:
sentence_id = self.store_sentence(sentence, annotated_text,
➥ text_id, i, storeTag) ❸
i += 1
def create_annotated_text(self, doc, id): ❹
query = """MERGE (ann:AnnotatedText {id: $id})
RETURN id(ann) as result
""" ❺
params = {"id": id}
results = self.execute_query(query, params) ❻
return results[0]
def store_sentence(self, sentence, annotated_text, text_id, sentence_id,
➥ storeTag): ❼
sentence_query = """MATCH (ann:AnnotatedText) WHERE id(ann) = $ann_id
MERGE (sentence:Sentence {id: $sentence_unique_id})
SET sentence.text = $text
MERGE (ann)-[:CONTAINS_SENTENCE]->(sentence)
RETURN id(sentence) as result
""" ❽
tag_occurrence_query = """MATCH (sentence:Sentence) WHERE id(sentence) =
➥ $sentence_id
WITH sentence, $tag_occurrences as tags
FOREACH ( idx IN range(0,size(tags)-2) |
MERGE (tagOccurrence1:TagOccurrence {id: tags[idx].id})
SET tagOccurrence1 = tags[idx]
MERGE (sentence)-[:HAS_TOKEN]->(tagOccurrence1)
MERGE (tagOccurrence2:TagOccurrence {id: tags[idx + 1].id})
SET tagOccurrence2 = tags[idx + 1]
MERGE (sentence)-[:HAS_TOKEN]->(tagOccurrence2)
MERGE (tagOccurrence1)-[r:HAS_NEXT {sentence: sentence.id}]->
➥ (tagOccurrence2))
RETURN id(sentence) as result
""" ❾
tag_occurrence_with_tag_query = """MATCH (sentence:Sentence) WHERE
➥ id(sentence) = $sentence_id
WITH sentence, $tag_occurrences as tags
FOREACH ( idx IN range(0,size(tags)-2) |
MERGE (tagOccurrence1:TagOccurrence {id: tags[idx].id})
SET tagOccurrence1 = tags[idx]
MERGE (sentence)-[:HAS_TOKEN]->(tagOccurrence1)
MERGE (tagOccurrence2:TagOccurrence {id: tags[idx + 1].id})
SET tagOccurrence2 = tags[idx + 1]
MERGE (sentence)-[:HAS_TOKEN]->(tagOccurrence2)
MERGE (tagOccurrence1)-[r:HAS_NEXT {sentence: sentence.id}]->
➥ (tagOccurrence2))
FOREACH (tagItem in [tag_occurrence IN {tag_occurrences} WHERE
➥ tag_occurrence.is_stop = False] |
MERGE (tag:Tag {id: tagItem.lemma}) MERGE
➥ (tagOccurrence:TagOccurrence {id: tagItem.id}) MERGE (tag)<-
➥ [:REFERS_TO]-(tagOccurrence))
RETURN id(sentence) as result
""" ❿
params = {"ann_id": annotated_text, "text": sentence.text,
➥ "sentence_unique_id": str(text_id) + "_" + str(sentence_id)}
results = self.execute_query(sentence_query, params) ⓫
node_sentence_id = results[0]
tag_occurrences = []
for token in sentence: ⓬
lexeme = self.nlp.vocab[token.text]
if not lexeme.is_punct and not lexeme.is_space: ⓭
tag_occurrence = {"id": str(text_id) + "_" + str(sentence_id) +
➥ "_" + str(token.idx),
"index": token.idx,
"text": token.text,
"lemma": token.lemma_,
"pos": token.tag_,
"is_stop": (lexeme.is_stop or lexeme.is_punct
➥ or lexeme.is_space)}
tag_occurrences.append(tag_occurrence)
params = {"sentence_id": node_sentence_id,
➥ "tag_occurrences":tag_occurrences}
if storeTag:
results = self.execute_query(tag_occurrence_with_tag_query, params)⓮
else:
results = self.execute_query(tag_occurrence_query, params) ⓯
return results[0]
❶ 不使用 NER 处理文本(提高性能)
❷ 遍历处理过的文档。管道接受文档列表并返回处理过的文档列表。
❸ 遍历句子,对每个句子调用存储函数
❹ 这个函数创建带有标签 AnnotatedText 的主要节点,所有其他节点都将连接到它。
❺ 创建 AnnotatedText 节点的查询很简单,只存储一个 ID 来标识原始文档。
❻ 这个常用函数执行代码中的所有查询。它需要查询和参数,并在事务中执行查询。
❺ 这个函数存储句子以及标签出现和标签。
❽ 此查询通过 id 搜索创建的 AnnotatedText 节点,创建一个句子,并将其连接到 AnnotatedText 节点。
❾ 此查询存储 TagOccurrence 节点,将它们连接到句子和彼此。
❿ 此查询存储 TagOccurrence 节点,将它们连接到句子和彼此以及 Tag 节点。它用作存储 Tag 节点的替代方案,用于之前的 tag_occurrence_ 查询。
⓫ 运行查询以存储句子
⓬ 遍历为每个句子提取的标记,并创建一个字典数组,用作存储句子的查询参数
⓭ 此过滤器避免存储标点和空格。
⓮ 使用 Tag 执行查询
⓯ 不使用 Tag 执行查询
在此代码中,通过一个参数(storeTag),可以决定是否存储 Tag 节点。因为 Tag 节点在本节的其余部分中不是必需的,所以此标志设置为 false,这将导致数据库更加简洁,并帮助我们避免密集节点问题。
分词是根据特定的分割规则来拆分文本的,这些规则通常比使用空格和标点符号要复杂一些。尽管如此,你可能希望从文本中获得更多信息。句子中的标记不是孤立的组件;它们通过语言关系相互关联。例如,句法关系可以捕捉一个词在句子中改变其他词语义的角色,并有助于确定主语和谓语。在前面使用的例子中,
当我看到我最喜欢的队伍赢得 2019-2020 赛季的奖杯时,我简直不敢相信自己的眼睛。
I 与 believe 在句法上相关,因为它是这个动词的主语。捕捉这些类型的依赖关系对于进一步理解文本至关重要:它们允许我们在管道的后续步骤中确定语义关系(谁对谁做了什么)。一般来说,这一阶段的丰富分析简化了后续的语义分析。
因此,让我们扩展我们之前的场景,添加一个新的要求:你希望识别文本中的关键句法元素(如动词及其主语和谓语),以改善对文本的理解,以便进行进一步分析。在迄今为止提出的各种解析方法中,关注于识别文本中依赖结构的依赖解析受到了最多的关注。图 11.16 显示了通过使用 CoreNLP 测试服务为我们示例句子获得的依赖解析。7

图 11.16 通过 corenlp.run 获得的标记之间的依赖关系
我希望到这本书的这一部分,你能够立即识别出哪里可以应用图。在这种情况下,它是一种特殊类型的图:一棵树。在依存句法分析中,每个句子都表示为一棵树,其根是句子的主要谓语或标记为根的虚拟节点,该虚拟节点以主要谓语作为其唯一的子节点 [Mihalcea and Radev, 2011]。边用于将每个词与其依存父节点连接起来。在句子“John likes green apples”中,谓语是 likes。它有两个论元:喜欢者(John)和被喜欢者(apples)。单词 green 修饰 apples,因此它被添加到树中作为 apples 的子节点。最终的树如图 11.17 所示。

图 11.17 依赖树的示例
将这些新的句法关系到我们的图模型中是直接的。图 11.18 展示了它是如何工作的。

图 11.18 带有依赖关系的扩展模式
在生成的图模型中,新的关系将连接 TagOccurrence 节点与依赖节点。这种连接是必要的,因为同一个标签在不同的句子中可能具有不同的关系(例如,“John”可能在某些句子中是主语,在其他句子中是宾语),而 TagOccurrence 代表特定句子上下文中的标签,并且只能具有特定的角色。关系的方向遵循图 11.14 中的模式,依赖树的根(主动词)可以通过自环识别。以下列表是提取和存储图中的依赖关系的代码。
列表 11.14 提取和存储依赖关系
def store_sentence(self, sentence, annotated_text, text_id, sentence_id,
➥ storeTag):
[... the same code as before ...]
params = {"ann_id": annotated_text, "text": sentence.text,
➥ "sentence_unique_id": str(text_id) + "_" + str(sentence_id)}
results = self.execute_query(sentence_query, params)
node_sentence_id = results[0]
tag_occurrences = []
tag_occurrence_dependencies = []
for token in sentence: ❶
lexeme = self.nlp.vocab[token.text]
if not lexeme.is_punct and not lexeme.is_space:
tag_occurrence_id = str(text_id) + "_" + str(sentence_id) + "_" +
➥ str(token.idx)
tag_occurrence = {"id": tag_occurrence_id,
"index": token.idx,
"text": token.text,
"lemma": token.lemma_,
"pos": token.tag_,
"is_stop": (lexeme.is_stop or lexeme.is_punct
➥ or lexeme.is_space)}
tag_occurrences.append(tag_occurrence)
tag_occurrence_dependency_source = str(text_id) + "_" +
➥ str(sentence_id) + "_" + str(token.head.idx)
dependency = {"source": tag_occurrence_dependency_source,
➥ "destination": tag_occurrence_id, "type": token.dep_} ❷
tag_occurrence_dependencies.append(dependency)
params = {"sentence_id": node_sentence_id,
➥ "tag_occurrences":tag_occurrences}
if storeTag:
results = self.execute_query(tag_occurrence_with_tag_query, params)
else:
results = self.execute_query(tag_occurrence_query, params)
self.process_dependencies(tag_occurrence_dependencies) ❸
return results[0]
def process_dependencies(self, tag_occurrence_dependencie):
tag_occurrence_query = """UNWIND $dependencies as dependency
MATCH (source:TagOccurrence {id: dependency.source})
MATCH (destination:TagOccurrence {id: dependency.destination})
MERGE (source)-[:IS_DEPENDENT {type: dependency.type}]->(destination)
""" ❹
self.execute_query(tag_occurrence_query, {"dependencies":
➥ tag_occurrence_dependencie})
❶ 受存储依赖关系更改影响的代码片段
❷ 准备包含依赖信息的字典,并将其附加到依赖关系列表中。
❸ 在创建 TagOccurrence 节点之后,会调用一个特定函数来存储它们之间的依赖关系。
❹ 查询通过依赖关系进行,搜索 TagOccurrence 节点,并将它们连接起来。
在这个阶段,生成的图包含句子、标记(词形还原、标记为停用词,并带有词性信息)以及描述它们在句子中角色的标记之间的关系。这是一些可以服务于广泛用例的大量信息,例如以下内容:
-
下一词建议——与 11.1 节中的下一个模式模型一样,考虑到当前词或任何数量的前一个词,可以建议下一个词。
-
高级搜索引擎——当我们有了关于词的顺序以及它们之间依存关系的信息时,我们可以实现高级搜索功能,除了检查词的精确顺序外,还可以考虑目标词之间有一些词的情况,并提供一些建议。具体示例如下。
-
基于内容的推荐——通过将文本分解成组件,我们可以比较项目描述(电影、产品等)。这一步是提供基于内容推荐的第一步要求。在这种情况下,如果已经实现了词干提取和其他规范化(如停用词去除、标点符号处理等),将使比较更加准确。
带着模式和手头的代码,让我们尝试完成一个具体任务。假设你有以下三个句子:
-
“John likes green apples.”
-
“Melissa picked up three small red apples.”
-
“That small tree produces tasty yellow apples.”
练习
使用列表 11.13 和 11.14 将三个句子导入图表。要查找包含单词 apples 的所有文档,可以使用以下查询。
列表 11.15 搜索包含单词 apples 的文档
WITH "apples" as searchQuery
MATCH (t:TagOccurrence)<-[*2..2]-(at:AnnotatedText)
WHERE t.lemma = searchQuery OR t.text = searchQuery
RETURN at
虽然这可以用任何搜索引擎完成,但现在让我们考虑一个更复杂的使用案例:搜索 小苹果。使用搜索引擎,你有两种选择:按特定顺序搜索这些词,或者搜索文档中这两个词的任何顺序。在前一种情况下,你将不会得到任何结果(因为 red 出现在这两个词之间),而在后一种情况下,你将得到两个文档(因为这两个词也出现在第三个文档中)。这种场景正是我们创建的图模型显示其力量的地方。以下是执行此搜索的查询。
列表 11.16 搜索小苹果
WITH "small" as firstWord, "apples" as secondWord
MATCH (t0:TagOccurrence)-[:HAS_NEXT*..2]-(t1:TagOccurrence)
WHERE (t0.lemma = firstWord or t0.text = firstWord) AND (t1.lemma =
➥ secondWord or t1.text = secondWord)
MATCH (t1)-[:IS_DEPENDENT]->(t0)<-[*2..2]-(at:AnnotatedText) ❶
return at
❶ 这一行检查两个标记之间是否存在句法依存关系。
练习
通过图表,我们可以用与在搜索引擎中相同的方式表达查询。编写查询以找到包含确切短语 小苹果 的文档,以及找到包含这两个词以任何顺序出现的文档。
让我们再来看一个例子,这个例子展示了自然语言处理(NLP)与图表方法的结合力量。正如以下查询所展示的,我们可以使用图表来回答甚至更复杂的问题,这为信息检索、聊天机器人和对话平台等应用奠定了基础。
列表 11.17 回答问题“苹果是什么样的?”
WITH "apples" as searchQuery
MATCH (t0:TagOccurrence)
WHERE (t0.lemma = searchQuery or t0.text = searchQuery)
MATCH (t0)-[:IS_DEPENDENT {type: "amod"}]->(t1:TagOccurrence)
return t1.text
图形能够无需任何人工训练努力回答复杂问题。分解文本并建立适当的图结构使我们能够做很多事情。第十二章通过构建适当的知识图来扩展这一想法,该知识图为更复杂的场景提供支持。
11.2.1 图形方法的优点
本节清楚地展示了自然语言处理(NLP)和图如何很好地协同工作。由于 NLP 任务产生数据的强连接性,将这些结果存储在图模型中似乎是一个逻辑上合理的、理性的选择。在某些情况下,例如与句法依赖关系一样,关系作为 NLP 任务的输出生成,图模型只需存储它们。在其他情况下,模型被设计为同时服务于多个范围,提供易于导航的数据结构。
这里提出的图模型不仅存储了信息提取过程中提取的主要数据和关系,还允许通过在后期处理阶段添加新信息进行扩展:相似度计算、情感提取等。因此,我们只需付出相对较小的努力,就可以服务于词建议用例以及更复杂的搜索需求,甚至问答(“苹果是什么样的?”)。
在第十二章中进一步扩展了此模型,包括从文本和后处理结果中提取的更多信息,使其能够服务于更多场景和应用。
摘要
本章介绍了与 NLP 和知识表示相关的关键概念,将它们与图和图模型相匹配。主要观点是,图不仅可以存储文本数据,还可以提供处理文本的概念工具集,并通过最小努力提供高级功能——例如,使复杂的搜索场景成为可能。
涵盖的主题包括
-
如何以最简单的方式存储文本,通过将其分解成易于导航的块
-
如何从文本中提取有意义的信息集
-
如何设计一个强大的图模型来存储文本并在不同的应用程序中访问它
-
如何根据不同的目的查询图,例如搜索、问答和词建议
参考文献
[Farris et al., 2013] Farris, Andrew L., Grant S. Ingersoll, 和 Thomas S. Morton. 文本驯服:如何查找、组织和操作它. 纽约州舍托岛:Manning, 2013。
[Grishman, 2015] Grishman, Ralph. “信息提取。” IEEE 智能系统 30:5 (2015): 8-15.
[Lane et al., 2019] Lane, Hobson, Cole Howard, 和 Hannes Hapke. 自然语言处理实战. 纽约州舍托岛:Manning, 2019。
[Mihalcea and Radev, 2011] Mihalcea, Rada, 和 Dragomir Radev. 基于图的自然语言处理和信息检索. 纽约:剑桥大学出版社,2011。
[Turnbull and Berryman, 2016] Turnbull, Doug, 和 John Berryman. 相关搜索:适用于 Solr 和 Elasticsearch 的应用. 纽约州舍托岛:Manning, 2016。
(1.)mng.bz/y9Dq.
(2.)使用 MATCH (n) DETACH DELETE n.
(3.)文件可以在此处下载:mng.bz/MgKn.
(4.)我明确搜索了一个复杂名称,并在这里找到了它:mng.bz/aKzB.
代码可在本书的仓库中找到,位于 ch11/basic_nlp_examples/01_spacy_basic_nlp_tasks.py。
12 知识图谱
本章涵盖
-
介绍知识图谱及其应用
-
从文本中提取实体和关系以创建知识图谱
-
在知识图谱之上使用后处理技术:语义网络
-
自动提取主题
在本章中,我们将继续第十一章开始的工作:将文本分解成一组有意义的 信息并将其存储在图中。在这里,我们有一个明确的目标:构建知识图谱。
这样,我们将完成 11 章前开始的旅程,即通过使用图作为核心技术和心理模型来管理和处理数据。知识图谱代表了整本书讨论内容的顶峰。在前面的章节中,你学习了如何存储和处理用户-项目交互数据以提供不同形式和形状的推荐,如何处理交易数据和社会网络以打击欺诈,等等。现在,我们将深入探讨如何从非结构化数据中提取知识。
本章比其他章节要长一些,内容也相当密集。你需要通读全文,才能理解不仅如何从文本数据中构建知识图谱,而且如何利用它来构建高级服务。通过图表和具体示例,我试图使本章更容易阅读和理解;请在阅读时仔细查看它们,以确保你掌握了关键概念。
12.1 知识图谱:简介
在第三章中,我介绍了利用知识将数据转化为洞察和智慧的概念,使用图 12.1 中的系列图像。正如你所见,整个过程都是关于连接信息,而图是理想的表示方式。

图 12.1 由 David Somerville 绘制,基于 Hugh McLeod 的原始作品
知识图谱以一种无与伦比的方式解决了机器学习中知识表示的反复问题(想想我在本书中所有关于知识表示的讨论!),并为知识推理提供了最佳工具,例如从表示的数据中得出推论。
当谷歌在 2012 年一篇开创性的博客文章¹中宣布其知识图谱将使用户能够搜索“事物,而不是字符串”时,知识图谱进入了公众视野。该文章解释说,当用户搜索“Taj Mahal”时,这个字符串被分成两个同等重要的部分(单词),搜索引擎试图将它们与所有文档匹配。但实际情况是,用户并不是在搜索两个独立的单词,而是在搜索一个具体的“事物”,无论是位于阿格拉的美丽纪念碑、一家印度餐厅,还是获得格莱美奖的音乐家。名人、城市、地理特征、事件、电影——这些都是用户在搜索特定对象时希望得到的结果类型。返回与查询真正相关的信息,极大地改变了搜索过程中的用户体验。
谷歌将这种方法应用于其核心业务——网络搜索。从用户的角度来看,最引人注目的功能之一是除了由关键词(基于字符串)搜索产生的按排名排列的网页列表之外,谷歌还显示了一个结构化的知识卡片——一个包含关于可能与搜索词相对应的实体(如图 12.2)的总结信息的盒子。

图 12.2 当前对于字符串“taj mahal”的结果。注意右边的盒子。
搜索只是开始。在谷歌发布博客文章后的几年里,知识图谱开始进入信息检索领域:数据库、语义网、人工智能(AI)、社交媒体和企业信息系统 [Gomez-Perez et al., 2017]。多年来,各种倡议扩展和发展了谷歌最初引入的概念。引入了额外的功能、新的想法和见解,以及一系列应用,因此,知识图谱的概念已经变得非常广泛,包括新的方法和技术。
但什么是知识图谱?什么使一个普通图成为知识图谱?没有黄金标准,普遍接受的定义,但我最喜欢的是 Gomez-Perez 等人给出的定义:“知识图谱由一组相互连接的具有类型的实体及其属性组成。”
根据这个定义,知识图谱的基本单元是实体的表示,例如人、组织或地点(如泰姬陵的例子),或者可能是体育赛事、书籍或电影(如在推荐引擎的情况下)。每个实体可能有各种属性。对于人来说,这些属性可能包括姓名、地址、出生日期等。实体通过关系相互连接。例如,一个人可能“为”一家公司工作,或者用户喜欢一个页面或关注另一个用户。关系也可以用来连接两个不同的知识图谱。
与其他以知识为导向的信息系统相比,知识图谱的特点在于其独特的组合
-
知识表示结构和推理,例如语言、模式、标准词汇表以及概念之间的层次结构
-
信息管理流程(信息如何被摄取并转换为知识图谱)
-
访问和处理模式,例如查询机制、搜索算法以及预处理和后处理技术
正如我们在整本书中所做的那样,我们将使用标签属性图(LPG)来表示知识图谱——这与常规做法不同,因为知识图谱通常使用资源描述框架(RDF)数据模型来表示。RDF 是 W3C 的互联网数据交换标准,设计为一种表示关于网络资源信息(如网页的标题、作者和修改日期,或关于网络文档的版权和许可信息)的语言。但通过泛化网络资源的概念,我们也可以使用 RDF 来表示关于其他事物(如在线商店可用的商品或用户对信息传递的偏好)的信息 [RDF 工作组,2004]。
RDF 中任何表达式的底层结构是一组三元组,每个三元组由一个主语、一个谓语和一个宾语组成。每个三元组可以表示为一个节点-弧-节点链接,也称为RDF 图,其中主语是一个资源(图中的一个节点),谓语是弧(一个关系),宾语是另一个节点或一个字面值。图 12.3 显示了这种结构的外观。

图 12.3 简单的 RDF 图
RDF 旨在处理其编码的信息需要由应用程序处理的情况,而不是仅向人们展示。它提供了一个通用的框架来表示此类信息,以便在没有意义损失的情况下在应用程序之间交换。这个框架使得它比 LPG(通过使用具有属性的关系和节点以紧凑的方式存储复杂图而设计的)更冗长,对人类来说可读性较差。请参考图 12.4 中的示例,这是耶稣·巴拉斯在一篇博客文章²中的内容。

图 12.4 LPG 与 RDF 图对比
LPG 在表示知识图谱方面比 RDF 图更灵活、更强大。值得注意的是,本章重点介绍如何构建和访问由文本数据创建的知识图谱。从结构化数据构建知识图谱绝对是一个更简单的任务,这是我们已经在之前多个场景中完成过的。
当从文本中获取知识图谱时,使用本章将要探讨的技术,它会被后处理或增强以提取洞察和智慧。图 12.5 展示了整个过程,我们将在本章中详细探讨。

图 12.5 整个过程的心智地图
从文本中提取结构的技术将通过识别关键实体及其之间的关系得到扩展。这些技术对于知识图谱的创建至关重要。因为相同的实体和关系往往在特定领域语料库的文档中反复出现,因此推断一个代表这种信息的通用模型非常重要,该模型从文本中出现的实例中抽象出来。这个模型被称为推断知识图谱。此过程的结果代表了一个知识库,它可以用于多种高级机器学习技术,或者更普遍地说,用于 AI 应用。表示知识库最常见的方法之一是通过语义网络:一组概念及其之间预定义的连接。
12.2 知识图谱构建:实体
从文本数据构建知识图谱的一个关键元素是识别文本中的实体。假设你有一组文档(例如来自维基百科),你被要求在这些文档中找到与你领域相关的人或其他实体的名称,如地点、组织等。当提取此信息时,你必须通过图使其易于访问,以便进一步探索。
命名实体识别(NER)的任务涉及在文本中找到每个命名实体(NE)的提及并标记其类型[Grishman and Sundheim, 1996]。NE 类型的构成取决于领域;人物、地点和组织是常见的,但 NE 可以包括各种其他结构,例如街道地址、时间、化学公式、数学公式、基因名称、天体名称、产品以及品牌——简而言之,任何与您应用相关的信息。用通用术语来说,我们可以将 NE 定义为任何可以用适当名称指代且与我们要考虑的分析领域相关的东西。例如,如果您正在处理用于某些医疗用例的电子病历,您可能希望识别患者、疾病、治疗方法、药物、医院等等。正如前面的例子所暗示的,许多命名实体具有超语言结构,这意味着它们是根据与通用语言规则不同的规则组成的。这个术语也通常被扩展到包括不是实体本身的东西:数值(如价格)、日期、时间、货币等等。每个 NE 都与一个特定类型相关联,该类型指定了其类别,例如 PERSON、DATE、NUMBER 或 LOCATION。领域至关重要,因为同一个实体可以根据领域与不同的类型相关联。
当一个文本中的所有命名实体(NE)都被提取出来后,它们可以与对应于现实世界实体的集合相链接,这使我们能够推断,例如,“United Airlines”和“United”的提及指的是同一家公司[Jurafsky and Martin, 2019]。假设您有以下文档:
Marie Curie,Pierre Curie 的妻子,于 1911 年获得了诺贝尔化学奖。她之前在 1903 年获得了诺贝尔物理学奖。
需要提取的实体集合将根据您分析目标的相关实体而变化,但假设我们对所有实体都感兴趣,一个合适的命名实体识别(NE)结果应该能够识别并将“Marie Curie”和“Pierre Curie”识别为人物名称,将“Nobel Prize in Chemistry”和“Nobel Prize in Physics”识别为奖项名称,以及将“1911”和“1903”识别为日期。这项任务对人类来说很简单,但对机器来说并不那么简单。您可以通过使用像开源 displaCy 这样的 NE 可视化器来尝试它。³ 如果您粘贴前面的文本并选择所有实体标签,您将得到类似于图 12.6 的结果。

图 12.6 displaCy NE 可视化器对我们样本文本的结果
有趣的是,无需任何调整,该服务就能识别句子中的所有实体(尽管奖项被分类为“艺术品”)。
将 NER 任务添加到图模型中很简单。如图 12.7 所示,最佳解决方案是添加带有标签 NamedEntity 的新节点,其中包含从文档中提取的实体。这些节点链接到任何相关的 TagOccurrence 节点(例如,“Marie Curie”是一个由两个 TagOccurrence 节点“Marie”和“Curie”组成的单个名称)。为文本中实体的每个出现创建 NamedEntity 节点,因此“Marie Curie”可以以不同的节点出现多次。在本节稍后,我们将看到如何将它们链接到一个表示“Marie Curie”作为人的特定实体的公共节点。

图 12.7 带有 NER 的扩展模式
从第十一章扩展我们的数据模型以在图中存储 NER 任务的输出相当简单。以下列表包含从文本中提取 NE 并存储所需的更改。完整代码位于 ch12/04_spacy_ner_schema.py 和 ch12/text_processors.py 文件中。
列表 12.1 将 NER 任务添加到模型中
def tokenize_and_store(self, text, text_id, storeTag):
docs = self.nlp.pipe([text])
for doc in docs:
annotated_text = self.__text_processor.create_annotated_text(doc,
➥ text_id)
spans = self.__text_processor.process_sentences(annotated_text, doc,
➥ storeTag, text_id)
nes = self.__text_processor.process_entities(spans, text_id) ❶
def process_entities(self, spans, text_id): ❷
nes = []
for entity in spans:
ne = {'value': entity.text, 'type': entity.label_, 'start_index':
➥ entity.start_char,
'end_index': entity.end_char}
nes.append(ne)
self.store_entities(text_id, nes)
return nes
def store_entities(self, document_id, nes): ❸
ne_query = """ ❹
UNWIND $nes as item
MERGE (ne:NamedEntity {id: toString($documentId) + "_" +
➥ toString(item.start_index)})
SET ne.type = item.type, ne.value = item.value, ne.index =
➥ item.start_index
WITH ne, item as neIndex
MATCH (text:AnnotatedText)-[:CONTAINS_SENTENCE]->(sentence:Sentence)-
➥ [:HAS_TOKEN]->(tagOccurrence:TagOccurrence)
WHERE text.id = $documentId AND tagOccurrence.index >=
➥ neIndex.start_index AND tagOccurrence.index < neIndex.end_index
MERGE (ne)<-[:PARTICIPATES_IN]-(tagOccurrence)
"""
self.execute_query(ne_query, {"documentId": document_id, "nes": nes})
❶ 添加新的步骤以提取和存储命名实体
❷ 该函数接收 NLP 处理的结果并提取命名实体。
❸ 函数将实体存储在图中。
❹ 查询遍历实体,并为每个实体创建一个新节点,并将其链接到组成 NE 的标签。
如您所见,所需的更改在管道和保存 NER 任务结果的代码方面都很小。spaCy 有自己的基本 NE 模型,这是我们在这段代码中使用过的,但它还提供了通过传递标注句子样本来训练新的 NER 模型的机会。请参阅 spaCy 文档⁴ 了解详情。
在书面和口语语言中,如果提到一个人、一个地点或其他相关实体多次,后续提及通常不会重复完整的名称。因此,在前面给出的例子中,我们可能会看到缩写名称(“Mme. Curie”)、代词(“她”)或描述性短语(“著名的科学家”)。此时的问题是如何识别这些关系并将它们从普通文本中提取出来。
我们可以通过添加另一个要求来进一步开发我们的场景。假设您想通过考虑所有命名实体的提及来改进您的访问模式。作为一个具体的例子,在以下文本中,我们希望将“她”与“Marie Curie”连接起来:
玛丽·居里在 1903 年获得了诺贝尔物理学奖。她成为了第一个获奖的女性,也是第一个两次获奖的人—无论是男性还是女性。
在自然语言处理(NLP)中,这个任务是通过共指消解来完成的,它被定义为识别文本中实体引用之间关系的问题,无论它们是否由名词或代词表示[Mihalcea 和 Radev, 2011]。解决代词引用涉及约束和偏好的组合:先行词必须与代词匹配(在数量、性别等方面),并且作为先行词,我们更倾向于主语而不是宾语,文本中离代词更近的词,以及可能在代词语境中出现的词[Grishman, 2015]。典型的共指消解算法试图通过使用基于规则的系统来识别引用链,尽管最终标准是基于对语料库中大量文本的统计或使用机器学习分类器。
链接一般的共指名词短语是一个更困难的任务。一些简单的情况使用相同的名词多次,但大多数例子需要一些世界知识,基于观察在其他地方用来指代特定实体的短语。这种方法允许我们将“著名的波兰科学家”解析为“玛丽·居里”,或将“奖项”解析为“诺贝尔奖”。
提出的基于图的方法用于共指消解[Nicolae 和 Nicolae, 2006; Ng, 2009]使用图割算法来近似文本中实体引用的正确分配,但这些方法超出了本书的范围,因为我们在代码库中使用的 NLP 库有自己的共指实现。这里的重点是讨论如何建模此类任务的结果并充分利用它。
考虑我们的示例文本。图 12.8 显示了使用第十一章中提到的斯坦福 CoreNLP 测试服务获得的结果。

图 12.8 共指结果
我们可以通过将代词和其他引用链接到它们所指向的真实实体来在我们的图模型中表示这些连接。图 12.9 显示了扩展到包括共指消解的模型。通常,图提供了必要的灵活性,以最小的努力适应新的需求,同时保持先前的访问模式有效运行。

图 12.9 带有共指消解的图模型
扩展的图模型通过使用 MENTIONS 关系连接命名实体节点。以下列表显示了存储新共指的代码更改。完整代码位于 ch12/05_spacy_coref_schema.py 和 ch12/text_processors.py。
列表 12.2 提取共指
def __init__(self, language, uri, user, password):
spacy.prefer_gpu()
self.nlp = spacy.load('en_core_web_sm')
coref = neuralcoref.NeuralCoref(self.nlp.vocab)
self.nlp.add_pipe(coref, name='neuralcoref'); ❶
self._driver = GraphDatabase.driver(uri, auth=(user, password),
➥ encrypted=0)
self.__text_processor = TextProcessor(self.nlp, self._driver)
self.create_constraints()
def tokenize_and_store(self, text, text_id, storeTag):
docs = self.nlp.pipe([text])
for doc in docs:
annotated_text = self.__text_processor.create_annotated_text(doc,
➥ text_id)
spans = self.__text_processor.process_sentences(annotated_text, doc,
➥ storeTag, text_id)
nes = self.__text_processor.process_entities(spans, text_id)
coref = self.__text_processor.process_co-reference(doc, text_id) ❷
def process_co-reference(self, doc, text_id):
coref = []
if doc._.has_coref: ❸
for cluster in doc._.coref_clusters:
mention = {'from_index': cluster.mentions[-1].start_char,
➥ 'to_index': cluster.mentions[0].start_char}
coref.append(mention)
self.store_coref(text_id, coref)
return coref
def store_coref(self, document_id, corefs):
coref_query = """ ❹
MATCH (document:AnnotatedText)
WHERE document.id = $documentId
WITH document
UNWIND $corefs as coref
MATCH (document)-[*3..3]->(start:NamedEntity), (document)-
➥ [*3..3]->(end:NamedEntity)
WHERE start.index = coref.from_index AND end.index =
➥ coref.to_index
MERGE (start)-[:MENTIONS]->(end)
"""
self.execute_query(coref_query,
{"documentId": document_id, "corefs": corefs})
❶ 在 spaCy 的 NLP 管道中添加一个新的共指元素,这是一个共指消解的神经网络实现(见github.com/huggingface/neuralcoref)。
❷ 提取共指并将它们存储在图中
❸ 在文档中找到的共指上进行循环,并为在图中存储它们创建字典
❹ 查询通过 MENTIONS 将命名实体连接起来。
共指关系对于将所有关键 NE 的提及与来源连接起来很有用,即使它们的规范名称没有被使用。
NEs 和共指在知识图谱构建中扮演着重要角色。它们都是一等对象,代表文本中相关实体及其相互关系的发生。但为了提高从图中提取知识的质量,有必要从这些文本发生中抽象出来,并识别出在文本中被多次提及的关键实体。自然语言理解系统(以及人类)根据话语模型 [Karttunen, 1969]来解释语言表达——这是一个系统在处理语料库(或人类听者的情况,来自对话)中的文本时逐步构建的心理模型,其中包含文本中提及的实体的表示,以及实体的属性和它们之间的关系 [Jurafsky and Martin, 2019]。我们说如果两个提及与同一实体相关联,则它们是共指的。
话语模型的概念可以应用于知识图谱用例中,以简化并改进对其所体现知识的访问。如图 12.10 所示,我们可以构建知识图谱的补充——本章引言中提到的推断知识图谱——它包含处理文本中提及的实体的唯一表示,以及它们的属性和它们之间的关系。

图 12.10 包含推断知识的知识图谱
虽然知识图谱的主体包含语料库中文本的分解,并最终在图模型中重新组织为结构化数据,但这一部分——与第一部分相连——提炼了关键元素和关系,以回答不同的问题并支持多种服务,消除了导航整个图的需要。这个推断知识图谱包含一个易于共享的知识表示,它不直接连接到从中提取此知识的特定实例(文档)。
下面的列表显示了在共指消解任务之后如何应用此推断来逐步构建知识图谱的第二部分。该函数在 ch12/text_processors.py 中调用,来自 ch12/06_spacy_entity_relationship_extraction.py。
列表 12.3 创建推断知识图谱
def build_entities_inferred_graph(self, document_id): ❶
extract_direct_entities_query = """ ❷
MATCH (document:AnnotatedText)
WHERE document.id = $documentId
WITH document
MATCH (document)-[*3..3]->(ne:NamedEntity)
WHERE NOT ne.type IN ['NP', 'NUMBER', 'DATE']
WITH ne
MERGE (entity:Entity {type: ne.type, id:ne.value})
MERGE (ne)-[:REFERS_TO {type: "evoke"}]->(entity)
"""
extract_indirect_entities_query = """ ❸
MATCH (document:AnnotatedText)
WHERE document.id = $documentId
WITH document
MATCH (document)-[*3..3]->(ne:NamedEntity)<-[:MENTIONS]-(mention)
WHERE NOT ne.type IN ['NP', 'NUMBER', 'DATE']
WITH ne, mention
MERGE (entity:Entity {type: ne.type, id:ne.value})
MERGE (mention)-[:REFERS_TO {type: "access"}]->(entity)
"""
self.execute_query(extract_direct_entities_query, {"documentId":
➥ document_id})
self.execute_query(extract_indirect_entities_query, {"documentId":
➥ document_id})
❶ 从先前创建的图中提取推断图的步骤
❷ 第一个查询从主要命名实体创建实体。
❸ 第二个查询通过使用图中可用的共指连接通过 MENTIONS 创建到主要实体的连接。
使用列表 12.3 中的代码,我们有了从文本中提取命名实体和共指的所有必要代码,以及创建知识图谱的第二层。在此阶段,通过使用 ch12/07_process_larger_corpus.py 中可用的代码,您可以导入并处理第十一章中使用的 MASC 语料库,并开始从中获得更多见解。
练习
使用我们创建的图数据库,可以通过查询执行以下操作:
-
找出创建的命名实体的不同类型。
-
计算每种类型的出现次数,按降序排列,并取前三个。
-
计算推断知识图谱中组织实体的出现次数。应该更少,因为系统在创建推断图时应将它们聚合。
12.3 知识图谱构建:关系
当实体被识别后,下一步是辨别检测到的实体之间的关系,这在很大程度上提高了知识图谱的质量,包括您可以从中提取的见解和可用的访问模式。这一步在从文本创建有意义的图谱中至关重要,因为它允许您在实体之间建立联系并正确导航。您可以执行的查询类型以及因此可以回答的问题类型将显著增加。
为了更好地分解文本并使其更易于机器和人类理解,假设您想识别提取实体之间的关系,以突出它们之间的联系——例如,奖项与其获得者之间的关系,或公司与为其工作的人之间的关系。
回答诸如根据患者症状最可能的诊断或谁获得了物理学诺贝尔奖等问题,需要您不仅识别特定实体,还要识别它们之间的关系。存在不同的技术来完成这项任务;有些比其他技术更复杂,有些需要监督(标记样本关系以创建训练集)。最早且至今仍常用的关系抽取算法基于词汇句法模式。它涉及将一些上述标记或特定标签序列之间的句法关系映射到一组与关键命名实体相关的(对于用例)关系。
这个任务可能看起来很复杂,在某些情况下确实如此,但拥有图模型有很大帮助。可以通过一组语义分析规则获得一个粗略的简单近似,每个规则将句法图的子图(例如包含与关键实体相关句法关系的图的一部分)映射到数据库关系,这些关系应用于相应的实体。以下是一个具体的例子:
玛丽·居里在 1903 年获得了物理学诺贝尔奖。
句法分析确定“received”的主语是“玛丽·居里”,宾语是“诺贝尔物理学奖”。这些依存关系可以用以下代码轻松可视化。
列表 12.4 可视化依存关系
import spacy
nlp = spacy.load('en_core_web_sm')
doc = nlp(u"Marie Curie received the Nobel Prize in Physics") ❶
options = {"collapse_phrases": True} ❷
spacy.displacy.serve(doc, style='dep', options=options) ❸
❶ 注释一个简单句子
❷ 此选项允许我们将“玛丽·居里”等合并为可视化中的单个实体。
❸ 创建一个服务器实例,这将允许我们可视化结果
结果将类似于图 12.11。

图 12.11 使用 spaCy 可视化的句法依存关系
一个可能的模式将把这个句法结构映射到语义谓词
(verb: receive, subject: p:Person, object: a:Prize) → (relationship:
➥ RECEIVE_PRIZE, from: p, to:a)
在这里,“receive”(在词形还原版本中考虑)是一个英语动词,而 RECEIVE_PRIZE 是一个关系类型(一个语义关系)。使用像 Cypher 这样的适当基于图的查询语言表达这些类型的模式是直接的。作为一个练习,我们将推导出我们拥有的图。我们拥有的代码处理的句子产生了图 12.12 中所示的结果。

图 12.12 处理句子“玛丽·居里在 1903 年获得了诺贝尔物理学奖”后的结果图
使用以下查询可以执行寻找所有符合我们寻找模式的子图的任务。
列表 12.5 搜索符合我们寻找模式的子图
MATCH (verb:TagOccurrence {pos: "VBD", lemma:"receive"})
WITH verb
MATCH p=(verb)-[:IS_DEPENDENT {type:"nsubj"}]->(subject)-[:PARTICIPATES_IN]->
➥ (person:NamedEntity {type: "PERSON"})
MATCH q=(verb)-[:IS_DEPENDENT {type:"dobj"}]->(object)-[:PARTICIPATES_IN]->
➥ (woa:NamedEntity {type: "WORK_OF_ART"})
RETURN verb, person, woa, p, q
我们知识图谱中的结果将类似于图 12.13。

图 12.13 列表 12.5 的结果
注意,模式必须指定主语和宾语的语义类型——即实体类型,例如以 Person 作为主语,Prize 作为宾语。(在我们的例子中,类型是“艺术品”;拥有更好的 NER 模型会有所帮助。)但是,“receive”传达了许多关系,我们不希望涉及其他类型参数的“receive”实例被翻译成 RECEIVE_PRIZE 关系。另一方面,需要大量的替代模式来捕捉传达此类信息的广泛表达方式,例如
(relationship: "win", subject: p:Person, object: a:Prize) ®
(relationship: RECEIVE_PRIZE, from: p, to:a)
(relationship: "award", indirect-object: p:Person, object: a:Prize) ®
(relationship: RECEIVE_PRIZE, from: p, to:a)
注意,在后一个例子中,获奖者作为间接宾语出现(“委员会将奖项授予玛丽·居里。”)。如果我们没有包括将被动句转换为主动句的句法正则化步骤,我们还需要一个被动形式的模式(“奖项被授予玛丽·居里。”):
(relationship: "was awarded", object: a:Prize, indirect-object: p:Person) ®
(relationship: RECEIVE_PRIZE, from: p, to: a)
当这些关系被提取出来后,它们必须存储在我们设计的图模型中。图 12.14 显示了添加新信息所需的模式更改。

图 12.14 带有关系的扩展模型
以下列表展示了为了收集句法关系、将它们转换为感兴趣的关系并存储到我们的图模型中所需的更改。完整的代码位于 ch12/06_spacy_entity_relationship_extraction.py 和 ch12/text_processors.py 中。
列表 12.6 从类型依存关系提取关系
def tokenize_and_store(self, text, text_id, storeTag):
docs = self.nlp.pipe([text])
for doc in docs:
annotated_text = self.__text_processor.create_annotated_text(doc,
➥ text_id)
spans = self.__text_processor.process_sentences(annotated_text, doc,
➥ storeTag, text_id)
nes = self.__text_processor.process_entities(spans, text_id)
coref = self.__text_processor.process_co-reference(doc, text_id)
self.__text_processor.build_inferred_graph(text_id)
rules = [
{
'type': 'RECEIVE_PRIZE',
'verbs': ['receive'],
'subjectTypes': ['PERSON', 'NP'],
'objectTypes': ['WORK_OF_ART']
}
] ❶
self.__text_processor.extract_relationships(text_id, rules) ❷
def extract_relationships(self, document_id, rules):
extract_relationships_query = """ ❸
MATCH (document:AnnotatedText)
WHERE document.id = $documentId
WITH document
UNWIND $rules as rule
MATCH (document)-[*2..2]->(verb:TagOccurrence {pos: "VBD"})
MATCH (verb:TagOccurrence {pos: "VBD"})
WHERE verb.lemma IN rule.verbs
WITH verb, rule
MATCH (verb)-[:IS_DEPENDENT {type:"nsubj"}]->(subject)-
➥ [:PARTICIPATES_IN]->(subjectNe:NamedEntity)
WHERE subjectNe.type IN rule.subjectTypes
MATCH (verb)-[:IS_DEPENDENT {type:"dobj"}]->(object)-
➥ [:PARTICIPATES_IN]->(objectNe:NamedEntity {type: "WORK_OF_ART"})
WHERE objectNe.type IN rule.objectTypes
WITH verb, subjectNe, objectNe, rule
MERGE (subjectNe)-[:IS_RELATED_TO {root: verb.lemma, type:
➥ rule.type}]->(objectNe)
"""
self.execute_query(extract_relationships_query, {"documentId":
➥ document_id, "rules":rules})
❶ 可能定义的可能规则示例
❷ 基于定义的规则从现有图中提取关系的步骤
❸ 查询通过图进行,导航 NE 和参与者标签之间的关系,并提取所需的关系。
如代码所示,将语义关系转换为感兴趣的关系的规则必须列出,但列举这些模式提出了双重问题:
-
语义关系可以通过多种方式表达,这使得单独列出模式难以获得良好的覆盖率。
-
这些规则可能无法捕捉到特定领域特定谓词所需的所有区别。例如,如果我们想收集关于军事攻击的文档,我们可能希望在关于冲突的文本中包括“strike”和“hit”的实例,但不包括在体育故事中。我们还可能要求某些论点是必需的,而其他论点则是可选的。
为了解决这些问题,可以使用其他机制。其中大部分方法都是监督式的,这意味着它们需要人类支持来学习。最常见的方法是基于分类器,它确定 NE 之间关系(或无关系)的类型。这可以通过使用不同的机器学习或深度学习算法来实现,但这样的分类器应该输入什么?
训练分类器需要一个标注了实体和关系的语料库。首先,我们在每个文档中标记实体;然后,对于句子中每一对实体,我们记录连接它们的关系的类型,或者记录它们之间没有关系。前者是正训练实例,后者是负训练实例。在训练了分类器之后,我们可以通过将其应用于同一句子中出现的所有实体提及对来提取新测试文档中的关系 [Grishman, 2015]。尽管这种方法有效,但它有一个与训练过程中所需数据量相关的重大缺点。
第三种方法结合了基于模式的方法和监督方法,使用基于模式的方法作为引导过程:关系自动从模式中推断出来,这些模式用于训练分类器。
无论您使用哪种方法来提取关系,当它们作为表示文本的知识图中 NE(命名实体)之间的连接存储时,可以将这些关系投影到之前讨论的推断知识图上。在这种情况下,关系应连接实体。
在模型的定义中,重要的是要记住,有必要追溯为什么创建了那种关系。一个问题是在今天大多数可用的图数据库中,包括 Neo4j,关系只能连接两个节点。但在这个情况下,我们希望连接更多,以追溯到关系的源头。这个问题有两个解决方案:
-
在关系中添加一些信息作为属性,这将使我们能够追溯两个实体之间连接的起源,例如表示连接的 NEs 的 ID 列表。
-
添加表示关系的节点。我们无法连接到关系,但这些关系节点将实体之间的连接具体化,我们将能够将它们连接到其他节点。
第一种方案在节点数量方面不那么冗长,但导航起来要复杂得多,这就是为什么提出的模式(图 12.15)遵循第二种方法。

图 12.15 扩展的推断模式以包括关系
推断知识图的创建对图的可导航性和它支持的访问模式类型有很大影响。假设你已经处理了一个像我们例子中关于居里夫人的大量文本语料库,并且你想知道谁获得了诺贝尔物理学奖。
以下列表显示了如何扩展我们已有的代码,从文本中提取实体,并在推断的知识图中推断实体之间的关系。完整的代码在 ch12/text_processors.py 中,从 ch12/06_spacy_entity_relationship_extraction.py 中调用。
列表 12.7 从推断知识图中提取关系并将其存储
def build_relationships_inferred_graph(self, document_id): ❶
extract_relationships_query = """ ❷
MATCH (document:AnnotatedText)
WHERE document.id = $documentId
WITH document
MATCH (document)-[*2..3]->(ne1:NamedEntity)
MATCH (entity1:Entity)<-[:REFERS_TO]-(ne1:NamedEntity)-
➥ [r:IS_RELATED_TO]->(ne2:NamedEntity)-[:REFERS_TO]->
➥ (entity2:Entity)
MERGE (evidence:Evidence {id: id(r), type:r.type})
MERGE (rel:Relationship {id: id(r), type:r.type})
MERGE (ne1)<-[:SOURCE]-(evidence)
MERGE (ne2)<-[:DESTINATION]-(evidence)
MERGE (rel)-[:HAS_EVIDENCE]->(evidence)
MERGE (entity1)<-[:FROM]-(rel)
MERGE (entity2)<-[:TO]-(rel)
"""
self.execute_query(extract_relationships_query, {"documentId":
➥ document_id})
❶ 提取推断知识图关系的新的步骤
❷ 查询提取在先前步骤中创建的关系列表,并在推断的知识图中创建证据和关系。
在此阶段,文本经过处理并按描述存储后,获取所需结果的查询将看起来如下。
列表 12.8 获取诺贝尔物理学奖获得者
MATCH (nodelPrize:Entity {type:"WORK_OF_ART"})<-[:TO]-(rel:Relationship
➥ {type: "RECEIVE_PRIZE"})-[:FROM]->(winner:Entity {type: "PERSON"})
WHERE nodelPrize.id CONTAINS "the Nobel Prize in Physics"
RETURN winner
从 rel 节点,也可以找到所有使这种关系显而易见的文本。
我们构建的知识图以使原始内容在处理过的语料库中可用,并使多种类型的搜索和不同的访问模式及问答成为可能。当我们使用原始格式的文本时,大多数这些操作都是不可能的。识别 NEs 及其之间的关系使得查询成为可能,否则这些查询是不可能的,推断知识图创建了一个第二层,其中提炼的知识更容易导航。图是一个抽象,它代表了文本中的关键概念。
12.4 语义网络
我们迄今为止构建的知识图谱包含大量从文本中提取并转换为可用于使用的知识的信息。具体来说,推断出的知识图谱代表了在处理越来越多的文本时提取的浓缩知识。在此阶段,研究如何具体地使用这个知识图谱为最终用户提供新的高级服务是相关的。
知识图谱是在知识库之上构建的表示,其上可以构建多种类型的自动化推理和有趣的功能。知识表示和推理是符号人工智能的一个分支,旨在设计能够基于感兴趣领域的机器可解释表示进行推理(类似于人类)的计算机系统。在这个计算模型中,符号作为物理对象、事件、关系和其他领域实体的替身 [Sowa, 2000]。
表示此类知识库最常见的方法之一是使用语义网络——节点代表概念,弧代表这些概念之间关系的图。语义网络提供了关于感兴趣领域陈述的结构表示,或者“一种从自然语言中抽象出来的方法,以更适合计算的形式表示文本中捕获的知识” [Grimm et al., 2007]。
通常,概念被选择来表示此类文本中名词的意义,关系被映射到动词短语。让我们考虑我们之前使用的一个具体例子。句子
玛丽·居里,这位著名的科学家,在 1903 年获得了诺贝尔物理学奖。
应生成图 12.16 所示的语义网络。

图 12.16 一个简单的语义网络
这个结构正是为推断出的知识图谱所创建的。唯一的区别是我们将关系实体化以跟踪来源,这在想要知道为什么创建这些关系时是必要的。因此,推断出的知识图谱是一个语义网络——因为在我们模式中,我们将关系实体化以跟踪每个推断关系的来源,这是一个简化的版本。
因此,在我们的心智图中,我们将从推断出的知识图谱中提取语义网络视为一个特定的过程(如图 12.17 所示),移除所有与关系映射到来源相关的开销,仅保留相关的概念和关系,例如通过考虑它们在原始语料库中出现的频率。

图 12.17 心智图:提取语义网络
语义网络的内容取决于与感兴趣领域和应用程序应提供的特定服务相关的概念和关系。在我们的案例中,语义网络是从当前的大图中提取的推断知识图谱。在这个过程中,可以稍微简化一下图,例如通过删除关系节点并用适当的关系来替换它们。
有时候,使用你拥有的语料库不足以构建一个能够满足所有需求的有效语义网络。幸运的是,存在可公开获取的通用语义网络。其中最广泛使用的是 ConceptNet 5⁵,其创造者将其描述为“一个知识表示项目,提供了一个大型语义图,描述了普遍的人类知识和它在自然语言中的表达方式” [Speer and Havasi, 2013]。图中表示的知识来自各种来源,包括专家创建的资源、众包和游戏。ConceptNet 的目标是通过使它们能够更好地理解人们使用词语背后的含义来改善自然语言应用[Speer et al., 2017]。图 12.18,来自网站,展示了它是如何工作的。

图 12.18 ConceptNet 5 如其在网站上的描述
ConceptNet 5 API 的使用非常简单。例如,如果你想了解更多关于玛丽·居里的信息,你可以调用以下 URL
http://api.conceptnet.io/c/en/marie_curie/
并得到以下答案:
{
"@id": "/a/[/r/Synonym/,/c/en/marie_curie/n/wn/person/,
➥ /c/en/marya_sklodowska/n/wn/person/]",
"@type": "Edge",
"dataset": "/d/wordnet/3.1",
"end": {
"@id": "/c/en/marya_sklodowska/n/wn/person",
"@type": "Node",
"label": "Marya Sklodowska",
"language": "en",
"sense_label": "n, person",
"term": "/c/en/marya_sklodowska"
},
"license": "cc:by/4.0",
"rel": {
"@id": "/r/Synonym",
"@type": "Relation",
"label": "Synonym"
},
"sources": [
{
"@id": "/s/resource/wordnet/rdf/3.1",
"@type": "Source",
"contributor": "/s/resource/wordnet/rdf/3.1"
}
],
"start": {
"@id": "/c/en/marie_curie/n/wn/person",
"@type": "Node",
"label": "Marie Curie",
"language": "en",
"sense_label": "n, person",
"term": "/c/en/marie_curie"
},
"surfaceText": "[[Marie Curie]] is a synonym of [[Marya Sklodowska]]",
"weight": 2.0
}
这个答案立即告诉你玛丽·居里的另一个名字是玛丽亚·斯克洛多夫斯卡。
在这一章的这个点上研究 ConceptNet 有几个原因:
-
它是以与文本中描述的完全相同的方式创建的,这验证了我们迄今为止的路径。如图 12.18 中的架构所示,它集成了所有关键概念:知识图谱、语义网络、人工智能、自然语言处理等等。
-
如果你没有足够的信息在你的语料库中构建一个适当的知识图谱,而你参考的领域是一个常见的领域,你可以使用 ConceptNet 来填补空白。如果你正在处理来自在线来源的新闻文章,并且你只得到文本中的城市名称,例如“洛杉矶”,你可以查询 ConceptNet 来找到这些城市所在的州(在这种情况下,“加利福尼亚”)。⁶
-
我喜欢它。这是一个理解文本和扩展知识图谱的好资源,我在很多项目中经常使用它。它简单易用,免费,而且相当快:为了获得最佳速度,你可以下载它,或者更好的是,将其导入 Neo4j 实例中。
图 12.19,来自 Speer 和 Havasi [2013]的论文,介绍了 ConceptNet 的最新迭代,描述了主要关系及其如何连接文本中的不同组件。它清楚地表明,这种方法与本书中提出的方法相似。在此图中,NP代表名词短语,VP代表动词短语,AP代表形容词短语。

图 12.19 来自 Speer 和 Havasi [2013]的表格,展示了 ConceptNet 5 中哪些关键关系是可用的
通过 Python 访问 ConceptNet 5 非常简单,如下面的列表所示。你可以使用 requests 库来获取内容。
列表 12.9 通过 Python 访问 ConceptNet
import requests
obj = requests.get('http://api.conceptnet.io/c/en/marie_curie').json()
print(obj['edges'][0]['rel']['label'] + ": " +
➥ obj['edges'][0]['end']['label'])
练习
在列表 12.9 中的代码上稍作实验,以查看不同的处理结果。这里给出的示例只是一个建议。
12.5 无监督关键词提取
命名实体识别(NER)并不是识别文本中关键元素的唯一方法。任何文本都有一些特定的单词和短语——并不总是与命名实体(NEs)相关——它们比其他单词和短语更重要,因为它们表达了与整个文档、段落或句子内容相关的关键概念。这些单词和短语通常被称为关键词,它们在处理大型语料库时提供了巨大的支持。
任何规模的公司都必须管理和访问大量数据,以向最终用户提供高级服务或处理其内部流程。这些数据的大部分通常以文本的形式存储。处理和分析这一巨大知识来源的能力代表了一种竞争优势,但通常,由于文本数据的非结构化性质和问题规模,即使提供简单有效的访问也是一个复杂任务。
假设你希望通过识别主要概念、组织索引和提供适当的可视化来有效地访问大量文档(电子邮件、网页、文章等)。关键词提取——识别和选择最能描述文档的单词和小短语的过程——对于这项任务至关重要。除了构成构建语料库索引的有用条目外,你提取的关键词还可以用于对文本进行分类,在某些情况下还可以作为给定文档的简单摘要。用于自动识别文本中重要术语的系统可用于多种目的,例如
-
识别训练好的 NER 模型无法识别的命名实体
-
创建特定领域的词典(在这种情况下,也使用提取的 NEs)
-
通过频繁和重复的关键词以及与实体的连接扩展推断出的知识图谱
-
创建索引并使用关键术语在用户查找特定关键词时提升结果
关键词在构建知识图谱的过程中起着重要作用,提高了最终结果的质量(从知识和访问模式的角度来看)。那么,如何获取它们呢?当使用本节讨论的无监督技术时,关键词提取的任务甚至不需要人工支持!
本节描述了一种用于关键词提取 7 的方法,该方法使用一个表示文档中标签或概念之间关系的图模型。解决方案从基于图的无监督技术 TextRank 开始。此后,通过使用类型依赖图和其他技巧(用于过滤掉无意义的短语,或用形容词和名词扩展关键词以更好地描述文本)大大提高了提取关键词的质量。值得注意的是,尽管提出的方法是无监督的,但最终结果的质量与监督方法达到的质量相当。本书中偏好使用此算法有几个原因:
-
它完全基于我们之前讨论过的图技术和算法,例如 PageRank。
-
它使用了我们在第十一章中详细分析的句法依赖关系。
-
结果的质量非常出色,即使与监督算法相比也是如此。
Mihalcea 和 Tarau 于 2004 年提出的 TextRank 算法是一种相对简单的无监督文本摘要方法,它可以直接应用于主题提取任务。其目标是通过对单词共现构建图并使用 PageRank 算法对单个单词的重要性进行排序,检索关键词并构建最能描述给定文档的关键短语。图 12.20 显示了这种共现图是如何创建的。

图 12.20 TextRank 的关键概念:将文本转换为共现图
Mihalcea 和 Tarau 提出的算法结构总结在图 12.21 中。

图 12.21 TextRank 算法的关键步骤
算法的关键步骤如下:
-
从 NLP 标注文本中预选相关词汇。每个文档都被分词并标注。这些处理过的词汇是基本的词汇单位,或称为标签。应用一个可配置的停用词列表和句法过滤器来细化选择,以获得最相关的词汇单位。句法过滤器仅选择名词和形容词,遵循 Mihalcea 和 Tarau 的观察,即即使是人工标注者也倾向于使用名词而不是动词短语来总结文档。
-
创建标签共现的图。过滤后的标签根据它们在文档中的位置排序,并在相邻标签之间建立共现关系,遵循文本中的自然词流。这一步将文档的句法元素之间的关系引入到图中。默认情况下,只有相邻出现的标签才能有共现关系。在句子“Pieter eats fish”中,没有创建共现边,因为“eats”是一个没有通过句法过滤的动词。但如果将共现窗口的大小从默认的 2 改为 3,则“Pieter”和“fish”将连接起来。最后,每个共现边都分配一个权重属性,表示两个标签在给定文档中共现的次数。此时得到的图看起来像图 12.20。
-
运行无向加权 PageRank。在加权共现关系上运行无向 PageRank 算法,根据节点(标签)在图中的重要性对它们进行评分。无加权 PageRank 的实验表明,权重对于将重要关键词提前是有用的。
-
将前三分之一的标签保存为关键词并识别关键短语。标签根据 PageRank 评分排序;然后取前三分之一(可配置)作为最终关键词。如果这些选定的标签中的一些是相邻的,它们将被合并成一个关键短语。
在这个过程结束时,通过关键词节点和 AnnotatedText 节点之间的 DESCRIBES 关系,将识别出的关键词和关键短语保存到图数据库中。得到的图将类似于图 12.22。

图 12.22 带有关键词扩展的图模型
从包含迄今为止描述的所有算法和技术的新版本代码开始,以下列表显示了如何将这个新算法添加到我们不断增长的项目中。完整代码位于 ch12/text_processors.py 和 ch12/08_spacy_textrank_extraction.py。
列表 12.10 应用 TextRank
def tokenize_and_store(self, text, text_id, storeTag):
docs = self.nlp.pipe([text])
for doc in docs:
annotated_text = self.__text_processor.create_annotated_text(doc,
➥ text_id)
spans = self.__text_processor.process_sentences(annotated_text, doc,
➥ storeTag, text_id)
self.__text_processor.process_entities(spans, text_id)
self.__text_processor.process_textrank(doc, text_id) ❶
def process_textrank(self, doc, text_id): ❷
keywords = []
spans = []
for p in doc._.phrases:
for span in p.chunks: ❸
item = {"span": span, "rank": p.rank}
spans.append(item)
spans = filter_extended_spans(spans) ❹
for item in spans:
span = item['span'
lexme = self.nlp.vocab[span.text];
if lexme.is_stop or lexme.is_digit or lexme.is_bracket or "-PRON-" in
➥ span.lemma_:
continue
keyword = {"id": span.text, "start_index": span.start_char,
➥ "end_index": span.end_char}
if len(span.ents) > 0:
keyword['NE'] = span.ents[0].label_
keyword['rank'] = item['rank']
keywords.append(keyword)
self.store_keywords(text_id, keywords)
def store_keywords(self, document_id, keywords):
ne_query = """ ❺
UNWIND $keywords as keyword
MERGE (kw:Keyword {id: keyword.id})
SET kw.NE = keyword.NE, kw.index = keyword.start_index, kw.endIndex =
➥ keyword.end_index
WITH kw, keyword
MATCH (text:AnnotatedText)
WHERE text.id = $documentId
MERGE (text)<-[:DESCRIBES {rank: keyword.rank}]-(kw)
"""
self.execute_query(ne_query, {"documentId": document_id, "keywords":
➥ keywords})
❶ 添加提取关键词的新步骤
❷ 该函数处理注释文档,识别关键词并将它们存储起来。
❸ 在文档中找到的关键词循环,称为块
❹ 过滤重叠的关键词并取最长的那个
❺ 创建新的关键词节点并将它们通过 DESCRIBES 关系连接到文档
上一段代码使用了 spaCy 的一个现有插件,名为 pytextrank,⁸,它正确地实现了 TextRank 算法。对于这个句子,
委员会将诺贝尔物理学奖授予玛丽·居里。
它返回以下关键词列表(括号中的数字是 TextRank 算法分配的排名):
-
委员会(0.15)
-
玛丽·居里(0.20)
-
诺贝尔物理学奖(0.14)
还不错,尤其是考虑到我们只处理一个句子。TextRank 在较长的文档上表现更好,因为它可以考虑到特定单词出现的频率。
使用 TextRank 获得的初始结果相当有希望,但可以通过使用更多关于文本的见解来提高质量。在 GraphAware,我们还实现了一个 TextRank 算法,可在我们的开源 NLP 插件 Neo4j 中使用。⁹ 基本算法已被修改,以利用 Stanford CoreNLP 提供的类型化依赖关系图。
为了提高自动关键词提取的质量,扩展算法考虑了类型化的依赖关系amod和nn。一个名词短语(NP)的形容词修饰语(amod)是指任何用于修饰 NP 意义的形容词短语:
"Sam eats red meat" -> amod(meat, red)
"Sam took out a 3 million dollar loan" -> amod(loan, dollar)
"Sam took out a $ 3 million loan" -> amod(loan, $)
一个名词复合修饰语(nn)是指任何用于修饰中心名词的名词:
"Oil price futures" -> nn(futures, oil), nn(futures, price)
这些类型化的依赖关系可以用来改进 TextRank 算法的结果。考虑一个具体的例子。以下关键短语是通过使用标准方法由 TextRank 提取的:
personal protective
显然,这个短语没有意义,因为两个词都是形容词;缺少一个名词。在这种情况下,名词是装备。这种遗漏可能是因为该名词在考虑的文档中的排名低于三分之一的最顶部单词的阈值,并且在合并过程中,这些单词被忽略。因此,讨论相同主题——“个人防护装备”——的文档没有被分配任何共同的关键短语。
在这种情况下,amod 依赖关系可以帮助。在文本“个人防护装备”中,三个词之间存在 amod 依赖关系:
amod(equipment, personal)
amod(equipment, protective)
指定这些依赖关系意味着在合并阶段我们还需要考虑“装备”,因为它与出现在结果顶部三分之一的单词相关联。类型化依赖关系不仅可以用以补充缺少标签的现有关键短语,还可以删除没有 COMPOUND 或 amod 类型相互关系的短语。因此,改进的 TextRank 算法引入了两个新的原则:
-
关键短语候选中的所有标签都必须通过 COMPOUND 或 amod 依赖关系相关联。
-
如果一个相邻的标签由于排名不够高而没有被原始 TextRank 算法包含在关键短语中,但它通过 COMPOUND 或 amod 类型的依赖关系与一个或多个得分最高的词相关联,那么这个标签将被添加。
后者原则负责处理之前提到的缺点,并增加了更高的细节级别。
通过这些小的变化(以及这里未提及的几个其他变化,例如基于标签词性的后过滤或考虑 NEs 等),得到的结果类似于许多人类标注者用来描述给定文档的关键短语。
由于其高精度,关键词提取支持不同类型的分析,可以揭示关于语料库的大量信息。提取的关键词可以用不同的方式来发现关于语料库中文档的新见解,包括提供索引或甚至文档内容的摘要。
为了证明这一概念,让我们尝试使用维基百科电影情节数据集。¹⁰ 该数据集包含来自世界各地的 34,886 部电影描述,包括它们情节的摘要。您可以使用 ch12/08_spacy_textrank_ extraction.py 中的代码导入完整的数据集。由于数据集很大,处理和存储结果将需要一些时间。然后,您可以使用以下查询获取最频繁的关键词列表。
列表 12.11 获取 100 个最频繁关键词的列表
MATCH (n:Keyword)-[:DESCRIBES]->(text:AnnotatedText)
RETURN n.id as keywords, count(n) as occurrences
order by occurrences desc
limit 100
结果如图 12.23 所示。

图 12.23 执行列表 12.11 在数据库上的结果
即使是考虑语料库中关键词出现次数的这样一个简单查询,我们也能从数据集的内容中提取出大量信息:
-
主要主题是“love”,这在许多情节摘要中作为关键词出现。这一事实可能反映了浪漫作为主题的统治地位以及用户对所描述电影的热情。
-
“film” 和 “story” 这些术语出现得相当频繁,这是可以预料的,因为它们在描述电影情节时被广泛使用。
-
第二常见的关键词是“police”,这表明关于犯罪的电影相当常见。
-
另一个有趣的观察是,“man”似乎作为情节的关键组成部分出现得更为频繁;“woman”在排名中则要低得多。
这个简单的例子让您了解,仅通过考虑关键词,就可以从语料库中提取出多少信息。在 12.5.1 节中,我们将进一步发展这一想法,但首先考虑以下练习。
练习
在数据集上玩耍,不仅使用关键词,还要使用从文本中提取的 NE(命名实体)。通过使用 NamedEntity 节点而不是 Keyword 节点执行相同的查询。(注意,这并不是您在查询中必须做出的唯一更改。)您可以从数据中得出哪些观察?
12.5.1 关键词共现图
关键词本身提供了大量的知识,但通过考虑它们的组合,我们可以进一步扩展它们的价值。通过考虑它们共同出现的文档,关键词可以通过关系连接起来。这种方法生成了一个关键词共现图(其中我们只有关键词类型的节点以及它们之间的连接)。
共现图的概念已被用作在其他场景中(特别是推荐章节)构建图的技巧。生成的图充满了可用于分析原始图本身的信息。在我们考虑的情况中——关键词——这个图将看起来像图 12.24。

图 12.24 关键词共现图
通过在原始图上运行特定查询可以获得这样的图。再次强调,图以及可用的插件和库允许你避免为重复性任务编写代码。在以下示例中,我们使用的是已经广泛使用的 APOC 库。
列表 12.12 创建共现图
CALL apoc.periodic.submit('CreateCoOccurrence', ❶
'CALL apoc.periodic.iterate("MATCH (k:Keyword)-[:DESCRIBES]->
➥ (text:AnnotatedText)
WITH k, count(DISTINCT text) AS keyWeight
WHERE keyWeight > 5
RETURN k, keyWeight",
"MATCH (k)-[:DESCRIBES]->(text)<-[:DESCRIBES]-(k2:Keyword)
WHERE k <> k2
WITH k, k2, count(DISTINCT text) AS weight, keyWeight
WHERE weight > 10
WITH k, k2, k.value as kValue, k2.value as k2Value, weight,
➥ (1.0f*weight)/keyWeight as normalizedWeight
CREATE (k)-[:CO_OCCUR {weight: weight, normalizedWeight: normalizedWeight}]->
➥ (k2)", {batchSize:100, iterateList:true, parallel:false})')
❶ 根据图的大小,此操作可能需要很长时间。因此,我使用了 apoc.periodic.submit,因为它允许你将以下查询作为后台作业提交。你可以通过使用“CALL apoc.periodic.list()”来检查状态。
在这个查询中,请注意提交过程和迭代过程的组合,这导致查询在后台执行,与浏览器请求断开连接,并且允许你定期提交结果以避免单个大事务。你可以通过使用“CALL apoc.periodic.list”来检查后台作业的状态。
注意,我们正在过滤掉不相关的关键词(那些出现次数少于 5 次的关键词,如 WHERE keyWeight > 5 所指定的)并且只有当关键词对至少一起出现 10 次时(WHERE weight > 10)我们才考虑这些连接。这种方法使我们能够创建一个合适的共现图,其中相关信息更加明显。
练习
当查询完成后,通过检查关键词的连接来探索结果知识图。你会注意到图要密集得多。
12.5.2 关键词聚类和主题识别
共现图包含大量可用于从文本中提取知识的新信息。在我们的案例中,目标是提取处理过的文本(情节摘要)的见解。我们已经使用关键词频率来获取关于我们数据集内容的一些见解,但通过关键词共现图,我们将能够更好地识别文档中的主题。图 12.25 提供了获取主题列表所需步骤的概述。

图 12.25 使用关键词提取和社区检测提取主题的步骤
共现图连接了在同一图中一起出现的关键词,因此它能够将多个关键词聚合到代表相同类型电影的组中。至少,我们希望证明这个想法。我们用于创建共现图的过滤器(相关关键词和相关连接)在这个阶段是有帮助的,因为它们很好地隔离了共现图中的关键词。
在第十章中,我介绍了一种识别社交网络中人群社区的方法:Louvain 算法。该算法在识别聚类方面表现出高度的准确性以及高速率。这种方法也可以应用于共现图,以查看哪些关键词聚类是最相关的。
在这种情况下,为了简化运行 Louvain 算法的查询,我们将操作分为两部分。第一个查询创建了一个虚拟图,我们只指定我们感兴趣的图的部分:共现图。(记住,我们确实拥有完整的知识图谱!)这样,我们可以指定在哪里执行社区检测算法,忽略所有其他部分。
列表 12.13 在知识图谱中创建虚拟图
CALL gds.graph.create(
'keywordsGraph',
'Keyword',
{
CO_OCCUR: {
orientation: 'NATURAL'
}
},
{
relationshipProperties: 'normalizedWeight'
}
)
拥有表示仅包含共现图的虚拟图后,可以使用以下简单查询运行 Louvain 算法。
列表 12.14 通过 Louvain 揭示社区
CALL gds.louvain.write('keywordsGraph', {
relationshipWeightProperty: 'normalizedWeight',
writeProperty: 'community'
}) YIELD nodePropertiesWritten, communityCount, modularity
RETURN nodePropertiesWritten, communityCount, modularity
由于在第十章中讨论过,这个算法性能惊人,Neo4j 实现高度优化,因此这个查询应该相当快。每个关键词分配的社区作为相关节点中的社区属性保存;它包含社区的标识符。在这个过程结束时,可以使用以下查询来探索结果。
列表 12.15 获取社区和每个社区的顶级 25 个关键词
MATCH (k:Keyword)-[:DESCRIBES]->(text:AnnotatedText)
WITH k, count(text) as weight
WHERE weight > 5
with k.community as community, k.id as keyword, weight
order by community, weight desc
WITH community, collect(keyword) as communityMembers
order by size(communityMembers) desc
RETURN community as communityId, communityMembers[0..25] as topMembers,
➥ size(communityMembers) as size
查询首先根据社区(由社区标识符,communityId)和频率对关键词进行排序,然后按社区标识符分组,并只取每个社区的顶级 25 个关键词(由于它们的频率而最相关)。社区的大小用于对最终结果进行排序,这些结果在图 12.26 中显示。它们可能会让你感到惊讶!

图 12.26 应用在共现图上的社区检测算法的结果
这个例子只是一个摘录,但它清楚地展示了结果的质量。在每一个簇中,很容易识别主题:关于世界大战的电影、科幻电影、与体育相关的电影、中世纪电影,最后是汤姆和杰瑞电影。
练习
运行列表 12.15,并探索完整的结果列表。你能否识别所有结果的主题?
12.6 图方法的优势
本章提出的解决方案——知识图谱——不能存在于图模型之外,因此我们实际上无法谈论图方法相对于其他可用方法的优点。但是,正如你所看到的,以图的形式表示数据和信息通过使探索数据中隐藏的知识变得容易,从而赋予了一系列解决方案和服务。这种方法是提供 AI 解决方案的最佳方式。
特别是,知识图谱是表示文本格式数据的自然方式,通常被认为是非结构化的,因此难以处理。当数据以这种方式存储时,可以从中提取的知识量和可以对数据进行的分析类型是无限的。
我们看到如何在提取的关系或提及之间导航是多么简单,如何在语义网络中创建概念层次,以及如何使用共现图中的自动关键词提取从我们的数据集中的电影情节中提取主题。这些概念同样适用于任何其他类型的语料库。
摘要
本章是本书的最后一章;其目的是展示本书中提出的内容如何在知识图谱中达到顶峰。在这种情况下,图不是可能的解决方案,而是驱动力,它允许信息结构化、访问模式以及类型分析和操作,这些在其他情况下是不可行的。
结构化和非结构化数据和信息可以共存于这种强大的知识表示中,这可以用来为你的机器学习项目提供比其他情况下更多的先进服务。语义网络开辟了一整系列新的可能性。
在本章中,你学习了
-
如何从文本中提取命名实体(NEs)并将其适当地存储在图模型中
-
如何从命名实体(NEs)中提取关系并在图中对其进行建模
-
如何从文本的不同实例中推断关键实体和关系,并创建一个强大的知识表示:语义网络
-
如何使用基于图的算法以无监督的方式从文本中提取关键词并将它们存储在你创建的知识图谱中
-
如何创建关键词共现图并对其进行处理
-
如何仅使用图技术识别语料库中的关键主题
我想以这样的说法结束:这本书不是你旅程的终点——只是新旅程的开始。现在你有了在许多情况下正确使用图的主要概念工具。当然,这本书不可能回答你关于图可能有的所有问题,但我希望它已经为你提供了必要的心理模式,以便从不同的角度接近机器学习项目。
参考文献
[Gomez-Perez et al., 2017] Gomez-Perez, Jose Manuel, Jeff Z. Pan, Guido Vetere, 和 Honghan Wu. “企业知识图谱:简介.” 在 Exploiting Linked Data and Knowledge Graphs in Large Organisations 中. 瑞士:Springer, 2017: 1-14.
[Grishman, 2015] Grishman, Ralph. “信息提取.” IEEE Intelligent Systems 30:5 (2015): 8-15.
[Grishman and Sundheim, 1996] Grishman, Ralph, 和 Beth Sundheim. “消息理解会议 - 6:简史.” 第 16 届国际计算语言学会议论文集 (1996): 466-471.
[Grimm et al., 2007] Grimm, Stephan, Pascal Hitzler, 和 Andreas Abecker. “知识表示和本体.” 在 Semantic Web Services: Concepts, Technology and Applications 中. 柏林,海德堡:Springer, 2007: 51-106.
[Jurafsky 和 Martin,2019] Jurafsky, Dan, 和 James H. Martin. 语音和语言处理:自然语言处理、计算语言学和语音识别导论. 新泽西州上萨德尔河:Prentice Hall,2019(第 3 版草案,可在 web.stanford.edu/~jurafsky/slp3 获取).
[Karttunen,1969] Karttunen, Lauri. “话语指称.” 第 1969 年计算语言学会议论文集 (1969): 1-38.
[Mihalcea 和 Radev,2011] Mihalcea, Rada, 和 Dragomir Radev. 基于图的自然语言处理和信息检索. 纽约:剑桥大学出版社,2011.
[Mihalcea 和 Tarau,2004] Mihalcea, Rada, 和 Paul Tarau. 2004 年 7 月. “TextRank: 将秩序带入文本.” 自然语言处理实证方法会议论文集 (2004): 404-411.
[Ng,2009] Ng, Vincent. 2009. “基于图割的共指消解指代性确定.” 人语言技术会议:北美计算语言学协会分会会议 (2009): 575-583.
[Nicolae 和 Nicolae,2006] Nicolae, Cristina, 和 Gabriel Nicolae. “BESTCUT: 用于共指消解的图算法.” 第 2006 年自然语言处理实证方法会议论文集 (2006): 275-283.
[RDF 工作组,2004] “RDF 入门:W3C 建议书,2004 年 2 月 10 日.” www.w3.org/TR/rdf-primer.
[Sowa,2000] Sowa, John F. 知识表示:逻辑、哲学和计算基础. 加利福尼亚州太平洋 Grove: Brooks Cole, 2000.
[Speer 和 Havasi,2013] Speer, Robyn, 和 Catherine Havasi. “ConceptNet 5: 一个大型语义关系知识网络.” 见 Iryna Gurevych 和 Jungi Kim 编著的 人民的网络与自然语言处理:协作构建的语言资源. 柏林,海德堡:Springer,2013: 161-176.
[Speer 等人,2017] Speer, Robyn, Joshua Chin, 和 Catherine Havasi. “ConceptNet 5.5: 一个开放的多语言通用知识图.” 第 31 届 AAAI 人工智能会议论文集 (2017): 4444-4451.
^(1.)mng.bz/gxDE.
^(2.)mng.bz/eMjv.
^(3.)explosion.ai/demos/displacy-ent.
^(4.)spacy.io/usage.
^(5.)conceptnet.io.
^(6.)在此示例中,要使用的查询是 api.conceptnet.io/query?start=/c/en/los_angeles&rel=/r/PartOf.
^(7.)mng.bz/pJD8.
^(8.)github.com/DerwenAI/pytextrank.
^(9.)github.com/graphaware/neo4j-nlp.
^(10.)mng.bz/O1jR.
附录 A. 机器学习算法分类法
机器学习是一个深度且广泛的领域。因此,存在许多不同的机器学习分支。根据四个标准,算法可以被分类或组织到广泛的类别中:
-
是否使用人类提供的标记数据进行训练
-
是否可以增量学习
-
是否通过构建预测模型或通过将新数据点与已知数据点进行比较来工作
-
学习者是否积极与环境互动或被动地观察环境提供的信息
这个分类法提供了对大量机器学习算法的概述,但并不详尽。其目的是帮助您确定适合您特定问题的正确算法集,同时考虑可用的数据和它如何流入系统。这种分类对于理解
-
需要的数据类型以及如何准备数据
-
多久以及以何种方式重新训练模型(如果需要的话)
-
随着时间的推移,预测质量可能会受到哪些影响
-
要设计的解决方案的架构限制
A.1 监督学习与无监督学习
学习需要学习者和环境之间的交互。首先提出的分类是基于训练阶段这种交互的性质。根据监督的数量和类型,我们可以区分监督学习和无监督学习。
如果我们将学习视为使用经验获得专业知识的过程,那么监督学习需要包含显著信息的训练示例/样本(经验)。这个学习过程的典型例子是垃圾邮件过滤器。学习者在训练数据集中需要标签,例如“垃圾邮件”和“非垃圾邮件”(显著信息),对于每个元素(电子邮件)。它从这些标签中学习如何分类电子邮件。一般来说,这类算法在预测准确性方面具有更高的性能。另一方面,提供标记数据所需的工作量很大,在某些情况下甚至无法执行。一些最重要的监督学习算法(其中一些在本书中有所涉及)包括
-
k-最近邻(k-NN)
-
决策树和随机森林
-
贝叶斯网络
-
线性回归
-
逻辑回归
-
支持向量机
在光谱的另一端,无监督学习不需要标记数据,因此没有训练数据和测试数据之间的区别。学习者处理输入数据的目标是得出关于数据的一些见解或总结或压缩版本。一些最重要的无监督学习算法(其中一些在本书中有所涉及)包括
-
聚类(k-means)
-
图聚类
-
关联规则挖掘
-
PageRank
一种中间学习过程类型可以处理部分标记的训练数据,通常由标记和无标记数据混合组成。这个过程被称为半监督学习。大多数半监督算法是监督和无监督算法的组合。这种类型算法的一个例子是半监督标签传播。
这种分类标准的异常是强化学习,其中学习者只能观察环境(定义为当前时间可用的信息集),选择并执行动作,并获得相应的奖励。由于这种环境与学习者的相互作用,算法学习到追求最大奖励的最优策略(称为策略)。策略定义了系统在特定环境条件下执行的最佳动作。强化学习主要用于在房间内移动机器人或玩象棋和其他游戏。
A.2 批量学习与在线学习
第二种分类是基于学习者适应在线或短时间内适应数据流的能力。一些被称为在线学习者的算法可以从新数据中增量学习。其他被称为批量学习者的算法,在数据变化时需要再次使用整个数据集或其大部分。 [Géron, 2017]
在批量学习中,系统使用所有可用数据进行训练。这种学习过程可能需要大量时间和计算资源,这取决于要处理的数据量,因此通常在离线时进行。因此,批量学习也被称为离线学习。为了通知批量学习系统新的数据,需要从头开始在新数据集上训练一个新版本。当新模型准备好投入生产时,旧模型可以被替换。经典的数据挖掘过程,如市场篮子分析,¹属于这一类别。数据挖掘者在输出结论之前,有大量的训练数据可以操作。
在在线学习中,系统是增量训练的;数据点依次输入,一个接一个或以小批量形式。在这种情况下,学习过程快速且成本低,可以非常频繁地进行。在线学习非常适合接收连续数据并需要快速自主适应变化的系统,例如必须基于迄今为止收集的股价做出每日或每小时决策的股票预测算法。
在线学习也可以用于使用大量无法适应可用资源的数据进行系统训练。这种学习类型被称为离核学习。算法加载数据的小批量,执行一个训练步骤,清除数据,然后进行下一批。在线学习通常更受欢迎(当适用时),有两个原因:
-
它提供了对当前数据和当前状态的更好拟合。
-
在资源消耗方面更有效率。
然而,这种学习对不良数据很敏感。为了减少与不良数据相关的风险,有必要持续监控系统并最终关闭学习。值得注意的是,在线和离线学习算法可以是监督的或非监督的。
A.3 基于实例学习与基于模型学习
另一种根据系统将训练期间使用的数据泛化以创建预测模型的能力来分类学习者的方法是。两种主要的方法是实例学习和基于模型学习 [Géron, 2017]。
在基于实例的学习中,系统首先学习所有训练示例;然后,对于新的实例(数据点),它从训练示例中找到最近的实例。这种方法需要一种测量元素之间距离的方法,例如计算使用 TF-IDF ²创建的向量之间的余弦距离,或者计算它们共有的单词数量。
在基于模型的学习中,系统从训练数据集中构建一个模型,该模型泛化训练示例,然后用于进行预测。一个典型的例子是推荐引擎的协同过滤技术。此类算法使用用户-项目交互(购买、查看、点击等)作为训练数据来构建模型。然后,该模型用于预测用户对未见或未购买项目的兴趣,并推广预测兴趣最高的项目。
使用训练数据集来泛化预测模型通常在预测性能方面是一个更好的解决方案,预测性能由响应时间和结果质量来定义。与此方法相关的问题包括
-
构建模型所需的时间。
-
过拟合训练数据,这发生在训练数据集不包含覆盖所有可能情况的示例时。在这种情况下,模型只知道少数示例,并且没有足够泛化来正确处理未见过的样本。
A.4 主动学习与被动学习
学习范式也可以根据学习者在训练阶段所扮演的角色而变化。被动学习者观察环境提供的信息。在垃圾邮件过滤器示例中,被动学习者会等待用户标记电子邮件。主动学习者在训练时间通过提问或进行实验来主动与环境互动。在垃圾邮件过滤器示例中,主动学习者会选择电子邮件并要求用户将其标记为垃圾邮件或非垃圾邮件。这种方法可能在预测质量方面有更好的表现,因为主动学习者可以选择正确的数据进行标记(例如避免过拟合),但与用户或环境的交互会影响用户体验。
参考文献
[Géron, 2017] Géron, Aurélien. 《使用 Scikit-Learn 和 TensorFlow 进行机器学习实践:构建智能系统的概念、工具和技术》. Sebastopol, CA: O’Reilly, 2017.
(1.) 市场篮子分析是通过寻找客户在购物篮中放置的商品之间的关联来分析客户购买习惯的过程。发现这样的关联可以帮助零售商通过了解哪些商品客户经常一起购买来制定营销策略。
(2.) TF-IDF 指的是一个向量,其中每个元素代表一个词及其值是词频-逆文档频率,这是一个旨在反映一个词在文档集合(或语料库)中对于文档重要性的数值统计量。
附录 B. Neo4j
在本书中,示例、代码和练习都是基于特定的图数据库:Neo4j。尽管如此,所有的理论、算法甚至代码都可以轻松地适应与市场上现在和(在良好近似下)将来的任何图数据库一起工作。我选择这个数据库是因为
-
我过去 10 年一直在使用这个数据库(并且对其了如指掌)。
-
它是一个原生的图数据库(它带来的所有后果,如本书中所述)。
-
它拥有广泛的专家社区。
根据 DB-Engines 的数据,Neo4j 已经连续几年是最受欢迎的图数据库管理系统(DBMS)(mng.bz/YAMB; 图 B.1)。¹

图 B.1 图数据库管理系统(DBMS)的 DB-Engines 排名
本附录包含启动 Neo4j 所需的最基本信息,包括对 Neo4j 的通用介绍、安装说明、Cypher 语言(用于查询数据库的语言)的描述,以及示例中使用的某些插件的配置。
B.1 Neo4j 简介
Neo4j 作为 GPL3 许可的开源社区版提供。Neo4j Inc.还根据封闭源代码的商业条款,以企业级功能(包括备份、扩展和其它企业级特性)许可企业版。Neo4j 是用 Java 实现的,可以通过事务 HTTP 端点或二进制 Bolt 协议在网络中访问。² 我们将在本书中使用 Neo4j 作为我们的图数据库参考实现。
Neo4j 因其以下原因被广泛采用:
-
它实现了带标签的属性图数据库。³
-
它使用基于无索引邻接的原生图存储。⁴
-
它提供了原生的图查询和相关语言 Cypher⁵,该语言定义了图数据库如何描述、计划、优化和执行查询。
-
每个架构层——从使用 Cypher 的查询到磁盘上的文件——都针对存储和检索图数据进行了优化。
-
它提供了一个易于使用的开发者工作台,带有图可视化界面。
Neo4j 旨在提供全功能的工业级数据库。事务支持是其优势之一,使其与大多数 NoSQL 解决方案区分开来。Neo4j 提供了完整的 ACID 支持 [Vukotic et al., 2014]:
-
原子性(A)——您可以在单个事务中包装多个数据库操作,并确保它们都是原子性地执行的。如果其中一个操作失败,整个事务将被回滚。
-
一致性(C)——当您将数据写入 Neo4j 数据库时,您可以确信随后访问数据库的每个客户端都将读取最新更新的数据。
-
隔离性 (I)—您可以确信单个事务内的操作将相互隔离,因此一个事务中的写入不会影响另一个事务中的读取。
-
耐用性 (D)—您可以确信您写入 Neo4j 的数据将被写入磁盘,并在数据库重启或服务器崩溃后仍然可用。
ACID 支持使得习惯于传统关系数据库保证的任何人都能轻松过渡到 Neo4j,并且使得处理图数据既安全又方便。除了 ACID 事务支持外,在选择适合架构堆栈的数据库时还应考虑以下特性:
-
可恢复性—这一特性与数据库在失败后恢复事物的能力有关。数据库,就像所有其他软件系统一样,“容易受到其实施中的错误、运行其上的硬件以及该硬件的电力、冷却和连接性的影响。尽管勤勉的工程师们试图最小化所有这些失败的可能性,但最终数据库崩溃是不可避免的。当失败的服务器恢复操作时,它必须不向用户提供损坏的数据,无论崩溃的性质或时间如何。在从未清理的关闭中恢复时,可能是由于故障或甚至是一个过于热情的操作员,Neo4j 会检查最近的活跃事务日志,并重新播放它发现的任何事务。可能有些事务已经应用到存储中,但由于重新播放是一个幂等操作,最终结果是相同的:恢复后,存储将与失败前成功提交的所有事务保持一致” [Robinson 等人,2015]。此外,Neo4j 提供了一个在线备份程序,允许您在原始数据丢失时恢复数据库。在这种情况下,恢复到最后提交的事务是不可能的,但比丢失所有数据要好 [Robinson 等人,2015]。
-
可用性——除了恢复性和增加恢复机会之外,“一个好的数据库需要具有高度的可用性,以满足数据密集型应用程序日益复杂的需求。数据库在崩溃后能够识别并(如果需要)修复实例的能力意味着数据可以快速恢复,无需人工干预。当然,更多的活动实例会增加数据库处理查询的整体可用性。在典型的生产场景中,通常不希望有单独的断开连接的数据库实例。更常见的是,我们为了高可用性而集群数据库实例。Neo4j 使用主/从集群配置来确保每个机器上存储了图的一个完整副本。写入操作会频繁地从主节点复制到从节点。在任何时候,主节点和一些从节点将拥有图的一个完全最新的副本,而其他从节点正在追赶(通常,它们将落后几毫秒)”[Robinson 等人,2015]。
-
容量——另一个关键方面与数据库中可以存储的数据量有关——在我们的特定案例中,是一个图数据库。得益于 Neo4j 3.0 及以后版本中动态大小指针的采用,数据库可以扩展到运行任何可想象大小的图工作负载,上限“在万亿级别”的节点[Woodie,2016]。
关于这个主题有两本优秀的书籍,分别是《Neo4j 实战》(Vukotic 等人,2014 年)和《图数据库》(Robinson 等人,2015 年)。在撰写本文时,可用的最新版本是 4.2.x,因此代码和查询都是与该版本进行测试的。
B.2 Neo4j 安装
Neo4j 有两种版本:社区版和企业版。社区版可以从 Neo4j 网站免费下载,并且可以在 GPLv3 许可证下无限期地用于非商业目的。⁶ 企业版可以下载并试用一定时间,并受到特定约束(因为它要求你购买适当的许可证)。本书的代码已经调整为与社区版完美兼容;我建议使用它,这样你就有足够的时间。你也可以使用作为 Docker 镜像打包的 Neo4j。
另一个选择是使用 Neo4j Desktop,⁷,这是一种 Neo4j 的开发者环境。你可以管理你喜欢的任意数量的项目和数据库服务器,并且也可以连接到远程的 Neo4j 服务器。Neo4j Desktop 附带了 Neo4j 企业版的免费开发者许可证。从 Neo4j 下载页面,你可以选择要下载和安装的版本。
B.2.1 Neo4j 服务器安装
如果你决定下载 Neo4j 服务器(社区版或企业版),安装过程很简单。对于 Linux 或 macOS,请确保你已经安装了 Java 11 或更高版本,然后按照以下步骤操作:
-
打开你的终端/壳。
-
使用 tar xf
(例如 tar xf neo4j-community-4.2.3-unix.tar.gz)提取存档内容。 -
将提取的文件放置在服务器上的永久主目录。顶级目录是 NEO4J_HOME。
-
要以控制台应用程序运行 Neo4j,请使用 <NEO4J_HOME>/bin/neo4j console。
-
要在后台进程运行 Neo4j,请使用 <NEO4J_HOME>/bin/neo4j start。
-
在您的网络浏览器中访问 http://localhost:7474。
-
使用默认用户名 neo4j 和密码 neo4j 连接。您将被提示更改密码。
在 Windows 机器上,过程类似;解压缩下载的文件,然后继续操作。
在过程结束时,当您在浏览器中打开指定的链接时,您应该看到类似于图 B.2 的内容。

图 B.2 Neo4j 浏览器
Neo4j 浏览器是一个简单的基于网络的 Web 应用程序,允许用户与 Neo4j 实例交互,提交查询,并执行基本配置。
B.2.2 Neo4j Desktop 安装
如果您决定安装 macOS 的桌面版本,请按照以下步骤快速安装和运行:⁸
-
在下载文件夹中,找到并双击您下载的 .dmg 文件以启动 Neo4j Desktop 安装程序。
-
通过将 Neo4j Desktop 图标拖到该文件夹中(图 B.3),将应用程序保存到应用程序文件夹(无论是全局的还是您特定的用户文件夹)。
![APPB_F03_Negro]()
图 B.3 在 macOS 中安装 Neo4j Desktop
-
双击 Neo4j Desktop 图标以启动它(图 B.4)。
![APPB_F04_Negro]()
图 B.4 启动 Neo4j Desktop
-
第一次启动 Neo4J Desktop 时,您将被要求输入在下载软件时收到的激活码。将代码复制并粘贴到激活密钥框中。或者,您可以通过填写屏幕右侧的表单(图 B.5)在应用程序内部生成密钥。
![APPB_F05_Negro]()
图 B.5 在 Neo4j Desktop 中激活许可证
-
当产品被激活(图 B.6)时,点击“添加图”按钮。
![APPB_F06_Negro]()
图 B.6 将图添加到 Neo4j Desktop
-
选择创建本地图(图 B.7)。
![APPB_F07_Negro]()
图 B.7 创建新的本地图
-
输入数据库名称和密码,然后点击“创建”按钮(图 B.8)。
![APPB_F08_Negro]()
图 B.8 在创建新的本地图时,创建一个管理员密码。
-
通过点击“启动”按钮(图 B.9)启动新图。
![APPB_F09_Negro]()
图 B.9 启动新创建的数据库实例。
-
点击“管理”按钮(图 B.10)。
![APPB_F10_Negro]()
图 B.10 点击“管理”来操作图数据库。
-
在下一屏,点击“打开浏览器”以在新窗口中打开 Neo4j 浏览器(图 B.11)。

图 B.11 打开 Neo4j 浏览器的“打开浏览器”按钮。
你将能够访问浏览器,你可以从中与 Neo4j 进行交互。
如果你想要避免所有这些努力,Neo4j 有一个名为 Aura 的云版本。⁹ 在撰写本文时,如果你想在跳入之前稍微尝试一下,有一个免费层版本可用。但请注意,然而,对于本书中的练习和学习周期,最好是在你的机器上或你能够运行 Python 代码的地方安装 Neo4j。
B.3 Cypher
Neo4j 使用的查询语言是 Cypher。¹⁰ 与 SQL(它受到了它的启发)一样,Cypher 允许用户从图数据库中存储和检索数据。Cypher 容易学习、理解和使用,同时它提供了其他标准数据访问语言的力量和功能。
Cypher 是一种使用 ASCII-Art 语法描述图中的视觉模式的声明性语言。通过使用其语法,你可以以直观、逻辑的方式描述图模式。以下是一个简单的示例,用于在图中查找所有类型为 Person 的节点:
MATCH (p:Person)
RETURN p
这种模式可以用来在图中搜索节点和关系,或者创建它们。它允许你声明你想要从你的图数据中选择、插入、更新或删除的内容,而不必描述如何确切地执行它。
除了被 Neo4j 使用外,Cypher 还已成为开源。openCypher¹¹ 项目提供了一个开放的语言规范、技术兼容性套件以及 Cypher 的解析器、规划器和运行时的参考实现。该项目由数据库行业中的几家公司在背后支持,允许数据库和客户端的实现者自由地从 openCypher 语言的开发中受益、使用和贡献。
在本书中,你将通过示例和练习学习这门语言。如果你想了解更多关于 Cypher 的信息,我推荐 Neo4j 的指南;¹² 它是一个很好的参考,充满了示例。
B.4 插件安装
Neo4j 的一个优点是它很容易扩展。Neo4j 允许开发者以多种方式对其进行定制。你可以在查询图时使用新的过程和函数来丰富 Cypher 语言。你可以通过身份验证和授权插件来定制安全性。你还可以通过服务器扩展启用在 HTTP API 中创建新的表面。
此外,你可以下载、配置和使用许多现有的插件。其中最相关的是由 Neo4j 开发的,并且得到整个社区的支持,因为它们是开源的。为了本书的目的,我们将考虑其中的两个:
-
Cypher 上的神奇程序 (APOC)——APOC 库是一个标准的实用程序库,包含常见的程序和函数。它包含超过 450 个程序,并提供从 JDBC 源或 JSON 读取数据、转换、图更新等功能。在大多数情况下,函数和程序都得到了良好的支持和稳定性。
-
图数据科学 (GDS) 库——这个程序库实现了许多常见的图算法,如 PageRank、几个中心度度量、相似度以及更近期的技术,如节点嵌入和链接预测。因为这些算法在 Neo4j 引擎内部运行,所以在分析前对节点和关系的读取以及结果的存储进行了优化。这一特性使得这个库能够在数十亿个节点上快速计算结果。
下两个部分将描述如何下载、安装和配置这些插件。我建议在您开始使用第四章中的 Cypher 查询之前完成所有这些操作。
B.4.1 APOC 安装
在 Neo4j 中安装插件很简单。让我们从 APOC 库开始。如果您安装了服务器版本,请从相关的 GitHub 发布页面下载插件¹³(使用 *-all.jar 获取完整库,并选择与您的 Neo4j 版本匹配的版本),并将其复制到 NEO4J_HOME 文件夹内的插件目录中。此时,通过调整或添加以下行来编辑配置文件 conf/neo4j.conf:
dbms.security.procedures.unrestricted=apoc.*
dbms.security.procedures.allowlist=apoc.*
重启 Neo4j,并打开浏览器。运行以下程序来检查是否一切就绪:
CALL dbms.procedures() YIELD name
WHERE name STARTS WITH "apoc"
RETURN name
您应该能看到 APOC 程序的列表。

图 B.12 Neo4j Desktop 中的 APOC 安装
如果您使用的是 Neo4j Desktop 版本,过程会更简单。在创建数据库后,打开管理屏幕,点击插件选项卡,点击 APOC 框中的安装,等待看到已安装的消息(图 B.12)。有关更详细的信息和解释,请参阅官方 APOC 安装指南。¹⁴
B.4.2 GDS 库
您可以遵循类似的步骤来安装 GDS 库。如果您安装了服务器版本,请从相关的 GitHub 发布页面下载插件¹⁵(使用 *-standalone .jar),并将其复制到 NEO4J_HOME 文件夹内的插件目录中。此时,通过调整或添加以下行来编辑配置文件 conf/neo4j.conf:
dbms.security.procedures.unrestricted=apoc.*,gds.*
dbms.security.procedures.allowlist=apoc.*,gds.*
重启 Neo4j,打开浏览器,并运行以下程序来检查是否一切就绪:
RETURN gds.version()
您应该能够看到您下载的 GDS 版本。

图 B.13 从 Neo4j Desktop 安装 GDS
如果您使用 Neo4j Desktop,过程会更加简单。在创建数据库后,打开管理屏幕,点击插件标签,点击 Graph Data Science Library 中的安装,等待看到已安装的消息(图 B.13)。完成这些步骤后,您就可以享受 Neo4j 的乐趣,并运行书中的所有示例和练习。
B.5 清理
有时,您需要清理数据库。您可以通过将安装到数据库中的 APOC 库中的函数来完成这项工作。要删除所有内容:
CALL apoc.periodic.iterate('MATCH (n) RETURN n', 'DETACH DELETE n',
➥ {batchSize:1000})
要删除所有约束:
CALL apoc.schema.assert({}, {})
参考文献
[Robison 等人,2015] Robinson, Ian, Jim Webber, 和 Emil Eifrem. 《图数据库》。第 2 版。Sebastopol, CA: O’Reilly, 2015.
[Vukotic 等人,2014] Vukotic, Aleksa, Dominic Fox, Jonas Partner, Nicki Watt, 和 Tareq Abedrabbo. 《Neo4j 实战》。Shelter Island, NY: Manning, 2014.
[Woodie, 2016] Woodie, Alex. “Neo4j Pushes Graph DB Limits Past a Quadrillion Nodes.” Datanami,2016 年 4 月 26 日。mng.bz/0rJN.
(1.) 评分考虑了多个因素,例如数据库在网站上的提及次数、Stack Overflow 和 Database Administrators Stack Exchange 上的技术问题频率、工作机会以及在社会网络中的相关性。
(2.) Bolt 协议.
(3.) 如果您还没有阅读第二章,请参阅 2.3.5 节以获取有关标签属性图的详细信息。
(4.) 请参阅附录 D。
(5.) OpenCypher.
(6.) GNU 通用公共许可证 v3.0.
(7.) Neo4j 开发者桌面.
(8.) 安装过程取自安装指南,该指南在您下载软件时可用。请参考该安装指南以获取您操作系统的说明。
(9.) Neo4j 云服务 Aura.
(10.) Neo4j 开发者 Cypher.
(11.) OpenCypher
(12.) Neo4j 开发者 Cypher
(13.) mng.bz/G6mv
(14.) APOC 安装.
(15.) mng.bz/zGDB.
附录 C. 用于处理模式和流程的图
在许多机器学习项目中,包括本书中描述的许多项目,生成的图都是大的。这些图的规模使得高效处理它们变得困难。为了应对这些挑战,已经出现了各种分布式图处理系统。在本附录中,我们将探讨这些系统之一:Pregel,这是第一个用于处理大规模图的计算模型(并且仍然是使用最广泛的模型之一)。¹ 这个主题适合附录的目的主要有两个原因:
-
它定义了一个处理模型,这对于提供本书中讨论的一些算法的替代实现(基于图和非基于图)非常有用。
-
它展示了图的表达能力,并提出了基于信息图表示的计算的替代方法。
C.1 Pregel
假设你想要在一个大图上执行 PageRank 算法,比如整个互联网。如第 3.3.1 节所述,PageRank 算法是由 Google 的创始人为其搜索引擎开发的,因此算法的原始目的是相同的。我们在第三章中探讨了算法的工作原理,现在让我们专注于如何解决一个具体问题:处理如此大规模图的 PageRank 值。由于节点(网页)和边(网页之间的链接)数量众多,这项任务将非常复杂,需要分布式方法。
Pregel 计算输入是一个有向图,其中每个节点都有一个唯一的标识符,并关联一个可修改的、用户定义的值,该值以某种方式初始化(也是输入的一部分)。每条有向边都与
-
一个源节点标识符
-
一个目标节点标识符
-
一个可修改的、用户定义的值
在 Pregel 中,程序被表示为一串迭代(称为超级步),由全局同步点分隔,直到算法终止并产生其输出。在每一个超级步 S 中,一个节点可以完成以下任务之一,概念上是在并行进行的 [Malewicz et al., 2010]:
-
接收在上一迭代、超级步 S - 1 发送给它的消息
-
向其他节点发送将在超级步 S + 1 读取的消息
-
修改自己的状态和其出度边的状态,或突变图拓扑
消息通常沿着输出边(到直接连接的节点)发送,但可以向任何已知标识符的节点发送消息。在超级步 0 时,每个节点都是活跃的;所有活跃节点都参与任何给定超级步的计算。在每个迭代的末尾,一个节点可以通过投票停止来决定自己停止活动。此时它变为非活动状态,并且除非它从另一个节点收到消息,否则它将不会参与后续的超级步,此时它将被重新激活。在重新激活后,想要停止的节点必须再次显式地停止活动。这个简单的状态机在图 C.1 中进行了说明。当所有节点都投票停止时,达到迭代的终止条件,因此在下一个超级步中不再进行任何工作。

图 C.1 根据 Pregel 计算模型显示的节点状态
在将 Pregel 框架应用于我们的 PageRank 用例之前,让我们考虑一个更简单的例子:给定一个强连通图,其中每个节点包含一个值,找到节点中存储的最高值。这个算法的 Pregel 实现工作方式如下:
-
图和每个节点的初始值代表输入。
-
在超级步 0 时,每个节点将其初始值发送给所有邻居。
-
在每个后续的超级步 S 中,如果一个节点在超级步 S - 1 收到的消息中学习到一个更大的值,它将把这个值发送给所有它的邻居;否则,它将停止活动并停止投票。
-
当所有节点都停止活动并且没有进一步的变化时,算法终止。
这些步骤在图 C.2 中用具体数字进行了展示。

图 C.2 Pregel 实现寻找节点中存储的最高值
Pregel 使用纯消息传递模型,原因有两个:
-
消息传递对于图算法来说已经足够表达;不需要远程读取(从处理集群中的其他机器读取数据)或其他模拟共享内存的方式。
-
通过避免从远程机器读取值并以批量异步发送消息,可以降低延迟,从而提高性能。
虽然 Pregel 的以节点为中心的模型易于编程,并且已被证明对许多图算法很有用,但值得注意的是,这种模型隐藏了分区信息,从而阻止了许多针对特定算法的优化,这通常会导致由于过度的网络负载而执行时间更长。为了解决这一限制,存在其他方法。这种方法可以定义为一种以图为中心的编程范式。在这个以图为中心的模型中,分区结构对用户开放,并且可以被优化,以便分区内的通信可以绕过繁重的消息传递[Tian 等人,2013]。
现在模型已经明确,优势和劣势已经突出,让我们回到我们的场景,并检查使用 Pregel 实现 PageRank 算法的逻辑步骤,这可能会像图 C.3 所示。

图 C.3 使用 Pregel 框架实现的 PageRank
图 C.3 中的模式可以进一步描述如下:
-
图的初始化方式是,在超级步 0 中,每个节点的值为 1/ NumNodes()。每个节点通过每个出边发送这个值除以出边的数量。
-
在每个后续的超级步中,每个节点将收到的消息中的值加到 sum 中,并将自己的临时 PageRank 设置为 0.15/NumNodes() + 0.85 × sum。然后它通过每个出边发送自己的临时 PageRank 除以出边的数量。
-
如果所有整体价值的变化都低于某个阈值,或者达到预定义的迭代次数,算法将终止。
Pregel 实现的 PageRank 算法在互联网场景中的有趣之处在于,我们有一个基于自然的数据集(互联网链接),一个纯图算法(PageRank),以及一个基于图的计算范式。
C.2 用于定义复杂处理工作流程的图
在机器学习项目中,图模型不仅可以用于表示复杂的数据结构,使其易于存储、处理或访问,而且还可以有效地描述复杂的处理工作流程——完成更大任务所需的子任务序列。图模型使我们能够可视化整个算法或应用,简化问题的识别,并使得即使通过自动化过程也能轻松实现并行化。尽管本书不会详细介绍这种特定的图应用,但介绍它是很重要的,因为它展示了图模型在表示与机器学习不必要相关联的复杂规则或活动中的价值。
数据流是一种编程范式(通常称为 DFP,即 数据流编程),它使用有向图来表示复杂的应用,并且在并行计算中被广泛使用。在数据流图中,节点代表计算单元,边代表计算消耗或产生的数据。TensorFlow² 使用这些图来表示计算,这些计算基于单个操作之间的依赖关系。
C.3 数据流
假设你正在期待一个宝宝的出生,在你最后一次访问时,医生预测新生儿的体重为 7.5 磅。你可能会想知道这可能与婴儿的实际测量体重有何不同。
让我们设计一个函数来描述新生儿所有可能体重的可能性。你可能会想知道 8 磅是否比 10 磅更可能,例如[Shukla, 2018]。对于这种预测,通常使用高斯(也称为正态)概率分布函数。这个函数接受一个数字和一些其他参数作为输入,并输出一个非负数,描述观察输入的概率。正态分布的概率密度由以下方程给出

其中
-
μ是分布的均值或期望(也是它的中位数和众数)。
-
σ是标准差。
-
σ²是方差。
这个公式指定了如何计算 x(在我们的场景中是重量)的概率,考虑到中位数μ(在我们的例子中,是 7.5 磅)和标准差(它指定了与平均值的差异)。中位数不是随机的;它是北美新生儿实际的平均体重。³ 这个函数可以用 XY 图表表示,如图 C.4 所示。

图 C.4 正态分布曲线(钟形曲线)
根据σ(标准差)的值,曲线可以是更高的或更胖的,而根据μ(均值)的值,它可以移动到图表的左侧或右侧。图 C.4 将均值的值定为中心 7.5。根据标准差的值,最近值的概率分布可能更多或更少。较高的曲线具有 0.2 的方差,而较胖的曲线具有 5 的方差。方差较小的值意味着最可能值最接近均值(在两侧)。
在任何情况下,图表呈现的结构相似,因此非正式地被称为钟形曲线。这个公式及其相关表示意味着曲线尖端附近的事件比曲线两侧的事件更有可能发生。在我们的例子中,如果新生儿的预期平均体重是 7.5 磅,且已知方差,我们可以使用这个函数来获取 8 磅与 10 磅体重的概率。这个函数在机器学习中经常出现,并且在 TensorFlow 中很容易定义;它只使用乘法、除法、取反和一些其他基本运算符。
要将此类函数转换为数据流中的图形表示,可以通过将均值设置为 0 和标准差设置为 1 来简化它。使用这些参数值,公式变为

这个新函数有一个特定的名称:标准正态分布。转换为图形格式需要以下步骤:
-
每个运算符都成为图中的一个节点,因此我们将有表示乘法、幂、取反、平方根等的节点。
-
操作符之间的边表示数学函数的组合。
从这些简单的规则开始,结果的高斯概率分布的图形表示如图 C.5 所示。

图 C.5 数据流编程中正态分布的图形表示(σ = 1)
图的简单部分代表简单的数学概念。如果一个节点只有一个入边,它是一个一元操作符(一个操作单个输入的操作符,如否定或加倍),而一个有两个入边的节点是一个二元操作符(一个操作两个输入变量的操作符,如加法或指数)。在图 C.5 中,将 8 磅(我们希望考虑的新生儿的重量)作为公式的输入将提供这个重量的概率。该图清楚地显示了函数的不同分支,这意味着可以很容易地识别可以并行处理的公式部分。
在 TensorFlow 中,这种方法使得即使是看似相当复杂的算法的可视化和处理也变得容易。尽管在某些场景中非常有用,但 DFP 范式通常被遗忘,但 TensorFlow 通过展示图形表示在复杂过程和任务中的力量而使其复兴。DFP 方法的优势可以总结如下 [Johnstonet et al., 2004; Sousa, 2012]:
-
它提供了一种具有简化界面的可视化编程语言,这使得某些系统的快速原型设计和实现成为可能。我们已讨论过视觉感知和图形作为更好地理解复杂数据结构的方法的重要性。DFP 能够在保持简单易懂和易于修改的同时,表示复杂的应用程序和算法。
-
它隐式地实现了并发性。数据流研究最初的动机是利用大规模并行性。在数据流应用程序中,内部每个节点都是一个独立的处理块,它独立于所有其他节点工作,并且不会产生副作用。这种执行模型允许节点在数据到达时立即执行,而不会产生死锁的风险,因为系统中没有数据依赖。数据流模型的重要特性可以显著提高在多核 CPU 上执行的应用程序的性能,而无需程序员进行任何额外的工作。
数据流应用程序是图形在将复杂问题分解为易于可视化、修改和并行化的子任务方面的表达能力的另一个例子。
参考文献
[Malewicz, 2010] Malewicz, Grzegorz, Matthew H. Austern, Aart J. C. Bik, James C. Dehnert, Ilan Horn, Naty Leiser, and Grzegorz Cjajkowski. “Pregel: A System for Large-Scale Graph Processing.” Proceedings of the 2010 ACM SIGMOD International Conference on Management of Data (2010): 135-146.
[Tianet al., 2013] Tian, Yuanyuan, Andrey Balmin, Severin Andreas Corsten, Shirish Tatikonda, 和 John McPherson. “从‘像顶点一样思考’到‘像图一样思考’。” VLDB 奖励会议论文集 7:3 (2013): 193-204.
[Johnston et al., 2004] Johnston, Wesley M., J. R. Paul Hanna, 和 Richard J. Millar. “数据流编程语言的发展。” ACM 计算评论 (CSUR) 36:1 (2004): 1-34.
[Sousa, 2012] Sousa, Tiago Boldt. “数据流编程:概念、语言和应用。” 信息工程博士研讨会 (2012).
[Shukla, 2018] Shukla, Nishant. TensorFlow 机器学习. 避风岛,纽约:Manning,2018.
^(1.)其名称是为了纪念莱昂哈德·欧拉;启发欧拉定理的柯尼斯堡大桥横跨普雷格尔河 [Malewicz, 2010].
^(2.)www.tensorflow.org.
^(3.)www.uofmhealth.org/health-library/te6295.
附录 D. 表示图
在合适的方式下表示图 G = (V, E) 的两种标准方法:作为邻接表的集合或作为邻接矩阵。每种方法都可以应用于有向、无向和非加权图 [Cormen 等人,2009]。
图 G = (V, E) 的邻接表表示由一个数组 Adj 的列表组成,每个列表对应 V 中的一个顶点。对于 V 中的每个顶点 u,邻接表 Adj[u] 包含所有存在边 E[uv] 连接 u 和 v 的顶点 v。换句话说,Adj[u] 包含 G 中与 u 相邻的所有顶点。
图 D.1(b) 是图 D.1(a) 中无向图的邻接表表示。顶点 1 有两个邻居,2 和 5,因此 Adj[1] 是列表 [2,5]。顶点 2 有三个邻居,1、4 和 5,因此 Adj[2] 是 [1,4,5]。其他列表以相同的方式创建。值得注意的是,由于关系没有顺序,列表中也没有特定的顺序;因此,Adj[1] 可以是 [2,5] 也可以是 [5, 2]。

图 D.1 一个无向图 (a) 和相关的邻接表表示 (b)
类似地,图 D.2(b) 是图 D.2(a) 中有向图的邻接表表示。这种列表被可视化为一个链表,其中每个条目都包含对下一个条目的引用。在节点 1 的邻接表中,第一个元素是节点 2;它也包含对下一个元素的引用,即节点 5 的元素。这种方法是存储邻接表中最常见的方法之一,因为它使得添加和删除等操作变得高效。在这种情况下,我们只考虑出边关系,但我们可以用入边关系做同样的处理;重要的是在创建邻接表时选择一个方向并保持一致性。在这里,顶点 1 只有一个出边关系,与顶点 2,因此 Adj[1] 将是 [2]。顶点 2 有两个出边关系,与 4 和 5,因此 Adj[2] 是 [4,5]。顶点 4 没有出边关系,因此 Adj[4] 是空的 ([]).

图 D.2 一个有向图 (a) 和相关的邻接表表示 (b)
如果 G 是一个有向图,所有邻接表长度的总和是 |E |。因为每条边只能沿一个方向遍历,所以 E[u]v 只会出现在 Adj[u] 中。如果 G 是一个无向图,所有邻接表长度的总和是 2 × |E |,因为如果 E[uv] 是无向边,E[uv] 会出现在 Adj[u] 和 Adj[v] 中。有向或无向图的邻接表表示所需的内存与 |V | + |E | 成正比。
邻接表可以通过在 Adj[u] 中存储边 E[uv] 的权重 w 来轻松地适应表示加权图。邻接表表示也可以类似地修改以支持许多其他图变体。
这种表示方法的一个缺点是,它没有提供比在邻接表 Adj[u] 中搜索 v 更快的方法来确定给定的边 E[uv] 是否存在于图中。图的邻接矩阵表示法可以弥补这一缺点,但代价是使用渐增更多的内存。
对于图 G = (V, E) 的邻接矩阵表示,我们假设顶点以某种任意方式编号为 1,2,...,|V|,并且在邻接矩阵的生命周期内保持这些编号的一致性。那么,图 G 的邻接矩阵表示由一个 |V| × |V| 矩阵 A = (a[uv]) 组成,其中 a[uv] = 1 如果 E[uv] 在图中存在,否则 a[uv] = 0。
图 D.3(b) 是图 D.3(a) 中表示的无向图的邻接矩阵表示。例如,第一行与顶点 1 相关。矩阵中的这一行在第 2 列和第 5 列有 1,因为它们代表与顶点 1 相连的顶点。所有其他值都是 0。与顶点 2 相关的第二行在第 1 列、第 4 列和第 5 列有 1,因为这些顶点是连接顶点。

图 D.3 一个无向图(a)及其作为邻接矩阵的相关表示(b)
图 D.4(b) 是图 D.4(a) 中表示的有向图的邻接矩阵表示。至于邻接表,在创建矩阵时必须选择一个方向并使用它。在这种情况下,矩阵的第一行在第 2 列有 1,因为顶点 1 与顶点 2 有一个出边关系;所有其他值都是 0。矩阵表示的一个有趣特点是,通过查看列,可以查看入边关系。例如,第 4 列显示顶点 4 有两个入边连接:来自顶点 2 和 3。

图 D.4 一个有向图(a)及其作为邻接矩阵的相关表示(b)
图的邻接矩阵所需的内存与 |V| × |V| 成正比,与图中边的数量无关。在一个无向图中,得到的矩阵沿主对角线是对称的。在这种情况下,可以只存储矩阵的一半,将存储图所需的内存几乎减半。
与图的邻接表表示法一样,邻接矩阵可以表示加权图。如果 G = (V, E) 是一个加权图,并且 w 是边 E[uv] 的权重,则 a[uv] 将被设置为 w 而不是 1。
尽管邻接表表示在空间效率上至少与邻接矩阵表示相当,但邻接矩阵更简单,因此当图相对较小时,你可能更倾向于使用它们。此外,对于无权图,邻接矩阵还有一个额外的优势:每个条目只需要一个比特。因为邻接表表示提供了一种紧凑的方式来表示稀疏图——即边数少于顶点数的图——所以它通常是首选的方法。但是,当图是密集的——即|E|接近|V| × |V|时——或者当你需要快速判断两个给定顶点之间是否存在边时,你可能更喜欢邻接矩阵表示。
参考文献
[Cormen et al., 2009] Cormen, Thomas H., Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. 算法导论. 第 3 版. 波士顿,马萨诸塞州:麻省理工学院出版社,2009。










浙公网安备 33010602011771号