一步步实现一个卷积神经网络

导入相关库

利用python实现一个简单的神经网络之前,需要先导入相关库,具体代码实现如下所示:


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实现一个简单的卷积神经网络时,首先需要了解整个实现过程的概况,具体的实现步骤,如下所示,通过这些步骤,一步步实现一个卷积神经网络的过程包含一个卷积层和池化层,卷积层和池化层需要实现的函数如下所示:

  • 卷积层
    • 零值填充
    • 卷积窗口
    • 卷积的前向传播
    • 卷积的反向传播和优化
  • 池化层
    • 池化层的前向传播
    • 创建池化的池化函数
    • 提取池化值
    • 池化层的反向传播

整个卷积神经网络的架构如下图所示:


卷积化的神经元网络

尽管利用编程框架使用卷积是非常容易实现的,但是,实现卷积神经网咯中需要注意的一点就是,通过卷积,其输出和输入的尺寸会发生变化,如下图所示:


零值填充

使用卷积神经网络的第一步是实现零值填充,一个RGB3通道的图像,零值填充的具体的实现过程,如下图所示:

使用零值填充的主要有以下益处:

  • 能够保证卷积后的输出的图像尺寸和输入保持一致。
  • 能够有助于保留图像的边缘信息。

numpy中,可以直接使用numpy提供的函数np.pad()来实现填充,关于,这个函数,其参数的解释如下所示:
假设有一个shape=(5,5,5,5,5)的五维矩阵,如果利用np.pad()实现填充对地2维填充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_pad = np.pad(X, ((0, 0),(pad, pad),(pad, pad),(0, 0)), 'constant', constant_values=0)
    return X_pad

以上代码中,输入参数x的尺寸是3通道图像的大小,为(m,n_H,n_W,n_C)
而输出表示的是实现零值填充之后的输入图像的尺寸,为(m, n_H + 2*pad, n_W + 2*pad, n_C)

随机生成一个矩阵,进行零值填充之后,如下所示:


卷积的实现

卷积的实现过程,可以分为以下三个步骤

  • 依次选取输入
  • 对选定的部分利用过滤矩阵实现卷积运算
  • 得到卷积后的输入重新组成的矩阵

根据以上所述,卷积的实现代码如所示

def conv_single_step(a_slice_prev, W, b):
    """
对前一层网络上的激活值的输出进行滑动切片,由参数W和b组成的过滤矩阵
    
    参数
    a_slice_prev -- 输入数据的切片,其尺寸是(f,f,前一层网络的输出)
    W -- 权重参数 -窗口矩阵的形状是 (f, f, n_C_prev)
    b --偏差参数 - 窗口矩阵的形状是 (1, 1, 1)
    
    返回值:
    Z -- 标量值,是将滑动窗口(W,b)卷积在输入数据的切片x上的结果
    """
   #利用参数相乘
    s = np.multiply(a_slice_prev, W) + b
    #求和并作为输出
    Z = np.sum(s)
    return Z

卷积神经网络的前向传播

卷积神经网络的前向传播过程中,需要使用多个过滤矩阵,分别利用不同的过滤矩阵对输入进行滑动卷积,卷积完成之后,将输出值组合在一起。

为了实现滑动窗口切片,可以利用python中的切片来实现,并且为了能够实现滑动的功能,可以定义四个变量表示滑动的方向,这四个变量分别表示横轴和纵轴的起始点和终止点,具体实现如下所示:

并且,卷积后的输出大小是:


根据以上所述,卷积的前向传播实现的代码如下所示:




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)
    b -偏置参数,其尺寸大小是 (1, 1, 1, n_C)
    hparameters -- 是一个python字典,包含了卷积步长和填充大小
        
    返回值:
    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']
    
    # 计算卷积后的输出大小,并且利用int()实现向下取整
    n_H = 1 + int((n_H_prev + 2 * pad - f) / stride)
    n_W = 1 + int((n_W_prev + 2 * pad - f) / stride)
    
    # 初始换输出值为0
    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]        # 选取第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] = np.sum(np.multiply(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

池化层

