標籤傳播演算法(Label Propagation Algorithm, LPA)初探
0. 社區劃分簡介
0x1:非重疊社區劃分方法
在一個網路裡面,每一個樣本只能是屬於一個社群的,那麼這樣的問題就稱為非重疊社區劃分。
在非重疊社區劃分演算法裡面,有很多的方法 :
1. 基於模組度優化的社區劃分
基本思想是將社區劃分問題轉換成了 模組度函式 的優化,而模組度是對社區劃分演算法結果的一個很重要的衡量標準。
模組度函式在實際求解中無法直接計算得到全域性最優解析解(類似深度神經網路對應的複雜高維非線性函式),所以通常是採用近似解法,根據求解方法不同可以分為以下幾種方法:
1. 凝聚方法(down to top): 通過不斷合併不同社群,實現對整個網路的社區劃分,典型的方法有Newman快速演算法,CNM演算法和MSG-MV演算法; 2. 分裂方法(top to down): 通過不斷的刪除網路的邊來實現對整個網路的社區劃分,典型的方法有GN演算法; 3. 直接近似求解模組度函式(近似等價解): 通過優化演算法直接對模組度函式進行求解,典型的方法有EO演算法;
2. 基於譜分析的社區劃分演算法
3. 基於資訊理論的社區劃分演算法
4. 基於標籤傳播的社區劃分演算法
undone
Relevant Link:
https://www.cnblogs.com/LittleHann/p/9078909.html
1. Label Propagation簡介
LPA是一種基於標籤傳播的區域性社區劃分。對於網路中的每一個節點,在初始階段,Label Propagation演算法對於每一個節點都會初始化一個唯一的一個標籤。每一次迭代都會根據與自己相連的節點所屬的標籤改變自己的標籤,更改的原則是選擇與其相連的節點中所屬標籤最多的社群標籤為自己的社群標籤,這就是標籤傳播的含義了。隨著社群標籤不斷傳播。最終,連線緊密的節點將有共同的標籤。
0x1:LPA基本思想
LPA認為每個結點的標籤應該和其大多數鄰居的標籤相同,將一個節點的鄰居節點的標籤中數量最多的標籤作為該節點自身的標籤(bagging思想)。給每個節點新增標籤(label)以代表它所屬的社群,並通過標籤的“傳播”形成同一個“社群”內部擁有同一個“標籤”。
在基本思想上,LPA 和 Kmean 本質非常類似,在 LPA 的每輪迭代中,節點被歸屬於哪個社群,取決於其鄰居中累加權重最大的label(取數量最多的節點列表對應的label是weight=1時的一種特例),而 Kmeans的則是計算和當前節點“最近”的社群,將該節點歸入哪個社群。
但是這兩個演算法還是有細微的區別的:
1. 首先: Kmeans是基於歐式空間計算節點向量間的距離的,而LPA則是根據節點間的“共有關係”以及“共有關係的強弱程度”來度量度量節點間的距離; 2. 第二點: Kmeasn中節點處在歐式空間中,它假設所有節點之間都存在“一定的關係”,不同的距離體現了關係的強弱。但是 LPA 中節點間只有滿足“某種共有關係”時,才存在節點間的邊,沒有共有關係的節點是完全隔斷的,計算鄰居節點的時候也不會計算整個圖結構,而是僅僅計算和該節點有邊連線的節點,從這個角度看,LPA 的這個圖結構具有更強的社群型;
0x2:LPA演算法優點
LPA演算法的最大的優點就是演算法的邏輯非常簡單,相對於優化模組度演算法的過程是非常快的,不用pylouvain那樣的多次迭代優化過程。
LPA演算法利用自身的網路的結構指導標籤傳播,這個過程是無需任何的任何的優化函式,而且演算法初始化之前是不需要知道社群的個數的,隨著演算法迭代最後可以自己知道最終有多少個社群。
筆者思考: 其實 LPA 之所以可以做到無需開發者指定聚類的社群個數,核心原因是因為 LPA 是一個徹底的 down to top 聚類演算法,其實如果對 Kmeans 稍加改造,將其初始化過程改為將所有節點都初始化為單獨的cluster,然後也進行 down to top 的聚類,Kmeasn也可以做到無需顯式指定cluster數量 。
0x3:LPA演算法缺點
劃分結果不穩定,隨機性強是這個演算法致命的缺點。具體體現在:
1. 更新順序:節點標籤更新順序隨機,但是很明顯,越重要的節點越早更新會加速收斂過程; 2. 隨機選擇:如果一個節點的出現次數最大的鄰居標籤不止一個時,隨機選擇一個標籤作為自己標籤。這種隨機性可能會帶來一個雪崩效應,即剛開始一個小小的聚類錯誤會不斷被放大。不過話也說話來,如果相似鄰居節點出現多個,可能是weight計算的邏輯有問題,需要回過頭去優化weight抽象和計算邏輯;
0x4:LPA的一個簡單例子
演算法初始化:a、b、c、d各自為獨立的社群;
第一輪標籤傳播:
一開始c選擇了a,因為大家的社群標籤都是一樣的,所以隨機選擇了一個;
d也根據自己周圍的鄰居節點來確定標籤數,最多的是a,所以就是d為a了;
繼續標籤傳播:以此類推,最後就全部都是a了;
Relevant Link:
https://www.jianshu.com/p/cff65d7595f9 https://arxiv.org/pdf/0709.2938.pdf https://blog.csdn.net/Katherine_hsr/article/details/82343647 http://sighingnow.github.io/%E7%A4%BE%E4%BC%9A%E7%BD%91%E7%BB%9C/community_detection_k_means_clustering.html
2. LPA演算法過程
0x1:演算法過程描述
第一步:先給每個節點分配對應標籤,即節點1對應標籤1,節點i對應標籤i;
第二步:遍歷N個節點(for i=1:N),找到對應節點鄰居,獲取此節點鄰居標籤,找到出現次數最大標籤,若出現次數最多標籤不止一個,則隨機選擇一個標籤替換成此節點標籤;
第三步:若本輪標籤重標記後,節點標籤不再變化(或者達到設定的最大迭代次數),則迭代停止,否則重複第二步
0x2:邊權重計算
社群圖結構中邊的權重代表了這兩個節點之間的的“關係強弱”,這個關係的定義取決於具體的場景,例如:
1. 兩個DNS域名共享的client ip數量; 2. 兩個微博ID的共同好友數量;
0x3:標籤傳播方式
LPA標籤傳播分為兩種傳播方式,同步更新,非同步更新。
1. 同步更新
同步的意思是實時,即時的意思,每個節點label更新後立即生效,其他節點在統計最近鄰社群的時候,永遠取的是當前圖結構中的最新值。
對於節點 ,在第 t 輪迭代時,根據其所在節點在第t-1代的標籤進行更新。也就是
,其中
表示的就是節點
在第 t 次迭代時的社群標籤。
函式 表示的就是取引數節點中社群標籤最多的。
需要注意的是,這種同步更新的方法會存在一個問題,當遇到二分圖的時候,會出現標籤震盪,如下圖:
這種情況和深度學習中SGD在優化到全域性最優點附近時會圍繞最優點附近進行布朗運動(震盪)的原理類似。解決的方法就是設定最大迭代次數,提前停止迭代。
2. 非同步更新
非同步更新方式可以理解為取了一個當前社群的快照資訊,基於上一輪迭代的快照資訊來進行本輪的標籤更新。
0x4: 演算法程式碼
1. 資料集
3列分別是:【node_out,node_in,edge_weitght】
2. 社群初始化
import matplotlib.pyplot as plt import pandas as pd import numpy as np import string def loadData(filePath): f = open(filePath) vector_dict = {} edge_dict = {} for line in f.readlines(): lines = line.strip().split("") for i in range(2): if lines[i] not in vector_dict: vector_dict[lines[i]] = int(lines[i]) edge_list = [] if len(lines) == 3: edge_list.append(lines[1 - i] + ":" + lines[2]) else: edge_list.append(lines[1 - i] + ":" + "1") edge_dict[lines[i]] = edge_list else: edge_list = edge_dict[lines[i]] if len(lines) == 3: edge_list.append(lines[1 - i] + ":" + lines[2]) else: edge_list.append(lines[1 - i] + ":" + "1") edge_dict[lines[i]] = edge_list return vector_dict, edge_dict if __name__ == '__main__': filePath = './label_data.txt' vector, edge = loadData(filePath) print(vector) print(edge)
初始化時,所有節點都是一個獨立的社群。
3. LPA社群聚類迭代
# -*- coding: utf-8 -*- import matplotlib.pyplot as plt import pandas as pd import numpy as np import string def loadData(filePath): f = open(filePath) vector_dict = {} edge_dict = {} for line in f.readlines(): lines = line.strip().split("") for i in range(2): if lines[i] not in vector_dict: vector_dict[lines[i]] = int(lines[i]) edge_list = [] if len(lines) == 3: edge_list.append(lines[1 - i] + ":" + lines[2]) else: edge_list.append(lines[1 - i] + ":" + "1") edge_dict[lines[i]] = edge_list else: edge_list = edge_dict[lines[i]] if len(lines) == 3: edge_list.append(lines[1 - i] + ":" + lines[2]) else: edge_list.append(lines[1 - i] + ":" + "1") edge_dict[lines[i]] = edge_list return vector_dict, edge_dict def get_max_community_label(vector_dict, adjacency_node_list): label_dict = {} for node in adjacency_node_list: node_id_weight = node.strip().split(":") node_id = node_id_weight[0] node_weight = int(node_id_weight[1]) # 按照label為group維度,統計每個label的weight累加和 if vector_dict[node_id] not in label_dict: label_dict[vector_dict[node_id]] = node_weight else: label_dict[vector_dict[node_id]] += node_weight sort_list = sorted(label_dict.items(), key=lambda d: d[1], reverse=True) return sort_list[0][0] def check(vector_dict, edge_dict): for node in vector_dict.keys(): adjacency_node_list = edge_dict[node]# 獲取該節點的鄰居節點 node_label = vector_dict[node]# 獲取該節點當前label label = get_max_community_label(vector_dict, adjacency_node_list)# 從鄰居節點列表中選擇weight累加和最大的label if node_label >= label: continue else: return 0#找到weight權重累加和更大的label return 1 def label_propagation(vector_dict, edge_dict): t = 0 print('First Label: ') while True: if (check(vector_dict, edge_dict) == 0): t = t + 1 print('iteration: ', t) # 每輪迭代都更新一遍所有節點的社群label for node in vector_dict.keys(): adjacency_node_list = edge_dict[node] vector_dict[node] = get_max_community_label(vector_dict, adjacency_node_list) else: break return vector_dict if __name__ == '__main__': filePath = './label_data.txt' vector, edge = loadData(filePath) print "load and initial the community...." #print(vector) #print(edge) print "start lpa clustering...." vector_dict = label_propagation(vector, edge) print "ending lpa clustering...." print "the finnal cluster result...." print(vector_dict) cluster_group = dict() for node in vector_dict.keys(): cluster_id = vector_dict[node] print "cluster_id, node", cluster_id, node if cluster_id not in cluster_group.keys(): cluster_group[cluster_id] = [node] else: cluster_group[cluster_id].append(node) print cluster_group
最後得到的聚類社群為:
{8: ['15', '9', '8'], 13: ['11', '10', '13', '12', '14'], 6: ['3', '7', '6'], 5: ['1', '0', '2', '5', '4']}
Relevant Link:
https://github.com/GreenArrow2017/MachineLearning/tree/master/MachineLearning/Label%20Propagation https://www.jianshu.com/p/cff65d7595f9
3. LPA演算法改進思路
0x1:標籤隨機選擇改進
給節點或邊新增權重(勢函式、模組密度優化、LeaderRank值、區域性拓撲資訊的相似度、標籤從屬係數等),資訊熵等描述節點的傳播優先度。
這樣,在進行鄰居節點的最大標籤統計的時候,可以將鄰居節點的weight權值等作為參考因素。
0x2:標籤初始化改進
可以提取一些較為緊密的子結構來作為標籤傳播的初始標籤(例如非重疊最小極大團提取演算法),或通過初始社區劃分演算法先確定社群的雛形再進行傳播。
Relevant Link:
https://www.cnblogs.com/bethansy/p/6953625.html https://blog.csdn.net/zzz24512653/article/details/26151669