Spark ALS源码总结
来源:互联网 发布:恐怖漫画 软件 编辑:程序博客网 时间:2024/04/27 02:04
Spark ALS是ALS的分布式实现,非常高效,代码进行了大量的优化,有许多可以借鉴和思考的地方,它实现了Explicit ALS和Implicit ALS分布式算法。本文是阅读Spark ALS源码后的一些总结和思考。
Implicit ALS原理
先说一下隐式数据的特点:
- 没有负反馈
- 充满噪声
- 显式数据的数值代表偏好,隐式数据的数值代表了置信等级(confidence level)。例如,我们观测到该用户看了某部电影,推测他可能喜欢该电影,如果发现他看了这部电影好几遍,我们就很有信心认为他喜欢这部电影了
基于隐式数据的特点,Implicit ALS做如下假设:
引入二分值
pui :pu,i={10if ru,i>0otherwise 引入置信等级
cui cui=1+αrui 损失函数
minx,y∑(u,i)∈Kcu,i(pu,i−x⊤uyi)2+λ(∑u||xu||2+∑i||yi||2) 迭代公式
xu=(Y⊤CuY+λI)−1Y⊤Cupuyi=(X⊤CiX+λI)−1X⊤Cipi
其中,Cu=⎛⎝⎜⎜⎜cu1⋱cun⎞⎠⎟⎟⎟ 变换
以xu 为例,直接计算的话,计算量太大,注意到Y⊤CuY=Y⊤Y+Y⊤(Cu−I)Y ,而Cu−I 使得只需要计算用户u 有过行为的物品集合,Y⊤Y 可以在一轮迭代里只需要计算一次。这里已经有点分布式的味道了。
Spark ALS并行化分析
以
- 用户
u 的评分详情(对哪个物品评了多少分),用于计算Cu−I ,pu - 用户
u 关联的所有物品集的隐式因子,用于计算Y⊤(Cu−I)Y 和Y⊤Cupu
Spark ALS 主要有三个步骤
- partitionRatings:将原始评分数据分片为块
- makeBlocks:产生InBlock和OutBlock
- computeFactors:Normal Equation求解
分布式计算关注的重点是控制计算复杂度和通信复杂度。Spark ALS首先是以Block为单位进行正态方程求解的。例如在迭代Block 1中的所有用户
这里Spark ALS设计了两个结构InBlock和OutBlock。InBlock存储评分详情,OutBlock存储“因子关联索引”,用于索引相关的Item Facors,这部分的源码是个难点。
Spark ALS 数据格式衍变
通过阅读源码,总结其从最初的评分数据格式衍变格式如下
将原始ratings分片为blocks (srcBlockId, dstBlockId)
建立InBlock和OutBlock srcBlockId
dstLocalIndices表示Block本地索引 2.2 srcBlockId
dstEncodedIndices是dstBlockId和dstLocalIndices的组合 3 InBlock形态 srcBlockId
这里进行了排序和矩阵压缩 4 OutBlock形态 srcBlockId
Block用户集获取所需Item集Factors的过程
ItemOutBlock.join(ItemFactors).flatMap { case(ItemBlockId, (ItemOutBlock, ItemFactors)) => ItemOutBlock.view.zipWithIndex.map { case ([uniqItemIdLocalIndex], UserBlockId) => (UserBlockId, (ItemBlockId, AssocItemFactors))) } } =>(UserBlockId, Array[(ItemBlockId, ItemFactors)])
通过以上方式,所有需要的元素都传输到了一起。
重点源码分析
核心源码在org.apache.spark.ml.recommendation.ALS中,相比于Spark SVD++的200多行的代码,ALS的代码量真是巨无霸,洋洋洒洒1~2千行,不过核心模块的代码也是几百行左右,这里主要分析makeBlocks源码片段。
partitionRatings的作用
原始评分数据是
把这些打分直接按照 Tuple 存的话会有几个问题。首先是空间的额外开销,每个 Tuple 实例都需要一个指针,而每个 Tuple 所存的数据不过是两个 ID 和一个打分,非常不划算。而且存储大量的 Tuple 实例会降低 Java 垃圾回收效率。所以我们使用三个原始数组来存 InBlock 信息:([v1, v2, v1, v2, v2], [u1, u1, u2, u2, u3], [a11, a12, a21, a22, a32])。这样不仅大幅减少了实例数量,还有效地利用了连续内存
编号3 矩阵压缩
注释上说CSC压缩,但是我觉得是CSR压缩,并且和普通的CSR压缩不同,因为是hash到了block单位,每个block上的SrcIds更加稀疏,此时用普通矩阵压缩会产生大量的0值。于是就有了这种形式,注意在压缩之前已经排好序了,这样做的便利很多,1是方便压缩 2是方便最后的正态方程的计算。
其中,
编号4 OutBlock生成
OutBlock的意义就是建立连接,当前的SrcBlockId中的哪些srcIdLocalIndex是需要发送到哪些DstBlockId的。
val outBlocks = inBlocks.mapValues { case InBlock(srcIds, dstPtrs, dstEncodedIndices, _) => val encoder = new LocalIndexEncoder(dstPart.numPartitions) //activeIds就是需要建立的“联系”,发送给哪些dst,发送哪些srcIdLocalIndex val activeIds = Array.fill(dstPart.numPartitions)(mutable.ArrayBuilder.make[Int]) var i = 0 //标识该uniqSrcId已经发送 val seen = new Array[Boolean](dstPart.numPartitions) while (i < srcIds.length) { var j = dstPtrs(i) ju.Arrays.fill(seen, false) while (j < dstPtrs(i + 1)) { val dstBlockId = encoder.blockId(dstEncodedIndices(j)) if (!seen(dstBlockId)) { //添加local index到该out-block activeIds(dstBlockId) += i seen(dstBlockId) = true } j += 1 } //该uniqSrcId发送完毕 i += 1 } activeIds.map { x => x.result() } }.setName(prefix + "OutBlocks") .persist(storageLevel)
topK推荐
看了Spark ALS的JIRA下的讨论,mahout的作者Sean Owen提供了一种方法LSH来缩减搜索范围,不过该方法会损失虽小但可见的精度,最后讨论的结果还是维持原状。
在实际操作的时候,我们可以根据领域知识缩减这个内积操作的范围,比如推荐餐馆,我们通过标识该用户的活动区域,只把该用户的隐式因子与这个区域的餐馆的隐式因子乘积,这样可以极大地缩减计算量。
参考
- Spark ALS源码
- https://issues.apache.org/jira/browse/SPARK-3066
- http://www.csdn.net/article/2015-05-07/2824641
- Spark ALS源码总结
- ALS spark
- spark ALS 使用checkpoint 机制
- spark mllib ALS算法简介
- 为什么spark中只有ALS
- spark机器学习之als
- spark源码总结
- spark源码总结
- 深入理解Spark 2.1 MLlib(一):基于ALS矩阵分解的协同过滤算法与源码分析
- 【spark系列7】协同过滤之ALS
- 使用Spark ALS实现协同过滤
- 如何使用Spark ALS实现协同过滤
- spark mllib中ALS算法思想
- ALS 在 Spark MLlib 中的实现
- Spark 机器学习 —— ALS
- 如何使用Spark ALS实现协同过滤
- spark mllib als推荐引擎学习
- 如何使用Spark ALS实现协同过滤
- Android解决bug的思路:追本溯源
- httpclient 在获取返回值时,使用getResponseBody触发警告的问题
- css修改input中placeholder 的字体颜色
- IOS指定APP程序入口
- Spring学习笔记4-JDBC
- Spark ALS源码总结
- Android常用开源项目(十五)
- 仿IOS Switch 开关
- 图像检索
- 来到CSDN,希望能够开始记录自己
- JSON with HTTP
- linux安装mysql
- unity编辑器显示中文枚举
- Mac系统崩溃,如何备份、恢复数据