尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
GN算法实战:边介数社区划分与性能优化
1. 从一个真实需求说起为什么要做社区划分社交网络分析里有个特别经典的问题给你一张几万甚至几十万个节点的关系图你怎么把里面“抱团”的人找出来这个“抱团”在学术上叫社区结构通俗点说就是一群人内部联系紧密、跟外部联系稀疏。比如一个公司里市场部的人互相加好友、频繁互动但跟研发部的人几乎没交集那市场部和研发部就是两个社区。我最早接触这个需求是帮一个做社群运营的朋友分析用户互动数据。他手里有一批用户之间的私信、评论、点赞记录想搞清楚这批人到底分成了几个小圈子每个圈子的核心人物是谁。当时我第一反应是用K-Means或者Louvain但K-Means需要预先指定簇数量而且它假设簇是球形的社交网络这种图结构根本不满足Louvain虽然快但它是基于模块度优化的贪心算法对分辨率参数敏感小社区容易被合并掉。后来我选了GN算法Girvan-Newman原因很简单它不依赖任何先验的社区数量而且它的核心思想——边介数——非常直观能给出一个层次化的划分过程你可以根据实际业务需求在任意层次切一刀。这篇文章就是把我从零实现GN算法的完整过程拆开讲。包括边介数到底怎么算、为什么删边而不是删点、层次划分怎么落地、复杂度怎么优化、实测中遇到哪些坑。如果你手头正好有一张社交关系图需要做社区发现或者你在学复杂网络分析想找个能跑通的实战项目这篇内容可以直接抄作业。提示GN算法是2002年由Girvan和Newman提出的经典算法虽然时间复杂度偏高但它的思想是很多后续算法的基础。理解GN等于理解了社区发现领域的一半。2. GN算法的核心逻辑边介数为什么能切出社区2.1 边介数的直觉理解先把这个概念说透。边介数Edge Betweenness的定义是网络中所有最短路径中经过某条边的路径数量占该对节点间最短路径总数的比例之和。公式写出来是这样对于边 ( e )它的介数 ( B(e) \sum_{s \neq t} \frac{\sigma_{st}(e)}{\sigma_{st}} )其中 ( \sigma_{st} ) 是节点 ( s ) 到 ( t ) 的最短路径总数( \sigma_{st}(e) ) 是这些最短路径中经过边 ( e ) 的数量。这个公式看着唬人但直觉特别简单如果一条边被很多对节点之间的最短路径共用说明它是“交通要道”。在社交网络里社区内部的边通常只在社区内部被使用而连接两个社区的“桥边”会被大量跨社区的最短路径经过所以桥边的介数会异常高。我打个比方。想象一个城市被一条河分成东西两半河上只有一座桥。所有从东到西的车辆都必须走这座桥那这座桥的“介数”就极高。GN算法的思路就是找到这座桥把它拆掉城市就自然分成了两个独立的区域。拆完之后重新计算剩下的桥继续拆直到每个区域内部足够紧密。2.2 为什么删边而不是删点很多人第一次接触GN会问既然要找社区为什么不直接删掉那些连接不同社区的关键节点原因在于节点承载的信息比边多。一个节点可能同时属于多个社区比如一个跨部门协作的负责人删掉它会丢失这个节点在所有社区里的角色信息。而边只表示两个节点之间的关系强度删掉一条桥边两个节点本身还在只是它们之间的直接联系被切断了这更符合“社区划分”的语义——我们是在切分关系不是在消灭个体。另外从算法稳定性看删边是一个渐进过程每一步只影响局部结构而删点会导致整个图的连通性发生突变后续的介数计算会剧烈波动。GN算法选择删边本质上是在做一种层次聚类每次删掉介数最高的边图的连通分量数量增加形成一棵层次树树状图。你可以在树的任意高度切一刀得到不同粒度的社区划分。2.3 算法完整流程拆解把GN算法的步骤写清楚计算所有边的介数。这是最耗时的部分需要对每一对节点求最短路径并累加经过每条边的路径数。找到介数最高的边删除它。如果有多条边介数相同可以任选一条或者按某种规则批量删除。重新计算剩余边的介数。注意删边后图的结构变了最短路径也会变所以必须重新算不能复用。重复步骤2和3直到图中没有边为止。在每一步记录当前的连通分量最终得到一棵层次划分树。根据模块度Modularity最大化的原则选择最优的划分层次。这里有个关键点模块度是评价社区划分质量的指标GN算法本身不直接优化模块度但我们可以用模块度来从层次树里挑出最佳切分点。模块度的计算公式是( Q \frac{1}{2m} \sum_{ij} \left( A_{ij} - \frac{k_i k_j}{2m} \right) \delta(c_i, c_j) )其中 ( m ) 是总边数( A_{ij} ) 是邻接矩阵( k_i ) 是节点 ( i ) 的度( c_i ) 是节点 ( i ) 所属社区( \delta ) 是克罗内克函数。Q值越大社区结构越明显。实际跑的时候我会在每次删边后计算当前连通分量对应的Q值记录最大值对应的划分结果。注意GN算法的复杂度是 ( O(m^2 n) )( m ) 为边数( n ) 为节点数对于大规模网络非常慢。后面我会讲几种优化思路但如果你只是做几千个节点以内的分析原版GN完全够用。3. 手把手实现从邻接表到层次划分树3.1 数据结构选型与图存储实现GN算法第一步是把图存好。社交网络通常是稀疏图用邻接矩阵会浪费大量空间所以我选邻接表。Python里可以用字典套集合graph {node: set(neighbors)}。这种结构在删边时特别方便直接graph[u].remove(v)和graph[v].remove(u)就行。但有个细节要注意边介数的计算需要频繁查询任意两点间的最短路径如果每次都用BFS重新搜效率太低。我的做法是在每一轮介数计算前先对每个节点做一次BFS生成一棵最短路径树Shortest Path Tree然后自底向上累加边介数。这个技巧来自Brandes在2001年提出的算法能把单轮介数计算的复杂度从 ( O(nm) ) 降到 ( O(nm) ) 但常数小很多实际跑起来快好几倍。具体来说对每个源节点 ( s )BFS遍历得到每个节点的距离 ( d[v] ) 和从 ( s ) 到 ( v ) 的最短路径数量 ( \sigma[v] )。按距离从大到小处理节点对于每个节点 ( v )把它累积的“流量”按比例分配给它的前驱节点对应的边。最终每条边得到的流量就是它对所有源节点的介数贡献。这个流程我写成伪代码def edge_betweenness(graph): betweenness {edge: 0 for edge in all_edges(graph)} for s in graph.nodes(): # BFS dist {s: 0} sigma {s: 1} predecessors {v: [] for v in graph.nodes()} queue [s] order [] while queue: u queue.pop(0) order.append(u) for v in graph[u]: if v not in dist: dist[v] dist[u] 1 queue.append(v) if dist[v] dist[u] 1: sigma[v] sigma[u] predecessors[v].append(u) # 自底向上累加 delta {v: 0 for v in graph.nodes()} for w in reversed(order): for v in predecessors[w]: c (sigma[v] / sigma[w]) * (1 delta[w]) betweenness[(v, w)] c delta[v] c return betweenness这段代码是GN算法的心脏。我实测下来对于5000个节点、20000条边的图单轮介数计算大概要跑40秒左右确实不快但结果很稳。3.2 删边策略与连通分量更新每轮删掉介数最高的边之后需要重新计算连通分量。这里有个坑不要每次都用DFS全图重算因为删一条边最多把原来的一个连通分量切成两个其他分量不受影响。我的优化做法是只对包含被删边的那两个端点所在的连通分量做局部BFS看它们是否还连通。如果不连通了就分裂成两个新分量如果还连通说明删的边不是桥分量不变。这个优化能把连通分量更新的开销从 ( O(nm) ) 降到平均 ( O(\text{局部大小}) )在社区结构明显的图上效果特别好。因为大部分被删的边都是社区内部的边删了也不影响连通性只有少数桥边才会导致分裂。连通分量的数据结构我用并查集Union-Find来维护但注意并查集只支持合并不支持分裂所以这里不适合。我改用集合的集合每个分量是一个Python set删边后检查两个端点是否还在同一个set里如果在说明没分裂如果不在说明之前就已经分开了这种情况不会发生因为删边前它们一定在同一个分量。实际上更简单的做法是删边后从其中一个端点做BFS看能否到达另一个端点能则不变不能则把可达节点组成新分量剩余节点组成另一个分量。3.3 模块度计算与最佳切分点选择每轮删边后我记录当前的连通分量划分并计算对应的模块度Q。模块度的计算可以用矩阵形式但对于稀疏图更高效的方式是按社区累加( Q \sum_c \left( \frac{L_c}{m} - \left( \frac{d_c}{2m} \right)^2 \right) )其中 ( L_c ) 是社区 ( c ) 内部的边数( d_c ) 是社区 ( c ) 内所有节点的度之和( m ) 是总边数。这个公式比矩阵形式好算得多只需要遍历每个社区内部的边和节点度。我通常会把每轮的Q值存到一个列表里最后画一条Q值随删边轮次变化的曲线。曲线通常先上升后下降峰值对应的轮次就是最佳切分点。但要注意Q值最大不一定业务上最合理。比如你明明知道应该分成3个社区但Q值在分成5个社区时最大这时候要结合业务判断。我的经验是Q值峰值附近多看几轮选一个社区数量符合预期的切分。提示模块度有分辨率限制当社区规模差异很大时小社区可能被合并。如果发现Q值峰值对应的划分把明显的小圈子吞掉了可以手动往前切一轮。4. 实测中的性能瓶颈与优化手段4.1 复杂度分析与瓶颈定位GN算法的理论复杂度是 ( O(m^2 n) )其中 ( m ) 是边数( n ) 是节点数。这个复杂度怎么来的每轮删一条边最多删 ( m ) 轮每轮计算所有边的介数用Brandes算法是 ( O(nm) )所以总共 ( O(m \cdot nm) O(nm^2) )。对于 ( n10000, m50000 ) 的图这个数字是 ( 2.5 \times 10^{13} )普通机器根本跑不完。我实测过一个 ( n2000, m8000 ) 的图完整跑完要将近20分钟。瓶颈主要在两个方面一是每轮都要重新计算所有边的介数二是BFS的次数太多。优化思路也就围绕这两点展开。4.2 增量式计算与剪枝策略第一个优化是增量式计算。删掉一条边后并不是所有边的介数都会变只有那些最短路径经过被删边的边才会受影响。但精确判断哪些边受影响很困难所以实际中常用的是近似增量只重新计算被删边附近一定跳数内的边的介数远处的边沿用上一轮的值。这个近似在社区结构明显的图上误差很小因为桥边被删后社区内部的最短路径基本不变。第二个优化是剪枝。每轮不需要计算所有边的精确介数只需要找到介数最高的那条边。可以用采样法随机选一部分源节点做BFS用采样结果估计边介数选估计值最高的几条边再精确计算。我试过采样30%的源节点找到的最高介数边和精确计算的结果一致率在95%以上速度提升3倍多。第三个优化是批量删边。如果多条边的介数相同或非常接近可以一次性删掉它们减少轮次。但要注意批量删边可能导致过度切分所以批量大小要控制。我的做法是每轮最多删掉介数最高的前 ( k ) 条边( k ) 取 ( \max(1, \lfloor m/100 \rfloor) )这样既加速又不会太激进。4.3 并行化与工程落地建议如果图特别大单机跑不动可以考虑并行化。Brandes算法对每个源节点的BFS是独立的天然适合并行。我用Python的multiprocessing把源节点分成若干组每组一个进程算局部介数最后汇总。在8核机器上加速比大概能到5-6倍。但要注意进程间通信有开销源节点太少时并行反而更慢一般源节点数超过1000再考虑并行。工程落地时我建议把GN算法封装成一个类输入邻接表输出层次划分树和每轮的Q值。对外暴露三个方法fit()跑完整算法get_best_partition()返回Q值最大的划分get_partition_at_level(k)返回第k轮删边后的划分。这样业务方可以根据需要灵活调用。另外如果图是动态变化的社交网络每天都有新关注全量重跑GN不现实。我的做法是先用GN跑一次得到基准社区之后每天只对新增边和删除边做局部调整新增边如果连接了两个不同社区检查是否应该合并删除边如果导致社区内连通性下降检查是否应该分裂。这种增量维护的策略在实际业务中比全量重跑实用得多。注意GN算法对噪声边敏感。如果社交网络里存在大量随机关注比如僵尸粉这些边的介数可能很高导致算法把噪声当成桥边删掉。建议在跑GN之前先做边过滤比如去掉权重低于阈值的边或者用共同邻居数做一次预筛选。5. 踩过的坑与排查实录5.1 介数计算中的浮点误差累积第一个坑是浮点误差。Brandes算法里有个除法 ( \sigma[v] / \sigma[w] )当最短路径数量很大时这个比值会非常小累加多次后浮点误差会累积。我遇到过一种情况两条边的介数理论上应该相等但算出来差了 ( 10^{-12} )导致删边顺序随机最终划分结果不稳定。解决办法是在比较介数大小时加一个容差 ( \epsilon 10^{-9} )如果两条边介数差值小于 ( \epsilon )就认为它们相等按节点ID排序后删第一条。这样保证每次运行结果可复现。另外如果图是有权图最短路径数量可能是指数级的建议用对数域计算把乘法变加法避免溢出。5.2 连通分量更新的边界情况第二个坑是连通分量更新的边界情况。有一次我删了一条边两个端点明明还在同一个分量里但我的BFS却报告不连通。排查了半天发现是因为我在删边时只删了graph[u].remove(v)忘了删graph[v].remove(u)导致图变成了有向图BFS自然搜不到反向路径。这个bug很隐蔽因为大部分情况下单向删除也能跑只是结果不对。后来我写了一个辅助函数remove_edge(u, v)同时删两个方向并且在删之前断言v in graph[u] and u in graph[v]。这个断言帮我抓到了好几次数据不一致的问题。另外如果图里有自环或重复边也要提前清理否则介数计算会出错。5.3 模块度峰值与业务预期的偏差第三个坑是模块度峰值和业务预期不一致。有一次我分析一个技术社区的数据明明业务方说应该分成前端、后端、算法三个圈子但GN算法在Q值最大时给出了5个社区把前端又拆成了“框架组”和“业务组”。一开始我以为算法错了后来仔细看数据发现框架组和业务组之间的互动确实很少他们各自有独立的核心人物和讨论话题。这其实是算法发现了更细粒度的结构不是错误。这件事给我的教训是不要盲目相信业务方的先验判断也不要盲目相信Q值。正确的做法是把GN的层次树打印出来从粗到细看每一层的划分结合节点标签和边权重做人工校验。如果某个切分在业务上说不通就往上或往下切一层。GN算法的优势就在于它给了你一整棵树而不是一个固定结果。提示实际项目中我通常会把层次树的前10层划分都导出来每层算一个Q值然后画一张Q值曲线和社区数量曲线两条曲线结合着看选一个既满足Q值较高又符合业务直觉的切分点。6. 从GN出发还能怎么扩展GN算法跑通之后我顺手做了几个扩展实验效果不错这里分享两个方向。第一个是加权GN。社交网络里的边往往有权重比如互动频率、亲密度评分。加权GN的边介数计算需要把BFS换成Dijkstra最短路径数量也要按权重累加。我试过用互动次数做权重发现加权后的社区划分更符合实际——高频互动的圈子被更准确地识别出来低频的“点赞之交”被过滤到社区边缘。加权GN的复杂度比无权版高一个量级但对于几千节点的图仍然可接受。第二个是GN与标签传播结合。GN跑完之后每个社区可以提取出核心节点社区内部度最高的几个节点然后用这些核心节点做标签传播的种子对全图做一次快速标签传播。这样既能利用GN的层次划分能力又能利用标签传播的线性时间复杂度适合大规模网络的快速社区发现。我实测在 ( n50000 ) 的图上先用采样GN粗分再标签传播细化总耗时不到3分钟划分质量接近全量GN。最后说一个我个人的使用习惯GN算法我不会单独用通常把它作为社区发现流水线的第一环。先用GN得到层次划分和候选社区数量再用Louvain或标签传播做精细优化最后用模块度和业务指标双重校验。这套组合拳打下来社区划分的准确率和可解释性都比单用某个算法好得多。如果你也在做社交网络分析建议先把GN算法手写一遍把边介数、层次树、模块度这三个概念吃透。这三个概念是社区发现领域的基石理解了它们再看其他算法就是降维打击。
RELATED

相关推荐

Jenkins + GitHub 持续集成实战:从零搭建自动化构建流水线

Jenkins + GitHub 持续集成实战:从零搭建自动化构建流水线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/16 1:01:27
BMS电池均衡设计实战:从电芯不一致到主动均衡选型与调试

BMS电池均衡设计实战:从电芯不一致到主动均衡选型与调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/16 1:01:27
应届生求职网职位信息函数化爬取:从请求封装到数据清洗的完整教程

应届生求职网职位信息函数化爬取:从请求封装到数据清洗的完整教程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/16 1:01:27
MORE NEWS

更多资讯

📰

Spring Boot三层架构核心解析:职责边界、事务控制与常见坑

1. 三层架构的真面目:不只是分包那么简单我刚带项目组那会儿,最常被新人问的一个问题就是:“这代码到底该往哪一层写?”很多同学能背出“Controller是控制层、Service是业务层、Mapper是数据层”,可真到写代码的时候&a…

📰

Anaconda安装PyTorch GPU版:十分钟搞定CUDA环境配置

很多第一次碰深度学习的人,卡住的往往不是模型怎么写,而是环境装不对。明明照着教程敲了三行命令,torch.cuda.is_available()还是返回False,然后开始怀疑显卡、怀疑驱动、怀疑人生。其实在 Anaconda 里装 PyTorch GPU 版这件事&am…

