ARTICLE DETAIL

资讯详情

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

CNN图像分类实战:VGG与ResNet核心原理及PyTorch实现

CNN图像分类实战:VGG与ResNet核心原理及PyTorch实现 简介《计算机视觉图像分类》是一份面向人工智能入门者及计算机视觉学习者的PPTX教学课件系统讲解图像分类中的核心模型。课件从卷积神经网络(CNN)的基础原理入手以具体张量Shape示例说明输入图像、滤波器尺寸及输出特征图的变化并结合ReLU激活函数解释特征提取过程随后逐一剖析VGG网络通过堆叠3×3小卷积核与2×2最大池化构建深度结构的思路以及ResNet残差网络如何利用F(x)H(x)-x解决深层网络退化问题。内容兼顾理论与可视化示意适合课堂教学、自学复习或备课参考。资源为单个pptx文件压缩包大小1.07MB便于下载使用。目前已有358人学习适合需要快速建立图像分类知识框架的读者。1. 一份图像分类课件为什么值得你重新翻一遍做计算机视觉的同行对这类教学 PPT 大概都不陌生讲 CNN 基础、讲 VGG、讲 ResNet配几张网络结构图最后给个图像分类的收尾案例。如果你只是课件使用者把卷积、池化、残差这几个名词背下来就能过考试但如果你想在真实数据集上把图像分类跑通或者准备计算机视觉大作业、课程设计这份讲义里的细节就非常值得抠一抠——尤其是 X shape(8,8,3)、4 个 filters shape(3,3,3,4) 到输出 Z1 shape(6,6,4) 这一整套维度推导它恰恰是理解 CNN 前向传播、后面接 VGG/ResNet 乃至设计自己的图像分类模型的关键。这篇文章把课件里没展开的计算过程、VGG 堆叠 3×3 小卷积核的动机、ResNet 残差块解决退化问题的工程边界以及一份能直接改跑的 PyTorch 训练脚本都补全适合正在入门深度学习和做图像分类项目落地的人。2. 从 8×8×3 到 (6,6,4)CNN 卷积层与 ReLU 激活的完整推导2.1 输入张量的四个维度到底在描述什么课件里给的输入是 Xshape(8,8,3)这个表述在计算机视觉课程里非常典型指的是单张图片高 8 像素、宽 8 像素、3 个通道。实际工程里我们几乎不会把单张图直接送进网络而是以 batch 为单位所以 PyTorch 里输入张量的标准形状是 (N, C, H, W)N 是批大小C 是通道数。课件把 N 省略掉了初看没问题但当你用框架复现代码时维度对不上就会立刻暴露出来。更值得留意的是 filters 的写法。课件说 4 个 filters shape(3,3,3,4)这里的四个数字依次是卷积核高 3、卷积核宽 3、输入通道数 3、滤波器个数 4。在 PyTorch 的 Conv2d 中权重张量的形状是 (out_channels, in_channels, kernel_h, kernel_w)也就是 (4, 3, 3, 3)。顺序不一样但含义完全对应。很多人在这一步栽跟头直接把课件里的 shape 塞进框架发现参数对不上其实只是维度排列习惯不同。卷积输出尺寸公式也来自这里。输入 8×8卷积核 3×3步长 stride1没有 padding那么输出边长是 8−316所以 Z1 是 (6,6,4)。4 个滤波器各自产出一个 6×6 的特征图叠加成 4 个通道。这个推导是最基础的但也是后面理解 VGG 为什么堆 3×3、ResNet 为什么用 stride2 下采样的前提。2.2 用 PyTorch 复现这一次卷积前向传播把课件里的计算落成可执行代码核心代码如下。这里我故意把 padding0、stride1 保持默认使它和课件的维度推导一致import torch import torch.nn as nn torch.manual_seed(0) # 输入: batch_size1, channel3, height8, width8 x torch.randn(1, 3, 8, 8) # 卷积层: 4个输出通道, 卷积核3x3, 输入通道3自动推断 conv nn.Conv2d(in_channels3, out_channels4, kernel_size3, padding0, stride1) z1 conv(x) print(Z1 shape:, z1.shape) # 预期 torch.Size([1, 4, 6, 6]) # ReLU 激活, 把负值置零, 引入非线性 a1 torch.relu(z1) print(A1 shape:, a1.shape) # 预期 torch.Size([1, 4, 6, 6]) print(Z1 中的负值数量:, (z1 0).sum().item()) print(A1 中的零值数量:, (a1 0).sum().item())这段代码的逻辑说明Conv2d 的 in_channels3 对应输入图的 RGB 三通道out_channels4 对应课件里的 4 个滤波器kernel_size3 对应每一个滤波器的空间尺寸。padding0 保证输出尺寸严格按 8−316 收缩这样你才能亲眼验证课件的维度推演与实际计算一致。ReLU 层的输入是 Z1输出是 A1形状保持 (6,6,4) 不变它只对每个元素做 max(0,x) 的截断操作。参数说明里有一个容易忽略的点这层卷积的参数量是 3×3×3×4108 个权重外加 4 个偏置共 112 个参数。这个数字对理解后面 VGG 的参数量对比非常关键——同样感受野下小卷积核的参数量优势从这里就开始显现了。2.3 输出尺寸收缩带来的两个工程问题如果你试过把卷积层叠加起来训练图像分类模型会发现每过一层特征图边长都在缩小。8→6 只是损失 2 个像素但如果连续堆 10 个 3×3 卷积而不做任何处理特征图会迅速缩到无法继续计算。VGG 的办法是每堆几层卷积就接一个 2×2 最大池化把边长减半的同时加倍通道数保持信息容量不衰减。而后来很多分类模型干脆在卷积层里直接设 stride2或者用 padding1 让卷积不改变尺寸把下采样任务单独交给池化层或带步长的卷积。这里引出一个常见误区有人看到课件里的 (8,8,3)→(6,6,4)→(4,4,4) 这类尺寸变化会误以为 CNN 一定会不断缩小空间尺寸。实际上 padding 可以精确控制输出尺寸当 kernel3、padding1、stride1 时输出与输入尺寸完全一致。ResNet 大量使用这种配置来保持特征图尺寸稳定只在特定层用 stride2 完成下采样。提示课件里省略 batch 维度的写法在教学上方便但在工程里写代码时一定要在开头就统一用 (N,C,H,W) 的思维否则拼接、池化、全连接层的维度会对不上。3. VGG 的 3×3 堆叠逻辑深度、感受野与参数量的三角平衡3.1 为什么是 3×3而不是 5×5 或 7×7VGG 网络的核心贡献是系统验证了网络深度与分类性能之间的关系。它反复堆叠 3×3 的小卷积核和 2×2 的最大池化层把网络做到 16 到 19 层。这里有一个很重要的推导两个连续的 3×3 卷积其感受野等价于一个 5×5 卷积三个连续的 3×3 卷积感受野等价于一个 7×7 卷积。但参数量完全不同一个 7×7 卷积在 C 个输入输出通道下的参数量是 49×C²而三个 3×3 卷积是 3×9×C²27×C²少了将近一半。参数量的节省还不是全部。三个 3×3 卷积中间夹着两个 ReLU 激活层这意味着网络有了更多的非线性变换能力。换句话说你在不增加参数负担的前提下白赚了更多的非线性表达。这一点对图像分类这种高度非线性的任务非常关键。VGG 的另一个设计惯例是每次经过 2×2 最大池化把空间尺寸减半后通道数就翻倍从 64 逐步涨到 512。空间信息逐步压缩语义信息逐步增强这是 CNN 处理图像分类任务的基本节奏。3.2 从配置表到网络结构的构建方式VGG 论文里用配置表区分不同深度的网络VGG16(D 配置)和 VGG19(E 配置)是最常用的两个变体。下面的表格列出两者的层数差异方便对照实现配置卷积层数全连接层数总层数典型用途VGG16 (D)13316图像分类、特征提取、迁移学习VGG19 (E)16319更深场景下的分类与检测骨干课件的定位是《人工智能应用基础》所以 VGG 部分不会深入到实现层面但作为工程实践理解配置和代码的映射关系更实用。下面用 PyTorch 按 VGG 的思想搭建一个可配置的特征提取器卷积层部分完全复现 VGG 的堆叠策略后面接全局平均池化和分类头import torch.nn as nn def vgg_block(channel_list, in_channels3): layers [] for out_channels in channel_list: layers.append(nn.Conv2d(in_channels, out_channels, kernel_size3, padding1)) layers.append(nn.ReLU(inplaceTrue)) in_channels out_channels layers.append(nn.MaxPool2d(kernel_size2, stride2)) return nn.Sequential(*layers) # VGG16 风格的卷积通道配置: 每池化一次, 通道数翻倍 channels [64, 64, 128, 128, 256, 256, 256, 512, 512, 512, 512, 512, 512] conv_features vgg_block(channels) print(conv_features) # 这里没有写全连接层, 而是用全局平均池化缩小特征图后接分类头 classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(512, 10) )代码逻辑说明vgg_block 接收一个列表列表里的每个数字代表一层卷积的输出通道数两个连续的相同通道数卷积之后接一次最大池化。padding1 让卷积不改变特征图尺寸空间缩小完全交给 MaxPool2d(2,2)。最终所有特征图被 AdaptiveAvgPool2d 压成 1×1再展平后接全连接层分类。相比原版 VGG 的三个大全连接层这种写法大幅减少参数量训练起来也更不容易过拟合适合在自定义数据集上做图像分类实验。参数说明AdaptiveAvgPool2d((1,1)) 是全局平均池化它把任意尺寸的输入特征图都压成 1×1这样全连接层的输入维度就固定为 512不用关心输入图片的原始尺寸。Flatten 负责把形状从 (N,512,1,1) 展平成 (N,512)。Linear(512,10) 里的 10 对应你数据集的类别数换成自己的任务时只需要改这个数字。3.3 全连接层吃掉了大部分参数怎么在工程里规避VGG 最常被诟病的点在全连接层。原版 VGG16 的三个全连接层参数量加起来超过一亿占整个网络的绝大部分。而卷积层只占一小部分。在工业界的图像分类实践中直接训这么大一个网络是不现实的常见做法是只保留 VGG 的卷积部分作为特征提取器后面接一个轻量分类头或者直接拿在 ImageNet 上预训练好的权重做迁移学习冻结卷积部分只训练分类层。提示课件里 VGG 讲的是 16-19 层深度和 3×3 堆叠理论但你在自己的项目里复现时不需要照搬全连接层。用全局平均池化代替全连接已经是大多数现代分类模型的标配VGG 原版的 FC 层更多是历史产物。4. ResNet 残差块退化问题的成因与 PyTorch 实现要点4.1 退化问题的本质是优化困难不是过拟合ResNet 要解决的是网络加深后的性能退化问题。直觉上网络越深表达能力越强分类准确率应该越高。但实验发现当层数超过某个阈值后训练集上的误差反而比浅层网络更高。这不是过拟合——过拟合时训练误差通常很低。ResNet 论文里对此有一个经典解释对于浅层网络已经能做到的最优映射 H(x)深层网络理论上也应当能做到因为多余的层只要学一个恒等映射即可。但在实际训练中让堆积层直接拟合恒等映射非常困难于是 ResNet 把学习目标从 H(x) 改成残差 F(x)H(x)−x。这样一改网络要学的东西就变成了“相对于输入的增量”。如果恒等映射是最优的网络只需要让残差趋近于 0而让残差趋于 0 比让一堆非线性层直接拟合一个恒等映射容易得多。课件里提到的“当残差为 0 时堆积层做了恒等映射至少网络性能不会下降”指的就是这个逻辑。当然实际场景里残差不会恰好为 0网络会在输入特征的基础上继续学习新的特征这也是 ResNet 在图像分类任务上效果好的核心原因。4.2 残差块的工程实现shortcut、BN 与激活顺序残差块的标准结构是两条路径一条是主路卷积→BN→ReLU→卷积→BN另一条是 shortcut直接跳过主路把输入加到输出上。当主路和 shortcut 的特征图尺寸不一致时比如通道数翻倍或空间尺寸减半就需要在 shortcut 上加一个 1×1 卷积做投影。下面是 BasicBlock 的 PyTorch 实现这是 ResNet18/34 的基本组成单元import torch.nn as nn import torch.nn.functional as F class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) # shortcut 投影: 通道数或尺寸变化时, 用 1x1 卷积对齐 self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity self.shortcut(x) out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out F.relu(out identity) return out这段代码的逻辑说明前向传播时输入 x 先走主路做两次 3×3 卷积中间夹 BN 和 ReLU另一条路径 identity 是 shortcut 的输出如果 stride≠1 或通道数不匹配就通过 1×1 卷积把 x 投影到和主路输出相同的形状然后再把两个分支相加最后过 ReLU。这个相加操作就是残差连接的本质它让梯度可以从输出直接流回输入缓解深层网络的梯度消失问题。参数说明里有几个容易出错的地方。第一biasFalse卷积层后面紧跟 BatchNormBN 自带可学习的平移参数偏置是冗余的所以直接关掉。第二stride 参数它下采样时用在了第一个卷积上而不是池化层这和 VGG 用 MaxPool 下采样的思路不同。第三BN 放在卷积之后、激活之前这是原版 ResNet 的 post-activation 顺序后来的预激活残差块把顺序调换成 BN→ReLU→Conv效果各有千秋但要清楚你写的是哪一种。4.3 堆叠残差块与迁移学习ResNet18 快速搭建有了 BasicBlock搭建完整的 ResNet 就只是重复堆叠的过程。下面的代码把 4 个 stage 的残差块数量写进一个列表每进入下一个 stage 时通道数翻倍、空间尺寸减半class ResNet18(nn.Module): def __init__(self, num_classes10): super().__init__() self.stem nn.Sequential( nn.Conv2d(3, 64, kernel_size7, stride2, padding3, biasFalse), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2, padding1) ) self.layer1 self._make_layer(64, 64, 2, stride1) self.layer2 self._make_layer(64, 128, 2, stride2) self.layer3 self._make_layer(128, 256, 2, stride2) self.layer4 self._make_layer(256, 512, 2, stride2) self.pool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512, num_classes) def _make_layer(self, in_channels, out_channels, blocks, stride): layers [BasicBlock(in_channels, out_channels, stride)] for _ in range(1, blocks): layers.append(BasicBlock(out_channels, out_channels, stride1)) return nn.Sequential(*layers) def forward(self, x): x self.stem(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.pool(x) x torch.flatten(x, 1) return self.fc(x)_make_layer 的逻辑是每个 stage 的第一个残差块负责下采样stride 由外部传入后续残差块都用 stride1 保持尺寸。ResNet18 总共 4 个 stage每个 stage 含 2 个残差块加上 stem 和分类头层数与原始论文一致。实际使用中如果数据量不大强烈建议加载 torchvision 提供的 ResNet18 预训练权重把最后一层全连接替换成你自己的类别数只微调后面几层。这是图像分类项目里性价比最高的方案效果往往比从头训练好得多。提示BatchNorm 在训练和推理时行为不同。训练时统计每个 batch 的均值和方差推理时使用全局统计量。如果你加载了预训练模型但忘记切到 eval 模式BN 层的统计量会错乱导致推理结果异常这是新手最容易踩的坑之一。5. 训练脚本实战从数据加载到分类准确率验证的关键技巧有了网络结构剩下的问题是把它训起来。图像分类的训练流程在框架层面已经高度标准化但几个关键参数和排错技巧值得单独拿出来说。下面是一份浓缩版的训练核心逻辑以自定义数据集为例import torch from torch.utils.data import DataLoader, random_split from torchvision import datasets, transforms # 训练/验证分别用不同预处理: 训练加随机增强, 验证只做归一化 train_transform transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.3, 0.3, 0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) dataset datasets.ImageFolder(path/to/your_data, transformtrain_transform) train_ds, val_ds random_split(dataset, [0.8, 0.2]) # 注意: 验证集也要套用 val_transform, 但 random_split 是分割后再换 transform实际做法是先用 ImageFolder 加载原始目录结构的数据集train 和 val 分成两个独立的 ImageFolder 实例分别应用不同 transform。上面代码用 random_split 做简易划分但更规范的是提前把数据按类别目录拆分好train 目录和 val 目录各自用对应 transform。训练循环里有两个参数经常决定成败学习率和 batch size。图像分类里最常见的配置是“warmup cosine 退火”前 5 个 epoch 线性把学习率从 0 升到预设值如 0.1 对应 batch size 256之后按余弦曲线衰减到 0。优化器用带动量的 SGD 或 AdamW前者在分类任务上收敛更稳后者在模型微调时更好用。如果你用的是预训练权重做 fine-tune初始学习率一般取 0.001 到 0.01并配合低 weight decay1e-4 左右。排错时最重要的验证方法是先看训练集的 loss 能不能稳定下降。如果 loss 不降优先检查数据预处理——ToTensor() 是否在 Normalize 之前、图片通道数是不是 3、label 是否从 0 开始连续编号。如果训练 loss 降但验证准确率上不去先看是否过拟合此时加大数据增强强度、加 dropout 或减小模型容量都能缓解。如果训练和验证都上不去检查 BN 层是否在 eval 模式下运行、学习率是否过大导致 loss 震荡。最后一个实践建议如果你手头的项目是森林图像分类、特定物体识别这类自定义单标签场景先用 ResNet18 跑出一个基线记录 top-1 准确率和单 epoch 耗时再决定是否换 ResNet50 或引入更深的网络。课件里 VGG 和 ResNet 的理论到这里就全部转化成了可运行的工程路径剩下的就是调参和迭代了。本文还有配套的精品资源点击获取
返回列表