实验概述

项目

内容

实验名称

EMNIST Letters 字母识别器

实验日期

2026-03-31

目标数据集

EMNIST Letters (26类A-Z字母)

训练设备

NVIDIA GPU (自动检测)

硬件环境

项目

配置

CPU

Intel处理器 (训练控制)

GPU

NVIDIA CUDA GPU (模型训练)

内存

8GB+ RAM

存储

10GB+ 可用空间

软件环境

项目

版本/配置

操作系统

Windows 11 Pro

Python

3.8+

PyTorch

2.0+

torchvision

0.15+

CUDA

11.8+ (GPU加速)

PyTorch GPU版本安装命令

 # 使用pip安装PyTorch GPU版本
 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
 ​
 # 验证GPU是否可用
 python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}'); print(f'GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else None}')"

数据集说明

EMNIST Letters 26类字母标签对照表

标签(原始)

标签(转换后)

字母

1

0

A

2

1

B

3

2

C

4

3

D

5

4

E

6

5

F

7

6

G

8

7

H

9

8

I

10

9

J

11

10

K

12

11

L

13

12

M

14

13

N

15

14

O

16

15

P

17

16

Q

18

17

R

19

18

S

20

19

T

21

20

U

22

21

V

23

22

W

24

23

X

25

24

Y

26

25

Z

注意: EMNIST Letters原始标签为1-26,代码中通过target_transform=lambda t: t - 1转换为0-25以适配CrossEntropyLoss

数据集属性

属性

图像尺寸

28x28像素

图像通道

1 (灰度)

训练集样本数

124,800

测试集样本数

20,800

类别数量

26 (A-Z)

总类别数

26

数据预处理代码

 from torchvision import transforms
 ​
 # 训练集数据增强
 train_transform = transforms.Compose([
     transforms.RandomCrop(28, padding=2),   # 随机裁剪
     transforms.RandomRotation(10),          # 随机旋转(-10°到10°)
     transforms.ToTensor(),                   # 转换为张量
     transforms.Normalize(mean=(0.1307,), std=(0.3081,))  # 标准化
 ])
 ​
 # 测试集/验证集预处理
 test_transform = transforms.Compose([
     transforms.ToTensor(),
     transforms.Normalize(mean=(0.1307,), std=(0.3081,))
 ])

模型架构

网络结构描述

DeepEMNISTNet - 深度卷积神经网络

层类型

配置

输出尺寸

Conv Block 1

Conv(1->32) x2 + BN + ReLU + MaxPool

28x28 -> 14x14

Conv Block 2

Conv(32->64) x2 + BN + ReLU + MaxPool

14x14 -> 7x7

Conv Block 3

Conv(64->128) x2 + BN + ReLU + MaxPool

7x7 -> 3x3

Flatten

-

12833 = 1152

FC Block

Linear(1152->256) + ReLU + Dropout(0.5)

256

FC Block

Linear(256->128) + ReLU + Dropout(0.5)

128

Output

Linear(128->26)

26

模型参数统计

组件

参数数量

Conv Block 1

32x1x3x3 + 32 + 32x32x3x3 + 32 = 1,664

Conv Block 2

32x64x3x3 + 64 + 64x64x3x3 + 64 = 37,504

Conv Block 3

64x128x3x3 + 128 + 128x128x3x3 + 128 = 148,864

FC Block

1152x256 + 256 + 256x128 + 128 + 128x26 + 26 = 329,034

总参数量

约 517,000+

