Free Will

推荐系统 | 召回06:双塔模型

双塔模型的结构

下面开始讲双塔模型的结构。我们先来看用户的特征,我们知道用户ID还能从用户填写的资料和用户行为中获取很多特征,包括离散特征和连续特征。


所有这些特征不能直接输入神经网络,而是要先做一些处理,比如用embedding层把用户ID映射到一个向量。跟之前我们讲过的离散特征的做法相同,用户还有很多离散特征,比如所在城市感兴趣的话题等等。用embedding层把用户的离散特征映射成向量。

对于每个离散特征,用单独一个embedding层得到一个向量,比如用户所在城市,用一个embedding层,用户感兴趣的话题,用另一个embedding层

对于性别这样类别数量很少的离散特征,直接用one hot编码就行,可以不做embedding。

用户还有很多连续特征,比如年龄、活跃程度、消费金额等等。

不同类型的连续特征有不同的处理方法,最简单的是做归一化,让特征均值是零,标准差是一。有些长尾分布的连续特征需要特殊处理,比如取log,比如做分桶。

做完特征处理,得到很多特征向量,把这些向量都拼起来输入神经网络。神经网络可以是简单的全连接网络,也可以是更复杂的结构,比如深度交叉网络。神经网络输出一个向量,这个向量就是对用户的表征。做召回用到这个向量,

同理,物品的特征也是用类似的方法处理。用embedding层处理物品ID和其他离散特征,用归一化取对数或者分桶等方法处理物品的连续特征,把得到的特征输入一个神经网络。

双塔模型

下面这样的模型就叫双塔模型,

尤其要注意:本模型直接拿用户表征rep和物品表征rep去融合,史称后端特征融合模型!!这是召回的用法,绝对不能提前concat再灌入神经网络又去映射一个打分rate(那是精排的做法!)

左边的塔提取用户的特征,右边的它提取物品的特征,跟矩阵补充模型相比,双塔模型的不同之处就在于使用了ID 之外的多种特征,作为双塔的输入,两个塔各输出一个向量记作a和b,两个向量的内积就是模型最终的输出rate,它即预估用户对物品的兴趣。现在更常用的输出方法是余弦相似度。

两个塔的输出,分别记作向量a和b,余弦相似度意思是两个向量夹角的余弦值,它等于向量内积除以a的二范数,再除以b的二范数,其实就相当于先对两个向量做归因化,然后再求内积余弦,相似度的大小介于负一到正一之间。

双塔模型的训练方法

第一种是pointwise训练,独立看待每个正样本和负样本,做简单的二元分类训练模型,把正样本、负样本组成一个数据集。在数据集上做随机梯度下降训练双塔模型。

第二种训练双塔模型的方式是parawise,每次取一个正样本,一个负样本组成一个二元组。损失函数用triplely hing loss或者triple logistic loss。可以参考下面这篇Facebook发的论文。《Jui-Ting Huang et al. Embedding-based Retrieval in Facebook Search. InKDD, 2020.》

第三种训练双塔模型的方式是listwise,每次取一个正样本和多个负样本组成一个list。训练方式类似于多元分类,参考下面的文献二,这是Youtube发的论文,《Xinyang Yi et al. Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations. In RecSys, 2019.》训练的时候要用到正样本和负样本。

正负样本选择

正样本很简单,就是用户点击过的物品,用户点击过一个物品,就说明用户对这个物品感兴趣,负样本的意思是用户不感兴趣的。

那么显然,在实践中负样本的选择是比较讲究的,有几种看起来比较合理的负样本:

  • 比如没有被召回的
  • 召回了,但是被粗排,精排淘汰的
  • 还有曝光了,但是用户没有点击的

该用哪种?下一篇文章我们再详细讲解如何选择正负样本,感兴趣的话,可以自己阅读刚才提到的两篇论文,论文讲解了Facebook Youtube如何选择正负样本,我们小红书基本是照着做的,再加一些自己的小技巧,取得了很好的效果。

三种训练双塔模型的方式

Pointwise训练

第一种是pointwise训练,pointwise是最简单的训练方式,我们把召回看作简单的二元分类任务,

正样本意思是历史记录显示用户对物品感兴趣,对于正样本,我们要鼓励向量a和b的cos相似度接近+1;负样本的意思是用户对物品不感兴趣,对于负样本,我们要鼓励向量a和b的cos相似度接近-1

