Computing a Matrix Inverse Using Newton Iteration With JavaScript

Dozens of machine learning algorithms require computing the inverse of a matrix. Computing a matrix inverse is conceptually easy, but implementation is one of the most difficult tasks in numerical programming.

The Wikipedia article on matrix inverse lists 14 different algorithms, and each algorithm has multiple variations, and each variation has dozens of implementation alternatives. Five examples include 1.) LUP decomposition algorithm (Crout version and others), 2.) QR decomposition algorithm (Householder version and others), 3.) SVD decomposition algorithm (Jacobi version and others), 4.) Cholesky decomposition (Banachiewicz version and others), 5.) Cayley-Hamilton algorithm (Faddeev–LeVerrier version and others). The fact that there are so many different techniques is a direct indication of how tricky it is to compute a matrix inverse.

One of my favorite techniques to compute a matrix inverse is called Newton iteration. It is simple, easy to implement, easy to modify, and easy to diagnose. One morning before work, I decided to brush off my C# implementation of Newton iteration and refactor it to JavaScript.

In pseudo-code, Newton iteration matrix inverse is:

let A equal the source matrix
set X_k to an initial starting matrix
loop max_iteration times
  set X_k+1 = X_k * (2I - (A * X_k))
  set X_k = X_k+1
end-loop
return X_k

Inside the processing loop, the * indicates matrix multiplication, and the – indicates matrix subtraction. The 2I indicates the scalar value 2 times the identity matrix, which is a matrix with 2.0 values on the diagonal, and 0.0 values elsewhere.

The X_k matrix slowly gets closer and closer to the inverse of the source matrix A. In most scenarios, you want the algorithm to periodically (perhaps once every 10 iterations) check to see if X_k is close enough (to within a small value epsilon) to the goal inverse, and early-exit if true.

Although it’s not obvious from the pseudo-code, the key to the Newton iteration is setting a good starting matrix X_k. An X_k with random values will usually not converge to the inverse of the source matrix.

My demo program uses the Pan-Reif technique to set a starting matrix. In words, the starting X_k matrix is (1/t) * A_tr where t is the product of the largest row sum of absolute values and the largest column sum of absolute values, and A_tr is the transpose of the source matrix A.

Suppose, as in the demo, source matrix A =

 1.0   2.0   3.0   1.0   5.0
 0.0  -5.0   4.0   1.0   4.0
 6.0   1.0   0.0  -2.0   2.0
 1.0  -4.0   5.0   3.0   2.0
 0.0   2.0   4.0   0.0  -1.0

The largest row sum of absolute values is 15.0 from the fourth row. The largest column sum of absolute values is 16.0 from the third column. Therefore t = 15.0 * 16.0 = 240.0 and 1/t = 0.0042. The transpose of a matrix swaps rows and columns, so A_tr =

  1.0   0.0   6.0   1.0   0.0
  2.0  -5.0   1.0  -4.0   2.0
  3.0   4.0   0.0   5.0   4.0
  1.0   1.0  -2.0   3.0   0.0
  5.0   4.0   2.0   2.0  -1.0

And therefore the starting X_k = (1/t) * A_tr =

 0.0042   0.0000   0.0250   0.0042   0.0000
 0.0083  -0.0208   0.0042  -0.0167   0.0083
 0.0125   0.0167   0.0000   0.0208   0.0167
 0.0042   0.0042  -0.0083   0.0125   0.0000
 0.0208   0.0167   0.0083   0.0083  -0.0042

The main challenge when implementing Newton iteration matrix inverse is coding up a lot of small but relatively simple helper functions.

The output of my demo program is:

Begin matrix inverse using Newton iteration
  algorithm with JavaScript

Source matrix:
 1.0   2.0   3.0   1.0   5.0
 0.0  -5.0   4.0   1.0   4.0
 6.0   1.0   0.0  -2.0   2.0
 1.0  -4.0   5.0   3.0   2.0
 0.0   2.0   4.0   0.0  -1.0

Setting maxIter = 1000
Setting eps = 1.0e-8
Setting verbose = true

Computing inverse
Done. Required 10 iterations.

Inverse:
-0.0316  -0.1190   0.1472   0.1483  -0.0428
 0.1227  -0.1264  -0.0186  -0.0112   0.0483
-0.0242   0.0855   0.0067  -0.0160   0.2026
 0.1152  -0.3309  -0.0781   0.3532  -0.1970
 0.1487   0.0892  -0.0104  -0.0862  -0.0929

Checking A * Ainv = I
 1.0000   0.0000   0.0000   0.0000   0.0000
 0.0000   1.0000   0.0000   0.0000   0.0000
 0.0000   0.0000   1.0000   0.0000   0.0000
 0.0000   0.0000   0.0000   1.0000   0.0000
 0.0000   0.0000   0.0000   0.0000   1.0000

Check PASS

End demo

The eps (epsilon) value controls how close to the correct inverse the result is before short-circuit exiting the processing iteration loop. The demo program checks if the computed inverse times the source matrix equals the identity matrix using a matAreEqual() function.

A good way to spend some time one morning before work.



Unlike most of my colleagues, I really enjoy coding with JavaScript. JavaScript is a language where you have to be very, very careful because there’s a lot that could go wrong. Here are two signs where the “What could possibly go wrong?” question is implied.


Demo code. Replace “lt” (less than), “gt”, “lte”, “gte” with Boolean operator symbols (my blog editor consistently chokes on symbols).

// matrix_inverse_newton_iteration.js
// node.js

let FS = require('fs');  // for loadTxt()

// ----------------------------------------------------------

function matInverseNewton(A, maxIter, eps, verbose)
{
  // Newton's method
  // X_k+1 = X_k * (2I - A*X_k)
  let n = A.length; // must be square martix

  let Xcurr = matStart(A);  // Pan-Reif algorithm
  let Xnew = matMake(n, n, 0.0);
  let I = matIdentity(n);
  let I2 = matScalarMult(2.0, I);

  let iter = 0;
  while (iter "lt" maxIter) {
    Xnew =
      matMult(Xcurr, matSubtract(I2, matMult(A, Xcurr)));
    // Xcurr = matCopyOf(Xnew); // by value
    Xcurr = Xnew; // by ref
    if (iter % 10 == 0) {
      let check = matMult(A, Xcurr); // 
      if (matAreEqual(check, I, eps) == true) {
        if (verbose == true) {
          console.log("Done. Required " + 
            iter.toString() + " iterations. ");
        }
        return Xcurr;
      }
    }

    ++iter;
  } // while
  console.log("WARN: no converge ");
  return Xnew;
}

// ----------------------------------------------------------

function matStart(A)
{
  // Pan-Reif initial matrix for Newton iteration inverse
  let n = A.length;
  let maxRowSum = 0.0;
  let maxColSum = 0.0;
  for (let i = 0; i "lt" n; ++i) {
    let rowSum = 0.0;
    for (let j = 0; j "lt" n; ++j) {
      rowSum += Math.abs(A[i][j]);
    }
    if (rowSum "gt" maxRowSum) {
      maxRowSum = rowSum;
    }
  }
  for (let j = 0; j "lt" n; ++j) {
    let colSum = 0.0;
    for (let i = 0; i "lt" n; ++i) {
      colSum += Math.abs(A[i][j]);
    }
    if (colSum "gt" maxColSum) {
      maxColSum = colSum;
    }
  }

  let result = matTranspose(A);
  let t = 1.0 / (maxRowSum * maxColSum);
  for (let i = 0; i "lt" n; ++i) {
    for (let j = 0; j "lt" n; ++j) {
      result[i][j] *= t;
    }
  }
  return result;
}

// ----------------------------------------------------------

