动手学深度学习(四)卷积神经网络-下
全连接层存在的问题:参数过大,计算成本过高。
一、网络中的网络(NiN)
1、NiN块
①NiN块的结构
NiN串联多个由卷积层和“全连接”层构成的小网络来构建一个深层网络。这种由卷积层和“全连接”层构成的小网络就是NiN块。
(1)为什么卷积层不后接真的全连接层
卷积层的输入和输出通常是四维数组(样本,通道,高,宽),而全连接层的输入和输出则通常是二维数组(样本,特征)。如果想在全连接层后再接上卷积层,则需要将全连接层的输出变换为四维。
(2)1x1卷积层
1×1卷积层可以看成全连接层,其中空间维度(高和宽)上的每个元素相当于样本,通道相当于特征。因此,NiN使用1×1卷积层来替代全连接层,从而使空间信息能够自然传递到后面的层中去。
def nin_block(in_channels, out_channels, kernel_size, stride, padding):
blk = 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())
return blk
②NiN模型
(1)组成结构
NiN模型在每个NiN块后接一个步幅为2、窗口形状为3×3的最大池化层。
(2)输出结构
NiN去掉了AlexNet最后的3个全连接层,取而代之地,NiN使用了输出通道数等于标签类别数的NiN块,然后使用全局平均池化层对每个通道中所有元素求平均并直接用于分类。
这样设计的优点是:可以显著减小模型参数尺寸,从而缓解过拟合。
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, stride=4, padding=0),
nn.MaxPool2d(kernel_size=3, stride=2),
nin_block(96, 256, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(kernel_size=3, stride=2),
nin_block(256, 384, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
nin_block(384, 10, kernel_size=3, stride=1, padding=1),
# 全局平均池化层可通过将窗口形状设置成输入的高和宽实现
nn.AvgPool2d(kernel_size=5),
# 将四维的输出转成二维的输出,其形状为(批量大小, 10)
d2l.FlattenLayer())
二、含并行连结的网络(GoogLeNet)
GoogLeNet中的基础卷积块叫作Inception块。
Inception块里有4条并行的线路:前3条线路使用窗口大小分别是1×1、3×3和5×5的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做1×1卷积来减少输入通道数,以降低模型复杂度。第四条线路则使用3×3最大池化层,后接1×1卷积层来改变通道数。4条线路都使用了合适的填充来使输入与输出的高和宽一致。最后我们将每条线路的输出在通道维上连结,并输入接下来的层中去。
Inception块中可以自定义的超参数是每个层的输出通道数,我们以此来控制模型复杂度。
每一条线路的输出在空间维度(即高和宽)上保持一致,不同线路之间的主要差异在于输出通道数,将每条线路的输出在通道维度上拼接。假如当每条线路经过自己的卷积或池化操作后,输出的特征图的形状是 H×W×C1,H×W×C2,H×W×C43,H×W×C4,其中 C1、C2、C3、C4 是不同线路输出的通道数。
class Inception(nn.Module):
# c1 - c4为每条线路里的层的输出通道数
def __init__(self, in_c, c1, c2, c3, c4):
super(Inception, self).__init__()
# 线路1,单1 x 1卷积层
self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
# 线路2,1 x 1卷积层后接3 x 3卷积层
self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1 x 1卷积层后接5 x 5卷积层
self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3 x 3最大池化层后接1 x 1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_c, 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) # 在通道维上连结输出
三、批量归一化
底部的层指的是靠近原始输入数据的层。
1、为什么要做批量归一化
即使输入数据已做标准化,训练中模型参数的更新依然很容易造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常令我们难以训练出有效的深度模型。
2、批量归一化思路
在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。
3、对全连接层做批量归一化
①批量归一化层处于的位置
位于全连接层中的仿射变换和激活函数之间。
②方法实现
全连接层的输入为u,权重参数和偏差参数分别为W和b,激活函数为ϕ,设批量归一化的运算符为BN:
小批量B由若干样本组成,其中任意样本x_i是d维的,批量归一化层的输出同样是d维向量:
具体的批量归一化实现如下:
(1)对小批量B求均值和方差,其中的平方计算是按元素求平方:
(2)使用按元素开方和按元素除法对x(i)标准化,这里ϵ>0是一个很小的常数,保证分母大于0:
(3)在上面标准化的基础上,批量归一化层引入了两个可以学习的模型参数,拉伸(scale)参数 γ 和偏移(shift)参数 β,这两个参数和x(i)形状相同,皆为d维向量。它们与x̂ (i)分别做按元素乘法(符号⊙)和加法计算::
可学习的拉伸和偏移参数保留了不对x(i)做批量归一化的可能:此时只需学出√γ=σB2+ϵ和β==μB。可以这样理解:如果批量归一化无益,理论上,学出的模型可以不使用批量归一化。
4、对卷积层做批量归一化
①批量归一化层处于的位置
对卷积层来说,批量归一化发生在卷积计算之后、应用激活函数之前。
②方法实现
如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数,并均为标量。
设小批量中有m个样本。在单个通道上,假设卷积计算输出的高和宽分别为p和q。我们需要对该通道中m×p×q个元素同时做批量归一化。对这些元素做标准化计算时,我们使用相同的均值和方差,即该通道中m×p×q个元素的均值和方差。
5、预测时的批量归一化
批量归一化确实在训练模式和预测模式下表现不同,这主要是因为在训练时需要使用小批量样本的均值和方差,而在预测时为了确保输出的确定性,使用的是整个训练数据的均值和方差的估计值。
nn.BatchNorm1d
:用于全连接层输出或一维序列数据的归一化。nn.BatchNorm2d
:用于卷积层输出的归一化,特别是图像或二维特征图数据。
def batch_norm(is_training, X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 判断当前模式是训练模式还是预测模式
if not is_training:
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
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:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。这里我们需要保持
# X的形状以便后面可以做广播运算
mean = X.mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=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, moving_var
net = nn.Sequential(
nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
nn.BatchNorm2d(6),
nn.Sigmoid(),
nn.MaxPool2d(2, 2), # kernel_size, stride
nn.Conv2d(6, 16, 5),
nn.BatchNorm2d(16),
nn.Sigmoid(),
nn.MaxPool2d(2, 2),
d2l.FlattenLayer(),
nn.Linear(16*4*4, 120),
nn.BatchNorm1d(120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.BatchNorm1d(84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
四、残差网络(ResNet)
1、ResNet的提出背景
ResNet主要解决了深度神经网络在训练过程中出现的退化问题。在深层网络中,随着网络深度的增加,模型的训练效果并不总是变好,反而可能变得更差。具体表现为,较深的网络不仅会导致梯度消失或梯度爆炸的问题,还会使得训练误差增大,模型性能下降。
2、残差块
ResNet的核心设计是残差学习(Residual Learning)。它通过引入残差连接(skip connections)解决了深层网络中的优化问题,核心设计点包括:
①残差块
Net中每一层并不直接学习期望的输出,而是学习残差,即输入与输出之间的差异。对于每一个残差块,输入直接通过“跳跃连接”传递到输出,并且与通过卷积层学习的输出叠加:
F(x)是通过若干层卷积和非线性变换学习得到的残差函数(F(x)即输入与输出之间的差值),y 是残差块的输出,x 是输入数据。
- 如果 F(x)很小(接近零),意味着输出与输入相差不大,此时网络可以很容易地学到恒等映射 H(x)≈x,因此即使网络非常深也不会出现退化问题。
- 如果 F(x)较大,网络会逐渐学习输入与输出之间的差异,这也比直接去学习复杂的 H(x)容易得多。
②跳跃连接(Skip Connection)
传统的深度网络在每一层都会完全依赖前一层的输出,而在ResNet中,通过跳跃连接,输入 x 直接跳过中间的非线性变换,参与到后面的计算中。这种设计减少了信息丢失,使得梯度更容易传播,缓解了梯度消失的问题。
③恒等映射(Identity Mapping)
如果某一层的输出与输入相同,理论上通过跳跃连接可以确保这一层对网络的最终表现没有负面影响。这使得即使网络加深,也不会因为层数增加而使训练误差变得更高。