深度学习10. CNN经典网络 LeNet-5
- 一、 LeNet-5简介
- 二、网络详解
- 1. 输入图像
- 2. 卷积层 C1
- 3. 池化层S2
- 4. 卷积层C3
- 5. 池化层 S4
- 6. 全连接层F5,F6
- 7. 输出层
- 三、 PyTorch的实现
- 图像展平
- 损失函数
一、 LeNet-5简介
LeNet-5是一个经典的卷积神经网络模型,1998年被提出,论文题目是 “Gradient-Based Learning Applied to Document Recognition” ,作者为 Yann LeCun, Léon Bottou, Yoshua Bengio, and Patrick Haffner。
LeNet-5是一个用于手写数字识别的深度神经网络模型,由两个卷积层和三个全连接层组成。
LeNet-5是深度神经网络的开创者之一,对后来的深度学习算法发展产生了重要的影响。
LeNet-5的结构如下:
- 输入层:32x32的图像
- 卷积层C1:使用6个5x5的卷积核,步长为1,激活函数为sigmoid
- 池化层S2:使用2x2的最大池化操作,步长为2
- 卷积层C3:使用16个5x5的卷积核,步长为1,激活函数为sigmoid
- 池化层S4:使用2x2的最大池化操作,步长为2
- 全连接层F5:输出120个神经元,激活函数为sigmoid
- 全连接层F6:输出84个神经元,激活函数为sigmoid
- 输出层:10个神经元,对应10个手写数字类别,使用softmax激活函数
论文地址
识别动图:
二、网络详解
1. 输入图像
LeNet-5使用32*32图像。
本文示例将会使用MNIST实现LeNet-5,数据集包含 60000张28x28像素的训练图像和10000张测试图像。
2. 卷积层 C1
C1 用来提取输入图像的特征,输入是一个2828的灰度图像,共6个卷积核,每 个卷积核大小55,卷积核的深度与输入图像的深度相同(即为1)。
卷积核的参数需要进行学习,每个卷积核都学习一个5x5的参数矩阵,这些参数在整个网络训练的过程中进行更新。
在进行卷积计算之前,C1卷积层对输入图像进行了一些预处理。首先,将28x28的输入图像填充成32x32的大小,这样可以使得卷积核的中心始终在输入图像内部移动,从而避免了边缘像素的信息丢失。然后,对填充后的图像进行了局部响应归一化(local response normalization)操作,这个操作可以增强图像特征的对比度。
接着,C1卷积层对输入图像进行6次卷积计算,每次计算都使用一个卷积核。卷积操作的结果是得到6个28x28的特征图,这些特征图分别对应着6个卷积核在输入图像上的响应。
最后,将得到的6个特征图进行叠加,得到的结果是一个6 x 28 x 28的立方体,这个立方体可以被看作是一个三维的特征图。这个特征图将作为下一个卷积层的输入。
使用PyTorch模拟此层可以使用:
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1)
- 其中参数1 是输入数据通道数,灰度图为1;
- 参数6是输出数据通道数,即卷积核数量;
- kernel_size=5是卷积核大小;
- stride=1是步长。
此层的输入为 batch_size x 1 x 28 x 28,输出图像的形状为 batch_size x 6 x 24 x 24,卷积操作后图像大小为24 x 24。
3. 池化层S2
在 LeNet-5 中,池化层 S2 的作用是对卷积层 C1 的输出进行下采样,减少模型的参数数量,提高模型的泛化能力。
S2 层的输入是 C1 层的输出,即经过卷积操作后的特征图,其中每个特征图的大小为 24 x 24 x 6。S2 层采用的是 2x2 的池化窗口,因此每个池化单元会处理一个 2x2 的区域,将其中的四个值取平均作为输出。
这样,经过 S2 层的处理,每个特征图变成了 1 x 6 x 12 x 12。这样做的好处是能够降低特征图的大小和数量,减少了参数数量,避免过拟合。同时,池化操作也能够使特征图的位置对平移更加鲁棒,从而提高模型的泛化能力。
LeNet-5里使用的是最大池化层,下文使用PyTorch模拟LeNet-5将使用平均池化层。
self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
4. 卷积层C3
它的输入是池化层S2的输出,该层的作用是提取更高级别的特征。
C3层使用 16 个 5 x 5 的卷积核对 S2 的输出进行卷积,每个卷积核对应一个特征图,因此 C3 层输出的通道数为 16。卷积操作的步长为 1,对边缘进行了补零,因此输出的特征图大小为 8 x 8。
C3层的输出经过ReLU激活函数,然后传递到下一层,即池化层S4。
C3 输出的形状是:1 x 16 x 8 x 8。
5. 池化层 S4
S4层的输入是C3层的输出,它对输入进行降采样(最大池化)。S4层使用的池化窗口大小为2×2,步长为2。
S4层会将C3层的输出沿着宽和高两个维度分别缩小一半。
在LeNet-5中,S4层与C5层之间没有使用任何归一化、正则化等操作,只是简单地使用了最大池化降采样,以保留最显著的特征。
S4输出的形状是: 1 x 16 x 4 x 4
6. 全连接层F5,F6
在 LeNet-5 中,全连接层分为 F5 和 F6 两个部分,其中 F5 包含 120 个神经元,F6 包含 84 个神经元。这两个全连接层负责将前面卷积和池化层的特征进行组合,产生分类所需的输出。
F5 层的输入为一个形状为 1x120 的向量,它与每个神经元都进行连接。每个神经元都有 120 个输入连接,其中每个连接都与 C3 层的一个 5x5 的卷积核进行卷积运算,将其结果相加后再加上一个偏置项进行激活。这样,F5 层产生的输出是一个 1x120 的向量。
F6 层与 F5 层类似,输出是1 x 84的向量。权重在训练期间通过反向传播算法进行优化,以最小化训练数据上的损失函数。
7. 输出层
输出层是一个全连接层,用于将前面的特征提取和处理结果映射到目标类别上。输出层的输出是一个大小为
输出层的激活函数使用 softmax 函数,它能够将原始的输出值转换为每个类别的概率分布。softmax 函数的公式如下:
其中 表示输入向量的第 个元素,
对于 LeNet-5 模型中的输出层,假设 分别表示该模型属于
最终的预测结果是概率最大的那个数字,即 。
三、 PyTorch的实现
import os
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 定义 LeNet-5 模型
class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()
# 定义卷积层C1,输入通道数为1,输出通道数为6,卷积核大小为5x5
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1)
# 定义池化层S2,池化核大小为2x2,步长为2
self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
# 定义卷积层C3,输入通道数为6,输出通道数为16,卷积核大小为5x5
self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1)
# 定义池化层S4,池化核大小为2x2,步长为2
self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
# 定义全连接层F5,输入节点数为16x4x4=256,输出节点数为120
self.fc1 = nn.Linear(16 * 4 * 4, 120)
# 定义全连接层F6,输入节点数为120,输出节点数为84
self.fc2 = nn.Linear(120, 84)
# 定义输出层,输入节点数为84,输出节点数为10
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
# 卷积层C1
x = self.conv1(x)
# print('卷积层C1后的形状:', x.shape)
# 池化层S2
x = self.pool1(torch.relu(x))
# print('池化层S2后的形状:', x.shape)
# 卷积层C3
x = self.conv2(x)
# print('卷积层C3后的形状:', x.shape)
# 池化层S4
x = self.pool2(torch.relu(x))
# print('池化层S4后的形状:', x.shape)
# 全连接层F5
x = x.view(-1, 16 * 4 * 4)
x = self.fc1(x)
# print('全连接层F5后的形状:', x.shape)
x = torch.relu(x)
# 全连接层F6
x = self.fc2(x)
# print('全连接层F6后的形状:', x.shape)
x = torch.relu(x)
# 输出层
x = self.fc3(x)
# print('输出层后的形状:', x.shape)
return x
# 设置超参数
batch_size = 64
learning_rate = 0.01
epochs = 10
# 准备数据
train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
# 实例化模型和优化器
model = LeNet5()
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
# 定义模型保存路径和文件名
model_path = 'model.pth'
if os.path.exists(model_path):
# 存在,直接加载模型
model.load_state_dict(torch.load(model_path))
print('Loaded model from', model_path)
else:
# 训练模型
for epoch in range(epochs):
model.train()
for images, labels in train_loader:
# 将图像展平
# images = images.view(images.size(0), -1)
images = images.view(-1, 1, 28, 28)
# 将数据放入模型
optimizer.zero_grad()
outputs = model(images)
loss = nn.functional.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
# 在测试集上测试模型
model.eval()
correct = 0
with torch.no_grad():
for images, labels in test_loader:
# 将图像展平
images = images.view(-1, 1, 28, 28)
# 将数据放入模型
outputs = model(images)
_, predicted = torch.max(outputs, 1)
correct += (predicted == labels).sum().item()
accuracy = 100 * correct / len(test_dataset)
print('Epoch [{}/{}], Loss: {:.4f}, Accuracy: {:.2f}%'.format(epoch + 1, epochs, loss.item(), accuracy))
torch.save(model.state_dict(), 'model.pth')
for i in range(10):
img, label = next(iter(test_loader))
img = img[i].unsqueeze(0)
# 使用模型进行预测
model.eval()
with torch.no_grad():
output = model(img)
# 解码预测结果
pred = output.argmax(dim=1).item()
print(f'Predicted class: {pred}, actual value: {label[i]}')
打印的各层的形状:
部分内容解释 :
图像展平
images = images.view(-1, 1, 28, 28)
输入图像 images 要进行形状的变换。
首先,使用 view 函数将 images 变换为 4 维张量,其中:
- 第 1 维的大小被设置为 -1,表示这一维度的大小应该是根据输入数据的总大小和其他维度的大小来自动计算的;
- 第 2 维是 1,表示输入图像只有一个通道;
- 第 3 维和第 4 维的大小都是 28,表示输入图像的高和宽都是 28。
损失函数
loss = nn.functional.cross_entropy(outputs, labels)
outputs 是模型的输出,labels 是对应的真实标签。