解最優化問題

解最優化問題

本文版權屬於重慶大學計算機學院劉驥,禁止轉載

最優化問題的分類

按照目標函數的可導性,可以將最優化問題分爲兩類:
1. 目標函數可導,例如目標函數爲f(x)=x2+x+1
2. 目標函數不可導,例如目標函數爲f(X)=pXep

對於不可導的目標函數,其解法比較特殊。採用模擬退火算法、粒子羣算法、遺傳算法、蟻羣算法、圖割(Graph Cut)算法等方法,可以求解這類問題,但通常無法得到精確的解(或者說全局最優解)。並且有些問題,僅能用特定的解法,求出特定的解。由於該類問題的複雜性,本文對不可導目標函數問題不進行討論。如果你在實際中遇到了這類問題,可以採用如下方法解決:
查學術論文,github上找代碼;如果找不到解決方案,恭喜你,這是一個獲得數學大獎的機會

本文餘下部分,針對可導的目標函數進行討論,這類問題存在大量通用的算法。人類歷史發展到2017年,你甚至不需要了解具體算法的原理,也能夠很happy的解決這類問題。

直接開始求解

下面我們用一個簡單的最優化問題爲例:

argminx,yf(x,y)=x2+y2+1

顯然x=0y=0 時,f(x,y) 有最小值1。問題在於,計算機如何求解這個問題呢?

downhill simplex

Python語言的scipy.optimize包的fmin函數實現了downhill simplex方法,也稱爲Nelder–Mead方法(downhill、simplex和downhill simplex是不同的概念,有興趣可以參考維基百科)。這裏不去講解downhill simplex的原理,有需要原理的讀者,請移步到https://en.wikipedia.org/wiki/Nelder–Mead_method
fmin函數的說明如下:
Alt text
我估計你看不懂,所以我不準備解釋。我們直接看代碼吧。

# encoding=utf-8
import scipy.optimize as opt
#定義目標函數f
def f(X):
    x=X[0]
    y=X[1]
    return x**2+y**2+1;
x=10
y=10
X=[x,y]
X=opt.fmin(f,X) #f是目標函數,X是初始猜測的解
x=X[0]
y=X[1]
print x,y 

程序執行結果如下:
這裏寫圖片描述
最終xy 的值不是0,而是一個接近0的值。這是由於數值計算畢竟是缺乏精度的。fmin函數是一個迭代算法。在執行過程中,它還同時輸出了結束迭代時的最小值,迭代的次數,以及函數計算的次數。

Conjugate Gradient

downhill simplex比較簡單,實現沒有難度。但它的問題在於比較慢,下面我們試試 Conjugate Gradient法。使用scipy.optimize包的fmin_cg方法。這個方法需要求出目標函數的偏導數(梯度):

fx=2xfy=2y
# encoding=utf-8
import scipy.optimize as opt
import numpy as np
#定義目標函數f
def f(X):
    x=X[0]
    y=X[1]
    return x**2+y**2+1;
#定義目標函數f對應的梯度函數
def gradf(X):
    x=X[0]
    y=X[1]
    gx=2*x
    gy=2*y
    return np.asarray((gx,gy)) #梯度以向量形式返回
x=10
y=10
X=[x,y]
X=opt.fmin_cg(f,X,fprime=gradf) #f是目標函數,X是初始猜測的解,增加梯度
x=X[0]
y=X[1]
print x,y 

運行結果如下:
這裏寫圖片描述
可以看出該方法比downhill simplex運行得更快。當然這是顯然的,因爲有了梯度信息。

你居然不會求偏導數!

OK!OK!有辦法可以解決!今年是2017年,如果你還不會求偏導數,那麼交給計算機吧!

X=opt.fmin_cg(f,X)

OK!計算結果如下:
這裏寫圖片描述
慢了一些,函數的演化次數增加了15次,why?因爲算法用數值法來計算梯度!具體來說(你可以忽略下面的內容,把問題交給人工智能):

def gradf(X):
    x=X[0]
    y=X[1]
    gx=(f([x+0.0000001,y])-f([x,y]))/0.0000001
    gy=(f([x,y+0.0000001])-f([x,y]))/0.0000001
    return np.asarray((gx,gy))

也就是:

f(x)=f(x+Δx)f(x)Δx

Newton-CG

試試Newton-CG,也就是fmin_ncg函數,該函數需要2階偏導數構成的Hessian矩陣:

f2x2f2yxf2xyf2y2=[2002]

代碼如下:
# encoding=utf-8
import scipy.optimize as opt
import numpy as np
#定義目標函數f
def f(X):
    x=X[0]
    y=X[1]
    return x**2+y**2+1;
#定義目標函數f對應的梯度函數
def gradf(X):
    x=X[0]
    y=X[1]
    gx=2*x
    gy=2*y
    return np.asarray((gx,gy)) #梯度以向量形式返回
def hess(X):
    return np.array([[2,0],[0,2]]) #hessian矩陣
x=10
y=10
X=[x,y]
X=opt.fmin_ncg(f,X,fprime=gradf,fhess=hess) #f是目標函數,X是初始猜測的解,增加梯度
x=X[0]
y=X[1]
print x,y 

運行結果如下:
這裏寫圖片描述
非常精確!當然如果不會計算hessian,如下操作:

X=opt.fmin_ncg(f,X,fprime=gradf) #注意梯度是必須的

結果如下:
這裏寫圖片描述

總結

現在是2017年,基本的最優化問題已經經過了幾百年的發展(不是幾十年),因此有大量現成的函數庫可以使用。直接調用這些庫函數,你甚至不需要知道如何求導數、如何求Hessian矩陣,也可以很好的解決最優化問題(可導的)。如果不追求效率,如果精度可以接受,那麼最最原始的downhill simplex方法已經能夠適用於大多數場景(你需要小數點後幾位的精度?)。

數學原理

大多數最優化的書籍,在講解數學原理時,都抱着一定要讓人看不懂,才能顯得高深莫測的心態。其實求解可導目標函數的數學原理是極其簡單的,那就是:窮舉,更高效的窮舉。

假設目標函數爲f(X) ,其中XRn (注意這意味着X 是一個向量,f 是多元函數)。例如X=(x,y,z)T ,則f(X) 表示的函數就是f(x,y,z) 。我如此定義不是爲了把你搞暈,而是爲了環保!試想如果我想定義函數f(x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22) ,難道不是寫成f(X)XR22 更環保嗎?Yes,Yes,你從來沒有見過3元以上的函數,那不是因爲你一直都是在做考題嗎?一道考試題,要是有22個變量,你要用多大的草稿紙?現實問題則不然,考慮女性擇偶問題,男方必須有錢、長得帥、有安全感、父母雙亡、有車、有房、有股票、有存款、初戀、非鳳凰男等等。表示爲函數就是:

f(,,,,,,,,,)

學過代數吧?於是這個函數就變成了:
f(x1,x2,x3,x4,x5,x6,x7,x8,x9,x10)=f(X)

現在我們來討論女性擇偶函數f(X)的最優化!假定女性朋友給出的函數是這樣的:
f(X)=XTC10×10X

這是一個二次函數(請自行展開),C是係數矩陣(我們只是假設女性朋友給出了這樣的二次函數,真實情況未必)。OK,其實這個函數長什麼樣子,不影響後面的討論。我們所要討論的是f(X) 可導的情況。如何確定f(X) 的極值呢?

最笨的方法就是窮舉!!換言之,先試試f(X1) ,再看看f(X2) ,不停的嘗試f(Xi) ,直到成爲單身貴族……對,我告訴你的解法就是成爲單身貴族的解法,因爲解空間幾乎是無限大的,窮舉法太笨!

聰明一點的方式是窮舉n次,找其中的最優。先試試f(X1) ,再看看f(X2) ,不停的嘗試直到f(Xn) ,找出f(X1)f(Xn) 中最優的f(Xi) 。這個方法的缺陷在於,女性朋友不一定能夠找到真正的靈魂伴侶。

再聰明一點的方法固定一些變量,搜索另外一些變量。必須長得帥,像鹿晗;必須有錢,和王思聰一樣;必須是初戀。你看解空間一下就變小了。接下來的搜索就簡單了很多。如果找不到解,那麼就修改某些固定的變量。爲什麼長相必須像鹿晗?郭德綱也可以接受!這樣我們就可以開始新的搜索啦!事實上,這就是絕大多數人,擇偶的方式。

如果變量是連續的呢?數學家發現,在這種情況下,搜索策略其實是這樣的:
1. 首先選擇一個起始的解X0
2. 比較f(X0+Δ) ,如果f(X0+Δ)<f(X0) 那麼X0+Δ 是更好的解
3. 另X1=X0+Δ ,跳到第1步重新執行。

上述方法的核心在於存在一個X 的更新公式:

Xn+1=Xn+Δn

上述算法有三個終止條件:
1. 迭代次數達到限額(都已經120歲的單身貴族啦!)
2. Δn 趨近於0
3. f(Xn)f(Xn+1) 趨近於0

下面舉個例子,目標函數如下:

f(x,y)=x2+y2+1

假設初始值爲X0=(1,1)T ,令Δn=(0.1,0.1)T ,於是:
X0=(1,1)TX1=(0.9,0.9)T......X10=(0,0)TX11=(0.1,0.1)T

Stop!不是X10 已經達到最優了嗎?爲什麼程序還在跑?問題在於那個Δn ,如果它能夠越接近最小值越小,程序不就可以停下來了嗎?越接近你的靈魂伴侶,越需要放慢搜索的速度不是嗎?
怎麼解決這個問題呢?數學家出場了。我們知道f(Xn+1)=f(Xn+Δn) ,根據泰勒公式展開就是:

f(Xn+1)=f(Xn+Δn)=f(Xn)+f(Xn)XΔn+ΔTnf2(Xn)X2Δn/2+......

針對該公式求Δn 的導數,並令結果等於0,則:
f(Xn)X+f2(Xn)X2Δn=0f2(Xn)X2Δn=f(Xn)X

現在我們可以解出Δn !如果你對上述推導的原理感到好奇,不奇怪啊,你不是數學家對吧?總之,我們會算就可以啦。現在用新的公式來求解老問題。
f(x,y)=x2+y2+1f(x,y)x=2xf(x,y)y=2yf2(x,y)x2=2f2(x,y)xy=0f2(x,y)y2=2f(x,y)yx=0[2002]Δn=[2xn2yn]Δn=[xnyn]

計算過程如下:
X0=(1,1)TΔ0=(1,1)TX1=X0+Δ0=(0,0)TΔ1=(0,0)TΔ1=(0,0)TX=X1

數學白癡表示求偏導數矩陣太難理解!!如何破!!數學家馬上送上福利:

λΔn=f(Xn)X

假設λ=2 ,我們看看目標函數f(x,y)=x2+y2+1 的計算過程:

Δn=[xnyn]X0=(1,1)TΔ0=(1,1)TX1=X0+Δ0=(0,0)TΔ1=(0,0)TΔ1=(0,0)TX=X1

運氣不錯!如果λ=4 呢?推導如下:
Δn=[0.5xn0.5yn]X0=(1,1)TΔ0=(0.5,0.5)TX1=X0+Δ0=(0.5,0.5)TΔ1=(0.25,0.25)TX2=X1+Δ1=(0.25,0.25)TΔ2=(0.0625,0.0625)T......

如果λ=1 ,推導如下:
Δn=[2xn2yn]X0=(1,1)TΔ0=(2,2)TX1=X0+Δ0=(1,1)TΔ1=(2,2)TX2=X1+Δ1=(1,1)TΔ2=(2,2)T......

可見如果採用新的計算公式,λ 的取值會影響算法的收斂速度,甚至無法收斂。但的確這種算法避免了計算Hessian,數學白癡非常高興!那麼它存在的問題可以解決嗎?當然可以,方法就是動態調整λ 的取值,從而加快收斂,避免震盪。
最後,再來考慮一下,如果數學白癡連梯度都不會算怎麼辦?Nelder–Mead Method!對有高等數學背景的人而言,理解Nelder–Mead Method反而很困難!這裏就不再詳談了。

數學白癡的福利

我們都不喜歡數學,還好現在是2017年,大量的工作已經由擅長數學的科學家做完了。你所需要的就是找一個算法豐富的庫!比如Python的scipy或者matlab。爲了寫作本文,我對最優化問題進行了很多簡化,首先只考慮可導的目標函數,其次也沒有考慮函數變量的條件約束(比如X 必須是整數、X 必須大於0)。但無論問題多麼複雜,這些問題已經被充分研究,充分解決。99%的情況,你只需要scipy或者matlab,剩下0.9999%的情況需要github。最後,還剩下一丟丟的未知領域,等待着學者去研究。別搶他們的飯碗,茶葉蛋已經夠貴的啦!

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