本文作为对该课程第23课“经典卷积神经网络 LeNet”的学习笔记,介绍LeNet网络并对其代码进行较为详细的注释与解读。

一. 什么是LeNet?

LeNet 是一个经典的卷积神经网络(CNN)架构,由 Yann LeCun 等人在 1998 年提出,最早应用于手写体字符识别,特别是在邮政编码识别中表现非常出色。它是现代卷积神经网络的起源之一,奠定了CNN在计算机视觉领域的基础。其原理如下图所示:

LeNet的网络结构主要由卷积层、池化层和全连接层组成,这里将介绍经典的 LeNet-5 架构。

LeNet网络的输入层通常为通道数为1的灰度图像,其大小为32×32,输出层是一个由10个神经元组成的softmax高斯连接层,可以用来做分类任务,例如手写数字0-9的分辨。

除了输入输出层外共有6个核心层组成,分别是

卷积层C1:使用6个大小为5×5的不同类型的卷积核,步幅为1,无填充,卷积后输出特征图大小为 28×28×6;池化层S2:使用最大池化层,池化窗口大小为2×2,步幅为2,经池化后得到输出特征图大小为14×14×6;卷积层C3:使用16个大小为5×5的不同卷积核,步幅为1,无填充,卷积后得到输出特征图大小为10×10×16;池化层 S4:使用最大池化层,池化窗口大小为2×2,步幅为 2,输出特征图大小为5×5×16;全连接层C5:将5×5×16=400的特征图展平为一维向量,输入到120个神经元的全连接层,输出120个1×1大小的特征图;全连接层F6:120个神经元作为输入,84个神经元输出,使其成为一个由84个神经元组成的全连接隐藏层,激活函数使用sigmoid 函数。

LeNet 是卷积神经网络的先驱,证明了卷积层在图像识别任务中的有效性,并为后来的深度学习模型(如 AlexNet、VGG、ResNet 等)奠定了基础,尽管结构相对简单,但它仍然是理解卷积神经网络原理的经典范例。

二、代码实现

我们要利用LeNet网络做mnist手写数字识别,而mnist数据集中的图像大小为28×28,也就是说,要对mnist的数据进行两圈值为零的像素填充,除此之外的其他部分与LeNET-5一致。

首先,我们导入需要用到的函数库并将LeNet中所有的层进行定义。

import torch
from torch import nn
from d2l import torch as d2l

class Reshape(torch.nn.Module): # 自定义类,用于调整输入张量形状
  def forward(self,x):
    return x.view(-1,1,28,28) # 批量数不变、通道数1(单通道灰度图像)、28×28的输入

# 定义神经网络net、torch.nn.Sequential用于按顺序堆叠神经网络层
net = torch.nn.Sequential( 
  Reshape(), # 将输入数据调整为刚才设定的形状
  # 第一个卷积层,输入通道数为1,输出通道数为6,卷积核大小为5x5,填充为2
  # Sigmoid激活函数,用于引入非线性
  nn.Conv2d(1, 6, kernel_size=5,padding=2), nn.Sigmoid(),
  # 最大池化层,池化窗口大小为2x2,步幅为2
  nn.MaxPool2d(kernel_size=2, stride=2),
    
  # 第二个卷积层,输入通道数为6,输出通道数为16,卷积核大小为5x5
  nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
  nn.MaxPool2d(kernel_size=2, stride=2), # 第二个最大池化层
  nn.Flatten(), # 将多维张量展平为一维,以便输入全连接层。

  # 第一个全连接层,输入大小为16*5*5(经过卷积和池化后的特征图大小)输出大小为120
  nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(), 
  nn.Linear(120, 84), nn.Sigmoid(), # 第二个全连接层,输入大小为120,输出大小为84
  nn.Linear(84, 10)) # 最后一个全连接层,输入大小为84,输出大小为10

通过对每一层的打印,检查一下模型是否按照它的原理进行不断的输入输出。

X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32) # 生成一个随机张量
for layer in net: # 遍历net的每一层
    X = layer(X) # 将输入X通过当前层layer进行前向传播,得到新的X
    print(layer.__class__.__name__, 'output shape: \t', X.shape) 
    # 打印当前层的类名和输出张量的形状

输出结果:

其中,比较重要的层分别是:Conv2d是卷积层,输入到输出的过程中使用了适当的填充(padding),使得输出的高度和宽度与输入相同,且通道数增加6;Sigmoid是对卷积层的输出应用的激活函数,其输出形状与输入形状相同;MaxPool2d是最大池化层,使用 2x2 的池化窗口,步幅为 2,因此高度和宽度减半;第二个Conv2d未使用填充(padding),因此输出的高度和宽度减小;Flatten将多维张量展平为一维向量,输入形状为 (1,16,5,5),展平后为 (1,16*5*5) = (1,400);Linear是全连接层,对展平后的数据进行线性变换,输出为特征数量。

通过这种方式,可以逐层检查网络的输出形状,确保每一层的设计符合预期。

接下来,导入数据集,这里使用Fashion-MNIST数据集(因为mnist太常见了)

batch_size = 256 # 设置批量大小,表示每次从数据集中加载的样本数量
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
# 调用d2l库中的load_data_fashion_mnist函数,加载Fashion-MNIST数据集

运行后会自动下载Fashion-MNIST数据集。

这里定义了一个函数evaluate_accuracy_gpu,用于计算模型在指定数据集上的精度,并且在GPU上运行(CPU也可以跑,但是GPU更快),因此我们要将内存上的数据集复制到显存中。以下是对代码的注释:

def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
    """使用GPU计算模型在数据集上的精度"""
    if isinstance(net, nn.Module):
        net.eval() # 将模型设置为评估模式
        if not device: # 如果未指定device,则从模型的参数中推断设备
            device = next(iter(net.parameters())).device 
            # net.parameters()返回模型的所有参数 next()获取第一个参数
            # .device获取该参数所在的设备
    metric = d2l.Accumulator(2) 
    # 创建一个长度为2的累加器,用于存储两个值:metric[0]正确预测的数量和metric[1]总样本数
    with torch.no_grad(): # 在评估过程中禁用梯度计算,以减少内存消耗并加速计算
        for X, y in data_iter: # 遍历数据迭代器,每次获取一个批量的数据
            if isinstance(X, list): # 将输入数据X和标签y移动到指定的设备
                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()) 
            # 计算当前批量的精度,并更新累加器,算一下y的元素个数
    return metric[0] / metric[1] # 计算并返回模型在整个数据集上的精度
    # 精度 = 正确预测的数量/总样本数

接下来,我们定义训练函数:

这其中有个Xavier函数,它初始化的核心思想是根据输入和输出的维度,合理地初始化权重,使得每一层的输出值的方差保持一致,防止模型梯度消失或梯度爆炸。

#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    # train_iter训练数据迭代器, test_iter测试数据迭代器
    """用GPU训练模型"""
    # 初始化模型权重
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight) # Xavier初始化方法,适用于激活函数为线性的情况
    net.apply(init_weights) # 对模型的所有子模块递归调用init_weights函数
    print('training on', device) # 打印当前训练设备
    net.to(device) # 将模型移动到指定设备
    optimizer = torch.optim.SGD(net.parameters(), lr=lr) # 随机梯度下降 SGD作为优化器
    loss = nn.CrossEntropyLoss() # loss用于计算预测值与真实标签之间的差异
    # 初始化一个动画绘制器,用于可视化训练过程中的损失和精度
    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):  # 开始逐轮 epoch训练模型
        metric = d2l.Accumulator(3) # 初始化累加器 metric,用于统计损失、正确预测数和样本数
        net.train()
        for i, (X, y) in enumerate(train_iter): # 遍历训练数据迭代器,每次获取一个批量的数据
            timer.start() # 启动计时器
            optimizer.zero_grad() # 清空优化器的梯度
            X, y = X.to(device), y.to(device) # 将输入数据 X和标签 y移动到指定设备
            y_hat = net(X) # 模型通过前向传播得到预测值 y_hat
            l = loss(y_hat, y) # 计算损失l
            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)}')

最后,指定学习率和epoch,调用训练函数开始训练。 

lr, num_epochs = 0.9, 10 # 指定学习率和训练轮数
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) # 调用训练函数

训练结果如下: 测试精度为0.833,这里显示用cpu跑的,但也不影响,装gpu版本的pytorch会显示gpu,也有显示conda的情况。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