1. 程式人生 > >【深度學習】5:CNN卷積神經網路原理、識別MNIST資料集

【深度學習】5:CNN卷積神經網路原理、識別MNIST資料集

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

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

—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-

一、機器如何識圖

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

我們畫大熊貓的方式,其實和媽媽們的十字繡很接近——在給定的格子裡,繡上不同的顏色,最後就可以展現出一幅特定的“圖片”。而機器識圖的方式正好和繡十字繡的方式相反,現在有了一幅圖片,機器通過識別圖片中每個格子(畫素點)上的顏色,將每個格子裡的顏色都用數字型別儲存,得到一張很大的數字矩陣,圖片資訊也就儲存在這張數字矩陣中。
這裡寫圖片描述


上圖中每一個格子代表一個畫素點,畫素點裡的數字代表顏色碼,顏色碼範圍是[0,255],(各式各樣的顏色都是由紅、綠、藍三色組成,每個顏色都是0~255之間數字)
—-—-—-—-—-—-—-—-—-—-—-—–—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
—-—-—-—-—-—-—-—-—-—-—-—–—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
我們在得到的一張大數字矩陣的基礎上開展卷積神經網路識別工作:
機器識圖的過程:機器識別影象並不是一下子將一個複雜的圖片完整識別出來,而是將一個完整的圖片分割成許多個小部分,把每個小部分裡具有的特徵提取出來(也就是識別每個小部分),再將這些小部分具有的特徵彙總到一起,就可以完成機器識別影象的過程了

—-—-—-—-—-—-—-—-—-—-—-—–—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
—-—-—-—-—-—-—-—-—-—-—-—–—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-

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

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

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

1、卷積層工作原理

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

假定我們有一個尺寸為6*6 的影象,每一個畫素點裡都儲存著影象的資訊。我們再定義一個卷積核(相當於權重),用來從影象中提取一定的特徵。卷積核與數字矩陣對應位相乘再相加,得到卷積層輸出結果。
這裡寫圖片描述
(429 = 18*1+54*0+51*1+55*0+121*1+75*0+35*1+24*0+204*1)
卷積核的取值在沒有以往學習的經驗下,可由函式隨機生成,再逐步訓練調整

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

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

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

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

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

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

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

2、池化層工作原理

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

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

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

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

舉例說明兩種池化方式:(池化步長為2,選取過的區域,下一次就不再選取)
這裡寫圖片描述
在4*4的數字矩陣裡,以步長2*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的很經典的一個做影象識別的資料集,圖片大小是28*28的,需要先下載才能使用。MNIST資料集下載連結: https://pan.baidu.com/s/1d9ty82 密碼: jcam
  • x、y_現在都是用佔位符表示,當程式執行到一定指令,向x、y_傳入具體的值後,就可以代入進行計算了
  • shape=[None, 784]是資料維度大小——因為MNIST資料集中每一張圖片大小都是28*28的,計算時候是將28*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
  • 這裡的卷積核大小是5*5的,輸入的通道數是1,輸出的通道數是32
  • 卷積核的值這裡就相當於權重值,用隨機數列生成的方式得到
  • 由於MNIST資料集圖片大小都是28*28,且是黑白單色,所以準確的圖片尺寸大小是28*28*1(1表示圖片只有一個色層,彩色圖片都是3個色層——RGB),所以經過第一次卷積後,輸出的通道數由1變成32,圖片尺寸變為:28*28*32(相當於拉伸了高)
  • 再經過第一次池化(池化步長是2),圖片尺寸為14*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)                          
  • 這裡的卷積核大小也是5*5的,第二次輸入的通道數是32,輸出的通道數是64
  • 第一次卷積+池化輸出的圖片大小是14*14*32,經過第二次卷積後圖片尺寸變為:14*14*64
  • 再經過第二次池化(池化步長是2),最後輸出的圖片尺寸為7*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
  • 全連線層的輸入就是第二次池化後的輸出,尺寸是7*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%,越往後學習,準確率越高

特別提醒:由於我的電腦配置比較低,執行耗時較長,而且在儲存引數時候還會出現卡死情況,大家請注意。
—-—-—-—-—-—-—-—-—-—-—-—–—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
—-—-—-—-—-—-—-—-—-—-—-—–—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
結束語:自己也是通過學習前輩們的講解,自己慢慢摸索的,學習就是自我填坑的過程,希望我們都能堅持下來,也希望這篇能幫到你,朋友。

系列推薦: