1.引言
假如你經營着一家網店,里面賣各種商品(Items),有很多用戶在你的店里面買過東西,並對買過的Items進行了評分,我們稱之為歷史信息,現在為了提高銷售量,必須主動向用戶推銷產品,所以關鍵是要判斷出用戶除了已經買過的商品之外還會喜歡哪些商品,這就需要利用用戶購買商品過程產生的歷史信息。協同過濾通常分為基於用戶的協同過濾和基於商品的協同過濾。
- 基於用戶的協同過濾:利用用戶之間的相似度進行推薦
- 基於物品的協同過濾:利用物品之間的相似度進行推薦
2.原理
關於協同過濾的原理網上到處都有,思想很簡單,這里就不贅述,下面舉一個簡單的實例來說明基於用戶的協同過濾:
上面每一行代表一個用戶,每一列代表一個商品,比如第2行第一列的3表示用戶2對商品1的評分為3,0代表對應的用戶還沒有購買過該商品,現在想預測用戶2對商品4的評分:
- 找出對商品4評過分的用戶:用戶1,3,5,8,9,10,評分分別為:4, 2, 1, 3, 3, 1
- 分別計算用戶2與用戶1,3,5,8,9,10之間的相似度,相似度的計算方法有很多,常用的分為3類:歐氏距離,余弦相似度,皮爾遜相關系數,網上很容易查到,這里以常用的余弦相關系數說明:
要計算用戶2與用戶1之間的相似度,首先找到二者都評過分的商品為:商品1, 2, 9, 10,用戶1對這4個商品的評分向量為r1=[5 3 4 4],用戶2對這4個商品評分向量為r2=[3 1 1 2];所謂余弦相似度就是利用兩個向量之間夾角的余弦值來衡量兩個向量之間的相似度,顯然夾角越小,余弦值就越大,兩個向量就越靠近,即二者越相似,於是用戶2和用戶1之間的相似度就為sim2_1=(5*3 + 3*1 + 4*1 + 4*2)/ (||r1|| * ||r2||) = 0.953, 其中||r||代表向量r的模長或者2范數,類似地分別計算出用戶2與用戶3 5 8 9 10之間的sim2_3,sim2_5,sim2_8,sim2_9,sim2_10
- 最后利用相似度加權得到用戶2對商品4的預測評分:predict = 4*sim2_1 + 2*sim2_3 + 1*sim2_5 + 3*sim2_8 + 3*sim2_9 + 1*sim2_10
- 基於物品相似度就是與上面計算過程幾乎相似,只是計算的是物品之間的相似度
3.實現
關於Matlab的實現可以參考:http://blog.csdn.net/google19890102/article/details/28112091,這里我用C++實現,並用movielens.rar進行測試,這個數據集是包括訓練集和測試集,已經處理成矩陣形式。
- 首先給出讀取訓練數據和保存預測結果的頭文件
-
#ifndef LOAD_H #define LOAD_H #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; template <typename T> vector<vector<T> > txtRead(string FilePath,int row,int col) { ifstream input(FilePath); if (!input.is_open()) { cerr << "File is not existing, check the path: \n" << FilePath << endl; exit(1); } vector<vector<T> > data(row, vector<T>(col,0)); for (int i = 0; i < row; ++i) { for (int j = 0; j < col; ++j) { input >> data[i][j]; } } return data; } template<typename T> void txtWrite(vector<vector<T> > Matrix, string dest) { ofstream output(dest); vector<vector<T> >::size_type row = Matrix.size(); vector<T>::size_type col = Matrix[0].size(); for (vector<vector<T> >::size_type i = 0; i < row; ++i) { for (vector<T>::size_type j = 0; j < col; ++j) { output << Matrix[i][j]; } output << endl; } } #endif
- 再給出評價預測好壞的計算RMSE的頭文件
1 #ifndef EVALUATE_H 2 #define EVALUATE_H
3 #include <cmath>
4 #include <vector>
5
6 double ComputeRMSE(vector<vector<double> > predict, vector<vector<double> > test) 7 { 8 int Counter = 0; 9 double sum = 0; 10 for (vector<vector<double> >::size_type i = 0; i < test.size(); ++i) 11 { 12 for (vector<double>::size_type j = 0; j < test[0].size(); ++j) 13 { 14 if (predict[i][j] && test[i][j]) 15 { 16 ++Counter; 17 sum += pow((test[i][j] - predict[i][j]), 2); 18 } 19 } 20 } 21 return sqrt(sum / Counter); 22 } 23
24 #endif
- 最后給出主函數:
1 #include "load.h"
2 #include "evaluate.h"
3 #include <vector>
4 #include <string>
5 #include <cmath>
6 #include <assert.h>
7 using namespace std; 8
9 double norm(vector<double> A) 10 { 11 double res = 0; 12 for(vector<double>::size_type i = 0; i < A.size(); ++i) 13 { 14 res += pow(A[i], 2); 15 } 16 return sqrt(res); 17 } 18
19 double InnerProduct(vector<double> A, vector<double> B) 20 { 21 double res = 0; 22 for(vector<double>::size_type i = 0; i < A.size(); ++i) 23 { 24 res += A[i] * B[i]; 25 } 26 return res; 27 } 28
29 double ComputeSim(vector<double> A, vector<double> B, int method) 30 { 31 switch (method) 32 { 33 case 0://歐氏距離
34 { 35 vector<double> C; 36 for(vector<double>::size_type i = 0; i < A.size(); ++i) 37 { 38 C.push_back((A[i] - B[i])); 39 } 40 return 1 / (1 + norm(C)); 41 break; 42 } 43 case 1://皮爾遜相關系數
44 { 45 double A_mean = 0; 46 double B_mean = 0; 47 for(vector<double>::size_type i = 0; i < A.size(); ++i) 48 { 49 A_mean += A[i]; 50 B_mean += B[i]; 51 } 52 A_mean /= A.size(); 53 B_mean /= B.size(); 54 vector<double> C(A); 55 vector<double> D(B); 56 for(vector<double>::size_type i = 0; i < A.size(); ++i) 57 { 58 C[i] = A[i] - A_mean; 59 D[i] = B[i] - B_mean; 60 } 61 assert(norm(C) * norm(D)); 62 return InnerProduct(C,D) / (norm(C) * norm(D)); 63 break; 64 } 65 case 2: 66 { 67 assert(norm(A) * norm(B)); 68 return InnerProduct(A,B) / (norm(A) * norm(B)); 69 break; 70 } 71 default: 72 { 73 cout << " Choose method:" << endl; 74 cout << "0:歐氏距離\n1:皮爾遜相關系數\n2:余弦相似度\n"; 75 return -1; 76 } 77 } 78
79 } 80
81 void FindCommon(vector<double> A, vector<double> B, vector<double> &C, vector<double> &D) 82 { 83 for(vector<double>::size_type i = 0; i < A.size(); ++i) 84 { 85 if (A[i] && B[i]) 86 { 87 C.push_back(A[i]); 88 D.push_back(B[i]); 89 } 90 } 91 } 92
93
94 vector<vector<double> > UserBasedCF(vector<vector<double> > train, int usersNum, int itemsNum) 95 { 96 vector<vector<double> > predict(usersNum, vector<double>(itemsNum, 0)); 97 for (int i = 0; i < usersNum; ++i) //對每個用戶進行預測
98 { 99 //找出user i未評分的item j,預測user i 對item j的評分
100 for (int j = 0; j < itemsNum; ++j) 101 { 102
103
104 if (train[i][j]) 105 continue; 106 //如果item j沒有被user i評過分,找出對 item j評過分的用戶
107 else
108 { 109 vector<double> sim; 110 vector<double> historyScores; 111 for (int k = 0; k < usersNum; ++k) 112 { 113 //如果user k對item j 評過分,計算user k與user i的相似度
114
115 if (train[k][j])//找出對item j 評過分的user k
116 { 117 // 為了計算user k與user i的相似度,必須找出二者共同評過分的items 118 // 把二者對共同評過分的items的評分分別存儲在兩個vector中
119 vector<double> commonA,commonB; 120 FindCommon(train[i], train[k], commonA, commonB); 121 //如果二者存在共同評過分的items,計算相似度
122 if (!commonA.empty()) 123 { 124 sim.push_back(ComputeSim(commonA, commonB, 2)); 125 // 把user k對item j 的歷史評分記錄下來
126 historyScores.push_back(train[k][j]); 127 } 128 } 129
130 } 131 // 計算出所有與user i存在共同評過分的items的users與user i之間的相似度, 132 // 保存在sim中,這些users對目標items j(即user i沒有評過分)的歷史評分記 133 // 錄在historyScores中。利用這兩個vector,計算出相似度加權平均分作為預 134 // 測user i對item j的評分
135 double SimSum = 0; 136 if (!sim.empty()) 137 { 138 for(vector<double>::size_type m = 0; m < sim.size(); ++m) 139 { 140 SimSum += sim[m]; 141 } 142 predict[i][j] = InnerProduct(sim, historyScores) / (SimSum); 143 cout << "User "<< i << " 對第 " << j << " 個Item的評分為 " << predict[i][j] << endl; 144 } 145 } 146 } 147 } 148 return predict; 149 } 150
151 int main() 152 { 153 string FilePath1("E:\\Matlab code\\recommendation system\\data\\movielens\\train.txt"); 154 string FilePath2("E:\\Matlab code\\recommendation system\\data\\movielens\\test.txt"); 155
156 int row = 943; 157 int col = 1682; 158 vector<vector<double> > train = txtRead<double>(FilePath1, row, col); 159 vector<vector<double> > predict = UserBasedCF(train, row, col); 160 txtWrite(predict, "predict.txt"); 161 vector<vector<double> > test = txtRead<double>(FilePath2, 462, 1591); 162 double rmse = ComputeRMSE(predict,test); 163 cout << "RMSE is " << rmse <<endl; 164 return 0; 165 }
4.運行
由於程序沒有優化,循環比較多,時間比較長,程序沒寫好,如果讀者有興趣幫我優化,請聯系我,多謝,歡迎有興趣的可以自己構造一個小點的數據集試一試,以前我用這個數據在Matlab中運行的RMSE是1左右,所以如果讀者運行結果得到測試集上的RMSE是0.9-1.3之間問題應該不大,如果偏離太多,程序設計可能就有問題。