找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1186

积分

0

好友

210

主题
发表于 3 天前 | 查看: 5| 回复: 0

LeNet5_LeNet5

LeNet5是Yann LeCun于1998年提出的首个成功应用于手写数字识别的卷积神经网络(CNN),在OCR和图像识别领域具有里程碑意义。该网络通过卷积层、池化层、全连接层等结构,实现了对MNIST数据集中手写数字的高效分类。本项目包含完整的LeNet5模型搭建与训练流程,使用Sigmoid激活函数和交叉熵损失函数,结合梯度下降优化算法,在真实数据集上完成端到端的学习任务。通过本实践,学习者可深入掌握CNN基础组件的工作机制及其在图像识别中的应用。

LeNet5:从零理解卷积神经网络的奠基之作

在深度学习的浩瀚星河中,如果要选出一颗最具开创性的启明星,那一定是LeNet5。1998年,Yann LeCun和他的团队提出了这个看似简单却影响深远的模型。它首次将卷积神经网络(CNN)成功应用于手写数字识别任务,并在MNIST数据集上大放异彩。

在那个计算资源匮乏、概念尚未普及的时代,这个只有七层的小网络奠定了现代计算机视觉的基石结构:局部感受野、权值共享与空间下采样。今天,我们将像拆解精密机械一样,逐层剖析LeNet5,理解像素如何一步步转变为最终的识别结果。

卷积与池化:CNN的核心引擎是如何工作的?

为什么一张图片经过几层操作后,机器就能认出上面是“7”而不是“1”?这背后的关键在于两个核心组件:卷积层池化层。它们分别负责提取特征和提炼重点。

局部感受野 vs 全连接:为何CNN更高效?

我们先来对比两种不同的连接方式: 特性 全连接层 卷积层
连接方式 全局连接 局部连接 ✅
参数量 高($N \times M$ 低($K^2 \times C_{\text{out}} + C_{\text{out}}$)✅
平移敏感性 强 ❌ 可控(配合池化增强不变性)✅
生物合理性 低 ❌ 高 ✅

全连接层让每个神经元记忆整张图的所有细节,导致参数爆炸且对位置变化敏感。而卷积层则聪明得多,它只关注局部区域(如某个角落是否有边缘或角点),模拟了生物视觉皮层中简单细胞的响应模式。

例如,检测垂直边缘可以使用以下核子:

import numpy as np
vertical_edge_kernel = np.array([
    [-1, 0, 1],
    [-1, 0, 1],
    [-1, 0, 1]
], dtype=np.float32)

当这个核在图像上滑动时,遇到亮度突变的竖边输出值就会增高,这就是一个简单的“特征探测器”。网络可以同时使用多个不同功能的核,形成一组多角度的“侦察兵”。

权值共享:不只是省参数,更是赋予平移等变性

如果说局部连接是第一步优化,那么权值共享就是革命性的飞跃。同一个滤波器在整个图像上重复使用,无论在哪个位置,只要是“竖线”,就用同一套权重去检测。

数学表达式为:$F_k = X * W_k + b_k$,这里的 $*$ 表示互相关操作。这样做的好处显而易见:

  • 参数量暴减:从数万个参数减少到几十个。
  • 平移等变性:物体移动时,特征图也会随之移动,但形状保持一致。
  • 鲁棒性强:即使字形发生粗细、倾斜等变化,也能保持稳定响应。

多通道卷积:RGB图像怎么处理?

现实中的图像是彩色的,例如一张RGB图像拥有三个通道。处理多通道输入时,每个卷积核也必须是三维的。如果输入是 $28\times28\times3$,要生成6个特征图,就需要设计6个大小为 $5\times5\times3$ 的核。计算时,三个通道的信息被加权融合,最终生成一个新的二维特征图。

公式如下:$F_{i,j,c} = \sum_{m=0}^{K-1}\sum_{n=0}^{K-1}\sum_{d=0}^{C_{in}-1} X_{i+m,j+n,d} \cdot K_{m,n,d,c} + b_c$

使用现代深度学习框架可以轻松实现,例如在PyTorch中:

import torch
import torch.nn as nn

conv_layer = nn.Conv2d(in_channels=3, out_channels=8, kernel_size=5)
input_tensor = torch.randn(2, 3, 28, 28)  # 批次×通道×高×宽
output_tensor = conv_layer(input_tensor)
print(f“输入形状: {input_tensor.shape}”)   # [2, 3, 28, 28]
print(f“输出形状: {output_tensor.shape}”) # [2, 8, 24, 24]

框架已经帮你处理好了所有维度转换和并行计算。

LeNet5实战:动手搭建第一个卷积网络

理论铺垫完成后,让我们动手复现经典的LeNet5网络。

第一卷积层 C1:初识特征提取

LeNet5的输入是 $32\times32$ 的灰度图(MNIST原图28×28通过补零扩展)。第一层C1使用了6个 $5\times5$ 的卷积核,步长为1,无填充。
根据公式 $H_{out} = \lfloor \frac{H_{in} - K + 2P}{S} \rfloor + 1 = \frac{32-5}{1} + 1 = 28$,输出尺寸为 $28\times28\times6$,该层仅包含156个参数(含偏置)。

class LeNet5_C1(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, 5)  # in:1, out:6, kernel:5x5
        self.tanh = nn.Tanh()

    def forward(self, x):
        return self.tanh(self.conv1(x))

# 测试
model = LeNet5_C1()
x = torch.randn(1, 1, 32, 32)
print(model(x).shape)  # torch.Size([1, 6, 28, 28])

图:像素化的T型图案
这一层提取的特征图通常对应简单的几何结构,如直线段、拐角等,类似于绘画的基本笔画。

第二卷积层 C3:稀疏连接的智慧设计

第二卷积层C3接收来自S2池化后的 $14\times14\times6$ 输入,使用16个 $5\times5$ 核进行卷积。
这里有一个关键细节:根据LeCun的原始设计,C3层采用的是部分连接策略,而非全连接。

输出通道 输入通道组合(来自S2)
0 0,1,2
1 1,2,3
12 0,2,4
13 1,3,5

这种“选择性融合”鼓励网络学习跨通道的特征组合,堪称后来Inception模块的雏形。虽然现代框架难以直接支持任意稀疏连接,但可以通过分组卷积或掩码近似实现。

以下是使用PyTorch实现的LeNet5卷积部分:

class LeNet5_ConvPart(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 6, 5), nn.Tanh(), nn.AvgPool2d(2),
            nn.Conv2d(6, 16, 5), nn.Tanh(), nn.AvgPool2d(2)
        )

    def forward(self, x):
        return self.features(x)

net = LeNet5_ConvPart()
out = net(torch.randn(1, 1, 32, 32))
print(out.shape)  # [1, 16, 5, 5]

图:像素化的T型图案

展平与全连接:从特征到决策的最后一跃

卷积和池化层完成了“看”的任务,接下来需要通过全连接层进行“思考”。但首先,需要将三维张量转换为一维向量。

展平层:无声的桥梁

展平层负责将高维特征图“压平”为一维向量,不进行任何计算,仅改变数据形状。

feature_map = torch.randn(1, 16, 5, 5)  # 来自S4的输出
flattened = feature_map.view(1, -1)     # 自动计算总长度
print(flattened.shape)  # [1, 400]

# 或使用 nn.Flatten()
flatten_layer = nn.Flatten(start_dim=1)
output = flatten_layer(feature_map)

注意:如果之前使用了 permutetranspose 操作,记得先调用 .contiguous() 再使用 view(),否则可能报错。

展平层虽然简单,却是连接卷积模块与分类决策模块的关键节点。一旦展平,空间结构信息即被抹除,这也是后来全局平均池化(GAP)兴起的原因之一。

全连接层:全局判断的中枢

LeNet5使用了两个全连接层(F5和F6),分别有120和84个神经元,最后映射到10类输出(对应数字0-9)。

class LeNet5Classifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.classifier = nn.Sequential(
            nn.Linear(400, 120), nn.Tanh(),
            nn.Linear(120, 84), nn.Tanh(),
            nn.Linear(84, 10)
        )

    def forward(self, x):
        return self.classifier(x)

