深度學習入門|第七章 卷積神經網絡(三)
前言
本文為學習《深度學習入門》一書的學習筆記,詳情請閱讀原著
五、CNN的實現
搭建進行手寫數字識別的 CNN。這裏要實現如圖 7-23 所示的 CNN。
圖 7-23 簡單 CNN 的網絡構成
如圖 7-23 所示,網絡的構成是“Convolution - ReLU - Pooling -Affine - ReLU - Affine - Softmax”,我們將它實現為名為 SimpleConvNet
的類。
首先來看一下 SimpleConvNet
的初始化(__init__
),取下面這些參數。
參數
input_dim
——輸入數據的維度:(通道,高,長)
conv_param
——卷積層的超參數(字典)。字典的關鍵字如下:
filter_num
——濾波器的數量
filter_size
——濾波器的大小
stride
——步幅
pad
——填充
hidden_size
——隱藏層(全連接)的神經元數量
output_size
——輸出層(全連接)的神經元數量
weitght_int_std
——初始化時權重的標準差
這裏,卷積層的超參數通過名為 conv_param
的字典傳入。我們設想它會像{‘filter_num‘:30,‘filter_size‘:5, ‘pad‘:0, ‘stride‘:1}
這樣,保存必要的超參數值。
SimpleConvNet
的初始化的實現稍長,我們分成 3 部分來說明,首先是初始化的最開始部分。
class SimpleConvNet: def __init__(self, input_dim=(1, 28, 28), conv_param={‘filter_num‘:30, ‘filter_size‘:5,‘pad‘:0, ‘stride‘:1}, hidden_size=100, output_size=10, weight_init_std=0.01): filter_num = conv_param[‘filter_num‘] filter_size = conv_param[‘filter_size‘] filter_pad = conv_param[‘pad‘] filter_stride = conv_param[‘stride‘] input_size = input_dim[1] conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1 pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))
這裏將由初始化參數傳入的卷積層的超參數從字典中取了出來(以方便後面使用),然後,計算卷積層的輸出大小。接下來是權重參數的初始化部分。
self.params = {} self.params[‘W1‘] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size) self.params[‘b1‘] = np.zeros(filter_num) self.params[‘W2‘] = weight_init_std * np.random.randn(pool_output_size, hidden_size) self.params[‘b2‘] = np.zeros(hidden_size) self.params[‘W3‘] = weight_init_std * np.random.randn(hidden_size, output_size) self.params[‘b3‘] = np.zeros(output_size)
學習所需的參數是第 1 層的卷積層和剩余兩個全連接層的權重和偏置。將這些參數保存在實例變量的 params
字典中。將第 1 層的卷積層的權重設為關鍵字W1
,偏置設為關鍵字 b1
。同樣,分別用關鍵字 W2
、b2
和關鍵字 W3
、b3
來保存第 2 個和第 3 個全連接層的權重和偏置。
最後,生成必要的層。
self.layers = OrderedDict() self.layers[‘Conv1‘] = Convolution(self.params[‘W1‘], self.params[‘b1‘], conv_param[‘stride‘], conv_param[‘pad‘]) self.layers[‘Relu1‘] = Relu() self.layers[‘Pool1‘] = Pooling(pool_h=2, pool_w=2, stride=2) self.layers[‘Affine1‘] = Affine(self.params[‘W2‘], self.params[‘b2‘]) self.layers[‘Relu2‘] = Relu() self.layers[‘Affine2‘] = Affine(self.params[‘W3‘], self.params[‘b3‘]) self.last_layer = softmaxwithloss()
從最前面開始按順序向有序字典(OrderedDict
)的 layers
中添加層。只有最後的 SoftmaxWithLoss
層被添加到別的變量 lastLayer
中。
以上就是 SimpleConvNet
的初始化中進行的處理。像這樣初始化後,進行推理的 predict
方法和求損失函數值的 loss
方法就可以像下面這樣實現。
def predict(self, x): for layer in self.layers.values(): x = layer.forward(x) return x def loss(self, x, t): y = self.predict(x) return self.lastLayer.forward(y, t)
這裏,參數 x
是輸入數據,t
是教師標簽。用於推理的 predict
方法從頭開始依次調用已添加的層,並將結果傳遞給下一層。在求損失函數的 loss
方法中,除了使用 predict
方法進行的 forward
處理之外,還會繼續進行forward
處理,直到到達最後的 SoftmaxWithLoss
層。
接下來是基於誤差反向傳播法求梯度的代碼實現。
def gradient(self, x, t): # forward self.loss(x, t) # backward dout = 1 dout = self.lastLayer.backward(dout) layers = list(self.layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) # 設定 grads = {} grads[‘W1‘] = self.layers[‘Conv1‘].dW grads[‘b1‘] = self.layers[‘Conv1‘].db grads[‘W2‘] = self.layers[‘Affine1‘].dW grads[‘b2‘] = self.layers[‘Affine1‘].db grads[‘W3‘] = self.layers[‘Affine2‘].dW grads[‘b3‘] = self.layers[‘Affine2‘].db return grads
參數的梯度通過誤差反向傳播法(反向傳播)求出,通過把正向傳播和反向傳播組裝在一起來完成。因為已經在各層正確實現了正向傳播和反向傳播的功能,所以這裏只需要以合適的順序調用即可。最後,把各個權重參數的梯度保存到grads
字典中。這就是 SimpleConvNet
的實現。
現在,使用這個 SimpleConvNet
學習 MNIST 數據集。用於學習的代碼與 4.5 節中介紹的代碼基本相同,因此這裏不再羅列(源代碼在ch07/train_convnet.py
中)。
如果使用 MNIST 數據集訓練 SimpleConvNet
,則訓練數據的識別率為 99.82%,測試數據的識別率為 98.96%(每次學習的識別精度都會發生一些誤差)。測試數據的識別率大約為 99%,就小型網絡來說,這是一個非常高的識別率。下一章,我們會通過進一步疊加層來加深網絡,實現測試數據的識別率超過 99% 的網絡。
如上所述,卷積層和池化層是圖像識別中必備的模塊。CNN 可以有效讀取圖像中的某種特性,在手寫數字識別中,還可以實現高精度的識別。
六、CNN 的可視化
本節將通過卷積層的可視化,探索 CNN 中到底進行了什麽處理。
1、第一層權重的可視化
剛才我們對 MNIST 數據集進行了簡單的 CNN 學習。當時,第 1 層的卷積層的權重的形狀是 (30, 1, 5, 5),即 30 個大小為 5 × 5、通道為 1 的濾波器。濾波器大小是 5 × 5、通道數是 1,意味著濾波器可以可視化為 1 通道的灰度圖像。現在,我們將卷積層(第 1 層)的濾波器顯示為圖像。這裏,我們來比較一下學習前和學習後的權重,結果如圖 7-24 所示(源代碼在 ch07/visualize_filter.py
中)。
圖 7-24 中,學習前的濾波器是隨機進行初始化的,所以在黑白的濃淡上沒有規律可循,但學習後的濾波器變成了有規律的圖像。我們發現,通過學習,濾波器被更新成了有規律的濾波器,比如從白到黑漸變的濾波器、含有塊狀區域(稱為 blob)的濾波器等。
圖 7-24 學習前和學習後的第 1 層的卷積層的權重:雖然權重的元素是實數,但是在圖像的顯示上,統一將最小值顯示為黑色(0),最大值顯示為白色(255)
如果要問圖 7-24 中右邊的有規律的濾波器在“觀察”什麽,答案就是它在觀察邊緣(顏色變化的分界線)和斑塊(局部的塊狀區域)等。比如,左半部分為白色、右半部分為黑色的濾波器的情況下,如圖 7-25 所示,會對垂直方向上的邊緣有響應。
圖 7-25 對水平方向上和垂直方向上的邊緣有響應的濾波器:輸出圖像 1 中,垂直方向的邊緣上出現白色像素,輸出圖像 2 中,水平方向的邊緣上出現很多白色像素
圖 7-25 中顯示了選擇兩個學習完的濾波器對輸入圖像進行卷積處理時的結果。我們發現“濾波器 1”對垂直方向上的邊緣有響應,“濾波器 2”對水平方向上的邊緣有響應。
由此可知,卷積層的濾波器會提取邊緣或斑塊等原始信息。而剛才實現的 CNN 會將這些原始信息傳遞給後面的層。
2、基於分層結構的信息提取
上面的結果是針對第 1 層的卷積層得出的。第 1 層的卷積層中提取了邊緣或斑塊等“低級”信息,那麽在堆疊了多層的 CNN 中,各層中又會提取什麽樣的信息呢?根據深度學習的可視化相關的研究 [17][18],隨著層次加深,提取的信息(正確地講,是反映強烈的神經元)也越來越抽象。
圖 7-26 中展示了進行一般物體識別(車或狗等)的 8 層 CNN。這個網絡結構的名稱是下一節要介紹的 AlexNet。AlexNet 網絡結構堆疊了多層卷積層和池化層,最後經過全連接層輸出結果。圖 7-26 的方塊表示的是中間數據,對於這些中間數據,會連續應用卷積運算。
圖 7-26 CNN 的卷積層中提取的信息。第 1 層的神經元對邊緣或斑塊有響應,第 3 層對紋理有響應,第 5 層對物體部件有響應,最後的全連接層對物體的類別(狗或車)有響應
如圖 7-26 所示,如果堆疊了多層卷積層,則隨著層次加深,提取的信息也愈加復雜、抽象,這是深度學習中很有意思的一個地方。最開始的層對簡單的邊緣有響應,接下來的層對紋理有響應,再後面的層對更加復雜的物體部件有響應。也就是說,隨著層次加深,神經元從簡單的形狀向“高級”信息變化。換句話說,就像我們理解東西的“含義”一樣,響應的對象在逐漸變化。
七、具有代表性的CNN
介紹其中特別重要的兩個網絡,一個是在 1998 年首次被提出的 CNN 元祖 LeNet[20],另一個是在深度學習受到關註的 2012 年被提出的 AlexNet[21]。
1、LeNet
LeNet 在 1998 年被提出,是進行手寫數字識別的網絡。如圖 7-27 所示,它有連續的卷積層和池化層(正確地講,是只“抽選元素”的子采樣層),最後經全連接層輸出結果。
圖 7-27 LeNet 的網絡結構
和“現在的 CNN”相比,LeNet 有幾個不同點。第一個不同點在於激活函數。LeNet 中使用 sigmoid 函數,而現在的 CNN 中主要使用 ReLU 函數。此外,原始的 LeNet 中使用子采樣(subsampling)縮小中間數據的大小,而現在的 CNN 中 Max 池化是主流。
綜上,LeNet 與現在的 CNN 雖然有些許不同,但差別並不是那麽大。
2、AlexNet
在 LeNet 問世 20 多年後,AlexNet 被發布出來。AlexNet 是引發深度學習熱潮的導火線,不過它的網絡結構和 LeNet 基本上沒有什麽不同,如圖 7-28 所示。
圖 7-28 AlexNet
AlexNet 疊有多個卷積層和池化層,最後經由全連接層輸出結果。雖然結構上 AlexNet 和 LeNet 沒有大的不同,但有以下幾點差異。
- 激活函數使用 ReLU。
- 使用進行局部正規化的 LRN(Local Response Normalization)層。
- 使用 Dropout(6.4.3 節)。
八、小結
本章所學的內容
- CNN 在此前的全連接層的網絡中新增了卷積層和池化層。
- 使用
im2col
函數可以簡單、高效地實現卷積層和池化層。 - 通過 CNN 的可視化,可知隨著層次變深,提取的信息愈加高級。
- LeNet 和 AlexNet 是 CNN 的代表性網絡。
- 在深度學習的發展中,大數據和 GPU 做出了很大的貢獻。
深度學習入門|第七章 卷積神經網絡(三)