1. 程式人生 > 實用技巧 >Brown Clustering演算法和程式碼學習

Brown Clustering演算法和程式碼學習

2019獨角獸企業重金招聘Python工程師標準>>> hot3.png

一、演算法

布朗聚類是一種自底向上的層次聚類演算法,基於n-gram模型和馬爾科夫鏈模型。布朗聚類是一種硬聚類,每一個詞都在且只在唯一的一個類中。

w是詞,c是詞所屬的類。

布朗聚類的輸入是一個語料庫,這個語料庫是一個詞序列,輸出是一個二叉樹,樹的葉子節點是一個個詞,樹的中間節點是我們想要的類(中間結點作為根節點的子樹上的所有葉子為類中的詞)。

初始的時候,將每一個詞獨立的分成一類,然後,將兩個類合併,使得合併之後評價函式最大,然後不斷重複上述過程,達到想要的類的數量的時候停止合併。

上面提到的評價函式,是對於n個連續的詞(w)序列能否組成一句話的概率的對數的歸一化結果。於是,得到評價函式:

n是文字長度,w是詞

上面的評價公式是PercyLiang的“Semi-supervised learning for natural languageprocessing”文章中關於布朗聚類的解釋,Browm原文中是基於class-based bigram language model建立的,於是得到下面公式:

T是文字長度,t是文字中的詞

上述公式是由於對於bigram,於是歸一化處理只需要對T-1個bigram。我覺得PercyLiang的公式容易理解評價函式的定義,但是,Brown的推導過程更加清晰簡明,所以,接下來的公式推導遵循Brown原文中的推導過程。

上面的推導式數學推導,接下來是一個重要的近似處理,

近似等於w2在訓練集中出現的頻率,也就是Pr(w2),於是公式變為:

H(w)是熵,只跟1-gram的分佈有關,也就是與類的分佈無關,而I(c1,c2)是相鄰類的平均互資訊。所以,I決定了L。所以,只有最大化I,L才能最大。

二、優化

Brown提出了一種估算方式進行優化。首先,將詞按照詞頻進行排序,將前C(詞的總聚類數目)個詞分到不同的C類中,然後,將接下來詞頻最高的詞,新增到一個新的類,將(C+1)類聚類成C類,即合併兩個類,使得平均互資訊損失最小。雖然,這種方式使得計算不是特別精確,類的加入順序,決定了合併的順序,會影響結果,但是極大的降低了計算複雜度。

顯然上面提及的演算法仍然是一種naive的演算法,演算法複雜度十分高。(上述結果包括下面的複雜度結果來自Percy Liang的論文)。對於這麼高的複雜度,對於成百上千詞的聚類將變得不現實,於是,優化演算法變得不可或缺。Percy Liang和Brown分別從兩個角度去優化。

Brown從代數的角度優化,通過一個表格記錄下每次合併的中間結果,然後,用來計算下一次結果。

Percy Liang從幾何的角度考慮優化,更加清晰直觀。但是,Percy Liang是從跟Brown的損失函式L相反的角度去考慮(即兩者正負號不同),但是,都是為了保留中間結果,減少計算量,個人覺得PercyLiang的演算法比較容易理解,而且,他少忽略了一些沒必要計算的中間結果,更加優化,後面介紹的程式碼,也是PercyLiang寫的,所以,將會重點介紹一下他的思考方式。

Percy Liang將聚類結果表示成一個無向圖,圖的節點有C個,代表C個類,同時,任何兩個節點都有一條邊,邊代表相鄰兩個節點之間(兩個類之間)的平均互資訊。邊的權重如下表達式:

而評價的總的平均互資訊I就是所有邊的權重之和。下面是實際程式碼中的計算損失評價的函式即合併後的I減去合併前的I的損失。


上述的(c並c')代表合併c和兩個節點後的一個節點,C是當前集合,而C'是合併後的集合:

三、程式碼實現

程式碼實現的主要過程概覽:

1、讀取文字並預處理

1) 將文字中的每個詞讀入並編碼(其中過濾一些頻次極其低的)

2)統計詞表大小、出現次數

3)將文字左右兩個方向的n-gram儲存

2、初始化布朗聚類(N log N)

1)將詞進行排序

2)將頻次最高的initC個詞分配到每個類

3)初始化p1(概率),q2(邊的權重)

3、進行布朗聚類

1)初始化L2(合併減少的互資訊)

2) 將當前未聚類的詞中,出現頻次最高的,作為一個類,新增進去,並同時,計算p1,q2,L2

3)找到最小的L2

4)合併,並更新q2,L2

程式碼還實現了計算KL散度比較相關性,此部分略去。

這裡p1如下

q2如下

四、重要程式碼段解析

初始化L2:

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//O(C^3)time.
  2. voidcompute_L2(){
  3. track("compute_L2()","",true);
  4. track_block("ComputingL2","",false)
  5. FOR_SLOT(s){
  6. track_block("L2","L2["<<Slot(s)<<",*]",false)
  7. FOR_SLOT(t){
  8. if(!ORDER_VALID(s,t))continue;
  9. doublel=L2[s][t]=compute_L2(s,t);
  10. logs("L2["<<Slot(s)<<","<<Slot(t)<<"]="<<l<<",resultingminfo="<<curr_minfo-l);
  11. }
  12. }
  13. }</span>

上面呼叫,單步計算L2:

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//O(C)time.
  2. doublecompute_L2(ints,intt){//computeL2[s,t]
  3. assert(ORDER_VALID(s,t));
  4. //stisthehypotheticalnewclusterthatcombinessandt
  5. //Loseoldassociationswithsandt
  6. doublel=0.0;
  7. for(intw=0;w<len(slot2cluster);w++){
  8. if(slot2cluster[w]==-1)continue;
  9. l+=q2[s][w]+q2[w][s];
  10. l+=q2[t][w]+q2[w][t];
  11. }
  12. l-=q2[s][s]+q2[t][t];
  13. l-=bi_q2(s,t);
  14. //Formnewassociationswithst
  15. FOR_SLOT(u){
  16. if(u==s||u==t)continue;
  17. l-=bi_hyp_q2(_(s,t),u);
  18. }
  19. l-=hyp_q2(_(s,t));//q2[st,st]
  20. returnl;
  21. }
  22. </span>

聚類過程中,更新p1,q2,L2,呼叫時(兩次):

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//Stage1:MaintaininitCclusters.ForeachofthephrasesinitC..N-1,make
  2. //itintoanewcluster.ThenmergetheoptimalpairamongtheinitC+1
  3. //clusters.
  4. //O(N*C^2)time.
  5. track_block("Stage1","",false){
  6. mem_tracker.report_mem_usage();
  7. for(inti=initC;i<len(freq_order_phrases);i++){//Mergephrasenew_a
  8. intnew_a=freq_order_phrases[i];
  9. track("Mergingphrase",i<<'/'<<N<<":"<<Cluster(new_a),true);
  10. logs("Mutualinfo:"<<curr_minfo);
  11. incorporate_new_phrase(new_a);//新增後,C->C+1
  12. repcheck();
  13. merge_clusters(find_opt_clusters_to_merge());//合併後,C+1->C
  14. repcheck();
  15. }
  16. }
  17. </span>

