大规模协同过滤,预测OkCupid上谁会喜欢你*,与JAX**188bet金宝搏官网

佛罗里达州杰克逊维尔的照片,没有关系。

*不能保证你的受欢迎程度,但我们会尝试

** JAX是实验性的,不是官方支持的谷歌产品。尽可能使用容器化对代码进行版本锁定。

术语简介

在Ok188bet金宝搏官网Cupid上,当你告诉我们你喜欢谁,不喜欢谁时,我们会在后端记录为“投票”。接下来,我们将讨论“投票者”和“投票者”,分别指代你和你(不)喜欢的人。每一个点赞或通过都是一次“投票”。

我们记录了很多选票!

动机

(如果您已经了解了协作过滤和SVD近似,请直接跳到实现部分)

我们想知道,考虑到你去世的历史和你的喜好,你会喜欢哪些你还没见过的人?如果我们有了这个,我们就可以利用它向你展示你更可能喜欢的人,也就是说你喜欢他们,他们也会喜欢你,希望一切顺利!

理解我们如何处理这一问题的第一步是将所有这些投票放入“互动矩阵”中。本质上,这个矩阵的行是“投票者”,列是“投票者”。当一个选民喜欢一个被投票的人,我们在这个(选民,被投票的人)对对应的坐标上放一个1,如果这个选民通过了一个被投票的人,我们放一个0。

假设我们有选民A, B和C,我们记录了一些来自A对B和C的投票,来自B对A的投票,来自C对B的投票。

我们的互动矩阵现在是这样的,投票者作为行,投票者作为列:

忽略奇怪的格式

很简单,现在我们的任务是试着找出如何“填写”这里的问号,例如,C将如何投票给A?

那么,我们该怎么做呢?我们可以试着找一个和C相似的选民,根据他们喜欢谁,把他传给谁。一旦我们这样做了,我们可以把那些相似的选民的选票推断出我们的选民的缺失选票。

这被称为基于内存的协同过滤,虽然理论上很好,但在计算上却不是很可伸缩。当我们有数百万的投票者,并且每周有数亿的投票者进行投票时,当用户需要快速的推荐列表时,这是非常困难的!

学习表示相反

所以,让我们采用另一种方法,来近似这些相似之处。

为此,我们将用一个向量表示每个投票者和被投票的人。对于我们来说,向量就是一个固定长度的数字数组。我们想从这些向量中得到的是,当我们对它们进行点积(即,将每个数与另一个向量中相应的数相乘,取其和)时,它对应于我们观察到的(或将观察到的)选民与被选民交互时的结果。

所以,我们想要。点(A_vector, B_vector) = 0点(A_vector, C_vector) = 1.这里有一个重要的注意,你会注意到,我们想要点(A_vector, B_vector) !=点(B_vector, A_vector).这实际上是行不通的(点积是可交换的),所以我们将为每个用户记录两个向量——一个投票向量(A_votee_vector)及选民向量(A_voter_vector).

有了这个,我们就有点(A_voter_vector, B_votee_vector) = 0点(B_voter_vector, A_votee_vector) = 1

抱歉,B, A只是没那么喜欢你。

好的,现在我们已经确定了我们需要向量。向量里面有数字。但是,我们怎么算出具体是哪些数字呢?有很多数字可以填满它们!

好吧,我们可以从我们观察到的投票中知道数字!我们将使用梯度下降来做这个,它可以描述如下:

  • 随机初始化每个投票者和被投票者向量
  • 对于一些“时代”(跳过观察到的投票),我们将通过我们观察到的每一票,并且
  • *计算投票人的投票人向量和被投票人的投票人向量之间的点积
  • *计算点积和实际结果之间的差异(误差)
  • *取梯度的错误
  • **这告诉我们矢量中的每个数字对错误的贡献有多大
  • **我们可以将此作为移动矢量的“方向”
  • *从矢量中减去梯度,向相反方向移动
  • **这将减少()错误
  • **值得注意的是,我们不必逐个投票,而是可以一次做一堆批处理这些计算。
  • 返回我们为每个投票人和被投票的人学习的向量。

(作者注:Medium没有嵌套列表,但原始平台有,抱歉)

一旦这个完成了,我们能对这些向量做什么?现在,当我们想知道一个选民是否喜欢一个选民时,我们可以简单地对他们各自的向量做点积,看看模型认为会发生什么!

这是一个非常快的操作,我们可以卸载118金宝搏app下载 提供推荐,我们的用户越多,速度也不会越慢!

但我们为什么要做这个过程呢?我跳过了很多过程,但从本质上讲,整个过程是通过近似奇异值分解(SVD)来重建选民与选民之间的交互矩阵。如果你对这部分感兴趣,我建议你读一下开创性的部分Netflix大奖后这是很多研究成果的来源。

为了简单起见,我们省略了偏见、规则化、洗牌等我们在实践中使用的东西。

规模问题

虽然当我们想要弄清楚一个用户是否会喜欢另一个用户时,这种方法会快得多,但训练过程仍然需要我们循环并对每个投票进行梯度更新。虽然这些渐变更新相对便宜,但它们累积了数亿的选票!

