Spark MLlibでHyperParameterチューニングを行う方法のメモです。

以下の公式ガイドに書いてあるとおりなのですが、
spark-shellで、順に動かしながら見ていこうと思います。

ML Tuning: model selection and hyperparameter tuning | spark.apache.org
https://spark.apache.org/docs/latest/ml-tuning.html

基本的な用語

まず、基本的な用語を整理しておきます。

  • Estimator ... チューニング対象のモデルの学習器
  • Evaluator ... 評価指標の計算器
  • ParameterGrid ... HyperParameterの候補値セット

Estimatorは、チューニング対象モデルの学習器。
fitメソッドでModelを返却するクラスです。
ML Pipelineを対象にしても問題ありません。
このクラスに与えるHyperParameterの最適値を探すことが目的になります。
例: LogisticRegression, LinearRegression DecisionTreeClassifier など

Evaluatorは、評価指標の計算機です。
evaluateメソッドに予測結果を与えると、指標の値を返却するクラスです。
分類・回帰など、予測モデルにあわせたEvaluatorを利用します。
評価指標は、AUC(ROC曲線下部面積)などになります。
例: RegressionEvaluator, BinaryClassificationEvaluator, MulticlassClassificationEvaluator

ParameterGridは、Estimatorに与えるHyperParameterの候補値セットです。
HyperParameterが複数ある場合は、それらの組み合わせ分の検証を実施します。
例えば、パラメータAに2候補、パラメータBに3候補であれば、2*3で6パターン検証を実施。

これらの情報を、
TrainValidationSplitまたはCrossValidatorに与えて、
実行すると最適なHyperParameterを探すことが出来ます。

TrainValidationSplitはテストデータを教師・検証データに分割して評価を実施。
CrossValidatorは、テストデータをk分割して、それぞれを検証データとしてk回評価を実施。

spark-shellで実行してみる

それでは実際に、HyperParameterを実行してみます。

sample_libsvm_data.txtをテストデータとして、
LogisticRegressionを対象に、TrainValidationSplitで評価します。

テストデータの読み込み

spark-shellを起動します。

$ spark-shell

テストデータを読み込みます。

scala> val testdata = spark.read.
   format("libsvm").load("file://※sparkのインストール先※/data/mllib/sample_libsvm_data.txt")
scala> testdata.show()
+-----+--------------------+
|label|            features|
+-----+--------------------+
|  0.0|(692,[127,128,129...|
|  1.0|(692,[158,159,160...|
|  1.0|(692,[124,125,126...|
|  1.0|(692,[152,153,154...|
※以降省略※

ロジスティック回帰のEstimator準備

ロジスティック回帰のEstimatorを準備します。

scala> import org.apache.spark.ml.classification.LogisticRegression
scala> val lr = new LogisticRegression().setMaxIter(10)

ParameterGridの作成

ParameterGridを作成します。
ここでは、
regParamの候補値を、1.0, 0.5, 0.3, 0.1
elasticNetParamの候補値を、1.0, 0.5, 0.1
と置いています。

scala> import org.apache.spark.ml.tuning.ParamGridBuilder
scala> val paramGrid = new ParamGridBuilder().
    addGrid(lr.regParam, Array(1.0, 0.5, 0.3, 0.1)).
    addGrid(lr.elasticNetParam, Array(1.0, 0.5, 0.1)).
    build()

以下のように、4*3=12の検証パターンが出来ていることを確認出来ます。

scala> paramGrid.length
res1: Int = 12

BinaryClassificationEvaluatorの準備

2値分類問題の評価指標を利用するので、
BinaryClassificationEvaluatorを準備します。

scala> import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
scala> val bce = new BinaryClassificationEvaluator

評価の実行

Estimator, Evaluator, ParameterGridが準備出来たので、評価を実行します

scala> import org.apache.spark.ml.tuning.TrainValidationSplit
scala> val tv = new TrainValidationSplit().
    setEstimator(lr).
    setEvaluator(bce).
    setEstimatorParamMaps(paramGrid)
scala> val tvModel = tv.fit(testdata)

ParameterGrid分のパターンが実行されるので、しばらく時間がかかります。

結果の確認

評価結果モデルのbestModelに、
評価指標が最大となったケースの予測モデルが入っています。

scala> tvModel.bestModel
res2: org.apache.spark.ml.Model[_] = LogisticRegressionModel: uid = logreg_2cb4113ea1e3, numClasses = 2, numFeatures = 692

bestModelのパラメータは以下のようにして確認出来ます。

scala> tvModel.bestModel.extractParamMap()
res3: org.apache.spark.ml.param.ParamMap =
{
    logreg_2cb4113ea1e3-aggregationDepth: 2,
    logreg_2cb4113ea1e3-elasticNetParam: 1.0,
    logreg_2cb4113ea1e3-family: auto,
    logreg_2cb4113ea1e3-featuresCol: features,
    logreg_2cb4113ea1e3-fitIntercept: true,
    logreg_2cb4113ea1e3-labelCol: label,
    logreg_2cb4113ea1e3-maxIter: 10,
    logreg_2cb4113ea1e3-predictionCol: prediction,
    logreg_2cb4113ea1e3-probabilityCol: probability,
    logreg_2cb4113ea1e3-rawPredictionCol: rawPrediction,
    logreg_2cb4113ea1e3-regParam: 0.3,
    logreg_2cb4113ea1e3-standardization: true,
    logreg_2cb4113ea1e3-threshold: 0.5,
    logreg_2cb4113ea1e3-tol: 1.0E-6
}

全ての検証パターン(候補値と評価指標の組)を知りたい場合は、
以下のように確認出来ます。

scala> tvModel.validationMetrics.zip(paramGrid).foreach(println)
(0.5,{
    logreg_2cb4113ea1e3-elasticNetParam: 1.0,
    logreg_2cb4113ea1e3-regParam: 1.0
})
(0.5,{
    logreg_2cb4113ea1e3-elasticNetParam: 1.0,
    logreg_2cb4113ea1e3-regParam: 0.5
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 1.0,
    logreg_2cb4113ea1e3-regParam: 0.3
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 1.0,
    logreg_2cb4113ea1e3-regParam: 0.1
})
(0.5,{
    logreg_2cb4113ea1e3-elasticNetParam: 0.5,
    logreg_2cb4113ea1e3-regParam: 1.0
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 0.5,
    logreg_2cb4113ea1e3-regParam: 0.5
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 0.5,
    logreg_2cb4113ea1e3-regParam: 0.3
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 0.5,
    logreg_2cb4113ea1e3-regParam: 0.1
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 0.1,
    logreg_2cb4113ea1e3-regParam: 1.0
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 0.1,
    logreg_2cb4113ea1e3-regParam: 0.5
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 0.1,
    logreg_2cb4113ea1e3-regParam: 0.3
})
(1.0,{
    logreg_2cb4113ea1e3-elasticNetParam: 0.1,
    logreg_2cb4113ea1e3-regParam: 0.1
})

ここまでの手順で、
Spark MLlibのHyperParameterチューニングの基本的な流れが確認出来ました。

その他のこと

MLPipelineを利用すると、
EstimatorをまたぐHyperParamterの組み合わせを評価することが出来ます。

また、評価指標をカスタマイズしたい場合は、
org.apache.spark.ml.evaluation.Evaluatorをextendsして、
evaluateメソッドを実装します。

以上。