在这篇文章中我们基于 《无敌的Log-Likelihood Ratio(1)——LLR的计算方式》中所介绍的 计算方式,具体的介绍一下 在推荐系统业务中的具体落地和使用。本文以电影推荐为例,简要阐述 在推荐系统中具体的使用和落地。
本文具体的组织结构如下:在第1节中主要回归了 的计算公式和推荐系统场景下相似度的计算。在第2节中,我们主要介绍了 相似度计算在公开的推荐相关数据集上的使用。在第3节中讨论了如何完成 相似度推荐在线上实时推荐的部署情况和可能存在的一些问题。在第4节中讨论几个我能够想到并关注的问题。
1. 相似度计算
1.1 相似度计算公式
在基于物品相似度的推荐中,我们主要通过
来衡量两个物品之间的相似度。当两个物品在用户的交互行为中共同出现的次数越多和其他物品共同出现的次数越少,理论上认为这两个物品越相似度越高。
在计算两个物品相似度之前,我们需要能够建立衡量两个物品共现关系的
的矩阵如下所示。
与 发生交互 | 没有与 发生交互 | |
---|---|---|
与 发生交互 | ||
没有与 发生交互 |
根据共现次数矩阵结合《无敌的Log-Likelihood Ratio(1)——LLR的计算方式》给出的计算原理,可以计算物品
和物品
的相似度如下。
1.2 推荐场景LLR的计算逻辑
在章节1.1中简要的对
计算公式和《无敌的Log-Likelihood Ratio(1)——LLR的计算方式》博客中讨论的内容作了一下回顾。那么在电影推荐系统中如何通过
来有效的计算两两物品之间的相似度?
假设在给出的数据集中一共有
个用户,这
个用户与所有的物品发生了
次交互行为,也就是
次的点击记录。对于给定的物品中的每一个物品
,其出现在用户的交互行为中的次数为
。物品
和物品
共同出现在某一个用户行为中的交互次数为
。基于上述给出的数据符号描述,对于物品
和物品
我们构建物品的共现次数关系矩阵如下:
与物品 发生交互 | 没有与物品 发生交互 | |
---|---|---|
与物品 发生交互 | ||
没有与物品 发生交互 |
根据上述共现次数关系矩阵和 的计算公式,我们可以具体得到两个物品之间两两相似度的计算方式,接下来我们讨论如何在代码中具体实现这个相似度的计算。
2. 相关数据集实现
在这一个小节中,我们会主要介绍一下 在movieLens ml-25的行为数据集上使用。movieLens数据集具体的下载链接可以参考博文《推荐系统常用的数据集》。数据集中包含的有以下几个文件:
- genome-socres
- genome-tag
- links.csv
- movies.csv
- tags.csv:
- README.txt
- ratings.csv
在上面的几个文件中,我们只需要用到rating.csv文件。在rating.csv文件中包含了所有的用户对电影的评分记录。在rating.csv文件中,每一行表示以为用户对一部电影的评分。rating.csv文件的具体格式如下:
userId,movieId,rating,timestamp
1,296,5.0,1147880044
1,306,3.5,1147868817
1,307,5.0,1147868828
1,665,5.0,1147878820
1,899,3.5,1147868510
1,1088,4.0,1147868495
1,1175,3.5,1147868826
1,1217,3.5,1147878326
1,1237,5.0,1147868839
在Spark上实现 衡量movieLens 数据集中两个电影之间的相似度。在以下的代码实现中,我们不考虑用户对电影的评分所产生的影响,也就是说在这里每一次共现都是正向的影响。代码实现的具体如下:
- 数据读取
val data = spark.sqlContext.read.format("com.databricks.spark.csv").option("header","true").option("inferSchema", true.toString).load("xzy/test/ratings.csv")
通过spark的sqlContext读取rating.csv文件并转成DataFrame格式,最终得到的DataFrame格式如下。
- 计算 的值
val n_count = data.count()
累计每个用户和物品之间发生的交互行为(这里指的是用户对电影的评分行为)的次数,得到计算共现次数矩阵所需要的
的值。
- 计算 和 的值
val count_by_movie = data.rdd.map{case Row(userId:Int, movieId:Int, rating:Double, timestamp:Int)=>(movieId, 1)}.reduceByKey(_+_)
累计每个物品(电影)被评分的次数得到计算共现次数矩阵所需要的
和
的值
- 计算 的值
val item_pairs = data.rdd.map{case Row(userId:Int, movieId:Int, rating:Double, timestamp:Int)=>(userId, movieId)}
.aggregateByKey(new mutable.ArrayBuffer[Int]())((b, r) => b += r, (b1, b2) => b1 ++= b2).filter(line=>line._2.length < 500)
val pairs_count = item_pairs.repartition(1000).flatMap { line =>
for (i <- line._2.indices; j <- line._2.indices; if i < j && line._2(i) != line._2(j)) yield {
val item1 = line._2(i)
val item2 = line._2(j)
((item1, item2), 1)
}
}.reduceByKey(_+_).flatMap {
case ((item1, item2), count) =>
Iterator((item1, item2, count),(item2, item1, count))
}
在上述过程中,对每个用户发生过交互的所有物品(电影)两两组成一个pair对并累计次数,得到所有
和
共同出现的次数
- 相似度计算公式实现
在博客《无敌的Log-Likelihood Ratio(1)——LLR的计算方式》中我们已经介绍过了 相似度计算公式的具体实现,在这边我们简单的做一下回顾,具体的代码实现如下。
private def xLogX(x: Long): Double = {
if (x <= 0) 0.0 else x * Math.log(1 + x)
}
private def entropy(a: Long, b: Long): Double = {
xLogX(a + b) - xLogX(a) - xLogX(b)
}
private def matrixEntropy(a: Long, b: Long, c: Long, d: Long): Double = {
xLogX(a + b + c + d) - xLogX(a) - xLogX(b) - xLogX(c) - xLogX(d)
}
private def logLikelihoodRatio(k11: Long, k12: Long, k21: Long, k22: Long) = {
//Preconditions.checkArgument(k11 >= 0 && k12 >= 0 && k21 >= 0 && k22 >= 0)
val rEntropy: Double = entropy(k11 + k12, k21 + k22)
val cEntropy: Double = entropy(k11 + k21, k12 + k22)
val mEntropy: Double = matrixEntropy(k11, k12, k21, k22)
if (rEntropy + cEntropy < mEntropy) {
0.0
}
else{
2.0 * (rEntropy + cEntropy - mEntropy)
}
}
- 计算相似度
通过面的步骤,我们计算得到了构建共现次数矩阵所需要的数据 、 、 以及 。我们可以构建共现次数矩阵并计算 , , , 的值。在得到 , , , 的值之后调用logLikelihoodRatio
计算电影之间的相似度。
val movie_count_map = spark.sparkContext.broadcast(count_by_movie.collectAsMap())
val pairs_sim_matrix = pairs_count.map{ line=>
val item1_count = movie_count_map.value.get(line._1).get
val item2_count = movie_count_map.value.get(line._2).get
val k11 = line._3
val k12 = item1_count - k11
val k21 = item2_count - k11
val k22 = n_count - k12 - k21 + k11
val sim = logLikelihoodRatio(k11, k12, k21, k22)
(line._1, line._2, sim)
}
根据上面的代码实现,计算得到的相似度如下。
在上表中我们挑选电影id为
所表示的电影和电影id为
的电影
,这两个电影计算得到相似度比较高。我们可以看一下这两个电影分别是什么。
4334,Yi Yi (2000),Drama
2908,Boys Don't Cry (1999),Drama
虽然,我也没有看过这两部电影不知道这里面的剧情具体是什么样子,但是看着好像都是爱情伦理剧的样子。
3. 实时部署及可能遇到问题
3.1 线上实时部署
在章节2中,我们完成了简单的物品到物品之间相似度的计算,但是在实际的线上部署过程中应该考虑到系统线上部署的实时性。也就是在用户行为发生变化的时候实时地更新给用户推荐的结果。
线上的实时部署过程简要概括分为两个部分:
- 倒排索引的构建
基于第二节运算得到的计算结果,构成了一个简单的三元组具体表示为如下形式:
其中:
:表示源电影的id
:表示目的电影的id
:表示两部电影的相似度
在这里需要注意的是, 和交叉熵不同, 计算得到的
和 中相似度 的值时相同的。
在上述计算结果的基础上,以 为 构建倒排索引,就能得到与 最相似度的 个视频,如下格式。
movie1 moive2:1.0435,movie3:0.343532,movie4:0.1034
...
...
- 实时检索并融合结果
在得到倒排索引之后,将倒排索引以key value形式存储在Redis中,实时检索的过程分为两种情况:
(1) 用户行为历史中只有一个对应的
获取用户的观看过得 ,根据key选取最相似的视频推荐给用户。
(2) 用户行为历史中有多个对应的
获取用户的观看过的所有 ,根据key选取最相似的视频推荐给用户,然后通过每一个视频相似度得分累加计算最高的相似度视频,推荐给用户。
3.2 冷启动问题
-
物品冷启动
根据前面给出的例子,在推荐系统通过 计算物品的相似度的时候需要,能够覆盖到的物品对和物品必须具备以下几个条件:
(1) 能覆盖到的物品必须被用户点击
(2) 点击该物品的用户还点击了其他物品,能够跟其他被点击的物品组成有效的pair对。
当系统中引入新物品的时候,新物品没有用户的交互行为,这样的情况下无法完成相似度的计算,存在物品的冷启动问题 -
用户冷启动
根据章节3.1中给出的线上实时部署的情况,当我们给一个用户推荐可能喜欢的物品的时候,需要具备的条件如下:
(1) 该用户有点击行为
(2) 该用户点击行为中涉及到的视频能够在相似矩阵中找到 相似度计算的可能相似结果。也就是这个视频在过往的用户行为中和其他视频发生共现。
当系统中引入有新用户进入的时候,用户还没有产生交互行为无法给用户推荐可能喜欢的物品,存在用户冷启动问题
4 问题
- 问题1: 在代码实现中,同时省去了 的值(具体描述可参考无敌的Log-Likelihood Ratio(1)——LLR的计算方式),对于整个计算结果最后求出来的相似度的大小有影响吗?
无影响,对于整个数据集来说, 的值时相同的,也就是所有用户和电影发生交互的次数。
- 问题2:我在上述代码中计算
pair_count
的时候为什么需要采用filter
过滤长度为500以上的用户?
val item_pairs = data.rdd.map{case Row(userId:Int, movieId:Int, rating:Double, timestamp:Int)=>(userId, movieId)}
.aggregateByKey(new mutable.ArrayBuffer[Int]())((b, r) => b += r, (b1, b2) => b1 ++= b2).filter(line=>line._2.length < 500)
考虑到数据的置信度和数据倾斜问题
- 数据的置信度:在这里我们认为一个用户观看500个电影以上的行为是不合理的。
- 数据倾斜问题:
aggregateByKey
执行后会得到一个长度为 用户的观影序列,并且对序列中的视频两两组成pair对,那么组成pair对的个数为 ,当 过大的时候在spark实现中可能会在某个executor产生过大的数据量,之后我们会讨论一下有哪些工程上的方法可以避免这个问题。
- 问题3:在《On Log-Likelihood-Ratios and the Significance of Rare Events》一文中有关于稀疏事件上 用来计算相似度准确性的讨论,如何看待这个问题?
- 问题4:在使用 的时候,如何把用户的评分也考虑进去?
参考资料
【1】无敌的Log-Likelihood Ratio(1)——LLR的计算方式
【2】On Log-Likelihood-Ratios and the Significance of Rare Events
【3】推荐系统常用数据集