【圖算法】社區發現算法——Fast unfolding

參考博客:https://blog.csdn.net/google19890102/article/details/48660239

1. 社區劃分問題的定義:

在社交網絡中,用戶相當於每一個點,用戶之間通過互相的關注關係構成了整個網絡的結構,在這樣的網絡中,有的用戶之間的連接較爲緊密,有的用戶之間的連接關係較爲稀疏,在這樣的的網絡中,連接較爲緊密的部分可以被看成一個社區,其內部的節點之間有較爲緊密的連接,而在兩個社區間則相對連接較爲稀疏,這便稱爲社團結構。如何去劃分上述的社區便稱爲社區劃分的問題。

如圖:
在這裏插入圖片描述

整個網絡被劃分成了兩個部分(紅色和黑色),其中,這兩個部分的內部連接較爲緊密,而這兩個社區之間的連接則較爲稀疏。

如何去劃分上述的社區便稱爲社區劃分問題

2. 社區劃分的評價標準:

社區劃分的目標是使得劃分後的社區內部的連接較爲緊密,而在社區之間的連接較爲稀疏。

通過模塊度的可以刻畫這樣的劃分的優劣,模塊度越大,則社區劃分的效果越好 。

模塊度的公式如下所示:

A=12mi,j[Ai,jkikj2m]δ(ci,cj)A=\frac{1}{2m}\sum_{i,j}[A_{i,j}-\frac{k_ik_j}{2m}]\delta(c_i,c_j)

其中:

  1. m=12i,jAi,jm=\frac{1}{2}\sum_{i,j}A_{i,j}表示網絡中權重之和;
  2. Ai,jA_{i,j}表示節點ii和節點jj之間的權重;
  3. k=jAi,jk=\sum_jA_{i,j}表示與節點ii相連的邊的權重和;
  4. cic_i表示節點分配到的社區;
  5. δ(ci,cj)\delta(c_i,c_j)判斷節點ii和節點jj是否劃分到同一個社區,若是,返回11;否則,返回00

因此,模塊度也可以理解爲網絡中連接社區結構內部頂點的邊所佔的比例,減去在同樣的社團結構下任意連接這兩個節點的比例的期望值。

3. Fast unfolding算法:

3.1 Fast Unfolding算法的基本思路:

Fast Unfolding算法是一種迭代的算法,主要目標是不斷劃分社區使得劃分後的整個網絡的模塊度不斷增大。

3.2 算法流程:

主要分爲兩個階段:

第一階段稱爲Modularity Optimization,主要是將每個節點劃分到與其鄰接的節點所在的社區中,以使得模塊度的值不斷變大;

第二階段稱爲Community Aggregation,主要是將第一步劃分出來的社區聚合成爲一個點,即根據上一步生成的社區結構重新構造網絡。重複以上的過程,直到網絡中的結構不再改變爲止。

具體的算法過程如下所示:

  1. 初始化,將每個點劃分在不同的社區中;

  2. 對每個節點,將每個點嘗試劃分到與其鄰接的點所在的社區中,計算此時的模塊度,判斷劃分前後的模塊度的差值ΔQΔQ是否爲正數,若爲正數,則接受本次的劃分,若不爲正數,則放棄本次的劃分;

  3. 重複以上的過程,直到不能再增大模塊度爲止;

  4. 構造新圖,新圖中的每個點代表的是步驟3中劃出來的每個社區,繼續執行步驟2和步驟3,直到社區的結構不再改變爲止。

在這裏插入圖片描述

4. 代碼實現:

4.1 Python實現:

import networkx as nx

from itertools import permutations
from itertools import combinations
from collections import defaultdict


