吴恩达Deep Learning编程作业 Course4- 卷积神经网络-第一周作业:搭建卷积神经网络模型和应用
本周作业我们将使用numpy实现卷积层(CONV)和池化层(POOL)层,以及正向传播和反向传播。
注意:
- 上标表示第层。
- 例如:是指第4层的激活层。和是第五层的权重参数和偏置值。
- 上标表示第个样本
- 例如:指输入的第个样本。
- 下标指向量的第项。
- 例如:指第层的第个激活值。
- 分别表示某个层图像的高度、宽度和通道数。如果想特指某一层,可以写成。
- 分别表示前一层图像的高度、宽度和通道数。如果想特指某一层,可以写成。
1. 使用的包
- numpy:是Python用于科学计算的基本包。
- matplotlib:python用于画图的库。
- np.random.seed(1)用于保持所有随机函数调用的一致性。方便验证你与我的答案是否一致。
import numpy as np
import h5py
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (5.0, 4.0)
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'
np.random.seed(1)
2. 作业大纲
我们需要实现的卷积模块包含以下函数
- 使用0扩充图片边界(Zero padding)
- 卷积窗口(Convolve window)
- 前向卷积(Convolution forward)
- 反向卷积(Convolution backward)
池化模块: - 前向池化(Pooling forward)
- 创建掩码(Create mask)
- 值分配(Distribute value)
- 反向池化(Pooling backward)
我们将使用numpy建立下图中的模型:
注意,每个正向传播函数都有一个对应的反向传播,我们在前向传播的每一步中都需要在缓存中存储一些值,用来计算对应的反向传播的梯度。
3. 卷积神经网络
使用框架实现卷积的过程比较简单,但是其原理理解起来还是比较困难。卷积层将输入矩阵转换成不同大小的输出矩阵,如下图所示:
在这一部分中我们将实现构建卷积层的第一步,实现两个辅助函数:一个用于将图片补零,另一个用于计算卷积函数本身。
3.1 Zero-Padding
Zero-Padding实现的是为图片加上零边界。
Zero-padding的优势:
- 使得卷积后图像的高度和宽度都没有变小,这对于构建更深层次的网络很重要,不然当我们更新到更深层次的时候,高度与宽度将会缩小。一个比较典型的例子是“same”卷积,其高度和宽度在卷积完后不发生改变。
- 可以保留住图片更多的边缘信息。假设没有填充,卷积过程中很容易丢失图像的边缘信息。
练习:实现一个函数,将所有输入图像X填充0,我们将用的使用np.pad
。填充维数为(5,5,5,5,5,5)的数组a,将第二维填充1,第思维填充3,其余填充0,我们将使用到代码:a = np.pad(a, ((0,0), (1,1), (0,0), (3,3), (0,0)), 'constant', constant_values = (..,..))
代码:
def zero_pad(X, pad):
"""
为图像X填充0
:param X:表示m张输入图像,维数为(m, n_H, n_W, n_C)
:param pad:整数类型,表示在垂直和水平方向的填充量
:return:返回填充过的图像维数(m,n_H + 2*pad,n_W + 2*pad,n_C)
"""
X_pad = np.pad(X, ((0, 0), (pad, pad), (pad, pad), (0, 0)), 'constant', constant_values=0)
return X_pad
调用:
if __name__ == '__main__':
np.random.seed(1)
X = np.random.randn(4, 3, 3, 2)
x_pad = zero_pad(X, 2)
print("x.shape = ", X.shape)
print("x_pad.shape = ", x_pad.shape)
print("x[1, 1] = ", X[1, 1])
print("x_pad[1, 1] = ", x_pad[1, 1])
fig, axarr = plt.subplots(1, 2)
axarr[0].set_title('x')
axarr[0].imshow(X[0, :, :, 0])
axarr[1].set_title('x_pad')
axarr[1].imshow(x_pad[0, :, :, 0])
运行结果:
通过上面的代码运行结果可以得到输入数据的信息:
(4,3,3,2),4表示4张图片,第一个3表示图像的高度,第二个3表示图像的宽度,2表示图像的通道数。
3.2 卷积计算过程
在这一部分中,我们将实现卷积的单个步骤,在这个步骤中我们将使用过滤器来计算输入的数据,过程如下图所示:
在计算机视觉应用中,左边矩阵中的每个值对应一个像素值,我们将一个3x3的滤波器与图像卷积,将它的各个元素的值与原始矩阵相乘,然后相加。在练习的第一步中,我们将实现卷积的单个步骤,仅对其中一个位置应用过滤器以获得单个实值输出。
代码:
def conv_single_step(a_slice_prev, W, b):
"""
对前一层输出的激活值用一个含有参数W的过滤器处理。
:param a_slice_prev:输入数据的一部分,维度为(过滤器大小,过滤器大小,上一通道数)
:param W:权重参数,包含在了一个矩阵中,维度为(过滤器大小,过滤器大小,上一通道数)
:param b:偏置参数,包含在了一个矩阵中,维度为(1,1,1)
:return:Z - 在输入数据X经卷积滑动窗口(w,b)处理后的结果。
"""
s = np.multiply(a_slice_prev, W) + b
Z = np.sum(s)
return Z
调用:
np.random.seed(1)
a_slice_prev = np.random.randn(4, 4, 3)
W = np.random.randn(4, 4, 3)
b = np.random.randn(1, 1, 1)
Z = conv_single_step(a_slice_prev, W, b)
print("Z =", Z)
运行结果:
3.3 卷积神经网络的正向传播
在正向传播中,我们将使用许多过滤器对输入数据进行卷积。每个“卷积”会有一个2维矩阵输出。然后你将这些输出叠加起来形成一个三维矩阵。
实现一个函数对上一层获得的激活值进行卷积,该函数的输入为A_prev,前一层的激活输出(对于一批m个输入);F个过滤器权值用W表示,一个偏置向量用b表示,其中每个过滤器都有自己的偏置值。最后,返回超参数字典中包含stride和padding。
注意:
- 如果需要在矩阵A_prev(维数(5,5,3))的左上角选择一个2*2的切片,可以使用下面的代码:
a_slice_prev = a_prev[0:2,0:2,:]
- 定义一个切片通常需要定义切片的角,例如vert_start, vert_end, horiz_start 和horiz_end。如下图所示:
注意:卷积的输出形状与输入形状的关系式为:
代码:
def conv_forward(A_prev, W, b, hparameters):
"""
实现卷积网络的前向传播
:param A_prev:输出激活前一层,维数(m, n_H_prev, n_W_prev, n_C_prev)
:param W:权重,维数(f,f,n_C_prev,n_C)
:param b:偏置值,维数(1,1,1,n_C)
:param hparameters:参数字典,包含stride,pad
:return:Z,维数(m,n_H,n_W,n_C);Cache,参数字典
"""
m, n_H_prev, n_W_prev, n_C_prev = A_prev.shape
f, f, n_C_prev, n_C = W.shape
stride = hparameters['stride']
pad = hparameters['pad']
n_H = int((n_H_prev - f + 2 * pad) / stride) + 1
n_W = int((n_W_prev - f + 2 * pad) / stride) + 1
Z = np.zeros((m, n_H, n_W, n_C))
A_prev_pad = zero_pad(A_prev, pad)
for i in range(m): #训练样本循环
a_prev_pad = A_prev_pad[i] #获取正在处理的样本zero-padding后的结果
for h in range(n_H): #输出的垂直轴上
for w in range(n_W):#输出的水平轴上
for c in range(n_C):#循环遍历输出的通道
#定位当前的切片位置
vert_start = h * stride
vert_end = vert_start + f
horiz_start = w * stride
horiz_end = horiz_start + f
#取出所有层的切片(设想A_prev为一个三通道的彩色图片,你需要把三层都取出来)
a_slice_prev = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end,:]
Z[i, h, w, c] = conv_single_step(a_slice_prev, W[:, :, :, c], b[0, 0, 0, c])
assert (Z.shape == (m, n_H, n_W, n_C))
cache = (A_prev, W, b, hparameters)
return Z, cache
调用:
np.random.seed(1)
A_prev = np.random.randn(10, 4, 4, 3)
W = np.random.randn(2, 2, 3, 8)
b = np.random.randn(1, 1, 1, 8)
hparameters = {"pad":2, "stride":1}
Z, cache_conv = conv_forward(A_prev, W, b, hparameters)
print("Z's mean =", np.mean(Z))
print("cache_conv[0][1][2][3] =", cache_conv[0][1][2][3])
print("cache_conv[0] =", cache_conv[0].shape)
print(Z.shape)
运行结果:
4. 池化(Pooling)层
池化层的主要作用是减少输入的高度和宽度,有助于减少计算损耗,有助于特征检测器在输入中
池(池)层减少了输入的高度和宽度。它有助于减少计算量,也有助于使特征检测器在输入中的位置不变。
两种常见的池化方法:
- 最大值池化层:输入矩阵中滑动一个大小为f*f的窗口,每次选取窗口中的最大值作为输出。
- 均值池化层:输入矩阵中滑动一个大小为f*f的窗口,计算窗口中的平均值,将平均值作为输出。
如下图所示:
在池化层中没有用于反向传播训练的参数,但是有一些超参数,例如窗口的大小。
4.1 正向池化
我们将在同一个函数中实现MAX-POOL和AVG-POOL,但是在池化层中没有padding的过程,计算输出维度公式为:
代码:
def pool_forward(A_prev, hparameters, mode = "max"):
"""
实现池化层的正向传播
:param A_prev:输入的数据,维数(m, n_H_prev, n_W_prev, n_C_prev)
:param hparameters:包含f和stride参数的字典
:param mode:你将使用的池化模式“max”或者“average”
:return:A,cache
"""
m, n_H_prev, n_W_prev, n_C_prev = A_prev.shape
f = hparameters["f"]
stride = hparameters["stride"]
n_H = int(1 + (n_H_prev - f) / stride)
n_W = int(1 + (n_W_prev - f) / stride)
n_C = n_C_prev
A = np.zeros((m, n_H, n_W, n_C))
for i in range(m):
for h in range(n_H):
for w in range(n_W):
for c in range (n_C):
vert_start = h * stride
vert_end = vert_start + f
horiz_start = w * stride
horiz_end = horiz_start + f
a_prev_slice = A_prev[i, vert_start:vert_end, horiz_start:horiz_end, c]
if mode == "max":
A[i, h, w, c] = np.max(a_prev_slice)
elif mode == "average":
A[i, h, w, c] = np.mean(a_prev_slice)
cache = (A_prev, hparameters)
assert (A.shape == (m, n_H, n_W, n_C))
return A, cache
调用:
np.random.seed(1)
A_prev = np.random.randn(2, 4, 4, 3)
hparameters = {"stride": 1, "f": 4}
A, cache = pool_forward(A_prev, hparameters)
print("mode = max")
print("A =", A)
print()
A, cache = pool_forward(A_prev, hparameters, mode="average")
print("mode = average")
print("A =", A)
运行结果:
5.卷积网络中的反向传播
现在有很多深度学习框架使得开发者只需要实现前向传播,由框架负责后向传播。卷积网络的反向传播是复杂的,我们可以在这一部分了解一下卷积网络中的反向传播。
由于我们并没有在课程中学习卷积网络中反向传播公式,因此在下面我们简单的了解一下它们。
5.1 卷积层反向传播
5.1.1 计算dA
计算公式:
是过滤器,是一个标量,是代价函数关于卷积层第h行、第w列输出的梯度。注意,每次更新dA时,我们都将同一个过滤器乘以不同的dZ。每次更新dA时,我们都将同一个过滤器乘以不同的dZ。我们这样做主要是因为在计算正向传播时,每个过滤器和不同的a_slice点乘和求和。因此,在计算dA时,我们只需要添加所有a_slice的梯度。
在编程时我们一般使用下面的代码:da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:,:,:,c] * dZ[i, h, w, c]
5.1.2 计算dW
计算公式:
其中对应于用于生成激活值的切片。因此,就给出了关于这个切片的梯度。因为是相同的,所以我们将所有这些梯度相加得到。
我们将使用到下面的代码:dW[:,:,:,c] += a_slice * dZ[i, h, w, c]
5.1.3 计算db
这是针对某个过滤器的成本计算的公式:
正如我们之前在基本神经网络中看到的,db是通过对求和来计算的。在本例中,您只需将conv输出(Z)相对于成本的所有梯度相加即可。
实现这一步我们将使用下面的代码:db[:,:,:,c] += dZ[i, h, w, c]
练习:实现下面的conv_back函数。总结所有的训练示例、过滤器、高度和宽度。然后使用上面的公式计算导数。
def conv_backward(dZ, cache):
"""
实现卷积神经网络的反向传播
:param dZ:成本相对于conv层(Z)输出的梯度,numpy数组的形状(m, n_H, n_W, n_C)
:param cache:conv_back()所需值的缓存,conv_forward()的输出
:return:
dA_prev:(m, n_H_prev, n_W_prev, n_C_prev)
dW:(f, f, n_C_prev, n_C)
db:(1, 1, 1, n_C)
"""
A_prev, W, b, hparameters = cache
m, n_H_prev, n_W_prev, n_C_prev = A_prev.shape
f, f, n_C_prev, n_C = W.shape
stride = hparameters["stride"]
pad = hparameters["pad"]
m, n_H, n_W, n_C = dZ.shape
dA_prev = np.zeros((m, n_H_prev, n_W_prev, n_C_prev))
dW = np.zeros((f, f, n_C_prev, n_C))
db = np.zeros((1, 1, 1, n_C))
A_prev_pad = zero_pad(A_prev, pad)
dA_prev_pad = zero_pad(dA_prev, pad)
for i in range(m):
a_prev_pad = A_prev_pad[i]
da_prev_pad = dA_prev_pad[i]
for h in range(n_H):
for w in range(n_W):
for c in range (n_C):
vert_start = h * stride
vert_end = vert_start + f
horiz_start = w * stride
horiz_end = horiz_start + f
a_slice = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :]
da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:, :, :, c] * dZ[i, h, w, c]
dW[:, :, :, c] += a_slice * dZ[i, h, w, c]
db[:, :, :, c] += dZ[i, h, w, c]
dA_prev[i, :, :, :] = da_prev_pad[pad:-pad, pad:-pad, :]
assert (dA_prev.shape == (m, n_H_prev, n_W_prev, n_C_prev))
return dA_prev, dW, db
调用:
np.random.seed(1)
dA, dW, db = conv_backward(Z, cache_conv)
print("dA_mean =", np.mean(dA))
print("dW_mean =", np.mean(dW))
print("db_mean =", np.mean(db))
运行结果:
5.2 池化层-反向传播
接下来,我们将实现池化层的反向传播,从MAX-POOL层开始。即使池化层没有需要更新的参数,我们仍然需要通过池化层反向传播梯度,以便计算池化层之前的层的梯度。
5.2.1 最大池化层-反向传播
在跳转到池化层的反向传播之前,我们需要构建一个名为create_mask_from_window()的函数,该函数执行以下操作:
M矩阵中1的位置表示X矩阵中最大元素的位置。
练习:实现create_mask_from_window ()函数实现池化层反向传播的计算。
- np.max()将帮助我们找到矩阵中的最大值。
- 如果有一个矩阵X,假设最大值元素为x:
A = (X = x)
将返回一个与X相同维数的矩阵,其中的元素值:A[i,j] = True if X[i,j] = x
A[i,j] = False if X[i,j] != x
实现代码:
def create_mask_from_window(x):
"""
找到矩阵x经过最大池化后最大值的位置
:param x:
:return: mask:和x有相同的大小,值为1的位置对应矩阵x中的最大值的位置
"""
mask = x == np.max(x)
return mask
调用:
np.random.seed(1)
x = np.random.randn(2, 3)
mask = create_mask_from_window(x)
print('x = ', x)
print("mask = ", mask)
运行结果:
5.2.2 平均池化的反向传播
在max pooling中,对于每个输入窗口,所有对输出的“影响”都来自一个单一的输入值——max。在平均池中,输入窗口的每个元素对输出都有相同的影响。因此,要实现backprop,现在需要实现一个函数。
例如,如果我们使用2x2过滤器对前向通道进行平均池处理,那么用于后向通道的掩码将如下所示:
这意味着矩阵中的每个位置对输出的贡献相等,因为在前向传递中,我们取平均值。
练习:实现一个函数实现,通过维形状矩阵平均分配一个值dz。
def distribute_value(dz, shape):
"""
将输入值分布在维数形状的矩阵中
:param dz:输入值,标量
:param shape:输出矩阵的形状(n_H, n_W),我们要为它分配dz的值
:return:数组的大小(n_H, n_W),我们为其分配了dz的值
"""
(n_H, n_W) = shape
average = dz / (n_H * n_W)
a = np.ones(shape) * average
return a
调用:
np.random.seed(1)
x = np.random.randn(2, 3)
mask = create_mask_from_window(x)
print('x = ', x)
print("mask = ", mask)
运行结果:
5.2.3 将两个方法合并在一起:池化层反向传播
练习:在两种模式下实现pool_back函数(“max”和“average”)。我们将再次使用4个for循环(遍历训练示例、高度、宽度和通道)。您应该使用if/elif语句来查看模式是否等于“max”或“average”。如果它等于“average”,那么应该使用上面实现的distribute_value()函数来创建一个与a_slice形状相同的矩阵。如果模式等于’max’,您将使用create_mask_from_window()创建一个掩码,并将其乘以相应的dZ值。
def pool_backward(dA, cache, mode = "max"):
"""
实现池化层的反向传播
:param dA:成本梯度相对于输出池层,形状与A相同
:param cache:缓存来自池化层的前向传递的输出,包含该层的输入和hparameters
:param mode:池化方式("max" or "average")
:return:dA_prev代价相对于池化层输入的梯度,形状与A_prev相同
"""
A_prev, hparameters = cache
stride = hparameters["stride"]
f = hparameters["f"]
m, n_H_prev, n_W_prev, n_C_prev = A_prev.shape
m, n_H, n_W, n_C = dA.shape
dA_prev = np.zeros_like(A_prev)
for i in range(m):
a_prev = A_prev[i]
for h in range(n_H):
for w in range(n_W):
for c in range(n_C):
vert_start = h * stride
vert_end = vert_start + f
horiz_start = w * stride
horiz_end = horiz_start + f
if mode == "max":
a_prev_slice = a_prev[vert_start:vert_end, horiz_start:horiz_end, c]
mask = create_mask_from_window(a_prev_slice)
dA_prev[i, vert_start:vert_end, horiz_start:horiz_end, c] += np.multiply(mask, dA[i, h, w, c])
elif mode == "average":
da = dA[i, h, w, c]
shape = (f, f)
dA_prev[i, vert_start:vert_end, horiz_start:horiz_end, c] += distribute_value(da, shape)
assert (dA_prev.shape == A_prev.shape)
return dA_prev
调用:
np.random.seed(1)
A_prev = np.random.randn(5, 5, 3, 2)
hparameters = {"stride": 1, "f": 2}
A, cache = pool_forward(A_prev, hparameters)
dA = np.random.randn(5, 4, 2, 2)
dA_prev = pool_backward(dA, cache, mode="max")
print("mode = max")
print('mean of dA = ', np.mean(dA))
print('dA_prev[1,1] = ', dA_prev[1, 1])
print()
dA_prev = pool_backward(dA, cache, mode="average")
print("mode = average")
print('mean of dA = ', np.mean(dA))
print('dA_prev[1,1] = ', dA_prev[1, 1])
运行结果: