Code covered by the BSD License  

Highlights from
Fminspleas

from Fminspleas by John D'Errico
Efficient nonlinear regression fitting using a constrained, partitioned least squares overlay to fmi

fminspleas(funlist,NLPstart,xdata,ydata,INLB,INUB,weights,options)
function [INLP,ILP] = fminspleas(funlist,NLPstart,xdata,ydata,INLB,INUB,weights,options)
% pleas: partitioned nonlinear least squares estimation, using fminsearch
% usage: [INLP,ILP] = FMINSPLEAS(funlist,NLPstart,xdata,ydata)
% usage: [INLP,ILP] = FMINSPLEAS(funlist,NLPstart,xdata,ydata,INLB,INUB)
% usage: [INLP,ILP] = FMINSPLEAS(funlist,NLPstart,xdata,ydata,INLB,INUB,weights)
% usage: [INLP,ILP] = FMINSPLEAS(funlist,NLPstart,xdata,ydata,INLB,INUB,weights,options)
%
% FMINSPLEAS will be far more robust (and faster) in solving nonlinear
% least squares than the basic FMINSEARCH because of its partitioned
% least squares estimation procedure. (As described in my optimization
% tips and tricks document.) In fact, FMINSPLEAS may be faster and more
% robust than some other optimizers that do not use a partitioned least
% squares scheme. Finally, FMINSPLEAS is the only nonlinear regression
% tool that explicitly allows the user to provide weights in the
% regression.
%
% http://www.mathworks.com/matlabcentral/fileexchange/loadFile.do?objectId=8553&objectType=FILE
%
% arguments: (input)
%  funlist - cell array of functions comprising the nonlinear parts
%            of each term in the model. Each independent function in
%            this list must transform xdata using a vector of intrinsicly
%            nonlinear parameters into an array of the same size and
%            shape as ydata. (Constants will be expanded in size.) The
%            arguments to each function will be in the order (INLP,xdata).
%
%            These functions may be
%             - scalar (double) constants (E.g., 1)
%             - anonymous functions
%             - inline functions
%             - character function names
%
%            FMINSPLEAS assumes a model of the form:
%
%            ydata = a1*f1(INLP,xdata) + a2*f2(INLP,xdata) + ...
%
%            funlist is the list of functions {f1,f2,...}. The
%            intrinsically linear parameters [a1,a2,...] are estimated
%            using an internal linear regression.
%
%            Any additional parameters to these functions should be
%            passed anonymously.
%
%  NLPstart - vector of starting values for the intrinsicly nonlinear
%            parameters only.
%
%  xdata   - array of independent variables
%            
%  ydata   - array of dependent variable data
%
%  INLB    - (OPTIONAL) array of lower bounds of the NONLINEAR
%            variables. Must be the same size as NLPstart.
%
%            If no lower bounds are supplied, then INLB should
%            be left empty. If only some variables have lower
%            bounds, then use -inf as the lower bounds for the
%            unconstrained variables.
%
%  INUB    - (OPTIONAL) array of upper bounds of the NONLINEAR
%            variables. Must be the same size as NLPstart.
%
%            If no upper bounds are supplied, then INUB should
%            be left empty. If only some variables have upper
%            bounds, then use inf as the upper bounds for the
%            unconstrained variables.
%
%  weights - (OPTIONAL) array of non-negative weights, of the same
%            size as ydata. If provided, these weights will be used
%            to scale the squared residual errors in the estimation.
%
%            To be specific, a weight of 2 is equivalent to a
%            replication of the corresponding data point so there
%            would have been 2 samples at that value.
%
%            Only the relative weights are important.
%
%            A weight of zero is equivalent to dropping that data
%            point from the estimation. Negative weights will
%            cause an error.
%
%  options - (OPTIONAL) options structure appropriate for fminsearch
%
% arguments (output)
%  INLP - optimized list of intrinsicly nonlinear parameters
%
%  ILP  - optimized list of intrinsicly linear parameters 
%
%
% Example 1:
%  Fit a simple exponential model plus a constant term to data.
%  Note that fminspleas deals with the linear parameters, the
%  user needs only worry about the nonlinear parameters.
%
%   x = rand(100,1);
%   y = 4 - 3*exp(2*x) + randn(size(x));
%
%   funlist = {1, @(coef,xdata) exp(xdata*coef)};
%   % If you have an older release of matlab, use this form:
%   % funlist = {1, inline('exp(xdata*coef)','coef','xdata')};
%
%   NLPstart = 1;
%   options = optimset('disp','iter');
%   [INLP,ILP] = fminspleas(funlist,NLPstart,x,y,[],[],[],options)
%
% Output:
%  Iteration   Func-count     min f(x)         Procedure
%      0            1           101.57         
%      1            2           98.314         initial simplex
%      2            4          92.4016         expand
%      3            6           82.945         expand
%      4            8           73.175         expand
%      5           10          72.6363         contract outside
%      6           12          72.5564         contract inside
%      7           14          72.5101         contract inside
%      ...
%  
% Optimization terminated:
%  the current x satisfies the termination criteria using OPTIONS.TolX of 1.000000e-04 
%  and F(X) satisfies the convergence criteria using OPTIONS.TolFun of 1.000000e-04 
% 
% INLP =
%        1.8884
% 
% ILP =
%        4.3268
%       -3.3207
%
% Similar accuracy with fminsearch directly operating on all three
% parameters at once took roughly 300 function evaluations.
%
% Example 2:
%  Fit a sum of several sine terms to data. Beware of terms
%  with a similar form - you must use distinct starting
%  values for the nonlinear parameters.
%
%   x = rand(500,1);
%   y = 4 - 3*sin(2*x) + 2*cos(3*x) + randn(size(x))/10;
%
%   funlist = {1, @(c,xdata) sin(c(1)*xdata), @(c,xdata) cos(c(2)*xdata)};
%   [INLP,ILP] = fminspleas(funlist,[3 4],x,y)
%
% INLP =
%          1.91830441497209          3.04626358883634
%
% ILP =
%          4.16097130716892
%         -3.21354044158414
%          1.86372562015106
%
% See also: fminsearch, fminsearchbnd, pleas, lsqcurvefit
%  Note: pleas is found as an adjunct to my optimtips document.
%
% Author: John D'Errico
% E-mail: woodchips@rochester.rr.com
% Release: 2.1
% Release date: 5/23/06

