深度学习 | 实战:逐步实现卷积神经网络

我们将使用numpy实现卷积(CONV)和池化(POOL)层,包括正向传播和反向传播。

符号: 

  • 上标[l]表示第  层的对象。
        - 例如:  是  层的激活。  和  是  层的参数。

  • 上标(i)表示第  个示例中的对象。
        - 示例:  是  个训练数据的输入。       

  • 下标i表  的向量输入。
        - 示例:  表l层中  个激活,假设这是全连接层(FC)。   

  •   和  分别表示给定层的通道的高度,宽度和数量。如果要引用特定l,则还可以写入  。 

  • 和 分别表示前一层的高度,宽度和通道数。如果引用特定层,则也可以表示为  

我们已经熟悉numpy,那就开始吧!

准备数据:

cd /Users/bingo/Desktop/dp/work/L3W1

运行以下代码以导入所需的库和数据。

import numpy as np
import h5py
import matplotlib.pyplot as plt

%matplotlib inline
plt.rcParams['figure.figsize'] = (5.0, 4.0) # 设置画布的默认大小
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

%load_ext autoreload
%autoreload 2

np.random.seed(1) #使所有随机函数调用保持一致

1 作业大纲

实现构建卷积神经网络的需要的模块

  • 卷积函数,包括:
    - 零填充
    - 卷积窗口
    - 正向卷积
    - 反向卷积(可选) 

  • 池化函数,包括:
    - 正向池化
    - 创建mask
    - 分配值
    - 反向池化(可选)

任务一将要求你使用 numpy从头开始实现这些函数。在任务二中,将学习使用TensorFlow来实现:

图片

注意,对于每个正向函数,都有其对应的反向等式。因此,在正向传播模块的每一步中,都将一些参数存储在缓存中。这些参数用于在反向传播时计算梯度。

2 卷积神经网络

尽管编程框架可以方便使用卷积,但它们仍然是深度学习中最难理解的概念之一。卷积层将输入体积转换为不同大小的输出体积,如下所示。

图片

在这一部分,你将构建卷积层的每一步。首先实现两个辅助函数:一个用于零填充,另一个用于计算卷积函数本身。

2.1 零填充

零填充将在图像的边界周围添加零:

图片

图1 :零填充

图像(3个通道,RGB),填充2次。

填充的主要好处有:

  • 允许使用CONV层而不必缩小其高度和宽度。这对于构建更深的网络很重要,因为高度/宽度会随着更深的层而缩小。一个重要、特殊的例子是"same"卷积,其中高度/宽度在一层之后被精确保留。 

  • 有助于我们将更多信息保留在图像边缘。如果不进行填充,下一层的一部分值将会受到图像边缘像素的干扰。 

练习:实现以下函数,该功能将使用零填充处理一个批次X的所有图像数据。Use np.pad。注意,如果要填充维度为(5,5,5,5,5)的数组“a”,则第二维的填充为pad = 1,第四维的填充为pad = 3,其余为pad = 0,你可以这样做:

a = np.pad(a, ((0,0), (1,1), (0,0), (3,3), (0,0)), 'constant', constant_values = (..,..))
# GRADED FUNCTION: zero_pad

def zero_pad(X, pad):
    """
    用零填充数据集x的所有图像。填充应用于图像的高度和宽度,如图1所示。

    Argument:
    X -- python numpy array of shape (m, n_H, n_W, n_C) 表示一批m个图像
    pad -- 整数,在垂直和水平维度上的每个图像周围的填充量

    Returns:
    X_pad -- 填充图像的形状 (m, n_H + 2*pad, n_W + 2*pad, n_C)
    """

    ### START CODE HERE ### (≈ 1 line)
    X_pad = np.pad(X, ((0, 0),(pad, pad),(pad, pad),(0, 0)), 'constant', constant_values=0)
    ### END CODE HERE ###

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

预期输出:

图片

3.2 卷积的单个步骤

在这一部分中,实现卷积的单个步骤,其中将滤波器(卷积核)应用于输入的单个位置。这将用于构建卷积单元,该卷积单元:

  • 占用输入体积 

  • 在输入的每个位置都应用滤波器 

  • 输出另一个体积(通常大小不同)

图片

图2卷积操作

滤波器大小为3x3,步幅为1(步幅=每次滑动时移动窗口的数量)

在计算机视觉应用中,左侧矩阵中的每个值都对应一个像素值,我们将3x3滤波器与图像进行卷积操作,首先将滤波器元素的值与原始矩阵相乘,然后将它们相加。在练习的第一步中,你将实现卷积的单个步骤,相当于仅对一个位置应用滤波器以获得单个实值输出。

在本笔记本的后面,你将应用此函数于输入的多个位置以实现完整的卷积运算。

练习:实现conv_single_step()。 提示.

# GRADED FUNCTION: conv_single_step

