aboutsummaryrefslogtreecommitdiff
path: root/mllib-local
diff options
context:
space:
mode:
authorsethah <seth.hendrickson16@gmail.com>2016-10-24 23:47:59 -0700
committerYanbo Liang <ybliang8@gmail.com>2016-10-24 23:47:59 -0700
commit78d740a08a04b74b49b5cba4bb6a821631390ab4 (patch)
tree50f519c891e3d168071537f3d64b36d5ef854841 /mllib-local
parent483c37c581fedc64b218e294ecde1a7bb4b2af9c (diff)
downloadspark-78d740a08a04b74b49b5cba4bb6a821631390ab4.tar.gz
spark-78d740a08a04b74b49b5cba4bb6a821631390ab4.tar.bz2
spark-78d740a08a04b74b49b5cba4bb6a821631390ab4.zip
[SPARK-17748][ML] One pass solver for Weighted Least Squares with ElasticNet
## What changes were proposed in this pull request? 1. Make a pluggable solver interface for `WeightedLeastSquares` 2. Add a `QuasiNewton` solver to handle elastic net regularization for `WeightedLeastSquares` 3. Add method `BLAS.dspmv` used by QN solver 4. Add mechanism for WLS to handle singular covariance matrices by falling back to QN solver when Cholesky fails. ## How was this patch tested? Unit tests - see below. ## Design choices **Pluggable Normal Solver** Before, the `WeightedLeastSquares` package always used the Cholesky decomposition solver to compute the solution to the normal equations. Now, we specify the solver as a constructor argument to the `WeightedLeastSquares`. We introduce a new trait: ````scala private[ml] sealed trait NormalEquationSolver { def solve( bBar: Double, bbBar: Double, abBar: DenseVector, aaBar: DenseVector, aBar: DenseVector): NormalEquationSolution } ```` We extend this trait for different variants of normal equation solvers. In the future, we can easily add others (like QR) using this interface. **Always train in the standardized space** The normal solver did not previously standardize the data, but this patch introduces a change such that we always solve the normal equations in the standardized space. We convert back to the original space in the same way that is done for distributed L-BFGS/OWL-QN. We add test cases for zero variance features/labels. **Use L-BFGS locally to solve normal equations for singular matrix** When linear regression with the normal solver is called for a singular matrix, we initially try to solve with Cholesky. We use the output of `lapack.dppsv` to determine if the matrix is singular. If it is, we fall back to using L-BFGS locally to solve the normal equations. We add test cases for this as well. ## Test cases I found it helpful to enumerate some of the test cases and hopefully it makes review easier. **WeightedLeastSquares** 1. Constant columns - Cholesky solver fails with no regularization, Auto solver falls back to QN, and QN trains successfully. 2. Collinear features - Cholesky solver fails with no regularization, Auto solver falls back to QN, and QN trains successfully. 3. Label is constant zero - no training is performed regardless of intercept. Coefficients are zero and intercept is zero. 4. Label is constant - if fitIntercept, then no training is performed and intercept equals label mean. If not fitIntercept, then we train and return an answer that matches R's lm package. 5. Test with L1 - go through various combinations of L1/L2, standardization, fitIntercept and verify that output matches glmnet. 6. Initial intercept - verify that setting the initial intercept to label mean is correct by training model with strong L1 regularization so that all coefficients are zero and intercept converges to label mean. 7. Test diagInvAtWA - since we are standardizing features now during training, we should test that the inverse is computed to match R. **LinearRegression** 1. For all existing L1 test cases, test the "normal" solver too. 2. Check that using the normal solver now handles singular matrices. 3. Check that using the normal solver with L1 produces an objective history in the model summary, but does not produce the inverse of AtA. **BLAS** 1. Test new method `dspmv`. ## Performance Testing This patch will speed up linear regression with L1/elasticnet penalties when the feature size is < 4096. I have not conducted performance tests at scale, only observed by testing locally that there is a speed improvement. We should decide if this PR needs to be blocked before performance testing is conducted. Author: sethah <seth.hendrickson16@gmail.com> Closes #15394 from sethah/SPARK-17748.
Diffstat (limited to 'mllib-local')
-rw-r--r--mllib-local/src/main/scala/org/apache/spark/ml/linalg/BLAS.scala18
-rw-r--r--mllib-local/src/test/scala/org/apache/spark/ml/linalg/BLASSuite.scala45
2 files changed, 63 insertions, 0 deletions
diff --git a/mllib-local/src/main/scala/org/apache/spark/ml/linalg/BLAS.scala b/mllib-local/src/main/scala/org/apache/spark/ml/linalg/BLAS.scala
index 4ca19f3387..ef38909624 100644
--- a/mllib-local/src/main/scala/org/apache/spark/ml/linalg/BLAS.scala
+++ b/mllib-local/src/main/scala/org/apache/spark/ml/linalg/BLAS.scala
@@ -244,6 +244,24 @@ private[spark] object BLAS extends Serializable {
}
/**
+ * y := alpha*A*x + beta*y
+ *
+ * @param n The order of the n by n matrix A.
+ * @param A The upper triangular part of A in a [[DenseVector]] (column major).
+ * @param x The [[DenseVector]] transformed by A.
+ * @param y The [[DenseVector]] to be modified in place.
+ */
+ def dspmv(
+ n: Int,
+ alpha: Double,
+ A: DenseVector,
+ x: DenseVector,
+ beta: Double,
+ y: DenseVector): Unit = {
+ f2jBLAS.dspmv("U", n, alpha, A.values, x.values, 1, beta, y.values, 1)
+ }
+
+ /**
* Adds alpha * x * x.t to a matrix in-place. This is the same as BLAS's ?SPR.
*
* @param U the upper triangular part of the matrix packed in an array (column major)
diff --git a/mllib-local/src/test/scala/org/apache/spark/ml/linalg/BLASSuite.scala b/mllib-local/src/test/scala/org/apache/spark/ml/linalg/BLASSuite.scala
index 6e72a5fff0..877ac68983 100644
--- a/mllib-local/src/test/scala/org/apache/spark/ml/linalg/BLASSuite.scala
+++ b/mllib-local/src/test/scala/org/apache/spark/ml/linalg/BLASSuite.scala
@@ -422,4 +422,49 @@ class BLASSuite extends SparkMLFunSuite {
assert(dATT.multiply(sx) ~== expected absTol 1e-15)
assert(sATT.multiply(sx) ~== expected absTol 1e-15)
}
+
+ test("spmv") {
+ /*
+ A = [[3.0, -2.0, 2.0, -4.0],
+ [-2.0, -8.0, 4.0, 7.0],
+ [2.0, 4.0, -3.0, -3.0],
+ [-4.0, 7.0, -3.0, 0.0]]
+ x = [5.0, 2.0, -1.0, -9.0]
+ Ax = [ 45., -93., 48., -3.]
+ */
+ val A = new DenseVector(Array(3.0, -2.0, -8.0, 2.0, 4.0, -3.0, -4.0, 7.0, -3.0, 0.0))
+ val x = new DenseVector(Array(5.0, 2.0, -1.0, -9.0))
+ val n = 4
+
+ val y1 = new DenseVector(Array(-3.0, 6.0, -8.0, -3.0))
+ val y2 = y1.copy
+ val y3 = y1.copy
+ val y4 = y1.copy
+ val y5 = y1.copy
+ val y6 = y1.copy
+ val y7 = y1.copy
+
+ val expected1 = new DenseVector(Array(42.0, -87.0, 40.0, -6.0))
+ val expected2 = new DenseVector(Array(19.5, -40.5, 16.0, -4.5))
+ val expected3 = new DenseVector(Array(-25.5, 52.5, -32.0, -1.5))
+ val expected4 = new DenseVector(Array(-3.0, 6.0, -8.0, -3.0))
+ val expected5 = new DenseVector(Array(43.5, -90.0, 44.0, -4.5))
+ val expected6 = new DenseVector(Array(46.5, -96.0, 52.0, -1.5))
+ val expected7 = new DenseVector(Array(45.0, -93.0, 48.0, -3.0))
+
+ dspmv(n, 1.0, A, x, 1.0, y1)
+ dspmv(n, 0.5, A, x, 1.0, y2)
+ dspmv(n, -0.5, A, x, 1.0, y3)
+ dspmv(n, 0.0, A, x, 1.0, y4)
+ dspmv(n, 1.0, A, x, 0.5, y5)
+ dspmv(n, 1.0, A, x, -0.5, y6)
+ dspmv(n, 1.0, A, x, 0.0, y7)
+ assert(y1 ~== expected1 absTol 1e-8)
+ assert(y2 ~== expected2 absTol 1e-8)
+ assert(y3 ~== expected3 absTol 1e-8)
+ assert(y4 ~== expected4 absTol 1e-8)
+ assert(y5 ~== expected5 absTol 1e-8)
+ assert(y6 ~== expected6 absTol 1e-8)
+ assert(y7 ~== expected7 absTol 1e-8)
+ }
}