《機器學習》之 一文讀懂神經網路原理及程式碼實現
1 介紹
本文內容主要包含神經網路(Neural Network)的原理以及程式碼實現。我看了很多神經網路的實現方法,但全部都是結構固定,擴充套件性差。本文將實現一種可以熱拔插的程式碼來實現神經網路,
無需修改程式碼,只需修改引數即可搭建不同結構的神經網路。
2 原理及程式碼
看了很多文章,博主覺得講原理時配上程式碼,食用更佳。
誤差反向傳播
是 NN 的難點所在,本文會以一種步驟更為清晰的求導方式,帶你理解誤差反向傳播過程。讀別人的程式碼總是很煎熬,所以我會盡可能在程式碼中加入詳細註釋,讓渴望學習的你易於理解。
2。1 正向傳播
正向傳播很簡單,不再詳細介紹,正向傳播的公式如下:
上式是三層結構的一個前向傳播公式,相信大家都能看懂,
為啟用函式,在本文中表示sigmoid。
本文所有的公式中的變數都使用矩陣形式,避免太多的累加符號,不易理解。
為了求導時容易理解,下面再定義每層神經元未啟用時的輸出為 z , 啟用後為 a 。即:
在正向傳播之前,需要先隨機初始化權重 W 及偏置 b 。下面程式碼中的 layer_list 是要定義的各層神經元個數,比如 [32, 16, 8, 1] 表示1個輸入層3個隱藏層的 NN,第一個數32為輸入 X 的維度(特徵個數),透過 layer_list 即可定義不同的NN的結構。
NN 各層權重可根據各層神經元的個數來定義對應的 shape。程式碼如下:
def
__init__
(
self
,
layer_list
=
[],
lr
=
0。1
,
epochs
=
100
):
self
。
lr
=
lr
#學習率
self
。
layer_list
=
layer_list
#每層神經元個數
self
。
epochs
=
epochs
#迭代次數
def
weight_bias_init
(
self
):
self
。
W
=
{}
#權重字典,key是層號,value是對應權重矩陣
self
。
b
=
{}
#偏置字典,key是層號,value是對應偏置矩陣
self
。
layer_num
=
len
(
self
。
layer_list
)
-
1
#網路層數(權重矩陣的個數,輸入層無權重)
for
idx
in
range
(
self
。
layer_num
):
#為每層layer初始化W與b矩陣,每層 W 的shape為(前一層神經元個數,後一層神經元個數)
self
。
W
[
idx
]
=
np
。
random
。
randn
(
self
。
layer_list
[
idx
],
\
self
。
layer_list
[
idx
+
1
])
*
0。01
#正態分佈
self
。
b
[
idx
]
=
np
。
random
。
randn
(
self
。
layer_list
[
idx
+
1
])
有了矩陣及偏置後,對輸入 X 進行累乘即可得到輸出output,程式碼如下:
def
forward
(
self
,
X
,
y
):
self
。
X
=
X
#將輸入X儲存為類的屬性,可供其他函式使用
self
。
y
=
np
。
array
(
y
)
。
reshape
(
-
1
,
1
)
#更改y的shape,防止運算出錯
#記錄各層的z與a,反向傳播時會用到
self
。
z
=
{}
#字典,記錄每層啟用前的輸出(z = W*X + b)
self
。
a
=
{}
#字典,記錄每層啟用後的輸出(a = sigmoid(z))
input
=
self
。
X
for
idx
in
range
(
self
。
layer_num
):
#迴圈向前累乘
self
。
z
[
idx
]
=
np
。
dot
(
input
,
self
。
W
[
idx
])
+
self
。
b
[
idx
]
#z = W*X + b
self
。
a
[
idx
]
=
self
。
sigmoid
(
self
。
z
[
idx
])
#a = sigmoid(z)
input
=
self
。
a
[
idx
]
#更新輸入
self
。
output
=
self
。
a
[
self
。
layer_num
-
1
]
#記錄最後一層輸出
self
。
loss
=
-
np
。
mean
(
self
。
y
*
np
。
log
(
self
。
output
)
+
\
(
1
-
self
。
y
)
*
np
。
log
(
1
-
self
。
output
))
#對數損失
此時,就實現了對輸入 X 的正向傳播,並且記錄了各層的輸出 z 與 a ,很簡單吧!
2。2 誤差反傳
對於二分類的對數損失函式: (此程式碼針對二分類任務設計)
需要求L對各個網路層的權重W及偏置b的導數,即:
與
。
鏈式求導之前,先梳理一下要求導的目標位置。
1> 要求導的目標 W 及 b 都包含在
中;
2> 每層的權重比如
與
都位於該層的輸出
與
中,所以求導需先對 z 或者 a 求導,再對 W 及 b 求導;
3> 前層的輸出比如
與
都位於後層的輸出
與
中,所以對前層權重求導時需先對後層的輸出求導;
4> 要對每層的 W 和 b 求導,只需求得每層輸出 z 的導數 dz 即可,因為 z=W*X+b, 所以dW=Xdz, db=dz,有了每層的dz,dW與db就很好求了。
所以我們的鏈式求導, 先不考慮 W 與 b,避免式子複雜。我們只針對每層的未啟用的輸出 z 進行求導得到 dz,最後再根據每層的 dz 求 dW 與 db。
先對最後一層的輸出
進行求導得到
, 最後一層求導比較特殊,也是最麻煩的一層。
對
的求導跟邏輯迴歸一樣,很容易得到:
,可以自己手推一下,也可以參照下文中邏輯迴歸求導過程,不熟悉可以先記住結果,繼續往下看。
得到了
,前層的梯度
與
都可以根據
迭代得到:
同理,得到
迭代格式:
即前一層輸出 z 的導數
等於後一層的權重
,乘上sigmoid函式的導數,再乘後一層 z的導數
即可。
如此迭代可得到所有層的dz,最後再根據每層的
計算每層的
與
即可, 如下:
Tips: 誤差反向傳播是不是也很簡單,不要先想著對W與b進行求導,它們巢狀的太深,求導複雜。換一個角度,只對每層未啟用時的輸出z進行求導,再根據dz對W與b進行求導,這個問題就變得清晰了。
下面是誤差反向傳播的程式碼,透過迭代公式求每一層的梯度:
# sigmoid的一階導數
def
Dsigmoid
(
self
,
x
):
return
self
。
sigmoid
(
x
)
*
(
1
-
self
。
sigmoid
(
x
))
# 反向傳播
def
backward
(
self
):
#跟權重儲存方式一樣,使用字典儲存,key為對應的層號
self
。
dz
=
{}
#對每層z的求導
self
。
dW
=
{}
#對每層W的求導
self
。
db
=
{}
#對每層b的求導
idx
=
self
。
layer_num
-
1
#從後往前求導
while
(
idx
>=
0
):
if
(
idx
==
self
。
layer_num
-
1
):
#最後一層的求導比較特殊,套最後一層求導的公式dz3
self
。
dz
[
idx
]
=
(
self
。
output
-
self
。
y
)
*
self
。
Dsigmoid
(
self
。
z
[
idx
])
#元素乘
else
:
#前層都可根據最後一層的dz迭代得到,套迭代公式dzi
self
。
dz
[
idx
]
=
np
。
dot
(
self
。
dz
[
idx
+
1
],
self
。
W
[
idx
+
1
]
。
T
)
\
*
self
。
Dsigmoid
(
self
。
z
[
idx
])
if
(
idx
==
0
):
#idx為0時,即到達第一層時,前層輸入a[idx-1]是X
self
。
dW
[
idx
]
=
np
。
dot
(
self
。
X
。
T
,
self
。
dz
[
idx
])
/
len
(
self
。
X
)
#梯度需除上總樣本數
else
:
#idx不為0時迭代計算即可
self
。
dW
[
idx
]
=
np
。
dot
(
self
。
a
[
idx
-
1
]
。
T
,
self
。
dz
[
idx
])
/
len
(
self
。
X
)
self
。
db
[
idx
]
=
np
。
sum
(
self
。
dz
[
idx
],
axis
=
0
)
/
len
(
self
。
X
)
#db=dz, 但是需要所有維度取平均
idx
-=
1
#跳前一層
# 求完所有層的梯度後,更新即可
for
idx
in
range
(
self
。
layer_num
):
self
。
W
[
idx
]
-=
self
。
lr
*
self
。
dW
[
idx
]
self
。
b
[
idx
]
-=
self
。
lr
*
self
。
db
[
idx
]
到此,前向傳播與反向傳播的函式都已經實現了,最後用一個train函式對所有功能函式進行封裝,即可實現完整的神經網路程式碼。
3 完整程式碼
此程式碼是我在學習了好朋友的文章之後,擴充套件的更靈活的版本,覺得有難度的話可以先看看他的文章。
下面是我的完整版程式碼:
import
numpy
as
np
class
NN
(
object
):
def
__init__
(
self
,
layer_list
=
[],
lr
=
0。1
,
epochs
=
100
):
self
。
lr
=
lr
#學習率
self
。
layer_list
=
layer_list
#每層神經元個數
self
。
epochs
=
epochs
#迭代次數
#權重與偏執初始化
def
weight_bias_init
(
self
):
self
。
W
=
{}
#權重字典,key是層號,value是權重矩陣
self
。
b
=
{}
#偏置字典,key是層號,value是怕偏置矩陣
self
。
layer_num
=
len
(
self
。
layer_list
)
-
1
#網路層數
#為每層layer初始化W與b矩陣
for
idx
in
range
(
self
。
layer_num
):
self
。
W
[
idx
]
=
np
。
random
。
randn
(
self
。
layer_list
[
idx
],
\
self
。
layer_list
[
idx
+
1
])
*
0。01
#正態分佈
self
。
b
[
idx
]
=
np
。
random
。
randn
(
self
。
layer_list
[
idx
+
1
])
# sigmoid函式
def
sigmoid
(
self
,
x
):
return
1。0
/
(
1
+
np
。
exp
(
-
x
))
# sigmoid的一階導數
def
Dsigmoid
(
self
,
x
):
return
self
。
sigmoid
(
x
)
*
(
1
-
self
。
sigmoid
(
x
))
# 前向傳播
def
forward
(
self
,
X
,
y
):
self
。
X
,
self
。
y
=
X
,
np
。
array
(
y
)
。
reshape
(
-
1
,
1
)
self
。
z
=
{}
#記錄每層啟用前的輸出(z = W*X + b)
self
。
a
=
{}
#記錄每層啟用後的輸出(a = sigmoid(z))
#迴圈向前累乘
input
=
self
。
X
for
idx
in
range
(
self
。
layer_num
):
self
。
z
[
idx
]
=
np
。
dot
(
input
,
self
。
W
[
idx
])
+
self
。
b
[
idx
]
self
。
a
[
idx
]
=
self
。
sigmoid
(
self
。
z
[
idx
])
input
=
self
。
a
[
idx
]
#更新輸入
self
。
output
=
self
。
a
[
self
。
layer_num
-
1
]
#最後一層輸出
self
。
loss
=
-
np
。
mean
(
self
。
y
*
np
。
log
(
self
。
output
)
+
\
(
1
-
self
。
y
)
*
np
。
log
(
1
-
self
。
output
))
#對數損失
#誤差反向傳播
def
backward
(
self
):
#跟權重儲存方式一樣,使用字典儲存,key為對應的層號
self
。
dz
=
{}
#對每層z的求導
self
。
dW
=
{}
#對每層W的求導
self
。
db
=
{}
#對每層b的求導
idx
=
self
。
layer_num
-
1
#從後往前求導
while
(
idx
>=
0
):
if
(
idx
==
self
。
layer_num
-
1
):
#最後一層的求導比較特殊,套最後一層求導的公式dz3
self
。
dz
[
idx
]
=
(
self
。
output
-
self
。
y
)
*
self
。
Dsigmoid
(
self
。
z
[
idx
])
#元素乘
else
:
#前層都可根據最後一層的dz迭代得到,套迭代公式dzi
self
。
dz
[
idx
]
=
np
。
dot
(
self
。
dz
[
idx
+
1
],
self
。
W
[
idx
+
1
]
。
T
)
\
*
self
。
Dsigmoid
(
self
。
z
[
idx
])
if
(
idx
==
0
):
#idx為0時,即到達第一層時,前層輸入a[idx-1]是X
self
。
dW
[
idx
]
=
np
。
dot
(
self
。
X
。
T
,
self
。
dz
[
idx
])
/
len
(
self
。
X
)
#梯度需除上總樣本數
else
:
#idx不為0時迭代計算即可
self
。
dW
[
idx
]
=
np
。
dot
(
self
。
a
[
idx
-
1
]
。
T
,
self
。
dz
[
idx
])
/
len
(
self
。
X
)
self
。
db
[
idx
]
=
np
。
sum
(
self
。
dz
[
idx
],
axis
=
0
)
/
len
(
self
。
X
)
#db=dz, 但是需要所有維度取平均
idx
-=
1
#跳前一層
# 求完所有層的梯度後,更新即可
for
idx
in
range
(
self
。
layer_num
):
self
。
W
[
idx
]
-=
self
。
lr
*
self
。
dW
[
idx
]
self
。
b
[
idx
]
-=
self
。
lr
*
self
。
db
[
idx
]
#迭代訓練
def
train
(
self
,
X
,
y
):
self
。
weight_bias_init
()
for
i
in
range
(
self
。
epochs
):
self
。
forward
(
X
,
y
)
self
。
backward
()
#每10輪列印一次loss
if
(
i
%
10
==
0
):
(
“Epoch {}: loss={}”
。
format
(
i
//
10
+
1
,
self
。
loss
))
#預測機率輸出
def
predict
(
self
,
X_test
):
input
=
X_test
for
idx
in
range
(
self
。
layer_num
):
z
=
np
。
dot
(
input
,
self
。
W
[
idx
])
+
self
。
b
[
idx
]
a
=
self
。
sigmoid
(
z
)
input
=
a
return
a
使用測試:
from
sklearn。datasets
import
load_iris
from
sklearn。model_selection
import
train_test_split
from
sklearn。metrics
import
accuracy_score
X
,
y
=
load_iris
(
return_X_y
=
True
)
X
,
y
=
X
[:
100
],
y
[:
100
]
X_train
,
X_test
,
y_train
,
y_test
=
train_test_split
(
X
,
y
,
test_size
=
0。4
)
layer_list
=
[
4
,
1
]
#自定義神經網路結構
model
=
NN
(
layer_list
,
lr
=
0。1
,
epochs
=
100
)
model
。
train
(
X_train
,
y_train
)
pre
=
model
。
predict
(
X_test
)
pre
=
[
1
if
x
>=
0。5
else
0
for
x
in
pre
]
(
accuracy_score
(
pre
,
y_test
))
此程式碼寫完之後,除錯了很久。幾束青絲又不經意間飄落,程式猿太難了~
碼字不易,覺得有用就點個贊吧~萬分感謝
寫在最後
如果你對機器學習感興趣,歡迎關注我的機器學習專欄~