目录
  1. 一、卷积运算
  2. 1.1 如何检测边缘
  3. 1.2 Padding
  4. 1.3 卷积步长
  5. 二、三维卷积
  6. 2.1 三维卷积基础
  7. 2.2 三维卷积在神经网络的应用
  8. 三、池化层
  9. 3.1 最大池化
  10. 3.2 平均池化
  11. 3.3 池化总结
  12. 四、全连接层
  13. 五、代码实现

一、卷积运算

前面已经提到,对于一个神经网络来说,例如人脸识别,前面的层主要用来提取图像的边缘,中间的层主要用来检测部分如眼睛鼻子嘴等,后面的层主要用来检测整个人脸。

1.1 如何检测边缘

通常使用不同的卷积核或者称过滤器来实现各种边缘检测。

(1)垂直边缘

使用如下的卷积核进行检测:

如何检测边缘

例如,对于一张具有垂直边缘的图像,利用此卷积核进行卷积运算,输出结果在边缘处数值较大,在图像变化缓慢的区域数值较小。

如何检测边缘 (2)

上面的卷积运算可以很好的计算出图像中边缘在什么位置,并且,如果卷积的结果是正值,说明从左到右是从亮到暗的变化,如果是负值,说明从左到右是从暗到亮的变化。

(2)水平边缘

如何检测边缘 (3)

此卷积核可以计算图像的水平边缘,并且正值代表上方亮下方暗,数值越大边缘变化越明显。

(3)其他滤波器

如何检测边缘 (4)

通过定义卷积核中的值的分布,可以实现检测不同方向的,不仅是水平竖直,也可以是45°75°。

此外还可以改变各行的数值,例如1:2:1,来实现更有目标的检测。

1.2 Padding

(1)普通卷积存在的问题

  • 图像缩小:对于一个(n, n)的图像,使用(f, f)的卷积核做卷积,得到的图像大小是(n-f+1, n-f+1),因此每做一次卷积,图像大小都会减小。
  • 边缘信息丢失:图像角落的像素只会被一个卷积核处理,边缘像素相比于中间的像素,也会容易丢失很多信息。

(2)解决方法

在进行卷积操作之前,先在图像边缘填充一层像素。

例如在图像边缘添加p层像素,那么卷积后图像大小是(n+2p-f+1, n+2p-f+1)。

通常情况下,卷积核边长是奇数

1.3 卷积步长

使用一个(f, f)的卷积核,卷积一个(n, n)的图像,padding为p,步长为s,输出的图像维度为(n+2pfs+1,n+2pfs+1)(\frac{n+2p-f}{s}+1, \frac{n+2p-f}{s}+1)

二、三维卷积

2.1 三维卷积基础

例如对于一个RGB图像来说,其有(w, h, 3)的维度,对其进行卷积,必须是(f, f, 3)的维度。

图像和卷积核的通道数要相同

卷积完成后的输出图像是(w-f+1, h-f+1, n)的n通道图像,其中n是卷积核的个数。

三维卷积基础

2.2 三维卷积在神经网络的应用

三维卷积在神经网络的应用,与传统方法基本一致。

在传统方法中有,z[1]=w[1]a[0]+b[1]z^{[1]}=w^{[1]}a^{[0]}+b^{[1]},其中a[0]a^{[0]}就是输入x,a[1]=g(z[1])a^{[1]}=g(z^{[1]})

而若是卷积运算,则只需将输入图片替换x,卷积核替换w,然后同样添加偏差和非线性激活函数即可。

三维卷积在神经网络的应用

三、池化层

池化层可以缩小模型大小,提高运算速度,提高模型的鲁棒性。

3.1 最大池化

例如对一个4x4的输入,最大池化选择2x2,那么输出就是将4x4的输入分成4部分,每部分取最大值填充到2x2中。最大池化输出维度的计算方法与卷积相同。

最大池化

最大池化的作用是,如果卷积过滤提取到了某个特征,那么保留其最大值,如果没有提取到特征,那么最大值也还是很小。

3.2 平均池化

平均池化与最大池化类似,是在不同区域内求平均值,主要用在很深的网络。

目前来说最大池化比平均池化更常用。

3.3 池化总结

池化的参数主要有:

  • f:池化过滤器大小
  • s:补偿
  • 最大或平均池化

由于池化层没有权重,只有上面一些超参数,因此一般来说会将卷积层和池化层合并称为一层。

四、全连接层

在神经网络的最后,经过多轮的卷积和池化处理,最后的输出往往是宽高较小,通道数较多。这是我们会将其展开成为一个向量,这个向量的长度就等于上一层输出的w×h×cw\times h\times c,将向量中的所有参数作为同样数量单元的输入,进行常规神经网络计算,这就是全连接层。

五、代码实现

(1)添加padding

