将要介绍的5种卷积网络如下:
LetNet由Yann LeCun于1998年提出,是卷积网络的开篇之作。通过共享卷积核减少了网络的参数。在统计卷积神经网络层数时,一般只统计卷积计算层和全连接计算层,其余操作可以认为是卷积计算层的附属。
LeNet一共有5层网络,使用如下模型配置。
输入为32x32x3的特征图,使用2层卷积网络,3层全连接。
第一层卷积网络卷积核尺寸为5x5,个数为6个,步长为1。第二层卷积网络卷积核尺寸为5x5,卷积核个数为16个,步长为1。两层卷积层均不进行全0填充,均使用2x2池化核最大池化,且步长为2;使用三层全连接进行训练,第一层神经元个数为120个,第二层神经元个数为84个,第三层神经元个数为10个,均采用sigmoid函数作为激活函数。
class LeNet5(Model):
def __init__(self):
super(LeNet5, self).__init()
self.c1 = Conv2D(filters=6, Kernel_size(5,5), activation="sigmoid")
self.p1 = MaxPool2D(pool_size=(2,2), strides=2)
self.c2 = Conv2D(filters=16, kernel_size=(5,5), activation="sigmoid")
self.p2 = MaxPool2D(pool_size=(2,2), strides=2)
self.flatten=Flatten()
self.f1 = Dense(120,activation="sigmoid")
self.f2 = Dense(84, activation="sigmoid")
self.f3 = Dense(10,activation="sigmoid")
AlexNet使用relu激活函数提升了训练速度,使用Dropout减少了过拟合。
AlexNet共有8层,第一层使用了96个3x3卷积核,步长为1,不使用全零填充,原论文种使用局部响应标准化LRM,它的功能是批标准化相似,所以我们使用BN代替,使用relu激活函数,使用3x3的池化核,步长为2,做最大池化,不使用Droout
第二层使用了256个3x3卷积核,步长为1,不使用全零填充,实现BN操作实现特征标准化,使用relu激活函数,用3x3池化核,步长是2,做最大池化,不使用Dropout。
第三层使用了384个3x3卷积核,步长为1,使用全零填充,不使用BN操作,使用relu激活函数,不使用池化和Dropout。
第四层与第三层做了一样的操作。
第五层使用了256个3x3卷积核,步长为1,使用全零填充,不使用BN操作,使用relu激活函数,用3x3池化核,步长是2,做最大池化,不使用Dropout
第六,七,八层是全连接层,六七层使用2048个神经元,relu激活函数,50%Dropout,第八层使用10个神经元,用softmax使输出符合概率分布。
class AlexNet8(Model):
def __init__(self):
super(AlexNet8, self).__init__()
# 第一层
self.c1 = Conv2D(filters=96, kernel_size=(3,3))
self.b1 = BatchNormalization()
self.a1 = Activation("relu")
self.p1 = MaxPool2D(pool_size=(3,3),strides=2)
# 第二层
self.c2 = Conv2D(filters=256, kernel_size=(3,3))
self.b2 = BatchNormalization()
self.c2 = Activation("relu")
self.p2 = MaxPool2D(pool_size(3,3), strides=2)
# 第三层
self.c3 = Conv2D(filters=384, kernel_size=(3,3), padding="same", activation="relu")
# 第四层
self.c4 = Conv2D(filters=384, kernel_size=(3,3), padding="same", activation="relu")
# 第五层
self.c5 = Conv2D(filters=256, kernel=(3,3), padding="same", activation="relu")
self.p5 = MaxPool2D(pool_size=(3,3), strides=2)
# 第六层
self.flatten = Flatten()
self.f1 = Dense(2048,activation="relu")
self.d1 = Dropout(0.5)
self.f2 = Dense(2048, activation="relu")
self.d2 = Dropout(0.5)
self.f3 = Dense(10, activation="softmax")
VGGNet诞生于2014年,当年ImageNet竞赛的亚军,Top5错误率减小到7.3%。
VGGNet使用小尺寸卷积核,在减少参数的同时,提高了识别准确率。VGGNet网络结构规整,非常适合硬件加速。
以16层VGGNet为例,包括13层卷积层,3层全连接层。
网络结构是两次(CBA,CBAPD),三次(CBA,CBA,CBAPD),即CBA,CBAPD,CBA,CBAPD,CBA,CBA,CBAPD,CBA,CBA,CBAPD,CBA,CBA,CBAPD,最后是三层全连接。
卷积核的个数是从64(2层)到128(2层)到256(3层)到512(8层),逐渐增加,因为越靠后,特征图尺寸越小,通过增加卷积核的个数,增加了特征图的深度,保持了信息的承载能力。
所有卷积层数均采用3x3卷积核,步长为1,采用BN操作,激活函数为relu,采用2x2最大化池化核,步长为2,dropout为0.2。全连接层前两层的神经元个数为512个,激活函数为relu,dropout为0.2,第三层全连接层神经元个数为10个,激活函数为softmax。
模型搭建方法与上述模型的搭建方法一致,只是层数和参数不同而已。
InceptionNet诞生于2014年,当年ImageNet竞赛冠军,Top5错误率为6.67%
InceptionNet引入了Inception结构块,在同一层网络种使用了多个尺寸的卷积核提升了模型感知力,使用了批标准化缓解了梯度消失
Inception结构块在同一层网络中使用了多个尺寸的卷积核,可以提取不同尺寸的特征,通过1*1卷积核,作用到输入特征的每个像素点,通过设定少于输入特征图深度的1*1卷积核个数,减少了输出特征图深度,起到了降维的作用,减少了参数量和计算量。
Inception结构块包含四个分支,分别经过1*1卷积核输出到卷积连接器,经过1*1卷积核配合3*3卷积核输出到卷积连接器,经过1*1卷积核配合5*5卷积核输出到卷积连接器,经过3*3最大池化核配合1*1卷积核输出到卷积连接器,送到卷积连接器的特征数据尺寸相同,卷积连接器会把收到的四路特征数据按深度反向拼接,形成Inception结构块的输出。Inception结构块结构如下:
其中第一分支卷积采用了16个1*1卷积核,步长为1,全零填充,采用BN操作,relu激活函数;第二分支先用16个1*1卷积核降维,步长为1,采用BN操作,relu激活函数,再用16个3*3卷积核,步长为1,全零填充,采用BN操作,relu激活函数;第三分支先用16个1*1卷积核降维,步长为1,全零填充,采用BN操作,relu激活函数再用16个5*5卷积核,步长为1,全零填充,采用BN操作,relu激活函数;第四分支先采用最大池化,池化核尺寸是3*3,步长是1,全零填充,再用16个1*1卷积核降维,步长为1,全零填充,采用BN操作,relu激活函数。
卷机器把这四个分支按照深度反向堆叠在一起,构成Inception结构块的输出。由于Inception结构块中的卷积操作均采用了CBA结构,所以将其定义为以下的一个类:
class ConvBNRelu(Model):
def __init__(self, ch, kernelsz=3, strides=1, padding="same")
super(ConvBNRelu, self).__init__()
self.model = tf.keras.models.Sequential([
Conv2D(ch, kernelsz, strides=strides,padding=padding),
BatchNormalization(),
Activation("relu")
])
def call(self,x):
x = self.model(x)
return x
class InceptionBlk(Model):
def __init__(self, ch, strides=1):
super(InceptionBlk, self).__init__()
self.ch = ch
self.strides = strides
# 第一分支
self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
# 第二分支
self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=1)
# 第三分支
self.c3_1 = ConvBNRelu(ch, kernelsz=1. strides=strides)
self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=1)
# 第四分支
self.p4_1 = Maxpool2D(3, strides=1, padding="same")
self.c4_2 = ConvBNRelu(ch, kernelsz=1,strides=strides)
def call(self,x):
x1 = self.c1(x)
x2_1 = self.c2_1(x)
x2_2 = self.c2_2(x2_1)
x3_1 = self.c3_1(x)
x3_2 = self.c3_2(x3_1)
x4_1 = self.p4_1(x)
x4_2 = self.c4_2(x4_1)
x = tf.concat([x1,x2_2,x3_2,x4_2], axis=3) # axis=3表示堆叠方向是沿着深度方向
return x
第一层采用16个3*3卷积核,步长为1,全零填充,采用BN操作,relu激活。随后是4个Inception结构块顺序相连,每两个Inception结构块组成一个block,每个block中第一个Inception结构块的卷积步长是2,第二个Inception结构块卷积步长是1,这使得第一个Inception结构块输出特征图尺寸减半,因此,我们把输出特征图深度加深,尽可能保持特征抽取中信息的承载量一致。第一个block的起始深度是16,经过Inception结构后是64。第二个block起始是32,经过Inception结构后是128。之后将128个通道的数据送入平均池化,再送入10个分类的全连接。
class Inception10(Model):
def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):
super(Inception10, self).__init__()
self.in_channels = init_ch
self.out_channels = init_ch
self.num_blocks = num_blocks
self.init_ch = init_ch
# 第一层
self.c1 = ConvBNRelu(init_ch)
self.blocks = tf.keras.models.Sequential()
# 循环每个block
for block_id in range(num_blocks):
# 循环每个block中的Inception
for layer_id in range(2):
# 对于block中的第一个Inception,设置步长为2,使特征图尺寸减半
if layer_id == 0:
block = InceptionBlk(self.out_channels, strides=2)
# 用第二个Inception继续训练特征数据
else:
block = InceptionBLK(self.out_channels, strides=1)
self.blocks.add(block)
# 执行完第一个block后,由于特征尺寸已减半,所以加深通道保证信息量
self.out_channels *= 2
# 送入平均池
self.p1 = GlobalAveragePooling2D()
# 全连接训练
self.f1 = Dense(num_classes, activation="softmax")
def call(self,x):
x = self.c1(x)
x = self.blocks(x)
x = self.p1(x)
y = self.f1(x)
return y
ResNet诞生于2015年,当年的ImageNet冠军,Top5的错误率为3.75%。
ResNet提出了层间残差跳连,引入了前方信息,缓解梯度消失,使神经网络层数增加成为可能。我们总结以上模型的层数,如下表所示:
模型名称 | 网络层数 |
---|---|
LeNet | 5 |
AlexNet | 8 |
VGG | 16/19 |
InceptionNet v1 | 22 |
由上表可见,随着网络层数的增加,效果是越来越好的。但ResNet作者经过实验发现,56层卷积网络的错误率要高于20层。他认为单纯堆叠神经网络层数,会使神经网络模型退化,以至于后边的特征丢失了前边特征的原本模样,于是他用了一根跳连线,将前边的特征直接接到了后边 ,使输出结果F(x)包含了堆叠卷积的非线性输出F(x)和跳过两层卷积直接连过来的恒等映射x,让它们对应元素相加,这一操作有效缓解了神经网络模型堆叠导致的退化,使得神经网路可以向着更深层次发展,如下图所示:
在ResNet块中的"+“和Inception块中的”+“是不同的,Inception块中的”+“是沿深度方向叠加,ResNet块中的”+"是特征图对应元素相加 (矩阵值相加)
ResNet块中有两种情况,如下图所示:
一种情况用图中的实线表示,这种情况,两层堆叠卷积没有改变特征图的维度,也就是它们特征图的个数,高,宽和深度都相同,可以直接将F(x)和x相加;另一种情况用图中的虚线表示,这种情况中这两层堆叠卷积改变了特征图的维度,需要借助1*1的卷积来调整x的维度,使W(x)与F(x)的维度一致。
将ResNet的两种结构封装到一起,写出ResNet类,
import tensorflow as tf
from tensorflow.keras import Model
from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation
class ResNetBlock(Model):
def __init__(self, filters, strides=1, residual_path=False):
super(ResNetBlock, self).__init__()
self.filters = filters
self.strides = strides
self.residual_path = residual_path
self.c1 = Conv2D(filters, (3,3), strides=strides, padding="same", use_bias=False)
self.b1 = BatchNormalization()
self.a1 = Activation("relu")
self.c2 = Conv2D(filters, (3,3), strides=1, padding="same", use_bias=False)
self.b2 = BatchNormalization()
# residual_path为True时,对输入进行下采样,即用1*1的卷积核做卷积操作,保证x能和F(x)维度相同,顺利相加
if residual_path:
self.down_c1 = Conv2D(filters, (1, 1), strides=strides, padding="same",use_bias=False)
self.down_b1 = BatchNormalization()
self.a2 = Activation("relu")
def call(self, inputs):
residual = inputs
# 将输入通过卷积,BN,激活层,计算F(x)
x = self.c1(residual)
x = self.b1(x)
x = self.a1(x)
x = self.c2(x)
y = self.b2(x)
# 将输入特征转换为与F(x)相同即与y相同维度的特征数据
if self.residual_path:
residual = self.down_c1(inputs)
residual = self.down_b1(residual)
# 拼接数据
out = self.a2(y + residual)
return out
第一层是一个卷积层,之后是8个ResNet块,然后放入平均池化,最后送入10个分类的全连接
# 搭建ResNet18网络
class ResNet18(Model):
def __init__(self, block_list, initial_filters=64): # block_list表示每个block有几个卷积层
super(ResNet18, self).__init__()
self.num_block = 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, kernel_initializer="he_normal")
self.b1 = BatchNormalization()
self.a1 = Activation("relu")
self.blocks = tf.keras.Sequential()
# 构建ResNet网络结构
for block_id in range(self.num_block): # 第几个block
for layer_id in range(block_list[block_id]): # 第几个卷积层
if block_id != 0 and layer_id == 0: # 除第一个block,其余每个block的第一个ResNet进行下采样
block = ResNetBlock(self.out_filters, strides=2, residual_path=True)
else:
block = ResNetBlock(self.out_filters, strides=1, residual_path=False)
self.blocks.add(block)
self.out_filters *= 2
# 将数据加入到平均池化
self.p1 = tf.keras.layers.GlobalAveragePooling2D()
# 全连接
self.f1 = tf.keras.layers.Dense(10)
def call(self, inputs):
x = self.c1(inputs)
x = self.b1()
x = self.a1()
x = self.blocks(x)
x = self.p1(x)
y = self.f1(x)
return y
ResNet18([2,2,2,2])