def conv_single_step(a_slice_prev, W, b):
    """
    在前一层的输出激活的单个片(a_slice_prev)上应用由参数W定义的一个过滤器。

    Arguments:
    a_slice_prev -- 形状(f, f, n_C_prev)输入数据的切片
    W -- 形状为(f, f, n_C_prev)的窗口矩阵中包含的权重参数
    b -- 状为(1,1,1)的窗矩阵中包含的偏差参数

    Returns:
    Z -- 一个标量值,对滑动窗口(W, b)在切片x上进行卷积的结果
    """

    ### START CODE HERE ### (≈ 2 lines of code)
    # a_slice和W之间的元素级乘积. 加上偏差.
    s = np.multiply(a_slice_prev, W) + b
    # 对s的所有项求和
    Z = np.sum(s)
    ### END CODE HERE ###

    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)

预期输出:

Z = -23.16021220252078

2.3 卷积神经网络--正向传递

在正向传递中,使用多个滤波器对输入进行卷积。每个“卷积”都会输出一个2D矩阵。然后,你将堆叠这些输出以获得3D:

图片

练习:实现以下函数,使用滤波器W卷积输入A_prev。此函数将上一层的激活输出(对于一批m个输入)A_prev作为输入,F表示滤波器/权重(W)和偏置向量(b),其中每个滤波器都有自己(单个)偏置。最后,你还可以访问包含stride和padding的超参数字典。

提示
1.要在矩阵“a_prev”(5,5,3)的左上角选择一个2x2切片,请执行以下操作:

a_slice_prev = a_prev[0:2,0:2,:]

使用定义的start/end索引定义a_slice_prev时将非常有用。
2.要定义a_slice,你需要首先定义其角点 vert_startvert_endhoriz_start 和 horiz_end。该图可能有助于你找到如何在下面的代码中使用h,w,f和s定义每个角。

图片

图3 :使用垂直和水平的start/end(2x2滤波器)定义切片

该图仅显示一个通道。

提醒
卷积的输出维度与输入维度相关公式为:

     

对于此作业,我们不必考虑向量化,只使用for循环实现所有函数。

# GRADED FUNCTION: conv_forward

def conv_forward(A_prev, W, b, hparameters):
    """
    实现卷积函数的前向传播

    Arguments:
    A_prev -- 前一层的输出激活,numpy数组的形状(m, n_H_prev, n_W_prev, n_C_prev)
    W -- 权重,numpy数组的形状(f, f, n_C_prev, n_C)
    b -- 偏差, numpy数组的形状为(1,1,1,n_C)
    hparameters -- python字典,包含"stride"和"pad"

    Returns:
    Z -- conv输出,numpy数组的形状(m, n_H, n_W, n_C)
    cache -- 缓存conv_backward()函数所需的值
    """

    ### START CODE HERE ###
    # 从A_prev的形状中获取维度 (≈1 line)  
    (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape

    # 从W的形状中检索维度 (≈1 line)
    (f, f, n_C_prev, n_C) = W.shape

    # 从“hparameters”检索信息 (≈2 lines)
    stride = hparameters['stride']
    pad = hparameters['pad']

    # 使用上面给出的公式计算CONV输出量的尺寸。提示:使用int()来表示层。(≈2 lines)
    n_H = 1 + int((n_H_prev + 2 * pad - f) / stride)
    n_W = 1 + int((n_W_prev + 2 * pad - f) / stride)

    # 用零初始化输出量Z。 (≈1 line)
    Z = np.zeros((m, n_H, n_W, n_C))

    # 通过填充A_prev创建A_prev_pad
    A_prev_pad = zero_pad(A_prev, pad)

    for i in range(m):                               # 循环遍历一批训练示例
        a_prev_pad = A_prev_pad[i]                               # 选择第i个训练示例的填充激活
        for h in range(n_H):                           # 循环在垂直轴上的输出量
            for w in range(n_W):                       # 在输出量的水平轴上循环
                for c in range(n_C):                   # 循环输出量的通道(= #过滤器)

                    # 找到当前“切片”的角落 (≈4 lines)
                    vert_start = h * stride
                    vert_end = vert_start + f
                    horiz_start = w * stride
                    horiz_end = horiz_start + f

                    # 使用角来定义a_prev_pad的(3D)切片(参见单元格上面的提示). (≈1 line)
                    a_slice_prev = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :]

                    # 将(3D)切片与正确的滤波器W和偏置b进行卷积,得到一个输出神经元。 (≈1 line)
                    Z[i, h, w, c] = np.sum(np.multiply(a_slice_prev, W[:, :, :, c]) + b[:, :, :, c])

    ### END CODE HERE ###

    # 确保输出形状是正确的
    assert(Z.shape == (m, n_H, n_W, n_C))

    # 保存信息在“缓存”的backprop
    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])

预期输出:

Z's mean = 0.15585932488906465
cache_conv[0][1][2][3] = [-0.20075807  0.18656139  0.41005165]

最后,CONV层还应包含一个激活,此情况下,我们将添加以下代码行:

# 对窗口进行卷积得到一个输出神经元
Z[i, h, w, c] = ...  
# 应用激活
A[i, h, w, c] = activation(Z[i, h, w, c])

在这里你不需要做这个。

3 池化层

池化(POOL)层减少了输入的高度和宽度。它有助于减少计算量,而且可以使特征检测器在输入中的位置保持不变。池化层有两种:

  • 最大池化:在输入上滑动 ()窗口,并将窗口的最大值存储在输出中。

  • 平均池化:在输入上滑动 ()窗口,并将该窗口的平均值存储在输出中。

