无约束问题是指只有优化目标,而不存在约束条件的问题,用数学模型可表示为
min x ∈ R n f ( x ) \min\limits_{x\in R^n}f(x) x∈Rnminf(x)
通常假设 f ( x ) f(x) f(x)具有二阶连续偏导数。
无约束优化方法是通常是由给定的初始点出发,按一定的规则不断向最优解趋近。在前文最优化 | 一维搜索与方程求根中提到的一维搜索方法、梯度下降法、牛顿法等都可以用来解决无约束优化问题。
无约束最优化方法的核心问题是选择搜索方向。
下面主要介绍以及实现几种常见的无约束优化方法。
所涉及c++代码均存于github仓库。
部分python版本代码见于github仓库。
梯度下降法的优化思想是用当前位置负梯度方向作为搜索方向,因为该方向为当前位置的最快下降方向,所以也被称为是最速下降法。
基本的算法为:
在算法中,迭代收敛可以用梯度向量长度小于某个预定值, 或者两次迭代间函数值变化小于某个预定值来判断。迭代中需要用某种一维搜索方法求步长,不过实际应用时,绝大部分是使用预先确定的步长,所以步长也叫做学习率。
在适当正则性条件下, 最速下降法可以收敛到局部极小值点并且具有线性收敛速度。 但是, 最速下降法连续两次的下降方向 − ∇ f ( x ( t ) ) -\nabla f(x^{(t)}) −∇f(x(t))和 − ∇ f ( x ( t + 1 ) ) -\nabla f(x^{(t+1)}) −∇f(x(t+1))是正交的, 这是因为在第 t t t步沿 − ∇ f ( x ( t ) ) -\nabla f(x^{(t)}) −∇f(x(t))搜索时找到点时已经使得函数值在该方向上不再变化, 下一个搜索方向如果与该方向不正交就不是下降最快的方向。 这样,最速下降法收敛速度比较慢。 所以, 在每次迭代的一维搜索时, 可以不要求找到一维搜索的极小值点, 而是函数值足够降低就结束一维搜索。
求函数 f ( x ) = x 2 − 4 x f(x)=x^2-4x f(x)=x2−4x的极值点。
//
// Created by CHH3213 on 2022/9/12.
//
#include
#include
/*
* 使用梯度下降法求解函数极值
*/
using namespace std;
#define DELTA 1e-4
#define EPS 1e-6
double func(double x){
/**
* 函数
*/
return x*x-4*x;
}
double func_prime(double x){
/**
* 函数导数
*/
return (func(x+DELTA)-func(x-DELTA))/(2*DELTA);
}
double GradientDescent(double x0,double learning_rate ){
/**
* 梯度下降算法
* x0为迭代初值
* learning_rate为学习率
*/
double x = x0;
double x_last = 0.5;
while(abs(x-x_last)>EPS){
x_last=x;
x = x_last-learning_rate* func_prime(x_last);
}
return x;
}
int main(){
double x1= GradientDescent(0,0.01);
cout<<x1<<endl;
}
求解 f ( x ) = 4 ( x 1 − 1 ) 2 + ( x 2 − 2 ) 4 f(x)=4(x_1-1)^2+(x_2-2)^4 f(x)=4(x1−1)2+(x2−2)4的极值点,假设从初始点 [ 0 , 0 ] [0,0] [0,0]出发。(显然,函数具有全局极小值点(1,2)。)
非常容易求得
∇ f ( x ) = [ 8 ( x 1 − 1 ) , 4 ( x 2 − 2 ) 3 ] T \nabla f(x)=[8(x_1-1), 4(x_2-2)^3]^T ∇f(x)=[8(x1−1),4(x2−2)3]T
然后使用梯度更新公式即可,和一元函数差别不大(后面举例均已一元函数为主)。
c++代码实现如下:
//
// Created by CHH3213 on 2022/9/12.
//
#include
#include
#include
/*
* 使用梯度下降法求解多元函数极值:f(x)=4(x1-1)^2+(x2-2)^4
*/
using namespace std;
#define DELTA 1e-4
#define EPS 1e-9
double func(double x1, double x2){
/**
* 函数
*/
return 4*pow(x1-1,2)+pow(x2-2,4);
}
vector<double> func_prime(double x1,double x2){
/**
* 函数导数
*/
double nabla_x1 = (func(x1+DELTA,x2)-func(x1-DELTA,x2))/(2*DELTA);//对x1的偏导数
double nabla_x2 = (func(x1,x2+DELTA)-func(x1,x2-DELTA))/(2*DELTA);//对x2的偏导数
return {nabla_x1,nabla_x2};
}
vector<double> GradientDescent(vector<double> x0,double learning_rate ){
/**
* 梯度下降算法
* x0为迭代初值
* learning_rate为学习率
*/
vector<double> x = x0;
vector<double> x_last = {0.5,0.5};
while(abs(x[0]-x_last[0])>EPS||abs(x[1]-x_last[1])>EPS){
x_last=x;
vector<double>prime = func_prime(x_last[0],x_last[1]);
x[0] = x_last[0]-learning_rate*prime[0] ;
x[1] = x_last[1]-learning_rate*prime[1] ;
// cout<
}
return x;
}
int main(){
vector<double> x= GradientDescent({0,0},0.01);
cout<<x[0]<<' '<<x[1]<<endl;
}
最速下降法收敛速度较慢,而且越靠近最小值点步长越短,速度越慢。对于一个函数曲面为狭长的山谷形 状的目标函数,为了达到山谷的最低点,会沿相互正交的梯度方向反复跳跃。
一种解决的方法是前面所说的不使用最优一维搜索而是使用预先确定的步长或步长序列,还有一种方法是将两步之间的梯度正交, 推广到两步之间为“共轭”关系,即对某个矩阵 A t A_t At 使得
[ g ( t ) ] T A t g ( t + 1 ) = 0. \left[\boldsymbol{g}^{(t)}\right]^T A_t \boldsymbol{g}^{(t+1)}=0 . [g(t)]TAtg(t+1)=0.
这种方法的来源是对目标函数 f ( x ) f(\boldsymbol{x}) f(x) 用二次多项式近似(泰勒展开,忽略高次项):
f ( x ) ≈ 1 2 x T A x + b T x + c , f(\boldsymbol{x}) \approx \frac{1}{2} \boldsymbol{x}^T A \boldsymbol{x}+\boldsymbol{b}^T \boldsymbol{x}+c, f(x)≈21xTAx+bTx+c,
而为了将一个 k k k元的凸二次多项式达到最小值点,只要从负梯度方向出发,沿着 k k k 个相互都关于 A A A 共轭的方向进行一维搜索就保证达到最小值点。连续可微函数在最小值点附近一般都很接近于一个凸二次多项式函数, 所以共轭梯度法能够克服最速下降法在靠近最小值点时收敛变慢的问题。
A t A_t At 矩阵选取最好是Hessian阵 ∇ 2 f ( x ( t ) ) \nabla^2 f\left(\boldsymbol{x}^{(t)}\right) ∇2f(x(t)) , 但Hessian阵一般比梯度更难获得。共轭梯度法实际的算法并不去求 A t A_t At ,而是从 x ( 1 ) \boldsymbol{x}^{(1)} x(1) 的负梯度出发, 然后每次用当前 x ( t ) \boldsymbol{x}^{(t)} x(t) 处的负梯度和 上一步的共轭方向的线性组合作为 t t t 到 t + 1 t+1 t+1 步的搜索方向。即:
d ( 1 ) = − g ( 1 ) ; d ( t ) = − g ( t ) + β ( t ) d ( t − 1 ) , t = 2 , 3 , … . \begin{aligned} &\boldsymbol{d}^{(1)}=-\boldsymbol{g}^{(1)} ; \\ &\boldsymbol{d}^{(t)}=-\boldsymbol{g}^{(t)}+\beta^{(t)} \boldsymbol{d}^{(t-1)}, t=2,3, \ldots . \end{aligned} d(1)=−g(1);d(t)=−g(t)+β(t)d(t−1),t=2,3,….
对 k k k元凸二次多项式,其中的系数 β ( t ) \beta^{(t)} β(t) 可以用 A A A 解出精确公式, 使得各个 d ( t ) \boldsymbol{d}^{(t)} d(t) 彼此共轭。但是对一般标函 数 f ( x ) f(\boldsymbol{x}) f(x) ,只能使用一些近似的 β ( t ) \beta^{(t)} β(t) ,方法如:
Fletcher-Reeves共轭梯度法在极小值点附近可以确保收敛。
牛顿法利用Hessian阵和梯度向量求下一步,不需要一维搜索,对于二次多项式函数可以一步得到最小值点。 许多光滑的目标函数在靠近极小值点的邻域内可以很好地用凸二阶多项式逼近。对一个存在二阶连续偏导的多元函数 f ( x ) f(\boldsymbol{x}) f(x), 有如下的泰勒近似
f ( x ) ≈ f ( x ( 0 ) ) + ∇ f ( x 0 ) T ( x − x ( 0 ) ) + 1 2 ( x − x ( 0 ) ) T ∇ 2 f ( x ( 0 ) ) ( x − x ( 0 ) ) , (1) \tag{1} f(\boldsymbol{x}) \approx f\left(\boldsymbol{x}^{(0)}\right)+\nabla f\left(\boldsymbol{x}_0\right)^T\left(\boldsymbol{x}-\boldsymbol{x}^{(0)}\right)+\frac{1}{2}\left(\boldsymbol{x}-\boldsymbol{x}^{(0)}\right)^T \nabla^2 f\left(\boldsymbol{x}^{(0)}\right)\left(\boldsymbol{x}-\boldsymbol{x}^{(0)}\right), f(x)≈f(x(0))+∇f(x0)T(x−x(0))+21(x−x(0))T∇2f(x(0))(x−x(0)),(1)
若Hessian阵 ∇ 2 f ( x ( 0 ) ) \nabla^2 f\left(\boldsymbol{x}^{(0)}\right) ∇2f(x(0)) 为正定矩阵,上式右侧的二次多项式函数的最小值点在
x ∗ = x ( 0 ) − [ ∇ 2 f ( x ( 0 ) ) ] − 1 ∇ f ( x ( 0 ) ) (2) \tag{2} \boldsymbol{x}^*=\boldsymbol{x}^{(0)}-\left[\nabla^2 f\left(\boldsymbol{x}^{(0)}\right)\right]^{-1} \nabla f\left(\boldsymbol{x}^{(0)}\right) x∗=x(0)−[∇2f(x(0))]−1∇f(x(0))(2)
处达到。所以牛顿法取初值 x ( 0 ) \boldsymbol{x}^{(0)} x(0) 后,用公式
x ( t + 1 ) = x ( t ) − [ ∇ 2 f ( x ( t ) ) ] − 1 ∇ f ( x ( t ) ) , t = 0 , 1 , 2 , … (3) \tag{3} \boldsymbol{x}^{(t+1)}=\boldsymbol{x}^{(t)}-\left[\nabla^2 f\left(\boldsymbol{x}^{(t)}\right)\right]^{-1} \nabla f\left(\boldsymbol{x}^{(t)}\right), t=0,1,2, \ldots x(t+1)=x(t)−[∇2f(x(t))]−1∇f(x(t)),t=0,1,2,…(3)
进行迭代,直至收敛。收敛的判断准则可以取为 ∥ ∇ f ( x ) ∥ \|\nabla \boldsymbol{f}(x)\| ∥∇f(x)∥ 足够小,或者取为 ∣ f ( x ( t + 1 ) ) − f ( x ( t ) ) ∣ \left|f\left(\boldsymbol{x}^{(t+1)}\right)-f\left(\boldsymbol{x}^{(t)}\right)\right| ∣ ∣f(x(t+1))−f(x(t))∣ ∣ 足够 小。
公式(3)在算法实现时,应将逆矩阵表示改为如下的线性方程组求解:
∇ 2 f ( x ( t ) ) d ( t ) = − ∇ f ( x ( t ) ) , x ( t + 1 ) = x ( t ) − d ( t ) (4) \tag{4} \nabla^2 f\left(\boldsymbol{x}^{(t)}\right) \boldsymbol{d}^{(t)}=-\nabla f\left(\boldsymbol{x}^{(t)}\right), \\ \boldsymbol{x}^{(t+1)}=\boldsymbol{x}^{(t)}-\boldsymbol{d}^{(t)} ∇2f(x(t))d(t)=−∇f(x(t)),x(t+1)=x(t)−d(t)(4)
如果初值接近于最小值点并且目标函数满足一定正则性条件,牛顿法有二阶收敛速度。
牛顿法的性能对Hessian阵 H ( x ) H(\boldsymbol{x}) H(x) 十分依赖。
另外,从公式(1)看出,Hessian阵也是函数的局部性质的重要特征。
使用牛顿法求最小值时,最好的情况是Hessian阵始终为正定阵, 这时函数是严格凸函数,极小值点也是全局最小值点。在线性最小二乘问题中,目标函数的Hessian阵是 X T X X^T X XTX 矩阵。
如果目标函数非负定但是接近于不满秩,可以考虑给 H ( x ( t ) ) H\left(\boldsymbol{x}^{(t)}\right) H(x(t)) 加一个对角的正定阵,例如,岭回归就是将 X T X X^T X XTX 替换成 X T X + λ I X^T X+\lambda I XTX+λI 。
求函数 f ( x ) = x 2 − 4 x f(x)=x^2-4x f(x)=x2−4x的极值点。
//
// Created by CHH3213 on 2022/9/12.
//
#include
#include
using namespace std;
#define DELTA 1e-4
#define EPS 1e-6
/*
* 使用牛顿法求函数极值点
*/
double func(double x){
/**
* 构造函数
* x:自变量
* return:函数值
*/
return x*x-4*x;
}
double func_prime(double x){
/**
* 计算函数导数,以导数定义的方式求解
*/
return (func(x+DELTA)-func(x-DELTA))/(2*DELTA);
}
double func_prime_prime(double x){
/**
* 计算函数二阶导,以导数定义的方式求解
*/
return (func_prime(x+DELTA)-func_prime(x-DELTA))/(2*DELTA);
}
double Newton(double x0){
double x = x0;
double x_last=0.1;
while(abs(x-x_last)>EPS){
x_last=x;
x=x_last-func_prime(x_last)/ func_prime_prime(x_last);
}
return x;
}
int main(){
cout<<"extreme value x: "<<Newton(1)<<endl;
return 0;
}
退火是一种机械加工技术,加热金属零件到一定温度后进行加工,然后控制温度缓慢地下降到常温。在高 温时,分子运动速度较快, 在缓慢降温时可以调整机械应力、减少缺陷。
模拟退火算法的名称是利用了“缓慢降温”的做法和“温度”参数,这种算法利用随机性进行全局优化,在目标函数定义域进行随机游动,试图找到全局最小值点。
算法从当前位置 x ( t ) \boldsymbol{x}^{(t)} x(t) 按照某种抽样分布生成随机数 ϵ ( t ) \boldsymbol{\epsilon}^{(t)} ϵ(t) ,令 x ′ = x ( t ) + ϵ ( t ) \boldsymbol{x}^{\prime}=\boldsymbol{x}^{(t)}+\boldsymbol{\epsilon}^{(t)} x′=x(t)+ϵ(t) , 记 Δ y = f ( x ′ ) − f ( x ( t ) ) \Delta y=f\left(\boldsymbol{x}^{\prime}\right)-f\left(\boldsymbol{x}^{(t)}\right) Δy=f(x′)−f(x(t)) , 是否接受 x ′ \boldsymbol{x}^{\prime} x′作为下一步迭代的解取决于下面计算的概率 :
p ( t + 1 ) = { 1 , 若 Δ y ≤ 0 , exp ( − Δ y / T ( t + 1 ) ) , 若 Δ y > 0. p^{(t+1)}= \begin{cases}1, & \text { 若 } \Delta y \leq 0, \\ \exp \left(-\Delta y / T^{(t+1)}\right), & \text { 若 } \Delta y>0 .\end{cases} p(t+1)={1,exp(−Δy/T(t+1)), 若 Δy≤0, 若 Δy>0.
换句话说,如果生成的解 x ′ \boldsymbol{x}^{\prime} x′的函数值比前一个解的函数值更小,则接受 x ( t + 1 ) = x ′ \boldsymbol{x}^{(t+1)}=\boldsymbol{x}^{\prime} x(t+1)=x′作为一个新解。否则以概率 exp ( − Δ y / T ( t + 1 ) ) \exp \left(-\Delta y / T^{(t+1)}\right) exp(−Δy/T(t+1))接受 x ′ \boldsymbol{x}^{\prime} x′作为一个新解。否则令 x ( t + 1 ) = x ( t ) \boldsymbol{x}^{(t+1)}=\boldsymbol{x}^{(t)} x(t+1)=x(t) ,停留不动,等待下一次随机转移。
这里 T ( t + 1 ) T^{(t+1)} T(t+1) 是转移概率的参数, 类似于退火时 的温度, T ( t + 1 ) T^{(t+1)} T(t+1) 越大,则 p ( t + 1 ) p^{(t+1)} p(t+1) 越大,逆向的随机跳转越频繁,可以用来从局部极小值点跳开;
随着迭代的进行, T ( t + 1 ) T^{(t+1)} T(t+1) 应该逐步变小,使得逆向随机跳转变稀少, 这样迭代能最终收敛。
T ( t + 1 ) T^{(t+1)} T(t+1) 应选取为趋于 0 的数列, 在一定条件下如果取
T ( t ) = T ( 0 ) ln 2 ln ( t + 1 ) , T^{(t)}=T^{(0)} \frac{\ln 2}{\ln (t+1)}, T(t)=T(0)ln(t+1)ln2,
可以确保迭代收收敛到全局最小值点, 但显然这个速度太慢了。实际应用中往往用
T ( t + 1 ) = γ T ( t ) , T^{(t+1)}=\gamma T^{(t)}, T(t+1)=γT(t),
其中 0 < γ < 1 0<\gamma<1 0<γ<1 , 或取
T ( t ) = T ( 0 ) / t T^{(t)}=T^{(0)} / t T(t)=T(0)/t
求函数 f ( x ) = x 2 − 4 x f(x)=x^2-4x f(x)=x2−4x的极值点。
//
// Created by CHH3213 on 2022/9/12.
//
#include
#include
using namespace std;
#define DELTA 1e-4
#define EPS 1e-6
/*
* 使用模拟退火求函数极值点
*/
double func(double x){
/**
* 构造函数
* x:自变量
* return:函数值
*/
return x*x-4*x;
}
double rnd(double low,double high){
/**
* 返回指定范围内的随机浮点数
*/
double rd=rand()/((double)RAND_MAX+1.0);
return low+rd*(high-low);
}
double Annealing(double T0, double T_min, double x0,double gamma){
/**
* T0:初始温度
* T_min:温度的下限,若温度T达到T_min,则停止搜索
* x0:初始迭代值
* gamma:T_{k+1} = gamma * T_k, 0
double T=T0;
double x = x0;
double x_ = 2;
double delta_y=1.0;
while(abs(delta_y)>EPS){//如果函数值满足精度,停止搜索
// while(T>T_min){//温度T达到T_min,则停止搜索
double x_ = x+rnd(-1,1);
cout<<x_<<endl;
delta_y = func(x_)-func(x);
if(delta_y<=0){
x = x_;
}else{
if(exp(-delta_y/T)>rnd(0,1)){
x=x_;
}
}
T=gamma*T;
cout<<x<<endl;
}
return x;
}
int main(){
double T0=200,T_min = 1,x0=2,gamma=0.9;
cout<<"extreme value: "<<Annealing(T0,T_min,x0,gamma)<<endl;
return 0;
}
模拟退火算法是利用随机移动来寻找全局最优点。遗传算法是另一类算法的典型代表,它产生多个搜索点, 利用这些搜索点以及搜索点之间的交互去接近全局最优点。
遗传算法利用了生物学中择优配对、交叉遗传、遗传突变的思想使得整个种群(搜索点集合)不断进化 (接 近全局最优点)。设要求 min x ∈ R d f ( x ) \min _{\boldsymbol{x} \in \mathbb{R}^d} f(\boldsymbol{x}) minx∈Rdf(x) , 算法的核心包括:
这种利用多个搜索点的方法还有许多, 例如“粒子蜂拥”方法,记录每一个 搜索点的位置、速度、历史最优点,更新速度为倾向于自己的历史最优点和群体的历史最优点。“茧火虫”方 法,是每个搜索点都逐次被每一个优于自己的点吸引。
这些利用群体随机变化寻找全局最优的方法有利用查找比较广泛的定义域,但是收敛到最小值点比较慢, 所以可以配合一些局部搜索算法;可以对每一个搜索点都作局部搜索,并更新搜索点和目标函数值;也可以对每一个搜索点都作局部搜索后,仅更新其搜索得到的局部更优的目标函数值,但并不更新搜索点本 身,这种做法更能避开局部最小点的吸引。