残差网络
1、非常深的神经网络的问题
2、构建残差函数
3、在自己的图像上进行测试
第四门课:卷积神经网络
第二周:深度卷积网络:实例探究
你将学习如何构建非常深的卷积网络,使用残差网络(ResNets)。在理论上,非常深的网络可以表示非常复杂的函数;但在实践中,它们很难训练。残差网络由何凯明等人引入,使你能够训练比以前实际可行的更深的网络。
在这个任务中,你将:
1、实现残差网络的基本构建块。
2、将这些构建块组合起来,实现和训练一个用于图像分类的最先进的神经网络。
3、本次任务将使用Keras完成。
import numpy as np
from keras import layers
from keras.layers import Input, Add, Dense, Activation, ZeroPadding2D, BatchNormalization, Flatten, Conv2D, AveragePooling2D, MaxPooling2D, GlobalMaxPooling2D
from keras.models import Model, load_model
from keras.preprocessing import image
from keras.utils import layer_utils
from keras.utils.data_utils import get_file
from keras.applications.imagenet_utils import preprocess_input
import pydot
from IPython.display import SVG
from keras.utils.vis_utils import model_to_dot
from keras.utils import plot_model
from resnets_utils import *
from keras.initializers import glorot_uniform
import scipy.misc
from matplotlib.pyplot import imshow
import keras.backend as K
K.set_image_data_format('channels_last')
K.set_learning_phase(1)
一、非常深的神经网络的问题
在上周,你构建了你的第一个卷积神经网络。近年来,神经网络变得越来越深,最先进的网络层数从几层(例如AlexNet)增加到超过一百层。
非常深的网络的主要优点是它可以表示非常复杂的函数。它还可以在许多不同的抽象层次上学习特征,从边缘(在较低层次上)到非常复杂的特征(在更深的层次上)。然而,使用更深的网络并不总是有帮助的。训练它们的一个巨大障碍是梯度消失:非常深的网络通常具有迅速趋近于零的梯度信号,从而使梯度下降变得难以忍受地慢。更具体地说,在梯度下降期间,当你从最后一层向后传播到第一层时,你在每个步骤中都要乘以权重矩阵,因此梯度可以呈指数级地迅速减小到零(或者,在极少数情况下,指数级地增长并“爆炸”到非常大的值)。
在训练过程中,你可能会发现早期层的梯度的幅度(或范数)随着训练的进行而迅速减小到零:
二、构建残差网络
在ResNets中,“shortcut"或"skip connection"允许梯度直接反向传播到较早的层次:
左侧的图像显示了网络中的"主路径”。右侧的图像在主路径上添加了一个shortcut。通过将这些ResNet块堆叠在一起,你可以构建一个非常深的网络。
我们在课程中还看到,使用shortcut的ResNet块也使得其中一个块很容易学习成为一个恒等函数。这意味着你可以在额外的ResNet块上堆叠,而几乎不会对训练集性能造成影响。(还有一些证据表明,学习恒等函数的便利性——甚至比跳过连接对抗梯度消失有更大的帮助——解释了ResNets的显著性能。)
在ResNet中使用两种主要类型的块,主要取决于输入/输出维度是否相同。你将实现这两种类型的块。
1、恒等块
恒等块是ResNets中使用的标准块,对应于输入激活(假设为𝑎[𝑙])与输出激活(假设为𝑎[𝑙+2])具有相同维度的情况。为了详细说明ResNet的恒等块中发生的不同步骤,这里是一个显示各个步骤的替代图表:
上方路径是"shortcut路径"。下方路径是"main路径"。在这个图表中,我们还明确了每个层中的CONV2D和ReLU步骤。为了加快训练速度,我们还添加了一个BatchNorm步骤。不要担心这个实现会很复杂——你会发现在Keras中,BatchNorm只需一行代码!
在这个练习中,你将实际上实现一个稍微更强大的恒等块版本,其中跳过连接"跳过了"3个隐藏层,而不是2个层。它的结构如下所示:
主路径的第一个组件:
1、第一个CONV2D层具有形状为(1,1)和步幅为(1,1)的𝐹1个滤波器。填充方式为"valid",名称应为conv_name_base + ‘2a’。使用0作为随机初始化的种子。
2、第一个BatchNorm层对通道轴进行归一化。名称应为bn_name_base + ‘2a’。
3、然后应用ReLU激活函数。这没有名称和超参数。
主路径的第二个组件:
1、第二个CONV2D层具有形状为(𝑓,𝑓)和步幅为(1,1)的𝐹2个滤波器。填充方式为"same",名称应为conv_name_base + ‘2b’。使用0作为随机初始化的种子。
2、第二个BatchNorm层对通道轴进行归一化。名称应为bn_name_base + ‘2b’。
3、然后应用ReLU激活函数。这没有名称和超参数。
主路径的第三个组件:
1、第三个CONV2D层具有形状为(1,1)和步幅为(1,1)的𝐹3个滤波器。填充方式为"valid",名称应为conv_name_base + ‘2c’。使用0作为随机初始化的种子。
2、第三个BatchNorm层对通道轴进行归一化。名称应为bn_name_base + ‘2c’。请注意,此组件中没有ReLU激活函数。
最后一步:
1、将shortcut与输入相加。
2、然后应用ReLU激活函数。这没有名称和超参数。
练习:实现ResNet的恒等块。我们已经实现了主路径的第一个组件。请仔细阅读并确保你理解它的功能。你应该实现其余的组件。
1、实现Conv2D步骤:参考文档
2、实现BatchNorm:参考文档(轴:整数,应进行归一化的轴(通常是通道轴))
3、对于激活函数,使用:Activation(‘relu’)(X)
4、将通过shortcut传递的值相加:参考文档
按照图3中定义的恒等块实现如下:
参数:
X -- 输入张量,形状为 (m, n_H_prev, n_W_prev, n_C_prev)
f -- 整数,指定主路径中间 CONV 层窗口的形状
filters -- Python 列表,定义主路径中 CONV 层的滤波器数量
stage -- 整数,用于根据其在网络中的位置给层命名
block -- 字符串/字符,用于根据其在网络中的位置给层命名
返回:X -- 恒等块的输出,形状为 (n_H, n_W, n_C)
# GRADED FUNCTION: identity_block
def identity_block(X, f, filters, stage, block):
# defining name basis
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch'
# Retrieve Filters
F1, F2, F3 = filters
# Save the input value. You'll need this later to add back to the main path.
X_shortcut = X
# First component of main path
X = Conv2D(filters = F1, kernel_size = (1, 1), strides = (1,1), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X)
X = Activation('relu')(X)
# Second component of main path
X = Conv2D(filters = F2, kernel_size = (f, f), strides = (1,1), padding = 'same', name = conv_name_base + '2b', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X)
X = Activation('relu')(X)
# Third component of main path
X = Conv2D(filters = F3, kernel_size = (1, 1), strides = (1,1), padding = 'valid', name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X)
# Final step: Add shortcut value to main path, and pass it through a RELU activation
X = Add()([X, X_shortcut])
X = Activation('relu')(X)
return X
tf.reset_default_graph()
with tf.Session() as test:
np.random.seed(1)
A_prev = tf.placeholder("float", [3, 4, 4, 6])
X = np.random.randn(3, 4, 4, 6)
A = identity_block(A_prev, f = 2, filters = [2, 4, 6], stage = 1, block = 'a')
test.run(tf.global_variables_initializer())
out = test.run([A], feed_dict={A_prev: X, K.learning_phase(): 0})
print("out = " + str(out[0][1][1][0]))
out [ 0.94822985 0. 1.16101444 2.747859 0. 1.36677003]
2、卷积块
已经实现了ResNet中的恒等块。接下来,ResNet的"卷积块"是另一种类型的块。当输入和输出的维度不匹配时,可以使用这种类型的块。与恒等块的区别在于,快捷路径中有一个CONV2D层:
快捷路径中的CONV2D层用于调整输入 𝑥 的维度,以便在最后的加法中,将快捷值添加回主路径时维度匹配。(这类似于课程中讨论的矩阵 𝑊𝑠 的作用。) 例如,要将激活的高度和宽度缩小2倍,可以使用步幅为2的1x1卷积。快捷路径上的CONV2D层不使用任何非线性激活函数。它的主要作用只是应用一个(学习到的)线性函数来减小输入的维度,以便在后续的加法步骤中维度匹配。
卷积块的详细步骤如下:
主路径的第一个组件:
1、第一个CONV2D层具有形状为(1,1)和步幅为(s,s)的𝐹1个滤波器。填充方式为"valid",名称应为conv_name_base + ‘2a’。
2、第一个BatchNorm层对通道轴进行归一化。名称应为bn_name_base + ‘2a’。
3、然后应用ReLU激活函数。这没有名称和超参数。
主路径的第二个组件:
1、第二个CONV2D层具有形状为(f,f)和步幅为(1,1)的𝐹2个滤波器。填充方式为"same",名称应为conv_name_base + ‘2b’。
2、第二个BatchNorm层对通道轴进行归一化。名称应为bn_name_base + ‘2b’。
3、然后应用ReLU激活函数。这没有名称和超参数。
主路径的第三个组件:
1、第三个CONV2D层具有形状为(1,1)和步幅为(1,1)的𝐹3个滤波器。填充方式为"valid",名称应为conv_name_base + ‘2c’。
2、第三个BatchNorm层对通道轴进行归一化。名称应为bn_name_base + ‘2c’。请注意,此组件中没有ReLU激活函数。
快捷路径:
1、CONV2D层具有形状为(1,1)和步幅为(s,s)的𝐹3个滤波器。填充方式为"valid",名称应为conv_name_base + ‘1’。
2、BatchNorm层对通道轴进行归一化。名称应为bn_name_base + ‘1’。
最后一步:
1、将快捷路径和主路径的值相加。
2、然后应用ReLU激活函数。这没有名称和超参数。
练习:实现卷积块。我们已经实现了主路径的第一个组件,你应该实现其余的组件。与之前一样,始终使用0作为随机初始化的种子,以确保与我们的评分器一致。
Conv提示
BatchNorm 提示(axis:整数,应进行归一化的轴(通常是特征轴))
对于激活函数,使用:Activation(‘relu’)(X)
加法提示
按照图4中定义的卷积块实现如下:
参数:
X -- 输入张量,形状为 (m, n_H_prev, n_W_prev, n_C_prev)
f -- 整数,指定主路径中间 CONV 层窗口的形状
filters -- Python 列表,定义主路径中 CONV 层的滤波器数量
stage -- 整数,用于根据其在网络中的位置给层命名
block -- 字符串/字符,用于根据其在网络中的位置给层命名
s -- 整数,指定要使用的步幅
返回:X -- 卷积块的输出,形状为 (n_H, n_W, n_C)
# GRADED FUNCTION: convolutional_block
def convolutional_block(X, f, filters, stage, block, s = 2):
# defining name basis
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch'
# Retrieve Filters
F1, F2, F3 = filters
# Save the input value
X_shortcut = X
# First component of main path
X = Conv2D(filters = F1, kernel_size = (1, 1), strides = (s,s), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X)
X = Activation('relu')(X)
# Second component of main path (≈3 lines)
X = Conv2D(F2, (f, f), strides = (1,1), name = conv_name_base + '2b', padding = 'same', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X)
X = Activation('relu')(X)
# Third component of main path (≈2 lines)
X = Conv2D(F3, (1, 1), strides = (1,1), name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X)
##### SHORTCUT PATH ####
X_shortcut = Conv2D(F3, (1, 1), strides = (s,s), name = conv_name_base + '1', kernel_initializer = glorot_uniform(seed=0))(X_shortcut)
X_shortcut = BatchNormalization(axis = 3, name = bn_name_base + '1')(X_shortcut)
# Final step: Add shortcut value to main path, and pass it through a RELU activation (≈2 lines)
X = Add()([X, X_shortcut])
X = Activation('relu')(X)
return X
tf.reset_default_graph()
with tf.Session() as test:
np.random.seed(1)
A_prev = tf.placeholder("float", [3, 4, 4, 6])
X = np.random.randn(3, 4, 4, 6)
A = convolutional_block(A_prev, f = 2, filters = [2, 4, 6], stage = 1, block = 'a')
test.run(tf.global_variables_initializer())
out = test.run([A], feed_dict={A_prev: X, K.learning_phase(): 0})
print("out = " + str(out[0][1][1][0]))
out [ 0.09018463 1.23489773 0.46822017 0.0367176 0. 0.65516603]
3、构建你的第一个ResNet模型(50层)
你现在已经拥有了构建非常深的ResNet所需的块。下面的图详细描述了这个神经网络的架构。图中的"ID BLOCK"代表"恒等块",“ID BLOCK x3"表示你应该将3个恒等块堆叠在一起。
这个ResNet-50模型的细节如下:
1、使用(3,3)的填充将输入进行填充。
2、第1阶段:
2D卷积层具有64个形状为(7,7)的滤波器,并使用步幅为(2,2)。它的名称为"conv1”。
BatchNorm应用于输入的通道轴。
MaxPooling使用(3,3)的窗口和(2,2)的步幅。
2、第2阶段:
卷积块使用大小为[64,64,256]的三组滤波器,"f"为3,“s"为1,块的名称为"a”。
两个恒等块使用大小为[64,64,256]的三组滤波器,“f"为3,块的名称分别为"b"和"c”。
3、第3阶段:
卷积块使用大小为[128,128,512]的三组滤波器,"f"为3,“s"为2,块的名称为"a”。
三个恒等块使用大小为[128,128,512]的三组滤波器,“f"为3,块的名称分别为"b”、“c"和"d”。
4、第4阶段:
卷积块使用大小为[256,256,1024]的三组滤波器,"f"为3,“s"为2,块的名称为"a”。
五个恒等块使用大小为[256,256,1024]的三组滤波器,“f"为3,块的名称分别为"b”、“c”、“d”、“e"和"f”。
5、第5阶段:
卷积块使用大小为[512,512,2048]的三组滤波器,“f"为3,“s"为2,块的名称为"a”。
两个恒等块使用大小为[512,512,2048]的三组滤波器,“f"为3,块的名称分别为"b"和"c”。
6、2D平均池化层使用形状为(2,2)的窗口,名称为"avg_pool”。
7、Flatten层没有任何超参数或名称。
8、全连接(Dense)层使用softmax激活函数将输入减少到类别数。它的名称应为’fc’ + str(classes)。
练习:实现上面图中描述的具有50层的ResNet。我们已经实现了第1阶段和第2阶段。请实现剩下的部分。(实现第3-5阶段的语法应该与第2阶段的相似。)请确保按照上述文本中的命名规范进行命名。
你将需要使用以下函数:
1、Average pooling 参考
以下是我们在下面的代码中使用的其他函数:
2、Conv2D:参考
3、BatchNorm:参考(axis:整数,应进行归一化的轴(通常是特征轴))
4、Zero padding:参考
5、Max pooling:参考
6、全连接层:参考
7、加法:参考
实现了ResNet50的流行架构如下:
CONV2D -> BATCHNORM -> RELU -> MAXPOOL -> CONVBLOCK -> IDBLOCK2 -> CONVBLOCK -> IDBLOCK3
-> CONVBLOCK -> IDBLOCK5 -> CONVBLOCK -> IDBLOCK2 -> AVGPOOL -> TOPLAYER
参数:
input_shape -- 数据集图像的形状
classes -- 整数,类别数量
返回:model -- Keras中的一个Model()实例
# GRADED FUNCTION: ResNet50
def ResNet50(input_shape = (64, 64, 3), classes = 6):
# Define the input as a tensor with shape input_shape
X_input = Input(input_shape)
# Zero-Padding
X = ZeroPadding2D((3, 3))(X_input)
# Stage 1
X = Conv2D(64, (7, 7), strides = (2, 2), name = 'conv1', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = 'bn_conv1')(X)
X = Activation('relu')(X)
X = MaxPooling2D((3, 3), strides=(2, 2))(X)
# Stage 2
X = convolutional_block(X, f = 3, filters = [64, 64, 256], stage = 2, block='a', s = 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')
# Stage 3
X = convolutional_block(X, f = 3, filters = [128, 128, 512], stage = 3, block='a', s = 2)
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')
# Stage 4
X = convolutional_block(X, f = 3, filters = [256, 256, 1024], stage = 4, block='a', s = 2)
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')
# Stage 5
X = convolutional_block(X, f = 3, filters = [512, 512, 2048], stage = 5, block='a', s = 2)
X = identity_block(X, 3, [512, 512, 2048], stage=5, block='b')
X = identity_block(X, 3, [512, 512, 2048], stage=5, block='c')
# AVGPOOL. Use "X = AveragePooling2D(...)(X)"
X = AveragePooling2D((2, 2), name='avg_pool')(X)
# output layer
X = Flatten()(X)
X = Dense(classes, activation='softmax', name='fc' + str(classes), kernel_initializer = glorot_uniform(seed=0))(X)
# Create model
model = Model(inputs = X_input, outputs = X, name='ResNet50')
return model
运行以下代码以构建模型的图。如果你的实现不正确,通过运行下面的model.fit(…)来检查你的准确性,你将会知道。
model = ResNet50(input_shape = (64, 64, 3), classes = 6)
如在Keras教程笔记本中所见,在训练模型之前,你需要通过编译模型来配置学习过程。
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = load_dataset()
# Normalize image vectors
X_train = X_train_orig/255.
X_test = X_test_orig/255.
# Convert training and test labels to one hot matrices
Y_train = convert_to_one_hot(Y_train_orig, 6).T
Y_test = convert_to_one_hot(Y_test_orig, 6).T
print ("number of training examples = " + str(X_train.shape[0]))
print ("number of test examples = " + str(X_test.shape[0]))
print ("X_train shape: " + str(X_train.shape))
print ("Y_train shape: " + str(Y_train.shape))
print ("X_test shape: " + str(X_test.shape))
print ("Y_test shape: " + str(Y_test.shape))
model.fit(X_train, Y_train, epochs = 2, batch_size = 32)
输出:
** Epoch 1/2** loss: between 1 and 5, acc: between 0.2 and 0.5, although your results can be different from ours.
** Epoch 2/2** loss: between 1 and 5, acc: between 0.2 and 0.5, you should see your loss decreasing and the accuracy increasing.
让我们看看这个模型(仅经过两个epoch训练)在测试集上的表现。
preds = model.evaluate(X_test, Y_test)
print ("Loss = " + str(preds[0]))
print ("Test Accuracy = " + str(preds[1]))
Test Accuracy between 0.16 and 0.25
为了这个作业的目的,我们要求你只训练模型两个epoch。你可以看到它的性能很差。请继续提交你的作业;为了检查正确性,在线评估器也只会运行你的代码一小部分epoch。
在完成这个官方(评分)部分的作业后,如果你愿意,你还可以选择为ResNet进行更多的迭代训练。当我们在CPU上训练时,训练大约需要一个多小时才能获得更好的性能。
使用GPU,我们已经在SIGNS数据集上训练了自己的ResNet50模型的权重。你可以在下面的单元格中加载和运行我们训练好的模型在测试集上。加载模型可能需要大约1分钟的时间。
model = load_model('ResNet50.h5')
preds = model.evaluate(X_test, Y_test)
print ("Loss = " + str(preds[0]))
print ("Test Accuracy = " + str(preds[1]))
当适当数量的迭代训练时,ResNet50是一个强大的图像分类模型。我们希望你能运用所学,并将其应用于你自己的分类问题,以获得最先进的准确性。
你现在已经实现了一个最先进的图像分类系统!
三、在自己的图像上进行测试
你也可以拍一张你自己的手的照片,并查看模型的输出。操作步骤如下:
- 点击此笔记本上方的"文件",然后点击"打开"以进入你的Coursera Hub。
- 将你的图像添加到此Jupyter笔记本的目录中,放在"images"文件夹中。
- 在下面的代码中写入你的图像名称。
- 运行代码,检查算法是否正确!
img_path = 'images/my_image.jpg'
img = image.load_img(img_path, target_size=(64, 64))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
print('Input image shape:', x.shape)
my_image = scipy.misc.imread(img_path)
imshow(my_image)
print("class prediction vector [p(0), p(1), p(2), p(3), p(4), p(5)] = ")
print(model.predict(x))
# print a summary of your model by running the following code
model.summary()
# run the code below to visualize your ResNet50. You can also download a .png picture of your model by going to "File -> Open...-> model.png".
plot_model(model, to_file='model.png')
SVG(model_to_dot(model).create(prog='dot', format='svg'))
记住:
- 跳跃连接有助于解决梯度消失问题。它们还使得ResNet块很容易学习身份函数。
- 有两种主要类型的块:身份块和卷积块。
- 构建非常深的残差网络是通过将这些块堆叠在一起实现的。
- 参考资料
该笔记本介绍了He等人(2015年)提出的ResNet算法。这里的实现也在很大程度上受到Francois Chollet的github存储库中给出的结构的启发和影响:
Kaiming He,Xiangyu Zhang,Shaoqing Ren,Jian Sun - 深度残差学习用于图像识别(2015年)
Francois Chollet的github存储库:https://github.com/fchollet/deep-learning-models/blob/master/resnet50.py