LFM算法
LFM算法是来源于论文《Detecting the overlapping and hieerarchical community structure in complex networks》
原文的abstract对算法做了个大概的总结:
Here, we present the first algorithm that finds both overlapping communities and the hierarchical structure. The method is based on the local optimization of a fitness function. Community structure is revealed by peaks in the fitness histogram. The resolution can be tuned by a parameter enabling different hierarchical levels of organization to be investigated. Tests on real and artificial networks give excellent results.
LFM算法可以同时发现重叠社区和分层结构,方法是基于局部优化的,社区结构是由适应度直方图中的峰值反映。里面有一个超参数——分辨率,通过调整分辨率可以调查不同分层的社区结构。
前面提到过的CPM, SLPA等算法虽然也可以发现重叠社区,但他们在并不能发现社区中的分层结构。而具有层次结构的重叠社区在生活中很常见。如下图是具有层次结构的网络。这四个大型集群中的每一个都由128个节点组成,并在内部细分为4个具有32个节点的集群:
该方法对网络进行局部搜索,搜索每个节点的自然社区。在这个过程中,节点可以被访问多次,不管它们是否被分配到一个社区。通过这种方式,重叠的社区自然得到恢复。通过改变决定社区平均规模的分辨率参数允许我们探索网络的所有层次级别。
算法背后的基本假设是社区本质上是局部结构的,包括属于模块本身的节点加上它们的一个扩展的领域。就比如我们的万维网网络中,人们甚至不知道这个网络有多大,而局部社区只是基于该图的部分信息形成的。所以我们通过局部扩展就能发现社区。
LFM 算法是基于局部扩展优化类方法中的典型算法。该方法基于适应度函数局部最优化的思想对网络进行社区重叠划分。LFM 从一个任意种子节点开始扩展形成一个社区,当社区适应度值不再增加时停止扩展,然后再随机的从另一个未被划分的节点开始扩展以构建新的社区结构,当所有节点都被划分后算法终止。该算法可以通过调整参数 a 的大小发现社区层次性结构。
在算法中作者用适应度来标识社区,公式如下:
其中 k i n , k o u t k_{in},k_{out} kin,kout分别代表社区总的的内部度与外部度。社区的内部度等于社区内部链接数的两倍,外部度是社区的每个成员与图的其余部分的链接数。
α是一个正实值参数,控制着社区的大小,决定了该方法的分辨率。调整α决定了观察社区的尺度。大的α值产生非常小的社区,小的α值反而提供大的社区。如果α足够小,所有的节点最终都在同一个集群中,即网络本身。我们发现,在大多数情况下,对于α < 0.5,只有一个群落;对于α > 2,恢复到最小的社区。自然的选择是α = 1,因为它是内部度与社区总度的比值。
通过改变分辨率参数,我们可以探索图覆盖的整个层次结构,从整个网络到单个节点,从而得到关于网络社区结构 的最完整的信息。
节点对于社区的适应度 表示为:
定义为社区G的适应度在有节点a和无节点a时的变化量。
发现节点A的自然社区步骤如下:
首先将节点a看做一个社区g,这是g的内部度为0,然后开始迭代:
- 对社区g中所有节点的邻居节点(要求不在社区g中的)进行遍历计算适应度
- 将适应度最大的邻居节点添加到社区g中,得到一个更大的社区g’
- 重新计算g’中每个节点的适应度
- 如果一个节点的适应度为负,则将其从社区g’中删除,生成一个新的子图g"
- 如果出现了步骤4则从3开始重复,否则从步骤1开始重复。
当步骤1中计算的所有适应度都为负时,算法停止。该方法是一个贪婪算法。
上述算法只针对一个社区,要使所有点找到自己的社区,我们做以下处理:
- 随机选取结点A
- 检测A的自然群落(上面的算法)
- 随机选取一个尚未分配到任何组的结点B
- 检测结点B的自然群落,探测所有结点,不考虑他们可能属于其他组(所以可以发现重叠社区)
- 重复步骤3当所有结点被分配到至少一个组时,算法停止。
算法存在的问题有:
(1)在获取自然社区时会有节点反复加入社区然后被剔除的死循环现象;
(2)在获取自然社区时每加入一个新的节点都要重新计算一下社区 内所有节点针对该社区的适应度值,导致计算量巨大。
针对上述两条缺点,我们采取如下措施:
(1)在自然社区发现过程中,对现有社区G的所有邻接点进行区别标记,如果某个邻接点曾被加入临时社区则在自然社区的获取过程中不再将其加入临时社区。
(2)引入核心区域的概念,并规定在获取自然社区的过程中,如果节点A属于现有社区的核心区域则则认为该节点将确定属于现有社区,不再计算其适应度值。
这样就解决了原算法中出现的问题,极大地提高了算法速度和划分社区的质量。
python代码如下:
class Community:
""" 定义一个社区类 方便计算操作"""
def __init__(self, G, alpha=1.0):
self._G = G
# α为超参数 控制观察到社区的尺度 大的α值产生非常小的社区,小的α值反而提供大的社区
self._alpha = alpha
self._nodes = set()
# k_in,k_out分别代表社区总的的内部度与外部度
self._k_in = 0
self._k_out = 0
def add_node(self, node):
""" 添加节点到社区 """
# 获得节点的邻居集 因为两点维护一边 从邻居点可以得到它的内部、外部度
neighbors = set(self._G.neighbors(node))
# 这里使用了集合操作简化运算 节点的k_in就等于节点在社区内的节点数(也就是节点的邻居与已经在社区中的节点集的集合)
node_k_in = len(neighbors & self._nodes)
# k_out自然就等于邻居数(总边数) - k_in(内部边数)
node_k_out = len(neighbors) - node_k_in
# 更新社区的节点、k_in、k_out
self._nodes.add(node)
# 对于内部度 节点本身的内部度以及在社区内的邻居节点以前的外部度变为了内部度 所以度*2
self._k_in += 2 * node_k_in
# 对于外部度 邻居节点在社区外 只需要计算一次 但要减去一个内部度(因为添加节点后 该节点到了社区内,以前提供的外部度变为了内部度 应该减去)
self._k_out = self._k_out + node_k_out - node_k_in
def remove_node(self, node):
""" 社区去除节点 """
neighbors = set(self._G.neighbors(node))
# 计算与添加相反
# community_nodes = self._nodes
# node_k_in = len(neighbors & community_nodes)
node_k_in = len(neighbors & self._nodes)
node_k_out = len(neighbors) - node_k_in
self._nodes.remove(node)
self._k_in -= 2 * node_k_in
self._k_out = self._k_out - node_k_out + node_k_in
def cal_add_fitness(self, node):
""" 添加时计算适应度该变量 """
neighbors = set(self._G.neighbors(node))
old_k_in = self._k_in
old_k_out = self._k_out
vertex_k_in = len(neighbors & self._nodes)
vertex_k_out = len(neighbors) - vertex_k_in
new_k_in = old_k_in + 2 * vertex_k_in
new_k_out = old_k_out + vertex_k_out - vertex_k_in
# 分别用适应度公式计算
new_fitness = new_k_in / (new_k_in + new_k_out) ** self._alpha
old_fitness = old_k_in / (old_k_in + old_k_out) ** self._alpha
return new_fitness - old_fitness
def cal_remove_fitness(self, node):
""" 删除时计算适应度该变量 """
neighbors = set(self._G.neighbors(node))
new_k_in = self._k_in
new_k_out = self._k_out
node_k_in = len(neighbors & self._nodes)
node_k_out = len(neighbors) - node_k_in
old_k_in = new_k_in - 2 * node_k_in
old_k_out = new_k_out - node_k_out + node_k_in
old_fitness = old_k_in / (old_k_in + old_k_out) ** self._alpha
new_fitness = new_k_in / (new_k_in + new_k_out) ** self._alpha
return new_fitness - old_fitness
def recalculate(self):
# 遍历社区中是否有适应度为负的节点
for vid in self._nodes:
fitness = self.cal_remove_fitness(vid)
if fitness < 0.0:
return vid
return None
def get_neighbors(self):
""" 获得社区的邻居节点 方便后面遍历 """
neighbors = set()
# 统计社区内所有节点的邻居,其中不在社区内部的邻居节点 就是社区的邻居节点
for node in self._nodes:
neighbors.update(set(self._G.neighbors(node)) - self._nodes)
return neighbors
def get_fitness(self):
return float(self._k_in) / ((self._k_in + self._k_out) ** self._alpha)
class LFM:
def __init__(self, G, alpha):
self._G = G
# α为超参数 控制观察到社区的尺度 大的α值产生非常小的社区,小的α值反而提供大的社区
self._alpha = alpha
def execute(self):
communities = []
# 统计还没被分配到社区的节点(初始是所有节点)
# node_not_include = self._G.node.keys()[:]
node_not_include = list(self._G.nodes())
while len(node_not_include) != 0:
# 初始化一个社区
c = Community(self._G, self._alpha)
# 随机选择一个种子节点
seed = random.choice(node_not_include)
# print(seed)
c.add_node(seed)
# 获得社区的邻居节点并遍历
to_be_examined = c.get_neighbors()
while to_be_examined:
# 添加适应度最大的节点到社区
m = {}
for node in to_be_examined:
fitness = c.cal_add_fitness(node)
m[node] = fitness
to_be_add = sorted(m.items(), key=lambda x: x[1], reverse=True)[0]
# 当所有节点适应度为负 停止迭代
if to_be_add[1] < 0.0:
break
c.add_node(to_be_add[0])
# 遍历社区中是否有适应度为负的节点 有则删除
to_be_remove = c.recalculate()
while to_be_remove is not None:
c.remove_node(to_be_remove)
to_be_remove = c.recalculate()
to_be_examined = c.get_neighbors()
# 还没被分配到社区的节点集中删除已经被添加到社区中的节点
for node in c._nodes:
if node in node_not_include:
node_not_include.remove(node)
communities.append(c._nodes)
return communities
参考结果
社区0,节点数目7,社区节点[1, 17, 22, 25, 26, 27, 31]
社区1,节点数目13,社区节点[4, 11, 15, 18, 21, 23, 24, 29, 35, 45, 51, 55, 59]
社区2,节点数目21,社区节点[2, 12, 14, 16, 20, 33, 34, 36, 37, 38, 39, 40, 43, 44, 46, 49, 50, 52, 53, 58, 61]
社区3,节点数目9,社区节点[0, 2, 10, 28, 30, 42, 47, 53, 61]
社区4,节点数目21,社区节点[1, 5, 6, 7, 9, 13, 17, 19, 22, 25, 26, 27, 31, 32, 39, 41, 48, 54, 56, 57, 60]
社区5,节点数目14,社区节点[0, 2, 3, 8, 10, 20, 28, 30, 42, 44, 47, 53, 59, 61]
0.4431
算法执行时间0.003000974655151367