写在前面的说明:

  • 查阅了很多资料,发现资料里对于非精确线搜索的Wolfe-Powell准则求步长都讲得很粗糙,中英文的教材里(黄红选.数学规划 4.2.1节、Jorge Nocedal.Numerical Optimization 3.1节等)只介绍了Wolfe-Powell准则(Wolfe Conditions)的原理,没有给出算法步骤;而能查到的W-P代码实现,都只给了代码,也没有给算法步骤。
  • 关于Wolfe Conditions的数学原理可以看Numerical Optimization一书,不再具体介绍。这篇笔记主要介绍了怎样把原理转化为算法步骤,再转化为Python代码。
  • 关于二次插值法,大多数教材里面只讲了三点二次插值法(陈宝林.最优化理论与算法 9.3.3节),这篇笔记里二点二次插值的内插和外插公式是自己推的,如果有问题也可以指出~

1 准备知识:二次插值法

1.1 概述

二次插值法(抛物线法)基本思路:在极小点附近,用二次三项式python格点插值到某个点上_插值法逼近目标函数python格点插值到某个点上_python格点插值到某个点上_02

分为三点二次插值法和二点二次插值法:

  • 三点二次插值法:已知三点的函数值,求极小点
  • 二点二次插值法:
  • 已知两点的函数值,和其中一点的导数值,求极小点(内插)
  • 已知两点的导数值,求极小点(外插)

1.2 二点二次插值法

(1)内插:找两点之间的极小点

令二次三项式python格点插值到某个点上_算法_03

已知python格点插值到某个点上_插值法_04python格点插值到某个点上_python_05python格点插值到某个点上_算法_06,其中python格点插值到某个点上_方程组_07

为保证二次插值函数python格点插值到某个点上_插值法有极小点,要求python格点插值到某个点上_方程组_09或者python格点插值到某个点上_python_10

得到以下方程组:
python格点插值到某个点上_python_11
解方程组,得python格点插值到某个点上_python格点插值到某个点上_12python格点插值到某个点上_方程组_13python格点插值到某个点上_算法_14

为求python格点插值到某个点上_插值法的极小点,则令python格点插值到某个点上_python_16python格点插值到某个点上_python格点插值到某个点上_17,将python格点插值到某个点上_python格点插值到某个点上_18代入得极小点
python格点插值到某个点上_方程组_19

(2)外插:找两点之外的极小点

令二次三项式python格点插值到某个点上_python格点插值到某个点上_20

已知python格点插值到某个点上_python_05python格点插值到某个点上_方程组_22

为保证二次插值函数python格点插值到某个点上_插值法有极小点,要求python格点插值到某个点上_python_24

得到以下方程组:
python格点插值到某个点上_方程组_25
解方程组,得python格点插值到某个点上_python格点插值到某个点上_26python格点插值到某个点上_python格点插值到某个点上_27

为求python格点插值到某个点上_插值法的极小点,则令python格点插值到某个点上_插值法_29python格点插值到某个点上_方程组_30,将python格点插值到某个点上_python格点插值到某个点上_18代入得极小点
python格点插值到某个点上_算法_32

2 准备知识:Wolfe Conditions

python格点插值到某个点上_python_33

其中python格点插值到某个点上_python格点插值到某个点上_34


3 基于二点二次插值的Wolfe-Powell法

3.1 基本思路

  • 满足python格点插值到某个点上_算法_35,且满足python格点插值到某个点上_方程组_36,直接输出步长python格点插值到某个点上_python_37
  • 满足python格点插值到某个点上_算法_35,但不满足python格点插值到某个点上_方程组_36,用外插公式增大步长python格点插值到某个点上_python_37(见Step 3)
  • 不满足python格点插值到某个点上_算法_35,用内插公式缩小步长python格点插值到某个点上_python_37(见Step 2)

3.2 计算步骤

Step 1.

选取初始数据,给定初始搜索区间python格点插值到某个点上_python格点插值到某个点上_43,给出参数python格点插值到某个点上_python格点插值到某个点上_34,令python格点插值到某个点上_python_45,取python格点插值到某个点上_插值法_46,计算python格点插值到某个点上_python格点插值到某个点上_47python格点插值到某个点上_python格点插值到某个点上_48

def WolfePowell(f,d,x,alpha_,rho,sigma):
    a1 = 0
    a2 = alpha_
    alpha = (a1+a2)/2

输入的初始数据有,f:目标函数,d:初始方向,x:初始点,alpha_python格点插值到某个点上_python格点插值到某个点上_49rhopython格点插值到某个点上_python_50sigmapython格点插值到某个点上_python_51。另外,a1python格点插值到某个点上_python_52a2python格点插值到某个点上_python_53alphapython格点插值到某个点上_方程组_54,算法中取python格点插值到某个点上_python格点插值到某个点上_55