% Check the functions
if ~iscell(funlist)
  error 'funlist must be a cell array of functions, even if only one fun'
end
nfun=length(funlist);
for i = 1:nfun
  fi = funlist{i};
  
  % There are two cases where we need to turn the supplied
  % function into an executable function
  if isa(fi,'double')
    % a constant
    funlist{i} = @(coef,xdata) repmat(fi,size(ydata));
  elseif ischar(fi)
    % a character function name
    funlist{i} = str2func(fi);
  end
end

% were any options supplied?
if (nargin<8) || isempty(options)
  options = optimset('fminsearch');
end

if (nargin<7)
  weights = [];
end

nnlin = size(NLPstart);
% any bound constraints?
if (nargin<5) || isempty(INLB)
  INLB = repmat(-inf,nnlin);
elseif any(nnlin~=size(INLB))
  error 'INLB (if provided) must be the same size as NLPstart'
end
if (nargin<6) || isempty(INUB)
  INUB = repmat(inf,nnlin);
elseif any(nnlin~=size(INUB))
  error 'INUB (if provided) must be the same size as NLPstart'
end
% check for inverted bounds
if any(INLB(:) > INUB(:))
  error 'Some lower/upper bounds were inconsistent with each other'
end

% what transformations will be used for the nonlinear parameters?
INLPclass = zeros(nnlin);
%  0 --> no bounds
%  1 --> only a lower bound
%  2 --> only an upper bound
%  3 --> both lower and upperbounds
%  4 --> Fixed variable
for i = 1:numel(INLPclass);
  if ~isinf(INLB(i))
    INLPclass(i) = INLPclass(i) + 1;
  end
  if ~isinf(INUB(i))
    INLPclass(i) = INLPclass(i) + 2;
  end
  if INLB(i) == INUB(i)
    INLPclass(i) = 4;
  end
end
params.INLB = INLB;
params.INUB = INUB;
params.INLPclass = INLPclass;

% Transform starting values into the unconstrained domain
tNLPstart = xtransform(NLPstart,params,1);

% make sure that ydata is a column vector
ydata = ydata(:);
ny = length(ydata);

% check that weights is the proper size (same as ydata)
if ~isempty(weights)
  weights = weights(:);
  if length(weights)~=ny
    error 'If provided, weights must be the same size as ydata'
  end
  
  if any(weights<0)
    error 'If provided, weights must be non-negative'
  elseif all(weights==0)
    error 'If provided, at least some weights must be non-zero'
  end
  
  % sqrt so that we can scale the residuals by the weights
  % this makes the linear regression simple.
  weights = sqrt(weights);
