深度卷积神经网络进化史与实战:从AlexNet到ResNet,手把手实现服装分类

前言

如果你关注过人工智能的发展历程,一定听说过2012年那个划时代的时刻——AlexNet在ImageNet图像分类挑战赛中以碾压性的优势夺冠,将Top-5错误率从26.2%直接拉到15.3%。从此,深度学习像一颗核弹,引爆了整个计算机视觉领域,并迅速蔓延到自然语言处理、语音识别等几乎所有AI分支。

而引爆这一切的核心,正是深度卷积神经网络(Deep CNN)

本文将带你系统回顾CNN的经典进化史:AlexNet、VGG、GoogLeNet、ResNet,并从一个完整的服装分类实战项目出发,手把手教你用PyTorch搭建、训练和评估一个CNN模型。全文配有详细代码注释和原理解释,即使你是初学者,也能轻松跟上。


一、深度CNN为何能“深度”?

传统的浅层神经网络(1~3层)在图像任务上表现平平,因为图像中的特征具有层次性

  • 浅层网络只能提取边缘、颜色等低级特征;

  • 深层网络则可以组合低级特征,逐步形成纹理、部件乃至物体的高级语义表示。

同时,CNN通过权值共享局部连接大大减少了参数数量,使得训练深层网络成为可能。


二、四大经典CNN架构解析

1. AlexNet(2012)——深度学习“开山之作”

2012年由Alex Krizhevsky、Ilya Sutskever与 Geoffrey Hinton合作提出,是一个基于CNN构建的神经网络模型,主要架构包含8层(5个卷积层+3个全连接层),激活函数使用 ReLU,最后经全连接层输出结果,并且使用了Dropout。

创新点:

  • 使用ReLU激活函数,加速收敛并缓解梯度消失。

  • 采用Dropout随机失活,防止过拟合。

  • 利用GPU并行训练,大幅提升计算速度。

2. VGG(2014)——简单即美

VGG由牛津大学视觉几何组提出,主要有VGG-16和VGG-19两种。它证明了:堆叠多个小卷积核(3×3)比使用大卷积核(7×7)效果更好,同时参数更少。

  • 所有卷积层均使用3×3滤波器,步长1,padding=1,保持尺寸不变。

  • 池化层统一使用2×2最大池化,步长2,将尺寸减半。

  • 最后跟3个全连接层(与AlexNet类似)。

VGG结构非常规整,易于理解和复现,因此被广泛用于迁移学习。

3. GoogLeNet(2014)——更宽而非更深

GoogLeNet(也称Inception v1)引入了Inception模块:在同一层中并行使用1×1、3×3、5×5的卷积和3×3池化,然后将结果在通道维度拼接起来。这种设计使得网络在“横向”上也具有了深度。

Inception模块的优势:

  • 1×1卷积可降维,大幅减少参数。

  • 多尺度特征提取,提高对不同大小物体的适应能力。

  • 缓解梯度消失问题。

4. ResNet(2015)——残差学习,训练超深网络不再难

何恺明等人提出的ResNet(残差网络)是CNN史上的里程碑。它通过跳跃连接(skip connection) 实现了恒等映射,使网络学习的目标从原始的 h(x) 变为残差 f(x)=h(x)-x。

残差块公式:

h(x) = F(x) + x

其中 F(x) 是需要学习的残差部分。当网络很深时,残差学习比直接学习原始映射更容易优化,有效解决了梯度消失和梯度爆炸。

ResNet-152可以达到152层,远超VGG的19层,而性能却更好。


三、实战:基于Fashion MNIST的服装分类

理论部分结束,下面进入硬核实战环节。我们将使用Fashion MNIST数据集,搭建一个与LeNet-5类似的CNN模型,完成10类服装图像的分类任务。

3.1 数据集介绍

Fashion MNIST由Zalando Research发布,包含70,000张28×28的灰度图像,其中训练集60,000张,测试集10,000张。共10个类别:

标签

类别

标签

类别

0

T恤/上衣

5

凉鞋

1

裤子

6

衬衫

2

套头衫

7

运动鞋

3

连衣裙

8

4

外套

9

靴子

3.2 环境准备与数据加载

首先导入必要的库:

import torch
import torch.nn as nn
import torch.optim as optim
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import TensorDataset, DataLoader
 
plt.rcParams['font.sans-serif'] = ['SimHei']   # 黑体
# 方案B:使用微软雅黑
# plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
 
# 解决负号显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
 
# 设置随机种子,保证结果可复现
torch.manual_seed(42)

下载数据集(CSV格式)并读取:

# 从本地路径读取CSV文件
train_df = pd.read_csv("data/fashion-mnist_train.csv")
test_df  = pd.read_csv("data/fashion-mnist_test.csv")
 
# 查看数据形状
print(f"训练集形状: {train_df.shape}")   # (60000, 785)
print(f"测试集形状: {test_df.shape}")    # (10000, 785)

数据集下载:https://pan.baidu.com/s/1k-kQvWID5p_q9MAUXc61sQ?pwd=i8jx

数据预处理:

  • 第一列是标签(0~9),后面784列是像素值(28×28展开)。

  • 需要将像素值reshape为 (batch, 1, 28, 28),并转换为 torch.float32。

  • 标签转换为 torch.int64(CrossEntropyLoss要求)。

# 提取特征和标签
X_train = torch.tensor(train_df.iloc[:, 1:].values, dtype=torch.float32).reshape(-1, 1, 28, 28)
y_train = torch.tensor(train_df.iloc[:, 0].values, dtype=torch.int64)
 
X_test = torch.tensor(test_df.iloc[:, 1:].values, dtype=torch.float32).reshape(-1, 1, 28, 28)
y_test = torch.tensor(test_df.iloc[:, 0].values, dtype=torch.int64)
 
# 归一化:将像素值从 [0, 255] 缩放到 [0, 1],有利于模型收敛
X_train /= 255.0
X_test  /= 255.0
 
# 查看一个样本图像
plt.imshow(X_train[12345, 0, :, :], cmap='gray')
plt.title(f"Label: {y_train[12345].item()} -> {['T恤','裤子','套头衫','连衣裙','外套','凉鞋','衬衫','运动鞋','包','靴子'][y_train[12345].item()]}")
plt.axis('off')
plt.show()

创建PyTorch的TensorDataset和DataLoader:

train_dataset = TensorDataset(X_train, y_train)
test_dataset  = TensorDataset(X_test, y_test)
 
# DataLoader会自动打乱数据(shuffle=True)并分批
batch_size = 256
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader  = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

3.3 模型搭建

我们构建一个类似LeNet-5的CNN,结构如下:

使用nn.Sequential实现:

model = nn.Sequential(
    # 第一个卷积块
    nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1, padding=2),
    nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
 
    # 第二个卷积块
    nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),  # 输出尺寸: 28-5+1=24? 不对,前一层池化后是14,再卷积:14-5+1=10
    nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),  # 10/2=5
 
    # 全连接部分
    nn.Flatten(),
    nn.Linear(16 * 5 * 5, 120),
    nn.Sigmoid(),
    nn.Linear(120, 84),
    nn.Sigmoid(),
    nn.Linear(84, 10)
)
 
# 测试各层输出形状
X_test_shape = torch.rand(1, 1, 28, 28)
print("逐层输出尺寸验证:")
for name, layer in model.named_children():
    X_test_shape = layer(X_test_shape)
    print(f"{name:20} -> {X_test_shape.shape}")

输出应类似:

3.4 权重初始化

合适的初始化能加速收敛。我们采用Xavier均匀分布初始化线性层和卷积层:

def init_weights(m):
    if isinstance(m, nn.Linear) or isinstance(m, nn.Conv2d):
        nn.init.xavier_uniform_(m.weight)
        # 如果存在偏置项,将其初始化为0
        if m.bias is not None:
            nn.init.constant_(m.bias, 0)
 
model.apply(init_weights)

3.5 训练函数定义

我们定义train函数,包含:

  • 模型移动至GPU(如果可用)

  • 损失函数:交叉熵损失(CrossEntropyLoss),内部已包含Softmax

  • 优化器:随机梯度下降SGD

  • 训练循环 + 验证循环

  • 记录每个epoch的损失和准确率

