卷積神經(jīng)網(wǎng)絡(luò)之VGG(附完整代碼)

前言

VGG是Oxford的Visual Geometry Group的組提出的(大家應(yīng)該能看出VGG名字的由來了)。該網(wǎng)絡(luò)是在ILSVRC 2014上的相關(guān)工作,主要工作是證明了增加網(wǎng)絡(luò)的深度能夠在一定程度上影響網(wǎng)絡(luò)最終的性能。VGG有兩種結(jié)構(gòu),分別是VGG16和VGG19,兩者并沒有本質(zhì)上的區(qū)別,只是網(wǎng)絡(luò)深度不一樣。

VGG原理

VGG16相比AlexNet的一個改進是采用連續(xù)的幾個3x3的卷積核代替AlexNet中的較大卷積核(11x11,7x7,5x5)。對于給定的感受野(與輸出有關(guān)的輸入圖片的局部大小),采用堆積的小卷積核是優(yōu)于采用大的卷積核,因為多層非線性層可以增加網(wǎng)絡(luò)深度來保證學(xué)習更復(fù)雜的模式,而且代價還比較?。▍?shù)更少)。

簡單來說,在VGG中,使用了3個3x3卷積核來代替7x7卷積核,使用了2個3x3卷積核來代替5*5卷積核,這樣做的主要目的是在保證具有相同感知野的條件下,提升了網(wǎng)絡(luò)的深度,在一定程度上提升了神經(jīng)網(wǎng)絡(luò)的效果。

比如,3個步長為1的3x3卷積核的一層層疊加作用可看成一個大小為7的感受野(其實就表示3個3x3連續(xù)卷積相當于一個7x7卷積),其參數(shù)總量為 3x(9xC^2) ,如果直接使用7x7卷積核,其參數(shù)總量為 49xC^2 ,這里 C 指的是輸入和輸出的通道數(shù)。很明顯,27xC2小于49xC2,即減少了參數(shù);而且3x3卷積核有利于更好地保持圖像性質(zhì)。

這里解釋一下為什么使用2個3x3卷積核可以來代替5*5卷積核:

5x5卷積看做一個小的全連接網(wǎng)絡(luò)在5x5區(qū)域滑動,我們可以先用一個3x3的卷積濾波器卷積,然后再用一個全連接層連接這個3x3卷積輸出,這個全連接層我們也可以看做一個3x3卷積層。這樣我們就可以用兩個3x3卷積級聯(lián)(疊加)起來代替一個 5x5卷積。

具體如下圖所示:

至于為什么使用3個3x3卷積核可以來代替7*7卷積核,推導(dǎo)過程與上述類似,大家可以自行繪圖理解。

VGG網(wǎng)絡(luò)結(jié)構(gòu)
下面是VGG網(wǎng)絡(luò)的結(jié)構(gòu)(VGG16和VGG19都在):

VGG16包含了16個隱藏層(13個卷積層和3個全連接層),如上圖中的D列所示
VGG19包含了19個隱藏層(16個卷積層和3個全連接層),如上圖中的E列所示
VGG網(wǎng)絡(luò)的結(jié)構(gòu)非常一致,從頭到尾全部使用的是3x3的卷積和2x2的max pooling。

如果你想看到更加形象化的VGG網(wǎng)絡(luò),可以使用經(jīng)典卷積神經(jīng)網(wǎng)絡(luò)(CNN)結(jié)構(gòu)可視化工具來查看高清無碼的VGG網(wǎng)絡(luò)。

VGG優(yōu)缺點
VGG優(yōu)點
VGGNet的結(jié)構(gòu)非常簡潔,整個網(wǎng)絡(luò)都使用了同樣大小的卷積核尺寸(3x3)和最大池化尺寸(2x2)。

幾個小濾波器(3x3)卷積層的組合比一個大濾波器(5x5或7x7)卷積層好:

驗證了通過不斷加深網(wǎng)絡(luò)結(jié)構(gòu)可以提升性能。

VGG缺點
VGG耗費更多計算資源,并且使用了更多的參數(shù)(這里不是3x3卷積的鍋),導(dǎo)致更多的內(nèi)存占用(140M)。其中絕大多數(shù)的參數(shù)都是來自于第一個全連接層。VGG可是有3個全連接層??!

PS:有的文章稱:發(fā)現(xiàn)這些全連接層即使被去除,對于性能也沒有什么影響,這樣就顯著降低了參數(shù)數(shù)量。

注:很多pretrained的方法就是使用VGG的model(主要是16和19),VGG相對其他的方法,參數(shù)空間很大,最終的model有500多m,AlexNet只有200m,GoogLeNet更少,所以train一個vgg模型通常要花費更長的時間,所幸有公開的pretrained model讓我們很方便的使用。

關(guān)于感受野:

假設(shè)你一層一層地重疊了3個3x3的卷積層(層與層之間有非線性激活函數(shù))。在這個排列下,第一個卷積層中的每個神經(jīng)元都對輸入數(shù)據(jù)體有一個3x3的視野。

代碼篇:VGG訓(xùn)練與測試
這里推薦兩個開源庫,訓(xùn)練請參考tensorflow-vgg,快速測試請參考VGG-in TensorFlow。

代碼我就不介紹了,其實跟上述內(nèi)容一致,跟著原理看code應(yīng)該會很快。我快速跑了一下VGG-in TensorFlow,代碼親測可用,效果很nice,就是model下載比較煩。

70108F1D-3E7A-4591-A036-2E4310E790FA.png
# -- encoding:utf-8 --
"""
Create on 19/5/25 10:06
"""

import os
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 定義外部傳入的參數(shù)
tf.app.flags.DEFINE_bool(flag_name="is_train",
                         default_value=True,
                         docstring="給定是否是訓(xùn)練操作,True表示訓(xùn)練,F(xiàn)alse表示預(yù)測?。?)
tf.app.flags.DEFINE_string(flag_name="checkpoint_dir",
                           default_value="./mnist/models/models_vgg",
                           docstring="給定模型存儲的文件夾,默認為./mnist/models/models_vgg")
tf.app.flags.DEFINE_string(flag_name="logdir",
                           default_value="./mnist/graph/graph_vgg",
                           docstring="給定模型日志存儲的路徑,默認為./mnist/graph/graph_vgg")
tf.app.flags.DEFINE_integer(flag_name="batch_size",
                            default_value=8,
                            docstring="給定訓(xùn)練的時候每個批次的樣本數(shù)目,默認為16.")
tf.app.flags.DEFINE_integer(flag_name="store_per_batch",
                            default_value=100,
                            docstring="給定每隔多少個批次進行一次模型持久化的操作,默認為100")
tf.app.flags.DEFINE_integer(flag_name="validation_per_batch",
                            default_value=100,
                            docstring="給定每隔多少個批次進行一次模型的驗證操作,默認為100")
tf.app.flags.DEFINE_float(flag_name="learning_rate",
                          default_value=0.001,
                          docstring="給定模型的學(xué)習率,默認0.01")
FLAGS = tf.app.flags.FLAGS


def create_dir_with_not_exits(dir_path):
    """
    如果文件的文件夾路徑不存在,直接創(chuàng)建
    :param dir_path:
    :return:
    """
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)


def layer_normalization(net, eps=1e-8):
    # 縮放參數(shù)、平移參數(shù)y=gamma * x + beta
    gamma = tf.get_variable('gamma', shape=[],
                            initializer=tf.constant_initializer(1))
    beta = tf.get_variable('beta', shape=[],
                           initializer=tf.constant_initializer(0))

    # 計算當前批次的均值和標準差
    mean, variance = tf.nn.moments(net, axes=(1, 2, 3), keep_dims=True)

    # 執(zhí)行批歸一化操作
    return tf.nn.batch_normalization(net, mean, variance,
                                     offset=beta, scale=gamma, variance_epsilon=eps)


def create_model(input_x, show_image=False):
    """
    構(gòu)建模型(VGG 11)
    :param input_x: 占位符,格式為[None, 784]
    :param show_image:是否可視化圖像
    :return:
    """
    # 定義一個網(wǎng)絡(luò)結(jié)構(gòu):  conv3-64 -> LRN -> MaxPooling -> conv3-128 -> MaxPooling -> conv3-256 -> conv3-256 -> MaxPooling -> FC1024 -> FC10
    with tf.variable_scope("net",
                           initializer=tf.random_normal_initializer(0.0, 0.0001)):
        with tf.variable_scope("Input"):
            # 這里定義一些圖像的處理方式,包括:格式轉(zhuǎn)換、基礎(chǔ)處理(大小、剪切...)
            net = tf.reshape(input_x, shape=[-1, 28, 28, 1])
            print(net.get_shape())

            if show_image:
                # 可視化圖像
                tf.summary.image(name='image', tensor=net, max_outputs=5)

        # 定義一個網(wǎng)絡(luò)結(jié)構(gòu)
        # layers = [
        #     ["conv", 3, 3, 1, 64, 1, "relu"],
        #     ["lrn"],
        #     ["max_pooling", 2, 2, 2],
        #     ["conv", 3, 3, 1, 128, 1, "relu"],
        #     ["max_pooling", 2, 2, 2],
        #     ["conv", 3, 3, 1, 256, 2, "relu"],
        #     ["max_pooling", 2, 2, 2],
        #     ["reshape"],
        #     ["FC", 1024, "relu"],
        #     ["FC", 10]
        # ]
        # layers = [
        #     ["conv", 3, 3, 1, 64, 1, "relu"],
        #     ["lrn"],
        #     ["max_pooling", 2, 2, 2],
        #     ["conv", 3, 3, 1, 128, 2, "relu"],
        #     ["ln"],
        #     ["max_pooling", 2, 2, 2],
        #     ["conv", 3, 3, 1, 256, 2, "relu"],
        #     ["ln"],
        #     ["max_pooling", 2, 2, 2],
        #     ["reshape"],
        #     ["FC", 1024, "relu"],
        #     ["FC", 10]
        # ]
        layers = [
            ["conv", 3, 3, 1, 32, 2, "relu"],
            ["max_pooling", 2, 2, 2],
            ["conv", 3, 3, 1, 64, 2, "relu"],
            # 第一個是池化,第二個窗口高度,第三個是窗口的寬度,第四個是步長
            ["max_pooling", 2, 2, 2],
            ["reshape"],
            ["FC", 1024, "relu"],
            ["FC", 10]
        ]
        for idx, layer in enumerate(layers):
            shape = net.get_shape()
            name = layer[0]
            if "conv" == name:
                # a. 獲取相關(guān)的參數(shù)
                # ["conv", 3, 3, 1, 64, 1, "relu" ] -> 名稱 窗口高度 窗口寬度 步長(一個值) 輸出通道數(shù) 重復(fù)幾個卷積 激活函數(shù)(None表示不激活)
                filter_height, filter_width, stride, out_channels, num_conv = layer[1:6]
                try:
                    ac = layer[6]
                except:
                    ac = None

                # 遍歷進行卷積層的構(gòu)建
                for i in range(num_conv):
                    with tf.variable_scope("CONV_{}_{}".format(idx, i)):
                        # 獲取當前卷積的輸入的通道數(shù)
                        shape = net.get_shape()
                        in_channels = shape[-1]
                        # 構(gòu)建變量
                        filter = tf.get_variable(name='w', shape=[filter_height, filter_width,
                                                                  in_channels, out_channels])
                        bias = tf.get_variable(name='b', shape=[out_channels])
                        # 卷積操作
                        net = tf.nn.conv2d(input=net, filter=filter,
                                           strides=[1, stride, stride, 1], padding='SAME')
                        net = tf.nn.bias_add(net, bias)
                        # 做一個激活操作
                        if ac is not None:
                            if "relu" == ac:
                                net = tf.nn.relu(net)
                            elif "relu6" == ac:
                                net = tf.nn.relu6(net)
                            else:
                                net = tf.nn.sigmoid(net)

                if show_image:
                    # 對于卷積之后的值做一個可視化操作
                    shape = net.get_shape()
                    for k in range(shape[-1]):
                        image_tensor = tf.reshape(net[:, :, :, k], shape=[-1, shape[1], shape[2], 1])
                        tf.summary.image(name='image', tensor=image_tensor, max_outputs=5)
            elif "lrn" == name:
                with tf.variable_scope("LRN_{}".format(idx)):
                    # lrn(input, depth_radius=5, bias=1, alpha=1, beta=0.5, name=None)
                    # depth_radius就是ppt上的n,bias就是ppt上的k,beta就是β,alpha就是α
                    net = tf.nn.local_response_normalization(input=net, depth_radius=5,
                                                             bias=1, alpha=1, beta=0.5)
            elif "max_pooling" == name:
                with tf.variable_scope("Max_Pooling_{}".format(idx)):
                    ksize_height = layer[1]
                    ksize_width = layer[2]
                    stride = layer[3]
                    net = tf.nn.max_pool(value=net,
                                         ksize=[1, ksize_height, ksize_width, 1],
                                         strides=[1, stride, stride, 1], padding='SAME')
            elif "FC" == name:
                with tf.variable_scope("FC_{}".format(idx)):
                    # 獲取相關(guān)變量,輸入的維度,輸出的維度大小以及激活函數(shù)
                    dim_size = shape[-1]
                    unit_size = layer[1]
                    try:
                        ac = layer[2]
                    except:
                        ac = None
                    w = tf.get_variable(name='w', shape=[dim_size, unit_size])
                    b = tf.get_variable(name='b', shape=[unit_size])
                    net = tf.matmul(net, w) + b
                    # 做一個激活操作
                    if ac is not None:
                        if "relu" == ac:
                            net = tf.nn.relu(net)
                        elif "relu6" == ac:
                            net = tf.nn.relu6(net)
                        else:
                            net = tf.nn.sigmoid(net)
            elif "reshape" == name:
                with tf.variable_scope('reshape'):
                    dim_size = shape[1] * shape[2] * shape[3]
                    net = tf.reshape(net, shape=[-1, dim_size])
            elif "ln" == name:
                with tf.variable_scope("LN_{}".format(idx)):
                    net = layer_normalization(net)

        with tf.variable_scope("Prediction"):
            # 每行的最大值對應(yīng)的下標就是當前樣本的預(yù)測值
            predictions = tf.argmax(net, axis=1)

    return net, predictions


