Day 58:卷积神经网络透彻解析和实际应用
发布日期:2022年3月14日 14:57 阅读: 281 访问: 282
今天我们学习图像处理中最常用的卷积神经网络。 卷积神经网络 图像处理中,往往把图像表示为像素的向量,比如一个 $1000\times1000$ 的图像,可以表示为一个 $10^6$ 的向量,如在 MNIST 手写字神经网络中,输入层为 $28×28 = 784$ 维的向量。 如果隐含层的节点个数与输入层一样,即也是 $10^6$ 时,
今天我们学习图像处理中最常用的卷积神经网络。
卷积神经网络
图像处理中,往往把图像表示为像素的向量,比如一个 $1000\times1000$ 的图像,可以表示为一个 $10^6$ 的向量,如在 MNIST 手写字神经网络中,输入层为 $28×28 = 784$ 维的向量。
如果隐含层的节点个数与输入层一样,即也是 $10^6$ 时,那么输入层到隐含层的参数数据为 $10^6\times10^6 =10^{12}$,参数个数暴多,要想在正常的时间内训练完,基本是不可能的。
所以要想处理 $1000 \times 1000$ 的图像分类,就得首先想办法减少参数的个数,也就是只基于普通全连接的深度神经网络(DNN)已经很难训练,有没有更加优秀的算法可以专门处理这种图像分类呢?
这就是卷积神经网络,Convolutional Neural Network ,简称为 CNN。
图像理论预备知识
一副图像在计算机中是如何表达的?一幅单通道图像,可以表示为二维,也就是一个二维的矩阵,空白的地方取值较小,越是颜色黑的区域,矩阵对应的色素值越大。
通道是一幅图像的特定组成部分,常见的手机拍出来的图片会有 3 个通道:红色、绿色、蓝色(RGB),也就是三通道,我们可以把它看作为 3 个二维的数组,每一个二维数组代表一种颜色值,像素值在 0~255 之间。对于灰色图(grayscale),比较特殊,它仅有一个通道,如上图所示的手写字数据集中的手写字 8,它就是由一个通道组成。
DNN 到 CNN 做的改变
一般地,如下图所示为全连接的深度神经网络(DNN),每层的每个神经元节点与前层的所有神经元节点有连接,也会与后一层的所有节点相连接,这样导致的问题是每个节点都有很多个权重参数和偏置量,刚才在上文中我们提到过,那么卷积神经网络想要做的第一件事,想办法解决参数过多的问题。
CNN 有几种措施可以降低参数的数目,主要介绍两种:
- 第一种是局部连接(local connection),也称为局部感知
- 另一种是权值共享(weight sharing)
局部连接
根据图像其局部的像素联系较为紧密,距离较远的像素相关性较弱,这一合理的假设,CNN 认为每个神经元没有必要对整个的全局图像进行感知,只需要对局部进行感知,然后接下来的隐含层中再对局部的信息综合起来,这样就提取成一个新的保留原来主要特征的图像。
局部连接对权重参数的减少力度大吗?我们来计算下,文章开始说到一个如果采用 DNN,那么权重参数为 $10^{12}$ 个,假如采取局部连接,定义隐含层的每个神经元只与输入层的 $100$ 个像素建立关系,也就是说共有 $10^6 \times 100$ 个权重参数,$10^8$ 个,这个参数量还是不小吧,所以需要第二种措施——权值共享。
权值共享
只减少隐含层的节点关联的输入层的像素点,对参数的减少力度一般,那么,在这基础上还能做些什么?
如果我们再做这么一个假设:从紧邻的 100 个像素点抽取出一小块,并已知这一块的每个像素点的权重参数,假定这一块的权重参数也会被100个像素点的权重参数被其他块所共享,这就是权值共享。
抽取的那一小块对应的权重参数为:kernel(也可称为 filter, feature detector),并且往下隐含层网络可以继续使用这种卷积核,这样图像的特征会随着隐含层的加深,而逐渐变得抽象起来。
CNN借助以上两种措施对权重参数做减法,并且结合这两种措施,起名为卷积操作,并且将这种深度学习算法称为卷积神经网络算法。
总结来说,DNN 中是节点与前后层是全连接的,而 CNN 算法对节点启用局部连接和权重参数共享的措施,以此减少权重参数,加快学习训练和收敛速度,使得用神经网络模型对图像进行分类操作成为可能。
卷积操作
单核卷积
在卷积操作中涉及到一种特殊的操作,叫做求内积。此操作过程为先让两个同型矩阵对应的元素相乘,然后再求和。具体说来如下:
A 和 B 求内积:
$$ 1\times3 + 0\times1 + 2\times1 + 3\times5 = 20 $$
这就是两个矩阵求内积得到的结果。
接下来解释,如何用一个指定大小的卷积核,做卷积操作。
为了演示的方便,直接使用一个 $5\times5$ 的图像块:
使用 $3\times3$ 的卷积核如何提取特征,使用如下 $3 \times 3$ 的卷积核:
如下图所示卷积核与 $5\times5$ 的图像块第一次做内积后得到 4, 放在卷积后的矩阵中的第一元素中。
第二步,移动卷积核,移动的步幅大小称为步长(stride),此处移动步长取为 1,这就是 CNN 中的一个重要超参数,移动 1个步长和卷积操作后得到 3,再放入结果中,如下图所示:
这样依次移动 9 步,最后的卷积结果如下图所示,是一个 $3\times3$ 矩阵:
多核卷积
上面介绍使用单个卷积核做卷积操作,下面介绍介绍使用多核卷积操作。
分享一个多核卷积操作的动画,输入为 $ 7 \times 7 \times 3$,使用一层零填充(Zero-padding),它是 CNN 网络中另一个重要超参数。
使用两个过滤核:$W_0$ 和 $W_1$,在 CNN 中称为深度(Depth),是 CNN 三个超参数的最后一个,分别使用 2 个过滤核 $W_0$ 和 $W_1$ 卷积,对应的得到两个卷积结果。
使用卷积核 $W_0$ 卷积:
使用卷积核 $W_1$ 卷积:
本 GIF 参考网址:
实战练习
使用 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 函数,图像如下所示:
在 $x$ 大于 0 时是 $y=x$,在 $x$ 小于 0 时,$y=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
希望大家动手实践一遍这些常见操作。