近期把以前協同過濾的練習拿出來炒了回冷飯,結合最近正學着Python,完成了一個計算好友類似度的簡單實例。 python
背景是一個8萬多人的小型社區,平均每一個用戶添加了4.792名好友,好友數最多的用戶有3000多名好友,也有4萬多用戶沒有添加任何好友(挺符合社交網絡長尾效應的)。 linux
話說經典的計算好友交集大小的代碼應該是這樣的: 算法
for i in user_ids: for j in user_ids: if i == j: continue s = len(friends_i & friends_j)
(friends_i和friends_j分別表示i和j的好友集合) 網絡
這種實現的算法複雜度是O(N^2*M*2),其中N表示用戶規模,M表示計算共同好友的兩名用戶添加的最小好友數。經測算,大概每名用戶須要5s的計算時間。 app
而MapReduce就是把原來一步能完成的工做切成了三步,mapper -> sort -> reducer。其中mapper負責化整爲零,把要計算的數據轉化成一行一行的key-value,sort負責把相同的key比鄰排列,reducer則負責和mapper相反的工做,把零散的value值以必定的模式(好比累加)結合起來。 設計
網上流傳的演示MapReduce的都是一個WordCount的例子,好友類似度的計算涉及到兩個對象的關係,數據的輸入和key-value的設計仍是要花點心思的。 code
具體mapper的程序是這樣實現的: 對象
def do_map(): for i in user_ids: if friends_i is None: continue for j in friends_i: if friends_j is None: continue for k in friends_j: # 排除k和i原本就已是好友的狀況 if k == i or k in friends_i: continue print '%s_%s,%s' % (i, k, 1)
這一步輸出的是這樣的key-value格式: 排序
74722_10622,1 it
50041_10622,1
74722_10622,1
10622_50041,1
……
兩個有共同好友的id用下劃線分割,value表示出現的次數。
通過了sort以後,相同的key被放在一塊兒,變成了這樣的排列:
10622_50041,1
50041_10622,1
74722_10622,1
74722_10622,1
……
reducer是一個標準的累加計數程序,和WordCount的實現並沒有二致:
def do_reduce(fin = sys.stdin): prev_key = None key = None current_count = 0 for line in fin: key, count = line.split(',', 1) count = int(count) if key == prev_key: current_count += count else: if prev_key: print >> x, '%s,%s' % (prev_key, current_count) current_count = count prev_key = key #打印最後一行 if key == prev_key: print '%s,%s' % (prev_key, current_count)
通過reducer以後,相同的key出現的次數被累加,得出任意兩個用戶的共同好友數,也就是好友類似度的分子:
10622_50041,1
50041_10622,1
74722_10622,2
……
總共耗時17分21秒。與頭一種算法相比,MapReduce的計算時間只有前者的百分之一。
大概估算了算法複雜度:
1. maper的算法複雜度大約是O(N*M^2),其中M表示整個社區全部用戶的平均好友數,應該能夠輕鬆得證,任意兩名用戶的好友數積的平均值小於平均好友數的平方。
2. linux的sort採用的是歸併排序,算法複雜度不超過O(N*logN)。
3. reducer的算法複雜度大約是O(N*M)。
可見MapReduce確實下降了算法複雜度,另外一方面也歸功於Python在文本處理方面的效率。
後記:
以後好奇了一把,用python的dict重複了實驗,發現寫dict寫到一半就假死了,dict大小是40665020。