在上一篇文章:機器學習之PageRank算法應用與C#實現(1)算法介紹 中,對PageRank算法的原理和過程進行了詳細的介紹,並通過一個很簡單的例子對過程進行了講解。從上一篇文章可以很快的了解PageRank的基礎知識。相比其他一些文獻的介紹,上一篇文章的介紹非常簡潔明了。說明:本文的主要內容都是來自“趙國,宋建成.Google搜索引擎的數學模型及其應用,西南民族大學學報自然科學版.2010,vol(36),3”這篇學術論文。鑒於文獻中本身提供了一個非常簡單容易理解和入門的案例,所以本文就使用文章的案例和思路來說明PageRank的應用,文章中的文字也大部分是復制該篇論文,個人研究是對文章的理解,以及最后一篇的使用C#實現該算法的過程,可以讓讀者更好的理解如何用程序來解決問題。所以特意對作者表示感謝。如果有認為侵權,請及時聯系我,將及時刪除處理。
論文中的案例其實是來源於1993年全國大學生數學建模競賽的B題—足球隊排名問題。
本文原文鏈接:【原創】機器學習之PageRank算法應用與C#實現(2)球隊排名應用與C#代碼
1.足球隊排名問題
1993年的全國大學生數學建模競賽B題就出了這道題目,不過當時PageRank算法還沒有問世,所以現在用PageRank來求解也只能算馬后炮,不過可以借鑒一下思路,順便可以加深對算法的理解,並可以觀察算法實際的效果怎么樣。順便說一下,全國大學生數學建模競賽的確非常有用,我在大學期間,連續參加過2004和2005年的比賽,雖然只拿了一個省二等獎,但是這個過程對我的影響非常大。包括我現在的編程,解決問題的思路都是從建模培訓開始的。希望在校大學生珍惜這些機會,如果能入選校隊,參加集訓,努力學習,對以后的學習,工作都非常有幫助。下面看看這個題目的具體問題:
具體數據由於篇幅較大,已經上傳為圖片,需要看的,點擊鏈接:數據鏈接
2.利用PageRank算法的思路
2.1 問題分析
足球隊排名次問題要求我們建立一個客觀的評估方法,只依據過去一段時間(幾個賽季或幾年)內每個球隊的戰績給出各個球隊的名次,具有很強的實際背景.通過分析題中12支足球隊在聯賽中的成績,不難發現表中的數據殘缺不全,隊與隊之間的比賽場數相差很大,直接根據比賽成績來排名次比較困難。
下面我們利用PageRank算法的隨機沖浪模型來求解.類比PageRank算法,我們可以綜合考慮各隊的比賽成績為每支球隊計算相應的等級分(Rank),然后根據各隊的等級分高低來確定名次,直觀上看,給定球隊的等級分應該由它所戰勝和戰平的球隊的數量以及被戰勝或戰平的球隊的實力共同決定.具體來說,確定球隊Z的等級分的依據應為:一是看它戰勝和戰平了多少支球隊;二要看它所戰勝或戰平球隊的等級分的高低.這兩條就是我們確定排名的基本原理.在實際中,若出現等級分相同的情況,可以進一步根據凈勝球的多少來確定排名.由於表中包含的數據量龐大,我們先在不計平局,只考慮獲勝局的情形下計算出各隊的等級分,以說明算法原理。然后我們綜合考慮獲勝局和平局,加權后得到各隊的等級分,並據此進行排名。考慮到競技比賽的結果的不確定性,我們最后建立了等級分的隨機沖浪模型,分析表明等級分排名結果具有良好的參數穩定性。
2.2 獲取轉移概率矩陣
首先利用有向賦權圖的權重矩陣來表達出各隊之間的勝負關系.用圖的頂點表示相應球隊,用連接兩個頂點的有向邊表示兩隊的比賽結果。同時給邊賦權重,表明占勝的次數。所以,可以得到數據表中給出的12支球隊所對應的權重矩陣,這是計算轉義概率矩陣的必要步驟,這里直接對論文中的截圖進行引用:
2.3 關於加權等級分
上述權重不夠科學,在論文中,作者提出了加權等級分,就是考慮平局的影響,對2個矩陣進行加權得到權重矩陣,從而得到轉移概率矩陣。這里由於篇幅比較大,但是思路比較簡單,不再詳細說明,如果需要詳細了解,可以看論文。本文還是集中在C#的實現過程。
2.4 隨機沖浪模型
3.C#編程實現過程
下面我們將使用C#實現論文中的上述過程,注意,2.3和2.2的思想是類似的,只不過是多了一個加權的過程,對程序來說還是很簡單的。下面還是按照步驟一個一個來,很多人看到問題寫程序很難下手,其實習慣就好了,按照算法的步驟來,一個一個實現,總之要先動手,不要老是想,想來想去沒有結果,浪費時間。只有實際行動起來,才能知道實際的問題,一個一個解決,持之以恆,思路會越來越清晰。
3.1 計算權重矩陣
權重矩陣要根據測試數據,球隊和每2個球隊直接的比分來獲取,所以我們使用一個字典來存儲原始數據,將每個節點,2個隊伍的比賽結果比分都寫成數組的形式,來根據勝平負的場次計算積分,得到邊的權重,看代碼吧:
1 /// <summary>根據比賽成績,直接根據積分來構造權重矩陣,根據i,對j比賽獲取的分數</summary> 2 /// <param name="data">key為2個對的邊名稱,value是比分列表,分別為主客進球數</param> 3 /// <param name="teamInfo">球隊的編號列表</param> 4 /// <returns>權重矩陣</returns> 5 public static double[,] CalcLevelTotalScore(Dictionary<String, Int32[][]> data, List<Int32> teamInfo) 6 { 7 Int32 N = teamInfo.Count; 8 double[,] result = new double[N, N]; 9 10 #region 利用對稱性,只計算一半 11 for (int i = 1; i < N; i++) 12 { 13 for (int j = i + 1; j <= N; j++) 14 { 15 #region 循環計算 16 String key = String.Format("{0}-{1}", teamInfo[i - 1], teamInfo[j - 1]); 17 //不存在比賽成績 18 if (!data.ContainsKey(key)) 19 { 20 result[i - 1, j - 1] = result[j - 1, i - 1] = 0; 21 continue; 22 } 23 //計算i,j直接的互勝場次 24 var scores = data[key];//i,j直接的比分列表 25 var Si3 = scores.Where(n => n[0] > n[1]).ToList();//i勝場次 26 var S1 = scores.Where(n => n[0] == n[1]).ToList();//i平場次 27 var Si0 = scores.Where(n => n[0] < n[1]).ToList();//i負場次 28 result[i - 1, j - 1] = Si3.Count*3 + S1.Count ; 29 result[j - 1, i - 1] = Si0.Count *3 + S1.Count ; 30 #endregion 31 } 32 } 33 #endregion 34 //按照列向量進行歸一化 35 return GetNormalizedByColumn(result); 36 }
上面最后返回調用了歸一化的函數,比較簡單,直接代碼貼出來,折疊一下:

1 /// <summary>按照列向量進行歸一化</summary> 2 /// <param name="data"></param> 3 /// <returns></returns> 4 public static double[,] GetNormalizedByColumn(double[,] data) 5 { 6 int N = data.GetLength(0); 7 double[,] result = new double[N, N]; 8 #region 各個列向量歸一化 9 for (int i = 0; i < N; i++) //列 10 { 11 double sum = 0; 12 //行 13 for (int j = 0; j < N; j++) sum += data[j, i]; 14 for (int j = 0; j < N; j++) 15 { 16 if (sum != 0) result[j, i] = data[j, i] / (double)sum;//歸一化,每列除以和值 17 else result[j, i] = data[j, i]; 18 } 19 } 20 #endregion 21 22 return result; 23 }
3.2 計算最大特征值及特征向量
計算特征值和特征向量是一個數學問題,我們采用了Math.NET數學計算組件,可以直接計算很方便。詳細的使用可以參考下面代碼,組件的其他信息可以參考本站導航欄上的專題目錄,有大量的使用文章。看代碼吧。
1 /// <summary>求最大特征值下的特征向量</summary> 2 /// <param name="data"></param> 3 /// <returns></returns> 4 public static double[] GetEigenVectors(double[,] data) 5 { 6 var formatProvider = (CultureInfo)CultureInfo.InvariantCulture.Clone(); 7 formatProvider.TextInfo.ListSeparator = " "; 8 9 int N = data.GetLength(0); 10 Matrix<double> A = DenseMatrix.OfArray(data); 11 var evd = A.Evd(); 12 var vector = evd.EigenVectors;//特征向量 13 var ev = evd.EigenValues;//特征值,復數形式發 14 15 if (ev[0].Imaginary > 0) throw new Exception("第一個特征值為復數"); 16 //取 vector 第一列為最大特征向量 17 var result = new double[N]; 18 for (int i = 0; i < N; i++) 19 { 20 result[i] =Math.Abs(vector[i, 0]);//第一列,取絕對值 21 } 22 return result; 23 }
3.3 隨機沖浪模型的實現
隨機沖浪模型主要是有一個比例,設置之后可以直接求解,也比較簡單,函數如下:
1 /// <summary>獲取隨機沖浪模型的 轉移矩陣: 2 /// 作用很明顯,結果有明顯的改善 3 /// </summary> 4 /// <returns></returns> 5 public static double[,] GetRandomModeVector(double[,] data ,double d = 0.35) 6 { 7 int N = data.GetLength(0); 8 double k = (1.0 - d) / (double)N; 9 double[,] result = new double[N, N]; 10 for (int i = 0; i < N; i++) 11 { 12 for (int j = 0; j < N; j++) result[i, j] = data[i, j] * d + k; 13 } 14 return result; 15 }
3.4 其他
其他問題就是數據組合的過程,這里太多,不詳細講解。主要是構建測試數據以及排序后結果的處理,很簡單。貼一個球隊排序的函數,根據特征向量:
1 /// <summary>排序,輸出球隊編號</summary> 2 /// <param name="w"></param> 3 /// <param name="teamInfo"></param> 4 /// <returns></returns> 5 public static Int32[] TeamOrder(double[] w, List<Int32> teamInfo) 6 { 7 Dictionary<int, double> dic = new Dictionary<int, double>(); 8 for (int i = 1; i <= w.Length; i++) dic.Add(i , w[i-1]); 9 return dic.OrderByDescending(n => n.Value).Select(n => n.Key).ToArray(); 10 }
4.算法測試
我們使用問題1中的數據,進行測試,首先構建測試集合,代碼如下,太長,折疊一下,主要是問題1的原始數據:

1 /// <summary> 2 /// 獲取測試的數據集,key=對1-對2,value = int[,] 為比分 3 /// </summary> 4 public static Dictionary<String, Int32[][]> GetTestData() 5 { 6 Dictionary<String, Int32[][]> data = new Dictionary<string, int[][]>(); 7 #region 依次添加數據 8 #region T1 9 data.Add("1-2", new Int32[][]{ new Int32[] { 0, 1 }, new Int32[] { 1, 0 }, new Int32[] { 0, 0 } }); 10 data.Add("1-3", new Int32[][] { new Int32[] { 2, 2 }, new Int32[] { 1, 0 }, new Int32[] { 0, 2 } }); 11 data.Add("1-4", new Int32[][] { new Int32[] { 2, 0 }, new Int32[] { 3, 1 }, new Int32[] { 1, 0 } }); 12 data.Add("1-5", new Int32[][] { new Int32[] { 3, 1 } }); 13 data.Add("1-6", new Int32[][] { new Int32[] { 1, 0 } }); 14 data.Add("1-7", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 1, 3 } }); 15 data.Add("1-8", new Int32[][] { new Int32[] { 0, 2 }, new Int32[] { 2, 1 } }); 16 data.Add("1-9", new Int32[][]{ new Int32[] { 1, 0 }, new Int32[] { 4, 0 } }); 17 data.Add("1-10", new Int32[][]{ new Int32[] { 1, 1 }, new Int32[] { 1, 1 } }); 18 #endregion 19 20 #region T2 21 data.Add("2-3", new Int32[][] { new Int32[] { 2, 0 }, new Int32[] { 0, 1 }, new Int32[] { 1, 3 } }); 22 data.Add("2-4", new Int32[][] { new Int32[] { 0, 0 }, new Int32[] { 2, 0 }, new Int32[] { 0, 0 } }); 23 data.Add("2-5", new Int32[][] { new Int32[] { 1, 1 } }); 24 data.Add("2-6", new Int32[][] { new Int32[] { 2, 1 } }); 25 data.Add("2-7", new Int32[][] { new Int32[] { 1, 1 }, new Int32[] { 1, 1 } }); 26 data.Add("2-8", new Int32[][] { new Int32[] { 0, 0 }, new Int32[] { 0, 0 } }); 27 data.Add("2-9", new Int32[][] { new Int32[] { 2, 0 }, new Int32[] { 1, 1 } }); 28 data.Add("2-10", new Int32[][] { new Int32[] { 0, 2 }, new Int32[] { 0, 0 } }); 29 #endregion 30 31 #region T3 32 data.Add("3-4", new Int32[][] { new Int32[] { 4, 2 }, new Int32[] { 1, 1 }, new Int32[] { 0, 0 } }); 33 data.Add("3-5", new Int32[][] { new Int32[] { 2, 1 } }); 34 data.Add("3-6", new Int32[][] { new Int32[] { 3, 0 } }); 35 data.Add("3-7", new Int32[][] { new Int32[] { 1, 0 }, new Int32[] { 1, 4 } }); 36 data.Add("3-8", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 3, 1 } }); 37 data.Add("3-9", new Int32[][] { new Int32[] { 1, 0 }, new Int32[] { 2, 3 } }); 38 data.Add("3-10", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 2, 0 } }); 39 #endregion 40 41 #region T4 42 data.Add("4-5", new Int32[][] { new Int32[] { 2, 3 } }); 43 data.Add("4-6", new Int32[][] { new Int32[] { 0, 1 } }); 44 data.Add("4-7", new Int32[][] { new Int32[] { 0, 5 }, new Int32[] { 2, 3 } }); 45 data.Add("4-8", new Int32[][] { new Int32[] { 2, 1 }, new Int32[] { 1, 3 } }); 46 data.Add("4-9", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 0, 0 } }); 47 data.Add("4-10", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 1, 1 } }); 48 #endregion 49 50 #region T5 51 data.Add("5-6", new Int32[][] { new Int32[] { 0, 1 } }); 52 data.Add("5-11", new Int32[][] { new Int32[] { 1, 0 }, new Int32[] { 1, 2 } }); 53 data.Add("5-12", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 1, 1 } }); 54 #endregion 55 56 #region T7 57 data.Add("7-8", new Int32[][] { new Int32[] { 1, 0 }, new Int32[] { 2, 0 }, new Int32[] { 0, 0 } }); 58 data.Add("7-9", new Int32[][] { new Int32[] { 2, 1 }, new Int32[] { 3, 0 }, new Int32[] { 1, 0 } }); 59 data.Add("7-10", new Int32[][] { new Int32[] { 3, 1 }, new Int32[] { 3, 0 }, new Int32[] { 2, 2 } }); 60 data.Add("7-11", new Int32[][] { new Int32[] { 3, 1 } }); 61 data.Add("7-12", new Int32[][] { new Int32[] { 2, 0 } }); 62 #endregion 63 64 #region T8 65 data.Add("8-9", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 1, 2 }, new Int32[] { 2, 0 } }); 66 data.Add("8-10", new Int32[][] { new Int32[] { 1, 1 }, new Int32[] { 1, 0 }, new Int32[] { 0, 1 } }); 67 data.Add("8-11", new Int32[][] { new Int32[] { 3, 1 } }); 68 data.Add("8-12", new Int32[][] { new Int32[] { 0, 0 } }); 69 #endregion 70 71 #region T9 72 data.Add("9-10", new Int32[][] { new Int32[] { 3, 0 }, new Int32[] { 1, 0 }, new Int32[] { 0, 0 } }); 73 data.Add("9-11", new Int32[][] { new Int32[] { 1, 0 } }); 74 data.Add("9-12", new Int32[][] { new Int32[] { 1, 0 } }); 75 #endregion 76 77 #region T10 78 data.Add("10-11", new Int32[][] { new Int32[] { 1, 0 } }); 79 data.Add("10-12", new Int32[][] { new Int32[] { 2, 0 } }); 80 #endregion 81 82 #region T11 83 data.Add("11-12", new Int32[][] { new Int32[] { 1, 1 }, new Int32[] { 1, 2 }, new Int32[] { 1, 1 } }); 84 #endregion 85 #endregion 86 return data; 87 }
測試的主要方法是:
1 var team = new List<Int32>(){1,2,3,4,5,6,7,8,9,10,11,12}; 2 var data = GetTestData(); 3 var k3 = CalcLevelScore3(data,team); 4 var w3 = GetEigenVectors(k3); 5 6 var teamOrder = TeamOrder(w3,team); 7 Console.WriteLine(teamOrder.ArrayToString());
排序結果如下:
7,3,1,9,8,2,10,4,6,5,12,11
結果和論文差不多,差別在前面2個,隊伍7和3的位置有點問題。具體應該是計算精度的關系如果前面的計算有一些精度損失的話,對后面的計算有一點點影響。
PageRank的一個基本應用今天就到此為止,接下來如果大家感興趣,我將繼續介紹PageRank在球隊排名和比賽預測結果中的應用情況。看時間安排,大概思路和本文類似,只不過在細節上要處理一下。