web-dev-qa-db-ja.com

Apacheでmlパイプラインを作成する最適な方法Spark=列数の多いデータセットの場合

私はSpark 2.1.1を〜2000機能のデータセットで使用しており、いくつかのトランスフォーマーと分類子で構成される基本的なMLパイプラインを作成しようとしています。

簡単にするために、私が使用しているパイプラインは、かなり一般的なユースケースであるVectorAssembler、StringIndexer、およびClassifierで構成されていると仮定します。

// Pipeline elements
val assmbleFeatures: VectorAssembler = new VectorAssembler()
  .setInputCols(featureColumns)
  .setOutputCol("featuresRaw")

val labelIndexer: StringIndexer = new StringIndexer()
  .setInputCol("TARGET")
  .setOutputCol("indexedLabel")

// Train a RandomForest model.
val rf: RandomForestClassifier = new RandomForestClassifier()
  .setLabelCol("indexedLabel")
  .setFeaturesCol("featuresRaw")
  .setMaxBins(30)

// add the params, unique to this classifier
val paramGrid = new ParamGridBuilder()
  .addGrid(rf.numTrees, Array(5))
  .addGrid(rf.maxDepth, Array(5))
  .build()

// Treat the Pipeline as an Estimator, to jointly choose parameters for all Pipeline stages.
val evaluator = new BinaryClassificationEvaluator()
  .setMetricName("areaUnderROC")
  .setLabelCol("indexedLabel")

パイプラインのステップがトランスフォーマーパイプライン(VectorAssembler + StringIndexer)と2番目の分類子パイプラインに分離され、不要な列が両方のパイプラインの間にドロップされた場合、トレーニングは成功します。つまり、モデルを再利用するには、トレーニング後に2つのPipelineModelを保存し、中間の前処理ステップを導入する必要があります。

// Split indexers and forest in two Pipelines.
val prePipeline = new Pipeline().setStages(Array(labelIndexer, assmbleFeatures)).fit(dfTrain)
// Transform data and drop all columns, except those needed for training 
val dfTrainT = prePipeline.transform(dfTrain)
val columnsToDrop = dfTrainT.columns.filter(col => !Array("featuresRaw", "indexedLabel").contains(col))
val dfTrainRdy = dfTrainT.drop(columnsToDrop:_*)

val mainPipeline = new Pipeline().setStages(Array(rf))

val cv = new CrossValidator()
  .setEstimator(mainPipeline)
  .setEvaluator(evaluator)
  .setEstimatorParamMaps(paramGrid)
  .setNumFolds(2)

val bestModel = cv.fit(dfTrainRdy).bestModel.asInstanceOf[PipelineModel]

(imho)よりクリーンなソリューションは、すべてのパイプラインステージを1つのパイプラインにマージすることです。

val pipeline = new Pipeline()
  .setStages(Array(labelIndexer, assmbleFeatures, rf))

val cv = new CrossValidator()
  .setEstimator(pipeline)
  .setEvaluator(evaluator)
  .setEstimatorParamMaps(paramGrid)
  .setNumFolds(2)

// This will fail! 
val bestModel = cv.fit(dfTrain).bestModel.asInstanceOf[PipelineModel]

ただし、すべてのPipelineStageを1つのPipelineに配置すると、おそらく this PRの問題が原因で次の例外が発生し、最終的に解決されます。

エラーCodeGenerator:コンパイルに失敗しました:org.codehaus.janino.JaninoRuntimeException:クラスorg.Apache.spark.sql.catalyst.expressions.GeneratedClass $ SpecificUnsafeProjectionの定数プールがJVM制限0xFFFFを超えています

この理由は、VectorAssemblerがDataFrame内のデータ量を効果的に(この例では)2倍にするためです。不要な列を削除する可能性のあるトランスフォーマーがないからです。 ( スパークパイプラインベクトルアセンブラーが他の列を削除する)を参照

例では golubデータセット で機能し、次の前処理手順が必要です。

import org.Apache.spark.sql.types.DoubleType
import org.Apache.spark.ml.classification.RandomForestClassifier
import org.Apache.spark.ml.{Pipeline, PipelineModel, PipelineStage}
import org.Apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.Apache.spark.ml.feature._
import org.Apache.spark.sql._
import org.Apache.spark.ml.tuning.{CrossValidator, ParamGridBuilder}

val df = spark.read.option("header", true).option("inferSchema", true).csv("/path/to/dataset/golub_merged.csv").drop("_c0").repartition(100)

// Those steps are necessary, otherwise training would fail either way
val colsToDrop = df.columns.take(5000)
val dfValid = df.withColumn("TARGET", df("TARGET_REAL").cast(DoubleType)).drop("TARGET_REAL").drop(colsToDrop:_*)

// Split df in train and test sets
val Array(dfTrain, dfTest) = dfValid.randomSplit(Array(0.7, 0.3))

// Feature columns are columns except "TARGET"
val featureColumns = dfTrain.columns.filter(col => col != "TARGET")

私はSparkを初めて使用するので、この問題を解決するための最良の方法は何なのかわかりません。提案しますか...

  1. 列を削除し、パイプラインに組み込むことができる新しいトランスフォーマーを作成するには?
  2. 両方のパイプラインを分割し、中間ステップを導入する
  3. 他に何か? :)

または、この問題を解決する重要な要素(パイプラインの手順、PRなど)がありませんか?


編集:

