在社交網絡中,用戶至關於每個點,用戶之間經過互相的關注關係構成了整個網絡的結構。html
在這樣的網絡中,有的用戶之間的鏈接較爲緊密,有的用戶之間的鏈接關係較爲稀疏。其中鏈接較爲緊密的部分能夠被當作一個社區,其內部的節點之間有較爲緊密的鏈接,而在兩個社區間則相對鏈接較爲稀疏。node
整個總體的結構被稱爲社團結構。以下圖,紅色的黑色的點集呈現出社區的結構,python
用紅色的點和黑色的點對其進行標註,整個網絡被劃分紅了兩個部分,其中,這兩個部分的內部鏈接較爲緊密,而這兩個社區之間的鏈接則較爲稀疏。git
如何去劃分上述的社區便稱爲社區劃分的問題。github
直觀地說,community detection的通常目標是要探測網絡中的「塊」cluster或是「社團」community。算法
這麼作的目的和效果有許多,好比說機房裏機器的鏈接方式,這裏造成了網絡結構,那麼,哪些機器能夠視做一個「塊」?進一步地,什麼樣的鏈接方式纔有比較高的穩定性呢?若是咱們想要讓這組服務癱瘓,選擇什麼樣的目標呢?
咱們再看一個例子,word association network。即詞的聯想/搭配構成的網絡: 網絡
咱們用不一樣的顏色對community進行標記,能夠看到這種detection獲得的結果頗有意思。app
這個網絡從詞bright開始進行演化,到後面分別造成了4個組:Colors, Light, Astronomy & Intelligence。ide
能夠說以上這4個詞能夠較好地歸納其所在community的特色(有點聚類的感受);另外,community中心的詞,好比color, Sun, Smart也有很好的表明性(自動提取摘要)。函數
同時咱們注意到,那些處在交疊位置的詞呢,好比Bright、light等詞,他們是同義項比較多的詞。這個圖也揭示出了這一層含義。
社區的節點間是網絡拓樸結構,即節點間是存在拓樸鏈接結構的,咱們不能將其和歐式空間或者P空間中的點向量集合空間混爲一談。
以歐式空間爲例,不一樣的節點向量存在於不一樣的空間位置中,向量夾角近的點向量彼此距離近,而向量夾角遠的向量彼此距離遠。可是即便是歐式距離很近的向量點,也不必定就表明這它們之間存在拓樸鏈接關係,只能說在必定的度量下(例如歐式距離度量),這兩個節點很相近。
可是在社區結構中,節點之間沒有什麼空間位置的概念。相對的,節點間存在的是一種邏輯拓樸結構,即存在一種共有關係。
存在共有關係的節點在邏輯上會彙集爲一個社區,而社區以前不存在或者存在很弱的共有關係,則呈現分離的邏輯拓樸結構。
讀者朋友必定要注意不要用空間結構的概念來試圖理解社區結構,否則會陷入理解的困境,社區中的節點只是由於邏輯上的共有關係而彙集在一塊兒而已,彼此之間的位置也沒有實際意義,而社區族羣之間的分離也是表達一種邏輯上的弱共有關係。
舉一些實際的例子:
1. 節點表明消費者:節點間的鏈接表明了它們共同購買了一批書籍,weight表明共同購買的書籍數; 2. 節點表明DNS域名:節點間的鏈接表明了它們擁有一批共同的src client ip(客戶端),weight表明了共同的src_ip數量;
下面這句話很明確地說明了在什麼業務場景下可使用社區發現算法:
(Newman and Gievan 2004) A community is a subgraph containing nodes which are more densely linked to each other than to the rest of the graph or equivalently, a graph has a community structure if the number of links into any subgraph is higher than the number of links between those subgraphs.
即咱們須要先肯定要解決的業務場景中,存在明顯的彙集規律,節點(能夠是抽象的)之間造成必定的族羣結構,而不是呈現無規律的隨機分散。同時另外一方面,這種彙集的結構是「有意義的」,這裏所謂的有意義是指這種彙集自己能夠翻譯爲必定的上層業務場景的表現。
可是不少時候,咱們業務場景中的數據集之間的共有關係並非表現的很明顯,即節點之間互相都或多或少存在一些共有關係,這樣直接進行社區發現效果確定是很差的。
因此一個很重要點是,咱們在進行社區發現以前,必定要進行數據降噪。
理想狀況下,降噪後獲得的數據集已是社區徹底內聚,社區間徹底零鏈接,這樣pylouvain只要一輪運行就直接獲得結果。固然實際場景中不可能有這麼好的狀況,數據源質量,專家經驗的豐富程度等等都會影響降噪的效果,通常狀況下,降噪只要能cutoff 90%以上的噪音,pylouvain就基本能經過幾輪的迭代完成總體的社區發現過程。
什麼樣的結構能成爲團?一種很直觀的想法是,同一團內的節點鏈接更緊密,即具備更大的density。
接下來的問題是,什麼樣的metrics能夠用來描述這種density?Louvian 定義了一個數值上的概念(本質上就是一個目標函數),有了這個目標函數,就能夠引出接下來要討論的 method based on modularity optimization
要注意的,社區劃分有不少不一樣的算法,本文討論的 Fast Unfolding(Louvian)只是其中一種,而這種所謂的density密度評估方法也其實其中一種思想,不要固話地認爲社區劃分就只有這一種方法。
Relevant Link:
https://stackoverflow.com/questions/21814235/how-can-modularity-help-in-network-analysis http://iopscience.iop.org/article/10.1088/1742-5468/2008/10/P10008/fulltext/ https://www.researchgate.net/publication/1913681_Fast_Unfolding_of_Communities_in_Large_Networks?enrichId=rgreq-d403e26a5cb211b7053c36946c71acb3-XXX&enrichSource=Y292ZXJQYWdlOzE5MTM2ODE7QVM6MTAxOTUyNjc5NTc5NjY3QDE0MDEzMTg4MjE3ODA%3D&el=1_x_3&_esc=publicationCoverPdf https://www.jianshu.com/p/4ebe42dfa8ec https://blog.csdn.net/u011089523/article/details/79090453 https://blog.csdn.net/google19890102/article/details/48660239 《Fast Unfolding of Communities in Large Networks》
模塊度是評估一個社區網絡劃分好壞的度量方法,它的物理含義是社區內節點的連邊數與隨機狀況下的邊數只差,它的取值範圍是 [−1/2,1),其定義以下:
A爲鄰接矩陣,Aij表明了節點 i 和節點 j 之間 邊的權重,網絡不是帶權圖時,全部邊的權重能夠看作是 1;
是全部與節點 i 相連的 邊的權重之和(度數),kj也是一樣;
表示全部邊的權重之和(邊的數目),充當歸一化的做用;
是節點 i 的社區,
函數表示若節點 i 和節點 j 在同一個社區內,則返回 1,不然返回 0;
模塊度的公式定義能夠做以下簡化:
其中 Σin 表示社區 C 內的邊的權重之和;Σtot 表示與社區 C 內的節點相連的全部邊的權重之和。
上面的公式還能夠進一步簡化成:
這樣模塊度也能夠理解是:
首先modularity是針對一個社區的全部節點進行了累加計算。
modularity Q的計算公式背後體現了這種思想:社區內部邊的權重減去全部與社區節點相連的邊的權重和,對無向圖更好理解,即社區內部邊的度數減去社區內節點的總度數。
能夠直觀去想象一下,若是一個社區節點徹底是「封閉的(即全部節點都互相內部鏈接,可是不和社區外部其餘節點有鏈接,則modularity公式的計算結果爲1)」
基於模塊度的社區發現算法,都是以最大化模塊度Q爲目標。能夠看到,這種模型能夠支持咱們經過策略優化,去不斷地構造出一個內部彙集,外部稀疏鏈接的社區結構
在一輪迭代後,若整個 Q 沒有變化,則中止迭代,不然繼續迭代,直至收斂。
模塊增益度是評價本次迭代效果好壞的數值化指標,這是一種啓發式的優化過程。相似決策樹中的熵增益啓發式評價。
表明由節點 i 入射集羣 C 的權重之和;
表明入射集羣 C 的總權重;ki 表明入射節點 i 的總權重;
在算法的first phase,判斷一個節點加入到哪一個社區,須要找到一個delta Q最大的節點 i,具體的算法咱們後面會詳細討論,這裏只須要記住 delta Q的做用相似決策樹中的信息增益評估的做用,它幫助整個模型向着Modularity不斷增大的方向去靠攏。
Louvain算法是基於模塊度的社區發現算法,該算法在效率和效果上都表現較好,而且可以發現層次性的社區結構,其優化目標是:最大化整個社區網絡的模塊度。
即讓整個社區網絡呈現出一種模塊彙集的結構。
1. 兩臺主機擁有相似的網絡對外發包模式 2. 兩臺主機間擁有累計的event log序列 3. 兩個攻擊payload擁有相似的詞頻特徵,能夠認爲是同一組漏洞利用方式 4. 在netword gateway上發現了相似的網絡raw流量,也能夠反過來用一直的label流量特徵進行有監督的聚類 ..
社區發現可能能夠提供一種更高層的視角來看待總體的大盤狀況,具體的應用場景還須要不斷的摸索。
社區發現算法,或者說在社區發現的項目中,很容易遇到的一個問題就是:「社區過大,將過多的outerlier包括到了社區中」,換句話說,社區聚類的過程當中沒有能及時收斂。
咱們來看下面這張圖:
若是按照啓發式/貪婪思想進行」one-step one node「的社區聚類,O九、O十、O11會被先加入到社區D中,由於在每次這樣的迭代中,D社區內部的緊密度(無論基於node密度仍是edge得modularity評估)都是不斷提升,符合算法的check條件,所以,O九、O十、O11會被加入到社區D中。
隨後,O1 ~ O8也會被逐個被加入到社區D中,加入的緣由和O九、O十、O11被加入是同樣的。
從局部上來看,這些步驟是合理的,可是若是從上帝視角的全局來看,這種作法致使outerlier被錯誤的聚類到的社區中,致使precision降低。
解決這種問題的一個辦法我以爲能夠從CNN/DNN的作法中獲得靈感,即設置一個Delta增益的閾值,即在每輪的迭代中(社區擴增後緊密度提高的度量)若是不能超過這個閾值,則斷定爲收斂成功,當即中止算法迭代。
louvain社區發現是將一個無向權重圖,轉化爲多個節點集合,每一個節點集合表明了一個社區。
社區發現的思想是很是直接簡單的,由於聚類的效果很是依賴於weight權重計算的方法,咱們選擇的權重計算方法必需要可以「很好地」在值域空間中分離開來,不然會致使overcluster。
舉個例子,假設咱們有一組節點間權重,咱們按列的形式寫出來:
A - B:2 A - C:2 B - C:2 D - E:12(明顯和2不同) D - F:13 E - F:14 F - G:11 A - D:1(社區間存在弱關係) B - F:1
上述社區中,咱們很容易理解,應該分爲兩個社區:
A/B/C、D/E/F
由於這2個社區知足社區內內聚,社區間相異的社區拓樸結構特性。
反過來能夠想象,若是weight權重的計算公式不夠合理,社區間的關係(A-D、B-F)和各自的社區weight很接近,則pylouvain就沒法很好的將兩個社區區分開來了。由於在pylouvain看來,它們也屬於社區的一部分。
將圖中的每一個節點當作一個獨立的社區,社區的數目與節點個數相同;
對每一個節點i,依次嘗試把節點 i 分配到其每一個鄰居節點所在的社區,計算分配前與分配後的模塊度變化ΔQ,並記錄ΔQ最大的那個鄰居節點,若是maxΔQ>0,則把節點 i 分配ΔQ最大的那個鄰居節點所在的社區,不然保持不變;
直到全部節點的所屬社區再也不變化,即社區間的節點轉移結束,能夠理解爲本輪迭代的 Local Maximization 已達到;
由於在這輪的first phase中,社區 C 中新增了一個新的節點 i,而 i 所在的舊的社區少了一個節點,所以須要對整個圖進行一個rebuild。
對圖進行重構,將全部在同一個社區的節點重構成一個新社區,社區內節點之間的邊的權重更新爲新節點的環的權重,社區間的邊權重更新爲新節點間的邊權重;
直到整個圖的模塊度再也不發生變化。
DeltaQ 分了兩部分,前面部分表示把節點i加入到社區c後的模塊度,後一部分是節點i做爲一個獨立社區和社區c的模塊度
https://blog.csdn.net/xuanyuansen/article/details/68941507 https://www.cnblogs.com/fengfenggirl/p/louvain.html http://www.cnblogs.com/allanspark/p/4197980.html https://github.com/gephi/gephi/wiki https://blog.csdn.net/qq547276542/article/details/70175157
#!/usr/bin/env python3 # -*- coding: utf-8 -*- ''' Implements the Louvain method. Input: a weighted undirected graph Ouput: a (partition, modularity) pair where modularity is maximum ''' class PyLouvain: ''' Builds a graph from _path. _path: a path to a file containing "node_from node_to" edges (one per line) ''' @classmethod def from_file(cls, path): f = open(path, 'r') lines = f.readlines() f.close() nodes = {} edges = [] for line in lines: n = line.split() if not n: break nodes[n[0]] = 1 nodes[n[1]] = 1 w = 1 if len(n) == 3: w = int(n[2]) edges.append(((n[0], n[1]), w)) # rebuild graph with successive identifiers nodes_, edges_ = in_order(nodes, edges) print("%d nodes, %d edges" % (len(nodes_), len(edges_))) return cls(nodes_, edges_) ''' Builds a graph from _path. _path: a path to a file following the Graph Modeling Language specification ''' @classmethod def from_gml_file(cls, path): f = open(path, 'r') lines = f.readlines() f.close() nodes = {} edges = [] current_edge = (-1, -1, 1) in_edge = 0 for line in lines: words = line.split() if not words: break if words[0] == 'id': nodes[int(words[1])] = 1 elif words[0] == 'source': in_edge = 1 current_edge = (int(words[1]), current_edge[1], current_edge[2]) elif words[0] == 'target' and in_edge: current_edge = (current_edge[0], int(words[1]), current_edge[2]) elif words[0] == 'value' and in_edge: current_edge = (current_edge[0], current_edge[1], int(words[1])) elif words[0] == ']' and in_edge: edges.append(((current_edge[0], current_edge[1]), 1)) current_edge = (-1, -1, 1) in_edge = 0 nodes, edges = in_order(nodes, edges) print("%d nodes, %d edges" % (len(nodes), len(edges))) return cls(nodes, edges) ''' Initializes the method. _nodes: a list of ints _edges: a list of ((int, int), weight) pairs ''' def __init__(self, nodes, edges): self.nodes = nodes self.edges = edges # precompute m (sum of the weights of all links in network) # k_i (sum of the weights of the links incident to node i) self.m = 0 self.k_i = [0 for n in nodes] self.edges_of_node = {} self.w = [0 for n in nodes] for e in edges: self.m += e[1] self.k_i[e[0][0]] += e[1] self.k_i[e[0][1]] += e[1] # there's no self-loop initially # save edges by node if e[0][0] not in self.edges_of_node: self.edges_of_node[e[0][0]] = [e] else: self.edges_of_node[e[0][0]].append(e) if e[0][1] not in self.edges_of_node: self.edges_of_node[e[0][1]] = [e] elif e[0][0] != e[0][1]: self.edges_of_node[e[0][1]].append(e) # access community of a node in O(1) time self.communities = [n for n in nodes] self.actual_partition = [] ''' Applies the Louvain method. ''' def apply_method(self): network = (self.nodes, self.edges) best_partition = [[node] for node in network[0]] best_q = -1 i = 1 while 1: i += 1 partition = self.first_phase(network) q = self.compute_modularity(partition) partition = [c for c in partition if c] # clustering initial nodes with partition if self.actual_partition: actual = [] for p in partition: part = [] for n in p: part.extend(self.actual_partition[n]) actual.append(part) self.actual_partition = actual else: self.actual_partition = partition if q == best_q: # 若是本輪迭代modularity沒有改變,則認爲收斂,中止 break network = self.second_phase(network, partition) best_partition = partition best_q = q return (self.actual_partition, best_q) ''' Computes the modularity of the current network. _partition: a list of lists of nodes ''' def compute_modularity(self, partition): q = 0 m2 = self.m * 2 for i in range(len(partition)): q += self.s_in[i] / m2 - (self.s_tot[i] / m2) ** 2 return q ''' Computes the modularity gain of having node in community _c. _node: an int _c: an int _k_i_in: the sum of the weights of the links from _node to nodes in _c ''' def compute_modularity_gain(self, node, c, k_i_in): return 2 * k_i_in - self.s_tot[c] * self.k_i[node] / self.m ''' Performs the first phase of the method. _network: a (nodes, edges) pair ''' def first_phase(self, network): # make initial partition best_partition = self.make_initial_partition(network) while 1: improvement = 0 for node in network[0]: node_community = self.communities[node] # default best community is its own best_community = node_community best_gain = 0 # remove _node from its community best_partition[node_community].remove(node) best_shared_links = 0 for e in self.edges_of_node[node]: if e[0][0] == e[0][1]: continue if e[0][0] == node and self.communities[e[0][1]] == node_community or e[0][1] == node and self.communities[e[0][0]] == node_community: best_shared_links += e[1] self.s_in[node_community] -= 2 * (best_shared_links + self.w[node]) self.s_tot[node_community] -= self.k_i[node] self.communities[node] = -1 communities = {} # only consider neighbors of different communities for neighbor in self.get_neighbors(node): community = self.communities[neighbor] if community in communities: continue communities[community] = 1 shared_links = 0 for e in self.edges_of_node[node]: if e[0][0] == e[0][1]: continue if e[0][0] == node and self.communities[e[0][1]] == community or e[0][1] == node and self.communities[e[0][0]] == community: shared_links += e[1] # compute modularity gain obtained by moving _node to the community of _neighbor gain = self.compute_modularity_gain(node, community, shared_links) if gain > best_gain: best_community = community best_gain = gain best_shared_links = shared_links # insert _node into the community maximizing the modularity gain best_partition[best_community].append(node) self.communities[node] = best_community self.s_in[best_community] += 2 * (best_shared_links + self.w[node]) self.s_tot[best_community] += self.k_i[node] if node_community != best_community: improvement = 1 if not improvement: break return best_partition ''' Yields the nodes adjacent to _node. _node: an int ''' def get_neighbors(self, node): for e in self.edges_of_node[node]: if e[0][0] == e[0][1]: # a node is not neighbor with itself continue if e[0][0] == node: yield e[0][1] if e[0][1] == node: yield e[0][0] ''' Builds the initial partition from _network. _network: a (nodes, edges) pair ''' def make_initial_partition(self, network): partition = [[node] for node in network[0]] self.s_in = [0 for node in network[0]] self.s_tot = [self.k_i[node] for node in network[0]] for e in network[1]: if e[0][0] == e[0][1]: # only self-loops self.s_in[e[0][0]] += e[1] self.s_in[e[0][1]] += e[1] return partition ''' Performs the second phase of the method. _network: a (nodes, edges) pair _partition: a list of lists of nodes ''' def second_phase(self, network, partition): nodes_ = [i for i in range(len(partition))] # relabelling communities communities_ = [] d = {} i = 0 for community in self.communities: if community in d: communities_.append(d[community]) else: d[community] = i communities_.append(i) i += 1 self.communities = communities_ # building relabelled edges edges_ = {} for e in network[1]: ci = self.communities[e[0][0]] cj = self.communities[e[0][1]] try: edges_[(ci, cj)] += e[1] except KeyError: edges_[(ci, cj)] = e[1] edges_ = [(k, v) for k, v in edges_.items()] # recomputing k_i vector and storing edges by node self.k_i = [0 for n in nodes_] self.edges_of_node = {} self.w = [0 for n in nodes_] for e in edges_: self.k_i[e[0][0]] += e[1] self.k_i[e[0][1]] += e[1] if e[0][0] == e[0][1]: self.w[e[0][0]] += e[1] if e[0][0] not in self.edges_of_node: self.edges_of_node[e[0][0]] = [e] else: self.edges_of_node[e[0][0]].append(e) if e[0][1] not in self.edges_of_node: self.edges_of_node[e[0][1]] = [e] elif e[0][0] != e[0][1]: self.edges_of_node[e[0][1]].append(e) # resetting communities self.communities = [n for n in nodes_] return (nodes_, edges_) ''' Rebuilds a graph with successive nodes' ids. _nodes: a dict of int _edges: a list of ((int, int), weight) pairs ''' def in_order(nodes, edges): # rebuild graph with successive identifiers nodes = list(nodes.keys()) nodes.sort() i = 0 nodes_ = [] d = {} for n in nodes: nodes_.append(i) d[n] = i i += 1 edges_ = [] for e in edges: edges_.append(((d[e[0][0]], d[e[0][1]]), e[1])) return (nodes_, edges_)
社區發現的最後一輪結果爲:
以polbooks.gml爲例,1六、1七、1八、19被歸類爲同一個社區:
node [ id 16 label "Betrayal" value "c" ] node [ id 17 label "Shut Up and Sing" value "c" ] node [ id 18 label "Meant To Be" value "n" ] node [ id 19 label "The Right Man" value "c" ]
從讀者的共同購買狀況做爲權重評估,社區發現的結果暗示了這幾本書可能屬於同一類的書籍
利用louvain進行社區發現的核心在於,咱們須要對咱們的業務場景進行抽象,提取出node1_src/node_dst的節點概念,同時要經過專家領域經驗,進行節點間weight的計算,獲得了weight後,就能夠經過louvain這種迭代算法進行社區拓樸的發現了。
Relevant Link:
http://www.cnblogs.com/allanspark/p/4197980.html https://arxiv.org/pdf/0803.0476.pdf https://github.com/LittleHann/pylouvain http://www.cnblogs.com/allanspark/p/4197980.html https://www.jianshu.com/p/e543dc63454f
Relevant Link:
http://blog.sina.com.cn/s/blog_63891e610101722t.html https://www.zhihu.com/question/29042018 https://wenku.baidu.com/view/36fa145a3169a4517623a313.html