def zero_pad(X, pad):
    """
    对数据集X的所有图像添加pad,padding被应用再一张图片的宽和高方向上。
    参数:
    X -- numpy数组,shape (m, n_H, n_W, n_C) 代表批量为m的图片
    pad -- 整数,图片边缘填充的pad大小
    Returns:
    X_pad -- 添加了以0填充的pad图片,shape (m, n_H + 2*pad, n_W + 2*pad, n_C)
    """

    X_pad = np.pad(X, ((0, 0), (pad, pad), (pad, pad), (0, 0)))

    return X_pad

(2)单步卷积(numpy实现)

def conv_single_step(a_slice_prev, W, b):
    """
    在上一层的一个切片用卷积核W进行卷积, 并添加偏差b
    
    参数:
    a_slice_prev -- 输入数据的切片,shape (f, f, n_C_prev)
    W -- 权重参数,以卷积核的形式体现,shape (f, f, n_C_prev)
    b -- 偏置参数,shape (1, 1, 1)

    返回值:
    Z -- 标量,滑动窗口(W, b)与输入切片x的卷积计算的结果
    """
    # a_slice 和 W 按元素相乘并添加偏置.
    S = np.multiply(W, a_slice_prev) + b
    # 求和
    Z = np.sum(S)
    
    return Z

(3)三维卷积(numpy实现)

def conv_forward(A_prev, W, b, hparameters):
    """
    卷积前向计算
    
    参数:
    A_prev -- 上一层的激活输出,shape (m, n_H_prev, n_W_prev, n_C_prev)
    W -- 权重, shape (f, f, n_C_prev, n_C)
    b -- 偏置, shape (1, 1, 1, n_C)
    hparameters -- 包含 "stride" 和 "pad" 的字典
        
    返回值:
    Z -- 卷积输出,shape (m, n_H, n_W, n_C)
    cache -- 缓存,用于conv_backward()函数
    """
    
    # 获取 A_prev 的维度  
    m, n_H_prev, n_W_prev, n_C_prev = A_prev.shape

    # 获取 W 的维度
    f, _, n_C_prev, n_C = W.shape
    
    # 获取 hparameters 中的参数
    stride = hparameters["stride"]
    pad = hparameters["pad"]

    # 计算卷积输出的维度
    n_H = (n_H_prev - f + 2 * pad) // stride + 1
    n_W = (n_W_prev - f + 2 * pad) // stride + 1
    n_C = W.shape[3]
    
    # 零初始化卷积输出变量Z
    Z = np.zeros((m, n_H, n_W, n_C))
    
    # 调用函数,填充0创建 A_prev_pad 
    A_prev_pad = zero_pad(A_prev, pad)
    
    # 在训练样本的批量中循环
    for e in range(m):
        # 选择第e个填充样本
        A_prev_pad_e = A_prev_pad[e]
        # 在输出Z的竖直方向遍历
        for h in range(n_H):
            # 在输出Z的水平方向遍历
            for w in range(n_W):
                # 遍历所有通道,通道数为卷积核个数
                for c in range(n_C):
                    # Find the corners of the current "slice" (≈4 lines)
                    vert_start = h * stride
                    vert_end = vert_start + f
                    horiz_start = w * stride
                    horiz_end = horiz_start + pad
                    
                    # 确定被卷积区域
                    A_prev_pad_slice = A_prev_pad_e[vert_start:vert_end, horiz_start:horiz_end, :]
                    
                    # 使用卷积核W和偏置b进行卷积
                    Z[e, h, w, c] = np.sum(np.multiply(A_prev_pad_slice, W[:, :, :, c]) + b[:, :, :, c])

    # 确保输出维度正确
    assert(Z.shape == (m, n_H, n_W, n_C))
    
    # 将信息保存在cache中,用于反向传播
    cache = (A_prev, W, b, hparameters)
    
    return Z, cache

(4)池化

# GRADED FUNCTION: pool_forward

def pool_forward(A_prev, hparameters, mode = "max"):
    """
    前向传播的池化层
    
    参数:
    A_prev -- 输入数据,shape (m, n_H_prev, n_W_prev, n_C_prev)
    hparameters -- 包含 "f" 和 "stride" 的python字典
    mode -- 想要使用的池化模式, 定义为字符串 ("max" or "average")
    
    返回值:
    A -- 池化层输出,shape (m, n_H, n_W, n_C)
    cache -- 保存池化层的数据,用于反向传播
    """
    
    # 获取输入数据的维度
    (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))              
                
    # 在训练样本的批量中循环
    for e in range(m):
        # 在输出Z的竖直方向遍历
        for h in range(n_H):
            # 在输出Z的水平方向遍历
            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

                    # 使用上面的角坐标定义slice区域
                    A_prev_slice = A_prev[e, vert_start:vert_end, horiz_start:horiz_end, c]
                    
                    # 根据池化模式在slice进行计算,求最大值或平均值
                    if mode == "max":
                        A[e, h, w, c] = np.max(A_prev_slice)
                    elif mode == "average":
                        A[e, 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