文章翻译:Recommending items to more than a billion people(面向十亿级用户的推荐系统)

时间:2022-09-30 21:09:31

Web上数据的增长使得在完整的数据集上使用许多机器学习算法变得更加困难。特别是对于个性化推荐问题,数据采样通常不是一种选择,需要对分布式算法设计进行创新,以便我们能够扩展到这些不断增长的数据集。

协同过滤(CF)是其中一个重要的应用领域。CF是一种推荐系统技术,能够帮助人们发现感兴趣的东西。在Facebook,这些东西包括页面、兴趣组、事件、游戏等等。CF的核心思想是,最好的推荐来自品味相似的人。换句话说,它通过使用相似的人对历史物品的评分来预测某人会如何评价一件物品。

文章翻译:Recommending items to more than a billion people(面向十亿级用户的推荐系统)

1. CF and Facebook scale

一般情况下,Facebook CF的数据集有1000亿的评分,超过10亿的user,以及数百万的item。相比之下,著名的Netflix奖项推荐大赛拥有1亿收视率、48万user、17770部电影(项)的大型行业数据集。从那以后,这个领域有了更多的发展,但是,我们读到的最大的数字至少比我们正在处理的数字小两个数量级。

我们面临的一个挑战是需要设计一种分布式算法,可以扩展到这些海量的数据集,以及如何克服由于数据的某些属性(如ttem的倾斜度分布,或隐式的参与信号而不是评分)而引起的问题。

正如我们将在下面讨论的,现有解决方案中的方法不能有效地处理我们的数据大小。简而言之,我们需要一个新的解决方案。我们以前已经介绍过Apache Giraph,这是一个用于分布式迭代和图形处理的强大平台,以及我们为使其符合我们的需求所做的工作。我们还写了关于图分区的应用程序。Giraph在海量数据集上工作得非常好,易于扩展,而且我们在开发高性能应用程序方面有很多经验。因此,Giraph是我们解决这一问题的首选。

2. Matrix factorization

CF的一种常见方法是矩阵分解。在分解过程中,我们将问题视为拥有一组users和一组item,以及表示已知user-to-item评分的一个非常稀疏的矩阵。我们想预测这个矩阵中缺失的值。为了做到这一点,我们将每个user和每个item表示为潜在特征的向量,以便这些向量的点积与已知的user-to-item的评分非常匹配。预测未知的user-to-item评分也可以近似为对应的特征向量的点积。因此,我们要最小化下面的目标函数:

文章翻译:Recommending items to more than a billion people(面向十亿级用户的推荐系统)

这里,r是已知的user-to-item的评分,x和y是我们试图找到的user和item特征向量。由于*参数较多,需要正则化部分来防止过拟合和数值问题,其中gamma是正则化因子。

目前在合理的时间内找到上述公式的最优解是不可行的,但有一些迭代方法是从随机特征向量出发,逐步改进求解。经过一定次数的迭代,特征向量的变化变得非常小,达到收敛。有两种常用的迭代方法。

2.1 Stochastic gradient descent optimization

随机梯度下降法(SGD)在许多其他问题中得到了成功的应用。该算法以随机顺序遍历训练数据中的所有评分,对于每一个已知的评分r,通过预测r*(基于向量x和y的点积),计算预测误差e。当改变x和y时,将它们向梯度的反方向移动,得到x和y的每个特征的某些更新公式。

2.2 Alternating least square

交替最小二乘(ALS)是非线性回归模型的另一种方法,当有两个因变量(在我们的例子中是向量x和y)时,该算法修复一个参数(user向量x),同时通过最小化二次形式优化求解另一个参数(物品向量y)。该算法在固定user向量和更新物品向量、固定物品向量和更新user向量之间交替,直到满足收敛条件。

3. Standard approach and problems

为了以一种分布式的方式有效地计算上述公式,我们首先研究了在设计上与Giraph相似的系统是如何做到这一点的(使用消息传递而不是map/reduce)。标准的方法是将user和item都作为图的顶点,边表示已知的评分。然后SGD/ALS的迭代计算将发送user 和/或 tem特征向量到图的所有边,并进行本地更新。

