從代碼學AI——卷積神經網絡(CNN)

今天和大家一起來看下基於TensorFlow實現CNN的代碼示例,源碼參見Convolutional_netWork。使用MNIST數據集進行訓練和預測,下面開始代碼註解。

'''
這裏從源碼的角度詳細說明使用TensorFlow如何簡單的實現卷積神經網絡

使用MNIST數據集進行卷積神經網絡的實現

使用MNIST數據集

數據集參見:http://yann.lecun.com/exdb/mnist/)

源碼地址: 

    https://github.com/aymericdamien/TensorFlow-Examples/blob/master/examples/3_NeuralNetworks/convolutional_network.py

'''

導入相關方法和模塊

from __future__ import print_function

import tensorflow as tf

# 導入 MNIST 數據
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)

基本參數設置

# 參數設置
#學習速率(也有稱爲步長的)
learning_rate = 0.001
#迭代的次數
training_iters = 200000
#每個批次的數據大小(這裏表示每個批次使用128張圖片)
batch_size = 128
#每10次進行一次結果輸出
display_step = 10

#網絡相關參數
#輸入數據,每個待處理圖片是28*28的矩陣,可以理解爲將其拉平則變爲一個長度爲784的數組
n_input = 784 
#分類數量,最終是解決分類問題,這裏分爲10類,卷積神經網絡也只是替代傳統機器學習中特徵提取的這個過程
n_classes = 10
#隨機失活概率,也有叫留存率的,即在dropout層,以這個概率進行隨機失活,進而減少過擬合
dropout = 0.75

基本輸入的佔位

# 圖的輸入參數定義,TensorFlow中會將各個Option編排成一個DAG(有向無環圖)
# 這裏定義輸入,可以理解爲採用佔位符進行站位,待實際運行時進行具體填充,這裏None表示運行時指定
# [參見]tf.placeholder使用
x = tf.placeholder(tf.float32, [None, n_input])
y = tf.placeholder(tf.float32, [None, n_classes])
#失活概率
keep_prob = tf.placeholder(tf.float32)

定義卷積神經網絡

# 該函數實現卷基層的定義
# 在卷積神經網絡中,往往存在多層卷基層,這裏將卷積的定義當初抽出來
# 實現很簡單,即採用卷積核(或者理解成濾波器)W對待處理數據x進行卷積計算(理解成在圖像上平移過濾,當然也會處理深度的問題)
# 在每個窗口都可以理解成進行窗口內的WX+b這樣的計算(這裏的X是窗口內的數據,W是卷積核)
def conv2d(x, W, b, strides=1):
    #進行卷積計算,padding = 'SAME'表示處理後輸出圖像大小不變,如果valid則是變動的
    #一般在多層卷積中,往往會控制圖像大小不變,否則處理起來比較困難,每層處理都要考慮圖像大小
    #[參見]卷積計算
    x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1], padding='SAME')
    x = tf.nn.bias_add(x, b)
    #對結果進行非線性激活
    #[參見]激活函數
    return tf.nn.relu(x)

定義池化操作

# 進行池化操作,這裏進行最大池化
# 池化可以達到降低數據量、利用更過的局部信息的目的
# 這裏k默認爲2,移動的步長是2,那麼圖像處理後相當於減小一半
# [參見]池化操作
def maxpool2d(x, k=2):
    return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, k, k, 1],
                          padding='SAME')

構建卷積神經網絡

# 進行卷積神經網絡的構建
# 這裏的網絡層級關係爲:卷積(Conv)->池化(Pooling)->卷積(Conv)->池化(Pooling)->全連接(包含非線性激活,FullConnect)->隨機失活(Dropout)->(Class Pre)分類
def conv_net(x, weights, biases, dropout):

    #可以簡單理解爲,將輸入數據還原爲28*28的矩陣
    #輸入x爲[128,784] 經過reshape後變爲 [128,28,28,1]的tensor(依次爲[batchSize,width,height,channel])
    x = tf.reshape(x, shape=[-1, 28, 28, 1])

    #卷積層1
    #卷積核爲[5,5,1,32]([width,height,input_channel,out_channel])即採用5*5的卷積核,輸入圖像的通道是1,輸出通道是32
    #[128,28,28,1]的輸入,採用[5,5,1,32]的filter,得到[128,28,28,32]輸出
    conv1 = conv2d(x, weights['wc1'], biases['bc1'])
    #池化層 池化後得到[128,14,14,32]的輸出
    conv1 = maxpool2d(conv1, k=2)

    #卷積層2
    #上面的結果是32通道的,所以這裏的卷積核採用[5,5,32,64],輸出通道是64
    #卷積計算後輸出[128,14,14,64]
    conv2 = conv2d(conv1, weights['wc2'], biases['bc2'])

    #池化層 輸出[128,7,7,64]
    conv2 = maxpool2d(conv2, k=2)

    # 全連接層
    # reshape後數據變成[128,7*7*64]
    fc1 = tf.reshape(conv2, [-1, weights['wd1'].get_shape().as_list()[0]])
    #矩陣乘法 [128,7*7*64] X [7*7*64,1024] -> [128,1024]
    fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
    #非線性激活,這裏採用relu
    fc1 = tf.nn.relu(fc1)

    #失活層
    #對結果進行隨機失活
    fc1 = tf.nn.dropout(fc1, dropout)

    # 分類 實際上就是做WX+b 這個運算
    #[128,1024]X[1024,10] ->[128,10] 即得到每個圖片在10個分類中的得分
    # 如某個圖片分類結果是[0.1, 0.0, 0.01, 0.3, 0.8, 0.2, 0.1, 0.1, 0.0, 0.2],那麼最大的0.8這一列表示的類別即爲預測類別
    out = tf.add(tf.matmul(fc1, weights['out']), biases['out'])
    return out

權重等相關參數

weights = {
    # 第一層的過濾器,採用[5,5,1,32]的filter輸出通道是32
    'wc1': tf.Variable(tf.random_normal([5, 5, 1, 32])),
    # 第二層的過濾器,輸入通道是32,輸出通道是64
    'wc2': tf.Variable(tf.random_normal([5, 5, 32, 64])),
    # 全連接層,輸入數據是7*7*64,輸出是1024
    'wd1': tf.Variable(tf.random_normal([7*7*64, 1024])),
    # 分類,權重矩陣是[1024,10],輸出10個分類
    'out': tf.Variable(tf.random_normal([1024, n_classes]))
}

biases = {
    'bc1': tf.Variable(tf.random_normal([32])),
    'bc2': tf.Variable(tf.random_normal([64])),
    'bd1': tf.Variable(tf.random_normal([1024])),
    'out': tf.Variable(tf.random_normal([n_classes]))
}

定義優化(迭代)以及準確率相關

#構建網絡
pred = conv_net(x, weights, biases, keep_prob)

#定義優化目標(損失函數,這裏採用交叉熵)
#[參見] 交叉熵 https://www.tensorflow.org/api_docs/python/tf/nn/softmax_cross_entropy_with_logits
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=y))
#採用Adam方法進行優化
#[參見]優化方法
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

#進行效果評估
#看預測值和實際值是否一致
correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))

#計算準確率
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

開始進行循環迭代

#下面開始構建計算圖
# 首先,開始初始化所有的變量
init = tf.global_variables_initializer()
# 然後,開始準備構建圖
with tf.Session() as sess:
    sess.run(init)
    step = 1
    while step * batch_size < training_iters:
        #獲得批次內的訓練數據
        batch_x, batch_y = mnist.train.next_batch(batch_size)

        # 進行batch優化
        sess.run(optimizer, feed_dict={x: batch_x, y: batch_y,
                                       keep_prob: dropout})
        #每10次輸出一下結果
        if step % display_step == 0:
            # 計算當前批次內的損失值和準確率
            loss, acc = sess.run([cost, accuracy], feed_dict={x: batch_x,
                                                              y: batch_y,
                                                              keep_prob: 1.})
            print("迭代次數 " + str(step*batch_size) + ", 當前損失值= " + \
                  "{:.6f}".format(loss) + ",  Accuracy= " + \
                  "{:.5f}".format(acc))
        step += 1
    print("優化完成!")

    # 進行準確率測試,這裏使用256張測試圖片
    print("測試準確率:", \
        sess.run(accuracy, feed_dict={x: mnist.test.images[:256],
                                      y: mnist.test.labels[:256],
                                      keep_prob: 1.}))

看完整個代碼,其實從結構和代碼數量上,已經很簡潔了,相比之前的基於TFLearn的LSTM麻煩了一點,也多出來了一些概念,這裏會重點對卷積、池化、失活做重點解釋,但不會太涉及公式相關,只做物理層面的解釋和方法註釋。


卷積

本節的重點在於:
1.理解相關的函數定義,不會重點闡釋卷積的意義,而是重點對函數定義進行解釋;
2.如何快速計算卷積後的shape

簡單不負責任的對於卷積進行一個物理意義上的解釋即:
一個函數(如:單位響應)在另一個函數(如:輸入信號)上的加權疊加
即卷積的物理意義可以理解爲:加權疊加

