mirror of
https://github.com/gonum/gonum.git
synced 2025-10-03 14:26:36 +08:00
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
// Copyright ©2014 The gonum Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package optimize
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
// Local finds a local minimum of a minimization problem using a sequential
|
|
// algorithm. A maximization problem can be transformed into a minimization
|
|
// problem by multiplying the function by -1.
|
|
//
|
|
// The first argument represents the problem to be minimized. Its fields are
|
|
// routines that evaluate the objective function, gradient, and other
|
|
// quantities related to the problem. The objective function, p.Func, must not
|
|
// be nil. The optimization method used may require other fields to be non-nil
|
|
// as specified by method.Needs. Local will panic if these are not met. The
|
|
// method can be determined automatically from the supplied problem which is
|
|
// described below.
|
|
//
|
|
// If p.Status is not nil, it is called before every evaluation. If the
|
|
// returned Status is not NotTerminated or the error is not nil, the
|
|
// optimization run is terminated.
|
|
//
|
|
// The second argument is the initial location at which to start the minimization.
|
|
// The initial location must be supplied, and must have a length equal to the
|
|
// problem dimension.
|
|
//
|
|
// The third argument contains the settings for the minimization. It is here that
|
|
// gradient tolerance, etc. are specified. The DefaultSettings function
|
|
// can be called for a Settings struct with the default values initialized.
|
|
// If settings == nil, the default settings are used. See the documentation
|
|
// for the Settings structure for more information. The optimization Method used
|
|
// may also contain settings, see documentation for the appropriate optimizer.
|
|
//
|
|
// The final argument is the optimization method to use. If method == nil, then
|
|
// an appropriate default is chosen based on the properties of the other arguments
|
|
// (dimension, gradient-free or gradient-based, etc.). The optimization
|
|
// methods in this package are designed such that reasonable defaults occur
|
|
// if options are not specified explicitly. For example, the code
|
|
// method := &optimize.BFGS{}
|
|
// creates a pointer to a new BFGS struct. When Local is called, the settings
|
|
// in the method will be populated with default values. The methods are also
|
|
// designed such that they can be reused in future calls to Local.
|
|
//
|
|
// If method implements Statuser, method.Status is called before every call
|
|
// to method.Iterate. If the returned Status is not NotTerminated or the
|
|
// error is non-nil, the optimization run is terminated.
|
|
//
|
|
// Local returns a Result struct and any error that occurred. See the
|
|
// documentation of Result for more information.
|
|
//
|
|
// Be aware that the default behavior of Local is to find the minimum.
|
|
// For certain functions and optimization methods, this process can take many
|
|
// function evaluations. If you would like to put limits on this, for example
|
|
// maximum runtime or maximum function evaluations, modify the Settings
|
|
// input struct.
|
|
func Local(p Problem, initX []float64, settings *Settings, method Method) (*Result, error) {
|
|
startTime := time.Now()
|
|
dim := len(initX)
|
|
if method == nil {
|
|
method = getDefaultMethod(&p)
|
|
}
|
|
if settings == nil {
|
|
settings = DefaultSettings()
|
|
}
|
|
|
|
stats := &Stats{}
|
|
|
|
err := checkOptimization(p, dim, method, settings.Recorder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
optLoc, err := getStartingLocation(&p, method, initX, stats, settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if settings.FunctionConverge != nil {
|
|
settings.FunctionConverge.Init(optLoc.F)
|
|
}
|
|
|
|
stats.Runtime = time.Since(startTime)
|
|
|
|
// Send initial location to Recorder
|
|
if settings.Recorder != nil {
|
|
err = settings.Recorder.Record(optLoc, InitIteration, stats)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Check if the starting location satisfies the convergence criteria.
|
|
status := checkConvergence(optLoc, settings, true)
|
|
|
|
// Run optimization
|
|
if status == NotTerminated && err == nil {
|
|
// The starting location is not good enough, we need to perform a
|
|
// minimization. The optimal location will be stored in-place in
|
|
// optLoc.
|
|
status, err = minimize(&p, method, settings, stats, optLoc, startTime)
|
|
}
|
|
|
|
// Cleanup and collect results
|
|
if settings.Recorder != nil && err == nil {
|
|
// Send the optimal location to Recorder.
|
|
err = settings.Recorder.Record(optLoc, PostIteration, stats)
|
|
}
|
|
stats.Runtime = time.Since(startTime)
|
|
return &Result{
|
|
Location: *optLoc,
|
|
Stats: *stats,
|
|
Status: status,
|
|
}, err
|
|
}
|
|
|
|
func minimize(p *Problem, method Method, settings *Settings, stats *Stats, optLoc *Location, startTime time.Time) (status Status, err error) {
|
|
loc := &Location{}
|
|
copyLocation(loc, optLoc)
|
|
x := make([]float64, len(loc.X))
|
|
|
|
statuser, _ := method.(Statuser)
|
|
|
|
var op Operation
|
|
op, err = method.Init(loc)
|
|
if err != nil {
|
|
status = Failure
|
|
return
|
|
}
|
|
|
|
for {
|
|
// Sequentially call method.Iterate, performing the operations it has
|
|
// commanded, until convergence.
|
|
|
|
switch op {
|
|
case NoOperation:
|
|
case InitIteration:
|
|
panic("optimize: Method returned InitIteration")
|
|
case PostIteration:
|
|
panic("optimize: Method returned PostIteration")
|
|
case MajorIteration:
|
|
copyLocation(optLoc, loc)
|
|
stats.MajorIterations++
|
|
status = checkConvergence(optLoc, settings, true)
|
|
default: // Any of the Evaluation operations.
|
|
status, err = evaluate(p, loc, op, x)
|
|
updateEvaluationStats(stats, op)
|
|
}
|
|
|
|
status, err = finishIteration(status, err, stats, settings, statuser, startTime, loc, op)
|
|
if status != NotTerminated || err != nil {
|
|
return
|
|
}
|
|
|
|
op, err = method.Iterate(loc)
|
|
if err != nil {
|
|
status = Failure
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func getDefaultMethod(p *Problem) Method {
|
|
if p.Grad != nil {
|
|
return &BFGS{}
|
|
}
|
|
return &NelderMead{}
|
|
}
|
|
|
|
// getStartingLocation allocates and initializes the starting location for the minimization.
|
|
func getStartingLocation(p *Problem, method Method, initX []float64, stats *Stats, settings *Settings) (*Location, error) {
|
|
dim := len(initX)
|
|
loc := newLocation(dim, method)
|
|
copy(loc.X, initX)
|
|
|
|
if settings.UseInitialData {
|
|
loc.F = settings.InitialValue
|
|
if loc.Gradient != nil {
|
|
initG := settings.InitialGradient
|
|
if initG == nil {
|
|
panic("optimize: initial gradient is nil")
|
|
}
|
|
if len(initG) != dim {
|
|
panic("optimize: initial gradient size mismatch")
|
|
}
|
|
copy(loc.Gradient, initG)
|
|
}
|
|
if loc.Hessian != nil {
|
|
initH := settings.InitialHessian
|
|
if initH == nil {
|
|
panic("optimize: initial Hessian is nil")
|
|
}
|
|
if initH.Symmetric() != dim {
|
|
panic("optimize: initial Hessian size mismatch")
|
|
}
|
|
loc.Hessian.CopySym(initH)
|
|
}
|
|
} else {
|
|
eval := FuncEvaluation
|
|
if loc.Gradient != nil {
|
|
eval |= GradEvaluation
|
|
}
|
|
if loc.Hessian != nil {
|
|
eval |= HessEvaluation
|
|
}
|
|
x := make([]float64, len(loc.X))
|
|
evaluate(p, loc, eval, x)
|
|
updateEvaluationStats(stats, eval)
|
|
}
|
|
|
|
if math.IsInf(loc.F, 1) || math.IsNaN(loc.F) {
|
|
return loc, ErrFunc(loc.F)
|
|
}
|
|
for i, v := range loc.Gradient {
|
|
if math.IsInf(v, 0) || math.IsNaN(v) {
|
|
return loc, ErrGrad{Grad: v, Index: i}
|
|
}
|
|
}
|
|
|
|
return loc, nil
|
|
}
|