机器学习调优:模型选择与超参数调优

\[ \newcommand{\R}{\mathbb{R}} \newcommand{\E}{\mathbb{E}} \newcommand{\x}{\mathbf{x}} \newcommand{\y}{\mathbf{y}} \newcommand{\wv}{\mathbf{w}} \newcommand{\av}{\mathbf{\alpha}} \newcommand{\bv}{\mathbf{b}} \newcommand{\N}{\mathbb{N}} \newcommand{\id}{\mathbf{I}} \newcommand{\ind}{\mathbf{1}} \newcommand{\0}{\mathbf{0}} \newcommand{\unit}{\mathbf{e}} \newcommand{\one}{\mathbf{1}} \newcommand{\zero}{\mathbf{0}} \]

本节描述了如何使用 MLlib 的工具来调优 ML 算法和管道。内置的交叉验证和其他工具允许用户优化算法和管道中的超参数。

目录

模型选择(即超参数调优)

在机器学习中,一个重要的任务是 模型选择 ,或者说使用数据来寻找给定任务的最佳模型或参数。这也被称为 调优 。调优可以针对单个 Estimator ,例如 LogisticRegression ,也可以针对包含多个算法、特征化和其他步骤的整个 Pipeline 。用户可以一次性调优整个 Pipeline ,而不是单独调优 Pipeline 中的每个元素。

MLlib支持使用工具进行模型选择,例如 CrossValidator TrainValidationSplit 。这些工具需要以下几项:

在高层次上,这些模型选择工具的工作原理如下:

这个 Evaluator 可以是一个 RegressionEvaluator 用于回归问题,一个 BinaryClassificationEvaluator 用于二元数据,一个 MulticlassClassificationEvaluator 用于多类问题,一个 MultilabelClassificationEvaluator 用于多标签分类,或者一个 RankingEvaluator 用于排名问题。用于选择最佳 ParamMap 的默认指标可以通过每个评估器中的 setMetricName 方法进行覆盖。

为了帮助构建参数网格,用户可以使用 ParamGridBuilder 工具。默认情况下,参数网格中的参数集是串行评估的。通过在运行模型选择之前将 parallelism 设置为2或更高的值,可以并行进行参数评估(值为1将是串行的),可以使用 CrossValidator TrainValidationSplit parallelism 的值应谨慎选择,以最大化并行性而不超过集群资源,较大的值不一定总能提高性能。一般来说,值高达10对于大多数集群来说应该是足够的。

交叉验证

CrossValidator 首先将数据集拆分为一组 折叠 ,这些折叠将用作单独的训练和测试数据集。例如,使用 $k=3$ 折叠, CrossValidator 将生成 3 对(训练,测试)数据集,每对使用 2/3 的数据进行训练,1/3 的数据进行测试。为了评估特定的 ParamMap CrossValidator 计算通过在 3 对不同的(训练,测试)数据集上拟合 Estimator 生成的 3 个 Model 的平均评估指标。

在识别出最佳的 ParamMap 后, CrossValidator 最终使用最佳的 ParamMap 和整个数据集重新拟合 Estimator

示例:通过交叉验证进行模型选择

以下示例演示了使用 CrossValidator 从参数网格中进行选择。

请注意,在参数网格上进行交叉验证是昂贵的。 例如,在下面的示例中,参数网格对 hashingTF.numFeatures 有 3 个值,对 lr.regParam 有 2 个值,而 CrossValidator 使用 2 次折叠。这意味着需要训练 $(3 \times 2) \times 2 = 12$ 个不同的模型。 在实际情况下,尝试更多参数和使用更多折叠是很常见的( $k=3$ $k=10$ 是常见的)。 换句话说,使用 CrossValidator 可能非常昂贵。 然而,这也是一种选择参数的成熟方法,其统计基础优于启发式手动调优。

有关API的更多详细信息,请参阅 CrossValidator Python文档

from pyspark.ml import Pipeline
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import BinaryClassificationEvaluator
from pyspark.ml.feature import HashingTF, Tokenizer
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
# 准备训练文档,这些文档是有标签的。
training = spark.createDataFrame([
(0, "a b c d e spark", 1.0),
(1, "b d", 0.0),
(2, "spark f g h", 1.0),
(3, "hadoop mapreduce", 0.0),
(4, "b spark who", 1.0),
(5, "g d a y", 0.0),
(6, "spark fly", 1.0),
(7, "was mapreduce", 0.0),
(8, "e spark program", 1.0),
(9, "a e c l", 0.0),
(10, "spark compile", 1.0),
(11, "hadoop software", 0.0)
], ["id", "text", "label"])
# 配置一个机器学习管道,包含三个阶段:分词器,哈希TF,以及逻辑回归。
tokenizer = Tokenizer(inputCol="text", outputCol="words")
hashingTF = HashingTF(inputCol=tokenizer.getOutputCol(), outputCol="features")
lr = LogisticRegression(maxIter=10)
pipeline = Pipeline(stages=[tokenizer, hashingTF, lr])
# 现在我们将管道视为一个估算器,包装在一个CrossValidator实例中。
# 这将使我们能够共同选择所有管道阶段的参数。
# CrossValidator需要一个估算器、一组估算器参数映射和一个评估器。
# 我们使用ParamGridBuilder来构建一个参数网格进行搜索。
# 对于hashingTF.numFeatures有3个值,lr.regParam有2个值,
# 这个网格将有3 x 2 = 6个参数设置供CrossValidator选择。
paramGrid = ParamGridBuilder() \
    .addGrid(hashingTF.numFeatures, [10, 100, 1000]) \
    .addGrid(lr.regParam, [0.1, 0.01]) \
    .build()
crossval = CrossValidator(estimator=pipeline,
estimatorParamMaps=paramGrid,
evaluator=BinaryClassificationEvaluator(),
numFolds=2) # 在实践中使用3个以上的折

# 运行交叉验证,并选择最佳参数集。
cvModel = crossval.fit(training)
# 准备测试文档,这些文档是没有标签的。
test = spark.createDataFrame([
(4, "spark i j k"),
(5, "l m n"),
(6, "mapreduce spark"),
(7, "apache hadoop")
], ["id", "text"])
# 对测试文档进行预测。cvModel使用找到的最佳模型(lrModel)。
prediction = cvModel.transform(test)
selected = prediction.select("id", "text", "probability", "prediction")
for row in selected.collect():
print(row)
Find full example code at "examples/src/main/python/ml/cross_validator.py" in the Spark repo.

有关API的详细信息,请参阅 CrossValidator Scala文档

import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.feature.{HashingTF, Tokenizer}
import org.apache.spark.ml.linalg.Vector
import org.apache.spark.ml.tuning.{CrossValidator, ParamGridBuilder}
import org.apache.spark.sql.Row
// 从 (id, text, label) 元组列表准备训练数据。
val training = spark.createDataFrame(Seq(
(0L, "a b c d e spark", 1.0),
(1L, "b d", 0.0),
(2L, "spark f g h", 1.0),
(3L, "hadoop mapreduce", 0.0),
(4L, "b spark who", 1.0),
(5L, "g d a y", 0.0),
(6L, "spark fly", 1.0),
(7L, "was mapreduce", 0.0),
(8L, "e spark program", 1.0),
(9L, "a e c l", 0.0),
(10L, "spark compile", 1.0),
(11L, "hadoop software", 0.0)
)).toDF("id", "text", "label")
// 配置一个 ML 管道,它由三个阶段组成:分词器、hashingTF 和 lr。
val tokenizer = new Tokenizer()
.setInputCol("text")
.setOutputCol("words")
val hashingTF = new HashingTF()
.setInputCol(tokenizer.getOutputCol)
.setOutputCol("features")
val lr = new LogisticRegression()
.setMaxIter(10)
val pipeline = new Pipeline()
.setStages(Array(tokenizer, hashingTF, lr))
// 我们使用 ParamGridBuilder 构建一个参数网格进行搜索。
// 对于 hashingTF.numFeatures 的 3 个值和 lr.regParam 的 2 个值,
// 这个网格将有 3 x 2 = 6 个参数设置供 CrossValidator 选择。
val paramGrid = new ParamGridBuilder()
.addGrid(hashingTF.numFeatures, Array(10, 100, 1000))
.addGrid(lr.regParam, Array(0.1, 0.01))
.build()
// 现在我们将 Pipeline 视为估计器,将其封装在一个 CrossValidator 实例中。
// 这将使我们能够联合作出所有 Pipeline 阶段的参数选择。
// CrossValidator 需要一个估计器、一组估计器参数映射,以及一个评估器。
// 请注意,这里的评估器是 BinaryClassificationEvaluator,默认度量为
// areaUnderROC。
val cv = new CrossValidator()
.setEstimator(pipeline)
.setEvaluator(new BinaryClassificationEvaluator)
.setEstimatorParamMaps(paramGrid)
.setNumFolds(2) // 实际上使用 3 以上
.setParallelism(2) // 在并行中评估最多 2 个参数设置
// 运行交叉验证,并选择最佳参数集。
val cvModel = cv.fit(training)
// 准备测试文档,这些文档是未标记的 (id, text) 元组。
val test = spark.createDataFrame(Seq(
(4L, "spark i j k"),
(5L, "l m n"),
(6L, "mapreduce spark"),
(7L, "apache hadoop")
)).toDF("id", "text")
// 对测试文档进行预测。cvModel 使用找到的最佳模型 (lrModel)。
cvModel.transform(test)
.select("id", "text", "probability", "prediction")
.collect()
.foreach { case Row(id: Long, text: String, prob: Vector, prediction: Double) =>
println(s"($id, $text) --> prob=$prob, prediction=$prediction")
}
Find full example code at "examples/src/main/scala/org/apache/spark/examples/ml/ModelSelectionViaCrossValidationExample.scala" in the Spark repo.

