Reverse Influence Sampling in Python(譯文)

影響最大化(IM)問題尋求網絡中的一組種子節點,以最大化通過在該種子集啓動的影響級聯激活的預期節點數。先前的文章比較了兩種IM算法Kempe等人(2003) 的Greedy算法和 Leskovec等人 的CELF算法(2007)。多年來,CELF(以及Goyal等人於2011年修改的CELF ++版本)是最快的IM算法,具有理論上可保證的性能範圍。隨後的文獻主要側重於通過啓發式來提高計算效率,而啓發式則犧牲了對速度的理論保證。然而,最近,出現了一種新的解決方法-反向影響採樣(RIS),該方法既快速又在理論上得到了保證,現在已成爲最先進的IM方法之一。這篇文章逐步介紹了RIS算法在Python中的實現,並將其解決方案和計算速度與上一篇文章中描述的CELF算法進行了比較。

The Reverse Influence Sampling Algorithm

啓用RIS系列算法的關鍵見解是Borgs 等人將隨機反向可達集的概念引入IM問題(2014)。通過首先從原圖中根據邊緣的傳播概率 1pe1-p_e 去掉其中某些邊 ee, 得到採樣圖 gg,然後取 gg 中可以“到達”的節點集來生成針對任意節點 vv 的反向可達集 (RR)。隨機反向可達集 (RRR) 只是針對隨機選擇的均勻節點的RR集。下面是一個可視示例。根據每個邊緣分配的概率從左邊的原始網絡 GG 採樣邊緣(本文中的代碼假定所有邊緣共享相同的傳播概率,因此 pe=pp_e = p)。這將在中間圖形中生成一個採樣網絡 gg,在該網絡中傾向於選擇具有較高概率的邊緣。選擇一個隨機節點D,然後得到的RRR集由具有指向D的定向路徑的那些節點組成,這些節點被虛線包圍。
在這裏插入圖片描述
直觀地,如果一個節點 uu 出現在另一個節點 vv 的RR集中,則存在從 uuvv 的定向路徑。因此,包含 uu 的種子集的擴散過程具有激活 vv 的可能性。節點激活以引理形式化,該引理指出,來自任何種子集 SS 的擴散過程將激活任何節點 vv 的概率等於針對 vv 的RR集內包含 SS 中至少一個節點的概率 (參考 Borgs et al 2014)。基於此引理,RIS算法系列分兩個步驟進行:

  • 生成許多​​獨立的RRR集的集合 RR
  • 使用標準貪婪算法選擇 kk 個節點以覆蓋 RR 中的最大RRR集數量,該算法可獲得該問題的 (11/eϵ)(1-1/e-\epsilon) 近似解。

該方法之所以有效,是因爲對於任何種子集 SS,由 SS 覆蓋的 RR 中的RRR集的比例都是 SS 傳播的無偏估計(由於上述引理)。因此,RR 中覆蓋大量RR集的種子集可能具有較大的預期影響,這使其成爲IM問題的良好解決方案。

RIS算法具有很高的計算性能的關鍵在於,與Greedy或CELF不同,它不會重複使用擴展計算過程來逐步構建該問題的解。取而代之的是,它先執行所有的蒙特卡洛模擬/採樣以構造 RR,然後從中選擇整個種子集。因此,該方法避免了貪婪算法及其大多數後繼算法的“浪費”擴展計算,因爲生成的RRR集用於通知網絡內所有節點的擴展。

下面,我用兩個單獨的函數描述RIS算法的實現。第一個是 get_RRS(),它具有圖和傳播概率,並散出隨機的反向可達集。第二個 ris()使用第一個函數生成大量隨機反向可到達集,然後將其用於選擇有影響力的種子集。我們首先加載一些軟件包:

%matplotlib inline
import matplotlib.pyplot as plt
from random import uniform, seed
import numpy as np
import pandas as pd
import time
from igraph import *
import random
from collections import Counter

