avatar


6.卷积神经网络

让子弹飞
《让子弹飞》这部电影,还是有很多地方值得我们琢磨的。
如下图所示,比如影片开头的这一段,张牧之和马邦德等人进城,黄四郎通过一个望远镜扫描这支队伍。每扫描一段,黄四郎就会得出一个结论,“张牧之很嚣张”、“马邦德很嚣张”。
扫描
最后所有的结论综合在一起,就是黄四郎对这支队伍的评价:霸气外露。
霸气外露
而我们的卷积神经网络的工作原理,就和这个相似。

卷积神经网络相比我们之前讨论的神经网络主要多了两个东西。一个是卷积,一个是池化。现在我们分别讨论。

卷积

卷积工作原理

现在,让我们回到影片开头这一段。张牧之和马邦德等人组成的队伍进城,黄四郎带着望远镜扫描这支队伍。

这里有两个主体:

  1. 张牧之和马邦德等人组成的队伍
  2. 带着望远镜的黄四郎

而这一段,又有两个特点:

  1. 每次望远镜只能观察到这支队伍的局部。
  2. 全程都在用同一个望远镜进行观察。

现在,让我们把以这一段抽象出来。
如下图所示:左侧的就是张牧之和马邦德等人组成的队伍,我们称之为图像;右侧的就是带着望远镜的黄四郎,我们称之为卷积核。
图像和卷积

  1. 卷积核每次只能观察到图像的局部,我们称之为局部连接
  2. 全程都在用同一个卷积核扫描图像,我们称之为权值共享

即卷积核的两个特点:

  1. 局部连接
  2. 权值共享

那么卷积核怎么对图像进行观察呢?卷积

卷积的运算法则

卷积和加减乘除一样,是一种运算。那么,既然是运算呢,就有运算法则。

特别注意: 我们这里只讨论离散序列的运算法则。

一维

假设我们养金鱼,我们每隔一小时投放一次饲料,而且每次投放的饲料数是一个关于时刻的函数,在tt时刻投放饲料为xtx_t。金鱼会吃饲料,因此,饲料在投放kk时刻后,只剩下原来的wkw_k,而且金鱼更偏好吃新投放的饲料,所以w1=1w_1 = 1w2=12w_2 = \frac{1}{2}w3=14w_3 = \frac{1}{4}。现在问,在tt时刻,鱼缸中有多少饲料?
我们令yty_ttt时刻,鱼缸中的饲料。

yt=1xt+12xt1+14xt2=w1xt+w2xt1+w3xt2=k=13wkxtk+1\begin{aligned} y_t &= 1 \cdot x_t + \frac{1}{2} \cdot x_{t-1} + \frac{1}{4} \cdot x_{t-2} \\ &= w_1 \cdot x_t + w_2 \cdot x_{t-1} + w_3 \cdot x_{t-2} \\ &= \sum_{k=1}^{3} w_k \cdot x_{t-k+1} \end{aligned}

上述运算可总结为:

yt=k=1mwkxtk+1y_t = \sum_{k=1}^{m}w_k \cdot x_{t-k+1}

记作y=wxy = w \otimes x,其中\otimes表示卷积运算。
这就是一维下的卷积运算法则。

二维

我们发现卷积的运算法则是"倒着乘的乘积的和",根据一维的运算法则,我们很容易类比写出二维的运算法则。
给定一个图像XRMN\bold{X} \in \boldsymbol{R}^{M \cdot N},和滤波器WRmn\bold{W} \in \boldsymbol{R}^{m \cdot n},通常m<<Mm << Mn<<Nn << N

y=u=1mv=1n=wu,vxmu+1,nv+1y = \sum_{u=1}^{m} \sum_{v=1}^{n} = w_{u,v} \cdot x_{m-u+1,n-v+1}

二维