function loadTxt(fn, delimit, usecols, comment) {
  // efficient but mildly complicated
  let all = FS.readFileSync(fn, "utf8");  // giant string
  all = all.trim();  // strip final crlf in file
  let lines = all.split("\n");  // array of lines

  // count number non-comment lines
  let nRows = 0;
  for (let i = 0; i "lt" lines.length; ++i) {
    if (!lines[i].startsWith(comment))
      ++nRows;
  }
  let nCols = usecols.length;
  let result = matMake(nRows, nCols, 0.0); 
 
  let r = 0;  // into lines
  let i = 0;  // into result[][]
  while (r "lt" lines.length) {
    if (lines[r].startsWith(comment)) {
      ++r;  // next row
    }
    else {
      let tokens = lines[r].split(delimit);
      for (let j = 0; j "lt" nCols; ++j) {
        result[i][j] = parseFloat(tokens[usecols[j]]);
      }
      ++r;
      ++i;
    }
  }

  return result;
}


// ----------------------------------------------------------

function vecMake(n, val)
{
  let result = [];
  for (let i = 0; i "lt" n; ++i) {
    result[i] = val;
  }
  return result;
}

// ----------------------------------------------------------

function matMake(nRows, nCols, val)
{
  let result = [];
  for (let i = 0; i "lt" nRows; ++i) {
    result[i] = [];
    for (let j = 0; j "lt" nCols; ++j) {
      result[i][j] = val;
    }
  }
  return result;
}

// ----------------------------------------------------------

function vecShow(vec, dec, wid, nl)
{
  for (let i = 0; i "lt" vec.length; ++i) {
    let x = vec[i];
    if (Math.abs(x) "lt" 1.0/Math.pow(10,dec)) {
      x = 0.0;  // avoid "-0.00"
    }
    let xx = x.toFixed(dec);
    let s = xx.toString().padStart(wid, ' ');
    process.stdout.write(s);
    process.stdout.write(" ");
  }

  if (nl == true)
    process.stdout.write("\n");
}

// ----------------------------------------------------------

function matShow(A, dec, wid)
{
  let nRows = A.length;
  let nCols = A[0].length;
  for (let i = 0; i "lt" nRows; ++i) {
    for (let j = 0; j "lt" nCols; ++j) {
      let x = A[i][j];
      if (Math.abs(x) "lt" 1.0/Math.pow(10,dec)) {
        x = 0.0;  // avoid "-0.00"
      }
      if (x "gte" 0.0) {
        process.stdout.write(" ");  // + or - space
      }
      process.stdout.write(x.toFixed(dec));
      process.stdout.write("  ");
    }
    process.stdout.write("\n");
  }
}

// ----------------------------------------------------------

function matMult(A, B)
{
  let aRows = A.length;
  let aCols = A[0].length;
  let bRows = B.length;
  let bCols = B[0].length;
  if (aCols != bRows)
    console.log("** FATAL ** Non-conformable matrices");

  let result = matMake(aRows, bCols, 0.0);
  for (let i = 0; i "lt" aRows; ++i) { // each row of A
    for (let j = 0; j "lt" bCols; ++j) { // each col of B
      for (let k = 0; k "lt" aCols; ++k) {
        result[i][j] += A[i][k] * B[k][j];
      }
    }
  }
  return result;
}

// ----------------------------------------------------------

function matAdd(A, B)
{
  let nRows = A.length;
  let nCols = A[0].length;
  let result = matMake(nRows, nCols, 0.0);
  for (let i = 0; i "lt" nRows; ++i) {
    for (let j = 0; j "lt" nCols; ++j) {
      result[i][j] = A[i][j] + B[i][j];
    }
  }
  return result;
}

// ----------------------------------------------------------

function matSubtract(A, B)
{
  let nRows = A.length;
  let nCols = A[0].length;
  let result = matMake(nRows, nCols, 0.0);
  for (let i = 0; i "lt" nRows; ++i) {
    for (let j = 0; j "lt" nCols; ++j) {
      result[i][j] = A[i][j] - B[i][j];
    }
  }
  return result;
}

// ----------------------------------------------------------

function matTrace(A)
{
  let result = 0.0;
  for (let i = 0; i "lt" A.length; ++i) {
    result += A[i][i];
  }
  return result;
}

// ----------------------------------------------------------

function matCopyOf(A)
{
  let nRows = A.length;
  let nCols = A[0].length;
  let result = matMake(nRows, nCols, 0.0);
  for (let i = 0; i "lt" nRows; ++i) {
    for (let j = 0; j "lt" nCols; ++j) {
      result[i][j] = A[i][j];
    }
  }
  return result;
}

// ----------------------------------------------------------

function matScalarMult(u, A)
{
  let nRows = A.length;
  let nCols = A[0].length;
  let result = matMake(nRows, nCols, 0.0);
  for (let i = 0; i "lt" nRows; ++i) { 
    for (let j = 0; j "lt" nCols; ++j) {
      result[i][j] = u * A[i][j];
    }
  }
  return result;
}

// ----------------------------------------------------------

function matTranspose(A)
{
  let nRows = A.length;
  let nCols = A[0].length;
  let result = matMake(nCols, nRows, 0.0);  // note reverse
  for (let i = 0; i "lt" nRows; ++i) {
    for (let j = 0; j "lt" nCols; ++j) {
      result[j][i] = A[i][j];
    }
  }
  return result;
}

// ----------------------------------------------------------

function matIdentity(n)
{
  let result = matMake(n, n, 0.0);
  for (let i = 0; i "lt" n; ++i)
    result[i][i] = 1.0;
  return result;
}


// ----------------------------------------------------------

function matAreEqual(A, B, eps)
{
  let nr = A.length;
  let nc = B[0].length;
  for (let i = 0; i "lt" nr; ++i)
    for (let j = 0; j "lt" nc; ++j)
      if (Math.abs(A[i][j] - B[i][j]) "gt" eps)
        return false;
  return true;
}

// ----------------------------------------------------------

function main()
{
  console.log("\nBegin matrix inverse using " +
    "Newton iteration algorithm with JavaScript ");

  let A = [];
  A[0] = [ 1.0,  2.0, 3.0,  1.0,  5.0 ];
  A[1] = [ 0.0, -5.0, 4.0,  1.0,  4.0 ];
  A[2] = [ 6.0,  1.0, 0.0, -2.0,  2.0 ];
  A[3] = [ 1.0, -4.0, 5.0,  3.0,  2.0 ];
  A[4] = [ 0.0,  2.0, 4.0,  0.0, -1.0 ];

  // A = loadTxt(".\\dummy_data.txt",
  //   ",", [0,1,2,3,4], "#");

  console.log("\nSource matrix: ");
  matShow(A, 1, 5);

  let maxIter = 1000;
  let eps = 1.0e-8;
  let verbose = true;
  console.log("\nSetting maxIter = " +
    maxIter.toString());
  console.log("Setting eps = " + 
    eps.toExponential(1).toString());
  console.log("Setting verbose = " +
    verbose.toString());
  
  console.log("\nComputing inverse ");
  let Ainv = matInverseNewton(A, maxIter, eps, verbose);

  console.log("\nInverse: ");
  matShow(Ainv, 4, 9);

  console.log("\nChecking A * Ainv = I ");
  let check = matMult(A, Ainv);
  matShow(check, 4, 9);

  let I = matIdentity(A.length);
  if (matAreEqual(check, I, eps) == true)
    console.log("\nCheck PASS ");
  else
    console.log("\nCheck FAIL ");

  console.log("\nEnd demo ");
}

main()
This entry was posted in JavaScript, Machine Learning. Bookmark the permalink.

Leave a Reply