def train_model(model, train_dataset, test_dataset, lr=0.9, epochs=20, batch_size=256, device='cpu'):
    # 移动到设备
    model = model.to(device)
 
    # 损失函数与优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=lr)
 
    # 创建DataLoader(每个epoch重新打乱)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
    test_loader  = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
 
    train_losses = []
    train_accs = []
    test_accs = []
 
    for epoch in range(epochs):
        # ---------- 训练阶段 ----------
        model.train()
        running_loss = 0.0
        correct_train = 0
        total_train = 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()
 
            running_loss += loss.item()
            # 计算训练准确率
            _, predicted = torch.max(outputs, 1)
            total_train += labels.size(0)
            correct_train += (predicted == labels).sum().item()
 
        epoch_loss = running_loss / len(train_loader)
        epoch_train_acc = correct_train / total_train
        train_losses.append(epoch_loss)
        train_accs.append(epoch_train_acc)
 
        # ---------- 验证阶段 ----------
        model.eval()
        correct_test = 0
        total_test = 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)
                total_test += labels.size(0)
                correct_test += (predicted == labels).sum().item()
 
        epoch_test_acc = correct_test / total_test
        test_accs.append(epoch_test_acc)
 
        # 打印进度
        print(f"Epoch [{epoch+1:2d}/{epochs}]  Loss: {epoch_loss:.4f}  |  Train Acc: {epoch_train_acc:.4f}  |  Test Acc: {epoch_test_acc:.4f}")
 
    return train_losses, train_accs, test_accs

3.6 启动训练

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
 
# 重新初始化模型(确保参数随机)
model = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5, padding=2),
    nn.Sigmoid(),
    nn.AvgPool2d(2, 2),
    nn.Conv2d(6, 16, kernel_size=5),
    nn.Sigmoid(),
    nn.AvgPool2d(2, 2),
    nn.Flatten(),
    nn.Linear(400, 120),
    nn.Sigmoid(),
    nn.Linear(120, 84),
    nn.Sigmoid(),
    nn.Linear(84, 10)
)
model.apply(init_weights)
 
losses, train_accs, test_accs = train_model(
    model, train_dataset, test_dataset,
    lr=0.9, epochs=20, batch_size=256, device=device
)

3.7 训练结果可视化

plt.figure(figsize=(12, 4))
 
plt.subplot(1, 2, 1)
plt.plot(range(1, 21), losses, marker='o')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
 
plt.subplot(1, 2, 2)
plt.plot(range(1, 21), train_accs, label='Train Acc', marker='s')
plt.plot(range(1, 21), test_accs, label='Test Acc', marker='^')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Accuracy Curve')
plt.legend()
plt.grid(True)
 
plt.tight_layout()
plt.show()

在合理超参数下,最终测试准确率通常能达到 90%~92% 左右。如果调整学习率、增加Batch Normalization或使用Adam优化器,还可以进一步提升。

3.8 使用模型进行预测

# 随机取5张测试集图片进行预测
model.eval()
test_iter = iter(test_loader)
images, labels = next(test_iter)
 
# 取前5张
images_sample = images[:5].to(device)
labels_sample = labels[:5]
 
with torch.no_grad():
    outputs = model(images_sample)
    _, preds = torch.max(outputs, 1)
 
# 显示结果
class_names = ['T恤/上衣', '裤子', '套头衫', '连衣裙', '外套', '凉鞋', '衬衫', '运动鞋', '包', '靴子']
plt.figure(figsize=(12, 4))
for i in range(5):
    plt.subplot(1, 5, i+1)
    plt.imshow(images_sample[i].cpu().squeeze(), cmap='gray')
    plt.title(f"True: {class_names[labels_sample[i]]}\nPred: {class_names[preds[i].item()]}")
    plt.axis('off')
plt.tight_layout()
plt.show()


四、总结与展望

通过本文,我们回顾了深度卷积神经网络的发展历程,从AlexNet的革命性突破到ResNet的残差学习,每一次架构创新都极大推动了计算机视觉的发展。随后,我们使用PyTorch完整实现了一个服装分类任务,涵盖了数据加载、模型构建、训练、评估和预测的全流程。

要点回顾:

  • 深度CNN

     通过层次化特征提取,极大提升了图像分类精度。

  • 经典架构

     各有侧重:AlexNet引入ReLU和Dropout;VGG强调小卷积核堆叠;GoogLeNet用Inception模块增加宽度;ResNet用跳跃连接训练超深网络。

  • 实战代码

     展示了从CSV数据到模型部署的每一步,注释详尽,可直接复用。

下一步建议:

  • 尝试将本项目的简单CNN替换为ResNet-18或VGG-16,观察精度提升。

  • 加入数据增强(随机旋转、翻转等),进一步提升泛化能力。

  • 使用学习率调度器(如torch.optim.lr_scheduler.StepLR)动态调整学习率。

深度学习的魅力在于:你不需要从零发明一切,但必须深刻理解已有工具的原理。希望本文能为你打下坚实的基础,在AI之路上走得更远。

如果觉得本文对你有帮助,欢迎点赞、收藏、转发。有任何疑问,欢迎评论区交流讨论!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容