LeNet网络
手写的数字识别
MNIST
①输入的是:32×32的image
②放到一个5×5的卷积层里面(为什么是5?因为32-x+1=28,∴x=5),然后它的输出通道是6,6个通道数,高宽是28的输出。
③用一个Padding层,2×2的Padding层,就把28×28变成14×14了,通道数没变还是6。
④接下来又是一个卷积层,这个卷积层仍然是一个5×5的,(14-x+1=10,∴x=5),然后通道数由6变成16。
⑤之后再接一个Padding层,高宽减半,通道数不变。
⑥然后把它拉成一个向量,输入到一个全连接层,第一个全连接是一个120,第二个是84,最后一个是高斯层。得到10个数字。
两个卷积层,两个池化层,两个全连接层,最后一个输出层
总结
①LeNet是早期成功的神经网络
②先使用卷积层来学习图片空间信息
③然后使用全连接层来转换到类别空间
卷积神经网络(LeNet)
LeNet(LeNet-5)由两个部分组成:卷积编码器和全连接层密集块。
import torch
from torch import nn
class Reshape(torch.nn.Module):
def forward(self, x):
return x.view(-1, 1, 28, 28) # 批量数自适应得到,通道数为1,图片为28X28
net = torch.nn.Sequential(
# 将1×28×28的图片放到第一个卷积层里面,输入通道是1,输出通道是6,卷积核的尺寸是5×5,填充是2×2
# 为了得到非线性,在卷积后面加了sigmoid激活函数
Reshape(), nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
# 用均值池化层,步长为2
nn.AvgPool2d(2, stride=2),
# 卷积层输入是6,输出是16,kernel不变,然后在使用激活函数
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
# 在使用一个均值池化层,因为卷积层出来是一个4D的,把最后的通道数、高和宽变为一个一维的向量输入到多层感知机
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))
# 从区间 [0, 1) 上的均匀分布中随机抽取的浮点数。参数分别代表批量大小、通道数、高度和宽度
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) # 上一层的输出为这一层的输入
结果:
经过卷积层和激活函数以及平均池化层为什么高度和宽度减半?
在卷积神经网络中,池化层(如AvgPool2d)用于减少数据的空间维度(即高度和宽度),从而减少计算量和参数数量,同时帮助网络学习到空间层次上的抽象特征。
AvgPool2d层的kernel_size(池化窗口大小)和stride(步长)都被设置为2。这意味着在每个2x2的区域内,池化操作会计算这四个值的平均值,并将结果作为该区域的输出。由于步长也是2,这意味着在水平和垂直方向上,每次池化操作都会跳过两个像素(或特征),因此输出特征图的高度和宽度都会减半。
例:有一个4x4的输入特征图
应用一个AvgPool2d(kernel_size=2, stride=2)层后,输出特征图将会是:
即:
输出特征图的大小从4x4变为了2x2,高度和宽度都减半了。故上述减半同理。
LeNet在Fashion-MNIST数据集上的表现
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
要用GPU(CPU也是能跑的,LeNet的是CPU唯一能跑的网络)
# 接受三个参数:net(模型),data_iter(数据集迭代器),以及可选的device(设备,默认为None)
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度"""
# 检查net是否是nn.Module的实例
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
# 如果device参数没有被明确指定(即None),则通过获取模型参数的第一个元素的设备来确定device。
#这确保了模型和数据将被发送到相同的设备上(CPU或GPU)。
if not device:
device = next(iter(net.parameters())).device
# 创捷累加器,两个参数分别代表正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
# 使用torch.no_grad()上下文管理器来关闭梯度计算。在评估模型时,我们不需要计算梯度,这可以节省内存和计算资源。
with torch.no_grad():
# 每次迭代获取一批数据X和对应的标签y。
for X, y in data_iter:
# 检查X是否是列表。
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
# 如果是列表,则将列表中的每个元素都发送到device上。
X = [x.to(device) for x in X]
# 如果不是列表,则直接将X发送到device上。
else:
X = X.to(device)
# 将标签y也发送到device上,以确保模型输入和标签都在同一设备上。
y = y.to(device)
# 计算当前批次数据的准确率,并将该准确率和当前批次的总样本数(y.numel())累加到metric中
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
为了使用GPU,我们还需要一点小改动。与之前不同,在进行正向和反向传播之前,我们需要将每一小批量数据移动到我们指定的设备(例如GPU)上。
训练函数train_ch6将实现多层神经网络,因此我们将主要使用高级API。以下训练函数假定从高级API创建的模型作为输入,并进行相应的优化。使用Xavier随机初始化模型参数。与全连接层一样,我们使用交叉熵损失函数和小批量随机梯度下降。
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型(在第六章定义)"""
def init_weights(m):
# 如果是全连接层和卷积层的话,就使用Xavier均匀初始化
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)
# 设置优化器,使用SGD(随机梯度下降)
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()
# 遍历训练迭代器中的每个批次
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
# 将数据和标签移至指定设备 (GPU)
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]
# 如果当前批次是每5个批次的最后一个或最后一个批次,则更新动画器
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
# 在每个epoch结束时,评估测试集上的准确率
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)}')
训练和评估LeNet-5模型
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
结果:
问题
为什么view而不用reshape呢?
本质上没什么区别,唯一不一样的是view对数据的构造不会发生变化,reshape可以对数据进行一些copy