图机器学习(1)——图论基础
图机器学习(1)——图论基础
- 0. 前言
- 1. 使用 networkx 初识图结构
- 1.1 图论基础
- 1.2 图的基础属性
- 2. 图的类型
- 2.1 有向图
- 2.2 多重图
- 2.3 加权图
- 2.4 二部图
- 3. 图的表示
- 3.1 邻接矩阵
- 3.2 边列表
- 小结
0. 前言
图 (Graph
) 是一种描述实体间关系的数学结构,几乎在各个领域都有应用。例如,社交网络就是图,其中用户之间的连接取决于一个用户是否“关注”另一个用户;图也可以用于表示地图,其中城市通过道路相连接;生物结构、网页链接乃至神经疾病的演变过程均可由图来描述。
图论,即对图的研究,一直以来广受关注,包含了大量算法、性质分析方法和数学模型,以解析复杂系统的行为特征。
本节将介绍图数据结构的核心概念,通过理论阐述与实例演示相结合的方式,理解基础理论用以实践应用。重点介绍用于创建、操作和研究复杂网络结构与功能的 Python
库。
1. 使用 networkx 初识图结构
本节将介绍图论基础概念,并通过 Python
代码片段(基于 networkx
库)实现理论与实践的结合。
1.1 图论基础
一个简单的无向图(或简称图,Graph
) GGG 定义为二元组 G=(V,E)G=(V,E)G=(V,E),其中 V={v1,v2,...,vn}V=\{v_1, v_2, ..., v_n\}V={v1,v2,...,vn} 表示节点( node
,或称顶点,vertice
)集合, E={(v1,v2),...,(vn,vm)}E=\{(v_1,v_2), ..., (v_n,v_m)\}E={(v1,v2),...,(vn,vm)} 表示边( edge
,或称连接,link
)集合,每条边由 VVV 中两个节点构成的无序对组成。
需要注意的是,由于E中的元素均为无序二元组,因此每条边之间没有顺序关系。具体来说,{v1,v2}\{v_1,v_2\}{v1,v2} 与 {v2,v1}\{v_2,v_1\}{v2,v1} 表示同一条边。
接下来,我们介绍图和节点的基本属性定义:
- 图的阶 (
order
) 是其顶点的数量 ∣V∣|V|∣V∣,图的大小是其边的数量 ∣E∣|E|∣E∣ - 顶点的度 (
degree
) 是与该顶点相邻的边的数量 - 图 GGG 中一个顶点 vvv 的邻居 (
neighbor
) 是由所有与 vvv 相邻的顶点构成的子集 - 图 GGG 中顶点 vvv 的邻域图(
neighborhood graph
,也称为自我图,ego graph
)是一个由与 vvv 相邻的顶点及所有连接这些顶点的边组成的子图
简单的无向图如下所示:
根据这种表示方法,由于没有方向,从 Beijing
到 Baoding
的边与从 Baoding
到 Beijing
的边是相等的。因此,可以在这两个方向上自由移动。分析上图中所示图结构的性质,可以看到它的阶数和大小都等于 4
(总共有四个顶点和四条边)。Beijing
和 Tangshan
顶点的度为 2
,Baoding
的度为 3
,Langfang
的度为 1
。每个节点的邻居如下所示:
- Tangshan = {Beijing, Baoding}
- Baoding = {Beijing, Langfang, Tangshan}
- Beijing = {Baoding, Tangshan}
- Langfang = {Baoding}
在 networkx
中可通过以下代码实现该图结构:
import networkx as nx
G = nx.Graph()
V = {'Beijing', 'Langfang', 'Tangshan', 'Baoding'}
E = [('Beijing','Baoding'), ('Beijing','Tangshan'), ('Baoding','Langfang'), ('Baoding','Tangshan')]
G.add_nodes_from(V)
G.add_edges_from(E)
默认情况下,nx.Graph()
生成无向图,因此无需为每条边指定双向关系。networkx
允许节点为任何可哈希对象,包括字符串、类对象甚至其他 networkx
图对象。接下来,计算已生成图的基础属性。
1.2 图的基础属性
(1) 获取图中所有节点和边:
print(f"V = {G.nodes}")
print(f"E = {G.edges}")
输出结果如下所示:
V = ['Tangshan', 'Baoding', 'Beijing', 'Langfang']
E = [('Tangshan', 'Beijing'), ('Tangshan', 'Baoding'), ('Baoding', 'Beijing'), ('Baoding', 'Langfang')]
(2) 计算图的阶数、大小,以及每个节点的度和邻域:
print(f"Graph Order: {G.number_of_nodes()}")
print(f"Graph Size: {G.number_of_edges()}")
print(f"Degree for nodes: { {v: G.degree(v) for v in G.nodes} }")
print(f"Neighbors for nodes: { {v: list(G.neighbors(v)) for v in G.nodes} }")
输出结果如下所示:
Graph Order: 4
Graph Size: 4
Degree for nodes: {'Tangshan': 2, 'Baoding': 3, 'Beijing': 2, 'Langfang': 1}
Neighbors for nodes: {'Tangshan': ['Beijing', 'Baoding'], 'Baoding': ['Beijing', 'Langfang', 'Tangshan'], 'Beijing': ['Baoding', 'Tangshan'], 'Langfang': ['Baoding']}
(3) 计算特定节点的邻域图 (ego graph
):
ego_graph_milan = nx.ego_graph(G, "Baoding")
print(f"Nodes: {ego_graph_milan.nodes}")
print(f"Edges: {ego_graph_milan.edges}")
输出结果如下所示:
Nodes: ['Tangshan', 'Baoding', 'Beijing', 'Langfang']
Edges: [('Tangshan', 'Beijing'), ('Tangshan', 'Baoding'), ('Baoding', 'Beijing'), ('Baoding', 'Langfang')]
(4) 通过添加新节点或边修改原始图:
new_nodes = {'Tianjin', 'Chengde'}
new_edges = [('Tianjin','Langfang'), ('Chengde','Beijing')]
G.add_nodes_from(new_nodes)
G.add_edges_from(new_edges)
print(f"V = {G.nodes}")
print(f"E = {G.edges}")
输出结果如下所示:
V = ['Tangshan', 'Baoding', 'Beijing', 'Langfang', 'Chengde', 'Tianjin']
E = [('Tangshan', 'Beijing'), ('Tangshan', 'Baoding'), ('Baoding', 'Beijing'), ('Baoding', 'Langfang'), ('Beijing', 'Chengde'), ('Langfang', 'Tianjin')]
(5) 删除节点(关联边会自动从边列表中删除):
node_remove = {'Tianjin', 'Chengde'}
G.remove_nodes_from(node_remove)
print(f"V = {G.nodes}")
print(f"E = {G.edges}")
输出结果如下所示:
V = ['Tangshan', 'Baoding', 'Beijing', 'Langfang']
E = [('Tangshan', 'Beijing'), ('Tangshan', 'Baoding'), ('Baoding', 'Beijing'), ('Baoding', 'Langfang')]
(6) 删除指定边:
node_edges = [('Tangshan','Baoding'), ('Baoding','Beijing')]
G.remove_edges_from(node_edges)
print(f"V = {G.nodes}")
print(f"E = {G.edges}")
输出结果如下所示:
V = ['Tangshan', 'Baoding', 'Beijing', 'Langfang']
E = [('Tangshan', 'Beijing'), ('Baoding', 'Langfang')]
networkx
库还支持单节点/边删除操作:
- 删除单个节点:
G.remove_node('Langfang')
- 删除单条边:
G.remove_edge('Baoding','Tangshan')
2. 图的类型
在上一小节中,我们介绍了如何创建和修改简单无向图。接下来,我们将介绍如何通过有向图、加权图和多重图来扩展基础数据结构,以封装更多信息。
2.1 有向图
有向图 (Digraphs
) GGG 定义为二元组 G=(V,E)G = (V, E)G=(V,E),其中 V={v1,v2,...,vn}V=\{v_1, v_2, ..., v_n\}V={v1,v2,...,vn} 是节点集合, E={(v1,v2),...,(vn,vm)}E=\{(v_1,v_2), ..., (v_n,v_m)\}E={(v1,v2),...,(vn,vm)} 是表示连接 VVV 中两个节点之间关系的有序对的集合。
由于 EEE 中的每个元素(边)都是有序对,具有明确的方向区分。边 (Vw,Vk)(V_w, V_k)(Vw,Vk) 表示节点 VwV_wVw 指向 VkV_kVk 的连接关系,这与 (Vk,Vw)(V_k, V_w)(Vk,Vw) 不同——后者表示的是节点 VkV_kVk 指向 VwV_wVw 的连接。起始节点 VwV_wVw 称为头 (head
),结束节点 VkV_kVk 称为尾 (tail
)。由于存在边的方向,节点度的定义需要扩展。
入度和出度
对于一个节点 vvv,以 vvv 为尾节点的边数称为入度( indegree
,用 deg−(v)deg^-(v)deg−(v) 表示),而以 vvv 为头节点的边数称为出度( outdegree
,用deg+(v)deg^+(v)deg+(v) 表示)。
如下所示是一个典型的有向图:
方向通过箭头表示——例如,Baoding-> Beijing
表示从 Baoding
到 Beijing
。Beijing
的入度 deg−(v)=2deg^-(v)= 2deg−(v)=2,出度 deg+(v)=0deg^+(v) = 0deg+(v)=0,Tangshan
的入度 deg−(v)=0deg^-(v) = 0deg−(v)=0,出度 deg+(v)=2deg^+(v)= 2deg+(v)=2,Baoding
的入度 deg−(v)=1deg^-(v)= 1deg−(v)=1,出度 deg+(v)=2deg^+(v)= 2deg+(v)=2,Langfang
的入度 deg−(v)=1deg^-(v)= 1deg−(v)=1,出度 deg+(v)=0deg^+(v)= 0deg+(v)=0。
在 networkx
中可通过以下代码实现该有向图结构:
G = nx.DiGraph()
V = {'Beijing', 'Tangshan', 'Baoding', 'Langfang'}
E = [('Baoding','Beijing'), ('Tangshan','Baoding'), ('Tangshan','Beijing'), ('Baoding','Langfang')]
G.add_nodes_from(V)
G.add_edges_from(E)
与简单无向图类似,唯一的区别在于用于实例化对象的 networkx
类。对于有向图,使用 nx.DiGraph()
类。
计算入度和出度:
print(f"Indegree for nodes: { {v: G.in_degree(v) for v in G.nodes} }")
print(f"Outdegree for nodes: { {v: G.out_degree(v) for v in G.nodes} }")
输出结果如下所示:
Indegree for nodes: {'Tangshan': 0, 'Baoding': 1, 'Beijing': 2, 'Langfang': 1}
Outdegree for nodes: {'Tangshan': 2, 'Baoding': 2, 'Beijing': 0, 'Langfang': 0}
对于有向图,同样可以使用 G.add_nodes_from()
,G.add_edges_from()
,G.remove_nodes_from()
和 G.remove_edges_from()
函数来修改给定图 GGG。
2.2 多重图
多重图 (multigraph
) 是图定义的推广,允许同一对节点间存在多条边。多重图 GGG 定义为 G=(V,E)G = (V, E)G=(V,E),其中 VVV 是节点集,EEE 是边的多重集(允许每个元素有多个实例)。
如果 EEE 是有序对的多重集,则该多重图称为有向多重图;否则,如果 EEE 是无序二元组的多重集,则称为无向多重图。
如下所示是一个典型的有向多重图:
使用 networkx
创建有向和无向多重图:
directed_multi_graph = nx.MultiDiGraph()
undirected_multi_graph = nx.MultiGraph()
V = {'Baoding', 'Beijing', 'Langfang', 'Tangshan'}
E = [('Langfang','Baoding'), ('Langfang','Baoding'), ('Beijing','Langfang'), ('Beijing','Baoding'), ('Langfang','Tangshan'), ('Langfang','Tangshan')]
directed_multi_graph.add_nodes_from(V)
undirected_multi_graph.add_nodes_from(V)
directed_multi_graph.add_edges_from(E)
undirected_multi_graph.add_edges_from(E)
有向多重图和无向多重图之间的唯一区别在于实例化对象的 networkx
类不同:nx.MultiDiGraph()
用于创建有向多重图,而 nx.MultiGraph()
用于构建无向多重图。用于添加节点和边的函数对于这两个对象是相同的。
2.3 加权图
接下来我们介绍有向、无向和多重加权图。
边加权图 (edge-weighted graph
,或简称为加权图) GGG 定义为 G=(V,E,w)G = (V, E, w)G=(V,E,w),其中 VVV 是节点集,EEE 是边集,w:E→Rw:E\rightarrow \mathbb Rw:E→R 是加权函数,它为每条边 e∈Ee\in Ee∈E 分配一个权重,表示为实数。
节点加权图 (node-weighted graph
) GGG 定义为 G=(V,E,w)G = (V, E, w)G=(V,E,w),其中 VVV 是节点集,EEE 是边集,w:V→Rw:V\rightarrow \mathbb Rw:V→R 是加权函数,它为每个节点 v∈Vv\in Vv∈V 分配一个权重,表示为实数。
需要注意的是:
- 如果 EEE 是有序对的集合,则称为有向加权图 (
directed weighted graph
) - 如果 EEE 是无序二元组的集合,则称为无向加权图 (
undirected weighted graph
) - 如果 EEE 是多重集,则称为加权多重图(
weighted multigraph
,或无向加权多重图) - 如果 EEE 是有序对的多重集,则称为有向加权多重图 (
directed weighted multigraph
)
如下所示是一个典型的有向加权图:
可以看到,图中加权边的存在有助于为数据结构添加有用的信息。我们可以将边的权重视为从一个节点到另一个节点的“成本”。例如,从 Baoding
到 Beijing
的“成本”是 12
,而从 Tangshan
到 Beijing
的“成本”是 4
。
使用 networkx
创建有向加权图:
G = nx.DiGraph()
V = {'Beijing', 'Baoding', 'Langfang', 'Tangshan'}
E = [('Baoding','Beijing', 12), ('Baoding','Langfang', 9), ('Tangshan','Beijing', 4), ('Tangshan','Baoding', 20)]
G.add_nodes_from(V)
G.add_weighted_edges_from(E)
2.4 二部图
接下来,介绍另一种类型的图:多部图 (multipartite graph
)。二部图、三部图以及更一般的 k
部图,是图的节点可以分为两个、三个或 k
个互斥节点集的图。边只允许在不同集合的节点之间连接,而不允许在属于同一集合的节点之间连接。在大多数情况下,属于不同集合的节点也具有特定的类型属性。例如,推荐系统中的用户-商品属于典型的二部图。
(1) 使用 networkx
创建二部图:
import pandas as pd
import numpy as np
n_nodes = 10
n_edges = 12
bottom_nodes = [ith for ith in range(n_nodes) if ith % 2 ==0]
top_nodes = [ith for ith in range(n_nodes) if ith % 2 ==1]
iter_edges = zip( np.random.choice(bottom_nodes, n_edges), np.random.choice(top_nodes, n_edges))
edges = pd.DataFrame([{"source": a, "target": b} for a, b in iter_edges])
B = nx.Graph()
B.add_nodes_from(bottom_nodes, bipartite=0)
B.add_nodes_from(top_nodes, bipartite=1)
B.add_edges_from([tuple(x) for x in edges.values])
(2) 可以使用 networkx
的 bipartite_layout
函数方便地绘制二部图:
from networkx.drawing.layout import bipartite_layout
pos = bipartite_layout(B, bottom_nodes)
nx.draw_networkx(B, pos=pos)
输出结果如下所示:
3. 图的表示
如前所述,使用 networkx
时我们可以直接通过节点和边对象来定义和操作图结构。但在应用场景中,这种表示方式可能不够高效。在本节中,我们将介绍两种图数据结构的紧凑表示方法:邻接矩阵和边列表。
3.1 邻接矩阵
对于图 G=(V,E)G=(V,E)G=(V,E),其邻接矩阵 (adjacency matrix
) MMM 是一个 ∣V∣×∣V∣|V|×|V|∣V∣×∣V∣ 的方阵,其中当存在从节点 iii 指向节点 jjj 的边时,元素 MijM_{ij}Mij 为 1
;当不存在对应边时,元素 MijM_{ij}Mij 为 0
。以下展示了不同类型图结构对应的邻接矩阵示例:
可以看到,无向图的邻接矩阵是对称矩阵,因为边没有定义方向。相反,由于边的方向存在约束,有向图的邻接矩阵通常并不对称。对于多重图,矩阵中的值可以大于 1
,因为多个边可以用来连接相同的一对节点。对于加权图,矩阵中元素的值等于对应边的权重值。
在 networkx
中,给定图的邻接矩阵可以通过以下方式计算:
nx.to_pandas_adjacency(G)
输出结果如下所示:
Baoding Beijing Langfang Tangshan
Baoding 0.0 12.0 9.0 0.0
Beijing 0.0 0.0 0.0 0.0
Langfang 0.0 0.0 0.0 0.0
Tangshan 20.0 4.0 0.0 0.0
3.2 边列表
与邻接矩阵一样,边列表是另一种表示图的紧凑方式,其核心思想是将图表示为边的列表。
图 G=(V,E)G=(V,E)G=(V,E) 的边列表 LLL 是一个大小为 ∣E∣|E|∣E∣ 的矩阵,其元素 LiL_iLi 是一个表示边 iii 的起始节点和终止节点的元组。以下展示了不同类型图的边列表示例:
使用 networkx
中计算简单无向图 GGG 的边列表:
print(nx.to_pandas_edgelist(G))
输出结果如下所示:
source target weight
0 Baoding Beijing 12
1 Baoding Langfang 9
2 Tangshan Beijing 4
3 Tangshan Baoding 20
除上述表示方法外,networkx
还支持其他图结构表示方式,如 nx.to_dict_of_dicts(G)
和 nx.to_numpy_array(G)
等。
小结
本文介绍了图数据结构及其在 Python
中的实现方法,主要围绕 networkx
库展开。首先阐述了图论的基本概念,包括图的定义(节点集合 VVV 和边集合 EEE 构成的二元组 G=(V,E)G=(V,E)G=(V,E) )及其基础属性(阶、度、邻居、邻域图等),并通过城市连接示例演示了 networkx
的代码实现(如 nx.Graph()
的创建、节点/边的增删操作)。
其次,详细探讨了图的多种变体:
- 有向图 (
nx.DiGraph
) 通过有序边区分方向,引入入度/出度概念 - 多重图 (
nx.MultiGraph
/nx.MultiDiGraph
) 支持节点间多边连接 - 加权图通过
add_weighted_edges_from
为边赋予权重,扩展了成本建模能力 - 二部图以互斥节点集和跨集合边为特征,适用于推荐系统等场景
最后,对比了图的两种高效表示方法:
- 邻接矩阵 (
to_pandas_adjacency
) 以矩阵形式反映节点连接关系 - 边列表 (
to_pandas_edgelist
) 直接存储边信息,适合稀疏图