oct versions of BFGSMin
Michael Creel <[email protected]> Wed, 03 Mar 2004 16:12:33 +0100
| Newsgroups | gmane.comp.gnu.octave.sources |
|---|---|
| Organization | UAB |
| Message-ID | <[email protected]> |
Hello, I've spent the last few days learning how to create oct files, and have some results. The attached files allow one to do bfgs minimization of a function, using numeric derivatives. Any comments or suggestions, especially about how to do type checking, would be welcome. These are preliminary versions. They work, but I expect to have more polished versions soon. To use them (with linux, don't know about windows), type "make all" from the shell prompt. Then type "octave ExampleBFGSMin.m" Regards, Michael
Makefile
(text/x-makefile, 136 B)
# Makefile optimization stuff .phony: all all: BisectionStep.oct NewtonStep.oct NumGradient.oct BFGSMin.oct %.oct: %.cc mkoctfile $<
BFGSMin.cc
(text/x-c++src, 8.5 KB)
// ============================ BFGSMin.m =====================================
//
// Minimize a function of the form
//
// [obj_value, ...] = f(arg, otherargs)
//
// or
//
// [obj_value, ...] = f(arg)
//
// with respect to arg. The first return of f() must be the value of f().
// f() may have other returns, but they are not used here.
//
// Calling syntax:
//
// [theta, obj_value, iters, convergence] = BFGSMin(f, arg, otherargs, control)
// or
// [theta, obj_value, iters, convergence] = BFGSMin(f, arg, {1}, control)
//
// NOTE: the last argument (control) is optional
//
//
// Input arguments:
//
// REQUIRED
// f - (string) the function to minimize
// arg - the arg we min with respect to, a vector
// otherargs - a cell array of other arguments of func, or a placeholder, e.g., {1}
//
// OPTIONAL
// control - 3x1 vector
// * 1st elem. controls maximum iterations
// scalar > 0 - max. iters
// or -1 for infinity (default)
// * 2nd elem. is verbosity control
// 0 = no results printed (default)
// 1 = intermediate results
// 2 = only last iteration results
// * 3rd elem. specifies convergence criterion
// 1 = function, gradient, and parameter change
// are all tested (default)
// 0 = only function conv tested
//
// Outputs:
// theta - the minimizer
// obj_value - the minimized funtion value
// iters - number of iterations used
// convergence (1 = success, 0 = failure)
//#include <stdio.h>
#include <oct.h>
#include <octave/parse.h>
DEFUN_DLD(BFGSMin, args, , "BFGSMin.cc")
{
std::string f = args(0).string_value();
Matrix theta = args(1).matrix_value();
octave_value otherargs = args(2);
octave_value_list f_args(2,1);
octave_value_list g_args = args;
octave_value_list step_args(4,1);
octave_value_list f_return;
int criterion, max_iters, convergence, verbosity, iter;
int gradient_failed, i, j, conv1, conv2, conv3;
int k = theta.rows();
double func_tol, param_tol, gradient_tol, stepsize, obj_value;
double last_obj_value, denominator, test, tempscalar;
Matrix control, thetain, thetanew, H, g, d, g_new, p, q, temp, H1, H2;
f_args(0) = theta;
f_args(1) = otherargs;
step_args(0) = f; // the 4 elements are func, direction, theta, otherargs
step_args(3) = otherargs;
// Check number of args
if (args.length() > 4) error("\nBFGSMin: you have provided more than 4 arguments\n");
if (args.length() < 3) error("\nBFGSMin: you have provided fewer than 3 arguments\n");
// Type checking: not done yet, since I don't know how to do it
// // Check types of required arguments
// if !ischar(func) error("\nBFGSMin: first argument must be a string that gives name of objective function\n"); endif
// if !isvector(theta) error("\nBFGSMin: second argument must be a vector\n"); endif
// if !iscell(otherargs) error("\nBFGSMin: third argument must be a cell array\n\
// (even if it's just placeholder such as {1}\n"); endif
// if (!is_matrix_type(control)) error("\nBFGSMin: If 4 arguments passed, the 4th must be control vector\n"); endif
// if (control.rows() != 3) error("\nBFGSMin: control (4th argument) must be a 3x1 vector\n"); endif
// tolerances
func_tol = 10*sqrt(2.22e-16);
param_tol = 1e-5;
gradient_tol = 1e-4;
// Default values for controls
max_iters = INT_MAX; // no limit on iterations
verbosity = 0; // by default don't report results to screen
criterion = 1; // strong convergence required
// use provided controls, if applicable
if (args.length() == 4)
{
control = args(3).matrix_value();
max_iters = (int) control(0);
if (max_iters == -1) max_iters = INT_MAX;
verbosity = (int) control(1);
criterion = (int) control(2);
}
// initialize things
convergence = -1; // if this doesn't change, it means that maxiters were exceeded
thetain = theta;
H = identity_matrix(k,k);
// Initial obj_value
f_return = feval(f, f_args);
last_obj_value = f_return(0).double_value();
// initial gradient
f_return = feval("NumGradient", args);
g = f_return(0).matrix_value();
g = g.transpose();
// check that gradient is ok
gradient_failed = 0; // test = 1 means gradient failed
for (i=0; i<k;i++)
{
gradient_failed = gradient_failed + isnan(g(i));
}
if (gradient_failed)
{
error("BFGSMin: Initial gradient could not be calculated: exiting");
convergence = 2;
}
// MAIN LOOP STARTS HERE
for (iter = 0; iter < max_iters; iter++)
{
d = -H*g;
// Regular step
step_args(1) = d;
step_args(2) = theta;
f_return = feval("NewtonStep", step_args);
stepsize = f_return(0).double_value();
obj_value = f_return(1).double_value();
// Steepest descent if regular step fails
if (isnan(stepsize))
{
d = -g; // try steepest descent
step_args(1) = d;
f_return = feval("NewtonStep", step_args);
stepsize = f_return(0).double_value();
obj_value = f_return(1).double_value();
warning("BFGSMin: Stepsize failure in BFGS direction, trying steepest descent direction");
// if that didn't work, were out of luck
if (isnan(stepsize))
{
warning("BFGSMin: unable to find direction of improvement: exiting");
theta = thetain;
convergence = 2;
break;
}
}
p = stepsize*d;
// check normal convergence: all 3 must be satisfied
// function convergence
if (fabs(last_obj_value) > 1.0)
{
conv1 = (fabs(obj_value - last_obj_value)/fabs(last_obj_value)) < func_tol;
}
else
{
conv1 = fabs(obj_value - last_obj_value) < func_tol;
}
// parameter change convergence
temp = theta.transpose() * theta;
test = sqrt(temp(0,0));
if (test > 1)
{
temp = p.transpose() * p;
conv2 = (temp(0,0) / test) < param_tol ;
}
else
{
temp = p.transpose() * p;
conv2 = temp(0,0) < param_tol;
}
// gradient convergence
temp = g.transpose() * g;
conv3 = sqrt(temp(0,0)) < gradient_tol;
// Want intermediate results?
if (verbosity == 1)
{
printf("\nBFGSMin intermediate results: Iteration %d\n",iter);
printf("Stepsize %8.7f\n", stepsize);
printf("Objective function value %16.10f\n", last_obj_value);
printf("Function conv %d Param conv %d Gradient conv %d\n", conv1, conv2, conv3);
printf(" params gradient change\n");
for (j = 0; j<k; j++)
{
printf("%8.4f %8.4f %8.4f\n",theta(j),g(j),p(j));
}
printf("\n");
}
// Are we done?
if (criterion == 1)
{
if (conv1 && conv2 && conv3)
{
convergence = 1;
break;
}
}
else if (conv1)
{
convergence = 1;
break;
}
last_obj_value = obj_value;
thetanew = theta + p;
theta = thetanew;
// get gradient at new parameter value
g_args(1) = theta;
f_return = feval("NumGradient", g_args);
g_new = f_return(0).matrix_value();
g_new = g_new.transpose();
// Check that gradient is ok
gradient_failed = 0; // test = 1 means gradient failed
for (i=0; i<k;i++)
{
gradient_failed = gradient_failed + isnan(g_new(i));
}
// Hessian update if gradient ok
if (!gradient_failed)
{
q = g_new-g;
g = g_new;
temp = q.transpose() * p;
denominator = temp(0,0);
if (denominator < 2.2e-16) // reset Hessian if necessary
{
// if we already reset Hessian then we're out of luck
if (H == identity_matrix(k,k))
{
convergence = 2;
theta = thetain;
warning("BFGSMin: unable to find direction of improvement: exiting");
break;
}
// Here's the Hessian re-set
H = identity_matrix(k,k);
}
else // normal update
{
temp = (1.0+(q.transpose() * H * q) / denominator) / denominator;
tempscalar = temp(0,0);
H1 = tempscalar * (p * p.transpose());
H2 = (p * q.transpose() * H + H*q*p.transpose());
H2 = H2 / denominator;
H = H + H1 - H2;
}
}
else // failed gradient - try Hessian reset
{
// if we already tried it, we're out of luck
if (H == identity_matrix(k,k))
{
convergence = 2;
theta = thetain;
warning("BFGSMin: failure of gradient: exiting");
break;
}
H = identity_matrix(k,k);
}
}
// // Want last iteration results?
if (verbosity == 2)
{
printf("\nBFGSMin final results: Iteration %d\n",iter);
printf("Stepsize %8.7f\n", stepsize);
printf("Objective function value %16.10f\n", last_obj_value);
printf("Function conv %d Param conv %d Gradient conv %d\n", conv1, conv2, conv3);
printf(" params gradient change\n");
for (j = 0; j<k; j++)
{
printf("%8.4f %8.4f %8.4f\n",theta(j),g(j),p(j));
}
printf("\n");
}
f_return(0) = theta;
f_return(1) = obj_value;
f_return(2) = iter;
f_return(3) = convergence;
return octave_value_list(f_return);
}
BisectionStep.cc
(text/x-c++src, 1.9 KB)
// ============================ BisectionStep ===================================== // // this is for use by BFGSMin // // Uses bisection to find a stepsize that leads to a decrease, then // continues until no further improvement // Michael Creel [email protected] // // // usage: // [a, obj_value] = BisectionStep(f, dx, x, otherargs) // inputs: // f: the objective function // dx: the direction // x: the parameter value // args: the other arguments of the function, in a cell array. #include <oct.h> #include <octave/parse.h> DEFUN_DLD(BisectionStep, args, , "BisectionStep.cc") { std::string func = args(0).string_value(); Matrix dx = args(1).matrix_value(); Matrix x = args(2).matrix_value(); octave_value otherargs = args(3); octave_value_list f_args(2,1); f_args(1) = otherargs; double obj_0, obj, a; octave_value_list f_return; octave_value_list stepobj(2,1); f_args(0) = x; Matrix x_in = x; // possibly function returns a cell array // obj. value will be in first position f_return = feval(func, f_args); obj_0 = f_return(0).double_value(); a = 1.0; // this first loop goes until an improvement is found while (a > 4e-16) // limit iterations { f_args(0) = x + a*dx; f_return = feval(func, f_args); obj = f_return(0).double_value(); // reduce stepsize if worse, or if function can't be evaluated if ((obj > obj_0) || isnan(obj)) { a = 0.5 * a; } else { obj_0 = obj; break; } } // now keep going until we no longer improve, or reach max trials while (a > 4e-16) { a = 0.5*a; f_args(0) = x + a*dx; f_return = feval(func, f_args); obj = f_return(0).double_value(); // if improved, record new best and try another step if ((obj < obj_0) & !isnan(obj)) { obj_0 = obj; } else { a = a / 0.5; // put it back to best found break; } } stepobj(0) = a; stepobj(1) = obj_0; return octave_value_list(stepobj); }
NumGradient.cc
(text/x-c++src, 1.9 KB)
// ============================= NumGradient =============================
// Central difference gradient of a function of the form
// f(theta, otherargs), where theta is a vector and otherargs is a cell array.
//
// The gradient is respect to the FIRST element of args.
//
// * Allows diff. of vector-valued function
// * Uses systematic finite difference
//
// See ExampleNumGradient.m
#include <oct.h>
#include <octave/parse.h>
DEFUN_DLD(NumGradient, args, ,"NumGradient, C++ version\n\
Numeric gradient via central difference")
{
std::string f = args(0).string_value();
Matrix parameter = args(1).matrix_value();
octave_value otherargs = args(2);
octave_value_list f_args(2,1);
f_args(0) = parameter;
f_args(1) = otherargs;
Matrix obj_value, obj_left, obj_right;
octave_value_list f_return;
double p, d, delta, delta_right, delta_left;
int i, j;
// possibly function return cell array
// obj. value will be in first position
f_return = feval(f, f_args);
obj_value = f_return(0).matrix_value();
const int n = obj_value.rows();
const int k = parameter.rows();
Matrix derivative(n, k);
Matrix columnj;
for (j=0; j<k; j++) // get 1st derivative by central difference
{
p = parameter(j);
// delta = FiniteDifference(p,1);
delta = 6e-6;
// right side
parameter(j) = d = p + delta;
delta_right = d - p;
f_args(0) = parameter;
f_return = feval(f, f_args);
obj_right = f_return(0).matrix_value();
// left size
d = p - delta;
parameter(j) = d;
delta_left = p - d;
f_args(0) = parameter;
f_return = feval(f, f_args);
obj_left = f_return(0).matrix_value();
parameter(j) = p; // restore original parameter
columnj = (obj_right - obj_left) / (delta_right + delta_left);
for (i=0; i<n; i++)
{
derivative(i, j) = columnj(i);
}
}
return octave_value(derivative);
}
NewtonStep.cc
(text/x-c++src, 2.4 KB)
// ============================ NewtonStep ===================================== // // this is for use by BFGSMin // // Uses a Newton interation to attempt to find a good stepsize // falls back to BisectionStep if no improvement found // Michael Creel [email protected] // 13/01/2004 // // usage: // [a, obj_value] = NewtonStep(f, dx, x, otherargs) // inputs: // f: the objective function // dx: the direction // x: the parameter value // args: the other arguments of the function, in a cell array. #include <oct.h> #include <octave/parse.h> DEFUN_DLD(NewtonStep, args, , "NewtonStep.cc") { std::string func = args(0).string_value(); Matrix dx = args(1).matrix_value(); Matrix x = args(2).matrix_value(); octave_value otherargs = args(3); octave_value_list f_args(2,1); f_args(1) = otherargs; double obj, obj_0, obj_left, obj_right, delta, a, gradient, hessian; octave_value_list f_return; octave_value_list stepobj(2,1); f_args(0) = x; Matrix x_in = x; gradient = 1.0; // possibly function return cell array // obj. value will be in first position f_return = feval(func, f_args); obj = f_return(0).double_value(); obj_0 = obj; delta = 0.001; // experimentation show that this is a good choice Matrix x_right = x + delta*dx; Matrix x_left = x - delta*dx; // possibly function return cell array // obj. value will be in first position f_args(0) = x_right; f_return = feval(func, f_args); obj_right = f_return(0).double_value(); f_args(0) = x_left; f_return = feval(func, f_args); obj_left = f_return(0).double_value(); gradient = (obj_right - obj_left) / (2*delta); // take central difference hessian = (obj_right - 2*obj + obj_left) / pow(delta, 2.0); hessian = fabs(hessian); // ensures we're going in a decreasing direction if (hessian <= 2e-16) hessian = 1.0; // avoid div by zero a = - gradient / hessian; // hessian inverse gradient: the Newton step a = (a < 5.0)*a + 5.0*(a>=5.0); // Let's avoid extreme steps that might cause crashes // check that this is improvement f_args(0) = x + a*dx; f_return = feval(func, f_args); obj = f_return(0).double_value(); // if not, fall back to bisection if ((obj > obj_0) || isnan(obj)) { f_return = feval("BisectionStep", args); a = f_return(0).double_value(); obj = f_return(1).double_value(); } stepobj(0) = a; stepobj(1) = obj; return octave_value_list(stepobj); }
ExampleBFGSMin.m
(text/octave, 2.4 KB)
1;
# This shows how to call BFGSMin.m
# This is just used to form objective functions, not important
# Function value and gradient vector of the rosenbrock function
# The minimizer is at the vector (1,1,..,1),
# and the minimized value is 0.
function [obj_value, gradient] = rosenbrock(x);
dimension = length(x);
obj_value = sum(100*(x(2:dimension)-x(1:dimension-1).^2).^2 + (1-x(1:dimension-1)).^2);
if nargout > 1
gradient = zeros(dimension, 1);
gradient(1:dimension-1) = - 400*x(1:dimension-1).*(x(2:dimension)-x(1:dimension-1).^2) - 2*(1-x(1:dimension-1));
gradient(2:dimension) = gradient(2:dimension) + 200*(x(2:dimension)-x(1:dimension-1).^2);
endif
endfunction
# example obj. fn. - illustrates use of "otherargs"
function obj_value = objective(theta, otherargs)
location = otherargs{1};
x = theta - location + ones(rows(theta),1); # move minimizer to "location"
obj_value = rosenbrock(x);
endfunction
# example obj. fn. - illustrates calling BFGSMin with 2 arguments
function obj_value = objective2(theta)
# location defined internally, since not passed
dim = rows(theta) - 1;
location = 5*(0:dim)/dim;
location = location';
x = theta - location + ones(rows(theta),1); # move minimizer to "location"
obj_value = rosenbrock(x);
endfunction
# control options and initial value
control = [1000;2;1]; # max 1000 iterations; only report results of last iteration; strong convergence required
dim = 10; # dimension of Rosenbrock function
theta = zeros(dim+1,1); # starting values
location = 5*(0:dim)/dim;
location = location';
# do the minimization
printf("If this was successful, the minimizer should be\n");
printf("a vector of even steps from 0 to 5\n\n");
printf("BFGSMin - 4 input args\n");
theta = theta - theta;
t=cputime();
[theta, obj_value, iterations, convergence] = BFGSMin("objective", theta, {location}, control);
t = cputime() - t;
printf("Elapsed time = %f\n",t);
printf("BFGSMin - 4 input args (dummy otherargs, control supplied)\n");
theta = theta - theta;
t=cputime();
[theta, obj_value, iterations, convergence] = BFGSMin("objective2", theta, {1}, control);
t = cputime() - t;
printf("Elapsed time = %f\n",t);
printf("BFGSMin - 3 input args (dummy otherargs, control not supplied)\n");
theta = theta - theta;
t=cputime();
[theta, obj_value, iterations, convergence] = BFGSMin("objective", theta, {location});
t = cputime() - t;
printf("check that this last worked, since it doesn't print results\n");
theta
printf("Elapsed time = %f\n",t);