函數的梯度方向表示了函數值增長速度最快的方向,那麼和它相反的方向就可以看作函數值減少速度最快的方向。就機器學習模型優化的問題而言,當目標設定爲求解目標函數最小值時,只要朝着梯度下降的方向前進就能不斷逼近最優值。
最簡單的梯度下降算法—固定學習率的方法,這種梯度下降算法由兩個函數和三個變量組成。
函數1:待優化的函數f(x),它可以根據給定的輸入返回函數值
函數2:待優化函數的導數g(x),它可以根據給定的輸入返回函數的導數值
變量x:保存當前優化過程的參數值,優化開始時該變量將被初始化爲某個數值,優化過程中這個變量會被不斷變化,直到它找到最小值。
變量grad:保存變量x點處的梯度值
變量step:表示沿着梯度下降方向行進的步長,也被稱爲學習率。
(備註:代碼在本文最後有完整代碼,此處只給出其中函數部分。所以實現的圖和結果想看的直接先到文末把代碼copy下來,然後直接執行即可)
python函數舉例:
- def gd(x_start, step, g):
-
- x = np.array(x_start,'float64')
-
- for i in range(30):
-
- grad = g(x)
-
- x -= grad*step
-
- print("[Epoch{0}] grad={1}, x={2}".format(i, grad, x))
-
- if isinstance(grad, float):
-
- if abs(grad) < 1e-6:
-
- break
-
- else:
-
- if abs(sum(grad)) < 1e-6:
-
- break
-
- return x
假設函數f(x)=x^2-2x+1,則很容易看出最小值是x=1,f(x)=0。其圖像就是一個簡單的拋物線。
執行gd(5,0.55,g):
如果步長設置過大我們可以看到,參數的梯度不但沒有收斂,反而越來越大:
執行gd(5,2.55,g):
優化的本意是讓目標值朝着梯度下降的方向前進,結果它卻走向了另外一個方向。實際上,函數在某一點的梯度指的是它在當前變量處的梯度,對於這一點來說,它的梯度方向指向了函數上升的方向,可以利用泰勒公式證明在一定範圍內,沿着負梯度方向前進,函數值是會下降的。但是,公式只能保證在一定範圍內是成立的,從函數的實際圖像中也可以看出,如果優化的步長太大,就有可能跳出函數值下降的範圍,那麼函數值是否下降就不好說了 。 當然有可能越變越大,造成優化的悲劇。
既然小步長會使目標值的梯度下降,大步長會使梯度發散,那麼有沒有一個步長會讓優化問題原地打轉呢?在這個問題中,這樣的步長是存在且容易找到的。如果step= 1時,求解會原地打轉,梯度下降法就失效了。
執行gd(5,1,g):
通過上面的實驗可以發現,對於初始值爲5這個點,當步長大於1時,梯度下降法會出現求解目標值發散的現象;而步長小於1時,則不會發散,參數會逐漸收斂。所以1就是步長的臨界點。那麼問題又來了,對於別的初始值,這個規律還適用嗎?接下來就把初始值換成4,再進行一次實驗:
實驗發現,步長等於1時,初始值設置成(最優值除外)任何數字,參數都不會收斂到最優值。這個實驗揭示了一個道理:對於這個二次函數,如果採用固定步長的梯度下降法進行優化,步長要小於1,否則不論初始值等於多少,問題都會發散或者原地打轉!
若換個函數:fx=4x2-4x+1,它的安全步長就不再是1了,而是0.25,這裏就不再重複了,感興趣的可以自己改下函數表達式f和導數表達式g,然後執行gd(5,0.25,g)即可.
動量(Momentum)算法:動量代表了前面優化過程積累的“能量”,它將在後面的優化過程中持續發戚,推動目標值前進。擁有了動量,一個已經結束的更新量不會立刻消失,只會以一定的形式衰減,剩下的能量將繼續在優化中發揮作用。
python函數舉例:
- def momentum(x_start, step, g, discount=0.7):
-
- x = np.array(x_start, dtype='float64')
-
- pre_grad = np.zeros_like(x) # 存儲積累的動量
-
- for i in range(60):
-
- grad = g(x)
-
- pre_grad = pre_grad*discount + grad*step
-
- x -= pre_grad
-
- print("[Epoch {0}] grad={1},x={2}".format(i, grad, x))
-
- if abs(sum(grad)) < 1e-6:
-
- break
-
- return x
代碼中多出了一個新變量pre_grad。這個變量就是用於存儲歷史積累的動量,每一輪迭代動量都會乘以一個打折量(discount)做能量衰減但是它依然會被用於更新參數。動量算法和前面介紹的梯度下降法相比有什麼優點呢?用形象的話來說,它可以幫助目標值穿越“狹窄山谷”形狀的優化曲面,從而到達最終的最優點。
假設函數,函數在等高線圖上的樣子如圖(三維圖和平面圖):
則容易看出其中中心的點表示最優值。把等高線上的圖像想象成地形圖,從等高線的疏密程度可以看出,這個函數在y軸方向十分陡峭,在x軸方向則相對平緩。也就是說,函數在y軸的方向導數比較大,在x軸的方向導數比較小
將50輪迭代過程中參數的優化過程圖畫出來,如圖:
gd([150,75],0.016,g)
可以看出目標值從某個點出發,整體趨勢向着最優點前進,它和最優值的距離不斷靠近,說明優化過程在收斂,步長設置是沒有問題的,但是前進的速度似乎有點乏力,50輪迭代井沒有到達最優值,有可能是步長設置得偏小。有了前面的經驗,這次設置步長時很謹慎。我們只將步長稍變大一點,結果如圖:
雖然優化效果有了一定的提高,但成效依然不明顯,而且優化的過程圖中出現了參數值左右抖動的現象,這是怎麼回事呢?看上去參數在優化過程中產生了某種“打轉兒”的現象,做了很多無用功。如果繼續增加步長,優化曲線會變成:
從結果來看,這個步長已經是能設置的最大步長,如果步長再設大些,就要從U形賽道飛出去,優化參數的梯度也將發散出去。在這個問題中,由於兩個座標軸方向的函數的陡峭性質不同,兩個方向對最大步伏的限制不同,顯然y方向對步長的限制更嚴格。但滿足y方向的更新,x方向就無法獲得充分的更新,這樣梯度下降法將無法獲得很好的效果。
我們會發現每一次的行動只會在以下三個方向進行:沿-x方向滑行;沿+y方向滑行。沿-y方向滑行。
這樣看來,要是能把行動的力量集中在往-x方向走而不是沿y方向打轉就好了。這個想法可以被動量算法實現。
我們可以想象,使用了動量後,歷史的更新量會以衰減的形式不斷作用在這些方向上,那麼沿-y和+y兩個方向的動量就可以相互抵消,而-x方向的力則會一直加強,這樣雖然還會沿y方向打轉,但是他在-x方向的速度會因爲之前的累積變得越來越快。
momentum ( [150, 75],0.016, g)
優化曲線果然沒有令人失望,儘管還是有打轉現象,但是在50輪迭代後,他進入了最優點的鄰域,完成了優化任務,和前面的梯度下降法相比有了很大的進步。
當然,還是暴露了動量優化存在的一點問題,前面幾輪迭代過程中目標值在ν軸上的震盪比過去還要大。在發明動量算法後,又有科研人員發明了基於動量算法的改進算法,解決了動量算法沒有解決的問題一一更強烈的抖動,乾脆停止玩耍,專心趕路。這就是牛頓動量(Nesterov)法
python函數代碼:
- def nesterov(x_start, step, g, discount=0.7):
-
- x = np.array(x_start, dtype='float64')
-
- pre_grad = np.zeros_like(x)
-
- for i in range(50):
-
- x_future = x - step*discount*pre_grad
-
- grad = g(x_future) # 計算更新後的優化點的梯度
-
- pre_grad = pre_grad*discount+grad
-
- x -= pre_grad*step
-
- print('[Epoch {0} grad={1}, x={2}'.format(i, grad, x))
-
- if abs(sum(grad)) < 1e-6:
-
- break
-
- return x
-
算法不再貪玩,放棄在U形賽道上摩擦。那麼,Nesterov算法和動量算法相比有什麼區別呢?動量算法計算的梯度是在當前的目標點的;而 Nesterov算法計算的梯度是在動量更新後的優化點的。這其中關鍵的區別在於計算梯度的點。
可以想象一個場景,當優化點已經積累了某個抖動方向的梯度後,對動量算法來說,雖然當前點的梯度指向積累梯度的相反方向,但是量不夠大,所以最終的優化方向還會在積累的方向上前進一段,這就是上圖所示的效果。對Nesterov方法來說,如果按照積累方法再向前多走一段,則梯度中指向積累梯度相反方向的量會變大許多,最終兩個方向的梯度抵消,反而使抖動方向的量迅速減少。Nesterov的衰減速度確實比動量方法的快不少。
最後來講述動量在數值上的事情。科研人員已經給出了動量打折率的建議配置一一0.9 (剛纔的例子全部是0.7)那麼0.9的動量打折率能使歷史更新量發揮多大作用呢?
如果用G表示每一輪迭代的動量,g表示當前一輪迭代的更新量(方向×步長),t表示迭代輪數,γ表示動量的打折率,那麼對於時刻t的梯度更新量就有公式:
這樣可以計算出對於第一輪迭代的更新g0來說,從G0到GT,它的總貢獻量爲。它的貢獻和是一個等比數列的和,比值爲γ。如果γ=0.9,那麼根據公比小於1的等比數列的極限公式,可以知道更新量在極限狀態下的貢獻值:
那麼當γ=0.9時,它一共貢獻了相當於自身10倍的能量。如果γ=0.99,那就是100倍能量。在實際應用中,打折率的設置需要分析具體任務中更新量需要多“持久”的動力。
python完整代碼:
import numpy as np
import sklearn.datasets as d
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def plot1():
reg_data = d.make_regression(100, 1, 1, 1, 1.0)
plt.plot(reg_data[0], reg_data[1])
plt.show()
cls_data = d.make_classification(100, 2, 2, 0, 0, 2)
plt.plot(cls_data[0], 'o')
plt.show()
fig = plt.figure()
ax = Axes3D(fig)
X = np.linspace(0.01, 0.99, 101)
Y = np.linspace(0.01, 0.99, 101)
X, Y = np.meshgrid(X, Y)
Z = -X * np.log2(Y) - (1 - X) * np.log2(1 - Y)
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='rainbow')
plt.show()
xi = np.linspace(-200,200,800)
yi = np.linspace(-100,100,800)
X, Y = np.meshgrid(xi, yi)
Z = X*X+50*Y*Y
plt.contour(X, Y, Z, 10)
plt.plot(0,0,"o")
plt.show()
fig = plt.figure()
ax = Axes3D(fig)
ax.plot_surface(Y, X, Z, rstride=1, cstride=1, cmap='rainbow')
plt.show()
def gd(x_start, step, g):
"""
梯度下降
:param x_start:
:param step:
:param g:
:return:
"""
result = [[], []]
x = np.array(x_start,'float64')
for i in range(30):
grad = g(x)
x -= grad*step
print("[Epoch{0}] grad={1}, x={2}".format(i, grad, x))
if isinstance(grad, float):
if abs(grad) < 1e-6:
break
else:
if abs(sum(grad)) < 1e-6:
break
result[0].append(x[0])
result[1].append(x[1])
return result
def momentum(x_start, step, g, discount=0.7):
"""
動量算法
:param x_start:
:param step:
:param g:
:param discount:
:return:
"""
result = [[], []]
x = np.array(x_start, dtype='float64')
pre_grad = np.zeros_like(x) # 存儲積累的動量
for i in range(60):
grad = g(x)
pre_grad = pre_grad*discount + grad*step
x -= pre_grad
print("[Epoch {0}] grad={1},pre_grad={2},x={3}".format(i, grad, pre_grad, x))
if abs(sum(grad)) < 1e-6:
break
result[0].append(x[0])
result[1].append(x[1])
return result
def nesterov(x_start, step, g, discount=0.7):
"""
Nesterov算法
:param x_start:
:param step:
:param g:
:param discount:
:return:
"""
result = [[], []]
x = np.array(x_start, dtype='float64')
pre_grad = np.zeros_like(x)
for i in range(50):
x_future = x - step*discount*pre_grad
grad = g(x_future) # 計算更新後的優化點的梯度
pre_grad = pre_grad*discount+grad
x -= pre_grad*step
print('[Epoch {0} grad={1}, x={2}'.format(i, grad, x))
if abs(sum(grad)) < 1e-6:
break
result[0].append(x[0])
result[1].append(x[1])
return result
def test1():
def f(x):
return x ** 2 - 2 * x + 1
def g(x):
return 2 * x - 2
x = np.linspace(-5,7,100)
y = f(x)
plt.plot(x,y)
plt.show()
gd(5, 0.55, g)
gd(5,2.55,g)
gd(5,1,g)
def test2():
xi = np.linspace(-200, 200, 800)
yi = np.linspace(-100, 100, 800)
X, Y = np.meshgrid(xi, yi)
Z = X * X + 50 * Y * Y
def f(x):
return x[0]**2+50*x[1]**2
def g(x):
return np.array([2*x[0], 100*x[1]])
result = gd([150,75],0.016,g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
result = gd([150,75],0.019,g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
result = gd([150, 75], 0.02, g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
result = momentum([150,75],0.016,g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
result = nesterov([150,75],0.012,g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
if __name__ == '__main__':
plot1()
test1()
test2()
參考書籍:《強化學習精要 核心算法與TensorFlow實現》馮超著,電子工業出版社