class Louvain(object):
    def __init__(self):
        self.MIN_VALUE = 0.0000001
        self.node_weights = {}    #節點權重

    @classmethod
    def convertIGraphToNxGraph(cls, igraph):
        node_names = igraph.vs["name"]
        edge_list = igraph.get_edgelist()
        weight_list = igraph.es["weight"]
        node_dict = defaultdict(str)

        for idx, node in enumerate(igraph.vs):
            node_dict[node.index] = node_names[idx]

        convert_list = []
        for idx in range(len(edge_list)):
            edge = edge_list[idx]
            new_edge = (node_dict[edge[0]], node_dict[edge[1]], weight_list[idx])
            convert_list.append(new_edge)

        convert_graph = nx.Graph()
        convert_graph.add_weighted_edges_from(convert_list)
        return convert_graph

    def updateNodeWeights(self, edge_weights):
        node_weights = defaultdict(float)
        for node in edge_weights.keys():
            node_weights[node] = sum([weight for weight in edge_weights[node].values()])
        return node_weights

    def getBestPartition(self, graph, param=1.):
        node2com, edge_weights = self._setNode2Com(graph)    #獲取節點和邊

        node2com = self._runFirstPhase(node2com, edge_weights, param)
        best_modularity = self.computeModularity(node2com, edge_weights, param)

        partition = node2com.copy()
        new_node2com, new_edge_weights = self._runSecondPhase(node2com, edge_weights)

        while True:
            new_node2com = self._runFirstPhase(new_node2com, new_edge_weights, param)
            modularity = self.computeModularity(new_node2com, new_edge_weights, param)
            if abs(best_modularity - modularity) < self.MIN_VALUE:
                break
            best_modularity = modularity
            partition = self._updatePartition(new_node2com, partition)
            _new_node2com, _new_edge_weights = self._runSecondPhase(new_node2com, new_edge_weights)
            new_node2com = _new_node2com
            new_edge_weights = _new_edge_weights
        return partition

    def computeModularity(self, node2com, edge_weights, param):
        q = 0
        all_edge_weights = sum(
            [weight for start in edge_weights.keys() for end, weight in edge_weights[start].items()]) / 2

        com2node = defaultdict(list)
        for node, com_id in node2com.items():
            com2node[com_id].append(node)

        for com_id, nodes in com2node.items():
            node_combinations = list(combinations(nodes, 2)) + [(node, node) for node in nodes]
            cluster_weight = sum([edge_weights[node_pair[0]][node_pair[1]] for node_pair in node_combinations])
            tot = self.getDegreeOfCluster(nodes, node2com, edge_weights)
            q += (cluster_weight / (2 * all_edge_weights)) - param * ((tot / (2 * all_edge_weights)) ** 2)
        return q

    def getDegreeOfCluster(self, nodes, node2com, edge_weights):
        weight = sum([sum(list(edge_weights[n].values())) for n in nodes])
        return weight

    def _updatePartition(self, new_node2com, partition):
        reverse_partition = defaultdict(list)
        for node, com_id in partition.items():
            reverse_partition[com_id].append(node)

        for old_com_id, new_com_id in new_node2com.items():
            for old_com in reverse_partition[old_com_id]:
                partition[old_com] = new_com_id
        return partition

    def _runFirstPhase(self, node2com, edge_weights, param):
        # 計算所有邊上的權重之和
        all_edge_weights = sum(
            [weight for start in edge_weights.keys() for end, weight in edge_weights[start].items()]) / 2
        self.node_weights = self.updateNodeWeights(edge_weights) #輸出一個字典,每個node對應node上邊的權重和
        status = True
        while status:
            statuses = []
            for node in node2com.keys():   # 逐一選擇節點和周邊連接的節點進行比較
                statuses = []
                com_id = node2com[node]    # 獲取節點對應的社團編號
                neigh_nodes = [edge[0] for edge in self.getNeighborNodes(node, edge_weights)] #獲取連接的所有邊節點

                max_delta = 0.              # 用於計算比對
                max_com_id = com_id         # 默認當前社團id爲最大社團id
                communities = {}
                for neigh_node in neigh_nodes:
                    node2com_copy = node2com.copy()
                    if node2com_copy[neigh_node] in communities:
                        continue
                    communities[node2com_copy[neigh_node]] = 1
                    node2com_copy[node] = node2com_copy[neigh_node] # 把node對應的社團id放到臨近的neigh_node中

                    delta_q = 2 * self.getNodeWeightInCluster(node, node2com_copy, edge_weights) - (self.getTotWeight(
                        node, node2com_copy, edge_weights) * self.node_weights[node] / all_edge_weights) * param
                    if delta_q > max_delta:
                        max_delta = delta_q                     # max_delta 選擇最大的增益的node
                        max_com_id = node2com_copy[neigh_node]  # 對應 max_com_id 選擇最大的增益的臨接node的id

                node2com[node] = max_com_id
                statuses.append(com_id != max_com_id)

            if sum(statuses) == 0:
                break

        return node2com

    def _runSecondPhase(self, node2com, edge_weights):
        """
        :param node2com:       第一層phase 構建完之後的node->社團結果
        :param edge_weights:   社團邊字典
        :return:
        """
        com2node = defaultdict(list)

        new_node2com = {}
        new_edge_weights = defaultdict(lambda: defaultdict(float))

        for node, com_id in node2com.items():
            #生成了社團:--->節點映射
            com2node[com_id].append(node)  #添加同一一個社團id對應的node
            if com_id not in new_node2com:
                new_node2com[com_id] = com_id

        nodes = list(node2com.keys())
        node_pairs = list(permutations(nodes, 2)) + [(node, node) for node in nodes]
        for edge in node_pairs:
            new_edge_weights[new_node2com[node2com[edge[0]]]][new_node2com[node2com[edge[1]]]] += edge_weights[edge[0]][
                edge[1]]
        return new_node2com, new_edge_weights

    def getTotWeight(self, node, node2com, edge_weights):
        """
        :param node:
        :param node2com:
        :param edge_weights:
        :return:
        """
        nodes = [n for n, com_id in node2com.items() if com_id == node2com[node] and node != n]

        weight = 0.
        for n in nodes:
            weight += sum(list(edge_weights[n].values()))
        return weight

    def getNeighborNodes(self, node, edge_weights):
        """
        :param node:  輸入節點
        :param edge_weights: 邊字典
        :return: 輸出每個節點連接點邊集合
        """
        if node not in edge_weights:
            return 0
        return edge_weights[node].items()

    def getNodeWeightInCluster(self, node, node2com, edge_weights):
        neigh_nodes = self.getNeighborNodes(node, edge_weights)
        node_com = node2com[node]
        weights = 0.
        for neigh_node in neigh_nodes:
            if node_com == node2com[neigh_node[0]]:
                weights += neigh_node[1]
        return weights

    def _setNode2Com(self,graph):
        """
        :return: 節點->團,edge_weights 形式:{'a': defaultdict(<class 'float'>, {'c': 1.0, 'b': 1.0})}
        """
        node2com = {}
        edge_weights = defaultdict(lambda: defaultdict(float))
        for idx,node in enumerate(graph.nodes()):
            node2com[node] = idx    #給每一個節點初始化賦值一個團id
            for edge in graph[node].items():
                edge_weights[node][edge[0]] = edge[1]['weight']
        return node2com,edge_weights

