【阅读笔记】Applying Deep Learning To Airbnb Search

Applying Deep Learning To Airbnb Search

Airbnb Inc.
[email protected]
2018年10月25日

ABSTRACT

最初使用 gradient boosted decision tree model 来做 search ranking ,搜索效果从刚开始的上升逐渐趋于稳定。本文讨论如何突破趋于平稳的效果。本文的目的不是讲述模型上的突破,而是如何建立一个神经网络模型在一个实际的产品上。
KEYWORDS: Search ranking, Deep learning, e-commerce

INTRODUCTION

Airbnb 搜索排名的打分函数最早的版本是手动设计的,后来使用 GBDT(gradient boosted decision tree) 模型替换手动设计的打分函数,房屋预定得到了大幅度的提升,接着迭代优化了很多次。经过很长一段时间的迭代优化实验,发现在线预定房屋的收益达到了瓶颈。所以,在这个时候想尝试一些新的突破。
Fig.1
完整的模型生态是预测 guest 预定房间,预测 host 接受,预测 guest 给出5星好评的整个流程。本文讨论的是对可选房间进行排序的问题。如图1所示,典型的 guest 搜索 session 是 guest 搜索若干次,偶尔点击来查看房间细节,成功的 session 是 guest 预定预定某个 listing。这一过程被记录在 log 中。利用 log 训练新模型,使得离线效果有所提升,再使用 A/B test 线上测试,看指标是否有明显的提升,然后再上线新模型。
本文概述:首先概述模型架构演变的情况; 其次是一些工程方面的注意事项;然后描述了一些使用的工具和超参数探索;最后总结回顾。

MODEL EVOLUTION

我们模型的演变是一个渐进的过程,图2展示了离线指标 NDCG 和预定数随着模型演变而增长的过程。
Fig.2

Simple NN

Andrej Karpathy 对于 model architecture 有一个建议: don’t be a hero。
我们第一个模型是 a simple single hidden layer NN with 32 fully connected ReLU activations (minimizing the L2 regression loss where booked listings are assigned a utility of 1.0 and listings that are not booked a utility of 0.0)。结果是与 GBDT model 效果差不多。这个过程验证了神经网络上线的流程的可行性。

Lambdarank NN

当我们将 NN 与 Lambdarank 背后的想法结合起来时,我们的第一个突破就来了。Lambdarank 为我们提供了一种直接针对 NDCG 优化 NN 的方法。这涉及到简单 NN 的基于回归的公式的两个关键改进:

  • 损失函数转变为交叉熵。
  • 通过对 NDCG 影响的差异来权衡 pairwise loss。 例如,从第2位调整为第1位将优先于从第10位移动到第9位。
def apply_discount(x): 
    '''Apply positional discount curve''' 
    return np.log(2.0)/np.log(2.0 + x)

def compute_weights(logit_op, session): 
    '''Compute loss weights based on delta ndcg. 
    logit_op is a [BATCH_SIZE, NUM_SAMPLES] shaped tensor 
    corresponding to the output layer of the network. 
    Each row corresponds to a search and each column a listing in the 
    search result. Column 0 is the booked listing, while columns 1 through 
    NUM_SAMPLES - 1 the not-booked listings. '''
    logit_vals = session.run(logit_op)
    ranks = NUM_SAMPLES - 1 - logit_vals.argsort(axis=1)
    discounted_non_booking = apply_discount(ranks[:, 1:])
    discounted_booking = apply_discount(np.expand_dims(ranks[:, 0], axis=1))
    discounted_weights = np.abs(discounted_booking - discounted_non_booking)
    return discounted_weight

# Compute the pairwise loss 
pairwise_loss = tf.nn.sigmoid_cross_entropy_with_logits(targets=tf.ones_like(logit_op[:, 0]), logits=logit_op[:, 0] - logit_op[:, i:] ) 
# Compute the lambdarank weights based on delta ndcg 
weights = compute_weights(logit_op, session) 
#Multiply pairwise loss by lambdarank weights 
loss = tf.reduce_mean(tf.multiply(pairwise_loss, weights))

Decision Tree/Factorization Machine NN

For the FM model we took the final prediction as a feature into the NN. From the GBDT model, we took the index of the leaf node activated per tree as a categorical feature.
在这里插入图片描述

Deep NN

Typical configuration of the network: an input layer with a total of 195 features after expanding categorical features to embeddings, feeding the first hidden layer with 127 fully connected ReLUs, and then the second hidden layer with 83 fully connected ReLUs.
为DNN提供的 feature 大多是用最少的特征工程得到的简单的属性,如价格,便利设施数目种类,历史预订计数等。还有少量的其他评价模型输出的特征。
随着训练数据量的增加,我们明显的减少了 generalization gap。
Fig.3

