ALS推荐算法在Spark上的优化--从50分钟到3分钟

从50多分钟到3分钟的优化

某推荐系统需要基于Spark用ALS算法对近一天的数据进行实时训练, 然后进行推荐. 输入的数据有114G, 但训练时间加上预测的时间需要50多分钟, 而业务的要求是在15分钟左右, 远远达不到实时推荐的要求, 因此, 我们与业务侧一起对Spark应用进行了优化.

另外提一下, 该文最好与之前我写的另一篇blog < Spark + Kafka 流计算优化 > 一起看, 因为一些细节我不会再在该文中描述.

优化分析

从数据分析, 虽然数据有114G, 但ALS的模型训练时间并不长, 反而是数据加载和ALS预测所占用的时间较长. 因此我们把重点放在这两点的优化中.

从Spark的Web UI可以看到, 数据加载的job中task的数量较多, 较小. 模型预测的job也是有这样的问题, 因此, 可以猜测并行度过大造成了集群的协调负荷过重.

 

仅降低并行度和优化JVM 参数

         我们用常见的”rdd.repartitionBy”把并行度降低后, 作业计算耗时减少并不明显, 且在模型预测的job中会有executor死掉的现象. 进而查看日志, 发现是内存占用过多, yarn把Spark应用给kill了.

         为解决该现象, 加入这些JVM参数: “ -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=72 -XX:NewRatio=2 -XX:SurvivorRatio=6 -XX:+CMSParallelRemarkEnabled -XX:+ParallelRefProcEnabled -XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled -XX:MaxTenuringThreshold=31 -XX:SurvivorRatio=8 -XX:+ExplicitGCInvokesConcurrent -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+AlwaysPreTouch -XX:-OmitStackTraceInFastThrow -XX:+UseCompressedStrings -XX:+UseStringCache -XX:+OptimizeStringConcat -XX:+UseCompressedOops -XX:+CMSScavengeBeforeRemark -XX:+UseBiasedLocking -XX:+AggressiveOpts " 具体说明和原因请参考本人的另一篇blog < Spark + Kafka 流计算优化 >.

 

笛卡尔积操作中的预先分块

作了上述例行优化后,  ALS预测步骤的耗时减少依然不明显, 为发掘原因, 我们看了下源码, 惊奇的发现在ALS预测中竟然是用了笛卡尔积操作, 114G的数据少说也有几千万行记录, 几千万行记录进行笛卡尔积, 不慢才怪吧.

还好, 我们有扩展版的ALS预测方法, 可以将数据预先分块, 而不必一行行的进行笛卡尔积, 加快了笛卡尔积的速度.

val recommendations = ExtMatrixFactorizationModelHelper.recommendProductsForUsersmodel.getnumRecommendations420000StorageLevel.MEMORY_AND_DISK_SER )

该方法会对model中的userFeatures和itemFeatures矩阵进行预先分块, 减少网络包的传输量和笛卡尔积的计算量.

这里的第三个参数是每一个块所包含的行数, 此处的420000表示当我们对userFeatures或itemFeatures进行分块时, 每一个块包含了矩阵的420000行.

如何计算这里的一个块要包含多少行呢, 举例如下:

由于ALS算法中设置的rank是10, 因此生成的userFeatures和itemFeatures的个数是10, 它们的每一行是(Int,Array[Double]), 其中Array.size是10.

因此可根据如下计算每行所占的空间大小:

空Array[10]的大小=16+8*10=96Byte. 数组中的元素是Double, 十个Double对象的大小是 16*10=160Byte. 作为key的Integer的大小是16Byte. 因此每行占空间96+160+16=212Byte.

另外,要计算带宽: 由于是千兆网卡,因此带宽为1Gbit,转换为Byte也就是128MByte的带宽.

考虑到” spark.akka.frameSize=100”以及网络包包头需要占的空间, 和Java的各种封装要占的空间, 我们计划让1个block就几乎占满带宽, 也就是一个block会在100MByte左右.

因此, 一个block要占 60*1024*1024/212=296766行, 因此blocksize=494611, 考虑到各个object也占内存, 因此行数定为420000左右.

在分块后ExtMatrixFactorizationModelHelper.recommendProductsForUsers中会对块进行重新分区, 以达到基于块的均匀分布.

 

 

提高文件加载速度

         以前都是加载小文件, 每个文件才几M大小, 远远低于Hadoop的块大小, 使得IO频繁, 文件也频繁打开关闭, 加载速度自然就慢. 为解决该问题, 我们使用sc.wholeTextFiles(dirstr,inputSplitNum)来加载HDFS的文件到Spark中. 该方法使用Hadoop的CombineFileInputFormat把多个小文件合并成一个Split再加载到Spark中.

         但其实, 加载速度对整个Job的运行效率影响不大, 效果有限.


上图,是val inputData = sc.wholeTextFiles(dirstr,80) 和RangePartition.

貌似加载速度也好不到哪儿去.第一个job用了11min

'

上图用的是val inputData = sc.textFile(dirstr)RangeRepartition,同等情况下,加载也需要11min. (job6)


上图,是val inputData = sc.wholeTextFiles(dirstr,120) 和RangePartition.

貌似提高了wholeTextFiles()的split数量可以提升性能, 从8.4min多(此时split为80左右)到现在的8.1min


上图,是val inputData = sc.wholeTextFiles(dirstr,220) 和RangePartition. 第一次加载提升到6.0min.

其它job由于loclity的问题,时间有所拉长, 影响不大.

 

上图,是val inputData = sc.wholeTextFiles(dirstr,512) 和RangePartition. 第一次加载要7.1min. 估计220个split应该是个比较好的值了. 按比例就是 220(file split数) / 27000(小文件数) = 0.8% , 该场景中每个小文件10M左右. 也就是每个file split包含123个小文件­­­­, 每个file split 1230M, 也就是约1G左右.

减少笛卡尔积计算量

回到对笛卡尔积计算的优化, 因为50分钟的计算量基本上都是耗在笛卡尔积的计算上的.

         我们先看一下task的分布图, 由图看到, 数据并不是十分散列:

     猜测原因肯是通过 Array.mkString.hashCode 作为 key 并不能保证数据的均匀散列 . 因此 我们 disable 掉了在笛卡尔积计算前的预分块时的再分区 而是把再分区提到分块之前 .

这样一来, task分布稍微均衡了一些, 但依然不甚理想. 为了合理的降低task数量和均匀task的分布, 我们进一步使用了Spark扩展版本的自动分区功能.

         val userFactors = ExtSparkHelper.repartionPairRDDBySize(oldUsrFactThreeM)

         这个方法有两个参数, 第一个是需要分区的RDD, 第二个是我们期望的每一个task的input data的大小. 一般来说, input data与计算产生的data的大小相差不大, 但笛卡尔积却不同, 有可能产生上百倍的中间数据量. 因此, 我这里设置的每个task的input data是3M, 计算产生的中间数据刚好在1G左右, 一个executor可以同时跑3个task, 也算是比较理想的.

优化后的task分布图也比较理想, 十分均匀且没有任何浪费, 如下:


因此, 这一步优化后, 原笛卡尔积的运行速度从几十分钟变为十几秒.

整个ALS应用原来跑114G数据需要20多分钟,如下图现在只需要不到4min:



试了一下跑一天的全量数据231.9G, 则原来需要1个多小时,现在只要不到6分钟如下图:





  • 13
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
协同过滤是一种基于用户行为的推荐算法,它通过对用户历史行为数据进行分析,找到用户之间的相似性和物品之间的相似性,从而推荐用户可能感兴趣的物品。Spark Mllib中提供了两种协同过滤算法:基于用户的协同过滤和基于物品的协同过滤。其中,基于用户的协同过滤是指通过计算用户之间的相似度,来推荐给用户那些和他们相似的用户喜欢的物品;而基于物品的协同过滤则是通过计算物品之间的相似度,来推荐和用户喜欢的物品相似的物品。 在Spark Mllib中,协同过滤算法的实现主要分为两个步骤:模型训练和推荐。模型训练的目的是学习用户和物品的隐含特征向量,而推荐则是根据学习到的特征向量,预测用户对物品的评分并进行推荐。 LS(Least Squares)和ALS(Alternating Least Squares)都是协同过滤算法中的常用优化方法。LS方法通过最小化预测评分和实际评分之间的均方误差来学习用户和物品的隐含特征向量;而ALS方法则是把学习用户和物品的隐含特征向量的过程转化成一个交替最小二乘优化问题。具体地说,ALS方法先固定物品的隐含特征向量,最小化预测评分和实际评分之间的均方误差,得到用户的隐含特征向量;然后再固定用户的隐含特征向量,最小化预测评分和实际评分之间的均方误差,得到物品的隐含特征向量。这个过程不断交替进行,直到收敛为止。 在ALS算法中,还有一些优化过程可以提高算法的性能,比如使用正则化项来防止过拟合、使用随机梯度下降来加快学习速度等。 隐式反馈是指用户行为数据中的隐含信息,比如用户浏览过哪些物品,用户购买过哪些物品等。ALS-WR(Alternating Least Squares with Weighted-λ-Regularization)算法则是在ALS算法的基础上,引入了隐式反馈的权重信息,从而进一步提高了推荐的准确性。具体地说,ALS-WR算法中,用户对某个物品的评分不再是0或1,而是一个介于0和1之间的概率值,通过对这些概率值进行加权,可以更好地反映用户对物品的偏好程度。同时,算法中还使用了正则化项来防止过拟合。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值