《大数据处理实践探索》 ---- 使用spark MLlib进行机器学习(3超参数调优)

文章大纲

  • 基于树的模型
    • 决策树
    • 随机森林
  • k-Fold交叉验证
  • 参考文献


当数据科学家谈论调整他们的模型时,他们经常讨论调整超参数以提高模型的预测能力。 超参数是您在训练之前定义的关于模型的属性,它在训练过程中是不学习的(不要与参数混淆,这些参数是在训练过程中学习的)。 随机森林中的树数是超参数的一个例子。

在本节中,我们将重点使用基于树的模型作为超参数调优过程的示例,但同样的概念也适用于其他模型。一旦我们建立了用spark.ml进行超参数调优的力学,我们将讨论如何优化管道。 让我们先简单介绍一下决策树,然后介绍我们如何在spark.ml中使用它们。

基于树的模型

基于树的模型,如决策树、梯度增强树和随机森林,是相对简单但强大的模型,易于解释(这意味着,很容易解释他们所做的预测)。因此,它们在机器学习任务中很受欢迎。我们很快就会到达随机森林,但首先我们需要覆盖决策树的乐趣。

决策树

作为一种现成的解决方案,决策树非常适合于数据挖掘。 它们的构建速度相对较快,具有高度可解释性,并且不变比例(即,标准化或缩放数字特征不会改变树的性能)。那么什么是决策树呢?

决策树是从数据中学习的一系列if-then-else规则,用于分类或回归任务。假设我们试图建立一个模型来预测某人是否会接受工作机会,其特征包括工资、上下班时间、免费咖啡等。如果我们将决策树拟合到这个数据集,我们可能会得到一个类似于图10-9的模型。

《大数据处理实践探索》 ---- 使用spark MLlib进行机器学习(3超参数调优)_第1张图片

树顶上的节点被称为树的“根”,因为这是我们“分裂”的第一个事实。这一功能应该提供最有信息的分割-在这种情况下,如果工资低于5万$,那么大多数候选人将拒绝工作机会。“下降报价”节点被称为“叶节点”,因为没有其他分支从该节点中出来;它位于分支的末尾。(是的,有点好笑的是,我们把它叫做“树”,但把树的根画在顶部,叶子画在底部! )

然而,如果提供的工资大于5万,我们将继续在决策树中进行下一个信息最丰富的功能,在这种情况下,这是通勤时间。即使工资超过5万,如果通勤时间超过一个小时,那么大多数人会拒绝工作机会。

我们不会了解如何确定哪些功能将给您最高的信息增益在这里,但如果您是相互影响的,请查看第9章的统计学习要素,由特雷弗哈斯蒂,罗伯特Tibshirani和杰罗姆弗里德曼(斯普林格)。

我们的模型的最终特点是免费咖啡。在这种情况下,决策树显示,如果工资大于5万$,通勤不到一小时,而且有免费咖啡,那么大多数人会接受我们的工作提议(如果是这样的话! )。作为后续资源,R2D3对决策树的工作方式有很好的可视化。

可以在单个决策树中多次拆分同一特征,但每次拆分将以不同的值发生。

决策树的深度是从根节点到任何给定叶节点的最长路径。在图10-9中,深度为3。非常深的树很容易被过度拟合,或者在你的训练数据集中记住噪音,但是太浅的树会低于你的数据集(也就是说,可能从数据中获取更多的信号)。
随着决策树的本质被解释,让我们继续讨论决策树的特征预配比。对于决策树,您不必担心标准化或缩放您的输入特性,因为这对拆分没有影响-但是您必须小心如何准备您的分类特性。
基于树的方法可以自然地处理分类变量。在spark.ml中,您只需要将分类列传递给String Indexer,决策树就可以处理其余部分。让我们将决策树拟合到我们的数据集:

# In Python
from pyspark.ml.regression import DecisionTreeRegressor
dt = DecisionTreeRegressor(labelCol="price")
# Filter for just numeric columns (and exclude price, our label)
numericCols = [field for (field, dataType) in trainDF.dtypes
 if ((dataType == "double") & (field != "price"))]
# Combine output of StringIndexer defined above and numeric columns
assemblerInputs = indexOutputCols + numericCols
vecAssembler = VectorAssembler(inputCols=assemblerInputs, outputCol="features")
# Combine stages into pipeline
stages = [stringIndexer, vecAssembler, dt]
Hyperparameter Tuning | 309
pipeline = Pipeline(stages=stages)
pipelineModel = pipeline.fit(trainDF) # This line should error
// In Scala
import org.apache.spark.ml.regression.DecisionTreeRegressor
val dt = new DecisionTreeRegressor()
 .setLabelCol("price")
// Filter for just numeric columns (and exclude price, our label)
val numericCols = trainDF.dtypes.filter{
      case (field, dataType) =>
 dataType == "DoubleType" && field != "price"}.map(_._1)
// Combine output of StringIndexer defined above and numeric columns
val assemblerInputs = indexOutputCols ++ numericCols
val vecAssembler = new VectorAssembler()
 .setInputCols(assemblerInputs)
 .setOutputCol("features")
// Combine stages into pipeline
val stages = Array(stringIndexer, vecAssembler, dt)
val pipeline = new Pipeline()
 .setStages(stages)
val pipelineModel = pipeline.fit(trainDF) // This line should error

由此产生以下错误:
java。朗。. 非法参数异常:需求失败:决策树要求最大Bins(=32)至少与每个分类特征中的值数一样大,但是分类特征3有36个值,考虑用大量的值删除这个和其他分类特征,或者添加更多的训练示例。
我们可以看到,maxBins参数有问题。那是做什么的?最大Bins确定连续特征被离散或分割的桶数。这个离散化步骤对于执行分布式训练至关重要。在scikit学习中没有最大Bins参数,因为所有的数据和模型都驻留在一台机器上。然而,在Spark中,工作人员拥有数据的所有col-umns,但只有行的子集。因此,在沟通要拆分哪些特性和值时,我们需要确保它们都在谈论相同的拆分值,这是我们从训练时设置的常见离散化中得到的。让我们看看图10-10,它显示了分布式决策树的PLANET实现,以更好地理解分布式机器学习,并说明最大Bins参数。

《大数据处理实践探索》 ---- 使用spark MLlib进行机器学习(3超参数调优)_第2张图片

每个工人都必须计算每个特征和每个可能的分裂点的汇总统计数据,这些统计数据将在工人之间进行汇总。MLlib要求max Bins足够大,以处理分类列的离散化。 最大Bins的默认值是32,我们有一个具有36个不同值的分类列,这就是为什么我们更早地得到错误的原因。虽然我们可以将maxBins增加到64,以更准确地表示我们的连续特征,但这将使连续变量的可能分裂次数增加一倍,大大增加了我们的计算时间。让我们将maxBins设置为40,并重新培训管道。您将在这里注意到,我们使用setter方法集MaxBins()来修改决策树,而不是完全重新定义它:

# In Python
dt.setMaxBins(40)
pipelineModel = pipeline.fit(trainDF)
// In Scala
dt.setMaxBins(40)
val pipelineModel = pipeline.fit(trainDF)

由于实现上的差异,在使用scikit-learn与MLlib构建模型时,通常不会得到完全相同的结果。不过,没关系。 关键是要理解它们为什么不同,并看看在您的控制中有哪些参数,以使它们以您需要的方式执行。 如果您将工作负载从scikit-learn移植到MLlib,我们鼓励您查看spark.ml和scikit-learn文档,以查看哪些参数不同,并调整这些参数以获得相同数据的可比结果。一旦值足够接近,您就可以将MLlib模型扩展到scikit学习无法处理的更大数据大小。

现在我们已经成功地建立了我们的模型,我们可以提取决策树学习的if-then-else规则

# In Python
dtModel = pipelineModel.stages[-1]
print(dtModel.toDebugString)
// In Scala
val dtModel = pipelineModel.stages.last
 .asInstanceOf[org.apache.spark.ml.regression.DecisionTreeRegressionModel]
