/** * This class solves a least-squares problem using the Levenberg-Marquardt algorithm. * *
This implementation should work even for over-determined systems * (i.e. systems having more point than equations). Over-determined systems * are solved by ignoring the point which have the smallest impact according * to their jacobian column norm. Only the rank of the matrix and some loop bounds * are changed to implement this.
* *The resolution engine is a simple translation of the MINPACK lmder routine with minor * changes. The changes include the over-determined resolution, the use of * inherited convergence checker and the Q.R. decomposition which has been * rewritten following the algorithm described in the * P. Lascaux and R. Theodor book Analyse numérique matricielle * appliquée à l'art de l'ingénieur, Masson 1986.
*The authors of the original fortran version are: *
| * Minpack Copyright Notice (1999) University of Chicago. * All rights reserved * |
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
|
This implementation is a translation in Java of the MINPACK * lmpar * routine.
*This method sets the lmPar and lmDir attributes.
*The authors of the original fortran function are:
*Luc Maisonobe did the Java translation.
* * @param qy array containing qTy * @param delta upper bound on the euclidean norm of diagR * lmDir * @param diag diagonal matrix * @param work1 work array * @param work2 work array * @param work3 work array */ this.determineLMParameter = function(qy, delta, diag, work1, work2, work3) { var nC = this.weightedJacobian[0].length; // compute and store in x the gauss-newton direction, if the // jacobian is rank-deficient, obtain a least squares solution for (var j = 0; j < this.rank; ++j) { this.lmDir[this.permutation[j]] = qy[j]; } for (var j = this.rank; j < nC; ++j) { this.lmDir[this.permutation[j]] = 0; } for (var k = this.rank - 1; k >= 0; --k) { var pk = this.permutation[k]; var ypk = this.lmDir[pk] / this.diagR[pk]; for (var i = 0; i < k; ++i) { this.lmDir[this.permutation[i]] -= ypk * this.weightedJacobian[i][pk]; } this.lmDir[pk] = ypk; } // evaluate the function at the origin, and test // for acceptance of the Gauss-Newton direction var dxNorm = 0; for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; var s = diag[pj] * this.lmDir[pj]; work1[pj] = s; dxNorm += s * s; } dxNorm = Math.sqrt(dxNorm); var fp = dxNorm - delta; if (fp <= 0.1 * delta) { this.lmPar = 0; return; } // if the jacobian is not rank deficient, the Newton step provides // a lower bound, parl, for the zero of the function, // otherwise set this bound to zero var sum2; var parl = 0; if (this.rank == this.solvedCols) { for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; work1[pj] *= diag[pj] / dxNorm; } sum2 = 0; for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; var sum = 0; for (var i = 0; i < j; ++i) { sum += this.weightedJacobian[i][pj] * work1[this.permutation[i]]; } var s = (work1[pj] - sum) / this.diagR[pj]; work1[pj] = s; sum2 += s * s; } parl = fp / (delta * sum2); } // calculate an upper bound, paru, for the zero of the function sum2 = 0; for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; var sum = 0; for (var i = 0; i <= j; ++i) { sum += this.weightedJacobian[i][pj] * qy[i]; } sum /= diag[pj]; sum2 += sum * sum; } var gNorm = Math.sqrt(sum2); var paru = gNorm / delta; if (paru == 0) { // 2.2251e-308 is the smallest positive real for IEE754 paru = 2.2251e-308 / Math.min(delta, 0.1); } // if the input par lies outside of the interval (parl,paru), // set par to the closer endpoint this.lmPar = Math.min(paru, Math.max(this.lmPar, parl)); if (this.lmPar == 0) { this.lmPar = gNorm / dxNorm; } for (var countdown = 10; countdown >= 0; --countdown) { // evaluate the function at the current value of lmPar if (this.lmPar == 0) { this.lmPar = Math.max(2.2251e-308, 0.001 * paru); } var sPar = Math.sqrt(this.lmPar); for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; work1[pj] = sPar * diag[pj]; } this.determineLMDirection(qy, work1, work2, work3); dxNorm = 0; for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; var s = diag[pj] * this.lmDir[pj]; work3[pj] = s; dxNorm += s * s; } dxNorm = Math.sqrt(dxNorm); var previousFP = fp; fp = dxNorm - delta; // if the function is small enough, accept the current value // of lmPar, also test for the exceptional cases where parl is zero if ((Math.abs(fp) <= 0.1 * delta) || ((parl == 0) && (fp <= previousFP) && (previousFP < 0))) { return; } // compute the Newton correction for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; work1[pj] = work3[pj] * diag[pj] / dxNorm; } for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; work1[pj] /= work2[j]; var tmp = work1[pj]; for (var i = j + 1; i < this.solvedCols; ++i) { work1[this.permutation[i]] -= this.weightedJacobian[i][pj] * tmp; } } sum2 = 0; for (var j = 0; j < this.solvedCols; ++j) { var s = work1[this.permutation[j]]; sum2 += s * s; } var correction = fp / (delta * sum2); // depending on the sign of the function, update parl or paru. if (fp > 0) { parl = Math.max(parl, this.lmPar); } else if (fp < 0) { paru = Math.min(paru, this.lmPar); } // compute an improved estimate for lmPar this.lmPar = Math.max(parl, this.lmPar + correction); } } /** * Solve a*x = b and d*x = 0 in the least squares sense. *This implementation is a translation in Java of the MINPACK * qrsolv * routine.
*This method sets the lmDir and lmDiag attributes.
*The authors of the original fortran function are:
*Luc Maisonobe did the Java translation.
* * @param qy array containing qTy * @param diag diagonal matrix * @param lmDiag diagonal elements associated with lmDir * @param work work array */ this.determineLMDirection = function(qy, diag, lmDiag, work) { // copy R and Qty to preserve input and initialize s // in particular, save the diagonal elements of R in lmDir for (var j = 0; j < this.solvedCols; ++j) { var pj = this.permutation[j]; for (var i = j + 1; i < this.solvedCols; ++i) { this.weightedJacobian[i][pj] = this.weightedJacobian[j][this.permutation[i]]; } this.lmDir[j] = this.diagR[pj]; work[j] = qy[j]; } // eliminate the diagonal matrix d using a Givens rotation for (var j = 0; j < this.solvedCols; ++j) { // prepare the row of d to be eliminated, locating the // diagonal element using p from the Q.R. factorization var pj = this.permutation[j]; var dpj = diag[pj]; if (dpj != 0) { Arrays_fill(lmDiag, j + 1, lmDiag.length, 0); } lmDiag[j] = dpj; // the transformations to eliminate the row of d // modify only a single element of Qty // beyond the first n, which is initially zero. var qtbpj = 0; for (var k = j; k < this.solvedCols; ++k) { var pk = this.permutation[k]; // determine a Givens rotation which eliminates the // appropriate element in the current row of d if (lmDiag[k] != 0) { var sin; var cos; var rkk = this.weightedJacobian[k][pk]; if (Math.abs(rkk) < Math.abs(lmDiag[k])) { var cotan = rkk / lmDiag[k]; sin = 1.0 / Math.sqrt(1.0 + cotan * cotan); cos = sin * cotan; } else { var tan = lmDiag[k] / rkk; cos = 1.0 / Math.sqrt(1.0 + tan * tan); sin = cos * tan; } // compute the modified diagonal element of R and // the modified element of (Qty,0) this.weightedJacobian[k][pk] = cos * rkk + sin * lmDiag[k]; var temp = cos * work[k] + sin * qtbpj; qtbpj = -sin * work[k] + cos * qtbpj; work[k] = temp; // accumulate the tranformation in the row of s for (var i = k + 1; i < this.solvedCols; ++i) { var rik = this.weightedJacobian[i][pk]; var temp2 = cos * rik + sin * lmDiag[i]; lmDiag[i] = -sin * rik + cos * lmDiag[i]; this.weightedJacobian[i][pk] = temp2; } } } // store the diagonal element of s and restore // the corresponding diagonal element of R lmDiag[j] = this.weightedJacobian[j][this.permutation[j]]; this.weightedJacobian[j][this.permutation[j]] = this.lmDir[j]; } // solve the triangular system for z, if the system is // singular, then obtain a least squares solution var nSing = this.solvedCols; for (var j = 0; j < this.solvedCols; ++j) { if ((lmDiag[j] == 0) && (nSing == this.solvedCols)) { nSing = j; } if (nSing < this.solvedCols) { work[j] = 0; } } if (nSing > 0) { for (var j = nSing - 1; j >= 0; --j) { var pj = this.permutation[j]; var sum = 0; for (var i = j + 1; i < nSing; ++i) { sum += this.weightedJacobian[i][pj] * work[i]; } work[j] = (work[j] - sum) / lmDiag[j]; } } // permute the components of z back to components of lmDir for (var j = 0; j < this.lmDir.length; ++j) { this.lmDir[this.permutation[j]] = work[j]; } } /** * Decompose a matrix A as A.P = Q.R using Householder transforms. *As suggested in the P. Lascaux and R. Theodor book * Analyse numérique matricielle appliquée à * l'art de l'ingénieur (Masson, 1986), instead of representing * the Householder transforms with uk unit vectors such that: *
* Hk = I - 2uk.ukt
*
* we use k non-unit vectors such that:
*
* Hk = I - betakvk.vkt
*
* where vk = ak - alphak ek.
* The betak coefficients are provided upon exit as recomputing
* them from the vk vectors would be costly.
* This decomposition handles rank deficient cases since the tranformations * are performed in non-increasing columns norms order thanks to columns * pivoting. The diagonal elements of the R matrix are therefore also in * non-increasing absolute values order.
* * @param jacobian Weighted Jacobian matrix at the current point. * @exception ConvergenceException if the decomposition cannot be performed */ this.qrDecomposition = function(jacobian) { // Code in this class assumes that the weighted Jacobian is -(W^(1/2) J), // hence the multiplication by -1. this.weightedJacobian = this.scalarMultiply(jacobian, -1); var nR = this.weightedJacobian.length; var nC = this.weightedJacobian[0].length; // initializations for (var k = 0; k < nC; ++k) { this.permutation[k] = k; var norm2 = 0; for (var i = 0; i < nR; ++i) { var akk = this.weightedJacobian[i][k]; norm2 += akk * akk; } this.jacNorm[k] = Math.sqrt(norm2); } // transform the matrix column after column for (var k = 0; k < nC; ++k) { // select the column with the greatest norm on active components var nextColumn = -1; var ak2 = Number.NEGATIVE_INFINITY; for (var i = k; i < nC; ++i) { var norm2 = 0; for (var j = k; j < nR; ++j) { var aki = this.weightedJacobian[j][this.permutation[i]]; norm2 += aki * aki; } if (!isFinite(norm2)) { throw "UNABLE_TO_PERFORM_QR_DECOMPOSITION_ON_JACOBIAN"; } if (norm2 > ak2) { nextColumn = i; ak2 = norm2; } } if (ak2 <= this.qrRankingThreshold) { this.rank = k; return; } var pk = this.permutation[nextColumn]; this.permutation[nextColumn] = this.permutation[k]; this.permutation[k] = pk; // choose alpha such that Hk.u = alpha ek var akk = this.weightedJacobian[k][pk]; var alpha = (akk > 0) ? -Math.sqrt(ak2) : Math.sqrt(ak2); var betak = 1.0 / (ak2 - akk * alpha); this.beta[pk] = betak; // transform the current column this.diagR[pk] = alpha; this.weightedJacobian[k][pk] -= alpha; // transform the remaining columns for (var dk = nC - 1 - k; dk > 0; --dk) { var gamma = 0; for (var j = k; j < nR; ++j) { gamma += this.weightedJacobian[j][pk] * this.weightedJacobian[j][this.permutation[k + dk]]; } gamma *= betak; for (var j = k; j < nR; ++j) { this.weightedJacobian[j][this.permutation[k + dk]] -= gamma * this.weightedJacobian[j][pk]; } } } this.rank = this.solvedCols; } /** * Compute the product Qt.y for some Q.R. decomposition. * * @param y vector to multiply (will be overwritten with the result) */ this.qTy = function(y) { var nR = this.weightedJacobian.length; var nC = this.weightedJacobian[0].length; for (var k = 0; k < nC; ++k) { var pk = this.permutation[k]; var gamma = 0; for (var i = k; i < nR; ++i) { gamma += this.weightedJacobian[i][pk] * y[i]; } gamma *= this.beta[pk]; for (var i = k; i < nR; ++i) { y[i] -= gamma * this.weightedJacobian[i][pk]; } } } /** * Computes the weighted Jacobian matrix. * * @param params Model parameters at which to compute the Jacobian. * @return the weighted Jacobian: W1/2 J. * @throws DimensionMismatchException if the Jacobian dimension does not * match problem dimension. */ this.computeWeightedJacobian = function(params) { // return this.weightMatrixSqrt.multiply(this.jacobian(params)); //TODO: since weighted matrix is always identity return jacobian itself return this.jacobian(params); } this.scalarMultiply = function(m, s) { var rowCount = m.length; var columnCount = m[0].length; var out = []; for (var row = 0; row < rowCount; ++row) { out.push([]); for (var col = 0; col < columnCount; ++col) { out[row].push(m[row][col] * s); } } return out; } this.operate = function(m, v) { var nRows = m.length; var nCols = m[0].length; if (v.length != nCols) { throw "DimensionMismatchException: " + v.length + "!=" + nCols; } var out = []; for (var row = 0; row < nRows; row++) { var dataRow = m[row]; var sum = 0; for (var i = 0; i < nCols; i++) { sum += dataRow[i] * v[i]; } out[row] = sum; } return out; } /** * Computes the cost. * * @param residuals Residuals. * @return the cost. * @see #computeResiduals(double[]) */ this.computeCost = function(residuals) { return Math.sqrt(this.dotProduct( residuals, this.operate(this.getWeight(), residuals))); } this.dotProduct = function(v1, v2) { var dot = 0; for (var i = 0; i < v1.length; i++) { dot += v1[i] * v2[i]; } return dot; } /** * Gets the root-mean-square (RMS) value. * * The RMS the root of the arithmetic mean of the square of all weighted * residuals. * This is related to the criterion that is minimized by the optimizer * as follows: If c if the criterion, and n is the * number of measurements, then the RMS is sqrt (c/n). * * @return the RMS value. */ this.getRMS = function() { return Math.sqrt(this.getChiSquare() / this.target.length); } /** * Get a Chi-Square-like value assuming the N residuals follow N * distinct normal distributions centered on 0 and whose variances are * the reciprocal of the weights. * @return chi-square value */ this.getChiSquare = function() { return this.cost * this.cost; } /** * Gets the square-root of the weight matrix. * * @return the square-root of the weight matrix. */ this.getWeightSquareRoot = function() { return this.weightMatrixSqrt;//.copy(); FIXME for now it's always identity } this.getWeight = function() { return this.weightMatrix;//.copy(); FIXME for now it's always identity } /** * Sets the cost. * * @param cost Cost value. */ this.setCost = function(cost) { this.cost = cost; } /** * Computes the residuals. * The residual is the difference between the observed (target) * values and the model (objective function) value. * There is one residual for each element of the vector-valued * function. * * @param objectiveValue Value of the the objective function. This is * the value returned from a call to * {@link #computeObjectiveValue(double[]) computeObjectiveValue} * (whose array argument contains the model parameters). * @return the residuals. * @throws DimensionMismatchException if {@code params} has a wrong * length. */ this.computeResiduals = function(objectiveValue) { var target = this.target; if (objectiveValue.length != target.length) { throw "DimensionMismatchException: " + target.length + " != " + objectiveValue.length; } var residuals = arr(target.length); for (var i = 0; i < target.length; i++) { residuals[i] = target[i] - objectiveValue[i]; } return residuals; } this.computeObjectiveValue = function(params) { if (++this.evalCount > this.evalMaximalCount) { throw "TOO MANY FUNCTION EVALUATION" } return this.model(params); } }