文章目錄
1. 前言
我之前寫過一篇關於BP神經網絡原理的文章,主要內容包括:BP神經網絡的反向傳播原理,以及簡單的代碼實現。本文和《BP 神經網絡入門:從原理到應用》一文一脈相承,因此,我希望讀者務必先看看這篇文章(鏈接在這裏:BP 神經網絡入門:從原理到應用,
好多讀者都說,之前那篇BP神經網絡的文章,公式太多,閱讀體驗不好,所以本文我將盡量減少公式,加入更多的圖解和通俗解釋。
本文主要包括以下內容:
-
卷積神經網絡結構
-
前向傳播原理
-
反向傳播原理
-
代碼實現
-
模型測試
-
簡單的優化設計方法;優化設計方法有很多,本文不做過多介紹,如果有必要,我會後面會寫個有關優化設計的課程。
2. 全連接BP神經網絡的缺點
像全連接BP神經網絡一樣,卷積神經網絡,也有一定的生物學基礎(雖然,有時候確實感覺它的生物學解釋有些牽強)。
我在BP 神經網絡入門:從原理到應用一文,實現了一個,識別圖片是不是貓的程序。在這個程序裏,我們把輸入的圖片“展平”了,“展平”是什麼意思呢?我們都知道,計算機存儲圖片,實際是存儲了一個 的數組(W,H,D分別表示寬,高和維數,彩色圖片包含RGB三維),如下圖所示,“展平”就是,把這個數組變成一列,然後,輸入神經網絡進行訓練:
這種方法,在圖片尺寸較小的時候,還是可以接受的;但是,當圖片尺寸比較大的時候,由於圖像的像素點很多,神經網絡的 層與層之間 的連接數量,會爆炸式增長,大大降低網絡的性能。
除此之外,把圖片數據展開成一列,破壞了圖片的空間信息,試想,如果把上圖中的貓圖片變成一列,再把這一列數據,以圖片的形式展示給你,你還能看出這表示一隻貓嗎?並且,有相關研究表明,人類大腦,在理解圖片信息的過程中,並不是同時觀察整個圖片,而是更傾向於,觀察部分特徵,然後根據特徵匹配、組合,得出整圖信息。以下圖爲例:
在這張圖片中,你只能看到,貓的鼻子,1隻眼睛,2只耳朵和嘴,雖然沒有尾巴,爪子等其它信息,但是,你依然可以判斷,這裏有一隻貓。所以說,人在理解圖像信息的過程中,更專注於,局部特徵,以及這些特徵之間的組合。
換句話說,在BP全連接神經網絡中,隱含層每一個神經元,都對輸入圖片 每個像素點 做出反應。這種機制包含了太多冗餘連接。爲了減少這些冗餘,只需要每個隱含神經元,對圖片的一小部分區域,做出反應就好了!卷積神經網絡,正是基於這種想法而實現的。
3. 卷積神經網絡基本單元
3.1 卷積(Convolution)
如果,你學過信號處理,或者有相關專業的數學基礎,可能需要注意一下,這裏的“卷積”,和信號處理裏的卷積,不太一樣,雖然,在數學表達式形式上有一點類似。
在卷積神經網絡中,“卷積”更像是一個,特徵提取算子。什麼是特徵提取算子呢?簡單來說就是,提取圖片紋理、邊緣等特徵信息的濾波器。下面,舉個簡單的例子,解釋一下 邊緣 特徵提取算子是怎麼工作的:
比如有一張貓圖片,人類在理解這張圖片的時候,可能觀察到圓圓的眼睛,可愛的耳朵,於是,判斷這是一隻貓。但是,機器怎麼處理這個問題呢?傳統的計算機視覺方法,通常設計一些算子(特徵提取濾波器),來找到比如眼睛的邊界,耳朵的邊界,等信息,然後綜合這些特徵,得出結論——這是一隻貓。
具體是怎麼做的呢:
如上圖所示,假設,貓的上眼皮部分(框出部分),這部分的數據,展開後如圖中數組所示**(這裏,爲了敘述簡便,忽略了rgb三維通道,把圖像當成一個二維數組[類比灰度圖片]),我們使用一個算子,來檢測橫向邊沿**——這個算子,讓第一行的值減去第三行的值,得出結果。具體計算過程如下,比如,原圖左上角數據,經過這個算子:
然後,算子向右滑動一個像素,得到第二個輸出:533,依次向右滑動,最終得到第一行輸出;
然後,向下滑動一個像素,到達第二行;在第二行,從左向右滑動,得到第二行輸出……
動圖演示過程如下(圖片來自csdn):
從輸出結果可以看出,在上眼皮處顏色較深,而眼皮上方和下方,無論是眼皮上方的毛髮,還是眼皮下方的眼白,顏色都很淺,因此,這樣做之後,如果濾波結果中有大值出現,就意味着,這裏出現了橫向邊沿。不斷在原圖上滑動這個算子,我們就可以,檢測到圖像中所有的橫向邊沿。傳統的機器視覺方法,就是設計更多更復雜的算子,檢測更多的特徵,最後,組合這些特徵,得出判斷結果。
卷積神經網絡,做了類似的事情,所謂卷積,就是把一個算子在原圖上不斷滑動,得出濾波結果——這個結果,我們叫做“特徵圖”(Feature Map),這些算子被稱爲“卷積核”(Convolution Kernel)。不同的是,我們不必人工設計這些算子,而是使用隨機初始化,來得到很多卷積核(算子),然後通過反向傳播,優化這些卷積核,以期望得到更好的識別結果。
3.2 填充(Padding)和步長(Stride)
你可能注意到了,剛剛,我們“滑動”卷積核之後,輸出變小了;具體來說,貓眼睛部分數據,尺寸是,但是,經過一個的卷積核之後,變成了,這是因爲卷積核有尺寸,如下圖所示,當卷積核(橙色)滑動到邊界時,就無法向右繼續滑動了:
可想而知,如果經過很多層卷積的話,輸出尺寸會變的很小,同時圖像邊緣信息,會迅速流失,這對模型的性能,有着不可忽視的影響。爲了減少卷積操作導致的,邊緣信息丟失,我們需要進行填充(Padding),即在原圖周圍,添加一圈值爲“0”的像素點(zero padding),這樣的話,輸出維度就和輸入維度一致了。
還有一個很重要的因素是,步長(Stride),即卷積核每次滑動幾個像素。前面,我們默認,卷積核每次滑動一個像素,其實也可以,每次滑動兩個像素。其中,每次滑動的像素數,稱爲“步長”。
你可能會有疑問,如果步長大於 1 ,那不也會造成輸出尺寸變小嗎?是的!但是這種情況下,不會“故意”丟失邊緣信息,即使有信息丟失,也是在整張圖片上,比較“溫和”地,捨棄了一些無關緊要的信息(反向傳播調節卷積核參數,使之“智能”地捨棄無關信息)。但是,不能頻繁使用步長爲2。因爲,如果輸出尺寸變得,過小的話,即使卷積核參數優化的再好,也會必可避免地丟失大量信息!所以,你可以看到,在應用卷積網絡的時候,通常使用步長爲 1 的卷積核。
**卷積核大小()**也可以變化,比如 、 等,此時,需要根據卷積核大小,來調節填充尺寸(Padding size)。一般來說,卷積核尺寸取奇數(因爲我們希望卷積核有一箇中心,便於處理輸出)。卷積核尺寸爲奇數時,填充尺寸可以根據以下公式確定:
其中, 表示卷積核大小。
如果用 表示步長, 表示圖片寬度, 表示圖片高度,那麼輸出尺寸可以表示爲:
下面提供一些動圖幫助大家理解(圖片來自CSDN):
類別 | 動圖 |
---|---|
no padding, s=1, f=3 | no padding, s=2, f=3 |
with padding, s=1, f=3 | with padding, s=2, f=3 |
3.3 完整的卷積過程
上面的卷積過程,沒有考慮,彩色圖片有rgb三維通道(Channel),如果考慮rgb通道,那麼,每個通道,都需要一個卷積核:
當輸入有多個通道時,我們的卷積核也需要,有同樣數量的通道。以上圖爲例,輸入有RGB三個通道,我們的就卷積核,也有三個通道,只不過計算的時候,卷積核的每個通道,在對應通道滑動(卷積核最前面的通道在輸入圖片的紅色通道滑動,卷積核中間的通道在輸入圖片的綠色通道滑動,卷積核最後面的通道在輸入圖片的藍色通道滑動),三個通道的計算結果相加得到輸出。注意,輸出只有一個通道,因爲這裏我們只用了一個卷積核(只不過這個卷積核有三個通道)。用下面的動圖加深理解(圖片來自網絡,這裏我們用了 2 個卷積核,每個卷積核 3 個通道,輸出有 2 個通道):
三維展示如下(如果視頻無法加載可以點擊此鏈接查看):
3.4 池化(Pooling)
池化(Pooling),有的地方也稱匯聚,實際是一個下采樣(Down-sample)過程。由於輸入的圖片尺寸可能比較大,這時候,我們需要下采樣,減小圖片尺寸。池化層,可以減小模型規模,提高運算速度,同時,提高所提取特徵的魯棒性。池化操作也有核大小 和步長 參數,參數意義和卷積相同。
本文主要介紹最大池化(Max Pooling)和平均池化(Average Pooling)。
所謂最大池化,就是對於大小的池化核,選取原圖中數值最大的那個保留下來。比如,池化核 大小,步長爲2的池化過程如下(左邊是池化前,右邊是池化後),對於每個池化區域都取最大值:
最大池化最爲常用,並且一般都取的池化核代大小且步長爲2。
平均池化是取每個區域的均值(平均池化現在很少使用):
3.5 激活函數
本文主要介紹 2 種激活函數,分別是和函數,函數公式如下:
函數圖如下:
補充說明
引入激活函數的目的是,在模型中引入非線性。如果沒有激活函數,那麼,無論你的神經網絡有多少層,最終都是一個線性映射,單純的線性映射,無法解決線性不可分問題。引入非線性可以讓模型解決線性不可分問題。
一般來說,在神經網絡的中間層,更加建議使用函數,兩個原因:
- 函數計算簡單,可以加快模型速度;
- 由於反向傳播過程中,需要計算偏導數,通過求導可以得到,函數導數的最大值爲0.25,如果使用函數的話,每一層的反向傳播,都會使梯度最少變爲原來的四分之一,當層數比較多的時候可能會造成梯度消失,從而模型無法收斂。
4. 卷積神經網絡的前向傳播過程
本節主要介紹,典型的卷積神經網絡模型結構。
典型的卷積網絡結構,如上圖所示(注意觀察圖中顏色對應關係)。首先輸入圖像,然後卷積。卷積過程採用n個卷積核,每個卷積核都有三個通道(卷積核通道數,和輸入圖片的通道數目相同),輸出結果尺寸爲,這裏的輸出有n個通道,所以,下一層卷積,每個卷積核都要有n個通道;重複卷積-池化過程;模型的最後一層或兩層,一般爲全連接結構,輸出最終結果。全連接結構部分和BP神經網絡一樣。最後,我們需要設計合適的損失函數,反向傳播就是通過最小化損失函數,來優化網絡參數,使模型達到預期結果。
我們把每次卷積之後,得到輸出稱爲“特徵圖(Feature map)”。
我們定義以下符號,表示卷積網絡結構(這裏我儘可能模仿Tensorflow的API):
- 卷積 conv2d(input, filter, strides, padding)
- input:卷積輸入(,指的是輸入的通道數)
- filter:卷積核W ()
- strides:步長,一般爲
- padding:指明padding的算法,‘SAME’或者‘VALID’
- 激活 relu(conv)
- conv:卷積結果
- 偏置相加 add(conv, b)
- conv:卷積的激活輸出
- b:偏置係數
- 池化 max_pool(value, ksize, strides, padding)
- value:卷積輸出
- ksize:池化尺寸,一般
- strides:步長
- padding:指明padding的算法,‘SAME’或者‘VALID’
5. 卷積神經網絡的反向傳播過程
終於講到反向傳播了。現在大多數深度學習框架,都幫我們做好了誤差反傳的工作。大部分工程師,僅僅需要構建前向傳播過程,就可以了。但是,對於研究人員,有必要了解一下誤差反傳的原理。
在BP神經網絡中,我們已經介紹過,全連接神經網絡的反向傳播過程。很多教程,在介紹卷積網絡反向傳播的時候,總是說,類比全連接神經網絡,然後一筆帶過。其實,卷積神經網絡的誤差反傳過程,要複雜得多。原因有以下幾點:
-
卷積神經網絡的卷積核,每次滑動只與部分輸入相連,具有局部映射的特點,在誤差反傳的時候,需要確定卷積核的局部連接方式;
-
卷積神經網絡的池化過程,丟失了大量信息,誤差反傳,需要恢復這些丟失的信息;
-
……
總之,卷積神經網絡的誤差反傳過程,很複雜。之前,在寫BP神經網絡那篇文章的時候,推導的公式過多,導致很多讀者反應,文章晦澀難懂;所以,這次我儘可能用通俗的語言來表述。
本小節的符號定義,和BP神經網絡一文相同,如果出現了新的符號,會在文中對應位置說明。
5.1 卷積層的誤差反傳
假設損失函數爲, 我們希望求解,損失函數對參數 W 和偏置 b 的梯度。
首先,來看損失函數對卷積核 W 的梯度:
層的卷積輸出爲:
其中,表示上一層池化的輸出;和表示卷積核尺寸; 和 表示輸入的長和寬。
卷積層反向傳播的基礎,依舊是鏈式法則,所以,我們必須搞清楚,權重W的局部連接方式。
考慮前向傳播過程:
可以發現,由於卷積核在輸入的特徵圖上滑動,所以,特徵圖的所有像素都參與了卷積核運算,與此對應,輸出特徵圖的每個像素,都和卷積核 W 直接相連。
損失函數對參數W的梯度,根據鏈式法則:
其中,Z表示沒有激活的輸出,表示層的誤差。現在我們來求解部分:
綜上,
同理,可得:
下面來看看如何求解:
也就是說我們需要求解,所以我們需要知道在層的一個像素值如何影響卷積輸出:
上圖說明,一個像素點,通過卷積運算,會影響輸出的一個區域(輸入特徵圖的藍色像素點,影響了輸出特徵圖的藍色虛線框出的部分)。所以反向傳播就是要找到,卷積核滑動過程中,一個像素點,如何通過 W 影響輸出,然後逐步應用鏈式法則即可。
5.2 池化層的誤差反傳
池化層沒有參數需要學習,但是,由於池化層的下采樣操作損失了大量信息,因此,在反向傳播過程中,我們需要恢復這些信息。
對於最大池化而言,我們需要知道在池化之前最大元素的位置:
然後,在反向傳播的時候將對應值填入最大位置,這可以用池化後誤差乘以M得到,這樣一個數就可以恢復爲的矩陣,對池化層誤差矩陣的每個元素都應用這樣的上採樣,我們就可以恢復維度信息,從而誤差可以順利反傳。
對於平均池化而言,我們保存的是均值信息:
反傳過程中依舊是用池化層誤差乘以dZ,將一個數恢復爲的矩陣即,可恢復維度信息。
6. 代碼實現
本小節,給出了Numpy實現卷積網絡的核心代碼。另外,因爲卷積神經網絡的計算過程很複雜,因此,使用自己實現的核心代碼效率很低。所以,完整模型的訓練採用TensorFlow庫來實現。
6.1 Numpy實現
6.1.1 卷積層前向傳播
首先實現一個0填充的函數:
def zero_pad(X, pad):
"""零填充。
Args:
X: python numpy array of shape (m, n_H, n_W, n_C) representing a batch of m images
pad: integer, amount of padding around each image on vertical and horizontal dimensions
Returns:
X_pad -- padded image of shape (m, n_H + 2*pad, n_W + 2*pad, n_C)
"""
X_pad = np.pad(X, ((0,0),(pad,pad),(pad,pad),(0,0)), 'constant')
return X_pad
接下來實現一個單步卷積,即考慮全圖一個切片的卷積:
def conv_single_step(a_slice_prev, W, b):
""""
Args:
a_slice_prev: slice of input data of shape (f, f, n_C_prev)
W: Weight parameters contained in a window - matrix of shape (f, f, n_C_prev)
b: Bias parameters contained in a window - matrix of shape (1, 1, 1)
Returns:
Z: a float value.
"""
s = a_slice_prev * W
Z = np.sum(s)
Z = Z + b
return Z
完整的一步卷積,即卷積核在輸入特徵圖滑動:
def conv_forward(A_prev, W, b, hparameters):
"""前向傳播
Args:
A_prev: output activations of the previous layer, numpy array of shape (m, n_H_prev, n_W_prev, n_C_prev)
W: Weights, numpy array of shape (f, f, n_C_prev, n_C)
b: Biases, numpy array of shape (1, 1, 1, n_C)
hparameters: a python dictionary containing "stride" and "pad"
Returns:
Z: conv output, numpy array of shape (m, n_H, n_W, n_C)
cache: cache of values needed for the conv_backward() function
"""
(m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape
(f, f, n_C_prev, n_C) = W.shape
stride = hparameters['stride']
pad = hparameters['pad']
n_H = int((n_H_prev+2*pad-f)/stride) + 1
n_W = int((n_W_prev+2*pad-f)/stride) + 1
Z = np.zeros((m,n_H,n_W,n_C))
A_prev_pad = zero_pad(A_prev, pad)
for i in range(m):
a_prev_pad = A_prev_pad[i,:,:,:]
for h in range(n_H):
for w in range(n_W):
for c in range(n_C):
vert_start = h * stride
vert_end = vert_start+f
horiz_start = w * stride
horiz_end = horiz_start+f
a_slice_prev = a_prev_pad[vert_start:vert_end,horiz_start:horiz_end, :]
Z[i, h, w, c] = conv_single_step(a_slice_prev, W[:,:,:,c], b[:,:,:,c])
cache = (A_prev, W, b, hparameters)
return Z, cache
6.1.2 卷積層反向傳播
def conv_backward(dZ, cache):
"""卷積層反向傳播
Args:
dZ: gradient of the cost with respect to the output of the conv layer (Z), numpy array of shape (m, n_H, n_W, n_C)
cache: cache of values needed for the conv_backward(), output of conv_forward()
Returns:
dA_prev: gradient of the cost with respect to the input of the conv layer (A_prev), numpy array of shape (m, n_H_prev, n_W_prev, n_C_prev)
dW: gradient of the cost with respect to the weights of the conv layer (W)
numpy array of shape (f, f, n_C_prev, n_C)
db: gradient of the cost with respect to the biases of the conv layer (b)
numpy array of shape (1, 1, 1, n_C)
"""
(A_prev, W, b, hparameters) = cache
(m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape
(f, f, n_C_prev, n_C) = W.shape
stride = hparameters['stride']
pad = hparameters['pad']
(m, n_H, n_W, n_C) = dZ.shape
dA_prev = np.zeros(A_prev.shape)
dW = np.zeros(W.shape)
db = np.zeros(b.shape)
A_prev_pad = zero_pad(A_prev, pad)
dA_prev_pad = zero_pad(dA_prev, pad)
for i in range(m):
a_prev_pad = A_prev_pad[i]
da_prev_pad = dA_prev_pad[i]
for h in range(n_H):
for w in range(n_W):
for c in range(n_C):
vert_start = h * stride
vert_end = vert_start + f
horiz_start = w * stride
horiz_end = horiz_start + f
a_slice = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :]
da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:, :, :, c] * dZ[i, h, w, c]
dW[:,:,:,c] += a_slice * dZ[i, h, w, c]
db[:,:,:,c] += dZ[i, h, w, c]
dA_prev[i, :, :, :] = da_prev_pad[pad:-pad, pad:-pad, :]
return dA_prev, dW, db
6.1.3 池化前向傳播
池化前向傳播:
def pool_forward(A_prev, hparameters, mode = "max"):
"""
Args:
A_prev: Input data, numpy array of shape (m, n_H_prev, n_W_prev, n_C_prev)
hparameters: python dictionary containing "f" and "stride"
mode: "max" or "average"
Returns:
A: output of the pool layer, a numpy array of shape (m, n_H, n_W, n_C)
cache: cache used in the backward pass of the pooling layer, contains the input and hparameters
"""
(m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape
f = hparameters["f"]
stride = hparameters["stride"]
n_H = int(1 + (n_H_prev - f) / stride)
n_W = int(1 + (n_W_prev - f) / stride)
n_C = n_C_prev
A = np.zeros((m, n_H, n_W, n_C))
for i in range(m):
for h in range(n_H):
for w in range(n_W):
for c in range (n_C):
vert_start = h*stride
vert_end = vert_start+f
horiz_start = w*stride
horiz_end = horiz_start+f
a_prev_slice = A_prev[i, vert_start:vert_end, horiz_start:horiz_end, c]
if mode == "max":
A[i, h, w, c] = np.max(a_prev_slice)
elif mode == "average":
A[i, h, w, c] = np.mean(a_prev_slice)
cache = (A_prev, hparameters)
return A, cache
6.1.4 池化反向傳播
首先,對於最大池化,我們需要記錄前向傳播過程中最大值在哪個位置:
def create_mask_from_window(x):
"""
Args:
x: Array of shape (f, f)
Returns:
maskL: Array of the same shape as window, contains a True at the position corresponding to the max entry of x.
"""
mask = x == np.max(x)
return mask
對於平均池化,我們需要把誤差平均分到每個位置:
def distribute_value(dz, shape):
"""
Args:
dz: input scalar
shape: the shape (n_H, n_W) of the output matrix for which we want to distribute the value of dz
Returns:
a: Array of size (n_H, n_W) for which we distributed the value of dz
"""
(n_H, n_W) = shape
average = dz / (n_H * n_W)
a = np.ones((n_H, n_W)) * average
return a
接下來,我們實現完整的池化層反向傳播過程:
def pool_backward(dA, cache, mode = "max"):
"""
Args:
dA: gradient of cost with respect to the output of the pooling layer, same shape as A
cache: cache output from the forward pass of the pooling layer, contains the layer's input and hparameters
mode: the pooling mode you would like to use, defined as a string ("max" or "average")
Returns:
dA_prev: gradient of cost with respect to the input of the pooling layer, same shape as A_prev
"""
(A_prev, hparameters) = cache
stride = hparameters['stride']
f = hparameters['f']
m, n_H_prev, n_W_prev, n_C_prev = A_prev.shape
m, n_H, n_W, n_C = dA.shape
dA_prev = np.zeros(A_prev.shape)
for i in range(m):
a_prev = A_prev[i]
for h in range(n_H):
for w in range(n_W):
for c in range(n_C):
vert_start = h * stride
vert_end = vert_start + f
horiz_start = w * stride
horiz_end = horiz_start + f
if mode == "max":
a_prev_slice = a_prev[vert_start:vert_end, horiz_start:horiz_end, c]
mask = create_mask_from_window(a_prev_slice)
dA_prev[i, vert_start: vert_end, horiz_start: horiz_end, c] += np.multiply(mask, dA[i, h, w, c])
elif mode == "average":
da = dA[i, h, w, c]
shape = (f, f)
dA_prev[i, vert_start:vert_end, horiz_start:horiz_end, c] += distribute_value(da, shape)
return dA_prev
6.2 Tensorflow框架實現
由於自己實現的卷積網絡核心代碼,計算速度過慢(只能在CPU運行,且包含太多for循環,效率過低),所以最終決定用TensorFlow來完成一個測試模型。
我們選用TensorFlow實現一個CIFAR-10數據集分類的例子。CIFAR-10是一個用於圖片分類的數據集。此數據集由60000張圖片組成,包涵10類。其中50000張圖片用於訓練,10000張圖片用於測試。
本文不會具體介紹TensorFlow的詳細用法,如果需要,可以參考TensoFlow官網教程,或者gitchat也有人寫過一些入門文章。TensorFlow實現深度學習模型一般包涵以下步驟:
-
定義佔位符
-
構建模型的前向傳播過程
-
定義損失函數
-
指定優化方法
-
運行優化方法最小化損失函數
我使用jupyter notebook編程環境。首先,導入必須的包,導入數據文件:
import os
import tensorflow as tf
import numpy as np
import math
import timeit
import matplotlib.pyplot as plt
gpu_no = '0' # or '1'
os.environ["CUDA_VISIBLE_DEVICES"] = gpu_no
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
%matplotlib inline
def load_cifar10(num_training=49000, num_validation=1000, num_test=10000):
"""
導入CIFAR數據集。
"""
cifar10 = tf.keras.datasets.cifar10.load_data()
(X_train, y_train), (X_test, y_test) = cifar10
X_train = np.asarray(X_train, dtype=np.float32)
y_train = np.asarray(y_train, dtype=np.int32).flatten()
X_test = np.asarray(X_test, dtype=np.float32)
y_test = np.asarray(y_test, dtype=np.int32).flatten()
mask = range(num_training, num_training + num_validation)
X_val = X_train[mask]
y_val = y_train[mask]
mask = range(num_training)
X_train = X_train[mask]
y_train = y_train[mask]
mask = range(num_test)
X_test = X_test[mask]
y_test = y_test[mask]
mean_pixel = X_train.mean(axis=(0, 1, 2), keepdims=True)
std_pixel = X_train.std(axis=(0, 1, 2), keepdims=True)
X_train = (X_train - mean_pixel) / std_pixel
X_val = (X_val - mean_pixel) / std_pixel
X_test = (X_test - mean_pixel) / std_pixel
return X_train, y_train, X_val, y_val, X_test, y_test
X_train, y_train, X_val, y_val, X_test, y_test = load_cifar10()
class Dataset(object):
def __init__(self, X, y, batch_size, shuffle=False):
assert X.shape[0] == y.shape[0], 'Got different numbers of data and labels'
self.X, self.y = X, y
self.batch_size, self.shuffle = batch_size, shuffle
def __iter__(self):
N, B = self.X.shape[0], self.batch_size
idxs = np.arange(N)
if self.shuffle:
np.random.shuffle(idxs)
return iter((self.X[i:i+B], self.y[i:i+B]) for i in range(0, N, B))
train_dset = Dataset(X_train, y_train, batch_size=64, shuffle=True)
val_dset = Dataset(X_val, y_val, batch_size=64, shuffle=False)
test_dset = Dataset(X_test, y_test, batch_size=64)
構建計算圖(前向傳播過程):
def conv2d(x, scope, W_shape, b_shape, stddev = 5e-2, stride = [1, 1, 1, 1], lambda_ = 0.005):
"""
定義卷積層
"""
with tf.variable_scope(scope, reuse = tf.AUTO_REUSE) as scope:
initializer = tf.truncated_normal_initializer(stddev=stddev, dtype=tf.float32)
W = tf.get_variable('W', W_shape, initializer=initializer, dtype=tf.float32)
b = tf.get_variable('b', b_shape, initializer=tf.constant_initializer(0.0), dtype=tf.float32)
tf.add_to_collection("l2_loss", tf.nn.l2_loss(W)*lambda_)
conv = tf.nn.conv2d(x, W, stride, padding='SAME')
conv = tf.add(tf.nn.relu(conv), b)
return conv
def max_pool(x, ksize = [1, 2, 2,1], stride = [1, 2, 2, 1]):
"""
池化層
"""
out = tf.nn.max_pool(x, ksize, stride, padding='SAME')
return out
def fully_connect(x, scope, indim, outdim, stddev=5e-2):
"""
全連接層
"""
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE) as scope:
reshape = tf.reshape(x, [tf.shape(x)[0], -1])
initializer = tf.truncated_normal_initializer(stddev=stddev, dtype=tf.float32)
W = tf.get_variable('W', [indim, outdim], initializer=initializer, dtype=tf.float32)
b = tf.get_variable('b', [outdim], initializer=tf.constant_initializer(0.0), dtype=tf.float32)
tf.add_to_collection("l2_loss", tf.nn.l2_loss(W)*0.004)
fc_out = tf.nn.relu(tf.matmul(reshape, W) + b)
return fc_out
def build_model(images):
"""
構建完整模型
conv1->relu->pool->norm->conv2->pool->norm->fully1->relu->fully2->relu->softmax
"""
temp_conv = conv2d(images, "conv1", [5, 5, 3, 64], [64], stddev = 5e-2)
temp_pool = max_pool(temp_conv, [1, 3, 3, 1])
temp_norm = tf.nn.lrn(temp_pool, 4, bias=1.0, alpha=0.001/9, beta=0.75)
temp_conv = conv2d(temp_norm, "conv2", [5, 5, 64, 64], [64], stddev = 5e-2)
temp_pool = max_pool(temp_conv)
temp_norm = tf.nn.lrn(temp_pool, 4, bias=1.0, alpha=0.001/9, beta=0.75)
fc = fully_connect(temp_pool, 'fully1', 8*8*64, 256)
fc = fully_connect(fc, 'fully2', 256, 10)
out = tf.nn.softmax(fc)
return out
def loss(y_pred, y):
"""
損失函數
"""
labels = tf.reshape(tf.one_hot(tf.reshape(tf.cast(y, dtype=tf.int64),[1, -1]), 10), [-1, 10])
softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=y_pred,
labels=labels))
loss = tf.reduce_sum(tf.get_collection("l2_loss")) + softmax_loss
#loss = softmax_loss
return loss
def accuracy(y_pred, y):
"""
準確率計算
"""
y_pred = tf.argmax(y_pred, axis=1)
y = tf.reshape(tf.cast(y,tf.int64),[1, -1])
acc = tf.reduce_mean(tf.cast(tf.equal(y_pred,y), tf.float32))
return acc
def train(train_dst, sess, epochs=100):
"""
訓練圖
"""
train_accs = []
val_accs = []
#佔位符
images = tf.placeholder(tf.float32, [None, 32, 32, 3])
y_label = tf.placeholder(tf.float32, [None, 1])
#模型
model = build_model(images)
y_pred = model
#準確率
acc = accuracy(y_pred, y_label)
#損失函數
losses = loss(y_pred, y_label)
#優化器
with tf.variable_scope('opt', reuse = tf.AUTO_REUSE):
opt = tf.train.AdamOptimizer(learning_rate=0.0001, use_locking=True)
train_step = opt.minimize(losses)
#參數初始化
init = tf.global_variables_initializer()
sess.run(init)
for epoch in range(epochs):
#迭代訓練
print("****************epoch "+str(epoch))
l = 0
for (X, y) in train_dst:
y_pred_ = sess.run(model, feed_dict={images:X})
#print(y_pred.shape,type(y_pred))
l = sess.run(losses, feed_dict={images:X, y_label: y.reshape((-1, 1))} )
train_acc = sess.run(acc, feed_dict={images:X, y_label: y.reshape((-1, 1))})
train_accs.append(train_acc)
sess.run(train_step, feed_dict={images:X, y_label:y.reshape((-1, 1))})
print("loss: " +str(l))
if epoch%1==0:
print("train_acc: "+str(train_acc))
val_acc = 0
for i, (X_v, y_v) in enumerate(val_dset):
y_pred_val = sess.run(model,feed_dict={images: X_v})
val_acc += sess.run(acc, feed_dict={images: X_v, y_label: y_v.reshape((-1, 1))})
val_acc /= (i+1)
val_accs.append(val_acc)
print("val_acc: "+ str(val_acc))
#測試集
test_acc = 0
for i, (X_t, y_t) in enumerate(test_dset):
y_pred_test = sess.run(model, feed_dict={images: X_t})
test_acc += sess.run(acc, feed_dict={images: X_t, y_label:y_t.reshape((-1, 1))})
test_acc /= (i+1)
print("%%%%%%%%%%%%% Test accuracy")
print(test_acc)
return train_accs, val_accs, test_acc
運行計算圖:
train_acc, val_acc, test = [], [], 0.0
with tf.Session(config=config) as sess:
train_acc, val_acc, tes=train(train_dset, sess )
訓練過程中,模型在訓練集的準確率變化如下:
在測試集的準確率爲69.7%。
可以看出,模型表現還是可以的(我們僅僅迭代了100次),但是距離完美還有很大差距。如果想要更好的結果,需要設計更復雜的網絡結構,以及更加精細的調參過程。考慮到本文主要目的不是如何優化網絡,而是爲大家展示卷積網絡原理,所以沒有實現很複雜的網絡。
7. 一些優化方法
本部分內容主要是作者個人理解,僅僅是爲了給讀者說一下優化方法的概念,不必深究。如果需要深入瞭解,可以查閱相關論文。
常用的優化方法有正則化(Regularization),批規範化(Batch Normalization),動量法(Momentum),Adam梯度下降法等。這裏主要提一下正則化和規範化。
所謂正則化,就是在損失函數裏,考慮網絡參數的複雜度(事實上,在我們剛剛的TensorFlow模型中已經用到了正則化)。舉個簡單的例子,如果你想要擬合一條曲線(圖片來自網絡):
假設你有數據集(X, Y),如上圖藍色圈所示(共N個數據)。你希望對這些數據,擬合一條曲線,如果你把擬合公式定義爲:
損失函數定義爲:
經過梯度下降法之後,得到紅色的擬合曲線。這時候,曲線經過所有的樣本點,loss爲0,可以說,完美地達到了你的要求。但是,可達鴨眉頭一皺,發現事情並不簡單。
即使沒有機器學習基礎,只學過初等數學的人,也知道“過擬合”(Over fit)的概念。沒錯!這裏顯然過擬合了!這些參數過於“肆意妄爲”,以至於得到一個很不靠譜的模型。
正則化要做的,就是“約束”這些參數,防止模型過擬合。具體來說,加入正則化之後,損失函數變爲:
上述公式的最後一項被稱爲L2正則化項(可以看作是對模型參數複雜度的度量), 需要人工選取。用新的損失函數,我們可以得到綠色的擬合曲線,這條曲線顯然看起來更合理。
那麼,你可能會問,爲什麼正則化項的加入,會產生這麼神奇的效果呢?先來看看,我們那條“過擬合”的紅色曲線,這條曲線彎彎曲曲的,曲線各點的斜率,劇烈變化(斜率是導數):
在x相同的時候,w的值越大,斜率變化越複雜。因此,當加入正則化項之後,參數w趨向變小,從而擬合曲線的斜率變化趨於平緩,換言之,擬合曲線變得更加“平滑”,因此在很大程度上解決了過擬合問題。總結一下,正則化主要是,讓參數W趨向於小值,讓模型趨向於簡單。
在深度神經網絡中,正則化依舊是有用的。在深度神經網絡中,除了上面提到的優點,正則化還趨向於讓W參數趨於“分散”,即,不會讓 W 矩陣的某個元素的絕對值過大或過小,而是趨向於,讓W矩陣的每個元素,都對模型有差不多的貢獻,從而可以在一定程度上,防止模型過擬合。
下面,再來說一說批規範化(Batch Normalization)。批規範化,即每次輸入一批數據後,先對數據進行處理,使數據均值爲0,方差爲1。在卷積神經網絡中,批規範化一般用在激活函數之前(也可以在之後)。現在,一些論文研究了各種各樣的規範化,除了批規範化(Batch Normalization)還有層規範化(Layer Normalization),組規範化(Group Normalization)等。雖然目前對規範化的作用機理有各種解釋,但是從個人角度來看,大多數解釋都有些牽強。個人角度看,規範化的主要作用是,強行穩定各層特徵圖的分佈,使模型的訓練更加容易,並且,批規範化解決了模型對參數初始化過於敏感的問題。如下圖所示,藍色表示“等高線”(損失函數值相等的線),這些等高線的中心表示最低點(損失函數最小的地方)。規範化之前,你的特徵空間可能“又細又長”,梯度下降法的路徑,可能如下圖左所示,震盪幅度很大;規範化之後,由於各個方向的特徵比較均衡,因此梯度下降更加穩定。
關於深度神經網絡的優化方法還有很多,比如Adam變步長算法,學習率衰減算法以及各種巧妙的初始化方法等。目前來看,深度神經網絡的可解釋性還比較差,並且,神經網絡的損失函數是非凸的,非凸優化的求解難度很大,因爲可能存在無數個局部最優點,而通常求解全局最優的複雜度是指數級。目前有研究表明,深度神經網絡模型很複雜,因此存在足夠多的局部最優點,而這些局部最優點接近全局最優,並且,由於深度神經網絡的損失函數,維度很高,因此存在大量鞍點,所以總有一個梯度方向,能朝着一個合理的局部最優點前進。因此,只要模型結構設計地合理,訓練方法得當,我們總能得到一個可接受的結果。
總之,目前來看,深度神經網絡,黑箱的成分很大,不僅沒有足夠的數學理論支撐,而且,即使得到了一個優秀的模型,也很難說清楚模型爲什麼表現的好。所以,如果你讀論文比較多的話,就會發現,好多論文更像是得到好的實驗結果之後,再去嘗試爲模型添加理論解釋,而不是以理論突破來指導模型創新。
添加各種優化算法之後的誤差反傳更加繁瑣,感興趣的讀者可以自行推導(強烈不建議,我自己推過,還是很麻煩的),本文不再贅述。
8. 後記
卷積神經網絡,在圖像領域有着廣泛的應用,但是,目前可解釋性較差,就像一個“黑箱”一樣,雖然不斷進化,人們卻很難說清楚模型爲什麼表現地好。
誤差反傳是深度神經網絡的核心,也是最難推理的部分。雖然誤差反傳的基礎是鏈式法則,但是當模型變得複雜的時候,試圖推導出每一層的誤差反傳解析式極其困難。現在,大多數深度學習框架都用到了一種叫做“自動微分”(Automatic Differentiation)的技術,來進行誤差反傳的工作。對於大多數工程師而言,不需要關心誤差反傳的過程。自動微分的知識可以參見書籍Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation, Second Edition。
另外,關於深度學習框架,我也有一點想說。目前流行的深度學習框架,主推TensorFlow(Google)和PyTorch(Facebook)。TensorFlow大而全,但是對新手不是很友好,應該說,由於其使用靜態計算圖(static graph),先構建計算圖,再計算,和普通的編程思路有所不同,大部分新手在學習TensorFlow的時候,會懵逼好一陣子。PyTorch對新手友好,使用動態計算圖(dynamic graph),使用PyTorch編寫代碼和普通的python代碼幾乎沒有區別,並且和numpy無縫結合(主要指張量和numpy.ndarray可以相互轉換),特別方便。我的建議是,如果你之前沒有用過任何深度學習框架,推薦你從PyTorch入手,當然,如果你是老手的話,就根據自己的需要選擇吧。
好了,本次內容分享就到這裏了,謝謝大家。
參考資料:
[1] cs231n http://cs231n.stanford.edu/
[2] Coursera Deep learning https://www.coursera.org/specializations/deep-learning
[3] Deep Learning http://www.deeplearningbook.org/