本算法的具体内容请参考Finding and evaluating community structure in networks(Newman and Girvan)。
重要概念
边介数(betweenness):网络中任意两个节点通过此边的最短路径的数目。
GN算法的思想:
在一个网络之中,通过社区内部的边的最短路径相对较少,而通过社区之间的边的最短路径的数目则相对较多。GN算法是一个基于删除边的算法,本质是基于聚类中的分裂思想,在原理上是使用边介数作为相似度的度量方法。在GN算法中,每次都会选择边介数高的边删除,进而网络分裂速度远快于随机删除边时的网络分裂。
GN算法的步骤如下:
(1)计算每一条边的边介数;
(2)删除边界数最大的边;
(3)重新计算网络中剩下的边的边阶数;
(4)重复(3)和(4)步骤,直到网络中的任一顶点作为一个社区为止。
GN算法计算边界数的时间复杂度为 O(m*n),总时间复杂度在m条边和n个节点的网络下为 O(m^2 *n)。
GN算法的缺陷:
(1)不知道最后会有多少个社区;
(2)在计算边介数的时候可能会有很对重复计算最短路径的情况,时间复杂度太高;
(3)GN算法不能判断算法终止位置。
为了解决这些问题,Newman引入了模块度Q的概念,它用来一个评价社区结构划分的质量。网络中的社区结构之间的边数并不是绝对数量上的少,而是应该比期望的边数要少。
模块度
模块度介绍
设Avw为网络的邻接矩阵的一个元素,定义为:
假设cv和cw分别表示点v和点w所在的两个社区,社区内部的边数和网络中总边数的比例:
函数δ(cv,cw)的取值定义为:如果v和w在一个社区,即cv=cw,则为 1,否则为 0。m 为网络中边的总数。
模块度的大小定义为社区内部的总边数和网络中总边数的比例减去一个期望值,该期望值是将网络设定为随机网络时同样的社区分配所形成的社区内部的总边数和网络中总边数的比例的大小,于是模块度Q为:
其中kv表示点v的度。
设eij表示社区I和社区J之间的连接边和与总边数的比例,ai表示社区i内部的点所关联的所有的边的数目与总边数的比例。
为了简化Q的计算,假设网络已经划分成n个社区,这个时候就有一个 n维矩阵,Q 的计算可以变成:
eii表示的是节点全在社区i内部中的边所占的比例
模块度的物理意义是,网络中连接两个同种类型结点的边(即社区内部的边)的比例减去在同样的社区结构”下任意连接节点的边的比例的期望值。如果社区内部边的比例不大于任意连接时的期望值,则有Q=0。Q的上限为1,而Q越接近于这个值,就说明社区结构越明显。实际网络中,该值通常位于0.3-0.7之间。
引入模块度后代码如下:
数据集为Karate数据集:
Zachary空手道俱乐部成员关系网络是复杂网络、社会学分析等领域中最常用的一个小型检测网络之一。从1970到1972年,Zachary观察了美国一所大学空手道俱乐部成员间的社会关系,并构造出了34个成员,78条成员关系的社会关系网。两个成员经常一起出现在俱乐部活动之外的其他场合,就认为两个成员间有边。该俱乐部因为主管(节点34)与教练(节点1)之间的争执而分裂成2个各自为核心的小俱乐部。
python代码如下:
class GN:
def __init__(self, G):
self._G = G
self._G_cloned = cloned_graph(G)
# 初始划分情况为所有节点为一个社区
self._partition = [[n for n in G.nodes()]]
self._max_Q = 0.0
def execute(self):
while len(self._G.edges()) != 0:
# 1.计算每一条边的边介数
# nx.edge_betweenness返回边介数字典,items返回可遍历的(键, 值) 元组数组。这里每个item是((vi,vj), edge_betweenness))
# 因此我们取的item[1]最大的item,再取该最小item的[0],为我们要删除的两个点(即要删除的边)
edge = max(nx.edge_betweenness_centrality(self._G).items(),
key=lambda item: item[1])[0]
# 2. 移去边介数最大的边
self._G.remove_edge(edge[0], edge[1])
# 获得移去边后的子连通图
components = [list(c)
for c in list(nx.connected_components(self._G))]
if len(components) != len(self._partition):
# 3. 计算Q值
cur_Q = cal_Q(components, self._G_cloned)
if cur_Q > self._max_Q:
self._max_Q = cur_Q
self._partition = components
print(self._max_Q)
print(self._partition)
return self._partition
结果如下:
Q值:0.40129848783694944
算法执行时间0.08202219009399414
本算法的具体内容请参考 Fast algorithm for detecting community structure in networks(Newman)。
GN算法通过模块度可以准确的划分网络,但它只适用于中小型规模的网络(时间复杂度高)。Newman提出一种基于贪心的快速社区发现算法,算法的基本思想是:首先将网络中的每个顶点设为一个单独社区,每次迭代选择产生最大Q值的两个社团合并,直至整个网络融合成一个社团。整个过程是自底向上的过程,且这个过程最终得到一个树图,即树的叶子节点表示网络中的顶点,树的每一层切分对应着网络的某个具体划分,从树图的所有层次划分中选择模块度值最大的划分作为网络的有效划分。该算法的总体时间复杂度为O(m(m+n))
设网络有n个节点,m条边,每一步合并对应的社区数目为r,组成一个r*r矩阵e,矩阵元素eij表示社区i中的节点与社区j中节点之间连边的数目在网络总变数的百分比。
主要步骤:
(1) 初始化网络,开始网络有n 个社区,初始化的eij和ai为:
(2)依次按照∆Q的最大或者最小的方向进行合并有边相连的社区对,并计算合并后的模块度增量∆Q:
(3)合并社区对以后修改对社区对称矩阵e 和社区i和j对应的行列;
(4)重复执行步骤(2)和(3),不断合并社区,直至整个网络合并成一个社区为止。
python代码如下:
class FastNewman:
def __init__(self, path):
self.G = load_graph(path)
# G = nx.read_gml('dolphins.gml')
self.A = nx.to_numpy_array(self.G) # 邻接矩阵
self.num_node = len(self.A) # 点数
self.num_edge = sum(sum(self.A)) # 边数
self.c = {} # 记录所有Q值对应的社团分布
# def merge_community(self, iter_num, detaQ, e, b):
# # 一起合并容易出bug 查询的结果I在遍历过程中 可能在已经前面某次作为J被合并了
# # 比如某次是[ 3, 11] [11, 54] 第一轮迭代中11被合并 第二轮54合并到旧的11中 会导致后面被删除 导致节点消失 需要将54合并到现在11所在位置 比较麻烦 不如一个个合并
# b_num = sum([len(i) for i in b])
# det_max = np.amax(detaQ)
#
# (I, J) = np.where(detaQ == det_max)
# print((I, J) )
# # 由于先遍历的I I中可能有多个相同值 所以合并时候因应该将J合并到I中
# # 如果将I合并到J中 后续删除删不到
# for m in range(len(I)):
# # 确保J还未被合并
# if J.tolist().index(J[m]) == m:
# # 将第J合并到I 然后将J清零
# e[I[m], :] = e[J[m], :] + e[I[m], :]
# e[J[m], :] = 0
# e[:, I[m]] = e[:, J[m]] + e[:, I[m]]
# e[:, J[m]] = 0
# b[I[m]] = b[I[m]] + b[J[m]]
#
# e = np.delete(e, J, axis=0)
# e = np.delete(e, J, axis=1)
# J = sorted(list(set(J)), reverse=True)
# for j in J:
# b.remove(b[j]) # 删除第J组社团,(都合并到I组中了)
# b_num2 = sum([len(i) for i in b])
# if b_num2 != b_num:
# print("111")
# self.c[iter_num] = b.copy()
# return e, b
def merge_community(self, iter_num, detaQ, e, b):
# 一个个合并
(I, J) = np.where(detaQ == np.amax(detaQ))
# 由于先遍历的I I中可能有多个相同值 所以合并时候因应该将J合并到I中
# 如果将I合并到J中 后续删除删不到
e[I[0], :] = e[J[0], :] + e[I[0], :]
e[J[0], :] = 0
e[:, I[0]] = e[:, J[0]] + e[:, I[0]]
e[:, J[0]] = 0
b[I[0]] = b[I[0]] + b[J[0]]
e = np.delete(e, J[0], axis=0)
e = np.delete(e, J[0], axis=1)
b.remove(b[J[0]]) # 删除第J组社团,(都合并到I组中了)
self.c[iter_num] = b.copy()
return e, b
def Run_FN(self):
e = self.A / self.num_edge # 社区i,j连边数量占总的边的比例
a = np.sum(e, axis=0) # e的列和,表示与社区i中节点相连的边占总边数的比例
b = [[i] for i in range(self.num_node)] # 本轮迭代的社团分布
Q = []
iter_num = 0
while len(e) > 1:
num_com = len(e)
detaQ = -np.power(10, 9) * np.ones((self.num_node, self.num_node)) # detaQ可能为负数,初始设为负无穷
for i in range(num_com - 1):
for j in range(i + 1, num_com):
if e[i, j] != 0:
detaQ[i, j] = 2 * (e[i, j] - a[i] * a[j])
if np.sum(detaQ + np.power(10, 9)) == 0:
break
e, b = self.merge_community(iter_num, detaQ, e, b)
a = np.sum(e, axis=0)
# 计算Q值
Qt = 0.0
for n in range(len(e)):
Qt += e[n, n] - a[n] * a[n]
Q.append(Qt)
iter_num += 1
max_Q, community = self.get_community(Q)
return max_Q, community
def get_community(self, Q):
max_k = np.argmax(Q)
community = self.c[max_k]
return Q[max_k], community
运行结果如下:
Q值:0.3806706114398422
算法执行时间0.00400090217590332
可以看到与GN算法相比,GN算法模块度略微下降,但执行时间快了很多。
代码与数据集下载