今日目标:从历史中理解CNN的进化逻辑,手写LeNet/AlexNet/VGG,掌握架构设计原则
预计阅读:10分钟 | 动手操作:40分钟


一、CNN进化史:每代解决什么问题

1998  LeNet-5    → 第一个CNN,证明卷积可以识别手写数字
2012  AlexNet    → 深度学习革命开始,GPU+ReLU+Dropout
2014  VGGNet     → 证明深度很重要,全用3×3小卷积核
2014  GoogLeNet  → 多尺度特征提取,Inception模块
2015  ResNet     → 残差连接,让152层网络能训练
2017  MobileNet  → 端侧部署,深度可分离卷积
...

每代架构都在解决一个核心问题,理解了问题,就理解了架构!

二、LeNet-5 (1998) — 卷积神经网络的开山之作

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import torch.optim as optim

torch.manual_seed(42)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# ===== LeNet-5 =====
class LeNet5(nn.Module):
    """
    LeNet-5:Yann LeCun 1998年提出,用于手写数字识别
    
    架构:C1→S2→C3→S4→C5→F6→Output
    输入: 32×32 灰度图
    输出: 10个数字类别
    
    设计哲学:
    - 卷积提取特征,池化下采样,全连接分类
    - 这个三阶段模式影响至今
    """
    def __init__(self, num_classes=10):
        super().__init__()
        
        # 特征提取器
        self.features = nn.Sequential(
            nn.Conv2d(1, 6, kernel_size=5),   # 32×32 → 28×28×6
            nn.Tanh(),                         # 当年用Tanh,现在用ReLU
            nn.AvgPool2d(kernel_size=2, stride=2),  # 28×28 → 14×14×6
            
            nn.Conv2d(6, 16, kernel_size=5),  # 14×14 → 10×10×16
            nn.Tanh(),
            nn.AvgPool2d(kernel_size=2, stride=2),  # 10×10 → 5×5×16
        )
        
        # 分类器
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(16 * 5 * 5, 120),
            nn.Tanh(),
            nn.Linear(120, 84),
            nn.Tanh(),
            nn.Linear(84, num_classes),
        )
    
    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

# 测试
model_lenet = LeNet5(num_classes=10)
dummy = torch.randn(1, 1, 32, 32)
output = model_lenet(dummy)
print(f"LeNet-5: 输入 {dummy.shape} → 输出 {output.shape}")
print(f"参数量: {sum(p.numel() for p in model_lenet.parameters()):,}")

# 用LeNet-5训练MNIST
def train_lenet_on_mnist():
    transform = transforms.Compose([
        transforms.Resize(32),
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,)),
    ])
    
    train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
    test_dataset = datasets.MNIST('./data', train=False, transform=transform)
    
    train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False)
    
    model = LeNet5().to(device)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    
    for epoch in range(5):
        model.train()
        correct, total = 0, 0
        for x, y in train_loader:
            x, y = x.to(device), y.to(device)
            optimizer.zero_grad()
            loss = criterion(model(x), y)
            loss.backward()
            optimizer.step()
            correct += (model(x).argmax(1) == y).sum().item()
            total += y.size(0)
        
        model.eval()
        with torch.no_grad():
            test_correct = sum((model(x.to(device)).argmax(1) == y.to(device)).sum().item()
                               for x, y in test_loader)
        
        print(f"Epoch {epoch+1}: Train Acc={correct/total:.2%}, Test Acc={test_correct/len(test_dataset):.2%}")

# train_lenet_on_mnist()

LeNet-5 的历史意义

  • 第一次把卷积+池化+全连接的三段式结构固化为CNN标准范式
  • 证明了端到端学习(不需要手工特征)的可行性
  • 在银行支票识别上达到商业可用水平

三、AlexNet (2012) — 深度学习革命的起点

class AlexNet(nn.Module):
    """
    AlexNet:2012年ImageNet冠军,深度学习革命的开端
    
    核心创新:
    1. ReLU替代Sigmoid/Tanh → 解决梯度消失
    2. Dropout防止过拟合
    3. 数据增强(随机裁剪、水平翻转)
    4. 双GPU并行训练(当时显存不够)
    5. Local Response Normalization (LRN,现在被BN取代)
    
    输入: 224×224×3
    输出: 1000类
    """
    def __init__(self, num_classes=1000):
        super().__init__()
        
        self.features = nn.Sequential(
            # Block 1: 224×224 → 55×55
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            
            # Block 2: 55×55 → 27×27
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            
            # Block 3: 27×27 → 13×13
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            
            # Block 4: 13×13
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            
            # Block 5: 13×13 → 6×6
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        
        self.classifier = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )
    
    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

model_alexnet = AlexNet(num_classes=1000)
dummy = torch.randn(1, 3, 224, 224)
output = model_alexnet(dummy)
print(f"\nAlexNet: 输入 {dummy.shape} → 输出 {output.shape}")
print(f"参数量: {sum(p.numel() for p in model_alexnet.parameters()):,}")

# AlexNet的问题分析
print("\nAlexNet架构问题:")
print("  1. 第一层用11×11大卷积核 → 参数多、计算量大")
print("  2. 全连接层4096维 → 占模型参数的90%以上")
print("  3. 需要224×224固定输入 → 不灵活")
print("  4. 这些缺陷催生了VGGNet的改进")

AlexNet 的历史意义

  • 当年ImageNet Top-5错误率从26%降到15.3%,吊打所有传统方法
  • 证明了深度学习在视觉任务上的巨大潜力
  • GPU训练+ReLU+Dropout成为标配

四、VGGNet (2014) — 深度就是力量

class VGGNet(nn.Module):
    """
    VGGNet:牛津大学VGG组提出,证明深度的重要性
    
    核心创新:
    1. 全用3×3小卷积核(堆叠两个3×3 = 一个5×5的感受野,但参数更少)
    2. 统一的结构:n个Conv+ReLU → MaxPool → 通道数翻倍
    3. 只增深度,不增复杂度
    
    两个3×3 vs 一个5×5:
      参数量: 2×(3×3×C×C) = 18C² vs 5×5×C×C = 25C² → 减少28%
      但感受野相同!
    
    版本:VGG11/VGG13/VGG16/VGG19(数字=层数)
    """
    def __init__(self, config='VGG16', num_classes=1000):
        super().__init__()
        
        # VGG各版本的配置
        cfgs = {
            'VGG11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
            'VGG13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
            'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
            'VGG19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
        }
        
        self.features = self._make_layers(cfgs[config])
        
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(4096, num_classes),
        )
    
    def _make_layers(self, cfg):
        layers = []
        in_channels = 3
        for v in cfg:
            if v == 'M':
                layers.append(nn.MaxPool2d(2))
            else:
                layers.extend([
                    nn.Conv2d(in_channels, v, kernel_size=3, padding=1),
                    nn.BatchNorm2d(v),  # 原版VGG没有BN,这里加上
                    nn.ReLU(inplace=True),
                ])
                in_channels = v
        return nn.Sequential(*layers)
    
    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x


# 对比各版本
for name in ['VGG11', 'VGG13', 'VGG16', 'VGG19']:
    model = VGGNet(config=name, num_classes=1000)
    params = sum(p.numel() for p in model.parameters())
    print(f"{name:<8s}: {params:>15,} 参数")

# 感受野分析
print("\nVGG的感受野(3×3堆叠的魅力):")
print("  1个3×3 → 感受野 = 3×3")
print("  2个3×3 → 感受野 = 5×5  (等于1个5×5)")
print("  3个3×3 → 感受野 = 7×7  (等于1个7×7)")
print("  堆叠3×3的优势:")
print("    - 更少的参数:3×3×3×C² = 27C² vs 7×7×C² = 49C²")
print("    - 更多的非线性:3个ReLU vs 1个ReLU")
print("    - 更强的表达能力")

VGGNet 的历史意义

  • 证明了深度比单个大卷积核更重要
  • 统一了CNN的设计语言:3×3卷积 + 2×2池化
  • VGG16至今仍是目标检测的常用backbone(如Faster R-CNN)

五、三代架构对比

def compare_architectures():
    """对比三代CNN的关键指标"""
    models = {
        'LeNet-5': {
            'year': 1998,
            'input_size': '32×32',
            'layers': 5,
            'params': '60K',
            'top_1_acc': '~99% (MNIST)',
            'key_innovation': 'CNN三阶段范式',
            'activation': 'Tanh',
            'normalization': '无',
            'dropout': '无',
        },
        'AlexNet': {
            'year': 2012,
            'input_size': '224×224',
            'layers': 8,
            'params': '60M',
            'top_1_acc': '~63% (ImageNet)',
            'key_innovation': 'ReLU+Dropout+GPU',
            'activation': 'ReLU',
            'normalization': 'LRN',
            'dropout': '0.5',
        },
        'VGG16': {
            'year': 2014,
            'input_size': '224×224',
            'layers': 16,
            'params': '138M',
            'top_1_acc': '~74% (ImageNet)',
            'key_innovation': '全3×3小卷积核',
            'activation': 'ReLU',
            'normalization': '无(原版)',
            'dropout': '0.5',
        },
    }
    
    print(f"{'指标':<16s} {'LeNet-5':<16s} {'AlexNet':<16s} {'VGG16':<16s}")
    print('-' * 64)
    
    keys = ['year', 'input_size', 'layers', 'params', 'top_1_acc', 'key_innovation']
    for key in keys:
        print(f"{key:<16s} {str(models['LeNet-5'][key]):<16s} "
              f"{str(models['AlexNet'][key]):<16s} "
              f"{str(models['VGG16'][key]):<16s}")

compare_architectures()

六、动手实践:从零搭建经典架构

6.1 实战:统一训练框架对比三代架构

class ModelBenchmark:
    """统一训练框架,对比不同架构的性能"""
    def __init__(self, model, device, lr=0.001):
        self.model = model.to(device)
        self.device = device
        self.criterion = nn.CrossEntropyLoss()
        self.optimizer = optim.Adam(model.parameters(), lr=lr)
    
    def train_epoch(self, loader):
        self.model.train()
        correct, total, loss_sum = 0, 0, 0.0
        for x, y in loader:
            x, y = x.to(self.device), y.to(self.device)
            self.optimizer.zero_grad()
            loss = self.criterion(self.model(x), y)
            loss.backward()
            self.optimizer.step()
            loss_sum += loss.item() * x.size(0)
            correct += (self.model(x).argmax(1) == y).sum().item()
            total += x.size(0)
        return loss_sum / total, correct / total
    
    @torch.no_grad()
    def evaluate(self, loader):
        self.model.eval()
        correct, total = 0, 0
        for x, y in loader:
            x, y = x.to(self.device), y.to(self.device)
            correct += (self.model(x).argmax(1) == y).sum().item()
            total += x.size(0)
        return correct / total


def benchmark_on_cifar10():
    """在CIFAR-10上对比LeNet-5、AlexNet、VGG"""
    # CIFAR-10适配版(输入32×32,10类)
    models = {
        'LeNet-5 (adapted)': LeNet5(num_classes=10),
        'AlexNet (adapted)': AlexNet(num_classes=10),
        'VGG11 (adapted)': VGGNet(config='VGG11', num_classes=10),
    }
    
    # 数据
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)),
    ])
    
    train_dataset = datasets.CIFAR10('./data', train=True, download=True, transform=transform)
    test_dataset = datasets.CIFAR10('./data', train=False, transform=transform)
    
    train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False)
    
    results = {}
    for name, model in models.items():
        print(f"\n训练 {name}...")
        # 注意:LeNet-5输入是灰度图,需要转换
        if 'LeNet' in name:
            # 对于LeNet,需要把RGB转灰度
            model = LeNet5(num_classes=10)
            # 简化处理:只取第一个通道
            gray_transform = transforms.Compose([
                transforms.Grayscale(),
                transforms.ToTensor(),
                transforms.Normalize((0.5,), (0.5,)),
            ])
            # 跳过,直接看原理
        
        trainer = ModelBenchmark(model, device, lr=0.001)
        
        for epoch in range(5):
            train_loss, train_acc = trainer.train_epoch(train_loader)
            val_acc = trainer.evaluate(test_loader)
        
        results[name] = {
            'params': sum(p.numel() for p in model.parameters()),
            'val_acc': val_acc,
        }
        print(f"  {name}: 参数量={results[name]['params']:,}, 准确率={val_acc:.2%}")
    
    return results

# benchmark_on_cifar10()

6.2 实战:计算每层输出尺寸

def trace_feature_map_sizes(model_name):
    """追踪网络每一层的特征图尺寸变化"""
    models = {
        'LeNet-5': LeNet5(num_classes=10),
        'AlexNet': AlexNet(num_classes=1000),
        'VGG16': VGGNet(config='VGG16', num_classes=1000),
    }
    
    model = models[model_name]
    input_sizes = {
        'LeNet-5': (1, 1, 32, 32),
        'AlexNet': (1, 3, 224, 224),
        'VGG16': (1, 3, 224, 224),
    }
    
    x = torch.randn(input_sizes[model_name])
    
    print(f"\n{model_name} 特征图尺寸变化:")
    print(f"{'层':<5s} {'输出尺寸':<20s} {'空间尺寸':<12s} {'通道数':<8s}")
    print('-' * 50)
    print(f"{'输入':<5s} {str(x.shape):<20s} {f'{x.shape[2]}×{x.shape[3]}':<12s} {x.shape[1]:<8d}")
    
    with torch.no_grad():
        if hasattr(model, 'features'):
            for i, layer in enumerate(model.features):
                x = layer(x)
                if isinstance(layer, (nn.Conv2d, nn.MaxPool2d, nn.AvgPool2d)):
                    print(f"L{i+1:<4d} {str(x.shape):<20s} "
                          f"{f'{x.shape[2]}×{x.shape[3]}':<12s} {x.shape[1]:<8d}")

