写点什么

腾讯信息流亿级相似视频识别技术架构优化实践

邹建勋,袁易之,常郅博

  • 2022-06-09
  • 本文字数:7477 字

    阅读完需:约 25 分钟

腾讯信息流亿级相似视频识别技术架构优化实践

信息流业务背景介绍


信息流是一种可以滚动浏览,持续给用户提供内容的数据形式。信息流源于内容信息平台,兴起于社交媒体、新闻资讯类平台。信息流内容会出现在外观相似、一个接连一个显示的版块中。近年来,信息流内容市场发展迅速,通常内嵌在各类 App 中,由平台主动推送,用户的抵达率高。而通过对用户的行为偏好进行跟踪分析建立算法推荐模型,当内容足够丰富时,可以为用户主动推荐无限多感兴趣的内容。


随着各类视频 App 火爆,目前短视频已经成为信息流中最重要的流量窗口。短视频碎片化的内容突破了空间、时间、人群的限制,广受用户欢迎。同时, 其个性化、平民化的传播形式和语言表达,也契合了用户的情绪价值。


视频内容理解主要负责视频的理解分析和加工处理,是视频信息流生态中重要的组成部分,也是其核心财富。而相似视频识别是内容理解中非常重要的一环。



图 1 短视频信息流形态

为什么需要相似视频识别能力?


随着短视频业务不断发展,吸引了越来越多的优秀内容创作者前来发布,同时也出现了一些搬运号搬运内容的情况。这些内容或完全相同,或只是添加了水印、花边、裁剪、旋转、滤镜等干扰因素。


图 2 搬运内容的形态


这些重复或相似的内容,可能会带来以下影响:


  • 对用户来说,它们都是相同的内容,若重复消费,则会给用户带来不好的体验。

  • 对业务来说,这些重复内容的处理,会浪费大量的机器资源和审核的人力资源,显著增加内容处理成本,限制业务规模。


因此我们需要构建相似视频识别的能力,用来对重复的视频进行去重。此外,相似视频识别的能力在推荐打散、搬运号识别、低质识别、品牌采买等场景也能得到有效收益。


本文将围绕相似视频识别技术展开详细介绍,包括架构演进、工程优化、组件沉淀等部分,希望能为有相同诉求的读者带来一些启发。

亿级别相似视频识别的挑战


不同人群对于相似视频识别能力的需求各不相同。对用户来说,推荐池中一旦存在重复视频,就很容易被推荐系统基于画像反复推荐,因此从用户体验考虑,需要更重召回;对号主来说,一旦判断错误,视频被误打击,号主这条视频就不可能再被启用,因此从号主体验考虑,又需要重准确。


综上,我们的相似视频识别能力既要保证足够高的召回率,还必须足够准确,这就需要对多个环节进行全面优化。

原有架构存在不足


早期的相似视频识别架构相对简单,整个去重的流程基本在一个服务中就完成了。比如很早就支持标题去重服务,整个流程包括:对标题提取特征向量,向量写入样本池;然后用此向量检索样本池,召回相似标题;再对召回的相似标题进行校准,是否真正重复;最后做出决策,是否进行去重拦截。


但后来我们发现仅标题去重无法满足产品发展需要,于是仿照标题去重又新增了视频的画面去重、封面图去重等等。系统架构变成了如下图 3 所示:


图 3 早期去重架构


这种架构存在多种问题:


1. 单机存在性能上限


最初每一种召回服务的样本池都是自己在内存中管理。服务都使用大内存机器,进行单机部署,同时部署 1 台冷备机器。主机读写时,与备机进行强一致性数据同步。这样的架构很明显存在性能上限,无法利用分布式的优势。


2. 各去重模块相互耦合,无法利用多种判断结果,重复开发工作量大


对于仅标题重复、画面不重复的视频,如果想要做到不拦截、只打标记,在原有架构下是无法完成的。因为标题去重服务在决策的时候没有画面信息和封面信息。


