【圖算法】社區發現算法——Fast unfolding
參考博客:https://blog.csdn.net/google19890102/article/details/48660239
1. 社區劃分問題的定義:
在社交網絡中,用戶相當於每一個點,用戶之間通過互相的關注關係構成了整個網絡的結構,在這樣的網絡中,有的用戶之間的連接較爲緊密,有的用戶之間的連接關係較爲稀疏,在這樣的的網絡中,連接較爲緊密的部分可以被看成一個社區,其內部的節點之間有較爲緊密的連接,而在兩個社區間則相對連接較爲稀疏,這便稱爲社團結構。如何去劃分上述的社區便稱爲社區劃分的問題。
如圖:
整個網絡被劃分成了兩個部分(紅色和黑色),其中,這兩個部分的內部連接較爲緊密,而這兩個社區之間的連接則較爲稀疏。
如何去劃分上述的社區便稱爲社區劃分問題。
2. 社區劃分的評價標準:
社區劃分的目標是使得劃分後的社區內部的連接較爲緊密,而在社區之間的連接較爲稀疏。
通過模塊度的可以刻畫這樣的劃分的優劣,模塊度越大,則社區劃分的效果越好 。
模塊度的公式如下所示:
其中:
- 表示網絡中權重之和;
- 表示節點和節點之間的權重;
- 表示與節點相連的邊的權重和;
- 表示節點分配到的社區;
- 判斷節點和節點是否劃分到同一個社區,若是,返回;否則,返回;
因此,模塊度也可以理解爲網絡中連接社區結構內部頂點的邊所佔的比例,減去在同樣的社團結構下任意連接這兩個節點的比例的期望值。
3. Fast unfolding算法:
3.1 Fast Unfolding算法的基本思路:
Fast Unfolding算法是一種迭代的算法,主要目標是不斷劃分社區使得劃分後的整個網絡的模塊度不斷增大。
3.2 算法流程:
主要分爲兩個階段:
第一階段稱爲Modularity Optimization,主要是將每個節點劃分到與其鄰接的節點所在的社區中,以使得模塊度的值不斷變大;
第二階段稱爲Community Aggregation,主要是將第一步劃分出來的社區聚合成爲一個點,即根據上一步生成的社區結構重新構造網絡。重複以上的過程,直到網絡中的結構不再改變爲止。
具體的算法過程如下所示:
-
初始化,將每個點劃分在不同的社區中;
-
對每個節點,將每個點嘗試劃分到與其鄰接的點所在的社區中,計算此時的模塊度,判斷劃分前後的模塊度的差值是否爲正數,若爲正數,則接受本次的劃分,若不爲正數,則放棄本次的劃分;
-
重複以上的過程,直到不能再增大模塊度爲止;
-
構造新圖,新圖中的每個點代表的是步驟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 測試結果:
生成的隨機圖:
測試結果: