前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spark-ALS 分布式实现详解

Spark-ALS 分布式实现详解

原创
作者头像
涂小刚
修改2017-10-17 16:39:25
4K3
修改2017-10-17 16:39:25
举报
文章被收录于专栏:涂小刚的专栏涂小刚的专栏

如今,协同过滤推荐(CollaboratIve Filtering)技术已广泛应用于各类推荐系统中,其通常分为两类,一种是基于用户的协同过滤算法(User-Based CF),它是根据用户对物品的历史评价数据,如喜欢、点击、购买等,计算不同用户之间的相似度,在有相同喜好的用户间进行物品推荐,例如将跟我有相同电影爱好的人看过的电影推荐给我。

另一种是基于物品的协同过滤算法(Item-Based CF),它是根据用户对物品的历史评价数据,计算物品之间的相似度,用户如果喜欢A物品,那么可以给用户推荐跟A物品相似的其他物品,例如如果我们在购物网站上买过尿片,第二天你再到购物网站上浏览时,可能会被推荐奶瓶。更多关于User-Based CF和Item-Based CF的阐述请参考《推荐算法协同过滤》文章。然而,在用户评分不足的情况下,上述两种方法就不是很好使了,近年来,基于模型的推荐算法ALS(交替最小二乘)在Netflix成功应用并取得显著效果提升,ALS使用机器学习算法建立用户和物品间的相互作用模型,进而去预测新项。

基本原理

用户对物品的打分行为可以表示成一个打分矩阵
[1500349872660_9322_1500349872762.png]
[1500349872660_9322_1500349872762.png]
,例如下表所示:
[1500349895395_867_1500349895443.png]
[1500349895395_867_1500349895443.png]

矩阵中的打分值

[1500349909791_6684_1500349909807.png]
[1500349909791_6684_1500349909807.png]
表示用户
[1500349928825_8287_1500349928842.png]
[1500349928825_8287_1500349928842.png]
对物品
[1500349945063_7305_1500349945091.png]
[1500349945063_7305_1500349945091.png]
的打分,其中"?"表示用户没有打分,这也就是要通过机器学习的方法去预测这个打分值,从而达到推荐的目的。

模型抽象

按照User-Based CF的思想,

[1500349996572_8126_1500349996586.png]
[1500349996572_8126_1500349996586.png]
的行向量对应每个用户
[1500350010044_5628_1500350010061.png]
[1500350010044_5628_1500350010061.png]
,按照Item-Based CF的思想,
[1500350032920_1329_1500350032937.png]
[1500350032920_1329_1500350032937.png]
的列向量对应每个物品
[1500350051607_1764_1500350051607.png]
[1500350051607_1764_1500350051607.png]
。ALS 的核心思想是,将用户和物品都投影到
[1500350287345_9549_1500350287351.png]
[1500350287345_9549_1500350287351.png]
维空间,也就是说,假设有
[1500350300178_8592_1500350300190.png]
[1500350300178_8592_1500350300190.png]
个隐含特征,至于个隐含特征具体指什么不用关心,将每个用户和物品都用
[1500350313486_1297_1500350313495.png]
[1500350313486_1297_1500350313495.png]
维向量来表示,把它们之间的内积近似为打分值,这样就可以得到如下近似关系:

[1500350337188_8866_1500350337199.png]
[1500350337188_8866_1500350337199.png]

其中,

[1500350352180_2601_1500350352196.png]
[1500350352180_2601_1500350352196.png]
为打分矩阵
[1500350369039_8069_1500350369046.png]
[1500350369039_8069_1500350369046.png]
[1500350382309_4327_1500350382312.png]
[1500350382309_4327_1500350382312.png]
个用户,
[1500350417214_2464_1500350417235.png]
[1500350417214_2464_1500350417235.png]
个物品,
[1500350435662_1366_1500350435688.png]
[1500350435662_1366_1500350435688.png]
为用户对隐含特征的偏好矩阵
[1500350463245_7762_1500350463322.png]
[1500350463245_7762_1500350463322.png]
[1500350487706_1004_1500350487733.png]
[1500350487706_1004_1500350487733.png]
为物品对隐含特征的偏好矩阵
[1500350501588_3359_1500350501633.png]
[1500350501588_3359_1500350501633.png]

上述模型的参数就是矩阵

[1500350513598_7963_1500350513630.png]
[1500350513598_7963_1500350513630.png]
[1500350521160_3415_1500350521191.png]
[1500350521160_3415_1500350521191.png]
,即求解出
[1500350531123_2327_1500350531149.png]
[1500350531123_2327_1500350531149.png]
[1500350539305_845_1500350539344.png]
[1500350539305_845_1500350539344.png]
我们就可以重现打分矩阵,填补原始打分矩阵中的缺失值"?"。

显示反馈代价函数

要求解上述模型中的

[1500350593306_4355_1500350593452.png]
[1500350593306_4355_1500350593452.png]
[1500350600427_528_1500350600457.png]
[1500350600427_528_1500350600457.png]
,那么就需要一个代价函数来衡量参数的拟合程度,如果有比较明确的显式反馈打分数据,那么可以比较重构出来的打分矩阵与实际打分矩阵,即得到重构误差,由于实际打分矩阵有很多缺失值,所以仅计算已知打分的重构误差,下面函数为显示反馈代价函数。

[1500350617965_1900_1500350618009.png]
[1500350617965_1900_1500350618009.png]

其中,

[1500350636635_2433_1500350636666.png]
[1500350636635_2433_1500350636666.png]
表示用户
[1500350649449_5393_1500350649484.png]
[1500350649449_5393_1500350649484.png]
对物品
[1500350661883_3954_1500350661908.png]
[1500350661883_3954_1500350661908.png]
的打分,
[1500350672130_7849_1500350672151.png]
[1500350672130_7849_1500350672151.png]
为矩阵
[1500350695945_4550_1500350695969.png]
[1500350695945_4550_1500350695969.png]
的第
[1500350716208_8072_1500350716260.png]
[1500350716208_8072_1500350716260.png]
[1500350734887_2657_1500350734918.png]
[1500350734887_2657_1500350734918.png]
[1500350749829_4016_1500350749863.png]
[1500350749829_4016_1500350749863.png]
为矩阵
[1500350777543_4346_1500350777571.png]
[1500350777543_4346_1500350777571.png]
的第
[1500350803476_1984_1500350803504.png]
[1500350803476_1984_1500350803504.png]
[1500350815863_2948_1500350815886.png]
[1500350815863_2948_1500350815886.png]
[1500350827460_3789_1500350827485.png]
[1500350827460_3789_1500350827485.png]
为正则项系数。

隐式反馈代价函数

很多情况下,用户并没有明确反馈对物品的偏好,需要通过用户的相关行为来推测其对物品的偏好,例如,在视频推荐问题中,可能由于用户就懒得对其所看的视频进行反馈,通常是收集一些用户的行为数据,得到其对视频的偏好,例如观看时长等。通过这种方式得到的偏好值称之为隐式反馈值,即矩阵

[1500350873747_2389_1500350873771.png]
[1500350873747_2389_1500350873771.png]
为隐式反馈矩阵,引入变量
[1500350885095_3106_1500350885110.png]
[1500350885095_3106_1500350885110.png]
表示用户
[1500350898314_1740_1500350898430.png]
[1500350898314_1740_1500350898430.png]
对物品
[1500350911387_3539_1500350911406.png]
[1500350911387_3539_1500350911406.png]
的置信度,如果隐式反馈值大于0,置信度为1,否则置信度为0。

[1500350921671_50_1500350921689.png]
[1500350921671_50_1500350921689.png]

但是隐式反馈值为0并不能说明用户就完全不喜欢,用户对一个物品没有得到一个正的偏好可能源于多方面的原因,例如,用户可能不知道该物品的存在,另外,用户购买一个物品也并不一定是用户喜欢它,所以需要一个信任等级来显示用户偏爱某个物品,一般情况下,越大,越能暗示用户喜欢某个物品,因此,引入变量

[1500350940529_2146_1500350940554.png]
[1500350940529_2146_1500350940554.png]
,来衡量
[1500350954639_9521_1500350954660.png]
[1500350954639_9521_1500350954660.png]
的信任度,其中
[1500350967728_7579_1500350967755.png]
[1500350967728_7579_1500350967755.png]
为置信系数。

[1500350982953_9485_1500350982985.png]
[1500350982953_9485_1500350982985.png]

那么,代价函数则变成如下形式:

[1500350996008_5172_1500350996037.png]
[1500350996008_5172_1500350996037.png]

算法

无论是显示反馈代价函数还是隐式反馈代价函数,它们都不是凸的,变量互相耦合在一起,常规的梯度下降法可不好使了。但是如果先固定

[1500351020729_8619_1500351020756.png]
[1500351020729_8619_1500351020756.png]
求解
[1500351030839_8366_1500351030865.png]
[1500351030839_8366_1500351030865.png]
,再固定
[1500351037324_1066_1500351037351.png]
[1500351037324_1066_1500351037351.png]
求解
[1500351044430_830_1500351044446.png]
[1500351044430_830_1500351044446.png]
,如此迭代下去,问题就可以得到解决了。

[1500351068209_3929_1500351068233.png]
[1500351068209_3929_1500351068233.png]

那么固定一个变量求解另一个变量如何实现呢,梯度下降?虽然可以用梯度下降,但是需要迭代,计算起来相对较慢,试想想,固定

[1500351082706_7271_1500351082723.png]
[1500351082706_7271_1500351082723.png]
求解
[1500351088360_7861_1500351088386.png]
[1500351088360_7861_1500351088386.png]
,或者固定
[1500351096846_9873_1500351096872.png]
[1500351096846_9873_1500351096872.png]
求解
[1500351105702_7889_1500351105724.png]
[1500351105702_7889_1500351105724.png]
,其实是一个最小二乘问题,由于一般隐含特征个数
[1500351138604_6845_1500351138628.png]
[1500351138604_6845_1500351138628.png]
取值不会特别大,可以将最小二乘转化为正规方程一次性求解,而不用像梯度下降一样需要迭代。如此交替地解最小二乘问题,所以得名交替最小二乘法ALS,下面是基于显示反馈和隐式反馈的最小二乘正规方程。

显示反馈

固定
[1500351168147_1476_1500351168162.png]
[1500351168147_1476_1500351168162.png]
求解
[1500351175289_9187_1500351175311.png]
[1500351175289_9187_1500351175311.png]

[1500360037298_373_1500360037363.png]
[1500360037298_373_1500360037363.png]

更直观一点,每个用户向量的求解公式如下:

[1500360050716_6730_1500360050702.png]
[1500360050716_6730_1500360050702.png]

其中,

[1500360064265_8712_1500360064248.png]
[1500360064265_8712_1500360064248.png]
为矩阵
[1500360079227_3635_1500360079206.png]
[1500360079227_3635_1500360079206.png]
的第
[1500360099861_3173_1500360099836.png]
[1500360099861_3173_1500360099836.png]
行的转置
[1500360112281_601_1500360112256.png]
[1500360112281_601_1500360112256.png]
[1500360123843_58_1500360123817.png]
[1500360123843_58_1500360123817.png]
为矩阵
[1500360138637_8742_1500360138613.png]
[1500360138637_8742_1500360138613.png]
的第
[1500360154469_6628_1500360154432.png]
[1500360154469_6628_1500360154432.png]
行的转置
[1500360167732_7873_1500360167703.png]
[1500360167732_7873_1500360167703.png]

固定

[1500360178708_7718_1500360178682.png]
[1500360178708_7718_1500360178682.png]
求解
[1500360190642_5627_1500360190721.png]
[1500360190642_5627_1500360190721.png]

[1500360203602_6384_1500360203588.png]
[1500360203602_6384_1500360203588.png]

更直观一点,每个物品向量的求解公式如下:

[1500360228005_1516_1500360228020.png]
[1500360228005_1516_1500360228020.png]

其中,

[1500360250568_4832_1500360250585.png]
[1500360250568_4832_1500360250585.png]
为矩阵
[1500360266463_7833_1500360266435.png]
[1500360266463_7833_1500360266435.png]
的第
[1500360279679_9281_1500360279653.png]
[1500360279679_9281_1500360279653.png]
[1500360295779_3022_1500360295756.png]
[1500360295779_3022_1500360295756.png]
[1500360308371_8205_1500360308335.png]
[1500360308371_8205_1500360308335.png]
为矩阵
[1500360321498_1634_1500360321478.png]
[1500360321498_1634_1500360321478.png]
的第
[1500360336551_533_1500360336518.png]
[1500360336551_533_1500360336518.png]
[1500360348638_3480_1500360348621.png]
[1500360348638_3480_1500360348621.png]

隐式反馈

固定

[1500360372775_8701_1500360372754.png]
[1500360372775_8701_1500360372754.png]
求解
[1500360378960_9506_1500360378935.png]
[1500360378960_9506_1500360378935.png]

[1500360390991_2588_1500360390971.png]
[1500360390991_2588_1500360390971.png]

更直观一点,每个用户向量的求解公式如下:

[1500360412470_7166_1500360412449.png]
[1500360412470_7166_1500360412449.png]

其中,

[1500360452270_4461_1500360452241.png]
[1500360452270_4461_1500360452241.png]
为对角矩阵
[1500360470150_1679_1500360470122.png]
[1500360470150_1679_1500360470122.png]

固定

[1500360477778_6541_1500360477750.png]
[1500360477778_6541_1500360477750.png]
求解
[1500360488648_8890_1500360488613.png]
[1500360488648_8890_1500360488613.png]

[1500360502178_6543_1500360502261.png]
[1500360502178_6543_1500360502261.png]

更直观一点,每个物品向量的求解公式如下:

[1500360514610_8372_1500360514590.png]
[1500360514610_8372_1500360514590.png]

其中,

[1500360528008_7070_1500360527980.png]
[1500360528008_7070_1500360527980.png]
为对角矩阵
[1500360546141_5376_1500360546114.png]
[1500360546141_5376_1500360546114.png]

Spark 分布式实现

上述ALS算法虽然明朗了,但是要将其实现起来并不是信手拈来那么简单,尤其是数据量较大,需要使用分布式计算来实现,就更加不是那么地容易了。下面详细阐述Spark ML是如何完成ALS分布式实现的。为了更加直观的了解其分布式实现,下面用前面的打分矩阵作为例子,如下图所示。

[1500360565127_9670_1500360565111.png]
[1500360565127_9670_1500360565111.png]

由前面的原理介绍可知,按照显示反馈模型,固定

[1500360635553_9816_1500360635524.png]
[1500360635553_9816_1500360635524.png]
求解
[1500360643623_2016_1500360643590.png]
[1500360643623_2016_1500360643590.png]
,每个物品对隐含特征的偏好向量
[1500360659550_5623_1500360659515.png]
[1500360659550_5623_1500360659515.png]
由以下公式得到:

[1500360595411_3835_1500360595385.png]
[1500360595411_3835_1500360595385.png]

计算时,只需要计算得到

[1500360679001_6806_1500360678962.png]
[1500360679001_6806_1500360678962.png]
[1500360694420_8272_1500360694386.png]
[1500360694420_8272_1500360694386.png]
,再利用BLAS库即可解方程,初次迭代计算时,随机初始化矩阵
[1500360703407_7987_1500360703374.png]
[1500360703407_7987_1500360703374.png]
,假设得到如下初始形式:

[1500360714656_7023_1500360714627.png]
[1500360714656_7023_1500360714627.png]

假如求解

[1500360729671_6550_1500360729640.png]
[1500360729671_6550_1500360729640.png]
,由于只有
[1500360747666_6854_1500360747628.png]
[1500360747666_6854_1500360747628.png]
[1500360755353_1451_1500360755309.png]
[1500360755353_1451_1500360755309.png]
[1500360768134_6134_1500360768098.png]
[1500360768134_6134_1500360768098.png]
有打分,那么只需基于
[1500360789169_9391_1500360789130.png]
[1500360789169_9391_1500360789130.png]
[1500360798367_6557_1500360798338.png]
[1500360798367_6557_1500360798338.png]
来计算,根据相关线性代数知识就可以得到:

[1500360829558_2818_1500360829640.png]
[1500360829558_2818_1500360829640.png]

有了这个基本求解思路后,考虑

[1500360842128_6322_1500360842092.png]
[1500360842128_6322_1500360842092.png]
的维度为
[1500360856417_5108_1500360856379.png]
[1500360856417_5108_1500360856379.png]
,可以在单机上完成上述求解,那么就可以在不同task里完成不同物品
[1500360874984_833_1500360874938.png]
[1500360874984_833_1500360874938.png]
的计算,实现分布式求解,由打分矩阵可以得到如下图所示的关系图。

[1500360897620_1590_1500360897610.png]
[1500360897620_1590_1500360897610.png]

基于上述思路,就是要想办法把有打分关联的 u 和 v 想办法放到同一个分区里,这样就可以在一个task里完成对 v 的求解。首先对uid和vid以Hash分区的方式分区,假设分区数均为2,那么分区后的大致情况如下图所示,

[1500360919315_6514_1500360919278.png]
[1500360919315_6514_1500360919278.png]
[1500360940077_1481_1500360940046.png]
[1500360940077_1481_1500360940046.png]
在同一个分区中被求解,
[1500360951170_9701_1500360951133.png]
[1500360951170_9701_1500360951133.png]
[1500360965020_8465_1500360964987.png]
[1500360965020_8465_1500360964987.png]
在同一个分区中被求解。

[1500360979070_3966_1500360979052.png]
[1500360979070_3966_1500360979052.png]

上面的图仅为感性认识图,实际上手头仅有的数据就是打分矩阵,可以通过一个RDD表示打分矩阵ratings,RDD中的每条记录为(uid, vid, rating)形式,由于是基于

[1500360992469_4474_1500360992418.png]
[1500360992469_4474_1500360992418.png]
求解
[1500361000656_2654_1500361000619.png]
[1500361000656_2654_1500361000619.png]
,把uid称之为srcId,vid称之为dstId,按照srcId和dstId的分区方式,将ratings重新分区,得到的RDD为blockRatings,其中的每条记录为((srcBlockId, dstBlockId), RatingBlock)形式,key为srcId和dstId对应的分区id组成的二元组,value(RatingBlock)包含一个三元组(srcIds, dstIds, ratings)。对于前面的打分关系,原始打分矩阵重新分区如下图所示。

[1500361058584_4351_1500361058618.png]
[1500361058584_4351_1500361058618.png]

对于 u 来说,是要将自身信息发给不同的 v,对于 v 来说,是要接收来自不同 u 的信息,例如,要将

[1500361078368_8436_1500361078317.png]
[1500361078368_8436_1500361078317.png]
发给
[1500361096211_3774_1500361096168.png]
[1500361096211_3774_1500361096168.png]
[1500361102960_1262_1500361102913.png]
[1500361102960_1262_1500361102913.png]
[1500361110954_8632_1500361110910.png]
[1500361110954_8632_1500361110910.png]
,要接收
[1500361122387_5329_1500361122351.png]
[1500361122387_5329_1500361122351.png]
[1500361129222_6132_1500361129180.png]
[1500361129222_6132_1500361129180.png]
。那么基于上述重新分区后的打分RDD,分别得到关于 u 的出口信息userOutBlocks,以及 v 的入口信息itemInBlocks,就可以通过join将两者联系起来计算了。由于后面基于
[1500361143936_2816_1500361143996.png]
[1500361143936_2816_1500361143996.png]
[1500361151073_159_1500361151023.png]
[1500361151073_159_1500361151023.png]
,也需要求解关于 u 的入口信息userInBlocks,以及 v 的出口信息itemOutBlocks,所以一次性计算好并缓存起来。以计算 u 的入口信息和出口信息为例,在前面得到的重新分区后的blockRatings基础上求解,如下图所示。

