AI 学习之卷积神经网络 (CNN)

一、基本概念

卷积神经网络(Convolutional Neural Network,CNN)是一种前馈神经网络,它的人工神经元可以响应一部分覆盖范围内的周围单元,对于大型图像处理有出色表现。 它包括卷积层(convolutional layer)和池化层(pooling layer)。

二、创建虚拟环境

我们使用Conda集成环境,

创建一个新的环境,使用python=3.6
conda create -n deep-learning python=3.6
source activate deep-learning

启动jupty:
source ~/.bash_profile  重新加载
jupyter notebook

三、公式

简单公式
图像的维度(n,n), 过滤器的维度(f,f),那么结果的矩阵维度为(n-f+1,n-f+1)。

(n-f+1,n-f+1)

Padding填充
填充的元素维度我们可以用 p 来表示,因为我们只填充了 1 个元素,所以这里的 p=1。
公式中为什么要乘以一个 2 呢?因为图像左右上下都填充了一个元素。

(n+2p-f+1, n+2p-f+1)

卷积步长

[((n+2p-f) / s) + 1]  x  [((n+2p-f) / s) + 1] 

说明:n、p、f 分别表示输入矩阵的维度、padding数量、过滤器的维度。s 表示的就是卷积步长。[] 表示向下取整,例如[5.7]的结果就是 5。

多过滤器

[((n+2p-f) / s) + 1]  x  [((n+2p-f) / s) + 1]  x m

说明:n、p、f 分别表示输入矩阵的维度、padding数量、过滤器的维度。s 表示的就是卷积步长,m表示过滤器数量。[] 表示向下取整,例如[5.7]的结果就是 5。

上边我们使用了一个过滤器与输入矩阵进行卷积运算,其实是可以同时使用多个过滤器的。如果我们可以同时检测输入图像的垂直边缘和水平边缘,那么输入图像可以同时与两个过滤器进行卷积运算。

假设输入图像是 6 x 6 x 3, 垂直过滤器为 3 x 3 x 3, 水平过滤器为3 x 3 x 3,输入图像与每个过滤器进行卷积运算之后都会产生一个相应的 4x4 矩阵。最后我们可以将这两个 4 x 4 的矩阵,合成为一个三维的 4x4x2 的矩阵。

最后我们来看下,多过滤器时,输出的矩阵。假设输入的矩阵宽高都是n,过滤器的宽高都是f,过滤器的数量我们这里用m 来表示,最终输出矩阵的维度等于 (n-f+1) x (n-f+1) x m = (6-3+1) x (6-3+1) x 2 = 4x4x2。这个例子暂不考虑padding和步长。

问题:
输入图像是 37 x 37 x 10, 用 20 个 5x5 的过滤器来与输入图像进行卷积, 步长为 2,padding为0,那么输出的结果是多少?

解答:((37 + 2x0 - 5) / 2 + 1) x ((37 + 2x0 - 5) / 2 + 1) x 20 = 17 x 17 x 20;

池化

[((n+2p-f) / s) + 1]

池化是找里边最大的值,池化操作中的矩阵维度和卷积操作时的计算公式是一样的。当然一般来说,不会用到 padding, 所以,p是0。

四、实战

手把手教你构建CNN(一)

本次我们将用numpy库来实现卷积层和池化层,以及前向传播和反向传播。

提示:

  • 上标 $[l]$ 表示属于第几层。

    • 例如: $a^{[4]}$ 表示第4层的激活值。 $W^{[5]}$ 和 $b^{[5]}$ 表示第5层的参数。
  • 上标 $(i)$ 表示第i个样本。

    • 例如: $x^{(i)}$表示第i个样本的输入特征。
  • 下标 $i$ 表示向量的第i个元素。

    • 例如: $a^{[l]}_i$ 表示第l层的第i个激活值。
  • $n_H$, $n_W$ 和 $n_C$ 表示某一层相关矩阵的高,宽以及深度。如果你想指明第l层相关矩阵的高,宽和深度,那么你可以写成如下形式 $n_H^{[l]}$, $n_W^{[l]}$, $n_C^{[l]}$.

  • $n{H{prev}}$, $n{W{prev}}$ 和 $n{C{prev}}$ 表示前一层相关矩阵的高宽和深度。当然,如果你想表示l层前面一层的高宽深度,也可以写成如下形式 $n_H^{[l-1]} $, $n_W^{[l-1]}$, $ n_C^{[l-1]}$。

1 - 导入工具库

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)

2 - 概述

在本次实战编程中我们将学会如何实现CNN相关的一些基本函数:

  • 卷积相关的函数:
    • 零填补(Zero Padding)
    • 卷积窗口(Convolve window )
    • 前向卷积(Convolution forward)
    • 反向卷积(Convolution backward)
  • 池化相关的函数:
    • 前向池化(Pooling forward)
    • 创建掩码(Create mask )
    • Distribute value
    • 反向池化(Pooling backward)

本次实战编程我们会使用numpy库来手工实现这些函数。下一个实战编程我们会用TensorFlow,当然,TensorFlow中已经帮我们实现了这些函数,所以,下一个实战编程的重点在于调用这些函数来构建出下面的CNN。

4dSzSNzQPt.png

3 - 卷积层

卷积层会把一个矩阵转化成另一个不同尺寸的矩阵,如下图所示

0bIuD1TQpm.png

下面我们就来一步一步地实现卷积层。

3.1 - 零填补(Zero-Padding)

零填补就是往输入矩阵四周添加0,如下图所示:

FoAdhYEUid.png

padding的作用如下:

  • 它可以让我们既使用卷积层又可以避免矩阵的尺寸变得越来越小。对于很深的神经网络来说意义很大,因为如果不使用padding,那么每卷积一次矩阵尺寸就会变小,这样一来等到很深的网络层时,矩阵就没啦~

  • 它有助于保留图像四周的信息。因为如果没有padding,那么图像四周的元素被卷积的次数就很少。

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
# 测试
test = np.random.randn(2,4,4,3)
print(test)
[[[[ 1.62434536 -0.61175641 -0.52817175]
   [-1.07296862  0.86540763 -2.3015387 ]
   [ 1.74481176 -0.7612069   0.3190391 ]
   [-0.24937038  1.46210794 -2.06014071]]

  [[-0.3224172  -0.38405435  1.13376944]
   [-1.09989127 -0.17242821 -0.87785842]
   [ 0.04221375  0.58281521 -1.10061918]
   [ 1.14472371  0.90159072  0.50249434]]

  [[ 0.90085595 -0.68372786 -0.12289023]
   [-0.93576943 -0.26788808  0.53035547]
   [-0.69166075 -0.39675353 -0.6871727 ]
   [-0.84520564 -0.67124613 -0.0126646 ]]

  [[-1.11731035  0.2344157   1.65980218]
   [ 0.74204416 -0.19183555 -0.88762896]
   [-0.74715829  1.6924546   0.05080775]
   [-0.63699565  0.19091548  2.10025514]]]

 [[[ 0.12015895  0.61720311  0.30017032]
   [-0.35224985 -1.1425182  -0.34934272]
   [-0.20889423  0.58662319  0.83898341]
   [ 0.93110208  0.28558733  0.88514116]]

  [[-0.75439794  1.25286816  0.51292982]
   [-0.29809284  0.48851815 -0.07557171]
   [ 1.13162939  1.51981682  2.18557541]
   [-1.39649634 -1.44411381 -0.50446586]]

  [[ 0.16003707  0.87616892  0.31563495]
   [-2.02220122 -0.30620401  0.82797464]
   [ 0.23009474  0.76201118 -0.22232814]
   [-0.20075807  0.18656139  0.41005165]]

  [[ 0.19829972  0.11900865 -0.67066229]
   [ 0.37756379  0.12182127  1.12948391]
   [ 1.19891788  0.18515642 -0.37528495]
   [-0.63873041  0.42349435  0.07734007]]]]
# 获取一个(5,5,3)矩阵中的左上角的一个2x2的子矩阵
test = np.random.randn(5,5,3)
print(test)

a_slice_prev = test[0:2,0:2,:]
print("2x2子矩阵:\n")
print(a_slice_prev)
[[[-0.34385368  0.04359686 -0.62000084]
  [ 0.69803203 -0.44712856  1.2245077 ]
  [ 0.40349164  0.59357852 -1.09491185]
  [ 0.16938243  0.74055645 -0.9537006 ]
  [-0.26621851  0.03261455 -1.37311732]]

 [[ 0.31515939  0.84616065 -0.85951594]
  [ 0.35054598 -1.31228341 -0.03869551]
  [-1.61577235  1.12141771  0.40890054]
  [-0.02461696 -0.77516162  1.27375593]
  [ 1.96710175 -1.85798186  1.23616403]]

 [[ 1.62765075  0.3380117  -1.19926803]
  [ 0.86334532 -0.1809203  -0.60392063]
  [-1.23005814  0.5505375   0.79280687]
  [-0.62353073  0.52057634 -1.14434139]
  [ 0.80186103  0.0465673  -0.18656977]]

 [[-0.10174587  0.86888616  0.75041164]
  [ 0.52946532  0.13770121  0.07782113]
  [ 0.61838026  0.23249456  0.68255141]
  [-0.31011677 -2.43483776  1.0388246 ]
  [ 2.18697965  0.44136444 -0.10015523]]

 [[-0.13644474 -0.11905419  0.01740941]
  [-1.12201873 -0.51709446 -0.99702683]
  [ 0.24879916 -0.29664115  0.49521132]
  [-0.17470316  0.98633519  0.2135339 ]
  [ 2.19069973 -1.89636092 -0.64691669]]]
2x2子矩阵:

[[[-0.34385368  0.04359686 -0.62000084]
  [ 0.69803203 -0.44712856  1.2245077 ]]

 [[ 0.31515939  0.84616065 -0.85951594]
  [ 0.35054598 -1.31228341 -0.03869551]]]
# 单元测试
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])
x.shape = (4, 3, 3, 2)
x_pad.shape = (4, 7, 7, 2)
x[1, 1] = [[ 0.90085595 -0.68372786]
 [-0.12289023 -0.93576943]
 [-0.26788808  0.53035547]]
x_pad[1, 1] = [[0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]]

<matplotlib.image.AxesImage at 0x119ba2a58>

F6xodUTBRe.png

3.2 - 单步卷积

下面我们实现单步卷积函数。这个函数只卷积一步。后面我们将调用这个函数来完成对整个输入矩阵的卷积操作。如下图所示,卷积层会对输入矩阵进行很多步卷积。每卷积一步就会得出一个输出矩阵的元素。

T0E2BWwDr6.png

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

    print()
    print("s=:", s)

    # 将上面相乘的结果累加起来
    Z = np.sum(s)

    return Z
np.random.seed(1)
a_slice_prev = np.random.randn(4, 4, 3)
print(a_slice_prev)

W = np.random.randn(4, 4, 3)
b = np.random.randn(1, 1, 1)
print(b)

Z = conv_single_step(a_slice_prev, W, b)
print("Z =", Z)
[[[ 1.62434536 -0.61175641 -0.52817175]
  [-1.07296862  0.86540763 -2.3015387 ]
  [ 1.74481176 -0.7612069   0.3190391 ]
  [-0.24937038  1.46210794 -2.06014071]]

 [[-0.3224172  -0.38405435  1.13376944]
  [-1.09989127 -0.17242821 -0.87785842]
  [ 0.04221375  0.58281521 -1.10061918]
  [ 1.14472371  0.90159072  0.50249434]]

 [[ 0.90085595 -0.68372786 -0.12289023]
  [-0.93576943 -0.26788808  0.53035547]
  [-0.69166075 -0.39675353 -0.6871727 ]
  [-0.84520564 -0.67124613 -0.0126646 ]]

 [[-1.11731035  0.2344157   1.65980218]
  [ 0.74204416 -0.19183555 -0.88762896]
  [-0.74715829  1.6924546   0.05080775]
  [-0.63699565  0.19091548  2.10025514]]]
[[[-0.34385368]]]

s=: [[[-0.14867404 -0.72143164 -0.50239516]
  [ 0.03409936 -1.33259764  0.46017212]
  [-0.70833479 -0.7903953  -0.07618517]
  [-0.57604295  0.07370582 -2.16736902]]

 [[-0.1006228  -0.82502315  0.23769048]
  [-0.01598397 -0.42808798 -0.27751241]
  [-0.29608336  0.54191869 -2.74933988]
  [-1.94245614 -1.64585328 -0.59734492]]
Z = -23.16021220252078

3.3 - 前向传播

我们可以使用多个过滤器。每个过滤器卷积之后都会得到一个二维的矩阵。然后我们会将这些二维的矩阵叠加起来成为多维的矩阵。如下面的短视频所示。视频中有2个过滤器。

提示:

  1. 在python中,通过开始索引和结束索引,可以获取数组中某一区域的元素。例如,如果你想获取一个(5,5,3)矩阵中的左上角的一个2x2的子矩阵,那么你可以使用下面的代码:
    a_slice_prev = a_prev[0:2,0:2,:]

    在下面的函数中,我们会用这一语法来从输入矩阵中获取一小块数据来与过滤器进行卷积。

  2. 为了从矩阵中获取一个子矩阵(一小块数据),我们首先需要计算出子矩阵在母矩阵中的坐标,纵向开始索引vert_start, 纵向结束索引vert_end, 横向开始索引horiz_start 和横向结束索引 horiz_end。然后就可以用这些索引来定位一个子矩阵,如下图所示。

LZL7fWW57G.png

函数中还会用到如下公式来计算输出矩阵的维度:

$$nH = \lfloor \frac{n{H_{prev}} - f + 2 \times pad}{stride} \rfloor +1$$

$$ nW = \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)。后面的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
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])
s=: [[[0.37245685 0.37245685 0.37245685]
  [0.37245685 0.37245685 0.37245685]]

 [[0.37245685 0.37245685 0.37245685]
  [0.37245685 0.37245685 0.37245685]]]

s=: [[[-0.1484898 -0.1484898 -0.1484898]
  [-0.1484898 -0.1484898 -0.1484898]]

 [[-0.1484898 -0.1484898 -0.1484898]
  [-0.1484898 -0.1484898 -0.1484898]]]

s=: [[[-0.1834002 -0.1834002 -0.1834002]
  [-0.1834002 -0.1834002 -0.1834002]]

 [[-0.1834002 -0.1834002 -0.1834002]
  [-0.1834002 -0.1834002 -0.1834002]]]

s=: [[[1.1010002 1.1010002 1.1010002]
  [1.1010002 1.1010002 1.1010002]]

 [[1.1010002 1.1010002 1.1010002]
  [1.1010002 1.1010002 1.1010002]]]
...
     [[-0.06741002 -0.06741002 -0.06741002]
  [-0.06741002 -0.06741002 -0.06741002]]]
Z's mean = 0.15585932488906465
cache_conv[0][1][2][3] = [-0.20075807  0.18656139  0.41005165]

继续学习如何实现池化层和反向传播!

4 - 池化层

池化层可以将矩阵的尺寸变小。池化层不仅仅可以降低神经网络的计算量,还可以让网络的预测鲁棒性更好。

之前我给大家介绍了两种池化层:

  • 最大池化层(Max-pooling layer): 取输入矩阵的子矩阵中的最大值元素作为输出矩阵的一个元素。

  • 平均池化层(Average-pooling layer): 取输入矩阵的子矩阵中的元素的平均值作为输出矩阵的一个元素。“子矩阵”在这里也被叫做“窗口”。

76CDOZ2LnO.pngog0QJPfaU7.png

池化层是没有参数的,因为它的过滤器是虚拟的是不存在的。但是它有超参数,例如窗口大小f,也就是指定池化的子矩阵的高宽。

4.1 - 池化的前向传播

下面我们实现了一个池化的前向传播函数,它可以指定用最大池化还是平均池化。

提示:
因为池化没有padding,所以计算输出矩阵的维度公式会与卷积层的有所不同:

$$ nH = \lfloor \frac{n{H_{prev}} - f}{stride} \rfloor +1 $$
$$ nW = \lfloor \frac{n{W_{prev}} - f}{stride} \rfloor +1 $$
$$ nC = n{C_{prev}}$$

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
# 单元测试
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
A = [[[[1.74481176 1.6924546  2.10025514]]]

 [[[1.19891788 1.51981682 2.18557541]]]]

mode = average
A = [[[[-0.09498456  0.11180064 -0.14263511]]]

 [[[-0.09525108  0.28325018  0.33035185]]]]

至此,我们已经实现了CNN的所有前向传播操作。


参考文章:
一文让你彻底了解卷积神经网络

为者常成,行者常至