作者:李理
目前就職于環(huán)信,即時(shí)通訊云平臺和全媒體智能客服平臺,在環(huán)信從事智能客服和智能機器人相關(guān)工作,致力于用深度學(xué)習來(lái)提高智能機器人的性能。
相關(guān)文章:
Optimization
這一部分內容來(lái)自:CS231n Convolutional Neural Networks for Visual Recognition
簡(jiǎn)介
我們的目標:x是一個(gè)向量,f(x)是一個(gè)函數,它的輸入是一個(gè)向量(或者認為是多變量的函數,這個(gè)輸入向量就是自變量),輸出是一個(gè)實(shí)數值。我們需要計算的是f對每一個(gè)自變量的導數,然后把它們排成一個(gè)向量,也就是梯度。

為什么要求這個(gè)呢?前面我們也講了,我們的神經(jīng)網(wǎng)絡(luò )的損失函數最終可以看成是權重weights和bias的函數,我們的目標就是調整這些參數,使得損失函數最小。
簡(jiǎn)單的表達式和梯度的解釋
首先我們看一個(gè)很簡(jiǎn)單的函數f(x,y)=xy,求f對x和y的偏導數很簡(jiǎn)單:

首先來(lái)看導數的定義:

函數在某個(gè)點(diǎn)的導數就是函數曲線(xiàn)在這個(gè)點(diǎn)的斜率,也就是f(x)隨x的變化率。
比如上面的例子,當x=4,y=-3時(shí)f(x,y)=-12,f對x的偏導數

也就是說(shuō),如果我們固定y=4,然后給x一個(gè)很小的變化h,那么f(x,y)的變化大約是-3*h。
因此乘法的梯度就是

同樣,加法的梯度更簡(jiǎn)單:

最后一個(gè)簡(jiǎn)單函數是max函數:

這個(gè)導數是ReLU(x)=max(x,0)的導數,其實(shí)也簡(jiǎn)單,如果x>=y,那么max(x,y)=x,則導數是1,否則max(x,y)=0,那么對x求導就是0.
復雜表達式的鏈式法則
接下來(lái)看一個(gè)稍微復雜一點(diǎn)的函數f(x,y,z)=(x+y)z。我們引入一個(gè)中間變量q,f=qz,q=x+y,我們可以使用鏈式法則求f對x和y的導數。

對y的求導也是類(lèi)似的。
下面是用python代碼來(lái)求f對x和y的導數在某一個(gè)點(diǎn)的值。
# 設置自變量的值
x = -2; y = 5; z = -4
# “前向”計算f
q = x + y # q becomes 3
f = q * z # f becomes -12
# 從“后”往前“反向”計算
# 首先是 f = q * z
dfdz = q # 因為df/dz = q, 所以f對z的梯度是 3
dfdq = z # 因為df/dq = z, 所以f對q的梯度是 -4
# 然后 q = x + y
dfdx = 1.0 * dfdq # 因為dq/dx = 1,所以使用鏈式法則計算dfdx=-4
dfdy = 1.0 * dfdq # 因為dq/dy = 1,所以使用鏈式法則計算dfdy=-4
我們也可以用計算圖來(lái)表示和計算:

綠色的值是feed forward的結果,而紅色的值是backprop的結果。
不過(guò)我覺(jué)得cs231n課程的這個(gè)圖沒(méi)有上面blog的清晰,原因是雖然它標示出來(lái)了最終的梯度,但是沒(méi)有標示出local gradient,我在下面會(huì )畫(huà)出完整的計算過(guò)程。
反向傳播算法的直覺(jué)解釋
我們如果把計算圖的每一個(gè)點(diǎn)看成一個(gè)“門(mén)”(或者一個(gè)模塊),或者說(shuō)一個(gè)函數。它有一個(gè)輸入(向量),也有一個(gè)輸出(標量)。對于一個(gè)門(mén)來(lái)說(shuō)有兩個(gè)計算,首先是根據輸入,計算輸出,這個(gè)一般很容易。還有一種計算就是求輸出對每一個(gè)輸入的偏導數,或者說(shuō)輸出對輸入向量的”局部“梯度(local gradient)。一個(gè)復雜計算圖(神經(jīng)網(wǎng)絡(luò ))的計算首先就是前向計算,然后反向計算,反向計算公式可能看起來(lái)很復雜,但是如果在計算圖上其實(shí)就是簡(jiǎn)單的用local gradient乘以從后面傳過(guò)來(lái)的gradient,然后加起來(lái)。
Sigmoid模塊的例子
接下來(lái)我們看一個(gè)更復雜的例子:

這個(gè)函數是一個(gè)比較復雜的復合函數,但是構成它的基本函數是如下4個(gè)簡(jiǎn)單函數:

下面是用計算圖畫(huà)出這個(gè)計算過(guò)程:

這個(gè)圖有4種gate,加法,乘法,指數和倒數。加法有加一個(gè)常數和兩個(gè)變量相加,乘法也是一樣。
上圖綠色的值是前向計算的結果,而紅色的值是反向計算的結果,localgraident并沒(méi)有標示出來(lái),所以看起來(lái)可能有些跳躍,下面我在紙上詳細的分解了其中的步驟,請讀者跟著(zhù)下圖自己動(dòng)手計算一遍。

上圖就是前向計算的過(guò)程,比較簡(jiǎn)單。

第二個(gè)圖是計算local gradient,對于兩個(gè)輸入的乘法和加法,local gradient也是兩個(gè)值,local gradient的值我是放到圖的節點(diǎn)上了。

第三個(gè)圖是具體計算一個(gè)乘法的local gradient的過(guò)程,因為上圖可能看不清,所以單獨放大了這一步。

最后計算真正的梯度,是把local gradient乘以來(lái)自上一步的gradient。不過(guò)這個(gè)例子一個(gè)節點(diǎn)只有一個(gè)輸出,如果有多個(gè)的話(huà),梯度是加起來(lái)的,可以參考1.4的

上面我們看到把

分解成最基本的加法,乘法,導數和指數函數,但是我們也可以不分解這么細。之前我們也學(xué)習過(guò)了sigmoid函數,那么我們可以這樣分解:

σ(x)σ(x)的導數我們之前已經(jīng)推導過(guò)一次了,這里再列一下:

因此我們可以把后面一長(cháng)串的gate”壓縮“成一個(gè)gate:

我們來(lái)比較一下,之前前向計算σ(x)σ(x)需要一次乘法,一次exp,一次加法導數;而反向計算需要分別計算這4個(gè)gate的導數。
而壓縮后前向計算是一樣的,但是反向計算可以”利用“前向計算的結果

這只需要一次減法和一次乘法!當然如果不能利用前向的結果,我們如果需要重新計算σ(x)σ(x),那么壓縮其實(shí)沒(méi)有什么用處。能壓縮的原因在于σ函數導數的特殊形式。而神經(jīng)網(wǎng)絡(luò )的關(guān)鍵問(wèn)題是在訓練,訓練性能就取決于這些細節。如果是我們自己來(lái)實(shí)現反向傳播算法,我們就需要利用這樣的特性。而如果是使用工具,那么就依賴(lài)于工具的優(yōu)化水平了。
下面我們用代碼來(lái)實(shí)現一下:
w = [2,-3,-3] # assume some random weights and data
x = [-1, -2]
# forward pass
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid function
# backward pass through the neuron (backpropagation)
ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
dx = [w[0] * ddot, w[1] * ddot] # backprop into x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
# we're done! we have the gradients on the inputs to the circuit
上面的例子用了一個(gè)小技巧,就是所謂的stagedbackpropagation,說(shuō)白了就是給中間的計算節點(diǎn)起一個(gè)名字。比如dot。為了讓大家熟悉這種技巧,下面有一個(gè)例子。
Staged computation練習

