社区发现之标签传播算法(LPA)python实现

社区发现在图领域中备受关注,其根源可以追溯到子图分割问题。在真实的社交网络中,用户之间的联系紧密度不尽相同,导致形成了不同的社区结构。社区发现问题主要分为两类:非重叠和重叠社区。非重叠社区发现指的是每个节点仅属于一个社区,社区之间没有交集。在非重叠社区发现中,有多种解决方法。其中,基于模块度的算法通过最大化模块度来划分社区,以找到最优的社区结构。另一种常见的方法是基于标签传播的算法,例如标签传播算法(LPA)。这种算法通过在节点之间传播标签信息,直到达到收敛条件,从而识别出社区结构。标签传播算法相对简单而且高效,适用于大规模图的社区发现。这里将重点讨论标签传播算法,探讨其原理、优缺点以及在实际应用中的效果和局限性。

一、标签传播算法概述

标签传播算法(Label Propagation Algorithm,简称LPA)是一种用于图领域中的社区发现方法。其基本原理是利用节点之间标签的传播来将具有相似特征的节点划分到同一社区。算法开始时,每个节点被赋予一个唯一的标签,代表其所属的潜在社区。随后,每个节点根据其邻居节点的标签信息进行标签更新,迭代更新直至达到收敛状态。具体而言,标签传播算法的迭代过程如下:

初始化: 为每个节点分配一个唯一的标签。
节点标签更新: 随机选择一个节点,统计其邻居节点的标签分布情况,选择出现频率最高的标签作为该节点的新标签。若有多个标签出现频率相同,则随机选择一个作为新标签。
迭代: 重复节点标签更新步骤,直到标签分布不再发生变化或达到预设的迭代次数。
通过这个过程,标签传播算法能够发现网络中的社区结构,将具有相似特征的节点归为同一社区。

1.1 标签传播算法的优缺点

优点 缺点
逻辑简单: 算法的核心思想直观易懂,易于理解和实现 随机性强: 算法结果的稳定性可能受到初始化条件和迭代顺序的影响
无需预设社区数量: 不需要事先确定社区的数量,能够根据网络结构自动划分社区 无法处理重叠社区: 每个节点只能属于一个社区,无法处理节点属于多个社区的情况
运行效率高: 算法在迭代过程中只关注局部标签更新,避免了全局优化问题,因此具有较高的运行效率 对噪声和异常值敏感: 容易受到噪声和异常值的影响,可能导致社区划分不准确

1.2标签传播算法在实际应用中的效果与局限性

标签传播算法在实际应用中取得了一定的成功,尤其在处理大规模网络数据时表现出较高的效率。它可以帮助人们理解和分析复杂网络中的社交关系、用户行为等。然而,算法也存在一些局限性:
稳定性问题: 算法结果可能受到初始化和迭代顺序的影响,导致结果不稳定。
无法处理重叠社区: 算法无法准确处理节点同时属于多个社区的情况,限制了其适用范围。
对噪声和异常值敏感: 在存在噪声和异常值的情况下,算法可能无法准确识别社区结构。
为了克服这些局限性,研究者们提出了一些改进算法,如COPRA、SLPA等。这些改进算法通过引入更复杂的标签传播策略或额外的信息来提高算法的稳定性和准确性。
标签传播算法作为一种社区发现方法,具有简单易懂、无需预设社区数量等优点,但也存在随机性强、无法处理重叠社区等缺点。在实际应用中,需要根据具体情况选择合适的算法或进行改进以提高效果。随着图领域研究的不断深入,我们期待看到更多针对标签传播算法的改进和优化,使其在社区发现及其他相关领域发挥更大的作用。

二、标签传播算法

2.1 算法原理

LPA是基于图的半监督方法,所以首先就需要对原数据进行图方法的展示,如下图:

那蓝色的球(即节点)表示没有标签的样本,棕色和黄色则可以表示有标签的样本。那样本与样本之间的连线(即权重)则可以表示它们之间的相似度。那么这里相似度的计算方法为:

\[w_{ij}=\exp^{- \frac{\| \mathbf{x_i-x_j} \|^2}{\alpha^2}} \]

这里的\(\alpha\)只是一个超参数而已。