end

% ================================================
% =========== begin nested function ==============
% ================================================
function [obj,ILP] = fminspleas_obj(tINLP)
  % nested objective function for lsqnonlin, so all
  % the data and funs from pleas are visible to pleas_obj
  
  % Untransform the parameters into the constrained range space.
  INLP = xtransform(tINLP,params,0);
  
  % loop over funlist
  A = zeros(ny,nfun);
  for ii=1:nfun
    fi = funlist{ii};
    term = fi(INLP,xdata);
    A(:,ii) = term(:);
  end
  
  % do the linear regression using \
  if isempty(weights)
    % unweighted regression
    ILP = A\ydata;
    
    % residuals
    res = A*ILP - ydata;
  else
    % use weights in the regression
    ILP = (repmat(weights,1,nfun).*A)\(ydata.*weights);
    
    % weighted residuals
    res = (A*ILP - ydata).*weights;
  end
  
  % sum of squares objective for fminsearch. Weights are
  % built in already here.
  obj = sum(res.^2);
  
end % nested function termination
% ================================================
% ============= end nested function ==============
% ================================================

% call fminsearch, using a nested function handle
tINLP = fminsearch(@fminspleas_obj,tNLPstart,options);

% call one final time to get the final linear parameters
[junk,ILP] = fminspleas_obj(tINLP);

% undo the variable transformations into the original space
INLP = xtransform(tINLP,params,0);


end % main function terminator


% ================================================
% ============== begin subfunctions ==============
% ================================================
function xtrans = xtransform(x,params,transdirection)
% transdirection == 0 --> unconstrained domain into constrained set
% transdirection == 1 --> constrained range into constrained domain

if transdirection == 0
  % transform from unconstrained problem into the user's
  % parameter space
  xtrans = zeros(size(params.INLPclass));
  k = 1;
  for i = 1:numel(xtrans)
    switch params.INLPclass(i)
      case 1
        % lower bound only
        xtrans(i) = params.INLB(i) + x(k).^2;
        k = k + 1;
      case 2
        % upper bound only
        xtrans(i) = params.INUB(i) - x(k).^2;
        k = k + 1;
      case 3
        % lower and upper bounds
        temp = (sin(x(k))+1)/2;
        xtrans(i) = temp*(params.INUB(i) - params.INLB(i)) + params.INLB(i);
        k = k + 1;
      case 4
        % fixed variable, bounds are equal, set it at either bound
        xtrans(i) = params.INLB(i);
      case 0
        % unconstrained variable.
        xtrans(i) = x(k);
        k = k + 1;
    end
    
    % ensure that floating point irregularities
    % do not exceed the bounds
    xtrans(i) = min(params.INUB(i),max(params.INLB(i),xtrans(i)));
    
  end

else
  % send values into their unconstrained surrogates.
  % Check for infeasible starting guesses.
  xtrans = zeros(1,sum(params.INLPclass(:)~=4));
  k = 1;
  for i = 1:length(params.INLPclass)
    switch params.INLPclass(i)
      case 1
        % lower bound only
        if x(k)<=params.INLB(i)
          % infeasible starting value. Use bound.
          xtrans(k) = 0;
        else
          xtrans(k) = sqrt(x(i) - params.INLB(i));
        end
        k = k + 1;
      case 2
        % upper bound only
        if x(i)>=params.INUB(i)
          % infeasible starting value. use bound.
          xtrans(k) = 0;
        else
          xtrans(k) = sqrt(params.INUB(i) - x(i));
        end
        k = k + 1;
      case 3
        % lower and upper bounds
        if x(i)<=params.INLB(i)
          % infeasible starting value
          xtrans(k) = -pi/2;
        elseif x(i)>=params.INUB(i)
          % infeasible starting value
          xtrans(k) = pi/2;
        else
          xtrans(k) = 2*(x(i) - params.INLB(i))/(params.INUB(i)-params.INLB(i)) - 1;
          xtrans(k) = asin(max(-1,min(1,xtrans(k))));
        end
        k = k + 1;
      case 4
        % Fixed. Drop this variable so the optimizer will not see it.
      case 0
        % unconstrained variable. xtrans(i) is set.
        xtrans(k) = x(i);
        k = k + 1;
    end
  end
  
end

end % subfunction terminator
% ================================================
% ================ end subfunctions ==============
% ================================================




Contact us at files@mathworks.com