Skip to content
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

[SPARK-19155][ML] MLlib GeneralizedLinearRegression family and link should case insensitive #16516

Closed
wants to merge 1 commit into from

Conversation

yanboliang
Copy link
Contributor

@yanboliang yanboliang commented Jan 9, 2017

What changes were proposed in this pull request?

MLlib GeneralizedLinearRegression family and link should be case insensitive. This is consistent with some other MLlib params such as featureSubsetStrategy.

How was this patch tested?

Update corresponding tests.

@yanboliang
Copy link
Contributor Author

cc @felixcheung

@SparkQA
Copy link

SparkQA commented Jan 9, 2017

Test build #71082 has finished for PR 16516 at commit f1337d8.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@imatiach-msft
Copy link
Contributor

This is a nice fix. It looks like some other learners have this issue as well, eg LogisticRegression.scala under $(root)/mllib/src/main/scala/org/apache/spark/ml/classification/LogisticRegression.scala

@imatiach-msft
Copy link
Contributor

Maybe a more generic fix would be to fix the method ParamValidators.inArray to be case insensitive. I see this method used in a lot of places. Doing a simple search brings up not just LogisticRegression.scala but also:
/mllib/src/main/scala/org/apache/spark/ml/evaluation/BinaryClassificationEvaluator.scala
/mllib/src/main/scala/org/apache/spark/ml/evaluation/MulticlassClassificationEvaluator.scala
/mllib/src/main/scala/org/apache/spark/ml/evaluation/RegressionEvaluator.scala
/mllib/src/main/scala/org/apache/spark/ml/feature/Bucketizer.scala
/mllib/src/main/scala/org/apache/spark/ml/feature/ChiSqSelector.scala
and many others as well, and it looks like they all suffer from the same bug. A more general fix would be preferred I think, especially to make all code consistent and use the same method, no? It doesn't seem like any parameter should be case-sensitive.

@felixcheung
Copy link
Member

I'd agree with that. Given that wider scope of changes I'd suggest creating another JIRA to make it clear the scope & impact - it wouldn't be just affecting SparkR.

@yanboliang yanboliang changed the title [SPARK-19133][ML] ML GLR family and link could be uppercase. [WIP][SPARK-19155][ML] ML GLR family and link could be uppercase. Jan 10, 2017
@yanboliang
Copy link
Contributor Author

@imatiach-msft @felixcheung Sounds good, I opened SPARK-19155 to track and will update this PR soon. Thanks.

@yanboliang yanboliang changed the title [WIP][SPARK-19155][ML] ML GLR family and link could be uppercase. [SPARK-19155][ML] Make some string params of ML algorithms case insensitive Jan 11, 2017
@@ -365,7 +365,7 @@ class LogisticRegression @Since("1.2.0") (
case None => histogram.length
}

val isMultinomial = $(family) match {
val isMultinomial = $(family).toLowerCase match {
Copy link
Member

Choose a reason for hiding this comment

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

is there a way to store the param as the lowered case version, instead of turning it into lower case when accessed? it might be less error prone that way?

Copy link
Contributor

Choose a reason for hiding this comment

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

It can, but I think it would need to be done in the concrete setXXX method each time.

Copy link
Contributor Author

@yanboliang yanboliang Jan 11, 2017

Choose a reason for hiding this comment

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

I don't think we can do that in setXXX methods, since they are not the only entrance to set params, we can also use the following API to set value for params:

def fit(dataset: Dataset[_], firstParamPair: ParamPair[_], otherParamPairs: ParamPair[_]*): M = {
    val map = new ParamMap()
      .put(firstParamPair)
      .put(otherParamPairs: _*)
    fit(dataset, map)
  }

Please feel free to correct me if I have misunderstand. Thanks.
cc @jkbradley @sethah

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we need to have a different accessor that is consistently used on the transform/estimator side internally to:
1.) change the value to lowercase 2.) trim any whitespace
Changing the setter might cause issues because then when users try to validate that their parameters are set correctly they will see that they are modified, which is unexpected. The case-insensitive compare should be done as in this PR, but instead of calling toLowerCase everywhere explicitly we should be accessing using some other method that normalizes the parameter internally

Copy link
Member

Choose a reason for hiding this comment

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

