【python】最優化方法之一維搜索(黃金分割法+斐波那契法)

1.概念

\qquad一維搜索是最優化方法最簡單的一種,即求一個在(a,b)內,連續下單峯函數f(x)f(x)的極小值。所謂下單峯函數就是隻有一個極小值的函數。

2.遍歷搜索

\qquad這個問題看似簡單,我們只需要指定步長,從區間左端點搜索到右端點,每次根據步長新產生一個值,比對它和前兩個值的大小。記錄三個值從左到由依次爲x1x_1,x2x_2,x3x_3,若出現x1>x2<x3x_1>x_2<x_3的情況,則說明極小值點必定落在x1,x3(x_1,x_3)之間。因此如果指定了最終區間不得超過eps的話,那麼搜索步長不得大於0.5eps。
我們很快就會發現,這種遍歷法雖然算法簡單,但是迭代次數多,浪費資源嚴重,因此我們需要更快地得到最終區間的方法。在最優化方法這個數學分支中,可以證明斐波拉契法是壓縮比(在同樣迭代次數下開始區間和最終區間的比值)最高的方法,黃金分割比相比它壓縮比小一些,但是算法會比較簡單,佔用計算資源也會略少。

3.優化算法

3.1.一維搜索原則

\qquad首先在初始區間[a,b]中取兩個點x1,x2(x1<x2)x_1,x_2(x_1<x_2),計算f(a),f(x1),f(x2),f(b)f(a),f(x_1),f(x_2),f(b)的值。由於f(x)f(x)在(a,b)爲下單峯函數,即有唯一的極小值,因此a或b必定不是極小值點。
示例圖
1.若f(x1)<f(x2)f(x_1)<f(x_2)則說明(x1,x2)(x_1,x_2)我下降段,極小值xx^*點應該在x1x_1的右側,因此修改查找區間爲(x1,b)(x_1,b)
2.若f(x1)>f(x2)f(x_1)>f(x_2)則說明(x1,x2)(x_1,x_2)我上升段,極小值xx^*點應該在x2x_2的左側,因此修改查找區間爲(a,x2)(a,x_2)
一維搜索算法的核心就是解決取x1x2x_1和x_2的問題

3.2.黃金分割法

幾何畫板
\qquad設黃金分割比爲k,取x1=b(ba)k,x2=a+(ba)kx_1=b-(b-a)*k,x_2=a+(b-a)*k
在一維搜索中,如果滿足條件1,那麼需要在(x1,b)(x_1,b)取值。設ρ=1k\rho=1-k
CD=1ρ,DE=ρ,EF=1ρ\overline{CD}=1-\rho,\overline{DE}=\rho,\overline{EF}=1-\rho由於在黃金分割比的作用下,1ρ1=12ρ1ρ\frac{1-\rho}{1}=\frac{1-2\rho}{1-\rho},因此DEGH=DEDF=DFCF=k0.618\frac{DE}{GH}=\frac{DE}{DF}=\frac{DF}{CF}=k≈0.618
因此若將E點平移至GH線段,將G的橫座標當做a,H的橫座標當做b,它就相當於是x1x_1的位置,因此只需要計算x2=a+(ba)k=b(ba)ρx_2=a+(b-a)*k=b-(b-a)*\rho即可
同理,如果滿足條件2,DEIJ=DECE=CECF=k0.618\frac{DE}{IJ}=\frac{DE}{CE}=\frac{CE}{CF}=k≈0.618D點相當於IJ線段的x2x_2的位置,因此只需要計算x1=b(ba)k=a+(ba)ρx_1=b-(b-a)*k=a+(b-a)*\rho

Code Block

編寫用戶自定義函數的代碼講解參見以下鏈接:

[動態創建數學函數(CSDN])
以下爲黃金分割法進行一維搜索的代碼:

from math import *
import matplotlib.pyplot as plt
from pylab import *
# 通用函數f(x)靠用戶錄入
def function(x):
    fx = str_fx.replace("x", "%(x)f")  # 所有的"x"換爲"%(x)function"
    return eval(fx % {"x": x})  # 字典類型的格式化字符串,將所有的"x"替換爲變量x