文章翻译:Recommending items to more than a billion people(面向十亿级用户的推荐系统)

这个解决方案有几个问题:

  1. 海量的网络流量:这是所有分布式矩阵分解算法的主要瓶颈。由于我们通过图形的每条边发送一个特征向量,所以在一次迭代中通过线路发送的数据量与# rating * #Features成比例(在这里以及稍后的文本中,我们使用#作为“number of”的表示法)。对于1000亿个评分和100个双重特性,这将导致每次迭代产生80 TB的网络流量。在这里,我们假设user和item是随机分布的,并且我们忽略了一个事实,即一些评分可以存在于同一个worker上(平均来说,应该乘以因子1 (1 / #Workers))。请注意,智能分区不能大幅减少网络流量,因为这些item的规模很大,这并不能解决我们的问题。
  2. 我们数据集中的一些items非常流行,所以item的度分布是高度倾斜的:这可能会导致内存问题,每个item都接收到degree * #Features 的数据。例如,如果一个item有1亿个已知评分,并且使用了100个双重功能,那么仅这个item就可以占据80 GB的数据。大度item还会导致处理瓶颈(因为每个顶点都是原子处理的),每个人都会等待完成几个最大度items。
  3. 这并没有在原来的公式中完全实现SGD:每个顶点都使用迭代开始时接收到的特征向量,而不是它们的最新版本。例如,:例如,假设item A对uesr B和uesr  C进行评分。在一个连续的解决方案中,我们首先更新A和B,得到A' 和 B',然后更新A'和C。通过这个解决方案,B和C都将被更新为A,即迭代开始时item的特征向量。(这是一些无锁并行执行算法的实践,可以减慢收敛速度。)

4. Our solution — rotational hybrid approach

主要问题是在每次迭代中发送所有更新,因此我们需要一种新的技术来组合这些更新并发送更少的数据。首先,我们试图利用聚合器并使用它们来分发item数据,但是我们试图组合item特征向量上的部分更新的公式都不能很好地工作。

我们最终提出了一种方法,它要求我们使用 worker-to-worker 的消息传递来扩展Giraph框架。user仍然显示为图形的顶点,但是item被划分为#Workers不相交的部分,每个部分存储在一个worker的全局数据中。我们将所有worker放入一个圆圈中,并在每个超步骤之后顺时针方向旋转item,方法是将包含item的worker到worker的消息从每个Workers发送到行中的下一个Workers。

文章翻译:Recommending items to more than a billion people(面向十亿级用户的推荐系统)

通过这种方式,在每个超步骤中,我们为当前在超步骤上的item处理worker的部分user评分,因此在#Workers超步骤之后处理所有评分。让我们分析一下之前的解决方案存在的问题:

  1. 网络流量:对于SGD,在一次迭代中通过线路发送的数据量与#Items * #Features * #Workers成比例,它不再依赖于已知评分的数量。对于1000万个item、100个双功能和50个工作人员,这总共带来了400 GB,比标准方法小20倍。因此,对于#Workers <= #Ratings / #Items,旋转方法的性能要好得多,即,如果工人人数低于平均item学位。在我们使用的所有数据集中,度小的item被忽略了,因为这些并不代表好的建议,可能只是噪音,所以平均项度很大。我们将在下面更多地讨论ALS。
  2. 倾斜item度:这不再是一个问题,user顶点是唯一做处理的,item从来没有保存关于他们的user评分的信息。
  3. SGD的计算:这与顺序解是一样的,因为在任何时间点上都只有一个版本的特征向量,而不是将它们的副本发送给许多工作者,并在此基础上进行更新。

使用ALS的计算比使用SGD要复杂,因为为了更新一个user/item,我们需要它的所有item/user特征向量。更新在ALS实际上是我们解决一个类型的矩阵方程a * X = B,一个是X # #功能特征矩阵和B是1 X #特征向量,并根据user/ a和B是计算item特征向量形成所有已知的评分item/user。因此,在更新item时,我们不仅可以旋转它们的特征向量,还可以旋转A和B,在每个#Workers超步骤中更新它们,最后计算新的特征向量。这会将网络流量增加到#Items * # features s2 * #Workers。根据所有数据维之间的比例,对于某些item,这比标准方法好,对于某些item,则不是。

这就是为什么我们的旋转方法和标准方法的混合可以得到更好的解决方案。通过在一定程度上查看item,在标准方法中,与之关联的网络通信量是degree * #Features,而在我们的轮换方法中,是#Workers * #Features^2。我们仍将使用标准方法更新degree < #Workers^ * #Features 的item,并将对所有更高级别的item使用轮换方法,从而显著提高性能。例如,对于100个双功能和50个worker,选择方法的项度限制在5000左右。

为了求解矩阵方程A * X = B,我们需要找到A-1的逆矩阵,为此我们使用了开源库JBLAS,它对矩阵逆有最有效的实现。

由于SGD和ALS具有相同的优化公式,也可以将这些算法进行组合。ALS在计算上比SGD更复杂,我们包含了一个选项,即对SGD进行若干次迭代,然后对ALS进行一次迭代。对于一些数据集,这有助于离线度量(例如,均方根误差或平均秩)。

我们遇到了大程度item的数字问题。有几种方法可以绕过这个问题(忽略这些item或对它们进行取样),但是我们使用的是基于item和user级别的正则化。这使得user和item向量的值保持在一定的数值范围内。

5. Evaluation data and parameters

为了度量推荐的质量,在运行实际的A/Btest之前,我们可以使用现有数据的一个示例来计算一些离线指标,这些指标反映了我们的估计与实际user首选项之间的差异。上述两种算法都有许多超参数需要通过交叉验证进行优化,以获得最佳推荐,我们还提供了其他选项,如添加user和item偏差。

输入评分可以分为两个数据集(训练和测试)。在测试数据由所有培训实例之后的时间间隔内的所有user操作组成的情况下,这一点非常有用。否则,为了构建测试数据,我们随机选择每个user的T=1个item,并将它们与训练分开。在算法中,对于一定比例的user,我们对所有未评分的item(即,并观察培训和测试item在建议的排名列表中的位置。然后我们可以评估以下指标:指的是平均排名(位置在排名列表中,平均超过所有测试item)、精密位置1/10/100,意味着所有测试item的平均精度(MAP),等。另外我们计算均方误差(RMSE),而放大的贡献之间的绝对误差预测和真正的价值。为了帮助监控结果的收敛性和质量,在每次迭代之后,我们都会打印所有这些指标。

在一个有350亿加权训练评分和2亿测试评分的样本数据集上,下图显示了RMSE是如何在#Features=8或#Features=128的训练和测试集上减少的,而其他参数是固定的。

文章翻译:Recommending items to more than a billion people(面向十亿级用户的推荐系统)

6. Item recommendation computation

为了得到所有user的真实推荐,我们需要为每个user找到预测评分最高的item。在处理庞大的数据集时,检查每个(user、item)对的点积是不可行的,即使我们将问题分发给更多的workers。我们需要一种更快的方法来找到每个user的前K个推荐,或者一个很好的近似。

一种可能的解决方案是使用球树数据结构来保存item向量。球树是一种二叉树,其中叶节点包含item向量的某个子集,每个内部节点定义一个球,该球包围其子树中的所有向量。使用查询向量和球内任何向量的点积的上界的公式,我们可以做贪婪树遍历,首先到更有前途的分支,并修剪不能包含比我们已经找到的更好的解决方案的子树。这种方法比查找每一对数据快10-100倍,可以在合理的时间内完成对数据集的搜索。我们还添加了一个选项,允许在寻找TOP建议时出现指定的错误,从而进一步加快计算速度。

另一种近似求解该问题的方法是基于item特征向量对item进行聚类,将问题简化为寻找TOP cluster推荐,然后根据TOP cluster提取实际item。这种方法加快了计算速度,同时略微降低了基于实验结果的推荐质量。另一方面,集群中的item是相似的,通过从每个集群中获取有限数量的item,我们可以得到一组不同的建议。注意,我们在Giraph之上也有k-means集群实现,将这一步合并到计算中非常容易。

7. Comparison with MLlib

Spark MLlib是一个非常流行的机器学习库,它包含该领域中领先的开源实现之一。2014年7月,Databricks团队在Spark上发布了ALS实现的性能数据。实验是在亚马逊评论数据集的按比例复制的基础上进行的,该数据集最初包含3500万个评分,运行了5次迭代。

在下面的图,我们相比rotational hybrid方法(我们在Giraph中实现的)的标准方法(火花MLlib中实现,包括一些额外的优化,比如发送一次最多特征向量机),在相同的数据集。由于硬件差异(我们每台机器的处理能力的2倍),为了使一个公平的比较我们看总CPU分钟。旋转混合溶液大约快10倍。

文章翻译:Recommending items to more than a billion people(面向十亿级用户的推荐系统)

此外,使用标准方法进行实验的最大数据集有35亿个评分。使用旋转混合方法,我们可以轻松处理超过1000亿的评分。请注意,结果的质量对于这两种情况都是相同的,并且所有性能和可伸缩性收益都来自不同的数据布局和减少的网络流量。

8. Facebook use cases and implicit feedback

我们在Facebook的多个应用程序中使用了这种算法,例如推荐你可能喜欢的页面或你应该加入的群组。如前所述,我们的数据集由10多亿user组成,通常有数千万个items。实际上有更多的页面或组,但是我们将自己限制在通过某个质量阈值的item上——最简单的版本是item度大于100。(有趣的是,另一方面,我们有一些非常大的页面——“Facebook for Every Phone”页面实际上被Facebook近一半的user喜欢。

我们的第一个迭代包括页面喜欢/组连接作为积极信号。Facebook上的负面信号并不常见(负面信号包括不喜欢某个页面或在一段时间后离开某个群组)。而且,这并不意味着user对该item有负面反馈;相反,他或她可能对主题或接收更新失去了兴趣。为了获得好的推荐,需要从集合中未评分对中添加负面item。以前的方法包括从未评分的item中随机抽取负的训练样本(导致有偏差的非最优解),或者将所有未知的评分都视为负的,这极大地增加了算法的复杂性。在这里,我们实现了添加随机负面评分考虑user和item度(负面评分比例添加到user基于item度分布),和权衡负面评分不到积极的,我们没有一个好的模型学习与统一的随机抽样方法。

另一方面,我们有来自user的隐式反馈(user是否在积极查看页面、喜欢或评论组中的帖子)。我们还为隐式反馈数据集实现了一种著名的基于als的算法。这种方法没有尝试直接对评分矩阵建模,而是将数据视为二进制首选项和置信度值的组合。然后,评分与观察到的user首选项的可信度有关,而不是与对item的显式评分有关。

在运行矩阵分解算法之后,我们还有一个Giraph工作,即为所有user计算TOP推荐。

下面的代码展示了使用我们的框架、优化参数和插入不同数据集是多么容易:

CFTrain(
ratings=CFRatings(table='cf_ratings'),
feature_vectors=CFVectors(table='cf_feature_vectors'),
features_size=128,
iterations=100,
regularization_factor=0.02,
num_workers=5,
)
CFRecommend(
ratings=CFRatings(table='cf_ratings'),
feature_vectors=CFVectors(table='cf_feature_vectors'),
recommendations=CFRecommendations(table='cf_recommendations'),
num_recommendations=50,
num_workers=10,
)

此外,可以通过扩展SGD或ALS计算来简单地实现其他目标函数(例如秩优化或邻近模型)。

9. Scalable CF

推荐系统正在成为预测user偏好的重要工具。我们的矩阵分解和计算TOP user推荐的框架能够有效地处理Facebook拥有1000亿评分的海量数据集。它很容易使用和扩展其他方法。

我们正在考虑许多改进和算法,包括:

  • 结合社交图和user联系,提供一组更好的推荐
  • 从以前的模型出发,代替随机初始化,进行循环学习
  • 自动参数拟合与交叉验证,以优化不同指标的给定数据集
  • 尝试更好的分区和跳过机器,这些机器在旋转过程中不需要某些item数据

我们正积极致力于推荐和Giraph之上的许多其他应用程序,所以请继续关注这个领域中更令人兴奋的特性和开发。