当前位置: 首页 > news >正文

早期 CNN 的经典模型—卷积神经网络(LeNet)

目录

LeNet 的设计背景与目标

 LeNet 的网络结构(经典 LeNet-5)

局部感受野详解

一、局部感受野和全连接网络的区别

1. 传统全连接网络的问题

2. 局部感受野的解决方案

二、局部感受野的优势

1. 参数大幅减少

2. 提取局部特征

3. 平移不变性

参数共享(权值共享)详解

一、定义与核心思想

二、为什么需要参数共享?

1. 减少参数数量

2. 平移不变性

3. 强制特征学习的普适性

三、参数共享与局部感受野的协同作用

 池化详解

一、池化的核心作用

二、常见的池化方式

1. 最大池化(Max Pooling)

2. 平均池化(Average Pooling)

3. 其他变体

三、池化的工作流程

四、池化与卷积的区别

五、池化的参数设置

 为什么每个 epoch 都需要新的累加器?

代码执行流程验证

 完整代码

实验结果


LeNet 的设计背景与目标

在 LeNet 出现之前,图像识别主要依赖全连接神经网络,但存在两个关键问题:

 
  1. 参数爆炸:全连接网络将图像展平为向量,若输入为 32×32 的图像,仅第一层就需要约 1000 个神经元(参数近百万),计算成本极高。
  2. 缺乏平移不变性:图像中目标的微小位移会导致全连接网络的输入向量大幅变化,识别鲁棒性差。

LeNet 通过局部感受野、参数共享和池化三大核心思想解决了这些问题,专为图像识别设计。

 LeNet 的网络结构(经典 LeNet-5)

LeNet-5 的结构简洁且层次分明,可分为卷积层块(特征提取)和全连接层块(分类决策)两部分。整体结构如下:

层类型名称核心参数输入维度输出维度作用
输入层-手写数字图像(灰度图)32×32×132×32×1原始像素输入
卷积层C16 个 5×5 卷积核,步幅 = 1,无填充(padding=0)32×32×128×28×6提取局部特征(如边缘、拐角),输出 6 个特征图
平均池化层S22×2 窗口,步幅 = 2,无填充28×28×614×14×6降低维度(宽高减半),增强平移不变性
卷积层C316 个 5×5 卷积核,步幅 = 1,无填充14×14×610×10×16提取更复杂的组合特征(如纹理、局部形状)
平均池化层S42×2 窗口,步幅 = 2,无填充10×10×165×5×16进一步降维,宽高再减半
卷积层(特殊)C5120 个 5×5 卷积核,步幅 = 1,无填充(等价于全连接,因输入 5×5 刚好被卷积核覆盖)5×5×161×1×120提取高层抽象特征,输出 120 个特征
全连接层F684 个神经元12084整合特征,为分类做准备
输出层F710 个神经元(对应数字 0-9),使用 softmax 激活8410输出

局部感受野详解

一、局部感受野和全连接网络的区别

1. 传统全连接网络的问题

在处理图像时,若使用全连接网络:

  • 输入层神经元与图像像素一一对应(如 32×32 图像需 1024 个神经元)。
  • 隐藏层每个神经元连接所有输入神经元,导致参数极多(如 1000 个隐藏神经元需 1024×1000≈100 万参数)。
  • 这种 “全局连接” 忽略了图像的局部相关性(相邻像素关联紧密,远距离像素关联弱)。
2. 局部感受野的解决方案

在 CNN 中,卷积层的每个神经元仅连接输入图像的局部区域,而非全部像素:

  • 局部区域大小:由卷积核(滤波器)的尺寸决定。例如,5×5 的卷积核对应 5×5 的局部感受野。
  • 滑动窗口机制:卷积核在输入图像上滑动,每个位置生成一个输出值,形成特征图。

示例(输入图像 32×32,卷积核 5×5):

  • 第一个卷积神经元仅连接输入图像左上角 5×5 的区域(感受野)。
  • 第二个神经元连接向右滑动 1 步后的 5×5 区域(假设步幅 = 1)。
  • 依此类推,直到覆盖整个图像。

二、局部感受野的优势

