论算法工程师的自我修养
前言
本文落笔于2021年的1月1日,是我2021年的第一篇文章。本文将根据我的实践经验,讨论一个算法工程师,如何提升自我修养,由菜鸟小白进化为高手。
本文同样不关注某个具体算法那样的“术”,也不像《推荐算法的"五环之歌"》讲“算法”之道,而是聚集于“算法工程”之道,即如何将算法从纸面落地于实际项目中。毕竟大家都是工程师出身,做算法的目的,既不是在象牙塔中灌水发文章,也不是打Kaggle比赛那种"一锤子"买卖。我们做算法的终极目标,是要在复杂的互联网工业环境中成功落地,能够取得收益,最重要的,能够持续取得收益。否则,掌握再多的NN、FM、Attention,也只不过是纸上谈兵罢了。
理论化:扎实的知识基础虽然讲工程, 但还是需要对算法的充分理解做基础。项目开始前,选择最可能产生收益的算法来实现,算法遇到问题,找到最可能的原因,这些都需要扎实的算法理论为后盾。否则,如果你只会将别人口中的知名的算法挨个试一遍,在机器已经开始能够自动调参,能自动搜索最优网络结构的今天,你作为一个工程师的价值在哪里?
体系化+脉络化
现在讲算法的文章不是太少,而是太多,信息爆炸。每年KDD, SIGIR, CIKM上有那么多中外的王婆一起卖瓜,各种各样的NN、FM、Attention满天飞,其中不乏实打实的干货,更不缺乏湿漉漉的灌水文,让算法新人无所适从。这是因为这些同学孤立地读论文,结果只能是一叶障目,只见树木,不见森林。轮到自己的项目,选作者来头最大的、发布时间最新的来实现,与“听小道消息买股票”没啥区别。
正确的姿势应该是,梳理一门学问的脉络,构建自己的知识体系。只有这样,才能真正将各篇论文中的观点融汇贯通。在自己的项目中,采各家之所长,构建最适合自己项目的算法。我已经在《推荐算法的"五环之歌"》和《无中生有:论推荐算法中的Embedding思想》梳理了推荐算法的发展脉络,请感兴趣的同学移步阅读。
重视基本功
现如今,有些人将各种NN+FM+Attention像搭积木一样拼来拼去,组成一个全新的网络结构,起个古怪的名字,就能够在KDD, SIGIR, CIKM成功灌水,让一众算法新人趋之若鹜。我劝大家,不要沉迷于奇形怪状的NN结构,千万不要以为它们是能够解决你问题的“银弹”,更不要相信什么“深度学习使特征工程过时”这样的外行话。
像我在《负样本为王:评Facebook的向量化召回算法》一文中的观点,“排序是特征的艺术,召回是样本的艺术”,特征工程、样本选择这样的基本功,才值得广大算法同行重视。否则,喂入的样本与特征都有问题,garbage in, garbage out,再fancy的NN也不过是沙滩上的城堡。
正确对待细节
关注细节
通过面试,我发现很多人好读书,但不求甚解。知道很多算法,但一问细节都不掌握,孰不知,算法成败的关键都在细节里。随便举几个例子:
- Wide&Deep为什么用了两个优化器分别优化Wide侧和Deep侧?
- DSSM中的负样本为什么是随机采样得到的,而不用“曝光未点击”当负样本?
- 在DNN中加入position bias为什么不能和其他特征一样喂入DNN底层,而必须通过一个浅层网络接入?
细节搞清楚了,才算你真正搞明白了一门算法,未来你才能放心使用它。
也不要拘泥于细节
要从本质上理解算法,而不要形而上学。比如,Youtube召回模型与双塔召回模型,前者使用sampled softmax loss,后者使用hinge loss或bpr loss,到底有没有必要在你的推荐系统中都实现一遍?我个人的观点,尽管二者在细节上有很多不同,但是它们本质上都属于Pairwise LTR,优化目标都是让user embedding与正例item embedding足够近,而与负例item embedding距离足够远。因此,没必要在一个系统中分别实现,而应该合二为一,否则召回结果高度重合,对大盘指标发挥不了多少作用。
练习不依赖框架实现一遍算法
Keras、PyTorch之类的框架,将实现深度学习算法,变成了基本模块的拼装,大大降低了实现难度,但也此同时也使很多同学固步自封,没有兴趣再去了解算法细节。比如,如果你调用TensorFlow自带DNNLinearCombinedClassifier来实现Wide&Deep,你能想到如何实现用两个优化器分布优化Wide侧与Deep侧吗?
掌握算法细节最彻底的方式,莫过于不借助高级框架,自己动手从头实现一遍。在实现过程中,除了要保证准确无误,还要考虑程序的运行效率。
如果时间不允许,看看别人是如何从头实现的,对提升自己的算法功力,也大有裨益。对Wide&Deep实现细节感兴趣的同学,可以看我的另一遍文章《用NumPy手工打造 Wide & Deep》。对FM感兴趣的同学,强烈建议通读一遍alphaFM的源代码。
规范化:写得一手好代码算法落地的痛点
链条太长
落地一个算法,需要经过“文献调研→算法选型→收集样本→特征工程→模型编码→离线测试→线上AB实验”这漫长的链条,线上AB测试起码要一周以上,得出的结论才solid。全新算法的落地,花费的时间以月计,牵扯算法、工程多方人员的配合。不到最后一刻,我们都不知道算法的成败,但是无论哪个环节出错,都可能导致算法落地功亏一篑。
不容易debug
如前所述,算法落地的中间环节太多,每个环节都可能出问题
- 一开始选择的算法,可能压根就不合适你们的场景
- 有脏数据。但是线上数据存在个别脏数据,再正常不过了。况且,在几T的数据中查找几千条脏数据,无异于大海捞针
- 算法实现有bug
- 超参不合适。当算法没效果时,很多算法同学不愿相信数据或模型有问题,觉得只要调调超参就能fix,但是大多数时候,这种想法不过一相情愿罢了。
而最困难的地方在于,你有可能压根就察觉不出问题。线上AB测试有了正向效果,大家就觉得功德圆满了,可以开心写工作总结了。但是孰不知,代码依然有bug,修正后,线上指标会有更明显的提升。
解决痛点要靠“防患于未然”
既然算法项目不好debug,就尽量不要出bug。就好比,二战中德国空军涌现出各参战国中最多的王牌飞行员,好几位的击落战绩过百,但是二战德国空军谈不上是最优秀的空军。我个人最欣赏的是以色列空军,“犹太长臂”追求的是,战端一开,就把敌人的飞机都击毁在地面,使其压根没有升空的机会。
如何才能做到少出、不出bug?一要靠良好的编程习惯,二要靠小心谨慎的态度。
良好的编程习惯
良好的编程习惯和你刷过多少leetcode题没关系。而且就个人感受而言,前者要比后者更重要。因为,在日常算法编程工作中,我们很少用得上leetcode的解题思路,但是良好的编程习惯却贯穿码农生涯的始终。
良好的编程习惯,说来也不复杂,无外乎就那么几条
- 花点心思在命名上,给类、函数、变量起个“见名知义”的好名字。
- 模块化。不要一个功能,从头写到尾,写成一个几百行的大函数。而要将其拆分成其他函数或类。
- 注意代码复用,最忌讳拷贝代码。
- 开闭原则,代码模块应该对扩展开放,而对修改封闭。
- 单一职责原则,每个代码模块只负责单一功能,不要将杂七杂八的功能都写到一个类或函数中。
看似简单,但是这些习惯能够使你的代码清晰、易读。而易读的代码容易发现问题。但是,这几条规则,知易行难,做不到的人,归根到底还是一个“懒”字。
另外,我强烈建议,限制使用jupyter notebook。可能是受许多公开课的影响,很多人喜欢在notebook中写程序、调模型,毕竟jupyter notebook能够边写代码,边运行查看结果,再加上图表可视化,功能上的确很有吸引力。
但是,在notebook写代码,完全没有条理,想到哪里,写到哪里,你不会想到要划分函数或类。另外,在notebook中非常容易就使用了全局变量,使你的代码变成紧密耦合的意大利肉酱面。notebook的底层存储格式,将算法逻辑代码与页面渲染代码,杂揉一处,使版本控制失去了作用。因此,我对jupyter notebook的态度就是草稿纸,做一些前期的数据探索性分析还可以,实战代码免谈。
小心谨慎的态度
我写代码,一来写的时候,就小心遵循着以上那些良好的编程习惯,保持代码清晰、易读。小心到什么程度?比如,我将向量命名为user_embedding/doc_embedding,而不是embedding_user/embedding_doc。这是因为现在的IDE都有代码自动提示功能,如果采用将共同部分作为名字的前缀,我担心要用到embedding_user的地方,当我输入embedding前缀时,IDE就自动提示embedding_doc,而我不小心一回车,就张冠李戴,犯下错误。
第二,写完代码后,不着急吼吼地就接入数据,开始训练,我起码要将代码检查上两遍。检查时,要设想各种corner case,比如各种可能的脏数据。而以前Quora上有一个问题就是“算法工程师在等待模型的训练结果时做什么?”,而我会利用那段时间,再次检查我的代码。
总而言之,就是我们在开发算法代码时,要小心、小心、再小心。因为每个环节犯下的bug,都有可能导致算法落地失败,使你和团队投入的时间精力付之东流。更可怕的是,由于大数据+黑盒模型的双重影响,你犯下的bug会非常非常难于被发现。
数字化:构建模型的指标看板现如今,深度学习大行其道,在带来模型性能提升的同时,也使模型愈发“黑盒”化,使之不容易调优,出现问题,也不好定位原因。
面对模型黑盒,打开它是一种思路。学术界提出很多特征解释算法,但是受限于这些算法的复杂度,在工业界落地的并不多。另一种思路就是,没必要打开黑盒,只要对模型的输入输出长期监控,通过历史数据的积累,也能对模型的行为建立合理的预期。一旦模型出现异常,我们也能够很快发现并定位原因。本小节就是介绍如何实现这第2种思路。
接下来所说的指标分两种,输入模型的的上游数据的指标,和模型的离线评测指标,不包括线上业务指标。这是因为,第一,线上指标受其他非模型因素(e.g., 节假日或运营)的影响比较大;第二,线上业务指标会有专门的人员与工具去分析;第三,线上指标已经是target了,它出了问题只能告诉我们模型可能出了问题,但是对于定位问题还远远不够。
重视离线评测指标
我认识一个有几年工作经验的老菜鸟 (这才是最可悲的),团队分配他去做一个召回模型,很快跑出模型,离线AUC还可以(这就是理论水平不足埋下的隐患,用AUC是照搬排序时的经验,而召回模型因为没有真负样本,很少用AUC评价),加上team leader是个工程出身,对算法一知半解,就批准他上线。接下来几个星期,他就只知道催工程同学帮他上线模型。终于上线了,第一天的线上指标就不好,他选择继续观察,同时尝试不同的超参来撞大运。观察了一个多月,线上指标不升反降,他又去怀疑是工程实现有问题,又去怀疑是ranker打压了新召回。最后实在搞不定,找我帮忙。我拿我自制的召回分析脚本一跑,召回的东西与用户兴趣与历史,驴唇不对马嘴,模型本身就有严重问题,换我是他的team leader,压根就不会批准他上线,浪费大家的时间与精力。
这个案例留给我们的教训就是,线上AB实验的代价太高了
- 一方面,现在的模型越来越复杂,很多时候,上线需要花费工程同学的时间与精力,来保证线上服务的实时性能。更不用说,再小的流量也会影响用户体验。所以,如果你什么样的模型都只能靠上线来一试效果,最终耗费的团队对你的耐心与信任。
- 另一方面,线上AB实验的耗时太长,为了得到solid的实验结果,最起码要进行一周,而且线上指标受非模型因素(e.g.,运营)的影响也比较大。所以,线上实验对模型效果的反馈慢又有噪声,一点也不“敏捷”。
所以,我们一定要重视模型的离线指标,尽可能在上线前发现模型可能存在的问题
- 看论文的时候,指标的具体数值可看可不看,反正都是王婆卖瓜,自卖自夸。但是,一定要看离线实验的设计,看作者是怎么无偏地收集测试样本,看作者从哪些维度来评测模型(不是只有准确性,还比如:推荐结果的丰富性、对冷启是否友好、......等),看作者用了哪些指标来衡量模型在各个维度上的性能,看作者如何设计图表使模型性能一目了然。
- 另一方面, 指标有时是片面的(比如,召回算法都只召回热门item, 指标未必很差),这时候人工评测模型,虽然土,但是也能发挥效果。比如,我就自制了一个工具来帮助我评测召回模型,采样1000个用户,把每个用户的画像与点击历史展示在左边,把模型召回的item展示在右边。这个工具能让我对召回效果具备最直观的认识,比如:召回结果是不是太偏热门,而缺乏个性化?召回结果是不是只集中于用户的个别兴趣,对用户兴趣的覆盖面太窄?
指标监控看板
运维有指标看板来监控机群性能,算法也需要有自己的指标看板来监控模型的性能。
- 我们要监控上游的输入数据,比如:每小时有多少样本流入模型?样本中的CTR和平均时长是多少?样本中有多少user/doc/feature?feature经过hash后的冲突率是多少?……
- 离线评测不是只在上线之前做一次就完事了,而是也需要周期性运行,与同时段的上游输入数据的指标相对比,才能让我们对模型建立合理预期,将来定位问题时用得上。常见的监控指标包括,auc/precision/recall等,另外还包括“今天的embedding相较于昨天embedding的变化幅度”这样的对比指标。
举个例子来说明,指标监控在定位模型问题时发挥的重大作用。我们算法工程师的噩梦之一就是,之前好好的模型,线上指标(突然或缓慢)掉下来了。有一天,我就遇到了这样的名场面。借助指标看板,
- 首先,我发现模型的离线指标也掉下来了,说明与线上工程实现或外界环境无关,应该是模型本身出问题了
- 接着,我发现输入模型的样本中的ctr下降了,说明上游数据出问题了
- 询问了负责上游数据的同学,发现他们缩短了等待用户反馈的时间,导致一部分正样本由于未能等到用户反馈被当成负样本,从而定位了问题
看我的轻描淡写,是不是感觉我解决这个问题超easy。实际情况恰恰相反,当时我还没有构建出针对模型的指标看板,模型的离线指标和输入样本的CTR都是我遇到问题后再手工回溯的,花费了一两天才定位问题。吃一堑,长一智,经此一役,我才充分认识到模型指标看板的重要性,现如今它成为了我团队每个算法工程师的标配。
自动化:优秀的工程师造工具算法工作毕竟是实验科学,所以成功的关键是高效做实验(由于线上容量有限,所以下文所说的实验,主要指离线实验)
- 谈到高效,我们的第一反应是快。单位时间能做的实验越多,尝试的方向(不同特征+不同模型+不同超参)越多,越可能找到最优解。为了能够快速实验,首先,实验的代价必须小,为了新实验要修改代码是不可接受的,最好是修改一下配置,然后一键启动;第二,能够并行跑多组实验,充分发挥算力,这就要求各实验之间完全解耦,比如不会向同一个路径下写入结果。
- 但是,快只是高效的一个方面,能做的实验多了,实验管理就成了问题。
- 每个实验,我们都要记录:特征的版本、模型的版本、超参的版本、用哪段时间的数据训练的、在哪段时间上测试的、各种离线指标、......
- 跑几十、上百个离线实验非常常见,这么多实验数据,如果缺乏管理,也就无从分析比较,无法得到正确结论,实验等于白做
- 手动管理,比如把实验数据记录在excel里,也不是不可以,估计也是大多数算法同行的选择。但是,一来人懒,特别是模型结果不好的时候,更没心情做记录了;二来,实验数据一般在服务器上,而excel在本地笔记本上,记录时不得不ctrl-c/ctrl-v,出错在所难免
- 因此,我们需要一个类似Source Version Control的工具,自动将实验数据管理起来。
实验管理工具
这个实验管理工具,应该具备什么样的功能?
- 我们下班前配置好要进行的实验,然后一键启动实验
- 实验管理系统,自动向后台机群提交多组实验,多组实验并行运行
- 每组实验结束后,自动记录实验版本和结果
- 明早到了办公室,打开电脑,昨天实验的各种报表、曲线已经ready了
我在Youtube上看过一个Yelp工程师的介绍,听说Yelp的实验管理系统不仅具备以上功能,而且每个实验结束后,还能向实验发起人发一封邮件,邮件里有完整的实验报告,包含图表与曲线。实验发起人用新参数回一封邮件,后台服务器收到邮件后,就会用新超参数开启下一轮实验。
网上有一些开源的实验管理工具,但是
- 一来,浸入式太强,需要我们修改代码,削足适履地适应那套框架;
- 二来,运维也不可能允许你在公共机群内安装一大堆乱七八糟的第三方框架
因此,我自己动手写一个简单的实验管理工具
- 实验是高度配置的,调节使用特征、调节网络结构、调节超参数、......,都通过配置完成,而无需修改代码
- 实验一键启动,各环节自动跨平台衔接(数据准备在Spark集群完成,模型在分布式训练集群完成训练与评估),并具备一定容错能力
- 多组实验可以并行进行,而一个实验内部“数据准备”与“训练模型”异步进行,以减少等待
虽然前期投入时间多一些,但是有了实验工具的帮助, 我团队的实验效率大大提高。目前,我们正在朝着Yelp那套工具的方向迈进,已经隐约看到了“边吃着火锅,唱着歌,边调模型”的美好未来。
利用工具+制造工具
实验管理工具只是一个例子。事实上,对待工具的态度,是小白与高手的区别之一:
- 小白的每个模型都是匠人手工版:为了新实验,要手动改一堆代码,要手动ctrl-c/ctrl-v记录结果,为了等实验结果不敢下班,......
- 高手善于利用工具,制造工具。为了每天节省5分钟,他宁可花两天时间造个工具。每天到点下班,吃着火锅,唱着歌,等着实验结果曲线发到手机上。
本文讲述的,并非某个具体算法的那样的“技”,而是聚集于“算法工程”之道,从“理论化+规范化+数字化+自动化”四个方面提出了方法论,根据我的实战经验,讨论了如何将算法从纸面落地于一个互联网级别的实际系统中,从而为你老板和你创造价值。
本文落笔于2021年的1月1日,是我本人在2021年的第一篇文章。请无处不在的互联网记录下我的新年心愿:希望我和我爱的人在2021年平安顺遂!
- END -