天天看點

【白話機器學習】算法理論+實戰之PageRank算法

1. 寫在前面

如果想從事資料挖掘或者機器學習的工作,掌握常用的機器學習算法是非常有必要的,常見的機器學習算法:

  • 監督學習算法:邏輯回歸,線性回歸,決策樹,樸素貝葉斯,K近鄰,支援向量機,內建算法Adaboost等
  • 無監督算法:聚類,降維,關聯規則,PageRank等

為了詳細的了解這些原理,曾經看過西瓜書,統計學習方法,機器學習實戰等書,也聽過一些機器學習的課程,但總感覺話語裡比較深奧,讀起來沒有耐心,并且理論到處有,而實戰最重要, 是以在這裡想用最淺顯易懂的語言寫一個白話機器學習算法理論+實戰系列。

個人認為,了解算法背後的idea和使用,要比看懂它的數學推導更加重要。idea會讓你有一個直覺的感受,進而明白算法的合理性,數學推導隻是将這種合理性用更加嚴謹的語言表達出來而已,打個比方,一個梨很甜,用數學的語言可以表述為糖分含量90%,但隻有親自咬一口,你才能真正感覺到這個梨有多甜,也才能真正了解數學上的90%的糖分究竟是怎麼樣的。如果算法是個梨,本文的首要目的就是先帶領大家咬一口。另外還有下面幾個目的:

  • 檢驗自己對算法的了解程度,對算法理論做一個小總結
  • 能開心的學習這些算法的核心思想, 找到學習這些算法的興趣,為深入的學習這些算法打一個基礎。
  • 每一節課的理論都會放一個實戰案例,能夠真正的做到學以緻用,既可以鍛煉程式設計能力,又可以加深算法理論的把握程度。
  • 也想把之前所有的筆記和參考放在一塊,友善以後檢視時的友善。

學習算法的過程,獲得的不應該隻有算法理論,還應該有樂趣和解決實際問題的能力!

今天是白話機器學習算法理論+實戰的第三篇,PageRank算法,通過今天的學習,快速Get到PageRank的原理,并最後運用PageRank算法實作一個項目:分析希拉裡郵件裡面的人物關系,在這能看到誰和希拉裡交往最為活躍。

大綱如下:

  • PageRank的簡化模型和計算原理
  • PageRank的随機浏覽模型(解決等級沉沒和等級洩露)
  • PageRank給我們帶來的啟發很重要 (人脈杠杆)
  • PageRank實戰 (分析希拉裡郵件裡面的人物關系,看看誰和希拉裡交往最活躍)

Ok, let's go !

2. PageRank?還是從日常生活談起吧

提到PageRank,好多人可能一上來一臉懵逼,不知道這是個什麼東西,但是提到一個詞?想必大家一定不陌生,那就是從衆心理。大家一般都有從衆心理,不信的話可以看看下面的場景:

比如,當你某一家飯店,或者理發店,或者小吃店裡面的顧客非常多,每天爆滿的時候,心理一定會想,這家店想必不錯,要不然不可能這麼多人,我也進去試試。 或者,在淘寶上買某個商品的時候,肯定是喜歡挑人多的店鋪,好評量高的店鋪買的放心等等吧。 是以當我們在生活中遇到艱難選擇的時候,往往喜歡看看别人是怎麼做的,一般都會選大部分人的選擇。這其實就是一種從衆。

這些店鋪也好,選擇也罷,其實都是通過很多人的投票進而提高了自己的影響力,再比如說,微網誌上如何去衡量一個人的影響力呢? 我們習慣看他的粉絲,如果他的粉絲多,并且裡面都是一些大V,明星的話,很可能這個人的影響力會比較強。 再比如說,職場上如何衡量一個人的影響力呢?我們習慣看與他交往的人物, 如果和他交往的都是像馬雲,王健林,馬化騰這樣的人物,那麼這個人的影響力估計也小不了。再比如說,如何判斷一篇論文好?我們習慣看他的引用次數,或者影響因子,高的論文就比較好等等。

但是你隻知道嗎?其實我們的這種方式就在用PageRank算法的思想了,隻不過我們沒有發覺罷了,所謂的算法來源于生活,并服務于生活就是這個道理。