y=u=1mv=1n=wu,vxmu+1,nv+1=u=13v=13=wu,vx3u+1,3v+1=w1,1x31+1,31+1+w1,2x31+1,32+1+w1,3x31+1,33+1+ w2,1x32+1,31+1+w2,2x32+1,32+1+w2,3x32+1,33+1+ w3,1x33+1,31+1+w3,2x33+1,32+1+w3,3x33+1,33+1=10+01+01+ 01+00+03+ 01+01+11+=1\begin{aligned} y &= \sum_{u=1}^{m} \sum_{v=1}^{n} = w_{u,v} \cdot x_{m-u+1,n-v+1} \\ &= \sum_{u=1}^{3} \sum_{v=1}^{3} = w_{u,v} \cdot x_{3-u+1,3-v+1} \\ &=w_{1,1} \cdot x_{3-1+1,3-1+1} + w_{1,2} \cdot x_{3-1+1,3-2+1} + w_{1,3} \cdot x_{3-1+1,3-3+1} + \\ &\quad \ w_{2,1} \cdot x_{3-2+1,3-1+1} + w_{2,2} \cdot x_{3-2+1,3-2+1} + w_{2,3} \cdot x_{3-2+1,3-3+1} + \\ &\quad \ w_{3,1} \cdot x_{3-3+1,3-1+1} + w_{3,2} \cdot x_{3-3+1,3-2+1} + w_{3,3} \cdot x_{3-3+1,3-3+1} \\ &= 1 \cdot 0 + 0 \cdot -1 + 0 \cdot 1 + \\ &\quad \ 0 \cdot 1 + 0 \cdot 0 + 0 \cdot -3 + \\ &\quad \ 0 \cdot 1 + 0 \cdot 1 + -1 \cdot 1 + \\ &= -1 \end{aligned}

不翻转卷积

如上,在二维的时候,我们将卷积核进行了翻转,但是在机器学习、深度学习或者图像处理中,通常我们不这么做,我们不进行翻转。

y=u=1mv=1n=wu,vxu,vy = \sum_{u=1}^{m} \sum_{v=1}^{n} = w_{u,v} \cdot x_{u,v}

这种称为不翻转卷积,记做y=WXy = \bold{W} \otimes \bold{X}

特别注意: 通常,我们所指的的卷积,都是不翻转卷积。

步长和填充

再让我们回到《让子弹飞》电影的开头。现在我们知道了什么是"张牧之和马邦德的队伍",知道了什么是"带着望远镜的黄四郎",而且连"如何观察"我们也知道了。但是正如影片的描述,黄四郎并不是盯着一个地方一直看,我们注意到望远镜一直在动,黄四郎是在扫描。

步长

扫描其实很简单,我们按照从左到右,从上到下的顺序进行扫描。但是我们得定,我们每次移动多少,这就是步长。
如下图所示,就是步长为2的时候。

步长为二

填充

在上面的例子中,被观察对象是5*5的,卷积核是3*3的,步长是2。这时候恰好能扫描到被观察对象的每一个像素点。
那么,假如被观察对象是4*4的,这时候就无法恰好扫描到被观察对象的每一个像素点。那么这时候这么办呢?

  1. 放弃部分像素点。
  2. 填充0。

此外,有些时候,即使我们能扫描到每一个像素点的时候,仍然会填充0。因为有时候我们会希望输出O\bold{O}的高宽能够与输入X\bold{X}的高宽相同,从而方便网络参数的设计、残差连接等。
例如:
填充0

输出和步长填充的关系

卷积层的输出尺寸[h,w][h',w']由卷积核的大小kk,步长ss,上下填充数量php_h,左右填充数量pwp_w以及输入X\bold{X}的高宽[h,w][h,w]共同决定,它们之间的数学关系为:

h=h+2phks+1h' = \frac{h + 2 \cdot p_h - k}{s} + 1

w=w+2pwks+1w' = \frac{w + 2 \cdot p_w - k}{s} + 1

  • 特别注意:这里只考虑上下填充数量php_h,左右填充数量pwp_w相同的情况。类似"左边填两个,右边填一个",这种不在考虑范围内。

在上面的例子中。h=3,w=5,k=3,ph=pw=1,s=1h=3,w=5,k=3,p_h=p_w=1,s=1

h=5+2131+1=4+1=5h' = \frac{5 + 2 \cdot 1 - 3}{1} + 1 = 4 + 1 = 5

w=5+2131+1=4+1=5w' = \frac{5 + 2 \cdot 1 - 3}{1} + 1 = 4 + 1 = 5

在TensorFlow中,如果希望输出O\bold{O}和输入X\bold{X}高、宽相等,只需要简单地设置参数padding='SAME'、strides=1即可。

多通道输入

多通道输入、单卷积核

截至目前,我们讨论的被观察对象,都只有张牧之和马邦德等人所组成的一支队伍,这个在计算机中被称为单通道。单通道在计算机中只能表示黑白图像。那么对于彩色图像怎么办呢?我想这个问题,我们的周润发同学,能给出答案。
三张压成一张
是的,三张压成一张。
我们知道,计算机有RGB颜色空间,RGB颜色的排列组合有2563256^3种。我们先做一张R的,再做一张G的,最后做一张B的。把三张合成一张,彩色图片的效果就做出来了。
那么,对于多通道的输入,单卷积核怎么处理呢?
我想,我们只用一张图片,就可以说明。
多通道输入、单卷积核
一句话:各个击破,直接相加。

多通道输入、多卷积核

在影片结尾,黄四郎对张牧之说:“进城那天,如果我亲自去接你。不是叫胡万过去给你捣乱,结果会不会不一样?”
如果
实际上过去似流云,过去不必追。但假如当时还有其他人在带着望远镜观察,或许能得出"合作共赢"的结论,如果还能和黄四郎的结论综合的话,结果或许会不一样。

不同的卷积核相当于不同的特征提取器。我们之前讨论都是只有一个卷积核,观察结果是一维结构。既然卷积神经网络主要应用在图像处理上,而彩色图像为三维结构。为了更充分地利用图像的局部信息,通常将卷积核组织为三维结构,其大小为高度HH、宽度WW、深度DD,有DDH×WH \times W大小的特征映射构成。

特征映射(Feature Map)为一幅图像(或其它特征映射)在经过卷积提取到的特征,每个特征映射可以作为一类抽取的图像特征。为了提高卷积网络的表示能力,可以在每一层使用多个不同的特征映射,以更好地表示图像的特征。

那么,多通道的输入、多卷积核怎么处理呢?
我想,我们同样只用一张图就可以说明。
多通道输入、多卷积核
有几个卷积核,结果就有几个通道。

梯度传播

梯度

现在我们的思路已经很清楚了。

  1. 我们有一个单通道或多通道的图像,例如是[28,28,3]
  2. 我们用多个卷积核去观察图像,或者我们说是用多个卷积核对图像进行特征提取。例如是5个[3,3,3]的卷积,步长为1,不进行填充。
    则,我们得到新的数据是2831=25\frac{28-3}{1} = 25。即[25,25,5]
  3. 然后我们再对数据进行卷积、或者全连接层等等。

但,似乎还少了一样。5个[3,3,3]的卷积核。那权重是多少?偏置又是多少?
权重和偏置很好求,梯度下降即可。唯一麻烦的是梯度。

现在,我们讨论梯度。

我们以最简单的一种为例。

梯度传播

  • x[b,3,3,1]:代表b个样本,3*3的高宽,1个通道
  • w[2,2,c,N]:代表2*2的滤波器,c个通道,N个卷积核
  • o[b,2,2,c]:代表b个样本,2*2的高宽,c个通道,这里的通道数等于卷积核的个数

对于输出,我们有:

  • o00=x00w00+x01w01+x10w10+x11w11+bo_{00} = x_{00}w_{00} + x_{01}w_{01} + x_{10}w_{10} + x_{11}w_{11} + b
  • o01=x01w00+x02w01+x11w10+x12w11+bo_{01} = x_{01}w_{00} + x_{02}w_{01} + x_{11}w_{10} + x_{12}w_{11} + b
  • o10=x10w00+x11w01+x20w10+x21w11+bo_{10} = x_{10}w_{00} + x_{11}w_{01} + x_{20}w_{10} + x_{21}w_{11} + b
  • o11=x11w00+x12w01+x21w10+x22w11+bo_{11} = x_{11}w_{00} + x_{12}w_{01} + x_{21}w_{10} + x_{22}w_{11} + b

我们以Lw00\frac{\partial L}{\partial w_{00}}为例。

Lw00=i{00,01,10,11}Loioiw00\frac{\partial L}{\partial w_{00}} = \sum_{i \in \{00,01,10,11\}} \frac{\partial L}{\partial o_i} \frac{\partial o_i}{\partial w_{00}}

oiw00\frac{\partial o_i}{\partial w_{00}}的计算非常简单。

o00w00=x00o01w00=x01o10w00=x10o11w00=x11\frac{\partial o_{00}}{\partial w_{00}} = x_{00} \quad \frac{\partial o_{01}}{\partial w_{00}} = x_{01} \quad \frac{\partial o_{10}}{\partial w_{00}} = x_{10} \quad \frac{\partial o_{11}}{\partial w_{00}} = x_{11}

关于sum的讨论

Lw00\frac{\partial L}{\partial w_{00}}中,我们看到在损失函数还没确定的情况下,就已经确定需要\sum了。最初我在看到这个的时候,认为这个是不严谨的。但仔细分析,确实如此。

我们这里讨论一下,以加深我们对损失函数的理解。

我们知道,常见的损失函数一般有两种。

  1. 均方误差
  2. 交叉熵损失,这种通常和Softmax配合使用

均方误差

Loss=12(yioi)2Loss = \frac{1}{2} \sum (y_i - o_i)^2

Lossw00=(yioi)oiw00\frac{\partial Loss}{\partial w_{00}} = \sum (y_i - o_i) \frac{\partial o_i}{\partial w_{00}}

所以,均方误差作为损失函数,其梯度要对oiw00\frac{o_i}{w_{00}}进行\sum,这个是毫无疑问的。

交叉熵损失

Loss=logq2Loss = - \log q_2

而,交叉熵损失,通常和Softmax配合使用。所以:

q2=eo2eoiq_2 = \frac{e^{o_2}}{\sum e^{o_i}}

我们对q2q_2进行求导,则有

q2=(eo2)(eoi)(eo2)(eoi)(eoi)2q_{2}' = \frac{(e^{o_2})'(\sum e^{o_i}) - (e^{o_2})(\sum e^{o_i})'}{(\sum e^{o_i})^2}

所以,交叉熵损失,也是需要对oiw00\frac{o_i}{w_{00}}进行\sum的。

卷积层的实现

至此,整个卷积层的主要内容,我们几乎都讨论过了。现在我们试图实现一个卷积层。
这里我们用两种方法:

  1. TensorFlow
  2. Keras

基于TensorFlow

在TensorFLow中,实现一个卷积层的方法是

1
tf.nn.conv2d(input=x,filters=w,strides=1,padding=[[0,0],[0,0],[0,0],[0,0]])

其中需要特别说明的是,padding参数的设置格式:padding=[[0,0],[上,下],[左,右],[0,0]]
例如,上下左右各填充一个单位:padding=[[0,0],[1,1],[1,1],[0,0]]
示例代码:

1
2
3
4
5
6
7
8
9
10
import tensorflow as tf

# 两个样本,高宽为5,3个通道。
x = tf.random.normal([2,5,5,3])
# 3*3,3个通道的卷积核,一共4个卷积核
w = tf.random.normal([3,3,3,4])
# 步长为1,padding为0
out = tf.nn.conv2d(input=x,filters=w,strides=1,padding=[[0,0],[0,0],[0,0],[0,0]])
# 打印张量的shape
print(out.shape)

运行结果:

1
(2, 3, 3, 4)

解释:

  1. 第一个数字2,代表样本数:2。
  2. 第二个数字3,代表高:3。531+1=3\frac{5-3}{1} + 1 = 3
  3. 第三个数字3,代表宽:3。531+1=3\frac{5-3}{1} + 1 = 3
  4. 第四个数字4,代表通道数:4。也就是卷积核的个数。

特别的:通过设置参数padding='SAME',strides设置为大于1的值,可以直接得到输入、输出同大小的卷积层。
示例代码:

1
2
3
4
5
6
7
8
9
10
import tensorflow as tf

# 两个样本,高宽为5,3个通道。
x = tf.random.normal([2,5,5,3])
# 3*3,3个通道的卷积核,一共4个卷积核
w = tf.random.normal([3,3,3,4])
# 需要注意的是, padding=same 只有在 strides=1 时才是同大小
out = tf.nn.conv2d(x,w,strides=1,padding='SAME')
# 打印张量的shape
print(out.shape)

运行结果:

1
(2, 5, 5, 4)

通过设置参数padding='SAME'、strides>1可以直接得到输出是输入1s\frac{1}{s}的卷积层。
示例代码:

1
2
3
4
5
6
7
import tensorflow as tf

x = tf.random.normal([2,5,5,3])
w = tf.random.normal([3,3,3,4])
# 向上取整
out = tf.nn.conv2d(x,w,strides=2,padding='SAME')
print(out.shape)

运行结果:

1
(2, 3, 3, 4)

如果我们还需要偏置,怎么办?
示例代码:

1
tf.nn.bias_add(value, bias, data_format=None, name=None)

基于Keras

在之前,我们都手动定义卷积核的W\bold{W}和偏置b\bold{b}张量。现在我们有更方便的方法,直接调用类实例即可完成卷积层的前向计算。

在TensorFlow中,API的命名有一定的规律,首字母大写的对象一般表示类,全部小写的一般表示函数。如layers.Conv2D表示卷积层类,nn.conv2d表示卷积运算函数。

1
layer = layers.Conv2D()

调用示例的__call__方法即可完成前向运算。还可以通过layer.trainable_variable查看卷积核的W\bold{W}b\bold{b}

示例代码:

1
2
3
4
5
6
7
8
9
10
import tensorflow as tf
from tensorflow.keras import layers

# 两个样本,高宽为5,3个通道。
x = tf.random.normal([2,5,5,3])
layer = layers.Conv2D(filters=4,kernel_size=3,strides=1,padding='SAME')
out = layer(x)
print(out.shape)
# 返回所有待优化的张量列表
print(layer.trainable_variables)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
(2, 5, 5, 4)
[<tf.Variable 'conv2d_1/kernel:0' shape=(3, 3, 3, 4) dtype=float32, numpy=
array([[[[-0.10979834, 0.05611697, 0.07756695, -0.19519049],
[-0.25532633, 0.20102468, -0.01257691, -0.19999951],
[ 0.05867973, 0.21140966, 0.18826845, -0.11313015]],

[[ 0.0214611 , -0.08925012, -0.09455462, 0.1089299 ],
[ 0.09674495, -0.11473732, 0.16129279, -0.11318652],
[-0.08330335, -0.12321712, 0.15122685, -0.08722048]],

[[ 0.01963314, 0.24888834, -0.14285056, 0.2565213 ],
[ 0.17318556, -0.22936352, 0.14522609, 0.16536382],
[ 0.15357831, -0.2821764 , -0.2947683 , 0.29289815]]],


[[[-0.17826964, 0.28726915, -0.24313128, 0.09069496],
[ 0.15975893, 0.17163727, -0.24049374, -0.28351462],
[-0.00981075, 0.29518244, -0.03597486, -0.07030587]],

[[ 0.23602709, -0.13521343, 0.21714154, 0.06650102],
[ 0.09308881, 0.01543975, 0.06182054, -0.22127749],
[-0.1318672 , 0.20529953, 0.26045617, 0.06962284]],

[[ 0.13752908, 0.24204233, -0.1383889 , 0.22505233],
[ 0.20928249, -0.05574575, 0.04975668, 0.11022466],
[-0.22181004, 0.21804205, -0.21576726, -0.10982512]]],


[[[-0.18072543, -0.3039346 , 0.17995346, -0.1626617 ],
[-0.0756432 , -0.15350364, 0.03744105, -0.02793446],
[-0.03010941, -0.06961726, -0.07880872, 0.0398711 ]],

[[-0.19787115, -0.19895059, 0.12084219, -0.25610667],
[ 0.13629442, -0.18237923, -0.15923041, -0.00456873],
[ 0.19431832, 0.21129027, 0.06643423, 0.07392919]],

[[-0.24683592, -0.20284012, 0.18018603, 0.2478585 ],
[ 0.08107573, -0.17767616, 0.11991593, -0.19584629],
[ 0.22088405, -0.03678539, 0.0819014 , 0.03509244]]]],
dtype=float32)>, <tf.Variable 'conv2d_1/bias:0' shape=(4,) dtype=float32, numpy=array([0., 0., 0., 0.], dtype=float32)>]

除了这种我们常见的卷积,还有三种卷积:

  1. 空洞卷积
  2. 转置卷积
  3. 分离卷积

池化

池化运算

除了用望远镜(卷积)进行观察,还有一种方法:池化
池化

之前我们讨论的卷积,有两个特点:

  1. 局部连接
  2. 权值共享

池化层同样是局部连接,通过从局部连接的一组元素中进行采样或信息聚合,从而得到新的元素值。常见的池化有两种:

  1. 最大池化层(Max Pooling)从局部相关元素集中选取最大的一个元素值。
  2. 平均池化层(Average Pooling)从局部相关元素集中计算平均值并返回。

既然是最大平均,权值共享的特点肯定是没有了,毕竟连权值都没了。
不过,感受野和步长的概念还是有的。

池化的运算也很简单,例如:最大池化层运算。
最大池化层

池化实现

和卷积层的实现一样,池化层的实现有两种方法。

1
tf.nn.max_pool2d()
1
layers.MaxPooling2D()

示例代码:

1
2
3
4
5
6
7
8
import tensorflow as tf
from tensorflow.keras import layers

# 两个样本,高宽为5,3个通道。
x = tf.random.normal([2,5,5,3])
print(tf.nn.max_pool2d(input=x,ksize=3,strides=1,padding='SAME').shape)
layer = layers.MaxPooling2D(pool_size=3,strides=1,padding='same')
print(layer(x).shape)

运行结果

1
2
(2, 5, 5, 3)
(2, 5, 5, 3)

卷积神经网络

通过之前的讨论,我们已经知道了什么是卷积。那么把卷积应用在神经网络中,就是卷积神经网络。现在,我们来实现一个卷积神经网络。

我们以LeNet-5,这是一个非常经典的卷积神经网络。出现30年前,当时Yann LeCun等人用于手写数字识别。其结构如下:
LeNet-5

我们这里对其进行简单的修改,使其符合现代深度学习开发框架。

构建网络

首先,我们根据网络结构来构建网络。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from tensorflow.keras import Sequential,layers

network = Sequential([
# 卷积层1
layers.Conv2D(filters=6,kernel_size=(5,5),activation="relu",input_shape=(28,28,1),padding="same"),
layers.MaxPool2D(pool_size=(2,2),strides=2),

# 卷积层2
layers.Conv2D(filters=16,kernel_size=(5,5),activation="relu",padding="same"),
layers.MaxPool2D(pool_size=2,strides=2),

# 卷积层3
layers.Conv2D(filters=32,kernel_size=(5,5),activation="relu",padding="same"),

layers.Flatten(),

# 全连接层1
layers.Dense(200,activation="relu"),

# 全连接层2
layers.Dense(10,activation="softmax")
])
network.summary()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 28, 28, 6) 156
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 14, 14, 6) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 14, 14, 16) 2416
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 7, 7, 16) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 7, 7, 32) 12832
_________________________________________________________________
flatten (Flatten) (None, 1568) 0
_________________________________________________________________
dense (Dense) (None, 200) 313800
_________________________________________________________________
dense_1 (Dense) (None, 10) 2010
=================================================================
Total params: 331,214
Trainable params: 331,214
Non-trainable params: 0
_________________________________________________________________

加载数据集

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

import tensorflow as tf
from tensorflow.keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

print(x_train.shape)
print(y_train.shape)
print(x_test.shape)
print(y_test.shape)

# 扩展为单通道
x_train = tf.expand_dims(x_train, axis=3)
x_test = tf.expand_dims(x_test, axis=3)
print(x_train.shape)
print(x_test.shape)

db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(200)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(200)

运行结果:

1
2
3
4
5
6
(60000, 28, 28)
(60000,)
(10000, 28, 28)
(10000,)
(60000, 28, 28, 1)
(10000, 28, 28, 1)

训练模型

示例代码:

1
2
3
4
5
6
7
from tensorflow.keras import optimizers,losses

adam = optimizers.Adam()
sparse_categorical_crossentropy = losses.SparseCategoricalCrossentropy()

network.compile(optimizer=adam,loss=sparse_categorical_crossentropy,metrics=['accuracy'])
network.fit(db_train,epochs=10)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Epoch 1/10
300/300 [==============================] - 15s 51ms/step - loss: 0.4975 - accuracy: 0.9157
Epoch 2/10
300/300 [==============================] - 15s 51ms/step - loss: 0.0659 - accuracy: 0.9785
Epoch 3/10
300/300 [==============================] - 16s 52ms/step - loss: 0.0385 - accuracy: 0.9877
Epoch 4/10
300/300 [==============================] - 16s 54ms/step - loss: 0.0259 - accuracy: 0.9914
Epoch 5/10
300/300 [==============================] - 16s 54ms/step - loss: 0.0217 - accuracy: 0.9929
Epoch 6/10
300/300 [==============================] - 16s 54ms/step - loss: 0.0185 - accuracy: 0.9936
Epoch 7/10
300/300 [==============================] - 16s 53ms/step - loss: 0.0181 - accuracy: 0.9938
Epoch 8/10
300/300 [==============================] - 16s 54ms/step - loss: 0.0140 - accuracy: 0.9950
Epoch 9/10
300/300 [==============================] - 16s 53ms/step - loss: 0.0140 - accuracy: 0.9954
Epoch 10/10
300/300 [==============================] - 16s 54ms/step - loss: 0.0152 - accuracy: 0.9947

测试模型

示例代码:

1
2
3
4
# 测试模型
loss,accuracy=network.evaluate(db_test)
print(loss)
print(accuracy)

运行结果:

1
2
3
50/50 [==============================] - 1s 15ms/step - loss: 0.0780 - accuracy: 0.9835
0.07802382856607437
0.9835000038146973

画图

我们还可以画个图,这样更直观。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import matplotlib.pyplot as plt

for i in range(25):
plt.subplot(5,5,i+1)
plt.imshow(x_test[i], cmap='gray')
plt.show()

# 预测前25张图片结果
result = network.predict(x_test)[0:25]
pred = tf.argmax(result, axis=1)
pred_list=[]
for item in pred:
pred_list.append(item.numpy())
print(pred_list)

运行结果:
MNIST

1
[7, 2, 1, 0, 4, 1, 4, 9, 5, 9, 0, 6, 9, 0, 1, 5, 9, 7, 3, 4, 9, 6, 6, 5, 4]

BatchNorm

BN层

2015年,Google研究人员Sergey Ioffe等人提出了一种参数标准化(Normalize)的手段,并基于参数标准化设计了Batch Nomalization(简写为BatchNorm,或 BN)。BatchNorm的提出,使得网络的超参数的设定更加自由,比如更大的学习率、更随意的网络初始化等,同时网络的收敛速度更快,性能也更好。BatchNorm广泛地应用在各种深度网络模型上,Conv-BN-ReLU-Pooling甚至一度成为标配。

那么,什么是BatchNorm呢?
Norm的意思是标准化,Batch的意思是批量。BatchNorm的意思就是批量标准化。
那么?对谁进行批量的标准化呢?
我们用一张图片说明。
BN

BN的公式

在训练阶段和在测试阶段,还略有不同。我们分情况讨论。

训练阶段

x~train=xtrainμBσB2+ϵγ+β\tilde{x}_{train} = \frac{x_{train} - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} \cdot \gamma + \beta

  • μB\mu_BσB2\sigma_B^2是当前Batch的均值和方差。
  • ϵ\epsilon是为防止出现除0错误而设置的较小数字,如10910^{-9}
  • γ\gammaβ\beta,将由反向传播算法进行优化。

所以,Batch_Size也是一个超参数了,在Batch_Norm中。当然,如果数据集很大的话,可能影响有限。

同时按照

μrmomentumμr+(1momentum)μB\mu_r \leftarrow momentum \cdot \mu_r + (1 - momentum) \cdot \mu_B

σr2momentumσr2+(1momentum)σB2\sigma_r^2 \leftarrow momentum \cdot \sigma_r^2 + (1 - momentum) \cdot \sigma_B^2

迭代更新全局训练数据的统计值μr\mu_rσr2\sigma_r^2,其中momentummomentum在之前我们讨论过,动量。在TensorFlow中,momentummomentum默认为0.990.99

测试阶段

x~test=xtestμrσr2+ϵγ+β\tilde{x}_{test} = \frac{x_{test} - \mu_r}{\sqrt{\sigma_r^2 + \epsilon}} \cdot \gamma + \beta

测试阶段的μr\mu_rσr2\sigma_r^2是之前训练阶段所得到的μr\mu_rσr2\sigma_r^2

BN的实现和应用

创建BN层:

1
layer = layers.BatchNormalization()

应用:
直接添加到Sequential网络容器中即可。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from tensorflow.keras import Sequential, layers

network = Sequential([

# 卷积层1
layers.Conv2D(filters=6, kernel_size=(5, 5), activation="relu", input_shape=(28, 28, 1), padding="same"),
layers.MaxPool2D(pool_size=(2, 2), strides=2),

layers.BatchNormalization(),
# 卷积层2
layers.Conv2D(filters=16, kernel_size=(5, 5), activation="relu", padding="same"),
layers.MaxPool2D(pool_size=2, strides=2),

layers.BatchNormalization(),
# 卷积层3
layers.Conv2D(filters=32, kernel_size=(5, 5), activation="relu", padding="same"),

layers.Flatten(),

# 全连接层1
layers.Dense(200, activation="relu"),

# 全连接层2
layers.Dense(10, activation="softmax")
])
network.summary()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 28, 28, 6) 156
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 14, 14, 6) 0
_________________________________________________________________
batch_normalization (BatchNo (None, 14, 14, 6) 24
_________________________________________________________________
conv2d_1 (Conv2D) (None, 14, 14, 16) 2416
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 7, 7, 16) 0
_________________________________________________________________
batch_normalization_1 (Batch (None, 7, 7, 16) 64
_________________________________________________________________
conv2d_2 (Conv2D) (None, 7, 7, 32) 12832
_________________________________________________________________
flatten (Flatten) (None, 1568) 0
_________________________________________________________________
dense (Dense) (None, 200) 313800
_________________________________________________________________
dense_1 (Dense) (None, 10) 2010
=================================================================
Total params: 331,302
Trainable params: 331,258
Non-trainable params: 44
_________________________________________________________________

特别注意layers.BatchNormalization()的位置。第一层不用,后面两层都在输入的时候做。

BN的优点

BatchNorm已经广泛被证明其有效性和重要性,尽管有些细节处理还解释不清其理论原因。机器学习领域有个很重要的假设:独立同分布假设,即假设训练数据和测试数据是独立同分布的。那BatchNorm的作用是什么呢?BatchNorm就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布的。
那么,BatchNorm的优点是什么呢?

你至少有三句要说

  1. 不仅仅极大提升了训练速度,收敛过程大大加快。
  2. 还能增加分类效果,一种解释是这是类似于Dropout的一种防止过拟合的正则化表达方式,所以不用Dropout也能达到相当的效果。
  3. 另外调参过程也简单多了,对于初始化要求没那么高,而且可以使用大的学习率等。
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10406
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区