1. 程式人生 > >TensorFlow中關於LeNet-5網路的一些小坑

TensorFlow中關於LeNet-5網路的一些小坑

本篇文章提到的一些坑主要來自於學習《TensorFlow實戰Google深度學習框架》一書第6.4章節中關於使用LeNet5做MNIST

一 LeNet-5簡介

LeNet-5模型是Yann LeCun教授於1998年在論文Gradient-based learning applied to document recognition中提出的,它是第一個成功應用於數字識別問題的卷積神經網路。LeNet-5模型一共有7層,LeNet-5模型的結果框架圖如下:

layer1:卷積層Conv1

接收輸入層大小[32,32,1],經過尺寸為[5,5,6],步長為1的過濾器的無全0填充卷積運算,將得到[28,28,6]的卷積結果。

layer2:池化層Pool1

接收layer1,經過尺寸為[2,2],步長為2的過濾器進行最大值池化,得到[14,14,6]的池化結果。

layer3:卷積層Conv2

接收layer2,經過尺寸為[5,5,16],步長為1的過濾器的無全0填充卷積運算,將得到[10,10,16]的卷積結果。

layer4:池化層Pool2

接收layer3,經過尺寸為[2,2],步長為2的過濾器進行最大值池化,得到[5,5,16]的池化結果。

將[5,5,16]的結果轉化成FC層可接收資料形式,即將[5,5,16]reshpe成[1,5*5*16]

layer5:全連線層FC1

接收layer4,將400個節點進行全連線,輸出120個節點

layer6:全連線層FC2

接收layer5,將120個節點進行全連線,輸出84個節點

layer7:全連線層SoftMax

接收layer5,將84個節點進行全連線,輸出10個節點

二 書中示例程式碼

# -*- coding=utf-8 -*-
"""
LeNet網路結構程式碼
lenet_inference.py
"""
import tensorflow as tf

# 配置神經網路引數
INPUT_NODE = 784
OUTPUT_NODE = 10

# 配置影象資料引數
IMAGE_SIZE = 28
NUM_CHANNELS = 1
NUM_LABELS = 10

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

def inference(input_tensor, train, regularizer):
    # 定義第一層卷積層:輸入28×28×1 輸出28×28×32
    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×5×32的過濾器,步長為1,全0填充
        conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME')
        # 使用relu啟用函式
        relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))

    # 定義第二層池化層:輸入28×28×32 輸出14×14×32
    with tf.name_scope('layer2-pool1'):
        # ksize和strides首尾必須為1,ksize過濾器尺寸,strides步長
        pool1 = tf.nn.max_pool(relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    # 定義第三層卷積層:輸入14×14×32 輸出14×14×64
    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×5×64的過濾器,步長為1,全0填充
        conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME')
        # 使用relu啟用函式
        relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))

    # 定義第四層池化層:輸入14×14×64 輸出7×7×64
    with tf.name_scope('layer4-pool2'):
        # ksize和strides首尾必須為1,ksize過濾器尺寸,strides步長
        pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    # 將池化結果轉化為全連線層的輸入:輸入7×7×64 輸出3136×1
    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])

    # 定義第5層全連線層:輸入3136×1 輸出512×1
    with tf.variable_scope('layer5-fc1'):
        # 只有全連線層的權重需要加入正則化
        fc1_weights = tf.get_variable('weight', [nodes, FC_SIZE],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        fc1_biases = tf.get_variable('bias', [FC_SIZE], initializer=tf.constant_initializer(0.1))
        # 使用relu啟用函式
        fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)
        if regularizer != None:
            tf.add_to_collection('losses', regularizer(fc1_weights))
        # dropout避免過擬合:在訓練過程中會隨機將部分節點輸出為0
        if train: fc1 = tf.nn.dropout(fc1, 0.5)

    # 定義第6層softmax層:輸入512×1 輸出10×1
    with tf.variable_scope('layer6-fc2'):
        # 只有全連線層的權重需要加入正則化
        fc2_weights = tf.get_variable('weight', [FC_SIZE, NUM_LABELS],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        fc2_biases = tf.get_variable('bias', [NUM_LABELS], initializer=tf.constant_initializer(0.1))
        if regularizer != None:
            tf.add_to_collection('losses', regularizer(fc2_weights))
        # 使用relu啟用函式
        logit = tf.matmul(fc1, fc2_weights) + fc2_biases

    return logit
# -*- coding:utf-8 -*-
'''
訓練LeNet
lenet_train.py
'''
import os
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
import lenet_inference as mnist_inference

# 配置神經網路引數
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.00010
TRAINING_STEPS = 30000
MOVING_AVERAGE_DECAY = 0.99
# 模型儲存的路徑和檔名
MODEL_SAVE_PATH = 'MNIST_MODEL'
MODEL_NAME = 'model.ckpt'

'''
訓練模型
'''


def train(mnist):
    # 定義輸入輸出的placeholder
    x = tf.placeholder(tf.float32, [BATCH_SIZE, 28, 28, 1], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
    # 定義L2正則化損失函式
    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)

    # 前向傳播
    y = mnist_inference.inference(x, True, regularizer)
    global_step = tf.Variable(0, trainable=False)
    # 帶滑動平均模型的前向傳播
    variable_averages = tf.train.ExponentialMovingAverage(decay=MOVING_AVERAGE_DECAY, num_updates=global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())
    # 計算損失函式
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.argmax(y_, 1), logits=y)
    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, variables_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):
            # 產生本輪batch的訓練資料,並執行訓練程式
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            reshaped_xs = np.reshape(xs, (BATCH_SIZE, 28, 28, 1))
            _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: reshaped_xs, y_: ys})
            # 每1000輪儲存一次模型
            if i % 1000 == 0:
                # 通過損失函式的大小了解本輪訓練的基本情況
                print("\rAfter %d training step(s), loss on training batch is %g" % (step, loss_value), end='')
                # 儲存模型,給出global_step引數可以讓每個被儲存的檔名末尾加上訓練的輪數
                saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME), global_step=global_step)


# 主程式入口
def main(argv=None):
    # 如果指定路徑下沒有資料,則自動下載
    mnist = input_data.read_data_sets("MNIST_DATA", one_hot=True)
    train(mnist)


# TensorFlow提供的一個主程式入口
if __name__ == '__main__':
    tf.app.run()
# -*- coding=utf-8 -*-
"""
測試神經網路準確率
lenet_eval.py
"""
import time
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
import lenet_inference as lenet
import lenet_train as nt

# 每10秒載入一次最新模型,並計算其正確率
EVAL_INTERVAL_SECS = 10
BATCH_SIZE = 100


def evaluate(mnist):
    # 準備驗證資料
    xs = mnist.validation.images
    ys = mnist.validation.labels
    print('xs.shape:{}, ys.shape:{}'.format(xs.shape, ys.shape))
    reshaped_xs = np.reshape(xs, [xs.shape[0], 28, 28, 1])

    with tf.Graph().as_default() as g:
        # 定義輸入輸出的placeholder
        x = tf.placeholder(tf.float32, [xs.shape[0], 28, 28, 1], name='x-input')
        y_ = tf.placeholder(tf.float32, [None, 10], name='y-input')
        validate_feed = {x: reshaped_xs, y_: mnist.validation.labels}

        # 前向傳播,測試不關注正則化
        y = lenet.inference(x, train=False, regularizer=None)
        # 計算模型預測精度
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        # 將比爾型轉化為float32並求平均值,即得一組資料的正確率
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

        # 通過變數重新命名的方式來載入模型
        # 這樣在前向傳播的過程中就不需要呼叫求滑動平均的函式來獲取平均值
        # 這樣就可以完全共用mnist_inference.py中定義的前向傳播過程
        variable_averages = tf.train.ExponentialMovingAverage(nt.MOVING_AVERAGE_DECAY)
        variables_to_restore = variable_averages.variables_to_restore()
        saver = tf.train.Saver(variables_to_restore)

        # 每隔EVAL_INTERVAL_SECS秒呼叫一次計算正確率的過程以檢測訓練過程中正確率的變化
        while True:
            with tf.Session() as sess:
                ckpt = tf.train.get_checkpoint_state(nt.MODEL_SAVE_PATH)
                if ckpt and ckpt.model_checkpoint_path:
                    saver.restore(sess, ckpt.model_checkpoint_path)
                    global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]
                    accuracy_score = sess.run(accuracy, feed_dict=validate_feed)
                    print('After %s training step(s), validation accuracy=%g' % (global_step, accuracy_score))
                else:
                    print('No checkpoint file found')
                    return
                time.sleep(EVAL_INTERVAL_SECS)

def main(argv=None):
    mnist = input_data.read_data_sets("MNIST_DATA", one_hot=True)
    evaluate(mnist)

if __name__ == '__main__':
    tf.app.run()

三 執行程式碼之後遇到的大坑

當你興高采烈地一行行敲完這三個程式碼,而且你還很確定自己基本瞭解了LeNet的結構以及在TensorFlow中該如何實現,想想以後就可以動手搭建自己的卷積網路還有點小興奮。於是,你在Pycharm中Shift+Alt+F10開始執行自己的程式碼,結果你得到了下面的結果。

What???!!!這是什麼鬼,損失函式這麼大!!!

原因是書中對學習率設定的太高,只需將學習率調低即可

學習率:控制神經網路中引數更新的幅度,過大易導致結果在最優值兩側浮動,過小迭代次數過多

# lenet_train.py
LEARNING_RATE_BASE = 0.01

修改之後再次執行起來,希望可以一切順利,^_^

結果得到了下面的結果

What???!!!這是什麼鬼,損失函式怎麼還是比較大!!!

這個坑的具體原因我不太清楚,因為也是才接觸。我的做法是將原書中程式碼的FC1層的正則化去掉,只在最後一層使用正則化。

# lenet_inference.py
# 定義第5層全連線層:輸入3136×1 輸出512×1
    with tf.variable_scope('layer5-fc1'):
        # 只有全連線層的權重需要加入正則化
        fc1_weights = tf.get_variable('weight', [nodes, FC_SIZE],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        fc1_biases = tf.get_variable('bias', [FC_SIZE], initializer=tf.constant_initializer(0.1))
        # 使用relu啟用函式
        fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)
        # dropout避免過擬合:在訓練過程中會隨機將部分節點輸出為0
        if train: fc1 = tf.nn.dropout(fc1, 0.5)

    # 定義第6層softmax層:輸入512×1 輸出10×1
    with tf.variable_scope('layer6-fc2'):
        # 只有全連線層的權重需要加入正則化
        fc2_weights = tf.get_variable('weight', [FC_SIZE, NUM_LABELS],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        fc2_biases = tf.get_variable('bias', [NUM_LABELS], initializer=tf.constant_initializer(0.1))
        if regularizer != None:
            tf.add_to_collection('losses', regularizer(fc2_weights))
        # 使用relu啟用函式
        logit = tf.matmul(fc1, fc2_weights) + fc2_biases

執行結果如下:

這樣的結果才是正常的嘛?

如果有幸被哪位大牛看到這篇文章,請講解一下為什麼我只在最後一層使用正則化,執行結果才正常