好了,通過上面的案例,相信已經對PageRank這個詞有點感覺了吧。那麼趁熱打鐵,說說它的來源吧。

3. PageRank是怎麼來的?

想必大家上網的時候,都用過搜尋引擎,現在已經非常好用了,基本上輸入關鍵詞,都能找到比對的内容,品質還不錯。但在 1998 年之前,搜尋引擎的體驗并不好。早期的搜尋引擎,會遇到下面的兩類問題:

  1. 傳回結果品質不高:搜尋結果不考慮網頁的品質,而是通過時間順序進行檢索;
  2. 容易被人鑽空子:搜尋引擎是基于檢索詞進行檢索的,頁面中檢索詞出現的頻次越高,比對度越高,這樣就會出現網頁作弊的情況。有些網頁為了增加搜尋引擎的排名,故意增加某個檢索詞的頻率。

基于這些缺陷,當時 Google 的創始人拉裡·佩奇提出了 PageRank 算法,目的就是要找到優質的網頁,這樣 Google 的排序結果不僅能找到使用者想要的内容,而且還會從衆多網頁中篩選出權重高的呈現給使用者。其靈感就是論文影響力因子的啟發。

4. PageRank的簡化模型

假設一共有 4 個網頁 A、B、C、D。它們之間的連結資訊如圖所示:

【白話機器學習】算法理論+實戰之PageRank算法

首先先知道兩個概念:

對外連結指的是連結出去的連結。傳入連結指的是連結進來的連結。比如圖中 A 有 2 個傳入連結,3 個對外連結。

那麼我們如何計算一個網頁的影響力或者重要程度呢?

簡單來說,一個網頁的影響力 = 所有傳入連結集合的頁面的權重影響力之和,用公式表示為:
【白話機器學習】算法理論+實戰之PageRank算法
u 為待評估的頁面,Bu 為頁面 u 的傳入連結集合。針對傳入連結集合中的任意頁面 v,它能給 u 帶來的影響力是其自身的影響力 PR(v) 除以 v 頁面的對外連結數量,即頁面 v 把影響力 PR(v) 平均配置設定給了它的對外連結,這樣統計所有能給 u 帶來連結的頁面 v,得到的總和就是網頁 u 的影響力,即為 PR(u)。

What? 這是在說什麼? 隻需要先明白兩點:

  • 如果一個頁面,傳入連結比較多,也就是跳轉過去的機率比較大的時候,這個頁面往往就比較重要,影響力高一些(這就類似于購買商品的人越多,這個商品就貌似越好一樣)
  • 如果某個頁面本身的影響力很大,那麼也會相應的增加它對外連結指向的頁面影響力。(這個就類似于你的朋友都是大牛的話,你在别人心目中的印象也會提升)

是以你能看到,對外連結會給被連結的頁面賦予影響力,當我們統計了一個網頁鍊出去的數量,也就是統計了這個網頁的跳轉機率。

在這個例子中,你能看到 A 有三個對外連結分别連結到了 B、C、D 上。那麼當使用者通路 A 的時候,就有跳轉到 B、C 或者 D 的可能性,跳轉機率均為 1/3。

B 有兩個對外連結,連結到了 A 和 D 上,跳轉機率為 1/2。

這樣,我們可以得到 A、B、C、D 這四個網頁的轉移矩陣 M:

【白話機器學習】算法理論+實戰之PageRank算法
這個轉移矩陣,每一行代表了每個節點傳入連結上的權重(大家浏覽别的頁面的時候,有多大的機率能跳到我這來)。每一列代表了每個節點對其他頁面的影響力的賦予程度(也就是大家浏覽我這,有多大的機率跳到别的頁面上去)。

有了這個轉移矩陣,我們就可以計算每個頁面的影響力是多少了。

比如上圖A頁面的影響力怎麼計算呢?其實我們是通過他的傳入連結點B、C的影響力去計算的,也就是我們的那個公式。開始我們假設四個頁面的影響力相同,都是1/4。則A第一次的影響力這麼想,B有兩條對外連結,那麼會給我傳過它影響力的1/2。C有一條對外連結,那麼會把它的影響力全傳給A。

故:PR(A) = 1/4 * 1/2 + 1/4

