社區發現算法 - Fast Unfolding(Louvian)算法初探


1. 社團划分

0x1:社區是什么

在社交網絡中,用戶相當於每一個點,用戶之間通過互相的關注關系構成了整個網絡的結構。

在這樣的網絡中,有的用戶之間的連接較為緊密,有的用戶之間的連接關系較為稀疏。其中連接較為緊密的部分可以被看成一個社區,其內部的節點之間有較為緊密的連接,而在兩個社區間則相對連接較為稀疏。

整個整體的結構被稱為社團結構。如下圖,紅色的黑色的點集呈現出社區的結構,

用紅色的點和黑色的點對其進行標注,整個網絡被划分成了兩個部分,其中,這兩個部分的內部連接較為緊密,而這兩個社區之間的連接則較為稀疏。

如何去划分上述的社區便稱為社區划分的問題。

0x2:社區划分的出發點和意圖

直觀地說,community detection的一般目標是要探測網絡中的“塊”cluster或是“社團”community。

這么做的目的和效果有許多,比如說機房里機器的連接方式,這里形成了網絡結構,那么,哪些機器可以視作一個“塊”?進一步地,什么樣的連接方式才有比較高的穩定性呢?如果我們想要讓這組服務癱瘓,選擇什么樣的目標呢?

我們再看一個例子,word association network。即詞的聯想/搭配構成的網絡: 

我們用不同的顏色對community進行標記,可以看到這種detection得到的結果很有意思。

這個網絡從詞bright開始進行演化,到后面分別形成了4個組:ColorsLightAstronomy & Intelligence

可以說以上這4個詞可以較好地概括其所在community的特點(有點聚類的感覺);另外,community中心的詞,比如color, Sun, Smart也有很好的代表性(自動提取摘要)。

同時我們注意到,那些處在交疊位置的詞呢,比如Bright、light等詞,他們是同義項比較多的詞。這個圖也揭示出了這一層含義。

0x3:節點間存在連接的抽象本質 - 邏輯拓朴結構

社區的節點間是網絡拓朴結構,即節點間是存在拓朴連接結構的,我們不能將其和歐式空間或者P空間中的點向量集合空間混為一談

以歐式空間為例,不同的節點向量存在於不同的空間位置中,向量夾角近的點向量彼此距離近,而向量夾角遠的向量彼此距離遠。但是即使是歐式距離很近的向量點,也不一定就代表這它們之間存在拓朴連接關系,只能說在一定的度量下(例如歐式距離度量),這兩個節點很相近。

但是在社區結構中,節點之間沒有什么空間位置的概念。相對的,節點間存在的是一種邏輯拓朴結構,即存在一種共有關系

存在共有關系的節點在邏輯上會聚集為一個社區,而社區之前不存在或者存在很弱的共有關系,則呈現分離的邏輯拓朴結構。

讀者朋友一定要注意不要用空間結構的概念來試圖理解社區結構,不然會陷入理解的困境,社區中的節點只是因為邏輯上的共有關系而聚集在一起而已,彼此之間的位置也沒有實際意義,而社區族群之間的分離也是表達一種邏輯上的弱共有關系。

舉一些實際的例子:

1. 節點代表消費者:節點間的連接代表了它們共同購買了一批書籍,weight代表共同購買的書籍數;
2. 節點代表DNS域名:節點間的連接代表了它們擁有一批共同的src client ip(客戶端),weight代表了共同的src_ip數量;

0x4:什么時候可以使用社區發現算法

下面這句話很明確地說明了在什么業務場景下可以使用社區發現算法:

(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就基本能通過幾輪的迭代完成整體的社區發現過程。

0x5:社區划分的思路概要

什么樣的結構能成為團?一種很直觀的想法是,同一團內的節點連接更緊密,即具有更大的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》

 

2. LOUVAIN算法模型

Louvain算法是一種基於多層次(逐輪啟發式迭代)優化Modularity的算法。Modularity函數最初被用於衡量社區發現算法結果的質量,它能夠刻畫發現的社區的緊密程度。
同時,Modularity函數既然能刻畫社區的緊密程度,也就能夠被用來當作一個優化函數(目標函數),即將結點加入它的某個鄰居所在的社區中,如果能夠提升當前社區結構的modularity。則說明這次迭代優化是可接受的。
下面我們來討論Louvain算法模型的核心組件。

0x1:Modularity的定義 - 描述社區內緊密程度的值Q

模塊度是評估一個社區網絡划分好壞的度量方法,它的物理含義是社區內節點的連邊數與隨機情況下的邊數只差,它的取值范圍是 [−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 沒有變化,則停止迭代,否則繼續迭代,直至收斂。

0x2:模塊度增量 delta Q

模塊增益度是評價本次迭代效果好壞的數值化指標,這是一種啟發式的優化過程。類似決策樹中的熵增益啟發式評價

代表由節點 i 入射集群 C 的權重之和;代表入射集群 C 的總權重;ki 代表入射節點 i 的總權重;

在算法的first phase,判斷一個節點加入到哪個社區,需要找到一個delta Q最大的節點 i,具體的算法我們后面會詳細討論,這里只需要記住 delta Q的作用類似決策樹中的信息增益評估的作用,它幫助整個模型向着Modularity不斷增大的方向去靠攏。

 

3. LOUVAIN算法策略

Louvain算法是基於模塊度的社區發現算法,該算法在效率和效果上都表現較好,並且能夠發現層次性的社區結構,其優化目標是:最大化整個社區網絡的模塊度。

即讓整個社區網絡呈現出一種模塊聚集的結構。

0x1:算法思想的聯想

Louvain算法包括兩個階段,在步驟一它不斷地遍歷網絡中的結點,嘗試將單個結點加入能夠使modularity提升最大的社區中,直到所有結點都不再變化。在步驟二,它處理第一階段的結果,將一個個小的社區歸並為一個超結點來重新構造網絡,這時邊的權重為兩個結點內所有原始結點的邊權重之和。迭代這兩個步驟直至算法穩定。
從核心思想上來看,Louvain算法的分步迭代優化過程,和EM優化算法有異曲同工之妙。
同時有一個題外話值得注意,Louvain算法是一個迭代算法,每一輪迭代都會產出一個當前局部最優的社區結構,所以理論上,假如算法迭代了5次,我們可以得到5個不同粒度層次的社區結構,從業務場景上,這為我們發現不同的社區聚集提供了一個更靈活的視角。
 
關於社區發現算法可以應用在哪些領域,我也還在思考中,從這個算法的思想上來看,我傾向於認為社區發現算法比較適合發現一種"抽象泛共現模式",這種共現是一種泛化的共現,它可以是任何形式的共現,例如
1. 兩台主機擁有類似的網絡對外發包模式 2. 兩台主機間擁有累計的event log序列 3. 兩個攻擊payload擁有類似的詞頻特征,可以認為是同一組漏洞利用方式 4. 在netword gateway上發現了類似的網絡raw流量,也可以反過來用一直的label流量特征進行有監督的聚類 ..

社區發現可能可以提供一種更高層的視角來看待整體的大盤情況,具體的應用場景還需要不斷的摸索。

0x2:關於啟發式/貪婪思想的社區發現的進一步思考

社區發現算法,或者說在社區發現的項目中,很容易遇到的一個問題就是:“社區過大,將過多的outerlier包括到了社區中”,換句話說,社區聚類的過程中沒有能及時收斂。

我們來看下面這張圖:

如果按照啟發式/貪婪思想進行”one-step one node“的社區聚類,O9、O10、O11會被先加入到社區D中,因為在每次這樣的迭代中,D社區內部的緊密度(不管基於node密度還是edge得modularity評估)都是不斷提高,符合算法的check條件,因此,O9、O10、O11會被加入到社區D中。

隨后,O1 ~ O8也會被逐個被加入到社區D中,加入的原因和O9、O10、O11被加入是一樣的。

從局部上來看,這些步驟是合理的,但是如果從上帝視角的全局來看,這種做法導致outerlier被錯誤的聚類到的社區中,導致precision下降。

解決這種問題的一個辦法我覺得可以從CNN/DNN的做法中得到靈感,即設置一個Delta增益的閾值,即在每輪的迭代中(社區擴增后緊密度提升的度量)如果不能超過這個閾值,則判定為收斂成功,立即停止算法迭代。

0x3:社區發現(聚類)的效果非常依賴於weight權重計算的策略和方法

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看來,它們也屬於社區的一部分。

 

4. LOUVAIN算法流程

0x1:算法形式化描述

1)初始化:

將圖中的每個節點看成一個獨立的社區,社區的數目與節點個數相同;

2)開始first phase迭代 - 社區間節點轉移:

對每個節點i,依次嘗試把節點 i 分配到其每個鄰居節點所在的社區,計算分配前與分配后的模塊度變化ΔQ,並記錄ΔQ最大的那個鄰居節點,如果maxΔQ>0,則把節點 i 分配ΔQ最大的那個鄰居節點所在的社區,否則保持不變; 

3)重復2)- 繼續進行社區間節點轉移評估:

直到所有節點的所屬社區不再變化,即社區間的節點轉移結束,可以理解為本輪迭代的 Local Maximization 已達到;

4)second phase - Rebuilding Graph:

因為在這輪的first phase中,社區 C 中新增了一個新的節點 i,而 i 所在的舊的社區少了一個節點,因此需要對整個圖進行一個rebuild。

對圖進行重構,將所有在同一個社區的節點重構成一個新社區,社區內節點之間的邊的權重更新為新節點的環的權重,社區間的邊權重更新為新節點間的邊權重;

5)重復2)- 繼續開始下一輪的first/second phase:

直到整個圖的模塊度不再發生變化。

0x2:算法時間復雜度

從流程來看,該算法能夠產生層次性的社區結構,其中計算耗時較多的是最底一層的社區划分,節點按社區壓縮后,將大大縮小邊和節點數目,
並且計算節點 i 分配到其鄰居 j 時,模塊度的變化只與節點 i、j 的社區有關,與其他社區無關,因此計算很快。
在論文中,把節點 i 分配到鄰居節點 j 所在的社區 c 時模塊度變化為:

DeltaQ 分了兩部分,前面部分表示把節點i加入到社區c后的模塊度,后一部分是節點i作為一個獨立社區和社區c的模塊度

Relevant Link:
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

 

5. A Python implementation of the Louvain method to find communities in large networks

#!/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為例,16、17、18、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

 

6. 其他社區發現算法

Relevant Link:

http://blog.sina.com.cn/s/blog_63891e610101722t.html
https://www.zhihu.com/question/29042018 
https://wenku.baidu.com/view/36fa145a3169a4517623a313.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM