理解梯度下降(一)(代碼篇)|機器學習你會遇到的“坑”
先祝大家中秋快樂~
實際的模型在訓練過程中,尤其是在深度學習中,參數會達到幾千萬,參數空間會變的非常龐大。爲了更好幫助大家理解優化算法,我們先用一維的Loss function,在代碼的演示過程中,我將不會展示座標下降,不僅因爲一維的座標下降和基於梯度的更新都是在同一個方向上進行搜索,更重要的是,此代碼篇希望大家通過這一節可以掌握GIF圖的繪製和可視化優化算法,它會在我們的後續的課程中被廣泛使用。
我們假設Loss function對於參數來說是一個非常簡單的二次函數:
那麼它的梯度就是:
def f(x):
return(x**2)
def df(x):
return(2*x)
同時根據上一節理論篇所說的梯度下降的公式:
我們可以定義一個用於梯度下降的函數,它接受參數的初始值和學習率,返回每次更新以後的Loss值和參數值,注意到我們此時只迭代了100次:
def GD(lr,start):
x = start
GD_x, GD_y = [], []
for it in range(100):
GD_x.append(x)
GD_y.append(f(x))
dx = df(x)
x = x - lr * dx
return(GD_x,GD_y)
在實際使用過程中,我們不需要保存每一迭代的結果,當參數非常多時,對計算機的內存消耗也會非常大,但是我們需要利用每一步的結果直觀地給大家展示下降的過程。關於代碼很多人都能快速的理解甚至應用,但是展示下降的過程,需要我們畫出動態圖,我們需要用到matplotlib中的animation,此外如果我們需要保存GIF,我們還需要安裝ffmpeg。
在實現GIF的代碼實操中,我們需要明確兩步:
不會被更新的圖形部分不斷更新的圖形部分和圖形的初始化
對於第一步,我們可以很方便的寫出:
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style='darkgrid')
FFwriter =animation.FFMpegWriter()
fig, ax = plt.subplots()
fig.set_tight_layout(True)
points_x = np.linspace(-20, 20, 1000)
points_y = f(points_x)
ax.plot(points_x,points_y, c="k", alpha=0.9, linestyle="-")
對於第二步,我們首先要獲得梯度下降的結果:
GD_x,GD_y=GD(lr=pow(2,-10)*16,start=-20)
然後初始化,定義一個更新公式,將我們上述獲得結果依次更新:
point_line,=ax.plot(GD_x[0],GD_y[0],'or')
def update(i):
label = 'timestep {0}'.format(i)
print(label)
point_line.set_xdata(GD_x[i])
point_line.set_ydata(GD_y[i])
ax.set_xlabel(label)
return point_line, ax
將fig,update作爲參數傳入到我們的animation的FuncAnimation類中,並設置幀數爲60幀,每幀間隔200ms:
anim = FuncAnimation(fig, update, frames=np.arange(0, 60), interval=200)
我們的代碼總結如下:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
from matplotlib.animation import FuncAnimation
import seaborn as sns
sns.set(style='darkgrid')
FFwriter =animation.FFMpegWriter()
fig, ax = plt.subplots()
fig.set_tight_layout(True)
def f(x):
return(x**2)
def df(x):
return(2*x)
points_x = np.linspace(-20, 20, 1000)
points_y = f(points_x)
ax.plot(points_x,points_y, c="k", alpha=0.9, linestyle="-")
def GD(lr,start):
x = start
GD_x, GD_y = [], []
for it in range(100):
GD_x.append(x)
GD_y.append(f(x))
dx = df(x)
x = x - lr * dx
return(GD_x,GD_y)
GD_x,GD_y=GD(lr=pow(2,-10)*16,start=-20)
print('fig size: {0} DPI, size in inches {1}'.format(
fig.get_dpi(), fig.get_size_inches()))
point_line,=ax.plot(GD_x[0],GD_y[0],'or')
def update(i):
label ='timestep {0}'.format(i)
print(label)
point_line.set_xdata(GD_x[i])
point_line.set_ydata(GD_y[i])
ax.set_xlabel(label)
return point_line, ax
if__name__=='__main__':
anim = FuncAnimation(fig, update, frames=np.arange(0, 100), interval=200)
anim.save('GD.gif', writer=FFwriter)
如圖,我們實現了Loss function上實現了梯度下降法,同時,可以看出,梯度在越變越小,在固定好學習率的情形下,下降的也越來越慢,迭代60次之後仍然沒有到達最低點。
我們可以嘗試將學習率設置的大一點,看看是否會更快的迭代到最小值:
......
GD_x,GD_y=GD(lr=pow(2,-7)*16,start=-20)
......
如圖,我們更快的到達了極小值,迭代十次就達到了,後面的迭代是不必要的。
如果繼續增大學習率會發生什麼情況呢?我們繼續增加學習率:
......
GD_x,GD_y=GD(lr=pow(2,-4)*16,start=-20)
......
如圖,當學習率繼續增大的時候,參數點會在極小值附近來回震盪,可想而知,如果我們繼續增大學習率,更新點會超出圖形的範圍。
接下來嘗試牛頓法的時候,需要注意到此時的Loss function的二階導數是一個常數,如果我們直接利用牛頓法的更新公式:
就會發現此時的牛頓法不過是另一個學習率下的梯度下降,學習率固定爲二階導數的倒數,我們要想在牛頓法和梯度下降法中看到差別,就需要更換Loss function,爲達到此目的,我們原來Loss function中添加一個小的三角函數項:
三角函數好處在於它的無窮階不僅可導,而且不會成爲常數,同時也不會對原來函數的形狀有太大的影響,我們可以很方便的寫出:
def f(x):
return(-np.cos(np.pi*x/20)+x**2)
def df(x):
return(np.sin(np.pi*x/20)*np.pi/20+2*x)
def ddf(x):
return((np.pi/20)**2*np.cos(np.pi*x/20)+2)
這樣就可以保證二階導數不是一個常數,接下來我們構建牛頓法的更新函數:
def Newton(start):
x = start
Newton_x, Newton_y = [], []
for it in range(100):
Newton_x.append(x), Newton_y.append(f(x))
g = df(x)
h = ddf(x)
x = x -g/h
return(Newton_x,Newton_y)
然後我們利用該函數獲得更新點,然後更新到GIF上:
......
newton_x,newton_y=Newton(start=-20)
point_line,=ax.plot(newton_x[0],newton_y[0],'or')
def update(i):
label ='timestep {0}'.format(i)
print(label)
point_line.set_xdata(newton_x[i])
point_line.set_ydata(newton_y[i])
ax.set_xlabel(label)
return point_line, ax
......
如圖,參數只用了一次迭代就到了最低點,後面的迭代就不起作用,因爲梯度爲零。
牛頓法看起來很快,但我們在不清楚loss function性質的情況下卻很少使用它,尤其是在深度學習中,這不僅是因爲需要每一步需要計算Hessian,還因爲我們如果真的希望牛頓法執行下降步驟,必須保證Hessian的正定,這在某些情況下是不成立的,比如,我們直接將Loss function 定義爲一個餘弦函數:
我們將參數初始值設置爲-15,因爲當-20的時候,剛好在極大值點,梯度爲零,無法進行迭代,同時將幀數設置爲10,節約我們看圖的時間:
def f(x):
return(-np.cos(np.pi*x/20))
def df(x):
return(np.sin(np.pi*x/20)*np.pi/20)
def ddf(x):
return((np.pi/20)**2*np.cos(np.pi*x/20))
如圖,牛頓法反而執行的是上升的步驟,因爲Hessian此時是負定的,對於一維情況來說,二階導數就是負值,從梯度的更新公式來看牛頓法,在Hessian負定的時候,學習率就是負的。
但梯度下降卻不會出現這個問題,因爲我們在執行梯度下降時,學習率永遠都是正的。
如圖,梯度下降應用在餘弦函數構成的Loss function上,下降的結果也能得到保證。
如何解決牛頓法不降反升的問題呢?我們可以模仿Ridge regression添加L2正則化的辦法,使得樣本矩陣強行滿秩,同樣的,我們也可以在Hessian上加上一個正則化項,使得Hessian強行正定:
代碼就可以很方便的寫成:
def Newton(start,alpha):
x = start
Newton_x, Newton_y = [], []
for it in range(100):
Newton_x.append(x), Newton_y.append(f(x))
g = df(x)
h = ddf(x)
x = x -g/(h+alpha)
return(Newton_x,Newton_y)
我們設置正則化參數alpha,並將其應用於此時的Loss function:
......
Newton_x,Newton_y=Newton(start=-15,alpha=pow(2,-8)*20)
......
圖爲添加了正則化的牛頓法,正則化會使得原本可能執行上升步驟牛頓法變爲下降。
讀芯君開扒
課堂TIPS
• 在使用matplotlib保存圖片的時候,即使安裝了ffmpeg(添加到環境變量),仍然有可能出現保存GIF出錯的問題,推薦使用保存先保存爲MP4文件,然後在當前文件目錄下運行如下命令,即可獲得GIF圖:
ffmpeg.exe -i .\filename.mp4filename.gif
• 本文所採用的Loss function較爲簡單,實際過程中我們可能還會面臨全局最小和局部最小的問題,但在深度學習中,凸優化並不是一個大問題,因爲我們只要求找到使得泛化誤差達到我們接受的程度,同時全局優化算法目前在理論上並沒有可靠的保證。
• 除了學習率和Loss function的適應性問題,參數的初始值和鞍點也是優化過程中非常頭疼的問題,牛頓法最大的問題就是它找到的一般都是鞍點,我們將在下一章集中講解以隨機梯度下降爲代表的目前流行的一系列技術,同時會涉及到優化算法的搜索方向問題(本節參數只有一維,下一節的代碼篇會拓展到多維)。
作者:唐僧不用海飛絲
如需轉載,請後臺留言,遵守轉載規範
查看原文 >>