// 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" "gonum.org/v1/gonum/floats" ) // LinesearchMethod represents an abstract optimization method in which a // function is optimized through successive line search optimizations. type LinesearchMethod struct { // NextDirectioner specifies the search direction of each linesearch. NextDirectioner NextDirectioner // Linesearcher performs a linesearch along the search direction. Linesearcher Linesearcher x []float64 // Starting point for the current iteration. dir []float64 // Search direction for the current iteration. first bool // Indicator of the first iteration. nextMajor bool // Indicates that MajorIteration must be commanded at the next call to Iterate. eval Operation // Indicator of valid fields in Location. lastStep float64 // Step taken from x in the previous call to Iterate. lastOp Operation // Operation returned from the previous call to Iterate. } func (ls *LinesearchMethod) Init(loc *Location) (Operation, error) { if loc.Gradient == nil { panic("linesearch: gradient is nil") } dim := len(loc.X) ls.x = resize(ls.x, dim) ls.dir = resize(ls.dir, dim) ls.first = true ls.nextMajor = false // Indicate that all fields of loc are valid. ls.eval = FuncEvaluation | GradEvaluation if loc.Hessian != nil { ls.eval |= HessEvaluation } ls.lastStep = math.NaN() ls.lastOp = NoOperation return ls.initNextLinesearch(loc) } func (ls *LinesearchMethod) Iterate(loc *Location) (Operation, error) { switch ls.lastOp { case NoOperation: // TODO(vladimir-ch): Either Init has not been called, or the caller is // trying to resume the optimization run after Iterate previously // returned with an error. Decide what is the proper thing to do. See also #125. case MajorIteration: // The previous updated location did not converge the full // optimization. Initialize a new Linesearch. return ls.initNextLinesearch(loc) default: // Update the indicator of valid fields of loc. ls.eval |= ls.lastOp if ls.nextMajor { ls.nextMajor = false // Linesearcher previously finished, and the invalid fields of loc // have now been validated. Announce MajorIteration. ls.lastOp = MajorIteration return ls.lastOp, nil } } // Continue the linesearch. f := math.NaN() if ls.eval&FuncEvaluation != 0 { f = loc.F } projGrad := math.NaN() if ls.eval&GradEvaluation != 0 { projGrad = floats.Dot(loc.Gradient, ls.dir) } op, step, err := ls.Linesearcher.Iterate(f, projGrad) if err != nil { return ls.error(err) } switch op { case MajorIteration: // Linesearch has been finished. ls.lastOp = complementEval(loc, ls.eval) if ls.lastOp == NoOperation { // loc is complete, MajorIteration can be declared directly. ls.lastOp = MajorIteration } else { // Declare MajorIteration on the next call to Iterate. ls.nextMajor = true } case FuncEvaluation, GradEvaluation, FuncEvaluation | GradEvaluation: if step != ls.lastStep { // We are moving to a new location, and not, say, evaluating extra // information at the current location. // Compute the next evaluation point and store it in loc.X. floats.AddScaledTo(loc.X, ls.x, step, ls.dir) if floats.Equal(ls.x, loc.X) { // Step size has become so small that the next evaluation point is // indistinguishable from the starting point for the current // iteration due to rounding errors. return ls.error(ErrNoProgress) } ls.lastStep = step ls.eval = NoOperation // Indicate all invalid fields of loc. } ls.lastOp = op default: panic("linesearch: Linesearcher returned invalid operation") } return ls.lastOp, nil } func (ls *LinesearchMethod) error(err error) (Operation, error) { ls.lastOp = NoOperation return ls.lastOp, err } // initNextLinesearch initializes the next linesearch using the previous // complete location stored in loc. It fills loc.X and returns an evaluation // to be performed at loc.X. func (ls *LinesearchMethod) initNextLinesearch(loc *Location) (Operation, error) { copy(ls.x, loc.X) var step float64 if ls.first { ls.first = false step = ls.NextDirectioner.InitDirection(loc, ls.dir) } else { step = ls.NextDirectioner.NextDirection(loc, ls.dir) } projGrad := floats.Dot(loc.Gradient, ls.dir) if projGrad >= 0 { return ls.error(ErrNonDescentDirection) } op := ls.Linesearcher.Init(loc.F, projGrad, step) switch op { case FuncEvaluation, GradEvaluation, FuncEvaluation | GradEvaluation: default: panic("linesearch: Linesearcher returned invalid operation") } floats.AddScaledTo(loc.X, ls.x, step, ls.dir) if floats.Equal(ls.x, loc.X) { // Step size is so small that the next evaluation point is // indistinguishable from the starting point for the current iteration // due to rounding errors. return ls.error(ErrNoProgress) } ls.lastStep = step ls.eval = NoOperation // Invalidate all fields of loc. ls.lastOp = op return ls.lastOp, nil } // ArmijoConditionMet returns true if the Armijo condition (aka sufficient // decrease) has been met. Under normal conditions, the following should be // true, though this is not enforced: // - initGrad < 0 // - step > 0 // - 0 < decrease < 1 func ArmijoConditionMet(currObj, initObj, initGrad, step, decrease float64) bool { return currObj <= initObj+decrease*step*initGrad } // StrongWolfeConditionsMet returns true if the strong Wolfe conditions have been met. // The strong Wolfe conditions ensure sufficient decrease in the function // value, and sufficient decrease in the magnitude of the projected gradient. // Under normal conditions, the following should be true, though this is not // enforced: // - initGrad < 0 // - step > 0 // - 0 <= decrease < curvature < 1 func StrongWolfeConditionsMet(currObj, currGrad, initObj, initGrad, step, decrease, curvature float64) bool { if currObj > initObj+decrease*step*initGrad { return false } return math.Abs(currGrad) < curvature*math.Abs(initGrad) } // WeakWolfeConditionsMet returns true if the weak Wolfe conditions have been met. // The weak Wolfe conditions ensure sufficient decrease in the function value, // and sufficient decrease in the value of the projected gradient. Under normal // conditions, the following should be true, though this is not enforced: // - initGrad < 0 // - step > 0 // - 0 <= decrease < curvature< 1 func WeakWolfeConditionsMet(currObj, currGrad, initObj, initGrad, step, decrease, curvature float64) bool { if currObj > initObj+decrease*step*initGrad { return false } return currGrad >= curvature*initGrad }