# 繪圖函數:給定閉區間(繪圖間隔),繪圖間隔默認爲0.05,若區間較小,請自行修改
def drawf(a,b,interp=0.05):
    x = [a+ele*interp for ele in range(0, int((b-a)/interp))]
    y = [function(ele) for ele in x]
    plt.figure(1)
    plt.plot(x, y)
    xlim(a, b)
    title(init_str, color="b")
    plt.show()

# 黃金分割法進行一維搜索的函數
def gold_div_search(a,b,esp):
    data=list()
    x1=a+rou*(b-a)
    x2=b-rou*(b-a)
    data.append([a,x1,x2,b])
    while(b-a>esp):
        if function(x1)>function(x2):  #如果f(x1)>function(x2),則在區間(x1,b)內搜索
            a=x1
            x1=x2
            x2=b-rou*(b-a)
            plt.plot(x2,function(x2),'r*')
        elif function(x1)<function(x2):  #如果f(x1)<function(x2),則在區間(a,x2)內搜索
            b=x2
            x2=x1
            x1=a+rou*(b-a)
            plt.plot(x1,function(x1),'r*')
        else:  #如果f(x1)=function(x2),則在區間(x1,x2)內搜索
            a=x1
            b=x2
            x1=a+rou*(b-a)
            x2=b-rou*(b-a)
            plt.plot(x1,function(x1),'r*',x2,function(x2),'r*')
        data.append([a,x1,x2,b])
    with open("一維搜索(黃金分割法).txt",mode="w",encoding="utf-8")as a_file: 
    # 保存的txt文件在程序的同目錄下
        for i in range(0,len(data)):
            a_file.write("%d:\t"%(i+1))
            for j in range(0,4):
                a_file.write("function(%.3f)=%.3f\t"%(data[i][j],function(data[i][j])))
            a_file.write("\n")
    print("寫入文件成功!")
    return [a,b]

rou = 1-(sqrt(5)-1)/2  # 1-rou爲黃金分割比
init_str = input("請輸入一個函數,默認變量爲x:\n")  # 輸入的最初字符串
para=input("請依次輸入一維搜索的區間a,b和最終區間的精確值(用空格分隔)").split() # 導入區間
para=[float(ele) for ele in para]
a,b,esp=para
str_fx = init_str.replace("^", "**")  # 將所有的“^"替換爲python的冪形式"**"
gold_div_search(a,b,esp)  # 調用黃金分割法並保存文件
drawf(a,b,(b-a)/2000)  # 繪製函數圖形

以下爲交互式界面的效果:(請在Shell或Pycharm中的Python Console裏面運行)

    請輸入一個函數,默認變量爲x:
    atan(x)-log(2*x+1)+4*x**2-3*x
    請依次輸入一維搜索的區間a,b和最終區間的精確值(用空格分隔)0 1 0.001
    寫入文件成功!