4.2 算法測試:

測試:

import networkx as nx
from  fast_unfolding import *
import matplotlib.pyplot as plt
from collections import defaultdict
import random

def makeSampleGraph():
    '''
    生成圖
    '''
    g = nx.Graph()
    g.add_edge("a", "b", weight=1.)
    g.add_edge("a", "c", weight=1.)
    g.add_edge("b", "c", weight=1.)
    g.add_edge("b", "d", weight=1.)

    return g

def random_Graph():
    '''
    生成隨機圖
    '''
    g = nx.Graph()

    node_num = random.randint(10, 15)

    node_chars = [chr(ord('a')+i) for i in range(node_num)]

    for n in node_chars:
        g.add_node(n)

    for _ in range(20):
        v = random.sample(node_chars, 2)
        w = 1
        while w==1 or w==0:
            w = round(random.random(), 2)
        g.add_edge(v[0], v[1], weight=w)

    return g

if __name__ == "__main__":
    # sample_graph = makeSampleGraph()
    sample_graph = random_Graph()
    print(sample_graph.nodes,sample_graph.edges)
    print(sample_graph['a'])
    louvain = Louvain()
    partition = louvain.getBestPartition(sample_graph)

    p = defaultdict(list)
    for node, com_id in partition.items():
        p[com_id].append(node)

    for com, nodes in p.items():
        print(com, nodes)

    edge_labels=dict([((u,v,),d['weight']) for u,v,d in sample_graph.edges(data=True)])

    pos=nx.spring_layout(sample_graph)
    nx.draw_networkx_edge_labels(sample_graph,pos,edge_labels=edge_labels)
    # print(edge_labels)
    nx.draw_networkx(sample_graph,pos)
    plt.show()

4.3 測試結果:

生成的隨機圖:
在這裏插入圖片描述

測試結果:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章