这就是个很典型的二元分类问题。如果做pointwise训练,可以把正负样本的数量控制在1 : 2或者1 : 3。我也不知道为什么,但是互联网大厂的人都这么做,这算是业内的经验。

Pairwise训练

第二种训练双塔模型的方式是pair wise,做训练的时候,每一组的输入是一个三元组,包括一个用户ID和两个物品ID。

左边的物品是这样的,就是用户感兴趣的物品。右边的物品是负样本,是用户不感兴趣的物品,把用户的特征和物品的特征各自做变换,然后输入神经网络,最终输出三个向量,用户的特征向量记作a,两个物品的特征,向量记作B加和B减两个物品,

特征变换映射物品的embedding结构是相同的,里面的embedding层和全连接层都用一样的参数

分别计算用户对两个物品的兴趣,用户对正样本的兴趣是向量a和向量b加的余弦相似度,这个值越大越好,最好是接近+1。

用户对负样本的兴趣是向量a和向量b减的余弦相似度,这个值越接近-1越好。

做pair wise训练的基本想法是让用户对正样本的兴趣尽量大,对负样本的兴趣尽量小。也就是让向量a和b加的运行相似度大于向量a和b减的余弦相似度,而且两者之差越大越好。

我们来推导一下损失函数,我们希望看到用户对正样本的兴趣很大,而对负样本的兴趣很小,最好是前者比后者大M这么多。

这个M是个超参数,需要调,比如设置成1,如果前者比后者大了M,那么就没有损失,否则如果用户对正样本的兴趣不够大,没有比负样本的兴趣大M这么多,就会有损失。损失等于cos(a,b减)加上M,再减去cos(a,b加)这个loss是正数,需要让它降到0才行。这样就推导出了triple hinge loss。

如果熟悉孪生网络sign is network,应该见过这种损失函数。训练的时候每个训练样本都是个三元组,向量a是用户的表征,B加和B减分别是物品正样本和负样本的表征。

我们希望损失函数越小越好,训练的过程就是对损失函数求最小化,用梯度更新双塔神经网络的参数。

Triplely hinge loss只是一种损失函数,还有别的损失函数起到同样的作用。

triplet logistic loss是这样定义的,其实就是把logistic函数作用到这一项最小化,Logistic函数会鼓励这一项尽量小,也就是让$cos(a,b^-)$尽量小,让$cos(a,b^+)$尽量大,跟上面的Triplely hinge loss道理是一样的,都是让用户对正样本的兴趣分数尽量高,用户对负样本的兴趣分数尽量低。这里的sigma是个大于零的超参数控制损失函数的形状,Sigma需要手动设置。

我们已经推导出了Triplely hinge loss和Triplely logistic loss,可以通过最小化这样的损失函数来训练双塔模型。训练样本都是三元组,其中一个用户,一个正样本物品,还有一个负样本物品。

Listwise训练

第三种训练双塔模型的方式是list wise。做list wise训练的时候,每次取一个正样本和很多负样本,需要一个用户把它的特征向量记作a。


取一个正样本,意思是历史记录显示用户喜欢这个物品,把这个物品的特征向量记作B加,还需要取N个负样本,把它们的特征向量去做B1减到BN减。
做训练的时候要鼓励a和正样本b+的余弦相似度尽量大,鼓励a和负样本bn-的余弦相似度尽量小。

下面我演示一下list wise训练具体怎么做。

向量a和$b^+$的余弦相似度是个介于负一到正一之间的实数,意思是用户对正样本物品兴趣的预估分数。这个分数越大越好,最好是接近正一,其余的cos相似度对应负样本,它们是用户对N个负样本兴趣的预估分数,这N个分数越小越好,最好是接近负一,把这N个负样本的分,和正样本的分,全部输入soft max激活函数【它就是将整体归一化为0–1的函数】,激活函数输出N+1个分数,这些分数都介于零到一之间,最左边的分数S加对应正样本,我们希望这个分数越大越好,最好是能接近1。右边的N个分数对应负样本,我们希望这些分数越小越好,最好都接近零。

