//
// Math.NET Numerics, part of the Math.NET Project
// http://numerics.mathdotnet.com
// http://github.com/mathnet/mathnet-numerics
//
// Copyright (c) 2009-2017 Math.NET
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Collections.Generic;
using IStation.Numerics.LinearAlgebra;
using IStation.Numerics.LinearAlgebra.Double;
using IStation.Numerics.Optimization.LineSearch;
namespace IStation.Numerics.Optimization
{
///
/// Broyden–Fletcher–Goldfarb–Shanno Bounded (BFGS-B) algorithm is an iterative method for solving box-constrained nonlinear optimization problems
/// http://www.ece.northwestern.edu/~nocedal/PSfiles/limited.ps.gz
///
public class BfgsBMinimizer : BfgsMinimizerBase
{
public BfgsBMinimizer(double gradientTolerance, double parameterTolerance, double functionProgressTolerance, int maximumIterations = 1000)
: base(gradientTolerance,parameterTolerance,functionProgressTolerance,maximumIterations)
{
}
///
/// Find the minimum of the objective function given lower and upper bounds
///
/// The objective function, must support a gradient
/// The lower bound
/// The upper bound
/// The initial guess
/// The MinimizationResult which contains the minimum and the ExitCondition
public MinimizationResult FindMinimum(IObjectiveFunction objective, Vector lowerBound, Vector upperBound, Vector initialGuess)
{
_lowerBound = lowerBound;
_upperBound = upperBound;
if (!objective.IsGradientSupported)
throw new IncompatibleObjectiveException("Gradient not supported in objective function, but required for BFGS minimization.");
// Check that dimensions match
if (lowerBound.Count != upperBound.Count || lowerBound.Count != initialGuess.Count)
throw new ArgumentException("Dimensions of bounds and/or initial guess do not match.");
// Check that initial guess is feasible
for (int ii = 0; ii < initialGuess.Count; ++ii)
if (initialGuess[ii] < lowerBound[ii] || initialGuess[ii] > upperBound[ii])
throw new ArgumentException("Initial guess is not in the feasible region");
objective.EvaluateAt(initialGuess);
ValidateGradientAndObjective(objective);
// Check that we're not already done
var currentExitCondition = ExitCriteriaSatisfied(objective, null, 0);
if (currentExitCondition != ExitCondition.None)
return new MinimizationResult(objective, 0, currentExitCondition);
// Set up line search algorithm
var lineSearcher = new StrongWolfeLineSearch(1e-4, 0.9, Math.Max(ParameterTolerance, 1e-5), maxIterations: 1000);
// Declare state variables
Vector reducedSolution1, reducedGradient, reducedInitialPoint, reducedCauchyPoint, solution1;
Matrix reducedHessian;
List reducedMap;
// First step
var pseudoHessian = CreateMatrix.DiagonalIdentity(initialGuess.Count);
// Determine active set
var gradientProjectionResult = QuadraticGradientProjectionSearch.Search(objective.Point, objective.Gradient, pseudoHessian, lowerBound, upperBound);
var cauchyPoint = gradientProjectionResult.CauchyPoint;
var fixedCount = gradientProjectionResult.FixedCount;
var isFixed = gradientProjectionResult.IsFixed;
var freeCount = lowerBound.Count - fixedCount;
if (freeCount > 0)
{
reducedGradient = new DenseVector(freeCount);
reducedHessian = new DenseMatrix(freeCount, freeCount);
reducedMap = new List(freeCount);
reducedInitialPoint = new DenseVector(freeCount);
reducedCauchyPoint = new DenseVector(freeCount);
CreateReducedData(objective.Point, cauchyPoint, isFixed, lowerBound, upperBound, objective.Gradient, pseudoHessian, reducedInitialPoint, reducedCauchyPoint, reducedGradient, reducedHessian, reducedMap);
// Determine search direction and maximum step size
reducedSolution1 = reducedInitialPoint + reducedHessian.Cholesky().Solve(-reducedGradient);
solution1 = ReducedToFull(reducedMap, reducedSolution1, cauchyPoint);
}
else
{
solution1 = cauchyPoint;
}
var directionFromCauchy = solution1 - cauchyPoint;
var maxStepFromCauchyPoint = FindMaxStep(cauchyPoint, directionFromCauchy, lowerBound, upperBound);
var solution2 = cauchyPoint + Math.Min(maxStepFromCauchyPoint, 1.0)*directionFromCauchy;
var lineSearchDirection = solution2 - objective.Point;
var maxLineSearchStep = FindMaxStep(objective.Point, lineSearchDirection, lowerBound, upperBound);
var estStepSize = -objective.Gradient*lineSearchDirection/(lineSearchDirection*pseudoHessian*lineSearchDirection);
var startingStepSize = Math.Min(Math.Max(estStepSize, 1.0), maxLineSearchStep);
// Line search
LineSearchResult lineSearchResult;
try
{
lineSearchResult = lineSearcher.FindConformingStep(objective, lineSearchDirection, startingStepSize, upperBound: maxLineSearchStep);
}
catch (Exception e)
{
throw new InnerOptimizationException("Line search failed.", e);
}
var previousPoint = objective.Fork();
var candidatePoint = lineSearchResult.FunctionInfoAtMinimum;
ValidateGradientAndObjective(candidatePoint);
// Check that we're not done
currentExitCondition = ExitCriteriaSatisfied(candidatePoint, previousPoint, 0);
if (currentExitCondition != ExitCondition.None)
return new MinimizationResult(candidatePoint, 0, currentExitCondition);
var gradient = candidatePoint.Gradient;
var step = candidatePoint.Point - initialGuess;
// Subsequent steps
int totalLineSearchSteps = lineSearchResult.Iterations;
int iterationsWithNontrivialLineSearch = lineSearchResult.Iterations > 0 ? 0 : 1;
int iterations = DoBfgsUpdate(ref currentExitCondition, lineSearcher, ref pseudoHessian, ref lineSearchDirection, ref previousPoint, ref lineSearchResult, ref candidatePoint, ref step, ref totalLineSearchSteps, ref iterationsWithNontrivialLineSearch);
if (iterations == MaximumIterations && currentExitCondition == ExitCondition.None)
throw new MaximumIterationsException(FormattableString.Invariant($"Maximum iterations ({MaximumIterations}) reached."));
return new MinimizationWithLineSearchResult(candidatePoint, iterations, currentExitCondition, totalLineSearchSteps, iterationsWithNontrivialLineSearch);
}
protected override Vector CalculateSearchDirection(ref Matrix pseudoHessian,
out double maxLineSearchStep,
out double startingStepSize,
IObjectiveFunction previousPoint,
IObjectiveFunction candidatePoint,
Vector step)
{
Vector lineSearchDirection;
var y = candidatePoint.Gradient - previousPoint.Gradient;
double sy = step * y;
if (sy > 0.0) // only do update if it will create a positive definite matrix
{
double sts = step * step;
var Hs = pseudoHessian * step;
var sHs = step * pseudoHessian * step;
pseudoHessian = pseudoHessian + y.OuterProduct(y) * (1.0 / sy) - Hs.OuterProduct(Hs) * (1.0 / sHs);
}
else
{
//pseudo_hessian = LinearAlgebra.Double.DiagonalMatrix.Identity(initial_guess.Count);
}
// Determine active set
var gradientProjectionResult = QuadraticGradientProjectionSearch.Search(candidatePoint.Point, candidatePoint.Gradient, pseudoHessian, _lowerBound, _upperBound);
var cauchyPoint = gradientProjectionResult.CauchyPoint;
var fixedCount = gradientProjectionResult.FixedCount;
var isFixed = gradientProjectionResult.IsFixed;
var freeCount = _lowerBound.Count - fixedCount;
Vector solution1;
if (freeCount > 0)
{
var reducedGradient = new DenseVector(freeCount);
var reducedHessian = new DenseMatrix(freeCount, freeCount);
var reducedMap = new List(freeCount);
var reducedInitialPoint = new DenseVector(freeCount);
var reducedCauchyPoint = new DenseVector(freeCount);
CreateReducedData(candidatePoint.Point, cauchyPoint, isFixed, _lowerBound, _upperBound, candidatePoint.Gradient, pseudoHessian, reducedInitialPoint, reducedCauchyPoint, reducedGradient, reducedHessian, reducedMap);
// Determine search direction and maximum step size
Vector reducedSolution1 = reducedInitialPoint + reducedHessian.Cholesky().Solve(-reducedGradient);
solution1 = ReducedToFull(reducedMap, reducedSolution1, cauchyPoint);
}
else
{
solution1 = cauchyPoint;
}
var directionFromCauchy = solution1 - cauchyPoint;
var maxStepFromCauchyPoint = FindMaxStep(cauchyPoint, directionFromCauchy, _lowerBound, _upperBound);
var solution2 = cauchyPoint + Math.Min(maxStepFromCauchyPoint, 1.0) * directionFromCauchy;
lineSearchDirection = solution2 - candidatePoint.Point;
maxLineSearchStep = FindMaxStep(candidatePoint.Point, lineSearchDirection, _lowerBound, _upperBound);
if (maxLineSearchStep == 0.0)
{
lineSearchDirection = cauchyPoint - candidatePoint.Point;
maxLineSearchStep = FindMaxStep(candidatePoint.Point, lineSearchDirection, _lowerBound, _upperBound);
}
double estStepSize = -candidatePoint.Gradient * lineSearchDirection / (lineSearchDirection * pseudoHessian * lineSearchDirection);
startingStepSize = Math.Min(Math.Max(estStepSize, 1.0), maxLineSearchStep);
return lineSearchDirection;
}
static Vector ReducedToFull(List reducedMap, Vector reducedVector, Vector fullVector)
{
var output = fullVector.Clone();
for (int ii = 0; ii < reducedMap.Count; ++ii)
output[reducedMap[ii]] = reducedVector[ii];
return output;
}
Vector _lowerBound;
Vector _upperBound;
static double FindMaxStep(Vector startingPoint, Vector searchDirection, Vector lowerBound, Vector upperBound)
{
double maxStep = Double.PositiveInfinity;
for (int ii = 0; ii < startingPoint.Count; ++ii)
{
double paramMaxStep;
if (searchDirection[ii] > 0)
paramMaxStep = (upperBound[ii] - startingPoint[ii])/searchDirection[ii];
else if (searchDirection[ii] < 0)
paramMaxStep = (startingPoint[ii] - lowerBound[ii])/-searchDirection[ii];
else
paramMaxStep = Double.PositiveInfinity;
if (paramMaxStep < maxStep)
maxStep = paramMaxStep;
}
return maxStep;
}
static void CreateReducedData(Vector initialPoint, Vector cauchyPoint, List isFixed, Vector lowerBound, Vector upperBound, Vector gradient, Matrix pseudoHessian, Vector reducedInitialPoint, Vector reducedCauchyPoint, Vector reducedGradient, Matrix reducedHessian, List reducedMap)
{
int ll = 0;
for (int ii = 0; ii < lowerBound.Count; ++ii)
{
if (!isFixed[ii])
{
// hessian
int mm = 0;
for (int jj = 0; jj < lowerBound.Count; ++jj)
{
if (!isFixed[jj])
{
reducedHessian[ll, mm++] = pseudoHessian[ii, jj];
}
}
// gradient
reducedInitialPoint[ll] = initialPoint[ii];
reducedCauchyPoint[ll] = cauchyPoint[ii];
reducedGradient[ll] = gradient[ii];
ll += 1;
reducedMap.Add(ii);
}
}
}
protected override double GetProjectedGradient(IObjectiveFunctionEvaluation candidatePoint, int ii)
{
double projectedGradient;
bool atLowerBound = candidatePoint.Point[ii] - _lowerBound[ii] < VerySmall;
bool atUpperBound = _upperBound[ii] - candidatePoint.Point[ii] < VerySmall;
if (atLowerBound && atUpperBound)
projectedGradient = 0.0;
else if (atLowerBound)
projectedGradient = Math.Min(candidatePoint.Gradient[ii], 0.0);
else if (atUpperBound)
projectedGradient = Math.Max(candidatePoint.Gradient[ii], 0.0);
else
projectedGradient = base.GetProjectedGradient(candidatePoint, ii);
return projectedGradient;
}
}
}