前言
记得我入算法这一行的第一份工作面试的时候,最终的boss面的面试官是前微软工程院的副院长。面试进行得很顺利,不免向前院长卖弄一番,谈了谈我对算法的理解。我说算法工程师就好比厨师,模型是灶上功夫,而数据预处理 特征工程就好比刀工。再好的食材,不切不洗,一古脑地扔下锅,熟不熟都会成问题,更甭提味道了。好的刀工能够将食材加工成合适的形状,无需烈火烹油,也能做出好味道。同理,特征工程做得好,简单模型也能做出不错的效果,当然有了nb的模型,“或许”能够锦上添花。(这个故事的后半段是,前副院长听了我的比喻,问了我一个问题:有没有比刀工更重要的呢?我不记得当时是怎么回答的了,应该是答得不好。副院长说,好的厨师要擅于挑选食材,所以好的算法工程师要会挑选数据。嗯,这个道理等到我写《负样本为王》的时候,理解得就更通透了。负样本挑错了,再好的特征工程也无法挽救你的召回模型。)
讲这个故事是为了引出今天的主题,即推荐系统中的特征工程。这个话题比较大,是单单一篇文章无法涵盖的,所以也要先做一下限定。
首先,本文不介绍特征工程的基础套路。所谓基础套路,就是大家耳熟能详的那些常规原始特征。
- 用户侧无非就是人口属性(性别、年龄、职业、位置等)、用户安装的app列表、用户的各种观看 互动历史那一堆;
- 物料侧无非就是基本属性(作者、长度、语言),还有基于内容理解(thanks to nlp and cv)技术打上的一堆一/二级分类、标签、关键词等
之所以不讲这些,一来获得这些信息,更多依赖产品、前端、内容理解等团队,算法工程师的作用有限,等着用就是了;二来,根据我的个人经验,这些未经加工的原始信息发挥的作用有限。即使是对于新用户,一些产品同学往往以为知道了用户的性别、年龄,甚至安装的app,就能猜出用户兴趣,从而提升冷启动中的个性化成分,结果往往令人失望。
其次,我还是要再次批判“深度学习使特征工程过时”的论调。比如,用了阿里的DIxN家族(DIN/DIEN/DSIN)能够捕捉用户的短期兴趣,用了SIM能够捕捉用户的长期兴趣,那么未来是不是没必要再对用户历史做特征工程了,把一堆用户观看 互动过的item id扔进DIxN SIM不就行了,省时省力,效果也好?我的答案是否定的,原因有二:
- 2021年留给我最深印象的就是DCN作者的那一句话“People generally consider DNNs as universal function approximators, that could potentially learn all kinds of feature interactions. However, recent studies found that DNNs are inefficient to even approximately model 2nd or 3rd-order feature crosses.”。DNN的“火力”没那么强,原材料一古脑扔进去,有时熟不熟都会成问题,更何况各种滋味的融会贯通。
- 各种强大的网络结构好是好,但并不是无代价的。SIM中用target item去检索用户的长期历史再做attention,都是线上inference的耗时大头。做做候选item只有几百的精排倒还可以,但是让候选item有成千上万的粗排和召回,情何以堪?难道不用SIM就不能刻画用户的长期兴趣了吗?当然不是,接下来要介绍的几个特征工程上的技巧就能够派上用场。根据Lambda架构中“线上层--近线层--离线层”的划分,将计算压力从线上转移到线下,离线挖掘用户长期兴趣,使召回 粗排环节也能够用得上。
接下来,就介绍推荐系统特征工程中的几个高级技巧,“奇技淫巧”,将原始特征加工成“让模型更好吸收”的形状。
用户特征
无疑,描述用户兴趣最有用的信息就是各种用户历史。如上所述,除了将用户观看 交互过的item id一古脑地扔进DIN/DIEN/SIM,让模型自己学出用户各种长短期历史,我们还可以从这些用户历史中手工挖掘出一些有用的信号。
我的一个同事提出从以下6个维度挖掘用户的长短期兴趣
- 某一群用户:比如相同年龄段、同性别、同地域、安装了同款app的人群。基于人群的统计,对于新用户冷启意义重大。
- 最近、过去x小时、过去1天、过去1周、过去1月、从用户首次使用app至今、...
- 太长的时间粒度(e.g.,首次使用至今)在统计的时候,会考虑时间衰减
- 长期历史的统计,会通过离线批量任务(hadoop/spark)的形式完成
- 也可以是item上的属性,比如一二级分类、标签、关键词、...
- 正向:点击、有效观看、完整观看、点赞、转发、评论、...
通过以上6个维度的交叉,我们可以构造出一系列特征来描述用户的长期(e.g., 首次使用app至今)、短期(e.g. 过去1天)、超短期(e.g., 刚刚观看的x个视频)的兴趣。比如:
- 用户A,在过去1天,对tag='坦克'的CTR(i.e., 系统在过去1天,给该用户推了10篇带tag='坦克'的文章,该用户点击了6篇,ctr=0.6)
- 用户A,在过去1天,对tag=“坦克”的点击占比(i.e., 在过去1天,用户一共点击了10文章,其中6篇带tag='坦克',点击占比=0.6)
- 用户A,在过去1天,对tag=“坦克”的时长占比(i.e., 在过去1天,用户一共播放了100分钟,其中60分钟消费在带tag='坦克'的物料上,时长占比=0.6)
- 用户A,在过去1天,忽略(隐式负反馈)category=“时尚”的item个数
- 用户A,自首次使用app至今,对tag='坦克'的CTR(统计时,曝光数与点击数都要经过时间衰减)
- 男性用户,在过去1月,对tag='坦克'的文章的CTR
注意:
- 以上6个维度只是为我们手工挖掘用户兴趣提供了一个框架,使我们添加特征时更有章法。至于具体要离线挖掘哪些特征,也要根据算力和收益,综合考虑;
- 这些手工挖掘的用户兴趣信号,可以作为DIN/DIEN/SIM挖掘出来的用户兴趣的补充。而在召回/粗排这种计算压力大的环节,由于可以离线挖掘而节省线上耗时,以上这些手工挖掘出的用户长短期兴趣可以(局部)代替DIxN/SIM这些“强但重”的复杂模型。
物料特征
对于item侧,我认为最重要的特征就是这些item的后验统计数据,
- 时间粒度:全部历史(带衰减)、过去1天,过去6小时,过去1小时、......
- 统计对象:CTR、平均播放进度、平均消费时长、......
比如:某文章在过去6小时的CTR,某文章在过去1天的平均播放时长、......
但是也要谨记,
- 这些统计数据肯定是有偏的,一个item的后验指标好,只能说明推荐系统把它推荐给了对的人,并不意味着把它推给任何人都能取得这么好的效果。
- 其实这个问题其实也不大,毕竟交给精排模型打分的都已经通过了召回 粗排的筛选,多多少少是和当前用户相关的,之前的统计数据还是有参考意义。
- 利用这些后验统计数据做特征,多少有些纵容马太效应,之前后验数据好的item可能会被排得更靠前,不利于新item的冷启。
- 那么新item没有后验证数据怎么办?填写成0岂不是太受歧视了?其实有一个办法就是建立一个模型,根据物料的静态信息(e.g., 作者、时长、内容理解打得各种标签等基本稳定不变的信息)来预测它们的后验数据。
另外再介绍一个由用户给物料反向打标签的trick。
- 一般画像的流程,都是先有物料标签,再将用户消费过的物料的标签积累在用户身上,形成用户画像。
- 反向打标签是指,将消费过这个物料的用户身上的标签积累到这个物料身上。比如:一篇关于某足球明星八卦绯闻的文章,由于该明星的名字出现频繁,NLP可能会将其归为“体育新闻”,但是后验数据显示,带“体育”标签的用户不太喜欢这篇文章,反而带“娱乐”标签的用户更喜欢,显然这篇文章也应该被打上“娱乐”的标签。
- 类似的,给物料打上“小资文青喜欢的top10电影之一”,或者“在京日本人光顾最多的日料店”等,都是由用户消费反向给物料打上的极其重要的标签。
交叉特征
说到交叉特征,受“DNN万能论”的影响,近年来已经不再是研究 关注的热点。既然DNN'能够'让喂入的特征“充分”交叉,那何必再费神费力地去做交叉特征。
我的答案还是那两条,一来,不要再迷信DNN'万能函数模拟器'的神话;二来,手动交叉的特征犹如加工好的食材,这么强烈的信号递到模型的嘴边上,模型没必要费力咀嚼,更容易被模型消化吸引。
比较简单的一种交叉特征就是计算用户与物料在某个维度上的相似度。以“标签”维度举例:
- 某用户过去7天在“标签”维度上的画像,可以写成一个稀疏向量,
u={'坦克’:0.8, '足球’:0.4, '二战':0.6, '台球':-0.3}
。每个标签后面的分数是由第2节的方法统计出的用户在某个标签上的兴趣强度(可以根据xtr/时长等指标计算出); - 某篇文章的标签,可以写成一个稀疏向量,
d={'坦克':1, '二战':0.5, '一战':0.8}
。每个标签后面的分数是NLP在打这个标签时的置信度; - 拿这两个稀疏向量做点积,
u x d={'坦克’:0.8, '足球’:0.4, '二战':0.6, '台球':-0.3} x {'坦克':1, '二战':0.5, '一战':0.8}=0.8*1 0.5*0.6=1.3
,就可以表示这个用户对这篇文章在“标签”维度上的匹配程度。
另一个复杂一点的交叉特征是任意一对儿<user特征,item特征>上的消费指标,比如<男性用户,item标签=“足球”>
这一特征对上的xtr。要得到这样的特征,离线统计当然是可以的,数数<男性用户,item标签=“足球”>
共同出现的样本有多少,其中带正标签的样本又有多少,两者一除就能得到。但是这么做,有两个缺点:
- 数量太多了,我们要统计、存储的特征对儿的量是
'所有用户特征 * 所有物料特征'
,要耗费大量的资源,线上检索起来也是个麻烦 - 扩展性太差。只有对共现超过一定次数的特征对上的xtr才置信,才有保留价值。对于共现次数较少,甚至没有共现过的特征对上的xtr,我们也拿不到。
为了克服以上缺点,阿里妈妈在SIGIR 21《Explicit Semantic Cross Feature Learning via Pre-trained Graph Neural Networks for CTR Prediction》一文中提出了用预训练来解决以上难题的思路
- 图的顶点就是样本中出现过的categorical feature,样本中常见共现特征之间建立边,边上的值就是这一对儿特征离线统计出的xtr
- 按照link prediction的方式来训练GNN
- 线下训练和线上预测时,将每个用户特征与每个物料特征两两组合,每对儿特征喂入训练好的GNN模型,返回预估xtr作为特征。
- 这种作法,即节省了存储 检索海量特征组合的开销,又因为GNN本身也是基于embedding的,对于罕见或少见特征对的扩展性也比较好。
此篇文章的重点是“用预估代替统计 存储”的思路,是否必须用GNN模型,我看倒也未必。如果担心GNN线上预估耗时问题,我看用FM模型也不错:
- 模型训练好后,将每个categorical feature的embedding存储起来
- 线下训练或线上预估时,将所有用户特征与所有物料特征,两两组合。
- 针对每一对儿特征,获取各自的feature embedding,点积再sigmoid,就得到这一对儿特征上的预估xtr,供训练或预测
后记
多多少少受“DNN万能函数模拟器”、“DNN能让特征充分交叉”的神话影响,业界对特征工程的研究,远不如模型结构那么热闹。
本文希望传递这样一种思想,即便在DNN时代,特征工程仍然值得我们投入精力,深入研究。相比于粗犷地将原始特征一古脑地扔进DIN/DIEN/SIM这些“强但重”的模型,在原始特征上施以精细的刀工,挖掘出更加直白的信息喂到模型嘴边上,
- 二来,离线挖掘出强有力的特征,某种程度上能够取代那些“强但重”的模型结构,节省预测耗时,适用于召回 粗排这些计算紧张的场景
其实输入侧的故事,还远没有结束。
- 挖掘出重要特征,如何接入DNN才能让它们发挥使用?是不是和其他特征一样,喂到DNN的最底层,让它们的信息慢慢逐层向上传递?关于这一点,请出门左转,看我的另一篇文章《先入为主:将先验知识注入推荐模型》。
- 如我之前所述,推荐系统中,ID类特征才是一等公民。而文中介绍的重要特征,大多是实数型的。如何将这些重要的实数型特征转化为id类特征再进行embedding,业界最近也有一些新的研究成果,突破了传统的bucketize embedding table的老套路,准备在下一篇文章中总结一下。欲知后事如何,请听下回分解:-)
关于深度传送门