FAILED MODELS

Listing ID

把每个 listing 变成一个 embedding 作为特征训练模型,发现过拟合了。这是因为 Airbnb 独特的性质,就算是最受欢迎的房间一年也只能预定365次,对于某些需要 embeding 的特征数某些值的数据量受到了很大的限制(我的理解就是噪声太大,导致 embeding 的结果不准确)。

Multi-task learning

把任务分为两个(对某个 listing 浏览多久和是否预定)进行多任务学习。在线效果不好。对看页面多久的理解是我们要继续的课题。

FEATURE ENGINEERING

Feature normalization

刚开始时,我么用与 GBDT 相同的特征训练 NN,效果很差。因为树模型对特征的大小关系敏感,而神经网络需要进行归一化,我们对正态分布进行中心归一化( f e a t u r e μ σ \frac{feature-\mu}{\sigma} ),对幂律分布进行 log 归一化( l o g ( 1 + f e a t u r e 1 + m e a n ) log(\frac{1+feature}{1+mean}) )。

Feature distribution

除了将特征映射到受限制的数值范围外,我们还确保其中大部分分布平滑。 为什么要沉迷于分布的平滑? 以下是我们的一些原因。

发现错误

在处理数以亿计的特征样本时,我们如何验证它们中的一小部分没有错误? 范围检查很有用但有限。 我们发现分布的平滑性是发现错误的宝贵工具,因为错误的分布通常与典型的分布不同。 举个例子,我们在某些地区的价格记录中,存在与市价明显不一致的错误。 这是因为在这些地区,对于超过28天的期间,记录的价格是每月价格而不是每日价格。 这些错误表现为分布图上的尖峰。

有利于泛化

解释 DNN 的泛化能力是研究前沿的复杂话题。我们发现在我们构建的 DNN 中,输出层的分布会逐渐变得越来越平滑。 图8显示了最终输出层的分布,而图9和图10显示了隐藏层的一些样本。 为了显示隐藏层中的值,我们忽略了零值并且进行 l o g ( 1 + v a l u e ) log(1 + value) 变换。这些分布图给予我们 DNN 泛化能力的直觉。当建立一个以数百个特征为基础的模型时,所有特征的组合空间不可思议的大,并且在训练期间,而且覆盖了一小部分组合特征。来自较低层的平滑分布确保了上层可以正确的输出。 将这种直觉一直延伸到输入层,我们尽最大努力确保输入功能具有平滑的分布。
我们而且发现下面的技术可用做模型鲁棒性检查:缩放测试集中给定特征的所有值,例如价格为2x,3x,4x等,并观察 NDCG 的变化。我们发现模型的性能非常稳定。
Fig.4
为了使地理特征分布更平滑,通过计算与中心点的偏移量来表征地理特征信息。

Checking feature completeness.

某些特征分布的不平滑,会导致模型的学习信息缺失。图 12(a)展示的是原始房屋占用分布,(b)展示的是(房屋占用 / 居住时长)归一化后的分布,分布不太符合正常理解,调查发现列表中有一些房屋有最低的住宿要求,可能延长到几个月。然而,开始我们没有添加最低的居住时长特征。所以,我们考虑添加最低居住时长作为模型的一个特征。
Fig.5

High cardinality categorical features

低数量类别的特征可以使用 one-hot 编码,对于高数量类别特征(例如邮编)利用一个哈希函数映射成一个数字。类别特征映射成 embedding,输入神经网络模型中,训练过程中,通过反向传播来学习这些位置偏好信息。

SYSTEM ENGINEERING

我们目前的 pipeline :一个访客的搜索查询通过 Java 服务端返回检索结果和分数;Thrift 来存储查询日志,Spark 来处理训练数据,TensorFlow 训练模型,各个工具都是使用 Scala 和 Java 来编写,模型上传到 Java 服务端给访客提供搜索服务。

Protobufs and Datasets

最开始使用训练 GBDT 的 CSV 格式,输入给 TensorFlow 模型的 feed_dict,后来发现我们的 GPU 利用率只有 25%,大部分的训练时间花费在解析 CSV 数据。后来使用 Protobufs 格式的数据集来训练,速度提升了 17 倍,GPU 利用率提升到 90%。

Refactoring static features

我们业务中有一些特征变化不大,比如位置、房间卧室的数量等,为了减少每次重复读取磁盘消耗时间,我们将它们组合起来为其创建一个索引,通过 list 的 id 来检索。

Java NN library.

