此博文是本主题的最后一篇,主要是记录实现该算法时,所遇到的性能问题,以及解决方法,并且在最后给出实现代码,希望对读者有用。
实现的工具是scala/spark,spark的api使得实现迭代的算法非常容易。但是如果不优化,在大的数据集上运行效率非常堪忧。好在经过一定摸索,$9千万\times 15$的数据集,用了13个小时计算完成,最后得到了138亿的相似对。Spark的参数配置如下:
num-executors 100
driver-memory 1G
executor-cores 2
executor-memory 10G
spark.default.parallelism=200
spark.storage.memoryFraction=0.8
spark.yarn.allocation.executor.maxMemory=35G
spark.driver.maxResultSize=4G
优化1-分布合并hash桶
LSH算法需要合并L个hash桶。最开始是先将L个桶计算完后,然后合并去重。这样需要同时将L个桶的数据保存在内存中,非常消耗空间。所以,优化的方法是按每n(<L)个桶合并,这样最多也只需要同事保留n个hash桶,节省了空间。
优化2-巨片随机化
在LSH过程中,如果数据分布非常集中,那么必然导致hash桶中一个hash key上聚集非常多的数据。比如在我的试验数据中,300万的数据,有一个hash key上聚集了1.7万的数据,称此现象为巨片。如果对这些数据进行排列组合,那么单个partition的突破spark 2GB限制,导致计算异常结束。解决方法是设置一个阀值,如果单个key聚集的对象数量高于这个阀值,就随机取样少量相识对象。这样效果不会太差,因为能够聚到一起的,说明本来就很相似。但是效率却得到了极大提升。
优化3-数据id变成整型
有些数据的id是字符串型,该数据十分消耗内存,建议通过hash的方法将其转成整型。比如我的试验数据集合,原始id是32个字符串,通过hash变成Long后,只有8个字节,空间节省了75%。虽然hash过程中可能存在一定冲撞,但应是小概率时间,可以忽略。
优化4-减少频繁Iterable转IndexedSeq
这个地方是没有注意的细节,修改后效率极大提升,所以还是记录于此。GroupByKey后得到的对象是Iterable,无法随机访问,必须转成IndexedSeq对象。修改之前,每次都在随机访问时转成IndexedSeq,相当消耗新能,尤其是部分hash key特别密集时,部分倾斜的分区计算相当滞后。修改后,只转换一次IndexedSeq,后续访问重复利用,性能得到了极大的提升。
优化5-手动Hash分区
这个问题可以参考StackoverFlow中问题。在优化1-分布合并hash桶时,结果需要调用partitionBy(new HashPartitioner(parts)),将结果转成ShuffledRDD。因为distinct操作可以最大化利用ShuffledRDD的,减少不必要的重新排序和网络传输。
优化6-提前过滤 (2017-5-23更新)
由于多个rdd merge时会很多内存和时间,所以每次计算一个桶rdd即过滤。此过程的开销是每个rdd与原始数据join两次。但是,适当做好partition(参考这里),只有一次join需要shuffle,另外一次不需要。最终性能得到了巨大提升。原来1亿乘30的数据计算时会OOM,优化后同样配置可以稳定在6小时完成。2千万行乘以30列的数据,优化前需要3小时,优化后只需要1小时。spark 2.1中添加了欧式距离LSHBucketedRandomProjectionLSH,但是没有做太多优化,同样配置,该算计算上面提到的1亿乘以30的数据集时,出现OOM异常。
参考代码 (2018-12-15更新)
下面的实现是离线计算版本,在线计算版本需要后台服务器开发,无法使用spark实现。但是如果掌握了整个LSH原理,在线版本不会太困难。下面是源代码,有兴趣的同学可以自取。
算法参数说明(2018-12-15更新)
在使用此算法的时候,有同学反馈参数太多,不会配置,下面笔者简单的介绍一下参数的意义,以及相关资料。参数主要分为几类,
- 核心算法参数,这些参数与算法精度和召回相关,建议了解算法原理后进行设置效果更佳,
- lshWidth 映射后的桶的宽度,文献中对应w,该值越大,可能导致所有样本映射到一个桶中,可以参考这篇文章进行设置该值。
- bucketWidth 桶内相同lsh的个数,文献中对应k,该值越大,精度越高。
- bucketSize 桶的个数,文献中对应L,该值越大,召回越大,可以参考这篇文章进行设置。
- lshUpBound 单个桶样本上限,一旦突破此上限,采取随机取样策略,具体意义见本文[优化2-巨片随机化][优化2-巨片随机化]
- lshRandom 随机选取的数量,随机的个数,具体意义见本文[优化2-巨片随机化][优化2-巨片随机化]
- 性能优化参数,这些参数如果不太清楚,可以先使用默认值,然后逐步在实验中调整,对结果精度影响不大
- storageLevel 缓存策略,数据量小为Memory,数据量大为Memeory_and_Disk
- parts RDD分区数量,用于设置并发
- mergeSize LSH表格缓存个数
- 应用相关参数,这些参数与应用逻辑强相关,
- distance 每个相似对的最大距离小于等于distance
- maxSize 每个样本,最多保留maxSize个相似对
结语
这次编写LSH系列博文,对该技术的系统总结。此过程学到了很多,包括论文阅读技巧,基础概率与微积分应用,spark优化等。虽然蛋痛过,但是觉得很值得。该方法应该可以在实际场景中应用到,比如相似用户计算,相似文本识别等,这也是驱使我投入精力来学习和实现该算法的主要动力,希望后面有机会应用到工作中。
更新于2017-05-23
最近半年来,应用LSH在好友推荐中,主要用于推荐陌生人,该算法推荐的用户在后续一起活跃上,明显优于对照组。
参考文献