
鏈接:https://zhuanlan.zhihu.com/p/21475880
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
為了保證完整性,今天再扯一下另外一個在梯度下降中十分重要的東西,那就是沖量——momentum。
這是一個十分神秘的變量,我也只能以最簡單的方式理解它,於是在這里班門弄斧了。正如它的中文名字一樣,在優化求解的過程中,沖量扮演了對之前優化量的持續發威的推動劑。一個已經完成的梯度+步長的組合不會立刻消失,只是會以一定的形式衰減,剩下的能量將繼續發揮余熱。我們先不加解釋的給出基於沖量的梯度下降的代碼:
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(50):
grad = g(x)
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
那么沖量究竟有什么作用呢?今天主要扯它其中的一個作用,那就是幫助你穿越“山谷”。怎么來理解穿越“山谷”呢?先來一個待優化函數。這次的問題相對復雜些,是一個二元二次函數:
def f(x):
return x[0] * x[0] + 50 * x[1] * x[1]
def g(x):
return np.array([2 * x[0], 100 * x[1]])
xi = np.linspace(-200,200,1000)
yi = np.linspace(-100,100,1000)
X,Y = np.meshgrid(xi, yi)
Z = X * X + 50 * Y * Y
上面這個函數在等高線圖上是這樣的:
其中中心的藍色點表示了最優值。我們根據這個圖發揮下想象,這個函數在y軸十分陡峭,在x軸相對平緩些。好了話說完我們趕緊拿朴素梯度下降來嘗試下:
gd([150,75], 0.016, g)
經過50輪的迭代,他的優化過程圖如下所示:
可以看出我們從某個點出發,整體趨勢向着最優點前進,這個是沒有問題的,但是前進的速度似乎有點乏力,是不是步長又設小了?有了之前的經歷,這一回我們在設置步長時變得小心了許多:
res, x_arr = gd([150,75], 0.019, g)
contour(X,Y,Z, x_arr)
好像成效不是很明顯啊,而且優化的過程中左右來回抖是怎么回事?看着這個曲線讓我想起了一個極限運動:
(來自網絡,如有侵權立即刪除)
沒錯,其實算法眼中的這個函數很這張圖很像,而算法也果然沒有讓大家“失望”,選擇了一條艱難的道路進行優化——就像從一邊的高台滑下,然后滑到另一邊,這樣艱難地前進。沒辦法,這就是梯度下降法。在它的眼中,這樣走是最快的,而事實上,每個優化點所對應的梯度方向也確實是那個方向。
大神們這時可能會聊起特征值的問題,關於這些問題以后再說。好吧,現在我們只能繼續挑步長,說不定步長再大點,“滑板少年”還能再快點呢!
res, x_arr = gd([150,75], 0.02, g)
contour(X,Y,Z, x_arr)
好吧……我們的滑板少年已經徹底玩脫了……這已經是我們能設的最大的步長了(上一次關於步長和函數之間的關系在這里依然受用),再設大些我們的滑板少年就飛出去了。對於這個問題,由於兩個坐標軸方向的函數屬性不同,為了防止在優化的過程中發散,步長只能夠根據最陡峭的方向設定。當然,解決快速收斂這個問題還有其他的辦法,這里我們看看沖量如何搞定這位滑板少年。
很自然地,我們在想,要是少年能把行動的力量集中在往前走而不是兩邊晃就好了。這個想法分兩個步驟:首先是集中力量向前走,然后是盡量不要在兩邊晃。這時候,我們的沖量就閃亮登場了。我們發現滑板少年每一次的行動只會在以下三個方向進行:
- 沿-x方向滑行
- 沿+y方向滑行
- 沿-y方向滑行
我們可以想象到,當使用了沖量后,實際上沿-y和+y方向的兩個力可以相互抵消,而-x方向的力則會一直加強,這樣滑板少年會在y方向打轉,但是y方向的力量會越來越小,但是他在-x方向的速度會比之前快不少!
好了,那我們看看加了沖量技能的滑板少年的實際表現:
momentum([150,75], 0.016, g)
總算沒有讓大家失望,盡管滑板少年還是很貪玩,但是在50輪迭代后,他還是來到了最優點附近。可以說是基本完成了我們的任務吧。當然由於沖量的問題,前面幾輪迭代他在y軸上玩得似乎比以前還歡樂,這個問題我們后面會提。但不管怎么說,總算完成目標了。
后來,又有高人發明了解決前面沖量沒有解決的問題的算法,干脆不讓滑板少年愉快地玩耍了,也就是傳說中的Nesterov算法。這里就不細說了,有時間詳細聊下。直接給出代碼和結果:
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 * 0.7 + 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
nesterov([150,75], 0.012, g)
好了,滑板少年已經哭暈在廁所……
費了這么多話,我們總算把穿越“山谷”這件事情說完了,下面還要說一個數值上的事情。在CNN的訓練中,我們的開山祖師已經給了我們沖量的建議配置——0.9(剛才的例子全部是0.7),那么0.9的沖量有多大量呢?終於要來點公式了……
我們用G表示每一輪的更新量,g表示當前一步的梯度量(方向*步長),t表示迭代輪數,表示沖量的衰減程度,那么對於時刻t的梯度更新量有:




那么我們可以計算下對於梯度g0對從G0到GT的總貢獻量為

我們發現它的貢獻是一個等比數列,如果=0.9,那么跟據等比數列的極限運算方法,我們知道在極限狀態下,它一共貢獻了自身10倍的能量。如果
=0.99呢?那就是100倍了。
那么在實際中我們需要多少倍的能量呢?
本文相關代碼詳見:https://github.com/hsmyy/zhihuzhuanlan/blob/master/momentum.ipynb

那么在實際中我們需要多少倍的能量呢?
本文相關代碼詳見:https://github.com/hsmyy/zhihuzhuanlan/blob/master/momentum.ipynb