CNN入门实战:核心组件、特征图尺寸计算与LeNet-5/AlexNet解析 📅 发布时间:2026/9/1 2:50:24 👁 浏览次数: 很多初学者在 B 站、CSDN、知乎上看了大量卷积神经网络CNN的视频和文章每个组件都能说出名字卷积层、池化层、激活函数、全连接层甚至 LeNet-5、AlexNet 也背得滚瓜烂熟。但真到自己动手写代码、设计一个网络时立刻卡住——卷积核大小怎么选padding 加多少特征图尺寸到底是怎么从 32 变成 28 再变成 14 的全连接层的输入维度又是怎么算出来的说白了概念都认识但串不起来。这正是这篇文章要解决的问题。我写这篇教程的判断很明确学 CNN最重要的不是记住那些层叫什么而是建立一条“特征图流动”的主线。从输入图像开始数据经过每一个层之后尺寸、通道数、参数量发生了什么变化为什么这样设计这才是你真正需要掌握的东西。读完这篇文章你能独立完成三件事理解并复述 LeNet-5 和 AlexNet 的完整结构手写特征图尺寸计算公式用 PyTorch 从零实现并训练一个 CNN 模型完成手写数字识别。文章会按照“问题 → 概念 → 公式 → 经典网络拆解 → 完整代码 → 验证 → 排错 → 最佳实践”的顺序展开建议先收藏再阅读。1. 这篇文章真正要解决的问题先说说全连接网络处理图像时遇到的困境你就知道 CNN 为什么出现了。假设输入是一张 32x32 像素的灰度图如果用全连接网络输入层就要展开成 32x321024 个神经元。如果隐藏层有 1024 个神经元这一层的权重参数量就是 1024x1024约 105 万。这还算能接受。但如果是 224x224 的彩色图呢输入维度是 224x224x3150528同样 1024 个神经元的隐藏层权重参数量直接飙到 1.5 亿。训练这样的模型不仅需要海量数据而且极其容易过拟合。更关键的问题是全连接层忽略了图像的空间结构。一张猫的图片把像素顺序打乱后人眼能看出来但模型就认不出来了因为像素之间的相对位置关系全丢了。但图像识别的本质恰恰是局部纹理、边缘、形状这些空间特征。CNN 就是为这两大问题而生的CNN 的核心设计思想利用卷积操作提取局部特征通过参数共享大幅减少参数量通过层级结构从低层边缘到高层语义逐级抽象最终完成分类或检测任务。这篇文章适合以下读者学完了 Python 基础想进入深度学习但被各种概念绕晕的初学者。看完视频教程觉得自己懂了但一连代码就报维度错误的新手。想系统梳理 CNN 核心组件、经典网络结构准备面试或写毕业论文的技术人。不适合谁如果你已经能独立实现并训练 ResNet、读得懂源码那么这篇基础教程对你来说过于入门了。2. CNN 五个核心组件的原理与对比CNN 的标准结构可以概括成一句话通过卷积层和池化层反复提取特征通过激活函数引入非线性最后用全连接层输出分类结果。2.1 卷积层卷积层是 CNN 的灵魂。它的作用是提取图像的局部特征。通俗地理解卷积核就是一个小的滑动窗口比如 3x3 或 5x5在图像上从左到右、从上到下地滑动每次与覆盖的区域做“逐元素相乘再求和”生成一个新的值。滑动完成后就得到了一张特征图Feature Map。三个必须掌握的概念卷积核Kernel / Filter一个小的权重矩阵它学习的是某种特定的局部模式比如边缘、角点、纹理。步长Stride卷积核每次滑动的像素数。步长为 1滑动得密集特征图大步长为 2特征图缩小一半。填充Padding在图像边缘补一圈或多圈 0。作用是防止边缘信息丢失同时控制输出尺寸。当 padding1、kernel_size3、stride1 时输出尺寸和输入尺寸一致这种操作在同为 k 的卷积中很常用。为什么需要参数共享因为一个卷积核在整个图像上滑动时权重是相同的。这意味着无论图像多大一个卷积核只有 k×k 个参数。假设用 32 个 3x3 卷积核参数只有 32x9288 个远小于全连接的做法。2.2 池化层池化层的作用是降采样也就是缩小特征图的尺寸同时保留最重要的信息。最常用的是最大池化Max Pooling在 2x2 的窗口内取最大值窗口滑动步长为 2特征图的宽高各缩小一半。为什么要取最大值因为对卷积层输出的特征来说某个位置数值越大代表该位置对某种模式的响应越强保留这个最大响应可以保留最显著的特征。池化层没有需要学习的参数这是它和卷积层的本质区别。它只做一件事压缩信息。这样做的好处有三点降低计算量让后续层的计算更轻量。扩大感受野让后面层能看到更大范围的原始图像区域。增强平移不变性目标稍微偏移一点池化结果变化不大。2.3 激活函数卷积和池化本质都是线性操作卷积是乘加池化是取最大/平均。如果只有这些操作无论网络多深整体依然是线性的表达能力非常有限。激活函数的作用就是引入非线性让网络能够拟合复杂的映射关系。CNN 中最常用的是 ReLURectified Linear Unitf(x) max(0, x)x 大于 0 时保持原值小于 0 时变成 0。ReLU 的优势是计算简单、在正区间梯度恒为 1能有效缓解梯度消失问题。相比之下Sigmoid 在输入值很大或很小时梯度接近 0多个层叠加后梯度连乘会越来越小导致深层网络难以训练。这里给一个直观对比激活函数公式输出范围主要优点主要缺点Sigmoid1/(1e^-x)(0, 1)输出可解释为概率梯度消失、计算量大Tanh(e^x - e^-x)/(e^x e^-x)(-1, 1)均值接近 0梯度更强仍有梯度消失问题ReLUmax(0, x)[0, ∞)计算快、正区间梯度恒定神经元可能“死亡”Leaky ReLUmax(0.01x, x)(-∞, ∞)避免神经元死亡需要多调一个超参数2.4 全连接层经过多次卷积和池化后特征图已经高度抽象这时需要把它们“展平”成一维向量然后送入全连接层。全连接层的作用是把前面提取到的特征映射到样本标记空间完成分类。最后一个全连接层的神经元数量通常等于类别数。以 LeNet-5 为例最后一层输出 10 个值分别对应 0-9 十类数字。配合 Softmax 函数这 10 个值可以变成 10 个概率概率最高的类别就是预测结果。2.5 五者关系一句话总结卷积层负责“找特征”池化层负责“压缩信息”激活函数负责“引入非线性”全连接层负责“做决策”。整体流程是输入图像 - 卷积 - 激活 - 池化 - 卷积 - 激活 - 池化 - 展平 - 全连接 - 输出2.6 五者对比表组件核心功能是否有可学习参数对特征图尺寸影响常见配置卷积层提取局部特征是取决于核大小、步长、padding3x3、padding1、stride1池化层降采样、压缩信息否通常宽高减半2x2、stride2、MaxPool激活函数引入非线性否尺寸不变ReLU全连接层分类决策是展平后映射到类别128/64/类别数Softmax输出概率分布否尺寸不变与类别数一致这张表建议保存下来后面分析 LeNet-5 和 AlexNet 会反复用到。3. 特征图尺寸计算CNN 最容易算错的地方很多初学者写代码报错80% 的“维度不匹配”问题都出在特征图尺寸算错。这一节把这个基本功彻底搞定。3.1 输出尺寸的标准公式H_out floor((H_in 2 * padding - dilation * (kernel_size - 1) - 1) / stride 1) W_out floor((W_in 2 * padding - dilation * (kernel_size - 1) - 1) / stride 1)绝大多数场景下 dilation1公式可以简化为H_out floor((H_in 2 * padding - kernel_size) / stride 1)其中 floor 表示向下取整。这个公式适用于卷积层和池化层。3.2 三个手算案例案例一输入 32x32卷积核 5x5padding0stride1。(32 0 - 5) / 1 1 28所以输出 28x28。案例二输入 28x28池化核 2x2stride2。(28 0 - 2) / 2 1 14所以输出 14x14。案例三输入 224x224卷积核 3x3padding1stride1。(224 2 - 3) / 1 1 224So-called “same padding”输出尺寸保持不变所以 padding1 搭配 kernel3 时经常被用来保持尺寸不变。3.3 用 Python 验证计算# 文件路径feature_map_size.py def calc_output_size(input_size, kernel_size, stride1, padding0, dilation1): 计算卷积/池化后的特征图尺寸 参数: input_size: 输入尺寸宽或高 kernel_size: 卷积核尺寸 stride: 步长 padding: 填充大小 dilation: 空洞卷积系数默认为1 返回: 输出尺寸 output_size ( input_size 2 * padding - dilation * (kernel_size - 1) - 1 ) // stride 1 return output_size if __name__ __main__: # LeNet-5 中 C1 层输入32x325x5卷积输出28x28 print(calc_output_size(32, 5, stride1, padding0)) # 28 # 池化层输入28x282x2池化输出14x14 print(calc_output_size(28, 2, stride2, padding0)) # 14 # 保持尺寸不变的卷积输入224x2243x3卷积padding1 print(calc_output_size(224, 3, stride1, padding1)) # 224运行结果28 14 224在实际项目中你不会每一步都手算但必须在设计网络时对每层的特征图尺寸变化有预估。因为这会直接影响显存占用。例如一张 224x224 的彩色图经过 64 个卷积核提取特征后特征图是 64x224x224占用约 224x224x64x4 字节 ≈ 12.8MB如果 batch size 是 64仅这一层就是 800MB 的显存开销。算不清楚后面就是无休止的 OOM 报错。4. LeNet-5 逐层拆解CNN 的教科书LeNet-5 是 Yann LeCun 在 1998 年提出的经典卷积神经网络最初用于手写数字识别。它的结构简单清晰是理解 CNN 架构的最佳起点。4.1 完整结构表官方 LeNet-5 输入是 32x32 的灰度图注意 MNIST 原始图片是 28x28所以复现时会在前面补齐或调整网络输入尺寸。下面按原始结构看层名称类型核/池化配置输出尺寸参数量说明INPUT输入层无1x32x32灰度图C1卷积层6个5x5核stride16x28x286x(5x51)≈156S2池化层2x2stride2平均池化6x14x142x1x6≈12C3卷积层16个5x5核stride116x10x1016x(6x5x51)≈2416S4池化层2x2stride216x5x52x1x16≈32C5卷积层120个5x5核120x1x1120x(16x5x51)≈48120F6全连接层84个神经元84120x8484≈10164OUTPUT全连接层10个神经元1084x1010≈850总参数量约 6 万个。这个数量级在今天看来非常小但在 90 年代已经能实现银行支票手写数字识别。4.2 结构设计思想LeNet-5 确立了 CNN 的标准范式特征提取 分类。前半段C1-S2-C3-S4-C5是特征提取器通过卷积不断加深通道数、缩小空间尺寸从原始像素中提取越来越抽象的特征。后半段F6-OUTPUT是分类器把特征映射成类别概率。注意 S2 用的是平均池化平均后加可学习的权重和偏置而现代 CNN 更常用最大池化。这说明“池化方式的选择”在那个年代已经在探索了。4.3 动手计算一遍 C1 层输入1x32x32卷积核 6 个大小为 5x5stride1padding0。输出尺寸按公式H_out (32 0 - 5) / 1 1 28通道数变为 6。所以 C1 输出是 6x28x28。4.4 LeNet-5 的局限使用 Sigmoid / Tanh 激活存在一定的梯度消失问题。均值池化在现代实现中不如最大池化用得普遍。输入尺寸 32x32 偏小处理更大图像需要调整结构。没有 Dropout 等正则化手段在复杂任务上容易过拟合。尽管如此LeNet-5 依然是理解 CNN“如何从原始像素一步步变化到分类结果”的最佳范例。5. AlexNet 带来三个关键变革为什么它成了分水岭如果说 LeNet-5 证明了 CNN 的价值AlexNet 则让 CNN 真正“出圈”。2012 年AlexNet 在 ImageNet 图像分类竞赛中以巨大优势夺冠错误率远低于传统方法掀起了深度学习革命。AlexNet 的结构比 LeNet-5 深很多5 个卷积层 3 个全连接层总参数量约 6000 万使用两块 GPU 并行训练。结构本身值得了解但更值得关注的是它带来的三个可以复用到今天的技术变革。5.1 ReLU 激活函数取代 SigmoidAlexNet 使用 ReLU 替代 Sigmoid从根本上缓解了梯度消失问题。ReLU 在正区间的导数是常数 1即使网络很深反向传播时梯度也不会因为连乘不断衰减。这让训练深层网络变得可行而且收敛速度快很多。这一点在 LeNet-5 时代是做不到的。如果你用 Sigmoid 去训练一个 8 层以上的 CNN大概率会观察到 loss 下降极慢甚至不下降。5.2 Dropout 随机失活AlexNet 在全连接层中使用 Dropout训练时每次随机“丢弃”一部分神经元输出置为 0让网络不能依赖某几个特定神经元从而学习到更鲁棒的特征。Dropout 是一种正则化方法能有效缓解过拟合。模型有 6000 万参数而 ImageNet 数据集虽然大但模型容量更高如果没有 Dropout训练集上会很快过拟合。5.3 数据增强AlexNet 在训练时对图片做随机裁剪、水平翻转、颜色扰动等操作相当于从有限的数据中“变出”更多样本。这同样是为了缓解过拟合。数据增强到今天仍是几乎所有图像任务训练流程的标配。5.4 与 LeNet-5 的对比对比维度LeNet-5AlexNet提出年份19982012卷积层数3含 C55全连接层数23激活函数Sigmoid/TanhReLU池化方式平均池化最大池化 重叠池化正则化手段无Dropout 数据增强总参数量约 6 万约 6000 万训练硬件单 CPU双 GPU经典数据集MNISTImageNet从 LeNet-5 到 AlexNet网络变得更深、更大训练技巧更丰富。但核心原则没变先卷积提特征再全连接分类。后面出现的 VGG、ResNet 都是在这个范式上继续演进。6. 环境准备与前置条件接下来进入代码环节。我们使用 PyTorch 实现并训练 LeNet-5 和 AlexNet 风格的 CNN在 MNIST 手写数字数据集上验证效果。6.1 环境要求建议配置如下具体版本以你的实际环境为准Python 3.8 及以上PyTorch 1.10 及以上CPU 版本即可跑通 MNISTtorchvision与 PyTorch 版本匹配matplotlib用于可视化numpyMNIST 数据集非常小28x28 灰度图共 10 类数字6 万张训练图、1 万张测试图。即使只用 CPU训练几个 epoch 也就几分钟非常适合入门验证。6.2 安装依赖使用 pip 安装pip install torch torchvision matplotlib numpy如果显卡支持 CUDA 且你想用 GPU请到 PyTorch 官网选择合适的安装命令。这里不做过多展开因为本文示例在纯 CPU 环境下也能跑通。验证安装python -c import torch; print(torch.__version__)如果输出版本号说明环境正常。7. 完整代码实现从 LeNet-5 到 AlexNet 风格网络下面我们分四步完成整个项目。7.1 数据加载与预处理MNIST 数据集可以通过 torchvision 直接下载。注意下载需要联网。# 文件路径data_loader.py import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 数据预处理转为Tensor并归一化到 [0, 1] transform transforms.Compose([ transforms.Resize((32, 32)), # LeNet-5 原始输入是 32x32 transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 下载并加载训练集 train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) # 下载并加载测试集 test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse) if __name__ __main__: # 检查一个 batch 的数据形状 images, labels next(iter(train_loader)) print(f训练批次图像形状: {images.shape}) print(f训练批次标签形状: {labels.shape})这里的 transforms.Resize((32, 32)) 会先把 28x28 的 MNIST 图片放大到 32x32以匹配 LeNet-5 的输入设计。如果你不想 Resize也可以把网络第一层输入改为 28x28原理是一样的。Normalize 使用的均值和标准差是 MNIST 数据集的常用值作用是把像素分布标准化加速模型收敛。7.2 LeNet-5 模型定义在 PyTorch 中所有模型都继承自 nn.Module。下面实现一个在 MNIST 上可用的 LeNet-5 变体。# 文件路径lenet5.py import torch.nn as nn import torch.nn.functional as F class LeNet5(nn.Module): def __init__(self, num_classes10): super(LeNet5, self).__init__() # C1: 卷积层输入1通道输出6通道卷积核5x5 self.conv1 nn.Conv2d(in_channels1, out_channels6, kernel_size5, stride1, padding0) # S2: 池化层2x2最大池化stride2 self.pool1 nn.MaxPool2d(kernel_size2, stride2) # C3: 卷积层输入6通道输出16通道卷积核5x5 self.conv2 nn.Conv2d(in_channels6, out_channels16, kernel_size5, stride1, padding0) # S4: 池化层 self.pool2 nn.MaxPool2d(kernel_size2, stride2) # C5: 卷积层输入16通道输出120通道卷积核5x5 self.conv3 nn.Conv2d(in_channels16, out_channels120, kernel_size5, stride1, padding0) # F6: 全连接层 self.fc1 nn.Linear(120, 84) # OUTPUT: 输出层 self.fc2 nn.Linear(84, num_classes) def forward(self, x): # 输入 x: [batch_size, 1, 32, 32] x self.pool1(F.relu(self.conv1(x))) # 32 - 28 - 14 x self.pool2(F.relu(self.conv2(x))) # 14 - 10 - 5 x F.relu(self.conv3(x)) # 5 - 1 x x.view(x.size(0), -1) # 展平 x F.relu(self.fc1(x)) x self.fc2(x) return x注意几点C5 在原版 LeNet-5 中输出 120x1x1本质上是把 16x5x5 的特征图直接卷积成一个 120 维向量。这里省略了最后的展平前处理因为 conv3 后 x.view(x.size(0), -1) 会自动展平。激活函数我们使用 ReLU 而不是原版的 Sigmoid。原因前面说过ReLU 更好训练、收敛更快。在很多 LeNet-5 复现代码中这种修改是标准做法。原版 S2 使用带权重的平均池化这里用 MaxPool2d 代替。数学上略有差别但实践上更简洁、效果不差。原版 C3 层的 16 个卷积核并不是与输入所有 6 个通道做全连接而是按一定规则部分连接。这里为了简洁使用 PyTorch 默认的全连接模式16 个核每个都与 6 个通道卷积。如果你严格按论文复现需要自定义分组连接但实际工程中直接使用上述设计已经能达到很好的效果。7.3 AlexNet 风格简化版模型完整 AlexNet 需要处理 224x224 输入、参数量达 6000 万在 MNIST 上不合适。我们实现一个保留 AlexNet 核心思想的简化版本5 个卷积层 3 个全连接层、ReLU、Dropout、最大池化。# 文件路径simple_alexnet.py import torch.nn as nn import torch.nn.functional as F class SimpleAlexNet(nn.Module): 受 AlexNet 启发的简化 CNN适配 32x32 灰度图。 核心保留堆叠卷积、ReLU、Dropout、三层全连接 def __init__(self, num_classes10, dropout_rate0.5): super(SimpleAlexNet, self).__init__() # 卷积特征提取部分 self.features nn.Sequential( # 第一段32x32 - 16x16 nn.Conv2d(1, 32, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 第二段16x16 - 8x8 nn.Conv2d(32, 64, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 第三段8x8 - 4x4 nn.Conv2d(64, 128, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), # 第四段4x4 - 2x2 nn.Conv2d(128, 256, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), ) # 分类部分 self.classifier nn.Sequential( nn.Dropout(pdropout_rate), nn.Linear(256 * 2 * 2, 256), nn.ReLU(inplaceTrue), nn.Dropout(pdropout_rate), nn.Linear(256, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x这个简化版本保留了 AlexNet 的“深卷积堆叠 Dropout 正则化”精神。特征图的变化过程是32x32x1 - 16x16x32 - 8x8x64 - 4x4x128 - 2x2x256最后展平为 256x2x21024 维向量送入全连接层。7.4 训练与评估函数有了数据和模型还需要训练循环。下面的代码可以作为通用模板适用于上面两个模型。# 文件路径train.py import torch import torch.nn as nn import torch.optim as optim from data_loader import train_loader, test_loader from lenet5 import LeNet5 def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) # 清零梯度 optimizer.zero_grad() # 前向传播 outputs model(images) # 计算损失 loss criterion(outputs, labels) # 反向传播 更新参数 loss.backward() optimizer.step() # 统计 total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy def evaluate(model, loader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) print(f使用设备: {device}) model LeNet5(num_classes10).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) num_epochs 5 for epoch in range(1, num_epochs 1): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device ) val_loss, val_acc evaluate(model, test_loader, criterion, device) print( fEpoch {epoch}/{num_epochs} | fTrain Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f} | fVal Loss: {val_loss:.4f}, Val Acc: {val_acc:.4f} ) # 保存模型 torch.save(model.state_dict(), lenet5_mnist.pth) print(模型已保存为 lenet5_mnist.pth) if __name__ __main__: main()关键点optimizer.zero_grad() 必须在每次反向传播前清零梯度否则梯度会累加。loss.backward() 计算梯度optimizer.step() 更新参数。torch.max(outputs, 1) 返回每个样本概率最大的类别索引。model.train() 和 model.eval() 控制 Dropout 等训练专用层的行为。评估时不用 Dropout所以一定要切换模式。不同初始化下5 个 epoch 后 MNIST 测试准确率通常在 98% 以上这归功于 MNIST 任务本身简单也说明哪怕基础结构也能达到不错的效果。如果想切换到 SimpleAlexNet只需要修改 main 中的模型定义from simple_alexnet import SimpleAlexNet model SimpleAlexNet(num_classes10).to(device)其他代码不用改。8. 运行结果与效果验证在项目目录下依次执行python data_loader.py python train.py预期输出类似使用设备: cpu Epoch 1/5 | Train Loss: 0.2572, Train Acc: 0.9208 | Val Loss: 0.0808, Val Acc: 0.9742 Epoch 2/5 | Train Loss: 0.0768, Train Acc: 0.9769 | Val Loss: 0.0557, Val Acc: 0.9824 Epoch 3/5 | Train Loss: 0.0531, Train Acc: 0.9838 | Val Loss: 0.0437, Val Acc: 0.9846 Epoch 4/5 | Train Loss: 0.0412, Train Acc: 0.9875 | Val Loss: 0.0400, Val Acc: 0.9861 Epoch 5/5 | Train Loss: 0.0337, Train Acc: 0.9898 | Val Loss: 0.0338, Val Acc: 0.9873 模型已保存为 lenet5_mnist.pth如何判断训练成功训练损失和验证损失都在下降。验证准确率稳定在 97% 以上。训练集准确率和验证集准确率差距不大说明没有严重过拟合。如果损失不降第一步检查学习率lr 太大容易震荡太小收敛极慢。第二步检查数据加载器是否正常可以用可视化图片的方式确认。如果想直观看到模型效果可以加入一段推理代码# 文件路径predict.py import torch import matplotlib.pyplot as plt from torchvision import datasets, transforms from lenet5 import LeNet5 def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) # 加载模型 model LeNet5(num_classes10).to(device) model.load_state_dict(torch.load(lenet5_mnist.pth, map_locationdevice)) model.eval() # 处理单张图 transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # 取前5张测试图做预测 for i in range(5): image, label test_dataset[i] with torch.no_grad(): output model(image.unsqueeze(0).to(device)) _, predicted torch.max(output, 1) plt.subplot(1, 5, i 1) plt.imshow(image.squeeze(), cmapgray) plt.title(f真实:{label} 预测:{predicted.item()}) plt.axis(off) plt.show() if __name__ __main__: main()运行后可以看到 5 张手写数字图片和对应的预测结果。正常情况预测值和真实值一致。如果全部预测错误大概率是模型没有正常加载或者是 transform 和训练时不匹配。9. 常见问题与排查思路在训练 CNN 的过程中下面几个问题是最高频的故障点。问题现象可能原因排查方式解决方案报错 “size mismatch”全连接层输入维度与展平后的特征图维度不一致打印每一层输出的 shape逐步定位用特征图公式手算或使用 Summary 工具打印模型结构损失不下降或变成 NaN学习率过大或数据没有归一化打印 loss 值检查数据和 label 范围查看梯度值降低学习率添加 Normalize 预处理检查网络是否有除零/开方验证准确率低但训练准确率高过拟合对比训练集/验证集准确率差值增加 Dropout、数据增强减少模型参数量增加训练数据GPU 上显存不足OOMbatch size 过大或特征图通道数过多使用 torch.cuda.set_per_process_memory_fraction 限制显存或观察 nvidia-smi减小 batch size减小输入图像尺寸用梯度累积模拟大 batch训练速度极慢数据加载成为瓶颈或 CPU 训练大模型查看 CPU 占用率统计每个 batch 的耗时使用 DataLoader 的 num_workers 参数或改用 GPUMNIST 下载失败网络问题或 torchvision 版本过期检查下载地址是否能访问手动下载数据集到指定目录或者更换下载镜像源打印预测结果全是一个类别模型没有训练成功或评估时模式未切换检查 optimizer.step() 是否调用重新训练确认 model.eval() 在验证代码中已调用这里特别强调两个最容易在初学阶段被忽视的点第一model.eval()和model.train()不能写错。Dropout 只在训练时随机丢弃神经元评估时如果忘记切换模型的预测结果会带有随机性导致准确率下降。第二损失函数 CrossEntropyLoss 内部已经包含 Softmax。你不需要在全连接层后再手动加 Softmax直接在构造模型时让最后的 Linear 输出原始 logits 即可。如果在最后一个全连接层前自己加了一层 Softmax训练时的梯度会变得不稳定还容易让代码和公式对应不上。10. 最佳实践与工程建议看完代码能跑通只是第一步。如果要在真实项目中用好 CNN下面这些经验更值得关注。10.1 网络设计建议从简单模型开始而不是一上来就套 ResNet。MNIST 这种任务用 LeNet-5 就够了用大模型反而更容易过拟合。先用小模型跑通全流程再根据瓶颈扩大规模。优先使用 padding1、kernel_size3、stride1 的卷积组合。一方面保持尺寸不变卷积分支的设计更简洁另一方面 3x3 卷积在参数效率和计算效率上都很优。关于参数初始化PyTorch 默认初始化通常已经能工作不熟悉时不要乱改。如果你确实训练困难再考虑 Xavier / Kaiming 初始化原因在于激活函数不同合适的初始化方式也不同。10.2 训练技巧固定随机种子让每次训练结果可复现。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)学习率设置讲究“从大到小”。初期可以用 1e-3如果损失震荡就调小如果收敛极慢就尝试调大。更好的做法是使用学习率调度器StepLR、ReduceLROnPlateau。先跑 2-3 个 epoch 验证流程再跑完整训练。这一步能帮你提前发现数据格式错误、维度问题、硬件资源不足避免浪费大把时间。10.3 数据相关建议不要只做 ToTensor添加 Normalize 预处理。归一化能让优化过程更稳定。真实项目中数据增强非常关键随机裁剪、旋转、翻转、颜色抖动都能大幅度提升泛化能力。训练集、验证集、测试集要严格分开。不要在测试集上调参否则你验证的是“记忆”而不是“学习”。10.4 代码工程化建议用 argparse 或配置文件管理超参数。网络结构、学习率、batch size、epoch 这些值不要散落在代码各处。训练时定期保存模型 checkpoint包含 model_state_dict、optimizer_state_dict、epoch 等信息。这样即使中断也能恢复训练。用 TensorBoard 或 wandb 记录 loss、accuracy、梯度信息。可视化能极大加速排错效率。不要在训练脚本里直接改数据文件数据预处理和模型训练分离方便复用和测试。10.5 安全与合规提醒如果涉及真实业务数据例如人脸图片、医疗影像、用户图像务必注意数据隐私和合规授权。不要私自使用未授权的数据训练模型。涉及生产环境部署时先在小范围测试验证模型效果和性能后灰度发布同时保留回滚方案。模型上线后还需持续监控数据分布变化因为图像数据分布偏移会导致线上准确率下降。11. 入门后可以继续深入的方向学完 CNN 的全部组件跑通两个经典网络模型你已经掌握了深度学习的核心套路。很多看起来复杂的新模型本质上就是在“卷积提特征 全连接分类”这条主线上做了结构与训练技巧的升级。下一阶段你可以按顺序深入学习VGG观察“小卷积核堆叠”如何取代“大卷积核”。ResNet理解残差连接如何解决深层网络的退化问题。Vision Transformer (ViT)关注 Transformer 为什么能迁移到图像领域和 CNN 有什么本质区别。目标检测YOLO / Faster R-CNN掌握分类之外更实用的视觉任务。模型压缩与部署ONNX / TensorRT学完网络之后需要解决“如何在生产环境中跑得够快”的问题。真正重要的不是背下更多网络结构而是建立“看到一个新模型能手动推演特征图尺寸、参数量、计算量并能判断它在什么场景下有效”的能力。这条能力线能让你从看懂教程走向独立做项目。建议你现在就做一件事把文中 LeNet-5 的代码抄下来改成 28x28 输入去掉 Resize 这个预处理步骤然后重新计算每一层的特征图尺寸把模型跑通。做完这一步你对 CNN 的理解会超过绝大多数只看视频、只知道概念的人。