trace_feature_map_sizes('VGG16')

七、常见坑点

坑1:VGG的全连接层太大

# VGG16的FC层: 512×7×7=25088 → 4096 → 4096 → 1000
# 仅FC层参数就超过120M,占模型参数的90%以上

# ❌ 端侧部署时全连接层太大了
# ✅ 现代方法:用全局平均池化替代全连接层
# 如ResNet的: nn.AdaptiveAvgPool2d(1) → nn.Linear(512, 1000)

坑2:不同架构的输入尺寸要求不同

# LeNet-5: 32×32
# AlexNet: 224×224
# VGG: 224×224
# 如果输入尺寸不对,需要调整或加AdaptiveAvgPool2d

坑3:从零训练VGG很慢

# VGG16有138M参数,从零训练需要大量GPU时间
# 实际使用中99%都是加载预训练权重
# model = torchvision.models.vgg16(weights='IMAGENET1K_V1')

坑4:BatchNorm在VGG原版中不存在

# 原版VGG没有BN,但现代实现通常加上
# 有BN的VGG更容易训练,收敛更快
# 面试常问:VGG为什么没有BN?因为BN是2015年才提出的

八、今日作业

  1. 手写架构:实现LeNet-5,在MNIST上达到>98%准确率
  2. 架构对比:对比LeNet-5/AlexNet/VGG的参数量和计算量,画一张表格
  3. 感受野:计算VGG16的感受野,验证堆叠3×3的效果
  4. 打卡:评论区发你的对比结果,格式:“Day 22/100 打卡:经典CNN架构已掌握!”

今日小结

今天你学会了:
✅ CNN进化史:LeNet-5 → AlexNet → VGG
✅ LeNet-5:CNN三阶段范式(卷积→池化→全连接)
✅ AlexNet:ReLU+Dropout+GPU的革命
✅ VGG:全3×3小卷积核堆叠,深度就是力量
✅ 两个3×3 vs 一个5×5的参数量对比
✅ 三代架构的完整对比表
✅ 统一训练框架对比不同架构
✅ 特征图尺寸追踪
✅ 4个经典坑点

明日预告

Day 23:ResNet与残差连接 — 终结梯度消失
残差学习、Bottleneck、ResNet-18/34/50/101/152、Identity Mapping


🔥 关注我,每天解锁一个端侧AI技能!
微信公众号:xxx | 小红书:xxx | CSDN:xxx
评论区打卡,一起坚持100天!


附:小红书图文版

封面标题建议:CNN进化史 | LeNet→AlexNet→VGG,三代经典一篇看懂 🏛️

P1 — 封面
标题:经典CNN架构
副标题:LeNet / AlexNet / VGG
关键词:CNN / 架构进化 / 深度学习

P2 — LeNet-5 (1998)
第一个CNN,手写数字识别
卷积→池化→全连接三阶段
60K参数,银行支票识别商用

P3 — AlexNet (2012)
深度学习革命起点
ReLU代替Sigmoid解决梯度消失
Dropout+数据增强防过拟合
60M参数,ImageNet夺冠

P4 — VGGNet (2014)
全用3×3小卷积核堆叠
2个3×3 = 1个5×5感受野,参数更少
深度从11层到19层
138M参数,至今仍是最流行backbone

P5 — 进化规律
更深:5层→8层→16层
更小核:11×11→5×5→3×3
更统一:VGG之后架构设计标准化
下一站:ResNet终结梯度消失

P6 — 今日作业
手写LeNet/AlexNet/VGG + 架构对比
评论区打卡 Day 22/100

标签:#CNN #LeNet #AlexNet #VGG #深度学习


CSDN发布提示:CSDN版本建议在每代架构放一张网络结构图,在对比部分用表格,在感受野部分放一张堆叠3×3的示意图。

更多推荐