爱奇艺视频推荐领域的 ANN 检索实践
作者:张吉
爱奇艺深度学习云研发工程师
一、背景介绍
(一)推荐系统
推荐系统是现在互联网发展一个比较重要的基石,上图左边是爱奇艺手机端的应用,右边是 PC 端的 Web 页面。可以看到,很多剧都是通过推荐而来的,随着数据量的发展,用户的搜索习惯也改变了。原来用户有目的性搜索,如今更希望系统进行主动的智能推荐,提升用户体验,目前推荐系统在爱奇艺里占据重要地位。
(二)推荐系统框架
爱奇艺推荐系统框架分为三段,呈现漏斗型的结构。所有候选集即爱奇艺视频库,先过滤出一些当下热剧,然后建一个索引。通过召回阶段,将数据从百万量级筛选到千量级的规模,接着经过排序阶段,排出十量级的剧最后推送给用户。
本文主要分享的是基于 ANN 做的召回阶段,对于召回的要求是速度快和数据量大。
二、工程实践
(一)算法选型
算法选型选的是 YouTubeNet,做的是 User-Item 的召回,选取它是因为模型召回效果较好。它下边是一些用户的向量,通过拼接用户向量形成分类网络,最后分类出来的这一层对应爱奇艺的视频,最终形成 User-Item 的推荐,在训练阶段就是一个千百万级别的分类器。
最后的 Serving 阶段比较有挑战,因为召回阶段难以在线计算整个神经网络的推理,因此在Serving 阶段用 ANN 的方式,然后近似地把全连接之后的数据取 Topk 的过程,用 ANN 做近似。
我们把网络倒数第二层看做用户向量,分类里面矩阵的权重看做视频向量,
普通召回:
把网络倒数第二层看做用户向量,分类里面矩阵的权重看做视频向量,在计算的时候,假如是一个5维的用户向量,然后它跟视频向量做全连接乘法,最后得到的视频向量比如有100万条,则得出100万的视频得分,代表的是100万个视频。这时候再取一个 Topk,最后得到的是希望召回的视频ID,这就是通常情况下召回的推理过程。
ANN 方式召回:
用 ANN 的方式就比较巧妙,因为向量检索可以选择用内积的形式来做距离度量方式,矩阵层其实也是内积,这两个近似等价。
也就是说 YouTubeNe t在 Serving 的时候,把全连接全乘完之后的结果再取 Topk 的过程,转化成了一个用内积形式做度量,直接做 ANN 近似召回的过程,所以在 Serving 阶段就使得千百万级别的全连接的效率是得到了一定保证。
(二)Serving结构
如上图所示,Serving 结构模型分为三段,第一段是 DNN 用户销量的在线推理,第二部分通过用户向量做 ANN 的召回,然后推送给后边排序的是每一个视频的 ID 还有对应的得分。
我们在做的过程中,面临以下三个问题:
1) 整体模型如何更新
整个模型分为用户向量生成的 DNN 推理部分,还有 ANN 索引库部分,这两个其实是从同一个模型里拆出来的,因此在真正 Serving,包括更新的时候,一定要保证 DNN 和 ANN 的版本一定要是同一个模型拆出来的,否则在真正计算的时候,它们不在一个向量空间里,算出来就都是错误的数据。
2)Serving 效率
召回对整体性能有一定要求,如果请求端自己先请求 DNN 然后再返回,接着再去请求一个独立的 ANN 服务,中间存在一些网络开销。
3)接口与 TF Serving 一致
我们对外提供的是 TF Serving 接口,不改这个接口的话对整体的技术栈没有影响,上线效率高,不需要额外做接口方面的开发。
基于这三点,我们设计了两个方案。
方案一:
第一个方案把整个 ANN 的这部分,相当于对前面的请求方给它屏蔽掉。用 DNN 之后,直接拿到用户向量去 ANN 做向量的检索。DNN 部分选用 Tensorflow,ANN 部分选用 Milvus,Milvus 的优点有文档丰富、封装完善和开箱即用。
上图为方案一整体结构,Milvus 有 C++ 的 SDK,在用的时候,推荐量大概在百万到千万级别,单个容器下面容量可以满足,因此先都在单个容器里面。
容器里包含了两个进程,一个是 TF Serving,负责 DNN 的编码,里边有 Milvus 的一个装置,然后 Milvus 请求同一个容器内部的 ANN 服务。对于外部来说,整体的接口还是 TF Serving,并没有改变接口。容器内部减少了一些网络上的开销,整体上性能较为良好。
- Tensorflow Serving 改造
Tensorflow Serving 就是一个 Tensorflow Runtime 的封装,主要是做 RPC 的请求处理和模型管理方面的工作。
红框中是整个 Tensorflow Serving 在运行推理时,最核心的函数叫 RunPredict,里边包了一层 Tensorflow Runtime,然后把对应的 Return proto 进行解析,最后再统一回传给请求方。
我们在这个函数里边加了一层 Milvus SDK RPC,通过 Tensorflow Runtime 拿出来的用户向量,直接在这边就封装成 Milvus 的请求,然后去请求本地 ANN 服务。然后再返回 Proto 时候,会返回对应的 ID 和得分。
-改造步骤总结如下:
- 检查签名
- 解析 proto
- TF Runtime 计算
- 解封 proto
- 模型升级
如上图所示,2个Version1 是现在正在服务的一些模型。由于我们这是一个只读的场景,相当于在线服务时加载的是已经训练好的索引,Tensorflow 有这个模型动态加载的能力,但是 Milvus 目前没有,所以通过整体进行 AB 切换的方式来升级。
升级 Version2 的时候,会先启动一个 Version2,当 Version2 就绪时,会把 Version1 摘掉,通过 CONSUL 保证服务的发现性。
这里存在一个缺点,在请求的时候,Version1 和 Version2 会存在一些混乱的情况,混乱程度取决于升级的时间,如果升级得快,可能抖动就不会很大,如果升级得慢就会受到一些影响。
方案优点:开发速度快
方案缺点:不够灵活、模型版本难以管理。
- Milvus 场景&配置
对于Milvus我们做了一些简单的配置:
- 只读场景
- 数据在百万级别
- 索引文件可共享
由于我们是一个只读场景,所以希望在线 Serving 时可以直接加载离线训练好的 Milvus 索引,相当于在真正建索引的时候,是有一个专门创建索引的 Milvus 容器不断地去创建索引,然后把这些 Milvus 所创建的索引都汇到对应的网络存储上,在 Serving 的时候只需要拉下来,然后做加载就可以了。
- TF serving 与 Milvus 结合
- TF serving 静态编译 GRPC,Protobuf
- Milvus 静态编译 GRPC,Protobuf
在做TF Serving 和 Milvus 结合的时候,因为这两者都是用到了 GRPC,而且都是静态编译,如果放到同一个进程里面,由于两者的 GRPC 版本不一样,会导致一些问题,比如数据格式转换等。
因此我们将 GRPC 的版本统一为 Tensorflow Serving 的1.19.1版本,之后就可以在 Tensorflow 版本里面调用 Milvus 的服务。
- 业务需求
结合方案一的优缺点,后续产生了如下业务需求:
需求一:召回后重排序
许多算法人员反馈,召回之后如果结合一些业务指标,对召回再进行一次重排序,效果会有所提升。而且对于召回而言,在很多召回引擎中,召回 Top3 版跟 Top5 版所需时间相差无几。因此可以召回 Top5 版,然后再进行一次重排序,这样效果会提升很多。
需求二:召回模型带版本
召回模型带版本的话,后续可以对模型版本进行管理。
需求三:前置过滤
在爱奇艺视频中,有的视频是用户已经观看过的,因此在召回时希望过滤掉这一部分视频,从而不会给用户进行重复推荐,提升用户体验。
结合以上三个需求,我们做了方案二。
方案二
如上图所示,在原来的基础上,除了 DNN 部分是原生的 Tensorflow 之外,我们把 ANN 也放到了 Tensorflow 的 Runtime 中。
如果都在计算图中的话,剩下的一些其他逻辑,例如重排序,对于算法人员而言整体就比较简单了。除了一些业务指标之外,也可以用简单的树模型根据用户的 Feature 做一次再重排序。而且如果都集中到 Tensorflow 里边,对外接口也是一致的,对于整个技术栈,贴合度也会更高一些。
- 方案二技术选型
基于以上方案,我们需要把 ANN 的这个功能做成一个OP,因此选取了 Hnswlib,它有三个优点:
- 性能好
- 线程模型简单
- Head only
- 方案二结构
方案二使用 HNSW OP 创建计算图,升级部署与纯 TF Serving 一致。
RunPredict有的逻辑都放在了 Tensorflow Runtime 中,这也符合 Tensorflow Serving 设计的初衷,因为它更多的是处理 RPC 请求与模型管理的工作。
- 优缺点
优点:可支持更灵活的逻辑,模型版本易于管理
缺点:ANN OP需要开发
- 模型升级
整体的模型升级部分与 Tensorflow 一样,是热升级的模式。
比如这个 Tensorflow 有两个版本 Version1 跟 Version2,然后大家都在请求 Version2。这个时候如果触发一次升级,容器会分先后,大家都会去加载 Version3,整体都加载完之后,就可以都切到 Version3了。通过热升级的方式,整个模型易于管理。
-
整体 Serving 工作流
作为一个基础的 AI 平台,为用户提供的服务不止是 Serving,也会提供训练与模型管理方面的工作,整体 Serving 的工作流主要分为4个部分。
首先用户上传训练好的模型,接着触发创建索引的任务,这个任务基于方案一或方案二会产生不同的索引。如果是 Milvus 则会创建 Milvus 的索引,如果是定制的 OP,会把这个图转换成HNSW OP 的图,然后把创建好的索引或转换好的图上传到网络存储上。
在真正 Serving 时,有一个全局唯一的 Version,通过 Version 指定网络存储下载对应的模型/索引,下载完之后就可以进行在线 Serving。
- HNSW OP 性能测试
- HNSW OP 性能测试(纯 ANN)
如果是对 YouTubeNet 做 Serving,把前面的 DNN 去掉,就变成一个纯召回服务。基于这种情况,我们去测了 sift 模型,测下来准确率为95.35%。
上图是单次请求的在线 Serving 数据,并发数代表同时请求的 euclidean 数量,Wait time 代表单个请求的时间间隔。可以看到,并发度20与40的测试结果里,除了 QPS 和 CPU 利用率翻倍以外,延迟分位点等数据大致相同,Tensorflow 对并发扩展处理优秀。
- HNSW OP 性能测试(YoutubeNet)