上图中,$y+=1$是正样本的标签,意思是鼓励$S+$接近1,负样本的标签是$Y_1^-$到$Y_N^-$,把它们都设置成0,意思是鼓励$S_1^-$到$S_N^-$都接近0,我们用Y和S的交叉熵作为损失函数。

训练的时候,最小化交叉熵,意思是鼓励soft max,输出的预测值S,尽可能接近标签Y,其实交叉商就等于负的log s加【上面公式】

训练的时候最小化交叉商也就是最大化S加,这就等价于最大化正样本的余弦相似度,最小化负样本的余弦相似度。

三种训练方法的本质都是让用户和正样本物品的余弦相似度大,而用户跟负样本的余弦相似度小。

我已经讲完了双塔模型和三种训练方式,最后总结一下这本文讲过的内容,

总结

双塔模型,顾名思义有两个塔,一个用户塔,一个物品塔,两个塔各输出一个向量,向量的余线相似度就是对兴趣的预估值,这个值越大,用户就越有可能对物品感兴趣。

有三种训练双塔模型的方式:

一种是pointwise,每次用一个用户和一个物品,物品可以是正样本也可以是负样本,

第二种训练方式是pair wise,每次取一个用户一个这样本物品,一个负样本物品,训练的目标是最小化Triplely hinge loss和Triplely logistic loss,也就是让正样本的余弦相似度尽量大,与负样本余弦相似度尽量小。

第三种训练方法是list wise,每次取一个用户,一个正样本物品,很多个负样本物品,训练的时候用soft max激活函数,用交叉熵损失函数训练,也是鼓励正样本余弦相似度尽量大,负样本余弦相似度尽量小。

  • 召回是后端特征融合,而精排是前端特征融合

在结束之前,我们讨论一种错误的召回模型的设计,大家一看到这种结构就应该知道这是粗排或者精排的模型,而不是召回的模型,这种模型没办法应用到召回。下面这块结构跟双塔模型是一样的,都是分别提取用户和物品的特征,得到两个特征向量,但是上层的结构就不一样了,这里直接把两个向量做concat。然后输入一个神经网络,神经网络可以有很多层,这种神经网络结构属于前期融合。在进入全连接层之前就把特征向量拼起来了。

看一下精排模型图中的神经网络,神经网络最终输出一个实数作为预估分数,表示用户对物品的兴趣,把两个特征向量拼起来输入神经网络,这种前期融合的模型不适用于召回。

这种前期融合的神经网络结构跟前面讲的双塔模型有很大区别,双塔模型属于后期融合,两个塔在最终输出相似度的时候才融合起来,前面压根没用神经网络去输出相似度,假如把这种精排的模型用于召回,就必须把所有物品的特征都挨个输入模型,预估用户对所有物品的兴趣。

假设一共有1亿个物品,每给用户做一次召回,就要把这个模型跑1亿次,这种计算量显然不可行。如果用这种模型,就没办法用近似最近邻查找来加速计算。这种模型通常用于精排排序,即从几千个候选物品中选出几百个,计算量不会太大。

以后大家一看到这种前期融合的模型,就要明白这是排序模型,不是召回模型。召回只能用双塔那样的后期融合模型。

okay,今天讲解了双塔模型的神经网络结构和三种训练的方式。做训练的时候需要选择正负样本,这部分内容下文我再详细给你说。

选对正负样本的作用大于改进模型结构

双塔模型需要用到正样本和负样本,选对正负样本的作用大于改进模型结构。

首先来讨论如何选择正样本,正样本比较好选:

如果物品给用户曝光之后,用户有点击行为,就说明用户对物品感兴趣,把用户和物品二元组作为正样本。

但是选取正样本的时候有个问题需要解决,大家应该听说过二八法则,就是少部分物品占据了大部分点。正样本是有点击的物品,所以正样本大多属于热门物品,拿过多的热门物品作为正样本,会对冷门物品不公平,这样会让热门物品更热,冷门物品更冷。

解决方案是对冷门物品做过采样,或者对热门物品做降采样。过采样的英文是up sampling,意思是让一个样本出现多次。降采样的英文是down sampling,意思是抛弃一些样本。

比如一定概率抛弃热门物品,抛弃的概率与样本点击次数正相关。一会细说

前面咱们讲过,推荐系统的链路分为召回、粗排、精排、重排,稍后要用到这里。

再简单回顾一下,召回是从物品的数据库中快速取回一些物品,比如小红书有上亿篇笔记,用几十条召回通道各取回一两百篇笔记,一共取回几千篇笔记,然后用粗排和精排给召回的笔记逐一打分,保留分数最高的笔记,最后一步是重排,这一步主要是做多样性采样,还会用很多规则做调整,最终有几十篇笔记被选中曝光给用户。

负样本咋搞?

下面我们讨论怎样选取训练双塔模型的负样本,负样本就是用户不感兴趣的物品,也就是链路上每一步被淘汰的物品,召回模块儿从几亿物品中选出几千个。被召回的物品只是极少数,这些没被召回的几亿个物品可以看作是负样本。

粗排和精排,从几千个召回的物品中选出几百个,也就是说几千个物品会被这一步淘汰,最终有几十个物品曝光给用户,这些没有被曝光给用户的就是负样本。

当然了,不是每个曝光的物品都会被点击,曝光了,但是用户没有点击的也可以视作是负样本。

不过一定要小心,这仨不能同时用于某一个阶段:比如曝光未点击的千万不能用于召回!一会给你说为啥!

接下来我们分别讨论这三类负样本

简单负样本

首先是简单负样本:简单负样本是未被召回的物品,这种物品通常是用户不感兴趣的,几亿个物品里面只有几千个被召回,也就是说几乎所有的物品都没有被召回,那么未被召回的物品跟全体物品基本上是一回事儿,所以直接在全体物品中做随机抽样就可以了。抽到的物品作为负样本。

全局均匀抽样or非均匀抽样

当然,问题在于怎样做抽样?是均匀抽样还是非均匀抽样?

均匀抽样的坏处是对冷门物品不公平。前面讨论过二八法则,少部分物品占据了大部分点击,这会导致这样本大多是热门物品。

如果在全体笔记中做均匀抽样生成负样本,那么负样本大多是冷门物品,总拿热门物品做正样本,冷门物品做负样本,对冷门物品会不公平,这样会让热门物品更热,冷门物品更冷,


所以负样本抽样要随机非均匀。这样可以打压热门物品抽样的概率与热门程度正相关,热门物品成为负样本的概率大。物品的热门程度可以用它的点击次数来衡量。可以这样做,抽样每个物品的抽样概率正比于它点击次数的0.75次方。0.75是个经验值。

刚才介绍的简单负样本是从全体物品中抽的。

batch内负样本

下面我介绍另一种简单负样本,叫做batch内负样本。

图中是一个batch内的样本,左边是用户,右边是物品,中间的箭头表示用户点击过的物品。

图中任意一个样本都是正样本,比方说第二个用户点击过第二个物品,这就说明用户对物品感兴趣。这个二元组是个正样本。

什么是batch内负样本?这里举个例子,第一个用户跟第二个物品可以组成一个负样本。第一个用户跟第三个物品也可以组成一个负样本,设一个batch内有N个正样本,那么一个用户跟N-1个物品可以组成负样本,所以这个batch内一共有$N×(N-1)$个负样本,这些都属于简单负样本。

对于第一个用户来说,第二个物品就相当于是从全体物品中随机抽样的第一个用户大概率不会喜欢第二个物品。

batch内负样本存在一个问题,我来分析一下,图中这些二元组都是通过点击行为选取的,第一个用户和第一个物品之所以成为一个正样本,原因是用户点击了物品,所以一个物品出现在batch内的概率正比于它的点击次数,也就是它的热门程度。

这样就会有个问题,前面讨论了物品成为简单负样本的概率应该是正比于点击次数的0.75次方,但是在这里我们做batch内负采样,物品成为负样本的概率正比于它点击次数的1次方,有点过大了。

重复一遍,抽样概率本应该正比于点击次数的0.75次方,但这里实际上是1次方。

也就是说,任何物品成为负样本的概率过大,一个物品成为负样本的概率越大,模型对这个物品打压就会越狠,对负样本应该打压,但这里打压的太狠了。这样会造成偏差。这篇Youtube论文讲了如何修正偏差,《Xinyang Yi et al. Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations. In RecSys, 2019.》

我们在小红书照着做了,确实拿到了收益。

我具体讲一下如何修正偏差。我们把物品$i$被抽到的概率记作$p_i$,它正比于物品$i$的点击次数,反映出物品的热门程度。

双塔模型通常用余弦相似度预估用户对物品I的兴趣分数rate,Cos中的a是用户的特征向量,bi是物品i的特征向量,训练的时候要鼓励正样本的余弦相似度尽量大,鼓励负样本的余弦相似度尽量小。

根据Youtube论文的建议,训练双塔模型的时候,应该把$cos(a,b_i)$调整为$cos(a,b_i)-log(p_i)$,这样可以纠偏,人为地把这个负样本的概率减掉,避免过分打压热门的物品,

训练结束之后在线上做召回的时候,还是用原本的余弦相似度$cos(a,b_i)$,线上召回的时候不用做这种调整,不用减 $log(p_i)$。

刚才我讲了两种简单负样本,一种是从全体物品中做随机抽样生成负样本,另一种是在batch内生成负样本。

困难负样本

接下来我要介绍困难负样本,困难负样本是被排序淘汰的物品,比如物品被召回,但是被粗排淘汰,比方说召回5000个物品进入粗排,粗排按照分数做截断,只保留前500个,那么被淘汰的4500个物品都可以被视作困难负样本。

为什么被粗排淘汰的物品叫做困难负样本?

这些物品被召回,说明他们跟用户兴趣多少有些关系,被粗排淘汰说明用户对物品的兴趣不够强,所以分成了负样本。被正负样本做二元分类的话,这些困难负样本容易被分错,容易被错误的判定为正样本。

更困难的样本是通过了粗排,但是在精牌中分数排名靠后的物品,比方说精牌给500物品打分,排名在后300的物品都算是极度困难的负样本。能够通过粗排进入精排,说明物品已经比较符合用户兴趣了,但未必是用户最感兴趣的,所以在精排中排名靠后的物品可以被视为极度困难负样本训练。

双塔模型其实是个二元分类任务,让模型区分正负样本,

把全体物品作为简单负样本,分类准确率会很高,因为他们明显跟用户兴趣不符。

被粗排淘汰的物品也是负样本,但他们多少跟用户兴趣有些相关,所以比较困难,分类准确率会稍微低一些。

精排分数靠后的物品也可以视作是负样本,这些物品跟正样本有些相似,所以他们很容易被判定为正样本,对他们做分类非常困难。

工业界比较常用的做法是把简单负样本与困难负样本混合起来作为训练数据。


比如50%是简单负样本,就是从全体物品中随机非均匀抽样出来的,
另外50%是困难负样本,也就是从粗排和精排淘汰的物品中随机抽样出来的。

错误的负样本

前面解释了简单负样本和困难负样本,最后讨论一种常见的错误,很多人以为可以把曝光但是没有点击的物品作为负样本。其实这是错误的,如果你训练双塔模型的时候,用一些这样的负样本,效果肯定会变差,工业界的人紧踩过这个雷,把这个作为教训。我再解释一遍,这种错误的负样本是什么意思,看一下这张图:

物品1–80这些都是重排之后曝光给用户的物品。既然都曝光给用户了,这所有80个都认为是用户很感兴趣的了,你咋能做召回的负样本呢?通过一些技术手段,我们得知用户只看了前五个物品曝光了。也就是说前五个物品曝光给了用户,第6到80物品没有曝光,在曝光的物品里面,第一个和第五个被用户点击,其余三个物品没有被用户点击,有人觉得可以把这三个物品当做负样本,因为他们有曝光,但是没有点击。

我再强调一遍,这种想法是错误的,训练召回模型 不能用这样的负样本,
原因我稍后解释,这种负样本不是给训练召回模型用的。而是给训练排序模型用的。

训练排序模型的时候确实要用这种曝光但是没有点击的物品作为负样本。我解释一下选择负样本的原理。召回的目的是快速找到用户可能感兴趣的物品,凡是用户可能感兴趣的全都取回来,然后再交给后面的排序模型逐一做甄别。召回模型的任务是区分用户不感兴趣的物品和可能能感兴趣的物品,
而不是区分比较感兴趣的物品和非常感兴趣的物品,这是排序干的事,这就是选择负样本的基本思路。

