论文地址:https://arxiv.org/abs/1810.09591
这篇论文将的是airbnb搜索在深度学习方面的探索
airbnb最开始在搜索排序中使用的是gbdt,但是随着模型的稳定,gbdt带来的提升越来越有限。而这篇文章就是要讲airbnb将深度学习技术应用到实际环境中去的实践。论文并有没提出什么新的理论技术,重点放在了整个工程实现以及模型优化,迭代,各种在由传统机器学习向深度学习迁移过程中遇到的坑,对那些想开始往深度学习进行探索的团队来说有很多干货可以借鉴。中间很多在遇到问题后的思考以及深度的分析做的确实很到位,值得学习。
airbnb是房屋分享平台,供房方提供出租的房子,供全世界的潜在客户预定。典型的预定方式是客户在airbnb上搜索指定地理位置的可用房源,然后搜索排序的任务就是从成千上万的库存中向客户提供一个排序的可用房源。搜索排序的最快速实现方法就是使用手工的静态分,计算出客户提供的query和待排序的房源,具体大家可以参考下es的静态分计算方法。当然静态分的计算,只考虑了query和房源titile本身的信息,而且计算方式是固定的。虽然他们利用gbdt代替静态分的计算方式,在搜索排序效果上获得了巨大的提升。但是gbdt的迭代获得的收益越来越低,就开始往深度学习开始探索。
搜索排序模型是整个搜索系统的重要一部分,它直接决定了向客户展示的房源以及顺序。在整个模型生态中,机器学习模型可以用来预测客户接受预定的请求,预测客户对旅行体验的评价等等。而我们这里讨论的是搜索排序中最复杂的一块,它需要根据客户的需要,列出所有用户可能会喜欢的房源。
作者展示了一个搜索行为session,如上。用户通过多次搜索,点击查看详情页。一个成功的session会以客户预定一个房源结束。而这些信息都会被记录下来,在训练的时候,新的模型利用这些日志信息训练模型。而新的模型训练的目的就是去让被预定的房源在整个曝光房源中仅可能的高。然后训练好的模型再放到线上去,通过A/B测试,来检验新的模型比原来的模型是否更加有效。
模型的演变是一次次迭代形成的必然结果,如下图所示分别展示了airbnb在模型演变过程中在离线和线上分别获得的收益。
Andrej Karpathy(特斯拉人工智能和自动驾驶视觉总监)对模型结构设计的建议是:don't be a hero。 但是谁不想做出点不一样的成绩,作者开始也认为:Why can't we be heros? 开始设计复杂的模型结构,只能花费大量的四件去优化模型,最后消耗大量的时间精力。最后作者上线的第一个网络就是一个简单的单隐层的32个节点的NN。网络的输入特征和GBDT的特征一样,模型的训练目标也保持不变:最小化L2回归损失,预定的商品清单为1,没有预定的为0。整个流程证明NN可以应用到线上服务。
结合NN和Lamdarank。因为离线采用的是NDCG指标来作为最直接的模型效果度量。Lambdarank给了我们一个最直接的方式去利用NDCG指标优化NN模型。这里涉及到两项对回归公式的改进:
1.使用成对的数据作为训练样本,如{booked listing, not-booked listing} 作为训练样本,训练过程中,最小化预定房源和非预定房源得分之间的交叉熵损失。
2.通过交换组成训练样本对的两个列表内的数据位置,根据产生的NDCG差异来衡量每对损失。这样可以使得模型更加关注列表顶部的数据。如把预定的列表排名从2排到1 比从10排到9要重要。
文章还列出来整个loss的tensorflow计算代码:
在主要流量为NN的同时,作者也同时研究来其他的模型。值得注意的是:作者发现gbdt,fm和nn三者虽然预测离线指标都差不多,但是预测出的结果差异较大,所以结合来三者的效果,将GBDT的叶子节点做embedding后和FM的预测结果作为网络的特征放到NN里面,具体如下图所示:
提出了作者采用的典型网络配置:在将类别特征做完embedding后总共输入195个特征,输入到一个包含127维的隐层,激活函数为relu,再继续到83维的relu层。喂到DNN里的特征都是房源的基础属性,如价格,设备,历史订单量等,几乎在没有特征工程的情况下直接喂到网络中,还包括了些从其他模型输出的特征:
1.智能定价特征,由专门的模型产生;
2.当前列房源和用户过去看的房源的相似度,利用共现embedding计算。
这些模型挖掘的数据并不直接是搜索排序中的数据,所以给DNN提供了额外的信息。
模型通过17亿的样本对进行训练,如下图所示,经过不断的训练,模型的泛化性能越来越好,训练集和测试集的ndcg越来越接近。
在airbnb中每个房源都有一个对应的id,NN是可以有机会直接使用这些房源id来作为特征。用法很简单,将id直接embedding成向量,然后将该embedding学习出来就可以了。但是在aribnb的实践中房源id的embedding几乎都导致了过拟合。下图是学习曲线
经过分析,作者认为过拟合的原因在于潜在市场的独特属性。因为我们如果需要学习出每个id的embedding,那么我们就需要针对每个id都有大量的数据,这样才能拟合出每个id合理的embedding值。所以像文本中的文字或者在线短视频之类的,因为可以无限重复的出现,那么我们就可以收集大量的交互数据用来拟合向量。但是因为airbnb的贡献出来的item是房源,一个人预定了,其他人就不能预定了,那么如果一个房源很受欢迎,那么它一年也最多被预定365次,而常见的交互是更少的。这样就造成针对该id的交互数据很少,这就导致了id值在训练过程中极为稀疏,从而导致了id特征的过拟合问题。
先分析数据,预定的量是比查看详情页的量要少一个数据集的;但是预定有实际环境的限制,可访问详情页是没有的。而且在详情页的查看时间和上架情况是成正比的。所以针对这一点,作者提出来如下的想法来结果id导致过拟合的问题。
为了解决房源id过拟合的问题,作者打算采用多任务学习的方法来解决。利用模型来同时预测预定和长曝光的概率,同时优化两个损失函数,一个是以预定为正样本的损失,还有一个是长曝光的损失,两者共享同一个隐层。网络结构如下:
通过共享曝光和预定两者的id embedding,这样就可以是得id embedding的学习可以来自于长曝光的数据。因为长曝光的房源数据比预定的量要多出一个数据级。同时在损失函数中,为了保持整个模型对预定房源的关注,对预定的损失应用来更高的补偿权重。每个长曝光标签被进一步压缩为log(曝光时间),而模型上线的只用到了预定的概率值。
在在线测试的时候,该模型大幅增加来长曝光,但是预定情况保持不变。通过手动查看那些长曝光和预定比例过高的房源,作者提出来几个可能的原因。可能的原因在于这些房源都是高端但是价格昂贵的房源,或者是拥有很长的描述,又或者是比较幽默比较特别的房源等。
典型的特征工程包括一些计算比例,滑动窗口平均,还有一些其他的操作等等。但是很难去评价一个特征是不是好的,以及该特征随着时间变化会不会随着市场的变化而变得过时,DNN的一个吸引力就是可以做到自动的特征选择,只需要将特征输入模型,神经网络就可以在隐层做好特征工程以及特征选择。所以NN相对传统的机器学习来说,需要的不是基于更多的特征上的特征工程,而是将重点放在输入NN的数据符合某些基本的属性,以便NN能够自己有效的进行数学计算。
神经网络的输入做归一化是必须的,所以这里作者也对他们的输入做了归一化,将其数值限制在{-1,1}之间,中值在0。主要的特征归一化方法有以下两种:
1.当特征分布类似于正态分布的时候,可以将特征做正常的归一化,如下
其中fea是特征值,是特征的均值,是标准差
2.如果特征分布接近于幂律分布,则做如下转换
其中fea是特征值,median是中位值
除了特征需要归一化,将特征数值控制在一定的范围内之外;我们还需要特征的分布相对来说比较平滑,原因有如下几点:
1.定位异常;当处理成千上万的特征样本的时候,验证一小部分数据是否有异常是比较困难的,范围检查可以起到一定的作用,但是相对来说比较有限。作者发现分布的平滑是一个定位异常的有效方法,可以和错误的分布形成对比。比如一些价格的异常就会在原始的分布图上形成一些峰值。
2.促进泛化;作者团队从实践中得出,网络的输出层分布是趋向于越来越平滑的,如下三个图
分别是同一批样本经过网络的输出,以及在网络第二和第一层隐层中的分布。根据这些图,作者认为DNN之所以泛化性能好,是因为我们在构建一个大量特征的模型的时候,特征的组合空间非常的大,在训练过程中往往只能覆盖一小部分的特征组合。为了让网络的泛化性能更好,模型下层的分布平滑就进一步保证来上层对那些没见过的值的行为进行更为准确的预测,所以模型一层层上去就会越来越平滑,那么反过来,如果在输入数据的时候就尽最大努力让输入数据有一个平滑的分布,那么模型拟合起来也应该更好。
那么怎么来测试训练好的模型泛化性能远远超出所有日志中的样本呢?在真实生成环境下的话,可以直接测试,但是这里作者提出来一种可以用来离线测试的方法:缩放测试集中给定特征的所有值,比如价格扩大2倍,4倍等。然后观察指标NDCG的变化,可以观察到模型的性能在这些之前没有见过的值上面也非常的稳定。
大多数特征在经过调试和适当的归一化后,就可以获得平滑的分布。但是对于少数的特征,就需要做专门的特征工程,比如房源的地理位置,它是由其经纬度表示的,如图
图a和b是经纬度原始的分布,为了确保分布的平滑,作者通过计算点到地图显示的中心偏移来作为优化后的经纬度特征。
图c和d描述的是经过偏移后的特征分布,图d展示的是再经过log偏移量后的特征分布,图e和f是最后平滑好的特征的分布,可以看见特征明显比之前平滑来很多。
3.用于检查特征的完整性。 什么意思呢?在某些情况下,通过查看特征的分布是否平滑,可以发现模型是否存在缺失的特征。比如,作者将房源的未来可用天数作为一个特征,直观感觉是高质量的房源肯定会在短时间内被卖出去。但是如下图所示:
分布并不是平滑的,或者说平滑但是缺失了一块,通过分析发现,原来房源都有最短居住要求,这个时间长短不一,所以需要进一步加入这个特征,才能更全面的描述数据。
高数量的类别特征就是那些取值特别多的类别特征。所以这些特征就需要特殊额编码处理。作者举例了位置编码,通过将位置query利用hash函数映射到整数上,建立新的类别特征,然后再把这些类别特征映射为embedding向量,喂到网络中,再利用网络训练。
整个搜索的过程如下:用户输入搜索的query,调用java服务进行检索并为房源打分排序。利用Thrift存储产生的日志,利用spark 处理日志以此获得训练数据。模型的训练使用的是Tensorflow,大部分代码都是用Scala和java来完成,训练好的模型加载到java服务中,用于检索和排序打分,所有的组建都运行在AWS上。
Protobufs 和数据集
GBDT模型的训练使用的训练数据是csv格式的数据,作者使用来大量相似的流程来将数据通过feef dict的方法输入tensorflow训练模型。但是分析后发现,整个训练过程大量的时间都花在了解析csv文件和数据的复制上面,而且CPU的使用率只有25%作用。所以最后作者改用Protobuf来产生训练数据,速度提升来17倍,GPU利用率达到了90%。使得原本只能用几周的数据进行训练,现在可以扩大到几个月的训练数据。
重构静态特征
其实输入的大量特征都是固定不变的,比如房源的位置,房间数量,各自设备等等各自属性,每次都要去读取全部的特征需要消耗大量的时间。所以为了解决这个磁盘io的问题,作者只使用了房源id作为类别特征,然后把其他全部静态的特征打包一个不能训练的embedding
Java NN Library
自己创建了一个神经网络库
dropout. 第一感觉,dropout是一种正则化技术,所以使用dropout是必须的,但是随着作者团队不断的尝试,发现基本用了dropout效果就会轻微的下降,所以作者认为dropout更接近于一种数据增强技术,用来模拟部分数据可能缺失的场景,但是在作者提出的这个场景下并没有相关的实际场景,所以使用dropout并不能有效的提高模型的泛化性能。
Initialiazation. 全0初始化肯定不行,因为每个节点学出来的值会都一样,所以最后作者对网络的权重使用了Xavier初始化,使用范围为{-1,1}的uniform来生成embedding。
Learning rate. 学习率他们使用的是adam的一种变体,LayAdamOptimizer,发现在使用大量的embedding后,它的训练速度更快。
Batch Size. batch的大小对训练速度的影响很大,但是对模型本身的影响却很难把握,最终他们根据经验选择来batch 大小为200.
评估特征重要性和模型的可解释性是模型优化迭代的基础。
1.分数分解。就是根据输出的预测值,去判断每个节点对这个分数的贡献。但是对于神经网络来说,因为层层相交,所以并不能清晰的划分出输入节点和输出的贡献。
2.烧蚀试验(Ablation Test)。还有一个想法就是,通过加减特征来查看模型的性能变化,但是这也有问题,第一很费时,第二因为模型之间存在冗余,所以很多时候模型的结果也并不能反应特征的重要程度。
3.置换试验(permutation test)。就是在测试集上,随机的置换特征,然后观察测试集上模型的性能,希望的是越重要的特征,越可能影响模型的性能。但是特征之间是存在关联的,其实实验输出的结果也没有什么意义。
4.TopBot 分析。自行研发的工具,用来解释这些特征而不以任何形式干扰模型。它以测试集作为输入,使用模型对每个测试query进行排序,然后统计每个query的顶部预测数据的特征分布,和底部预测数据的特征分布进行比较。通过比较可以了解到模型是怎么使用特征的。
通过上图可以发现,排在top的房源的价格是要相对来说偏低的,表面价格对模型排序的影响比较大。而评论次数相对来说对模型的影响并不大,说明评论次数这个特征对模型来说并不是很敏感。这就需要去考虑为什么了?是这个特征真的不重要,还是没有学习出来?
完