Skip to content
Closed
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
0ae1c0a
basic gradient boosting code from earlier branches
manishamde Sep 28, 2014
5538521
disable checkpointing for now
manishamde Sep 28, 2014
6251fd5
modified method name
manishamde Oct 1, 2014
cdceeef
added documentation
manishamde Oct 1, 2014
f1c9ef7
Merge branch 'master' into gbt
manishamde Oct 6, 2014
1a8031c
sampling with replacement
manishamde Oct 6, 2014
aa8fae7
minor refactoring
manishamde Oct 6, 2014
3973dd1
minor indicating subsample is double during comparison
manishamde Oct 6, 2014
78ed452
added newline and fixed if statement
manishamde Oct 6, 2014
4784091
formatting
manishamde Oct 7, 2014
62cc000
basic checkpointing
manishamde Oct 11, 2014
9af0231
classification attempt
manishamde Oct 11, 2014
6dd4dd8
added support for log loss
manishamde Oct 12, 2014
2fbc9c7
fixing binomial classification prediction
manishamde Oct 12, 2014
bdca43a
added timing parameters
manishamde Oct 12, 2014
f62bc48
added unpersist
manishamde Oct 12, 2014
8e10c63
modified unpersist strategy
manishamde Oct 12, 2014
3b8ffc0
added documentation
manishamde Oct 12, 2014
2cb1258
public API support
manishamde Oct 13, 2014
631baea
Merge branch 'master' into gbt
manishamde Oct 13, 2014
9155a9d
consolidated boosting configuration and added public API
manishamde Oct 13, 2014
5ab3796
minor reformatting
manishamde Oct 13, 2014
5b67102
shortened parameter list
manishamde Oct 14, 2014
1f47941
changing access modifier
manishamde Oct 14, 2014
823691b
fixing RF test
manishamde Oct 14, 2014
6a11c02
fixing formatting
manishamde Oct 20, 2014
9b2e35e
Merge branch 'master' into gbt
manishamde Oct 20, 2014
3b43896
added learning rate for prediction
manishamde Oct 20, 2014
9366b8f
minor: using numTrees instead of trees.size
manishamde Oct 20, 2014
2ae97b7
added documentation for the loss classes
manishamde Oct 20, 2014
d2c8323
Merge branch 'master' into gbt
manishamde Oct 26, 2014
9bc6e74
adding random seed as parameter
manishamde Oct 26, 2014
1b01943
add weights for base learners
manishamde Oct 26, 2014
fee06d3
added weighted ensemble model
manishamde Oct 26, 2014
d971f73
moved RF code to use WeightedEnsembleModel class
manishamde Oct 26, 2014
0e81906
improving caching unpersisting logic
manishamde Oct 26, 2014
3a18cc1
cleaned up api for conversion to bagged point and moved tests to it's…
manishamde Oct 26, 2014
781542a
added support for fractional subsampling with replacement
manishamde Oct 26, 2014
a32a5ab
added test for subsampling without replacement
manishamde Oct 26, 2014
3fd0528
moved helper methods to new class
manishamde Oct 27, 2014
eff21fe
Added gradient boosting tests
manishamde Oct 27, 2014
49ba107
merged from master
manishamde Oct 27, 2014
9f7359d
simplified gbt logic and added more tests
manishamde Oct 30, 2014
035a2ed
jkbradley formatting suggestions
manishamde Oct 30, 2014
eadbf09
parameter renaming
manishamde Oct 30, 2014
e33ab61
minor comment
manishamde Oct 30, 2014
1c40c33
add newline, removed spaces
manishamde Oct 30, 2014
0183cb9
fixed naming and formatting issues
manishamde Oct 30, 2014
8476b6b
fixing line length
manishamde Oct 30, 2014
b4c1318
removing spaces
manishamde Oct 30, 2014
ff2a796
addressing comments
manishamde Oct 31, 2014
991c7b5
public api
manishamde Oct 31, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.{RandomForest, DecisionTree, impurity}
import org.apache.spark.mllib.tree.configuration.{Algo, Strategy}
import org.apache.spark.mllib.tree.configuration.Algo._
import org.apache.spark.mllib.tree.model.{RandomForestModel, DecisionTreeModel}
import org.apache.spark.mllib.tree.model.{WeightedEnsembleModel, DecisionTreeModel}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I personally think that Boosted Model can be a separate one from RandomForestModel. E.g., it's not inconceivable to have boosted models to use RandomForestModel as its base learners.

