1 介紹

本文內容主要包含神經網路(Neural Network)的原理以及程式碼實現。我看了很多神經網路的實現方法,但全部都是結構固定,擴充套件性差。本文將實現一種可以熱拔插的程式碼來實現神經網路,

無需修改程式碼,只需修改引數即可搭建不同結構的神經網路。

2 原理及程式碼

看了很多文章,博主覺得講原理時配上程式碼,食用更佳。

誤差反向傳播

是 NN 的難點所在,本文會以一種步驟更為清晰的求導方式,帶你理解誤差反向傳播過程。讀別人的程式碼總是很煎熬,所以我會盡可能在程式碼中加入詳細註釋,讓渴望學習的你易於理解。

2。1 正向傳播

正向傳播很簡單,不再詳細介紹,正向傳播的公式如下:

《機器學習》之 一文讀懂神經網路原理及程式碼實現

上式是三層結構的一個前向傳播公式,相信大家都能看懂,

\sigma

()

為啟用函式,在本文中表示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的導數,即:

\frac{\partial L}{\partial W_{i}}

\frac{\partial L}{\partial b_{i}}

鏈式求導之前,先梳理一下要求導的目標位置。

1> 要求導的目標 W 及 b 都包含在

y_{pre}

中;

2> 每層的權重比如

W_{2}

b_{2}

都位於該層的輸出

z_{2}

a_{2}

中,所以求導需先對 z 或者 a 求導,再對 W 及 b 求導;

3> 前層的輸出比如

z_{1}

a_{1}

都位於後層的輸出

z_{2}

a_{2}

中,所以對前層權重求導時需先對後層的輸出求導;

4> 要對每層的 W 和 b 求導,只需求得每層輸出 z 的導數 dz 即可,因為 z=W*X+b, 所以dW=Xdz, db=dz,有了每層的dz,dW與db就很好求了。

所以我們的鏈式求導, 先不考慮 W 與 b,避免式子複雜。我們只針對每層的未啟用的輸出 z 進行求導得到 dz,最後再根據每層的 dz 求 dW 與 db。

先對最後一層的輸出

z_{3}

進行求導得到

dz_{3}

, 最後一層求導比較特殊,也是最麻煩的一層。

《機器學習》之 一文讀懂神經網路原理及程式碼實現

《機器學習》之 一文讀懂神經網路原理及程式碼實現

L

z_{3}

的求導跟邏輯迴歸一樣,很容易得到:

dz_{3}=(y_{pre}-y_{true})* \sigma^{

,可以自己手推一下,也可以參照下文中邏輯迴歸求導過程,不熟悉可以先記住結果,繼續往下看。

得到了

dz_{3}

,前層的梯度

dz_{2}

dz_{1}

都可以根據

dz_{3}

迭代得到:

《機器學習》之 一文讀懂神經網路原理及程式碼實現

《機器學習》之 一文讀懂神經網路原理及程式碼實現

《機器學習》之 一文讀懂神經網路原理及程式碼實現

同理,得到

迭代格式:

《機器學習》之 一文讀懂神經網路原理及程式碼實現

《機器學習》之 一文讀懂神經網路原理及程式碼實現

即前一層輸出 z 的導數

dz_{2}

等於後一層的權重

W_{3}

,乘上sigmoid函式的導數,再乘後一層 z的導數

dz_{3}

即可。

如此迭代可得到所有層的dz,最後再根據每層的

dz

計算每層的

dW

db

即可, 如下:

《機器學習》之 一文讀懂神經網路原理及程式碼實現

《機器學習》之 一文讀懂神經網路原理及程式碼實現

《機器學習》之 一文讀懂神經網路原理及程式碼實現

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

):

print

“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

print

accuracy_score

pre

y_test

))

此程式碼寫完之後,除錯了很久。幾束青絲又不經意間飄落,程式猿太難了~

碼字不易,覺得有用就點個贊吧~萬分感謝

寫在最後

如果你對機器學習感興趣,歡迎關注我的機器學習專欄~