%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Copyright (C) 2020 Zaiwen Wen, Haoyang Liu, Jiang Hu
%
% This program is free software: you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation, either version 3 of the License, or
% (at your option) any later version.
%
% This program is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with this program. If not, see .
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Change log:
%
% 2020.4.18 (Jiang Hu):
% First version
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 利用牛顿-共轭梯度法解优化问题
% 该算法利用非精确牛顿法(牛顿-共轭梯度法)求解无约束优化问题:
%
% $$ \min_{x} \;\; f(x)$$
%
% 在第 $k$ 步迭代,下降方向 $d^k$ 通过求解下面的牛顿方程
% $\displaystyle (\nabla^2 f(x^k)) d^k = -\nabla f(x^k)$
% 得到。选取合适的步长 $\alpha_k$,牛顿法的迭代格式为
%
% $$ x^{k+1} = x^{k} + \alpha_k d_k.$$
%
% 对于规模较大的问题,精确求解牛顿方程组的代价比较高。事实上,牛顿方程求解等价于无约束二次优化问题:
%
% $$ \displaystyle\min_{d^k} \frac{1}{2}(d^k)^\top \nabla^2 f(x^k) d^k +
% (\nabla f(x^k))^\top d^k, $$
%
% 其可以通过共轭梯度法来进行求解。
%
%% 初始化和迭代准备
% 输入信息:迭代初始点 |x| ,目标函数 |fun| (依次返回给定 |x| 处的函数值和梯度),
% 海瑟矩阵 |hess| 以及提供参数的结构体 |opts| 。
%
% 输出信息:迭代得到的解 |x| 和迭代信息结构体 |out| 。
%
% * |out.msg| :标记是否达到收敛
% * |out.nrmG| :迭代退出时的梯度范数
% * |out.iter| :迭代退出时的迭代步数、函数值
% * |out.f| :迭代退出时的目标函数值
% * |out.nfe| :调用原函数的次数
function [x, out] = fminNewton(x, fun, hess, opts, varargin)
%%%
% 检查输入信息,MATLAB 以 |nargin| 表示函数输入的参数个数。当参数量不足 3 时报错,
% 等于 3 时认为 |opts| 为空结构体,即全部采用默认参数。
%
% 从输入的结构体 |opts| 中读取参数或采取默认参数。
%
% * |opts.xtol| :针对优化变量的停机准则
% * |opts.gtol| :针对梯度范数的停机准则
% * |opts.ftol| :针对函数值的停机准则
% * |opts.rho1| :线搜索参数 $\rho_1$
% * |opts.rho2| :线搜索参数 $\rho_2$
% * |opts.maxit| :主循环的最大迭代次数
% * |opts.verbose| : |>=1| 时输出迭代信息
% * |opts.itPrint| :每隔几步输出一次迭代信息
if (nargin < 3); error('[x, out] = fminNewton(x, fun, hess, opts)'); end
if (nargin < 4); opts = []; end
if ~isfield(opts,'gtol'); opts.gtol = 1e-6; end
if ~isfield(opts,'xtol'); opts.xtol = 1e-6; end
if ~isfield(opts,'ftol'); opts.ftol = 1e-12; end
if ~isfield(opts, 'rho1'); opts.rho1 = 1e-4; end
if ~isfield(opts, 'rho2'); opts.rho2 = 0.9; end
if ~isfield(opts,'maxit'); opts.maxit = 200; end
if ~isfield(opts,'verbose'); opts.verbose = 0; end
if ~isfield(opts,'itPrint'); opts.itPrint = 1; end
%%%
% 从结构体 |opts| 中复制参数。
maxit = opts.maxit; verbose = opts.verbose; itPrint = opts.itPrint;
xtol = opts.xtol; gtol = opts.gtol; ftol = opts.ftol;
%%%
% |parsls| 为由线搜索参数构成的结构体。
parsls.ftol = opts.rho1; parsls.gtol = opts.rho2;
%%%
% 迭代准备,计算初始点 $x$ 处的函数值和梯度值。
[f,g] = fun(x);
nrmg = norm(g,2);
nrmx = norm(x,2);
%%%
% 预先设定输出信息,在没有达到收敛条件时,记为超出最大迭代次数退出。
out = struct();
out.msg = 'MaxIter';
out.nfe = 1;
out.nrmG = nrmg;
%%%
% 当需要详细输出时,设定输出格式。
if verbose >= 1
if ispc; str1 = ' %10s'; str2 = ' %8s';
else str1 = ' %10s'; str2 = ' %8s'; end
stra = ['%5s', str1, str2, str2, str2, '\n'];
str_head = sprintf(stra, ...
'iter', 'F', 'nrmg', 'fdiff', 'xdiff');
fprintf('%s', str_head);
str_num = ['%4d %+8.7e %+2.1e %+2.1e %+2.1e\n'];
end
%%%
% 非精确牛顿法使用共轭梯度法求解牛顿方程, |opts_tCG| 为共轭梯度法提供参数。
opts_tCG = struct();
%% 迭代主循环
for iter = 1:maxit
%%%
% 记录上一步迭代的信息(上一步的优化变量和对应的目标函数值、梯度、范数)。
fp = f;
gp = g;
xp = x;
nrmxp = nrmx;
%%%
% 调用截断共轭梯度法 |tCG| (见参考页面)不精确地求解牛顿方程得到牛顿方向 $d$。
% 这里函数 |hess(x,d)| 对应矩阵向量乘 $\nabla^2 f(x) d$。
opts_tCG.kappa = 0.1;
opts_tCG.theta = 1;
hess_tCG = @(d) hess(x,d);
[d, ~, ~] = tCG(x, gp, hess_tCG, inf, opts_tCG);
%%%
% 沿方向 $d$ 做线搜索。调用函数 |ls_csrch| 进行线搜索,具体步骤可以参考
% MINPACK-2。
%
% 首先初始化线搜索标记 |workls.task| 为 1, |deriv| 为当前线搜索方向上的导数。
% 线搜索函数寻找合适的步长 $\alpha_k$,在对应的搜索方向上满足条件
%
% $$ \begin{array}{rl}
% f(x^k+\alpha_k d^k)&\hspace{-0.5em}\le
% f(x^k)+\rho_1\cdot\alpha_kg(x^k), \\
% |g(x^k+\alpha_kd^k)|&\hspace{-0.5em}\le \rho_2\cdot |g(x^k)|.
% \end{array} $$
%
% |ls_csrch| 每次调用只执行线搜索的一步,并用 |workls.task| 指示下一步应当执行的操作。
% 此处 |workls.task==2| 意味着需要重新计算当前点函数值和梯度等。具体的步长寻找过程可以参考源文件。
%
% 当线搜索条件满足时,退出线搜索循环,得到更新的迭代点 $x$。
workls.task = 1;
deriv = d'*g;
normd = norm(d);
stp = 1;
while 1
[stp, f, deriv, parsls, workls] = ....
ls_csrch(stp, f, deriv , parsls , workls);
if (workls.task == 2)
x = xp + stp*d;
[f, g] = feval(fun, x, varargin{:});
out.nfe = out.nfe + 1;
deriv = g'*d;
else
break
end
end
%%%
% 计算一步更新后的点 $x$处的相关数据以判断是否停机。
% |nrms| 表示 $\|x^{k+1}-x^k\|_2$,
% |xdiff| 表示 $\|x^{k+1}-x^k\|/\max\{1,\|x^k\|\}$ 为迭代点 $x$的相对变化量,
% |nrmg| 表示 $x^k$ 处梯度范数, |nrmx| 为点 $x$ 的范数,
% |fdiff| 为函数值相对变化量。 |out.nfe| 记录函数值计算的总次数。
nrms = stp*normd;
xdiff = nrms/max(nrmxp,1);
nrmg = norm(g,2);
out.nrmG = [out.nrmG; nrmg];
nrmx = norm(x,2);
out.nfe = out.nfe + 1;
fdiff = abs(fp-f)/(abs(fp)+1);
%%%
% 停机判断:当梯度范数小于阈值,或者函数值的相对变化量和优化变量的相对变化量均小于阈值时,
% 迭代终止。
cstop = nrmg <= gtol || (abs(fdiff) <= ftol && abs(xdiff) <= xtol);
%%%
% 当需要详细输出时,在(1)开始迭代时(2)达到收敛时(3)达到最大迭代次数退出迭代时
% (4)每若干步,打印详细结果。
% 当满足收敛条件时,标记为达到最优值。退出迭代,记录退出信息为达到最优值退出。
if verbose>=1 && (cstop || iter == 1 || iter==maxit || mod(iter,itPrint)==0)
if mod(iter,20*itPrint) == 0 && iter ~= maxit && ~cstop
fprintf('\n%s', str_head);
end
fprintf(str_num, ...
iter, f, nrmg, fdiff, xdiff);
end
if cstop
out.msg = 'Optimal';
break;
end
end
%%%
% 迭代退出时,记录输出信息。
out.iter = iter;
out.f = f;
out.nrmg = nrmg;
end
%% 参考页面
% 算法中的共轭梯度法部分,详见 。在 中,我们展示该算法的一个应用。
%
% 代码中利用了翻译为 MATLAB 代码的 MINPACK-2 的线搜索函数 |ls_csrch| ,详见
%
% ,或参考 Fortran 版本的官方代码 。
%
% 此页面的源代码请见: <../download_code/newton/fminNewton.m fminNewton.m>。
%% 版权声明
% 此页面为《最优化:建模、算法与理论》、《最优化计算方法》配套代码。
% 代码作者:文再文、刘浩洋、户将,代码整理与页面制作:杨昊桐。
%
% 著作权所有 (C) 2020 文再文、刘浩洋、户将