神經網絡的學習的目的就是找到合適的參數使損失函數的值儘可能的小。這種尋找最優參數的過程就叫做最優化(optimization)。然而在深度神經網絡中,參數的數量非常龐大,導致最優化的問題非常複雜。下面介紹四種常見的最優化的方法,並通過一個例子進行比較。
1. SGD(stochastic gradient descent)
SGD即隨機梯度下降法,這個方法通過梯度下降法更新參數,不過因爲這裏使用的數據是隨機選擇的min batch數據,所以稱爲隨機梯度下降法。這裏的“隨機”指的是“隨機選擇的”的意思,因此隨機梯度下降法是“對隨機選擇的數據進行的梯度下降法”。用數學式表示爲:
這裏把需要更新的權重參數記爲W,把損失函數關於W的梯度記爲。表示學習率,實際上會取0.001或0.0001這些事先決定好的值。這個式子表示,SGD是朝着梯度方向前進一小步的簡單方法。現在將SGD實現爲一個python類。
class SGD:
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
2. Momentum
Momentum是“動量”的意思,和物理有關。用數學式表示如下:
和前面SGD一樣,W表示要更新的權重參數,表示損失函數關於W的梯度,表示學習率。這裏新出現了一個變量v,對應物理上的速度。第一個式子表示物體在梯度方向上受力,在這個力的作用下,物體的速度增加這一法則。表示物理在不受任何力時,該項承擔使物體逐漸減速的任務(設定爲0.9之類的值),對應物理上的地面摩擦力或空氣阻力。Momentum方法給人的感覺就像是小球在地面上滾動。
下面是Momentum代碼實現:
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
實例變量v會保存物體的速度。初始化時,v中什麼都不保存,但當第一次調用update()時,v會以字典型變量的形式保存與參數結構相同的數據。
3. AdaGrad
在神經網絡的學習中,學習率(數學式中記爲)的值很重要。學習率過小,會導致學習花費過多時間;學習率過大,則會導致學習發散而不能正確進行。
在關於學習率的有效技巧中,有一種被稱爲學習率衰減(learning rate decay)的方法,即隨着學習的進行,使學習率逐漸減小。實際上,一開始“多”學,然後逐漸“少”學的方法,在神經網絡的學習中經常被使用。
逐漸減少學習率的想法,相當於將“全體”參數的學習率值一起降低。而AdaGrad進一步發展了這個想法,針對“一個一個”參數,賦予其“定製” 的值。用數學式表示:
和前面SGD一樣,W表示要更新的權重參數,表示損失函數關於W的梯度,表示學習率。這裏新出現了一個變量h,它保存了以前的所有梯度值的平方和(表示對應矩陣元素的乘法)。然後在更新參數時,通過乘以,就可以調整學習的尺度。這意味着,參數的元素中變動較大(被大幅更新)的元素的學習率將變小。也就是說,可以按參數的元素進行學習率衰減,使變動大的參數的學習率逐漸減小。
現在來實現AdaGrad:
class AdaGrad:
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] += grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
這裏需要注意的是,最後加上的1e-7是爲了防止self.h[key]中有0時,將0用作除數的情況。在很多深度學習框架中,這個微小值也可以設定爲參數,但這裏我們用的是1e-7這個固定值。
拓展:AdaGrad會記錄過去所有梯度的平方和。因此,學習越深入,更新的幅度越小。實際上,如果無止境的學習,更新量就會變成0,完全不再更新。爲了改善這個問題,可以使用RMSProp方法。RMSProp方法不是將過去的所有梯度一視同仁的相加,而是逐漸遺忘過去的梯度,在做加法運算時將新的梯度信息更多的反映出來。這種操作從專業上講,稱爲“指數移動平均”,呈指數函數式地減小過去的梯度的尺度。
4. Adam
Momentum參照小球在碗中滾動的物理規則進行移動,AdaGrad爲參數的每個元素適當的調整更新步伐。Adam就是將這兩個方法融合在一起。這只是一個直觀的說明,並不完全正確,詳細內容可以參考作者的論文。Diederik Kingma and Jimmy Ba. Adam: A Method for Stochastic Optimization.
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0
self.m = None
self.v = None
def update(self, params, grads):
if self.m is None:
self.m, self.v = {}, {}
for key, val in params.items():
self.m[key] = np.zeros_like(val)
self.v[key] = np.zeros_like(val)
self.iter += 1
lr_t = self.lr * np.sqrt(1.0 - self.beta2 ** self.iter) / (1.0 - self.beta1 ** self.iter)
for key in params.keys():
self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
self.v[key] += (1 - self.beta2) * (grads[key] ** 2 - self.v[key])
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
例:下面用這個四種方法尋找的最小值點。
從(x,y)=(-7.0,2.0)的位置開始搜索,用導數法求梯度,,
具體代碼如下:
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from ch06.optimizer import *
# 原函數
def f(x, y):
return x ** 2 / 20.0 + y ** 2
# 求梯度
def df(x, y):
return x / 10.0, 2.0 * y
init_pos = (-7.0, 2.0) # 初始點
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0
optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["AdaGrad"] = AdaGrad(lr=1.5)
optimizers["Adam"] = Adam(lr=0.3)
idx = 1
for key in optimizers:
optimizer = optimizers[key]
x_history = []
y_history = []
params['x'], params['y'] = init_pos[0], init_pos[1]
for i in range(30):
x_history.append(params['x'])
y_history.append(params['y'])
grads['x'], grads['y'] = df(params['x'], params['y'])
optimizer.update(params, grads)
x = np.arange(-10, 10, 0.01)
y = np.arange(-5, 5, 0.01)
X, Y = np.meshgrid(x, y) # 生成網格點座標矩陣
Z = f(X, Y)
# for simple contour line
mask = Z > 7
Z[mask] = 0
# plot
plt.subplot(2, 2, idx)
idx += 1
plt.plot(x_history, y_history, 'o-', color="red")
plt.contour(X, Y, Z)
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.plot(0, 0, '+')
# colorbar()
# spring()
plt.title(key)
plt.xlabel("x")
plt.ylabel("y")
plt.show()
運行結果:
到目前爲止,介紹了4種最優化的方法,那麼哪種方法好呢。其實並不存在能在所有問題中表現都好的方法。這4種方法各有各的特點,都有各自擅長的領域。目前用的比較多的就是SGD和Adam.