14 构建CNN(一)

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

这段代码看起来是用于设置环境和导入一些常用的库。

  1. import numpy as np:这行代码导入了名为NumPy的Python库,并为它创建了一个别名np,这是一种常见的惯例,使得在代码中更容易引用NumPy的函数和类。

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

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

  4. %matplotlib inline:这是一个IPython魔术命令,它用于在IPython交互式环境中内联显示图形,而不是弹出新的窗口。这可以让您在Notebook中直接看到图形输出。

  5. plt.rcParams:这里设置了Matplotlib的一些全局参数,包括图形的大小和插值方式以及颜色映射。这些参数将影响之后绘制的图形样式。

  6. %load_ext autoreload:这是另一个IPython魔术命令,用于启用自动重新加载模块的功能。当代码中的模块发生更改时,它会自动重新加载,以确保最新的代码正在运行。

  7. %autoreload 2:这个命令设置自动重新加载模式的详细级别为2,意味着模块的代码将在任何函数调用之前自动重新加载。

  8. np.random.seed(1):这行代码设置了随机数生成器的种子,这样可以使随机数生成过程具有可重复性。使用相同的种子将产生相同的随机数序列,这对于调试和复现实验结果非常有用。

这些是代码的初始化部分,用于设置环境和随机种子等。

 

实现CNN相关的一些基本函数:

  • 卷积相关的函数:
    • 零填补(Zero Padding)
    • 卷积窗口(Convolve window )
    • 前向卷积(Convolution forward)
    • 反向卷积(Convolution backward)
  • 池化相关的函数:
    • 前向池化(Pooling forward)
    • 创建掩码(Create mask )
    • Distribute value
    • 反向池化(Pooling backward)
def zero_pad(X, pad):
    """
    给样本集X的所有样本进行零填补。
    
    参数:
    X -- 样本集,维度是(m, n_H, n_W, n_C) ,
         m表示样本的数量,这里的样本是图片数据,n_H, n_W, n_C表示图片的高,宽,深度。
    pad -- 表示padding的个数,就是我们教程里说的p的数量。
    
    返回值:
    X_pad -- 返回填补后的样本集。维度是(m, n_H + 2*pad, n_W + 2*pad, n_C),每张图片的四周都填补了pad个0
    """
    
    # np.pad是numpy提供的一个零填补函数,下面代码给X的n_H和n_W这两个维度填补pad个零。对m和n_C的维度不进行填补
    # 例如第一组(pad, pad)表示给图像的上面和下面都填补pad个零。当然,上面和下面也可以填充不同数量的零。
    X_pad = np.pad(X, ((0, 0), (pad, pad), (pad, pad), (0, 0)), 'constant', constant_values=0)
    
    return X_pad

这个函数是一个用于对输入样本集进行零填充(zero padding)的函数。

  • def zero_pad(X, pad)::这是函数的定义,它有两个参数,X是输入的样本集,pad是填充的数量。

  • 函数的文档字符串(位于三重引号之间的注释)提供了函数的描述、参数说明和返回值说明。

  • np.pad(X, ((0, 0), (pad, pad), (pad, pad), (0, 0)), 'constant', constant_values=0):这是实际的填充操作。它使用了NumPy的np.pad函数来对输入的样本集X进行填充。具体细节如下:

    • X_pad 是填充后的结果。

    • ((0, 0), (pad, pad), (pad, pad), (0, 0)) 是用于定义填充方式的参数。这个参数是一个元组,包含四个元组,分别对应着样本集X的不同维度。在这里,它表示对样本数量维度不进行填充(0个填充),对高度维度和宽度维度都分别进行了pad个填充,对通道数维度也不进行填充(0个填充)。

    • 'constant' 是填充的模式,表示用常数值填充,常数值为0。

    • constant_values=0 指定了用于填充的常数值,这里是0。

  • 最后,函数返回填充后的样本集 X_pad,其维度是 (m, n_H + 2*pad, n_W + 2*pad, n_C),其中 m 是样本数量,n_Hn_W 是输入样本的高度和宽度,n_C 是输入样本的通道数。填充后,每张图片的四周都被填充了 pad 个0。

