微信扫码识别是典型的“线下写作,线上阅读”的业务。业务数据的入库和检索库的构建都在离线环节完成。我们利用爬虫系统采集小程序生态中的商品图片,下载后对图片进行检测裁剪,提取检索特征,最终构建检索库并投放到线上环境。本文将主要介绍这部分的工作。0什么是可识别物可识别物以图片或视频为输入,挖掘微信生态中的商品、物品等有价值的信息。在这里我们基本覆盖了微信上的全量优质小程序电商,覆盖了上亿的产品SKU,聚合了搜一搜、搜狗等微信内的信息,聚合后最终呈现给用户。百度力图、阿里拍力淘也是基于该技术开发的。从工程上来说,识别工作可以分为三个部分,如图1所示:图11算法模型算法端主要是检测模型和多类别检索模型的连续炼金术。检测模型需要返回物品信息在图片中的准确位置;检索模型需要保证同一物品的特征表达尽可能接近。2、离线工程鉴定是典型的“离线写作,在线阅读”的业务。业务数据的存储和检索库的构建都在离线环节完成。我们利用爬虫系统采集小程序生态中的商品图片,下载后对图片进行检测裁剪,提取检索特征,最终构建检索库并投放到线上环境。本文将主要介绍这部分的工作。3.在线部署算法模型和离线生成的检索库最终部署并对外服务。当用户识别出对象后,搜索库会召回一批相似的物品,然后经过一系列复杂的排序和过滤逻辑,最终返回用户看到的结果。1挑战1.数据版本数据版本主要分为两类。一种是算法模型版本。我们有10多种商业模式,平均每周迭代升级2-3次。二是检索库的版本。模型不迭代时,每天合并新数据,即增量迭代;并且每次改变算法模型,特征表达都会改变,需要根据新的特征重新构建检索库,即全迭代。在版本频繁变更的场景下,如何平衡灵活性和安全性。2.数据处理性能目前我们采集的图片数量约为10亿张,平均每天新增1500万张图片。除了图片数量多之外,这个过程中还有很多任务,比如图片下载、物体检测、特征提取等任务。每个任务每天的数据处理量都在千万级别。如何高效处理数据,提升业务迭代效率。3、流程复杂随着业务的发展,简单的业务流程已经不能满足我们日益复杂的业务需求。为了提高业务指标,我们可能还需要图像质量、文本语义、断链、下架产品过滤等任务。如何防止整个系统因越来越多的进程而变得臃肿。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中只有tops,对应的检索id为1;改变检测模型后,检索库v2可以同时检测tops和bottoms,对应的retrievalids为2和3。这样online模块可以逐步更新检索库,不影响在线新旧检索图书馆的存在。如果请求落到旧库,则返回1,如果落到新库,则返回2,但最终会返回正确的结果,结果是一致的。.图5检测模型变化2.2.2检索特征模型变化该场景下的检索数据库变化要复杂得多,检索库存的特征来源于检索特征模型。变更检索模型后,同一张物品图片的特征表达完全不同,甚至维度发生变化,如图6所示。我们需要同步变更检索特征模型服务和新的检索库,实现共存通过双缓存对新旧数据进行处理,并执行严格的路由协议,保证同一请求在同一版本的特征检索服务和检索库中完成。图6检索特征模型的变化2.3数据版本管理系统在开发过程中,算法需要将各种模型下发到离线和线上,离线生成的检索数据库也需要下发到线上,数据版本的迭代也需要考虑版本性的回滚。为了解耦多方之间的依赖关系,避免同步过程中直接操作文件的风险,设计了数据版本管理系统。如图7所示,资源发布者将资源上传到系统中,对应业务、版本号和md5。资源使用者只需要了解对应业务的当前版本号,版本管理系统就会返回对应的资源文件。在实际在线使用中,在线模块会周期性地训练某个业务对应的数据版本文件的md5是否与本地文件的md5一致。如果不一致,它会拉取最新的文件。拉取完成后检查md5是否一致,最后执行更新。当业务模型或检索库需要回滚时,只需要修改配置文件,重启服务即可。图7数据版本管理系统2.4Dockerized对象检测和检索特征提取是典型的图像深度学习任务。业界有caffe、pytorch、tensorflow、tensorRT等多种深度学习框架,部分框架无法保证向上兼容。我们负责炼丹的同学,第一要务就是追求效果指标。尝试各种奇技淫巧所练的炼金术,通常不能很好地适应微信的在线环境。总之,在一个重算法的工程系统中,不仅有业务代码的更新,还有工程环境的迭代。这非常适合使用docker封装和迭代业务环境。通过docker化部署,我们可以更方便的引入更多的开源组件来支撑业务,也可以让我们在一些框架的选择上更加灵活。就我们自己的业务场景而言,我们也可以使用微信深度学习任务平台(院子)的计算资源,属于公共资源,需要抢占使用。Yard也被码头化以执行任务。这为我们的业务使用院子公共资源作为工作节点的临时扩展铺平了道路。3分布式计算我们平均每天有1500万的增量数据,总量是亿级数据。单机必然达不到处理的有效性,只有分布式计算才能满足要求。3.1数据拆分和mapreduce一样,我们需要在map阶段对数据进行拆分。这里的拆分原则除了平均之外,还考虑了从拆分到数据的运行时间。分割太细会降低GPU的运行效率,分割太粗会增加bug修复的时间成本。我们尽量将每个拆分任务控制在1小时内完成,最终的拆分粒度在10w/包左右。3.2数据并行计算拆分数据的并行计算相当于reduce阶段。这里的重点是如何将拆分后的数据分布到多台机器上进行计算。此外,我们也希望公共资源在闲置时可以灵活扩展和访问,从而提高并发处理能力。我们结合zookeeper的分布式锁特性,实现了一套可靠的分布式任务队列。worker使用拉模式从队列中拉取任务。这样做的好处是扩展性好,可以灵活增减堆场的机器资源。如图8所示,当有新的worker接入时,从队列中拉取任务,直接执行,可以实现秒级扩展。图8良好的可扩展性对于我们的场景来说,需要可靠地消费任务,这里的可靠性包括两层意思。首先是避免重复消费任务。我们使用zookeeper的keep-alive锁,通过心跳来保持锁的存活。在图9的时刻1和时刻2,worker只有在队列中获得锁后才能执行任务;如果出现机器宕机,如图9中时刻3所示,锁将自动释放。二是完全消费。我们在任务消费满后删除队列中对应的任务,比如在时刻4的task2。在时刻3,由于机器宕机,task1还没有被完全消费,所以它仍然存在,可以在后面消费。图9可靠消费从理论上讲,我们的消费方式属于至少一次消费(atleastonce)。极端情况下,如果worker在执行完task后crash了,还没有返回状态,这个task还处于未成功消费状态,仍然有可能被后续的worker消费。这里需要保证任务的幂等性。通用计算资源的引入增加了我们的处理能力,但也给我们带来了一些小问题。比如公共集群的机器配置就比我们自己的集群好很多。为了最大化不同集群的GPU性能,我们支持不同集群的不同全局参数配置。而且公有集群和文件系统不在同一个idc,导致网络IO时间长,减少了GPU利用时间。我们在公有集群同一个idc实现了一套文件预取系统。根据任务队列中的任务,将需要消费的文件同步到同一个idc的文件缓存系统中。为了提高GPU的利用率,我们也做了很多的工程优化,这里不再赘述。基于分布式计算框架,我们的计算效率得到了极大的提升。以计算效率最低的目标检测任务为例。目前我们集群的处理能力可以达到5600w张地图/天。如果加上公共计算资源,可以达到1.2亿张图/天(集群12张P4双卡,公共集群yard-g7a集群平均10张双卡,深度学习框架使用的tensorRT)。4任务调度虽然我们每天有大约1500万张原始图片,但最终满足检索数据库要求的产品还不到一半。因为为了保证检索数据的质量,我们会对数据进行多维度的过滤。现在我们的图片从下载到建库要经过30+个中间任务。图10仅显示了主要任务流程模型。图10任务流程4.1任务系统随着任务的增多,尤其是很多任务之间存在复杂的依赖关系,每个任务都不是一个独立的个体,每个任务的成败都会影响到最终的结果。为了更好的管理各个任务的状态,理清任务之间的依赖关系,使项目的复杂度不随着任务的增多而增加,我们开发了一套任务调度系统。调度系统主要由以下几个部分组成:文件系统:文件系统采用微信自研的WFS分布式文件存储系统。我们所有的中间数据和结果数据都存储在这里。存储系统:主要是任务存储和实例存储,与一般的实例存储不同,对于分布式计算,我们在数据维度和类别维度进行了拆分,一个实例包含一个或多个子实例调度系统:主要负责收集和管理任务status,检查任务依赖触发Server:定时循环训练调度系统,寻找满足执行条件的任务实例Taskqueue:存放要执行的任务实例,由worker获取,顺序消费Deployment,只要某个模块没有完全挂掉,整套任务调度可以正常执行。4.2联机服务组合部署对日常例行任务的有效性不敏感,早几个小时或晚几个小时对业务影响不大。但是GPU资源非常宝贵,所以我们结合部署了一些GPU机器和在线GPU服务。结合在线流量屏蔽策略,可以在高峰期将资源借给在线服务,在低峰期运行离线任务。如图12所示,是一个在线模块,参与离线任务的空闲时间调度。我们建议0:00-7:00的低峰时间为离线运行时间,7:00-24:00的高峰时间为在线模块服务时间。.最大限度地利用宝贵的机器资源。图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小结我们已经简单介绍了“扫描识别对象”离线系统涉及的一些关键点,还有一些模块还在优化中。未来扫描识别物体将引入更多场景识别,拓展更多维度项目,追求“万物皆可扫描”的目标。
