1. 程式人生 > >TensorFlow學習筆記(8) 經典卷積網路模型

TensorFlow學習筆記(8) 經典卷積網路模型

LeNet-5模型

LeNet-5模型一共有7層:

  1. 卷積層。這一層的輸入是原始影象畫素,模型接受的輸入層大小為32*32*1。過濾器尺寸為5*5,深度為6,不使用全0填充,步長為1。輸出尺寸為32-5+1=28,深度為6。該卷積層共有5*5*1*6+6=156個引數。
  2. 池化層。這一層的輸入是第一層的輸出,是一個28*28*6的節點矩陣。本層過濾器尺寸為2*2,長和寬步長為2,輸出矩陣為14*14*6。
  3. 卷積層。本層輸入矩陣尺寸大小為14*14*6。使用過濾器大小為5*5*16,不使用全0填充,步長為1。輸出矩陣大小為10*10*16,共有5*5*6*16+16=2416個引數。
  4. 池化層。
    輸入矩陣5*5*16,過濾器大小2*2,步長2,輸出5*5*16。
  5. 全連線層。輸入矩陣5*5*16,過濾器大小5*5,輸出長度為120的向量,共有5*5*16*120+120=48120個引數
  6. 全連線層。輸入節點120,輸出節點84,總引數120*84+84=10164個。
  7. 全連線層。輸入節點84,輸出節點10,總引數120*84+84=10164個。

下面為一個TF程式實現類似該模型的卷積神經網路解決MNIST數字識別問題:

mnist_cnn_inference.py

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

#定義卷積神經網路,train用於區分訓練過程和測試過程
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))
        conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME')
        conv1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))

    #宣告第一層池化層,輸入28*28*32,輸出14*14*32
    with tf.variable_scope('layer2-pool1'):
        pool1 = tf.nn.max_pool(conv1, 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))
        conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME')
        conv2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))

    #宣告第四層池化層,輸入14*14*64,輸出7*7*64
    with tf.variable_scope('layer4-pool2'):
        pool2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    #將第四層輸出格式(7*7*64)轉化為第五層的輸入格式一個向量
    pool2_shape = pool2.get_shape().as_list()
    nodes = pool2_shape[1] * pool2_shape[2] * pool2_shape[3]   #7*7*64,pool2_shape[0]為一個bantch中資料的個數
    reshaped = tf.reshape(pool2, [pool2_shape[0], nodes])

    #宣告第五層全連線層,輸入7*7*64=3136長度的向量,輸出512
    #引入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))
        fc1_biases = tf.get_variable('bias', [FC_SIZE], initializer=tf.constant_initializer(0.1))
        if regularizer != None:#只有全連線層的權重需要加入正則化
            tf.add_to_collection('losses', regularizer(fc1_weights))
        fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)
        if train:fc1 = tf.nn.dropout(fc1, 0.5)

    #宣告第6層全連線層,輸入512,輸出10,通過softmax之後得到最後的分類結果
    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))
        fc2 = tf.matmul(fc1, fc2_weights) + fc2_biases

    return fc2

mnist_cnn_train.py
import os
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import mnist_cnn_inference
import numpy as np


# 配置神經網路的引數
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.001
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.0001 #描述模型複雜度的正則化項在損失函式中的係數
TRAINING_STEPS = 10000
MOVING_AVERAGE_DECAY = 0.99 #滑動平均衰減率

#模型儲存的路徑和檔名
MODEL_SAVE_PATH = '/mnist_cnn_model/'
MODEL_SAVE_NAME = 'mnist_cnn_model.ckpt'

