來源 |《Python人工智慧開發從入門到精通》
作者 | 楊柳、郭坦、魯銀芝
責編 | 晉兆雨
*文末有贈書福利
受20世紀中期興起的神經科學及腦科學研究的啟發,通過模擬生物神經元接收和處理信息的基本特性,研究人員提出並設計了人工神經元。作為計算機科學、生物學和數學的交叉融合,卷積神經網絡已經發展成為計算機視覺領域中最具影響力和有效的基礎技術。
早在20 世紀60 年代,生物學家Hubel 和Wiesel 通過研究貓的視覺皮層,發現每個視覺神經元都只對一個小區域範圍內的視覺圖像產生響應,即感受野(Receptive Field)。初級視覺皮層中的神經元能夠響應視覺環境中特定的簡單特徵,除此之外,Hubel 和Wiesel 通過研究發現了簡單和複雜兩種不同類型的細胞,其中簡單細胞只在特定的空間位置對它們偏好的方向產生最強烈響應,而複雜細胞具有更大的空間不變性。
根據這些實驗和分析,他們得出結論:複雜細胞通過在來自多個簡單細胞(每個都有一個不同的偏好位置)的輸入進行池化而實現這種不變性,這兩個特性,即對特定特徵的選擇性和通過前饋連接增大空間不變性,構成了CNN人工視覺系統的生物及神經學基礎。
LeNet-5 網絡雖然較小,但它含有諸多神經網絡學習的關鍵模塊,具體包括卷積層、池化層及全連接層,這些基本模塊構成當前深度神經網絡模型的基礎, 下文將對LeNet-5 的結構及工作原理進行深入分析。同時,藉助實例加深讀者對卷積神經網絡各個模塊功能的理解。
卷積神經網絡與LeNet-5
LeNet-5 出自Yann LeCun 教授於1998 發表的論文Gradient-Based Learning Applied to DocumentRecognition 中,LeNet-5 模型共有7 層,如圖11-1 所示為LeNet-5 的基本網絡架構。
LeNet-5 的基本網絡架構
該模型除了輸入層之外,每層都包含可訓練參數,每個網絡層產生多個特徵圖,每個特徵圖可通過一種卷積濾波器提取輸入數據一種類型的特徵。各個網絡層的功能與參數情況介紹如下。
1. 輸入層
首先是輸入數據網絡層,上例中輸入圖像尺寸統一歸一化為32×32×1,其中1 表示輸入圖像為單通道的灰度圖,一般不將該層作為LeNet-5 網絡的基本構成,即不將輸入層視為網絡層次結構之一。
2. C1 層
C 取自Convolutional 的首字母,指卷積。讀者可能對卷積的概念並不陌生,對數字圖像做卷積運算,本質上是通過卷積核(卷積模板)在圖像上滑動,將圖像上的像素灰度值與對應卷積核上的數值相乘,然後將所有相乘後的值相加作為卷積核中間像素對應像素的灰度值,以此方式遍歷完成對整張圖像像素的卷積計算。如圖11-2顯示了圖像卷積計算過程中一次相乘後相加的運算過程,該卷積核大小為3×3,卷積核內共有9 個數值,數值個數即為圖像像素值與卷積核上數值相乘次數,運算結果-4 代替了原圖像中對應位置處的值。按此方式,沿著圖片以步長為1 滑動,每次滑動1個像素都進行一次相乘再相加的操作,即可得到最終的輸出結果。
卷積計算過程
圖像卷積計算中,卷積核的設計十分重要,一般需遵循如下基本規則。
經卷積計算所得輸出通常被稱為「響應」,如果是邊緣檢測運算元,那麼響應為圖像邊緣,能夠檢測到特定的圖像邊緣。在LeNet-5網絡中得到的響應是特徵圖(Feature Map),計算結果為輸入圖像的特徵表達,卷積核的參數權重可以通過優化算法在監督信息的指導下自適應地學習得到。LeNet-5 網絡中C1 層輸入圖像尺寸為32×32×1,卷積核大小為5×5,一共包括6 種大小為5×5 的卷積核,卷積核滑動一行之後,得到的結果的邊長變為32-5+1,提取的特徵映射大小是28×28,即(32-5+1)=28。6種不同的卷積核,可以從不同的角度提取圖像不同特性的特徵。
神經元數量為28×28×6,則可訓練參數為(5×5+1)×6,即每個濾波器含5×5=25個單元權值參數和1個偏置參數,一共6 個濾波器,因此總的連接數為(5×5+1)×6×28×28=122 304。針對122 304 個連接,通過權值共享策略,只需學習156 個參數。
3. S2 層
S 指的是Subsamples,該網絡層完成下採樣操作,得到對應的特徵圖。下採樣的原則是在減少數據量的同時儘可能保留有用的信息。與普通插值下採樣的方式不同,該層實際採用的是一種被稱為池化(Pooling)的方法。具體是將一幅圖像分割成若干塊,每個圖像塊的輸出是該圖像塊原有像素的統計結果。
圖像下採樣池化方法有很多,如Mean-pooling( 均值採樣)、Max-pooling( 最大值採樣)、Overlapping ( 重疊採樣)、L2-pooling( 均方採樣)、Local Contrast Normalization( 局部對比歸一化)、Stochastic-pooling( 隨機採樣) 和Def-pooling( 形變約束採樣) 等,其中最經典的是最大池化,也是
最常用的,下面簡要介紹最大池化的實現原理。
為直觀起見,假設有如圖11-3(a)中大小為4×4 的圖像,圖像中每個像素點的值是上面各個格子中的數值。現在對這張4×4 大小的圖像進行池化操作,池化的大小為(2,2),步長為2。採用最大池化操作,首先對圖像進行分塊,每個圖像塊大小為2×2,然後按照圖11-3(b)中方式統計每個圖像塊的最大值,作為下採樣後圖像的像素值,得到圖11-3(c)中結果,該過程即為最大池化。
除此之外,還有其他池化方法,如均值池化,具體是對每個塊求取平均值作為下採樣的新像素值。上述例子未涉及重疊採樣,即每個圖像塊之間沒有相互重疊的部分,而步長為2 時,圖像分塊不重疊。
最大池化操作示意圖
LeNet-5 網絡中的S2 層的輸入是上一層的輸出,共有6 個特徵映射,每個特徵映射的尺寸為28×28,使用2×2 大小的核進行池化操作,得到S2,即6 個14×14 大小的特徵映射(28/2=14)。換言之,S2 中的池化層是對C1 中的2×2 區域內的像素求和乘以一個權值係數再加上一個偏置,然後將這個結果再做一次映射。與卷積層連接數的計算方法一樣,連接數=參數個數×特徵映射大小,即(2×2+1)×6×14×14=5880。
4. C3 層
C3 層同樣是卷積層,輸入為S2 中所有6 個或若干個特徵圖的組合。具體地,該層卷積核大小為5×5,一共有6種卷積核,輸出特徵圖大小為10×10,即(14-5+1)=10。需要注意的是,C3 中每個特徵圖是連接到S2 中的所有6 個或若干個特徵圖的,即該層的特徵圖是上一層提取到的特徵圖的不同組合。如圖11-4 所示,LeCun 在原論文中給出的一種組合連接方式。
LeNet-5 網絡C3 層特徵映射組合方式
圖11-4 中共有6 行16 列,橫軸代表C3 特徵映射索引,縱軸代表S2 特徵圖索引。每列的X表示C3 中的每個特徵映射與S2 中的特徵圖的連接情況,可以看到C3 的前6 個特徵圖,對應上圖第1 個紅框的6 列,以S2 中3 個相鄰的特徵圖子集為輸入,緊接著6 個特徵圖(對應上圖第2 個紅框的6 列)以S2 中4 個相鄰特徵圖子集為輸入。
然後,接下來的3 個特徵圖(對應上圖第3 個紅框的3 列)以S2 中不相鄰的4 個特徵圖子集為輸入,C3 中的最後一個特徵圖對應上圖第4 個紅框的1 列將S2 中所有特徵圖為輸入。這裡得到的每一個特徵圖為多核多通道卷積,將每一列稱為一個卷積核,它由若干個卷積模板構成,因為是多個卷積核模板卷積的結果得到一個特徵圖,仍然認為是一個卷積核,所以每列只有一個偏置參數。之所以採取這種組合方式,LeCun 主要是基於以下兩點考慮:減少參數;採用不對稱的組合連接方式有利於提取多種組合特徵。
5. S4 層
S4 層為下採樣層,即池化層,窗口大小為2×2,包括16 個特徵圖,C3 層的16 個10×10 的特徵圖分別進行以2×2 為單位的池化得到16 個5×5 的特徵圖,步長為2,即本網絡層的輸出張量大小為5×5×16,一共有5×5×5×16=2000 個連接,連接的方式與S2 層類似。
6. C5 層
C5 層是一個卷積層,輸入為S4 層的全部16 個特徵圖,該層卷積原理與普通卷積層一致,只是因為恰巧卷積核大小與輸入特徵圖尺寸一樣,因此得到一維,即1×1(5-5+1)的輸出,卷積核種類為120,得到120 維的卷積結果,每個都與上一層的16 個特徵圖相連,因此一共有(5×5×16+1)×120=48 120 個可訓練參數。
7. F6 層
F6 層是全連接層,採用全連接的方式與C5 層連接,由對C5 層的輸入乘以權重加上偏置,結果通過激活Sigmoid 函數輸出。F6 層有84 個節點,對應於一個7×12 的比特圖,-1 表示白色,1 表示黑色,這樣每個符號的比特圖的黑白色對應一個編碼,F6 層的訓練參數/ 連接數為(120+1)×84=10 164。
不過,CNN 的基本組成單元和模塊相對一致,可以像搭積木一樣將不同功能的網絡層組合起來,從而實現規模更大、深度更深的網絡。因此,從某種意義上說,CNN 或深度學習中的網絡層本質上是能夠進行信息處理的積木單元。LeNet-5 對於更複雜問題的處理效果並不理想,但通過對LeNet-5 的網絡結構的分析與研究,可以直觀地了解卷積神經網絡的構建方法,能夠為分析和構建更複雜、更深層的卷積神經網絡打下堅實的基礎。
總結卷積神經網絡的成功經驗,主要在於局部連接(LocalConnection)、權值共享(Weight Sharing)和池化層(Pooling)中的降採樣(Down-Sampling)。
(2)池化層(Pooling Layer)。池化是非線性下採樣的一種形式,主要作用是通過減少網絡的參數來減小計算量,同時池化層能降低卷積層輸出的特徵向量,通常在卷積層的後面會加上一個池化層,通過卷積層與池化層交替使用可以獲得更複雜的高層抽象特徵,並且能夠在一定程度上避免和緩解過擬合現象。常用的池化操作包括最大池化、平均池化等,其中最大池化是用不重疊的矩形框將輸入層分成不同的區域,對於每個矩形框內的數值取最大值作為統計輸出。
(3)全連接層(Full Connected Layer)。如果說卷積層、池化層和激活函數映射等操作是將原始數據映射到隱層特徵空間的話,那麼全連接層則起到將學到的「分布式特徵表示」映射到樣本標記空間的作用,將多層的特徵表達拉直成一個一維的向量,實現神經網絡的高層抽象推理能力,在整個卷積神經網絡中起到「分類器」的作用。
(4)局部連接(Local Connection)。局部連接指的是每個神經元僅與輸入神經元的一塊區域相連,該局部區域也被稱為感受野(Receptive Field)。局部連接的思想可追溯至生物學裡面的視覺系統結構,即視覺皮層的神經元實質上是局部接收信息的。在圖像卷積操作中,神經元在空間維度上是局部連接的,但在深度上是全部連接的。對於二維圖像本身而言,局部像素關聯較強,這種局部連接保證了學習後的過濾器能夠對於局部的輸入特徵有最強的響應。
(5)權重共享(Weight Sharing)。實際中,圖像的底層邊緣特徵與特徵在圖中的具體位置無關,即特徵可能出現在圖像的任意位置,權重共享正是利用這一特點,具體是指卷積核內權重參數被整張圖共享,而不會因圖像內位置的不同而改變,可在圖像中的不同位置學習到同樣的特徵,權重共享可以在很大程度上減少參數數量。
LeNet-5 的TensorFlow 實現
前文介紹了LeNet-5 的基本網絡結構,以及各個網絡功能層的特點與作用,本節將利用TensorFlow 具體實現這一網絡。首先需要說明以下幾點。
(1)LeNet-5 主要採用Tanh 和Sigmoid 作為非線性激活函數,但相對這兩者採用ReLu 激活函數的卷積神經網絡更加有效。
(2)LeNet-5 採用平均池化作為下採樣操作,但是目前最大池化操作應用更為廣泛。
(3)LeNet-5 網絡最後一層採用Gaussian 連接層,用於輸出0~9 這10 個類別中的一類,但是目前分類器操作已經被Softmax 層取代。
第1 步:建立config.py 文件,可以將超參數設置在config.py 中,方便後期對模型進行調整。代碼實現與說明如程序清單11-1 所示。
程序清單11-1 config.py 文件建立及超參數設置
1. """
2. 設置模型的超參數
3.
4.
5. KEEP_PROB: 網絡隨機失活的機率
6. LEARNING_RATE: 學習的速率,即梯度下降的速率
7. BATCH_SIZE: 一次訓練所選取的樣本數
8. PARAMETER_FILE: 模型參數保存的路徑
9. MAX_ITER: 最大疊代次數
10. """
11.
12. KEEP_PROB = 0.5
13. LEARNING_RATE = 1e-5
14. BATCH_SIZE =50
15. PARAMETER_FILE = "checkpoint/variable.ckpt"
16. MAX_ITER = 50000
第2 步:構建LeNet 模型的LeNet.py 文件,建立一個名為Lenet 的類,類中實現模型的初始化與構建,代碼實現與說明如程序清單11-2 所示。
程序清單11-2 構建LeNet 模型與LeNet.py 文件
1.importtensorflow astf
2.importtensorflow.contrib.slim asslim
3.importconfig ascfg
4.
5.classLenet:
6.def__init__(self):
7."""
8. 初始化LeNet 網絡
9. """
10.# 設置網絡輸入的圖片為二維張量,數據的類型為float32,行數不固定,列固定為784
11.self.raw_input_image = tf.placeholder(tf.float32, [ None, 784])
12.
13.# 改變網絡輸入張量的形狀為四維,-1 表示數值不固定
14.self.input_images = tf.reshape(self.raw_input_image, [ -1, 28, 28, 1])
15.
16.# 設置網絡輸入標籤為二維張量,數據類型為float,行數不固定,列固定為10
17.self.raw_input_label = tf.placeholder( "float", [ None, 10])
18.
19.# 改變標籤的數據類型為int32
20.self.input_labels = tf.cast(self.raw_input_label,tf.int32)
21.
22.# 設置網絡的隨機失活機率
23.self.dropout = cfg.KEEP_PROB
24.
25.# 構建兩個網絡
26.# train_digits 為訓練網絡,開啟dropout
27.# pred_digits 為預測網絡,關閉dropout
28.withtf.variable_scope( "Lenet") asscope:
29.self.train_digits = self.construct_net( True)
30.scope.reuse_variables
31.self.pred_digits = self.construct_net( False)
32.
33. # 獲取網絡的預測數值
34. self.prediction = tf.argmax( self.pred_digits, 1)
35.
36. # 獲取網絡的預測數值與標籤的匹配程度
37. self.correct_prediction = tf.equal(tf.argmax( self.pred_digits, 1), tf.argmax
( self.input_labels, 1))
38.
39. # 將匹配程度轉換為float 類型,表示為精度
40. self.train_accuracy = tf.reduce_mean(tf.cast( self.correct_prediction, "float"))
41.
42. # 計算train_digits 與labels 之間的係數softmax 交叉熵,定義為loss
43. self.loss = slim.losses.softmax_cross_entropy( self.train_digits, self.
input_labels)
44.
45. # 設置學習速率
46. self.lr = cfg.LEARNING_RATE
47. self.train_op = tf.train.AdamOptimizer( self.lr).minimize( self.loss)
48.
49.
50. defconstruct_net( self,is_trained = True) :
51. """
52. 接收is_trained 參數判斷是否開啟dropout
53. 用slim 構建LeNet 模型
54. 第一、三、五層為卷積層、第二、四層為池化層
55. 接下來對第五層扁平化,再接入全連接
56. 接著進行隨機失活防止過擬合,再次接入全連接層
57. 最後返回構建的網絡
58. " ""
59. with slim.arg_scope([slim.conv2d], padding= 'VALID',
60. weights_initializer=tf.truncated_normal_initializer(stddev= 0. 01),
61. weights_regularizer=slim.l2_regularizer( 0. 0005)):
62. net = slim.conv2d( self.input_images, 6,[ 5, 5], 1,padding= 'SAME',scope= 'conv1')
63. net = slim.max_pool2d(net, [ 2, 2], scope= 'pool2')
64. net = slim.conv2d(net, 16,[ 5, 5], 1,scope= 'conv3')
65. net = slim.max_pool2d(net, [ 2, 2], scope= 'pool4')
66. net = slim.conv2d(net, 120,[ 5, 5], 1,scope= 'conv5')
67. net = slim.flatten(net, scope= 'flat6')
68. net = slim.fully_connected(net, 84, scope= 'fc7')
69. net = slim.dropout(net, self.dropout,is_training=is_trained, scope=
'dropout8')
70. digits = slim.fully_connected(net, 10, scope= 'fc9')
71. returndigits
本模型是對著名的手寫字體MNIST數據集進行訓練,可以在網站http://yann.lecun.com/exdb/mnist/ 上很方便地直接下載數據,得到如圖11-5 的MNIST 數據集。
MNIST 數據集列表
第3 步:建立模型訓練文件Train.py,主要實現數據讀取、模型訓練等功能,代碼實現與說明如程序清單11-3 所示。
程序清單11-3 模型訓練文件Train.py 的 建立
1. import tensorflow.examples.tutorials.mnist.input _data as input_data
2. import tensorflow as tf
3. import config as cfg
4. import os
5. import lenet
6. from lenet import Lenet
7.
8.
9. def main:
10. # 從指定路徑加載訓練數據
11. mnist = input _data.read_data _sets("MNIST_data/", one_hot=True)
12.
13. # 開啟TensorFlow 會話
14. sess = tf.Session
15.
16. # 設置超參數
17. batch _size = cfg.BATCH_SIZE
18. parameter _path = cfg.PARAMETER_FILE
19. lenet = Lenet
20. max _iter = cfg.MAX_ITER
21.
22. # 加載已保存的模型參數文件,如果不存在則調用初始化函數生成初始網絡
23. saver = tf.train.Saver
24. if os.path.exists(parameter_path):
25. saver.restore(parameter_path)
26. else:
27. sess.run(tf.initialize _all_variables)
28.
29. # 疊代訓練max_iter 次,每次抽取50 個樣本進行訓練
30. # 每100 次列印出當前數據的精度
31. # 訓練完成後保存模型參數
32. for i in range(max_iter):
33. batch = mnist.train.next_batch(50)
34. if i % 100 == 0:
35. train _accuracy = sess.run(lenet.train_accuracy,feed_dict={
36. lenet.raw _input_image: batch[0],lenet.raw _input_label: batch[1]
37. })
38. print("step %d, training accuracy %g" % (i, train_accuracy))
39. sess.run(lenet.train _op,feed_dict={lenet.raw _input_image: batch[0],lenet.
raw _input_label: batch[1]})
40. save _path = saver.save(sess, parameter_path)
41.
42. if __name__== ' __main__':
43. main(
第4 步:在上述完成步驟的基礎上,運行Train.py。如圖11-6 所示,可以看到隨著不斷疊代優化,模型精度在逐步提高。
模型疊代優化過程
程序清單11-4 測試文件Inference.py 的建立
1.importtensorflow astf
2.fromPIL importImage,ImageOps
3.importnumpy asnp
4.fromlenet importLenet
5.importconfig ascfg
6.
7.classinference:
8.def__init__(self):
9."""
10. 構建Lenet 網絡,設置模型參數文件路徑,開啟TensorFlow 會話
11. """
12.self.lenet = Lenet
13.self.sess = tf.Session
14.self.parameter_path = cfg.PARAMETER_FILE
15.self.saver = tf.train.Saver
16.
17.defpredict(self,image):
18."""
19. 接收要測試的圖片作為參數,返回預測值
20. """
21.# 將圖片轉換為合適的大小進行輸入
22.img = image.convert( 'L')
23.img = img.resize([ 28, 28], Image.ANTIALIAS)
24.image_input = np.array(img, dtype= "float32") / 255
25.image_input = np.reshape(image_input, [ -1, 784])
26.
27.# 讀取模型參數並對圖片進行預測,返回預測值
28.self.saver.restore(self.sess,self.parameter_path)
29.predition = self.sess.run(self.lenet.prediction, feed_dict={self.lenet.raw_
input_image: image_input})
30.returnpredition
程序清單11-5 利用tkinter 建立UI
1.importtkinter
2.fromPIL importImage,ImageDraw
3.fromInference importinference
4.
5.classMyCanvas:
6."""
7. 設置一個256*256 大小的容器進行手寫介面的繪製
8. 背景色設置為黑色,繪製軌跡設置為白色
9. """
10.def__init__(self,root):
11.self.root=root
12.self.canvas=tkinter.Canvas(root,width= 256,height= 256,bg= 'black')
13.self.canvas.pack
14.self.image1 = Image.new( "RGB", ( 256, 256), "black")
15.self.draw = ImageDraw.Draw(self.image1)
16.self.canvas.bind( '
17.
18.# 繪製軌跡
19.defDraw(self,event):
20.self.canvas.create_oval(event.x,event.y,event.x,event.y,outline= "white",
width = 20)
21.self.draw.ellipse((event.x -10,event.y -10,event.x+ 10,event.y+ 10),fill=( 255,
255, 255))
22.
23.
24.defmain:
25.# 建立一個tkinter 對象, 設置大小為380*300
26.root = tkinter.Tk
27. root.geometry('380x300')
28. # 創建一個256*256 的框架容納手寫的容器,位於tkinter 對象的左邊,填充y 方向
29. frame = tkinter.Frame(root, width=256, height=256)
30. frame.pack_propagate(0)
31. frame.pack(side="left", fill='y')
32. # 將frame 導入canvas 容器
33. canvas1 = MyCanvas(frame)
34. # 創建一個圖像識別的實例
35. infer = inference
36.
37. # 定義識別按鈕觸發函數
38. # 按下的時候將cavas 導出為圖片,放入infer 中進行圖像識別,並將結果顯示在label2 中
39. def inference_click:
40. img = canvas1.image1
41. result = infer.predict(img)
42. result = int(result)
43. label2["text"] = str(result)
44.
45. # 定義清除按鈕的觸發函數
46. # 按下的時候將canvas 情況並重新繪製背景,並將label 設置為空
47. def clear_click:
48. canvas1.canvas.delete("all")
49. canvas1.image1 = Image.new("RGB", (256, 256), "black")
50. canvas1.draw = ImageDraw.Draw(canvas1.image1)
51. label2["text"] = ""
52.
53. # 定義識別按鈕的樣式
54. botton_Inference = tkinter.Button(root,
55. text=" 檢測",
56. width=14,
57. height=2,
58. command=inference_click
59. )
60. # 定義清除按鈕的樣式
61. botton_Clear = tkinter.Button(root,
62. text=" 清屏",
63. width=14,
64. height=2,
65. command=clear_click
66. )
67. # 綁定識別按鈕到tkinter 中,設置位置為頂層
68. botton_Inference.pack(side="top")
69.
70. # 綁定清除按鈕到tkinter 中
71. botton_Clear.pack(side="top")
72.
73. # 定義label1
74. label1 = tkinter.Label(root, justify="center", text=" 檢測結果為:")
75. label1.pack(side="top")
76.
77. # 定義label2
78. label2 = tkinter.Label(root, justify="center")
79.
80. # 設置字體樣式與大小
81. label2["font"] = ("Arial, 48")
82. label2.pack(side="top")
83. root.mainloop
84.
85. if __name__== ' __main__':
86. main
第7 步:運行代碼並進行如下的幾組測試,測試結果如圖11-7 所示。
手寫數字測試示例
#歡迎留言 在評論區和我們討論#
看完本文,對於人工智慧、卷積神經網絡你有什麼想說的?
歡迎在評論區留言
我們將在8 月 2 2 日精選出 3 條優質留言
贈送《Python人工智慧開發從入門到精通》紙質書籍一本哦!