完整模型代码

 import torch.nn as nn
 ​
 class DeepEMNISTNet(nn.Module):
     """
     深度CNN模型:5层卷积块 + 3层全连接
     Conv Block 1: 28x28 -> 14x14 (Conv-BN-ReLU x2 + Pool)
     Conv Block 2: 14x14 -> 7x7 (Conv-BN-ReLU x2 + Pool)
     Conv Block 3: 7x7 -> 3x3 (Conv-BN-ReLU x2 + Pool)
     FC: 128*3*3 -> 256 -> 128 -> 26
     Dropout 0.5
     """
 ​
     def __init__(self, num_classes=26):
         super(DeepEMNISTNet, self).__init__()
 ​
         # Conv Block 1: 28x28 -> 14x14
         self.conv_block1 = nn.Sequential(
             nn.Conv2d(1, 32, kernel_size=3, padding=1),
             nn.BatchNorm2d(32),
             nn.ReLU(inplace=True),
             nn.Conv2d(32, 32, kernel_size=3, padding=1),
             nn.BatchNorm2d(32),
             nn.ReLU(inplace=True),
             nn.MaxPool2d(kernel_size=2, stride=2)
         )
 ​
         # Conv Block 2: 14x14 -> 7x7
         self.conv_block2 = nn.Sequential(
             nn.Conv2d(32, 64, kernel_size=3, padding=1),
             nn.BatchNorm2d(64),
             nn.ReLU(inplace=True),
             nn.Conv2d(64, 64, kernel_size=3, padding=1),
             nn.BatchNorm2d(64),
             nn.ReLU(inplace=True),
             nn.MaxPool2d(kernel_size=2, stride=2)
         )
 ​
         # Conv Block 3: 7x7 -> 3x3
         self.conv_block3 = nn.Sequential(
             nn.Conv2d(64, 128, kernel_size=3, padding=1),
             nn.BatchNorm2d(128),
             nn.ReLU(inplace=True),
             nn.Conv2d(128, 128, kernel_size=3, padding=1),
             nn.BatchNorm2d(128),
             nn.ReLU(inplace=True),
             nn.MaxPool2d(kernel_size=2, stride=2)
         )
 ​
         # 全连接层: 128*3*3 -> 256 -> 128 -> 26
         self.fc_block = nn.Sequential(
             nn.Flatten(),
             nn.Linear(128 * 3 * 3, 256),
             nn.ReLU(inplace=True),
             nn.Dropout(0.5),
             nn.Linear(256, 128),
             nn.ReLU(inplace=True),
             nn.Dropout(0.5),
             nn.Linear(128, num_classes)
         )
 ​
     def forward(self, x):
         x = self.conv_block1(x)
         x = self.conv_block2(x)
         x = self.conv_block3(x)
         x = self.fc_block(x)
         return x

训练配置

超参数表格

超参数

说明

批量大小 (batch_size)

128

每批次训练样本数

训练轮数 (num_epochs)

15

完整数据集训练次数

学习率 (learning_rate)

0.001

Adam优化器学习率

优化器

Adam

自适应矩估计优化器

损失函数

CrossEntropyLoss

交叉熵损失函数

Dropout

0.5

防止过拟合

权重衰减

0 (默认)

L2正则化系数

数据增强配置

增强方法

参数

说明

RandomCrop

padding=2

随机裁剪回28x28

RandomRotation

degrees=10

随机旋转正负10度

Normalize

mean=0.1307, std=0.3081

MNIST标准化参数

训练过程

各Epoch结果表格

Epoch

Train Loss

Train Acc

Test Loss

Test Acc

Time(s)

Best

1

0.7936

75.15%

0.2808

90.91%

53.0

*

2

0.3733

88.46%

0.1977

93.61%

68.7

*

3

0.3143

90.55%

0.1797

94.12%

59.0

*

4

0.2878

91.28%

0.1818

93.96%

33.1

5

0.2646

92.09%

0.1758

94.24%

33.2

*

6

0.2493

92.60%

0.1663

94.38%

33.8

*

7

0.2389

92.80%

0.1605

94.72%

35.2

*

8

0.2260

93.17%

0.1667

94.59%

33.2

9

0.2141

93.49%

0.1652

94.77%

33.2

*

10

0.2093

93.68%

0.1592

94.86%

33.1

*

11

0.1976

93.87%

0.1508

94.96%

33.3

*

12

0.1925

94.07%

0.1564

95.00%

33.5

*

13

0.1865

94.20%

0.1499

94.90%

34.1

14

0.1828

94.25%

0.1480

95.19%

34.2

*

15

0.1776

94.38%

0.1469

95.16%

33.8

注: * 表示该Epoch刷新最佳测试准确率

关键指标

指标

最佳测试准确率

95.19% (Epoch 14)

最终训练准确率