tensorflow的api_guide中對卷積操作的定義如下

The convolution ops sweep a 2-D filter over a batch of images, 
applying the filter to each window of each image of the appropriate size. 
The different ops trade off between generic vs. specific filters。

個人翻譯如下:
卷積操作實際上是用卷積核在圖像上進行掃描過濾,將卷積核應用到適當大小的圖片的每個窗口。tensorflow提供了不同的卷積實現,不同的實現在通用性和特定功能之間做取捨。

TensorFlow中提供了conv2d,depthwiseconv2d,separableconv2d 三個Op:

a. conv2d: 可以將通道混合在一起的任意濾波器

b. depthwise_conv2d: 在每個通道上獨立工作的濾波器

c. separable_conv2d: 深度方向的空間濾波器,後跟一個點濾波器。

在示例代碼中使用了conv2d,所以這裏只重點介紹conv2d

tf.nn.conv2d

conv2d(
    input,
    filter,
    strides,
    padding,
    use_cudnn_on_gpu=None,
    data_format=None,
    name=None
)

對4維的輸入和卷積核進行二維的卷積操作。

input:給定輸入tensor,輸入數據的形式爲[batch,in_height,in_width,in_channels]即[批次大小,圖像高度,圖像寬度,通道數量]。

filter:給定卷積核的tensor,形式爲[filter_height,filter_width,in_channels,out_channels]即[卷積核的高度,卷積核的寬度,輸入通道數,輸出通道數量]

注意這裏input的in_channels和filter中的in_channels是一樣的。

進行二維卷積操作大致流程如下:

  1. 將卷積核的維度轉換成一個二維的矩陣形狀 [filter_height * filter_width * in_channels, output_channels]

  2. 對於每個批處理的圖片,我們將輸入張量轉換成一個虛擬的數據維度 [batch, out_height, out_width, filter_height * filter_width * in_channels]

  3. 對於每個批處理的圖片,我們右乘以卷積核,得到最後的輸出結果

參數說明
input:輸入Tensor,4維且數據類型必須爲(half, float32, float64)中之一. tensor中每個維度表示的含義和data_format相關

filter: 代表卷積核的定義的tensor,數據類型和input一致。4維張量,每個維度表示的意義爲[filter_height, filter_width, in_channels, out_channels]

strides: 一個長度是4的一維整數類型數組,每一維度對應的是 input 中每一維的對應移動間隔,比如,strides[1] 對應 input[1] 的移動間隔,通常每個維度的移動間隔相同且置爲1.


padding: 一個取值爲 "SAME"或"VALID"的字符串. 決定邊緣填充的算法

use_cudnn_on_gpu: 可選參數,默認爲True,表示啓用GPU


data_format: 一個取值爲 "NHWC"或 "NCHW"的可選變量,默認爲"NHWC". 如果爲"NHWC"則輸入數據每個維度表示的意思爲[batch, height, width, channels]. 否則表示爲[batch, channels, height, width].

name: 可選變量,表示當前OP的名稱.

實例說明

實例代碼如下:

import numpy as np
import tensorflow as tf

input_data = tf.Variable( np.random.rand(1,6,6,3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(3, 3, 3, 1), dtype = np.float32)

y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'VALID')

with tf.Session() as sess:
    init = tf.initialize_all_variables();
    sess.run(init);
    print (sess.run(y));
    print (sess.run(tf.shape(y)));

輸出爲:

#卷積結果
[[[[ 6.2532773 ]
   [ 6.09991837]
   [ 4.29529333]
   [ 5.97482729]]

  [[ 5.49180269]
   [ 4.88662767]
   [ 4.4858675 ]
   [ 4.94966698]]

  [[ 5.19242811]
   [ 6.68614483]
   [ 6.02896595]
   [ 5.76866436]]

  [[ 6.8271122 ]
   [ 6.84668255]
   [ 6.29273748]
   [ 5.54653883]]]]

#卷積結果的shape
[1 4 4 1]

如果Padding改爲SAME則結果爲

[[[[ 3.13691545]
   [ 4.51995659]
   [ 4.12897968]
   [ 3.91766047]
   [ 4.65173292]
   [ 3.75577402]]

  [[ 4.15843153]
   [ 6.25889444]
   [ 6.30766678]
   [ 6.38891935]
   [ 7.19078541]
   [ 4.80262566]]

  [[ 4.68451309]
   [ 6.82779837]
   [ 6.46818972]
   [ 5.57897949]
   [ 6.88392735]
   [ 4.24736738]]

  [[ 4.93537664]
   [ 7.15243053]
   [ 5.66338301]
   [ 4.92590523]
   [ 5.60546398]
   [ 3.04639673]]

  [[ 4.68692017]
   [ 6.24736881]
   [ 5.82442379]
   [ 3.48279071]
   [ 3.68962264]
   [ 2.33093119]]

  [[ 2.54168987]
   [ 3.39097095]
   [ 3.19667697]
   [ 2.36519623]
   [ 2.3597281 ]
   [ 1.1976347 ]]]]

    [1 6 6 1]

從上面的例子可以看出當Padding參數爲valid的時候,卷積核移動到邊緣的時候,不採用填充算法,直接終止,那麼輸出結果的寬高爲input_width - filter_width+1,input_height-filter_height+1(此時stride[1]=stride[2]=1)即(6-3+1,6-3+1) = (4,4)

當strides改爲[1,2,2,1]時結果爲

[[[[ 5.08089352]
   [ 5.5374465 ]]

  [[ 5.4308424 ]
   [ 6.23833847]]]]

[1 2 2 1]

所以輸出的寬高(padding=valid時),計算公式爲

(input_width - filter_width + 1)/strides[1],(input_height - filter_height + 1)/strides[2]

重點

這裏需要強調2點:

a. 1x1的filter是有意義的,這個在以後,我會再解釋一下

b.padding的意義在於不去處理圖像size的問題,如果網絡中處理的Image的size總是變來變去,那麼網絡在設計和處理時會很麻煩,所以一般會選擇SAME


池化(Pooling)

按照TensorFlow的API文檔解釋,池操作對輸入張量進行矩形窗口掃描並計算的縮減操作(即計算窗口內的平均值、最大值、帶參數的最大值)。

一般來說池化層往往跟在卷積層後面,通過avg_pool或者max_pool的方法將之前卷基層得到的特徵做一個聚合統計。假設前一層的卷積層得到的某一特徵圖是500 * 500,那麼如果選擇一個2 * 2的窗口做池化,那麼池化層會輸出250 * 250的圖(窗口不重疊),這就達到了降低數據量的目的。

換一種解釋,一般來說圖像具有一種“靜態性”的屬性,這也就意味着在一個圖像區域有用的特徵極有可能在另一個區域同樣適用。例如,卷積層輸出的特徵圖中兩個相連的點的特徵通常會很相似,假設a[0,0],a[0,1],a[1,0],a[1,1]都表示顏色特徵是紅色,沒有必要都保留作下一層的輸入。池化層可以將這四個點做一個整合,輸出紅色這個特徵。可以達到降低模型的規模,加速訓練的目的。

TensorFlow中對pooling相關的函數有如下六個:

  1. tf.nn.avg_pool

  2. tf.nn.max_pool

  3. tf.nn.max_pool_with_argmax

  4. tf.nn.avg_pool3d

    進行3D平均池化,後面不詳細介紹

  5. tf.nn.fractional_avg_pool

    在池化區域執行局部平均池化,目前只支持在row和column上進行局部平均池化,需要分別制定在row和column上的比例,在這兩個方向上進行獨立的平均池化操作,後面不詳細介紹。

  6. tf.nn.fractional_max_pool

    在池化區域執行局部最大池化,目前只支持在row和column上進行局部平均池化,需要分別制定在row和column上的比例,在這兩個方向上進行獨立的最大池化操作,後面不詳細介紹。

示例中使用了max_pool,所以這裏只對最大池化進行函數解釋。

max_pool
tf.nn.max_pool
    max_pool(
        value,
        ksize,
        strides,
        padding,
        data_format='NHWC',
        name=None
    )

這個函數的作用是計算池化區中元素的最大值

參數說明
value: 4-D 的輸入 Tensor,shape爲 [batch, height, width, channels] 數據類型是 float32, float64, qint8, quint8, 或者qint32.

ksize: 一個長度不小於4的整型數組,數組中每一個值對應於輸入tensor中每一維的窗口大小.

strides: 一個長度不小於4的整型數組,數組中每個值對應輸入tensor上每個維度滑動窗口的移動步長。

padding: 字符變量,取值爲'VALID'或'SAME',表示移動到邊界的處理方法    
data_format: 表示輸入數據的格式,字符變量,取值爲'NHWC'或'NCHW'.

name: 當期OP的名字.

使用實例:

# -*- coding: utf-8 -*-
import numpy as np
import tensorflow as tf

input_data = tf.Variable( np.random.rand(1,6,6,3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)

y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'VALID')
output = tf.nn.max_pool(value = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1], padding = 'VALID')