這是一次疊代。你發現了嗎?這其實就是轉移矩陣M的第一行 * 一個全為1/4的列向量得到的(向量乘法)

是以如果我們利用向量的乘法原理的話,隻需要一個向量乘法就可以計算出每個頁面的影響力了。

我們假設 A、B、C、D 四個頁面的初始影響力都是相同的,即:

【白話機器學習】算法理論+實戰之PageRank算法

當進行第一次轉移之後,各頁面的影響力 w1 變為:

【白話機器學習】算法理論+實戰之PageRank算法

然後我們再用轉移矩陣乘以 w1 得到 w2 結果,直到第 n 次疊代後 wn 影響力不再發生變化,可以收斂到 (0.3333,0.2222,0.2222,0.2222),也就是對應着 A、B、C、D 四個頁面最終平衡狀态下的影響力。

你能看出 A 頁面相比于其他頁面來說權重更大,也就是 PR 值更高。而 B、C、D 頁面的 PR 值相等。

至此,我們模拟了一個簡化的 PageRank 的計算過程,也就是PageRank簡化模型的原理了。

是不是計算起來很簡單啊?那就再回去看看那個公式了解了沒?

但是你發現了嗎?雖然這個模型簡單,但是有問題的,比如萬一我有一個網頁隻有傳入連結沒有對外連結怎麼辦? 萬一我有一個網頁隻有對外連結,沒有傳入連結怎麼辦?

上面的兩個問題分别對應着等級洩露和等級沉沒。

  1. 等級洩露(Rank Leak):如果一個網頁沒有對外連結,就像是一個黑洞一樣,吸收了其他網頁的影響力而不釋放,最終會導緻其他網頁的 PR 值為 0。
  2. 【白話機器學習】算法理論+實戰之PageRank算法
  3. 等級沉沒(Rank Sink):如果一個網頁隻有對外連結,沒有傳入連結(如下圖所示),計算的過程疊代下來,會導緻這個網頁的 PR 值為 0(也就是不存在公式中的 V)。
  4. 【白話機器學習】算法理論+實戰之PageRank算法

這其實就和練功一樣,如果其他所有人把功力傳給一個人,而這個人隻接收别人的功力并不傳給其他人,那麼這個人必定非常強大,其他人慢慢的就會功力喪失, 武俠小說裡面的高手很多是這麼煉成的吧(吸星大法這樣的)。

如果一個人隻往外傳功力,不接收外來人的功力,比如給人療傷的時候,那麼這個人很快也就會功力喪失了。(武俠小說中療傷的時候,某個大俠給人療着療着自己就昏過去了)

上面的兩個例子可以很好的了解這兩個問題。

那麼如何有效解決呢?

5. PageRank 的随機浏覽模型

為了解決簡化模型中存在的等級洩露和等級沉沒的問題,拉裡·佩奇提出了 PageRank 的随機浏覽模型。他假設了這樣一個場景:使用者并不都是按照跳轉連結的方式來上網,還有一種可能是不論目前處于哪個頁面,都有機率通路到其他任意的頁面,比如說使用者就是要直接輸入網址通路其他頁面,雖然這個機率比較小。

是以他定義了阻尼因子 d,這個因子代表了使用者按照跳轉連結來上網的機率,通常可以取一個固定值 0.85,而 1-d=0.15 則代表了使用者不是通過跳轉連結的方式來通路網頁的,比如直接輸入網址。

【白話機器學習】算法理論+實戰之PageRank算法

其中 N 為網頁總數,這樣我們又可以重新疊代網頁的權重計算了,因為加入了阻尼因子 d,一定程度上解決了等級洩露和等級沉沒的問題。

通過數學定理(這裡不進行講解)也可以證明,最終 PageRank 随機浏覽模型是可以收斂的,也就是可以得到一個穩定正常的 PR 值。

6. PageRank給我們帶來的啟發

這個算法有個好處,就是可以通過這個算法,給我們帶來一點生活中的啟示,會是什麼啟示呢? 啥?不要盲目從衆? 這個就有點low了。

PageRank 可以說是 Google 搜尋引擎重要的技術之一,在 1998 年幫助 Google 獲得了搜尋引擎的領先優勢,現在 PageRank 已經比原來複雜很多,但它的思想依然能帶給我們很多啟發。