新增後,更新p1,q2,L2

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//Addnewphraseasacluster.
  2. //ComputeitsL2betweenaandallexistingclusters.
  3. //O(C^2)time,O(T)timeoverallcalls.
  4. voidincorporate_new_phrase(inta){
  5. track("incorporate_new_phrase()",Cluster(a),false);
  6. ints=put_cluster_in_free_slot(a);
  7. init_slot(s);
  8. cluster2rep[a]=a;
  9. rep2cluster[a]=a;
  10. //Computep1
  11. p1[s]=(double)phrase_freqs[a]/T;
  12. //Overallallcalls:O(T)
  13. //Computep2,q2betweenaandeverythinginclusters
  14. IntIntMapfreqs;
  15. freqs.clear();//rightbigrams
  16. forvec(_,int,b,right_phrases[a]){
  17. b=phrase2rep.GetRoot(b);
  18. if(!contains(rep2cluster,b))continue;
  19. b=rep2cluster[b];
  20. if(!contains(cluster2slot,b))continue;
  21. freqs[b]++;
  22. }
  23. forcmap(int,b,int,count,IntIntMap,freqs){
  24. curr_minfo+=set_p2_q2_from_count(cluster2slot[a],cluster2slot[b],count);
  25. logs(Cluster(a)<<''<<Cluster(b)<<''<<count<<''<<set_p2_q2_from_count(cluster2slot[a],cluster2slot[b],count));
  26. }
  27. freqs.clear();//leftbigrams
  28. forvec(_,int,b,left_phrases[a]){
  29. b=phrase2rep.GetRoot(b);
  30. if(!contains(rep2cluster,b))continue;
  31. b=rep2cluster[b];
  32. if(!contains(cluster2slot,b))continue;
  33. freqs[b]++;
  34. }
  35. forcmap(int,b,int,count,IntIntMap,freqs){
  36. curr_minfo+=set_p2_q2_from_count(cluster2slot[b],cluster2slot[a],count);
  37. logs(Cluster(b)<<''<<Cluster(a)<<''<<count<<''<<set_p2_q2_from_count(cluster2slot[b],cluster2slot[a],count));
  38. }
  39. curr_minfo-=q2[s][s];//q2[s,s]wasdouble-counted
  40. //UpdateL2:O(C^2)
  41. track_block("UpdateL2","",false){
  42. the_job.s=s;
  43. the_job.is_type_a=true;
  44. //startthejobs
  45. for(intii=0;ii<num_threads;ii++){
  46. thread_start[ii].unlock();//thethreadwaitsforthislocktobegin
  47. }
  48. //waitforthemtobedone
  49. for(intii=0;ii<num_threads;ii++){
  50. thread_idle[ii].lock();//thethreadreleasesthelocktofinish
  51. }
  52. }
  53. //dump();
  54. }
  55. </span>

合併後,更新

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//O(C^2)time.
  2. //Mergeclustersa(inslots)andb(inslott)intoc(inslotu).
  3. voidmerge_clusters(ints,intt){
  4. assert(ORDER_VALID(s,t));
  5. inta=slot2cluster[s];
  6. intb=slot2cluster[t];
  7. intc=curr_cluster_id++;
  8. intu=put_cluster_in_free_slot(c);
  9. free_up_slots(s,t);
  10. //Recordmergeintheclustertree
  11. cluster_tree[c]=_(a,b);
  12. curr_minfo-=L2[s][t];
  13. //Updaterelationshipbetweenclustersandrepphrases
  14. intA=cluster2rep[a];
  15. intB=cluster2rep[b];
  16. phrase2rep.Join(A,B);
  17. intC=phrase2rep.GetRoot(A);//Newrepphraseofclusterc(mergedaandb)
  18. track("Mergingclusters",Cluster(a)<<"and"<<Cluster(b)<<"into"<<c<<",lost"<<L2[s][t],false);
  19. cluster2rep.erase(a);
  20. cluster2rep.erase(b);
  21. rep2cluster.erase(A);
  22. rep2cluster.erase(B);
  23. cluster2rep[c]=C;
  24. rep2cluster[C]=c;
  25. //Computep1:O(1)
  26. p1[u]=p1[s]+p1[t];
  27. //Computep2:O(C)
  28. p2[u][u]=hyp_p2(_(s,t));
  29. FOR_SLOT(v){
  30. if(v==u)continue;
  31. p2[u][v]=hyp_p2(_(s,t),v);
  32. p2[v][u]=hyp_p2(v,_(s,t));
  33. }
  34. //Computeq2:O(C)
  35. q2[u][u]=hyp_q2(_(s,t));
  36. FOR_SLOT(v){
  37. if(v==u)continue;
  38. q2[u][v]=hyp_q2(_(s,t),v);
  39. q2[v][u]=hyp_q2(v,_(s,t));
  40. }
  41. //ComputeL2:O(C^2)
  42. track_block("ComputeL2","",false){
  43. the_job.s=s;
  44. the_job.t=t;
  45. the_job.u=u;
  46. the_job.is_type_a=false;
  47. //startthejobs
  48. for(intii=0;ii<num_threads;ii++){
  49. thread_start[ii].unlock();//thethreadwaitsforthislocktobegin
  50. }
  51. //waitforthemtobedone
  52. for(intii=0;ii<num_threads;ii++){
  53. thread_idle[ii].lock();//thethreadreleasesthelocktofinish
  54. }
  55. }
  56. }
  57. voidmerge_clusters(constIntPair&st){merge_clusters(st.first,st.second);}
  58. </span>