图片

    

图片

这些池化层没有用于反向传播训练的参数。但是,它们具有超参数,例如窗口大小,它指定了你要计算最大值或平均值的窗口的高度和宽度。

3.1 正向池化

现在,你将在同一函数中实现最大池化和平均池化。

练习:实现池化层的正向传播。请遵循下述提示。

提示
由于没有填充,因此将池化的输出维度绑定到输入维度的公式为:
      

# GRADED FUNCTION: pool_forward

def pool_forward(A_prev, hparameters, mode = "max"):
    """
    实现池化层的前向传递

    Arguments:
    A_prev -- 输入数据,numpy数组的形状(m, n_H_prev, n_W_prev, n_C_prev)
    hparameters -- python字典包含"f"和"stride"
    mode -- 您想要使用的池模式,定义为字符串(“max”或“average”)

    Returns:
    A -- 池层的输出,一个numpy数组的形状(m, n_H, n_W, n_C)
    cache -- 在池化层的向后传递中使用的Cache,包含输入和hparameters
    """

    # 从输入形状中检索维度
    (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape

    # 从“hparameters”中检索超参数
    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
    A = np.zeros((m, n_H, n_W, n_C))              

    ### START CODE HERE ###
    for i in range(m):                         # 循环训练示例
        for h in range(n_H):                     # 在垂直轴上循环输出
            for w in range(n_W):                 # 在水平轴上循环输出
                for c in range (n_C):            # 在通道上循环输出

                    # 找到当前“切片”的角落 (≈4 lines)
                    vert_start = h * stride
                    vert_end = vert_start + f
                    horiz_start = w * stride
                    horiz_end = horiz_start + f

                    # 使用角来定义通道c, A_prev的第i个训练示例上的当前切片. (≈1 line)
                    a_prev_slice = A_prev[i, vert_start:vert_end, horiz_start:horiz_end, c]

                    # 计算切片上的池操作。使用if语句区分模式。使用np.max / np.mean.
                    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)

    ### END CODE HERE ###

    # 将输入和hparameters存储在pool_backward()的“cache”中
    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)

预期输出:

mode = max
A = [[[[1.74481176 1.6924546  2.10025514]]]


 [[[1.19891788 1.51981682 2.18557541]]]]

mode = average
A = [[[[-0.09498456  0.11180064 -0.14263511]]]


 [[[-0.09525108  0.28325018  0.33035185]]]]

4 卷积神经网络中的反向传播

在深度学习框架中,你只需要实现正向传播,该框架就可以处理反向传播,因此大多数深度学习工程师不需要理会反向传播的细节。卷积网络的反向传播很复杂。但是,如果你愿意,可以来了解卷积网络中反向传播的原理。

之前,当你实现了一个简单的(全连接)神经网络时,你就使用了反向传播来计算损失的导数以更新参数。类似地,在卷积神经网络中,你可以计算损失的导数以更新参数。反向传播方程并非不重要,下面简要介绍了过程。

4.1 卷积层的反向传播

让我们从实现CONV层的反向传播开始。

4.1.1 计算 dA:

这是用于针对特定滤波器的损失和给定训练示例计算的公式:
 其中  是一个滤波器,  是一个标量,相对于第h行和第w列的conv层Z的输出的梯度的损失。请注意,每次更新dA时,我们都会将相同的滤波器  乘以不同的dZ。我们这样做主要是因为在计算正向传播时,每个滤波器都由不同的a_slice进行点乘和求和。因此,在为dA计算backprop时,我们只是加上所有a_slices的梯度。

在适当的for循环内,此公式转换为:

da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:,:,:,c] * dZ[i, h, w, c]
4.1.2 计算 dW:

这是用于针对损失计算  的公式(  是一个滤波器的导数):
 其中a_slice对应于用于生成激活  的切片,最终我们得到W相对于该切片的梯度。由于它是相同的W,因此我们将所有这些梯度加起来即可得到dW。

在适当的for循环内,此公式转换为:

dW[:,:,:,c] += a_slice * dZ[i, h, w, c]
4.1.3 计算 db:

这是用于某个滤波器  的损失计算db的公式:  正如你先前在基本神经网络中所见,db是通过将dZ相加得出的。在这种情况下,你只需要对转换输出(Z)相对于损失的所有梯度求和。

在适当的for循环内,此公式转换为:

db[:,:,:,c] += dZ[i, h, w, c]

练习:在下面实现conv_backward函数。你应该总结所有训练数据,滤波器,高度和宽度。然后,你应该使用上面的公式1、2和3计算导数。

def conv_backward(dZ, cache):
    """
    实现卷积函数的反向传播

    Arguments:
    dZ -- 相对于conv层(Z)输出的代价梯度,numpy数组的形状(m, n_H, n_W, n_C)
    cache -- 缓存conv_backward()所需的值,conv_forward()的输出

    Returns:
    dA_prev -- 成本相对于conv层输入的梯度(A_prev),numpy数组形状(m, n_H_prev, n_W_prev, n_C_prev)
    dW -- 成本相对于conv层权值的梯度(W),数组的形状(f, f, n_C_prev, n_C)
    db -- 成本相对于conv层(b)的偏差的梯度,numpy数组的形状(1,1,1,n_C)
    """

    ### START CODE HERE ###
    # 从“缓存”中检索信息
    (A_prev, W, b, hparameters) = cache

    # 从A_prev的形状中检索维度
    (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape

    # 从W的形状中检索维度
    (f, f, n_C_prev, n_C) = W.shape

    # 从“hparameters”检索信息
    stride = hparameters['stride']
    pad = hparameters['pad']

    # 从dZ的形状中检索维度
    (m, n_H, n_W, n_C) = dZ.shape

    # 用正确的形状初始化dA_prev, dW, db
    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))

    # Pad A_prev 和 dA_prev
    A_prev_pad = zero_pad(A_prev, pad)
    dA_prev_pad = zero_pad(dA_prev, pad)

    for i in range(m):                       # 循环训练示例

        # 从A_prev_pad和dA_prev_pad中选择第i个训练示例
        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_prev_pad中的切片
                    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]

        # 设置第i个训练示例的dA_prev为未填充的da_prev_pad(提示:使用X[pad:-pad, pad:-pad,:])
        dA_prev[i, :, :, :] = dA_prev_pad[i, pad:-pad, pad:-pad, :]
    ### END CODE HERE ###

    # 确保输出形状是正确的
    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))

预期输出:

dA_mean = 9.608990675868995
dW_mean = 10.581741275547566
db_mean = 76.37106919563735

4.2 池化层--反向传播

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

4.2.1 最大池化--反向传播

在进入池化层的反向传播之前,首先构建一个名为create_mask_from_window()的辅助函数,该函数将执行以下操作:  此函数创建一个“掩码”矩阵,该矩阵追踪矩阵的最大值。True(1)表示最大值在X中的位置,其他条目为False(0)。稍后你将看到,平均池的反向传播与此相似,但是使用了不同的掩码。

