1. 手写数字识别与CNN基础

手写数字识别是计算机视觉领域的经典入门项目,相当于深度学习的"Hello World"。想象一下,如果能让计算机自动识别银行支票上的金额或者快递单上的邮政编码,这背后离不开手写数字识别技术的支持。而卷积神经网络(CNN)正是处理这类图像识别任务的利器。

我第一次接触这个项目时,发现很多教程要么过于理论化,要么代码片段不完整。这里我会用最直白的语言,带你从零实现一个准确率98%以上的识别系统。我们使用的MNIST数据集包含6万张手写数字图片,每张都是28x28像素的灰度图,就像下面这个例子:

import matplotlib.pyplot as plt
sample = train_dataset[0][0].squeeze()
plt.imshow(sample, cmap='gray')
plt.title(f"Label: {train_dataset[0][1]}")
plt.show()

CNN之所以适合图像处理,是因为它的卷积层能自动提取局部特征。比如第一层可能识别笔画边缘,第二层组合这些边缘形成数字部件,最后全连接层完成分类。这模仿了人类视觉系统的工作方式,比传统全连接网络更高效。

2. 数据预处理实战技巧

2.1 数据加载与标准化

PyTorch的torchvision.datasets已经内置了MNIST数据集,下载非常方便。但原始像素值(0-255)直接输入模型效果不好,我们需要进行标准化:

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))  # MNIST专用均值标准差
])

这里有个坑我踩过:Normalize的mean和std如果用默认的0.5,反而会降低最终准确率。MNIST的标准值应该是mean=0.1307,std=0.3081,这是经过大量实验得出的最优值。

2.2 数据增强策略

虽然MNIST数据质量已经很好,但适当的数据增强能进一步提升模型鲁棒性。我推荐加入随机旋转和小幅度平移:

transform_train = transforms.Compose([
    transforms.RandomRotation(10),
    transforms.RandomAffine(0, translate=(0.1, 0.1)),
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

注意测试集不要做增强!否则会干扰评估结果。数据加载器的batch_size设置也有讲究,一般GPU显存8G可以设到128-256,太大容易爆显存。

3. CNN模型搭建详解

3.1 网络结构设计

我们采用经典的两层卷积+两层全连接结构。关键是要计算好每层的张量尺寸变化:

class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1, padding=1)  # 28x28 -> 28x28
        self.conv2 = nn.Conv2d(32, 64, 3, 1, padding=1) # 14x14 -> 14x14
        self.fc1 = nn.Linear(64*7*7, 256)
        self.fc2 = nn.Linear(256, 10)
        
    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))  # 28x28 -> 14x14
        x = F.relu(F.max_pool2d(self.conv2(x), 2))  # 14x14 -> 7x7
        x = x.view(-1, 64*7*7)
        x = F.relu(self.fc1(x))
        return self.fc2(x)

这里有几个设计要点:

  1. 使用3x3小卷积核代替5x5,减少参数量的同时保持感受野
  2. 每层卷积后立即接ReLU激活,增强非线性
  3. 池化层使用2x2窗口,步长2,实现下采样

3.2 参数初始化技巧

模型参数初始化对训练效果影响很大。我习惯用Xavier初始化卷积层,全连接层用Kaiming初始化:

def _initialize_weights(self):
    for m in self.modules():
        if isinstance(m, nn.Conv2d):
            nn.init.xavier_normal_(m.weight)
            if m.bias is not None:
                nn.init.constant_(m.bias, 0)
        elif isinstance(m, nn.Linear):
            nn.init.kaiming_normal_(m.weight)

4. 模型训练与调优

4.1 训练流程配置

训练时我推荐使用Adam优化器,它比SGD更容易收敛。学习率设置为0.001起步,配合余弦退火调度:

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)

训练循环中加入梯度裁剪可以防止梯度爆炸:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

4.2 过拟合应对策略

当训练准确率远高于测试准确率时,说明出现了过拟合。我常用的解决方法:

  1. 增加Dropout层(在全连接层前加dropout=0.5)
  2. 使用L2正则化(weight_decay=1e-4)
  3. 早停机制(当验证集loss连续3轮不下降时停止)
self.dropout = nn.Dropout(0.5)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

5. 模型评估与部署

5.1 准确率评估技巧

除了整体准确率,还应该查看每个类别的单独表现:

class_correct = [0] * 10
class_total = [0] * 10
with torch.no_grad():
    for images, labels in test_loader:
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(len(labels)):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1

for i in range(10):
    print(f'Accuracy of {i}: {100 * class_correct[i]/class_total[i]:.2f}%')

5.2 模型部署实践

训练好的模型可以保存为TorchScript格式,方便生产环境调用:

script_model = torch.jit.script(model)
torch.jit.save(script_model, 'mnist_cnn.pt')

加载模型进行预测时,记得保持相同的预处理流程:

def predict(img):
    transform = transforms.Compose([
        transforms.Resize(28),
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
    img_tensor = transform(img).unsqueeze(0)
    with torch.no_grad():
        output = model(img_tensor)
    return torch.argmax(output).item()

在实际项目中,我还遇到过图片背景色反转的问题(黑底白字 vs 白底黑字)。这时可以在预处理中加入自动阈值处理:

img = ImageOps.invert(img.convert('L')).point(lambda x: 255 if x > 30 else 0)
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