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中,卷积编码器包含两个卷积层和两个池化层:
- 第一卷积层:将输入图像通过卷积核(kernel)进行卷积操作,输出6个通道(feature maps)。激活函数使用Sigmoid函数。
- 第一池化层:对卷积后的特征进行平均池化(Average Pooling),减少特征的空间维度。
- 第二卷积层:将第一池化层的输出作为输入,再进行卷积操作,输出16个通道。
- 第二池化层:同样进行平均池化,进一步减少特征的空间维度。
2.2 全连接层密集块
卷积层提取的特征需要通过全连接层进行进一步的处理。在LeNet中,全连接层密集块包含三个全连接层:
- 第一个全连接层:将卷积层的输出展平后,输入到一个具有120个神经元的全连接层。
- 第二个全连接层:该层有84个神经元,进一步对特征进行处理。
- 输出层:输出层包含10个神经元,对应着数字分类任务的10个类别。
2.3 总结
LeNet模型的输入为28x28大小的图像(例如手写数字图像),输出为10维向量,表示该图像属于10个类别中的哪一个。
模型架构图如图所示:
3. LeNet的工作原理
在LeNet中,数据首先通过两个卷积层进行处理。在卷积操作中,每个卷积层会使用卷积核与输入图像进行卷积,产生多个特征图(feature maps)。这些特征图能够捕捉到图像中的局部信息,如边缘、纹理等。接着,池化层会对这些特征图进行降维操作,减少计算量和存储空间。
卷积和池化操作的输出会进入全连接层进行进一步的处理。全连接层将卷积块提取的特征组合起来,用于最终的分类任务。LeNet使用Sigmoid激活函数进行非线性变换,使模型能够处理复杂的模式。
3.1 数学公式
卷积操作
卷积操作的公式为:
其中,表示输入图像,表示卷积核,表示卷积后的输出。通过这个操作,卷积层将输入的图像转化为多个特征图。
池化操作
池化操作通过对邻域中的像素进行聚合来减少特征图的空间尺寸,常见的池化方式有最大池化和平均池化。LeNet中使用的是平均池化。假设池化窗口大小为2x2,则池化操作为:
全连接层
全连接层的计算可以表示为:
其中,为权重矩阵,为输入向量,为偏置项,为输出。
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
5. 小结
LeNet是卷积神经网络的经典模型,它通过卷积层、池化层和全连接层的组合,能够有效地进行图像分类任务。在本博客中,我们详细介绍了LeNet的架构和实现过程,希望能帮助你更好地理解卷积神经网络的基本原理。如果你想深入学习深度学习的其他内容,可以继续关注后续的教程和博客。