1. 参数大幅减少
  • 每个卷积核的参数固定(如 5×5 卷积核仅 25 个参数),无论输入图像多大。
  • 相比全连接网络,参数减少几个数量级,缓解过拟合,降低计算成本。
2. 提取局部特征
  • 卷积核能自动学习图像的局部特征(如边缘、纹理、角点)。
  • 多层 CNN 通过堆叠卷积层,可从低级特征(边缘)逐步构建高级特征(物体部件、整体)。
3. 平移不变性
  • 无论特征出现在图像的哪个位置,卷积核都能检测到(因参数共享,见下文)。
  • 例如,识别数字 “7” 时,无论 “7” 出现在图像左上角还是右下角,同一卷积核都能响应。

参数共享(权值共享)详解

参数共享(Parameter Sharing) 是卷积神经网络(CNN)的核心机制之一,它与局部感受野共同构成了 CNN 的效率基石。通过参数共享,CNN 能够在大幅减少模型参数的同时,保持强大的特征提取能力。

一、定义与核心思想

参数共享是指在神经网络中,一组参数(权重)被应用于多个不同的输入位置。在 CNN 中,这一思想体现在以下方面:

  1. 卷积核的参数共享
    每个卷积核(滤波器)的参数在整个输入图像上保持不变。例如,一个 5×5 的卷积核在扫描输入图像的所有位置时,使用的是同一组权重。

  2. 跨通道的独立性
    不同卷积核的参数是独立的,但同一卷积核在不同位置的参数共享。例如,32 个卷积核会生成 32 个特征图,每个特征图使用各自独立的参数。

二、为什么需要参数共享?
1. 减少参数数量

传统全连接网络处理图像时,参数数量与图像尺寸的平方成正比(如 32×32 图像需约 100 万参数)。而 CNN 通过参数共享,将参数数量降低到与图像尺寸无关:

 
  • 一个 5×5 的卷积核仅需 25 个参数(加偏置 26 个)。
  • 若有 32 个卷积核,总参数仅 32×26=832 个,远少于全连接网络。
2. 平移不变性

参数共享使 CNN 对图像中的平移变换具有鲁棒性:

  • 无论特征出现在图像的哪个位置,同一卷积核都能检测到它。
  • 例如,一个识别 “猫耳朵” 的卷积核,在图像左上角和右下角都能发挥作用。
3. 强制特征学习的普适性

通过参数共享,网络被迫学习在图像所有位置都有效的特征,而非特定位置的特征,从而增强泛化能力。

三、参数共享与局部感受野的协同作用

参数共享与局部感受野是 CNN 的两大核心思想,二者相互配合:

  1. 局部感受野:每个神经元仅连接输入的局部区域,提取局部特征。
  2. 参数共享:同一卷积核在所有局部区域使用相同参数,检测相同特征。

示例

  • 一个垂直边缘检测器(卷积核)通过参数共享,在图像的所有位置检测垂直边缘。
  • 若没有参数共享,网络需为每个位置学习独立的边缘检测器,参数数量爆炸式增长。

 池化详解

在卷积神经网络(CNN)中,池化(Pooling) 是一种重要的下采样操作,主要用于减少特征图的空间维度(高度和宽度),同时保留关键特征。它通常紧跟在卷积层之后,是 CNN 中控制模型复杂度、提升计算效率和增强平移不变性的核心手段之一。

一、池化的核心作用

  1. 降低维度:通过对特征图进行聚合操作,减少输出特征图的尺寸(例如将 2×2 区域压缩为 1 个值),从而降低后续层的计算量和参数数量,避免过拟合。
  2. 增强平移不变性:对局部区域的特征进行聚合(如取最大值或平均值),使得模型对输入数据的微小位移不敏感(例如图像中物体的轻微移动不影响特征提取结果)。
  3. 保留关键特征:通过选择局部区域的显著特征(如最大值)或统计特征(如平均值),过滤冗余信息,突出重要模式。

二、常见的池化方式

池化操作通过一个固定大小的 “池化窗口” 在特征图上滑动(类似卷积窗口),对窗口内的元素进行聚合计算。常见的池化方式有:

1. 最大池化(Max Pooling)
  • 操作:取池化窗口内所有元素的最大值作为输出。
  • 特点:保留局部区域内的最显著特征(如边缘、纹理的强度),对噪声更鲁棒,是实际应用中最常用的池化方式。
  • 示例:对窗口[[1, 3], [2, 4]]进行最大池化,结果为4
2. 平均池化(Average Pooling)
  • 操作:取池化窗口内所有元素的平均值作为输出。
  • 特点:保留局部区域的整体强度信息,对特征的平滑性更好,但可能会弱化显著特征。
  • 示例:对窗口[[1, 3], [2, 4]]进行平均池化,结果为(1+3+2+4)/4 = 2.5
3. 其他变体
  • 最小池化:取窗口内最小值(较少用,适用于检测暗区域特征)。
  • L2 池化:取窗口内元素平方和的平方根,兼顾数值大小和稳定性。

三、池化的工作流程

2×2 最大池化为例,步骤如下:

  1. 设定池化窗口大小(如 2×2)和滑动步长(通常与窗口大小相同,如步长 = 2)。
  2. 窗口从特征图左上角开始,依次在水平和垂直方向滑动。
  3. 对每个窗口内的元素执行池化操作(如取最大值),生成输出特征图的一个元素。
  4. 重复滑动,直到覆盖整个特征图。

示例
输入特征图为 3×3 矩阵:

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

使用 2×2 最大池化(步长 = 2),输出为 2×2 矩阵:

 
[[4, 5],  # 窗口(0-1行, 0-1列)最大值=4;窗口(0-1行, 1-2列)最大值=5[7, 8]]  # 窗口(1-2行, 0-1列)最大值=7;窗口(1-2行, 1-2列)最大值=8

四、池化与卷积的区别

维度卷积操作池化操作
核心目的提取局部特征(如边缘、纹理)降低维度,保留关键特征
是否有参数有卷积核参数(需要学习)无参数(仅执行固定聚合规则)
操作对象输入与卷积核的加权求和窗口内元素的聚合(max/avg 等)
输出维度变化可通过 padding 和 stride 控制通常尺寸减小(下采样)

五、池化的参数设置

与卷积类似,池化操作也需要指定:

  • 池化窗口大小(kernel_size):如 2×2、3×3(常用奇数,便于对称滑动)。
  • 步长(stride):窗口滑动的步幅,默认与窗口大小相同(如 2×2 窗口对应步长 = 2)。
  • 填充(padding):在特征图边缘补 0,用于保持输出尺寸(较少用,因池化通常目的是降维)。

例如,在 PyTorch 中定义一个 3×3 最大池化层:

 
import torch.nn as nn
pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)  # 窗口3×3,步长2,边缘补1层0

 为什么每个 epoch 都需要新的累加器?

累加器(d2l.Accumulator)的作用是统计当前 epoch 内的训练指标(训练损失总和、正确预测数、总样本数)。每个 epoch 是一个独立的训练周期,需要单独统计该周期的指标,因此必须在每个 epoch 开始时重置累加器。

具体来说:

  1. metric 的三个累加项

    • metric[0]:当前 epoch 所有样本的损失总和(l * X.shape[0])。
    • metric[1]:当前 epoch 所有样本中正确预测的数量(d2l.accuracy(y_hat, y))。
    • metric[2]:当前 epoch 处理的总样本数(X.shape[0],即批次大小)。
  2. 每个 epoch 独立统计

    • 第 1 个 epoch 结束后,metric 中存储的是第 1 个 epoch 的指标,用于计算该 epoch 的平均损失和准确率。
    • 第 2 个 epoch 开始时,必须创建新的 metric,否则会与第 1 个 epoch 的指标混淆,导致计算错误。

代码执行流程验证

假设 num_epochs=2(训练 2 个 epoch),流程如下:

 
# 第1个epoch开始
epoch=0:metric = d2l.Accumulator(3)  # 新累加器,初始值[0,0,0]遍历所有批次:metric.add(当前批次损失, 当前批次正确数, 当前批次样本数)计算第1个epoch的指标:train_l = metric[0]/metric[2]  # 第1个epoch的平均损失train_acc = metric[1]/metric[2]  # 第1个epoch的准确率# 第2个epoch开始
epoch=1:metric = d2l.Accumulator(3)  # 新累加器,重置为[0,0,0]遍历所有批次:metric.add(当前批次损失, 当前批次正确数, 当前批次样本数)计算第2个epoch的指标:train_l = metric[0]/metric[2]  # 第2个epoch的平均损失(独立于第1个epoch)

 

