跳到主要内容

经典 CNN 模型 LeNet

LeNet-5 是什么?

LeNet-5 是由 Yann LeCun 在 1998 年提出的,是早期卷积神经网络的经典结构之一。它主要用于手写数字识别任务。LeNet-5 的结构相对简单,但它为后续的深度学习研究奠定了基础。

LeNet-5 的结构如下:

  1. 卷积层 1 (C1):使用 6 个 5×55 \times 5 的卷积核,输出 6 个特征图,每个特征图的大小为 28×2828 \times 28
  2. 汇聚层 1 (S2):使用 2×22 \times 2 的最大汇聚,输出 6 个 14×1414 \times 14 的特征图。
  3. 卷积层 2 (C3):使用 16 个 5×55 \times 5 的卷积核,输出 16 个特征图,每个特征图的大小为 10×1010 \times 10
  4. 汇聚层 2 (S4):使用 2×22 \times 2 的最大汇聚,输出 16 个 5×55 \times 5 的特征图。
  5. 全连接层 (F5):包含 120 个神经元。
  6. 全连接层 (F6):包含 84 个神经元。
  7. 输出层:包含 10 个神经元,对应于 10 个数字类别。

全连接层是怎么计算的?

全连接层(也称为稠密层或线性层)是神经网络中的一个基本组件。在全连接层中,每个输入神经元都与每个输出神经元相连接。这意味着输入数据的每个元素都会影响输出数据的每个元素。

以下是全连接层的计算过程:

  1. 权重和偏置:全连接层有两个主要的参数:权重(W)和偏置(b)。如果输入层有 mm 个神经元,输出层有 nn 个神经元,那么权重矩阵的形状为 n×mn \times m,偏置向量的形状为 n×1n \times 1

  2. 线性变换:给定一个输入向量 xx(形状为 m×1m \times 1),全连接层的输出 yy(形状为 n×1n \times 1)可以通过以下线性变换计算:

y=Wx+b y = Wx + b

其中,WW 是权重矩阵,bb 是偏置向量。

  1. 激活函数:通常,全连接层的输出会传递给一个激活函数,如 Sigmoid、ReLU、tanh 等,以增加网络的非线性。这个激活函数会逐元素地应用于向量 yy

假设我们有一个全连接层,输入层有 3 个神经元,输出层有 2 个神经元。权重和偏置可以表示为:

W=[w11w12w13w21w22w23]W = \begin{bmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ \end{bmatrix} b=[b1b2]b = \begin{bmatrix} b_1 \\ b_2 \\ \end{bmatrix}

给定一个输入向量:

x=[x1x2x3]x = \begin{bmatrix} x_1 \\ x_2 \\ x_3 \\ \end{bmatrix}

全连接层的输出为:

y=[w11x1+w12x2+w13x3+b1w21x1+w22x2+w23x3+b2]y = \begin{bmatrix} w_{11}x_1 + w_{12}x_2 + w_{13}x_3 + b_1 \\ w_{21}x_1 + w_{22}x_2 + w_{23}x_3 + b_2 \\ \end{bmatrix}

然后,这个输出 yy 可以传递给一个激活函数进行进一步的处理。

这就是全连接层的基本计算过程。在实际的深度学习框架中,如 PyTorch、TensorFlow 等,这些计算都被高度优化,并可以在大型数据集和复杂网络结构上高效地进行。

汇聚层到全连接层的转换

在 LeNet-5 中,汇聚层到全连接层的转换是通过将汇聚层的输出特征图展平(flatten)为一维向量来实现的。这个一维向量然后被送入全连接层进行处理。

让我们详细看一下这个转换过程:

  1. 汇聚层 2 (S4) 的输出:如上所述,S4 的输出是 16 个 5×55 \times 5 的特征图。

  2. 展平操作:每个 5×55 \times 5 的特征图包含 25 个值。因为我们有 16 个这样的特征图,所以总共有 16×25=40016 \times 25 = 400 个值。这些值被展平为一个长度为 400 的一维向量。

  3. 全连接层 (F5):这个长度为 400 的一维向量被送入一个包含 120 个神经元的全连接层。这意味着网络中存在 400×120=48000400 \times 120 = 48000 个权重连接这两层。

举例说明:

假设 S4 的输出特征图如下(为了简化,我们只考虑 2 个 2×22 \times 2 的特征图):

Feature Map 1:    Feature Map 2:
| 1 | 2 | | 5 | 6 |
|---|---| |---|---|
| 3 | 4 | | 7 | 8 |

展平操作后的一维向量为:

[1, 2, 3, 4, 5, 6, 7, 8]

这个一维向量然后被送入全连接层。如果全连接层有 3 个神经元,那么会有 8×3=248 \times 3 = 24 个权重连接这两层。

这就是汇聚层到全连接层的转换过程。在实际的深度学习框架中,这种展平操作通常由一个特殊的层(如 PyTorch 中的 torch.nn.Flatten)或操作来完成。

PyTorch 定义 LeNet-5 模型

import torch.nn as nn

class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()

# C1: Convolutional layer
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1, padding=2)
# S2: Max-pooling layer
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)

# C3: Convolutional layer
self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1)
# S4: Max-pooling layer
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)

# F5: Fully connected layer
self.fc1 = nn.Linear(16*5*5, 120)
# F6: Fully connected layer
self.fc2 = nn.Linear(120, 84)
# Output layer
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
x = self.pool1(nn.ReLU()(self.conv1(x)))
x = self.pool2(nn.ReLU()(self.conv2(x)))
x = x.view(-1, 16*5*5)
x = nn.ReLU()(self.fc1(x))
x = nn.ReLU()(self.fc2(x))
x = self.fc3(x)
return x

model = LeNet5()
print(model)

假设我们有一个 32×3232 \times 32 的手写数字图像作为输入:

  1. C1:图像首先通过 6 个 5×55 \times 5 的卷积核,输出 6 个 28×2828 \times 28 的特征图。
  2. S2:这 6 个特征图经过 2×22 \times 2 的最大汇聚,输出 6 个 14×1414 \times 14 的特征图。
  3. C3:这 6 个特征图通过 16 个 5×55 \times 5 的卷积核,输出 16 个 10×1010 \times 10 的特征图。
  4. S4:这 16 个特征图经过 2×22 \times 2 的最大汇聚,输出 16 个 5×55 \times 5 的特征图。
  5. F5-F6-Output:这些特征图被展平并传递给三个全连接层,最后输出 10 个数字类别的概率分布。

这个流程将图像的原始像素值转化为一个 10 维的概率分布,表示图像属于每个数字类别的概率。

训练 Fashion-MNIST 数据集

之前在学习基础线性回归时,学习了使用 Softmax 回归去训练 Fashion-MNIST 数据集。现在,我们将使用 LeNet-5 模型去训练 Fashion-MNIST 数据集,并比较两种方法的性能。

提示

Fashion-MNIST 是一个包含 60,000 个训练样本和 10,000 个测试样本的数据集,每个样本都是一个 28×2828 \times 28 的灰度图像,代表 10 种不同类别的服装。

两个模型的区别

Softmax 回归(也称为多项逻辑回归)和 LeNet-5 是两种不同的机器学习模型,它们在结构和应用上有明显的区别。以下是它们之间的主要区别:

  1. 模型结构

    • Softmax 回归:是一个简单的线性模型。它首先将输入特征与权重矩阵相乘,然后加上偏置,最后通过 Softmax 函数得到每个类别的概率分布。
    • LeNet-5:是一个卷积神经网络模型,包含多个卷积层、汇聚层和全连接层。它可以捕获输入数据中的局部模式和层次结构。
  2. 参数数量

    • Softmax 回归:参数数量与输入特征的数量和类别的数量成线性关系。
    • LeNet-5:由于其多层结构,参数数量通常远大于 Softmax 回归。
  3. 应用领域

    • Softmax 回归:通常用于简单的分类任务,特别是当输入数据是低维的或已经经过特征提取的情况。
    • LeNet-5:主要用于图像分类任务,尤其是当输入数据是原始像素值时。
  4. 性能

    • Softmax 回归:对于复杂的图像分类任务,性能可能不佳,因为它不能捕获图像中的局部和层次结构。
    • LeNet-5:由于其深度结构,通常在图像分类任务上表现得更好。
  5. 计算复杂性

    • Softmax 回归:由于其简单的线性结构,计算复杂性相对较低。
    • LeNet-5:由于其多层结构,计算复杂性较高。

总的来说,Softmax 回归是一个简单的线性分类器,而 LeNet-5 是一个深度卷积神经网络。尽管 Softmax 回归在某些任务上可能足够有效,但 LeNet-5 由于其能够捕获复杂的图像模式,通常在图像分类任务上表现得更好。

  1. 数据加载和预处理:首先,我们需要加载 Fashion-MNIST 数据集,并对其进行适当的预处理。这通常包括将图像数据标准化到 [0,1] 范围、分割数据集为训练集和测试集等。

  2. 定义模型:我们可以使用上面定义的 LeNet-5 结构。由于 Fashion-MNIST 的图像大小为 28×2828 \times 28,与原始 LeNet-5 的输入 32×3232 \times 32 稍有不同,但我们可以通过适当的填充来适应这种差异。

  3. 训练模型:使用训练数据集训练模型,并使用测试数据集验证模型的性能。

  4. 评估模型:在测试数据集上评估模型的性能,查看分类准确率等指标。

PyTorch 代码示例:

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import time # 导入 time 模块

# 数据加载和预处理
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
trainset = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
testset = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)

# 使用上面定义的 LeNet-5 结构
model = LeNet5()

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 开始训练计时
start_time = time.time()

# 训练模型
for epoch in range(10):
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {running_loss/len(trainloader)}")

