[深度学习]利用DNN做推荐的实现过程总结

[深度学习]利用DNN做推荐的实现过程总结

最近在利用来自google的YouTube团队发表在16年9月的RecSys会议的论文Deep Neural Networks for YouTube Recommendations做用户个性化商品推荐,看到不少论文上的理论总结分析,都很精彩,我手动实现了一遍,总结了一些实际工程中的体会,给大家也给自己一个总结交代。

一、系统概览

上面这张图可以说是比较干练的涵盖了基础框架,整体的模型的优点我就不详述了,包括规模容纳的程度大啊、鲁棒性好啊、实时性优秀啊、延展性好啊等等,网上很多优秀的文章,就比如清凇:用深度学习(DNN)构建推荐系统 - Deep Neural Networks for YouTube Recommendations论文精读,我主要总结了几个我在实际去做的过程中,个人觉得比较有意思的点,方便以后大家优化自己的算法模型。


- dnn网络及点击数据构造

- 负采样的“避坑”

- example age

- user feature的选择方向

- attention机制

- "video vectors"

- 实时化的选择

- 转化为二分类模式的可行性


另外,G厂的这套算法基于的是两个部分:matching+ranking,这个的也给我们带来了更大的工作量,在做的时候,建议分成两个部分进行评估,我们在实际处理的时候,通过recall rate来判断matching部分的好坏,通过[NDCG](cnblogs.com/eyeszjwang/)来判断排序部分的好坏。candidate generation就是我们matching的模块,目的是把百万级的商品、视频筛选出百级、千级的可排序的量级;再通过ranking模块,选出十位数的展示商品、视频作为最后的推送内容。之所以把推荐系统划分成Matching和Ranking两个阶段,主要是从性能方面考虑的。Matching阶段面临的是百万级,而Ranking阶段的算法则非常消耗资源,不可能对所有目标都算一遍,而且就算算了,其中大部分在Ranking阶段排名也很低,也是浪费计算资源。


二、实现细节

  • dnn网络及点击数据构造

如我图中两处红色标记,论文中虽然给出了模型的整体流程,但是却没有指明,1处的video vectors需要单独的embed video vector,还是延用最下方的embedded video watches里面的已经embed好的结果,我称之为softmax问题(因为靠着softmax=.=);2处论文没有提及一个问题,就是在固定好历史watch的长度,比如过去20次浏览的video,可能存在部分用户所有的历史浏览video数量都不足20次,在average的时候,是应该除以固定长度(比如上述例子中的20)还是选择除以用户真实的浏览video数量,我们称之为revise问题(大厂确实不用担心数据长度的问题)。

根据我们的数据实测,效果对比如下:

nosoftmax:沿用最下方的embedded video watches里面的已经embedding好的vedio vector
softmax:重新embed vedio vector 进行用户商品相似度计算
norevise:不足固定历史浏览长度填充0-vector后计算
revise:不进行填充,除以用户真实的浏览video数量

nosoftmax比softmax好的原因猜测是因为user vector是由最下方的embedded video watches里面的已经embedded好的结果进行多次全连接传递得来的,相比较单独构建embedded video vector,相关性会更强一些;revise比norevise好的原因是,实际在yoho!buy的购物场景下,用户的点击历史比较我们实际选取的历史浏览长度要短不少,如果所有的用户都采取填充后除以固定长度的话,大量的用户history click average的vector大小接近于填充向量。

网络结构的变化比较常规,对比不同激活函数、不同的层数,参考了论文中推荐的深度、节点数,效果对比如下:

sigmoid会导致神经元批量死亡,这个很常识,还有就是我们虽然看到增加网络的深度(3-->4)一定程度上会提高模型的命中率,增加leakyrelu的一层网络也可以有些许的提升,但是总的来说,对模型没有啥大的影响,这个与原论文中说的一致,所以在之后的实际模型中,我们选择了原论文中relu+relu+relu,1024+512+256的框架,建议大家在做的时候采取3层就够了,再多加的话性价比就不高了。


  • 负采样的“避坑”

我们都知道,算法写起来小半天就可以搞定,但是前期的数据处理要搞个小半个月都不一定能出来。作为爱省事的我,为了快速实现算法,没有重视负采样的部分,刚开始的时候直接采取了基于所有列表页展示的结果,点击的skn为label=1,未点击的skn为label=0的方式,详情如下:

