Skip to content

Commit

Permalink
Merge pull request #386 from cmjiang/tuner
Browse files Browse the repository at this point in the history
Add hyper-parameter tuner interface
  • Loading branch information
joshvfleming committed Aug 20, 2018
2 parents 46208e2 + a9d2769 commit ee56747
Show file tree
Hide file tree
Showing 13 changed files with 357 additions and 91 deletions.
@@ -0,0 +1,63 @@
/*
* Copyright 2018 LinkedIn Corp. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linkedin.photon.ml.hyperparameter.tuner

import breeze.linalg.DenseVector

import com.linkedin.photon.ml.HyperparameterTuningMode
import com.linkedin.photon.ml.HyperparameterTuningMode.HyperparameterTuningMode
import com.linkedin.photon.ml.evaluation.Evaluator
import com.linkedin.photon.ml.hyperparameter.EvaluationFunction
import com.linkedin.photon.ml.hyperparameter.search.{GaussianProcessSearch, RandomSearch}

/**
* A hyper-parameter tuner which depends on an internal LinkedIn library.
*/
class AtlasTuner[T] extends HyperparameterTuner[T] {

/**
* Search hyper-parameters to optimize the model
*
* @param n The number of points to find
* @param dimension Numbers of hyper-parameters to be tuned
* @param mode Hyper-parameter tuning mode (random or Bayesian)
* @param evaluationFunction Function that evaluates points in the space to real values
* @param evaluator the original evaluator
* @param observations Observations made prior to searching, from this data set (not mean-centered)
* @param priorObservations Observations made prior to searching, from past data sets (mean-centered)
* @param discreteParams Map that specifies the indices of discrete parameters and their numbers of discrete values
* @return A Seq of the found results
*/
def search(
n: Int,
dimension: Int,
mode: HyperparameterTuningMode,
evaluationFunction: EvaluationFunction[T],
evaluator: Evaluator,
observations: Seq[(DenseVector[Double], Double)],
priorObservations: Seq[(DenseVector[Double], Double)] = Seq(),
discreteParams: Map[Int, Int] = Map()): Seq[T] = {

val searcher = mode match {
case HyperparameterTuningMode.BAYESIAN =>
new GaussianProcessSearch[T](dimension, evaluationFunction, evaluator)

case HyperparameterTuningMode.RANDOM =>
new RandomSearch[T](dimension, evaluationFunction)
}

searcher.findWithPriors(n, observations, priorObservations)
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2018 LinkedIn Corp. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linkedin.photon.ml.hyperparameter.tuner

import breeze.linalg.DenseVector

import com.linkedin.photon.ml.HyperparameterTuningMode.HyperparameterTuningMode
import com.linkedin.photon.ml.evaluation.Evaluator
import com.linkedin.photon.ml.hyperparameter.EvaluationFunction

/**
* A dummy hyper-parameter tuner which runs an empty operation.
*/
class DummyTuner[T] extends HyperparameterTuner[T] {

/**
* Search hyper-parameters to optimize the model
*
* @param n The number of points to find
* @param dimension Numbers of hyper-parameters to be tuned
* @param mode Hyper-parameter tuning mode (random or Bayesian)
* @param evaluationFunction Function that evaluates points in the space to real values
* @param evaluator the original evaluator
* @param observations Observations made prior to searching, from this data set (not mean-centered)
* @param priorObservations Observations made prior to searching, from past data sets (mean-centered)
* @param discreteParams Map that specifies the indices of discrete parameters and their numbers of discrete values
* @return A Seq of the found results
*/
def search(
n: Int,
dimension: Int,
mode: HyperparameterTuningMode,
evaluationFunction: EvaluationFunction[T],
evaluator: Evaluator,
observations: Seq[(DenseVector[Double], Double)],
priorObservations: Seq[(DenseVector[Double], Double)] = Seq(),
discreteParams: Map[Int, Int] = Map()): Seq[T] = Seq()
}
@@ -0,0 +1,50 @@
/*
* Copyright 2018 LinkedIn Corp. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linkedin.photon.ml.hyperparameter.tuner

import breeze.linalg.DenseVector

import com.linkedin.photon.ml.HyperparameterTuningMode.HyperparameterTuningMode
import com.linkedin.photon.ml.evaluation.Evaluator
import com.linkedin.photon.ml.hyperparameter.EvaluationFunction

/**
* Interface for hyper-parameter tuner.
*/
trait HyperparameterTuner[T] {

/**
* Search hyper-parameters to optimize the model
*
* @param n The number of points to find
* @param dimension Numbers of hyper-parameters to be tuned
* @param mode Hyper-parameter tuning mode (random or Bayesian)
* @param evaluationFunction Function that evaluates points in the space to real values
* @param evaluator the original evaluator
* @param observations Observations made prior to searching, from this data set (not mean-centered)
* @param priorObservations Observations made prior to searching, from past data sets (mean-centered)
* @param discreteParams Map that specifies the indices of discrete parameters and their numbers of discrete values
* @return A Seq of the found results
*/
def search(
n: Int,
dimension: Int,
mode: HyperparameterTuningMode,
evaluationFunction: EvaluationFunction[T],
evaluator: Evaluator,
observations: Seq[(DenseVector[Double], Double)],
priorObservations: Seq[(DenseVector[Double], Double)] = Seq(),
discreteParams: Map[Int, Int] = Map()): Seq[T]
}
@@ -0,0 +1,52 @@
/*
* Copyright 2018 LinkedIn Corp. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linkedin.photon.ml.hyperparameter.tuner

import com.linkedin.photon.ml.HyperparameterTunerName.{ATLAS, DUMMY, HyperparameterTunerName}

object HyperparameterTunerFactory {

// Use DUMMY_TUNER for photon-ml, which does an empty operation for hyper-parameter tuning
val DUMMY_TUNER = "com.linkedin.photon.ml.hyperparameter.tuner.DummyTuner"

// TODO: Move AtlasTuner into atlas-ml for the auto-tuning system migration:
// TODO: val ATLAS_TUNER = "com.linkedin.atlas.ml.hyperparameter.tuner.AtlasTuner".
// TODO: Temporarily stay in photon-ml for test purpose.
val ATLAS_TUNER = "com.linkedin.photon.ml.hyperparameter.tuner.AtlasTuner"

/**
* Factory for different packages of [[HyperparameterTuner]].
*
* @param tunerName The name of the auto-tuning package
* @return The hyper-parameter tuner
*/
def apply[T](tunerName: HyperparameterTunerName): HyperparameterTuner[T] = {

val className = tunerName match {
case DUMMY => DUMMY_TUNER
case ATLAS => ATLAS_TUNER
case other => throw new IllegalArgumentException(s"Invalid HyperparameterTuner name: ${other.toString}")
}

try {
Class.forName(className)
.newInstance
.asInstanceOf[HyperparameterTuner[T]]
} catch {
case ex: Exception =>
throw new IllegalArgumentException(s"Invalid HyperparameterTuner class: $className", ex)
}
}
}
Expand Up @@ -25,7 +25,7 @@ import org.testng.Assert._
import org.testng.annotations.{DataProvider, Test}

import com.linkedin.photon.avro.generated.BayesianLinearModelAvro
import com.linkedin.photon.ml.{DataValidationType, HyperparameterTuningMode, TaskType}
import com.linkedin.photon.ml.{DataValidationType, HyperparameterTunerName, HyperparameterTuningMode, TaskType}
import com.linkedin.photon.ml.cli.game.GameDriver
import com.linkedin.photon.ml.constants.{MathConst, StorageLevel}
import com.linkedin.photon.ml.data.{FixedEffectDataConfiguration, GameConverters, RandomEffectDataConfiguration}
Expand Down Expand Up @@ -353,48 +353,50 @@ class GameTrainingDriverIntegTest extends SparkTestUtils with GameTestUtils with

/**
* Test GAME training with a fixed effect model only and hyperparameter tuning. Note that the best model may not be
* one of the tuned models.
*/
@Test
def testHyperParameterTuning(): Unit = sparkTest("testHyperParameterTuning", useKryo = true) {

val hyperParameterTuningIter = 1
val outputDir = new Path(getTmpDir, "hyperParameterTuning")
val newArgs = mixedEffectSeriousRunArgs
.copy
.put(GameTrainingDriver.rootOutputDirectory, outputDir)
.put(GameTrainingDriver.outputMode, ModelOutputMode.TUNED)
.put(GameTrainingDriver.hyperParameterTuning, HyperparameterTuningMode.RANDOM)
.put(GameTrainingDriver.hyperParameterTuningIter, hyperParameterTuningIter)

runDriver(newArgs)

val allModelsPath = new Path(outputDir, s"${GameTrainingDriver.MODELS_DIR}")
val bestModelPath = new Path(outputDir, s"${GameTrainingDriver.BEST_MODEL_DIR}")
val fs = outputDir.getFileSystem(sc.hadoopConfiguration)

assertTrue(fs.exists(allModelsPath))
assertTrue(fs.exists(bestModelPath))

val allFixedEffectModelsPathContents = fs.listStatus(allModelsPath)
assertEquals(allFixedEffectModelsPathContents.length, hyperParameterTuningIter)
allFixedEffectModelsPathContents.forall(_.isDirectory)

val bestRMSE = evaluateModel(bestModelPath)

(0 until hyperParameterTuningIter).foreach { i =>
val modelPath = new Path(allModelsPath, s"$i")
val fixedEffectModelPath = outputModelPath(outputDir, AvroConstants.FIXED_EFFECT, fixedEffectCoordinateId, i)
assertTrue(fs.exists(fixedEffectModelPath))

randomEffectCoordinateIds.foreach { randomEffectCoordinateId =>
val randomEffectModelPath = outputModelPath(outputDir, AvroConstants.RANDOM_EFFECT, randomEffectCoordinateId, i)
assertTrue(fs.exists(randomEffectModelPath))
}

assertTrue(evaluateModel(modelPath) >= bestRMSE)
}
}
* one of the tuned models. (This test is commented out since hyper-parameter tuning is temporarily disabled in
* photon-ml. Hyper-parameter tuning is still available in LinkedIn internal library li-photon-ml.)
*/
// @Test
// def c(): Unit = sparkTest("testHyperParameterTuning", useKryo = true) {
//
// val hyperParameterTuningIter = 1
// val outputDir = new Path(getTmpDir, "hyperParameterTuning")
// val newArgs = mixedEffectSeriousRunArgs
// .copy
// .put(GameTrainingDriver.rootOutputDirectory, outputDir)
// .put(GameTrainingDriver.outputMode, ModelOutputMode.TUNED)
// .put(GameTrainingDriver.hyperParameterTunerName, HyperparameterTunerName.DUMMY)
// .put(GameTrainingDriver.hyperParameterTuning, HyperparameterTuningMode.RANDOM)
// .put(GameTrainingDriver.hyperParameterTuningIter, hyperParameterTuningIter)
//
// runDriver(newArgs)
//
// val allModelsPath = new Path(outputDir, s"${GameTrainingDriver.MODELS_DIR}")
// val bestModelPath = new Path(outputDir, s"${GameTrainingDriver.BEST_MODEL_DIR}")
// val fs = outputDir.getFileSystem(sc.hadoopConfiguration)
//
// assertTrue(fs.exists(allModelsPath))
// assertTrue(fs.exists(bestModelPath))
//
// val allFixedEffectModelsPathContents = fs.listStatus(allModelsPath)
// assertEquals(allFixedEffectModelsPathContents.length, fixedEffectRegularizationWeights.size)
// allFixedEffectModelsPathContents.forall(_.isDirectory)
//
// val bestRMSE = evaluateModel(bestModelPath)
//
// (0 until hyperParameterTuningIter).foreach { i =>
// val modelPath = new Path(allModelsPath, s"$i")
// val fixedEffectModelPath = outputModelPath(outputDir, AvroConstants.FIXED_EFFECT, fixedEffectCoordinateId, i)
// assertTrue(fs.exists(fixedEffectModelPath))
//
// randomEffectCoordinateIds.foreach { randomEffectCoordinateId =>
// val randomEffectModelPath = outputModelPath(outputDir, AvroConstants.RANDOM_EFFECT, randomEffectCoordinateId, i)
// assertTrue(fs.exists(randomEffectModelPath))
// }
//
// assertTrue(evaluateModel(modelPath) >= bestRMSE)
// }
// }

/**
* Test GAME partial retraining using a pre-trained fixed effect model.
Expand Down
Expand Up @@ -20,8 +20,9 @@ import org.apache.spark.ml.param.{Param, ParamMap, ParamValidators, Params}
import org.apache.spark.ml.linalg.{Vector => SparkMLVector}
import org.apache.spark.sql.DataFrame

import com.linkedin.photon.ml.HyperparameterTuningMode.HyperparameterTuningMode
import com.linkedin.photon.ml._
import com.linkedin.photon.ml.HyperparameterTunerName.HyperparameterTunerName
import com.linkedin.photon.ml.HyperparameterTuningMode.HyperparameterTuningMode
import com.linkedin.photon.ml.TaskType.TaskType
import com.linkedin.photon.ml.Types._
import com.linkedin.photon.ml.cli.game.GameDriver
Expand All @@ -30,7 +31,7 @@ import com.linkedin.photon.ml.data.{DataValidators, FixedEffectDataConfiguration
import com.linkedin.photon.ml.data.avro.{AvroDataReader, ModelProcessingUtils}
import com.linkedin.photon.ml.estimators.GameEstimator.GameOptimizationConfiguration
import com.linkedin.photon.ml.estimators.{GameEstimator, GameEstimatorEvaluationFunction}
import com.linkedin.photon.ml.hyperparameter.search.{GaussianProcessSearch, RandomSearch}
import com.linkedin.photon.ml.hyperparameter.tuner.HyperparameterTunerFactory
import com.linkedin.photon.ml.index.IndexMapLoader
import com.linkedin.photon.ml.io.{CoordinateConfiguration, ModelOutputMode, RandomEffectCoordinateConfiguration}
import com.linkedin.photon.ml.io.ModelOutputMode.ModelOutputMode
Expand Down Expand Up @@ -141,6 +142,11 @@ object GameTrainingDriver extends GameDriver {
"Suggested depth for tree aggregation.",
ParamValidators.gt[Int](0.0))

val hyperParameterTunerName: Param[HyperparameterTunerName] = ParamUtils.createParam[HyperparameterTunerName](
"hyper parameter tuner",
"Package name of hyper-parameter tuner."
)

val hyperParameterTuning: Param[HyperparameterTuningMode] = ParamUtils.createParam[HyperparameterTuningMode](
"hyper parameter tuning",
"Type of automatic hyper-parameter tuning to perform during training.")
Expand Down Expand Up @@ -307,6 +313,7 @@ object GameTrainingDriver extends GameDriver {
setDefault(outputMode, ModelOutputMode.BEST)
setDefault(overrideOutputDirectory, false)
setDefault(normalization, NormalizationType.NONE)
setDefault(hyperParameterTunerName, HyperparameterTunerName.DUMMY)
setDefault(hyperParameterTuning, HyperparameterTuningMode.NONE)
setDefault(computeVariance, false)
setDefault(dataValidation, DataValidationType.VALIDATE_DISABLED)
Expand Down Expand Up @@ -637,27 +644,18 @@ object GameTrainingDriver extends GameDriver {
validationData match {
case Some(testData) if getOrDefault(hyperParameterTuning) != HyperparameterTuningMode.NONE =>

// TODO: Match on this to make it clearer
val evaluator = models.head._2.get.head._1
val baseConfig = models.head._3
val dimension = baseConfig.toSeq.length

val evaluationFunction = new GameEstimatorEvaluationFunction(
estimator,
baseConfig,
trainingData,
testData)
val (_, evaluationResults, baseConfig) = models.head

val searcher = getOrDefault(hyperParameterTuning) match {
case HyperparameterTuningMode.BAYESIAN =>
new GaussianProcessSearch[GameEstimator.GameResult](dimension, evaluationFunction, evaluator)

case HyperparameterTuningMode.RANDOM =>
new RandomSearch[GameEstimator.GameResult](dimension, evaluationFunction)
}
val iteration = getOrDefault(hyperParameterTuningIter)
val dimension = baseConfig.toSeq.length
val mode = getOrDefault(hyperParameterTuning)
val evaluationFunction = new GameEstimatorEvaluationFunction(estimator, baseConfig, trainingData, testData)
val evaluator = evaluationResults.get.head._1
val observations = evaluationFunction.convertObservations(models)

searcher.findWithPriors(getOrDefault(hyperParameterTuningIter), observations, Seq())
val hyperparameterTuner = HyperparameterTunerFactory[GameEstimator.GameResult](getOrDefault(hyperParameterTunerName))

hyperparameterTuner.search(iteration, dimension, mode, evaluationFunction, evaluator, observations)

case _ => Seq()
}
Expand Down

0 comments on commit ee56747

Please sign in to comment.