在 2017 年我们打算开始将 TensorFlow 运用到生产环境的时候,发现没有基于 Java 的高效的技术栈。多个语言之间切换导致产生服务延迟。所以,我们在 Java 上自己创建了自己的神经网络打分函数库。

HYPERPARAMETERS

像 GBDT 中的超参数树的个数、正则化等一样,神经网络也许多超参数。下面是我们调超参数的一些经验分享:

Dropout

Dropout 对神经网络防止过拟合是必不可少的,但是在我们的实际应用中,尝试了多种正则化,都导致离线评估效果下降。所以,我们在训练数据集中随机复制一些无效的场景,是一种类似数据增强(data augmentation)的技术,来弥补这种缺失。另外,考虑到特定特征分布,我们手工添加了一些噪声数据,离线评估的 NDCG 提高大约 1%,但是在线统计评估并没有显著的提升。

Initialization

第一个模型所有权重和 embeddings 都初始化为零,效果非常差。现在选择 Xavier 来初始化所有的神经网络权重,使用 random uniform 初始化所有的 embeddings,其分布区间在{-1,1}之间。

Learning rate

对于我们的数据,发现使用 Adam 优化算法的默认参数很难提升效果,最后选择了 LazyAdamOptimizer,当训练 embeddings 时,速度非常快。

Batch size

改变 batch size 对训练速度影响非常大,但是它对模型的确切影响是很难把握的。在我们使用的 LazyAdamOptimizer 优化器中,剔除学习率的影响外,我们选择 batch size 的大小为 200 时,对我们目前的模型来说是最好的。

FEATURE IMPORTANCE

估计特征重要性和模型可解释性对于模型的实际应用有很重要的意义。特征重要性可以指导我们更好的迭代模型。神经网络最大的优势是解决特征之间非线性组合。这同时导致了解哪些特征对模型效果提升起关键作用这件事情变得困难了。下面分享一下我们在神经网络特征重要性方面的一些探索:

Score Decomposition

在神经网络中,分析特征的重要性很困难,容易让人产生混乱。我们最初的做法是获取神经网络产生的最终得分,并尝试将其分解为各个节点贡献得分。但是,在查看结果之后发现这个想法在逻辑上有个错误:没有一个清晰的方法可以将特定输入节点和经过非线性激活函数(ReLU 等)后的影响分开。

Ablation Test

另一种简单想法是一次次删减、替换特征,重新训练然后观察模型的性能,同时也可以考虑特征缺失导致性能成比例下降来衡量特征的重要性程度。然而,通过这种方法评估特征重要性有点困难,因为一些冗余的特征缺失,神经网络模型是可以弥补这种缺失的。

Permutation test

受随机森林模型特征重要性排序的启发,这一次我们尝试复杂一点的方法。在测试集上随机的置换特征,然后观察测试上模型的性能。我们期望的是越重要的特征,越会影响模型的性能。经试验测试发现好多无意义的结果,比如: 列表中房屋的数量特征对于预测房屋预定的概率影响非常大,但是仅仅测试这个特征,其实是无意义的,因为房屋的数量还跟房屋的价格有关联。

TopBot

TopBot 是我们自己设计的分析特征重要性的工具,它可以依据排序自上向下分析。图 14 展示了如何判断特征重要性,从图中可以看出 price 特征比较重要,review count 特征不是特别重要。
Fig.6

RETROSPECTIVE

在无处不在的深度学习成功案例中,最初我们很乐观的认为用深度学习直接取代 GBDT 模型就可以带来巨大的收益。所以最初的讨论都是围绕其他保持不变的情况下,仅替换当前的 GBDT 模型为神经网络模型,看能带来多大的收益。但这么做使我们陷入了绝望的低谷,没有得到任何的收益。随着时间的推移,我们意识到仅仅替换模型不够,还需要对特征处理加以细化,并且需要重新思考整个模型系统的设计。(像 GBDT 这样的模型受限于规模,易于操作,性能方面也表现不错,可以用来处理中等大小的问题。)
基于我们尝试的经验,我们极力向大家推荐深度学习。这不仅仅是因为深度学习在线获得的强大收益,它还改变了我们未来的技术路线图。早期的机器学习主要精力花费在特征工程上,转移到深度学习后,特征组合的计算交给神经网络隐层来处理,我们有更多的精力思考更深层次的问题,比如:改进我们的优化目标。目前的搜索排名是否满足所有用户的需求?经过两年探索,我们迈出了第一步,深度学习在搜索上的应用才刚刚开始。

猜你喜欢

转载自blog.csdn.net/SrdLaplace/article/details/84400900