@yanboliang is correct that there are other entrance points for setting and getting Params. I agree it'd be nice to consolidate them, but that would be quite a bit of work and lower priority than other tech debt we currently have, IMO.

@SparkQA
Copy link

SparkQA commented Jan 11, 2017

Test build #71178 has finished for PR 16516 at commit de6994c.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

Copy link
Contributor

@imatiach-msft imatiach-msft left a comment

Choose a reason for hiding this comment

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

Is it possible to change the ParamValidators.inArray[String] method to verify the given string in a case-insensitive way? Then you wouldn't need to make as many changes.

@@ -91,8 +91,8 @@ private[classification] trait LogisticRegressionParams extends ProbabilisticClas
@Since("2.1.0")
final val family: Param[String] = new Param(this, "family",
"The name of family which is a description of the label distribution to be used in the " +
s"model. Supported options: ${supportedFamilyNames.mkString(", ")}.",
ParamValidators.inArray[String](supportedFamilyNames))
s"model (case-insensitive). Supported options: ${supportedFamilyNames.mkString(", ")}.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to change the ParamValidators.inArray[String] method to verify the given string in a case-insensitive way? Then you wouldn't need to make as many changes. (eg this change could be reverted)

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe you could add a ParamValidators.inStringArray(supportedFamilyNames)) method which would both normalize to lowercase and trim whitespace (?)

Copy link
Contributor Author

@yanboliang yanboliang Jan 12, 2017

Choose a reason for hiding this comment

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

@imatiach-msft I think we should not to change the behavior of ParamValidators.inArray[String], since some other string params may case-sensitive which use the original check.
Adding a new method sounds reasonable, but I'm a bit worried that whether we should add a so concrete method in the common validation object ParamValidators which use generic type. I'm still open on this topic and would like to hear more thoughts. Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

you're right, I searched through the code base and case-sensitivity matters when:
1.) we are specifying some column name as a parameter
2.) RModel formula (from RFormula.scala)
3.) Tokenizer.scala regex pattern
In all other cases it doesn't seem like it should matter.

Copy link
Contributor

Choose a reason for hiding this comment

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

Searching through the code base these are the places where we use Param[String]:

spark-mllib_2.11
org.apache.spark.ml.classification
LogisticRegression.scala
final val family: Param[String] = new Param(this, "family",
MultilayerPerceptronClassifier.scala
final val solver: Param[String] = new Param[String](this, "solver",
final val solver: Param[String] = new Param[String](this, "solver",
NaiveBayes.scala
final val modelType: Param[String] = new Param[String](this, "modelType", "The model type " +
final val modelType: Param[String] = new Param[String](this, "modelType", "The model type " +
org.apache.spark.ml.clustering
KMeans.scala
final val initMode = new Param[String](this, "initMode", "The initialization algorithm. " +
LDA.scala
final val optimizer = new Param[String](this, "optimizer", "Optimizer or inference" +
final val topicDistributionCol = new Param[String](this, "topicDistributionCol", "Output column" +
org.apache.spark.ml.evaluation
BinaryClassificationEvaluator.scala
val metricName: Param[String] = {
MulticlassClassificationEvaluator.scala
val metricName: Param[String] = {
RegressionEvaluator.scala
val metricName: Param[String] = {
org.apache.spark.ml.feature
Bucketizer.scala
val handleInvalid: Param[String] = new Param[String](this, "handleInvalid", "how to handle " +
val handleInvalid: Param[String] = new Param[String](this, "handleInvalid", "how to handle " +
ChiSqSelector.scala
final val selectorType = new Param[String](this, "selectorType",
QuantileDiscretizer.scala
val handleInvalid: Param[String] = new Param[String](this, "handleInvalid", "how to handle " +
val handleInvalid: Param[String] = new Param[String](this, "handleInvalid", "how to handle " +
RFormula.scala
val formula: Param[String] = new Param(this, "formula", "R model formula")
SQLTransformer.scala
final val statement: Param[String] = new Param[String](this, "statement", "SQL statement")
final val statement: Param[String] = new Param[String](this, "statement", "SQL statement")
Tokenizer.scala
val pattern: Param[String] = new Param(this, "pattern", "regex pattern used for tokenizing")
org.apache.spark.ml.param
ParamsSuite.scala
val param = new Param[String](dummy, "name", "doc")
org.apache.spark.ml.param.shared
sharedParams.scala
final val featuresCol: Param[String] = new Param[String](this, "featuresCol", "features column name")
final val featuresCol: Param[String] = new Param[String](this, "featuresCol", "features column name")
final val labelCol: Param[String] = new Param[String](this, "labelCol", "label column name")
final val labelCol: Param[String] = new Param[String](this, "labelCol", "label column name")
final val predictionCol: Param[String] = new Param[String](this, "predictionCol", "prediction column name")
final val predictionCol: Param[String] = new Param[String](this, "predictionCol", "prediction column name")
final val rawPredictionCol: Param[String] = new Param[String](this, "rawPredictionCol", "raw prediction (a.k.a. confidence) column name")
final val rawPredictionCol: Param[String] = new Param[String](this, "rawPredictionCol", "raw prediction (a.k.a. confidence) column name")
... P...
... P...
final val varianceCol: Param[String] = new Param[String](this, "varianceCol", "Column name for the biased sample variance of prediction")
final val varianceCol: Param[String] = new Param[String](this, "varianceCol", "Column name for the biased sample variance of prediction")
final val inputCol: Param[String] = new Param[String](this, "inputCol", "input column name")
final val inputCol: Param[String] = new Param[String](this, "inputCol", "input column name")
final val outputCol: Param[String] = new Param[String](this, "outputCol", "output column name")
final val outputCol: Param[String] = new Param[String](this, "outputCol", "output column name")
... P...
... P...
final val weightCol: Param[String] = new Param[String](this, "weightCol", "weight column name. If this is not set or empty, we treat all instance weights as 1.0")
final val weightCol: Param[String] = new Param[String](this, "weightCol", "weight column name. If this is not set or empty, we treat all instance weights as 1.0")
final val solver: Param[String] = new Param[String](this, "solver", "the solver algorithm for optimization. If this is not set or empty, default value is 'auto'")
final val solver: Param[String] = new Param[String](this, "solver", "the solver algorithm for optimization. If this is not set or empty, default value is 'auto'")
org.apache.spark.ml.recommendation
ALS.scala
val userCol = new Param[String](this, "userCol", "column name for user ids. Ids must be within " +
val itemCol = new Param[String](this, "itemCol", "column name for item ids. Ids must be within " +
val ratingCol = new Param[String](this, "ratingCol", "column name for ratings")
val intermediateStorageLevel = new Param[String](this, "intermediateStorageLevel",
val finalStorageLevel = new Param[String](this, "finalStorageLevel",
org.apache.spark.ml.regression
AFTSurvivalRegression.scala
final val censorCol: Param[String] = new Param(this, "censorCol", "censor column name")
final val quantilesCol: Param[String] = new Param(this, "quantilesCol", "quantiles column name")
GeneralizedLinearRegression.scala
final val family: Param[String] = new Param(this, "family",
final val link: Param[String] = new Param(this, "link", "The name of link function " +
final val linkPredictionCol: Param[String] = new Param[String](this, "linkPredictionCol",
final val linkPredictionCol: Param[String] = new Param[String](this, "linkPredictionCol",
org.apache.spark.ml.tree
treeParams.scala
final val impurity: Param[String] = new Param[String](this, "impurity", "Criterion used for" +
final val impurity: Param[String] = new Param[String](this, "impurity", "Criterion used for" +
final val impurity: Param[String] = new Param[String](this, "impurity", "Criterion used for" +
final val impurity: Param[String] = new Param[String](this, "impurity", "Criterion used for" +
final val featureSubsetStrategy: Param[String] = new Param[String](this, "featureSubsetStrategy",
final val featureSubsetStrategy: Param[String] = new Param[String](this, "featureSubsetStrategy",
val lossType: Param[String] = new Param[String](this, "lossType", "Loss function which GBT" +
val lossType: Param[String] = new Param[String](this, "lossType", "Loss function which GBT" +
val lossType: Param[String] = new Param[String](this, "lossType", "Loss function which GBT" +
val lossType: Param[String] = new Param[String](this, "lossType", "Loss function which GBT" +
org.apache.spark.ml.util
DefaultReadWriteTest.scala
final val stringParam: Param[String] = new Param[String](this, "stringParam", "doc")
final val stringParam: Param[String] = new Param[String](this, "stringParam", "doc")

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we can add an additional string param validators class then to the same params.scala file in ml folder? There should be a generic function and the params.scala file seems to be the right place.

@imatiach-msft
Copy link
Contributor

It looks like you can also update the metric name in the evaluators (binary, regression, multiclass) as well. Those should be case-insensitive too, I think.

@yanboliang
Copy link
Contributor Author

yanboliang commented Jan 13, 2017

@imatiach-msft I think not all string params should be case-insensitive, such as:

  • All column name params should not case-insensitive, like inputCol.
  • Param names which were composed by multiple words, like areaUnderROC.

Please see the PR description.

And for lots of other string params that you searched out, like impurity, are already case-insensitive.
Other string params, like SQLTransformer.statement, are not need to be updated, since they are not set with string words. The backend Spark SQL engine will handle all kinds of SQL statements.

@imatiach-msft
Copy link
Contributor

yep, I wrote that in a comment above, I totally agree:
1.) we are specifying some column name as a parameter
2.) RModel formula (from RFormula.scala)
3.) Tokenizer.scala regex pattern
for AUC I don't think it should matter though, but it's not too significant.
I still think for the check we should have one method instead of duplicating code, and same for accessing the value (instead of calling .toLower everywhere in the transform's/estimator's code).
I believe anywhere where there is duplicate code there is room for refactoring. Otherwise, the changes look good to me.

@yanboliang
Copy link
Contributor Author

I found this involves lots of problems which need further defined and refactor some code, so I will narrow the scope of this PR to only make GeneralizedLinearRegression family and link case insensitive, since it's a bug that GLM should support Gamma family.

@yanboliang yanboliang changed the title [SPARK-19155][ML] Make some string params of ML algorithms case insensitive [SPARK-19155][ML] MLlib GeneralizedLinearRegression family and link should case insensitive Jan 20, 2017
@imatiach-msft
Copy link
Contributor

Hmm ok, I guess that's fine. I'm just worried this line is duplicated, maybe you could add a method for it and put it in a common place:
(value: String) => supportedFamilyNames.contains(value.toLowerCase))

Otherwise the code looks great to me!

@yanboliang
Copy link
Contributor Author

@imatiach-msft Yeah, that line was also duplicated in some other estimators. I don't think we have a good way to add it to the base class Param, since it's abstract and not bound to specific type. Do you have some better suggestion? Thanks.

@SparkQA
Copy link

SparkQA commented Jan 20, 2017

Test build #71722 has finished for PR 16516 at commit f1f4c89.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@felixcheung
Copy link
Member

looks good to me

asfgit pushed a commit that referenced this pull request Jan 22, 2017
…hould case insensitive

## What changes were proposed in this pull request?
MLlib ```GeneralizedLinearRegression``` ```family``` and ```link``` should be case insensitive. This is consistent with some other MLlib params such as [```featureSubsetStrategy```](https://github.com/apache/spark/blob/master/mllib/src/main/scala/org/apache/spark/ml/tree/treeParams.scala#L415).

## How was this patch tested?
Update corresponding tests.

Author: Yanbo Liang <[email protected]>

Closes #16516 from yanboliang/spark-19133.

(cherry picked from commit 3dcad9f)
Signed-off-by: Yanbo Liang <[email protected]>
asfgit pushed a commit that referenced this pull request Jan 22, 2017
…hould case insensitive

## What changes were proposed in this pull request?
MLlib ```GeneralizedLinearRegression``` ```family``` and ```link``` should be case insensitive. This is consistent with some other MLlib params such as [```featureSubsetStrategy```](https://github.com/apache/spark/blob/master/mllib/src/main/scala/org/apache/spark/ml/tree/treeParams.scala#L415).

## How was this patch tested?
Update corresponding tests.

Author: Yanbo Liang <[email protected]>

Closes #16516 from yanboliang/spark-19133.

(cherry picked from commit 3dcad9f)
Signed-off-by: Yanbo Liang <[email protected]>
@yanboliang
Copy link
Contributor Author

yanboliang commented Jan 22, 2017

Merged into master and branch-2.1. Thanks for all your reviewing.

asfgit pushed a commit that referenced this pull request Jan 23, 2017
## What changes were proposed in this pull request?
This is a supplement to PR #16516 which did not make the value from `getFamily` case insensitive. Current tests of poisson/binomial glm with weight fail when specifying 'Poisson' or 'Binomial', because the calculation of `dispersion` and `pValue` checks the value of family retrieved from `getFamily`
```
model.getFamily == Binomial.name || model.getFamily == Poisson.name
```

## How was this patch tested?
Update existing tests for 'Poisson' and 'Binomial'.

yanboliang felixcheung imatiach-msft

Author: actuaryzhang <[email protected]>

Closes #16675 from actuaryzhang/family.

(cherry picked from commit f067ace)
Signed-off-by: Yanbo Liang <[email protected]>
ghost pushed a commit to dbtsai/spark that referenced this pull request Jan 23, 2017
## What changes were proposed in this pull request?
This is a supplement to PR apache#16516 which did not make the value from `getFamily` case insensitive. Current tests of poisson/binomial glm with weight fail when specifying 'Poisson' or 'Binomial', because the calculation of `dispersion` and `pValue` checks the value of family retrieved from `getFamily`
```
model.getFamily == Binomial.name || model.getFamily == Poisson.name
```

## How was this patch tested?
Update existing tests for 'Poisson' and 'Binomial'.

yanboliang felixcheung imatiach-msft

Author: actuaryzhang <[email protected]>

Closes apache#16675 from actuaryzhang/family.
uzadude pushed a commit to uzadude/spark that referenced this pull request Jan 27, 2017
…hould case insensitive

## What changes were proposed in this pull request?
MLlib ```GeneralizedLinearRegression``` ```family``` and ```link``` should be case insensitive. This is consistent with some other MLlib params such as [```featureSubsetStrategy```](https://github.com/apache/spark/blob/master/mllib/src/main/scala/org/apache/spark/ml/tree/treeParams.scala#L415).

## How was this patch tested?
Update corresponding tests.

Author: Yanbo Liang <[email protected]>

Closes apache#16516 from yanboliang/spark-19133.
uzadude pushed a commit to uzadude/spark that referenced this pull request Jan 27, 2017
## What changes were proposed in this pull request?
This is a supplement to PR apache#16516 which did not make the value from `getFamily` case insensitive. Current tests of poisson/binomial glm with weight fail when specifying 'Poisson' or 'Binomial', because the calculation of `dispersion` and `pValue` checks the value of family retrieved from `getFamily`
```
model.getFamily == Binomial.name || model.getFamily == Poisson.name
```

## How was this patch tested?
Update existing tests for 'Poisson' and 'Binomial'.

yanboliang felixcheung imatiach-msft

Author: actuaryzhang <[email protected]>

Closes apache#16675 from actuaryzhang/family.
cmonkey pushed a commit to cmonkey/spark that referenced this pull request Feb 15, 2017
…hould case insensitive

## What changes were proposed in this pull request?
MLlib ```GeneralizedLinearRegression``` ```family``` and ```link``` should be case insensitive. This is consistent with some other MLlib params such as [```featureSubsetStrategy```](https://github.com/apache/spark/blob/master/mllib/src/main/scala/org/apache/spark/ml/tree/treeParams.scala#L415).

## How was this patch tested?
Update corresponding tests.

Author: Yanbo Liang <[email protected]>

Closes apache#16516 from yanboliang/spark-19133.
cmonkey pushed a commit to cmonkey/spark that referenced this pull request Feb 15, 2017
## What changes were proposed in this pull request?
This is a supplement to PR apache#16516 which did not make the value from `getFamily` case insensitive. Current tests of poisson/binomial glm with weight fail when specifying 'Poisson' or 'Binomial', because the calculation of `dispersion` and `pValue` checks the value of family retrieved from `getFamily`
```
model.getFamily == Binomial.name || model.getFamily == Poisson.name
```

## How was this patch tested?
Update existing tests for 'Poisson' and 'Binomial'.

yanboliang felixcheung imatiach-msft

Author: actuaryzhang <[email protected]>

Closes apache#16675 from actuaryzhang/family.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants