1. 程式人生 > >機器學習筆記(十三):TensorFlow實戰五(經典卷積神經網路: LeNet -5 )

機器學習筆記(十三):TensorFlow實戰五(經典卷積神經網路: LeNet -5 )

1 - 引言

之前我們介紹了一下卷積神經網路的基本結構——卷積層和池化層。通過這兩個結構我們可以任意的構建各種各樣的卷積神經網路模型,不同結構的網路模型也有不同的效果。但是怎樣的神經網路模型具有比較好的效果呢?

下圖展示了CNN的發展歷程。

在這裡插入圖片描述

經過人們不斷的嘗試,誕生了許多有有著里程碑式意義的CNN模型。因此我們接下來會學習這些非常經典的卷積神經網路

  • LeNet -5
  • AlexNet
  • VGG
  • Inception
  • ResNet

2 - LeNet-5模型

LeNet-5模型是Yann LeCun教授於1998年在論文Gradient-Based Learning Applied to Document Recognition中提出的,它是第一個成功應用與數字識別問題的卷積神經網路。在MNIST資料集上,LeNet-5模型可以達到大約99.2%的正確率,LeNet-5模型如下圖所示:
在這裡插入圖片描述

下面我們來詳細介紹一個LeNet-5模型每一層的結構

2.1 第一層:卷積層

資料維數詳細說明:

這一層的輸入就是原始的影象畫素,LeNet-5模型輸入層為32X32X1的影象(只能識別灰度影象而不能識別彩色影象)。第一個卷積層的過濾器尺寸為5X5,深度為6(深度既是通道值),不使用padding,步長為1,因為沒有使用padding,這一層的輸出尺寸為32-5+1=28,深度為6。這一個卷積層總共有5x5x1x6+6=156個引數,其中6個為偏置項引數。本層卷積層總共有28x28x6x(5x5+1)=122304個連線

總結:

輸入圖片: 32

32 1 32*32*1

卷積核大小: 5 5

5*5

卷積核種類:6

輸出featuremap大小: 28 28 32 5 + 1 = 28 28*28 (32-5+1)=28

神經元數量: 28 28 6 28*28*6

可訓練引數: 5 5 + 1 ) 6 5 5 = 25 u n i t b i a s 6 (5*5+1) * 6(每個濾波器5*5=25個unit引數和一個bias引數,一共6個濾波器)

連線數: 5 5 + 1 6 28 28 = 122304 (5*5+1)*6*28*28=122304

2.2 第二層:池化層

這一層的輸入為第一層的輸出,是一個28x28x6的節點矩陣。本層採用的過濾器大小為2x2,步長為2,所以輸出矩陣為14x14x6。

輸入: 28 28 6 28*28*6

取樣區域: 2 2 2*2

取樣方式:4個輸入相加,乘以一個可訓練引數,再加上一個可訓練偏置。結果通過sigmoid

取樣種類:6

輸出featureMap大小: 14 14 28 / 2 14*14(28/2)

神經元數量: 14 14 6 14*14*6

連線數: 2 2 + 1 6 14 14 (2*2+1)*6*14*14

S2中每個特徵圖的大小是C1中特徵圖大小的1/4。

詳細說明:第一次卷積之後緊接著就是池化運算,使用$ 2*2$核 進行池化,於是得到了S2,6個 14 14 14*14 的 特徵圖(28/2=14)。S2這個pooling層是對C1中的 2 2 2*2 區域內的畫素求和乘以一個權值係數再加上一個偏置,然後將這個結果再做一次對映。同時有5x14x14x6=5880個連線。

2.3 第三層:卷積層

本層的輸入矩陣大小為14x14x6,使用的過濾器大小為5x5,深度為16。本層不使用padding,步長為1,所以輸出節點為10x10x16。所以有5x5x6x16+16 = 2416個引數。10x10x16x(25+1)=41600個連線。

輸入:14x14x6

卷積核大小: 5 5 5*5

卷積核種類:16

輸出featureMap大小: 10 10 ( 14 5 + 1 ) 10*10 (14-5+1)

2.4 第四層:池化層

本層輸入矩陣大小為10x10x16,採用的過濾器大小為2x2,步長為2,本層輸出矩陣大小為5x5x16