请参阅 CrossValidator Java文档 以获取有关API的详细信息。

import java.util.Arrays;
import org.apache.spark.ml.Pipeline;
import org.apache.spark.ml.PipelineStage;
import org.apache.spark.ml.classification.LogisticRegression;
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator;
import org.apache.spark.ml.feature.HashingTF;
import org.apache.spark.ml.feature.Tokenizer;
import org.apache.spark.ml.param.ParamMap;
import org.apache.spark.ml.tuning.CrossValidator;
import org.apache.spark.ml.tuning.CrossValidatorModel;
import org.apache.spark.ml.tuning.ParamGridBuilder;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
// 准备标记的训练文档。
Dataset<Row> training = spark.createDataFrame(Arrays.asList(
new JavaLabeledDocument(0L, "a b c d e spark", 1.0),
new JavaLabeledDocument(1L, "b d", 0.0),
new JavaLabeledDocument(2L, "spark f g h", 1.0),
new JavaLabeledDocument(3L, "hadoop mapreduce", 0.0),
new JavaLabeledDocument(4L, "b spark who", 1.0),
new JavaLabeledDocument(5L, "g d a y", 0.0),
new JavaLabeledDocument(6L, "spark fly", 1.0),
new JavaLabeledDocument(7L, "was mapreduce", 0.0),
new JavaLabeledDocument(8L, "e spark program", 1.0),
new JavaLabeledDocument(9L, "a e c l", 0.0),
new JavaLabeledDocument(10L, "spark compile", 1.0),
new JavaLabeledDocument(11L, "hadoop software", 0.0)
), JavaLabeledDocument.class);
// 配置一个包含三个阶段(分词器、哈希TF和lr)的机器学习管道。
Tokenizer tokenizer = new Tokenizer()
.setInputCol("text")
.setOutputCol("words");
HashingTF hashingTF = new HashingTF()
.setNumFeatures(1000)
.setInputCol(tokenizer.getOutputCol())
.setOutputCol("features");
LogisticRegression lr = new LogisticRegression()
.setMaxIter(10)
.setRegParam(0.01);
Pipeline pipeline = new Pipeline()
.setStages(new PipelineStage[] {tokenizer, hashingTF, lr});
// 我们使用ParamGridBuilder构建参数搜索网格。
// 对于hashingTF.numFeatures有3个值,lr.regParam有2个值,
// 这个网格将会有3 x 2 = 6个参数设置供CrossValidator选择。
ParamMap[] paramGrid = new ParamGridBuilder()
.addGrid(hashingTF.numFeatures(), new int[] {10, 100, 1000})
.addGrid(lr.regParam(), new double[] {0.1, 0.01})
.build();
// 现在我们将Pipeline视为Estimator,将其封装在CrossValidator实例中。
// 这将允许我们同时选择所有Pipeline阶段的参数。
// CrossValidator需要一个Estimator、一组Estimator ParamMaps和一个Evaluator。
// 请注意,这里的评估器是BinaryClassificationEvaluator,默认指标
// 是areaUnderROC。
CrossValidator cv = new CrossValidator()
.setEstimator(pipeline)
.setEvaluator(new BinaryClassificationEvaluator())
.setEstimatorParamMaps(paramGrid)
.setNumFolds(2) // 实际应用中使用3+ 
.setParallelism(2); // 并行评估最多2个参数设置
// 运行交叉验证,选择最佳参数集。
CrossValidatorModel cvModel = cv.fit(training);
// 准备无标签的测试文档。
Dataset<Row> test = spark.createDataFrame(Arrays.asList(
new JavaDocument(4L, "spark i j k"),
new JavaDocument(5L, "l m n"),
new JavaDocument(6L, "mapreduce spark"),
new JavaDocument(7L, "apache hadoop")
), JavaDocument.class);
// 对测试文档进行预测。cvModel使用找到的最佳模型(lrModel)。
Dataset<Row> predictions = cvModel.transform(test);
for (Row r : predictions.select("id", "text", "probability", "prediction").collectAsList()) {
System.out.println("(" + r.get(0) + ", " + r.get(1) + ") --> prob=" + r.get(2)
+ ", prediction=" + r.get(3));
}
Find full example code at "examples/src/main/java/org/apache/spark/examples/ml/JavaModelSelectionViaCrossValidationExample.java" in the Spark repo.

