卷积神经网络透彻解析和实际应用

今天我们学习图像处理中最常用的卷积神经网络。

卷积神经网络

图像处理中,往往把图像表示为像素的向量,比如一个 1000×10001000×1000 的图像,可以表示为一个 106106 的向量,如在 MNIST 手写字神经网络中,输入层为 28×28=78428×28=784 维的向量。

如果隐含层的节点个数与输入层一样,即也是 106106 时,那么输入层到隐含层的参数数据为 106×106=1012106×106=1012,参数个数暴多,要想在正常的时间内训练完,基本是不可能的。

所以要想处理 1000×10001000×1000 的图像分类,就得首先想办法减少参数的个数,也就是只基于普通全连接的深度神经网络(DNN)已经很难训练,有没有更加优秀的算法可以专门处理这种图像分类呢?

这就是卷积神经网络,Convolutional Neural Network ,简称为 CNN。

图像理论预备知识

一副图像在计算机中是如何表达的?一幅单通道图像,可以表示为二维,也就是一个二维的矩阵,空白的地方取值较小,越是颜色黑的区域,矩阵对应的色素值越大。

通道是一幅图像的特定组成部分,常见的手机拍出来的图片会有 3 个通道:红色、绿色、蓝色(RGB),也就是三通道,我们可以把它看作为 3 个二维的数组,每一个二维数组代表一种颜色值,像素值在 0~255 之间。对于灰色图(grayscale),比较特殊,它仅有一个通道,如上图所示的手写字数据集中的手写字 8,它就是由一个通道组成。

DNN 到 CNN 做的改变

一般地,如下图所示为全连接的深度神经网络(DNN),每层的每个神经元节点与前层的所有神经元节点有连接,也会与后一层的所有节点相连接,这样导致的问题是每个节点都有很多个权重参数和偏置量,刚才在上文中我们提到过,那么卷积神经网络想要做的第一件事,想办法解决参数过多的问题。

CNN 有几种措施可以降低参数的数目,主要介绍两种:

  • 第一种是局部连接(local connection),也称为局部感知
  • 另一种是权值共享(weight sharing)

局部连接

根据图像其局部的像素联系较为紧密,距离较远的像素相关性较弱,这一合理的假设,CNN 认为每个神经元没有必要对整个的全局图像进行感知,只需要对局部进行感知,然后接下来的隐含层中再对局部的信息综合起来,这样就提取成一个新的保留原来主要特征的图像。

局部连接对权重参数的减少力度大吗?我们来计算下,文章开始说到一个如果采用 DNN,那么权重参数为 10121012 个,假如采取局部连接,定义隐含层的每个神经元只与输入层的 100100 个像素建立关系,也就是说共有 106×100106×100 个权重参数,108108 个,这个参数量还是不小吧,所以需要第二种措施——权值共享。

权值共享

只减少隐含层的节点关联的输入层的像素点,对参数的减少力度一般,那么,在这基础上还能做些什么?

如果我们再做这么一个假设:从紧邻的 100 个像素点抽取出一小块,并已知这一块的每个像素点的权重参数,假定这一块的权重参数也会被100个像素点的权重参数被其他块所共享,这就是权值共享。

抽取的那一小块对应的权重参数为:kernel(也可称为 filter, feature detector),并且往下隐含层网络可以继续使用这种卷积核,这样图像的特征会随着隐含层的加深,而逐渐变得抽象起来。

CNN借助以上两种措施对权重参数做减法,并且结合这两种措施,起名为卷积操作,并且将这种深度学习算法称为卷积神经网络算法。

总结来说,DNN 中是节点与前后层是全连接的,而 CNN 算法对节点启用局部连接和权重参数共享的措施,以此减少权重参数,加快学习训练和收敛速度,使得用神经网络模型对图像进行分类操作成为可能。

卷积操作

单核卷积

在卷积操作中涉及到一种特殊的操作,叫做求内积。此操作过程为先让两个同型矩阵对应的元素相乘,然后再求和。具体说来如下:

A 和 B 求内积:

1×3+0×1+2×1+3×5=201×3+0×1+2×1+3×5=20

这就是两个矩阵求内积得到的结果。

接下来解释,如何用一个指定大小的卷积核,做卷积操作。

为了演示的方便,直接使用一个 5×55×5 的图像块:

使用 3×33×3 的卷积核如何提取特征,使用如下 3×33×3 的卷积核:

如下图所示卷积核与 5×55×5 的图像块第一次做内积后得到 4, 放在卷积后的矩阵中的第一元素中。

第二步,移动卷积核,移动的步幅大小称为步长(stride),此处移动步长取为 1,这就是 CNN 中的一个重要超参数,移动 1个步长和卷积操作后得到 3,再放入结果中,如下图所示:

这样依次移动 9 步,最后的卷积结果如下图所示,是一个 3×33×3 矩阵:

多核卷积

上面介绍使用单个卷积核做卷积操作,下面介绍介绍使用多核卷积操作。

分享一个多核卷积操作的动画,输入为 7×7×37×7×3,使用一层零填充(Zero-padding),它是 CNN 网络中另一个重要超参数。

使用两个过滤核:W0W0 和 W1W1,在 CNN 中称为深度(Depth),是 CNN 三个超参数的最后一个,分别使用 2 个过滤核 W0W0 和 W1W1 卷积,对应的得到两个卷积结果。

使用卷积核 W0W0 卷积:

使用卷积核 W1W1 卷积:

本 GIF 参考网址:

http://cs231n.github.io/assets/conv-demo/index.html

实战练习

使用 TensorFlow 2.0 练习对一张图片的卷积操作。

首先导入所需要的库:

import tensorflow as tf
import numpy as np
import matplotlib.image as mpimg
import matplotlib.pyplot as plt

打印 TensorFlow 的版本:

tf.__version__ # '2.1.0'

显示图片:

x_input = mpimg.imread('../input/guloupic/gulou.png')
plt.imshow(x_input) # 显示图片
w,h,ch = x_input.shape

二维卷积操作的具体过程:

x_input2 = x_input.reshape(1,w,h,4).astype('float32')

strides = [1,5,5,1] # 卷积步长
np.random.seed(1) 
filters = np.random.normal(0,1,(3,3,ch,ch)).astype('float32') # 卷积核
padding = 'VALID' # padding 填充方式

conv2d_result = tf.nn.conv2d(
    x_input2, filters, strides, padding, data_format='NHWC', dilations=None, name=None
) # 二维卷积结果

展示卷积结果:

conv2d_result_reshape = tf.reshape(
    conv2d_result, (conv2d_result.shape[1],conv2d_result.shape[2],4), name=None
)

plt.imshow(conv2d_result_reshape)

ReLU 操作

CNN 常用的激活函数不是 Sigmoid 函数,Sigmoid 函数最大的问题是随着深度学习会出现梯度消失,这样会导致最后的收敛速度变得很慢。

经过实践证明,采取另外一个函数,性能会更好些,这就是 ReLU 函数,图像如下所示:

在 xx 大于 0 时是 y=xy=x,在 xx 小于 0 时,y=0y=0。

下面解释 ReLU 函数对 CNN 的实际意义。CNN 的卷积操作是线性操作,因为是对应元素相乘然后再求和。但是在现实世界中,数据很多是非线性的。

所以,有必要引入一个非线性的激活函数,下面观察 ReLU 操作对图片的改变。

ReLU 练习

上一节“实战练习”中的卷积结果:

conv2d_result_reshape[:-13]

<tf.Tensor: shape=(127, 223, 4), dtype=float32, numpy=
array([[[-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        ...,
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00]],

       [[-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        ...,
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00]],

       [[-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        ...,
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00],
        [-4.0177217e+00, -2.3501704e+00,  1.1822378e+01,  2.4460835e+00]],

       ...,

       [[-4.4699464e+00, -2.8374207e-01,  7.4266491e+00,  2.7553670e+00],
        [-4.4534636e+00, -2.1501386e-01,  7.3919554e+00,  2.7302613e+00],
        [-4.4412746e+00, -2.7056146e-01,  7.4853611e+00,  2.7247365e+00],
        ...,
        [-4.5163803e+00,  7.9367775e-01,  5.4839144e+00,  2.7873764e+00],
        [-4.4310455e+00,  1.0796341e+00,  4.9612689e+00,  2.5615096e+00],
        [-4.4181938e+00,  7.1417266e-01,  5.8086748e+00,  2.7583740e+00]],

       [[-4.4662819e+00, -9.6915960e-02,  6.4962173e+00,  2.7503664e+00],
        [-4.4769859e+00, -7.4161291e-03,  6.4029207e+00,  2.7244992e+00],
        [-4.4345908e+00, -1.7963517e-01,  6.8255968e+00,  2.6881800e+00],
        ...,
        [-4.6531935e+00,  8.3264470e-01,  5.5537596e+00,  2.8101211e+00],
        [-4.6456270e+00,  1.0831457e+00,  5.1316061e+00,  2.7046926e+00],
        [-4.6310258e+00,  1.0026519e+00,  5.0778046e+00,  2.7622297e+00]],

       [[-4.8606205e+00, -6.7689800e-01,  7.4223537e+00,  3.0271709e+00],
        [-4.7272291e+00, -1.0091350e+00,  8.2068634e+00,  2.8835974e+00],
        [-4.6644588e+00, -8.6081040e-01,  7.8960209e+00,  2.8981597e+00],
        ...,
        [-4.5784292e+00,  3.1594539e-01,  6.2224164e+00,  2.8287945e+00],
        [-4.6265659e+00,  1.7540711e-01,  6.4480362e+00,  2.8683124e+00],
        [-4.5952659e+00,  1.3691759e-01,  6.6132722e+00,  2.7729447e+00]]],
      dtype=float32)>