练习:实现create_mask_from_window()。此函数将有助于反向池化。
提示:

  • [np.max()]()可能会有所帮助。它计算一个数组的最大值。 

  • 如果有一个矩阵X和一个标量x:

    A =(X == x)
    

    将返回与X大小相同的矩阵A,从而:

    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创建掩码,以识别x的最大条目。

    Arguments:
    x -- 形状(f, f)的数组

    Returns:
    mask -- 与window形状相同的数组,在x的最大条目对应的位置包含一个True。
    """

    ### START CODE HERE ### (≈1 line)
    mask = (x == np.max(x))
    ### END CODE HERE ###

    return mask
np.random.seed(1)
x = np.random.randn(2,3)
mask = create_mask_from_window(x)
print('x = ', x)
print("mask = ", mask)

预期输出:

x =  [[ 1.62434536 -0.61175641 -0.52817175]
 [-1.07296862  0.86540763 -2.3015387 ]]
mask =  [[ True False False]
 [False False False]]

为什么我们要追踪最大值的位置?因为这是最终影响输出的输入值,也影响了损失。反向传播算法是根据损失计算梯度的,因此影响最终损失的任何事物都应具有非零的梯度。因此,反向传播将使梯度“传播”回影响损失的特定输入值。

4.2.2 平均池化--反向传播

在最大池化中,对于每个输入窗口,输出上的所有“影响”都来自单个输入值,即最大值。在平均池化中,输入窗口的每个元素对输出的影响均相同 因此,要实现反向传播,你现在将实现一个反映此点的辅助函数。

例如,如果我们使用2x2滤波器在正向传播中进行平均池化,那么用于反向传播的掩码将如下所示:
  这意味着矩阵中的每个位置对输出的贡献均等,因为在正向传播中,我们取平均值。

练习:实现以下函数,以通过维度矩阵平均分配值dz。 提示

def distribute_value(dz, shape):
    """
    将输入值分配到维形矩阵中

    Arguments:
    dz -- 输入标量
    shape -- 我们要分配dz值的输出矩阵的形状(n_H, n_W)

    Returns:
    a -- 我们分配了dz的值的数组大小(n_H, n_W)
    """

    ### START CODE HERE ###
    # 从形状中检索维度 (≈1 line)
    (n_H, n_W) = shape

    # 计算分配到矩阵上的值 (≈1 line)
    average = dz / (n_H * n_W)

    # 创建一个矩阵,其中每个条目都是“平均值” (≈1 line)
    a = np.ones(shape) * average
    ### END CODE HERE ###

    return a
a = distribute_value(2, (2,2))
print('distributed value =', a)

预期输出:

distributed value = [[0.5 0.5]
 [0.5 0.5]]
4.2.3 组合:反向池化

现在,你准备好了在池化层上计算反向传播所需的一切。

练习:在两种模式("max""average")都实现“pool_backward”功能。再次使用4个for循环(遍历训练数据,高度,宽度和通道)。使用 if/elif语句来查看模式是否等于'max''average'。如果等于'average' ,则应使用上面实现的distribute_value()函数创建与 a_slice维度相同的矩阵。此外,模式等于'max'时,你将使用 create_mask_from_window()创建一个掩码,并将其乘以相应的dZ值。

def pool_backward(dA, cache, mode = "max"):
    """
    实现池化层的向后传递

    Arguments:
    dA -- 成本相对于汇聚层产出的梯度,形状与A相同
    cache -- 缓存池层的前传输出,包含该层的输入和hparameters 
    mode -- 您想要使用的池模式,定义为字符串(“max”或“average”)

    Returns:
    dA_prev -- 成本相对于池层输入的梯度,形状与A_prev相同
    """

    ### START CODE HERE ###

    # 从缓存中检索信息 (≈1 line)
    (A_prev, hparameters) = cache

    # 从“hparameters”中检索超参数(≈2 lines)
    stride = hparameters['stride']
    f = hparameters['f']

    # 从A_prev的形状和dA的形状中检索维度 (≈2 lines)
    m, n_H_prev, n_W_prev, n_C_prev = A_prev.shape
    m, n_H, n_W, n_C = dA.shape

    # 用零初始化dA_prev (≈1 line)
    dA_prev = np.zeros_like(A_prev)

    for i in range(m):                       # 循环训练示例

        # 从A_prev中选择训练示例 (≈1 line)
        a_prev = A_prev[i]

        for h in range(n_H):                   # 在垂直轴上循环
            for w in range(n_W):               # 在水平轴上循环
                for c in range(n_C):           # 在通道上循环

                    # 找到当前“切片”的角落(≈4 lines)
                    vert_start = h * stride
                    vert_end = vert_start + f
                    horiz_start = w * stride
                    horiz_end = horiz_start + f

                    # 计算两种模式下的反向传播。
                    if mode == "max":

                        # 使用角和“c”来定义a_prev中的当前切片 (≈1 line)
                        a_prev_slice = a_prev[vert_start:vert_end, horiz_start:horiz_end, c]
                        # 从a_prev_slice创建掩码 (≈1 line)
                        mask = create_mask_from_window(a_prev_slice)
                        # 将dA_prev设置为dA_prev +(掩码乘以dA的正确条目) (≈1 line)
                        dA_prev[i, vert_start: vert_end, horiz_start: horiz_end, c] += mask * dA[i, vert_start, horiz_start, c]

                    elif mode == "average":

                        # 从dA中得到a的值(≈1 line)
                        da = dA[i, vert_start, horiz_start, c]
                        # 定义过滤器的形状为fxf (≈1 line)
                        shape = (f, f)
                        # 分配它以获得正确的dA_prev切片。即将da的分布值相加. (≈1 line)
                        dA_prev[i, vert_start: vert_end, horiz_start: horiz_end, c] += distribute_value(da, shape)

    ### END CODE ###

    # 确保输出形状是正确的
    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]) 

预期输出:

mode = max
mean of dA =  0.14571390272918056
dA_prev[1,1] =  [[ 0.          0.        ]
 [ 5.05844394 -1.68282702]
 [ 0.          0.        ]]

mode = average
mean of dA =  0.14571390272918056
dA_prev[1,1] =  [[ 0.08485462  0.2787552 ]
 [ 1.26461098 -0.25749373]
 [ 1.17975636 -0.53624893]]

现在已经了解了卷积神经网络如何工作,实现了构建神经网络所需的所有模块。在下一项作业中,你将使用TensorFlow实现ConvNet。

卷积神经网络的应用

接下来是第二项作业!

在此笔记本中,你将:

  • 实现模型构建所需的辅助函数 

  • 使用TensorFlow实现功能全面的ConvNet 

完成此作业后,你将能够:

  • 用TensorFlow构建和训练ConvNet解决分类问题 

1 TensorFlow模型

在上一项作业中,我们使用numpy构建了辅助函数,以了解卷积神经网络背后的机制。实际上现在大多数深度学习的应用都是使用编程框架构建的,框架具有许多内置函数,你可以轻松地调用它们。

和之前一样,我们将从加载包开始。

cd /Users/bingo/Desktop/dp/work/L3W1
import math
import numpy as np
import h5py
import matplotlib.pyplot as plt
import scipy
from PIL import Image
from scipy import ndimage
import tensorflow as tf
from tensorflow.python.framework import ops
from cnn_utils import *

%matplotlib inline
np.random.seed(1)

运行以下单元格以加载要使用的“SIGNS”数据集。

# Loading the data (signs)
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = load_dataset()

SIGNS数据集是6个手势符号的图片集,这些符号表示从0到5的数字。

图片

以下单元格将显示数据集中标记图像的示例。随时更改index的值,然后重新运行以查看不同的示例。

# Example of a picture
index = 6
plt.imshow(X_train_orig[index])
print ("y = " + str(np.squeeze(Y_train_orig[:, index])))

预期输出:

图片

在之前的学习中,我们已经为此数据集构建了一个全连接的网络。但是由于这是图像数据集,因此应用ConvNet将更自然。

首先,让我们检查数据的维度。

X_train = X_train_orig/255.
X_test = X_test_orig/255.
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))
conv_layers = {}

预期输出:

number of training examples = 1080
number of test examples = 120
X_train shape: (1080, 64, 64, 3)
Y_train shape: (1080, 6)
X_test shape: (120, 64, 64, 3)
Y_test shape: (120, 6)

1.1 创建占位符

TensorFlow需要为运行会话时输入的数据创建占位符。

练习:实现以下函数为输入图像X和输出Y创建占位符。暂时不用定义训练数据的数量。为此,你可以使用 "None" 作为批次大小,稍后灵活地选择它。因此,X的维度应为 [None, n_H0, n_W0, n_C0],Y的尺寸应为 [None, n_y]。 提示。

# GRADED FUNCTION: create_placeholders

def create_placeholders(n_H0, n_W0, n_C0, n_y):
    """
    为tensorflow会话创建占位符。

    Arguments:
    n_H0 -- 标量,输入图像的高度
    n_W0 -- 标量,输入图像的宽度
    n_C0 -- 标量,输入通道的数量
    n_y -- 标量,类的数量

    Returns:
    X -- 数据输入的占位符,形状[None, n_H0, n_W0, n_C0]和dtype "float"
    Y -- 形状[None, n_y]和dtype "float"的输入标签的占位符
    """

    ### START CODE HERE ### (≈3 lines)#TensoFlow2.0及以上的版本,需要加入第一句
    tf.compat.v1.disable_eager_execution()
    X = tf.compat.v1.placeholder(tf.float32, shape=(None, n_H0, n_W0, n_C0))
    Y = tf.compat.v1.placeholder(tf.float32,shape=(None,n_y))
    ### END CODE HERE ###

    return X, Y
X, Y = create_placeholders(64, 64, 3, 6)
print ("X = " + str(X))
print ("Y = " + str(Y))

预期输出:

X = Tensor("Placeholder:0", shape=(None, 64, 64, 3), dtype=float32)
Y = Tensor("Placeholder_1:0", shape=(None, 6), dtype=float32)

1.2 初始化参数

你将使用tf.contrib.layers.xavier_initializer(seed = 0)初始化权重/滤波器W1和W2。你无需担心偏差变量,因为TensorFlow函数可以处理偏差。还要注意你只会为conv2d函数初始化权重/滤波器,TensorFlow将自动初始化全连接部分的层。在本作业的后面,我们将详细讨论。

练习:实现initialize_parameters(),下面提供了每组过滤器的尺寸。
提示:在Tensorflow中初始化维度为[1,2,3,4]的参数W,使用:

W = tf.get_variable("W", [1,2,3,4], initializer = ...)
# GRADED FUNCTION: initialize_parameters

def initialize_parameters():
    """
    初始化权值参数,构建张量流神经网络。形状是:
                        W1 : [4, 4, 3, 8]
                        W2 : [2, 2, 8, 16]
    Returns:
    parameters -- 包含W1, W2张量的字典
    """

    tf.compat.v1.set_random_seed(1)                              # "随机"数字统一了

    ### START CODE HERE ### (approx. 2 lines of code)
    W1 =tf.compat.v1.get_variable('W1',[4,4,3,8],initializer=tf.initializers.GlorotUniform(0))
    W2 = tf.compat.v1.get_variable('W2',[2,2,8,16],initializer=tf.initializers.GlorotUniform(0))
    ### END CODE HERE ###
    parameters = {"W1": W1,
                  "W2": W2}

    return parameters
tf.compat.v1.reset_default_graph()
with tf.compat.v1.Session() as sess_test:
    parameters = initialize_parameters()
    init = tf.compat.v1.global_variables_initializer()
    sess_test.run(init)
    print("W1 = " + str(parameters["W1"].eval()[1,1,1]))
    print("W2 = " + str(parameters["W2"].eval()[1,1,1]))

预期输出:

W1 = [-0.05346771  0.18349849 -0.01215445  0.00138046  0.0012947  -0.02904211
 -0.11260509 -0.143055  ]
W2 = [-0.1713624   0.09527719 -0.0744766  -0.02245569  0.24450928 -0.06879854
  0.21546292 -0.08803296 -0.16513646 -0.19527972 -0.22957063  0.15745944
  0.13090086 -0.12304181 -0.05287278  0.03434092]

1.3 正向传播

在TensorFlow 1中,有内置函数为你执行卷积步骤。

  • tf.nn.conv2d(X,W1, strides = [1,s,s,1], padding = 'SAME'): 给定输入和一组滤波器,函数将使用的滤波器卷积X。第三个输入([1,f,f,1])表示输入的每个维度(m, n_H_prev, n_W_prev, n_C_prev)的步幅。

  • tf.nn.max_pool(A, ksize = [1,f,f,1], strides = [1,s,s,1], padding = 'SAME'):给定输入A,此函数使用大小为(f,f)的窗口和大小为(s,s)的步幅在每个窗口上进行最大池化。

  • tf.nn.relu(Z1): 计算Z1的ReLU激活输出(可以是任何形状)。

  • tf.contrib.layers.flatten(P): 给定输入P,此函数将每个示例展平为一维向量,同时保持批量大小。它返回维度为[batch_size,k]的展平张量。

  • tf.contrib.layers.fully_connected(F, num_outputs): 给定展平的输入F,它将返回用全连接层计算出的输出。

在上面的最后一个函数(tf.contrib.layers.fully_connected)中,全连接层会自动初始化图中的权重,并在训练模型时继续对其进行训练。因此,初始化参数时无需初始化这些权重。

练习

实现下面的forward_propagation函数以构建以下模型:CONV2D-> RELU-> MAXPOOL-> CONV2D-> RELU-> MAXPOOL-> FLATTEN-> FULLYCONNECTED。使用上面那些函数。

具体地,我们将在所有步骤中使用以下参数:
- Conv2D:步幅为1,填充为“SAME”
- ReLU
- Max pool:使用8x8的滤波器和8x8的步幅,填充为“SAME”
- Conv2D:步幅为1,填充为“SAME”
- ReLU
- Max pool:使用4x4的滤波器和4x4的步幅,填充为“SAME”
- 展平之前的输出。
- FULLYCONNECTED(FC)层:应用不含非线性激活函数的全连接层。请勿在此处调用softmax。这将在输出层中产生6个神经元,然后将其传递给softmax。在TensorFlow中,softmax和cost函数被合并为一个函数,在计算损失时将调用另一个函数。

# GRADED FUNCTION: forward_propagation

def forward_propagation(X, parameters):
    """
    实现模型的前向传播:
    CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

    Arguments:
    X -- 输入数据集占位符,形状(输入大小,示例数量)
    parameters -- python字典包含你的参数“W1”,“W2”这些形状在initialize_parameters中给出

   Returns:
    Z3 -- 最后一个线性单元的输出
    """

    # 从字典“parameters”中检索参数
    W1 = parameters['W1']
    W2 = parameters['W2']

    ### START CODE HERE ###
    # CONV2D: stride of 1, padding 'SAME'
    Z1 = tf.nn.conv2d(X,W1, strides = [1,1,1,1], padding = 'SAME')
    # RELU
    A1 = tf.nn.relu(Z1)
    # MAXPOOL: window 8x8, sride 8, padding 'SAME'
    P1 = tf.nn.max_pool(A1, ksize = [1,8,8,1], strides = [1,8,8,1], padding = 'SAME')
    # CONV2D: filters W2, stride 1, padding 'SAME'
    Z2 = tf.nn.conv2d(P1,W2, strides = [1,1,1,1], padding = 'SAME')
    # RELU
    A2 = tf.nn.relu(Z2)
    # MAXPOOL: window 4x4, stride 4, padding 'SAME'
    P2 = tf.nn.max_pool(A2, ksize = [1,4,4,1], strides = [1,4,4,1], padding = 'SAME')
    # FLATTEN
    P2 = keras.layers.Flatten(input_shape=[1,4,4,1])(P2)
    # 完全连接没有非线性激活功能(不是不调用softmax)。
    # 6输出层的神经元。提示:其中一个参数应该是"activation_fn=None"
    Z3 =tf.compat.v1.layers.dense(P2, 6)
    ### END CODE HERE ###


    return Z3
tf.compat.v1.reset_default_graph()

with tf.compat.v1.Session() as sess:
    np.random.seed(1)
    X, Y = create_placeholders(64, 64, 3, 6)
    parameters = initialize_parameters()
    Z3 = forward_propagation(X, parameters)
    init = tf.compat.v1.global_variables_initializer()
    sess.run(init)
    a = sess.run(Z3, {X: np.random.randn(2,64,64,3), Y: np.random.randn(2,6)})
    print("Z3 = " + str(a))

预期输出:

Z3 = [[ 1.4086003  -0.17228425 -1.1539357  -0.58591306 -0.79419005 -1.6718631 ]
 [ 1.611041   -0.3182032  -1.3212292  -0.89768225 -0.81365263 -1.4538314 ]]

1.4 计算损失

在下面实现损失函数的计算,你可能会发现以下两个函数很有帮助:

  • tf.nn.softmax_cross_entropy_with_logits(logits = Z3, labels = Y): 计算softmax熵损失,该函数会计算softmax激活函数以及由此产生的损失。

  • tf.reduce_mean: 计算张量各维度上元素的均值,用它来对所有训练示例的损失求和,以获得总损失。 

练习:使用上面的函数计算下述损失。

# GRADED FUNCTION: compute_cost 

def compute_cost(Z3, Y):
    """
    计算成本

    Arguments:
    Z3 -- 前向传播输出(最后一个线性单元的输出),形状(6,样例数)
    Y -- “true”标记矢量占位符,形状与Z3相同

    Returns:
    cost - 成本函数的张量
    """

    ### START CODE HERE ### (1 line of code)
    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=Z3, labels=Y))
    ### END CODE HERE ###

    return cost
tf.compat.v1.reset_default_graph()

with tf.compat.v1.Session() as sess:
    np.random.seed(1)
    X, Y = create_placeholders(64, 64, 3, 6)
    parameters = initialize_parameters()
    Z3 = forward_propagation(X, parameters)
    cost = compute_cost(Z3, Y)
    init = tf.compat.v1.global_variables_initializer()
    sess.run(init)
    a = sess.run(cost, {X: np.random.randn(4,64,64,3), Y: np.random.randn(4,6)})
    print("cost = " + str(a))

预期输出:

cost = 1.523118

1.5 构建模型

最后,你将合并以上实现的辅助函数以构建模型并在SIGNS数据集上对其进行训练。

我们已经在之前的“优化算法”编程作业中实现了random_mini_batches(),记住此函数返回的是一个小批次的处理列表。

练习:完成以下函数:

以下模型应:

  • 创建占位符 

  • 初始化参数 

  • 正向传播 

  • 计算损失 

  • 创建优化函数 

最后,你将创建一个会话并为num_epochs运行一个for循环,获取小批次处理,然后针对每个小批次运行优化函数。

# GRADED FUNCTION: model

def model(X_train, Y_train, X_test, Y_test, learning_rate = 0.009,
          num_epochs = 100, minibatch_size = 64, print_cost = True):
    """
    Tensorflow实现三层ConvNet:
    CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

    Arguments:
    X_train -- 训练集,形状 (None, 64, 64, 3)
    Y_train -- 测试集,形状(None, n_y = 6)
    X_test -- 训练集,形状 (None, 64, 64, 3)
    Y_test -- 测试集,形状 (None, n_y = 6)
    learning_rate -- 优化的学习速率
    num_epochs -- 优化循环的epoch数
    minibatch_size -- minibatch的大小
    print_cost -- True 每100个epoch打印成本

    Returns:
    train_accuracy -- 实数,训练集的精确度(X_train)
    test_accuracy -- 实数,测试集的精确度 (X_test)
    parameters -- 模型学习到的参数。它们可以用来预测。
    """

    ops.reset_default_graph()                         # 能够重新运行模型而不重写tf变量
    tf.compat.v1.set_random_seed(1)                             # 保持结果一致(tensorflow seed)
    seed = 3                                          # 保持结果一致(numpy seed)
    (m, n_H0, n_W0, n_C0) = X_train.shape             
    n_y = Y_train.shape[1]                            
    costs = []                                        # 跟踪成本

    # 创建正确形状的占位符
    ### START CODE HERE ### (1 line)
    X, Y = create_placeholders(n_H0, n_W0, n_C0, n_y)
    ### END CODE HERE ###

    # 初始化参数
    ### START CODE HERE ### (1 line)
    parameters = initialize_parameters()
    ### END CODE HERE ###

    # 前向传播:在tensorflow graph中建立前向传播
    ### START CODE HERE ### (1 line)
    Z3 = forward_propagation(X, parameters)
    ### END CODE HERE ###

    # 本函数:在tensorflow graph中添加成本函数
    ### START CODE HERE ### (1 line)
    cost = compute_cost(Z3, Y)
    ### END CODE HERE ###

    # 反向传播:定义张量流优化器。使用一个最小化成本的AdamOptimizer。
    ### START CODE HERE ### (1 line)
    optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
    ### END CODE HERE ###

    # 全局初始化所有变量
    init = tf.compat.v1.global_variables_initializer()

    # 启动会话来计算tensorflow graph
    with tf.compat.v1.Session() as sess:

        # 运行初始化
        sess.run(init)

        # 做循环训练
        for epoch in range(num_epochs):

            minibatch_cost = 0.
            num_minibatches = int(m / minibatch_size) # 训练集中大小为minibatch_size的批数
            seed = seed + 1
            minibatches = random_mini_batches(X_train, Y_train, minibatch_size, seed)

            for minibatch in minibatches:

                # 选择一个minibatch
                (minibatch_X, minibatch_Y) = minibatch
                # 重要提示:在小批处理中运行图表的线。
                #运行会话来执行优化器和成本,feed应该包含(X,Y)的小批处理。
                ### START CODE HERE ### (1 line)
                _ , temp_cost = sess.run([optimizer, cost], feed_dict={X:minibatch_X, Y:minibatch_Y})
                ### END CODE HERE ###

                minibatch_cost += temp_cost / num_minibatches


            # 每个epoch都打印成本
            if print_cost == True and epoch % 5 == 0:
                print ("Cost after epoch %i: %f" % (epoch, minibatch_cost))
            if print_cost == True and epoch % 1 == 0:
                costs.append(minibatch_cost)


        # 画出成本
        plt.plot(np.squeeze(costs))
        plt.ylabel('cost')
        plt.xlabel('iterations (per tens)')
        plt.title("Learning rate =" + str(learning_rate))
        plt.show()

        # 计算正确的精确度
        predict_op = tf.argmax(Z3, 1)
        correct_prediction = tf.equal(predict_op, tf.argmax(Y, 1))

        # 在测试集上计算精度
        accuracy = tf.compat.v1.reduce_mean(tf.cast(correct_prediction, "float"))
        print(accuracy)
        train_accuracy = accuracy.eval({X: X_train, Y: Y_train})
        test_accuracy = accuracy.eval({X: X_test, Y: Y_test})
        print("Train Accuracy:", train_accuracy)
        print("Test Accuracy:", test_accuracy)

        return train_accuracy, test_accuracy, parameters

运行以下单元格以训练模型100个epoch。

_, _, parameters = model(X_train, Y_train, X_test, Y_test)

预期输出:

图片

图片

参考资料:

1.吴恩达深度学习教程
https://mooc.study.163.com/learn/2001281002?tid=2403041000&_trace_c_p_k2_=825e6d16de8047ab8ff9d73f13571115#/learn/content?type=detail&id=2403379427&cid=2403403252
2.吴恩达《深度学习》作业线上版
https://zhuanlan.zhihu.com/p/95510114
3.Adam paper: https://arxiv.org/pdf/1412.6980.pdf
  • 38
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值