不要な列を削除する新しいTransformer DroppingVectorAssemblerを実装しましたが、同じ例外がスローされます。

それ以外に、設定spark.sql.codegen.wholeStage to falseは問題を解決しません。

42
aMKa

janinoエラーは、オプティマイザプロセス中に作成された定数変数の数が原因です。 JVMで許可される定数変数の最大制限は((2 ^ 16)-1)です。この制限を超えると、Constant pool for class ... has grown past JVM limit of 0xFFFF

この問題を修正するJIRAは SPARK-18016 ですが、現時点ではまだ進行中です。

単一の最適化タスク中に何千もの列に対して実行する必要があるVectorAssemblerステージ中に、コードが失敗する可能性が最も高くなります。

この問題に対して私が開発した回避策は、列のサブセットに対して作業し、結果を最後にまとめて特異な特徴ベクトルを作成することにより、「ベクトルのベクトル」を作成することです。これにより、単一の最適化タスクがJVM定数の制限を超えないようにします。エレガントではありませんが、1万列の範囲に到達するデータセットで使用しました。

この方法では、単一のパイプラインを維持することもできますが、機能させるにはいくつかの追加の手順が必要です(サブベクトルの作成)。サブベクトルから特徴ベクトルを作成したら、必要に応じて元のソース列を削除できます。

コード例:

// IMPORT DEPENDENCIES
import org.Apache.spark.sql.SparkSession
import org.Apache.spark.sql.functions._
import org.Apache.spark.sql.{SQLContext, Row, DataFrame, Column}
import org.Apache.spark.ml.feature.VectorAssembler
import org.Apache.spark.ml.{Pipeline, PipelineModel}

// Create first example dataframe
val exampleDF = spark.createDataFrame(Seq(
  (1, 1, 2, 3, 8, 4, 5, 1, 3, 2, 0, 4, 2, 8, 1, 1, 2, 3, 8, 4, 5),
  (2, 4, 3, 8, 7, 9, 8, 2, 3, 3, 2, 6, 5, 4, 2, 4, 3, 8, 7, 9, 8),
  (3, 6, 1, 9, 2, 3, 6, 3, 8, 5, 1, 2, 3, 5, 3, 6, 1, 9, 2, 3, 6),
  (4, 7, 8, 6, 9, 4, 5, 4, 9, 8, 2, 4, 9, 2, 4, 7, 8, 6, 9, 4, 5),
  (5, 9, 2, 7, 8, 7, 3, 5, 3, 4, 8, 0, 6, 2, 5, 9, 2, 7, 8, 7, 3),
  (6, 1, 1, 4, 2, 8, 4, 6, 3, 9, 8, 8, 9, 3, 6, 1, 1, 4, 2, 8, 4)
)).toDF("uid", "col1", "col2", "col3", "col4", "col5", 
        "col6", "col7", "col8", "col9", "colA", "colB", 
        "colC", "colD", "colE", "colF", "colG", "colH", 
        "colI", "colJ", "colK")

// Create multiple column lists using the sliding method
val Array(colList1, colList2, colList3, colList4) = exampleDF.columns.filter(_ != "uid").sliding(5,5).toArray

// Create a vector assembler for each column list
val colList1_assembler = new VectorAssembler().setInputCols(colList1).setOutputCol("colList1_vec")
val colList2_assembler = new VectorAssembler().setInputCols(colList2).setOutputCol("colList2_vec")
val colList3_assembler = new VectorAssembler().setInputCols(colList3).setOutputCol("colList3_vec")
val colList4_assembler = new VectorAssembler().setInputCols(colList4).setOutputCol("colList4_vec")

// Create a vector assembler using column list vectors as input
val features_assembler = new VectorAssembler().setInputCols(Array("colList1_vec","colList2_vec","colList3_vec","colList4_vec")).setOutputCol("features")

// Create the pipeline with column list vector assemblers first, then the final vector of vectors assembler last
val pipeline = new Pipeline().setStages(Array(colList1_assembler,colList2_assembler,colList3_assembler,colList4_assembler,features_assembler))

// Fit and transform the data
val featuresDF = pipeline.fit(exampleDF).transform(exampleDF)

// Get the number of features in "features" vector
val featureLength = (featuresDF.schema(featuresDF.schema.fieldIndex("features")).metadata.getMetadata("ml_attr").getLong("num_attrs"))

// Print number of features in "features vector"
print(featureLength)

(注:列リストを作成する方法は、実際にはプログラムで行う必要がありますが、概念を理解するために、この例は単純にしています。)

1
JamCon

発生しているjaninoエラーは、機能セットによっては、生成されるコードが大きくなるためです。

ステップを異なるパイプラインに分割し、不要な機能を削除し、StringIndexerOneHotEncoderなどの中間モデルを保存して、予測ステージ中にそれらをロードします。これは、変換が予測する必要があるデータ。

最後に、VectorAssemblerステージを実行した後、機能をfeature vectorおよびlabel列に変換し、実行する必要があるのはそれだけなので、機能列を保持する必要はありません。予測。

Scala中間ステップの保存ありのパイプラインの例-(Older spark API)

また、spark 1.6.0のような古いバージョンを使用している場合は、パッチが適用されたバージョン、つまり2.1.1または2.2.0または1.6.4を確認する必要があります。そうでない場合は、Janinoエラーは約400のフィーチャ列でも発生します。

2
Ramandeep Nanda