1. 程式人生 > >CNN是如何一層一層'理解'影象資訊的

CNN是如何一層一層'理解'影象資訊的

卷積神經網路CNN

  CNN是一種至少包含一層卷積層的神經網路模型,卷積層的目的是將卷積核應用到影象張量的所有點上,並通過將卷積核在輸入張量上滑動生成經過濾波處理的張量.簡單的CNN架構通常會包含卷積層,池化層,非線性變換層,全連線層.通過這些層網路會被填充大量的資訊,因此模型便可以進行復雜的模式匹配.
本文主要是通過TF1.5這個工具,展示CNN中各個層對影象資訊的加工過程.
主要內容:

import tensorflow as tf
import matplotlib.pyplot as plt

1.tensor(張量)

# 影象經過解碼後是一張量的形式表示
# 定義一個包含兩個張量[影象]的常量 image_batch = tf.constant([ [ [[0, 255, 0], [0, 255, 0], [0, 255, 0]], [[0, 255, 0], [0, 255, 0], [0, 255, 0]] ], [ [[0, 0, 255], [0, 0, 255], [0, 0, 255]], [[0, 0, 255], [0, 0, 255], [0, 0, 255]] ] ])

檢視張量尺寸資訊

image_batch.get_shape()
TensorShape([Dimension(2), Dimension(2), Dimension(3), Dimension(3)])
TensorShape資訊:兩個張量,高為2個畫素,寬為3個畫素,顏色空間為RGB
# 訪問第一副影象的第一個畫素點
sess = tf.Session()
sess.run(image_batch)[0][0][0]
array([  0, 255,   0], dtype=int32)
sess = tf.Session()

TF載入影象並解碼

# 載入一張彩色影象
image_filename = 'bcd.jpg'
# 此處可能會出現編碼錯誤,因為系統預設uhf-8編碼,檔案可能不是該編碼方式-'rb'
image_file = tf.gfile.FastGFile(image_filename,'rb'
).read() # 解碼 image = tf.image.decode_jpeg(image_file) sess.run(image)

影象解碼後是一個三階的張量,每個元素為影象畫素點的值

array([[[243, 245, 224],
        [242, 244, 223],
        [242, 243, 225],
        ...,
        [225, 227, 222],
        [225, 227, 222],
        [225, 227, 222]],

       [[242, 244, 223],
        [242, 244, 223],
        [242, 243, 225],
        ...,
        [225, 227, 222],
        [225, 227, 222],
        [225, 227, 222]],

       [[241, 243, 222],
        [241, 243, 222],
        [240, 241, 223],
        ...,
        [225, 227, 222],
        [225, 227, 222],
        [225, 227, 222]],
    ...,
       ...,
       ...,
        [217, 183, 146],
        [217, 183, 146],
        [216, 182, 145]]], dtype=uint8)
plt.imshow(sess.run(image))
plt.show()

     重繪影象

         這裡寫圖片描述

卷積層

卷積是CNN的重要組成,通常也是網路的第一層,下面主要介紹卷積層對影象的一些操作

# 在TensorFlow中卷積運算通過tf.nn.conv2d()完成.同時TF還提供其特定的卷積運算.
# 定義一個張量,簡單實現卷積運算
tensor_1 = tf.constant([
    [
        [[0.0],[1.0]],
        [[2.0],[3.0]]
    ],
])

# 卷積核
kernel_1 = tf.constant([
    [
        [[1.0, 2.0]]
    ]
])
# 張量的shape
tensor_1.get_shape()
# TensorShape[張量數量,張量高度,張量寬度,通道數]
TensorShape([Dimension(1), Dimension(2), Dimension(2), Dimension(1)])
# kernel_1
kernel_1.get_shape()
TensorShape([Dimension(1), Dimension(1), Dimension(1), Dimension(2)])
# 卷積後,得到一個新張量(特徵圖)
conv2d_1 = tf.nn.conv2d(tensor_1, kernel_1 ,strides=[1, 1, 1, 1],
                                                  padding='SAME')
sess.run(conv2d_1)
array([
    [
        [[0.0, 0.0], [1.0, 2.0]],
        [[2.0, 4.0], [3.0, 6.0]]
    ]
])

卷積後得到一個和原張量大小相同,通道數為2,與卷積核相同

conv2d_1.get_shape()
TensorShape([Dimension(1), Dimension(2), Dimension(2), Dimension(2)])

訪問tensor_1卷積前後相同的位置畫素值,瞭解下卷積運算時,畫素值是如何變化的