我們用代碼來(lái)計算這個(gè)函數對x和y的梯度在某一點(diǎn)的值
前向計算
x = 3 # example values
y = -4
# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # 分子上的sigmoid #(1)
num = x + sigy # 分子 #(2)
sigx = 1.0 / (1 + math.exp(-x)) # 分母上的sigmoid #(3)
xpy = x + y #(4)
xpysqr = xpy**2 #(5)
den = sigx + xpysqr # 分母 #(6)
invden = 1.0 / den #(7)
f = num * invden # done! #(8)
反向計算
# backprop f = num * invden
dnum = invden # gradient on numerator #(8)
dinvden = num #(8)
# backprop invden = 1.0 / den
dden = (-1.0 / (den**2)) * dinvden #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden #(6)
dxpysqr = (1) * dden #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr #(5)
# backprop xpy = x + y
dx = (1) * dxpy #(4)
dy = (1) * dxpy #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below #(3)
# backprop num = x + sigy
dx += (1) * dnum #(2)
dsigy = (1) * dnum #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy #(1)
# done! phew
需要注意的兩點(diǎn):1.前向的結果都要保存下來(lái),反向的時(shí)候要用的。2.如果某個(gè)變量有多個(gè)出去的邊,第一次是等于,第二次就是+=,因為我們要把不同出去點(diǎn)的梯度加起來(lái)。
下面我們來(lái)逐行分析反向計算:
(8)f=num*invden
local gradient

而上面傳過(guò)來(lái)的梯度是1,所以dnum=1*invden。注意變量的命名規則,df/dnum就命名為dnum【省略了df,因為默認我們是求f對所有變量的偏導數】
同理:dinvden=num
(7)invden=1.0/den
local gradient是(-1.0/(den**2)),然后乘以上面來(lái)的dinvden
(6)den=sigx+xpysqr
這個(gè)函數有兩個(gè)變量sigx和xpysqr,所以需要計算兩個(gè)local梯度,然后乘以dden
加法的local梯度是1,所以就是(1)*dden
(5)xpysqr=xpy**2
localgradient是2*xpy,再乘以dxpysqr
(4)xpy=x+y
還是一個(gè)加法,local gradient是1,所以dx和dy都是dxpy乘1
(3)sigx=1.0/(1+math.exp(-x))
這是sigmoid函數,local gradient是(1-sigx)*sigx,再乘以dsigx。
不過(guò)需要注意的是這是dx的第二次出現,所以是+=,表示來(lái)自不同路徑反向傳播過(guò)來(lái)給x的梯度值
(2)num=x+sigy
還是個(gè)很簡(jiǎn)單的加法,local gradient是1.需要注意的是dx是+=,理由同上。
(1)sigy=1.0/(1+math.exp(-y))
最后是sigmoid(y)和前面(3)一樣的。
請仔細閱讀上面反向計算的每一步代碼,確保自己理解了之后再往下閱讀。
梯度的矩陣運算
前面都是對一個(gè)標量的計算,在實(shí)際實(shí)現時(shí)用矩陣運算一次計算一層的所有梯度會(huì )更加高效。因為矩陣乘以向量和向量乘以向量都可以看出矩陣乘以矩陣的特殊形式,所以下面我們介紹矩陣乘法怎么求梯度。
首先我們得定義什么叫矩陣對矩陣的梯度!
我查閱了很多資料,也沒(méi)找到哪里有矩陣對矩陣的梯度的定義,如果哪位讀者知道,請告訴我,謝謝!唯一比較接近的是Andrew Ng的課程cs294的背景知識介紹的slides linalg的4.1節定義了gradient of Matrix,關(guān)于矩陣對矩陣的梯度我會(huì )有一個(gè)猜測性的解釋?zhuān)赡軙?huì )有問(wèn)題。
首先介紹graiden to fmatrix
假設f:Rm×n→R是一個(gè)函數,輸入是一個(gè)m×n的實(shí)數值矩陣,輸出是一個(gè)實(shí)數。那么f對A的梯度是如下定義的:

看起來(lái)定義很復雜?其實(shí)很簡(jiǎn)單,我們把f看成一個(gè)mn個(gè)自變量的函數,因此我們可以求f對這mn個(gè)自變量的偏導數,然后把它們排列成m*n的矩陣就行了。為什么要多此一舉把變量拍成矩陣把他們的偏導數也排成矩陣?想想我們之前的神經(jīng)網(wǎng)絡(luò )的weights矩陣,這是很自然的定義,同時(shí)我們需要計算loss對weights矩陣的每一個(gè)變量的偏導數,寫(xiě)出這樣的形式計算起來(lái)比較方便。
那么什么是矩陣對矩陣的梯度呢?我們先看實(shí)際神經(jīng)網(wǎng)絡(luò )的一個(gè)計算情況。對于全連接的神經(jīng)網(wǎng)絡(luò ),我們有一個(gè)矩陣乘以向量D=WxD=Wx【我們這里把向量x看成矩陣】。現在我們需要計算loss對某一個(gè) WijWij的偏導數,根據我們之前的計算圖, WijWij有多少條出邊,那么就有多少個(gè)要累加的梯度乘以local梯度。
假設W是m×n的矩陣,x是n×p的矩陣,則D是m×p的矩陣

根據矩陣乘法的定義

我們可以計算:

請仔細理解上面這一步,如果k≠i,則不論s是什么,Wks跟Wij不是同一個(gè)變量,所以導數就是0;如果k=i,∑sWisxsl=xjl,也就求和的下標s取j的時(shí)候有WijWij。
因此

上面計算了loss對一個(gè)Wijj的偏導數,如果把它寫(xiě)成矩陣形式就是:

前面我們推導出了對Wij的偏導數的計算公式,下面我們把它寫(xiě)成矩陣乘法的形式并驗證【證明】它。

為什么可以寫(xiě)成這樣的形式呢?

上面的推導似乎很復雜,但是我們只要能記住就行,記法也很簡(jiǎn)單——把矩陣都變成最特殊的11的矩陣(也就是標量,一個(gè)實(shí)數)。D=wx,這個(gè)導數很容易吧,對w求導就是local gradientx,然后乘以得到dW=dDx;同理dx=dDW。
但是等等,剛才那個(gè)公式里還有矩陣的轉置,這個(gè)怎么記?這里有一個(gè)小技巧,就是矩陣乘法的條件,兩個(gè)矩陣能相乘他們的大小必須匹配,比如D=W x,W是m n,x是n p,也就是第二個(gè)矩陣的行數等于第一個(gè)的列數。
現在我們已經(jīng)知道dW是dD”乘以“x了,dW的大小和W一樣是m n,而dD和D一樣是m p,而x是n p,那么為了得到一個(gè)mn的矩陣,唯一的辦法就是dD∗xT
同理dx是np,dD是mp,W是m*n,唯一的乘法就是WT∗dD
下面是用python代碼來(lái)演示,numpy的dot就是矩陣乘法,可以用numpy.dot(A,B),也可以直接調用ndarray的dot函數——A。dot(B):
# forward pass
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)
# now suppose we had the gradient on D from above in the circuit
dD = np.random.randn(*D.shape) # same shape as D
dW = dD.dot(X.T) #.T gives the transpose of the matrix
dX = W.T.dot(dD)
至此,本系列文章的第5部分告一段落。在接下來(lái)的文章中,作者將為大家詳細講述關(guān)于常見(jiàn)的深度學(xué)習框架/工具的使用方法、使用自動(dòng)求導來(lái)實(shí)現多層神經(jīng)網(wǎng)絡(luò )等內容,敬請期待。