比如,如果你想要自己的媒體影響力有所提高,就盡量要混在大 V 圈中;如果想找到高職位的工作,就盡量結識公司高層,或者認識更多的獵頭,因為獵頭和很多高職位的人員都有連結關系。 這也就是很重要的理論:加人脈杠杆,搞定一人,撬動千人。(好吧,這個隻是逆襲學習理論中的一小小部分,後面打算也會把逆襲學習理論整理出來,大家共勉。)

說了這麼多,也知道了PageRank的原理了,那就讓我們實戰吧!

7. PageRank實戰

7.1 如何使用工具實作PageRank算法

PageRank 算法工具在 sklearn 中并不存在,我們需要找到新的工具包。實際上有一個關于圖論和網絡模組化的工具叫 NetworkX,它是用 Python 語言開發的工具,内置了常用的圖與網絡分析算法,可以友善我們進行網絡資料分析。

上面舉了一個網頁權重的例子,假設一共有 4 個網頁 A、B、C、D,它們之間的連結資訊如圖所示:

【白話機器學習】算法理論+實戰之PageRank算法

針對這個例子,我們看下用 NetworkX 如何計算 A、B、C、D 四個網頁的 PR 值,具體代碼如下:

import networkx as nx
# 建立有向圖
G = nx.DiGraph()
# 有向圖之間邊的關系
edges = [("A", "B"), ("A", "C"), ("A", "D"), ("B", "A"), ("B", "D"), ("C", "A"), ("D", "B"), ("D", "C")]
for edge in edges:
    G.add_edge(edge[0], edge[1])
pagerank_list = nx.pagerank(G, alpha=1)
print("pagerank值是:\n", pagerank_list)      

結果如下:

pagerank值是:
{'A': 0.33333396911621094, 'B': 0.22222201029459634, 'C': 0.22222201029459634, 'D': 0.22222201029459634}      

關鍵代碼就nx.pagerank(G, alpha=1) 這一句話, 這裡的alpha就是我們上面說的阻尼因子,代表使用者按照跳轉連結的機率。預設是0.85。這裡是1,表示我們都是用跳轉連結,不直接輸入網址的那種。

好了,運作完這個例子之後,來看下 NetworkX 工具都有哪些常用的操作。

  • 關于圖的建立圖可以分為無向圖和有向圖,在 NetworkX 中分别采用不同的函數進行建立。無向圖指的是不用節點之間的邊的方向,使用 nx.Graph() 進行建立;有向圖指的是節點之間的邊是有方向的,使用 nx.DiGraph() 來建立。在上面這個例子中,存在 A→D 的邊,但不存在 D→A 的邊。
  • 關于節點的增加、删除和查詢如果想在網絡中增加節點,可以使用 G.add_node(‘A’) 添加一個節點,也可以使用 G.add_nodes_from([‘B’,‘C’,‘D’,‘E’]) 添加節點集合。如果想要删除節點,可以使用 G.remove_node(node) 删除一個指定的節點,也可以使用 G.remove_nodes_from([‘B’,‘C’,‘D’,‘E’]) 删除集合中的節點。

    那麼該如何查詢節點呢?如果你想要得到圖中所有的節點,就可以使用 G.nodes(),也可以用 G.number_of_nodes() 得到圖中節點的個數。

  • 關于邊的增加、删除、查詢增加邊與添加節點的方式相同,使用 G.add_edge(“A”, “B”) 添加指定的“從 A 到 B”的邊,也可以使用 add_edges_from 函數從邊集合中添加。我們也可以做一個權重圖,也就是說邊是帶有權重的,使用add_weighted_edges_from 函數從帶有權重的邊的集合中添加。在這個函數的參數中接收的是 1 個或多個三元組[u,v,w]作為參數,u、v、w 分别代表起點、終點和權重。

    另外,我們可以使用 remove_edge 函數和 remove_edges_from 函數删除指定邊和從邊集合中删除。另外可以使用 edges() 函數通路圖中所有的邊,使用 number_of_edges() 函數得到圖中邊的個數。

以上是關于圖的基本操作,如果我們建立了一個圖,并且對節點和邊進行了設定,就可以找到其中有影響力的節點,原理就是通過 PageRank 算法,使用 nx.pagerank(G) 這個函數,函數中的參數 G 代表建立好的圖。

