學習率的問題
學習率的大小對梯度下降的搜索過程影響非常大,太小搜索速度慢,太大又可能跳過極值。上述手動選擇學習率的方式,稱為SGD 隨機梯度下降法,每次的學習率都是固定的。手動選擇適當的學習率往往要花費不少時間,所以可以用如下三種方式自動幫我們選擇學習率
1. 衰減因子
2. 引入動量
3. 自動調整學習率(自適應梯度策略,又稱為優化器 Optimizer)
學習率衰減因子
把初始學習率預設為 1,然後依迭代的次數,逐步減少。迭代公式如下
$(lr_{i} = \frac{lr_{0}}{(1.0 + decay * i)})$
$(lr_{0})$ 為初始學習率,decay 為衰減因子,i 為第 i 次迭代,$(lr_{i})$ 為第 i 次迭代所計算出來的學習率。
衰減因子愈大,衰減率會愈大,就會更快接近極值。但此時的初始學習率亦必需設定大一點,如果初始學習率設太小,衰減愈不明顯。底下是 Python 完整代碼
import threading
import time
import numpy as np
import matplotlib.pyplot as plt
#目標函數 y=x^2
def f(x):
return np.square(x)
#目標函數的一階導數 dy/dx=2*x
def df(x):
return 2 * x
def bias(a,x):
b=f(x) - a * x
return b
def runnable():
xs = np.linspace(-10, 10, 100)
x=-10
ys=f(xs)
point_x=[x]
v=0
mu = 0.9
for i in range(epochs):
ax.clear()
ax.set_xlim(-10,10)
ax.set_ylim(-50, 200)
plt.plot(xs, ys)
plt.scatter(point_x, f(point_x), c='r')
#對目標函數進行微分
a=df(x)
b=bias(a, x)
#畫導線
x_l=x-3
x_r=x+3
line_x=[x_l, x_r]
line_y=[a*(x_l)+b,a*(x_r)+b]
plt.scatter(x, f(x), s=300, c='g')
plt.plot(line_x, line_y)
ax.text(-5,-15, f'{a} * x + {b}', color='red')
plt.draw()
#計算下一步 (x,y)
dx = df(x)
lr_i = lr / (1 + decay * i)
v= -dx *lr_i
x += v
point_x.append(x)
time.sleep(0.1)
plt.figure(figsize=(10,6))
epochs = 50
lr = 0.2
decay = 0.99
ax=plt.subplot()
t=threading.Thread(target=runnable)
t.start()
plt.show()
底下是初始學習率 0.4 ,decay 為 0.99 所執行的圖解。
底下是初始學習率 0.6 ,decay 為 0.99 所執行的圖解, 一下就找到極值。如果初始學習率設定為 0.5 ,則會更快,馬上就不動了
引入動量
物理學的動量定義為 質量 * 速度,也就是 m * v。當 m 為一單位質量時,v 就是動量。一開始設定 v = 0 (一開始不動),然後每次迭代 v = -dx * lr + mu * v ,mu 是要縮小動量的小參數。底下是 Python 作碼。
import threading import time import numpy as np import matplotlib.pyplot as plt #目標函數 y=x^2 def f(x): return np.square(x) #目標函數的一階導數 dy/dx=2*x def df(x): return 2 * x def bias(a,x): b=f(x) - a * x return b def runnable(): xs = np.linspace(-10, 10, 100) x=-10 ys=f(xs) point_x=[x] v=0 mu = 0.9 for i in range(epochs): ax.clear() ax.set_xlim(-10,10) ax.set_ylim(-50, 200) plt.plot(xs, ys) plt.scatter(point_x, f(point_x), c='r') #對目標函數進行微分 a=df(x) b=bias(a, x) #畫導線 x_l=x-3 x_r=x+3 line_x=[x_l, x_r] line_y=[a*(x_l)+b,a*(x_r)+b] plt.scatter(x, f(x), s=300, c='g')
plt.plot(line_x, line_y)
ax.text(-5,-15, f'{a} * x + {b}', color='red') plt.draw() #計算下一步 (x,y) dx = df(x) v = -dx * lr + mu * v x += v point_x.append(x) time.sleep(0.1) plt.figure(figsize=(10,6)) epochs = 200 lr = 0.2 ax=plt.subplot() t=threading.Thread(target=runnable) t.start() plt.show()
下圖是執行後的結果,可以看到就好像一顆圓球,滑入一個凹槽中,然後來回左右滾動。所以如果想要製作這種模擬現實生活中滾來滾去的動畫,加入動量是最好的方法。
動量的作用,其實就是在平滑之處,有鼓力量將現在的位置往前推(慣性),讓它越過目前的門檻。當然啦,如果動量不足以跨不出目前的門檻,還是會被打回而卡住。
所以引入動量的方法,只是去探測未來的高度是否還是跟往惜一樣的平坦,用此動能衝看看,如果可以跨過就往前,如果跨不過去就反彈。
鞍點無動量
加入動量的重要性如下。當一個函數有鞍點且使用傳統學習方式時,就會卡在局部最小值中,無法突破
import threading import time import numpy as np import matplotlib.pyplot as plt #目標函數 y=x^4 - 60 x^3 -x + 1 def f(x): x=np.array(x) y = (np.power(x,4) - 60 * np.power(x,3) - x + 1)/shrink_y return y #目標函數的一階導數 dy/dx=2*x def df(x): return (4 * x**3 - 180 * x**2 - 1) / shrink_y def bias(a,x): b=f(x) - a * x return b def runnable(): xs = np.linspace(-30,60, 100) x=xs[0] ys=f(xs) point_x=[x] v=0 mu = 0.9 for i in range(epochs): ax.clear() ax.set_xlim(-45,70) ax.set_ylim(-2, 3) #ax.set_ylim(-1.5e6, 3e6) plt.plot(xs, ys) plt.scatter(point_x, f(point_x), c='r') #對目標函數進行微分 a=df(x) b=bias(a, x) #畫導線 x_l=x-5 x_r=x+5 line_x=[x_l, x_r] line_y=[a*(x_l)+b,a*(x_r)+b] plt.scatter(x, f(x), s=300, c='g') plt.plot(line_x, line_y) ax.text(-5,-15, f'{a} * x + {b}', color='red') plt.draw() #計算下一步 (x,y) dx = df(x) v = -dx * lr x += v point_x.append(x) time.sleep(0.1) plt.figure(figsize=(10,6)) shrink_y=1e6 epochs = 60 lr=35 decay = 0.99 ax=plt.subplot() t=threading.Thread(target=runnable) t.start() plt.show()
上述就是使用傳統的方式,結果會卡在局部的低值(0,0) 中。
鞍點加動量
加入動量就可能突破鞍點,底下的圖就是加入球的慣性,讓球能越過下一波的高點,但這高點不能高於上一波的高點,否則會因動能不足而跨不過去。
import threading import time import numpy as np import matplotlib.pyplot as plt #目標函數 y=x^4 - 60 * x^3 -x + 1 def f(x): x=np.array(x) y = (np.power(x,4) - 60 * np.power(x,3) - x + 1) / shrink_y return y #目標函數的一階導數 dy/dx=2*x def df(x): return (4 * x**3 - 180 * x**2 - 1) / shrink_y def bias(a,x): b=f(x) - a * x return b def runnable(): xs = np.linspace(-30,60, 100) x=xs[0] ys=f(xs) point_x=[x] v=0 mu = 0.9 for i in range(epochs): ax.clear() ax.set_xlim(-45,70) ax.set_ylim(-2, 3) #ax.set_ylim(-1.5e6, 3e6) plt.plot(xs, ys) plt.scatter(point_x, f(point_x), c='r') #對目標函數進行微分 a=df(x) b=bias(a, x) #畫導線 x_l=x-5 x_r=x+5 line_x=[x_l, x_r] line_y=[a*(x_l)+b,a*(x_r)+b] plt.scatter(x, f(x), s=300, c='g') plt.plot(line_x, line_y) ax.text(-5,-15, f'{a} * x + {b}', color='red') plt.draw() #計算下一步 (x,y) dx = df(x) v= -dx * lr + mu * v x += v point_x.append(x) time.sleep(0.1) plt.figure(figsize=(10,6)) shrink_y=1e6 epochs = 60 lr=35 #decay = 0.99 ax=plt.subplot() t=threading.Thread(target=runnable) t.start() plt.show()