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

导入相关库

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

并且,卷积后的输出大小是:
n_H = \lfloor \frac{n_{H_{prev}} - f + 2 \times pad}{stride} \rfloor +1
n_W = \lfloor \frac{n_{W_{prev}} - f + 2 \times pad}{stride} \rfloor +1
n_C = \text{输入图像的通道}

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




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

池化层

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

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


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


池化层的前向传播

实现池化层的前向传播,池化后输出矩阵的大小如下所示:
n_H = \lfloor \frac{n_{H_{prev}} - f}{stride} \rfloor +1
n_W = \lfloor \frac{n_{W_{prev}} - f}{stride} \rfloor +1
n_C = n_{C_{prev}}

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



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

卷积神经网络的反向传播

卷积层的反向传播实现

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

计算dA

dA的计算公式如下所示,其中W_c表示的是过滤矩阵,dZ_{hw}表示的是卷积层输出的损失函数所对应的梯度,在整个计算过程中,W_c保持不变,通过迭代,计算每次过滤矩阵的覆盖的区域,最后,再将每次迭代所得到的结果累加。
dA += \sum _{h=0} ^{n_H} \sum_{w=0} ^{n_W} W_c \times dZ_{hw} \tag{1}

计算dW

计算dW的公式,如下所示,与计算dA相似,需要考虑过滤矩阵的滑动(即每次所覆盖的区域不同),最后,通过迭代计算出每一个窗口岁对应的dW,并将其累加求和。
dW_c += \sum _{h=0} ^{n_H} \sum_{w=0} ^ {n_W} a_{slice} \times dZ_{hw} \tag{2}

计算db

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

db = \sum_h \sum_w dZ_{hw} \tag{3}

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

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函数,该函数的实现效果如下所示:
X = \begin{bmatrix} 1 && 3 \\ 4 && 2 \end{bmatrix} \quad \rightarrow \quad M =\begin{bmatrix} 0 && 0 \\ 1 && 0 \end{bmatrix}\tag{4}

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

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

def create_mask_from_window(x):

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

均值池化层的反向传播

与最大池化层的mask不同,如果在前向传播函数中使用的是2×2的过率矩阵,假定dZ=1则均值池化层的mask如下所示:
dZ = 1 \quad \rightarrow \quad dZ =\begin{bmatrix} 1/4 && 1/4 \\ 1/4 && 1/4 \end{bmatrix}\tag{5}

这意味着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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358