Python-网络科学-全-

Python 网络科学(全)

原文:annas-archive.org/md5/3df7c5feb0bf40d7b9d88197a04b0b37

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

网络无处不在,渗透在每一件事物中。一旦你学会了如何去看它们,你会发现它们无处不在。自然数据是没有结构的,它是非结构化的。语言就是非结构化数据的一个例子,而网络则是另一个例子。我们时刻都在受到周围事物的影响。我写这本书的目的就是希望展示如何将语言数据转化为可以分析的网络,并进一步展示如何实现这一分析过程。

本书并不打算讲授网络科学的每一个方面,而是提供足够的介绍,让读者能够自信地使用工具和技术,揭示新的见解。

本书适读对象

我写这本书的目的是希望它能成为计算机科学与社会科学之间的桥梁。我希望两者都能受益,而不仅仅是其中一方。我的期望是,软件工程师能像我一样从社会科学中学到有用的工具和技巧,同时社会科学领域的人也能够利用软件工程扩展他们的思想和研究。数据科学家也会发现这本书对于深入网络分析非常有帮助。

然而,这本书不仅对社会科学家、软件工程师和数据科学家有用。影响力无处不在。理解如何衡量影响力或描绘思想流动的方式,在许多领域都是非常有价值的。

本书内容

第一章介绍自然语言处理,介绍了自然语言处理NLP)以及语言与网络之间的关系。我们将讨论 NLP 是什么、它的应用场景,并且讲解几个具体的使用案例。读者在本章结束时将能够对 NLP 有一个基本的理解,了解它是什么,长什么样子,以及如何开始使用 NLP 来研究数据。

第二章网络分析,介绍了网络分析,它对于分析网络的各个方面非常有用,例如网络的整体结构、关键节点、社区和组成部分。我们将讨论不同类型的网络和不同的应用场景,并且会逐步讲解一个简单的网络研究数据收集工作流程。本章对理解网络进行了温和的介绍,并阐述了理解网络如何在解决问题时发挥作用。

第三章实用的 Python 库,讨论了本书中使用的 Python 库,包括安装说明及其他相关内容。本章还提供了一些起始代码,帮助你开始使用库并确保其正常工作。库按照类别进行分组,以便容易理解和比较。我们将讨论用于数据分析和处理、数据可视化、自然语言处理、网络分析与可视化的 Python 库,以及机器学习ML)。本书不依赖图数据库,从而减少了学习网络分析技能的负担。

第四章自然语言处理与网络协同,讨论了如何将文本数据转换为可供分析的网络。我们将学习如何加载文本数据、提取实体(人名、地点、组织等)以及仅通过文本构建社交网络。这使我们能够创建角色和人物在文本中的互动可视化图谱,并且这些图谱可以作为互动方式,让我们更深入地理解所研究的内容或实体的上下文。

在本章中,我们还将讨论爬虫技术,也叫做网页抓取。学习如何从互联网收集数据并在自然语言处理和网络分析中使用它,将大大增强个人学习这些技能的能力。它还解锁了研究自己感兴趣的事物的能力,而不必依赖他人的数据集。

最后,我们将把文本转换为实际的网络并进行可视化。这是一个非常长的章节,因为它涵盖了将文本转换为网络的每一个步骤。

第五章更简便的抓取,展示了从互联网收集文本数据的更简便方法。某些 Python 库专门用于从新闻网站、博客网站和社交媒体中提取文本数据。本章将展示如何轻松快捷地获取干净的文本数据,以便用于后续处理。在本章中,我们将把文本转换为实际的网络,并且进行网络可视化。

第六章图构建与清理,深入探讨了如何处理网络。章节开始时展示了如何使用边列表创建图,然后描述了节点和边等重要概念。在本章中,我们将学习如何向图中添加、编辑和移除节点和边,这一过程是图构建与清理的一部分。我们将在本章的最后模拟一次网络攻击,展示即便移除少数几个关键节点也会对网络产生灾难性破坏的效果。这体现了单个节点对整个网络的重要性。

第七章整体网络分析,是我们真正开始进行网络分析的章节。例如,我们将寻找关于网络规模、复杂性和结构的答案。我们会寻找有影响力和重要的节点,并使用连接组件来识别网络中存在的结构。我们还将简要讨论社区检测,后者将在第九章中进行更深入的探讨。

第八章自我中心网络分析,研究网络中存在的自我中心网络。这意味着我们将更加关注感兴趣的节点(自我节点)以及环绕它们的节点(他者节点)。目标是理解自我节点周围的社会结构。这对于识别个人所属于的社区也很有帮助。

第九章社区检测,讨论了识别网络中存在的社区的几种方法。将展示并讨论几种强大的社区检测算法。我们还将讨论如何使用连接组件来识别社区。

第十章网络数据上的监督式机器学习,展示了我们如何利用网络创建用于机器学习分类任务的训练数据。在本章中,我们将手动创建训练数据,从网络中提取有用的特征。然后,我们将这些特征结合成训练数据,并构建一个分类模型。

第十一章网络数据上的无监督式机器学习,展示了无监督机器学习如何用于创建可以用于分类的节点嵌入。在上一章中,我们手动创建了网络特征。在本章中,我们将展示如何使用 Karate Club 创建节点嵌入,并将其用于分类。我们还将展示如何使用 Karate Club 的 GraphML 模型进行社区检测。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,地址是github.com/PacktPublishing/Network-Science-with-Python。如果代码有更新,将会在 GitHub 仓库中进行更新。

我们还提供了其他代码包,来自我们丰富的书籍和视频目录,您可以在github.com/PacktPublishing/查看。快去看看吧!

使用的约定

本书中使用了若干文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

一段代码如下所示:

html, body, #map {
 height: 100%;
 margin: 0;
 padding: 0
}

当我们希望您注意代码块中的某一部分时,相关的行或项目会以粗体显示:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

所有命令行输入或输出按以下格式书写:

$ mkdir css
$ cd css

粗体:表示新术语、重要单词或屏幕上出现的单词。例如,菜单或对话框中的单词会以粗体显示。以下是一个示例:“选择系统信息管理面板。”

提示或重要说明

显示如下。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何内容有疑问,请通过 customercare@packtpub.com 联系我们,并在邮件主题中注明书名。

勘误:虽然我们已尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现任何错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上遇到我们作品的非法复制品,请提供该位置地址或网站名称。请通过 copyright@packt.com 联系我们并附上相关链接。

如果您有兴趣成为作者:如果您在某个主题方面具有专业知识,并且有兴趣撰写或参与书籍的编写,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《使用 Python 进行网络科学》,我们很希望听到您的想法!请点击这里直接前往亚马逊书评页面并分享您的反馈。

您的反馈对我们以及技术社区非常重要,将帮助我们确保提供优质的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢随时随地阅读,但又无法随身携带纸质书籍吗?

您购买的电子书与您选择的设备不兼容吗?

不用担心,现在购买每本 Packt 书籍时,您都可以免费获得该书的无 DRM PDF 版本。

随时随地,在任何设备上阅读。直接将您最喜爱的技术书中的代码搜索、复制并粘贴到您的应用程序中。

福利不仅仅到此为止,您还可以获得独家折扣、时事通讯,并且每天有精彩的免费内容发送到您的邮箱。

按照以下简单步骤获取福利:

  1. 扫描二维码或访问以下链接

packt.link/free-ebook/9781801073691

  1. 提交您的购买凭证

  2. 就是这样!我们将直接通过电子邮件发送您的免费 PDF 和其他福利。

第一部分:自然语言处理与网络的入门

你将获得关于自然语言处理NLP)、网络科学和社交网络分析的基础知识。这些章节的内容旨在为你提供处理语言和网络数据所需的知识。

本节包括以下章节:

  • 第一章介绍自然语言处理

  • 第二章网络分析

  • 第三章有用的 Python 库

第一章:介绍自然语言处理

为什么一本网络分析书籍会从自然语言处理NLP)开始呢?我猜你一定会问这个问题,这个问题问得非常好。原因如下:我们人类用语言和文本来描述周围的世界。我们写关于我们认识的人、我们做的事情、我们去的地方等等。文本可以用来揭示存在的关系。事物之间的关系可以通过网络可视化来展示。我们可以通过网络分析来研究这些关系。

简而言之,文本可以用来提取有趣的关系,网络可以用来进一步研究这些关系。我们将使用文本和 NLP 来识别关系,使用网络分析和可视化来深入了解。

NLP 对于创建网络数据非常有用,我们可以利用这些网络数据来学习网络分析。本书是一个学习 NLP 和网络分析的机会,了解它们如何一起使用。

在以非常高的层次解释 NLP 时,我们将讨论以下主题:

  • 什么是 NLP?

  • 为什么在一本网络分析书中讨论 NLP?

  • NLP 的简短历史

  • NLP 是如何帮助我的?

  • NLP 的常见用途

  • NLP 的高级应用

  • 初学者如何开始学习 NLP?

技术要求

尽管本章有几个地方展示了代码示例,但我并不期望你现在就写代码。这些示例仅用于展示,给你一个预览,看看能做什么。本书的其余部分将非常实践,所以先看一看并理解我在做什么。现在不用担心写代码。首先,学习概念。

什么是 NLP?

NLP 是一组帮助计算机处理人类语言的技术。然而,它不仅仅可以用于处理单词和句子。它还可以处理应用日志文件、源代码或任何其他使用人类文本的地方,甚至是虚构语言,只要文本遵循语言的规则。自然语言是人类说或写的语言。处理是计算机使用数据的行为。所以,NLP就是计算机使用口语或书面的人类语言。这么简单。

作为软件开发人员中的许多人,可能多年来我们都在做 NLP,甚至没有意识到。我就拿我自己的例子来说。我从事网页开发时完全是自学的。在我职业生涯的初期,我建立了一个非常受欢迎的网站,拥有一个不错的社区,于是我从当时流行的 Yahoo Chats 中获得灵感,逆向工程它,并建立了我自己的互联网留言板。它迅速成长,提供了多年的娱乐,并让我结交了几个亲密的朋友。然而,像所有优秀的社交应用一样,恶搞者、机器人以及各种恶劣的用户最终成为了问题,所以我需要一种方式来自动标记和隔离恶意内容。

当时,我创建了包含侮辱性词汇和字符串的列表,这些词汇可以帮助识别虐待行为。我并不想停止所有的脏话,因为我不相信完全控制人们在线发布的文本;然而,我希望识别有毒行为、暴力和其他不良行为。任何拥有评论区的网站都很可能做类似的事情来管理网站,或者应该这样做。关键是,我从职业生涯开始就一直在做 NLP,而我甚至没有意识到,但那时还是基于规则的。

现在,机器学习主导了 NLP 领域,因为我们能够训练模型来检测虐待、暴力,或几乎任何我们能想象的东西,这也是我最喜欢 NLP 的一个原因。我感觉自己的创造力才是唯一的限制。因此,我已经创建了分类器来检测包含极端政治情绪、暴力、音乐、艺术、数据科学、自然科学和虚假信息的讨论,在任何时刻,我通常都有几个 NLP 模型在脑海中,想要构建却还没找到时间。我甚至使用 NLP 来检测恶意软件。但是,NLP 不仅仅是针对书面或口语的,如我的恶意软件分类器所示。如果你记住这一点,你会发现 NLP 的潜在用途会大大扩展。我的经验法则是如果数据中存在可以提取为词语的序列——即使它们本身不是词语——也可以使用 NLP 技术

过去,可能现在也一样,分析师会丢弃包含文本的列,或者进行一些非常基础的转换或计算,例如一热编码、计数,或确定某物是否存在(真/假)。然而,你可以做得更多,我希望这一章和这本书能激发你们的灵感和好奇心,带给你们启发。

为什么在一本网络分析书中讲 NLP?

你们中的大多数人可能是为了学习如何使用 Python 进行应用社会网络分析才购买了这本书。那么,为什么我要讲解 NLP 呢?原因是:如果你熟悉 NLP 并且能够从文本中提取数据,那么这对于创建网络数据并调查文本中提到的事物之间的关系来说,可以非常强大。下面是我最喜欢的书《爱丽丝梦游仙境》中的一个例子,作者是路易斯·卡罗尔。

“从前有三个小妹妹,”睡鼠急匆匆地开始讲,“她们的名字叫 Elsie、Lacie 和 Tillie,她们住在一个井底。”

从这些词语中我们能观察到什么?提到了哪些角色或地方?我们可以看到,睡鼠正在讲述一个关于三姐妹的故事,她们的名字分别是ElsieLacieTillie,并且她们住在一个井底。如果你能以关系的角度来思考,你会发现这些关系是存在的:

  • 三姐妹 -> 睡鼠(他要么认识她们,要么知道一个关于她们的故事)

  • Dormouse -> Elsie

  • Dormouse -> Lacie

  • Dormouse -> Tillie

  • Elsie -> 井底

  • Lacie -> 井底

  • Tillie -> 井底

也很可能这三姐妹彼此都认识,因此出现了额外的关系:

  • Elsie -> Lacie

  • Elsie -> Tillie

  • Lacie -> Elsie

  • Lacie -> Tillie

  • Tillie -> Elsie

  • Tillie -> Lacie

我们的大脑如此高效地构建这些关系图,以至于我们甚至没有意识到自己在这么做。当我读到这三人是姐妹时,我脑海中立刻浮现出这三人彼此认识的画面。

让我们再看一个来自当前新闻故事的例子:奥卡西奥-科尔特斯加大对曼钦批评力度(CNN,2021 年 6 月:edition.cnn.com/videos/politics/2021/06/13/alexandria-ocasio-cortez-joe-manchin-criticism-sot-sotu-vpx.cnn)。

代表亚历山大·奥卡西奥-科尔特斯(纽约州 D)表示,乔·曼钦参议员(西弗吉尼亚州 D)不支持一项房屋投票权法案,受到了该法案广泛改革的影响,旨在限制游说者的角色和“黑暗资金”政治捐赠的影响。

谁被提到,他们之间是什么关系?我们从这段简短的文字中能学到什么?

  • 代表亚历山大·奥卡西奥-科尔特斯在谈论参议员乔·曼钦

  • 两者都是民主党成员

  • 参议员乔·曼钦不支持一项房屋投票权法案

  • 代表亚历山大·奥卡西奥-科尔特斯声称参议员乔·曼钦受到该法案改革的影响

  • 代表亚历山大·奥卡西奥-科尔特斯声称,参议员乔·曼钦受到了“黑暗资金”政治捐赠的影响

  • 可能存在参议员乔·曼钦与“黑暗资金”政治捐赠者之间的关系

我们可以看到,即使是少量的文本,也蕴含了大量的信息。

如果你在处理文本时无法确定关系,我在大学创意写作课上学到,要考虑“W”问题(以及如何),以便在故事中解释事物:

  • 谁:谁参与其中?谁在讲述故事?

  • 什么:在谈论什么?发生了什么?

  • 什么时候:这发生在什么时候?是哪一天的什么时间?

  • 哪里:发生在何处?描述的是哪个地点?

  • 为什么:这为什么重要?

  • 如何:这件事是怎么做的?

如果你提出这些问题,你会注意到事物之间的关系,这对构建和分析网络是基础。如果你能做到这一点,你就能在文本中识别关系。如果你能识别文本中的关系,你就能利用这些知识构建社交网络。如果你能构建社交网络,你就能分析关系,检测重要性,发现弱点,并利用这些知识深入理解你所分析的任何事物。你还可以利用这些知识攻击黑暗网络(犯罪、恐怖主义等)或保护人、地方和基础设施。这不仅仅是洞察力,这些是可操作的洞察力——最好的那种。

这就是本书的要点。将 NLP 与社交网络分析和数据科学相结合,对于获得新的视角来说极为强大。如果你能抓取或获得所需的数据,你将真正深入了解事物之间的关系及其原因。

这就是为什么本章旨在非常简单地解释什么是 NLP,如何使用它,以及它能做什么。但在此之前,让我们稍微了解一下 NLP 的历史,因为这通常被 NLP 书籍所忽略。

一段简短的 NLP 历史

如果你研究 NLP 的历史,你不会找到一个关于其起源的确凿答案。在我为本章制定大纲时,我意识到自己对 NLP 的应用和实现了解颇多,但对其历史和起源却有盲点。我知道它与计算语言学息息相关,但我也不了解计算语言学的历史。关于机器翻译MT)的最早概念据说出现在十七世纪;然而,我对这个说法持深深怀疑态度,因为我敢打赌,人类在语言诞生的同时,就已经在为词汇和字符之间的关系困惑不解。我认为这是不可避免的,因为几千年前的人并非愚笨。他们和我们一样聪明、好奇,甚至可能更为聪明。然而,让我给出一些我挖掘到的关于 NLP 起源的有趣信息。请理解,这并不是完整的历史。关于 NLP 的起源和历史可以写成一本书。所以,为了快速推进,我将简要地列出我发现的一些亮点。如果你想了解更多,这个话题是一个值得深入研究的领域。

有件事让我困惑,那就是我很少看到密码学(密码学和密码分析)被提到作为 NLP 或甚至 MT 的起源之一,然而密码学本身就是将信息转换为乱码,而密码分析则是将密文恢复为有用的信息。因此,对我来说,任何能够辅助进行密码学或密码分析的自动化技术,哪怕是几百年前或几千年前的,也应该是讨论的一部分。虽然它可能不像现代的翻译那样是机器翻译(MT),但它仍然是一种翻译的形式。所以,我认为机器翻译甚至可以追溯到由尤利乌斯·凯撒发明的凯撒密码,甚至更早。凯撒密码通过将文本按一定的数字偏移来将信息转化为代码。举个例子,我们来看这个句子:

我真的很 喜欢 NLP。

首先,我们应该去掉空格和大小写,以免任何窃听者能够猜测出单词的边界。现在字符串如下:

ireallylovenlp

如果我们进行shift-1,每个字母都向右偏移一个字符,那么我们就得到:

jsfbmmzmpwfomq

我们移动的数字是任意的。我们也可以使用反向移动。木棍曾被用来将文本转换为密码,因此我认为它也可以作为一种翻译工具。

在凯撒密码之后,还发明了许多其他复杂的加密技术。一部名为《密码书》的杰出作品,由西蒙·辛格(Simon Singh)撰写,深入探讨了几千年来的密码学历史。话虽如此,我们接着讨论人们通常认为与自然语言处理(NLP)和机器翻译(MT)相关的内容。

在十七世纪,哲学家们开始提交关于如何在语言之间建立联系的编码提案。这些提案完全是理论性的,且没有被用于实际机器的开发,但像机器翻译这样的思想最初是通过考虑未来的可能性提出的,随后才考虑其实现。几百年后,在 20 世纪初,瑞士语言学教授费迪南·德·索绪尔(Ferdinand de Saussure)提出了一种将语言作为一个系统来描述的方法。他在 20 世纪初去世,几乎让“语言作为科学”的概念失传,但意识到他的思想重要性后,他的两位同事于 1916 年撰写了《普通语言学教程》(Cours de linguistique generale)。这本书为结构主义方法奠定了基础,该方法起初应用于语言学,后来扩展到了包括计算机在内的其他领域。

最后,在 1930 年代,第一批机器翻译的专利开始申请。

后来,第二次世界大战爆发,这让我开始考虑凯撒密码和密码学作为早期的机器翻译形式。在二战期间,德国使用一种名为恩尼格玛的机器来加密德语信息。该技术的复杂性使得这些密码几乎无法破解,造成了极其严重的后果。1939 年,艾伦·图灵与其他英国密码分析师一道,设计了“炸弹机”(bombe),该机灵感来源于波兰的 bomba,后者在七年前曾用于破解恩尼格玛信息。最终,炸弹机能够破解德国语言密码,剥夺了德国潜艇利用密码保护的秘密优势,拯救了许多生命。这个故事本身非常引人入胜,我鼓励读者了解更多关于破解恩尼格玛密码的努力。

战后,机器翻译(MT)和自然语言处理(NLP)的研究真正起飞。1950 年,艾伦·图灵发布了《计算机与智能》,提出了图灵测试作为评估智能的方式。至今,图灵测试常被提及作为衡量人工智能AI)智能水平的标准。

1954 年,乔治敦实验将超过 60 个俄语句子自动翻译成英语。1957 年,诺姆·乔姆斯基的《句法结构》通过基于规则的句法结构系统革新了语言学,被称为普遍语法UG)。

为了评估机器翻译(MT)和 NLP 研究的进展,美国国家研究委员会NRC)在 1964 年创建了自动语言处理咨询委员会ALPAC)。与此同时,在麻省理工学院,Joseph Weizenbaum 创造了世界上第一个聊天机器人ELIZA。基于反射技巧和简单的语法规则,ELIZA 能够将任何句子重述为另一句作为对用户的回应。

然后冬天来临了。1966 年,由于 ALPAC 报告的影响,NLP 研究遭遇了停滞,NLP 和机器翻译(MT)的资金被撤销。因此,AI 和 NLP 研究在许多人眼中变成了死胡同,但并非所有人都这么认为。这一停滞期持续到 1980 年代末期,当时一场新的 NLP 革命开始了,推动力来自计算能力的稳步增长和转向机器学习ML)算法,而非硬编码规则。

1990 年代,统计模型在 NLP 领域的流行开始崛起。随后,在 1997 年,长短期记忆网络LSTM)和递归神经网络RNN)模型被引入,它们在 2007 年找到了用于语音和文本处理的应用场景。2001 年,Yoshua Bengio 及其团队提出了第一个前馈神经语言模型。2011 年,苹果的 Siri 成为全球第一个被普通消费者广泛使用的成功 AI 和 NLP 助手之一。

自 2011 年以来,NLP 的研究与发展爆炸性增长,所以我在这里讲到的历史仅到此为止。我相信 NLP 和 MT 的历史中还有很多空白,因此我鼓励你自己做一些研究,深入挖掘那些令你着迷的部分。我大部分职业生涯都在从事网络安全工作,所以我对几乎任何与密码学历史相关的事物都感兴趣,特别是古老的密码学技术。

NLP 如何帮助了我?

我想做的不仅仅是教你如何做某件事,我还想展示它如何帮助你。最简单的解释这种方法如何对你有用的方式,就是解释它如何对我有用。有几件事让我对自然语言处理(NLP)感到非常吸引。

简单文本分析

我非常喜欢阅读,从小就热爱文学,所以当我第一次了解到 NLP 技术可以用于文本分析时,我立刻被吸引住了。即便是像统计书中某个特定单词出现次数这么简单的事,也能引发兴趣并激发好奇心。例如,夏娃,圣经中的第一位女性,在创世纪中提到过多少次?她在整个圣经中被提到多少次?亚当在创世纪中被提到多少次?亚当在整个圣经中被提到多少次?这个例子中,我使用的是《钦定版圣经》。

让我们做个对比:

名称 创世纪计数 圣经计数
夏娃 2 4
亚当 17 29

图 1.1 – 亚当与夏娃在圣经中的提及表

这些结果很有趣。即使我们不将《创世纪》故事视为字面上的真相,它仍然是一个有趣的故事,我们常常听到亚当和夏娃的故事。因此,容易假设他们会被同样频繁地提到,但在《创世纪》中,亚当的出现频率是夏娃的八倍多,而在整本圣经中,亚当的出现频率是夏娃的七倍多。理解文学的一部分是建立一个关于文本内容的心智图。对我来说,夏娃出现得如此少有点奇怪,这让我想要调查男性与女性的提及频率,或者研究哪几本圣经书籍中女性角色最多,以及这些故事的内容。如果没有其他的话,这至少激发了我的好奇心,应该会促使我进行更深入的分析和理解。

自然语言处理(NLP)为我提供了从原始文本中提取可量化数据的工具。它使我能够在研究中使用这些可量化的数据,而这些研究在没有这些工具的情况下是不可能完成的。试想一下,如果要手动完成这个过程,逐页阅读每一页而不遗漏任何细节,来得到这些小的计数,可能需要多长时间。而现在,考虑到代码编写完成后,这只花费了我大约一秒钟。这是非常强大的,我可以利用这个功能在任何文本中研究任何人。

社区情感分析

其次,和我刚才提到的观点相关,NLP 提供了调查和分析群体情感与主题的方法。在 Covid-19 大流行期间,一些人群公开表示反对佩戴口罩,传播恐惧和误信息。如果我从这些人群中抓取文本,我可以利用情感分析技术来确定并衡量这些人群在不同话题上的情感共识。我就做了这一点。我能够抓取成千上万条推文,并了解他们对不同话题的真实感受,比如 Covid-19、国旗、第二修正案、外国人、科学等。我为我的一个项目 #100daysofnlp 在 LinkedIn 上做了这个分析 (www.linkedin.com/feed/hashtag/100daysofnlp/),结果非常有启发性。

自然语言处理(NLP)让我们能够客观地调查和分析人群对任何事物的情感,只要我们能够获取文本或音频。许多推特数据是公开发布的,公众可以自由使用。唯一的警告是:如果你打算进行数据抓取,请将你的能力用于正当用途,而非恶意用途。利用这些数据来了解人们在思考什么、感受什么。将其用于研究,而不是监视。

解答以前无法回答的问题

事实上,将这两者联系在一起的是,NLP 帮助我回答那些以前无法回答的问题。在过去,我们可以讨论人们的感受以及为什么这样,或描述我们阅读过的文学作品,但只能停留在表面层次。通过本书中我要向你展示的内容,你将不再局限于表面。你将能够快速绘制出几千年前发生的复杂关系,并能够深入分析任何关系,甚至是关系的演变。你将能够将这些相同的技术应用于任何类型的文本,包括转录音频、书籍、新闻文章和社交媒体帖子。自然语言处理打开了一个未被开发的知识宇宙。

安全与保障

2020 年,Covid-19 疫情席卷全球。当疫情爆发时,我担心许多人会失去工作和住所,我害怕世界会失控,陷入彻底的无政府状态。虽然情况变得严峻,但我们并没有看到武装帮派在全球各地袭击城镇和社区。紧张局势加剧,但我希望找到一种方法,可以实时监控我所在地区的暴力事件。因此,我从我所在地区的多个警察账户上抓取了推文,因为他们几乎实时报告各种犯罪事件,包括暴力。我创建了一个包含暴力与非暴力推文的数据集,其中暴力推文包含诸如枪击、刺伤等暴力相关的词汇。然后,我训练了一个机器学习分类器,用来检测与暴力相关的推文。通过这个分类器的结果,我可以随时了解我所在地区的暴力情况。我可以关注任何我想关注的内容,只要我能获取文本,但了解我所在地区的街头暴力状况可以为我提供警示或安慰。再次强调,将原本仅限于感觉和情感的内容转化为可量化的数据是非常强大的。

自然语言处理的常见用途

我最喜欢自然语言处理(NLP)的一点是,你的主要限制就是你的想象力和你能用它做的事情。如果你是一个富有创造力的人,你将能够提出许多我没有解释的想法。

我将解释一些我发现的自然语言处理常见用途。虽然其中一些可能通常不会出现在 NLP 书籍中,但作为一个终身程序员,每当我想到 NLP 时,我自然会想到任何与字符串相关的编程工作,而字符串就是字符的序列。例如,ABCDEFG 是一个字符串A 是一个字符

注意事项

请现在不要担心编写代码,除非你只是想用一些自己的数据进行实验。本章中的代码仅用于展示可能实现的功能以及代码可能的样子。我们将在本书的后续章节深入探讨实际代码。

真/假 – 存在/不存在

这可能不完全符合 NLP 的严格定义,但它经常出现在任何文本操作中,这在 NLP 中的机器学习中也会发生,那里使用了独热编码(one-hot encoding)。在这里,我们严格关注某个事物的存在与否。例如,如我们在本章之前看到的,我想计算亚当和夏娃在《圣经》中出现的次数。我也可以写一些简单的代码,来确定亚当和夏娃是否出现在《圣经》中,或者是否出现在《出埃及记》中。

对于这个例子,让我们使用我已经设置好的这个 DataFrame:

图 1.2 – 包含《圣经》全书金句版文本的 pandas DataFrame

图 1.2 – 包含《圣经》全书的金句版文本的 pandas DataFrame

我特别想查看夏娃是否作为df['entities']中的一个实体存在。我希望将数据保存在 DataFrame 中,因为我会用到它,所以我会在entities字段上进行一些模式匹配:

check_df['entities'].str.contains('^Eve$')
0        False
1        False
1        False
2        False
3        False
         ...
31101    False
31101    False
31102    False
31102    False
31102    False
Name: entities, Length: 51702, dtype: bool

在这里,我使用了所谓的^符号,表示E在夏娃中的位置位于字符串的最开始,$表示e在字符串的末尾。这确保了存在一个名为 Eve 的实体,且前后没有其他字符。使用正则表达式,你可以获得比这更大的灵活性,但这是一个简单的例子。

在 Python 中,如果你有一个TrueFalse值的系列,.min()会给你False.max()会给你True,这也有道理,因为从另一个角度来看,TrueFalse分别是10,而1大于0。虽然有其他方法可以做到这一点,但我打算用这种方式。所以,为了查看《圣经》是否至少提到一次夏娃,我可以做如下操作:

check_df['entities'].str.contains('^Eve$').max()
True

如果我想查看亚当是否在《圣经》中,可以将Eve替换为Adam

check_df['entities'].str.contains('^Adam$').max()
True

检测文本中某个事物的存在或不存在是很有用的。例如,如果我们想快速获取一份关于夏娃的圣经经文列表,可以这样做:

check_df[check_df['entities'].str.contains('^Eve$')]

这将给我们一个包含提及夏娃的圣经经文的 DataFrame:

图 1.3 – 包含严格提及夏娃的圣经经文

图 1.3 – 包含严格提及夏娃的圣经经文

如果我们想要得到关于诺亚的经文列表,可以这样做:

check_df[check_df['entities'].str.contains('^Noah$')].head(10)

这将给我们一个包含提及诺亚的圣经经文的 DataFrame:

图 1.4 – 包含严格提及诺亚的圣经经文

图 1.4 – 包含严格提及诺亚的圣经经文

我已经添加了.head(10),只查看前十行。对于文本,我经常发现自己想要查看更多内容,而不是默认的五行。

如果我们不想使用entities字段,我们也可以改为在文本中查找。

df[df['text'].str.contains('Eve')]

这将给我们一个包含提到夏娃的圣经经文的 DataFrame。

图 1.5 – 包含提及夏娃的圣经经文

图 1.5 – 包含提及夏娃的圣经经文

这就是问题变得有些复杂的地方。我已经做了一些繁重的工作,提取了该数据集中的实体,稍后我会展示如何做到这一点。当你处理原始文本时,正则表达式(regex)和模式匹配可能会很麻烦,正如前面图示所示。我只想要包含“Eve”的诗句,但结果却匹配了evenevery等词。这不是我想要的。

任何处理文本数据的人都会想要学习正则表达式的基础知识。不过,别担心。我已经使用正则表达式超过二十年了,但我仍然经常需要在谷歌上搜索来确保正则表达式正确工作。我会再次讲解正则表达式,但我希望你能明白,判断一个词是否存在于字符串中其实是相当简单的。举个更实际的例子,如果你有 40 万个抓取的推文,而你只对那些关于特定主题的推文感兴趣,你可以轻松地使用前面提到的技巧或正则表达式来查找精确匹配或相近匹配。

正则表达式(regex)

我在前一部分简要解释了正则表达式,但它的用途远不止于此,远比仅仅用来判断某些东西的存在或不存在。例如,你还可以使用正则表达式从文本中提取数据,以丰富你的数据集。我们来看一个我抓取的数据科学动态:

图 1.6 – 抓取的数据科学 Twitter 动态

图 1.6 – 抓取的数据科学 Twitter 动态

这些文本字段中包含很多有价值的信息,但以当前形式处理起来很困难。如果我只想要每天发布的链接列表怎么办?如果我想看到数据科学社区使用的标签怎么办?如果我想把这些推文拿去构建一个社交网络来分析谁在互动呢?我们应该做的第一件事是通过提取我们需要的内容来丰富数据集。所以,如果我想创建包含标签、提及和网址列表的三个新字段,我可以这样做:

df['text'] = df['text'].str.replace('@', ' @')
df['text'] = df['text'].str.replace('#', ' #')
df['text'] = df['text'].str.replace('http', ' http')
df['users'] = df['text'].apply(lambda tweet: [token for token in tweet.split() if token.startswith('@')])
df['tags'] = df['text'].apply(lambda tweet: [token for token in tweet.split() if token.startswith('#')])
df['urls'] = df['text'].apply(lambda tweet: [token for token in tweet.split() if token.startswith('http')])

在前面三行中,我在每个提及、标签和网址后面加了一个空格,以便为分割提供一些空间。在接下来的三行中,我通过空格分割每条推文,然后应用规则来识别提及、标签和网址。在这种情况下,我没有使用复杂的逻辑。提及以@开头,标签以#开头,网址以 HTTP(包括 HTTPS)开头。这个代码的结果是,我最终得到了三列额外的数据,分别包含用户、标签和网址的列表。

如果我对用户、标签和网址使用explode(),我将得到一个每个用户、标签和网址都有自己一行的 DataFrame。以下是explode()后的 DataFrame 样式:

图 1.7 – 抓取的数据科学 Twitter 动态,已增加用户、标签和网址

图 1.7 – 抓取的数据科学 Twitter 动态,已增加用户、标签和网址

然后,我可以使用这些新列来获取使用过的独特标签列表:

sorted(df['tags'].dropna().str.lower().unique())
['#',
 '#,',
 '#1',
 '#1.',
 '#10',
 '#10...',
 '#100daysofcode',
 '#100daysofcodechallenge',
 '#100daysofcoding',
 '#15minutecity',
 '#16ways16days',
 '#1bestseller',
 '#1m_ai_talents_in_10_years!',
 '#1m_ai_talents_in_10_yrs!',
 '#1m_ai_talents_in_10yrs',
 '#1maitalentsin10years',
 '#1millionaitalentsin10yrs',
 '#1newrelease',
 '#2'

很明显,我在数据丰富过程中使用的正则表达式并不完美,因为标点符号不应该包含在标签中。这是需要修正的地方。请注意,处理人类语言是非常混乱的,很难做到完美。我们只需要坚持不懈,才能准确地得到我们想要的结果。

让我们看看独特的提及是怎样的。所谓独特的提及,指的是推文中去重后的单独账户:

sorted(df['users'].dropna().str.lower().unique())
['@',
 '@027_7',
 '@0dust_himanshu',
 '@0x72657562656e',
 '@16yashpatel',
 '@18f',
 '@1ethanhansen',
 '@1littlecoder',
 '@1njection',
 '@1wojciechnowak',
 '@20,',
 '@26th_february_2021',
 '@29mukesh89',
 '@2net_software',

这看起来好多了,不过@符号不应该单独出现,第四个看起来有些可疑,其中一些看起来像是被错误地作为提及,而本应作为标签使用。这可能是推文文本的问题,而不是正则表达式的问题,但值得进一步调查。

我喜欢将提及和标签转换为小写字母,这样更容易找到独特的标签。这通常作为预处理 用于自然语言处理(NLP)

最后,让我们获取提到的独特网址列表(这些可以进一步用于抓取):

sorted(df['urls'].dropna().unique())
['http://t.co/DplZsLjTr4',
 'http://t.co/fYzSPkY7Qk',
 'http://t.co/uDclS4EI98',
 'https://t.co/01IIAL6hut',
 'https://t.co/01OwdBe4ym',
 'https://t.co/01wDUOpeaH',
 'https://t.co/026c3qjvcD',
 'https://t.co/02HxdLHPSB',
 'https://t.co/02egVns8MC',
 'https://t.co/02tIoF63HK',
 'https://t.co/030eotd619',
 'https://t.co/033LGtQMfF',
 'https://t.co/034W5ItqdM',
 'https://t.co/037UMOuInk',
 'https://t.co/039nG0jyZr'

这看起来很干净。我能够提取多少个网址?

len(sorted(df['urls'].dropna().unique()))
19790

这有很多链接。由于这是 Twitter 数据,很多网址通常是照片、自拍、YouTube 链接和其他一些对研究者可能不太有用的内容,但这是我的抓取数据科学信息流,它从数十个与数据科学相关的账户中获取信息,所以这些网址很可能包含一些令人兴奋的新闻和研究成果。

正则表达式(Regex)允许你从数据中提取额外的数据,并使用它来丰富数据集,以便进行更简单或进一步的分析。如果你提取了网址,可以将其作为额外抓取的输入。

我不会对正则表达式做一个长篇的讲解。关于这个话题有很多专门的书籍。最终,你很可能需要学习如何使用正则表达式。对于本书中的内容,前面提到的正则表达式大概是你所需要的全部,因为我们使用这些工具来构建可以分析的社交网络。本书的核心并不是关于 NLP 的,我们只是在创建或丰富数据时使用一些 NLP 技术,其他部分则主要依赖网络分析。

词频统计

词频统计也很有用,特别是当我们想要进行对比时。例如,我们已经比较了《圣经》中亚当和夏娃被提及的次数,但如果我们想要看到所有实体在《圣经》中被提及的次数该怎么办呢?我们可以用简单的方法来做,也可以用 NLP 方法来做。我个人偏好尽可能使用简单的方法,但很多时候,NLP 或图形方法反而变成了更简单的方式,所以要学会所有的方法,再根据情况做选择。

我们将用简单的方法来做,计算实体被提及的次数。我们再次使用数据集,并进行一些聚合操作,看看在《圣经》中最常被提及的人物是谁。记住,我们可以对任何我们抓取的内容执行这个操作,只要我们已将数据集丰富到包含提及列表。但在这个示范中,我将使用《圣经》。

在第三行,我保留了名字长度超过两个字符的实体,有效地去除了那些最终出现在数据中的无关实体。我使用的是这个过滤器:

check_df = df.explode('entities')
check_df.dropna(inplace=True) # dropping nulls
check_df = check_df[check_df['entities'].apply(len) > 2] # dropping some trash that snuck in
check_df['entities'] = check_df['entities'].str.lower()
agg_df = check_df[['entities', 'text']].groupby('entities').count()
agg_df.columns = ['count']
agg_df.sort_values('count', ascending=False, inplace=True)
agg_df.head(10)

这将在下面的 DataFrame 中显示:

图 1.8 – 整个圣经中实体计数

图 1.8 – 整个圣经中实体计数

这看起来很不错。实体是指人、地点和事物,唯一不同的地方是词汇thou。之所以会出现它,是因为在圣经中,thou这个词通常会被大写为Thou,而在进行实体识别和提取时,它会被标记为NNP专有名词)。然而,thou指的是You,因此也能理解。例如,Thou shalt not kill, thou shalt not steal

如果我们有这样的数据,我们也可以非常容易地进行可视化,帮助我们从不同的角度理解:

agg_df.head(10).plot.barh(figsize=(12, 6), title='Entity Counts by Bible Mentions', legend=False).invert_yaxis()

这将给我们一个水平条形图,显示实体计数:

图 1.9 – 整个圣经中实体计数的可视化

图 1.9 – 整个圣经中实体计数的可视化

显然,这不仅限于圣经中的应用。如果你有任何你感兴趣的文本,你可以使用这些技术来构建更深的理解。如果你想将这些技术用于艺术创作,你可以。如果你想使用这些技术来帮助打击犯罪,你也可以。

情感分析

这是我在所有 NLP 技术中最喜欢的一项。我想知道人们在谈论什么,他们对这些事物的感受如何。这是 NLP 中一个经常被低估的领域,如果你关注大多数人如何使用它,你会看到很多关于如何构建分类器的展示,这些分类器可以确定积极或消极的情感。然而,我们人类很复杂。我们不仅仅是开心或悲伤。有时,我们是中立的。有时,我们大部分是中立的,但更多的是积极而非消极。我们的感情是微妙的。我用过的一本书为我自己的情感分析教育和研究提供了很多帮助,书中提到了一项研究,讲述了人类情感被划分为主要、次要和三级情感(Liu, Sentiment Analysis, 2015,第 35 页)。这里有几个例子:

主要情感 次要情感 三级情感
愤怒 反感 蔑视
愤怒 嫉妒 妒忌
恐惧 惊悚 警觉
恐惧 紧张 焦虑
喜爱 崇拜
欲望 渴望

图 1.10 – 主要、次要和三级情感的表格

有一些主要情感,更多的是次要情感,而更多的是三级情感。情感分析可以用来尝试分类任何情感的“是”或“否”,只要你有训练数据。

情感分析并不仅仅用于检测情感。这些技术也可以用于分类,因此我觉得情感分析这个术语并不完全准确,也许这就是为什么有这么多人只是简单地从 Yelp 和 Amazon 评论中检测积极/消极情感的原因。

我有一些更有趣的情感分类应用。目前,我使用这些技术来检测有毒言论(真正的辱骂性语言)、积极情感、消极情感、暴力、好消息、坏消息、问题、虚假信息研究和网络科学研究。你可以将其视为一种智能模式匹配,它能够学习关于某个话题的文本通常是如何被写作的。例如,如果我们想捕捉关于虚假信息的推文,我们可以训练一个关于虚假信息、误导信息和假新闻的文本模型。在训练过程中,模型会学习到其他相关术语,能够比任何人都更快速、更精准地捕捉到它们。

情感分析和文本分类建议

在我进入下一部分之前,这里有一些建议:对于情感分析和文本分类,在很多情况下,你不需要神经网络来处理这么简单的任务。如果你正在构建一个检测仇恨言论的分类器,使用“词袋”方法进行预处理,再加上一个简单的模型进行分类就足够了。总是从简单的开始。如果你努力训练,神经网络可能会提高几个百分点的准确率,但它需要更多时间,且可解释性较差。一个 linearsvc 模型可以在瞬间训练完成,并且通常能达到同样的效果,有时甚至更好,你也应该尝试一些其他简单的模型和技术。

另一个建议是:试验停用词去除,但不要仅仅因为别人告诉你要去除停用词就去除它们。有时候它有帮助,有时候却会对模型产生不利影响。大多数时候,它可能有帮助,但这足够简单,可以进行实验。

此外,在构建数据集时,如果你对句子进行情感分析,而不是对大块文本进行分析,通常能得到最佳效果。假设我们有以下文本:

今天我早早醒来,喝了些咖啡,然后出去查看花朵。天空湛蓝,是一个温暖的六月早晨。然而,当我回到屋子里时,我发现水管漏水,整个厨房被淹了。接下来的一整天都很糟糕。我现在真是太生气了。

你认为第一句话的情感和最后一句话的情感是完全一样的吗?这个虚构故事的情感变化很大,从一开始的愉快和积极到最后的灾难和愤怒。如果你在句子层面进行分类,你能做到更精确。然而,即便如此,这也并不完美。

今天一开始一切都很完美,但最后一切都崩溃了,我现在真是太生气了。

那个句子的情感是什么?是积极的还是消极的?它其实是两者都有。因此,理想情况下,如果你能够捕捉到一个句子同时表现出多种情感,那将会非常强大。

最后,在构建模型时,你总是可以选择是否构建二分类或多分类的语言模型。根据我自己的使用经验以及一些与我产生共鸣的研究,通常最简单的是构建一些小模型,只需要检查某些内容是否存在。因此,与其构建一个神经网络来判断文本是积极、消极还是中立的,不如构建一个模型检查“积极”与其他情感的对比,另一个模型检查“消极”与其他情感的对比。

这看起来可能会更费力,但我发现这做起来要快得多,可以使用非常简单的模型,而且这些模型可以组合在一起,寻找各种不同的信息。例如,如果我想分类政治极端主义,我可以使用三个模型:有毒语言、政治和暴力。如果一段文本被分类为有毒语言,属于政治话题,并且倡导暴力,那么这篇内容的发布者可能表现出一些危险的特征。如果只显示有毒语言和政治情感,那很常见,通常不具备极端政治或危险性。政治讨论往往带有敌意。

信息提取

我们已经在之前的示例中做了一些信息提取,所以我会简要说明。在前一部分中,我们提取了用户提及、话题标签和网址。这些操作是为了丰富数据集,使进一步的分析变得更加容易。我将这些提取步骤直接加到我的抓取程序中,这样我在下载新数据时就能立即获得用户、提及和网址的列表。这让我可以立即开始网络分析或调查最新的 URL。基本上,如果你正在寻找某些信息,并且你找到了一种可以反复从文本中提取信息的方法,而你发现自己在不同数据集上重复做这些步骤,那么你应该考虑将这些功能加入到你的抓取程序中。

最丰富的、提升我 Twitter 数据集的数据来自两个字段:发布者用户发布者是发布推文的账号。用户是发布者提到的账号。我的每个信息流中都有几十个发布者。有了发布者和用户,我可以从原始文本中构建社交网络,本书中将对此进行详细解释。这是我发现的一项最有用的技巧,你也可以利用这些结果去寻找其他有趣的账号进行抓取。

社区检测

社区检测通常不会在自然语言处理(NLP)中提及,但我认为它应该被提到,尤其是在使用社交媒体文本时。例如,如果我们知道某些标签(hashtag)被特定群体使用,我们可以通过他们使用的标签检测到其他可能与这些群体有联系或支持这些群体的人。利用这一点进行人群研究非常简单。只需抓取一堆数据,查看他们使用的标签,然后搜索这些标签。提及也可以给你提供其他账户的信息,供你进一步抓取。

社区检测通常在社交网络分析中提到,但它也可以非常容易地通过 NLP 实现,我也曾使用话题建模和上述方法来进行社区检测。

聚类

聚类是无监督学习中常见的一种技术,也常用于网络分析。在聚类中,我们是在寻找与其他事物相似的事物。做这项工作的方式有很多种,甚至 NLP 的话题建模也可以作为一种聚类方式。在无监督机器学习中,你可以使用像 k-means 这样的算法,找到与其他推文、句子或书籍相似的推文、句子或书籍。你也可以使用话题建模,利用 TruncatedSVD 来做类似的事情。或者,如果你有一个实际的社交图(社交网络图),你可以查看连接的组件,看看哪些节点是连接的,或者应用 k-means 算法来分析某些网络度量(我们稍后会深入讨论),看看哪些节点具有相似的特征。

NLP 的高级应用

你日常进行的大部分 NLP 工作可能都会属于较为简单的应用,但我们也来讨论一些高级应用。在某些情况下,我所描述的高级应用实际上是将多个简单应用结合起来,提供更复杂的解决方案。所以,让我们讨论一些更高级的应用,比如聊天机器人和对话代理、语言建模、信息检索、文本摘要、话题发现和建模、文本转语音和语音转文本、机器翻译(MT)以及个人助手。

聊天机器人和对话代理

聊天机器人是能够与用户进行对话的程序。这类程序已经存在多年,最早的聊天机器人出现在 20 世纪 60 年代,但它们一直在不断改进,现在已成为一种有效的工具,用于将用户引导到更具体的客户支持形式。例如,如果你进入一个网站的支持部分,可能会看到一个小的聊天框弹出,里面写着类似“今天我们能为您提供什么帮助?”的话。你可能会输入“我想偿还我的信用卡余额。”当应用程序收到你的答案时,它就能用这个信息来判断你需要哪种支持形式。

虽然聊天机器人是为处理人类文本而构建的,但对话代理可以处理语音音频。Siri 和 Alexa 就是对话代理的例子。你可以与它们对话并提问。

然而,聊天机器人和对话代理不仅限于文本;当我们用电话拨打公司电话时,也经常遇到类似的电话交换机。我们会接到一系列相同的问题,可能需要回答一个单词或输入数字。因此,在后台,如果涉及语音,就会有一个语音转文本的转换元素。同时,应用程序还需要确定用户是在提问还是在陈述,因此很可能还会涉及文本分类。

最后,为了提供答案,文本摘要可以将搜索结果转化为简洁的陈述,以文本或语音的形式返回给用户,完成交互。

然而,聊天机器人不仅仅是简单的问答系统。我认为它们将成为我们与文本互动的有效方式。例如,你可以围绕《爱丽丝梦游仙境》这本书(或者圣经)构建一个聊天机器人,以回答有关这本书的具体问题。你还可以根据自己的私人信息构建一个聊天机器人,与自己对话。在这里,有很多创造空间。

语言建模

语言建模关注的是在给定一系列单词的情况下,预测下一个单词。例如,接下来会是什么:“The cow jumped over the ______.” 或者:“Green eggs and _____.”如果你去 Google 并开始在搜索栏中输入,你会注意到下一个预测的单词会显示在下拉列表中,以加速你的搜索。

我们可以看到,Google 预测下一个单词是ham,但它也在查找与已输入文本相关的查询。这看起来像是语言建模与聚类或主题建模的结合。它们在你输入之前就预测了下一个单词,甚至还进一步寻找与你已输入文本相关的其他查询。

数据科学家还可以将语言建模作为生成模型创建的一部分。在 2020 年,我用数千行圣诞歌曲的歌词训练了一个模型,并训练它写圣诞诗。结果虽然粗糙且幽默,因为我只花了几天时间,但它能够以种子文本为基础,利用这些文本生成整首诗。例如,种子文本可以是“铃儿响叮当”,然后模型会不断根据之前的文本生成诗句,直到达到单词和行数的限制。以下是我最喜欢的那首:

youre the angel
and the manger is the king
of kings and sing the sea
of door to see to be
the christmas night is the world
and i know i dont want
to see the world and much
see the world is the sky
of the day of the world
and the christmas is born and
the world is born and the
world is born to be the
christmas night is the world is
born and the world is born
to you to you to you
to you to you to a
little child is born to be
the world and the christmas tree
is born in the manger and
everybody sing fa
la la la la la la
la la la la la la
la la la la la la
la la la la la la
la la la la la la

我构建了一个生成过程,从训练数据中的任意一行随机选取一个单词作为开头。接着,它会生成由 6 个单词组成的句子,直到完成 25 行。我只训练了 24 小时,因为我希望能在圣诞节前迅速完成这项工作。有几本关于创建生成模型的书籍,如果你想利用人工智能来增强你的创造力,我强烈建议你了解一下它们。这更像是与模型的合作,而不是用模型来取代我们自己。

现如今,生成式文本模型变得相当令人印象深刻。ChatGPT——2022 年 11 月发布——凭借其回答大多数问题并提供看似现实的答案的能力,吸引了众多人的关注。这些答案并不总是正确的,因此生成模型仍有很长的路要走,但如今关于生成模型的讨论热度很高,人们也在考虑如何将它们应用到自己的工作和生活中,以及它们对我们未来的意义。

文本摘要

文本摘要几乎不言自明。其目标是将文本作为输入,返回一个摘要作为输出。当你需要管理成千上万或数百万篇文档,并希望提供关于每篇文档的简明概述时,这项技术非常有力。它本质上类似于你在学术文章中找到的“摘要”部分。许多细节会被去除,最终只保留核心内容。

然而,这并不是一种完美的艺术,因此请注意,如果使用此方法,算法可能会舍弃文本中的重要概念,而保留那些较不重要的部分。机器学习并不完美,因此请时刻关注结果。

然而,这个方法更适用于返回简短摘要,而非搜索。你可以使用主题建模和分类来判断文档的相似性,再利用这些信息来总结最终的文档集。

如果你将整本书的内容输入到文本摘要算法中,我希望它能够捕捉到自然语言处理(NLP)与网络分析的结合是强大且人人可及的。你不需要成为天才才能使用机器学习、自然语言处理或社交网络分析。我希望这本书能激发你的创造力,使你在解决问题和批判性思考方面更加高效。文中有许多重要的细节,但这就是其本质。

主题发现与建模

主题发现与建模非常类似于聚类。这在潜在语义索引LSI)中得到应用,它对于识别文本中存在的主题(topic)非常有效,并且还可以作为文本分类的一个有效预处理步骤,使得模型能够根据上下文而非单纯的词语进行训练。此前我在聚类社区检测小节中提到过,这一方法如何根据用户在其账户描述中使用的词汇和标签来识别社区内的细微群体。

比如,主题建模会在主题中找到相似的字符串。如果你对政治新闻和社交媒体帖子进行主题建模,你会注意到,在这些主题中,类似的事物往往会聚集在一起。词语会和其他相似的词汇出现在一起。例如,2A 可能会写作 第二修正案USA 可能会写作其扩展形式(美利坚合众国)等等。

语音合成与语音识别转换

这种类型的自然语言处理(NLP)模型旨在将文本转换为语音音频,或将语音转换为文本记录。然后,这些文本可以作为输入用于分类或对话代理(聊天机器人、个人助理)。

我的意思是,你不能仅仅将音频输入到文本分类器中。此外,如果没有任何语言分析组件,仅凭音频捕捉上下文也是困难的,因为人们说话时会使用不同的方言、语气等等。

第一步通常是将音频转录为文本,然后分析文本本身。

机器翻译(MT)

从自然语言处理的历史来看,我认为可以安全地说,从语言 A 翻译到语言 B 可能在人类开始与使用不同语言的其他人互动时,就已经成为人类的心思。例如,圣经中甚至有关于巴别塔的故事,说在塔被摧毁时,我们失去了彼此理解对方语言的能力。机器翻译有着许多有用的应用,不仅在合作、保密性上,在创造力方面也有重要意义。

比如,对于合作来说,你需要能够共享知识,即使团队成员之间不共享相同的语言。事实上,这在任何需要共享知识的地方都非常有用,所以你经常会在社交媒体帖子和评论中看到 查看翻译 的链接。今天,机器翻译(MT)几乎已经完美了,尽管偶尔会有一些有趣的错误。

在安全领域,你需要知道敌人正在计划什么。如果你根本无法理解敌人正在说什么或打算做什么,那么间谍活动可能就毫无意义。翻译是一项专业技能,当涉及人工翻译时,这通常是一个漫长且手动的过程。机器翻译可以大大加快分析速度,因为另一种语言可以被迅速翻译成你自己的语言。

对于创造力来说,将文本从一种语言转化为自己创造的语言是多么有趣啊?这是完全可行的。

由于机器翻译和文本生成的重要性,庞大的神经网络已经被训练来处理文本生成和机器翻译。

个人助理

我们大多数人可能已经知道像 Alexa 和 Siri 这样的个人助理,它们已经成为我们生活中的重要组成部分。我猜我们将变得更加依赖这些助手,最终,我们会像在老电视节目 霹雳游侠 中一样与我们的汽车对话(该节目于 1982 年至 1986 年间播出)。“嘿,车子,带我去超市”可能会像“嘿 Alexa,明天的天气怎么样?”一样常见。

个人助手结合了前面提到的几种 NLP 技术。它们可能使用分类技术来判断你的查询是一个问题还是一个陈述。然后,它们可能会在互联网上搜索与你的问题最相关的网页内容。接着,它可以从一个或多个结果中提取原始文本,再使用摘要技术来构建简洁的答案。最后,它会将文本转化为语音,并将答案反馈给用户。

个人助手结合了前面提到的几种 NLP 技术:

  1. 它们可能使用分类技术来判断你的查询是一个问题还是一个陈述。

  2. 然后,它们可能会在互联网上搜索与你的问题最相关的网页内容。

  3. 它们可以从一个或多个结果中提取原始文本,再使用摘要技术来构建简洁的答案。

  4. 最后,它们会将文本转化为语音,并将答案反馈给用户。

我对个人助手的未来感到非常兴奋。我很想拥有一个可以与之对话的机器人和汽车。我认为,创造力可能是我们在创建不同类型的个人助手或它们所使用的模块时唯一的限制。

初学者如何入门自然语言处理(NLP)?

如果我们最终不深入探讨如何使用这些工具和技术,这本书将几乎没有什么用处。我在这里描述的常见和高级应用只是其中的一部分。当你对 NLP 感到熟悉时,我希望你不断考虑其他可能尚未得到满足的 NLP 应用。例如,仅仅在文本分类方面,你就可以深入探讨。你可以使用文本分类技术尝试分类更复杂的概念,比如讽刺或共情,但我们暂时不提前讨论这些。这是我希望你做的事。

从一个简单的想法开始

简单地思考,只有在需要时才增加复杂性。想想有什么事情是你感兴趣的,并且想要了解更多的,然后找到讨论这个话题的人。如果你对摄影感兴趣,找几个讨论摄影的 Twitter 账号。如果你想分析政治极端主义,找一些在 Twitter 上公开展示其统一标签的账号。如果你对花生过敏研究感兴趣,找一些研究人员的 Twitter 账号,他们发布自己的研究成果和文章,努力挽救生命。我之所以反复提到 Twitter,是因为它是一个研究人们如何讨论问题的宝贵资源,且人们常常会发布链接,这可能引导你进一步抓取更多内容。但你也可以使用任何社交媒体平台,只要它能被抓取。

然而,从一个非常简单的想法开始。你想了解一段文本(或大量的推文)中的什么内容?你想了解一个社区的人们的什么情况?头脑风暴一下。拿出一个笔记本,开始写下你脑海中浮现的每一个问题。给它们排个优先级。然后,你就会有一份问题清单,去寻找答案。

例如,我的研究问题可能是:“人们在说关于黑人的命也是命抗议的事情吗?”或者,我们也可以研究一些不那么严肃的话题,问:“人们在说关于最新的漫威电影的事吗?” 个人来说,我更倾向于至少尝试用数据科学为善,去让世界变得稍微安全一些,所以我对电影评论不太感兴趣,但别人可能会。每个人都有自己的偏好。研究你感兴趣的内容。

对于这个演示,我将使用我抓取的数据科学信息流。我有一些初步的问题:

  • 哪些账户每周发布最多?

  • 哪些账户被提及最多?

  • 这个群体的人们主要使用哪些标签?

  • 在回答完这些问题后,我们可以想到哪些后续问题?

我们将只使用自然语言处理(NLP)和简单的字符串操作来回答这些问题,因为我还没有开始讲解社交网络分析。我还假设你熟悉 Python 编程,并且熟悉 pandas 库。我将在后面的章节中更详细地讲解 pandas,但不会进行深入的培训。有一些很棒的书籍可以深入讲解 pandas。

这是我抓取的数据科学信息流的原始数据样式:

图 1.11 – 抓取并丰富的数据科学 Twitter 信息流

图 1.11 – 抓取并丰富的数据科学 Twitter 信息流

为了节省时间,我已经在抓取程序中设置了正则表达式步骤,用来创建用户、标签和 URL 的列。所有这些都是在自动化抓取的过程中被抓取或生成的。这将使得回答我提出的四个问题变得更加容易和迅速。那么,我们开始吧。

发布频率最高的账户

我首先想做的是看看哪些账户总共发布了最多的内容。我还会看看哪些账户发布最少,以检查是否有账户自从被加入到我的抓取程序中后已经停止更新。为此,我将简单地选取publisher(发布推文的账户)和tweet这两列,对publisher进行groupby操作,然后进行计数:

Check_df = df[['publisher', 'tweet']]
check_df = check_df.groupby('publisher').count()
check_df.sort_values('tweet', ascending=False, inplace=True)
check_df.columns = ['count']
check_df.head(10)

这将显示一个按推文数量排序的发布者数据框,向我们展示最活跃的发布者:

图 1.12 – 数据科学 Twitter 信息流中的用户推文数量

图 1.12 – 数据科学 Twitter 信息流中的用户推文数量

太棒了。如果你想进入数据科学领域,而且你使用 Twitter,那么你应该关注这些账户。

然而,对我来说,这些问题的实用性有限。我真正想看到的是每个账户的发布行为。为此,我将使用数据透视表。我将使用publisher作为索引,created_week作为列,并进行计数聚合。以下是按当前周排序的前十名:

Check_df = df[['publisher', 'created_week', 'tweet']].copy()
pvt_df = pd.pivot_table(check_df, index='publisher', columns='created_week', aggfunc='count').fillna(0)
pvt_df = pvt_df['tweet']
pvt_df.sort_values(202129, ascending=False, inplace=True)
keep_weeks = pvt_df.columns[-13:-1] # keep the last twelve weeks, but excluding current
pvt_df = pvt_df[keep_weeks]
pvt_df.head(10)

这将生成以下数据框:

图 1.13 – 按周的用户推文数量数据透视表

图 1.13 – 按周的用户推文数量数据透视表

这看起来更有用,而且对周次敏感。作为可视化,它也应该很有趣,可以感受一下规模:

_= pvt_df.plot.bar(figsize=(13,6), title='Twitter Data Science Accounts – Posts Per Week', legend=False)

我们得到了以下图表:

图 1.14 – 用户推文按周数的条形图

图 1.14 – 用户推文按周数的条形图

这种方式可视化时,看到单独的周次有点困难。对于任何可视化,你都需要考虑如何最容易地讲述你想要表达的故事。由于我主要对展示哪些账户的总推文数最多感兴趣,因此我将使用第一次聚合的结果。这看起来很有趣,也很酷,但并不特别实用:

_= check_df.plot.bar(figsize=(13,6), title='Twitter Data Science Accounts – Posts Per Week', legend=False)

这段代码给我们以下图表:

图 1.15 – 用户推文总数的条形图

图 1.15 – 用户推文总数的条形图

这更容易理解。

最常被提及的账户

现在,我想看看哪些账户被发布者(发推文的账户)提及得最频繁。这可以显示合作伙伴,也可以显示其他值得抓取的有趣账户。为此,我将使用value_counts来查看前 20 个账户。我想要一个快速的答案:

Check_df = df[['users']].copy().dropna()
check_df['users'] = check_df['users'].str.lower()
check_df.value_counts()[0:20]
users
@dataidols         623
@royalmail         475
@air_lab_muk       231
@makcocis          212
@carbon3it         181
@dictsmakerere     171
@lubomilaj         167
@brittanymsalas    164
@makererenews      158
@vij_scene         151
@nm_aist           145
@makerereu         135
@packtpub          135
@miic_ug           131
@arm               127
@pubpub            124
@deliprao          122
@ucberkeley        115
@mitpress          114
@roydanroy         112
dtype: int64

这看起来很棒。我敢打赌这些账户中有一些有趣的数据科学家。我应该去看看,并考虑抓取这些账户并将它们添加到我的数据科学信息流中。

前 10 个数据科学标签

接下来,我想看看哪些标签使用得最频繁。代码会非常相似,唯一不同的是,我需要对标签字段运行explode(),以便为每个推文的标签列表中的每个元素创建一行。我们先做这个。为此,我们可以简单地创建 DataFrame,去除空值,将标签小写化以保持一致,然后使用value_counts()来得到我们想要的结果:

Check_df = df[['tags']].copy().dropna()
check_df['tags'] = check_df['tags'].str.lower()
check_df.value_counts()[0:10]
tags
#datascience           2421
#dsc_article           1597
#machinelearning        929
#ai                     761
#wids2021               646
#python                 448
#dsfthegreatindoors     404
#covid19                395
#dsc_techtarget         340
#datsciafrica           308
dtype: int64

这看起来很棒。我打算可视化前十名结果。然而,value_counts() somehow 使得标签有些损坏,所以我改用了 DataFrame 的 groupby 操作:

图 1.16 – 数据科学 Twitter 信息流中的标签计数

图 1.16 – 数据科学 Twitter 信息流中的标签计数

让我们用一些相关的想法结束这一部分。

简单分析得出的额外问题或行动项

总的来说,如果我不在写书,这个分析大约需要我花费 10 分钟时间。代码看起来可能有些奇怪,因为你可以在 Python 中将命令链式连接。我更喜欢将重要操作单独放在一行,这样下一个需要管理我代码的人就不会漏掉任何附加在一行末尾的重要内容。然而,笔记本是相当个人化的,笔记本中的代码通常不是写得非常干净。当你在研究数据或进行粗略的可视化时,重点应该放在你要做什么上。直到你准备好编写生产版本的代码之前,你不需要写出完美的代码。话虽如此,不要把笔记本质量的代码直接投入生产环境。

现在我们已经完成了快速分析,我有一些后续问题,应该去回答:

  • 这些账号中有多少实际上与数据科学相关,而我没有已经在抓取的?

  • 这些账号中有哪一些给了我新的推送灵感吗?例如,我有关于数据科学、虚假信息研究、艺术、自然科学、新闻、政治新闻、政治人物等方面的推送。也许我应该增加一个摄影方面的推送。

  • 是否值得通过关键词抓取任何一个热门关键词,来收集更多有趣的内容和账号?

  • 是否有任何账号已经停止更新(从未发布新帖子)?是哪些账号?它们何时停止更新的?为什么停止更新?

你试试看。你能从这个数据集中想到任何问题吗?

接下来,让我们尝试一个相似但稍微不同的方法,使用自然语言处理工具分析《爱丽丝梦游仙境》这本书。具体来说,我想看看是否能将 tf-idf 向量化并绘制出每章中角色的出现情况。如果你不熟悉的话,词频-逆文档频率TF-IDF)这个名称非常合适,因为这正是数学原理。我不会讲解代码,但这就是结果的展示:

图 1.17 – 基于书籍章节的《爱丽丝梦游仙境》TF-IDF 人物可视化

图 1.17 – 基于书籍章节的《爱丽丝梦游仙境》TF-IDF 人物可视化

通过使用堆叠条形图,我可以看到哪些角色在同一章节中一起出现,以及它们的相对重要性,基于它们被提到的频率。这完全是自动化的,我认为这将带来一些非常有趣的应用,比如一种更互动的方式来研究各种书籍。在下一章中,我将介绍社交网络分析,如果你也加入这一部分,你甚至可以构建《爱丽丝梦游仙境》或任何其他文学作品中的社交网络,从而看到哪些角色之间的互动。

为了执行 tf-idf 向量化,你需要将句子分割成词汇单元。分词是自然语言处理(NLP)的基础,词汇单元就是一个词。所以,比如说,如果我们要分词这个句子:

今天是美好的一天。

我最终会得到以下词元的列表:

['今天', '是', '一个', '美好', '的', '``一天', '.']

如果你有几个句子,你可以将它们输入到 tf-idf 中,以返回文本语料库中每个词元的相对重要性。这通常对使用简单模型进行文本分类非常有用,也可以作为主题建模或聚类的输入。然而,我从未见过其他人使用它来按书籍章节确定角色的重要性,所以这是一个创造性的做法。

这个例子只是 NLP 所能做的一小部分,它只探讨了我们可能提出的几个问题。当你进行自己的研究时,我鼓励你随时保持一个纸质笔记本,这样当有问题冒出来时,你可以随时记录下来进行调查。

总结

在这一章中,我们介绍了什么是 NLP,它是如何帮助我的,NLP 的一些常见和高级应用,以及初学者如何入门。

我希望这一章能给你一个大致的概念,了解什么是 NLP,它可以用于什么,文本分析是什么样的,以及一些可以进一步学习的资源。这一章绝不是 NLP 的完整图景。即使是写历史部分也很困难,因为其中有太多内容,而且很多已经随着时间的推移被遗忘了。

感谢阅读。这是我第一次写书,也是我第一次为书写的章节,所以这对我来说意义重大!希望到目前为止你喜欢这本书,并且我希望我能给你提供一个充分的初步了解 NLP 的机会。

接下来:网络科学和社交网络分析!

第二章:网络分析

在这一章中,我将描述三个不同的主题,但从一个非常高的层次来讲:图论社交网络分析网络科学。我们将从讨论围绕“网络”一词的混淆开始,探讨为什么这可能仍然会令人困惑。接着,我们将回顾一下这三者的过去和现在。最后,我们将深入探讨网络分析如何帮助了我,以及如何希望它能帮助你。这不是一个代码密集型的章节。这是一个高层次的介绍。

本章将讨论以下主题:

  • 网络背后的混乱

  • 这些网络到底是什么?

  • 学习网络分析的资源

  • 常见的网络应用案例

  • 高级网络应用案例

  • 网络入门

网络背后的混乱

首先,为了减少混淆,如果你看到我提到NetworkX,那不是打错字。那是我们在本书中将大量使用的一个 Python 库。Python 是一种非常流行的编程语言。

我整个职业生涯都在信息技术IT)领域工作,甚至更进一步。在我职业生涯的某些阶段,我为了工作要求获得了如 Security+和 CISSP 等安全认证,并且我一直在与其他 IT 专业人士合作,如网络工程师。所以,相信我,当我告诉你,我理解与那些将网络主要视为基于 TCP/IP 和子网的人的讨论中的尴尬时。

网络无处不在,甚至在我们体内。事实上,我们的大脑是我们在宇宙中发现的最复杂的东西,正如《如何创造大脑》一书(Kurzweil,2012 年)中所讨论的那样。我们的大脑由数百亿个细胞通过万亿级的连接相互联接。多么复杂的网络啊!

当我想到我们在人工神经网络方面所取得的所有成就,以及它们在翻译、计算机视觉和生成方面的表现时,我觉得它们非常令人印象深刻,但我更为印象深刻的是我们这些超复杂的大脑是如何自然进化出来的,以及即使拥有如此复杂的机制,我们有时也会显得多么愚蠢。

回到我的观点,网络不仅仅是计算机网络。网络是事物之间的关系和信息交换的集合。

在本书中,当我谈论网络时,我指的是。我指的是事物之间复杂的关系。一本我非常喜欢的关于这一主题的书,《网络》(Newman,2018 年),描述了几种不同类型的网络,例如以下这些:

  • 技术网络

    • 互联网

    • 电力网

    • 电话网络

    • 运输网络

      • 航空公司航线

      • 道路

      • 铁路网络

    • 配送和分发网络

  • 信息网络

    • 万维网

    • 引文网络

    • 对等网络

    • 推荐网络

    • 关键词索引

    • 数据流网络

  • 社交网络

    • 社交互动网络

    • 以自我为中心的网络

    • 隶属/协作网络

  • 生物网络

    • 代谢网络

    • 蛋白质-蛋白质网络

    • 基因调控网络

    • 药物相互作用网络

    • 大脑网络

      • 神经元网络

      • 大脑功能连接网络

    • 生态网络

      • 食物链

      • 寄主-寄生网络

      • 共生网络

就我个人而言,我最喜欢的网络是社交网络(而不是社交媒体公司),因为它们让我们能够绘制和理解人们之间的关系,即使在大规模的情况下也是如此。我最不喜欢的网络是计算机网络,因为我对子网掩码或不同的计算机网络架构完全没有兴趣。

在这个美丽的宇宙中,图论、社交网络分析和网络科学赋予你调查和探究许多人甚至没有注意到或意识到的关系的能力。

在这本书中,你会看到“图”和“网络”这两个词交替使用。它们本质上是相同的结构,但通常用于不同的目的。我们在NetworkX中使用图来进行网络分析。当这些图被可视化用于社交网络分析时,它们也被称为社交图。是的,这有点令人困惑。不过,我保证你会克服这个困惑。为了让自己更轻松,我告诉自己它们是相同的东西,只是名字不同,有时用于不同的目的。

这些网络究竟是啥?

让我们稍微进一步细分一下。我想分别讨论图论、社交网络分析和网络科学之间的差异。我会保持高层次的讨论,这样我们可以尽快开始构建。如果你想深入了解这些领域,亚马逊上可能有数十本书可以阅读,我个人大概有 10 本书,而且只要发现有新书出版,我就会再买。

图论

最近,图论引起了很多关注。我在数据科学社区中最为关注它,甚至看到数据库管理员和安全专家也开始对此感兴趣。凭借这些热度,很多人可能会认为图论是全新的东西,但实际上它已经有几百年的历史。

图论的历史与起源

图论的历史与起源可以追溯到 1735 年,距今 286 年。当时有一个谜题叫做柯尼斯堡七桥问题,其目标是找到一种方法,能够不重复穿越任何一座桥就穿越所有七座桥。1735 年,瑞士数学家莱昂哈德·欧拉证明了这个谜题是无法解决的。根本没有办法在不穿越任何一座桥两次的情况下穿越每座桥。1736 年,欧拉就这个证明写了一篇论文,这篇论文成为图论历史上的第一篇论文。

1857 年,爱尔兰数学家威廉·罗恩·哈密尔顿发明了一种名为 Icosian 游戏的谜题,涉及寻找一种特殊类型的路径,后来被称为哈密尔顿回路。

几乎在欧拉论文发表后的 150 年,即 1878 年,一词由詹姆斯·约瑟夫·西尔维斯特在《自然》期刊上发表的论文中首次提出。

最后,1936 年,匈牙利数学家德内什·科尼格(Dénes Kőnig)写下了第一本图论教材《有限图与无限图的理论》,这距离欧拉解决七桥问题已经过去了 201 年。1969 年,美国数学家弗兰克·哈拉里(Frank Harary)写下了图论的权威教材。

换句话说,图论并不新鲜。

图论的实际应用

当前,图论在寻找最优路径方面引起了极大的兴趣。这一知识非常宝贵,因为它在将数据、产品和人员从 A 点到 B 点的路由应用中具有重要作用。我们可以在地图软件找到从源点到目的地的最短路径时看到这一点。我也在解决生产数据库和软件问题时使用路径,所以它的应用不仅限于交通。

在本书中,我们将研究最短路径。继续阅读!

社交网络分析

社交网络分析并没有像图论那样受到大量的关注,我认为这是件遗憾的事。社交网络分析是对我们每个人都参与其中的社会网络——社会结构——的分析。如果你研究社交网络,你就能更好地理解人们的行为。你可以研究人们与谁互动、谁是他们讨厌的人、谁是他们爱的人、毒品如何传播、白人至上主义者如何组织、恐怖分子如何运作、欺诈如何发生、恶意软件如何传播、如何阻止流行病的蔓延等等。这个过程涉及数学,但在我自己的研究中,我最感兴趣的是揭示社会互动。只要你理解这些技术的作用、它们所做的假设,以及如何判断它们是否有效,你就可以不写一个方程式进行社交网络分析。从数学角度来说,我认为这个领域对数学不感兴趣的人也非常友好。然而,数学能力在这里可以成为一种超能力,因为它将使你能够设计出自己的技术,用以剖析网络并发现洞察。

社交网络分析的历史与起源

图论和社交网络分析之间有很大的重叠,因为像最短路径这样的概念在社交网络环境中也非常有用,例如,用它来确定从你当前所在的位置到见到总统需要多少次握手。或者,你需要先见到谁才能有机会见到你最喜欢的明星?

然而,在 1890 年代,法国社会学家大卫·埃米尔·杜尔凯姆和德国社会学家费迪南德·滕尼斯在他们对社会群体的理论和研究中预示了社交网络的概念。滕尼斯认为,社会群体作为连接共享价值观和信仰的个体的纽带(社区)或非个人化和工具性的社会联系(社会)存在。另一方面,杜尔凯姆则提供了一个非个人主义的解释,认为社会现象的产生是因为互动的个体是某种比单个个体属性更大的现实的一部分。我个人对这个观点很感兴趣,当我研究人们在社交图中所处的位置时,我能感受到这一点。人们是否创造了自己在社交网络中的位置,还是部分是偶然的,基于他们认识的人以及他们的连接本身所处的位置?例如,如果你在某个国家的特定地区长大,你很可能会继承社区成员共享的许多情感,但这些情感可能比社区现存的成员还要古老。

在 1930 年代,心理学、人类学和数学领域的多个独立研究小组在社会网络分析方面取得了重要进展。即使在 90 年前,社会网络分析已经是一个多学科的话题。在 1930 年代,雅各布·莫雷诺被认为创造了第一个社会图,以研究人际关系,甚至在 1933 年,这一图被印刷在《纽约时报》上。他说:“在社会度量学出现之前,没有人知道一个群体的‘人际结构’究竟是怎样的。”就我个人而言,每当我第一次可视化我构建的任何社交网络时,我都会感受到同样的兴趣。网络结构就像一个谜,直到你第一次看到它被可视化出来,看到它被首次呈现出来总是令人兴奋的。

在 1970 年代,学者们致力于将独立的网络研究的不同路径和传统结合起来。几乎 50 年前,人们意识到独立的研究正在朝着多个方向发展,因此有了将其整合在一起的努力。

在 1980 年代,社交网络分析的理论和方法在社会和行为科学中变得普及。

社交网络分析的实际应用

社交网络分析可以用于研究任何社会实体之间的关系。社交网络分析的目标是理解社会实体和社区如何互动。这可以在个人层面,也可以是在国际层面。

这甚至可以用来对抗文学。例如,这是我从《爱丽丝梦游仙境》一书中构建的社交网络:

图 2.1 - 《爱丽丝梦游仙境》中的社交网络

图 2.1 - 《爱丽丝梦游仙境》中的社交网络

这是我从《动物农场》一书中构建的社交网络:

图 2.2 – 《动物庄园》的社会网络

图 2.2 – 《动物庄园》的社会网络

这些是我最初创建时的原始社会图,因此它们可能还需要做些改进,我们将在本书中对其进行清理和分析。

我比起图论或网络科学,更喜欢社会网络分析。我想理解人类,我想制止坏人,让世界变得更安全。我想阻止恶意软件,让互联网更安全。这一切都是我动力的源泉,因此我把很多空闲时间投入到社会网络分析的研究中。这是我的重点。

网络科学

网络科学是我另一个兴趣所在。网络科学是关于理解网络是如何形成的,以及它们如何形成的意义。

网络科学的历史与起源

和社会网络分析一样,图论和网络科学的起源有很多重叠。对我来说,网络科学似乎更注重统一各种网络学科,将所有内容集中在一个框架下,就像数据科学试图统一各种数据学科一样。事实上,最近我看到网络科学在数据科学家中被广泛使用,因此我预测,最终网络科学会被视为数据科学的一个子领域。

在我读过的关于网络科学的书籍中,欧拉的七桥问题常常被当作网络科学的起源,展示了图论是网络科学的根源。然而,1990 年代有几个关键的研究努力,专注于数学地描述不同的网络拓扑结构。

邓肯·沃茨(Duncan Watts)和史蒂文·斯特罗加茨(Steven Strogatz)描述了“即小世界”网络,大多数节点不是彼此的邻居,但可以通过很少的跳跃相互到达。阿尔伯特·拉斯洛·巴拉巴西(Albert-László Barabási)和瑞卡·阿尔伯特(Reka Albert)发现,小世界网络在现实世界中并不是常态,并提出了无标度网络(scale-free network),这种网络由少数几个中心节点和许多边缘节点组成,后者具有较少的连接。该网络拓扑会不断增长,以维持连接数和所有其他节点之间的恒定比率。

网络科学的实际应用

网络科学的实际应用也是图论和社会网络分析的实际应用。我认为图论和社会网络分析是网络科学的一部分,尽管这些技术针对每个学科进行了专门化。我个人不做区分,也不太思考图论的问题。在进行网络科学时,我会运用图论和社会网络分析的技术。当我通过代码实现时,图论的大部分数学内容已经被抽象化,但我仍然经常使用最短路径和中心性等内容,前者来自图论,后者则来自社会网络分析。

学习网络分析的资源

那么,开始进行网络思维的旅程需要什么呢?我会给出一些建议,帮助你入门,但请注意,等到这本书出版时,其中一些建议可能已经有些过时,新技术和新方法可能已经出现。这并不是一个完整的清单,而是最低要求。首先,你需要的是一颗好奇心。如果你愿意去探索构成我们存在的隐秘网络,那么你已经具备了第一个前提条件。不过,我还是会给你更多的建议。

笔记本界面

我在 Jupyter 笔记本中进行所有的网络分析。你可以通过 Anaconda 下载和安装 Jupyter,网址是:docs.anaconda.com/anaconda/install

如果你不想安装 Jupyter,你也可以直接使用 Google Colab,无需任何安装。你可以在 research.google.com/colaboratory 找到并立即开始使用 Google Colab。

与典型的集成开发环境IDE)不同,笔记本界面允许你在“单元格”中编写代码,然后按顺序运行或重新运行它们。这对研究很有用,因为你可以快速地在数据上进行实验,实时进行探索性数据分析和可视化,且可以在你的网页浏览器中查看。以下是效果图:

df = pd.read_csv(data)
df['book'] = df['book'].str.lower()
df['entities'] = create_entity_list(df)
df.head()

《圣经》包含在数据文件中,所以这将加载数据并输出《圣经》前五节经文的预览。

图 2.3 – 《圣经》的 pandas DataFrame

图 2.3 – 《圣经》的 pandas DataFrame

现在,让我们来看一下《创世记》中提到夏娃的章节:

entity_lookup('Eve', book='genesis')

这将显示提到夏娃的两节经文的预览:

图 2.4 – 《创世记》中提到的夏娃

图 2.4 – 《创世记》中提到的夏娃

让我们看看夏娃在整个《圣经》中被提到多少次:

entity_lookup('Eve')

再次,这将显示经文的预览:

图 2.5 – 《圣经》中提到的夏娃

图 2.5 – 《圣经》中提到的夏娃

我加载了自己创建的《圣经》数据集,并进行了些许清理和丰富,然后我创建了两个额外的单元格,一个是查找《创世记》中的夏娃,另一个是查找《圣经》全书中的夏娃。如果你仔细看,你会发现我不小心把df设置成了全局变量,这样不太好。我应该修复那个代码。哦,好吧,下次吧,代码能运行就好。

一个笔记本允许你进行数据实验,并发挥极大的创意。笔记本有优缺点,其中一个我之前提到过。

优点是你可以非常快速且轻松地在数据集上进行实验,构建加载、预处理、丰富和甚至机器学习ML)模型开发的代码。你可以用一个笔记本做很多事情,且非常迅速。这是我进行初步探索和开发时最喜欢的工具。

缺点是,笔记本代码有个不太好的声誉,被认为是快速且不够规范的代码,这种代码其实不适合生产环境。另一个问题是,笔记本代码可能会变得杂乱无章,不仅仅是我们笔记本电脑上的文件,还包括 Jupyter 服务器(如 SageMaker、Kubeflow 等),所以很容易导致事情变得混乱和难以管理。

我建议用它们来探索数据并激发创新,但一旦你有了有效的解决方案,就把这些代码拿到集成开发环境(IDE)中好好构建。我目前偏好的 IDE 是 Visual Studio Code,但你可以使用任何你舒适的工具。IDE 将在下一节中定义。

你可以在 code.visualstudio.com/download 下载 Visual Studio Code。

集成开发环境(IDEs)

集成开发环境(IDE)是一种用于编写代码的软件。例如 Visual Studio Code、Visual Studio、PyCharm、IntelliJ 等。不同的 IDE 支持多种编程语言。试试几个,选择最适合自己的工具。如果某个 IDE 使用起来让你效率低下,太长时间没法适应,那就说明它可能不适合你。不过,现代的 IDE 通常都有一定的学习曲线,所以要对自己和软件有耐心。如果你想了解更多关于 IDE 的信息,可以在 Google 上搜索 popular IDEs。刚开始时,我建议你花点时间深入学习所选的 IDE,观看视频并阅读文档。我通常每隔几年会这样做,尤其是在刚开始的时候。

网络数据集

没有数据,你无法进行网络分析。你要么需要创建数据,要么需要去找一些现成的数据。与创建其他类型的数据集(例如机器学习训练数据)相比,创建网络数据集通常更容易、更简单,也更少需要手动操作,而且它也是快速建立领域知识的好方法。我们的兴趣和好奇心可能比使用别人的数据集更具探索性。不过,为了让你了解,确实有一些网络数据集可以在你熟悉网络分析工具时作为起点:

图 2.6 – Google 搜索:网络科学数据集

图 2.6 – Google 搜索:网络科学数据集

除了顶部的链接,其他数据集看起来有些被忽视且基础,似乎好几年没有更新了。

搜索社交网络分析数据集会找到看起来稍微更有趣的数据集,这也有道理,因为社交网络分析比网络科学更为久远。然而,顶部链接是一样的。如果你需要一些现成的数据,看看斯坦福网站以及 data.world

图 2.7 – Google 搜索:社交网络分析数据集

图 2.7 – Google 搜索:社交网络分析数据集

Kaggle 数据集

Kaggle 是另一个寻找数据的不错地方,几乎可以找到任何类型的数据。Kaggle 偶尔会受到一些人的不公平对待,有些人瞧不起那些使用 Kaggle 数据集的人,因为 Kaggle 上的数据不像现实世界中的数据那样脏乱,但 Kaggle 的数据和前面提到的数据集没有太大区别。它们都已经经过清洗,准备好供你练习机器学习或网络分析工具。Kaggle 上有许多有趣的数据集和问题需要解决,它是一个学习和建立信心的好地方。使用那些对你有帮助的资源。

NetworkX 和 scikit-network 图形生成器

在本书中,我将解释如何使用 Python 库 NetworkXscikit-network。我使用 NetworkX 来构建图,使用 scikit-network 来进行可视化。我这么做是因为 scikit-network 并没有真正的网络分析能力,但它可以渲染 可缩放矢量图形SVG)格式的网络可视化图,比 NetworkX 的可视化更快、更好看。这两个库还包含了若干个用于生成常见实践网络的生成器。如果你使用它们,你不会熟悉如何从零开始创建网络,但如果你只是想尝试一个预构建的网络,这是一个不错的起点:

我的建议是,你应该忽略 scikit-network 的加载器,直接熟悉使用 NetworkX 来创建网络。个人而言,它更为优秀。在目前的情况下,除非是可视化,scikit-network 完全无法替代 NetworkX。

创建你自己的数据集

在本书中,我将向你展示如何创建自己的数据集。就我个人而言,我认为这是最好的方法,也是我每次实际操作时都采用的方法。通常,我会有一个想法,然后把它写在白板上或笔记本里,想办法获取数据,然后开始工作。这个过程通常需要几天时间,我通常会采用这样的方式:

  1. 想一个点子。

  2. 跑去白板或笔记本上写下来。

  3. 带着笔记本坐到外面,幻想一下如何获得数据。

  4. 设置一些网页抓取器。

  5. 在抓取数据一两天后下载数据。

  6. 从句子中提取实体并构建边列表数据框。

  7. 构建网络。

  8. 深入网络分析。

  9. 可视化那些看起来有趣的东西。

  10. 玩转自我图(ego graphs),从更多角度探索自我网络。

这绝对是一个过程,虽然看起来步骤很多,但随着时间的推移,它们都会融合在一起。你有一个想法,获取数据,进行预处理,然后分析并使用数据。从某种程度上讲,这与其他数据科学工作流并没有太大区别。

在这本书中,我将解释如何将原始数据转化为网络数据。

获取网络数据的途径有很多种。没有哪一种是错误的。有些方法只是省略了一些步骤,简化了操作,使得你可以用工具和技术进行练习。这完全没问题。然而,你应该以自给自足为目标,并且习惯与脏数据打交道。数据永远不会是干净的,即使有人似乎做了很多清理工作。即便是我自己 GitHub 上的数据集,通常还有更多的清理工作要做。文本数据是杂乱的。你需要学会在这些“脏”数据中游刃有余。对我来说,这正是我最喜欢的部分——将混乱的文本数据转化成美丽且有用的网络。我喜欢这样做。

所以,熟悉我们谈论过的所有内容,然后去探索吧。如果你真的很有雄心,甚至可以考虑创建第一个成功将所有内容整合在一起的网络数据存储库。

NetworkX 和文章

我发现学习网络科学的另一个有用方法是直接在 NetworkX 文档网站上发布的文章链接。当它们描述其功能时,通常会提供一个链接,指向关于构建该功能所用技术的文章。这里是一个例子:

图 2.8 – NetworkX 文档:k_core 算法

图 2.8 – NetworkX 文档:k_core 算法

如果你去 NetworkX 的k_core页面,你可以看到关于该函数、参数、返回值和一些说明的清晰文档。这还不是最棒的部分,最棒的部分隐藏得更深。向下滚动,你通常会看到像这样的内容:

图 2.9 – NetworkX 文档:k-core 文章引用

图 2.9 – NetworkX 文档:k-core 文章引用

太棒了!这里有一个 arXiv 链接!

能够阅读关于k_core的研究背景非常有用。对我个人来说,得知很多内容其实已经相当陈旧,感到有些宽慰。例如,我提到的自然语言处理NLP)历史。我们常常听到说数据科学的进展如此之快,以至于无法跟上。但这只有在你被每个闪亮的新事物分散注意力时才是真的。专注于你想做的事,研究如何实现你的目标,然后构建它。你不应该仅仅使用更新的技术。新的并不总是更好的,甚至通常不是。

以 k_core 为例,我当时在寻找一种方法,快速丢弃所有只有一条边的节点。在我第一次做网络时,我会写很多代码行来完成这个任务,碰到列表推导的麻烦,或者只是写一些能工作但很难看的代码。然而,我本可以直接做nx.k_core(G, 2),就这么简单。问题解决了。

所以,阅读文档吧。如果你想深入了解,可以寻找学术期刊链接,然后不断构建,直到你理解该技术。

你会在 NetworkX 中找到类似的链接,用于中心性或其他主题。去探索一下吧。它是很好的阅读材料,能激发灵感。

非常棒!这为我们进一步了解网络分析中中心性的起源和应用打下了基础。

常见的网络使用案例

正如我在第一章中所做的,《自然语言处理介绍》,我现在也将解释一些我自己最喜欢的网络数据使用案例。我在本章开头提到过,网络有很多种不同的类型,但我个人更喜欢处理社交网络和我称之为数据流网络的内容。

以下是我在处理网络数据时的一些用途:

  • 映射生产数据流

  • 映射社区互动

  • 映射文学社交网络

  • 映射历史社交网络

  • 映射语言

  • 映射黑暗网络

我将从数据流网络开始,因为这是我意识到的第一个网络数据使用案例,也是彻底改变我工作方式的东西。

映射生产数据流

如前所述,这是我为自己使用网络数据时的第一个想法。我在软件领域工作了超过 20 年,并且花费了大量时间“解剖”生产代码。这不是为了娱乐,而是有实际用途的。在过去,我曾被要求“升级”旧的生产系统,这意味着将一个旧的生产系统拆解,弄清楚它上面运行的所有内容(代码、数据库以及文件读写操作),然后将一切迁移到新服务器上,以便将旧服务器淘汰。我们称之为去风险化生产系统。最终,这会导致一个极其快速的新生产系统,因为这些通常是运行在非常新的基础设施上的老旧代码。

在我以前的方式中,我会通过从cron开始,来清点生产系统,即如何在服务器上调度任务。通过从cron开始,你可以找到一个过程的根源,看看哪些脚本调用了其他脚本。如果你将这个过程映射出来,最终你会得到这样的内容:

  • cron:

    • script_a.php > script_a.log

    • script_b.php > script_b.log

    • script_c.php > script_c.log

    • script_c.php 使用 script_d.php

    • script_c.php 使用 script_e.php

以此类推。这并不是cron的真实模样,但它展示了脚本调用和数据写入的方向。因此,在上一个示例中,代码执行了以下操作:

  • script_a.php 写入 script_a.log 日志文件

  • script_b.php 写入 script_b.log 日志文件

  • script_c.php 写入 script_c.log 日志文件

  • script_c.php 启动另一个脚本,script_d.php

  • script_c.php 启动另一个脚本,script_e.php

我曾经是手动进行这些工作的,这是一项非常繁琐且精确的工作,因为如果漏掉任何东西,可能会导致迁移时无法迁移或无法在新服务器上测试,这在迁移日发现时会非常痛苦。在一台有几百个脚本的服务器上,甚至需要几周时间才能构建一个脚本图,而完整的输入和输出图则需要更长的时间。从前面的例子来看,脚本图看起来是这样的:

  • cron:

    • script_a.php

    • script_b.php

    • script_c.php

      • script_d.php

      • script_e.php

在生产环境中,这种交互可能非常深入且高度嵌套。2017 年,我发现了一种新的方法,通过使用网络,脚本图会利用图中构建的节点和边缘自动生成。这本可以是一个非常长的讨论,我甚至想过写一本书来详细讲述——不过为了节省时间,这种新方法的工作效率大约是旧方法的 10%,并且能够捕捉到所有信息,只要我在源代码分析时足够仔细。我还不需要在脑中维持一个服务器的运行状态图像,因为我有一个图形可视化工具可以直观检查,并且有一个网络可以进行查询。通过这种方法,我的工作效率提高了 10 倍,在相同的时间内,我可以提升更多的服务器,而过去我可能只能完成一台服务器的工作。通过建立的网络,我还可以快速排查生产问题。过去需要几天才能解决的问题,现在我通常可以在几分钟内解决。

另一个不错的副作用是,如果你想要重新设计软件并使用这种方法,你可以理解服务器上所有内容的输入和输出,利用这些信息找到低效的地方,并提出如何在新平台上更好地构建软件的想法。我也曾经这样做过,将 SQL Server 数据库替换为 Spark 数据流,而且非常成功。

映射社区互动

现在,我不再提升生产系统,而是转向了更有趣的工作,因此接下来我将描述的内容来自我自己的独立研究,而不是我在工作中做的事情。对于我的研究,我做了大量的社交媒体抓取与分析,因为这给了我一个了解周围世界的方式。我想要理解的一件事是,某些群体中的人们是如何与彼此以及与群体外的其他人互动的。

例如,我抓取了几十个与数据科学相关的 Twitter 账户,以便关注数据科学和科学社区中的最新动态。在最初抓取一些账户后,我提取了用户提及并映射了它们之间的所有互动。这有助于我发现其他值得抓取的用户,并将它们添加到我的抓取器中,然后我继续提取用户提及并映射它们之间的所有互动。我一次又一次地重复这一过程,不断为我感兴趣的任何领域—数据科学、自然科学、艺术、摄影,甚至 K-pop—建立社交网络。我觉得分析社区非常有趣,这也是我最喜欢做的事情之一。这是本书中我将花很多时间讨论的话题,因为我已经见识到这项技术对研究的巨大价值,甚至曾用抓取的结果构建高度定制的机器学习训练数据,从而自动发现更多我感兴趣的内容!这些结果是自然语言处理、社交网络分析和机器学习的美妙结合!

这对于关注社区内的趋势和讨论话题也非常有用。例如,如果有一个大型数据科学会议即将召开,关于该话题的讨论会突然增多,可能会分享类似的标签。通过创建用户-标签网络而不是用户-用户网络,也可以找到这些标签。这对于观察趋势的变动非常有效,尤其是当你将时间元素加入到网络数据中时。

映射文学社交网络

我最喜欢的另一个网络应用是将一部文学作品,如爱丽丝梦游仙境动物农场,的社交网络进行映射。我将在本书中详细讨论这一点。与社交媒体相比,这个方法稍微复杂一些,因为前面提到的技术可以单靠正则表达式完成,而这个技术需要自然语言处理中的词性标注或命名实体识别。然而,结果是你能够得到该文本中人物之间互动的社交网络,从而更好地理解角色间的关系。

映射历史社交网络

你还可以使用自然语言处理(词性标注和命名实体识别)的方法来映射历史社交网络,只要你有关于某个事件的文本或能展示人物关系的数据。例如,通过我前面描述的文学社交网络技术,可以轻松构建整个《圣经》的社交网络。虽然涉及大量清理工作,但如果你足够细心,最终结果会让人叹为观止。我非常希望看到研究者也能对其他历史文献进行类似的操作,这可能会为我们提供前所未有的新的见解。

映射语言

网络的另一个酷炫用途是映射语言本身。这是我在玩《傲慢与偏见》这本书时发现的。我想看看如果我构建一个网络,连接她的书中每个句子里每个单词之间的互动,会发生什么。例如,我们来看《傲慢与偏见》中的下面这句话:

人们普遍承认,拥有丰厚财富的单身男人,一定是想找个妻子。

让我们通过使用单词序列来映射单词关系。如果一个单词跟随另一个单词,那么我们就说这两个单词之间有关系。把关系想象成连接两个事物的无形线条。对于前面的引用,我们可以这样映射:

  • it -> is

  • is -> a

  • a -> truth

  • truth -> universally

  • universally -> acknowledged

  • acknowledged -> that

  • that -> a

  • a -> single

  • single -> man

  • man -> in

  • in -> possession

  • possession -> of

  • of -> a

  • a -> good

  • good -> fortune

  • fortune -> must

  • must -> be

  • be -> in

  • in -> want

  • want -> of

  • of -> a

  • a -> wife

在网络中,我们称节点之间的连接为节点是我们正在连接的事物。在这种情况下,这是一个单词网络,所以单词是节点,单词之间的连接叫做边。你可以把边看作是连接两个事物的线条。

在引用的句子中,你可能会注意到atruthsinglegoodwife之间存在联系,in也有两个联系,分别与possessionwant相关。

这里是另一个例子,使用了 Smashing Pumpkins 的歌曲:

尽管我充满愤怒,我仍然只是一个笼中之鼠。

你也可以像这样将其映射出来:

  • despite -> all

  • all -> my

  • my -> rage

  • rage -> i

  • i -> am

  • am -> still

  • still -> just

  • just -> a

  • a -> rat

  • rat -> in

  • in -> a

  • a -> cage

字母aratcage之间有联系。

如果你用整本书来做这个,你最终会得到一个超密集的网络,几乎无法可视化;然而,分析这个网络并不是毫无价值的。如果你查看《傲慢与偏见》的外部节点,你会找到简·奥斯汀最少使用的单词,它们通常能组成一个惊人的词汇表。如果你看核心单词,它们通常是连接词,比如“a”、“the”和“of”。

我也使用了同样的技术进行了一些研究,看看我能否通过视觉识别出 AI 生成的文本与人类生成的文本之间的区别。你猜怎么着?可以。如果你用机器学习生成文本,它会选择下一个单词,不是因为它最合适,而是因为它最有可能被使用。当你试图预测句子“Jack and Jill went up the ______”中的下一个单词时,这很棒,但当你真的想用它创造出一些真正原创的东西时,这就不那么棒了。我们人类更具细腻性。如果我想把这个句子变得更加阴郁,我可以用“chimney”来完成这个句子。我真的怀疑机器学习会选择“chimney”。

从视觉上看,AI 生成的文本和人类生成的文本在网络结构上有所不同。当我在 2019 年做这项工作时,AI 生成的网络外观非常参差不齐,核心部分却更为密集,因为它会选择更高概率的词汇。而人类则更为细致,外缘看起来更柔和。我能够在视觉上区分二者。更近期的 AI 会显得不同吗?如果你感兴趣,我鼓励你试试看。

绘制黑暗网络

最后一部分有点黑暗。有时,当世界上发生不好的事情时,我想知道一些团体正在策划什么。黑暗网络是那些希望伤害他人的人组成的社交网络,但黑暗网络有不同的种类(涉及犯罪、恐怖、仇恨、毒品等等)。即使是危险人物也会围绕某些社交媒体标签聚集,因此他们偶尔会暗示即将发生的事情。我不会详细讲解可以收集到的情报层级,但你可以按照我在 绘制社区互动 下讨论的内容,利用它来关注事态发展,或找到其他感兴趣的人。你也可以用它来举报违反社区准则的社交媒体帖子,甚至在看到具体情况时上报给执法部门。只要知道,如果你进行这种 开放源代码情报OSINT)收集,它可能对你的情绪和心理健康产生不良影响,因此要小心处理。不过,它也可以是很好的研究工具。

重要说明

当我用它进行 OSINT 时,我通常会从一些人或感兴趣的事物开始,然后逐渐绘制出它们之间的社交网络。但这并不限于黑暗网络。你也可以使用这种技术来进行攻击、防御,或者仅仅满足你的好奇心。这是研究人与组织之间互动的一种有用方式。

OSINT 有许多用途,关于这一主题已有整本书籍写成。然而,OSINT 的目标是利用公开的数据更好地理解或监控某些事物。例如,你可以使用 OSINT 来研究你正在面试的公司。公司是谁拥有的?谁在面试我?这家公司曾经上过新闻吗?这是个正面新闻还是负面新闻?公众对这家公司的普遍看法是什么?这是一家合法公司吗?前员工对它怎么说?他们在社交媒体上说什么?大部分评论是正面的、负面的,还是中立的?如果你提出正确的问题,并能访问互联网,你可以了解任何事物的很多信息。

市场调研

假设一个情境:你创造了一款新产品,想要找到可能会对你的产品感兴趣的人。我为自己的需求做过完全相同的事情。

我曾使用社交网络分析构建一个社区成员的地图,并将结果用于市场研究。对于某个感兴趣的话题,我可以看到来自世界各地的数千名研究人员,如果我想为这个社区创建一个产品,开发一个有效的推广活动来激发对我的产品的兴趣将不需要太多工作。为了这个目的,我只使用了 Twitter 数据,经过几天的抓取和分析,我就能够识别出成千上万的潜在接触对象。

查找特定内容

这一部分有点像“先有鸡还是先有蛋”的问题。你需要社交媒体文本才能提取文本中存在的社交网络数据,然后你可以利用社交网络来识别其他需要抓取的账户,进一步提升你的数据流。这是一个迭代的过程:识别、抓取、分析,识别、抓取、分析,依此类推。你可以通过基于抓取的文本构建 NLP 分类器,进一步改善分析部分。社交网络分析将帮助你找到重要账户,但你的 NLP 分类器将帮助你过滤噪音,获取相关和集中的内容。

创建机器学习训练数据

如前所述,你可以使用抓取的数据流来创建 NLP 分类器,并利用这些分类器帮助你过滤噪音,从而得到更集中的内容。然后,你可以利用这些集中的内容构建更好的分类器。然而,你必须首先创建初步的分类器,而通过结合 NLP 和网络分析创建的数据流将为你提供所需的丰富过滤数据,以创建有用的训练数据。这个过程是有步骤的,既耗时又需要大量工作。我将在第十四章网络与监督式 机器学习 中解释这一点。

具体而集中的内容流入分类训练数据,训练后的分类器将帮助识别更多有用的内容。

高级网络应用场景

第一章自然语言处理简介 中,我指定了 NLP 的几个高级应用场景,如语言翻译和文本生成。然而,在考虑网络分析时,我的脑海里立刻出现了一个问题:一个高级网络应用场景到底意味着什么?这些内容都相当先进。对于 NLP,你有一些简单的任务,如分词、词形还原和简单的情感分析(积极或消极,是否为仇恨言论),同时也有一些高级任务。对于网络分析,我能想到三个潜在的高级应用场景:

  • 图形机器学习

  • 知识图谱

  • 推荐系统

然而,我并不认为它们有多先进。我认为它们只是实现方式与我提到的其他东西不同而已。此外,仅仅因为某件事在技术上更具挑战性,并不意味着它更先进或更重要。事实上,如果它更困难且返回的结果更无用,那就不是理想的做法。那简直是浪费时间。

图形机器学习

我曾参与过图形机器学习(Graph ML)项目,主要发现有两种方法:一种是将图形度量(中心性、局部聚类系数等)作为训练数据中的特征,另一种是直接将图形表示数据输入机器学习,让它自己去分析。图形元数据可以为训练数据提供强有力的补充,因为它可以为模型提供一个额外的视角,因此我认为这一点非常有前景。

然而,过去当我看到人们试图直接在机器学习中使用图形数据时,通常图形知识是缺失的或很薄弱的。在你能够分别做好网络分析和机器学习之前,我不建议采用这种方法。在机器学习中,输入数据的重要性通常和模型本身一样大,甚至更重要,因此对网络的了解应该是存在的,但并非总是如此。领域知识在数据科学中非常重要。然而,这仍然是非常有趣的内容,所以一定要去了解一下。

推荐系统

推荐系统很有趣,尽管我没有花太多时间研究它们;其中一个概念是,如果两个人喜欢相似的事物,他们也可能喜欢其他尚未共有的事物。例如,如果你和我都喜欢乐队 Soundgarden、Smashing Pumpkins 和 Nirvana,而我喜欢 The Breeders,您喜欢 Stone Temple Pilots,那么我很可能也会喜欢 Stone Temple Pilots,而你也会喜欢 The Breeders。如果你想探索推荐系统,我鼓励你深入研究。只是这不是我的兴趣所在,因为我主要使用网络进行社交网络分析、市场调研和开源情报(OSINT)。

然而,推荐系统也有一些缺点,我想指出这些问题。这些系统推荐我们可能喜欢的事物,但结果通常并不令人意外。在音乐方面,我很可能会喜欢 Stone Temple Pilots,而你会喜欢 The Breeders,这完全可以理解,但我个人更兴奋的是当我爱上完全意想不到的东西时。我希望未来我们的推荐系统能够推荐我们可能无法自己发现的食物、音乐和电视节目。我不想只体验类似的事物。我也想要那些意外的东西。

此外,当我们仅被展示我们期望看到的东西时,我们往往会强化一些不太好的东西。例如,如果社交媒体公司只给我们展示他们认为我们喜欢的故事,或者我们可能会参与的内容,我们最终会陷入回声室,这让我们主要与那些和我们一样的人交往,讨论相同的话题,并且在同样的事情上达成一致。这既不具教育意义,也不具有建设性。它导致了两极化,甚至可能引发暴力。当人们只愿意与自己相似的人交往,并开始妖魔化那些与自己不同的人时,这就成了一个危险的问题,而这种现象时常发生,我们也很容易受到影响。

我将留给你自己去决定网络使用的常见与高级应用。在我看来,它们都是高级的,可能只是实现方式不同。

网络入门

如果你想开始自己的第一个原创网络分析项目,首先需要想出一个你感兴趣的问题。在社交网络分析中,你通常是想建立一个社交图,一个展示人类如何互动的可视化图。对于你的第一个网络项目,你可以提出以下类似的问题:

  • 》中的人物是如何相互互动的?

  • 《动物农场》这本书中的不同动物是否只与相同类型的动物互动?人类是否只与某些类型的动物互动,还是与所有类型的动物都有互动?

  • 我所在小镇的 Twitter 圈子是什么样的?

  • 餐食配料的网络可视化是什么样的?不同地区的配料网络可视化有什么不同?不同地区的配料网络可视化又是什么样的?

  • 某个政治人物周围的 Twitter 社交网络是什么样的?朋友网络是什么样的?敌人网络又是什么样的?

  • 如果我构建一个示例暗网,如何才能将其以最优方式摧毁,使其无法修复,以至于即使在持续的攻击下,也永远无法重新形成?

  • 如果我构建自己的基础设施网络,并且如果我能够识别出可能导致它在被攻击时破裂的结构性弱点,我该如何保护它,以防止这些类型的攻击打乱网络?

这些只是我在过去几分钟内想到的一些例子。你越是实验并玩弄网络,越容易想出属于你自己的研究思路,所以从一些简单的开始,随着时间的推移再逐步变得更有雄心。这些听起来可能有些复杂,但只要你能获取所需的数据,网络构建、分析和可视化其实并不难,并且它们之间有很多重叠之处。

简而言之,你需要找到一个你感兴趣的主题,并识别这些事物之间的关系。这可能是非常简单的,比如谁认识谁,谁喜欢谁,谁信任谁,谁讨厌谁,谁和谁一起工作,哪些食材可以做哪些菜肴,哪些基础设施和其他基础设施通信,哪些网站链接到其他网站,或者哪些数据库表可以与其他数据库表连接。

对于我自己的独立研究,我通常使用 Twitter 数据,因为在我看来,它是一个自然语言处理(NLP)和网络分析的宝库。而且我的许多工作都遵循一个可重复的过程:

  1. 想出一个研究主题。

  2. 找到一些与该主题相关的 Twitter 账户。

  3. 抓取账户。

  4. 分析抓取的数据:

    • 提取网络数据。

    • 提取更多用户进行抓取。

  5. 根据需要重复步骤 3 和 4

如前所述,这是一个可重复的过程。我将在第六章《图谱构建与清理》和第七章《整体网络分析》中解释如何开始抓取数据。一旦你学会了通过抓取和应用程序编程接口APIs)获取数据,你将能够获得比你能使用的更多的数据。而对于社交网络分析,你可以按照前面的示例进行操作。慢慢来,它最终会变得自然而然。

示例 – K-pop 实现

我的女儿们喜欢 K-pop 音乐。经常在晚餐时,我会听到她们讨论像 BLACKPINK、TWICE 和 BTS 这样的组合,但我根本不知道她们在说什么。然后有一天,我正在思考为我的 LinkedIn 挑战 #100daysofnetworks 找一个研究主题,我突然想到应该做一些关于 K-pop 的内容,因为如果我能多了解一点,我就能和女儿们有共同话题,而且这也是向她们展示数据科学的一种方式。于是,我想出了一个过程来把这个想法变成现实:

  1. 我决定研究围绕许多著名 K-pop 艺人的社交网络,并且这个网络应该包括音乐中心和粉丝俱乐部。

  2. 我花了大约 30 分钟做 Twitter 查询,目的是找出几十个 K-pop 组合、音乐中心和粉丝俱乐部。我找到了大约 30 个,并把它们写在笔记本上。

  3. 我把这 30 个账户添加到我的 Twitter 抓取工具中,然后将数据合并成一个kpop.csv的文件。这让我可以更轻松地进行所有网络数据的提取和分析,所有的内容都集中在一个文件里。我的抓取工具是 24/7/365 运行的,所以我总是能获得新鲜的数据进行探索。

  4. 在抓取了几天的数据后,我分析了数据流,提取了网络数据(用户提及和话题标签),并找出了几十个可以继续抓取的账户。

  5. 我重复了步骤 3 和 4大约一个月,现在我已经有了一个围绕 K-pop 艺人、音乐中心和粉丝俱乐部的以 Twitter 为中心的社交网络。

现在我正在积极爬取与 K-pop 相关的 97 个账户,我有新鲜的媒体供我的女儿们享用,并且我有有趣的网络数据可以进一步提升我的技能。

因此,对于您自己的网络研究,请找到您感兴趣的内容,然后开始工作。选择您感兴趣的内容。研究不应该是无聊的。这本书将向您展示如何将您的研究好奇心转化为成果。我们将在通过下几章后讨论图构建、分析和可视化。

总结

在这一简短的章节中,我们涵盖了很多内容。我们讨论了关于“网络”一词的混淆,深入探讨了图论、社交网络分析和网络科学的历史和起源,讨论了学习和实践的资源,讨论了一些我喜欢的网络应用案例,并最后解释了如何开始制定您自己的网络研究。

我希望这一章给您提供了关于所有这些网络内容的大致了解。我知道我没有详细讨论起源,我主要谈论了社交网络分析,但那是因为那是我的兴趣领域。我希望您现在了解网络可以用于什么,并且希望您明白我只是触及了表面。我的目标是激发您的好奇心。

在下一章中,我将解释用于自然语言处理的工具。我们将逐渐超越理论,进入数据科学领域。

进一步阅读

  • Barabási, A.L. (2014). 链接. 基础书籍。

  • Kurzweil, R. (2012). 如何创造一个心灵. 企鹅图书。

  • Newman, M. (2018). 网络. 牛津大学出版社。

第三章:有用的 Python 库

在本章中,我将介绍我们将在本书中使用的几个 Python 库。我会描述它们是什么、如何安装,并给出一些有用的示例。你不需要记住每个 Python 库的所有功能,或者深入理解你使用的每个函数的内部原理。重要的是,你需要知道哪些库是可用的,它们的整体功能是什么,以及库中有哪些关键的时间节省工具(这会让你反复使用)。每个人的使用场景不同,无法记住所有内容。我建议你尽快理解这一点,在需要时学习所需内容。根据需要深入了解库的内部结构。

为了保持条理,我将按照类别来划分软件库。以下是我们将讨论的库:

Python 库 类别
pandas 数据分析与处理
NumPy 数据分析与处理
Matplotlib 数据可视化
Seaborn 数据可视化
Plotly 数据可视化
NLTK 自然语言处理
spaCy 自然语言处理
NetworkX 网络分析与可视化
Scikit-Network 网络可视化(更好)
scikit-learn 机器学习
Karate Club 机器学习(图形)
spaCy (重复) 机器学习(自然语言处理)

图 3.1 – 自然语言处理的 Python 库表格

以这种方式划分库非常有用,因为它是合乎逻辑的。我们需要首先收集、处理和分析数据,然后再做其他任何事情。在分析数据的过程中,我们应当进行数据可视化。某些库专注于自然语言处理,其他库专注于网络分析与可视化。最后,有些库对于不同类型的机器学习(ML)非常有用,甚至一些非 ML 专注的库也往往具有 ML 功能,例如 spaCy

我将保持这个介绍的高层次,因为我们将在接下来的章节中实际使用这些库。本章介绍的是关于给定库的“什么”和“为什么”的问题,其他章节将介绍它们的使用方法。

继续之前的最后一点:你不需要记住这些 Python 库的每个方面。在软件开发和数据科学中,技能是逐步积累的。现在学习对你有用的部分,然后如果某个库证明有用,再深入学习。不要因为只知道一些小部分而感到愧疚,知识是随着时间积累的,而不是一蹴而就的。

本章的内容将包括以下几个部分:

  • 使用笔记本

  • 数据分析与处理

  • 数据可视化

  • 自然语言处理(NLP)

  • 网络分析与可视化

  • 机器学习(ML)

技术要求

本章将介绍我们在全书中将使用的许多资源。

所有代码都可以在 GitHub 仓库中找到:github.com/PacktPublishing/Network-Science-with-Python.

使用笔记本

进行数据分析和原型开发时,通常最简单且非常有用的方法是使用我们常亲切地称为笔记本的工具。Jupyter 将 Jupyter Notebook 定义为一个基于网页的交互式计算平台。我喜欢这个简单的定义。笔记本本质上是一系列可以包含代码或文本的“单元”,这些单元可以单独运行,也可以顺序运行。这使得你可以在网页浏览器中编写代码,在网页浏览器中运行代码,并看到即时的结果。对于数据分析或实验,这种即时反馈非常有用。

在本书中,我们使用 Jupyter Notebook。我建议从 Anaconda 网站下载并安装它。你可以在www.anaconda.com进行下载。

在 Jupyter 中,你可以运行代码并查看该代码的即时结果,无论是文本、数字还是数据可视化。你将在本书中看到很多笔记本的使用,所以我会简短说明。

Google Colab 是另一种使用笔记本的选择,你无需安装任何东西即可使用它。这可能是使用笔记本的一种更简便的方式,但它也有优点和缺点。我建议你学习如何使用两者,并选择一个最适合你,或者允许你与他人高效合作的工具。

你可以查看 Google Colab,网址是 colab.research.google.com

接下来,让我们探索一下我们将在数据分析中使用的库。

数据分析与处理

在处理数据时,有许多有用的库,而在数据生命周期的不同阶段,你会希望使用不同的库和技术。例如,在数据处理时,探索性数据分析EDA)通常是一个有用的起点。之后,你可能需要进行数据清理、数据整理、预处理时的各种转换等等。下面是一些常用的 Python 库及其用途。

pandas

pandas 无疑是 Python 中进行数据处理时最重要的库之一。简而言之,如果你在 Python 中处理数据,你应该了解 pandas,并且很可能应该使用它。你可以在数据处理时使用它做多种不同的事情,例如:

  • 从各种文件类型或互联网读取数据

  • EDA

  • 提取、转换、 加载ETL

  • 简单快捷的数据可视化

  • 还有更多更多的内容

如果有一个 Python 库我会推荐给这个星球上的每一个人——不仅仅是数据专业人士——那就是 pandas。能够在不使用电子表格的情况下快速进行数据分析是非常解放且强大的。

设置

如果你在 Jupyter 或 Google Colab 笔记本中工作,pandas 很可能已经安装好了,你可以直接开始使用它。不过,如果需要,你可以按照官方安装指南操作:pandas.pydata.org/docs/getting_started/install.html

一旦安装完成,或者如果你怀疑它已经安装,只需在你喜欢的笔记本中运行这个语句:

import pandas as pd

如果 Python 没有提示库未安装,那么你就准备好了。

如果你想查看你正在使用的 pandas 版本,可以运行这个语句:

pd.__version__

我看到我正在运行版本 1.3.4。

启动功能

阅读数据是我帮助大家入门 pandas 的首选方式。毕竟,如果没有数据玩,数据工作会非常枯燥。在 pandas 中,你通常使用 DataFrame,这是一个类似于数据表或电子表格的数据结构。一个 pandas DataFrame 是一个由行和列组成的数据对象。

要将 CSV 文件读取到 pandas DataFrame 中,你可以像这样做:

data = 'data/fruit.csv'
df = pd.read_csv(data)

我想使用的数据位于当前目录的子目录 data 中,文件名为 fruit.csv

一旦数据被读取到 DataFrame 中,你可以预览数据:

df.head()

这将显示数据框的前五行预览。

图 3.2 – 简单的 pandas DataFrame

图 3.2 – 简单的 pandas DataFrame

你还可以将其他数据集读取到 pandas 中。例如,如果你已经安装了 scikit-learn,你可以执行如下操作:

from sklearn import datasets
iris = datasets.load_iris()
df = pd.DataFrame(iris['data'], columns=iris['feature_names'])
df.head()

这给我们展示了 图 3.3

图 3.3 – iris 数据集的 pandas DataFrame

图 3.3 – iris 数据集的 pandas DataFrame

pandas 不仅仅可以从 CSV 文件读取数据。它非常多才多艺,而且比其他加载 CSV 文件的方法更不容易出错。例如,我常常发现 pandas 可以毫无问题地处理这些数据。

什么是 Spark?

Spark 是一种常用于处理大量数据的技术。Spark 对于“大数据”工作负载非常有用。到了一定程度,庞大的数据量可能会超出像 pandas 这样的工具的处理能力,了解像 Spark 这样的更强大工具是非常有用的。

这里是一些其他的 pandas read_* 函数:

  • pd.read_clipboard()

  • pd.read_excel()

  • pd.read_feather()

  • pd.read_fwf()

  • pd.read_gbq()

  • pd.read_hdf()

  • pd.read_html()

  • pd.read_json()

  • pd.read_orc()

  • pd.read_parquet()

  • pd.read_pickle()

  • pd.read_sas()

  • pd.read_spss()

  • pd.read_sql()

  • pd.read_sql_query()

  • pd.read_sql_table()

  • pd.read_stata()

  • pd.read_table()

  • pd.read_xml()

这些看起来有点多,我个人并没有全部记住。我通常只使用其中的一小部分,比如 read_csvread_jsonread_parquet,但在紧急情况下,read_html 偶尔也会很有用。

对我而言,EDA(探索性数据分析)和简单的可视化是我迷上 pandas 的原因。例如,如果你想绘制直方图,可以这样做:

df['petal width (cm)'].plot.hist()

图 3.4 – 花瓣宽度直方图

图 3.4 – 花瓣宽度直方图

pandas能做的远不止我展示的内容。关于使用pandas进行数据分析的博客文章和书籍不计其数。专门讲解如何使用pandas的书籍也写了很多。我鼓励你买几本书,尽可能多地了解这个库。你对pandas了解得越多,你在处理数据时就会越得心应手。

文档

你可以通过访问这个网站来了解更多关于pandas的信息:pandas.pydata.org/docs/getting_started/overview.html

你可以将本书作为参考,以便更熟练地使用pandas,但也有许多在线指南可以提供动手练习,例如这个:pandas.pydata.org/docs/getting_started/intro_tutorials/01_table_oriented.html

NumPy

pandas非常适合对 DataFrame 进行数据分析,NumPy则更为通用,包含了各种各样的数学函数和转换。

那它和pandas有什么区别呢?这是一个很好的问题,因为这两个库通常是一起使用的。你在使用pandas时,通常也会看到NumPy。经过几年同时使用这两个库,我通常会先在pandas中尝试做某件事,发现做不到,然后发现NumPy可以做到。这两个库很好地协作,它们满足不同的需求。

使用pandas时,第一次读取 CSV 文件并绘制直方图总是令人兴奋。而使用NumPy时,没有什么特别令人兴奋的时刻,它只是非常有用。它是一个强大而多功能的工具,当pandas做不到你需要做的事情时,它在紧急情况下能为你省下 9 次中的 10 次。

安装

pandas一样,如果你在笔记本环境中工作,NumPy可能已经安装好了。不过,如果你需要跟随步骤操作,可以参考官方安装指南:numpy.org/install/

启动功能

如果你想玩一玩NumPy,感受一下它能做什么,可以查看他们的快速入门教程,比如numpy.org/doc/stable/user/quickstart.html。在教程中,他们解释了一些基本操作,比如生成随机数、重塑数组、打印数组等等。

文档

你可以在numpy.org/doc/stable/上了解更多关于NumPy的信息。在本书中,当pandas或其他库无法完成我们需要做的事情时,我们会在紧急情况下依赖NumPy,正如我之前描述的那样。通常,NumPy中有一个简单的解决方案。因此,注意我们对NumPy的使用,并了解其过程。

让我们从分析和处理转向我们将用来可视化数据的库。

数据可视化

有几个 Python 库可以用来进行数据可视化。Matplotlib 是一个很好的起点,但其他库,如 Seaborn 可以生成更具吸引力的可视化,而 Plotly 则可以生成交互式可视化。

Matplotlib

Matplotlib 是一个用于数据可视化的 Python 库。就是这样。如果你有数据,Matplotlib 很可能可以用来可视化它。该库已直接集成到 pandas 中,所以如果你使用 pandas,你很可能也在使用 Matplotlib

Matplotlib 的学习曲线非常陡峭,根本不直观。我认为如果你在学习 Python 数据科学,它是一个必需的恶魔。不管我用 Matplotlib 做多少数据可视化,它都永远不会变得容易,而且我记住的也很少。我这么说并不是为了贬低 Matplotlib,而是让你知道,如果你在使用这个库时感到挣扎,不要对自己感到沮丧。我们都在和这个库斗争。

设置

pandas 以及 NumPy 一样,如果你在一个笔记本环境中工作,Matplotlib 很可能已经安装好了。然而,如果你需要按照步骤进行,你可以按照官方安装指南操作:matplotlib.org/stable/users/installing/

启动功能

如果你使用 pandas 进行数据可视化,那么你已经在使用 Matplotlib 了。例如,在 pandas 的讨论中,我们使用了以下代码来绘制直方图:

df['petal width (cm)'].plot.hist()

像条形图这样的简单可视化可以很容易地完成。它们最直接地在 pandas 中完成。

例如,你可以创建一个水平条形图:

import matplotlib.pyplot as plt
 df['petal width (cm)'].value_counts().plot.barh(figsize=(8,6)).invert_yaxis()

这将渲染一个非常简单的数据可视化。

图 3.5 – 花瓣宽度水平条形图

图 3.5 – 花瓣宽度水平条形图

文档

Matplotlib 的教程部分可能是开始学习的最佳地方:matplotlib.org/stable/tutorials/

然而,在本书中,我们将大量使用 pandasMatplotlib,所以你应该通过跟随和练习,掌握相当多的知识。

Seaborn

Seaborn 本质上是“美化版的 Matplotlib。”它是 Matplotlib 的扩展,能够生成更具吸引力的数据可视化。缺点是,一些在 Matplotlib 中完全不直观的东西,在 Seaborn 中变得更不直观。它有着像 Matplotlib 一样的学习曲线,而且学习永远不会记住。我发现每次需要制作可视化时,我都在谷歌搜索。

然而,一旦你开始弄清楚事情,它也没有那么糟。你会开始记得你克服的困难,并且处理那些应该很简单的难题(比如调整可视化的大小)随着时间的推移变得稍微不那么烦人。

这些可视化效果美观,远胜于 Matplotlib 的默认设置。浏览 Seaborn 可视化示例目录非常有趣:seaborn.pydata.org/examples/.

我们在本书中偶尔会使用 Seaborn,但大多数情况下会使用 Matplotlibscikit-network 来进行可视化。不过,还是要了解这个库,它非常重要。

设置

我不记得笔记本是否已经安装了 Seaborn。如果现在已经安装了,我也不会感到惊讶。如果你需要安装 Seaborn,可以参考官方安装指南:seaborn.pydata.org/installing.html

启动功能

Seaborn 能做 Matplotlib 可以做的事,但做得更好、更漂亮。例如,让我们重新做一下之前创建的直方图,并加上 核密度估计KDE):

import seaborn as sns
sns.histplot(df['petal width (cm)'], kde=True)

这将生成以下图表:

图 3.6 – 带有 KDE 的 Seaborn 花瓣宽度直方图

图 3.6 – 带有 KDE 的 Seaborn 花瓣宽度直方图

学习 Seaborn 最好的方式就是直接上手。获取一些数据,然后尝试将其可视化。浏览他们的示例画廊,找一些与你的数据相符的例子,然后尝试构建相同的内容。最开始可以复制他们的代码,并尝试调整参数。

文档

你可以通过他们的网站了解更多关于 Seaborn 的信息:seaborn.pydata.org/

Plotly

说实话,我们在本书中不会经常使用 Plotly,但是有一章需要使用交互式散点图,Plotly 提供了一个非常有用的可视化工具。不过,了解 Plotly 还是很有用的,它在需要简单交互的数据可视化时可以成为一个强大的工具。

Plotly 宣传称其可以制作交互式、出版质量的图表。仅此而已。它本质上做的是 MatplotlibSeaborn 所做的工作,但以交互的方式进行。将交互功能添加到 Matplotlib 是可能的,但过程比较痛苦。使用 Plotly,则既简单又快速,而且过程充满乐趣。

设置

若要安装 Plotly,请参考其官方指南:plotly.com/python/getting-started/#installation

启动功能

他们的网站有一个非常有趣和互动的画廊,可以激发创意。我建议你先阅读他们的入门指南,探索他们的示例:plotly.com/python/getting-started/

我们将在 sw 中使用 Plotly《网络数据的无监督机器学习》

图 3.7 – Plotly 交互式散点图

图 3.7 – Plotly 交互式散点图

文档

你可以在他们的网站上学到更多关于 Plotly 的知识:plotly.com/python/

现在我们已经介绍了一些用于数据可视化的有用库,接下来我们将做同样的事情,针对自然语言处理(NLP)和文本处理。

自然语言处理(NLP)

从数据可视化转到 NLP,仍有几个 Python 库会对我们处理文本数据有所帮助。每个库都有独特的功能、优点和缺点,文档需要仔细阅读。

自然语言工具包

NLTK是一个较老的 Python 库,通常对于曾经用NLTK完成的任务(例如命名实体识别或词性标注),使用spaCy等其他库会更好。

然而,仅仅因为它较老并不意味着它已经过时。它仍然非常适合分析文本数据,并且提供了比spaCy等库更多的语言学功能。

简而言之,NLTK是做 Python NLP 的基础性库,你不应跳过它,去直接使用更新的库和方法。

设置

安装NLTK非常简单。只需按照指南操作:www.nltk.org/install.html

启动功能

在本书中,我们将大量使用NLTK。当你准备好时,跳入下一章,我们会立刻开始。作为入门功能,并没有什么特别值得展示的。随着学习的深入,NLP 变得更为有趣。起初,它至少有点让人困惑。

文档

NLTK网站作为学习工具并不太有用,但你可以在这里看到一些代码片段和示例:www.nltk.org

如今,你很可能通过博客文章(TowardsDataScience等)了解NLTK,而非通过官方NLTK网站。如果你想从其他来源学习,可以搜索“nltk getting started”,会找到一些有用的指南。

本书将足以帮助你入门NLTK,但我确实建议向其他撰写关于 NLP 文章的作者学习。NLP 有太多内容值得学习和探索。

spaCy

NLTK。例如,如果你正在做命名实体识别、词性标注或词形还原,我建议你使用spaCy,因为它更快捷且简单,能够获得较好的结果。然而,NLTK的数据加载器、停用词、分词器和其他语言学工具依然非常有用,而且有时候spaCy无法匹敌。个人认为,如果你从事 Python 中的 NLP 工作,应该同时学习NLTKspaCy,而不仅仅是spaCy。了解两者,并记住spaCy在某些方面比NLTK更强。

设置

要安装spaCy,请使用官方安装指南:spacy.io/usage#quickstart

启动功能

在本书中,你将看到spaCy的使用,尤其是在命名实体识别方面,这对图谱构建非常有用。如果你想看到它的实际应用,可以跳到后面。

例如,让我们安装语言模型,如下所示:

!python -m spacy download en_core_web_sm

我们可以运行这段代码:

import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("David Knickerbocker wrote this book, and today is September 4, 2022.")
for ent in doc.ents:
    print(ent.text, ent.label_)

这将输出一些命名实体识别(NER)结果:

David Knickerbocker PERSON
today DATE
September 4, 2022 DATE

你可能会想,它是怎么做到的呢?是魔法!开个玩笑——其实是机器学习。

文档

NLTK不同,spaCy的文档非常详尽且有帮助,而且看起来现代化。我建议你花大量时间在他们的网站上,了解它的功能。spaCy能做的事情远比本书所涵盖的要多。要了解更多内容,请访问spacy.io/usage/linguistic-features

网络分析与可视化

我们接下来要使用的几种库对于分析和可视化各种类型的网络很有用。网络可视化是数据可视化的另一种形式,但专门针对网络。没有网络可视化软件,创建有用的网络可视化是非常困难和繁琐的。

NetworkX

NetworkX,没有比这更好的资源来进行 Python 中的网络分析了。

还有其他库可以用于非常简单的网络可视化。在本书中,我忽略了这些,并展示了如何使用NetworkXscikit-network进行良好的网络分析并制作出相对美观的网络可视化。

还有其他一些库可以与NetworkX一起使用,用于社区检测或分析网络的脆弱性等,但NetworkX是核心。我写这本书的目的是传播网络科学的知识以及在软件开发中使用网络的理念,而NetworkX对于这一目标至关重要。我希望更多的软件工程师能够掌握这些技能。

设置

请按照官方指南安装NetworkXnetworkx.org/documentation/stable/install.html

启动功能

我们将用NetworkX做很多非常酷的事情,但如果你想确认它是否正常工作,你可以运行这段代码:

import networkx as nx
G = nx.les_miserables_graph()
sorted(G.nodes)[0:10]

在一本笔记本中,这应该显示一个 Python 列表,包含悲惨世界图中的前 10 个节点:

['Anzelma',
 'Babet',
 'Bahorel',
 'Bamatabois',
 'BaronessT',
 'Blacheville',
 'Bossuet',
 'Boulatruelle',
 'Brevet',
 'Brujon']

或者,你也可以这样做,快速获取悲惨世界图的概览:

print(nx.info(G))
Graph with 77 nodes and 254 edges

文档

学习NetworkX的最佳方式可能就是阅读这本书。关于如何使用NetworkX的书籍非常少。也有一些博客文章,NetworkX网站上的一些指南也很有帮助。这里有一个,但它假设你已经理解了图的概念:networkx.org/documentation/stable/tutorial.html

该网站确实有非常详细的文档,介绍了不同工具的使用:networkx.org/documentation/stable/reference/

我最喜欢他们在线资源的一部分是,他们会指向关于已实现功能的学术论文。例如,如果你访问链接预测部分中的优先附加页面,你会看到这个块:

图 3.8 – NetworkX 参考

图 3.8 – NetworkX 参考

在多次场合中,我很喜欢深入研究开发几个算法背后的编写工作。了解背景是很有意义的,算法绝对不应该被盲目使用。如果你要使用一个工具,了解它的工作原理,以及它在哪些方面可能不起作用。

scikit-network

NetworkX的可视化速度很慢,以至于我不会教它们。即使在小型网络上,它们的渲染也需要很长时间,而且其外观单调且基础。另一方面,scikit-network的可视化加载非常快速,因为它们是以 SVG 格式渲染的。即使你有成百上千个节点,加载速度也很合理。

然而,将其视为一个用于分析大规模图形的包并不准确。从本质上讲,这是一款可视化软件,尽管它的功能稍微超出了这个范围。看起来他们已经加入了一些额外的功能(例如图神经网络GNNs)),但大多数文档页面还是侧重于可视化。不过,还是可以看看他们在嵌入和社区检测方面的工作。似乎他们正在朝着超越网络可视化的方向发展,这个库也在不断演化。

设置

要安装scikit-network,请按照官方指南操作:scikit-network.readthedocs.io/en/latest/first_steps.html

启动功能

在本书中,我们将仅使用scikit-network进行网络可视化,因此如果你想开始使用它,请查看他们的一些教程,例如scikit-network.readthedocs.io/en/latest/tutorials/visualization/graphs.html。有多个教程可以帮助你深入了解这个库。

文档

scikit-network的文档非常好且组织得很有条理。文档的简洁性表明它实际上并不是一个非常深奥的 Python 库。如果你想深入了解,先从这里开始:scikit-network.readthedocs.io/en/latest/

你可能会想,既然scikit-network还提供一些机器学习、社区检测和其他功能,为什么我只用它来做可视化呢?原因是这样的:NetworkXKarateClub也提供类似的功能,而且它们做得更好。例如,KarateClub提供的功能更强,而NetworkX提供的scikit-network功能也能很好地实现,而且更干净。

这就是为什么我只展示它在可视化方面的使用。其他功能并未表现得比NetworkXKarateClub提供的更好。这并非针对个人。每个人都有自己喜欢的工具。

在接下来的章节中,我们将讨论可以用于机器学习的库。

机器学习

Python 还有一组用于机器学习的库。了解 Python 库和算法是不够的。请尽可能多地了解你将使用的模型和技术。总是有更多东西可以学习。以下是你可能经常使用的一些库。

scikit-learn

TensorFlowPyTorch,但我们在本书中不会直接使用它们。只需知道它们的存在,并且随着你的机器学习技能的提高,你应该了解它们。

如果你正在学习或使用 Python 进行机器学习,你很快就会接触到scikit-learnscikit-learn 提供了大量的机器学习模型、数据加载器、预处理器、转换器等等。有很多关于scikit-learn的书籍和博客文章,包括一些由 Packt 出版的。如果你有兴趣学习机器学习,你会阅读很多内容,而scikit-learn是一个很好的起点。

设置

如果你在使用笔记本,那么scikit-learn可能已经安装好了。但是,如果你需要安装它,请按照官方指南操作:scikit-learn.org/stable/install.html

初学者功能

机器学习很复杂,构建和使用模型需要多个步骤。现在,只需确保安装了scikit-learn并且能够查看运行的版本:

import sklearn
sklearn.__version__

对我来说,这表明我正在运行版本1.0.2。对于本书的练习,我们不需要精确匹配,但是在实际部署中,所有库的版本应保持一致以确保可复现性。

文档

关于scikit-learn的资源比我在本章提到的其他每个 Python 库的资源都要多。机器学习是一个热门话题,对书籍、博客文章、视频和其他教程的需求很大,因此学习资源丰富。然而,scikit-learn文档是开始学习的地方。

scikit-learn入门指南可以在这里找到:scikit-learn.org/stable/getting_started.html

完整的用户指南可以在scikit-learn.org/stable/user_guide.html找到,并且还有一个 API 参考文档在scikit-learn.org/stable/modules/classes.html

在本书中,我们将进行一些监督和无监督的机器学习,但不会深入讨论。如果你有兴趣,还有很多东西可以学习。本书只是浅尝辄止地介绍了机器学习,希望能引发你的兴趣。

空手道俱乐部

NetworkX。换句话说,Karate Club弥合了机器学习和图形分析之间的差距。它提供了几种有用的无监督学习模型,可用于社区检测以及从图形中创建节点嵌入,这些嵌入可以用于下游的有监督学习任务。这通常被称为图形机器学习,且目前人们对将图形和机器学习结合使用非常感兴趣,例如预测新连接(谁应该成为朋友,谁会喜欢某种音乐或产品等)。

这和scikit-learn有什么区别?scikit-learn可以用于任何数字数据,而Karate Club专注于通过无监督机器学习将图形转换为数字数据。Karate Club的输出可以作为scikit-learn的输入,但反过来却很少。

要使Karate Club有用,你需要图形(NetworkX)和机器学习(scikit-learnTensorFlowPyTorch等)。Karate Club本身的用途有限。

Karate Club尚不支持更高版本的 Python,如 3.10。

设置

要安装Karate Club,请按照官方指南:karateclub.readthedocs.io/en/latest/notes/installation.html

入门功能

没有值得展示的入门功能。机器学习很复杂,需要几个步骤才能真正有用。现在,我们只需要验证库是否已安装,并且可以看到版本号:

import karateclub
karateclub.__version__

我可以看到我正在运行的版本是1.3.0

文档

Karate Club提供了一个入门指南:karateclub.readthedocs.io/en/latest/notes/introduction.html。在这里,你可以开始了解如何使用该库。你还可能会注意到一些对其他库的引用,如scikit-learn

Karate Club拥有出色的文档。你可以在karateclub.readthedocs.io/en/latest/modules/root.html找到它。我最喜欢的一点是,他们会引用关于这些模型的期刊文章。例如,如果你查看任何算法的文档,通常会找到提到关于该模型的论文。

图 3.9 – 空手道俱乐部文档参考

图 3.9 – 空手道俱乐部文档参考

在本书中,我查看了多个Karate Club模型,因此有机会阅读许多相关的论文,这也给了我很多小时的乐趣。我非常喜欢这个。

如果你在代码中使用Karate Club,你也可以在笔记本中看到提到这篇论文的内容,只需在函数调用时按下Shift + Tab

图 3.10 – 空手道俱乐部代码参考

图 3.10 – 空手道俱乐部代码参考

这个库不仅使用起来令人兴奋,学习它也具有很高的教育价值。

spaCy(再探)

是的,我又提到spaCy了。为什么?因为spaCy提供了多个语言模型,而且涵盖了多种语言。这意味着你可以将spaCy预训练的机器学习模型用于自己的目的,例如用于命名实体识别。它们的模型已经过训练,可以直接导入并使用。在本书的接下来的几章中,你将学习如何做到这一点。

总结

本书中还会使用其他一些 Python 库,但它们将在相关章节中进行解释。在本章中,我想描述我们将用于工作的主要库。为了能够进入本书讨论的真正实验性内容,我们需要打下基础。

例如,你需要能够读取和分析表格(结构化)数据。你还需要能够将数据可视化。对于文本,你需要能够将文本转换为适合分析和使用的格式。对于图表,你也需要具备相同的能力。最后,如果你想将机器学习应用于网络或文本,你应该理解如何做到这一点。

这就是为什么本节被分解为数据分析与处理、数据可视化、自然语言处理、网络分析与可视化以及机器学习。我希望这样的结构能有所帮助。

在安装并简要解释了这些库之后,我们现在可以开始深入探索网络科学、社交网络分析和自然语言处理,所有这些都将在 Python 中进行。

第二部分:图表构建与清理

网络数据并不总是存在于分析师感兴趣的研究中。本部分的章节展示了如何从各种在线资源中提取文本数据,并将其转换为可以分析的网络。这些章节还介绍了数据处理步骤,用于清理网络,以便进行分析和后续处理。

本节包括以下章节:

  • 第四章**,自然语言处理与网络协同作用

  • 第五章**,更简单的网页抓取

  • 第六章**,图表构建与清理

第四章:NLP 和网络的协同作用

在前几章中,我们讨论了自然语言处理NLP)、网络分析以及在 Python 编程语言中用于这两者的工具。我们还讨论了用于进行网络分析的非编程工具。

在本章中,我们将把所有这些知识付诸实践。我希望能解释通过结合 NLP 和网络分析所揭示的强大功能和洞察力,这也是本书的主题。在后续章节中,我们将继续围绕这一主题展开讨论,同时还会涉及其他用于处理 NLP 和网络的工具,如无监督和监督的机器学习。本章将展示确定文本片段所讲述的对象或内容的技术。

本章将涵盖以下主题:

  • 为什么我们在一本网络书中学习 NLP?

  • 提出问题讲述故事

  • 介绍网页抓取

  • 在库、API 和源数据之间进行选择

  • 使用自然语言工具包(NLTK)库进行词性标注(PoS)

  • 使用 spaCy 进行词性标注和命名实体识别(NER)

  • 将实体列表转换为网络数据

  • 将网络数据转换为网络

  • 做网络可视化的抽查

技术要求

在本章中,我们将使用几种不同的 Python 库。在每一节中都会列出pip install命令来安装每个库,因此只需要跟着做,按需进行安装。如果遇到安装问题,通常可以在 Stack Overflow 上找到答案。用 Google 搜索错误信息!

在开始之前,我想解释一件事,这样我们使用的库的数量就不会显得那么令人不知所措。重要的是我们使用每个库的理由。

本书的大部分内容将会做三件事之一:网络分析、网络可视化,或使用网络数据进行机器学习(也称为 GraphML)。

每当我们处理网络数据时,我们都会使用NetworkX来操作它。

每当我们进行分析时,可能会使用pandas

关系看起来是这样的:

  • NetworkX

  • pandas

  • scikit-network

  • scikit-learnKarate Club

看看最开始的几个词。如果我要进行网络分析,我会使用NetworkXpandas。如果我要做网络可视化,我会使用NetworkXscikit-network。如果我要用网络数据做机器学习,我会使用NetworkXscikit-learn,可能还会使用Karate Club

这些是本章中将使用的核心库。

此外,你必须随时准备好draw_graph() 函数的代码,因为你将在本书中多次使用它。那段代码有点复杂,因为在写作时它确实需要这样做。不幸的是,NetworkX在网络可视化方面并不优秀,而scikit-network在网络构建或分析方面也不够强大,所以我将两者结合起来进行可视化,这效果很好。我希望这本书的后续版本中,网络可视化能够得到改进和简化。

所有必要的代码可以在github.com/PacktPublishing/Network-Science-with-Python找到。

为什么我们在一本网络书中学习 NLP?

我在第一章的介绍中简要回答了这个问题,但值得更详细地重复一遍。许多从事文本分析工作的人都知道情感分析和文本分类。文本分类是预测一段文本是否可以被归类为某种类型的能力。例如,我们来看这个字符串:

你今天 怎么样?

我们能从这段字符串中得出什么结论?这是一个问题还是陈述?这是一个问题。这个问题是问谁的?是问你。这个问题中有积极、消极还是中立的情绪?我觉得它看起来是中立的。我们再试试另一段字符串。

Jack 和 Jill 爬上了山坡,但 Jack 跌倒了,因为他是 个傻瓜。

这是关于 Jack 和 Jill 的陈述,其中 Jack 被称为傻瓜,这并不是个很友善的说法。然而,这个侮辱似乎是开玩笑写的,因此不清楚作者写的时候是生气还是在笑。我可以确认,在写这句话时我是在笑的,所以其中包含了积极的情绪,但文本分类可能难以识别这一点。我们再试试另一个。

我这一生从未像现在这样愤怒过! 真是愤怒到了极点!

作者表达了非常强烈的负面情绪。情感分析和文本分类很容易识别这一点。

情感分析是一种使用算法自动检测文本或转录记录中嵌入的情绪的技术集合。文本分类使用相同的算法,但其目标不是识别情绪,而是识别主题。例如,文本分类可以检测文本中是否存在侮辱性语言。

这不是关于情感分析或文本分类的章节。本章节的目的是解释如何自动提取文本中存在的实体(人物、地点、事物等),以便识别和研究文本中描述的社交网络。然而,情感分析和文本分类可以与这些技术结合,为社交网络提供更多上下文,或者用于构建专门的社交网络,例如友谊网络。

提出问题来讲述故事

我从讲故事的角度进行工作;我让我的故事决定工作,而不是反过来。例如,如果我开始一个项目,我会考虑,甚至写下关于谁、什么、哪里、何时、为什么和如何的一系列问题:

  • 我们有哪些数据?足够吗?

  • 我们从哪里获得更多数据?

  • 我们如何获得更多数据?

  • 什么阻碍了我们获取更多数据?

  • 我们多久需要更多数据?

但这是一个不同类型的项目。我们想要深入了解一篇文本的内容,而不仅仅是通过阅读来获得信息。即使读完一本书后,大多数人也无法记住文本中描述的关系,而且即使记得,也可能是错误的回忆。但我们应该有这样的疑问:

  • 文中提到了谁?

  • 他们认识谁?

  • 他们的对手是谁?

  • 这篇文章的主题是什么?

  • 存在哪些情感?

  • 文中提到了哪些地方?

  • 这件事发生在什么时候?

在开始任何形式的分析之前,制定一套可靠的问题非常重要。深入分析时,你会遇到更多的问题。

本章将为你提供工具,自动调查所有这些问题,除了关于主题和对手的问题。本章的知识,再加上对文本分类和情感分析的理解,将使你能够回答所有这些问题。这就是本章的“为什么”。我们希望自动提取文中提到的人、地点,甚至可能还有一些事物。大多数时候,我只想要人物和地点,因为我想研究社交网络。

然而,重要的是要解释清楚,这对于辅助文本分类和情感分析非常有用。例如,如果你不知道正在评审的内容,正面的亚马逊或 Yelp 评论几乎没有什么意义。

在我们进行任何揭示文本中存在的关系的工作之前,我们需要获取一些文本。作为练习,我们有几个选择。我们可以使用像 NLTK 这样的 Python 库加载它,我们可以使用 Twitter(或其他社交网络)库收集它,或者我们可以自己抓取它。即使是抓取也有不同的方式,但我只会解释一种方法:使用 Python 的BeautifulSoup库。只要知道有很多选择,但我喜欢BeautifulSoup的灵活性。

在本章中,演示将使用从互联网上抓取的文本,你可以对代码做些小修改,以适应你自己的网页抓取需求。

介绍网页抓取

首先,什么是 网页抓取,谁能做呢?任何具有编程技能的人都可以使用几种不同的编程语言进行抓取,但我们将使用 Python。网页抓取是从网络资源中获取内容的行为,您可以将数据用于您的产品和软件。您可以使用抓取来获取网站没有通过数据馈送或 API 暴露的信息。但有一个警告:不要抓取得过于激进,否则您可能会通过意外的 拒绝服务攻击DoS)击垮一个网站服务器。只获取您需要的内容,按需获取。慢慢来,不要贪婪或自私。

介绍 BeautifulSoup

BeautifulSoup 是一个强大的 Python 库,用于抓取您可以访问的任何在线内容。我经常用它从新闻网站收集故事的 URL,然后我抓取这些 URL 的文本内容。我通常不需要实际的 HTML、CSS 或 JavaScript,所以我会渲染网页,然后抓取内容。

BeautifulSoup 是一个重要的 Python 库,如果您打算进行网页抓取,了解它非常重要。虽然 Python 还有其他网页抓取选项,但 BeautifulSoup 是最常用的。

没有什么比看到 BeautifulSoup 在实际操作中的效果更能解释它的了,所以让我们开始吧!

使用 BeautifulSoup 加载和抓取数据

在这个实践演示中,我们将看到三种不同的加载和抓取数据的方式。它们各自独立都有用,但也可以以多种方式结合使用。

首先,最简单的方法是使用一个库,一次性获取您想要的内容,且需要最少的清理。多个库,如 pandasWikipediaNLTK 都有加载数据的方法,让我们从这些开始。由于本书主要讲解如何从文本中提取关系并进行分析,因此我们需要文本数据。我将演示几种方法,然后描述每种方法的优缺点。

Python 库 – Wikipedia

有一个强大的库可以从维基百科中提取数据,叫做 Wikipedia。您可以通过运行以下命令安装它:

pip install wikipedia

安装完成后,您可以像这样将其导入到代码中:

import wikipedia as wiki

一旦导入,您就可以通过编程方式访问维基百科,允许您搜索并使用任何您感兴趣的内容。由于在本书中我们将进行大量的社交网络分析,让我们看看 Wikipedia 对该主题有什么内容:

search_string = 'Social Network Analysis'
page = wiki.page(search_string)
content = page.content
content[0:680]

最后一行,content[0:680],仅显示 content 字符串中的内容,直到第 680 个字符,这是以下代码中展示的句子的结束部分。680 之后还有很多内容。我选择在此演示中截断它:

'Social network analysis (SNA) is the process of investigating social structures  through the use of networks and graph theory. It characterizes networked structures in terms of nodes (individual actors, people, or things within the network) and the ties, edges, or links (relationships or interactions) that connect them.  Examples of social structures commonly visualized through social network analysis include social media networks, memes spread, information circulation, friendship and acquaintance networks, business networks, knowledge networks, difficult working relationships, social networks, collaboration graphs, kinship, disease transmission, and sexual relationships.'

通过几行代码,我们能够直接从维基百科中提取文本数据。我们能看到那个维基百科页面上有什么链接吗?是的,我们可以!

links = page.links
links[0:10]

我使用方括号[,只选择 links 中的前 10 项:

['Actor-network theory',
 'Adjacency list',
 'Adjacency matrix',
 'Adolescent cliques',
 'Agent-based model',
 'Algorithm',
 'Alpha centrality',
 'Anatol Rapoport',
 'Anthropology',
 'ArXiv (identifier)']

如果我们仅选择前 10 个链接并只在 A 链接中,那么我认为可以安全地说,可能还有更多。维基百科上关于社交网络分析的内容非常丰富!这真的很简单!让我们继续尝试另一种便捷的抓取方法:pandas库。

Python 库 – pandas

如果你有兴趣使用 Python 进行数据科学,你将不可避免地学习和使用pandas。这个库在处理数据时非常强大和多功能。在这个演示中,我将用它从网页中提取表格数据,但它可以做更多的事情。如果你对数据科学感兴趣,尽可能多地学习关于pandas的知识,并熟悉它的使用。

pandas可以从网页中提取表格数据,但它对原始文本的处理能力较弱。如果你想从维基百科加载文本,应该使用之前提到的Wikipedia库。如果你想从其他网页提取文本,则应该同时使用RequestsBeautifulSoup库。

让我们使用pandas从维基百科提取一些表格数据。首先,尝试使用pandas抓取同一个维基百科页面,看看我们能得到什么。如果你已经在电脑上安装了 Jupyter,那么很可能你已经安装了pandas,所以让我们直接进入代码:

  1. 首先导入pandas库:

    import pandas as pd
    
    url = 'https://en.wikipedia.org/wiki/Social_network_analysis'
    
    data = pd.read_html(url)
    
    type(data)
    

在这里,我们导入了pandas库并给它起了一个简短的名字,然后使用pandas读取了关于社交网络分析的同一维基百科页面。我们得到了什么?type(data)显示了什么?我预计会得到一个pandas DataFrame。

  1. 输入以下代码。只需输入data并运行代码:

    data
    

你应该看到我们得到了一个列表。在pandas中,如果你执行read操作,通常会得到一个 DataFrame,那么为什么我们得到了一个列表呢?这是因为该页面上有多个数据表,因此pandas返回了所有表格的 Python 列表。

  1. 让我们检查一下列表的元素:

    data[0]
    

这应该会给我们一个来自维基百科页面的第一个表格的pandas DataFrame:

图 4.1 – pandas DataFrame 显示维基百科数据的第一个元素

图 4.1 – pandas DataFrame 显示维基百科数据的第一个元素

这些数据看起来有点问题。为什么我们在表格中看到一堆文本?为什么最后一行看起来像是一堆代码?下一个表格是什么样子的呢?

data[1]

这将给我们一个来自同一维基百科页面的第二个表格的pandas DataFrame。请注意,维基百科页面偶尔会被编辑,因此您的结果可能会有所不同:

图 4.2 – pandas DataFrame 显示维基百科数据的第二个元素

图 4.2 – pandas DataFrame 显示维基百科数据的第二个元素

糟糕。这看起来更糟。我之所以这么说,是因为似乎有一些非常短的字符串,看起来像是网页部分。这看起来不太有用。记住,pandas非常适合加载表格数据,但对于维基百科使用的文本数据表格并不适用。我们已经看到,这比我们通过维基百科库轻松捕获的数据更没用。

  1. 让我们尝试一个包含有用数据的页面。这个页面包含关于俄勒冈州犯罪的表格数据:

    url = 'https://en.wikipedia.org/wiki/Crime_in_Oregon'
    
    data = pd.read_html(url)
    
    df = data[1]
    
    df.tail()
    

这将展示一个包含俄勒冈州犯罪数据的Pandas数据框架中的最后五行:

图 4.3 – Pandas 数据框架中的维基百科表格数值数据

图 4.3 – Pandas 数据框架中的维基百科表格数值数据

哇,这看起来是有用的数据。然而,数据中没有显示 2009 年以后的信息,这已经是相当久远的时间了。也许有一个更好的数据集,我们应该使用那个。无论如何,这显示了Pandas能够轻松地从网上抓取表格数据。然而,有几点需要注意。

首先,如果你在项目中使用抓取的数据,知道自己已经把自己交给了网站管理员的支配。如果他们决定丢弃数据表,或者重命名或重新排序列,那么直接从维基百科读取数据的应用程序可能会崩溃。你可以通过在抓取数据时保留本地数据副本以及在代码中加入错误处理来防范这种情况。

为最坏的情况做好准备。当你进行数据抓取时,要建立必要的错误检查机制,并且知道何时抓取程序无法再拉取数据。接下来我们将转向下一种方法——使用NLTK

Python 库 – NLTK

让我们直奔主题:

  1. 首先,NLTK 并不随 Jupyter 安装,因此你需要自行安装。你可以使用以下命令安装:

    pip install nltk
    
  2. Python 的NLTK库可以轻松地从古腾堡计划中提取数据,古腾堡计划是一个包含超过 60,000 本免费书籍的库。让我们看看我们能获取到什么:

    from nltk.corpus import gutenberg
    
    gutenberg.fileids()
    
    ['austen-emma.txt',
    
     'austen-persuasion.txt',
    
     'austen-sense.txt',
    
     'bible-kjv.txt',
    
     'blake-poems.txt',
    
     'bryant-stories.txt',
    
     'burgess-busterbrown.txt',
    
     'carroll-alice.txt',
    
     'chesterton-ball.txt',
    
     'chesterton-brown.txt',
    
     'chesterton-thursday.txt',
    
     'edgeworth-parents.txt',
    
     'melville-moby_dick.txt',
    
     'milton-paradise.txt',
    
     'shakespeare-caesar.txt',
    
     'shakespeare-hamlet.txt',
    
     'shakespeare-macbeth.txt',
    
     'whitman-leaves.txt']
    

如你所见,结果远远少于 60,000 个,因此我们通过这种方式能获取的内容有限,但这仍然是练习自然语言处理(NLP)有用的数据。

  1. 让我们看看blake-poems.txt文件中有什么内容:

    file = 'blake-poems.txt'
    
    data = gutenberg.raw(file)
    
    data[0:600]
    
    '[Poems by William Blake 1789]\n\n \nSONGS OF INNOCENCE AND OF EXPERIENCE\nand THE BOOK of THEL\n\n\n SONGS OF INNOCENCE\n \n \n INTRODUCTION\n \n Piping down the valleys wild,\n   Piping songs of pleasant glee,\n On a cloud I saw a child,\n   And he laughing said to me:\n \n "Pipe a song about a Lamb!"\n   So I piped with merry cheer.\n "Piper, pipe that song again;"\n   So I piped: he wept to hear.\n \n "Drop thy pipe, thy happy pipe;\n   Sing thy songs of happy cheer:!"\n So I sang the same again,\n   While he wept with joy to hear.\n \n "Piper, sit thee down and write\n   In a book, that all may read."\n So he vanish\'d'
    

我们可以加载整个文件。文件内容比较凌乱,包含了换行符和其他格式,但我们会清理这些内容。如果我们想要获取不在此列表中的其他书籍,而这些书籍位于包含 60,000 本书的完整库中,该怎么办呢?我们就没机会了吗?浏览网站时,我看到我可以阅读我最喜欢的书之一——弗朗茨·卡夫卡的《变形记》,网址是www.gutenberg.org/files/5200/5200-0.txt。让我们尝试获取这个数据,不过这次我们将使用 Python 的 Requests 库。

Python 库 – Requests

Requests库随 Python 预安装,所以你只需要导入它。Requests 用于从网页抓取原始文本,但它的功能不止如此。请研究这个库,了解它的更多功能。

重要提示

如果你使用这种方法,请注意不要过于激进。像这样一次加载一本书是可以的,但如果你尝试一次性下载太多书籍,或者在古腾堡项目上过于激烈地抓取所有书籍,你很可能会暂时被封禁 IP 地址。

在我们的演示中,首先导入库,然后从古腾堡提供的变形记中抓取原始文本:

import requests
url = 'https://www.gutenberg.org/files/5200/5200-0.txt'
data = requests.get(url).text
data
…
'The Project Gutenberg eBook of Metamorphosis, by Franz Kafka\r\n\r\nThis eBook is for the use of anyone anywhere in the United States and\r\nmost other parts of the world at no cost and with almost no restrictions\r\nwhatsoever. You may copy it, give it away or re-use it under the terms\r\nof the Project Gutenberg License included with this eBook or online at\r\nwww.gutenberg.org. If you are not located in the United States, you\r\nwill have to check the laws of the country where you are located before\r\nusing this eBook.\r\n\r\n** This is a COPYRIGHTED Project Gutenberg eBook, Details Below **\r\n**     Please follow the copyright guidelines in this file.     *\r\n\r\nTitle: Metamorphosis\r\n\r\nAuthor: Franz Kafka\r\n\r\nTranslator: David Wyllie\r\n\r\nRelease Date: May 13, 2002 [eBook #5200]\r\n[Most recently updated: May 20, 2012]\r\n\r\nLanguage: English\r\n\r\nCharacter set encoding: UTF-8\r\n\r\nCopyright (C) 2002 by David Wyllie.\r\n\r\n*** START OF THE PROJECT GUTENBERG EBOOK METAMORPHOSIS ***\r\n\r\n\r\n\r\n\r\nMetamorphosis\r\n\r\nby Franz Kafka\r\n\r\nTranslated by David Wyllie\r\n\r\n\r\n\r\n\r\nI\r\n\r\n\r\nOne morning, when Gregor Samsa woke from troubled dreams, he found\r\nhimself transformed in his bed into a horrible vermin…'

我将所有在“vermin”之后的文本剪切掉,目的是简要展示数据中的内容。就这样,我们得到了整本书的完整文本。正如在 NLTK 中一样,数据充满了格式和其他字符,因此这些数据需要清理才能变得有用。清理是本书中我将解释的每一部分的一个非常重要的环节。我们继续吧;在这些演示中,我会展示一些清理文本的方法。

我已经展示了从古腾堡或维基百科提取文本是多么简单。但这两者只是互联网中可供抓取的内容的一小部分。pandas可以从任何网页读取表格数据,但它是有限的。大部分网页内容并非完美格式化或干净的。如果我们想要设置爬虫从我们感兴趣的各种新闻网站上收集文本和内容怎么办?NLTK 无法帮助我们获取这些数据,而 Pandas 在返回的数据上也有限制。我们有哪些选择呢?我们看到Requests库能够拉取另一本通过 NLTK 无法获取的古腾堡书籍。我们能否像这样使用 requests 从网站抓取 HTML 呢?让我们尝试抓取一些来自日本冲绳的新闻吧!

url = 'http://english.ryukyushimpo.jp/'
data = requests.get(url).text
data
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r\n<html  dir="ltr" lang="en-US">\r\n\r\n<!-- BEGIN html head -->\r\n<head profile="http://gmpg.org/xfn/11">\r\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\r\n<title>Ryukyu Shimpo - Okinawa, Japanese newspaper, local news</title>…'

好了!我们刚刚从给定的 URL 加载了原始 HTML,我们可以对任何公开访问的网页进行相同的操作。但是,如果你认为古腾堡的数据已经很杂乱,那看看这个!我们在这个 HTML 中还能指望什么呢?更别提建立自动化工具来解析 HTML 并提取有用数据了。令人惊讶的是,答案是肯定的,我们可以感谢BeautifulSoup Python 库以及其他抓取库。它们为我们打开了一个数据的新世界。让我们看看如何利用BeautifulSoup从中提取内容。

Python 库 - BeautifulSoup

首先,BeautifulSoupRequests库一起使用。Requests 随 Python 预安装,但BeautifulSoup并没有,因此你需要安装它。你可以使用以下命令来安装:

pip install beautifulsoup4

BeautifulSoup有很多功能,所以请去探索这个库。但提取冲绳新闻网站上所有链接需要做什么呢?下面是你可以做的:

from bs4 import BeautifulSoup
soup = BeautifulSoup(data, 'html.parser')
links = soup.find_all('a', href=True)
links
[<a href="http://english.ryukyushimpo.jp">Home</a>,
 <a href="http://english.ryukyushimpo.jp">Ryukyu Shimpo – Okinawa, Japanese newspaper, local news</a>,
 <a href="http://english.ryukyushimpo.jp/special-feature-okinawa-holds-mass-protest-rally-against-us-base/">Special Feature: Okinawa holds mass protest rally against US base</a>,
 <a href="http://english.ryukyushimpo.jp/2021/09/03/34020/">Hirokazu Ueyonabaru returns home to Okinawa from the Tokyo Paralympics with two bronze medals in wheelchair T52 races, "the cheers gave me power"</a>,
 <a href="http://english.ryukyushimpo.jp/2021/09/03/34020/"><img alt="Hirokazu Ueyonabaru returns home to Okinawa from the Tokyo Paralympics with two bronze medals in wheelchair T52 races, "the cheers gave me power"" class="medium" src="img/RS20210830G01268010100.jpg"/> </a>…]

很简单!我这里只展示了前几个提取的链接。第一行导入了库,第二行设置了 BeautifulSoup 以便解析 HTML,第三行查找了所有包含 href 属性的链接,最后一行则显示了这些链接。我们抓取了多少个链接?

len(links)
…
277

你的结果可能会有所不同,因为该页面可能在这本书写完后进行了编辑。

在不到一秒钟的时间内,已经抓取了 277 个链接!让我们看看是否能清理它们并只提取 URL。我们不需要担心链接的文本内容。我们还应该将其转换为一个 URL 列表,而不是 <a> HTML 标签列表:

urls = [link.get('href') for link in links]
urls
…
['http://english.ryukyushimpo.jp',
 'http://english.ryukyushimpo.jp',
 'http://english.ryukyushimpo.jp/special-feature-okinawa-holds-mass-protest-rally-against-us-base/',
 'http://english.ryukyushimpo.jp/2021/09/03/34020/',
 'http://english.ryukyushimpo.jp/2021/09/03/34020/',
 'http://english.ryukyushimpo.jp/2021/09/03/34020/',
 'http://english.ryukyushimpo.jp/2021/09/03/34020/'…]

在这里,我们使用了列表推导式和 BeautifulSoup 来提取每个已抓取链接中的 href 值。我发现有些重复的链接,因此我们需要在最终存储结果之前去除它们。让我们看看是否丢失了原先的 277 个链接:

len(urls)
…
277

完美!我们选择其中一个,看看是否能够从页面中提取出原始文本,去掉所有 HTML。我们来试试这个我手动选择的 URL:

url = 'http://english.ryukyushimpo.jp/2021/09/03/34020/'
data = requests.get(url).text
soup = BeautifulSoup(data, 'html.parser')
soup.get_text()
…
"\n\n\n\n\nRyukyu Shimpo – Okinawa, Japanese newspaper, local news  » Hirokazu Ueyonabaru returns home to Okinawa from the Tokyo Paralympics with two bronze medals in wheelchair T52 races, "the cheers gave me power"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nHome\n\n\n\nSearch\n\n\n\n\n\n\n\n\n\n\nTuesdaySeptember 07,2021Ryukyu Shimpo – Okinawa, Japanese newspaper, local news\n\n\n\n\n\n\r\nTOPICS:Special Feature: Okinawa holds mass protest rally against US base\n\n\n\n\n\n\n\n\n\n\n\n\nHirokazu Ueyonabaru returns home to Okinawa from the Tokyo Paralympics with two bronze medals in wheelchair T52 races, "the cheers gave me power"…"

完成!我们已经从网页中捕获到了相当干净且可用的文本!这可以自动化,以便不断从任何感兴趣的网站抓取链接和文本。现在,我们已经拥有了进入本章有趣部分的基础:从文本中提取实体,然后利用实体来构建社交网络。到现在为止,应该显而易见,我们探索的所有选项都需要进行一些清理,所以我们现在就开始处理这一部分。要做到完美,你需要做的比我接下来要做的更多,但至少我们可以让它变得有些可用:

text = soup.get_text()
text[0:500]
…
'\n\n\n\n\nRyukyu Shimpo – Okinawa, Japanese newspaper, local news  » Hirokazu Ueyonabaru returns home to Okinawa from the Tokyo Paralympics with two bronze medals in wheelchair T52 races, "the cheers gave me power"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nHome\n\n\n\nSearch\n\n\n\n\n\n\n\n\n\n\nTuesdaySeptember 07,2021Ryukyu Shimpo – Okinawa, Japanese newspaper, local news\n\n\n\n\n\n\r\nTOPICS:Special Feature: Okinawa holds mass protest rally against US base\n\n\n\n\n\n\n\n\n\n\n\n\nHirokazu Ueyonabaru returns home t'

第一件引起我注意的事是文本中存在大量的文本格式和特殊字符。我们有几个选择。首先,我们可以把所有的换行符转换为空格。我们来看看效果如何:

text = text.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ').replace('\xa0', ' ')
text
…
"     Ryukyu Shimpo – Okinawa, Japanese newspaper, local news  » Hirokazu Ueyonabaru returns home to Okinawa from the Tokyo Paralympics with two bronze medals in wheelchair T52 races, "the cheers gave me power"                                                            Home    Search           TuesdaySeptember 07,2021Ryukyu Shimpo – Okinawa, Japanese newspaper, local news        TOPICS:Special Feature: Okinawa holds mass protest rally against US base             Hirokazu Ueyonabaru returns home to Okinawa from the Tokyo Paralympics with two bronze medals in wheelchair T52 races, "the cheers gave me power"   Tokyo Paralympic bronze medalist Hirokazu Ueyonabaru receiving congratulations from some young supporters at Naha Airport on August 30…"

如果你继续向下滚动文本,你可能会看到故事在“Go to Japanese”处结束,那么我们也将删除该部分及其之后的内容:

cutoff = text.index('Go to Japanese')
cutoff
…
1984

这显示了截断字符串从第 1,984 个字符开始。让我们保留所有直到截断位置的内容:

text = text[0:cutoff]

这成功地去除了页脚的杂项,但仍然有一些页头的杂项需要处理,看看我们能否去除它。这个部分总是比较棘手的,每个网站都有其独特之处,但我们可以尝试去除故事之前的所有内容作为练习。仔细看,我发现故事从第二次出现“Hirokazu Uevonabaru”开始。我们从那个点开始捕获所有内容。我们将使用 .rindex() 代替 .index() 来捕获最后一次出现。这段代码对于实际应用来说过于具体,但希望你能看到你有一些清理脏数据的选项:

cutoff = text.rindex('Hirokazu Ueyonabaru')
text = text[cutoff:]

如果你不熟悉 Python,这可能会让你感到有些奇怪。我们保留了从最后一次出现“Hirokazu Ueyonabaru”开始的所有内容,这正是故事的起点。现在看起来怎么样?

text
…
'Hirokazu Ueyonabaru, 50, – SMBC Nikko Securities Inc. – who won the bronze medal in both the 400-meter and 1,500-meter men's T52 wheelchair race, returned to Okinawa the evening of August 30, landing at Naha airport. Seeing the people who came out to meet him, he said "It was a sigh of relief (to win a medal)" beaming a big smile. That morning he contended with press conferences in Tokyo before heading home. He showed off his two bronze medals, holding them up from his neck in the airport lobby, saying "I could not have done it without all the cheers from everyone…'

看起来几乎完美!总是可以做更多的清理,但现在这样已经足够好了!当你刚开始进行网页抓取时,你需要清理、检查、清理、检查、清理和检查——逐渐地,你会停止发现明显需要删除的东西。你不想削减太多。只需要让文本可以使用——我们将在后续步骤中进行额外的清理。

选择库、API 和源数据

作为这次演示的一部分,我展示了几种从互联网获取有用数据的方法。我展示了几个库如何直接加载数据,但它们提供的数据是有限的。NLTK 只提供了完整古腾堡书籍档案的一小部分,因此我们必须使用Requests库来加载变形记。我还演示了如何通过RequestsBeautifulSoup轻松提取链接和原始文本。

当 Python 库将数据加载功能集成到库中时,它们也可以使加载数据变得非常简单,但你只能使用那些库提供的数据。如果你只是想要一些数据来玩,并且不需要太多清理,这可能是理想选择,但仍然需要清理。在处理文本时,你无法避免这一点。

其他网络资源提供了自己的 API,这使得在向它们发送请求后加载数据变得非常简单。Twitter 就是这样做的。你通过 API 密钥进行身份验证,然后你就可以提取你想要的任何数据。这是在 Python 库和网页抓取之间的一个理想平衡。

最后,网页抓取让你能够访问整个互联网。如果你能访问一个网页,你就可以抓取它并使用它提供的任何文本和数据。网页抓取具有灵活性,但它更为复杂,且结果需要更多的清理。

我通常会按照以下顺序考虑我的抓取和数据增强项目:

  • 有没有 Python 库可以让我轻松加载我想要的数据?

  • 没有吗?好吧,有没有我可以用来提取我想要的数据的 API?

  • 没有吗?好吧,我能用BeautifulSoup直接抓取吗?能吗?那就开始吧。我们开始跳舞吧。

从简单开始,只有在需要时才逐步增加复杂度。开始简单意味着从最简单的方法开始——在这种情况下,就是使用 Python 库。如果库不能满足需求,可以通过查看是否有 API 可用来帮助,且是否负担得起,来增加一点复杂度。如果没有 API 可用,那么网页抓取就是你需要的解决方案,无法避免,但你将能够获得你需要的数据。

现在我们已经有了文本,我们将进入自然语言处理(NLP)。具体来说,我们将使用词性标注(PoS tagging)和命名实体识别(NER),这两种不同的方法从原始文本中提取实体(人物和事物)。

使用 NLTK 进行词性标注

在这一部分,我将解释如何使用 NLTK Python 库进行所谓的 PoS 标注。NLTK 是一个较老的 Python 自然语言处理库,但它仍然非常有用。在将 NLTK 与其他 Python 自然语言处理库(例如spaCy)进行比较时,也有其优缺点,因此了解每个库的优缺点是有益的。然而,在我进行这次演示的编码过程中,我意识到spaCy在 PoS 标注和命名实体识别(NER)方面确实让一切变得更加简便。因此,如果你想要最简单的方法,随时可以跳到spaCy部分。我仍然喜欢NLTK,在某些方面,这个库对我来说比spaCy更自然,但这可能只是因为我已经用了很多年。无论如何,我想先用NLTK演示 PoS 标注,然后在下一部分演示如何使用spaCy进行 PoS 标注和 NER。

PoS 标注是一个过程,它将文本中的单词标记为相应的词性。回顾一下,token 是单个单词。一个 token 可能是apples

在 NLP 中,token 很有用,但二元组(bigram)通常更加有用,它们可以提高文本分类、情感分析,甚至是无监督学习的结果。二元组本质上是两个 token——例如,two tokens

我们不要想太多。这只是两个 token。你认为三元组(trigram)是什么?没错,就是三个 token。例如,如果在提取三元组之前移除掉一些填充词,你可能会得到一个像green eggs ham这样的三元组。

有许多不同的pos_tags,你可以在这里查看完整列表:www.ling.upenn.edu/courses/Fall_2003/ling001/penn_treebank_pos.html

对于我们正在做的工作,我们将只使用我们需要的 NLP 特性,PoS 标注和 NER 是两种有用的不同方法,可以帮助我们识别文本中描述的实体(人和物)。在上述列表中,我们需要的是 NNP 和 NNPS,在大多数情况下,我们会找到 NNP,而不是 NNPS。

为了解释我们要做的事情,我们将遵循以下步骤:

  1. 获取一些文本进行处理。

  2. 将文本拆分成句子。

  3. 将每个句子拆分成 token。

  4. 识别每个 token 的 PoS 标签。

  5. 提取每个专有名词的 token。

专有名词是指人、地方或事物的名称。我一直在说我们要提取实体,并将实体定义为人、地方或事物,因此 NNP 标签将准确标识我们想要的内容:

  1. 让我们开始工作,获取一些文本数据!

    url = 'https://www.gutenberg.org/files/5200/5200-0.txt'
    
    text = requests.get(url).text
    

我们之前使用这段代码加载了卡夫卡的书《变形记》中的整个文本,The Metamorphosis

  1. 这个文件的头部有很多杂乱的内容,但故事从“One morning”开始,所以我们从那之前的部分删除掉。你可以在我们操作时随意查看text变量。我省略了反复展示数据的步骤以节省空间:

    cutoff = text.index('One morning')
    
    text = text[cutoff:]
    

这里,我们已经识别出了短语的起始点One morning,并移除了所有到这个点为止的内容。那只是我们不需要的头部垃圾。

  1. 接下来,如果你查看文本底部,你会看到故事在*** END OF THE PROJECT GUTENBERG EBOOK METAMORPHOSIS处结束,那么让我们从这个点开始裁剪:

    cutoff = text.rindex('*** END OF THE PROJECT GUTENBERG EBOOK METAMORPHOSIS ***')
    
    text = text[:cutoff]
    

仔细看截断,你会发现截断的位置与删除头部时使用的位置不同。我本质上是在说,“给我所有内容直到截断位置。”现在结束的文本看起来怎么样?

text[-500:]
…
'talking, Mr. and Mrs.\r\nSamsa were struck, almost simultaneously, with the thought of how their\r\ndaughter was blossoming into a well built and beautiful young lady.\r\nThey became quieter. Just from each otherâ\x80\x99s glance and almost without\r\nknowing it they agreed that it would soon be time to find a good man\r\nfor her. And, as if in confirmation of their new dreams and good\r\nintentions, as soon as they reached their destination Grete was the\r\nfirst to get up and stretch out her young body.\r\n\r\n\r\n\r\n\r\n'
We have successfully removed both header and footer junk. I can see that there are a lot of line breaks, so let's remove all of those as well.
text = text.replace('\r', ' ').replace('\n', ' ')
text
…
'One morning, when Gregor Samsa woke from troubled dreams, he found  himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his  brown belly, slightly domed and divided by arches into stiff sections…'

不错。这是一个显著的进步,我们离干净的文本又近了一步。撇号也被破坏,显示为â\x80\x99,所以我们需要将其替换:

text = text.replace('â\x80\x99', '\'').replace('â\x80\x9c', '"').replace('â\x80\x9d', '""')\
.replace('â\x80\x94', ' ')
print(text)
…
One morning, when Gregor Samsa woke from troubled dreams, he found  himself transformed in his bed into a horrible vermin. He lay on his  armour-like back, and if he lifted his head a little he could see his  brown belly, slightly domed and divided by arches into stiff sections.  The bedding was hardly able to cover it and seemed ready to slide off  any moment. His many legs, pitifully thin compared with the size of the  rest of him, waved about helplessly as he looked.    "What's happened to me?"" he thought…
  1. 这个几乎完美了,接下来我们将这些步骤转化为一个可重用的函数:

    def get_data():
    
        url = 'https://www.gutenberg.org/files/5200/5200-0.txt'
    
        text = requests.get(url).text
    
        # strip header junk
    
        cutoff = text.index('One morning')
    
        text = text[cutoff:]
    
        # strip footer junk
    
        cutoff = text.rindex('*** END OF THE PROJECT GUTENBERG EBOOK METAMORPHOSIS ***')
    
        text = text[:cutoff]
    
        # pre-processing to clean the text
    
        text = text.replace('\r', ' ').replace('\n', ' ')
    
        text = text.replace('â\x80\x99', '\'').replace('â\x80\x9c', '"')\
    
         .replace('â\x80\x9d', '""').replace('â\x80\x94', ' ')
    
        return text
    
  2. 运行这个函数后,我们应该得到相当干净的文本:

    text = get_data()
    
    text
    
    'One morning, when Gregor Samsa woke from troubled dreams, he found  himself transformed in his bed into a horrible vermin. He lay on his  armour-like back, and if he lifted his head a little he could see his  brown belly, slightly domed and divided by arches into stiff sections.  The bedding was hardly able to cover it and seemed ready to slide off  any moment. His many legs, pitifully thin compared with the size of the  rest of him, waved about helplessly as he looked.    "What\'s happened to me?"" he thought'
    

出色!我们现在准备好进入下一步了。

在我们继续之前,我想解释一件事:如果你对任何书籍或文章的完整文本进行PoS 标注,那么文本会被当作一个巨大的整体处理,你就失去了理解实体如何关联和互动的机会。你最终得到的只会是一个巨大的实体列表,这对于我们的需求并不太有帮助,但如果你只是想从某个文本中提取实体,它倒是很有用。

对于我们的目的,首先你需要做的事情是将文本拆分成句子、章节或其他想要的类别。为了简单起见,我们就按句子来拆分。这可以通过 NLTK 的句子标记化工具轻松完成:

from nltk.tokenize import sent_tokenize
sentences = sent_tokenize(text)
sentences[0:5]
…
['One morning, when Gregor Samsa woke from troubled dreams, he found  himself transformed in his bed into a horrible vermin.',
 'He lay on his  armour-like back, and if he lifted his head a little he could see his  brown belly, slightly domed and divided by arches into stiff sections.',
 'The bedding was hardly able to cover it and seemed ready to slide off  any moment.',
 'His many legs, pitifully thin compared with the size of the  rest of him, waved about helplessly as he looked.',
 '"What\'s happened to me?""']

漂亮! 我们现在有了一个句子列表可以使用。接下来,我们要做的是从这些句子中提取出任何提到的实体。我们需要的是 NNP 标记的词语。这部分稍微复杂一些,我会一步步带你完成。如果我们直接将句子传给 NLTK 的pos_tag 工具,它会错误分类所有内容:

import nltk
nltk.pos_tag(sentences)
[('One morning, when Gregor Samsa woke from troubled dreams, he found  himself transformed in his bed into a horrible vermin.',
  'NNP'),
 ('He lay on his  armour-like back, and if he lifted his head a little he could see his  brown belly, slightly domed and divided by arches into stiff sections.',
  'NNP'),
 ('The bedding was hardly able to cover it and seemed ready to slide off  any moment.',
  'NNP'),
 ('His many legs, pitifully thin compared with the size of the  rest of him, waved about helplessly as he looked.',
  'NNP'),
 ('"What\'s happened to me?""', 'NNP'),
 ('he thought.', 'NN')…]

很好的尝试,但这不是我们需要的。我们需要做的是遍历每个句子并识别 PoS 标签,所以我们先手动处理一个句子:

sentence = sentences[0]
sentence
…
'One morning, when Gregor Samsa woke from troubled dreams, he found  himself transformed in his bed into a horrible vermin.'

首先,我们需要对句子进行标记化。NLTK 中有许多不同的标记化工具,每个工具有自己的优缺点。我习惯了使用随意的标记化工具,所以我将使用它。随意的标记化工具适用于随意的文本,但也有其他几个标记化工具可以选择:

from nltk.tokenize import casual_tokenize
tokens = casual_tokenize(sentence)
tokens
…
['One',
 'morning',
 ',',
 'when',
 'Gregor',
 'Samsa',
 'woke',
 'from',
 'troubled',
 'dreams',
 ',',
 'he',
 'found',
 'himself',
 'transformed',
 'in',
 'his',
 'bed',
 'into',
 'a',
 'horrible',
 'vermin',
 '.']

很好。现在,对于每个标记,我们可以找到它对应的pos_tag

nltk.pos_tag(tokens)
…
[('One', 'CD'),
 ('morning', 'NN'),
 (',', ','),
 ('when', 'WRB'),
 ('Gregor', 'NNP'),
 ('Samsa', 'NNP'),
 ('woke', 'VBD'),
 ('from', 'IN'),
 ('troubled', 'JJ'),
 ('dreams', 'NNS'),
 (',', ','),
 ('he', 'PRP'),
 ('found', 'VBD'),
 ('himself', 'PRP'),
 ('transformed', 'VBN'),
 ('in', 'IN'),
 ('his', 'PRP$'),
 ('bed', 'NN'),
 ('into', 'IN'),
 ('a', 'DT'),
 ('horrible', 'JJ'),
 ('vermin', 'NN'),
 ('.', '.')]

这也完美!我们要提取的是 NNP。你能看到我们想要提取的两个标记吗?没错,就是 Gregor Samsa。让我们遍历这些 PoS 标签并提取 NNP 标记:

entities = []
for row in nltk.pos_tag(tokens):
    token = row[0]
    tag = row[1]
    if tag == 'NNP':
        entities.append(token)
entities
…
['Gregor', 'Samsa']

这就是我们需要的。希望 NER 能识别这两项结果为同一个人,但一旦把它放进图中,很容易就能纠正。让我们将其转化为一个函数,它将接受一个句子并返回 NNP 标记——即实体:

def extract_entities(sentence):
    entities = []
    tokens = casual_tokenize(sentence)
    for row in nltk.pos_tag(tokens):
        token = row[0]
        tag = row[1]
        if tag == 'NNP':
            entities.append(token)
    return entities

看起来不错。我们试试看!

extract_entities(sentence)
…
['Gregor', 'Samsa']

现在,让我们大胆尝试一下,将其应用到整本书的每一个句子上:

entities = [extract_entities(sentence) for sentence in sentences]
entities
[['Gregor', 'Samsa'],
 [],
 [],
 [],
 ["What's"],
 [],
 [],
 [],
 ['Samsa'],
 [],
 ['Gregor'],
 [],
 [],
 [],
 [],
 ['Oh', 'God', '"', '"', "I've"],
 [],
 [],
 ['Hell']]

为了让分析稍微简单一些,我们做两件事:首先,将那些空的列表替换成None。其次,把所有这些数据放入一个Pandas DataFrame 中:

def extract_entities(sentence):
    entities = []
    tokens = casual_tokenize(sentence)
    for row in nltk.pos_tag(tokens):
        token = row[0]
        tag = row[1]
        if tag == 'NNP':
            entities.append(token)
    if len(entities) > 0:
        return entities
    else:
        return None
entities = [extract_entities(sentence) for sentence in sentences]
entities
[['Gregor', 'Samsa'],
 None,
 None,
 None,
 ["What's"],
 None,
 None,
 None,
 ['Samsa'],
 None,
 ['Gregor'],
 None,
 None,
 None,
 None,
 ['Oh', 'God', '"', '"', "I've"],
 None,
 None,
 ['Hell']]
import pandas as pd
df = pd.DataFrame({'sentence':sentences, 'entities':entities})
df.head(10)

这将给我们一个包含句子和从句子中提取的实体的 DataFrame:

图 4.4 – Pandas DataFrame 中的句子实体

图 4.4 – Pandas DataFrame 中的句子实体

这是一个好的开始。我们可以看到“What’s”被 NLTK 错误地标记了,但在处理文本时,垃圾信息被误识别是正常的。这些问题很快会得到清理。现在,我们想利用这本书构建社交网络,所以让我们获取所有包含两个或更多实体的实体列表。我们至少需要两个实体来识别关系:

df = df.dropna()
df = df[df['entities'].apply(len) > 1]
entities = df['entities'].to_list()
entities
[['Gregor', 'Samsa'],
 ['Oh', 'God', '"', '"', "I've"],
 ['"', '"'],
 ["I'd", "I'd"],
 ["I've", "I'll"],
 ['First', "I've"],
 ['God', 'Heaven'],
 ['Gregor', '"', '"', '"'],
 ['Gregor', "I'm"],
 ['Gregor', 'Gregor', '"', '"', '"'],
 ['Gregor', "I'm"],
 ['"', '"', 'Gregor', '"'],
 ['Seven', '"'],
 ["That'll", '"', '"'],
 ["They're", '"', '"', 'Gregor'],
 ["Gregor's", 'Gregor'],
 ['Yes', '"', 'Gregor'],
 ['Gregor', '"', '"'],
 ['Mr', 'Samsa', '"', '"']]

除了一些标点符号悄悄混入外,这看起来还不错。让我们回顾一下之前的代码,并忽略所有非字母字符。这样,Gregor's会变成GregorI'd会变成I,以此类推。这样清理起来会更加容易:

def extract_entities(sentence):
    entities = []
    tokens = casual_tokenize(sentence)
    for row in nltk.pos_tag(tokens):
        token = row[0]
        tag = row[1]
        if tag == 'NNP':
            if "'" in token:
                cutoff = token.index('\'')
                token = token[:cutoff]
            entities.append(token)
    if len(entities) > 0:
        return entities
    else:
        return None
entities = [extract_entities(sentence) for sentence in sentences]
entities
[['Gregor', 'Samsa'],
 None,
 None,
 None,
 ['What'],
 None,
 None,
 None,
 ['Samsa']…]

让我们将这个数据重新放入 DataFrame 中,并重复我们的步骤,看看实体列表是否有所改善:

df = pd.DataFrame({'sentence':sentences, 'entities':entities})
df = df.dropna()
df = df[df['entities'].apply(len) > 1]
entities = df['entities'].to_list()
entities
[['Gregor', 'Samsa'],
 ['Oh', 'God', '"', '"', 'I'],
 ['"', '"'],
 ['I', 'I'],
 ['I', 'I'],
 ['First', 'I'],
 ['God', 'Heaven'],
 ['Gregor', '"', '"', '"'],
 ['Gregor', 'I'],
 ['Gregor', 'Gregor', '"', '"', '"'],
 ['Gregor', 'I'],
 ['"', '"', 'Gregor', '"'],
 ['Seven', '"'],
 ['That', '"', '"'],
 ['They', '"', '"', 'Gregor'],
 ['Gregor', 'Gregor'],
 ['Yes', '"', 'Gregor'],
 ['Gregor', '"', '"'],
 ['Mr', 'Samsa', '"', '"']]

这已经好很多了,但数据中仍然有一些双引号。让我们去除所有标点符号以及之后的内容:

from string import punctuation
def extract_entities(sentence):
    entities = []
    tokens = casual_tokenize(sentence)
for row in nltk.pos_tag(tokens):
        token = row[0]
        tag = row[1]
        if tag == 'NNP':
            for p in punctuation:
                if p in token:
                    cutoff = token.index(p)
                    token = token[:cutoff]
            if len(token) > 1:
                entities.append(token)
if len(entities) > 0:
        return entities
    else:
        return None
entities = [extract_entities(sentence) for sentence in sentences]
df = pd.DataFrame({'sentence':sentences, 'entities':entities})
df = df.dropna()
df = df[df['entities'].apply(len) > 1]
entities = df['entities'].to_list()
entities
…
[['Gregor', 'Samsa'],
 ['Oh', 'God'],
 ['God', 'Heaven'],
 ['Gregor', 'Gregor'],
 ['They', 'Gregor'],
 ['Gregor', 'Gregor'],
 ['Yes', 'Gregor'],
 ['Mr', 'Samsa'],
 ['He', 'Gregor'],
 ['Well', 'Mrs', 'Samsa'],
 ['No', 'Gregor'],
 ['Mr', 'Samsa'],
 ['Mr', 'Samsa'],
 ['Sir', 'Gregor'],
 ['Oh', 'God']]

这个结果已经足够好了!我们可以增加一些逻辑,防止同一标记连续出现两次,但我们可以很容易地从网络中去除这些,所以让我们重构代码继续进行:

def get_book_entities():
    text = get_data()
    sentences = sent_tokenize(text)
    entities = [extract_entities(sentence) for sentence in sentences]
    df = pd.DataFrame({'sentence':sentences, 'entities':entities})
    df = df.dropna()
    df = df[df['entities'].apply(len) > 1]
    entities = df['entities'].to_list()
    return entities
entities = get_book_entities()
entities[0:5]
…
[['Gregor', 'Samsa'],
 ['Oh', 'God'],
 ['God', 'Heaven'],
 ['Gregor', 'Gregor'],
 ['They', 'Gregor']]

这非常棒。此时,不管我们是做词性标注还是 NER,我们都需要一个实体列表,这已经足够接近了。接下来,我们将使用 spaCy 做同样的操作,你应该能够看到 spaCy 在某些方面更为简便。然而,它的设置更为复杂,因为你需要安装一个语言模型来与 spaCy 一起使用。每种方法都有其优缺点。

使用 spaCy 进行词性标注和 NER

在这一节中,我将解释如何使用 spaCy 做我们刚刚用 NLTK 完成的工作。我还将展示如何使用 NER 作为一种通常优于词性标注(PoS)的方式来识别和提取实体。在开始编写本章内容之前,我主要使用 NLTK 的PoS tagging作为我的实体提取核心,但在为这一节编写代码并稍微深入探索后,我意识到 spaCy 有了显著的改进,因此我认为我在这一节展示的内容优于之前用 NLTK 做的工作。我认为解释 NLTK 的有用性还是有帮助的。学习两者并使用最适合你的方法。但对于实体提取,我相信 spaCy 在易用性和处理速度方面优于 NLTK。

之前,我编写了一个函数来加载弗朗茨·卡夫卡的书《变形记》,所以我们也将使用这个加载器,因为它不依赖于 NLTK 或 spaCy,并且可以很容易地修改,以便从古腾堡计划的档案中加载任何书籍。

要使用 spaCy 做任何事情,首先需要做的是加载所选的 spaCy 语言模型。在我们加载之前,必须先安装它。你可以通过运行以下命令来完成安装:

python -m spacy download en_core_web_md

有几种不同的模型可供选择,但我使用的三个英文文本模型分别是小型、中型和大型。md代表中型。你可以将其替换为smlg,分别获取小型或大型模型。你可以在这里了解更多关于 spaCy 模型的信息:spacy.io/usage/models

一旦模型安装完成,我们就可以将其加载到我们的 Python 脚本中:

import spacy
nlp = spacy.load("en_core_web_md")

如前所述,你可以根据需要将 md 替换为 smlg,取决于你想使用的模型。较大的模型需要更多的存储和内存。较小的模型需要较少。选择一个足够适合你工作的模型。你可能不需要大型模型。中型和小型模型表现很好,不同模型之间的差异通常不易察觉。

接下来,我们需要一些文本,因此让我们重用之前编写的函数:

def get_data():
    url = 'https://www.gutenberg.org/files/5200/5200-0.txt'
text = requests.get(url).text
    # strip header junk
    cutoff = text.index('One morning')
    text = text[cutoff:]
    # strip footer junk
    cutoff = text.rindex('*** END OF THE PROJECT GUTENBERG EBOOK METAMORPHOSIS ***')
    text = text[:cutoff]
    # pre-processing to clean the text
    text = text.replace('\r', ' ').replace('\n', ' ')
    text = text.replace('â\x80\x99', '\'').replace('â\x80\x9c', '"').replace('â\x80\x9d', '""').replace('â\x80\x94', ' ')
return text
That looks good. We are loading The Metamorphosis, cleaning out header and footer junk, and then returning text that is clean enough for our purposes. Just to lay eyes on the text, let's call our function and inspect the returned text.
text = get_data()
text[0:279]
…
'One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his  armour-like back, and if he lifted his head a little he could see his  brown belly, slightly domed and divided by arches into stiff sections.'

句子中间有一些额外的空格,但这对我们来说不会构成任何问题。分词会自动清理这些空格,而我们无需做额外的工作。我们很快就会涉及到这个问题。首先,像在 NLTK 中那样,我们需要将文本拆分成句子,以便可以根据每个句子中揭示的实体构建网络。使用 spaCy 而非 NLTK 做这件事要容易得多,下面是操作方法:

doc = nlp(text)
sentences = list(doc.sents)

第一行将 变形记 的完整文本传递给 spaCy,并使用我们选择的语言模型,而第二行则提取文本中的句子。现在,我们应该得到一个 Python 列表,包含所有的句子。让我们检查列表中的前六个句子:

for s in sentences[0:6]:
    print(s)
    print()
…
One morning, when Gregor Samsa woke from troubled dreams, he found  himself transformed in his bed into a horrible vermin.
He lay on his  armour-like back, and if he lifted his head a little he could see his  brown belly, slightly domed and divided by arches into stiff sections.
The bedding was hardly able to cover it and seemed ready to slide off  any moment.
His many legs, pitifully thin compared with the size of the  rest of him, waved about helplessly as he looked.
"What's happened to me?"
" he thought.

六个句子可能看起来是一个奇怪的数量,但我想给你展示一些东西。看看最后两个句子。SpaCy 已成功地将主要角色的内心独白提取为一个独立的句子,并且创建了一个单独的句子来补充周围的句子。对于我们的实体提取来说,如果这些句子合并在一起也不会有任何问题,但我喜欢这样。这不是一个 bug,这是一个功能,正如我们软件工程师常说的那样。

SpaCy 词性标注

现在我们已经有了句子,让我们使用 spaCy 的词性标注作为实体提取的预处理:

for token in sentences[0]:
    print('{}: {}'.format(token.text, token.tag_))
…
One: CD
morning: NN
,: ,
when: WRB
Gregor: NNP
Samsa: NNP
woke: VBD
from: IN
troubled: JJ
dreams: NNS
,: ,
he: PRP
found: VBD
 :
himself: PRP
transformed: VBD
in: IN
his: PRP$
bed: NN
into: IN
a: DT
horrible: JJ
vermin: NN
.: .

好的。我们需要的是 NNP,因为这些是专有名词。如果你使用 pos_ 而不是 tag_,你可以看到这一点:

for token in sentences[0]:
    print('{}: {}'.format(token.text, token.pos_))
…
One: NUM
morning: NOUN
,: PUNCT
when: ADV
Gregor: PROPN
Samsa: PROPN
woke: VERB
from: ADP
troubled: ADJ
dreams: NOUN
,: PUNCT
he: PRON
found: VERB
 : SPACE
himself: PRON
transformed: VERB
in: ADP
his: ADJ
bed: NOUN
into: ADP
a: DET
horrible: ADJ
vermin: NOUN
.: PUNCT

让我们添加一些提取的逻辑。我们需要做两件事——我们需要一个列表来存储结果,并且需要一些逻辑来提取 NNP:

entities = []
for token in sentences[0]:
    if token.tag_ == 'NNP':
        entities.append(token.text)
entities
…
['Gregor', 'Samsa']
Perfect! But this is only working on a single sentence. Let's do the same for all sentences.
entities = []
for sentence in sentences:
    sentence_entities = []
    for token in sentence:
        if token.tag_ == 'NNP':
            sentence_entities.append(token.text)
    entities.append(sentence_entities)
entities[0:10]
…
[['Gregor', 'Samsa'], [], [], [], [], [], [], [], [], ['Samsa']]

对于 NLTK,我们创建了一个函数来提取给定句子的实体,但是通过这种方式,我一次性做完了所有事情,而且非常简单。让我们把这个转化为一个函数,这样我们就能方便地用于以后的工作。同时,让我们防止实体列表中返回空列表,因为我们不需要这些:

def extract_entities(text):
    doc = nlp(text)
    sentences = list(doc.sents)
    entities = []
    for sentence in sentences:
        sentence_entities = []
        for token in sentence:
            if token.tag_ == 'NNP':
                sentence_entities.append(token.text)
        if len(sentence_entities) > 0:
            entities.append(sentence_entities)
    return entities

现在我们应该有一个干净的实体列表,其中不包含任何空的内部列表:

extract_entities(text)
…
[['Gregor', 'Samsa'],
 ['Samsa'],
 ['Gregor'],
 ['God'],
 ['Travelling'],
 ['God', 'Heaven'],
 ['Gregor'],
 ['Gregor'],
 ['Gregor'],
 ['Gregor'],
 ['Gregor']…]

这比 NLTK 的结果好多了,而且步骤更少。这简单而优雅。我不需要用到Pandas,也不需要删除空行,或者清理那些莫名其妙出现的标点符号。我们可以在任何清理过的文本上使用这个函数。你也可以在清理之前使用它,但那时你会得到一些无用的实体,尤其是在针对抓取的网页数据时。

SpaCy 命名实体识别(NER)

SpaCy 的 NER 同样简单易用。词性标注(PoS tagging) 和 NER 的区别在于,NER 进一步识别了人物、地点、事物等。关于 spaCy 语言特性的详细描述,我强烈推荐 Duygu Altinok 的书籍 Mastering spaCy。简而言之,spaCy 将标记归类为 18 种不同的实体类型。在我看来,这有些过多,因为 MONEY 并不是一个实体,但我只取我需要的。请查看 spaCy 获取完整的实体类型列表。我们需要的是被标记为 PERSONORGGPE 的实体。ORG 代表组织,GPE 包含国家、城市和州。

让我们遍历第一句话中的所有标记,看看这在实践中是如何运作的:

for token in sentences[0]:
    print('{}: {}'.format(token.text, token.ent_type_))
…
One: TIME
morning: TIME
,:
when:
Gregor: PERSON
Samsa: PERSON
woke:
from:
troubled:
dreams:
,:
he:
found:
 :
himself:
transformed:
in:
his:
bed:
into:
a:
horrible:
vermin:
.:

这个方法有效,但有一个小问题:我们希望“Gregor Samsa”作为一个实体出现,而不是两个。我们需要做的是创建一个新的 spaCy 文档,然后遍历文档的 ents,而不是单独的标记。在这方面,NER 的方法与 词性标注(PoS tagging)略有不同:

doc = nlp(sentences[0].text)
for ent in doc.ents:
    print('{}: {}'.format(ent, ent.label_))
…
One morning: TIME
Gregor Samsa: PERSON

完美!接下来做几件事:我们将重做之前的实体提取函数,不过这次使用 NER 而不是词性标注(PoS tagging),然后将实体限制为PERSONORGGPE。请注意,只有在一句话中有多个实体时,我才会添加实体。我们需要识别人物之间的关系,而你至少需要两个人才算得上有关系:

def extract_entities(text):
    doc = nlp(text)
    sentences = list(doc.sents)
    entities = []
    for sentence in sentences:
        sentence_entities = []
        sent_doc = nlp(sentence.text)
        for ent in sent_doc.ents:
            if ent.label_ in ['PERSON', 'ORG', 'GPE']:
                entity = ent.text.strip()
                if "'s" in entity:
                    cutoff = entity.index("'s")
                    entity = entity[:cutoff]
                if entity != '':
                    sentence_entities.append(entity)
        sentence_entities = list(set(sentence_entities))
        if len(sentence_entities) > 1:
            entities.append(sentence_entities)
    return entities

我添加了一些代码,去除任何多余的空白,并且清除了每个 sentence_entity 列表中的重复项。我还去除了出现在名字后面的 's 字符——比如 Gregor's,它会显示为 Gregor。我本可以在网络清理时处理这些,但这是一个不错的优化。让我们看看结果如何。我把实体列表命名为 morph_entities,代表 metaMORPHosis 实体。我想要一个描述性的名字,这个是我能想到的最合适的:

morph_entities = extract_entities(text)
morph_entities
…
[['Gregor', 'Grete'],
 ['Gregor', 'Grete'],
 ['Grete', 'Gregor'],
 ['Gregor', 'Grete'],
 ['Grete', 'Gregor'],
 ['Grete', 'Gregor'],
 ['Grete', 'Gregor'],
 ['Samsa', 'Gregor'],
 ['Samsa', 'Gregor'],
 ['Samsa', 'Grete'],
 ['Samsa', 'Grete'],
 ['Samsa', 'Grete']]

看起来很棒!哇,我已经很多年没读过 变形记 了,竟然忘记了故事中的人物有多少!

SpaCy 的 NER 是使用预训练的深度学习模型完成的,而机器学习从来不是完美的。请记住这一点。总是需要一些清理工作。SpaCy 允许你根据自己的文档自定义语言模型,如果你在某个专业领域工作,这非常有用,但我更倾向于将 spaCy 的模型作为通用工具使用,因为我处理的文本类型种类繁多。我不想为推文、文学作品、虚假信息和新闻定制 spaCy。我宁愿按原样使用它,并根据需要进行清理。对我来说,这一直是非常有效的。

你应该能看到有很多重复项。显然,在变形记中,很多时间都在谈论格雷戈尔。稍后我们可以通过一行NetworkX代码删除这些重复项,所以我决定保留它们,而不是调整函数。能达到“足够好”就可以。如果你正在处理大量数据并且支付云存储费用,你可能需要修复这些低效之处。

在本章的其余部分,我将使用 NER 结果作为我们的网络数据。我们同样可以使用pos_tag实体,但这样做不如 NER 好,因为 NER 能够结合名字和姓氏。在我们当前的实体中,这些信息并没有被提取出来,但在其他文本中会有。这就是变形记的写作方式。我们将在创建网络时清理这些数据。

为了做个理智检查,让我们看看《爱丽丝梦游仙境》中的实体!

def get_data():
    url = 'https://www.gutenberg.org/files/11/11-0.txt'
    text = requests.get(url).text
    # strip header junk
    cutoff = text.index('Alice was beginning')
    text = text[cutoff:]
    # strip footer junk
    cutoff = text.rindex('THE END')
    text = text[:cutoff]
    # pre-processing to clean the text
    text = text.replace('\r', ' ').replace('\n', ' ')
    text = text.replace('â\x80\x99', '\'').replace('â\x80\x9c', '"').replace('â\x80\x9d', '""').replace('â\x80\x94', ' ')
    return text
text = get_data()
text[0:310]
…
'Alice was beginning to get very tired of sitting by her sister on the  bank, and of having nothing to do: once or twice she had peeped into  the book her sister was reading, but it had no pictures or  conversations in it, "and what is the use of a book,"" thought Alice  "without pictures or conversations?""  '

我同意你的看法,爱丽丝。

我已经调整了加载函数,以加载《爱丽丝梦游仙境》这本书,并去除了任何页眉或页脚文本。其实这挺有趣的。砍掉他们的头(或页头)!让我们尝试提取实体。我预期这会有点杂乱,因为我们在处理幻想类角色,但让我们看看会发生什么。也许简·奥斯汀的作品会给出更好的结果。我们拭目以待!

alice_entities = extract_entities(text)
alice_entities[0:10]
…
[['Alice', 'Rabbit'],
 ['Alice', 'Longitude'],
 ['New Zealand', "Ma'am", 'Australia'],
 ['Fender', 'Alice'],
 ['Alice', 'Rabbit'],
 ['Mabel', 'Ada'],
 ['Rome', 'Paris', 'London'],
 ['Improve', 'Nile'],
 ['Alice', 'Mabel'],
 ['Alice', 'William the Conqueror']]

结果比我预期的要好,但还是有一些垃圾数据混进来了。我们将使用这两个实体列表来创建和可视化网络。这为我们的下一步工作打下了良好的基础。现在我们有了一些看起来非常有用的实体,接下来让我们着手创建一个可以加载到 NetworkX 图形中的 Pandas DataFrame!这正是将实体列表转换为实际社交网络所需要的。

这就结束了我们关于使用 spaCy 进行词性标注(PoS tagging)和命名实体识别(NER)的演示。我希望你能看到,虽然增加了一个额外的依赖项(语言模型),但实体提取过程变得简单得多。现在,是时候进入我认为最激动人心的部分:将实体列表转换为网络数据,然后用这些数据创建社交网络,之后我们可以可视化并进行调查。

将实体列表转换为网络数据

既然我们已经得到了相当干净的实体数据,接下来是将它转换成一个可以轻松加载到 NetworkX 中的 Pandas DataFrame,以便创建一个实际的社交网络图。这句话需要解释的内容有点多,但我们的工作流程是这样的:

  1. 加载文本。

  2. 提取实体。

  3. 创建网络数据。

  4. 使用网络数据创建图。

  5. 分析图。

再次强调,我将“图”和“网络”这两个术语交替使用。虽然这样会引起一些混淆,但这些名字不是我起的。我更喜欢说“网络”,但是人们会认为我在讲计算机网络,然后我不得不提醒他们我是在讲图,接着他们又会认为我在讲柱状图。对于不熟悉这些概念的人来说,解释图和网络真的很难,即使是我自己,在别人开始谈论网络和图时也会感到困惑。你是指节点和边,还是指 TCP/IP 和柱状图?唉,真是没法赢。

在接下来的部分,我们确实有多种方法来实现这一点,但我会解释我通常使用的方法。来看一下来自爱丽丝梦游仙境的实体:

alice_entities[0:10]
…
[['Alice', 'Rabbit'],
 ['Alice', 'Longitude'],
 ['New Zealand', "Ma'am", 'Australia'],
 ['Fender', 'Alice'],
 ['Alice', 'Rabbit'],
 ['Mabel', 'Ada'],
 ['Rome', 'Paris', 'London'],
 ['Improve', 'Nile'],
 ['Alice', 'Mabel'],
 ['Alice', 'William the Conqueror']]

在大多数情况下,每个内部列表中只有两个实体,但有时会有三个或更多。我通常做的是将第一个实体视为源,任何其他实体视为目标。那在句子中是怎样的呢?我们来看这个句子:“Jack 和 Jill 上山去向他们的朋友 Mark 打招呼。”如果我们将其转换为实体,列表会是这样:

['Jack', 'Jill', 'Mark']

为了实现我的方法,我会将列表中的第一个元素添加到我的源列表中,然后将第一个元素后的所有内容添加到我的目标列表中。下面是用代码展示的样子,但我使用的是Alice中的实体:

final_sources = []
final_targets = []
for row in alice_entities:
    source = row[0]
    targets = row[1:]
    for target in targets:
        final_sources.append(source)
        final_targets.append(target)

仔细看一下捕获sourcetargets的两行代码。source是每个实体列表的第一个元素,targets是每个实体列表中除第一个元素外的所有内容。然后,对于每个目标,我将源和目标添加到final_sourcesfinal_targets中。我循环遍历targets,因为它们可以是一个或多个。source永远不会多于一个,因为它是第一个元素。理解这一点很重要,因为这个过程对于如何在最终的社交网络中展示关系至关重要。我们本可以采用另一种方法,将每个实体相互连接,但我更喜欢我展示的方法。稍后的列表如果有关系证据,可能会填补任何空白。那么我们的最终源是什么样的呢?

final_sources[0:5]
…
['Alice', 'Alice', 'New Zealand', 'New Zealand', 'Fender']
Nice. How about our final targets?
final_targets[0:5]
…
['Rabbit', 'Longitude', "Ma'am", 'Australia', 'Alice']

两者看起来都不错。记住,我们使用了命名实体识别(NER)来捕获人物、地点和事物,所以这看起来没问题。稍后,我会直接从社交网络中删除一些源和目标。现在这样就足够了。

将第一个元素与目标连接的这种方法,我仍然经常考虑。另一种方法是将同一句中出现的每个实体都连接起来。我更倾向于我的方法,但你应该考虑这两种选择。

第一个实体与同一句中的其他实体互动,但并非所有实体都会彼此互动。例如,看看这句话:

John 去看他的好朋友 Aaron,然后他和 Jake 一起去了公园。”

这就是实体列表的样子:

['John', 'Aaron', 'Jake']

在这个例子中,Aaron 可能认识 Jake,但根据这句话我们无法确定。希望的是,如果确实存在某种关系,最终会被识别出来。也许在另一句话中,例如这句:

Aaron 和 Jake 去滑冰,然后吃了些比萨饼。”

在那句话之后,将会有明确的联系。我的首选方法需要更多证据才能连接实体。

我们现在有了一个代码,用来处理实体列表并创建两个列表:final_sourcesfinal_targets,但这对于传递给 NetworkX 来创建图形并不实用。我们做两个额外的事情:使用这两个列表创建一个 Pandas DataFrame,然后创建一个可重用的函数,接受任何实体列表并返回这个 DataFrame:

def get_network_data(entities):
    final_sources = []
    final_targets = []
    for row in entities:
        source = row[0]
        targets = row[1:]
        for target in targets:
            final_sources.append(source)
            final_targets.append(target)
    df = pd.DataFrame({'source':final_sources, 'target':final_targets})
    return df

看起来不错。让我们看看它的实际效果!

alice_network_df = get_network_data(alice_entities)
alice_network_df.head()

这将显示一个包含源节点和目标节点的网络数据的 DataFrame。这被称为边列表:

图 4.5 – 《爱丽丝梦游仙境》实体关系的 Pandas DataFrame

图 4.5 – 《爱丽丝梦游仙境》实体关系的 Pandas DataFrame

很好。那么它如何处理来自变形记的实体?

morph_network_df = get_network_data(morph_entities)
morph_network_df.head()

这将显示变形记的网络边列表:

图 4.6 – 《变形记》实体关系的 Pandas DataFrame

图 4.6 – 《变形记》实体关系的 Pandas DataFrame

完美,而且这个函数是可重用的!

我们现在准备将这两个都转化为实际的 NetworkX 图形。我认为这是有趣的地方。我们之前做的所有事情只是预处理工作。现在,我们可以玩转网络,尤其是社交网络分析!在这一章之后,我们将主要学习社交网络分析和网络科学。至于自然语言处理(NLP)中的一些领域,我略过了,例如词形还原和词干提取,但我故意这样做,因为它们在实体提取上没有 PoS 标注和命名实体识别(NER)那么重要。如果你想更深入了解 NLP,推荐你阅读 Duygu Altinok 的《Mastering spaCy》。在本书中,我们的 NLP 内容就到这里,因为这已经是我们所需的全部内容。

将网络数据转换为网络

是时候拿出我们创建的网络数据,创建两个图,一个是 Alice’s Adventures in Wonderland,另一个是 The Metamorphosis。我们暂时不深入进行网络分析,那是在后面的章节里讲的内容。但让我们看看它们的样子,看看能从中得出什么见解。

首先,我们需要导入 NetworkX 库,然后创建我们的图。这非常简单,因为我们已经创建了 Pandas DataFrame,NetworkX 将使用这些 DataFrame。这是我发现的创建图的最简单方法。

首先,如果你还没有安装 NetworkX,你需要通过以下命令安装:

pip install networkx

现在我们已经安装了 NetworkX,让我们来创建两个网络:

import networkx as nx
G_alice = nx.from_pandas_edgelist(alice_network_df)
G_morph = nx.from_pandas_edgelist(morph_network_df)

就这么简单。我们已经在文本预处理上完成了困难的工作。它成功了吗?让我们看一下每个图:

nx.info(G_alice)
…
'Graph with 68 nodes and 71 edges'
…
nx.info(G_morph)
…
'Graph with 3 nodes and 3 edges'

很棒!我们已经在了解两个社交网络之间的差异了。在图中,节点通常是与其他节点有关系的事物。没有任何关系的节点叫做孤立节点,但由于我们图的构建方式,不会有孤立节点。因为我们查找了包含两个或更多实体的句子。试着想象这两个实体就像两个点,中间有一条线。这实际上就是图/网络可视化的样子,只不过通常有许多点和许多线。两个节点之间的关系被称为边。你需要理解节点和边的区别,才能更好地处理图。

看一下 Alice 图的汇总信息,我们可以看到有 68 个节点(角色)和 71 条边(这些角色之间的关系)。

看一下 The Metamorphosis 网络的汇总信息,我们可以看到只有三个节点(角色)和三条边(这些角色之间的关系)。当进行可视化时,这将是一个非常简单的网络,因此我很高兴我们也做了 Alice

NetworkX 中还隐藏着许多其他有用的度量和总结,我们将在讨论中心性、最短路径以及其他社交网络分析和网络科学主题时讨论这些内容。

做一个网络可视化抽查

让我们可视化这些网络,快速看一眼,然后完成本章内容。

这里有两个我经常使用的可视化函数。在我看来,使用 sknetwork 后,我再也没有回过头去使用 NetworkX 进行可视化。

第一个函数将 NetworkX 图转换为邻接矩阵,sknetwork 使用该矩阵来计算 PageRank(重要性分数),然后将网络渲染为 SVG 图像。第二个函数使用第一个函数,但目标是可视化 ego_graph,稍后会介绍。在 ego 图中,你探索围绕单个节点存在的关系。第一个函数是更通用的。

说够了。你会在看到结果时更能理解:

def draw_graph(G, show_names=False, node_size=1, font_size=10, edge_width=0.5):
    import numpy as np
    from IPython.display import SVG
    from sknetwork.visualization import svg_graph
    from sknetwork.data import Bunch
    from sknetwork.ranking import PageRank
    adjacency = nx.to_scipy_sparse_matrix(G, nodelist=None, dtype=None, weight='weight', format='csr')
    names = np.array(list(G.nodes()))
    graph = Bunch()
    graph.adjacency = adjacency
    graph.names = np.array(names)
    pagerank = PageRank()
    scores = pagerank.fit_transform(adjacency)
    if show_names:
        image = svg_graph(graph.adjacency, font_size=font_size, node_size=node_size, names=graph.names, width=700, height=500, scores=scores, edge_width=edge_width)
    else:
        image = svg_graph(graph.adjacency, node_size=node_size, width=700, height=500, scores = scores, edge_width=edge_width)
    return SVG(image)

接下来,让我们创建一个显示自我图的函数。

为了明确,最好不要在函数内部包含import语句。最好将import语句保留在函数外部。然而,在这个情况下,这样做可以更容易地将代码复制并粘贴到你的 Jupyter 或 Colab 笔记本中,所以我做个例外:

def draw_ego_graph(G, ego, center=True, k=0, show_names=True, edge_width=0.1, node_size=3, font_size=12):
    ego = nx.ego_graph(G, ego, center=center)
    ego = nx.k_core(ego, k)
    return draw_graph(ego, node_size=node_size, font_size=font_size, show_names=show_names, edge_width=edge_width)

仔细看看这两个函数。拆解它们,试着弄清楚它们在做什么。为了快速完成本章内容,我将展示使用这些函数的结果。我已经抽象化了可视化这些网络的难度,让你可以做这件事:

draw_graph(G_alice)

由于我们没有向函数传递任何参数,这应该会显示一个非常简单的网络可视化。它将看起来像一堆点(节点),其中一些点通过线(边)连接到其他点:

重要提示

请将draw_graph函数保持在手边。我们将在本书中多次使用它。

图 4.7 – 爱丽丝梦游仙境的粗略社交网络

图 4.7 – 爱丽丝梦游仙境的粗略社交网络

好吧,这看起来有点没帮助,但这是故意的。我通常处理的是大规模网络,因此我更倾向于一开始省略节点名称,以便可以直观地检查网络。不过,你可以覆盖我正在使用的默认值。让我们做这些,并稍微减少线条宽度,增加节点大小,并添加节点名称:

draw_graph(G_alice, edge_width=0.2, node_size=3, show_names=True)

这将绘制我们的社交网络,并带有标签!

图 4.8 – 爱丽丝梦游仙境的标记社交网络

图 4.8 – 爱丽丝梦游仙境的标记社交网络

字体大小有点小,看起来有些难以阅读,所以我们将增加字体大小,并将node_size减小一个:

draw_graph(G_alice, edge_width=0.2, node_size=2, show_names=True, font_size=12)

这创建了以下网络:

图 4.9 – 爱丽丝梦游仙境的最终社交网络

图 4.9 – 爱丽丝梦游仙境的最终社交网络

太棒了。想一想我们做了什么。我们从爱丽丝中提取了原始文本,提取了所有实体,并构建了书中描述的社交网络。这是如此强大,它还为你打开了学习更多关于社交网络分析和网络科学的大门。例如,你是想分析别人的玩具数据集,还是更愿意研究你感兴趣的东西,比如你最喜欢的书?我更喜欢追随自己的好奇心。

让我们看看爱丽丝周围的自我图是什么样子的!

draw_ego_graph(G_alice, 'Alice')

这给我们带来了以下网络:

图 4.10 – 爱丽丝自我图

图 4.10 – 爱丽丝自我图

什么?!这太不可思议了。我们可以看到实体列表中有些杂乱的内容,但我们将在下一章学习如何清理这些内容。我们可以再进一步。如果我们想从她的自我图中取出爱丽丝,只探究她周围的关系,这可能吗?

draw_ego_graph(G_alice, 'Alice', center=False)

这给我们提供了以下的可视化:

图 4.11 – 爱丽丝自我图(去除中心)

图 4.11 – 爱丽丝自我图(去除中心)

太简单了。但是分析现有的群体聚类是很困难的。在去除中心后,许多节点变成了孤立节点。如果有一种方法能去除这些孤立节点,我们就能更容易地看到群体。哦,等等!

draw_ego_graph(G_alice, 'Alice', center=False, k=1)

我们得到以下的输出:

图 4.12 – 爱丽丝自我图(去除中心和孤立节点)

图 4.12 – 爱丽丝自我图(去除中心和孤立节点)

总的来说,爱丽丝社交网络看起来相当不错。虽然还需要一些清理工作,但我们可以研究关系。那么变形记的社交网络是什么样的呢?记住,这里只有三个节点和三条边。即使是爱丽丝的自我图,也比变形记的社交网络要复杂。让我们来可视化它吧!

draw_graph(G_morph, show_names=True, node_size=3, font_size=12)

这段代码生成了以下网络:

图 4.13 – *变形记*的标记社交网络

图 4.13 – 变形记的标记社交网络

等等,但为什么有六条边?我只看到了三条。原因是sknetwork会将多条边绘制为一条边。我们确实有一些选项,比如根据边的数量增加线条宽度,但我们还是来看看 Pandas DataFrame,确保我的想法是正确的:

morph_network_df

这将给我们以下的 DataFrame:

图 4.14 – *变形记*的网络数据 Pandas DataFrame

图 4.14 – 变形记的网络数据 Pandas DataFrame

如果我们去掉重复项,会发生什么?

morph_network_df.drop_duplicates()

我们得到以下的 DataFrame:

图 4.15 – *变形记*的网络数据 Pandas DataFrame(去除重复项)

图 4.15 – 变形记的网络数据 Pandas DataFrame(去除重复项)

啊哈!Gregor 和 Grete 之间有关系,但反过来也是如此。我看到的一点是,Samsa 与 Gregor 和 Grete 都有连接,但 Grete 没有回连到 Samsa。换句话说,正如我们在本书中将要讨论的,方向性也是很重要的。你可以有一个有向图。在这种情况下,我只是使用了一个无向图,因为关系往往(但并非总是)是互惠的。

这标志着本次演示的结束。我们最初的目标是使用原始文本创建一个社交网络,而我们轻松地达成了目标。现在,我们有了网络数据可以进行操作。接下来,本书将变得更加有趣。

额外的 NLP 和网络考虑

这一章真是一次马拉松式的挑战。请再耐心等我一会儿。我有一些最后的想法想表达,然后我们就可以结束这一章了。

数据清理

首先,如果你处理的是语言数据,总是需要清理。语言是混乱且复杂的。如果你只习惯处理预先清理过的表格数据,那么这会显得很凌乱。我喜欢这种挑战,因为每个项目都能让我提高技巧和战术。

我展示了两种提取实体的方法:PoS 标注和 NER。两种方法都非常有效,但考虑一下哪种方法能够更快、更轻松地让我们得到一个干净且有用的实体列表。使用PoS 标注时,我们一次得到一个词元。而使用 NER 时,我们能很快得到实体,但模型有时会出错或漏掉一些内容,因此仍然需要进行清理。

没有银弹。我希望使用任何能够让我尽快接近目标的方法,因为清理是不可避免的。越少的修正意味着我可以更快地开始从网络中提取洞察。

比较 PoS 标注和 NER

PoS 标注可能需要额外的步骤,但清理通常更为简单。另一方面,NER 可能步骤较少,但如果你将它应用于抓取的网页文本,可能会得到错误的结果。某些情况下步骤虽然少,但清理工作可能让人头疼。我曾看到 spaCy 的 NER 在抓取的网页内容上出现很多误标。如果你处理的是网页文本,在将数据送入 NER 之前,务必花些时间进行清理。

最后,稍微有些混乱的结果远比没有结果要好得多。这些技术对于丰富数据集以及提取文本中的“谁、什么、在哪里”部分是非常有用的。

抓取注意事项

在规划任何抓取项目时,也有一些需要注意的事项。首先是隐私问题。如果你抓取社交媒体的文本,并从他人的文本中提取实体信息,那么你就相当于在对他们进行监控。想一想,如果有人对你做同样的事情,你会有什么感受。此外,如果你存储这些数据,你就存储了个人数据,这也可能涉及到法律问题。为了避免麻烦,除非你在政府或执法机关工作,否则最好仅针对文学和新闻使用这些技术,直到你有了处理其他类型内容的计划。

还有伦理问题。如果你决定使用这些技术来构建一个监控引擎,你应该考虑构建这样一个系统是否符合伦理。考虑一下对随机陌生人进行监控是否是道德的行为。

最后,抓取就像是自动浏览一个网站,但抓取工具可能会造成损害。如果你在一秒钟内用抓取工具访问一个网站一千次,你可能不小心触发了DoS攻击。按你需要的速度获取你需要的数据。如果你在循环遍历网站上的所有链接并进行抓取,最好在每次抓取前加一个 1 秒的延迟,而不是每秒钟抓取一千次。即便是无意中,你也要为导致网站服务器瘫痪的后果负责。

说了这么多话只是为了告诉你,除非你是为了新闻或文学用途,否则要注意你在做什么。对于新闻和文学内容来说,这可能会揭示一些信息,并可能促使新技术的诞生。对于其他类型的内容,在动手之前请先考虑清楚你在做什么。

概述

在这一章中,我们学习了如何找到并抓取原始文本,将其转化为实体列表,然后将实体列表转化为实际的社交网络,以便我们可以调查揭示出的实体和关系。我们是否捕获了文本中的什么哪里?绝对是的。我希望你现在能理解当自然语言处理(NLP)和社交网络分析结合使用时的有用性。

在这一章中,我展示了几种获取数据的方法。如果你不熟悉网页抓取,这可能会显得有些压倒性,但一旦开始,你会发现其实并不难。不过,在下一章中,我会展示几种更简单的数据获取方法。

第五章:更简便的抓取方法!

在上一章中,我们讲解了网页抓取的基础知识,即从网络上获取数据以供自己使用和项目应用。在本章中,我们将探讨更简便的网页抓取方法,并且介绍社交媒体抓取的内容。上一章非常长,因为我们需要覆盖很多内容,从定义抓取到解释如何使用Requests库和BeautifulSoup来收集网页数据。我会展示一些更简单的方法,获取有用的文本数据,减少清理工作。请记住,这些简单的方法并不一定能取代上一章中解释的内容。在处理数据或进行软件项目时,当事情没有立即按预期工作时,拥有多个选择是非常有用的。但目前,我们将采用更简便的方法来抓取网页内容,并且介绍如何抓取社交媒体的文本数据。

首先,我们将介绍Newspaper3k Python 库,以及Twitter V2 Python 库。

当我说Newspaper3k是一种更简便的收集网页文本数据的方法时,这是因为Newspaper3k的作者在简化收集和丰富网页数据的过程上做得非常出色。他们已经完成了你通常需要自己做的许多工作。例如,如果你想收集关于网站的元数据,比如它使用的语言、故事中的关键词,或者甚至是新闻故事的摘要,Newspaper3k都能提供给你。这本来是需要很多工作的。它之所以更简便,是因为这样你不需要从头开始做。

第二步,你将学习如何使用Twitter V2 Python 库,因为这是一个非常简便的方法来从 Twitter 抓取推文,这对于自然语言处理(NLP)和网络分析非常有用。

本章将涵盖以下主题:

  • 为什么要介绍 Requests 和 BeautifulSoup?

  • 从 Newspaper3k 入门

  • 介绍 Twitter Python 库

技术要求

在本章中,我们将使用 NetworkX 和 pandas Python 库。这两个库现在应该已经安装完毕,所以可以直接使用。如果还没有安装,您可以通过以下命令安装 Python 库:

pip install <library name>

例如,要安装 NetworkX,可以执行以下命令:

pip install networkx

我们还将讨论一些其他的库:

  • Requests

  • BeautifulSoup

  • Newspaper3k

Requests 应该已经包含在 Python 中,通常不需要单独安装。

BeautifulSoup可以通过以下方式安装:

pip install beautifulsoup4

Newspaper3k可以通过以下方式安装:

pip install newspaper3k

第四章中,我们还介绍了一个draw_graph()函数,它使用了 NetworkX 和 scikit-network 库。每次进行网络可视化时,你都需要使用到这段代码,记得保留它!

你可以在本书的 GitHub 仓库中找到本章的所有代码:github.com/PacktPublishing/Network-Science-with-Python

为什么要介绍RequestsBeautifulSoup

我们都喜欢事物变得简单,但生活充满挑战,事情并不总是如我们所愿。在抓取过程中,这种情况常常发生,简直让人发笑。最初,你可以预期更多的事情会出错而不是成功,但只要你坚持不懈并了解自己的选择,最终你会获得你想要的数据。

在上一章中,我们介绍了Requests Python 库,因为它让你能够访问和使用任何公开可用的网页数据。使用Requests时,你有很大的自由度。它能为你提供数据,但使数据变得有用则是一个既困难又耗时的过程。接着,我们使用了BeautifulSoup,因为它是一个强大的 HTML 处理库。通过BeautifulSoup,你可以更具体地指定从网页资源中提取和使用的数据类型。例如,我们可以轻松地从一个网站收集所有的外部链接,甚至获取网站的完整文本,排除所有 HTML 代码。

然而,BeautifulSoup默认并不会提供完美清洁的数据,特别是当你从成百上千的网站抓取新闻故事时,这些网站都有不同的页头和页脚。我们在上一章中探讨的使用偏移量来裁剪页头和页脚的方法,在你只从少数几个网站收集文本时非常有用,你可以轻松为每个独特的网站设置一些规则进行处理,但当你想扩展抓取的网站数量时,就会遇到可扩展性问题。你决定抓取的网站越多,你就需要处理更多的随机性。页头不同,导航结构可能不同,甚至语言可能不同。抓取过程中遇到的困难正是让我觉得它非常有趣的一部分。

介绍Newspaper3k

如果你想做自然语言处理(NLP)或将文本数据转化为社交网络分析和网络科学中使用的网络数据,你需要干净的数据。Newspaper3k提供了继BeautifulSoup之后的下一步,它进一步抽象了数据清理的过程,为你提供了更干净的文本和有用的元数据,减少了工作量。许多性能优化和数据清理工作都被抽象了出来。而且,由于你现在已经了解了上一章中关于数据清理的一些方法,当你看到Newspaper3k的数据清洁度时,你可能会更好地理解背后发生的事情,并且希望你会对他们的工作感到非常赞赏,感谢他们为你节省的时间。

现在,我们可以认为Newspaper3k是从网上收集文本的“简便方法”,但它之所以简便,是因为它建立在之前的基础之上。

在你的网页抓取和内容分析项目中,你仍然可能需要使用RequestsBeautifulSoup。我们需要先了解这些内容。当进行网页抓取项目时,不要一开始就从最基础的部分开始,一边重造每个需要的工具,也许我们应该通过问自己几个问题,来决定从哪里开始:

  • 我可以用Newspaper3k做吗?

  • 不行吗?好吧,那我能用RequestsBeautifulSoup做吗?

  • 不行吗?好吧,那我至少可以使用Requests获取一些数据吗?

你在哪里获得有用的结果,就应该从哪里开始。如果你能从Newspaper3k中获得所需的一切,就从那里开始。如果用Newspaper3k不能得到你需要的内容,但用BeautifulSoup可以,那就从BeautifulSoup开始。如果这两种方法都不可行,那你就需要使用Requests来获取数据,然后写清理文本的代码。

我通常建议人们从基础开始,只有在必要时才增加复杂性,但对于数据收集或文本抓取,我不推荐这种方法。重造 HTML 解析器几乎没有用处,没有必要去制造不必要的麻烦。只要它能满足你的项目需求,使用任何能快速提供有用数据的工具。

什么是 Newspaper3k?

Newspaper3k是一个 Python 库,用于从新闻网站加载网页文本。然而,与BeautifulSoup不同,Newspaper3k的目标不在于灵活性,而是快速获取有用数据快速。我最为钦佩的是 Newspaper3k 的清洗数据的能力,结果相当纯净。我曾比较过使用BeautifulSoupNewspaper3k的工作,后者给我留下了非常深刻的印象。

Newspaper3k不是BeautifulSoup的替代品。它可以做一些BeautifulSoup能做的事情,但并不是所有的,而且它的设计并没有考虑到处理 HTML 时的灵活性,这正是BeautifulSoup的强项。你给它一个网站,它会返回该网站的文本。使用BeautifulSoup时,你有更多的灵活性,你可以选择只查看链接、段落或标题。而Newspaper3k则提供文本、摘要和关键词。BeautifulSoup在这一抽象层次上略低一些。理解不同库在技术栈中的位置非常重要。BeautifulSoup是一个高层次的抽象库,但它的层次低于Newspaper3k。同样,BeautifulSoup的层次也高于Requests。这就像电影《盗梦空间》——有层层叠叠的结构。

Newspaper3k 的用途是什么?

Newspaper3k对于获取网页新闻故事中的干净文本非常有用。这意味着它解析 HTML,剪切掉无用的文本,并返回新闻故事、标题、关键词,甚至是文本摘要。它能返回关键词和文本摘要,意味着它背后具有一些相当有趣的自然语言处理能力。你不需要为文本摘要创建机器学习模型,Newspaper3k会透明地为你完成这项工作,而且速度惊人。

Newspaper3k似乎受到了在线新闻解析的启发,但它不限于此。我也曾用它来抓取博客。如果你有一个网站想尝试抓取,不妨给Newspaper3k一个机会,看看效果如何。如果不行,可以使用BeautifulSoup

Newspaper3k的一个弱点是,它无法解析使用 JavaScript 混淆技术将内容隐藏在 JavaScript 中,而不是 HTML 中的网站。网页开发者有时会这样做,以阻止抓取,原因多种多样。如果你将Newspaper3kBeautifulSoup指向一个使用 JavaScript 混淆的网站,它们通常只会返回很少甚至没有有用的结果,因为数据隐藏在 JavaScript 中,而这两个库并不适用于处理这种情况。解决方法是使用像Selenium配合Requests这样的库,这通常足以获取你需要的数据。Selenium超出了本书的范围,而且经常让人觉得麻烦,不值得花费太多时间。因此,如果你在 JavaScript 混淆面前卡住了,可以查阅相关文档,或干脆跳过并抓取更简单的网站。大多数网站是可以抓取的,那些不能抓取的通常可以忽略,因为它们可能不值得投入太多精力。

开始使用Newspaper3k

在使用Newspaper3k之前,你必须先安装它。这非常简单,只需要运行以下命令:

pip install newspaper3k

在之前的安装过程中,我曾收到一个错误,提示某个 NLTK 组件未下载。留意奇怪的错误信息。解决办法非常简单,只需要运行一个 NLTK 下载命令。除此之外,库的表现一直非常好。安装完成后,你可以立即将它导入到 Python 代码中并使用。

在上一章中,我展示了灵活但更为手动的抓取网站方法。许多无用的文本悄悄混入其中,数据清理相当繁琐,且难以标准化。Newspaper3k将抓取提升到了另一个层次,让它变得比我见过的任何地方都要简单。我推荐你尽可能使用Newspaper3k进行新闻抓取。

从一个网站抓取所有新闻 URL

使用Newspaper3k从域名中提取 URL 非常简单。这是加载网页域名中所有超链接所需的全部代码:

import newspaper
domain = 'https://www.goodnewsnetwork.org'
paper = newspaper.build(domain, memoize_articles=False)
urls = paper.article_urls()

但是,我想指出一个问题:当你以这种方式抓取所有 URL 时,你也会发现我认为是“垃圾 URL”的一些链接,这些 URL 指向网站的其他区域,而不是文章。这些 URL 可能有用,但在大多数情况下,我只想要文章的 URL。如果我不采取任何措施去删除垃圾,URL 会像这样:

urls
['https://www.goodnewsnetwork.org/2nd-annual-night-of-a-million-lights/',
'https://www.goodnewsnetwork.org/cardboard-pods-for-animals-displaced-by-wildfires/',
'https://www.goodnewsnetwork.org/category/news/',
'https://www.goodnewsnetwork.org/category/news/animals/',
'https://www.goodnewsnetwork.org/category/news/arts-leisure/',
'https://www.goodnewsnetwork.org/category/news/at-home/',
'https://www.goodnewsnetwork.org/category/news/business/',
'https://www.goodnewsnetwork.org/category/news/celebrities/',
'https://www.goodnewsnetwork.org/category/news/earth/',
'https://www.goodnewsnetwork.org/category/news/founders-blog/']

请注意,如果你在不同的时间爬取一个网站,你可能会得到不同的结果。新内容可能已被添加,旧内容可能已被删除。

在我大多数抓取中,那些位于前两个 URL 下方的内容就是我认为的垃圾。我只需要文章 URL,那些是指向特定分类页面的 URL。有几种方法可以解决这个问题:

  • 你可以删除包含“category”一词的 URL。在这种情况下,这看起来非常合适。

  • 你可以删除那些 URL 长度超过某个阈值的 URL。

  • 你可以将这两种选项合并成一种方法。

对于这个例子,我决定选择第三个选项。我将删除所有包含“category”一词的 URL,以及长度小于 60 个字符的 URL。你也许想尝试不同的截止阈值,看看哪个最适合你。简单的清理代码如下:

urls = sorted([u for u in urls if 'category' not in u and len(u)>60])

现在我们的 URL 列表看起来更加干净,只包含文章 URL。这正是我们需要的:

urls[0:10]
…
['https://www.goodnewsnetwork.org/2nd-annual-night-of-a-million-lights/',
 'https://www.goodnewsnetwork.org/cardboard-pods-for-animals-displaced-by-wildfires/',
 'https://www.goodnewsnetwork.org/couple-living-in-darkest-village-lights-sky-with-huge-christmas-tree/',
 'https://www.goodnewsnetwork.org/coya-therapies-develop-breakthrough-treatment-for-als-by-regulating-t-cells/',
 'https://www.goodnewsnetwork.org/enorme-en-anidacion-de-tortugasen-tailandia-y-florida/',
 'https://www.goodnewsnetwork.org/good-talks-sustainable-dish-podcast-with-shannon-hayes/',
 'https://www.goodnewsnetwork.org/gopatch-drug-free-patches-good-gifts/',
 'https://www.goodnewsnetwork.org/horoscope-from-rob-brezsnys-free-will-astrology-12-10-21/',
 'https://www.goodnewsnetwork.org/how-to-recognize-the-eight-forms-of-capital-in-our-lives/',
 'https://www.goodnewsnetwork.org/mapa-antiguo-de-la-tierra-te-deja-ver-su-evolucion/']

现在我们有了一个干净的 URL 列表,我们可以遍历它,抓取每个故事,并加载文本供我们使用。

在继续之前,你应该注意到,在这个单一的网络域上,他们发布的故事是多语言的。他们发布的大部分故事是英语的,但也有一些不是。如果你将 Newspaper3k 指向该域(而不是指向单独的故事 URL),它可能无法正确地识别该域的语言。最好在故事级别做语言查找,而不是在域级别。我会展示如何在故事级别做这个。

从网站抓取新闻故事

现在我们有了一个包含故事 URL 的列表,我们想从中抓取文章文本和元数据。下一步是使用选定的 URL,收集我们需要的任何数据。对于这个例子,我将下载并使用我们 URL 列表中的第一个故事 URL:

from newspaper import Article
url = urls[0]
article = Article(url)
article.download()
article.parse()
article.nlp()

这个代码片段中有几行比较令人困惑,我会逐行解释:

  1. 首先,我从报纸库中加载 Article 函数,因为它用于下载文章数据。

  2. 接下来,我将 Article 指向我们的 URL 列表中的第一个 URL,也就是 urls[0]。此时它并没有执行任何操作;它只是被指向了源 URL。

  3. 然后,我从给定的 URL 下载并解析文本。这对于获取完整的文章和标题很有用,但它不会捕获文章的关键词。

  4. 最后,我运行 Articlenlp 组件来提取关键词。

通过这四个步骤,我现在应该已经拥有了这篇文章所需的所有数据。让我们深入看看,看看我们有什么!

  • 文章标题是什么?

    title = article.title
    
    title
    
    'After Raising $2.8M to Make Wishes Come True for Sick Kids, The 'Night of a Million Lights' Holiday Tour is Back'
    
  • 干净利落。那文本呢?

    text = article.text
    
    text[0:500]
    
    'The Night of A Million Lights is back—the holiday spectacular that delights thousands of visitors and raises millions to give sick children and their weary families a vacation.\n\n'Give Kids The World Village' has launched their second annual holiday lights extravaganza, running until Jan. 2\n\nIlluminating the Central Florida skyline, the 52-night open house will once again provide the public with a rare glimpse inside Give Kids The World Village, an 89-acre, whimsical nonprofit resort that provide'
    
  • 文章摘要是什么?

    summary = article.summary
    
    summary
    
    'The Night of A Million Lights is back—the holiday spectacular that delights thousands of visitors and raises millions to give sick children and their weary families a vacation.\nWhat began as an inventive pandemic pivot for Give Kids The World has evolved into Central Florida's most beloved new holiday tradition.\n"Last year's event grossed $2.8 million to make wishes come true for children struggling with illness and their families," spokesperson Cindy Elliott told GNN.\nThe 
    
    display features 1.25M linear feet of lights, including 3.2 million lights that were donated by Walt Disney World.\nAll proceeds from Night of a Million Lights will support Give Kids The World, rated Four Stars by Charity Navigator 15 years in a row.'
    
  • 文章是用什么语言写的?

    language = article.meta_lang
    
    language
    
    'en'
    
  • 文章中发现了哪些关键词?

    keywords = article.keywords
    
    keywords
    
    ['million',
    
     'kids',
    
     'children',
    
     'lights',
    
     'world',
    
     'true',
    
     'tour',
    
     'wishes',
    
     'sick',
    
     'raising',
    
     'night',
    
     'village',
    
     'guests',
    
     'holiday',
    
     'wish']
    
  • 这篇文章配的是什么图片?

    image = article.meta_img
    
    image
    
    'https://www.goodnewsnetwork.org/wp-content/uploads/2021/12/Christmas-disply-Night-of-a-Million-Lights-released.jpg'
    

而且,你还可以使用 Newspaper3k 做更多的事情。我鼓励你阅读该库的文档,看看还有哪些功能能对你的工作有所帮助。你可以在newspaper.readthedocs.io/en/latest/阅读更多内容。

优雅地抓取并融入其中

在构建任何爬虫时,我通常会做两件事:

  • 与人群融为一体

  • 不要过度抓取

这两者之间有些重叠。如果我与真实的网页访问者融为一体,我的爬虫就不那么显眼,也不太容易被封锁。其次,如果我不进行过度抓取,我的爬虫也不太可能被注意到,从而减少被封锁的可能性。然而,第二点更为重要,因为对 web 服务器进行过度抓取是不友好的。最好在 URL 抓取之间设置 0.5 或 1 秒的等待:

  1. 对于第一个想法,即与人群融为一体,你可以伪装成一个浏览器用户代理。例如,如果你希望你的爬虫伪装成在 macOS 上运行的最新 Mozilla 浏览器,可以按如下方式操作:

    from newspaper import Config
    
    config = Config()
    
    config.browser_user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.0; rv:95.0) Gecko/20100101 Firefox/95.0'
    
    config.request_timeout = 3
    
  2. 接下来,为了在每次抓取 URL 之间添加 1 秒钟的暂停,你可以使用 sleep 命令:

    import time
    
    time.sleep(1)
    

user_agent 配置通常足以绕过简单的机器人检测,且 1 秒的暂停是一个友好的操作,有助于与人群融为一体。

将文本转换为网络数据

为了将我们刚刚抓取的文本转换为网络数据,我们可以重用前一章创建的函数。提醒一下,这个函数是使用一种名为命名实体识别NER)的 NLP 技术来提取文档中提到的人物、地点和组织:

  1. 这是我们将使用的函数:

    import spacy
    
    nlp = spacy.load("en_core_web_md")
    
    def extract_entities(text):
    
        doc = nlp(text)
    
        sentences = list(doc.sents)
    
        entities = []
    
        for sentence in sentences:
    
            sentence_entities = []
    
            sent_doc = nlp(sentence.text)
    
            for ent in sent_doc.ents:
    
                if ent.label_ in ['PERSON', 'ORG', 'GPE']:
    
                    entity = ent.text.strip()
    
                    if "'s" in entity:
    
                        cutoff = entity.index("'s")
    
                        entity = entity[:cutoff]
    
                    if entity != '':
    
                        sentence_entities.append(entity)
    
            sentence_entities = list(set(sentence_entities))
    
            if len(sentence_entities) > 1:
    
                entities.append(sentence_entities)
    
        return entities
    
  2. 我们可以简单地将抓取的文本传入此函数,它应该返回一个实体列表:

    entities = extract_entities(text)
    
    entities
    
    [['Night', 'USA'],  ['Florida', 'Kissimmee'],  ['GNN', 'Cindy Elliott'],  ['the Centers for Disease Control and Prevention', 'CDC'],  ['Florida', 'Santa'],  ['Disney World', 'Central Florida']]
    
  3. 完美!现在,我们可以将这些实体传递给另一个函数,以获取我们可以用来构建网络的 pandas DataFrame 边缘列表数据。

  4. 接下来,我们将使用 get_network_data 函数,其代码如下:

    import pandas as pd
    
    def get_network_data(entities):
    
        final_sources = []
    
        final_targets = []
    
        for row in entities:
    
            source = row[0]
    
            targets = row[1:]
    
            for target in targets:
    
                final_sources.append(source)
    
                final_targets.append(target)
    
        df = pd.DataFrame({'source':final_sources, 'target':final_targets})
    
        return df
    
  5. 我们可以通过传入实体列表来使用它:

    network_df = get_network_data(entities)
    
    network_df.head()
    

经检查,效果很好。网络边缘列表必须包含一个源节点和一个目标节点,而现在我们已经有了这两者:

图 5.1 – pandas DataFrame 实体边缘列表

图 5.1 – pandas DataFrame 实体边缘列表

很好。第四行很有趣,因为 NER 成功识别出了两种不同的表示 CDC 的方式,既有全称也有缩写。第一行似乎有一个误报,但我会在下一章讲解如何清理网络数据。现在这一切都很完美。

端到端 Network3k 抓取与网络可视化

现在我们有了所有演示两个事项所需的东西。我想展示如何抓取多个 URL,并将数据合并为一个 DataFrame 以供使用和存储,你将学习如何将原始文本转换为网络数据并可视化它。在上一章中我们做过这个,但再做一次会更有助于加深记忆。

将多个 URL 抓取结果合并到一个 DataFrame 中

在这两个演示之间,这部分是最基础和最重要的。我们将在下一个演示中使用此过程的结果。在大多数实际的抓取项目中,重复抓取单个 URL 没有什么意义。通常,你想对你抓取的任何域名重复这些步骤:

  1. 抓取所有的 URL。

  2. 删除你已经抓取过文本的 URL。

  3. 抓取剩余 URL 的文本。

对于这个演示,我们只会做步骤 1步骤 3。对于你的项目,你通常需要想出一个方法来删除你已经抓取的 URL,这取决于你将抓取后的数据写入哪里。本质上,你需要检查你已经抓取的内容,并忽略你已经使用过的 URL。这可以防止重复工作、不必要的抓取噪音、不必要的抓取负担以及重复的数据。

以下代码抓取给定域名的所有 URL,抓取每个发现的 URL 的文本,并创建一个pandas DataFrame,供使用或输出到文件或数据库。我还加入了一个额外的 Python 库:tqdmtqdm库在你想了解一个过程需要多长时间时非常有用。如果你在后台自动化中使用这个功能,你可能不需要tqdm的功能,但现在它很有用,因为你正在学习。

你可以通过运行pip install tqdm来安装tqdm

图 5.2 – TQDM 进度条的实际应用

图 5.2 – TQDM 进度条的实际应用

这是完整的 Python 代码,它接受一个域名并返回一个抓取的故事的pandas DataFrame:

import newspaper
from newspaper import Article
from tqdm import tqdm
def get_story_df(domain):
    paper = newspaper.build(domain, memoize_articles=False)
    urls = paper.article_urls()
    urls = sorted([u for u in urls if 'category' not in u and len(u)>60])
    titles = []
    texts = []
    languages = []
    keywords = []
    for url in tqdm(urls):
        article = Article(url)
        article.download()
        article.parse()
        article.nlp()
        titles.append(article.title)
        texts.append(article.text)
        languages.append(article.meta_lang)
        keywords.append(article.keywords)
    df = pd.DataFrame({'urls':urls, 'title':titles, 'text':texts, 'lang':languages, 'keywords':keywords})
    return df

要使用这个函数,你可以运行以下代码,并指向任何感兴趣的新闻域名:

domain = 'https://www.goodnewsnetwork.org'
df = get_story_df(domain)
df.head()

现在,你应该有一个干净的新闻故事 DataFrame 可供使用。如果你遇到 404(页面未找到)错误,可能需要在函数中添加一些 try/except 异常处理代码。我将这些和其他边缘情况留给你处理。不过,URL 抓取和文章文本抓取的时间间隔越短,遇到 404 错误的可能性就越小。

让我们来检查一下结果!

图 5.3 – 抓取的 URL 数据的 pandas DataFrame

图 5.3 – 抓取的 URL 数据的 pandas DataFrame

酷!tqdm进度条一直工作到完成,我们还可以看到最终故事的语言已被设置为西班牙语。这正是我们想要的。如果我们试图通过抓取主页(landing page)来检测整个域的语言,语言检测组件可能会给出错误的结果,甚至什么都不返回。这个网站既有英语故事也有西班牙语故事,我们可以在故事级别看到这一点。

捕捉一段文本的语言对于 NLP 工作非常有用。通常,经过一种语言训练的机器学习分类器在应用到另一种语言时会遇到困难,而在使用无监督机器学习(聚类)处理文本数据时,用不同语言写的数据会聚集在一起。我的建议是,利用捕捉到的语言数据将数据按语言拆分,以便进行后续的 NLP 扩展工作。这样你会获得更好的结果,同时分析结果也会变得更加简单。

接下来,让我们使用这些故事来创建网络数据和可视化!

将文本数据转换成网络进行可视化

本书中,我们已经多次提取文本、提取实体、创建网络数据、构建网络并进行可视化。我们在这里也会做同样的事情。不同之处在于,我们现在有一个包含多篇新闻文章的pandas DataFrame,每篇文章都可以转换成一个网络。对于这个示例,我只做两次。从现在开始,你应该不会再有困难将文本转换成网络,你也可以重用已经写好的代码。

我们的实体提取是基于英语语言的 NLP 模型构建的,因此我们只使用英语语言的故事。为了简化演示,我们将使用 DataFrame 中的第二篇和第四篇文章,因为它们给出了有趣且干净的结果:

  1. 首先,我们将使用第二篇文章。你应该能看到我正在加载df['text'][1],其中[1]是第二行,因为索引是从0开始的:

    text = df['text'][1]
    
    entities = extract_entities(text)
    
    network_df = get_network_data(entities)
    
    G = nx.from_pandas_edgelist(network_df)
    
    draw_graph(G, show_names=True, node_size=4, edge_width=1, font_size=12)
    

这是网络可视化:

图 5.4 – 文章实体关系的网络可视化(第二篇文章)

图 5.4 – 文章实体关系的网络可视化(第二篇文章)

这看起来不错,但非常简单。一篇新闻文章通常涉及几个个人和组织,所以这并不令人惊讶。我们仍然可以看到一些野生动物保护组织之间的关系,一个人与大学之间的关系,以及澳大利亚考拉之间的关系。所有这些看起来都很现实。

  1. 接下来,让我们尝试第四篇文章:

    text = df['text'][3]
    
    entities = extract_entities(text)
    
    network_df = get_network_data(entities)
    
    G = nx.from_pandas_edgelist(network_df)
    
    draw_graph(G, show_names=True, node_size=4, edge_width=1, font_size=12)
    

这是网络可视化。这一部分更加有趣且复杂:

图 5.5 – 文章实体关系的网络可视化(第四篇文章)

图 5.5 – 文章实体关系的网络可视化(第四篇文章)

这是一个比新闻报道中常见的实体集合更加丰富的集合,事实上,这个故事中涉及的实体和关系比我们在上一章研究的《变形记》这本书还要多。这看起来很棒,我们可以进一步研究这些被揭示出来的关系。

从本书这一章开始,我将主要使用 Twitter 数据来创建网络。我想解释如何对任何文本进行这种操作,因为这让我们能够自由地揭示任何文本中的关系,而不仅仅是社交媒体文本。然而,你应该已经理解这一点了。本书剩余部分将更多关注分析网络,而不是创建网络数据。一旦文本数据转化为网络,其余的网络分析信息同样具有相关性和实用性。

介绍 Twitter Python 库

Twitter 是一个非常适合 NLP 项目的宝藏。它是一个非常活跃的社交网络,且审查制度不太严格,这意味着用户在讨论各种话题时都很自在。这意味着 Twitter 不仅适合研究轻松的话题,也可以用来研究更为严肃的话题。你有很大的灵活性。

与其他社交网络相比,Twitter 也有一个相对简单的 API。它很容易入门,并且可以用来捕捉数据,这些数据可以为未来的 NLP 研究提供支持。在我个人的 NLP 研究中,学习如何抓取 Twitter 数据极大地推动了我的 NLP 学习。当你有了自己感兴趣的数据时,学习 NLP 变得更加有趣。我曾使用 Twitter 数据来理解各种网络、创造原创的 NLP 技术以及生成机器学习训练数据。我虽然不常用 Twitter,但我发现它对所有与 NLP 相关的内容来说都是一座金矿。

什么是 Twitter Python 库?

多年来,Twitter 一直在开放其 API,以便软件开发者和研究人员能够利用他们的数据。由于文档有些零散且令人困惑,使用该 API 是一项挑战,因此创建了一个 Python 库来简化与 API 的交互。我将解释如何使用这个 Python 库,但你需要自行探索 Twitter API,了解 Twitter 为防止过度使用 API 所设置的各种限制。

你可以在developer.twitter.com/en/docs查看更多关于 Twitter API 的信息。

Twitter 库有哪些用途?

一旦你学会了使用 Twitter 库和 API,你就可以完全自由地用它来做任何研究。你可以用它来了解 K-Pop 音乐圈,或者用它来关注机器学习或数据科学领域的最新动态。

另一个用途是分析整个受众。例如,如果一个账户有 50,000 个粉丝,你可以使用 Twitter 库加载关于所有 50,000 个粉丝的数据,包括他们的用户名和描述。利用这些描述,你可以使用聚类技术来识别更大群体中的各种子群体。你还可以使用这类数据潜在地识别机器人和其他形式的人工放大。

我建议你找到一些你感兴趣的东西,然后追寻它,看看它会引导你到哪里。这种好奇心是培养 NLP 和社交网络分析技能的极好驱动力。

可以从 Twitter 收集哪些数据?

即使与两年前相比,Twitter 似乎已经扩展了其 API 提供的功能,允许更多不同类型的数据科学和 NLP 项目。然而,在他们的 版本一V1)API 中,Twitter 会返回一个包含大量数据的字典,这些数据都是他们可以提供的。这在他们的 V2 API 中有所变化,现在他们要求开发者明确指定请求哪些数据。这使得知道 Twitter 提供的所有数据变得更加困难。任何与 Twitter API 一起工作的人都需要花时间阅读文档,以了解有哪些可用的数据。

对于我的研究,我通常只对以下几个方面感兴趣:

  • 谁在发布内容?

  • 他们发布了什么?

  • 它是什么时候发布的?

  • 他们提到了谁?

  • 他们使用了什么话题标签?

所有这些内容都可以轻松地通过 Twitter API 获取,我会向你展示如何操作。但这并不是 Twitter 提供的全部功能。我最近对 V2 API 曝露的一些新发现的数据印象深刻,但我还没有足够的了解来写关于它的内容。当你遇到感觉应该由 API 曝露的内容时,查阅文档。在我与 Twitter API 工作的经验中,一些本应默认暴露的内容,现在相比于 V1,可能需要做更多额外的工作。尝试找出如何获取你需要的内容。

获取 Twitter API 访问权限

在你能使用 Twitter API 做任何事情之前,首先你需要做的就是获得访问权限:

  1. 首先,创建一个 Twitter 账户。你不需要用它发布任何内容,但你确实需要有一个账户。

  2. 接下来,访问以下 URL 请求 API 访问权限:developer.twitter.com/en/apply-for-access

申请访问权限的时间可能从几分钟到几天不等。你需要填写一些表格,说明你将如何使用这些数据,并同意遵守 Twitter 的服务条款。在描述你使用 Twitter 数据的方式时,你可以说明你是为了学习 NLP 和社交网络分析而使用这些数据。

  1. 一旦你获得了访问权限,你将拥有自己的开发者门户。在身份验证部分搜索,直到看到类似这样的内容:

图 5.6 – Twitter 认证 Bearer Token

图 5.6 – Twitter 认证 Bearer Token

具体来说,你需要一个 Bearer Token。生成一个并将其保存在安全的地方。你将用它来进行 Twitter API 的身份验证。

一旦你生成了 Bearer Token,你就可以通过 Twitter Python 库开始与 Twitter API 进行交互了。

Twitter 身份验证

在进行身份验证之前,你需要安装 Twitter Python 库:

  1. 你可以通过运行以下代码来实现:

    pip install python-twitter-v2
    
  2. 接下来,在你选择的笔记本中,尝试导入库:

    from pytwitter import Api
    
  3. 接下来,你需要使用 Bearer Token 来进行 Twitter 身份验证。将以下代码中的 bearer_token 文本替换为你自己的 Bearer Token,然后尝试进行身份验证:

    bearer_token = 'your_bearer_token'
    
    twitter_api = Api(bearer_token=bearer_token)
    

如果没有失败,那么你应该已经通过身份验证,并准备好开始抓取推文、连接以及更多数据。

抓取用户推文

我创建了两个辅助函数,用于将用户推文加载到 pandas DataFrame 中。如果你需要更多的数据,可以扩展 tweet_fields,并可能需要添加 user_fields

  1. 请查看以下代码块中的 search_tweets() 函数,了解如何将 user_fields 添加到 Twitter 调用中。此函数不使用 user_fields,因为我们已经传入了用户名:

    def get_user_id(twitter_api, username):
    
        user_data = twitter_api.get_users(usernames=username)
    
        return user_data.data[0].id
    
  2. 第一个函数接受一个 Twitter 用户名并返回其 user_id。这是很重要的,因为某些 Twitter 调用需要 user_id 而不是 username。以下函数使用 user_id 来查找该用户的推文:

    def get_timeline(twitter_api, username):
    
        tweet_fields = ['created_at', 'text', 'lang']
    
        user_id = get_user_id(twitter_api, username)
    
        timeline_data = twitter_api.get_timelines(user_id, return_json=True, max_results=100, tweet_fields=tweet_fields)
    
        df = pd.DataFrame(timeline_data['data'])
    
        df.drop('id', axis=1, inplace=True)
    
        return df
    

在这个函数中,twitter_api.get_timelines() 函数完成了大部分工作。我已经指定了我需要的 tweet_fields,传入了用户的 user_id,指定了要获取该用户最近的 100 条推文,并指定返回的数据格式为 JSON,方便转换为 pandas DataFrame。如果调用这个函数,我应该能立即获得结果。

  1. 让我们看看圣诞老人都在谈些什么:

    df = get_timeline(twitter_api, 'officialsanta')
    
    df.head()
    

现在我们应该能看到圣诞老人五条推文的预览:

图 5.7 – 圣诞老人推文的 pandas DataFrame

图 5.7 – 圣诞老人推文的 pandas DataFrame

完美。现在我们已经获取到了圣诞老人最近的 100 条推文。

  1. 我还为你提供了一个额外的辅助函数。这个函数会提取 text 字段中的实体和标签,我们将利用这些信息绘制社交网络:

    def wrangle_and_enrich(df):
    
        # give some space for splitting, sometimes things get smashed together
    
        df['text'] = df['text'].str.replace('http', ' http')
    
        df['text'] = df['text'].str.replace('@', ' @')
    
        df['text'] = df['text'].str.replace('#', ' #')
    
        # enrich dataframe with user mentions and hashtags
    
        df['users'] = df['text'].apply(lambda tweet: [clean_user(token) for token in tweet.split() if token.startswith('@')])
    
        df['tags'] = df['text'].apply(lambda tweet: [clean_hashtag(token) for token in tweet.split() if token.startswith('#')])
    
        return df
    
  2. 我们可以将这个函数作为一个增强步骤添加进去:

    df = get_timeline(twitter_api, 'officialsanta')
    
    df = wrangle_and_enrich(df)
    
    df.head()
    

这为我们提供了额外的有用数据:

图 5.8 – 丰富版圣诞老人推文的 pandas DataFrame

图 5.8 – 丰富版圣诞老人推文的 pandas DataFrame

这非常适合我们在本章接下来的工作,但在继续之前,我们也将看看如何抓取连接信息。

抓取用户关注列表

我们可以轻松抓取一个账户所关注的所有账户。可以通过以下函数来完成:

def get_following(twitter_api, username):
    user_fields = ['username', 'description']
    user_id = get_user_id(twitter_api, username)
    following = twitter_api.get_following(user_id=user_id, return_json=True, max_results=1000, user_fields=user_fields)
    df = pd.DataFrame(following['data'])
    return df[['name', 'username', 'description']]

在这里,我指定了max_results=1000。这是 Twitter 一次最多返回的结果数,但你可以加载比 1000 条更多的数据。你需要传入一个'next_token'键,继续获取1000条粉丝的数据集。你也可以做类似的操作来加载某个用户的超过 100 条推文。理想情况下,如果你有这个需求,应该在编程中使用递归。你可以使用前面的函数加载第一批数据,如果需要构建递归功能,应该能够扩展它。

你可以调用以下函数:

df = get_following(twitter_api, 'officialsanta')
df.head()

这将为你提供以下格式的结果:

图 5.9 – 圣诞老人追踪的 Twitter 账户的 pandas DataFrame

图 5.9 – 圣诞老人追踪的 Twitter 账户的 pandas DataFrame

对于调查一个群体内部存在的子群体,包含账户描述是非常有用的,因为人们通常会描述自己的兴趣和政治倾向。为了捕获描述,你需要将描述包含在user_fields列表中。

抓取用户粉丝

抓取粉丝几乎是相同的。以下是代码:

def get_followers(twitter_api, username):
    user_fields = ['username', 'description']
    user_id = get_user_id(twitter_api, username)
    followers = twitter_api.get_followers(user_id=user_id, return_json=True, max_results=1000, user_fields=user_fields)
    df = pd.DataFrame(followers['data'])
    return df[['name', 'username', 'description']]

你可以调用这个函数:

df = get_followers(twitter_api, 'officialsanta')
df.head()

这将为你提供与之前所示相同格式的结果。确保包含账户描述。

使用搜索词抓取

收集关于搜索词的推文也很有用。你可以用这个来探索参与了关于某个搜索词的讨论,同时也可以收集这些推文,以便阅读和处理。

以下是我为根据搜索词抓取推文编写的代码:

def search_tweets(twitter_api, search_string):
    tweet_fields = ['created_at', 'text', 'lang']
    user_fields = ['username']
    expansions = ['author_id']
    search_data = twitter_api.search_tweets(search_string, return_json=True, expansions=expansions, tweet_fields=tweet_fields, user_fields=user_fields, max_results=100)
    df = pd.DataFrame(search_data['data'])
    user_df = pd.DataFrame(search_data['includes']['users'])
    df = df.merge(user_df, left_on='author_id', right_on='id')
    df['username'] = df['username'].str.lower()
    return df[['username', 'text', 'created_at', 'lang']]

这个函数比之前的函数稍微复杂一些,因为我指定了tweet_fieldsuser_fields,这是我感兴趣的内容。为了捕获用户名,我需要在author_id上指定一个扩展,最后,我想要 100 条最新的推文。如果你想包含额外的数据,你需要探索 Twitter API,找出如何添加你感兴趣的数据字段。

你可以像这样调用该函数:

df = search_tweets(twitter_api, 'natural language processing')
df = wrangle_and_enrich(df)
df.head()

我还通过wrangle_and_enrich()函数调用,丰富了pandas DataFrame,使其包括用户提及和话题标签。这样就得到了以下的pandas DataFrame:

图 5.10 – Twitter 搜索推文的 pandas DataFrame

图 5.10 – Twitter 搜索推文的 pandas DataFrame

这些搜索推文非常适合用于创建社交网络可视化,因为这些推文来自多个账户。在数据的前两行,你可能会在视觉上注意到intempestadespascal_bornetcogautocom之间有关系。如果我们要可视化这个网络,它们将显示为相连的节点。

将 Twitter 推文转换为网络数据

将社交媒体数据转换为网络数据比处理原始文本要容易得多。幸运的是,推特的情况正好符合这一点,因为推文通常很简短。这是因为用户经常在推文中互相标记,以增加可见性和互动,而且他们经常将推文与话题标签关联,这些关联可以用来构建网络。

使用用户提及和话题标签,我们可以创建几种不同类型的网络:

  • 账户到提及网络 (@ -> @):有助于分析社交网络。

  • 账户到话题标签网络 (@ -> #):有助于找到围绕某个主题(话题标签)存在的社区。

  • 提及到话题标签网络 (@ -> #):类似于之前的网络,但链接到被提及的账户,而不是推文账户。这对于寻找社区也很有用。

  • 话题标签到话题标签网络 (# -> #):有助于发现相关的主题(话题标签)和新兴的趋势话题。

此外,你可以使用 NER 从文本中提取额外的实体,但推文通常比较简短,因此这可能不会提供太多有用的数据。

在接下来的几个部分中,你将学习如何构建第一类和第三类网络。

账户到提及网络 (@ -> @)

我创建了一个有用的辅助函数,将pandas DataFrame 转换为此账户到提及网络数据:

def extract_user_network_data(df):
    user_network_df = df[['username', 'users', 'text']].copy()
    user_network_df = user_network_df.explode('users').dropna()
    user_network_df['users'] = user_network_df['users'].str.replace('\@', '', regex=True)
    user_network_df.columns = ['source', 'target', 'count'] # text data will be counted
    user_network_df = user_network_df.groupby(['source', 'target']).count()
    user_network_df.reset_index(inplace=True)
    user_network_df.sort_values(['source', 'target'], ascending=[True, True])
    return user_network_df

这个函数中涉及的内容非常多:

  1. 首先,我们从df DataFrame 中复制用户名、用户和文本字段,并将它们用于user_network_df DataFrame。users字段的每一行包含一个用户列表,因此我们会“展开”users字段,为每个用户在 DataFrame 中创建一行。我们还会删除不包含任何用户的行。

  2. 接下来,我们移除所有的@字符,这样数据和可视化结果会更加易读。

  3. 然后,我们重命名 DataFrame 中的所有列,为创建我们的图形做准备。NetworkX 的图形需要源和目标字段,count字段也可以作为额外数据传入。

  4. 接下来,我们进行聚合,并统计 DataFrame 中每个源-目标关系的数量。

  5. 最后,我们对 DataFrame 进行排序并返回。虽然我们不需要对 DataFrame 进行排序,但我习惯这样做,因为它有助于查看 DataFrame 或进行故障排除。

你可以将search_tweets DataFrame 传递给这个函数:

user_network_df = extract_user_network_data(df)
user_network_df.head()

你将得到一个边列表 DataFrame,表示用户之间的关系。我们将利用这个来构建和可视化我们的网络。仔细观察,你应该能看到有一个额外的count字段。我们将在后面的章节中使用这个字段作为选择哪些边和节点在可视化中显示的阈值:

图 5.11 – 账户到提及 pandas DataFrame 边列表

图 5.11 – 账户到提及 pandas DataFrame 边列表

这个 DataFrame 中的每一行表示一个用户()与另一个用户(目标)之间的关系。

提及到话题标签网络 (@ -> #)

我创建了一个有用的辅助函数,将 pandas DataFrame 转换为 提及 到话题标签的网络数据。这个函数与之前的类似,但我们加载了用户和话题标签,而完全不使用原账户的用户名:

def extract_hashtag_network_data(df):
    hashtag_network_df = df[['users', 'tags', 'text']].copy()
    hashtag_network_df = hashtag_network_df.explode('users')
    hashtag_network_df = hashtag_network_df.explode('tags')
    hashtag_network_df.dropna(inplace=True)
    hashtag_network_df['users'] = hashtag_network_df['users'].str.replace('\@', '', regex=True)
    hashtag_network_df.columns = ['source', 'target', 'count'] # text data will be counted
    hashtag_network_df = hashtag_network_df.groupby(['source', 'target']).count()
    hashtag_network_df.reset_index(inplace=True)
    hashtag_network_df.sort_values(['source', 'target'], ascending=[True, True])
    # remove some junk that snuck in
    hashtag_network_df = hashtag_network_df[hashtag_network_df['target'].apply(len)>2]
    return hashtag_network_df
You can pass the search_tweets DataFrame to this function.
hashtag_network_df = extract_hashtag_network_data(df)
hashtag_network_df.head()

你将获得一个用户关系的边列表 DataFrame。如前所示,还会返回一个 count 字段,我们将在后面的章节中将其用作选择显示哪些节点和边的阈值:

图 5.12 – 提及到话题标签的 pandas DataFrame 边列表

图 5.12 – 提及到话题标签的 pandas DataFrame 边列表

这个 DataFrame 中的每一行显示一个用户(源)和一个话题标签(目标)之间的关系。

从头到尾抓取 Twitter 数据

我希望之前的代码和示例已经展示了如何轻松使用 Twitter API 来抓取推文,也希望你能看到如何轻松地将推文转化为网络。在本章的最终演示中,我希望你能按照以下步骤进行:

  1. 加载一个包含与网络科学相关的推文的 pandas DataFrame。

  2. 丰富该 DataFrame,使其包括用户提及和话题标签作为独立字段。

  3. 创建账户到提及网络数据。

  4. 创建提及到话题标签的网络数据。

  5. 创建账户到提及网络。

  6. 创建提及到话题标签的网络。

  7. 可视化账户到提及的网络。

  8. 可视化提及到话题标签的网络。

让我们在代码中顺序进行操作,重用本章中使用的 Python 函数。

这是前六步的代码:

df = search_tweets(twitter_api, 'network science')
df = wrangle_and_enrich(df)
user_network_df = extract_user_network_data(df)
hashtag_network_df = extract_hashtag_network_data(df)
G_user = nx.from_pandas_edgelist(user_network_df )
G_hash = nx.from_pandas_edgelist(hashtag_network_df)

真的就这么简单。虽然背后有很多复杂的操作,但你越多地练习网络数据,编写这类代码就会变得越简单。

这两个网络现在都已准备好进行可视化:

  1. 我将从账户到提及的网络可视化开始。这是一个社交网络。我们可以这样绘制它:

    draw_graph(G_user, show_names=True, node_size=3, edge_width=0.5, font_size=12)
    

这应该会渲染出一个网络可视化图:

图 5.13 – 账户到提及社交网络可视化

图 5.13 – 账户到提及社交网络可视化

这有点难以阅读,因为账户名重叠了。

  1. 让我们看看没有标签的网络长什么样:

    draw_graph(G_user, show_names=False, node_size=3, edge_width=0.5, font_size=12)
    

这将给我们一个没有节点标签的网络可视化图。这样我们可以看到整个网络的样子:

图 5.14 – 账户到提及社交网络可视化(无标签)

图 5.14 – 账户到提及社交网络可视化(无标签)

哇!对我来说,这真是既美丽又有用。我看到有几个用户群体或集群。如果我们仔细观察,将能够识别出数据中存在的社区。我们将在第九章中进行此操作,本章专门讲解社区检测。

  1. 现在,让我们看看提及到话题标签的网络:

    draw_graph(G_hash, show_names=True, node_size=3, edge_width=0.5, font_size=12)
    

这应该会渲染出一个网络可视化图:

图 5.15 – 提及到话题标签的网络可视化

图 5.15 – 从提及到话题标签的网络可视化

与“提及账户”网络不同,这个网络更易于阅读。我们可以看到与各种话题标签相关联的用户。没有标签的话显示这些数据没有任何价值,因为没有标签它是无法读取和使用的。这标志着演示的结束。

总结

在本章中,我们介绍了两种更简单的方法来抓取互联网上的文本数据。Newspaper3k轻松抓取了新闻网站,返回了干净的文本、标题、关键词等。它让我们跳过了使用BeautifulSoup时的一些步骤,能够更快地得到清洁数据。我们使用这些清洁的文本和命名实体识别(NER)来创建并可视化网络。最后,我们使用了 Twitter 的 Python 库和 V2 API 来抓取推文和连接,我们也用推文来创建和可视化网络。通过本章和前一章所学,你现在在抓取网络和将文本转换为网络的过程中有了更大的灵活性,这样你就能探索嵌入的和隐藏的关系。

这里有个好消息:收集和清理数据是我们要做的工作中最困难的部分,这标志着数据收集和大部分清理工作的结束。在本章之后,我们将大部分时间都在享受与网络相关的乐趣!

在下一章中,我们将研究图形构建。我们将利用本章中使用的技术,创建用于分析和可视化的网络。

第六章:图形构建与清洗

到目前为止,我们已经覆盖了很多内容。在前几章中,我们介绍了自然语言处理(NLP)、网络科学和社交网络分析,并学习了如何将原始文本转换为网络数据。我们甚至还可视化了其中的一些网络。我希望看到文本转化为可视化网络对你产生的影响和我当时的感觉一样强烈。第一次尝试时,我使用的是《创世纪》一书(来自《圣经》),能将几千年前的文本转换为一个实际的交互式网络,真是让我大吃一惊。

在前两章中,我们学习了从互联网上的网站和社交网络收集文本数据的几种不同方式,并利用这些文本数据创建网络。好消息是,我不需要再展示更多的文本抓取方法了。你已经有了足够的选择,应该能够将这些知识作为其他类型抓取的基础。

坏消息是,现在该讨论每个人的“最爱”话题:数据清洗!说实话,这是我处理网络数据时最喜欢的部分。清洗工作需要一些努力,但其实并不复杂。现在是时候播放一些音乐,泡一杯热饮,放松一下,开始寻找需要修复的小问题了。

为了让本章特别有趣,我们将使用来自 《爱丽丝梦游仙境》 的社交网络。我已经按照前几章中描述的过程创建了这个网络。由于我们已经讨论了几遍如何将文本转换为实体、实体转换为网络数据以及网络数据转换为图形的步骤,所以这次我将跳过这些内容的解释。原始的网络数据已经上传到我的 GitHub,我们将在本章中使用这些数据。

本章我们将涵盖以下内容:

  • 从边列表创建图形

  • 列出节点

  • 移除节点

  • 快速视觉检查

  • 重命名节点

  • 移除边

  • 持久化网络

  • 模拟攻击

技术要求

本章中,我们将使用 NetworkX 和 pandas Python 库。我们还将导入一个 NLTK 分词器。到现在为止,这些库应该已经安装好了,所以它们应该可以立即使用。

本章的所有代码都可以在本书的 GitHub 仓库中找到,地址是 github.com/PacktPublishing/Network-Science-with-Python

从边列表创建图形

我们将使用这个文件作为我们的原始边列表:raw.githubusercontent.com/itsgorain/datasets/main/networks/alice/edgelist_alice_original.csv。让我们来看一下:

  1. 在创建图形之前,我们必须导入两个我们将使用的库:pandasnetworkx。我们使用 pandas 将边列表读取到一个 DataFrame 中,然后将该 DataFrame 传递给 networkx 来创建图形。你可以按如下方式导入这两个库:

    import pandas as pd
    
    import networkx as nx
    
  2. 导入库后,让我们使用 pandas 将 CSV 文件读取为 DataFrame,然后显示它,如下代码块所示:

    data = 'https://raw.githubusercontent.com/itsgorain/datasets/main/networks/alice/edgelist_alice_original.csv'
    
    network_df = pd.read_csv(data)
    
    network_df.head()
    

如果你在 Jupyter notebook 中运行这个,你应该会看到以下的 DataFrame:

图 6.1 – 《爱丽丝梦游仙境》边列表的 pandas DataFrame

图 6.1 – 《爱丽丝梦游仙境》边列表的 pandas DataFrame

在继续之前,我想说的是,如果你能将任何两个事物表示为具有某种关系的形式,那么你就可以将它们作为网络数据。在我们的 DataFrame 中,源节点和目标节点是人、地点和组织,就像我们在 命名实体识别NER)工作中配置的那样,但你也可以制作以下类型的网络:

  • 配料和菜肴

  • 学生和老师

  • 行星和星系

我可以一直说下去。意识到网络在我们周围是如此普遍,并且在所有事物中都能看到它们:这就是解锁网络分析力量的前奏。这是关于理解事物之间关系的。我对文学和安全感兴趣,我的大部分网络分析都涉及人类语言和安全之间的交集。你可能有其他的兴趣,因此你会在其他类型的网络中找到更多的用处。试着从本书中汲取灵感,激发你去研究感兴趣的话题的新方式。

既然这一点已经说清楚了,我们有了边列表的 DataFrame,接下来让我们把它转换成图形。最简单的形式就是下面这样:

G = nx.from_pandas_edgelist(network_df)

真的吗?就这样?是的,就是这样。当我第一次了解到从 pandas DataFrame 转换为可用的图形竟然这么简单时,我立刻被吸引住了。就是这么简单。但它之所以如此简单,有几个原因:

  • 首先,networkx 函数期望 .csv 文件包含这些列,我们不需要重命名任何列或指定哪些列是源节点和目标节点。

  • 其次,我们没有指定使用哪种图,因此 networkx 默认使用 nx.Graph()。这是最简单的图形形式,只允许节点之间有一条边,并且不包括方向性。

在笔记本中,如果我们检查 G,我们会看到以下内容:

G
<networkx.classes.graph.Graph at 0x255e00fd5c8>

这验证了我们正在使用默认的图类型 Graph

有几种方法可以将网络数据加载到 networkx 中,但我更喜欢使用边列表。边列表最简单的形式是带有两个字段的表格数据:sourcetarget。由于这种简单性,它们可以轻松地存储为纯文本或数据库中。你不需要一个复杂的图形数据库来存储边列表。

有些人在处理网络数据时更喜欢使用邻接矩阵。邻接矩阵不容易存储在数据库中,而且也不易扩展。你可以选择你喜欢的方式,但边列表非常容易使用,所以我建议你学习如何使用、创建和存储它们。

图的类型

NetworkX 提供了四种不同类型的图:

  • Graph

  • DiGraph

  • MultiGraph

  • MultiDiGraph

你可以在这里了解更多关于它们的信息:networkx.org/documentation/stable/reference/classes/。我将简要概述每种图的类型,并分享我对何时使用它们的看法。

Graph是 NetworkX 提供的默认和最简单的图形式。在一个简单的图中,节点之间只能有一条边。如果你的边列表包含多个源和目标之间的边,它将被简化为一条边。这并不总是坏事。存在减少网络复杂度的方法,其中一种方法就是聚合数据——例如,计算两个节点之间存在的边的数量,并将该值作为加权计数,而不是让边列表如下所示:

source, target

而是像这样:

source, target, edge_count

以这种形式,图依然有效,因为多个边被简化成了一条边,原本存在的边数也被减少到了计数。这是一种非常好的方法,能够在保持所有信息的同时简化网络数据。

对于我的大部分工作,图是非常合适的。如果我没有选择默认的图,那是因为我需要方向性,因此我选择了DiGraph

创建默认图网络可以通过以下代码完成:

G = nx.from_pandas_edgelist(network_df)

DiGraph

DiGraph类似于图,主要区别在于它是有向的。DiGraph 代表有向图。就像图一样,每个节点之间只能有一条边。关于聚合的大部分内容仍然适用,但如果遇到自环,你可能需要进行处理。

当你需要理解信息的方向性和流动时,这些非常有用。知道两者之间存在关系并不总是足够的。通常,最重要的是理解影响的方向性,以及信息如何传播。

例如,假设我们有四个人,分别是 Sarah、Chris、Mark 和 John。Sarah 写了很多东西,并与她的朋友 Chris 分享她的想法。Chris 有些影响力,并将从 Sarah(和其他人)那里获得的信息分享给他的追随者,这些人包括 Mark 和 John。在这种情况下,数据流动是这样的:

Sarah -> Chris -> (Mark and John)

在这个数据流中,Sarah 是一个重要人物,因为她是全新信息的发起者。

Chris 也是一个重要人物,因为信息通过他流向了许多其他人。我们将在后续章节中学习如何捕捉这种重要性,当我们讨论“介于中心性”时。

最后,Mark 和 John 是这些信息的接收者。

如果这不是一个有向图,我们无法直观地看出是谁创造了信息,或者谁是信息的最终接收者。这种方向性使我们能够追溯到源头,并跟踪信息流动。

有向图在映射生产数据流方面也非常有用,这些数据流发生在生产中的服务器和数据库上。当流程以这种方式映射时,如果某个环节停止工作,你可以一步步回溯,直到找出问题所在。使用这种方法,我能够在几分钟内排查出以前需要几天时间才能解决的问题。

创建一个有向图就像这样简单:

G = nx.from_pandas_edgelist(network_df, create_using=nx.DiGraph)

如果你检查G,你应该看到如下内容:

<networkx.classes.digraph.DiGraph at 0x255e00fcb08>

MultiGraph

一个MultiGraph可以在任意两个节点之间有多个边。MultiGraph不保留任何方向性上下文。说实话,我不使用MultiGraph。我更喜欢将多个边汇总成计数,并使用GraphDiGraph。不过,如果你想创建一个MultiGraph,可以使用以下命令:

G = nx.from_pandas_edgelist(network_df, create_using = nx.MultiGraph)

如果你检查G,你会看到如下内容:

<networkx.classes.multigraph.MultiGraph at 0x255db7afa88>

MultiDiGraph

一个MultiDiGraph可以在任意两个节点之间有多个边,并且这些图形也传达了每条边的方向性。我不使用MultiDiGraph,因为我更喜欢将多个边汇总成计数,然后使用GraphDiGraph。如果你想创建一个MultiDiGraph,可以使用以下命令:

G = nx.from_pandas_edgelist(network_df, create_using = nx.MultiDiGraph)

如果你检查G,你应该看到如下内容:

<networkx.classes.multidigraph.MultiDiGraph at 0x255e0105688>

总结图形

为了确保我们覆盖了所有内容,让我们回顾一下这些图形:

  1. 让我们使用默认图形重新创建我们的图:

    G = nx.from_pandas_edgelist(network_df)
    

很好。我们已经将所有数据加载到G中。这是一个很小的网络,因此在 Jupyter 笔记本中加载速度非常快,我想你也会很快加载完成。考虑到操作速度如此之快,我常常会觉得不过瘾,就会想:“就这些?我花了那么多功夫创建这些数据,结果就这样?”嗯,是的。

  1. 然而,有一个函数对于快速概览图形非常有用:

    print(nx.info(G))
    

如果我们运行这个命令,我们会看到如下内容:

Graph with 68 nodes and 68 edges

整洁。这是一个很小、简单的网络。节点和边数这么少,应该足够清晰地帮助清理。

还有其他方法可以快速检查图形,但这是最简单的一种。现在,让我们来看看清理工作;我们将在后面的章节中了解更多关于网络分析的内容。

列出节点

在从文本构建网络之后,我通常会列出已添加到网络中的节点。这让我可以快速查看节点名称,从而估算出需要清理和重命名节点的工作量。在我们的实体提取过程中,我们有机会清理实体输出。实体数据用于创建网络数据,而网络数据又用于生成图形本身,因此在多个步骤中都可以进行清理和优化,越是在前期做得好,后期需要做的工作就越少。

不过,查看节点名称仍然很重要,目的是识别任何仍然成功进入网络的异常:

  1. 获取节点列表的最简单方法是运行以下networkx命令:

    G.nodes
    

这将给你一个NodeView

NodeView(('Rabbit', 'Alice', 'Longitude', 'New Zealand', "Ma'am", 'Australia', 'Fender', 'Ada', 'Mabel', 'Paris', 'Rome', 'London', 'Improve', 'Nile', 'William the Conqueror', 'Mouse', 'Lory', 'Eaglet', 'Northumbria', 'Edwin', 'Morcar', 'Stigand', 'Mercia', 'Canterbury', 'â\x80\x98it', 'William', 'Edgar Atheling', "â\x80\x98I'll", 'Said', 'Crab', 'Dinah', 'the White Rabbit', 'Bill', 'The Rabbit Sends', 'Mary Ann', 'Pat', 'Caterpillar', 'CHAPTER V.', 'William_', 'Pigeon', 'Fish-Footman', 'Duchess', 'Cheshire', 'Hare', 'Dormouse', 'Hatter', 'Time', 'Tillie', 'Elsie', 'Lacie', 'Treacle', 'Kings', 'Queens', 'Cat', 'Cheshire Cat', 'Somebody', 'Mystery', 'Seaography', 'Lobster Quadrille', 'France', 'England', 'â\x80\x98Keep', 'garden_.', 'Hm', 'Soup', 'Beautiful', 'Gryphon', 'Lizard'))
  1. 这样是可读的,但看起来可能更容易一些。这个函数将稍微整理一下:

    def show_nodes(G):
    
        nodes = sorted(list(G.nodes()))
    
        return ', '.join(nodes)
    

这可以如下运行:

show_nodes(G)

这将输出一个更清晰的节点列表:

"Ada, Alice, Australia, Beautiful, Bill, CHAPTER V., Canterbury, Cat, Caterpillar, Cheshire, Cheshire Cat, Crab, Dinah, Dormouse, Duchess, Eaglet, Edgar Atheling, Edwin, Elsie, England, Fender, Fish-Footman, France, Gryphon, Hare, Hatter, Hm, Improve, Kings, Lacie, Lizard, Lobster Quadrille, London, Longitude, Lory, Ma'am, Mabel, Mary Ann, Mercia, Morcar, Mouse, Mystery, New Zealand, Nile, Northumbria, Paris, Pat, Pigeon, Queens, Rabbit, Rome, Said, Seaography, Somebody, Soup, Stigand, The Rabbit Sends, Tillie, Time, Treacle, William, William the Conqueror, William_, garden_., the White Rabbit, â\x80\x98I'll, â\x80\x98Keep, â\x80\x98it"

现在我们有了一个清理过的《爱丽丝梦游仙境》社交网络中的节点列表。立刻,我的目光被最后三个节点吸引。它们甚至不像是名字。我们将删除它们。我还看到CHAPTER V., Soup和其他一些非实体节点被加入了。这是使用自然语言处理(NLP)进行词性标注(pos_tagging)或命名实体识别(NER)时常见的问题。这两种方法经常会在单词首字母大写的情况下出错。

我们有一些工作要做。我们将删除错误添加的节点,并重命名一些节点,使它们指向白兔

在检查图时,我列出的是节点,而不是边。你可以使用以下方式列出边:

G.edges

这会给你一个EdgeView,像这样:

EdgeView([('Rabbit', 'Alice'), ('Rabbit', 'Mary Ann'), ('Rabbit', 'Pat'), ('Rabbit', 'Dinah'), ('Alice', 'Longitude'), ('Alice', 'Fender'), ('Alice', 'Mabel'), ('Alice', 'William the Conqueror'), ('Alice', 'Mouse'), ('Alice', 'Lory'), ('Alice', 'Mary Ann'), ('Alice', 'Dinah'), ('Alice', 'Bill'), ('Alice', 'Caterpillar'), ('Alice', 'Pigeon'), ('Alice', 'Fish-Footman'), ('Alice', 'Duchess'), ('Alice', 'Hare'), ('Alice', 'Dormouse'), ('Alice', 'Hatter'), ('Alice', 'Kings'), ('Alice', 'Cat'), ('Alice', 'Cheshire Cat'), ('Alice', 'Somebody'), ('Alice', 'Lobster Quadrille'), ('Alice', 'â\x80\x98Keep'), ('Alice', 'garden_.'), ('Alice', 'Hm'), ('Alice', 'Soup'), ('Alice', 'the White Rabbit'), ('New Zealand', "Ma'am"), ('New Zealand', 'Australia'), ('Ada', 'Mabel'), ('Paris', 'Rome'), ('Paris', 'London'), ('Improve', 'Nile'), ('Mouse', 'â\x80\x98it'), ('Mouse', 'William'), ('Lory', 'Eaglet'), ('Lory', 'Crab'), ('Lory', 'Dinah'), ('Northumbria', 'Edwin'), ('Northumbria', 'Morcar'), ('Morcar', 'Stigand'), ('Morcar', 'Mercia'), ('Morcar', 'Canterbury'), ('William', 'Edgar Atheling'), ("â\x80\x98I'll", 'Said'), ('the White Rabbit', 'Bill'), ('the White Rabbit', 'The Rabbit Sends'), ('Caterpillar', 'CHAPTER V.'), ('Caterpillar', 'William_'), ('Duchess', 'Cheshire'), ('Duchess', 'Cat'), ('Duchess', 'Lizard'), ('Hare', 'Hatter'), ('Hare', 'Lizard'), ('Dormouse', 'Hatter'), ('Dormouse', 'Tillie'), ('Dormouse', 'Elsie'), ('Dormouse', 'Lacie'), ('Dormouse', 'Treacle'), ('Hatter', 'Time'), ('Kings', 'Queens'), ('Mystery', 'Seaography'), ('France', 'England'), ('Soup', 'Beautiful'), ('Soup', 'Gryphon')])

我通常不列出边,因为当我删除或重命名节点时,边会自动修正。指向已删除节点的边会被删除,指向已重命名节点的边会连接到新的节点。EdgeView也更难阅读。

有了清理过的节点列表,这是我们的攻击计划:

  1. 删除不良节点。

  2. 重命名白兔节点。

  3. 添加我知道的任何缺失节点。

  4. 添加我能识别的任何缺失的边。

让我们从这些步骤的第一步开始。

删除节点

接下来我们要做的是删除那些错误进入网络的节点,通常是由于pos_taggingNER的假阳性结果。你可能会听到我提到这些节点是“不良”节点。我也可以称它们为“无用”节点,但重点是这些节点不属于网络,应该被删除。为了简化,我称它们为不良节点。

删除节点的一个原因是清理网络,使其更贴近现实,或者贴近某段文本中描述的现实。然而,删除节点也可以用于模拟攻击。例如,我们可以从爱丽丝梦游仙境社交网络中删除关键角色,模拟如果红心皇后实现她的愿望处决多个角色的结果。我们将在本章中进行此操作。

模拟攻击也有助于增强防御。如果一个节点是单点故障,并且它的删除会对网络造成灾难性后果,你可以在某些位置添加节点,这样即使关键节点被删除,网络仍能保持完整,信息流动不受影响:

  • networkx中,有两种删除节点的方法:一次删除一个,或者一次删除多个。你可以像这样删除单个节点:

    G.remove_node('â\x80\x98it')
    
  • 你可以一次性删除多个节点,像这样:

    drop_nodes = ['Beautiful', 'CHAPTER V.', 'Hm', 'Improve', 'Longitude', 'Ma\'am', 'Mystery', 'Said', 'Seaography', 'Somebody', 'Soup', 'Time', 'garden_.', 'â\x80\x98I\'ll', 'â\x80\x98Keep']
    
    G.remove_nodes_from(drop_nodes)
    

我更喜欢第二种方法,因为它还可以用来移除单个节点,只要drop_nodes变量仅包含一个节点名称。你可以简单地不断扩展drop_nodes,直到列出所有不良实体,然后继续刷新剩余节点的列表。现在我们已经移除了一些节点,让我们看看剩下哪些实体:

show_nodes(G)
'Ada, Alice, Australia, Bill, Canterbury, Cat, Caterpillar, Cheshire, Cheshire Cat, Crab, Dinah, Dormouse, Duchess, Eaglet, Edgar Atheling, Edwin, Elsie, England, Fender, Fish-Footman, France, Gryphon, Hare, Hatter, Kings, Lacie, Lizard, Lobster Quadrille, London, Lory, Mabel, Mary Ann, Mercia, Morcar, Mouse, New Zealand, Nile, Northumbria, Paris, Pat, Pigeon, 
Queens, Rabbit, Rome, Stigand, The Rabbit Sends, Tillie, Treacle, William, William the Conqueror, William_, the White Rabbit'

现在看起来干净多了。接下来,我们将通过重命名和合并某些节点,进一步清理网络,特别是与白兔相关的节点。

快速视觉检查

在继续进行更多清理之前,让我们对网络进行一次快速的视觉检查。我们将重用本书中一直使用的draw_graph函数:

draw_graph(G, show_names=True, node_size=5, edge_width=1)

这输出了以下网络:

图 6.2 – 快速视觉检查网络

图 6.2 – 快速视觉检查网络

好的,我们看到什么了?我可以看到有一个大型的连接实体集群。这是《爱丽丝梦游仙境》网络的主要组成部分。

我们还看到了什么?爱丽丝是主要组件中最中心的节点。这很有意义,因为她是故事中的主角。想到主要角色,我看到许多我熟悉的名字,比如睡鼠柴郡猫白兔。但令我感兴趣的是,不仅它们被展示出来,而且我还能开始看到哪些角色对故事最重要,基于与它们连接的实体数量。然而,我也注意到红心皇后和红心国王缺席了,这让我有些失望。命名实体识别(NER)未能将它们识别为实体。从我所看到的,NER 在处理奇幻和古代名字时存在困难,因为它是基于更现实的数据训练的。它对真实名字的处理要好得多。我们将手动添加皇后宫廷的几位成员,包括国王和皇后。

我还可以看到一些奇怪的节点,它们似乎是故事的一部分,但与主要组件没有连接。为什么狮鹫与一切都断开连接?狮鹫认识谁?我们应该在故事文本中寻找这些关系。我们将手动添加这些边。

最后,我看到一些与地球上的地方有关的节点,比如尼罗河法国英格兰新西兰澳大利亚。我们可以保留这些,因为它们从技术上讲是故事的一部分,但我打算将它们移除,这样我们就可以更多地关注仙境中的角色关系社交网络。我们将移除这些。

让我们从移除非仙境节点开始:

drop_nodes = ['New Zealand', 'Australia', 'France', 'England', 'London', 'Paris', 'Rome', 'Nile', 'William_', 'Treacle', 'Fender', 'Canterbury', 'Edwin', 'Mercia', 'Morcar', 'Northumbria', 'Stigand']
G.remove_nodes_from(drop_nodes)

现在,让我们再次可视化这个网络:

图 6.3 – 快速视觉检查网络(已清理)

图 6.3 – 快速视觉检查网络(已清理)

看起来好多了。我们仍然有狮鹫像孤岛一样漂浮着,但我们很快会处理它。尽管如此,红心皇后到底在哪呢?我写了一个辅助函数来帮助调查这个问题:

from nltk.tokenize import sent_tokenize
def search_text(text, search_string):
    sentences = sent_tokenize(text)
    for sentence in sentences:
        if search_string in sentence.lower():
            print(sentence)
            print()

通过这个函数,我们可以传入任何文本和任何搜索字符串,它将输出包含搜索字符串的句子。这将帮助我们找到 NER 未能找到的实体和关系。我使用的是 NLTK 的句子分割器,而不是 spaCy,因为它更快且能更容易地得到我目前需要的结果。有时,NLTK 是更快且更简便的方式,但在这个情况下不是。

注意

要运行以下代码,你需要使用第四章第五章中的一种方法加载text变量。我们已经展示了多种加载《爱丽丝梦游仙境》文本的方法。

让我们找一下与皇后相关的文本:

search_text(text, 'queen')

这里是一些结果:

An invitation from the  Queen to play croquet.""
The Frog-Footman repeated, in the same solemn  tone, only changing the order of the words a little, "From the Queen.
"I must go and get ready to play  croquet with the Queen,"" and she hurried out of the room.
"Do you play croquet with the  Queen to-day?""
"We  quarrelled last March just before _he_ went mad, you know "" (pointing  with his tea spoon at the March Hare,) " it was at the great concert  given by the Queen of Hearts, and I had to sing    âTwinkle, twinkle, little bat!
"Well, I'd hardly finished the first verse,"" said the Hatter, "when the  Queen jumped up and bawled out, âHe's murdering the time!
The Queen's Croquet-Ground      A large rose-tree stood near the entrance of the garden: the roses  growing on it were white, but there were three gardeners at it, busily  painting them red.
"I heard the Queen say only  yesterday you deserved to be beheaded!""

运行这个函数会得到比这些更多的结果——这些只是其中的一些。但我们已经可以看到Queen of Hearts认识Frog-Footman,而Frog-Footman在我们的网络中,因此我们应该添加Queen of Hearts和其他缺失的角色,并在它们与互动的角色之间添加边。

添加节点

我们需要添加缺失的节点。由于《爱丽丝梦游仙境》是一个幻想故事,而 NER 模型通常是用更现代和现实的文本进行训练的,因此 NER 难以识别一些重要的实体,包括红心皇后。这其中有几个教训:

  • 首先,永远不要盲目相信模型。它们所训练的数据将对它们做得好或做得不好产生影响。

  • 其次,领域知识非常重要。如果我不了解《爱丽丝梦游仙境》的故事,我可能根本不会注意到皇家人物的缺失。

  • 最后,即使有缺陷,NER 和这些方法仍然能完成大部分的工作,将文本转换为网络,但你的领域知识和批判性思维将带来最佳的结果。

就像删除节点一样,networkx有两种添加节点的方法:一次一个,或者一次添加多个:

  • 我们可以只添加'Queen of Hearts'

    G.add_node('Queen of Hearts')
    
  • 或者,我们也可以一次性添加所有缺失的节点:

    add_nodes = ['Queen of Hearts', 'Frog-Footman', 'March Hare', 'Mad Hatter', 'Card Gardener #1', 'Card Gardener #2', 'Card Gardener #3', 'King of Hearts', 'Knave of Hearts', 'Mock Turtle']
    
    G.add_nodes_from(add_nodes)
    

我仍然偏好批量处理方式,因为我可以不断扩展add_nodes列表,直到我对结果满意。如果我们现在可视化网络,这些新增的节点将呈现为孤岛,因为我们尚未在它们与其他节点之间创建边:

图 6.4 – 添加缺失节点的网络

图 6.4 – 添加缺失节点的网络

看起来不错。接下来,我们来添加那些缺失的边。

添加边

我们使用了search_text函数来识别不仅是缺失的角色,还有这些角色之间缺失的关系。采用的方法如下:

  1. 找出“红心皇后”认识谁;做笔记,因为这些是缺失的边。

  2. 添加红心皇后和其他任何缺失的节点。

  3. 找出每个缺失角色认识谁;做笔记,因为这些是缺失的边。

这涉及使用search_text函数进行大量查找,并在我的 Jupyter 笔记本中将关系跟踪为注释。最终,它看起来是这样的:

图 6.5 – 识别出的缺失边

图 6.5 – 识别出的缺失边

这些是我们需要添加的已识别边。我们可能遗漏了一些,但这已经足够满足我们的需求:

  • 我们可以一次性添加一条边:

    G.add_edge('Frog-Footman', 'Queen of Hearts')
    
  • 另外,我们也可以一次性添加多个。我还是更喜欢批量方式。要使用批量方式,我们将使用一个元组列表来描述边:

    add_edges = [('Alice', 'Mock Turtle'), ('King of Hearts', 'Alice'), ('King of Hearts', 'Card Gardener #1'),
    
                 ('King of Hearts', 'Card Gardener #2'), ('King of Hearts', 'Card Gardener #3'),
    
                 ('King of Hearts', 'Dormouse'), ('King of Hearts', 'Frog-Footman'), ('King of Hearts', 'Kings'),
    
                 ('King of Hearts', 'Lizard'), ('King of Hearts', 'Mad Hatter'), ('King of Hearts', 'March Hare'),
    
                 ('King of Hearts', 'Mock Turtle'), ('King of Hearts', 'Queen of Hearts'), ('King of Hearts', 'Queens'),
    
                 ('King of Hearts', 'White Rabbit'), ('Knave of Hearts', 'King of Hearts'),
    
                 ('Knave of Hearts', 'Queen of Hearts'),
    
                 ('Queen of Hearts', 'Alice'), ('Queen of Hearts', 'Card Gardener #1'),
    
                 ('Queen of Hearts', 'Card Gardener #2'), ('Queen of Hearts', 'Card Gardener #3'),
    
                 ('Queen of Hearts', 'Dormouse'), ('Queen of Hearts', 'Frog-Footman'), ('Queen of Hearts', 'Kings'),
    
                 ('Queen of Hearts', 'Lizard'), ('Queen of Hearts', 'Mad Hatter'), ('Queen of Hearts', 'March Hare'),
    
                 ('Queen of Hearts', 'Mock Turtle'), ('Queen of Hearts', 'Queens'), ('Queen of Hearts', 'White Rabbit')]
    
    G.add_edges_from(add_edges)
    

现在我们的网络看起来如何?

图 6.6 – 添加了缺失边的网络

图 6.6 – 添加了缺失边的网络

现在看起来好多了,我们已经把女王的宫廷安排好。不过,Gryphon 仍然是一个孤岛,所以让我们查找一下缺失的关系或关系:

search_text(text, 'gryphon')

这给了我们一些文本来查看,我用它来识别缺失的边。让我们添加它们:

add_edges = [('Gryphon', 'Alice'), ('Gryphon', 'Queen of Hearts'), ('Gryphon', 'Mock Turtle')]
G.add_edges_from(add_edges)

现在,让我们再一次可视化这个网络:

图 6.7 – 添加了缺失边的网络(最终版)

图 6.7 – 添加了缺失边的网络(最终版)

啊!当一个断开的网络终于连接起来,所有的孤岛/孤立节点都得到处理时,真是一种美妙的感觉。这是干净且易于阅读的。我们已经成功地移除了垃圾节点,添加了缺失节点,并将缺失的节点连接到它们应该共享边的节点!我们可以继续前进了!

重命名节点

这个网络看起来足够好,以至于我们可能会忍不住认为我们的清理工作已经完成。然而,还有一些事情需要做,特别是对白兔的处理,还有一些其他角色。我可以看到有三个节点与白兔有关:

  • the White Rabbit

  • Rabbit

  • The Rabbit Sends

如果我们将这三个节点都重命名为White Rabbit,那么它们将合并成一个节点,它们的边也会正确连接。还有一些其他的节点也应该被重命名。以下是重命名节点的方法:

relabel_mapping = {'Cheshire':'Cheshire Cat', 'Hatter':'Mad Hatter', 'Rabbit':'White Rabbit','William':'Father William', 'the White Rabbit':'White Rabbit', 'The Rabbit Sends':'White Rabbit', 'Bill':'Lizard Bill', 'Lizard':'Lizard Bill', 'Cat':'Cheshire Cat', 'Hare':'March Hare'}
G = nx.relabel_nodes(G, relabel_mapping)

我们传入一个包含节点及其重命名的 Python 字典。例如,我们将Cheshire改为Cheshire Cat,将Hatter改为Mad Hatter

现在我们的节点看起来如何?

show_nodes(G)
'Ada, Alice, Card Gardener #1, Card Gardener #2, Card Gardener #3, Caterpillar, Cheshire Cat, Crab, Dinah, Dormouse, Duchess, Eaglet, Edgar Atheling, Elsie, Father William, Fish-Footman, Frog-Footman, Gryphon, King of Hearts, Kings, Knave of Hearts, Lacie, Lizard Bill, Lobster Quadrille, Lory, Mabel, Mad Hatter, March Hare, Mary Ann, Mock Turtle, Mouse, Pat, Pigeon, Queen of Hearts, Queens, Tillie, White Rabbit, William the Conqueror'

很好。看起来完美。我们的网络在视觉上如何呢?

图 6.8 – 重命名后的网络

图 6.8 – 重命名后的网络

完美。White Rabbit 已经正确放置,节点的颜色和位置表明它是一个核心角色,就在Alice旁边,离Queen of HeartsKing of Hearts也不远。

移除边

很可能你会遇到需要删除边的情况。这不仅对清理网络有用,还可以用于模拟攻击,或者识别团体和社区。例如,我常常使用所谓的最小割最小边割来找到将网络分割成两部分所需的最少边数。我用这个方法来进行社区检测,也用它来发现社交媒体上的新兴趋势。

对于爱丽丝梦游仙境网络,实际上没有需要删除的边,因此我将首先演示如何删除一些边,然后再演示如何将它们放回去:

  • 你可以逐一删除边:

    G.remove_edge('Dormouse', 'Tillie')
    
  • 或者,你也可以一次性删除几条边:

    drop_edges = [('Dormouse', 'Tillie'), ('Dormouse', 'Elsie'), ('Dormouse', 'Lacie')]
    
    G.remove_edges_from(drop_edges)
    

这个可视化效果怎么样?

图 6.9 – 删除边的网络

图 6.9 – 删除边的网络

这看起来正如预期的那样。如果我们删除的是ElsieTillieLacie 的节点,而不是它们的边,那么节点和边都会被删除。相反,我们删除的是边,这有点像剪断一根绳子。这三个节点现在变成了孤岛,孤立无援,什么也没有连接。

让我们把它们放回去:

add_edges = [('Dormouse', 'Elsie'), ('Dormouse', 'Lacie'), ('Dormouse', 'Tillie')]
G.add_edges_from(add_edges)

现在网络看起来怎么样?

图 6.10 – 添加边的网络

图 6.10 – 添加边的网络

完美。ElsieTillieLacie 已经回到它们应该在的位置,连接到 Dormouse。这样一来,我认为这个网络非常适合我们的用途。

持久化网络

我希望将这个网络持久化,以便我们在后面的章节中可以使用它,而无需再次进行所有这些工作。在本书中,我们将多次使用这个网络:

outfile = r'C:\blah\blah\blah\networks\alice\edgelist_alice_cleaned.csv'
final_network_df = nx.to_pandas_edgelist(G)
final_network_df.to_csv(outfile, header=True, index=False)

我正在使用微软 Windows。你的输出文件路径可能不同。

模拟攻击

我们已经完成了将一个粗略的网络边列表转换成网络、清理网络并持久化清理后的网络边列表的端到端工作流,因此接下来让我们进行一个模拟。

在大多数网络中,一些节点充当着关键枢纽。这些节点可以通过查看节点的度数(边数),或检查 PageRank 或各种中心性指标来发现。我们将在后面的章节中使用这些方法来识别重要节点。现在,我们有可以利用的领域知识。我们这些知道这个故事的人,可能能够脱口而出几个故事中的重要主角:爱丽丝、疯帽子、柴郡猫等等。而且,我们这些熟悉这个故事的人,也可能非常清楚红心女王反复喊着“砍掉他们的头!”

在一个网络中,如果你移除最连接的和最重要的节点,通常会发生类似于星球大战中死亡星爆炸的场景。瞬间,许多节点变成了孤立节点,它们的边缘也与被移除的中央节点一起被摧毁。这对网络来说是灾难性的,信息流被中断。你能想象当关键节点从网络中被移除时,现实世界会带来怎样的影响吗?你的互联网断了。电力中断了。供应链被打乱了。超市没有货物了,等等等等。理解网络并模拟中断可以为如何加强供应链和信息流提供一些思路。这就是这个练习的意义所在。

但我们要玩得开心。我们将让红心皇后大获全胜。我们将让她执行四个故事中的主要角色,然后看看会发生什么:

  1. 首先,让我们执行这些操作:

    drop_nodes = ['Alice', 'Dormouse', 'White Rabbit', 'Mad Hatter']
    
    G.remove_nodes_from(drop_nodes)
    

我们决定红心皇后成功执行了AliceDormouseWhite RabbitMad Hatter。如果真的发生了这种情况,那将是一个可怕的故事,但我们要将它演绎出来。我选择这四个角色是因为我知道他们是故事中的关键人物。移除他们应该会摧毁网络,这是我想要展示的。

  1. 仅仅移除这四个关键节点后,剩余的网络会是什么样子?这会带来什么后果?

图 6.11 – 被摧毁的爱丽丝网络

图 6.11 – 被摧毁的爱丽丝网络

灾难。我们可以看到几个节点被孤立了。中心依然有一个主组件,我们还有两个较小的组件,每个组件包含两到四个节点。但总的来说,网络已经被打碎,信息流被中断。需要重新建立新的关系,新的层级结构也需要建立。女王的宫廷已经变得主导,仅仅通过移除四个节点就实现了这一点。

  1. 让我们仔细看看主组件:

    components = list(nx.connected_components(G))
    
    main_component = components[4]
    
    G_sub = G.subgraph(main_component)
    
    draw_graph(G_sub, show_names=True, node_size=4, edge_width = 0.5)
    

这段代码中有一些需要理解的地方。首先,nx.connected_components(G)将图转换成了一个连接组件的列表。列表中的一个组件将是主组件,但它不一定是列表中的第一个组件。经过一些调查,我们发现第四个组件是主组件,所以我们将其设置为main_component,然后可视化该组件的子图。我们看到的是这样的:

图 6.12 – 女王的宫廷子图

图 6.12 – 女王的宫廷子图

女王的宫廷完好无损,包含了一些在执行发生之前不幸被困在网络中的角色。

本章到此为止!

总结

在这一章中,我们从原始数据开始,执行了一系列步骤来清理网络,甚至进行了一个非常简单的攻击模拟。

我希望到目前为止,查看和操作网络开始变得更加自然。随着我越来越多地与网络打交道,我开始在每一件事物中看到它们,并且它们影响着我对世界的理解。我们使用一个虚构的故事来展开这一章,因为它的规模适中,便于讲解构建、清理和一些简单的分析。随着你对网络的了解越来越深,你可能会发现现实中的网络通常更加杂乱、复杂且庞大。我希望这个简单的网络能为你提供所需的工具和实践,最终帮助你去解决更加雄心勃勃的问题。

在接下来的章节中,我们将会非常有趣。下一章我们将讨论如何分析整体网络。你将学到各种有用的知识,比如如何识别网络中最有影响力的节点。从现在开始,我们将进行大量的网络分析和可视化。

第三部分:网络科学与社交网络分析

在这些章节中,我们学习如何分析网络并寻找洞察。我们从整体网络分析的讨论开始,逐步缩小到节点层面,探讨自我中心网络。接着,我们会寻找网络中存在的社区和子群体。最后,我们通过展示图数据如何对监督式和无监督式机器学习有用来结束本书。

本节包括以下章节:

  • 第七章**, 整体网络分析

  • 第八章**, 自我中心网络分析

  • 第九章**, 社区检测

  • 第十章**, 网络数据上的监督式机器学习

  • 第十一章**, 网络数据上的无监督机器学习

第七章:整体网络分析

在前几章中,我们花了很多时间讲解如何通过文本构建网络以及如何清理网络数据。在本章中,我们将开始进行整体网络分析。为了简便起见,我将其简称为WNA。WNA 用来了解网络的整体情况,分析网络的密度、哪些节点在不同方面最为重要、存在哪些社区等。我将介绍一些我认为有用的内容,这些内容与大多数社交网络分析SNA)或网络科学书籍中的内容有所不同。我每天都在进行应用网络科学,我的目标是展示一些可以让读者快速开始网络分析的选项。

网络科学和 SNA 都是非常丰富的主题,如果你觉得本章某一部分特别有趣,我鼓励你自己进行研究,进一步了解。在本书中,我会引用一些 NetworkX 文档中的特定部分。请注意,这些参考页面上还有许多没有覆盖的功能,了解那些鲜为人知的函数,它们的功能及使用方式,会非常有帮助。

NetworkX 的在线文档分享了期刊文章的链接,供你阅读和学习。

在阅读本章时,我希望你考虑自己工作中遇到的问题,并尝试找出你可以将我所描述的方法应用到自己工作的方式。一旦你开始处理网络问题,你会发现它们无处不在,一旦学会如何分析和操作它们,机会的世界便会向你展开。

我们将涵盖以下主题:

  • 创建基准 WNA 问题

  • WNA 实践

  • 比较中心性

  • 可视化子图

  • 调查连通分量

  • 理解网络层次

技术要求

本章中,我们将使用 Python 库 NetworkX 和 pandas。这两个库现在应该已经安装完成,可以随时使用。如果没有安装,你可以通过以下命令安装这些 Python 库:

pip install <library name>

比如,要安装 NetworkX,你可以使用以下命令:

pip install networkx

第四章中,我们还介绍了一个draw_graph()函数,它同时使用了 NetworkX 和 Scikit-Network。每次进行网络可视化时,你都需要用到这个代码,记得随时备好!

你可以在 GitHub 仓库中找到本章所有代码:github.com/PacktPublishing/Network-Science-with-Python

创建基准 WNA 问题

在进行任何分析之前,我通常会记录下自己的一些问题。这有助于我明确自己要寻找的目标,并为我设定一个框架,去追寻这些答案。

在进行任何类型的 WNA 时,我关注的是寻找每一个问题的答案:

  • 网络有多大?

  • 网络有多复杂?

  • 网络在视觉上是什么样子的?

  • 网络中最重要的节点是什么?

  • 是不是有孤岛,还是只有一个大大陆?

  • 网络中可以找到哪些社区?

  • 网络中存在哪些桥梁?

  • 网络的层次揭示了什么?

这些问题为我提供了一个起点,可以作为我进行网络分析时的任务清单。这使我在进行网络分析时有了一个有条理的方法,而不仅仅是追随自己的好奇心。网络是嘈杂且混乱的,而这个框架为我提供了一个保持专注的工具。

修改后的 SNA 问题

在本章中,我们将使用一个 K-pop 社交网络。你可以在第二章中了解更多关于此网络数据的信息。

我的目标是了解网络的形态,以及信息如何在个体和社区之间流动。我还希望能够探索网络的不同层次,就像剥洋葱一样。核心通常特别有趣。

由于这是一个社交网络,我有一些额外的问题,超出了之前的基本问题:

  • 社交网络有多大?这意味着什么?

  • 网络的复杂性和相互连接程度如何?

  • 网络在视觉上是什么样子的?

  • 网络中最重要的人和组织是谁?

  • 网络中只有一个巨大的集群吗,还是有孤立的人群?

  • 网络中可以找到哪些社区?

  • 网络中存在哪些桥梁?

  • 网络的层次揭示了什么?

社交网络分析重访

第二章,《网络分析》中,我描述了网络科学和社会网络分析(SNA)的定义、起源和用途。尽管这两个领域是独立的研究领域,但它们有很多重叠,因此我认为社交网络是一组应该整合到网络科学中的技术。这是因为 SNA 可以很好地利用网络科学的工具和技术,而将网络科学应用于社交网络会使其更加有趣。我个人并不区分这两者。

什么是社交网络分析?在我看来,它是从社会角度看网络分析的一种不同视角。网络科学涉及的是网络如何构建、网络的属性以及网络如何随时间演变。而在社交网络分析中,我们更加关注个体。我们想知道网络中哪些人和组织是重要的,哪些个体作为社区之间的桥梁,哪些社区存在以及它们存在的原因。

内容分析是 NLP 与网络科学结合最为重要的领域。NLP 允许提取实体(人、地点和组织)并预测文本的情感分类。网络科学和 SNA 则使我们能够更深入地理解这些网络中存在的关系。因此,通过 NLP 和网络分析,你不仅可以获得内容背景,还能获得关系背景。这是一个强大的协同效应,1 + 1 = 3

在本章中,我们不会进行任何自然语言处理(NLP)。我将解释网络科学和社会网络分析(SNA)的一些功能。那么,让我们开始吧!

WNA(网络分析)实战

正如前一章所提到的,在 NetworkX 中,你可以构建无向图、有向图、多重图或多重有向图。在本章中,我们将使用无向图,因为我想展示某些功能如何帮助理解网络。需要知道的是:我接下来展示的内容如果使用其他类型的网络,会有不同的意义。当使用有向网络时,你还有更多的选择,比如研究in_degreesout_degrees,不仅仅是总度数。

加载数据并创建网络

我们需要做的第一件事是构建图形。没有图形,我们就无法进行分析:

  1. 你可以像这样从我的 GitHub 读取 K-pop 边列表:

    import pandas as pd
    
    data = 'https://raw.githubusercontent.com/itsgorain/datasets/main/networks/kpop/kpop_edgelist.csv'
    
    df = pd.read_csv(data)
    
    df['source'] = df['source'].str[0:16]
    
    df['target'] = df['target'].str[0:16]
    
    df.head()
    

预览 pandas DataFrame,我们可以看到有'source''target'两列。这正是 NetworkX 用来构建图形所需要的。如果你想为图的列命名不同的名称,NetworkX 也允许你指定自己的源和目标列。

  1. 查看边列表的形状,我们可以看到边列表中有 1,286 条边:

    df.shape[0]
    
    1286
    

记住,边是指一个节点与另一个节点之间,或者一个节点与其自身之间的关系,这被称为自环

  1. 现在我们已经准备好了 pandas 边列表,我们可以用它来构建无向图:

    import networkx as nx
    
    G = nx.from_pandas_edgelist(df)
    
    G.remove_edges_from(nx.selfloop_edges(G))
    
    G.remove_node('@') # remove a junk node
    
  2. 最后,让我们检查G,确保它是一个无向 NetworkX 图:

    G
    
    <networkx.classes.graph.Graph at 0x217dc82b4c8>
    

这看起来完美无缺,所以我们可以开始分析了。

网络的大小和复杂性

我们要调查的第一件事是网络的大小、形状和整体复杂性。让我来定义一下我的意思:

  • 网络大小:网络中节点和边的数量

  • 网络复杂性:网络中的聚类程度和密度。聚类指的是在网络中实际存在的三角形数量,密度则类似于指网络中节点之间的互联程度。

NetworkX 使得查找网络中节点和边的数量变得非常容易。你只需使用nx.info(G),如下所示:

nx.info(G)
'Graph with 1163 nodes and 1237 edges'

我们的网络有 1,163 个节点和 1,237 条边。简单来说,我们的 K-pop 社交网络由 1,163 个人和组织组成,在这 1,163 个人和组织之间,有 1,237 个已识别的互动。由于这是 Twitter 数据,因此在此情况下,互动意味着两个账户在同一条推文中被提到,意味着它们以某种方式是相关的。回到自然语言处理和内容分析的重要性,我们可以利用这些已识别的关系进一步挖掘这些关系到底是什么类型的。它们是合作关系吗?他们在争论吗?他们一起写了论文吗?社会网络分析(SNA)无法给出这些答案。我们需要内容分析来解决这些问题。但这一章是关于网络分析的,所以让我们继续。

这是一个密集的网络吗?除非你分析的是一个紧密的社群,否则互联网社交网络往往是稀疏的,而非密集的。

让我们看看网络的聚类和密度是什么样子的:

  1. 首先,让我们检查一下平均聚类:

    nx.average_clustering(G)
    
    0.007409464946430933
    

聚类的结果约为0.007,这表明这是一个稀疏的网络。如果聚类返回的结果是1.000,那就表示每个节点都与网络中的其他节点连接。在 SNA 的背景下,这意味着网络中的每个人和组织彼此相识并进行互动。但在 K-pop 中,情况显然不是这样的。并非所有的音乐人都认识他们的粉丝,粉丝们也不一定和他们最喜欢的偶像是朋友。

  1. 密度是什么样子的呢?

    from networkx.classes.function import density
    
    density(G)
    
    0.001830685967059492
    

密度给出的结果约为0.002,进一步验证了这个网络的稀疏性。

我们先不要继续。我想确保这些概念被理解。让我们构建一个完全连接的图——一个“完全”图——包含 20 个节点,并重复前面几段的步骤。NetworkX 有一些方便的函数用于生成图形,我们将使用nx.complete_graph进行演示:

  1. 让我们构建图表吧!

    G_conn = nx.complete_graph(n=20)
    
  2. 首先,让我们调查一下网络的大小:

    nx.info(G_conn)
    
    'Graph with 20 nodes and 190 edges'
    

很棒。我们有一个包含 20 个节点的网络,这 20 个节点之间有 190 条边。

  1. 但这真的是一个完全连接的网络吗?如果是的话,那么我们应该会得到1.0的聚类和密度值:

    nx.average_clustering(G_conn)
    
    1.0
    
    density(G_conn)
    
    1.0
    
  2. 完美。那正是我们预期的结果。但这个网络到底是什么样子的呢?让我们使用我们在本书中一直使用的相同函数来绘制可视化图:

    draw_graph(G_conn, edge_width=0.3)
    

这将绘制出没有节点标签的网络。

图 7.1 – 完全图

图 7.1 – 完全图

如你在网络可视化中看到的,每个节点都与其他节点相连。这是一个完全连接的网络。我们的 K-pop 网络是一个稀疏连接的网络,因此其可视化图将看起来非常不同。

网络可视化与思考

我们知道完全连接的网络是什么样子,我们也知道 K-pop 社交网络是稀疏连接的,但这到底是什么样子的呢?让我们看看:

draw_graph(G, node_size=1, show_names=False)

这将创建一个没有标签的节点和边的网络可视化。

图 7.2 – K-pop 网络

图 7.2 – K-pop 网络

需要注意的一点是,即使只有一千个节点,这仍然需要几秒钟才能渲染完成,并且无法从网络中提取任何真正的洞察。我们看到一堆小点,看到这些小点与其他小点之间的许多线条。我们还可以注意到,网络有一个核心部分,并且随着我们向网络的边缘推进,网络的稀疏度增加。本章稍后会探讨网络层的概念。关键是,除了考虑它看起来很酷之外,我们对这种可视化几乎做不了什么。至少我们可以将其可视化,而在本章的后续部分,我将解释如何“剥洋葱”以理解网络中的各种层次。

但现在为了展示一些内容,这里有一种非常快速的方法来删除所有只有一个边的节点,而这些节点占据了大部分网络。如果你这样做,你可以非常迅速地去噪网络。这是一个巨大的时间节省,因为我之前做完全相同事情的方法是如下:

  1. 使用列表推导识别每个只有一个边的节点。

  2. 从网络中移除它。

这一行代码消除了所有这些需求。K_coreG图转换为另一个只包含两个或更多边的节点的图:

draw_graph(nx.k_core(G, 2), node_size=1, show_names=False)

很简单。现在网络看起来怎么样?

图 7.3 – 简化后的 K-pop 网络

图 7.3 – 简化后的 K-pop 网络

我希望你能看到,这一个单独的步骤迅速揭示了所有只有一个边的节点下存在的网络结构。有几种方法可以简化网络,我经常使用这种方法。

重要节点

我们现在已经了解了网络的一般形态,但我们更关心的是了解谁是最重要的人物和组织。在网络科学中,存在着所谓的中心性得分,它根据节点的位置以及信息流动的方式来表示节点在网络中的重要性。NetworkX 提供了数十种不同的中心性度量方法。你可以在networkx.org/documentation/stable/reference/algorithms/centrality.html了解它们。

我将介绍一些我常用的中心性,但这些不一定是最重要的中心性。每种中心性在揭示不同的背景时都有其用途。谷歌的创始人们也创造了他们自己的中心性,著名的叫做 PageRank。PageRank 是许多数据专业人士常用的中心性,但它可能还不够全面。为了全面了解,你应该理解节点的重要性,不仅要看它们是如何连接的,还要看信息是如何流动的。让我们探索几种衡量网络中节点重要性的方法。

度数

判断网络中某个事物或人的重要性最简单的方法就是根据它与其他节点之间的连接数。以 Twitter 或 Facebook 等流行社交网络为例,网红通常连接非常广泛,而我们则会对连接非常少的账户产生怀疑。我们正在借助代码提取这个概念,从我们的网络中获取这一洞察。

在网络中,实体(如人、地方、组织等)称为节点,节点与节点之间的关系称为边。我们可以通过调查网络中节点的度数来计算每个节点的边数:

degrees = dict(nx.degree(G))
degrees
{'@kmg3445t': 1,
 '@code_kunst': 13,
 '@highgrnd': 1,
 '@youngjay_93': 1,
 '@sobeompark': 1,
 '@justhiseung': 1,
 '@hwajilla': 1,
 '@blobyblo': 4,
 '@minddonyy': 1,
 '@iuiive': 1,
 '@wgyenny': 1,
 ...
 }

现在,我们有一个包含节点及其度数的 Python 字典。如果我们将这个字典放入 pandas DataFrame 中,我们可以轻松地排序并可视化度数:

  1. 首先,我们将其加载到 pandas DataFrame 中,并按度数降序排序(从高到低):

    degree_df = pd.DataFrame(degrees, index=[0]).T
    
    degree_df.columns = ['degrees']
    
    degree_df.sort_values('degrees', inplace=True, ascending=False)
    
    degree_df.head()
    

这将展示一个 Twitter 账户及其度数的 DataFrame。

图 7.4 – 节点度数的 pandas DataFrame

图 7.4 – 节点度数的 pandas DataFrame

  1. 现在,让我们创建一个水平条形图,以便快速获取一些洞察:

    import matplotlib.pyplot as plt
    
    title = 'Top 20 Twitter Accounts by Degrees'
    
    _= degree_df[0:20].plot.barh(title=title, figsize=(12,7))
    
    plt.gca().invert_yaxis()
    

这将通过度数可视化 Twitter 账户之间的连接。

图 7.5 – 按度数排列的 Twitter 账户水平条形图

图 7.5 – 按度数排列的 Twitter 账户水平条形图

一个显著的现象是,即使是比较连接最多的前 20 个节点,度数也迅速下降。在最连接的节点之后,度数下降非常明显。网络中连接最多的节点属于歌手/词曲创作人/演员边伯贤(Byun Baek-hyun),他是组合 Exo 的成员,更为人知的名字是 Baekhyun。这很有意思。为什么他会有这么多连接?是人们在连接他,还是他在连接其他人?每一个洞察通常会引发更多可以探索的问题。把这些问题写下来,根据价值进行优先级排序,然后你可以利用这些问题进行更深入的分析。

度数中心性

度数中心性类似于根据节点的度数判断其重要性。度数中心性是网络中一个节点与其他节点连接的比例。一个节点的度数越多,它与其他节点连接的比例就越高,因此度数和度数中心性可以互换使用:

  1. 我们可以计算网络中每个节点的度数中心性:

    degcent = nx.degree_centrality(G)
    
    degcent
    
    {'@kmg3445t': 0.0008605851979345956,
    
     '@code_kunst': 0.011187607573149742,
    
     '@highgrnd': 0.0008605851979345956,
    
     '@youngjay_93': 0.0008605851979345956,
    
     '@sobeompark': 0.0008605851979345956,
    
     '@justhiseung': 0.0008605851979345956,
    
     '@hwajilla': 0.0008605851979345956,
    
     '@blobyblo': 0.0034423407917383822,
    
     '@minddonyy': 0.0008605851979345956,
    
     '@iuiive': 0.0008605851979345956,
    
     ...
    
     }
    
  2. 我们可以用它来创建另一个 pandas DataFrame,按度数中心性降序排序:

    degcent_df = pd.DataFrame(degcent, index=[0]).T
    
    degcent_df.columns = ['degree_centrality']
    
    degcent_df.sort_values('degree_centrality', inplace=True, ascending=False)
    
    degcent_df.head()
    

这将显示一个 Twitter 账户及其度数中心性的 DataFrame。

图 7.6 – 节点度数中心性的 pandas DataFrame

图 7.6 – 节点度数中心性的 pandas DataFrame

  1. 最后,我们可以将其可视化为一个横向条形图:

    title = 'Top 20 Twitter Accounts by Degree Centrality'
    
    _= degcent_df[0:20].plot.barh(title=title, figsize=(12,7))
    
    plt.gca().invert_yaxis()
    

这将绘制一个按度数中心性排序的 Twitter 账户横向条形图。

图 7.7 – 按度数中心性排序的 Twitter 账户横向条形图

图 7.7 – 按度数中心性排序的 Twitter 账户横向条形图

你注意到度数和度数中心性的条形图除了数值外,看起来几乎一模一样吗?这就是我说它们可以互换使用的原因。使用度数通常会更容易解释和辩护。

介数中心性

介数中心性与信息如何在网络中流动有关。如果一个节点位于其他两个节点之间,那么这两个节点中的任何一个的信息必须通过位于它们之间的节点传递。信息通过位于中间的节点流动。这个节点可以被视为一个瓶颈,或者是一个优势的地方。拥有他人所需信息的节点可以提供战略上的优势。

然而,通常情况下,介数中心性较高的节点位于多个节点之间,而不仅仅是两个节点之间。这通常出现在一个启动网络中,其中一个核心节点连接到几十个或更多的其他节点。想象一下一个社交媒体上的网红。这个人可能与 2200 万粉丝相连,但这些粉丝之间很可能互不相识。他们肯定认识这个网红(或者是一个虚假的机器人)。这个网红就是一个核心节点,介数中心性将会体现这一点。

在我们了解如何计算介数中心性之前,请注意,计算介数中心性对于大型或密集的网络来说非常耗时。如果你的网络较大或密集,且导致介数中心性的计算速度慢到无法使用,考虑使用其他中心性指标来计算重要性:

  1. 我们可以计算网络中每个节点的介数中心性:

    betwcent = nx.betweenness_centrality(G)
    
    betwcent
    
    {'@kmg3445t': 0.0,
    
     '@code_kunst': 0.016037572215773392,
    
     '@highgrnd': 0.0,
    
     '@youngjay_93': 0.0,
    
     '@sobeompark': 0.0,
    
     '@justhiseung': 0.0,
    
     '@hwajilla': 0.0,
    
     '@blobyblo': 0.02836579219003866,
    
     '@minddonyy': 0.0,
    
     '@iuiive': 0.0,
    
     '@wgyenny': 0.0,
    
     '@wondergirls': 0.0013446180439736057,
    
     '@wg_lim': 0.0026862711087984274,
    
     ...
    
     }
    
  2. 我们可以用它来创建另一个 pandas DataFrame,按介数中心性降序排序:

    betwcent_df = pd.DataFrame(betwcent, index=[0]).T
    
    betwcent_df.columns = ['betweenness_centrality']
    
    betwcent_df.sort_values('betweenness_centrality', inplace=True, ascending=False)
    
    betwcent_df.head()
    

这将显示一个 Twitter 账户及其介数中心性的 DataFrame。

图 7.8 – 节点介数中心性的 pandas DataFrame

图 7.8 – 节点介数中心性的 pandas DataFrame

  1. 最后,我们可以将其可视化为一个横向条形图:

    title = 'Top 20 Twitter Accounts by Betweenness Centrality'
    
    _= betwcent_df[0:20].plot.barh(title=title, figsize=(12,7))
    
    plt.gca().invert_yaxis()
    

这将绘制一个按介数中心性排序的 Twitter 账户横向条形图。

图 7.9 – 按介数中心性排序的 Twitter 账户横向条形图

图 7.9 – 按介数中心性排序的 Twitter 账户横向条形图

请注意,条形图与度数和度中心性的图表非常不同。还要注意,@youtube@spotifykr@kchartsmaster 是具有最高中介中心性的节点。这可能是因为艺术家和其他人在推特中提到 YouTube、Spotify 和 KChartsMaster。这些节点位于节点和其他节点之间。

接近中心性

接近中心性与节点之间的接近程度有关,这与被称为最短路径的概念相关,计算大规模或密集网络的最短路径是计算上昂贵且缓慢的。因此,接近中心性可能比中介中心性还要慢。如果由于网络的大小和密度,获取接近中心性结果太慢,你可以选择其他中心性度量来评估重要性。

最短路径将在另一个章节中讨论,它与从一个节点到另一个节点所需的跳数或握手次数有关。这是一个非常缓慢的操作,因为涉及到许多计算:

  1. 我们可以计算网络中每个节点的接近中心性:

    closecent = nx.closeness_centrality(G)
    
    closecent
    
    {'@kmg3445t': 0.12710883458078617,
    
     '@code_kunst': 0.15176930794223495,
    
     '@highgrnd': 0.12710883458078617,
    
     '@youngjay_93': 0.12710883458078617,
    
     '@sobeompark': 0.12710883458078617,
    
     '@justhiseung': 0.12710883458078617,
    
     '@hwajilla': 0.12710883458078617,
    
     '@blobyblo': 0.18711010406907921,
    
     '@minddonyy': 0.12710883458078617,
    
     '@iuiive': 0.12710883458078617,
    
     '@wgyenny': 0.07940034854856182,
    
     ...
    
     }
    
  2. 我们可以用它来创建另一个 pandas 数据框,按接近中心性降序排序:

    closecent_df = pd.DataFrame(closecent, index=[0]).T
    
    closecent_df.columns = ['closeness_centrality']
    
    closecent_df.sort_values('closeness_centrality', inplace=True, ascending=False)
    
    closecent_df.head()
    

这将展示一个包含 Twitter 账户及其接近中心性的数据框。

图 7.10 – pandas 数据框,节点的接近中心性

图 7.10 – pandas 数据框,节点的接近中心性

  1. 最后,我们可以将其可视化为水平条形图:

    title = 'Top 20 Twitter Accounts by Closeness Centrality'
    
    _= closecent_df[0:20].plot.barh(title=title, figsize=(12,7))
    
    plt.gca().invert_yaxis()
    

这将绘制一个根据接近中心性排序的 Twitter 账户水平条形图。

图 7.11 – 根据接近中心性排序的 Twitter 账户水平条形图

图 7.11 – 根据接近中心性排序的 Twitter 账户水平条形图

请注意,结果与我们之前看到的其他任何中心性度量都不同。@blackpink 排名第一,其次是 @youtube@kchartsmaster@spotifykr。BLACKPINK 是著名的 K-pop 女团,它们在 K-pop 网络中有很强的连接性,能够获得影响力。其他 K-pop 艺术家可能需要调查 BLACKPINK 在做什么,才能使其处于一个战略上有利的网络位置。

PageRank

最后,PageRank 是 Google 搜索背后的算法。Google 的创始人在 1999 年发表了这篇论文:ilpubs.stanford.edu:8090/422/1/1999-66.pdf。如果你曾经在 Google 上搜索过任何内容,那么返回的结果部分是因为 PageRank,尽管自 1999 年以来,搜索算法可能已经发生了显著的演变。

PageRank 的数学公式不仅考虑了目标节点的入度和出度,还考虑了连接节点的入度和出度。这也是为什么搜索引擎优化SEO)成为一项重要工作,因为人们了解到,要获得 Google 的高排名,一个网站应该尽可能多地拥有外部链接,并且还要链接到其他信息来源。如需了解 PageRank 背后的数学原理,请查看斯坦福大学的 PDF 文档。

PageRank 是一个非常快速的算法,适用于大规模和小规模的网络,并且作为重要性度量非常有用。许多图解决方案在其工具中提供 PageRank 功能,许多人将 PageRank 视为首选的中心性。就个人而言,我认为你应该了解几种中心性,了解它们的应用场景及其局限性。PageRank 在大型和密集网络中也非常有用,因此我建议在进行任何中心性分析时都包含它:

  1. 我们可以计算网络中每个节点的 PageRank 得分:

    pagerank = nx.pagerank(G)
    
    pagerank
    
    {'@kmg3445t': 0.00047123124840596525,
    
     '@code_kunst': 0.005226313735064201,
    
     '@highgrnd': 0.00047123124840596525,
    
     '@youngjay_93': 0.00047123124840596525,
    
     '@sobeompark': 0.00047123124840596525,
    
     '@justhiseung': 0.00047123124840596525,
    
     '@hwajilla': 0.00047123124840596525,
    
     '@blobyblo': 0.0014007295303692594,
    
     '@minddonyy': 0.00047123124840596525,
    
     ...
    
     }
    
  2. 我们可以使用这个来创建另一个按 PageRank 降序排序的 pandas DataFrame:

    pagerank_df = pd.DataFrame(pagerank, index=[0]).T
    
    pagerank_df.columns = ['pagerank']
    
    pagerank_df.sort_values('pagerank', inplace=True, ascending=False)
    
    pagerank_df.head()
    

这将显示一个 Twitter 账户及其 PageRank 得分的数据框。

图 7.12 – 节点的 PageRank 得分的 pandas DataFrame

图 7.12 – 节点的 PageRank 得分的 pandas DataFrame

  1. 最后,我们可以将其可视化为水平条形图:

    title = 'Top 20 Twitter Accounts by Page Rank'
    
    _= pagerank_df[0:20].plot.barh(title=title, figsize=(12,7))
    
    plt.gca().invert_yaxis()
    

这将绘制一个按 PageRank 排序的 Twitter 账户水平条形图。

图 7.13 – 按 PageRank 排序的 Twitter 账户水平条形图

图 7.13 – 按 PageRank 排序的 Twitter 账户水平条形图

这些结果实际上与度和度中心性所得到的条形图非常相似。再次,Exo 的 Baekhyun 位居榜首。

边的中心性

在结束这一部分关于中心性的内容之前,我想指出,你并不仅限于节点的中心性。也有边的中心性。例如,边介数中心性可以用来识别连接最多节点的边。如果你剪断了这条连接最多节点的边,网络通常会被分割成两个大块,称为连通分量。这实际上对识别社区或新兴趋势非常有用,我们将在后续章节中深入探讨。

比较中心性

为了了解不同中心性之间的差异,或者将多种中心性一起使用(例如,在构建机器学习分类器并希望使用图度量时),将不同的中心性合并成一个 pandas DataFrame 可能非常有用。你可以通过 pandas 的concat函数轻松实现:

combined_importance_df = pd.concat([degree_df, degcent_df, betwcent_df, closecent_df, pagerank_df], axis=1)
combined_importance_df.head(10)

这将把我们的所有中心性和 PageRank DataFrame 合并成一个统一的 DataFrame。这样可以更方便地比较不同类型的中心性。

图 7.14 – 合并重要性指标的 pandas DataFrame

图 7.14 – 综合重要性度量的 pandas DataFrame

你可能会注意到,如果你按不同类型的中心性排序,一些中心性结果非常相似,而其他则差异很大。我留给你这个结论:没有一个中心性能够统治所有的网络。它们是不同的,应在不同的情境下使用。如果你正在绘制信息流的图谱,那么介数中心性非常有用,只要网络的规模是可管理的。如果你只想查看网络中哪些节点最为连接,可以通过查看节点的度数来最轻松地做到这一点。如果你想了解哪些节点与每个其他节点的距离最短,可以尝试使用接近中心性。如果你想要一个能很好地识别重要节点,并且即使在大型网络上也能高效运行的算法,可以尝试 PageRank:

combined_importance_df.sort_values('pagerank', ascending=False)[0:10]

这将展示一个包含 Twitter 账户和综合网络中心性及 PageRank 得分的 DataFrame。

图 7.15 – 按 PageRank 排序的综合重要性度量的 pandas DataFrame

图 7.15 – 按 PageRank 排序的综合重要性度量的 pandas DataFrame

只需要知道,即使是 PageRank 和介数中心性也可能给出非常不同的结果,因此你应该学习几种不同的衡量重要性的方法,并了解你想要做什么。这些对初学者来说可能是非常陌生的,但不要害怕,跳进去学习吧。NetworkX 文档中的文档和相关期刊将足以帮助你入门。

如果你刚刚开始进行社交网络分析和网络科学,中心性可能是这一章中最不寻常的部分。从这一章节开始,接下来的概念应该就不那么陌生了。

可视化子图

在网络分析中,我们常常希望查看网络的一部分,以及该部分中的节点如何彼此连接。例如,如果我有一个 100 个感兴趣的网页域名或社交媒体账号的列表,那么创建一个包含所有节点的子图来进行分析和可视化可能会非常有用。

对于子图的分析,本章中的所有内容仍然适用。例如,你可以在子图中使用中心性来识别一个社区中的重要节点。你还可以使用社区检测算法来识别子图中存在的社区,尤其是当这些社区尚未被识别时。

当你想去除网络中的大部分噪音并研究某些节点之间的交互时,可视化子图也非常有用。可视化子图与我们可视化整个网络、个人图和时序图的方式是一样的。但创建子图需要稍微花费一点工作。首先,我们需要识别出感兴趣的节点,然后我们需要构建一个仅包含这些节点的子图,最后,我们将可视化这个子图:

  1. 举个例子,让我们选择网络中 PageRank 得分最高的 100 个节点:

    subgraph_nodes = pagerank_df[0:100].index.to_list()
    
    subgraph_nodes
    
    ['@b_hundred_hyun',
    
     '@zanelowe',
    
     '@haroobomkum',
    
     '@itzailee',
    
     '@spotifykr',
    
     '@shxx131bi131',
    
     '@thinktwicekpop',
    
     '@leehi_hi',
    
     '@bambam1a',
    
     '@bighitent',
    
     '@ericnamofficial',
    
     '@twicetly',
    
     ...
    
     ]
    

很简单。我这里只展示了几个节点,因为整个屏幕滚动下去时,节点会显示得很远。

  1. 接下来,我可以构建一个子图,如下所示:

    G_sub = G.subgraph(subgraph_nodes)
    
  2. 最后,我可以像可视化任何其他网络一样进行可视化:

    draw_graph(G_sub, node_size=3)
    

在这个例子中,我省略了节点名称,但我也可以很容易地将它们添加进来。我觉得不加节点名称会使这个示例的可视化更简洁。

图 7.16 – 按 PageRank 排序的前 100 个 K-pop Twitter 账号的子图可视化

图 7.16 – 按 PageRank 排序的前 100 个 K-pop Twitter 账号的子图可视化

就这样。实际上,关于子图创建的内容并不多,除了它是可行的以及如何做。只要知道方法,过程是简单的。

调查岛屿和大陆——连接组件

如果你查看子图的可视化图,你可能会注意到有一个大的节点簇,几个小的节点岛屿(有两条或更多的边),以及几个孤立节点(没有边的节点)。这是许多网络中常见的现象。通常,网络中会有一个巨大的超级簇,几个中等大小的岛屿,以及许多孤立节点。

这带来了挑战。当一些人刚开始接触网络分析时,他们通常会对网络进行可视化,并使用 PageRank 来识别重要节点。但这远远不够。提取网络中的洞察有很多不同的方法,我将在本书的过程中向你展示几种方法。

但是有一种非常简单的方式可以去除噪声,那就是识别网络中存在的大陆和岛屿,利用它们创建子图,然后分析和可视化这些子图。

这些大陆岛屿在正式术语中称为连接组件。连接组件是一个网络结构,其中每个节点至少与另一个节点相连。实际上,NetworkX 允许孤立节点存在于自己的连接组件中,这让我感到奇怪,因为孤立节点除了可能自连接外,实际上没有与任何其他节点相连(自环存在)。

在网络中找到所有存在的连接组件是非常容易的:

components = list(nx.connected_components(G))
len(components)

我这里做了两件事:首先,我将我们G图的所有连接组件加载到一个 Python 列表中,然后计算存在的组件数量。K-pop 网络中有 15 个连接组件。

很好,但这 15 个中哪些是大陆,哪些是岛屿呢?通过一个简单的循环,我们可以计算每个连接组件中存在的节点数:

for i in range(len(components)):
    component_node_count = len(components[i])
    print('component {}: {}'.format(i, component_node_count))

这将给我们一个连接组件的列表,以及属于该连接组件的节点数:

component 0: 909
component 1: 2
component 2: 3
component 3: 4
component 4: 2
component 5: 2
component 6: 80
component 7: 129
component 8: 3
component 9: 7
component 10: 4
component 11: 4
component 12: 2
component 13: 10
component 14: 2

完美。注意到其中一个组件有 909 个节点。这是网络中可能存在的大型“大陆”之一。还要注意到,组件中有 80 和 129 个节点。这比最大连通组件中的节点数量少得多,但仍然是一个相当大的节点数。我把这些看作是岛屿。最后,注意到还有几个组件的节点数在 2 到 10 之间。这些就像是小岛屿。

每一个连通组件都可以作为子图进行分析和可视化。为了简化可视化,我将创建一个辅助函数,扩展我的主要draw_graph函数:

def draw_component(G, component, node_size=3, show_names=True)
    check_component = components[component]
    G_check = G.subgraph(check_component)
    return draw_graph(G_check, show_names=show_names, node_size=node_size)

让我们试试看吧。让我们可视化一个随机组件,组件 13:

draw_component(G, component=13, node_size=5)

它是怎么呈现的?

图 7.17 – 连通组件 #13 的子图可视化

图 7.17 – 连通组件 #13 的子图可视化

看起来不错。我们已经成功地可视化了整个网络中的一个单一组件。接下来,让我们可视化最大的组件:

draw_component(G, 0, show_names=False, node_size=2)

图 7.18 – 连通组件 #0 的子图可视化

图 7.18 – 连通组件 #0 的子图可视化

再次回到那个巨大的、混乱的线团。虽然我们成功地进行了可视化,但我们可以通过去除所有只有一个边的节点来大大简化它,比如这样做。

连通组件有点不寻常,就像中心性一样,但如果你把它们看作是存在于网络中的岛屿和大陆,那就能去除很多神秘感。总的来说,在一个网络中,通常会有几个连通组件,我把它们看作是大陆、岛屿或孤立点。在大多数网络中,通常至少有一个大型大陆,几个岛屿,以及零到多个孤立点。孤立点的数量取决于图的构建方式。使用我们在前几章中提到的 NER 方法,实际上是没有孤立点的。

我们将在第九章中查看更多关于连通组件的内容。

社区

社区检测算法在各种形式的网络分析中非常有用。在 WNA 中,它们可以用来识别整个网络中存在的社区。当应用于以自我为中心的网络(自我图)时,它们可以揭示围绕单个节点存在的社区和团体;在时间网络中,它们可以用来观察社区随时间的演变。

社区检测在社会网络分析(SNA)中很常见,因为在人口庞大的群体中存在不同的社群,识别这些社群是非常有用的。社区检测有多种方法,网络上有很多关于社区检测算法如何工作的资料。本书主要讲解应用网络科学,因此我只会演示其中一种,叫做Louvain 算法。与中心性一样,没有“最佳”的算法。我曾经参加过一些讨论,其中有人提到了一种边缘算法,并坚信它更好;也有讨论中人们更倾向于使用 Louvain 算法。

您可以在这里了解更多关于 Louvain 算法的信息:python-louvain.readthedocs.io/en/latest/

  1. Louvain 算法并未随 NetworkX 一起提供。您需要安装它,安装方法非常简单,如下所示:

    pip install python-louvain
    
  2. 之后,您可以通过以下方式导入该库进行使用:

    import community as community_louvain
    
  3. 为了节省大量时间并跳过数学部分,Louvain 算法可以识别节点所属的各种分区(社区)。与我们通常的网络可视化相比,可视化这些分区有点棘手,因为scikit-network对节点着色的灵活性并不高。为了节省时间,我将回到我以前的网络可视化实践,并使用 NetworkX 进行可视化。下面是绘制图形和着色社区的代码:

    def draw_partition(G, partition):
    
        import matplotlib.cm as cm
    
        import matplotlib.pyplot as plt
    
        # draw the graph
    
        plt.figure(3,figsize=(12,12))
    
        pos = nx.spring_layout(G)
    
        # color the nodes according to their partition
    
        cmap = cm.get_cmap('flag', max(partition.values()) + 1)
    
        nx.draw_networkx_nodes(G, pos, partition.keys(), node_size=20, cmap=cmap, node_color=list(partition.values()))
    
        nx.draw_networkx_edges(G, pos, alpha=0.5, width=0.3)
    
        return plt.show()
    
  4. 现在我们已经有了可视化功能,我们需要先识别分区,然后我们需要可视化网络。让我们将这两步一起完成。我在经过一些调整后使用resolution=2,因为社区布局看起来最优:

    partition = community_louvain.best_partition(G, resolution=2)
    
    draw_partition(G, partition)
    

看起来怎么样?

图 7.19 – 社区分区的可视化

图 7.19 – 社区分区的可视化

这些图像对我来说虽然凌乱,但却令人着迷。我可以轻松地看到一些以前从未注意到的、易于区分的社区。但是它们是什么呢?哪些节点属于每个社区?将这个分区列表转换为 pandas DataFrame 非常简单,我们可以利用它来识别社区,统计每个社区中节点的数量,确定某个节点属于哪个社区,并可视化各个社区:

  1. 首先,让我们从分区列表创建一个 pandas DataFrame:

    community_df = pd.DataFrame(partition, index=[0]).T
    
    community_df.columns = ['community']
    
    community_df.head()
    

现在看起来怎么样?

图 7.20 – pandas DataFrame 中的社区分区

图 7.20 – pandas DataFrame 中的社区分区

  1. 这看起来不错。我们可以看到,它已经按分区编号排序,我称之为community。现在它已经是一个 pandas DataFrame,统计每个社区中属于的节点数量变得很简单:

    community_df['community'].value_counts()
    

这将为我们提供一个社区列表(左侧数字)以及该社区中节点的数量(右侧数字):

21    170
10    133
14    129
16    104
2      91
3      85
13     80
23     70
0      66
15     55
4      51
22     48
1      36
17     10
19      7
9       4
20      4
5       4
8       3
18      3
12      2
11      2
7       2
6       2
24      2

我们可以轻松看到哪些社区拥有最多的节点。我们应该使用子图来分析和可视化这些社区,正如前面所解释的那样。

  1. 那么我们如何识别每个社区中的节点呢?我们可以直接在 pandas 中进行。以下是一个简单的辅助函数:

    def get_community_nodes(commmunity_df, partition):
    
        community_nodes = community_df[community_df['community']==partition].index.to_list()
    
        return community_nodes
    
  2. 我们可以直接使用这个功能,但我更倾向于将这些community节点提取出来,创建一个子图并进行可视化。以下是实现这一功能的辅助函数:

    def draw_community(G, community_df, partition, node_size=3, show_names=False):
    
        community_nodes = get_community_nodes(community_df, partition)
    
        G_community = G.subgraph(community_nodes)
    
        return draw_graph(G_community, node_size=node_size, show_names=show_names)
    

我们来试试一个:

draw_community(G, community_df, 1, show_names=True)

图 7.21 – 社区 #1 的子图可视化

图 7.21 – 社区 #1 的子图可视化

运行后,我可以看到这个可视化效果。如果你看到的是不同的结果,不用担心。在处理网络时,像连通分量和社区编号等东西,在下一次运行时位置不一定相同。

非常酷。这感觉与可视化连通分量非常相似,但社区不一定是孤岛或大陆。例如,多个社区可以出现在一个大型的连通分量中。算法寻找分隔节点群体的边界,然后据此标记社区。

如果你从事网络工作,特别是如果你有兴趣识别社交网络中存在的社群,你会想要尽可能多地了解如何识别团体和社区。尝试不同的算法。我选择了 Louvain,因为它快速且可靠,即使在大型网络中也是如此。

桥接节点

简单来说,桥接节点是位于两个不同社区之间的节点。在小型社交网络中,这些节点通常很容易被目视识别,因为会有一个或几个看起来像橡皮筋的连接强度,如果被剪断,两个群体就会分开。就像桥梁让人们可以跨越水面从一块陆地走到另一块陆地一样,网络中的桥接节点也能让信息从一个社区传播到另一个社区。作为人类,身处桥接节点是一个强有力的位置,因为信息和资源必须通过你才能传递到另一方。

在复杂的网络中,桥接节点更难以目视识别,但它们通常存在,位于两个社区之间。我们的 K-Pop 网络相当复杂,因此网络比在较小的社交网络中更不容易看出,但它们确实存在。

  1. 你可以像这样在网络中找到桥接节点:

    list(nx.bridges(G))
    
    [('@kmg3445t', '@code_kunst'),
    
     ('@code_kunst', '@highgrnd'),
    
     ('@code_kunst', '@youngjay_93'),
    
     ('@code_kunst', '@sobeompark'),
    
     ('@code_kunst', '@justhiseung'),
    
     ('@code_kunst', '@hwajilla'),
    
     ('@code_kunst', '@blobyblo'),
    
     ('@code_kunst', '@minddonyy'),
    
     ('@code_kunst', '@iuiive'),
    
     ('@code_kunst', '@eugenius887'),
    
     ...
    
    ]
    
  2. 这是一个非常长的桥接节点列表,我这里只展示了一部分行,但我们可以结合 pandas 来识别最重要的桥接节点:

    bridges = [s[0] for s in list(nx.bridges(G))]
    
    pd.Series(bridges).value_counts()[0:10]
    
    @b_hundred_hyun    127
    
    @zanelowe           90
    
    @haroobomkum        84
    
    @itzailee           78
    
    @spotifykr          60
    
    @shxx131bi131       57
    
    @thinktwicekpop     53
    
    @leehi_hi           53
    
    @bambam1a           49
    
    @bighitent          46
    
  3. 删除桥接节点的一个副作用是,它可能类似于删除高度中心的节点——网络会碎裂成一大群孤立节点和一些较小的连通分量。让我们把拥有最多边的 10 个桥接节点删除:

    cut_bridges = pd.Series(bridges).value_counts()[0:10].index.to_list()
    
    G_bridge_cut = G.copy()
    
    G_bridge_cut.remove_nodes_from(cut_bridges)
    
  4. 做完这一步后,我们的网络可能看起来像一颗超级新星爆发,碎片飞向太空。让我们来看看:

    draw_graph(G_bridge_cut, show_names=False)
    

这应该会绘制一个没有节点标签的网络。

图 7.22 – 切割主要桥梁后的网络可视化

图 7.22 – 切割主要桥梁后的网络可视化

如我们所见,网络中心仍然存在一个密集连接的组件,少数由几个节点组成的小型连接组件,以及许多独立的孤立节点。切割桥梁并不总是如此具有破坏性。在我处理的其他网络中,存在一个由两个社区组成的核心结构,几个节点位于这两个社区之间作为桥梁。当移除这些桥梁时,网络的核心社区就会分裂开来,几乎没有或没有孤立节点。

识别桥梁是有原因的。在社交网络中,这些节点是信息必须经过的节点,才能到达另一边的社区。如果你想在这些网络中战略性地定位自己,理解桥梁节点的作用,并模仿他们所做的,和两边的人建立联系,将会是通向权力的捷径。

同样,如果你的目标是禁用一个网络,识别并移除重要的桥梁将会停止信息从一个社区流向另一个社区。这将造成高度的干扰。这对于破坏暗网(如犯罪、仇恨等)可能非常有用。

这些是可以从网络中提取的有用见解,没有网络分析很难识别。识别桥梁并为处理它们制定计划,可以提供战略优势。你可以利用它们来获取权力,也可以用来破坏网络,或者将网络社区分离,进行更清晰的分析。

使用 k_core 和 k_corona 理解网络的层次

网络可以被看作是洋葱,通常也以类似的方式进行可视化,孤立节点位于最外层,接下来是具有单一边缘的节点,然后是具有两条边的节点,依此类推,直到到达网络的核心。NetworkX 提供了两种方法来“剥洋葱”,即 k_corek_corona

k_core

NetworkX 的 k_core 函数允许我们轻松将网络简化为仅包含具有 k 条或更多边缘的节点,其中 "k" 是一个介于 0 和网络中任意节点的最大边数之间的数字。因此,您得到的是一个包含 k 条或更多边缘的网络“核心”。如果执行 k_core(G, 2),则返回的图形将仅包含那些具有两条或更多边缘的节点,同时移除了孤立节点和仅有一个边缘的节点,一步完成。

这一单一的网络去噪步骤可能看起来没什么大不了,但如果通过列表推导或循环来实现,需要更多的步骤、更多的思考以及更多的调试。这一步骤轻松地完成了清理工作。因此,当我最关注去除孤立节点和单边节点后的网络形态时,k_core(G, 2) 是我代码中常见的操作。

比如说,这就是我们完整的 K-pop 网络渲染出来的样子。很难看清任何东西,因为那些单边节点已经将网络可视化变成了一团乱麻。

图 7.23 – 整个网络的可视化

图 7.23 – 整个网络的可视化

然而,我们可以很容易地删除所有度数小于两个的节点:

G_core = nx.k_core(G, 2)

那么网络现在看起来怎么样?

draw_graph(G_core, show_names=True, node_size=3)

这应该绘制出我们的G_core网络,并显示节点标签。

图 7.24 – 带有 k_core 和 k=2 的整个网络可视化

图 7.24 – 带有 k_core 和 k=2 的整个网络可视化

很明显,这样要更容易解释。

学习k_core是我学习如何分析图形和社交网络过程中最重要的时刻之一。我以前是通过不太直接的方法来去噪网络,识别度数少于两个的节点,将它们添加到列表中,然后从网络中删除它们。这个单一功能节省了我大量的时间。

k_corona

正如k_core允许我们提取网络的核心一样,k_corona让我们可以研究网络的每一层。k_corona不是为了找出核心,而是为了研究网络每一层发生的事情。例如,如果我们只想查看那些有零条或一条边的节点,我们可以这样做:

G_corona = nx.k_corona(G, 1)

这将显示一堆孤立节点,并且可能还会有一些节点之间存在一条边:

  1. 首先,让我们可视化k_corona(G, 1)的结果:

    draw_graph(G_corona, show_names=False, node_size=2)
    

这应该渲染出所有拥有一个或更少边的节点的网络可视化。没有任何边的节点称为孤立节点,将以点的形式显示。

图 7.25 – k_corona k=1 层的可视化

图 7.25 – k_corona k=1 层的可视化

如我们所见,有很多孤立节点。你能识别出那些只有一条边连接的节点吗?我看不出来。这就像在读那本《在哪里是沃尔多?》一样。那么,我们如何识别出这个层次中有边相连的节点呢?如何移除所有度数小于一的节点呢?想一想。

  1. 没错,我们将使用k_core进行清理:

    G_corona = nx.k_corona(G, 1)
    
    G_corona = nx.k_core(G_corona, 1)
    

如果我们将其可视化,可以看到有五个连接组件,每个组件包含两个节点,每个节点与该组件中另一个节点之间有一条边。

draw_graph(G_corona, show_names=True, node_size=5, font_size=12)

这将绘制出G_corona网络,孤立节点已被移除,并且显示节点标签。

图 7.26 – k_corona k=1 层的简化可视化

图 7.26 – k_corona k=1 层的简化可视化

  1. 有没有简单的方法来提取这些节点,以便我们可以进一步分析它们?是的,很简单:

    corona_nodes = list(G_corona.nodes)
    
    corona_nodes
    

这将显示我们corona_nodes中所有节点的列表:

['@day6official',
 '@9muses_',
 '@bol4_official',
 '@8_ohmygirl',
 '@withdrama',
 '@elris_official',
 '@hunus_elris',
 '@miiiiiner_misog',
 '@into__universe',
 '@shofarmusic']
  1. 第二层网络看起来如何,即每个节点具有两个度数的层?这些层上的节点是否彼此连接?让我们创建并呈现这个可视化:

    G_corona = nx.k_corona(G, 2)
    
    draw_graph(G_corona, show_names=True, node_size=3)
    

这将呈现一个所有节点边数为两个或更少的网络可视化。

图 7.27 – k=2 层的 k_corona 可视化

图 7.27 – k=2 层的 k_corona 可视化

它看起来非常类似于第一层的k_corona,但我们可以更容易地看到一些节点连接到其他节点。我们还可以看到这一层中孤立节点显著减少。我们可以重新进行k_core步骤来清理,但我认为你已经理解了重点。

就我个人而言,我并不经常使用k_corona。我对逐层剥离网络并没有太大兴趣,但这是一个选择,也许对你而言比对我更有用。然而,我几乎每次处理网络时都会使用k_core,用于去噪声网络,并研究存在于社交网络核心的核或核。我建议你了解这两者,但可能你对k_core的需求比对k_corona更大。不过,k_corona为分析开启了一些有趣的可能性。

挑战自己!

在结束这一章之前,我想向你提出一个挑战。你已经学会了如何使用文本创建网络,边缘列表的样子以及如何在 pandas 中构建一个,如何创建网络,如何清理网络,现在你已经介绍了整体网络分析。你现在拥有开始你的网络分析之旅所需的每一样工具。我将在后续章节中详细解释如何做更多事情,但你已经拥有了开始并迷上网络分析所需的所有工具。

我想挑战你,思考一下你自己的数据,关于你处理的数据,你玩耍的社交网络,你协作的工作网络,以及更多。我希望你考虑如何将这些活跃的网络描述成边缘列表(只是源和目标列),绘制网络可视化,并分析这些网络。你有能力做到这一点,而网络无处不在。我建议,当你学习调查社交网络和各种网络时,使用对你真正有趣的数据。你不需要在网上找数据集。你可以轻松地自己创建一个,或者像我们在之前章节中探索的那样,抓取社交媒体上的数据。

我挑战你,在这一章节停下来,玩上一会儿。在网络中迷失。重新阅读本书中的前几章。探索。变得怪异。玩得开心。这是我最喜欢的学习方式。我非常享受构建我自己的数据集,并分析我最感兴趣的事物。

总结

我们在本章中走了很长一段路。本章内容可以独立成书,但我的目标是快速带你了解网络中可以做的事情。正如我在开始时所说,这本书不会是一本数学书。我想为你开启新的能力和机会,我相信本章和本书能够为你实现这一点。

在本章中,我们涉及了很多内容:解释了整体网络分析,描述了有助于分析的问题,并花了很多时间进行实际的网络分析。我们从整体上看待网络,同时也研究了节点中心性、连接组件和层级。

在下一章中,我们将学习自我中心网络分析。我们将其称为自我网络,为了简洁。在那一章中,我们将聚焦于感兴趣的节点,了解它们周围的社区和节点。你可以把自我中心网络分析想象成一次放大。

第八章:自我中心网络分析

前一章内容非常丰富,我们学习了如何可视化和分析整个网络。相比之下,本章应该会感觉更简单,内容也会少得多。在之前的章节中,我们学习了如何获取和创建网络数据,如何从网络数据构建图形,如何清理图形数据,以及如何做一些有趣的事情,比如识别社区。在本章中,我们将进行所谓的 自我中心 网络分析

好消息是,前一章所学的所有内容都可以应用于自我中心网络。中心性可以帮助我们找出重要的节点。社区算法可以帮助我们识别社区。最棒的消息是,本章其实不需要涉及太多内容。自我中心网络分析在规模和范围上都更简单。最重要的是,我会解释如何开始,展示你可以做什么,并说明你未来可能想采取的进一步分析步骤。就像任何类型的分析一样,总是有更多的事情可以做,但我们将在本章中保持简单。

本章将涉及以下主题:

  • 进行自我中心网络分析

  • 调查自我节点和连接

  • 识别其他研究机会

技术要求

在本章中,我们将主要使用 Python 库 NetworkX 和 pandas。这些库应该已经安装好了,所以你可以直接使用它们。如果没有安装,你可以通过以下方式安装 Python 库:

pip install <library name>

例如,要安装 NetworkX,你可以按照以下步骤操作:

pip install networkx

第四章中,我们也介绍了一个 draw_graph() 函数,它使用了 NetworkX 和 scikit-network。每当我们进行网络可视化时,你都需要使用这段代码。本章以及本书的大多数章节中,你都需要用到它。

代码可以在 GitHub 上找到:github.com/PacktPublishing/Network-Science-with-Python

自我中心网络分析

自我中心网络分析 是一种网络分析方法,适用于研究社交网络中围绕特定个人存在的关系。我们不再研究整个社交网络,而是聚焦于某个个体以及该个体与之互动的人。自我中心网络分析使用一种更简单的网络形式,称为 自我中心网络自我网络)。从现在起,我将这些网络称为 自我网络

在自我网络中,有两种类型的节点:自我节点他者节点。自我节点是你正在研究的个体的节点。另一方面,他者节点是存在于自我网络中的所有其他节点。如果我基于自己的生活创建一个自我网络,我就是自我节点,我认识的人就是他者节点。如果我想调查在前一章中提到的 K-pop 社交网络中,@spotifykr Twitter 账号提到或被提到的人,我会为 spotifykr 创建一个自我网络。spotifykr 是自我节点,所有其他节点都是他者节点。

这有什么意义呢?你可以通过与某个个体互动的人了解很多关于这个人或组织的情况。许多情况下,相似的人会相互吸引。在我的生活中,我的大多数朋友都是工程师或数据科学家,但我另外的一些朋友是艺术家。我喜欢认识既具创造力又具分析力的人。其他人可能有完全不同类型的朋友。分析和可视化自我网络可以帮助我们洞察那些我们可能无法在当下察觉或看到的关系。我们可能会有某种直觉,觉得某些类型的关系存在,或者某个人是如何受到影响的,但能够分析和可视化自我网络本身是非常有启发性的。

与整个网络相比,使用自我网络的一个好处是自我网络通常比整个网络小且不太复杂。这很有道理,因为自我网络是更大生态系统的子集。例如,在一个由数百万人的社交网络中,一个自我网络将专注于自我节点和围绕它的其他节点。由于这些网络更小且不太复杂,因此可以使用原本计算量大的算法进行轻松处理。处理的数据量较少。然而,显然,自我网络的规模取决于个体的受欢迎程度。一个名人网红的自我网络将比我自己的自我网络复杂得多。我没有数百万粉丝。

自我中心网络分析的应用

在社交网络分析中,自我网络用于理解一个人周围的关系和社区。然而,这并不是你可以使用自我网络分析的唯一限制。我已经在各种不同的工作中使用自我网络,来理解人们的关系、沟通流和影响力。我还使用网络来绘制数据中心中的生产数据流,并利用自我网络来调查围绕某个软件或数据库表格的数据流和流程。如果你能创建一个网络,你就可以使用自我网络深入挖掘该网络,进行更细致的观察。你不仅限于分析人类,你还可以用它来分析恶意软件家族。例如,你可以用它来理解社交媒体上的放大效应。你也可以用它来检查供应链中的一个组件。你的限制只有你自己的创造力和你能够创建或获取的数据。只要网络存在,自我网络就可以用于分析。

解释分析方法论

本章将完全以实践为主,并且是可重复的。我们将使用一个预先构建的 NetworkX 网络,该网络包含了小说《悲惨世界》中的人物。我选择使用这个网络是因为它既大且复杂,足够有趣,同时也有清晰的社区,可以在不同的自我网络中看到这些社区。这是一个进行社交网络分析练习的极好网络。

NetworkX 网络自带权重,这使得我们可以在互动较多的节点之间绘制较粗的边,区别于互动较少的节点。然而,在本次分析中,我选择去掉权重,因为我希望你们更多地关注围绕自我节点的其他节点和社区。在这个分析中,我更感兴趣的是自我网络的结构以及自我网络内部存在的社区。我确实建议你挑战自己。在本章的代码中,或许你可以保留权重,而不是去除它们,然后看看这对网络可视化产生了什么影响。

我们将从快速检查整个网络开始,先看看网络的样子,并挑选出一些可能对识别有趣节点、进而进行自我网络分析的中心节点。

之后,我们将查看四个独立的自我网络。我们将从了解小说中自我角色的简短信息开始,但我们不会深入探讨。然后,我们将分别可视化有无中心的自我网络。在自我网络中,如果你去掉自我节点,这就叫做去掉中心。自我节点位于中心。在自我网络中,所有 alters 都与自我节点之间有一条边。如果一个 alter 在自我网络中,那么它与自我节点之间有某种关系。那么,你认为如果去掉自我节点会发生什么?自我网络会变得更简单,甚至可能断裂成多个部分。这种断裂尤其有用,因为它能够帮助我们很容易地识别不同的社区,这些社区表现为节点群集。因此,我们将执行去除中心的自我网络分析。

我们将识别自我网络中的 alters,找出最重要的 alters,并比较四个自我网络的密度。我们还将寻找存在于不同社区之间的桥梁。

开始吧!

整个网络抽查

在我们进行自我网络分析之前,首先需要构建我们的图。我们已经做过好几次了,所以这应该是熟悉的内容,但这次我们将使用 NetworkX 提供的预构建图。加载一个预构建图非常简单:

import networkx as nx
G = nx.les_miserables_graph()

NetworkX 提供了其他几种图形,因此请务必浏览文档,您可能会找到其他对您工作和学习有帮助的网络。

该图包含边权。虽然这对于理解节点之间的交互数量非常有用,但我决定从我们的图中移除它,以便使线条更加清晰,并且能够专注于自我网络本身。以下命令将把图转化为一个 pandas 边列表 DataFrame —— 只保留源节点和目标节点字段 —— 并使用 DataFrame 创建一个新图:

df = nx.to_pandas_edgelist(G)[['source', 'target']]
G = nx.from_pandas_edgelist(df)

现在我们的修改图已经构建完成,我们可以查看节点和边的数量:

nx.info(G)
'Graph with 77 nodes and 254 edges'

只有 77 个节点和 254 条边,这是一个简单的网络,我们可以轻松地在 图 8.1 中可视化它:

draw_graph(G, font_size=12, show_names=True, node_size=4, edge_width=1)

这将产生如下网络:

图 8.1 – 《悲惨世界》整个网络

图 8.1 – 《悲惨世界》整个网络

现在的结果足够了,但我想提醒你,我们在上一章学到过的内容。我们可以使用 k_core 来移除可视化中节点数小于 k 的节点。在这个例子中,我选择不显示节点数少于两个的节点:

draw_graph(nx.k_core(G, 2), font_size=12, show_names=True, node_size=4, edge_width=1)

这将绘制一个网络可视化,显示具有两个或更多边的节点,有效地移除孤立节点和仅有一条边的节点。这将帮助我们快速了解和预览网络的结构:

图 8.2 – 《悲惨世界》整个网络 (k=2)

图 8.2 – 《悲惨世界》整个网络 (k=2)

在可视化一个网络后,我通常会收集网络中所有节点的 PageRank 分数。PageRank 是一个快速的算法,能够在网络规模不管多大时都表现良好,因此它是一个快速识别节点重要性的好算法,正如上一章所讨论的那样。提醒一下,pagerank算法是基于一个节点的进出边数量来计算其重要性分数的。对于这个网络,我们使用的是无向图,因此pagerank实际上是基于一个节点所拥有的边的数量来计算分数的,因为在无向网络中并没有in_degreeout_degree的概念。以下是我们如何计算pagerank并将分数放入 pandas DataFrame 中进行快速分析和可视化:

import pandas as pd
pagerank = nx.pagerank(G)
pagerank_df = pd.DataFrame(pagerank, index=[0]).T
pagerank_df.columns = ['pagerank']
pagerank_df.sort_values('pagerank', inplace=True, ascending=False)
pagerank_df.head(20)

让我们可视化pagerank算法的计算过程:

图 8.3 – 《悲惨世界》网络中的前 20 个 PageRank 节点

图 8.3 – 《悲惨世界》网络中的前 20 个 PageRank 节点

一张图片可以帮助我们更容易地看到每个节点之间 PageRank 的差异:

pagerank_df.head(20).plot.barh(figsize=(12,8)).invert_yaxis()

可视化结果如下:

图 8.4 – 《悲惨世界》网络中前 20 个 PageRank 节点的可视化

图 8.4 – 《悲惨世界》网络中前 20 个 PageRank 节点的可视化

很好。我们可以清楚地看到瓦尔让在这个故事中是一个非常重要的角色。我们肯定需要检查瓦尔让的自我网络,以及米里埃尔加夫罗什的自我网络。为了确保我们不会得到太相似的自我网络,我选择了乔利作为第四个要检查的角色。乔利在 PageRank 榜单上排得较低。

这就是我们在本章中进行的全部网络分析。从此时起,我们将开始学习自我网络。让我们开始吧!

调查自我节点和连接

在自我中心网络分析中,我们感兴趣的是了解围绕一个单一节点存在的社区。我们对整个网络的结构和组成不太感兴趣。可以说,我们是“放大”来看。我们将使用自我中心网络分析来检查《悲惨世界》中核心角色周围存在的社区。

自我 1 – 瓦尔让

根据维基百科,尚·瓦尔让是《悲惨世界》的主角。了解这一点后,我们就能明白瓦尔让在网络中拥有最高的 PageRank 分数。任何故事的主角通常都会与比其他人更多的角色互动,PageRank 也会反映这一点。为了本章的目的,这就是我们对每个角色所做的背景分析。如果你想对一部文学作品中的网络进行深入分析,你需要做得更深。在本章中,我最感兴趣的是展示如何处理自我网络。

完整的自我网络

在我们能够分析自我网络之前,我们必须先创建一个。在 NetworkX 中,这叫做 ego_graph,可以通过简单地传入完整的图以及你想要分析的节点名称来创建:

ego_1 = nx.ego_graph(G, 'Valjean')

就这样。这就是如何创建自我网络。你还可以传入其他参数,但实际上,这已经是最复杂的部分了:

draw_graph(ego_1, font_size=12, show_names=True, node_size=4, edge_width=1)

我们现在可以可视化自我网络:

图 8.5 – Valjean 自我网络

图 8.5 – Valjean 自我网络

如果你仔细看,你应该能看到 Valjean 位于他自己自我网络的中心。这是有道理的。自我节点(Valjean)与存在于自我网络中的所有他人节点(其他节点)都有某种关系。

在继续之前,有一件重要的事情我想指出。自我网络只是另一种网络。我们在上一章中学习的所有内容——中心性社区度数k_corek_corona——同样适用于自我网络。考虑到上一章提到过,某些中心性计算开销大且耗时,特别是在整个网络上运行时。但根据我的经验,这种情况并不总是发生,甚至通常不会发生。对于自我网络,原本对整个网络来说不切实际的算法反而可以派上用场,而且应用起来非常简单。整个网络分析和自我中心网络分析之间是有重叠的。我们在上一章中学到的所有内容都可以应用于自我网络。

从自我网络中移除中心节点

一个常被忽视的重要选项是能够去除自我网络的中心。简单来说,这意味着将 Valjean 从他的自我网络中去除。我发现这样做非常有用,因为当你去除一个中心节点时,网络通常会被拆分成几个部分,这使得识别网络中的社区变得更加容易。我们可以像这样从自我网络中去除中心节点:

ego_1 = nx.ego_graph(G, 'Valjean', center=False)

现在,我们已经移除了中心节点——自我节点——让我们再一次可视化网络:

draw_graph(ego_1, font_size=12, show_names=True, node_size=4, edge_width=1)

图 8.6 – 删除中心后的 Valjean 自我网络

图 8.6 – 删除中心后的 Valjean 自我网络

将这个可视化与之前的进行对比。你看到了什么?我看到有一个较大的节点簇,它的顶部和左侧似乎至少有两个社区。右侧我还看到一个由三个节点组成的社区,它已经独立成岛。我还看到四个孤立节点。我希望你能明白,去掉中心节点可以使这些事情变得更加容易看出。

自我网络(去除中心,去噪)

我常常使用 k_core 来去噪一个网络,同样的操作也可以用于自我网络。让我们去掉所有度数小于一个的节点,实际上就是去掉这四个孤立节点:

draw_graph(nx.k_core(ego_1, 1), font_size=12, show_names=True, node_size=4, edge_width=1)

现在,让我们使用前面的代码来可视化它:

图 8.7 – Valjean 自我网络,去掉中心节点和孤立节点

图 8.7 – Valjean 自我网络,去掉中心节点和孤立节点

现在我们有了一个更干净的网络,很明显,网络中有两个节点集群。在维基百科上快速搜索发现 Myriel 是一位主教,Magloire 是他的仆人和妹妹,Baptistine 也是他的妹妹。他们属于自己的社区是有道理的。

交互者列表和数量

我们查找整个网络中存在的节点的方式,也可以应用于自我网络。我们将使用 ego_1.nodes,而不是 G.nodes,因为 ego_1 是我们的自我网络:

sorted(ego_1.nodes)
['Babet', 'Bamatabois', 'Bossuet', 'Brevet', 'Champmathieu', 'Chenildieu', 'Claquesous', 'Cochepaille', 'Cosette', 'Enjolras', 'Fantine', 'Fauchelevent', 'Gavroche', 'Gervais', 'Gillenormand', 'Gueulemer', 'Isabeau', 'Javert', 'Judge', 'Labarre', 'Marguerite', 'Marius', 'MlleBaptistine', 'MlleGillenormand', 'MmeDeR', 'MmeMagloire', 'MmeThenardier', 'Montparnasse', 'MotherInnocent', 'Myriel', 'Scaufflaire', 'Simplice', 'Thenardier', 'Toussaint', 'Woman1', 'Woman2']

有两种不同的方式可以获取自我网络中存在的交互者数量。记住,我们已经去掉了中心节点(自我节点),因此所有剩余的节点都是交互者:

  1. 第一个方法是简单地计算网络中节点的数量:

    len(ego_1.nodes)
    
    36
    

没问题,但如果我们还想看到边的数量呢?

  1. 让我们直接使用 nx.info() 函数,而不是查找如何获取网络中所有边的列表,因为这样更简单:

    nx.info(ego_1)
    
    Graph with 36 nodes and 76 edges'
    

整个网络有 77 个节点,因此显然,自我网络更简单。

重要的交互者

获取交互者列表是一回事,但如果我们能得到一个带有相关中心性分数的交互者列表,那就更有用,这样我们就可以评估网络中单个节点的重要性。记住,没有单一的中心性分数能够代表所有的中心性。我们可以使用 PageRank、接近中心性、中介中心性或其他任何度量方法。这些是与自我网络中最多其他节点相连的节点:

degcent = nx.degree_centrality(ego_1)
degcent_df = pd.DataFrame(degcent, index=[0]).T
degcent_df.columns = ['degree_centrality']
degcent_df.sort_values('degree_centrality', inplace=True, ascending=False)
degcent_df.head(10)

让我们可视化这一点,看看我们的中心性:

图 8.8 – Valjean 自我网络中交互者的度中心性

图 8.8 – Valjean 自我网络中交互者的度中心性

degree_centrality 大约为 0.457。考虑到 Javert 在自我网络中的中心地位,这很有道理。

图 8.9 – Javert 在 Valjean 自我网络中的网络位置

图 8.9 – Javert 在 Valjean 自我网络中的网络位置

其他具有最高中心性的交互者则更难以察觉。MmeThenardier 位于中心,右侧突出了。

自我网络密度

让我们通过计算网络密度来总结这个自我网络。密度与网络中所有节点之间的连接程度有关。为了达到 1.0 的密度,每个节点都会与网络中的每个其他节点相连。要有 0.0 的密度,网络将完全由孤立节点组成,节点之间没有任何连接。你能大致猜测这个网络的密度是多少吗?它看起来连接松散,且有几个密集连接的社区。因此,我的猜测是一个相当低的分数。让我们使用 NetworkX 来计算密度:

nx.density(ego_1)
0.12063492063492064

密度大约为0.12的网络是一个连接松散的网络。我计算了密度,因为我希望用它来比较每个自我网络的密度。

你可能在想,为什么我们在看介数中心性(betweenness centrality)和密度,或者想知道中心性和密度之间的关系。中心性得分有助于了解网络中一个节点的重要性。密度则告诉我们网络的整体构成。如果一个网络是密集的,那么节点之间的连接比稀疏网络要多。中心性和密度得分是快速了解网络的一种方式。

自我节点 2 – 马吕斯

维基百科将马吕斯·庞特马西(Marius Pontmercy)列为小说中的另一个主角。接下来,让我们看看他的自我网络。

完整的自我网络

首先,我们将构建完整的自我网络,而不去除中心节点:

ego_2 = nx.ego_graph(G, 'Marius')
draw_graph(ego_2, font_size=12, show_names=True, node_size=4, edge_width=1)

接下来,我们将可视化整个自我网络:

图 8.10 – 马吕斯的自我网络

图 8.10 – 马吕斯的自我网络

完美。有一点很明确:这个自我网络看起来与瓦尔让的完全不同。看向左下角,我能看到一个密集连接的个体社区。看向右边,我能看到一个马吕斯(Marius)对其有深厚感情的角色,但不剧透。

自我网络(去除中心)

让我们从这个网络中去除中心节点(自我节点),看看它是什么样子:

ego_2 = nx.ego_graph(G, 'Marius', center=False)
draw_graph(ego_2, font_size=12, show_names=True, node_size=4, edge_width=1)

这将绘制出我们的自我网络:

图 8.11 – 马吕斯的自我网络,去除中心节点

图 8.11 – 去除中心节点后的马吕斯自我网络

去除中心节点的结果与我们为瓦尔让的自我网络所做的完全不同。在瓦尔让的情况下,四个孤立节点从网络中断开。而在马吕斯的自我网络中,即使去除了中心节点,也没有孤立节点。他的自我网络中的成员连接得足够紧密,以至于去掉马吕斯的节点也没有破坏网络结构。这个网络具有很强的韧性。

在这个自我网络中,密集连接的社区在右侧也很容易看见。我还可以看到接近网络中心的瓦尔让(Valjean)。

之前,我们使用了k_core方法来去除孤立节点,以便在去除中心节点后能更容易查看剩余节点。在马吕斯的自我网络中,我们将跳过这一步骤。因为没有孤立节点需要去除。

更改列表和数量

让我们来看看马吕斯自我网络中的外部节点:

sorted(ego_2.nodes)
['Bahorel', 'BaronessT', 'Bossuet', 'Combeferre', 'Cosette', 'Courfeyrac', 'Enjolras', 'Eponine', 'Feuilly', 'Gavroche', 'Gillenormand', 'Joly', 'LtGillenormand', 'Mabeuf', 'MlleGillenormand', 'Pontmercy', 'Thenardier', 'Tholomyes', 'Valjean']

接下来,让我们轻松地获取节点和边的数量:

nx.info(ego_2)
'Graph with 19 nodes and 57 edges'

完美。这是一个非常简单的网络。

重要的外部连接

现在,让我们看看哪些外部节点处于中心位置。它们在网络中占据着强势地位:

degcent = nx.degree_centrality(ego_2)
degcent_df = pd.DataFrame(degcent, index=[0]).T
degcent_df.columns = ['degree_centrality']
degcent_df.sort_values('degree_centrality', inplace=True, ascending=False)
degcent_df.head(10)

这将给我们提供度中心性。让我们仔细看看!

图 8.12 – 马吕斯的自我网络改变后的度中心性

图 8.12 – 马吕斯的自我网络外部节点的度中心性

哇,有意思。我本以为让·瓦尔让会是最中心的节点之一,但有几个人排在他前面。你能猜到为什么吗?他们是紧密连接社区的一部分,每个人与自我网络中的成员连接的数量都超过了瓦尔让。这个社区应该有很多信息共享。现在看看瓦尔让的位置,我可以看到他是一个核心人物,但他连接的节点比紧密连接社区的成员要少。

请注意,有几个节点的中心性得分相同。是的,这种情况会发生。中心性得分只是数学的结果,而不是魔法。

自我网络密度

最后,为了比较自我网络,让我们计算密度得分:

nx.density(ego_2)
0.3333333333333333

记住,瓦尔让的自我网络密度大约是0.12。马吕斯的自我网络密度几乎是瓦尔让的三倍。这可以解释为什么在去掉马吕斯的中心节点时,网络并没有破裂。紧密连接的网络在去除中心节点时更加具有韧性。这一点在考虑如何增强现实世界网络的可用性时非常重要。从人的角度来看,即使关键节点被去除,这个社区仍然会继续存在。

自我 3 – 加夫罗什

在《悲惨世界》中,加夫罗什是一个生活在巴黎街头的小男孩。考虑到这一点,我想他的自我网络会与成年人或社会中更为联系紧密的人截然不同。让我们来看看。

完整的自我网络

首先,让我们可视化网络,保留中心节点:

ego_3 = nx.ego_graph(G, 'Gavroche')
draw_graph(ego_3, font_size=12, show_names=True, node_size=4, edge_width=1)

这将呈现加夫罗什的完整自我网络:

图 8.13 – 加夫罗什的自我网络

图 8.13 – 加夫罗什的自我网络

有趣。加夫罗什的连接广泛。考虑到他是一个生活在街头的孩子,看到Child1Child2位于顶部很有意思。这三个人物之间的关系看起来可能非常有趣。我还看到一个人(MmeBurgon),当去除加夫罗什的中心节点时,她的节点将变成孤立节点。最后,我看到在自我网络的左下角和右下角似乎有两个社区。这些社区在去除中心节点后应该会更加清晰。

自我网络(去除中心节点)

让我们去掉中心节点,再次可视化网络:

ego_3 = nx.ego_graph(G, 'Gavroche', center=False)
draw_graph(ego_3, font_size=12, show_names=True, node_size=4, edge_width=1)

这将呈现加夫罗什的自我网络,去除中心节点后。这应该能让不同的社区更容易识别:

图 8.14 – 加夫罗什的自我网络,去除中心节点

图 8.14 – 加夫罗什的自我网络,去除中心节点

完美。如预期,一些节点变成了孤立点,两个孩子形成了他们自己的小群体,剩余的连接组件包含两个独立的社区。使用社区检测算法分析最大群体可能会很有趣,但让我们继续。很明显,瓦尔让位于这两个社区之间。

从这个网络中移除孤立点没有多大意义,因为只有一个孤立点,所以让我们继续前进。

修改列表和数量

让我们看看还有哪些其他角色是加夫罗什自我网络的一部分。换句话说,我想知道加夫罗什认识谁。这将告诉我们哪些角色在他的生活中扮演了重要角色:

  1. 输入以下代码:

    sorted(ego_3.nodes)
    

这一简单的代码将为我们提供加夫罗什自我网络中的所有节点,按字母顺序排序:

['Babet', 'Bahorel', 'Bossuet', 'Brujon', 'Child1', 'Child2', 'Combeferre', 'Courfeyrac', 'Enjolras', 'Feuilly', 'Grantaire', 'Gueulemer', 'Javert', 'Joly', 'Mabeuf', 'Marius', 'MmeBurgon', 'MmeHucheloup', 'Montparnasse', 'Prouvaire', 'Thenardier', 'Valjean']

很好。我能看到一些熟悉的名字,而且我也清楚地看到了Child1Child2

  1. 接下来,我们来看看这个网络中有多少个节点和边,采用简单的方法:

    nx.info(ego_3)
    

这将给我们以下输出:

'Graph with 22 nodes and 82 edges'

哇。这比主角自己的自我网络要紧密得多。瓦尔让的自我网络有36个节点和76条边。你认为节点较少而边较多会如何影响这个网络的密度分数?

在我们进入这个话题之前,我们先看看中心性。

重要的变动者

再次,我们使用度中心性来看谁是这个自我网络中最有连接的人。到现在为止,这些步骤应该已经开始变得相似了。我们使用不同的中心性来理解哪些节点在我们的网络中是重要的:

degcent = nx.degree_centrality(ego_3)
degcent_df = pd.DataFrame(degcent, index=[0]).T
degcent_df.columns = ['degree_centrality']
degcent_df.sort_values('degree_centrality', inplace=True, ascending=False)
degcent_df.head(10)

这将给我们一个按度中心性排序的角色数据框。我们仔细看看:

图 8.15 – 加夫罗什的自我网络变更后的度中心性

图 8.15 – 加夫罗什的自我网络变更后的度中心性

哇,恩乔伊拉斯是一个高度连接的人,还有其他几个高度连接的人。我们可以看到他们在特别紧密的社区中。

自我网络密度

最后,让我们计算密度,以便比较自我网络:

nx.density(ego_3)
0.354978354978355

这真有趣。加夫罗什是一个生活在巴黎街头的孩子,但他的社交网络比我们之前看到的任何一个都要密集。瓦尔让的自我网络密度大约是0.12,马吕斯的是0.33,而加夫罗什的更高。我从没想到过这一点。如果我读这本书,我会特别关注这个角色。他看起来非常有联系,我很好奇这在故事中的表现如何。

自我 4 – 乔利

在本章的开头,我选择了四个人来创建自我网络。前三个有较高的 PageRank 分数,而我特意选择了一个 PageRank 分数较低的人作为第四个,因为我希望能得到一个与其他三个截然不同的自我网络。

乔利是一个医学学生,我想知道这是否会影响他的自我网络。学生通常会与其他学生社交,所以我会调查一下他的一些直接连接。

完整的自我网络

首先,让我们创建并可视化一个保持中心不变的自我网络:

ego_4 = nx.ego_graph(G, 'Joly')
draw_graph(ego_4, font_size=12, show_names=True, node_size=4, edge_width=1)

这将展示乔利的自我网络。

图 8.16 – 乔利的自我网络

图 8.16 – 乔利的自我网络

哇,通常在可视化网络时,总会有一个网络给我留下特别深刻的印象,觉得它特别独特。在这一章我们做过的四个网络中,这个网络最为突出。它看起来不像典型的自我网络,而是一个密集连接的网络。乔利的自我网络中的每个人都与其他人紧密相连。如果我们移除乔利的节点,这个自我网络可能几乎不会发生变化。在此之前,我们先看看这些人物中的一些,看看是否有其他人是医学学生或“革命学生友会”的成员,乔利正是这个组织的一员。

博苏埃

博苏埃·莱斯格尔被称为最不幸的学生,也是“革命学生友会”(Les Amis de l’ABC)的成员。作为一名学生和革命学生的成员,乔利与他有联系是合乎逻辑的。

恩乔拉斯

恩乔拉斯是“革命学生友会”(Les Amis de l’ABC)的领导人。这一联系也是合情合理的。

巴霍雷特

巴霍雷特是“革命学生友会”(Les Amis de l’ABC)的另一名成员。

加夫罗什

加夫罗什似乎不是“革命学生友会”(Les Amis de l’ABC)的成员,但他协助与他们并肩作战。

即使我们对故事或人物了解不多,我们也能通过检查整体网络、自我网络以及社区,轻松识别同一社区的成员。

自我网络(去除中心)

现在,让我们将中心从自我网络中去除,看看我的假设是否正确,即这个自我网络不会发生太大变化,因为它是密集连接的:

ego_4 = nx.ego_graph(G, 'Joly', center=False)
draw_graph(ego_4, font_size=12, show_names=True, node_size=4, edge_width=1)

这将绘制出去除中心的乔利自我网络。如果该网络中存在独立的社区,它们将作为群集单独展示。如果只有一个社区,那么该社区将作为一个单一的群集展示:

图 8.17 – 去除中心的乔利自我网络

图 8.17 – 去除中心的乔利自我网络

这很有趣。去除乔利后,自我网络仍然完整,核心人物依旧是核心。这个网络具有很强的韧性,作为革命者,他们的网络具有韧性是有意义的,因为这样他们的革命才有机会取得成功。

这个网络没有孤立点,因此没有必要对其进行去噪处理。

参与者列表和数量

让我们列出所有参与此自我网络的个体:

sorted(ego_4.nodes)

这将给我们提供一个排序后的参与者列表,这些人物是该自我网络的一部分:

['Bahorel', 'Bossuet', 'Combeferre', 'Courfeyrac', 'Enjolras', 'Feuilly', 'Gavroche', 'Grantaire', 'Mabeuf', 'Marius', 'MmeHucheloup', 'Prouvaire']

与其他自我网络相比,参与者非常少。

让我们来统计节点和边的数量:

nx.info(ego_4)

这应该能让我们了解这个自我网络的规模和复杂性:

'Graph with 12 nodes and 57 edges'

这个网络的节点数和边数比我们之前分析的其他网络少,但它的密度明显大于其他网络。

重要的参与者

让我们来看一下自我中心网络中最重要的“改变者”:

degcent = nx.degree_centrality(ego_4)
degcent_df = pd.DataFrame(degcent, index=[0]).T
degcent_df.columns = ['degree_centrality']
degcent_df.sort_values( 'degree_centrality', inplace=True, ascending=False)
degcent_df.head(10)

图 8.18 – Joly 的自我中心网络的“改变者”的度中心性

图 8.18 – Joly 的自我中心网络的“改变者”的度中心性

与我们看到的其他网络相比,这些中心性得分令人难以置信。1.0的中心性得分表明这是一个高中心性的网络,意味着网络连接性非常好。你认为这将如何影响密度得分?

自我中心网络的密度

让我们计算一下这个自我中心网络的密度。我怀疑这个网络的密度会非常高:

nx.density(ego_4)
0.8636363636363636

这与其他密度得分相比,确实非常高:

  • Valjean 的自我中心网络的密度约为0.12

  • Marius 的自我中心网络的密度约为0.33

  • Gavroche 的自我中心网络的密度约为0.35

  • Joly 的自我中心网络的密度约为0.86

这个密度比其他任何密度得分都要高得多。

但这也是有道理的。Joly 既是学生又是一个革命团体的成员。我预期这两个群体都会与其他学生和革命者保持良好的联系。

自我中心网络之间的洞察

我们查看了四个不同的自我中心网络。我们本可以为网络中的每个节点创建一个自我中心网络,但那样会非常耗时。在进行自我中心网络分析时,我通常从筛选几个我感兴趣的节点开始,然后再进一步调查。我通常是在寻找一些东西,比如以下内容:

  • 谁的连接性最强

  • 谁拥有最多的out_degrees

  • 谁的pagerank最高

  • 谁与已知的对立面有联系

在本章中,我们研究了小说《悲惨世界》中的人物,因此我故意选择了那些具有最高 PageRank 得分的人物,因为我预期他们的自我中心网络会很有趣。这一策略非常有效。

在总结本章之前,我希望为你留下以下几点启示:

  • 首先,通过增加连接来增强网络是可行的,这会使网络具有抗失败能力。如果删除一个节点,网络仍然可以保持完整,而不会破裂成碎片。这对于许多事物有深远的影响。例如,为了保持信息流动的稳定,一个信息共享网络希望能抵抗攻击。除此之外,在哪些情况下拥有一个抗失败的网络会是有价值的呢?

  • 其次,删除自我中心网络的中心节点可以告诉你很多关于网络中存在的社区的信息,以及该网络的韧性。当我们移除中心节点时,哪些网络出现了孤立节点或孤岛?哪些网络保持了完全的完整性?我们能看到哪些社区?

让我们看看接下来会发生什么。

识别其他研究机会

在进行任何类型的网络分析时,了解总是有更多的操作空间是很重要的;例如,我们可以做以下几项:

  • 我们还可以在图中嵌入更多信息,比如权重或节点类型(教师、学生、革命者等)

  • 我们可以根据节点类型为节点上色,便于识别社区。

  • 我们可以根据节点的度数或中心性得分来调整节点大小,便于识别重要节点。

  • 我们可以使用有向网络来理解信息共享的方向性。

你可以做的事情永远不止这些,但重要的是知道“足够”就好。你应该只使用你需要的内容。在这一章,我一开始在做太多事情时遇到困难,浪费了时间去思考一些实际上对教学这个主题并不重要的内容。保持简单,只添加必要和有用的内容。如果有时间,添加更多有价值的内容。

总结

在这一章中,你学到了一种新的网络分析方法,叫做自我中心网络分析。我习惯将自我中心网络简称为自我网络,以便简洁。我们学到的是,我们不必将网络作为整体进行分析。我们可以将其拆分成部分,这样可以研究某个节点在与另一个节点关系中的位置。

就个人而言,自我中心网络分析是我最喜欢的网络分析方法,因为我喜欢研究网络中个体层面的事物。整体网络分析作为一个广阔的地图很有用,但通过自我中心网络分析,你可以对网络中存在的各种关系有更深入的了解。希望你和我一样享受这一章的阅读和学习。我也希望这能激发你更深入的学习。

在下一章,我们将深入探讨社区检测算法!

第九章:社区检测

在过去的两章中,我们介绍了整体网络分析和自我中心网络分析。前者有助于理解复杂网络的完整结构。后者则有助于研究存在于“自我”节点周围的人和关系。然而,在整个网络和自我之间,还存在一个我们尚未讨论的缺失层次。社区存在于其中。我们是人类,我们是地球上全球人口的一部分,但我们每个人也是个体社区的一部分。例如,我们在公司工作,作为个人团队的一部分。我们中的许多人有社交兴趣,我们通过参与活动认识人。生活有层次,我们可以使用算法自动识别网络中存在的各种社区。

本章包含以下几个部分:

  • 介绍社区检测

  • 入门社区检测

  • 探索连接组件

  • 使用 Louvain 方法

  • 使用标签传播

  • 使用 Girvan-Newman 算法

  • 社区检测的其他方法

技术要求

在本章中,我们将主要使用 NetworkX 和 pandas Python 库。这些库应该已经安装好,可以供您使用。如果尚未安装,您可以使用以下命令安装 Python 库:

pip install <library name>

例如,要安装 NetworkX,您可以使用以下命令:

pip install networkx

第四章中,我们还介绍了一个draw_graph()函数,该函数同时使用了 NetworkX 和 Scikit-Network。您将在进行网络可视化时需要这段代码。这不仅限于本章,几乎适用于本书的大部分章节。

对于社区检测,我们还将使用python-louvain。您可以使用以下命令进行安装:

pip install python-louvain

您可以像这样导入它,稍后您将在本章看到:

from community import community_louvain

如果您对python-louvain的安装和导入命令感到困惑,这是可以理解的。该库的名称与导入库名称不匹配。这是一个用于社区检测的有用库,因此让我们接受这种奇怪现象并继续前进。

介绍社区检测

社区检测涉及识别网络中存在的各种社区或群组。这在社交网络分析中非常有用,因为人类作为我们各种社区的一部分与他人互动,但这些方法不仅限于研究人类。

我们还可以使用这些方法来研究与其他节点紧密交互的任何类型的节点,无论这些节点是动物、标签、网站还是网络中的任何节点。稍作思考,我们正在做什么。社区检测是对我们正在做的事情的一个明确、简洁且恰当的名称。我们正在聚焦于网络中存在的社区。您对探索和理解哪些社区感兴趣,以及为什么?

这种方法有很多很好的使用场景。你可以用它来了解社区对你产品的情感反应。你可以用它来了解威胁格局。你可以用它来了解不同群体之间思想是如何传播和转变的。这里可以发挥创意。它可能有比你想象的更多的用途。

在本章中,我们将从人类生活的角度来探讨这一点,但你不应只限于将其应用于社交网络分析。它在社交网络分析中非常有用,但它在分析大多数网络数据时也很有用,不仅仅是社交网络数据。例如,这在网络安全(恶意软件分析)和计算人文学科中非常有用,或者在理解思想如何在群体之间传播并演变时也很有用。

至少有三种不同的方法进行社区检测,其中最常被研究的包括以下几种:

  • 节点连通性

  • 节点接近度

  • 网络拆分

我所说的节点连通性与节点是否属于同一个连通组件有关。如果两个节点不属于同一个连通组件,那么它们属于完全不同的社交群体,而不是同一个社区。

节点接近度与两个节点之间的距离有关,即使它们是同一个连通组件的一部分。例如,两个可能在同一个大型组织中一起工作的人,但如果他们之间有超过两个握手的距离,他们可能不属于同一个社区。要让他们相遇,需要经过几轮介绍。想一想,要认识你最喜欢的名人,你需要通过多少人介绍。你需要经过多少人?

网络拆分实际上是通过移除节点或边来将一个网络切割成多个部分。我将解释的首选方法是对边进行切割,但我也做过类似的操作,移除节点,我在本书中做过几次,通过去除中心节点将网络打碎成碎片。

我不认为我们已经在社区检测的发现上走到了尽头。我希望通过阅读本章内容,你能够获得一些新的思路来识别网络中存在的各种社区。

开始进行社区检测

在开始之前,我们需要一个网络来使用。让我们继续使用上章中提到的 NetworkX 的悲惨世界图,因为它包含了几个独立的社区:

  1. 加载网络是简单的:

    import networkx as nx
    
    import pandas as pd
    
    G = nx.les_miserables_graph()
    

这就是加载图形所需要的全部操作。

  1. 有一个weight属性,我不打算在网络中包含它,因为在这个简单的演示中我们不需要边权重。因此,我将删除它并重新构建图形:

    df = nx.to_pandas_edgelist(G)[['source', 'target']]
    
    # dropping 'weight'
    
    G = nx.from_pandas_edgelist(df)
    

在这两个步骤中,我们将悲惨世界图转换为pandas的边列表,并且仅保留sourcetarget字段,有效地去除了weight字段。让我们看看网络中有多少节点和边:

nx.info(G)
'Graph with 77 nodes and 254 edges'

这是一个小网络。这个网络包含孤立点和孤岛吗,还是只有一个大的连通分量?让我们来检查一下。

  1. 首先,让我们添加draw_graph函数:

    def draw_graph(G, show_names=False, node_size=1, font_size=10, edge_width=0.5):
    
        import numpy as np
    
        from IPython.display import SVG
    
        from sknetwork.visualization import svg_graph
    
        from sknetwork.data import Bunch
    
        from sknetwork.ranking import PageRank
    
        adjacency = nx.to_scipy_sparse_matrix(G, nodelist= None, dtype=None, weight='weight', format='csr')
    
        names = np.array(list(G.nodes()))
    
        graph = Bunch()
    
        graph.adjacency = adjacency
    
        graph.names = np.array(names)
    
        pagerank = PageRank()
    
        scores = pagerank.fit_transform(adjacency)
    
        if show_names:
    
            image = svg_graph(graph.adjacency, font_size = font_size , node_size=node_size, names=graph.names, width=700, height=500, scores=scores, edge_width = edge_width)
    
        else:
    
            image = svg_graph(graph.adjacency, node_size = node_size, width=700, height=500, scores = scores, edge_width=edge_width)
    
        return SVG(image)
    
  2. 现在,让我们全面展示网络:

    draw_graph(G, font_size=12, show_names=True, node_size =4, edge_width=1)
    

输出如下:

图 9.1 – 悲惨世界图

图 9.1 – 悲惨世界图

一眼看去,我们应该能够看到没有孤立点(没有边的节点),有几个只有一条边的节点,有几个节点群体非常接近(社区),还有一些非常关键的节点。如果移除这些关键节点,网络就会支离破碎。

  1. 让我们稍微放大一点,使用k_core,并且只显示具有两条或更多边的节点。我们也不显示标签,这样可以更好地看到网络的整体形状:

    draw_graph(nx.k_core(G, 2), font_size=12, show_names=False, node_size=4, edge_width=0.5)
    

我们将得到以下输出:

图 9.2 – 悲惨世界图,k_core,K=2,无标签

图 9.2 – 悲惨世界图,k_core,K=2,无标签

现在社区应该更清晰了。寻找节点紧密且边/线更多的图部分。你看到了几个社区?我看到有四个社区特别明显,但周围也有一些较小的群体,而且网络中心可能还有一个社区。

现在我们已经准备好尝试社区检测了。

探索连通分量

理解网络中存在的各种社区和结构的第一步常常是分析连通分量。正如我们在w中讨论的,连通分量是网络中的结构,其中所有节点都与同一组件中的另一个节点连接。

正如我们之前看到的,连通分量对于查找较小的连接部分非常有用。这些可以被视为社区,因为它们与主要组件和整体网络分离,但最大的连通分量通常不是单个社区。它通常由几个社区组成,并且通常可以分割成单独的社区。

悲惨世界网络中,只有一个连通分量。没有孤岛或孤立点。只有一个单一组件。这是有道理的,因为这些都是文学作品中的角色,书中的角色不会整天和自己说话。然而,这也削弱了检查此图的连通分量的一些用处。

有一个解决方法!正如我之前提到的,如果我们从网络中移除几个关键节点,那么网络往往会支离破碎:

  1. 让我们从网络中移除五个非常重要的角色:

    G_copy = G.copy()
    
    G_copy.remove_nodes_from(['Valjean', 'Marius', 'Fantine', 'Cosette', 'Bamatabois'])
    
  2. 在这两行代码中,我们构建了一个名为G_copy的第二个图,然后移除了五个关键节点。让我们再次可视化这个网络!

    draw_graph(G_copy, font_size=12, show_names=True, node_size=4, edge_width=1)
    

这给我们带来了如下输出:

图 9.3 – 破碎后的《悲惨世界》网络

图 9.3 – 破碎后的《悲惨世界》网络

很好。这与许多现实世界网络的样子更为接近。依然有一个主要的连通组件(大陆),有三个较小的连通组件(岛屿),还有六个孤立节点。把这些叫做岛屿和大陆是我个人的命名方式。没有一个明确的标准来决定岛屿是否就是大陆。只是大多数网络都包含一个超大组件(大陆),大量孤立节点,以及若干连通组件(岛屿)。这对我有帮助,但你可以随意处理。

另一个需要记住的事情是,我们刚才所做的可以作为社区检测中的一步。移除一些关键节点可以将网络拆散,揭示出存在的小社区。这些至关重要的节点将一个或多个社区维系在一起,作为更大结构的一部分。移除这些重要节点使得社区之间可以自由漂移。我们通过移除重要节点来完成这一操作,虽然这通常不是最理想的做法。然而,其他实际的社区检测方法工作原理类似,通过移除边而非节点来实现。

  1. 在破碎网络后,还剩下多少个连通组件?

    components = list(nx.connected_components(G_copy))
    
    len(components)
    
    10
    

NetworkX 说有 10 个连通组件,但孤立节点除了可能与自身连接外,不与任何东西相连。

  1. 在查看连通组件之前,让我们先移除它们:

    G_copy = nx.k_core(G_copy, 1)
    
    components = list(nx.connected_components(G_copy))
    
    len(components)
    
    4
    

看起来有四个连通组件。

  1. 由于它们数量很少,我们来检查每一个:

    community = components[0]
    
    G_community = G_copy.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

让我们来看一下可视化效果:

图 9.4 – 破碎后的《悲惨世界》网络中组件 0 的子图

图 9.4 – 破碎后的《悲惨世界》网络中组件 0 的子图

非常有趣!第一个连通组件几乎是一个星型网络,所有节点都连接到一个中心角色——米里埃尔。然而,如果你看看左上角,你应该会看到两个角色也共享一个链接。这种关系可能值得进一步研究。

  1. 让我们看下下一个组件:

    community = components[1]
    
    G_community = G_copy.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=4)
    

这给我们带来了如下输出:

图 9.5 – 破碎后的《悲惨世界》网络中组件 1 的子图

图 9.5 – 破碎后的《悲惨世界》网络中组件 1 的子图

这更加有趣了。我把这个叫做主要组件。它是破碎网络中最大的连通组件。然而,正如我之前说的,连通组件并不适合用来识别社区。稍微左偏一点,你应该能看到网络中心左边有两个节点簇,两个独立的社区。右侧至少还有一个其他社区。如果移除两条边或节点,右侧的社区就会从网络中分离出来。继续前进!

  1. 让我们继续打破社区:

    community = components[2]
    
    G_community = G_copy.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=4)
    

我们将得到以下输出:

图 9.6 – 破碎的《悲惨世界》网络的组件 2 子图

图 9.6 – 破碎的《悲惨世界》网络的组件 2 子图

这是一个强连接组件。每个节点都与这个网络中的其他节点有连接。如果移除一个节点,网络仍然保持完整。从网络的角度来看,每个节点与其他节点同样重要或中心。

  1. 让我们检查最后一个组件:

    community = components[3]
    
    G_community = G_copy.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=4)
    

这为我们提供了以下的可视化:

图 9.7 – 破碎的《悲惨世界》网络的组件 3 子图

图 9.7 – 破碎的《悲惨世界》网络的组件 3 子图

这是另一个密集连接的网络。每个节点同样重要或中心。如果移除一个节点,网络仍然保持完整。

如你所见,我们通过查看连接组件找到了三个社区,但连接组件没有揭示出在更大主组件中存在的社区。如果我们想要识别这些社区,我们需要移除其他重要节点,然后重复分析。丢弃节点是一种丢失信息的方式,因此我不推荐这种方法,但在快速的临时分析中,它可以是有用的。

我不认为研究连接组件属于社区检测,但在研究连接组件时可以找到社区。我认为这是任何网络分析过程中应该做的第一步,获得的见解非常有价值,但它对于社区检测来说不够敏感。

如果你的网络中没有连接组件的超级集群,那么连接组件对于社区检测来说是相当足够的。然而,你必须将超级集群当作一个社区,实际上,那个集群包含了多个社区。随着网络规模的增大,连接组件方法的效果会变得不那么有效。

让我们继续讨论更合适的方法。

使用卢瓦恩方法

卢瓦恩方法无疑是我最喜欢的社区检测方法,原因有很多。

首先,这种算法可以应用于非常大的数百万节点的网络,且效果显著且快速。我们将在本章探讨的其他方法在大型网络中无法使用,也没有那么快,因此使用这种算法我们能得到其他地方找不到的效果和速度。因此,这是我首选的社区检测算法,其他的算法我则保留作为备选方案。

其次,可以调节分辨率参数来找到最适合的社区检测分割,这在默认结果不理想时提供了灵活性。而其他算法则没有这种灵活性。

总结来说,使用 Louvain 方法,我们有了一个快速的算法,它在大规模网络中的社区检测中非常有效,我们还可以优化该算法以获得更好的结果。我建议从 Louvain 方法入手,尝试社区检测,然后在学习过程中逐步掌握其他方法。了解不同的选择是很有帮助的。

它是如何工作的?

Louvain 方法的创始人能够在一个包含数亿个节点和超过十亿条边的网络上使用他们的算法,这使得这种方法非常适用于大型网络。你可以在 arxiv.org/pdf/0803.0476.pdf 阅读更多关于 Louvain 方法的内容。

该算法通过一系列的传递工作,每次传递包含两个阶段。第一阶段将不同的社区分配给网络中的每个节点。最初,每个节点都会分配一个不同的社区。接着,对每个邻居进行评估,并将节点分配到社区。第一步在无法再进行改进时结束。在第二阶段,构建一个新的网络,其中节点是第一阶段发现的社区。然后,可以重复第一阶段的结果。两个步骤不断迭代,直到找到最佳社区。

这是算法工作原理的简化描述。建议完整阅读研究论文,以更好地理解算法是如何工作的。

Louvain 方法的实际应用!

我们在 第三章 中简要使用了 Louvain 方法,所以如果你有注意的话,这段代码应该很熟悉。Louvain 方法已经包含在最新版本的 NetworkX 中,因此如果你使用的是最新版本的 NetworkX,就不需要使用 community Python 库了,但你的代码会有所不同。为了保持一致,我将使用community库的方法:

  1. 首先,让我们导入库:

    import community as community_louvain
    
  2. 下面是一些帮助我们绘制 Louvain 分区的代码:

    def draw_partition(G, partition):
    
        import matplotlib.cm as cm
    
        import matplotlib.pyplot as plt
    
        # draw the graph
    
        plt.figure(3,figsize=(12,12))
    
        pos = nx.spring_layout(G)
    
        # color the nodes according to their partition
    
        cmap = cm.get_cmap('jet', max(partition.values()) + 1)
    
        nx.draw_networkx_nodes(G, pos, partition.keys(), node_size=40, cmap=cmap, node_color = list(partition.values()))
    
        nx.draw_networkx_edges(G, pos, alpha=0.5, width = 0.3)
    
        return plt.show()
    
  3. 现在,让我们使用 best_partition 函数,利用 Louvain 方法来识别最佳分区。在我的测试中,我发现 resolution=1 是理想值,但在其他网络中,你应该尝试调整这个参数:

    partition = community_louvain.best_partition(G, resolution=1)
    
    draw_partition(G, partition)
    

这将生成一个可视化:

图 9.8 – Louvain 方法对《悲惨世界》网络的社区检测

图 9.8 – Louvain 方法对《悲惨世界》网络的社区检测

步骤 2 中的辅助函数将根据节点所属的社区为其着色。重要的是,已经检测到独立的社区,并且每个节点的社区都用不同的颜色标识。每个节点都属于不同的分区,而这些分区就是社区。

  1. 让我们来看看 partition 变量中包含了什么:

    partition
    
    {'Napoleon': 1,
    
     'Myriel': 1,
    
     'MlleBaptistine': 1,
    
     'MmeMagloire': 1,
    
     'CountessDeLo': 1,
    
     'Geborand': 1,
    
      …
    
     'Grantaire': 0,
    
     'Child1': 0,
    
     'Child2': 0,
    
     'BaronessT': 2,
    
     'MlleVaubois': 2,
    
     'MotherPlutarch': 0}
    

为了节省空间,我剪掉了一些节点和分区。每个节点都有一个关联的分区编号,这就是它所属的社区。如果你想获取属于某个特定社区的节点列表,你可以像这样操作:

[node for node, community in partition.items() if community == 2]

那么,为什么这令人兴奋?Louvain 方法到底有什么酷的地方?首先,它能够扩展到庞大的网络,允许对像互联网这样的最大网络进行研究。其次,它很快,这意味着它是实用的。如果一个算法慢到只适用于小型网络,那它就没有多大意义。Louvain 方法在大规模网络上实用。这个算法既快速又高效,结果也非常好。它是你工具箱中会需要的一个社区检测算法。

接下来,让我们看看标签传播作为社区检测的另一个选择。

使用标签传播

标签传播是另一种快速识别网络中社区的方法。根据我的经验,结果没有 Louvain 方法那么好,但它是一个可以探索的工具,作为社区检测的一部分。你可以在arxiv.org/pdf/0709.2938.pdf上阅读关于标签传播的更多内容。

它是如何工作的?

这是一种迭代方法。每个节点被初始化为一个唯一的标签,在每次算法迭代过程中,每个节点都会采用其大多数邻居的标签。例如,如果David节点有七个邻居节点,且其中四个邻居的标签是label 1,另外三个邻居的标签是label 0,那么David节点将选择label 1。在每次步骤中,每个节点都会选择大多数邻居的标签,最终通过将拥有相同标签的节点分组为社区来结束该过程。

标签传播在行动!

这个算法可以直接从 NetworkX 中导入:

from networkx.algorithms.community.label_propagation import label_propagation_communities

一旦导入了算法,你所要做的就是将它传递给你的图,然后你会得到一个社区列表:

  1. 让我们使用《悲惨世界》图来试试看:

    communities = label_propagation_communities(G)
    

这一行将我们的图传递给标签传播算法,并将结果写入一个community变量。在这么小的网络上,这个算法非常快速,只需要不到一秒钟的时间就能识别社区。我更喜欢将这些结果转换成列表,以提取社区节点。

  1. 我们可以像这样做到:

    communities = list(communities)
    
    communities[0]
    
    {'Champtercier',
    
     'Count',
    
     'CountessDeLo',
    
     'Cravatte',
    
     'Geborand',
    
     'Myriel',
    
     'Napoleon',
    
     'OldMan'}
    

在最后一行中,我们检查了第一个社区,社区 0。可视化这些社区非常简单。

  1. 我们可以将它们提取为子图,然后使用我们在本书中一直使用的相同的draw_graph函数:

    community = communities[1]
    
    G_community = G.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

我们从输出中可以看到什么?

图 9.9 – 《悲惨世界》网络的标签传播社区检测,社区 1

图 9.9 – 《悲惨世界》网络的标签传播社区检测,社区 1

这个结果看起来不错,但还不如 Louvain 方法的结果那么好。虽然它速度很快,但精度不如我预期的那么高。例如,看看Valjean左边,有一个紧密连接的社区,节点之间的连接非常密集。这个应该是一个独立的社区,而不是这个大社区的一部分。这个算法并不完美,但没有算法是完美的。然而,这个算法很快,能够扩展到大规模网络,因此它是大规模社区检测的另一种选择。

  1. 让我们再看看几个社区:

    community = communities[2]
    
    G_community = G.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

这为我们提供了以下输出:

图 9.10 – 悲惨世界网络的标签传播社区检测,社区 2

图 9.10 – 悲惨世界网络的标签传播社区检测,社区 2

这个社区看起来几乎完美。在社区中,除了最密集连接的部分外,出现一些额外的节点并不罕见。

  1. 让我们看另一个例子:

    community = communities[3]
    
    G_community = G.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

这为我们提供了以下的可视化:

图 9.11 – 悲惨世界网络的标签传播社区检测,社区 3

图 9.11 – 悲惨世界网络的标签传播社区检测,社区 3

这个社区看起来也很好。总体来说,这个算法效果很好,且速度很快。此外,设置比 Louvain 方法更容易、更快,因为你只需导入算法,传入一个图形,然后可视化结果。在易用性方面,这是我见过的最简单的算法。结果看起来不错,社区很快就被识别出来了。

但是,快速和易于使用并不足够。Louvain 更准确,并且快速且易于使用。尽管如此,这个算法仍然有其用处。

使用 Girvan-Newman 算法

在本章开始时,我们注意到悲惨世界网络由一个大的连通分量组成,并且没有孤立点或除了大连通分量之外的小型“岛屿”社区。为了展示连通分量如何有助于识别社区,我们通过去除一些关键节点来打破这个网络。

这种方法通常并不理想。尽管节点(人、地点、事物)和边(关系)中都包含信息,但根据我的经验,通常更倾向于去掉边而不是去掉节点。

一个比我们之前做的更好的方法是,找出能导致网络分裂的最少边,这些边将是切断网络的关键。我们可以通过寻找通过最多最短路径的边来实现这一点——也就是具有最高edge_betweenness_centrality的边。

这正是Girvan-Newman 算法的作用。

它是如何工作的?

Girvan-Newman 算法通过尽可能少地切割边来识别社区,从而将网络分割成两部分。你可以在这里了解他们的研究方法:www.pnas.org/doi/full/10.1073/pnas.122653799

很多时候,当我在查看网络时,我会看到几个节点被几条边连接在两个不同的侧面上。它几乎看起来像几根橡皮筋将这两组捆绑在一起。如果你剪断橡皮筋,这两个社区应该会分开,类似于当关键节点被移除时,网络会分裂成碎片的情况。

从某种程度上来说,这比删除节点更具精准性。信息损失较少。当然,丧失某些关系的信息是一个缺点,但所有节点仍然完好无损。

通过一系列迭代,Girvan-Newman 算法识别出具有最高edge_betweenness_centrality得分的边并将其删除,将网络分割成两部分。然后,过程再次开始。如果没有足够重复,社区会太大;如果重复太多次,社区最终会只剩一个节点。因此,在使用此算法时需要进行一些实验,以找到理想的分割次数。

这个算法的核心就是切割。这个算法的缺点是它并不快。计算edge_betweenness_centrality要比 Louvain 方法或标签传播的计算更加耗费计算资源。因此,这个算法很快就变得不再实用,因为它变得非常慢,无法再实际应用。

然而,如果你的网络足够小,这是一个非常酷的社区检测算法,可以进行探索。它也很直观,容易向他人解释。

Girvan-Newman 算法在运行中!

让我们用我们的悲惨世界图来试试看。图足够小,这个算法应该能够很快地将其分割成多个社区:

  1. 首先,导入算法:

    from networkx.algorithms.community import girvan_newman
    
  2. 接下来,我们需要将图作为参数传递给算法。这样做时,算法将返回每次迭代分割的结果,我们可以通过将结果转换为列表来进行调查:

    communities = girvan_newman(G)
    
    communities = list(communities)
    
  3. 在每个社区由单个节点组成之前,算法最多可以进行多少次迭代?我们来看一下:

    len(communities)
    
    76
    

太棒了!我们有76次分割迭代保存在 Python 列表中。我建议你研究不同的分割级别,找到最适合你需求的一个。可能是过程中的非常早期,前 10 次分割,或者可能稍微晚一些。这部分需要一些分析,使得这个算法更具实践性。

  1. 然而,为了推进进度,假设我们发现第十次迭代的分割得到了最好的结果。我们将第十次迭代的结果作为最终的社区分组,然后像使用 Louvain 方法和标签传播一样可视化这些社区:

    communities = communities[9]
    

我们保留了第十次迭代的结果,删除了其他所有结果。如果我们不想丢弃这些结果,可以使用不同的变量名。

  1. 我们来看一下这些社区是什么样的,以便将它们与我们讨论的其他算法进行比较:

    community = communities[0]
    
    G_community = G.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

我们得到以下输出:

图 9.12 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 0

图 9.12 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 0

这个子图应该很熟悉!当我们通过节点将网络分裂然后可视化连接组件时,看到的正是这个。这个算法通过切割边缘分裂网络,并成功找到了相同的社区。

  1. 我们来看另一个社区:

    community = communities[1]
    
    G_community = G.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

这将生成以下网络可视化:

图 9.13 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 1

图 9.13 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 1

这也看起来非常好。社区中有密集连接的节点组,以及一些连接较少的节点,这并不罕见。

  1. 另一个社区:

    community = communities[2]
    
    G_community = G.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

我们将看到以下输出:

图 9.14 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 2

图 9.14 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 2

这与上一个社区类似。我们有一个密集连接的节点组和两个只有一条边的节点。看起来很棒。

  1. 我们来看一下社区 3:

    community = communities[3]
    
    G_community = G.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

社区 3 看起来是这样的:

图 9.15 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 3

图 9.15 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 3

这也应该很熟悉。标签传播方法找到了相同的社区,但 Girvan-Newman 算法删除了一个额外的节点。

  1. 以及下一个:

    community = communities[4]
    
    G_community = G.subgraph(community)
    
    draw_graph(G_community, show_names=True, node_size=5)
    

我们将看到以下网络:

图 9.16 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 4

图 9.16 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 4

尽管这可能在视觉上不如其他的网络可视化那么吸引人,但我认为它比其他的更令人印象深刻。这是一个不那么明显的社区,通过切割具有最高edge_betweenness_centrality得分的边找到的。中间有一个连接较多的节点组,周围是每个只与一条边连接的节点。

Girvan-Newman 算法可以给出非常好的、干净的结果。唯一的缺点是速度。计算edge_betweenness_centrality和最短路径是一个耗时的过程,因此该算法比我们讨论的其他算法要慢得多,但如果你的网络不太大,它仍然非常有用。

其他社区检测方法

我们探讨过的所有这些算法,都是人们关于如何在网络中识别社区的想法,基于与其他节点的接近度或通过切割边缘来找到社区。然而,这些并不是唯一的方法。我在了解 Girvan-Newman 算法之前,曾提出过一种方法,通过切割节点而不是边缘来识别社区。然而,当我了解了 Girvan-Newman 方法后,我发现它更理想,因此放弃了我的实现。但这让我思考,识别网络社区可能还有其他方法吗?

随着你对网络的理解逐渐深入,并且在使用网络分析时越来越得心应手,尝试发现其他识别社区的方法。

总结

在这一章中,我们探讨了几种不同的社区检测方法。每种方法都有其优缺点。

我们看到,连通组件在识别社区时是有用的,但前提是网络不仅仅由一个单一的主要组件组成。要利用连通组件识别社区,网络中需要有一些较小的连通组件被分割出来。在网络分析的初期使用连通组件非常重要,这有助于了解网络的整体结构,但作为单独的工具用于识别社区并不理想。

接下来,我们使用了 Louvain 方法。这个算法非常快速,适用于节点数量达到数亿、边缘数量达到数十亿的网络。如果你的网络非常大,这将是一个很有用的社区检测初步方法。算法运行速度快,结果清晰。你还可以调整一个参数,以获得最佳的划分结果。

然后,我们使用了标签传播方法来识别社区。在悲惨世界网络中,这个算法识别社区的时间仅为几分之一秒。总体来说,结果不错,但它似乎在将一个密集的节点群体从一个较大的社区中分割出来时遇到了一些困难。然而,其他社区的划分效果都很好。这个算法很快,并且应该能够扩展到大型网络,但我从未听说过它被应用于一个拥有数百万节点的网络。值得尝试。

最后,我们使用了 Girvan-Newman 算法,这是一种通过对具有最高edge_betweenness_centrality分数的边缘进行多轮切割来寻找社区的算法。结果非常清晰。这个算法的缺点是它非常慢,并且在大规模网络中扩展性差。然而,如果你的网络较小,这将是一个非常有用的社区检测算法。

这一章的编写过程非常有趣。对我来说,社区检测是网络分析中最有趣的领域之一。分析整个网络或探索自我中心网络是一回事,但能够识别并提取社区则是一项位于整体网络分析和自我中心网络分析之间的技能。

在接下来的几章中,我们将进入未知领域,探索如何将网络科学与机器学习结合起来!第一章将讲解监督式机器学习,而最后一章则将讲解无监督式机器学习。我们只剩下几章了!坚持住!

第十章:网络数据上的监督学习

在前面的章节中,我们花费了大量时间探讨如何从互联网上收集文本数据,将其转换为网络数据,进行网络可视化,并分析网络。我们能够使用中心性和各种网络指标来获得有关单个节点在网络中位置和影响力的更多上下文信息,并使用社区检测算法来识别网络中存在的各种社区。

在本章中,我们将开始探索网络数据如何在机器学习ML)中发挥作用。由于这是一本数据科学和网络科学的书,我预计许多读者已经对机器学习有所了解,但我会给出一个非常简短的解释。

本章包括以下几个部分:

  • 引入机器学习

  • 从机器学习开始

  • 数据准备和特征工程

  • 选择模型

  • 准备数据

  • 训练和验证模型

  • 模型洞察

  • 其他应用案例

技术要求

在本章中,我们将使用 Python 库 NetworkX、pandas 和 scikit-learn。现在这些库应该已经安装好了,因此可以随时使用。如果没有安装,你可以通过以下方式安装 Python 库:

pip install <library name>

例如,要安装 NetworkX,你可以执行以下命令:

pip install networkx

第四章中,我们还介绍了一个draw_graph()函数,利用了 NetworkX 和scikit-network库。每次进行网络可视化时,你都需要使用这段代码。记得保留它!

本章的代码可以在 GitHub 上找到:github.com/PacktPublishing/Network-Science-with-Python

引入机器学习

机器学习(ML)是一组技术,能够使计算机从数据中的模式和行为中学习。通常说,机器学习有三种不同的类型:监督学习无监督学习强化学习

在监督学习中,数据会附带一个答案——称为标签——以便让机器学习模型学习那些可以帮助其预测正确答案的模式。简单来说,你给模型提供数据答案,然后它会弄清楚如何做出正确预测。

在无监督学习中,模型不会被提供答案。目标通常是找出相似数据的聚类。例如,你可以使用聚类算法来识别数据集中不同类型的新闻文章,或在文本语料库中找出不同的主题。这与我们做的社区检测工作类似。

在强化学习中,模型会被赋予一个目标,并逐渐学习如何达到这个目标。在许多强化学习演示中,你会看到模型玩乒乓球或其他视频游戏,或者学习走路。

这些是机器学习类型的极简描述,还有更多的变体(半监督学习等)。机器学习是一个广泛的主题,因此如果本章内容让你感兴趣,我鼓励你查阅相关书籍。对我来说,它使自然语言处理成为了一种痴迷。

开始机器学习

有很多关于如何使用自然语言处理(NLP)进行情感分析的指南和书籍,但关于如何将图数据转换成可以用于机器学习分类格式的指南和书籍却少之又少。在本章中,你将看到如何使用图数据进行机器学习分类。

对于这个练习,我创建了一个名为“找出革命者”的小游戏。与前两章一样,我们将使用 《悲惨世界》 网络,因为它包含足够多的节点和社区,足够有趣。在前面的章节中,我指出革命者社区是高度连接的。作为提醒,这就是它的样子:

图 10.1 – 《悲惨世界》ABC 革命社群网络

图 10.1 – 《悲惨世界》ABC 革命社群网络

社区中的每个成员几乎都与其他成员相互连接。没有与外部人员的连接。

网络的其他部分看起来不同。

图 10.2 – 《悲惨世界》全网

图 10.2 – 《悲惨世界》全网

即使是视觉检查也能看出,在网络的不同部分,节点的连接方式不同,结构也有所差异。在某些地方,连接类似于星形;在其他地方,连接则像网格。网络指标将为我们提供这些值,机器学习模型可以使用它们进行预测。

我们将使用这些指标来玩“找出革命者”的游戏。这会很有趣。

注意

我不会深入解释机器学习,只会简要介绍它的能力。如果你对数据科学或软件工程感兴趣,我强烈建议你花时间学习机器学习。它不仅仅适用于学者、数学家和科学家。机器学习变得越来越复杂,因此强烈建议具备数学和统计学基础,但你完全可以自由探索这一主题。

本章不会是一堂数学课。所有的工作都将通过代码完成。我将展示一个使用网络数据进行分类的例子。这不是唯一的应用场景,使用网络数据进行机器学习的应用远不止这些。我也只会展示一个模型(随机森林),而不是所有可用的模型。

我还将展示有时不正确的预测和正确的预测一样具有启发性,并且有时模型预测中也包含有用的见解。

我将展示从图数据到预测和见解的工作流程,以便你可以在自己的实验中使用这一方法。你并不需要每次都使用图神经网络(NN)。使用更简单的模型也是完全可能的,它们同样能够提供有价值的见解。

足够的免责声明。开始吧。

数据准备和特征工程

在我们可以使用机器学习之前,首先需要收集数据,并将其转换为模型可以使用的格式。我们不能直接将图 G 传递给随机森林并就此结束。我们可以将图的邻接矩阵和一组标签传递给随机森林,它也能工作,但我想展示一些我们可以做的特征工程。

特征工程是利用领域知识创建额外的特征(大多数人称之为列),这些特征将对我们的模型有用。例如,回顾前一节中的网络,如果我们想能够识别革命者,我们可能希望为模型提供额外的数据,如每个节点的度数(连接数)、介数中心性、紧密中心性、页面排名、聚类系数和三角形:

  1. 让我们从先构建网络开始。现在应该很容易,因为我们已经做过好几次了:

    import networkx as nx
    
    import pandas as pd
    
    G = nx.les_miserables_graph()
    
    df = nx.to_pandas_edgelist(G)[['source', 'target']] # dropping 'weight'
    
    G = nx.from_pandas_edgelist(df)
    

我们在前面的章节中已经采取了这些步骤,但提醒一下,《悲惨世界》图谱带有边权重,而我不需要这些。第一行加载图谱,第二行从图谱中创建边列表,去掉边权重,第三行则从边列表重建图谱,去掉权重。

  1. 我们现在应该有一个有用的图谱。让我们来看看:

    draw_graph(G)
    

这将生成以下图谱:

图 10.3 – 《悲惨世界》图谱(不带节点名称)

图 10.3 – 《悲惨世界》图谱(不带节点名称)

看起来不错!我可以清楚地看到这个网络中有几个不同的节点簇,而其他部分的网络则更为稀疏。

那么,我们如何将这个混乱的纠结结转化为机器学习模型可以使用的东西呢?好吧,我们在前几章中已经研究过中心性和其他度量。所以我们已经有了在这里使用的基础。我将创建几个包含我所需数据的数据框,然后将它们合并成训练数据。

度数

度数只是一个节点与其他节点连接的数量。我们首先获取这个数据:

degree_df = pd.DataFrame(G.degree)
degree_df.columns = ['person', 'degrees']
degree_df.set_index('person', inplace=True)
degree_df.head()

我们得到如下输出:

图 10.4 – 《悲惨世界》特征工程:度数

图 10.4 – 《悲惨世界》特征工程:度数

接下来我们进入下一步。

聚类

接下来,我们将计算聚类系数,它告诉我们节点周围的连接密度。值为 1.0 表示每个节点都与其他节点相连,值为 0.0 表示没有邻近节点与其他邻近节点连接。

让我们来捕捉聚类:

clustering_df = pd.DataFrame(nx.clustering(G), index=[0]).T
clustering_df.columns = ['clustering']
clustering_df.head()

这将给我们聚类的输出:

图 10.5 – 《悲惨世界》特征工程:聚类

图 10.5 – 《悲惨世界》特征工程:聚类

这告诉我们 MlleBaptistineMmeMagloire 都是高度连接的社区的一部分,这意味着这两个人也认识同样的人。Napoleon 与其他人没有任何交集,CountessDeLo 也是如此。

三角形

triangle_df 是统计给定节点属于多少个三角形。如果一个节点属于许多不同的三角形,那么它与网络中的许多节点相连接:

triangle_df = pd.DataFrame(nx.triangles(G), index=[0]).T
triangle_df.columns = ['triangles']
triangle_df.head()

这将给我们带来如下结果:

图 10.6 – 《悲惨世界》特征工程:三角形

图 10.6 – 《悲惨世界》特征工程:三角形

这是理解节点之间连接性的一种方式。这些节点代表人物,所以它也是理解人与人之间连接性的一种方式。请注意,结果类似于但并不完全相同于聚类。

介数中心性

介数中心性与节点在其他节点之间的位置有关。举个例子,假设有三个人(ABC),如果 B 坐在 AC 之间,那么从 AC 传递的所有信息都会通过 B,使得 B 处于一个重要且有影响力的位置。这只是介数中心性有用性的一个例子。我们可以通过以下代码获取这个信息:

betw_cent_df = pd.DataFrame(nx.betweenness_centrality(G), index=[0]).T
betw_cent_df.columns = ['betw_cent']
betw_cent_df.head()

这将给我们带来以下输出:

图 10.7 – 《悲惨世界》特征工程:介数中心性

图 10.7 – 《悲惨世界》特征工程:介数中心性

接近中心性

接近中心性与一个给定节点与网络中所有其他节点的距离有关,具体来说是最短路径。因此,接近中心性在大规模网络中计算起来非常慢。然而,对于《悲惨世界》网络来说,它会表现得很好,因为这是一个非常小的网络:

close_cent_df = pd.DataFrame(nx.closeness_centrality(G), index=[0]).T
close_cent_df.columns = ['close_cent']
close_cent_df.head()

图 10.8 – 《悲惨世界》特征工程:接近中心性

图 10.8 – 《悲惨世界》特征工程:接近中心性

PageRank

最后,即使在大规模网络中,pagerank 仍然有效。因此,它被广泛用于衡量重要性:

pr_df = pd.DataFrame(nx.pagerank(G), index=[0]).T
pr_df.columns = ['pagerank']
pr_df.head()

这将给我们 图 10.9

图 10.9 – 《悲惨世界》特征工程:pagerank

图 10.9 – 《悲惨世界》特征工程:pagerank

邻接矩阵

最后,我们可以将 邻接矩阵 纳入训练数据,使得我们的模型可以将邻居节点作为特征来进行预测。例如,假设你有 10 个朋友,但其中一个是罪犯,而每个这个朋友介绍给你的人也都是罪犯。你可能会随着时间的推移,学到不应该和这个朋友或他们的朋友交往。你的其他朋友没有这个问题。在你的脑海中,你已经开始对那个人及其交往的人做出判断。

如果我们省略邻接矩阵,模型将试图仅从其他特征中学习,但它无法意识到相邻节点的上下文。在“识别革命者”游戏中,它将仅使用中心性、聚类、度数和其他特征,因为它无法从其他任何地方获取学习信息。

我们将使用邻接矩阵。这感觉几乎像是泄漏(答案隐藏在另一个特征中),因为在社交网络中,相似的事物往往会相互吸引,但这也展示了将网络数据与机器学习结合的实用性。如果你觉得这是一种作弊方式,可以不使用邻接矩阵,我个人不这么认为:

adj_df = nx.to_pandas_adjacency(G)
adj_df.columns = ['adj_' + c.lower() for c in adj_df.columns]
adj_df.head()

这段代码输出以下 DataFrame:

图 10.10 – 《悲惨世界》特征工程:邻接矩阵

图 10.10 – 《悲惨世界》特征工程:邻接矩阵

合并数据框

现在我们有了所有这些有用的特征,是时候将 DataFrame 合并在一起了。这很简单,但需要几个步骤,以下代码展示了如何操作:

clf_df = pd.DataFrame()
clf_df = degree_df.merge(clustering_df, left_index=True, right_index=True)
clf_df = clf_df.merge(triangle_df, left_index=True, right_index=True)
clf_df = clf_df.merge(betw_cent_df, left_index=True, right_index=True)
clf_df = clf_df.merge(close_cent_df, left_index=True, right_index=True)
clf_df = clf_df.merge(pr_df, left_index=True, right_index=True)
clf_df = clf_df.merge(adj_df, left_index=True, right_index=True)
clf_df.head(10)

在第一步,我创建了一个空的 DataFrame,这样我就可以反复运行 Jupyter 单元,而不需要创建带有奇怪名称的重复列。这只是节省了工作量和减少了烦恼。接着,我按照 DataFrame 的索引顺序,将各个 DataFrame 合并到 clf_df 中。DataFrame 的索引是《悲惨世界》中的角色名称。这确保了每个 DataFrame 中的每一行都能正确地合并在一起。

图 10.11 – 《悲惨世界》特征工程:合并的训练数据

图 10.11 – 《悲惨世界》特征工程:合并的训练数据

添加标签

最后,我们需要为革命者添加标签。我已经快速查找了《ABC 朋友们》(Les Amis de l’ABC)成员的名字,这是革命者小组的名称。首先,我会将这些成员添加到 Python 列表中,然后进行抽查,确保名字拼写正确:

revolutionaries = ['Bossuet', 'Enjolras', 'Bahorel', 'Gavroche', 'Grantaire',
                   'Prouvaire', 'Courfeyrac', 'Feuilly', 'Mabeuf', 'Marius', 'Combeferre']
# spot check
clf_df[clf_df.index.isin(revolutionaries)]

这会生成以下 DataFrame:

图 10.12 – 《悲惨世界》特征工程:ABC 朋友们

图 10.12 – 《悲惨世界》特征工程:ABC 朋友们

这看起来很完美。列表中有 11 个名字,DataFrame 也有 11 行。为了创建监督学习的训练数据,我们需要添加一个 1

clf_df['label'] = clf_df.index.isin(revolutionaries).astype(int)

就这么简单。让我们快速查看一下 DataFrame,确保我们已经有了标签。我会按索引排序,以便在数据中看到一些 1 标签:

clf_df[['label']].sort_index().head(10)

这将输出以下内容:

图 10.13 – 《悲惨世界》特征工程:标签抽查

图 10.13 – 《悲惨世界》特征工程:标签抽查

完美。我们有了节点,每个节点都有标签。标签为 1 表示他们是 ABC 朋友会的成员,标签为 0 表示他们不是。这样,我们的训练数据就准备好了。

选择模型

对于这次练习,我的目标只是向你展示网络数据如何在机器学习(ML)中可能有所帮助,而不是深入探讨机器学习的细节。关于这个主题,有很多非常厚的书籍。这本书是关于如何将自然语言处理(NLP)和网络结合起来,理解我们周围存在的隐形联系以及它们对我们的影响。因此,我将迅速跳过关于不同模型如何工作的讨论。对于这次练习,我们将使用一个非常有用且强大的模型,它通常足够有效。这个模型叫做随机森林(Random Forest)

随机森林(Random Forest)可以接受数值型和类别型数据作为输入。我们选择的特征对于本次练习来说应该非常合适。随机森林的设置和实验也非常简单,而且它也很容易了解模型在预测中最为有用的特征。

其他模型也可以使用。我尝试使用了k-近邻(k-nearest neighbors),并且达到了几乎相同的成功水平,我确信逻辑回归(Logistic regression)在一些额外的预处理之后也会很好用。XGBoost支持向量机(SVM)也会有效。你们中的一些人可能也会想使用神经网络(NN)。请随意。我选择不使用神经网络,因为它的设置更复杂,训练时间通常更长,而且可能只能带来微小的准确度提升,这也可能只是偶然。实验不同的模型!这是学习的好方法,即使你是在学习不该做什么

准备数据

我们应该再进行几次数据检查。最重要的是,让我们检查一下训练数据中类别的平衡:

  1. 从以下代码开始:

    clf_df['label'].value_counts()
    
    0    66
    
    1    11
    
    Name: label, dtype: int64
    

数据不平衡,但问题不大。

  1. 让我们以百分比形式展示,这样可以让它更容易理解:

    clf_df['label'].value_counts(normalize=True)
    
    0    0.857143
    
    1    0.142857
    
    Name: label, dtype: float64
    

看起来我们在类别之间大约有 86/14 的比例。还不错。记住这一点,因为仅仅基于类别不平衡,模型就应该能够以大约 86%的准确率进行预测。如果它只有 86%的准确率,那它就不会是一个令人印象深刻的模型。

  1. 接下来,我们需要将数据切割成适合我们模型的数据。我们将使用特征作为X数据,答案作为y数据。由于标签是最后添加的,这个过程很简单:

    X_cols = clf_df.columns[0:-1]
    
    X = clf_df[X_cols]
    
    y = clf_df['label'].values
    

X_cols是除了最后一列(即标签)之外的所有列,X是一个只包含X_cols字段的数据框,y是我们答案的数组。不要仅仅听我说,做个抽查吧。

  1. 运行以下代码:

    X.head()
    

这将显示一个数据框(DataFrame)。

  1. 向右滚动数据框。如果你没有看到标签列,那我们就可以开始了:

    y[0:5]
    

这将显示y中的前五个标签。这是一个数组。我们准备好了。

最后,我们需要将数据分成训练数据和测试数据。训练数据将用于训练模型,测试数据是模型完全不知道的数据。我们不关心模型在训练数据上的准确性。记住这一点。我们不关心模型的训练数据准确率或任何性能指标。我们只关心模型在未见数据上的表现。这将告诉我们它的泛化能力以及在实际环境中的表现。是的,我知道这个模型在实际环境中不会有太大用处,但这就是我们的思路。

  1. 我们将使用scikit-learntrain_test_split函数来拆分数据:

    from sklearn.model_selection import train_test_split
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1337, test_size=0.4)
    

由于我们的训练数据非常少,而且 ABC 协会的成员也很少,我将test_size设置为0.4,是默认值的两倍。如果数据不那么不平衡,我会将其减少到0.30.2。如果我真的希望模型能够使用尽可能多的训练数据,并且我认为它足够好,我甚至可能尝试0.1。但是在这个练习中,我选择了0.4。这是我的理由。

这个函数将数据以 60/40 的比例分割,将 60%的数据放入X_trainy_train,其余 40%放入X_testy_test。这样就将 40%的数据作为模型无法知道的“未见数据”。如果模型能在这 40%的未见数据上表现良好,那么它就是一个不错的模型。

我们现在准备好训练我们的模型,看看它的表现如何!

训练和验证模型

在人们谈论机器学习时,模型训练是最受关注的部分,但通常它是最简单的一步,只要数据已被收集和准备好。可以并且应该花费大量的时间和精力来优化你的模型,通过超参数调优。无论你对哪个模型感兴趣并想使用,做一些关于如何调优该模型的研究,以及数据准备所需的任何额外步骤。

对于这个简单的网络,默认的随机森林模型已经是最优的了。我进行了几个检查,发现默认模型已经足够好。以下是代码:

from sklearn.ensemble import RandomForestClassifier
clf = RandomForestClassifier(random_state=1337, n_jobs=-1, n_estimators=100)
clf.fit(X_train, y_train)
train_acc = clf.score(X_train, y_train)
test_acc = clf.score(X_test, y_test)
print('train_acc: {} - test_acc: {}'.format(train_acc, test_acc))

我们正在使用随机森林分类器,所以我们首先需要从sklearn.ensemble模块导入模型。随机森林使用决策树的集成来做出预测。每个集成都基于训练数据中的不同特征进行训练,然后做出最终预测。

设置random_state为你喜欢的任何数字。我喜欢1337,这是一个老黑客笑话。它是1337leetelite。将n_jobs设置为-1,确保在训练模型时使用所有的 CPU。将n_estimators设置为100,将允许使用 100 个决策树的集成。可以尝试不同的估计器数量。增加它可能有帮助,但在这种情况下没有。

最后,我收集并打印了训练准确率和测试准确率。我们的得分如何?

train_acc: 1.0 - test_acc: 0.9354838709677419

在测试集上,结果还不错。这个数据集是未见过的数据,所以我们希望它的准确度较高。如前所述,由于类别不平衡,模型应该至少能达到 86%的准确率,因为 86%的标签属于大类。93.5%的准确率还算不错。不过,你应当注意欠拟合过拟合。如果训练集和测试集的准确率都很低,模型很可能是欠拟合,需要更多的数据。如果训练集的准确率远高于测试集的准确率,这可能是过拟合的表现,而这个模型似乎存在过拟合问题。不过,考虑到我们目前的数据量,以及本次实验的目的,今天的结果也算是“够好了”。

你必须知道,模型准确率永远不足以评估模型的表现。它并不能告诉你模型的表现,特别是在少数类的表现。我们应该查看混淆矩阵和分类报告,以了解更多信息。为了使用这两个工具,我们首先需要将X_test的预测结果存入一个变量:

predictions = clf.predict(X_test)
predictions
…
array([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,1, 0, 0, 1, 0, 0, 1, 0, 1])

很好,我们得到了一个预测数组。接下来,让我们导入confusion_matrixclassification_report函数:

from sklearn.metrics import confusion_matrix, classification_report, plot_confusion_matrix

我们可以通过将X_test数据以及对X_test做出的预测作为输入,来使用这两个工具。首先,我们来看看混淆矩阵:

confusion_matrix(y_test, predictions)
 …
array([[26,  2],
             [ 0,  3]], dtype=int64)

如果这还不够清楚,我们也可以将其可视化:

plot_confusion_matrix(clf, X_test, y_test)

这将生成以下矩阵。

图 10.14 – 模型混淆矩阵

图 10.14 – 模型混淆矩阵

混淆矩阵展示了模型如何根据类别进行预测。图示很好地展示了这一点。y 轴显示的是真实标签,值为01,而x 轴显示的是预测标签,值为01。我可以看到,有 26 个字符被正确预测为不是 ABC 革命者(Friends of the ABC)成员。我们的模型正确预测了三个 ABC 革命者的成员,但也错误地预测了两个非成员为成员。我们需要深入研究这个问题!有时候,错误的预测能帮助我们发现数据中的问题,或者给我们带来一些有趣的见解。

我还发现查看分类报告极其有帮助:

report = classification_report(y_test, predictions)
print(report)

我们得到以下输出:

图 10.15 – 模型分类报告

图 10.15 – 模型分类报告

这个报告清楚地显示了模型在预测 ABC 革命者的非成员时表现很好,但在预测革命者时表现较差。为什么会这样?它是被什么困扰了?从网络上看,模型本应能够学习到,不同群体之间有明显的结构差异,特别是在将 ABC 革命者与其他人群进行比较时。到底发生了什么?让我们构建一个简单的数据框来检查一下:

check_df = X_test.copy()
check_df['label'] = y_test
check_df['prediction'] = predictions
check_df = check_df[['label', 'prediction']]
check_df.head()

我们得到以下的数据框:

图 10.16 – 预测检查的数据框

图 10.16 – 预测检查的 DataFrame

现在,让我们创建一个掩码来查找所有标签与预测不匹配的行:

mask = check_df['label'] != check_df['prediction']
check_df[mask]

这给了我们图 10.17

图 10.17 – 漏掉预测的 DataFrame

图 10.17 – 漏掉预测的 DataFrame

好的,现在我们可以看到模型哪里出了错,但为什么会这样呢?为了节省你的时间,我调查了这两个角色。Joly 实际上是ABC 朋友会的成员,而 Madame Hucheloup 经营一家咖啡馆,ABC 朋友会的成员经常在这里聚会。她曾是科林特酒馆的老板,那是成员们的聚会场所,也是他们的最后防线!由于她与该小组成员有联系,模型预测她也是其中的一员。

公平来说,我敢打赌一个人类可能也会做出相同的判断,认为 Madame Hucheloup 是其中之一!对我来说,这就是一个美丽的错误分类!

下一步肯定是给 Joly 一个正确的标签,并重新训练模型。我会保持 Madame Hucheloup 不变,因为她不是成员,但如果我是反叛者,我会密切关注她。

简而言之,我认为模型表现非常好,并且完全使用了图形数据。

模型洞察

对我来说,模型洞察比构建和使用模型进行预测更让人兴奋。我喜欢了解周围的世界,而机器学习模型(和网络)让我能够以我的眼睛无法感知的方式理解这个世界。我们看不到所有将我们联系在一起的线条,也很难理解基于周围人的社会网络中他们如何被战略性地安排从而产生的影响。这些模型可以帮助我们做到这一点!网络能够提供提取信息流动和影响的上下文意识的结构。机器学习可以告诉我们哪些信息在理解某些事情时最有用。有时候,机器学习能够穿越噪音,直接找到那些真正影响我们生活的信号。

在我们刚刚建立的模型中,一个见解是《悲惨世界》中的不同角色在不同的网络结构中有不同的类型。革命者们彼此靠得很近,并且紧密连接。学生们也有很强的连接性,我感到惊讶并且高兴的是,模型在许多学生身上没有出现错误分类。书中的其他角色几乎没有什么连接,他们的邻居连接稀疏。我认为,作者在定义这个故事中存在的社交网络方面付出了很多努力,这很美妙。它能让我们重新审视这个故事的创作过程。

但是,哪些特征对模型最重要,帮助它做出如此准确的预测呢?让我们来看一下。随机森林使我们这一点变得非常容易!

你可以通过以下方式非常容易地获取特征重要性

clf.feature_importances_

但这些数据在这个格式下并不太有用。如果你将特征重要性放入一个 DataFrame,它们就能被排序和可视化,这样会更有用:

importance_df = pd.DataFrame(clf.feature_importances_, index=X_test.columns)
importance_df.columns = ['value']
importance_df.sort_values('value', ascending=False, inplace=True)
importance_df.head()

我们得到了这个 DataFrame:

图 10.18 – 特征重要性的 DataFrame

图 10.18 – 特征重要性的 DataFrame

这些是数字格式的特征重要性。这展示了模型在进行预测时,认为最有用的 10 个特征。值得注意的是,角色与博苏埃和昂若拉的关系是判断一个角色是否为革命者的好指标。在网络特征中,三角形是唯一进入前 10 名的特征。其余的重要特征来自邻接矩阵。让我们通过条形图来可视化这些,以便能看到更多内容,以及每个特征的重要性程度:

import matplotlib.pyplot as plt
importance_df[0:20].plot.barh(figsize=(10,8)).invert_yaxis()

我们得到以下图表:

图 10.19 – 特征重要性的水平条形图

图 10.19 – 特征重要性的水平条形图

要好得多。这看起来更容易理解,而且它准确地显示了模型认为每个特征的有用程度。

顺便说一下,你可以利用特征重要性,积极识别出你可以从训练数据中剔除的特征,从而使模型更加精简。我常常会创建一个基准的随机森林模型,以帮助进行积极的特征选择。积极的特征选择是我用来形容在训练模型之前,毫不留情地剔除不必要数据的过程。对于这个模型,我并没有做积极的特征选择。我使用了我收集到的所有数据。

其他使用场景

虽然这可能很有趣,但我们大多数人并不会把捕捉革命者当作日常工作的一部分。那么,这有什么用呢?其实,对网络进行预测有很多用途。最近,图神经网络(Graph ML)引起了很多关注,但大多数文章和书籍展示的都是别人建立的模型(而不是如何从零开始构建模型),或者使用神经网络。这没问题,但它复杂且不总是实用。

我展示的这种方法是轻量且实用的。如果你有网络数据,你也可以做类似的事情。

但还有哪些其他的使用场景呢?对我来说,我最感兴趣的是机器人检测和人工放大检测。我们该如何进行呢?对于机器人检测,您可能需要关注诸如账户年龄(按天计算)、一段时间内发布的帖子数量等特征(真实用户通常会在变得活跃之前,慢慢学习如何使用社交网络)等内容。对于人工放大,您可能会关注一个账户发布每条推文时,获得的粉丝数。例如,如果一个账户一周前上线,发布了 2 条帖子,并获得了 2000 万粉丝,这种增长是如何发生的呢?自然增长要慢得多。也许他们从另一个社交网络带来了粉丝,或者他们的账户被数百个博客推介了。

你还能想到其他哪些使用场景吗?发挥创造力吧。你现在知道什么是网络,也知道如何构建它们并与之互动。你希望预测什么,或者更好地理解什么呢?

总结

我们做到了!我们又完成了一个章节。我真心希望你觉得这一章特别有趣,因为很少有资料能解释如何从零开始做这些事情。我决定写这本书的原因之一,就是希望像这样的想法能够得到推广。所以,我希望这一章能引起你的注意,并激发你的创造力。

在本章中,我们将一个实际的网络转化为可以用于机器学习的训练数据。这是一个简化的例子,但这些步骤适用于任何网络。最终,我们创建了一个能够识别 ABC 革命小组成员的模型,尽管它是一个非常简单的模型,并不适用于现实世界中的任何应用。

下一章将与这一章非常相似,但我们将使用无监督学习来识别与其他节点相似的节点。很可能,无监督学习也会识别出 ABC 革命小组的成员,但它也可能揭示出其他有趣的见解。

第十一章:无监督机器学习在网络数据上的应用

欢迎来到另一个激动人心的章节,我们将一起探索网络科学和数据科学。在上一章中,我们使用监督学习训练了一个模型,通过图特征来识别《悲惨世界》中的革命者。在本章中,我们将探讨无监督机器学习以及它如何在图分析和节点分类中与监督学习结合使用。

这两章的编写顺序是有意安排的。我希望你能够学习如何通过图创建自己的训练数据,而不是依赖于无监督机器学习的嵌入。这一点很重要:当你依赖嵌入时,你失去了理解机器学习模型分类原因的能力。你失去了可解释性和可说明性。无论使用哪种模型,分类器基本上都像一个黑箱。我想先向你展示可解释和可说明的方法。

在本章中,我们将使用一个名为 Karate Club 的 Python 库。这个库在社区检测和使用图机器学习创建图嵌入方面非常出色。然而,使用这种方法时,无法得知模型究竟发现了哪些有用的信息。因此,我将其放在最后介绍。如果你不介意失去可解释性,它仍然非常有效。

这是一个有趣的章节,因为我们将把书中的许多内容汇聚在一起。我们将创建图、生成训练数据、进行社区检测、创建图嵌入、做一些网络可视化,甚至使用监督机器学习进行节点分类。如果你从本章开始阅读这本书,可能一切看起来都像魔法。如果你从第一章就跟着阅读,那么这一切应该都能理解,而且很容易掌握。

技术要求

在本章中,我们将使用 Python 库 NetworkX、pandas、scikit-learn 和 Karate Club。除了 Karate Club 之外,这些库应该已经安装好,可以直接使用。安装 Karate Club 的步骤会在本章中介绍。如果其他库没有安装,你可以通过以下方式安装 Python 库:

pip install <library name>

例如,要安装 NetworkX,你可以这样操作:

pip install networkx

第四章中,我们还介绍了 draw_graph() 函数,它同时使用了 NetworkX 和 scikit-network。每当我们进行网络可视化时,你将需要这段代码。随时准备好使用它!

所有代码都可以从 GitHub 仓库获取:github.com/PacktPublishing/Network-Science-with-Python

什么是无监督机器学习?

在关于机器学习的书籍和课程中,通常会解释有三种不同的类型:监督学习、无监督学习和强化学习。有时会解释组合方法,比如半监督学习。在监督学习中,我们提供数据(X)和答案(y),模型学习进行预测。而在无监督学习中,我们只提供数据(X),没有答案(y)。目标是让模型自主学习识别数据的模式和特征,然后我们可以利用这些模式和特征做其他事情。例如,我们可以使用无监督机器学习自动学习图形的特征,并将这些特征转换为可以在监督学习预测任务中使用的嵌入。在这种情况下,无监督机器学习算法接受一个图(G),并生成作为训练数据(X)的嵌入,这些数据将用于预测答案。

简而言之,无监督机器学习的目标是识别数据中的模式。我们通常称这些模式为簇(clusters),但这不仅仅限于聚类。创建嵌入(embeddings)并不是聚类。然而,通过嵌入,一个复杂的网络被简化为几个数字特征,机器学习将更容易使用这些特征。

在本章中,你将亲眼看到这种方法实际的样子,以及它的优缺点。这并非全是积极的。使用嵌入作为训练数据会有一些不太理想的副作用。

介绍 Karate Club

我将展示一本书中我们之前提到过的 Python 库:Karate Club。我在前几章简要提到过它,但现在我们将实际使用它。我故意推迟详细讲解,因为我想先教授一些关于如何处理网络的核心方法,再展示使用机器学习从网络中提取社区和嵌入的看似简单的方法。这是因为使用网络嵌入而不是从网络中提取的度量数据,可能会带来一些不良副作用。我稍后会详细说明。现在,我想介绍这个强大、高效且可靠的 Python 库。

Karate Club 的文档(karateclub.readthedocs.io/en/latest/)清晰简洁地解释了该库的功能:

Karate Club 是一个为 NetworkX 提供的无监督机器学习扩展库。它基于其他开源线性代数、机器学习和图信号处理库,如 NumPy、SciPy、Gensim、PyGSP 和 Scikit-learn。Karate Club 包含了用于对图结构数据进行无监督学习的最先进的方法。简单来说,它是小规模图挖掘研究的瑞士军刀。

这一段中有两点应该特别引起注意:无监督机器学习图形。你可以将 Karate Club 简单地看作是图形的无监督学习。然后,Karate Club 的输出可以与其他库一起用于实际的预测。

Karate Club 中有许多很酷的无监督学习方法,这让了解它们成为一种真正的乐趣。你可以在karateclub.readthedocs.io/en/latest/modules/root.html上了解它们。我最喜欢的一点是,文档链接到关于这些算法的原始研究论文。这让你能够真正了解无监督机器学习模型背后的过程。为了选择本章使用的模型,我阅读了七篇研究论文,每一刻我都很喜欢。

这个库的另一个优点是,输出在各个模型间是标准化的。一个模型生成的嵌入与另一个模型生成的嵌入是相似的。这意味着你可以轻松地尝试不同的嵌入方法,看看它们如何影响用于分类的模型。我们将在本章中准确地做到这一点。

最后,我从未见过像 Karate Club 那样简单的社区检测。使用 NetworkX 或其他库进行 Louvain 社区检测需要一些工作来进行设置。而使用 Karate Club 中的可扩展社区检测SCD),你可以通过非常少的代码行从图形转换为已识别的社区。它非常简洁。

如果你想了解更多关于 Karate Club 和图机器学习的内容,我推荐《图机器学习》这本书。你可以在www.amazon.com/Graph-Machine-Learning-techniques-algorithms/dp/1800204493/上购买。这本书比本章将要讨论的内容更详细地讲解了 Karate Club 的能力。它也是本书之后的好跟读书籍,因为本书讲解了如何使用 Python 与网络进行交互的基础,而《图机器学习》则在此基础上更进一步。

网络科学选项

需要注意的是,你并不需要使用机器学习来处理图形。机器学习只是很有用。实际上,什么是机器学习、什么不是机器学习之间有一个模糊的界限。例如,我认为任何形式的社区检测都可以视为无监督机器学习,因为这些算法能够自动识别网络中存在的社区。按照这个定义,我们可以认为 NetworkX 提供的一些方法是无监督机器学习,但由于它们没有明确地被称为图机器学习,它们并没有在数据科学界受到同等的关注。对此需要保持警惕。

我之所以这么说,是希望你记住,已经学过的一些方法可以避免使用所谓的图机器学习(graph ML)。例如,你可以使用 Louvain 方法来识别社区,甚至只是识别连接组件。你可以使用 PageRank 来识别枢纽——你不需要嵌入方法来做这些。你可以使用k_corona(0)来识别孤立点——这完全不需要机器学习。你可以将几个图特征链在一起作为训练数据,就像我们在上一章所做的那样。如果你对模型可解释性感兴趣,你不需要使用卡拉泰社交网络来创建嵌入,甚至不应该使用卡拉泰社交网络的嵌入。

记住你在本书中学到的关于分析和剖析网络的内容。将本章中的内容作为捷径使用,或者如果你所做的事情的背后原理已经弄清楚了,嵌入方法可以作为一个不错的捷径,但任何使用这些嵌入的模型都会变成一个不可解释的黑箱。

我的建议是:尽可能使用网络科学方法(在 NetworkX 中),而不是卡拉泰社交网络(Karate Club),但要注意卡拉泰社交网络,并且它可能有其用处。这个建议并不是因为我对卡拉泰社交网络有任何蔑视,而是因为我发现从模型中提取的洞察非常有启发性,几乎没有什么能让我放弃这些洞察。例如,什么特征使得一个模型能够预测机器人和人工放大效应?

可解释性的丧失意味着你将无法理解模型的行为。这绝不是一件好事。这并不是对将图分解为嵌入方法或这些方法背后的研究的贬低;只不过值得知道,某些方法可能导致模型行为完全无法解释。

在网络数据上使用无监督机器学习

如果你查看卡拉泰社交网络的网站,你可能会注意到,针对无监督机器学习的两种方法可以分为两类:识别社区和创建嵌入。无监督机器学习不仅可以为节点创建嵌入,还可以为边或整个图创建嵌入。

社区检测

社区检测最容易理解。使用社区检测算法的目标是识别网络中存在的节点社区。你可以把社区看作是相互以某种方式互动的节点群体。在社交网络分析中,这被称为社区检测,因为它本质上是识别社交网络中的社区。然而,社区检测不仅仅局限于涉及人类的社交网络分析。也许可以将图形看作是一个由相互作用的事物组成的社交网络。网站之间有互动。国家和城市之间有互动。人们之间有互动。国家和城市之间有互动的社区(盟友和敌人)。网站之间有互动的社区。人们之间有互动的社区。这只是识别那些相互作用的事物群体。

我们在本书的第九章中讨论了社区检测。如果你还没读过这一章,我建议你回去阅读,深入了解它。

这里是一个示例社区,帮助你刷新记忆:

图 11.1 – 来自《悲惨世界》的社区

图 11.1 – 来自《悲惨世界》的社区

看这个社区,我们可以看到它是紧密相连的。每个成员与社区中的其他所有成员都相互连接。其他社区的连接较为稀疏。

图嵌入

我喜欢把图嵌入看作是将复杂的网络转化为数学模型能够更好使用的数据格式。例如,如果你使用图的边列表或 NetworkX 图(G)与随机森林模型,什么都不会发生。模型根本无法使用输入数据。因此,为了让这些模型发挥作用,我们需要将图形分解成更易用的格式。在前一章关于监督式机器学习的内容中,我们将图转换成了这种格式的训练数据:

图 11.2 – 手工制作的图形训练数据

图 11.2 – 手工制作的图形训练数据

我们还包括了一个标签,这是机器学习模型将从中学习的答案。之后,我们为每个节点附加了邻接矩阵,以便分类模型也能从网络连接中学习。

如你所见,我们很容易知道训练数据中的特征。首先,我们有一个节点的度数,然后是它的聚类、三角形的数量、它的中介中心性和接近中心性,最后是它的 PageRank 得分。

使用嵌入技术,图中的所有信息都被解构成一系列的嵌入。如果你阅读该模型背后的文章,你可以理解过程中的发生情况,但在嵌入创建之后,它实际上是不可直接解释的。这就是嵌入的样子:

图 11.3 – 无监督机器学习图嵌入

图 11.3 – 无监督机器学习图嵌入

太棒了,我们把一个图转换成了 1,751 列……什么?

尽管如此,这些嵌入仍然很有用,可以直接输入到监督学习模型中进行预测,尽管模型和数据的可解释性可能不高,但这些预测仍然非常有用。

但是这些嵌入能做什么呢?它们仅仅是一大堆没有描述的数字列。它怎么可能有用呢?好吧,有两个下游应用,一个是使用更多的无监督机器学习进行聚类,另一个是使用监督学习进行分类。

聚类

聚类中,你的目标是识别出看起来或行为相似的聚类、群组或事物。通过手工制作的训练数据和 Karate Club 生成的嵌入,可以进行聚类。将这两个数据集输入聚类算法(如 K-means)中,可以识别出相似的节点。例如,使用任何模型都有其含义,因此要花时间了解你打算使用的模型。例如,使用 K-means 时,你需要指定期望在数据中存在的聚类数量,而这个数量通常是无法事先知道的。

回到k_corona,查看了连接组件,或者按 PageRank 对节点进行排序。如果你在使用机器学习,你应该首先问自己,是否有一种基于网络的方法可以消除使用机器学习的需求。

分类

在分类任务中,你的目标是预测某些内容。在社交网络中,你可能想预测谁最终会成为朋友,谁可能想成为朋友,谁可能会点击广告,或者谁可能想购买某个产品。如果你能做出这些预测,就能自动化推荐和广告投放。或者,你可能想识别欺诈、人工放大或滥用行为。如果你能做出这些预测,你就可以自动隔离那些看起来像是不良行为的内容,并自动化响应这些类型的案例。

分类通常是机器学习中最受关注的部分,它当之无愧。在分类任务中,我们可以防止垃圾邮件破坏我们的邮箱和生产力,我们可以自动将文本从一种语言翻译成另一种语言,我们还可以防止恶意软件破坏我们的基础设施并让犯罪分子利用我们。分类可以在正确且负责任的使用下,确实让世界变得更美好、更安全。

在上一章中,我们发明了一个有趣的游戏,叫做“发现革命者”。这个游戏在现实生活中可以有不同的目的。你可以自动识别影响力人物、识别欺诈行为、识别恶意软件,或者识别网络攻击。并非所有分类器都是严肃认真的。有些分类器帮助我们更好地了解周围的世界。例如,如果你使用的是手工制作的训练数据而非嵌入数据,你可以训练一个模型来预测机器人的行为,然后你可以了解模型在识别机器人时认为最有用的特征。例如,可能是一个机器人账号创建于两天前,做了零条推文,做了 2000 条转发,并且已经有了 15000 个粉丝,这些可能与此有关。一个训练在嵌入数据上的模型可能告诉你,嵌入编号 72 很有用,但这没有任何意义。

好了,够多的说法了。让我们开始编码,看看这些如何实际运行。在本章剩下的部分,我们将使用 Karate Club 的方法。

构建图

在我们进行任何操作之前,我们需要一个图来进行实验。和上一章一样,我们将使用 NetworkX 的《悲惨世界》图,确保熟悉。

首先,我们将创建图,并去除不需要的附加字段:

import networkx as nx
import pandas as pd
G = nx.les_miserables_graph()
df = nx.to_pandas_edgelist(G)[['source', 'target']] # dropping 'weight'
G = nx.from_pandas_edgelist(df)
G_named = G.copy()
G = nx.convert_node_labels_to_integers(G, first_label=0, ordering='default', label_attribute=None)
nodes = G_named.nodes

如果你仔细看,我已经包括了两行代码,创建了一个G_named图,作为 G 的副本,并且将图 G 中的节点标签转换为数字,以便稍后在本章中使用 Karate Club。这是使用 Karate Club 时必需的步骤。

让我们先可视化一下图 G,进行简单的检查:

draw_graph(G, node_size=4, edge_width=0.2)

这会生成以下图形。我们没有包含节点标签,因此它只会显示点和线(节点和边)。

图 11.4 – 《悲惨世界》网络

图 11.4 – 《悲惨世界》网络

这看起来符合预期。每个节点都有标签,但我们没有显示它们。

我还创建了一些带标签的训练数据。这些数据包含在本书附带的 GitHub 仓库的/data部分:

train_data = 'data/clf_df.csv'
clf_df = pd.read_csv(train_data).set_index('index')

创建训练数据的过程稍微复杂一些,并且已经在上一章中解释过,所以请按照那些步骤手动学习如何操作。对于本章,你可以直接使用 CSV 文件来节省时间。让我们检查一下数据是否正确:

clf_df.head()

图 11.5 – 手工制作的训练数据

图 11.5 – 手工制作的训练数据

在本章中,只有一个模型会使用手工制作的训练数据作为输入,但我们会将标签与我们的嵌入数据一起使用。我稍后会展示如何做。

有了图和训练数据,我们就可以继续进行下去了。

社区检测实践

在社区检测中,我们的明显目标是识别网络中存在的社区。我在第九章中解释了各种方法,社区检测。在本章中,我们将使用两种 Karate Club 算法:SCD 和 EgoNetSplitter。

对于这一章,通常来说,我倾向于选择那些能够良好扩展的模型。如果一个模型或算法仅在小型网络中有用,我会避免使用它。现实世界的网络庞大、稀疏且复杂。我不认为我见过某个扩展性差的模型比那些扩展性好的算法更优秀。在社区检测中尤为如此。最好的算法确实具有良好的扩展性。我最不喜欢的则完全不具备扩展性。

SCD

我想展示的第一个社区检测算法是 SCD。你可以在karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.community_detection.non_overlapping.scd.SCD找到关于该模型的文档和期刊文章。

该模型声称比最准确的最先进社区检测解决方案要快得多,同时保持或甚至超过它们的质量。它还声称能够处理具有数十亿条边的图,这意味着它可以在现实世界网络中使用。它声称比 Louvain 算法表现得更好,后者是最快的社区检测算法。

这些都是大胆的声明。Louvain 在社区检测中非常有用,原因有几点。首先,它非常快速,适用于大规模网络。其次,Python 实现简单易用。因此,我们已经知道 Louvain 是快速且易于使用的。这个模型究竟有多好呢?让我们试试:

  1. 首先,确保你已经在计算机上安装了 Karate Club。你可以通过简单的pip install karateclub来安装。

  2. 现在,让我们使用该模型。首先,从导入开始。你需要这两个:

    from karateclub.community_detection.non_overlapping.scd import SCD
    
    import numpy as np
    
  3. 既然我们有了这些,将图的社区分配到节点上就像 1、2、3 一样简单:

    model = SCD()
    
    model.fit(G)
    
    clusters = model.get_memberships()
    

我们首先实例化 SCD,然后将图拟合到 SCD,接着获取每个节点的聚类成员关系。Karate Club 模型就是这么简单。你需要阅读文章来了解其背后的运作。

  1. 聚类是什么样的?如果我们打印clusters变量,应该会看到如下内容:

    {0: 34,
    
     1: 14,
    
     2: 14,
    
     3: 14,
    
     4: 33,
    
     5: 32,
    
     6: 31,
    
     7: 30,
    
     8: 29,
    
     9: 28,
    
     10: 11,
    
    }
    

节点零在聚类 34 中,节点 1-3 在聚类 14 中,节点 4 在聚类 33 中,以此类推。

  1. 接下来,我们将这些聚类数据塞进一个numpy数组中,以便可以用我们的命名节点更容易地确定哪些节点属于哪些聚类:

    clusters = np.array(list(clusters.values()))
    

现在,clusters变量看起来是这样的:

array([34, 14, 14, 14, 33, 32, 31, 30, 29, 28, 11, 27, 13, 26, 25, 24,  7,
       15, 15,  4, 15,  9, 11,  6, 23, 35, 11, 11, 11, 11, 11, 36,  9,  1,
        4,  4,  1,  1,  1, 15, 15, 15, 15, 37,  7,  7,  7,  7,  7,  7,  7,
        6, 15, 15, 22, 17, 21, 15,  4, 20, 17,  1,  1, 19, 19,  1,  1,  1,
        1,  1,  1,  2,  2,  1,  0, 18, 16])
  1. 然后,我们创建一个cluster数据框:

    cluster_df = pd.DataFrame({'node':nodes, 'cluster':clusters})
    
    cluster_df.head(10)
    

这将给我们如下输出:

图 11.6 – SCD 聚类数据框

图 11.6 – SCD 聚类数据框

太好了。以这种格式呈现更容易理解。我们现在有了实际的节点和它们所属的社区。

  1. 让我们通过节点成员关系找出最大的社区:

    title = 'Clusters by Node Count (SCD)'
    
    cluster_df['cluster'].value_counts()[0:10].plot.barh(title=title).invert_yaxis()
    

这将给我们如下结果:

图 11.7 – 按节点数划分的 SCD 社区

图 11.7 – 按节点数划分的 SCD 社区

  1. 社区 1 是最大的,有 13 个成员,其次是社区 15,有 10 个成员。让我们一起检查这两个社区:

    check_cluster = 1
    
    community_nodes = cluster_df[cluster_df['cluster']==check_cluster]['node'].to_list()
    
    G_comm = G_named.subgraph(community_nodes)
    
    draw_graph(G_comm, show_names=True, node_size=5)
    

这给我们带来了以下内容:

图 11.8 – SCD 社区 1

图 11.8 – SCD 社区 1

这非常棒。这是一个清晰的高度连接的社区。这是一个密集连接的社区,并不是所有节点都连接得一样好,一些节点比其他节点更为中心。

  1. 让我们看看社区 15:

    check_cluster = 15
    
    community_nodes = cluster_df[cluster_df['cluster']==check_cluster]['node'].to_list()
    
    G_comm = G_named.subgraph(community_nodes)
    
    draw_graph(G_comm, show_names=True, node_size=5)
    

结果如下:

图 11.9 – SCD 社区 15

图 11.9 – SCD 社区 15

这是另一个高质量的社区提取。所有节点都与社区中的其他节点相连。一些节点比其他节点更为中心。

  1. 让我们再看看一个社区:

    check_cluster = 7
    
    community_nodes = cluster_df[cluster_df['cluster']==check_cluster]['node'].to_list()
    
    G_comm = G_named.subgraph(community_nodes)
    
    draw_graph(G_comm, show_names=True, node_size=5)
    

我们得到了图 11.10

图 11.10 – SCD 社区 7

图 11.10 – SCD 社区 7

这是另一个高质量的社区提取。社区中的所有节点都相互连接。在这种情况下,这是一种相当令人愉悦的可视化效果,因为所有节点的连接性相同。它非常对称且美丽。

悲惨世界网络非常小,因此,自然地,SCD 模型几乎能立即进行训练。

我喜欢这种方法的一个原因是,它的设置比我在第九章中解释的其他方法简单。我可以在几乎不需要任何代码的情况下,从图形直接生成社区。事实上,如果它真的能扩展到具有数十亿条边的网络,那将是不可思议的。它快速、简洁且实用。

EgoNetSplitter

我们将要测试的下一个社区检测模型叫做 EgoNetSplitter。你可以在这里了解它:karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.community_detection.overlapping.ego_splitter.EgoNetSplitter

在 Jupyter 中,如果你按Shift + Tab进入模型实例化代码,你可以查看相关信息:

工具首先创建节点的自我网络。然后,使用 Louvain 方法创建一个人物图,并对其进行聚类。生成的重叠聚类成员关系以字典形式存储。

所以,这个模型创建自我网络,然后使用 Louvain 进行聚类,最后将重叠的成员关系存储为字典。这是一个有趣的方式,与其他方法不同,所以我觉得测试一下它的表现会很有意思。步骤与 SCD 的略有不同:

  1. 首先,让我们先把模型搭建好:

    from karateclub.community_detection.overlapping.ego_splitter import EgoNetSplitter
    
    model = EgoNetSplitter()
    
    model.fit(G)
    
    clusters = model.get_memberships()
    
    clusters = np.array(list(clusters.values()))
    
    clusters = [i[0] for i in clusters] # needed because put clusters into an array of arrays
    
  2. 这将得到我们的聚类。接下来,创建我们的cluster数据框并进行可视化的代码与 SCD 相同:

    cluster_df = pd.DataFrame({'node':nodes, 'cluster':clusters})
    
  3. 让我们通过计数来检查社区成员:

    title = 'Clusters by Node Count (EgoNetSplitter)'
    
    cluster_df['cluster'].value_counts()[0:10].plot.barh(title=title).invert_yaxis()
    

我们得到以下输出:

图 11.11 – 按节点数量划分的 EgoNetSplitter 社区

图 11.11 – 按节点数量划分的 EgoNetSplitter 社区

结果与 SCD 已经看起来不同了。这应该很有趣。

  1. 让我们来看看有什么不同。聚类 7 和 1 是最大的,我们来看看这两个:

    check_cluster = 7
    
    community_nodes = cluster_df[cluster_df['cluster']==check_cluster]['node'].to_list()
    
    G_comm = G_named.subgraph(community_nodes)
    
    draw_graph(G_comm, show_names=True, node_size=5)
    

这将绘制我们的自我网络。

图 11.12 – EgoNetSplitter 社区 7

图 11.12 – EgoNetSplitter 社区 7

我不喜欢这样。我认为左侧的节点不应该和右侧与密集连接节点相连的节点属于同一个社区。就我个人而言,我觉得这不像 SCD 的结果那样有用。

  1. 让我们看看下一个人口最多的聚类:

    check_cluster = 1
    
    community_nodes = cluster_df[cluster_df['cluster']==check_cluster]['node'].to_list()
    
    G_comm = G_named.subgraph(community_nodes)
    
    draw_graph(G_comm, show_names=True, node_size=5)
    

图 11.13 显示了结果输出。

图 11.13 – EgoNetSplitter 社区 1

图 11.13 – EgoNetSplitter 社区 1

再次出现类似的情况,其中一个节点被包含在网络中,但实际上它不应该在其中。MotherPlutarch 可能与 Mabeuf 连接,但她与社区中的其他人没有任何关系。

  1. 让我们最后来看一下下一个社区:

    check_cluster = 5
    
    community_nodes = cluster_df[cluster_df['cluster']==check_cluster]['node'].to_list()
    
    G_comm = G_named.subgraph(community_nodes)
    
    draw_graph(G_comm, show_names=True, node_size=5)
    

代码产生了以下输出:

图 11.14 – EgoNetSplitter 社区 5

图 11.14 – EgoNetSplitter 社区 5

再次看到一个节点与另一个节点连接,但没有与网络中的其他节点连接。

我不想说 EgoNetSplitter 比 SCD 或其他任何模型差。我个人更喜欢 SCD 的社区检测输出,而不是 EgoNetSplitter。然而,也可以说,考虑到它们之间仅有的一条连接,将这些额外的节点作为社区的一部分可能比将它们排除在外更好。了解这两种方法的区别以及它们结果的差异非常重要。

然而,鉴于 SCD 的可扩展性声明以及它对社区的清晰划分,我倾向于选择 SCD 进行社区检测。

既然我们已经探讨了使用无监督机器学习进行社区检测,那么让我们继续使用无监督机器学习来创建图嵌入。

图嵌入实战

既然我们已经走出了社区检测的舒适区,现在我们进入了图嵌入的奇异领域。我理解图嵌入的最简单方式就是将一个复杂的网络解构成一个更适合机器学习任务的格式。这是将复杂的数据结构转换为不那么复杂的数据结构。这是一个简单的理解方式。

一些无监督机器学习模型会创建比其他模型更多的嵌入维度(更多的列/特征),正如你将在本节中看到的那样。在本节中,我们将创建嵌入,检查具有相似嵌入的节点,然后使用这些嵌入与监督机器学习结合来预测“是否革命性”,就像我们上一章的“找出革命者”游戏。

我们将快速浏览几种不同模型的使用方法——如果我详细讲解每个模型,这一章节可能会有几百页长。所以,为了节省时间,我会提供文档链接和一个简单的总结,我们将做一些简单的比较。请理解,使用机器学习时绝不可盲目操作。请阅读文档、阅读相关文章,了解模型如何工作。我已经做了很多准备工作,你也应该这样做。当然,可以随意尝试不同的模型,看看它们的表现。如果你只是进行实验,并且不将它们投入生产环境,你不会因为使用 scikit-learn 模型而意外造成时空裂缝。

我们将需要这个辅助函数来可视化接下来的嵌入:

def draw_clustering(embeddings, nodes, title):
    import plotly.express as px
    from sklearn.decomposition import PCA
    embed_df = pd.DataFrame(embeddings)
    # dim reduction, two features; solely for visualization
    model = PCA(n_components=2)
    X_features = model.fit_transform(embed_df)
    embed_df = pd.DataFrame(X_features)
    embed_df.index = nodes
    embed_df.columns = ['x', 'y']
    fig = px.scatter(embed_df, x='x', y='y', text=embed_df.index)
    fig.update_traces(textposition='top center')
    fig.update_layout(height=800, title_text=title, font_size=11)
    return fig.show()

我需要解释一些事情。首先,这个 draw_clustering 函数使用 plotly 来创建一个交互式散点图。你可以进行缩放并交互式地检查节点。你需要安装 plotly,可以通过 pip install plotly 来完成安装。

其次,我使用 主成分分析PCA)将嵌入降至二维,主要是为了可视化。PCA 也是一种无监督学习方法,适用于降维。我需要这样做,以便向你展示这些嵌入模型表现不同。将嵌入降至二维后,我可以在散点图上可视化它们。我不建议在创建嵌入后进行 PCA。此过程仅用于可视化。

FEATHER

我们将使用的第一个算法叫做 FEATHER,你可以在 karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.node_embedding.attributed.feathernode.FeatherNode 了解它。

在 Jupyter 中,如果你 Shift + Tab 进入模型实例化代码,你可以阅读相关内容:

“FEATHER-N” 的实现,< arxiv.org/abs/2005.07959 >,来自 CIKM ‘20 论文《图上的特征函数:同类相聚,从统计描述符到参数模型》。该过程使用节点特征的特征函数与随机游走权重来描述节点邻域。

FEATHER 声称能够创建高质量的图形表示,有效地执行迁移学习,并且能很好地扩展到大规模网络。它创建节点嵌入。

这个模型实际上非常有趣,因为它可以同时使用图形和附加的训练数据来创建嵌入。我很想进一步探索这个想法,看看它在不同类型的训练数据(如 tf-idf 或主题)下表现如何:

  1. 现在,让我们使用之前用过的手工制作的训练数据:

    from karateclub.node_embedding.attributed.feathernode import FeatherNode
    
    model = FeatherNode()
    
    model.fit(G, clf_df)
    
    embeddings = model.get_embedding()
    

首先,我们导入模型,然后实例化它。在model.fit这一行,注意到我们同时传入了Gclf_df。后者是我们手动创建的训练数据。与其他模型不同,我们只传入G。对我来说,这是非常有趣的,因为它似乎让模型能够基于其他上下文数据学习更多关于网络的内容。

  1. 让我们可视化这些嵌入,以便看看模型是如何工作的:

    title = 'Les Miserables Character Similarity (FeatherNode)'
    
    draw_clustering(embeddings, nodes, title)
    

我们得到以下输出:

图 11.15 – FEATHER 嵌入

图 11.15 – FEATHER 嵌入

这很有趣。我们可以看到有几个节点出现在一起。由于这是一个交互式可视化,我们可以检查其中任何一个。如果我们放大左下角的聚类,我们可以看到以下内容:

图 11.16 – FEATHER 嵌入放大图

图 11.16 – FEATHER 嵌入放大图

由于重叠,很难阅读,但Feuilly出现在左下角,靠近Prouvaire

  1. 让我们检查一下他们的自我网络,看看有哪些相似之处:

    node = 'Feuilly'
    
    G_ego = nx.ego_graph(G_named, node)
    
    draw_graph(G_ego, show_names=True, node_size=3)
    

这产生了图 11**.17

图 11.17 – Feuilly 自我网络

图 11.17 – Feuilly 自我网络

  1. 现在,让我们检查一下 Prouvaire 的自我网络:

    node = 'Prouvaire'
    
    G_ego = nx.ego_graph(G_named, node)
    
    draw_graph(G_ego, show_names=True, node_size=3)
    

这输出了图 11**.18

图 11.18 – Prouvaire 自我网络

图 11.18 – Prouvaire 自我网络

很好。第一个观察结果是,它们都是彼此自我网络的一部分,并且也是彼此社区的一部分。第二,它们的节点连接性相当强。在两个自我网络中,两个节点都显示出相当强的连接性,并且都是密集连接社区的一部分。

  1. 让我们看一下其他一些节点:

图 11.19 – FEATHER 嵌入放大图

图 11.19 – FEATHER 嵌入放大图

  1. 让我们检查一下MotherInnocentMmeMagloire的自我网络。首先是MotherInnocent

    node = 'MotherInnocent'
    
    G_ego = nx.ego_graph(G_named, node)
    
    draw_graph(G_ego, show_names=True, node_size=3)
    

图 11**.20显示了输出。

图 11.20 – MotherInnocent 自我网络

图 11.20 – MotherInnocent 自我网络

现在是MmeMagloire

node = 'MmeMagloire'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)

图 11**.21显示了结果。

图 11.21 – MmeMagloire 自我网络

图 11.21 – MmeMagloire 自我网络

MotherInnocent有两条边,MmeMagloire有三条。它们的自我网络相当小。这些相似性被 FEATHER 捕捉并转化为嵌入。

  1. 但实际的嵌入是什么样的呢?

    eb_df = pd.DataFrame(embeddings, index=nodes)
    
    eb_df['label'] = clf_df['label']
    
    eb_df.head(10)
    

这将生成以下数据框。

图 11.22 – FEATHER 嵌入数据框

图 11.22 – FEATHER 嵌入数据框

图形被转化为 1,750 维的嵌入。在这种格式下,你可以将它们看作列或特征。一个简单的网络被转化为 1,750 列,这对于这么小的网络来说数据量相当大。我们在处理其他模型(如 FEATHER)时,注意这些模型所创建的维度数量。

这些嵌入对分类很有用,所以我们就做这个。我将直接把数据丢给分类模型,并希望能够得到好结果。这种做法除了用于简单实验之外从来不是一个好主意,但这正是我们要做的。我鼓励你深入探索你感兴趣的任何模型。

  1. 前面的代码已经添加了label字段,但我们还需要创建我们的Xy数据来进行分类:

    from sklearn.model_selection import train_test_split
    
    X_cols = [c for c in eb_df.columns if c != 'label']
    
    X = eb_df[X_cols]
    
    y = eb_df['label']
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1337, test_size=0.3)
    

X是我们的数据,y是正确答案。这是我们的“识别革命者”训练数据。标记为革命者的节点其y1,其余节点的y0

  1. 让我们训练一个随机森林模型,因为我想向你展示一些关于可解释性的东西:

    from sklearn.ensemble import RandomForestClassifier
    
    clf = RandomForestClassifier(random_state=1337)
    
    clf.fit(X_train, y_train)
    
    train_acc = clf.score(X_train, y_train)
    
    test_acc = clf.score(X_test, y_test)
    
    print('train accuracy: {}\ntest accuracy: {}'.format(train_acc, test_acc))
    

如果我们运行这段代码,得到的结果是:

train accuracy: 0.9811320754716981
test accuracy: 1.0

使用 FEATHER 嵌入作为训练数据,模型能够在未见过的数据上 100%准确地识别出革命者。这是一个小型网络,不过,绝对不要,永远不要信任任何能给出 100%准确度的模型。一个看似达到 100%准确度的模型,通常会隐藏一个更深层次的问题,比如数据泄漏,因此很有必要对非常高的分数持怀疑态度,或者在模型经过彻底验证之前,通常要对模型结果保持怀疑。这是一个玩具模型。然而,这表明这些嵌入可以通过图形创建,并且监督式机器学习模型可以在预测中使用这些嵌入。

然而,使用这些嵌入与模型结合时有一个严重的缺点。你失去了所有的可解释性。在前面这一章中展示的手工制作的训练数据中,我们可以看到模型在做出预测时最有用的特征是什么。让我们检查一下这些嵌入的特征重要性:

importances = pd.DataFrame(clf.feature_importances_, index=X_train.columns)
importances.columns = ['importance']
importances.sort_values('importance', ascending=False, inplace=True)
importances[0:10].plot.barh(figsize=(10,4)).invert_yaxis()

我们得到这个输出:

图 11.23 – FEATHER 嵌入特征重要性

图 11.23 – FEATHER 嵌入特征重要性

太棒了!发现 1531 嵌入比 1134 稍微有用,但这两个嵌入都比其他嵌入要有用得多!太棒了!这是完全丧失了解释性,但这些嵌入确实有效。如果你只是想从图到机器学习,这种方法可以使用,但无论使用哪个模型进行预测,最终都会得到一个黑箱模型。

好了,对于接下来的模型,我将加快速度。我们将重复使用很多代码,我只是会减少可视化部分,并减少代码量,以便本章不会变成 100 页长。

NodeSketch

我们接下来要看的算法是NodeSketch,你可以在karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.node_embedding.neighbourhood.nodesketch.NodeSketch了解更多信息。

在 Jupyter 中,如果你按Shift + Tab进入模型实例化代码,你可以查看相关信息:

“NodeSketch”的实现 https://exascale.info/assets/pdf/yang2019nodesketch.pdf

来自 KDD ‘19 论文 “NodeSketch: 高效图嵌入

通过递归草图化”。该过程从绘制图的自环增强邻接矩阵开始,输出低阶节点嵌入,然后基于自环增强邻接矩阵和(k-1)阶节点嵌入递归生成 k 阶节点嵌入。

与 FEATHER 类似,NodeSketch 也会创建节点嵌入:

  1. 让我们使用模型,一次性进行可视化:

    from karateclub.node_embedding.neighbourhood.nodesketch import NodeSketch
    
    model = NodeSketch()
    
    model.fit(G)
    
    embeddings = model.get_embedding()
    
    title = 'Les Miserables Character Similarity (NodeSketch)'
    
    draw_clustering(embeddings, nodes, title)
    

以下图表是结果:

图 11.24 – NodeSketch 嵌入

图 11.24 – NodeSketch 嵌入

  1. 如前所示,这个可视化是互动式的,你可以放大节点集群以进行更细致的检查。让我们来看几个被发现相似的节点。

首先,Eponine

node = 'Eponine'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)

你可以在 图 11**.25 中看到结果。

图 11.25 – Eponine 自我网络

图 11.25 – Eponine 自我网络

  1. 接下来,Brujon

    node = 'Brujon'
    
    G_ego = nx.ego_graph(G_named, node)
    
    draw_graph(G_ego, show_names=True, node_size=3)
    

图 11**.26 所示。

图 11.26 – Brujon 自我网络

图 11.26 – Brujon 自我网络

经检查,自我网络看起来差异很大,但这两个节点似乎具有相同数量的连接,并且它们都是一个连接良好的社区的一部分。我很满意这两个节点在结构和位置上非常相似。两个节点也是同一个社区的一部分。

  1. 嵌入的样子是什么?

    eb_df = pd.DataFrame(embeddings, index=nodes)
    
    eb_df['label'] = clf_df['label']
    
    eb_df.head(10)
    

这将展示我们的数据框。

图 11.27 – NodeSketch 嵌入数据框

图 11.27 – NodeSketch 嵌入数据框

哇,这是一个比 FEATHER 生成的数据集简单得多。32 个特征,而不是 1,750 个。另外,请注意,嵌入中的值是整数而不是浮动值。随机森林能在这个训练数据上做出多好的预测?

X_cols = [c for c in eb_df.columns if c != 'label']
X = eb_df[X_cols]
y = eb_df['label']
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1337, test_size=0.3)
clf = RandomForestClassifier(random_state=1337)
clf.fit(X_train, y_train)
train_acc = clf.score(X_train, y_train)
test_acc = clf.score(X_test, y_test)
print('train accuracy: {}\ntest accuracy: {}'.format(train_acc, test_acc))

如果我们运行代码,会得到以下结果:

train accuracy: 0.9811320754716981
test accuracy: 1.0

该模型能够在训练数据上以 98% 的准确率进行预测,在测试数据上则达到了 100% 的准确率。同样,永远不要测试一个给出 100% 准确率的模型。但这仍然显示出该模型能够使用这些嵌入。

  1. 它发现了哪些重要特征?

    importances = pd.DataFrame(clf.feature_importances_, index=X_train.columns)
    
    importances.columns = ['importance']
    
    importances.sort_values('importance', ascending=False, inplace=True)
    
    importances[0:10].plot.barh(figsize=(10,4)).invert_yaxis()
    

这将产生 图 11**.28

图 11.28 – NodeSketch 嵌入特征重要性

图 11.28 – NodeSketch 嵌入特征重要性

很好。如前所示,使用这些嵌入将随机森林变成了一个黑盒模型,我们无法从中获取任何模型可解释性。我们知道模型发现特征 2423 最为有用,但我们不知道为什么。之后我不会再展示特征重要性了,你明白了。

这是一个很酷的模型,它默认创建比 FEATHER 更简单的嵌入。随机森林在这两种模型的嵌入上表现得都很好,我们无法在没有更多实验的情况下说哪个更好,而这超出了本章的范围。祝你在实验中玩得开心!

RandNE

接下来是RandNE,它声称对于“百亿级网络嵌入”非常有用。这意味着它适用于具有数十亿节点或数十亿边的网络。这一声明使得该模型对于大规模的现实世界网络非常有用。你可以在karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.node_embedding.neighbourhood.randne.RandNE阅读相关文档。

在 Jupyter 中,如果你Shift + Tab进入模型实例化代码,你可以查看相关信息:

“RandNE”实现来自 ICDM '18 论文《百亿级网络嵌入与迭代随机投影》 https://zw-zhang.github.io/files/2018_ICDM_RandNE.pdf。该过程使用基于正则化邻接矩阵的平滑方法,并在正交化的随机正态生成基节点嵌入矩阵上进行操作。

  1. 再次,我们来一次性生成嵌入并进行可视化:

    from karateclub.node_embedding.neighbourhood.randne import RandNE
    
    model = RandNE()
    
    model.fit(G)
    
    embeddings = model.get_embedding()
    
    title = 'Les Miserables Character Similarity (RandNE)'
    
    draw_clustering(embeddings, nodes, title)
    

输出的图表如下:

图 11.29 – RandNE 嵌入

图 11.29 – RandNE 嵌入

  1. 你可以立即看到,这个散点图与 FEATHER 和 NodeSketch 的都很不同。让我们来看看MariusMotherPlutarch这两个已被认为是相似的节点的自我网络:

    node = 'Marius'
    
    G_ego = nx.ego_graph(G_named, node)
    
    draw_graph(G_ego, show_names=True, node_size=3)
    

我们得到了一个网络输出:

图 11.30 – Marius 自我网络

图 11.30 – Marius 自我网络

  1. 接下来是MotherPlutarch

    node = 'MotherPlutarch'
    
    G_ego = nx.ego_graph(G_named, node)
    
    draw_graph(G_ego, show_names=True, node_size=3)
    

网络如下所示:

图 11.31 – MotherPlutarch 自我网络

图 11.31 – MotherPlutarch 自我网络

哇,这些自我网络差异如此之大,节点也一样。Marius是一个连接良好的节点,而MotherPlutarch与另一个节点只有一条边。这是两个非常不同的节点,而嵌入结果却显示它们是相似的。不过,这可能是由于散点图可视化中的 PCA 步骤导致的,因此请不要仅凭这一例子就对 RandNE 做出过快判断。查看其他相似节点。我将把这个留给你,作为你自己的练习和学习。

嵌入结果是什么样子的?

eb_df = pd.DataFrame(embeddings, index=nodes)
eb_df['label'] = clf_df['label']
eb_df.head(10)

这将显示我们的嵌入。

图 11.32 – RandNE 嵌入数据框

图 11.32 – RandNE 嵌入数据框

最终的嵌入是 77 个特征,因此它默认创建的嵌入比 FEATHER 更简单。相比之下,NodeSketch 创建了 32 个特征。

  1. 随机森林能够多好地利用这些嵌入?

    X_cols = [c for c in eb_df.columns if c != 'label']
    
    X = eb_df[X_cols]
    
    y = eb_df['label']
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1337, test_size=0.3)
    
    clf = RandomForestClassifier(random_state=1337)
    
    clf.fit(X_train, y_train)
    
    train_acc = clf.score(X_train, y_train)
    
    test_acc = clf.score(X_test, y_test)
    
    print('train accuracy: {}\ntest accuracy: {}'.format(train_acc, test_acc))
    

如果我们运行这段代码,结果将如下所示:

train accuracy: 0.9811320754716981
test accuracy: 0.9166666666666666

该模型在测试集上的准确率为 98.1%,在测试集上的准确率为 91.7%。这比使用 FEATHER 和 NodeSketch 嵌入的结果要差,但这可能是个偶然。我不会在如此少的训练数据下相信这些结果。该模型能够成功地将嵌入作为训练数据使用。然而,如前所述,如果你检查嵌入的特征重要性,你将无法解释这些结果。

其他模型

这些并不是 Karate Club 中唯一可用于创建节点嵌入的三个模型。还有两个模型。你可以像我们使用 FEATHER、NodeSketch 和 RandNE 一样进行实验。所有嵌入模型在随机森林上的结果大致相同。它们都可以是有用的。我建议你对 Karate Club 保持好奇,开始研究它提供的内容。

这些模型做的是相同的事情,但实现方式不同。它们的实现非常有趣。我建议你阅读关于这些方法的论文。你可以把这些看作是创建节点嵌入的演变过程。

GraRep

GraRep 是我们可以使用的另一个模型。你可以在这里找到文档:karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.node_embedding.neighbourhood.grarep.GraRep

from karateclub.node_embedding.neighbourhood.grarep import GraRep
model = GraRep()
model.fit(G)
embeddings = model.get_embedding()

DeepWalk

DeepWalk 是我们可以使用的另一个可能模型:karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.node_embedding.neighbourhood.deepwalk.DeepWalk

from karateclub.node_embedding.neighbourhood.deepwalk import DeepWalk
model = DeepWalk()
model.fit(G)
embeddings = model.get_embedding()

既然我们有了几种创建图嵌入的选项,接下来让我们在监督式机器学习中使用它们进行分类。

在监督式机器学习中使用嵌入

好的!我们已经完成了涉及网络构建、社区检测、无监督和监督机器学习的有趣实践工作;进行了自我中心网络可视化;并检查了使用不同嵌入的结果。本章将所有内容整合在一起。我希望你和我一样享受这次实践工作,也希望你觉得它有用且富有启发性。在结束本章之前,我想回顾一下我们使用嵌入的优缺点。

请记住,我们本来可以测试很多其他分类模型,而不仅仅是随机森林。你也可以将这些嵌入用于神经网络,或者用逻辑回归进行测试。利用你在这里学到的知识,去尽可能多地学习并享受乐趣。

优缺点

让我们来讨论使用这些嵌入式方法的优缺点。首先,我们从缺点开始。我在本章中已经提到过几次这一点,所以我再重复一次:如果你使用这些嵌入式方法,无论分类模型多么可解释,你都会失去所有的可解释性。无论如何,你现在得到的是一个黑箱模型,好的坏的都不重要。如果有人问你为什么你的模型会有某种预测,你只能耸耸肩说那是魔法。当你选择使用嵌入式方法时,你就失去了检查特征重要性的能力。它没了。回到原点的方法是使用像我们在本章开头和上一章中创建的手工制作的网络训练数据,但那需要网络科学的知识,这也许是一些人宁愿只使用这些嵌入式方法的原因。这就引出了这些嵌入式方法的优点。

好处是,创建和使用这些嵌入式方法比创建你自己的训练数据要容易得多,速度也更快。你必须了解网络科学,才能知道什么是中心性、聚类系数和连通组件。而你不需要了解任何关于网络科学的知识,就可以运行以下代码:

from karateclub.node_embedding.neighbourhood.grarep import GraRep
model = GraRep()
model.fit(G)
embeddings = model.get_embedding()

当人们盲目使用数据科学中的工具时,这就是一个问题,但这总是发生。我并不是为此辩解。我只是陈述事实,这种情况在各个领域都存在,而空手道俱乐部的嵌入式方法让你在使用图数据进行分类时,不需要真正了解任何关于网络的知识。我认为这是一个问题,但它不仅仅发生在图数据中。在自然语言处理(NLP)和机器学习(ML)中,这种情况普遍存在。

失去可解释性和洞察力

使用这些嵌入式方法的最糟糕部分是你失去了所有模型的可解释性和洞察力。就我个人而言,构建模型并不会让我感到兴奋。我更兴奋的是通过预测和学习到的重要性获得的洞察力。我兴奋的是理解模型捕捉到的行为。使用嵌入式方法后,这一切都没有了。我把可解释性丢进了垃圾桶,希望能快速创建一个有效的模型。

这与我对主成分分析(PCA)的看法是一样的。如果你使用它来进行降维,你就会失去所有的可解释性。我希望你在决定使用 PCA 或图嵌入之前已经做过科学探索。否则,这就是数据炼金术,而不是数据科学。

更简便的分类和聚类工作流

不过,并非全是坏事。如果你发现某种类型的嵌入式方法是可靠且高质量的,那么你确实可以通过它快速进行分类和聚类,只要你不需要可解释性。你可以在几分钟内从图数据到分类或聚类,而不是几个小时。这与从图特征手工制作训练数据相比,是一个巨大的加速。因此,如果你只是想看看图数据是否能用于预测某些东西,这确实是构建原型的一个捷径。

这全是优缺点和使用案例。如果你需要可解释性,在这里是得不到的。如果你需要快速推进,这里可以提供帮助。而且,很有可能在了解更多关于嵌入的内容之后,你会从中获得一些见解。我也确实见证过这一点。有时候,你能够找到一些见解——只是需要间接的方法才能得到它们,希望本书能给你一些启发。

总结

我简直不敢相信我们已经走到这一步。在本书开始时,这看起来像是一个不可能完成的任务,但现在我们已经做到了。为了完成本章的实践练习,我们使用了前几章学到的内容。我希望我已经向你展示了网络如何有用,以及如何与它们一起工作。

在本书的开头,我决定写一本实践性强的书,内容侧重于代码,而非数学。有很多网络分析书籍,侧重于数学,但很少展示实际的实现,甚至没有。我希望本书能够有效填补这一空白,为程序员提供一项新技能,并向社会科学家展示通过编程方式提升他们的网络分析水平。非常感谢你阅读这本书!

posted @ 2025-10-24 09:52  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报