Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d46e5ed
add lbfgs as default optimizer of LinearSVC
YY-OnCall May 4, 2017
f7d5559
set owlqn as default
YY-OnCall May 6, 2017
8a7c10f
set check
YY-OnCall May 9, 2017
4ce0787
:Merge remote-tracking branch 'upstream/master' into svclbfgs
YY-OnCall Jun 12, 2017
c8afc63
Merge remote-tracking branch 'upstream/master' into svclbfgs
YY-OnCall Jun 13, 2017
3707580
merge loss change
YY-OnCall Jun 13, 2017
2ffd0eb
Merge remote-tracking branch 'upstream/master' into svclbfgs
YY-OnCall Jun 13, 2017
2ca5a74
fix r and python
YY-OnCall Jun 14, 2017
5f7f456
switch between Hinge and Square
YY-OnCall Jun 14, 2017
d19f619
Merge remote-tracking branch 'upstream/master' into svclbfgs
YY-OnCall Jun 15, 2017
0297057
use RDDLossFunction
YY-OnCall Jun 15, 2017
15d611e
merge conflict
YY-OnCall Jun 26, 2017
a545267
Merge remote-tracking branch 'upstream/master' into svclbfgs
YY-OnCall Jun 27, 2017
7be6bac
r and new ut
YY-OnCall Jun 27, 2017
aaf35ec
ut update
YY-OnCall Jun 30, 2017
ea82f35
merge conflict
YY-OnCall Sep 2, 2017
93f7b68
merge conflict and add unit tests
YY-OnCall Sep 3, 2017
cec628b
style
YY-OnCall Sep 3, 2017
55ce6b9
resolve conflict
YY-OnCall Sep 15, 2017
0f5cad5
fix python ut
YY-OnCall Sep 16, 2017
bf4d955
resolve conflict
YY-OnCall Oct 2, 2017
1f8e984
style
YY-OnCall Oct 2, 2017
a6b4cda
Merge remote-tracking branch 'upstream/master' into svclbfgs
YY-OnCall Oct 3, 2017
f778f97
Merge remote-tracking branch 'upstream/master' into svclbfgs
YY-OnCall Oct 14, 2017
0bb5afe
minor updates
YY-OnCall Oct 15, 2017
64bc339
Merge remote-tracking branch 'upstream/master' into svclbfgs
YY-OnCall Oct 23, 2017
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 @@ -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
Expand All @@ -42,7 +45,7 @@ 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 with HasSolver

/**
* :: Experimental ::
Expand All @@ -60,6 +63,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"))

Expand Down Expand Up @@ -145,6 +150,23 @@ class LinearSVC @Since("2.2.0") (
def setAggregationDepth(value: Int): this.type = set(aggregationDepth, value)
setDefault(aggregationDepth -> 2)

/**
* Set solver for LinearSVC. Supported options: "l-bfgs" and "owlqn" (case insensitve).
* - "l-bfgs" denotes Limited-memory BFGS which is a limited-memory quasi-Newton
* optimization method.
* - "owlqn" denotes Orthant-Wise Limited-memory Quasi-Newton algorithm .
* (default: "owlqn")
* @group setParam
*/
@Since("2.2.0")
def setSolver(value: String): this.type = {
val lowercaseValue = value.toLowerCase(Locale.ROOT)
require(supportedOptimizers.contains(lowercaseValue),
s"Solver $value was not supported. Supported options: l-bfgs, owlqn")
set(solver, lowercaseValue)
}
setDefault(solver -> "owlqn")

@Since("2.2.0")
override def copy(extra: ParamMap): LinearSVC = defaultCopy(extra)

Expand Down Expand Up @@ -205,8 +227,14 @@ 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 optimizer = $(solver) match {
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: " + $(solver))
}

val initialCoefWithIntercept = Vectors.zeros(numFeaturesPlusIntercept)

val states = optimizer.iterations(new CachedDiffFunction(costFun),
Expand Down Expand Up @@ -258,6 +286,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 = "l-bfgs".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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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().setSolver(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().setSolver(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") {
Expand All @@ -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.getSolver === "owlqn")
val model = lsvc.setMaxIter(5).fit(smallBinaryDataset)
model.transform(smallBinaryDataset)
.select("label", "prediction", "rawPrediction")
Expand Down Expand Up @@ -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).setSolver(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().setSolver(LinearSVC.OWLQN)
.setRegParam(0.00002) // set regParam = 2.0 / datasize / c

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can I ask why we set regParam = 2.0 / datasize / c to match solution of sklearn.svm.LinearSVC? AFAIK, sklearn called liblinear to train linear SVM classification model, I can understand liblinear using C for penalty parameter of the error term which is different from regParam, and it doesn't multiply 1/n on the error term, but where is the 2.0 come from? Thanks.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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:
ftp://ftp.cs.wisc.edu/math-prog/talks/informs99ssv.ps
ftp://ftp.cs.wisc.edu/pub/dmi/tech-reports/99-03.pdf
I can add the formulation if there is interest...it needs some tuning for soft-max parameter but the convergence will be good with LBFGS (OWLQN is not needed)

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.

@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?

@debasish83 debasish83 May 10, 2017

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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)
Expand Down Expand Up @@ -223,6 +229,25 @@ class LinearSVCSuite extends SparkFunSuite with MLlibTestSparkContext with Defau
assert(model1.coefficients ~== coefficientsSK relTol 4E-3)
}

test("linearSVC L-BFGS comparison with R e1071 and scikit-learn") {
val trainer1 = new LinearSVC().setSolver(LinearSVC.LBFGS)
.setRegParam(0.00003)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why we switch to different regParam compared with the above test? Does it means the converged solution will depends on optimizer?

@hhbyyh hhbyyh May 10, 2017

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.

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)
Expand Down