MNIST 手写数字识别可以说是计算机视觉里最经典的入门案例。它就像编程中的“Hello World”——数据简单、任务清晰、反馈直接。而 LeNet5 正是解决这个问题的经典卷积神经网络也是很多人深度学习之旅的起点。这篇文章我会把整套流程完整走一遍从数据预处理、网络结构设计、训练评估到最后一堆实战中遇到的小坑。如果你是刚开始接触 CV想亲手训练一个图像分类网络这份内容可以直接照着做。1. 项目概述LeNet5 与 MNIST 的经典组合1.1 这个项目解决什么问题MNIST 数据集包含 0~9 十个数字类别每张图片是 28×28 的灰度图训练集有 6 万张测试集有 1 万张。任务非常明确输入一张手写数字图片输出它对应的数字类别。这个问题看起来很简单但它涵盖了图像分类的全部核心环节图像输入、特征提取、特征降维、分类输出。很多抽象的概念比如卷积、池化、全连接、损失函数都可以在这个项目里获得非常具体的感知。LeNet5 是上世纪针对这一类任务设计的卷积神经网络结构非常紧凑参数量只有六万左右。在当时的硬件条件下就已经能在 MNIST 上取得很不错的准确率放到今天更是属于“轻量级”模型。我们用现代深度学习框架去复现它普通 CPU 上几分钟就能完成训练测试准确率可以稳定在 99% 左右。这本身就说明网络结构设计得非常经典即使硬件和框架迭代了很多代它的核心思想依然成立。1.2 为什么选 LeNet5 作为入门项目在实际学习和复现的过程中我觉得 LeNet5 特别适合作为第一个从零搭建的 CNN 模型。原因可以归纳为三点结构小、参数量少CPU 上训练几分钟就跑完完全不用考虑 GPU 资源分配。每一层的作用都很直观卷积层提取局部特征池化层压缩尺寸全连接层做分类。结构层次与理论课上的内容一一对应方便边写代码边理解。在 MNIST 上效果极好不需要复杂的调参就能拿到高准确率能给初学者很强的正反馈。当然也有人会问MNIST 已经被刷到 99.7% 以上了再复现 LeNet5 是不是有点过时我的看法是MNIST 的价值不在于“挑战最高分”而在于它的数据足够干净任务足够简单可以让你把注意力全部放在理解模型本身而不是被大模型的数据处理和训练技巧淹没。LeNet5 是理解现代 CNN 的基石后面再看 ResNet、EfficientNet 甚至 Vision Transformer很多核心思想都能在这个小网络上找到源头。1.3 工具选型与环境准备这个项目我使用的是 PyTorch 框架版本 2.x 都可以。选择 PyTorch 主要是因为它的动态图机制对新手非常友好调试时可以直接打印中间张量的形状非常直观。如果你用的是其他主流深度学习框架核心思路也完全一样只是代码写法有差异。我建议初学者先用 PyTorch 把这套逻辑跑通后面再拓展其他框架时迁移成本会低很多。环境方面只需要安装 Python 和 torch再加一个 torchvision 就能跑通。torchvision 是必须的因为加载 MNIST 数据集时需要用torchvision.datasets.MNIST它会自动下载并解析原始数据。如果下载速度慢可以手动把数据下载后放到对应目录或者配置镜像源。这里我推荐用 conda 创建虚拟环境避免污染系统 Pythonconda create -n lenet python3.9 conda activate lenet pip install torch torchvision我没有指定 CUDA 版本因为 MNIST 这个项目用 CPU 训练就够了。LeNet5 每一轮训练大概十几秒到几十秒完全没有必要折腾显卡驱动。如果后续要切换到 CIFAR-10 或者更大的数据集再装 GPU 版也不迟。注意torch 和 torchvision 的版本必须匹配否则导入时会报错。建议在干净的虚拟环境里直接安装避免和系统里的其他包冲突。这个坑看起来小但很容易在项目刚开始时卡住你。2. LeNet5 网络结构拆解每一层到底在学什么2.1 从输入到输出完整的结构图LeNet5 的完整输入并不是 MNIST 原始的 28×28而是 32×32。这一点很多人会忽略。为什么是 32×32因为网络里的第一个卷积核是 5×5stride 为 1没有 padding。如果输入是 28×28经过第一层卷积后会变成 24×24再往后尺寸缩得更快到最后一层卷积时特征图可能已经无法正确处理。标准做法是先把图片 padding 到 32×32让网络能够按照原始设计完成尺寸变换。整个网络流程可以概括为输入1 通道 32×32 灰度图C16 个 5×5 卷积核输出 6 通道 28×28 特征图S22×2 平均池化stride2输出 6 通道 14×14C316 个 5×5 卷积核输出 16 通道 10×10S42×2 平均池化输出 16 通道 5×5C5120 个 5×5 卷积核输出 120 通道 1×1F6全连接层输出 84 维Output全连接层输出 10 维分类得分这里 C5 虽然是卷积层但它的卷积核尺寸恰好等于输入特征图的尺寸卷积后得到 1×1所以实际上起到了全连接层的作用。这是 LeNet5 设计上很有意思的一个点用卷积实现全局特征综合避免了直接展平可能带来的维度计算混乱。理解这一点之后再看其他 CNN 网络的“全局池化 全连接”结构会更容易融会贯通。2.2 各层参数计算与感受野分析计算每一层的参数量是理解网络最直接的方式。我们来完整算一遍。C1 层输入 1 通道输出 6 通道卷积核 5×5。每个输出特征图带一个偏置参数量为1×5×5×6 6 156。S2 层是池化层没有可学习参数。原始 LeNet5 的池化层其实带有可学习的加权系数但在现代简化版里基本都用平均池化或最大池化替代。池化本身只做下采样不需要训练权重也不会明显影响最终精度。C3 层输入 6 通道输出 16 通道卷积核 5×5。按全连接的方式计算参数量为6×5×5×16 16 2416。这里有一个细节原始 LeNet5 中 C3 并没有对所有输入通道做全连接卷积而是采用了部分连接策略也就是某些输出通道只和部分输入通道相连目的是打破特征图之间的对称性并减少计算量。在现代实现里我们直接使用全连接卷积效果完全不差甚至更好所以不必刻意复刻那种非对称连接。S4 层池化层无参数。C5 层输入 16 通道输出 120 通道卷积核 5×5输出 1×1。参数量为16×5×5×120 120 48120。F6 层输入 120输出 84参数量为120×84 84 10164。Output 层输入 84输出 10参数量为84×10 10 850。总参数约为156 2416 48120 10164 850 61706。这个参数量放到今天非常小甚至不如很多模型的一个残差模块多。但它在 MNIST 上能达到约 99% 的准确率说明手写数字识别的特征空间确实很紧凑不需要海量参数就能拟合得很好。从感受野的角度看第一个卷积层每次观察 5×5 的局部区域随着网络加深后面层的每个神经元能看到越来越大的输入范围最终在 C5 层能够覆盖整张输入图像。这种从局部到全局的渐进式特征提取就是 CNN 处理图像的核心逻辑。你在 MNIST 上把这里想明白了以后理解 VGG、ResNet 的感受野计算就会非常轻松。2.3 经典结构与现代实现的差异在复现 LeNet5 时你会发现不同代码版本之间有各种细节差异主要集中在这几方面激活函数原始设计使用 tanh现代实现通常用 ReLU。ReLU 计算更简单训练时梯度更稳定。我实测在 MNIST 上两者差别不大但 ReLU 让网络收敛更容易代码也更贴近现代风格。池化方式原始实现是带学习系数的加权平均池化现代实现有的用平均池化有的用最大池化。MNIST 上最大池化效果也完全可以但我为了贴近原版在下面的代码里使用了平均池化。输出层原始实现用了 RBF 径向基函数现代实现直接用线性全连接层配交叉熵损失。这主要是训练习惯的变化神经网络分类器通常不需要自己额外设计距离度量。输入尺寸很多代码为了省事直接用 28×28 输入然后手动调整网络里的卷积 padding 或全连接维度。这样做也能工作但已经不是标准 LeNet5 了。我的建议是学习阶段尽量按照原版的设计来也就是 32×32 输入、平均池化、ReLU 激活这套组合。既能领会原版的设计意图又能利用现代优化技巧获得更好的训练效果。先理解经典再去做简化或修改才是站稳基础的正确路径。3. 数据读取与预处理细节决定准确率3.1 MNIST 数据集的加载与归一化用 torchvision 加载 MNIST 数据集非常方便核心代码就几行from torchvision import datasets, transforms transform transforms.Compose([ transforms.Pad(2), transforms.ToTensor(), transforms.Normalize(mean(0.1307,), std(0.3081,)) ]) train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform)这段代码里有三个关键操作很多人知道要写但不太明白为什么。这里我展开说一下。第一Pad(2)的作用是把 28×28 的图片四周各补 2 个像素的黑边变成 32×32。MNIST 背景本身就是黑色像素值 0所以补零不会引入任何额外噪声数字仍然居中形状完全不变。这个操作解决的是 LeNet5 标准输入尺寸问题。第二ToTensor()会把 PIL 图片转成张量并将像素值从 0~255 缩放到 0~1 区间。这一步必须做否则不归一化的像素值会让初始梯度太大网络很难收敛。第三Normalize对每个通道做标准化。MNIST 是单通道灰度图所以 mean 和 std 各一个值。0.1307 和 0.3081 是 MNIST 训练集的统计均值与标准差。用它们把数据变换到均值 0、标准差 1 的分布有利于梯度下降时的数值稳定。注意标准化使用的 mean 和 std 必须是训练集的统计量不应该混入测试集。MNIST 比较简单即使写错影响也不大但在真实项目中这是一个很容易犯且代价很高的错误。3.2 尺寸对齐padding 还是 resize我第一次复现时图省事用的是transforms.Resize((32, 32))。准确率也正常但心里总觉得不是原汁原味。后来改成了 padding 方案也就是上面代码里的Pad(2)。这两种方式有什么区别Resize是对图片进行插值缩放会改变像素内容相当于对图像做了变形Pad(2)只是在四周补黑边保留了原始像素。从保持信息的角度看padding 显然更干净。实际效果上MNIST 数字本来就很居中两种方式对准确率的影响很小。但如果你以后要迁移到其他数据集或者复现更复杂的网络预处理方式会和网络设计深度绑定。输入尺寸不对网络结构再漂亮也白搭。所以建议从一开始就养成“先想清楚输入尺寸再写网络”的习惯。我还有一个习惯在训练开始前手动打印一个 batch 的数据统计值确认标准化是否生效import torch images, labels next(iter(train_loader)) print(images.mean(), images.std())理论上应该分别接近 0 和 1。如果偏差很大说明 transform 没有正确应用到数据上这时候就需要先排查预处理流程而不是急着调模型。3.3 DataLoader 配置与数据可视化DataLoader 的配置有几个参数需要注意from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers2) test_loader DataLoader(test_dataset, batch_size256, shuffleFalse, num_workers2)batch_size我选了 64对 LeNet5 来说很合适。如果太大每个 epoch 的梯度更新次数变少收敛速度会变慢如果太小比如 1训练会非常不稳定且容易过拟合。新手可以固定为 64 或 128不用过度纠结。shuffle在训练集设置为 True是为了打乱样本顺序防止模型学到样本排列中的伪规律。测试集不需要打乱因为评估时样本顺序不影响结果。num_workers在 Windows 环境下建议设置为 0否则可能会遇到多进程相关的报错。Linux 或 macOS 可以设为 2 或 4加快数据读取。在正式训练前强烈建议先可视化一批图片。我会把一个小 batch 拼成网格显示出来同时打印对应的标签确认图片和标签是否对齐。如果图片是颠倒的、标签对不上那训练再久也没有意义。这一步只需要一次性的几行代码却能避免很多后期排查时间。数据这一关过了之后就可以放心开始搭建模型了。很多时候模型表现不好不是网络结构的问题而是数据进模型之前就已经埋下了隐患。4. 模型实现与训练全流程从零写出一个可用的 LeNet54.1 PyTorch 代码实现 LeNet5网络定义我严格按照前面说的结构来写每一层都标注了尺寸变化方便对照import torch.nn as nn class LeNet5(nn.Module): def __init__(self, num_classes10): super(LeNet5, self).__init__() self.features nn.Sequential( nn.Conv2d(1, 6, kernel_size5, stride1), # C1: 32x32 - 28x28 nn.ReLU(inplaceTrue), nn.AvgPool2d(kernel_size2, stride2), # S2: 28x28 - 14x14 nn.Conv2d(6, 16, kernel_size5, stride1), # C3: 14x14 - 10x10 nn.ReLU(inplaceTrue), nn.AvgPool2d(kernel_size2, stride2), # S4: 10x10 - 5x5 nn.Conv2d(16, 120, kernel_size5, stride1),# C5: 5x5 - 1x1 nn.ReLU(inplaceTrue) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(120, 84), # F6 nn.ReLU(inplaceTrue), nn.Linear(84, num_classes) # Output ) def forward(self, x): x self.features(x) x self.classifier(x) return x这里Flatten()会把 C5 输出的(batch_size, 120, 1, 1)展平成(batch_size, 120)再送入全连接层。如果你喜欢更显式的写法也可以直接调用x.view(x.size(0), -1)效果等价。关于AvgPool2d和MaxPool2d的选择原版 LeNet5 用的是平均池化所以我这里保留了。如果你好奇最大池化的效果也可以替换成nn.MaxPool2d(kernel_size2, stride2)在 MNIST 上通常也能得到可接受的结果只是梯度传导方式略有不同。这种小实验非常适合用来感受不同操作对训练动态的影响。4.2 训练脚本损失函数、优化器与训练循环损失函数我直接使用交叉熵损失。PyTorch 的CrossEntropyLoss已经内部整合了 Softmax所以模型的最后一层不需要额外加 Softmax直接输出 10 个原始得分即可。这一点经常有人搞错在输出层先加了 Softmax再用交叉熵虽然也能跑但梯度计算方式会受到影响不如直接用标准写法。优化器我选择了 Adam学习率 0.001。经典 LeNet5 用的是 SGD 加动量但 Adam 在 MNIST 上的调参成本最低对新手非常友好。我也对比过 SGD 加 momentum如果学习率调到 0.01训练 15 个 epoch 左右同样能收敛最终准确率甚至略高但前期收敛比 Adam 慢。这里给出 Adam 版本足够稳定。训练一个 epoch 的函数可以这样写import torch import torch.nn as nn from torch.optim import Adam model LeNet5() criterion nn.CrossEntropyLoss() optimizer Adam(model.parameters(), lr0.001) def train_one_epoch(model, train_loader, criterion, optimizer, device): model.train() total_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy循环里还有几个值得一提的细节。optimizer.zero_grad()必须在每次反向传播前清空上一步的梯度否则梯度会累积。loss.item()取的是 Python 标量可以用来累加计算平均损失。torch.max(outputs, 1)返回每行最大值的索引那就是模型预测的类别。在训练循环中我习惯在每个 epoch 结束后打印一次训练损失和准确率。如果你想要更直观的进度可以用 tqdm 包装 data loader按 batch 输出进度条。但 LeNet5 跑得很快打印太多反而刷屏所以我在正式代码里简洁了一点。另一个非常重要的习惯是设置随机种子。深度学习训练中有很多随机性如果不固定种子每次结果都会有浮动。为了实验可复现我会在训练前设置torch.manual_seed(42)如果有 CUDA 也一并设置。虽然 MNIST 这种任务即使不设置种子也能得到好结果但从一开始就养成可复现的习惯对后续做研究和比赛很有帮助。4.3 模型评估准确率与混淆矩阵训练完成后在测试集上评估模型。这里有一个很多新手会踩的坑忘记切到评估模式。虽然 LeNet5 没有 Dropout 和 BatchNormtrain和eval模式差别不大但一旦换了更深的网络漏写model.eval()会导致结果完全不可靠。同时评估阶段不需要计算梯度用torch.no_grad()包起来可以显著减少内存和耗时。def evaluate(model, test_loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) return correct / total在 MNIST 测试集上LeNet5 的准确率通常落在 98.5%~99.2% 之间。如果训练到第 10 个 epoch往往能到 98.8% 以上偶尔调整一下随机种子或训练更久能靠近 99.3%但再往上提升空间不大因为测试集里本身就有一些非常模糊、人类都难以辨认的图片。光看准确率还不够我建议再多看一个指标混淆矩阵。它比单个数字更能反映模型在哪些类别上容易出错也更容易定位问题。from sklearn.metrics import confusion_matrix all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(cm)我在多次实验里发现最容易混淆的组合通常是 4 和 97 和 9以及 3 和 8。这些数字在视觉上确实形态相近尤其是快速手写时人眼也不一定能分辨清楚。如果你发现某个数字的准确率特别低比如 5那就要回头检查数据预处理是否对这类图片造成了扭曲或者考虑是否需要对类别做平衡。混淆矩阵能让你看到错误模式而不是只盯着一个孤零零的准确率数字。5. 常见问题与排查技巧实录5.1 模型不收敛的排查清单在实际训练 LeNet5 的过程中我总结了一套排查“不收敛”问题的清单按优先级排列希望能帮你快速定位。先检查输入数据范围。如果 transform 里漏写了ToTensor或Normalize模型看到的是一张像素值 0~255 的图初始梯度容易爆炸损失可能直接变成 NaN。这是最常见的原因。解决方法是打印一个 batch 的images.min()和images.max()确认数据确实在合理范围内。再检查损失函数是否和最后的分类头匹配。多分类任务应该用CrossEntropyLoss并且模型输出层不需要手动加 Softmax。如果你用了BCELoss就会得到错误的结果如果输出是 10 类但标签不是 0~9也会有奇异行为。然后是学习率。Adam 的默认学习率 0.001 对 LeNet5 来说是黄金值。如果学习率设为 0.1 以上损失曲线会震荡甚至发散如果设为 1e-5收敛速度会慢到怀疑人生。可以打印前几个 batch 的 loss 变化确认是否在下降。最后检查shuffle。训练 DataLoader 里如果忘记shuffleTrue模型可能会学到样本顺序中的伪规律训练集表现高但测试集表现差。MNIST 数据本身是按类别排序的不 shuffle 会让 0 类图片连续出现很多个 epoch虽然最终也能收敛但训练过程会很不稳定。5.2 维度不匹配的错误处理LeNet5 的结构虽然简单但还是容易在 C5 到全连接层之间出现维度问题。最常见的报错是size mismatch原因多半是输入图片没有 pad 到 32×32。如果你把 28×28 图片直接输进去C1 输出 24×24S2 输出 12×12C3 输出 8×8S4 输出 4×4这时候 C5 的 5×5 卷积就无法在 4×4 上滑动直接报错。遇到这种问题先在网络定义里加一行测试代码方便定位是哪一层出了问题x torch.randn(1, 1, 32, 32) print(model.features[0](x).shape) print(model.features[2](x).shape) print(model.features[4](x).shape) print(model.features[6](x).shape)这种“打印中间张量形状”的调试方式是所有深度学习调试手段里最直接、最有效的。不要靠猜直接看每一层的输出 shape几十秒就能找出尺寸错误的具体位置。另一个容易忽略的问题是Flatten的位置。如果 Flatten 写在全连接之前但特征图不是 1×1展平后的维度可能不是 120而是更大的数字。比如你调整了 C5 的 padding 或者输入尺寸C5 输出变成 3×3Flatten 后就是 120×91080再踩进 Linear 层就会报 mismatch。要避免这个问题核心是保证 C5 的输出严格是 1×1这样才能让展平后的维度保持 120。5.3 过拟合与欠拟合的平衡MNIST 简单LeNet5 参数也少过拟合风险不高但并不是完全没有。尤其是如果你把 batch size 设成 1然后训练几百个 epoch模型确实会把训练集“背下来”测试准确率反而不升反降。判断过拟合的方式很简单看训练集准确率和测试集准确率的差距。如果训练集已经接近 100%但测试集准确率只有 95% 或者更低那就说明模型泛化出了问题。这时候可以尝试三个方向增加数据多样性。对训练集做轻微平移、旋转或加入少量噪声让模型见得更多。MNIST 本身不够多变平移两个像素是成本最低且有效的增强方式。降低模型容量或加入正则化。可以在分类器部分加一层nn.Dropout(p0.5)对抑制过拟合有帮助但在这个任务上效果不算明显。早停。训练到第 5 个 epoch 时测试准确率已经不再提升就不要再盲目加 epoch。可以保存验证集上表现最好的模型用那个权重做最终测试而不是用最后一个 epoch 的权重。相反如果测试准确率卡在 60% 左右那多半不是过拟合而是网络结构或预处理出了问题。先换回最简单的配置确认输入是 32×32、数据归一化正确、损失函数匹配。很多时候问题就藏在这几个基础环节里。5.4 训练速度太慢怎么办在 CPU 上训练 LeNet5 其实非常快。我用一台普通笔记本每个 epoch 大概十几秒10 个 epoch 也就三五分钟。如果你感觉速度慢得离谱先看这几件事第一测试评估阶段是否没有包torch.no_grad()。如果测试时还在计算梯度消耗的时间和内存都会增加。加上之后速度能快一倍以上。第二num_workers设置是否过高。Windows 下num_workers0是最稳定的选择2 或 4 虽然理论上能加速但会产生进程切换和通信开销在小数据集上反而可能更慢。第三batch size 是否太小。batch size 为 1 时每次迭代只处理一张图CPU 利用率很低效率极差。调到 64 或 128 之后向量化计算能充分发挥硬件性能。还有一个不太起眼的点数据集的存储位置。如果数据放在很慢的机械硬盘上每次 epoch 都要重新读取数据加载会成为瓶颈。可以把数据集放到内存或固态硬盘上一般能明显感觉到训练过程更顺畅。6. 个人经验与后续扩展方向6.1 从 LeNet5 学到的通用方法论做完这个项目之后我最大的收获不是记住了一个网络结构而是建立了一套调试深度学习模型的“感觉”。第一个感觉是对卷积层尺寸变化的敏感。无论哪个网络只要告诉我卷积核大小、步长、padding我都能立刻算出输出尺寸。这个熟练程度来自反复推导 LeNet5 每一层的输出形状。只要把最基础的计算弄熟后面看任何 CNN 结构都会快很多。第二个感觉是理解“特征提取 分类器”的分层思想。LeNet5 前面的卷积层是在做特征提取后面的全连接层是在做分类。现代很多模型仍然是这样的思路只不过特征提取器从简单的卷积栈变成了非常深的残差网络甚至 Transformer但整体框架一脉相承。你完全可以把 LeNet5 的features部分替换成任意一个预训练特征提取器然后只训练后面的分类头这就是最简单的迁移学习。第三个感觉是小模型也能解决很多问题。很多人一上来就想上大模型其实在 MNIST 这种简单任务里LeNet5 的六万参数已经绰绰有余。先跑通一个简单 baseline再根据实际需求逐步增加复杂度是工程上非常高效的策略。很多所谓的“模型效果不好”其实是数据或流程的问题而不是模型容量不够。6.2 后续改进方向从 MNIST 到更复杂的任务做完 LeNet5 之后你可以沿着几个方向继续深入把这次学到的能力迁移到更广泛的场景。一个方向是结构实验。把平均池化换成最大池化在两卷基层之间加入 BatchNorm或者在全连接层里加 Dropout观察训练速度和最终准确率的变化。通过这些改动你能直观地感受到每一个模块对网络行为的影响而不仅是停留在纸面上的理解。另一个方向是更换数据集。MNIST 太简单你可以试试 Fashion-MNIST 或者 CIFAR-10。LeNet5 在 MNIST 上能到 99%但在 CIFAR-10 上可能只有 60% 左右这能拆掉你对网络结构的“迷信”让你意识到数据分布对模型效果的影响有多大。你还可以尝试用 LeNet5 做特征抽取器把卷积层输出的特征送到 SVM 或随机森林里做分类。早期很多人用这种方法进行手写数字识别。虽然现代端到端训练已经普及但亲手做一次特征抽取和分类器分离能让你对表示学习有更直观的认识。我后来把 LeNet5 的最后一个卷积层从 5×5 改成 3×3在全连接层之间加了 Dropout在 Fashion-MNIST 上也能得到很不错的分类效果。这说明 LeNet5 的骨架并不局限于 MNIST它的核心设计理念完全可以迁移到其他图像任务上。最后说一点个人体会新手阶段很容易把注意力放在“刷高准确率”上但 LeNet5 这个项目的真正价值不在于那个数字而在于把数据、模型、训练、评估、排错这整条链路完整地走通一遍。后面无论你去做目标检测、图像分割还是自然语言处理这些基础能力都会反复用到。所以别急着追求花哨的网络结构先把 LeNet5 吃透你的深度学习地基就牢了。
阅读完成 · 觉得有帮助?