机器学习调优:模型选择与超参数调优
\[
\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
。这些工具需要以下几项:
-
Estimator: 用于调优的算法或Pipeline -
一组
ParamMap:可供选择的参数,有时称为“参数网格”以进行搜索 -
Evaluator: 指标,用于衡量拟合的Model在保留的测试数据上的表现
在高层次上,这些模型选择工具的工作原理如下:
- 他们将输入数据分成单独的训练集和测试集。
-
对于每一对(训练,测试),他们迭代
ParamMap的集合:-
对于每个
ParamMap,他们使用这些参数拟合Estimator,获得拟合的Model,并使用Evaluator评估Model的性能。
-
对于每个
-
他们选择性能最好的参数集产生的
Model。
这个
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)
有关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")
}
请参阅
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));
}
训练-验证划分
除了
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()
请参阅
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()
有关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();