Skip to content
39 changes: 38 additions & 1 deletion mllib/src/main/scala/org/apache/spark/ml/Predictor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.spark.ml

import org.apache.spark.annotation.DeveloperApi
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.ml.param._
import org.apache.spark.ml.param.shared._
import org.apache.spark.ml.util.SchemaUtils
Expand Down Expand Up @@ -176,17 +177,53 @@ abstract class PredictionModel[FeaturesType, M <: PredictionModel[FeaturesType,
override def transform(dataset: DataFrame): DataFrame = {
transformSchema(dataset.schema, logging = true)
if ($(predictionCol).nonEmpty) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could call:

transformImpl(dataset)

dataset.withColumn($(predictionCol), callUDF(predict _, DoubleType, col($(featuresCol))))
transformImpl(dataset)
} else {
this.logWarning(s"$uid: Predictor.transform() was called as NOOP" +
" since no output columns were set.")
dataset
}
}

protected def transformImpl(dataset: DataFrame): DataFrame = {
dataset.withColumn($(predictionCol), callUDF(predict _, DoubleType, col($(featuresCol))))
}

/**
* Predict label for the given features.
* This internal method is used to implement [[transform()]] and output [[predictionCol]].
*/
protected def predict(features: FeaturesType): Double
}


/**
* :: DeveloperApi ::
*
* Abstraction for a model for prediction tasks that will broadcast the model used to predict.
*
* @tparam FeaturesType Type of features.
* E.g., [[org.apache.spark.mllib.linalg.VectorUDT]] for vector features.
* @tparam M Specialization of [[PredictionModel]]. If you subclass this type, use this type
* parameter to specify the concrete type for the corresponding model.
*/
@DeveloperApi
abstract class PredictionModelBroadcasting[
FeaturesType, M <: PredictionModelBroadcasting[FeaturesType, M]
]
extends PredictionModel[FeaturesType, M] {

protected def transformImpl(dataset: DataFrame, bcastModel: Broadcast[M]): DataFrame = {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if using the Broadcast variable as a parameter is a good idea


dataset.withColumn($(predictionCol),
callUDF((features: FeaturesType) => predictWithBroadcastModel(features, bcastModel),
DoubleType, col($(featuresCol)))
)
}

/**
* Predict label for the given features using a broadcasted model.
* This internal method is used to implement [[transform()]] and output [[predictionCol]].
*/
protected def predictWithBroadcastModel(features: FeaturesType, bcastModel: Broadcast[M]): Double
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ package org.apache.spark.ml.classification
import scala.collection.mutable

import org.apache.spark.annotation.AlphaComponent
import org.apache.spark.ml.{PredictionModel, Predictor}
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.ml.{PredictionModelBroadcasting, Predictor}
import org.apache.spark.ml.param.ParamMap
import org.apache.spark.ml.tree.{RandomForestParams, TreeClassifierParams, DecisionTreeModel, TreeEnsembleModel}
import org.apache.spark.ml.util.{Identifiable, MetadataUtils}
Expand Down Expand Up @@ -109,6 +110,7 @@ object RandomForestClassifier {
RandomForestParams.supportedFeatureSubsetStrategies
}


/**
* :: AlphaComponent ::
*
Expand All @@ -122,7 +124,7 @@ object RandomForestClassifier {
final class RandomForestClassificationModel private[ml] (
override val uid: String,
private val _trees: Array[DecisionTreeClassificationModel])
extends PredictionModel[Vector, RandomForestClassificationModel]
extends PredictionModelBroadcasting[Vector, RandomForestClassificationModel]
with TreeEnsembleModel with Serializable {

require(numTrees > 0, "RandomForestClassificationModel requires at least 1 tree.")
Expand All @@ -134,13 +136,28 @@ final class RandomForestClassificationModel private[ml] (

override def treeWeights: Array[Double] = _treeWeights

override def transform(dataset: DataFrame): DataFrame = {
val bcastModel = dataset.sqlContext.sparkContext.broadcast(this)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned that we might want to selectively broadcast the model, only if it's large enough. Do you think that is something we can do here automatically, or would it need to be a configuration setting?

transformImpl(dataset, bcastModel)
}

override protected def predict(features: Vector): Double = {
// TODO: Override transform() to broadcast model. SPARK-7127
// TODO: When we add a generic Bagging class, handle transform there: SPARK-7128
// Predict without using a broadcasted mode
predictImpl(features, () => this)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still want to support prediction without broadcasting?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we will want to. If it's a small model, then it can be faster to not broadcast it.

}

override protected def predictWithBroadcastModel(features: Vector,
bcastModel: Broadcast[RandomForestClassificationModel]): Double = {
// Predict using the given broadcasted model
predictImpl(features, () => bcastModel.value)
}

protected def predictImpl(features: Vector, modelAccesor: () => TreeEnsembleModel): Double = {
// Classifies using majority votes.
// Ignore the weights since all are 1.0 for now.
val votes = mutable.Map.empty[Int, Double]
_trees.view.foreach { tree =>
modelAccesor().trees.view.foreach { tree =>
val prediction = tree.rootNode.predict(features).toInt
votes(prediction) = votes.getOrElse(prediction, 0.0) + 1.0 // 1.0 = weight
}
Expand Down