15 构建CNN(二)

发布时间 2023-10-07 13:48:11作者: 王哲MGG_AI
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)

这段代码是Python代码,主要是导入一些库并进行一些配置。

  1. import numpy as np: 这行代码导入了NumPy库,通常使用np作为别名来引用它。NumPy是Python中用于科学计算的库,它提供了对多维数组和矩阵的支持,以及用于数学操作的函数。

  2. import h5py: 这行代码导入了h5py库,它用于处理HDF5格式的数据文件。HDF5是一种用于存储和组织大型数据集的文件格式。

  3. import matplotlib.pyplot as plt: 这行代码导入了Matplotlib库的子模块pyplot,并将其别名为plt。Matplotlib是一个用于绘制图表和图形的库。

  4. %matplotlib inline: 这是一个Jupyter Notebook的魔术命令,它告诉Jupyter在Notebook中显示Matplotlib绘制的图形,并且图形将嵌入在Notebook中而不是弹出一个新的窗口。

  5. plt.rcParams['figure.figsize'] = (5.0, 4.0): 这行代码设置Matplotlib图形的默认大小为5.0x4.0英寸。

  6. plt.rcParams['image.interpolation'] = 'nearest': 这行代码设置图像的插值方法为“nearest”,这意味着在显示图像时不进行插值,保持图像像素的原始值。

  7. plt.rcParams['image.cmap'] = 'gray': 这行代码设置图像的颜色映射为灰度('gray'),这通常用于显示灰度图像。

  8. %load_ext autoreload: 这是另一个Jupyter Notebook的魔术命令,用于加载autoreload扩展,允许在修改代码后自动重新加载模块。

  9. %autoreload 2: 这是autoreload扩展的设置,将其配置为在代码被修改后自动重新加载所有模块。

  10. np.random.seed(1): 这行代码设置NumPy的随机数种子为1,这样可以确保在不同的运行中生成的随机数是相同的,以便进行实验的可重复性。

这段代码主要用于配置工作环境,包括导入所需的库、设置图形参数以及设置随机种子,以便在进行随机操作时获得可重复的结果。

def zero_pad(X, pad):
    X_pad = np.pad(X, ((0, 0), (pad, pad), (pad, pad), (0, 0)), 'constant', constant_values=0)
    return X_pad

这段代码定义了一个名为 zero_pad 的函数,用于在输入的多维数组 X 的周围添加零填充。

  • def zero_pad(X, pad):: 这是函数的定义,它接受两个参数:X 表示输入的多维数组,pad 表示要添加的零填充的数量。

  • X_pad = np.pad(X, ((0, 0), (pad, pad), (pad, pad), (0, 0)), 'constant', constant_values=0): 这行代码使用NumPy的 np.pad 函数来进行零填充操作。具体来说:

    • X 是要填充的多维数组。
    • ((0, 0), (pad, pad), (pad, pad), (0, 0)) 是指定填充量的参数。这个参数是一个四元组,每个元素都是一个元组,分别表示四个维度的填充量。在这里:
      • (0, 0) 表示在第一个维度上不填充,因此 (0, 0) 表示不在样本数维度上填充。
      • (pad, pad) 表示在第二维度上填充 pad 个零,即在图像宽度上填充。
      • (pad, pad) 表示在第三维度上填充 pad 个零,即在图像高度上填充。
      • (0, 0) 表示在第四维度上不填充,因此 (0, 0) 表示不在通道数维度上填充。
    • 'constant' 表示要使用常数值进行填充。
    • constant_values=0 指定了填充时使用的常数值,这里是零。
  • 最后,函数返回了填充后的数组 X_pad

这个函数的主要作用是在输入数组 X 的周围添加零填充,通常在卷积神经网络(CNN)中用于处理不同大小的输入图像,以确保它们具有相同的尺寸。

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