可以把全体物品当做负样本,把它们叫做简单负样本,这些物品绝大多数都是用户根本不感兴趣的,双塔模型很容易区分,这些负样本被召回,然后被粗排精排淘汰的叫做困难负样本,这些物品能被召回,说明他们跟用户的兴趣有一定的相关性,被排序模型过滤掉,说明用户对这些物品的兴趣不够强,他们可以作为排序模型的负样本,这样的负样本跟正样本有点像,做分类的时候比较难以区分,所以算是困难负样本

有曝光但是没有点击的物品看起来可以作为负样本,但其实不能,只要用了这种负样本,双塔模型的效果肯定会变差,

为什么?

一个物品能够通过精牌模型的甄别,最终曝光给用户,说明物品已经非常匹配用户的兴趣点了,每次给用户展示几十个物品,用户不可能每个物品都点击,而曝光了没有点击不代表不感兴趣,可能只是用户对别的物品更感兴趣就点击了别的,或者是用户感兴趣,只是碰巧没有点击

曝光但是没有点击的物品已经算是非常匹配的了,甚至可以拿来做召回的正样本,不应该把曝光但是没有点击的物品作为召回的负样本,记住这一点。

再说一遍!!!

召回的目的是区分不感兴趣的和比较感兴趣的,
而排序才是区分比较感兴趣的和非常感兴趣的。

这类负样本只适用于训练排序模型,但不适用于训练召回模型。

这是工业界的共识,是通过反复做实验得出的结论。

总结

最后总结一下本文的内容,详细讲解了如何选择正负样本,正样本意思是曝光而且有点击的用户物品二元组。

简单负样本有两种,一种是从全体物品中做随机抽样,另一种是batch内负样本。

困难负样本是被召回但是被排序淘汰的物品。这些物品跟正样本比较像,做分类会有困难,所以叫做困难负样本。

我们还讨论了一种错误的做法,就是拿曝光但是未点击的物品作为召回的负样本,这种负样本只能用于排序,不能用于召回。

这样子的话,你就明白了负样本的本质了吧,不同的阶段负样本不同,排序的负样本可以作为召回的正样本

双塔模型的线上召回

今儿个我们继续学习双塔模型,主要内容是双塔模型的线上召回和更新,在训练好双塔模型之后,就可以把模型部署到线上做召回。比如在用户刷小红书的时候,快速找到这个用户可能感兴趣的一两百篇笔记,下面的内容就是双塔模型怎样做召回这是训练好的两个塔,他们分别提取用户特征和物品特征。

在训练好模型之后,在开始线上服务之前,先用右边的物品塔提取物品的特征,把物品特征向量记作b,小红书有几亿篇笔记,那么就得到几亿个向量b,把物品特征向量–物品ID这样的二元组保存到Oracle或者MySQL这样的向量数据库。

至于左边的用户塔,处理方式完全不一样,不要事先计算和存储用户向量,而是当用户发起推荐请求的时候,调用神经网络在线上线算一个特征向量a,然后把向量a作为query去数据库中做检索查找最近邻,

也就是跟向量a相似度(余弦先相似度就是兴趣rate评分)最高的K个红色向量,每个红色向量对应一篇笔记。K近邻查找一共召回了K篇笔记,作为这条召回通道的结果返回。

我已经讲完了双塔模型的召回,我再总结一遍,训练好双塔模型之后,在开始线上服务之前,先要做离线存储,用神经网络计算每个物品的特征向量B,把这几亿个物品向量存入向量数据库,比如⽐如Milvus、Faiss、HnswLib这样的开源系统。

向量数据库会建立索引,前面的文章我就讲过建索引就是把向量空间划分成很多区域,每个区域用一个向量表示,这样可以加速最近邻查找。在向量数据库建好索引之后,可以开始做线上召回。比如一个用户刷小红书的时候,双塔模型召回通道可以返回用户可能感兴趣的K个物品。

用户发起推荐请求,我们知道用户ID和用户画像,把这些信息输入用户塔神经网络算出用户向量a,然后在向量数据库中做最近邻查找,把用户的特征向量a作为query,调用Milvus、Faiss、HnswLib这种向量数据库做近似最近邻查找,数据库返回与用户向量a余弦相似度最大的K物品作为召回的结果。


