目前推荐系统中用的最多的就是矩阵分解方法,在Netflix Prize推荐系统大赛中取得突出效果。以用户-项目评分矩阵为例,矩阵分解就是预测出评分矩阵中的缺失值,然后根据预测值以某种方式向用户推荐。常见的矩阵分解方法有基本矩阵分解(basic MF),正则化矩阵分解)(Regularized MF),基于概率的矩阵分解(PMF)等。今天以“用户-项目评分矩阵R(N×M)”说明三种分解方式的原理以及应用。
用户-项目评分矩阵
Basic MF:
Basic MF是最基础的分解方式,将评分矩阵R分解为用户矩阵U和项目矩阵S, 通过不断的迭代训练使得U和S的乘积越来越接近真实矩阵,矩阵分解过程如图:
矩阵分解过程
预测值接近真实值就是使其差最小,这是我们的目标函数,然后采用梯度下降的方式迭代计算U和S,它们收敛时就是分解出来的矩阵。我们用损失函数来表示误差(等价于目标函数):
损失函数 公式1
公式1中R_ij是评分矩阵中已打分的值,U_i和S_j相当于未知变量。为求得公式1的最小值,相当于求关于U和S二元函数的最小值(极小值或许更贴切)。通常采用梯度下降的方法:
梯度下降
学习速率是学习速率,表示迭代的步长。其值为1.5时,通常以震荡形式接近极值点;若<1迭代单调趋向极值点;若>2围绕极值逐渐发散,不会收敛到极值点。具体取什么值要根据实验经验。
Regularized MF
正则化矩阵分解是Basic MF的优化,解决MF造成的过拟合问题。其不是直接最小化损失函数,而是在损失函数基础上增加规范化因子,将整体作为损失函数。
红线表示正则化因子,在求解U和S时,仍然采用梯度下降法,此时迭代公式变为:(图片截取自相关论文,S和V等价)
梯度下降
梯度下降结束条件:f(x)的真实值和预测值小于自己设定的阈值(很小的值,之前一直理解为是变量U和V的迭代值差小于阈值就行)
Java代码实现矩阵分解
public class Test {
private static int N=5;//用户的数目
private static int M=4;//产品的数目
private static int K=2;//特征的数目
private static DecimalFormat df=new DecimalFormat("###.000");
public static void main(String[] args) {
double[] R=new double[N*M];
double[] P=new double[N*K];
double[] Q=new double[N*M];
R[0]=5;
R[1]=3;
R[2]=0;
R[3]=1;
R[4]=4;
R[5]=0;
R[6]=0;
R[7]=1;
R[8]=1;
R[9]=1;
R[10]=0;
R[11]=5;
R[12]=1;
R[13]=0;
R[14]=0;
R[15]=4;
R[16]=0;
R[17]=1;
R[18]=5;
R[19]=4;
System.out.println("R矩阵");
for(int i=0;i<N;++i) {
for(int j=0;j<M;++j){
System.out.print(R[i*M+j]+",");
}
System.out.println();
}
//初始化P,Q矩阵,这里简化了,通常也可以对服从正态分布的数据进行随机数生成
for(int i=0;i<N;++i)
{
for(int j=0;j<K;++j)
{
P[i*K+j]=Math.random()%9;
}
}
for(int i=0;i<K;++i)
{
for(int j=0;j<M;++j)
{
Q[i*M+j]=Math.random()%9;
}
}
System.out.println("矩阵分解开始");
matrix_factorization(R,P,Q,N,M,K);
System.out.println("重构出来的R矩阵");
for(int i=0;i<N;++i)
{
for(int j=0;j<M;++j)
{
double temp=0;
for (int k=0;k<K;++k){
temp+=P[i*K+k]*Q[k*M+j];
}
System.out.print(df.format(temp)+",");
}
System.out.println();
}
}
public static void matrix_factorization(double[] R,double[] P,double[] Q,int N,int M,int K){
int steps=5000;
double alpha=0.0002;
double beta=0.02;
for(int step =0;step<steps;++step){
for(int i=0;i<N;++i) {
for(int j=0;j<M;++j){
if(R[i*M+j]>0){
double eij = R[i*M+j];
for(int k=0;k<K;++k){
eij -= P[i*K+k]*Q[k*M+j];
}
for(int k=0;k<K;++k){
P[i*K+k] +=alpha * (2 * eij * Q[k*M+j] - beta * P[i*K+k]);
Q[k*M+j] +=alpha * (2 * eij * P[i*K+k] - beta * Q[k*M+j]);
}
}
}
}
double loss=0;
for(int i=0;i<N;++i){
for(int j=0;j<M;++j) {
if(R[i*M+j]>0){
double eij =0;
for(int k=0;k<K;++k){
eij += P[i*K+k]*Q[k*M+j];
}
loss += Math.pow(R[i*M+j]-eij,2);
for(int k=0;k<K;++k){
loss += (beta/2) * (Math.pow(P[i*K+k],2) + Math.pow(Q[k*M+j],2));
}
}
}
}
if(loss<0.001){
break;
}
if (step%1000==0){
System.out.println("loss:"+loss);
}
}
}
}