Create Random Reverse Reachable Set

下面的get_RRS() 函數獲取一個網絡對象,並從該網絡生成一個RRR集。網絡在dataframe(numpy中的一個數據結構,類似矩陣) G 中定義,其中每一行代表網絡中的有向邊,兩列['source','target'] 分別描述給定邊的源節點和目標節點。有很多方法可以用Python表示網絡 (請參閱使用流行的 igraph 程序包實現CELF算法的博文,以及比較四種不同網絡實現的博文),但是此設置是透明的,不需要了解一組方法特定於給定的圖形建模包。該功能包括三個步驟:

  1. G 的源列中包含的所有潛在源節點的集合中隨機選擇源節點。
  2. 通過將傳播參數 pp 與均勻隨機抽籤進行比較以模擬每個邊的採樣過程,從網絡 G 採樣實例 g,然後將抽取的那些邊保存在新的 dataframe 中,代表採樣圖。這與下面的 IC() 函數中用於模擬傳播的方法非常相似,這暗示了RIS爲什麼起作用。
  3. 通過迭代過程自己生成RR集,該過程首先找到邊上通向 source 的所有節點,然後將其添加到 RRS 列表對象。然後,循環將找到所有邊都通向上一步中找到的新節點的節點,依此類推。網絡的dataframe表示使此過程非常容易,因爲我們僅基於目標列進行過濾,然後從源列中選擇“新節點”。替代表示將需要某種形式的鄰近功能。
def get_RRS(G,p):   
    """
    Inputs: G:  Ex2 dataframe of directed edges. Columns: ['source','target']
            p:  Disease propagation probability
    Return: A random reverse reachable set expressed as a list of nodes
    """
    
    # Step 1. Select random source node
    source = random.choice(np.unique(G['source']))
    
    # Step 2. Get an instance of g from G by sampling edges  
    g = G.copy().loc[np.random.uniform(0,1,G.shape[0]) < p]

    # Step 3. Construct reverse reachable set of the random source node
    new_nodes, RRS0 = [source], [source]   
    while new_nodes:
        
        # Limit to edges that flow into the source node
        temp = g.loc[g['target'].isin(new_nodes)]

        # Extract the nodes flowing into the source node
        temp = temp['source'].tolist()

        # Add new set of in-neighbors to the RRS
        RRS = list(set(RRS0 + temp))

        # Find what new nodes were added
        new_nodes = list(set(RRS) - set(RRS0))

        # Reset loop variables
        RRS0 = RRS[:]

    return(RRS)

Create RIS algorithm

ris() 函數分兩步實現了實際的RIS解決方案過程:

  1. 通過遍歷 get_RRS() 函數,生成包含 mc 個種子的隨機反向可達集的大集合 RR
  2. 運行“最大貪婪覆蓋率”算法,該算法僅查找 RRR 集中出現最多的節點。 一旦找到此節點,將從集合 RR 中刪除具有該節點特徵的 RRR 集,然後重複該過程,直到選擇了 kk 個節點並將其存儲在列表對象 SEED 中爲止。

在此過程中,我們還跟蹤時間,將其與下面的CELF運行時間進行比較。

def ris(G,k,p=0.5,mc=1000):    
    """
    Inputs: G:  Ex2 dataframe of directed edges. Columns: ['source','target']
            k:  Size of seed set
            p:  Disease propagation probability
            mc: Number of RRSs to generate
    Return: A seed set of nodes as an approximate solution to the IM problem
    """
    
    # Step 1. Generate the collection of random RRSs
    start_time = time.time()
    R = [get_RRS(G,p) for _ in range(mc)]

    # Step 2. Choose nodes that appear most often (maximum coverage greedy algorithm)
    SEED, timelapse = [], []
    for _ in range(k):
        
        # Find node that occurs most often in R and add to seed set
        flat_list = [item for sublist in R for item in sublist]
        seed = Counter(flat_list).most_common()[0][0]
        SEED.append(seed)
        
        # Remove RRSs containing last chosen seed 
        R = [rrs for rrs in R if seed not in rrs]
        
        # Record Time
        timelapse.append(time.time() - start_time)
    
    return(sorted(SEED),timelapse)

