特征本身应该被学习;在合理地复杂性前提下,特征应该由多个共同学习的神经网络层组成,每个层都有学习的参数。在机器视觉中,最底层可能检测边缘、颜色和纹理。
2012年,AlexNet横空出世。首次证明学习到的特征可以超越手工设计的特征。AlexNet使用8层卷积神经网络:5个卷积层、2个全连接隐藏层和1个全连接输出层。AlexNet使用ReLU作为其激活函数。
在AlexNet的第一层,卷积窗口的形状是 11 ∗ 11 11*11 11∗11。由于ImageNet中大多数图像的宽和高比MNIST的多10倍以上,因此需要一个更大的卷积窗口来捕获目标。第二层的卷积窗口形状缩减为 5 ∗ 5 5*5 5∗5,然后是 3 ∗ 3 3*3 3∗3。此外,在第一层、第二层和第5层卷积层之后,加入窗口形状为 3 ∗ 3 3*3 3∗3、步幅为2的最大汇聚层。AlexNet卷积通道数目是LeNet的10倍。
AlexNet将ReLU作为激活函数,⼀⽅⾯,ReLU激活函数的计算更简单;另⼀⽅⾯,当使⽤不同的参数初始化⽅法时,ReLU激活函
数使训练模型更加容易。
AlexNet通过暂退法控制全连接层的模型复杂度,而LeNet只使用权重衰减。为了进⼀步扩充数据,AlexNet在训练时增加了⼤量的图像增强数据,如翻转、裁切和变⾊。这使得模型更健壮,更⼤的样本量有效地减少了过拟合。
import torch
from torch import nn
from d2l import torch as d2l
net = 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))
X = torch.randn(1, 1, 224, 224)
for layer in net:
X=layer(X)
print(layer.__class__.__name__,'output shape:\t',X.shape)
# Conv2d output shape: torch.Size([1, 96, 54, 54])
# ReLU output shape: torch.Size([1, 96, 54, 54])
# MaxPool2d output shape: torch.Size([1, 96, 26, 26])
# Conv2d output shape: torch.Size([1, 256, 26, 26])
# ReLU output shape: torch.Size([1, 256, 26, 26])
# MaxPool2d output shape: torch.Size([1, 256, 12, 12])
# Conv2d output shape: torch.Size([1, 384, 12, 12])
# ReLU output shape: torch.Size([1, 384, 12, 12])
# Conv2d output shape: torch.Size([1, 384, 12, 12])
# ReLU output shape: torch.Size([1, 384, 12, 12])
# Conv2d output shape: torch.Size([1, 256, 12, 12])
# ReLU output shape: torch.Size([1, 256, 12, 12])
# MaxPool2d output shape: torch.Size([1, 256, 5, 5])
# Flatten output shape: torch.Size([1, 6400])
# Linear output shape: torch.Size([1, 4096])
# ReLU output shape: torch.Size([1, 4096])
# Dropout output shape: torch.Size([1, 4096])
# Linear output shape: torch.Size([1, 4096])
# ReLU output shape: torch.Size([1, 4096])
# Dropout output shape: torch.Size([1, 4096])
# Linear output shape: torch.Size([1, 10])
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
- AlexNet的架构与LeNet相似,但使用更多的卷积层和更多的参数来拟合大规模的ImageNet数据集。
- Dropout、ReLU和预处理是提升计算机视觉任务性能的其他关键步骤。
虽然AlexNet证明深层网络卓有成效,但没有提供一个通用的模板来指导后续的研究人员设计新的网络。牛津大学的视觉几何组(visualgeometry group)首先提出使用块的想法,并应用在VGG网络中。通过使用循环和子程序,可以更容易地在任何现代深度学习框架的代码中实现这些重复的架构。
经典卷积神经网络的基本组成部分:
- 带填充以保持分辨率的卷积层;
- 非线性激活函数,如ReLU;
- 汇聚层,如最大汇聚层。
而一个VGG块与之类似,由一系列卷积层组成,后面再加上用于空间下采样的最大汇聚层。
import torch
from torch import nn
from d2l import torch as d2l
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels,out_channels,kernel_size=3,padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
VGG⽹络可以分为两部分:第⼀部分主要由卷积层和汇聚层组成,第⼆部分由全连接层组成。
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)
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
X = blk(X)
print(blk.__class__.__name__,'output shape:\t',X.shape)
# Sequential output shape: torch.Size([1, 64, 112, 112])
# Sequential output shape: torch.Size([1, 128, 56, 56])
# Sequential output shape: torch.Size([1, 256, 28, 28])
# Sequential output shape: torch.Size([1, 512, 14, 14])
# Sequential output shape: torch.Size([1, 512, 7, 7])
# Flatten output shape: torch.Size([1, 25088])
# Linear output shape: torch.Size([1, 4096])
# ReLU output shape: torch.Size([1, 4096])
# Dropout output shape: torch.Size([1, 4096])
# Linear output shape: torch.Size([1, 4096])
# ReLU output shape: torch.Size([1, 4096])
# Dropout output shape: torch.Size([1, 4096])
# Linear output shape: torch.Size([1, 10])
net = vgg(conv_arch)
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
lr, num_epochs = 0.05, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
- VGG-11使用可复用的卷积块构造网络。不同的VGG模型可通过每个块中卷积层数量和输出通道数量的差异来定义。
- 块的使用导致网络定义非常简洁。使用块可以有效地设计复杂的网络。
- 在VGG论文中,Simonyan和Ziserman尝试了各种架构。特别是发现深层且窄的卷积( 3 ∗ 3 3*3 3∗3)比浅层且宽的卷积更有效。
LeNet、AlexNet和VGG都有一个共同的设计模式:通过一系列的卷积层与汇聚层来提取空间结构特征;然后通过全连接层对特征的表征进行处理。AlexNet和VGG对LeNet的改进主要在于如何扩大和加深这两个模块。设想,在这个过程的早期使用全连接层,即便可能会完全放弃表征的空间结构。网络中的网络(NiN)提供了一个非常简单的解决方案:在每个像素的通道上分别使用多层感知机。
卷积层的输入和输出由四维张量组成,张量的每个轴分别对应样本、通道、高度和宽度。另外,全连接层的输入和输出通道通常是分别对应于样本和特征的二维张量。NiN的相反是在每个像素位置应用一个全连接层。将权重连接到每个空间位置,可以将其视为 1 ∗ 1 1*1 1∗1卷积层,或作为在每个像素位置上独立作用的全连接层。从另一个角度看,即将空间维度中的每个像素视为单个样本,将通道维度视为不同特征(feature)。
NiN块以一个普通卷积层开始,后面是两个 1 ∗ 1 1*1 1∗1的卷积层。这两个 1 ∗ 1 1*1 1∗1卷积层充当带有ReLU激活函数的逐像素全连接层。第一层的卷积窗口形状通常由用户设置,随后的卷积窗口形状固定为 1 ∗ 1 1*1 1∗1。
import torch
from torch import nn
from d2l import torch as d2l
def nin_block(in_channels, out_channels,kernel_size,stride, padding):
return nn.Sequential(nn.Conv2d(in_channels,out_channels,kernel_size,stride,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())
最初的NiN网络是在AlexNet后不久提出的,显然从中得到了一些启示。NiN使用窗口形状为 11 ∗ 11 11*11 11∗11、 5 ∗ 5 5*5 5∗5和 3 ∗ 3 3*3 3∗3的卷积层,输出通道数量与AlexNet中的相同。每个NiN块后有一个最大汇聚层,汇聚窗口形状为 3 ∗ 3 3*3 3∗3,步幅为2。
NiN和AlexNet之间的一个显著区别是NiN完全取消了全连接层。相反,NiN使用一个NiN块,其输出通道数等与标签类别的数量。最后放一个全局平局汇聚层(global average pooling layer),生成一个对数几率(logits)。NiN设计的一个优点是,它显著减少了模型所需参数的数量。然而,在实践中,射中设计有时会增加训练模型的实践。
net = nn.Sequential(nin_block(1,96,kernel_size=11,stride=4,padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(96,256,kernel_size=5,stride=1,padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(256,384,kernel_size=3,stride=1,padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
nin_block(384,10,kernel_size=3,stride=1,padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten())
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)
# Sequential output shape: torch.Size([1, 96, 54, 54])
# MaxPool2d output shape: torch.Size([1, 96, 26, 26])
# Sequential output shape: torch.Size([1, 256, 26, 26])
# MaxPool2d output shape: torch.Size([1, 256, 12, 12])
# Sequential output shape: torch.Size([1, 384, 12, 12])
# MaxPool2d output shape: torch.Size([1, 384, 5, 5])
# Dropout output shape: torch.Size([1, 384, 5, 5])
# Sequential output shape: torch.Size([1, 10, 5, 5])
# AdaptiveAvgPool2d output shape: torch.Size([1, 10, 1, 1])
# Flatten output shape: torch.Size([1, 10])
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())
- NiN使用由一个卷积层和多个 1 ∗ 1 1*1 1∗1卷积层组成的块。该块可以在卷积神经网络中使用,以允许更多的每像素非线性。
- NiN去除了容易造成过拟合的全连接层,将它们替换为全局平均汇聚层(即在所有位置上进行求和)。该汇聚层通道数为所需的输出数量。
- 移除全连接层可减少过拟合,同时显著减少NiN参数
- NiN的设计影响了许多后续卷积神经网络的设计。
在GoogLeNet中,基本的卷积块被称为Inception块。Inception块由四条并行路径组成。前三条路径使用窗口大小分别为 1 ∗ 1 1*1 1∗1、 3 ∗ 3 3*3 3∗3和 5 ∗ 5 5*5 5∗5的卷积层,从不同空间大小中提取信息。中间的两条路径在输入上执行 1 ∗ 1 1*1 1∗1卷积,以减少通道数,从而降低模型的复杂性。第四条路径使用 3 ∗ 3 3*3 3∗3最大汇聚层,然后使用 1 ∗ 1 1*1 1∗1卷积层来改变通道数。这四条路径都使用合适的填充来使输入月输出的高和宽一致,最后将每条线路的输出在通道维度上连结,并构成Inception块的输出。在Inception块中,通常调整的超参数是每层输出通道数。
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
class 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=1)
self.p4_1 = nn.MaxPool2d(3,stride=1,padding=1)
self.p4_2 = nn.Conv2d(in_channels, c4, 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)
GoogLeNet⼀共使⽤9个Inception块和全局平均汇聚层的堆叠来⽣成其估计值。Inception块之间的最⼤汇聚层可降低维度。第⼀个模块类似于AlexNet和LeNet,Inception块的组合从VGG继承,全局平均汇聚层避免了在最后使⽤全连接层。
第一个模块使用64个通道、 7 ∗ 7 7*7 7∗7卷积层。第二个模块使用两个卷积层:一个卷积层是64个通道、 1 ∗ 1 1*1 1∗1卷积层;第二个卷积层使用将通道数量增加三倍的 3 ∗ 3 3*3 3∗3卷积层。这对应于Inception块中的第二条路径。
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))
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))
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))
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))
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))
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten())
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)
# Sequential output shape: torch.Size([1, 64, 24, 24])
# Sequential output shape: torch.Size([1, 192, 12, 12])
# Sequential output shape: torch.Size([1, 480, 6, 6])
# Sequential output shape: torch.Size([1, 832, 3, 3])
# Sequential output shape: torch.Size([1, 1024])
# Linear output shape: torch.Size([1, 10])
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())
训练深层神经网络是十分困难的,特别是在较短的时间内使它们收敛。在本节中,将介绍批量规范化(batch normalization),这是一种流行且有效的技术,可持续加速深层网络的收敛速度。
为什么需要批量规范化层呢?
回归一下训练神经网络时出现的一些实际挑战:
- 首先,数据预处理的方式通常会对最终结果产生巨大影响。使用真实数据时,第一步是标准化输入特征,使其均值为0,方差为1。直观地说,这种标准化可以很好地与优化器配合使用,因为它可以将参数的量级进行统一。
- 第二,对于典型的多层感知机或卷积神经网络。训练时,中间层的变量可能具有更广的变化范围:不论是沿着从输入到输出的层,跨同一层中的单元,或是随着时间的推移,模型参数随着训练更新变化莫测。批量规范化的发明者非正式地假设,这些变量分布中的这种偏移可能会阻碍网络的收敛。直观地说,我们可能会猜想,如果一个层的可变值是另一层的100倍,这可能需要对学习率进行补偿调整。
- 第三,更深层的网络很复杂,容易过拟合。这意味着正则化变得更加重要。
批量规范化应用于单个可选层(也可以应用到所有层),其原理如下:
- 在每次训练迭代中,首先规范化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。
- 接下来,应用比例系数和比例偏移。
正是由于这个基础批量统计的标准化,才有了批量规范化的名称。注意,如果使用大小为1的小批量应用批量规范化,我们将无法学到任何东西。这是因为在减去均值之后,每个隐藏单元将为0。所以,只有使用足够多的小批量,批量规范化操作才是有效且稳定的。在应用批量规范化时,批量大小的选择可能比没有批量规范化时更重要。
从形式上来说,用 x ∈ B \bold x \in \Beta x∈B表示一个来自小批量 B \Beta B的输入,批量规范化BN根据以下表达式转换 x \bold x x: B N ( x ) = γ ⊙ x − μ B ^ σ B ^ BN(\bold x)=\gamma \odot \frac{\bold x -\bold {\hat{\mu_{\Beta}}}}{\bold {\hat{\sigma_\Beta}}} BN(x)=γ⊙σB^x−μB^
其中, μ B ^ \bold{\hat{\mu_{\Beta}}} μB^是小批量 B \bold{\Beta} B的样本均值, σ B ^ \bold{\hat{\sigma_{\Beta}}} σB^是小批量 B \bold{\Beta} B的样本标准差。应用标准化后,生成的小批量的平均值为0和单位方差为1。由于单位方差是一个主观的选择,因此通常包含拉伸参数(scale) γ \gamma γ和偏移参数(shift) β \beta β,它们的形状与 x \bold x x相同。注意, γ \gamma γ和 β \beta β是需要与其他模型参数一起学习的参数。由于在训练过程中,中间层的变化幅度不能过于强烈,而批量规范化将每一层主动居中,并将它们重新调整为给定的平均值和大小(通过 μ ^ B \bold {\hat \mu_{\Beta}} μ^B和 σ ^ B \bold {\hat \sigma_{\Beta}} σ^B)。
从形式上来说,我们计算出的 μ ^ B \bold {\hat \mu_{\Beta}} μ^B和 σ ^ B \bold {\hat \sigma_{\Beta}} σ^B为如下公式所示:
μ ^ B = 1 ∣ B ∣ ∑ x ∈ B x \bold {\hat \mu_{\Beta}}=\frac{1}{|{\Beta}|} \sum_{\bold x \in \bold \Beta} \bold x μ^B=∣B∣1x∈B∑x σ ^ B 2 = 1 ∣ B ∣ ∑ x ∈ B ( x − μ ^ B ) 2 + ϵ \bold {\hat \sigma_{\Beta}^2} = \frac{1}{|{\Beta}|} \sum_{\bold x \in \bold \Beta} (\bold x - \bold {\hat \mu_{\Beta}})^2 + \epsilon σ^B2=∣B∣1x∈B∑(x−μ^B)2+ϵ
注意,在方差估计值中添加一个小的常量 ϵ > 0 \epsilon >0 ϵ>0,以确保永远不会尝试除以零。估计值 μ ^ B \bold {\hat \mu_{\Beta}} μ^B和 σ ^ B \bold {\hat \sigma_{\Beta}} σ^B通过使用平均值和方差的噪声(noise)估计来抵消缩放问题。你可能会认为这种噪声是一个问题,而事实上它是有益的。
事实证明,这是深度学习中一个反复出现的主题。虽然在理论上尚未明确原因,但优化中的各种噪声源通常会导致更快的训练和较少的过拟合,这种变化似乎是正则化的一种形式。
另外,批量规范化层在"训练模式"(通过小批量统计数据规范化)和“预测模式”(通过数据集统计规范化)中的功能不同。在训练过程中,我们无法得知使用整个数据集来估计平均值和方差,所以智能根据每个小批量的平均值和方差不断训练模型。而在预测模式下,可以根据整个数据集进度计算批量规范化所需的平均值和方差。
批量规范化和其他层之间的一个关键区别是,由于批量规范化在完整的小批量上运行,因此我们不能忽略批量大小。下面讨论全连接层和卷积层两种情况的批量规范化实现。
通常,我们将批量规范化层置于全连接层中的仿射变换和激活函数之间。设全连接层的输入为 x x x,权重参数和偏置参数分别是 w \bold w w和 b \bold b b,激活函数为 Φ \Phi Φ,批量规范化的运算符为 B N BN BN。那么,使用批量规范化的全连接层的输出计算为: h = ϕ ( B N ( W x + b ) ) \bold h = \phi(BN(\bold {Wx + b})) h=ϕ(BN(Wx+b))
同样,对于卷积层,可以在卷积层之后和非线性激活函数之前应用批量规范化。当卷积层由多个输出通道时,需要对这些通道的“每个”输出执行批量规范化,每个通道都有自己的拉伸(scale)和偏移(shift)参数,这两个参数都是标量。假设小批量包含m个样本,并且对于每个通道,卷积的输出具有高度p和宽度q。那么对于卷积层,在每个输出通道的mpq个元素上同时执行每个批量规划化。因此,在计算平均值和方差时,会收集所有空间位置的值,然后在给定通道内应用相同的均值和方差,以便在每个空间位置对值进行规范化。
批量规范化在训练模式和预测模式下的行为通常不同。首先,将训练好的模型用于预测时,不再需要样本均值中的噪声以及在小批量上估计每个小批次产生的样本方差。其次,例如,我们可能需要使用我们的模型对逐个样本进行预测。一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。可见,和暂退法一样,批量规范化层在训练模式和预测模式下的计算结果也是不一样的。
import torch
from torch import nn
from d2l import torch as d2l
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 在预测模式下,直接使用传入的均值和方差
X_hat = (X-moving_mean)/torch.sqrt(moving_var+eps)
else:
assert len(X.shape) in (2,4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维度上的均值和方差
mean = X.mean(dim=0)
var = ((X-mean)**2).mean(dim=0)
else:
# 使用卷积层的情况,计算通道维度上的均值和方差
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
# 训练模式下,用当前的均值和方差做标准化
X_hat = (X - mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 缩放和移位
return Y,moving_mean.data, moving_var.data
撇开算法细节,注意我们实现层的基础设计模式。通常情况下,我们用一个单独的函数定义其数学原理,比如说batch_norm。然后,我们将此功能集成到一个自定义层中,其代码主要处理数据移动到训练设备、分配和初始化任何必需的变量、跟踪移动平均线等问题。为了方便起见,我们并不担心在这里自动推断输入形状,因此我们需要指定整个特征的数量。
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)
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化为1.0和0.0
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 非模型参数的变量初始化为0和1
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.ones(shape)
def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var复制到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)
# 保存更新过的moving_mean和moving_var
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
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))
lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
net = nn.Sequential(
nn.Conv2d(1,6,kernel_size=5),nn.BatchNorm2d(6), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2,stride=2),
nn.Conv2d(6,16,kernel_size=5),nn.BatchNorm2d(16), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(16*4*4, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
nn.Linear(84, 10))
- 在模型训练过程中,批量规划化利用小批量的均值和标准差,不断调整神经网络的中间输出,使真个神经网络各层的中间输出值更加稳定。
- 批量规范化在全连接层和卷积层的使用略有不同。
- 批量规范化和暂退层一样,在训练模式和预测模式下计算不同。
- 批量规范化有许多有益的副作用,主要是正则化。另一方面,“减少内部协变量偏移”的原始动机似乎不是一个有效的解释。
随着我们设计越来越深的网络,深刻理解“新添加的层如何提升神经网络的性能”变得至关重要。更重要的是设计网络的能力,在这种网络中,添加层会使网络更具表现力,为了取得质的突破,我们需要一些数学基础知识。
首先,假设有一类特定的神经网络架构 F F F,它包括学习速率和其他超参数设置。对于所有 f ∈ F f\in F f∈F,存在一些参数集(例如权重和偏置),这些参数可以通过在合适的数据集上进行训练而获得。现在假设 f ∗ f^* f∗是我们真正想要找到的函数,如果是 f ∗ ∈ F f^*\in F f∗∈F,那么我们可以轻而易举的训练得到它,但通常我们不会那么幸运。相反,我们将尝试找到一个函数 f F ∗ f^*_F fF∗,这是我们在 F F F中的最佳选择。例如,给定一个具有 x \bold x x和 y \bold y y标签的数据集,我们可以尝试通过解决一下优化问题找到它: f F ∗ : = a r g m i n f L ( X , y , f ) s u b j e c t t o f ∈ F f^*_F:= argmin_f L(\bold {X,y,f}) \space subject \space {to} \space f \in F fF∗:=argminfL(X,y,f) subject to f∈F
那么,怎样得到更近似真正 f ∗ f^* f∗的函数呢?唯一合理的可能性是,我们需要设计一个更强大的架构 F ′ F' F′。换句话说,我们预计 f ∗ F ′ f{^*}{_{F'}} f∗F′比 f ∗ F f{^*}{_{F}} f∗F“更近似”。然而,如果,果F ̸⊆ F′,则无法保证新的体系更近似。
因此,只有当较复杂的函数类包含较小的函数类时,我们才能确保提高它们的性能。对于深度神经网络,如果我们能将新添加的层训练成恒等映射(identity function) f ( x ) = x f(\bold x) = \bold x f(x)=x,新模型和原模型将同样有效。同时,由于新模型可能得出更优的解来拟合训练数据集。因此添加层似乎更容易降低训练误差。
针对这一问题,何恺明等人提出了残差网络(ResNet)。它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。残差网络的核心思想:每个附加层都应该更容易地包含原始函数作为其元素之一。于是,残差块(residual blocks)便诞生了。这个设计对如何建立深层神经网络产生了深远的影响。
假设我们的原始输入为 x x x,而希望学出的理想映射为 f ( x ) f(\bold x) f(x)。左图虚线框部分需要直接拟合出该映射 f ( x ) f(\bold x) f(x),而右图虚线框部分这需要拟合出残差映射 f ( x ) − x f(\bold x) - \bold x f(x)−x。残差映射在现实中往往更容易优化。以本节开头提到的恒等映射作为希望学出的理想映射,只需将右图虚线框内上方的加权运算的权重和偏置参数设置成0,那么 f ( x ) f(\bold x) f(x)即为恒等映射。十几种,当理想映射 f ( x ) f(\bold x) f(x)接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。右图是ResNet的基础架构-残差块(residual block)。在残差块中,输入可通过跨层数据线路更快地向前传播。
ResNet沿用了VGG完整的 3 ∗ 3 3*3 3∗3卷积层设计。残差块里首先有2个相同输出通道数的 3 ∗ 3 3*3 3∗3卷积层。每个卷积层后接一个批量规范化层和ReLU激活函数。然后通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。这样的设计要求2个卷积层的输出与输入形状一样,从而使它们可以相加。如果想改变通道数,就需要引入一个额外的 1 ∗ 1 1*1 1∗1卷积层来将输入变换成需要的形状后再做相加运算。残差块的实现如下:
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
class Residual(nn.Module):
def __init__(self, inputs_channels, num_channels, use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(inputs_channels, num_channels, kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels,num_channels, kernel_size=3,padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(inputs_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)
此代码生成两种类型的网络:一种是当 u s e 1 x 1 c o n v = F a l s e use_1x1conv=False use1x1conv=False时,应用ReLU非线性函数之前,将输入添加到输出。另一种是当 u s e 1 x 1 c o n v = T r u e use_1x1conv=True use1x1conv=True时,添加通过 1 ∗ 1 1*1 1∗1卷积调整通道和分辨率。
示例查看输入和输出形状一致的情况;以及增加输出通道,减半高和宽的情况。
blk = Residual(3,3)
X = torch.rand(4,3,6,6)
Y = blk(X)
Y.shape # torch.Size([4, 3, 6, 6])
blk = Residual(3,6, use_1x1conv=True, strides=2)
blk(X).shape # torch.Size([4, 6, 3, 3])
ResNet的前两层跟之前介绍的GoogLeNet模型一样:在输出通道为64、步幅为2的 7 ∗ 7 7*7 7∗7卷积层后,接步幅为2的 3 ∗ 3 3*3 3∗3的最大池化层。不同之处在于ResNet每个卷积层后增加批量规范化层。
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))
GoogLeNet在后面接了4个由Inception块组成的模块。ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。第一个模块的通道数同输入通道数一致。由于之前已经使用了步幅为2的最大汇聚层,所以无需减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。
def resnet_block(input_channels, num_channels, num_residuals, first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
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(64,64,2,first_block=True))
b3 = nn.Sequential(*resnet(64, 128, 2))
b4 = nn.Sequential(*resnet(128, 256, 2))
b5 = nn.Sequential(*resnet(256, 512, 2))
net = nn.Sequential(b1,b2,b3,b4,b5,nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(), nn.Linear(512,10))
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)
# Sequential output shape: torch.Size([1, 64, 56, 56])
# Sequential output shape: torch.Size([1, 64, 56, 56])
# Sequential output shape: torch.Size([1, 128, 28, 28])
# Sequential output shape: torch.Size([1, 256, 14, 14])
# Sequential output shape: torch.Size([1, 512, 7, 7])
# AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1])
# Flatten output shape: torch.Size([1, 512])
# Linear output shape: torch.Size([1, 10])
lr, num_epochs, batch_size = 0.05, 10, 256
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())