本篇介绍卷积神经网络。今年来深度学习在计算机视觉领域取得突破性成果的基石。目前的工业场景应用也是越来越多,比如自然语言处理、推荐系统和语音识别等领域广泛使用。下面会主要描述卷积神经网络中卷积层和池化层的工作原理,并解释填充、步幅、输入通道和输出通道的含义。
后面也会介绍一点比较有代表性的神经网络网络结构,比如:AlexNet、VGG、NiN、GoogLeNet、ResNet、DenseNet。
什么是二维卷积层?
【二维互相关运算】
【二维卷积层】
【图像中物体边缘检测】
【互相关运算和卷积运算】
【特征图和感受野】
上面提到了卷积运算,我们知道,使用3 x 3 的输入,与2 x 2 的卷积核得到的输出是 2 x 2 ;一般来说,假设输入的形状是 n h ∗ n w n_h * n_w nh∗nw,卷积核窗口形状是 k h ∗ k w k_h*k_w kh∗kw,那么输出形状将会是:
( n h − k h + 1 ) ∗ ( n w − k w + 1 ) . (n_h - k_h + 1)*(n_w - k_w +1). (nh−kh+1)∗(nw−kw+1).
所以,卷积层的输出形状有输入形状和卷积核窗口形状决定。下面介绍的卷积层的两个超参数,即填充和步幅。它们可以对给定形状的输入和卷积核改变输出形状。
【填充】
【步幅】
上面所说的输入和输出都是二维的数组,但真实数据的维度经常更高。例如,彩色图像在高和宽2个维度外还有RGB(红、绿、蓝)3个颜色通道,假设彩色图像的高和宽分别为h和w(像素),那么它可以表示为一个3hw的多维数组。我们将大小为3的这一维称为通道(channel)维。下面就介绍含多个输入通道或多个输出通道的卷积核。
【多输入通道】
【多输出通道】
【卷积层】
在“二维卷积层”中,有说过图像物体边缘检测的应用,我们构造卷积核从而精确找到了像素变化的位置。
【二维最大池化层和平均池化层】
【填充和步幅】
【多通道】
【概括】
在前面使用的,一个含有单隐层的多层感知机模型对MNIST数据集中的图像进行分类。每张图像高和宽均是28像素。将每个图像进行展开,得到长度为784的向量,并输入进全连接层。然而,这种分类方法有一定的局限性。
卷积层尝试决绝这两个问题:
卷积神经网络就是含有卷积层的网络。LeNet可以说是神经网络的开山鼻祖,在这个网络中包含了前面提到的卷积层、池化层、激活层、全连接层,该网络结构最早被应用于手写数字图像识别,作者叫Yann LeCun,这也是LeNet名字的由来;LeNet展示了通过梯度下降训练卷积神经网络可以达到手写数字识别在当时最先进的结果,这个奠基性的工作第一次将卷积神经网络推上舞台,为众人所知。
【LeNet模型】
LeNet分为卷积层块和全连接层块两个部分:
这是改进后的LeNet-5版本,在原来的基础上进行了一些优化调整。
def LeNet():
# 定义模型
model = Sequential()
# conv1
model.add(Conv2D(32,(5,5),strides=(1,1),input_shape=(28,28,1),padding='valid',activation='relu',kernel_initializer='uniform'))
# max1
model.add(MaxPooling2D(pool_size=(2,2)))
# conv2
model.add(Conv2D(64,(5,5),strides=(1,1),padding='valid',activation='relu',kernel_initializer='uniform'))
# max2
model.add(MaxPooling2D(pool_size=(2,2)))
# 多通道压平
model.add(Flatten())
# fc1
model.add(Dense(500,activation='relu'))
# fc2
model.add(Dense(10,activation='softmax'))
return model
【概括】
在LeNet提出后的将近20年里,神经网络一度被其他机器学习方法超越,如SVM。虽然LeNet可以在早期的小数据集上取得好的成绩,但是在更大的真实数据集上的表现并不是很令人满意。
在上面的LeNet中看到,神经网络可以直接基于图像分类的原始像素进行分类,这种称为端到端(end - to - end)的方法节省了很多中间步骤。然而,在很长一段时间里更流行的是研究者通过勤劳与智慧所涉及并生成的手动特征,这类图像分类研究的主要流程是:
当时认为的机器学习部分仅限最后这一步,如果那时候跟机器学习研究者交谈,他们认为机器学习既重要又优美。优美的定理证明了许多分类器的性质。机器学习领域生机勃勃、严谨而且机器有用。然而,如果跟计算机视觉研究者交谈,则是另外一幅景象。他们会告诉你图像识别里“不可告人”的现实是:计算机视觉流程中真正重要的是数据和特征。也就是说,使用较干净的数据和较有效的特征甚至比机器学习模型的选择对图像分类结果的影响更大。
【学习特征表示】
特征如此的重要,如何去表征他就成为了一个很关键的问题。
在上面的学习中,特征的提取方式有两种:
尽管,有很多人在这一表征方式的方向上进行着多种研究,但是在很长一段时间内,这个表征方式并没有被实现,至于原因大致有两方面原因:
【AlexNet】
1.conv1层,使用较大的11 x 11窗口来捕获物体。同时使用步幅 4 来较大幅度减小输出高和宽。这里使用的输出通道为48,比LeNet的通道数多很多。
2.max1层,池化核尺寸为3 x 3,步幅为2,减小卷积的窗口
3.conv2层,卷积核尺寸为5 x 5,使用填充为padding = 2来使得输入与输出的高和宽一致,且增大了输出通道数。
4.max2层,池化核尺寸为3 x 3,步幅为2,减小卷积的窗口
5.conv3层,卷积核尺寸为3 x 3,步幅为1,自动填充
6.conv4层,卷积核尺寸为3 x 3,步幅为1,自动填充
7.conv5层,卷积核尺寸为3 x 3,步幅为1,自动填充
8.max3层,池化核尺寸为3 x 3,步幅为2,减小卷积的窗口
9.fc1、fc2层,全连接层的输出个数比LeNet中的大数倍,使用dropout来缓解过拟合
10.fc3输出层,根据需要输出类别个数。
下面给出基于Keras的一个实现
def AlexNet():
# 定义模型
model = Sequential()
# conv1,卷积核11 * 11,步长4,第一层要指定输入的形状
model.add(Conv2D(96,(11,11),strides=(4,4),input_shape=(227,227,3),padding='valid',activation='relu',kernel_initializer='uniform'))
# Max1,池化核3 * 3,步长2
model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))
# conv2,卷积核5 * 5,自动padding
model.add(Conv2D(256,(5,5),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
# Max2,池化核3 * 3 ,步长2
model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))
# conv3,卷积核 3 * 3,步长1,连续3个卷积层
model.add(Conv2D(384,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(384,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
# Max3,池化核3 * 3,步长2
model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))
# 向量化
model.add(Flatten())
# FC1,全连接,后面紧接一个dropout,降低复杂度
model.add(Dense(4096,activation='relu'))
model.add(Dropout(0.5))
# FC2, 全连接
model.add(Dense(4096,activation='relu'))
model.add(Dropout(0.5))
# FC3,输出层,多分类softmax作用
model.add(Dense(1000,activation='softmax'))
return model
【概括】
AlexNet在LeNet的基础之上增加了3个卷积层。但AlexNet作者对他们的卷积窗口、输出通道数和构造顺序做了大量的调整。虽然AlexNet指明了深度卷积神经网络可以取得出色的结果,但并没有提供简单的规则以指导后来的研究者如何设计新的网络。
下面提到的VGG网络结构,提出了可以通过重复使用简单的基础块来构建深度模型的思路。
【VGG块】
【VGG网络模型】
上述的网络结构不再逐层分析,大致是前面的卷积部分和后面的全连接部分,而全连接部分也是平移了AlexNet的3层全连接。
VGG的特点:
小卷积核的有点:
下面给出基于Keras的一个实现
def VGG_16():
# 定义模型
model = Sequential()
# vgg_block1,2个卷积层,后面接一个池化
model.add(Conv2D(64,(3,3),strides=(1,1),input_shape=(224,224,3),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(64,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(2,2)))
# vgg_block2,2个卷积层,后面接一个池化
model.add(Conv2D(128,(3,2),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(128,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(2,2)))
# vgg_block3,3个卷积层,后面接一个池化
model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(2,2)))
# vgg_block4,3个卷积层,后面接一个池化
model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(2,2)))
# vgg_block5,3个卷积层,后面接一个池化
model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Flatten())
# 2个FC层,隐藏层
model.add(Dense(4096,activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(4096,activation='relu'))
model.add(Dropout(0.5))
# 1个FC层,输出层
model.add(Dense(1000,activation='softmax'))
return model
【概括】
前面介绍了LeNet、AlexNet、VGG网络结构,三者在设计上的共同之处是:先以由卷积层构成的模块充分抽取空间特征,再以由全连接层构成的模块来输出分类结果(简单说就是:卷积层+全连接层)。其中,AlexNet和VGG对LeNet的改进主要在于如何对这两个模块加宽(增加通道数)和加深(增加层)。下面介绍的NiN(Network in Network)网络,它提出了另外的一个思路,即串联多个由卷积层和“全连接”层构成的小网络来构建一个深层网络。
【两个重要概念】
【NiN块】
from mxnet import gluon,init,nd
from mxnet.gluon import nn
def nin_block(num_channels,kernel_size,strides,padding):
"""
构建一个NiN块,这个块也叫做MLPconv(多层感知卷积层),其实就是:传统卷积层+1 x 1卷积层。
用这个NiN块代替传统卷积可以增强网络提取抽象特征和泛化能力。
Parameters:
----------------
num_channels:通道数,也就是卷积后的厚度
kernel_size:卷积核的形状
strides:步幅
padding:填充
return:
----------------
blk:定义好的一个mlpconv结构
"""
blk = nn.Sequential()
blk.add(nn.Conv2D(num_channels,kernel_size,strides,padding,activation = 'relu'),
nn.Conv2D(num_channels,kernel_size = 1,activation = 'relu'),
nn.Conv2D(num_channels,kernel_size = 1,activation = 'relu'))
return blk
【NiN模型】
NiN是AlexNet问世不久后提出的。他们的卷积层设定有类似之处。NiN使用卷积窗口形状分别为11 x 11、5 x 5和3 x 3 的卷积层,相应的输出通道数也与AlexNet中的一致。每个NiN块后接一个步幅为2、窗口形状为3 x 3的最大池化层。
除了使用NiN块以外NiN还有一个设计与AlexNet显著不同:NiN去掉了AlexNet最后的3个全连接层,取而代之的,NiN使用了输出通道数 = 标签类别数的NiN块,然后使用全局平均池化层对每个通道中所有元素求平均并直接用于分类。这里的全军平均池化层即窗口形状等于输入空间维度形状的平均池化层。NiN的这个设计的好处是可以显著减小模型参数尺寸,从而缓解过拟合。然而该设计有时会造成获得有效模型的训练时间的增加。
net = nn.Sequential()
# 前面的卷积层部分,用mlpconv代替了传统的方式;后面的全连接部分用NiN块结合全局平均处理
net.add(nin_block(96,kernel_size = 11,strides = 4,padding = 0),
nn.MaxPool2D(pool_size = 3,strides = 2),
nin_block(256,kernel_size=5,strides = 1,padding =2),
nn.MaxPool2D(pool_size = 3,strides = 2),
nin_block(384,kernel_size = 3,strides = 1,padding = 1),
nn.MaxPool2D(pool_size = 3,strides =2),
nn.Dropout(0.5),
# 后面部分就是AlexNet的'全连接部分'
# 类别标签,类别数为10,因为使用的手写体
nin_block(10,kernel_size = 3,strides = 1,padding = 1),
# 全局平均池化层将窗口形状自动设置为输入的高和宽
nn.GlobalAvgPool2D(),
# 将四维的输入转化成二维的输出,其形状为(批量大小,10)
nn.Flatten())
X = nd.random.uniform(shape=(1,1,244,244))
net.initialize()
for layer in net:
X = layer(X)
print(layer.name,'output shape :\t',X.shape)
[out]:sequential6 output shape : (1, 96, 59, 59)
pool4 output shape : (1, 96, 29, 29)
sequential7 output shape : (1, 256, 29, 29)
pool5 output shape : (1, 256, 14, 14)
sequential8 output shape : (1, 384, 14, 14)
pool6 output shape : (1, 384, 6, 6)
dropout1 output shape : (1, 384, 6, 6)
sequential9 output shape : (1, 10, 6, 6)
pool7 output shape : (1, 10, 1, 1)
flatten1 output shape : (1, 10)
from mxnet.gluon import data as gdata
import mxnet as mx
from mxnet.gluon import loss as gloss,nn
import os
import sys
import time
# 需要定义几个函数
def load_data_fashion_mnist(batch_size, resize=None, root=os.path.join('~', '.mxnet', 'datasets', 'fashion-mnist')):
"""
用于加载‘fashion-mnist’数据集,并返回一定批次的训练集和测试集
"""
root = os.path.expanduser(root) # 展开用户路径'~'
transformer = []
if resize:
transformer += [gdata.vision.transforms.Resize(resize)]
transformer += [gdata.vision.transforms.ToTensor()]
transformer = gdata.vision.transforms.Compose(transformer)
mnist_train = gdata.vision.FashionMNIST(root=root, train=True)
mnist_test = gdata.vision.FashionMNIST(root=root, train=False)
num_workers = 0 if sys.platform.startswith('win32') else 4
train_iter = gdata.DataLoader(
mnist_train.transform_first(transformer), batch_size, shuffle=True,
num_workers=num_workers)
test_iter = gdata.DataLoader(
mnist_test.transform_first(transformer), batch_size, shuffle=False,
num_workers=num_workers)
return train_iter, test_iter
def try_gpu():
"""
如果有GPU就优先使用,否则使用CPU
"""
try:
ctx = mx.gpu()
_ = nd.zeros((1,), ctx=ctx)
print('use gpu')
except mx.base.MXNetError:
ctx = mx.cpu()
print('use cpu')
return ctx
def evaluate_accuracy(data_iter, net, ctx):
"""
用于评估模型
"""
acc_sum, n = nd.array([0], ctx=ctx), 0
for X, y in data_iter:
# 如果ctx代表GPU及相应的显存,将数据复制到显存上
X, y = X.as_in_context(ctx), y.as_in_context(ctx).astype('float32')
acc_sum += (net(X).argmax(axis=1) == y).sum()
n += y.size
return acc_sum.asscalar() / n
def train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx,num_epochs):
"""
定义训练器
"""
print('training on', ctx)
loss = gloss.SoftmaxCrossEntropyLoss()
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time()
for X, y in train_iter:
X, y = X.as_in_context(ctx), y.as_in_context(ctx)
with autograd.record():
y_hat = net(X)
l = loss(y_hat, y).sum()
l.backward()
trainer.step(batch_size)
y = y.astype('float32')
train_l_sum += l.asscalar()
train_acc_sum += (y_hat.argmax(axis=1) == y).sum().asscalar()
n += y.size
test_acc = evaluate_accuracy(test_iter, net, ctx)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, '
'time %.1f sec'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc,
time.time() - start))
##############################
# 开始训练
# 定义参数
lr,num_epochs,batch_size,ctx = 0.1,5,128,try_gpu()
# 初始化参数,初始化方式:Xavier
net.initialize(force_reinit= True,init=init.Xavier())
# 初始化训练器
trainer = gluon.Trainer(net.collect_params(),'sgd',{'learning_rate':lr})
# 加载数据集
train_iter,test_iter = load_data_fashion_mnist(batch_size,resize = 224)
# 训练模型
train_ch5(net,train_iter,test_iter,batch_size,trainer,ctx,num_epochs)
[out]:training on gpu(0)
epoch 1, loss 2.2493, train acc 0.190, test acc 0.373, time 24.4 sec
epoch 2, loss 1.7309, train acc 0.353, test acc 0.539, time 23.3 sec
epoch 3, loss 0.8903, train acc 0.659, test acc 0.761, time 23.4 sec
epoch 4, loss 0.6465, train acc 0.760, test acc 0.805, time 23.3 sec
epoch 5, loss 0.5096, train acc 0.812, test acc 0.831, time 23.3 sec
【三种网络结构】
【概括】
在2014年的ImageNet图像识别挑战赛中,一个叫做GoogLeNet的网络结构大放异彩。它虽然在名字上想LeNet致敬,但在网络结构上已经很难看到LeNet的影子了。GoogLeNet吸收了NiN中网络串联网络的思想,并在此基础上做了很大改进。在随后的几年里,研究人员对GoogLeNet进行了数次改进。下面介绍这个模型系列的第一个版本。
【Inception块】
from mxnet import gluon, init, nd
from mxnet.gluon import nn
class Inception(nn.Block):
# c1 - c4为每条线路⾥的层的输出通道数
def __init__(self, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# 线路1,单1 x 1卷积层
self.p1_1 = nn.Conv2D(c1, kernel_size=1, activation='relu')
# 线路2, 1 x 1卷积层后接3 x 3卷积层
self.p2_1 = nn.Conv2D(c2[0], kernel_size=1, activation='relu')
self.p2_2 = nn.Conv2D(c2[1], kernel_size=3, padding=1,activation='relu')
# 线路3, 1 x 1卷积层后接5 x 5卷积层
self.p3_1 = nn.Conv2D(c3[0], kernel_size=1, activation='relu')
self.p3_2 = nn.Conv2D(c3[1], kernel_size=5, padding=2,activation='relu')
# 线路4, 3 x 3最⼤池化层后接1 x 1卷积层
self.p4_1 = nn.MaxPool2D(pool_size=3, strides=1, padding=1)
self.p4_2 = nn.Conv2D(c4, kernel_size=1, activation='relu')
def forward(self, x):
p1 = self.p1_1(x)
p2 = self.p2_2(self.p2_1(x))
p3 = self.p3_2(self.p3_1(x))
p4 = self.p4_2(self.p4_1(x))
return nd.concat(p1, p2, p3, p4, dim=1) # 在通道维上连结输出
【GoogLeNet模型】
b1 = nn.Sequential()
b1.add(nn.Conv2D(64,kernel_size = 7,strides = 2,padding = 3,activation = 'relu'),
nn.MaxPool2D(poo_size = 3,strides = 2,padding = 1))
b2 = nn.Sequential()
b2.add(nn.Conv2D(64,kernel_size = 1,activation = 'relu'),
nn.Conv2D(192,kernel_size = 3,padding = 1,activation = 'relu'),
nn.MaxPool2D(pool_size = 3,strides = 2,padding = 1))
b3 = nn.Sequential()
b3.add(Inception(64,(96,128),(16,32),32),
Inception(129,(128,192),(32,96),64,)
nn.MaxPool2D(pool_size = 3,stides = 2,padding = 1))
b4 = nn.Sequential()
b4.add(Inception(192, (96, 208), (16, 48), 64),
Inception(160, (112, 224), (24, 64), 64),
Inception(128, (128, 256), (24, 64), 64),
Inception(112, (144, 288), (32, 64), 64),
Inception(256, (160, 320), (32, 128), 128),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
b5 = nn.Sequential()
b5.add(Inception(256, (160, 320), (32, 128), 128),
Inception(384, (192, 384), (48, 128), 128),
nn.GlobalAvgPool2D())
net = nn.Sequential()
net.add(b1, b2, b3, b4, b5, nn.Dense(10))
def Conv2d_BN(x, nb_filter,kernel_size, padding='same',strides=(1,1),name=None):
if name is not None:
bn_name = name + '_bn'
conv_name = name + '_conv'
else:
bn_name = None
conv_name = None
x = Conv2D(nb_filter,kernel_size,padding=padding,strides=strides,activation='relu',name=conv_name)(x)
x = BatchNormalization(axis=3,name=bn_name)(x)
return x
def Inception(x,nb_filter):
branch1x1 = Conv2d_BN(x,nb_filter,(1,1), padding='same',strides=(1,1),name=None)
branch3x3 = Conv2d_BN(x,nb_filter,(1,1), padding='same',strides=(1,1),name=None)
branch3x3 = Conv2d_BN(branch3x3,nb_filter,(3,3), padding='same',strides=(1,1),name=None)
branch5x5 = Conv2d_BN(x,nb_filter,(1,1), padding='same',strides=(1,1),name=None)
branch5x5 = Conv2d_BN(branch5x5,nb_filter,(1,1), padding='same',strides=(1,1),name=None)
branchpool = MaxPooling2D(pool_size=(3,3),strides=(1,1),padding='same')(x)
branchpool = Conv2d_BN(branchpool,nb_filter,(1,1),padding='same',strides=(1,1),name=None)
x = concatenate([branch1x1,branch3x3,branch5x5,branchpool],axis=3)
return x
def GoogLeNet():
inpt = Input(shape=(224,224,3))
#padding = 'same',填充为(步长-1)/2,还可以用ZeroPadding2D((3,3))
x = Conv2d_BN(inpt,64,(7,7),strides=(2,2),padding='same')
x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding='same')(x)
x = Conv2d_BN(x,192,(3,3),strides=(1,1),padding='same')
x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding='same')(x)
x = Inception(x,64)#256
x = Inception(x,120)#480
x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding='same')(x)
x = Inception(x,128)#512
x = Inception(x,128)
x = Inception(x,128)
x = Inception(x,132)#528
x = Inception(x,208)#832
x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding='same')(x)
x = Inception(x,208)
x = Inception(x,256)#1024
x = AveragePooling2D(pool_size=(7,7),strides=(7,7),padding='same')(x)
x = Dropout(0.4)(x)
x = Dense(1000,activation='relu')(x)
x = Dense(1000,activation='softmax')(x)
model = Model(inpt,x,name='inception')
return model
【概括】
先让我们思考一个问题,对于神经网络模型添加新的层,充分训练后的模型是否只可能更有更有效地降低训练误差?理论上,原模型解的空间只是新模型解的空间的子空间。也就是说,如果我们能将新添加的层训练成恒等映射 f ( x ) = x f(x) = x f(x)=x,新模型和原模型将同样有效。由于新模型可能得出更优 的解来拟合训练数据,因此添加层似乎更容易降低训练误差。然而在实践中,添加过多的层后训练误差往往不降反升。即使利用批量归一化带来的数值稳定性使训练深层模型更加容易。该问题仍然存在。
针对这一问题,何凯明等人提出了残差网络(ResNet),它在2015年的ImageNet图像识别赛夺冠,并深刻影响了后来的深度神经网络的设计。
【残差块】
让我们聚焦于神经网络局部。如下图,设输入为 x x x。假设我们希望学出的理想映射为 f ( x ) f(x) f(x)。从而作为下图5.9上方激活函数的输入。左图虚线框中的部分需要直接拟合出该映射 f ( x ) f(x) f(x),而右图虚线框中的部分则需要拟合出有关恒等映射的残差映射 f ( x ) − x f(x)-x f(x)−x。残差映射在实际中往往更容易优化。
以本节开头提到的恒等映射作为我们希望学出的理想映射 f ( x ) f(x) f(x)。我们只需要将图5.9中右图虚线框内上方的加权运算(如仿射)的权重和偏差学成0,那么 f ( x ) f(x) f(x)即为恒等映射。图5.9右图也是ResNet的基础块,即残差快(residual block)。在残差快中,输入可通过跨层的数据线路更快地向前传播。
ResNet沿用了VGG全3 x 3卷积层的设计。残差块里首先由2个相同输出通道数的3 x 3卷积层。每个卷积层后接一个批量归一化层和ReLU激活函数。然后我们将输入跳过这个两个卷积层后直接加在最后的ReLU激活函数前。这样的设计要求两个卷积层的输出与输入形状一样,从而可以相加。如果想要改变通道数,就需要引入一个额外的1 x 1卷积层来将输入变成需要的形状后再做相加运算。
残差块的实现如下,它可以设定输出通道、是否使用额外的1 x 1卷积层来修改通道数以及卷积层的步幅。
from mxnet.gluon import nn
from mxnet import gluon,init,nd
class Residual(nn.Block):
def __init__(self,num_channels,use_1x1conv = False,strides = 1,**kwargs):
super(Residual,self).__init__(**kwargs)
self.conv1 = nn.Conv2D(num_channels,kernel_size=3,padding=1,strides=strides)
self.conv2 = nn.Conv2D(num_channels,kernel_size=3,padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2D(num_channels,kernel_size=1,strides=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm()
self.bn2 = nn.BatchNorm()
def forward(self,X):
Y = nd.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3
return nd.relu(Y + X)
【ResNet模型】
net = nn.Sequential()
net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3),
nn.BatchNorm(),
nn.Activation('relu'),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
def resnet_block(num_channels, num_residuals, first_block=False):
blk = nn.Sequential()
for i in range(num_residuals):
if i == 0 and not first_block:
blk.add(Residual(num_channels, use_1x1conv=True, strides=2))
else:
blk.add(Residual(num_channels))
return blk
net.add(resnet_block(64, 2, first_block=True),
resnet_block(128, 2),
resnet_block(256, 2),
resnet_block(512, 2))
net.add(nn.GlobalAvgPool2D(), nn.Dense(10))
【概括】
ResNet中的跨层连接设计引申出了数个后续⼯作。本节我们介绍其中的⼀个:稠密连接⽹络
(DenseNet)。它与ResNet的主要区别如图5.10所⽰。
【稠密块】
from mxnet import gluon, init, nd
from mxnet.gluon import nn
def conv_block(num_channels):
blk = nn.Sequential()
blk.add(nn.BatchNorm(),
nn.Activation('relu'),
nn.Conv2D(num_channels, kernel_size=3, padding=1))
return blk
class DenseBlock(nn.Block):
def __init__(self, num_convs, num_channels, **kwargs):
super(DenseBlock, self).__init__(**kwargs)
self.net = nn.Sequential()
for _ in range(num_convs):
self.net.add(conv_block(num_channels))
def forward(self, X):
for blk in self.net:
Y = blk(X)
X = nd.concat(X, Y, dim=1) # 在通道维上将输⼊和输出连结
return X
【过渡层】
由于每个稠密块都会带来通道数的增加,使⽤过多则会带来过于复杂的模型。过渡层⽤来控制模型复杂度。它通过1 × 1卷积层来减小通道数,并使⽤步幅为2的平均池化层减半⾼和宽,从而进⼀步降低模型复杂度。
def transition_block(num_channels):
blk = nn.Sequential()
blk.add(nn.BatchNorm(),
nn.Activation('relu'),
nn.Conv2D(num_channels, kernel_size=1),
nn.AvgPool2D(pool_size=2, strides=2))
return blk
【DenseNet】
net = nn.Sequential()
net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3),
nn.BatchNorm(),
nn.Activation('relu'),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
num_channels, growth_rate = 64, 32 # num_channels为当前的通道数
num_convs_in_dense_blocks = [4, 4, 4, 4]
for i, num_convs in enumerate(num_convs_in_dense_blocks):
net.add(DenseBlock(num_convs, growth_rate))
# 上⼀个稠密块的输出通道数
num_channels += num_convs * growth_rate
# 在稠密块之间加⼊通道数减半的过渡层
if i != len(num_convs_in_dense_blocks) - 1:
num_channels //= 2
net.add(transition_block(num_channels))
net.add(nn.BatchNorm(),
nn.Activation('relu'),
nn.GlobalAvgPool2D(),
nn.Dense(10))
【概括】