梯度下降法的全面講解及python實現

函數的梯度方向表示了函數值增長速度最快的方向,那麼和它相反的方向就可以看作函數值減少速度最快的方向。就機器學習模型優化的問題而言,當目標設定爲求解目標函數最小值時,只要朝着梯度下降的方向前進就能不斷逼近最優值。

最簡單的梯度下降算法—固定學習率的方法,這種梯度下降算法由兩個函數和三個變量組成。

    函數1:待優化的函數f(x),它可以根據給定的輸入返回函數值

    函數2:待優化函數的導數g(x),它可以根據給定的輸入返回函數的導數值

    變量x:保存當前優化過程的參數值,優化開始時該變量將被初始化爲某個數值,優化過程中這個變量會被不斷變化,直到它找到最小值。

    變量grad:保存變量x點處的梯度值

    變量step:表示沿着梯度下降方向行進的步長,也被稱爲學習率。

(備註:代碼在本文最後有完整代碼,此處只給出其中函數部分。所以實現的圖和結果想看的直接先到文末把代碼copy下來,然後直接執行即可)

python函數舉例:

  1. def gd(x_start, step, g):
  2.     x = np.array(x_start,'float64')
  3.     for i in range(30):
  4.         grad = g(x)
  5.         x -= grad*step
  6.         print("[Epoch{0}] grad={1}, x={2}".format(i, grad, x))
  7.         if isinstance(grad, float):
  8.             if abs(grad) < 1e-6:
  9.                 break
  10.         else:
  11.             if abs(sum(grad)) < 1e-6:
  12.                 break
  13.     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函數舉例:

  1. def momentum(x_start, step, g, discount=0.7):
  2.     x = np.array(x_start, dtype='float64')
  3.     pre_grad = np.zeros_like(x)  # 存儲積累的動量
  4.     for i in range(60):
  5.         grad = g(x)
  6.         pre_grad = pre_grad*discount + grad*step
  7.         x -= pre_grad
  8.         print("[Epoch {0}] grad={1},x={2}".format(i, grad, x))
  9.         if abs(sum(grad)) < 1e-6:
  10.             break
  11.     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函數代碼:

  1. def nesterov(x_start, step, g, discount=0.7):
  2.     x = np.array(x_start, dtype='float64')
  3.     pre_grad = np.zeros_like(x)
  4.     for i in range(50):
  5.         x_future = x - step*discount*pre_grad
  6.         grad = g(x_future)  # 計算更新後的優化點的梯度
  7.         pre_grad = pre_grad*discount+grad
  8.         x -= pre_grad*step
  9.         print('[Epoch {0} grad={1}, x={2}'.format(i, grad, x))
  10.         if abs(sum(grad)) < 1e-6:
  11.             break
  12.     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實現》馮超著,電子工業出版社

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