*您的可爱不保证,但我们会尝试
**JAX是实验性的,不是官方支持的Google产品。在可能的情况下,用容器化来锁定代码的版本。

关于术语的简要说明

在Ok188bet金宝搏官网cupid上,当你告诉我们你喜欢谁以及你,好吧,不喜欢,我们在后端记录为“票”。在其余部分,我们将谈论“选民”和“投票”分别引用您和您(不)喜欢的人。每种都喜欢或通过是一个“投票”。

我们录制了很多选票!

动机

(如果您已经了解协作过滤和SVD近似,请随时跳至实施部分)

我们希望能够告诉你,鉴于你的过去和喜欢的历史,你会喜欢谁,你还没有看到呢?如果我们有这个,我们可以利用它来向你展示你更喜欢的人,也就是说,你喜欢他们,他们会喜欢,也许他们喜欢你,希望一切都会从那里开始!

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

所以让我们说我们有选民A,B和C,我们已经从B和C上录制了一些投票,这是来自A的B的投票,以及B在B上的投票。

我们的互动矩阵现在看起来像这样,选民作为列和投票是列的行:

一种 B. C
一种 药方: 0. 1
B. 1 药方:
C 0. 药方:

简单,现在我们的任务是尝试弄清楚如何在这里“填写”问号,例如如何“填写”问号。C票票吗?

那么,我们如何这样做?好吧,我们可以尝试根据他们喜欢和通过的人来寻找类似的选民。一旦我们这样做了,我们就可以将这些类似的选民的投票带到他们的选民失踪的投票中。

这被称为基于内存的协作过滤,虽然理论上很好,但计算上的可伸缩性不强。当我们有数以百万计的投票者和选民每周都要投上数亿张票的时候,当一个用户需要一个推荐列表的时候,要做到这一点是非常困难的!

学习陈述代替

所以,让我们采取替代方法,并近似这些相似之处。

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

所以,我们想要例如。点(a_vector,b_vector)= 0点(a_vector,c_vector)= 1。这里的一个重要说明是,正如你要注意的那样,我们想要点(A\u向量,B\u向量)!=dot(B_向量,A_向量)。这实际上并没有工作(Dot产品是换向的),因此我们将为每个用户录制两个向量 - 选票向量(矢量)和选民矢量(a_voter_vector.).

有了这个,我们就有了点(a_voter_vector,b_votee_vector)= 0点(B\u votee\u向量,A\u votee\u向量)=1

对不起,B,A对你没那么感兴趣。

好的,现在我们已经确定我们需要向量。向量里面有数字。但是,我们如何准确地计算出哪些数字呢?有那么多的数字我们可以填满他们!

好吧,我们可以从我们观察到的选票中得知数字!我们将使用梯度下降来实现这一点,可以描述如下:

  1. 随机初始化每个人的投票者和votee向量
  2. 对于一些数量的“时代”(通过观察到的投票),我们将经过我们观察到的每投票,以及
    1. 计算选民的选民矢量和选票的选票矢量之间的点产品
    2. 计算点积和实际结果之间的差异(误差)
    3. 拿着坡度错误的定义
      • 这告诉我们向量中的每个数字对错误的贡献有多大
      • 我们可以用它作为“方向”来移动矢量
    4. 通过从向量中减去梯度来相反的方式移动
      • 这将减少(下降) 错误
      • 值得注意的是,我们不必通过投票来进行投票,相反,我们可以通过投票一次做一堆批处理这些计算。
  3. 返回我们为每个选民和选票学到的向量。

所以,一旦完成了,我们可以用这些向量来做什么?那么现在,当我们想知道选民喜欢选票时,我们只需拍摄各自的向量的DOT产品,并找出模特认为会发生的事情!

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

但是为什么我们要特别做这个过程呢?我跳过了很多过程,但从本质上讲,整个过程都是通过近似奇异值分解(SVD)来重建选民votee交互矩阵的。如果你有兴趣阅读更多关于这一部分的内容,我建议你读一下这本书Netflix奖帖子很多这项工作都源于此。

为了简单起见,我们省略了一些我们在实践中使用的东西,比如偏差、正则化、无序化等等。

规模问题

当我们想弄清楚一个用户是否会喜欢另一个用户时,这会快得多,但是训练过程仍然需要我们循环并对每个投票进行梯度更新。虽然这些梯度更新相对便宜,但它们加起来有数亿张选票可供训练!

实施

因此,我们需要快速实现这一目标。为了让每个人的建议都很新鲜,我们每天都希望计算(并重新计算这些向量。此外,我们不希望只是这种简单的操作陷入困境 - 我们希望能够尝试使用不同的误差函数,培训方法,优化器等来研究不同的方法来更新向量。

我们在那里遇到了很少的图书馆,它实现了这种基本算法。下面,我们将介绍我们的古老最爱,然后谈谈为什么我们使用JAX建立自己的方法。

底线-惊喜

当我们第一次开始这个项目时,我们使用了流行的惊喜尼古拉斯拥抱图书馆。虽然惊喜提供了许多推荐算法,但我们专门对其实施感兴趣SVD.。尽管是Python库,但它使用花哨的C级Python扩展实现所有关键零件。这使得它比纯Python快得多。

但是,如果我们提出一些复杂的新错误功能,我们必须手动计算相应的渐变函数。除了纯粹的懒惰,我们希望避免这种情况是它是一个可以引入很多难以诊断错误和难以维护代码的过程。

有很多框架是用来自动计算梯度的(例如TensorFlow,PyTorch),但是我们能在保持效率的同时获得这种灵活性吗?

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

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

它还远不止这些,因为它提供了许多方法来优化我们的代码,使用即时编译和矢量化,以及其他整洁的特性。如果你感兴趣的话,我建议你仔细阅读他们的文章文件

导入Jax Import Jax.Numpy作为JNP

因此,让我们定义我们的点产品的函数。简单。

def dot_product(Voter_vector,Votee_vector):返回jnp.dot(Voter_vector,Votee_vector)

现在我们来定义误差函数。现在,我们将使用平方欧几里德距离实际结果与预测结果之间。这通常称为L2损失。

def l2_loss(Voter_vector,Votee_vector,Real_outcome):返回0.5 * jnp.power(real_outcome  -  dot_product(Voter_vector,Votee_vector),2)

现在,我们将在我们培训的批量票据上平均损失我们正在使用的培训jax.vmap文件函数转换以获得批量的最终损失功能。jax.vmap文件这里使用以允许我们有效地在输入的每一行上运行功能。

def丢失(Voter_vectors,Votee_vectors,Real_outcomes):返回JNP.mean(JAX.VMAP(L2_LOSS)(Voter_vectors,Votee_vectors,Real_outcomes))

这使得我们可以给出两个矩阵,每个矩阵都是形状(batch_size,vector_size)(用于选民向量和选民向量),以及形状向量(batch_size,)(对于真正的结果)到损失功能。然后,此函数将返回一个标量值,该值平均表示预测结果时向量当前的“错误”程度。

那么,现在我们如何得到梯度呢?我们将使用JAX.GRAD.功能如下:

grad_fn = jax.jit(jax.grad(丢失,argnums =(0,1)))

这将产生一个函数,当给出与相同的参数一样损失函数,返回与投票者u向量精氨酸0)和VOU矢量精氨酸1). 此外,我们正在通过JAX.JIT.因此,一旦编译,我们就可以利用更快的执行!

所以,现在我们拥有所有这些,让我们把我们的训练循环放在一起。

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

(选民指数、选民指数、实际结果)

这些指数被映射(事先)以对应于相关的选民和投票中的投票Voter_matrix.Votee_matrix.

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

LEARNING_RATE=1e-3 def train_epoch(投票者矩阵,投票者矩阵,投票者指数,投票者指数,真实结果,批量大小):对于投票者ib,投票者ib,zip中的结果b(create_batch(投票者指数,大小=批量大小),create_batch(投票者指数,大小=批量大小),create_batch(真实结果,大小=批量大小)):,vector_size)#index part vorter_vectors_batch=vorter_matrix[vorter_ib]votee_vectors_batch=votee_matrix[votee_ib]#gradient computation part vorter_grad,votee_grad=grad_fn(vorter_vectors_batch,votee_vectors_batch,outcours#b)#现在让我们开始渐变步骤!#更新部分投票者矩阵[votter\u ib]=LEARNING\u RATE*votter\u grad votee\u matrix[votee\u ib]=LEARNING\u RATE*votee\u grad return votter\u matrix,votee\u matrix

