1 Deep Retrieval

经典的双塔模型把⽤户、物品表示为向量,线上做最近邻查找。Deep Retrieval 把物品表征为路径(path),线上查找用户最匹配的路径。Deep Retrieval 类似于阿⾥的 TDM。

1.1 索引

索引是把物品表征为路径,如下图所示:

  • 深度:depth=3depth =3
  • 宽度:width=Kwidth=K

把一个物品表示为一条路径(path),比如 [2, 4, 1]。一个物品可以表示为多条路径,比如 {[2,4,1],[4,1,1]}\{[2,4,1],[4,1,1]\}

之后建立两个索引,分别是物品到路径的索引和路径到物品的索引。其中一个物品可以对应多条路径,一条路径也可以对应多个物品。

1.2 预估模型

之后需要预估用户对路径的兴趣,用 3 个节点表示⼀条路径:path=[a,b,c]path =[a, b, c]

  • 给定用户特征 xx,预估用户对结点 aa 的兴趣 p1(ax)p_{1}(a \mid {x})
  • 给定用户特征 xxaa,预估用户对结点 aa 的兴趣 p2(ba;x)p_{2}(b \mid a; {x})
  • 给定用户特征 x,a,bx,a,b,预估用户对结点 aa 的兴趣 p3(ca,b;x)p_{3}(c \mid a,b; {x})

则可以得到用户对 path=[a,b,c]path =[a, b, c] 的兴趣为:

p(a,b,cx)=p1(ax)×p2(ba;x)×p3(ca,b;x)p(a, b, c \mid {x})=p_{1}(a \mid {x}) \times p_{2}(b \mid a ; {x}) \times p_{3}(c \mid a, b ; {x})

1.3 线上召回

线上召回的流程如下:

  1. 给定用户特征,用神经网络做预估,用 beam search(束搜索)召回⼀批路径。
  2. 利⽤索引,召回⼀批物品,查看索引 pathListitempath \rightarrow List〈item〉,每条路径对应多个物品。
  3. 对物品做排序,选出一个子集。

线上召回:userpathitemuser \rightarrow path \rightarrow item

1.3.1 束搜索

假设有 3 层,每层 KK 个节点,那么⼀共有 k3k^3 条路径。⽤神经⽹络给所有 k3k^3 条路径打分,计算量太大。而是用 beam search,可以减小计算量。beam search 需要设置超参数 beam size。

Beam Search(size = 1)

Beam Search(size = 4)

1.4 训练

1.4.1 学习神经网络参数

训练时同时学习神经网络参数和物品表征,把神经网络的输出 p(a,b,cx)p(a, b, c \mid \mathbf{x}) 用来预估用户对路径 [a,b,c][a, b,c] 的兴趣。把⼀个物品表征为多条路径 {[a,b,c]}\{[a, b, c]\},建⽴索引:

  • itemListpathitem \rightarrow List〈path〉
  • pathListitempath \rightarrow List〈item \rangle

正样本 (user,item):click(user,item)=1(user, item):click(user,item)=1。把物品表征为 JJ 条路径:[a1,b1,c1],,[aJ,bJ,cJ]\left[a_{1}, b_{1}, c_{1}\right], \cdots,\left[a_{J}, b_{J}, c_{J}\right],则用户对路径 [a,b,c][a,b,c] 的兴趣:

p(a,b,cx)=p1(ax)×p2(ba;x)×p3(ca,b;x).p(a, b, c \mid \mathbf{x})=p_{1}(a \mid \mathbf{x}) \times p_{2}(b \mid a ; \mathbf{x}) \times p_{3}(c \mid a, b ; \mathbf{x}).

如果用户点击过物品,说明用户对能够表示该物品的 JJ 条路径全部感兴趣。所以应该让以下公式变大:

j=1Jp(aj,bj,cjx)\sum_{j=1}^{J} p\left(a_{j}, b_{j}, c_{j} \mid \mathbf{x}\right)

因此定义损失函数,损失应该越小越好,所以应该加上符号:

loss=log(j=1Jp(aj,bj,cjx))loss =-\log \left(\sum_{j=1}^{J} p\left(a_{j}, b_{j}, c_{j} \mid \mathbf{x}\right)\right)

这个神经网络的作用是判断用户对路径有多感兴趣,如果用户点击过物品,则认为用户对物品的 JJ 条路径都感兴趣。

1.4.2 学习物品表征

除了训练上述神经网络的参数,还要学习物品的表征。用户 user 对路径 path=[a,b,c]path =[a, b, c] 的兴趣记作:

p(pathuser)=p(a,b,cx)p( path \mid user )=p(a, b, c \mid \mathbf{x})

物品 item 与路径 path 的相关性:

score(item,path)=user p(pathuser)×click(user,item)score(item, path) =\sum_{\text {user }} p (path \mid user ) \times click(user, item)

公式中的第一项是预估用户对路径的兴趣,第二项是用户是否点击(0或1)。根据 score(item,path)score(item, path) 选出 JJ 条路径作为 item 的表征。

以用户作为中间过渡,来计算物品和路径的关系。

之后选出 JJ 条路径 Π=path_1,,path_J\Pi=\\{\operatorname{path}\_{1}, \cdots, \operatorname{path}\_{J} \\},作为物品的表征。则损失函数(选择出与 item 高度相关的 path)为:

loss(item,Π)=log(j=1Jscore(item,path_j))\operatorname{loss}( item,\Pi)=-\log \left(\sum_{j=1}^{J} \operatorname{score}(item, \operatorname{path}\_{j} ) \right)

为了避免过多的 item 集中在⼀条 path 上,我们希望每条路径上的物品都比较平均,需要添加一个正则项,避免让它关联到更多的物品:

reg(pathj)=( number of items on path _j)4\operatorname{reg}\left(\operatorname{path}_{j}\right)=\left(\text { number of items on path }{ }\_{j}\right)^{4}

选中的路径有较高的分数 score(item,pathl)score(item, path _{l} ),而且路径上的物品数量不会太多。

1.5 总结

**召回:**用户 → 路径 → 物品

在做召回的时候给定用户特征 xx,用神经网络预估用户对路径 path=[a,b,c]path=[a,b,c] 的兴趣,分数记作 p(pathx)p(path|x)

用 beam search 寻找分数 p(pathx)p(path|x) 最高的 ss 条 path,之后利用索引 pathListitempath \rightarrow List〈item〉召回每条路径上的 nn 个物品。一共召回 s×ns \times n 个物品,对物品做初步排序,返回分数最高的若干物品。

**训练:**同时学习 用户—路径 和 物品—路径 的关系

一个物品被表征为 $J $ 条路径:path_1,,path_J\operatorname{path}\_{1}, \cdots, \operatorname{path}\_{J}。如果用户点击过物品,则更新神经网络参数,使分数增大。

j=1Jp(path_jx)\sum_{j=1}^{J} p\left(\operatorname{path}\_{j} \mid \mathbf{x}\right)

如果用户对路径的兴趣分数 p(pathx)p(path|x) 较高,且用户点击过物品 item,则 item 与 path 具有相关性。之后寻找与 item 最相关的 JJ 条 path,且避免一条路径上物品过多。


2 其他召回通道

2.1 GeoHash 召回

GeoHash 召回是一种地理位置召回,⽤户可能对附近发⽣的事感兴趣。GeoHash 对经纬度的编码,表示地图上⼀个长⽅形区域,之后以 GeoHash 建立索引,取回相关的优质笔记列表(按时间倒排)。

这条召回通道没有个性化,只是根据地理位置来进行召回。

如果用户允许 APP 获取用户定位,根据⽤户定位的 GeoHash,取回该地点最新的 kk 篇优质笔记。

2.2 同城召回

和上面的 GeoHash 相同,唯一的区别就是使用同一个城市内的内容进行召回。以城市为索引,建立优质笔记列表(按时间倒序),这条召回通道也没有个性化。

2.3 作者召回

如果你对一个作者感兴趣,那么系统就会给你推这个作者发布的其他内容。

系统维护2个索引:

  1. 用户 → 关注的作者
  2. 作者 → 发布的笔记

做召回时,按照用户 → 关注的作者 → 最新的笔记来进行召回。

2.3.1 有交互的作者召回

**有交互的作者召回:**如果⽤户对某笔记感兴趣(点赞、收藏、转发),那么⽤户可能对该作者的其他笔记感兴趣。

  1. **索引:**用户 → 有交互的作者
  2. **召回:**用户 → 有交互的作者 → 最新的笔记

2.3.2 相似作者召回

如果⽤户喜欢某作者,那么⽤户喜欢相似的作者。

  1. **索引:**作者 → 相似作者(kk 个作者)
  2. **召回:**用户 → 感兴趣的作者(nn 个作者)→ 相似作者(nknk 个作者)→ 最新的笔记(nknk 篇笔记)

2.4 缓存召回

主要想法是复⽤前 nn 次推荐精排的结果:精排输出几百篇笔记,送⼊重排,重排做多样性抽样,选出几十篇。其中,精排结果⼀⼤半没有曝光,被浪费。

把精排前 50 的物品,但是没有曝光的,缓存起来,作为一条召回通道。下次用户刷小红书的时候,作为一条召回通道再选出来。

由于缓存大小固定,需要退场机制。一些退场机制如下:

  1. ⼀旦笔记成功曝光,就从缓存退场
  2. 如果超出缓存⼤⼩,就移除最先进⼊缓存的笔记
  3. 笔记最多被召回 10 次,达到 10 次就退场
  4. 每篇笔记最多保存 3 天,达到 3 天就退场

2.5 总结

一共介绍了三大类一共六条召回通道,这些都是工业界一直在使用的,虽然比不上之前介绍的召回通道,但是也是很有效的召回方式。

  1. 地理位置召回:GeoHash 召回、同城召回
  2. 作者召回:关注的作者、有交互的作者、相似的作者
  3. 缓存召回

3 曝光过滤和 Bloom Filter

**曝光过滤问题:**如果用户看过某个物品,则不再把该物品曝光给该用户。并不是所有的物品都有曝光过滤问题,像 youtube 这种长视频,看过的内容可以再次观看。

想要做曝光过滤,需要对于每个⽤户,记录已经曝光给他的物品(⼩红书只召回 1 个⽉以内的笔记,因此只需要记录每个⽤户最近 1 个⽉的曝光历史)。对于每个召回的物品,判断它是否已经给该⽤户曝光过,排除掉曾经曝光过的物品。

⼀位⽤户看过 nn 个物品,本次召回 rr 个物品,如果暴力对比,需要 O(nr)O(nr) 的时间,计算量太大,所以在实际应用中不使用暴力对比,而是使用 Bloom Filter。

3.1 Bloom Filter

Bloom Filter 判断⼀个物品 ID 是否在已曝光的物品集合中。

  • 如果判断为 no,那么该物品一定不在集合中
  • 如果判断为 yes,那么该物品很可能在集合中(可能误伤,错误判断未曝光物品为已曝光,将其过滤掉)

所以使用Bloom Filter 进行过滤,那么一定不会把曝光过的物品再次给用户,但是有可能会误伤。

Bloom Filter 把物品集合表征为⼀个 mm 维⼆进制向量,取值为 0 或 1。每个⽤户有⼀个曝光物品的集合,表征为⼀个向量,需要 mm bit 的存储。Bloom Filter 有 kk 个哈希函数,每个哈希函数把物品 ID 映射成介于 0 和 mm 之间的整数

Bloom Filter(k = 1)

Bloom Filter(k = 3)

记曝光物品集合大小为 nn,⼆进制向量维度为 mm,使⽤ kk 个哈希函数。则Bloom Filter 误伤的概率为:

δ(1exp(knm))k\delta \approx\left(1-\exp \left(-\frac{k n}{m}\right)\right)^{k}

  • nn 越大,向量中的 1 越多,误伤的概率越大,即未曝光物品的 kk 个位置恰好都是 1 的概率⼤
  • mm 越大,向量越长,越不容易发生哈希碰撞,但是需要存储的信息就越多
  • kk 太大、太小都不好,kk 有最优取值

设定可容忍的误伤概率为 δ\delta,那么最优参数为:

k=1.44ln(1δ),m=2nln(1δ)k=1.44 \cdot \ln \left(\frac{1}{\delta}\right), \quad m=2 n \cdot \ln \left(\frac{1}{\delta}\right)

3.2 曝光过滤的链路

在用户刷新之前就要把本次曝光的结果写到 Bloom Filter上,否则下一刷很有可能曝光重复的物品。所以使用实时流处理,Flink 实时读取 Kafka 队列,做实时计算,计算曝光物品的哈希值,把结果写到 Bloom Filter 的二进制向量上。用这样的实时数据链路,在物品曝光之后,这位用户的 Bloom Filter 就会被修改,之后就能有效避免曝光重复。但是实时流也最容易出问题,例如实时流挂掉了或者延迟特别大,那么推荐效果就会变差。

之后 Bloom Filter 把物品的二进制向量传送给召回服务器。在召回服务器上,用 Bloom Filter 计算召回的物品的哈希值,之后再和二进制向量做对比,把已经曝光的物品给过滤掉。

3.3 Bloom Filter 的缺点

Bloom Filter 把物品的集合表⽰成⼀个二进制向量,每往集合中添加⼀个物品,只需要把向量 kk 个位置的元素置为 1(如果原本就是 1,则不变)。

但是 Bloom Filter 只⽀持添加物品,不⽀持删除物品。从集合中移除物品,无法消除它对向量的影响,这是因为向量中的元素是所有物品共享的,如果把向量的一个元素改为 0,相当于很多物品都给删除了。想要删除一个物品,就需要重新计算所有物品的二进制向量。

每天都需要从物品集合中移除年龄⼤于 1 个⽉的物品(超龄物品不可能被召回,没必要把它们记录在 Bloom Filter,降低 nn 可以降低误伤率)。