ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

VGG19全面解析:从结构原理到PyTorch迁移学习实战

VGG19全面解析:从结构原理到PyTorch迁移学习实战 我平时看到不少学视觉的同学第一个真正“吃透”的模型就是VGG19。这个系列走到第12篇按理说早该聊点更花哨的结构了但我还是想把VGG19单独拿出来细讲一次。原因很简单与其说VGG19是一个具体的模型不如说它是一把“万能钥匙”。无论是做图像分类、目标检测的骨干网络、还是风格迁移里的特征提取器VGG19几乎都能直接上手用。你只要把它搞明白后面看ResNet、DenseNet、EfficientNet这些结构都会轻松太多。这篇就专门讲VGG19的实现。我会从网络设计为什么长这个样子开始一直讲到用PyTorch从零搭建、加载官方预训练权重、在CIFAR-10上做微调、以及训练过程中我实测踩过的各种坑。无论你是刚学完卷积神经网络基础、准备动手跑第一个完整项目的新手还是已经跑过几个模型但想深挖主流结构的老手这篇都能给你一些参考资料。1. 为什么VGG19是绕不开的“基石”很多人一上来就学ResNet觉得残差结构先进、效果好。但VGG19依然值得学而且我觉得在一个完整的学习路径里VGG19才是那个承上启下的关键节点。理解了VGG19的规整、简洁与那种极致的堆叠思想后面再学任何基于CNN的模型都会顺手很多。1.1 一张图看懂VGG19的整体结构VGG19来自牛津大学Visual Geometry Group在2014年提出的工作它在当年的ImageNet大规模视觉识别竞赛中取得了非常好的成绩而且思路极其简单从头到尾只用3×3的小卷积核通过不断堆叠卷积层来增加网络的深度每隔一段就用一次2×2的最大池化来降低特征图分辨率。整个VGG19的结构可以分成五个阶段Stage每个阶段包含若干个卷积层和一个池化层最后接三个全连接层Stage 12个卷积层3×364通道Stage 22个卷积层3×3128通道Stage 34个卷积层3×3256通道Stage 44个卷积层3×3512通道Stage 54个卷积层3×3512通道池化 Flatten三个全连接层4096、4096、1000这个设计的精髓在于没有花哨的分支结构没有各种不同尺寸的卷积核混用就是一个极其均匀的“卷积激活池化”堆叠。这种规整性让它很容易被复现、修改和移植也让它成为了后来许多任务中默认的骨干网络。1.2 为什么偏爱3×3小卷积核这里有一个值得好好琢磨的设计动机为什么不用5×5或7×7的大卷积核而非要叠好几个3×3核心原因有两点。第一感受野可以做到相同但参数量更少。单个5×5卷积的感受野是5×5但两个连续的3×3卷积叠加之后感受野同样是5×5单个7×7卷积的感受野是7×7而三个连续3×3卷积叠加感受野恰好也是7×7。用公式来看连续L个3×3卷积的感受野是 2L1所以 L2 时感受野是5L3 时感受野是7。参数量的差距非常明显单个7×7卷积的参数量是 49×C×CC为输入输出通道数而三个3×3卷积只有 3×9×C×C27×C×C参数直接少了约45%。第二更多的非线性层数让决策函数更有判别力。每多一层卷积中间都会接一个ReLU激活函数三个3×3卷积就意味着有三个非线性变换层而单个7×7卷积只有一个。非线性更强模型表达复杂特征的能力也更强。这也是VGG在当年能用“深度”取胜的根本原因。1.3 VGG19的参数量到底“大”在哪里VGG19总参数量大约在1.43亿个左右这个数字放在今天依然不算小。但你仔细拆开看会发现真正吃参数的大头并不是卷积层而是最后那三个全连接层。以ImageNet输入224×224为例经过五段池化之后最后一层卷积输出的特征图尺寸是7×7×512展平后就是7×7×512 25088 维第一个全连接层是25088×4096这一层就有1.02亿个参数几乎占总参数量的一大半。这也直接导致了两个结果第一个结果是模型非常吃显存因为全连接层的权重矩阵本身就很大第二个结果是它特别容易在小数据集上过拟合所以在训练时需要配合比较强的数据增强和正则化手段。后面我讲到训练实操时会再具体展开。2. 实现前的准备与整体设计VGG19实现起来并不复杂但如果你不想只是“调库跑通”而是真的把每个环节都理清楚动手之前还是要把整体方案选型想明白。工具、数据集、代码结构、以及到底是从零训练还是用预训练权重微调这些都是绕不开的决策点。2.1 环境与工具选型我用的是PyTorch原因有三点生态成熟官方torchvision里直接内置了VGG19的预训练权重和模型定义代码风格清晰非常适合学习社区资料多踩坑之后很容易找到解决方案。硬件方面VGG19最麻烦的就是显存和训练时间。我自己在实验室用一张RTX 309024GB显存跑过训练CIFAR-10完全没压力但如果是ImageNet这种224×224的大图单卡训练一轮要跑很久建议直接用云平台租卡或者分布式训练。没有GPU的读者可以先把代码跑通、把流程看懂真正训练时再想办法用GPU资源。CPU上想完整训完一个VGG19说实话不太现实光是一轮CIFAR-10可能就要按下性子等十多分钟。关于CUDA和PyTorch的版本建议PyTorch 2.x以上自带混合精度和更好的性能优化代码里通过torch.cuda.is_available()来检测GPU即可。2.2 数据集选型快速验证还是全量训练实现VGG19时选择数据集会直接影响你的实验策略和效果预期。我的建议是如果只是想学习和验证整个流程就用CIFAR-10或CIFAR-100如果想真正体验VGG19在分类任务上的威力或者后续要用预训练权重做迁移学习那肯定得上ImageNet子集或者直接用torchvision自带的ImageFolder加载完整ImageNet数据。CIFAR-10的特点是图片尺寸小32×32×3、类别少、数据量中等5万张训练图单卡训练很友好。但注意因为图片尺寸只有32×32VGG19原始的224×224设计其实并不能直接套用最大的影响是感受野和参数量不匹配。实践中通常有两种办法一是把CIFAR-10图片resize到224×224会丢一些细节且增加计算量二是保持32×32输入但这样后面几个Stage的特征图会比较小最后全连接层的输入维度也要跟着改。我在实际操作里更倾向于直接resize到224×224配合预训练权重来微调这样能最大程度复用ImageNet上学到的特征。2.3 整体代码结构与设计思路在写代码之前我建议把整个项目划分为几个模块数据集加载与预处理、模型定义、训练函数、验证函数、超参数配置、以及日志和可视化。这样分工清晰后期调参和排查问题都非常方便。在设计上我选择了“优先复用官方预训练权重再针对任务做迁移学习”的思路。原因很简单VGG19有1.43亿个参数如果从头训练不仅时间成本高而且在小数据集上很难收敛到理想效果。用torchvision内置的weights参数加载预训练权重再根据我们自己的类别数量去替换最后的全连接层这是最稳妥、最省力的路径。整个流程大概是定义数据集和数据增强 → 加载模型结构 → 替换分类头 → 选择合适的优化器/学习率/损失函数 → 训练若干轮 → 在验证集上评估。这套流程不管换什么模型换成ResNet还是EfficientNet都可以直接复用。3. 核心细节解析与实操要点VGG19的模型定义本身不难真正影响最终效果的是训练过程中的那些“软细节”。数据处理、超参数、优化器、正则化、以及预训练权重的合理利用每一点都很关键。我把这些实操中容易遇到问题的细节单独拆出来讲。3.1 数据预处理别小看归一化和数据增强数据预处理是整个实验里最容易被忽视、但影响最大的环节之一。VGG19在ImageNet上预训练时图片是按每个通道的均值 [0.485, 0.456, 0.406] 和标准差 [0.229, 0.224, 0.225] 做过归一化的。所以无论你是微调还是用预训练权重推理输入数据都必须用相同的均值和标准差做标准化否则权重完全不匹配效果会大打折扣。数据增强方面我常用的组合是RandomResizedCrop随机裁剪缩放、RandomHorizontalFlip随机水平翻转、以及ColorJitter颜色抖动。这些都是ImageNet系列模型训练时最基础也最有效的增强手段。要注意的是验证集和测试集不能做随机增强只用Resize到固定尺寸和Normalize即可。我在代码里会区分train_transform和val_transform两个流程避免把训练时的随机性带进评估环节。3.2 训练超参数的选择与调整超参数是训练深度学习模型时最玄学也最讲究的部分。我结合自己的实测结果给出一个对VGG19比较稳妥的起点配置Batch Size64或128。VGG19很吃显存如果GPU显存只有8GBbatch size设为32更安全。Epoch如果是微调30到50轮基本够用如果从头训练至少100轮以上。CIFAR-10微调我一般跑50轮。初始化学习率微调时推荐0.0011e-3从头训练时推荐0.011e-2。优化器SGD Momentum(0.9) Weight Decay(5e-4)。学习率策略CosineAnnealingLR或者StepLR每30轮衰减为原来的0.1。我实测CosineAnnealing更平滑更适合VGG这种容易震荡的大模型。这里特别想强调一下学习率的选择逻辑。微调时因为预训练权重已经把网络带到了一个比较好的局部区域过大的学习率非常容易把学好的特征破坏掉从头训练时则相反需要相对大一点的学习率来加快收敛。所以微调1e-3、从头训练1e-2这个经验值并不是拍脑袋定的。3.3 优化器选型为什么视觉任务首选SGDMomentum虽然Adam用起来省心、自适应学习率但在图像分类这种任务上我的实际体验是SGD加Momentum往往能取得更好的泛化效果。原因在于SGD的更新方向更“纯粹”配合适当的momentum设置能够更稳定地收敛到比较平坦的极小值Adam虽然初期收敛快但在后期容易出现训练震荡有时测试集精度反而不如SGD。当然这不是说Adam不能用于VGG19如果你不想花太多精力调学习率AdamW配合cosine衰减也能得到不错的结果。但如果你想尽量复现官方那种稳定的训练效果就优先考虑SGD Momentum。参数weight decay选5e-4左右这个值也是ImageNet分类实验里很常见的正则化设置能在不影响模型拟合能力的情况下有效压制过拟合。3.4 损失函数交叉熵的底层逻辑图像分类任务最常用的损失函数就是交叉熵。PyTorch里是nn.CrossEntropyLoss()它已经帮你把LogSoftmax和NLLLoss合并在一起计算了所以在模型最后的输出层不需要手动加Softmax。这一点很多新手容易搞混往往在最后一层又加了一个Softmax导致训练时梯度计算出问题损失值异常。为什么不用MSE做分类简单说MSE假设误差是高斯分布适合回归问题而分类问题输出的真实标签是离散的类别分布交叉熵直接度量预测分布与真实分布的差异梯度更新也更高效。所以分类任务直接无脑用CrossEntropyLoss就好。3.5 冻结与微调分阶段训练的最佳实践微调VGG19时一个常见的操作是先冻结前面若干层只训练后面的卷积层和分类头。这样做的好处是既利用了预训练模型已经学到的通用特征边缘、纹理、形状又避免小数据集上重新训练全部参数带来的过拟合风险。我常用的做法是第一步把整个网络的所有参数都冻结只训练替换后的新全连接分类头跑5到10轮让分类头先适应新任务第二步解冻最后两个Stage的卷积层以较小的学习率比如1e-4继续训练20到30轮第三步如果数据量仍然偏少进一步解冻更多层或者整体用更小学习率做全量微调。在实际CIFAR-10实验中这种方式比直接全部解冻从头微调要稳定得多验证集精度也普遍更高。模型定义和冻结操作的代码大致是这个样子import torch import torch.nn as nn import torchvision.models as models model models.vgg19(weightsmodels.VGG19_Weights.IMAGENET1K_V1) num_features model.classifier[6].in_features model.classifier[6] nn.Linear(num_features, 10) # 10类分类任务 # 冻结所有参数 for param in model.parameters(): param.requires_grad False # 解冻第3、4、5个Stage及分类头 # 具体层索引可以通过打印 model.features 确认 for param in model.features[24:].parameters(): param.requires_grad True for param in model.classifier.parameters(): param.requires_grad True4. 实操过程与核心环节实现这一部分我完整复盘一遍在CIFAR-10上实现VGG19的实际过程包括网络定义、数据加载、训练循环、验证评估以及我实测的训练时长、显存占用和精度变化。所有内容都是基于我自己的实验记录参数和结果可以直接参考。4.1 从零定义VGG19模型结构如果你不用torchvision自带的实现自己写一个VGG19也并不复杂。核心思路是先用一个make_layers函数根据配置字典生成连续的卷积层再接分类头。我通常直接用torchvision内置定义但为了帮助理解底层结构下面给出一个精简版的自定义实现方便你后续做结构修改import torch.nn as nn cfgs { VGG19: [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 VGG19(nn.Module): def __init__(self, num_classes10, init_weightsTrue): super(VGG19, self).__init__() self.features make_layers(cfgs[VGG19], batch_normTrue) self.avgpool nn.AdaptiveAvgPool2d((7, 7)) self.classifier nn.Sequential( nn.Linear(512 * 7 * 7, 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 self.avgpool(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)这段代码里用到了AdaptiveAvgPool2d((7, 7))它可以把任意尺寸的输入特征图统一池化到7×7这样分类头的输入维度就固定了。如果你要保持和官方完全一致输入图片就按224×224来池化后也是7×7。这里加入BatchNorm层能在训练时显著稳定梯度不过要注意加载官方预训练权重时如果你的模型结构和官方不一致权重都会对不上所以实际做微调时我还是直接用models.vgg19原版。4.2 数据加载与预处理完整代码数据部分我使用torchvision的CIFAR-10数据集整体流程分三步定义训练集和验证集的数据变换 → 用DataLoader加载 → 按batch迭代训练。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集数据增强 train_transform transforms.Compose([ transforms.Resize(224), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtrain_transform) val_dataset datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformval_transform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue)这里有两个细节需要特别留意。一是transforms.Resize(224)和RandomResizedCrop的组合能保证训练时裁剪的位置和比例有多样性同时验证时固定中心224区域这样评估指标才稳定二是pin_memoryTrue在GPU训练时能加快数据从内存拷贝到显存的速度对训练吞吐量有一定帮助。4.3 训练循环与验证评估的标准写法训练核心循环我习惯写成一个通用函数传入模型、数据加载器、优化器、损失函数和设备这样不管换什么模型都可以复用。需要特别注意的有两点每轮训练开始前调用model.train()每轮验证之前调用model.eval()这关系到Dropout和BatchNorm在训练/推理两种模式下的行为差异验证阶段用torch.no_grad()包裹避免计算梯度浪费显存和算力。import torch.nn as nn import torch.optim as optim from tqdm import tqdm device torch.device(cuda if torch.cuda.is_available() else cpu) model models.vgg19(weightsmodels.VGG19_Weights.IMAGENET1K_V1).to(device) model.classifier[6] nn.Linear(4096, 10).to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay5e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50, eta_min1e-6) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss 0.0 correct 0 total 0 for images, labels in tqdm(loader, descTraining): images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in tqdm(loader, descEvaluating): images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total for epoch in range(50): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() print(fEpoch {epoch1:03d} | Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f})这里我在每个epoch结束后调用scheduler.step()让学习率按照余弦策略逐步衰减。论文里常见的做法是每30轮衰减一次但在微调VGG19时余弦退火通常更省心因为它会自动把学习率慢慢降下去。4.4 实测训练记录与精度对比以CIFAR-10为例输入resize到224×224batch size为64单张RTX 3090上每个epoch大约需要15到25秒包括验证50轮训练大概15到25分钟结束。显存占用大约7到9GB非常稳定。如果batch size提升到128显存占用会接近14GB此时30系列以下显卡就容易爆显存。精度方面的实测结果直接加载ImageNet预训练权重后只替换分类头训练50轮的最终验证精度大约在92%左右如果从零开始随机初始化训练VGG19同样的CIFAR-10数据上只能跑到85%上下而且训练轮数需要大幅增加。这个差距很好地解释了为什么实际项目中几乎没人会从头训练VGG19预训练权重带来的特征迁移收益太明显了。下列是我一次典型实验的日志摘录记录了几个关键节点EpochTrain LossTrain AccVal Acc备注11.62030.42850.6172分类头刚训练整体精度较低100.54210.83540.8746最后几层开始适应新任务250.30170.91680.9043准确率稳定在90%以上400.18520.95340.9184训练集精度继续爬升500.14070.96580.9236验证集趋近收敛从日志里能明显看出微调过程中训练集和验证集精度都在上升但训练集精度的增速明显高于验证集到后期开始出现轻微过拟合的征兆。这个现象在VGG19上特别典型因为模型容量太大了。5. 常见问题与排查技巧实录VGG19的训练过程与更大模型相比已经算稳定但依然有不少经典问题。我把自己在实验里踩过的坑和排查思路整理出来做成一份速查表方便你在复现时遇到问题直接对照解决。5.1 训练不收敛或损失值震荡最常遇见的两个原因一个是学习率过大一个是数据归一化写错了。如果你训练刚开始loss直接飙到几十甚至几百或者loss曲线剧烈抖动那基本可以断定是学习率太大了先把学习率降一个数量级试试。如果是用预训练权重微调初始学习率超过0.01就可能破坏已经学好的特征我建议微调时初始学习率绝对不要超过0.001。另一个容易被忽视的原因是归一化时用了错误的mean和std。如果你用的是自己的数据集却套用了其他数据集的统计值网络前向传播出来的特征分布就会完全异常。解决办法很简单检查数据预处理代码里的mean和std确认和训练所用数据集一致。5.2 VGG19过拟合严重怎么办VGG19参数多、模型容量大如果在小型数据集上训练过拟合几乎是必然的。我见过不少新手在自己的数据集上跑VGG19训练集精度接近100%验证集却只有70%出头这就是典型的过拟合。针对这个问题有几个非常有效的措施数据增强RandomResizedCrop、RandomHorizontalFlip、ColorJitter尽量都用上尤其是小数据集。增加正则化模型自带Dropout层可以保留但要注意训练和验证模式的切换。此外适当增大weight decay到1e-3或5e-4也有帮助。提前停止在验证集精度不再提升且开始下降时停止训练我一般会保存验证集精度最高的模型权重而不是最后一轮的权重。“保存最优模型”是一个能救命的习惯。很多新手训练完直接保存最后一轮模型结果发现验证精度已经回落白白浪费了训练时间。我都是每个epoch评估一次记录验证集最好的参数然后另存一份best_model.pth最终测试用的都是这个最优权重。5.3 显存不足怎么办VGG19的全连接层对显存并不友好输入分辨率越高、batch size越大显存消耗就越明显。如果你的GPU只有6GB或8GB显存可以依次尝试下面几种方法降低batch size比如从64降到32或16观察显存占用再定。混用混合精度训练PyTorch的torch.cuda.amp可以自动将部分计算转为FP16大幅减小显存占用同时训练速度还有提升。我在3090上开AMP之后显存占用可以从9GB降到6GB左右。用梯度累积模拟大batch本地batch size设为16每4个batch累加一次梯度再更新等于是32的等效batch size。降低输入分辨率CIFAR-10如果从224降到112或者干脆用原始32显存占用会骤降。但注意这样预训练权重的适配性会受影响需要重新设计网络感受野。5.4 训练和推理时结果不一致BatchNorm的坑VGG19带BatchNorm的话有一个非常经典的坑训练时明明loss很低一旦切换到model.eval()做推理结果突然变差。这是因为BatchNorm在训练时会使用当前batch的均值和方差来归一化而在推理时使用的是训练阶段累积的全局均值和方差。如果你在推理前忘记了model.eval()BatchNorm仍然按训练模式处理均值和方差还是当前batch的输出自然不稳定。解决办法很简单训练循环里加model.train()验证和推理阶段加model.eval()。但如果你在训练循环里没有调用过model.train()那么Dropout层也会保持打开状态导致推理结果带有随机性表现也会异常。这一个点我在调bug时遇到过太多次了。5.5 加载预训练权重时尺寸不匹配使用torchvision的models.vgg19(weights...)后再替换分类头多数情况下不会出问题。但如果你自己定义了VGG19结构又想加载官方权重就会碰到state_dict中键名不一致或shape不匹配的问题。解决方法有两个方向第一尽量直接用官方models.vgg19定义只替换最后分类头第二如果必须自定义结构加载权重时把最后的分类头键过滤掉或者手动修改键名来对齐。例如官方分类头的键名是classifier.0.weight、classifier.3.weight、classifier.6.weight如果你的自定义分类头顺序不同加载时就会报错。我通常会在加载权重之前打印model.state_dict()的键手动核对一下再加载几秒钟的检查能省很多调试时间。6. 最后的经验小结与扩展建议VGG19的实现本身并不算复杂但它在整个深度学习视觉任务里的地位远不止“一个分类模型”这么简单。它的特征提取部分也就是前面那五个Stage可以直接迁移到很多其他任务中做目标检测时它可以作为YOLO或SSD的骨干网络做图像风格迁移时VGG19的低层特征保留纹理、高层特征保留内容这是风格迁移最经典的技术路线做相似图像检索时全连接层之前得到的特征向量也可以直接当图像嵌入使用。我个人在实际使用中最大的体会是VGG19是那种“慢但稳”的模型。它的推理速度确实比不上后来的轻量级网络结构很重、参数很多但正由于结构极度规整它在各种任务上表现都很稳定尤其适合作为基线模型用来验证一个新的想法或流程。如果你刚开始学深度学习不久我建议你把VGG19的每一个细节都跑通、吃透包括修改分类头、冻结部分层、观察不同学习率的训练曲线变化然后再去接触ResNet、Transformer这些更复杂的结构理解起来会顺手非常多。最后再分享一个小习惯每次实验前先把随机种子固定住保证结果可复现。这个细节虽然不起眼但在调参和对比实验时帮了我大忙能让每一次改动都看得清清楚楚。关于VGG19的实现就聊到这里下一步你可以试着把它替换成ResNet看看同样的训练流程会有哪些变化。
返回列表