接下来,这些物品会跟其他的等召回通道的结果融合,然后经过排序最终展示给用户。

我已经讲完了召回的内容,请大家思考一个问题,在开始线上召回之前,我们先要把物品向量B存储到向量数据库,但是我们不存储用户向量a,而是在线上用神经网络现算用户向量a。为什么要区别对待物品向量和用户向量?

道理是这样的,每做一次召回,只用到一个用户向量a,而要用到几亿个物品向量B,拿神经网络现算一个用户向量,计算量不大,算的起。但是几亿个物品向量显然是算不起的,所以我们不得不离线算好物品向量。

那么,能不能把几亿个用户向量也事先算好,把几亿个向量存储起来,进一步减少线上的计算负担?这样做当然可以,推荐团队早期基础很弱的时候通常会这样做,这样工程实现很简单,但这样不利于推荐的效果。

因为用户的兴趣点是动态变化的,所以应该在线上实时计算用户向量,而不是事先计算好存起来。事先存储用户向量效果不好,但事先存储物品向量是OK的,这是因为物品的特征相对比较稳定,短期内不会发生变化。

模型更新:全量更新和增量更新

刚才介绍的线上召回,在实践中还会更复杂一些,会涉及到模型的全量更新和增量更新。全量更新,意思是在每天凌晨用前一天的数据训练模型,比方说在今天凌晨就用昨天的数据训练模型。注意,是在昨天模型参数的基础上做训练,而不是重新随机初始化。

把昨天一天的数据打包成tf record的文件,在昨天模型参数的基础上做训练,把昨天的数据过一遍,每条数据只用一次,也就是训练只做one episode,训练完成之后发布新的用户塔神经网络,用户塔的作用是在线上实时计算用户向量。

作为召回的query还要发布新的物品向量,这几个向量存入向量数据库,向量数据库会重新建索引,然后可以在线上做最近邻查找。

全量更新的实现相对比较容易,对数据流和整个系统的要求不高。全量更新不需要实时的数据流,对生成训练数据的速度没有要求,延迟一两个小时也没有关系,只需要把每天的数据落表在凌晨做个批处理,把数据打包成tf record格式的文件就可以了。

全量更新对系统的要求也很低,每天做一次全量更新,所以只需要把神经网络和物品向量,每天发布一次就够了。

与全量更新相对的是增量更新,也就是做online learning,更新模型参数,每隔几十分钟就把新的模型参数给发布出去。

为什么要做增量更新?这是因为用户的兴趣随时会发生变化,举个例子,早上刷小红书的时候看到几篇有意思的笔记,也就是说我产生了新的兴趣点。我在中午刷新小红书,小红书能不能根据我新的兴趣点做推荐?

如果模型是做天级别的全量更新肯定是不行的,想要让模型在用户行为发生几小时之内就做出反应,模型需要做到小时级别的增量更新。

增量更新对数据流的要求很高,需要实时收集线上的数据,并且对数据做流式处理,实时生成训练模型用的tf record文件。然后对模型做online learning做梯度下降,更新ID embedding的参数,也就是说从早到晚训练数据文件不断生成,不断做梯度下降更新模型的embedding层

注意,Online learning 不更新神经网络其他部分的参数,全连接层的参数都是锁住的,不做增量更新,只更新embedding的参数,只有做全量更新的时候才会更新全连接层。

至于为什么只更新embedding层参数,不更新全链接层参数,主要是出于工程实现的考量,道理一两句话也解释不清楚。

对模型更新之后,再把算出的用户ID embedding给发布出去,用户的ID embedding是一个哈希表的形式,
给定用户ID可以查出ID embedding向量,发布用户ID embedding的目的是为了线上计算用户的特征向量,最新的用户ID embedding可以捕捉到用户的最新的兴趣点,对推荐很有帮助。

发布用户ID embedding,这个过程会有延迟,在我们小红书刚上线online learning的时候,这个过程会有小时级的延迟,通过对系统做优化,延迟可以降低到几十分钟甚至更短。也就是说,用户在小红书上产生行为,几十分钟之后,他的用户向量就会被更新。他再次刷新小红书的时候,双塔模型会考虑到他最新的兴趣。

图示全量更新和增量更新

在我们小红书模型既要做全量更新,也要做增量更新,我画个图演示一下,