图:像素化的T型图案

参数统计:

  • F5层:$400 \times 120 + 120 = 48,120$
  • F6层:$120 \times 84 + 84 = 10,164$
  • 输出层:$84 \times 10 + 10 = 850$
    总计约 59,134 个参数,占据了整个模型参数的绝大部分。

全连接层的优势在于其“全局视野”,可以综合整张图像的全局信息进行判断。例如识别数字“8”,需要确认上下两个闭环是否存在,这种跨区域的关系建模依赖于全连接层。但这也带来了过拟合的风险,在实践中可以加入Dropout进行正则化:

self.dropout = nn.Dropout(0.5)
def forward(self, x):
    x = self.classifier[:2](x)
    x = self.dropout(x)
    x = self.classifier[2:](x)
    return x

激活函数、损失函数与优化器:训练的灵魂三要素

网络结构搭建完毕,还需要决定它如何学习。这就引出了三大核心组件:激活函数、损失函数和优化算法

Sigmoid的辉煌与落幕

LeNet5最初使用的是Sigmoid激活函数:$\sigma(z) = \frac{1}{1+e^{-z}}$
它的优点在于输出范围在(0,1),便于进行概率解释,且函数平滑可导。但其缺点更为致命:

  • 梯度饱和:在输入值极大或极小时,导数接近0,导致深层网络训练时出现梯度消失问题。
  • 非零中心:输出恒为正,可能导致权重更新出现“Z”字型抖动,降低收敛效率。

因此,Sigmoid后来被Tanh、ReLU等激活函数全面取代。我们可以通过简单的代码观察其梯度问题:

import matplotlib.pyplot as plt
import numpy as np

def sigmoid(x): return 1 / (1 + np.exp(-x))
def sigmoid_grad(x): return sigmoid(x) * (1 - sigmoid(x))

x = np.linspace(-8, 8, 200)
plt.plot(x, sigmoid(x), label=‘Sigmoid’)
plt.plot(x, sigmoid_grad(x), ‘--’, label=“Gradient”)
plt.legend(); plt.grid(True, alpha=0.3)
plt.title(“Sigmoid and Its Derivative”); plt.show()

交叉熵损失:分类任务的最佳拍档

对于多分类问题,交叉熵损失是最常用且理论依据充分的损失函数。对于单个样本,其形式为:$L = -\log(\hat{y}_c)$,其中 $\hat{y}_c$ 是模型对正确类别的预测概率。预测错误越严重,惩罚越大。
在PyTorch中,可以便捷地使用内置函数,该函数已包含数值稳定的Softmax计算:

loss = F.cross_entropy(logits, labels)  # logits是未归一化的网络输出

动量SGD:给梯度加上“惯性”

LeNet5时代使用的是带动量的随机梯度下降(SGD)优化器。

optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

动量的作用类似于物理中的惯性,使梯度更新方向不易被个别批次中的噪声所干扰,能帮助优化更快地穿越平坦区域,加速收敛。

提示:现代训练中,Adam优化器往往收敛更快,但SGD配合适当的学习率调度器,在最终性能上可能更优,需根据任务选择。

训练全流程:从数据到评估的闭环系统

数据预处理:不容忽视的第一步

良好的数据预处理能显著提升模型性能和训练稳定性。

from torchvision import transforms

transform = transforms.Compose([
    transforms.ToTensor(),                   # 将PIL图像或numpy数组转为Tensor,并归一化到[0.0, 1.0]
    transforms.Normalize((0.1307,), (0.3081,))  # 使用MNIST数据集的均值和标准差进行标准化
])

归一化的主要目的:

  1. 防止输入数据尺度差异过大导致激活函数过早饱和。
  2. 加速优化器的收敛过程。
  3. 在一定程度上提升模型的泛化能力。

批量加载与训练循环

使用DataLoader进行批量数据加载,并编写标准的训练循环。

from torch.utils.data import DataLoader

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)

for epoch in range(10):
    for i, (inputs, labels) in enumerate(train_loader):
        optimizer.zero_grad()         # 清除上一轮梯度
        outputs = model(inputs)       # 前向传播
        loss = criterion(outputs, labels) # 计算损失
        loss.backward()               # 反向传播,计算梯度
        optimizer.step()              # 更新参数

        if i % 100 == 0:
            print(f“Epoch {epoch}, Step {i}, Loss: {loss.item():.4f}”)

图:像素化的T型图案
关键点

  • 每次迭代前必须调用 zero_grad(),防止梯度累积。
  • 使用 loss.item() 提取损失标量值,避免构建计算图导致内存占用。
  • 若有GPU,可使用 inputs = inputs.to(device) 将数据转移至GPU加速计算。

模型保存与断点续训

在长时间训练或工业部署中,保存和加载模型至关重要。

# 保存检查点
torch.save({
    ‘model_state_dict’: model.state_dict(),
    ‘optimizer_state_dict’: optimizer.state_dict(),
    ‘epoch’: epoch,
    ‘loss’: loss
}, ‘checkpoint.pth’)

# 从检查点恢复训练
checkpoint = torch.load(‘checkpoint.pth’)
model.load_state_dict(checkpoint[‘model_state_dict’])
optimizer.load_state_dict(checkpoint[‘optimizer_state_dict’])
start_epoch = checkpoint[‘epoch’]

图:像素化的T型图案

模型评估:不只是准确率那么简单

混淆矩阵:揭示模型的错误模式

准确率是一个宏观指标,而混淆矩阵能清晰展示模型在哪些类别上容易混淆。

from sklearn.metrics import confusion_matrix
import seaborn as sns

cm = confusion_matrix(true_labels, pred_labels)
sns.heatmap(cm, annot=True, fmt=“d”, cmap=“Blues”)
plt.title(“Confusion Matrix”); plt.show()

例如,你可能会发现模型有时将“4”误判为“9”,或将“7”误判为“1”,这些都是手写数字识别中常见的混淆情况,为进一步优化提供了方向。

特征可视化:洞察网络“所见”

理解中间层特征图有助于直观感受网络的学习过程。

def visualize_feature_maps(model, img, layer_name=“conv1”):
    # 假设有一个函数能获取指定层的输出
    feat_extractor = get_sub_model(model, layer_name)
    feat_map = feat_extractor(img.unsqueeze(0)).squeeze(0)

    fig, axes = plt.subplots(4, 4, figsize=(8,8))
    for i in range(min(16, feat_map.size(0))): # 最多显示16个特征图
        axes[i//4][i%4].imshow(feat_map[i].detach(), cmap=“gray”)
        axes[i//4][i%4].axis(“off”)
    plt.suptitle(f“Feature Maps from {layer_name.upper()}”)
    plt.show()

visualize_feature_maps(model, test_img, “conv1”)

图:像素化的T型图案
通过可视化,你会发现:

  • C1层的滤波器主要响应边缘、角点等低级特征。
  • C3层的特征图开始呈现出更复杂的纹理组合。
  • 不同的滤波器似乎学会了关注不同方向和模式的特征。

总结与启示:LeNet5留给我们的遗产

LeNet5虽小,却意义非凡。它确立了现代卷积神经网络的基本设计原则:

  1. 层级抽象:网络从边缘、纹理等低级特征,逐步组合成部件和整体等高级特征。
  2. 局部连接与权值共享:极大地提升了特征提取的效率和泛化能力。
  3. 池化操作:在保留主要特征的同时降低空间维度,增强平移不变性。
  4. 全连接层整合:最终将全局特征映射为分类决策。

尽管当今我们有ResNet、Vision Transformer等更强大复杂的模型,但它们的思想源头皆可追溯至LeNet5。这个经典的网络结构不仅是一个可运行的Python代码示例,更是一把理解深度卷积神经网络运作机理的钥匙。对于初学者而言,亲手实现并训练一个LeNet5,是迈向计算机视觉和深度学习领域的坚实一步。它提醒我们,所有复杂系统的伟大,往往都始于一个简洁而深刻的开端。




上一篇:FHeatMap:iOS高性能热力图库开发与MapKit集成实战
下一篇:学术论文AI率降低实战:从检测原理到深度改写技巧
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 20:13 , Processed in 0.148908 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表