println(dtModel.toDebugString)
DecisionTreeRegressionModel: uid=dtr_005040f1efac, depth=5, numNodes=47,...
 If (feature 12 <= 2.5)
 If (feature 12 <= 1.5)
 If (feature 5 in {
     1.0,2.0})
 If (feature 4 in {
     0.0,1.0,3.0,5.0,9.0,10.0,11.0,13.0,14.0,16.0,18.0,24.0})
 If (feature 3 in
{
     0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,...})
 Predict: 104.23992784125075
 Else (feature 3 not in {
     0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,...})
 Predict: 250.7111111111111
...

这只是打印输出的一个子集,但您会注意到,可以不止一次地在同一特性上拆分(例如,特征12),但在不同的拆分值上。还请注意决策树如何分割数字特征与分类特征之间的区别:对于数字特征,它检查值是否小于或等于阈值,对于分类特征,它检查值是否在该集合中。
我们还可以从我们的模型中提取特征重要性分数,以看到最重要的特征:

# In Python
import pandas as pd
featureImp = pd.DataFrame(
 list(zip(vecAssembler.getInputCols(), dtModel.featureImportances)),
312 | Chapter 10: Machine Learning with MLlib
 columns=["feature", "importance"])
featureImp.sort_values(by="importance", ascending=False)
// In Scala
val featureImp = vecAssembler
 .getInputCols.zip(dtModel.featureImportances.toArray)
val columns = Array("feature", "Importance")
val featureImpDF = spark.createDataFrame(featureImp).toDF(columns: _*)
featureImpDF.orderBy($"Importance".desc).show()

《大数据处理实践探索》 ---- 使用spark MLlib进行机器学习(3超参数调优)_第3张图片

虽然决策树非常灵活和易于使用,但它们并不总是最精确的模型。如果我们要计算我们的R2在测试数据集上,我们实际上会得到一个负分数!这比仅仅预测平均值还要糟糕。(你可以在这一章的笔记本中看到这一点,这本书的GitHub回购。 )
让我们看看如何通过使用集成方法来改进这个模型,该方法结合了不同的模型,以获得更好的结果:随机森林。

随机森林

集会以民主的方式开展工作。想象一下,一个罐子里有很多M&MS。你让一百个人来猜测M&MS的数量,然后取所有猜测的平均值。平均值可能比大多数个人猜测更接近真实值。同样的概念也适用于机器学习模型。如果你建立了许多模型,并结合/平均它们的预测,它们将比任何单个模型产生的模型更健壮。
随机森林是决策树的集合,有两个关键的调整:

按行引导样本
引导是一种通过从原始数据中替换采样来模拟新数据的技术。每个决策树都是在数据集的不同引导带样本上训练的,它会产生稍微不同的决策树,然后汇总它们的预测。这种技术被称为引导聚合或套袋。在一个典型的随机森林实现中,每棵树从原始数据集中替换相同数量的数据点,并且该数字可以通过次采样速率参数来控制。

按列随机特征选择

套袋的主要缺点是树都是高度相关的,因此在数据中学习类似的模式。为了缓解这个问题,每次您想进行拆分时,您只考虑列的随机子集(随机森林回归器的三分之一特征和随机森林分类器的#特征)。由于您介绍的这种随机性,您通常希望每棵树都很浅。你可能在想:这些树中的每一棵都会比任何一棵决策树表现得更差,那么这种方法怎么可能更好呢?事实证明,每一棵树都了解到了关于数据集的不同之处,并将这些“弱”学习者的集合组合成一个集合,使for-est比单个决策树更健壮。
显示了训练时间的随机森林。在每个拆分中,它考虑10个原始特征中的3个进行拆分;最后,它从这些特征中选择最佳特征

《大数据处理实践探索》 ---- 使用spark MLlib进行机器学习(3超参数调优)_第4张图片

随机森林和决策树的API是相似的,两者都可以应用于回归或分类任务:


# In Python
from pyspark.ml.regression import RandomForestRegressor
rf = RandomForestRegressor(labelCol="price", maxBins=40, seed=42)
// In Scala
import org.apache.spark.ml.regression.RandomForestRegressor
val rf = new RandomForestRegressor()
 .setLabelCol("price")
 .setMaxBins(40)
 .setSeed(42)

一旦你训练了你的随机森林,你就可以通过在集合中训练的不同树传递新的数据点。
如图10-12所示,如果您构建一个用于分类的随机森林,它将通过森林中的每一棵树的测试点,并在单个树木的预测中获得多数票。 (相反,在回归中,随机森林模拟将这些预测平均化。) 尽管这些树中的每一棵比任何单个决策树都不那么有效,但集合(或集合)实际上提供了一个更健壮的模型。
图10-12。 随机森林预测

《大数据处理实践探索》 ---- 使用spark MLlib进行机器学习(3超参数调优)_第5张图片
随机森林真实地演示了使用Spark进行分布式机器学习的能力,因为每棵树都可以独立于其他树(例如,在构建树10之前,不需要构建树3)。此外,在树的每个级别内,您可以并行化工作以找到最佳分割。

那么,我们如何确定我们的随机森林中的最佳树数或这些树的最大深度应该是多少呢?这个过程称为超参数调优与参数相反,超参数是一个值,它控制模型的学习过程或结构,并且在训练过程中不学习它。. 树的num-ber和最大深度都是超参数的例子,您可以为随机森林调优。让我们现在把重点转移到如何通过调整一些超参数来发现和评估最佳随机森林模型。

k-Fold交叉验证

我们应该使用哪个数据集来确定最优超参数值?如果我们使用训练集,那么模型很可能会过度拟合,或者记住我们训练数据的细微差别。这意味着它不太可能推广到看不见的数据。但是,如果我们使用测试集,那么这将不再代表“看不见的”数据,因此我们将无法使用它来验证我们的模型泛化的程度。因此,我们需要另一个数据集来帮助我们确定最优的超参数:验证数据集。
例如,不像我们做的那样,将我们的数据分割成80/20列车/测试拆分,我们可以分别执行60/20/20拆分以生成培训、验证和测试数据集。然后,我们可以在训练集上建立我们的模型,在验证集上评估性能以选择最佳的超参数配置,并将模型应用于测试集,看看它在新数据上的表现如何。然而,这种方法的缺点之一是我们失去了25%的训练数据(80%->60%),这可以用来帮助改进模型。这促使使用k-折叠交叉验证技术来解决这个问题。
使用这种方法,我们不像以前那样将数据集分割成单独的培训、验证和测试集,而是将其分割成培训和测试集-但是我们使用培训数据进行培训和验证。为了实现这一点,我们将训练数据分成k个子集,或“折叠”(例如,三个)。然后,对于给定的超参数配置,我们在k-1倍上训练我们的模型,并对剩余的折叠进行评估,重复这个过程k次。图10-13说明了这种方法。
图10-13.k-折叠交叉验证

《大数据处理实践探索》 ---- 使用spark MLlib进行机器学习(3超参数调优)_第6张图片
如图所示,如果我们将数据分割成三个折叠,我们的模型首先在数据的第一和第二个折叠(或分裂)上进行训练,并在第三个折叠上进行评估。然后,我们在第一次和第三次折叠上建立了相同的超参数模型

数据,并评估其在第二次折叠上的性能。最后,在第二和第三褶皱上建立模型,并在第一褶皱上进行评价。然后,我们将这三个(或k)验证数据集的性能进行平均,作为该模型对未见数据执行情况的一个代理,因为每个数据点都有机会准确地成为验证数据集的一部分。接下来,我们对所有不同的超参数配置重复这个过程,以确定最优配置。
确定超参数的搜索空间可能很困难,通常随机搜索超参数优于结构化网格搜索。 有专门的库,如Hyperopt,帮助您识别最优的超参数配置,我们在第11章中讨论了这些配置。
要在Spark中执行超参数搜索,请采取以下步骤:
1.定义要评估的估计量。
2.使用ParamGridBuilder指定您想要更改的超参数以及它们各自的val-ue。
3.定义一个评估器来指定使用哪个度量来比较各种模型。
4.使用交叉验证器执行交叉验证,评估每个变量模型。
让我们从定义管道评估器 estimator开始:

# In Python
pipeline = Pipeline(stages = [stringIndexer, vecAssembler, rf])
// In Scala
val pipeline = new Pipeline()
 .setStages(Array(stringIndexer, vecAssembler, rf))

参考文献

你可能感兴趣的:(spark,参数调优,pyspark)