搞點枯燥的公式推導:深度學習中的矩陣求導
最近看到一篇極視角轉的文章演算法推導核心!一次性梳理清楚,是時候搞定矩陣求導了!,想到前不久面試愛奇藝的時候一面的小哥一直讓我手推全連線的公式推導,以及用Python+Numpy將過程實現,感覺自己對深度學習核心之一的矩陣求導並不是很熟悉(對鏈式法則更熟悉的是單元素標量的求導),為此寫下這篇作為後續工作的筆記之用,也供需要的小夥伴查詢。
前向傳播
程式碼大部分參考Python——numpy實現簡單BP神經網路識別手寫數字,將batch從1設定為64,以符合一般意義的做法。
該網路只有兩層,維度為
的輸入層
和維度為
的輸出層
,中間為全連線,網路定義為:
nn = NeuralNetwork([784, 10]) # 神經網路各層神經元個數
維度為
的權重矩陣
和維度為
的偏差矩陣
對應的程式碼:
for i in range(1, len(layers)): # 正態分佈初始化
self。weights。append(np。random。randn(layers[i-1], layers[i]))
self。bias。append(np。random。randn(layers[i]))
正向傳播的公式為
,其中啟用函式為sigmoid函式即
,對應的程式碼為:
def sigmoid(x): # 啟用函式採用Sigmoid
return 1 / (1 + np。exp(-x))
損失函式採用平方誤差
,所以輸出
的梯度為
,對應的程式碼為:
# 平方誤差得到的梯度值,非loss
error = (a[-1] - label)
這裡利用的一個知識就是演算法推導核心!一次性梳理清楚,是時候搞定矩陣求導了!提及的,標量對矩陣
的求導得到的是大小為
的矩陣,其中
。
梯度的反向傳播
然後就開始梯度的反向傳播,這裡採用鏈式法則,
,因為
,而
,所以
,對應的程式碼為:
def sigmoid_derivative(x): # Sigmoid的導數
return sigmoid(x) * (1 - sigmoid(x))
deltas = [error * self。activation_deriv(a[-1])] # 儲存各層誤差值的列表
需要注意的一點是這裡是*(即矩陣內的元素乘),而不是np。dot(即矩陣乘),這是因為使用的是啟用函式,啟用函式本身也是對單個元素進行操作。
而deltas裡面的元素表示的啟用函式的輸入即
的梯度,那麼權重矩陣
和維度為
的偏差矩陣
的梯度可以表示為:
(對應的定理是
以及鏈式法則
)、
,對應的程式碼為(需要除以batch):
layer = np。atleast_2d(a[i])
delta = np。atleast_2d(deltas[i])
# print (“delta。shape = %s” % str(delta。shape)) # (64, 10)
# reduce in dimension 0
self。weights[i] -= learning_rate * layer。T。dot(delta) / batch
self。bias[i] -= learning_rate * np。sum(delta, axis=0) / batch
如果是多層網路,可以利用鏈式法則從後往前不斷得到各層的梯度:
layer_num = len(a) - 2 # 倒數第二層開始
for j in range(layer_num, 0, -1):
deltas。append(deltas[-1]。dot(self。weights[j]。T) * self。activation_deriv(a[j])) # 誤差的反向傳播
訓練後可以看到loss和訓練準確率的變化:
其中藍色曲線表示的是loss,可以看到很快就變為0了,橙色曲線趨近的是batch數量(在本例中為64),測試的結果為:
訓練集大小33597,測試集大小8403
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5249/5249 [00:01<00:00, 2855。96it/s]
訓練完成!
開始檢測模型:
模型識別正確率: 0。7891229322860883
這個結果對兩層網路是很棒的。
對於啟用函式為ReLU的情況,可以參考論智:只用NumPy實現神經網路:
def relu(Z):
return np。maximum(0,Z)
def relu_backward(dA, Z):
dZ = np。array(dA, copy = True)
dZ[Z <= 0] = 0;
return dZ;
因為大於0的情況導數為1,小於0的情況導數為0,因此反向推導時只需要將輸入小於0的部分設定為0即可。
後記
其實本篇還有一些疑問沒有解答,有時間看書後再解決吧:
1.矩陣的求導
演算法推導核心!一次性梳理清楚,是時候搞定矩陣求導了!裡面提到,矩陣對矩陣求導並不具有向量對向量求導的鏈式法則,但是這裡輸入
、輸出
因為具有batch均為矩陣,這是一點疑問。
我能想象中的一點是batch只是為了並行化處理,batch之間的元素是相互不干擾的,比如看下面這段程式碼:
deltas。append(deltas[-1]。dot(self。weights[j]。T) * self。activation_deriv(a[j])) # 誤差的反向傳播
deltas[-1]的維度是
,self。weights[j]。T的維度是
,其中64即batch所處的維度是不參與計算的。
2.啟用函式的導數
Python——numpy實現簡單BP神經網路識別手裡面的啟用函式的導數是這樣的:
def sigmoid_derivative(x): # Sigmoid的導數
return sigmoid(x) * (1 - sigmoid(x))
但是用公式推導後發現啟用函式應該是這樣的:
def sigmoid_derivative(x): # Sigmoid的導數
return x * (1 - x)
因為傳入的引數已經經過sigmoid啟用函數了,但是測試結果表明最初的程式碼是正確的(測試準確率為0。110,對比最初的程式碼的準確率為0。789),這就百思不得姐了。
【已完結】