phi1 = fun(x) 
phi1_ = np.dot(grad,d)

phi1python格点插值到某个点上_python_56phi1_python格点插值到某个点上_python_57,求梯度的方法会在后面说明。

Step 2.

计算python格点插值到某个点上_python格点插值到某个点上_58

python格点插值到某个点上_插值法_59条件1)成立,转到Step 3;否则,由二点二次插值法的内插公式计算python格点插值到某个点上_python_60
python格点插值到某个点上_方程组_61
python格点插值到某个点上_python_62python格点插值到某个点上_算法_63,转到Step 2

if phi <= phi1 + rho * alpha * phi1_:
    ……
else:
    alpha_new = a1 - 0.5*(alpha-a1)**2*phi1_/((phi-phi1)-(alpha-a1)*phi1_)
    a2 = alpha
    alpha = alpha_new

Step 3.

计算python格点插值到某个点上_插值法_64

python格点插值到某个点上_插值法_65条件2)成立,则令python格点插值到某个点上_python格点插值到某个点上_66,输出python格点插值到某个点上_python格点插值到某个点上_67,停止;否则,由二点二次插值法的外插公式计算python格点插值到某个点上_python_60
python格点插值到某个点上_插值法_69

if phi <= phi1 + rho * alpha * phi1_:
    ……
    if phi_ >= sigma*phi0_:
        break
    else:
        alpha_new = alpha + (alpha-a1) *phi_ / (phi1_-phi_)
        a1 = alpha
        alpha = alpha_new
        phi1 = phi
        phi1_ = phi_
else:
    ……

4 对Wolfe-Powell法的改进

4.1 计算步长方法的改进

  • 网上大多数的W-P程序中,采用如下的方法计算步长α

本文的算法中,二点二次插值法的内插和外插,分别对应上面第(3)步和第(4)步。但不同之处在于对步长放缩的方式:

  • 转到第(3)步时,不满足python格点插值到某个点上_算法_70,所以要缩小步长python格点插值到某个点上_插值法_71。此时步长python格点插值到某个点上_插值法_71太大,取python格点插值到某个点上_插值法_71和步长下界python格点插值到某个点上_python格点插值到某个点上_74的中值为新步长,就能使步长缩小。因为python格点插值到某个点上_算法_75,所以步长最多缩小到原来的python格点插值到某个点上_方程组_76。但是,这种方法得到的新步长,并不一定能使目标函数最小。采用二次插值法的内插法,可以找到python格点插值到某个点上_算法_77之间的极小点,能使目标函数近似最小,提高了算法效率。
  • 转到第(4)步时,满足python格点插值到某个点上_算法_70,但不满足python格点插值到某个点上_算法_79,所以要增大步长python格点插值到某个点上_插值法_71。此时步长python格点插值到某个点上_插值法_71太小,取python格点插值到某个点上_插值法_71和步长上界python格点插值到某个点上_算法_83的中值为新步长,就能使步长放大。因为python格点插值到某个点上_算法_84,步长最多放大到原来的python格点插值到某个点上_插值法_85倍。但是,这种方法得到的新步长,并不一定能使目标函数最小。采用二次插值法的外插法,可以找到python格点插值到某个点上_算法_77之外(大于python格点插值到某个点上_插值法_87一侧)的极小点,能使目标函数近似最小,提高了算法效率。

4.2 求梯度方法的改进

  • 在有些程序中,需要手动计算梯度并以数组形式输入
def fun(x):
    return x[0]**2+2*x[1]**2-2*x[0]*x[1]+2*x[1]+2
def gfun(x):
    return np.array([2*x[0]-2*x[1], 4*x[1]-2*x[0]+2])
  • 在另一些程序中,采用了数值法求梯度,数值法在参数比较少的情况下,效果较好;但参数极多的情况下,计算量非常大、运行效率极差;常用于检验所写梯度的正确性。
def num_gradient(f, x, delta = DELTA):
    """ 计算数值梯度 """
    # 中心差分
    try:
        params = x.tolist()
        gradient_vector = np.array([(f(*(params[:i] + [params[i] + delta] + params[i + 1:]))
                                    -f(*(params[:i] + [params[i] - delta] + params[i + 1:])))
                                    /(2 * delta) for i in range(len(params))])
        return gradient_vector
    # 若中心差分报错(例如根号x在0处求梯度,定义域不可行),改为前向差分或后向差分
    except:
        try:
            params = x.tolist()
            gradient_vector = np.array([(f(*(params[:i] + [params[i] + delta] + params[i + 1:]))
                                        -f(*(params[:])))
                                        /delta for i in range(len(params))])
            return gradient_vector
        except:
            params = x.tolist()
            gradient_vector = np.array([(-f(*(params[:i] + [params[i] - delta] + params[i + 1:]))
                                        +f(*(params[:])))
                                        /delta for i in range(len(params))])
            return gradient_vector

因此,我们对求梯度方法进行了改进,采用了解析法,也就是先计算出偏导表达式,再形成梯度向量。用解析法求得的梯度更为精确,效果更好。

dx = []
grad = []
for a in range(dimensions):
    dx.append(diff(f, symbols('x'+str(a),real=True)))  #偏导表达式,梯度
    item={}
    for i in range(dimensions):
        item[x_[i]] = x[i]
    grad.append(dx[a].subs(item))  #梯度值

若输入为:

f = 100*(x_[1]-x_[0]**2)**2+(1-x_[0])**2
x = [2,2]
d = [-1,-1]
print(dx)
print(grad)

输出为:

[-400*x0*(-x0**2 + x1) + 2*x0 - 2, -200*x0**2 + 200*x1] #偏导表达式,梯度
[1602,-400]  #梯度值

4.3 统计维度方法的改进

  • 在有些程序中,需要手动输入目标函数的维度:
n = int(input("please input the dimensions n="))

我们利用正则表达式来统计目标函数的维度:

import re
dimension_set = []
dimension_set = re.findall(r'x[0-9]\d*',str(f))  
  #统计x0,x1,...总数,'x[0-9]\d*'表示非负整数,\d表示单个数字字符,*表示前面的字符0次或多次
dimensions = len(set(dimension_set))  # 用set()去重

5 Python代码实现

import numpy as np
from sympy import *
import re

'''Wolfe-Powell非精确线性搜索,返回函数f在x处,方向d时的步长alpha'''
def WolfePowell(f,d,x,alpha_,rho,sigma):
    maxk = 1000  #迭代上限
    k = 0
    phi0 = fun(x) 
    dimensions = dim(f_)

    dx = []
    grad = []
    for a in range(dimensions):
        dx.append(diff(f, symbols('x'+str(a),real=True))) #求偏导
        item={}
        for i in range(dimensions):
            item[x_[i]] = x[i]
        grad.append(dx[a].subs(item)) #求梯度
        
    phi0_ = np.dot(grad,d)
    print(dx)
    print(grad)
    
    a1 = 0
    a2 = alpha_
    alpha = (a1+a2)/2
    phi1 = phi0
    phi1_ = phi0_

    k = 0;
    for k in range(maxk):    #限制迭代上限,避免时间太长
        phi = fun(x + alpha * d)
        if phi <= phi1 + rho * alpha * phi1_:
            newx = x + alpha * d
            newdx = []
            newgrad = []
            for a in range(dimensions):
                newdx.append(diff(f, symbols('x'+str(a),real=True)))  # 求偏导
                newitem={}
                for i in range(dimensions):
                    newitem[x_[i]] = newx[i]
                newgrad.append(newdx[a].subs(newitem)) #求梯度

            phi_ = np.dot(newgrad,d)
            
            if phi_ >= sigma*phi0_:
                break
            else:
                alpha_new = alpha + (alpha-a1) *phi_ / (phi1_-phi_)
                a1 = alpha
                alpha = alpha_new
                phi1 = phi
                phi1_ = phi_
        else:
            alpha_new = a1 + 0.5*(a1-alpha)**2*phi1_/((phi1-phi)-(a1-alpha)*phi1_)
            a2 = alpha
            alpha = alpha_new
    k = k + 1
    return alpha

'''利用正则表达式统计目标函数维度'''
def dim(f_):
    dimension_set = []
    dimension_set = re.findall(r'x[0-9]\d*',str(f_))
    dimensions = len(set(dimension_set))
    return dimensions
'''测试'''
x_ = []
for a in range(100):
    x_.append(symbols('x'+str(a),real=True))  #设置符号变量
   
def fun(x_):
    return 100*(x_[1]-x_[0]**2)**2+(1-x_[0])**2  #目标函数
f_ = fun(x_)   #用于W-P

alpha_ = 1  #alpha_max
rho = 0.25  # rho∈(0,1/2) 
sigma = 0.5  # sigma∈(rho,1)
x = np.random.rand(dim(f_))   #随机的初始点
d = np.array([-1,-1])     #初始方向

alpha=WolfePowell(f_,d,x,alpha_,rho,sigma)
print(alpha)