1. 引入
卷積神經網絡(CNN)是一種專門用來處理具有網格結構數據的神經網絡.它屬于前饋神經網絡,它被定義為:至少在某一層用卷積代替了矩陣乘法的神經網絡.最常見的應用場景是圖像識別.
前篇我們自己動手,用Python實現了一個BP神經網絡,本篇我們在Keras框架之下實現卷積神經網絡(Keras框架詳見《深度學習_工具》篇).Keras幾乎是搭建CNN最簡單的工具了,然而原理并不簡單:除了基本的神經網絡中用的誤差函數,激活函數等概念以外(具體詳見《深度學習_BP神經網絡》),CNN還用到了卷積,池化,DropOut等方法.將在本文中逐一介紹.
2. 原理
1) 圖像識別
先來看看圖形學中的圖像識別是如何實現的.

我們拿到了原圖(圖上左),一般先將其轉換成灰度圖(圖上中).然后進行邊緣檢測,圖像處理中常使用計算梯度方法(判斷某像素與它相鄰像素的差值)檢測邊緣.在CNN中我們用卷積來檢測:先設計一個卷積核計算相鄰像素的差值,然后用ReLU(f(x)=max(0,x))激活函數將那些差值小的置為0,即識別為非邊緣(看到激活函數的厲害了吧,它把低于閾值的都扔掉了,這可是一般線性變換做不到的).于是得到了邊緣圖(圖上右).處理后的圖像是個稀疏的矩陣(多數點值都為0).
?為了簡化計算,我們希望將圖片縮小,但在縮放的過程中,細的邊緣線就會被弱化,甚至消失(圖下左).在CNN中用池化解決這一問題,如果把3*3個點縮放到1點,”最大池化”會將這9個點中最大的值,作為新點的值,于是無論強邊緣還是弱邊緣都被保留了下來(圖下右).
邊緣檢測只是圖像識別的第一步,之后還將識別一些更上層的特征,比如拐角,車輪,汽車的輪廓等等,它們都可由卷積實現.越往后的層越抽象,最終一個神經網絡模型建起來和圖像處理中的"金字塔"有類似的結構,雖然下層的每個點都是局部的,但是上層的點具有全局性.
2) 卷積
下面來看看卷積到底是什么,它與之前學習的全連接神經網絡有什么不同.
卷積定義是:通過兩個函數f 和g 生成第三個函數的一種數學算子.呵呵,很抽象啊,形象地說,處理圖像時,卷積就好像拿著一小塊濾鏡,把圖像從左到右,從上到下,一格一格地掃一遍,如果在濾鏡中看到想要的,就在下一層做個標記.看看下圖就明白了.

圖中是一個二維平面上的卷積運算,輸入是一個4x3的矩陣,卷積核(Kernel)是2x2的矩陣,輸出是一個3x2的矩陣(卷積不都是2維的).這就是卷積——一種特殊的線性運算,考慮以下幾種情況:
如果上圖中的w,x,y,z都為0.25,則該卷積實現了均值運算(圖片的模糊處理)
如果上圖中w+x+y+z=1,則該卷積實現了實現了加權平均.
這個運算是不是有點眼熟,如果把Input中各點排著一列,把Kernel看成權值參數,則它是一個神經網絡的連接.輸入層有12個元素,隱藏層1有6個元素(先不管其它層)

如圖所示,左則是卷積層,右則是全連接的神經網絡,全連接時,共有126=72個連接(輸入輸出),72種權值;而卷積層只有24個連接(核大小*輸出),4個權值w,x,y,z,分別用四種顏色標出.此處,引出了兩個概念:
共享權值(參數共享):共享權值就是多個連接使用同一權值,卷積神經網絡中共享的權值就是卷積核的內容,這樣不但減小了學習難度.還帶來的”平移等變”的性質,比如集體合影中每張臉都可以使用相同的卷積核(一組權值)識別出來,無論它在圖中的什么位置,這樣學習一張臉后,就能對每張臉應用相同的處理了.此技術多用于同一狀態(tài)重復出現的情況下.(若圖中只有一張臉,臉相關的卷積核共享作用就不大了)
全連接,局部連接與卷積:全連接就是上一層的每個點都與下一層的每個點連接,每個連接都有其自已的權值;局部連接是只有部分點相互連續(xù);卷積是在局部連續(xù)的基礎上又共享了權值.
卷積核可以是指定的,也可以是用梯度向前推出來的,可以是聚類算出來的(無監(jiān)督學習),還可以先取一小塊訓練,然后用這小塊訓練的結果定義卷積的核.這取決于我們設計的不同算法.
由此可見,卷積層是兩層之間的連接方法,與全連接相比,它大大簡化了運算的復雜度,還節(jié)省了存儲空間.之所以能這樣簡化是因為圖像中距離越近的點關系越大(序列處理也同理:離得越近關系越大).
3) 池化
池化是使用某一位置的相鄰輸出的總體統(tǒng)計特征來代替網絡在該位置的輸出,和卷積差不多,它也是層到層之間的運算,經過池化處理,層中節(jié)點可能不變,也可能變?。ǔS盟挡蓸?,以減少計算量).
最大池化就是將相鄰的N個點作為輸入,將其中最大的值輸出到下一層.除了最大池化,池化算法還有:取平均值,加權平均等等.
池化具有平移不變性:若對圖片進行少量平移,經過池化函數后的大多數輸出并不會發(fā)生改變,比如最大池化,移動一像素后,區(qū)域中最大的值可能仍在該區(qū)域內.
同理,在一個稀疏的矩陣中(不一定是圖像),假設對n點做最大池化,那么只要其中有一點非0,則池化結果非0.如下圖所示,只要手寫數據5符合其中一個判斷標準,則將其識別為數字5.

池化還經常用于處理不同尺寸的圖片:比如有800x600和200*150兩張圖,想對它們做同一處理,可通過設定池化的輸出為100x75來實現保留特征的縮放,以至擴展到任意大小的圖片.
基本上卷積網絡都用使用池化操作.
4) DropOut

再舉個例子,現在我們來識別老鼠,一般老鼠都有兩只耳朵,兩只眼睛,一個鼻子和一個嘴,它們有各自的形狀且都是從上到下排列的.如果嚴格按照這個規(guī)則,那么"一只耳"就不會被識別成老鼠.
為提高魯棒性,使用了DropOut方法.它隨機地去掉神經網絡中的一些點,用剩余的點訓練,假設有一些訓練集中的老鼠,被去掉的正是耳朵部分,那么"一只耳"最終也可能由于其它特征都對而被識別.
除了提高魯棒性,DropOut還有其它一些優(yōu)點,比如在卷積神經網絡中,由于共享參數,我們訓練一小部分的子網絡(由DropOut剪出),參數共享會使得剩余的子網絡也能有同樣好的參數設定。保證學習效果的同時,也大大減少了計算量.
DropOut就如同在層與層之間故意加入了一些噪聲,雖然避免了過擬合,但它是一個有損的算法,小的網絡使用它可能會丟失一些有用的信息.一般在較大型的網絡中使用DropOut.
5) 總結
卷積,池化,DropOut都是設計者根據數據的性質,采取的對全連接網絡的優(yōu)化和簡化,雖然它們都是有損的,但是對計算和存儲的優(yōu)化也非常明顯,使我們有機會將神經網絡擴展到成百上千層.
3. 代碼
1) 說明
代碼的主要功能是根據Mnist庫的圖像數據訓練手寫數字識別.核心代碼在后半部分,它使用了卷積層,池化層,Dropout層,Flatten層,和全連接層.
2) 代碼
(為了讓大家復制粘貼就能運行,還是附上了全部代碼)
# -*- coding: utf-8 -*-
from __future__ import print_function
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
batch_size = 128 # 批尺寸
num_classes = 10 # 0-9十個數字對應10個分類
epochs = 12 # 訓練12次
# input image dimensions
img_rows, img_cols = 28, 28 # 訓練圖片大小28x28
# the data, shuffled and split between train and test sets
(x_train, y_train), (x_test, y_test) = mnist.load_data()
if K.image_data_format() == 'channels_first':
x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)
x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
input_shape = (1, img_rows, img_cols)
else:
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
input_shape = (img_rows, img_cols, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')
# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
# 下面model相關的是關鍵部分
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), #加卷積層,核大小3x3,輸出維度32
activation='relu', #激活函數為relu
input_shape=input_shape)) #傳入數據
model.add(Conv2D(64, (3, 3), activation='relu')) #又加一個卷積層
model.add(MaxPooling2D(pool_size=(2, 2))) # 以2x2為一塊池化
model.add(Dropout(0.25)) # 隨機斷開25%的連接
model.add(Flatten()) # 扁平化,例如將28x28x1變?yōu)?84的格式
model.add(Dense(128, activation='relu')) # 加入全連接層
model.add(Dropout(0.5)) # 再加一層Dropout
model.add(Dense(num_classes, activation='softmax')) # 加入到輸出層的全連接
model.compile(loss=keras.losses.categorical_crossentropy, # 設損失函數
optimizer=keras.optimizers.Adadelta(), # 設學習率
metrics=['accuracy'])
model.fit(x_train, y_train,
batch_size=batch_size, # 設批大小
epochs=epochs, # 設學習次數
verbose=1,
validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0) # 用測試集評測
print('Test loss:', score[0])
print('Test accuracy:', score[1])
4. CNN的歷史
看CNN的發(fā)展,從1998年LeCun的經典之作LeNet, 到將ImageNet圖像識別率提高了10個百分點的AlexNet, VGG(加入更多卷積層), GoogleNet(使用了Inception一種網中網的結構), 再到RssNet(使用殘差網絡),ImageNet的Top-5錯誤率已經降到3.57%,低于人眼識別的錯誤率5.1%,并且仍在不斷進步.這些不斷提高的成績以及在更多領域的應用讓神經變得越來越熱門.

從圖中可見,從AlexNet的3層全連接神經網絡,到ResNet的152層神經網絡,全連接層越來越少,卷積層越來越多.除了算法的進步,人的知識也越來越多越來越細化地溶入了神經網絡.