def create_loss(labels, logits):
    """
    基于給定的實際值labels和預(yù)測值logits進行一個交叉熵損失函數(shù)的構(gòu)建
    :param labels:  是經(jīng)過啞編碼之后的Tensor對象,形狀為[n_samples, n_class]
    :param logits:  是神經(jīng)網(wǎng)絡(luò)的最原始的輸出,形狀為[n_samples, n_class], 每一行最大值那個位置對應(yīng)的就是預(yù)測類別,沒有經(jīng)過softmax函數(shù)轉(zhuǎn)換。
    :return:
    """
    with tf.name_scope("loss"):
        # loss = tf.reduce_mean(-tf.log(tf.reduce_sum(labels * tf.nn.softmax(logits))))
        loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=labels, logits=logits))
        tf.summary.scalar('loss', loss)
    return loss


def create_train_op(loss, learning_rate=0.0001, global_step=None):
    """
    基于給定的損失函數(shù)構(gòu)建一個優(yōu)化器,優(yōu)化器的目的就是讓這個損失函數(shù)最小化
    :param loss:
    :param learning_rate:
    :param global_step:
    :return:
    """
    with tf.name_scope("train"):
        optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
        train_op = optimizer.minimize(loss, global_step=global_step)
    return train_op


def create_accuracy(labels, predictions):
    """
    基于給定的實際值和預(yù)測值,計算準確率
    :param labels:  是經(jīng)過啞編碼之后的Tensor對象,形狀為[n_samples, n_class]
    :param predictions: 實際的預(yù)測類別下標,形狀為[n_samples,]
    :return:
    """
    with tf.name_scope("accuracy"):
        # 獲取實際的類別下標,形狀為[n_samples,]
        y_labels = tf.argmax(labels, 1)
        # 計算準確率
        accuracy = tf.reduce_mean(tf.cast(tf.equal(y_labels, predictions), tf.float32))
        tf.summary.scalar('accuracy', accuracy)
    return accuracy


def train():
    # 對于文件是否存在做一個檢測
    create_dir_with_not_exits(FLAGS.checkpoint_dir)
    create_dir_with_not_exits(FLAGS.logdir)

    with tf.Graph().as_default():
        # 一、執(zhí)行圖的構(gòu)建
        # 0. 相關(guān)輸入Tensor對象的構(gòu)建
        input_x = tf.placeholder(dtype=tf.float32, shape=[None, 784], name='input_x')
        input_y = tf.placeholder(dtype=tf.float32, shape=[None, 10], name='input_y')
        global_step = tf.train.get_or_create_global_step()

        # 1. 網(wǎng)絡(luò)結(jié)構(gòu)的構(gòu)建
        logits, predictions = create_model(input_x)
        # 2. 構(gòu)建損失函數(shù)
        loss = create_loss(input_y, logits)
        # 3. 構(gòu)建優(yōu)化器
        train_op = create_train_op(loss,
                                   learning_rate=FLAGS.learning_rate,
                                   global_step=global_step)
        # 4. 構(gòu)建評估指標
        accuracy = create_accuracy(input_y, predictions)

        # 二、執(zhí)行圖的運行/訓(xùn)練(數(shù)據(jù)加載、訓(xùn)練、持久化、可視化、模型的恢復(fù)....)
        with tf.Session() as sess:
            # a. 創(chuàng)建一個持久化對象(默認會將所有的模型參數(shù)全部持久化,因為不是所有的都需要的,最好僅僅持久化的訓(xùn)練的模型參數(shù))
            var_list = tf.trainable_variables()
            # 是因為global_step這個變量是不參與模型訓(xùn)練的,所以模型不會持久化,這里加入之后,可以明確也持久化這個變量。
            var_list.append(global_step)
            saver = tf.train.Saver(var_list=var_list)

            # a. 變量的初始化操作(所有的非訓(xùn)練變量的初始化 + 持久化的變量恢復(fù))
            # 所有變量初始化(如果有持久化的,后面做了持久化后,會覆蓋的)
            sess.run(tf.global_variables_initializer())
            # 做模型的恢復(fù)操作
            ckpt = tf.train.get_checkpoint_state(FLAGS.checkpoint_dir)
            if ckpt and ckpt.model_checkpoint_path:
                print("進行模型恢復(fù)操作...")
                # 恢復(fù)模型
                saver.restore(sess, ckpt.model_checkpoint_path)
                # 恢復(fù)checkpoint的管理信息
                saver.recover_last_checkpoints(ckpt.all_model_checkpoint_paths)

            # 獲取一個日志輸出對象
            train_logdir = os.path.join(FLAGS.logdir, 'train')
            validation_logdir = os.path.join(FLAGS.logdir, 'validation')
            train_writer = tf.summary.FileWriter(logdir=train_logdir, graph=sess.graph)
            validation_writer = tf.summary.FileWriter(logdir=validation_logdir, graph=sess.graph)
            # 獲取所有的summary輸出操作
            summary = tf.summary.merge_all()

            # b. 訓(xùn)練數(shù)據(jù)的產(chǎn)生/獲?。ɑ趎umpy隨機產(chǎn)生<可以先考慮一個固定的數(shù)據(jù)集>)
            mnist = input_data.read_data_sets(
                train_dir='../datas/mnist',  # 給定本地磁盤的數(shù)據(jù)存儲路徑
                one_hot=True,  # 給定返回的數(shù)據(jù)中是否對Y做啞編碼
                validation_size=5000  # 給定驗證數(shù)據(jù)集的大小
            )

            # c. 模型訓(xùn)練
            batch_size = FLAGS.batch_size
            step = sess.run(global_step)
            vn_accuracy_ = 0
            while True:
                # 開始模型訓(xùn)練
                x_train, y_train = mnist.train.next_batch(batch_size=batch_size)
                _, loss_, accuracy_, summary_ = sess.run([train_op, loss, accuracy, summary], feed_dict={
                    input_x: x_train,
                    input_y: y_train
                })
                print("第{}次訓(xùn)練后模型的損失函數(shù)為:{}, 準確率:{}".format(step, loss_, accuracy_))
                train_writer.add_summary(summary_, global_step=step)

                # 持久化
                if step % FLAGS.store_per_batch == 0:
                    file_name = 'model_%.3f_%.3f_.ckpt' % (loss_, accuracy_)
                    save_path = os.path.join(FLAGS.checkpoint_dir, file_name)
                    saver.save(sess, save_path=save_path, global_step=step)

                if step % FLAGS.validation_per_batch == 0:
                    vn_loss_, vn_accuracy_, vn_summary_ = sess.run([loss, accuracy, summary],
                                                                   feed_dict={
                                                                       input_x: mnist.validation.images,
                                                                       input_y: mnist.validation.labels
                                                                   })
                    print("第{}次訓(xùn)練后模型在驗證數(shù)據(jù)上的損失函數(shù)為:{}, 準確率:{}".format(step,
                                                                    vn_loss_,
                                                                    vn_accuracy_))
                    validation_writer.add_summary(vn_summary_, global_step=step)

                # 退出訓(xùn)練(要求當前的訓(xùn)練數(shù)據(jù)集上的準確率至少為0.8,然后最近一次驗證數(shù)據(jù)上的準確率為0.8)
                if accuracy_ > 0.99 and vn_accuracy_ > 0.99:
                    # 退出之前再做一次持久化操作
                    file_name = 'model_%.3f_%.3f_.ckpt' % (loss_, accuracy_)
                    save_path = os.path.join(FLAGS.checkpoint_dir, file_name)
                    saver.save(sess, save_path=save_path, global_step=step)
                    break
                step += 1
            # 關(guān)閉輸出流
            train_writer.close()
            validation_writer.close()


def prediction():
    # TODO: 參考以前的代碼自己把這個區(qū)域的內(nèi)容填充一下。我下周晚上講。
    # 做一個預(yù)測(預(yù)測的評估,對mnist.test這個里面的數(shù)據(jù)進行評估效果的查看)
    with tf.Graph().as_default():
        pass


def main(_):
    if FLAGS.is_train:
        # 進入訓(xùn)練的代碼執(zhí)行中
        print("開始進行模型訓(xùn)練運行.....")
        train()
    else:
        # 進入測試、預(yù)測的代碼執(zhí)行中
        print("開始進行模型驗證、測試代碼運行.....")
        prediction()
    print("Done!!!!")


if __name__ == '__main__':
    # 默認情況下,直接調(diào)用當前py文件中的main函數(shù)
    tf.app.run()

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容