主要参考论文《A Guide to Singular Value Decomp osition for Collab orative Filtering》
其实一开始是比较疑惑的,因为一开始没有查看论文,只是网上搜了一下svd的概念和用法,搜到的很多都是如下的公式:其中假设C是m*n的话,那么可以得到三个分解后的矩阵,分别为m*r,r*r,r*n,这样的话就可以大大降低存储代价,但是这里特别需要注意的是:这个概念一开始是用于信息检索方面的,它的C矩阵式完整的,故他们可以直接把这个矩阵应用svd分解,但是在推荐系统中,用户和物品的那个评分矩阵是不完整的,是个稀疏矩阵,故不能直接分解成上述样子,也有的文章说把空缺的补成平均值等方法,但效果都不咋滴。。。在看了上述论文之后,才明白,在协同过滤中应用svd,其实是个最优化问题,我们假设用户和物品之间没有直接关系,但是定义了一个维度,称为feature,feature是用来刻画特征的,比如描述这个电影是喜剧还是悲剧,是动作片还是爱情片,而用户和feature之间是有关系的,比如某个用户必选看爱情片,另外一个用户喜欢看动作片,物品和feature之间也是有关系的,比如某个电影是喜剧,某个电影是悲剧,那么通过和feature之间的联系,我们可以把一个评分矩阵rating
= m*n(m代表用户数,n代表物品数)分解成两个矩阵的相乘:user_feature*T(item_feature), T()表示转置,其中user_feature是m*k的(k是feature维度,可以随便定),item_feature是n*k的,那么我们要做的就是求出这两者矩阵中的值,使得两矩阵相乘的结果和原来的评分矩阵越接近越好。
这里所谓的越接近越好,指的是期望越小越好,期望的式子如下:
其中n表示用户数目,m表示物品数目,I[i][j]是用来表示用户i有没有对物品j评过分,因为我们只需要评过分的那些越接近越好,没评过的就不需要考虑,Vij表示训练数据中给出的评分,也就是实际评分,p(Ui,Mj)表示我们对用户i对物品j的评分的预测,结果根据两向量点乘得到,两面的两项主要是为了防止过拟合,之所以都加了系数1/2是为了等会求导方便。
这里我们的目标是使得期望E越小越好,其实就是个期望最小的问题,故我们可以用随机梯度下降来实现。随机梯度下降说到底就是个求导问题,处于某个点的时候,在这个点上进行求导,然后往梯度最大的反方向走,就能快速走到局部最小值。故我们对上述式子求导后得:
所以其实这个算法的流程就是如下过程:
实现起来还是比较方便快捷的,这里rmse是用来评测效果的,后面会再讲。
上述算法其实被称为批处理式学习算法,之所以叫批处理是因为它的期望是计算整个矩阵的期望(so big batch),其实还存在增量式学习算法,批处理和增量式的区别就在于前者计算期望是计算整个矩阵的,后者只计算矩阵中的一行或者一个点的期望,其中计算一行的期望被称为不完全增量式学习,计算一个点的期望被称为完全增量式学习。
不完全增量式学习期望如下(针对矩阵中的一行的期望,也就是针对一个用户i的期望):
那么求导后的式子如下:
算法的大致思想如下:
完全增量式学习算法是对每一个评分进行期望计算,期望如下:
求导后如下:
所以整个算法流程是这样的:
上述都是svd的变种,只不过实现方式不一样,根据论文所说,其中第三种完全增量式学习算法效果最好,收敛速度非常快。
当然还有更优的变种,考虑了每个用户,每个物品的bias,这里所谓的bias就是每个人的偏差,比如一个电影a,b两人都认为不错,但是a评分方便比较保守,不错给3分,b评分比较宽松,不错给4分,故一下的评分方式考虑到了每个用户,每个物品的bias,要比上述算法更加精准。原来评分的话是直接计算user_feature*T(item_feature), T()表示转置,但现在要考虑各种bias,如下:
其中a表示所有评分的平均数,ai表示用户i的bias,Bj表示物品j的偏差,相乘的矩阵还是和上面一样的意思。
故这时候的期望式子和求导的式子如下(这里只写了bias的求导,矩阵求导还是和上面一样):
当然了,光说不练假把式,我们选择了最后一种算法,及考虑bias的算法来实现了一把,数据源是来自movielens的100k的数据,其中包含了1000个用户对2000件物品的评分(当然,我这里是直接开的数组,要是数据量再大的话,就不这么实现了,主要是为了验证一把梯度下降的效果),用其中的base数据集来训练模型,用test数据集来测试数据,效果评测用一下式子来衡量:
说白了就是误差平方和。。。同时我们也记录下每一次迭代后训练数据集的rmse.
代码如下:
#include <iostream>
#include <string>
#include <fstream>
#include <math.h>
using namespace std;
const int USERMAX = 1000;
const int ITEMMAX = 2000;
const int FEATURE = 50;
const int ITERMAX = 20;
double rating[USERMAX][ITEMMAX];
int I[USERMAX][ITEMMAX];//indicate if the item is rated
double UserF[USERMAX][FEATURE];
double ItemF[ITEMMAX][FEATURE];
double BIASU[USERMAX];
double BIASI[ITEMMAX];
double lamda = 0.15;
double gamma = 0.04;
double mean;
double predict(int i, int j)
{
double rate = mean + BIASU[i] + BIASI[j];
for (int f = 0; f < FEATURE; f++)
rate += UserF[i][f] * ItemF[j][f];
if (rate < 1)
rate = 1;
else if (rate>5)
rate = 5;
return rate;
}
double calRMSE()
{
int cnt = 0;
double total = 0;
for (int i = 0; i < USERMAX; i++)
{
for (int j = 0; j < ITEMMAX; j++)
{
double rate = predict(i, j);
total += I[i][j] * (rating[i][j] - rate)*(rating[i][j] - rate);
cnt += I[i][j];
}
}
double rmse = pow(total / cnt, 0.5);
return rmse;
}
double calMean()
{
double total = 0;
int cnt = 0;
for (int i = 0; i < USERMAX; i++)
for (int j = 0; j < ITEMMAX; j++)
{
total += I[i][j] * rating[i][j];
cnt += I[i][j];
}
return total / cnt;
}
void initBias()
{
memset(BIASU, 0, sizeof(BIASU));
memset(BIASI, 0, sizeof(BIASI));
mean = calMean();
for (int i = 0; i < USERMAX; i++)
{
double total = 0;
int cnt = 0;
for (int j = 0; j < ITEMMAX; j++)
{
if (I[i][j])
{
total += rating[i][j] - mean;
cnt++;
}
}
if (cnt > 0)
BIASU[i] = total / (cnt);
else
BIASU[i] = 0;
}
for (int j = 0; j < ITEMMAX; j++)
{
double total = 0;
int cnt = 0;
for (int i = 0; i < USERMAX; i++)
{
if (I[i][j])
{
total += rating[i][j] - mean;
cnt++;
}
}
if (cnt > 0)
BIASI[j] = total / (cnt);
else
BIASI[j] = 0;
}
}
void train()
{
//read rating matrix
memset(rating, 0, sizeof(rating));
memset(I, 0, sizeof(I));
ifstream in("ua.base");
if (!in)
{
cout << "file not exist" << endl;
exit(1);
}
int userId, itemId, rate;
string timeStamp;
while (in >> userId >> itemId >> rate >> timeStamp)
{
rating[userId][itemId] = rate;
I[userId][itemId] = 1;
}
initBias();
//train matrix decomposation
for (int i = 0; i < USERMAX; i++)
for (int f = 0; f < FEATURE; f++)
UserF[i][f] = (rand() % 10)/10.0 ;
for (int j = 0; j < ITEMMAX; j++)
for (int f = 0; f < FEATURE; f++)
ItemF[j][f] = (rand() % 10)/10.0 ;
int iterCnt = 0;
while (iterCnt < ITERMAX)
{
for (int i = 0; i < USERMAX; i++)
{
for (int j = 0; j < ITEMMAX; j++)
{
if (I[i][j])
{
double predictRate = predict(i, j);
double eui = rating[i][j] - predictRate;
BIASU[i] += gamma*(eui - lamda*BIASU[i]);
BIASI[j] += gamma*(eui - lamda*BIASI[j]);
for (int f = 0; f < FEATURE; f++)
{
UserF[i][f] += gamma*(eui*ItemF[j][f] - lamda*UserF[i][f]);
ItemF[j][f] += gamma*(eui*UserF[i][f] - lamda*ItemF[j][f]);
}
}
}
}
double rmse = calRMSE();
cout << "Loop " << iterCnt << " : rmse is " << rmse << endl;
iterCnt++;
}
}
void test()
{
ifstream in("ua.test");
if (!in)
{
cout << "file not exist" << endl;
exit(1);
}
int userId, itemId, rate;
string timeStamp;
double total = 0;
double cnt = 0;
while (in >> userId >> itemId >> rate >> timeStamp)
{
double r = predict(userId, itemId);
total += (r - rate)*(r - rate);
cnt += 1;
}
cout << "test rmse is " << pow(total / cnt, 0.5) << endl;
}
int main()
{
train();
test();
return 0;
}
可以看到,rmse能非常快的收敛,训练数据中的rmse能很快收敛到0.8左右,然后拿测试集的数据去测试,rmse为0.949,也是蛮不错的预测结果了,当然这里可以调各种参数来获得更优的实验结果。。。就是所谓的黑科技?。。。:)
原文地址:http://blog.csdn.net/wangyuquanliuli/article/details/43850931