协同过滤推荐之基于模型协同过滤

几种推荐系统图:
在这里插入图片描述

(1)基于模型协同过滤的核心思想

概述:

基于模型的协同过滤推荐就是基于样本的用户喜好信息,训练一个推荐模型,然后根据实时的用户喜好的信息进行预测,计算推荐。

基于模型的推荐算法,是与基于近邻的推荐算法相对的。基于近邻的推荐算法,主要是将所有的用户数据,读入内存,进行运算,当数据量特别大时,显然这种方法是不靠谱的。因此出现了基于模型的推荐算法,依托于一些机器学习的模型,通过离线进行训练,在线进行推荐。

基于模型推荐系统的优势:

  1. 节省空间:一般情况下,学习得到的模型大小远小于原始的评分矩阵,所以空间需求通常较低。
  2. 训练和预测速度快:基于近邻的方法的一个问题在于预处理环节需要用户数或物品数的平方级别时间,而基于模型的系统在建立训练模型的预处理环节需要的时间往往要少得多。在大多数情况下,压缩和总结模型可以被用来加快预测

算法分类:
基于模型的协同过滤作为目前最主流的协同过滤类型,当只有部分用户和部分物品之间是有评分数据的,其它部分评分是空白,此时我们要用已有的部分稀疏数据来预测那些空白的用户和物品之间的评分关系,找到最高评分的物品推荐给用户。

基于模型协同过滤的方法:

  • 关联算法
  • 聚类算法
  • 分类算法
  • 回归算法
  • 矩阵算法
  • 神经网络、图模型以及隐语义模型来解决

(2)矩阵分解详解

基本思想:
矩阵分解,直观上来说就是把原来的大矩阵,近似分解成两个小矩阵的乘积,在实际推荐计算时不再使用大矩阵,而是使用分解得到的两个小矩阵。按照矩阵分解的原理,我们会发现原来m x n的大矩阵会分解成m x k 和k x n的两个小矩阵,这里多出来一个k维向量,就是隐因子向量(Latent Factor Vector),类似的表达还有隐因子、隐向量、隐含特征、隐语义、隐变量等。

在这里插入图片描述
基于矩阵分解的推荐算法的核心假设是用隐语义(隐变量)来表达用户和物品,他们的乘积关系就成为了原始的元素。这种假设之所以成立,是因为我们认为实际的交互数据是由一系列的隐变量的影响下产生的,这些隐变量代表了用户和物品一部分共有的特征,在物品身上表现为属性特征,在用户身上表现为偏好特征,只不过这些因子并不具有实际意义,也不一定具有非常好的可解释性,每一个维度也没有确定的标签名字,所以才会叫做“隐变量”。

而矩阵分解后得到的两个包含隐变量的小矩阵,一个代表用户的隐含特征,一个代表物品的隐含特征,矩阵的元素值代表着相应用户或物品对各项隐因子的符合程度,有正面的也有负面的

根据如下的计算公式,我们来举例:
在这里插入图片描述

矩阵的转置:
在这里插入图片描述

(3)矩阵分解图例及数据演化过程

矩阵分解过程举例:

使用之前的用户一电影评分表,5个用户(U表示),6个电影(M表示)。现在假设电影的风格有以下几类:喜剧,动作,恐怖。分别用K1、K2、K3来表示。那么我们希望得到用户对于风格偏好的矩阵U,以及每个风格在电影中所占比重的矩阵M

在这里插入图片描述
通常情况下,隐因子数量k的选取要远远低于用户和电影的数量,大矩阵分解成两个小矩阵实际上是用户和电影在k维隐因子空间上的映射,这个方法其实是也是一种“降维”(Dimension Reduction)过程。

矩阵分解目标:

我们再从机器学习的角度来了解矩阵分解,我们已经知道电影评分预测实际上是一个矩阵补全的过程,在矩阵分解的时候原来的大矩阵必然是稀疏的,即有一部分有评分,有一部分是没有评过分的,不然也就没必要预测和推荐了。

所以整个预测模型的最终目的是得到两个小矩阵,通过这两个小矩阵的乘积来补全大矩阵中没有评分的位置。所以对于机器学习模型来说,问题转化成了如何获得两个最优的小矩阵。因为大矩阵有一部分是有评分的,那么只要保证大矩阵有评分的位置(实际值)与两个小矩阵相乘得到的相应位置的评分(预测值)之间的误差最小即可,其实就是一个均方误差损失,这便是模型的目标函数。

矩阵分解的优势:

  1. 比较容易编程实现,随机梯度下降方法依次迭代即可训练出模型。比较低的时间和空间复杂度,高维矩阵映射为两个低维矩阵节省了存储空间,训练过程比较费时,但是可以离线完成;评分预测一般在线计算,直接使用离线训练得到的参数,可以实时推荐。
  2. 预测的精度比较高,预测准确率要高于基于领域的协同过滤以及内容过滤等方法。

矩阵分解的缺点:

  1. 模型训练比较费时。
  2. 推荐结果不具有很好的可解释性,分解出来的用户和物品矩阵的每个维度无法和现实生活中的概念来解释,无法用现实概念给每个维度命名,只能理解为潜在语义空间。

矩阵分解的作用:

  1. 矩阵填充(通过矩阵分解来填充原有矩阵,例如协同过滤的ALS算法就是填充原有矩阵)
  2. 清理异常值与离群点
  3. 降维、压缩
  4. 个性化推荐
  5. 间接的特征组合(计算特征间相似度)

(4)SVD算法之交替最小二乘(ALS)详解

前面我们已经提到类似与下面公式的计算过程就是矩阵分解,还有一个更常见的名字叫做SVD;但是,SVD 和矩阵分解不能划等号,因为除了SVD还有一些别的矩阵分解方法。

SVD全称奇异值分解,属于线性代数的知识;然而在推荐算法中实际上使用的并不是正统的奇异值分解,而是一个伪奇异值分解。SVD是矩阵分解、降维、压缩、特征学习的一个基础的工具,所以SVD在机器学习领域相当的重要

在这里插入图片描述
前面已经从直观上大致说了矩阵分解是怎么回事,这里再从物理意义上解释一遍。矩阵分解,就是把用户和物品都映射到一个k维空间中,这个k维空间不是我们直接看得到的,也不一定具有非常好的可解释性,每一个维度也没有名字,所以常常叫做隐因子,代表藏在直观的矩阵数据下面的。

举个例子,用户u的向量是pu,物品i的向量是qi,那么,要计算物品i推荐给用户u的推荐分数,直接计算点积即可:

在这里插入图片描述

如何为每个用户和物品生成k维向量

这个问题可以转化成机器学习问题,要解决机器学习问题,就需要寻找损失函数以及优化算法。SVD的损失函数是这样定义的:

在这里插入图片描述
这个损失函数由两部分构成

前一部分就是: 用分解后的矩阵预测分数,要和实际的用户评分之间误差越小越好。
后一部分就是: 得到的隐因子向量要越简单越好,以控制这个模型的方差,换句话说,让它在真正执行推荐任务时发挥要稳定。

整个SVD的学习过程就是

  1. 准备好用户物品的评分矩阵,每一条评分数据看做一条训练样本;
  2. 给分解后的U矩阵和V矩阵随机初始化元素值;
  3. 用U和V计算预测后的分数;
  4. 计算预测的分数和实际的分数误差;
  5. 按照梯度下降的方向更新U和V中的元素值;
  6. 重复步骤3到5,直到达到停止条件。

交替最小二乘原理(ALS)

按照机器学习的套路,就是使用优化算法求解下面这个损失函数,重点是求出用户向量Pu和物品向量Qi,来保证损失函数的值最小。这种模式可以套在几乎所有的机器学习训练中:就是一个负责衡量模型准不准,另一个负责衡量模型稳不稳定,有了这个目标函数后,就要用到优化算法找到能使结果最小的参数。优化方法常用的选择有两个,一个是随机梯度下降(SGD),另一个是交替最小二乘(ALS)

交替最小二乘的本质是找出能使结果最小的参数,让两个矩阵P和Q相乘后约等于原矩阵R:
在这里插入图片描述
交替最小二乘过程

  1. 初始化随机矩阵Q里面的元素值;
  2. 把Q矩阵当做已知的,直接用线性代数的方法求得矩阵P;
  3. 得到了矩阵P后,把P当做已知的,故技重施,回去求解矩阵Q;
  4. 上面两个过程交替进行,一直到误差可以接受为止。

交替最小二乘优势

  • 在交替的其中一步,也就是假设已知其中一个矩阵求解另一个时,要优化的参数是很容易并行化的;
  • 在不那么稀疏的数据集合上,交替最小二乘通常比随机梯度下降要更快地得到结果

自Spark2.0之后,对ALS的支持力度非常大,包含的API非常丰富。

在这里插入图片描述

(5)基于SVD算法之交替最小二乘(ALS)完成推荐开发

package com.similarity

import org.apache.spark.ml.evaluation.RegressionEvaluator
import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.ml.recommendation.ALS
import org.apache.spark.sql.SparkSession

object ALSRecommend {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    /**
     * 1.准备源数据集
     */
    import spark.implicits._
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/ratings.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    movieRatings.show()

    /**
     * userid、movieid、rate
     * +----+----+----+
     * |user|item|rate|
     * +----+----+----+
     * |   1|   1| 7.0|
     * |   1|   2| 6.0|
     * |   1|   3| 7.0|
     * |   1|   4| 4.0|
     * |   1|   5| 5.0|
     * |   1|   6| 4.0|
     * |   2|   1| 6.0|
     * ....
     */

    // 电影数据源
    val movieData = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/movies.csv")
      .map(line => {
    
    
        val field = line.split(",")
        (field(0).toInt,field(1).toString,field(2).toString)
      }).toDF("item","movie_name","genres")

    // 查看417用户喜欢看的电影
    movieRatings.join(movieData,"item").where("user = 417").select("user","item","movie_name","genres")
      .show(false)

    /**
     * 可以看出417用户喜欢看Drama、Comedy、Thriller、Romance类型的电影
     * +----+-----+-------------------------------------------------------------+---------------------------------------------------+
     * |user|item |movie_name                                                   |genres                                             |
     * +----+-----+-------------------------------------------------------------+---------------------------------------------------+
     * |471 |1    |Toy Story (1995)                                             |Adventure|Animation|Children|Comedy|Fantasy        |
     * |471 |296  |Pulp Fiction (1994)                                          |Comedy|Crime|Drama|Thriller                        |
     * |471 |356  |Forrest Gump (1994)                                          |Comedy|Drama|Romance|War                           |
     * |471 |527  |Schindler's List (1993)                                      |Drama|War                                          |
     * |471 |2324 |Life Is Beautiful (La Vita è bella) (1997)                   |Comedy|Drama|Romance|War
     * ....
     */

    /**
     * 2.从源数据集中拆分出一部分作为训练集
     */
    val Array(traing,test) = movieRatings.randomSplit(Array(0.8,0.2))

    /**
     * 3.用训练集来训练一个模型
     */
    val als = new ALS()
      .setMaxIter(15)
      .setUserCol("user")
      .setItemCol("item")
      .setRatingCol("rate")
      .setRegParam(0.15)
      .setRank(5)

    val model = als.fit(traing)

    /**
     * 在打分数据里面,有这样的情况,就是数据集切割之后,用户对电影的评分有不完整的情况,就是测试集里面可能会出现训练集当中
     * 没有出现过的电影或者用户,这个叫做冷启动
     * ALS有两种策略,一种是标识为NAN,还有一种是丢弃掉。我们这里选择丢掉
     */
    model.setColdStartStrategy("drop")

    /**
     * 4.通过模型预测我们的测试集(源数据中的另一部分)
     */
    val prediction = model.transform(test)

    /**
     * 5.通过指标来评估模型的好坏(即预测的评分结果和测试集中的用户的评分结果的误差值大小,误差越小,模型较好,误差越大,模型越差)
     * 我们使用RMSE指标来判断模型的好坏,其实就是真是评分和预测评分的和
     * RMSE的结果越小,实际的模型的效果越小
     * 这里要数据真实值的一列和预测值的一列
     */
    val evaluator = new RegressionEvaluator()
      .setMetricName("rmse")
      .setLabelCol("rate")
      .setPredictionCol("prediction")

    val rmse = evaluator.evaluate(prediction)
    println("评估值:" + rmse)


    /**
     * 6.完成模型物品的推荐
     */

    // Returns top `numItems` items recommended for each user, for all users.(给所有的用户推荐前N个商品)
    // 为每一个用户推荐10个商品
    val forUserRecommendItem = model.recommendForAllUsers(10)
    forUserRecommendItem.show(false)

    /**
     * 以417用户为例,查看推荐的电影类型,和源数据类似
     * +----+-----------------------
     * |user|recommendations
     * +----+---------------------------------------------------------
     * |471 |[[68945, 4.925072], [3379, 4.925072], [25947, 4.8449087]...]]
     *
     * item:68945 - Action|Animation|Mystery|Sci-Fi
     * item:3379 - Drama
     * item:25947 - Comedy
     * item:141718 - Comedy|Horror
     * item:60943 - Drama
     * item:7841 - Fantasy|Sci-Fi
     * ...+89
     * 8
     */


    // Returns top `numUsers` users recommended for each item, for all items.(给所有的商品推荐前N个用户)
    // 为每一个商品推荐10个用户
    val forItemRecommendUser =  model.recommendForAllItems(10)
    forItemRecommendUser.show(false)

    // 为指定的用户推荐商品
    val users = movieRatings.select(als.getUserCol).distinct().limit(5)
    val userSubset = model.recommendForUserSubset(users,10)
    userSubset.show(false)

    // 为指定的商品推荐用户
    val items = movieRatings.select(als.getItemCol).distinct().limit(5)
    val itemSubset = model.recommendForItemSubset(items,10)
    itemSubset.show(false)

  }

}

以471用户为例,由推荐结果可以看出与471用户喜欢看的电影类似,即推荐结果相似度较为准确。

使用模型推荐,基于机器学习算法,计算的过程可解释性不强,但是可以看出矩阵分解的结果

