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