用 Python 從頭開始實現一個全連線的神經網路
持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第31天,點選檢視活動詳情
在這篇文章中,準備用 Python 從頭開始實現一個全連線的神經網路。你可能會問,為什麼需要自己實現,有很多庫和框架可以為我們做這件事,比如 Tensorflow、Pytorch 等。這裡只想說只有自己親手實現了,才是自己的。
想到今天自己從接觸到從事與神經網路相關工作已經多少 2、3 年了,其中也嘗試用 tensorflow 或 pytorch 框架去實現一些經典網路。不過對於反向傳播背後機制還是比較模糊。
梯度
梯度是函式上升最快方向,最快的方向也就是說這個方向函式形狀很陡峭,那麼也是函式下降最快的方向。
雖然關於一些理論、梯度消失和結點飽和可以輸出一個 1、2、3 但是深究還是沒有底氣,畢竟沒有自己動手去實現過一個反向傳播和完整訓練過程。所以感覺還是浮在表面,知其所以然而。
因為最近有一段空閒時間、所以利用這段休息時間將要把這部分知識整理一下、深入瞭解瞭解
| 型別 | 符號 | 說明 | 表示式 | 維度 | | --- | --- | --- | --- | --- | | 標量 | $n^L$ | 表示第 L 層神經元的數量 | | | | 向量 | $B^L$ | 表示第 L 層偏置 | |$n^L \times 1$ | | 矩陣 | $W^L$ | 表示第 L 層的權重 | |$n^L \times n^L$ | | 向量 | $Z^L$ | 表示第 L 層輸入到啟用函式的值 | $Z^L=W^LA^{(L-1)} + B^L$ | $n^L \times 1$ | | 向量 | $A^L$ | 表示第 L 層輸出值 | $A^L = \sigma(Z^L)$ | $n^L \times 1$ |
我們大家可能都瞭解訓練神經網路的過程,就是更新網路引數,更新的方向是降低損失函式值。也就是將學習問題轉換為了一個優化的問題。那麼如何更新引數呢?我們需要計算參與訓練引數相對於損失函式的導數,然後求解梯度,然後使用梯度下降法來更新引數,迭代這個過程,可以找到一個最佳的解決方案來最小化損失函式。
我們知道反向傳播主要就是用來結算損失函式相對於權重和偏置的導數
可能已經聽到或讀到了,很多關於在網路通過反向傳播來傳遞誤差的資訊。然後根據神經元的 w 和 b 對偏差貢獻的大小。也就是將誤差分配到每一個神經元上。 但這裡的誤差(error)是什麼意思呢?這個誤差的確切的定義又是什麼?答案是這些誤差是由每一層神經網路所貢獻的,而且某一層的誤差是後繼層誤差基礎上分攤的,網路中第 $l$ 層的誤差用 $\delta^l$ 來表示。
反向傳播是基於 4 個基本方程的,通過這些方程來計算誤差 $\delta^L$ 和損失函式,這裡將這 4 個方程一一列出 $$ \delta^{(L)} = \nabla_a C \odot \sigma^{\prime}(z^L) \tag{BP1} $$
$$ \delta^l = ((w^l)^T \delta^{l+1}) \odot \sigma^{\prime}(z^l) \tag{BP1} $$
$$ \frac{\partial C}{\partial b_{j}^l} = \delta_j^l \tag{BP3} $$
$$ \frac{\partial C}{\partial w_{jk}^l} = a_k^{l-1}\delta_j^l \tag{BP4} $$
關於如何解讀這個 4 個方程,隨後想用一期分享來說明。
python
class NeuralNetwork(object):
def __init__(self):
pass
def forward(self,x):
# 返回前向傳播的 Z 也就是 w 和 b 線性組合,輸入啟用函式前的值
# 返回啟用函式輸出值 A
# z_s , a_s
pass
def backward(self,y,z_s,a_s):
#返回前向傳播中學習引數的導數 dw db
pass
def train(self,x,y,batch_size=10,epochs=100,lr=0.001):
pass
我們都是神經網路學習過程,也就是訓練過程。主要分為兩個階段前向傳播和後向傳播
- 在前向傳播函式中,主要計算傳播的 Z 和 A,關於 Z 和 A 具體是什麼請參見前面表格
- 在反向傳播中計算可學習變數 w 和 b 的導數
python
def __init__(self,layers = [2 , 10, 1], activations=['sigmoid', 'sigmoid']):
assert(len(layers) == len(activations)+1)
self.layers = layers
self.activations = activations
self.weights = []
self.biases = []
for i in range(len(layers)-1):
self.weights.append(np.random.randn(layers[i+1], layers[i]))
self.biases.append(np.random.randn(layers[i+1], 1))
- layers 引數用於指定每一層神經元的個數
- activations 為每一層指定啟用函式,也就是$ \sigma(wx + b)$
來簡單讀解一下程式碼 assert(len(layers) == len(activations)+1)
python
for i in range(len(layers)-1):
self.weights.append(np.random.randn(layers[i+1], layers[i]))
self.biases.append(np.random.randn(layers[i+1], 1))
因為權重連線每一個層神經元的 w 和 b ,也就兩兩層之間的方程,上面程式碼是對
前向傳播
在前向傳播中,將輸入 X 輸入到 a_s 中,$z = wx + b$ 然後對輸出再計算 $a=\sigma(z)$,
python
def feedforward(self, x):
# 返回前向傳播的值
a = np.copy(x)
z_s = []
a_s = [a]
for i in range(len(self.weights)):
activation_function = self.getActivationFunction(self.activations[i])
z_s.append(self.weights[i].dot(a) + self.biases[i])
a = activation_function(z_s[-1])
a_s.append(a)
return (z_s, a_s)
這裡啟用函式,這個函式返回值是一個函式,在 python 用 lambda
來返回一個函式,這裡簡答留下一個伏筆,隨後會對其進行修改。
python
@staticmethod
def getActivationFunction(name):
if(name == 'sigmoid'):
return lambda x : np.exp(x)/(1+np.exp(x))
elif(name == 'linear'):
return lambda x : x
elif(name == 'relu'):
def relu(x):
y = np.copy(x)
y[y<0] = 0
return y
return relu
else:
print('Unknown activation function. linear is used')
return lambda x: x
```python
[@staticmethod] def getDerivitiveActivationFunction(name): if(name == 'sigmoid'): sig = lambda x : np.exp(x)/(1+np.exp(x)) return lambda x :sig(x)*(1-sig(x)) elif(name == 'linear'): return lambda x: 1 elif(name == 'relu'): def relu_diff(x): y = np.copy(x) y[y>=0] = 1 y[y<0] = 0 return y return relu_diff else: print('Unknown activation function. linear is used') return lambda x: 1 ```
反向傳播
這是本次分享重點 ```python def backpropagation(self,y, z_s, a_s): dw = [] # dC/dW db = [] # dC/dB deltas = [None] * len(self.weights) # delta = dC/dZ 計算每一層的誤差 # 最後一層誤差
deltas[-1] = ((y-a_s[-1])*(self.getDerivitiveActivationFunction(self.activations[-1]))(z_s[-1]))
# 反向傳播
for i in reversed(range(len(deltas)-1)):
deltas[i] = self.weights[i+1].T.dot(deltas[i+1])*(self.getDerivitiveActivationFunction(self.activations[i])(z_s[i]))
#a= [print(d.shape) for d in deltas]
batch_size = y.shape[1]
db = [d.dot(np.ones((batch_size,1)))/float(batch_size) for d in deltas]
dw = [d.dot(a_s[i].T)/float(batch_size) for i,d in enumerate(deltas)]
# 返回權重(weight)矩陣 and 偏置向量(biases)
return dw, db
```
首先計算最後一層誤差根據 BP1 等式可以得到下面的式子
deltas[-1] = ((y-a_s[-1])*(self.getDerivitiveActivationFunction(self.activations[-1]))(z_s[-1]))
$$ \delta^L = (a^L - y)\sigma(z^L) $$
接下來基於上一層的 $\delta^{l+1}$ 誤差來計算當前層 $\delta^l$ $$ \delta^l = ((w^l)^T \delta^{l+1}) \odot \sigma^{\prime}(z^l) \tag{BP1} $$
python
batch_size = y.shape[1]
db = [d.dot(np.ones((batch_size,1)))/float(batch_size) for d in deltas]
dw = [d.dot(a_s[i].T)/float(batch_size) for i,d in enumerate(deltas)]
$$ \frac{\partial C}{\partial b_{j}^l} = \delta_j^l \tag{BP3} $$
$$ \frac{\partial C}{\partial w_{jk}^l} = a_k^{l-1}\delta_j^l \tag{BP4} $$
開始訓練
```python def train(self, x, y, batch_size=10, epochs=100, lr = 0.01):
update weights and biases based on the output
for e in range(epochs):
i=0
while(i<len(y)):
x_batch = x[i:i+batch_size]
y_batch = y[i:i+batch_size]
i = i+batch_size
z_s, a_s = self.feedforward(x_batch)
dw, db = self.backpropagation(y_batch, z_s, a_s)
self.weights = [w+lr*dweight for w,dweight in zip(self.weights, dw)]
self.biases = [w+lr*dbias for w,dbias in zip(self.biases, db)]
# print("loss = {}".format(np.linalg.norm(a_s[-1]-y_batch) ))
```
- 深入學習入門系列(1)—線性迴歸(上)
- 深入淺出 Pytorch 系列 — 優化器的選擇(2) Adagrad 、MSProp 和 adam
- 目標檢測系列(6)—YOLOv4 讓每個人都能體驗深度學習給我們帶來的快樂(中)
- 自己寫—YOLOv3(1)—網路架構
- 目標檢測系列(5)—讓每個人都能體驗深度學習給我們帶來的快樂—YOLOv4(上)
- 日更 SLAM 一邊理論一邊實踐第三天—提取特徵(3)
- 目標檢測系列(4)— 在目標檢測中不能不聊的 YOLOv3
- 目標檢測系列(3)—又一個 one stage 的目標檢測模型—SSD(下)
- 雖然有些過時,但如果自己動手實現一遍 YOLOv1 勢必會有所收穫(1) 網路架構
- 雖然有些過時,但如果自己動手實現一遍 YOLOv1 勢必會有所收穫(2) 損失函式
- 做一件有趣的事,嘗試學著自己動手寫一個深度學習框架(1)—深入反向傳播
- 難捨的機器學習(1)—最小二乘法和極大似然來解線性迴歸
- 目標檢測系列(2)—又一個 one stage 的目標檢測模型—SSD(上)
- 目標檢測系列—深度解讀 YOLOv1 (1)
- 從頭用 numpy 來寫一個識別 MNIST 手寫資料的神經網路
- 推薦系統看到此可以稍作休息(1)—基於協同過濾構建推薦引擎
- 動手寫一個深度學習框架(1) 用 pytorch 搭建神經網路
- 深度學習從理論到實踐—logistic 迴歸(1)
- 從放棄到重啟 C —new 和 delete 關鍵字
- 用 Python 從頭開始實現一個全連線的神經網路