【见闻录系列】工作一年总结——复杂度和困难度

从实习开始算起,土豆已经工作一年半了,不过从正式入职的时间来看还不到一年,那么四舍五入下就算个工作一年吧,正好写个工作总结,记录一下心路历程。土豆先后在腾讯,蚂蚁金服和百度三家公司实习过,虽然都是算法工程师岗位,但是三份实习工作的工作内容都不太相同。在腾讯的时候,主要是对一些视频识别&分类的论文进行总结和跟踪,然后在Kinetics数据集上进行复现和一些新方法的探索,在这个期间也总结了一篇比较长的博文《万字长文漫谈视频理解》[1],以及其他一些相关工作的博文[2]...

FesianXu 20220317 at Baidu Search Team

前言

从实习开始算起,土豆已经工作一年半了,不过从正式入职的时间来看还不到一年,那么四舍五入下就算个工作一年吧,正好写个工作总结,记录一下心路历程。土豆先后在腾讯,蚂蚁金服和百度三家公司实习过,虽然都是算法工程师岗位,但是三份实习工作的工作内容都不太相同。在腾讯的时候,主要是对一些视频识别&分类的论文进行总结和跟踪,然后在Kinetics数据集上进行复现和一些新方法的探索,在这个期间也总结了一篇比较长的博文《万字长文漫谈视频理解》[1],以及其他一些相关工作的博文[2]。这段实习时间比较短,土豆自我感觉对组内并没有太多输出,也没有接触到很多工业界的知识,可以看成是在学校学习生活的一种延续吧。第二段实习是在蚂蚁金服支付宝部门担任计算机图形算法工程师,在这段时间里面我从零开始接触了计算机图形学知识,并尝试将计算机视觉的方法(特别是动作识别相关的)应用在图形建模上,这段时间也写了一些博客作为记录[3-6]。第二段实习做了一些demo,同样没有涉及到线上的工作,比较多的还是调研相关的工作。

第一二段实习都是在公司的技术中台部门,而第三段实习经历是在百度的偏向于业务的部门,主要是视频搜索相关的工作,后来土豆也就正式入职百度,继续实习期间的工作内容。总的来说,从百度开始实习到现在正式工作一年多的时间里,从模型离线调研到在线调研,模型上线和评估的过程中,接触到了很多工业界的业务场景和知识,有些知识在学校是难以接触到的,值得独立成文总结下。如有谬误请联系指出,本文遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明并且联系笔者,谢谢

联系方式:

e-mail: FesianXu@gmail.com

github: https://github.com/FesianXu

知乎专栏: 计算机视觉/计算机图形理论与应用

微信公众号: 机器学习杂货铺3号店


复杂度(Complexity)

在接触了工业界的应用场景和工作后,我最大的感触就是:复杂度(Complex)。我之所以用复杂度(Complex)而不是困难度(Complication, Difficulty),是因为如果把整个工作拆分为每个小块,你会发现每部分其实都不算难(或者说大部分都不算难),但是因为整个搜索全链路涉及到的模块太多了,整个全链路需要参与的流程也太多了,组成如此复杂的系统势必带来巨大的复杂度,而合理地控制复杂度并不是容易的事情。土豆认为任何一个系统都可以拆分为复杂度和困难度两个维度进行分析。

我们知道信息检索的基本过程可以分为:信息爬取,召回,粗排,精排,重排等四个步骤,而我们作为搜索策略更为关心的是后面四个步骤。这每个步骤中都蕴含着数不尽的策略在里面,其中不少策略还是上下游耦合在一起的,因此所谓牵一发而动全身,上游某个策略小小的改动可能导致下游排序结果的巨大区别。这意味着在策略迭代过程中,如果在线上出现bad case,要对其进行调试并不是一件容易的事情。导致这个bad case的根本原因可能并不是迭代的策略带来的,比如说可能是某些doc由于在线上的某些特征因某些原因(比如在线特征 or 在线字典请求超时,特征覆盖率问题等等)而没有取到,而该doc又被迭代的策略排到精排了,那么就可能导致bad case,然而这个bad case的本质原因并不是你迭代的策略。这种例子还有不少,总得来说上游策略大的影响更容易传递到下游策略中,如果下游策略无法兜住上游策略带来的影响,就可能爆出很多bad case了。

从我目前的认知来看,我会把复杂度拆分到以下几个较为独立的部分中:离线调研,在线调研,模型上线。其中离线调研和在线调研可能又能分为:数据,模型,人工规则策略,效果评估流程及其辅助工具。在线调研又包含有:模型在线化工具及其流程,在线效果评估等。

离线调研

当分析某个策略的bad case发现了可改进点时,会先考虑对模型或者策略进行离线调研,也就是在离线数据集上进行回归,直到在离线数据集上比基线有较大幅度的提升为止。这个过程通常会包括:数据的升级,模型的改进,人工规则策略的改进和效果评估等。

数据

搜索过程中每一步都是对上一步返回结果更优的排序/召回,精确度越来越高,doc数量也越来越少。显然每一步骤的数据分布也就有着非常大的区别,如果策略生效在某个步骤,如何挑选更符合该步骤的训练和测试数据,如何进行合适的标注,是一个非常关键的问题。如果构建的测试数据并不能很好地表征线上的数据分布,那么即便在离线阶段取得了不错的收益,一旦进入在线调研就可能出现负向的结论。在『数据』这块儿,又可以分为无标注数据和有标注的数据,无标注数据典型的就是用户行为数据,比如展现无点击的query-doc对,点击次数小于某个阈值的doc,停留时间小于某个阈值的doc等等,用户行为在一定意义上可以表示query-doc的需求满足程度。通过筛选出海量的无标注数据,可以对模型进行预训练从而大大增强模型的表征能力。相关预训练在信息检索中的应用见[7,8]。有标注的数据通常数量量级远比无标注数据少,通常是万级别到十万级别,有标注的数据一般可以用于Finetune预训练模型,或者可以用于训练Learning To Rank(LTR)模型,比如GBDT/GBRank模型。通常对于标注数据而言,根据产品的定为,会有若干档位,比如描述相关性的0-4档,描述质量性的0-3档,搜索过程中的不同阶段中的数据档位分布也是不同,而用于Finetune预训练模型和训练LTR模型的训练数据的档位分布是很重要的,如果数据档位构建有偏置,很容易出现离在线效果不一致的现象。

注意到即便是某一步骤的数据分布和数据档位分布也不会是一成不变的,而是随着时间而变化的,这意味着需要定期对离线数据集进行更新,同时需要定期对模型进行迭代。在这个过程中会产生若干版本的数据集,当迭代次数多了后,对数据集的版本管理问题也是一件不可忽视的复杂度问题。人都会犯错,特别是在一堆命名规则不一致,版本管理混乱,数据预处理过程模糊,产出时间不明的数据集面前,一旦用期望外的数据集进行了模型的训练和测试,将会产生难以debug的问题,模型评估的结论也会不置信,甚至影响后续的在线调研阶段,产生很大的离在线不一致问题。解决问题的最好时机便是在问题发生之前,土豆在吃了好一些实践中的教训之后,发现数据集的版本管理和有效的文件命名规则是相当必要的,对于我来说,通常会考虑这样对数据文件管理:

对于普通的小规模数据集而言(通常数据量在几十万以下,用单个或少量文件可以组织,通常是文本形式的数据),进行规则的数据文件命名管理,命名规则如下:

文件名规则为: comment_phase_num.date.producer.process.type

命名字段字段有效取值范围字段含义示例
comment[str]简明的数据备注,用于区分数据的用途和使用场景,必要时还需要建立专门的wiki,用于阐述数据的细节qt_rel_v1
phasetrain/eval/test表示数据用于的阶段,通常有训练,验证和测试等-
num[int/str]数据的规模,当数据比较大时,可以用12k, 3w简化名称12349, 12k, 10w
date[int]数据产出的日期20220314
producer[str]数据产出人,用于后续复盘问题时候跟踪数据来源Fesianxu
process[str]通常数据会经过一系列的处理,比如过滤掉某些无效字段,拼接上某些字段等等,为了有效地对数据进行标识,可以考虑添加这个字段对数据进行命名可参考的字段有:orig-原始文件, dedup-去重后的文件
type[str]数据扩展名,用于表征数据的字段组织形式,比如csf, tsf, meta, csv等-

对于大规模的数据(通常是用于预训练的数据),规模在千万到亿级别,由于数据量过大会采用分part的方式储存在集群中,这个时候无法对每个文件名进行统一管理(每个part的文件名通常是part-xxxxx,同个数据集的所有part都在同一个文件夹内储存),通常对文件夹名进行管理即可,并且最好在wiki中对数据细节进行记录。

数据命名方式无法表明数据的分布情况,还需要同步有wiki记录每个数据集的数据分布情况,比如数据档位分布情况等。

工作中遇到的很多数据都是以文本的形式组织的,即便遇到多模态数据集,也会将图片转成base64编码的字符串后进行储存。通常文本形式的数据会存在有多个字段,不同字段之间通过特殊不可视字符隔开(比如\t \1 \2 )等,为了方便日常工作中对数据进行查看,处理和分析,有以下若干工具是值得深入学习和掌握的,比如:

  1. awk: 一个强大的文本处理工具,以数据行为处理单位,提供有很多基本的字符串处理函数,并且提供有类C语言的编程接口,可以对数据字段进行灵活的处理,支持正则表达式
  2. sed: Stream EDitor, 对数据流的流式处理,可以自动编辑和处理文本,比如字符串替换,查找删除,指定行输出等等,支持正则表达式
  3. tr: TRansform, 用于对字符串的转换或者删除
  4. grep: 专为字符串查找而生,用于查找字符串中符合条件的字符串,支持正则表达式

之前土豆整理了一些在工作中比较常用的一些命令 [9],欢迎一起交流~ 数据是模型的粮食,可以说万物源于数据,再怎么小心都不为过。

模型

就土豆的感知而言,对于搜索系统而言,在大部分场景中,数据的关键程度要比模型高。众所周知,对模型结构的改造和设计,很多时候可以看成是对建模问题引入先验知识,比如卷积网络的设计初衷就是引入了视觉统计特征具有局部性的先验知识,而引入先验知识的主要原因很大程度上在于可利用的数据稀少,通过引入模型先验知识从而减少数据的需求。然而,搜索系统中有着大量可用于预训练的用户行为数据,这些数据虽然嘈杂(可视为弱标注数据),经过一番筛选和滤波后仍可提供非常宝贵的信息量。当一个模型有足够的表征能力时,通过充足的预训练足以提供良好的模型先验,而类Transformer模型(如BERT,ERNIE,GPT等)正是这一类模型。以百度的ERNIE [10,11]为例,在大部分场景中对模型结构的改动是很微小的,大多集中在:

模型改动目的举例
嵌入特征维度模型嵌入特征的维度,大部分时候都是需要结合模型部署的性能和资源消耗(比如推理时间,吞吐,时延,需要计算实例等)以及模型性能(模型表征能力,模型准确度等)进行权衡768
层数同样,模型层数也是结合模型性能和部署压力进行权衡的一个超参数3, 6, 12
FC层数在Transformer后面通常会连接若干FC层,这些FC层和任务强相关,其层数以及激活函数,Batch Norm/Layer Norm布置通常也需要考虑
词表词表(vocabulary)也是模型一个很重要的因素,采用词(word)粒度词表还是字(character)粒度词表,亦或是混合粒度词表,词表的大小等等在不同应用场景中都是需要考量的因素

在精排阶段需要更为精确的模型打分,通常是Transformer模型大展身手的地方,一些涉及到Query-Doc联合建模的单塔Transformer模型(比如Query-Title相关性,Query-Title-Content相关性)需要在线计算。层数较多的Transformer模型计算复杂度较高(当然效果也更好),通常需要大量资源才能支撑得起,为了减少资源的消耗需要进行模型的小型化,比如模型量化,剪枝,蒸馏等,以微小的模型性能损失换来大量的资源节省。

在模型阶段很重要的一点是模型的训练超参数配置和训练方式,比如模型的初始化方式,训练的学习率,优化器,权重衰减方式,学习率采用的一些动态更新策略(余弦衰减,退火,warmup等),对于多任务学习,还需要考虑不同任务的训练方式(交替进行?用加权loss的方式同时训练?可持续训练?),对于GAN这类训练需要精细控制Generator和Discriminator训练过程的模型来说可能就更需要注意了。

个人认为,模型层面的复杂度主要体现在:

  1. 通常调优一个模型需要尝试很多超参数,这意味着需要进行很多对照实验,当实验数量多起来后如何对实验进行良好管理是一种复杂度问题,一个实验的什么指标是需要记录的?训练曲线,测试指标,测试样本的原始打分输出,超参数网格,数据集,数据处理方式... 如何对实验进行管理也是一个值得注意的内容。

  2. 通常在对模型调优过程中会对代码进行多次修改和调整,如果修改过于频繁,而且没做好代码版本管理,那么就会是一场噩梦。想象下你昨天在某个旮旯角轻微修改了模型的结构,睡了一觉第二天可能就忘记了,然后跑了一版调参实验... 这个轻微的修改将会对后续所有实验造成影响,并且是在你不知晓的前提下,导致所有实验结论可能都不置信。对此,有以下建议:

    • 代码的版本管理:代码管理永远是管理复杂度的一个神器,最常用的莫过于gitsvn,永远保持stable分支稳定可用,dev分支进行架构开发迭代,expt分支用于日常实验(通常expt分支只需要修改超参数配置,而不需要改动代码),对commit进行规范的命名,比如参考文章优雅的提交你的 Git Commit Message [12], 采用[head, body, footer]的形式进行命名。永远不要为难未来将会阅读自己代码的自己。
    • 代码架构功能分离: 对整个代码架构进行合理的功能分离,比如将整个项目分为模型层(model),模块层(module,模型层将会从模块层调用小模块构成模型,比如RNN模块,Conv模块,Transformer Encoder模块等), 配置层(config,以yaml或者其他格式储存超参数),数据读取层(data_reader),数据广场(data_playground,可用于数据分析,后处理等,放置jupyter notebook脚本等),主程序层(main_process,用于组织训练和测试过程,是整个训练/测试任务的入口),启动层(launch,用于放置任务启动脚本,用于调用主程序进行训练/测试,通常还需要负责数据挂载,日志文件目录有效性检查,随机种子设置等等辅助过程),优化器层(optim,用于放置需要的优化器),参数解析器(parser,定义参数解析器,通常可以从yaml文件里读取配置),工具类(utils,一些常用工具文件),第三方层(thrid_party,如果项目需要调用第三方模组,可以考虑定义第三方层), 一个简单但不完全的例子可以见土豆两年前的小项目 [13]。通过代码架构的功能分离,可以将模型调优的过程局限在超参数层中,尽量减少不同功能代码的耦合。
    • 合理类继承: 对于一个可能会经常迭代的复杂模块而言(比如数据读取器DataReader),将其进行功能拆解,拆解成基类和子类,比如BaseDataReader将迭代过程中不会经常变动的共有功能(比如读取文件,解析,令牌化,拼接等)进行实现,通过子类继承BaseDataReader实现更为定制化的数据读取过程。这样即可以在基类里面定义接口,又可以在子类里面定制特定实验需要的功能。相似的,在Model里面也可以分为BaseModel和子类model。

过早的优化是万恶的根源』,在项目一开始不需要设计的非常细致,但是需要时刻关注整个项目的复杂度,一旦复杂度过高导致迭代困难,应该及时考虑优化。

在线调研

当离线调研得到显著正向结论后,就需要进行在线调研了。在线调研指的是通过真实的用户搜索行为,从最终返回的doc排序中对比策略和基线的优劣。显然在线调研是一个端到端的过程,包括你迭代的策略在内,整个搜索系统的绝大部分现有策略都会参与,因此最终结果将会受到多方影响,这是典型的一个复杂系统。即便策略在离线调研中有着非常大的收益,进入在线调研后结果持平甚至出现负向结论都是可能的,原因有多方面:

  1. 离线调研采用的测试集无法描述在线数据分布,因此离线调研拿到的收益不置信,导致线上结果不尽人意。
  2. 策略的收益被其他策略『吃掉』了,有可能你迭代的策略和现有的某个策略有重复的功能,一旦上线你的模型相当于就被其他策略取代掉了。理论上,这种情况应该在离线调研中能够被发现,因为在离线调研中通常还需要重新训练LTR模型,通过观察LTR模型的特征权重变化可以判断迭代策略的特征是否和其他特征功能上有所重合。
  3. 特征覆盖率问题,离线调研数据和线上数据的特征覆盖率由于资源的原因,可能并不能完全对齐,特征覆盖率的差别有可能导致离在线区别。
  4. 策略可能在某些query下效果就是较差,比如一些长尾query,其召回的doc通常也会比较长尾,可能相关性模型对其的判断能力就较弱。如果离线数据在采集过程中没有对特定query进行采集,有可能会导致这种区别。
  5. 代码bug,这一点也是经常出现的,比如土豆就曾经出现过不小心在线上代码中将输入提前截断,导致某些case下离在线打分diff的情况... :P 这一点可以认为是需要对齐线上线下的数据处理,数据如果都没对齐那么结论也是不置信的。
  6. 离在线输入数据的差别,这一点和5其实不太一样,有可能线上建库时候有特别的逻辑,导致线上源数据输入和线下数据就是不一致的,比如线下可能是全角标点符号,而建库时候都被处理成了半角标点符号 :- (

导致离在线结论差别的原因太多了,五花八门,其中很大程度上是因为整个搜索系统太过于复杂,没有人能了解其中所有策略的细节,可能在某个旮旯角就出现了预期外的情况。对其怎么解决,土豆暂时也没有思路,只能平时尽量小心,尽量不要把低级bug引到在线调研中吧,希望在以后的工作中能总结出有用的结论出来。

困难度 (Complication)

然而系统并不是只有复杂度,而没有困难度的,控制复杂度能保证系统的高效迭代,解决困难度能保证系统的领先性。对于数据这块而言,个人感觉困难度集中在如何挑选合适的数据,这又可以分为几个维度考虑:

  1. 如何挑选合适的训练数据。模型是否需要预训练?预训练数据应该从哪里构造,用户行为数据中是否会存在预期外的偏置?采用有展现无点击的数据作为负样本是否合适 [15],是否所有有点击数据都能作为正样本? 是否需要从粗排数据中进行随机采样?还是需要从精排数据中进行采样?用户和用户之间是否可能存在隐式关系,可以用于数据挖掘? 是否需要数据标注,如何标注,标准是什么,如何筛选数据去送标?这些都是问题。
  2. 如何挑选合适的测试集。离线调研中测试集用于评估策略对比基线的有效性,如果测试集无法正确描述在线数据情况,那么将会导致在线调研结论不符合预期。测试集的挑选同样面临着训练集相似的问题,在某些垂类细分问题上,如何正确的构建测试集的正样本和负样本并不是一件容易的事情,有时候甚至问题都不容易定义,很容易导致离在线的diff。
  3. 数据需要提供什么信息量。数据需要提供什么信息直接影响模型应该如何构建和训练,对于相关性而言,可能构建Query-Title相关,Query-Title-Content相关,Query-Content相关;对于质量而言可能构建Title-Content质量性。如何结合资源挑选合适的数据输入,如何把每个输入信息单元精准做好,也是一件困难的事情。

对于模型而言,需要探索一些在特定场景下有效的模型结构和训练方式。比如在多模态检索中,近年来大规模跨模态对比学习的兴起,已经证实了其在搜索应用中的有效性[15,16,17],一些新的基于memory bank和momentum更新的方法使得对比学习的规模愈来愈大[15,18,19]。这些对模型的改进,或者模型训练方式的改进,一旦在应用中落地,就有可能拿到巨大的收益。

总结

虽然感觉已经写了蛮多了,也不知道对诸位读者有没有帮助,其实在工作中还遇到很多『复杂度』的内容,比如流程性的东西,代码的准入,测试,上线规范,资源申请,诸多内部工具的使用(百度的基建很完善,有很多内部工具给策略同学提供了便利)等等,然而这些东西更多的是『繁琐』,有时候也会在这些流程中踩坑,但是写出来一是可能违规,二是可能对大家也没帮助,因此也就不提了。希望后续还能对这个系列进行更新,分享一些工作中学习到的知识。

Reference

[1]. https://blog.csdn.net/LoseInVain/article/details/105545703

[2]. https://blog.csdn.net/LoseInVain/article/details/108212429

[3]. https://blog.csdn.net/LoseInVain/article/details/107265821

[4]. https://blog.csdn.net/LoseInVain/article/details/107720442

[5]. https://blog.csdn.net/LoseInVain/article/details/108322781

[6]. https://blog.csdn.net/LoseInVain/article/details/108710063

[7]. Liu, Yiding, Weixue Lu, Suqi Cheng, Daiting Shi, Shuaiqiang Wang, Zhicong Cheng, and Dawei Yin. "Pre-trained language model for web-scale retrieval in baidu search." In Proceedings of the 27th ACM SIGKDD Conference on Knowledge Discovery & Data Mining, pp. 3365-3375. 2021.

[8]. Fan, Yixing, Xiaohui Xie, Yinqiong Cai, Jia Chen, Xinyu Ma, Xiangsheng Li, Ruqing Zhang, Jiafeng Guo, and Yiqun Liu. "Pre-training Methods in Information Retrieval." arXiv preprint arXiv:2111.13853 (2021).

[9]. https://blog.csdn.net/LoseInVain/article/details/120272575

[10]. Sun, Yu, Shuohuan Wang, Yukun Li, Shikun Feng, Xuyi Chen, Han Zhang, Xin Tian, Danxiang Zhu, Hao Tian, and Hua Wu. “Ernie: Enhanced representation through knowledge integration.” arXiv preprint arXiv:1904.09223 (2019).

[11]. https://blog.csdn.net/LoseInVain/article/details/113859683

[12]. https://juejin.cn/post/6844903606815064077

[13]. https://github.com/FesianXu/LipNet_ChineseWordsClassification

[14]. https://blog.csdn.net/LoseInVain/article/details/113706168

[15]. https://blog.csdn.net/LoseInVain/article/details/120364242

[16]. https://blog.csdn.net/LoseInVain/article/details/122735603

[17]. https://blog.csdn.net/LoseInVain/article/details/121699533

[18]. https://fesian.blog.csdn.net/article/details/119515146

[19]. https://fesian.blog.csdn.net/article/details/119516894