- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
一、理论知识储备
1.CNN算法发展
- AlexNet是2012年ImageNet竞赛中,由Alex Krizhevsky和Ilya Sutskever提出,在2012年ImageNet竞赛中,AlexNet以top5错误率为15.3%取得了分类任务的第一名。
- VGGNet是2014年ImageNet竞赛中,由Karen Simonyan和Andrew Zisserman提出,在2014年ImageNet竞赛中,VGGNet以top5错误率为7.3%取得了分类任务的第二名。
- GoogLeNet是2014年ImageNet竞赛中,由Christian Szegedy提出,在2014年ImageNet竞赛中,GoogLeNet以top5错误率为6.6%取得了分类任务的第一名。
- ResNet是2015年ImageNet竞赛中,由Kaiming He、Xiangyu Zhang、Saining Xie、Trevor Darrell提出,在2015年ImageNet竞赛中,ResNet以top5错误率为3.57%取得了分类任务的第一名。
- DenseNet是2016年ImageNet竞赛中,由Gao Huang、Zhuang Liu、Kaiming He、Xiangyu Zhang提出,在2016年ImageNet竞赛中,DenseNet以top5错误率为3.03%取得了分类任务的第一名。
- SE-ResNet是2017年ImageNet竞赛中,由Xiaolong Wang、Kaiming He、Jian Sun提出,在2017年ImageNet竞赛中,SE-ResNet以top5错误率为2.97%取得了分类任务的第一名。
- ResNeXt是2017年ImageNet竞赛中,由Saining Xie、Zhifeng Cai、Trevor Darrell提出,在2017年ImageNet竞赛中,ResNeXt以top5错误率为2.80%取得了分类任务的第一名。
2.残差网络的由来
深度残差网络RestNet(deep residual network)是2015年ImageNet竞赛中由何凯明等提出,因为它简单与实用并存,随后很多研究都是建立在ResNet-50
或者ResNet-101
的基础上完成。
ResNet主要解决深度卷积网络在深度加深时候的“退化”问题。在一般的卷积神经网络中,增大网络深度后带来的第一个问题就是梯度消失、爆炸,这个问题Szegedy提出BN层后被顺利解决。BN层能对各层的输出做归一化,这样梯度在反向层层传递后仍能保持大小稳定,不会出现过小或过大的情况。但是作者发现加了BN后再加大深度仍然不容易收敛,其提到了第二个问题–准确率下降问题:层级大到一定程度时准确率就会饱和,然后迅速下降,这种下降即不是梯度消失引起的也不是过拟合造成的,而是由于网络过于复杂,以至于光靠不加约束的放养式的训练很难达到理想的错误率。
准确率下降问题不是网络结构本身的问题,而是现有的训练方式不够理想造成的。当前广泛使用的优化器,无论是SGD,还是RMSProp,或是Adam,都无法在网络深度变大后达到理论上最优的收敛结果。
作者在文中证明了只要有合适的网络结构,更深的网络肯定会比较浅的网络效果要好。证明过程也很简单:假设在一种网络A的后面添加几层形成新的网络B,如果增加的层级只是对A的输出做了个恒等映射(identity mapping),即A的输出经过新增的层级变成B的输出后没有发生变化,这样网络A和网络B的错误率就是相等的,也就证明了加深后的网络不会比加深前的网络效果差。
何恺明提出了一种残差结构来实现上述恒等映射(图1):整个模块除了正常的卷积层输出外,还有个分支把输入直接连到输出上,该分支输出和卷积的输出做算术相加得到最终的输出,用公式表达就是 H ( x ) = F ( x ) + x H(x)= F(x)+ x H(x)=F(x)+x $ x $是输入, $ F(x) 是卷积分支的输出, 是卷积分支的输出, 是卷积分支的输出, H(x) $是整个结构的输出。可以证明如果 $ F(x) $分支中所有参数都是0 $ H(x) $就是个恒等映射。残差结构人为制造了恒等映射,就能让整个结构朝着恒等映射的方向去收敛,确保最终的错误率不会因为深度的变大而越来越差。如果一个网络通过简单的手工设置参数值就能达到想要的结果,那这种结构就很容易通过训练来收敛到该结果,这是一条设计复杂的网络时通用的规则。
图2左边的单元为 ResNet
两层的残差单元,两层的残差单元包含两个相同输出的通道数的 3x3
卷积,只是用于较浅的 ResNet 网络,对较深的网络主要使用三层的残差单元。三层的残差单元又称为bottleneck 结构,先用一个1x1
卷积进行降维,然后 3x3
卷积,最后用 1x1
升维恢复原有的维度。另外,如果有输入输出维度不同的情况,可以对输入做一个线性映射变换维度,再连接后面的层。层的残差单元对于相同数量的层又减少了参数量,因此可以拓展更深的模型。通过残差单元的组合有经典的 ResNet-50
,ResNet-101
等网络结构。
二、前期工作
1.设置GPU
import tensorflow as tf
gpus = tf.config.list_physical_devices('GPU')
if gpus:
tf.config.explicitly_set_memory_growth(gpus[0], True)
tf.config.set_visible_devices(gpus[0], 'GPU')
print("GPUs available")
2.导入数据
import matplotlib.pyplot as plt
# 支持中文
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
import os,PIL,pathlib
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers,models
data_dir = pathlib.Path('F:/host/Data/bird_photos')
3.查看数据
image_count = len(list(data_dir.glob('*/*')))
print("图片总数为:",image_count)
三、数据预处理
文件夹 | 数量 |
---|---|
Bananaquit | 166张 |
Black Skimmer | 111张 |
Black Throated Bushtiti | 122张 |
Cockatoo | 166张 |
1.加载数据
使用image_dataset_from_directory
方法将磁盘中的数据加载到tf.data.Dataset
对象中。
batch_size = 8
img_height = 224
img_width = 224
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size,
)
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size,
)
# 我们可以通过`class_names`属性查看类别名称
class_names = train_ds.class_names
print(class_names)
2.可视化数据
plt.figure(figsize=(10, 5)) # 图形的宽为10高为5
plt.suptitle('Bird Photos')
for images, labels in train_ds.take(1):
for i in range(8):
ax = plt.subplot(2, 4, i+1)
plt.imshow(images[i].numpy().astype("uint8"))
plt.title(class_names[labels[i]])
plt.axis("off")
plt.imshow(images[1].numpy().astype("uint8"))
3.再次检查数据
for image_batch, labels_batch in train_ds:
print(image_batch.shape)
print(labels_batch.shape)
break
image_batch
: 包含8张图像的张量,形状为(8, 224, 224, 3)。labels_batch
: 包含8个标签的张量,形状为(8,)。
4.配置数据集
shuffle
: 随机打乱数据集。prefetch
: 预取数据集,以加速数据集的迭代。cache
: 缓存数据集,以加速数据集的迭代。
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)
四、残差网络(ResNet)介绍
1.残差网络解决了什么
残差网络是为了解决深度神经网络(DNN)训练过程中梯度消失和梯度爆炸的问题而提出的。它通过引入残差连接,将输入直接加到输出上,从而允许网络学习更复杂的函数。
2.ResNet-50介绍
ResNet-50有两个基本的块,分别名为Conv Block
和Identity Block
。
五、构建ResNet-50网络模型
from keras import layers
from keras.layers import Input, Activation, BatchNormalization, Flatten
from keras.layers import Conv2D, AveragePooling2D, Dense, MaxPooling2D, ZeroPadding2D
from keras.models import Model
def identity_block(input_tensor, kernel_size, filters, stage, block):
filters1, filters2, filters3 = filters
name_base = str(stage) + block + '_identity_block_'
x = Conv2D(filters1, (1, 1), name=name_base + 'conv1')(input_tensor)
x = BatchNormalization(name=name_base + 'bn1')(x)
x = Activation('relu',name=name_base + 'relu1')(x)
x = Conv2D(filters2, kernel_size, padding='same', name=name_base + 'conv2')(x)
x = BatchNormalization(name=name_base + 'bn2')(x)
x = Activation('relu',name=name_base + 'relu2')(x)
x = Conv2D(filters3, (1, 1), name=name_base + 'conv3')(x)
x = BatchNormalization(name=name_base + 'bn3')(x)
x = layers.add([x, input_tensor], name=name_base + 'add')
x = Activation('relu',name=name_base + 'relu4')(x)
return x
# 在残差网络中,广泛地使用了BN层;但是没有使用MaxPoo1ing以便减小特征图尺寸,
# 作为替代,在每个模块的第一层,都使用了strides=(2,2)的方式进行特征图尺寸缩减,
# 与使用MaxPooling相比,毫无疑问是减少了卷积的次数,输入图像分辨率较大时比较适合
# 在残差网络的最后一级,先利用layer.add()实现H(x)=x+F(x)
def conv_block(input_tensor, kernel_size, filters, stage, block, strides=(2, 2)):
filters1, filters2, filters3 = filters
res_name_base = str(stage) + block + '_conv_block_res_'
name_base = str(stage) + block + '_conv_block_'
x = Conv2D(filters1, (1, 1), strides=strides, name=name_base + 'conv1')(input_tensor)
x = BatchNormalization(name=name_base + 'bn1')(x)
x = Activation('relu',name=name_base + 'relu1')(x)
x = Conv2D(filters2, kernel_size, padding='same', name=name_base + 'conv2')(x)
x = BatchNormalization(name=name_base + 'bn2')(x)
x = Activation('relu',name=name_base + 'relu2')(x)
x = Conv2D(filters3, (1, 1), name=name_base + 'conv3')(x)
x = BatchNormalization(name=name_base + 'bn3')(x)
shortcut = Conv2D(filters3, (1, 1), strides=strides, name=res_name_base + 'conv')(input_tensor)
shortcut = BatchNormalization(name=res_name_base + 'bn')(shortcut)
x = layers.add([x, shortcut],name=name_base + 'add')
x = Activation('relu',name=name_base + 'relu4')(x)
return x
def ResNet50(input_shape=(224, 224, 3), num_classes=1000):
img_input = Input(shape=input_shape)
x = ZeroPadding2D((3, 3))(img_input)
x = Conv2D(64, (7, 7), strides=(2, 2), name='conv1')(x)
x = BatchNormalization(name='bn_conv1')(x)
x = Activation('relu')(x)
x = MaxPooling2D((3, 3), strides=(2, 2))(x)
x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1))
x = identity_block(x, 3, [64, 64, 256], stage=2, block='b')
x = identity_block(x, 3, [64, 64, 256], stage=2, block='c')
x = conv_block(x, 3, [128, 128, 512], stage=3, block='a')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='b')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='c')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='d')
x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f')
x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a')
x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b')
x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c')
x = AveragePooling2D((7, 7), name='avg_pool')(x)
x = Flatten()(x)
x = Dense(num_classes, activation='softmax', name='fc1000')(x)
model = Model(img_input, x, name='ResNet50')
# 加载预训练模型
model.load_weights('./weights/resnet50_weights_tf_dim_ordering_tf_kernels.h5')
return model
model = ResNet50()
model.summary()
六、编译
在准备对模型进行训练之前,还需要再对其进行一些设置。以下内容是在模型的编译步骤中添加的:
- 损失函数(loss):用于衡量模型在训练期间预测值和实际值之间的差距。
- 优化器(optimizer):决定模型如何根据其看到的数据和自身的损失函数进行更新。
- 指标(metrics):用于监控训练和测试步骤。
# 设置优化器
opt = tf.keras.optimizers.Adam(learning_rate=1e-7)
model.compile(optimizer="adam",
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
七、训练模型
epochs = 10
history = model.fit(train_ds,
validation_data=val_ds,
epochs=epochs)
八、评估模型
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs_range = range(epochs)
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
九、预测
# 采用加载的模型来看预测结果
plt.figure(figsize=(10, 5))
plt.suptitle('Predictions')
for images, labels in val_ds.take(1):
for i in range(8):
ax = plt.subplot(2, 4, i + 1)
# 显示图片
plt.imshow(images[i].numpy().astype("uint8"))
# 需要给图片增加一个维度
img_array = tf.expand_dims(images[i], 0)
# 使用模型预测图片
predictions = model.predict(img_array)
plt.title(class_names[np.argmax(predictions)])
plt.axis("off")
十、个人小结
在这篇文章中,我深入探讨了卷积神经网络(CNN)的发展历程,特别是残差网络(ResNet)的诞生和原理。CNN在图像识别领域取得了显著的进展,但随着网络深度的增加,梯度消失和爆炸的问题逐渐显现,影响了深层网络的性能。ResNet通过引入残差学习框架,有效地解决了这一问题,使得网络能够学习到恒等映射,从而在保持性能的同时增加深度。