利用卷積神經網絡(VGG19)實現火災分類(附tensorflow代碼及訓練集)

源碼地址 https://github.com/stephen-v/tensorflow_vgg_classifypython

1. VGG介紹

1.1. VGG模型結構

VGG網絡是牛津大學Visual Geometry Group團隊研發搭建,該項目的主要目的是證實增長網絡深度可以在必定程度上提升網絡的精度。VGG有5種模型,A-E,其中的E模型VGG19是參加ILSVRC 2014挑戰賽使用的模型,並得到了ILSVRC定位第一名,和分類第二名的成績。整個過程證實,經過把網絡深度增長到16-19層確實可以提升網絡性能。VGG網絡跟以前學習的LeNet網絡和AlexNet網絡有不少類似之處,如下搭建的VGG19模型也像上一次搭建的AlexNet同樣,分紅了5個大的卷積層,和3個大的全鏈層,不一樣的是,VGG的5個卷積層層數相應增長了;同時,爲了減小網絡訓練參數的數量,整個卷積網絡均使用3X3大小的卷積。git

首先來看看原論文中VGG網絡的5種模型結構。A-E模型均是由5個stage和3個全鏈層和一個softmax分類層組成,其中每一個stege有一個max-pooling層和多個卷積層。每層的卷積核個數從首階段的64個開始,每一個階段增加一倍,直到達到512個。github

A:是最基本的模型,8個卷基層,3個全鏈接層,一共11層。
A-LRN:忽略
B:在A的基礎上,在stage1和stage2基礎上分別增長了1層3X3卷積層,一共13層。
C:在B的基礎上,在stage3,stage4和stage5基礎上分別增長了一層1X1的卷積層,一共16層。
D:在B的基礎上,在stage3,stage4和stage5基礎上分別增長了一層3X3的卷積層,一共16層。
E:在D的基礎上,在stage3,stage4和stage5基礎上分別增長了一層3X3的卷積層,一共19層。算法

模型D是就是常常說的VGG16網絡,模型E則爲VGG19網絡。markdown

2017-10-30-16-00-23

雖然VGG網絡使用的均是3X3的卷積filter,極大的減少了參數個數,但和AlexNet比較起來,參數個數仍是至關的多,以模型D爲例,每一層的參數個數以下表所示,總參數個數爲1.3億左右,龐大的參數增長了訓練的時間,下一章單搭建的VGG19模型僅在CPU上進行訓練,單單一個epoch就要訓練8小時以上!網絡

2017-10-31-09-49-44

儘管VGG19有那麼多的參數,可是在訓練過程當中,做者發現VGG須要不多的迭代次數就開始收斂了,這是由於:
一、深度和小的filter尺寸起到了隱式的規則化做用
二、一些層的pre-initialisation
怎麼作pre-initialisation呢?做者先訓練最淺的網絡A,而後把A的前4個卷積層和最後全鏈層的權值看成其餘網絡的初始值,未賦值的中間層經過隨機初始化進行訓練。這樣避免了很差的權值初始值對於網絡訓練的影響,從而加快了收斂。架構

爲何在整個VGG網絡中都用的是3X3大小的filter呢,VGG團隊給出了下面的解釋:
一、3 * 3是最小的可以捕獲上下左右和中心概念的尺寸。ide

二、兩個3 * 3的卷基層的有限感覺野是5X5;三個3X3的感覺野是7X7,能夠替代大的filter尺寸。(感覺野表示網絡內部的不一樣位置的神經元對原圖像的感覺範圍大小,神經元感覺野的值越大表示其能接觸到的原始圖像範圍就越大,也意味着他可能蘊含更爲全局、語義層次更高的特徵;而值越小則表示其所包含的特徵越趨向於局部和細節。)函數

三、多個3 * 3的卷基層比一個大尺寸filter卷基層有更多的非線性,使得判決函數更加具備判決性。性能

四、多個3 * 3的卷積層比一個大尺寸的filter有更少的參數,假設卷基層的輸入和輸出的特徵圖大小相同爲C,那麼三個3 * 3的卷積層參數個數爲\(3(3^2C^2)=27C^2\);一個7 * 7的卷積層參數爲\(49C^2\),整整比3 * 3的多了81%。

1.2. VGG19架構

首先來看看論文中描述的VGG19的網絡結構圖,輸入是一張224X224大小的RGB圖片,在輸入圖片以前,仍然要對圖片的每個像素進行RGB數據的轉換和提取。而後使用3X3大小的卷積核進行卷積,做者在論文中描述了使用3X3filter的意圖:
「we use filters with a very small receptive field: 3 × 3 (which is the smallest size to capture the notion of left/right, up/down, center).」
即上面提到的「3X3是最小的可以捕獲上下左右和中心概念的尺寸」。接着圖片依次通過5個Stage和3層全連層的處理,一直到softmax輸出分類。卷積核深度從64一直增加到512,更好的提取了圖片的特徵向量。

2017-10-30-12-34-33

Stage1
包含兩個卷積層,一個池化層,每一個卷積層和池化層的信息以下:

卷積核 深度 步長
3 * 3 64 1 * 1

Stage2
包含兩個卷積層,一個池化層,每一個卷積層和池化層的信息以下:

卷積核 深度 步長
3 * 3 128 1 * 1

Stage3
包含四個卷積層,一個池化層,每一個卷積層和池化層的信息以下:

卷積核 深度 步長
3 * 3 256 1 * 1

Stage4
包含四個卷積層,一個池化層,每一個卷積層和池化層的信息以下:

卷積核 深度 步長
3 * 3 512 1 * 1

Stage5
包含四個卷積層,一個池化層,每一個卷積層和池化層的信息以下:

卷積核 深度 步長
3 * 3 512 1 * 1

池化層
整個網絡包含5個池化層,分別位於每個Stage的後面,每一個池化層的尺寸均同樣,以下:

池化層過濾器 步長
2 * 2 2 * 2

對於其餘的隱藏層,做者在論文中作了以下闡述:
「All hidden layers are equipped with the rectification (ReLU (Krizhevsky et al., 2012)) non-linearity.We note that none of our networks (except for one) contain Local Response Normalisation(LRN) normalisation (Krizhevsky et al., 2012): as will be shown in Sect. 4, such normalisation does not improve the performance on the ILSVRC dataset, but leads to increased memory consumption and computation time. 」

整個網絡不包含LRN,由於LRN會佔用內存和增長計算時間。接着通過3個全鏈層的處理,由Softmax輸出1000個類別的分類結果。

2. 用Tensorflow搭建VGG19網絡

VGG團隊早已用Tensorflow搭建好了VGG16和VGG19網絡,在使用他們的網絡前,你須要下載已經訓練好的參數文件vgg19.npy,下載地址爲:https://mega.nz/#!xZ8glS6J!MAnE91ND_WyfZ_8mvkuSa2YcA7q-1ehfSm-Q1fxOvvs 。原版的VGG16/19模型代碼在 https://github.com/machrisaa/tensorflow-vgg (該模型中提到的weights文件已不可用), 咱們根據該模型代碼對VGG19網絡作了一些微調以適應本身的訓練需求,同時也像上一篇的AlexNet同樣,增長了精調訓練代碼,後面會有介紹。

使用Tensorflow來搭建一個完整的VGG19網絡,包含我修改過的整整用了160行代碼,以下附上一部分代碼,該網絡也是VGG團隊已經訓練好了的,你能夠拿來直接進行圖片識別和分類,可是若是你有其餘的圖片識別需求,你須要用本身的訓練集來訓練一次以得到想要的結果,並存儲好本身的權重文件。

咱們在原版的基礎上作了一些改動,增長了入參num_class,該參數表明分類個數,若是你有100個種類的圖片須要訓練,這個值必須設置成100,以此類推。

class Vgg19(object):
    """
    A trainable version VGG19.
    """

    def __init__(self, bgr_image, num_class, vgg19_npy_path=None, trainable=True, dropout=0.5):
        if vgg19_npy_path is not None:
            self.data_dict = np.load(vgg19_npy_path, encoding='latin1').item()
        else:
            self.data_dict = None
        self.BGR_IMAGE = bgr_image
        self.NUM_CLASS = num_class
        self.var_dict = {}
        self.trainable = trainable
        self.dropout = dropout

        self.build()

    def build(self, train_mode=None):

        self.conv1_1 = self.conv_layer(self.BGR_IMAGE, 3, 64, "conv1_1")
        self.conv1_2 = self.conv_layer(self.conv1_1, 64, 64, "conv1_2")
        self.pool1 = self.max_pool(self.conv1_2, 'pool1')

        self.conv2_1 = self.conv_layer(self.pool1, 64, 128, "conv2_1")
        self.conv2_2 = self.conv_layer(self.conv2_1, 128, 128, "conv2_2")
        self.pool2 = self.max_pool(self.conv2_2, 'pool2')

        self.conv3_1 = self.conv_layer(self.pool2, 128, 256, "conv3_1")
        self.conv3_2 = self.conv_layer(self.conv3_1, 256, 256, "conv3_2")
        self.conv3_3 = self.conv_layer(self.conv3_2, 256, 256, "conv3_3")
        self.conv3_4 = self.conv_layer(self.conv3_3, 256, 256, "conv3_4")
        self.pool3 = self.max_pool(self.conv3_4, 'pool3')

        self.conv4_1 = self.conv_layer(self.pool3, 256, 512, "conv4_1")
        self.conv4_2 = self.conv_layer(self.conv4_1, 512, 512, "conv4_2")
        self.conv4_3 = self.conv_layer(self.conv4_2, 512, 512, "conv4_3")
        self.conv4_4 = self.conv_layer(self.conv4_3, 512, 512, "conv4_4")
        self.pool4 = self.max_pool(self.conv4_4, 'pool4')

        self.conv5_1 = self.conv_layer(self.pool4, 512, 512, "conv5_1")
        self.conv5_2 = self.conv_layer(self.conv5_1, 512, 512, "conv5_2")
        self.conv5_3 = self.conv_layer(self.conv5_2, 512, 512, "conv5_3")
        self.conv5_4 = self.conv_layer(self.conv5_3, 512, 512, "conv5_4")
        self.pool5 = self.max_pool(self.conv5_4, 'pool5')

        self.fc6 = self.fc_layer(self.pool5, 25088, 4096, "fc6")
        self.relu6 = tf.nn.relu(self.fc6)
        if train_mode is not None:
            self.relu6 = tf.cond(train_mode, lambda: tf.nn.dropout(self.relu6, self.dropout), lambda: self.relu6)
        elif train_mode:
            self.relu6 = tf.nn.dropout(self.relu6, self.dropout)

        self.fc7 = self.fc_layer(self.relu6, 4096, 4096, "fc7")
        self.relu7 = tf.nn.relu(self.fc7)
        if train_mode is not None:
            self.relu7 = tf.cond(train_mode, lambda: tf.nn.dropout(self.relu7, self.dropout), lambda: self.relu7)
        elif train_mode:
            self.relu7 = tf.nn.dropout(self.relu7, self.dropout)

        self.fc8 = self.fc_layer(self.relu7, 4096, self.NUM_CLASS, "fc8")

        self.prob = tf.nn.softmax(self.fc8, name="prob")

        self.data_dict = None

使用Tenforflow來搭建網絡確實代碼量比較大,網上有使用Keras來搭建的,而且能夠不用訓練,直接用於圖片識別,代碼量少,使用簡單方便,感興趣的同窗能夠去 https://gist.github.com/baraldilorenzo/07d7802847aaad0a35d3#file-vgg-16_keras-py-L24 看看,因爲Keras已經發布了新版本,這個github上的代碼存在一些問題,須要作一些修改,附上我本身修改好的代碼,僅有70多行就能夠進行使用了。以前看了兩天Keras的官方document,在使用fit方法訓練的時候,入參就有epoch的設置,感受不須要用到for循環,同時,不須要自定義Optimizer和acuuracy,只需指定名字就能夠了,簡直方便快捷,可是對於如何把每個epoch獲得的accuracy和loss用相似tensorboard視圖的方式顯示出來我就不太清楚了,若有知道的同窗,請不吝賜教。

from keras.models import Sequential
from keras.layers.core import Flatten, Dense, Dropout
from keras.layers.convolutional import Conv2D, MaxPooling2D, ZeroPadding2D


def VGG_16(weights_path=None):
    model = Sequential()
    model.add(ZeroPadding2D((1, 1), input_shape=(224, 224, 3)))
    model.add(Conv2D(64, (3, 3), activation='relu'))
    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(64, (3, 3), activation='relu'))
    model.add(MaxPooling2D((2, 2), strides=(2, 2)))

    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(128, (3, 3), activation='relu'))
    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(128, (3, 3), activation='relu'))
    model.add(MaxPooling2D((2, 2), strides=(2, 2)))

    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(256, (3, 3), activation='relu'))
    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(256, (3, 3), activation='relu'))
    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(256, (3, 3), activation='relu'))
    model.add(MaxPooling2D((2, 2), strides=(2, 2)))

    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(512, (3, 3), activation='relu'))
    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(512, (3, 3), activation='relu'))
    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(512, (3, 3), activation='relu'))
    model.add(MaxPooling2D((2, 2), strides=(2, 2)))

    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(512, (3, 3), activation='relu'))
    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(512, (3, 3), activation='relu'))
    model.add(ZeroPadding2D((1, 1)))
    model.add(Conv2D(512, (3, 3), activation='relu'))
    model.add(MaxPooling2D((2, 2), strides=(2, 2)))

    model.add(Flatten())
    model.add(Dense(4096, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(4096, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(1000, activation='softmax'))

    if weights_path:
        model.load_weights(weights_path)

    return model


if __name__ == "__main__":
    im = cv2.resize(cv2.imread('cat.jpg'), (224, 224)).astype(np.float32)
    im[:,:,0] -= 103.939
    im[:,:,1] -= 116.779
    im[:,:,2] -= 123.68
    im = im.transpose((2,0,1))
    im = np.expand_dims(im, axis=0)

    # Test pretrained model
    model = VGG_16('vgg16_weights.h5')
    sgd = SGD(lr=0.1, decay=1e-6, momentum=0.9, nesterov=True)
    model.compile(optimizer=sgd, loss='categorical_crossentropy')
    out = model.predict(im)
    print (np.argmax(out))

3. 訓練網絡

雖然使用訓練好的網絡和網絡權值能夠直接進行分類識別應用,可是本着學習研究的精神,咱們須要知道如何訓練數據以及測試和驗證網絡模型。

目前,咱們有項識別火災的任務(該項目的數據集來自一位挪威教授,他的github地址爲:https://github.com/UIA-CAIR/Fire-Detection-Image-Dataset ,他使用的是改進過的VGG16網絡),須要使用VGG19進行訓練,而一般模型訓練須要的正樣本和負樣本數量要相等,而且數據集越多越好,但在本次訓練中,全部圖片均是做者從網絡抓取而來,且訓練集中的fire的圖片和non-fire圖片是不相等的,分別爲223張和445張(原圖沒有那麼多,咱們本身增長了一些火災圖片),測試集中的fire圖片和non-fire的圖片則相等,均爲50張。

對於如何獲取batch數據,在以前的AlexNet中使用的是數據迭代器,在本次訓練中咱們使用Tensorflow的隊列來自動獲取每一個batch的數據,使用隊列能夠把圖片及對應的標籤準確的取出來,同時還自動打亂順序,很是好使用。因爲使用了tf.train.slice_input_producer創建了文件隊列,所以必定要記住,在訓練圖片的時候須要運行tf.train.start_queue_runners(sess=sess)這樣數據纔會真正的填充進隊列,不然程序將會掛起。
程序代碼以下:

import tensorflow as tf

VGG_MEAN = tf.constant([123.68, 116.779, 103.939], dtype=tf.float32)



class ImageDataGenerator(object):
    def __init__(self, images, labels, batch_size, num_classes):
        self.filenames = images
        self.labels = labels
        self.batch_size = batch_size
        self.num_class = num_classes
        self.image_batch, self.label_batch = self.image_decode()


    def image_decode(self):
        # 創建文件隊列,把圖片和對應的實際標籤放入隊列中
        #注:在沒有運行tf.train.start_queue_runners(sess=sess)以前,數據其實是沒有放入隊列中的
        file_queue = tf.train.slice_input_producer([self.filenames, self.labels])

        # 把圖片數據轉化爲三維BGR矩陣
        image_content = tf.read_file(file_queue[0])
        image_data = tf.image.decode_jpeg(image_content, channels=3)
        image = tf.image.resize_images(image_data, [224, 224])
        img_centered = tf.subtract(image, VGG_MEAN)
        img_bgr = img_centered[:, :, ::-1]

        labels = tf.one_hot(file_queue[1],self.num_class, dtype=tf.uint8)

        # 分batch從文件隊列中讀取數據
        image_batch, label_batch = tf.train.shuffle_batch([img_bgr, labels],
                                                          batch_size=self.batch_size,
                                                          capacity=2000,
                                                          min_after_dequeue=1000)
        return image_batch, label_batch

在精調中,代碼和以前的AlexNet差很少,只是去掉了自定義的數據迭代器。整個VGG19網絡一共訓練100個epochs,每一個epoch有100個迭代,同時使用交叉熵和梯度降低算法精調VGG19網絡的最後三個全鏈層fc6,fc7,fc8。衡量網絡性能的精確度(Precision)、召回率(Recall)及F1值咱們沒有使用,簡單使用了準確率這一指標。值得注意的是,在訓練獲取數據以前,必定要運行tf.train.start_queue_runners(sess=sess),這樣才能保證數據真實的填充入文件隊列。經過使用Tensorflow的隊列存取數據,整個精調代碼比AlexNet要精簡一些,部分代碼以下:

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

    # 運行隊列
    tf.train.start_queue_runners(sess=sess)

    # 把模型圖加入TensorBoard
    writer.add_graph(sess.graph)

    # 總共訓練100代
    for epoch in range(num_epochs):
        print("{} Epoch number: {} start".format(datetime.now(), epoch + 1))
        # 開始訓練每一代
        for step in range(num_iter):
            img_batch = sess.run(training.image_batch)
            label_batch = sess.run(training.label_batch)
            sess.run(train_op, feed_dict={x: img_batch, y: label_batch})

在測試網絡性能的時候,之前採用的準確率(Accuracy)是不太準備的,首先看準確率計算公式(以下),這個指標有很大的缺陷,在正負樣本不平衡的狀況下,好比,負樣本量很大,即便大部分正樣本預測正確,所佔的比例也是比較少的。因此,在統計學中經常使用精確率(Precision)、召回率(Recall)和二者的調和平均F1值來衡量一個網絡的性能。

準確率(Accuracy)的計算公式爲:
\[Accuracy=\frac{TP + TN}{TP + TN + FP +FN}\]

其中:
True Positive(真正, TP):將正類預測爲正類數.
True Negative(真負 , TN):將負類預測爲負類數.
False Positive(假正, FP):將負類預測爲正類數 →→ 誤報 (Type I error).
False Negative(假負 , FN):將正類預測爲負類數 →→ 漏報 (Type II error).

精確率(Precision)的計算公式爲:
\[Precision=\frac{TP}{TP + FP}\]

召回率(Recall)的計算公式爲:
\[Recall=\frac{TP}{TP + FN}\]

二者的調和平均F1:
\[F1=\frac{2TP}{2TP + FP +FN}\]

精確率(Precision)是針對預測結果而言的,它表示的是預測爲正的樣本中有多少是對的。有兩種值,一種就是把正類預測爲正類(TP),另外一種就是把負類預測爲正類(FP)。精確率又叫查準率。
召回率(Recall)是針對原來的樣本而言,它表示的是樣本中的正例有多少被預測正確了。也有兩種值,一種是把原來的正類預測成正類(TP),另外一種就是把原來的正類預測爲負類(FN)。召回率又稱查全率。
F1爲精確率和召回率的調和平均,當二者值較高時,F1值也較高。

測試網絡精確度代碼以下:

print("{} Start testing".format(datetime.now()))

tp = tn = fn = fp = 0

    for _ in range(num_iter):
        img_batch = sess.run(testing.image_batch)
        label_batch = sess.run(testing.label_batch)
        softmax_prediction = sess.run(score, feed_dict={x: img_batch, y: label_batch})
        prediction_label = sess.run(tf.argmax(softmax_prediction, 1))
        actual_label = sess.run(tf.argmax(label_batch, 1))

        for i in range(len(prediction_label)):
            if prediction_label[i] == actual_label[i] == 1:
                tp += 1
            elif prediction_label[i] == actual_label[i] == 0:
                tn += 1
            elif prediction_label[i] == 1 and actual_label[i] == 0:
                fp += 1
            elif prediction_label[i] == 0 and actual_label[i] == 1:
                fn += 1

    precision = tp / (tp + fp)
    recall = tp / (tp + fn)
    f1 = (2 * tp) / (2 * tp + fp + fn)  # f1爲精確率precision和召回率recall的調和平均
    print("{} Testing Precision = {:.4f}".format(datetime.now(), precision))
    print("{} Testing Recall = {:.4f}".format(datetime.now(), recall))
    print("{} Testing F1 = {:.4f}".format(datetime.now(), f1))

通過一天的訓練和測試,網絡精確度(Precision)爲60%左右,召回率(Recall)爲95%,F1值爲72.6%。因爲圖片較少,所以性能指標不是很理想,後續咱們接着改進。
下面開始驗證網絡。首先在網絡上任選幾張圖片,而後編寫代碼以下,

class_name = ['not fire', 'fire']


def test_image(path_image, num_class):
    img_string = tf.read_file(path_image)
    img_decoded = tf.image.decode_png(img_string, channels=3)
    img_resized = tf.image.resize_images(img_decoded, [224, 224])
    img_resized = tf.reshape(img_resized, shape=[1, 224, 224, 3])
    model = Vgg19(bgr_image=img_resized, num_class=num_class, vgg19_npy_path='./vgg19.npy')
    score = model.fc8
    prediction = tf.argmax(score, 1)
    saver = tf.train.Saver()
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        saver.restore(sess, "./tmp/checkpoints/model_epoch50.ckpt")
        plt.imshow(img_decoded.eval())
        plt.title("Class:" + class_name[sess.run(prediction)[0]])
        plt.show()


test_image('./validate/11.jpg', 2)

對於大多數真正的火災圖片,該網絡仍是可以識別出來,可是一些夕陽圖片或者暖色的燈光就不容易識別。如下是一些識別圖片結果:
2017-11-23-09-50-55

2017-11-23-09-51-45

2017-11-23-09-52-29

2017-11-23-09-54-41

2017-11-23-09-56-39

2017-11-23-09-58-28

2017-11-23-09-59-03

2017-11-23-10-02-12

參考文獻

相關文章
相關標籤/搜索