文本文檔數據:(如用此程序運行,保存的文本文檔與程序同目錄

1: f(0.000)=0.0000000 f(0.382)=-0.7649875 f(0.618)=-0.5773825 f(1.000)=0.6867859
2: f(0.000)=0.0000000 f(0.236)=-0.6401822 f(0.382)=-0.7649875 f(0.618)=-0.5773825
3: f(0.236)=-0.6401822 f(0.382)=-0.7649875 f(0.472)=-0.7485370 f(0.618)=-0.5773825
4: f(0.236)=-0.6401822 f(0.326)=-0.7399126 f(0.382)=-0.7649875 f(0.472)=-0.7485370
5: f(0.326)=-0.7399126 f(0.382)=-0.7649875 f(0.416)=-0.7669244 f(0.472)=-0.7485370
6: f(0.382)=-0.7649875 f(0.416)=-0.7669244 f(0.438)=-0.7630202 f(0.472)=-0.7485370
7: f(0.382)=-0.7649875 f(0.403)=-0.7673941 f(0.416)=-0.7669244 f(0.438)=-0.7630202
8: f(0.382)=-0.7649875 f(0.395)=-0.7669382 f(0.403)=-0.7673941 f(0.416)=-0.7669244
9: f(0.395)=-0.7669382 f(0.403)=-0.7673941 f(0.408)=-0.7673906 f(0.416)=-0.7669244
10: f(0.395)=-0.7669382 f(0.400)=-0.7672874 f(0.403)=-0.7673941 f(0.408)=-0.7673906
11: f(0.400)=-0.7672874 f(0.403)=-0.7673941 f(0.405)=-0.7674185 f(0.408)=-0.7673906
12: f(0.403)=-0.7673941 f(0.405)=-0.7674185 f(0.406)=-0.7674176 f(0.408)=-0.7673906
13: f(0.403)=-0.7673941 f(0.404)=-0.7674129 f(0.405)=-0.7674185 f(0.406)=-0.7674176
14: f(0.404)=-0.7674129 f(0.405)=-0.7674185 f(0.406)=-0.7674196 f(0.406)=-0.7674176
15: f(0.405)=-0.7674185 f(0.406)=-0.7674196 f(0.406)=-0.7674194 f(0.406)=-0.7674176
16: f(0.405)=-0.7674185 f(0.405)=-0.7674193 f(0.406)=-0.7674196 f(0.406)=-0.7674194

可以看出我們的最終區間爲(0.405,0.406),精度爲0.001.
黃金分割法查找的點會被記錄在matplotlib繪製的函數圖像上,一目瞭然,可以看出,在最終區間精度爲0.001的情況下,我們的算法收斂速度很快。
總覽
圖像2
\qquad繪圖可以看出函數在極小點附近非常平坦,極小值點差不多在0.405的位置,與文本文檔的數據一致。

3.3.斐波拉契法

\qquad斐波拉契法是一維搜索中壓縮比最高的搜索算法。斐波拉契法基於斐波拉契數列產生比例值,斐波拉契數列{Fn}\lbrace F_n \rbrace的定義如下:
F1=F2=1,Fn+2=Fn+1+FnF_1=F_2=1,F_{n+2}=F_{n+1}+F_{n}
取分點時,x1=a+Fn2Fn(ba),x2=a+Fn1Fn(ba)x_1=a+\frac{F_{n-2}}{F_n}(b-a),x_2=a+\frac{F_{n-1}}{F_n}(b-a)

{x1a=Fn2Fn(ba)x2a=Fn1Fn(ba)x2x1=Fn1Fn2Fn(ba)=Fn3Fn(ba)bx2=FnFn1Fn(ba)=Fn2Fn(ba)\begin{cases} x_1-a=\frac{F_{n-2}}{F_n}(b-a) \\x_2-a=\frac{F_{n-1}}{F_n}(b-a) \\x_2-x_1=\frac{F_{n-1}-F_{n-2}}{F_n}(b-a)=\frac{F_{n-3}}{F_n}(b-a) \\b-x_2=\frac{F_{n}-F_{n-1}}{F_n}(b-a)=\frac{F_{n-2}}{F_n}(b-a) \end{cases}

幾何畫板
1.若x1x2x_1<x_2,則取新區間爲[a,x2][a,x_2],則DE=Fn3Fn(ba)IJ=Fn1Fn(ba),CD=Fn2Fn(ba),CDIJ=Fn2Fn1DIJx2x1=a+Fn3Fn1(ba)\overline{DE}=\frac{F_{n-3}}{F_n}(b-a),\overline{IJ}=\frac{F_{n-1}}{F_n}(b-a),\overline{CD}=\frac{F_{n-2}}{F_n}(b-a), \\ \frac{CD}{IJ}=\frac{F_{n-2}}{F_{n-1}},因此D相當於IJ中的x_2的位置\\只需要再取x_1=a+\frac{F_{n-3}}{F_{n-1}}(b-a)即可

2.若x1&gt;x2x_1&gt;x_2,則取新區間爲[x1,b][x_1,b],則DE=Fn3Fn(ba)GH=Fn1Fn(ba),CD=Fn2Fn(ba),DEGH=Fn3Fn1DGHx1x2=a+Fn2Fn1(ba)\overline{DE}=\frac{F_{n-3}}{F_n}(b-a),\overline{GH}=\frac{F_{n-1}}{F_n}(b-a),\overline{CD}=\frac{F_{n-2}}{F_n}(b-a), \\ \frac{DE}{GH}=\frac{F_{n-3}}{F_{n-1}},因此D相當於GH中的x_1的位置\\只需要再取x_2=a+\frac{F_{n-2}}{F_{n-1}}(b-a)即可

對於要求精度esp,取壓縮比C=baespFn=min{xxC,xN+}C=\frac{b-a}{esp},F_n=min\lbrace x|x≥C,x∈N^+\rbrace,按照上述方法迭代至分母爲F2F_2爲止,最後按照以下方法進行:
在這裏插入圖片描述

Code Block

首先我們需要一個生產斐波那契數列(Fabonaci)的函數:

# 在本案例中,使用一次性生成斐波拉契列表的方法算法複雜度更低
# 生成的斐波拉契數列F(0)=0,F(1)=F(2)=1,剩餘項和普通斐波拉契數列類似
def Fab_list(Fmax):
    a,b=0,1
    Fablist = [a,b]  # 返回一個列表
    while Fablist[-1]< Fmax:
        a,b = b,a+b
        Fablist.append(b)
    return Fablist  

再按照斐波那契法迭代編寫算法即可:
通用函數的編寫仍然參考以下鏈接:
[動態創建數學函數(CSDN)]
注:需要安裝matplotlib模塊、pylab模塊和Pillow模塊,版本爲python3

步驟:
Windows鍵+R,在【運行】窗口輸入cmd並回車,在命令提示行下輸入:
pip install matplotlib
pip install pylab
pip install Pillow
即可

from math import *
import matplotlib.pyplot as plt  # 繪圖模塊
from pylab import *  # 繪圖輔助模塊



# 通用函數f(x)靠用戶錄入
def function(x):
    fx = str_fx.replace("x", "%(x)f")  # 所有的"x"換爲"%(x)function"
    return eval(fx % {"x": x})  # 字典類型的格式化字符串,將所有的"x"替換爲變量x


# 繪圖函數:給定閉區間(繪圖間隔),繪圖間隔默認爲0.05,若區間較小,請自行修改
def drawf(a, b, interp=0.05):
    x = [a + ele * interp for ele in range(0, int((b - a) / interp))]
    y = [function(ele) for ele in x]
    plt.figure(1)
    plt.plot(x, y)
    xlim(a, b)
    title(init_str, color="b")  # 標註函數名
    plt.show()
    return "繪圖完成!"


# 在本案例中,使用一次性生成斐波拉契列表的方法算法複雜度更低
# 生成的斐波拉契數列F(0)=0,F(1)=F(2)=1,剩餘項和普通斐波拉契數列類似
def Fab_list(Fmax):
    a,b=0,1
    Fablist = [a,b]  # 返回一個列表
    while Fablist[-1]< Fmax:
        a,b = b,a+b
        Fablist.append(b)
    Fablist.pop()  # 舍掉最後一個元素
    return Fablist    


def Fabonaci_search(a, b, Fab):
    n = len(Fab)-1  # 獲取斐波那契數列的長度
    if n < 3:
        return "精度過低,無法進行斐波那契一維搜索"
    data = list()
    data.append([a, b])
    x1 = a + Fab[n - 2]/Fab[n] * (b - a)
    x2 = a + Fab[n - 1]/Fab[n] * (b - a)
    t = n
    while (t > 3):
        if function(x1) > function(x2):  # 如果f(x1)>f(x2),則在區間(x1,b)內搜索
            a = x1
            x1 = x2
            x2 = a + Fab[t - 1]/Fab[t] * (b - a)
            plt.plot(x2, function(x2), 'r*')
            data.append([x1, b])
        elif function(x1) < function(x2):  # 如果f(x1)<(x2),則在區間(a,x2)內搜索
            b = x2
            x2 = x1
            x1 = a + Fab[t - 2]/Fab[t]* (b - a)
            plt.plot(x1, function(x1), 'r*')
            data.append([a, x2])
        else:  # 如果f(x1)=function(x2),則在區間(x1,x2)內搜索
            a = x1
            b = x2
            x1 = a + Fab[t - 2]/Fab[t] * (b - a)
            x2 = a + Fab[t - 1]/Fab[t] * (b - a)
            plt.plot(x1, function(x1), 'r*', x2, function(x2), 'r*')
        data.append([x1, x2])
        t -= 1
    x1 = a + 0.5 * (b - a)  # 斐波那契數列第3項和第2項的比
    x2 = x1 + 0.1 * (b - a)  # 偏離一定距離,人工構造的點
    if function(x1) > function(x2):  # 如果f(x1)>function(x2),則在區間(x1,b)內搜索
        plt.plot(x2, function(x2), 'r*')
        data.append([x1, b])
        a = x1
    elif function(x1) < function(x2):  # 如果f(x1)<function(x2),則在區間(a,x2)內搜索
        plt.plot(x1, function(x1), 'r*')
        data.append([a, x2])
        b = x2
    else:  # 如果f(x1)=function(x2),則在區間(x1,x2)內搜索
        plt.plot(x1, function(x1), 'r*', x2, function(x2), 'r*')
        data.append([x1, x2])
    with open(r"C:\Users\ouni\桌面\一維搜索(黃金分割法).txt", mode="w", encoding="utf-8")as a_file:
        for i in range(0, len(data)):
            a_file.write("%d:\t" % (i + 1))
            for j in range(0, 2):
                a_file.write("f(%.3f)=%.7f\t" % (data[i][j], function(data[i][j])))
            a_file.write("\n")
    print("寫入文件成功!")
    return [a, b]


init_str = input("請輸入一個函數,默認變量爲x:\n")  # 輸入的最初字符串
para = input("請依次輸入一維搜索的區間a,b和最終區間的精確值(用空格分隔)").split()  # 導入區間
para = [float(ele) for ele in para]  # 將輸入的字符串轉換爲浮點數
low, high, esp = para  # 輸入參數列表(最小值、最大值和最終精度)
str_fx = init_str.replace("^", "**")  # 將所有的“^"替換爲python的冪形式"**"
print(Fabonaci_search(low, high, Fab_list(int(ceil((high-low)/esp)))))  # 傳入區間和斐波拉契列表
drawf(low, high, (high - low) / 2000)  # 默認精度是2000個點


以下代碼請在交互式界面運行:

請輸入一個函數,默認變量爲x:
e^(-x)+x^2
請依次輸入一維搜索的區間a,b和最終區間的精確值(用空格分隔)0 1 0.01
寫入文件成功!
[0.3402663805075117, 0.35279320792829183]
關閉繪圖框以繼續交互式命令

文本文檔如下:

1: f(0.000)=1.0000000 f(1.000)=1.3678794
2: f(0.000)=1.0000000 f(0.382)=0.8284208
3: f(0.236)=0.8454509 f(0.382)=0.8284208
4: f(0.382)=0.8284208 f(0.618)=0.9209301
5: f(0.382)=0.8284208 f(0.472)=0.8465897
6: f(0.236)=0.8454509 f(0.382)=0.8284208
7: f(0.326)=0.8280571 f(0.382)=0.8284208
8: f(0.236)=0.8454509 f(0.326)=0.8280571
9: f(0.292)=0.8320851 f(0.326)=0.8280571
10:f(0.326)=0.8280571 f(0.382)=0.8284208
11: f(0.326)=0.8280571 f(0.347)=0.8272109
12: f(0.347)=0.8272109 f(0.382)=0.8284208
13: f(0.347)=0.8272109 f(0.361)=0.8273036
14: f(0.326)=0.8280571 f(0.347)=0.8272109
15: f(0.340)=0.8273620 f(0.347)=0.8272109
16: f(0.347)=0.8272109 f(0.361)=0.8273036
17: f(0.347)=0.8272109 f(0.354)=0.8271921
18: f(0.340)=0.8273620 f(0.353)=0.8271855
}

圖形如下:
在這裏插入圖片描述
在這裏插入圖片描述
可以看出函數的極小值確實在0.35附近,與文本文檔標定的一致。
注:以上程序請在Shell或Cmd中運行,如需當做模塊運行,請在主要程序的前面加上if __name__=="__main__"

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章