交通运输地理空间分析 - 第 2 节课:复杂网络及其指标的计算

这份文件是我上课的笔记。

严格来讲,因为笔记里面有一半是老师的课件里面的内容,我只是加上了自己的理解和解释在里面,所以这份笔记的内容创作有一半是我老师完成的。

  • 这门课程最主要是关于一些地理空间分析的实践方法。
  • 比起理论,更关心具体怎么去实现的问题。
  • 我自己结合了平常自己经常使用的 Python 来解释和解决问题
  • 基于 Jupyter Notebook

在这里分享出来,希望能够帮助到一些人。

往期交通地理空间分析笔记回顾:


在上一节课当中,我们介绍了一连串基本的图论概念,包括节点和边的概念、如何用矩阵表示图等等,也给出了一些简单的图论方法的 python 实现。我们介绍了一些关于网络的概念,并且进入了复杂网络概念的讲述。

现实生活中存在许多适用复杂网络的模型:Web,Internet 网络,电影演员合作网络,科学家合作网络,论文引用网络,电话呼叫网络,语言学网络,电力网络,经济网络,交通网络,疾病传播,神经网络,人类性关系网络,蛋白质互作用网络,蛋白质折叠关系网络……

那么这一节课程,我们会继续介绍复杂网络的一些概念。

在这份笔记当中,为了熟悉一些实践性的操作方法,我会用 Python 做一些示例,包括绘图和一些数据的处理方法。

下面是这份笔记用到的库:

import numpy as np 
import networkx as nx
import matplotlib.pyplot as plt

在这里我想介绍一下我用来处理网络规划问题的工具包 NetWorkX。以下是工具包的简单介绍:

NetworkX

NetworkX is a Python package for the creation, manipulation, and study of the structure, dynamics, and functions of complex networks.

See https://networkx.org for complete documentation.

NetWorkX 是 Python 的一个包,用于构建和操作复杂的图结构,提供分析图的算法。这个包可以说就是专门针对复杂网络图的,可以帮助我们实现网络的处理、分析、运算和可视化的各种问题。

  • 研究社会、生物和基础设施网络结构和动态的工具;
  • 一种适用于多种应用的标准编程接口和图形实现;
  • 为协作性、多学科项目提供快速发展环境;
  • 与现有的数值算法和 C、C++ 和 FORTRAN 代码的接口;
  • 能够轻松处理大型非标准数据集。

因为原版的技术文档经常被墙,所以我推荐大家到开源地理空间协会官网的镜像文档站点去阅读技术文档:NetWorkX - OSGeo 开源空间地理协会

下面是这份笔记的绘图设置:

plt.rcParams.update(
    {
    
    
        # "figure.figsize":(8, 6), 
        "font.sans-serif":'SimHei', 
        "figure.dpi":100, 
        "axes.unicode_minus":False, 
        "image.cmap":'viridis'
    }
)

网络的衡量指标

首先,我希望大家能够明白一个概念,就是说网络它是分好坏的,网络有好坏之分,因为网络是我们抽象出来的概念模型,而在这个概念模型的背后,是大量的工程建设的实践性问题。

打个比方说,假如我们现在要在一个村子里面架设光缆,那么我们就希望光缆的花销费用尽可能少,而又能实现稳定的联通功能。那么,假设我们把全村的光缆抽象成为一个网络模型,我们就希望这个网络模型同时兼具良好的连通性和较短的总长度。可是问题在于,连通性和总长度该如何定义呢?

我们制订了下面几个指标。注意:这些指标有些是针对网络的,而有些则是针对网络中的点,在具体理解的时候要加以甄别。

在这种情况下,要衡量一个网络的好坏,我们就要制定一些指标来加以衡量。

我们可以用下面这个网络图作一个示例:

w = np.array(
    [
        [0, 2, 4, 5, 0, 0, 0],
        [2, 0, 0, 2, 0, 7, 0],
        [4, 0, 0, 1, 4, 0, 0],
        [5, 2, 1, 0, 3, 4, 0],
        [0, 0, 4, 3, 0, 1, 7],
        [0, 7, 0, 4, 1, 0, 5],
        [0, 0, 0, 0, 7, 5, 0]
     ]
)
G = nx.from_numpy_array(w, create_using = nx.Graph())
G = nx.convert_node_labels_to_integers(G, first_label = 1)
weights = nx.get_edge_attributes(G, "weight")

我们把这个示例的网络画一下

pos = nx.spring_layout(G)
nx.draw_networkx(G, pos, with_labels = True)
nx.draw_networkx_edge_labels(G, pos, edge_labels = weights)
{(1, 2): Text(0.7243447156107068, 0.051857859258879646, '2'),
 (1, 3): Text(0.36250316341599337, -0.7783934189391077, '4'),
 (1, 4): Text(0.506341301696545, -0.3038377920385775, '5'),
 (2, 4): Text(0.4686328641327283, 0.3048069050985177, '2'),
 (2, 6): Text(0.3027391038445652, 0.6863178638748365, '7'),
 (3, 4): Text(0.1067913119380149, -0.5254443730994698, '1'),
 (3, 5): Text(-0.34659661811461046, -0.6187230923540853, '4'),
 (4, 5): Text(-0.20275847983405879, -0.14416746545355502, '3'),
 (4, 6): Text(0.08473568993040345, 0.33062221257737934, '4'),
 (5, 6): Text(-0.3686522401222219, 0.23734349332276378, '1'),
 (5, 7): Text(-0.7905569923698414, 0.11751992816374057, '7'),
 (6, 7): Text(-0.5030628226053793, 0.5923096061946749, '5')}

在这里插入图片描述

度分布

一个顶点的度指的是与此顶点连接的边的数量。 在有向网络当中,分为 入度出度

研究包括:度的分布及其特征、度的相关性。

度值的分布是网络的重要几何性质

  • 规则网络 各顶点度值相同,因而符合 Δ \Delta Δ 分布。
  • 随机网络 符合泊松分布。

在 NetWorkX 里面有下面两个函数可以计算各个节点的度

用途 函数
获取所有节点的度 G.degree()
获取某个节点的出度 G.out_degree(i)
G.degree()
DegreeView({1: 3, 2: 3, 3: 3, 4: 5, 5: 4, 6: 4, 7: 2})

复杂网络理论一般研究的问题包括:度及其分布特征,度的相关性…… (这些在这里只是简单地提及一下,不作深入的讨论)

度值的分布特征是网络的重要几何性质。

  • 规则网络各顶点度值相同,因而符合 delta 分布
  • 随机网络符合泊松分布

度的相关性:Newman 把它称为 “匹配模式”,意思是考察度值大的点倾向于和度值大的点连接,还是倾向于和度值小的点连接。

实际网络的分析表明,不同的网络存在不同的匹配模式,有正相关也有负相关。(有向网络中)基于顶点的 In-Out 度关联性。

当然关于网络的度分布还有更加复杂的问题和指标在里面。我们先跳过这个,看一下其他的指标(方便我们以后返回来研究度分布的问题)

平均路径长度

在这个问题里面有若干个独立的概念,我们分别加以介绍。

最短路径

最短路径:网络中两个顶点 i, j 之间的 最短路径 定义为所有连通 (i, j) 的通路中, 所经过的其他顶点最少的一条或几条路径。

换句话说,网络最短路可能不止一条。在这种情况下,所有符合我们这种最短路径的定义的路径,都是我们所说的最短路径。

因为我们这门课主要讲的还是一些概念性的东西,然后最终是要付诸于理论的实践的,所以我们在这里不是很关心获得最短路径的具体算法是什么。我们涉及到的这些概念,网络也会比较简单,基本上用肉眼或者遍历算法就可以找出所有的最短路径。但是我们可以知道,最常用的算法之一是 Dijkstra 算法。

最短路径问题不是最小流量问题

有一个很重要的问题得声明一下,就是这个最短路径问题不是最小流量问题。这两个问题特别容易搞混。而且他们使用的很多算法也是一样的,特别容易搞混。就这个简单的小问题,之前给我卡了半个小时。

对于最小流量问题而言,计算的是每条边上的权重,也就是说用 Dijkstra 考虑每条边上的权重的时候,求取两个节点之间的一条总权重最小的路径。

而最短路径问题,只是考虑经过几个节点,几条边,尽量使经过的节点和边最少,在这种情况下,当我们使用 Dijkstra 算法的时候,相当于直接把每条边上的权重都默认为了 1。

如果这两个概念没有搞清楚的话,下面理解其他概念也会搞错的

用下面的 networkx.all_shortest_paths() 方法可以找到所有的最短路:

shortest_paths = nx.all_shortest_paths(
    G, 
    source = 1, # 起始节点标号
    target = 6, # 终末节点标号
    method = 'dijkstra', # 指定算法
)

如果我们要求解最小流量问题,那么就要带上参数 weight = 'weight',表示指定以之前给网络图赋值的时候指定的网络图权重作为 dijkstra 算法的权重标准。这个问题之前排查了很难长时间。

官网上对这个问题的描述如下:

weight

None, string or function, optional (default = None)

If None, every edge has weight/distance/cost 1. If a string, use this edge attribute as the edge weight. Any edge attribute not present defaults to 1. If this is a function, the weight of an edge is the value returned by the function. The function must accept exactly three positional arguments: the two endpoints of an edge and the dictionary of edge attributes for that edge. The function must return a number.

method

string, optional (default = ‘dijkstra’)

The algorithm to use to compute the path. Supported options: dijkstra, bellman-ford. Other inputs produce a ValueError. If weight is None, unweighted graph methods are used, and this suggestion is ignored.

不过这里我们不需要。可以看到,根据官网上的解释,再不给出这个参数的情况下,就默认每条边的权重是 1 了

这里还有一个问题,就是 这个方法的返回值是一个生成器类型的对象,或者叫迭代器类型的对象,Python 里面叫做 generator

生成器是 Python 中一种特殊的数据类型,这种数据类型的特点是并不能像列表那样直接读取,而是在每一次循环调用输出数值的时候独立进行一次计算,这样做的好处就是不需要像列表那样,每次创建的时候输入大量的数据量,否则这样一来,当列表非常大的时候就会浪费很多的内存空间。

type(shortest_paths)
generator

也就是说,你不能直接通过 print() 来获得对象的信息,这个方法只会返回 generator 对象的二进制地址。。

print(shortest_paths)
<generator object _build_paths_from_predecessors at 0x000001CE2BBAF660>

如果我们想要真的获得最短路径的信息,可以选择遍历 generator 并同时 print() ( generator 是一个可迭代对象,在遍历的过程中会产生数据)

或者像下面这样,把输出结果转化为子列表之后追加到一个大列表。

shortest_path_list = []
for path in shortest_paths:
    shortest_path_list.append( list(path) ) 
shortest_path_list
[[1, 2, 6], [1, 4, 6]]

其实这里也设计了一个隐藏的问题,就是 Dijkstra 算法的时间复杂度的问题。

简单的来说,根据数学推算发现 Dijkstra 算法的时间复杂度不是多项式时间。在最坏情况下,Dijkstra算法的时间复杂度为:

O ( ( V + E ) log ⁡ V ) O( ( V + E ) \log{ V }) O((V+E)logV)

其中,V 是图中顶点的数量,E 是图中边的数量。

这是因为 Dijkstra 算法需要使用优先队列来选择下一个最短路径,并且在每次更新距离时需要进行堆操作。因此,Dijkstra 算法的时间复杂度与图中顶点和边的数量相关,并不是多项式时间复杂度。

这就意味着当一个网络图的边和节点数特别多的时候,Dijkstra 算法所消耗的时间将会非常的长,因此使用生成器而不是一次生成的列表是一个非常明智的选择,因为在很多情形下我们其实只是要找到相对多的最短路,而不是全部的最短路。

Big O Notation 是一个很复杂的数学问题,鉴于我薄如蝉翼的计算机基础,我就不解释了。详细的可以参考这篇博客文章:Dijkstra算法时间复杂度分析 - michealoven - CSDN。这篇博文的分析相对而言更加详细,考虑了 Dijkstra 算法在包括顺序遍历集合 T、使用二叉堆作为优先队列、使用二项堆作为优先队列、使用斐波那契堆作为优先队列在内的四种算法设计形式的情形的不同时间复杂度的计算,无一例外都不是多项式。

接下来我们要对我们找到的最短路进行可视化:

如果我们想要在图上把最短路径给他画出来,那么我们就要把最短路径经过的节点和边分别用列表标记下来。

path_edge_list = [] # 创建记录路径包含的边的列表
path_node_list = [] # 创建包含路径经过节点的列表
for sublist in shortest_path_list: # 遍历记录了所有最短路的列表
    for i in range( len(sublist)-1 ): 
        if [ sublist[i], sublist[i+1]] not in path_edge_list:
            path_edge_list.append([ sublist[i], sublist[i+1]])
    for node in sublist:
        if node not in path_node_list:
            path_node_list.append(node)

我来解释一下这段代码的原理:这段代码的作用是根据给定的最短路径列表,生成包含路径经过的边和节点的列表。

首先,代码创建了两个空列表:path_edge_listpath_node_list,分别用于记录路径包含的边和路径经过的节点。

接下来,通过遍历最短路径列表 shortest_path_list ,对每一个子列表(表示一条最短路径)进行处理。

第一个内层循环通过遍历子列表中的元素,将每条边(由相邻两个节点组成)加入到 path_edge_list 中。这里使用了条件判断 if [sublist[i], sublist[i+1]] not in path_edge_list: 来确保不重复添加边。

因为我们知道,这里我们看见的最短路径,实际上是写成一连串节点编号的列表。列表中的前一个节点编号和后一个节点编号之间一定是有一条边连接的,而列表中间隔一个节点编号的两个节点编号之间不一定有边连接;即使有边连接,也一定不在最短路里。因此,我们在遍历子列表元素并添加编的过程当中,只需要对于每两个节点进行一次添加边就好了。在这种情况下,假设最短路的长度是 n 个节点,那么这些节点之间的边就总共有 n - 1 条。这就是为什么是 for i in range( len(sublist)-1 )

第二个内层循环则是将子列表中的每个节点加入到 path_node_list 中。同样使用了条件判断 if node not in path_node_list: 来避免重复添加节点。

最终,当所有最短路径都被处理完毕后,path_edge_list 将包含所有路径经过的边,而 path_node_list 将包含所有路径经过的节点。

查看一下结果:

print("包含的边:", path_edge_list)
print("包含的节点:", path_node_list)
包含的边: [[1, 2], [2, 6], [1, 4], [4, 6]]
包含的节点: [1, 2, 6, 4]

然后我们就可以把找到的最短路在图中画出来:

# 绘制图形
pos = nx.spring_layout(G)  # 定义节点的布局位置
nx.draw_networkx_nodes(G, pos)  # 绘制节点
nx.draw_networkx_edges(G, pos)  # 绘制边

# 标记最短路径的边为红色,节点为橙色
nx.draw_networkx_edges(
    G, pos, 
    edgelist = path_edge_list,
    edge_color='red'
    )
nx.draw_networkx_nodes(
    G, pos, 
    nodelist = path_node_list,
    node_color='orange'
    )  # 绘制节点

# 绘制标签
nx.draw_networkx_labels(G, pos) # 绘制节点标签
nx.draw_networkx_edge_labels(G, pos, 
    edge_labels = weights
    ) # 绘制边权标签

{(1, 2): Text(0.7431046103364115, -0.19366616675567772, '2'),
 (1, 3): Text(0.10253599139712566, -0.9058957750675259, '4'),
 (1, 4): Text(0.4016773066925905, -0.47008285523609383, '5'),
 (2, 4): Text(0.5743088613970223, 0.14804252814328034, '2'),
 (2, 6): Text(0.5365892696416266, 0.5832644548689024, '7'),
 (3, 4): Text(-0.06625975754226353, -0.5641870801685679, '1'),
 (3, 5): Text(-0.5520885611755522, -0.5103461600670893, '4'),
 (4, 5): Text(-0.2529472458800874, -0.07453324023565716, '3'),
 (4, 6): Text(0.1951619659978056, 0.3068477663884863, '4'),
 (5, 6): Text(-0.29066683763548307, 0.36068868648996494, '1'),
 (5, 7): Text(-0.7555660399153519, 0.38681840036719195, '7'),
 (6, 7): Text(-0.3074568280374589, 0.7681994069913354, '5')}

在这里插入图片描述

是的,这就是我们需要的结果。搞定。

网络节点之间的距离

两个顶点 i, j 之间的 距离 d i j d_{ij} dij 定义为 i, j 之间 最短路径上的边数

有了距离的概念之后,我们就能够实现 距离矩阵。一个距离矩阵是一个包含一组点两两之间距离的矩阵(即 二维数组)。因此给定N个欧几里得空间中的点,其距离矩阵就是一个非负实数作为元素的 N*N 的对称矩阵。比如对于下面这个网络:

在这里插入图片描述

可以写出距离矩阵如下:

D = ( 0 1 1 2 1 0 1 1 2 0 2 2 0 3 0 ) D = \begin{pmatrix} 0 & 1 & 1 & 2 & 1 \\ & 0 & 1 & 1 & 2 \\ & & 0 & 2 & 2 \\ & & & 0 & 3 \\ & & & & 0 \\ \end{pmatrix} D= 010110212012230

矩阵左下半三角可以不用写,反正对于这样的无向图,距离矩阵肯定是对称的。

在 Python 里面,如果我们想要用距离矩阵创建图,一般是把距离矩阵转换为接邻矩阵来实现。转换的方法也很简单:如果两个节点之间的距离为 1,就说明两个节点接邻;相反,无论两个节点之间的数值是除了 1 之外的什么数字,只要不是 1,就说明不接邻。

dist_matrix = np.array(
    [
        [ 0, 1, 1, 2, 1 ],
        [ 1, 0, 1, 1, 2 ],
        [ 1, 1, 0, 2, 2 ],
        [ 2, 1, 2, 0, 3 ],
        [ 1, 2, 2, 3, 0 ],
    ]
)
adj_matrix = np.where( dist_matrix == 1, 1, 0 )
adj_matrix
array([[0, 1, 1, 0, 1],
       [1, 0, 1, 1, 0],
       [1, 1, 0, 0, 0],
       [0, 1, 0, 0, 0],
       [1, 0, 0, 0, 0]])

这样就可以得到一个与原始距离矩阵形状相同的邻接二值矩阵,其中等于 1 的位置表示原始距离矩阵中对应位置存在一条边(距离为 1),而等于 0 的位置表示不存在边。

解释一下代码实现:np.where() 是一个函数,用于根据给定的条件返回数组中的元素。

在这段代码中,np.where( dist_matrix == 1, 1, 0 ) 的作用是将距离矩阵 dist_matrix 中等于1的元素替换为1,其余元素替换为0,并返回一个新的数组。

具体而言,它会遍历 dist_matrix 中的每个元素,如果元素等于 1,则将对应位置的新数组元素设为 1;如果元素不等于 1,则将对应位置的新数组元素设为 0。其实就相当于下面的代码:

for i in range(dist_matrix.shape[0]):
    for j in range(dist_matrix.shape[1]):
        if dist_matrix[i, j] == 1:
            adj_matrix[i, j] = 1
        else:
            adj_matrix[i, j] = 0

但是我们这里用 adj_matrix = np.where( dist_matrix == 1, 1, 0 ) 一句话代替,降低了代码的复杂程度。

然后我们通过接邻矩阵创建图:

demo_G = nx.from_numpy_array(adj_matrix, create_using = nx.Graph())
demo_G = nx.convert_node_labels_to_integers(demo_G, first_label = 1)

我们进行一下绘图,可以看见产生的图就是我们上述的图。

pos = nx.circular_layout(demo_G)
nx.draw_networkx(demo_G, pos)

在这里插入图片描述

在 Python 中,如果想要获得一个图的距离矩阵,可以通过 nx.all_pairs_shortest_path_length() 函数来获取。函数的返回值是一个包含距离值的接邻字典:

distanceDict = dict( nx.all_pairs_shortest_path_length(G) )
distanceDict
{1: {1: 0, 2: 1, 3: 1, 4: 1, 6: 2, 5: 2, 7: 3},
 2: {2: 0, 1: 1, 4: 1, 6: 1, 3: 2, 5: 2, 7: 2},
 3: {3: 0, 1: 1, 4: 1, 5: 1, 2: 2, 6: 2, 7: 2},
 4: {4: 0, 1: 1, 2: 1, 3: 1, 5: 1, 6: 1, 7: 2},
 5: {5: 0, 3: 1, 4: 1, 6: 1, 7: 1, 1: 2, 2: 2},
 6: {6: 0, 2: 1, 4: 1, 5: 1, 7: 1, 1: 2, 3: 2},
 7: {7: 0, 5: 1, 6: 1, 3: 2, 4: 2, 2: 2, 1: 3}}

我们可以通过遍历字典值的方式来生成距离矩阵,转换为我们熟悉的 np.array() 类型。

这里因为图节点的名称都是整型数据,所以直接用 range() 函数进行遍历就行了。但是如果节点的名称是诸如 "A", "B", "C" 这样的字符,就要先通过 G.nodes() 获得节点名称、转换为列表再遍历。(此处不再演示)

n = len( distanceDict )
D_mat = np.zeros( (n, n) )
for node_i in range( 0, n ):
    for node_j in range( 0, n ):
        D_mat[node_i][node_j] = distanceDict[node_i + 1][node_j + 1]
D_mat
array([[0., 1., 1., 1., 2., 2., 3.],
       [1., 0., 2., 1., 2., 1., 2.],
       [1., 2., 0., 1., 1., 2., 2.],
       [1., 1., 1., 0., 1., 1., 2.],
       [2., 2., 1., 1., 0., 1., 1.],
       [2., 1., 2., 1., 1., 0., 1.],
       [3., 2., 2., 2., 1., 1., 0.]])

网络的直径与网络的平均路径长度

网络的直径 (diameter),定义为网络中任意两个顶点之间 距离 的最大值。

简单的来说,网络上离得最远的两个点的距离就是网络的直径。

由于距离的定义是 最短路径上的边数,所以网络的直径也是最短路径上边数的最大值。假如你现在在网络上面绕很大一圈,然后指定说这是这个网络的直径,那么这种定义就是错误的。这是你搞混了距离的定义的概念。

在 NetWorkX 里面,我们可以使用 nx.diameter() 来获得网络的直径。

print( nx.diameter(G) )
3

网络的 平均路径长度 (average path length), 定义为网络中任意两个顶点之间距离的平均值

L = ∑ i > j d i j C N 2 L = \dfrac{ \sum_{i \gt j}^{}{d_{ij}} }{ C_N^2 } L=CN2i>jdij

注意这里的 C N 2 C_N^2 CN2 是排列组合的意思

此外,这里还要注意:由于 距离 的定义是 最短路径上的边数,所以平均路径长度其实也就是平均最短路径。

在 Python 里面,NetWorkX 提供了 nx.average_shortest_path_length() 来获得平均路径长度。

print( nx.average_shortest_path_length(G) )
1.4761904761904763

聚类系数

在朋友关系网中,你的两个朋友很可能彼此也是朋友。这种属性称为网络的聚类特性。

用数学化的语言来说,对于某个节点 i i i,它的聚类系数 C i C_i Ci 被定义为它所有相邻节点之间连的数目占可能的最大连边数目的比例。

具体地,设节点 i i i k i k_i ki 条边与之相连(即节点 i i i k i k_i ki 个邻居),显然这个节点最多有 C k i 2 C_{k_i}^{2} Cki2 条边,设这 k i k_i ki 个节点之间实际有 E i E_i Ei 条边相连,则聚类系数 C i = E i / C k i 2 C_i = E_i / C_{k_i}^2 Ci=Ei/Cki2

整个网络的聚类系数 C 则是 所有节点聚类系数的平均值

在随机网络中, C = p C = p C=p, (由于边的分布是随机的)

在 Python 中,可以用 networkx.clustering() 来获取每一个节点的聚类系数,用 networkx.average_clustering() 来获取平均聚类系数。

print( "聚类系数:", nx.clustering(G) )
print( "平均聚类系数:", nx.average_clustering(G))
聚类系数: {1: 0.6666666666666666, 2: 0.6666666666666666, 3: 0.6666666666666666, 4: 0.5, 5: 0.5, 6: 0.5, 7: 1.0}
平均聚类系数: 0.6428571428571429

网络拓扑的基本模型及其性质

我们介绍一下下面的几个网络:

  • 规则网络
  • 随机网络
  • Small World网络
  • Scale Free网络
  • 等级网络

Python 的 networkx.random_graphs 模块提供了绘制下面各种图的函数。注意:在此处绘制的所有随机图,每一次绘制都会产生不同的结果。

用法可以参考这个文章:图论与复杂网络建模工具Networkx的四种网络模型

然后这个文章把每一种网络的生成都自己写了一遍:Python实现四种网络模型的生成、平均度等指标的计算及度分布函数的展示

规则网络

规则网络是指平移对称性晶格,任何一个格点的近邻数目都相同

各个节点的具有相同的度值。

如图为最近邻耦合网络:每个节点都与它左右的 K / 2 个节点相连。

在这里插入图片描述

在 NetWorkX 中提供了 networkx.random_regular_graph() 方法,让我们可以一步到位地生成符合我们需求的规则网络:

# 随机生成 20 个节点,每个节点的度都是 3
regular_G = nx.random_regular_graph(3, 30)
pos = nx.spectral_layout(regular_G)  # 根据图的拉普拉斯特征向量排列节点
nx.draw_networkx(regular_G, pos, with_labels = True )

在这里插入图片描述

一般地,规则网络具有大的平均距离,但是会有较好的聚集情况

print( "直径:", nx.diameter(regular_G) )
print( "平均路径长度:", nx.average_shortest_path_length(regular_G) )
print( "平均聚类系数:", nx.average_clustering(regular_G) )
直径: 6
平均路径长度: 3.1816091954022987
平均聚类系数: 0.06666666666666667

随机网络

ER 随机图模型:顶点的度值服从 Poisson distribution,也称 Poisson 随机图

随机网络背后的哲学思想:在节点之间随机放置链接

知乎文章 附录二 随机网络模型与小世界模型简介 对这个问题进行了相对详细的描述。

网络的构建过程:

  1. N N N 个孤立节点开始;
  2. 选择一对节点,产生一个 0 0 0 1 1 1 之间的随机数。如果该随机数小于给定的概率 p p p,在这对节点之间放置一条链接;否则,该节点对保持不连接;
  3. 对所有节点对(总共有 N ( N − 1 ) 2 \dfrac{N(N-1)}{2} 2N(N1) 个节点对),重复步骤 2。

我们可以自己来简单地写一下 Python 的实现: 我们定义下面的函数 random_matrix(N, p)

def random_matrix(N, p):
    import random
    import networkx as nx
    g = nx.Graph()
    for i in range(N):
        for j in range(i + 1, N):
            if random.random() < p:
                g.add_edge( i, j )
    return g

下面是对代码的详细解释:

首先,random_matrix 函数接受两个参数:Np。参数N表示要生成的矩阵的大小,即节点的数量。参数p表示连接两个节点的概率。

函数内部导入了两个模块:random用于生成随机数,networkx用于处理图形数据结构。

  • g = nx.Graph() 创建了一个名为 g 的空图。
  • 使用嵌套循环遍历所有可能的节点对。
  • 对于每一对节点 (i, j),通过生成一个介于0到1之间的随机数来决定是否在它们之间添加一条边。
  • 如果生成的随机数小于给定的概率 p,则将节点 i 和节点 j 之间添加一条边。
  • 返回最终生成的图 g

通过调整参数 Np 的值,你可以控制生成图的大小和连接概率,从而得到不同类型的随机图。

上述过程得到的网络被称为随机图或随机网络,又被称作埃尔德什 —— 雷尼(Erdős-Rényi)网络。可以用 G ( N , p ) G(N, p) G(N,p) 来表示。

我们可以把我们生成的图给他绘制出来:

random_model_G = random_matrix( 30, 0.2 )
pos = nx.spectral_layout(random_model_G)  # 根据图的拉普拉斯特征向量排列节点
nx.draw_networkx(random_model_G, pos, with_labels = True)

在这里插入图片描述

当然,NetWorkX 也已经给我们提供了现成的接口方法 nx.erdos_renyi_graph(N, p) 可以直接调用。

# 随机生成 30 个节点,节点间的连接概率都是 0.2
random_G = nx.erdos_renyi_graph(30, 0.2)
pos = nx.spectral_layout(random_G)  # 根据图的拉普拉斯特征向量排列节点
nx.draw_networkx(random_G, pos, with_labels = True)

在这里插入图片描述

然后我们来看一下随机网络的各项指标参数。(如果重新执行了上面的生成图代码,那么这下面的指标也会随之变化。图的生成是完全随机的!)

print( "直径:", nx.diameter(random_G) )
print( "平均路径长度:", nx.average_shortest_path_length(random_G) )
print( "平均聚类系数:", nx.average_clustering(random_G) )
直径: 4
平均路径长度: 2.197701149425287
平均聚类系数: 0.24142857142857144

随机网络是另一个极端。由 N N N 个顶点构成的图中,可以存在 C N 2 C_N^2 CN2 条边,我们从中随机连接 M M M 条边所构成的网络就叫随机网络。

还有一种生成随机网络的方法是,给一个概率 p p p,对于 C N 2 C_N^2 CN2 中任何一个可能连接,我们都尝试一遍以概率 p p p 的连接。如果我们选择 M = p C N 2 M = p C_N^2 M=pCN2 这两种随机网络模型就可以联系起来。

如:pajek 的生成 聚类系数: C = p ≪ 1 C = p \ll 1 C=p1 (由于极度稀疏)

一般地,随机网络具有小的平均距离;但与此同时,聚集性也会更差。

是否存在一个同时具有高的集聚程度,小的最短路径网络呢?

这个问题有着非常深刻的实践意义。

  • 对于传染病模型,平均集聚程度对应于传播的广度,平均最短距离代表的是传播的深度。

  • 因此,如果实际网络同时存在宽的广度和大的深度的话,在这样的网络上的传染病传播显然将大大高于规则网络与随机网络。

小世界网络

1998 年,Watts 和 Strogatz 为我们找到了这样的网络模型:Small World 网络(发表在 Nature 上)

现在常称为:WS model

方法:Watts和Strogatz发现,只需要在规则网络上稍作随机改动就可以同时具备以上两个性质。

改动的方法是:对于规则网络的每一个顶点的所有边,以概率 p p p 断开一个端点,并重新连接,连接的新的端点从网络中的其他顶点里随机选择;如果所选的顶点已经与此顶点相连,则再随机选择别的顶点来重连。

p = 0 p = 0 p=0 时,产生的就是规则网络, p = 1 p=1 p=1 则为随机网络。对于 0 < p < 1 0 \lt p \lt 1 0<p<1 的情况,存在一个很大的 p p p 的区域,同时拥有较大的集聚程度和较小的最小距离。

在这里插入图片描述

  • 形成机制:规则网络,以概率 p p p 断开一个端点,随机连接

Python 里面可以用 watts_strogatz_graph(N, k, p) 来创建随机的 WS 模型。

#生成一个含有 30 个节点、每个节点有 6 个邻居
# 以概率 p = 0.5 随机化重连边的WS小世界网络
smallworld_G = nx.watts_strogatz_graph(30, 6, 0.5)
pos = nx.spectral_layout(smallworld_G)  # 根据图的拉普拉斯特征向量排列节点
nx.draw_networkx(smallworld_G, pos, with_labels = True)

在这里插入图片描述

print( "直径:", nx.diameter(smallworld_G) )
print( "平均路径长度:", nx.average_shortest_path_length(smallworld_G) )
print( "平均聚类系数:", nx.average_clustering(smallworld_G) )
直径: 4
平均路径长度: 2.0597701149425287
平均聚类系数: 0.2635714285714286

NW 小世界网络

WS model 的构造过程有可能破坏网络的连通性

1999 年,Newman and Watts 提出了 NW 模型:用 “随机化加边” 替代 “随机化重连”。

我参考了这篇博客:NW小世界网络模型python代码实现及平均路径聚类系数计算

NW小世界网络具体构造算法如下

  • 从规则图开始

给定一个含有 N 个节点的环状最近邻耦合网络,其中每个节点都与它左右相邻的各 K/2 个节点相连,K是偶数。

  • 随机化加边

以概率 p 在随机选取的 NK/2 对节点之间添加边,其中规定不得有重边和自环。在 p = 0 时,WS 模型和 NW 模型都对应于原来的最近邻耦合网络;在 p = 1 时,WS 模型相当于随机图,而 NW 模型则相当于在规则最近邻耦合网络的基础上再叠加一个一定边数的随机图。当 p 足够小而 N 足够大时,可以认为 WS 模型和 NW 模型是等价的。

博客里面提供了生成网络的代码,但是我按照那个代码写下来,发现是个死循环。我传入参数 (30, 6, 0.2),运行了几分钟都没有停。反正我没太理解,就,挺奇怪的吧。

国内网上关于这个的资料也不是很多,查来查去,描述这个模型构建方法的也只有这两句话。我按照自己的理解写了一下:

def NW_network(N, K, p):
    import random
    import networkx as nx
    g = nx.random_graphs.random_regular_graph( int(K / 2), int(N) )
    for i in range(N): # 遍历:增加边时的起始节点
        for j in range(i + 1, N): # 遍历:增加边时的中止节点节点
            if random.random() < p: # 如果随机数小于概率 p 
                g.add_edge( i, j )  # 那就增加一条边
    return g

这是一个简单的函数,函数分为三步:

  1. 引进 random 和 networkx 两个库
  2. 调用 nx.random_graphs.random_regular_graph( int(K / 2), int(N) ),创建一个含有 N 个节点的环状最近邻耦合网络,其中每个节点都与它左右相邻的各 K/2 个节点相连
    • 以防万一,这里给它转化为 int() 类型
  3. 在随机选取的 NK/2 对节点之间添加边,如果随即生成的概率值小于 p 就添加边。通过循环嵌套的设计避免自环

我们调用一下函数看看效果:

NWmodel_G = NW_network( 30, 6, 0.2 )
# pos = nx.spectral_layout(NWmodel_G)  # 根据图的拉普拉斯特征向量排列节点
pos = nx.spectral_layout(NWmodel_G)
nx.draw_networkx(NWmodel_G, pos, with_labels = True)

在这里插入图片描述

嗯,好像是正常工作了(?)我也不确定。

看看这个模型的参数吧

print( "直径:", nx.diameter(NWmodel_G) )
print( "平均路径长度:", nx.average_shortest_path_length(NWmodel_G) )
print( "平均聚类系数:", nx.average_clustering(NWmodel_G) )
直径: 3
平均路径长度: 1.7563218390804598
平均聚类系数: 0.2531649831649832

还有许多改进的模型:加点,加边,去点,去边,

以及不同形式的交叉,产生多种形式的小世界模型

Scale Free 网络

节点度服从幂律分布,就是说具有某个特定度的节点数目与这个特定的度之间的关系可以用一个幂函数近似地表示:

P ( k ) ∼ k − γ P(k) \sim k^{- \gamma} P(k)kγ

其中, P ( k ) P(k) P(k) 是度数为 k k k 的节点出现的概率, γ \gamma γ 是幂律指数。

在这个公式中, k k k 表示节点的度数,而 P ( k ) P(k) P(k) 表示具有度数 k k k 的节点出现的概率。

幂律分布表明,较大的度数 k k k 对应的节点出现的概率较小,而较小的度数 k k k 对应的节点出现的概率较大。

幂律分布具有以下特点:

  1. 长尾性质:幂律分布是一个长尾分布,也就是说,在较大的度数范围内,节点出现的概率呈现出指数级下降。这意味着存在少量极高度连接的节点,它们与大多数其他节点相比具有更多的连接。

  2. 无界性:幂律分布在理论上是无界的,也就是说,在理论上可能存在任意高度连接的节点。然而,在实际网络中,由于限制因素(如资源、物理约束等),通常存在一个上限或截断值。

  3. 幂律指数 γ \gamma γ:幂律指数 γ \gamma γ 是一个重要的参数,它决定了幂律分布的形状。较小的 γ \gamma γ 值表示更陡峭的幂律分布,而较大的 γ \gamma γ 值表示更平缓的幂律分布。

上面这些数学原理比较复杂,但是我们可以从下面两个比较直观的角度来简单的解释这个问题:

  1. “富者愈富”原则:在无标度网络中,度数较高的节点更有可能被选择为新节点的邻居。这是因为这些高度连接的节点已经具有了大量的连接机会,并且由于其高度连通性,它们更容易被其他新加入的节点选择为邻居。因此,这些中心节点倾向于吸引更多的连接,导致它们的度数继续增长。
  2. 网络增长过程中的随机性:无标度网络通常是通过逐步添加新节点和连接来构建的。在这个过程中,新加入的节点会随机选择一些已存在的节点作为邻居。由于随机性的存在,度数较高的节点更有可能被选中,因为它们具有更多的连接机会。这种随机性也有助于形成幂律分布。

幂函数曲线是一条下降相对缓慢的曲线,这使得度很大的节点可以在网络中存在。

对于随机网络和规则网络,度分布区间非常狭窄,几乎找不到偏离节点度均值较大的点,故其 平均度可以被看作其节点度的一个特征标度在这个意义上,我们把节点度服从幂律分布的网络叫做无标度网络(scale-free networks),并称这种节点度的幂律分布为网络的 无标度特性

这个可以参考知乎文章 什么是无标度网络 | 集智百科

我们可以根据上述的定义,自己来写一个函数实现:

def SF_network(N, k):
    import networkx as nx
    import random
    g = nx.Graph()
    g.add_edge(0, 1)  # 初始情况下至少需要两个节点
    N = N - 2 # 因为我们已经给定了初始的两个节点,所以总节点数减去 2
    for i in range(2, N):
        node_list = list(g.nodes())
        degrees = [g.degree(node) for node in node_list]
        probabilities = [degree / sum(degrees) for degree in degrees]
        selected_nodes = random.choices(node_list, probabilities, k = k)
        for node in selected_nodes:
            g.add_edge(i, node)
    
    return g

让我来详细解释一下这段代码:

我们使用了 networkx 库来创建和操作图形对象。代码首先创建了一个空的图形对象 G,然后添加了两个初始节点。接下来,从第三个节点开始,每次添加一个新节点,并随机选择已存在的节点进行连接,直到达到指定的节点数量 N。每个新节点连接到现有节点的数量由参数 k 控制。

接下来,degrees = [g.degree(node) for node in node_list]:计算每个节点的度数,并将这些度数存储在列表 degrees 中。

然后计算每个节点被选择的概率:probabilities = [degree / sum(degrees) for degree in degrees]

probabilities 是一个列表,里面保存了每个节点被选中去连接新节点的概率。每个节点被选中去连接新节点的概率等于该节点现在的度数除以整个网络的总度数。

  • 换句话说:对于每个节点,我们将其度数除以所有节点的总度数,得到一个概率值,这个概率值就是每个节点被选择的概率。

  • 这样一来,度数较高的节点具有更高的选择概率,从而更容易成为网络中新连接的节点。这样就实现了模拟符合幂分布的度数分布。

  • 具体而言,在无标度网络中,每次添加一个新节点时,它会与已存在的一些节点建立连接。根据“优先连接”的原则,即度数较高的节点更有可能被选择为新节点的邻居。通过计算每个节点被选择的概率,并将其存储在列表 probabilities 中,我们可以根据这些概率来随机选择邻居节点。

使用 random.choices 函数,根据概率从现有节点中选择 m 个目标节点,并将这些目标节点存储在列表 selected_nodes 中。

最后,我们遍历目标节点列表,并使用 g.add_edge 函数向图中添加边,连接新节点与目标节点。循环结束后,我们返回生成的无标度网络对象 g

我们来输出一下代码的绘图,看一下效果。可以看到,这就是我们想要的 Scale Free 无标度网络

scaleFree_G = SF_network( 30, 1 )
pos = nx.spring_layout(scaleFree_G)  # 根据图的拉普拉斯特征向量排列节点
nx.draw_networkx(scaleFree_G, pos, with_labels = True)

在这里插入图片描述

如果你还是不理解上面的代码究竟做了什么,不如我们让代码输出每一步循环的变量,仔细看一看这个过程是怎么进行的。

第一轮循环:

node list : [0, 1]
degrees : [1, 1]
probabilities : [0.5, 0.5]
selected nodes : [0]

可以看到,这个循环开始于两个初始节点 0 和 1。两个节点,最初被设定为连接在一起的状态,因此,两个节点的度数都是 1 1 1

这个时候,整个网络的总度数是 2 2 2,由于 1 / 2 = 0.5 1 / 2 = 0.5 1/2=0.5,所以这个时候两个结点与新产生的结点发生连接的概率都是 1 / 2 1 / 2 1/2

第二轮循环:

node list : [0, 1, 2]
degrees : [2, 1, 1]
probabilities : [0.5, 0.25, 0.25]
selected nodes : [2]

在第二轮循环当中这里增加了一个新的节点 2。

新来的节点 2 是在第一轮循环的末尾增加到网络图的。从输出的结果来看,节点 0 的度是 2。说明这个时候节点 0 刚好和两个节点相连接了。图上总共只有 3 个节点,所以,第一轮循环的结尾,概率判断的结果应该是新的节点 1 被追加到了 0 节点上。

刚才已经跟大家讲过这里的节点,增加的概率是遵循 “富者愈富” 的原则的,在这个情况下,节点 0 所拥有的次数高于其他节点,因此,节点 0 获得与新节点连接的概率也大于其他的节点。从下面的数字可以看出,这里节点 0 连接新节点的概率是 0.5,而其他节点只有 0.25

第三轮循环:

node list : [0, 1, 2, 3]
degrees : [2, 1, 2, 1]
probabilities : [0.3333333333333333, 0.16666666666666666, 0.3333333333333333, 0.16666666666666666]
selected nodes : [2]

可以看到,在这里第三轮循环当中,增加了一个新的节点 3。从各个节点的度数来看,新增加的节点 3,被连接到了原本第一轮循环末尾出现的节点 2 上。在这种情况下,整张图的总度数就变成了 2 + 1 + 2 + 1 = 6 2+1+2+1 = 6 2+1+2+1=6,通过每个节点的度数除以这个 6,计算出下一轮循环当中,每一个节点连接下一个新节点的可能性,如此往复,依次循环。

当然,在 Python 里面直接提供了生成 BA 无标度网络的方法。为了更好地展示 Scale Free 网络所具有的特点,我这里把网络的布局设为 spring_layout()

