天天看點

經典卷積神經網絡算法(3):VGG

VGG的實質是AlexNet結構的增強版,它将卷積層的深度提升到了19層,并且在2014年的ImageNet大賽中的定位問題中獲得了亞軍(冠軍是GoogLeNet,将在下一篇部落格中介紹)。

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可能會解決這些問題。

In [5]:

import os
import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import matplotlib.pyplot as plt
      

In [6]:

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
tf.random.set_seed(2345)
      

先來加載圖像資料集。這裡,我們使用tensorflow自帶的cifar100資料集。

In [16]:

(x_train, y_train), (x_test, y_test) = datasets.cifar100.load_data()
      

檢視資料大緻情況:

In [17]:

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)
      

In [18]:

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()
      

In [13]:

y_train = tf.squeeze(y_train, axis=1)
y_test = tf.squeeze(y_test, axis=1)
      

In [7]:

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存儲并打亂:

In [13]:

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層進行過渡,在本文中,為更好示範網絡的各個細節,對這兩個功能我們均手動實作。

In [14]:

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通道,檢測一下輸出大小:

In [15]:

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
      

Out[15]:

TensorShape([1, 1, 1, 512])      

可知,經過5層卷積核池化之後,最終的輸出大小為[1, 1, 1, 512],根據這一資訊,我們就可以進一步設計全連接配接網絡。在設計全連接配接網絡時需要注意,因為資料集圖像有100個類别,是以全連接配接層中最後一層節點數量為100.

In [16]:

fc_layers = [  # 全連接配接層
    layers.Dense(256, activation=tf.nn.relu),
    layers.Dense(128, activation=tf.nn.relu),
    layers.Dense(100, activation=None)
]
      

指定輸入全連接配接層資料大小,并建立模型:

In [17]:

fc_net = Sequential(fc_layers)

fc_net.build(input_shape=[None, 512])
      

将卷積層和全連接配接層參數同一存儲,友善後續友善後續更新:

In [18]:

variables = conv_net.trainable_variables + fc_net.trainable_variables
      

建立優化器:

In [19]:

optimizer = optimizers.Adam(lr=1e-4)
      

手動實作fit功能,進行模型訓練。

In [28]:

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