实现

所以,我们需要尽快实现这一点。为了保持每个人的建议新鲜,我们需要每天计算(并重新计算)这些向量。此外,我们不想仅仅被这个简单的操作所困住——我们希望能够尝试和研究使用不同的错误函数、训练方法、优化器等来更新向量的不同方法。

我们已经遇到了很多实现这个基本算法的库。下面,我们将介绍我们最喜欢的方法,然后讨论为什么使用JAX构建我们自己的方法。

基线-惊喜

当我们开始这个项目时,我们使用了流行的惊喜尼古拉斯·哈格(Nicolas Hug)著。虽然Surprise提供了许多推荐算法,但我们特别感兴趣的是它的实现圣言会.尽管它是一个Python库,但它使用奇特的c级Python扩展实现了所有关键部分。这使得它比纯Python快得多。

然而,如果我们提出一些复杂的新误差函数,我们就必须手动计算相应的梯度函数。除了纯粹的懒惰,我们想要避免这个过程,因为它可能会引入许多难以诊断的bug和难以维护的代码。

许多框架都是用来自动计算梯度的(例如TensorFlow, PyTorch),但我们能否在保持Surprise效率的同时实现这种灵活性呢?

什么是JAX吗?那不是在佛罗里达吗?

JAX提供了一个简单的Python接口来表达(使用类似numpy的API)我们想要的操作,同时还允许我们将这些函数转换为它们的梯度返回版本!

除此之外,它还提供了许多方法来优化代码,包括使用just - in - time编译和向量化,以及其他整洁的特性。如果你感兴趣,我建议你多读一些文档

我们来定义做点积的函数。很简单。

现在我们来定义误差函数。现在,我们用的是平方欧氏距离真实结果和预测结果之间的差距。这通常被称为L2损耗。

现在,我们要对这批选票的损失进行平均我们要用函数变换来得到这批选票的最终损失函数。jax.vmap是用来让我们在每一行输入上有效地运行函数。

这允许我们给出两个矩阵,每个矩阵都有形状(batch_size vector_size)(用于投票人和被投票人向量),以及形状向量(batch_size)(为了真正的结果)损失函数。然后,该函数将返回一个标量值,该值表示,在预测结果时,向量当前的“错误”程度。

那么,现在我们如何得到梯度呢?我们将使用jax.grad函数为:

这将产生一个函数,当给定与损失函数,返回两个梯度矩阵voter_vectorsargnum0)和votee_vectorsargnum1).此外,我们正在运行所有这些函数jax.jit这样,一旦它们被编译,我们就可以利用更快的执行!

那么,现在我们有了所有这些,让我们把训练循环放在一起。

对于我们的数据,我们将以元组的形式进行投票,如下所示:

(votee_index voter_index real_outcome)

这些索引被(预先)映射,以使投票者和被投票者与相关的行相对应voter_matrixvotee_matrix

所以我们的训练循环是这样的:

我们称之为方法jax_naive.那么,这是怎么做到的呢?

忽略掉那些乱七八糟的东西,那些没有被淘汰

呵!即使是少量的投票,我们花的时间也比惊喜要多!我们能做得更好吗?

要走了快(er)

好吧,那显然不太顺利。如果我们回想一下,我们是否可以尝试使用JAX的类似傻瓜的API来jit编译大量的索引和更新部分?那样会更快吗?

抱歉,太多了

好的,很酷。那么,几个要点,你怎么jax.ops.index_add?为什么要把结果赋值给new_voter_matrix

jit编译JAX函数的一个关键限制是我们不能有任何限制就地操作.这意味着任何修改其输入的函数都将不起作用。想了解更多原因,请查看他们的文档

这个版本,我们称之为amateur_jax_model,测量了?

好吧,我们还是太慢了。惊喜就在眼前!

我们还可以在jit编译的函数中添加什么?

我应该注意到我们用了abatch_size1的比较。这并不是因为这是一个好的批量大小,而是因为这是Surprise所允许的—所以我们想确保我们的比较是公平的。

要走了快(美国东部时间)

现在,我们正在每批将整个投票者矩阵和投票者矩阵复制到jit编译的函数中(记住,没有就地更改),即使我们只是(最多)更新batch_size行!当我们有数百万的选民时,这可能会变得昂贵。

但如果我们能把整个train_epoch在一个jit编译的函数中?

哇,所以,这看起来很不同,但同时又很相似!这多亏了实验JAX循环API,它允许我们编写一个看起来有状态的循环,通过JAX将其转换为一个就地(或纯)函数。循环中所有的可变状态都必须放在循环。范围对象。

但一旦完成,我们就可以用jax.jit

然而,这里有一些事情变得复杂,问题的根源是batch_start在不断变化发展的。这意味着我们不能使用典型的切片操作符voter_indices [batch_start batch_end):-我们必须使用lax.dynamic_slice代替。此外,我们还必须指定它batch_sizebatched_dataset_size是静态值,这意味着JAX将重新编译train_epoch如果他们改变。所以,当调用train_epoch我们现在必须计算在我们的训练运行中有多少批。对于简单的梯度下降来说不是什么大问题!

这很复杂,我花了大概一个月的时间用头撞键盘才弄好,但这是怎么做到的呢(我们称之为full_jax_model)比较?

其他的都不值得一看

这才是我所谓的速度!事实上,当我们获得数千万选票时,我们并没有像Surprise那样放慢速度!我们做梦也想不到能与早期版本的模型相比达到这么高的水平!

利用JIT编译?

即使只有一个纪元,这也是一个令人印象深刻的加速,当我们开始训练一个以上的纪元时,我们应该看到更显著的差异train_epoch,第一次调用之后的后续调用应该使用已编译的版本。

因此,与Surprise相比,JAX在每个epoch上花费的额外时间要少得多,对吧?让我们看看经过几个纪元后每一个都比只有一个纪元慢了多少倍。

嗯,不完全是。事实证明,相对于一个训练时期,这里的JAX版本速度与Surprise相似,但要好一些。然而,从绝对意义上说,我们增加的是每个纪元更少的秒的数量级!

JAX领域与numpy领域

有一件事一直没有弄清楚,那就是train_epoch不返回numpy.ndarray对象,而是jax.interpreters.xla.DeviceArray对象。虽然它们有很大程度上相似的api,但在分析JAX模型及其jit编译方法时,必须小心。

基本上,当它看起来像是计算完成时train_epoch(内部部分)调用时,返回的结果实际上是未来。所以,如果我们想在numpy中使用它们或打印它们或其他有用的东西,我们必须'extract'它们。如果我们测量运行所需的时间,就能看到这一点train_epoch在相同的矩阵上做几次,看起来所花费的时间并不会随着纪元的数量而增加!

然而,一旦我们提取了这些数据,我们可以看到所花费的时间在增加。因此,分析您的JAX代码时要小心!你可以阅读更多关于异步调度的内容在这里

为什么不用TensorFlow或PyTorch呢?

虽然TensorFlow和PyTorch对于很多梯度下降驱动的问题来说都非常棒,但其中有一些关键部分他们处理得不好。

  • 因为我们需要为每个投票人训练一个向量,所以我们不能对训练集进行下采样,因此需要一种快速的方法来遍历它
  • TensorFlow和PyTorch都没有对此进行优化
  • 我们还对优化许多不同的参数感兴趣
  • 神经网络每次都趋向于优化相同的参数
  • 我们在这里处理的是在每一步更新的少量参数,但是是从一个很大的参数矩阵

经过大量的研究和失败的尝试,我们认为在TensorFlow或PyTorch中没有可行的方法来实现这种性能。然而,我们已经研究了一年,如果有人知道,请让我知道我错了!

不要在家里尝试

早在2019年12月,COVID-19是“异常肺炎”,JAX版本是1.55,我构建了这种方法,并获得了这些伟大的结果。

上周,当我写这篇文章的时候,我意识到我有一些想要重新运行和进一步探索的结果,我有一个糟糕的发现。我的计时结果不能再复制!结果,JAX库中发生了一些变化(我认为),显著降低了该方法的速度。

看起来不太好

我们已经申请一张票对于这一点,但同时,如果你想复制这些结果,你可以用以下版本的jaxnumpy

jax = = 0.1.55
jaxlib = = 0.1.37
numpy = = 1.17.5

结论与进一步工作

虽然这绝对是一个改善我们的基线,这仍然是一个相当基本形式的协同过滤推荐——但这是迄今为止对我们都挺好的,让我们这些向量训练每一个选民,而在过去一周在整个OkCupid网站平均在3小时左右!188bet金宝搏官网

今年,我们利用这一点为所有用户提供了显著的改进建议。

但是,我们该怎么办呢?

替代训练循环和损失功能

有很多有趣的方法来排列训练集而不是一个接一个或者随机排列。通常,像BPR和WARP这样的方法会使用模型的当前性能来选择它没有得到正确的“困难”示例。这些方法可能是有用的,也是我们今后工作的方向。

神经协同过滤

NCF也是一种很有前途的方法,允许我们利用这一点深度学习做一个非线性的版本。我们正等着被注射毒品pytrees支持循环。范围然而,这是最近才加入的。

没有这种支持,管理NCF所需的所有不同状态将是一场噩梦。

更好的优化方法

我们将梯度转换成新的向量的方法是相当粗糙的,而且有很多替代方法显示出希望——但是,最终还是依赖于前面提到的pytrees支持,或者被大大简化了。

最初发表在118博宝娱乐官方网站 2021年2月2日。

188bet金宝搏官网OkCupid科技博客

阅读来自OkCupid工程团队的故事,每天连188bet金宝搏官网接着数百万人

188bet金宝搏官网OkCupid科技博客

188bet金宝搏官网OkCupid的工程团队负责每天为数百万人配对。在OkCupid科技博客上阅读他们的故事188bet金宝搏官网

188bet金宝搏官网OkCupid科技博客

188bet金宝搏官网OkCupid的工程团队负责每天为数百万人配对。在OkCupid科技博客上阅读他们的故事188bet金宝搏官网