介绍系统
作者 | vivo 互联网主机团队-Tang Shutao
现如今介绍无处不在,例如抖音、淘宝、京东App均能见到介绍系统的身影,其面前触及许多的技术。
本文以经典的协同过滤为切入点,重点引见了被工业界宽泛经常使用的矩阵合成算法,从通常与通常两个维度引见了该算法的原理,深刻易懂,宿愿能够给大家带来一些启示。
笔者以为要彻底搞懂一篇论文,最好的方式就是入手复现它,复现的环节你会遇到各种各样的纳闷、通常细节。
一、背景
1.1 引言
在消息爆炸的二十一世纪,人们很容易淹没在常识的陆地中,在该场景下搜查引擎可以协助咱们迅速找到咱们想要查找的内容。
在电商场景,如今的社会物质极大丰盛,商品目不暇接,种类单一。消费者很容易挑花眼,即用户将会面临消息过载的疑问。
为了处置该疑问,介绍引擎应运而生。例如咱们关上淘宝App,JD app,B站视频app,每一个场景下都有介绍的模块。
那么此时有一个幼儿园小好友突然问你,为什么JD给你介绍这本《程序员颈椎痊愈指南》?你或许会回答,由于我的职业是程序员。
接着小好友又问,为什么《Spark大数据剖析》这本书排在第6个介绍位,而《Scala编程》排在第2位?这时你或许不可回答这个疑问。
为了回答该疑问,咱们想象上方的场景:
在JD的电商系统中,存在着用户和商品两种角色,并且咱们假定用户都会对自己购置的商品打一个0-5之间的分数,分数越高代表越喜欢该商品。
基于此假定,咱们将上方的疑问转化为用户对《程序员颈椎痊愈指南》,《Spark大数据剖析》,《Scala编程》这三本书打分的话,用户会打多少分(用户之前未购置过这3本书)。因此东西在页面的先后顺序就等价于预测用户对这些东西的评分,并且依据这些评分启动排序的疑问。
为了便于预测用户对东西的评分疑问,咱们将一切三元组(User, Item, Rating),即用户User给自己购置的商品Item的评分为Rating,组织为如下的矩阵方式:
其中,表格蕴含 m 个用户和n个东西,将表格定义为评分矩阵Rm×nRm×n,其中的元素ru,iru,i示意第u个用户对第i个东西的评分。
例如,在上方的表格中,用户user-1购置了东西 item-1, item-3, item-4,并且区分给出了4,2,5的评分。最终,咱们将原疑问转化为预测红色空格处的数值。
1.2 协同过滤
协同过滤,便捷来说是应用与用户兴味相投、领有独特阅历之个体的喜好来介绍给用户感兴味的东西。兴味相投经常使用数学言语来表白就是相似度(人与人,物与物)。因此,依据相似度的对象,协同过滤可以分为基于用户的协同过滤和基于东西的协同过滤。
以评分矩阵为例,以行方向观测评分矩阵,每一行代表每个用户的向量示意,例如用户user-1的向量为 [4, 0, 2, 5, 0, 0]。以列方向观测评分矩阵,每一列示意每个东西的向量示意,例如东西item-1的向量为[4, 3, 0, 0, 5]。
基于向量示意,相似度的计算有多种公式,例如余弦相似度,欧氏距离,皮尔森。这里咱们以余弦相似度为例,它是咱们中学学过的向量夹角 (中学只触及2维和3维) 的高维推行,余弦相似度公式很容易了解和经常使用。给定两个向量A={a1,⋯,an}A={a1,⋯,an}和B={b1,⋯,bn}B={b1,⋯,bn},其夹角定义如下:
例如,咱们计算user-3和user-4的余弦相似度,二者对应的向量区分为 [0, 2, 0, 3, 0, 4],[0, 3, 3, 5, 4, 0]
向量夹角的余弦值越凑近1代表两个东东方向越凑近平行,也就是越相似,反之越凑近-1代表两个东东方向越凑近反向,示意两个东西相似度凑近雷同,凑近0,示动向量凑近垂直/正交,两个东西简直有关联。显然,这和人的直觉齐全分歧。
咱们用《血族第一部》在向量库 (存储向量的数据库,该系统能够依据输入向量,用相似度公式在库中启动检索,找出TopN的候选向量) 外面启动相似度检索,找到了前7部高相似度的影片,值得留意的是第一部是自己自身,相似度为1.0,其余三部是《血族》的其余3部同系列作品。
1.2.1 基于用户的协同过滤 (UserCF)
基于用户的协同过滤分为两步
例如,由用户u的相似用户{u1, u3, u5, u9}可得候选东西为
咱们如今预测用户u对东西i1的评分,由于东西在两个用户{u1, u5}的购置记载里,因此用户u对东西i1的预测评分为:
其中sim(u,u1)sim(u,u1)示意用户u与用户u1u1的相似度。
在介绍时,依据用户u对一切候选东西的预测分启动排序,取TopM的候选东西介绍给用户u即可。
1.2.2 基于东西的协同过滤(ItemCF)
基于东西的协同过滤分为两步
例如,咱们预测用户u对东西i3的评分,由于东西i3与东西{i6, i1, i9}均相似,因此用户u对东西i3的预测评分为:
其中sim(i6,i3)sim(i6,i3)示意东西 i6i6与东西i3的相似度,其余符号同理。
1.2.3 UserCF与ItemCF的比拟
咱们对ItemCF和UserCF做如下总结:
UserCF重要用于给用户介绍那些与之有独特兴味喜好的用户喜欢的东西,其介绍结果着重于反映和用户兴味相似的小个体的热点,更社会化一些,反映了用户所在的小型兴味个体中东西的抢手水平。在实践运行中,UserCF通常被运行于用于资讯介绍。
ItemCF给用户介绍那些和他之前喜欢的东西相似的东西,即ItemCF的介绍结果着重于维系用户的历史兴味,介绍愈加共性化,反运行户自己的兴味。在实践运行中,图书、电影平台经常使用ItemCF,比如豆瓣、亚马逊、Netflix等。
除了基于用户和基于东西的协同过滤,还有一类基于模型的协同过滤算法,如上图所示。此外基于用户和基于东西的协同过滤又可以归类为基于邻域 (K-Nearest Neighbor,KNN) 的算法,实质都是在找"TopN街坊",而后应用街坊和相似度启动预测。
二、矩阵合成
经典的协同过滤算法自身存在一些缺陷,其中最清楚的就是稠密性疑问。咱们知道评分矩阵是一个大型稠密矩阵,造成在计算相似度时,两个向量的点积等于0 (以余弦相似度为例)。为了更直观的了解这一点,咱们举例如下:
rom sklearn.metrics.pairwise import cosine_similaritya = [[0,0,0,3,2,0, 3.5,0,1 ],[0,1,0,0,0,0,0,0,0 ],[0,0,1,0,0,0,0,0,0 ],[4.1, 3.8, 4.6, 3.8, 4.4,3,4,0, 3.6]]cosine_similarity(a)# array([[1., 0., 0., 0.66209271],#[0., 1., 0., 0.34101639],#[0., 0., 1., 0.41280932],#[0.66209271, 0.34101639, 0.41280932, 1.]])
咱们从评分矩阵中抽取item1 - item4的向量,并且应用余弦相似度计算它们之间的相似度。
经过相似度矩阵,咱们可以看到东西item-1, item-2, item-3的之间的相似度均为0,而且与item-1, item-2, item-3最相似的东西都是item-4,因此在以ItemCF为基础的介绍场景中item-4将会被介绍给用户。
然而,东西item-4与东西item-1, item-2, item-3最相似的要素是item-4是一件抢手商品,购置的用户多,而东西item-1, item-2, item-3的相似度均为0的要素仅仅是它们的特色向量十分稠密,不足相似度计算的直接数据。
综上,咱们可以看到经典的基于用户/东西的协同过滤算法有自然的缺陷,不可处置稠密场景。为了处置该疑问,矩阵合成被提出。
2.1 显示反应
咱们将用户对东西的评分行为定义为显示反应。基于显示反应的矩阵合成是将评分矩阵Rm×n用两个矩阵Xm×k和Yn×k的乘积近似示意,其数学示意如下:
其中,k≪m/nk≪m/n示意隐性因子,以用户侧来了解,k=2k=2示意的就是用户的年龄和性别两个属性。此外有个很好的比喻就是物理学的三棱镜,白光在三棱镜的作用下被合成为7种色彩的光,在矩阵合成算法中,合成的作用就相似于"三棱镜",如下图所示,因此,矩阵合成也被称为隐语义模型。矩阵合成将系统的自在度从O(mn)降到了O((m+n)k,从而成功了降维的目的。
为了求解矩阵Xm×kXm×k和Yn×k,须要最小化平方误差损失函数,来尽或许地使得两个矩阵的乘积迫近评分矩阵 Rm×nRm×n,即
其中,λ(∑uxTuxu+∑iyTiyi)λ(∑uxuTxu+∑iyiTyi)为处罚项,λ为处罚系数/正则化系数,xu示意第u个用户的k维特色向量,yiyi示意第 i 个东西的k维特色向量。
整体用户的特色向量构成了用户矩阵Xm×kXm×k,整体东西的特色向量构成了东西矩阵Yn×k。
咱们训练模型的时刻,就只要要训练用户矩阵中的m×k个参数和东西矩阵中的n×k个参数。因此,协同过滤就成功转化成了一个优化疑问。
2.2 预测评分
经过模型训练 (即求解模型系数的环节),咱们获取用户矩阵Xm×k和东西矩阵Yn×kYn×k,所有用户对所有东西的评分预测可以经过Xm×k(Yn×k)TXm×k(Yn×k)T取得。如下图所示。
获取所有的评分预测后,咱们就可以对每个东西启动择优介绍。须要留意的是,用户矩阵和东西矩阵的乘积,获取的评分预估值,与用户的实践评分不是全等相关,而是近似相等的相关。如上图中两个矩阵粉色局部,用户实践评分和预估评分都是近似的,有必定的误差。
2.3 通常推导
矩阵合成ALS的通常推导网上也有不少,然而很多推导不是那么谨严,在操作向量导数时有的步骤甚至是失误的。有的博主对损失函数的求和项了解解产生失误,例如
然而评分矩阵是稠密的,求和并不会贯通整个用户集和东西集。正确的写法应该是
其中,(u,i)is known(u,i)is known示意已知的评分项。
咱们在本节给出详细的、正确的推导环节,一是当做数学小练习,其次也是对算法有更深层的了解,便于阅读Spark ALS的源码。
将(u,i)is known(u,i)is known经常使用数学言语形容,矩阵合成的损失函数定义如下:
其中 K 为评分矩阵中已知的(u,i) 汇合。例如上方的评分矩阵对应的K为
求解上述损失函数存在两种典型的优化方法,区分为
交替最小二乘,指的是固定其中一个变量,应用最小二乘求解另一个变量,以此交替启动,直至收敛或许抵达最大迭代次数,这也是“交替”一词的由来。
随机梯度降低,是优化通常中最罕用的一种方式,经过计算梯度,而后降级待求的变量。
在矩阵合成算法中,Spark最终选用了ALS作为官网的惟一成功,要素是ALS很容易成功并行化,义务之间没有依赖。
上方咱们入手推导一下整个计算环节,在机器学习通常中,微分的单位普通在向量维度,很少去对向量的重量为偏微分推导。
首先咱们固定东西矩阵 Y,将东西矩阵 Y看成常量。不失普通性,咱们定义用户u 评分过的东西汇合为IuIu,应用损失函数对向量xuxu求偏导,并且令导数等于0可得:
由于向量xuxu与求和符号∑i∈Iu∑i∈Iu有关,一切将其移出求和符号,由于xTuyiyTixuTyiyiT是矩阵相乘 (不满足替换性),因此xuxu在左边
等式两边取转置,咱们有
为了化简∑i∈IuyiyTi∑i∈IuyiyiT 与∑i∈Iuru,iyi∑i∈Iuru,iyi,咱们将Iu倒退。
假定Iu={ic1,⋯,icN}, 其中N示意用户u评分过的东西数量,iciici示意第cici个东西对应的索引/序号,借助于Iu,咱们有
其中,
YIuYIu为以Iu={ic1,⋯icN}Iu={ic1,⋯icN}为行号在东西矩阵 Y 当选取的N个行向量构成的子矩阵
Ru,Iu为以Iu={ic1,⋯icN}为索引,在评分矩阵 R的第u 行的行向量当选取的N 个元素,构成的子行向量
因此,咱们有
网上的博客,许多博主给出相似上方方式的论断不是很谨严,重要是损失函数的了解不到位造成的。
同理,咱们定义东西 i 被评分的用户汇合为Ui={ud1,⋯udM}Ui={ud1,⋯udM}
依据对称性可得
其中,
XUiXUi为以Ui={ud1,⋯,udM}Ui={ud1,⋯,udM}为行号在用户矩阵X当选取的M个行向量构成的子矩阵
Ri,UiRi,Ui为以Ui={ud1,⋯,udM}Ui={ud1,⋯,udM}为索引,在评分矩阵 R的第i列的列向量当选取的 M个元素,构成的子列向量
此外,IkIk为单位矩阵
假设读者觉得上述的推导还是很形象,咱们也给一个详细实例来体会一下两边环节
留意到损失函数是一个标量,这里咱们只倒退触及到x1,1,x1,2x1,1,x1,2的项,如下所示
让损失函数对x1,1,x1,2x1,1,x1,2区分求偏导数可以获取
写成矩阵方式可得
应用咱们上述的规定,很容易测验咱们导出的论断。
总结来说,ALS的整个算法环节只要两步,触及2个循环,如下图所示:
算法经常使用RMSE(root-mean-square error)评价误差。
当RMSE值变动很小时或许抵达最大迭代步骤时,满足收敛条件,中止迭代。
“Talk is cheap. Show me the code.” 作为小练习,咱们给出上述伪代码的Python成功。
import numpy as npfrom scipy.linalg import solve as linear_solve# 评分矩阵 5 x 6R = np.array([[4, 0, 2, 5, 0, 0], [3, 2, 1, 0, 0, 3], [0, 2, 0, 3, 0, 4], [0, 3, 3,5, 4, 0], [5, 0, 3, 4, 0, 0]])m = 5# 用户数n = 6# 东西数k = 3# 隐向量的维度_lambda = 0.01 # 正则化系数# 随机初始化用户矩阵, 东西矩阵X = np.random.rand(m, k)Y = np.random.rand(n, k)# 每个用户打分的东西汇合X_idx_dict = {1: [1, 3, 4], 2: [1, 2, 3, 6], 3: [2, 4, 6], 4: [2, 3, 4, 5], 5: [1, 3, 4]}# 每个东西被打分的用户汇合Y_idx_dict = {1: [1, 2, 5], 2: [2, 3, 4], 3: [1, 2, 4, 5], 4: [1, 3, 4, 5], 5: [4], 6: [2, 3]}
# 迭代10次for iter in range(10):for u in range(1, m+1):Iu = np.array(X_idx_dict[u])YIu = Y[Iu-1]YIuT = YIu.TRuIu = R[u-1, Iu-1]xu = linear_solve(YIuT.dot(YIu) + _lambda * np.eye(k), YIuT.dot(RuIu))X[u-1] = xufor i in range(1, n+1):Ui = np.array(Y_idx_dict[i])XUi = X[Ui-1]XUiT = XUi.TRiUi = R.T[i-1, Ui-1]yi = linear_solve(XUiT.dot(XUi) + _lambda * np.eye(k), XUiT.dot(RiUi))Y[i-1] = yi
最终,咱们打印用户矩阵,东西矩阵,预测的评分矩阵如下,可以看到预测的评分矩阵十分迫近原始评分矩阵。
# Xarray([[1.30678487, 2.03300876, 3.70447639],[4.96150381, 1.03500693, 1.62261161],[6.37691007, 2.4290095 , 1.03465981],[0.41685, 3.31805612, 3.24755801],[1.26803845, 3.57580564, 2.08450113]])# Yarray([[ 0.24891282,1.07434519,0.40258993],[ 0.12832662,0.17923216,0.72376732],[-0.00149517,0.77412863,0.12191856],[ 0.12398438,0.46163336,1.05188691],[ 0.07668894,0.61050204,0.59753081],[ 0.53437855,0.20862131,0.08185176]])# X.dot(Y.T) 预测评分array([[4.00081359, 3.2132548 , 2.02350084, 4.9972158 , 3.55491072, 1.42566466],[3.00018371, 1.99659282, 0.99163666, 2.79974661, 1.98192672, 3.00005934],[4.61343295, 2.00253692, 1.99697545, 3.00029418, 2.59019481, 3.99911584],[4.97591903, 2.99866546, 2.96391664, 4.99946603, 3.99816006, 1.18076534],[4.99647978, 2.31231627, 3.02037696, 4.0005876 , 3.5258348 , 1.59422188]])# 原始评分矩阵array([[4,0,2,5,0,0],[3,2,1,0,0,3],[0,2,0,3,0,4],[0,3,3,5,4,0],[5,0,3,4,0,0]])
三、Spark ALS运行
Spark的外部成功并不是咱们上方所列的算法,然而外围原理是齐全一样的,Spark成功的是上述伪代码的散布式版本,详细算法参考Large-scale Parallel Collaborative Filtering for the Netflix Prize。其次,查阅Spark的官网文档,咱们也留意到,Spark经常使用的处罚函数与咱们上文的有纤细的差异。
其中nu,ninu,ni区分示意用户u打分的东西数量和东西 i 被打分的用户数量。即
本小节经过两个案例来了解Spark ALS的详细经常使用,以及在面对互联网实践工程场景下的运行。
3.1 Demo案例
以第一节给出的数据为例,将三元组(User, Item, Rating)组织为als-demo-data.csv,该demo数据集触及5个用户和6个东西。
userId,itemId,rating1,1,41,3,21,4,52,1,32,2,22,3,12,6,33,2,23,4,33,6,44,2,34,3,34,4,54,5,45,1,55,3,35,4,4
经常使用Spark的ALS类经常使用十分便捷,只要将三元组(User, Item, Rating)数据输入模型启动训练。
import org.apache.spark.sql.SparkSessionimport org.apache.spark.ml.recommendation.ALSval spark = SparkSession.builder().appName("als-demo").master("local[*]").getOrCreate()val rating = spark.read.options(Map("inferSchema" -> "true", "delimiter" -> ",", "header" -> "true")).csv("./data/als-demo-data.csv")// 展现前5条评分记载rating.show(5)val als = new ALS().setMaxIter(10)// 迭代次数,用于最小二乘交替迭代的次数.setRank(3)// 隐向量的维度.setRegParam(0.01)// 处罚系数.setUserCol("userId")// user_id.setItemCol("itemId")// item_id.setRatingCol("rating")// 评分列val model = als.fit(rating)// 训练模型// 打印用户向量和东西向量model.userFactors.show(truncate = false)model.itemFactors.show(truncate = false)// 给一切用户介绍2个东西model.recommendForAllUsers(2).show()
上述代码在控制台输入结果如下:
+------+------+------+|userId|itemId|rating|+------+------+------+|1|1|4||1|3|2||1|4|5||2|1|3||2|2|2|+------+------+------+only showing top 5 rows+---+------------------------------------+|id |features|+---+------------------------------------+|1|[-0.17339179, 1.3144133, 0.04453602]||2|[-0.3189066, 1.0291641, 0.12700711] ||3|[-0.6425665, 1.2283803, 0.26179287] ||4|[0.5160747, 0.81320006, -0.57953185]||5|[0.645193, 0.26639006, 0.68648624]|+---+------------------------------------++---+-----------------------------------+|id |features|+---+-----------------------------------+|1|[2.609607, 3.2668495, 3.554771]||2|[0.85432494, 2.3137972, -1.1198239]||3|[3.280517, 1.9563107, 0.51483333]||4|[3.7446978, 4.259611, 0.6627]||5|[1.6036265, 2.5602736, -1.8897828] ||6|[-1.2651576, 2.4723763, 0.51556784]|+---+-----------------------------------++------+--------------------------------+|userId|recommendations|+------+--------------------------------+|1|[[4, 4.9791617], [1, 3.9998217]]|// 对应东西的序号和预测评分|2|[[4, 3.273963], [6, 3.0134287]] ||3|[[6, 3.9849386], [1, 3.2667]]||4|[[4, 5.011649], [5, 4.004795]]||5|[[1, 4.994258], [4, 4.0065994]] |+------+--------------------------------+
咱们经常使用numpy来验证Spark的结果,并且用Excel可视化评分矩阵。
import numpy as npX = np.array([[-0.17339179, 1.3144133, 0.04453602],[-0.3189066, 1.0291641, 0.12700711],[-0.6425665, 1.2283803, 0.26179287],[0.5160747, 0.81320006, -0.57953185],[0.645193, 0.26639006, 0.68648624]])Y = np.array([[2.609607, 3.2668495, 3.554771],[0.85432494, 2.3137972, -1.1198239],[3.280517, 1.9563107, 0.51483333],[3.7446978, 4.259611, 0.6627],[1.6036265, 2.5602736, -1.8897828],[-1.2651576, 2.4723763, 0.51556784]])R_predict = X.dot(Y.T)R_predict
输入预测的评分矩阵如下:
array([[3.99982136, 2.84328038, 2.02551472, 4.97916153, 3.0030386,3.49205357],[2.98138452, 1.96665, 1.03257371, 3.27396294, 1.88351875, 3.01342882],[3.26670123, 2.0001004 , 0.42992289, 3.00003605, 1.61982132, 3.98493822],[1.94325135, 2.97144913, 2.98550149, 5.011649, 4.00479503, 1.05883274],[4.99425778, 0.39883335, 2.99113433, 4.00659955, 0.41937014, 0.19627587]])
从Excel可视化的评分矩阵可以观察到预测的评分矩阵十分迫近原始的评分矩阵,以user-3为例,Spark介绍的东西是item-6和item-1, [[6, 3.9849386], [1, 3.2667]],这和Excel展现的预测评分矩阵齐全分歧。
从Spark函数recommendForAllUsers()给出的结果来看,Spark外部并没有去除用户曾经购置的东西。
3.2 工程运行
在互联网场景,用户数m(千万~亿级别) 和东西数 n(10万~100万级别) 规模很大,App的埋点数据普通会保留在HDFS中,以互联网的长视频场景为例,用户的埋点消息最终聚合为用户行为表t_user_behavior。
行为表蕴含用户的imei,东西的content-id,然而没有直接的用户评分,通常中咱们的处置打算是应用用户的其余行为启动加权得出用户对东西的评分。即
rating=w1* play_time (播放时长) +w2* finsh_play_cnt (成功的播放次数) +w3* praise_cnt (点赞次数) +w4* share_cnt (分享次数) +其余适宜于你业务逻辑的目的
其中,wi为每个目的对应的权重。
如下的代码块演示了工程通常中对大规模用户和商品场景启动介绍的流程。
import org.apache.spark.ml.feature.{IndexToString, StringIndexer}// 从hive加载数据,并应用权重公式计算用户对东西的评分val rating_df = spark.sql("select imei, content_id, 权重公式计算评分 as rating from t_user_behavior group by imei, content_id")// 将imei和content_id转换为序号,Spark ALS入参要求userId, itemId为整数// 经常使用org.apache.spark.ml.feature.StringIndexerval imeiIndexer= new StringIndexer().setInputCol("imei").setOutputCol("userId").fit(rating_df)val contentIndexer = new StringIndexer().setInputCol("content_id").setOutputCol("itemId").fit(rating_df)val ratings = contentIndexer.transform(imeiIndexer.transform(rating_df))// 其余code,相似于上述demoval model = als.fit(ratings)// 给每个用户介绍100个东西val _userRecs = model.recommendForAllUsers(100)// 将userId, itemId转换为原来的imei和content_idval imeiConverter= new IndexToString().setInputCol("userId").setOutputCol("imei").setLabels(imeiIndexer.labels)val contentConverter = new IndexToString().setInputCol("itemId").setOutputCol("content_id").setLabels(contentIndexer.labels)val userRecs = imeiConverter.transform(_userRecs)// 离线保留供线上调用userRecs.foreachPartition {// contentConverter 将itemId转换为content_id// 保留redis逻辑}
值得留意的是,上述的工程场景还有一种处置打算,即隐式反应。用户给商品评分很单一,在实践的场景中,用户未必会给东西打分,然而少量的用户行为,雷同能够直接反映用户的喜好,比如用户的购置记载、搜查关键字,添加购物车,单曲循环播放同一首歌。咱们将这些直接用户行为称之为隐式反应,以区别于评分对应的显式反应。胡一凡等人在论文Collaborative filtering for implicit feedback>
四、总结
本文从介绍的场景登程,引出了协同过滤这一经典的介绍算法,并且由此解说了被Spark惟一成功和保养的矩阵合成算法,详细推导了显示反应下矩阵合成的通常原理,并且给出了Python版本的单机成功,能够让读者更好的了解矩阵这一算法,最后咱们以demo和工程通常两个实例解说了Spark ALS的经常使用,能够让没有接触过介绍算法的同窗有个直观的了解,用通常与通常的方式明白矩阵合成这一介绍算法面前的原理。
参考文献: