自然語言菜鳥學習筆記(六):CNN(卷積神經網絡)理解與實現(TensorFlow)

目錄

卷積神經網絡結構圖

爲什麼從神經網絡到卷積神經網絡?

卷積

解決上述問題:

卷積過程

卷積多通道處理

池化

全連接層

使用TensorFlow框架簡單實現CNN(手寫數字識別)


卷積神經網絡結構圖

上圖就是一個典型的卷積神經網絡,卷積神經網絡 = 卷積 + 池化 + 全連接

爲什麼從神經網絡到卷積神經網絡?

普通的神經網絡,最經典的那種神經網絡模型圖也就是多個輸入,一層隱藏層,後面接輸出層。

那麼普通的神經網絡(BP神經網絡)在對圖像進行處理的時候,會像下圖一樣:

 

假設圖片是 1000 * 1000像素的,有一層隱藏層,隱藏層的大小是 10 ^ 6,那麼我們的參數大小有 10 ^ 12。

第一個問題:這種程度大小的參數對於我們機器的內存和計算量來說是巨大的,分分鐘扛不住。

第二個問題:除了硬件上的開銷太大,參數過多會導致過擬合,由於模型的參數太多,模型的表達能力太強,它會儘可能記住模型中的每個樣本

(過擬合:模型在訓練集上表現非常不錯,到了測試集就不能很好的預測)

 

卷積

解決上述問題:

卷積層兩大特點:

局部連接

對於一張圖片而言,也就是上面這張圖片,像素點和它周圍的像素點的值會比較有聯繫,比如紅色區域中,眼睛和眉毛的像素點的關聯非常大,而眼睛和綠色區域的嘴巴的關聯性不那麼大。所以圖片的數據有較強的區域性。所以我們可以將每個神經元和每個圖像特徵的全連接改成局部連接。

如上面那個圖顯示的一樣,每個神經元只和一部分的像素點進行連接,比如他這裏有 10 ^ 6個神經元,每個神經元只和 10 * 10的像素點進行連接,那麼就有 10 ^ 8 比起一開始的時候少很多。爲了不影響下面的理解這裏說細一點,也就是每一個神經元都是 10 * 10, 有10 ^ 6個神經元  10 * 10 * 10 ^ 6 = 10 ^ 8個參數,但此時每個神經元內的參數和其他神經元參數是不一樣的。

共享參數

圖像特徵和圖像位置無關:比如一個蘋果可能出現在相片的任何位置

比如所我們現在的目標是找一張圖片內的蘋果,因爲原本一個神經元會和整張圖片去做連接,也就是相當於,它會將整張圖片掃一遍,那麼是可以找到蘋果的,我們現在引出一個問題,就是我們現在每個神經元只和局部的 10 * 10 像素範圍的局部圖片做連接,那麼在這個範圍內,是有可能不包含蘋果的,那麼我們捕捉不到蘋果特徵。

這時候我們引入參數共享的概念:

讓每個神經元和局部圖片的連接都採用同樣的參數,也就是卷積核

有什麼好處:

假設用一個 10 * 10 的卷積核

1. 降低模型參數,每個神經元共享一個卷積核參數,那麼我們的 10 ^ 2的參數,和之前10 ^ 12個參數相比,降低了非常多。

2.

 

(畫的有點醜,見諒)

那麼我們的卷積核從圖片上滑過,從左到右,從上到下,每個區域都和卷積核計算出來一個值放在輸出的一個地方,每個值可以代表一個神經元,那麼一整張圖片我們不知道我們要捕捉的特徵在哪,那麼假設我們的特徵在上圖中的左上角,也就是藍色方框內,那麼我們在右側的輸出的左上角對應的那個神經元被激活,若是圖像特徵在橙色的區域,那麼我們右側的最上面的第二個神經元被激活,也就是不管圖像在哪,都會被卷積核捕捉放到某個神經元上。

這也就是爲什麼卷積層往往會有多個卷積核(甚至幾十個,上百個),因爲權值共享後意味着每一個卷積核只能提取到一種特徵,爲了增加CNN的表達能力,當然需要多個核

 

卷積過程

002928_hnHI_876354.gif       

好吧我從網上找了個很清晰的流程圖,讓大家可以更好的理解上圖的卷積核在右側

ok,那麼我們來看輸出的大小和原圖大小的關係: 輸出大小 = 圖片大小 - 卷積核大小 + 1 (上圖舉例: 3 = 5 - 3 + 1)

步長

控制每一次滑動在圖像上移動多少,上圖的步長就是1,也就是每一次往右移1,若步長是2的話就向右移2

 

問題:每次圖像都變小一層,對於實際運算的時候可能是比較麻煩的

爲了解決這個問題,我們可以在圖像的外層padding一圈0,讓他輸出的大小和原圖大小一致。

就向上圖這樣子,非常好理解就不解釋了

 

 

卷積多通道處理

上面講的都是卷積單通道的處理比如說灰度圖,每個像素由一個值表示,下面我們來研究一下卷積如何處理多通道

左側是一個多通道的圖, 我們將右側的藍色的卷積核也變爲多通道的,每個通道 5 * 5,每個通道的卷積的參數是不共享的,一般來說也可以描述成尺寸 5 * 5,深度爲3的filter。

接下來我們用這三個通道每個通道的卷積參數和相應通道的圖片輸入做內積,然後將卷積核三個通道對應得到的結果想加起來作爲輸出神經元的值,如下圖的過程

這樣子經過從左到右從上到下的卷積之後這一個卷積核就可以生成右側的這一張特徵圖,也就是一個通道的圖。那麼我們如何生成多個輸出神經元的圖呢:

多加幾個卷積核,這些卷積核參數不共享,那麼我們用六個卷積核就可以得到六個輸出圖。

卷積核其實用來提取某種特徵,我們採用多個卷積核就可以提取多個圖像特徵。

但是和卷積覈計算完內積之後這個計算並沒有結束,我們還需要使用激活函數:

卷積通常使用的激活函數是ReLu

 

池化

池化就是對特徵圖進行特徵的特徵壓縮,也叫做亞採樣,選擇一個區域的max值或者mean值來代替該區域,就實現了一個特徵壓縮的效果。

最大值池化——Max Pooling

這個過程也是有一個窗口類似卷積那種從左到右從上到下的滑動,有一點不一樣的是他的步長一般是和窗口的寬度一致的,若是滑動到右邊有多餘的列,有兩種處理方法:padding(上面介紹過)和捨棄,一般來說會採用捨棄。

max-pooling:取filter覆蓋區域的最大值

 

平均值池化

滑動過程和最大池化一致,只是取filter覆蓋區域的平均值

 

池化的特性

一、不重疊

設置的步長和窗口的寬度一直,窗口滑過是不會出現重疊的

二、減少圖像的尺寸減少計算量

三、可以解決平移魯棒

什麼是平移魯棒:

假設上面藍色區域是一個圖片,橙色區域是我們的圖片特徵,那麼我們的圖片特徵稍微的往旁邊挪動一點,就會變成下圖的樣子

那麼橙色區域和藍色區域通過最大池化的出來的結果可能是一樣的,那麼平移前和平移後的結果可能是一樣的,池化可以一定程度上解決這種平移問題。

四、降低了空間精度

因爲池化的過程會損失幾個值,所以一定程度上會損失一些精度,讓圖像特徵更具體,表達能力更好,減少不重要的特徵。

一定程度上防止過擬合。

 

全連接層

將上一層的輸出展開到每一個神經元上也就是普通的神經網絡層。

全連接層後面可以再接全連接層,但是不能再接池化,因爲已經不是多維的圖片信息了

 

使用TensorFlow框架簡單實現CNN(手寫數字識別)

手寫數字識別就是有非常多張黑白的手寫數字圖片,給CNN進行學習,然後從測試集上抽取幾張圖片輸入訓練完的模型中,訓練完的模型會給出它自己識別的數字是多少,和正確的結果進行比對並輸出準確率,這是一個基本流程,下面是流程圖:

最終想要的效果:

每一張手寫數字圖片都是 28 * 28 * 1,因爲不是RBG的

經過一層卷積,32個filters,變成 28 * 28 * 32。每個filter都會像手電筒一樣掃一遍原本的圖片,掃一遍就增加一層。掃了32遍

經過池化層max pooling變成 14 * 14 * 32,在經過第二層的卷積,64個過濾器,變成14 * 14 * 64 在經過一層pooling變成 7 * 7 * 64

扁平序列化變成一個 1 * 1 * 1024在經過一個全連接層的輸出

實現如下(逐行註釋好了):

# -*- coding: UTF-8 -*-

import tensorflow as tf

# 載入手寫數字庫[55000 * 28 * 28] 55000訓練圖像
from tensorflow.examples.tutorials.mnist import input_data

# 存到本地
mnist = input_data.read_data_sets('mnist_data', one_hot=True)

# ont-hot 獨熱碼的編碼形式,其實從字面意思很好理解就是,有一位是熱的,有一位是取一,其他都取零
# 0,1,2,3,4,5,6,7,8,9 的十位數字
# 0:1000000000  也就是第一位是1,後面同理

# None 表示張量(Tensor)的第一個維度可以是任意長度
# 除以255是爲了歸一化,把灰度值從[0, 255]變成[0, 1]區間,歸一化可以讓優化器更快更好的找到誤差最小值
input_x = tf.placeholder(tf.float32, [None, 28 * 28]) / 255
output_y = tf.placeholder(tf.int32, [None, 10])
input_images = tf.reshape(input_x, [-1, 28, 28, 1])     # 改變形狀

# 從Test(測試)數據集裏選取3000個手寫數字的圖片和對應的標籤
test_x = mnist.test.images[: 3000]  # 圖片
test_y = mnist.test.labels[: 3000]  # 標籤


# 構建卷積神經網絡
# 第一層卷積    input_images => 形狀 [ 28 * 28 * 1 ]
# conv2d   =>    二維的,2 dim
# [28 * 28 * 32]
conv_1 = tf.layers.conv2d(inputs=input_images,
                          filters=32,               # 32個過濾器
                          kernel_size=[5, 5],       # 過濾器大小是5 * 5
                          strides=1,                # 步長是1
                          padding='same',           # same 表示輸出大小不變,需要在外圍補零
                          activation=tf.nn.relu
                          # 激活函數
                          )


# 第一層池化(亞採樣)
# [14 * 14 * 32]
pool_1 = tf.layers.max_pooling2d(
    # [28 * 28 * 32]
    inputs=conv_1,
    # 過濾器 2 *  2
    pool_size=[2, 2],
    # 步長 2
    strides=2
)


# 第二層卷積    pool_1 => 形狀 [ 14 * 14 * 32 ]
# [14 * 14 * 64]
conv_2 = tf.layers.conv2d(inputs=pool_1,
                          filters=64,               # 64個過濾器
                          kernel_size=[5, 5],       # 過濾器大小是5 * 5
                          strides=1,                # 步長是1
                          padding='same',           # same 表示輸出大小不變,需要在外圍補零
                          activation=tf.nn.relu
                          # 激活函數
                          )

# 第二層池化
# [7 * 7 * 64]
pool_2 = tf.layers.max_pooling2d(
    # [14 * 14 * 64]
    inputs=conv_2,
    # 過濾器 2 *  2
    pool_size=[2, 2],
    # 步長 2
    strides=2
)

# 平坦化
flat = tf.reshape(pool_2, [-1, 7 * 7 * 64])

# 1024神經元全連接層
layer_dense = tf.layers.dense(
    # [7 * 7 * 64]
    inputs=flat,
    units=1024,
    activation=tf.nn.relu
)


# Dropout : 0.5
dropout = tf.layers.dropout(
    inputs=layer_dense,
    rate=0.5
)

# 10個神經元的全連接層,不用激活函數做非線性化

logits = tf.layers.dense(
    inputs=dropout,
    units=10
)

# 計算loss : Cross_entropy(交叉熵)
# 再用softmax計算百分比輸出
loss = tf.losses.softmax_cross_entropy(onehot_labels=output_y, logits=logits)

# 用Adam Optimizer 最小化誤差, 學習率 0.001
train_op = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)

# 計算正確率
accuracy = tf.metrics.accuracy(labels=tf.argmax(output_y, axis=1), predictions=tf.argmax(logits, axis=1))[1]

# 創建會話
sess = tf.Session()
# 初始化全局和局部變量
init = tf.group(tf.global_variables_initializer(), tf.local_variables_initializer())
sess.run(init)


# 我們拿一個epoch來跑一遍  20000 * batch_size(50)
for _ in range(20000):
    # 拿下一個batch的信息
    batch = mnist.train.next_batch(50)
    # run一下loss和優化器, 把之前的兩個佔位符分別填圖片和標籤
    train_loss, _train_op = sess.run([loss, train_op], {input_x: batch[0], output_y: batch[1]})
    # 每一百個batch輸出一下
    if _ % 100 == 0:
        # 測試集上的準確率
        # 之前測試集是會取出3000個樣本的
        test_accuracy = sess.run(accuracy, {input_x: test_x, output_y: test_y})
        # 輸出訓練集損失和測試集準確率
        print("%.3f percentage, Train loss = %.3f, (Test accuracy: %.3f)" % (_ / 20000 * 100, train_loss, test_accuracy))


可以觀察到訓練過程是逐漸收斂的,訓練好的模型實際效果就不展示了。

 

 

本菜鳥學習不好,如有不妥望各位大佬指點

如要轉載請說明原文:https://blog.csdn.net/qq_36652619/article/details/89437256

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