接觸深度學習

有一段時間了,之前也寫了幾篇筆記,但是零零散散的,並不系統,而且自己的代碼也改動的比較多,和之前的筆記內容已經搭配不上了;所以借這一次機會進行一次系統的歸納,也算是對自己進行一次backward,讓自己能夠更儘可能的將所學的內容完整的轉述出來,而不是只有自己明了於心,卻無法言傳,算是一次錘鍊吧

所有的計算基本上都是基於Numpy來實現的,參考了pytorch

的一些結構,當然速度和功能是比不上的。不過重點也不在這裡,主要還是在於弄清楚原理和功能,以及如何實現,整個網路的結構基本上如下:

在整個網路的forward過程中,一開始傳進來的是訓練樣本的數據,之後的每一次的輸出都是作為下一層的輸入;backward也是如此,開始傳入的是誤差項(或者說損失關於輸出的梯度),然後層層計算回傳

不同的層(Layer)有不同的功能,通過將不同的層搭配到一起組成各種網路,就可以實現不同的功能,既然如此,那麼主要還是在於各個層具體的forward和backward實現

在此之前,先簡單的介紹一些神經網路的基本單元——神經元:

不對,不是這個,這個是神經細胞,但是和這個很像:

它接受n個輸入和一個偏置(或閾值)b,並進行加權求和,然後通過激活函數g運算並輸出,如果用公式來表示,令輸入為向量x,其各自權重為向量w:

oldsymbol{x}=left[ egin{array}{llll}{x_{1}} & {x_{2}} & {cdots} & {x_{n}}end{array}
ight]

oldsymbol{w}=left[ egin{array}{c}{w_{1}} \ {w_{2}} \ {vdots} \ {w_{n}}end{array}
ight]

所以該神經元的輸出就是:

a=g(oldsymbol x oldsymbol w+b)

激活函數用很多種,以後會一一講到,這裡先說一下sigmoid激活函數

圖片來自網路

其函數圖像如上,公式如下:

y=frac{1}{1+e^{-x}}

直觀的理解,它可以將輸入壓縮到(0,1)的範圍內,可以作為一個二分類器的實現

單個神經元的功能也非常簡單,只能解決線性可分的問題,什麼是線性可分?簡單的說就是一條線就能分開的:

圖片來自網路

而對於線性不可分的問題,哪怕再簡單也無能為力:

圖片來自網路

神經元的內容就說這麼多了,更多更詳細的的內容可以去百度,下面首先是實現最基本的一個層:線性層(Linear),也叫全連接層

如上圖所示,基本的線性層就是多個神經元組合在一起,每個神經元有各自的權重,但是共享輸入,將上面的公式該一下,第一個、第二個、……第m個神經元的分別表示為:

w_{1}=left[ egin{array}{c}{w_{1,1}} \ {w_{2,1}} \ {vdots} \ {w_{n, 1}}end{array}
ight]w_{2}=left[ egin{array}{c}{w_{1,2}} \ {w_{2,2}} \ {vdots} \ {w_{n, 2}}end{array}
ight]w_{m}=left[ egin{array}{c}{w_{1, m}} \ {w_{2, m}} \ {vdots} \ {w_{n, m}}end{array}
ight]

相應的,每個神經元的輸出分別為:

a_{1}=gleft(x w_{1}
ight)

a_{2}=gleft(x w_{2}
ight)

……

a_{m}=gleft(x w_{m}
ight)

上述過程可以用代碼可以寫成:

for i in range(m):
a[i] = g(sum(x * w[i]))

但是,顯然不能這麼干,不僅是計算速度的問題,表示起來也很複雜,所以要將上述公式向量化,將全部神經元的權重組成一個矩陣:

W=left[ egin{array}{llll}{w_{1}} & {w_{2}} & {cdots} & {w_{m}}end{array}
ight]

注意,這裡的W是一個n行m列的矩陣,裡面的每一個元素都是一個含有n個元素的列向量,我寫成這樣不過是方便一點,看的時候要分清楚

然後整個層的輸出也可以用一個向量表示:

oldsymbol{a}=gleft(oldsymbol x W
ight) =left[ egin{array}{llll}{a_{1}} & {a_{2}} & {cdots} & {a_{m}}end{array}
ight]

以上,就是線性層(全連接層)的forward過程了,不過在貼代碼之前,還有一個小問題需要解決

那就是激活函數的選擇,從前面的圖中可以看到,sigmoid在離原點很遠的地方斜率變得非常小,這樣在反向傳播的過程中容易造成梯度消失的問題,而且不經過原點,均值不為0,並且指數計算比較耗時

再就是有時候需要根據不同的情況選擇不同的激活函數,甚至不要激活函數,所以與其把各種激活函數寫到線性層的裡面,比較方便的一個做法是將其分離出來作為單獨的一個層,如下:

激活層里沒並沒有神經元,所以也就沒有權重,它按照特定的演算法(如前面的sigmoid函數)對輸入進行處理然後輸出,輸入輸出的大小是一致的,下面貼代碼:

class Layer(metaclass=ABCMeta):

作為所有層的基類,如果自定義新的層應該從此類繼承並重寫下面兩個方法

@abstractmethod
def forward(self, *args):
pass

@abstractmethod
def backward(self, *args):
pass

class Linear(Layer):
def __init__(self, shape, **kwargs):

shape = (in_size, out_size)

self.W = Parameter(shape)
self.b = Parameter(shape[-1])

def forward(self, x):
return np.dot(x, self.W.data) + self.b.data

首先定義了一個抽象的Layer類作為所有層的基類,其中的forward和backward方法是這個Layer要實現的主要功能,需要繼承類重現

可以看到線性層的forward實現非常簡單,其中self.W和self.b是它的權重和偏置,Parameter是一個包含了numpy數組的自定義類,之所以不直接用numpy數組而多封裝一層是因為參照的pytorch的做法,後續會有用

那一行代碼基本上就是對上面公式的照搬,不多解釋了,然後是sigmoid實現

class Sigmoid(Layer):
def forward(self, x):
self.y = 1 / (1 + np.exp(-x))
return self.y

也是對照著公式實現的,不多解釋;另外之所以把計算結果保存下來是因為在backward中會用到

之前說到過激活函數不只這一個,下面在介紹2個:ReLU和Softmax

圖片來自網路

上圖是Relu的函數圖像,下面是實現代碼

class Relu(Layer):
def forward(self, x):
self.x = x
return np.maximum(0, x)

這裡保存輸入也是為了backward而留的,實際功能就是numpy一個函數就實現了,其作用正如函數圖像那樣,x小於0就返回0,否則返回x自身。非常簡單,計算也非常快,並且不會有梯度消失的問題

然後是softmax,說它是激活函數有點不恰當,因為它一般都是在最後一層,也就是輸出層在才用的,它的作用是實現多分類:

class Softmax(Layer):
def forward(self, x):
v = np.exp(x - x.max(axis=-1, keepdims=True)) # X.shape = batch, N_classes
self.y = v / v.sum(axis=-1, keepdims=True)
return self.y

公式如下:

softmaxleft(x_{i}
ight)=frac{e^{x_{i}}}{sum_{j} e^{x_{j}}}

它接受一個向量作為輸入,輸出是全部元素和為一的向量,關於它的詳細講解,會在下一篇文章中進行,最後,先把完整的代碼貼出來

leeroee/CNN?

github.com
圖標

推薦閱讀:
相关文章