文章目錄
- 由於PageRank演算法有非常高的知名度和普及度,我們接下來以PageRank演算法為例講述“並行計算+資料演算法”的經典搭配,並且這種“海量資料平行處理、迭代多輪後收斂”的分析過程也跟其他的資料採礦或者機器學習演算法應用類似,能起到很好的參考作用。
談到並行計算應用,會有人想到PageRank演算法,我們有成千上萬的網頁分析連結關係確定排名先後,藉助並行計算完成是一個很好的情境。長期以來,Google的創始發明PageRank演算法吸引了很多人學習研究,據說當年Google創始者興奮的找到Yahoo!公司,說他們找到一種更好的搜尋引擎演算法,但是被Yahoo!公司技術人員潑了冷水,說他們關心的不是更好的技術,而是搜尋的盈利。後來Google封裝成了“更先進技術的新一代搜尋引擎”的身份,逐漸取代了市場,並實現了盈利。
由於PageRank演算法有非常高的知名度和普及度,我們接下來以PageRank演算法為例講述“並行計算+資料演算法”的經典搭配,並且這種“海量資料平行處理、迭代多輪後收斂”的分析過程也跟其他的資料採礦或者機器學習演算法應用類似,能起到很好的參考作用。
下面是PageRank演算法的公式:
我們其實可以直接闡述該公式本身,並介紹如何使用並行計算套用上面公式得到各網頁的PageRank值,這樣雖然通過並行計算方式完成了PageRank計算,但是大家仍然不明白上面的PageRank公式是怎麼來的。
我們把這個PageRank演算法公式先放在一邊,看看一個賭錢的遊戲:
有甲、乙、丙三個人賭錢,他們的輸贏關係如下:
甲的錢輸給乙和丙
乙的錢輸給丙
丙的錢輸給甲
例如,甲、乙、丙各有本錢100元,按照以上輸贏關係,玩一把下來:
甲輸給乙50元、輸給丙50元
乙輸給丙100元
丙輸給甲100元
如果僅是玩一把的話很容易算出誰輸誰贏
但如果他們幾個維持這樣的輸贏關係,贏的錢又投進去繼續賭,這樣一輪一輪賭下去的話,最後會是什麼樣子呢?
我們可以寫個單機程式看看,為了方便計算,初始本錢都設為1塊錢,用x1,x2,x3代表甲、乙、丙:
double x1=1.0,x2=1.0,x3=1.0;
用x1_income,x2_income,x3_income代表每賭一把後各人贏的錢,根據輸贏關係:
double x2 income =x1/2.0;
double x3 income =x1/2.0+x2;
double x1_ income =x3;
最後再把各人贏的錢覆蓋掉本錢,繼續往下算。完整程式如下:
// Gamble單機程式
public class Gamble
{
public static double x1=1.0,x2=1.0,x3=1.0;
public static void playgame(){
double x2_income=x1/2.0;
double x3_income=x1/2.0+x2;
double x1_income=x3;
x1=x1_income;
x2=x2_income;
x3=x3_income;
System.out.println("x1:"+x1+", x2:"+x2+", x3:"+x3);
} public static void main(String[] args){
for(int i=0;i<500;i++){
System.out.print("第"+i+"輪 ");
playgame();
}
}
}
我們運行500輪後,看到結果如下:
我們發現,從107輪後,各人的輸贏結果就一直是
x1:1.2000000000000002, x2:0.6000000000000001, x3:1.2000000000000002
…...
可能你都沒想到會有這麼個規律,這樣一直賭下去,雖然各人每輪有輸有贏,但是多輪後的輸贏結果居然保持平衡,維持不變了。用技術術語來說就是多輪迭代後產生了收斂,用俗話來講,就是玩下去甲和丙是不虧的,乙不服輸再繼續賭下去,也不會有扳本的機會的。
我們再把輸贏關係稍微改一下:丙的錢輸給甲和乙
double x2_income=x1/2.0+x3/2.0;
double x3_income=x1/2.0+x2;
double x1_income=x3/2.0;
運行10000輪後,發現又收斂了:
x1:0.6666666666666667, x2:1.0, x3:1.3333333333333333
…
不過這次就變成了“甲是輸的,乙保本,丙是贏的”,我們發現收斂的結果可用於排名,如果給他們做一個賭王排名的話,很顯然:“丙排第一,乙第二、甲第三”。
那麼這樣的收斂是在所有情況下都會發生嗎,什麼情況不會收斂呢?
我們回過頭觀察上面的輸贏關係,甲、乙、丙三人互相各有輸贏,導致錢沒有流走,所以他們三人才一直可以賭下去,如果把輸贏關係改一下,讓甲只輸錢,不贏錢,如下:
double x2_income=x1/2.0+x3/2.0;
double x3_income=x1/2.0+x2;
double x1_income=0;
那麼運行下來會是什麼結果呢?
我們發現很多輪後,全部為0了。我們分析一下過程,第一輪後,甲的錢就輸光了,沒有贏得一分錢。但是乙和丙各有輸贏,他們一直賭到2000多輪時,乙的錢全部輸光了,甲乙都沒錢投進來賭了,導致丙再也贏不到錢了,最後所有人結果都變為0了。
我們再分析一下輸贏關係,甲的錢全部輸給丙和乙後,丙跟乙賭,贏的多輸的少,於是所有的錢慢慢都被丙贏走了,導致最後無法維持一個平衡的輸贏結果。因此,如果我們要維持平衡和收斂,必須保證贏了錢的人不準走,必須又輸給別人才行,讓錢一直在三人圈裡轉不流失。換句話說,如果存在某人只輸不贏,那麼這個遊戲就玩不下去。
賭錢遊戲講完了,我們再看看PageRank演算法的公式:
上面的L(B)代表頁面B指向其他頁面的串連數,我們舉個例子:
假設有A、B、C三張網頁,他們的連結關係如下:
A包含B和C的連結
B包含C的連結
C包含A的連結
根據上面的公式,得到各網頁PR值如下:
PR(B)=PR(A)/2;
PR(B)=PR(A)/2+PR(C);
PR(A)=PR(C);
可以回過頭對照一下,把A、B、C改成甲、乙、丙就是上面舉的賭錢遊戲例子。
那麼q是幹嗎的?公式裡的q叫做逃脫因子,名字很抽象,目的就是用於解決上面賭錢遊戲中“只輸不贏”不收斂的問題,1-q會保證其中一個PR值為0時計算下來不會全部為0,那麼加了這麼一個(…)*q+1-q的關係後,整體的PR值會變化嗎?
當每個頁面的初始PR值為1時,0<=q<=1(計算時通常取值0.8),我們把所有頁面的PR值相加看看,假設有n張網頁:
PR(x1)+ PR(x2)+ …+PR(xn)
=( (PR(x2)/ L(x2)+ … )q+1-q) + … + ( (PR(x1)/ L(x1)+ … )q+1-q)
=(PR(x1) L(x1)/L(x1) + PR(x2) L(x2)/L(x2) + … + PR(xn) L(xn)/L(xn))q + n(1-q)
=( PR(x1) + PR(x2) + … + PR(xn))q + n - nq
=nq + n – n*q
= n
由於初始PR值為1,所以最後所有頁面的PR值相加結果還是為n,保持不變,但是加上(…)*q+1-q的關係後,就避免了PR值為0可以尋求收斂進行排序。
當然實際應用中,這個公式還可以設計的更複雜,並可以通過高等代數矩陣旋轉求解,我們這裡只是為了理解原理,並不是為了做搜尋演算法,所以就不再深入下去了。
總結:世界的很多東西都是零和遊戲,就像炒股,股民賺的錢也就是機構虧的錢,機構賺的錢也就是股民虧的錢,也許股民們應該研究一下PageRank演算法,看看股市起起落落的背後是不是收斂了,收斂了說明炒下去永遠別想解套,而且機構永遠不會虧。
如何使用並行計算方式求PR值:
我們這裡通過fourinone提供的各種並行計算模式去設計,思路方法可以有很多種。
第一次使用可以參考分散式運算上手demo指南,開發包:http://www.skycn.com/soft/68321.html
思路一:可以採取工人互相合并的機制(工人互相合并及receive使用可參見sayhello demo),每個工人分析當前網頁連結,對每個連結進行一次PR值投票,通過receive直接投票到該連結對於網頁所在的工人機器上,這樣經過一輪工人的互相投票,然後再統計一下本機器各網頁所得的投票數得到新的PR值。但是這種方式,對於每個連結投票,都要調用一次receive到其他工人機器,比較耗用頻寬,網頁數量龐大連結眾多時要調用很多次receive,導致效能不高。
思路二:由於求PR值的特點是輸入資料大,輸出資料小,也就是網頁成千上萬占空間多,但是算出來的PR值占空間小,我們姑且用記憶體可以裝下。因此我們優先考慮每個工人統計各自機器上的網頁,計算各連結對應網頁的所得投票,然後返回工頭統一合并得到各網頁的PR值。可以採用最基本的“總—分—總”並行計算模式實現(請參考分散式運算上手demo指南)。
並行計算的拆分和合并設計如下:
可以看到:
工人負責統計各自機器上網頁的各個連結的PR得票。
工頭負責合并累加得到各連結對應網頁的新PR值,并迭代計算。
程式實現:
PageRankWorker:是一個PageRank工人實現,為了方便示範,它通過一個字串數組代表包括的連結(實際上應該從本地網頁檔案裡擷取)
links = new String[]{"B","C"};
然後對連結集合中的每個連結進行PR投票
for(String p:links)
outhouse.setObj(p, pr/links.length);
PageRankCtor:是一個PageRank包工頭實現,它將A、B、C三個網頁的PageRank初始值設定為1.00,然後通過doTaskBatch進行階段計算,doTaskBatch提供一個柵欄機制,等待每個工人計算完成才返回,工頭將各工人返回的連結投票結果合并累加:
pagepr = pagepr+(Double)prwh.getObj(page);
得到各網頁新的PR值(這裡取q值為1進行計算),然後連續迭代500輪計算。
運行步驟:
1、 啟動ParkServerDemo(它的IP連接埠已經在設定檔指定)
java -cp fourinone.jar; ParkServerDemo
2、運行A、B、C三個PageRankWorker,傳入不同的IP和連接埠號碼
java -cp fourinone.jar; PageRankWorker localhost 2008 A
java -cp fourinone.jar; PageRankWorker localhost 2009 B
java -cp fourinone.jar; PageRankWorker localhost 2010 C
3、運行PageRankCtor
java -cp fourinone.jar; PageRankCtor
我們可以看到跟開始的單機程式的結果是一樣的,同時各工人視窗依次輸出了各自的PR值:
完整demo源碼如下:
// ParkServerDemoimport com.fourinone.BeanContext;
public class ParkServerDemo{
public static void main(String[] args){
BeanContext.startPark();
}
}
// PageRankWorker import com.fourinone.MigrantWorker;
import com.fourinone.WareHouse;
import com.fourinone.Workman;
public class PageRankWorker extends MigrantWorker{
public String page = null;
public String[] links = null;
public PageRankWorker(String page, String[] links){
this.page = page;
this.links = links;
}
public WareHouse doTask(WareHouse inhouse) {
Double pr = (Double)inhouse.getObj(page);
System.out.println(pr);
WareHouse outhouse = new WareHouse();
for(String p:links)
outhouse.setObj(p, pr/links.length);//對包括的連結PR投票
return outhouse;
}
public static void main(String[] args){
String[] links = null;
if(args[2].equals("A"))
links = new String[]{"B","C"};//A頁麵包括的連結
else if(args[2].equals("B"))
links = new String[]{"C"};
else if(args[2].equals("C"))
links = new String[]{"A"};
PageRankWorker mw = new PageRankWorker(args[2],links);
mw.waitWorking(args[0],Integer.parseInt(args[1]),"pagerankworker");
}
}
// PageRankCtor
import com.fourinone.Contractor;
import com.fourinone.WareHouse;
import com.fourinone.WorkerLocal;
import java.util.Iterator;
public class PageRankCtor extends Contractor{
public WareHouse giveTask(WareHouse inhouse){
WorkerLocal[] wks = getWaitingWorkers("pagerankworker");
System.out.println("wks.length:"+wks.length);
for(int i=0;i<500;i++){//500輪
WareHouse[] hmarr = doTaskBatch(wks, inhouse);
WareHouse prwh = new WareHouse();
for(WareHouse result:hmarr){
for(Iterator iter=result.keySet().iterator();iter.hasNext();){
String page = (String)iter.next();
Double pagepr = (Double)result.getObj(page);
if(prwh.containsKey(page))
pagepr = pagepr+(Double)prwh.getObj(page);
prwh.setObj(page,pagepr);
}
}
inhouse = prwh;//迭代
System.out.println("No."+i+":"+inhouse);
}
return inhouse;
}
public static void main(String[] args){
PageRankCtor a = new PageRankCtor();
WareHouse inhouse = new WareHouse();
inhouse.setObj("A",1.00d);//A的pr初始值
inhouse.setObj("B",1.00d);//B的pr初始值
inhouse.setObj("C",1.00d);//C的pr初始值
a.giveTask(inhouse);
a.exit(); }
}