吴恩达Deep Learning编程作业 Course4- 卷积神经网络-第一周作业:搭建卷积神经网络模型和应用

本周作业我们将使用numpy实现卷积层(CONV)和池化层(POOL)层,以及正向传播和反向传播。

注意:

  • 上标吴恩达深度学习作业 结构化机器学习项目_吴恩达深度学习作业 结构化机器学习项目表示第吴恩达深度学习作业 结构化机器学习项目_吴恩达深度学习作业 结构化机器学习项目_02层。
  • 例如:吴恩达深度学习作业 结构化机器学习项目_卷积_03是指第4层的激活层。吴恩达深度学习作业 结构化机器学习项目_深度学习_04吴恩达深度学习作业 结构化机器学习项目_神经网络_05是第五层的权重参数和偏置值。
  • 上标吴恩达深度学习作业 结构化机器学习项目_卷积_06表示第吴恩达深度学习作业 结构化机器学习项目_深度学习_07个样本
  • 例如:吴恩达深度学习作业 结构化机器学习项目_吴恩达深度学习作业 结构化机器学习项目_08指输入的第吴恩达深度学习作业 结构化机器学习项目_python_09个样本。
  • 下标吴恩达深度学习作业 结构化机器学习项目_深度学习_07指向量的第吴恩达深度学习作业 结构化机器学习项目_深度学习_07项。
  • 例如:吴恩达深度学习作业 结构化机器学习项目_深度学习_12指第吴恩达深度学习作业 结构化机器学习项目_python_13层的第吴恩达深度学习作业 结构化机器学习项目_python_09个激活值。
  • 吴恩达深度学习作业 结构化机器学习项目_python_15分别表示某个层图像的高度、宽度和通道数。如果想特指某一层,可以写成吴恩达深度学习作业 结构化机器学习项目_python_16
  • 吴恩达深度学习作业 结构化机器学习项目_深度学习_17分别表示前一层图像的高度、宽度和通道数。如果想特指某一层,可以写成吴恩达深度学习作业 结构化机器学习项目_卷积_18

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建立下图中的模型:

吴恩达深度学习作业 结构化机器学习项目_python_19


注意,每个正向传播函数都有一个对应的反向传播,我们在前向传播的每一步中都需要在缓存中存储一些值,用来计算对应的反向传播的梯度。

3. 卷积神经网络

使用框架实现卷积的过程比较简单,但是其原理理解起来还是比较困难。卷积层将输入矩阵转换成不同大小的输出矩阵,如下图所示:

吴恩达深度学习作业 结构化机器学习项目_神经网络_20


在这一部分中我们将实现构建卷积层的第一步,实现两个辅助函数:一个用于将图片补零,另一个用于计算卷积函数本身。

3.1 Zero-Padding

Zero-Padding实现的是为图片加上零边界。

吴恩达深度学习作业 结构化机器学习项目_深度学习_21


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])

运行结果:

吴恩达深度学习作业 结构化机器学习项目_深度学习_22


通过上面的代码运行结果可以得到输入数据的信息:

(4,3,3,2),4表示4张图片,第一个3表示图像的高度,第二个3表示图像的宽度,2表示图像的通道数。

3.2 卷积计算过程

在这一部分中,我们将实现卷积的单个步骤,在这个步骤中我们将使用过滤器来计算输入的数据,过程如下图所示:

吴恩达深度学习作业 结构化机器学习项目_python_23


在计算机视觉应用中,左边矩阵中的每个值对应一个像素值,我们将一个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)

运行结果:

吴恩达深度学习作业 结构化机器学习项目_python_24

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。如下图所示:

    注意:卷积的输出形状与输入形状的关系式为:
    吴恩达深度学习作业 结构化机器学习项目_卷积_25吴恩达深度学习作业 结构化机器学习项目_python_26吴恩达深度学习作业 结构化机器学习项目_吴恩达深度学习作业 结构化机器学习项目_27

代码:

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)

运行结果:

吴恩达深度学习作业 结构化机器学习项目_深度学习_28

4. 池化(Pooling)层

池化层的主要作用是减少输入的高度和宽度,有助于减少计算损耗,有助于特征检测器在输入中
池(池)层减少了输入的高度和宽度。它有助于减少计算量,也有助于使特征检测器在输入中的位置不变。
两种常见的池化方法:

  • 最大值池化层:输入矩阵中滑动一个大小为f*f的窗口,每次选取窗口中的最大值作为输出。
  • 均值池化层:输入矩阵中滑动一个大小为f*f的窗口,计算窗口中的平均值,将平均值作为输出。
    如下图所示:


    在池化层中没有用于反向传播训练的参数,但是有一些超参数,例如窗口的大小吴恩达深度学习作业 结构化机器学习项目_python_29

4.1 正向池化

我们将在同一个函数中实现MAX-POOL和AVG-POOL,但是在池化层中没有padding的过程,计算输出维度公式为:
吴恩达深度学习作业 结构化机器学习项目_吴恩达深度学习作业 结构化机器学习项目_30吴恩达深度学习作业 结构化机器学习项目_python_31吴恩达深度学习作业 结构化机器学习项目_吴恩达深度学习作业 结构化机器学习项目_32

代码:

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)

运行结果:

吴恩达深度学习作业 结构化机器学习项目_深度学习_33

5.卷积网络中的反向传播

现在有很多深度学习框架使得开发者只需要实现前向传播,由框架负责后向传播。卷积网络的反向传播是复杂的,我们可以在这一部分了解一下卷积网络中的反向传播。
由于我们并没有在课程中学习卷积网络中反向传播公式,因此在下面我们简单的了解一下它们。

5.1 卷积层反向传播

5.1.1 计算dA

计算公式:
吴恩达深度学习作业 结构化机器学习项目_python_34
吴恩达深度学习作业 结构化机器学习项目_深度学习_35是过滤器,吴恩达深度学习作业 结构化机器学习项目_神经网络_36是一个标量,吴恩达深度学习作业 结构化机器学习项目_神经网络_36是代价函数关于卷积层第h行、第w列输出的梯度。注意,每次更新dA时,我们都将同一个过滤器吴恩达深度学习作业 结构化机器学习项目_深度学习_35乘以不同的dZ。每次更新dA时,我们都将同一个过滤器吴恩达深度学习作业 结构化机器学习项目_深度学习_35乘以不同的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

计算公式:
吴恩达深度学习作业 结构化机器学习项目_python_40

其中吴恩达深度学习作业 结构化机器学习项目_吴恩达深度学习作业 结构化机器学习项目_41对应于用于生成激活值吴恩达深度学习作业 结构化机器学习项目_深度学习_42的切片。因此,就给出了吴恩达深度学习作业 结构化机器学习项目_卷积_43关于这个切片的梯度。因为是相同的吴恩达深度学习作业 结构化机器学习项目_卷积_43,所以我们将所有这些梯度相加得到吴恩达深度学习作业 结构化机器学习项目_卷积_45
我们将使用到下面的代码:
dW[:,:,:,c] += a_slice * dZ[i, h, w, c]

5.1.3 计算db

这是针对某个过滤器吴恩达深度学习作业 结构化机器学习项目_深度学习_35的成本计算吴恩达深度学习作业 结构化机器学习项目_python_47的公式:
吴恩达深度学习作业 结构化机器学习项目_深度学习_48

正如我们之前在基本神经网络中看到的,db是通过对吴恩达深度学习作业 结构化机器学习项目_神经网络_49求和来计算的。在本例中,您只需将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))

运行结果:

吴恩达深度学习作业 结构化机器学习项目_python_50

5.2 池化层-反向传播

接下来,我们将实现池化层的反向传播,从MAX-POOL层开始。即使池化层没有需要更新的参数,我们仍然需要通过池化层反向传播梯度,以便计算池化层之前的层的梯度。

5.2.1 最大池化层-反向传播

在跳转到池化层的反向传播之前,我们需要构建一个名为create_mask_from_window()的函数,该函数执行以下操作:
吴恩达深度学习作业 结构化机器学习项目_神经网络_51
M矩阵中1的位置表示X矩阵中最大元素的位置。

练习:实现create_mask_from_window ()函数实现池化层反向传播的计算。

  • np.max()将帮助我们找到矩阵中的最大值。
  • 如果有一个矩阵X,假设最大值元素为x:A = (X = x)将返回一个与X相同维数的矩阵,其中的元素值:
    A[i,j] = True if X[i,j] = xA[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)

运行结果:

吴恩达深度学习作业 结构化机器学习项目_深度学习_52

5.2.2 平均池化的反向传播

在max pooling中,对于每个输入窗口,所有对输出的“影响”都来自一个单一的输入值——max。在平均池中,输入窗口的每个元素对输出都有相同的影响。因此,要实现backprop,现在需要实现一个函数。

例如,如果我们使用2x2过滤器对前向通道进行平均池处理,那么用于后向通道的掩码将如下所示:
吴恩达深度学习作业 结构化机器学习项目_python_53
这意味着吴恩达深度学习作业 结构化机器学习项目_神经网络_49矩阵中的每个位置对输出的贡献相等,因为在前向传递中,我们取平均值。

练习:实现一个函数实现,通过维形状矩阵平均分配一个值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)

运行结果:

吴恩达深度学习作业 结构化机器学习项目_吴恩达深度学习作业 结构化机器学习项目_55

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])

运行结果:

吴恩达深度学习作业 结构化机器学习项目_卷积_56