end_time = time.time()
print(f"Training time: {end_time - start_time:.2f} seconds") # 打印训练所用时间

print("Finished Training")

# 开始评估计时
start_eval_time = time.time()

# 评估模型
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

end_eval_time = time.time()
print(f"Evaluation time: {end_eval_time - start_eval_time:.2f} seconds") # 打印评估所用时间

print(f"Accuracy on test set: {100 * correct / total}%")

这个代码示例首先加载和预处理 Fashion-MNIST 数据集,然后定义 LeNet-5 模型、损失函数和优化器。接着,它训练模型 10 个周期,并在测试数据集上评估模型的性能。

上面的输出是:

Epoch 1, Loss: 0.603213270065754
Epoch 2, Loss: 0.3682879089340091
Epoch 3, Loss: 0.31751148509922056
Epoch 4, Loss: 0.28929864099698027
Epoch 5, Loss: 0.26670469539061287
Epoch 6, Loss: 0.249649279939531
Epoch 7, Loss: 0.23472552415309175
Epoch 8, Loss: 0.22121301105519983
Epoch 9, Loss: 0.21048789316895547
Epoch 10, Loss: 0.19852293324845433
Training time: 129.36 seconds
Finished Training
Evaluation time: 1.00 seconds
Accuracy on test set: 90.3%

可以发现 60,000 个训练样本,训练十轮就花费了 129.36 秒,下面来看看使用了 GPU 后的训练速度

安装 CUDA

先检查当前环境上是否安装了 CUDA

python -c "import torch; print(torch.cuda.is_available())"

如果没有安装,则到官网找个支持 CUDA 的版本进行安装

https://pytorch.org/get-started/locally/

检查本地的 CUDA 版本,在cmd窗口的命令行执行 nvcc -Vnvcc --version 即可查看CUDA.

nvcc --version

可以看到使用的是 12.1 版本的 CUDA,所以我们需要安装 cu121 版本的 PyTorch,这个版本的 PyTorch 适用于 CUDA 11.1、11.3、11.4、12.0 和 12.1。

最终给出的安装地址

pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

如果报错

ERROR: Could not install packages due to an OSError: [WinError 5] 拒绝访问。

添加 --user 选项赋予权限就可以搞定。

pip3 install torch torchvision torchaudio --user --index-url https://download.pytorch.org/whl/cu121

注意:如果是在虚拟目录下则需要修改

到虚拟目录的 pyvenv.cfg 文件夹下,设置 include-system-site-packages 为 true

优化:使用 GPU 训练

要在 PyTorch 中使用 GPU 训练模型,需要执行以下步骤:

  1. 检查 CUDA 是否可用: 使用 torch.cuda.is_available() 检查是否有可用的 GPU。

  2. 定义设备: 根据 CUDA 的可用性定义设备为 GPU 或 CPU。

  3. 将模型移动到设备: 使用 model.to(device) 将模型移动到所选设备。

  4. 将数据移动到设备: 在训练循环中,确保每批数据也被移动到相同的设备。

以下是如何修改之前的代码以使用 GPU(如果可用)进行训练:

# ... [其他代码,如数据加载、模型定义等]

if torch.cuda.is_available():
print("CUDA (GPU support) is available and PyTorch can use GPUs!")
else:
print("CUDA is not available. PyTorch will use CPU.")

# 检查 CUDA 是否可用并定义设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 将模型移动到设备
model = LeNet5().to(device)

# ... [定义损失函数和优化器]

# 训练模型
for epoch in range(10):
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# 将数据移动到设备
inputs, labels = data[0].to(device), data[1].to(device)

optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {running_loss/len(trainloader)}")

print("Finished Training")

# 评估模型
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
# 将数据移动到设备
images, labels = data[0].to(device), data[1].to(device)

outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

print(f"Accuracy on test set: {100 * correct / total}%")

这样,如果您的机器上有可用的 GPU,并且已经安装了适当的 CUDA 版本,上述代码将自动使用 GPU 进行训练。如果没有 GPU,它将默认使用 CPU。

提示

注意,如果是 ipynb 文件,记得清空缓存,不然会默认走 CPU 的

再次检查输出,可以发现快了不少

CUDA (GPU support) is available and PyTorch can use GPUs!
Epoch 1, Loss: 0.6168776863832464
Epoch 2, Loss: 0.38093749684756245
Epoch 3, Loss: 0.32623425711081355
Epoch 4, Loss: 0.2969710909958079
Epoch 5, Loss: 0.27477049792229113
Epoch 6, Loss: 0.25681439583013055
Epoch 7, Loss: 0.2399967324965671
Epoch 8, Loss: 0.22924256587087283
Epoch 9, Loss: 0.21592489179215832
Epoch 10, Loss: 0.20399675992474373
Training time: 80.01 seconds
Finished Training
Evaluation time: 1.00 seconds
Accuracy on test set: 88.23%