#生成一个含有 20 个节点、每次加入 1 条边的 BA 无标度网络。
BAscalefree_G = nx.barabasi_albert_graph(30, 1)
pos = nx.spring_layout(BAscalefree_G)  # 根据图的拉普拉斯特征向量排列节点
nx.draw_networkx(BAscalefree_G, pos, with_labels = True)

在这里插入图片描述

print( "直径:", nx.diameter(BAscalefree_G) )
print( "平均路径长度:", nx.average_shortest_path_length(BAscalefree_G) )
print( "平均聚类系数:", nx.average_clustering(BAscalefree_G) )
直径: 4
平均路径长度: 2.52183908045977
平均聚类系数: 0.0

总结一下上面提到的 Python 绘图方法:

网络类型 绘图方法 参数
规则网络 random_regular_graph (度次, 节点数)
随机网络 erdos_renyi_graph (节点数, 连接概率)
小世界网络 watts_strogatz_graph (节点数, 邻居, 重连概率)
无标度网络 barabasi_albert_graph (节点数, 每次加入边数)

然后我们可以把四种网络放在一起比较一下:

fig, axes = plt.subplots(2, 2, figsize = (12, 10))
axes[0][0].set_title("规则网络")
G_1 = nx.random_regular_graph(3, 20)
pos = nx.spectral_layout(G_1) 
nx.draw_networkx(G_1, pos, with_labels = True, ax = axes[0][0])
axes[1][0].set_title("随机网络")
G_2 = nx.erdos_renyi_graph(20, 0.2)
pos = nx.spectral_layout(G_2) 
nx.draw_networkx(G_2, pos, with_labels = True, ax = axes[1][0])
axes[0][1].set_title("小世界网络")
G_3 = nx.watts_strogatz_graph(20, 4, 0.1)
pos = nx.spectral_layout(G_3) 
nx.draw_networkx(G_3, pos, with_labels = True, ax = axes[0][1])
axes[1][1].set_title("无标度网络")
G_4 = nx.barabasi_albert_graph(20, 1)
pos = nx.pos = nx.spectral_layout(G_4) 
nx.draw_networkx(G_4, pos, with_labels = True, ax = axes[1][1])

在这里插入图片描述

复杂网络理论的发展及应用

复杂网络研究简史

时间(年) 人物 事件
1736 Euler 七桥问题
1959 Erdos 和 Renyi 随机图理论
1967 Milgram 小世界实验
1973 Granovetter 弱连接的强度
1998 Watts 和 Strogatz 小世界模型
1999 Barabasi 和Albert 无标度网络

世纪之交复杂网络研究取得突破性进展的主要原因包括:

  1. 越来越强大的计算设备和迅猛发展的 Internet,使得人们开始能够收集和处理规模巨大且种类不同的实际网络数据。(过去研究的网络规模太小)
  2. 学科之间的相互交叉使得研究人员可以广泛比较各种不同类型的网络数据从而揭示复杂网络的共性。
  3. 以还原论和整体论相结合为重要特色的复杂性科学的兴起,也促使人们开始从整本上研究网络的结构与性能之间的关系。

发现:揭示刻画网络系统结构的统计性质,以及度量这些性质的合适方法

建模:建立合适的网络模型以帮助人们理解这些统计性质的意义与产生机理

分析:基于单个节点的特性和整个网络的结构性质分析与预测网络的行为。

控制:提出改善已有网络性能和设计新的网络的有效方法,特别是稳定性、同步和数据流通等方面。

从 2002 年起,国内不同学科的研究人员和青年学者对复杂网络研究的兴趣越来越浓,至今国内已召开过多次以复杂网络为主题的学术会议和论坛

2004 年 4 月在无锡组织了有40余人参加的首届全国复杂动态网络学术论坛。

武汉大学在国内率先成立了校级复杂网络研究中心并于 2005 年春季组织了全国复杂网络学术会议

2005 年 10 月在北京召开的由中国高等学术研究中心组织的第二届全国复杂网络学术论坛

一些国际著名大学(如 MIT,哥伦比亚大学和密歇根大学等)已相继开设了有关复杂网络的课程,汪小帆教授也在上海交通大学为研究生开设了复杂网络课程。

面临的挑战性课题

中国原子能科学研究院方锦清列举:

挑战性问题之一:从理论上急待深入探索复杂动态网络的数学物理模型,建立精确的理论框架,例如,统一混合择优理论,六度分离理论,无标度特性,多标度特性和超家族特性,以及量子信息网络等, 这是网络发展面临的一大课题

挑战性问题之二:探索从随机方法,确定性方法, 到多种混合方法, 以及不同网络特性的互相转变关系, 从而构造符合实际要求和工程应用的复杂网络

挑战性问题之三:复杂网络是否存在普遍动力学性质,是否存在更多的统计分布规律和非统计规律, 大规模复杂网络是否存在富标度特性, 它们之间有什么内在联系

挑战性问题之四:研究非线性动态复杂网络中动力学过程的时空复杂性及其主要表现形式, 包括: 相同和不同的结点动力学下,分叉,混沌,阵发混沌等及其各种广义同步.同步的产生机制分析,控制和同步问题

挑战性问题之五:探索不同类型网络的非线性演化和时空斑图的涌现产生的

挑战性问题之六:如何描述复杂物理机制, 为什么社会网络与技术网络和生物网络的拓扑特性差异很大

挑战性问题之七:动态网络基本性质的特征量 如何发展定量与定性分析方法,不仅需要几何描述,而且需要物理和信息等更多的描述,以便有效地刻画复杂动态网络的主要特性

挑战性问题之八:进一步研究和发展广义随机与确定论相结合理论,交叉理论方法,非平衡统计理论方法,相变理论方法,玻色-爱因斯坦凝聚理论方法,等等,推进整体复杂动态网络研究的深入

挑战性问题之九:复杂动态网络的应用研究,如何把复杂网络的研究成果尽快地实应用于我国实际工程,国防领域(例如无线特设通信网络等) 中去,这正是该领域深入研究的迫切要求和进一步发展的推动力所在

挑战性问题之十:生物复杂网络的研究

猜你喜欢

转载自blog.csdn.net/BOXonline1396529/article/details/132907253