本周作业我们将使用numpy实现卷积层(CONV)和池化层(POOL)层,以及正向传播和反向传播。
注意:
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)
我们需要实现的卷积模块包含以下函数
我们将使用numpy建立下图中的模型:
注意,每个正向传播函数都有一个对应的反向传播,我们在前向传播的每一步中都需要在缓存中存储一些值,用来计算对应的反向传播的梯度。
使用框架实现卷积的过程比较简单,但是其原理理解起来还是比较困难。卷积层将输入矩阵转换成不同大小的输出矩阵,如下图所示:
在这一部分中我们将实现构建卷积层的第一步,实现两个辅助函数:一个用于将图片补零,另一个用于计算卷积函数本身。
Zero-Padding实现的是为图片加上零边界。
Zero-padding的优势:
练习:实现一个函数,将所有输入图像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表示图像的通道数。
在这一部分中,我们将实现卷积的单个步骤,在这个步骤中我们将使用过滤器来计算输入的数据,过程如下图所示:
在计算机视觉应用中,左边矩阵中的每个值对应一个像素值,我们将一个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)
运行结果:
在正向传播中,我们将使用许多过滤器对输入数据进行卷积。每个“卷积”会有一个2维矩阵输出。然后你将这些输出叠加起来形成一个三维矩阵。
实现一个函数对上一层获得的激活值进行卷积,该函数的输入为A_prev,前一层的激活输出(对于一批m个输入);F个过滤器权值用W表示,一个偏置向量用b表示,其中每个过滤器都有自己的偏置值。最后,返回超参数字典中包含stride和padding。
注意:
a_slice_prev = a_prev[0:2,0:2,:]
代码:
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)
池化层的主要作用是减少输入的高度和宽度,有助于减少计算损耗,有助于特征检测器在输入中
池(池)层减少了输入的高度和宽度。它有助于减少计算量,也有助于使特征检测器在输入中的位置不变。
两种常见的池化方法:
我们将在同一个函数中实现MAX-POOL和AVG-POOL,但是在池化层中没有padding的过程,计算输出维度公式为:
n H = ⌊ n H p r e v − f s t r i d e ⌋ + 1 n_H = \lfloor \frac{n_{H_{prev}} - f}{stride} \rfloor +1 nH=⌊stridenHprev−f⌋+1 n W = ⌊ n W p r e v − f s t r i d e ⌋ + 1 n_W = \lfloor \frac{n_{W_{prev}} - f}{stride} \rfloor +1 nW=⌊stridenWprev−f⌋+1 n C = n C p r e v n_C = n_{C_{prev}} nC=nCprev
代码:
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)
现在有很多深度学习框架使得开发者只需要实现前向传播,由框架负责后向传播。卷积网络的反向传播是复杂的,我们可以在这一部分了解一下卷积网络中的反向传播。
由于我们并没有在课程中学习卷积网络中反向传播公式,因此在下面我们简单的了解一下它们。
计算公式:
KaTeX parse error: Expected 'EOF', got '}' at position 63: … \times dZ_{hw}}̲
W c W_c Wc是过滤器, d Z h w dZ_{hw} dZhw是一个标量, d Z h w dZ_{hw} dZhw是代价函数关于卷积层第h行、第w列输出的梯度。注意,每次更新dA时,我们都将同一个过滤器 W c W_c Wc乘以不同的dZ。每次更新dA时,我们都将同一个过滤器 W c W_c Wc乘以不同的dZ。我们这样做主要是因为在计算正向传播时,每个过滤器和不同的a_slice点乘和求和。因此,在计算dA时,我们只需要添加所有a_slice的梯度。
在编程时我们一般使用下面的代码:
da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:,:,:,c] * dZ[i, h, w, c]
计算公式:
d W c + = ∑ h = 0 n H ∑ w = 0 n W a s l i c e × d Z h w (2) dW_c += \sum _{h=0} ^{n_H} \sum_{w=0} ^ {n_W} a_{slice} \times dZ_{hw} \tag{2} dWc+=h=0∑nHw=0∑nWaslice×dZhw(2)
其中 a s l i c e a_{slice} aslice对应于用于生成激活值 Z i j Z_{ij} Zij的切片。因此,就给出了 W W W关于这个切片的梯度。因为是相同的 W W W,所以我们将所有这些梯度相加得到 d W dW dW。
我们将使用到下面的代码:
dW[:,:,:,c] += a_slice * dZ[i, h, w, c]
这是针对某个过滤器 W c W_c Wc的成本计算 d b db db的公式:
d b = ∑ h ∑ w d Z h w db = \sum_h \sum_w dZ_{hw} db=h∑w∑dZhw
正如我们之前在基本神经网络中看到的,db是通过对 d Z dZ dZ求和来计算的。在本例中,您只需将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))
接下来,我们将实现池化层的反向传播,从MAX-POOL层开始。即使池化层没有需要更新的参数,我们仍然需要通过池化层反向传播梯度,以便计算池化层之前的层的梯度。
在跳转到池化层的反向传播之前,我们需要构建一个名为create_mask_from_window()的函数,该函数执行以下操作:
X = [ 1 3 4 2 ] → M = [ 0 0 1 0 ] X = \left[\begin{matrix} 1 & 3 \\ 4 & 2 \end{matrix}\right] \quad \rightarrow \quad M =\left[\begin{matrix} 0 &0 \\ 1 &0 \end{matrix}\right] X=[1432]→M=[0100]
M矩阵中1的位置表示X矩阵中最大元素的位置。
练习:实现create_mask_from_window ()函数实现池化层反向传播的计算。
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)
在max pooling中,对于每个输入窗口,所有对输出的“影响”都来自一个单一的输入值——max。在平均池中,输入窗口的每个元素对输出都有相同的影响。因此,要实现backprop,现在需要实现一个函数。
例如,如果我们使用2x2过滤器对前向通道进行平均池处理,那么用于后向通道的掩码将如下所示:
d Z = 1 → d Z = [ 1 / 4 1 / 4 1 / 4 1 / 4 ] dZ = 1 \quad \rightarrow \quad dZ =\begin{bmatrix} 1/4 & 1/4 \\ 1/4 & 1/4 \end{bmatrix} dZ=1→dZ=[1/41/41/41/4]
这意味着 d Z dZ dZ矩阵中的每个位置对输出的贡献相等,因为在前向传递中,我们取平均值。
练习:实现一个函数实现,通过维形状矩阵平均分配一个值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)
练习:在两种模式下实现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])