2.5 第五層:卷積層

輸入:5x5x16

卷積核大小:5*5

卷積核種類:120

輸出featureMap大小:1*1(5-5+1)

可訓練引數/連線:120*(1655+1)=48120

雖然LeNet-5模型的論文中將這一層成為卷積層,但是因為過濾器的大小就是5x5,所以和全連線層沒有區別。

2.6 F6層-全連線層

本層的輸入節點個數為120個,輸出節點個數為84個,總共引數為120x84+84 = 10164個。

2.7 Output層-全連線層

本層輸入節點為84個,輸出節點個數為10個,總共引數為84x10+10 = 850個。

3 - TensorFlow實現LeNet-5模型

** mnist_train_LeNet_5.py檔案:**

import os

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import  numpy as np
#載入mnist_inference.py中定義的常量和前向傳播的函式。
import mnist_inference_LeNet_5

BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8 # 最開始的學習率
LEARNING_RATE_DECAY = 0.99 # 在指數衰減學習率的過程中用到
REGULARIZATION_RATE = 0.0001 # 描述模型複雜度的正則化項在損失函式中的係數
TRAINING_STEPS = 30000 # 訓練輪數,注意,訓練一個Batch就是一個step
MOVING_AVERAGE_DECAY = 0.99 # 滑動平均模型的衰減率,最後我會講解滑動平均模型
#模型儲存的路徑和中文名
MODEL_SAVE_PATH = "/path/to/model/"
MODEL_NAME = "model.ckpt"

def train(mnist):
    # 定義輸入輸出placeholder。
    x = tf.placeholder(tf.float32,[
        BATCH_SIZE, #第一維表示一個batch中樣例的個數
        mnist_inference_LeNet_5.IMAGE_SIZE, #第二維和第三維表示圖片的尺寸
        mnist_inference_LeNet_5.IMAGE_SIZE,
        mnist_inference_LeNet_5.NUM_CHANNELS],  #第四維表示圖片的深度,對於RBG格式的圖片,深度為5
        name='x-input')

    y_ = tf.placeholder(tf.float32, [None, mnist_inference_LeNet_5.OUTPUT_NODE], name='y-input')

    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    # 直接使用mnist_inference.py中定義的前向傳播過程
    y = mnist_inference_LeNet_5.inference(x,train, regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 定義損失函式、學習率、滑動平均操作以及訓練過程
    variable_averages = tf.train.ExponentialMovingAverage(
        MOVING_AVERAGE_DECAY, global_step
    )
    variable_averages_op = variable_averages.apply(
        tf.trainable_variables()
    )
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
        logits=y, labels=tf.argmax(y_, 1)
    )
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE,
        global_step,
        mnist.train.num_examples / BATCH_SIZE,
        LEARNING_RATE_DECAY
    )
    train_step = tf.train.GradientDescentOptimizer(learning_rate)\
                   .minimize(loss, global_step=global_step)
    with tf.control_dependencies([train_step, variable_averages_op]):
        train_op = tf.no_op(name='train')

    # 初始化TensorFlow持久化類
    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()

        # 在訓練過程中不再測試模型在驗證資料上的表現,驗證和測試的過程將會有一個獨
        # 立的程式來完成。
        for i in range(TRAINING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            xs = np.reshape(xs,(
                BATCH_SIZE,
                mnist_inference_LeNet_5.IMAGE_SIZE,
                mnist_inference_LeNet_5.IMAGE_SIZE,
                mnist_inference_LeNet_5.NUM_CHANNELS))

            _, loss_value, step = sess.run([train_op, loss, global_step],
                                           feed_dict={x: xs, y_: ys})
            # 每1000輪儲存一次模型
            if i % 1000 == 0:
                # 輸出當前的訓練情況。這裡只輸出了模型在當前訓練batch上的損失
                # 函式大小。通過損失函式的大小可以大概瞭解訓練的情況。在驗證數
                # 據集上正確率的資訊會有一個單獨的程式來生成
                print("After %d training step(s), loss on training "
                      "batch is %g." % (step, loss_value))
                # 儲存當前的模型。注意這裡給出了global_step引數,這樣可以讓每個
                # 被儲存的模型的檔名末尾加上訓練的輪數,比如“model.ckpt-1000”,
                # 表示訓練1000輪之後得到的模型。
                saver.save(
                    sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME),
                    global_step=global_step
                )
# 主程式入口
def main(argv=None):
    # 宣告處理MNIST資料集的類,這個類在初始化時會自動下載資料。
    mnist = input_data.read_data_sets("/path/to/MNIST_data", one_hot=True)
    train(mnist)

# TensorFlow提供的一個主程式入口,tf.app.run會呼叫上面定義的main函式
if __name__ == "__main__":
    tf.app.run()

mnist_inference_LeNet_5.py檔案:

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

# 定義神經網路結構相關的引數
INPUT_NODE = 784
OUTPUT_NODE = 10

IMAGE_SIZE = 20
NUM_CHANNELS = 1
NUM_LABELS = 10

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

#定義卷積神經網路的前向傳播過程。這裡添加了一個新的引數train,用於區分訓練過程和測試
#過程,在這個過程中將用到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))
        #使用邊長為5,深度為32的過濾器,過濾器移動的步長為1,且使用全零填充
        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))

    #實現第二層池化層的前向傳播過程,這裡選用最大池化層,池化層邊長為2,
    #使用全0填充且移動的步長為2,這一層的輸入時上一層的輸出,也就是28x28x32
    #的矩陣,輸出為14x14x32的矩陣
    with tf.name_scope('layer2-pool1'):
        pool1 = tf.nn.max_pool(
            relu1,ksize=[1,2,2,1],strides=[1,2,1,1],padding='SAME')

    #宣告第三層卷積層的變數並實現前向傳播過程,這一層的輸入為14x14x32的矩陣
    #輸出為14x14x64的矩陣
    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))
        #使用邊長為5,深度為64的過濾器,過濾器移動的步長為1,且使用全零填充
        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))

    #實現第四層池化層的前向傳播過程,這一層和第二層的結構是一樣的,這一層的輸入為
    #14x14x64的矩陣,輸出為7x7x64的矩陣。
    with tf.name_scope('layer4-pool2'):
        pool2 = tf.nn.max_pool(
            relu2,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
    #將第四層池化層的輸出轉化為第五層全連線的輸入格式。第四層的輸入為7x7x64的矩陣,
    #然而第五層全連線層需要的輸入格式為向量,所以在這裡需要將這個7x7x64的矩陣拉直成
    #一個向量,pool2.get_shape函式可以得到第四層輸出矩陣的維度而不需要手工計算。
    #注意因為每一層神經網路的輸入輸出都為一個batch的矩陣,所以這裡得到的維度也包含一個
    #batch中資料的個數

    pool_shape = pool2.get_shape().as_list()

    #計算將矩陣拉成向量之後的長度,這個長度就是矩陣長寬及深度的成績,注意這裡
    #pool_shape[0]為一個batch鍾資料的個數。
    nodes = pool_shape[1]*pool_shape[2]*pool_shape[3]

    #通過tf.reshape函式將第四層的輸出變成一個batch的向量。
    reshaped = tf.reshape(pool2,[pool_shape[0],nodes])

    #宣告第五層全連線的變數並實現前向傳播的過程,這一層的輸入時拉直之後的一組向量
    #向量長度為3136,輸出是一組長度為512的向量。這裡我們引入了dropout的概念。
    #dropout會隨機的將部分節點輸出改為0,避免過擬合的問題,這種優化方法一般只在
    #全連線層而不是卷積層使用
    with tf.variable_scope('layer5-fc1'):
        fc1_weights = tf.get_variable(
            "weight",[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)
        if train: fc1 = tf.nn.dropout(fc1,0.5)

    #宣告第六層全連線層的變數並實現前向傳播過程,這一層為長度512的向量
    #輸出一組長度為10的向量,並且輸出通過sotfmax之後就得到了最後的分類結果
    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

After 1 training step(s), loss on training batch is 3.2112.
After 1001 training step(s), loss on training batch is 0.231712.
After 2001 training step(s), loss on training batch is 0.182711.
·
·
·
After 27001 training step(s), loss on training batch is 0.0336458.
After 28001 training step(s), loss on training batch is 0.036755.
After 29001 training step(s), loss on training batch is 0.0390648.