Recap: Independent Cascade and CELF

下面是獨立的級聯傳播函數 IC(),用於計算網絡中給定種子集的傳播;而 celf() 函數,用於輸入一個網絡然後使用CELF算法查找最有影響力的節點。 有關這些功能的更詳細說明,請參見上一篇文章。 但是請注意,這些函數稍有不同,因爲它們在上述函數中使用了相同的網絡 dataframe 表示形式(而不是先前文章中的igraph表示形式)。

def IC(G,S,p=0.5,mc=1000):  
    """
    Input:  G:  Ex2 dataframe of directed edges. Columns: ['source','target']
            S:  Set of seed nodes
            p:  Disease propagation probability
            mc: Number of Monte-Carlo simulations
    Output: Average number of nodes influenced by the seed nodes
    """
    
    # Loop over the Monte-Carlo Simulations
    spread = []
    for _ in range(mc):
        
        # Simulate propagation process      
        new_active, A = S[:], S[:]
        while new_active:
            
            # Get edges that flow out of each newly active node
            temp = G.loc[G['source'].isin(new_active)]

            # Extract the out-neighbors of those nodes
            targets = temp['target'].tolist()

            # Determine those neighbors that become infected
            success  = np.random.uniform(0,1,len(targets)) < p
            new_ones = np.extract(success, targets)
            
            # Create a list of nodes that weren't previously activated
            new_active = list(set(new_ones) - set(A))
            
            # Add newly activated nodes to the set of activated nodes
            A += new_active
            
        spread.append(len(A))
        
    return(np.mean(spread))
    
def celf(G,k,p=0.5,mc=1000):   
    """
    Inputs: G:  Ex2 dataframe of directed edges. Columns: ['source','target']
            k:  Size of seed set
            p:  Disease propagation probability
            mc: Number of Monte-Carlo simulations
    Return: A seed set of nodes as an approximate solution to the IM problem
    """
      
    # --------------------
    # Find the first node with greedy algorithm
    # --------------------
    
    # Compute marginal gain for each node
    candidates, start_time = np.unique(G['source']), time.time()
    marg_gain = [IC(G,[node],p=p,mc=mc) for node in candidates]

    # Create the sorted list of nodes and their marginal gain 
    Q = sorted(zip(candidates,marg_gain), key = lambda x: x[1],reverse=True)

    # Select the first node and remove from candidate list
    S, spread, Q = [Q[0][0]], Q[0][1], Q[1:]
    timelapse = [time.time() - start_time]
    
    # --------------------
    # Find the next k-1 nodes using the CELF list-sorting procedure
    # --------------------
    
    for _ in range(k-1):    

        check = False      
        while not check:
            
            # Recalculate spread of top node
            current = Q[0][0]
            
            # Evaluate the spread function and store the marginal gain in the list
            Q[0] = (current,IC(G,S+[current],p=p,mc=mc) - spread)

            # Re-sort the list
            Q = sorted(Q, key = lambda x: x[1], reverse=True)

            # Check if previous top node stayed on top after the sort
            check = Q[0][0] == current

        # Select the next node
        S.append(Q[0][0])
        spread = Q[0][1]
        timelapse.append(time.time() - start_time)
        
        # Remove the selected node from the list
        Q = Q[1:]
    
    return(sorted(S),timelapse)
    

簡單總結一下IC模型,IC模型已知圖網絡 G 和 種子集合 S,首先初始化新激活的和已經激活的用戶均爲種子集 S,然後直到沒有新種子被激活前,每次循環首先找出種子集合影響的目標用戶集,根據激活概率判斷目標用戶中哪些被激活,然後將本輪激活的用戶除去已經在此前激活過的部分,將增量部分加入到已激活的用戶 AA 裏,計算本輪已激活用戶集 AA 的大小,作爲本輪傳播影響力,迭代直到沒有新節點被激活。

CELF模型用於選取影響力最大的 kk 個節點,首先已知圖網絡 G 和激活概率 p ,第一輪從便利圖中全量節點,利用IC模型輸出每個節點的影響力,選擇影響力最大的一個,然後再循環選擇剩下的 kk-1個,每次更新選擇增量影響力增加最大的節點,直到選出全部節點。

Example 1: A Simple Test Run

我們將首先測試RIS算法,看看它能否爲我們知道兩個最有影響力的節點的簡單玩具示例識別正確的解決方案。 下面我們創建一個10節點/ 20邊緣定向網絡,其中節點0和1最有影響力。 爲此,我們創建了8個從0和1傳出的鏈接,而其他8個節點最多隻有一個。 我們還確保0和1不是鄰居,以便在種子集中擁有一個不會使另一個成爲多餘。 通過繪製網絡,我們可以直觀地瞭解節點0和1最具影響力的原因。

# Create simple network with 0 and 1 as the influential nodes
source = [0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,2,3,4,5]
target = [2,3,4,5,6,7,8,9,2,3,4,5,6,7,8,9,6,7,8,9]

# Create dataframe representation
d = pd.DataFrame({'source':source,'target':target})

# Create igraph representation for plotting
gr = Graph(directed=True)
gr.add_vertices(range(10))
gr.add_edges(zip(source,target))

# Plot graph
gr.vs["label"], gr.es["color"], gr.vs["color"] = range(10), "#B3CDE3", "#FBB4AE"
plot(gr,bbox = (200, 200), margin = 20,layout = gr.layout("kk"))

在這裏插入圖片描述
在此圖上運行每種算法以找到最有影響力的k = 2大小的種子集,即可成功獲得兩個節點(p和mc的選擇在這裏並不重要)

# Run algorithms
ris_output  = ris(d,2,p=0.5,mc=1000)
celf_output = celf(d,2,p=0.5,mc=1000)

# Print resulting seed set
print("RIS seed set:  %s" %ris_output[0])
print("CELF seed set: %s" %celf_output[0])
RIS seed set:  [0, 1]
CELF seed set: [0, 1]

Example 2: A Larger Network

現在,我們不再使用小樣示例,而是考慮使用更大的100節點網絡。 我們將使用 igraph 軟件包中的 Barabasi 方法而不是像上面那樣直接指定網絡,而是自動構建通過Albert-Barabasi優先附着方法(近似於無標度網絡)創建的網絡。 圖形類型的選擇是任意的,因爲任何圖形的要點都不重要。 通過遍歷每個邊並提取源節點和目標節點,將網絡轉換爲 dataframe 表示形式。 繪製網絡圖表明,我們現在正在處理一個更大,更復雜的圖,其中有影響力的節點不明顯。

# Generate Graph
G = Graph.Barabasi(n=100, m=3,directed=True)

# Transform into dataframe of edges
source_nodes = [edge.source for edge in G.es]
target_nodes = [edge.target for edge in G.es]
df = pd.DataFrame({'source': source_nodes,'target': target_nodes})

# Plot graph
G.es["color"], G.vs["color"] = "#B3CDE3", "#FBB4AE"
plot(G,bbox = (280, 280),margin = 11,layout=G.layout("kk"))

在這裏插入圖片描述
與上一篇文章中Greedy算法和CELF算法的比較不同,與上述玩具示例的結果不同,對於有限數量的Monte Carlo模擬/ RRS集,CELF和RIS方法不能產生相同的結果,這在理論上沒有保證。唯一的保證是,這兩種算法都產生一個種子集,該種子集以給定的概率 11/eϵ1-1/e-\epsilon 接近最佳的傳播影響力,但是這些集可能有所不同。