94.38%

最终测试准确率

95.16%

最低测试Loss

0.1469

总训练时间

约 585 秒 (9分45秒)

平均每Epoch时间

约 39 秒

训练曲线分析

  1. 收敛速度: 模型在前3个Epoch快速收敛,测试准确率从90.91%提升至94.12%

  2. 稳定期: Epoch 4-15期间,模型进入稳定训练阶段,测试准确率在93.96%-95.19%之间波动

  3. 过拟合迹象: 训练准确率(94.38%)与测试准确率(95.16%)接近,未出现过拟合现象

  4. Loss下降: 测试Loss从0.2808持续下降至0.1469,表明模型持续学习

GPU加速效果

本实验使用NVIDIA GPU进行训练,GPU加速效果显著:

  • GPU型号: CUDA兼容GPU (自动检测)

  • 训练设备: CUDA Device

  • 数据迁移: 训练过程中自动完成CPU-GPU数据迁移

由于本实验未进行CPU对照实验,暂无CPU vs GPU加速比数据。典型GPU加速效果:

  • GPU训练速度通常比CPU快 10-50倍

  • 大批量训练时GPU加速效果更为明显

完整代码

 """
 EMNIST Letters Classifier Training Script
 Task 1 of EMNIST Letters experiment
 """
 ​
 import os
 import time
 import logging
 from datetime import datetime
 ​
 import torch
 import torch.nn as nn
 import torch.optim as optim
 from torch.utils.data import DataLoader
 from torchvision import transforms
 from torchvision.datasets import EMNIST
 ​
 ​
 def get_device():
     """自动检测并选择最佳计算设备"""
     if torch.cuda.is_available():
         device = torch.device("cuda")
         print(f"[GPU] 使用CUDA: {torch.cuda.get_device_name(0)}")
     elif torch.backends.mps.is_available():
         device = torch.device("mps")
         print("[GPU] 使用Apple MPS")
     else:
         device = torch.device("cpu")
         print("[CPU] 使用CPU")
     return device
 ​
 ​
 def setup_logger(log_file):
     """配置日志:同时输出到文件和控制台"""
     logger = logging.getLogger(__name__)
     logger.setLevel(logging.INFO)
 ​
     # 清除已有的handlers
     logger.handlers.clear()
 ​
     # 文件handler
     file_handler = logging.FileHandler(log_file, encoding='utf-8')
     file_handler.setLevel(logging.INFO)
     file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
     file_handler.setFormatter(file_formatter)
 ​
     # 控制台handler
     console_handler = logging.StreamHandler()
     console_handler.setLevel(logging.INFO)
     console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
     console_handler.setFormatter(console_formatter)
 ​
     logger.addHandler(file_handler)
     logger.addHandler(console_handler)
 ​
     return logger
 ​
 ​
 class DeepEMNISTNet(nn.Module):
     """
     深度CNN模型:5层卷积块 + 3层全连接
     Conv Block 1: 28x28 -> 14x14 (Conv-BN-ReLU x2 + Pool)
     Conv Block 2: 14x14 -> 7x7 (Conv-BN-ReLU x2 + Pool)
     Conv Block 3: 7x7 -> 3x3 (Conv-BN-ReLU x2 + Pool)
     FC: 128*3*3 -> 256 -> 128 -> 26
     Dropout 0.5
     """
 ​
     def __init__(self, num_classes=26):
         super(DeepEMNISTNet, self).__init__()
 ​
         # Conv Block 1: 28x28 -> 14x14
         self.conv_block1 = nn.Sequential(
             nn.Conv2d(1, 32, kernel_size=3, padding=1),
             nn.BatchNorm2d(32),
             nn.ReLU(inplace=True),
             nn.Conv2d(32, 32, kernel_size=3, padding=1),
             nn.BatchNorm2d(32),
             nn.ReLU(inplace=True),
             nn.MaxPool2d(kernel_size=2, stride=2)
         )
 ​
         # Conv Block 2: 14x14 -> 7x7
         self.conv_block2 = nn.Sequential(
             nn.Conv2d(32, 64, kernel_size=3, padding=1),
             nn.BatchNorm2d(64),
             nn.ReLU(inplace=True),
             nn.Conv2d(64, 64, kernel_size=3, padding=1),
             nn.BatchNorm2d(64),
             nn.ReLU(inplace=True),
             nn.MaxPool2d(kernel_size=2, stride=2)
         )
 ​
         # Conv Block 3: 7x7 -> 3x3
         self.conv_block3 = nn.Sequential(
             nn.Conv2d(64, 128, kernel_size=3, padding=1),
             nn.BatchNorm2d(128),
             nn.ReLU(inplace=True),
             nn.Conv2d(128, 128, kernel_size=3, padding=1),
             nn.BatchNorm2d(128),
             nn.ReLU(inplace=True),
             nn.MaxPool2d(kernel_size=2, stride=2)
         )
 ​
         # 全连接层: 128*3*3 -> 256 -> 128 -> 26
         self.fc_block = nn.Sequential(
             nn.Flatten(),
             nn.Linear(128 * 3 * 3, 256),
             nn.ReLU(inplace=True),
             nn.Dropout(0.5),
             nn.Linear(256, 128),
             nn.ReLU(inplace=True),
             nn.Dropout(0.5),
             nn.Linear(128, num_classes)
         )
 ​
     def forward(self, x):
         x = self.conv_block1(x)
         x = self.conv_block2(x)
         x = self.conv_block3(x)
         x = self.fc_block(x)
         return x
 ​
 ​
 def train_one_epoch(model, train_loader, criterion, optimizer, device, epoch):
     """训练一个epoch,带GPU数据迁移"""
     model.train()
     running_loss = 0.0
     correct = 0
     total = 0
 ​
     for batch_idx, (data, target) in enumerate(train_loader):
         # GPU数据迁移
         data, target = data.to(device), target.to(device)
 ​
         optimizer.zero_grad()
         outputs = model(data)
         loss = criterion(outputs, target)
         loss.backward()
         optimizer.step()
 ​
         running_loss += loss.item()
         _, predicted = outputs.max(1)
         total += target.size(0)
         correct += predicted.eq(target).sum().item()
 ​
         if (batch_idx + 1) % 100 == 0:
             print(f'  Epoch {epoch} - Batch {batch_idx + 1}/{len(train_loader)}: '
                   f'Loss={loss.item():.4f}, Acc={100.*correct/total:.2f}%')
 ​
     epoch_loss = running_loss / len(train_loader)
     epoch_acc = 100. * correct / total
     return epoch_loss, epoch_acc
 ​
 ​
 def evaluate(model, test_loader, criterion, device):
     """评估函数 - 带GPU推理"""
     model.eval()
     test_loss = 0.0
     correct = 0
     total = 0
 ​
     with torch.no_grad():
         for data, target in test_loader:
             # GPU推理
             data, target = data.to(device), target.to(device)
             outputs = model(data)
             test_loss += criterion(outputs, target).item()
             _, predicted = outputs.max(1)
             total += target.size(0)
             correct += predicted.eq(target).sum().item()
 ​
     test_loss = test_loss / len(test_loader)
     test_acc = 100. * correct / total
     return test_loss, test_acc
 ​
 ​
 def main():
     # 生成日志文件名
     timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
     log_file = f'training_log_emnist_{timestamp}.txt'
 ​
     # 配置日志
     logger = setup_logger(log_file)
     logger.info("=" * 60)
     logger.info("EMNIST Letters 分类器训练开始")
     logger.info("=" * 60)
 ​
     # 设备选择
     device = get_device()
     logger.info(f"使用设备: {device}")
 ​
     # 数据增强transform
     train_transform = transforms.Compose([
         transforms.RandomCrop(28, padding=2),
         transforms.RandomRotation(10),
         transforms.ToTensor(),
         transforms.Normalize(mean=(0.1307,), std=(0.3081,))
     ])
 ​
     test_transform = transforms.Compose([
         transforms.ToTensor(),
         transforms.Normalize(mean=(0.1307,), std=(0.3081,))
     ])
 ​
     # 加载EMNIST Letters数据集
     # EMNIST Letters 标签为 1-26 (A-Z),需要转换为 0-25 以适配 CrossEntropyLoss
     logger.info("加载EMNIST Letters数据集...")
     train_dataset = EMNIST(
         root='./data',
         split='letters',
         train=True,
         transform=train_transform,
         target_transform=lambda t: t - 1,  # 标签从 1-26 转为 0-25
         download=True
     )
 ​
     test_dataset = EMNIST(
         root='./data',
         split='letters',
         train=False,
         transform=test_transform,
         target_transform=lambda t: t - 1,  # 标签从 1-26 转为 0-25
         download=True
     )
 ​
     logger.info(f"训练集大小: {len(train_dataset)}")
     logger.info(f"测试集大小: {len(test_dataset)}")
     logger.info(f"类别数量: 26 (A-Z)")
 ​
     # DataLoader
     # Windows系统设置 num_workers=0, pin_memory=True
     train_loader = DataLoader(
         train_dataset,
         batch_size=128,
         shuffle=True,
         num_workers=0,
         pin_memory=True
     )
 ​
     test_loader = DataLoader(
         test_dataset,
         batch_size=128,
         shuffle=False,
         num_workers=0,
         pin_memory=True
     )
 ​
     # 初始化模型
     model = DeepEMNISTNet(num_classes=26).to(device)
     logger.info(f"模型结构:\n{model}")
 ​
     # 损失函数和优化器
     criterion = nn.CrossEntropyLoss()
     optimizer = optim.Adam(model.parameters(), lr=0.001)
 ​
     # 训练15个epoch
     num_epochs = 15
     best_acc = 0.0
 ​
     logger.info(f"\n开始训练: {num_epochs} epochs, batch_size=128")
     logger.info("-" * 60)
 ​
     for epoch in range(1, num_epochs + 1):
         epoch_start_time = time.time()
 ​
         # 训练
         train_loss, train_acc = train_one_epoch(
             model, train_loader, criterion, optimizer, device, epoch
         )
 ​
         # 评估
         test_loss, test_acc = evaluate(
             model, test_loader, criterion, device
         )
 ​
         epoch_time = time.time() - epoch_start_time
 ​
         logger.info(
             f"Epoch {epoch}/{num_epochs} - "
             f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% - "
             f"Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}% - "
             f"Time: {epoch_time:.1f}s"
         )
 ​
         # 保存最佳模型
         if test_acc > best_acc:
             best_acc = test_acc
             torch.save(model.state_dict(), 'emnist_best_model.pth')
             logger.info(f"*** 新最佳模型已保存! Test Acc: {best_acc:.2f}% ***")
 ​
     logger.info("-" * 60)
     logger.info(f"训练完成! 最佳测试准确率: {best_acc:.2f}%")
     logger.info(f"模型已保存到: emnist_best_model.pth")
     logger.info(f"日志已保存到: {log_file}")
     logger.info("=" * 60)
 ​
 ​
 if __name__ == '__main__':
     main()

结论

实验总结

指标

结果

最佳测试准确率

95.19%

最终测试准确率

95.16%

训练集准确率

94.38%

总训练时间

585秒 (约9分45秒)

模型大小

~2MB (约517K参数)

关键发现

  1. 高准确率: 模型在EMNIST Letters数据集上达到了95.19%的测试准确率,表明深度CNN架构对字母识别任务非常有效

  2. 快速收敛: 模型在前3个Epoch快速收敛,后续保持稳定训练状态

  3. 无过拟合: 训练准确率与测试准确率接近,表明Dropout和数据增强有效防止了过拟合

  4. GPU加速: 使用NVIDIA GPU显著加速了训练过程

改进建议

  1. 使用更深的网络结构(如ResNet)可能进一步提升准确率

  2. 尝试学习率衰减策略

  3. 增加训练Epoch数或使用早停法

  4. 尝试更强的数据增强策略

输出文件清单

文件名

说明

emnist_letters_classifier.py

完整训练代码

training_log_emnist_20260331_133449.txt

训练日志

emnist_best_model.pth

最佳模型权重

EMNIST_Letters_实验报告.md

本实验报告

./data/

EMNIST数据集缓存目录