从零手写VGGNet:深度卷积架构原理与PyTorch实现指南

发布时间:2026/10/3 8:06:30
从零手写VGGNet:深度卷积架构原理与PyTorch实现指南
说实话我第一次手写VGGNet的时候PyTorch已经能把整个模型封装成一行代码调用。但我还是坚持从零搭了一遍不为别的就因为这个2014年的网络几乎是现代CNN的标准教材——它结构规整、没有花哨的分支、全部由基础卷积和池化堆叠而成却把深度如何起作用这件事讲得明明白白。这篇文章我会把这些年折腾VGGNet的完整过程写出来包括架构设计的数学逻辑、完整可运行的PyTorch实现、训练时的显存账本以及在CIFAR-10和ImageNet不同场景下踩过的坑。不管你是刚入门深度学习、想搞清楚卷积网络内部原理的学生还是工作中需要快速复现经典模型做特征提取的工程师这篇文章应该都能给你一些不太容易在官方文档里看到的东西。1. 为什么是VGGNet一个老架构对现代深度学习的启示1.1 从2014年那场竞赛说起VGGNet来自牛津大学Visual Geometry Group在ILSVRC 2014图像分类任务上拿到第二名第一名是GoogLeNet但它在定位任务上拿了冠军。更关键的是VGGNet的代码和权重在开源社区传播极广成为之后几年很多人研究卷积网络的第一站。现在回头看VGGNet的核心贡献不是刷榜成绩而是用极其简单的实验证明了一件事在卷积网络里深度比单个卷积核的大小更重要。当时的主流做法是使用较大的卷积核比如7x7、11x11来获得更大的感受野而VGGNet反其道而行之全部使用3x3小卷积核通过增加层数来扩大感受野最终在同等计算成本下拿到了更优的结果。这个结论看起来简单但它直接影响了后续几乎所有卷积架构的设计思路。ResNet的残差块、DenseNet的密集连接、MobileNet的深度可分离卷积底层的主干部分都继承了小卷积核深层堆叠的范式。甚至到2024年了各种Transformer架构的视觉模型做特征提取时如果需要一个CNN骨干做对照实验很多人第一个想到的还是VGG16。1.2 VGGNet家族的五种配置VGGNet不是一个单独的网络而是一族配置不同的网络从A到E共有五种结构。我记得第一次看论文里的配置表时觉得这个作者真的非常实在把每种配置的层数和通道数全部列得清清楚楚实验做得特别完整。配置卷积层组数卷积层总数全连接层参数量约AVGG115组8层卷积3层133MBVGG135组10层卷积3层133MCVGG16部分5组13层卷积3层134MDVGG165组13层卷积3层138MEVGG195组16层卷积3层144M这里需要解释一下配置C和D的区别在于网络中是否使用1x1卷积。配置C插入了1x1卷积层来增加非线性但实验表明效果提升有限而配置D用额外的3x3卷积替代了这些1x1卷积效果更好于是D配置成了我们常说的VGG16。配置E就是VGG19比VGG16多了三个3x3卷积层。我在实际使用中90%以上的场景用的都是VGG16。它的参数量相对可控138M计算量比VGG19小不少但精度已经足够好而且社区里有大量基于VGG16的预训练权重和下游任务代码复现和迁移都非常方便。如果你的显存比较紧张可以考虑VGG11它精度略低一点但参数量和计算量都明显更小做特征提取实验足够了。1.3 为什么说VGGNet是最适合从零搭建的模型很多人问我市面上有那么多更先进、更高效的模型为什么学习时非要选择VGGNet我的理由有三个。第一结构极度规整。VGGNet没有分支、没有残差连接、没有注意力机制就是卷积、激活、池化三种操作反复堆叠。这意味着你在实现它的时候不需要处理任何复杂的控制流代码逻辑非常清晰每一步都在你掌控之中。这种一眼看穿的特性对初学者理解卷积网络的数据流非常有帮助。第二设计逻辑可以完整推演。为什么连续用三个3x3卷积而不是一个7x7卷积为什么每次池化后通道数翻倍为什么最后接了三个全连接层这些问题在VGGNet上都能找到确切的数学解释推演清楚之后你对卷积网络的整体认知会上一个台阶。第三复现成本可控。相比现在动辄上百层的ResNet变体或结构复杂的TransformerVGGNet的前向传播过程简单反向传播的计算图也容易分析。你甚至可以在单张消费级显卡上完成完整的训练实验不需要分布式训练的技巧。这是把从零搭建这件事完整走一遍的绝佳选择。2. VGGNet的架构解剖小卷积核、通道翻倍与全连接霸权的账本2.1 为什么连续堆叠小卷积核而不是直接用一个大的VGGNet最著名的设计决策是全部使用3x3卷积核并且在每个尺度的特征图上堆叠多个卷积层。论文里给出的理由是两个连续的3x3卷积层的感受野等于一个5x5卷积层三个连续的3x3卷积层的感受野等于一个7x7卷积层。这个感受野是怎么算出来的对于连续卷积层感受野的递推公式是第n层的感受野 第n-1层的感受野 (卷积核大小 - 1) × 步长累积如果卷积核大小是3x3、步长为1、padding为1那么每经过一层卷积感受野增加2。经过3层后感受野增加了6加上初始的1个像素正好是7等价于一个7x7卷积。那么用三个3x3卷积替代一个7x7卷积好处在哪里计算参数量的账就清楚了。假设输入和输出的通道数都是C单个7x7卷积的参数量7 × 7 × C × C 49C²三个3x3卷积的参数量3 × (3 × 3 × C × C) 27C²也就是说在获得相同感受野的情况下三个3x3卷积的参数量只有单个7x7卷积的55%27/49。同时三层卷积之间有两层ReLU激活引入了额外的非线性模型的表达能力也更强了。这是VGGNet以小博大的核心逻辑理解了这一点你就能明白为什么后来几乎所有的CNN都在堆小卷积核。2.2 通道数翻倍的节奏与特征图的尺寸轨迹我每次给学生讲VGGNet都会让他们画一张特征图的尺寸变化表。这张表画完整个网络的设计逻辑就清晰了。输入图像224 × 224 × 3阶段操作输出尺寸输出通道conv1两个3x3卷积 MaxPool112 × 11264conv2两个3x3卷积 MaxPool56 × 56128conv3三个3x3卷积 MaxPool28 × 28256conv4三个3x3卷积 MaxPool14 × 14512conv5三个3x3卷积 MaxPool7 × 7512fc全连接层1 × 14096 → 4096 → 1000注意看这个规律每次最大池化把空间尺寸缩小一半通道数就翻倍除了conv5之后从512到512没有翻倍这是为控制参数量做的取舍。这种空间缩小、通道增加的设计意图是保持信息总量大致不变——空间上丢掉的位置信息用通道上更丰富的特征来表示。从信息论的角度看这是卷积网络最朴素的维度再平衡策略。特征图从224缩小到7正好经过了5次减半224 → 112 → 56 → 28 → 14 → 7。这最终让全连接层的输入维度变成512 × 7 × 7 25088这个数字在后面算参数量时会用到。2.3 VGG16的参数量都去哪了全连接层的霸权VGG16的参数量约138M但如果你有兴趣具体算一下每一层的参数量分布会发现一个非常反直觉的事实绝大部分参数不在卷积层而在最后的三个全连接层。让我给你算一笔细账。以CIFAR-10为例输入224 × 224的原始VGG16所有13个卷积层的参数量合计约14.7M第一个全连接层25088 × 4096 ≈ 102.7M第二个全连接层4096 × 4096 ≈ 16.8M第三个全连接层4096 × 1000 ≈ 4.1M三个全连接层的参数量加起来约123.6M占了整个模型参数的近90%。这就是VGGNet作为现代标准模型最被诟病的地方——全连接层是一个巨大的参数黑洞。在CIFAR-10上做实验时输入32 × 32如果你把VGG16的网络结构直接搬过来用会发现最后经过5次池化后特征图变成1 × 1全连接层的输入只有512维参数量会大幅下降。但如果你把输入resize到224 × 224或者去掉一个池化层那么全连接层的参数量依然是天文数字。这也是为什么很多人说VGGNet能跑起来不代表你能跑得起——它的显存和内存消耗很大程度上是这三个全连接层贡献的。3. 从零搭建VGG16的实操代码数据、结构、训练一个不少3.1 环境准备与数据预处理我先说明一下这里用的深度学习框架是PyTorch版本2.x都可以。手写VGGNet不需要任何高级API只用最基础的torch.nn模块就够了。数据方面很多人想直接拿ImageNet复现VGG16的原始成绩但我建议你第一遍跑的时候用CIFAR-10原因有二一是CIFAR-10规模小、下载快一张普通显卡就能在合理时间内完成训练二是CIFAR-10的类别数少10类你只需要调整最后一个全连接层的输出维度其他结构完全不变适合做快速验证。CIFAR-10的图片是32 × 32的三通道图像而VGGNet原始设计输入是224 × 224。这就涉及一个适配问题。我试过两种方案方案一把输入图像resize到224 × 224严格保持VGG16原始结构。这个方案最原教旨但训练速度和显存消耗都很大32的batch size在消费级显卡上都可能爆显存。方案二保持32 × 32输入直接在结构上做微调。因为CIFAR-10图像经过5次池化后32 → 16 → 8 → 4 → 2 → 1会变成1 × 1全连接层的输入维度就变成512 × 1 × 1 512把nn.Linear(512 * 7 * 7, 4096)改成nn.Linear(512, 4096)即可。这也是我在实际实验中最常用的方案。数据预处理部分CIFAR-10的常规做法是import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) train_dataset datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform_train) test_dataset datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform_test) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse, num_workers4)这里Normalize用的均值和标准差是CIFAR-10数据集的统计值不是ImageNet的这个细节很多人容易搞混。如果你用ImageNet的均值和标准差(0.485, 0.456, 0.406)来归一化CIFAR-10模型也能收敛但训练初期loss下降会略微慢一点。数据增强方面我用了随机裁剪和随机水平翻转这是CIFAR-10上非常有效的两招能明显缓解过拟合。像Cutout、Mixup这类更强的增强手段等模型能稳定跑到90%准确率之后再考虑加也不迟。3.2 手写VGG16模型结构下面是我推荐的VGG16完整实现。核心思路是把卷积层部分和全连接分类器部分分开定义用配置字典来控制网络的深度和宽度。这种写法比直接堆十几个nn.Conv2d要清晰得多也方便你后续改成VGG19或其他变体。import torch import torch.nn as nn # 配置字典D就是VGG16 # M 表示最大池化层数字表示卷积核输出通道数 cfgs { A: [64, M, 128, M, 256, 256, M, 512, 512, M, 512, 512, M], B: [64, 64, M, 128, 128, M, 256, 256, M, 512, 512, M, 512, 512, M], D: [64, 64, M, 128, 128, M, 256, 256, 256, M, 512, 512, 512, M, 512, 512, 512, M], E: [64, 64, M, 128, 128, M, 256, 256, 256, 256, M, 512, 512, 512, 512, M, 512, 512, 512, 512, M], } def make_layers(cfg, batch_normTrue): layers [] in_channels 3 for v in cfg: if v M: layers.append(nn.MaxPool2d(kernel_size2, stride2)) else: conv2d nn.Conv2d(in_channels, v, kernel_size3, padding1) if batch_norm: layers.append(conv2d) layers.append(nn.BatchNorm2d(v)) layers.append(nn.ReLU(inplaceTrue)) else: layers.append(conv2d) layers.append(nn.ReLU(inplaceTrue)) in_channels v return nn.Sequential(*layers) class VGG16(nn.Module): def __init__(self, features, num_classes10, init_weightsTrue): super(VGG16, self).__init__() self.features features # CIFAR-10输入32x32,经过5次池化后特征图为1x1 # 如果输入是224x224这里要改为512 * 7 * 7 self.classifier nn.Sequential( nn.Linear(512 * 1 * 1, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, num_classes), ) if init_weights: self._initialize_weights() def forward(self, x): x self.features(x) x torch.flatten(x, 1) x self.classifier(x) return x def _initialize_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityrelu) if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) nn.init.constant_(m.bias, 0) def vgg16_bn(num_classes10): model VGG16(make_layers(cfgs[D], batch_normTrue), num_classesnum_classes) return model我来说说几个容易踩的细节。第一个是padding1。3x3卷积配合padding1才能保持特征图的尺寸不变这样池化才能严格按照2倍关系缩小。如果你漏了padding特征图尺寸会逐步减小网络运行到后面就会出现尺寸不匹配的报错。第二个是inplaceTrue。ReLU的inplace操作可以节省内存因为VGGNet的中间特征图非常大能省一点是一点。但要注意inplace操作会覆盖输入张量如果后续还需要保留该张量做别的事比如在某些自定义loss中就不能用inplace。第三个是BatchNorm的初始化。_initialize_weights里把BN的gamma初始化为1beta初始化为0这是常规操作。卷积层用Kaiming初始化全连接层用均值为0、标准差为0.01的正态分布这些初始化策略直接决定了模型能否快速收敛。如果你偷懒跳过初始化PyTorch默认的初始化方式在深网络上效果会差不少。3.3 训练循环、评估指标与学习率调度接下来是训练和评估的核心代码。我习惯把训练循环写成一个函数这样后续调整超参数时不需要改动整体结构。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model vgg16_bn(num_classes10).to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.05, momentum0.9, weight_decay5e-4) # 余弦退火学习率调度效果比固定学习率好 scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max200) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in 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() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc def evaluate(model, loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc num_epochs 200 for epoch in range(num_epochs): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) test_loss, test_acc evaluate(model, test_loader, criterion, device) scheduler.step() if (epoch 1) % 10 0: print(fEpoch [{epoch1}/{num_epochs}] fTrain Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f} | fTest Loss: {test_loss:.4f}, Test Acc: {test_acc:.4f})这里有几个超参数的选择是有讲究的。学习率我用了0.05配合SGD的momentum和weight_decay这是CIFAR-10上训练VGG类网络的常见配置。如果你用Adam学习率通常是0.001但Adam在VGG这种全连接层占比很大的网络上容易过拟合泛化效果往往不如SGDmomentum。weight_decay5e-4是L2正则化它能在一定程度上抑制全连接层的参数膨胀对泛化有帮助。如果你发现训练集准确率很高但测试集跟不上可以适当增大weight_decay。余弦退火调度器CosineAnnealingLR的作用是让学习率在整个训练过程中从初始值平滑下降到接近0这样在训练后期可以用很小的学习率精细调优权重。我试过用固定学习率0.05一路跑到底测试准确率大概在91%左右换成余弦退火后同样200个epoch能到93%以上。训练过程中你会发现一个有意思的现象前几个epoch的loss下降非常慢甚至看起来像在原地踏步。这是因为VGG16的深度较深梯度从最后一层传播到第一层需要时间。通常要到第10个epoch之后loss才会出现明显的下降趋势。这时候千万别心急去调学习率或者换优化器多给它一点时间。3.4 用ImageNet规模的数据时要调整什么如果你不只是做CIFAR-10实验而是真的想在ImageNet或类似规模的数据集上复现VGG16那么有两处必须改。第一处是全连接层的输入维度。ImageNet的输入通常是224 × 224经过5次池化后特征图是7 × 7所以第一个全连接层的输入维度要改回512 * 7 * 7。这个改动直接决定了模型参数量从CIFAR-10版的约25M飙升到138M这就是VGGNet真实体量的来源。第二处是输出的类别数。ImageNet有1000类最后一个全连接层的输出维度要改成1000。我在代码里把num_classes作为参数传入了就是方便你切换数据集时不用改结构定义。在ImageNet规模上训练VGG16普通的单张显卡基本是跑不动的。我实测过batch size 64的情况下一张24GB显存的显卡勉强能训练但每个epoch耗时非常长。更实际的做法是直接加载PyTorch官方提供的预训练权重在目标任务的数据集上做微调finetune。如果你需要自己从头训练建议至少用4卡及以上的分布式训练环境并且做好跑几周的心理准备。4. 实测踩坑实况显存膨胀、loss失控与CIFAR-10上的收敛路径4.1 一张图算清显存账为什么VGG16一上来就爆显存很多人第一次在224 × 224输入下训练VGG16开开心心把batch size设为64结果程序跑起来没过几秒就报OOMOut of Memory。这个坑我踩过太多次了这里帮你把账算清楚省得你反复试错。显存消耗的大头不只是模型参数更是前向传播过程中保存下来的中间激活值。反向传播计算梯度时需要用到每一层的输入和输出。以batch size 32、输入224 × 224为例第一层卷积的输出特征图尺寸是 32 × 64 × 224 × 224每个float占4字节单张特征图占内存32 × 64 × 224 × 224 × 4 ≈ 411MB整个网络有13个卷积层加上BN和ReLU每个层都要保存中间结果粗略估算下来训练时显存占用轻松超过8GB甚至逼近12GB。这还只是前向传播的激活值还没算上梯度、优化器状态SGD的momentum buffer以及全连接层中间那巨大的4096维向量。我的经验是在24GB显存的显卡上训练VGG16batch size不要超过64在12GB显存上batch size设成32就要提心吊胆了。所以在CIFAR-10上做实验时我通常用32的batch size并且把输入保持在32 × 32这样能大幅降低显存压力。如果你一定要在ImageNet规模上训练可以考虑开启PyTorch的torch.utils.checkpoint梯度检查点技术用以时间换显存的方式把部分层的激活值丢弃、反向传播时重新前向计算能节省一半以上的显存但训练时间会增加约30%。4.2 loss变成NaN的排查过程从学习率到数据预处理有一次我在CIFAR-10上训练VGG16跑了不到一个epochloss直接变成了NaN。当时的第一反应是学习率设大了但把学习率从0.05降到0.01之后问题依旧。接着我又怀疑是梯度爆炸逐层打印梯度的范数发现第一个卷积层的梯度已经变成了NaN。排查到最后问题出在数据预处理的归一化参数上。我用的是ImageNet的均值和标准差去归一化CIFAR-10的图片这本来不至于导致NaN但我同时用了太大的weight_decay我一度设置为0.5导致正则化项对梯度的贡献过大数值不稳定最终触发梯度爆炸。这个问题最后的解决方案是把weight_decay改回5e-4并且把输入数据的归一化参数换成CIFAR-10自己的统计值。训练马上恢复正常。这个经历给我的教训是VGGNet这种深度网络对数值稳定性非常敏感任何一个环节的数值设置不合理都可能在几十层反向传播的层层放大下变成灾难。遇到NaN按这个顺序排查检查学习率是否过大最常见检查数据归一化均值和标准差是否正确检查weight_decay是否过大检查标签是否有负数或超出类别范围检查损失函数是否选择了正确的类型多分类用CrossEntropyLoss4.3 CIFAR-10上的完整收敛路径参考如果你用上面的代码在CIFAR-10上从零训练VGG16带BatchNorm版本我给出一组我实测的参考数据方便你判断自己的训练是否正常。Epoch训练集准确率测试集准确率学习率10约65%约60%0.04930约85%约80%0.04660约95%约87%0.040100约99%约90%0.031150约99.8%约92%0.016200约99.9%约93%0.001注意训练集准确率会很快逼近100%因为VGG16的容量足够大CIFAR-10这个规模的数据集还不足以让它欠拟合。关键看测试集准确率能否稳步提升到93%左右。如果测试集准确率在85%左右就上不去了多半是数据增强不够或者正则化不足可以试试加大weight_decay或者加入Cutout增强。还有一个非常有用的技巧先用一个很小的子集比如500张图跑几个epoch确认模型能在这批数据上过拟合。如果模型连小数据集都过拟合不了说明代码层面大概率有bug没必要浪费大量时间去跑完整数据集。这个习惯我到现在还在用。4.4 全连接层的dropout一个看似简单但很容易用错的地方VGG16原版在两个全连接层之间使用了Dropout比例为0.5。我见过很多人把dropout错误地放到卷积层后面或者干脆删掉了dropout结果就是过拟合明显加剧。为什么dropout放在全连接层而不是卷积层因为全连接层占了绝大部分参数量是最容易过拟合的地方。卷积层本身由于权值共享和局部连接的特性参数效率高得多对dropout的需求不大。如果在卷积后面也加dropout除了某些特殊设计如Spatial Dropout反而会破坏特征的连续性损害性能。训练和推理时的行为差异也要注意PyTorch的model.train()和model.eval()会自动切换dropout的状态。一个非常常见的低级错误是测试时忘记切换model.eval()导致每个batch的推理结果都不一样准确率看起来忽高忽低。如果你发现测试集准确率跳动很剧烈先检查一下是不是忘了切换模式。5. 复现后的延伸思考VGGNet的遗产与新架构的接力5.1 VGGNet真正教会我们的事跑完整套流程之后再回头看VGGNet的设计我对那篇论文的看法发生了很大的变化。VGGNet没有提出任何革命性的组件它用的卷积、池化、全连接、ReLU全都是2012年AlexNet时代就有的东西。它做的最重要的工作是把这些组件以一种极其规整的方式组合起来并且用控制变量的实验方法验证了深度是关键这个命题。这种研究思路对我自己的工程实践有很深的影响。当我要设计一个新的特征提取网络时不会一上来就堆各种花哨的模块而是先搭一个最简单的基线确认它能跑通、能收敛然后再逐步添加新的设计。VGGNet这种基准确立的方法论比它本身的结构更有价值。5.2 VGGNet的局限为什么它被ResNet取代VGGNet最大的问题在于效率。138M的参数量超过90%都在全连接层上而ResNet-50的参数量只有约25M性能却更好。ResNet用全局平均池化Global Average Pooling取代了全连接层直接把最后一个卷积特征图的每个通道做平均得到512维的向量然后连到输出层。这一改动让参数量锐减并且让网络对输入图像的尺寸不那么敏感。此外VGGNet训练时的显存开销也太大13个卷积层的激活值全都需要保存用于反向传播。ResNet通过残差连接和更高效的stage设计在同样的深度下计算量更低。到了MobileNet时代深度可分离卷积更是把标准卷积的参数量和计算量都压缩到了原来的十分之一左右。不过VGGNet并没有完全退出历史舞台。一个很典型的例子是风格迁移和感知损失领域很多经典的实现用的依然是VGG16的预训练特征因为VGG16的特征图在风格和内容分离上表现很好。我现在做图像生成相关的实验时如果需要一个CNN特征提取器来计算感知损失第一个想到的还是VGG16——它的预训练权重获取方便特征语义清晰在这个场景下是性价比最高的选择。5.3 基于VGGNet做扩展轻量化改进和不同任务的适配如果你不满足于只是复制VGG16想在其基础上做点改动我给出几个我实际试过且有效果的方向。第一组尝试是轻量化改造。把全连接层替换为全局平均池化可以直接把参数量从138M降到约15M精度在CIFAR-10上几乎没有下降在ImageNet上会有1到2个百分点的损失。把标准卷积替换为深度可分离卷积MobileNet的核心思想参数量会进一步降低。这两个改造做完你的VGGNet就进化成了一个简易版的MobileNet。第二组尝试是用VGGNet做多尺度特征提取。VGGNet从conv1到conv5的特征图尺寸逐级减半这天然形成了金字塔结构很适合用来做目标检测类似SSD的做法或语义分割的特征融合。你可以在forward函数里把每一层池化后的输出都保存下来构成一个feature pyramid下游任务直接用这些多尺度特征去做预测。我当时做的一个小目标检测实验就是用这种方式效果比只用最后一层特征好很多。第三组尝试是对比实验的基线。现在提交论文或者做技术报告时经常需要对比不同骨干网络的效果。VGG16作为经典的CNN基线在参数量、FLOPs、准确率这几个维度上都是一个稳定的参照点。我在做模型压缩实验时习惯用VGG16作为参数量大、过拟合严重的对照组用MobileNet作为轻量高效的对照组两个极端一夹我的方法的位置就清晰了。5.4 如果现在才入门要不要从VGGNet开始学有人觉得2024年了还学VGGNet是不是有点过时我的看法是过时的不是结构而是你只会用现成模型、不懂底层原理的态度。VGGNet作为教学材料的价值至今无人能替代因为你可以在一个下午把它从零搭出来、训练起来然后亲眼看到深度、宽度、正则化、学习率这些因素如何影响最终结果。当然如果你已经有扎实的基础直接上手ResNet、EfficientNet甚至ViT也完全没问题。但如果你想找一条从零理解CNN的主干路径VGGNet仍然是性价比最高的起点。它像一个结构清晰、没有暗坑的入门跑车让你先学会驾驶技术再换更复杂的车型。最后再分享一个我个人的小习惯每次搭建一个神经网络我都会在代码里保留一份手写的、不依赖任何高层封装的实现就像上面的VGG16代码而不是直接用torchvision.models.vgg16()一行搞定。这不是低效而是一种手感训练。当某天模型在线上出了问题需要你去debug你从未写过、从未理解过的网络内部时你会感谢当年那个愿意从零搭建的自己。