機器學習之數學系列(二)梯度下降法(參數更新公式的由來)

一、引言

下山問題
  如下圖所示,假設我們位於黃山的某個山腰處,山勢連綿不絕,不知道怎麼下山。於是決定走一步算一步,也就是每次選個方向往山下走一步,這樣一步一步走下去,一直走到覺得我們已經到了山腳。問題是當我以一定的步長下坡時,我可以選擇的方向有很多,到底選哪個方向最好呢?經驗告訴我們選最陡的方向走,因爲這樣可以快速下山。那具體的最陡的方向是哪個方向?答案是:梯度的負方向。本文的工作有1.闡述爲什麼下坡最陡的方向即對於凸函數而言值變小最快的方向是梯度的反方向。2.給出梯度下降法中的參數更新公式。3.解釋爲啥不直接求導而要用梯度下降法。4.給出梯度下降法代碼示例。
在這裏插入圖片描述

二、梯度下降法

  什麼是梯度?通俗來說,梯度就是表示函數值變化最快的方向,即:f(θ)=(fθ1,fθ2,...,fθn)θ=(θ1,θ2,...,θn)\nabla f(\vec{\theta})=(\frac{\partial f}{\partial\theta_1},\frac{\partial f}{\partial\theta_2},...,\frac{\partial f}{\partial\theta_n}),其中\vec{\theta}=(\theta_1,\theta_2,...,\theta_n)
在這裏插入圖片描述  如上圖所示,紅點爲參數點,紅色點對應的函數值爲黑色點值,當紅色點沿着某一方向移動時函數值將跟隨變化。很明顯參數更新公式就應當是θcur=θpre+ηv\vec \theta_{cur}=\vec \theta_{pre}+\eta \vec v,其中η\eta是參數點移動的步長(各方向的合步長),v\vec v是參數更新單位方向向量。根據圖像我們發現只能沿着某些方向才能使得函數值往小了變化,例如與梯度方向呈鈍角的所有方向都可以,進一步的我需要考慮的是哪個方向能使得函數值變小最快。
在這裏插入圖片描述
  以一維函數爲例說明函數變小最快的方向。如上圖所示,凸函數f(θ)f(θ)的某一小段[θθ,θ0θ_0]由上圖黑色曲線表示,可以利用線性近似的思想求出f(θ)f(θ)的值即:當步長很小時,曲線近似於紅色斜直線。紅色直線的斜率等於f(θ)f(θ)θ0θ_0處的導數。則根據直線方程,很容易得到f(θ)f(θ)的近似表達式爲:f(θ)f(θ0)+(θθ0)f(θ0)f(\theta)\approx f(\theta_0)+(\theta-\theta_0)\nabla f(\theta_0)
  這也是一階泰勒展開式的推導過程,主要利用的數學思想就是曲線函數的線性擬合近似。
  根據多維的參數更新公式這裏的一維參數更新公式應當爲:θθ0=ηv\theta-\theta_0=\eta \vec v帶入一階泰勒展開式:f(θ)f(θ0)+ηvf(θ0)f(\theta)\approx f(\theta_0)+\eta \vec v\nabla f(\theta_0)
  我們在凸函數上是希望通過逐步下降方式來求得最小值,故而希望,f(θ)<f(θ0)f(\theta)<f(\theta_0),則有:f(θ)f(θ0)=vf(θ0)<0f(\theta)-f(\theta_{0})= \vec v \nabla f(\theta_0)<0
  從這個公式可知要使得函數值往小了變,參數更新方向必須和梯度方向是呈鈍角的。f(θ)f(θ0)f(\theta)-f(\theta_0)要想獲得最大值(負最大),即要求參數更新方向v\vec vf(θ0)\nabla f(\theta_0)方向相反即可。實際上無論凸函數還是凹函數,沿着當前點的梯度方向的反方向更新參數,它會往函數的極值點走並且這個方向是最快方向。

三、參數更新公式