池化层减少了输入矩阵的高和宽,减少了计算的复杂度,并且有助于特征检测器在更加有效的检测输入特征,池化层根据其池化方式,可以分为以下两类:

  • 最大池化: 选择窗口矩阵覆盖区域的最大值作为输出


  • 均值池化: 选择窗口矩阵覆盖区域的所有元素的平均值作为输出


池化层的前向传播

实现池化层的前向传播,池化后输出矩阵的大小如下所示:


根据以上,则池化层的前向传播的代码实现如下所示:



def pool_forward(A_prev, hparameters, mode = "max"):
    """
   实现池化层的前向传播
    参数:
    A_prev -- 输入矩阵,即前一层的输出矩阵 其矩阵大小为(m, n_H_prev, n_W_prev, n_C_prev)
    hparameters -- 字典参数,存储着步长参数和过滤矩阵的大小
    mode -- 池化的模式,如“最大池化”和“均值池化”
    
    返回:
    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):            # 图像通道的循环
                    
                    # 过滤矩阵的当前所覆盖的滑动窗口 "slice" (≈4 lines)
                    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

卷积神经网络的反向传播

卷积层的反向传播实现

卷积层的反向传播实现,大概分为以下几个步骤

计算

的计算公式如下所示,其中表示的是过滤矩阵,表示的是卷积层输出的损失函数所对应的梯度,在整个计算过程中,保持不变,通过迭代,计算每次过滤矩阵的覆盖的区域,最后,再将每次迭代所得到的结果累加。

计算

计算的公式,如下所示,与计算相似,需要考虑过滤矩阵的滑动(即每次所覆盖的区域不同),最后,通过迭代计算出每一个窗口岁对应的,并将其累加求和。

计算

偏置参数求导的计算公式如下所示,正如公式所示,整个计算过程就是,将每次卷积输出损失的梯度累加即可。

综上所述,则整个卷积层的反向传播的计算如下代码所示:

def conv_backward(dZ, cache):
    """
    实现卷积层的反向传播
    
    参数:
    dZ -- 卷积输出损失所对应的梯度,其大小是(m, n_H, n_W, n_C)
    cache -- 反向传播所需要的一些参数,是由前向传播的计算得到的
    
    返回值:
    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 and 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_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[i, :, :, :] = dA_prev_pad[i, pad:-pad, pad:-pad, :]

    assert(dA_prev.shape == (m, n_H_prev, n_W_prev, n_C_prev))
    
    return dA_prev, dW, db


最大池化层的反向传播

在实现最大池化层的反向传播之前,需要创建一个creat_mask_from_window函数,该函数的实现效果如下所示:

如上所示,该函数实现的效果就是创建一个与原矩阵相同大小的矩阵,并根据原矩阵所对应位置,将该矩阵对应位置上的值设置为1,其余值设置为0,其代码实现如下所示:

综上所述,其代码实现如下所示:

def create_mask_from_window(x):

    mask = (x == np.max(x))
    return mask

均值池化层的反向传播

与最大池化层的mask不同,如果在前向传播函数中使用的是2×2的过率矩阵,假定则均值池化层的mask如下所示:

这意味着dZ矩阵中的每个位置对输出的等价,因为在前向传递中,我们取了平均值。

综上所数,均值mask的实现代码如下所示:


def distribute_value(dz, shape):
    (n_H, n_W) = shape
    average = dz / (n_H * n_W)
    a = np.ones(shape) * average
    return a

池化层的反向传播

综上,池化层的反向传播的实现代码如下所示:



def pool_backward(dA, cache, mode = "max"):
    """
    池化层的反向传播的实现
    
   参数:
    dA -- 池化层输出所对应的损失的梯度
    cache -池化层前向传播所保留的一些参数
    mode -- 池化层的模式,最大池化或者均值池化
    
    返回值:
    dA_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] += mask * dA[i, vert_start, horiz_start, c]
                        
                    elif mode == "average":
                        
                       
                        da = dA[i, vert_start, horiz_start, 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

你可能感兴趣的:(一步步实现一个卷积神经网络)