原文:微信扫一扫识物的技术揭秘:抠图与检索 - 2020.04.24

出处:腾讯技术工程 - 微信公众号

作者:冉辰,腾讯 WXG 后台开发工程师

微信扫一扫识物是典型的“离线写,在线读”的业务,业务数据的存储和检索库的构建都是在离线环节完成. 我们通过爬虫系统收录了小程序生态下的商品图片,下载后进行检测抠图,提取检索特征,最终构建成检索库交付到线上环境. 这篇文章将主要介绍这一部分的工作.

0. 什么是识物

识物是以图像或视频作为输入,用以挖掘微信生态下商品、物品等有价值等信息. 这里我们基本覆盖了微信全量优质小程序电商,涵盖上亿商品 SKU,聚合了微信内的搜一搜、搜狗等资讯,最终聚合后呈现给用户. 百度识图和阿里拍立淘也是基于该技术发展而来.

工程上,识物工作主要可以分为三块,如图 1 所示:

0.1. 算法模型

算法侧主要是对检测模型和多类目的检索模型等持续炼丹,检测模型需要返回图片中物品的准确位置;检索模型需要保证同款物品的特征表达越近越好.

0.2. 离线工程

识物是典型的“离线写,在线读”的业务,业务数据的存储和检索库的构建都是在离线环节完成. 我们通过爬虫系统收录了小程序生态下的商品图片,下载后进行检测抠图,提取检索特征,最终构建成检索库 交付到线上环境. 这篇文章将主要介绍这一部分的工作.

0.3. 在线部署

算法模型和离线生成的检索库最终完成部署,对外服务. 用户识物时,检索库会召回一批相似物品,再经过一系列复杂的精排、过滤逻辑,最终返回用户看到的结果.

1. 挑战

1.1. 数据版本

数据版本主要分为两类:

一是算法模型版本,我们有 10+种业务模型,平均每周有 2-3 个模型迭代升级.

二是检索库版本,在模型不迭代的情况下,每天有新数据的合并,即增量迭代;

而每次算法模型变更,特征表达发生改变,需要根据新特征重新构建检索库,即全量迭代.

在高频的版本变更场景下,如何兼顾灵活性与安全性.

1.2. 数据处理性能

目前我们收录的图片数为 10 亿左右,平均每天新增 1500w. 除了图片数量多,任务的流程也很多,如图片下载、目标检测、特征提取等任务,每个任务每天都是千万级的数据处理量.

如何高效的处理数据,提升业务的迭代效率.

1.3. 繁杂的流程

随着业务的发展,简单的业务流程已经不能满足我们日益复杂的业务需求. 为了提升业务指标,我们可能还需要图片质量,文本语义,死链、下架商品的过滤等任务.

如何在流程日益变多的情况下,不导致整个系统的臃肿.

1.4. 数据质量

离线工程属于重流程的业务,数据从产生和落地将经历九九八十一环,任何一环出错都会导致结果有问题. 发现问题的时间越晚,修复的成本越高,对业务的影响越难以估计.

如何科学的监控和管理数据质量,使系统有良好的可维护性.

2. 数据版本

这里有多种维度的数据版本,例如模型版本,特征版本,检索库版本等,上游环节的版本变更将引发后续环节的变更,最终都将导致检索库版本变更.

图2. 数据流程简图

2.1. 检索库

在我们的业务场景下,检索库的迭代是高频操作,正常情况下每天会增量更新,而模型的变更又会引发检索库全量更新. 数据量级上,我们的全量图像是亿级别的,按类目分库后每个类目也是千万级.

我们调研了业界内主要用于图像检索的技术,如图 3 所示. 综合考虑后,我们选取了灵活性更强、相对内存占用更小的的 faiss-ivf 作为我们的索引库构建算法.

图3. 图像检索库选型

对于每天的增量数据,我们每天对每个类目(10+个类目)都会构造一个对应当天数据检索库. 每个类目的全量检索库是由N 天的检索库合并生成(faiss-ivf 特性),2000w 的数据合并仅需要 4 分钟. 基于这样的设计,使得我们可以灵活的选取时间窗口的范围,如图 3 所示了窗口为 2 的合并方法.

这样的好处是,如果某天数据发现有问题,只需要修复当天数据后再进行合并即可;如果需要丢弃某些数据,如旧数据,合并时不选取即可.

图4. 检索库生成

2.2. 数据版本兼容

前面我们讲到,模型变更最终都将引发检索库的全量迭代,这里的模型有检测模型和检索特征模型. 新检索库上线时,本质上是新旧数据的过渡,一般实现新旧数据的切换都会设计复杂的系统来保证数据一致性.

2.2.1. 检测模型变更

这种场景下的检索库变更,严格上来讲我们并没有实现新旧数据的一致性,我们只是通过简单的方法使得即使新旧数据同时存在也不影响用户的体验.

这里主要涉及到如何构建我们的映射关系,我们为每次检测出的结果都赋予一个唯一的单调递增 id. 替换模型后,同一张图片的检测结果会变化. 可能抠图的位置有变化、可能会扣取不同的物品、可能会扣取多个物品.

如图 5 所示,检索库 v1 里只有上衣,对应检索 id 为 1;变更检测模型后,检索库 v2 可以同时检测出上衣和下衣,对应检索 id 为 2,3. 这样在线模块可以逐步更新检索库,线上同时存在新旧检索库也没有影响,如果请求落到旧库返回 1,落到新库返回 2,但最终都将返回正确的结果,结果上是一致的.

图5. 检测模型变更

2.2.2. 检索特征模型变更

这种场景下的检索库变更则复杂许多,检索库存的特征来自于检索特征模型. 检索模型变更后,同一个物品图片的特征表达完全不同,维度甚至也发生了变化,如图 6 所示.

我们需要同步变更检索特征模型服务新检索库,通过双 buffer 的方式实现新旧数据的共存,而且要实现严格的路由协议来保证同一个请求在同版本的特征检索服务和检索库中完成.

图6. 检索特征模型变更

2.2.3. 数据版本管理系统

在开发过程中,算法需要交付各种模型给离线和在线,离线生成的检索库也需要交付给在线,数据版本的迭代也需要考虑版本的可回退性. 为了解耦多方之间的依赖,且避免在同步过程中直接操作文件带来的风险,设计了一套数据版本管理系统.

如图 7 所示,资源发布者上传资源到该系统,并附带对应业务、版本号及 md5. 资源使用者只需要理解对应业务当前的版本号,版本管理系统会返回对应的资源文件. 线上实际使用时,在线模块会定期轮训某业务对应数据版本文件的 md5 和本地文件 md5 是否一致,不一致则会拉取最新的文件,拉取完成后校验 md5 是否一致,最终实现更新.

在业务模型或检索库需要回退时,只需修改配置文件,重启服务即可.

图7. 数据版本管理系统

2.4. docker 化

目标检测、检索特征提取等是典型的图像深度学习任务,业界内有 caffe、pytorch、tensorflow、tensorRT 等多种深度学习框架,有的框架不能保证向上兼容. 而我们负责炼丹的同学第一要务是追求效果指标,在尝试各种奇淫巧技时练出来的丹通常并不能和微信的线上环境很好的兼容.

简而言之,在重算法的工程系统中,不仅有业务代码的更新,还有工程环境的迭代. 这非常适合使用 docker 来封装和迭代业务环境. 通过 docker 化部署,我们可以更方便的引入更多开源组件来支撑业务,也可以让我们在一些框架选型上更加灵活.

就我们自己的业务场景而言,我们还可以利用微信深度学习任务平台(yard)的计算资源,这部分属于公用资源,需要抢占式使用. yard 也是 docker 化去执行任务. 这为我们业务可以借助 yard 公用资源作为临时扩容 worker 节点做了很好的铺垫.

3. 分布式计算

我们每天平均有 1500w 增量数据,全量为十亿级别的数据. 单机必然无法满足处理的实效性,唯有分布式计算才能满足要求.

3.1. 数据拆分

正如 mapreduce,map 阶段的工作我们需要对数据进行拆分. 这里对拆分原则除了平均外,还考虑了拆分后到数据的运行时间. 如果拆分太细 GPU 的运行效率会降低,拆分太粗会导致错误修复的时间成本变大. 我们让每个拆分后的任务都尽量控制在 1 小时内完成,最终拆分的粒度为每个包 10w 左右.

3.2. 数据并行计算

拆分后的数据进行并行计算相当于 reduce 阶段,这里的重点是如何将拆分后的数据分发到多台机进行计算. 此外,我们还希望公用资源空闲时可以非常灵活的进行扩容接入,提高并发处理能力.

我们结合 zookeeper 的分布式锁特性,实现了一套可靠分布式任务队列. worker 采用拉模式拉取队列的任务. 这样的优点是伸缩性好,可以灵活的增加和减少 yard 的机器资源. 如图 8,当新 worker 接入后,从队列中拉到任务直接执行,可以实现秒级的扩容.

图8. 伸缩性好

对于我们的场景,任务需要被可靠消费,这里的可靠包含来两层含义.

第一是避免任务被重复消费,我们借助 zookeeper 的保活锁,锁通过心跳保持活性. 如图 9 中第 1,2 时刻,worker 拿到队列里的任务抢锁成功才可执行;如果出现机器宕机,如图 9 第三 3 时刻,锁会自动释放.

第二是完整消费,我们在 task 被完全消费结束后才删掉队列里的对应 task,如时刻 4 的 task2. 时刻 3 由于机器宕机,task1 并未被完整消费,因此依旧存在,后续可被继续消费.

图9. 可靠消费

理论上讲,我们的消费模式属于至少一次消费(at least once),极端情况下,如果 worker 执行完任务还没有回传状态时宕机,那任务仍处于未成功消费,仍可能被后续 worker 消费. 这里需要保证任务的幂等性.

引入公用计算资源提升了我们的处理能力,但同时也给我们带来了一些小问题. 例如,公用集群的机器配置比我们自己集群要好很多,为了使不同集群都能发挥最大的 GPU 性能,我们支持不同集群使用不同的全局参数配置. 而且公用集群和文件系统不在同一个 idc,导致网络 IO 时间过长,降低了 GPU 利用时间,我们在公用集群的同 idc 实现了一套文件预拉取系统,根据任务队列中存在的任务,提前同步待消费文件到同 idc 的文件缓存系统.

为了提高 GPU 利用率,我们还做了大量的工程优化,这里就不展开叙述了. 基于分布式计算的框架,极大提升了我们的计算效率. 拿计算效率最低的目标检测任务举例,目前我们集群的处理能力可达到 5600w 图/天,如果加上公用计算资源,可以达到 1.2 亿图/天(集群 12 台 P4 双卡,公用集群 yard-g7a 集群平均 10 个双卡,深度学习框架使用的 tensorRT).

4. 任务调度

虽然我们每天有 1500w 左右的原始图片,但最终符合录入检索库的商品仅有一半不到. 因为为了确保检索数据的质量,我们会在多个维度做数据过滤. 现在我们的图片从下载到建库一共会经历 30+种中间任务,图 10 仅展示了主要的任务流程模型.

图10. 任务流

4.1. 任务系统

随着任务的增多,尤其是许多任务间存在着复杂的依赖关系,每个任务都不是一个独立的个体,每个任务的成败都将影响最终的结果. 为了更好的管理每个任务的状态,梳理任务间的依赖,使得工程的复杂度不随任务变多而变大,我们自研了一套任务调度系统.

调度系统主要由以下几个部分组成:

  • 文件系统:文件系统这里使用了微信自研分布式文件存储系统的 WFS,我们所有中间数据和结果数据都存放在这里
  • 存储系统:主要有任务存储和实例存储,与一般实例存储不同的是,为了分布式计算,我们在数据维度和类目维度做了拆分,一个实例包含一个或多个子实例
  • 调度系统:主要负责收集、管理任务状态,检查任务依赖
  • 触发器:定时轮训调度系统,找到满足执行条件的任务实例
  • 任务队列:存储待执行的任务实例,由 worker 获取依次消费

图11. 任务调度系统

容灾性上,调度系统相关的模块均是多机多园区部署,只要不是某个模块完全挂掉,整套任务调度都可以正常执行.

4.2. 在线服务合并部署

对于每天的例行任务,实效性并不敏感,早几个小时或晚几个小时对业务影响不大. 但 GPU 资源是是十分宝贵的,我们将部分 GPU 机器和在线 GPU 服务合并部署. 结合在线流量屏蔽策略,实现高峰时期资源借给在线服务,低峰时期运行离线任务.

如图 12 所示,其为一台参与离线任务闲时调度的在线模块,我们拟定每天 0 点-7 点的低峰时间为离线运行时间,7 点-24 点的高峰时间为在线模块服务时间. 最大限度的利用了宝贵的机器资源.

图12. 分时调度运行

5. 数据质量

前面做的工作保证了我们以任务为粒度的工程可靠性,但任务的成功并不能保证业务数据是完整的,如数据丢失、代码逻辑有问题等. 为了监控数据维度上的业务质量,我们基于 ELK 搭建了一套数据系统,主要用于收集重要的基础数据、业务数据、运行结果等.

5.1. 数据可视化

我们曾在几次版本迭代过程中,发现数据出错,但发现时已经付出了极高的时间代价. 因此我们希望在任意时刻都能观察离线系统的运作是否正常,数据的流转是否符合预期. 出现问题后可以及时干预修正,降低错误成本.

我们对涉及数据流转的核心任务都做了数据结果上报,这样子我们可以通过数据漏斗发现是否出现问题. 这个问题在全量数据重跑的时候尤其重要. 图 13 展示了项目中核心任务的数据情况.

图13. 数据漏斗可视化

上图看上去是每天任务级的数据监控,但实际上我们我们的设计是扩展到了每次任务级(这里定义为 planid),既可以是每天,也可以是每次覆盖多天的重跑. 我们按图 14 的字段上报业务的运行结果,前 4 个字段组成联合唯一索引,planid 作为区分每次运行的逻辑字段. 这样即使同一个任务在不同时期运行结果是不同的,我们也能区分每一次运行后,真实的数据结果. 这个设计在保证每次大版本数据迭代时,对于把控数据整体运行质量十分重要也十分有效.

图14. 上报字段

5.2. 一致性检查

数据可视化方便了我们检查问题,但是还不利于我们发现问题. 我们还需要在数据出问题时,能及时告警、迅速修复. 这最最重要的就是数据一致性,这里的一致性主要是一些核心任务的数据漏斗,输入和输出应该是一致的. 图 15 中展示了一些存在关联的任务,带颜色线段代表数据存在关联性.

为了满足各种维度的统计、校验,同时又能快速支持新任务的检查. 我们封装了核心的统计和校验逻辑,配置化告警任务,确保层层流程运转后的结果准确无误.

图15. 一致性检查

5.3. 评测系统

我们在对我们的检索库做比较大的版本迭代,或是线上策略有比较大调整时,直接灰度上线再观察曲线有时并不能及时发现问题,存在很大的隐患. 基于这种情况,我们开发了自动化测试系统. 我们提前收集和整理了部分带标签的数据样本,每次更新都需要在测试环境自动化评测一次,如图 16 所示. 我们在结合具体指标分析此次迭代是否可以安全上线(关键数据打码).

图16. 评测系统

5.4. 数据淘汰

我们平均每天流入数据超过千万,数据膨胀的速度非常快,这给我们带来了极大的存储成本和迭代成本. 但回顾业务本身,其实许多商品数据在随着时间的推进,将变成过期、死链、下架数据. 最简单的做法就是使用窗口期来维护我们的数据,窗口外的数据自动淘汰,我们在 faiss 检索库选型时也是这样考虑的.

但是我们也想到,直接暴力的淘汰旧数据也会有个致命问题. 对于我们的业务而言,什么数据对我们是重要的,常见的热门商品固然重要,但是相对冷门长尾商品同样重要,后者决定来商品库长尾的多样性. 如果删掉某个商品,检索库可能就没有这个商品了,这是十分糟糕的.

因此我们在做数据淘汰的时候,需要考虑对有价值的长尾商品做保留. 图 17 展示了我们数据淘汰的方式,通过这种方式,我们窗口期内的数据质量将变得越来越高,全量数据的增长也相对可控.

图17. 窗口期为K的数据淘汰

6. 总结

以上我们大致介绍了“扫一扫识物”的离线系统中的所涉及的一些关键点,部分模块仍在持续优化中. 未来扫一扫识物将引入更多场景的识别,拓展更多维度的物品,追求“万物皆可扫”的目标.

Last modification:April 29th, 2020 at 05:15 pm