1. 程式人生 > >TensorFlow 深度學習框架(9)-- 經典卷積網路模型 : LeNet-5 模型 & Inception-v3 模型

TensorFlow 深度學習框架(9)-- 經典卷積網路模型 : LeNet-5 模型 & Inception-v3 模型

LeNet -5 模型

LeNet-5 模型總共有 7 層,以數字識別為例,圖展示了 LeNet-5 模型的架構


第一層,卷積層
這一層的輸入就是原始的影象畫素,LeNet-5 模型接受的輸入層大小為 32*32*1 。第一個卷積層過濾器的尺寸為 5 * 5,深度為 6,步長為 1 。因為沒有使用全 0 填充,所以這一層的輸出尺寸為 32-5+1 = 28,深度為 6 。這一個卷積層共有 5 * 5 * 1 * 6 + 6 = 156 個引數,其中 6 個偏置項引數。

第二層,池化層
這一層的輸入為第一層的輸出,是一個 28 * 28 * 6 的節點矩陣。本層採用的過濾器大小為 2 * 2,長和寬的步長均為 2 ,所以本層的輸出矩陣大小為 14 * 14 * 6 。

第三層,卷積層

本層的輸入矩陣大小為 14 * 14 * 6,使用的過濾器大小為 5 * 5,深度為 16。本層不使用全 0 填充,步長為 1 。本層的輸出局針大小為 10 * 10 * 16。有 5 * 5 * 6 * 16 + 16 =2416 個引數。

第四層,池化層

本層的輸入矩陣大小為 10 * 10 * 16,採用的過濾器大小為 2 * 2,步長為 2。 本層的輸出矩陣大小為 5 * 5 * 16。

第五層,全連線層

本層的輸入矩陣大小為 5 * 5 * 16,在LeNet-5 論文中將這一層稱之為卷積層。但是因為過濾器的大小就是 5 * 5,所以和全連線層沒有區別。如果將 5 * 5 * 16 矩陣中的節點拉成一個向量,那就和全連線層的輸入一樣了。本層的輸出節點個數為 120,總共有 5 * 5 * 16 * 120  + 120 = 48120 個引數。

第六層,全連線層

輸入節點 120 個,輸出節點  84 個。 引數 120 * 84 + 84 = 10164 個。

第七層,全連線層

輸入節點 84 個,輸出節點 10 個,引數 84 * 10 + 10 = 850 個。

下面給出TensorFlow 程式來實現一個類似 LeNet-5 模型的卷積神經網路來解決 mnist 數字識別問題。由於卷積神經網路的輸入是一個三維矩陣,所以調整資料輸入格式如下:

#調整輸入資料 placeholder 的格式,輸入為一個四維矩陣
x = tf.placeholder(tf.float32,[
            BATCH_SIZE,       #第一維表示一個 batch
            mnist_inference.IMAGE_SIZE, #第二位和第三維表示圖片的尺寸
            mnist_inference.IMAGE_SIZE,
            mnist_inference.NUM_CHANNELS] #第四維表示圖片的深度,對於黑白圖片深度為 1;對於 RGB 格式的圖片,深度為 3
        name = 'x-input')

#類似的將輸入的訓練資料格式調整為一個四維矩陣,並將這個調整後的資料傳入 sess.run 過程
reshaped_xs = np.reshape(xs,(BATCH_SIZE,
                             mnist_inference.IMAGE_SIZE,
                             mnist_inference.IMAGE_SIZE,
                             mnist_inference.NUM_CHANNELS))

其中修改後的類似 LeNet-5 模型的前向傳播過程 mnist_inference.py 程式碼如下:

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

#配置神經網路的引數
INPUT_NODE = 784
OUTPUT_NODE = 10
IMAGE_SIZE = 28
NUM_CHANNELS = 1
NUM_LABELS = 10

#第一層卷積層的尺寸
CONV1_DEEP = 32
CONV1_SIZE = 5
#第二層卷積層的尺寸
CONV2_DEEP = 64
CONV2_SIZE = 5
#全連線層的節點個數
FC_SIZE = 512

#定義卷積神經網路的前向傳播過程
#這個程式中將用到 dropout 方法,可以進一步提升模型可靠性並防止過擬合
#dropout 過程只有在訓練時使用
def inference(input_tensor,train,regularizer):
    #第一層卷積層
    with tf.variable_scope('layer1-conv1'):
        conv1_weights = tf.get_variable("weight",[CONV1_SIZE,CONV1_SIZE,NUM_CHANNELS,CONV1_DEEP],
                                initializer = tf.truncated_normal_initializer(stddev = 0.1))
        conv1_biases = tf.get_variable("bias",[CONV1_DEEP],
                                initializer = tf.constant_initializer(0.0))
        #步長為 1,全 0 填充
        conv1 = tf.nn.conv2d(input_tensor,conv1_weights,strides = [1,1,1,1],padding = 'SAME')
        relu1 = tf.nn.relu(tf.nn.bias_add(conv1,conv1_biases))
    
    #第二層池化層
    with tf.name_scope('layer2-pool1'):
        pool1 = tf.nn.max_pool(relu1,
                                ksize = [1,2,2,1],strides = [1,2,2,1],padding = 'SAME')
    with tf.variable_scope('layer3-conv2'):
        conv2_weights = tf.get_variable(
                "weight",[CONV2_SIZE,CONV2_SIZE,CONV1_DEEP,CONV2_DEEP],
                initializer = tf.truncated_normal_initializer(stddev = 0.1))
        conv2_biases = tf.get_variable(
                "bias",[CONV2_DEEP],initializer = tf.constant_initializer(0.0))
        conv2 = tf.nn.conv2d(pool1,conv2_weights,strides = [1,1,1,1],padding = 'SAME')
        relu2 = tf.nn.relu(tf.nn.bias_add(conv2,conv2_biases))
    #第四層池化層
    with tf.name_scope('layer4-pool2'):
        pool2 = tf.nn.max_pool(relu2,
                                ksize = [1,2,2,1],strides = [1,2,2,1],padding = 'SAME')
    #節點拉伸,進入全連線層
    pool_shape = pool2.get_shape().as_list()
    nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
    reshaped = tf.reshape(pool2,[pool_shape[0],nodes])
    
    #全連線層
    with tf.variable_scope('layer5-fc1'):
        fc1_weights = tf.get_variable("weights",[nodes,FC_SIZE],
                        initializer = tf.truncated_normal_initializer(stddev = 0.1))
        #只有全連線層的權重需要加入正則化
        if regularizer != None:
            tf.add_to_collection('losses',regularizer(fc1_weights))
        fc1_biases = tf.get_variable('bias',
                        [FC_SIZE],initializer = tf.constant_initializer(0.1))
        fc1 = tf.nn.relu(tf.matmul(reshaped,fc1_weights) + fc1_biases)
        #如果是訓練模式,加入 dropout
        if train: fc1 = tf.nn.dropout(fc1,0.5)
    #輸出層
    with tf.variable_scope('layer6-fc2'):
        fc2_weights = tf.get_variable("weight",[FC_SIZE,NUM_LABELS],
                            initializer = tf.truncated_normal_initializer(stddev = 0.1))
        if regularizer != None:
            tf.add_to_collection('losses',regularizer(fc2_weights))
        fc2_biases = tf.get_variable('bias',[NUM_LABELS],
                            initializer = tf.constant_initializer(0.1))
        logit = tf.matmul(fc1,fc2_weights) + fc2_biases
    
    #返回第六層的輸出
    return logit 
上述給出的卷積神經網路可以在 mnist 資料集上達到 99.4% 的正確率,相比全連線網路模型有很大的提升。然而一種卷積神經網路架構並不能解決所有的問題,比如 LeNet-5 模型就無法很好的處理比較大的影象資料集。那麼如何設計卷積神經網路的架構呢?下面的正則表示式公式總結了一些經典的用於圖片分類問題的卷積神經網路架構:
        輸入層 -> (卷積層+ -> 池化層? )+ -> 全連線層+
"卷積層+" 表示一層或多層卷積層,大部分卷積神經網路中一般最多連續使用三層卷積層。"池化層? " 表示沒有或者一層池化層。池化層雖然可以達到減少引數的作用,但也可以直接通過調整卷積層步長來完成,所以有些卷積神經網路中沒有池化層。

上述程式碼的架構為: 輸入層->卷積層->池化層->卷積層->池化層->全連線層->全連線層->輸出層

在過濾器深度上,大部分卷積神經網路都採用逐層遞增的方式。卷積層的步長一般為1,但有些模型中也會使用 2 或者3 作為步長。池化層的配置相對簡單,用得最多的是最大池化層,池化層的過濾器邊長一般為 2 或者 3,步長也一般為 2 或者 3 。

Inception-v3 模型

Inception-v3 結構是一種和 LeNet-5 結構是完全不同的卷積神經網路結構。在 LeNet-5 模型中,不同卷積層通過串聯的方式連線在一起,而 Inception-v3 模型中的 Inception 結構是將不同的卷積層通過並聯的方式結合在一起,下圖給出了 Inception 模組的一個單元結構示意圖:


Inception 模組會首先使用不同尺寸的過濾器處理輸入矩陣。在圖中,最上方矩陣為使用了邊長為 1 的過濾器的卷積層前向傳播的結果。類似的,中間矩陣使用的過濾器邊長為 3 ,下方矩陣使用的邊長為 5 。不同的矩陣代表了 Inception 模組中的一條計算路徑。雖然過濾器的大小不同,但如果所有的過濾器都使用全 0 填充並且步長為 1 ,那麼前向傳播得到的結果矩陣的長和寬都與輸入矩陣一致。這樣經過不同過濾器處理的結果矩陣可以拼接成一個更深的矩陣。如上圖所示,可以將它們在深度這個維度上組合起來。

圖中所示的 Inception 模組得到的結果矩陣的長和寬與輸入一樣,深度為紅黃藍三個矩陣深度的和。上圖展示的是 Inception 模組的核心思想,真正的 Inception-v3 模型中的 Inception 模組要更加複雜且多樣,下圖給出了 Inception-v3 模型的架構圖:


Inception-v3模型總共有 46 層,由 11 個 Inception 模組組成。圖中方框框出來的就是其中一個 Inception 模組。在 Inception-v3 模型中有 96 個卷積層,如果用 LeNet-5 中卷積層實現的程式碼,那麼一個卷積層就需要 5 行程式碼,96個就需要寫 480 行程式碼來實現。這樣使得程式碼的可讀性非常差。為了更好的實現類似 Inception-v3 模型這樣的複雜卷積神經網路,下面程式碼展示了 TensorFlow-Slim 工具來更加簡潔的實現一個卷積層:

# 直接使用 TensorFlow 原始 API 實現卷積層
with tf.variable_scope(scope_name):
    weights = tf.get_variable("weight",...)
    biases = tf.get_variable("bias",...)
    conv = tf.nn.conv2d(...)
    relu = tf.nn.relu(tf.nn.bias_add(conv,biases))

#使用TensorFlow-Slim 實現卷積層,可以在一行程式碼中實現卷積層的前向傳播演算法
# slim.conv2d 函式有 3 個引數是必填的
# 第一個引數是輸入節點矩陣,第二個引數是當前卷積層過濾器的深度,第三個引數是過濾器的尺寸
# 可選的引數有移動步長,是否使用全 0 填充,啟用函式的選擇以及變數的名稱空間等
net = slim.conv2d(input,32,[3,3])

利用 slim.conv2d 函式實現 Imception-v3 模型中方框框出來的那個 Inception 模組(第11個 Inception)的程式碼示例如下:

#slim.arg_scope 函式可以用於設定預設的引數值。slim.arg_scope 函式的第一個引數是一個列表
#這個列表中的引數將會使用預設的引數取值,在呼叫 slim.conv2d(net,320,[1,1]) 時會自動加上 stride = 1 ,padding = 'SAME'
with slim.arg_scope([slim.conv2d,slim.max_pool2d,slim.avg_pool2d],
                        stride = 1,padding = 'SAME'):
    ...
    #此處省略了 Inception-v3 中前面的網路結構而直接實現最後的 Inception 模組
    net = 上一層的輸出節點矩陣
    #為這個 Inception 模組宣告一個統一的變數名稱空間
    with tf.variable_scope('last_inception'):
        #給 Inception 模組中每一條並聯的路徑宣告一個名稱空間
        with tf.variable_scope('Branch_0'):
            # 實現一個過濾器邊長為 1,深度為 320 的卷積層
            branch_0 = slim.conv2d(net,320,[1,1],scope = 'Conv2d_0a_lxl')
        
        #實現第二條路徑,本身也是一個 Inception 結構,最終深度為 384*2
        with tf.variable_scope('Branch_1'):
            branch_1 = slim.conv2d(net,384,[1,1],scope = 'Conv2d_0a_1x1')
            #tf.concat 函式可以將多個矩陣拼接起來,第一個引數指定了拼接的維度,
            #這裡給出的 3 代表了矩陣是在深度這個維度上進行拼接
            branch_1 = tf.concat(3,[
                        #如圖所示,此處 2 層卷積層的輸入都是 branch_1 而不是 net
                        slim.conv2d(branch_1,384,[1,3],scope = 'Conv2d_0b_1x3'),
                        slim.conv2d(branch_1,384,[3,1],scope = 'Conv2d_0b_3x1')])
        
        #實現第三條路徑,此處也是一個小的 Inception 模型
        with tf.variable_scope('Branch_2'):
            branch_2 = slim.conv2d(net,448,[1,1],scope = 'Conv2d_0b_3x3')
            branch_2 = slim.conv2d(branch_2,384,[3,3],scope = 'Conv2d_0b_3x3')
            branch_2 = tf.concat(3,[
                         slim.conv2d(branch_2,384,[1,3],scope = 'Conv2d_0c_1x3'),
                         slim.conv2d(branch_2,384,[3,1],scope = 'Conv2d_0c_3x1')])
        
        #實現第四條路徑,池化卷積層,輸出層深度為 192
        with tf.variable_scope('Branch_3'):
            branch_3 = slim.avg_pool2d(net,[3,3],scope = 'AvgPool_0a_3x3')
            branch_3 = slim.conv2d(branch_3,192,[1,1],scope = 'Conv2d_0b_1x1')
        
        #當前 Inception 模組的最後輸出為以上 4 條路徑在第三維(深度)上的合併
        net = tf.concat(3,[branch_0,branch_1,branch_2,branch_3])

以上程式碼就是圖中所示 Inception-v3 模型中的第 11 個 Inception 模組的實現。也可以不使用 TensorFlow-Slim 工具實現,只是程式碼會顯得非常不好閱讀,或者定義一個傳統的卷積層的函式,類似替代 slim.conv2d 也行。主要是使用 tf.concat 進行了深度上的拼接,實現了圖 6-16 中 Inception 模組的核心思想。