卷积神经网络6-卷积神经网络(LeNet)

390 阅读7分钟

1. 引言

卷积神经网络(Convolutional Neural Networks, CNN)是目前深度学习领域中应用最广泛的模型之一,尤其在计算机视觉任务中,发挥着至关重要的作用。从图像分类到物体检测,CNN都展示了强大的能力。在本篇博客中,我们将通过 LeNet(LeNet-5) 模型的介绍,帮助大家理解卷积神经网络的基本原理与实现。

LeNet是最早的卷积神经网络之一,由Yann LeCun等人在1989年提出。它最初的目标是进行手写数字的识别,在当时的自动取款机(ATM)和支票处理系统中得到了广泛应用。通过介绍LeNet,我们将了解卷积层激活函数池化层等深度学习中的核心组件,并探索如何利用这些组件搭建一个高效的卷积神经网络。

2. LeNet架构概览

LeNet-5模型由两部分组成:

  • 卷积编码器(Convolutional Encoder):由两个卷积层和池化层组成,用于提取图像中的特征。
  • 全连接层密集块(Fully Connected Dense Block):由三个全连接层组成,用于对卷积提取的特征进行分类处理。

2.1 卷积编码器

卷积编码器负责从原始图像中提取特征。在LeNet中,卷积编码器包含两个卷积层和两个池化层:

  1. 第一卷积层:将输入图像通过卷积核(kernel)进行卷积操作,输出6个通道(feature maps)。激活函数使用Sigmoid函数。
  2. 第一池化层:对卷积后的特征进行平均池化(Average Pooling),减少特征的空间维度。
  3. 第二卷积层:将第一池化层的输出作为输入,再进行卷积操作,输出16个通道。
  4. 第二池化层:同样进行平均池化,进一步减少特征的空间维度。

2.2 全连接层密集块

卷积层提取的特征需要通过全连接层进行进一步的处理。在LeNet中,全连接层密集块包含三个全连接层:

  1. 第一个全连接层:将卷积层的输出展平后,输入到一个具有120个神经元的全连接层。
  2. 第二个全连接层:该层有84个神经元,进一步对特征进行处理。
  3. 输出层:输出层包含10个神经元,对应着数字分类任务的10个类别。

2.3 总结

LeNet模型的输入为28x28大小的图像(例如手写数字图像),输出为10维向量,表示该图像属于10个类别中的哪一个。

模型架构图如图所示:

lenet.svg

3. LeNet的工作原理

在LeNet中,数据首先通过两个卷积层进行处理。在卷积操作中,每个卷积层会使用卷积核与输入图像进行卷积,产生多个特征图(feature maps)。这些特征图能够捕捉到图像中的局部信息,如边缘、纹理等。接着,池化层会对这些特征图进行降维操作,减少计算量和存储空间。

卷积和池化操作的输出会进入全连接层进行进一步的处理。全连接层将卷积块提取的特征组合起来,用于最终的分类任务。LeNet使用Sigmoid激活函数进行非线性变换,使模型能够处理复杂的模式。

3.1 数学公式

卷积操作

卷积操作的公式为:

yi,j=mnxi+m,j+nwm,ny_{i,j} = \sum_m \sum_n x_{i+m, j+n} \cdot w_{m,n}

其中,xx表示输入图像,ww表示卷积核,yy表示卷积后的输出。通过这个操作,卷积层将输入的图像转化为多个特征图。

池化操作

池化操作通过对邻域中的像素进行聚合来减少特征图的空间尺寸,常见的池化方式有最大池化和平均池化。LeNet中使用的是平均池化。假设池化窗口大小为2x2,则池化操作为:

yi,j=14m=01n=01xi+m,j+ny_{i,j} = \frac{1}{4} \sum_{m=0}^{1} \sum_{n=0}^{1} x_{i+m, j+n}
全连接层

全连接层的计算可以表示为:

y=Wx+by = W x + b

其中,WW为权重矩阵,xx为输入向量,bb为偏置项,yy为输出。

3.2 LeNet在PyTorch中的实现

我们可以利用深度学习框架(如PyTorch)轻松实现LeNet。下面是PyTorch实现的代码:

from torch import nn

# 定义LeNet模型
net = nn.Sequential(
    nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(in_channels=6, out_channels=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)
)

print(net)

该代码定义了LeNet模型的结构,首先包括两个卷积层和池化层,然后通过展平操作(Flatten)将卷积块的输出送入全连接层。最后的输出层有10个神经元,用于进行分类。

Sequential(
  (0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
  (1): Sigmoid()
  (2): AvgPool2d(kernel_size=2, stride=2, padding=0)
  (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (4): Sigmoid()
  (5): AvgPool2d(kernel_size=2, stride=2, padding=0)
  (6): Flatten(start_dim=1, end_dim=-1)
  (7): Linear(in_features=400, out_features=120, bias=True)
  (8): Sigmoid()
  (9): Linear(in_features=120, out_features=84, bias=True)
  (10): Sigmoid()
  (11): Linear(in_features=84, out_features=10, bias=True)
)

4. LeNet训练与评估

在使用LeNet进行训练之前,我们需要加载并预处理数据集。在本例中,我们使用的是 Fashion-MNIST 数据集,这是一个包含手写数字和服装图像的数据集。

4.1 数据预处理和加载

# d2l.py

def load_data_fashion_mnist(batch_size, resize=None):
    """下载Fashion-MNIST数据集,然后将其加载到内存中"""
    trans = [transforms.ToTensor()]
    if resize:
        # 将输入图像的高度和宽度调整为指定的尺寸 resize,从而确保图像的大小一致
        trans.insert(0, transforms.Resize(resize))
    trans = transforms.Compose(trans)  # 将多个图像转换操作组合在一起,以形成一个“转换流水线”
    mnist_train = torchvision.datasets.FashionMNIST(root="../data",
                                                    train=True, transform=trans, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(root="../data",
                                                   train=False, transform=trans, download=True)
    return (data.DataLoader(mnist_train, batch_size, shuffle=True),
            data.DataLoader(mnist_test, batch_size, shuffle=True))

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(256)

# 检查数据形状
for images, labels in train_iter:
    print(f"Batch of images shape: {images.shape}")
    print(f"Batch of labels shape: {labels.shape}")
    break
Batch of images shape: torch.Size([256, 1, 28, 28])
Batch of labels shape: torch.Size([256])

4.2 训练函数

我们使用小批量随机梯度下降(SGD)优化算法,并使用交叉熵损失函数进行训练。以下是训练函数的部分代码:

def evaluate_accuracy_gpu(net, data_iter, device=None):
    """
    使用GPU计算模型在数据集上的精度
    """
    if isinstance(net, nn.Module):
        net.eval()  # 设置为评估模式
        if not device:
            """
            这行代码的目的是确定模型的第一个参数(通常是权重)被存储在哪个设备上。
            通常这个信息是用于确定当前模型运行在 CPU 还是 GPU 上,
            在使用 GPU 进行训练时非常有用,特别是当模型和数据需要显式迁移到同一个设备上时。
            """
            device = next(iter(net.parameters())).device
    # 正确预测的数量,总预测的数量
    metric = d2l.Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            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训练模型"""

    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'],
                            figsize=(9, 5), title="卷积神经网络(LeNet)")
    timer, num_batches = d2l.Timer(), len(train_iter)
    metric = d2l.Accumulator(3)
    train_l, train_acc, test_acc = 0, 0, 0
    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()
            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)}')
    animator.show()


lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
  • d2l 模块中的函数参见前面的文章,或者参看链接 d2l
training on cpu
loss 0.461, train acc 0.828, test acc 0.815
4502.0 examples/sec on cpu

myplot.png

5. 小结

LeNet是卷积神经网络的经典模型,它通过卷积层、池化层和全连接层的组合,能够有效地进行图像分类任务。在本博客中,我们详细介绍了LeNet的架构和实现过程,希望能帮助你更好地理解卷积神经网络的基本原理。如果你想深入学习深度学习的其他内容,可以继续关注后续的教程和博客。