这些是前天一天积累的数据,到了昨天凌晨的时候,我们把前天的数据打包成tf record的文件,要基于前天凌晨全量训练出来的模型做训练,也就是说昨天凌晨模型初始化的时候,参数用的是前天凌晨全量训练出的模型,而不是随机初始化。然后我们拿前天积累的数据来训练模型,要把前天的数据做random shuffle打乱。然后做随机梯度下降,只训练1 epoch,也就是说每条数据只过一遍。

接下来需要基于这个全量训练出来的模型做分钟级别的增量更新。从昨天凌晨到今天凌晨,不停做online learning,每隔几十分钟发布一次模型,刷新线上的用户塔的顶层参数,

在昨天我们又就积累了一天的数据,到了今天凌晨又该做一次全量更新。今天凌晨的全量更新是基于昨天凌晨全量训练出来的模型,而不是用下面增量训练出的模型。

在完成这次全量训练之后,下面增量训练出的模型就可以扔掉了,然后再基于今天凌晨全量训练出的模型,做分钟级别的增量更新,从今天凌晨到明天凌晨,不停做online learning,每隔几十分钟发布一次模型。

大家思考一个问题,能不能只做增量更新,不做全量更新?

什么意思?就是去掉上面的全量更新,接着昨天的增量更新训练,继续把增量更新给做下去。这样只做增量更新,当然比既要做增量也要做全量简单工程实现会更容易,训练消耗的机器资源也更少。

我们同时把两种都试过,发现只做增量的话效果不好,最好还是既要做全量,也要做增量。

为什么只做增量更新效果不好?如果你只看一个小时的数据,它是有偏的,它的统计值跟全天的数据差别很大

为什么有偏?在不同的时间段,用户的行为是不一样的,比如中午和傍晚的数据明显会不一致,如果你只看五分钟的数据,那么偏差就更大了,它跟全天数据的统计值差别巨大,我们做全量训练的时候要随机排列数据,也就是randen shuffle,这样就是为了消除偏差。全量更新是在random shuffle过的数据上做,而增量更新删掉数据,从早到晚的顺序做训练,同样使用一天的数据。

这两种排列数据的方式会导致训练的效果有差别,把数据按照从早到晚的时间顺序排列,效果不如把数据随机打乱。所以全量训练的效果比增量训练更好。

这就是为什么我们既要做增量训练,也要做全量训练,全量训练的模型更好,而增量训练可以实时捕捉用户的兴趣。

以实时捕捉用户的兴趣。

总结

刚刚我们详细讲解了双塔模型,最后总结一下,双塔模型,顾名思义有两个塔,一个用户塔,一个物品塔,两个塔各输出一个向量,两个向量的余弦值就是对兴趣的预估,这个值越大,用户就越有可能对物品感兴趣。

越有可能对物品感兴趣。

前面的课介绍了三种训练双塔模型的方式,分别是point wise,pair wise, list wise。做训练的时候要用到正负样本,正样本是用户点击过的物品,负样本稍微复杂一些,简单负样本是全体物品,是从全体物品中做随机抽样,困难负样本是被排序淘汰的物品。

做完训练,把物品塔输出的物品向量存储到向量数据库里面,供线上做最近邻查找

用做线上召回的时候拿到用户ID和用户画像,调用训练好的用户塔神经网络现算用户的向量a,然后把用户向量a作为query,查询物品的向量数据库,找到余弦相似度最大的K物品向量,返回这K物品ID作为召回结果,模型需要定期做全量更新和实时做增量更新,

全量更新,意思是在今天凌晨用昨天的数据训练整个神经网络做1epoch随机梯度下降,也就是每条数据只用一遍。

增量更新,意思是用最近产生的数据训练神经网络,训练的时候只更新embedding层,锁住全连接层不更新

现在先进的推荐系统都会结合全量更新和增量更新。每天做全量更新,实时做增量更新,每隔几十分钟要发布最近的用户embedding,供用户他在线上计算用户向量用。这样的好处是可以捕捉到用户最新的兴趣点

从双塔模型的原理,到线上召回的过程,我已经讲得很清楚很明了。最近这几篇文章我想你应该都熟悉了。



应统联盟


连接十万名应统专业同学


阿药算法


打通算法面试任督二脉