Author: tdunning
Date: Thu Dec 22 23:51:27 2011
New Revision: 1222513
URL: http://svn.apache.org/viewvc?rev=1222513&view=rev
Log:
MAHOUT-792 - Cholesky decomposition
MAHOUT-792 - New Cholesky decomposition for SSVD update.
MAHOUT-792 - Switch Cholesky to views.
Added:
mahout/trunk/math/src/main/java/org/apache/mahout/math/CholeskyDecomposition.java
mahout/trunk/math/src/test/java/org/apache/mahout/math/CholeskyDecompositionTest.java
Added:
mahout/trunk/math/src/main/java/org/apache/mahout/math/CholeskyDecomposition.java
URL:
http://svn.apache.org/viewvc/mahout/trunk/math/src/main/java/org/apache/mahout/math/CholeskyDecomposition.java?rev=1222513&view=auto
==============================================================================
---
mahout/trunk/math/src/main/java/org/apache/mahout/math/CholeskyDecomposition.java
(added)
+++
mahout/trunk/math/src/main/java/org/apache/mahout/math/CholeskyDecomposition.java
Thu Dec 22 23:51:27 2011
@@ -0,0 +1,211 @@
+package org.apache.mahout.math;
+
+import com.google.common.base.Preconditions;
+import org.apache.mahout.math.function.Functions;
+
+/**
+ * Cholesky decomposition shamelessly ported from JAMA.
+ * <p/>
+ * A Cholesky decomposition of a semi-positive definite matrix A is a lower
triangular matrix L such
+ * that L L^* = A. If A is full rank, L is unique. If A is real, then it
must be symmetric and R
+ * will also be real.
+ */
+public class CholeskyDecomposition {
+ private PivotedMatrix L;
+ private boolean isPositiveDefinite;
+
+ public CholeskyDecomposition(Matrix a) {
+ this(a, true);
+ }
+
+ public CholeskyDecomposition(Matrix a, boolean pivot) {
+ int rows = a.rowSize();
+ L = new PivotedMatrix(new DenseMatrix(rows, rows));
+
+ // must be square
+ Preconditions.checkArgument(rows == a.columnSize());
+
+ if (pivot) {
+ decomposeWithPivoting(a);
+ } else {
+ decompose(a);
+ }
+ }
+
+ private void decomposeWithPivoting(Matrix a) {
+ int n = a.rowSize();
+ L.assign(a);
+
+ // pivoted column-wise submatrix cholesky with simple pivoting
+ double uberMax = L.viewDiagonal().aggregate(Functions.MAX, Functions.ABS);
+ for (int k = 0; k < n; k++) {
+ double max = 0;
+ int pivot = k;
+ for (int j = k; j < n; j++) {
+ if (L.get(j, j) > max) {
+ max = L.get(j, j);
+ pivot = j;
+ if (uberMax < Math.abs(max)) {
+ uberMax = Math.abs(max);
+ }
+ }
+ }
+ L.swap(k, pivot);
+
+ double akk = L.get(k, k);
+ double epsilon = 1e-10 * Math.max(uberMax,
L.viewColumn(k).aggregate(Functions.MAX, Functions.ABS));
+
+ if (akk < -epsilon) {
+ // can't have decidedly negative element on diagonal
+ throw new IllegalArgumentException("Matrix is not positive
semi-definite");
+ } else if (akk <= epsilon) {
+ // degenerate column case. Set all to zero
+ L.viewColumn(k).assign(0);
+ isPositiveDefinite = false;
+
+ // no need to subtract from remaining sub-matrix
+ } else {
+ // normalize column by diagonal element
+ akk = Math.sqrt(Math.max(0, akk));
+ L.viewColumn(k).viewPart(k, n - k).assign(Functions.div(akk));
+ L.viewColumn(k).viewPart(0, k).assign(0);
+
+ // subtract off scaled version of this column to the right
+ for (int j = k + 1; j < n; j++) {
+ Vector columnJ = L.viewColumn(j).viewPart(k, n - k);
+ Vector columnK = L.viewColumn(k).viewPart(k, n - k);
+ columnJ.assign(columnK, Functions.minusMult(columnK.get(j - k)));
+ }
+
+ }
+ }
+ }
+
+ private void decompose(Matrix a) {
+ int n = a.rowSize();
+ L.assign(a);
+
+ // column-wise submatrix cholesky with simple pivoting
+ for (int k = 0; k < n; k++) {
+
+ double akk = L.get(k, k);
+
+ // set upper part of column to 0.
+ L.viewColumn(k).viewPart(0, k).assign(0);
+
+ double epsilon = 1e-10 * L.viewColumn(k).aggregate(Functions.MAX,
Functions.ABS);
+ if (akk <= epsilon) {
+ // degenerate column case. Set diagonal to 1, all others to zero
+ L.viewColumn(k).viewPart(k, n - k).assign(0);
+
+ isPositiveDefinite = false;
+
+ // no need to subtract from remaining sub-matrix
+ } else {
+ // normalize column by diagonal element
+ akk = Math.sqrt(Math.max(0, akk));
+ L.set(k, k, akk);
+ L.viewColumn(k).viewPart(k + 1, n - k - 1).assign(Functions.div(akk));
+
+ // now subtract scaled version of column
+ for (int j = k + 1; j < n; j++) {
+ Vector columnJ = L.viewColumn(j).viewPart(j, n - j);
+ Vector columnK = L.viewColumn(k).viewPart(j, n - j);
+ columnJ.assign(columnK, Functions.minusMult(L.get(j, k)));
+ }
+ }
+ }
+ }
+
+ public boolean isPositiveDefinite() {
+ return isPositiveDefinite;
+ }
+
+ public Matrix getL() {
+ return L.getBase();
+ }
+
+ public PivotedMatrix getPermutedL() {
+ return L;
+ }
+
+ /**
+ * @return Returns the permutation of rows and columns that was applied to L
+ */
+ public int[] getPivot() {
+ return L.getRowPivot();
+ }
+
+ public int[] getInversePivot() {
+ return L.getInverseRowPivot();
+ }
+
+ /**
+ * Compute inv(L) * z efficiently.
+ *
+ * @param z
+ */
+ public Matrix solveLeft(Matrix z) {
+ int n = L.columnSize();
+ int nx = z.columnSize();
+
+ Matrix X = new DenseMatrix(n, z.columnSize());
+ X.assign(z);
+
+ // Solve L*Y = Z using back-substitution
+ // note that k and i have to go in a funny order because L is pivoted
+ for (int internalK = 0; internalK < n; internalK++) {
+ int k = L.rowUnpivot(internalK);
+ for (int j = 0; j < nx; j++) {
+ for (int internalI = 0; internalI < internalK; internalI++) {
+ int i = L.rowUnpivot(internalI);
+ X.set(k, j, X.get(k, j) - X.get(i, j) * L.get(k, i));
+ }
+ if (L.get(k, k) != 0) {
+ X.set(k, j, X.get(k, j) / L.get(k, k));
+ } else {
+ X.set(k, j, 0);
+ }
+ }
+ }
+ return X;
+ }
+
+ /**
+ * Compute z * inv(L') efficiently
+ *
+ * @param z
+ * @return
+ */
+ public Matrix solveRight(Matrix z) {
+ int n = z.columnSize();
+ int nx = z.rowSize();
+
+ Matrix x = new DenseMatrix(z.rowSize(), z.columnSize());
+ x.assign(z);
+
+ // Solve Y*L' = Z using back-substitution
+ for (int internalK = 0; internalK < n; internalK++) {
+ int k = L.rowUnpivot(internalK);
+ for (int j = 0; j < nx; j++) {
+ for (int internalI = 0; internalI < k; internalI++) {
+ int i = L.rowUnpivot(internalI);
+ x.set(j, k, x.get(j, k) - x.get(j, i) * L.get(k, i));
+ if (Double.isInfinite(x.get(j, k)) || Double.isNaN(x.get(j, k))) {
+ System.out.printf("bad at %d,%d\n", j, k);
+ }
+ }
+ if (L.get(k, k) != 0) {
+ x.set(j, k, x.get(j, k) / L.get(k, k));
+ } else {
+ x.set(j, k, 0);
+ }
+ if (Double.isInfinite(x.get(j, k)) || Double.isNaN(x.get(j, k))) {
+ System.out.printf("bad at %d,%d\n", j, k);
+ }
+ }
+ }
+ return x;
+ }
+}
+
Added:
mahout/trunk/math/src/test/java/org/apache/mahout/math/CholeskyDecompositionTest.java
URL:
http://svn.apache.org/viewvc/mahout/trunk/math/src/test/java/org/apache/mahout/math/CholeskyDecompositionTest.java?rev=1222513&view=auto
==============================================================================
---
mahout/trunk/math/src/test/java/org/apache/mahout/math/CholeskyDecompositionTest.java
(added)
+++
mahout/trunk/math/src/test/java/org/apache/mahout/math/CholeskyDecompositionTest.java
Thu Dec 22 23:51:27 2011
@@ -0,0 +1,139 @@
+package org.apache.mahout.math;
+
+import org.apache.mahout.common.RandomUtils;
+import org.apache.mahout.math.function.DoubleDoubleFunction;
+import org.apache.mahout.math.function.DoubleFunction;
+import org.apache.mahout.math.function.Functions;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Random;
+
+public class CholeskyDecompositionTest extends MahoutTestCase {
+ @Test
+ public void rank1() {
+ Matrix x = new DenseMatrix(3, 3);
+ x.viewRow(0).assign(new double[]{1, 2, 3});
+ x.viewRow(1).assign(new double[]{2, 4, 6});
+ x.viewRow(2).assign(new double[]{3, 6, 9});
+
+ CholeskyDecomposition rr = new
CholeskyDecomposition(x.transpose().times(x), false);
+ assertEquals(0, new DenseVector(new double[]{3.741657, 7.483315,
11.22497}).aggregate(rr.getL().transpose().viewRow(0), Functions.PLUS, new
DoubleDoubleFunction() {
+ @Override
+ public double apply(double arg1, double arg2) {
+ return Math.abs(arg1) - Math.abs(arg2);
+ }
+ }), 1e-5);
+
+ assertEquals(0, rr.getL().viewPart(0, 3, 1, 2).aggregate(Functions.PLUS,
Functions.ABS), 1e-9);
+ }
+
+ @Test
+ public void test1() {
+ Matrix lastL;
+
+ final Random rand = RandomUtils.getRandom();
+
+ Matrix z = new DenseMatrix(100, 100);
+ z.assign(new DoubleFunction() {
+ @Override
+ public double apply(double arg1) {
+ return rand.nextDouble();
+ }
+ });
+
+ Matrix A = z.times(z.transpose());
+
+ for (boolean type = false; !type; type=true) {
+ CholeskyDecomposition cd = new CholeskyDecomposition(A, type);
+ Matrix L = cd.getL();
+ lastL = L;
+// Assert.assertTrue("Positive definite", cd.isPositiveDefinite());
+
+ Matrix Abar = L.times(L.transpose());
+
+ double error = A.minus(Abar).aggregate(Functions.MAX, Functions.ABS);
+ Assert.assertEquals("type = " + type, 0, error, 1e-10);
+
+ // L should give us a quick and dirty LQ decomposition
+ Matrix q = cd.solveLeft(z);
+ Matrix id = q.times(q.transpose());
+ for (int i = 0; i < id.columnSize(); i++) {
+ Assert.assertEquals("type = " + type, 1, id.get(i, i), 1e-9);
+ Assert.assertEquals("type = " + type, 1, id.viewRow(i).norm(1), 1e-9);
+ }
+
+ // and QR as well
+ q = cd.solveRight(z.transpose());
+ id = q.transpose().times(q);
+ for (int i = 0; i < id.columnSize(); i++) {
+ Assert.assertEquals("type = " + type, 1, id.get(i, i), 1e-9);
+ Assert.assertEquals("type = " + type, 1, id.viewRow(i).norm(1), 1e-9);
+ }
+ }
+ }
+
+ @Test
+ public void test2() {
+ // Test matrix from Nicholas Higham's paper at
http://eprints.ma.man.ac.uk/1199/01/covered/MIMS_ep2008_116.pdf
+ double[][] values = new double[3][];
+ values[0] = new double[]{1, -1, 1};
+ values[1] = new double[]{-1, 1, -1};
+ values[2] = new double[]{1, -1, 2};
+
+ Matrix A = new DenseMatrix(values);
+
+ // without pivoting
+ CholeskyDecomposition cd = new CholeskyDecomposition(A, false);
+ assertEquals(0,
cd.getL().times(cd.getL().transpose()).minus(A).aggregate(Functions.PLUS,
Functions.ABS), 1e-10);
+
+ // with pivoting
+ cd = new CholeskyDecomposition(A);
+ assertEquals(0,
cd.getL().times(cd.getL().transpose()).minus(A).aggregate(Functions.PLUS,
Functions.ABS), 1e-10);
+ }
+
+
+ @Test
+ public void testRankDeficient() {
+ Matrix A = rank4Matrix();
+
+ CholeskyDecomposition cd = new CholeskyDecomposition(A, false);
+
+ cd = new CholeskyDecomposition(A);
+
+ PivotedMatrix Ax = new PivotedMatrix(A, cd.getPivot());
+ CholeskyDecomposition cd2 = new CholeskyDecomposition(Ax, false);
+
+ assertEquals(0,
cd2.getL().times(cd2.getL().transpose()).minus(Ax).aggregate(Functions.PLUS,
Functions.ABS), 1e-10);
+ assertEquals(0,
cd.getL().times(cd.getL().transpose()).minus(A).aggregate(Functions.PLUS,
Functions.ABS), 1e-10);
+
+ Assert.assertFalse(cd.isPositiveDefinite());
+ Matrix L = cd.getL();
+ Matrix Abar = L.times(L.transpose());
+ double error = A.minus(Abar).aggregate(Functions.MAX, Functions.ABS);
+ Assert.assertEquals(0, error, 1e-10);
+ }
+
+ private Matrix rank4Matrix() {
+ final Random rand = RandomUtils.getRandom();
+
+ Matrix u = new DenseMatrix(10, 4);
+ u.assign(new DoubleFunction() {
+ @Override
+ public double apply(double arg1) {
+ return rand.nextDouble();
+ }
+ });
+
+ Matrix v = new DenseMatrix(10, 4);
+ v.assign(new DoubleFunction() {
+ @Override
+ public double apply(double arg1) {
+ return rand.nextDouble();
+ }
+ });
+
+ Matrix z = u.times(v.transpose());
+ return z.times(z.transpose());
+ }
+}