前几讲,使用 “六步法” 搭建了全连接网络,实现了图像识别应用。实践证明,全连接网络对于识别和预测任务都有很好的效果。
卷积神经网络与全连接神经网络的唯一区别就在于神经网络中相邻两层的连接方式。
全连接网络参数个数: ∑ 层 数 [ ( 前 层 ∗ 后 层 ) w + ( 后 层 ) b ] \sum_{层数}[(前层*后层)_w+(后层)_b] ∑层数[(前层∗后层)w+(后层)b]
针对MNIST数据集的分辨率仅为 28 ∗ 28 28*28 28∗28 的黑白图像 两层神经网络就包含了 101, 770 个参数。在实际应用中,图像的分辨率远高于此,且大多数是彩色图像,如下图所示。
虽然全连接网络一般被认为是分类预测的最佳网络,但待优化的参数过多,容易导致模型过拟合。 为了解决参数量过大而导致模型过拟合的问题,一般不会将原始图像直接输入,而是先对图像进行特征提取,再将提取到的特征输入全连接网络。如下图所示,就是将汽车图片经过多次特征提取后再输入全连接网络。
卷积计算可认为是一种有效提取图像特征的方法。
一般会用一个正方形的卷积核,按指定步长,在输入特征图上滑动,遍历输入特征图中的每个像素点。每一个步长,卷积核会与输入特征图出现重合区域,重合区域对应元素相乘、求和再加上偏置项得到输出特征的一个像素点。
卷积核的深度必须与输入层的通道数一致,所以输入特征图的深度(channel数)决定了当前层卷积核的深度 n n n ;
每个卷积核在卷积计算后,会得到一层输出特征图,所以当前层卷积核的个数决定了当前层输出特征图的深度。
所以一般只需要指定卷积核的尺寸 3 ∗ 3 3*3 3∗3 或 5 ∗ 5 5*5 5∗5 和卷积核的个数 (即输出层特征图的深度)。
所以如果觉得某层模型的特征提取能力不足,可以适当增加该层的卷积核个数,以提高特征提取能力。
卷积核中的参数数量计算如下图所示:
这些参数的更新过程与全连接神经网络一致,同样使用梯度下降法进行参数更新。
单通道卷积核 3 ∗ 3 ∗ 1 3*3*1 3∗3∗1 的计算过程:
多通道卷积核 3 ∗ 3 ∗ 3 3*3*3 3∗3∗3 或 3 ∗ 3 ∗ n 3*3*n 3∗3∗n 的计算过程:
卷积核移动计算过程: 当卷积核遍历完成输入特征图后,就得到了一张输出特征图,完成了一个卷积核的卷积计算过程。当有n个卷积核时,就会有n张输入特征图依次叠加。
感受野(Receptive Field): 卷积神经网络各输出特征图中的每个像素点,在原始输入图片上映射区域的大小。
如下图所示,对于一幅 5 ∗ 5 5*5 5∗5 的原始输入图像,经过两层 3 ∗ 3 3*3 3∗3 卷积核作用,和经过一层 5 ∗ 5 5*5 5∗5 的卷积核作用,都得到一个感受野是 5 5 5 的输出特征图。所以这两层 3 ∗ 3 3*3 3∗3 卷积核和一层 5 ∗ 5 5*5 5∗5 的卷积核的特征提取能力是一样的。
那么,是选择两层 3 ∗ 3 3*3 3∗3 卷积核好?还是选择一层 5 ∗ 5 5*5 5∗5 的卷积核好呢?这时就要考虑他们所承载的参数量和计算量了。
在上一节中介绍时,使用尺寸为 n ∗ n n*n n∗n 的卷积核,输出特征图的尺寸相比输入特征图的尺寸,减小了 n − 1 n-1 n−1 。但是有时候,我们希望卷积计算能够使得输入输出特征图的尺寸保持不变。这就需要用到全零填充。
全零填充(padding): 为了保持输出图像尺寸与输入图像一致, 经常会在输入图像周围进行全零填充,如下图所示,在 5 × 5 5×5 5×5 的输入图像周围填 0,则输出特征尺寸同为 5 × 5 5×5 5×5。
卷积输出特征图维度计算: p a d d i n g = { S A M E ( 使 用 全 0 填 充 ) 输 入 特 征 图 边 长 步 长 ( 向 上 取 整 ) V A L I D ( 不 使 用 全 0 填 充 ) 输 入 特 征 图 边 长 − ( 卷 积 核 边 长 − 1 ) 步 长 ( 向 上 取 整 ) padding=\begin{cases} SAME (使用全0填充) & \frac{输入特征图边长}{步长} & (向上取整) \\ \\ VALID (不使用全0填充) & \frac{输入特征图边长-(卷积核边长-1)}{步长} & (向上取整) \end{cases} padding=⎩⎪⎨⎪⎧SAME(使用全0填充)VALID(不使用全0填充)步长输入特征图边长步长输入特征图边长−(卷积核边长−1)(向上取整)(向上取整)
TensorFlow描述全零填充: 用参数padding = ‘SAME’ 或 padding = ‘VALID’表示。
Tensorflow给出了计算卷积的函数:
tf.keras.layers.Conv2D (filters = 卷积核个数, # 即卷积层输出特征图深度
kernel_size = 卷积核尺寸, # 正方形写核长整数,或(核高h,核宽w)
strides = 滑动步长, # 横纵向相同写步长整数,或(纵向步长h,横向步长w),默认1
padding = “same” or “valid”, # 使用全零填充是“same”, 不使用是“valid”(默认“valid”)
activation = “ relu ” or “ sigmoid ” or “ tanh ” or “ softmax”等, # 如果在该层之后使用了BN,则此处不使用激活函数
input_shape = (高, 宽 , 通道数) #输入特征图维度,可省略
)
举个栗子:
model = tf.keras.models.Sequential([
Conv2D(6, 5, padding='valid', activation='sigmoid'),
MaxPool2D(2, 2),
Conv2D(6, (5, 5), padding='valid', activation='sigmoid'),
MaxPool2D(2, (2, 2)),
Conv2D(filters=6, kernel_size=(5, 5),padding='valid', activation='sigmoid'),
MaxPool2D(pool_size=(2, 2), strides=2),
Flatten(),
Dense(10, activation='softmax')
])
神经网络对0附近的数据更敏感。但是随着网络层数的增加,特征数据会出现偏离0均值的情况。
批标准化后,第 k k k 个卷积核的输出特征图(feature map)中第 i i i 个像素点的值: H i ′ k = H i k − μ b a t c h k σ b a t c h k H_i'^k=\frac{H_i^k-\mu_{batch}^k}{\sigma_{batch}^k} Hi′k=σbatchkHik−μbatchk其中, H i k H_i^k Hik表示批标准化前,第 k 个卷积核,输出特征图中第 i i i 个像素点的值; μ b a t c h k = 1 m ∑ i = 1 m H i k \mu_{batch}^k=\frac{1}{m}\sum_{i=1}^mH_i^k μbatchk=m1∑i=1mHik表示批标准化前,第k个卷积核, batch张输出特征图中所有像素点平均值; σ b a t c h k = δ + 1 m ∑ i = 1 m ( H i k − μ b a t c h k ) \sigma_{batch}^k=\sqrt{\delta+\frac{1}{m}\sum_{i=1}^m(H_i^k-\mu_{batch}^k)} σbatchk=δ+m1∑i=1m(Hik−μbatchk)表示批标准化前,第k个卷积核, batch张输出特征图中所有像素点标准差。
一共包含 n n n 个卷积核, b a t c h = 32 batch=32 batch=32,每个卷积核对一个 b a t c h = 32 batch=32 batch=32 的输入特征图进行一次卷积计算输出 32 32 32 张特征图。 μ b a t c h k \mu_{batch}^k μbatchk 即对这 32 张特征图的所有像素值求平均; σ b a t c h k \sigma_{batch}^k σbatchk 即对这 32 张特征图的所有像素值求标准差。
如上图所示,BN操作将原本偏移的数据重新拉回标准正态分布,然后再进入激活函数,使进入激活函数的数据分布在激活函数线性区。 使得输入数据的微小变化,更明显的提现到激活函数的输出,提升了激活函数对输入数据的区分力。
但是,这种标准化方法使得特征数据完全满足标准正态分布,集中在激活函数中心的线性区域,使激活函数丧失了非线性特性,因此在BN操作中为每个卷积核引入可训练参数:缩放因子 γ \gamma γ 和偏移因子 β \beta β,调整批归一化的力度。 X i k = γ k H i ′ k + β k X_i^k=\gamma_kH_i'^k+\beta_k Xik=γkHi′k+βk 在反向传播时,缩放因子 γ \gamma γ 和偏移因子 β \beta β,会与其他可训练参数一同被优化。 使标准正态分布后的特征数据通过缩放因子和偏移因子优化了特征数据分布的宽窄和偏移量,保证了网络的非线性表达力。
BN层位于卷积层之后,激活层之前。
TensorFlow API: tf.keras.layers.BatchNormalization()
model = tf.keras.models.Sequential([
Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷积层
BatchNormalization(), # BN层
Activation('relu'), # 激活层
MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化层
Dropout(0.2), # dropout层
])
training = True
时,BN 操作采用当前 batch 的均值和标准差;training = False
时,BN 操作采用滑动平均(running)的均值和标准差。池化(pooling): 池化操作用于减少卷积神经网络中的特征参数量。
池化的主要方法有 最大池化 和 均值池化 。最大池化可以提取图片纹理,均值池化可以保留背景特征,如果使用 2 ∗ 2 2*2 2∗2 的池化核对输入图片以步长 2 进行池化,输出图片将变为输入图片的 1/4 大小。
TensorFlow API:
tf.keras.layers.MaxPool2D(pool_size=池化核尺寸, # 正方形写核长整数,或(核高h,核宽w)
strides=池化步长, # 步长整数, 或(纵向步长h,横向步长w),默认为pool_size
padding=‘valid’or‘same’) # 使用全零填充是“same”, 不使用是“valid”(默认)
tf.keras.layers.AveragePooling2D(pool_size=池化核尺寸, # 正方形写核长整数,或(核高h,核宽w)
strides=池化步长, # 步长整数, 或(纵向步长h,横向步长w),默认为pool_size
padding=‘valid’or‘same’) # 使用全零填充是“same”, 不使用是“valid”(默认)
举个栗子:
model = tf.keras.models.Sequential([
Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷积层
BatchNormalization(), # BN层
Activation('relu'), # 激活层
MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化层
Dropout(0.2), # dropout层
])
舍弃(Dropout): 在神经网络的训练过程中, 将隐含层的部分神经元按照一定概率从神经网络中暂时舍弃,使用时被舍弃的神经元恢复链接。
TensorFlow API: tf.keras.layers.Dropout(舍弃的概率)
举个栗子:
model = tf.keras.models.Sequential([
Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷积层
BatchNormalization(), # BN层
Activation('relu'), # 激活层
MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化层
Dropout(0.2), # dropout层随机舍弃20%的神经元
])
卷积神经网络:通过卷积层借助卷积核提取特征后,送入全连接网络,进行识别预测任务。
卷积神经网络主要模块:
卷积层→BN层→激活层→池化层→Dropout层→FC层
卷积层的实际作用就是一个特征提取器,就是CBAPD。
卷积特征提取借助卷积核实现参数空间共享,借助卷积计算层提取空间特征后,送入全连接网络。
提供 5 万张 32 ∗ 32 32*32 32∗32 像素点的十分类彩色图片和标签,用于训练。
提供 1 万张 32 ∗ 32 32*32 32∗32 像素点的十分类彩色图片和标签,用于测试。
导入cifar10数据集:
cifar10 = tf.keras.datasets.cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
观察CIFAR10数据集:
搭建一个一层卷积、两层全连接的网络。
卷积层搭建示例:
“六步法“搭建卷积神经网络示例:
运行结果:
接下来,将使用“六步法”实现LeNet、AlexNet、VGGNet、InceptionNet、ResNet卷积神经网络。
LeNet 1 由Yann LeCun于1998年提出,卷积网络开篇之作。卷积网络通过共享卷积核,减少了网络的参数。
在统计神经网络层数时,一般只统计卷积计算层和全连接计算层,其余操作可以认为是卷积计算层的附属操作。
LeNet一共有五层网络,包含两层卷积层和三层全连接层。LeNet提出时,还没有BN操作和Dropout操作,该时代的主流激活函数为sigmoid激活函数。
写出对应网络代码:
运行结果:
AlexNet 2 络诞生于2012年,是Hinton的代表作之一,当年ImageNet竞赛的冠军, Top5错误率为16.4%。AlexNet 使用了Relu激活函数,提升了训练速度,使用Dropout缓解了过拟合
AlexNet共有8层,包含5层卷积层和3层全连接层。
注:原文使用LRN(local response normalization) 局部响应标准化,本课程使用BN(Batch Normalization)替代。 近年来,LRN用的很少,其功能与BN相似。
写出对应网络代码:
运行结果:
VGGNet 3 诞生于2014年,当年ImageNet竞赛的亚军, Top5错误率减小到7.3%。
VGGNet使用小尺寸卷积核,在减少参数的同时,提高了识别准确率。VGGNet的网络结构规整,非常适合硬件加速。
以16层VGGNet网络为例: 包含13层卷积层和3层FC层。各层卷积核个数从64逐渐增加至512,越靠后,特征图尺寸越小,通过增加卷积核的个数,增加了特征图深度,保持了信息的承载能力。
写出对应网络代码:
运行结果:
InceptionNet 4 诞生于2014年,当年ImageNet竞赛冠军, Top5错误率为6.67%。
InceptionNet引入了Inception结构块,在同一层网络内使用不同尺寸的卷积核,提升了模型感知力,使用了BN,缓解了梯度消失。
InceptionNet的核心是他的基本单元Inception结构块。 无论是 GooGLeNet——Inception v1 ,还是InceptionNet的后续版本(v2,v3,v4)都是基于Inception结构块搭建的网络。
Inception结构块在同一层网络中使用了多个尺寸的卷积核,可以提取不同尺度的特征。
下图为一个Inception结构块的示意图:
Inception结构块共包含4个分支,分别为:
送到卷积连接器的特征数据尺寸相同。 卷积连接器会将收到的这四路特征数据按深度方向拼接,生成Inception结构块的输出。
编写代码实现Inception结构块:
因为Inception结构块中的所有卷积均为CBA无PD结构,使用编写 class ConvBNRelu(Model):
来减小代码长度。
在 class ConvBNRelu(Model):
基础上使用 class
实现Inception结构块:
下图中,x = tf.concat([x1, x2_2, x3_3, x4_2], axis=3)
指按照 axis=3 深度方向拼接四路输出特征。
有了Inception结构块后,就可以搭建一个精简的InceptionNet了。网络共包含10层,分别为:
self.out_channels *= 2
。self.out_channels *= 2
将通道数加倍,所以block_1通道数为block_0的两倍,即block_1的通道数为32,经过了4个分支,输出的深度为 4 ∗ 32 = 128 4*32=128 4∗32=128.编写代码实现:
定义类Inception10时,设定了默认 init_ch=16 ,即默认输出深度为16,定义 class ConvBNRelu(Model):
时,定义了默认卷积核边长为3,步长为1,全零填充。
ResNet 5 诞生于2015年,当年ImageNet竞赛冠军, Top5错误率为3.57%。
ResNet 提出了层间残差跳连,引入了前方信息,缓解了梯度消失,使得更深的神经网络的实现成为可能。前几小节介绍的LeNet、AlexNet、VGGNet和InceptionNet的层数:
可见,在探索卷积实现特征提取的道路上,通过加深网络层数,取得了越来越好的效果。而ResNet的作者何凯明在CIFAR10数据集上的实验发现56层卷积网络错误率高于与20层卷积网络。
作者认为,单纯堆叠网络层数,会使神经网络模型发生 退化现象 ,以至于后面的特征丢失了前面特征的原本模样。所以,作者引入了 跳跃连接 将前面的特征直接接到了后面。使得输出结果 H ( x ) H(x) H(x)包含了堆叠卷积的非线性输出 F ( x ) F(x) F(x) 和跳过这两层堆叠卷积层,直接连接的恒等映射 x x x ,即 H ( x ) = F ( x ) + x H(x)=F(x)+x H(x)=F(x)+x。这一操作有效缓解了深层神经网络层数过多所导致的 退化现象 。使得神经网络可以向着更深层发展。
注: ResNet中的特征图相加与InceptionNet中的特征图相加方式不同:
ResNet 结构块中的跳跃连接有两种情况:
对应这两种不同的ResNet结构块,写出代码:
下图为ResNet-18的网络结构:
第一层为一个卷积,然后为8个ResNet结构块共16层卷积层,每两个ResNet结构块构成一个ResNet block,接着通过全局平均池化层,最后进入十分类全连接层。
根据ResNet结构块和ResNet-18网络结构编写出相应代码:
class ResNet18(Model):
def __init__(self, block_list, initial_filters=64): # block_list表示每个block有几个卷积层
super(ResNet18, self).__init__()
self.num_blocks = len(block_list) # 共有几个block
self.block_list = block_list
self.out_filters = initial_filters
self.c1 = Conv2D(self.out_filters, (3, 3), strides=1, padding='same', use_bias=False)
self.b1 = BatchNormalization()
self.a1 = Activation('relu')
self.blocks = tf.keras.models.Sequential()
# 构建ResNet网络结构
for block_id in range(len(block_list)): # 第几个resnet block
for layer_id in range(block_list[block_id]): # 第几个卷积层
if block_id != 0 and layer_id == 0: # 对除第一个block以外的每个block的输入进行下采样
block = ResnetBlock(self.out_filters, strides=2, residual_path=True)
else:
block = ResnetBlock(self.out_filters, residual_path=False)
self.blocks.add(block) # 将构建好的block加入resnet
self.out_filters *= 2 # 下一个block的卷积核数是上一个block的2倍
self.p1 = tf.keras.layers.GlobalAveragePooling2D()
self.f1 = tf.keras.layers.Dense(10, activation='softmax', kernel_regularizer=tf.keras.regularizers.l2())
def call(self, inputs):
x = self.c1(inputs)
x = self.b1(x)
x = self.a1(x)
x = self.blocks(x)
x = self.p1(x)
y = self.f1(x)
return y
当网络框架搭建好后,还需要尝试更改学习率等超参数,微调网络结构,通过一些优化策略,提升CIFAR10测试集的准确率。当模型在CIFAR10测试集的准确率达到90%以上时,则模型效果较好。
本文仅为笔者 TensorFlow 学习笔记,部分图文来源于网络,若有侵权,联系即删。
Yann Lecun, Leon Bottou, Y. Bengio, Patrick Haffner. Gradient-Based Learning Applied to Document Recognition. Proceedings of the IEEE, 1998. ↩︎
Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton. ImageNet Classification with Deep Convolutional Neural Networks. In NIPS, 2012. ↩︎
K. Simonyan, A. Zisserman. Very Deep Convolutional Networks for Large-Scale Image Recognition. In ICLR, 2015. ↩︎
Szegedy C, Liu W, Jia Y, et al. Going Deeper with Convolutions. In CVPR, 2015. ↩︎
Kaiming He, Xiangyu Zhang, Shaoqing Ren. Deep Residual Learning for Image Recognition. In CPVR, 2016. ↩︎