%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % 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 . % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% L-BFGS 解优化问题 % 针对无约束优化问题 % % $$ \min_x f(x),$$ % % L-BFGS 在拟牛顿法 BFGS 迭代格式的基础上进行修改, % 用以解决大规模问题的存储和计算困难。对于拟牛顿法中的迭代方向 $d^k=H^k\nabla f(x^k)$ % 考虑利用递归展开的方式进行求解。 % % 首先,对于 BFGS 迭代格式, $H^{k+1}=(V^k)^\top H^kV^k+\rho_ks^k(s^k)^\top$,其中 % $\displaystyle\rho_k = \frac{1}{(y^k)^\top s^k},$ $V^k=I-\rho_k % y^k(s^k)^\top$。 % 将其递归地展开得到 % % $$ \displaystyle\begin{array}{rl} -H^k\nabla f(x^k)= & -(V^{k-m}\cdots % V^{k-1})^\top H^{k-m}(V^{k-m}\cdots V^{k-1})\nabla f(x^k)\\ % & -\rho_{k-m}(V^{k-m+1}\cdots V^{k-1})^\top s^{k-m}(s^{k-m})^\top % (V^{k-m+1}\cdots V^{k-1})\nabla f(x^k)\\ % & -\rho_{k-m+1}(V^{k-m+2}\cdots V^{k-1})^\top s^{k-m+1} % (s^{k-m+1})^\top(V^{k-m+2}\cdots V^{k-1})\nabla f(x^k)\\ % &-\cdots\\ &-\rho_{k-1}s^{k-1}(s^{k-1^\top})\nabla f(x^k).\end{array} $$ % % 我们只需对其中的 $H^{k-m}$ 进行某种估计,即可在展开深度为 $m$ 的情况下对 % $d^k=-H^k\nabla f(x^k)$ 进行近似求解。当用数量矩阵来近似时,即 % $\hat{H}^{k-m}=\gamma_k I$,其中 $\displaystyle\gamma_k=\frac{(s^{k-1})^\top % y^{k-1}}{(y^{k-1})^\top y^{k-1}}$ 对应 BB 方法的第二个步长。 % % 第一个循环:初始化 $q^{k}=-\nabla f(x^k)$,迭代 $\alpha_i=\rho_i(s^i)^\top q^{i+1}$, % $q^{i}=q^{i+1}-\alpha_i y^i$。其中 $i$ 从 $k-1$ 减小到 $k-m$。 % 不难证明 $q^{k-m}$ 递归地求出了 $-V^{k-m}\cdots V^{k-1}\nabla f(x^k)$ 并同时求出了 % $\alpha_{i}=-\rho_{i}(s^{i})^\top (V^{i+1}\cdots V^{k-1})\nabla f(x^k)$。 % % 经过第一个循环,我们可以将上述展开的表达式重写为 % % $$ \displaystyle \begin{array}{rl} -H^k\nabla f(x^k)= % & -(V^{k-m}\cdots V^{k-1})H^{k-m}q^{k-m}\\ % & -(V^{k-m+1}\cdots V^{k-1})^\top s^{k-m}\alpha_{k-m}\\ % & -(V^{k-m+2}\cdots V^{k-1})^\top s^{k-m+1}\alpha_{k-m+1}\\ &-\cdots\\ % &-s^{k-1}\alpha_{k-1}.\end{array} $$ % % 再引入第二个循环对这一求和式进行计算。 % % 初始化 $r^{k-m}=\hat{H}^{k-m}q^{k-m}$,迭代 % % $$ \begin{array}{rl} % \beta_{i}=&\hspace{-0.5em}\rho_i(y^i)^\top r^i,\\ % r^{i+1}=&\hspace{-0.5em}r^i+(\alpha_i-\beta_i)s^i. % \end{array} $$ % % 可以验证每一次循环 $r$ 都相当于对求和式提取公因式后的从前到后进行一步累加,最终得到 $r^k$ 即为所需的 % $-H^k\nabla f(x^k)$。 % % 利用上面双循环算法对下降方向进行求解的方法即为 L-BFGS 方法,这一方法只需记录 $m$ 步的信息 % $\{(s^i,y^i)\}_{i=k-m}^{k}$,在每一步更新时,将新得到的 % $(s^{k+1},y^{k+1})$ 覆盖 $(s^{k-m},y^{k-m})$,因此需要的空间大大减小。 % % 该函数完成上述的 L-BFGS 算法,利用双循环算法计算下降方向,并利用线搜索确定步长。 %% 初始化和迭代准备 %%% % 函数输入: |x| 为迭代的初始点, |fun| 提供函数值和梯度, |opts| 为提供算法参数的结构体。 % % 函数输出: |x| 为迭代得到的解, |f| 和 |g| 为该点处的函数值和梯度, |Out| % 为记录迭代信息的结构体。 % % * |Out.f| :迭代过程的函数值信息 % * |Out.nrmG| :迭代过程的梯度范数信息 % * |Out.xitr| :迭代过程的优化变量 $x$(仅在 |storeitr| 不为 0 时存在) % * |Out.nfe| :调用目标函数的次数 % * |Out.nge| :调用梯度的次数 % * |Out.nrmg| :迭代终止时的梯度范数 % * |Out.iter| :迭代步数 % * |Out.msg| :标记是否达到收敛 function [x, f, g, Out]= fminLBFGS_Loop(x, fun, opts, varargin) %%% % 从输入的结构体 |opts| 中读取参数或采取默认参数。 % % * |opts.xtol| :主循环针对优化变量的停机判断依据 % * |opts.gtol| :主循环针对梯度范数的停机判断依据 % * |opts.ftol| :主循环针对函数值的停机判断依据 % * |opts.rho1| :线搜索标准 $\rho_1$ % * |opts.rho2| :线搜索标准 $\rho_2$ % * |opts.m| :L-BFGS 的内存对存储数 % * |opts.maxit| :主循环的最大迭代次数 % * |opts.storeitr| :标记是否记录每一步迭代的 $x$ % * |opts.record| :标记是否需要迭代信息的输出 % * |opts.itPrint| :每隔几步输出一次迭代信息 if ~isfield(opts, 'xtol'); opts.xtol = 1e-6; end if ~isfield(opts, 'gtol'); opts.gtol = 1e-6; end if ~isfield(opts, 'ftol'); opts.ftol = 1e-16; end if ~isfield(opts, 'rho1'); opts.rho1 = 1e-4; end if ~isfield(opts, 'rho2'); opts.rho2 = 0.9; end if ~isfield(opts, 'm'); opts.m = 5; end if ~isfield(opts, 'maxit'); opts.maxit = 1000; end if ~isfield(opts, 'storeitr'); opts.storeitr = 0; end if ~isfield(opts, 'record'); opts.record = 0; end if ~isfield(opts,'itPrint'); opts.itPrint = 1; end %%% % 参数复制。 xtol = opts.xtol; ftol = opts.ftol; gtol = opts.gtol; maxit = opts.maxit; storeitr = opts.storeitr; parsls.ftol = opts.rho1; parsls.gtol = opts.rho2; m = opts.m; record = opts.record; itPrint = opts.itPrint; %%% % 初始化和迭代准备,计算初始点处的信息。初始化迭代信息。 [f, g] = feval(fun, x , varargin{:}); nrmx = norm(x); Out.f = f; Out.nfe = 1; Out.nrmG = []; %%% % 在 |storeitr| 不为 0 时,记录每一步迭代的 $x$。 if storeitr Out.xitr = x; end %%% % |SK| , |YK| 用于存储 L-BFGS 算法中最近的 $m$ 步的 $s$( $x$ 的变化量)和 $y$ % (梯度 $g$ 的变化量)。 n = length(x); SK = zeros(n,m); YK = zeros(n,m); istore = 0; pos = 0; status = 0; perm = []; %%% % 为打印每一步的迭代信息设定格式。 if record == 1 if ispc; str1 = ' %10s'; str2 = ' %6s'; else str1 = ' %10s'; str2 = ' %6s'; end stra = ['%5s',str2,str2,str1, str2, str2,'\n']; str_head = sprintf(stra, ... 'iter', 'stp', 'obj', 'diffx', 'nrmG', 'task'); str_num = ['%4d %+2.1e %+2.1e %+2.1e %+2.1e %2d\n']; end %% 迭代主循环 % 迭代最大步数 |maxit| 。当未达到收敛条件时,记录为超过最大迭代步数退出。 Out.msg = 'MaxIter'; for iter = 1:maxit %%% % 记录上一步迭代的结果。 xp = x; nrmxp = nrmx; fp = f; gp = g; %%% % L-BFGS 双循环方法寻找下降方向。在第一次迭代时采用负梯度方向,之后便使用 L-BFGS 方法来 % 估计 $d=-Hg$。 if istore == 0 d = -g; else d = LBFGS_Hg_Loop(-g); end %%% % 沿 L-BFGS 方法得到的下降方向做线搜索。调用函数 |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 %%% % 对于线搜索得到的步长 $\alpha_k$,令 $s^k=x^{k+1}-x^k=\alpha_kd^k$, % 则 $\|s^k\|_2=\alpha_k\|d^k\|_2$。计算 $\|s^k\|_2/\max(1,\|x^k\|_2)$ % 并将其作为判断收敛的标准。 nrms = stp*normd; diffX = nrms/max(nrmxp,1); %%% % 更新 $\|x^{k+1}\|_2$, $\|g^{k+1}\|_2$,记录一步迭代信息。 nrmG = norm(g); Out.nrmg = nrmG; Out.f = [Out.f; f]; Out.nrmG = [Out.nrmG; nrmG]; if storeitr Out.xitr = [Out.xitr, x]; end nrmx = norm(x); %%% % 停机准则, |diffX| 表示相邻迭代步 $x$ 的相对变化, |nrmG| 表示当前 $x$ 处的梯度范数, $\displaystyle\frac{|f(x^{k+1})-f(x^k)|}{1+|f(x^k)|}$ % 用以表示函数值的相对变化。当前两者均小于阈值,或者第三者小于阈值时,认为达到停机标准,退出当前循环。 cstop = ((diffX < xtol) && (nrmG < gtol) )|| (abs(fp-f)/(abs(fp)+1)) < ftol; %%% % 当需要详细输出时,在(1)开始迭代时(2)达到收敛时(3)达到最大迭代次数或退出迭代时(4)每若干步,打印详细结果。 if (record == 1) && (cstop || iter == 1 || iter==maxit || mod(iter,itPrint)==0) if iter == 1 || mod(iter,20*itPrint) == 0 && iter~=maxit && ~cstop fprintf('\n%s', str_head); end fprintf(str_num, ... iter, stp, f, diffX, nrmG, workls.task); end %%% % 当达到收敛条件时,停止迭代,记为达到收敛。 if cstop Out.msg = 'Converge'; break; end %%% % 计算 $s^k=x^{k+1}-x^{k}$, $y^k=g^{k+1}-g^{k}$。 % 当得到的 $\|y^k\|$ 不小于阈值时,保存当前的 $s^k, y^k$,否则略去。利用 |pos| % 记录当前存储位置,然后覆盖该位置上原来的信息。 ygk = g-gp; s = x-xp; if ygk'*ygk>1e-20 istore = istore + 1; pos = mod(istore, m); if pos == 0; pos = m; end YK(:,pos) = ygk; SK(:,pos) = s; rho(pos) = 1/(ygk'*s); %%% % 用于提供给 L-BFGS 双循环递归算法,以指明双循环的循环次数。当已有的记录超过 $m$ 时, % 则循环 $m$ 次。否则,循环次数等于当前的记录个数。 |perm| 按照顺序记录存储位置。 if istore <= m; status = istore; perm = [perm, pos]; else status = m; perm = [perm(2:m), perm(1)]; end end end %%% % 当从上述循环中退出时,记录输出。 Out.iter = iter; Out.nge = Out.nfe; %% L-BFGS 双循环递归算法 % 利用双循环递归算法,返回下一步的搜索方向即 $-Hg$。 % 初始化 $q$ 为初始方向,在 L-BFGS 主算法中,这一方向为负梯度方向。 function y = LBFGS_Hg_Loop(dv) q = dv; alpha = zeros(status,1); %%% % 第一个循环, |status| 步迭代。( |status| 的计算见上,当迭代步足够大时为 $m$ ) % |perm| 按照顺序记录了存储位置。从中提取出位置 $k$ 的格式为: % $\alpha_i=\rho_i(s^i)^\top q^{i+1}$ , % $q^{i}=q^{i+1}-\alpha_i y^i$。其中 $i$ 从 $k-1$ 减小到 $k-m$。 for di = status:-1:1 k = perm(di); alpha(di) = (q'*SK(:,k)) * rho(k); q = q - alpha(di)*YK(:,k); end %%% % $r^{k-m}=\hat{H}^{k-m}q^{k-m}.$ y = q/(rho(pos)* (ygk'*ygk)); %%% % 第二个循环,迭代格式 $\beta_{i}=\rho_i(y^i)^\top r^i$, % $r^{i+1}=r^i+(\alpha_i-\beta_i)s^i$。代码中的 |y| 对应于迭代格式中的 $r$,当两次循环结束时,以返回的 |y| 的值作为下降方向。 for di = 1:status k = perm(di); beta = rho(k)* (y'* YK(:,k)); y = y + SK(:,k)*(alpha(di)-beta); end end end %% 参考页面 % 关于 L-BFGS 算法的应用,参考页面 % 以及 。 % % 我们在其中利用了翻译为 MATLAB 代码的 MINPACK-2 的线搜索函数 |ls_csrch| , % 代码详见 和被其调用的 % ,或参考 Fortran 版本的官方代码 % 。 % % 此页面的源代码请见: % <../download_code/lbfgs/fminLBFGS_Loop.m fminLBFGS_Loop.m>。 %% 版权声明 % 此页面为《最优化:建模、算法与理论》、《最优化计算方法》配套代码。 % 代码作者:文再文、刘浩洋、户将,代码整理与页面制作:杨昊桐。 % % 著作权所有 (C) 2020 文再文、刘浩洋、户将