-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-20602] [ML]Adding LBFGS optimizer and Squared_hinge loss for LinearSVC #17862
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 1 commit
d46e5ed
f7d5559
8a7c10f
4ce0787
c8afc63
3707580
2ffd0eb
2ca5a74
5f7f456
d19f619
0297057
15d611e
a545267
7be6bac
aaf35ec
ea82f35
93f7b68
cec628b
55ce6b9
0f5cad5
bf4d955
1f8e984
a6b4cda
f778f97
0bb5afe
64bc339
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 |
|---|---|---|
|
|
@@ -17,10 +17,13 @@ | |
|
|
||
| package org.apache.spark.ml.classification | ||
|
|
||
| import java.util.Locale | ||
|
|
||
| import scala.collection.mutable | ||
|
|
||
| import breeze.linalg.{DenseVector => BDV} | ||
| import breeze.optimize.{CachedDiffFunction, DiffFunction, OWLQN => BreezeOWLQN} | ||
| import breeze.optimize.{CachedDiffFunction, DiffFunction, LBFGS => BreezeLBFGS, | ||
| OWLQN => BreezeOWLQN} | ||
| import org.apache.hadoop.fs.Path | ||
|
|
||
| import org.apache.spark.SparkException | ||
|
|
@@ -42,7 +45,21 @@ import org.apache.spark.sql.functions.{col, lit} | |
| /** Params for linear SVM Classifier. */ | ||
| private[classification] trait LinearSVCParams extends ClassifierParams with HasRegParam | ||
| with HasMaxIter with HasFitIntercept with HasTol with HasStandardization with HasWeightCol | ||
| with HasThreshold with HasAggregationDepth | ||
| with HasThreshold with HasAggregationDepth { | ||
|
|
||
| /** | ||
| * The optimization algorithm for LinearSVC. | ||
| * Supported options: "lbfgs" and "owlqn". | ||
| * (default: "lbfgs") | ||
| * @group param | ||
| */ | ||
| final val optimizer: Param[String] = new Param[String](this, "optimizer", "The optimization" + | ||
| " algorithm to be used", ParamValidators.inArray[String](LinearSVC.supportedOptimizers)) | ||
|
|
||
| /** @group getParam */ | ||
| final def getOptimizer: String = $(optimizer) | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * :: Experimental :: | ||
|
|
@@ -60,6 +77,8 @@ class LinearSVC @Since("2.2.0") ( | |
| extends Classifier[Vector, LinearSVC, LinearSVCModel] | ||
| with LinearSVCParams with DefaultParamsWritable { | ||
|
|
||
| import LinearSVC._ | ||
|
|
||
| @Since("2.2.0") | ||
| def this() = this(Identifiable.randomUID("linearsvc")) | ||
|
|
||
|
|
@@ -145,6 +164,15 @@ class LinearSVC @Since("2.2.0") ( | |
| def setAggregationDepth(value: Int): this.type = set(aggregationDepth, value) | ||
| setDefault(aggregationDepth -> 2) | ||
|
|
||
| /** | ||
| * Set optimizer for LinearSVC. Supported options: "lbfgs" and "owlqn". | ||
| * | ||
| * @group setParam | ||
| */ | ||
| @Since("2.2.0") | ||
| def setOptimizer(value: String): this.type = set(optimizer, value.toLowerCase(Locale.ROOT)) | ||
| setDefault(optimizer -> "lbfgs") | ||
|
|
||
| @Since("2.2.0") | ||
| override def copy(extra: ParamMap): LinearSVC = defaultCopy(extra) | ||
|
|
||
|
|
@@ -205,15 +233,21 @@ class LinearSVC @Since("2.2.0") ( | |
| val costFun = new LinearSVCCostFun(instances, $(fitIntercept), | ||
| $(standardization), bcFeaturesStd, regParamL2, $(aggregationDepth)) | ||
|
|
||
| def regParamL1Fun = (index: Int) => 0D | ||
| val optimizer = new BreezeOWLQN[Int, BDV[Double]]($(maxIter), 10, regParamL1Fun, $(tol)) | ||
| val optimizerAlgo = $(optimizer) match { | ||
|
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 refresh me on why OWLQN was used originally? AFAIK OWLQN is for L1 regularized loss functions, while L-BFGS only supports L2?
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. @MLnick Hinge Loss is a non-smooth and non-differentiable loss, the convex optimization problem is a non-smooth one, so we use use OWL-QN rather than L-BFGS as the underlying optimizer.
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. Thanks for providing the info @yanboliang. I'm thinking we should provide both LBFGS and OWNQN as options for L1-SVM (Standard hinge). From the several dataset I tested, LBFGS converges as good as (or even better than) OWNQN and uses part of the time. Yet due to the potential failure, we may not use LBFGS as the default optimizer(solver). A better solution as I see is to use L2-SVM(squared hinge loss) with LBFGS (combine this with #17645), but the change may be too large to be included into 2.2 now.
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 saw other libraries like
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. Sounds good to me. But we'll also need to update python and R API. I don't know the release date of 2.2.
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. Let's wait for more other comments, if we reach a consensus, I'd like to help review and merge this to catch 2.2. cc @jkbradley @MLnick
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. Personally I would be in favor of making square hinge the default with L-BFGS. But we would need to make it clear to users that if they select the pure hinge loss then there is a risk involved in using L-BFGS rather than OWL-QN. |
||
| case LBFGS => new BreezeLBFGS[BDV[Double]]($(maxIter), 10, $(tol)) | ||
| case OWLQN => | ||
| def regParamL1Fun = (index: Int) => 0D | ||
| new BreezeOWLQN[Int, BDV[Double]]($(maxIter), 10, regParamL1Fun, $(tol)) | ||
| case _ => throw new SparkException ("unexpected optimizer: " + $(optimizer)) | ||
| } | ||
|
|
||
| val initialCoefWithIntercept = Vectors.zeros(numFeaturesPlusIntercept) | ||
|
|
||
| val states = optimizer.iterations(new CachedDiffFunction(costFun), | ||
| val states = optimizerAlgo.iterations(new CachedDiffFunction(costFun), | ||
| initialCoefWithIntercept.asBreeze.toDenseVector) | ||
|
|
||
| val scaledObjectiveHistory = mutable.ArrayBuilder.make[Double] | ||
| var state: optimizer.State = null | ||
| var state: optimizerAlgo.State = null | ||
| while (states.hasNext) { | ||
| state = states.next() | ||
| scaledObjectiveHistory += state.adjustedValue | ||
|
|
@@ -258,6 +292,15 @@ class LinearSVC @Since("2.2.0") ( | |
| @Since("2.2.0") | ||
| object LinearSVC extends DefaultParamsReadable[LinearSVC] { | ||
|
|
||
| /** String name for Limited-memory BFGS. */ | ||
| private[classification] val LBFGS: String = "lbfgs".toLowerCase(Locale.ROOT) | ||
|
|
||
| /** String name for Orthant-Wise Limited-memory Quasi-Newton. */ | ||
| private[classification] val OWLQN: String = "owlqn".toLowerCase(Locale.ROOT) | ||
|
|
||
| /* Set of optimizers that LinearSVC supports */ | ||
| private[classification] val supportedOptimizers = Array(LBFGS, OWLQN) | ||
|
|
||
| @Since("2.2.0") | ||
| override def load(path: String): LinearSVC = super.load(path) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,21 +75,25 @@ class LinearSVCSuite extends SparkFunSuite with MLlibTestSparkContext with Defau | |
| } | ||
|
|
||
| test("Linear SVC binary classification") { | ||
| val svm = new LinearSVC() | ||
| val model = svm.fit(smallBinaryDataset) | ||
| assert(model.transform(smallValidationDataset) | ||
| .where("prediction=label").count() > nPoints * 0.8) | ||
| val sparseModel = svm.fit(smallSparseBinaryDataset) | ||
| checkModels(model, sparseModel) | ||
| LinearSVC.supportedOptimizers.foreach { opt => | ||
| val svm = new LinearSVC().setOptimizer(opt) | ||
| val model = svm.fit(smallBinaryDataset) | ||
| assert(model.transform(smallValidationDataset) | ||
| .where("prediction=label").count() > nPoints * 0.8) | ||
| val sparseModel = svm.fit(smallSparseBinaryDataset) | ||
| checkModels(model, sparseModel) | ||
| } | ||
| } | ||
|
|
||
| test("Linear SVC binary classification with regularization") { | ||
| val svm = new LinearSVC() | ||
| val model = svm.setRegParam(0.1).fit(smallBinaryDataset) | ||
| assert(model.transform(smallValidationDataset) | ||
| .where("prediction=label").count() > nPoints * 0.8) | ||
| val sparseModel = svm.fit(smallSparseBinaryDataset) | ||
| checkModels(model, sparseModel) | ||
| LinearSVC.supportedOptimizers.foreach { opt => | ||
| val svm = new LinearSVC().setOptimizer(opt).setMaxIter(10) | ||
| val model = svm.setRegParam(0.1).fit(smallBinaryDataset) | ||
| assert(model.transform(smallValidationDataset) | ||
| .where("prediction=label").count() > nPoints * 0.8) | ||
| val sparseModel = svm.fit(smallSparseBinaryDataset) | ||
| checkModels(model, sparseModel) | ||
| } | ||
| } | ||
|
|
||
| test("params") { | ||
|
|
@@ -112,6 +116,7 @@ class LinearSVCSuite extends SparkFunSuite with MLlibTestSparkContext with Defau | |
| assert(lsvc.getFeaturesCol === "features") | ||
| assert(lsvc.getPredictionCol === "prediction") | ||
| assert(lsvc.getRawPredictionCol === "rawPrediction") | ||
| assert(lsvc.getOptimizer === "lbfgs") | ||
| val model = lsvc.setMaxIter(5).fit(smallBinaryDataset) | ||
| model.transform(smallBinaryDataset) | ||
| .select("label", "prediction", "rawPrediction") | ||
|
|
@@ -154,22 +159,23 @@ class LinearSVCSuite extends SparkFunSuite with MLlibTestSparkContext with Defau | |
|
|
||
| test("linearSVC with sample weights") { | ||
| def modelEquals(m1: LinearSVCModel, m2: LinearSVCModel): Unit = { | ||
| assert(m1.coefficients ~== m2.coefficients absTol 0.05) | ||
| assert(m1.coefficients ~== m2.coefficients absTol 0.07) | ||
| assert(m1.intercept ~== m2.intercept absTol 0.05) | ||
| } | ||
|
|
||
| val estimator = new LinearSVC().setRegParam(0.01).setTol(0.01) | ||
| val dataset = smallBinaryDataset | ||
| MLTestingUtils.testArbitrarilyScaledWeights[LinearSVCModel, LinearSVC]( | ||
| dataset.as[LabeledPoint], estimator, modelEquals) | ||
| MLTestingUtils.testOutliersWithSmallWeights[LinearSVCModel, LinearSVC]( | ||
| dataset.as[LabeledPoint], estimator, 2, modelEquals, outlierRatio = 3) | ||
| MLTestingUtils.testOversamplingVsWeighting[LinearSVCModel, LinearSVC]( | ||
| dataset.as[LabeledPoint], estimator, modelEquals, 42L) | ||
| LinearSVC.supportedOptimizers.foreach { opt => | ||
| val estimator = new LinearSVC().setRegParam(0.02).setTol(0.01).setOptimizer(opt) | ||
| val dataset = smallBinaryDataset | ||
| MLTestingUtils.testArbitrarilyScaledWeights[LinearSVCModel, LinearSVC]( | ||
| dataset.as[LabeledPoint], estimator, modelEquals) | ||
| MLTestingUtils.testOutliersWithSmallWeights[LinearSVCModel, LinearSVC]( | ||
| dataset.as[LabeledPoint], estimator, 2, modelEquals, outlierRatio = 3) | ||
| MLTestingUtils.testOversamplingVsWeighting[LinearSVCModel, LinearSVC]( | ||
| dataset.as[LabeledPoint], estimator, modelEquals, 42L) | ||
| } | ||
| } | ||
|
|
||
| test("linearSVC comparison with R e1071 and scikit-learn") { | ||
| val trainer1 = new LinearSVC() | ||
| test("linearSVC OWLQN comparison with R e1071 and scikit-learn") { | ||
| val trainer1 = new LinearSVC().setOptimizer("owlqn") | ||
| .setRegParam(0.00002) // set regParam = 2.0 / datasize / c | ||
|
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. Can I ask why we set
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. http://www.robots.ox.ac.uk/~az/lectures/ml/lect2.pdf 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 slides also explain it...Please see slide 32...the max can be replaced by soft-max with the softness lambda can be tuned...log-sum-exp is a standard soft-max that can be used which is similar to ReLu functions and we can re-use it from MLP:
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. @debasish83 Not sure if it's a better solution than squared hinge loss. But I would be interested to learn the performance (accuracy and speed). Can you please help try to evaluate it? 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. hinge loss is not differentiable...how are you smoothing it before you can use a quasi newton solver ? Since the papers smooth the max, a newton/quasi-newton solver should work well...if you are keeping the non-differentiable loss better will be to use a sub-gradient solver as suggested by the talk...I will evaluate the formulation... 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. @hhbyyh I saw some posts that hinge loss is not differentiable but squared hinge loss is for practical purposes...can you please point to a reference on squared hinge loss ? |
||
| .setMaxIter(200) | ||
| .setTol(1e-4) | ||
|
|
@@ -223,6 +229,25 @@ class LinearSVCSuite extends SparkFunSuite with MLlibTestSparkContext with Defau | |
| assert(model1.coefficients ~== coefficientsSK relTol 4E-3) | ||
| } | ||
|
|
||
| test("linearSVC LBFGS comparison with R e1071 and scikit-learn") { | ||
| val trainer1 = new LinearSVC().setOptimizer("LBFGS") | ||
| .setRegParam(0.00003) | ||
|
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. Why we switch to different
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. Indeed. I can use your help here since I cannot find the theory proof for this case. LR may have the same behavior. Since if I set the optimizer of LR to OWLQN, all the L2 regularization cases will fail. |
||
| .setMaxIter(200) | ||
| .setTol(1e-4) | ||
| val model1 = trainer1.fit(binaryDataset) | ||
|
|
||
| // refer to last unit test for R and python code | ||
| val coefficientsR = Vectors.dense(7.310338, 14.89741, 22.21005, 29.83508) | ||
| val interceptR = 7.440177 | ||
| assert(model1.intercept ~== interceptR relTol 2E-2) | ||
| assert(model1.coefficients ~== coefficientsR relTol 1E-2) | ||
|
|
||
| val coefficientsSK = Vectors.dense(7.24690165, 14.77029087, 21.99924004, 29.5575729) | ||
| val interceptSK = 7.36947518 | ||
| assert(model1.intercept ~== interceptSK relTol 1E-2) | ||
| assert(model1.coefficients ~== coefficientsSK relTol 1E-2) | ||
| } | ||
|
|
||
| test("read/write: SVM") { | ||
| def checkModelData(model: LinearSVCModel, model2: LinearSVCModel): Unit = { | ||
| assert(model.intercept === model2.intercept) | ||
|
|
||
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.
Maybe just call it solver, since
LinearRegressionalso has something similar like this.Uh oh!
There was an error while loading. Please reload this page.
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.
👍 - can use
HasSolvertraitThere 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.
The solver is
l-bfgsif training with lbfgs(l2 reg) or owlqn(l1/elasticNet reg) for other algorithms such asLinearRegressionandLogisticRegression, so I concerned that it may confuse users if we distinguish them inLinearSVC. Or we should clarify it in document. Thanks.