当然这里也只是说了这么一种构建图的方法,除此以外,也可以用KNN的方式来构建节点图。不过KNN的方法会存在一种震荡的情况,导致数据的不稳定(主要是因为采用了同步更新法,换成异步更新就没有这个问题了)。不过KNN方法下的理论性没有这种强,所以以这个方法展开来叙述LPA算法。
有了上面的样本与样本间的相似度计算方法之后,我们可以构建一个基于相似矩阵的概率转移矩阵\(P\)\(P\)是一个N×N的矩阵:

\[P_{ij}=\frac{w_{ij}}{\sum_{k=1}^{N}w_{ik}} \]

显然这里是在做一个归一化的行为。\(P_{ij}\)表示的就是节点\(i\)\(j\)转移的可能性,通过所有节点间转移可能性的汇总,就可以得到我们需要的概率转移矩阵P。
接着需要构建一个初始的状态矩阵:

\[F = \begin{bmatrix} Y_L \\ Y_U \end{bmatrix} \]

上面\(Y_L\)是有标签的样本,下面的\(Y_U\)是没有标签的样本。我们想要实现的就是把\(Y_U\)这些样本也打上应有的标签。既然有了概率转移矩阵,也构建了初始的状态矩阵,下面就是LPA算法的实现过程了:

  • \(F=PF\),通过这个计算,就可以把\(F\)\(Y_U\)中部分无标签数据打上了标签。
  • 更新\(F\)中的\(Y_U\)样本情况
  • 重复迭代上面过程这个过程,直到\(Y_U\)都有标签为止。

既然我们关注点是\(Y_U\)部分,但是实际上我们每次在计算\(F=PF\)的过程中,\(Y_L\)都参与计算了。那么有没有什么更加简便的计算方式,让我们在计算过程中忽略\(Y_L\)呢?这是可以做到进一步优化:我们在构建概率转移矩阵的时候,考虑把有标签的样本放在矩阵的前方,把无标签的样本放在矩阵的后方即可。那么概率转移矩阵就可以拆分为这个样子了:

\[\begin{aligned} & P=\left[\begin{array}{ll} P_{L L} & P_{L U} \\ P_{U L} & P_{U U} \end{array}\right] \end{aligned} \]

这里:
PLL表示已有标签数据到其对应标签的转移概率,那肯定是1啊,所以PLL就是一个单位矩阵;
PLU表示已有标签数据到其对不应标签的转移概率,那自然就是0了,所以PLU就是一个0矩阵;
PUL和PUU两个矩阵则是未知的情况,因为没有标签的样本究竟是属于前面已经出现过的标签,还是未知的标签,目前还是未知状态。

所以上面的\(P\)矩阵可以写为:

\[\begin{aligned} P=\left[\begin{array}{cc} I & 0 \\ P_{U L} & P_{U U} \end{array}\right] \end{aligned} \]

继续运算,我们采用这个\(P\)矩阵和\(F\)矩阵来实现上面\(LP\)算法的迭代过程:

\[\begin{aligned} & \lim _{t \rightarrow \infty} P^t=\left[\begin{array}{cc} I & 0 \\ P_{U L} & P_{U U} \end{array}\right] \times\left[\begin{array}{cc} I & 0 \\ P_{U L} & P_{U U} \end{array}\right] \times \ldots \times\left[\begin{array}{cc} I & 0 \\ P_{U L} & P_{U U} \end{array}\right] \\ & =\left[\begin{array}{cc} I & 0 \\ \left(\sum_{t=0}^{\infty} P_{U U}^t\right) \cdot P_{U L} & P_{U U}^{\infty} \end{array}\right] \\ & =\left[\begin{array}{cc} I & 0 \\ \left(I-P_{U U}\right)^{-1} \cdot P_{U L} & 0 \end{array}\right] \\ & \end{aligned} \]

注意两点:

  • 正常情况下,未知的样本不管属于哪一个标签,它的可能性是绝不会大于已知样本属于已知标签的可能性(即1),所以:

\[P_{uu}<1\Rightarrow \lim_{t \rightarrow \infty}P_{\infty}^t=0 \]

  • 级数求和公式:

\[\sum_{k=0}^n x^k=\frac{1}{1-x} \quad|x|<1 \]

所以

\[\left(\sum_{t=0}^{\infty} P_{U U}^t\right) \cdot P_{U L}=\left(I-P_{U U}\right)^{-1} \quad 0 \leq P_{U U}<1 \]

接着实现 \(F=P F\),由于

\[F=\left[\begin{array}{l} Y_L \\ Y_U \end{array}\right] \]

所以 \(F=P F\) 可以写为:

\[\begin{bmatrix} \hat{Y}_L \\ \hat{Y}_U \end{bmatrix} = \lim_{t\rightarrow \infty}P^t =\begin{aligned} P=\left[\begin{array}{cc} I & 0 \\ (1-P_{UU})^{-1} \cdot P_{UL} & 0 \end{array}\right] \end{aligned} \begin{bmatrix} Y_L \\ Y_U \end{bmatrix} \]

最终得:

\[\hat{Y}_U = (I - P_{UU})^{-1} \cdot P_{UL} \cdot Y_L \]

这样,\(Y_L\)就可以完全排除我们的计算之外了。对于LPA算法的求解也有两种办法:

  • 正常求\(I-P_{UU}\)矩阵的逆,然后正常计算出结果即可。显然这个在数据量较大时,比较耗费算力。
  • 迭代法,设定迭代的次数或者误差的范围,进行迭代即可。

2.2 算法实例

再看个实际例子,按照上面的图可以构建我们的概率转移矩阵\(P\)

\[P=\left[\begin{array}{ccccccc} 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0.33 & 0 & 0.33 & 0 & 0.33 & 0 & 0 \\ 0 & 0 & 0 & 0.5 & 0 & 0.5 & 0 \\ 0 & 0.33 & 0 & 0 & 0.33 & 0 & 0.33 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 \end{array}\right] \]

列表示节点为\([1,7,2,3,4,5,6]\),行表示同样的节点顺序为\([1,7,2,3,4,5,6]\),如\(P\)的第一行:[1,0,0,0,0,0,0]表示:1号球属于标签1的可能性为1,属于其余标签的可能性为0;P的第四行:[0.33,0,0.33,0,0.33,0,0]表示:3号球连接了1、2和4号三个球,故共有三种属于的可能性,分别为0.33,所以在1、2以及4标签处的连接可能性为0.33,其余可能性为0,其他行的数据都是这么计算而来。
我们采用迭代法,计算\(P\)的无穷大次方:

import numpy as np
# 定义矩阵P
P = np.array([
    [1, 0, 0, 0, 0, 0, 0],
    [0, 1, 0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0, 0, 0],
    [0.33, 0, 0.33, 0, 0.33, 0, 0],
    [0, 0, 0, 0.5, 0, 0.5, 0],
    [0, 0.33, 0, 0, 0.33, 0, 0.33],
    [0, 0, 0, 0, 0, 1, 0]
], dtype=float)
# 计算矩阵P的n次幂
n=100
Pn = np.linalg.matrix_power(P, 100)
# 输出结果
print(Pn)

计算的结果是:

\[P=\left[\begin{array}{ccccccc} 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0.7316 & 0.239 & 0 & 0 & 0 & 0 & 0 \\ 0.7316 & 0.239 & 0 & 0 & 0 & 0 & 0 \\ 0.4853 & 0.4853 & 0 & 0 & 0 & 0 & 0 \\ 0.239 & 0.7316 & 0 & 0 & 0 & 0 & 0 \\ 0.239 & 0.7316 & 0 & 0 & 0 & 0 & 0 \end{array}\right] \]

再反过来看当时的\(P\)的求极限过程, 这个结果显然验证了当初的设定都是正确的。所以有

\[\begin{aligned} & \hat{Y}_U=\left(I-P_{U U}\right)^{-1} \cdot P_{U L} \times Y_L \\ & =\left[\begin{array}{cc} 0.7316 & 0.239 \\ 0.7316 & 0.239 \\ 0.4853 & 0.4853 \\ 0.239 & 0.7316 \\ 0.239 & 0.7316 \end{array}\right] \times\left[\begin{array}{ll} 1 & 0 \\ 0 & 1 \end{array}\right]=\left[\begin{array}{cc} 0.7316 & 0.239 \\ 0.7316 & 0.239 \\ 0.4853 & 0.4853 \\ 0.239 & 0.7316 \\ 0.239 & 0.7316 \end{array}\right] \end{aligned} \]

根据计算的结果:对于样本1的橙色标签来说,样本2、3、4、5和6属于它的可能性分别为:[0.7316,0.7316,0.4853,0.239,0.239];对于样本7的蓝色标签来说,样本2、3、4、5和6属于它的可能性分别为:[0.239,0.239,0.4853,0.7316,0.7316],综上:2和3是橙色;5和6是蓝色;对于4来说,还是无法确定的。如下图:

除此以外,还有对一个对label propagation改进的一个算法,即label spreading算法。这个算法本质上和label propagation是一样的,不过它在迭代过程中,对label propagation的迭代方程进行了一个简单的改进(就是加上了一个正则化),如公式:

\[F=P F \xrightarrow{\text { 正则化 }} F=\alpha P F+(1-\alpha) Y \]

这里前后的\(P\)是有点区别的,但是基本一样的方式计算得到的。不过重点不是这个概率转移矩阵,而是折中参数α,它的取值在[0,1]之间,一般都是取0.2。

2.3 电商评价同步行为示例

  • 电商评价同步行为数据

如第3列和第6列 x∩y 代表用户 BUY_01 和 BUY_02 在同一个小时内对同一商户进行评价的情况有 2 次,即他们出现同步行为的次数为 2 。

节点1 节点2 权重(x∩y) 节点1 节点2 权重(x∩y)
BUY_01 BUY_02 2 BUY_02 BUY_03 2
BUY_01 BUY_03 2 BUY_02 BUY_04 1
BUY_01 BUY_04 1 BUY_03 BUY_04 1
  • 将同步行为数据转化成图

由于 LPA 的算法输入为图,需要将其转换成图形式

import networkx as nx  
import matplotlib.pyplot as plt  
  
# 定义你的数据  
edges = [  
    ('BUY_01', 'BUY_02', 2),  
    ('BUY_01', 'BUY_03', 2),  
    ('BUY_01', 'BUY_04', 1),  
    ('BUY_02', 'BUY_03', 2),  
    ('BUY_02', 'BUY_04', 1),  
    ('BUY_03', 'BUY_04', 1)  
]  
  
# 创建一个空的图  
G = nx.Graph()  ##x∩y为weight
  
# 添加边和权重到图中  
G.add_weighted_edges_from(edges)  
  
# 创建一个figure和axes对象  
fig, ax = plt.subplots()  
  
# 绘制网络图  
nx.draw(G, ax=ax, with_labels=True, font_weight='bold')  
  
# 显示权重  
edge_labels = nx.get_edge_attributes(G, 'weight')  
nx.draw_networkx_edge_labels(G, ax=ax, edge_labels=edge_labels)  
  
# 显示图形  
plt.show()

示例样本量太少,不易作社区划分,手工添加多个社区节点

G.add_edge('BUY_04','BUY_05',weight=2)
G.add_edge('BUY_05','BUY_06',weight=1)
G.add_edge('BUY_05','BUY_07',weight=2)
G.add_edge('BUY_06','BUY_08',weight=1)
G.add_edge('BUY_07','BUY_08',weight=3)
G.add_edge('BUY_08','BUY_09',weight=1)
G.add_edge('BUY_08','BUY_10',weight=4)
G.add_edge('BUY_09','BUY_11',weight=2)

  • 使用 LPA 算法划分社区

使用同步行为出现的次数即 x∩y 值作为输入权重

import networkx as nx
from networkx.algorithms.community import asyn_lpa_communities as lpa  
  
# 定义一个函数来打印社区的节点  
def print_communities(G, communities):  
    for i, community in enumerate(communities):  
        print(f"社区 {i+1}: {community}")  
  
# 定义原始数据  
edges = [  
    ('BUY_01', 'BUY_02', 2),  
    ('BUY_01', 'BUY_03', 2),  
    ('BUY_01', 'BUY_04', 1),  
    ('BUY_02', 'BUY_03', 2),  
    ('BUY_02', 'BUY_04', 1),  
    ('BUY_03', 'BUY_04', 1),  
    ('BUY_04', 'BUY_05', 2),  
    ('BUY_05', 'BUY_06', 1),  
    ('BUY_05', 'BUY_07', 2),  
    ('BUY_06', 'BUY_08', 1),  
    ('BUY_07', 'BUY_08', 3),  
    ('BUY_08', 'BUY_09', 1),  
    ('BUY_08', 'BUY_10', 4),  
    ('BUY_09', 'BUY_11', 2)  
]  
  