对卷积结果使用 ReLU 激活函数:

relu_result = tf.nn.relu(
    conv2d_result_reshape, name=None
)

relu_result[:-13]

观察 ReLu 后的计算结果,负值全被置为 0。

<tf.Tensor: shape=(127, 223, 4), dtype=float32, numpy=
array([[[ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        ...,
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ]],

       [[ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        ...,
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ]],

       [[ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        ...,
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ],
        [ 0.        ,  0.        , 11.822378  ,  2.4460835 ]],

       ...,

       [[ 0.        ,  0.        ,  7.426649  ,  2.755367  ],
        [ 0.        ,  0.        ,  7.3919554 ,  2.7302613 ],
        [ 0.        ,  0.        ,  7.485361  ,  2.7247365 ],
        ...,
        [ 0.        ,  0.79367775,  5.4839144 ,  2.7873764 ],
        [ 0.        ,  1.0796341 ,  4.961269  ,  2.5615096 ],
        [ 0.        ,  0.71417266,  5.808675  ,  2.758374  ]],

       [[ 0.        ,  0.        ,  6.4962173 ,  2.7503664 ],
        [ 0.        ,  0.        ,  6.4029207 ,  2.7244992 ],
        [ 0.        ,  0.        ,  6.825597  ,  2.68818   ],
        ...,
        [ 0.        ,  0.8326447 ,  5.5537596 ,  2.810121  ],
        [ 0.        ,  1.0831457 ,  5.131606  ,  2.7046926 ],
        [ 0.        ,  1.0026519 ,  5.0778046 ,  2.7622297 ]],

       [[ 0.        ,  0.        ,  7.4223537 ,  3.027171  ],
        [ 0.        ,  0.        ,  8.206863  ,  2.8835974 ],
        [ 0.        ,  0.        ,  7.896021  ,  2.8981597 ],
        ...,
        [ 0.        ,  0.3159454 ,  6.2224164 ,  2.8287945 ],
        [ 0.        ,  0.17540711,  6.448036  ,  2.8683124 ],
        [ 0.        ,  0.13691759,  6.613272  ,  2.7729447 ]]],
      dtype=float32)>

Pooling 层

Pooling 层起到降低上一层输入的特征维数的作用,但是同时能保持其最重要的信息,Pooling 操作方法有多种,如最大池化、平均池化、求和池化等。

以最大池化为例,池化一般在 ReLU 操作之后,首先定义一个相邻区域,然后求出这个区域的最大值,再选定一个步长,依次遍历完图像,如下图所示:

Pooling 操作,使得输入的特征维数降低;权重参数个数变少;相当于决策树中的剪枝操作,能防止过拟合;经过池化操作后,CNN 可以适应图片小的位移和扭曲。

池化练习

接着上一节的 ReLU 操作,继续对图标做池化操作。

relu_result2 = tf.reshape(
    relu_result, (1,relu_result.shape[0],relu_result.shape[1],4), name=None
)
relu_result2.shape # TensorShape([1, 140, 223, 4])

pool_result = tf.nn.pool(
    relu_result2, [3,3], 'MAX', strides=[3,3], padding='VALID',
    data_format=None, dilations=None, name=None
) # MAX 池化操作
pool_result.shape # TensorShape([1, 46, 74, 4])

池化操作后,pool_result 的 shape 变为 ([1, 46, 74, 4])。

绘制池化操作后打印图片:

pool_result2 = tf.reshape(
    pool_result, (pool_result.shape[1],pool_result.shape[2],pool_result.shape[3]), name=None
)
plt.imshow(pool_result2)

CNN 总结

至此已经介绍完了 CNN 使用的主要操作包括:卷积操作、ReLU 操作、Pooling 操作。

与 DNN 网络不通,CNN 网络做了局部连接和权值共享,也就是卷积操作,而卷积又包括单核和多核卷积。

与线型的卷积操作不通,ReLU 操作实现非线性变化,而 Pooling 操作进一步减少权重参数。

最后附本文的练习代码的 notebook 完整版:

https://pan.baidu.com/s/1OzmC9VUwyJfRSvB6bFwBZg

提取码:b1m3

希望大家动手实践一遍这些常见操作。

发表评论

电子邮件地址不会被公开。 必填项已用*标注