深度残差网络(ResNet)
深度残差网络(ResNet)
- 0. 前言
- 1. 函数式 API
- 2. 创建双输入单输出模型
- 3. 深度残差网络 (ResNet)
- 4. ResNet v2
0. 前言
在本节中,我们将深入研究深度神经网络,这些网络在复杂数据集上展现了卓越的分类准确率。ResNet
引入了残差学习的概念,使残差学习能够通过解决深度卷积网络中消失的梯度问题来构建非常深的网络。
虽然本节重点在于深度神经网络,但我们将首先探讨 Keras
的一个重要特性——函数式 API
。作为 tf.keras
中构建网络的另一种方法,该 API
能够帮助我们构建顺序模型 API
无法实现的复杂网络结构。
1. 函数式 API
函数式 API
遵循以下两个概念:
- 层是接受张量作为参数的实例。为了构建模型,层实例是通过输入和输出张量彼此链接的对象,使用层实例会使模型更容易具有多个输入和输出,因为每个层的输入/输出将很容易访问
- 模型是一个或多个输入张量和输出张量之间的函数。在模型输入和输出之间,张量是通过层输入和输出张量彼此链接的层实例。因此,模型是一个或多个输入层和一个或多个输出层的函数。模型实例将数据从输入流到输出流的形式的计算图形式化
在函数式 API
中,一个具有 32
个卷积核的二维卷积层 Conv2D
,x
是层输入张量,y
是层输出张量,可以写成:
y = Conv2D(32)(x)
我们同样可以通过堆叠多个层级来构建模型。例如,我们可以使用函数式 API
重写 MNIST 数据集上的卷积神经网络:
import numpy as np
import tensorflow as tf
from tensorflow import keras#加载数据
(x_train,y_train),(x_test,y_test) = keras.datasets.mnist.load_data()#计算类别数
num_labels = len(np.unique(y_train))#转化为one-hot编码
y_train = keras.utils.to_categorical(y_train)
y_test = keras.utils.to_categorical(y_test)#预处理
image_size = x_train.shape[1]
x_train = np.expand_dims(x_train,axis=-1)
x_train = x_train.astype('float32') / 255.
x_test = np.expand_dims(x_test,axis=-1)
x_test = x_test.astype('float32') / 255.#超参数
input_shape = (image_size,image_size,1)
batch_size = 128
kernel_size = 3
filters = 64
dropout = 0.3
epochs = 10#模型
inputs = keras.layers.Input(shape=input_shape)
y = keras.layers.Conv2D(filters=64,kernel_size=kernel_size,activation='relu')(inputs)
y = keras.layers.MaxPool2D()(y)
y = keras.layers.Conv2D(filters=filters,kernel_size=kernel_size,activation='relu')(y)
y = keras.layers.MaxPool2D()(y)
y = keras.layers.Conv2D(filters=filters,kernel_size=kernel_size,activation='relu')(y)
y = keras.layers.Flatten()(y)
y = keras.layers.Dropout(dropout)(y)
outputs = keras.layers.Dense(num_labels,activation='softmax')(y)model = keras.Model(inputs=inputs,outputs=outputs)
model.summary()
keras.utils.plot_model(model,to_file='functional-api.png',show_shapes=True)model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
model.fit(x_train,y_train,validation_data=(x_test,y_test),epochs=epochs,batch_size=batch_size)
score = model.evaluate(x_test,y_test,batch_size=batch_size,verbose=0)
print('\ntest accuracy: %.2f%%' % (100 * score[1]))
每一层都生成一个张量作为输出,该张量成为下一层的输入。 要创建此模型,可以调用 Model()
并提供输入和输出张量,或者提供张量列表。
2. 创建双输入单输出模型
接下来,我们将构建一个具有双输入单输出的高级模型,而顺序模型 API
仅适用于构建单输入单输出模型。
假设我们为 MNIST
数字分类设计了一个新型模型,该网络通过左右两个 CNN
分支同时处理相同输入,并利用拼接层融合处理结果。这种拼接操作类似于将两个相同形状的张量沿指定轴堆叠合并:例如,将两个形状为 (3,3,16)
的张量沿最后一轴拼接后,将生成形状为 (3,3,32)
的新张量。两个分支由两个for循环创建。 两个分支有相同的输入形状:
import numpy as np
import tensorflow as tf
from tensorflow import keras#加载数据
(x_train,y_train),(x_test,y_test) = keras.datasets.mnist.load_data()#计算类别数
num_labels = len(np.unique(y_train))#转化为one-hot编码
y_train = keras.utils.to_categorical(y_train)
y_test = keras.utils.to_categorical(y_test)#预处理
image_size = x_train.shape[1]
x_train = np.expand_dims(x_train,axis=-1)
x_train = x_train.astype('float32') / 255.
x_test = np.expand_dims(x_test,axis=-1)
x_test = x_test.astype('float32') / 255.#超参数
input_shape = (image_size,image_size,1)
batch_size = 128
kernel_size = 3
n_filters = 32
dropout = 0.4left_inputs = keras.layers.Input(shape=input_shape)
x = left_inputs
filters = n_filters
for i in range(3):x = keras.layers.Conv2D(filters=filters,kernel_size=kernel_size,padding='same',activation='relu')(x)x = keras.layers.Dropout(dropout)(x)x = keras.layers.MaxPool2D()(x)filters = filters * 2right_inputs = keras.layers.Input(shape=input_shape)
y = left_inputs
filters = n_filters
for i in range(3):y = keras.layers.Conv2D(filters=filters,kernel_size=kernel_size,padding='same',activation='relu')(y)y = keras.layers.Dropout(dropout)(y)y = keras.layers.MaxPool2D()(y)filters = filters * 2#特征图链接
y = keras.layers.concatenate([x,y])
y = keras.layers.Flatten()(y)
y = keras.layers.Dropout(dropout)(y)
outputs = keras.layers.Dense(num_labels,activation='softmax')(y)
model = keras.Model(inputs=[left_inputs,right_inputs],outputs=outputs)
keras.utils.plot_model(model,to_file='y-network.png',show_shapes=True)
model.summary()
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
model.fit([x_train,x_train],y_train,validation_data=([x_test,x_test],y_test),epochs=20,batch_size=batch_size)
score = model.evaluate([x_test,x_test],y_test,batch_size=batch_size,verbose=0)
print("\n Test accuracy: %.2f%%" % (100 * score[1]))
3. 深度残差网络 (ResNet)
深度网络的一个关键优势在于其卓越的学习能力,能够从输入数据和特征映射中提取不同层级的表征。在分类、分割、检测及其他众多计算机视觉任务中,学习多样化的特征映射通常能带来更优异的性能表现。
然而,深度网络的训练并非易事,因为在反向传播过程中,梯度可能会随着网络层级的加深而在浅层逐渐消失(或爆炸)。网络参数通过从输出层到所有前置层的反向传播进行更新,由于反向传播基于链式法则,当梯度传递至浅层时会出现逐渐衰减的趋势,这主要源于连续小数相乘效应——特别是在损失函数值和参数取值较小时尤为明显。乘法运算的次数与网络深度成正比。需要特别注意的是,若梯度持续衰减,网络参数将无法得到有效更新。ResNet
的核心是为了防止梯度弥散,让信息流经跳跃连接到达浅层。典型 CNN
块与 ResNet
残差块的对比如下:
接下来我们将深入探讨这两种网络块的具体差异。我们将层级特征映射表示为 xxx,其中第 lll 层的特征映射记作 xlx_lxl。CNN
层的基本操作流程为:卷积-批归一化-激活函数 (Conv2D-BN-ReLU
)。若将这套操作序列定义为 H()=卷积-批归一化-激活函数
,则:
xl−1=H(xl−2)xl=H(xl−1)x_{l-1} = H(x_{l-2})\\ x_l = H(x_{l-1}) xl−1=H(xl−2)xl=H(xl−1)
换言之,通过 H()
操作将第 l−2l-2l−2 层的特征映射转换为 xl−1x_{l-1}xl−1,再通过相同的操作序列将 xl−1x_{l-1}xl−1 转换为 xlx_lxl。举例来说,对于一个 18
层的 VGG
网络,从输入图像到第 18
层特征映射需要经过 18
次 H()
运算。
总体而言,我们可以观察到传统网络中第l层的输出特征映射仅受前一层特征的直接影响。而在 ResNet
中:
xl−1=H(xl−2)xl=ReLU(F(xl−1)+xl−2)x_{l-1} = H(x_{l-2}) \\ x_l = ReLU(F(x_{l-1}) + x_{l-2}) xl−1=H(xl−2)xl=ReLU(F(xl−1)+xl−2)
其中 F(xl−1)F(x_{l-1})F(xl−1) 由卷积-批归一化操作构成,这也被称为残差映射。加号表示跳跃连接与 F(xl−1)F(x_{l-1})F(xl−1) 输出结果之间的张量逐元素相加。这种跳跃连接既不会引入额外参数,也不会增加计算复杂度。
在 tf.keras
中,加法操作可通过 add()
合并函数实现。但需要注意的是,F(xl−1)F(x_{l-1})F(xl−1) 与 xl−2x_{l-2}xl−2 必须保持相同的维度。
当维度不匹配时(例如特征图尺寸发生变化),我们需要对 xl−2x_{l-2}xl−2 进行线性投影以匹配 F(xl−1)F(x_{l-1})F(xl−1) 的尺寸。在原论文中,当特征图尺寸减半时,线性投影通过 1×1
卷积核配合步长 2
的卷积操作实现。
掌握了 ResNet
的基本构建模块后,我们现在可以设计用于图像分类的深度残差网络。
当连接两个不同大小的残差块时,使用“过渡层”。ResNet
使用 kernel_initializer ='he_normal'
进行初始化。最后一层由 AveragePooling2D-Flatten-Dense
组成。ResNet
不使用 Dropout
。合并操作和 1 x 1
卷积实现类似正则化效果。
通过修改 n
的值,能够增加网络的深度。对于 n = 18
,拥有 ResNet110
,这是一个具有 110
层的深度网络。要构建 ResNet20
,使用 n = 3
:
n = 3
version = 1
if version == 1:depth = n * 6 + 2
elif version == 2:
depth = n * 9 + 1
model_type = 'ResNet%dv%d' % (depth, version)if version == 2:model = resnet_v2(input_shape=intput_shape,depth=depth)
else:model = resnet_v1(input_shape=intput_shape,depth=depth)
resnet_v1()
方法是 ResNet
的模型构建器。它使用函数 resnet_layer()
构建 Conv2D-BN-ReLU
堆栈。改进的 ResNet
称为 ResNet_v2
。ResNet v2
改进了残差块设计,从而提高性能:
def resnet_layer(inputs,num_filters=16,kernel_size=3,strides=1,activation='relu',batch_normalization=True,conv_first=True):conv = keras.layers.Conv2D(num_filters,kernel_size=kernel_size,strides=strides,padding='same',kernel_initializer='he_normal',kernel_regularizer=keras.regularizers.l2(1e-4))x = inputsif conv_first:x = conv(x)if batch_normalization:x = keras.layers.BatchNormalization()(x)if activation is not None:x = keras.layers.Activation(activation)(x)else:if batch_normalization:x = keras.layers.BatchNormalization()(x)if activation is not None:x = keras.layers.Activation(activation)(x)x = conv(x)return xdef resnet_v1(input_shape,depth,num_classes=10):if (depth - 2) % 6 != 0:raise ValueError('depth should be 6n+2')#超参数num_filters = 16num_res_blocks = int((depth - 2) / 6)inputs = keras.layers.Input(shape=input_shape)x = resnet_layer(inputs=inputs)for stack in range(3):for res_block in range(num_res_blocks):strides = 1if stack > 0 and res_block == 0:strides = 2y = resnet_layer(inputs=x,num_filters=num_filters,strides=strides)y = resnet_layer(inputs=y,num_filters=num_filters,activation=None)if stack > 0 and res_block == 0:x = resnet_layer(inputs=x,num_filters=num_filters,kernel_size=1,strides=strides,activation=None,batch_normalization=False)x = keras.layers.add([x,y])x = keras.layers.Activation('relu')(x)num_filters *= 2x = keras.layers.AveragePooling2D(pool_size=8)(x)x = keras.layers.Flatten()(x)outputs = keras.layers.Dense(num_classes,activation='softmax',kernel_regularizer='he_normal')(x)model = keras.Model(inputs=inputs,outputs=outputs)return model
使用学习率 (lr
) 调度程序 lr_schedule()
,以将 lr
进行衰减。lr_schedule()
函数将在训练期间的每个 epoch
后作为回调变量的一部分被调用:
def lr_schedule(epoch):lr = 1e-3if epoch > 180:lr *= 0.5e-3elif epoch > 160:lr *= 1e-3elif epoch > 120:lr *= le-2elif epoch > 80:lr *= 1e-1print('learning rate: ',lr)
return lr
lr_scheduler = keras.callbacks.LearningRateScheduler(lr_schedule)
lr_reducer = keras.callbacks.ReduceLROnPlateau(factor=np.sqrt(0.1),cooldown=0,patience=5,min_lr=0.5e-6)
每当验证准确性方面取得进展时,另一个回调都会保存检查点。训练深层网络时,保存模型或权重检查点是一个好习惯,因为训练深度网络需要大量时间。
#模型保存
save_dir = os.path.join(os.getcwd(),'saved_models')
model_name = 'cifar10_%s_model.{epoch:03d}.h5' % model_type
if not os.path.isdir(save_dir):os.makedirs(save_dir)
filepath = os.path.join(save_dir,model_name)
checkpoint = keras.callbacks.ModelCheckpoint(filepath=filepath,monitor='val_acc',verbose=1,save_best_only=True)
callbacks = [checkpoint,lr_reducer,lr_scheduler]
当想使用网络时,只需要重新加载检查点,然后恢复经过训练的模型。通过调用 tf.keras.models.load_model()
来完成。包含 lr_reducer()
函数,如果指标在预定的学习率衰减之前已经达到稳定状态,如 patience= 5
个 epochs
后验证损失没有改善,则此回调将降低学习率:
model.compile(loss='categorical_crossentropy',optimizer=keras.optimizers.Adam(lr=lr_schedule(0)),metrics=['acc'])
model.summary()
keras.utils.plot_model(model,to_file='%s.png' % model_type,show_shapes=True)
调用 model.fit()
方法时将提供回调。tf.keras
使用数据扩充 ImageDataGenerator()
,以便提供其他训练数据作为正则化方案的一部分。
#数据增强
if not data_augmentation:print('Not using data augentation')model.fit(x_train,y_train,batch_size=batch_size,epochs=epochs,validation_data=(x_test,y_test),shuffle=True,callbacks=callbacks)
else:print('using real-time data augmentation.')#数据增强datagen = keras.preprocessing.image.ImageDataGenerator(#将数据集上的输入均值设置为0featurewise_center=False,#将每个样本均值设置为0samplewise_center=False,#将输入除以数据集的stdfeaturewise_std_normalization=False,#将每个输入除以其stdsamplewise_std_normalization=False,#应用ZCA白化zca_whitening=False,#随机旋转图像rotation_range=0,#随机水平移动width_shift_range=0.1,#随机垂直移动height_shift_range=0.1,#随机水平翻转horizontal_flip=True,#随机垂直翻转vertical_flip=False)datagen.fit(x_train)steps_per_epoch = math.ceil(len(x_train) / batch_size)model.fit(x=datagen.flow(x_train,y_train,batch_size=batch_size),verbose=1,epochs=epochs,validation_data=(x_test,y_test),steps_per_epoch=steps_per_epoch,callbacks=callbacks
)
scores = model.evaluate(x_test,y_test,batch_size=batch_size,verbose=0)
4. ResNet v2
ResNet v2
的改进主要体现在残差块中的层排列中,ResNet v2
的主要变化是:
- 使用
1 x 1 – 3 x 3 – 1×1 BN-ReLU-Conv2D
的堆栈 - 批归一化和
ReLU
激活先于二维卷积
n = 12
version = 2
if version == 1:depth = n * 6 + 2
elif version == 2:
depth = n * 9 + 2
def resnet_v2(input_shape,depth,num_classes=10):if (depth - 2) % 9 != 0:raise ValueError('depth should be 6n+2')#超参数num_filters_in = 16num_res_blocks = int((depth - 2) / 9)inputs = keras.layers.Input(shape=input_shape)x = resnet_layer(inputs=inputs,num_filters=num_filters_in,conv_first=True)for stage in range(3):for res_block in range(num_res_blocks):activation='relu'batch_normalization = Truestrides = 1if stage == 0:num_filters_out = num_filters_in * 4if res_block == 0:activation = Nonebatch_normalization = Falseelse:num_filters_out = num_filters_in * 2if res_block == 0:strides=2y = resnet_layer(inputs=x,num_filters=num_filters_in,kernel_size=1,strides=strides,activation=activation,batch_normalization=batch_normalization,conv_first=False)y = resnet_layer(inputs=y,num_filters=num_filters_in,conv_first=False)y = resnet_layer(inputs=y,num_filters=num_filters_out,kernel_size=1,conv_first=False)if res_block == 0:x = resnet_layer(inputs=x,num_filters=num_filters_out,kernel_size=1,strides=strides,activation=activation,batch_normalization=False)x = keras.layers.add([x,y])num_filters_in = num_filters_outx = keras.layers.BatchNormalization()(x)x = keras.layers.Activation('relu')(x)x = keras.layers.AveragePooling2D(pool_size=8)(x)x = keras.layers.Flatten()(x)outputs = keras.layers.Dense(num_classes,activation='softmax',kernel_initializer='he_normal')(x)model = keras.Model(inputs=inputs,outputs=outputs)
return model