如果不重置累加器,第 2 个 epoch 的指标会叠加到第 1 个 epoch 上,导致结果错误。

 完整代码

"""
文件名: 6.6  卷积神经网络(LeNet)
作者: 墨尘
日期: 2025/7/13
项目名: dl_env
备注: 
"""
import torch
from torch import nn
from d2l import torch as d2l
# 手动显示图像(关键)
import matplotlib.pyplot as plt
import matplotlib.text as text  # 新增:用于修改文本绘制# -------------------------- 核心解决方案:替换减号 --------------------------
# 定义替换函数:将Unicode减号U+2212替换为普通减号-
def replace_minus(s):if isinstance(s, str):return s.replace('\u2212', '-')return s# 安全重写Text类的set_text方法,避免super()错误
original_set_text = text.Text.set_text  # 保存原始方法
def new_set_text(self, s):s = replace_minus(s)  # 替换减号return original_set_text(self, s)  # 调用原始方法
text.Text.set_text = new_set_text  # 应用新方法
# -------------------------------------------------------------------------# -------------------------- 字体配置(关键修改)--------------------------
# 解决中文显示和 Unicode 减号(U+2212)显示问题
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams["text.usetex"] = True  # 使用Latex渲染
plt.rcParams["axes.unicode_minus"] = True  # 正确显示负号
plt.rcParams["mathtext.fontset"] = "cm"    # 确保数学符号(如减号)正常显示
d2l.plt.rcParams.update(plt.rcParams)      # 让 d2l 绘图工具继承字体配置
# -------------------------------------------------------------------------"""激活函数的核心作用是引入非线性,因此:
隐藏层必须使用激活函数,否则深层网络失去意义;
输出层根据任务选择是否使用,取决于是否需要对输出进行范围约束或概率化。"""# 评估模型在数据集上的准确率(使用GPU)
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save"""使用GPU计算模型在数据集上的精度"""if isinstance(net, nn.Module):net.eval()  # 设置为评估模式(关闭Dropout、BatchNorm等)if not device:device = next(iter(net.parameters())).device  # 获取模型参数所在的设备# 累加器:用于统计正确预测数和总样本数metric = d2l.Accumulator(2)  # 创建一个包含2个累加项的累加器with torch.no_grad():  # 关闭梯度计算,节省内存和计算资源for X, y in data_iter:# 将数据移至GPUif isinstance(X, list):X = [x.to(device) for x in X]  # 处理特殊输入(如BERT)else:X = X.to(device)y = y.to(device)# 计算预测正确的样本数,并累加到metric中metric.add(d2l.accuracy(net(X), y), y.numel())  # y.numel()返回y中元素的总数return metric[0] / metric[1]  # 返回准确率# 训练模型(使用GPU)自定义GPU训练模型
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):"""用GPU训练模型(在第六章定义)"""# 权重初始化函数:对线性层和卷积层使用Xavier均匀初始化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)  # 将模型移至GPU# 定义优化器和损失函数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'])timer, num_batches = d2l.Timer(), len(train_iter)  # 计时器和批次数量# 训练主循环for epoch in range(num_epochs):# 训练损失之和,训练准确率之和,样本数metric = d2l.Accumulator(3)  # 创建包含3个累加项的累加器net.train()  # 设置为训练模式# 遍历每个批次的数据for i, (X, y) in enumerate(train_iter):timer.start()  # 开始计时# 梯度清零,前向传播,计算损失,反向传播,更新参数optimizer.zero_grad()X, y = X.to(device), y.to(device)  # 将数据移至GPUy_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]# 每5个批次或最后一个批次时,更新动画if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:animator.add(epoch + (i + 1) / num_batches,(train_l, train_acc, None))# 每个epoch结束后,在测试集上评估模型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}, test acc {test_acc:.3f}')print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on {str(device)}')if __name__ == '__main__':# ------------------------------# 步骤1: 定义LeNet-5网络# ------------------------------# 定义LeNet-5网络(针对MNIST数据集优化版本)net = nn.Sequential(# ===== 第一卷积块:提取低级特征 =====nn.Conv2d(1, 6, kernel_size=5, padding=2),  # 输入1通道,输出6通道,保持尺寸28x28nn.Sigmoid(),  # 引入非线性nn.AvgPool2d(kernel_size=2, stride=2),  # 池化,尺寸减半至14x14# ===== 第二卷积块:提取中级特征 =====nn.Conv2d(6, 16, kernel_size=5),  # 输入6通道,输出16通道,尺寸减小至10x10nn.Sigmoid(),  # 引入非线性nn.AvgPool2d(kernel_size=2, stride=2),  # 池化,尺寸减半至5x5# ===== 展平层:将多维特征图转为一维向量 =====nn.Flatten(),  # 展平为400维向量# ===== 全连接层块:分类器 =====nn.Linear(16 * 5 * 5, 120),  # 400→120nn.Sigmoid(),  # 引入非线性nn.Linear(120, 84),  # 120→84nn.Sigmoid(),  # 引入非线性nn.Linear(84, 10)  # 84→10(输出10类,对应0-9数字))# -----------------------------------------------# 步骤2: 检查LeNet-5网络的可用性# ------------------------------------------------# 创建一个随机输入张量,模拟MNIST数据X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)# 逐层打印输出形状,验证网络结构for layer in net:X = layer(X)print(layer.__class__.__name__, 'output shape: \t', X.shape)# -----------------------------------------------# 步骤3: 初始化数据集# ------------------------------------------------# 加载Fashion-MNIST数据集(28x28灰度图像,10个类别)batch_size = 256train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)# -----------------------------------------------# 步骤4: 训练# ------------------------------------------------lr, num_epochs = 0.9, 10  # 学习率和训练轮数# 使用GPU训练模型(如果可用)train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())# 显示图像plt.show(block=True)  # block=True 确保窗口阻塞,直到手动关闭

实验结果

http://www.lryc.cn/news/587355.html

相关文章:

  • 板凳-------Mysql cookbook学习 (十一--------8)
  • 【深度学习新浪潮】什么是新视角合成?
  • STM32-第五节-TIM定时器-1(定时器中断)
  • JAVA并发——synchronized的实现原理
  • 特征选择方法
  • 一文打通MySQL任督二脉(事务、索引、锁、SQL优化、分库分表)
  • GraphRAG Docker化部署,接入本地Ollama完整技术指南:从零基础到生产部署的系统性知识体系
  • AEC线性处理
  • 【iOS】方法与消息底层分析
  • 【设计模式】命令模式 (动作(Action)模式或事务(Transaction)模式)宏命令
  • phpMyAdmin:一款经典的MySQL在线管理工具又回来了
  • 【RA-Eco-RA6E2-64PIN-V1.0 开发板】ADC 电压的 LabVIEW 数据采集
  • 第一个Flink 程序 WordCount,词频统计(批处理)
  • git实操
  • 鸿蒙项目构建配置
  • 区分三种IO模型和select/poll/epoll
  • Java设计模式之行为型模式(命令模式)
  • Spring Boot + MyBatis 实现用户登录功能详解(基础)
  • JAVA学习笔记 JAVA开发环境部署-001
  • 深入分析---虚拟线程VS传统多线程
  • 力扣刷题记录(c++)09
  • 在 OCI 生成式 AI 上搭一个「指定地区拉面店 MCP Server」——从 0 到 1 实战记录
  • opencv中contours的使用
  • 【设计模式】策略模式(政策(Policy)模式)
  • Java小白-设计模式
  • Java 接口 剖析
  • 操作系统-第四章存储器管理和第五章设备管理-知识点整理(知识点学习 / 期末复习 / 面试 / 笔试)
  • 什么是渐进式框架
  • 什么时候会用到 concurrent.futures?要不要背?
  • 17.使用DenseNet网络进行Fashion-Mnist分类