Gradient Descent、Momentum、Nesterov的實現及直覺對比

 

GradientDescent、Momentum(動量)、Nesterov(牛頓動量)的直覺含義對比:

Gradient Descent

 

def gd(x_start, step, g):#gradient descent
    x = np.array(x_start, dtype='float64')
    # print(x)
    passing_dot = [x.copy()]#training record
    for i in range(50):
        grad = g(x)
        x -= grad * step

        passing_dot.append(x.copy())
        print('[ epoch {0} ]   grad={1},   x={2}'.format(i, grad, x))
        if abs(sum(grad)) < 1e-6:#early stop
            break
    return x, passing_dot

就是有一步走一步,走到哪算哪,比如本例走個之字(zigzag),初期縱向步子大,上下來回繞(如果學習率再大點就不收斂了),後期縱向收斂。但是橫向步子小(因爲橫向縱向梯度不一樣,縱向梯度大,橫向梯度小),最後沒有什麼更新動力,最終在50步內沒有到達中心點。

Momentum

def momentum(x_start, step, g, discount = 0.7):
    x = np.array(x_start, dtype='float64')
    passing_dot = [x.copy()]
    pre_grad = np.zeros_like(x)

    for i in range(50):
        grad = g(x)
        pre_grad = pre_grad * discount + grad
        x -= pre_grad * step
        passing_dot.append(x.copy())

        print('[ Epoch {0} ] grad = {1}, x = {2}'.format(i,grad,x))
        if abs(sum(grad)) < 1e-6:
            break
    return x, passing_dot

Momentum會保留之前步子的趨勢(動量),相比Gradient Descent走過頭直接就往回返,Momentum返回時也會拉你一把,讓你不那麼容易回去。這樣看好像不如不加這個動量,往回的速度慢了。但是累加起來以後會越來越穩,最後直接但是這個把你往外跳的趨勢也給拉沒了。所以,其實動量算法的抗噪聲能力很強。

剛纔的例子不明顯,下邊增加一下學習率:同樣學習率下,Gradient Descent可能不收斂,而Momentum還能收斂,並且需要很少的步子就能辦到。而在橫軸方向,Momentum也因爲動量累積效應,很容易達到了中心點。這是同樣條件下Gradient Descent所沒有辦到的。

 

動量衰減的大小意味着之前的趨勢是否難以撼動。如果動量衰減很小,也就是discount數值很大,也是不容易收斂的,但是隨着步數積累,動量衰減的冪次也增多,還是有收斂的趨勢的。

本例比較簡單,條件不極端,極端情況下,同方向累積步數過多,如果動量衰減程度低,反而要比Gradient Descent波動還大,所以超參數discount的選擇也很重要。

左圖,小學習率同方向積累多步情況下,過大discount導致不易收斂;右圖,同學習率下,普通Gradient Descent縱軸早已收斂(因爲橫縱比例問題,橫向停留,前邊提過)。

可變的discount也可考慮,初期需要的discount小,防止加速逃逸,後期需要的discount大,穩定步伐、加速收斂。

順便在同學習率下,劇透一個Nesterov效果:

 

Nesterov

def nesterov(x_start, step, g, discount = 0.7):
    x = np.array(x_start, dtype='float64')
    passing_dot = [x.copy()]
    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

        passing_dot.append(x.copy())
        print('[ Epoch {0} ] grad = {1}, x = {2}'.format(i,grad,x))
        if abs(sum(grad)) < 1e-6:
            break
    return x, passing_dot

Nesterov是Momentum的變種,或者叫Nesterov動量,是受Nesterov算法啓發改進的Momentum算法。它是先走到你下一步將要到的那個點,然後把那個“未來的點”的梯度計算出來(取代當前點的地位),直接更新動量和x

這個特性就非常有意思了,進行一步之後,如果“第三步”和運動方向不一致,如本例,就會在第二步就提前產生反向的糾正,把x拉回去;如果第三步和運動方向一致,也不會產生放大效應,不會有double位移,因爲跳過了第二步,只算“第三步”或者叫“新二步”吧。那麼再迭代一次,頂多也就是用新分支下的“新四步”來代替“新三步”成爲“新新三步”(因爲還是要產生新分支),然後是“新新新四步”,以此類推。

好處:學習率合適,如果下一步趨勢不變,可以看做等價替換;如果下一步有加速、減速或者掉頭的趨勢,又能提前實現。

左圖Nesterov、右圖Momentum

右圖向量2是按Momentum本該有的行進路線,3是2結束後的下一步行進路線。1結束後就有了向下的動量,2帶着1的向下的趨勢多走了一段,“拉不回來”可以算是Momentum的一個劣勢。但是Nesterov就不同了,它是“預判加截停”,知道你要去哪個方向,直接繞你前邊往回打一巴掌。也就是從向量2的終點去找向量3,近似的看作向量3平移(不是2+3)成了向量4,也就是左圖的向量2。這個算法是對Momentum的進一步優化,算是一個修正。本例,直覺地說,前期走過站的操作更容易拉回來了。

 

下邊是Nesterov的第二種寫法,這種寫法更像《深度學習》算法8.3,而且看着更簡潔。

不過兩種寫法最終效果一樣,區別只是pre_grad(動量v)是先乘過step還是在更新x時再乘step。


def nesterov2(x_start, step, g, discount = 0.7):
    x = np.array(x_start, dtype='float64')
    passing_dot = [x.copy()]
    pre_grad = np.zeros_like(x)
    for i in range(50):
        x_future = x - discount * pre_grad
        grad = g(x_future)
        pre_grad = pre_grad * discount + grad * step
        x -= pre_grad

        passing_dot.append(x.copy())
        print('[ Epoch {0} ] grad = {1}, x = {2}'.format(i,grad,x))
        if abs(sum(grad)) < 1e-6:
            break
    return x, passing_dot
# res, x_arr = nesterov([150,75], 0.012, g)
res, x_arr = nesterov2([150,75], 0.0034, g)
contour(X,Y,Z,x_arr)

 

 

 

 

 

 

完整代碼:https://github.com/huqinwei/tensorflow_demo/blob/master/simple_demo/Momentum_Nesterov_GD.py

 

 

 

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