这段代码对之前定义的 zero_pad 函数进行了测试,并输出一些相关的结果。

  1. np.random.seed(1): 这行代码设置了随机数生成器的种子为1,以确保在不同运行中生成相同的随机数。

  2. x = np.random.randn(4, 3, 3, 2): 这行代码生成了一个随机的4维数组 x,其中包含了随机的浮点数值。它的维度是 (4, 3, 3, 2),表示有4个样本,每个样本是一个3x3x2的张量。

  3. x_pad = zero_pad(x, 2): 这行代码调用了之前定义的 zero_pad 函数,将输入数组 x 进行零填充,填充的数量为2。结果存储在 x_pad 中。

  4. print ("x.shape =", x.shape): 这行代码输出了 x 的形状(shape),即 (4, 3, 3, 2)

  5. print ("x_pad.shape =", x_pad.shape): 这行代码输出了 x_pad 的形状,由于进行了填充,形状会发生变化,具体取决于填充的数量。

  6. print ("x[1, 1] =", x[1, 1]): 这行代码输出了 x 中的一个元素,即 x[1, 1],它是一个3x2的子张量。

  7. print ("x_pad[1, 1] =", x_pad[1, 1]): 这行代码输出了 x_pad 中的一个元素,即 x_pad[1, 1],它是一个5x5的子张量,由于进行了填充,所以形状不同于 x[1, 1]

  8. fig, axarr = plt.subplots(1, 2): 这行代码创建了一个Matplotlib图形窗口,其中包含两个子图。

  9. axarr[0].set_title('x')axarr[1].set_title('x_pad'): 这两行代码设置了子图的标题。

  10. axarr[0].imshow(x[0,:,:,0])axarr[1].imshow(x_pad[0,:,:,0]): 这两行代码分别在第一个子图和第二个子图中显示 xx_pad 的子张量(通道0),以图像形式展示出来。

总之,这段代码用于创建一个随机的多维数组 x,然后对其进行零填充,最后输出数组的形状并可视化显示 xx_pad 中的一些子张量。这有助于理解填充对数组形状的影响。

def conv_single_step(a_slice_prev, W, b):
    s = np.multiply(a_slice_prev, W) + b

    Z = np.sum(s)

    return Z

这段代码定义了一个名为 conv_single_step 的函数,用于执行卷积操作的单步(单个滤波器)。

  • def conv_single_step(a_slice_prev, W, b):: 这是函数的定义,它接受三个参数:

    • a_slice_prev:输入的部分数据,也就是卷积核在输入数据上滑动时覆盖的区域。
    • W:卷积核的权重参数。
    • b:偏置参数。
  • s = np.multiply(a_slice_prev, W) + b: 这行代码计算了卷积操作的中间结果。具体来说:

    • np.multiply(a_slice_prev, W) 执行了元素级别的乘法操作,即将输入数据 a_slice_prev 中的每个元素与卷积核 W 中的对应元素相乘。
    • b 被加到上述结果中,以添加偏置项。
  • Z = np.sum(s): 这行代码计算了卷积的最终结果 Z。它对中间结果 s 中的所有元素求和,得到一个标量值。

  • 最后,函数返回了卷积操作的结果 Z

这个函数执行了卷积操作的一个步骤,将输入数据 a_slice_prev 与卷积核 W 进行逐元素相乘,然后将结果求和,最后添加偏置项。这是卷积神经网络(CNN)中卷积层的基本操作之一,用于提取特征映射。

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)

这段代码用于测试之前定义的 conv_single_step 函数,执行卷积操作的单步并输出结果。

  1. np.random.seed(1): 这行代码设置了随机数生成器的种子为1,以确保在不同运行中生成相同的随机数。

  2. a_slice_prev = np.random.randn(4, 4, 3): 这行代码生成了一个随机的3维数组 a_slice_prev,表示输入数据的一部分。它的形状是 (4, 4, 3),表示一个4x4x3的数据块。

  3. W = np.random.randn(4, 4, 3): 这行代码生成了一个随机的3维数组 W,表示卷积核的权重参数。它的形状与 a_slice_prev 相同,即 (4, 4, 3)

  4. b = np.random.randn(1, 1, 1): 这行代码生成了一个随机的偏置参数 b,它是一个标量值。

  5. Z = conv_single_step(a_slice_prev, W, b): 这行代码调用了之前定义的 conv_single_step 函数,将输入数据 a_slice_prev、卷积核 W 和偏置参数 b 传递给函数,并计算卷积操作的结果。结果存储在变量 Z 中。

  6. print("Z =", Z): 这行代码输出了卷积操作的结果 Z

总之,这段代码测试了 conv_single_step 函数,通过将随机生成的输入数据 a_slice_prev、卷积核 W 和偏置参数 b 传递给函数,计算了卷积操作的结果 Z。这是卷积神经网络中的基本卷积步骤。

def conv_forward(A_prev, W, b, hparameters):

    (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]                   
        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_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[...,c])
 
    assert(Z.shape == (m, n_H, n_W, n_C))
    cache = (A_prev, W, b, hparameters)
    
    return Z, cache

这段代码定义了一个卷积层的前向传播函数 conv_forward,该函数用于执行卷积操作,并包含了一些计算和循环来处理输入数据、卷积核、偏置等参数。

  • def conv_forward(A_prev, W, b, hparameters):: 这是函数的定义,它接受四个参数:

    • A_prev:先前层的激活输出,也就是输入数据,是一个4维数组。
    • W:卷积核的权重参数,是一个4维数组。
    • b:偏置参数,是一个3维数组。
    • hparameters:包含卷积的超参数的字典,其中包括 stride(步幅)和 pad(填充)的信息。
  • (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape: 这行代码获取输入数据 A_prev 的形状信息,其中 m 是样本数,n_H_prevn_W_prev 分别是输入图像的高度和宽度,n_C_prev 是通道数。

  • (f, f, n_C_prev, n_C) = W.shape: 这行代码获取卷积核 W 的形状信息,其中 f 是卷积核的高度和宽度,n_C_prev 是输入通道数,n_C 是输出通道数。

  • stride = hparameters['stride']pad = hparameters['pad']:这两行代码从超参数字典 hparameters 中提取步幅和填充信息。

  • n_Hn_W:这两行代码计算输出特征图的高度和宽度,根据输入数据、卷积核和超参数计算而得。

  • Z = np.zeros((m, n_H, n_W, n_C)):这行代码创建一个用于存储卷积结果的零数组 Z,其形状与输出特征图的形状相同。

  • A_prev_pad = zero_pad(A_prev, pad): 这行代码调用之前定义的 zero_pad 函数,对输入数据进行填充,以处理边界情况。

  • 接下来的循环嵌套用于执行卷积操作。它遍历样本 (m)、输出特征图的高度 (n_H)、输出特征图的宽度 (n_W) 和输出通道数 (n_C)。

  • 在循环内部,计算 a_slice_prev,这是从输入数据 A_prev_pad 中切片出来的输入数据块,然后调用 conv_single_step 函数来执行单步卷积操作,将结果存储在 Z 中。

  • assert(Z.shape == (m, n_H, n_W, n_C)):这行代码用来确保输出的 Z 的形状与预期的形状一致。

  • 最后,函数返回卷积的结果 Z 和一个包含输入数据、卷积核、偏置和超参数的缓存 cache

这个函数执行了卷积层的前向传播操作,将输入数据经过卷积核的滑动与相乘、加上偏置项后,生成了输出特征图 Z

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

这段代码测试了之前定义的 conv_forward 函数,用于执行卷积层的前向传播操作,并输出了卷积结果及缓存。

  1. np.random.seed(1): 这行代码设置了随机数生成器的种子为1,以确保在不同运行中生成相同的随机数。

  2. A_prev = np.random.randn(10, 4, 4, 3): 这行代码生成了一个随机的输入数据 A_prev,表示先前层的激活输出。它的形状是 (10, 4, 4, 3),表示有10个样本,每个样本是一个4x4x3的数据块。

  3. W = np.random.randn(2, 2, 3, 8): 这行代码生成了一个随机的卷积核 W,其形状为 (2, 2, 3, 8),表示卷积核的大小为2x2,输入通道数为3,输出通道数为8。

  4. b = np.random.randn(1, 1, 1, 8): 这行代码生成了一个随机的偏置参数 b,其形状为 (1, 1, 1, 8),与卷积核的输出通道数相匹配。

  5. hparameters = {"pad" : 2, "stride": 1}: 这行代码定义了超参数字典 hparameters,其中包括填充 (pad) 和步幅 (stride) 的信息。

  6. Z, cache_conv = conv_forward(A_prev, W, b, hparameters): 这行代码调用了 conv_forward 函数,传递输入数据 A_prev、卷积核 W、偏置参数 b 和超参数 hparameters 给函数,并计算卷积层的前向传播结果 Z,同时返回了缓存 cache_conv 用于后续的反向传播。

  7. print("Z's mean =", np.mean(Z)): 这行代码计算并输出卷积结果 Z 的平均值,以了解卷积操作的整体效果。

  8. print("cache_conv[0][1][2][3] =", cache_conv[0][1][2][3]): 这行代码输出了缓存 cache_conv 中的一个元素,用于展示缓存的结构。

总之,这段代码测试了卷积层的前向传播操作,使用随机生成的输入数据、卷积核和偏置参数,计算了卷积结果 Z,并展示了一些相关信息。

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

这段代码与之前的代码块相同,测试了卷积层的前向传播操作,使用了相同的输入数据 A_prev、卷积核 W、偏置参数 b 和超参数 hparameters。它执行了卷积操作并输出了卷积结果的平均值以及缓存的某个元素的值。这个示例旨在演示如何使用前向传播函数进行卷积操作。

def pool_forward(A_prev, hparameters, mode = "max"):
    """
    参数:
    A_prev -- 输入矩阵,也就是上一层的输出矩阵。维度是(m, n_H_prev, n_W_prev, n_C_prev)
    hparameters -- 超参数窗口大小f和步长s
    mode -- 池化模式,最大池化就写max,如果想用平均池化,就写average。
    
    返回值:
    A -- 池化层的输出矩阵,维度是(m, n_H, n_W, n_C)
    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) # np.max就是numpy库中求最大值的函数
                    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

这段代码定义了一个池化层的前向传播函数 pool_forward,用于执行池化操作(最大池化或平均池化),并返回池化层的输出和缓存。

  • def pool_forward(A_prev, hparameters, mode = "max"):: 这是函数的定义,它接受三个参数:

    • A_prev:输入矩阵,也就是上一层的输出矩阵,维度是 (m, n_H_prev, n_W_prev, n_C_prev)
    • hparameters:包含超参数的字典,其中包括窗口大小 f 和步幅 stride 的信息。
    • mode:池化模式,可以是 "max"(最大池化)或 "average"(平均池化),默认为 "max"。
  • (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape: 这行代码获取输入矩阵 A_prev 的形状信息,其中 m 是样本数,n_H_prevn_W_prev 分别是输入图像的高度和宽度,n_C_prev 是通道数。

  • f = hparameters["f"]stride = hparameters["stride"]: 这两行代码从超参数字典 hparameters 中提取窗口大小 f 和步幅 stride 的信息。

  • 计算输出矩阵的尺寸 n_Hn_W,以及输出通道数 n_C。这是根据输入矩阵、窗口大小和步幅计算得出的。

  • 初始化输出矩阵 A,其形状与输出尺寸相匹配。

  • 接下来的嵌套循环用于执行池化操作。它遍历样本 (m)、输出特征图的高度 (n_H)、输出特征图的宽度 (n_W) 和输出通道数 (n_C)。

  • 在循环内部,计算出输入矩阵中当前池化窗口的索引,并提取子矩阵窗口 a_prev_slice

  • 执行池化操作,如果 mode 是 "max",则使用 np.max 函数计算窗口中的最大值,如果 mode 是 "average",则使用 np.mean 函数计算窗口中的平均值。

  • 最后,函数返回池化层的输出 A 和一个包含输入数据和超参数的缓存 cache

这个函数用于执行池化层的前向传播操作,将输入数据 A_prev 经过池化操作(最大池化或平均池化)生成输出特征图 A

# 单元测试
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):

  1. np.random.seed(1): 这行代码设置了随机数生成器的种子为1,以确保在不同运行中生成相同的随机数。

  2. A_prev = np.random.randn(2, 4, 4, 3): 这行代码生成了一个随机的输入数据 A_prev,表示上一层的输出矩阵。它的形状是 (2, 4, 4, 3),表示有2个样本,每个样本是一个4x4x3的数据块。

  3. hparameters = {"stride" : 1, "f": 4}: 这行代码定义了超参数字典 hparameters,包括步幅 stride 和窗口大小 f 的信息。在这个测试中,步幅为1,窗口大小为4x4。

  4. A, cache = pool_forward(A_prev, hparameters): 这行代码调用了 pool_forward 函数,使用最大池化模式,默认情况下,计算池化层的输出 A 和缓存 cache

  5. print("mode = max"): 这行代码输出了当前测试模式,即最大池化模式。

  6. print("A =", A): 这行代码输出了最大池化模式下的池化层输出 A

接下来,测试平均池化模式(mode = average):

  1. A, cache = pool_forward(A_prev, hparameters, mode = "average"): 这行代码调用了 pool_forward 函数,指定了平均池化模式,计算池化层的输出 A 和缓存 cache

  2. print("mode = average"): 这行代码输出了当前测试模式,即平均池化模式。

  3. print("A =", A): 这行代码输出了平均池化模式下的池化层输出 A

总之,这段代码进行了两个池化层的单元测试,分别使用了最大池化模式和平均池化模式,并输出了池化层的输出结果。

def conv_backward(dZ, cache):
    """    
    参数:
    dZ -- 后一层相关的dZ,维度是(m, n_H, n_W, n_C)
    cache -- 前面的conv_forward()函数保存下来的缓存数据
    
    Returns:
    dA_prev -- 本卷积层输入矩阵的dA,维度是(m, n_H_prev, n_W_prev, n_C_prev)
    dW -- 本卷积层相关的dW,维度是(f, f, n_C_prev, n_C)
    db -- 本卷积层相关的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"] # padding数量
    
    (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
                    vert_end = vert_start + f
                    horiz_start = w
                    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

这段代码定义了卷积层的反向传播函数 conv_backward,用于计算关于输入数据 A_prev、权重参数 W 和偏置参数 b 的梯度,以及返回关于上一层的梯度 dA_prev

  • def conv_backward(dZ, cache):: 这是函数的定义,它接受两个参数:

    • dZ:后一层相关的梯度,维度是 (m, n_H, n_W, n_C),其中 m 是样本数,n_Hn_W 分别是输出特征图的高度和宽度,n_C 是输出通道数。
    • cache:前向传播函数 conv_forward 保存下来的缓存数据,其中包含了输入数据 A_prev、权重参数 W、偏置参数 b 和超参数的信息。
  • (A_prev, W, b, hparameters) = cache: 这行代码从缓存中获取前向传播所需的数据,包括输入数据 A_prev、权重参数 W、偏置参数 b 和超参数 hparameters

  • 获取输入数据的形状信息 (m, n_H_prev, n_W_prev, n_C_prev)

  • 获取权重参数 W 的形状信息 (f, f, n_C_prev, n_C)

  • 从超参数字典 hparameters 中获取步幅 stride 和填充 pad 的信息。

  • 计算关于输入数据的梯度 dA_prev,初始化为零数组,形状与输入数据 A_prev 相同。

  • 初始化权重参数 W 和偏置参数 b 的梯度 dWdb 为零数组,形状分别与 Wb 相同。

  • 使用 zero_pad 函数对输入数据 A_prevdA_prev 进行填充,以处理边界情况。

  • 开始循环遍历每个样本 (m) 和输出特征图的高度和宽度 (n_H, n_W) 以及输出通道数 (n_C)。

  • 在循环内部,计算输入矩阵中当前子矩阵的索引,并提取当前进行卷积的子矩阵 a_slice

  • 使用反向传播的链式法则,计算 da_prev_paddWdb

  • 更新 dA_prev 以将填充的部分去除,得到 dA_prev

  • 最后,函数返回关于输入数据 A_prev、权重参数 W 和偏置参数 b 的梯度,以及关于上一层的梯度 dA_prev

这个函数用于计算卷积层的反向传播,即计算关于输入数据、权重参数和偏置参数的梯度,以及返回关于上一层的梯度 dA_prev

def create_mask_from_window(x):

    # x是一个矩阵。np.max(x)会得到最大元素。
    # mask是一个与x维度相同的矩阵,里面其余元素都为0,只有x最大值元素的位置处为1
    mask = x == np.max(x)
    
    return mask

这段代码定义了一个函数 create_mask_from_window(x),用于创建一个掩码(mask)矩阵,该掩码矩阵与输入矩阵 x 的维度相同,其中除了最大元素的位置为1外,其余位置都为0。

  • def create_mask_from_window(x):: 这是函数的定义,它接受一个参数 x,该参数是一个矩阵。

  • mask = x == np.max(x): 这行代码创建了一个掩码 mask,通过比较输入矩阵 x 中的每个元素是否等于 np.max(x),即矩阵中的最大值。这将生成一个与输入矩阵 x 维度相同的布尔值矩阵,其中最大值位置为 True(1),其他位置为 False(0)。

  • 最后,函数返回生成的掩码 mask

这个函数通常用于最大池化的反向传播过程中,帮助确定哪些元素对于反向传播的梯度计算是最重要的,因为只有最大值元素的梯度会传播回原始输入。

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

这段代码测试了 create_mask_from_window 函数,用于创建一个掩码矩阵,以标记输入矩阵 x 中的最大值位置。

  1. np.random.seed(1): 这行代码设置了随机数生成器的种子为1,以确保在不同运行中生成相同的随机数。

  2. x = np.random.randn(2,3): 这行代码生成了一个随机的输入矩阵 x,其形状是 (2, 3),包含了随机的浮点数值。

  3. mask = create_mask_from_window(x): 这行代码调用了 create_mask_from_window 函数,传递输入矩阵 x 给函数,生成了一个掩码矩阵 mask,其中最大值位置为1,其他位置为0。

  4. print('x = ', x): 这行代码输出了输入矩阵 x 的值。

  5. print("mask = ", mask): 这行代码输出了生成的掩码矩阵 mask

根据生成的随机数种子和输入矩阵 x,您将看到 x 的随机值以及与之对应的掩码矩阵 mask,其中最大值位置为1,其他位置为0。

这个函数通常用于最大池化的反向传播中,以确定哪些元素对于反向传播的梯度计算是最重要的。

def distribute_value(dz, shape):
    """    
    参数:
    dz -- 一个数值
    shape -- 输出矩阵的维度
    
    返回值:
    a -- a的维度就是shape,里面的值是又dz平分而来的
    """
    (n_H, n_W) = shape
    
    # 计算平均值
    average = dz / (n_H * n_W)
    
    # 构建输出矩阵
    a = np.ones(shape) * average
    
    return a

这段代码定义了一个函数 distribute_value(dz, shape),用于将一个数值 dz 平均分配到一个指定维度的矩阵中,并返回这个矩阵。

  • def distribute_value(dz, shape):: 这是函数的定义,它接受两个参数:

    • dz:一个数值,要平均分配到输出矩阵中。
    • shape:输出矩阵的维度。
  • (n_H, n_W) = shape: 这行代码从参数 shape 中获取输出矩阵的高度 n_H 和宽度 n_W

  • average = dz / (n_H * n_W): 这行代码计算了要平均分配到每个位置的数值,通过将输入数值 dz 除以输出矩阵的总元素个数 (n_H * n_W) 得到平均值。

  • 构建输出矩阵 a,其维度与参数 shape 相同,然后将每个位置的值都设置为平均值,即 a = np.ones(shape) * average

  • 最后,函数返回构建的输出矩阵 a

这个函数通常用于平均池化的反向传播中,以将梯度平均分配回池化窗口中的所有位置。

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

这段代码测试了 distribute_value 函数,用于将数值 2 平均分配到一个 (2, 2) 维度的矩阵中。

  1. a = distribute_value(2, (2, 2)): 这行代码调用了 distribute_value 函数,将数值 2 平均分配到一个 (2, 2) 维度的矩阵中,将结果保存在变量 a 中。

  2. print('distributed value =', a): 这行代码输出了生成的矩阵 a,其中每个位置的值都是 2 除以矩阵中总元素个数 (2 * 2) 的结果,即平均值。

  3. 这表示数值 2 已经平均分配到了 (2, 2) 维度的矩阵中,每个位置的值都是 0.5。这个函数通常用于池化层的反向传播中,以将梯度平均分配回池化窗口中的所有位置 。

    def pool_backward(dA, cache, mode = "max"):
        """
        参数:
        dA -- 本池化层的输出矩阵对应的偏导数
        cache -- 前向传播时缓存起来的数值
        mode -- 是最大池化还是平均池化,("max" 或 "average")
        
        Returns:
        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(A_prev.shape)
        
        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
                        vert_end = vert_start + f
                        horiz_start = w
                        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
     

    这段代码定义了池化层的反向传播函数 pool_backward,用于计算关于输入数据 A_prev 的梯度 dA_prev,其中包括最大池化和平均池化两种模式。

    • def pool_backward(dA, cache, mode="max"):: 这是函数的定义,它接受三个参数:

      • dA:池化层的输出矩阵对应的偏导数,维度是 (m, n_H, n_W, n_C),其中 m 是样本数,n_Hn_W 分别是输出特征图的高度和宽度,n_C 是输出通道数。
      • cache:前向传播函数 pool_forward 缓存的信息,其中包含输入数据 A_prev 和超参数的信息。
      • mode:池化模式,可以是 "max"(最大池化)或 "average"(平均池化),默认为 "max"。
    • 从缓存中获取输入数据 A_prev 和超参数 hparameters

    • 从超参数中获取步幅 stride 和窗口大小 f

    • 获取输入数据 A_prev 的形状信息 (m, n_H_prev, n_W_prev, n_C_prev)

    • 获取池化层的输出梯度 dA 的形状信息 (m, n_H, n_W, n_C)

    • 初始化输出梯度 dA_prev 为与输入数据 A_prev 相同的形状,初始值为零。

    • 开始循环遍历每个样本 (m) 和输出特征图的高度和宽度 (n_H, n_W) 以及输出通道数 (n_C)。

    • 在循环内部,根据池化模式进行不同的计算:

      • 如果 mode 是 "max",则使用 create_mask_from_window 函数创建一个掩码 mask,将其与池化层的输出梯度 dA 相乘,然后加到 dA_prev 中,以计算最大池化的梯度。
      • 如果 mode 是 "average",则使用 distribute_value 函数将池化层的输出梯度 dA 平均分配到池化窗口的每个位置上,并加到 dA_prev 中,以计算平均池化的梯度。
    • 最后,函数返回计算得到的输入数据的梯度 dA_prev

    这个函数用于计算池化层的反向传播,根据池化模式(最大池化或平均池化)计算关于输入数据 A_prev 的梯度 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]) 

    这段代码测试了池化层的反向传播函数 pool_backward,包括最大池化模式和平均池化模式的测试。

    1. np.random.seed(1): 这行代码设置了随机数生成器的种子为1,以确保在不同运行中生成相同的随机数。

    2. A_prev = np.random.randn(5, 5, 3, 2): 这行代码生成了一个随机的输入数据 A_prev,其形状是 (5, 5, 3, 2),包含了随机的浮点数值。

    3. hparameters = {"stride" : 1, "f": 2}: 这行代码设置了池化层的超参数,包括步幅 stride 和窗口大小 f

    4. A, cache = pool_forward(A_prev, hparameters): 这行代码调用了池化层的前向传播函数 pool_forward,计算了池化层的输出 A 和缓存信息 cache

    5. dA = np.random.randn(5, 4, 2, 2): 这行代码生成了一个随机的池化层输出梯度 dA,其形状是 (5, 4, 2, 2),包含了随机的浮点数值。

    6. dA_prev = pool_backward(dA, cache, mode = "max"): 这行代码调用了池化层的反向传播函数 pool_backward,以最大池化模式计算了输入数据的梯度 dA_prev

    7. print("mode = max"): 这行代码输出当前测试的模式是最大池化。

    8. print('mean of dA = ', np.mean(dA)): 这行代码输出池化层输出梯度 dA 的均值。

    9. print('dA_prev[1,1] = ', dA_prev[1,1]): 这行代码输出输入数据的梯度 dA_prev 中的一个特定位置 (1, 1) 的值。

    10. dA_prev = pool_backward(dA, cache, mode = "average"): 这行代码再次调用了池化层的反向传播函数 pool_backward,以平均池化模式计算了输入数据的梯度 dA_prev

    11. print("mode = average"): 这行代码输出当前测试的模式是平均池化。

    12. print('mean of dA = ', np.mean(dA)): 这行代码再次输出池化层输出梯度 dA 的均值。

    13. print('dA_prev[1,1] = ', dA_prev[1,1]): 这行代码再次输出输入数据的梯度 dA_prev 中的一个特定位置 (1, 1) 的值。

    在每种池化模式下,您可以看到输出了池化层输出梯度的均值以及输入数据的梯度 dA_prev 中的一个特定位置的值。这个测试用例可以验证池化层的反向传播函数是否能够正确计算输入数据的梯度。