在這裏插入圖片描述  如圖所示:從點A(1,1)沿着v\vec v(22\frac{\sqrt2}{2},22\frac{\sqrt2}{2})方向移動2個單位到達點B(1+2\sqrt2, 1+2\sqrt2),那麼A、B兩點的關係是什麼?應該是B\vec B=A\vec A+2*v\vec v。由此可以得出一般的參數的更新公式是:
θcur=θpre+ηv\vec \theta_{cur}=\vec \theta_{pre}+\eta \vec v,其中η\eta是參數移動的步長,v\vec v是參數更新單位方向向量。參數更新公式一定要帶單位方向,如果只有步長那麼將不知道參數是往哪個方向更新的,在一維的情況下我們會根據圖像來看是+η+\eta還是η-\eta,符號即單位方向向量。如何確定更新方向?其實更新方向基本的就一個要求即:使得函數值變大或變小,例如二維凸函數求最小值時與梯度方向呈鈍角的所有方向都可以,之所以最終確定的是與梯度呈180度角的方向(即反方向)是因爲那個方向既可以使得函數值變小又保證了變小最快;在一維凸函數中,要想使得函數值變小它只有一個方向,只能取那個方向來更新參數。如果v\vec v取梯度的反方向那麼即:v=f(θ0)f(θ0)\vec v=-\frac{\nabla f(\theta_0)}{||\nabla f(\theta_0)||},那麼便得到了常用的參數更新公式:θcur=θpreηf(θ0)f(θ0)\vec \theta_{cur}=\vec \theta_{pre}-\eta\frac{\nabla f(\theta_0)}{||\nabla f(\theta_0)||}
將分母合併進步長表達爲更一般形式:θcur=θpreηf(θ0)\vec \theta_{cur}=\vec \theta_{pre}-\eta\nabla f(\theta_0)負梯度值表明了參數更新的方向,這個公式隱式的增大了步長,故而要把步長設小一點。
  參數更新方向不一定非得是梯度的反方向,還可能是其他方向,例如爲了跳出鞍點,往往不再選擇梯度反方向而是考慮其他更合適的方向,有興趣的可以去了解一下Momentum、RMSprop、Adam優化算法的參數更新公式,看看它們是如何考慮參數更新方向的。
  梯度下降法需把握以下兩點:
1.參數更新公式是什麼 。
2.梯度如何計算,這需要函數矩陣求導和鏈式法則的知識。

四、爲什麼要用梯度下降法

  爲什麼不直接求出導數,然後令導數爲0,然後解方程獲得極值點的參數值,而要去用梯度下降法來逐步逼近求?原因是:
1.梯度爲0的方程不一定能解出來,但是我們計算梯度的值是可以算的
2.以線性迴歸爲例通過代價函數對參數求導,令其爲零,得出參數爲:
在這裏插入圖片描述
參數的結果給出兩個信息,同時也是直接求導不可行的原因:
X的轉置乘以X必須要可逆,也就是X必須可逆,但是實際情況中並不一定都滿足這個條件,因此直接求導不可行;假設可逆,那麼就需要去求X的轉置乘以X這個整體的逆,線性代數中給出了求逆矩陣的方法,是非常複雜的(對計算機來說就是十分消耗性能的),數據量小時,還可行,一旦數據量大,計算機求矩陣的逆將會是一項非常艱鉅的任務,消耗的性能以及時間巨大,而在機器學習中,數據量少者上千,多者上億;因此直接求導不可行。相較而言,梯度下降算法同樣能夠實現最優化求解,通過多次迭代使得代價函數收斂,並且使用梯度下降的計算成本很低,所以基於以上兩個原因,迴歸中多數採用梯度下降而不是求導等於零來求參數。

五、梯度下降法示例

#example1
def grad_descent(n, initx, lambd=0.01):
	"""
	Q:隨機給一個值n,估算x,使得x**2=n
	A:令f(x) = x**2 - n,f(x)趨於0時的x即爲所求,用梯度下降法逼近求解
	"""
	x = initx
	fx = x**2 - n
	while fx>0:#結束條件可以是迭代次數也可以是某個指標的值達到閾值
		fx = x**2 - n
		print(fx)
		x -= lambd*2*x
	return float(format(x, '.2f'))
print(grad_descent(4.0, -5.9, 0.001))
#example2
import numpy as np
class LogisticRegression(object):
	# W = np.zeros((1,dim)), b = 0
	def __init__(self, W, b):
		self.W = W
		self.b = b

	def forward(self, X, Y):
		grad = {}
		dim, m = X.shape
		O = np.array([1 for _ in range(m)])
		Z = np.dot(self.W, X) + self.b
		A = 1 / (1 + np.exp(-Z))
		loss = 1/m * np.dot((-Y * np.log(A) - (O-Y)*np.log(O-A)), O.T)
		dw = 1/m * np.dot(A-Y, X.T)
		db = 1/m * np.dot(A-Y, O.T)
		grad['dw'] = dw
		grad['db'] = db
		return grad, loss

	def fit(self, X, Y, lambd, iteration_num):
		params = {}
		for i in range(iteration_num):
			grad, loss = self.forward(X,Y)
			dw = grad['dw']
			db = grad['db']
			self.W = self.W - lambd * dw
			self.b = self.b - lambd * db
			if i % 10==0:
				print('iter {0}, loss:{1}'.format(i, float(loss)))
		params['W'] = self.W
		params['b'] = self.b
		return params

	def predict(self, x):
		Z = np.dot(self.W, X) + self.b
		A = 1 / (1 + np.exp(-Z))
		return A

	
lr = LogisticRegression(np.zeros((1,2)), 0)
X = np.array([[1.,2.,-1.],[3.,4.,-3.2]])
Y = np.array([[1,0,1]])
print(lr.fit(X, Y, 0.1, 1000))

參考:

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