模型的優化與訓練 --- 梯度下降法及其衍生

梯度下降(Grandient Descent)

梯度下降的核心原理

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

最簡單的梯度下降算法 - 固定學習率的方法:

  • 待優化的函數 f(x)f(x)
  • 待優化函數的導數g(x)g(x)
  • 變量xx:保存當前優化過程中的參數值,優化開始時該變量將被初始化成某個數值,優化過程中這個變量會不斷變化,直到它找到最小值
  • 變量grad:保存變量xx點處的梯度值
  • 變量step:表示沿着梯度下降方向行進的步長,即學習率Learining Rate),在優化過程中它將固定不變
def gd(x_start, step, g):		   #  Gradient Descent
	x = x_start
	for i in range(20):
		grad = g(x)
		x -= grad * step
		print '[ epoch {0} ]  grad = {1}, x = {2}'.format(i, grad, x)
		if abs(grad) < 1e-6:
			break;
	return x
	

由於優化的目標是尋找梯度爲0的極值點,代碼在每一輪迭代結束後衡量變量xx所在的梯度值,因此當梯度值足夠小的時,就認爲xx已經進入最優值附近一個極小的領域,xx和最優值之間的差別不再明顯,就可以停止優化了。xx最後的值就是最優解的位置。

有興趣的可以跑一下以下簡單的二次函數的demo:

def f(x):
	return x * x - 2 * x + 1
def g(x):
	return 2 * x - 2
gd(5,0.1,g)

合適的步長非常重要

動量算法(Momentum)

在優化求解的過程中,動量代表了之前迭代優化量,它將在後面的優化過程中持續發揮作用,推動目標值前進。擁有了動量,一個已經結束的更新量不會立刻消失,只會以一定的形式衰減,剩下的能量將繼續在優化過程中發揮作用

def momentum(x_start, step, g ,discount = 0.7):
	x = np.array(x_start, dtype='float64')
	pre_grad = np.zeros_like(x)
	for i in range(50):
		grad = g(x)
		pre_grad = pre_grad * discount + grad * step
		x -= pre_grad
		print '[ epoch {0} ] grad = {1}, x = {2}'.format(i, grad ,x)
		if abs(sum(grad)) < 1e-6:
			break;
	return x

代碼中多出了一個新變量 pre_grad,這個變量就是用於存儲歷史積累的動量,每一輪迭代動量都會乘以一個打折量(discount)做能量衰減,但它依然會被用於更新參數。
動量算法相比較梯度下降,能使得梯度在下降時減少左右震盪(震盪的方向是相反的,由於歷史積累的動量,會相互抵消),加快梯度下降速度。
但是動量優化存在一點問題,前面幾輪的迭代過程中,梯度的震盪會比原來的還大,爲了解決這個更強烈的抖動,就有了接下來的Nesterov算法

Nesterov算法

def nesterov(x_start, step, g, discount = 0.7)
	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 * 0.7 + grad
		x -= pre_grad * step
		print '[ Epoch {0} ] grad = {1} , x = {2} '.format(i, grad, x)
		if abs(sum(grad)) < 1e-6:
			break;
	return x

與動量算法相比,動量算法計算了當前目標點的梯度,而Nesterov算法計算了動量更新後優化點的梯度。當優化點已經積累了某個抖動方向的梯度後,這時對於動量算法來說,雖然當前點的梯度指向積累梯度的相反方向,但是量不夠大,所以最終的優化方向還會在積累的方向上前進一段。對於Nesterov來說,如果按照積累方向再往前多走一段,這時梯度中指向積累梯度相反方向的量變大了許多,所以最終兩個方向的梯度抵消,反而使得抖動方向的量迅速減少。Nesterov的衰減速度確實比動量方法要快不少。

很多科研人員已經給出了動量打折率的建議配置 – 0.9
如果用GG表示每一輪迭代的動量,gg表示當前一輪迭代的更新量(方向 * 步長),tt表示迭代輪數,rr表示動量的打折率,那麼對於時刻tt的梯度更新量如下:
Gt=rGt1+gtG_t=rG_{t-1} + g_t
Gt=r(rGt2+gt1)+gtG_t=r(rG_{t-2}+g_{t-1}) + g_t
Gt=r2Gt2+rgt1+gtG_t=r^2G_{t-2} + rg_{t-1} + g_t
Gt=r2go+rt1g1+...+gtG_t=r^2 g_o + r^{t-1}g_1 + ... + g_t
所以,對於第一輪迭代的更新g0g_0來說,從G0G_0GTG_T,它的總貢獻量爲:
(rt+rt1+...+r+1)g0(r^t + r^{t-1} + ... + r + 1)g_0
它的貢獻和爲一個等比數列的和,比值爲rr。如果r=0.9r=0.9,那麼更新量在極限狀態下貢獻值:
g01r\frac {g_0}{1-r}
r=0.9r=0.9時,它一共貢獻了相當於自身10倍的能量。如果r=0.99r=0.99,那就是100倍能量了。