训练-验证划分

除了 CrossValidator ,Spark 还提供了 TrainValidationSplit 用于超参数调优。 TrainValidationSplit 只评估每种参数组合一次,而不是在 CrossValidator 的情况下评估 k 次。因此,它的开销较小,但在训练数据集不够大的情况下,结果可能不够可靠。

CrossValidator 不同, TrainValidationSplit 创建一个单一的 (训练, 测试) 数据集对。它使用 trainRatio 参数将数据集拆分为这两个部分。例如,使用 $trainRatio=0.75$ TrainValidationSplit 将生成一个训练和测试数据集对,其中 75% 的数据用于训练,25% 用于验证。

CrossValidator 类似, TrainValidationSplit 最终使用最佳 ParamMap 和整个数据集来拟合 Estimator

示例:通过训练验证拆分进行模型选择

有关API的更多详细信息,请参阅 TrainValidationSplit Python文档

from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.regression import LinearRegression
from pyspark.ml.tuning import ParamGridBuilder, TrainValidationSplit
# 准备训练和测试数据。
data = spark.read.format("libsvm")\
    .load("data/mllib/sample_linear_regression_data.txt")
train, test = data.randomSplit([0.9, 0.1], seed=12345)
lr = LinearRegression(maxIter=10)
# 我们使用 ParamGridBuilder 来构建一个参数网格进行搜索。
# TrainValidationSplit 会尝试所有参数值的组合,并使用评估器确定最佳模型。
paramGrid = ParamGridBuilder()\
    .addGrid(lr.regParam, [0.1, 0.01]) \
    .addGrid(lr.fitIntercept, [False, True])\
    .addGrid(lr.elasticNetParam, [0.0, 0.5, 1.0])\
    .build()
# 在这个例子中,估算器简单地是线性回归。
# TrainValidationSplit 需要一个估算器,一组估算器参数映射,以及一个评估器。
tvs = TrainValidationSplit(estimator=lr,
estimatorParamMaps=paramGrid,
evaluator=RegressionEvaluator(),
# 80% 的数据将用于训练,20% 用于验证。
 trainRatio=0.8)
# 运行 TrainValidationSplit,并选择最佳参数集。
model = tvs.fit(train)
# 在测试数据上进行预测。模型是表现最佳的参数组合所得到的模型。
model.transform(test)\
    .select("features", "label", "prediction")\
    .show()
Find full example code at "examples/src/main/python/ml/train_validation_split.py" in the Spark repo.

