卷积神经网络发展进程
1、卷积神经网络(LeNet)
LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关 注。这个模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名),目的是识别图像 (LeCun et al., 1998)中的手写数字。当时,Yann LeCun发表了第一篇通过反向传播成功训练卷积神经网络的 研究,这项工作代表了十多年来神经网络研究开发的成果。
总体来看,LeNet(LeNet‐5)由两个部分组成:
- 卷积编码器:由两个卷积层组成;
- 全连接层密集块:由三个全连接层组成。
每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均汇聚层。请注意,虽然ReLU和最大汇聚层更有效,但它们在20世纪90年代还没有出现。每个卷积层使用5 × 5卷积核和一个sigmoid激活函数。这 些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有6个输出通道,而第二个卷 积层有16个输出通道。每个2 × 2池操作(步幅2)通过空间下采样将维数减少4倍。卷积的输出形状由批量大小、通道数、高度、宽度决定。
为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。换言之,我们将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。LeNet的稠密块有三个全连接层,分别有120、84和10个输出。因为我们在执行分类任务, 所以输出层的10维对应于最后输出结果的数量。
1.1 代码演示
import torch
from d2l import torch as d2l
from torch import nn
# 定义模型
net = 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)
)
batch_size = 256
# 将mnist分为训练数据和测试数据
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
def evaluate_accuracy_gpu(net, data_iter, device=None):
"""使用GPU计算模型在数据集上的精度
net: 神经网络模型,预期是一个torch.nn.Module的实例。
data_iter: 数据迭代器,用于迭代访问数据集中的样本。
device: 指定计算应该在哪个设备上执行(CPU或GPU)。如果未指定,则自动从net的参数中推断出设备。
"""
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device # 确定设备,确保数据和模型在同一个设备上
# 用于累积两个值:正确预测的数量和总预测的数量。这个累积器在循环中用于计算准确率。
metric = d2l.Accumulator(2)
# 上下文管理器禁用梯度计算,因为我们在评估模式下不需要计算梯度。然后,遍历数据迭代器中的每一批数据
with torch.no_grad():
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
# 计算当前批次数据的准确率,并将其与当前批次中的样本数一起添加到累积器中。
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训练模型"""
# 初始化权重,使用Xavier均匀初始化方法,防止梯度爆炸或梯度消失
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
# 将模型移动到指定的设备中
net.to(device)
# 梯度下降优化方法
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
# 交叉熵损失函数
loss = nn.CrossEntropyLoss()
# 可视化训练过程中的损失和准确率
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 重置累计器,用于记录训练损失,训练精准率和样本数
metric = d2l.Accumulator(3)
# 将模式设置为训练模式
net.train()
"""
遍历训练数据迭代器:
对每个批次的数据,首先将其移动到指定设备。
前向传播,计算预测值。
计算损失。
反向传播,计算梯度。
更新模型参数。
在不计算梯度的情况下(torch.no_grad()),计算并累积损失、准确率和样本数。
如果达到一定的批次间隔或到达最后一个批次,更新动画器以显示当前的训练损失和准确率。
计算并显示测试集上的准确率。
"""
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))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
# 打印当前轮次的训练损失、训练准确率和测试准确率
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)}')
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
1.2 训练结果
1.3 创新点
深度学习的基本过程:前向传播、损失计算、反向传播和参数更新。
创新点1:使用GPU进行模型的训练。在训练LeNet或任何大型神经网络时,使用GPU可以显著加速计算过程,因为GPU能够并行处理大量数据。
创新点2:使用BERT进行模型的优化。
BERT(Bidirectional Encoder Representations from Transformers)是一个预训练的Transformer模型,通常用于自然语言处理任务。虽然BERT本身不直接用于优化神经网络参数(如通过梯度下降),但它提供了一种强大的表示学习方法,可以通过微调(fine-tuning)来适应各种下游任务。
尽管BERT主要用于NLP任务,但研究人员已经探索了将其与CNN结合用于图像或其他类型数据的方法。这种结合可能涉及将BERT用于提取图像的文本描述或元数据的特征,然后将这些特征与CNN从图像中直接提取的特征相结合。然而,这种结合并不是直接优化LeNet或CNN的参数,而是提供了一种特征融合的方式。
2、深度卷积神经网络(AlexNet)
2012年,AlexNet横空出世。它首次证明了学习到的特征可以超越手工设计的特征。它一举打破了计算机视 觉研究的现状。AlexNet使用了8层卷积神经网络,并以很大的优势赢得了2012年ImageNet图像识别挑战赛。
2.1 AlexNet和LeNet的不同之处
- AlexNet比相对较小的LeNet5要深得多。AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。
- AlexNet使用ReLU而不是sigmoid作为其激活函数-
1)模型设计方面
在AlexNet的第一层,卷积窗口的形状是11×11。由于ImageNet中大多数图像的宽和高比MNIST图像的多10倍 以上,因此,需要一个更大的卷积窗口来捕获目标。第二层中的卷积窗口形状被缩减为5×5,然后是3×3。此 外,在第一层、第二层和第五层卷积层之后,加入窗口形状为3 × 3、步幅为2的最大汇聚层。而且,AlexNet的 卷积通道数目是LeNet的10倍。 在最后一个卷积层后有两个全连接层,分别有4096个输出。这两个巨大的全连接层拥有将近1GB的模型参数。 由于早期GPU显存有限,原版的AlexNet采用了双数据流设计,使得每个GPU只负责存储和计算模型的一半 参数。幸运的是,现在GPU显存相对充裕,所以现在很少需要跨GPU分解模型。
2)激活函数
此外,AlexNet将sigmoid激活函数改为更简单的ReLU激活函数。一方面,ReLU激活函数的计算更简单,它不需要如sigmoid激活函数那般复杂的求幂运算。另一方面,当使用不同的参数初始化方法时,ReLU激活函 数使训练模型更加容易。当sigmoid激活函数的输出非常接近于0或1时,这些区域的梯度几乎为0,因此反向传播无法继续更新一些模型参数。相反,ReLU激活函数在正区间的梯度总是1。因此,如果模型参数没有正确初始化,sigmoid函数可能在正区间内得到几乎为0的梯度,从而使模型无法得到有效的训练。
3)容量控制和预处理
AlexNet通过暂退法(dropout)控制全连接层的模型复杂度,而LeNet只使用了权重衰减。为了进一步扩充数 据,AlexNet在训练时增加了大量的图像增强数据,如翻转、裁切和变色。这使得模型更健壮,更大的样本量 有效地减少了过拟合。
补充
暂退法:暂退法的主要思想是在训练过程中,按照一定的概率暂时丢弃(即设置为0)神经网络中的部分神经元(或称为隐藏单元),从而减少模型复杂度,避免过拟合。这种方法可以看作是集成学习的一种简化形式,通过随机丢弃神经元来生成多个子网络,并在训练过程中共享参数。
权重衰减:权重衰减通过在损失函数中添加一个正则化项(通常是权重的平方和)来惩罚模型中较大的权重值,从而减小模型的复杂度。这种正则化项使得模型在训练过程中倾向于学习较小的权重,进而减少过拟合的风险。
2.2 代码演示
from d2l import torch as d2l
from torch import nn
net = nn.Sequential(
# 这用一个11*11的更大窗口来捕捉对象,步幅为4,以减少输出的高度和宽度。另外,输出通道的数目远大于LeNet
nn.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(),
# Dropout暂退法,随机将一半的神经网络输出设置为0
nn.Dropout(p=0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10)
)
batch_size = 128
# Fashion‐MNIST图像的分辨率(28 × 28像素)低于ImageNet图像。为了解决这个问题,将它们增加到224 × 224,为了适应AlexNet
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())
2.3 训练结果
2.4 创新点
提出了暂退法(Dropout),并且使用了更多的卷积层和更多的参数来拟合数据集。
3、使用块的网络(VGG)
使用块的想法首先出现在牛津大学的视觉几何组(visual geometry group)的VGG网络中。通过使用循环和子程序,可以很容易地在任何现代深度学习框架的代码中实现这些重复的架构。
经典卷积神经网络的基本组成部分是下面的这个序列:
-
带填充以保持分辨率的卷积层;
-
非线性激活函数,如ReLU;
-
汇聚层,如最大汇聚层。
而一个VGG块与之类似,由一系列卷积层组成,后面再加上用于空间下采样的最大汇聚层。在最初的VGG论文中,作者使用了带有3 × 3卷积核、填充为1(保持高度和宽度)的卷积层, 和带有2 × 2汇聚窗口、步幅为2(每个块后的分辨率减半)的最大汇聚层。
3.1代码演示
from d2l.torch import d2l
from torch import nn
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
# 核函数规格3*3,零填充为1,经过卷积层后,输出大小不变
# 原来是112*112,输出后仍然是112*112,(112-3+1*2)/1+1=112
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
# 激活函数
layers.append(nn.ReLU())
in_channels = out_channels
# 池化层采用最大汇聚的方式,汇聚层规格2*2,面试每次缩小为原来的四分之一
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
return nn.Sequential(*layers)
# 这里的1是指重复多少次卷积层
conv_arch = ((1, 64), (1, 128), (1, 256), (1, 512), (1, 512))
def vgg(conv_arch):
conv_blks = []
in_channels = 1
# 卷积层部分
# for循环遍历卷积层输入通道和输出通道
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))
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())
3.2训练结果
3.3 创新点
使用块的思想实现神经网络,使得网络的定义变得简洁,有效的设计出复杂的网络
**注:**在VGG到ResNet中还有:网络中的网络(NiN)、 含并行连结的网络(GoogLeNet) 这里不再介绍,若感兴趣可自行查找相关资料
4、残差网络(ResNet)
随着神经网络研究的不断深度,针对于激活函数的使用,从sigmoid函数到relu函数,但都存在这样一个问题:梯度在反向传播的过程中可能会出现梯度消失或者梯度爆炸,导致深层神经网络学习起来比较困难。同时,在训练的过程中,可能会另外一种情况:学习了没有必要或不好的特征并延续下去。对于下一层网络而言,这种特征是无用的,甚至是起干扰作用,下一层学习的效果不如之前学习的效果好,但人为无法在运行过程手工阻止这一问题。便引入了残差网络(ResNet)。
4.1 代码演示
from d2l.torch import d2l
from torch import nn
from torch.nn import functional as F
"""
残差块里首先有2个有相同输出通道数的3 × 3卷积层。每个卷积
层后接一个批量规范化层和ReLU激活函数。然后我们通过跨层数据通路,跳过这2个卷积运算,将输入直接
加在最后的ReLU激活函数前。这样的设计要求2个卷积层的输出与输入形状一样,从而使它们可以相加。如
果想改变通道数,就需要引入一个额外的1 × 1卷积层来将输入变换成需要的形状后再做相加运算
"""
class Residual(nn.Module):
# use_1x1conv:一个布尔值,指示是否使用1x1卷积来调整输入X的维度,以便在将X加到Y上时,它们的维度能够匹配。
def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1):
super().__init__()
# 两个3x3卷积层,用来提取特征
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)
# 仅当use_1x1conv=True时存在,它的作用是调整输入X的通道数或空间维度,以便在加法操作中与Y的维度相匹配。
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides)
else:
self.conv3 = None
# bn1,bn2应用于批量规范化,以加速训练过程和改善泛化能力
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
# 前向传播
def forward(self, X):
# 输入X通过self.conv1,然后是ReLU激活函数和self.bn1进行批量归一化
Y = F.relu(self.bn1(self.conv1(X)))
# 将输出通过conv2和bn2
Y = self.bn2(self.conv2(Y))
# 如果存在conv3则进行残差连接
if self.conv3:
X = self.conv3(X)
Y += X
# 应用到relu激活函数中
return F.relu(Y)
# 存储需要构建的残差块
def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
blk = []
"""
对于第一个残差块(且不是网络的第一个残差块),它使用 Residual 类创建一个新的残差块实例,
输入通道数为 input_channels,输出通道数为 num_channels,并设置 use_1x1conv=True 和 strides=2。
这通常用于在残差网络的早期阶段减少特征图的尺寸(高度和宽度减半),同时增加通道数。
"""
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
# 在输出通道数为64、步幅为2的7 × 7卷积层后,接步幅为2的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))
# b2、b3、b4、b5分别是不同深度的残差块序列,它们通过resnet_block函数生成,并使用nn.Sequential进行封装
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))
net = nn.Sequential(
b1, # 初始的卷积层、批量归一化、ReLU激活和最大池化层
b2, # 第一个残差块序列,输出通道数为64
b3, # 第二个残差块序列,输出通道数增加到128
b4, # 第三个残差块序列,输出通道数增加到256
b5, # 第四个残差块序列,输出通道数增加到512
nn.AdaptiveAvgPool2d((1, 1)), # 自适应平均池化层,将特征图的大小调整为1x1
nn.Flatten(), # 扁平化层,将多维的输入一维化,准备输入到全连接层
nn.Linear(512, 10) # 全连接层,将512维的特征转换为10维的输出,对应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())
4.2 训练结果
4.3 创新点
-
残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零。
-
残差块(residual blocks)可以训练出一个有效的深层神经网络:输入可以通过层间的残余连接更快地向前传播。
-
有效解决了梯度下降和梯度消失的问题
4.4 残差的方法
1)高宽减半(h,w),通道数翻倍。这是因为卷积的步长设为了2。
2)高宽不变,通道数也不变。这是因为卷积的步长设为了1
本文有一个知识点未补充:批量规范化(batch
normalization),链接:循环神经网络这篇文章的末尾。