# 创建一个空的图并添加边和权重  
G = nx.Graph()  
G.add_weighted_edges_from(edges)  
  
# 计算图的布局(尽管这里我们并不真正绘制图形)  
pos = nx.spring_layout(G)  
  
# LPA本身不稳定,会存在波动
com = list(lpa(G,'weight')) #seed

# 打印社区数量  
print('社区数量:', len(com))  
  
# 打印每个社区的节点  
print_communities(G, com)
社区数量: 3
社区 1: {'BUY_02', 'BUY_03', 'BUY_04', 'BUY_01'}
社区 2: {'BUY_08', 'BUY_06', 'BUY_05', 'BUY_10', 'BUY_07'}
社区 3: {'BUY_09', 'BUY_11'}

  • 对划分的社区进行业务打标

算法将用户分成了三个社区,可以将每个社区中的用户的明细数据拉取出来,观察他们的行为特征。比如对于同一社区的用户,观察他们的评级时间密集程度如何,评价商户有没有集中性,评论相似性,所用设备环境是否正常或者有共性等,基于业务考虑对社区进行打标,如人肉刷评群体。

三、标签传播算法的Python算例

import matplotlib.pyplot as plt
import networkx as nx
from networkx.algorithms.community import asyn_lpa_communities as lpa

# 空手道俱乐部
G   = nx.karate_club_graph()
com = list(lpa(G))
print('社区数量',len(com))

com 
[{0, 1, 2, 3, 7, 8, 9, 11, 12, 13, 17, 19, 21, 30},
{4, 5, 6, 10, 16},
{14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 31, 32, 33}]

# 下面是画图
pos = nx.spring_layout(G) # 节点的布局为spring型
NodeId    = list(G.nodes())
node_size = [G.degree(i)**1.2*90 for i in NodeId] # 节点大小

plt.figure(figsize = (8,6)) # 设置图片大小
nx.draw(G,pos, 
        with_labels=True, 
        node_size =node_size, 
        node_color='w', 
        node_shape = '.'
       )

'''
node_size表示节点大小
node_color表示节点颜色
with_labels=True表示节点是否带标签
'''
color_list = ['pink','orange','r','g','b','y','m','gray','black','c','brown']

for i in range(len(com)):
    nx.draw_networkx_nodes(G, pos, 
                           nodelist=com[i], 
                           node_color = color_list[i+2],  
                           label=True)
plt.show()

总结

标签传播算法(Label Propagation Algorithm,LPA)是一种用于社区发现的简单而有效的半监督方法,其核心思想是通过节点之间标签的传播来划分社区,将具有相似特征的节点归为同一社区。算法适用于各种类型的图,包括社交网络、通信网络等。在实际应用中,标签传播算法被广泛应用于社交网络分析、推荐系统、生物信息学等领域。例如,在社交网络中,它可以帮助识别用户群体、发现潜在的社交圈子;在推荐系统中,可以利用用户行为数据进行社区划分,从而提高推荐的准确性和个性化程度;在生物信息学中,可以用于基因调控网络的模块化分析,发现基因间的关联性。此外,标签传播算法还可用于图像分割、文本聚类等领域。例如,在图像分割中,可以将像素点作为图的节点,利用它们的相似性进行社区划分,从而实现图像的自动分割;在文本聚类中,可以将文档表示为图的节点,通过词语之间的关联性进行社区划分,实现文本的自动分类和聚类。总之标签传播算法是一种简单而有效的社区发现方法,在各种领域都有着广泛的应用前景。通过利用其优势并克服其局限性,可以为社交网络分析、推荐系统、生物信息学等领域的问题提供有力的解决方案。

参考文献

  1. 两种 Python 方法实现社区发现之标签传播算法(LPA)
  2. LPA社区发现算法介绍和代码示例
  3. 27.标签传播算法
posted @ 2024-04-26 12:35  郝hai  阅读(2192)  评论(0)    收藏  举报