📰

LangChain与LangGraph怎么选?从本质差异到学习路线全解析

这段时间私信里问得最多的一个问题,已经从"怎么把对话记录传给模型"变成了"LangChain和LangGraph到底该学哪个、项目里到底用哪个"。我能理解这种纠结,因为LangChain官方文档一边把重心往LangGraph上偏,一边又在自己博客…

📰

JDK8 HashMap源码拆解:散列、红黑树与扩容机制一次讲透

1. 先看清HashMap的底层结构:数组、链表、红黑树是怎么拼在一起的HashMap大概是Java容器里被面试官问得最多、被线上故障坑得最惨的一个类。我最早认真读它的源码,正因为某个跨平台系统的压测频繁出现CPU100%,抓完线程栈发现所有线程都卡在Ha…

📰

ARM64中断处理全链路解析:从GIC硬件触发到Linux驱动回调

这段时间一直在调一块 ARM64 板卡上的外设中断丢事件问题,被迫把 ARM64 中断处理这条路从头到尾捋了一遍。说实话,网上讲 ARM64 中断的文章不少,但大多停留在"GIC 是中断控制器、内核里有中断子系统"这种概念层面,真正从…

📰

AI日报系统设计:分层流水线架构与本地化实践

1. 项目概述:这不是一份新闻简报,而是一套可复用的AI信息流处理系统“AI 日报(2026年10月8日)”这个标题乍看像一份时效性极强的媒体简报,但作为从业十多年、亲手搭建过二十多个垂直领域信息聚合系统的博主&#xff0c…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