原文:
annas-archive.org/md5/3df7c5feb0bf40d7b9d88197a04b0b37
译者:飞龙
协议:CC BY-NC-SA 4.0
前一章内容非常丰富,我们学习了如何可视化和分析整个网络。相比之下,本章应该会感觉更简单,内容也会少得多。在之前的章节中,我们学习了如何获取和创建网络数据,如何从网络数据构建图形,如何清理图形数据,以及如何做一些有趣的事情,比如识别社区。在本章中,我们将进行所谓的 自我中心 网络分析。
好消息是,前一章所学的所有内容都可以应用于自我中心网络。中心性可以帮助我们找出重要的节点。社区算法可以帮助我们识别社区。最棒的消息是,本章其实不需要涉及太多内容。自我中心网络分析在规模和范围上都更简单。最重要的是,我会解释如何开始,展示你可以做什么,并说明你未来可能想采取的进一步分析步骤。就像任何类型的分析一样,总是有更多的事情可以做,但我们将在本章中保持简单。
本章将涉及以下主题:
进行自我中心网络分析
调查自我节点和连接
识别其他研究机会
在本章中,我们将主要使用 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)
这将产生如下网络:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_001.jpg
图 8.1 – 《悲惨世界》整个网络
现在的结果足够了,但我想提醒你,我们在上一章学到过的内容。我们可以使用 k_core
来移除可视化中节点数小于 k
的节点。在这个例子中,我选择不显示节点数少于两个的节点:
draw_graph(nx.k_core(G, 2), font_size=12, show_names=True, node_size=4, edge_width=1)
这将绘制一个网络可视化,显示具有两个或更多边的节点,有效地移除孤立节点和仅有一条边的节点。这将帮助我们快速了解和预览网络的结构:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_002.jpg
图 8.2 – 《悲惨世界》整个网络 (k=2)
在可视化一个网络后,我通常会收集网络中所有节点的 PageRank 分数。PageRank 是一个快速的算法,能够在网络规模不管多大时都表现良好,因此它是一个快速识别节点重要性的好算法,正如上一章所讨论的那样。提醒一下,pagerank
算法是基于一个节点的进出边数量来计算其重要性分数的。对于这个网络,我们使用的是无向图,因此pagerank
实际上是基于一个节点所拥有的边的数量来计算分数的,因为在无向网络中并没有in_degree
或out_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
算法的计算过程:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_003.jpg
图 8.3 – 《悲惨世界》网络中的前 20 个 PageRank 节点
一张图片可以帮助我们更容易地看到每个节点之间 PageRank 的差异:
pagerank_df.head(20).plot.barh(figsize=(12,8)).invert_yaxis()
可视化结果如下:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_004.jpg
图 8.4 – 《悲惨世界》网络中前 20 个 PageRank 节点的可视化
很好。我们可以清楚地看到瓦尔让在这个故事中是一个非常重要的角色。我们肯定需要检查瓦尔让的自我网络,以及米里埃尔和加夫罗什的自我网络。为了确保我们不会得到太相似的自我网络,我选择了乔利作为第四个要检查的角色。乔利在 PageRank 榜单上排得较低。
这就是我们在本章中进行的全部网络分析。从此时起,我们将开始学习自我网络。让我们开始吧!
在自我中心网络分析中,我们感兴趣的是了解围绕一个单一节点存在的社区。我们对整个网络的结构和组成不太感兴趣。可以说,我们是“放大”来看。我们将使用自我中心网络分析来检查《悲惨世界》中核心角色周围存在的社区。
根据维基百科,尚·瓦尔让是《悲惨世界》的主角。了解这一点后,我们就能明白瓦尔让在网络中拥有最高的 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)
我们现在可以可视化自我网络:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_005.jpg
图 8.5 – Valjean 自我网络
如果你仔细看,你应该能看到 Valjean 位于他自己自我网络的中心。这是有道理的。自我节点(Valjean)与存在于自我网络中的所有他人节点(其他节点)都有某种关系。
在继续之前,有一件重要的事情我想指出。自我网络只是另一种网络。我们在上一章中学习的所有内容——中心性、社区、度数、k_core
和k_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)
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_006.jpg
图 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)
现在,让我们使用前面的代码来可视化它:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_007.jpg
图 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']
有两种不同的方式可以获取自我网络中存在的交互者数量。记住,我们已经去掉了中心节点(自我节点),因此所有剩余的节点都是交互者:
第一个方法是简单地计算网络中节点的数量:
len(ego_1.nodes)
36
没问题,但如果我们还想看到边的数量呢?
让我们直接使用 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)
让我们可视化这一点,看看我们的中心性:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_008.jpg
图 8.8 – Valjean 自我网络中交互者的度中心性
degree_centrality
大约为 0.457
。考虑到 Javert 在自我网络中的中心地位,这很有道理。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_009.jpg
图 8.9 – Javert 在 Valjean 自我网络中的网络位置
其他具有最高中心性的交互者则更难以察觉。MmeThenardier 位于中心,右侧突出了。
让我们通过计算网络密度来总结这个自我网络。密度与网络中所有节点之间的连接程度有关。为了达到 1.0
的密度,每个节点都会与网络中的每个其他节点相连。要有 0.0
的密度,网络将完全由孤立节点组成,节点之间没有任何连接。你能大致猜测这个网络的密度是多少吗?它看起来连接松散,且有几个密集连接的社区。因此,我的猜测是一个相当低的分数。让我们使用 NetworkX 来计算密度:
nx.density(ego_1)
0.12063492063492064
密度大约为0.12
的网络是一个连接松散的网络。我计算了密度,因为我希望用它来比较每个自我网络的密度。
注
你可能在想,为什么我们在看介数中心性(betweenness centrality)和密度,或者想知道中心性和密度之间的关系。中心性得分有助于了解网络中一个节点的重要性。密度则告诉我们网络的整体构成。如果一个网络是密集的,那么节点之间的连接比稀疏网络要多。中心性和密度得分是快速了解网络的一种方式。
维基百科将马吕斯·庞特马西(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)
接下来,我们将可视化整个自我网络:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_010.jpg
图 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)
这将绘制出我们的自我网络:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_011.jpg
图 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)
这将给我们提供度中心性。让我们仔细看看!
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_012.jpg
图 8.12 – 马吕斯的自我网络外部节点的度中心性
哇,有意思。我本以为让·瓦尔让会是最中心的节点之一,但有几个人排在他前面。你能猜到为什么吗?他们是紧密连接社区的一部分,每个人与自我网络中的成员连接的数量都超过了瓦尔让。这个社区应该有很多信息共享。现在看看瓦尔让的位置,我可以看到他是一个核心人物,但他连接的节点比紧密连接社区的成员要少。
请注意,有几个节点的中心性得分相同。是的,这种情况会发生。中心性得分只是数学的结果,而不是魔法。
最后,为了比较自我网络,让我们计算密度得分:
nx.density(ego_2)
0.3333333333333333
记住,瓦尔让的自我网络密度大约是0.12
。马吕斯的自我网络密度几乎是瓦尔让的三倍。这可以解释为什么在去掉马吕斯的中心节点时,网络并没有破裂。紧密连接的网络在去除中心节点时更加具有韧性。这一点在考虑如何增强现实世界网络的可用性时非常重要。从人的角度来看,即使关键节点被去除,这个社区仍然会继续存在。
在《悲惨世界》中,加夫罗什是一个生活在巴黎街头的小男孩。考虑到这一点,我想他的自我网络会与成年人或社会中更为联系紧密的人截然不同。让我们来看看。
首先,让我们可视化网络,保留中心节点:
ego_3 = nx.ego_graph(G, 'Gavroche')
draw_graph(ego_3, font_size=12, show_names=True, node_size=4, edge_width=1)
这将呈现加夫罗什的完整自我网络:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_013.jpg
图 8.13 – 加夫罗什的自我网络
有趣。加夫罗什的连接广泛。考虑到他是一个生活在街头的孩子,看到Child1和Child2位于顶部很有意思。这三个人物之间的关系看起来可能非常有趣。我还看到一个人(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)
这将呈现加夫罗什的自我网络,去除中心节点后。这应该能让不同的社区更容易识别:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_014.jpg
图 8.14 – 加夫罗什的自我网络,去除中心节点
完美。如预期,一些节点变成了孤立点,两个孩子形成了他们自己的小群体,剩余的连接组件包含两个独立的社区。使用社区检测算法分析最大群体可能会很有趣,但让我们继续。很明显,瓦尔让位于这两个社区之间。
从这个网络中移除孤立点没有多大意义,因为只有一个孤立点,所以让我们继续前进。
让我们看看还有哪些其他角色是加夫罗什自我网络的一部分。换句话说,我想知道加夫罗什认识谁。这将告诉我们哪些角色在他的生活中扮演了重要角色:
输入以下代码:
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']
很好。我能看到一些熟悉的名字,而且我也清楚地看到了Child1
和Child2
。
接下来,我们来看看这个网络中有多少个节点和边,采用简单的方法:
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)
这将给我们一个按度中心性排序的角色数据框。我们仔细看看:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_015.jpg
图 8.15 – 加夫罗什的自我网络变更后的度中心性
哇,恩乔伊拉斯是一个高度连接的人,还有其他几个高度连接的人。我们可以看到他们在特别紧密的社区中。
最后,让我们计算密度,以便比较自我网络:
nx.density(ego_3)
0.354978354978355
这真有趣。加夫罗什是一个生活在巴黎街头的孩子,但他的社交网络比我们之前看到的任何一个都要密集。瓦尔让的自我网络密度大约是0.12
,马吕斯的是0.33
,而加夫罗什的更高。我从没想到过这一点。如果我读这本书,我会特别关注这个角色。他看起来非常有联系,我很好奇这在故事中的表现如何。
在本章的开头,我选择了四个人来创建自我网络。前三个有较高的 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)
这将展示乔利的自我网络。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_016.jpg
图 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)
这将绘制出去除中心的乔利自我网络。如果该网络中存在独立的社区,它们将作为群集单独展示。如果只有一个社区,那么该社区将作为一个单一的群集展示:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_017.jpg
图 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)
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_08_018.jpg
图 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 的悲惨世界图,因为它包含了几个独立的社区:
加载网络是简单的:
import networkx as nx
import pandas as pd
G = nx.les_miserables_graph()
这就是加载图形所需要的全部操作。
有一个weight
属性,我不打算在网络中包含它,因为在这个简单的演示中我们不需要边权重。因此,我将删除它并重新构建图形:
df = nx.to_pandas_edgelist(G)[['source', 'target']]
# dropping 'weight'
G = nx.from_pandas_edgelist(df)
在这两个步骤中,我们将悲惨世界图转换为pandas
的边列表,并且仅保留source
和target
字段,有效地去除了weight
字段。让我们看看网络中有多少节点和边:
nx.info(G)
'Graph with 77 nodes and 254 edges'
这是一个小网络。这个网络包含孤立点和孤岛吗,还是只有一个大的连通分量?让我们来检查一下。
首先,让我们添加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)
现在,让我们全面展示网络:
draw_graph(G, font_size=12, show_names=True, node_size =4, edge_width=1)
输出如下:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_001.jpg
图 9.1 – 悲惨世界图
一眼看去,我们应该能够看到没有孤立点(没有边的节点),有几个只有一条边的节点,有几个节点群体非常接近(社区),还有一些非常关键的节点。如果移除这些关键节点,网络就会支离破碎。
让我们稍微放大一点,使用k_core
,并且只显示具有两条或更多边的节点。我们也不显示标签,这样可以更好地看到网络的整体形状:
draw_graph(nx.k_core(G, 2), font_size=12, show_names=False, node_size=4, edge_width=0.5)
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_002.jpg
图 9.2 – 悲惨世界图,k_core,K=2,无标签
现在社区应该更清晰了。寻找节点紧密且边/线更多的图部分。你看到了几个社区?我看到有四个社区特别明显,但周围也有一些较小的群体,而且网络中心可能还有一个社区。
现在我们已经准备好尝试社区检测了。
理解网络中存在的各种社区和结构的第一步常常是分析连通分量。正如我们在w中讨论的,连通分量是网络中的结构,其中所有节点都与同一组件中的另一个节点连接。
正如我们之前看到的,连通分量对于查找较小的连接部分非常有用。这些可以被视为社区,因为它们与主要组件和整体网络分离,但最大的连通分量通常不是单个社区。它通常由几个社区组成,并且通常可以分割成单独的社区。
在悲惨世界网络中,只有一个连通分量。没有孤岛或孤立点。只有一个单一组件。这是有道理的,因为这些都是文学作品中的角色,书中的角色不会整天和自己说话。然而,这也削弱了检查此图的连通分量的一些用处。
有一个解决方法!正如我之前提到的,如果我们从网络中移除几个关键节点,那么网络往往会支离破碎:
让我们从网络中移除五个非常重要的角色:
G_copy = G.copy()
G_copy.remove_nodes_from(['Valjean', 'Marius', 'Fantine', 'Cosette', 'Bamatabois'])
在这两行代码中,我们构建了一个名为G_copy
的第二个图,然后移除了五个关键节点。让我们再次可视化这个网络!
draw_graph(G_copy, font_size=12, show_names=True, node_size=4, edge_width=1)
这给我们带来了如下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_003.jpg
图 9.3 – 破碎后的《悲惨世界》网络
很好。这与许多现实世界网络的样子更为接近。依然有一个主要的连通组件(大陆),有三个较小的连通组件(岛屿),还有六个孤立节点。把这些叫做岛屿和大陆是我个人的命名方式。没有一个明确的标准来决定岛屿是否就是大陆。只是大多数网络都包含一个超大组件(大陆),大量孤立节点,以及若干连通组件(岛屿)。这对我有帮助,但你可以随意处理。
另一个需要记住的事情是,我们刚才所做的可以作为社区检测中的一步。移除一些关键节点可以将网络拆散,揭示出存在的小社区。这些至关重要的节点将一个或多个社区维系在一起,作为更大结构的一部分。移除这些重要节点使得社区之间可以自由漂移。我们通过移除重要节点来完成这一操作,虽然这通常不是最理想的做法。然而,其他实际的社区检测方法工作原理类似,通过移除边而非节点来实现。
在破碎网络后,还剩下多少个连通组件?
components = list(nx.connected_components(G_copy))
len(components)
10
NetworkX 说有 10
个连通组件,但孤立节点除了可能与自身连接外,不与任何东西相连。
在查看连通组件之前,让我们先移除它们:
G_copy = nx.k_core(G_copy, 1)
components = list(nx.connected_components(G_copy))
len(components)
4
看起来有四个连通组件。
由于它们数量很少,我们来检查每一个:
community = components[0]
G_community = G_copy.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
让我们来看一下可视化效果:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_004.jpg
图 9.4 – 破碎后的《悲惨世界》网络中组件 0 的子图
非常有趣!第一个连通组件几乎是一个星型网络,所有节点都连接到一个中心角色——米里埃尔。然而,如果你看看左上角,你应该会看到两个角色也共享一个链接。这种关系可能值得进一步研究。
让我们看下下一个组件:
community = components[1]
G_community = G_copy.subgraph(community)
draw_graph(G_community, show_names=True, node_size=4)
这给我们带来了如下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_005.jpg
图 9.5 – 破碎后的《悲惨世界》网络中组件 1 的子图
这更加有趣了。我把这个叫做主要组件。它是破碎网络中最大的连通组件。然而,正如我之前说的,连通组件并不适合用来识别社区。稍微左偏一点,你应该能看到网络中心左边有两个节点簇,两个独立的社区。右侧至少还有一个其他社区。如果移除两条边或节点,右侧的社区就会从网络中分离出来。继续前进!
让我们继续打破社区:
community = components[2]
G_community = G_copy.subgraph(community)
draw_graph(G_community, show_names=True, node_size=4)
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_006.jpg
图 9.6 – 破碎的《悲惨世界》网络的组件 2 子图
这是一个强连接组件。每个节点都与这个网络中的其他节点有连接。如果移除一个节点,网络仍然保持完整。从网络的角度来看,每个节点与其他节点同样重要或中心。
让我们检查最后一个组件:
community = components[3]
G_community = G_copy.subgraph(community)
draw_graph(G_community, show_names=True, node_size=4)
这为我们提供了以下的可视化:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_007.jpg
图 9.7 – 破碎的《悲惨世界》网络的组件 3 子图
这是另一个密集连接的网络。每个节点同样重要或中心。如果移除一个节点,网络仍然保持完整。
如你所见,我们通过查看连接组件找到了三个社区,但连接组件没有揭示出在更大主组件中存在的社区。如果我们想要识别这些社区,我们需要移除其他重要节点,然后重复分析。丢弃节点是一种丢失信息的方式,因此我不推荐这种方法,但在快速的临时分析中,它可以是有用的。
我不认为研究连接组件属于社区检测,但在研究连接组件时可以找到社区。我认为这是任何网络分析过程中应该做的第一步,获得的见解非常有价值,但它对于社区检测来说不够敏感。
如果你的网络中没有连接组件的超级集群,那么连接组件对于社区检测来说是相当足够的。然而,你必须将超级集群当作一个社区,实际上,那个集群包含了多个社区。随着网络规模的增大,连接组件方法的效果会变得不那么有效。
让我们继续讨论更合适的方法。
卢瓦恩方法无疑是我最喜欢的社区检测方法,原因有很多。
首先,这种算法可以应用于非常大的数百万节点的网络,且效果显著且快速。我们将在本章探讨的其他方法在大型网络中无法使用,也没有那么快,因此使用这种算法我们能得到其他地方找不到的效果和速度。因此,这是我首选的社区检测算法,其他的算法我则保留作为备选方案。
其次,可以调节分辨率
参数来找到最适合的社区检测分割,这在默认结果不理想时提供了灵活性。而其他算法则没有这种灵活性。
总结来说,使用 Louvain 方法,我们有了一个快速的算法,它在大规模网络中的社区检测中非常有效,我们还可以优化该算法以获得更好的结果。我建议从 Louvain 方法入手,尝试社区检测,然后在学习过程中逐步掌握其他方法。了解不同的选择是很有帮助的。
Louvain 方法的创始人能够在一个包含数亿个节点和超过十亿条边的网络上使用他们的算法,这使得这种方法非常适用于大型网络。你可以在 arxiv.org/pdf/0803.0476.pdf
阅读更多关于 Louvain 方法的内容。
该算法通过一系列的传递工作,每次传递包含两个阶段。第一阶段将不同的社区分配给网络中的每个节点。最初,每个节点都会分配一个不同的社区。接着,对每个邻居进行评估,并将节点分配到社区。第一步在无法再进行改进时结束。在第二阶段,构建一个新的网络,其中节点是第一阶段发现的社区。然后,可以重复第一阶段的结果。两个步骤不断迭代,直到找到最佳社区。
这是算法工作原理的简化描述。建议完整阅读研究论文,以更好地理解算法是如何工作的。
我们在 第三章 中简要使用了 Louvain 方法,所以如果你有注意的话,这段代码应该很熟悉。Louvain 方法已经包含在最新版本的 NetworkX 中,因此如果你使用的是最新版本的 NetworkX,就不需要使用 community
Python 库了,但你的代码会有所不同。为了保持一致,我将使用community
库的方法:
首先,让我们导入库:
import community as community_louvain
下面是一些帮助我们绘制 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()
现在,让我们使用 best_partition
函数,利用 Louvain 方法来识别最佳分区。在我的测试中,我发现 resolution=1
是理想值,但在其他网络中,你应该尝试调整这个参数:
partition = community_louvain.best_partition(G, resolution=1)
draw_partition(G, partition)
这将生成一个可视化:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_008.jpg
图 9.8 – Louvain 方法对《悲惨世界》网络的社区检测
步骤 2 中的辅助函数将根据节点所属的社区为其着色。重要的是,已经检测到独立的社区,并且每个节点的社区都用不同的颜色标识。每个节点都属于不同的分区,而这些分区就是社区。
让我们来看看 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
一旦导入了算法,你所要做的就是将它传递给你的图,然后你会得到一个社区列表:
让我们使用《悲惨世界》图来试试看:
communities = label_propagation_communities(G)
这一行将我们的图传递给标签传播算法,并将结果写入一个community
变量。在这么小的网络上,这个算法非常快速,只需要不到一秒钟的时间就能识别社区。我更喜欢将这些结果转换成列表,以提取社区节点。
我们可以像这样做到:
communities = list(communities)
communities[0]
{'Champtercier',
'Count',
'CountessDeLo',
'Cravatte',
'Geborand',
'Myriel',
'Napoleon',
'OldMan'}
在最后一行中,我们检查了第一个社区,社区 0。可视化这些社区非常简单。
我们可以将它们提取为子图,然后使用我们在本书中一直使用的相同的draw_graph
函数:
community = communities[1]
G_community = G.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
我们从输出中可以看到什么?
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_009.jpg
图 9.9 – 《悲惨世界》网络的标签传播社区检测,社区 1
这个结果看起来不错,但还不如 Louvain 方法的结果那么好。虽然它速度很快,但精度不如我预期的那么高。例如,看看Valjean左边,有一个紧密连接的社区,节点之间的连接非常密集。这个应该是一个独立的社区,而不是这个大社区的一部分。这个算法并不完美,但没有算法是完美的。然而,这个算法很快,能够扩展到大规模网络,因此它是大规模社区检测的另一种选择。
让我们再看看几个社区:
community = communities[2]
G_community = G.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
这为我们提供了以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_010.jpg
图 9.10 – 悲惨世界网络的标签传播社区检测,社区 2
这个社区看起来几乎完美。在社区中,除了最密集连接的部分外,出现一些额外的节点并不罕见。
让我们看另一个例子:
community = communities[3]
G_community = G.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
这为我们提供了以下的可视化:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_011.jpg
图 9.11 – 悲惨世界网络的标签传播社区检测,社区 3
这个社区看起来也很好。总体来说,这个算法效果很好,且速度很快。此外,设置比 Louvain 方法更容易、更快,因为你只需导入算法,传入一个图形,然后可视化结果。在易用性方面,这是我见过的最简单的算法。结果看起来不错,社区很快就被识别出来了。
但是,快速和易于使用并不足够。Louvain 更准确,并且快速且易于使用。尽管如此,这个算法仍然有其用处。
在本章开始时,我们注意到悲惨世界网络由一个大的连通分量组成,并且没有孤立点或除了大连通分量之外的小型“岛屿”社区。为了展示连通分量如何有助于识别社区,我们通过去除一些关键节点来打破这个网络。
这种方法通常并不理想。尽管节点(人、地点、事物)和边(关系)中都包含信息,但根据我的经验,通常更倾向于去掉边而不是去掉节点。
一个比我们之前做的更好的方法是,找出能导致网络分裂的最少边,这些边将是切断网络的关键。我们可以通过寻找通过最多最短路径的边来实现这一点——也就是具有最高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 方法或标签传播的计算更加耗费计算资源。因此,这个算法很快就变得不再实用,因为它变得非常慢,无法再实际应用。
然而,如果你的网络足够小,这是一个非常酷的社区检测算法,可以进行探索。它也很直观,容易向他人解释。
让我们用我们的悲惨世界图来试试看。图足够小,这个算法应该能够很快地将其分割成多个社区:
首先,导入算法:
from networkx.algorithms.community import girvan_newman
接下来,我们需要将图作为参数传递给算法。这样做时,算法将返回每次迭代分割的结果,我们可以通过将结果转换为列表来进行调查:
communities = girvan_newman(G)
communities = list(communities)
在每个社区由单个节点组成之前,算法最多可以进行多少次迭代?我们来看一下:
len(communities)
76
太棒了!我们有76
次分割迭代保存在 Python 列表中。我建议你研究不同的分割级别,找到最适合你需求的一个。可能是过程中的非常早期,前 10 次分割,或者可能稍微晚一些。这部分需要一些分析,使得这个算法更具实践性。
然而,为了推进进度,假设我们发现第十次迭代的分割得到了最好的结果。我们将第十次迭代的结果作为最终的社区分组,然后像使用 Louvain 方法和标签传播一样可视化这些社区:
communities = communities[9]
我们保留了第十次迭代的结果,删除了其他所有结果。如果我们不想丢弃这些结果,可以使用不同的变量名。
我们来看一下这些社区是什么样的,以便将它们与我们讨论的其他算法进行比较:
community = communities[0]
G_community = G.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
我们得到以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_012.jpg
图 9.12 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 0
这个子图应该很熟悉!当我们通过节点将网络分裂然后可视化连接组件时,看到的正是这个。这个算法通过切割边缘分裂网络,并成功找到了相同的社区。
我们来看另一个社区:
community = communities[1]
G_community = G.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
这将生成以下网络可视化:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_013.jpg
图 9.13 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 1
这也看起来非常好。社区中有密集连接的节点组,以及一些连接较少的节点,这并不罕见。
另一个社区:
community = communities[2]
G_community = G.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
我们将看到以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_014.jpg
图 9.14 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 2
这与上一个社区类似。我们有一个密集连接的节点组和两个只有一条边的节点。看起来很棒。
我们来看一下社区 3:
community = communities[3]
G_community = G.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
社区 3 看起来是这样的:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_015.jpg
图 9.15 – 《悲惨世界》网络的 Girvan-Newman 社区检测,社区 3
这也应该很熟悉。标签传播方法找到了相同的社区,但 Girvan-Newman 算法删除了一个额外的节点。
以及下一个:
community = communities[4]
G_community = G.subgraph(community)
draw_graph(G_community, show_names=True, node_size=5)
我们将看到以下网络:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_09_016.jpg
图 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)进行情感分析的指南和书籍,但关于如何将图数据转换成可以用于机器学习分类格式的指南和书籍却少之又少。在本章中,你将看到如何使用图数据进行机器学习分类。
对于这个练习,我创建了一个名为“找出革命者”的小游戏。与前两章一样,我们将使用 《悲惨世界》 网络,因为它包含足够多的节点和社区,足够有趣。在前面的章节中,我指出革命者社区是高度连接的。作为提醒,这就是它的样子:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_001.jpg
图 10.1 – 《悲惨世界》ABC 革命社群网络
社区中的每个成员几乎都与其他成员相互连接。没有与外部人员的连接。
网络的其他部分看起来不同。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_002.jpg
图 10.2 – 《悲惨世界》全网
即使是视觉检查也能看出,在网络的不同部分,节点的连接方式不同,结构也有所差异。在某些地方,连接类似于星形;在其他地方,连接则像网格。网络指标将为我们提供这些值,机器学习模型可以使用它们进行预测。
我们将使用这些指标来玩“找出革命者”的游戏。这会很有趣。
注意
我不会深入解释机器学习,只会简要介绍它的能力。如果你对数据科学或软件工程感兴趣,我强烈建议你花时间学习机器学习。它不仅仅适用于学者、数学家和科学家。机器学习变得越来越复杂,因此强烈建议具备数学和统计学基础,但你完全可以自由探索这一主题。
本章不会是一堂数学课。所有的工作都将通过代码完成。我将展示一个使用网络数据进行分类的例子。这不是唯一的应用场景,使用网络数据进行机器学习的应用远不止这些。我也只会展示一个模型(随机森林),而不是所有可用的模型。
我还将展示有时不正确的预测和正确的预测一样具有启发性,并且有时模型预测中也包含有用的见解。
我将展示从图数据到预测和见解的工作流程,以便你可以在自己的实验中使用这一方法。你并不需要每次都使用图神经网络(NN)。使用更简单的模型也是完全可能的,它们同样能够提供有价值的见解。
足够的免责声明。开始吧。
在我们可以使用机器学习之前,首先需要收集数据,并将其转换为模型可以使用的格式。我们不能直接将图 G 传递给随机森林并就此结束。我们可以将图的邻接矩阵和一组标签传递给随机森林,它也能工作,但我想展示一些我们可以做的特征工程。
特征工程是利用领域知识创建额外的特征(大多数人称之为列),这些特征将对我们的模型有用。例如,回顾前一节中的网络,如果我们想能够识别革命者,我们可能希望为模型提供额外的数据,如每个节点的度数(连接数)、介数中心性、紧密中心性、页面排名、聚类系数和三角形:
让我们从先构建网络开始。现在应该很容易,因为我们已经做过好几次了:
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)
我们在前面的章节中已经采取了这些步骤,但提醒一下,*《悲惨世界》*图谱带有边权重,而我不需要这些。第一行加载图谱,第二行从图谱中创建边列表,去掉边权重,第三行则从边列表重建图谱,去掉权重。
我们现在应该有一个有用的图谱。让我们来看看:
draw_graph(G)
这将生成以下图谱:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_003.jpg
图 10.3 – 《悲惨世界》图谱(不带节点名称)
看起来不错!我可以清楚地看到这个网络中有几个不同的节点簇,而其他部分的网络则更为稀疏。
那么,我们如何将这个混乱的纠结结转化为机器学习模型可以使用的东西呢?好吧,我们在前几章中已经研究过中心性和其他度量。所以我们已经有了在这里使用的基础。我将创建几个包含我所需数据的数据框,然后将它们合并成训练数据。
度数只是一个节点与其他节点连接的数量。我们首先获取这个数据:
degree_df = pd.DataFrame(G.degree)
degree_df.columns = ['person', 'degrees']
degree_df.set_index('person', inplace=True)
degree_df.head()
我们得到如下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_004.jpg
图 10.4 – 《悲惨世界》特征工程:度数
接下来我们进入下一步。
接下来,我们将计算聚类系数,它告诉我们节点周围的连接密度。值为 1.0 表示每个节点都与其他节点相连,值为 0.0 表示没有邻近节点与其他邻近节点连接。
让我们来捕捉聚类:
clustering_df = pd.DataFrame(nx.clustering(G), index=[0]).T
clustering_df.columns = ['clustering']
clustering_df.head()
这将给我们聚类的输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_005.jpg
图 10.5 – 《悲惨世界》特征工程:聚类
这告诉我们 MlleBaptistine 和 MmeMagloire 都是高度连接的社区的一部分,这意味着这两个人也认识同样的人。Napoleon 与其他人没有任何交集,CountessDeLo 也是如此。
triangle_df
是统计给定节点属于多少个三角形。如果一个节点属于许多不同的三角形,那么它与网络中的许多节点相连接:
triangle_df = pd.DataFrame(nx.triangles(G), index=[0]).T
triangle_df.columns = ['triangles']
triangle_df.head()
这将给我们带来如下结果:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_006.jpg
图 10.6 – 《悲惨世界》特征工程:三角形
这是理解节点之间连接性的一种方式。这些节点代表人物,所以它也是理解人与人之间连接性的一种方式。请注意,结果类似于但并不完全相同于聚类。
介数中心性与节点在其他节点之间的位置有关。举个例子,假设有三个人(A、B 和 C),如果 B 坐在 A 和 C 之间,那么从 A 到 C 传递的所有信息都会通过 B,使得 B 处于一个重要且有影响力的位置。这只是介数中心性有用性的一个例子。我们可以通过以下代码获取这个信息:
betw_cent_df = pd.DataFrame(nx.betweenness_centrality(G), index=[0]).T
betw_cent_df.columns = ['betw_cent']
betw_cent_df.head()
这将给我们带来以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_007.jpg
图 10.7 – 《悲惨世界》特征工程:介数中心性
接近中心性与一个给定节点与网络中所有其他节点的距离有关,具体来说是最短路径。因此,接近中心性在大规模网络中计算起来非常慢。然而,对于*《悲惨世界》*网络来说,它会表现得很好,因为这是一个非常小的网络:
close_cent_df = pd.DataFrame(nx.closeness_centrality(G), index=[0]).T
close_cent_df.columns = ['close_cent']
close_cent_df.head()
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_008.jpg
图 10.8 – 《悲惨世界》特征工程:接近中心性
最后,即使在大规模网络中,pagerank
仍然有效。因此,它被广泛用于衡量重要性:
pr_df = pd.DataFrame(nx.pagerank(G), index=[0]).T
pr_df.columns = ['pagerank']
pr_df.head()
这将给我们 图 10.9。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_009.jpg
图 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:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_010.jpg
图 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 中的每一行都能正确地合并在一起。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_011.jpg
图 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:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_012.jpg
图 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)
这将输出以下内容:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_013.jpg
图 10.13 – 《悲惨世界》特征工程:标签抽查
完美。我们有了节点,每个节点都有标签。标签为 1 表示他们是 ABC 朋友会的成员,标签为 0 表示他们不是。这样,我们的训练数据就准备好了。
对于这次练习,我的目标只是向你展示网络数据如何在机器学习(ML)中可能有所帮助,而不是深入探讨机器学习的细节。关于这个主题,有很多非常厚的书籍。这本书是关于如何将自然语言处理(NLP)和网络结合起来,理解我们周围存在的隐形联系以及它们对我们的影响。因此,我将迅速跳过关于不同模型如何工作的讨论。对于这次练习,我们将使用一个非常有用且强大的模型,它通常足够有效。这个模型叫做随机森林(Random Forest)。
随机森林(Random Forest)可以接受数值型和类别型数据作为输入。我们选择的特征对于本次练习来说应该非常合适。随机森林的设置和实验也非常简单,而且它也很容易了解模型在预测中最为有用的特征。
其他模型也可以使用。我尝试使用了k-近邻(k-nearest neighbors),并且达到了几乎相同的成功水平,我确信**逻辑回归(Logistic regression)在一些额外的预处理之后也会很好用。XGBoost和支持向量机(SVM)**也会有效。你们中的一些人可能也会想使用神经网络(NN)。请随意。我选择不使用神经网络,因为它的设置更复杂,训练时间通常更长,而且可能只能带来微小的准确度提升,这也可能只是偶然。实验不同的模型!这是学习的好方法,即使你是在学习不该做什么。
我们应该再进行几次数据检查。最重要的是,让我们检查一下训练数据中类别的平衡:
从以下代码开始:
clf_df['label'].value_counts()
…
0 66
1 11
Name: label, dtype: int64
数据不平衡,但问题不大。
让我们以百分比形式展示,这样可以让它更容易理解:
clf_df['label'].value_counts(normalize=True)
…
0 0.857143
1 0.142857
Name: label, dtype: float64
看起来我们在类别之间大约有 86/14 的比例。还不错。记住这一点,因为仅仅基于类别不平衡,模型就应该能够以大约 86%的准确率进行预测。如果它只有 86%的准确率,那它就不会是一个令人印象深刻的模型。
接下来,我们需要将数据切割成适合我们模型的数据。我们将使用特征作为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
是我们答案的数组。不要仅仅听我说,做个抽查吧。
运行以下代码:
X.head()
这将显示一个数据框(DataFrame)。
向右滚动数据框。如果你没有看到标签列,那我们就可以开始了:
y[0:5]
这将显示y
中的前五个标签。这是一个数组。我们准备好了。
最后,我们需要将数据分成训练数据和测试数据。训练数据将用于训练模型,测试数据是模型完全不知道的数据。我们不关心模型在训练数据上的准确性。记住这一点。我们不关心模型的训练数据准确率或任何性能指标。我们只关心模型在未见数据上的表现。这将告诉我们它的泛化能力以及在实际环境中的表现。是的,我知道这个模型在实际环境中不会有太大用处,但这就是我们的思路。
我们将使用scikit-learn
的train_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.3
或0.2
。如果我真的希望模型能够使用尽可能多的训练数据,并且我认为它足够好,我甚至可能尝试0.1
。但是在这个练习中,我选择了0.4
。这是我的理由。
这个函数将数据以 60/40 的比例分割,将 60%的数据放入X_train
和y_train
,其余 40%放入X_test
和y_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
,这是一个老黑客笑话。它是1337,leet,elite。将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_matrix
和classification_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)
这将生成以下矩阵。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_014.jpg
图 10.14 – 模型混淆矩阵
混淆矩阵展示了模型如何根据类别进行预测。图示很好地展示了这一点。y 轴显示的是真实标签,值为0或1,而x 轴显示的是预测标签,值为0或1。我可以看到,有 26 个字符被正确预测为不是 ABC 革命者(Friends of the ABC)成员。我们的模型正确预测了三个 ABC 革命者的成员,但也错误地预测了两个非成员为成员。我们需要深入研究这个问题!有时候,错误的预测能帮助我们发现数据中的问题,或者给我们带来一些有趣的见解。
我还发现查看分类报告极其有帮助:
report = classification_report(y_test, predictions)
print(report)
我们得到以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_015.jpg
图 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()
我们得到以下的数据框:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_016.jpg
图 10.16 – 预测检查的 DataFrame
现在,让我们创建一个掩码来查找所有标签与预测不匹配的行:
mask = check_df['label'] != check_df['prediction']
check_df[mask]
这给了我们图 10.17。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_017.jpg
图 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:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_018.jpg
图 10.18 – 特征重要性的 DataFrame
这些是数字格式的特征重要性。这展示了模型在进行预测时,认为最有用的 10 个特征。值得注意的是,角色与博苏埃和昂若拉的关系是判断一个角色是否为革命者的好指标。在网络特征中,三角形是唯一进入前 10 名的特征。其余的重要特征来自邻接矩阵。让我们通过条形图来可视化这些,以便能看到更多内容,以及每个特征的重要性程度:
import matplotlib.pyplot as plt
importance_df[0:20].plot.barh(figsize=(10,8)).invert_yaxis()
我们得到以下图表:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_10_019.jpg
图 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)并不是聚类。然而,通过嵌入,一个复杂的网络被简化为几个数字特征,机器学习将更容易使用这些特征。
在本章中,你将亲眼看到这种方法实际的样子,以及它的优缺点。这并非全是积极的。使用嵌入作为训练数据会有一些不太理想的副作用。
我将展示一本书中我们之前提到过的 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),但要注意卡拉泰社交网络,并且它可能有其用处。这个建议并不是因为我对卡拉泰社交网络有任何蔑视,而是因为我发现从模型中提取的洞察非常有启发性,几乎没有什么能让我放弃这些洞察。例如,什么特征使得一个模型能够预测机器人和人工放大效应?
可解释性的丧失意味着你将无法理解模型的行为。这绝不是一件好事。这并不是对将图分解为嵌入方法或这些方法背后的研究的贬低;只不过值得知道,某些方法可能导致模型行为完全无法解释。
如果你查看卡拉泰社交网络的网站,你可能会注意到,针对无监督机器学习的两种方法可以分为两类:识别社区和创建嵌入。无监督机器学习不仅可以为节点创建嵌入,还可以为边或整个图创建嵌入。
社区检测最容易理解。使用社区检测算法的目标是识别网络中存在的节点社区。你可以把社区看作是相互以某种方式互动的节点群体。在社交网络分析中,这被称为社区检测,因为它本质上是识别社交网络中的社区。然而,社区检测不仅仅局限于涉及人类的社交网络分析。也许可以将图形看作是一个由相互作用的事物组成的社交网络。网站之间有互动。国家和城市之间有互动。人们之间有互动。国家和城市之间有互动的社区(盟友和敌人)。网站之间有互动的社区。人们之间有互动的社区。这只是识别那些相互作用的事物群体。
我们在本书的第九章中讨论了社区检测。如果你还没读过这一章,我建议你回去阅读,深入了解它。
这里是一个示例社区,帮助你刷新记忆:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_001.jpg
图 11.1 – 来自《悲惨世界》的社区
看这个社区,我们可以看到它是紧密相连的。每个成员与社区中的其他所有成员都相互连接。其他社区的连接较为稀疏。
我喜欢把图嵌入看作是将复杂的网络转化为数学模型能够更好使用的数据格式。例如,如果你使用图的边列表或 NetworkX 图(G)与随机森林模型,什么都不会发生。模型根本无法使用输入数据。因此,为了让这些模型发挥作用,我们需要将图形分解成更易用的格式。在前一章关于监督式机器学习的内容中,我们将图转换成了这种格式的训练数据:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_002.jpg
图 11.2 – 手工制作的图形训练数据
我们还包括了一个标签,这是机器学习模型将从中学习的答案。之后,我们为每个节点附加了邻接矩阵,以便分类模型也能从网络连接中学习。
如你所见,我们很容易知道训练数据中的特征。首先,我们有一个节点的度数,然后是它的聚类、三角形的数量、它的中介中心性和接近中心性,最后是它的 PageRank 得分。
使用嵌入技术,图中的所有信息都被解构成一系列的嵌入。如果你阅读该模型背后的文章,你可以理解过程中的发生情况,但在嵌入创建之后,它实际上是不可直接解释的。这就是嵌入的样子:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_003.jpg
图 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)
这会生成以下图形。我们没有包含节点标签,因此它只会显示点和线(节点和边)。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_004.jpg
图 11.4 – 《悲惨世界》网络
这看起来符合预期。每个节点都有标签,但我们没有显示它们。
我还创建了一些带标签的训练数据。这些数据包含在本书附带的 GitHub 仓库的/data
部分:
train_data = 'data/clf_df.csv'
clf_df = pd.read_csv(train_data).set_index('index')
创建训练数据的过程稍微复杂一些,并且已经在上一章中解释过,所以请按照那些步骤手动学习如何操作。对于本章,你可以直接使用 CSV 文件来节省时间。让我们检查一下数据是否正确:
clf_df.head()
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_005.jpg
图 11.5 – 手工制作的训练数据
在本章中,只有一个模型会使用手工制作的训练数据作为输入,但我们会将标签与我们的嵌入数据一起使用。我稍后会展示如何做。
有了图和训练数据,我们就可以继续进行下去了。
在社区检测中,我们的明显目标是识别网络中存在的社区。我在第九章中解释了各种方法,社区检测。在本章中,我们将使用两种 Karate Club 算法:SCD 和 EgoNetSplitter。
对于这一章,通常来说,我倾向于选择那些能够良好扩展的模型。如果一个模型或算法仅在小型网络中有用,我会避免使用它。现实世界的网络庞大、稀疏且复杂。我不认为我见过某个扩展性差的模型比那些扩展性好的算法更优秀。在社区检测中尤为如此。最好的算法确实具有良好的扩展性。我最不喜欢的则完全不具备扩展性。
我想展示的第一个社区检测算法是 SCD。你可以在karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.community_detection.non_overlapping.scd.SCD
找到关于该模型的文档和期刊文章。
该模型声称比最准确的最先进社区检测解决方案要快得多,同时保持或甚至超过它们的质量。它还声称能够处理具有数十亿条边的图,这意味着它可以在现实世界网络中使用。它声称比 Louvain 算法表现得更好,后者是最快的社区检测算法。
这些都是大胆的声明。Louvain 在社区检测中非常有用,原因有几点。首先,它非常快速,适用于大规模网络。其次,Python 实现简单易用。因此,我们已经知道 Louvain 是快速且易于使用的。这个模型究竟有多好呢?让我们试试:
首先,确保你已经在计算机上安装了 Karate Club。你可以通过简单的pip install karateclub
来安装。
现在,让我们使用该模型。首先,从导入开始。你需要这两个:
from karateclub.community_detection.non_overlapping.scd import SCD
import numpy as np
既然我们有了这些,将图的社区分配到节点上就像 1、2、3 一样简单:
model = SCD()
model.fit(G)
clusters = model.get_memberships()
我们首先实例化 SCD,然后将图拟合到 SCD,接着获取每个节点的聚类成员关系。Karate Club 模型就是这么简单。你需要阅读文章来了解其背后的运作。
聚类是什么样的?如果我们打印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 中,以此类推。
接下来,我们将这些聚类数据塞进一个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])
然后,我们创建一个cluster
数据框:
cluster_df = pd.DataFrame({'node':nodes, 'cluster':clusters})
cluster_df.head(10)
这将给我们如下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_006.jpg
图 11.6 – SCD 聚类数据框
太好了。以这种格式呈现更容易理解。我们现在有了实际的节点和它们所属的社区。
让我们通过节点成员关系找出最大的社区:
title = 'Clusters by Node Count (SCD)'
cluster_df['cluster'].value_counts()[0:10].plot.barh(title=title).invert_yaxis()
这将给我们如下结果:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_007.jpg
图 11.7 – 按节点数划分的 SCD 社区
社区 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)
这给我们带来了以下内容:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_008.jpg
图 11.8 – SCD 社区 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)
结果如下:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_009.jpg
图 11.9 – SCD 社区 15
这是另一个高质量的社区提取。所有节点都与社区中的其他节点相连。一些节点比其他节点更为中心。
让我们再看看一个社区:
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。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_010.jpg
图 11.10 – SCD 社区 7
这是另一个高质量的社区提取。社区中的所有节点都相互连接。在这种情况下,这是一种相当令人愉悦的可视化效果,因为所有节点的连接性相同。它非常对称且美丽。
悲惨世界网络非常小,因此,自然地,SCD 模型几乎能立即进行训练。
我喜欢这种方法的一个原因是,它的设置比我在第九章中解释的其他方法简单。我可以在几乎不需要任何代码的情况下,从图形直接生成社区。事实上,如果它真的能扩展到具有数十亿条边的网络,那将是不可思议的。它快速、简洁且实用。
我们将要测试的下一个社区检测模型叫做 EgoNetSplitter。你可以在这里了解它:karateclub.readthedocs.io/en/latest/modules/root.html#karateclub.community_detection.overlapping.ego_splitter.EgoNetSplitter
。
在 Jupyter 中,如果你按Shift + Tab进入模型实例化代码,你可以查看相关信息:
工具首先创建节点的自我网络。然后,使用 Louvain 方法创建一个人物图,并对其进行聚类。生成的重叠聚类成员关系以字典形式存储。
所以,这个模型创建自我网络,然后使用 Louvain 进行聚类,最后将重叠的成员关系存储为字典。这是一个有趣的方式,与其他方法不同,所以我觉得测试一下它的表现会很有意思。步骤与 SCD 的略有不同:
首先,让我们先把模型搭建好:
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
这将得到我们的聚类。接下来,创建我们的cluster
数据框并进行可视化的代码与 SCD 相同:
cluster_df = pd.DataFrame({'node':nodes, 'cluster':clusters})
让我们通过计数来检查社区成员:
title = 'Clusters by Node Count (EgoNetSplitter)'
cluster_df['cluster'].value_counts()[0:10].plot.barh(title=title).invert_yaxis()
我们得到以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_011.jpg
图 11.11 – 按节点数量划分的 EgoNetSplitter 社区
结果与 SCD 已经看起来不同了。这应该很有趣。
让我们来看看有什么不同。聚类 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)
这将绘制我们的自我网络。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_012.jpg
图 11.12 – EgoNetSplitter 社区 7
我不喜欢这样。我认为左侧的节点不应该和右侧与密集连接节点相连的节点属于同一个社区。就我个人而言,我觉得这不像 SCD 的结果那样有用。
让我们看看下一个人口最多的聚类:
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 显示了结果输出。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_013.jpg
图 11.13 – EgoNetSplitter 社区 1
再次出现类似的情况,其中一个节点被包含在网络中,但实际上它不应该在其中。MotherPlutarch 可能与 Mabeuf 连接,但她与社区中的其他人没有任何关系。
让我们最后来看一下下一个社区:
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)
代码产生了以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_014.jpg
图 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,你可以在 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
或主题)下表现如何:
现在,让我们使用之前用过的手工制作的训练数据:
from karateclub.node_embedding.attributed.feathernode import FeatherNode
model = FeatherNode()
model.fit(G, clf_df)
embeddings = model.get_embedding()
首先,我们导入模型,然后实例化它。在model.fit
这一行,注意到我们同时传入了G
和clf_df
。后者是我们手动创建的训练数据。与其他模型不同,我们只传入G
。对我来说,这是非常有趣的,因为它似乎让模型能够基于其他上下文数据学习更多关于网络的内容。
让我们可视化这些嵌入,以便看看模型是如何工作的:
title = 'Les Miserables Character Similarity (FeatherNode)'
draw_clustering(embeddings, nodes, title)
我们得到以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_03_007.jpg
图 11.15 – FEATHER 嵌入
这很有趣。我们可以看到有几个节点出现在一起。由于这是一个交互式可视化,我们可以检查其中任何一个。如果我们放大左下角的聚类,我们可以看到以下内容:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_016.jpg
图 11.16 – FEATHER 嵌入放大图
由于重叠,很难阅读,但Feuilly出现在左下角,靠近Prouvaire。
让我们检查一下他们的自我网络,看看有哪些相似之处:
node = 'Feuilly'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)
这产生了图 11**.17:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_017.jpg
图 11.17 – Feuilly 自我网络
现在,让我们检查一下 Prouvaire 的自我网络:
node = 'Prouvaire'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)
这输出了图 11**.18。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_018.jpg
图 11.18 – Prouvaire 自我网络
很好。第一个观察结果是,它们都是彼此自我网络的一部分,并且也是彼此社区的一部分。第二,它们的节点连接性相当强。在两个自我网络中,两个节点都显示出相当强的连接性,并且都是密集连接社区的一部分。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_019.jpg
图 11.19 – FEATHER 嵌入放大图
让我们检查一下MotherInnocent和MmeMagloire的自我网络。首先是MotherInnocent:
node = 'MotherInnocent'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)
图 11*.20*显示了输出。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_020.jpg
图 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*显示了结果。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_021.jpg
图 11.21 – MmeMagloire 自我网络
MotherInnocent有两条边,MmeMagloire有三条。它们的自我网络相当小。这些相似性被 FEATHER 捕捉并转化为嵌入。
但实际的嵌入是什么样的呢?
eb_df = pd.DataFrame(embeddings, index=nodes)
eb_df['label'] = clf_df['label']
eb_df.head(10)
这将生成以下数据框。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_022.jpg
图 11.22 – FEATHER 嵌入数据框
图形被转化为 1,750 维的嵌入。在这种格式下,你可以将它们看作列或特征。一个简单的网络被转化为 1,750 列,这对于这么小的网络来说数据量相当大。我们在处理其他模型(如 FEATHER)时,注意这些模型所创建的维度数量。
这些嵌入对分类很有用,所以我们就做这个。我将直接把数据丢给分类模型,并希望能够得到好结果。这种做法除了用于简单实验之外从来不是一个好主意,但这正是我们要做的。我鼓励你深入探索你感兴趣的任何模型。
前面的代码已经添加了label
字段,但我们还需要创建我们的X
和y
数据来进行分类:
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
是正确答案。这是我们的“识别革命者”训练数据。标记为革命者的节点其y
为1
,其余节点的y
为0
。
让我们训练一个随机森林模型,因为我想向你展示一些关于可解释性的东西:
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()
我们得到这个输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_023.jpg
图 11.23 – FEATHER 嵌入特征重要性
太棒了!发现 1531 嵌入比 1134 稍微有用,但这两个嵌入都比其他嵌入要有用得多!太棒了!这是完全丧失了解释性,但这些嵌入确实有效。如果你只是想从图到机器学习,这种方法可以使用,但无论使用哪个模型进行预测,最终都会得到一个黑箱模型。
好了,对于接下来的模型,我将加快速度。我们将重复使用很多代码,我只是会减少可视化部分,并减少代码量,以便本章不会变成 100 页长。
我们接下来要看的算法是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 也会创建节点嵌入:
让我们使用模型,一次性进行可视化:
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)
以下图表是结果:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_024.jpg
图 11.24 – NodeSketch 嵌入
首先,Eponine:
node = 'Eponine'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)
你可以在 图 11*.25* 中看到结果。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_025.jpg
图 11.25 – Eponine 自我网络
接下来,Brujon:
node = 'Brujon'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)
如 图 11*.26* 所示。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_026.jpg
图 11.26 – Brujon 自我网络
经检查,自我网络看起来差异很大,但这两个节点似乎具有相同数量的连接,并且它们都是一个连接良好的社区的一部分。我很满意这两个节点在结构和位置上非常相似。两个节点也是同一个社区的一部分。
嵌入的样子是什么?
eb_df = pd.DataFrame(embeddings, index=nodes)
eb_df['label'] = clf_df['label']
eb_df.head(10)
这将展示我们的数据框。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_027.jpg
图 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% 准确率的模型。但这仍然显示出该模型能够使用这些嵌入。
它发现了哪些重要特征?
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*。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_028.jpg
图 11.28 – NodeSketch 嵌入特征重要性
很好。如前所示,使用这些嵌入将随机森林变成了一个黑盒模型,我们无法从中获取任何模型可解释性。我们知道模型发现特征 24 和 23 最为有用,但我们不知道为什么。之后我不会再展示特征重要性了,你明白了。
这是一个很酷的模型,它默认创建比 FEATHER 更简单的嵌入。随机森林在这两种模型的嵌入上表现得都很好,我们无法在没有更多实验的情况下说哪个更好,而这超出了本章的范围。祝你在实验中玩得开心!
接下来是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。该过程使用基于正则化邻接矩阵的平滑方法,并在正交化的随机正态生成基节点嵌入矩阵上进行操作。
再次,我们来一次性生成嵌入并进行可视化:
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)
输出的图表如下:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_029.jpg
图 11.29 – RandNE 嵌入
你可以立即看到,这个散点图与 FEATHER 和 NodeSketch 的都很不同。让我们来看看Marius
和MotherPlutarch
这两个已被认为是相似的节点的自我网络:
node = 'Marius'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)
我们得到了一个网络输出:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_030.jpg
图 11.30 – Marius 自我网络
接下来是MotherPlutarch:
node = 'MotherPlutarch'
G_ego = nx.ego_graph(G_named, node)
draw_graph(G_ego, show_names=True, node_size=3)
网络如下所示:
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_031.jpg
图 11.31 – MotherPlutarch 自我网络
哇,这些自我网络差异如此之大,节点也一样。Marius是一个连接良好的节点,而MotherPlutarch与另一个节点只有一条边。这是两个非常不同的节点,而嵌入结果却显示它们是相似的。不过,这可能是由于散点图可视化中的 PCA 步骤导致的,因此请不要仅凭这一例子就对 RandNE 做出过快判断。查看其他相似节点。我将把这个留给你,作为你自己的练习和学习。
嵌入结果是什么样子的?
eb_df = pd.DataFrame(embeddings, index=nodes)
eb_df['label'] = clf_df['label']
eb_df.head(10)
这将显示我们的嵌入。
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/net-sci-py/img/B17105_11_032.jpg
图 11.32 – RandNE 嵌入数据框
最终的嵌入是 77 个特征,因此它默认创建的嵌入比 FEATHER 更简单。相比之下,NodeSketch 创建了 32 个特征。
随机森林能够多好地利用这些嵌入?
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 是我们可以使用的另一个模型。你可以在这里找到文档: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 是我们可以使用的另一个可能模型: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 或图嵌入之前已经做过科学探索。否则,这就是数据炼金术,而不是数据科学。
不过,并非全是坏事。如果你发现某种类型的嵌入式方法是可靠且高质量的,那么你确实可以通过它快速进行分类和聚类,只要你不需要可解释性。你可以在几分钟内从图数据到分类或聚类,而不是几个小时。这与从图特征手工制作训练数据相比,是一个巨大的加速。因此,如果你只是想看看图数据是否能用于预测某些东西,这确实是构建原型的一个捷径。
这全是优缺点和使用案例。如果你需要可解释性,在这里是得不到的。如果你需要快速推进,这里可以提供帮助。而且,很有可能在了解更多关于嵌入的内容之后,你会从中获得一些见解。我也确实见证过这一点。有时候,你能够找到一些见解——只是需要间接的方法才能得到它们,希望本书能给你一些启发。
我简直不敢相信我们已经走到这一步。在本书开始时,这看起来像是一个不可能完成的任务,但现在我们已经做到了。为了完成本章的实践练习,我们使用了前几章学到的内容。我希望我已经向你展示了网络如何有用,以及如何与它们一起工作。
在本书的开头,我决定写一本实践性强的书,内容侧重于代码,而非数学。有很多网络分析书籍,侧重于数学,但很少展示实际的实现,甚至没有。我希望本书能够有效填补这一空白,为程序员提供一项新技能,并向社会科学家展示通过编程方式提升他们的网络分析水平。非常感谢你阅读这本书!