Uh oh!
There was an error while loading. Please reload this page.
[SPARK-18710][ML] Add offset in GLM - #16699
Conversation
SparkQA
commented
Jan 25, 2017
Test build #71974 has finished for PR 16699 at commit
|
SparkQA
commented
Jan 25, 2017
Test build #71975 has finished for PR 16699 at commit
|
zhengruifeng
commented
Jan 25, 2017
you should not modify sharedParams directly. And if there is no other algorithms inheriting hasoffset, I suggest that do not create this |
imatiach-msft
commented
Jan 25, 2017
can you please: [error] * abstract method getOffsetCol()java.lang.String in trait org.apache.spark.ml.param.shared.HasOffsetCol is inherited by class GeneralizedLinearRegressionBase in current version. |
SparkQA
commented
Jan 25, 2017
Test build #71994 has finished for PR 16699 at commit
|
SparkQA
commented
Jan 25, 2017
Test build #71995 has finished for PR 16699 at commit
|
actuaryzhang
commented
Jan 25, 2017
@zhengruifeng@imatiach-msft |
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.ml.feature.Instance | ||
| import org.apache.spark.ml.linalg._ | ||
| import org.apache.spark.ml.regression.GLRInstance |
There was a problem hiding this comment.
this is a bit strange - you are using the GLRInstance that you added to specifically GeneralizedLinearRegression.scala in the more generic optimizer IterativelyReweightedLeastSquares.scala. It doesn't seem right for this file to depend on anything in the regression directory, it should really be the other way around.
I'm wondering if either:
1.) We can move the GLRInstance to a more generic place
2.) OR add it to Instance.scala as a separate case class eg "OffsetInstance"
3.) OR keep the offset a separate value
| val eta = model.predict(instance.features) | ||
| val reweightFunc: (GLRInstance, WeightedLeastSquaresModel) => (Double, Double) = { | ||
| (instance: GLRInstance, model: WeightedLeastSquaresModel) => { | ||
| val eta = model.predict(instance.features) + instance.offset |
There was a problem hiding this comment.
minor suggestion - instead of doing:
eta = prediction + offset
mu = fitted(eta)
newLabel = eta - offset + stuff
maybe do:
eta = prediction
mu = fitted(eta + offset)
newLabel = eta + stuff
| val eta = BLAS.dot(features, coefficients) + intercept | ||
| familyAndLink.fitted(eta) | ||
| } else { | ||
| throw new SparkException("Must supply offset value when offset is set.") |
There was a problem hiding this comment.
maybe a better error message might be:
Must supply offset to predict when offset column is specified
| "column name. If this is not set or empty, we treat all instance offsets as 0.0") | ||
| /** @group getParam */ | ||
| def getOffsetCol: String = $(offsetCol) |
There was a problem hiding this comment.
it looks like you will need to update the validateAndTransformSchema method below to validate these parameters - eg check if the column exists? (similar to what the base class does for features/label columns)
commented
Jan 25, 2017
@actuaryzhang added a couple comments, please take a look, thanks! |
commented
Jan 25, 2017
@imatiach-msft Thanks so much for your detailed review. Incredibly helpful. I've addressed all your comments in the new commit. Major changes are highlighted below:
Let me know if there is anything else that needs improvement. |
commented
Jan 25, 2017
Test build #72003 has finished for PR 16699 at commit
|
| val model = if (familyObj == Gaussian && linkObj == Identity) { | ||
| // TODO: Make standardizeFeatures and standardizeLabel configurable. | ||
| val wlsInstances: RDD[Instance] = instances.map { instance => | ||
| Instance(instance.label - instance.offset, instance.weight, instance.features) |
There was a problem hiding this comment.
going over all OffsetInstance and converting again to Instance seems like it would be expensive and it would increase memory usage. What do you think about the alternative of making OffsetInstance inherit from Instance - but then you would have to change Instance from a case class - and then just passing Instance to the fit below? Is there anything else we could do here?
There was a problem hiding this comment.
another thing you can do, if you don't want to change the hierarchy, is to move the initialization of instances inside the if/else. Then, for weightedleastsquares, you can just create RDD[Instance], but for IRLS weighted instances.
There was a problem hiding this comment.
the more I think about this code, it looks like moving the initialization of instances inside the if/else below and creating Instance in one case and OffsetInstance in the other would save some memory/time and wouldn't force you to mess with the Instance case class.
| } | ||
| /** Converts to an [[Instance]] object by leaving out the offset. */ | ||
| private[ml] def toInstance: Instance = Instance(label, weight, features) |
There was a problem hiding this comment.
it looks like this method is only used once in a test case, might it be better to remove it?
There was a problem hiding this comment.
actually, another alternative might be to make OffsetInstance inherit from Instance (which I wrote below as well). What do you think about this idea?
There was a problem hiding this comment.
Yes, this is only used once in the current code, and I can get rid of it. But I feel other regression-type models may use offset at some point and having this method will make it easier to switch between Instance and OffsetInstance.
| private lazy val familyAndLink = new FamilyAndLink(familyObj, linkObj) | ||
| override protected def predict(features: Vector): Double = { | ||
| val eta = predictLink(features) |
There was a problem hiding this comment.
it looks like if the family = Gaussian and link = Identity you don't even need to check for offsetCol here
| val predictLinkUDF = udf { (features: Vector) => predictLink(features) } | ||
| val predictUDF = udf { (features: Vector, offset: Double) => predict(features, offset) } | ||
| val predictLinkUDF = udf { (features: Vector, offset: Double) => predictLink(features, offset) } | ||
| val off = if (!isSet(offsetCol) || $(offsetCol).isEmpty) lit(0.0) else col($(offsetCol)) |
There was a problem hiding this comment.
calling the offset column "off" was a little confusing to me, maybe we can give it a better name, like "offsetCol" or even "offset". When I looked at the code below I first thought it was some sort of flag passed to the predict function.
There was a problem hiding this comment.
also, as I mentioned above, if family = Gaussian and link = Identity we shouldn't be passing any offset, but then the user shouldn't be setting an offset column probably - so either adding that to the validation logic or to the code here or to the predict function you call below would fix this unusual case; I think this seems like a validation issue and probably should be added to the validation method.
| Vectors.dense(-1.9991044, 0.7247511, 0.1424392), | ||
| Vectors.dense(-0.27378146, 0.31599396, -0.06204946)) | ||
| import GeneralizedLinearRegression._ |
There was a problem hiding this comment.
should this be moved to the imports above?
| var idx = 0 | ||
| for (fitIntercept <- Seq(false, true)) { | ||
| for (family <- Seq("gaussian", "poisson", "gamma")) { | ||
| val trainer = new GeneralizedLinearRegression().setFamily(family) |
There was a problem hiding this comment.
it looks like the spacing is off a little here? the val trainer = ... is at the same level as the for above. I'm surprised the style checker didn't catch something like this.
commented
Jun 27, 2017
@yanboliang Thanks much for the review. The new commit includes everything you suggested except implementing |
commented
Jun 27, 2017
Test build #78727 has finished for PR 16699 at commit
|
commented
Jun 27, 2017
jenkins, retest this please |
commented
Jun 27, 2017
Test build #78729 has finished for PR 16699 at commit
|
commented
Jun 27, 2017
Not sure what this error msg means, but it seems unrelated to this PR. |
commented
Jun 28, 2017
this is a known issue in test runs currently - it's mentioned in dev@spark.apache.org, just so you know. |
commented
Jun 28, 2017
Got it. I should pay more attention to that mailing list from now on :) |
commented
Jun 28, 2017
Jenkins, test this please. |
commented
Jun 28, 2017
Test build #78812 has finished for PR 16699 at commit
|
| features: Vector) { | ||
| /** Constructs from an [[Instance]] object and offset */ | ||
| def this(instance: Instance, offset: Double = 0.0) = { |
There was a problem hiding this comment.
Remove it if it was not used anymore.
| } | ||
| /** Converts to an [[Instance]] object by leaving out the offset. */ | ||
| private[ml] def toInstance: Instance = Instance(label, weight, features) |
There was a problem hiding this comment.
Remove private[ml] since you have marked the whole class as private[ml].
| * as 0.0. The feature specified as offset has a constant coefficient of 1.0. | ||
| * @group param | ||
| */ | ||
| final val offsetCol: Param[String] = new Param[String](this, "offsetCol", "The offset " + |
| "column name. If this is not set or empty, we treat all instance offsets as 0.0") | ||
| /** @group getParam */ | ||
| def getOffsetCol: String = $(offsetCol) |
| * Default is not set, so all instances have offset 0.0. | ||
| * | ||
| * @group setParam | ||
| */ |
| /** | ||
| * Calculates the predicted value when offset is set. | ||
| */ | ||
| def predict(features: Vector, offset: Double): Double = { |
There was a problem hiding this comment.
Mark it private, since we don't support predict on single instance for all models currently.
| @@ -961,14 +1008,16 @@ class GeneralizedLinearRegressionModel private[ml] ( | |||
| } | |||
| override protected def transformImpl(dataset: Dataset[_]): DataFrame = { | |||
There was a problem hiding this comment.
I summarized all four cases for making prediction as following:
| Estimator(training data) | Transformer(prediction data) | How R predict | How Spark predict |
|---|---|---|---|
| w/ offset column | w/ offset column | use offset of prediction data | use offset of prediction data |
| w/ offset column | w/o offset column | use offset of training data | not use offset |
| w/o offset column | w/ offset column | not use offset | not use offset |
| w/o offset column | w/o offset column | not use offset | not use offset |
For case 1 and 4, there is not that controversial.
For case 2, the reason behind a different way to handle is we can't store all offset data in our model like what R does, but we should print a warning log to let users know that is different from R.
For case 3, in your current implementation, it ignores whether the model was trained with offset. I think it might be worth discussing. I think the correct way should consider whether the model was trained with offset. If the model was trained without offset, we should ignore the offset column when making prediction on new dataset. Or at least, we should print out warning to remind users.
However, I think we can discuss and resolve this issue in follow-up work. @actuaryzhang What do you think my proposal of how Spark make prediction? Thanks.
There was a problem hiding this comment.
Thanks for summarizing the different cases. I think this is worth a deeper discussion as follow-up work. Let me work on this in another PR.
commented
Jun 29, 2017
Made a new commit that fixes the issues you pointed out. |
commented
Jun 29, 2017
Test build #78920 has finished for PR 16699 at commit
|
commented
Jun 30, 2017 •
Merged into master. Thanks for the contribution and all reviews! This great feature will benefit lots of users. |
commented
Jun 30, 2017
@yanboliang@actuaryzhang this PR breaks the scala-2.10 build: |
commented
Jun 30, 2017
@hvanhovell I will send a quick fix soon, thanks for your kindly remind. |
commented
Jun 30, 2017
#18489 fixed the build failure. Thanks. |
What changes were proposed in this pull request?
Add support for offset in GLM. This is useful for at least two reasons:
How was this patch tested?
New test.
@yanboliang@srowen@felixcheung@sethah