让我们称之为这种方法JAX_NAIVE.. 那么,这是怎么回事?

别理那些洗牌的东西,那可不划算

哎呀!即使是少量的投票,我们也花了更多的时间而不是惊喜!我们可以做得更好吗?

得快(呃)

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

来自Jax.ops Import Index_Add Learning_rate = 1E-3 @ Jax.JIT Def Train_Batch(Voter_Matrix,Votee_Matrix,Voter_ib,Votee_IB,Outcomes_B):#这些是(Batch_size,Vector_Size)#index部分Voter_Vectors_batch = Voter_matrix [Voter_ib] Votee_vectors_batch = Votee_matrix [votee_ib]#梯度计算部voter_grad,votee_grad = grad_fn(voter_vectors_batch,votee_vectors_batch,outcomes_b)#更新部new_voter_matrix = index_add(voter_matrix,voter_ib,-LEARNING_RATE * voter_grad)new_votee_matrix = index_add(votee_matrix,votee_ib,-LEARNING_RATE * votee_grad)返回new_voter_matrix,new_votee_matrix def train_epoch(Voter_matrix,Votee_Matrix,Voter_Indices,Votee_Dinces,real_outcomes,Batch_size):#ceight_batch刚刚收益率Batch_size为zip中的python itable的python itable itable(create_batch(voter_indices,size = batch_size),create_batch(Votee_indices,size= batch_size),create_batch(real_outcomes,size = batch_size)):Voter_matrix,Votee_matrix = train_batch(Voter_matrix,Votee_matrix,Voter_ib,Votee_ib,Outcomes_B)返回Voter_Matrix,Votee_Matrix
对不起,这太多了

嗯不错。所以,几个笔记 - 怎么了JAX.OPS.INDEX_ADD.?为什么我们为此分配到的结果新的\u矩阵

JIT编译JAX函数的关键限制之一是我们不能拥有任何就地操作。这意味着修改其输入的任何功能都无法正常工作。有关为什么,请参阅他们文件

这个版本,我们称之为Amateur_Jax_Model.,衡量?

嗯,我们还是走得太慢了。惊喜轻而易举地打败了我们!

我们还可以在JIT编译的函数中填充什么?

我应该注意我们正在使用batch_size.对于这些比较为1。这不是因为这必然是一个很好的批量大小,但这是因为这就是这就是惊喜允许 - 所以我们想确保我们的比较是公平的。

得快(est)

现在我们正在复制(记住,没有就地改变)整个选民矩阵和选票矩阵进入JIT编译的函数,即使我们只是更新(最多)batch_size.排!当我们有数以百万计的选民和选民,这可能会变得昂贵。

但如果我们可以填充整个Train_epoch.进入一个jit编译的功能?

从jax.实验从jax导入循环从functools导入lax import partial@partial(jax.jit文件,static_argnums=(5,6))def train_epoch(vorter_矩阵、votee_矩阵、vorter_索引、votee_索引、real_结果、批处理数据集_大小、批处理大小):带循环。范围()作为s:s.vorter\u matrix=vorter\u matrix s.votee\u matrix=votee\u matrix for batch\u index在s.range(0,批处理数据集大小)中:批处理开始=批处理索引*批处理大小#批处理部分投票者Šib=松懈(投票者索引,(批量开始,),(批量大小,))投票者索引ib=松懈(votee\u索引,(batch\u start,),(batch\u size,))结果\u b=松懈(实际结果,(批量开始,),(批量大小,))#update part s.voter_matrix,s.votee_matrix=train_batch(s.voter_matrix,s.votee_matrix,voter_ib,votee_ib,outcours_b)return s.voter_matrix,s.votee_matrix

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

但一旦完成了,我们就可以用JAX.JIT.

然而,一些事情变得复杂,问题的根源是Batch_start.不断变化。这意味着我们不能使用典型的切片运算符Voter_indices [batch_start:batch_end]-我们必须使用lax.dynamic_slice.反而。此外,我们必须指定它batch_size.batched_dataset_size.是静态值,这意味着JAX将重新编译Train_epoch.如果他们改变了。所以,打电话的时候Train_epoch.我们现在必须计算我们的训练运行中的批次。简单渐变血统不是一个大的事情!

