Louvain 社团发现算法学习(我的java实现+数据用例)

本文介绍了Louvain社团发现算法,该算法基于模块度优化,旨在最大化社区网络的模块度。文章详细阐述了算法步骤,包括模块度计算、启发式规则以及可能存在的问题。提供了作者的Java实现代码,以及对大规模数据集的运行效果展示。同时讨论了如何通过不同序列多次运行以提高结果精度,并给出了优化后的高效实现和测试数据链接。
摘要由CSDN通过智能技术生成

为了大家方便,直接把数据放在github了:

https://github.com/qq547276542/Louvain

算法介绍:

Louvain 算法是基于模块度的社区发现算法,该算法在效率和效果上都表现较好,并且能够发现层次性的社区结构,其优化目标是最大化整个社区网络的模块度。

社区网络的模块度(Modularity)是评估一个社区网络划分好坏的度量方法,它的含义是社区内节点的连边数与随机情况下的边数之差,它的取值范围是 (0,1),其定义如下:


上式中,Aij代表结点i和j之间的边权值(当图不带权时,边权值可以看成1)。 ki代表结点i的領街边的边权和(当图不带权时,即为结点的度数)。

m为图中所有边的边权和。 ci为结点i所在的社团编号。

模块度的公式定义可以做如下的简化:

其中Sigma in表示社区c内的边的权重之和,Sigma tot表示与社区c内的节点相连的边的权重之和。

我们的目标,是要找出各个结点处于哪一个社团,并且让这个划分结构的模块度最大。


Louvain算法的思想很简单:

1)将图中的每个节点看成一个独立的社区,次数社区的数目与节点个数相同;

2)对每个节点i,依次尝试把节点i分配到其每个邻居节点所在的社区,计算分配前与分配后的模块度变化Delta Q,并记录Delta Q最大的那个邻居节点,如果maxDelta Q>0,则把节点i分配Delta Q最大的那个邻居节点所在的社区,否则保持不变;

3)重复2),直到所有节点的所属社区不再变化;

4)对图进行压缩,将所有在同一个社区的节点压缩成一个新节点,社区内节点之间的边的权重转化为新节点的环的权重,社区间的边权重转化为新节点间的边权重;

5)重复1)直到整个图的模块度不再发生变化。


在写代码时,要注意几个要点:

* 第二步,尝试分配结点时,并不是只尝试独立的结点,也可以尝试所在社区的结点数大于1的点,我在看paper时一开始把这个部分看错了,导致算法出问题。

* 第三步,重复2)的时候,并不是将每个点遍历一次后就对图进行一次重构,而是要不断循环的遍历每个结点,直到一次循环中所有结点所在社区都不更新了,表示当前网络已经稳定,然后才进行图的重构。

* 模块度增益的计算,请继续看下文


过程如下图所示:


可以看出,louvain是一个启发式的贪心算法。我们需要对模块度进行一个启发式的更新。这样的话这个算法会有如下几个问题:

1:尝试将节点i分配到相邻社团时,如果真的移动结点i,重新计算模块度,那么算法的效率很难得到保证

2:在本问题中,贪心算法只能保证局部最优,而不能够保证全局最优

3:将节点i尝试分配至相邻社团时,要依据一个什么样的顺序

......


第一个问题,在该算法的paper中给了我们解答。我们实际上不用真的把节点i加入相邻社团后重新计算模块度,paper中给了我们一个计算把结点i移动至社团c时,模块度的增益公式:

其中Sigma in表示起点终点都在社区c内的边的权重之和,Sigma tot表示入射社区c内的边的权重之和,ki代表结点i的带权度数和,m为所有边权和。

但是该增益公式还是过于复杂,仍然会影响算法的时间效率。

但是请注意,我们只是想通过模块增益度来判断一个结点i是否能移动到社团c中,而我们实际上是没有必要真正的去求精确的模块度,只需要知道,当前的这步操作,模块度是否发生了增长。

因此,就有了相对增益公式的出现:

相对增益的值可能大于1,不是真正的模块度增长值,但是它的正负表示了当前的操作是否增加了模块度。用该公式能大大降低算法的时间复杂度。


第二个问题,该算法的确不能够保证全局最优。但是我们该算法的启发式规则很合理,因此我们能够得到一个十分精确的近似结果。

同时,为了校准结果,我们可以以不同的序列多次调用该算法,保留一个模块度最大的最优结果。


第三个问题,在第二个问题中也出现了,就是给某个结点i找寻領接点时,应当以一个什么顺序?递增or随机or其它规则?我想这个问题需要用实验数据去分析。

在paper中也有提到一些能够使结果更精确的序列。我的思路是,如果要尝试多次取其最优,取随机序列应该是比较稳定比较精确的方式。


说了这么多,下面给上本人的Louvain算法java实现供参考。 本人代码注释比较多,适合学习。

我的代码后面是国外大牛的代码,时空复杂度和我的代码一样,但是常数比我小很多,速度要快很多,而且答案更精准(迭代了10次),适合实际运用~


本人的java代码:(时间复杂度o(e),空间复杂度o(e))


Edge.java:

package myLouvain;

public class Edge implements Cloneable{
	int v;     //v表示连接点的编号,w表示此边的权值
	double weight;
	int next;    //next负责连接和此点相关的边
	Edge(){}
	public Object clone(){
		Edge temp=null;
		try{  
		    temp = (Edge)super.clone();   //浅复制  
		}catch(CloneNotSupportedException e) {  
		    e.printStackTrace();  
		}   
		return temp;
	}
}



Louvain.java:

package myLouvain;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Random;

public class Louvain implements Cloneable{
    int n; // 结点个数
    int m; // 边数
    int cluster[]; // 结点i属于哪个簇
    Edge edge[]; // 邻接表
    int head[]; // 头结点下标
    int top; // 已用E的个数
    double resolution; // 1/2m 全局不变
    double node_weight[]; // 结点的权值
    double totalEdgeWeight; // 总边权值
    double[] cluster_weight; // 簇的权值
    double eps = 1e-14; // 误差

    int global_n; // 最初始的n
    int global_cluster[]; // 最后的结果,i属于哪个簇
    Edge[] new_edge;   //新的邻接表
    int[] new_head;
    int new_top = 0;
    final int iteration_time = 3; // 最大迭代次数
    
    Edge global_edge[];   //全局初始的临接表  只保存一次,永久不变,不参与后期运算
    int global_head[];
    int global_top=0;
    
    void addEdge(int u, int v, double weight) {  
        if(edge[top]==null)
            edge[top]=new Edge();
        edge[top].v = v;
        edge[top].weight = weight;
        edge[top].next = head[u];
        head[u] = top++;
    }

    void addNewEdge(int u, int v, double weight) {
        if(new_edge[new_top]==null)
            new_edge[new_top]=new Edge();
        new_edge[new_top].v = v;
        new_edge[new_top].weight = weight;
        new_edge[new_top].next = new_head[u];
        new_head[u] = new_top++;
    }
    
    void addGlobalEdge(int u, int v, double weight) {
        if(global_edge[global_top]==null)
            global_edge[global_top]=new Edge();
        global_edge[global_top].v = v;
        global_edge[global_top].weight = weight;
        global_edge[global_top].next = global_head[u];
        global_head[u] = global_top++;
    }

    void init(String filePath) {
        try {
            String encoding = "UTF-8";
            File file = new File(filePath);
            if (file.isFile() && file.exists()) { // 判断文件是否存在
                InputStreamReader read = new InputStreamReader(new FileInputStream(file), encoding);// 考虑到编码格式
                BufferedReader bufferedReader = new BufferedReader(read);
                String lineTxt = null;
                lineTxt = bufferedReader.readLine();

                // 预处理部分
                String cur2[] = lineTxt.split(" ");
                global_n = n = Integer.parseInt(cur2[0]);
                m = Integer.parseInt(cur2[1]);
                m *= 2;
                edge = new Edge[m];
                head = new int[n];
                for (int i = 0; i < n; i++)
                    head[i] = -1;
                top = 0;
                
                global_edge=new Edge[m];
                global_head = new int[n];
                for(int i=0;i<n;i++)
                    global_head[i]=-1;
                global_top=0;
                global
  • 20
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 50
    评论
评论 50
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值