矩阵分解(java)
时间:2022/6/20
1.奇异值分解
对于任意实矩阵 A ∈ R m ∗ n A\in R^{m*n} A∈Rm∗n都可以分解为
A = U ∑ V T (1) A=U\sum V^T\tag{1} A=U∑VT(1)
其中, U ∈ R m ∗ m U\in R^{m*m} U∈Rm∗m是满足 U T U = I U^TU=I UTU=I的m阶酋矩阵; V ∈ R n ∗ n V\in R^{n*n} V∈Rn∗n是满足 V T V = I V^TV=I VTV=I的n阶酋矩阵; ∑ ∈ R m ∗ n \sum\in R^{m*n} ∑∈Rm∗n是mxn的的矩阵,其中 ( ∑ ) i i = σ i (\sum)_{ii}=\sigma_i (∑)ii=σi且其他位置的元素均为0, σ i 为 非 负 实 数 且 σ 1 ≥ σ 2 ≥ . . . ≥ 0 \sigma_i为非负实数且\sigma_1\ge \sigma_2\ge ...\ge0 σi为非负实数且σ1≥σ2≥...≥0.
(1)式的分解便是奇异值分解(singular Value Decomposition,SVD),矩阵U的列向量称为A的左奇异向量,矩阵V的列向量,被称为A的右奇异向量, σ i \sigma_i σi则称为奇异值(singular value).矩阵A的秩r就等于非零奇异值的个数。
2.矩阵分解——Funk-SVD
由矩阵乘法可得,对于一个矩阵A,可分解成两个子矩阵的乘积,即
A m ∗ n = P m ∗ k ∗ Q k ∗ n (2) A_{m*n}=P_{m*k}*Q_{k*n}\tag{2} Am∗n=Pm∗k∗Qk∗n(2)
通过这种方法可以将一个高阶矩阵分解成低阶矩阵。这种方法和奇异值分解很类似,常用于推荐系统中。通过将原始数据进行分解得到两个子矩阵,使用子矩阵的值不断拟合已知数据,达到一定目标之后,便可以用子矩阵的乘积去预测原始数据矩阵中的未知项。以此来达到推荐的目的。
这种方法是借鉴的奇异值分解的方法。可以说是伪奇异值分解。
为什么不使用奇异值分解呢?原因是奇异值分解主要适用于稠密矩阵,不适用于稀疏矩阵。同时奇异值分解也不能处理缺失值。所以我们选择改进的FUNK—SVD.
对于分解后的两个子矩阵,便是两个隐空间。隐空间的维度k是需要人工调整的,不断调整,直至达到满意的结果。对于k值和隐空间维度的一种解释便是,以观众对电影的评分为例,k值便是影响电影评分的相关参数,包括导演水平,演员演技,电影剧情等等。矩阵P则为用户矩阵,P的每一行对应于一个科幻对于电影相关参数的需求。而矩阵Q则是电影矩阵,Q的每一列对应于一个电影对相关参数的满足程度。 P ∗ Q P*Q P∗Q便是该用户对该电影的评分。当然,这种分析看起来很合理,但并不是所有的情况都能这样解释。因为隐空间的维度其实很难进行合理的解释,算法的可解释性是很差的。
这样我们就很容易得到算法的具体流程:
-
首先使用随机数按照给定的k值,构造两个子矩阵。
-
使用子矩阵通过式(2)对已知数据进行拟合。
-
计算预测误差,调整子矩阵。
-
重复(2)(3)两个步骤,使得结果满足一定条件结束。例如迭代多少轮或者误差小于某个给定的阈值。
这里有个问题,如何使得调整之后的矩阵快速到达我们满意的结果。这里可以采用梯度下降法。
3.梯度下降与损失函数
3.1.损失函数
对于FUNK-SVD算法,用户评分矩阵 R m ∗ n R_{m*n} Rm∗n被分解成 P m ∗ k , Q k ∗ n P_{m*k},Q_{k*n} Pm∗k,Qk∗n两个矩阵,则相应的预测评分则为:
r ^ i j = p i ∗ q j T (3) \hat r_{ij}=p_i*q^T_j\tag{3} r^ij=pi∗qjT(3)
则对于的平方损失函数则为:
f ( u , i ) = Δ r 2 = ( r u , i − r ^ u , i ) 2 (4) f(u,i)=\Delta r^2=(r_{u,i}-\hat r_{u,i})^2\tag{4} f(u,i)=Δr2=(ru,i−r^u,i)2(4)
对于数据集S而言,最终的优化目标便是:
min p u , q i ∑ ( u , i ) ∈ S f ( u , i ) (5) \min_{p_u,q_i}\sum_{(u,i)\in S}f(u,i)\tag{5} pu,qimin(u,i)∈S∑f(u,i)(5)
使得所有项的损失最小。
3.2.梯度下降
如何使得算法快速迭代至我们满意的情况呢?这里便要使用梯度下降法。
梯度下降法(gradient descent)是一种常用的一阶优化方法,是求解无约束优化问题最简单、最经典的方法之一。
对于无约束优化问题 min x f ( x ) \min_xf(x) minxf(x),其中 f ( x ) f(x) f(x)是连续可微函数。欲构造一个数列 x 0 , x 1 , x 2 , . . . x_0,x_1,x_2,... x0,x1,x2,...满足:
f ( x t + 1 ) < f ( x t ) , t = 0 , 1 , 2 , . . . (6) f(x_{t+1})<f(x_{t}),t=0,1,2,...\tag{6} f(xt+1)<f(xt),t=0,1,2,...(6)
使得函数不断递减,直至趋于极小点,欲满足上式,则根据泰勒展开式有:
f ( x + Δ x ) ≈ f ( x ) + Δ x T ∇ f ( x ) (7) f(x+\Delta x)\approx f(x)+\Delta x^T \nabla f(x)\tag{7} f(x+Δx)≈f(x)+ΔxT∇f(x)(7)
故欲使得 f ( x + Δ x ) < f ( x ) f(x+\Delta x)<f(x) f(x+Δx)<f(x)可选择:
Δ x = − γ ∇ f ( x ) (8) \Delta x=-\gamma \nabla f(x)\tag{8} Δx=−γ∇f(x)(8)
其中 γ \gamma γ是步长,由人为调整参数。通过选取合适的步长,可以快速的达到函数的极小点。
对于本次实验,对损失函数(4)按上式进行计算偏导数,可以得到:
∂
Δ
r
2
∂
p
⃗
u
=
−
2
Δ
r
∗
q
⃗
i
(9)
{{\partial \Delta r^2 }\over{\partial \vec p_u}}=-2\Delta r* \vec q_i \tag{9}
∂pu∂Δr2=−2Δr∗qi(9)
∂
Δ
r
2
∂
q
⃗
i
=
−
2
Δ
r
∗
p
⃗
u
(10)
{{\partial \Delta r^2 }\over{\partial \vec q_i}}=-2\Delta r* \vec p_u \tag{10}
∂qi∂Δr2=−2Δr∗pu(10)
故:
Δ x p = γ ∗ 2 ∗ Δ r ∗ q ⃗ i Δ x q = γ ∗ 2 ∗ Δ r ∗ p ⃗ u (11) \Delta x_p=\gamma*2*\Delta r*\vec q_i \\ \Delta x_q=\gamma*2*\Delta r*\vec p_u \tag{11} Δxp=γ∗2∗Δr∗qiΔxq=γ∗2∗Δr∗pu(11)
据此式可以调整分解后的两个矩阵 P , Q P,Q P,Q.
4.算法实现
/**
* MyMatrixFactorization.java
*
* @author zjy
* @date 2022/6/19
* @Description: 矩阵分解
* @version V1.0
*/
package swpu.zjy.ML.MartixFactorization;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.Random;
public class MyMatrixFactorization {
//数据集,按照user-item-rating存储
Triple[] dataset;
//用户数量
int numUser;
//电影数量
int numItem;
//评分数量
int numRating;
//随机数生成器
Random random=new Random();
//控制学习规则的参数
double alpha;
//用于控制学习速率的参数
double lambda;
//隐空间维度k
int rank;
//用户子空间
double[][] userSubspace;
//电影项目子空间
double[][] itemSubspace;
//预测评分最低边界
double ratingLowerBound;
//预测评分最高边界
double ratingUpperBound;
/**
* 三元组类,存储数据元组。user-item-rating
*/
public class Triple{
int user;
int item;
double rating;
public Triple(int user,int item,double rating){
this.user=user;
this.item=item;
this.rating=rating;
}
@Override
public String toString() {
return "Triple{" +
"user=" + user +
", item=" + item +
", rating=" + rating +
'}';
}
}
/**
* 构造方法,初始化相应参数,读入数据集。
* @param datasetFileName 数据集地址
* @param numUser 用户数量
* @param numItem 电影数量
* @param numRating 评分数量
* @param ratingLowerBound 最低评分
* @param ratingUpperBound 最高评分
*/
public MyMatrixFactorization(String datasetFileName,int numUser,
int numItem,int numRating,double ratingLowerBound,double ratingUpperBound){
//初始化
this.numItem=numItem;
this.numUser=numUser;
this.numRating=numRating;
this.ratingLowerBound=ratingLowerBound;
this.ratingUpperBound=ratingUpperBound;
//读入数据集
try {
FileReader fileReader=new FileReader(datasetFileName);
BufferedReader bufferedReader=new BufferedReader(fileReader);
dataset=new Triple[numRating];
String tempString;
String[] tempData;
for (int i = 0; i < numRating; i++) {
tempString=bufferedReader.readLine();
tempData=tempString.split(",");
dataset[i]=new Triple(Integer.parseInt(tempData[0]),Integer.parseInt(tempData[1]),
Double.parseDouble(tempData[2]));
}
bufferedReader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置参数
* @param paraRank 隐空间维度k
* @param paraAlpha 梯度下降参数
* @param paraLambda
*/
public void setParameters(int paraRank, double paraAlpha, double paraLambda) {
rank = paraRank;
alpha = paraAlpha;
lambda = paraLambda;
}
/**
* 初始化用户子空间和项目子空间,使用随机数填入
*/
public void initalizeSubspace(){
//初始化两个子空间
userSubspace=new double[numUser][rank];
itemSubspace=new double[numItem][rank];
//用随机数填入
for (int i = 0; i < numUser; i++) {
for (int j = 0; j < rank; j++) {
userSubspace[i][j]=random.nextDouble();
}
}
for (int i = 0; i < numItem; i++) {
for (int j = 0; j < rank; j++) {
itemSubspace[i][j]=random.nextDouble();
}
}
}
/**
* 使用子空间进行预测用户对项目的评分
* @param userId 用户ID
* @param itemId 项目ID
* @return 预测评分
*/
public double predict(int userId,int itemId){
double result=0;
//两个矩阵相乘
for (int i = 0; i < rank; i++) {
result+=userSubspace[userId][i]*itemSubspace[itemId][i];
}
return result;
}
/**
* 按照梯度下降调整子空间
*/
public void updateNoRegular(){
for (int i = 0; i < numRating; i++) {
int tempUserId=dataset[i].user;
int tempItemId=dataset[i].item;
double tempRating=dataset[i].rating;
//计算误差
double tempError=tempRating-predict(tempUserId,tempItemId);
//更新矩阵
//使用梯度下降法
double tempValue=0;
for (int j = 0; j < rank; j++) {
tempValue=2*tempError*itemSubspace[tempItemId][j];
userSubspace[tempUserId][j]+=tempValue*alpha;
}
for (int j = 0; j < rank; j++) {
tempValue=2*tempError*userSubspace[tempUserId][j];
itemSubspace[tempItemId][j]+=tempValue*alpha;
}
}
}
/**
* 使用已有数据集进行训练
* @param paraRounds 训练次数
*/
public void train(int paraRounds){
initalizeSubspace();
for (int i = 0; i < paraRounds; i++) {
updateNoRegular();
//输出MAE
if(i%50==0){
System.out.println("Round="+i);
System.out.println("MAE="+MAE());
}
}
}
/**
* 计算均方根误差
* @return 均方根误差
*/
private double RSME(){
double resultRSME=0;
int tempTestCount=0;
for (int i = 0; i < numRating; i++) {
int tempUserId=dataset[i].user;
int tempItemId=dataset[i].item;
double tempRating=dataset[i].rating;
double tempPredict=predict(tempUserId,tempItemId);
//防止评分越界
if(tempPredict<ratingLowerBound){
tempPredict=ratingLowerBound;
}else if(tempPredict>ratingUpperBound){
tempPredict=ratingUpperBound;
}
double tempError=tempRating-tempPredict;
resultRSME+=tempError*tempError;
tempTestCount++;
}
return Math.sqrt(resultRSME/tempTestCount);
}
/**
* 平均绝对误差
* @return MAE
*/
private double MAE() {
double resultMAE=0;
int tempTestCount=0;
for (int i = 0; i < numRating; i++) {
int tempUserId=dataset[i].user;
int tempItemId=dataset[i].item;
double tempRating=dataset[i].rating;
double tempPredict=predict(tempUserId,tempItemId);
if(tempPredict<ratingLowerBound){
tempPredict=ratingLowerBound;
}else if(tempPredict>ratingUpperBound){
tempPredict=ratingUpperBound;
}
double tempError=tempRating-tempPredict;
resultMAE+=Math.abs(tempError);
tempTestCount++;
}
return resultMAE/tempTestCount;
}
/**
* 测试算法
* @param args
*/
public static void main(String[] args) {
//计算开始时间
long startTime = System.currentTimeMillis();
try {
// Step 1. 读取数据集
MyMatrixFactorization mf=new MyMatrixFactorization(
"E:\\DataSet\\movielens-943u1682m.txt",
943, 1682, 10000,
1, 5);
//step 2. 设置参数
mf.setParameters(5, 0.0001, 0.005);
// Step 3. 进行训练
System.out.println("Begin Training ! ! !");
mf.train(2000);
//输出相关误差
double tempMAE = mf.MAE();
double tempRSME = mf.RSME();
System.out.println("Finally, MAE = " + tempMAE + ", RSME = " + tempRSME);
//计算运行时间
long endTime = System.currentTimeMillis();
System.out.println("运行时间:"+(endTime-startTime));
} catch (Exception e) {
e.printStackTrace();
} // Of try
}
}
5.运行结果
运行2000轮,只用了662ms,java的速度还是很快的。