-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-12664][ML] Expose probability in mlp model #17373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
5beee7b
01987e9
645fdc4
bcb44af
df7439e
eedc647
7704930
5369b08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -361,9 +361,15 @@ private[ann] trait TopologyModel extends Serializable { | |
| * Forward propagation | ||
| * | ||
| * @param data input data | ||
| * @param includeLastLayer include last layer when computing. In MultilayerPerceptronClassifier, | ||
| * The last layer is always softmax, add the includeLastLayer parameter, | ||
| * when true the forward computing will contains last layer, otherwise | ||
| * not. The parameter is used when we need rawPrediction, the last layer | ||
| * softmax should discard. | ||
| * | ||
| * @return array of outputs for each of the layers | ||
| */ | ||
| def forward(data: BDM[Double]): Array[BDM[Double]] | ||
| def forward(data: BDM[Double], includeLastLayer: Boolean): Array[BDM[Double]] | ||
|
|
||
| /** | ||
| * Prediction of the model | ||
|
|
@@ -373,6 +379,22 @@ private[ann] trait TopologyModel extends Serializable { | |
| */ | ||
| def predict(data: Vector): Vector | ||
|
|
||
| /** | ||
| * Raw prediction of the model | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This documentation does not add any information. Can you please link to ProbabilisticClassifier instead? |
||
| * | ||
| * @param data input data | ||
| * @return raw prediction | ||
| */ | ||
| def predictRaw(data: Vector): Vector | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please match the Classifier API here (data -> features) unless there's a reason to deviate
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ping: rename data -> features |
||
|
|
||
| /** | ||
| * Probability of the model | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This documentation does not add any information. Can you please link to ProbabilisticClassifier instead? |
||
| * | ||
| * @param data input data | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "input data" sounds like input feature values, which is not correct. Why not match the ProbabilisticClassifier API? |
||
| * @return probability | ||
| */ | ||
| def raw2ProbabilityInPlace(data: Vector): Vector | ||
|
|
||
| /** | ||
| * Computes gradient for the network | ||
| * | ||
|
|
@@ -463,7 +485,7 @@ private[ml] class FeedForwardModel private( | |
| private var outputs: Array[BDM[Double]] = null | ||
| private var deltas: Array[BDM[Double]] = null | ||
|
|
||
| override def forward(data: BDM[Double]): Array[BDM[Double]] = { | ||
| override def forward(data: BDM[Double], includeLastLayer: Boolean): Array[BDM[Double]] = { | ||
| // Initialize output arrays for all layers. Special treatment for InPlace | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The last layer is always
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add the above comment in the code, it could be useful for folks reading/editing this in the future. Also it seems like the last layer could also be a SigmoidLayerWithSqueredError or a SigmiodFunction do we need to hand those cases any differently?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @MrBago In So MultiLayerPerceptronClassifier always use softmax as the last layer. |
||
| val currentBatchSize = data.cols | ||
| // TODO: allocate outputs as one big array and then create BDMs from it | ||
|
|
@@ -481,7 +503,8 @@ private[ml] class FeedForwardModel private( | |
| } | ||
| } | ||
| layerModels(0).eval(data, outputs(0)) | ||
| for (i <- 1 until layerModels.length) { | ||
| val end = if (includeLastLayer) layerModels.length else layerModels.length - 1 | ||
| for (i <- 1 until end) { | ||
| layerModels(i).eval(outputs(i - 1), outputs(i)) | ||
| } | ||
| outputs | ||
|
|
@@ -492,7 +515,7 @@ private[ml] class FeedForwardModel private( | |
| target: BDM[Double], | ||
| cumGradient: Vector, | ||
| realBatchSize: Int): Double = { | ||
| val outputs = forward(data) | ||
| val outputs = forward(data, true) | ||
| val currentBatchSize = data.cols | ||
| // TODO: allocate deltas as one big array and then create BDMs from it | ||
| if (deltas == null || deltas(0).cols != currentBatchSize) { | ||
|
|
@@ -527,9 +550,21 @@ private[ml] class FeedForwardModel private( | |
|
|
||
| override def predict(data: Vector): Vector = { | ||
| val size = data.size | ||
| val result = forward(new BDM[Double](size, 1, data.toArray)) | ||
| val result = forward(new BDM[Double](size, 1, data.toArray), true) | ||
| Vectors.dense(result.last.toArray) | ||
| } | ||
|
|
||
| override def predictRaw(data: Vector): Vector = { | ||
| val size = data.size | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This temp val of "size" is only used once, so I recommend removing it to make the code clearer. |
||
| val result = forward(new BDM[Double](size, 1, data.toArray), false) | ||
| Vectors.dense(result(result.length - 2).toArray) | ||
| } | ||
|
|
||
| override def raw2ProbabilityInPlace(data: Vector): Vector = { | ||
| val dataMatrix = new BDM[Double](data.size, 1, data.toArray) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add directly call the last layer function to compute it. |
||
| layerModels.last.eval(dataMatrix, dataMatrix) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assumes that the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ping: If this proposal sounds good, then can you please update accordingly? |
||
| data | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ import org.apache.spark.mllib.linalg.{Vectors => OldVectors} | |
| import org.apache.spark.mllib.regression.{LabeledPoint => OldLabeledPoint} | ||
| import org.apache.spark.mllib.util.MLlibTestSparkContext | ||
| import org.apache.spark.sql.{Dataset, Row} | ||
| import org.apache.spark.sql.functions._ | ||
|
|
||
| class MultilayerPerceptronClassifierSuite | ||
| extends SparkFunSuite with MLlibTestSparkContext with DefaultReadWriteTest { | ||
|
|
@@ -82,6 +83,49 @@ class MultilayerPerceptronClassifierSuite | |
| } | ||
| } | ||
|
|
||
| test("strong dataset test") { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the test title more descriptive so it is clear what it is testing. E.g. "Predicted class probabilities: calibration on toy dataset" |
||
| val layers = Array[Int](4, 5, 5, 2) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you make this test faster by using a simpler network, e.g., by removing one of the middle layers? |
||
|
|
||
| val strongDataset = Seq( | ||
| (Vectors.dense(1, 2, 3, 4), 0d, Vectors.dense(1d, 0d)), | ||
| (Vectors.dense(4, 3, 2, 1), 1d, Vectors.dense(0d, 1d)), | ||
| (Vectors.dense(1, 1, 1, 1), 0d, Vectors.dense(.5, .5)), | ||
| (Vectors.dense(1, 1, 1, 1), 1d, Vectors.dense(.5, .5)) | ||
| ).toDF("features", "label", "expectedProbability") | ||
| val trainer = new MultilayerPerceptronClassifier() | ||
| .setLayers(layers) | ||
| .setBlockSize(1) | ||
| .setSeed(123L) | ||
| .setMaxIter(100) | ||
| .setSolver("l-bfgs") | ||
| val model = trainer.fit(strongDataset) | ||
| val result = model.transform(strongDataset) | ||
| model.setProbabilityCol("probability") | ||
| MLTestingUtils.checkCopyAndUids(trainer, model) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. checkCopyAndUids is a generic test which should only be run in a single test; it does not need to be run in each test. Please remove it from here. |
||
| // result.select("probability").show(false) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The result is cc @MrBago Thanks!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove old comment |
||
| result.select("probability", "expectedProbability").collect().foreach { | ||
| case Row(p: Vector, e: Vector) => | ||
| assert(p ~== e absTol 1e-3) | ||
| } | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @MrBago it contains some values near 0.5 |
||
|
|
||
| test("test model probability") { | ||
| val layers = Array[Int](2, 5, 2) | ||
| val trainer = new MultilayerPerceptronClassifier() | ||
| .setLayers(layers) | ||
| .setBlockSize(1) | ||
| .setSeed(123L) | ||
| .setMaxIter(100) | ||
| .setSolver("l-bfgs") | ||
| val model = trainer.fit(dataset) | ||
| model.setProbabilityCol("probability") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's the default already, right?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ping --- this should not be necessary |
||
| val result = model.transform(dataset) | ||
| val features2prob = udf { features: Vector => model.mlpModel.predict(features) } | ||
| val cmpVec = udf { (v1: Vector, v2: Vector) => v1 ~== v2 relTol 1e-3 } | ||
| assert(result.select(cmpVec(features2prob(col("features")), col("probability"))) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this test fails, it will not give much info. How about collecting the data and comparing on the driver? |
||
| .rdd.map(_.getBoolean(0)).reduce(_ && _)) | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should include a stronger test for this. I did a quick search and couldn't find a strong test for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @MrBago |
||
| test("Test setWeights by training restart") { | ||
| val dataFrame = Seq( | ||
| (Vectors.dense(0.0, 0.0), 0.0), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This text is unclear. This phrasing is better: "Include the last layer in the output. In MultilayerPerceptronClassifier, the last layer is always softmax; the last layer of outputs is needed for class predictions, but not for rawPrediction."