本篇文章主要介绍KDD 2020 Applied Data Science Track Papers中的一篇来自Facebook的语义检索文章,Embedding-based Retrieval in Facebook Search。关于样本、模型、训练等细节可以参考:负样本为王:评Facebook的向量化召回算法。本文重点关注其工程实践上的经验,也借此机会对基于PQ量化的近似近邻搜索 (ANN) 的原理做个梳理。
首先从总体上预览一下paper提出的EBR系统的架构 (Embedding-based Retrieval System):
EBR系统架构图示意图
左上角是查询处理模块,是双塔模型中的query-side embedding model
右上角索引构建模块,是用document embedding model推断doc embedding后构造向量索引。
构造过程中,先拓展doc的metadata,加入doc embedding,并导入doc的正排索引中 (比如用于ranking的特征);
同时,通过向量量化技术来降低索引存储和计算距离代价,并将量化的结果存在倒排索引中。
这一部分也是paper中关注的重点,下文会重点介绍。
左侧中间部分是检索模块,拿到query embedding后,可以通过精心为语义召回设计的NN算子(Near Neighbor Operator) 计算距离并进行语义召回。
召回的过程中,既支持原来的布尔匹配召回,也支持向量语义召回,还支持混合召回。
这部分下文也会重点介绍。
左下角是排序模块,得到召回结果后,再经过排序模块进行实时排序,排序时会利用到embedding特征。
paper中涉及工程实现细节的主要是索引构建模块和检索模块。下文我会先介绍索引构建模块,这里头涉及很多向量量化的概念,比如PQ量化、粗糙量化、残差量化等,我会先介绍一些量化的背景知识,核心的索引过程和搜索过程,然后介绍在线检索模块,认识基于构建好的向量索引,线上是如何运转和实现召回的;接着探讨paper中提到的工程优化点和调参经验。最后,做个总结。
基于Product Quantization的近似最近邻搜索,核心的一些问题预览一下:
给定D维向量 和集合 ,需要找到与 距离最短的k个最近邻。距离的衡量可以是欧式距离、余弦距离等。
如果以最粗暴的方法进行穷举搜索,构造距离矩阵的复杂度为: 。从距离矩阵中查找到k个最近邻,最小堆,则复杂度为 。
举个例子:假设 ,则构造的距离矩阵包含 个元素,假设每个距离值32bit,至少占用1600TB空间,构建距离矩阵的时间是 量级,查找Top-K搜索时间是 量级。
所谓向量量化,就是将原来无限的空间 映射到一个有限的向量集合 中,其中 是一个自然数。将这个从 到集合 的函数记为 ,则 ,在信息论中称 为codebook。即:通过 来近似代表 。
最常用的就是k-means聚类,通过聚类后的聚类中心向量来近似量化原始的向量。聚类中心的个数即为codebook大小。因为量化的存在,任意两个向量之间的距离可以通过对应的量化向量的距离进行近似,也就是聚类中心向量的距离。因为聚类中心的个数小了很多,故计算距离的复杂度也下降了很多。(显然,这种方式太粗糙了,误差很大。除非聚类数非常大,极端情况下,聚类数等于样本数时,每个样本一个聚类簇,此时无误差,但是没有起到减少计算复杂度的目的)
聚类量化的结果:产出聚类中心向量的过程对应train的过程的一部分。量化后,每个向量都可以用聚类簇中心下标ID来标识,根据ID可以获取聚类簇的中心向量。
如上图所示,N个向量,通过聚类量化产生多个聚类中心,每个向量属于某个聚类簇中,那么就用该聚类簇对应的中心向量来量化该向量,可以用聚类簇中心对应的下标ID来表示,比如:C1量化vec1。
Faiss中对应的实现是IndexIVFFlat。
动机:即:PQ量化,很多时候我们向量不同部分之间的分布不同的,比如下图(3段向量),因此可以考虑对向量分段,并分别进行分块量化。这个只是直觉原因,本质原因下文讲。
分块量化也可以采取聚类量化来实现,则分块聚类中心的个数即为分块codebook的大小。相当于在这个方法下,对每个向量,有【m个分块向量】来量化它,即:【m个分块聚类中心向量】。示意图如下:
PQ量化过程示意图
如上图所示,将原始向量等分为m组分块向量,每组都进行聚类量化,那么每个向量就有m个分块聚类中心向量来表示,比如vec1用 , ..., 共m个向量来量化。乘积量化的好处:假设每个子codebook大小一样,记做
,排列组合一下,那么相当于能表达的向量空间容量是这么大,
,但是只需要
的codebook空间,这也是乘积量化大幅度降低空间占用的本质原因。PQ量化原论文中给出的经验取值是:
,即:分成8块,每个分块的codebook大小为256,对应的向量空间大小为约 ,能够表达的向量个数足够大了。
乘积量化结果: 个分块聚类中心向量。每个向量可以用m个分块聚类簇中心下标ID来标识,所有ID连起来称为code。假设每块的聚类中心个数为256,则需要8bits,即1byte标识某分块下哪个聚类中心,m块则需要m bytes,即code大小为m bytes。
核心思想:层次化量化,这个也是Faiss中PQ索引的实现方式。其中粗糙量化使用聚类量化,用划分到的粗糙聚类簇的中心向量来粗粒度量化该向量,该结果存在较大的误差;接着对残差结果进行细粒度乘积量化。这样的话,误差就小了。
1. 总体上,每个向量先进行粗糙量化划分到某个粗糙聚类簇里,1个向量对应1个粗糙聚类簇标识,通常称为粗糙量化ID;
2. 然后计算残差向量,即:向量-聚类簇中心向量,再对该残差向量进行分块,并进行细粒度分块残差量化。残差量化的时候,每一块对应一个细粒度聚类簇,1个向量M块,则对应M个细粒度聚类簇标识,通常称为残差量化code。
3. 为什么用残差量化?原始向量可能会有特别大的分布差异/不平衡,也就是说可能聚类后,不同聚类簇分布得非常分散,每个簇所拥有的样本数极度不平衡。但是通过残差化后,即:每个样本向量减去所属的聚类簇中心向量后,残差向量之间的差异就不太大了,然后再对残差向量进行量化,就能更精确的近似原向量。
过程:具体而言,向量库先构造一个小规模codebook ,量化器为 。这个就是所谓的粗糙量化,或者称为粗糙聚类。接着,每个向量y都会有一个残差 。具体而言:记残差量化步骤的量化器为 ,则y可以通过 来表示。
其中, 是粗糙量化结果, 是残差量化结果。
这样的话,【查询向量x】和y之间的距离:
对应Faiss中的实现是 IndexIVFPQ。
做个小结,量化的结果:
聚类量化 | 乘积量化 | 粗糙量化+残差量化 |
---|---|---|
k个聚类簇中心下标id | m x k*个分块聚类中心下标组成的code | k个聚类簇中心下标ID,m x k*个分块聚类中心下标组成的code |
也就是说,为了表示每个doc的量化结果,可以为doc可以添加两种结构化字段:粗糙量化id, 残差量化code,用于实时检索使用。
建立从粗粒度聚类中心id 到 doc的映射关系,其中doc的信息包括:向量id,向量的残差量化code。每个doc通过粗糙聚类中心id和残差量化code就能知道原始向量如何映射到量化向量。
整个倒排索引不需要存储原始向量本身,索引结构存储的内容:
存储标识:粗糙量化id,doc id和残差量化code。
空间占用很小。m bytes 残差量化code,即:code_size或者pq_bytes。这个数越大,那么细粒度聚类簇越大,则精度越高。
基于id,可以找到对应的粗粒度量化向量 (共k个) ;基于code可以找到对应细粒度残差量化向量(共 m x k* 个)。
搜索场景中,y可以理解为doc。
搜索场景中,x可以理解为查询向量。
通过粗糙量化器来量化查询向量x,即:找到离x最近的w个粗糙聚类簇。实际上是用于限定搜索的范围,只搜索w个粗糙聚类簇ID索引下的向量。w是个超参数,即nprobes,量化的结果对应term 1。
选定某个粗糙聚类簇,
计算x和该粗糙聚类簇下的k*(默认即256)个中心点向量的内积。对应term 3,计算时间复杂度O(m x D/m x k*) = O(Dk*),记录下来,下一步查表用。
遍历该聚类簇下的doc文档,计算距离时,实际上全是查表操作,term 2是提前算的,term 1粗糙量化时算的,term 3上一步算的。查表时间复杂度实际上是O(m)
w个粗糙聚类簇,搜索时间复杂度为O(w(Dk* + m)), 另外,返回top-k的话,要加上最小堆排序时间。
Facebook在自研的检索引擎Unicorn中支持了第一代的近邻搜索。
首先简单介绍下Unicorn。Unicorn原本的检索方式主要以Term布尔匹配为主。
doc侧:以Term词袋的方式来表示doc,会基于Term的语义来区分命名空间。Term上还可以添加一些term特定的payload信息。举例:John living in Seattle会表示成【text: john and location: seattle】。
query侧:定义 Term-level 的布尔表达式来表示query。举例:下述query主要返回doc的文本中有john和smithe字眼,并住在seattle和menlo_park的用户。
为了支持embedding,需要扩展doc和query的表示
索引和计算向量距离时,Facebook将向量索引和向量在线检索通过某种方式转化成上述已有的布尔检索语言,很巧妙,可以完美融入现有的布尔检索系统,而不需要重新写一套系统。
先做个对应关系。
布尔匹配检索 | 语义向量检索 |
---|---|
Term | 粗糙聚类中心ID 标识,Cluster-ID |
Payload | 细粒度聚类中心CODE 标识, Cluster-Code |
doc侧:每个doc embedding会被量化并转成一个term和一个payload,相当于是两个doc的结构化字段,完美兼容已有的检索系统Unicorn的设计。
query侧:
term: (nn) 重写成和粗糙聚类相关的 term。
重写规则:计算query embedding和所有粗糙聚类中心距离,选出nprobes个最近的,用粗糙聚类中心ID来标识 (和doc的结构化字段Cluster-ID进行比较),不同聚类中心对应的Term之间的关系就是or的关系。
payload: 对query进行残差量化,得到满足radius约束条件的细粒度聚类中心,用code标识。
重写规则:对每个粗糙聚类簇,计算query embedding和其细粒度聚类中心的距离,距离满足<radius约束的细粒度聚类中心对应的Code取值记录一下 (和doc的结构化字段Cluster-Code进行比较),Code之间是OR关系,但是Code和ID是AND关系。
举个query改写的例子:
or ((and( (term(Cluster-ID, '粗糙聚类中心ID-a')),
(or (term(Cluster-Code, '残差聚类中心ID-a1'), term(Cluster-Code, '残差聚类中心ID-a3'),...)))
),
(and( (term(Cluster-ID, '粗糙聚类中心ID-b')),
(or (term(Cluster-Code, '残差聚类中心ID-b1'), term(Cluster-Code, '残差聚类中心ID-b4'),...)))
),
...
)
另外,作者强调了radius-mode和topk-mode的差异,radius方式的性能和质量更高。radius是一种受限制NN搜索。top-K需要扫描整个索引库来找Top-K结果。radius性能更好,但是需要确定radius值。
正是因为在已有的布尔查询语言上融入目前的EBR语义检索,使得混合检索的实现非常方便。特别是在模糊匹配场景、或者query存在错误的场景。举个例子:
模型改进不大的时候,多调调参数,会有奇效。
此次分享主要介绍了Facebook在KDD 2020发表的文章中的工程实践经验。首先从全局总览其系统架构,然后针对索引构建模块和实时检索模块展开讨论。其中,索引构建模块主要介绍了ANN中向量量化方法的背景知识。实时检索模块主要介绍系统实现,如何将向量检索通过query改写规则等融入现有的布尔检索系统中。最后介绍了一些调参的经验。
做向量召回的初期可以先重点关注模型层面上的优化,索引上也可以先采用简单的聚类量化的索引构建方式。当向量召回优化到一定程度时,如果想进一步提升性能和召回率,可以考虑借鉴文中的一些经验,比如以PQ量化的方式来构建索引,调参等。
KDD 2020: Embedding-based Retrieval in Facebook Search
Faiss基于PQ的倒排索引实现
负样本为王:评Facebook的向量化召回算法
Faiss向量召回引擎如何做到快速查找最近邻
A library for efficient similarity search and clustering of dense vectors
由于微信平台算法改版,公号内容将不再以时间排序展示,如果大家想第一时间看到我们的推送,强烈建议星标我们和给我们多点点【在看】。星标具体步骤为:
(1)点击页面最上方"AINLP",进入公众号主页。
(2)点击右上角的小点点,在弹出页面点击“设为星标”,就可以啦。
感谢支持,比心。
推荐阅读
征稿启示| 200元稿费+5000DBC(价值20个小时GPU算力)
完结撒花!李宏毅老师深度学习与人类语言处理课程视频及课件(附下载)
模型压缩实践系列之——bert-of-theseus,一个非常亲民的bert压缩方法
文本自动摘要任务的“不完全”心得总结番外篇——submodular函数优化
斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用
关于AINLP
AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。
阅读至此了,分享、点赞、在看三选一吧🙏