学习内容
图片分类网络的发展
三个模型——AlexNet、VggNet、ResNet
(看懂三个模型代码,计算三个模型的参数量和输出特征图维度。)
一、图片分类网络的发展
1、核心概念:SOTA 是什么?
SOTA = the state of the art,指在某一任务上当前性能最好、最先进的模型。在图像分类领域,它代表在 ImageNet 等数据集上准确率 / 错误率最优的 CNN/Transformer 模型。
2、经典 CNN 模型发展时间线(按年份)

要点:
AlexNet:深度学习在计算机视觉的 “引爆点”,第一次证明深度 CNN 比传统方法强得多。
VGG:结构极简,全部 3×3 卷积,是后续很多模型的基础。
GoogLeNet:多尺度卷积思想,用 Inception 模块高效提取特征。
ResNet:残差连接是核心创新,解决了 “网络越深越难训练” 的问题,是 CNN 发展的里程碑。
SOTA:代表当前任务的最优性能,模型和数据集相互促进(更大数据集 → 更强模型 → 推动更大数据集需求)。
3、关键演进规律
1.网络越来越深
从 LeNet 的 5 层 → AlexNet 8 层 → VGG 19 层 → GoogLeNet 22 层 → ResNet 152 层。
早期问题:网络太深会出现梯度消失 / 退化,准确率不升反降。
解决:ResNet 用残差连接(Residual Connection),让深层网络可以训练到 100+ 层。
2.错误率持续下降
AlexNet (16.4%) → VGG (7.3%) → GoogLeNet (6.7%) → ResNet (3.57%)
人类在 ImageNet 上的 Top-5 错误率约为 5.1%,ResNet 已经超越人类水平。
真的如此吗?让我们看一看神经网络的“骗局”:
1)数值超越的本质:ResNet 的 Top-5 错误率低于人类,是因为评价规则(Top-5 容错)与训练样本分布带来的数值优势,非认知能力超越。
2)模型的缺陷:基于像素模式匹配学习,缺乏常识与泛化能力,对分布外样本、异常场景的识别能力极差。
3.从 CNN 到 Transformer
后续出现 ViT (Vision Transformer),用 Transformer 结构替代 CNN,在大模型 + 大数据下进一步刷新 SOTA,成为当前主流方向。
二、经典神经网络:AlexNet(深度学习的 “引爆点”)
四大创新:ReLU(抗梯度消失)、Dropout(抗过拟合)、池化(降维提效)、归一化(稳定训练)。

补充:
1)特征归一化(Feature Normalization)
特征归一化的经典形式——Z-Score 标准化,针对特征的 每个维度(Dimension)独立计算。

核心公式:
(
:第 r 个样本在第 i 个特征维度上的原始值;
:第 i 个特征维度在所有样本上的均值;
:第 i 个特征维度在所有样本上的标准差;
:归一化后的特征值。)
逐维度计算步骤(图示解读):
i)锁定维度:如图中绿色框所示,针对第 i 个特征维度(纵向列),遍历所有 R 个样本(横向行)。
ii)计算统计量:求出该维度下所有样本的均值
和标准差
。
iii)执行归一化:用每个样本的原始值减去均值,再除以标准差,得到归一化后的值。
图中黄色框明确了最终效果:所有特征维度的均值为 0,方差为 1。均值为 0:将特征分布的中心移至原点,消除偏置影响。方差为 1:将所有特征的数值范围统一,消除量纲差异。
2)批归一化(Batch Norm)

Batch Norm 要解决的核心问题:
1.输入层归一化不够:即便对输入特征做了归一化(Feature Normalization),经过一层线性变换(W^1)和激活函数后,各维度的数值范围又会变得不一致(Different dims have different ranges)。
2.深层训练困难:这种分布偏移会在网络层间不断累积,导致深层输入分布剧烈变化,优化变得非常困难(Also difficult to optimize)。
3.解决方案:在每一层的激活前 / 后都做归一化,这就是 Batch Norm(批归一化)。
代码示例
手动计算参数量并与代码计算结果比较。

AlexNet 核心结构:5 个卷积层(含池化) + 3 个全连接层,代码中严格遵循该结构,加入了 AlexNet 的标志性设计:11×11大卷积核、步长为4的卷积、3×3池化核、ReLU激活、Dropout(0.5),并使用自适应平均化池保证特征图尺寸匹配全连接层,最终输出 1000 维结果(对应 ImageNet 1000 分类任务)。
1、导入库 + 调用官方 AlexNet
import torchvision.models as models # 导入PyTorch官方预训练模型库
import torch.nn as nn # 导入PyTorch神经网络核心模块
alexnet = models.alexnet() # 调用官方实现的AlexNet
print(alexnet) # 打印官方AlexNet的网络结构(用于对比自定义版)
官方 AlexNet 已封装好所有层,自定义版是对其底层结构的拆解和复现,更易理解原理。
2、自定义 MyAlexNet 类(核心模块)
2.1 _init_:定义所有网络层(含激活 / 正则化 / 卷积 / 池化 / 全连接)
class MyAlexNet(nn.Module):
def __init__(self):
super(MyAlexNet, self).__init__() # 继承父类的初始化方法,必须写
# 激活函数/正则化层:AlexNet标志性的ReLU+Dropout(0.5)
self.relu = nn.ReLU() # 定义ReLU激活,所有层的激活共用一个实例
self.drop = nn.Dropout(0.5) # 定义Dropout,随机丢弃50%神经元,缓解过拟合
# 卷积块1:conv1 + pool1 | 大卷积核+大步长,快速降维提取全局特征
self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=11, stride=4,padding=2)
self.pool1 = nn.MaxPool2d(3, stride=2) # 3×3池化核,步长2,进一步压缩尺寸
# 卷积块2:conv2 + pool2 | 5×5卷积核,细化特征
self.conv2 = nn.Conv2d(64,192,5,1,2) # 输入通道匹配上一层输出64,输出192
self.pool2 = nn.MaxPool2d(3, stride=2)
# 卷积块3:conv3+conv4+conv5 + pool3 | 连续3个3×3卷积核,逐步细化特征,无池化穿插
self.conv3 = nn.Conv2d(192,384,3,1,1)
self.conv4 = nn.Conv2d(384,256,3,1,1)
self.conv5 = nn.Conv2d(256, 256, 3, 1, 1)
self.pool3 = nn.MaxPool2d(3, stride=2)
# 自适应平均池化:解决特征图尺寸不匹配问题,强制输出6×6特征图
self.adapool = nn.AdaptiveAvgPool2d(output_size=6)
# 全连接层:3层全连接,维度从9216→4096→4096→1000,是AlexNet的大参数量核心
self.fc1 = nn.Linear(9216,4096) # 9216=256(通道)×6(高)×6(宽),匹配adapool输出
self.fc2 = nn.Linear(4096,4096)
self.fc3 = nn.Linear(4096,1000) # 输出1000维,对应ImageNet 1000分类
关键层参数解读(卷积 / 池化):
卷积层通用参数:nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)(输入通道数,输出通道数,卷积核,步长,padding)
池化层通用参数:nn.MaxPool2d(kernel_size, stride)
2.2 forward:前向传播(层的拼接 + 激活函数的使用)
def forward(self,x):
# 卷积块1执行:conv1 → ReLU激活 → pool1
x = self.conv1(x)
x = self.relu(x) # 卷积后必加ReLU,引入非线性,否则多层卷积等价于一层
x = self.pool1(x)
# 卷积块2执行:conv2 → ReLU激活 → pool2
x = self.conv2(x)
x = self.relu(x)
x = self.pool2(x)
# 卷积块3执行:conv3 → ReLU → conv4 → ReLU → conv5 → ReLU → pool3
x = self.conv3(x)
x = self.relu(x)
print(x.size()) # 打印conv3+ReLU后的特征图尺寸,调试用
x = self.conv4(x)
x = self.relu(x)
print(x.size()) # 打印conv4+ReLU后的特征图尺寸,调试用
x = self.conv5(x)
x = self.relu(x)
x = self.pool3(x)
print(x.size()) # 打印pool3后的特征图尺寸,调试用
# 池化+展平:自适应池化 → 拉平为一维向量(适配全连接层输入)
x= self.adapool(x)
x = x.view(x.size()[0], -1) # x.size()[0]是批次大小,-1表示自动计算剩余维度
# 全连接块执行:fc1 → ReLU → fc2 → ReLU → fc3 → ReLU
x = self.fc1(x)
x = self.relu(x) # 全连接后加ReLU,引入非线性
x = self.fc2(x)
x = self.relu(x)
x = self.fc3(x)
x = self.relu(x) # 最终输出前也加ReLU,保证输出为非负(分类任务可后续加Softmax)
return x
3、参数量统计函数 + 调用
import torch
myalexnet = MyAlexNet() # 实例化自定义的MyAlexNet
# 定义参数量统计函数:统计总参数量、可训练参数量
def get_parameter_number(model):
# 总参数量:遍历模型所有参数,numel()计算每个参数的元素个数,求和
total_num = sum(p.numel() for p in model.parameters())
# 可训练参数量:仅统计requires_grad=True的参数(PyTorch默认所有层参数均可训练)
trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
return {'Total': total_num, 'Trainable': trainable_num}
print(get_parameter_number(myalexnet)) # 打印MyAlexNet的参数量
4、前向传播测试(验证网络正确性)
# 生成模拟输入:4个样本,3通道,224×224(AlexNet标准输入尺寸)
img = torch.zeros((4,3,224,224))
out = myalexnet(img) # 将模拟输入传入网络,执行前向传播
print(out.size()) # 打印网络输出尺寸
三、VggNet
1、两大关键创新
1)更深的网络结构:从 AlexNet 的 8 层扩展到 19 层(E 配置),大幅提升特征表达能力。
2)小卷积核堆叠:用连续的 3×3 卷积核替代 AlexNet 中的 11×11、5×5 大卷积核。用更少的参数,达到了更大的感受野,提取特征更细腻。
2、VGG 的 5 种经典配置(A-E)

规律非常明显:
Conv 层:固定使用 3×3 卷积,padding=1(保证尺寸不变),stride=1。
Pool 层:固定使用 2×2 最大池化,stride=2(尺寸减半)。
通道数规律:通道数翻倍(64 → 128 → 256 → 512 → 512),特征图尺寸减半。
最终结构:5 层卷积池化 + 3 层全连接(FC),最终通过 Softmax 输出 1000 分类。

3、感受野(Receptive Field)
为什么能用 3×3 小卷积核替代大卷积核?
1)感受野(Receptive Field)等效
感受野指卷积核输出的一个像素,能 “看到” 输入图片的多大范围。
5×5 卷积核:一次操作覆盖输入 5×5 区域。3×3 卷积核:一次操作覆盖输入 3×3 区域。
两个 3×3 卷积层堆叠,经过第一层后,第二层的每个像素已经能看到 5×5 区域。

2)参数量优势(VGG 的杀手锏)
VGG 用小卷积核堆叠,参数量更少,且增加了非线性表达。
假设输入输出通道均为 a:
1 个 7×7 卷积:参数量 = 7×7×a×a = 49a^2
3 个 3×3 卷积:参数量 = 3×(3×3×a×a) = 27a^2
参数量减少近一半,防止过拟合。且3 次卷积引入了 3 次 ReLU 激活,让网络能学习更复杂的非线性特征,特征表达能力更强。
代码示例
手动计算参数量并与代码计算结果比较。

1、导入库
import torchvision.models as models # 用于对比官方 VGG13(代码中注释了相关对比逻辑)
import torch.nn as nn # 构建神经网络的核心模块(如卷积、池化、全连接层)
import torch # 用于创建张量、验证网络前向传播
2、VGG 基础卷积层封装(vggLayer 类)
封装了两次卷积 + 两次 ReLU 激活 + 一次最大池化:
class vggLayer(nn.Module):
def __init__(self,in_cha, mid_cha, out_cha):
super(vggLayer, self).__init__()
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2) # 2×2最大池化,步长默认=核大小
self.conv1 = nn.Conv2d(in_cha, mid_cha, 3, 1, 1) # 3×3卷积,步长1,填充1(保持尺寸)
self.conv2 = nn.Conv2d(mid_cha, out_cha, 3, 1, 1)
def forward(self,x):
x = self.conv1(x)
x= self.relu(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
return x
3、自定义 VGG 网络(MyVgg 类)
class MyVgg(nn.Module):
def __init__(self):
super(MyVgg, self).__init__()
# 5个卷积块(对应VGG13的卷积部分)
self.layer1 = vggLayer(3, 64, 64) # 输入3通道(RGB),输出64通道
self.layer2 = vggLayer(64, 128, 128)
self.layer3 = vggLayer(128, 256, 256)
self.layer4 = vggLayer(256, 512, 512)
self.layer5 = vggLayer(512, 512, 512)
# 自适应平均池化(保证输出7×7特征图,兼容任意输入尺寸)
self.adapool = nn.AdaptiveAvgPool2d(7)
self.relu = nn.ReLU()
# 全连接层(VGG13的分类头)
self.fc1 = nn.Linear(25088, 4096) # 512*7*7=25088(池化后特征展平维度)
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, 1000) # 输出1000类(对应ImageNet分类)
def forward(self,x):
# 卷积部分:逐层前向传播
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.layer5(x)
# 池化+展平:自适应池化→7×7,展平为一维张量
x = self.adapool(x)
x = self.adapool(x) # 注:此处重复池化,属于冗余代码(无实际作用)
x = x.view(x.size()[0], -1) # 批量维度保留,其余展平(batch_size, 25088)
# 全连接部分:三层全连接+ReLU激活
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
x = self.relu(x)
x = self.fc3(x)
x = self.relu(x)
return x
4、网络验证与参数统计
# 1. 实例化自定义VGG
myVgg = MyVgg()
# 2. 创建测试张量(模拟224×224的RGB图像,批量大小1)
img = torch.zeros((1, 3, 224,224))
# 3. 前向传播验证,打印输出尺寸
out = myVgg(img)
print(out.size()) # 输出:torch.Size([1, 1000])(符合1000分类预期)
# 4. 定义参数统计函数
def get_parameter_number(model):
total_num = sum(p.numel() for p in model.parameters()) # 总参数数
trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad) # 可训练参数数
return {'Total': total_num, 'Trainable': trainable_num}
# 5. 打印不同层级的参数数量
print(get_parameter_number(myVgg)) # 整个网络的参数统计
print(get_parameter_number(myVgg.layer1)) # 第一个卷积块的参数统计
print(get_parameter_number(myVgg.layer1.conv1)) # 第一个卷积块的第一个卷积层参数统计
# print(get_parameter_number(vgg)) # 注释:对比官方VGG13的参数(取消注释可执行)
四、ResNet
单纯加深网络会导致性能下降(并非过拟合,而是训练不动)。ResNet核心创新,解决深度网络退化问题:残差连接(Residual Connection) + 1×1 卷积,让网络能训练到上百层甚至上千层。
残差连接的原理
1、残差块公式
x:输入(恒等映射 /shortcut)
:经过若干卷积层学习到的残差(即 “需要修正的部分”)
最后经过 ReLU 激活输出
2、为什么能缓解梯度消失 / 爆炸?
普通网络梯度链式求导,梯度会随层数乘积衰减 / 爆炸:

残差网络梯度:
,梯度至少为 1,避免了梯度消失,让信息能顺畅回传。(注:ReLU 也起到类似作用:正区间导数为 1,进一步稳定梯度。)
3、维度不匹配的解决办法