def train(mnist):
    x = tf.placeholder(tf.float32, [BATCH_SIZE, mnist_cnn_inference.IMAGE_SIZE, mnist_cnn_inference.IMAGE_SIZE,
                                    mnist_cnn_inference.NUM_CHANNELS], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, mnist_cnn_inference.OUTPUT_NODE], name='y-input')
    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)

    #直接使用mnist_cnn_inference.py中定義的前向傳播結果
    y = mnist_cnn_inference.inference(x, train=False, regularizer=regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 生成一個滑動平均的類,並在所有變數上使用滑動平均
    variables_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    variables_averages_op = variables_averages.apply(tf.trainable_variables())

    # 計算交叉熵及當前batch中的所有樣例的交叉熵平均值,並求出損失函式
    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, variables_averages_op]):
        train_op = tf.no_op(name='train')

    #初始化TF持久化類
    saver = tf.train.Saver()

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())

        for i in range(TRAINING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            reshaped_xs = np.reshape(xs, [BATCH_SIZE, mnist_cnn_inference.IMAGE_SIZE, mnist_cnn_inference.IMAGE_SIZE,
                                    mnist_cnn_inference.NUM_CHANNELS])
            _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: reshaped_xs, y_: ys})
            # if i%500 == 0:
            print('After %d training steps, loss on training batch is %g' % (step, loss_value))
            saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_SAVE_NAME), global_step=global_step)

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

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

如何設計卷積神經網路的架構?下面的正則表示式總結了一些經典的用於圖片分類問題的卷積神經網路架構:

輸入層>>(卷積層+ >> 池化層?) >> 全連線層+

在上面的公式中+表示一個或多個,?表示一個或沒有。一般的卷積神經網路都滿足這個結構。在過濾器的深度上,一般卷積網路都採取逐層遞增的方式。


Inception-v3模型

Inception結構是一種和上一個模型結構完全不同的卷積神經網路。在LeNet-5中,不同卷積層通過串聯的方式連線在一起,而Inception結構是將不同的卷積層通過並聯的方式結合在一起。

Inception模組會首先使用不同尺寸的過濾器處理輸入矩陣,不同的矩陣代表Inception模組的一條計算路徑。雖然過濾器的大小不一,但如果所有的過濾器都是用全0填充且步長為1,那麼得到的矩陣的長與寬都與輸入矩陣一致。最後將不同過濾器處理的結果矩陣拼接成更深的矩陣。

Inception-v3模型一共有46層,由11個Inception模組組成,由96個卷積層。由TensorFlow-Slim工具可以更加簡潔的實現一個卷積層,從而更好的實現複雜的卷積神經網路。

#使用TF原始API實現卷積層
with tf.variable_scope('layer1-conv1'):
    conv1_weights = tf.get_variable('weight', [1,1,1,1], initializer=tf.truncated_normal_initializer(stddev=0.1))
    conv1_biases = tf.get_variable('bias', [2], initializer=tf.constant_initializer(0.0))
    conv1 = tf.nn.conv2d(input, conv1_weights, strides=[1, 1, 1, 1], padding='SAME')
    conv1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))
    
    
#使用TensorFlow-Slim實現卷積層
#其中有三個引數必填:輸入節點矩陣,卷積過濾器的深度,卷積過濾器的長和寬
#可選引數:過濾器移動步長、是否使用全0填充,啟用函式的選擇、變數的名稱空間
net = slim.conv2d(input, 32, [3,3])


下面為 Inception-v3模型中的Inception模組的實現

import tensorflow as tf

#slim.arg_scope函式用於設定預設引數取值
#slim.arg_scope第一個引數是一個函式列表,包括下面所要用到的
#在定義函式時則會自動帶上後面的引數
with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d], stride=1, padding='SAME'):
    net = 上一層的輸出
    #整個模組宣告的名稱空間
    with tf.variable_scope('Mixed_7c'):
        #為該模組每一條路徑宣告一個名稱空間
        with tf.variable_scope('Branch_0'):
            branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_0a_1x1')
        with tf.variable_scope('Branch_1'):
            branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_0a_1x1')
            #tf.contact可以將多個矩陣拼接起來,第一個引數3表示矩陣在深度上進行拼接
            branch_1 = tf.concat(3,
                                 [slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'),
                                  slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1')])

        with tf.variable_scope('Branch_2'):
            branch_2 = slim.conv2d(net, 448, [1, 1], scope='Conv2d_0a_1x1')
            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_0d_3x1')])
        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')

        net = tf.concat(3, [branch_0, branch_1, branch_2, branch_3])