经典的卷积神经网络
在本次笔记中主要介绍一些经典的卷积神经网络模型,主要包含以下:
- LeNet:最早发布的卷积神经网络之一,目的是识别图像中的手写数字;
- AlexNet: 是第一个在大规模视觉竞赛中击败传统计算机视觉模型的大型神经网络;
- 使用重复块的网络(VGG):利用许多重复的神经网络块;
- 网络中的网络(NiN):重复使用由卷积层和1×1卷积层(用来代替全连接层)来构建深层网络;
- 含并行连结的网络(GoogLeNet):使用并行连结的网络,通过不同窗口大小的卷积层和最大汇聚层来并行抽取信息;
- 残差网络(ResNet):它通过残差块构建跨层的数据通道,是计算机视觉中最流行的体系架构;
- 稠密连接网络(DenseNet):它的计算成本很高,但能带来了更好的效果。
- 轻量化CNN(mobileNet):旨在优化移动和嵌入式设备上的性能
LeNet
介绍
最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。 该模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名),目的是识别图像中的手写数字(应用与美国邮政服务)。
LeNet(LeNet-5)由两个部分组成:
- 卷积编码器:由两个卷积层组成;
- 全连接层密集块:由三个全连接层组成。
在卷积层块中,每个卷积层都使用5 × 5 的窗口,并在输出上使用sigmoid激活函数。第一个卷积层输出通道数为6,第二个卷积层输出通道数则增加到16。
在图中网络结构中汇聚层就是应用最大池化完成的。其中,最大池化的窗口大小为2 × 2,且步幅为2。由于池化窗口与步幅形状相同,池化窗口在输入上每次滑动所覆盖的区域互不重叠。
该网络使用数据集:MNIST数据集
包含50000个训练数据、10000个测试数据;样本均为灰度图像:28*28;输出:10类(0-9)
总结
- LeNet是早期成功的神经网络
- 先使用卷积层来学习图片空间信息
- 然后使用全连接层来转换到类别空间
代码实现
# lenet模型的实现import torch
from torch import nn
from d2l import torch as d2lnet = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Flatten(),nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),nn.Linear(120, 84), nn.Sigmoid(),nn.Linear(84, 10)
)# 我们对原始模型做了一点小改动,去掉了最后一层的高斯激活。除此之外,这个网络与最初的LeNet-5一致。# 检查模型
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:X = layer(X)print(layer.__class__.__name__,'output shape: \t',X.shape)# 模型训练,上面已经实现了LeNet,接下来让我们看看LeNet在Fashion-MNIST数据集上的表现
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)# 评估模型
def evaluate_accuracy_gpu(net, data_iter, device=None):if isinstance(net, nn.Module):net.eval() # 设置为评估模式# 如果 device 参数未提供,则通过 next(iter(net.parameters())).device 获取模型第一个参数所在的设备,将其作为后续计算的设备。if not device:device = next(iter(net.parameters())).device# 正常预测的数量,总预测的数量metric = d2l.Accumulator(2)# 在评估阶段,不需要计算梯度with torch.no_grad():for X, y in data_iter:# 如果输入数据 X 是列表类型(通常在 BERT 微调等场景中会遇到),将列表中的每个张量迁移到指定设备上。if isinstance(X, list):# BERT 微调所需的(之后讲)X = [x.to(device) for x in X]else:# 否则直接将输入数据 X 迁移到指定设备上X = X.to(device)# 标签 y 迁移到指定设备上 y = y.to(device)# 使用 d2l 库中的 accuracy 函数计算当前批次的预测准确率,即正确预测的样本数量。metric.add(d2l.accuracy(net(X), y), y.numel())return metric[0] / metric[1]# 训练函数def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):"""用GPU训练模型"""# 1. 初始化模型的权重,使用 Xavier 均匀分布初始化线性层和卷积层的权重。def init_weight(m):if type(m) == nn.Linear or type(m) == nn.Conv2d:nn.init.xavier_uniform_(m.weight)net.apply(init_weight)print('training on', device)# 2. 将模型移动到指定的设备(通常是 GPU)上进行训练。net.to(device)# 3.定义优化器(随机梯度下降 SGD)和损失函数(交叉熵损失)。optimizer = torch.optim.SGD(net.parameters(), lr=lr)loss = nn.CrossEntropyLoss()# 4. 创建一个动画绘制器 Animator 用于实时可视化训练过程中的损失和准确率变化。animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],legend=['train loss', 'train acc', 'test acc'])timer, num_batches = d2l.Timer(), len(train_iter)# 5. 开始多个轮次的训练,在每个轮次中遍历训练数据集,计算损失、反向传播并更新模型参数。for epoch in range(num_epochs):# 训练损失之和,训练准确率之和,样本数metric = d2l.Accumulator(3)net.train()for i, (X, y) in enumerate(train_iter):timer.start()optimizer.zero_grad()X, y = X.to(device), y.to(device)y_hat = net(X)l = loss(y_hat, y)l.backward()optimizer.step()with torch.no_grad():metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])timer.stop()train_l = metric[0] / metric[2]train_acc = metric[1] / metric[2]if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:animator.add(epoch + (i + 1) / num_batches,(train_l, train_acc ,None))# 6.每个轮次结束后,评估模型在测试数据集上的准确率。test_acc = evaluate_accuracy_gpu(net, test_iter)animator.add(epoch + 1, (None, None, test_acc))# 7.最后输出训练结束后的训练损失、训练准确率、测试准确率以及训练速度。print(f'loss{train_l:.3f}, train acc {train_acc:.3f},'f'test acc {test_acc:.3f}')print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec'f'on {str(device)}')# 接下来训练和评估LeNet-5模型
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
得到结果:
AlexNet
先说点其他的
-
背景
当时关于特征提取有两种思想:- 观察图像特征的提取方法:2012年 前,图像特征都是机械地计算出来的。
- 认为特征本身应该被学习
-
AlexNet 诞生的关键因素
- 数据层面:深度模型需大量有标签数据才能优于传统方法。早期受存储和预算限制,研究多基于小数据集。2009 年 ImageNet 数据集发布并举办挑战赛,提供百万样本、千种类别,推动了研究发展。
- 硬件层面:深度学习计算需求高,早期优化凸目标的简单算法因计算资源限制受青睐。GPU 最初用于图形处理,其运算与卷积层计算相似,英伟达和 ATI 将其优化用于通用计算。GPU 由众多小型处理单元组成,虽单个核心性能弱,但数量庞大,在浮点性能、功耗和内存带宽方面优于 CPU。2012 年,Alex Krizhevsky 和 Ilya Sutskever 利用 GPU 实现快速卷积运算,推动了深度学习发展。
介绍
-
网络结构
使用了8层卷积神经网络,前5层是卷积层,剩下的3层是全连接层,具体如下所示。
(由当时GPU内存的限制引起的,作者使用两块GPU进行计算,因此分为了上下两部分) -
与LeNet结构的差异:
-
具体细节:
- 卷积层C1:使用96个核对224×224×3的输入图像进行滤波,卷积核大小为11×11×3,步长为4。将一对55×55×48的特征图分别放入ReLU激活函数,生成激活图。激活后的图像进行最大池化,size为3×3,stride为2,池化后的特征图size为27×27×48(一对)。池化后进行LRN处理。
- 卷积层C2:使用卷积层C1的输出(响应归一化和池化)作为输入,并使用256个卷积核进行滤波,核大小为5 × 5 × 48。
- 卷积层C3:有384个核,核大小为3 × 3 × 256,与卷积层C2的输出(归一化的,池化的)相连。
- 卷积层C4:有384个核,核大小为3 × 3 × 192。
- 卷积层C5:有256个核,核大小为3 × 3 × 192。卷积层C5与C3、C4层相比多了个池化,池化核size同样为3×3,stride为2。
- 全连接F6:此层的全连接实际上是通过卷积进行的,输入6×6×256,4096个6×6×256的卷积核,扩充边缘padding = 0, 步长stride = 1, 因此其FeatureMap大小为(6-6+0×2+1)/1 = 1,即1×1×4096;
-
创新特点:
-
更深的神经网络结构
- AlexNet 是首个真正意义上的深度卷积神经网络,它的深度达到了当时先前神经网络的数倍。通过增加网络深度,AlexNet 能够更好地学习数据集的特征,从而提高了图像分类的精度。
-
ReLU激活函数的使用
- AlexNet 首次使用了修正线性单元ReLU这一非线性激活函数。相比于传统的 sigmoid 和 tanh 函数,ReLU 能够在保持计算速度的同时,有效地解决了梯度消失问题,从而使得训练更加高效。
-
ReLU激活函数的使用
- 对于每个特征图上的每个位置,计算该位置周围的像素的平方和,然后将当前位置的像素值除以这个和。
- LRN是在卷积层和池化层之间添加的一种归一化操作。在卷积层中,每个卷积核都对应一个特征图(feature map),LRN就是对这些特征图进行归一化。具体来说,对于每个特征图上的每个位置,计算该位置周围的像素的平方和,然后将当前位置的像素值除以这个和。
- 对于每个特征图上的每个位置,计算该位置周围的像素的平方和,然后将当前位置的像素值除以这个和。
-
数据增强和Dropout
- 数据增强:通过随机裁剪、水平翻转图像以及对 RGB 通道强度进行 PCA 变换来扩大数据集,减少过拟合,降低错误率。
- Dropout:在全连接层使用 Dropout 技术,以 0.5 概率随机失活神经元,减少神经元间复杂的协同适应,防止过拟合,测试时将神经元输出乘以 0.5。
-
大规模分布式训练
- AlexNet在使用GPU进行训练时,可将卷积层和全连接层分别放到不同的GPU上进行并行计算,从而大大加快了训练速度。像这种大规模 GPU 集群进行分布式训练的方法在后来的深度学习中也得到了广泛的应用。
-
-
分布式GPU的使用方式
-
将网络分布在两个 GTX 580 GPU 上进行训练。两个 GPU 之间能够直接读写彼此内存,无需通过主机内存,这为跨 GPU 并行化提供了便利。
在分布式训练时,各层在两个 GPU 上有不同的分工,如下图:
-
对模型压缩的启发:
-
模型分割与并行化
-
减少冗余与参数共享
如:C4、C5卷积层仅与同GPU的特征图相连。(参数共享、剪枝) -
优化训练策略(调整学习率和优化器参数)
-
利用硬件特性(使用低精度计算、设计轻量级网络结构)
-
-
总结
-
AlexNet的架构与LeNet相似,但使用了更多的卷积层和更多的参数来拟合大规模的ImageNet数据集。
-
现在,AlexNet已经被更有效的架构所超越,但它是从浅层网络到深层网络的关键一步。
-
尽管AlexNet的代码只比LeNet多出几行,但学术界花了很多年才接受深度学习这一概念,并应用其出色的实验结果。这也是由于缺乏有效的计算工具。
-
Dropout、ReLU和预处理是提升计算机视觉任务性能的其他关键步骤。
代码实现
import torch
from torch import nn
from d2l import torch as d2lnet = nn.Sequential(# 这里使用一个11*11的更大窗口来捕捉对象。# 同时,步幅为4,以减少输出的高度和宽度。# 另外,输出通道的数目远大于LeNetnn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2),# 减小卷积窗口,使用填充为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(),# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合nn.Linear(6400, 4096),nn.ReLU(),nn.Dropout(p=0.5),nn.Linear(4096, 4096), nn.ReLU(),nn.Dropout(p=0.5),# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000nn.Linear(4096, 10)
)# 构造一个高度和宽度都为224的(单通道数据,来观察每一层输出的形状
X = torch.randn(1, 1, 224, 224)
for layer in net:X = layer(X)print(layer.__class__.__name__,'output shape:\t', X.shape)# 读取数据集
batch_size = 128
# Fashion-MNIST图像的分辨率 低于ImageNet图像。:我们将它们增加到224 x224;这里是为了在AlexNet上使用该数据集;一般不会使用resize=224
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())
结果:
可以看到损失比在LeNet上降了很多,训练精度和测试精度都大幅提高.
这里是在Kaggle上跑的,自己电脑根本跑不动(/(ㄒoㄒ)/~~)
VGG
- 设计背景:AlexNet 虽证明深层神经网络有效,但未提供通用设计模板。随着神经网络架构设计的抽象化发展,研究人员转向以块为单位设计网络,VGG 网络便是使用块进行设计的典型代表。
- 经典卷积神经网络的基本组成部分:
- 带填充以保持分辨率的卷积层;
- 非线性激活函数,如ReLU;
- 汇聚(池化)层,如最大汇聚(池化)层。
一个VGG块与之类似,由一系列的卷积层组成,后面再加上用于空间下采样的最大池化层。
介绍
概念说明:卷积层全部为3*3的卷积核,用conv3-xxx来表示,xxx表示通道数。
在VGG论文中展示了六次实验的结果:
其中,详细为:
A:简单的卷积神经网络结构
A-LRN:在A的基础上加了LRN
B:在A的基础上加了两个conv3,即多加了两个33卷积核
C:B的基础上加了三个conv1,即多加了三个11卷积核
D:C的基础上把三个conv1换成了三个33卷积核
E:D的基础上加了三个conv3,即多加了三个33卷积核
- 从以上的实验中可以得到:
- A和A-LRN对比,LRN并没有得到好的效果,可去除
- C和D对比:Conv3 比Conv1 更有效
- 统筹看这六个实验,可发现:随着网络层数的加深,模型的表现会越来越好
总结
- VGG使用 可复用的卷积块构造网络。不同的VGG模型可通过每个块中卷积层数量和输出通道数量的 差异来定义。
- 块的使用导致网络定义的非常简洁,使用块可以有效地设计复杂的网络。
- 在VGG论文中,Simonyan和Ziserman尝试了各种架构。特别是他们发现 深层且窄的卷积(即3 × 3)比 较浅层且宽的卷积更有效。
代码实现
import torch
from torch import nn
from d2l import torch as d2l# 实现 VGG块
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_channelslayers.append(nn.MaxPool2d(kernel_size=2, stride=2))return nn.Sequential(*layers)# VGG 网络: 主要由卷积层和汇聚层组成,第二部分由全连接层组成
# 原始VGG网络有5个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层。
# 第一个模块有64个输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到512。
# 由于该网络使用8个卷积层和3个全连接层,因此它通常被称为VGG-11。
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))# 下面代码实现了VGG-11
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_channelsreturn 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)# 接下来,构建一个高度和宽度为224的单通道数据样本,来观察每个层输出的形状
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:X = blk(X)print(blk.__class__.__name__,'output shape:\t',X.shape)# 训练模型
# [由于VGG-11比AlexNet计算量更大,因此我们构建了一个通道数较少的网络,足够用于训练Fashion-MNIST
ratio = 4
# 这行代码的主要目的是对 conv_arch 列表中的每个元素进行处理,将每个元素(一个二元组)的第二个值除以 ratio 后得到一个新的二元组,然后把这些新的二元组组合成一个新的列表 small_conv_arch。
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, 28
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
共同的设计模式(LeNet、AlexNet和VGG):通过一系列的卷积层与池化层来提取空间结构特征;然后通过全连接层对特征的表征进行处理。(AlexNet和VGG对LeNet的改进主要在于如何扩大和加深这两个模块)
带来的问题:使用了全连接层,可能会完全放弃表征的空间结构。
解决:网络中的网络(NiN)提供了一个非常简单的解决方案:在每个像素的通道上分别使用多层感知机(具体实现:使用多个连续的1 × 1 卷积层(被视为 MLP),对每个像素点进行多层的非线性特征变换)。
介绍
- NiN块
卷积层输入和输出:由四维张量组成,每个轴对应样本、通道、高度、宽度。
全连接层输入和输出:二维张量,对应样本、特征。
NiN的想法:在每个像素位置(针对每个高度和宽度)应用一个全连接层。
1×1 卷积层可看作逐像素的全连接层,增
加每像素的非线性变换,还能调整通道数,
在减少参数的同时丰富特征表达。
总结
-
NiN使用由一个卷积层和多个1 x 1卷积层组成的块。该块可以在卷积神经网络中使用,以允许更多的每像素非线性。
-
NiN去除了容易造成过拟合的全连接层,将它们替换为全局平均汇聚层(即在所有位置上进行求和)。该汇聚层通道数量为所需的输出数量(例如,Fashion-MNIST的输出为10)。
-
移除全连接层可减少过拟合,同时显著减少NiN的参数。
-
NiN的设计影响了许多后续卷积神经网络的设计。
代码实现
import torch
from torch import nn
from d2l import torch as d2l# NiN块
# NiN的想法是在每个像素位置(针对每个高度和宽度)应用一个全连接层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())# NiN 模型
# NiN使用窗口形状为 11×11 、 5×5 和 3×3 的卷积层,输出通道数量与AlexNet中的相同。
# 每个NiN块后有一个最大汇聚层,汇聚窗口形状为 3×3 ,步幅为2。net = nn.Sequential(nin_block(1, 96, kernel_size=11, strides=4, padding=0),nn.MaxPool2d(3, stride=2),nin_block(96, 256, kernel_size=5, strides=1, padding=2),nn.MaxPool2d(3, stride=2),nin_block(256, 384, kernel_size=3, strides=1, padding=1),nn.MaxPool2d(3, stride=2),nn.Dropout(0.5),# 标签类别数是10nin_block(384, 10, kernel_size=3, strides=1, padding=1),nn.AdaptiveAvgPool2d((1, 1)),# 将四维的输出转为二维的输出,其形状为(批量大小,10)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)# 使用Fashion-MNIST来训练模型
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())
结果:
GoogleNet
设计思想:吸收了NiN中串联网络的思想,并在此基础上做了改进。
重点:解决了什么样大小的卷积核最合适的问题
介绍
- Inception块是 GoogLeNet 的基本卷积块。
- 由四条并行路径构成,前三条路径分别使用 1×1、3×3 和 5×5 的卷积层提取不同空间大小的信息,中间两条路径先通过 1×1 卷积层减少通道数,以降低模型复杂度。第四条路径先使用 3×3 最大汇聚层,再用 1×1 卷积层改变通道数。
- 四条路径都使用合适的填充来使输入与输出的高和宽一致,最后将每条线路的输出在通道维度上连结构成输出。
- 在Inception块中,通常调整的超参数是每层输出通道数。
- 模型框架
一共使用9个Inception块和全局平均汇聚层的堆叠来生成其估计值.
总结
-
Inception块相当于一个有4条路径的子网络。它通过不同窗口形状的卷积层和最大汇聚层来并行抽取信息,并使用1 X 1卷积层减少每像素级别上的通道维数从而降低模型复杂度。
-
GoogLeNet将多个设计精细的Inception块与其他层(卷积层、全连接层)串联起来。其中Inception块的通道数分配之比是在ImageNet数据集上通过大量的实验得来的。
-
GoogLeNet和它的后继者们一度是ImageNet上最有效的模型之一:它以较低的计算复杂度提供了类似的测试精度。
GoogLeNet有一些后续版本:
-
添加批量规范化层 (Ioffe and Szegedy, 2015)(batch normalization)
-
对Inception模块进行调整 (Szegedy et al., 2016);
-
使用标签平滑(label smoothing)进行模型正则化 (Szegedy et al., 2016);
-
加入残差连接 (Szegedy et al., 2017)。( 下节将介绍)。
代码实现
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l# (Inception块)
# Inception块由四条并行路径组成。
# 前三条路径使用窗口大小为 1×1 、 3×3 和 5×5 的卷积层,从不同空间大小中提取信息class Inception(nn.Module):# c1--c4是每条路径的输出通道数def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):super(Inception, self).__init__(**kwargs)# 线路1:单1 x 1 卷积层self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)# 线路2:1x1卷积后接 3x3卷积层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)# 线路3:1x1卷积层后接 5 x 5卷积层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)# 线路4:3x3最大汇聚层后接 1x1卷积层self.p4_1 = nn.MaxPool2d(kernel_size=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)# 滤波器(filter)的组合,它们可以用各种滤波器尺寸探索图像,这意味着不同大小的滤波器可以有效地识别不同范围的图像细节。
# 同时,我们可以为不同的滤波器分配不同数量的参数。
# GoogLeNet一共使用9个Inception块和全局平均汇聚层的堆叠来生成其估计值。# 逐一来实现GoogleNet 的每个模块,第一个模块使用64个通道、7 x 7 卷积层
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个通道、1 * 1 卷积层;
# 第二个人卷积层使用将通道数量增加三倍的3 * 3 卷积层, 对应于Inception块中的第二天路径
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)
)# 第三个模块串联两个完整的Inception块
# 第三个模块串联两个完整的Inception块。
# 第一个Inception块的输出通道数为 64+128+32+32=256 ,四个路径之间的输出通道数量比为 64:128:32:32=2:4:1:1 。
# 第二个和第三个路径首先将输入通道的数量分别减少到 96/192=1/2 和 16/192=1/12 然后连接第二个卷积层。
# 第二个Inception块的输出通道数增加到 128+192+96+64=480 ,四个路径之间的输出通道数量比为 128:192:96:64=4:6:3:2 。
# 第二条和第三条路径首先将输入通道的数量分别减少到 128/256=1/2 和 32/256=1/8 。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)
)# 第4个模块,串联了五个Inception块
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)
)
# 第五模块包含输出通道数为 256+320+128+128=832 和 384+384+128+128=1024 的两个Inception块
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))# 模型计算复杂,不如VGG那样便于修改通道数
# 为了使Fashion-MNIST上的训练短小精悍,我们将输入的高和宽从224降到96]
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:X = layer(X)print(layer.__class__.__name__, 'output shape:\t', X.shape)# 训练模型
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())
结果:
批量规范化
批量规范化是在卷积层或全连接层之后、相应的激活函数之前应用的。
-
先计算小批量的均值和标准差:
-
BN公式: 输入x∈B
其中, 可学习参数:拉伸参数(scale)γ和偏移参数(shift)β;
应用标准化后,生成的小批量的平均值为0和单位方差为1。 -
标准化的作用:
- 标准化输入特征可统一参数量级,有助于优化器工作;
- 中间层变量变化范围大,其分布偏移会阻碍网络收敛。不同层变量值相差较大时,需针对性的调整学习率;
- 深层网络结构复杂,容易出现过拟合现象。
ResNet
-
函数类角度
-
具有X特性和y标签的数据集:
F: 神经网络架构(包含学习率及其他超参数设置)
-
当我们想要训练一个更强大的F’,需要满足 F ∈ F’,即较复杂函数类包含较小函数类。
-
也就是说: 新模型的函数类包含了原模型的函数,新模型具备原模型的所有能力,在处理相同任务时,至少能达到与原模型相同的效果。
-
-
针对深度神经网络, 将新添加层训练成恒等映射:
残差网络的核心思想:每个附加层都应更容易地包含原始函数作为其元素之一。 于是,残差块(residual blocks)便诞生了。
介绍
-
残差块
- 对于深度神经网络,学习恒等映射相对容易。以残差块为例,在残差块中拟合 f(x) - x 残差映射比直接拟合f(x)理想映射更简单。当理想映射f(x)极接近于恒等映射时,残差映射更易于捕捉恒等映射的细微波动。
- 将新添加层训练为恒等映射,其权重和偏置参数可近似为零,从优化的角度,这种简单的映射关系使得训练过程更稳定,不易出现梯度消失或爆炸等问题,从而保证新模型能达到与原模型相同的性能水平。
-
模型框架
- 前两层与GoogleNet一样,不同之处在于ResNet每个卷积层后增加了批量规范化层。
- 每个模块有4个卷积层(不包括恒等映射的1 * 1 卷积层)。 加上第一个卷积层和最后一个全连接层,共有18层。 因此,这种模型通常被称为ResNet-18。 (通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。)
- 虽然ResNet的主体架构跟GoogLeNet类似,但ResNet架构更简单,修改也更方便.
总结
-
学习嵌套函数(nested function)是训练神经网络的理想情况。在深层神经网络中,学习另一层作为恒等映射(identity function)较容易(尽管这是一个极端情况)。
-
残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零。
-
利用残差块(residual blocks)可以训练出一个有效的深层神经网络:输入可以通过层间的残余连接更快地向前传播。
代码实现
!pip install d2l
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l# 残差块定义
# 残差块里首先有2个有相同输出通道数的 3×3 卷积层。 每个卷积层后接一个批量规范化层和ReLU激活函数。
# 然后我们通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。
# 这样的设计要求2个卷积层的输出与输入形状一样,从而使它们可以相加# 实现如下:
class 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, padding=1, stride=strides)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 = Noneself.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 += Xreturn F.relu(Y)# 查看[输入和输出形状一致]
blk = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
Y.shape# 增加输出通道数的同时,减半输出的高和宽]。
blk = Residual(3,6, use_1x1conv=True, strides=2)
blk(X).shape# ResNet模型
# 前两层跟之前介绍的GoogLeNet中的一样: 在输出通道数为64、步幅为2的 7×7 卷积层后,接步幅为2的 3×3 的最大汇聚层
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)
)# ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。
# 第一个模块的通道数同输入通道数一致
# 注意:对第一个模块做了特别处理。
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# 接着在ResNet加入所有残差块,这里每个模块使用2个残差块。
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=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))# 最后,在ResNet中加入全局平均汇聚层,以及全连接层输出。
net = nn.Sequential(b1,b2,b3,b4,b5,nn.AdaptiveAvgPool2d((1,1)),nn.Flatten(), nn.Linear(512, 10)
)# 观察一下ResNet中不同模块的输入形状是如何变化的
# X = torch.rand(size=(1, 1, 224, 224))
# for layer in net:
# X = layer(X)
# print(layer.__class__.__name__,'output shape:\t', X.shape)# 训练模型
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())
结果:
使用残差网络可以看出训练损失降了很多,并且训练精度和测试精度有大幅提高;不过两者存在一定的gap。
DenseNet
-
从 ResNet 到 DenseNet:
-
继续拓展:
-
稠密连接:
稠密网络由2部分构成:稠密块(dense block)和过渡层(transition layer)。
前者定义如何连接输入和输出,而后者则控制通道数量。
介绍
-
稠密块(dense block)
-
由多个使用相同数量输出通道的卷积块组成
-
卷积块架构: BN - ReLU - Conv (bottleneck)
-
主要作用:实现特征重用(即对不同“级别”的特征——不同表征进行总体性地再探索)
-
-
过渡层(transition layer)
- 主要用于连接两个相邻的DenseBlock。
- 包括一个1x1卷积(用于调整通道数)和2x2AvgPooling(用于降低特征图大小),结构为BN+ReLU+1x1 Conv+2x2 AvgPooling。
- 作用:Transition层可以起到压缩模型的作用。
-
超参数调节
- θ取值(0,1],当θ=1时,feature维度不变,即无压缩;而θ<1时,这种结构称为DenseNet-C(文中使用θ=0.5);对于使用bottleneck层的DenseBlock结构和压缩系数小于1的Transition组合结构称为DenseNet-BC。
-
网络结构
DenseNet的网络结构主要由DenseBlock和Transition组成,一个DenseNet中有3个或4个DenseBlock。而一个DenseBlock中也会有多个Bottleneck layers。最后的DenseBlock之后是一个global AvgPooling层,然后送入一个softmax分类器,得到每个类别所属分数。
总结
代码实现
import torch
from torch import nn
from d2l import torch as d2l# 使用 ResNet 改良版中 "BN、ReLU、Conv"架构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(DenseBlock, self).__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), dim=1)return X# 定义一个有2个输出通道数为10的(DenseBlock)
# 使用通道数为3的输入时,我们会得到通道数为 3+2×10=23 的输出blk = DenseBlock(2, 3, 10)
X = torch.randn(4, 3, 8, 8)
Y = blk(X)
Y.shape# 过渡层
# 由于每个稠密块都会带来通道数的增加,使用过多则会过于复杂化模型。
# 而过渡层可以用来控制模型复杂度。
# 它通过 1×1 卷积层来减小通道数,并使用步幅为2的平均汇聚层减半高和宽,从而进一步降低模型复杂度。
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))# 使用 通道数为10的[过渡层]。 此时输出的通道数减为10,高和宽均减半。
blk = transition_block(23, 10)
blk(Y).shape# 构造 DenseNet模型
# DenseNet首先使用同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)
)# DenseNet使用的是4个稠密块
# num_channels为当前的通道数
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 = num_channels // 2# 最后接上全局汇聚层和全连接层来输出结果
net = nn.Sequential(b1, *blks,nn.BatchNorm2d(num_channels), nn.ReLU(),nn.AdaptiveAvgPool2d((1, 1)),nn.Flatten(),nn.Linear(num_channels, 10))# 训练模型lr, num_epochs, batch_size = 0.1, 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())
结果:
MobileNet
-
研究小而高效的CNN模型(思想):
- 一是对训练好的复杂模型进行压缩得到小模型;
- 二是直接设计小模型并进行训练。
-
核心目标:在保持模型性能(accuracy)的前提下降低模型大小(parameters size),同时提升模型速度(speed, low latency)
MobileNet 模型是 Google 针对手机等嵌入式设备提出的一种轻量级的深层神经网络.
V1 介绍
主要操作:
(1)将普通卷积换成了深度可分离卷积;
(2)引入了两个超参数使得可以根据资源来更加灵活的控制自己模型的大小。
-
深度可分离卷积(Depthwise separable convolution)
(追溯历史,可分为空间可分离卷积和深度可分离卷积)-
分解为两个操作: 深度卷积(depthwise convolution) 和点卷积( pointwise convolution)
-
回忆下标准卷积:
-
深度卷积(depthwise convolution)
- 对输入特征图的每个通道分别应用一个滤波器
- 对输入特征图的每个通道分别应用一个滤波器
-
点卷积( pointwise convolution)
- 用 1*1 卷积进行升维和降维
- 用 1*1 卷积进行升维和降维
-
-
与传统卷积的不同:
-
为什么使用ReLU6?
- 与relu函数区别: 对输入的最大输出值做了约束.
- 主要原因解释: 为了在移动端float16/int8等低精度设备时,也能够有很好的数值分辨率。如果对Relu的值没有加以任何限制,则输出范围可以从0到无限大,这就使得激活值很大,分布在一个很大的范围内,而低精度的float16等嵌入式设备就无法很好的精确描述如此大的范围,从而带来精度损失。(创新点1)
-
创新点2:虽然MobileNet网络结构和延迟已经比较小了,但是很多时候在特定应用下还是需要更小更快的模型,为此引入了宽度因子α,为了控制模型大小,引入了分辨因子ρ。
- 宽度因子α(Width Mutiplier)
- 通常α在(0, 1] 之间,比较典型的值由1, 0.75, 0.5, 0.25
- 在每一层对网络的输入输出通道数进行缩减,输出通道数由 M 到 αM,输出通道数由 N 到 αN,变换后的计算量为:
- 分辨因子ρ(resolution multiplier)
- 通常ρ在(0, 1] 之间,比较典型的输入分辨为 224, 192, 160, 128;
- 用于控制输入和内部层表示,即用分辨率因子控制输入的分辨率,深度卷积和逐点卷积的计算量为:
- 宽度因子α(Width Mutiplier)
在MobileNetV1中,深度卷积网络的每个输入通道都应用了单个滤波器。然后,逐点卷积应用 1*1 卷积网络来合并深度卷积的输出。这种标准卷积方法既能滤波,又能一步将输入合并成一组新的输出。
- 网络结构
- 一共由 28层构成(不包括AvgPool 和 FC 层,且把深度卷积和逐点卷积分开算)
其除了第一层采用的是标准卷积核之外,剩下的卷积层都是用Depth Wise Separable Convolution。
- 一共由 28层构成(不包括AvgPool 和 FC 层,且把深度卷积和逐点卷积分开算)
下面简要对 V2 和 V3 进行一些介绍
-
MobileNet V2
- 在V1的基础上,引入了倒残差块(Inverted Residual Block)和线性激活函数(Linear Activation)。这些改进使得V2在保持轻量级特性的同时,实现了更高的准确性和更低的延迟。倒残差块的设计有助于保留和增强特征信息,改善了模型在低资源环境中的表现。
-
MobileNet V3
- 进一步对V2进行了全面改进,采用了HardSwish激活函数、挤压励磁模块(Squeeze-and-Excitation Block),以及MnasNet和NetAdapt等**网络架构搜索(NAS)**技术。这些技术使得V3在保持高性能的同时,实现了更快的推理速度和更小的模型尺寸。
MobileNet V1代码实现
import torch
from torch import nn
from d2l import torch as d2l# 定义深度可分离卷积层
class DepthwiseSeparableConv(nn.Module):def __init__(self, in_channels, out_channels, stride=1):super(DepthwiseSeparableConv, self).__init__()# 深度卷积self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=stride, padding=1, groups=in_channels, bias=False)self.bn1 = nn.BatchNorm2d(in_channels)self.relu1 = nn.ReLU(inplace=True)# 逐点卷积self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)self.bn2 = nn.BatchNorm2d(out_channels)self.relu2 = nn.ReLU(inplace=True)def forward(self, x):x = self.relu1(self.bn1(self.depthwise(x)))x = self.relu2(self.bn2(self.pointwise(x)))return x# 定义 MobileNet V1 网络
class MobileNetV1(nn.Module):def __init__(self, num_classes=10):super(MobileNetV1, self).__init__()# 初始卷积层self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1, bias=False)self.bn1 = nn.BatchNorm2d(32)self.relu1 = nn.ReLU(inplace=True)# 深度可分离卷积层self.layers = nn.Sequential(DepthwiseSeparableConv(32, 64, stride=1),DepthwiseSeparableConv(64, 128, stride=2),DepthwiseSeparableConv(128, 128, stride=1),DepthwiseSeparableConv(128, 256, stride=2),DepthwiseSeparableConv(256, 256, stride=1),DepthwiseSeparableConv(256, 512, stride=2),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 1024, stride=2),DepthwiseSeparableConv(1024, 1024, stride=1))# 全局平均池化层self.avgpool = nn.AdaptiveAvgPool2d((1, 1))# 全连接层self.fc = nn.Linear(1024, num_classes)def forward(self, x):x = self.relu1(self.bn1(self.conv1(x)))x = self.layers(x)x = self.avgpool(x)x = x.view(x.size(0), -1)x = self.fc(x)return x# 初始化模型
net = MobileNetV1()# 训练模型
lr, num_epochs, batch_size = 0.1, 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())
结果:
总结
- AlexNet :首次运用大型神经网络在大规模视觉竞赛中战胜传统计算机视觉模型,开启深度卷积神经网络应用的新时代。
- VGG :利用大量重复的神经网络块构建模型,探索出一种规整化的网络构建方式,提升了网络的可扩展性和性能。
- NiN :创新性地用卷积层和 1×1 卷积层替代全连接层构建深层网络,减少模型参数数量,提升计算效率。
- GoogLeNet :通过设计 Inception 块,运用不同窗口大小的卷积层和最大汇聚层并行抽取信息,有效提高模型对图像特征的提取能力。(宽度方向)
- ResNet :引入残差块构建跨层数据通道,解决了深层网络训练中的梯度消失等问题,极大地加深了网络深度,提升模型性能。(深度方向)
- DenseNet :采用稠密连接方式,在通道维上连结各层输入与输出,增强了特征传播与复用能力,在一定程度上提升了模型效果,但计算成本较高。(从feature上入手)
- MobileNet 阶段:针对移动端等资源受限场景,提出深度可分离卷积,将普通卷积分解为深度卷积和逐点卷积,在大幅降低计算量与模型大小的同时,保持了较好的分类精度,推动了卷积神经网络在移动设备等领域的广泛应用.