请参阅 TrainValidationSplit 的Scala文档 获取API的详细信息。

import org.apache.spark.ml.evaluation.RegressionEvaluator
import org.apache.spark.ml.regression.LinearRegression
import org.apache.spark.ml.tuning.{ParamGridBuilder, TrainValidationSplit}
// 准备训练和测试数据。
val data = spark.read.format("libsvm").load("data/mllib/sample_linear_regression_data.txt")
val Array(training, test) = data.randomSplit(Array(0.9, 0.1), seed = 12345)
val lr = new LinearRegression()
.setMaxIter(10)
// 我们使用 ParamGridBuilder 来构造一个参数网格以供搜索。
// TrainValidationSplit 将尝试所有参数值的组合,并使用评估器确定最佳模型。
val paramGrid = new ParamGridBuilder()
.addGrid(lr.regParam, Array(0.1, 0.01))
.addGrid(lr.fitIntercept)
.addGrid(lr.elasticNetParam, Array(0.0, 0.5, 1.0))
.build()
// 在这种情况下,估计器就是线性回归。
//  TrainValidationSplit 需要一个估计器、一组估计器参数映射和一个评估器。
val trainValidationSplit = new TrainValidationSplit()
.setEstimator(lr)
.setEvaluator(new RegressionEvaluator)
.setEstimatorParamMaps(paramGrid)
// 80% 的数据将用于训练,剩下的 20% 用于验证。
.setTrainRatio(0.8)
// 在并行中评估最多 2 种参数设置
.setParallelism(2)
// 运行训练验证分割,并选择最佳参数集。
val model = trainValidationSplit.fit(training)
// 在测试数据上进行预测。model 是具有最佳参数组合的模型。
model.transform(test)
.select("features", "label", "prediction")
.show()
Find full example code at "examples/src/main/scala/org/apache/spark/examples/ml/ModelSelectionViaTrainValidationSplitExample.scala" in the Spark repo.

有关API的详细信息,请参阅 TrainValidationSplit Java文档

import org.apache.spark.ml.evaluation.RegressionEvaluator;
import org.apache.spark.ml.param.ParamMap;
import org.apache.spark.ml.regression.LinearRegression;
import org.apache.spark.ml.tuning.ParamGridBuilder;
import org.apache.spark.ml.tuning.TrainValidationSplit;
import org.apache.spark.ml.tuning.TrainValidationSplitModel;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
Dataset<Row> data = spark.read().format("libsvm")
.load("data/mllib/sample_linear_regression_data.txt");
// 准备训练和测试数据。
Dataset<Row>[] splits = data.randomSplit(new double[] {0.9, 0.1}, 12345);
Dataset<Row> training = splits[0];
Dataset<Row> test = splits[1];
LinearRegression lr = new LinearRegression();
// 我们使用 ParamGridBuilder 来构建一个参数网格以进行搜索。
// TrainValidationSplit 将尝试所有值的组合,并使用
// 评估器确定最佳模型。
ParamMap[] paramGrid = new ParamGridBuilder()
.addGrid(lr.regParam(), new double[] {0.1, 0.01})
.addGrid(lr.fitIntercept())
.addGrid(lr.elasticNetParam(), new double[] {0.0, 0.5, 1.0})
.build();
// 在这种情况下,估计器就是线性回归。
// TrainValidationSplit 需要一个估计器、一组估计器参数映射和一个评估器。
TrainValidationSplit trainValidationSplit = new TrainValidationSplit()
.setEstimator(lr)
.setEvaluator(new RegressionEvaluator())
.setEstimatorParamMaps(paramGrid)
.setTrainRatio(0.8) // 80% 用于训练,其余 20% 用于验证
.setParallelism(2); // 并行评估最多 2 个参数设置 
// 运行训练验证分割,并选择最佳参数集。
TrainValidationSplitModel model = trainValidationSplit.fit(training);
// 在测试数据上进行预测。 model 是具有最佳性能的参数组合的模型。
model.transform(test)
.select("features", "label", "prediction")
.show();
Find full example code at "examples/src/main/java/org/apache/spark/examples/ml/JavaModelSelectionViaTrainValidationSplitExample.java" in the Spark repo.