在上一篇文章:《機器學習之PageRank演算法應用與C#實現(1):演算法介紹》中,對PageRank演算法的原理和過程進行了詳細的介紹,並通過一個很簡單的例子對過程進行了講解。從上一篇文章可以很快的瞭解PageRank的基礎知識。
相比其他一些文獻的介紹,上一篇文章的介紹非常簡潔明瞭。說明:本文的主要內容都是來自“趙國,宋建成.Google搜尋引擎的數學模型及其應用,西南民族大學學報自然科學版.2010,vol(36),3”這篇學術論文。鑑於文獻中本身提供了一個非常簡單容易理解和入門的案例,所以本文就使用文章的案例和思路來說明PageRank的應用,文章中的文字也大部分是複製該篇論文,個人研究是對文章的理解,以及最後一篇的使用C#實現該演算法的過程,可以讓讀者更好的理解如何用程式來解決問題。所以特意對作者表示感謝。如果有認為侵權,請及時聯絡我,將及時刪除處理。
論文中的案例其實是來源於1993年全國大學生數學建模競賽的B題—足球隊排名問題。
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
/// <summary>根據比賽成績,直接根據積分來構造權重矩陣,根據i,對j比賽獲取的分數</summary> /// <param name="data">key為2個對的邊名稱,value是比分列表,分別為主客進球數</param> /// <param name="teamInfo">球隊的編號列表</param> /// <returns>權重矩陣</returns> public static double[,] CalcLevelTotalScore(Dictionary<String, Int32[][]> data, List<Int32> teamInfo) { Int32 N = teamInfo.Count; double[,] result = new double[N, N]; #region 利用對稱性,只計算一半 for (int i = 1; i < N; i++) { for (int j = i + 1; j <= N; j++) { #region 迴圈計算 String key = String.Format("{0}-{1}", teamInfo[i - 1], teamInfo[j - 1]); //不存在比賽成績 if (!data.ContainsKey(key)) { result[i - 1, j - 1] = result[j - 1, i - 1] = 0; continue; } //計算i,j直接的互勝場次 var scores = data[key];//i,j直接的比分列表 var Si3 = scores.Where(n => n[0] > n[1]).ToList();//i勝場次 var S1 = scores.Where(n => n[0] == n[1]).ToList();//i平場次 var Si0 = scores.Where(n => n[0] < n[1]).ToList();//i負場次 result[i - 1, j - 1] = Si3.Count*3 + S1.Count ; result[j - 1, i - 1] = Si0.Count *3 + S1.Count ; #endregion } } #endregion //按照列向量進行歸一化 return GetNormalizedByColumn(result); } |
上面最後返回撥用了歸一化的函式,比較簡單,直接程式碼貼出來:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/// <summary>按照列向量進行歸一化</summary> /// <param name="data"></param> /// <returns></returns> public static double[,] GetNormalizedByColumn(double[,] data) { int N = data.GetLength(0); double[,] result = new double[N, N]; #region 各個列向量歸一化 for (int i = 0; i < N; i++) //列 { double sum = 0; //行 for (int j = 0; j < N; j++) sum += data[j, i]; for (int j = 0; j < N; j++) { if (sum != 0) result[j, i] = data[j, i] / (double)sum;//歸一化,每列除以和值 else result[j, i] = data[j, i]; } } #endregion return result; } |
3.2 計算最大特徵值及特徵向量
計算特徵值和特徵向量是一個數學問題,我們採用了Math.NET數學計算元件,可以直接計算很方便。詳細的使用可以參考下面程式碼,元件的其他資訊可以參考本站導航欄上的專題目錄,有大量的使用文章。看程式碼吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/// <summary>求最大特徵值下的特徵向量</summary> /// <param name="data"></param> /// <returns></returns> public static double[] GetEigenVectors(double[,] data) { var formatProvider = (CultureInfo)CultureInfo.InvariantCulture.Clone(); formatProvider.TextInfo.ListSeparator = " "; int N = data.GetLength(0); Matrix<double> A = DenseMatrix.OfArray(data); var evd = A.Evd(); var vector = evd.EigenVectors;//特徵向量 var ev = evd.EigenValues;//特徵值,複數形式發 if (ev[0].Imaginary > 0) throw new Exception("第一個特徵值為複數"); //取 vector 第一列為最大特徵向量 var result = new double[N]; for (int i = 0; i < N; i++) { result[i] =Math.Abs(vector[i, 0]);//第一列,取絕對值 } return result; } |
3.3 隨機衝浪模型的實現
隨機衝浪模型主要是有一個比例,設定之後可以直接求解,也比較簡單,函式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/// <summary>獲取隨機衝浪模型的 轉移矩陣: /// 作用很明顯,結果有明顯的改善 /// </summary> /// <returns></returns> public static double[,] GetRandomModeVector(double[,] data ,double d = 0.35) { int N = data.GetLength(0); double k = (1.0 - d) / (double)N; double[,] result = new double[N, N]; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) result[i, j] = data[i, j] * d + k; } return result; } |
3.4 其他
其他問題就是資料組合的過程,這裡太多,不詳細講解。主要是構建測試資料以及排序後結果的處理,很簡單。貼一個球隊排序的函式,根據特徵向量:
1 2 3 4 5 6 7 8 9 10 |
/// <summary>排序,輸出球隊編號</summary> /// <param name="w"></param> /// <param name="teamInfo"></param> /// <returns></returns> public static Int32[] TeamOrder(double[] w, List<Int32> teamInfo) { Dictionary<int, double> dic = new Dictionary<int, double>(); for (int i = 1; i <= w.Length; i++) dic.Add(i , w[i-1]); return dic.OrderByDescending(n => n.Value).Select(n => n.Key).ToArray(); } |
4.演算法測試
我們使用問題1中的資料,進行測試,首先構建測試集合,程式碼如下,太長,摺疊一下,主要是問題1的原始資料:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
/// <summary> /// 獲取測試的資料集,key=對1-對2,value = int[,] 為比分 /// </summary> public static Dictionary<String, Int32[][]> GetTestData() { Dictionary<String, Int32[][]> data = new Dictionary<string, int[][]>(); #region 依次新增資料 #region T1 data.Add("1-2", new Int32[][]{ new Int32[] { 0, 1 }, new Int32[] { 1, 0 }, new Int32[] { 0, 0 } }); data.Add("1-3", new Int32[][] { new Int32[] { 2, 2 }, new Int32[] { 1, 0 }, new Int32[] { 0, 2 } }); data.Add("1-4", new Int32[][] { new Int32[] { 2, 0 }, new Int32[] { 3, 1 }, new Int32[] { 1, 0 } }); data.Add("1-5", new Int32[][] { new Int32[] { 3, 1 } }); data.Add("1-6", new Int32[][] { new Int32[] { 1, 0 } }); data.Add("1-7", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 1, 3 } }); data.Add("1-8", new Int32[][] { new Int32[] { 0, 2 }, new Int32[] { 2, 1 } }); data.Add("1-9", new Int32[][]{ new Int32[] { 1, 0 }, new Int32[] { 4, 0 } }); data.Add("1-10", new Int32[][]{ new Int32[] { 1, 1 }, new Int32[] { 1, 1 } }); #endregion #region T2 data.Add("2-3", new Int32[][] { new Int32[] { 2, 0 }, new Int32[] { 0, 1 }, new Int32[] { 1, 3 } }); data.Add("2-4", new Int32[][] { new Int32[] { 0, 0 }, new Int32[] { 2, 0 }, new Int32[] { 0, 0 } }); data.Add("2-5", new Int32[][] { new Int32[] { 1, 1 } }); data.Add("2-6", new Int32[][] { new Int32[] { 2, 1 } }); data.Add("2-7", new Int32[][] { new Int32[] { 1, 1 }, new Int32[] { 1, 1 } }); data.Add("2-8", new Int32[][] { new Int32[] { 0, 0 }, new Int32[] { 0, 0 } }); data.Add("2-9", new Int32[][] { new Int32[] { 2, 0 }, new Int32[] { 1, 1 } }); data.Add("2-10", new Int32[][] { new Int32[] { 0, 2 }, new Int32[] { 0, 0 } }); #endregion #region T3 data.Add("3-4", new Int32[][] { new Int32[] { 4, 2 }, new Int32[] { 1, 1 }, new Int32[] { 0, 0 } }); data.Add("3-5", new Int32[][] { new Int32[] { 2, 1 } }); data.Add("3-6", new Int32[][] { new Int32[] { 3, 0 } }); data.Add("3-7", new Int32[][] { new Int32[] { 1, 0 }, new Int32[] { 1, 4 } }); data.Add("3-8", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 3, 1 } }); data.Add("3-9", new Int32[][] { new Int32[] { 1, 0 }, new Int32[] { 2, 3 } }); data.Add("3-10", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 2, 0 } }); #endregion #region T4 data.Add("4-5", new Int32[][] { new Int32[] { 2, 3 } }); data.Add("4-6", new Int32[][] { new Int32[] { 0, 1 } }); data.Add("4-7", new Int32[][] { new Int32[] { 0, 5 }, new Int32[] { 2, 3 } }); data.Add("4-8", new Int32[][] { new Int32[] { 2, 1 }, new Int32[] { 1, 3 } }); data.Add("4-9", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 0, 0 } }); data.Add("4-10", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 1, 1 } }); #endregion #region T5 data.Add("5-6", new Int32[][] { new Int32[] { 0, 1 } }); data.Add("5-11", new Int32[][] { new Int32[] { 1, 0 }, new Int32[] { 1, 2 } }); data.Add("5-12", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 1, 1 } }); #endregion #region T7 data.Add("7-8", new Int32[][] { new Int32[] { 1, 0 }, new Int32[] { 2, 0 }, new Int32[] { 0, 0 } }); data.Add("7-9", new Int32[][] { new Int32[] { 2, 1 }, new Int32[] { 3, 0 }, new Int32[] { 1, 0 } }); data.Add("7-10", new Int32[][] { new Int32[] { 3, 1 }, new Int32[] { 3, 0 }, new Int32[] { 2, 2 } }); data.Add("7-11", new Int32[][] { new Int32[] { 3, 1 } }); data.Add("7-12", new Int32[][] { new Int32[] { 2, 0 } }); #endregion #region T8 data.Add("8-9", new Int32[][] { new Int32[] { 0, 1 }, new Int32[] { 1, 2 }, new Int32[] { 2, 0 } }); data.Add("8-10", new Int32[][] { new Int32[] { 1, 1 }, new Int32[] { 1, 0 }, new Int32[] { 0, 1 } }); data.Add("8-11", new Int32[][] { new Int32[] { 3, 1 } }); data.Add("8-12", new Int32[][] { new Int32[] { 0, 0 } }); #endregion #region T9 data.Add("9-10", new Int32[][] { new Int32[] { 3, 0 }, new Int32[] { 1, 0 }, new Int32[] { 0, 0 } }); data.Add("9-11", new Int32[][] { new Int32[] { 1, 0 } }); data.Add("9-12", new Int32[][] { new Int32[] { 1, 0 } }); #endregion #region T10 data.Add("10-11", new Int32[][] { new Int32[] { 1, 0 } }); data.Add("10-12", new Int32[][] { new Int32[] { 2, 0 } }); #endregion #region T11 data.Add("11-12", new Int32[][] { new Int32[] { 1, 1 }, new Int32[] { 1, 2 }, new Int32[] { 1, 1 } }); #endregion #endregion return data; } |
測試的主要方法是:
1 2 3 4 5 6 7 |
var team = new List<Int32>(){1,2,3,4,5,6,7,8,9,10,11,12}; var data = GetTestData(); var k3 = CalcLevelScore3(data,team); var w3 = GetEigenVectors(k3); var teamOrder = TeamOrder(w3,team); Console.WriteLine(teamOrder.ArrayToString()); |
排序結果如下:
1 |
7,3,1,9,8,2,10,4,6,5,12,11 |
結果和論文差不多,差別在前面2個,隊伍7和3的位置有點問題。具體應該是計算精度的關係如果前面的計算有一些精度損失的話,對後面的計算有一點點影響。
PageRank的一個基本應用今天就到此為止,接下來如果大家感興趣,我將繼續介紹PageRank在球隊排名和比賽預測結果中的應用情況。看時間安排,大概思路和本文類似,只不過在細節上要處理一下。