主要参考学习资料:
《动手学深度学习》阿斯顿·张 等 著
【动手学深度学习 PyTorch版】哔哩哔哩@跟李牧学AI
概述
- 硬件性能和大数据的发展为深度卷积神经网络(AlexNet)的实现提供了条件。
- VGG首次将块的思想用于搭建网络。
- NiN将多层感知机应用在每个像素上以取代最终的全连接层。
- GoogLeNet将不同卷积核大小的卷积层并行连接。
- 批量规范化可加速深层网络的收敛。
- ResNet用函数参数化思想缓解了深层网络的退化问题。
- DenseNet是ResNet在函数展开上的逻辑扩展。
目录
- 7.1 深度卷积神经网络(AlexNet)
- 7.1.1 学习表征
- 1. 数据
- 2. 硬件
- 7.1.2 AlexNet
- 7.1.3 读取数据集
- 7.1.4 训练AlexNet
- 7.2 使用块的网络(VGG)
- 7.2.1 VGG块
- 7.2.2 VGG网络
- 7.2.3 训练模型
- 7.3 网络中的网络(NiN)
- 7.3.1 NiN块
- 7.3.2 NiN模型
- 7.3.3 训练模型
- 7.4 含并行连接的网络(GoogLeNet)
- 7.4.1 Inception块
- 7.4.2 GoogLeNet模型
- 7.4.3 训练模型
- 7.5 批量规范化
- 7.5.1 训练深层网络
- 7.5.2 批量规范化层
- 1.全连接层
- 2.卷积层
- 3.预测
- 7.5.3 从零实现
- 7.5.4 使用批量规范化层的LeNet
- 7.5.5 简明实现
- 7.6 残差网络(ResNet)
- 7.6.1 函数类
- 7.6.2 残差块
- 7.6.3 ResNet模型
- 7.6.4 训练模型
- 7.7 稠密连接网络(DenseNet)
- 7.7.1 从ResNet到DenseNet
- 7.7.2 稠密块体
- 7.7.3 过渡层
- 7.7.4 DenseNet模型
- 7.7.5 训练模型
7.1 深度卷积神经网络(AlexNet)
提出LeNet后的一段时间里,出于以下几点原因,神经网络未能超越机器学习主导图像分类领域:
- 硬件不足以开发出有大量参数的深层多通道多层卷积神经网络。
- 数据集相对较小。
- 训练神经网络的一些关键技巧仍然确实。
和神经网络训练端到端的系统不同,经典机器学习的过程如下:
- ①获取一个有趣的数据集。
- ②根据光学、几何学、其他知识以及偶然的发现,手动对特征数据集进行预处理。
- ③通过标准的特征提取算法或其他手动调整的流水线来输入数据。
- ④将提取的特征送入最喜欢的分类器中以训练分类器。
7.1.1 学习表征
另一个影响图像分类领域发展的重要因素是图像特征的提取方法。
在2012年前,图像特征是机械地计算出来的,设计一套新的特征函数、改进结果并撰写论文是盛极一时的潮流。
另一批研究人员认为特征本身应该被学习,并在合理的复杂性前提下,应该由多个共同学习的神经网络层组成,基于此提出的AlexNet在2012年ImageNet挑战赛中取得了轰动一时的成绩。
在AlexNet的底层,模型学习到了一些类似传统滤波器的特征提取器,而较高层建立在这些底层表示的基础上以表示更大的特征,更高的层可以检测整个物体,最终的隐藏神经元可以学习图像的综合表示。这一突破可归因于以下两个关键因素:
1. 数据
包含许多特征的深度模型需要大量有标签数据才能显著优于基于凸优化的传统方法,然而限于早期计算机有限的存储资源和研究预算,大部分研究只基于小的公开数据集。随着大数据的发展,直至2009年,拥有涵盖一千个类别的一百万多个样本的ImageNet数据集发布,以此为基础发起的挑战赛推动了计算机视觉和机器学习研究的发展。
2. 硬件
深度学习对计算资源要求很高,训练可能需要数百轮,每次迭代需要通过代价高昂的许多线性代数层传递数据,因此早期优化凸目标的简单算法是研究人员的首选。GPU训练神经网络改变了这一格局。
图形处理器(GPU)早期用来加速图形处理以服务于电脑游戏。GPU可优化高吞吐量的 4 × 4 4\times4 4×4矩阵和向量乘法,这些数学运算与卷积层的计算惊人地相似。
GPU相对于CPU的优势如下:
- CPU的核时钟频率高,性能高,但制造成本也高,需要大量芯片面积、复杂支持结构,且单任务性能相对较差,总体性价比不高。
- GPU由成百上千个小处理单元组成,通常被分成更大的组。单个核时钟频率低,性能较弱,但庞大的核数量使GPU比CPU快几个数量级。其原因在于功耗随时钟频率呈平方级增长,且GPU简单的内核更节能,最后GPU满足了深度学习需要的高内存带宽。
7.1.2 AlexNet
AlexNet与LeNet架构对比
卷积层(更多的层数和输出通道):
全连接层(更多的输出):
除此之外,AlexNet还采用了早期没有的ReLU激活函数和暂退法。
import torch
from torch import nn
from d2l import torch as d2l
from matplotlib import pyplot as pltnet = nn.Sequential( nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2), nn.Flatten(), nn.Linear(6400, 4096), nn.ReLU(), nn.Dropout(p=0.5), nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(p=0.5), nn.Linear(4096, 10)
)
7.1.3 读取数据集
训练ImageNet模型可能需要数小时乃至数天,因此我们仍使用Fashion-MNIST数据集,但为了使用AlexNet架构需要将其分辨率提高到 224 × 224 224\times224 224×224像素。
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
7.1.4 训练AlexNet
lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
plt.show()
训练结果:
loss 0.326, train acc 0.880, test acc 0.880
407.4 examples/sec on cuda:0
7.2 使用块的网络(VGG)
使用块的想法首先出现在VGG网络中。
7.2.1 VGG块
一个VGG块由一系列卷积层组成,再加上用于空间降采样的最大汇聚层。
import torch
from torch import nn
from d2l import torch as d2l
from matplotlib import pyplot as plt#参数分别为卷积层数量、输入通道数量、输出通道数量
def vgg_block(num_convs, in_channel, out_channel): layers = [] for _ in range(num_convs): layers.append(nn.Conv2d(in_channel, out_channel, kernel_size=3, padding=1)) layers.append(nn.ReLU()) in_channel = out_channel layers.append(nn.MaxPool2d(kernel_size=2, stride=2)) return nn.Sequential(*layers)
7.2.2 VGG网络
VGG网络由数个VGG块构成的卷积层和全连接层组成,每个VGG块由超参数conv_arch指定卷积层个数和每个卷积层的输出通道数。
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512)) def vgg(conv_arch): conv_blks = [] in_channels = 1 for (num_convs, out_channels) in conv_arch: conv_blks.append(vgg_block(num_convs, in_channels, out_channels)) in_channels = out_channels return nn.Sequential( *conv_blks, nn.Flatten(), nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 10)) net = vgg(conv_arch)
7.2.3 训练模型
由于VGG-11比AlexNet计算量更大,我们构建一个通道数较少的网络,足够用于训练Fashion-MNIST数据集。
#将每层的输出通道数除以4
ratio = 4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch) lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
plt.show()
训练结果:
loss 0.179, train acc 0.933, test acc 0.917
333.2 examples/sec on cuda:0
7.3 网络中的网络(NiN)
LeNet、AlexNet和VGG最终都使用全连接层对特征进行表征处理,然而全连接层可能会完全放弃表征的空间结构。NiN的解决方案是在每个像素的通道上分别使用多层感知机( 1 × 1 1\times1 1×1卷积层)。
7.3.1 NiN块
NiN块以一个普通卷积层开始,后面是两个 1 × 1 1\times1 1×1卷积层,它们充当带有ReLU激活函数的逐像素全连接层。
import torch
from torch import nn
from d2l import torch as d2l def nin_block(in_channels, out_channels, kernel_size, strides, padding): return nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding), nn.ReLU(), nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(), nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())
7.3.2 NiN模型
NiN使用卷积核大小分别为 11 × 11 11\times11 11×11、 5 × 5 5\times5 5×5、 3 × 3 3\times3 3×3的卷积层,输出通道数与AlexNet相同。每个NiN块后有一个最大汇聚层,汇聚窗口形状为 3 × 3 3\times3 3×3,步幅为 2 2 2。NiN完全取消了全连接层,而是使用一个输出通道数等于标签类别数的NiN块和一个全局平均汇聚层来生成对数几率。
net = nn.Sequential( nin_block(1, 96, kernel_size=11, strides=4, padding=0), nn.MaxPool2d(kernel_size=3, stride=2), nin_block(96, 256, kernel_size=5, strides=1, padding=2), nn.MaxPool2d(kernel_size=3, stride=2), nin_block(256, 384, kernel_size=3, strides=1, padding=1), nn.MaxPool2d(kernel_size=3, stride=2), nin_block(384, 10, kernel_size=3, strides=1, padding=1), nn.AdaptiveAvgPool2d(1), nn.Flatten())
7.3.3 训练模型
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
训练结果:
loss 0.310, train acc 0.885, test acc 0.885
385.2 examples/sec on cuda:0
7.4 含并行连接的网络(GoogLeNet)
GoogLeNet在2014年的ImageNet挑战赛大放异彩,它吸收并改进了NiN中串联网络的思想,还提出了使用不同大小的卷积核组合是有利的以解决多大的卷积核最合适的问题。
7.4.1 Inception块
Inception块由 4 4 4条并行路径组成。前 3 3 3条路径使用卷积核大小为 1 × 1 1\times1 1×1、 3 × 3 3\times3 3×3、 5 × 5 5\times5 5×5的卷积层,从不同的空间大小提取信息。中间的 2 2 2条路径在输入上执行 1 × 1 1\times1 1×1卷积以减少通道数从而降低模型复杂度。第 4 4 4条路径使用 3 × 3 3\times3 3×3最大汇聚层,然后使用 1 × 1 1\times1 1×1卷积层改变通道数。所有路径都使用合适的填充使输入和输出的高度和宽度一致,最后每条路径的输出在通道维度上合并构成Inception块的输出。Inception块的超参数是每层输出通道数。
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
from matplotlib import pyplot as pltclass Inception(nn.Module): def __init__(self, in_channels, c1, c2, c3, c4, **kwargs): super(Inception, self).__init__(**kwargs) #编号代表第几条路径的第几层self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1) self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1) self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1) self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1) self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2) self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1) self.p4_2 = nn.Conv2d(in_channels, c4[0], kernel_size=1) def forward(self, x): p1 = F.relu(self.p1_1(x)) p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) p4 = F.relu(self.p4_2(self.p4_1(x))) return torch.cat([p1, p2, p3, p4], dim=1)
7.4.2 GoogLeNet模型
GoogLeNet使用 9 9 9个Inception块和全局平均汇聚层的堆叠来生成其估计值,Inception块之间的最大汇聚层可降低维度。
模块一
- 64 64 64通道 7 × 7 7\times7 7×7卷积层
- 3 × 3 3\times3 3×3最大汇聚层
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
模块二
- 64 64 64通道 1 × 1 1\times1 1×1卷积层
- 192 192 192通道 3 × 3 3\times3 3×3卷积层
- 3 × 3 3\times3 3×3最大汇聚层
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1), nn.ReLU(), nn.Conv2d(64, 192, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
模块三
- 256 ( 64 + 128 + 32 + 32 ) 256(64+128+32+32) 256(64+128+32+32)通道Inception块
- 480 ( 128 + 192 + 96 + 64 ) 480(128+192+96+64) 480(128+192+96+64)通道Inception块
- 3 × 3 3\times3 3×3最大汇聚层
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32), Inception(256, 128, (128, 192), (32, 96), 64), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
模块四
- 512 ( 192 + 208 + 48 + 64 ) 512(192+208+48+64) 512(192+208+48+64)通道Inception块
- 512 ( 160 + 224 + 64 + 64 ) 512(160+224+64+64) 512(160+224+64+64)通道Inception块
- 512 ( 128 + 256 + 64 + 64 ) 512(128+256+64+64) 512(128+256+64+64)通道Inception块
- 528 ( 112 + 288 + 64 + 64 ) 528(112+288+64+64) 528(112+288+64+64)通道Inception块
- 832 ( 256 + 320 + 128 + 128 ) 832(256+320+128+128) 832(256+320+128+128)通道Inception块
- 3 × 3 3\times3 3×3最大汇聚层
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64), Inception(512, 160, (112, 224), (24, 64), 64), Inception(512, 128, (128, 256), (24, 64), 64), Inception(512, 112, (144, 288), (32, 64), 64), Inception(528, 256, (160, 320), (32, 128), 128), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
模块五
- 832 ( 256 + 320 + 128 + 128 ) 832(256+320+128+128) 832(256+320+128+128)通道Inception块
- 1024 ( 384 + 384 + 128 + 128 ) 1024(384+384+128+128) 1024(384+384+128+128)通道Inception块
- 全局平均汇聚层
- 展平层
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128), Inception(832, 384, (192, 384), (48, 128), 128), nn.AdaptiveAvgPool2d(1), nn.Flatten()) #再连接一个输出个数为标签类别数的全连接层
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))
7.4.3 训练模型
GoogLeNet模型计算复杂,而且不如VGG便于修改通道数,为缩短训练过程我们将输入的高度和宽度从 224 224 224降为 96 96 96。
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
训练结果:
loss 0.248, train acc 0.904, test acc 0.864
601.7 examples/sec on cuda:0
7.5 批量规范化
训练深层神经网络十分困难,特别是想要在较短的时间内使它们收敛。批量规范化可持续加速深层网络的收敛。
7.5.1 训练深层网络
神经网络需要批量规范化层的原因如下:
- 数据预处理的方式通常会对最终结果产生巨大影响。
- 中间层中的变量可能具有更广的变化范围,批量规范化的发明者非正式地假设变量分布中的这种偏移可能会阻碍网络的收敛。
- 更深层的网络更复杂,也更容易过拟合,意味着正则化变得更加重要。
另一种说法是,由于深层网络得到的梯度比浅层大,参数更新也会更快,因而当输入数据的分布发生变化时,深层网络的参数会受到更大的影响而极不稳定。
批量规范化的原理是在每次训练迭代中首先规范化输入,即减去其均值并除以其标准差,使其均值为 0 0 0,标准差为 1 1 1,再应用比例系数和比例偏移。只有使用足够大的小批量,批量规范化才是有效且稳定的。对于来自小批量 B B B的输入 x \boldsymbol x x:
B N ( x ) = γ ⊙ x − μ ^ B σ ^ B + β \mathrm{BN}(\boldsymbol x)=\gamma\odot\displaystyle\frac{\boldsymbol x-\hat{\boldsymbol\mu}_B}{\hat{\boldsymbol\sigma}_B}+\beta BN(x)=γ⊙σ^Bx−μ^B+β
由于标准化处理是一个主观选择,因此引入模型学习的与 x \boldsymbol x x形状相同的拉伸参数 γ \gamma γ和偏移参数 β \beta β。小批量 B B B的样本均值和标准差如下计算:
μ ^ B = 1 ∣ B ∣ ∑ x ∈ B x \hat{\boldsymbol\mu}_B=\displaystyle\frac1{|B|}\sum_{\boldsymbol x\in B}\boldsymbol x μ^B=∣B∣1x∈B∑x
σ ^ B 2 = 1 ∣ B ∣ ∑ x ∈ B ( x − μ ^ B ) 2 + ϵ \hat{\boldsymbol\sigma}^2_B=\displaystyle\frac1{|B|}\sum_{\boldsymbol x\in B}(\boldsymbol x-\hat{\boldsymbol\mu}_B)^2+\epsilon σ^B2=∣B∣1x∈B∑(x−μ^B)2+ϵ
小常量 ϵ > 0 \epsilon>0 ϵ>0确保分母不为零。
均值和方差的噪声估计抵消了缩放效应。事实证明,这些优化中的各种噪声源通常会实现更快的训练和较少的过拟合,但尚未在理论上明确证明。
7.5.2 批量规范化层
1.全连接层
通常我们将批量规范化层置于全连接层中的仿射变换与激活函数之间:
h = ϕ ( B N ( W x + b ) ) \boldsymbol h=\phi(\mathrm{BN}(\boldsymbol{Wx}+b)) h=ϕ(BN(Wx+b))
2.卷积层
卷积层可以在卷积层之后和非线性激活函数之前应用批量规范化。在多输出通道情况下,每个通道有自己的拉伸参数和偏移参数以对各自的输出执行批量规范化。假设小批量包含 m m m个样本,且对于每个通道卷积的输出高度为 p p p、宽度为 q q q,则我们在每个输出通道上的 m ⋅ p ⋅ q m\cdot p\cdot q m⋅p⋅q个元素进行批量规范化。
3.预测
批量规范化在训练和预测时的行为通常不同,我们不再需要样本噪声,也可能需要对单个样本进行预测。一种常用的方法是使用训练集移动平均所得的样本均值和方差来得到确定的输出。移动平均是在不知道全局数据的情况下对均值和方差进行动态估计的方法,其将过去样本的移动平均估计值与新样本的统计值加权相加。
7.5.3 从零实现
import torch
from torch import nn
from d2l import torch as d2l
from matplotlib import pyplot as pltdef batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum): #判断是预测模式还是训练模式 if not torch.is_grad_enabled(): #预测模式传入移动平均所得均值和方差X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps) else: #限定X来自全连接层或卷积层assert len(X.shape) in (2, 4) if len(X.shape) == 2: #全连接层计算特征维上的均值和方差mean = X.mean(dim=0) var = X.var(dim=0) else: #卷积层计算通道上(axis=1)的均值和方差#保持X的形状以便广播mean = X.mean(dim=(0, 2, 3), keepdim=True) var = X.var(dim=(0, 2, 3), keepdim=True) #训练模式用当前均值和方差做标准化X_hat = (X - mean) / torch.sqrt(var + eps) #更新移动平均的均值和方差moving_mean = momentum * moving_mean + (1 - momentum) * mean moving_var = momentum * moving_var + (1 - momentum) * var #缩放和移位Y = gamma * X_hat + beta return Y, moving_mean.detach(), moving_var.detach()#批量规范化层
class BatchNorm(nn.Module): #num_features:全连接层的输出数量或卷积层的输出通道数#num_dims:2表示全连接层,4表示卷积层def __init__(self, num_features, num_dims): super().__init__() if num_dims == 2: shape = (1, num_features) else: shape = (1, num_features, 1, 1) #初始化参与梯度的拉伸参数、偏移参数self.gamma = nn.Parameter(torch.ones(shape)) self.beta = nn.Parameter(torch.zeros(shape)) #初始化非模型参数移动均值和移动方差 self.moving_mean = torch.zeros(shape) self.moving_var = torch.ones(shape) def forward(self, X): #同一设备计算if self.moving_mean.device != X.device: self.moving_mean = self.moving_mean.to(X.device) self.moving_var = self.moving_var.to(X.device) Y, self.moving_mean, self.moving_var = batch_norm(X, self.gamma, self.beta, self.moving_mean, self.moving_var, eps=1e-5, momentum=0.9) return Y
7.5.4 使用批量规范化层的LeNet
#将批量规范化层应用在卷积层和全连接层之后、激活函数之前
net = nn.Sequential( nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(), nn.Linear(16 * 4 * 4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(), nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(), nn.Linear(84, 10)) #训练与6.6相同,但学习率大得多
lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
plt.show()
训练效果:
loss 0.267, train acc 0.902, test acc 0.842
21184.6 examples/sec on cuda:0
7.5.5 简明实现
pytorch框架定义了批量规范化层,其中BatchNorm1d应用于全连接层,BatchNorm2d应用于卷积层。
net = nn.Sequential( nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6, num_dims=4), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16, num_dims=4), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(), nn.Linear(16 * 4 * 4, 120), nn.BatchNorm1d(120, num_dims=2), nn.Sigmoid(), nn.Linear(120, 84), nn.BatchNorm1d(84, num_dims=2), nn.Sigmoid(), nn.Linear(84, 10))
7.6 残差网络(ResNet)
残差网络基于网络退化问题而提出,当时的研究者发现,模型的深度越大,其训练效果未必会提升,反而可能下降。
7.6.1 函数类
假设有一类特定的神经网络架构 F F F(函数类)可以学习到的函数为 f ∈ F f\in F f∈F,而我们真正需要的函数是 f ∗ f^* f∗。通常 f ∗ ∉ F f^*\notin F f∗∈/F,因此我们只能在 F F F中寻找最接近 f ∗ f^* f∗的函数 f F ∗ f^*_F fF∗,而我们寻找的方法即是对给定的 X \boldsymbol X X特征和 y \boldsymbol y y标签的数据集解决如下优化问题:
f F ∗ : = a r g m i n f L ( X , y , f ) , f ∈ F f^*_F:=\underset f{\mathrm{argmin}}L(\boldsymbol X,\boldsymbol y,f),f\in F fF∗:=fargminL(X,y,f),f∈F
要得到更接近 f ∗ f^* f∗的函数只有设计更强大的架构 F ′ F' F′,使 f F ′ ∗ f^*_{F'} fF′∗比 f F ∗ f^*_F fF∗更接近 f ∗ f^* f∗。然而如果 F ⊈ F ′ F\nsubseteq F' F⊈F′(非嵌套函数类),更复杂的函数类并不意味着向 f ∗ f^* f∗靠拢,但嵌套函数类则可以保证这一点:
由此,如果新添加的层可以被训练为恒等函数 f ( x ) = x f(\boldsymbol x)=\boldsymbol x f(x)=x,那么它既可以保证和原模型一样的效果,也可能得出更优解。
7.6.2 残差块
残差块在正常块的基础上引出一条路径让输入与输出直接相加,这样即是新的块没有从训练中得到任何东西,新模型也能保证和原模型有一样的效果(权重和偏置参数设置成 0 0 0)。此外,输入在残差块中还可以通过跨层数据更快地向前传播。残差块的引入只会让原模型朝着接近目标函数的方向发展。
ResNet残差块的细节设计基于VGG块,额外路径中可选的 1 × 1 1\times1 1×1卷积层负责处理输入以匹配原有块输出通道融合的情况:
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
from matplotlib import pyplot as pltclass Residual(nn.Module): def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1): super().__init__() self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, stride=strides, padding=1) self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides) else: self.conv3 = None self.bn1 = nn.BatchNorm2d(num_channels) self.bn2 = nn.BatchNorm2d(num_channels) def forward(self, X): Y = F.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) Y += X return F.relu(Y)
7.6.3 ResNet模型
ResNet前两层与GoogLeNet一样,但在卷积层后增加了批量规范化层。后面ResNet使用 4 4 4个由残差块组成的模块,每个模块使用若干输出通道数相同的残差块,其中后 3 3 3个模块包含 1 × 1 1\times1 1×1卷积处理。最后与GoogLeNet一样使用全局平均汇聚层和全连接层输出:
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) #残差模块
def resnet_block(input_channels, num_channels, num_residuals, firstblock=False): blk = [] for i in range(num_residuals): #除了第一个模块,每个模块的第一个块做1×1卷积if i == 0 and not firstblock: blk.append(Residual(input_channels, num_channels, use_1x1conv=True, strides=2)) else: blk.append(Residual(num_channels, num_channels)) return blk b2 = nn.Sequential(*resnet_block(64, 64, 2, firstblock=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2)) net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(512, 10))
7.6.4 训练模型
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
plt.show()
训练结果:
loss 0.010, train acc 0.998, test acc 0.906
828.8 examples/sec on cuda:0
7.7 稠密连接网络(DenseNet)
ResNet极大地改变了如何参数化深度网络中函数的观点,稠密连接网络在某种程度上时ResNet的逻辑扩展。
7.7.1 从ResNet到DenseNet
泰勒展开将函数分解成越来越高阶的项:
f ( x ) = f ( 0 ) + f ′ ( 0 ) x + f ′ ′ ( 0 ) 2 ! x 2 + f ′ ′ ′ 3 ! x 3 + ⋯ f(x)=f(0)+f'(0)x+\displaystyle\frac{f^{''}(0)}{2!}x^2+\frac{f^{'''}}{3!}x^3+\cdots f(x)=f(0)+f′(0)x+2!f′′(0)x2+3!f′′′x3+⋯
而ResNet将函数展开为:
f ( x ) = x + g ( x ) f(\boldsymbol x)=\boldsymbol x+g(\boldsymbol x) f(x)=x+g(x)
ResNet将 f f f分解为一个简单的线性项和一个复杂的非线性项,而DenseNet提出了一种将 f f f拓展成超过两部分的方案。关键区别在于DenseNet输出是连接(用 [ , ] [,] [,]表示)而不是ResNet的简单相加:
x → [ x , f 1 ( x ) , f 2 ( [ x , f 1 ( x ) ] ) , f 3 ( [ x , f 1 ( x ) , f 2 ( [ x , f 1 ( x ) ] ) ] ) , ⋯ ] \boldsymbol x\rightarrow[\boldsymbol x,f_1(\boldsymbol x),f_2([\boldsymbol x,f_1(\boldsymbol x)]),f_3([\boldsymbol x,f_1(\boldsymbol x),f_2([\boldsymbol x,f_1(\boldsymbol x)])]),\cdots] x→[x,f1(x),f2([x,f1(x)]),f3([x,f1(x),f2([x,f1(x)])]),⋯]
稠密网络由稠密块和过渡层组成,前者定义如何连接输入和输出,后者控制通道数使其不会太复杂。
7.7.2 稠密块体
DenseNet使用了ResNet改良版的“批量规范化层+激活层+卷积层”架构,一个稠密块由多个这样的卷积块组成,每个卷积块使用相同数量的输出通道,但最终每个卷积块的输入和输出会在通道维度上连接,即每次都会使输出通道数增加一个卷积块的通道数。卷积块的通道数控制了输出通道数相对于输入通道数的增长率。
import torch
from torch import nn
from d2l import torch as d2l
import matplotlib.pyplot as plt #卷积块
def conv_block(input_channels, num_channels): return nn.Sequential( nn.BatchNorm2d(input_channels), nn.ReLU(), nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1)) #稠密块
class DenseBlock(nn.Module): def __init__(self, num_convs, input_channels, num_channels): super().__init__() layer = [] for i in range(num_convs): layer.append(conv_block(num_channels * i + input_channels, num_channels)) self.net = nn.Sequential(*layer) def forward(self, X): for blk in self.net: Y = blk(X) X = torch.cat((X, Y), 1) return X
7.7.3 过渡层
为了应对卷积块带来的通道增长,过渡层通过 1 × 1 1\times1 1×1卷积层减小通道数,并使用平均汇聚层减半高度和宽度,从而降低模型复杂度。
def transition_block(input_channels, num_channels): return nn.Sequential( nn.BatchNorm2d(input_channels), nn.ReLU(), nn.Conv2d(input_channels, num_channels, kernel_size=1), nn.AvgPool2d(kernel_size=2, stride=2))
7.7.4 DenseNet模型
b1 = nn.Sequential( nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) #当前输出通道数和增长率(卷积块通道数)
num_channels, growth_rate = 64, 32
#每个稠密块的卷积块个数
num_convs_in_dense_blocks = [4, 4, 4, 4]
blks = []
for i, num_convs in enumerate(num_convs_in_dense_blocks): blks.append(DenseBlock(num_convs, num_channels, growth_rate)) #更新输出通道数num_channels += num_convs * growth_rate if i != len(num_convs_in_dense_blocks) - 1: #过渡层将输出通道数减半blks.append(transition_block(num_channels, num_channels // 2)) num_channels //= 2 net = nn.Sequential( b1, *blks, nn.BatchNorm2d(num_channels), nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(num_channels, 10))
7.7.5 训练模型
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
plt.show()
训练结果:
loss 0.140, train acc 0.950, test acc 0.909
820.2 examples/sec on cuda:0