7.2 如何用 PageRank 揭秘希拉裡郵件中的人物關系

資料集可以再這裡下載下傳:https://github.com/cystanford/PageRank先了解下資料集:

整個資料集由三個檔案組成:Aliases.csv,Emails.csv 和 Persons.csv,其中 Emails 檔案記錄了所有公開郵件的内容,發送者和接收者的資訊。Persons 這個檔案統計了郵件中所有人物的姓名及對應的 ID。因為姓名存在别名的情況,為了将郵件中的人物進行統一,我們還需要用 Aliases 檔案來查詢别名和人物的對應關系。

整個資料集包括了 9306 封郵件和 513 個人名,資料集還是比較大的。不過這一次我們不需要對郵件的内容進行分析,隻需要通過郵件中的發送者和接收者(對應 Emails.csv 檔案中的 MetadataFrom 和 MetadataTo 字段)來繪制整個關系網絡。因為涉及到的人物很多,是以我們需要通過 PageRank 算法計算每個人物在郵件關系網絡中的權重,最後篩選出來最有價值的人物來進行關系網絡圖的繪制。

了解了資料集和項目背景之後,我們來設計到執行的流程步驟:

【白話機器學習】算法理論+實戰之PageRank算法
  1. 首先我們需要加載資料源;
  2. 在準備階段:我們需要對資料進行探索,在資料清洗過程中,因為郵件中存在别名的情況,是以我們需要統一人物名稱。另外郵件的正文并不在我們考慮的範圍内,隻統計郵件中的發送者和接收者,是以我們篩選 MetadataFrom 和 MetadataTo 這兩個字段作為特征。同時,發送者和接收者可能存在多次郵件往來,需要設定權重來統計兩人郵件往來的次數。次數越多代表這個邊(從發送者到接收者的邊)的權重越高;
  3. 在挖掘階段:我們主要是對已經設定好的網絡圖進行 PR 值的計算,但郵件中的人物有 500 多人,有些人的權重可能不高,我們需要篩選 PR 值高的人物,繪制出他們之間的往來關系。在可視化的過程中,我們可以通過節點的 PR 值來繪制節點的大小,PR 值越大,節點的繪制尺寸越大。

最終代碼如下:

# -*- coding: utf-8 -*-
# 用 PageRank 挖掘希拉裡郵件中的重要任務關系
import pandas as pd
import networkx as nx
import numpy as np
from collections import defaultdict
import matplotlib.pyplot as plt
# 資料加載
emails = pd.read_csv("./input/Emails.csv")
# 讀取别名檔案
file = pd.read_csv("./input/Aliases.csv")
aliases = {}
for index, row in file.iterrows():
    aliases[row['Alias']] = row['PersonId']
# 讀取人名檔案
file = pd.read_csv("./input/Persons.csv")
persons = {}
for index, row in file.iterrows():
    persons[row['Id']] = row['Name']
# 針對别名進行轉換
def unify_name(name):
    # 姓名統一小寫
    name = str(name).lower()
    # 去掉, 和 @後面的内容
    name = name.replace(",","").split("@")[0]
    # 别名轉換
    if name in aliases.keys():
        return persons[aliases[name]]
    return name
# 畫網絡圖
def show_graph(graph, layout='spring_layout'):
    # 使用 Spring Layout 布局,類似中心放射狀
    if layout == 'circular_layout':
        positions=nx.circular_layout(graph)
    else:
        positions=nx.spring_layout(graph)
    # 設定網絡圖中的節點大小,大小與 pagerank 值相關,因為 pagerank 值很小是以需要 *20000
    nodesize = [x['pagerank']*20000 for v,x in graph.nodes(data=True)]
    # 設定網絡圖中的邊長度
    edgesize = [np.sqrt(e[2]['weight']) for e in graph.edges(data=True)]
    # 繪制節點
    nx.draw_networkx_nodes(graph, positions, node_size=nodesize, alpha=0.4)
    # 繪制邊
    nx.draw_networkx_edges(graph, positions, edge_size=edgesize, alpha=0.2)
    # 繪制節點的 label
    nx.draw_networkx_labels(graph, positions, font_size=10)
    # 輸出希拉裡郵件中的所有人物關系圖
    plt.show()
# 将寄件人和收件人的姓名進行規範化
emails.MetadataFrom = emails.MetadataFrom.apply(unify_name)
emails.MetadataTo = emails.MetadataTo.apply(unify_name)
# 設定遍的權重等于發郵件的次數
edges_weights_temp = defaultdict(list)
for row in zip(emails.MetadataFrom, emails.MetadataTo, emails.RawText):
    temp = (row[0], row[1])
    if temp not in edges_weights_temp:
        edges_weights_temp[temp] = 1
    else:
        edges_weights_temp[temp] = edges_weights_temp[temp] + 1
# 轉化格式 (from, to), weight => from, to, weight
edges_weights = [(key[0], key[1], val) for key, val in edges_weights_temp.items()]
# 建立一個有向圖
graph = nx.DiGraph()
# 設定有向圖中的路徑及權重 (from, to, weight)
graph.add_weighted_edges_from(edges_weights)
# 計算每個節點(人)的 PR 值,并作為節點的 pagerank 屬性
pagerank = nx.pagerank(graph)
# 将 pagerank 數值作為節點的屬性
nx.set_node_attributes(graph, name = 'pagerank', values=pagerank)
# 畫網絡圖
show_graph(graph)

# 将完整的圖譜進行精簡
# 設定 PR 值的門檻值,篩選大于門檻值的重要核心節點
pagerank_threshold = 0.005
# 複制一份計算好的網絡圖
small_graph = graph.copy()
# 剪掉 PR 值小于 pagerank_threshold 的節點
for n, p_rank in graph.nodes(data=True):
    if p_rank['pagerank'] < pagerank_threshold:
        small_graph.remove_node(n)
# 畫網絡圖,采用circular_layout布局讓篩選出來的點組成一個圓
show_graph(small_graph, 'circular_layout')      

運作結果:

【白話機器學習】算法理論+實戰之PageRank算法
【白話機器學習】算法理論+實戰之PageRank算法

針對代碼中的幾個子產品個簡單的說明:

  1. 函數定義人物的名稱需要統一,是以設定了 unify_name 函數,同時設定了 show_graph 函數将網絡圖可視化。NetworkX 提供了多種可視化布局,這裡使用 spring_layout 布局,也就是呈中心放射狀。

    除了 spring_layout 外,NetworkX 還有另外三種可視化布局,circular_layout(在一個圓環上均勻分布節點),random_layout(随機分布節點 ),shell_layout(節點都在同心圓上)。

  2. 計算邊權重郵件的發送者和接收者的郵件往來可能不止一次,我們需要用兩者之間郵件往來的次數計算這兩者之間邊的權重,是以用 edges_weights_temp 數組存儲權重。而上面介紹過在 NetworkX 中添權重重邊(即使用 add_weighted_edges_from 函數)的時候,接受的是 u、v、w 的三元數組,是以我們還需要對格式進行轉換,具體轉換方式見代碼。3. PR 值計算及篩選使用 nx.pagerank(graph) 計算了節點的 PR 值。由于節點數量很多,我們設定了 PR 值門檻值,即 pagerank_threshold=0.005,然後周遊節點,删除小于 PR 值門檻值的節點,形成新的圖 small_graph,最後對 small_graph 進行可視化(對應運作結果的第二張圖)。

8. 總結

今天用了一天的時間,學習PageRank算法的理論,并且通過理論做了一個實戰,運用了一下,又抓緊記錄,完成了一篇部落格,心理還是很高興的。學習知識的過程就是這樣,如果隻是單純的輸入,沒有一點輸出的話,那麼很快就會忘記,輸出一遍,至少在大腦裡面停留了片刻,這裡也留下了自己的蹤迹,就像那就話說的:天空中沒有鳥的痕迹,但是我已經飛過。

努力學習,快速感悟,敢于嘗試,這樣我們才能做到永遠年輕,永遠熱淚盈眶。加油!

參考:

  • ​​http://note.youdao.com/noteshare?id=3568b0c7f636cb21f260cd9f83ea4817&sub=B90987F55A05445B8E07476DDFCB6D62​​

繼續閱讀