当
与 x 维度 / 通道数不同时:
1)通道数变化:用 1×1 卷积 对 shortcut 做升 / 降维(不改变空间尺寸)。
2)尺寸变化:shortcut 路径用 1×1 卷积 + stride=2 下采样,与残差路径尺寸对齐。
1×1 卷积
作用:
通道升降维:在不改变特征图尺寸的前提下,调整通道数(如 64→256 或 256→64)。
降参 + 非线性:用 1×1 卷积替代大卷积,大幅减少参数量,同时引入更多非线性。
比如:输入:64×112×112,输出:256×112×1121×1 ,卷积参数量:(64×1×1+1)×256=16,640。若用 3×3 卷积,参数量为 (64×3×3+1)×256=147,712→ 1×1 卷积参数量仅为 3×3 卷积的~11%,极大压缩计算量。
代码示例
1、导入库
import torch
import torch.nn as nn
import torchvision.models as models
2、加载官方 ResNet18 并打印
resNet = models.resnet18()
print(resNet)
3、残差块(Residual Block)实现
「主分支(两次 3×3 卷积 + BN)+ 残差分支(恒等映射 / 1×1 卷积调整维度)」
class Residual_block(nn.Module): #@save
def __init__(self, input_channels, out_channels, down_sample=False, strides=1):
super().__init__()
# 第一个卷积层(可带步长,用于下采样)
self.conv1 = nn.Conv2d(input_channels, out_channels, kernel_size=3, padding=1, stride=strides)
# 第二个卷积层(步长固定为1)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, stride=1)
# 1×1卷积层:仅当输入/输出通道数不一致时使用(匹配维度,保证残差相加)
if input_channels != out_channels:
self.conv3 = nn.Conv2d(input_channels, out_channels, kernel_size=1, stride=strides)
else:
self.conv3 = None
# 批归一化层(BN)
self.bn1 = nn.BatchNorm2d(out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU()
def forward(self, X):
# 主分支:卷积→BN→ReLU → 卷积→BN
out = self.relu(self.bn1(self.conv1(X)))
out = self.bn2(self.conv2(out))
# 残差分支:通道/尺寸不匹配时,用1×1卷积调整
if self.conv3:
X = self.conv3(X)
# 残差相加 + 最终ReLU
out += X
return self.relu(out)
4、自定义 ResNet18(MyResNet18)实现
class MyResNet18(nn.Module):
def __init__(self):
super(MyResNet18, self).__init__()
# 初始卷积层(7×7大卷积核,步长2,下采样)
self.conv1 = nn.Conv2d(3, 64, 7, 2, 3)
self.bn1 = nn.BatchNorm2d(64)
self.pool1 = nn.MaxPool2d(3, stride=2, padding=1) # 最大池化下采样
self.relu = nn.ReLU()
# 4个残差层(对应ResNet18的layer1-layer4)
self.layer1 = nn.Sequential(Residual_block(64, 64), Residual_block(64, 64)) # 无下采样
self.layer2 = nn.Sequential(Residual_block(64, 128, strides=2), Residual_block(128, 128)) # 下采样
self.layer3 = nn.Sequential(Residual_block(128, 256, strides=2), Residual_block(256, 256)) # 下采样
self.layer4 = nn.Sequential(Residual_block(256, 512, strides=2), Residual_block(512, 512)) # 下采样
# 分类头(自适应平均池化 + 展平 + 全连接)
self.flatten = nn.Flatten()
self.adv_pool = nn.AdaptiveAvgPool2d(1) # 自适应池化到1×1
self.fc = nn.Linear(512, 1000) # 输出1000类(匹配ImageNet分类任务)
def forward(self, x):
# 初始卷积+池化
x = self.relu(self.bn1(self.conv1(x)))
x = self.pool1(x)
# 残差层前向
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
# 分类头
x = self.adv_pool(x)
x = self.flatten(x)
x = self.fc(x)
return x
myres = MyResNet18() # 实例化自定义ResNet18
5、模型参数统计函数
def get_parameter_number(model):
total_num = sum(p.numel() for p in model.parameters()) # 总参数数
trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad) # 可训练参数数
return {'Total': total_num, 'Trainable': trainable_num}
# 打印参数统计(对比自定义模型和官方模型的卷积层参数)
print(get_parameter_number(myres.layer1))
print(get_parameter_number(myres.layer1[0].conv1))
print(get_parameter_number(resNet.layer1[0].conv1))
6、前向传播测试
生成一个随机张量模拟输入图像,分别测试官方 ResNet18 和自定义MyResNet18的前向传播是否正常(无维度错误)。
# 构造随机输入(batch_size=1,3通道,224×224,符合ImageNet输入规格)
x = torch.rand((1,3,224,224))
# 官方ResNet18前向传播
out = resNet(x)
# 自定义ResNet18前向传播
out = myres(x)




