图结构使用 Louvain 社区检测算法进行分组
图结构使用 Louvain 社区检测算法进行分组
flyfish
Louvain 算法是一种基于模块度最大化的社区检测算法,核心目标是在复杂网络中找到“内部连接紧密、外部连接稀疏”的社区结构。它的优势在于高效性(可处理百万级节点的大规模网络)和近似最优解,通过“局部优化→社区聚合”的迭代流程实现模块度的逐步提升,最终得到稳定的社区划分。
一、目标:最大化“模块度(Modularity, Q)”
要理解 Louvain 算法,首先必须明确其优化目标——模块度 Q。模块度是衡量“社区划分质量”的量化指标,它描述了“网络实际社区内的边密度”与“随机网络中相同社区内的边密度”的差异:
- 若 Q > 0:实际社区内边更密集,划分有效;
- Q 越大(最大值约 0.7):社区结构越显著。
1. 模块度 Q 的数学定义(无向无权网络)
对于无向无权网络,模块度公式表示为:
Q=12m∑i,j(Aij−kikj2m)δ(ci,cj)
Q = \frac{1}{2m} \sum_{i,j} \left( A_{ij} - \frac{k_i k_j}{2m} \right) \delta(c_i, c_j)
Q=2m1i,j∑(Aij−2mkikj)δ(ci,cj)
各符号含义:
符号 | 含义解释 |
---|---|
AijA_{ij}Aij | 网络的邻接矩阵元素:若节点 iii 和 jjj 之间有边,Aij=1A_{ij}=1Aij=1;否则 Aij=0A_{ij}=0Aij=0。 |
kik_iki | 节点 iii 的度(即与节点 iii 直接相连的边数)。 |
mmm | 网络的总边数(m=12∑i,jAijm = \frac{1}{2}\sum_{i,j} A_{ij}m=21∑i,jAij,无向图边计数不重复)。 |
cic_ici | 节点 iii 所属的社区标签(如 ci=0c_i=0ci=0 表示节点 iii 在社区 0 中)。 |
δ(x,y)\delta(x,y)δ(x,y) | 克罗内克函数:若 x=yx=yx=y(节点 iii 和 jjj 同属一个社区),δ=1\delta=1δ=1;否则 δ=0\delta=0δ=0。 |
2. 模块度变化量 ΔQ:节点移动的判断依据
Louvain 算法不直接计算全局 Q,而是通过局部模块度变化量 ΔQ 决定节点的移动——即“将某个节点 uuu 从当前社区移动到相邻节点 vvv 的社区后,模块度的变化值”。只有当 ΔQ > 0 时,移动才会提升全局模块度,才会执行。
节点 uuu 移动到社区 CCC 的 ΔQ 公式(简化版,核心逻辑):
ΔQ=(∑in+ku,C2m−(∑tot+ku2m)2)−(∑in2m−(∑tot2m)2−(ku2m)2)
\Delta Q = \left( \frac{\sum_{in} + k_{u,C}}{2m} - \left( \frac{\sum_{tot} + k_u}{2m} \right)^2 \right) - \left( \frac{\sum_{in}}{2m} - \left( \frac{\sum_{tot}}{2m} \right)^2 - \left( \frac{k_u}{2m} \right)^2 \right)
ΔQ=(2m∑in+ku,C−(2m∑tot+ku)2)−(2m∑in−(2m∑tot)2−(2mku)2)
各符号含义(聚焦局部社区 CCC 和节点 uuu):
符号 | 含义解释 |
---|---|
∑in\sum_{in}∑in | 社区 CCC 内部所有边的总权重(无权网络中即边数)。 |
ku,Ck_{u,C}ku,C | 节点 uuu 与社区 CCC 内所有节点之间的边的总权重(即 uuu 到 CCC 的“连接强度”)。 |
∑tot\sum_{tot}∑tot | 社区 CCC 内所有节点的度的总和(即社区 CCC 与外部节点连接的“总能力”)。 |
kuk_uku | 节点 uuu 的总度(同前)。 |
二、Louvain 算法的核心流程:两阶段迭代
Louvain 算法通过反复执行两个阶段,逐步聚合社区、提升模块度,直到模块度无法再优化为止。整个过程是“自下而上”的社区合并(从每个节点单独为一个社区,到最终聚合为少数几个大社区)。
阶段 1:局部移动(Local Moving Phase)—— 优化单个节点的社区归属
目标:对每个节点单独调整社区,最大化局部 ΔQ,直到网络中所有节点都无法通过移动提升模块度。
具体步骤:
- 初始状态:将网络中每个节点都视为一个独立的社区(即初始社区数 = 节点数)。
- 遍历节点:按随机顺序(或固定顺序)遍历每个节点 uuu。
- 尝试移动:对节点 uuu 的每个相邻节点 vvv(即与 uuu 有边相连的节点),计算“将 uuu 移动到 vvv 所属社区 CCC”的 ΔQ。
- 选择最优移动:
- 若所有 ΔQ 均 ≤ 0:节点 uuu 保持在原社区;
- 若存在 ΔQ > 0:选择 ΔQ 最大的社区,将 uuu 移动过去。
- 重复迭代:重复步骤 2-4,直到遍历所有节点后,没有任何节点的移动能提升模块度(此阶段终止)。
阶段 2:社区聚合(Community Aggregation Phase)—— 构建“超级节点”网络
目标:将阶段 1 得到的每个社区,聚合为一个“超级节点”,缩小网络规模,为下一轮迭代做准备。
具体步骤:
- 创建超级节点:每个社区对应一个新的“超级节点”(例如,社区 0 聚合为超级节点 C0C_0C0,社区 1 聚合为 C1C_1C1 等)。
- 计算超级节点间的边权重:
- 若原网络中,社区 CaC_aCa 的节点与社区 CbC_bCb 的节点之间有 www 条边(无权网络中 www 是边数,加权网络中 www 是边权重总和),则在新网络中,超级节点 CaC_aCa 与 CbC_bCb 之间添加一条权重为 www 的边。
- 注意:社区内部的边(同一社区节点间的边)会被“折叠”到超级节点内部,不计入超级节点间的边(因为后续迭代只关注社区间的连接)。
- 生成新网络:新网络的节点是超级节点,边是超级节点间的聚合边权重,网络规模大幅缩小。
迭代终止条件
将阶段 2 生成的新网络,再次代入阶段 1(局部移动) 和阶段 2(社区聚合),反复迭代。直到某一轮迭代后,模块度不再提升(即阶段 1 无法通过移动节点优化 ΔQ,阶段 2 也无法生成更优的超级节点网络),算法终止。
import networkx as nx
import community as community_louvain
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors# 设置中文字体
plt.rcParams["font.family"] = ["SimHei"] # Windows系统# 1. 构建水果相关的有向无环图(DAG)
G = nx.DiGraph()# 添加节点
nodes = ["苹果", "香蕉", "橙子", "草莓", # 水果"红色", "黄色", "橙色", "甜味", "酸味", # 属性"颜色", "味道", "浆果", "核果" # 类别
]
G.add_nodes_from(nodes)# 添加有向边(确保无环)
edges = [("苹果", "红色"), ("苹果", "甜味"), ("苹果", "核果"),("香蕉", "黄色"), ("香蕉", "甜味"),("橙子", "橙色"), ("橙子", "酸味"),("草莓", "红色"), ("草莓", "甜味"), ("草莓", "浆果"),("红色", "颜色"), ("黄色", "颜色"), ("橙色", "颜色"),("甜味", "味道"), ("酸味", "味道")
]
G.add_edges_from(edges)# 可视化原始有向图
plt.figure(figsize=(10, 6))
pos = nx.spring_layout(G, seed=42)
# 原始图使用红色系为主的颜色
nx.draw_networkx_nodes(G, pos, node_size=800, node_color='lightcoral')
nx.draw_networkx_labels(G, pos, font_size=12, font_family=plt.rcParams["font.family"], font_color='white')
nx.draw_networkx_edges(G, pos, arrowstyle="->", arrowsize=10)
plt.title("水果相关的有向无环图(DAG)")
plt.show()# 2. 转换为无向图以适配Louvain算法
G_undirected = G.to_undirected()# 3. 计算社区划分
partition = community_louvain.best_partition(G_undirected)# 输出分组结果
print("社区划分结果:")
for community_id in set(partition.values()):members = [node for node, id in partition.items() if id == community_id]print(f"社区 {community_id}:{members}")# 4. 可视化社区划分 - 使用自定义颜色列表(避免黄色,增加红色系)
plt.figure(figsize=(10, 6))# 自定义颜色列表(选择与白色文字对比度高的颜色,替换黄色为红色系)
custom_colors = ['#FF6B6B', # 浅红色'#4ECDC4', # 青绿色'#45B7D1', # 天蓝色'#FFA07A', # 浅橙色'#98D8C8' # 淡青色
]# 根据社区数量选择合适的颜色
num_communities = max(partition.values()) + 1
used_colors = custom_colors[:num_communities]# 绘制节点(使用自定义颜色)
nx.draw_networkx_nodes(G_undirected, pos, partition.keys(),node_size=800, node_color=[used_colors[id] for id in partition.values()])# 绘制标签(白色文字)
nx.draw_networkx_labels(G_undirected, pos, font_size=12, font_family=plt.rcParams["font.family"],font_color='white')# 绘制边
nx.draw_networkx_edges(G_undirected, pos, alpha=0.5)plt.title("Louvain算法社区划分结果")
plt.show()
为何 Louvain 算法高效且实用?
- 低时间复杂度:每轮迭代的时间复杂度约为 O(n)O(n)O(n)(nnn 是节点数),即使是百万级节点的网络,也能快速运行(这是它比传统全局优化算法(如谱聚类)更实用的核心原因)。
- 近似最优解:虽然是局部优化,但通过多轮迭代的社区聚合,最终能逼近全局最大模块度(在大多数真实网络中,效果接近最优)。
- 适配无向网络:标准 Louvain 算法仅支持无向网络(加权/无权均可);若处理有向网络(水果 DAG),需先将其转换为无向网络(如
G.to_undirected()
)。
结合代码,对应算法原理
import networkx as nx
import community as community_louvain
import matplotlib.pyplot as plt# 1. 构建水果DAG(有向)
G = nx.DiGraph()
nodes = ["苹果", "香蕉", "橙子", "草莓", "红色", "黄色", "橙色", "甜味", "酸味", "颜色", "味道", "浆果", "核果"]
G.add_nodes_from(nodes)
edges = [("苹果", "红色"), ("苹果", "甜味"), ("苹果", "核果"), ...] # 省略部分边
G.add_edges_from(edges)# 2. 转换为无向图 → 适配标准Louvain算法(仅支持无向网络)
G_undirected = G.to_undirected() # 关键:有向边转为无向边,确保A_ij=A_ji# 3. Louvain算法核心:community_louvain.best_partition()
partition = community_louvain.best_partition(G_undirected) # 内部执行完整的两阶段迭代
代码与算法原理的对应:
-
无向图转换(G.to_undirected()):
标准 Louvain 算法基于无向图的邻接矩阵 AijA_{ij}Aij(满足 Aij=AjiA_{ij}=A_{ji}Aij=Aji),因此必须将你的有向 DAG 转为无向图,否则无法计算节点度 kik_iki 和总边数 mmm。 -
community_louvain.best_partition() 内部逻辑:
这个函数封装了 Louvain 算法的完整迭代流程:- 初始状态:为每个水果/属性节点(如“苹果”“红色”“颜色”)分配独立社区 ID(例如,“苹果”=0,“香蕉”=1,…,“核果”=12)。
- 阶段 1(局部移动):
遍历每个节点(如“苹果”),计算其相邻节点(“红色”“甜味”“核果”)所属社区的 ΔQ,选择 ΔQ 最大的社区移动。例如:- “苹果”与“红色”相连,计算将“苹果”移入“红色”社区的 ΔQ;
- 若 ΔQ > 0,且是所有相邻社区中最大的,则“苹果”与“红色”归为同一社区。
- 阶段 2(社区聚合):
将阶段 1 得到的社区(如“苹果-红色-草莓”为一个社区,“香蕉-黄色”为另一个社区)聚合为超级节点,构建新的小网络,再次执行阶段 1。 - 迭代终止:直到模块度不再提升,输出最终的社区划分(即
partition
字典,键是节点,值是社区 ID)。
-
输出结果(社区划分):
打印的“社区 0:[苹果, 红色, 草莓],社区 1:[香蕉, 黄色]…”,正是 Louvain 算法通过多轮两阶段迭代后,最大化模块度得到的结果——这些社区内部的节点连接更紧密(如“苹果-红色”“草莓-红色”有边),外部连接更稀疏。