作者 | 邹建勋,袁易之,常郅博
编辑 | 蔡芳芳
1 信息流业务背景介绍
信息流是一种可以滚动浏览,持续给用户提供内容的数据形式。信息流源于内容信息平台,兴起于社交媒体、新闻资讯类平台。信息流内容会出现在外观相似、一个接连一个显示的版块中。近年来,信息流内容市场发展迅速,通常内嵌在各类 App 中,由平台主动推送,用户的抵达率高。而通过对用户的行为偏好进行跟踪分析建立算法推荐模型,当内容足够丰富时,可以为用户主动推荐无限多感兴趣的内容。
随着各类视频 App 火爆,目前短视频已经成为信息流中最重要的流量窗口。短视频碎片化的内容突破了空间、时间、人群的限制,广受用户欢迎。同时, 其个性化、平民化的传播形式和语言表达,也契合了用户的情绪价值。
视频内容理解主要负责视频的理解分析和加工处理,是视频信息流生态中重要的组成部分,也是其核心财富。而相似视频识别是内容理解中非常重要的一环。
图 1 短视频信息流形态
2 为什么需要相似视频识别能力?
随着短视频业务不断发展,吸引了越来越多的优秀内容创作者前来发布,同时也出现了一些搬运号搬运内容的情况。这些内容或完全相同,或只是添加了水印、花边、裁剪、旋转、滤镜等干扰因素。
图 2 搬运内容的形态
这些重复或相似的内容,可能会带来以下影响:
- 对用户来说,它们都是相同的内容,若重复消费,则会给用户带来不好的体验。
- 对业务来说,这些重复内容的处理,会浪费大量的机器资源和审核的人力资源,显著增加内容处理成本,限制业务规模。
因此我们需要构建相似视频识别的能力,用来对重复的视频进行去重。此外,相似视频识别的能力在推荐打散、搬运号识别、低质识别、品牌采买等场景也能得到有效收益。
本文将围绕相似视频识别技术展开详细介绍,包括架构演进、工程优化、组件沉淀等部分,希望能为有相同诉求的读者带来一些启发。
3 亿级别相似视频识别的挑战
不同人群对于相似视频识别能力的需求各不相同。对用户来说,推荐池中一旦存在重复视频,就很容易被推荐系统基于画像反复推荐,因此从用户体验考虑,需要更重召回;对号主来说,一旦判断错误,视频被误打击,号主这条视频就不可能再被启用,因此从号主体验考虑,又需要重准确。
综上,我们的相似视频识别能力既要保证足够高的召回率,还必须足够准确,这就需要对多个环节进行全面优化。
原有架构存在不足
早期的相似视频识别架构相对简单,整个去重的流程基本在一个服务中就完成了。比如很早就支持标题去重服务,整个流程包括:对标题提取特征向量,向量写入样本池;然后用此向量检索样本池,召回相似标题;再对召回的相似标题进行校准,是否真正重复;最后做出决策,是否进行去重拦截。
但后来我们发现仅标题去重无法满足产品发展需要,于是仿照标题去重又新增了视频的画面去重、封面图去重等等。系统架构变成了如下图 3 所示:
图 3 早期去重架构
这种架构存在多种问题:
1. 单机存在性能上限
最初每一种召回服务的样本池都是自己在内存中管理。服务都使用大内存机器,进行单机部署,同时部署 1 台冷备机器。主机读写时,与备机进行强一致性数据同步。这样的架构很明显存在性能上限,无法利用分布式的优势。
2. 各去重模块相互耦合,无法利用多种判断结果,重复开发工作量大
对于仅标题重复、画面不重复的视频,如果想要做到不拦截、只打标记,在原有架构下是无法完成的。因为标题去重服务在决策的时候没有画面信息和封面信息。
以一个非常简单的需求为例:调整去重优先级,对不同层级的账号调整对应的优先级。要满足这个需求,上面三个服务每一个都需要调整,相当于三倍开发量。碰到复杂的需求,必定会浪费大量开发人力,同时这种重复代码也不利于维护。
3. 不利于扩展
如果以后需要新增召回策略,只能再次新增一个类似的服务,进一步加剧系统的维护难度。
算法模型服务消耗大量成本
在我们的业务场景下,系统每天需要处理百万级的新增视频,我们需要对视频进行各个维度的特征提取、生成向量,用于召回和校准。而特征提取依赖的算法模型会消耗大量的机器资源,包括 CPU、GPU 和内存存储,存在极大的成本挑战。
检索架构高可用问题
在我们的检索架构中会存储若干天历史视频向量,总体视频数量达千万到亿级,对应的抽帧图数量则达几十亿到百亿级。每新增一个视频,我们都需要在秒级的时间内,从几千万到亿级的视频库中,召回重复视频。
这些业务特点对于我们的相似视频识别架构、算法模型处理效率和检索架构高可用等层面,都提出了非常大的挑战。下面分别从这三个角度出发,介绍我们所做的一些优化。
4 相似视频识别架构设计
我们重新设计了整个相似视频识别的架构,采用分层设计,把特征提取、召回、校准以及决策分离开。新架构如下图 4 所示:
图 4 新的去重架构
整个新架构共分为 6 层,自顶向下分别是:
- 预处理层:
预处理层负责对视频介质进行预处理,抽取 1 秒 1 帧的平均帧,以及场景切换的关键帧,同时提取视频中的音频,以及智能提取封面图。
这里为什么存在 2 种抽帧呢?因为我们发现它们在去重的效果上各有优势,无法相互替代。例如:有些场景切换比较频繁的视频,如果抽取平均帧时,时间轴刚好错开了,就会导致抽取的帧之间关联性很小,影响召回。反之,对于场景切换少,甚至静止类的视频,显然只用关键帧是不适用的。
- 特征提取层:
特征提取层主要负责对视频介质进行相应的特征提取。对于关键帧,提取为二值向量(值为 0/1);平均帧提取为 Embedding 向量;音频提取为 mfcc 和 chromaprint 向量;标题提取为 bert 向量;封面图提取 sift 特征。特征向量存储至 MySQL。在第 3 层中建立 Faiss 索引时,会读取 MySQL 中的向量。
为何需要多种特征和多种召回路径呢?因为业务对重复视频的定义是:只有画面 音频 语义,这三者都重复才算是视频重复。因此,任何单一的一种特征向量,都不足以判断视频是否重复。此外,即使 2 个视频的画面和音频都不重复,但标题或者封面图重复,如果这 2 个视频出现在用户的同 1 刷的展示列表中,也会带来不好的体验。因此,对这种仅标题或封面图重复的,也会进行召回,在推荐后台进行打散处理。
- 召回层:
召回层负责基于特征提取层中生成的各种向量,建立向量检索库,进行召回。我们使用的是内部基于 Faiss 之上开发的分布式向量检索引擎。对于每种向量,会建立一个相应的索引库,用于召回。
- 校准层:
校准层会对召回的疑似重复的视频 pair 进行校准。因为在召回层,我们更注重高召回率,会适当降低对准确率的要求,准确率交给校准层来保证。例如:视频 A,召回了和 B、C、D 重复,则校准层会分别对 A&B、A&C、A&D 这 3 个 pair 进行校准。校准的内容包括标题、画面、音频等维度,进一步识别这些 pair 的标题、画面、音频看是否真正重复。
- 决策层:
决策层主要用于处理业务逻辑,包含了大量业务规则。主要做的工作包括:识别到重复视频后,计算优先级,应该启用哪些、拦截掉哪些;同时,对视频 A,生成类似的关系链形式:B|C|D,给到推荐端,进行去重或者打散;也会记录标题、封面、画面、音频等哪些维度是重复的。
- 监控层:
监控层用于监控整个系统的运行情况,统计每天去重识别的量级等多个指标数据。人工定期抽检去重掉的视频是否合理。同时,我们也建立了 benchmark 机制,用于准确评估整个系统的准召率。当算法模型或者工程服务有版本更新时,先在 benchmark 系统上进行评估,达标后再发布上线,减少系统风险。
通过架构优化,我们对整个框架做了更合理的划分,每一层各司其职,同时每一层都可平行扩展,使得开发成本和维护成本都大大降低,也得以更好地支持业务的快速发展。
5 算法模型服务性能优化
如前文所述,特征提取层和校准层主要负责提取视频的各个特征,会消耗大量的机器资源。本节重点介绍如何提高算法模型服务的处理效率,进而降低机器资源成本。
PyTorch 模型服务性能提升 7 倍
相似视频识别系统其中一路召回的特征模型,输出的是二值向量结果(0/1)。该模块基于 PyTorch 框架开发,采用的 ResNet50 模型,整体过程是将视频的每张抽帧图转换为 N 维的 0/1 向量。最初上线部署至 Kubernetes pod 容器(8 核)上时,单帧向量化需 3.4 秒,但在同等配置的 8 核实体机上,只需 0.46 秒。
工程同学不断深挖,最终发现是环境配置导致的差异。PyTorch 中默认使用了 OpenMP 并行计算来加速模型。如果不配置 OMP_NUM_THREADS 环境变量,默认会使用 pod 容器所在母机 node 的所有 CPU 核。母机 node 是 64 核,则模型在 pod 容器中会开启 64 个线程,每个线程试图去抢占 CPU 核。然而 Kubernets 只分配给 pod 容器 8 核算力,64 个线程跑到一定时候会被内核强制调度让出 CPU,此时中间的一些 CPU Cache 数据会失效,程序会被迫进行上下文切换。而频繁的上下文切换非常影响性能,导致 64 核比 8 核运行的性能更差。通过设置正确的 OMP_NUM_THREADS,pod 容器计算性能可以恢复至实体机的 95%。
下图 5 是优化前后的耗时对比,最终性能提升了 7 倍。
图 5 优化前后的性能对比
同理,使用 golang 开发的服务,如果没有正确配置 GOMAXPROCS 变量,在 Kubernetes 容器(整体核数>所需核数)上部署时也会遭遇类似问题。
SKLearn 模型服务性能提升 9 倍
在相似视频识别系统中,对召回的疑似重复视频 pair,会提取其音频并转换为 chromaprint 向量,计算两个音频向量之间的海明距离,以此判定 pair 的音频是否重复。
该模型服务基于 sklearn 框架开发,使用了 RANSAC 回归模型。初期部署至 Kubernetes 容器时,一对 2 分钟左右的音频处理耗时需 4.7 秒左右,pod 基本满负载运行。工程同学通过分析 sklearn 模型代码,找出了计算复杂度较高的 top N 步骤,发现其中之一是海明距离计算。通过对其进行优化,使用 gmpy2 代替自己计算海明距离,耗时降低到 2.3 秒。然后,在同等配置实体机上进行测试,发现处理相同的音频 pair 只需 0.5 秒。在代码和调用栈分析协助下,发现原来 sklearn 也使用了 OpenMP 进行并行计算。和上一节情况类似,如果没有正确设置 OMP_NUM_THREADS 值,pod 使用的 CPU 核数会超过分配限制,被迫切换上下文,从而导致性能低下。
我们修改了适当的 OMP_NUM_THREADS 值,却发现在 sklearn 模型中并没有生效,翻阅 sklearn 文档也并没有找到相关的设置 API。我们推测是模型本身对于线程做了控制,就尝试将 sklearn 模型转换为 ONNX 模型,通过使用 ONNX 提供的 API 设置适当的 OMP_NUM_THREADS,最终把耗时降低到 0.5 秒。
图 6 优化前后的性能对比
ONNX Runtime 加速模型推理性能提升 8 倍
如前文所述,我们通过正确配置 Kubernetes pod 环境参数,大幅降低了模型处理耗时。然而因为每天要处理海量图片,该模型仍然消耗了非常可观的机器资源。于是,我们尝试对模型本身进行推理加速,提高整体图片处理的吞吐量。
我们调研了 ONNX Runtime、OpenVINO、TensorRT 等模型推理框架,发现这些框架通过对算法模型进行层间的融合,如卷积 BatchNorm 是 CNN 中常见的结构,而 BatchNorm 本身是线性操作,在推理时可以融合至卷积操作中,使计算量可以显著降低。同时这些框架也为卷积、矩阵乘法等耗时运算提供了一套性能较好的实现,使得推理速度相比于原生 PyTorch 有非常大的提升。
我们选取了 ResNet50 作为基准模型测试了 ONNX Runtime 和 OpenVINO 上的推理性能,其中 ResNet50 模型原生 PyTorch 推理速度为 93ms,ONNX Runtime 推理速度为 38ms,OpenVINO 推理速度为 43ms,ONNX Runtime 有 2.45 倍的加速比。
同时在重构推理服务的过程中,工程同学发现服务代码中遗留了算法同学的部分训练代码,存在 PyTorch 训练时的 DataLoader 逻辑,使得处理每个请求时都需要创建 DataLoader 和背后的进程池,在请求结束时再全部销毁。通过将此类冗余逻辑重构,处理耗时降低至 35ms。相比加速前的耗时,性能提升了近 8 倍。
综上,我们通过优化模型服务的性能,不仅有效提升了服务的处理速度,亦可以大大降低服务成本。
6 相似内容检索架构优化
我们需要用新入库的内容去检索所有目前已经在库中的内容,根据某种度量方式,来判断内容是否相似。目前业界常见都是将视频整体或者视频帧转为 Embedding 向量进行检索。我们库中现在有几十亿的向量,如果暴力计算出最相近的 K 个向量(K-Nearest Neighbor,KNN), 会导致服务计算量过大、耗时太长,在生产环境不可接受。所以我们一般都采用似近邻(Approximate Nearest Neighbor,ANN)算法,从而平衡线上召回效果以及检索性能。
分布式向量检索
Facebook 开源了一个 ANN 向量相似度检索算法库 Faiss,但是 Faiss 只支持单机,在我们十亿量级下,单机根本无法满足需求。业界一般都会基于 Faiss 搭建一套自己的分布式特征向量检索系统。
我们的检索架构早期是一个单机服务,而视频量的快速增长,我们也逐步演进到集群化。目前我们使用的是公司基于 Faiss 库之上实现的一个高可用、高吞吐的通用分布式相似性搜索组件。
也正是因为这套组件是基于 Faiss 开发的,不仅继承了 Faiss 的众多优点,但同时也继承了 Faiss 的一些固有限制:
- 实时写入性能低
在我们的业务场景中,每天会有百万量级的新视频需要加入到索引库中。同时,每个新视频还需要实时检索,召回重复的视频。
前文提到过,索引库会保存历史 N 天的所有视频,量级在几千万到上亿级。如果我们只建单个 Faiss 索引,对这种量级的索引进行实时的混合读写操作性能很低,无法使用。
- 数据淘汰困难
索引库最多保存历史 N 天的所有视频,意味着每天需要从库中淘汰掉第 N 1 天的视频。众所周知,直接从 Faiss 索引中删除大量向量是一种非常糟糕的方案。
- 模型定期更新
Faiss 索引的模型需要定期训练更新,不然会影响检索的准确度。但更新时不能中断服务,否则会影响线上检索请求。
分布式向量检索管理系统
为了解决上述几个问题,我们设计了一套分布式向量检索管理系统。整体架构如下图 7 所示,主要分为两层:统一管理向量读写接入的 Proxy 和索引管理系统 Manager。
图 7 向量索引管理系统
- 读写分离机制
我们采用大小两个索引读写分离的方式,来解决实时写入性能低的问题。其中大索引保存前 1 天至前 N 天的海量数据,只提供检索(读)功能,小索引保存当天的实时新增数据,提供实时写入和检索(读写)功能。因为小索引最多只保存 1 天的数据,量级相对小很多,足以支持实时的读写,性能满足要求。如下图 8 所示:通过 proxy 写入向量的时候,会实时写入小索引以及存储 db。而读的时候会并发读大索引以及小索引,然后 proxy 合并两者的检索结果。
图 8 大小索引的读写分离
- 双 buffer 切换机制
Manager 从逻辑上把索引数据抽象为两种类型。一个是工作索引,称为 buffer0,提供线上的写入和检索服务,包含大索引(保存历史 N-1 天的海量数据)和小索引(保存当天数据)。另一个是备用索引,称为 buffer1。
图 9 双 buffer 索引
Manager 每天会对 Faiss 索引进行重建,重建过程中,会淘汰掉 N 1 这天的旧数据,同时会重新训练 Faiss 模型。Manager 分别重建好新的大索引和小索引后,即完成了 buffer1 备用索引的创建。此时,会触发索引切换,buffer1 备用索引提升为工作索引。Proxy 会把线上请求引入至 buffer1 中。buffer1 变为新的工作索引,buffer0 变为备用索引,其中的索引数据会被清空释放。
以此循环,再到新的一天,Manager 会在 buffer0 中重建新的大索引和小索引,然后再把线上流量从 buffer1 切换至 buffer0。buffer0 变为新的工作索引,buffer1 变为备用索引,其中的索引数据会被清空释放。
依靠双 buffer 每天不断切换,就可以解决旧数据淘汰和模型定期训练的问题。具体的索引重建流程可参考图 7。
重建大索引时,Manager 从 MySQL 中导出前 1 天至前 N 天的向量数据,按照约定格式,落地为 N-1 个文件。每个文件即代表某一天的全量向量数据,而文件的一行即代表某个视频或者某个抽帧的 X 维向量。由此可知,导出 db 数据时,自然就把第 N 1 这天的旧数据淘汰掉了,最终重建好的大索引,就不再包括第 N 1 天的数据了。
向量原始数据准备完毕后,即可按照图中 step1 至 step6 的步骤,进行索引重建。包括采样数据,训练 Faiss 模型,然后 load 数据至 Faiss,进行索引重建。小索引的重建只需从 MySQL 中导出当天的向量数据,后面步骤同大索引。
- 多 set 索引机制
如上所述,采用读写分离能够解决索引的实时写入性能问题。但随着业务快速发展,视频量级不断增长,又出现了新的挑战:
- 每天新增视频越来越多,小索引即使只保存当天视频向量数据,量级也不再小了,在可预见的将来,混合读写的模式会遇到瓶颈。
- 大索引重建耗时太长。因为大索引保存的是 N-1 天的海量数据,数量在几十亿级以上,每天重建需花费数小时以上。
- 空闲资源浪费。双 buffer 机制,意味着需要预留 1 倍的资源给备用索引使用。而这些预留的资源只在每天重建的那段时间才会用到,大部分时间处于闲置状态。
为了解决以上问题,我们引入了分 set 索引的机制。即,把大小索引数据拆分成多份(每一份称之为 set),建多个 set,每次只是将增量数据加入需要淘汰数据的那一个 set,那么只需把那一份对应的数据重建索引即可。假设大索引分成 m 个 set,那每天重建大索引时,只要将需要淘汰数据所在的那一个 set 重建即可,预留的资源只需 m 分之一。
这样既加快了重建的速度,也减少了空闲资源。
当然,proxy 也会对分 set 进行适配。向量写入时,hash 写入某一个小索引 set 中;检索时,会并发检索所有的大索引 set 和小索引 set,合并检索结果。
图 10 分 set 后的索引重建
通过分布式向量检索以及分布式向量检索管理系统,大大节约了我们的存储成本,也使得我们的内容管理和检索变得更高效,平行扩展也更方便了。
7 小结
在业务规模快速增长的情况下,我们重新设计相似视频识别的架构与分层,各司其职,使得各层可以快速水平扩展。对算法模型服务的性能优化,在内容量快速增长的同时,更好的控制了业务成本。
通过对相似内容检索架构优化,有效的支撑了在海量内容的相似内容检索。至此,亿级别的相似视频识别问题基本得到解决。后续我们也会持续优化,提升整套框架的易用性,可配置化,同时也会支持更多的底层的向量检索库,以适配各种不同业务场景的需求。
最后感谢负责人 chale 的支持,这项工作也汇聚了 kiefer、jeven、scales、lusha、alvin、lizhi、yuxuan、edin、bowen、lusheng 等小伙伴的很多心血。
作者介绍
邹建勋,QQ 小世界内容理解高级算法工程师,曾负责腾讯看点内容理解能力建设以及 QQ 公众号的消息推送系统等,对高性能后台开发,和信息流业务中内容处理有一定的经验。
袁易之,QQ 小世界内容理解算法工程负责人,对信息流业务中内容处理的全链路有较多的经验。
常郅博,曾负责 QQ 公众号关系链系统,腾讯看点内容中心建设等,致力于算法工程效率提升,对内容处理与理解有独到见解。