optimize: make function converger an interface (#728)

* optimize: make function converger an interface

Fixes #488.
Updates #677.
This commit is contained in:
Brendan Tracey
2018-12-16 11:37:46 +01:00
committed by GitHub
parent 004553317c
commit 44a6721e0d
8 changed files with 78 additions and 37 deletions

View File

@@ -42,6 +42,7 @@ func cmaTestCases() []cmaTestCase {
}, },
settings: &Settings{ settings: &Settings{
FunctionThreshold: 0.01, FunctionThreshold: 0.01,
Converger: NeverTerminate{},
}, },
good: func(result *Result, err error, concurrent int) error { good: func(result *Result, err error, concurrent int) error {
if result.Status != FunctionThreshold { if result.Status != FunctionThreshold {
@@ -63,6 +64,7 @@ func cmaTestCases() []cmaTestCase {
method: &CmaEsChol{}, method: &CmaEsChol{},
settings: &Settings{ settings: &Settings{
FunctionThreshold: math.Inf(-1), FunctionThreshold: math.Inf(-1),
Converger: NeverTerminate{},
}, },
good: func(result *Result, err error, concurrent int) error { good: func(result *Result, err error, concurrent int) error {
if result.Status != MethodConverge { if result.Status != MethodConverge {
@@ -88,6 +90,7 @@ func cmaTestCases() []cmaTestCase {
settings: &Settings{ settings: &Settings{
FunctionThreshold: math.Inf(-1), FunctionThreshold: math.Inf(-1),
MajorIterations: 10, MajorIterations: 10,
Converger: NeverTerminate{},
}, },
good: func(result *Result, err error, concurrent int) error { good: func(result *Result, err error, concurrent int) error {
if result.Status != IterationLimit { if result.Status != IterationLimit {
@@ -117,6 +120,7 @@ func cmaTestCases() []cmaTestCase {
settings: &Settings{ settings: &Settings{
FunctionThreshold: math.Inf(-1), FunctionThreshold: math.Inf(-1),
FuncEvaluations: 250, // Somewhere in the middle of an iteration. FuncEvaluations: 250, // Somewhere in the middle of an iteration.
Converger: NeverTerminate{},
}, },
good: func(result *Result, err error, concurrent int) error { good: func(result *Result, err error, concurrent int) error {
if result.Status != FunctionEvaluationLimit { if result.Status != FunctionEvaluationLimit {
@@ -147,6 +151,7 @@ func cmaTestCases() []cmaTestCase {
}, },
settings: &Settings{ settings: &Settings{
FunctionThreshold: math.Inf(-1), FunctionThreshold: math.Inf(-1),
Converger: NeverTerminate{},
}, },
good: func(result *Result, err error, concurrent int) error { good: func(result *Result, err error, concurrent int) error {
if result.Status != MethodConverge { if result.Status != MethodConverge {
@@ -172,6 +177,7 @@ func cmaTestCases() []cmaTestCase {
}, },
settings: &Settings{ settings: &Settings{
FunctionThreshold: math.Inf(-1), FunctionThreshold: math.Inf(-1),
Converger: NeverTerminate{},
}, },
good: func(result *Result, err error, concurrent int) error { good: func(result *Result, err error, concurrent int) error {
if result.Status != MethodConverge { if result.Status != MethodConverge {

View File

@@ -4,10 +4,38 @@
package optimize package optimize
import "math" import (
"math"
)
// FunctionConverge tests for the convergence of function values. See comment // Converger returns the convergence of the optimization based on
// in Settings. // locations found during optimization. Converger must not modify the value of
// the provided Location in any of the methods.
type Converger interface {
Init(dim int)
Converged(loc *Location) Status
}
// NeverTerminate implements Converger, always reporting NotTerminated.
type NeverTerminate struct{}
func (NeverTerminate) Init(dim int) {}
func (NeverTerminate) Converged(loc *Location) Status {
return NotTerminated
}
// FunctionConverge tests for insufficient improvement in the optimum value
// over the last iterations. A FunctionConvergence status is returned if
// there is no significant decrease for FunctionConverge.Iterations. A
// significant decrease is considered if
// f < f_best
// and
// f_best - f > FunctionConverge.Relative * maxabs(f, f_best) + FunctionConverge.Absolute
// If the decrease is significant, then the iteration counter is reset and
// f_best is updated.
//
// If FunctionConverge.Iterations == 0, it has no effect.
type FunctionConverge struct { type FunctionConverge struct {
Absolute float64 Absolute float64
Relative float64 Relative float64
@@ -18,13 +46,14 @@ type FunctionConverge struct {
iter int iter int
} }
func (fc *FunctionConverge) Init() { func (fc *FunctionConverge) Init(dim int) {
fc.first = true fc.first = true
fc.best = 0 fc.best = 0
fc.iter = 0 fc.iter = 0
} }
func (fc *FunctionConverge) FunctionConverged(f float64) Status { func (fc *FunctionConverge) Converged(l *Location) Status {
f := l.F
if fc.first { if fc.first {
fc.best = f fc.best = f
fc.first = false fc.first = false

View File

@@ -12,7 +12,7 @@ import (
func DefaultSettingsGlobal() *Settings { func DefaultSettingsGlobal() *Settings {
return &Settings{ return &Settings{
FunctionThreshold: math.Inf(-1), FunctionThreshold: math.Inf(-1),
FunctionConverge: &FunctionConverge{ Converger: &FunctionConverge{
Absolute: 1e-10, Absolute: 1e-10,
Iterations: 100, Iterations: 100,
}, },

View File

@@ -48,7 +48,9 @@ func TestListSearch(t *testing.T) {
method := &ListSearch{ method := &ListSearch{
Locs: locs, Locs: locs,
} }
settings := &Settings{} settings := &Settings{
Converger: NeverTerminate{},
}
initX := make([]float64, c) initX := make([]float64, c)
result, err := Minimize(p, initX, settings, method) result, err := Minimize(p, initX, settings, method)
if err != nil { if err != nil {

View File

@@ -22,7 +22,7 @@ func ExampleMinimize() {
settings := optimize.DefaultSettingsLocal() settings := optimize.DefaultSettingsLocal()
settings.Recorder = nil settings.Recorder = nil
settings.GradientThreshold = 1e-12 settings.GradientThreshold = 1e-12
settings.FunctionConverge = nil settings.Converger = optimize.NeverTerminate{}
result, err := optimize.Minimize(p, x, settings, &optimize.BFGS{}) result, err := optimize.Minimize(p, x, settings, &optimize.BFGS{})
if err != nil { if err != nil {

View File

@@ -143,12 +143,17 @@ func Minimize(p Problem, initX []float64, settings *Settings, method Method) (*R
optLoc := newLocation(dim, method) optLoc := newLocation(dim, method)
optLoc.F = math.Inf(1) optLoc.F = math.Inf(1)
if settings.FunctionConverge != nil {
settings.FunctionConverge.Init()
}
initOp, initLoc := getInitLocation(dim, initX, settings.InitValues, method) initOp, initLoc := getInitLocation(dim, initX, settings.InitValues, method)
converger := settings.Converger
if converger == nil {
converger = &FunctionConverge{
Absolute: 1e-10,
Iterations: 100,
}
}
converger.Init(dim)
stats.Runtime = time.Since(startTime) stats.Runtime = time.Since(startTime)
// Send initial location to Recorder // Send initial location to Recorder
@@ -161,7 +166,7 @@ func Minimize(p Problem, initX []float64, settings *Settings, method Method) (*R
// Run optimization // Run optimization
var status Status var status Status
status, err = minimize(&p, method, settings, stats, initOp, initLoc, optLoc, startTime) status, err = minimize(&p, method, settings, converger, stats, initOp, initLoc, optLoc, startTime)
// Cleanup and collect results // Cleanup and collect results
if settings.Recorder != nil && err == nil { if settings.Recorder != nil && err == nil {
@@ -184,7 +189,7 @@ func getDefaultMethod(p *Problem) Method {
// minimize performs an optimization. minimize updates the settings and optLoc, // minimize performs an optimization. minimize updates the settings and optLoc,
// and returns the final Status and error. // and returns the final Status and error.
func minimize(prob *Problem, method Method, settings *Settings, stats *Stats, initOp Operation, initLoc, optLoc *Location, startTime time.Time) (Status, error) { func minimize(prob *Problem, method Method, settings *Settings, converger Converger, stats *Stats, initOp Operation, initLoc, optLoc *Location, startTime time.Time) (Status, error) {
dim := len(optLoc.X) dim := len(optLoc.X)
nTasks := settings.Concurrent nTasks := settings.Concurrent
if nTasks == 0 { if nTasks == 0 {
@@ -317,7 +322,7 @@ func minimize(prob *Problem, method Method, settings *Settings, stats *Stats, in
case NoOperation: case NoOperation:
// Just send the task back. // Just send the task back.
case MajorIteration: case MajorIteration:
status = performMajorIteration(optLoc, task.Location, stats, startTime, settings) status = performMajorIteration(optLoc, task.Location, stats, converger, startTime, settings)
case MethodDone: case MethodDone:
methodDone = true methodDone = true
status = MethodConverge status = MethodConverge
@@ -504,7 +509,7 @@ func updateEvaluationStats(stats *Stats, op Operation) {
// the convergence criteria given by settings. Otherwise a corresponding status is // the convergence criteria given by settings. Otherwise a corresponding status is
// returned. // returned.
// Unlike checkLimits, checkConvergence is called only at MajorIterations. // Unlike checkLimits, checkConvergence is called only at MajorIterations.
func checkLocationConvergence(loc *Location, settings *Settings) Status { func checkLocationConvergence(loc *Location, settings *Settings, converger Converger) Status {
if math.IsInf(loc.F, -1) { if math.IsInf(loc.F, -1) {
return FunctionNegativeInfinity return FunctionNegativeInfinity
} }
@@ -517,10 +522,7 @@ func checkLocationConvergence(loc *Location, settings *Settings) Status {
if loc.F < settings.FunctionThreshold { if loc.F < settings.FunctionThreshold {
return FunctionThreshold return FunctionThreshold
} }
if settings.FunctionConverge != nil { return converger.Converged(loc)
return settings.FunctionConverge.FunctionConverged(loc.F)
}
return NotTerminated
} }
// checkEvaluationLimits checks the optimization limits after an evaluation // checkEvaluationLimits checks the optimization limits after an evaluation
@@ -559,11 +561,11 @@ func checkIterationLimits(loc *Location, stats *Stats, settings *Settings) Statu
// performMajorIteration does all of the steps needed to perform a MajorIteration. // performMajorIteration does all of the steps needed to perform a MajorIteration.
// It increments the iteration count, updates the optimal location, and checks // It increments the iteration count, updates the optimal location, and checks
// the necessary convergence criteria. // the necessary convergence criteria.
func performMajorIteration(optLoc, loc *Location, stats *Stats, startTime time.Time, settings *Settings) Status { func performMajorIteration(optLoc, loc *Location, stats *Stats, converger Converger, startTime time.Time, settings *Settings) Status {
copyLocation(optLoc, loc) copyLocation(optLoc, loc)
stats.MajorIterations++ stats.MajorIterations++
stats.Runtime = time.Since(startTime) stats.Runtime = time.Since(startTime)
status := checkLocationConvergence(optLoc, settings) status := checkLocationConvergence(optLoc, settings, converger)
if status != NotTerminated { if status != NotTerminated {
return status return status
} }

View File

@@ -185,18 +185,18 @@ type Settings struct {
// The default value is 1e-6. // The default value is 1e-6.
GradientThreshold float64 GradientThreshold float64
// FunctionConverge tests that the function value decreases by a // Converger checks if the optimization has converged based on the (history
// significant amount over the specified number of iterations. // of) locations found during the optimization. Minimize will pass the
// Location at every MajorIteration to the Converger.
// //
// If f < f_best and // If the Converger is nil, a default value of
// f_best - f > FunctionConverge.Relative * maxabs(f, f_best) + FunctionConverge.Absolute // FunctionConverge {
// then a significant decrease has occurred, and f_best is updated. // Absolute: 1e-10,
// // Iterations: 100,
// If there is no significant decrease for FunctionConverge.Iterations // }
// major iterations, FunctionConvergence status is returned. // will be used. NeverTerminated can be used to always return a
// // NotTerminated status.
// If this is nil or if FunctionConverge.Iterations == 0, it has no effect. Converger Converger
FunctionConverge *FunctionConverge
// MajorIterations is the maximum number of iterations allowed. // MajorIterations is the maximum number of iterations allowed.
// IterationLimit status is returned if the number of major iterations // IterationLimit status is returned if the number of major iterations
@@ -245,7 +245,7 @@ func DefaultSettingsLocal() *Settings {
return &Settings{ return &Settings{
GradientThreshold: defaultGradientAbsTol, GradientThreshold: defaultGradientAbsTol,
FunctionThreshold: math.Inf(-1), FunctionThreshold: math.Inf(-1),
FunctionConverge: &FunctionConverge{ Converger: &FunctionConverge{
Absolute: 1e-10, Absolute: 1e-10,
Iterations: 20, Iterations: 20,
}, },

View File

@@ -1164,16 +1164,18 @@ func testLocal(t *testing.T, tests []unconstrainedTest, method Method) {
settings.Recorder = nil settings.Recorder = nil
if method != nil && method.Needs().Gradient { if method != nil && method.Needs().Gradient {
// Turn off function convergence checks for gradient-based methods. // Turn off function convergence checks for gradient-based methods.
settings.FunctionConverge = nil settings.Converger = NeverTerminate{}
} else { } else {
if test.fIter == 0 { if test.fIter == 0 {
test.fIter = 20 test.fIter = 20
} }
settings.FunctionConverge.Iterations = test.fIter c := settings.Converger.(*FunctionConverge)
c.Iterations = test.fIter
if test.fAbsTol == 0 { if test.fAbsTol == 0 {
test.fAbsTol = 1e-12 test.fAbsTol = 1e-12
} }
settings.FunctionConverge.Absolute = test.fAbsTol c.Absolute = test.fAbsTol
settings.Converger = c
} }
if test.gradTol == 0 { if test.gradTol == 0 {
test.gradTol = 1e-12 test.gradTol = 1e-12