package com.similarity

import org.apache.spark.ml.recommendation.ALS
import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.sql.SparkSession

object ALSRecommendTest {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    /**
     * 1.准备源数据集
     */
    import spark.implicits._
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/utou_main.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    movieRatings.show()

    /**
     * userid、movieid、rate
     * +----+----+----+
     * |user|item|rate|
     * +----+----+----+
     * |   1|   1| 7.0|
     * |   1|   2| 6.0|
     * |   1|   3| 7.0|
     * |   1|   4| 4.0|
     * |   1|   5| 5.0|
     * |   1|   6| 4.0|
     * |   2|   1| 6.0|
     * ....
     */


    /**
     * 2.从源数据集中拆分出一部分作为训练集
     */
    val Array(traing,test) = movieRatings.randomSplit(Array(0.8,0.2))

    /**
     * 3.用训练集来训练一个模型
     */
    val als = new ALS()
      .setMaxIter(15)
      .setUserCol("user")
      .setItemCol("item")
      .setRatingCol("rate")
      .setRegParam(0.15)
      .setRank(5)

    val model = als.fit(traing)

    model.userFactors.show(false)

    /**
     * u * k
     * +---+------------------------------------------------------------+
     * |id |features                                                    |
     * +---+------------------------------------------------------------+
     * |1  |[-0.42829245, 1.2100297, 1.5367326, 1.3456051, 1.1269084]   |
     * |2  |[-0.21696071, 1.4962134, 0.26255712, 1.696837, 0.91942203]  |
     * |3  |[-0.18513954, 0.30519018, 0.18084258, 0.9029759, 0.66609854]|
     * |4  |[0.12840177, 1.448471, 0.7422876, 0.29590353, -0.66244245]  |
     * |5  |[0.06075315, 1.1249603, 0.82610285, -0.1717914, -0.44684315]|
     * +---+------------------------------------------------------------+
     */

    model.itemFactors.show(false)

    /**
     * m * k
     * +---+-------------------------------------------------------------+
     * |id |features                                                     |
     * +---+-------------------------------------------------------------+
     * |1  |[-0.4974848, 0.8555969, 0.92813057, 1.6163523, 1.6363019]    |
     * |2  |[-0.2451112, 1.5849723, 0.23978408, 1.8503357, 1.0529858]    |
     * |3  |[-0.41123268, 0.9665007, 1.1656736, 1.5604833, 1.1811998]    |
     * |4  |[-0.06691618, 1.2421515, 0.94377655, 0.6829833, -0.020079583]|
     * |5  |[-0.17200808, 1.3129894, 1.5179192, 0.34047952, 0.16705556]  |
     * |6  |[0.03691037, 1.8315963, 0.83225644, 0.6670685, -0.28363428]  |
     * +---+-------------------------------------------------------------+
     */


    /**
     * 在打分数据里面,有这样的情况,就是数据集切割之后,用户对电影的评分有不完整的情况,就是测试集里面可能会出现训练集当中
     * 没有出现过的电影或者用户,这个叫做冷启动
     * ALS有两种策略,一种是标识为NAN,还有一种是丢弃掉。我们这里选择丢掉
     */
    model.setColdStartStrategy("drop")

    //为每一个用户推荐6个商品
    val forUserRecommendItem =  model.recommendForAllUsers(6)
    forUserRecommendItem.show(false)

    /**
     * +----+------------------------------------------------------------------------------------------------+
     * |user|recommendations                                                                                 |
     * +----+------------------------------------------------------------------------------------------------+
     * |1   |[[3, 6.6299453], [1, 6.5756474], [2, 6.1047587], [5, 4.9225373], [6, 3.8439806], [4, 3.815962]] |
     * |3   |[[2, 2.8790858], [1, 2.8270867], [3, 2.807918], [5, 1.7000057], [4, 1.1680722], [6, 1.0593476]] |
     * |5   |[[6, 2.7624767], [5, 2.1050837], [4, 1.7137223], [2, 1.2833189], [3, 1.2558985], [1, 0.9020073]]|
     * |4   |[[6, 3.618065], [5, 2.7936559], [4, 2.172113], [2, 2.0124884], [3, 1.8128169], [1, 1.3068774]]  |
     * |2   |[[2, 6.5499663], [3, 6.1952796], [1, 5.9651566], [5, 4.6789336], [6, 3.9758077], [4, 3.3447893]]|
     * +----+------------------------------------------------------------------------------------------------+
     */

  }

}

以上内容仅供参考学习,如有侵权请联系我删除!
如果这篇文章对您有帮助,左下角的大拇指就是对博主最大的鼓励。
您的鼓励就是博主最大的动力!

猜你喜欢

转载自blog.csdn.net/weixin_45366499/article/details/114087845