从MNIST手写数字迁移到EMNIST手写字母的识别之路
前言
深度学习框架:PyTorch
PyTorch是一个开源的深度学习框架,由 Facebook 的人工智能研究团队开发,广泛应用于计算机视觉(CV)、自然语言处理(NLP)等领域。它以灵活性和易用性著称,特别适合研究人员和开发者进行快速原型设计和实验。
💡PyTorch 的模型训练通常包括以下步骤:
-
数据加载:使用 Dataset 和 DataLoader 加载和预处理数据。
-
模型定义:通过继承 torch.nn.Module 构建神经网络。
-
损失函数与优化器:选择合适的损失函数(如交叉熵)和优化器(如 SGD 或 Adam)。
-
训练与验证:通过循环迭代训练模型,并使用验证集评估性能。
-
保存与加载模型:使用 torch.save 和 torch.load 保存和恢复模型。
深度学习模型:CNN
CNN(卷积神经网络),适合用在图像识别和分类中,通过一系列的卷积层、池化层和全连接层来处理数据
在卷积层中,CNN使用一组可学习的滤波器(卷积核)来扫描输入的图像或信号。每个滤波器都能够检测输入中的特定特征,如边缘或颜色斑点。通过这种方式,CNN能够捕捉到图像中的局部特征,并保持这些特征的空间关系。
池化层(也称为下采样层)则用于降低特征的空间尺寸,从而减少参数数量和计算复杂度,同时使特征检测更加鲁棒。全连接层则将学习到的高级特征用于分类或其他任务。
💡CNN的结构和工作原理
一个典型的CNN包含以下几个主要部分:
-
输入层:接收原始数据,如图像的像素值。
-
卷积层:使用多个卷积核提取输入的特征。
-
激活函数:如ReLU,用于引入非线性,使网络能够学习更复杂的特征。
-
池化层:降低特征的空间维度,减少计算量。
-
全连接层:将学习到的特征映射到最终的输出,如分类标签。
-
输出层:输出网络的最终结果,如分类的概率分布。
CNN通过这些层的堆叠,能够从简单到复杂逐渐提取图像的特征。在训练过程中,CNN通过反向传播算法调整卷积核中的权重,以最小化预测结果和真实标签之间的差异。
训练MNIST
❓什么是MNIST
全称:(Modified National Institute of Standards and Technology),是机器学习和深度学习领域最经典的入门数据集,被称为深度学习的“Hello World”。它包含手写数字(0-9) 的灰度图像,广泛用于图像分类算法的训练与测试。
模型定义
首先定义模型的结构,这个结构在训练和推理过程中都是要保持一致的,其中包括模型组件的定义和模型流程定义,代码如下所示:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3, 1)
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.dropout1 = nn.Dropout2d(0.25)
self.fc1 = nn.Linear(9216, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)
x = self.dropout1(x)
x = torch.flatten(x, 1)
x = self.fc1(x)
x = F.relu(x)
x = self.dropout2(x)
x = self.fc2(x)
output = F.log_softmax(x, dim=1)
return output
上面的代码中,首先定义了模型的组件结构,其次forward是模型的流程结构。这是一个经典的“特征提取 + 分类器”双阶段结构,分为两个阶段。
第一阶段:特征提取(卷积神经网络),负责把像素变成“语义特征”。
-
卷积层 1(Conv1):初级特征提取(边缘、线条)
self.conv1 = nn.Conv2d(1, 32, 3, 1)- in_channels=1 : 输入是灰度图(1个通道)
- out_channels=32: 派出32个不同的“侦探”(滤波器)
- kernel_size=3 : 每个侦探拿3x3的放大镜
- stride=1 : 每次移动1个像素
- 输出尺寸计算:(28 - 3 + 1) = 26 -> 输出 (32, 26, 26)
-
卷积层 2:高级特征提取(部件、纹理)
self.conv2 = nn.Conv2d(32, 64, 3, 1)- in_channels=32: 接收上一层的32个特征图
- out_channels=64: 增加到64个侦探,组合更复杂的模式
- 输出尺寸计算:(26 - 3 + 1) = 24 -> 输出 (64, 24, 24)
-
Dropout 层 1:针对卷积特征的正则化
self.dropout1 = nn.Dropout2d(0.25)- 作用:随机“掐断”25%的通道(整张特征图),强迫模型不要把赌注押在某几个特定的特征组合上
- 为什么用 Dropout2d:防止特征图之间产生共适应(Co-adaptation)
-
Dropout 层 2:针对全连接层的正则化
self.dropout2 = nn.Dropout2d(0.5)- 作用:随机“掐断”50%的神经元,因为全连接层参数最多,最容易过拟合
第二阶段:分类器(全连接网络),负责把特征变成具体的数字类别
-
全连接层 1:特征映射与降维
self.fc1 = nn.Linear(9216, 128)- 输入维度 9216 的由来(关键计算)
- 经过 Conv2(64, 24, 24) -> MaxPool(2, 2)
- 输出尺寸:64个通道 * 12 * 12 = 9216
- 输出维度 128:将高维特征压缩成128维的“数字指纹”
-
全连接层 2:最终输出层
self.fc2 = nn.Linear(128, 10)- 输入 128:接收上层的指纹
- 输出 10:对应 0-9 十个数字类别
而这些阶段落实到的流程执行中,需要用到一些额外的手段,如下所示:
relu,激活函数:引入非线性,筛选有效特征max_pool2d,最大池化,模型不再关心像素的精确位置,只关心“有没有”flatten,展平:把三维特征图拉直成一维向量,为了对接全连接层(只能吃一维数据)log_softmax,转换为对数概率
forward函数清晰地展示了数据流(Flow):像素 → 边缘 → 部件 → 抽象特征 → 概率。
❓ 我当时真实的困惑
- 为什么卷积核数量是
32和64?不能是30吗? 9216这个数字是怎么来的?- forward 里,数据尺寸到底是怎么一步步变化的?
这几个是非常典型、也非常值得认真回答的问题。
🔑首先回答第一个问题,可以是 30,但几乎没人这么做。
原因有三个,都是工程现实,不是玄学:
1️⃣ 最重要的是GPU 硬件对齐,因为现代 GPU 的运算单元(CUDA Core / Tensor Core)是按 2 的幂次并行工作的,能刚好对齐显存带宽和线程块(block),如果用 30会出现 padding / waste,计算效率下降,训练变慢。
2️⃣ 经验性容量设计,在第一层提取简单特征(边、角、点),32个通道足够覆盖常见低级模式;第二层组合成复杂结构(弧线、圈、部件),需要更多表示能力,64是实践中验证过的稳定选择
3️⃣ 历史惯性(LeNet 传统),很多教程只是继承这种 翻倍增长 的设计,而不是重新发明
🔑其次是第二个问题,9216 不是玄学,是尺寸推导,是算出来的
✅ 已知前提
- 输入图片:
28 × 28 - 卷积核:
3 × 3 - padding:
0 - stride:
1
✅ 每一层尺寸变化
| 层 | 操作 | 输出尺寸 |
|---|---|---|
| Input | 原始图片 | 1 × 28 × 28 |
| Conv1 | 3×3 卷积,无 padding | 32 × 26 × 26 |
| MaxPool | 2×2 池化 | 32 × 13 × 13 |
| Conv2 | 3×3 卷积 | 64 × 11 × 11 |
| MaxPool | 2×2 池化 | 64 × 5 × 5 |
⚠️ 注意:不同实现可能略有差异,但常见版本最后会再接一个卷积或 padding,使最终特征图为 64 × 12 × 12。
那么最终,9216 的来源就是
64 个通道
× 12 × 12 每个通道的特征图
= 9216
模型训练
整体来说,训练过程就是把一个 batch 的数据送进模型 → 算损失 → 反向传播 → 更新参数 → 记录日志,这是深度学习里“学习”发生的唯一地方
📌 PyTorch 的数据是“流式”的,不是一次性全塞进去
def train(model, device, train_loader, optimizer, epoch, losses):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()更多推荐



所有评论(0)