VGG的實質是AlexNet結構的增強版,它將卷積層的深度提升到了19層,並且在2014年的ImageNet大賽中的定位問題中獲得了亞軍(冠軍是GoogLeNet,將在下一篇博客中介紹)。整個網絡向人們證明了我們是可以用很小的卷積核取得很好地效果,前提是我們要把網絡的層數加深,這也論證了我們要想提高整個神經網絡的模型效果,一個較為有效的方法便是將它的深度加深,雖然計算量會大大提高,但是整個復雜度也上升了,更能解決復雜的問題。雖然VGG網絡已經誕生好幾年了,但是很多其他網絡上效果並不是很好地情況下,VGG有時候還能夠發揮它的優勢,讓人有意想不到的收獲。
與AlexNet網絡非常類似,VGG共有五個卷積層,並且每個卷積層之后都有一個池化層。當時在ImageNet大賽中,作者分別嘗試了六種網絡結構。這六種結構大致相同,只是層數不同,少則11層,多達19層。網絡結構的輸入是大小為224*224的RGB圖像,最終將分類結果輸出。當然,在輸入網絡時,圖片要進行預處理。
VGG網絡相比AlexNet網絡,在網絡的深度以及寬度上做了一定的拓展,具體的卷積運算還是與AlexNet網絡類似。我們主要說明一下VGG網絡所做的改進。
第一點,由於很多研究者發現歸一化層的效果並不是很好,而且占用了大量的計算資源,所以在VGG網絡中作者取消了歸一化層;
第二點,VGG網絡用了更小的3x3的卷積核,而兩個連續的3x3的卷積核相當於5x5的感受野,由此類推,三個3x3的連續的卷積核也就相當於7x7的感受野。這樣的變化使得參數量更小,節省了計算資源,將資源留給后面的更深層次的網絡。
第三點是VGG網絡中的池化層特征池化核改為了2x2,而在AlexNet網絡中池化核為3x3。
這三點改進無疑是使得整個參數運算量下降,這樣我們在有限的計算平台上能夠獲得更多的資源留給更深層的網絡。由於層數較多,卷積核比較小,這樣使得整個網絡的特征提取效果很好。其實由於VGG的層數較多,所以計算量還是相當大的,卷積層比較多成了它最顯著的特點。另外,VGG網絡的拓展性能比較突出,結構比較簡潔,所以它的遷移性能比較好,遷移到其他數據集的時候泛化性能好。到現在為止,VGG網絡還經常被用來提出特征。所以當現在很多較新的模型效果不好時,使用VGG可能會解決這些問題。
import os
import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import matplotlib.pyplot as plt
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
tf.random.set_seed(2345)
先來加載圖像數據集。這里,我們使用tensorflow自帶的cifar100數據集。
(x_train, y_train), (x_test, y_test) = datasets.cifar100.load_data()
查看數據大致情況:
print(x_train.shape, y_train.shape)
print(x_test.shape, y_test.shape)
(50000, 32, 32, 3) (50000, 1) (10000, 32, 32, 3) (10000, 1)
index = 1
fig, axes = plt.subplots(4, 3, figsize=(8, 4), tight_layout=True)
for row in range(4):
for col in range(3):
axes[row, col].imshow(x_train[index])
axes[row, col].axis('off')
axes[row, col].set_title(y_train[index][0])
index += 1
plt.show()
y_train = tf.squeeze(y_train, axis=1)
y_test = tf.squeeze(y_test, axis=1)
def preprocess(x, y):
x = tf.cast(x, dtype=tf.float32) / 255. # 將每個像素值映射到[0, 1]內
y = tf.cast(y, dtype=tf.float32)
return x, y
將數據集用TensorFlow的dataset存儲並打亂:
train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_db = train_db.shuffle(1000).map(preprocess).batch(64)
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.shuffle(1000).map(preprocess).batch(64)
現在來創建卷積部分網絡,共10個卷積層,每兩層之間添加一層最大池化層。在設計卷積網絡時,一般使核的數量保持增加,但每個核輸出的特征圖大小降低或保持不變。
在前面實現LeNet和AlexNet網絡博客中,我們是直接使用模型的fit方法訓練模型,在卷積網絡與全連接網絡的過渡部分通過TensorFlow的flatten層進行過渡,在本文中,為更好演示網絡的各個細節,對這兩個功能我們均手動實現。
conv_layers = [ # 5層卷積,每兩層卷積后添加一層最大池化
layers.Conv2D(64, kernel_size=[3,3],padding='same', activation=tf.nn.relu), # 64是指核的數量,
layers.Conv2D(64, kernel_size=[3,3],padding='same', activation=tf.nn.relu), # same是指輸入於輸出保持相同size
layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
layers.Conv2D(128, kernel_size=[3,3],padding='same', activation=tf.nn.relu),
layers.Conv2D(128, kernel_size=[3,3],padding='same', activation=tf.nn.relu),
layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
layers.Conv2D(256, kernel_size=[3,3],padding='same', activation=tf.nn.relu),
layers.Conv2D(256, kernel_size=[3,3],padding='same', activation=tf.nn.relu),
layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
layers.Conv2D(512, kernel_size=[3,3],padding='same', activation=tf.nn.relu),
layers.Conv2D(512, kernel_size=[3,3],padding='same', activation=tf.nn.relu),
layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
layers.Conv2D(512, kernel_size=[3,3],padding='same', activation=tf.nn.relu),
layers.Conv2D(512, kernel_size=[3,3],padding='same', activation=tf.nn.relu),
layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')
]
假如我們輸入網絡中的圖像大小為32*32,包含3通道,檢測一下輸出大小:
conv_net = Sequential(conv_layers)
conv_net.build(input_shape=[None, 32, 32, 3]) # 指定輸入
x = tf.random.normal([1, 32, 32, 3]) # 1是指輸入一張圖像,兩個32是圖像長寬,3是指3通道
out = conv_net(x)
out.shape
TensorShape([1, 1, 1, 512])
可知,經過5層卷積核池化之后,最終的輸出大小為[1, 1, 1, 512],根據這一信息,我們就可以進一步設計全連接網絡。在設計全連接網絡時需要注意,因為數據集圖像有100個類別,所以全連接層中最后一層節點數量為100.
fc_layers = [ # 全連接層
layers.Dense(256, activation=tf.nn.relu),
layers.Dense(128, activation=tf.nn.relu),
layers.Dense(100, activation=None)
]
指定輸入全連接層數據大小,並創建模型:
fc_net = Sequential(fc_layers)
fc_net.build(input_shape=[None, 512])
將卷積層和全連接層參數同一存儲,方便后續方便后續更新:
variables = conv_net.trainable_variables + fc_net.trainable_variables
創建優化器:
optimizer = optimizers.Adam(lr=1e-4)
手動實現fit功能,進行模型訓練。
for epoch in range(5):
for step , (x, y) in enumerate(train_db):
with tf.GradientTape() as tape:
# 第一步, 將圖像數據傳入卷積層網絡
# [batch, 32, 32, 3] --> [batch, 1, 1, 512]
out = conv_net(x)
# 第二步, 卷卷積層輸出的特征圖輸出到全連接層網絡
# 需要先將特征圖進行變形
out = tf.reshape(out, [-1, 512]) # [batch, 1, 1, 512] --> [Batch, 512]
# 全連接層:[batch, 512] --> [b, 100]
logits = fc_net(out)
# 對輸出進行獨熱編碼:
# y_onehot = tf.keras.one_hot(y, depth=100) # 直接使用tf.one_hot()報錯Could not find valid device for node.
y_onehot = tf.keras.utils.to_categorical(y, num_classes=100) # 所以使用這種方式進行獨熱編碼
# 計算損失函數
loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
loss = tf.reduce_mean(loss) # 損失均值
grads = tape.gradient(loss, variables) # 對卷積層和全連接層參數進行求導
optimizer.apply_gradients(zip(grads, variables)) # 更新參數
if step % 100 == 0: # 每1000次傳播輸出一次
print(epoch , step, 'loss:', float(loss))
total_num = 0
total_correct = 0
for x, y in test_db:
out = conv_net(x)
out = tf.reshape(out, [-1, 512])
logits = fc_net(out)
prob = tf.nn.softmax(logits, axis=1)
pred = tf.argmax(prob, axis=1)
pred = tf.cast(pred, dtype=tf.int32)
y = tf.cast(y, dtype=tf.int32)
correct = tf.cast(tf.equal(pred , y), dtype=tf.int32)
correct = tf.reduce_sum(correct)
total_num += x.shape[0]
total_correct += int(correct)
acc = total_correct / total_num
print(epoch, 'acc:', acc)
0 0 loss: 3.2746524810791016 0 100 loss: 3.1227006912231445 0 200 loss: 3.3354835510253906 0 300 loss: 3.5577585697174072 0 400 loss: 3.4442076683044434 0 500 loss: 3.5540578365325928 0 600 loss: 2.993718385696411 0 700 loss: 3.398216724395752 0 acc: 0.2156 1 0 loss: 3.2784264087677 1 100 loss: 2.915174961090088 1 200 loss: 3.1731386184692383 1 300 loss: 2.9105772972106934 1 400 loss: 2.889545202255249 1 500 loss: 3.0817737579345703 1 600 loss: 2.8999242782592773 1 700 loss: 2.847750186920166 1 acc: 0.2577 2 0 loss: 3.0487632751464844 2 100 loss: 2.961989164352417 2 200 loss: 2.810255527496338 2 300 loss: 2.921875476837158 2 400 loss: 3.0022480487823486 2 500 loss: 2.8648478984832764 2 600 loss: 2.313401222229004 2 700 loss: 2.9197773933410645 2 acc: 0.2714 3 0 loss: 3.2561397552490234 3 100 loss: 2.6284666061401367 3 200 loss: 2.611253499984741 3 300 loss: 2.6300625801086426 3 400 loss: 2.8262720108032227 3 500 loss: 2.4057717323303223 3 600 loss: 2.365994691848755 3 700 loss: 2.552517890930176 3 acc: 0.3038 4 0 loss: 2.8142364025115967 4 100 loss: 2.7545413970947266 4 200 loss: 2.5179195404052734 4 300 loss: 2.093433380126953 4 400 loss: 2.763005256652832 4 500 loss: 2.2059311866760254 4 600 loss: 2.128242015838623 4 700 loss: 2.2914538383483887 4 acc: 0.3382
上述訓練經過了5次迭代,准確率到達33.82%,增加迭代次數可進一步提高准確率。對比上篇博客的AlexNet網絡,我們發現VGG網絡明顯收斂速度更快,模型西能更佳。
參考:
https://baijiahao.baidu.com/s?id=1636567480736287260&wfr=spider&for=pc