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大小的卷積。
首先來看看原論文中VGG網絡的5種模型結構。A-E模型均是由5個stage和3個全鏈層和一個softmax分類層組成,其中每個stege有一個max-pooling層和多個卷積層。每層的卷積核個數從首階段的64個開始,每個階段增長一倍,直到達到512個。
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網絡。
雖然VGG網絡使用的均是3X3的卷積filter,極大的減小了參數個數,但和AlexNet比較起來,參數個數還是相當的多,以模型D為例,每一層的參數個數如下表所示,總參數個數為1.3億左右,龐大的參數增加了訓練的時間,下一章單搭建的VGG19模型僅在CPU上進行訓練,單單一個epoch就要訓練8小時以上!
盡管VGG19有那么多的參數,但是在訓練過程中,作者發現VGG需要很少的迭代次數就開始收斂了,這是因為:
1、深度和小的filter尺寸起到了隱式的規則化作用
2、一些層的pre-initialisation
怎么做pre-initialisation呢?作者先訓練最淺的網絡A,然后把A的前4個卷積層和最后全鏈層的權值當作其他網絡的初始值,未賦值的中間層通過隨機初始化進行訓練。這樣避免了不好的權值初始值對於網絡訓練的影響,從而加快了收斂。
為什么在整個VGG網絡中都用的是3X3大小的filter呢,VGG團隊給出了下面的解釋:
1、3 * 3是最小的能夠捕獲上下左右和中心概念的尺寸。
2、兩個3 * 3的卷基層的有限感受野是5X5;三個3X3的感受野是7X7,可以替代大的filter尺寸。(感受野表示網絡內部的不同位置的神經元對原圖像的感受范圍大小,神經元感受野的值越大表示其能接觸到的原始圖像范圍就越大,也意味着他可能蘊含更為全局、語義層次更高的特征;而值越小則表示其所包含的特征越趨向於局部和細節。)
3、多個3 * 3的卷基層比一個大尺寸filter卷基層有更多的非線性,使得判決函數更加具有判決性。
4、多個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,更好的提取了圖片的特征向量。
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)的計算公式為:
其中:
True Positive(真正, TP):將正類預測為正類數.
True Negative(真負 , TN):將負類預測為負類數.
False Positive(假正, FP):將負類預測為正類數 →→ 誤報 (Type I error).
False Negative(假負 , FN):將正類預測為負類數 →→ 漏報 (Type II error).
精確率(Precision)的計算公式為:
召回率(Recall)的計算公式為:
兩者的調和平均F1:
精確率(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)
對於大多數真正的火災圖片,該網絡還是能夠識別出來,但是一些夕陽圖片或者暖色的燈光就不容易識別。以下是一些識別圖片結果:
參考文獻
- 《VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION》Karen Simonyan& Andrew Zisserman
- 《Deep Convolutional Neural Networks for Fire Detection in Images》Jivitesh Sharma(B), Ole-Christoffer Granmo, Morten Goodwin, and Jahn Thomas Fidje
- https://github.com/UIA-CAIR/Fire-Detection-Image-Dataset
- https://github.com/machrisaa/tensorflow-vgg
- https://gist.github.com/baraldilorenzo/07d7802847aaad0a35d3#file-vgg-16_keras-py-L24