在做博弈模型評估的時候,遇到一個問題是如何評價多個模型的優劣。例如我有訓練好的三個圍棋模型A,B,C,兩兩之間對打之後有一個勝負關係,如何對這三個模型進行排序呢?通常對於人類選手這種水平有波動的情形,棋類比賽通常計算選手Elo得分按分值排序,足球籃球等通過聯賽積分或勝場進行排序,但對於固定不變的AI模型,我認為用類似PageRank的方式計算更方便也更加準確。
這篇文章先從問題來源講起,再講解PageRank演算法的思想,最後程式設計實現排序方法並指出一些需要注意的地方。
目錄
一、問題來源
現在,深度強化學習更多的用在博弈模型的訓練當中,比如圍棋的AlphaZero,星際爭霸的AlphaStar,DO他的OpenAI FIVE。比如我們已經訓練好了三個模型A,B,C,並且可以相互對打很多局,我們需要一個方法排出誰第一,誰第二。之前NeurIPS2019多智慧體競賽設計的排序方法就存在明顯的bug,出現了A能勝過B,且A對C的勝率高於B對C的勝率,最後算出的排名卻是B更靠前。主辦方也承認了計算方式有缺陷並表示會在之後的比賽中修正,但是當前排名維持不變。
那為什麼成熟的Elo值計算方式沒有用在這類模型評估上面呢?Elo值通常用在圍棋、象棋等棋類排名上,電子競技例如英雄聯盟等也可以認為是類似Elo的積分方式。這類問題的特點是
- 可通過一對一比賽得到一局的勝負關係,但和相同對手的對局次數有限,很難得到穩定的勝率關係。
- 玩家水平並非固定不變,可隨環境、狀態等因素波動(臨場發揮),也可因長期訓練/荒廢而提升/下降(絕對實力)。
我們需要根據這種1v1(or 5v5)的每一局的勝負關係,給出所有玩家的即時能力大小排序。由於每個人的水平都會因為身體因素、年齡因素等產生波動,這和一個固定的模型是不一樣的。而Elo可以根據每一局的實時對局結果立即更新當前排名,對棋類、競技體育等的時效性需求非常適合,也可以較為準確的反應玩家的當前水平排名。雖然它也不是絕對的準確,不過已經是針對這類需求很好的排序方法了。
回過頭來,對於已經訓練好的AI模型,它的能力不會發生變化,並且我們可以通過足夠多的測試得到兩兩之間的準確勝率關係,這種情況下我們如果強行套Elo的演算法一局一局挑選對手對打,更新Elo值,再挑對手對打,再更新Elo值,就會顯得沒有必要(因為我們並不關心每一局後的實時排名)而且很麻煩,再者如果中途有一個新加入的模型需要從0開始評估,要想得到較為穩定的排名關係就會顯得更加麻煩。
而PageRank的方法可以充分利用模型之間容易得到的穩定勝負關係,用矩陣迭代的方式計算出最終排名,簡單且準確。
二、PageRank演算法
演算法思想
PageRank演算法是Google發明用來做網頁排序的,依據網頁之間的連結關係對網頁重要度進行排序。其主要設計思想如下
- (1) 每個網頁的初始重要程度相同,比如\(a=1,b=1,c=1,...\)
- (2) 如果許多網頁\(b,c,d...\)指向某個網頁\(a\),則網頁\(a\)很重要
- (3) 如果某個重要的網頁\(a\)指向某個網頁\(b\),則網頁\(b\)因為\(a\)很重要也會獲得更高的重要度。
這個想法其實和paper的引用有相似之處,每一篇新paper剛發表,很難評價其質量,可以粗略認為paper質量都一樣;如果有一篇paper被引用很多,那麼這篇paper肯定質量比較好;如果某偏很好的paper引用了另一篇paper,那這篇被引用的paper也理應質量不錯。
基於這三點主要思想,我們假定有a,b,c,d四個網址,其連結關係如圖所示
首先根據思想(1),假定每個網頁的初始重要度相同,比如都是1,則有重要度向量\(x=(1,1,1,1)\)。
接下來我們根據思想(2)、(3)計算每個網頁被指向後的重要度變化。令\(T_{i,j}\)表示網頁\(j\)是否指向網頁\(i\),則有
其中\(i,j \in \{a,b,c,d\}\),且自己指向自己記為0,即\(T(i,i)=0\)。重要度的變化如下計算\(x_i=\sum x_j \times T_{i,j}\),這種方式也很直覺,就是把所有指向\(i\)的網頁的當前重要度加起來,就是網頁\(i\)的重要度。由於剛開始大家的初始重要度都是1,則從圖中的指向關係可以算出
同理有\(x^\prime(b)=1;x^\prime(c)=3;x^\prime(d)=2\)。這裡有一個問題需要注意,大家的初始權值都為1,但是發出去的權重卻大於1,例如網頁\(a\)指向了\(b,c,d\)三個網頁,它發出去的權值為3,這是不太合理的。一個簡單的修正方式是,令\(T_{i,j}\)中同一個網頁發出去的連結的和為1,從而每個\(T_{i,j}\)還表示發出去的權值,而不僅僅表示有無。即有
此時,我們有\(T_{i,a}=\frac{1}{3};T_{i,b}=\frac{1}{2};T_{i,c}=1;T_{i,d}=\frac{1}{2}\)。重新計算每個網頁的重要度有
同理有\(x^\prime(b)=\frac{1}{3};x^\prime(c)=\frac{4}{3};x^\prime(d)=\frac{5}{6}\)。我們繼續將重要度向量\(x\)進行第二次迭代計算,有
同理有\(x^{\prime\prime}(b)=\frac{1}{2};x^{\prime\prime}(c)=\frac{13}{12};x^{\prime\prime}(d)=\frac{2}{3}\)。將計算表示為矩陣形式,我們有
那麼前兩次迭代可以表示為
經過無窮次迭代\(x^\infty=T^\infty x\)收斂,\(x^\infty\)每個分量的大小即為對應網頁的重要度大小。實際情況中,不必作無限次運算即可收斂。
接下來的問題是:對於任意這樣的矩陣,是否都會收斂呢?如何判斷當前矩陣是否具有這種收斂性?下一步給出比較直觀的理解和判斷方法,忽略證明過程。
數學原理
如果我們把這個問題看作一個馬氏(隨機)過程,那麼四個網頁組成的向量\(x\)其實就是四個狀態。我們不取權值1,而是歸一化為\(x=(\frac{1}{4},\frac{1}{4},\frac{1}{4},\frac{1}{4})\),那麼\(x\)可以看做是該馬氏過程的初始狀態概率分佈。矩陣\(T\)就是一步的狀態轉移概率矩陣。我們的目標則是求該轉移矩陣的平穩分佈,這個平穩分佈是與初始狀態分佈\(x\)無關的,也就是說無論\(x\)的取值是多少,最後算出來的\(x^\infty\)都一樣。那麼現在的問題是什麼樣的\(T\)可以保證平穩分佈存在且唯一。這裡我們給出結論並簡單解釋,不作數學證明,可參考馬氏鏈平穩分佈存在與唯一性的簡潔證明與計算。
定理: 若馬氏鏈不可約且正常返,則平穩分佈存在且唯一。
- 不可約:通俗來說,就是每個狀態都可以通過一步或者多步轉移到達任意另一個狀態。
- 正常返:可以理解為每個狀態在有限步轉移後再回到自己的概率為1。
如下圖所示例子
從圖中可以看出,\(a\)可以通過\(a \rightarrow b \rightarrow c \rightarrow d\)到達\(d\),而\(d\)無法轉移到\(a\)等狀態,所以這個轉移矩陣不可約。同理,當\(a\)轉移到\(d\)等狀態時,就再也無法回到\(a\),則該轉移矩陣非正常返。這種情況下我們無法得到唯一的分佈。
例如我們分別取初始分佈為
狀態轉移矩陣為
則有
顯然\(x^\infty_1 \not = x^\infty_2\)。
到這裡我們說明了收斂性的問題,但其實真正在運用的時候還會遇到一些實際問題。下面,我們回到需要真正解決的AI模型排序的問題,用程式碼實現演算法,並解決一些運用中遇到的實際問題。
三、例項分析
通過前述方式構建勝率矩陣,我們可以算得平穩分佈,但還有一些實際問題需要微調演算法。
對角線取值
在之前的網頁排序裡,對角線的元素被取為0,如果在勝率矩陣中也取為0,會出現錯誤的排序。假如勝率矩陣為
a b c
a 0 0.2 0.9
b 0.8 0 1
c 0.1 0 0
其中\(a\)對\(b\)的勝率為0.2,\(a\)對\(c\)的勝率為0.9;而\(b\)對\(a\)的勝率為0.8,\(b\)對\(c\)的勝率為1。可以很容易的看出\(b\)是最厲害的,\(a\)次之,\(c\)最弱。但如果我們直接套前面的計算方式,有
import numpy as np
T = np.matrix([[0 ,0.2,0.9],
[0.8, 0 , 1 ],
[0.1, 0 , 0 ]])
for i in range(T.shape[0]): # 歸一化為狀態轉移概率矩陣
T[:,i] = T[:,i]/np.sum(T[:,i])
X = np.matrix([1/3,1/3,1/3]) # 初始分佈
X = X.T
print(T)
print(T**2000*X)
得到
T:
[[0. 1. 0.47368421]
[0.88888889 0. 0.52631579]
[0.11111111 0. 0. ]]
X:
[[0.48579545]
[0.46022727]
[0.05397727]]
可以發現\(a\)居然比\(b\)的分值高,這顯然是不合理的。出現這個問題的原因在於,在將勝率矩陣轉化為概率矩陣時,歸一化的操作改變了\(b\)指向\(a\)的權值,直接從0.2拉到了1,使得\(b\)把所有自身的重要度都貢獻給了\(a\)。一個合理的解決辦法是將對角線取為0.5,表示自己對自己的勝率是五五開。這種方式可以防止某個概率在歸一化的過程中被不合理的放縮。此時勝率矩陣為:
a b c
a 0.5 0.2 0.9
b 0.8 0.5 1
c 0.1 0 0.5
計算得到
T:
[[0.35714286 0.28571429 0.375 ]
[0.57142857 0.71428571 0.41666667]
[0.07142857 0. 0.20833333]]
X:
[[0.31038506]
[0.66161027]
[0.02800467]]
可以看到,這個結果是合理的。同時這種方式還可以防止某一列出現全為0的情形。
構造不可約且正常返
通常我們需要考慮到各種勝負關係的情況,來保證平穩分佈存在且唯一。假如勝率矩陣為
a b c
a 0.5 1 1
b 0 0.5 0.3
c 0 0.7 0.5
可以看出\(a\)對\(b\)和\(c\)的勝率都為1;而\(c\)對\(b\)的勝率為0.7。可以很容易的看出排序應該為\(a,c,b\)。但計算得到的結果為:
T:
[[1. 0.45454545 0.55555556]
[0. 0.22727273 0.16666667]
[0. 0.31818182 0.27777778]]
X:
[[1.]
[0.]
[0.]]
可以發現\(b\)和\(c\)的排序無法區分。出現這個問題的原因在於\(a\)是一個吸收態,只有指入沒有指出。可以通過一個權值很小的均勻的轉移矩陣進行微調。取
其中權重引數\(\alpha=0.001\),則修正後的矩陣表示為\(S = (1-\alpha)\times T + \alpha \times E\)。這裡的\(T\)是歸一化為概率矩陣的\(T\)。此時有矩陣\(S\)對每個狀態都至少有一個小的轉移概率,即不存在吸收態。同時可以注意到\(T\)並不滿足不可約且正常返的條件,但\(T\)存在平穩分佈,這說明了之前的定理條件是充分條件,而非必要條件。可以留意一下這點。最終有
T = np.matrix([[0.5, 1 , 1 ],
[ 0 ,0.5,0.3],
[ 0 ,0.7,0.5]])
for i in range(T.shape[0]): # 歸一化為狀態轉移概率矩陣
T[:,i] = T[:,i]/np.sum(T[:,i])
E = np.matrix(np.ones_like(T))/T.shape[0]
alpha = 1e-3
S = (1-alpha)*T+alpha*E
X = np.matrix([1/3,1/3,1/3]) # 初始分佈
X = X.T
print(S)
print(S**2000*X)
得到
S:
[[9.99333333e-01 4.54424242e-01 5.55333333e-01]
[3.33333333e-04 2.27378788e-01 1.66833333e-01]
[3.33333333e-04 3.18196970e-01 2.77833333e-01]]
X:
[[9.98694573e-01]
[5.86177258e-04]
[7.19249506e-04]]
此結果合理,且可以看出\(a\)遠遠強於\(c,b\)。
完整程式碼及示例
最終程式碼封裝為函式:
def pagerank(T):
assert type(T) == np.matrix, 'please use np.matrix'
for i in range(T.shape[0]):
T[:,i] = T[:,i]/np.sum(T[:,i])
E = np.matrix(np.ones_like(T))/T.shape[0]
alpha = 1e-3
S = (1-alpha)*T+alpha*E
X = np.matrix([1]*T.shape[0])/T.shape[0]
X = X.T
score = S**200*X
return score
我們給一個不太好肉眼判斷的勝率關係如下:
a b c
a 0.5 0.6 0.3
b 0.4 0.5 0.6
c 0.7 0.4 0.5
這裡三個模型出現了相互剋制的情形,即
\(a \stackrel{beats}{\longrightarrow} b \stackrel{beats}{\longrightarrow} c \stackrel{beats}{\longrightarrow} a\),帶入函式:
score:
matrix([[0.30789762],
[0.34109655],
[0.35100582]])
可得排序關係\(c,b,a\)。