And if this were a truly generic weighted ensemble model, then it could probably live outside of tree.model namespace, since boosting at least in theory doesn't care whether base learners are trees or not.

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.

These generalizations will rely on the new ML API (for which there will be a PR any day now); it makes sense to keep it in the tree namespace since there is not generic Estimator concept currently. But once we can, I agree it will be important to generalize meta-algorithms.

With respect to the models, I don't see how the model concepts are different. The learning algorithms are different, but that will not prevent a meta-algorithm to use another meta-algorithm as a weak learner (once the new API is available). (I think it's good to separate the concepts of Estimator (learning algorithm) and Transformer (learned model) here.) What do you think?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yea, I guess from the design perspective, it's tempting to unify these under the same umbrella.

IMO, RandomForest is mostly a specific instance of a generic ensemble model, so this makes sense.

However, I think that boosted models have some specific things about them due to their sequential nature (as opposed to parallel nature of RandomForest). E.g., if you have 1000 models, you can potentially predict based on the first 100 models whereas with RandomForest you can pick any 100. You also have to do overfitting/underfitting analyses on boosted models sequentially, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@codedeft I started with a separate model for boosting but @jkbradley (quite correctly IMO) convinced me otherwise. :-)

I agree methods like boosting require support such as early stopping, sequential selection of models, etc. but may be we can handle it as a part of the model configuration. AdaBoost and RF in some ways are more similar than AdaBoost and GBT in their combining operation. It might be better to capture all these nuances in one place. Of course, we can always split them later if we end up writing a lot of custom logic for each algorithm. Thoughts?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@manishamde Sounds good.

Just a side note. Because RF models tend to be much bigger than boosted ensembles, we've encountered situations where the model was too big to fit in a single machine memory. RandomForest model is in a way a good model for embarassingly parallel predictions so a model could potentially reside in a distributed fashion.

But we haven't yet decided whether we really want to do this (i.e. are humongous models really useful in practice and do we really expect crazy scenarios of gigantic models surpassing dozens of GBs?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@codedeft Agree about the distributed storage though I never bothered to check the size of deep trees in memory! :-) In fact, such a storage might be a good option for Partial Forest implementation.

import org.apache.spark.mllib.util.MLUtils
import org.apache.spark.rdd.RDD
import org.apache.spark.util.Utils
Expand Down Expand Up @@ -317,7 +317,7 @@ object DecisionTreeRunner {
/**
* Calculates the mean squared error for regression.
*/
private def meanSquaredError(tree: RandomForestModel, data: RDD[LabeledPoint]): Double = {
private def meanSquaredError(tree: WeightedEnsembleModel, data: RDD[LabeledPoint]): Double = {
data.map { y =>
val err = tree.predict(y.features) - y.label
err * err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class DecisionTree (private val strategy: Strategy) extends Serializable with Lo
// Note: random seed will not be used since numTrees = 1.
val rf = new RandomForest(strategy, numTrees = 1, featureSubsetStrategy = "all", seed = 0)
val rfModel = rf.train(input)
rfModel.trees(0)
rfModel.weakHypotheses(0)
}

}
Expand Down

Large diffs are not rendered by default.

49 changes: 30 additions & 19 deletions mllib/src/main/scala/org/apache/spark/mllib/tree/RandomForest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.apache.spark.api.java.JavaRDD
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.configuration.Algo._
import org.apache.spark.mllib.tree.configuration.QuantileStrategy._
import org.apache.spark.mllib.tree.configuration.EnsembleCombiningStrategy.Average
import org.apache.spark.mllib.tree.configuration.Strategy
import org.apache.spark.mllib.tree.impl.{BaggedPoint, TreePoint, DecisionTreeMetadata, TimeTracker}
import org.apache.spark.mllib.tree.impurity.Impurities
Expand Down Expand Up @@ -59,7 +60,7 @@ import org.apache.spark.util.Utils
* if numTrees == 1, set to "all";
* if numTrees > 1 (forest) set to "sqrt" for classification and
* to "onethird" for regression.
* @param seed Random seed for bootstrapping and choosing feature subsets.
* @param seed Random seed for bootstrapping and choosing feature subsets.
*/
@Experimental
private class RandomForest (
Expand All @@ -78,9 +79,9 @@ private class RandomForest (
/**
* Method to train a decision tree model over an RDD
* @param input Training data: RDD of [[org.apache.spark.mllib.regression.LabeledPoint]]
* @return RandomForestModel that can be used for prediction
* @return WeightedEnsembleModel that can be used for prediction
*/
def train(input: RDD[LabeledPoint]): RandomForestModel = {
def train(input: RDD[LabeledPoint]): WeightedEnsembleModel = {

val timer = new TimeTracker()

Expand Down Expand Up @@ -111,11 +112,20 @@ private class RandomForest (
// Bin feature values (TreePoint representation).
// Cache input RDD for speedup during multiple passes.
val treeInput = TreePoint.convertToTreeRDD(retaggedInput, bins, metadata)
val baggedInput = if (numTrees > 1) {
BaggedPoint.convertToBaggedRDD(treeInput, numTrees, seed)
} else {
BaggedPoint.convertToBaggedRDDWithoutSampling(treeInput)
}.persist(StorageLevel.MEMORY_AND_DISK)

val (subsample, withReplacement) = {
// TODO: Have a stricter check for RF in the strategy
val isRandomForest = numTrees > 1
if (isRandomForest) {
(1.0, true)
} else {
(strategy.subsamplingRate, false)
}
}

val baggedInput
= BaggedPoint.convertToBaggedRDD(treeInput, subsample, numTrees, withReplacement, seed)
.persist(StorageLevel.MEMORY_AND_DISK)

// depth of the decision tree
val maxDepth = strategy.maxDepth
Expand Down Expand Up @@ -184,7 +194,8 @@ private class RandomForest (
logInfo(s"$timer")

val trees = topNodes.map(topNode => new DecisionTreeModel(topNode, strategy.algo))
RandomForestModel.build(trees)
val treeWeights = Array.fill[Double](numTrees)(1.0)
new WeightedEnsembleModel(trees, treeWeights, strategy.algo, Average)
}

}
Expand All @@ -205,14 +216,14 @@ object RandomForest extends Serializable with Logging {
* if numTrees > 1 (forest) set to "sqrt" for classification and
* to "onethird" for regression.
* @param seed Random seed for bootstrapping and choosing feature subsets.
* @return RandomForestModel that can be used for prediction
* @return WeightedEnsembleModel that can be used for prediction
*/
def trainClassifier(
input: RDD[LabeledPoint],
strategy: Strategy,
numTrees: Int,
featureSubsetStrategy: String,
seed: Int): RandomForestModel = {
seed: Int): WeightedEnsembleModel = {
require(strategy.algo == Classification,
s"RandomForest.trainClassifier given Strategy with invalid algo: ${strategy.algo}")
val rf = new RandomForest(strategy, numTrees, featureSubsetStrategy, seed)
Expand Down Expand Up @@ -243,7 +254,7 @@ object RandomForest extends Serializable with Logging {
* @param maxBins maximum number of bins used for splitting features
* (suggested value: 100)
* @param seed Random seed for bootstrapping and choosing feature subsets.
* @return RandomForestModel that can be used for prediction
* @return WeightedEnsembleModel that can be used for prediction
*/
def trainClassifier(
input: RDD[LabeledPoint],
Expand All @@ -254,7 +265,7 @@ object RandomForest extends Serializable with Logging {
impurity: String,
maxDepth: Int,
maxBins: Int,
seed: Int = Utils.random.nextInt()): RandomForestModel = {
seed: Int = Utils.random.nextInt()): WeightedEnsembleModel = {
val impurityType = Impurities.fromString(impurity)
val strategy = new Strategy(Classification, impurityType, maxDepth,
numClassesForClassification, maxBins, Sort, categoricalFeaturesInfo)
Expand All @@ -273,7 +284,7 @@ object RandomForest extends Serializable with Logging {
impurity: String,
maxDepth: Int,
maxBins: Int,
seed: Int): RandomForestModel = {
seed: Int): WeightedEnsembleModel = {
trainClassifier(input.rdd, numClassesForClassification,
categoricalFeaturesInfo.asInstanceOf[java.util.Map[Int, Int]].asScala.toMap,
numTrees, featureSubsetStrategy, impurity, maxDepth, maxBins, seed)
Expand All @@ -293,14 +304,14 @@ object RandomForest extends Serializable with Logging {
* if numTrees > 1 (forest) set to "sqrt" for classification and
* to "onethird" for regression.
* @param seed Random seed for bootstrapping and choosing feature subsets.
* @return RandomForestModel that can be used for prediction
* @return WeightedEnsembleModel that can be used for prediction
*/
def trainRegressor(
input: RDD[LabeledPoint],
strategy: Strategy,
numTrees: Int,
featureSubsetStrategy: String,
seed: Int): RandomForestModel = {
seed: Int): WeightedEnsembleModel = {
require(strategy.algo == Regression,
s"RandomForest.trainRegressor given Strategy with invalid algo: ${strategy.algo}")
val rf = new RandomForest(strategy, numTrees, featureSubsetStrategy, seed)
Expand Down Expand Up @@ -330,7 +341,7 @@ object RandomForest extends Serializable with Logging {
* @param maxBins maximum number of bins used for splitting features
* (suggested value: 100)
* @param seed Random seed for bootstrapping and choosing feature subsets.
* @return RandomForestModel that can be used for prediction
* @return WeightedEnsembleModel that can be used for prediction
*/
def trainRegressor(
input: RDD[LabeledPoint],
Expand All @@ -340,7 +351,7 @@ object RandomForest extends Serializable with Logging {
impurity: String,
maxDepth: Int,
maxBins: Int,
seed: Int = Utils.random.nextInt()): RandomForestModel = {
seed: Int = Utils.random.nextInt()): WeightedEnsembleModel = {
val impurityType = Impurities.fromString(impurity)
val strategy = new Strategy(Regression, impurityType, maxDepth,
0, maxBins, Sort, categoricalFeaturesInfo)
Expand All @@ -358,7 +369,7 @@ object RandomForest extends Serializable with Logging {
impurity: String,
maxDepth: Int,
maxBins: Int,
seed: Int): RandomForestModel = {
seed: Int): WeightedEnsembleModel = {
trainRegressor(input.rdd,
categoricalFeaturesInfo.asInstanceOf[java.util.Map[Int, Int]].asScala.toMap,
numTrees, featureSubsetStrategy, impurity, maxDepth, maxBins, seed)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.spark.mllib.tree.configuration

import org.apache.spark.annotation.Experimental
import org.apache.spark.mllib.tree.configuration.Algo._
import org.apache.spark.mllib.tree.configuration.QuantileStrategy._
import org.apache.spark.mllib.tree.impurity.{Variance, Impurity}
import org.apache.spark.mllib.tree.loss.{SquaredError, Loss}

/**
* :: Experimental ::
* Stores all the configuration options for the boosting algorithms
* @param algo Learning goal. Supported:
* [[org.apache.spark.mllib.tree.configuration.Algo.Classification]],
* [[org.apache.spark.mllib.tree.configuration.Algo.Regression]]
* @param numEstimators Number of estimators used in boosting stages. In other words,
* number of boosting iterations performed.
* @param loss Loss function used for minimization during gradient boosting.
* @param maxDepth Maximum depth of the tree.
* E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes.
* @param learningRate Learning rate for shrinking the contribution of each estimator. The
* learning rate should be between in the interval (0, 1]
* @param subsample Fraction of the training data used for learning the decision tree.
* @param numClassesForClassification Number of classes for classification.
* (Ignored for regression.)
* Default value is 2 (binary classification).
* @param maxBins Maximum number of bins used for discretizing continuous features and
* for choosing how to split on features at each node.
* More bins give higher granularity.
* @param quantileCalculationStrategy Algorithm for calculating quantiles. Supported:
* [[org.apache.spark.mllib.tree.configuration.QuantileStrategy.Sort]]
* @param categoricalFeaturesInfo A map storing information about the categorical variables and the
* number of discrete values they take. For example, an entry (n ->
* k) implies the feature n is categorical with k categories 0,
* 1, 2, ... , k-1. It's important to note that features are
* zero-indexed.
* @param minInstancesPerNode Minimum number of instances each child must have after split.
* Default value is 1. If a split cause left or right child
* to have less than minInstancesPerNode,
* this split will not be considered as a valid split.
* @param minInfoGain Minimum information gain a split must get. Default value is 0.0.
* If a split has less information gain than minInfoGain,
* this split will not be considered as a valid split.
* @param maxMemoryInMB Maximum memory in MB allocated to histogram aggregation. Default value is
* 256 MB.
*/
@Experimental
case class BoostingStrategy(
// Required boosting parameters
algo: Algo,
numEstimators: Int,
loss: Loss,
// Required tree parameters
maxDepth: Int,
// Optional boosting parameters
learningRate: Double = 0.1,
subsample: Double = 1,
numClassesForClassification: Int = 2,
// Optional tree parametes
maxBins: Int = 32,
quantileCalculationStrategy: QuantileStrategy = Sort,
categoricalFeaturesInfo: Map[Int, Int] = Map[Int, Int](),
minInstancesPerNode: Int = 1,
minInfoGain: Double = 0.0,
maxMemoryInMB: Int = 256) extends Serializable {

// Note: Regression tree used even for classification for GBT.
val strategy = new Strategy(Regression, Variance, maxDepth, numClassesForClassification, maxBins,
quantileCalculationStrategy, categoricalFeaturesInfo, minInstancesPerNode, minInfoGain,
maxMemoryInMB, subsample)

require(learningRate <= 1, "Learning rate should be <= 1. Provided learning rate is " +
s"$learningRate.")
require(learningRate > 0, "Learning rate should be > 0. Provided learning rate is " +
s"$learningRate.")

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.spark.mllib.tree.configuration

import org.apache.spark.annotation.DeveloperApi

/**
* :: Experimental ::
* Enum to select ensemble combining strategy for base learners
*/
@DeveloperApi
object EnsembleCombiningStrategy extends Enumeration {

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.

I think this strategy option would be useful at some point, but not yet.

  • sum and average are essentially the same thing
  • Eventually, when we support options such as median, this could be nice to add
    Remove for now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why are sum and average the same? In RF you average predictions and in GBT you add predictions.

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.

You're right that they are a bit different; I was thinking in terms of thresholding for classification, but it would be important to sum, not average, for regression. I also revoke what I said about supporting things like median.
This is making me vote for removing EnsembleCombiningStrategy and only supporting sum. Do you have a use case for average? (Sorry for the confusion!)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we use average or majority for the random forest ensemble calculations. I moved the RF code to also return a WeightedEnsembleModel.

I am okay with removing EnsembleCombiningStrategy class but we still need to support both sum and averaging operations for combining ensemble predictions.

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.

OK, I agree; keeping it sounds good.

type EnsembleCombiningStrategy = Value
val Sum, Average = Value
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import org.apache.spark.mllib.tree.configuration.QuantileStrategy._
* this split will not be considered as a valid split.
* @param maxMemoryInMB Maximum memory in MB allocated to histogram aggregation. Default value is
* 256 MB.
* @param subsamplingRate Fraction of the training data used for learning decision tree.
*/
@Experimental
class Strategy (
Expand All @@ -70,7 +71,8 @@ class Strategy (
val categoricalFeaturesInfo: Map[Int, Int] = Map[Int, Int](),
val minInstancesPerNode: Int = 1,
val minInfoGain: Double = 0.0,
val maxMemoryInMB: Int = 256) extends Serializable {
val maxMemoryInMB: Int = 256,
val subsamplingRate: Double = 1) extends Serializable {

if (algo == Classification) {
require(numClassesForClassification >= 2)
Expand Down
Loading