2024/4/12に翔泳社よりApache Spark徹底入門を出版します!
書籍のサンプルノートブックをウォークスルーしていきます。Python/Chapter10/10-7 Hyperparameter Tuning
となります。
翻訳ノートブックのリポジトリはこちら。
ノートブックはこちら
ベストなハイパーパラメータを見つけ出すために、ランダムフォレストに対してハイパーパラメータチューニングを実施しましょう!
from pyspark.ml.feature import StringIndexer, VectorAssembler
filePath = "/databricks-datasets/learning-spark-v2/sf-airbnb/sf-airbnb-clean.parquet"
airbnbDF = spark.read.parquet(filePath)
(trainDF, testDF) = airbnbDF.randomSplit([.8, .2], seed=42)
categoricalCols = [field for (field, dataType) in trainDF.dtypes if dataType == "string"]
indexOutputCols = [x + "Index" for x in categoricalCols]
stringIndexer = StringIndexer(inputCols=categoricalCols, outputCols=indexOutputCols, handleInvalid="skip")
numericCols = [field for (field, dataType) in trainDF.dtypes if ((dataType == "double") & (field != "price"))]
assemblerInputs = indexOutputCols + numericCols
vecAssembler = VectorAssembler(inputCols=assemblerInputs, outputCol="features")
ランダムフォレスト
from pyspark.ml.regression import RandomForestRegressor
from pyspark.ml import Pipeline
rf = RandomForestRegressor(labelCol="price", maxBins=40, seed=42)
pipeline = Pipeline(stages = [stringIndexer, vecAssembler, rf])
グリッドサーチ
チューニング可能なハイパーパラメータは多数存在し、手動で設定するには長い時間を要します。
よりシステマティックなアプローチで最適なハイパーパラメータを見つけ出すために、SparkのParamGridBuilder
を活用しましょう Python/Scala。
テストするハイパーパラメータのグリッドを定義しましょう:
- maxDepth: 決定木の最大の深さ(
2, 4, 6
の値を使用) - numTrees: 決定木の数(
10, 100
の値を使用)
from pyspark.ml.tuning import ParamGridBuilder
paramGrid = (ParamGridBuilder()
.addGrid(rf.maxDepth, [2, 4, 6])
.addGrid(rf.numTrees, [10, 100])
.build())
交差検証
最適なmaxDepthを特定するために、3フォールドの交差検証を活用します。
3フォールドの交差検証によって、データの2/3でトレーニングを行い、(ホールドアウトされた)残りの1/3で評価を行います。このプロセスを3回繰り返すので、それぞれのフォールドは検証用セットとして動作する機会があります。そして、3ラウンドの結果を平均します。
以下を伝えるために、CrossValidator
にはestimator
(パイプライン), evaluator
, estimatorParamMaps
を入力します:
- 使用するモデル
- モデルの評価方法
- モデルに設定するハイパーパラメータ
また、データを分割するフォールドの数を(3)に設定し、データが同じように分割されるようにシードも設定します Python/Scala。
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.tuning import CrossValidator
evaluator = RegressionEvaluator(labelCol="price",
predictionCol="prediction",
metricName="rmse")
cv = CrossValidator(estimator=pipeline,
evaluator=evaluator,
estimatorParamMaps=paramGrid,
numFolds=3,
seed=42)
問題: この時点でいくつのモデルをトレーニングしていますか?
cvModel = cv.fit(trainDF)
Parallelismパラメーター
うーん...実行に長い時間を要しています。これは、並列ではなく直列でモデルがトレーニングされているからです!
Spark 2.3では、parallelismパラメータが導入されました。ドキュメントでは、並列アルゴリズムを実行する際のスレッド数 (>= 1)
と述べられています。
この値を4に設定し、トレーニングが早くなるかどうかを見てみましょう。
cvModel = cv.setParallelism(4).fit(trainDF)
問題: うーん...依然として時間がかかっています。交差検証器の中にパイプラインを埋め込むべきか、パイプラインに交差検証器を埋め込むべきでしょうか?
パイプラインにエスティメーターやトランスフォーマーが含まれるかに依存します。StringIndexer(エスティメーター)のようなものがパイプラインにある場合、交差検証器にパイプライン全体を埋め込むと、毎回再フィットさせなくてはなりません。
cv = CrossValidator(estimator=rf,
evaluator=evaluator,
estimatorParamMaps=paramGrid,
numFolds=3,
parallelism=4,
seed=42)
pipeline = Pipeline(stages=[stringIndexer, vecAssembler, cv])
pipelineModel = pipeline.fit(trainDF)
ベストなハイパーパラメータの設定を持つモデルを見てみましょう。
list(zip(cvModel.getEstimatorParamMaps(), cvModel.avgMetrics))
[({Param(parent='RandomForestRegressor_cee37cf57669', name='maxDepth', doc='Maximum depth of the tree. (>= 0) E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes. Must be in range [0, 30].'): 2,
Param(parent='RandomForestRegressor_cee37cf57669', name='numTrees', doc='Number of trees to train (>= 1).'): 10},
291.1822640924783),
({Param(parent='RandomForestRegressor_cee37cf57669', name='maxDepth', doc='Maximum depth of the tree. (>= 0) E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes. Must be in range [0, 30].'): 2,
Param(parent='RandomForestRegressor_cee37cf57669', name='numTrees', doc='Number of trees to train (>= 1).'): 100},
286.7714750274078),
({Param(parent='RandomForestRegressor_cee37cf57669', name='maxDepth', doc='Maximum depth of the tree. (>= 0) E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes. Must be in range [0, 30].'): 4,
Param(parent='RandomForestRegressor_cee37cf57669', name='numTrees', doc='Number of trees to train (>= 1).'): 10},
287.6963245160818),
({Param(parent='RandomForestRegressor_cee37cf57669', name='maxDepth', doc='Maximum depth of the tree. (>= 0) E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes. Must be in range [0, 30].'): 4,
Param(parent='RandomForestRegressor_cee37cf57669', name='numTrees', doc='Number of trees to train (>= 1).'): 100},
279.9927057236079),
({Param(parent='RandomForestRegressor_cee37cf57669', name='maxDepth', doc='Maximum depth of the tree. (>= 0) E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes. Must be in range [0, 30].'): 6,
Param(parent='RandomForestRegressor_cee37cf57669', name='numTrees', doc='Number of trees to train (>= 1).'): 10},
294.34810870889305),
({Param(parent='RandomForestRegressor_cee37cf57669', name='maxDepth', doc='Maximum depth of the tree. (>= 0) E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes. Must be in range [0, 30].'): 6,
Param(parent='RandomForestRegressor_cee37cf57669', name='numTrees', doc='Number of trees to train (>= 1).'): 100},
275.39862704729984)]
テストデータセットでどうなるのかを見てみましょう。
predDF = pipelineModel.transform(testDF)
regressionEvaluator = RegressionEvaluator(predictionCol="prediction", labelCol="price", metricName="rmse")
rmse = regressionEvaluator.evaluate(predDF)
r2 = regressionEvaluator.setMetricName("r2").evaluate(predDF)
print(f"RMSE is {rmse}")
print(f"R2 is {r2}")
RMSE is 211.70370310223277
R2 is 0.2265254865671944
前回のピュアな決定木の結果よりも精度が改善されました。