【积】louvain社团检测算法(python)代码图片双解(一)
louvain社团检测算法(python)Louvain算法共两篇,一篇为无向的代码解析,另一篇为有向的原理解析
louvain社团检测算法(python)
这里写目录标题
壹、完整代码
参考连接:
https://blog.csdn.net/qq_34356768/article/details/104888579
贰、分步解释
Louvain
分为循环迭代的两个阶段。
假设有V个节点的加权网络(可存在自环)
# coding=utf-8
import collections
import random
# 加载(顶点1,顶点2,权重)得图表示
def load_graph(path): #参数path:文件路径字符串
G = collections.defaultdict(dict) # 设置空白默认字典
with open(path) as text:
for line in text:
vertices = line.strip().split()
# .strip()删除字符串line开头结尾处得空格 # .split()按照空格分隔
# 返回一个列表,含有3个字符串元素
v_i = int(vertices[0])
v_j = int(vertices[1])
w = float(vertices[2])
G[v_i][v_j] = w
G[v_j][v_i] = w
return G
1.为网络中的每个节点分配一个社团;
每个节点具有属性:
vid
:节点编号;cid
:社团编号;k_in
:节点自环个数
class Vertex():
def __init__(self, vid, cid, nodes, k_in=0):
self._vid = vid
self._cid = cid
self._nodes = nodes #
self._kin = k_in # 结点内部的边的权重
2. 算法初始化
算法中的graph:
_G_
算法中的总边数:
_m_
有两个字典:
_cid_vertices
:用于存放社区编号,字典类型
_cid_vertices[vid]
:编号为vid的社区_vid_vertex
:用于存放顶点编号,字典类型,每个顶点进行class Vertex
实例化,赋予每个顶点各项初始属性
_vid_vertex[vid]
:编号为vid的顶点,字典类型
class Louvain():
def __init__(self, G): # 算法初始化
self._G = G # G是一个嵌套字典
self._m = 0 # 边数量
# 下面两个空字典存放信息
self._cid_vertices = {} # 需维护的关于社区的信息(社区编号,其中包含的结点编号的集合)
self._vid_vertex = {} # 需维护的关于结点的信息(结点编号,相应的Vertex实例)
for vid in self._G.keys(): # self._G.keyds()字典得键值列表
# 含vid得社区名为vid,社区用列表表示,必含有Vid
self._cid_vertices[vid] = set([vid])
# 对于顶点vid进行‘类初始化 class Vertex'
self._vid_vertex[vid] = Vertex(vid, vid, set([vid]))
# 求graph得边数量:对每个vid得邻居 neighbor ,如果neirbor>vid,则加1,避免(i,j)和(j,i)重复
self._m += sum([1 for neighbor in self._G[vid].keys() if neighbor > vid])
3. 第一阶段循环
# 属于 class Louvain():
def first_stage(self):
mod_inc = False # 用于判断算法是否可终止
visit_sequence = self._G.keys() # 需要拜访得节点列表(所有节点)
random.shuffle(list(visit_sequence)) # 转化为列表后并打算列表得排列顺序
while True:
can_stop = True # 第一阶段是否可终止
for v_vid in visit_sequence:
# 随机选择第一个节点v_vid(随机体现在shuffle),
# 其’属性_vid_vertex'是对节点实例化后,令‘属性_cid'(即节点所在得社区编号)赋值
v_cid = self._vid_vertex[v_vid]._cid
# 假设G={1102:{1221:2,1321:3},1221:{1102:2,1456:4}}
# G[1102].values()=[2,3] ,sum求和后表示含节点1102得边数目 # kin初始化为0
# k_v表示节点v_vid的所有边权重之和
k_v = sum(self._G[v_vid].values()) + self._vid_vertex[v_vid]._kin
cid_Q = {}
# G[1102].keys()=[1221,1321],即节点1102得邻居
for w_vid in self._G[v_vid].keys():
w_cid = self._vid_vertex[w_vid]._cid # 邻居所在得社区编号
if w_cid in cid_Q:
continue # 若邻居w_ci的社区在 字典cid_Q中则不操作
else:
'''
_cid_vertices[w_cid]:表示编号为w_cid的社区,k是社区w_cid的节点
G[k].values():是权重值
sum(self._G[k].values()) :节点k的所有出边的边总数
_vid_vertex[k]._kin: 表示对节点k的‘属性k_in’(自环边)
sum([k的所有边(出边+自环边)]):社区w_cid的内部边+连接边
'''
tot = sum(
[sum(self._G[k].values()) + self._vid_vertex[k]._kin \
for k in self._cid_vertices[w_cid]])
'''
v_vid节点的邻居w_vid的
v_vid的社区编号v_cid;w_vid的社区编号为w_cid
如果v_vid与其邻居w_vid的社区编号相同,则移除v_vid后计算社团的边权重之和
'''
if w_cid == v_cid:
tot -= k_v
'''
_cid_vertices[w_cid]:表示社区编号为w_cid的社区,k是社区内的节点
G[v_vid].items():G[1102].items()=dict_items([(1221, 2), (1321, 3)])
k_v_in:当遍历节点v_vid的邻居时判断k如果是w_cid社区内的节点,则求权重之和
'''
k_v_in = sum([v for k, v in self._G[v_vid].items() if k in self._cid_vertices[w_cid]])
delta_Q = k_v_in - k_v * tot / self._m # 由于只需要知道delta_Q的正负,所以少乘了1/(2*self._m)
cid_Q[w_cid] = delta_Q
cid, max_delta_Q = sorted(cid_Q.items(), key=lambda item: item[1], reverse=True)[0]
'''
参量 key:表示排序的关键字
参量 reverse=True:表示逆序,从大到小排列
lambda item :item[1]: item 是一个sorted的变量(即为字典的items迭代量),形式为(key,value)的二元组
item[1],即表示上述二元组的value
整体含义为:按照字典中的value,从大到小排序
'''
if max_delta_Q > 0.0 and cid != v_cid:
self._vid_vertex[v_vid]._cid = cid
self._cid_vertices[cid].add(v_vid)
self._cid_vertices[v_cid].remove(v_vid)
can_stop = False
mod_inc = True
if can_stop:
break
return mod_inc
4. 第一阶段文字图片双解释
随机选择random.shuffle(list(visit_sequence))
while True
1.can_stop:用于判断第一阶段是否终止
2.for v_vid in 考虑G中所有的节点:
2.1 选择一个顶点v_vid,得到其属性:1️⃣社团编号v_cid;2️⃣连接到v_vid的边的权重之和k_v
2.2 cid_Q={} 空字典,用于存储 模块度增量
2.3 for w_vid in 考虑顶点v_vid的所有邻居节点:
2.3.1 can_stop=True
2.3.2 (也就是计算将节点v_vid移动到其邻居所在的社区后,计算模块度增量)
2.3.4 选择邻居w_vid,得到其属性:
2.3.4.1 1️⃣社团编号w_cid
2.3.4.2 2️⃣判断社团编号w_cid是否是cid_Q的键;
2.3.4.2.1 ✔️continue
2.3.4.2.2 ✖️将原节点v_vid移除所属社团,加入其邻居所属的社团,评估模块度收益
2.3.4.2.2.1🥇tot:邻居所在的社团w_cid内的所有节点的边的权重之和以及节点自环的权重如果w_cid==v_cid(即属于同一个社团),tot-=k_v (节点v_vid移除社团后的权重变化)
2.3.4.2.2.2🥈k_v_in:表示‘移动节点v_vid’连接移动到社团w_cid内的节点的边的权重之和
2.3.4.2.2.3 🥉delta_Q:模块度增量的表达式,并将其结果存入字典cid_Q
2.4.& 1102的邻居: 1221 1321
& 移动后的 Δ Q \Delta Q ΔQ: Δ Q 1 \Delta Q_1 ΔQ1 Δ Q 2 \Delta Q_2 ΔQ2
2.5 cid, max_delta_Q :是对上述的[ Δ Q 1 \Delta Q_1 ΔQ1, Δ Q 2 \Delta Q_2 ΔQ2]从大到小排序后,获得的对应社区号以及模块度增量
2.6 if max_delta_Q > 0.0 and cid != v_cid:( 将v_vid移动到cid社区后,更新相关属性信息)
2.6.1 1️⃣v_vid的社区编号v_cid更新
2.6.2 2️⃣社区[cid]的增加新成员
2.6.3 3️⃣社区[v_cid]删除旧成员v_vid
2.6.4 4️⃣can_stop=False 当下只对一个节点移动了,继续寻找,第一阶段不停止
2.6.5 5️⃣mod_inc = True 算法可以终止了
3.if can_stop: break
参数解释:
._m
=10
Δ Q = s i , i n 2 E − ∑ t o t ⋅ s i 2 E 2 \Delta Q=\frac{s_{i,in}}{2E}-\frac{\sum_{tot}\cdot s_i}{2E^2} ΔQ=2Esi,in−2E2∑tot⋅si
其中 s i , i n s_{i,in} si,in=k_v_in:节点i连接到社团m内节点的边权重和
∑ t o t \sum_{tot} ∑tot=tot:连接到社团m内节点的边的权重总和
E E E=.m:表示网络中边权重总和
初始化的时候每个节点为独立的社团,如上图所示,用5个颜色表示5个社团
第一次选择移动节点v_vid=1321 |
1321移动到邻居1102社区,1321的社区编号为1102
第二次选择移动节点v_vid=1102 |
邻居节点有:1321,1221
- 首先1321的社区编号为1102,社区1102中有{1102,1321},k_v=5
, t o t 1102 = s u m ( s u m [ 2 , 3 ] , [ 3 ] ) = 8 tot_{1102}=sum(sum[2,3],[3])=8 tot1102=sum(sum[2,3],[3])=8, t o t 1102 = t o t 1102 − k v = 3 tot_{1102}=tot_{1102}-k_v=3 tot1102=tot1102−kv=3,
s 1102 , i n = 3 s_{1102,in}=3 s1102,in=3, s 1102 = 2 + 3 = 5 s_{1102}=2+3=5 s1102=2+3=5,
2 ⋅ 10 Δ Q 1321 = 3 − 3 ⋅ 5 10 = 1.5 > 0 2\cdot 10\Delta Q_{1321}=3-\frac{3\cdot 5}{10}=1.5>0 2⋅10ΔQ1321=3−103⋅5=1.5>0- 其次1221的社区编号为,社区1221中只有节点1221,k_v=1+2+4=7,
t o t 1221 = 7 , s 1102 , i n = 2 , s 1102 = 5 tot_{1221}=7,s_{1102,in}=2,s_{1102}=5 tot1221=7,s1102,in=2,s1102=5
2 ⋅ 10 Δ Q 1221 = 2 − 7 ⋅ 5 10 = − 0.5 < 0 2\cdot 10 \Delta Q_{1221}=2-\frac{7\cdot 5}{10}=-0.5<0 2⋅10ΔQ1221=2−107⋅5=−0.5<0
1102移除1102社区后又重新加入1102社区,即不变动
第三次选择移动节点v_vid=1221 |
邻居节点有:1102,1421,1456
- 首先1102的社区编号为1102.社区中有{1102,1321},k_v=7
t o t 1102 = 8 , s 1221 , i n = 2 , s 1221 = 7 tot_{1102}=8,s_{1221,in}=2,s_{1221}=7 tot1102=8,s1221,in=2,s1221=7
2 ⋅ 10 Δ Q = 2 − 8 ⋅ 7 10 = − 3.6 < 0 2\cdot 10 \Delta Q=2-\frac{8\cdot 7}{10}=-3.6<0 2⋅10ΔQ=2−108⋅7=−3.6<0- 其次1421的社区编号为1421,社区中只有1421,k_v=1
t o t 1421 = 1. s 1221 , i n = 1 tot_{1421}=1.s_{1221,in}=1 tot1421=1.s1221,in=1
2 ⋅ 10 Δ Q = 1 − 1 ∗ 1 10 = 0.9 > 0 2\cdot 10 \Delta Q=1-\frac{1*1}{10}=0.9>0 2⋅10ΔQ=1−101∗1=0.9>0- 最后1456的社区编号为1456,社区中只有1456,k_v=4
t o t 1456 = 4 , s 1221 , i n = 4 tot_{1456}=4,s_{1221,in}=4 tot1456=4,s1221,in=4
2 ⋅ 10 Δ Q = 4 − 4 ⋅ 4 10 = 2.4 > 0 2\cdot 10 \Delta Q=4-\frac{4\cdot 4}{10}=2.4>0 2⋅10ΔQ=4−104⋅4=2.4>0
1221移除1221社区后加入1456社区
第四次选择移动节点v_vid=1456 |
显然1456是不移动的
第五次选择移动节点v_vid=1421 |
1421的邻居节点为1221
- 1221的社区编号为1456,社区中有{1221,1456}k_v=1
t o t 1456 = s u m ( s u m [ 1 , 2 , 4 ] , [ 4 ] ) = 11 , s 1421 , i n = 1 tot_{1456}=sum(sum[1,2,4],[4])=11,s_{1421,in}=1 tot1456=sum(sum[1,2,4],[4])=11,s1421,in=1
2 ⋅ 10 Δ Q = 1 − 11 ⋅ 1 10 = − 0.1 < 0 2\cdot 10 \Delta Q=1-\frac{11\cdot 1}{10}=-0.1<0 2⋅10ΔQ=1−1011⋅1=−0.1<0
因为模块度增量减少,所以不移动
最终得到3个社区:社区1102{1102,1321}+社区1456{1221,1456}+社区1421{1421}
5.第二个阶段
它在于构建一个新网络,其节点属于第一阶段发现的社团。因此,新节点之间的连边权重由相应的两个社团的节点连边权重和给定。同以社团内的节点会导致新网络中社团出现自身环。
算法:
- 搭建两个新字典,用于存放新社团和新节点
- 对旧社团遍历构造新节点:
2.1 如果旧社团为空,跳过
2.2 以‘旧社团编号’作为新节点的节点编号、新社团编号
2.3 对新节点的‘node’属性,利用set()集合表示,将旧社团的节点update进去
2.4 对新节点的‘kin’属性,1️⃣包含就社团节点中的所有kin属性,2️⃣包含旧社团中节点的所有内边的权重之和- 对新节点放入存放字典,并将每个新节点当作独立的新社团存放入字典
- 搭建新网络(在新节点之间添加边的权重),设置默认字典G
- 对旧社团中的(社团编号cid1,vertices1[节点列表])遍历
5.1 如果旧社团cid1为空,则跳过
5.2 对旧社团中(社团编号cid2,vertices2[节点列表])遍历
5.2.1 if 避免新边重复、自环以及避免第二个新节点为空的情形
5.2.2 设置新边的初始权重为0
5.2.3 用vid 对旧社团cid1中的节点列表 vertices1遍历
5.2.3.1 用(wid,v)对节点vid 的邻居vid.items()遍历
5.2.3.1.1 判断wid 如果是旧社团cid2中的节点,则将权重v增加
5.2.4 如果新边权重不为0,则将这条新边记录到图G中- 将新节点字典、新社团字典、新图G更新
# calss louvain():
def second_stage(self):
# 搭建两个新字典,用于存放新社团和新节点
cid_vertices = {}
vid_vertex = {}
'''
_cid_vertices:字典,键为社区编号,值为对应社区中的节点构成的列表
_cid_vertices={1102:[1102,1321],1456:[1221,1456],1421:[1421],1321:[],1221:[]}
'''
for cid, vertices in self._cid_vertices.items():
if len(vertices) == 0:
continue # 如果是空社区跳过
# 将非空社团的作为新节点,新节点编号为社区编号,社区编号不变,节点设置为空集
new_vertex = Vertex(cid, cid, set())
# 更新新节点的各种属性
for vid in vertices:
# 新节点的_nodes属性中增加社团中的节点,update类似于append
new_vertex._nodes.update(self._vid_vertex[vid]._nodes)
# 新节点的_kin属性,是包含社团中节点的所有自环
new_vertex._kin += self._vid_vertex[vid]._kin
# 新节点的_kin属性,是包含社团中节点的内边的权重之和(边不重复)
for k, v in self._G[vid].items():
if k in vertices:
new_vertex._kin += v / 2.0
# 前面部分搭建完新节点属性后,对每个新节点作为一个新的社团,且搭建新节点存放
cid_vertices[cid] = set([cid])
vid_vertex[cid] = new_vertex
# 构建新网络
G = collections.defaultdict(dict)
# 在原社团字典中,搭建新网络的连边,
$ 节点1(cid1),节点2(cid2)都是是原社团中的社团编号
for cid1, vertices1 in self._cid_vertices.items():
if len(vertices1) == 0:
continue # 如果社团为空,则跳过
for cid2, vertices2 in self._cid_vertices.items():
if cid2 <= cid1 or len(vertices2) == 0:
continue # 如果cid2与cid1相同或者cid2社团为空,则跳过,<是为避免重复
'''
新节点边的初始权重为0,取社团cid1的节点vid,再取vid的邻居wid及权重v,
判断wid如果在社团cid2中,则新边权重增加v
'''
edge_weight = 0.0
for vid in vertices1:
for wid, v in self._G[vid].items():
if wid in vertices2:
edge_weight += v
# 如果新边权重不为0,则将这条新边记录到图G中
if edge_weight != 0:
G[cid1][cid2] = edge_weight
G[cid2][cid1] = edge_weight
# 新构建的节点、新社团划分、新图G更新
self._cid_vertices = cid_vertices
self._vid_vertex = vid_vertex
self._G = G
构造新节点 |
新节点属性名称 | 1102属性值 | 1456属性值 | 1421属性值 |
---|---|---|---|
_vid | 1102 | 1456 | 1421 |
_cid | 1102 | 1456 | 1421 |
_nodes | (1102,1321) | (1221,1456) | (1421) |
_kin | 3 | 4 | 0 |
构造新网络,遍历三个旧社团 |
第一个旧社团cid1=1102 |
旧社团1102中有两个节点,vid:1102,1321。
选取另一个旧社团(cid2>cid1,且不为空)li,cid2:1456,1421.初始权重G[1102,1456]=0,G[1102,1421]=0
- vid =1102,邻居1321即不是社团1456中的节点,也不是社团1421的节点,跳过
- vid =1321,邻居有[1102,1221]
- 1102,既不是社团1456中的节点,也不是社团1421的节点,跳过
- 1221,是社团1456中的节点,则G[1102,1456]=0+2=2
第二个旧社团cid1=1456 |
旧社团1456中有两个节点,vid:1221,1456
选取另一个旧社团(cid2>cid1,且不为l空),cid2,没得选,跳过
第三个旧社团cid1=1421 |
旧社团中只有1个节点,vid:1421
选取另一个旧社团(cid2>cid1,且不为l空),cid2:1456。初始权重G[1421,1456]=0
- vid=1421,邻居1221是社团1456中的节点,则G[1421,1456]=0+1=1
6.获得社团
# class louvain():
def get_communities(self):
communities = []
# vertices是社团中的节点列表
for vertices in self._cid_vertices.values():
# 如果社团不为空,则用vid遍历社团中的节点,将每个节点中node属性放入集合C中,再将集合放入列表
if len(vertices) != 0:
c = set()
for vid in vertices:
c.update(self._vid_vertex[vid]._nodes)
communities.append(c)
return communities
7.两个阶段进行循环
# class louvain():
def execute(self):
iter_time = 1 # 迭代次数
while True:
iter_time += 1
mod_inc = self.first_stage() # 第一阶段执行,
if mod_inc: # 若返回mod_inc=True,则执行第二阶段
self.second_stage()
else: # 否则,跳出循环
break
return self.get_communities() # 返回得到的社团
8. 主函数进行调用运行
if __name__ == '__main__':
G = load_graph('s.txt')
algorithm = Louvain(G)
communities = algorithm.execute()
# 按照社区大小从大到小排序输出
communities = sorted(communities, key=lambda b: -len(b)) # 按社区大小排序
count = 0
for communitie in communities:
count += 1
print("社区", count, " ", communitie)
9整体代码
# coding=utf-8
import collections
import random
def load_graph(path):
G = collections.defaultdict(dict) # 设置空白默认字典,无向图
DG=collections.defaultdict(dict) # 设置空白默认字典,有向图
with open(path) as text:
for line in text:
vertices = line.strip().split()
# .strip()删除字符串line开头结尾处得空格 # .split()按照空格分隔
# 返回一个列表,含有3个字符串元素
v_i = int(vertices[0])
v_j = int(vertices[1])
w = float(vertices[2])
G[v_i][v_j] = w
# G[v_j][v_i] = w
return G
class Vertex():
def __init__(self, vid, cid, nodes, k_in=0):
self._vid = vid
self._cid = cid
self._nodes = nodes
self._kin = k_in # 结点内部的边的权重
class Louvain():
def __init__(self, G):
self._G = G
self._m = 0 # 边数量
# 下面两个空字典存放信息
self._cid_vertices = {} # 需维护的关于社区的信息(社区编号,其中包含的结点编号的集合)
self._vid_vertex = {} # 需维护的关于结点的信息(结点编号,相应的Vertex实例)
for vid in self._G.keys():
self._cid_vertices[vid] = set([vid])
self._vid_vertex[vid] = Vertex(vid, vid, set([vid]))
self._m += sum([1 for neighbor in self._G[vid].keys() ])
def first_stage(self):
mod_inc = False # 用于判断算法是否可终止
visit_sequence = self._G.keys()
random.shuffle(list(visit_sequence))
while True:
can_stop = True # 第一阶段是否可终止
for v_vid in visit_sequence:
v_cid = self._vid_vertex[v_vid]._cid
k_v = sum(self._G[v_vid].values()) + self._vid_vertex[v_vid]._kin
cid_Q = {}
for w_vid in self._G[v_vid].keys():
w_cid = self._vid_vertex[w_vid]._cid
if w_cid in cid_Q:
continue
else:
tot = sum(
[sum(self._G[k].values()) + self._vid_vertex[k]._kin for k in self._cid_vertices[w_cid]])
if w_cid == v_cid:
tot -= k_v
k_v_in = sum([v for k, v in self._G[v_vid].items() if k in self._cid_vertices[w_cid]])
delta_Q = k_v_in - k_v * tot / self._m # 由于只需要知道delta_Q的正负,所以少乘了1/(2*self._m)
cid_Q[w_cid] = delta_Q
cid, max_delta_Q = sorted(cid_Q.items(), key=lambda item: item[1], reverse=True)[0]
if max_delta_Q > 0.0 and cid != v_cid:
self._vid_vertex[v_vid]._cid = cid
self._cid_vertices[cid].add(v_vid)
self._cid_vertices[v_cid].remove(v_vid)
can_stop = False
mod_inc = True
if can_stop:
break
return mod_inc
def second_stage(self):
cid_vertices = {}
vid_vertex = {}
for cid, vertices in self._cid_vertices.items():
if len(vertices) == 0:
continue
new_vertex = Vertex(cid, cid, set())
for vid in vertices:
new_vertex._nodes.update(self._vid_vertex[vid]._nodes)
new_vertex._kin += self._vid_vertex[vid]._kin
for k, v in self._G[vid].items():
if k in vertices:
new_vertex._kin += v / 2.0
cid_vertices[cid] = set([cid])
vid_vertex[cid] = new_vertex
G = collections.defaultdict(dict)
for cid1, vertices1 in self._cid_vertices.items():
if len(vertices1) == 0:
continue
for cid2, vertices2 in self._cid_vertices.items():
if cid2 <= cid1 or len(vertices2) == 0:
continue
edge_weight = 0.0
for vid in vertices1:
for k, v in self._G[vid].items():
if k in vertices2:
edge_weight += v
if edge_weight != 0:
G[cid1][cid2] = edge_weight
G[cid2][cid1] = edge_weight
self._cid_vertices = cid_vertices
self._vid_vertex = vid_vertex
self._G = G
def get_communities(self):
communities = []
for vertices in self._cid_vertices.values():
if len(vertices) != 0:
c = set()
for vid in vertices:
c.update(self._vid_vertex[vid]._nodes)
communities.append(c)
return communities
def execute(self):
iter_time = 1
while True:
iter_time += 1
mod_inc = self.first_stage()
if mod_inc:
self.second_stage()
else:
break
return self.get_communities()
if __name__ == '__main__':
G = load_graph('s.txt')
algorithm = Louvain(G)
communities = algorithm.execute()
# 按照社区大小从大到小排序输出
communities = sorted(communities, key=lambda b: -len(b)) # 按社区大小排序
count = 0
for communitie in communities:
count += 1
print("社区", count, " ", communitie)
sql=“select top 1 [id] from [youtable] where id<”&Request.QueryString(“id”)
下一篇:有向图中的louvain社区检测(二)
更多推荐
所有评论(0)