更新L2過程,其中使用了多執行緒:

使用資料結構

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//Variablesusedtocontrolthethreadpool
  2. mutex*thread_idle;
  3. mutex*thread_start;
  4. thread*threads;
  5. structCompute_L2_Job{
  6. ints;
  7. intt;
  8. intu;
  9. boolis_type_a;
  10. };
  11. Compute_L2_Jobthe_job;
  12. boolall_done=false;
  13. </span>

初始化,將所有執行緒鎖住:

[html]view plaincopy

  1. <spanstyle="font-size:18px;">//startthethreads
  2. thread_start=newmutex[num_threads];
  3. thread_idle=newmutex[num_threads];
  4. threads=newthread[num_threads];
  5. for(intii=0;ii<num_threads;ii++){
  6. thread_start[ii].lock();
  7. thread_idle[ii].lock();
  8. threads[ii]=thread(update_L2,ii);
  9. }
  10. </span>

呼叫執行緒,共計2處,第一處是在新增後:

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//UpdateL2:O(C^2)
  2. track_block("UpdateL2","",false){
  3. the_job.s=s;
  4. the_job.is_type_a=true;
  5. //startthejobs
  6. for(intii=0;ii<num_threads;ii++){
  7. thread_start[ii].unlock();//thethreadwaitsforthislocktobegin
  8. }
  9. //waitforthemtobedone
  10. for(intii=0;ii<num_threads;ii++){
  11. thread_idle[ii].lock();//thethreadreleasesthelocktofinish
  12. }
  13. }
  14. </span>

第二處是在合併後

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//ComputeL2:O(C^2)
  2. track_block("ComputeL2","",false){
  3. the_job.s=s;
  4. the_job.t=t;
  5. the_job.u=u;
  6. the_job.is_type_a=false;
  7. //startthejobs
  8. for(intii=0;ii<num_threads;ii++){
  9. thread_start[ii].unlock();//thethreadwaitsforthislocktobegin
  10. }
  11. //waitforthemtobedone
  12. for(intii=0;ii<num_threads;ii++){
  13. thread_idle[ii].lock();//thethreadreleasesthelocktofinish
  14. }
  15. }
  16. </span>

結束呼叫:

[cpp]view plaincopy

  1. <spanstyle="font-size:18px;">//finishthethreads
  2. all_done=true;
  3. for(intii=0;ii<num_threads;ii++){
  4. thread_start[ii].unlock();//threadwillgrabthistostart
  5. threads[ii].join();
  6. }
  7. delete[]thread_start;
  8. delete[]thread_idle;
  9. delete[]threads;
  10. </span>

通過兩個鎖實現呼叫,每次呼叫時通過更新the_job來改變計算引數,呼叫時開啟thread_start鎖,結束後,關閉thread_idle鎖。
參考文獻:

Liang: Semi-supervised learning for natural language processing

Brown, et al.: Class-Based n-gram Models of Natural Language

程式碼來源:

https://github.com/percyliang/brown-cluster

轉載於:https://my.oschina.net/airship/blog/895472