/*
* Copyright (c) 2012-2015, Peter Abeles. All Rights Reserved.
*
* This file is part of DDogleg (http://ddogleg.org).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ddogleg.optimization.impl;
import org.ejml.alg.dense.mult.VectorVectorMult;
import org.ejml.data.DenseMatrix64F;
import org.ejml.ops.CommonOps;
/**
*
* Base class for Levenberg type algorithms. TODO describe in more detail
*
*
* @author Peter Abeles
*/
public abstract class LevenbergBase {
// number of parameters
protected int N;
// number of functions
protected int M;
// tolerance for termination. magnitude of gradient. absolute
private double gtol;
// tolerance for termination, change in function value. relative
private double ftol;
// current set of parameters being considered
private DenseMatrix64F x = new DenseMatrix64F(1,1);
// gradient at 'x' = J'*f(x)
private DenseMatrix64F g = new DenseMatrix64F(1,1);
// function residuals values at x
private DenseMatrix64F funcVals = new DenseMatrix64F(1,1);
// Current x being considered
private DenseMatrix64F xtest = new DenseMatrix64F(1,1);
private DenseMatrix64F xdelta = new DenseMatrix64F(1,1);
// function value norm at x
private double fnorm;
// previous value of fnorm
private double fnormPrev;
// levenberg marquardt dampening parameter
private double dampParam;
private double initialDampParam;
// used to scale the dampening parameter
private double nu;
// is it searching for a new dampening parameter or setting up the next iteration
private int mode;
// true if x was updated in this iteration
private boolean updatedParameters;
// has it converged or not
private boolean hasConverged;
// total number of iterations
private int iterationCount;
// message explaining failure
private String message;
/**
* Specifies termination condition and dampening parameter
*
* @param initialDampParam Initial value of the dampening parameter. Tune.. try 1e-3;
*/
public LevenbergBase(double initialDampParam ) {
this.initialDampParam = initialDampParam;
}
/**
* Specify convergence tolerances
*
* @param gtol absolute convergence tolerance based on gradient norm. 0 ≤ gtol
*/
public void setConvergence( double ftol , double gtol ) {
if( ftol < 0 || ftol >= 1 )
throw new IllegalArgumentException("0 <= ftol < 1");
if( gtol < 0 )
throw new IllegalArgumentException("gtol < 0 ");
this.gtol = gtol;
this.ftol = ftol;
}
/**
* Specify the initial value of the dampening parameter.
*
* @param initialDampParam initial value
*/
public void setInitialDampParam(double initialDampParam) {
this.initialDampParam = initialDampParam;
}
/**
* Initializes internal parameters.
*/
protected void internalInitialize( int numParameters , int numFunctions ) {
this.N = numParameters;
this.M = numFunctions;
x.reshape(N, 1, false);
g.reshape(N,1,false);
xdelta.reshape(N, 1, false);
xtest.reshape(N,1,false);
funcVals.reshape(M,1,false);
}
/**
* Sets the initial parameter being searched.
*
* @param initial
*/
public void initialize(double[] initial) {
if( initial.length < N)
throw new IllegalArgumentException("Expected N="+N+" parameters");
System.arraycopy(initial,0,x.data,0,N);
// calculate residuals
setFunctionParameters(x.data);
computeResiduals(funcVals.data);
// error at this point
fnorm = computeError();
fnormPrev = 0;
hasConverged = false;
mode = 0;
dampParam = initialDampParam;
nu = 2;
iterationCount = 0;
message = "";
}
protected abstract void setFunctionParameters( double []param );
protected abstract void computeResiduals( double[] output );
/**
* Computes the Jacobian matrix,
*/
protected abstract void computeJacobian( DenseMatrix64F residuals ,
DenseMatrix64F gradient );
protected abstract boolean computeStep( double dampeningParam ,
DenseMatrix64F gradientNegative ,
DenseMatrix64F step );
protected abstract double predictedReduction( DenseMatrix64F param, DenseMatrix64F gradientNegative , double dampeningParam );
/**
* Returns the minimum allowed value for the dampening parameters. This should be a function
* of the Jacobian matrix's scale.
*
* @return
*/
protected abstract double getMinimumDampening();
public double[] getParameters() {
return x.data;
}
/**
* Performs a single step in the optimization.
*
* @return true if the optimization has finished.
*/
public boolean iterate() {
updatedParameters = false;
if( mode == 0 ) {
return initSamplePoint();
} else {
if( !computeStep() )
return true;
}
return false;
}
/**
* Evaluates the Jacobian, computes the gradient and Hessian approximation. Checks for convergence.
*
* @return true if it has converged
*/
private boolean initSamplePoint() {
// calculate the Jacobian values at the current sample point
computeJacobian(funcVals,g);
CommonOps.scale(-1, g);
// Find the derivative along the current Jacobian's direction
double gx = CommonOps.elementMaxAbs(g);
// check for convergence
if( Math.abs(fnorm-fnormPrev) <= ftol*Math.max(fnorm,fnormPrev) || Math.abs(gx) <= gtol )
return terminateSearch(true, null);
fnormPrev = fnorm;
mode = 1;
return false;
}
private boolean computeStep() {
// Solves for xdelta
// (B + mu*I)xdelta = -g
// where mu is dampParam
if( !solveForXDelta() )
return false;
// xtest = x + delta x
CommonOps.add(x, xdelta, xtest);
// take in account rounding error
CommonOps.subtract(xtest, x, xdelta);
// compute the residuals at x
setFunctionParameters(xtest.data);
computeResiduals(funcVals.data);
// actual reduction
double ftestnorm = computeError();
double actualReduction = fnorm - ftestnorm;
// Predicted reduction
double predictedReduction = predictedReduction(xdelta,g,dampParam);
// update the dampParam depending on the results
if( predictedReduction > 0 && actualReduction >= 0 ) {
// set the test point to be the new point
DenseMatrix64F temp = x;
x = xtest; xtest = temp;
// updated residual norm
fnorm = ftestnorm;
// reduction ratio
double ratio = actualReduction/predictedReduction;
// reduce the amount of dampening. Magic equation from [1]. My attempts to improve
// upon it have failed. It is truly magical.
dampParam *= Math.max(0.3333333,1-Math.pow(2*ratio-1,3));
nu = 2;
// start the iteration over again
mode = 0;
iterationCount++;
updatedParameters = true;
} else {
// did not improve, increase the amount of dampening
dampParam *= nu;
nu *= 2;
}
if( Double.isInfinite(dampParam) || Double.isNaN(dampParam))
return false;
return true;
}
/**
* Sets up the linear system to find the change in x. If the solver fails or is nearly singular
* then the dampParam is increased.
*/
protected boolean solveForXDelta() {
// double max = CommonOps.elementMax(Bdiag);
//
// // if the matrix is null do a simple gradient descent search
// if( max == 0 ) {
// for( int i = 0; i < N; i++ ) {
// xdelta.data[i] = g.data[i]/dampParam;
// }
// return true;
// }
// Adjust the dampening parameter until the solution can be solved
// This is not designed to take advantage of QR decomposition, which is why cholesky is recommended
boolean failed = true;
for( int iter = 0; iter < 1000 && failed; iter++ ) {
if( computeStep(dampParam,g,xdelta) ) {
failed = false;
}
if( failed ) {
dampParam = Math.max(10*dampParam, getMinimumDampening());
}
}
if( failed ) {
addToMessage("Failed to find dampParam which cold be solved");
return false;
}
return true;
}
/**
* Helper function that lets converged and the final message bet set in one line
*/
private boolean terminateSearch( boolean converged , String message ) {
this.hasConverged = converged;
return true;
}
/**
* Computes the residual's error:
*
* sum_i 0.5*fi(x)^2
*/
private double computeError() {
return VectorVectorMult.innerProd(funcVals,funcVals)/2.0;
}
public boolean isConverged() {
return hasConverged;
}
public int getIterationCount() {
return iterationCount;
}
public void addToMessage(String message) {
this.message += message+"\n";
}
public String getMessage() {
return message;
}
public double getFnorm() {
return fnorm;
}
public boolean isUpdatedParameters() {
return updatedParameters;
}
}
Ads help maintain this website.