[1500361169178_5511_1500361169214.png]
[1500361169178_5511_1500361169214.png]

首先通过一个map操作,将记录形式((srcBlockId, dstBlockId), RatingBlock)转换为(srcBlockId, (dstBlockId, srcIds, dstLocalIndices, ratings)),其中dstLocalIndices为dstIds去重排序后,每个dstId在其分区中的索引,最后根据srcBlockId做groupByKey,合并相同srcBlockId对应的value,合并过程中,对dstLocalIndices中的每个元素加上其对应的dstBlockId,这里做了一个优化,就是将localIndex和blockId用一个Int编码表示,同时采用类似CSC压缩编码的方式,进一步压缩srcIds和dstIds的对应关系。这样就按照 uid 进行分区后,得到 u 的入口信息,即将跟 u 关联的 v 绑定在一起了。基于该入口信息,可以进一步得到 u 的出口信息,如下图所示。

[1500361205435_7611_1500361205431.png]
[1500361205435_7611_1500361205431.png]

在userInBlocks基础上根据srcIds和dstIds的对应关系,通过map操作将(srcBlockId, (srcIds, dstPtrs, dstEncodedIndices, ratings))形式的记录转换为(srcBlockId, OutBlock)得到userOutBlocks,其中OutBlock是一个二维数组,有numDstBlock行,每一行为srcId所在srcBlockId中的索引,意为当前srcBlockId应该往每个dstBlockId发送哪些用户信息。

同理,在userInBlocks基础上初始化用户信息,得到userFactors,如下图所示,其中、、为随机初始化的向量。

[1500361222375_4115_1500361222380.png]
[1500361222375_4115_1500361222380.png]

接着对userOutBlocks和userFactors做join就可以模拟发送信息了,userOutBlocks中保存了应该往哪里发送的信息,userFactors中保存了用户信息,即一个掌握了方向,一个掌握了信息,如下图所示:

[1500361270298_5277_1500361270320.png]
[1500361270298_5277_1500361270320.png]

完成了从 u 到 v 的信息发送,后面就是基于 v 的入口信息来收集来自不同 u 的信息了,计算 v 的入口信息跟计算 u 的入口信息一样,只是先要把打分数据blockRatings的src和dst交换一下,如下图所示。

[1500361281804_2223_1500361281850.png]
[1500361281804_2223_1500361281850.png]

将itemInBlocks与前面的userOut做join,即可将具有相同dstBlockId的记录拉到一起,userOut中包含来自 u 的信息,itemInBlocks包含了与src的对应关系以及打分数据,针对每个 v 找到所有给它发送信息的 u,进而套最小二乘正规方程计算得到itemFactors。

[1500361295218_9162_1500361295250.png]
[1500361295218_9162_1500361295250.png]

得到itemFactors后可以以同样的方法基于求解,如此交替求解,直到最大迭代次数为止。

总结

ALS从基本原理上来看应该是很好理解的,但是要通过分布式计算来实现它,相对而言还是较为复杂的,本文重点阐述了Spark ML库中ALS的实现,要看懂以上计算流程,请务必结合源代码理解,凭空理解上述流程可能比较困难,在实际源码实现中,使用了很多优化技巧,例如使用在分区中的索引代替实际uid或vid,实现Int代替Long,使用数组等连续内存数据结构避免由于过多对象造成JVM GC后的内存碎片等。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本原理
  • 模型抽象
  • 显示反馈代价函数
  • 隐式反馈代价函数
  • 算法
  • 显示反馈
    • 固定[1500351168147_1476_1500351168162.png]求解[1500351175289_9187_1500351175311.png]
    • 隐式反馈
    • Spark 分布式实现
    • 总结
    相关产品与服务
    大数据
    全栈大数据产品,面向海量数据场景,帮助您 “智理无数,心中有数”!
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档