sess.run(tensor_1)[0][1][1],sess.run(conv2d_1)[0][1][1]
(array([3.], dtype=float32), array([3., 6.], dtype=float32))

 
  tensor_1中的每個畫素值,分別與卷積核不同通道(維度)的值相乘得到新的畫素值,然後對映到特徵圖的相應的通道(維度)中:3.0*1.0, 3.0*2.0,這個例子仍不能直觀感受到卷積是如何如何,且往下看.
 

strides(跨度)

  卷積的價值在對影象資料的降維能力,有利於CNN模型減少在影象學習上消耗的時間,降維是通過修改卷積核的strides引數來實現,簡單來說就是,通過改變卷積核在原始影象上移動的步長,來減少網路掃描影象的時間.在tf.nn.conv2d()中可以通過修改strides引數,來改變卷積核'滑動'的方式,使得卷積核可以跳過某些畫素.
比如,一個影象的高度為5(畫素),寬度為5,通道為1(1x5x5x1),卷積核的大小是(3x3x1)

# tensor_2 [1x5x5x1]
tensor_2 = tf.constant([
    [
        [[2.0], [1.0], [0.0], [2.0], [3.0]],
        [[9.0], [5.0], [4.0], [2.0], [0.0]],
        [[2.0], [3.0], [4.0], [5.0], [6.0]],
        [[1.0], [2.0], [3.0], [1.0], [0.0]],
        [[0.0], [4.0], [4.0], [2.0], [8.0]],
    ],
])

# 卷積核[3x3x1x1]
kernel_2 = tf.constant([
    [
        [[-1.0]], [[0.0]], [[1.0]]
    ],
    [
        [[-1.0]], [[0.0]], [[1.0]]
    ],
    [
        [[-1.0]], [[0.0]], [[1.0]]
    ]
])

注意!在卷積核中,張量的四個維度表示的數值,代表:卷積核的高,寬,通道和數量,其實可以這樣理解:是用3個高為3,寬為1,通道為1的張量.表示一個,高為3,寬為3,通道為1,數量為1的卷積核

tensor_2.get_shape()
TensorShape([Dimension(1), Dimension(5), Dimension(5), Dimension(1)])
kernel_2.get_shape()
TensorShape([Dimension(3), Dimension(3), Dimension(1), Dimension(1)])
# 卷積,strides,padding(下面會介紹)
# padding = 'VALID',輸入會比輸入尺寸小
conv2d_2 = tf.nn.conv2d(tensor_2, kernel_2, strides =[1,1,1,1],
                        padding='VALID')
sess.run(conv2d_2)

卷積後得到的特徵張量

array([
    [
     [[-5.],[ 0.],[ 1.]],
     [[-1.],[-2.],[-5.]],
     [[ 8.],[-1.],[ 3.]]
    ],
], dtype=float32)

kernel_2每次移動時,都會與tensor_2位置重疊的部分對應的值相乘,然後將乘積相加得到卷積的結果.卷積就是通過這中逐點相乘的方式將兩個輸入整合在一起.下圖更直觀展示卷積.
-1*1 + 0*0 + 1*2+
-1*5 + 0*4 + 1*2+
-1*3 + 0*4 + 1*5 = 0

    這裡寫圖片描述

strides,padding

strides:

引數的格式和tensor_2張量相對應
image_batch_size_stirde,image_height_stride,image_width_stride,image_channels_stirde
一般通過修改中間兩個引數,改變卷積核在tensor高和寬兩個方向上的移動步長,在卷積過程中跳過一些畫素點

padding:’SAME’OR’VALID’

在卷積過程中會遇到這種情況:卷積核未到達影象的邊界時,下一步的移動會越過影象的邊界.
針對這種情況,tensorflow提供的措施是對超出影象邊界的部分進行填充,即邊界填充.
SAME:當卷積核超出影象邊界時,會進行0填充,卷積的輸出與輸入相同
VALID:考慮卷積核的尺寸,儘量不越過影象邊界.卷積輸出小於輸入

張量的資料格式

  tf.nn.conv2d()輸入的張量的格式不是固定的可以自定義,通過修改data_format,預設引數是’NHWC’,N:batch_size; H:張量高度; W:寬;C:通道數
 
繼續討論卷積:

  在計算機視覺中,卷積常用於識別影象中的重要特徵,下面使用一個專為邊緣檢測的卷積核,來突出影象中的邊緣.不同的卷積核會突出影象中的不同模式,得到不同的結果.
 

# 邊緣檢測卷積核
kernel_3 = tf.constant([
    [
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]
    ],
    [
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[ 8., 0., 0.], [0.,  8., 0.], [0., 0.,  8.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]
    ],
    [
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]
    ]
])

原圖

# 卷積前影象
plt.imshow(sess.run(image))
plt.show()

      這裡寫圖片描述

原圖尺寸

sess.run(image).shape
(393, 700, 3)

邊緣檢測

# uint8轉化為浮點型
image = tf.to_float(image)
# 轉化為一個四維張量
conv2d_3 = tf.nn.conv2d(tf.reshape(image,[1,393,700,3]), kernel_3,
                        [1,1,1,1], padding='SAME')

卷積後圖像的大小

conv2d_3.get_shape()
TensorShape([Dimension(1), Dimension(393), Dimension(700), Dimension(3)])

卷積後的影象

# tf.minimum()和tf.nn.relu()是將卷積後的畫素值保持在RGB顏色值的合法範圍內[0,255]
conved_image = tf.minimum(tf.nn.relu(conv2d_3), 255)
plt.imshow(sess.run(conved_image).reshape(393,700,3))
plt.show()

       這裡寫圖片描述

 
影象銳化效果
 通過卷積核增加捲積核中心位置的畫素灰度,降低周圍畫素的灰度.

# 銳化卷積核
kernel_4 = tf.constant([
    [
        [[ 0., 0., 0.], [0.,  0., 0.], [0., 0.,  0.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[ 0., 0., 0.], [0.,  0., 0.], [0., 0.,  0.]]
    ],
    [
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[ 5., 0., 0.], [0.,  5., 0.], [0., 0.,  5.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]
    ],
    [
        [[ 0., 0., 0.], [0.,  0., 0.], [0., 0.,  0.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[ 0., 0., 0.], [0.,  0., 0.], [0., 0.,  0.]]
    ]
])

# 卷積
conv2d_4 = tf.nn.conv2d(tf.reshape(image,[1,393,700,3]), kernel_4,
              [1,1,1,1], padding='SAME')
sharpen_image = tf.minimum(tf.nn.relu(conv2d_4), 255)
# 通過索引訪問第一個張量,就不用對影象張量進行resize操作了
plt.imshow(sess.run(sharpen_image)[0])
plt.show()

        這裡寫圖片描述

 
  特定的卷積核能夠匹配特定的畫素模式,在CNN中模型通過大量的訓練能夠自動學習到更復雜的卷積核,不但能夠匹配邊緣,而且還能匹配更加複雜的模式.卷積核的初始值通常隨即設定,它會隨著訓練的迭代自動更新.當完成一輪訓練後,模型會繼續'閱讀'一副影象,並將其與卷積核進行卷積等等一系列計算,根據得到的結果與影象真實的標籤對比.推卷積核進行調整.直到模型收斂(或達到理想的準確率)對於影象識別和分類任務,通常是使用不同的層支援某個卷基層,不是單一使用一個卷積層.這樣有助於減少過擬合,加速訓練過程減少記憶體佔用率.
 

啟用函式

  啟用函式通常搭配卷積層和全連線層使用,對它們輸出的特徵(張量)進行'啟用',其實就是對運算的結果進行平滑處理.目的是為網路引入非線性.非線性意味著輸入輸出直接的關係是一條曲線.曲線能夠反映輸入中更復雜的變化,比如:在 大部分點值很小,但在某個單點會週期性地出現極值的輸入.引入非線性可以使網路對資料中發現更復雜的模式進行訓練.TensorFlow提供了多種啟用函式.他們的共同點是:單調,可微分.只要滿足這兩點就可以作為啟用函式

1.tf.nn.relu()

  ReLU是分段線性的,輸入為正時,輸出=輸入,輸入為負時,輸出=0,取值範圍[0,+oo]優點:不受梯度消失的影響;缺點:學習效率較大時,易受飽和神經元的影響, 梯度消失:每一層神經元對上一層的輸出的偏導乘上權重結果都小於1的話即使這個結果是0.99,在經過足夠多層傳播之後,誤差對輸入層的偏導會趨於0

2.tf.sigmoid()

  sigmoid函式的返回值在[0.0, 1.0]之間,輸入的數值較大時,返回的結果就越接近1.0,較小的數值返回結果就越接近0.0, 對於真實輸出位於[0,1]之間的資料來說使用sigmoid()將輸出保持在很小範圍很有用,但是對於輸入接近飽和或者變化劇烈的資料上使用sigmoid()壓縮輸出範圍往往會有不利的影響,比如:梯度消失

3.tf.tanh()

  與sigmoid()接近,區別在於tanh將輸出的值對映在[-1,1]之間.

4.tf.nn.dropout()

  dropout函式其實不太算是啟用函式,它並不對輸入的值進行非線性變換,只是根據一個概率引數將部分的輸出值置為0.0,這樣做的目的是為訓練中的模型引入少量的隨機噪聲,防止模型在學習過程中出現過擬合(僅在訓練階段)

函式影象

features = tf.to_float(tf.range(-20,20))
# Relu
plt.plot(sess.run(features), sess.run(tf.nn.relu(features)))
plt.grid(True)
plt.title('Relu')
plt.show()

        這裡寫圖片描述

# sigmoid
plt.plot(sess.run(features), sess.run(tf.sigmoid(features)))
plt.grid(True)
plt.title("Sigmoid")
plt.show()

        這裡寫圖片描述

# tanh
plt.plot(sess.run(features), sess.run(tf.tanh(features)))
plt.title("Tanh")
plt.grid(True)
plt.show()

       這裡寫圖片描述

# drop
plt.scatter(sess.run(features), sess.run(tf.nn.dropout(features, keep_prob=0.5)))
plt.title("Dropout")
plt.grid(True)
plt.show()

        這裡寫圖片描述

池化層

  池化層通過對輸入張量的尺寸進行壓縮,來對輸入進行降取樣,保留重要的特徵,也能有效減少過擬合,在卷積的過程中'VALID'模式下也能減小輸入張量的尺寸,但是池化效果更明顯.池化層的輸入通常是卷積層的輸出.
  池化分兩種: 1. 最大池化, 2. 平均池化
 
max_pool()
   tf.nn.max_pool()除了不進行卷積運算,尋找最大畫素點的過程和卷積是很相似的,不過kernel不是一個張量,而是一個接受域的範圍[batch_size,imput_height,input_width,channels],即在這個範圍內尋找最大的值輸出.

# 輸入張量
input_tensor = tf.constant([
    [
        [[1.0], [1.0], [2.0], [4.0]],
        [[5.0], [6.0], [7.0], [8.0]],
        [[3.0], [2.0], [1.0], [0.0]],
        [[1.0], [2.0], [3.0], [4.0]]
    ]
])
kernel_pool = [1, 2, 2, 1]
strides = [1, 2, 2, 1] # 跟卷積作用相同
max_pool = tf.nn.max_pool(input_tensor, kernel_pool, strides, 'VALID')
sess.run(max_pool)

最大池化結果

array([
    [
        [[6.],[8.]],
        [[3.],[4.]]
    ]
], dtype=float32)

從2x2的接受域,四個畫素中選出值最大的保留輸出,張量尺寸也縮小了一半:
          這裡寫圖片描述
          
avg_pool()

# 平均池化
avg_pool = tf.nn.avg_pool(input_tensor, kernel_pool, strides, 'VALID')
sess.run(avg_pool)

平均池化輸出

array([
    [
        [[3.25],[5.25]],
        [[2.],[2.]]
    ]
], dtype=float32)

 從2x2的接受域中,計算四個畫素平均值保留輸出:3.25 = (1.0+1.0+5.0+6.0)/4
 

下面在彩色影象上進行池化操作,觀察對影象輸出的影響,一般在CNN是不會直接對影象進行池化操作的.

# 下面分別對這副影象進行最大和平均池化
image_file2 = tf.gfile.FastGFile('123.jpg','rb').read()
image_2 = tf.image.decode_jpeg(image_file2)
plt.imshow(sess.run(image_2))
plt.show()

         這裡寫圖片描述

影象尺寸(873, 1070, 3)
最大池化

max_pool_image = tf.nn.max_pool(tf.reshape(image_2,[1,873,1070,3]),
                       [1,6,6,1],[1,5,5,1],'VALID')
plt.imshow(sess.run(max_pool_image)[0])
plt.show()

         這裡寫圖片描述

池化後圖像大小

max_pool_image
<tf.Tensor 'MaxPool_8:0' shape=(1, 174, 214, 3) dtype=uint8>

平均池化

# 平均池化層輸入張量為浮點型資料
avg_pool_image = tf.nn.avg_pool(tf.to_float(tf.reshape(image_2,[1,873,1070,3])),
                                [1,1,2,1],[1,1,1,1],'VALID')
plt.imshow(sess.run(avg_pool_image)[0])
plt.show()

        這裡寫圖片描述

增加滑動步長和接受域

# 平均池化層輸入張量為浮點型資料
avg_pool_image_1 = tf.nn.avg_pool(tf.to_float(tf.reshape(image_2,[1,873,1070,3])),
                                [1,2,2,1],[1,2,2,1],'VALID')
plt.imshow(sess.run(avg_pool_image_1)[0])
plt.show()

        這裡寫圖片描述

不管是最大池化還是平均池化,影象的輪廓都會隨著接受域和滑動'步長'的增加越來越模糊,平均池化影象輪廓消失的最快.
 
  在CNN中有不止一個卷積層和池化層,對影象的主要特徵進行多輪的處理,最後通常連線一個全連線層.跟普通的BP神經網路一樣.