《手写数字识别实战:从逻辑回归到CNN的进阶之路(完整代码解析)》
## 📚 前言 手写数字识别(MNIST)是机器学习领域的"Hello World"。本文将从最简单的逻辑回归模型出发,逐步实现ANN和CNN,通过**完整代码+可视化训练过程**,带你直观感受不同模型的性能差异。文末附完整代码和数据集! ---
## 🛠️ 环境准备
```python
# 基础库
import torch
import torch.nn as nn
from torchvision import datasets, transforms
# 可视化
import matplotlib.pyplot as plt
%matplotlib inline
# 设置GPU加速
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
📈 模型效果对比
模型 | 最高准确率 | 训练时间(15 epochs) |
---|---|---|
逻辑回归 | 85.85% | ~2分钟 |
ANN | 96.26% | ~2分钟 |
CNN | 98.26% | ~2分钟 |
一、逻辑回归:基础入门
1. 核心代码
class LogisticRegression(nn.Module):
def __init__(self, input_dim, output_dim):
super().__init__()
self.linear = nn.Linear(input_dim, output_dim)
def forward(self, x):
return self.linear(x)
2. 训练过程
Epoch: 15/15 | Iteration: 9500 | Loss: 0.5245 | Accuracy: 85.85%
二、增强版ANN(三隐藏层)
1. 模型结构
class ANNModel(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
# 第一隐藏层:784 → 150
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu1 = nn.ReLU() # 首层使用ReLU快速收敛
# 第二隐藏层:150 → 150(保持维度)
self.fc2 = nn.Linear(hidden_dim, hidden_dim)
self.tanh2 = nn.Tanh() # 中间层使用Tanh增强特征表达
# 第三隐藏层:150 → 150
self.fc3 = nn.Linear(hidden_dim, hidden_dim)
self.elu3 = nn.ELU(alpha=1.0) # 深层使用ELU防止梯度消失
# 输出层:150 → 10(无激活函数)
self.fc4 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
out = self.relu1(self.fc1(x))
out = self.tanh2(self.fc2(out))
out = self.elu3(self.fc3(out))
return self.fc4(out)
关键改进说明:
-
通过堆叠三个隐藏层实现深度特征提取
-
混合激活函数策略:ReLU(快速收敛) + Tanh(特征压缩) + ELU(深层稳定)
-
使用
.view()
保持张量维度一致性
三、专业级CNN实现
class CNNModel(nn.Module):
def __init__(self):
super().__init__()
# 卷积层1:1输入通道 → 16特征图(5x5核)
self.cnn1 = nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=0)
self.relu1 = nn.ReLU()
self.maxpool1 = nn.MaxPool2d(kernel_size=2) # 输出尺寸:12x12
# 卷积层2:16 → 32特征图(5x5核)
self.cnn2 = nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=0)
self.relu2 = nn.ReLU()
self.maxpool2 = nn.MaxPool2d(kernel_size=2) # 输出尺寸:4x4
# 全连接层:32*4*4=512 → 10
self.fc1 = nn.Linear(32*4*4, 10)
def forward(self, x):
# 输入形状:[batch, 1, 28, 28]
out = self.maxpool1(self.relu1(self.cnn1(x))) # → [b,16,12,12]
out = self.maxpool2(self.relu2(self.cnn2(out))) # → [b,32,4,4]
out = out.view(out.size(0), -1) # 展平 → [b,512]
return self.fc1(out)
参数计算原理:
-
卷积输出尺寸 = 【(输入尺寸 - 核尺寸 + 2×padding)/stride】 + 1
-
池化后尺寸 = 【(输入尺寸 - 核尺寸)/stride】 + 1
-
全连接输入维度需匹配最后一层特征图的展平尺寸
🚀 统一训练框架
# 通用训练流程
def train_model(model, train_loader, test_loader, num_epochs=15, lr=0.01):
# 初始化优化器和损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
# 训练监控
history = {'loss': [], 'accuracy': [], 'iterations': []}
iteration = 0
for epoch in range(num_epochs):
for images, labels in train_loader:
# 数据预处理(统一处理逻辑)
if isinstance(model, CNNModel):
inputs = images.view(-1, 1, 28, 28) # CNN需要保留空间维度
else:
inputs = images.view(-1, 28*28) # ANN需要展平
# 标准训练步骤
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 每50次迭代记录指标
iteration += 1
if iteration % 50 == 0:
acc = evaluate(model, test_loader)
history['loss'].append(loss.item())
history['accuracy'].append(acc)
history['iterations'].append(iteration)
return history
# 统一评估函数
def evaluate(model, loader):
correct = 0
total = 0
with torch.no_grad():
for images, labels in loader:
if isinstance(model, CNNModel):
inputs = images.view(-1, 1, 28, 28)
else:
inputs = images.view(-1, 28*28)
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
return 100 * correct / total
📊 性能对比更新
模型 | 最高准确率 | 参数量 | 训练时间(epoch=15) |
---|---|---|---|
逻辑回归 | 85.85% | 7,840 | 2分钟 |
三隐藏层ANN | 96.26% | 784×150 + 150×150×2 + 150×10 = 121,860 | 2分钟 |
双层CNN | 98.26% | (5×5×1×16) + (5×5×16×32) + (32×4×4×10) = 27,280 | 2分钟 |
关键发现:
-
CNN以更少参数量实现更高准确率 → 空间特征提取优势
-
ANN参数量是CNN的4.5倍但精度低2% → 卷积的权重共享优势
-
训练时间与模型复杂度正相关 → CNN计算密度更高
🎯 关键代码差异说明
ANN改进点:
-
增加
fc3
隐藏层和ELU
激活函数 -
使用
.view(-1, 28*28)
统一处理输入展平 -
学习率调整为0.02(原0.001)
CNN改进点:
-
明确卷积核参数计算逻辑
-
使用
.view(100,1,28,28)
保持batch维度 -
调整学习率为0.1(原0.001)