with tf.Session() as sess:
    init = tf.initialize_all_variables()
    sess.run(init)
    print (' --convResult--')
    print (sess.run(y))
    print (' --poolResult--')
    print (sess.run(output))
    print (sess.run(tf.shape(output)))

Dropout

tflearn.layers.core和tf.nn都包含dropout這個函數,功能相同,只差一個參數,這裏合在一起解釋

概念解釋:

簡單理解可以將Dropout理解爲爲了防止模型過擬合而採用的一種trick的方法。
進一步講,Dropout是指在深度學習網絡的訓練過程中,對於神經網絡單元,按照一定的概率將其暫時從網絡中丟棄。注意是暫時,例如對於隨機梯度下降來說,由於是隨機丟棄,故而每一個mini-batch都在訓練不同的網絡。

再簡明一些,Dropout說的簡單一點就是我們讓在前向傳導的時候,讓某個神經元的激活值以一定的概率p,讓其停止工作。

總結而言,Dropout的思想是訓練整體DNN,並平均整個集合的結果,而不是訓練單個DNN模型,DNNs是以概率p捨棄部分神經元,其它神經元以概率q=1-p被保留,捨去的神經元的輸出都被設置爲零。

函數定義:

tflearn.layers.core.dropout (x, keep_prob, noise_shape=None, name=’Dropout’)
和 tf.nn.dropout

    dropout(
        x,
        keep_prob,
        noise_shape=None,
        seed=None,
        name=None
    )

參數說明:

x : 待處理的Tensor

keep_prob : 浮點數,每個元素被保留或放電的概率

noise_shape : 一個一維的Tensor,數據類型是int32。代表元素保留/失活是否獨立的標誌

name : OP的名稱

seed :一個Python整數。 用於創建隨機種子
方法說明

一個神經元將以概率keep_prob決定是否保留或放電,如果不保留或放電,那麼該神經元的輸出將是0,如果該神經元保留或放電,那麼該神經元的輸出值將被放大到原來的1/keep_prob倍。這裏的放大操作是爲了保持神經元輸出總個數不變。例如,神經元的值爲[2,1],keep_prob的值是0.5,並且是第一個神經元是保留,第二個神經元不保留,那麼神經元輸出的結果是[4,0],也就是相當於,第一個神經元被當做了1/keep_prob個輸出,即2個。這樣保證了總和2個神經元保持不變。

默認情況下,每個神經元是否放電是相互獨立的。但是,如果noise_shape被修改了,那麼這對於待處理的張量x就是一個廣播形式,而且當且僅當 noise_shape[i] == shape(x)[i] ,x中的元素是相互獨立的,否則要麼保留,要麼保持同步。這裏舉個例子說明:

如果 shape(x) = [k, l, m, n], noise_shape = [k, 1, 1, n] ,那麼每個批和通道都是相互獨立的(這裏k表示批次,n表示通道數量),但是每行和每列(l,m表示行和列或者寬和高)的數據都是關聯的,即要麼爲0,要麼保持不變,即不是相互獨立的。

爲了感受一下noise_shape的作用,運行如下代碼

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tensorflow as tf

a = tf.constant([[-1.0, 2.0, 3.0, 4.0]])
with tf.Session() as sess:
    b = tf.nn.dropout(a, 0.5, noise_shape = [1,4])
    print (sess.run(b)) #[[-0.  0.  0.  8.]]
    b = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
    print (sess.run(b)) #[[-2.  4.  6.  8.]]
    c = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
    print (sess.run(c)) #[[-0.  0.  0.  0.]]
    d = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
    print (sess.run(d)) #[[-0.  0.  0.  0.]]
    e = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
    print (sess.run(e)) #[[-0.  0.  0.  0.]]

輸出如下(代碼中也已經標註了):

    [[-0.  0.  0.  8.]]
    [[-2.  4.  6.  8.]]
    [[-0.  0.  0.  0.]]
    [[-0.  0.  0.  0.]]
    [[-0.  0.  0.  0.]]

這就說明了如果noise_shape[i] == shape(x)[i],那麼保持獨立,否則不是相互獨立的。

相關論文

JMLRdropout


總結

本文對如何使用TensorFlow實現CNN相關功能做了註解,對代碼中引入的部分新的概念做了解釋和說明,總體而言在實現和使用上是相對簡單的,但還是需要理解其物理意義才能很好的使用CNN,讓其發揮更多作用。一般而言其用來處理圖像是首選,但目前越來越多的人使用其來處理自然語言問題,如長文本,感興趣的可以對其進行更多的探索。

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