ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深度学习图像分类入门:从VGG16卷积神经网络到PyTorch实战

深度学习图像分类入门:从VGG16卷积神经网络到PyTorch实战 深度学习、图像分类入门从VGG16卷积神经网络开始说出来有点不好意思我最早接触图像分类的时候也被各种名词砸晕过卷积、池化、步长、填充、感受野、特征图……看教程每个字都认识合在一起就不知道在说什么。后来真正把代码跑起来才意识到不是自己笨而是缺一个足够简单、足够规整的模型作为“锚点”。如果让我给刚入门的人推荐一个锚点我一定会说VGG16。这个模型在2014年ImageNet竞赛上拿了定位第一、分类第二之后几年成了学术界和工业界的“万金油”特征提取器。更重要的是它结构极其规整全部由3x3卷积、2x2池化和全连接层堆叠而成没有任何残差连接、注意力机制、多分支设计这些花活。正因为它“笨”反而最适合用来建立对卷积神经网络的直觉。这篇文章不是复述论文而是从“我想自己训练一个图像分类模型”的角度把VGG16拆开揉碎从原理讲到PyTorch实现再讲训练时你一定会遇到的坑和进阶路线。1. 为什么入门选VGG16而不是更“时髦”的模型1.1 结构规律性强到可以闭眼背下来我先说一个反直觉的事实VGG16真正厉害的地方不是它效果多好而是它把“卷积神经网络应该如何设计”这件事变成了一个有规律可循的套路。VGG16的“16”指的是16个带权重的层包括13个卷积层和3个全连接层。整个网络从输入到输出只有五段卷积块每一段卷积块后面跟一个池化层而且是同一个套路先做两次或三次3x3卷积再做一个2x2最大池化。前两段每段只有两个卷积层后面三段每段有三个卷积层。通道数从64涨到128再涨到256最后到512。全部都是倍数增长没有跳跃或者特殊处理。我第一次读完VGG16结构图以后闭上眼能把整个网络画出来。这种“规律性”对新手极其重要因为你可以把有限的脑力集中在理解每一层在做什么而不是去记忆复杂的网络拓扑。等你把VGG16吃透再去学ResNet、Inception甚至Transformer这些结构会轻松很多因为你有对比的基准了。1.2 和LeNet、ResNet相比VGG16对新手更友好的原因有些人可能说入门不都是LeNet吗LeNet确实更早手写数字识别效果也不错但它只有5层图像输入是32x32跟现代图像分类任务动辄224x224的输入不太一样。学完LeNet你仍然不知道面对一个真实数据集时怎么调整网络结构。ResNet则相反它引入了残差连接和BatchNorm效果确实好但如果你第一次接触CNN就直接上ResNet很容易被“为什么要有残差”“BatchNorm到底在干什么”这些问题牵扯精力反而忽略了卷积本身的工作机制。VGG16恰好处于一个中间位置结构深度足够能处理真实的中等分辨率图像结构套路简单没有绕弯的机制。它就像学车时的教练车配置不算豪华但每一项操作都有明确的教学目的。等你用VGG16跑通了图像分类的完整流程再去看ResNet的“捷径连接”你会有一种“哦原来它是为了解决网络变深之后梯度消失的问题”的顿悟感。1.3 预训练权重和非线性的分类头复用还有一个非常现实的原因VGG16的预训练权重在开源社区里到处都是PyTorch一行代码就能加载。这意味着你可以先拿ImageNet上训练好的权重做迁移学习训练自己的数据集效果通常都很不错。而且VGG16最后有一个非常“古典”的分类头——3层全连接层。今天很多新模型都是用全局平均池化代替全连接层但VGG16不是。全连接层的存在其实给新手提供了一个观察“特征如何变成分类打分”的窗口。你可以在代码里把某一层的输出打印出来看看一张图片经过网络后是怎么被“压扁”成一个4096维向量再映射到类别分数的。这个体验非常重要因为理解全连接层是理解“分类器”是怎么工作的第一步。2. VGG16分层拆解从像素到分类结果2.1 3x3卷积核到底在“看”什么很多人第一次接触卷积时最大的困惑是卷积到底对图像做了什么我用一个类比来解释卷积核就是一个滑动的小窗口每次只能看到图像的一小片区域但它会在这个小区域里做一次“加权求和”得到一个数值。窗口滑动到下一个位置再做一次同样的操作。如果这个窗口是3x3那它每次就“看”周围3x3共9个像素然后输出一个数——这个数可以理解为“这个局部区域长得像不像我要找的特征”。第一层卷积核通常学到的是边缘、颜色块这种非常底层的特征。比如某个卷积核可能对图像的水平边缘敏感另一个对垂直边缘敏感。到网络中间层卷积核会把底层特征组合成纹理、局部形状。到高层会抽象出更语义化的东西比如眼睛、车轮、窗户这种部件级别的特征。这就是为什么卷积网络被称为“从像素到语义”的层级抽象。VGG16全部使用3x3卷积核这本身是一个重要的设计决策。用两个3x3卷积堆叠感受野等价于一个5x5卷积三个堆叠等价于7x7卷积。但3x3卷积的参数更少而且中间多了一次非线性激活表达能力更强。VGG16通过堆叠小卷积核取代大卷积核网络可以做得更深同时参数量不涨反降。2.2 池化层的“压缩”作用池化层在VGG16里很简单每段卷积块之后都是一个2x2的最大池化步长为2。这意味着特征图的宽和高都减半。224x224的输入经过第一段池化变成112x112然后变成56x56再变成28x28、14x14、7x7。为什么要压缩两个原因。第一个原因是增大感受野。想象你在一个14x14的特征图上看一个像素它能“感受”到的原始图像范围肯定比在112x112特征图上看一个像素要广。池化让网络每一层能处理的信息范围不断扩大这是特征从局部到全局的关键。第二个原因是降低计算量。通道数一直在翻倍如果特征图尺寸不变计算量会爆炸式增长。池化把空间尺寸减半通道翻倍整体计算量基本保持平稳这也是VGG16虽然层数比现代网络多但训练起来还算可控的原因。最大池化就是取窗口里最大的那个值等价于“保留这一小片区域里最强烈的激活”。这个操作没有引入额外参数而且比平均池化更能保留纹理和边缘这类高频信息所以VGG16选择的是最大池化。2.3 全连接层的“打分”逻辑经过五段卷积和池化后一张224x224x3的输入图片会变成一个7x7x512的特征图。这时候数据已经从“图像”变成了“空间分辨率很低但通道数很深”的特征集合。接下来发生的事情很多人理解得不够透彻特征图被展平成一个一维向量长度是7x7x512 25088然后送进三层全连接层。全连接层做的事情本质上就是对展平的向量做加权求和。它不再考虑空间位置而是把整张图的特征“汇总”成全局表示。4096维的隐藏层可以看作一个能力很强的编码器把特征压缩成一个更紧凑、更有判别力的向量。最后一层全连接的输出维度等于类别数比如分类1000类就输出1000个值。这些值经过Softmax变成概率分布取概率最大的那一类就是预测结果。整个过程如果形象一点说卷积部分是在“看”图全连接部分是在“做决定”。2.4 步长、填充与特征图尺寸的完整推演新手最怕算尺寸我在这里把VGG16每一层的输出尺寸完整推演一遍你跟着走一遍就能彻底明白。先记住两个公式。卷积层输出尺寸 (输入尺寸 - 核大小 2 * 填充) / 步长 1。池化层同样适用。VGG16所有卷积层都是3x3核、步长为1、填充为1。代入公式(224 - 3 2) / 1 1 224。也就是说如果输入输出尺寸不变卷积层不会改变特征图的宽高。池化层是2x2核、步长为2不填充代入公式(224 - 2) / 2 1 112。也就是说池化层把尺寸减半。所以整个VGG16前向过程的尺寸变化非常规律224 - (卷积两次不变) - 112 - (卷积两次不变) - 56 - (卷积三次不变) - 28 - (卷积三次不变) - 14 - (卷积三次不变) - 7。最终得到7x7x512特征图。为什么代码里全连接层的输入维度是512 * 7 * 7就是从这里算出来的。很多新手报错“mat1 and mat2 shapes cannot be multiplied”就是因为在自定义网络时没算对这一步的尺寸。如果你改过输入图尺寸或者卷积参数这里必须跟着改否则全连接层的权重维度对不上。3. 动手前的准备环境、数据格式与预训练权重3.1 PyTorch环境配置的几个关键点这里我不推荐把时间花在从零搭建环境上直接用Anaconda建一个独立环境最省事。conda create -n vgg16 python3.9 conda activate vgg16 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install matplotlib jupyter如果你的机器有NVIDIA显卡装CUDA版本支持的PyTorch训练速度快很多倍。没有显卡用CPU跑也不是不行但建议把图片resize到更小的尺寸再训练不然全连接层那三个大矩阵乘会把你等哭。装完验证一下PyTorch能不能识别CUDAimport torch print(torch.__version__) print(torch.cuda.is_available())torch.cuda.is_available()返回True说明GPU可用。如果返回False多半是PyTorch版本和CUDA版本不匹配。一个非常容易踩的坑是下载预训练权重时网络超时。torchvision.models.vgg16(pretrainedTrue)会自动下载权重但下载源在国外经常卡住。解决办法是提前手动下载权重文件放到缓存目录或者直接用国内镜像源下载后放到~/.cache/torch/hub/checkpoints/下。权重文件大约528MB文件名是vgg16-397923af.pth。3.2 数据集的目录组织与预处理我建议用一个真实的小数据集跑通全流程而不是直接用ImageNet那种超大库。经典的入门选择是牛津花卉数据集17类或者102类类别数量适中图片内容差异明显非常适合观察模型是否学懂。目录结构按PyTorch的ImageFolder约定来组织即可data/ train/ class1/ img1.jpg img2.jpg class2/ ... val/ class1/ class2/ ...使用ImageFolder读取非常方便from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), 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.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_data datasets.ImageFolder(data/train, transformtrain_transform) val_data datasets.ImageFolder(data/val, transformval_transform)这里有两个值得说的细节。第一个是训练集用了RandomResizedCrop它会随机裁剪图片的一部分并resize到224x224相当于给模型看了同一张图的不同局部是一种非常有效的数据增强手段。验证集不能用随机裁剪因为那是评测必须每次看到完整一致的图像所以用Resize加CenterCrop。第二个是Normalize的参数。[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]是ImageNet数据集的均值和方法差因为预训练权重是在ImageNet上训练的输入数据服从同样的分布效果最好。如果你从零训练理论上应该根据自己的数据集重新统计均值方差但直接用ImageNet的也完全可以。3.3 迁移学习还是随机初始化这是一个战略选择第一次训练我强烈建议用ImageNet预训练权重。VGG16参数量约1.38亿全连接层的参数占了大部分。在小型数据集上从零训练极容易过拟合而且收敛很慢。用预训练权重做迁移学习相当于模型已经有了很强的“看图”能力你只需要微调它适配你的任务。PyTorch里加载预训练权重的推荐方式是import torchvision.models as models model models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) # 替换最后一层适配你的类别数 model.classifier[6] torch.nn.Linear(4096, num_classes)这里有个坑model.classifier[6]是最后一层注意classifier是一个Sequential下标0到6一共七层其中6才是最后那个输出层。不能直接把整个classifier替换掉否则预训练的全连接层参数就白加载了。如果你有特别大的数据集或者想更深度地适配任务可以把最后几层全连接也加入微调那属于进阶玩法后文再讲。4. 用PyTorch从零定义VGG16并训练自己的图像分类模型4.1 模型定义手动搭一遍比调用现成函数更有价值虽然torchvision.models.vgg16()一行代码就能拿到模型但我还是建议你手动把VGG16的类写一遍。因为这个过程会让你对网络结构产生肌肉记忆。下面是一个完整的VGG16定义我把卷积块写成了一个辅助函数这样代码更清晰import torch import torch.nn as nn def conv_block(in_channels, out_channels, num_layers): layers [] for i in range(num_layers): in_ch in_channels if i 0 else out_channels layers.append(nn.Conv2d(in_ch, out_channels, kernel_size3, padding1)) layers.append(nn.ReLU(inplaceTrue)) return nn.Sequential(*layers) class VGG16(nn.Module): def __init__(self, num_classes1000): super().__init__() self.features nn.Sequential( conv_block(3, 64, 2), # 第一段2个卷积层 nn.MaxPool2d(kernel_size2, stride2), conv_block(64, 128, 2), # 第二段2个卷积层 nn.MaxPool2d(kernel_size2, stride2), conv_block(128, 256, 3), # 第三段3个卷积层 nn.MaxPool2d(kernel_size2, stride2), conv_block(256, 512, 3), # 第四段3个卷积层 nn.MaxPool2d(kernel_size2, stride2), conv_block(512, 512, 3), # 第五段3个卷积层 nn.MaxPool2d(kernel_size2, stride2), ) 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), ) def forward(self, x): x self.features(x) x torch.flatten(x, 1) # 展平为 [batch, 512*7*7] x self.classifier(x) return x这里值得停一下解释几个新手容易忽略的点。inplaceTrue是一种内存优化让ReLU直接修改输入张量而不是创建新张量能省一些显存。VGG16本来就很占显存能省一点是一点。torch.flatten(x, 1)的意思是保留batch维度第0维从第1维开始展平。如果不做这一步全连接层会报维度错误。Dropout在这模型里非常重要。全连接层有4096维参数极多在小型数据集上没有任何正则化手段的话几乎必过拟合。Dropout(0.5)在训练时随机丢弃一半神经元逼着网络学冗余的特征表达是VGG16在分类阶段防止过拟合的主要手段。4.2 数据加载与训练的完整流程模型定义好之后用一句话验证前向能不能跑通model VGG16(num_classes17) x torch.randn(2, 3, 224, 224) y model(x) print(y.shape) # 应该是 torch.Size([2, 17])输入是2张224x224的RGB图输出是2行17列的打分矩阵。这个验证很重要很多结构错误在训练前就能暴露出来。接下来定义训练相关的组件import torch.optim as optim from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9)损失函数用交叉熵这是多分类的标准选择。优化器我推荐SGD带动量虽然Adam收敛更快但在图像分类上SGD的最终效果通常更好而且L2正则的手感更可控。如果你用的是预训练权重只替换了最后一层那应该给最后一层更大的学习率给前面的层较小的学习率。原因很简单前面的卷积特征提取能力已经经过ImageNet验证了不需要大幅度改动而最后一层是随机初始化的需要更多更新。写法是optimizer optim.SGD([ {params: model.features.parameters()}, {params: model.classifier[6].parameters(), lr: 0.01}, ], lr0.001, momentum0.9)最后是训练主循环train_loader DataLoader(train_data, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_data, batch_size32, shuffleFalse, num_workers4) num_epochs 20 best_acc 0.0 for epoch in range(num_epochs): model.train() running_loss 0.0 correct 0 total 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() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_loss running_loss / total train_acc correct / total # 验证阶段 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_acc val_correct / val_total print(fEpoch {epoch1}/{num_epochs}, fTrain Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f}, fVal Acc: {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_vgg16.pth)这里的几个细节值得说清楚。model.train()和model.eval()是必须的切换。train()模式下Dropout和BatchNormVGG16没有BatchNorm但你将来用其他模型会有的行为在训练和推理时不同。不切换的话验证结果会偏大或偏小造成误判。torch.no_grad()在验证时关闭梯度计算大幅节省显存和计算。很多人训练到一半显存不够往往是忘了在验证时关梯度。只保存验证精度最高的模型而不是最后一轮的模型。训练后期难免会有波动最后几轮不一定是最优的用“早停式保存”能拿到最好的那一版参数。你花的训练时间没有变多但拿到手的模型质量会提高。4.3 数据集规模不够时怎么把VGG16用起来如果你手上的图像很少每种几十张那种直接全量微调VGG16依然会过拟合。我试过最有效的方案是冻结卷积层只训练分类头。for param in model.features.parameters(): param.requires_grad False # 只更新分类器的参数 optimizer optim.SGD(model.classifier.parameters(), lr0.001, momentum0.9)这样做的原理是ImageNet上训练好的卷积层已经具备了提取通用视觉特征的能力边缘、纹理、形状这些特征在大多数图像任务中是通用的。你需要学习的只是怎么用这些特征来区分你自己的类别。把卷积层冻结只训练最后的分类层本质上是在拿预训练网络当特征提取器用参数量从1.38亿骤降到约1亿零6万主要是4096x4096和4096x17这两个矩阵训练速度和过拟合风险都会好很多。当数据量变多再逐渐解冻更深层的卷积做微调每个实验都比全量训练省时省力。5. 训练完看曲线怎么判断模型学没学好5.1 损失曲线和准确率的四种典型形态训练结束后很多人只关心最终准确率但我建议你养成把每轮train loss、train acc、val acc画出来的习惯。一张图能告诉你很多数值统计表看不出的信息。我总结过四种典型形态。第一种是train loss持续下降train acc和val acc同步上升。这是最理想的状态模型在正常学习说明数据预处理、学习率、模型结构都没大问题让训练继续即可。第二种是train loss下降train acc接近100%但val acc在某个点后不再上升甚至下降。这是过拟合的典型信号。模型已经把训练集“背”下来了但对没见过的图泛化能力差。对策是增强数据增强、提高Dropout比例、减小模型容量或者早停——在val acc开始下降前就终止训练保存之前的最佳模型。第三种是train loss和val loss都不怎么下降train acc一直在低水平震荡。这种一般不是过拟合而是欠拟合模型还没学会。原因常常是学习率太低收敛太慢、模型被错误冻结或者数据标签有严重错误。第四种是train loss先降后升val acc完全不动。这种多半是学习率过大训练后期在损失曲面振荡参数跳出了最优区域。可以试试学习率衰减比如每5轮乘以0.1或者从一开始就用余弦退火调度器。5.2 我在花卉数据集上的实际调参记录拿17类花卉数据集说我跑了三组对比实验。第一组用ImageNet预训练权重冻结卷积层只训练分类头SGD学习率0.001跑了20轮。结果val acc从第一轮的82%慢慢爬到91%左右就不再动了。因为卷积特征是通用的花的边缘、花瓣纹理这些特征ImageNet里都有所以初始精度已经很高但只训练分类头表达能力上限就这么高。第二组解冻最后一段卷积块第五段的三个卷积层和分类头一起微调学习率统一用0.0001。20轮后val acc达到了94.5%。这里学习率一定要小因为微调阶段是在一个已经很优化的参数空间附近做局部搜索步子迈大了容易跳出好区域。第三组全量微调所有层学习率0.0001跑了20轮。val acc反而掉到了93%左右而且训练时间明显变长。原因很简单17类花卉数据集只有几千张图不足够支撑1.38亿参数的充分训练。虽然ImageNet预训练给了好的起点但把所有层放开之后模型有了更多可以“破坏”原有好特征的空间。所以我的结论是小数据集上部分解冻微调是性价比最高的方案。具体解冻多少层取决于数据量大小和任务跟ImageNet的相似度。数据越多、任务差异越大就解冻更多层。5.3 学习率、Batch Size与优化器的取舍这里把我常用的参数给一个参考基于PyTorch实现。学习率是影响训练成败的第一因素。从头训练VGG16学习率0.01是一个相对安全的起点迁移学习微调0.0001到0.001之间。判断方法很简单跑两三个batch如果loss完全不动说明学习率太小如果loss直接变成NaN或者爆炸式增长说明学习率太大。Batch size受显存限制。VGG16在224x224输入下batch size32大约需要6到8GB显存具体取决于是否用混合精度。如果爆显存优先把batch size降到16同时考虑把训练图尺寸从224降到192或160。显存不足时降batch size比降分辨率对精度的影响更小。优化器选择上我用SGDmomentum(0.9)的时候通常配合学习率衰减用Adam时则不太需要衰减但Adam在图像分类上的最终收敛效果往往不如调好的SGD。这也算一个圈内共识Adam适合快速看到效果SGD适合追求最终精度。6. 那些没人提醒你但一定会踩的坑6.1 输入尺寸搞错导致的维度爆炸VGG16全连接层要求输入特征图是7x7x512这意味着网络输入必须是224x224。如果你给网络输入256x256的图片前面卷积池化都没问题到最后展平时会变成9x9x512全连接层直接报维度不匹配错误。很多教程会在数据预处理阶段用Resize(224)但如果你从别的代码库搬来一个只做了Resize(256)的数据流水线这个错会准时出现。我建议在使用任何预训练模型之前先打印一下自己数据集的张量形状确认是(batch, 3, 224, 224)再进模型。6.2 显存溢出的自救方案VGG16是出了名的“显存刺客”原因有两个第一个是卷积层中间特征图的尺寸大分辨率高224x224的特征图每张都要保存用于反向传播第二个是全连接层的前两个权重矩阵都是4096x4096占用的参数量异常惊人。显存溢出时的自救顺序是第一把batch size减半这招解决90%的问题第二开启梯度积累用多个小batch合成一个大batch的梯度等效于大batch训练accumulation_steps 4 for i, (images, labels) in enumerate(train_loader): outputs model(images) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()第三用PyTorch的自动混合精度简单几行代码就能省一半显存scaler torch.cuda.amp.GradScaler() with torch.autocast(device_typecuda): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()有朋友问为什么这么老的模型还占显存其实这不只是VGG16的问题任何网络训练时都要为中间激活值预留显存而VGG16的卷积层又深又宽特征图多占得多也正常。6.3 迁移学习时“冻结”不等于“不更新”初学者容易搞混两个概念requires_gradFalse和model.eval()。requires_gradFalse是告诉PyTorch不要计算这些参数的梯度训练时不会更新它们。model.eval()是切换BatchNorm和Dropout的运行模式。如果模型里有BatchNorm层训练模式会用当前batch的统计量测试模式会用累计的全局统计量。只用requires_gradFalse而不切model.eval()冻结层里的BatchNorm依然会用训练模式跑会让验证结果非常不稳定。我把冻结层和eval切换当成两件独立的事来管理写代码时在训练循环里显式控制。这个习惯让我少踩过很多莫名其妙的精度坑。6.4 类别不平衡导致的精度虚高如果你的数据集里类别样本数差别很大光看整体准确率很容易被“虚高”。比如二分类A类占90% B类占10%模型全预测A类准确率也有90%但这显然不是好模型。处理不平衡数据最简单有效的办法是记录混淆矩阵或者每类召回率而不是只看val acc。另一个办法是在DataLoader里给每类设置采样权重让每个batch尽量均衡。weights [1.0 / class_sample_count[c] for c in labels] sampler torch.utils.data.WeightedRandomSampler(weights, len(weights)) train_loader DataLoader(train_data, batch_size32, samplersampler)类别不平衡在真实业务数据里几乎必然出现这个习惯越早养成越好。7. 从VGG16出发下一步往哪走7.1 对VGG16做轻量化剪枝与全连接层替换VGG16有一个天然的问题推理速度慢、模型体积大一个权重文件500多MB部署到移动端基本不现实。入门阶段可以把“给VGG16瘦身”当成第一个进阶练习。最简单的瘦身方法是把全连接层替换成全局平均池化。既然卷积层已经提取了有用的特征为什么还要用三个巨大的全连接层去“消化”它们呢用一个nn.AdaptiveAvgPool2d((1, 1))把特征图压成一个向量再接一个单层线性分类头参数量瞬间减少一个量级精度损失通常非常小model.features # 复用卷积部分 model.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(512, num_classes) )更深一层的玩法是结构化剪枝。VGG16的卷积通道是均匀的很多通道对最终分类的贡献非常小。可以用BN层的gamma值作为通道重要性的衡量标准把gamma值小的通道直接剪掉整个网络就变窄了。原理虽然不复杂但实现涉及网络重参数化对PyTorch底层API的理解要求比较高适合作为第二个进阶项目。剪枝和轻量化做完后你会发现精度可能掉了一两个点但模型体积从528MB降到100MB以内推理速度翻倍。这就是工程能力。7.2 从VGG16到ResNet再到TransformerVGG16教会你的是“经典CNN怎么工作”而2015年的ResNet解决的是“网络能不能更深”的问题。ResNet引入残差连接让梯度能通过捷径直接回传到浅层这是VGG16没有的能力。在ImageNet这种超大数据集上ResNet的深度优势会完全展现出来。再往后Vision TransformerViT是另一套思路把图像切成16x16的小块当作“token”输入Transformer编码器让注意力机制自己去建模图像各区域之间的关系。ViT在数据量非常充足时能超过CNN但数据量少时不如CNN稳定。作为从VGG16走过来的新手我的建议是学完VGG16之后按ResNet - EfficientNet - ViT的顺序去拓展。每个新模型都跟VGG16做对比你就能深刻理解模型演进的核心驱动力是什么不是为复杂而复杂而是为了在精度、速度、参数量之间找到更好的平衡。7.3 图像分类的下一步目标检测与语义分割图像分类只是视觉任务的基础。当你对分类模型的前向传播、反向传播、微调流程轻车熟路之后目标检测和语义分割就是自然的延伸。目标检测要回答“图里有什么 在哪个位置”主流方法有YOLO系列和Faster R-CNN系列它们都大量借用CNN骨干网络提取特征VGG16就是一个绝佳的骨干起点。语义分割是像素级别的分类全卷积网络FCN就是从VGG16的卷积部分出发做的改造。学习这个方向的时候你会有一种很强烈的“复用感”原来在分类里学到的特征提取、数据增强、训练技巧换一个输出头就能迁移到新的任务上。这就是深度学习的底层逻辑——表示学习。VGG16作为一个可靠的特征提取器教会了你这一整套思维模式。最后分享一点个人体会模型会过时结构会有更优解但你通过手写VGG16建立起来的“看网络结构——推尺寸——写代码——训练——看曲线——调参数”这套闭环能力是永不过时的。把这个流程走通一遍比刷十篇论文都管用。入门最快的路径永远是找一个小数据集亲手从定义模型开始跑一版然后看着损失从2.5降到0.3准确率从40%爬到90%那种感觉会推着你继续往前走。
返回列表