双塔模型范式原理与结构解析
推荐系统的架构是候选物品集合、召回、粗排、精排到重排的一个流程,目前这个架构已经比较成熟,如图 1 所示。
典型的信息流推荐是当我们在刷某个APP的时候,主页会推荐一些相关的信息,这些信息就像流水一样不停的给你展现文章或视频。因此,引出一个问题:为什么推荐系统的信息能做到千人千面?
我们以某头条为例,假设它的后台数据库里存储有 1000 万篇文章可以进行展示,同时假定用户固定,那么把这 1000 万篇文章都与该用户的喜好程度算一个匹配的分值。
按照分值大小进行排序,匹配度高的放在前面,否则放在后面。当用户请求过来时,将匹配分值大的文章优先进行展示。
为什么后来演化出了召回、粗排、精排和重排呢?主要是因为用户请求是实时的,用户什么时候请求就什么时候计算匹配度,那么将 1000 万篇文章与用户的匹配分值全算一遍就会造成十分巨大延迟与卡顿,导致用户等不及推送便退出软件。
或许你可能会想离线计算,在离线阶段把这 1000 万篇文章与用户的匹配分值全算一遍,用户请求过来的时候就直接进行展示。但用户的兴趣是会变化的,还是需要根据用户最近的一些行为来推送相关内容。
那么依旧需要实时计算,性能依旧承受不了,这时我们可以考虑将量级先降下来,降为 1 万,这样就可以抽象出一层,叫做召回层,目的是快速的从 1000 万篇文章里面挑出来1万篇用户喜欢的内容。
当然速度快必定会造成精度的一些损失,因此保留下来的 1 万篇文章里可能存在用户不喜欢的内容,之后再计算剩下 1 万个内容与用户的匹配程度,然后按匹配分值的大小进行排序。
但1万篇文章也无法实时计算,这是因为后续的精排模型比较复杂。这有两条路线可以考虑,一是在召回层后面加个粗排层,这个粗排层和前面的召回层有一点相像之处,但比召回层更精细一点,就是再过滤到只有 1000 个给精排层,这个量级精排层就可以实时运算了。
还有一个方案是不要粗排层,直接召回层过滤到只有 1000 个再传递给精排层。这样就精排层计算出 100 个跟用户匹配度最高的给重排层,然后推荐出用户最喜欢的几个显示在屏幕上。
所以这里需要注意两点,第一,召回层是必不可少的,必须兼顾准确度和性能。第二,粗排层是可有可无的。
在我们一开始搭建一个推荐系统的时候,召回和精排层必须要有,粗排层不一定有。所以很自然引出一个问题,那召回是怎么做到相对来说又快又比较准的呢?因此引出了咱们这个双塔模型,它是一个可以被广泛用来做召回的一个模型。
什么是双塔?它是左面一个塔,右面一个塔,然后在最上层将这两个塔的输出做一个交互,当然左塔和右塔的网络结构是任意的一种网络结构,如图 2 所示。
推荐系统里面这两个塔的结构基本上是 MLP 全连接层,可以简单的认为双塔都是 2-5 层的全连接。那问题在于左塔和右塔输入的东西不一样,当用户请求过来时,左塔输入 user 特征。
经过 MLP 之后输出 user 的表征,右塔输入 item 特征,即需要给用户展示的东西,然后也是经过 MLP 之后输出 item 的表征,之后两个算内积就可以得到与 user 相近的一个 vector。
那双塔模型的离线和在线计算分别是怎么做呢?首先左边输入 user 特征,右面输入 item 特征,这两个分别经过两个塔,最终做一个内积,得到一个分值,即模型的预估分,再根据用户到底喜不喜欢它,做一个 loss 再回传梯度,我就可以把这网络训练出来了。
这样有一个好处是在训练完毕之后,item 特征基本上都属于静态特征,即不会经常发生变化,然后这两个塔是分离的,它俩只在最上层做了一层交互。既然 item 的特征基本不变化,那它不需要实时获取。
只要训练完这个网络之后,将所有的item特征输入到右塔经过2-5层的全连接层,最终可以把 item的表征全都离线能获取到。离线获取到了之后就存到库里面,那在线用户请求过来的时候,只需要时时计算用户的表征就行。
经过几层的 MLP 全连接网络,就可以时时获取用户的表征向量,然后直接跟刚才离线的item表征向量做一个 top n 的检索。
双塔范式在召回中的妙用
双塔模型在召回层中的妙用是可以直接离线训练好,批量的生成 item_embedding,并存入 Faiss 里面。
当用户在线实时请求的时候,只需要直接计算一次用户的 user_embedding ,速度就会非常快。所以它优点是部署比较方便,双侧分离。
双塔方式在粗排中的妙用与工程优化
粗排是比较粗糙的排序,没有精排的精确度,模型结构没多复杂。YouTube 比较经典的排序的如图 3 所示。
YouTube 经典推荐系统是在底层把所有的特征都输入网络,经过 3 层全连接之后输出用户对于它们喜欢的分值。这样做的一个问题就在于底层把所有特征全输入网络,即把用户的特征输入网络,把 item 的特征也输入网络。
也就是说当用户只有一个时,但 item特征列表里面有多少个 item,用户的表征就用这种网络结构形式计算多少次,计算量大。
如果粗排采用双塔结构,那优点就是用户的表征就只需要算 1 遍,计算流程十分简单,当然离线训练是一样的过程。
我们所说的粗排是要在 1000 个里面再选出 100 个扔给精排,那么就要排序每一个用户跟每一个 item 的匹配分值。
获得了用户的 embedding 表征之后,item 的表征是可以实时计算的,当然也可以不实时计算,因为在召回的时候,已经把 item 的 embedding 存下来了,可以用于继续计算。
实际上粗排的计算量是算了一遍用户的 embedding 表征,item 的 embedding 也就 1000,然后和用户的 embedding 算 1000 次内积就行了,这是是很快的,不需要再过一篇 YouTube 那样的网络。因此粗排在双塔模型最根本的好处就在于只算一次。
为什么双塔好用?根本原因一个是用户和 item 分离计算,另外一个是在于它只算一次,包括在粗排里面。
我们想在粗排的时候只算了一次 user 的表征,在召回的时候也算了一次 user 的表征,那这两次计算可不可以合并?如果使用双塔结构的话,召回和粗排计算 user 的 embedding 表征完全可以合并成一次,那就在请求召回的时候同时把用户的 embedding 表征拿到。
这虽然也是双塔结构,但结构不一样。
所以在请求召回的时候,同时并发的去请求粗排的一个模块,实际上相当于把召回计算用户的 embedding 表征和粗排计算的用户的 embedding 表征做一个并行的计算,它俩的时间会进一步的减小,因此整个推荐的返回时间会进一步减小,那这就是双塔在工程里的一个优化点。
粗排速度大大的提升有一个好处是召回的量就可以扩大,比如粗排原来是从 1 万选出 1000,由于前面讲了召回的速度很快但精确度不高,所以它会把一些用户不喜欢的内容放进 1 万个里面。
为了尽量不错过用户喜欢的内容,假如我给粗排层扩成 10 万,相比于之前多了 9 万的内容,那这 9 万个里面是这个用户喜欢的概率会大大增加,但如果粗排的性能扛不住扩到 10 万的数据量,那么系统会崩掉。
但如果你采用双塔这种范式,它可以大大的加速工程的优化速度,你就可以去扩大召回层的数据量,那后面的效果会变得更好。
双塔结构缺陷与算法优化
双塔结构的缺点在于相比于YouTube网络结构,user的特征跟item特征的交互在网络输入的时候就已经交互了。特征交互早的好处就是可以根据use的特征和item特征能组合出来对用户推荐。所以双塔结构的一个缺点是交互的太晚,只有在最上层做了一次内积的交互。
怎么去改进这个缺点?第一个角度是交互过晚但又不能在底层就去交互,因为底层一交互就破坏了双塔的结构,前面讲的优点也消失了。
在MLP多层感知机的过程中,可能把用户的一些重要特征给淹没掉了,所以第一种方式是在MLP中间插入一些网络结构。
比如注意力机制或Resnet结构,通过这些结构自动的把一些表现比较明显且重要的特征提取出来进行学习,例如图4所示的SENet改进的双塔结构。
第二种思路是可以多加几个塔,如图5所示。比如user加一个塔,item再加一个塔,那两个塔再用这种方式做一遍交互,然后将交互结果给并起来。
当然这4个塔采用的网络结构形式不一样,但缺点是计算量太大。如果模型很复杂会导致计算量更大,没有一定的算力完全无法运转。
最后一个方式是把双塔改成度量学习的方式,来缓解样本不自信的问题。因为在召回层的网络里,样本实际上是按照正常的曝光点击和曝光没点击的方式去采样。
如果按照这样去进行正负样本定义的话,线上的样本和离线期间的样本存在不一致的现象,因此导致的问题就是在召回的时候,有很多不自信的问题,因为没展现出来的样本根本不知道是正样本还是负样本。