推荐系统之隐含语义模型LFM--Java代码

Posted 老程序员学习笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了推荐系统之隐含语义模型LFM--Java代码相关的知识,希望对你有一定的参考价值。

前面两篇文章提到(、),我们可以获取用户-物品(User-Item)偏好度矩阵,而根据计算用户u对物品i偏好度的公式:

可知,我们还缺一个关键的K——隐因子。只有知道了K,我们才能将User-Item这个u*i的矩阵分解成Q(u,K)、P(i,K)两个矩阵。

先从矩阵分解说起,常用的奇异值分解(SVD)。矩阵R分解为:

推荐系统之隐含语义模型LFM(三)--Java代码

K一般远远小于u、i的数量。

所以,如果我们预测第i个用户对第j个物品的偏好度,计算

推荐系统之隐含语义模型LFM(三)--Java代码

即可。

但是SVD也有其局限性:

  1. SVD要求被分解的矩阵是稠密的。但从实际生活角度看,无论是购物、听音乐、看电影等,User-Item都是稀疏的。以我个人为例,2019年国内上映的电影共89部,而我也只看了16部,还不到五分之一。如果再扩大到全球历年上映的影片,我个人对这些影片偏好度的矩阵就极其稀疏了。另外,如果将这个稀疏的矩阵通过SVD分解降维,就要进行数据填充,使得这个矩阵的空间需求过大,算法复杂度大增。并且简单粗暴地填充数据也会导致数据失真。

  2. SVD时间复杂度较高,m*n的矩阵,时间复杂度大概就是n的3次方,在用户数、物品数极大的情况下进行分解,代价太大。

所以这才有了下面要说的优化变种算法Funk-SVD。后面还出现了Basic-SVD、SVD++等,都是基于此算法的。


Funk-SVD算法的基本思路就是,将稀疏的User-Item矩阵R,分解成用户-隐因子矩阵Q,物品-隐因子矩阵P,则有:

推荐系统之隐含语义模型LFM(三)--Java代码

那么用户u对物品i的偏好度的预测值:

推荐系统之隐含语义模型LFM(三)--Java代码

拿用户u对物品i的真实偏好度与预测值相比较,差值越小,则说明预测越准。这里使用和方差SSE作为评测指标,则有:

推荐系统之隐含语义模型LFM(三)--Java代码

SSE计算的是真实值与预测值误差的平方和,越趋近于0,说明预测的越准,拟合度越高。

为了防止过拟合,加入L2的正则化项,可得:

推荐系统之隐含语义模型LFM(三)--Java代码

其中u、i都是训练集中用户对物品有偏好的数据。

  • r(u,i)是用户u对物品i的真实偏好度。

  • λ是正则化系数

  • e(u,i)是真实值与预测值的误差。

通过随机梯度下降法不断地去学习,使学习出来的Q、P计算得到的预测矩阵逼近真实矩阵R。

首先要求Q(u,k)和P(i,k)的偏导数:

根据随机梯度下降法的要求,我们要按负梯度方向前进,递推公式如下:

  • α是学习速率。

  • λ是正则化参数。

Java代码如下:

import java.io.BufferedReader;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStreamReader;import java.util.Collections;import java.util.Comparator;import java.util.List;import java.util.Map;import java.util.Map.Entry;
import com.google.common.collect.Lists;import com.google.common.collect.Maps;
public class LFM {
private double α = 0.02;// 梯度下降学习速率,步长 private static final int F = 100;// 隐因子数 private static final double λ = 0.01;// 正则化参数,防止过拟合 private static final int iterations = 100;// 迭代求解次数
private Map<Integer, Double[]> userF = Maps.newHashMap();// 用户-隐因子矩阵 private Map<Integer, Double[]> itemF = Maps.newHashMap();// 物品-隐因子矩阵
public static void main(String[] args) { long start = System.currentTimeMillis(); LFM lfm = new LFM(); Map<Integer, Map<Integer, Double>> userItemRealData = lfm.buildInitData(); lfm.initUserItemFactorMatrix(userItemRealData); lfm.train(userItemRealData); lfm.getRecommondItemByUserId(1, 10, userItemRealData); // lfm.getRecommondItemByUserId(2, 10, userItemRealData); // lfm.getRecommondItemByUserId(3, 10, userItemRealData); long end = System.currentTimeMillis(); System.out.println("耗时" + (end - start) / 1000 + "秒"); }
/** * 构建初始数据<br/> * 实际开发中,应该读取日志,格式类似:userId,itemId,评分<br/> * 本例采用随机数 * * @return */ public Map<Integer, Map<Integer, Double>> buildInitData() { Map<Integer, Map<Integer, Double>> userItemRealData = Maps.newHashMap(); System.out.println("读取数据......"); String dataPath = "数据文件/ratings.csv"; BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(new FileInputStream(dataPath), "utf-8")); String line = null; Map<Integer, Double> itemRating = null; while ((line = br.readLine()) != null) { String[] dataRating = line.split(","); Integer userID = Integer.valueOf(dataRating[0].trim()); Integer itemID = Integer.valueOf(dataRating[1].trim()); double rating = Double.parseDouble(dataRating[2]);
if (userItemRealData.containsKey(userID)) { itemRating = userItemRealData.get(userID); itemRating.put(itemID, rating); } else { itemRating = Maps.newHashMap(); itemRating.put(itemID, rating); userItemRealData.put(userID, itemRating); } } br.close(); } catch (IOException e) { e.printStackTrace(); System.exit(1); } return userItemRealData; }
/** * 初始化用户-隐因子、物品-隐因子矩阵<br/> * 使用随机数进行填充,随机数应与1/sqrt(隐因子数量)成正比 * * @param userItemRealData */ public void initUserItemFactorMatrix(Map<Integer, Map<Integer, Double>> userItemRealData) { for (Entry<Integer, Map<Integer, Double>> userEntry : userItemRealData.entrySet()) { // 随机填充用户-隐因子矩阵 int userId = userEntry.getKey(); Double[] randomUserValue = new Double[LFM.F]; for (int j = 0; j < LFM.F; j++) { randomUserValue[j] = Math.random() / Math.sqrt(LFM.F); } this.getUserF().put(userId, randomUserValue);
// 随机填充物品-隐因子矩阵 Map<Integer, Double> itemMap = userItemRealData.get(userId); for (Entry<Integer, Double> entry : itemMap.entrySet()) { int itemId = entry.getKey(); if (this.getItemF().containsKey(itemId)) { continue;// 物品-隐因子矩阵已存在,不再做处理 } Double[] randomItemValue = new Double[LFM.F]; for (int j = 0; j < LFM.F; j++) { randomItemValue[j] = Math.random() / Math.sqrt(LFM.F); } this.getItemF().put(itemId, randomItemValue); } } }
/** * 训练 * @param userItemRealData */ public void train(Map<Integer, Map<Integer, Double>> userItemRealData) { for (int step = 0; step < LFM.iterations; step++) { System.out.println("第" + (step + 1) + "次迭代"); for (Entry<Integer, Map<Integer, Double>> userEntry : userItemRealData.entrySet()) { int userId = userEntry.getKey(); Map<Integer, Double> itemMap = userItemRealData.get(userId); for (Entry<Integer, Double> entry : itemMap.entrySet()) { int itemId = entry.getKey();// 物品ID double realRating = entry.getValue();// 真实偏好度 double predictRating = this.predict(userId, itemId);// 预测偏好度 double error = realRating - predictRating;// 偏好度误差 Double[] userVal = this.getUserF().get(userId); Double[] itemVal = this.getItemF().get(itemId);
for (int j = 0; j < LFM.F; j++) { double uv = userVal[j]; double iv = itemVal[j];
uv += this.α * (error * iv - LFM.λ * uv); iv += this.α * (error * uv - LFM.λ * iv);
userVal[j] = uv; itemVal[j] = iv; } } } this.α *= 0.9;// 按照随机梯度下降算法的要求,学习速率每步都要进行衰减,目的是使算法尽快收敛 } }
/** * 获取用户对物品的预测评分 * * @param userId * @param itemId * @return */ public double predict(Integer userId, Integer itemId) { double predictRating = 0.0;// 预测评分 Double[] userValue = this.getUserF().get(userId); Double[] itemValue = this.getItemF().get(itemId); for (int i = 0; i < LFM.F; i++) { predictRating += userValue[i] * itemValue[i]; } return predictRating; }
/** * 获取用户TopN推荐 * @param userId * @param count * @param userItemRealData * @return */ public List<Map.Entry<Integer, Double>> getRecommondItemByUserId(int userId, int count, Map<Integer, Map<Integer, Double>> userItemRealData) { Map<Integer, Double> result = Maps.newHashMap(); Map<Integer, Double> realItemVal = userItemRealData.get(userId); Map<Integer, Double[]> predictItemVal = this.getItemF(); // double lowestVal = Double.NEGATIVE_INFINITY;// 最小偏好度,初始为负无穷大 for (Integer itemId : predictItemVal.keySet()) { if (realItemVal.containsKey(itemId)) { continue;// 预测偏好度的物品在真实偏好度物品中,不处理 } double predictRating = this.predict(userId, itemId);// 预测值 if (predictRating < 0) { continue; } result.put(itemId, predictRating); }
List<Map.Entry<Integer, Double>> list = Lists.newArrayList(result.entrySet()); Collections.sort(list, new Comparator<Map.Entry<Integer, Double>>() { // 降序 public int compare(Map.Entry<Integer, Double> o1, Map.Entry<Integer, Double> o2) { if (o2.getValue() > o1.getValue()) { return 1; } else if (o2.getValue() < o1.getValue()) { return -1; } else { return 0; } } }); List<Map.Entry<Integer, Double>> topN = list.subList(0, count); System.out.println("用户" + userId + "前" + count + "个推荐结果:"); for (Entry<Integer, Double> entry : topN) { System.out.println(entry.getKey() + ":" + entry.getValue()); } return topN; }
//////////////////////////////////////////////////////////// public Map<Integer, Double[]> getUserF() { return userF; }
public void setUserF(Map<Integer, Double[]> userF) { this.userF = userF; }
public Map<Integer, Double[]> getItemF() { return itemF; }
public void setItemF(Map<Integer, Double[]> itemF) { this.itemF = itemF; }
}

有两点要说一下:

  1. 训练集采用的是MovieLens的数据,

    我选的是最小的数据集ml-latest-small.zip。

    http://files.grouplens.org/datasets/movielens/ml-latest-small.zip。

    其中的ratings.csv就是用户对电影的评分数据。

  2. LFM其实更适合预测用户对物品的评分(偏好度),当然了,评分越高自然越推荐了。在getRecommondItemByUserId()方法中,取出TopN推荐,我使用的方法其实不怎么优化,完全可以使用优先队列来处理。


以上是关于推荐系统之隐含语义模型LFM--Java代码的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学推荐系统二:隐语义模型

推荐系统--隐语义模型LFM

隐语义模型LFM(latent factor model)

推荐算法之隐语义模型(LFM)矩阵分解梯度下降算法实现

推荐算法之隐语义模型(LFM)矩阵分解梯度下降算法实现

电影推荐系统(037~039)