接觸深度學習有一段時間了,之前也寫了幾篇筆記,但是零零散散的,並不系統,而且自己的代碼也改動的比較多,和之前的筆記內容已經搭配不上了;所以借這一次機會進行一次系統的歸納,也算是對自己進行一次backward,讓自己能夠更儘可能的將所學的內容完整的轉述出來,而不是只有自己明了於心,卻無法言傳,算是一次錘鍊吧
所有的計算基本上都是基於Numpy來實現的,參考了pytorch的一些結構,當然速度和功能是比不上的。不過重點也不在這裡,主要還是在於弄清楚原理和功能,以及如何實現,整個網路的結構基本上如下:
在整個網路的forward過程中,一開始傳進來的是訓練樣本的數據,之後的每一次的輸出都是作為下一層的輸入;backward也是如此,開始傳入的是誤差項(或者說損失關於輸出的梯度),然後層層計算回傳
不同的層(Layer)有不同的功能,通過將不同的層搭配到一起組成各種網路,就可以實現不同的功能,既然如此,那麼主要還是在於各個層具體的forward和backward實現
在此之前,先簡單的介紹一些神經網路的基本單元——神經元:
不對,不是這個,這個是神經細胞,但是和這個很像:
它接受n個輸入和一個偏置(或閾值)b,並進行加權求和,然後通過激活函數g運算並輸出,如果用公式來表示,令輸入為向量x,其各自權重為向量w:
所以該神經元的輸出就是:
激活函數用很多種,以後會一一講到,這裡先說一下sigmoid激活函數
其函數圖像如上,公式如下:
直觀的理解,它可以將輸入壓縮到(0,1)的範圍內,可以作為一個二分類器的實現
單個神經元的功能也非常簡單,只能解決線性可分的問題,什麼是線性可分?簡單的說就是一條線就能分開的:
而對於線性不可分的問題,哪怕再簡單也無能為力:
神經元的內容就說這麼多了,更多更詳細的的內容可以去百度,下面首先是實現最基本的一個層:線性層(Linear),也叫全連接層
如上圖所示,基本的線性層就是多個神經元組合在一起,每個神經元有各自的權重,但是共享輸入,將上面的公式該一下,第一個、第二個、……第m個神經元的分別表示為:
, ,
相應的,每個神經元的輸出分別為:
……
上述過程可以用代碼可以寫成:
for i in range(m): a[i] = g(sum(x * w[i]))
但是,顯然不能這麼干,不僅是計算速度的問題,表示起來也很複雜,所以要將上述公式向量化,將全部神經元的權重組成一個矩陣:
注意,這裡的W是一個n行m列的矩陣,裡面的每一個元素都是一個含有n個元素的列向量,我寫成這樣不過是方便一點,看的時候要分清楚
然後整個層的輸出也可以用一個向量表示:
以上,就是線性層(全連接層)的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
公式如下:
它接受一個向量作為輸入,輸出是全部元素和為一的向量,關於它的詳細講解,會在下一篇文章中進行,最後,先把完整的代碼貼出來