使用4个机器学习库来处理大数据
在Booking.com,我们可以根据客户和合作伙伴(酒店,住宅,度假租赁等)如何与我们的平台互动来处理大量数据。我们的主要挑战之一是在合理的时间内在这些非常大且稀疏的数据集上训练准确的机器学习模型。
在这篇文章中,我们比较了四种最流行的机器学习库的性能,这些库为大数据提供了解决方案:H2O,TensorFlow,SparkMLlib 和VowpalWabbit。目前我们将仅涵盖线性模型,将其他机器学习模型的比较作为未来的工作。我们的目标是专注于更大的数据集的解决方案,这些数据集无法融入单个计算机的内存中。
数据集的描述
Booking.com网站为任何能上网的人提供全球各地的住宿机会。为了更好地分析我们的数据,我们将世界分为超过100k个不同的位置,并为它们分配一个唯一的id。当然可以通过相似的地点聚类或使用目标编码来降低维度问题。然而,由于booker和property location的每种组合都表现出非常不同的市场行为,因此将每个位置作为单独的特性使用通常更有效。
另一方面,训练一个具有如此多特征的模型需要大量的数据来防止过度拟合,在某些情况下,还需要大量的内存来执行计算。当数据科学家不得不选择一个机器学习库来使用时,这对他们来说是一个巨大的挑战。
为了比较最流行的库的算法和性能,我们使用了简化版的数据集来处理我们在Booking.com上正在处理的问题:
当客户到达Booking.com网站时,他们需要指定目的地和日期间隔。然后将向他们显示可用属性列表。在搜索会话期间,我们希望预测客户下次单击的属性的评分。良好的预测使Booking.com能够为用户提供更好,更个性化的体验。
不幸的是,一种简单的启发式方法并不能带来令人满意的结果。图2中的表格显示了基线预测的主要指标,这些指标是在会话中单击的评论分数的平均值。如果这是不可用的,我们使用一个给定目的地的平均评论分数。下面我们报告通过比较标记数据和基线预测获得的均方根误差(RMSE),决定系数(R 2)和平均绝对误差(MAE)。
图2:使用简单启发式的目标变量预测结果
为了改善这种预测,我们对80%的数据进行线性回归,将20%作为测试集。我们使用H2O,TensorFlow,SparkMLlib和VowpalWabbit来做到这一点。
线性回归
最简单但性能最佳且使用最广泛的机器学习算法之一是线性回归。要预测的变量Y被假定为一组特征的线性函数。如果我们有一个包含N个示例和M个特征的数据集,我们假设保持以下关系:
Y是 N个目标值的矢量,X是特征( N × M矩阵), α是必须估计的 M个系数的矢量。假设线性估计和ground truth 之间的剩余偏差ε的分布正态地分布为零均值。
训练线性回归意味着找到最适合观察数据的向量α。这相当于找到α最小化最小平方损失函数:
最小化损失函数的α值是将其梯度设置为零的值。找到这一点的典型方法是运行梯度下降 算法。从α空间中的随机点开始,算法计算该点的损失函数的梯度,并将系数更新为负梯度方向的一步:
该步骤的大小由参数λ控制,称为学习率。该算法通常在有限次数的迭代之后收敛,即参数值的微小变化不会导致损失函数值的任何显着降低。另一种流行的方法是使用二阶方法 ,通过将梯度与Hessian的倒数(或其近似值)相乘来选择下降方向。
一种流行的二阶方法是Broyden-Fletcher-Goldfarb-Shanno算法(L- bfgs),其中“L”只用于有限的内存使用。L-BGFS使用的是Hessian的逆的近似,它要求在内存中保留以前的梯度值的固定数量。
为了计算梯度,(如果需要的话),对逆Hessian的近似值,并更新系数,计算机必须记住所有的数据和每个迭代中系数的所有值。对于大于O(10GB)的数据集来说,这在技术上是不可行的,这是Booking.com的标准。幸运的是,有一些机器学习工具提供了不同的策略来解决这个问题:
- H2O通过将数据集拆分为块来分配计算。在每次迭代时,为每个块计算梯度和近似逆Hessian。这部分计算就像一个“mapper”,因为与随机梯度下降相比,模型参数没有更新。在迭代的第二部分中,就像在“reducer”中一样,渐变和近似Hessian被组合以更新权重。默认优化算法是LBGFS,但用户可以选择其他几种算法。所有数据都存储在内存中,因此H2O需要分配一个固定的,相对较大的内存来运行。
- SparkML使用类似于H2O的方法在执行程序上分配计算。然而,使用稀疏向量存储特征示例,这允许减少所需的存储量。用户可以通过调整分区数来控制数据集分割的块的大小。增加spark会话的执行程序的数量可能有助于减少训练时间,因为可以并行执行更多操作。
- VowpalWabbit使用随机梯度下降(SGD)。该算法一次读取一行数据,并仅使用该行的信息更新梯度和系数。然后它重新计算损失函数并进入下一个例子。消耗的内存量与数据集中的行数无关,并且相对较小,因为只需要存储系数的值和损失函数的值。对于在每个更新步骤读取所有数据的方法,在SGD需要更多迭代(即,更多数据点)来收敛时使用仅一行的信息。VowpalWabbit针对读取数据进行了高度优化。系数更新比普通梯度下降更复杂。学习率不是恒定的,而是取决于随时间更新和衰减的系数。根据读取的新值重新标准化功能。
- TensorFlow肯定是这四个中最可定制的库。它主要用于构建深度学习算法,但也提供了使用张量操作构建模型的通用框架。用户可以完全控制最小化算法,并可以根据需要进行修改。在本研究中,我们使用了小批量梯度下降,与SGD非常相似,但是使用一批数据而不是仅仅一行来更新梯度。在小批量大小表示在每个迭代中使用的行数。在本研究中,我们使用64的小批量大小。与Vowpal Wabbit类似,默认情况下,梯度更新规则比普通梯度下降更复杂。默认优化器是Follow-The-Regularized-Leader(FTRL)。
TensorFlow还支持散列技巧。与VowpalWabbit的主要区别在于可以独立地为每个特征列设置散列大小。
特征编码
超出数据集的大小,特征的数量也在训练所需的内存量中起着重要作用。事实上,在每次迭代时,需要计算所有的一阶导数,并且应该更新所有系数(对于L-BFGS,还需要保留在一些先前步骤中计算的导数)。这意味着训练具有多个分类变量的模型(例如属性的位置)可能是内存昂贵的。我们来解释一下原因。
要在线性回归中使用分类特征,必须先对其进行编码。最常用的编码之一是one hot编码,即将分类变量转换为二进制向量,如下所示:
图3:分类功能的one-hot编码,例如属性所在的国家/地区
包含200个不同值(大致相当于世界上国家的数量)的分类特征将被转化为200个单独的特征。对于我们想要解决的问题,我们有这样一个数据集:
图4:用于训练线性重新生成的数据的快照
不同的列是:
- visitor_loc_id:访问者的位置ID
- dest_id:属性的位置ID
- avg_score:会话中单击的属性的平均评分
- prev_score:点击最后一个属性的评分
- target:点击下一个属性的得分,我们想要预测的内容
所有特征都被视为分类。为此,prev_score和avg_score的值已在第一个小数位被截断。每个特征的基数如下表所示:
图5:数据集中分类特征的基数
经过one-hot-encoding后,我们获得了280164种不同的特征。为了在不事先知道VowpalWabbit特征的基数的情况下执行one-hot-encoding,使用所谓的散列技巧。它首先通过哈希函数将所有要特征转换为整数,然后对结果进行one-hot-encoding。用户可以通过指定散列值的位数来选择散列函数的共域的大小。对于这项研究,我们使用28位,即268435456可能的散列函数的不同结果(VowpalWabbit的默认值是18位)。较大数量的比特减少但不消除冲突的机会,即将两个不同的特征映射到相同的散列值。
我们使用H2O,SparkML,TensorFlow和VowpalWabbit,使用490M数据点(~20GB)对该数据集进行了线性回归。
机器学习库比较
我们为所有四个库训练了一个线性回归模型。由于行数与特征数量之比非常大,我们决定不应用任何正则化项。我们尝试尽可能少地从库的基本实现中进行更改,以重现典型用法。
在不改变初始参数的情况下,H2O在测试集上给出RMSE = 0.323 ,显着高于用所有其他库获得的RMSE <0.3。这是因为,默认情况下,H2O将弹性网正则化项添加到最小平方损失函数,强度为λ。使用基于训练数据的试探法计算参数λ的值。参数α,控制L1和L2之间的平衡惩罚默认设置为0.5。对于大数据和稀疏数据的问题,这种正则化太强,导致最小化算法陷入局部最小值。为了避免这个问题,H2O用户需要在每次训练线性回归时明确设置λ= 0。
将正则化项设置为零,H2O的性能与SparkML,TensorFlow和VowpalWabbit保持一致。四个库之间比较的最终结果如下图6所示:
图6:测试集上的RMSE和MAE与正在研究的四个库的比较
我们观察到训练速度和内存消耗之间的权衡,如下图所示:
图7:观察到的训练时间。* TensorFlow有两个值:一个是默认值,另一个是经过优化的管道
图8:训练过程中使用的内存
我们已经报告了TensorFlow的两个训练时间,因为我们已经观察到根据数据读取方式的显着变化。具体来说,我们使用TensorFlow版本1.8并使用推荐的tf.data API读取数据。此外,训练数据以推荐的T FRecords格式存储,使用TensorFlow连接器在Spark中编写。TFRecords是针对阅读优化的序列化训练示例,然后在训练时对其进行反序列化。
使用默认设置,我们观察了6小时15分钟的训练时间,这段时间大大减少到2小时7分钟,优化了数据读取过程。识别数据读取管道中的瓶颈被证明是非常重要的。关于这个优化的技术讨论超出了本文的范围。我们发现,在反序列化之前对tf记录进行批处理,并创建一个预取数据的缓冲区,可以显著提高速度。
对每个模型最重要的指标、训练的内存和时间、以及所使用的算法的完整概述如下表所示
图9:包含每个库最重要指标的表
括有关训练时间和内存使用情况的信息。* TensorFlow训练时间引用两个值:一个具有默认值,另一个具有优化管道
特征交互
为了用线性回归来描述非线性关系,通常将特征相互作用包括在最小化中。如果这些特性是绝对的,这就意味着创建新的级别,由原始级别的组合提供,并将它们插入到损失函数中。创建所有交互将产生超过170亿个独特的特征。
因为我们只有490M的数据点,所以不可能限制这么多的参数,因此显式地对所有级别进行one-hot编码没有多大意义。我们考虑了两种不同的方法:
- 散列所有特征空间。无论所有特征交互的基数如何,都将考虑最多2 ^ b个级别,其中b是散列函数的位数。如前所述,这种方法与VowpalWabbit开箱即用。用户只需要指定他想用二次或三次交互作为命令行参数训练模型。然而,我们注意到,在VW(-q ::)中交互所有特征的默认方法在训练时间中引入了大量开销。因此,我们使用参数-q aa和分配给同一名称空间a的所有功能手动指定了交互。与前一节一样,我们使用b = 28。TensorFlow允许使用交叉层创建交互,交叉层交互两个分类层,并将结果散列为用户指定的每个交互的多个buckets。对于6对成对相互作用中的每一个,我们使用了大小为10⁶的buckets。使用SparkML和H2O,用户需要预处理数据以创建散列记录。Spark版本2.3.0提供了一种允许非常容易地执行此操作的方法。但是,在我们的例子中,当我们使用Spark 2.2.0时,我们使用MurmurHash3实现了转换Scala 2.11库中提供的函数。我们将位数限制为24,否则程序将在one-hot编码阶段崩溃。H2O不是为执行此操作而设计的。用户原则上可以在Spark中进行散列,然后将数据复制到H2O。然而,这将是一个相当复杂的程序,我们不建议这样做。相反,要远离线性回归并使用其中一种H2O方法进行非线性算法,例如:随机森林或梯度增强机器,这样更容易,更有效。
- 削减低发生类别。只考虑了一些相互作用。在我们的问题中,我们有两个具有非常高基数的特性:dest_id和visitor_loc_id。我们只考虑了涉及这些特征的级别的相互作用,其发生次数大于10000.每个交互的其他组合被放在一个区域中。这导致了具有626310独特功能的数据集,可以通过H2O和SparkML消化,而无需用户进行任何进一步操作。
如下面的图10所示,使用交互对模型进行训练,可以使RMSE和MAE有一个小的改进,同时可以显著增加内存消耗和训练时间。对于SparkML,我们将结果绘制为切割低发生率类别。由于所采用的方法是不同的,所以在库中进行训练时间和内存消耗的比较是不完全公平的。
图10:在测试集上测量的RMSE和MAE与所研究的四个库的比较,包括模型中的特征相互作用
图11:包含每个库最重要指标的表
包括有关训练时间和内存使用情况的信息。“Strategy”列指示了在模型中包含要素交互的方法。* TensorFlow使用优化和默认管道运行
结论
我们比较了四种最流行的机器学习库来训练生产线性模型。尽管这些库以固有的不同方式处理大数据,但它们的表现非常相似。
H2O 强大卖点是其易于使用的语法和详细的界面。这使得即使是非专家也可以在很短的时间内训练大数据的机器学习模型。这种用户友好性的代价 是,有时H2O对于其背后发生的事情并不完全透明。这使得难以理解为什么模型表现不佳。事实上,在我们的例子中,最好覆盖默认参数以明确地将正则化项设置为零。
SparkML代表了分布式机器学习的有效替代方案,训练时间非常短。内存和资源占用空间很大,但可以根据可用资源进行调制(以速度为代价)。SparkML还具有在同一框架中提供机器学习工具和强大的Spark数据重复功能的巨大优势。不幸的是,没有任何界面来监控训练进度,并且语法不像H2O那样友好。
VowpalWabbit已经提供了开箱即用设置的良好效果。散列函数的位数基本上是在大型稀疏数据集上训练线性模型时唯一需要关注的参数。由于哈希技巧的开箱即用实现,它在内存消耗方面是最有效的库。另一方面,这些库提供了有限的非线性算法选择和一个非常基本的界面来监控训练的进度。用户需要努力计算最基本的训练指标。为了解释模型,需要反转散列函数。对于大位值,此操作可能在计算上很昂贵。
TensorFlow具有与VowpalWabbit类似的内存和资源占用空间,具有更好的用户界面和灵活的算法。用户可以完全定制最小化策略以使其适应每个特定问题。训练时间的显着增加是由于底层代码的复杂性更高。实际上,该库是一个针对各种优化问题的通用求解器。找到最小化训练时间的最佳配置本身就是一项非常重要的任务。在这里,我们使用了一种简单的方法来尝试重现库的标准用法,并使用更复杂的方法来显示可以观察到多少变化。
总之,关于使用哪个库的选择实际上取决于手头的问题和可用的资源量。有了适合您的大型集群,可以使用H2O库,特别是如果您熟悉大数据的机器学习。如果数据准备部分也在Spark中完成,SparkML提供了一个很好的选择。如果资源有限,VowpalWabbit在大型数据集上提供了良好的性能,但非线性选项很少。对于更具可扩展性和定制的解决方案,TensorFlow提供了其他任何库都无法提供的良好性能和灵活性。