【深度學習】5:CNN卷積神經網絡原理

前言:先坦白的說,深度神經網絡的學習在一開始對我造成的困擾還是很大的,我也是通過不斷地看相關的視頻資料、文獻講解嘗試去理解記憶。畢竟這些內容大多都是不可查的,我們看到的都只是輸入輸出的東西,裏面的內部運作以及工作原理,都需要沉心靜思。

這篇CNN卷積神經網絡的原理介紹,也是自己通過收集來的資料閱讀、理解、操練後,有了一定的見解後才拙筆,裏面的內容我會盡量詳盡,不清楚明白的地方,望大家慧眼指出。

-----------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------

一、機器如何識圖

先給大家出個腦筋急轉彎:在白紙上畫出一個大熊貓,一共需要幾種顏色的畫筆?——大家應該都知道,只需要一種黑色的畫筆,只需要將大熊貓黑色的地方塗上黑色,一個大熊貓的圖像就可以展現出來。

我們畫大熊貓的方式,其實和媽媽們的十字繡很接近——在給定的格子裏,繡上不同的顏色,最後就可以展現出一幅特定的“圖片”。而機器識圖的方式正好和繡十字繡的方式相反,現在有了一幅圖片,機器通過識別圖片中每個格子(像素點)上的顏色,將每個格子裏的顏色都用數字類型存儲,得到一張很大的數字矩陣,圖片信息也就存儲在這張數字矩陣中。
這裏寫圖片描述
上圖中每一個格子代表一個像素點,像素點裏的數字代表顏色碼,顏色碼範圍是[0,255],(各式各樣的顏色都是由紅、綠、藍三色組成,每個顏色都是0~255之間數字)
-----------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------
我們在得到的一張大數字矩陣的基礎上開展卷積神經網絡識別工作:
機器識圖的過程:機器識別圖像並不是一下子將一個複雜的圖片完整識別出來,而是將一個完整的圖片分割成許多個小部分,把每個小部分裏具有的特徵提取出來(也就是識別每個小部分),再將這些小部分具有的特徵彙總到一起,就可以完成機器識別圖像的過程了
-----------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------

二、卷積神經網絡原理介紹

用CNN卷積神經網絡識別圖片,一般需要的步驟有:

  1. 卷積層初步提取特徵
  2. 池化層提取主要特徵
  3. 全連接層將各部分特徵彙總
  4. 產生分類器,進行預測識別

1、卷積層工作原理

卷積層的作用:就是提取圖片每個小部分裏具有的特徵

假定我們有一個尺寸爲666*6 的圖像,每一個像素點裏都存儲着圖像的信息。我們再定義一個卷積核(相當於權重),用來從圖像中提取一定的特徵。卷積核與數字矩陣對應位相乘再相加,得到卷積層輸出結果。
這裏寫圖片描述
(429 = 181+540+511+550+1211+750+351+240+204*1)
卷積核的取值在沒有以往學習的經驗下,可由函數隨機生成,再逐步訓練調整

當所有的像素點都至少被覆蓋一次後,就可以產生一個卷積層的輸出(下圖的步長爲1)
這裏寫圖片描述

機器一開始並不知道要識別的部分具有哪些特徵,是通過與不同的卷積核相作用得到的輸出值,相互比較來判斷哪一個卷積核最能表現該圖片的特徵——比如我們要識別圖像中的某種特徵(比如曲線),也就是說,這個卷積核要對這種曲線有很高的輸出值,對其他形狀(比如三角形)則輸出較低。卷積層輸出值越高,就說明匹配程度越高,越能表現該圖片的特徵

卷積層具體工作過程:
比如我們設計的一個卷積核如下左,想要識別出來的曲線如下右:
這裏寫圖片描述

現在我們用上面的卷積核,來識別這個簡化版的圖片——一隻漫畫老鼠
這裏寫圖片描述

當機器識別到老鼠的屁股的時候,真實區域數字矩陣與卷積核相乘作用後,輸出較大:6600
這裏寫圖片描述

而用同一個卷積核,來識別老鼠的耳朵的時候,輸出則很小:0
這裏寫圖片描述

我們就可以認爲:現有的這個卷積覈保存着曲線的特徵,匹配識別出來了老鼠的屁股是曲線的。我們則還需要其他特徵的卷積核,來匹配識別出來老鼠的其他部分。卷積層的作用其實就是通過不斷的改變卷積核,來確定能初步表徵圖片特徵的有用的卷積核是哪些,再得到與相應的卷積核相乘後的輸出矩陣

2、池化層工作原理

池化層的輸入就是卷積層輸出的原數據與相應的卷積核相乘後的輸出矩陣
池化層的目的:

  • 爲了減少訓練參數的數量,降低卷積層輸出的特徵向量的維度
  • 減小過擬合現象,只保留最有用的圖片信息,減少噪聲的傳遞

最常見的兩種池化層的形式:

  • 最大池化:max-pooling——選取指定區域內最大的一個數來代表整片區域
  • 均值池化:mean-pooling——選取指定區域內數值的平均值來代表整片區域

舉例說明兩種池化方式:(池化步長爲2,選取過的區域,下一次就不再選取)
這裏寫圖片描述
444*4的數字矩陣裏,以步長222*2選取區域,比如上左將區域[1,2,3,4]中最大的值4池化輸出;上右將區域[1,2,3,4]中平均值5/2池化輸出

3、全連接層工作原理

卷積層和池化層的工作就是提取特徵,並減少原始圖像帶來的參數。然而,爲了生成最終的輸出,我們需要應用全連接層來生成一個等於我們需要的類的數量的分類器。

全連接層的工作原理和之前的神經網絡學習很類似,我們需要把池化層輸出的張量重新切割成一些向量,乘上權重矩陣,加上偏置值,然後對其使用ReLU激活函數,用梯度下降法優化參數既可。

三、卷積神經網絡代碼解析

1、數據集的讀取,以及數據預定義

from tensorflow.examples.tutorials.mnist import input_data
#讀取MNIST數據集
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
#預定義輸入值X、輸出真實值Y    placeholder爲佔位符
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
keep_prob = tf.placeholder(tf.float32)
x_image = tf.reshape(x, [-1,28,28,1])
  • MNIST是Google的很經典的一個做圖像識別的數據集,圖片大小是282828*28的,需要先下載才能使用。MNIST數據集下載鏈接, 密碼: jcam
  • x、y_現在都是用佔位符表示,當程序運行到一定指令,向x、y_傳入具體的值後,就可以代入進行計算了
  • shape=[None, 784]是數據維度大小——因爲MNIST數據集中每一張圖片大小都是282828*28的,計算時候是將282828*28的二維數據轉換成一個一維的、長度爲784的新向量。None表示其值大小不定,意即選中的x、y_的數量暫時不定
  • keep_prob 是改變參與計算的神經元個數的值。(下有詳細說明)

2、權重、偏置值函數

def weight_variable(shape):
	# 產生隨機變量
	initial = tf.truncated_normal(shape, stddev=0.1)
	return tf.Variable(initial)

def bias_variable(shape):
	initial = tf.constant(0.1, shape=shape)
	return tf.Variable(initial)

truncated_normal()函數:選取位於正態分佈均值=0.1附近的隨機值

3、卷積函數、池化函數定義

def conv2d(x, W):
	#stride = [1,水平移動步長,豎直移動步長,1]
	return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
	# stride = [1,水平移動步長,豎直移動步長,1]
	return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
						  strides=[1, 2, 2, 1], padding='SAME')

  • 輸入x是圖片信息矩陣,W是卷積核的值
  • 卷積層conv2d()函數裏strides參數要求第一個、最後一個參數必須是1;
  • 第二個參數表示:卷積核每次向右移動的步長
  • 第三個參數表示:卷積核每次向下移動的步長

在上面卷積層的工作原理中,有展示strides=[1, 1, 1, 1]的動態圖,
下面展示strides=[1, 2, 2, 1]時的情況:可以看到高亮的區域每次向右移動兩格,向下移動兩格
這裏寫圖片描述

可以得到:當我們的卷積層步長值越大,得到的輸出圖像的規格就會越小。爲了使得到的圖像的規格和原圖像保持一樣的大,在輸入圖像四周填充足夠多的 0 邊界就可以解決這個問題,這時padding的參數就爲“SAME”(利用邊界保留了更多信息,並且也保留了圖像的原大小)下圖:
這裏寫圖片描述

padding的另一個可選參數爲“VALID”,和“SAME”不同的是:不用0來填充邊界,這時得到的圖像的規格就會小於原圖像。新圖像尺寸大小 = 原數據尺寸大小-卷積核尺寸大小+1(一般我們選用的padding都爲“SAME”)

池化函數用簡單傳統的2x2大小的模板做max pooling,池化步長爲2,選過的區域下次不再選取

4、第一次卷積+池化

x_image = tf.reshape(x, [-1,28,28,1])

# 卷積層1網絡結構定義
# 卷積核1:patch=5×5;in size 1;out size 32;激活函數reLU非線性處理
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
# output size 28*28*32
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1) 
# output size 14*14*32
h_pool1 = max_pool_2x2(h_conv1)    
  • 圖片集是黑白單色,x_image 中的圖片尺寸參數最後一個 = 1,彩色 = 3
  • 這裏的卷積核大小是555*5的,輸入的通道數是1,輸出的通道數是32
  • 卷積核的值這裏就相當於權重值,用隨機數列生成的方式得到
  • 由於MNIST數據集圖片大小都是282828*28,且是黑白單色,所以準確的圖片尺寸大小是2828128*28*1(1表示圖片只有一個色層,彩色圖片都RGB3個色層),所以經過第一次卷積後,輸出的通道數由1變成32,圖片尺寸變爲:28283228*28*32(相當於拉伸了高)
  • 再經過第一次池化,池化步長是222*2,相當於每四個小格子池化成一個數值,所以經過池化後圖片尺寸爲14143214*14*32

5、第二次卷積+池化

#卷積層2網絡結構定義
#卷積核2:patch=5×5;in size 32;out size 64;激活函數reLU非線性處理
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
# output size 14*14*64
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) 
# output size 7 *7 *64
h_pool2 = max_pool_2x2(h_conv2)                          
  • 這裏的卷積核大小也是555*5的,第二次輸入的通道數是32,輸出的通道數是64
  • 第一次卷積+池化輸出的圖片大小是14143214*14*32,經過第二次卷積後圖片尺寸變爲:14146414*14*64
  • 再經過第二次池化(池化步長也是222*2),最後輸出的圖片尺寸爲77647*7*64

6、全連接層1、全連接層2

# 全連接層1
W_fc1 = weight_variable([7*7*64,1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1,7*7*64])  
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob) 

# 全連接層2
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
prediction = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
  • 全連接層的輸入就是第二次池化後的輸出,尺寸是77647*7*64,全連接層1設置有1024個神經元
  • tf.reshape(a,newshape)函數,當newshape = -1時,函數會根據已有的維度計算出數組的另外shape屬性值
  • keep_prob 是爲了減小過擬合現象。每次只讓部分神經元參與工作使權重得到調整。只有當keep_prob = 1時,纔是所有的神經元都參與工作
  • 全連接層2設置有10個神經元,相當於生成的分類器
  • 經過全連接層1、2,得到的預測值存入prediction 中

7、梯度下降法優化、求準確率

#二次代價函數:預測值與真實值的誤差
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=prediction))
#梯度下降法:數據太龐大,選用AdamOptimizer優化器
train_step = tf.train.AdamOptimizer(1e-4).minimize(loss)
#結果存放在一個布爾型列表中
correct_prediction = tf.equal(tf.argmax(prediction,1), tf.argmax(y_,1))
#求準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
  • 由於數據集太龐大,這裏採用的優化器是AdamOptimizer,學習率是1e-4
  • tf.argmax(prediction,1)返回的是對於任一輸入x預測到的標籤值,tf.argmax(y_,1)代表正確的標籤值
  • correct_prediction 這裏是返回一個布爾數組。爲了計算我們分類的準確率,我們將布爾值轉換爲浮點數來代表對與錯,然後取平均值。例如:[True, False, True, True]變爲[1,0,1,1],計算出準確率就爲0.75

8、其他說明、保存參數

for i in range(1000):
	batch = mnist.train.next_batch(50)
	if i%100 == 0:
		train_accuracy = accuracy.eval(feed_dict={x:batch[0], y_: batch[1], keep_prob: 1.0})
		print("step",i, "training accuracy",train_accuracy)
	train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

'''
#保存模型參數
saver.save(sess, './model.ckpt')
print("test accuracy %g"%accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
'''
  • batch 是來源於MNIST數據集,一個批次包含50條數據
  • feed_dict=({x: batch[0], y_: batch[1], keep_prob: 0.5}語句:是將batch[0],batch[1]代表的值傳入x,y_;
  • keep_prob = 0.5 只有一半的神經元參與工作

當完成訓練時,程序會保存學習到的參數,不用下次再訓練
特別提醒:運行非常佔內存,而且運行到最後保存參數時,有可能卡死電腦

四、源碼及效果展示

# -*- coding:utf-8 -*-
# -*- author:zzZ_CMing
# -*- 2018/01/24;14:14
# -*- python3.5

from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'

def weight_variable(shape):
	# 產生隨機變量
	# truncated_normal:選取位於正態分佈均值=0.1附近的隨機值
	initial = tf.truncated_normal(shape, stddev=0.1)
	return tf.Variable(initial)

def bias_variable(shape):
	initial = tf.constant(0.1, shape=shape)
	return tf.Variable(initial)

def conv2d(x, W):
	#stride = [1,水平移動步長,豎直移動步長,1]
	return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
	# stride = [1,水平移動步長,豎直移動步長,1]
	return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
						  strides=[1, 2, 2, 1], padding='SAME')

#讀取MNIST數據集
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
sess = tf.InteractiveSession()

#預定義輸入值X、輸出真實值Y    placeholder爲佔位符
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
keep_prob = tf.placeholder(tf.float32)
x_image = tf.reshape(x, [-1,28,28,1])
#print(x_image.shape)  #[n_samples,28,28,1]

#卷積層1網絡結構定義
#卷積核1:patch=5×5;in size 1;out size 32;激活函數reLU非線性處理
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1) #output size 28*28*32
h_pool1 = max_pool_2x2(h_conv1)                          #output size 14*14*32

#卷積層2網絡結構定義
#卷積核2:patch=5×5;in size 32;out size 64;激活函數reLU非線性處理
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) #output size 14*14*64
h_pool2 = max_pool_2x2(h_conv2)                          #output size 7 *7 *64

# 全連接層1
W_fc1 = weight_variable([7*7*64,1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1,7*7*64])   #[n_samples,7,7,64]->>[n_samples,7*7*64]
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob) # 減少計算量dropout

# 全連接層2
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
prediction = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
#prediction = tf.nn.softmax(stf.matmul(h_fc1_drop, W_fc2) + b_fc2)

#二次代價函數:預測值與真實值的誤差
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=prediction))
#梯度下降法:數據太龐大,選用AdamOptimizer優化器
train_step = tf.train.AdamOptimizer(1e-4).minimize(loss)
#結果存放在一個布爾型列表中
correct_prediction = tf.equal(tf.argmax(prediction,1), tf.argmax(y_,1))
#求準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

saver = tf.train.Saver()  # defaults to saving all variables
sess.run(tf.global_variables_initializer())

for i in range(1000):
	batch = mnist.train.next_batch(50)
	if i%100 == 0:
		train_accuracy = accuracy.eval(feed_dict={x:batch[0], y_: batch[1], keep_prob: 1.0})
		print("step",i, "training accuracy",train_accuracy)
	train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

'''
#保存模型參數
saver.save(sess, './model.ckpt')
print("test accuracy %g"%accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
'''

效果展示如下:
這裏寫圖片描述
訓練700次時候,成功率已經到達98%,越往後學習,準確率越高

特別提醒:由於我的電腦配置比較低,運行耗時較長,而且在保存參數時候還會出現卡死情況,大家請注意。
-----------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------

系列推薦:

【監督學習】1:KNN算法實現手寫數字識別的三種方法
-----------------------------------------------------------------------------------------------------------------------------
【無監督學習】1:K-means算法原理介紹,以及代碼實現
【無監督學習】2:DBSCAN算法原理介紹,以及代碼實現
【無監督學習】3:Density Peaks聚類算法(局部密度聚類)
-----------------------------------------------------------------------------------------------------------------------------
【深度學習】1:感知器原理,以及多層感知器解決異或問題
【深度學習】2:BP神經網絡的原理,以及異或問題的解決
【深度學習】3:BP神經網絡識別MNIST數據集
【深度學習】4:BP神經網絡+sklearn實現數字識別
【深度學習】5:CNN卷積神經網絡原理、MNIST數據集識別
【深度學習】8:CNN卷積神經網絡識別sklearn數據集(附源碼)
【深度學習】6:RNN遞歸神經網絡原理、MNIST數據集識別
【深度學習】7:Hopfield神經網絡(DHNN)原理介紹
-----------------------------------------------------------------------------------------------------------------------------
TensorFlow框架簡單介紹
-----------------------------------------------------------------------------------------------------------------------------

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章