SGD的變種算法

Adagrad

Adagrad是一種自適應的梯度下降方法。何爲自適應呢?在梯度下降法中,參數的更新量等於梯度乘以學習率,也就是說,更新量和梯度是正相關的;而在實際應用中,每個參數的梯度各有不同,有的梯度大,有的梯度比較小,那麼就有可能遇到參數優化不均衡的情況。

參數優化不均衡對模型訓練來說不是件好事,這意味着不同的參數更新適用於不同的學習率。而Adagrad的自適應算法也正是要解決這個問題。算法希望不同參數的更新量能夠比較均衡。對於已經更新比較多的參數,它的更新量要適當衰減,而更新比較少的參數,它的更新量要儘量多一些,它的參數更新公式如下:
x=lrgg2+εx-=lr \cdot \frac{g}{\sqrt{\sum g^2}+\varepsilon}

其中ε\varepsilon的取值一般比較小,它只是爲了防止分母爲0.

def adagrad(x_start, step, g , delta=1e-8):
	x.np.array(x_start,dtype='float64')
	sum_grad = np.zeros_like(x)
	for i in range(50):
		grad = g(x)
		sum_grad += grad * grad
		x -= step* grad /(np.sqrt(sum_grad)+delta)
		if abs(sum(grad)) < 1e-6:
			break;
	return x

從公式和代碼中可以發現,算法積累了歷史的梯度值的和,並用這個加和來調整每個參數的更新量——對於之前更新量大的參數,分母也會比較大,於是未來它的更新量會比較小;對於之前更新量小的參數,分母也相對小一些,於是未來它的更新量會相對大一些。

Rmsprop

Adagrad算法有一個很大的問題,那就是隨着優化的迭代次數不斷增加, 更新公式的分母項會變得越來越大。所以理論上更新量也會越來越小,這對優化十分不利。Rmsprop就試圖解決這個問題,在它的算法中,分母的梯度平方和不再隨優化而增加,而是做加權平均。更新公式如下:
Gt+1=βGt+(1β)g2G_{t+1}=\beta G_t + (1 - \beta)g^{2}
xt+1=xtlrgGt+1+εx_{t+1} = x_t - lr\frac{g}{\sqrt G_{t+1}+\varepsilon}

def rmsprop(x_start, step, g , rms_decay = 0.9, delta=1e-8):
	x = np.array(x_start, dtype = 'float64')
	sum_grad = np.zeros_like(x)
	passing_dot = [x.copy()]
	for i in range(50):
		grad = g(x)
		sum_grad = rms_decay * sum_grad + (1 - rms_decay) * grad * grad
		x -= step * grad /(np.sqrt(sum_grad) + delta)
		passing_dot.append(x.copy())
		if abs(sum(grad)) < 1e-6:
			break;
	return x , passing_dot

Adam

Adam既包含了動量算法的思想,也包含了RmsProp的自適應梯度的思想。在計算過程彙總,Adam既要像動量算法那樣計算累計的動量:
mt+1=β1mt+(1β1)gtm_{t+1} = \beta_1m_t + (1-\beta_1)g_t
又要像RmsProp那樣計算梯度的滑動平方和:
vt+1=β2vt+(1β2)gt2v_{t+1} = \beta_2v_t + (1 - \beta_2)g_t^{2}
作者沒有直接把這兩個計算值加入最終計算的公式中,作者推導了兩個計算量與期望的差距,於是給這兩個變量加上了修正量:
m^t=mt1β1t\hat m_t = \frac{m_t}{1 -\beta_1^t}
v^t=vt1β2t\hat v_t = \frac{v_t}{1-\beta_2^t}
最後,兩個計算量將融合到最終的公式中:
xt+1=xtlrm^tv^t+εx_{t+1} = x_t - lr\frac{\hat m_t}{\sqrt{\hat v_t}+\varepsilon}

def adam(x_start, step, g, beta1 = 0.9, beta2 = 0.999, delta=1e-8):
	x = np.array(x_start, dtype='float64')
	sum_m = np.zeros_like(x)
	sum_v = np.zeros_like(x)
	passing_dot = [x.copy()]
	for i in range (50):
		grad = g(x)
		sum_m = beta1 * sum_m + (1 - beta1) * grad
		sum_v = beta2 * sum_v + (1 - beta2) * grad * grad
		correction = np.sqrt(1 - beta2 ** i) / (1 - beta1 ** i)
		x -= step * correction * sum_m / (np.sqrt(sum_v) + delta)
		passing_dot.append(x.copy())
		if abs(sum(grad)) < 1e-6:
			break;
	return x, passing_dot

Adam算法結合了動量的“慣性”和自適應的“起步快”這兩個特點。綜合來看,RmsProp和Adam的表現更平穩,現在大部分科研人員都在使用這兩種優化方法。

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