这个函数通常用于在卷积神经网络(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):这里设置了随机数生成器的种子,以确保在每次运行代码时生成相同的随机数序列。这可以帮助确保测试结果的可重复性。

  2. x 是一个随机生成的四维数组,维度为 (4, 3, 3, 2),表示了一个样本集,其中包含了4张3x3大小的彩色图像(每张图像有2个通道)。这个数组被用作测试输入。

  3. x_pad 是通过调用 zero_pad(x, 2) 函数来生成的,这将对输入样本集 x 进行2个单位的零填充。

  4. 接下来的四行代码用于打印一些信息,以展示输入数据和填充后的数据的形状以及一些具体数值。

  5. fig, axarr = plt.subplots(1, 2):这里创建了一个包含两个子图的图形对象,用于在一行中显示两个图像。

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

  7. axarr[0].imshow(x[0,:,:,0])axarr[1].imshow(x_pad[0,:,:,0]):这两行代码分别在两个子图中显示了 xx_pad 的第一个样本的第一个通道的内容。imshow 函数用于显示图像数据。

这段代码的目的是测试 zero_pad 函数是否能够正确地对输入数据进行零填充,并且在输出结果上显示填充前后的图像。如果一切正常,您应该能够看到两幅图像,左侧是原始输入 x,右侧是填充后的 x_pad

def conv_single_step(a_slice_prev, W, b):
    """
    这个函数只执行一步卷积
    
    参数:
    a_slice_prev -- 输入矩阵中的一小块数据,如上面的动图所示,过滤器每次只与矩阵中的一小块数据进行卷积。
                 -- 这里的输入矩阵也就是上一层的输出矩阵。维度是(f, f, n_C_prev)
    W -- 权重参数w。其实这里就是指过滤器。过滤器就是权重参数w。
      -- 维度是(f, f, n_C_prev),与a_slice_prev是一样的。因为是它俩进行卷积,所以维度肯定是一样的。
    b -- 阈值b,教程中我们说过每一个过滤器会有一个对应的阈值。 维度是(1, 1, 1)
    
    返回值:
    Z -- 卷积一步后得到的一个数值。这个数值将是输出矩阵中的一个元素。
    """

    # 将a_slice_prev与W的每一个元素进行相乘
    s = np.multiply(a_slice_prev, W) + b
    # 将上面相乘的结果累加起来
    Z = np.sum(s)

    return Z

这个函数实现了卷积神经网络 (CNN) 中的一步卷积操作。

  • 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:将阈值 b 加到上述相乘结果上。每个卷积滤波器都有一个对应的阈值。

  • Z = np.sum(s):这行代码计算了所有相乘和加权的结果的总和,得到卷积一步后的一个数值 Z。这个数值将成为输出矩阵中的一个元素。

最后,函数返回了计算得到的卷积结果 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)

这段代码用于测试 conv_single_step 函数,对一个输入切片 a_slice_prev 和一个权重参数 W 进行一步卷积操作,并打印卷积的结果 Z

  1. np.random.seed(1):这里设置了随机数生成器的种子,以确保在每次运行代码时生成相同的随机数序列,以保持结果的可重复性。

  2. a_slice_prev 是一个随机生成的三维数组,维度为 (4, 4, 3),表示一个输入切片,其中包含4x4大小的数据块,每个数据块有3个通道。这个数组是卷积操作的输入。

  3. W 是一个随机生成的三维数组,维度也是 (4, 4, 3),表示卷积操作中的权重参数(过滤器),它与输入切片 a_slice_prev 具有相同的维度。

  4. b 是一个随机生成的三维数组,维度为 (1, 1, 1),表示卷积操作中的阈值(偏置)。

  5. Z = conv_single_step(a_slice_prev, W, b):这一行调用了 conv_single_step 函数,对输入切片 a_slice_prev 和权重参数 W 进行一步卷积操作,并将结果存储在变量 Z 中。

  6. print("Z =", Z):最后,打印卷积操作的结果 Z

这个代码块测试了 conv_single_step 函数是否能够正确地执行一步卷积操作。如果一切正常,您应该能够看到卷积操作的结果 Z 的值被打印出来。

def conv_forward(A_prev, W, b, hparameters):
    """
    实现卷积网络的前向传播
    
    参数:
    A_prev -- 本层的输入矩阵,也就是上一层的输出矩阵。维度是(m, n_H_prev, n_W_prev, n_C_prev)
    W -- 权重,也就是过滤器。维度是 (f, f, n_C_prev, n_C)。后面的n_C表示过滤器的个数
    b -- 阈值。维度是 (1, 1, 1, n_C)。一个过滤器配一个阈值。所以最后一维也是n_C
    hparameters -- 超参数步长s和padding数p
        
    返回值:
    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'] # 步长s
    pad = hparameters['pad'] # 填补数量p
    
    # 计算输出矩阵的维度。参考上面提供的公式    
    n_H = int((n_H_prev - f + 2 * pad) / stride) + 1 # 使用int()来实现向下取整
    n_W = int((n_W_prev - f + 2 * pad) / stride) + 1
    
    # 初始化输出矩阵
    Z = np.zeros((m, n_H, n_W, n_C))
    
    # 给输入矩阵进行padding填补0
    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, :]
                    # 利用之前我们实现的conv_single_step函数来对这块数据进行卷积。
                    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

这个函数实现了卷积神经网络 (CNN) 的前向传播操作,用于计算卷积层的输出。

  • def conv_forward(A_prev, W, b, hparameters)::这是函数的定义,它接受四个参数,分别是上一层的输出 A_prev、权重参数 W、阈值 b 以及包含超参数的字典 hparameters

  • 函数的文档字符串提供了函数的描述、参数说明和返回值说明。

  • (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 中获取了步长 (stride) 和填充数量 (pad)。

  • 计算输出矩阵的高度 n_H 和宽度 n_W,这是根据输入矩阵大小、过滤器大小、步长和填充数量计算得出的。

  • 初始化输出矩阵 Z,它的维度是 (m, n_H, n_W, n_C),即样本数量 m、输出高度 n_H、输出宽度 n_W 和卷积层的过滤器个数 n_C

  • A_prev_pad = zero_pad(A_prev, pad):这行代码使用之前定义的 zero_pad 函数对输入矩阵 A_prev 进行填充。

  • 接下来的嵌套循环用于遍历每个样本、输出矩阵的高度和宽度、以及卷积层的过滤器。在每个循环迭代中,执行以下操作:

    • 计算出输入矩阵中本次应该卷积的区域的索引,然后通过这些索引取出将被卷积的小块数据。

    • 利用之前实现的 conv_single_step 函数来对这块数据进行卷积操作,并将结果存储在输出矩阵 Z 的相应位置。

  • assert(Z.shape == (m, n_H, n_W, n_C)):这行代码用于确保输出矩阵 Z 的形状与预期的形状相匹配。

  • 最后,将一些数值缓存到 cache 中,以便在反向传播时使用。

  • 函数返回输出矩阵 Z 和缓存 cache

这个函数实现了卷积层的前向传播操作,它将输入矩阵 A_prev 与权重参数 W 进行卷积,并添加阈值 b,最终生成输出矩阵 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):这里设置了随机数生成器的种子,以确保在每次运行代码时生成相同的随机数序列,以保持结果的可重复性。

  2. A_prev 是一个随机生成的四维数组,维度为 (10, 4, 4, 3),表示一个包含10个样本的输入矩阵,每个样本是一个4x4大小的数据块,具有3个通道。

  3. W 是一个随机生成的四维数组,维度为 (2, 2, 3, 8),表示卷积层的权重参数,其中 2x2 是过滤器的大小,3 是输入通道数,8 是卷积层的过滤器个数。

  4. b 是一个随机生成的四维数组,维度为 (1, 1, 1, 8),表示卷积层的阈值(偏置),每个过滤器都有一个对应的阈值。

  5. hparameters 是包含超参数的字典,其中包括填充数量 pad 和步长 stride

  6. Z, cache_conv = conv_forward(A_prev, W, b, hparameters):这一行调用了 conv_forward 函数,执行卷积层的前向传播操作,并将输出矩阵 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 中的一个特定值,以检查是否正确存储了缓存。

这段代码块测试了 conv_forward 函数是否能够正确地执行卷积操作,并将结果存储在 Z 中,同时也存储了缓存值以备后用。