好的,所以,这是非常复杂的,让我像一个月的某个地方敲打我的头撞击键盘以获得正确的,但这种方法如何(我们将呼唤full_jax_model.) 比较?

在这一点上,其他的甚至都不值得一看

现在这就是我所说的速度!事实上,我们看到,随着我们击中数百万的选票,我们不会像惊喜那样放慢速度!我们甚至无法梦想与早期版本的模型进行这种比较!

利用JIT编译?

虽然即使只是一个时代,它也是一个令人印象深刻的加速,当我们开始训练超过一个时代,我们应该看到更加戏剧性的差异Train_epoch.编译,第一个后的后续调用应使用已编译的版本。

所以,与惊喜相比,我们应该看到jax每单时期的额外时间较少,对吧?让我们看一下,只有一个时代,距离每一个时期有多少次。

好吧,不太。事实证明,相对于一个训练时期,JAX版本在这里以类似的速度速度向下速度令人惊讶,但是做得更好。然而,在绝对条件下,我们每欧时都会增加速度的数量级!

JAX Land Vs. Numpy Land

有一件事在整个过程中都没有说得很清楚,那就是Train_epoch.没有返回numpy.ndarray.对象,但是Jax.Interpreters.xla.deviceArray.对象。虽然这些在很大程度上是类似的API,但在分析JAX模型及其JIT编译的方法时,人们必须小心。

从根本上说,虽然它可能看起来像计算完成的时候Train_epoch.(内部部分)被调用,你回来的结果实际上是期货。所以,如果我们想在Numpy Land中与它们一起工作或打印它们或任何有用的东西,我们必须“提取”它们。我们可以看到,如果我们衡量运行所需的时间Train_epoch.在同一个矩阵上几次,所花费的时间似乎并不随时间的增加而增加!

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

为什么不使用tensorflow或pytorch?

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

  • 由于我们需要为每个选民和每种选票培训向量,我们无法缩小我们的培训集,因此需要快速迭代它
  • TensorFlow和PyTorch都没有对此进行优化
    • tf.数据专注于加快大型功能集进入训练循环的过程,但这不是我们拥有的问题
  • 我们也对优化大量不同参数感兴趣
    • 神经网络倾向于每次优化相同的参数
    • 我们在这里处理的是在每一步更新的少量参数,但是来自一个大的参数矩阵

经过大量的研究和我们的尝试失败,我们确定了我们在Tensorflow或Pytorch中实现这种表现。然而,自从我们看那是一年以来,如果有人知道其他人可以自由地让我知道我有多错了!

别在家里试这个

回到2019年12月,当Covid-19是'异常肺炎'而JAX版本为1.55时,我建立了这种方法,并得到了这些结果。

过去一周,当我写这篇文章并意识到我有一些结果我想进一步重新运行和探索,我有一个可怕的发现。我的时序结果无法再复制!事实证明,JAX库(我认为)在此方法中发生了显着减慢了这种方法。

这看起来不太好

我们已经提起票为此,但在此期间,如果要重现这些结果,您可以使用以下版本jax.numpy.

JAX == 0.1.55 JAXLIB == 0.1.37 NUMPY == 1.17.5

结论和进一步工作

While this was definitely an improvement on our baseline, it is still a rather basic form of collaborative filtering for recommendations - but it’s worked out well for us so far, allowing us to train these vectors for every voter and votee seen in the past week over the entire OkCupid site within three hours or so on average!

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

但是,我们该怎么办呢?

替代训练回路和损失函数

除了在另一个或随机之后,还有各种有趣的方式来命令除了一个之外的培训。通常,像BPR和扭曲这样的方法使用模型的当前性能来选择它没有正确的“硬”的例子。这些方法可能有所帮助,并且是未来为我们工作的方向。

神经协作过滤

NCF也是一个有希望的方法,允许我们利用哦那么赞扬深度学习做一个非线性的版本。我们在等主线pytrees支持循环。范围然而,这是最近才加上的。

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

更好的优化方法

我们用于将梯度转化为新矢量的方法是相当粗糙的,并且有很多替代方案显示承诺 - 但是,最终依靠或通过上述Pytrees支持来显着更容易。