1.引言
許多網站都喜歡讓用戶點擊“喜歡/不喜歡”,“頂/反對”,也正是這種很簡單的信息也可以利用起來對用戶進行推薦!這里介紹一種基於網絡結構的推薦系統!
由於推薦系統深深植根於互聯網,用戶與用戶之間,商品與商品之間,用戶與商品之間都存在某種聯系,把用戶和商品都看作節點,他(它)們之間的聯系看作是邊,那么就很自然地構建出一個網絡圖,所以很多研究者利用這個網絡圖進行個性化推薦,取得了不錯的效果!
2.二部圖

上面就是一個二部圖:分為連個部分,圓圈代表的節點為一部分,方塊代表的節點為另一部分,二部圖的特點:邊只存在與不同類之間,同一部分之間的節點之間不存在邊連接,正如上圖所示,圓圈與圓圈之間沒有邊,方塊與方塊之間也沒有邊,邊只存在於圓圈與方塊之間.
3.概率傳播(probability spreading,ProbS)
本文要實現的基於二部圖的推薦系統利用了一種叫做概率傳播的機制,這里做一個介紹:

在(a)中,對上面的一部分節點分配初始資源x, y, z, 在(b)中資源以等概率傳播的方式,從上面的節點傳遞給下面的節點,所謂等概率傳播,就是每一節點的資源平均傳遞給它的每一個與它存在邊聯系的節點,在(c)中資源又以等概率傳播的方式傳回到上面的節點,可以看出原來三個節點的資源由x, y, z變為11x/18+y/6+5z/18, x/9+5y/12+5z/18, 5x/18+5y/12+4z/9,這樣傳播的目的是什么呢?
我們知道在二部圖中,同一部分節點之間是沒有邊連接的,那么同一部分之間節點之間的關系就沒法直接找到,通過這種二步傳播的方式之后,每一個節點的資源都混合有其他節點的資源,把上面三個節點從左到右分別記為A, B, C,A包含了1/6來自於B節點的資源,還包含了5/18來自於C節點的資源,很顯然對於A 節點而言, C節點要比B節點重要一些,所以就利用傳播后的這些系數來表示同一部分節點之間的關系權重,我們用x', y', z'來表示第二次傳播后的資源,則有:

上面的數值矩陣就是節點之間的關系權重矩陣,例如A節點對B節點之間的關系權重為1/6,注意這是一個非對稱的:B節點對A節點的關系權重為1/9,怎么理解呢?可以理解為你把一個妹子看作是女神,但是這個妹子心中的Mr Right很可能是另外一個人,這種關系是不對等的. 也正是隱藏的這種不對等關系,正好有利於個性化推薦.
3.利用ProbS產生推薦
還是對未評價過的商品進行預測評分,把評分較高的若干商品推薦給目標用戶:
還是以上面的二部圖為例,把上面的3個節點看作是商品節點,從左到右分別記作A, B, C,下面的4個節點看作是用戶,從左到右分別記作U1, U2, U3, U4, 存在邊連接的用戶和商品,表示對應的用戶喜歡該商品。那么這個二部圖用鄰接矩陣可以表示為:
| A | B | C | |
| U1 | 1 | 0 | 0 |
| U2 | 1 | 1 | 1 |
| U3 | 0 | 1 | 1 |
| U4 | 1 | 0 | 1 |
現在我們想預測U3對A商品的喜歡程度會如何,已知U3喜歡商品B和C,寫出上面推導出的關系權重矩陣:

由此可以知道A商品與B, C商品的關系權重分別為1/6, 5/18,那么預測喜歡程度:
1*1/6 + 1*5/18 = 4/9,
如果有更多未知喜歡程度的商品,都是以這種方式:根據用戶已經喜歡的商品與未知喜歡程度商品之間的關系權重來預測這個用戶對要預測商品的喜歡程度的評分,根據評分高低,優先向用戶推薦高分商品!
關於更多詳細的介紹請參考:Bipartite_network_projection_and_personal_recommendation.pdf
4.C++實現
由於程序沒有優化,在較大數據上運行較慢,所以這里自己隨便造了一個數據集,對其為0的地方進行評分預測,但是由於沒有測試集,所以就沒有測命中率,有興趣的讀者可以自己優化一下程序,然后在movielens.rar數據上運行並測試命中率,這里主要注重原理,如果有讀者根據此原理編出更高效得代碼歡迎與我交流,多謝!

由於該數據集是1-5的評分數據,在程序讀取的時候將其處理為喜歡/不喜歡(1/0)的數據集:評分大於等於3的視為喜歡,置為1,否則置為0.
1 #include <iostream>
2 #include <fstream>
3 #include <vector>
4 #include <string>
5 #include <vector>
6 #include <algorithm>
7 #include <iomanip>
8 using namespace std; 9
10 //從TXT中讀入數據到矩陣(二維數組)
11 template <typename T>
12 vector<vector<T> > txtRead(string FilePath,int row,int col) 13 { 14 ifstream input(FilePath); 15 if (!input.is_open()) 16 { 17 cerr << "File is not existing, check the path: \n" << FilePath << endl; 18 exit(1); 19 } 20 vector<vector<T> > data(row, vector<T>(col,0)); 21 for (int i = 0; i < row; ++i) 22 { 23 for (int j = 0; j < col; ++j) 24 { 25 //因為這里針對的情況是用戶只給出對items的喜歡與不喜歡的情況,而movielens 26 //是一個1-5的評分數據,所以把分數達到3的看作是喜歡,標記為1,小於3的視為 27 // 不喜歡,置為0
28 input >> data[i][j]; 29 if (data[i][j] >= 3) 30 data[i][j] = 1; 31 else
32 data[i][j] = 0; 33 } 34 } 35 return data; 36 } 37
38 //把矩陣中的數據寫入TXT文件
39 template<typename T>
40 void txtWrite(vector<vector<T> > Matrix, string dest) 41 { 42 ofstream output(dest); 43 vector<vector<T> >::size_type row = Matrix.size(); 44 vector<T>::size_type col = Matrix[0].size(); 45 for (vector<vector<T> >::size_type i = 0; i < row; ++i) 46 { 47 for (vector<T>::size_type j = 0; j < col; ++j) 48 { 49
50 output << setprecision(3)<< Matrix[i][j] << "\t"; 51 } 52 output << endl; 53 } 54 } 55
56 // 求兩個向量的內積
57 double InnerProduct(std::vector<double> A, std::vector<double> B) 58 { 59 double res = 0; 60 for(std::vector<double>::size_type i = 0; i < A.size(); ++i) 61 { 62 res += A[i] * B[i]; 63 } 64 return res; 65 } 66
67 //矩陣轉置操作
68 template<typename T>// 69 vector<vector<T> > Transpose(vector<vector<T> > Matrix) 70 { 71 unsigned row = Matrix.size(); 72 unsigned col = Matrix[0].size(); 73 vector<vector<T> > Trans(col,vector<T>(row,0)); 74 for (unsigned i = 0; i < col; ++i) 75 { 76 for (unsigned j = 0; j < row; ++j) 77 { 78 Trans[i][j] = Matrix[j][i]; 79 } 80 } 81 return Trans; 82 } 83
84 //求一個向量中所有元素的和
85 template<typename T>
86 T SumVector(vector<T> vec) 87 { 88 T res = 0; 89
90 for (vector<T>::size_type i = 0; i < vec.size(); ++i) 91 res += vec[i]; 92 return res; 93 } 94
95 //對一個向量中的元素進行降序排列,返回重排后的元素在原來 96 //向量中的索引
97 bool IsBigger(double a, double b) 98 { 99 return a >= b; 100 } 101 vector<unsigned> DescendVector(vector<double> vec) 102 { 103 vector<double> tmpVec = vec; 104 sort(tmpVec.begin(), tmpVec.end(), IsBigger); 105 vector<unsigned> idx; 106 for (vector<double>::size_type i = 0; i < tmpVec.size(); ++i) 107 { 108 for (vector<double>::size_type j = 0; j < vec.size(); ++j) 109 { 110 if (tmpVec[i] == vec[j]) 111 idx.push_back(j); 112 } 113 } 114 return idx; 115 } 116
117
118 //基於概率傳播(ProbS)的二部圖的推薦函數,data是訓練數據
119 vector<vector<double> > ProbS(vector<vector<double> > data) 120 { 121 auto row = data.size(); 122 auto col = data[0].size(); 123 vector<vector<double> > transData = Transpose(data); 124
125 //第一步利用概率傳播機制計算權重矩陣
126 vector<vector<double> > weights(col, vector<double>(col, 0)); 127 for (vector<double>::size_type i = 0; i < col; ++i) 128 { 129 for (vector<double>::size_type j = 0; j < col; ++j) 130 { 131 double degree = SumVector<double>(transData[j]); 132 double sum = 0; 133 for (vector<double>::size_type k = 0; k < row; ++k) 134 { 135 sum += transData[i][k] * transData[j][k] / SumVector<double>(data[k]); 136 } 137 if (degree) 138 weights[i][j] = sum / degree; 139 } 140 } 141
142 //第二步利用權重矩陣和訓練數據集針對每個用戶對每一個item評分
143 vector<vector<double> > scores(row, vector<double>(col, 0)); 144 for (vector<double>::size_type i = 0; i < row; ++i) 145 { 146 for (vector<double>::size_type j = 0; j < col; ++j) 147 { 148 //等於0的地方代表user i 還木有評價過item j,需要預測
149 if (0 == data[i][j]) 150 scores[i][j] = InnerProduct(weights[j],data[i]); 151 } 152 } 153 return scores; 154 } 155
156 //計算推薦結果的命中率:推薦的items中用戶確實喜歡的items數量/推薦的items數量 157 //用戶確實喜歡的items是由測試集給出,length表示推薦列表最長為多少,這里將測出 158 //推薦列表長度由1已知增加到length過程中,推薦命中率的變化
159 vector<vector<double> > ComputeHitRate(vector<vector<double> > scores, vector<vector<double> > test, 160 unsigned length) 161 { 162 auto usersNum = test.size(); 163 auto itemsNum = test[0].size(); 164
165 vector<vector<unsigned> > sortedIndex; 166 //因為只是對測試集中的用戶和items進行測試,於是選取與測試集大小一樣的預測數據
167 vector<vector<double> > selectedScores(usersNum, vector<double>(itemsNum,0)); 168 vector<double> line; 169 for (unsigned i = 0; i < usersNum; ++i) 170 { 171 for (unsigned j = 0; j < itemsNum; ++j) 172 { 173 line.push_back(scores[i][j]); 174 } 175 sortedIndex.push_back(DescendVector(line)); 176 line.clear(); 177 } 178 //hitRate的第一列存儲推薦列表的長度,第二列存儲對應的命中率
179 vector<vector<double> > hitRate(length); 180 for (unsigned k = 1; k <= length; ++k) 181 { 182 hitRate[k-1].push_back(k); 183 double Counter = 0; 184 for (unsigned i = 0; i < usersNum; ++i) 185 { 186 for (unsigned j = 0; j < k; ++j) 187 { 188 unsigned itemIndex = sortedIndex[i][j]; 189 if (test[i][itemIndex]) 190 ++Counter; 191 } 192 } 193 hitRate[k-1].push_back(Counter / (k * usersNum)); 194 } 195 return hitRate; 196 } 197 int main() 198 { 199 string FilePath1("data.txt"); 200 //string FilePath2("E:\\Matlab code\\recommendation system\\data\\movielens\\test.txt");
201
202 int row = 10; 203 int col = 10; 204 cout << "數據讀取中..." << endl; 205 vector<vector<double> > train = txtRead<double>(FilePath1, row, col); 206 //vector<vector<double> > test = txtRead<double>(FilePath2, 462, 1591);
207
208 cout << "利用二部圖網絡進行評分預測..." << endl; 209 vector<vector<double> > predictScores = ProbS(train); 210 txtWrite(predictScores, "predictScores.txt"); 211 /*
212 cout << "計算命中率..." << endl; 213 vector<vector<double> > hitRate = ComputeHitRate(predictScores, test, 1591); 214
215 txtWrite(hitRate, "hitRate.txt"); 216 cout << "命中率結果保存完畢!" << endl; 217 */
218 return 0; 219 }
5.運行

預測結果:

說明:預測為0的地方是在訓練集中已經標記為1的地方,即明確了用戶喜歡對應的商品,所以就沒有必要對其進行預測,由於初始化預測評分的時候,全部初始化為0,所以沒有必要預測的的元素為0.用戶已經喜歡的是商品就沒有必要再推薦給他(她)了,為了增加銷售額,必須向用戶盡可能推薦他(她)曾經不太注意的新商品,有了這些評分,系統就可以按照評分高低對用戶推薦相應的商品了!比如上面的預測結果,對於第一個用戶就要優先推薦第6個商品,其次推薦第3個商品,以此類推。