值得花時間在這裏討論每種方法中 mc 參數之間的差異,以及它們在近似中與誤差項 ϵ\epsilon 的關係。在CELF中,mc 指定運行多少個不同的傳播級聯來計算所考慮的每個節點的平均擴展。在RIS中,mc 反而代表的是了集合R中RRS的集合的數量。因此,爲每種方法設置相同的 mc 值通常將無法達到相同的準確度 ϵ\epsilon

因此,爲了進行公平的比較,我們理想地希望在每種方法中設置 mc 的各自值,以使它們都達到相同的 ϵ\epsilon。但是,這說起來容易做起來難。實際上,直到最近,文獻也只是遵循Kempe等人(2003年)的觀點,設置mc = 10000但沒有真正理解CELF或Greedy算法中 mcϵ\epsilon 之間的關係。直到 Tang et al2014年),我們終於得到了關係的明確陳述,其表達如下。CELF至少以概率爲 11n1-\frac{1}{n}返回 ϵ\epsilon 的近似誤差,如果將mc設置爲大於 (8k2+2kϵ)n(l+1)logn+logkϵ2OPT(8k^{2}+2k\epsilon)\cdot n\cdot\frac{(l+1)\log n+\log k}{\epsilon^{2}OPT}
其中OPT是最大的可達影響力,在RIS算法中的等價的表達式爲 (8+2ϵ)nllogn+log(nk)+log2ϵ2OPT(8+2\epsilon)\cdot n\cdot\frac{l\log n+\log \tbinom{n}{k}+\log 2}{\epsilon^{2}OPT}
現在,使用這些表達式的麻煩在於OPT通常是未知的 (因爲我們需要知道要解決的內容才能進行計算!)。因此,在實踐中,文獻試圖爲OPT設定一個下限,這反過來又暗示了上面表達式的上限,因此對爲達到給定what設置mc所做的保守估計。一個超級保守的估計將設置OPT = k,這有效地假設種子集僅激活自己。但這意味着我們創建了太多的RRS集或運行了太多的蒙特卡洛模擬,因此理想情況下,我們希望有一個更好的估計/更嚴格的下限。

但是讓事情變得更加棘手的是,事實證明,找出更嚴格的下限本身就是一個計算密集型過程,因此,在想要創建更少的RRS和不想浪費計算資源之間進行權衡往往是一種折衷。證明減少RRS所需的最佳OPT水平。最近的許多文獻都致力於解決這個問題(參見Tang等人(2014),Tang等人(2015),Nguyen等人(2016)和Huang等人(2017)等)。除了承認它們存在之外,我們在本文中不會涉及這些複雜性。從這裏開始,我們將設置較大的mc值,並希望它們獲得大致相同的ϵ。

#run algorithm
celf_output = celf(df,10,p = 0.1,mc = 10000)
ris_output = ris(df,10,p = 0.1,mc = 10000

下圖比較了隨着種子集大小的增長,兩種算法的速度。最明顯的一點是RIS算法要快得多(大約一個數量級)。該圖的另一個特徵是CELF的計算時間與種子集的大小成比例地增長(儘管不完全相同,請參閱上一篇文章以瞭解原因),而RIS算法的時間相對於種子大小保持相當恆定種子集。這是因爲CELF算法必須在迭代過程的每一輪中爲給定的潛在種子節點計算邊際增益。相反,RIS“預先”執行所有模擬,因此擴展種子集的唯一成本是找到列表中最頻繁出現的節點的“搜索成本”,這是一項相對便宜的計算工作。

# Plot
fig = plt.figure(figsize=(9,6))
ax = fig.add_subplot(111)
ax.plot(range(1,len(celf_output[1])+1),ris_output[1],label="RIS",color="#FBB4AE",lw=4)
ax.plot(range(1,len(celf_output[1])+1),celf_output[1],label="CELF",color="#B3CDE3",lw=4)
ax.legend(loc=2)
plt.ylabel('Computation Time (Seconds)')
plt.xlabel('Size of Seed Set')
plt.title('Computation Time')
plt.tick_params(bottom = False, left = False)
plt.show()

在這裏插入圖片描述
如前所述,唯一的保證是這兩種方法都將產生至少達到最佳散佈的(1-1 / e-1)≈63%的解決方案,這爲產生不同解決方案的方法留下了很大的空間。 據我所知,尚未對方法進行系統的比較,以找出哪種方法在經驗上以及在什麼條件下都能實現出色的傳播。 下面,我們首先打印每種方法產生的種子集,這些種子集雖然有所不同,但顯示出一些重疊。 然後,我們通過IC傳播函數運行每個種子集,這表明它們也實現了大致相同的傳播。

# Print resulting seed set solutions
print("CELF Seed Set: %s" %celf_output[0])
print("RIS Seed Set:  %s" %ris_output[0])

# Compute the spread of each seed set
celf_spread = IC(df,celf_output[0],0.1,mc=10000)
ris_spread  = IC(df,ris_output[0],0.1,mc=10000)

# Print resulting spread
print("\nCELF Spread: %s" %celf_spread)
print("RIS Spread:  %s" %ris_spread)
CELF Seed Set: [22, 28, 41, 69, 70, 75, 81, 82, 91, 93]
RIS Seed Set:  [22, 27, 28, 34, 40, 42, 48, 89, 90, 93]

CELF Spread: 13.328
RIS Spread:  13.3285

在上面的特定示例中,RIS的傳播略高,但這是由於隨機性造成的。 如果我們重複相同的分析,則“最佳”方法將發生變化,但將保持在13.3區域附近。

上面的速度比較顯示了每種算法如何相對於種子集的大小進行縮放。 最後,我們將展示它們如何根據網絡規模進行擴展。 下面,我們爲每種算法求解不同的網絡大小(通過調整一些參數以節省時間),並針對網絡中的節點數繪製計算時間。 同樣,我們發現CELF與網絡規模大致成線性比例關係,而RIS在這些小規模下對網絡規模幾乎無所謂。

CELF_TIME, RIS_TIME, SIZES = [], [], [10,50,100,500,750,1000]

for n in SIZES:
    
    # Generate Graph
    G = Graph.Barabasi(n=n,m=3,directed=True)

    # Transform into dataframe of edges
    source_nodes = [edge.source for edge in G.es]
    target_nodes = [edge.target for edge in G.es]
    df = pd.DataFrame({'source': source_nodes,'target': target_nodes})

    # Run algorithms
    celf_output = celf(df,5,p=0.1,mc=1000)
    ris_output  = ris(df,5,p=0.1,mc=1000)
    
    # Store
    CELF_TIME.append(celf_output[1][-1])
    RIS_TIME.append(ris_output[1][-1])
# Plot computation time with respect to network size
fig = plt.figure(figsize=(9,6))
ax1 = fig.add_subplot(111)
ax1.plot(SIZES, RIS_TIME, label="RIS", color="#FBB4AE",lw=4)
ax1.plot(SIZES, CELF_TIME, label="CELF", color="#B3CDE3",lw=4)
ax1.legend(loc=2)
plt.ylabel('Computation Time (Seconds)')
plt.xlabel('Network Size (Number of Nodes)')
plt.title('Computation Time Scaling with Network Size')
plt.tick_params(bottom = False, left = False)
plt.show()

在這裏插入圖片描述
結論
我們將CELF和RIS算法都實現爲簡單的Python函數,並顯示了以下內容:

兩種算法都會產生相似的種子集並影響傳播。
RIS算法的運行速度快得多,並且隨着網絡大小和種子集大小的增加,伸縮性也更好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章