看上去没什么问题,省略了从全量样本中抽样作为负样本的复杂过程,实际上,我把其他trick各种尝试效果也一直维持在1%-2%之间,可以说是没有任何提升,在此过程期间,我还是了拿用户的尾次点击(last_record)进行训练,拿了有较多行为的用户的尾次点击(change_last_record)进行训练,效果一直不好。最后,选择决定按照原论文中说的,每次label=0的我不拿展现给用户但是用户没有点击的商品,而是随机从全量商品中抽取用户没有点击过的商品作为label=0的商品后,click rate一下子达到了2.8pp,接近线上的RNN推荐模型。

事后我分析了原因:

  1. 在当次数据构造情况下,虽然用户只点击了click商品,其他商品没有点击,但是很多用户在后续浏览的时候未click的商品也在后续列表页的地方进行click,我实际上将用户感兴趣的商品误标记为了负样本
  2. 后来我仔细看了论文,也发现了原论文中也提及到,展现商品极有可能为热门商品,虽然该商品该用户未点击,但是我们不能降低热门商品的权重(通过label=0的方式),实际上最后的数据也证明了这一点

在采取论文中说的负采样的时候,需要注意负样本“偷窥未来”的情况,原论文中指出input构造时候不能拿还未发生的点击,只能拿label=1产生时之前的所有历史点击作为input;同理,在构造label=0的时候,只能拿在label=0的时候已经上架的商品,由于训练时间的拉长,不能偷窥label=1发生时还未上架的商品作为label=0的负样本。

  • example age有没有必要构造

首先,先稍微解释一下我对example age的概念的理解。所有的训练数据其实都是历史数据,距离当前的时刻都过去了一段时间,站在当前来看,距离当前原因的数据,对当前的影响应该是越小的。就比如1年前我买了白色的铅笔,对我现在需要不需要再买一支黑色的钢笔的影响是微乎其微的。而example age其实就是给了每一条数据一个权重,引用一下原论文的描述`In (5b), the example age is expressed as tmax − tN where tmax is the maximum observed time in the training data`,我这边采取了(tmax − tN)/tmax的赋权方式,很悲催的是,直观的离线训练数据并没有给出很直观的效果提升,因为离线训练的推荐结果的验证都是基于线上展示的结果,所以有一定的偏颇,我会在实际上线做abtest的时候重新验证我的这个example age的点,但是可以肯定的是,理论和逻辑上,给样本数据进行权重的更改,是一个可以深挖的点,对线上的鲁棒性的增强是正向的。

  • user feature的选择方向

原论文中的一大亮点就是加入了user feature,很不幸的是,在这一块的提升,我做的确实没有论文中说的那么好,对于整个网络的贡献,以我做的实际项目的结果来说,history click embedded item > history click embedded brand > history click embedded sort > user info > example age > others。不过,因为时间、数据质量、数据的真实性、个人能力的原因,可能作为原始input的数据构造的就没有那么好。这边主要和大家说两个点:


  1. topic数据

原论文中在第四节的RANKING中指出:

We observe that the most important signals are those that describe a user’s previous interaction with the item itself and other similar items, matching others’ experience in ranking ads

论文中还举出了比如用户前一天的每个频道(topic)的浏览视频个数,最后一次浏览距今时间,其实说白了就是强调了过去的行为汇总对未来的预测的作用,认为过去的行为贯穿了整体的用户点击轨迹。除此之外,G厂大佬还认为一些用户排序性质的描述特征对后面的ranking部分的提高也是蛮重要的,这边还举出了用户视频评分的例子,更多的内容大家可以自己去看一下原论文的部分,应该都会有自己的体会。

回到我们的项目,因为yoho!buy是电商,我类比着做了用户每个类目(裤子、衣服、鞋子...)的历史浏览点击购买次数、最后一次点击距今时长等等的topic信息,提升不是很明显。但是在大家做G厂这边论文,准确率陷入困境的时候,可以尝试一下这边的思路。

2. query infomation
相比于论文中的user information的添加,在实际模型测试中,我们发现,query的information的部分有更多的”遐想”。原论文中点名指出user language and video language 做为basic info的重要性,这边给出的提升也是相对于user info,rate有明显的增长的:

有提升也自然有该部分的缺点:

  1. 语言模型的处理复杂,耗时久

在该部分的处理中,文本数据清理更加复杂,句法分析、语句树解析专业性强,耗时久

2. 语言新增问题

商品的标题这类的文本处理还好,毕竟每日更新的数据存在一个可控的范围,但是用户搜索内容的变化是巨大的,粗略估测一下,一周时间间隔后,原提纯文本数据和新提纯文本数据的交集覆盖率不到80%

  • attention 机制的引入

attention 机制的引入是我老大的硬性需求,我这边也就做了下,如果不了解attention 机制的朋友,可以阅读以下这边文章:Attention Model。我通俗的解释一下自己的观点,不准确但是方便理解,简单的attention model就是让你每一个input与你的output计算一个similarity,再通过这些similarities给出每个input的权重。但是,很明显,我们离线训练还好,既有input也有output,但是线上预测的时候,就没有output了,所以,我们采取了倒数第一次点击替代的方式。

山大有另外一篇关于attention机制的模型Neural A entive Session-based Recommendation

我暂时还没有时间看,据我同事测试效果来说,比上面这种效果要好,能够提升约1个pp的准确率。但是会有一个让人头疼的问题耗时,就以简单的与倒数第一次计算相似度的为例,因为每一个input的weight需要和output进行一次相似度计算,而且后续还要对计算出的相似度进行处理,原本只需要6-7小时训练完的模型,在我加了3层Multihead Attention后被拖到了一天。数据量还只采样了一半,确实需要斟酌带来的提升与投入的成本之间的平衡问题。


  • "video vectors"

G厂一句话,我们测断腿。这句话不是瞎说的,大家应该还记得一开始我给出的那张图,在最上面有一行不是很明显的小字:video vectors Vj 。G厂的大佬们既没有说这些video vectors该怎么构造,也没有说video vectors需不需要变动,留下了一个"乐趣点"让大家体验。

刚开始我很傻的直接用了我们最开始的embedded video watches中的video部分的向量作为video vectors,与模型FC出来的user vectors进行点击,计算top。我来来回回测了近一个月,最后提升rate到4pp。然而RNN随便跑跑就能到达3pp,我是很不服气的,所以拉着同事一起脑洞了一下,我们之前做图片相似度匹配的时候,喜欢把图片的向量拆成颜色+款式+性别,所以我们就借用了一下,改成了embedded item + embedded brand + embedded sort作为video vectors,历史总是给我们惊喜,效果上一下子就能大到5.2pp左右,这个点的提升应该是得来的最意外的,建议大家在用的时候考虑一下。


  • 实时化的选择

实时部署上,我们用了tensorflow serving,没什么好说的,比较常用的方法,不做冗余的解释。这边只说一个我不知道是bug还是我个人的问题,就是在我用python进行线上调用的时候,会一直报错:

tensorflow.python.framework.errors_impl.NotFoundError

我最后按照下面的第四步的方式进行了解决。

部署及用python作为Client进行调用的测试:
#1.编译服务
bazel build //tensorflow_serving/model_servers:tensorflow_model_server
#2.启动服务
bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --port=9005 --model_name=test --model_base_path=/Data/sladesha/tmp/test/ 
#3.编译文件 
bazel build //tensorflow_serving/test:test_client
#4.注销报错的包
注销:/Data/muc/serving/bazel-bin/tensorflow_serving/test/test_client.runfiles/org_tensorflow/tensorflow/contrib/image/__init__.pyc中的from tensorflow.contrib.image.python.ops.single_image_random_dot_stereograms import single_image_random_dot_stereograms
参考:https://github.com/tensorflow/serving/issues/421
#5.运行
bazel-bin/tensorflow_serving/test/test_client --server=localhost:9005


相关的问题,有大佬已经梳理好了,自取其他可选的一些参数设置:tensorflow serving 参数设置

  • 转化为二分类模式的可行性

其实,我们都知道多分类本质上都可以转化为2分类来做,如果用二分类来做,就有很多想象的空间了。逻辑上其实没很大的差异,就是损失函数西格玛中的那个交互熵的不一致。

我这边只是做了简单测试,没有深度的去研究,效果暂时不如多分类:

我这边用的是wide&deep的二分类方法去做的,可以选择的方法和优化的思想还是很多的,大家也可以在这边可以试试。


三、总结

其实,我一直觉得理解一篇论文不是一件难事,最近实现了蛮多带有practical lessons的paper,我深刻体验到了论文告诉我的完全不代表论文的全部。文章中引用的地方都在对应的位置给了链接,不追加了。

最后如果你喜欢的话,那就...

算了,诸位,下回见。

编辑于 2018-07-12

文章被以下专栏收录