以一个非常简单的需求为例:调整去重优先级,对不同层级的账号调整对应的优先级。要满足这个需求,上面三个服务每一个都需要调整,相当于三倍开发量。碰到复杂的需求,必定会浪费大量开发人力,同时这种重复代码也不利于维护。


3. 不利于扩展


如果以后需要新增召回策略,只能再次新增一个类似的服务,进一步加剧系统的维护难度。

算法模型服务消耗大量成本


在我们的业务场景下,系统每天需要处理百万级的新增视频,我们需要对视频进行各个维度的特征提取、生成向量,用于召回和校准。而特征提取依赖的算法模型会消耗大量的机器资源,包括 CPU、GPU 和内存存储,存在极大的成本挑战。

检索架构高可用问题


在我们的检索架构中会存储若干天历史视频向量,总体视频数量达千万到亿级,对应的抽帧图数量则达几十亿到百亿级。每新增一个视频,我们都需要在秒级的时间内,从几千万到亿级的视频库中,召回重复视频。


这些业务特点对于我们的相似视频识别架构、算法模型处理效率和检索架构高可用等层面,都提出了非常大的挑战。下面分别从这三个角度出发,介绍我们所做的一些优化。

相似视频识别架构设计


我们重新设计了整个相似视频识别的架构,采用分层设计,把特征提取、召回、校准以及决策分离开。新架构如下图 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 系统上进行评估,达标后再发布上线,减少系统风险。


通过架构优化,我们对整个框架做了更合理的划分,每一层各司其职,同时每一层都可平行扩展,使得开发成本和维护成本都大大降低,也得以更好地支持业务的快速发展。

算法模型服务性能优化


如前文所述,特征提取层和校准层主要负责提取视频的各个特征,会消耗大量的机器资源。本节重点介绍如何提高算法模型服务的处理效率,进而降低机器资源成本。

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 倍。


综上,我们通过优化模型服务的性能,不仅有效提升了服务的处理速度,亦可以大大降低服务成本。

相似内容检索架构优化


我们需要用新入库的内容去检索所有目前已经在库中的内容,根据某种度量方式,来判断内容是否相似。目前业界常见都是将视频整体或者视频帧转为 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 索引机制


如上所述,采用读写分离能够解决索引的实时写入性能问题。但随着业务快速发展,视频量级不断增长,又出现了新的挑战:


  1. 每天新增视频越来越多,小索引即使只保存当天视频向量数据,量级也不再小了,在可预见的将来,混合读写的模式会遇到瓶颈。

  2. 大索引重建耗时太长。因为大索引保存的是 N-1 天的海量数据,数量在几十亿级以上,每天重建需花费数小时以上。

  3. 空闲资源浪费。双 buffer 机制,意味着需要预留 1 倍的资源给备用索引使用。而这些预留的资源只在每天重建的那段时间才会用到,大部分时间处于闲置状态。


为了解决以上问题,我们引入了分 set 索引的机制。即,把大小索引数据拆分成多份(每一份称之为 set),建多个 set,每次只是将增量数据加入需要淘汰数据的那一个 set,那么只需把那一份对应的数据重建索引即可。假设大索引分成 m 个 set,那每天重建大索引时,只要将需要淘汰数据所在的那一个 set 重建即可,预留的资源只需 m 分之一。


这样既加快了重建的速度,也减少了空闲资源。


当然,proxy 也会对分 set 进行适配。向量写入时,hash 写入某一个小索引 set 中;检索时,会并发检索所有的大索引 set 和小索引 set,合并检索结果。


图 10 分 set 后的索引重建


通过分布式向量检索以及分布式向量检索管理系统,大大节约了我们的存储成本,也使得我们的内容管理和检索变得更高效,平行扩展也更方便了。

小结


在业务规模快速增长的情况下,我们重新设计相似视频识别的架构与分层,各司其职,使得各层可以快速水平扩展。对算法模型服务的性能优化,在内容量快速增长的同时,更好的控制了业务成本。


通过对相似内容检索架构优化,有效地支撑了在海量内容的相似内容检索。至此,亿级别的相似视频识别问题基本得到解决。后续我们也会持续优化,提升整套框架的易用性,可配置化,同时也会支持更多的底层的向量检索库,以适配各种不同业务场景的需求。


最后感谢负责人 chale 的支持,这项工作也汇聚了 kiefer、jeven、scales、lusha、alvin、lizhi、yuxuan、edin、bowen、lusheng 等小伙伴的很多心血。


作者介绍:


邹建勋,QQ 小世界内容理解高级算法工程师,曾负责腾讯看点内容理解能力建设以及 QQ 公众号的消息推送系统等,对高性能后台开发,和信息流业务中内容处理有一定的经验。


袁易之,QQ 小世界内容理解算法工程负责人,对信息流业务中内容处理的全链路有较多的经验。


常郅博,曾负责 QQ 公众号关系链系统,腾讯看点内容中心建设等,致力于算法工程效率提升,对内容处理与理解有独到见解。


2022-06-09 17:007261

评论

发布
暂无评论
发现更多内容

ARTS_20200520

凌轩

Java ARTS 打卡计划

如何做好 To B 的 SAAS 服务

路边水果摊

SASS 企业 服务

nginx 概念及上手

HelloZyjS

Elastic Stack 系列专辑

Yezhiwei

elasticsearch Logstash Kibana ELK Elastic Stack

敏捷为什么会失败之「PA-SA-WAKA-DA」理论

易成研发中心

Scrum 敏捷开发 Agile

为提升网点业务员效率,我们做的事情。

黄大路

商业

深入剖析ThreadLocal原理

JFound

Java

Enhanced Github:一个 GitHub 专用的好插件

非著名程序员

GitHub 程序员 效率工具

天天都是520

Neco.W

爱情 表白日

JavaScript 基础拾遗(一)

hq

Java 学习 文章收集

关于架构的几件小事:System context

北风

系统架构 系统性思考 架构师 系统上下文 极客大学架构师训练营

万字长文带你看懂Mybatis缓存机制

程序员小岑

Java 源码 技术 mybatis

职场“潜”规则

L3C老司机

个人成长 职场 新人 人才培养 能力模型

程序员需要了解的硬核知识大全

苹果看辽宁体育

Java c 计算机基础

Redis6.0 多线程源码分析

代码诗人

redis 源码 技术 线程模型

Django的ListView超详细用法(含分页paginate功能)

BigYoung

Python django ListView 分页

当我们持续感觉很糟糕要怎么办

董一凡

写作 生活质量 情感

推动敏捷,就是推动软件业变革

盛安德软件

敏捷 推动软件业变革

Android | Tangram动态页面之路(五)Tangram原理

哈利迪

android

我的编程之路-4(进阶)

顿晓

进阶 看书 编程之路

kotlin 200行代码开发一个简化版Guice

陈吉米

Java kotlin guice ioc mynlp

SQLite是什么

山楂大卷

sqlite 数据库 RDBMS 存储

SpringBoot瘦身

JFound

Spring Boot sprnig

竟然有人想看我的「日记」,满足一下大家

非著名程序员

学习 程序人生 提升认知

识别代码中的坏味道(四)

Page

敏捷开发 面向对象 重构 CleanCode 代码坏味道

JVM源码分析之Java对象头实现

猿灯塔

JVM源码分析之synchronized实现

猿灯塔

2020年全球经济萎缩,火花国际PLUS逆袭而来闪耀数字经济

极客编

企业数字化转型:用 SpreadJS 打造互通互链的电力系统物联网

葡萄城技术团队

数字化转型 SpreadJS 电力

Redis 命令执行过程(下)

程序员历小冰

redis 源码分析

回“疫”录(22):我以为结束了,其实才开始

小天同学

疫情 回忆录 现实纪录 纪实

腾讯信息流亿级相似视频识别技术架构优化实践_语言 & 开发_InfoQ精选文章