1.知识回顾
1.网络过程
在进行下述网络的类别特征的学习前我们首先复习一下常见的功能函数以及不同的类别函数使用的位置和领域是怎么样的。
根据之前的文章,我们可以知道神经网络的使用中正常流程是
python//神经网络的详细过程 1.数据输入:将预处理后的样本喂给网络 2.前向传播=隐藏层+输出层。隐藏层:每一层都经过线性变换(例:卷积)+非线性变换(激活函数),目的是为了更好的提取特征。输出层:全连接(整合特征)+最终选取合适的激活函数得到y_pre。 3.计算损失:用损失函数对比前向传播完成的结果y_pre与真实标签y,得到损失值(LOSS) 4.反向传播:根据损失值,运用链式法则更新所有参数的梯度。 5.参数更新:根据梯度选取合适的优化器进行调整参数。 6.迭代重复:回到步骤2,直到损失收敛(预测准确) 总之一句话:神经网络的过程是一次前向传播,一次反向传播,更新一次参数。(batch:前向-损失-反向-更新,所有的batch执行完成为一个epoch)要点分析:
1.核心概念:激活函数参与前向传播的特征计算,损失函数用于评估前向传播结果的误差。
2输出层前的激活函数选取标准:二分类问题选取S函数,多分类互斥问题选取SoftMax函数,多分类非互斥用多个S函数。
3.损失函数:回归问题:MSE。分类问题:BCE 和 CE。
匹配关系:
二分类:(S函数 + BCE)
互斥多分类:(Softmax + CE)
非互斥多分类:(多个S函数 + 多个BCE)
回归:(无激活/线性激活 + MSE)
易混淆点:
(1)问题:上述所说的输出层前的激活函数选取标准,和隐藏函数中的激活函数有什么区别,为何要这样划分单独定制?
答:
1.隐藏层和输出层前的定制激活函数本质是一样的,都是非线性信号转换器。
2.定制的缘由是
隐藏层的激活函数功能【特征提取+稳定训练(避免梯度消失)】,输出层前的激活函数功能【任务匹配+兼容损失】,其中我们可以在隐藏层进行优化 Conv ->BN->ReLu 。
(2)隐藏层和输出层有无明显边界划分?真实实现如何区分?
答:三个角度
1.(层次角度)输出层是为任务输出结果的最后一层,输出层前的层都可称为隐藏层。
2.(输出后的接口):如果经过该层直接可以输入损失函数则称为输出层。
3.(功能):隐藏层:为特征服务。输出层:为任务服务。
(3).正则化Dropout层怎么理解,有什么作用?
答:
本质:通过约束模型的复杂度,较少模型对训练数据噪声的依赖,防止过拟合。
方式:
1.防止过拟合的手段:数据增强,正则化,早停。
2.正则化的手段:Dropout(随机屏蔽神经元),L1/L2权重惩罚。
2.工程化实践
工程化结构划分(BackBone和Head代码构建)
综上我们了解网络结构的前向传播过程为:
input -> 隐藏层(特征提取) ->【全连接层(特征融合)+Dropout层(正则化)】->输出层(任务映射)->损失计算。
在工程化中会根据 是否具有复用性和通用性 对【全连接层(特征融合)+Dropout层(正则化)】归属进行划分。简单理解就是 是否需要定制如果要定制就放Head中,如果通用就放BackBone中。
BackBone:隐藏层实现+(特征融合/正则化)
Head :(特征融合/正则化)+输出层(任务映射)
损失函数计算(独立)
实例:
python
import torch
import torch.nn as nn
import torch.optim as optim
# 按照你的工程化划分搭建模型
class SimpleMNISTModel(nn.Module):
def __init__(self):
super(SimpleMNISTModel, self).__init__()
# 1. BackBone:隐藏层实现 + 通用的特征融合/正则化
# 隐藏层:卷积层(特征提取,通用,可复用于其他图像任务)
# 通用特征融合:Flatten(展平)+ 通用全连接层(不针对10分类定制,可复用)
self.backbone = nn.Sequential(
# 隐藏层:特征提取(卷积+激活+池化,通用)
nn.Conv2d(1, 16, kernel_size=3, padding=1), # 输入[batch,1,28,28]→[batch,16,28,28]
nn.ReLU(), # 隐藏层激活函数
nn.MaxPool2d(2, 2), # 下采样,保留关键特征→[batch,16,14,14]
# 通用特征融合(无需定制,可复用)
nn.Flatten(), # 展平为1D向量→[batch,16*14*14=3136]
nn.Linear(3136, 256) # 通用全连接层,特征压缩→[batch,256](不绑定分类类别,可复用)
)
# 2. Head:需要定制的特征融合/正则化 + 输出层(任务映射)
# 定制特征融合:Dropout(正则化,针对当前分类任务调参)+ 定制全连接层
# 输出层:任务映射(10分类,完全定制)
self.head = nn.Sequential(
# 定制特征融合/正则化(针对MNIST分类,需调参,不可复用)
nn.Dropout(p=0.3), # 防止过拟合,p值针对当前任务定制
nn.Linear(256, 128), # 定制全连接层,进一步适配10分类→[batch,128]
# 输出层:任务映射(10分类,定制化)
nn.Linear(128, 10), # 映射为10个类别的原始分数(logits)→[batch,10]
nn.Softmax(dim=1) # 转为概率分布,满足分类任务需求
)
# 3. 损失函数:独立定义,不归属BackBone/Head
self.loss_fn = nn.CrossEntropyLoss()
def forward(self, x):
"""前向传播:input → BackBone → Head"""
# Step1: BackBone提取通用特征 + 通用特征融合
general_features = self.backbone(x)
# Step2: Head完成定制特征处理 + 任务映射
task_outputs = self.head(general_features)
return task_outputs
def compute_loss(self, outputs, labels):
"""损失计算:独立方法,接收Head输出和真实标签"""
loss = self.loss_fn(outputs, labels)
return loss
# -------------------------- 测试模型 --------------------------
if __name__ == "__main__":
# 1. 初始化模型、优化器
model = SimpleMNISTModel()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 2. 模拟输入(MNIST格式:[batch_size, channels, height, width])
batch_size = 32
fake_input = torch.randn(batch_size, 1, 28, 28) # 模拟32张手写数字图像
fake_labels = torch.randint(0, 10, (batch_size,)) # 模拟32个真实标签(0-9)
# 3. 前向传播 + 损失计算 + 反向传播(完整训练流程)
optimizer.zero_grad() # 梯度清零
# 前向传播:获取Head输出
model_outputs = model(fake_input)
# 独立计算损失
loss = model.compute_loss(model_outputs, fake_labels)
# 反向传播 + 参数更新(演示流程)
loss.backward() # 梯度回传
optimizer.step() # 更新参数
# 4. 打印关键信息
print(f"模型输出形状(概率分布):{model_outputs.shape}") # torch.Size([32, 10])
print(f"单批次损失值:{loss.item():.4f}")
print(f"预测概率最高的类别:{torch.argmax(model_outputs, dim=1)}")
print(f"真实标签:{fake_labels}")

2.网络结构和特性
1.LeNet-5网络(1998)
1.结构

python
输入层(Input)→ 卷积层C1 → 池化层S2 → 卷积层C3 → 池化层S4 → 全连接层F5 → 全连接层F6 → 输出层(G7)
2.设计亮点:
为了适配 MNIST 的数字特征、解决当时的算力不足问题、弥补传统全连接层的缺陷,最终形成了 "卷积 + 池化交替堆叠" 的架构。
3.学习要点:
用「卷积层 + 池化层」代替全连接层做特征提取,既减少参数和计算量(利用卷积的局部感知+权值共享)
2.AlexNet网络结构(2012)
1.结构

2.设计亮点(基于2012年)
亮点1:
对现在影响最大的亮点:多分枝架构,多branch。类似于总分总的结构。并在消费级显卡上训练成功,意义重大且深远。
亮点2:
基于当时的时代背景:
1.用 ReLU 激活函数替代 Sigmoid,彻底解决深层梯度消失
2.用 Dropout 正则化,有效防止过拟合
3.大卷积核 + 步长 4 快速降维(适配当时算力)
4.局部响应归一化(LRN)+ 数据增强,提升泛化能力
3.核心贡献
- 核心贡献:首次用深层 CNN 证明了深度学习在 CV 领域的有效性,解决了深层网络训练的关键痛点(ReLU 缓解梯度消失,Dropout 防过拟合);
- 设计精髓:工程化与算法创新结合------ 既考虑当时的硬件算力(大卷积核快速降维、双 GPU 并行),又创新算法(ReLU、Dropout、LRN),让深层网络能落地;
- 历史意义:奠定现代 CNN 的基础架构和训练范式,后续所有主流 CNN(VGG、ResNet、MobileNet)都是在它的创新思路上优化而来。
3.GoogleNet网络(2014)
1.结构

2.设计亮度
亮点1:
引入了inception_Module模块化结构,在不增加过多参数的前提下,提取多尺度特征。
python
输入特征图 → [分支1: 1×1卷积] → 输出A
→ [分支2: 1×1卷积→3×3卷积] → 输出B
→ [分支3: 1×1卷积→5×5卷积] → 输出C
→ [分支4: 3×3最大池化→1×1卷积] → 输出D
→ 输出A/B/C/D在**通道维度拼接**(concat) → 最终输出特征图
Module内部用1x1卷积核实现"降维",实现多尺度特征融合concat(通道维度),减少计算量和参数量。
核心:讲原本单一的CCP中的C进行了改进变为module结构,每个module中运用了多个卷积核对特征图操作融合。并在module中进一步运用1x1卷积的特性压缩通道进一步减少参数量和计算量。
亮点2:
全连接层进行了更新 FC -> 1x1卷积+GAP(全局平均池化)
**作用:**极大的减少了计算量和参数量,避免全连接层的参数爆炸。彻底解决了全连接层的参数冗余问题,同时避免过拟合。
亮点3:
增加了辅助头结构(过程监督)的功效,监督收敛进程,防止梯度消失。
设计原理:在深层深层网络中间的两个 Inception 模块输出后,各添加一个小型分类器(辅助分类器),训练时同时计算主分类器(网络最后)和两个辅助分类器的损失,再按比例加权求和(辅助分类器损失权重为 0.3)
3.学习要点
1.GoogleNet的影响:最小设计单元进行了升级,是从初始的layer层化结构设计更新到Inception Module模块化设计,引入了多头结构中的辅助头概念。
2.Inception Module的设计核心,「高效提取多尺度特征 + 控制参数量」。
代码案例:
python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Project :deep_learning
@File :my_googlenet.py
@IDE :PyCharm
@Author :wjj
@Date :2025/12/23 18:00
@Description: GoogLeNet(简化版,聚焦Inception模块)
"""
import torch
import torch.nn as nn
# 核心模块:Inception模块(多尺度特征融合)
class Inception(nn.Module):
def __init__(self, in_channels, ch1x1, ch3x3reduce, ch3x3, ch5x5reduce, ch5x5, pool_proj):
"""
Inception模块初始化参数说明
:param in_channels: 输入通道数
:param ch1x1: 第1个分支(1×1卷积)的输出通道数
:param ch3x3reduce: 第2个分支(1×1卷积)的输出通道数(3×3卷积前的降维通道数)
:param ch3x3: 第2个分支(3×3卷积)的输出通道数
:param ch5x5reduce: 第3个分支(1×1卷积)的输出通道数(5×5卷积前的降维通道数)
:param ch5x5: 第3个分支(5×5卷积)的输出通道数
:param pool_proj: 第4个分支(池化后1×1卷积)的输出通道数
"""
super(Inception, self).__init__()
# 分支1:1×1卷积(直接降维/升维,无padding)
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, ch1x1, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True)
)
# 分支2:1×1卷积 → 3×3卷积(3×3卷积需要padding=1保持尺寸不变)
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, ch3x3reduce, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.Conv2d(ch3x3reduce, ch3x3, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True)
)
# 分支3:1×1卷积 → 5×5卷积(5×5卷积需要padding=2保持尺寸不变)
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, ch5x5reduce, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.Conv2d(ch5x5reduce, ch5x5, kernel_size=5, stride=1, padding=2),
nn.ReLU(inplace=True)
)
# 分支4:3×3最大池化 → 1×1卷积(池化padding=1保持尺寸不变)
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels, pool_proj, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True)
)
def forward(self, x):
# 4个分支并行计算
b1 = self.branch1(x)
b2 = self.branch2(x)
b3 = self.branch3(x)
b4 = self.branch4(x)
# 在通道维度(dim=1)拼接4个分支的输出,得到融合后的特征
outputs = torch.cat([b1, b2, b3, b4], dim=1)
return outputs
# GoogLeNet整体网络(简化版,无辅助分类器)
class GoogLeNet(nn.Module):
def __init__(self, num_classes=10):
super(GoogLeNet, self).__init__()
# 初始卷积层:将输入(3通道)转换为64通道,缩小特征图尺寸
self.pre_layers = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3), # 224→112
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1), # 112→56
nn.Conv2d(64, 192, kernel_size=3, stride=1, padding=1),# 56保持不变
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 56→28
)
# 第1组Inception模块(2个):输入通道192,输出通道256
self.inception3a = Inception(192, 64, 96, 128, 16, 32, 32)
self.inception3b = Inception(256, 128, 128, 192, 32, 96, 64)
self.pool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 28→14
# 第2组Inception模块(5个,简化为2个):输入通道480,输出通道512
self.inception4a = Inception(480, 192, 96, 208, 16, 48, 64)
self.inception4b = Inception(512, 160, 112, 224, 24, 64, 64)
self.pool4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 14→7
# 第3组Inception模块(2个):输入通道512,输出通道512
self.inception5a = Inception(512, 256, 160, 320, 32, 128, 128)
self.inception5b = Inception(512, 384, 192, 384, 48, 128, 128)
# 尾部:全局平均池化 + 1×1卷积(替代flatten+全连接,贴合你的VGG修改习惯)
self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1)) # 7×7→1×1
self.fc = nn.Conv2d(512, num_classes, kernel_size=1, stride=1, padding=0) # 1×1卷积替代全连接
self.dropout = nn.Dropout(0.5) # 正则化
def forward(self, x):
# 初始卷积+池化
x = self.pre_layers(x) # 输出:(B, 192, 28, 28)
# 第1组Inception
x = self.inception3a(x) # 输出:(B, 256, 28, 28)
x = self.inception3b(x) # 输出:(B, 480, 28, 28)
x = self.pool3(x) # 输出:(B, 480, 14, 14)
# 第2组Inception
x = self.inception4a(x) # 输出:(B, 512, 14, 14)
x = self.inception4b(x) # 输出:(B, 512, 14, 14)
x = self.pool4(x) # 输出:(B, 512, 7, 7)
# 第3组Inception
x = self.inception5a(x) # 输出:(B, 512, 7, 7)
x = self.inception5b(x) # 输出:(B, 512, 7, 7)
# 尾部:全局池化+1×1卷积
x = self.global_avg_pool(x) # 输出:(B, 512, 1, 1)
x = self.dropout(x)
x = self.fc(x) # 输出:(B, num_classes, 1, 1)
x = torch.squeeze(x, dim=(2, 3)) # 仅删除空间维度,保留(B, num_classes)
return x
if __name__ == '__main__':
# 模拟输入:和你的VGG测试一致,(batch_size=1, 3, 224, 224)
rnd = torch.rand(1, 3, 224, 224)
# 初始化网络,类别数=10
net = GoogLeNet(num_classes=10)
# 前向传播
pre_y = net(rnd)
# 打印结果
print("预测结果:", pre_y)
print("输出形状:", pre_y.shape) # 输出:torch.Size([1, 10]),和你的VGG输出格式一致
4.VGG网络结构(2014)
1.结构:


2.设计亮点
亮点1:
卷积核的大小配置规范统一,网络结构规整:
1.Conv配置
1.(kernel_size:3x3,stride = 1,padding = 1)卷积完成后图像尺寸不发生改变。
2.(kernel_size:1x1,stride = 1,padding = 0)卷积完成后图像尺寸不发生改变。
2.pool配置(kernel_size:2x2,stride = 2,padding =0)池化完feature_map大小为原来的1/2。
知识储备:
**1.多个 3×3 卷积替代大卷积核,减少参数 + 增强非线性:**2 个 3×3 卷积堆叠的感受野(相当于 5×5),3 个 3×3 卷积堆叠的感受野(相当于 7×7),但参数数量远少于直接用大卷积核。
2.**1x1卷积核在VGG的主要作用体现在通道维度变换+引入非线性:**不改变特征图空间尺寸,仅调整通道数(比如从 256 通道压缩到 128 通道),减少后续计算量。1×1 卷积后接 ReLU,在不改变空间信息的前提下,增加特征的非线性表达能力。
3.额外收益:每个 3×3 卷积后都接 ReLU 激活函数,3 个卷积能引入 3 次非线性变换,而 1 个大卷积仅 1 次 ------ 非线性增强让模型更能拟合复杂特征
3. 结构规整:
python
输入 → [卷积组(2~3个3×3卷积,通道数固定)→ 2×2最大池化(尺寸减半)] × 5组 → 全连接层 × 3 → 输出
亮点2:
通道数和尺寸的黄金比例------ 通道翻倍,尺寸减半
VGG 的特征图变化严格遵循「每经过 1 次池化,通道数翻倍、尺寸减半」:
| 网络阶段 | 特征图尺寸 | 通道数 | 核心操作 |
|---|---|---|---|
| 输入 | 224×224 | 3 | - |
| 第 1 组后 | 112×112 | 64 | 2 个 3×3 卷积 + 池化 |
| 第 2 组后 | 56×56 | 128 | 2 个 3×3 卷积 + 池化 |
| 第 3 组后 | 28×28 | 256 | 3 个 3×3 卷积 + 池化 |
| 第 4 组后 | 14×14 | 512 | 3 个 3×3 卷积 + 池化 |
| 第 5 组后 | 7×7 | 512 | 3 个 3×3 卷积 + 池化 |
3.学习要点
VGG的意义
1.降低神经网络的理解成本核实现的复杂度。
2.提升了将层数推向更多提供了更多可能。
⭐实际开发过程经验:
1.可以根据输入图片大小尺寸调整VGG的层数(加一/多层CCCP),基本是确保全连接之前WxHxC -> 7x7xC
2.常见实际开发中数据量不会达到千万级别的量,所以我们可以通过修改FC的输出层调小,从之前的4096->128/256等,全连接层是我们计算的主要耗时区域。
3.根据需要处理数据量多少使用的VGG_A/VGG_16/VGG_19,如果欠拟合则换更复杂的。
VGG代码样例:
python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Project :deep_learning
@File :my_vgg.py
@IDE :PyCharm
@Author :wjj
@Date :2025/12/23 16:26
@Description:VGG
"""
import torch
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
#模板定义,本代码没有使用
class VGG(nn.Module):
def __init__(self,inc,outc,k,s,p=0):
super(VGG, self).__init__()
self.conv = nn.Conv2d(inc, outc, kernel_size=k, stride=s, padding=p)
self.relu = nn.ReLU()
def forward(self,x):
x = self.conv(x)
x = self.relu(x)
return x
class Vgg16(nn.Module):
def __init__(self, num_classes=10):
super(Vgg16, self).__init__()
# 卷积组1
self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1)
self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1)
self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 直接使用内置池化层
# 卷积组2
self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1)
self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 卷积组3
self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1)
self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 卷积组4
self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1)
self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.maxpool4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 卷积组5
self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.maxpool5 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
#使用全局平均池化+1x1卷积代替展平操作
#self.flat = nn.Flatten()
#self.fc1 = nn.Linear(512*7*7, 4096)
#self.fc2 = nn.Linear(4096, 4096)
#self.fc3 = nn.Linear(4096, num_classes)
#全局平均池化+1x1卷积
self.global_avg_pool = nn.AdaptiveAvgPool2d((1,1))
self.fc1 = nn.Conv2d(512, 4096,1,1,0)
self.fc2 = nn.Conv2d(4096, 4096,1,1,0)
self.fc3 = nn.Conv2d(4096, num_classes,1,1,0)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.5) #正则化
def forward(self, x):
# 卷积组1:添加ReLU激活
x = self.relu(self.conv1_1(x))
x = self.relu(self.conv1_2(x))
x = self.maxpool1(x)
# 卷积组2:添加ReLU激活
x = self.relu(self.conv2_1(x))
x = self.relu(self.conv2_2(x))
x = self.maxpool2(x)
# 卷积组3:添加ReLU激活
x = self.relu(self.conv3_1(x))
x = self.relu(self.conv3_2(x))
x = self.relu(self.conv3_3(x))
x = self.maxpool3(x)
# 卷积组4:添加ReLU激活
x = self.relu(self.conv4_1(x))
x = self.relu(self.conv4_2(x))
x = self.relu(self.conv4_3(x))
x = self.maxpool4(x)
# 卷积组5:添加ReLU激活
x = self.relu(self.conv5_1(x))
x = self.relu(self.conv5_2(x))
x = self.relu(self.conv5_3(x))
x = self.maxpool5(x) # 输出:(1, 512, 7, 7)
#添加展平操作
#x =self.flat(x)
# x = self.relu(self.fc1(x))
# x = self.relu(self.fc2(x))
# x = self.fc3(x)
# return x
#使用全局平均池化代替展平操作
output = self.global_avg_pool(x)#[1,512,1,1]
output = self.dropout(self.relu(self.fc1(output)))#[1,4090,1,1]
output = self.dropout(self.relu(self.fc2(output)))#[1,4090,1,1]
output = self.fc3(output)# (1, 10, 1, 1)
#去掉多余的维度
output = torch.squeeze(output,dim=(2,3))
return output
if __name__ == '__main__':
rnd = torch.rand(1,3,224,224) #bchw
net = Vgg16(10)
pre_y = net(rnd)
print("pre_y:",pre_y)
print("shape:",pre_y.shape)
5.ResNet(2015)
0.知识储备
问题产生:
在2014之后研究发现VGG等网络深层比浅层的性能更好。在网络层数叠加之后发现出现了一种不合常理的现象,层数越多(>20),效果越差,称之为深层网络性能退化(Degradation)也就是网络退化,其核心根源是由于 "恒等映射"( 即让深层网络的输出≈浅层网络的输出**)难以维系。**
核心矛盾:
前向传播的角度:随着卷积的层数叠加,深层网络难以实现 "恒等映射",特征表示能力下降。
反向传播的角度:反向梯度更新时由于层数跌多,梯度经过多层传递后衰减 / 爆炸,深层权重无法有效更新 。
针对上述现象何凯明团队提出【残差连接】:「残差连接(Residual Connection)」的本质 ------ 给深层网络开一条 "捷径",让梯度和特征可以直接传递,把 "学习复杂映射" 转化为 "学习简单残差"。
1.结构

知识储备:
1.残差的基本计算公式 是 H(x) = X+F(x) -> F(x) 本质是单个残差块的变化量。
X:输入tensor。
F(x):残差块操作后的tensor (本质上是与原tensor有微小的出入)。
H(x):下一个残差块的输入,本质为add融合tensor。
⭐【恒等映射】:本质是残差块希望输出的目标 。残差块希望输出 H (x)≈输入 x(输入 = 输出),通过 "shortcut 直接传 x + 主路径学残差 F (x)" 的设计,让深层网络能轻松学会这种 "不添乱、可微调" 的映射,避免特征退化。
2.设计亮点
亮点1:
添加残差连接(Residual Connection)== shortcut
本质:添加了一条捷径残差连接(也称shortcut)用于前向传播和反向传播的专属路径,前向传播传输的是X(feature_map),反向传播传播的是梯度。
⭐"前向双路传特征,反向双路传梯度"
前向传播的角度理解残差链接的作用:
答:主路拟合残差F(x),shortcut传输原始的输入特征X,最终对照恒等映射的目标阶段,有了捷径我们只需要在X的基础上进行微调F(x)即可。本质就是:不用从零构建 H (x),而是以 x 为 "基础模板",只学需要补充的 "微调部分"(F (x))
- 无残差连接:网络要硬学
H(x)(x 到输出的完整映射,难!); - 有残差连接:网络只学
F(x) = H(x) - x(x 和输出的 "差异 / 残差",易!);
反向传播的角度****理解残差链接的作用 :
数学角度分析:

亮点2:
标准化残差块设计
普通残差块(BasicBlock)和瓶颈残差块(Bottleneck)
本质是在主路径结构由网络层数深浅进行区分,浅层用普通残差块,深层用瓶颈残差块。瓶颈残差块其中添加了升维和降维的操作。
shortcut:本质就是残差链接的那条链接支线。不匹配指的是F(x)和X大小维度不匹配。
| 对比项 | 普通残差块(BasicBlock) | 瓶颈残差块(Bottleneck) |
|---|---|---|
| 主路径卷积层数 | 2 层 | 3 层 |
| 主路径核心特征 | 无降维,直接 3×3 卷积 | 1×1 降维→3×3 卷积→1×1 升维 |
| 适用 ResNet 型号 | ResNet18、ResNet34 | ResNet50、ResNet101、ResNet152 |
| 核心目的 | 简单堆叠,适合浅层网络 | 减少计算量 / 参数,适合深层网络 |
| 是否会处理 shortcut 不匹配 | 是(两种情况都有) | 是(两种情况都有) |
各版本核心参数对比(表格便于记忆)
| ResNet 版本 | 残差块类型 | 残差块组(共 4 组) | 总层数(卷积层 + 全连接层) | 核心适用场景 |
|---|---|---|---|---|
| ResNet18 | Basic Block | [2, 2, 2, 2] | 18 层(17 卷积 + 1 全连接) | 轻量化任务、嵌入式 |
| ResNet34 | Basic Block | [3, 4, 6, 3] | 34 层(33 卷积 + 1 全连接) | 平衡性能与速度 |
| ResNet50 | Bottleneck Block | [3, 4, 6, 3] | 50 层(49 卷积 + 1 全连接) | 通用场景(首选) |
| ResNet101 | Bottleneck Block | [3, 4, 23, 3] | 101 层(100 卷积 + 1 全连接) | 复杂任务(如检测) |
| ResNet152 | Bottleneck Block | [3, 8, 36, 3] | 152 层(151 卷积 + 1 全连接) | 超高精度需求 |
亮点3:
残差块中引入了BN(批量归一化)层次「卷积层之后、ReLU 激活之前」,和全局平均池化+1x1卷积核替代全连接层。
在残差块中所放的位置:


目的:将线性变化的值进行批量归一化操作,为之后的非线性变换做准备。
功能:
前向传播的角度:
1.加快收敛速度,延缓网络退化。2.解决了梯度爆炸的问题。
反向传播的角度:
1.缓解梯度消失。
3.小总结:
ResNet 精髓总结
- 核心创新:用残差连接(H (x)=F (x)+x)解决深层网络性能退化,通过恒等映射给梯度回传开 "捷径",彻底避免梯度消失;
- 工程设计:用基础块 / 瓶颈块适配不同深度,配合 BN、全局平均池化,实现 "深层不重、训练稳定";
- 历史意义:打破深层网络层数上限,奠定超深层 CNN 的基础,残差思想成为深度学习通用工具,至今仍是工业界核心 BackBone。
6.DenseNet(2017)
1.结构

DenceNet是在ResNet的基础上进行改进的。
ResNet 的残差连接是「当前层输入 + 前面某一层输出」(局部复用)
- 特征复用不够充分:ResNet 的相加会 "混合" 特征(比如两个特征相加后,原始特征的细节会被弱化);
- 仍有参数冗余:主路径的卷积层需要学习 "完整的残差",部分特征可能被重复学习。
DenseNet 的解决方案:让每个层的输入 = 前面所有层的输出(特征拼接)
2.设计亮点
亮点1:
密集连接(Dense Connection)
| 连接方式 | ResNet(残差连接) | DenseNet(密集连接) |
|---|---|---|
| 操作 | 特征逐元素相加(Add) | 特征通道维度拼接(Concat) |
| 第 L 层的输入 | 仅第 L-1 层的输出 | 第 1~L-1 层的所有输出 |
| 第 L 层的输出 | \(H_L(x) = F_L(x) + x\) | \(H_L(x) = [x_1, x_2, ..., x_{L-1}, F_L(x)]\)(方括号表示 Concat) |
| 特征复用范围 | 局部(仅前一层) | 全局(前面所有层) |
- ResNet 像 "接力赛":第 1 棒选手把接力棒(特征)传给第 2 棒,第 2 棒优化后传给第 3 棒(只复用前一棒);
- DenseNet 像 "多人合作":第 1 棒把接力棒给第 2 棒,第 2 棒把自己的棒 + 第 1 棒的棒一起给第 3 棒,第 3 棒把自己的棒 + 前两棒的棒一起给第 4 棒(所有前面的棒都被复用)。
亮点2:
工程优化 ------ 密集块(Dense Block)+ 过渡层(Transition Layer)(解决通道爆炸)
问题产生:
密集连接会导致通道数快速增长(比如前面有 5 层,每层输出 12 个通道,第 6 层的输入通道数就是 5×12=60),如果不控制,后续计算量会爆炸。DenseNet 用 "密集块 + 过渡层" 解决这个问题。
解决思路:密集快+过度层:
(1)密集块(Dense Block):特征复用的核心单元
- 结构:由多个 "瓶颈层(Bottleneck)" 组成,每个瓶颈层的流程是:
BN → ReLU → 1×1卷积(降维) → BN → ReLU → 3×3卷积(特征提取);- 设计巧思(避免通道爆炸):用 1×1 卷积先降维(比如把输入通道数从 60 降到 12),再用 3×3 卷积提取特征 ------ 这样每个瓶颈层的输出通道数固定(比如 12),密集连接后的通道数是 "层数 × 固定通道数",可控;
(2)过渡层(Transition Layer):密集块之间的通道压缩
- 作用:在两个密集块之间,压缩通道数,控制计算量;
- 结构:
BN → ReLU → 1×1卷积(降维,通道数变为前一个密集块输出的0.5~0.75) → 2×2平均池化(压缩尺寸);
3.小总结
| 对比维度 | ResNet | DenseNet |
|---|---|---|
| 连接方式 | 残差相加(Add) | 密集拼接(Concat) |
| 特征复用范围 | 局部(仅前一层) | 全局(前面所有层) |
| 通道数控制 | 残差相加不增加通道数 | 密集拼接增加通道数,靠过渡层压缩 |
| 参数效率 | 中等(ResNet50:25.6M) | 高(DenseNet-121:8M) |
| 计算复杂度 | 中等 | 略高(但参数少) |
| 适用场景 | 通用 BackBone(首选) | 小数据集、多任务(特征丰富) |
小案例:如果 DenseNet 的一个密集块有 5 个瓶颈层,增长率是 32,初始通道数是 64,那么这个密集块的输出通道数是?
答案:64 + 5×32 = 224**(初始通道数 + 层数 × 增长率)**
DenseNet 的核心是「用密集拼接实现全局特征复用,用瓶颈层 + 过渡层控制通道爆炸」
3.神经网络的设计
1.网络的构建标准
依据上述几种网络结构,每一种网络结构都有自己的优点和亮点,我们以已掌握的知识对神经网络进行设计,分为以下几个方面。
前向传播设计思考:
1.架构层面
- BackBone + Head
- CP->CCP->
- 使用module堆叠代替Layer堆叠
- 多分枝架构
- 多头架构
2.模块Module类型
- Inception
- ResNet
- DenseBlock
3.设计原则:更好的提取特征,核心就是特征融合
- 架构层面:FPN,PAN
- 模块层面: ResNet、DenseBlock、CSP....
- **尺寸(H,W):**多尺度、大特征+中特征+小特征
- 高级特征+低级特征融合:
- 高级特征:越高级越接近语义,人类能理解
- 低级特征:几何特征/图像特征
反向传播设计思考:避免梯度消失
2.残差边设计
归一化(如BN)的本质:"前层输出→当前层输入" 的分布映射,标准化为均值≈0、方差≈1 的稳定分布。
前向传播的角度理解(运用BN的优势):
运用在线性结果的归一化
1.解决内部协变量偏移,让每一层无需频繁适配前层的分布变化;
2.同时让激活函数的输入落在梯度较大的区域,避免饱和,保持特征提取的活性,最终缓解网络退化。
反向传播的角度理解(运用BN的优势):
运用在w权重的初始化
1.梯度量级稳定:通过标准化操作缩放梯度,避免梯度爆炸 / 消失;
2.正则化稳波动:训练 - 测试的分布差异带来轻微正则化,让梯度波动更平缓,减少过拟合导致的梯度异常。
通过以上3点保证梯度稳定回传
2.⭐训练架构的构建
常见的训练架构有8个流程
1.控制Layer层训练(冻结训练)
2.加载上次的模型(断点读训)
3.CE类别平衡系数
4.统计每个类别的ACC
5.AUG损失只做训练
6.最佳的best.pt存储
7.画趋势图
8.梯度学习率
接下来我对训练架构每一项进行解析,并最终运用到实例中
1.控制Layer层训练(冻结训练)
概述:是迁移学习中的核心技巧,指通过设置模型层参数的requires_grad=False,让这些层在反向传播时不计算梯度、不更新参数,仅训练未冻结的顶层 / 部分层。
为什么需要冻结?
- 预训练模型(如 ResNet、VGG)在大规模数据集(ImageNet)上学到的底层特征(边缘、纹理)具有通用性,无需重新训练,冻结后可保留这些有效特征。
- 减少待训练参数数量,降低显存占用,加快训练速度,同时避免小数据集上的过拟合。
- 分步训练:先冻结底层微调顶层,再解冻部分底层继续微调(解冻后学习率要更小,防止破坏已学特征)
实际怎么使用?
- 按层名 / 层级冻结:比如冻结 ResNet 的 conv1 到 layer3,只训练 layer4 和全连接层。
- 优化器仅传入未冻结参数:避免浪费计算资源在冻结参数上。
案例:
python
import torchvision.models as models
# 加载预训练模型
model = models.resnet18(pretrained=True)
# 冻结所有底层参数(先全局冻结)
for param in model.parameters():
param.requires_grad = False
# 解冻最后一层全连接层(仅训练该层)
in_features = model.fc.in_features
model.fc = torch.nn.Linear(in_features, 10) # 自定义分类头
# 此时只有model.fc的参数requires_grad=True
# 优化器仅传入可训练参数
optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)
2. 加载上次模型(断点续训)
概述:
加载上次模型并非仅加载模型参数,而是断点续训------ 恢复上一次中断的训练状态(模型参数、优化器状态、学习率、当前 epoch、训练 / 验证指标记录),确保训练无缝衔接,避免从头重训浪费时间。
两种加载方式:
| 加载方式 | 适用场景 | 代码示例 |
|---|---|---|
| 加载完整模型(结构 + 参数) | 无需修改模型结构,直接恢复预测 / 训练 | model = torch.load("model_full.pth") |
| 加载仅参数(推荐) | 需修改模型结构(如调整分类头),灵活性更高 | model.load_state_dict(torch.load("model_params.pth")) |
关键步骤:
- 训练前检查是否存在断点文件(如
checkpoint.pth)。 - 若存在,加载:模型参数、优化器状态、当前 epoch、最佳指标、指标记录列表。
- 若不存在,初始化相关状态。
python
import torch
# 断点文件路径
checkpoint_path = "checkpoint.pth"
start_epoch = 0
best_val_acc = 0.0
train_loss_list = []
val_loss_list = []
train_acc_list = []
val_acc_list = []
# 初始化模型、优化器
model = YourModel()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# 加载断点
if os.path.exists(checkpoint_path):
checkpoint = torch.load(checkpoint_path)
model.load_state_dict(checkpoint["model_state_dict"])
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
start_epoch = checkpoint["epoch"] + 1 # 从上一次结束的下一个epoch开始
best_val_acc = checkpoint["best_val_acc"]
train_loss_list = checkpoint["train_loss_list"]
val_loss_list = checkpoint["val_loss_list"]
print(f"成功加载断点,从epoch {start_epoch} 继续训练")
3. CE 类别平衡系数(解决类别不平衡)
概述:
交叉熵损失(Cross Entropy, CE)是分类任务的常用损失,但在类别不平衡场景(如医疗影像:患病样本 1%,正常样本 99%),模型会偏向多数类,导致少数类预测效果极差。
类别平衡系数即给 CE 损失添加类别权重(weight参数),对样本数少的类别赋予更大权重,样本数多的类别赋予更小权重,让损失计算更 "公平"。
公式:

案例:
python
import torch
import torch.nn as nn
import numpy as np
# 假设已统计每个类别的样本数:class_counts = [100, 500, 200, 800, 150, 300, 50, 400, 250, 600](CIFAR10不平衡版本)
class_counts = np.array([100, 500, 200, 800, 150, 300, 50, 400, 250, 600])
num_classes = len(class_counts)
total_samples = class_counts.sum()
# 计算简单权重
class_weights = total_samples / (num_classes * class_counts)
class_weights = torch.tensor(class_weights, dtype=torch.float32).cuda() # 对齐设备
# 定义带类别平衡的CE损失
criterion = nn.CrossEntropyLoss(weight=class_weights)
4. 统计每个类别 ACC(类别级准确率)
核心概念
整体准确率(Overall ACC)= 总正确预测数 / 总样本数,但在类别不平衡场景下极具欺骗性(比如 99% 正常样本,模型全部预测为正常,整体 ACC 达 99%,但患病样本 ACC 为 0)。
每个类别 ACC(Class-wise ACC)= 第 i 类正确预测数(TP_i) / 第 i 类真实样本数(TP_i + FN_i),能精准反映每个类别的预测性能
实现步骤(实际训练中)
- 初始化两个数组:
class_correct(记录每个类别的正确预测数)、class_total(记录每个类别的总样本数),长度为类别数,初始值为 0。 - 验证 / 测试阶段,遍历每个批次,获取预测标签和真实标签。
- 对每个样本,判断是否预测正确,更新对应类别的
class_correct和class_total。 - epoch 结束后,遍历每个类别,计算
class_correct[i] / class_total[i]得到该类 ACC。
案例:
python
import torch
num_classes = 10
class_correct = list(0. for i in range(num_classes))
class_total = list(0. for i in range(num_classes))
model.eval() # 评估模式
with torch.no_grad():
for data in val_loader:
images, labels = data
images, labels = images.cuda(), labels.cuda()
outputs = model(images)
_, predicted = torch.max(outputs, 1)
c = (predicted == labels).squeeze() # 每个样本是否预测正确
# 更新每个类别的统计
for i in range(len(labels)):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] += 1
# 打印每个类别ACC
class_acc_list = []
for i in range(num_classes):
class_acc = class_correct[i] / class_total[i] if class_total[i] > 0 else 0.0
class_acc_list.append(class_acc)
print(f"类别 {i} 的ACC: {class_acc:.4f}")
5. AUG 损失只做训练(数据增强仅用于训练阶段)
核心概念
数据增强(Data Augmentation,AUG)是通过对训练样本进行随机变换(如翻转、裁剪、亮度调整)来扩充数据量,提升模型泛化能力的技巧。
为什么验证 / 测试阶段不能用 AUG?
- 验证 / 测试的目的是评估模型对真实样本的预测能力,AUG 会改变样本的原始特征,导致评估结果失真(比如随机裁剪的测试样本,并非真实场景中的样本)。
- 仅训练阶段用 AUG,验证 / 测试阶段用原始样本(或仅做必要的归一化,无随机变换)。
案例:
python
import torch
from torchvision import datasets, transforms
# 训练集变换(含随机AUG)
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.RandomCrop(32, padding=4), # 随机裁剪
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
# 验证/测试集变换(无随机AUG,仅归一化)
val_test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
# 加载数据集
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform)
val_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=val_test_transform)
# 构建DataLoader
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=2)
6. 最佳的 best.pt 存储(保存验证集最优模型)
核心概念
模型训练过程中,最后一个 epoch 的模型不一定是最优的(可能出现过拟合,验证集性能下降),best.pt是指验证集性能最优(如最高 val_acc、最低 val_loss)的模型,训练时需实时监控并保存,同时删除之前的非最优模型,节省磁盘空间。
保存内容(推荐)
为了后续可继续微调或断点续训,best.pt应包含:
- 模型参数(
model_state_dict) - 最佳验证指标(
best_val_acc/best_val_loss) - (可选)优化器状态、当前 epoch(方便后续续训)
案例:
python
import os
import torch
best_val_acc = 0.0
best_model_path = "best.pt"
# 每个epoch验证后执行
current_val_acc = calculate_val_acc(model, val_loader) # 自定义函数计算验证集整体ACC
if current_val_acc > best_val_acc:
best_val_acc = current_val_acc
# 保存最佳模型
torch.save({
"model_state_dict": model.state_dict(),
"best_val_acc": best_val_acc,
"epoch": epoch,
"optimizer_state_dict": optimizer.state_dict()
}, best_model_path)
print(f"保存最佳模型,当前最佳val_acc: {best_val_acc:.4f}")
# (可选)删除旧的非最佳模型(若之前有保存)
# 此处可通过文件记录实现,简化版可直接覆盖
7. 画趋势图(训练指标可视化)
核心概念
训练指标可视化(趋势图)能直观反映模型的训练状态:
- 损失趋势:判断模型是否收敛(训练损失持续下降,验证损失先降后升说明过拟合)。
- ACC 趋势:判断模型分类性能的提升情况。
- 类别 ACC 趋势:判断少数类是否得到有效训练。
常用工具与实现步骤
- 工具 :
matplotlib(基础)、seaborn(更美观)。 - 步骤 :
- 训练时,将每个 epoch 的
train_loss、val_loss、train_acc、val_acc、class_acc_list存入列表。 - 训练结束后,用
matplotlib绘制子图(损失图 + 整体 ACC 图 + 类别 ACC 图)。 - 保存图片(如
training_curves.png),方便后续分析。
- 训练时,将每个 epoch 的
案例:
python
import matplotlib.pyplot as plt
import numpy as np
# 假设已记录各指标列表
# train_loss_list, val_loss_list, train_acc_list, val_acc_list, all_class_acc_list(每个epoch的类别ACC列表)
epochs = range(1, len(train_loss_list) + 1)
# 设置画布
plt.rcParams["font.sans-serif"] = ["SimHei"] # 中文显示
plt.rcParams["axes.unicode_minus"] = False # 负号显示
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 15))
# 1. 损失趋势图
ax1.plot(epochs, train_loss_list, label="训练损失", color="blue", linewidth=2)
ax1.plot(epochs, val_loss_list, label="验证损失", color="red", linewidth=2)
ax1.set_xlabel("Epoch")
ax1.set_ylabel("Loss")
ax1.set_title("训练损失 vs 验证损失趋势")
ax1.legend()
ax1.grid(True, alpha=0.3)
# 2. 整体ACC趋势图
ax2.plot(epochs, train_acc_list, label="训练ACC", color="blue", linewidth=2)
ax2.plot(epochs, val_acc_list, label="验证ACC", color="red", linewidth=2)
ax2.scatter(epochs[np.argmax(val_acc_list)], best_val_acc, color="green", s=100, label=f"最佳验证ACC: {best_val_acc:.4f}")
ax2.set_xlabel("Epoch")
ax2.set_ylabel("ACC")
ax2.set_title("训练ACC vs 验证ACC趋势")
ax2.legend()
ax2.grid(True, alpha=0.3)
# 3. 每个类别ACC趋势图
all_class_acc_np = np.array(all_class_acc_list)
num_classes = all_class_acc_np.shape[1]
for i in range(num_classes):
ax3.plot(epochs, all_class_acc_np[:, i], label=f"类别 {i}")
ax3.set_xlabel("Epoch")
ax3.set_ylabel("类别ACC")
ax3.set_title("每个类别ACC趋势")
ax3.legend()
ax3.grid(True, alpha=0.3)
# 保存图片
plt.tight_layout()
plt.savefig("training_curves.png")
plt.show()
print("趋势图已保存为 training_curves.png")
8. 阶梯学习率(StepLR,学习率衰减)
核心概念
学习率是优化器的核心超参数:
- 学习率过大:模型参数震荡,无法收敛到最优解。
- 学习率过小:模型收敛速度极慢,甚至陷入局部最优。
阶梯学习率(Step Learning Rate)是一种常用的学习率衰减策略:每隔固定数量的 epoch,将学习率乘以一个衰减因子(gamma),实现 "前期快速收敛,后期精细优化"。
相关策略(实际开发常用)
| 策略名称 | 核心逻辑 | 适用场景 |
|---|---|---|
| StepLR | 每 step_size 个 epoch,lr = lr * gamma | 通用场景,简单易调 |
| MultiStepLR | 在指定 epoch 列表(如 [10,20])处,lr = lr * gamma | 需精准控制衰减时机的场景 |
实现步骤
- 定义优化器后,定义学习率调度器。
- 每个 epoch 训练结束后(验证前 / 后),调用调度器的
step()方法更新学习率。 - (可选)打印学习率变化,方便监控。
python
import torch
import torch.optim as optim
# 定义优化器
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# 定义阶梯学习率调度器:每10个epoch,学习率乘以0.1
step_lr_scheduler = optim.lr_scheduler.StepLR(
optimizer,
step_size=10, # 每10个epoch衰减一次
gamma=0.1 # 衰减因子
)
# 训练循环中
for epoch in range(start_epoch, num_epochs):
# 训练步骤(省略)
train_one_epoch(model, train_loader, optimizer, criterion)
# 验证步骤(省略)
val_acc, val_loss = validate(model, val_loader, criterion)
# 更新学习率
step_lr_scheduler.step()
# 打印当前学习率
current_lr = optimizer.param_groups[0]['lr']
print(f"Epoch {epoch}, 当前学习率: {current_lr:.6f}")
完整代码:
python
import os
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import Subset
import matplotlib.pyplot as plt
# 配置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")
# 超参数
num_classes = 10
num_epochs = 30
batch_size = 64
lr = 1e-3
step_size = 10
gamma = 0.1
checkpoint_path = "checkpoint.pth"
best_model_path = "best.pt"
# ---------------------- 1. 数据准备(含AUG区分训练/验证、构造不平衡)----------------------
# 数据变换
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomCrop(32, padding=4),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
val_test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
# 加载CIFAR10
full_train_dataset = torchvision.datasets.CIFAR10(
root="./data", train=True, download=True, transform=train_transform
)
val_dataset = torchvision.datasets.CIFAR10(
root="./data", train=False, download=True, transform=val_test_transform
)
# 构造不平衡类别样本数
class_counts = np.array([50, 200, 100, 800, 150, 300, 20, 400, 250, 600])
np.random.seed(42)
imbalanced_indices = []
for cls in range(num_classes):
cls_indices = np.where(np.array(full_train_dataset.targets) == cls)[0]
sample_ratio = class_counts[cls] / max(class_counts)
sample_num = int(len(cls_indices) * sample_ratio)
selected_indices = np.random.choice(cls_indices, sample_num, replace=False)
imbalanced_indices.extend(selected_indices)
train_dataset = Subset(full_train_dataset, imbalanced_indices)
# 构建DataLoader
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
)
val_loader = torch.utils.data.DataLoader(
val_dataset, batch_size=batch_size, shuffle=False, num_workers=2
)
# ---------------------- 2. 模型搭建(含冻结训练)----------------------
model = torchvision.models.resnet18(pretrained=True)
# 冻结所有底层参数
for param in model.parameters():
param.requires_grad = False
# 解冻并替换顶层分类头
in_feat = model.fc.in_features
model.fc = nn.Linear(in_feat, num_classes)
model = model.to(device)
# ---------------------- 3. 损失函数(含类别平衡系数)----------------------
total_samples = class_counts.sum()
class_weights = total_samples / (num_classes * class_counts)
class_weights = torch.tensor(class_weights, dtype=torch.float32).to(device)
criterion = nn.CrossEntropyLoss(weight=class_weights)
# ---------------------- 4. 优化器与阶梯学习率----------------------
optimizer = optim.Adam(model.fc.parameters(), lr=lr)
lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=step_size, gamma=gamma)
# ---------------------- 5. 断点续训加载----------------------
start_epoch = 0
best_val_acc = 0.0
train_loss_list, val_loss_list = [], []
train_acc_list, val_acc_list = [], []
all_class_acc_list = []
if os.path.exists(checkpoint_path):
checkpoint = torch.load(checkpoint_path)
model.load_state_dict(checkpoint["model_state_dict"])
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
start_epoch = checkpoint["epoch"] + 1
best_val_acc = checkpoint["best_val_acc"]
train_loss_list = checkpoint["train_loss_list"]
val_loss_list = checkpoint["val_loss_list"]
train_acc_list = checkpoint["train_acc_list"]
val_acc_list = checkpoint["val_acc_list"]
all_class_acc_list = checkpoint["all_class_acc_list"]
print(f"加载断点成功,从epoch {start_epoch} 继续训练")
# ---------------------- 辅助函数:训练一个epoch----------------------
def train_one_epoch(model, loader, optimizer, criterion, 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 = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
epoch_loss = running_loss / total
epoch_acc = correct / total
return epoch_loss, epoch_acc
# ---------------------- 辅助函数:验证----------------------
def validate(model, loader, criterion, device, num_classes):
model.eval()
running_loss = 0.0
class_correct = [0. for _ in range(num_classes)]
class_total = [0. for _ in range(num_classes)]
total_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)
# 统计类别ACC
_, predicted = torch.max(outputs, 1)
correct_flag = (predicted == labels).squeeze()
for i in range(len(labels)):
label = labels[i]
if len(correct_flag.shape) == 0: # 批量大小为1时
class_correct[label] += correct_flag.item()
else:
class_correct[label] += correct_flag[i].item()
class_total[label] += 1
total += labels.size(0)
total_correct += (predicted == labels).sum().item()
# 计算指标
epoch_loss = running_loss / total
class_acc = [class_correct[i] / class_total[i] if class_total[i] > 0 else 0.0 for i in range(num_classes)]
epoch_acc = total_correct / total
return epoch_loss, class_acc, epoch_acc
# ---------------------- 6. 训练循环(含best.pt保存、指标统计)----------------------
for epoch in range(start_epoch, num_epochs):
print(f"\nEpoch {epoch}/{num_epochs - 1}")
print("-" * 50)
# 训练
train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, criterion, device)
train_loss_list.append(train_loss)
train_acc_list.append(train_acc)
print(f"训练损失: {train_loss:.4f}, 训练ACC: {train_acc:.4f}")
# 验证
val_loss, val_class_acc, val_acc = validate(model, val_loader, criterion, device, num_classes)
val_loss_list.append(val_loss)
val_acc_list.append(val_acc)
all_class_acc_list.append(val_class_acc)
print(f"验证损失: {val_loss:.4f}, 验证ACC: {val_acc:.4f}")
for i in range(num_classes):
print(f"类别 {i} ACC: {val_class_acc[i]:.4f}")
# 更新学习率
lr_scheduler.step()
current_lr = optimizer.param_groups[0]["lr"]
print(f"当前学习率: {current_lr:.6f}")
# 保存断点
torch.save({
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"epoch": epoch,
"best_val_acc": best_val_acc,
"train_loss_list": train_loss_list,
"val_loss_list": val_loss_list,
"train_acc_list": train_acc_list,
"val_acc_list": val_acc_list,
"all_class_acc_list": all_class_acc_list
}, checkpoint_path)
# 保存最佳模型
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save({
"model_state_dict": model.state_dict(),
"best_val_acc": best_val_acc,
"epoch": epoch,
"class_acc": val_class_acc
}, best_model_path)
print(f"保存最佳模型,当前最佳val_acc: {best_val_acc:.4f}")
# ---------------------- 7. 绘制趋势图----------------------
plot_training_curves(train_loss_list, val_loss_list, train_acc_list, val_acc_list, all_class_acc_list, best_val_acc)
# ---------------------- 加载最佳模型验证----------------------
best_checkpoint = torch.load(best_model_path)
model.load_state_dict(best_checkpoint["model_state_dict"])
print(f"\n加载最佳模型(epoch {best_checkpoint['epoch']},val_acc {best_checkpoint['best_val_acc']:.4f})")
final_val_loss, final_class_acc, final_val_acc = validate(model, val_loader, criterion, device, num_classes)
print(f"最佳模型最终验证ACC: {final_val_acc:.4f}")
# 删除断点文件(可选)
os.remove(checkpoint_path)
print("训练结束,断点文件已删除")
4.知识拓展补充
1.特征融合
特征融合的前提:遵循空间位置不变性原则。
特征融合的本质:整合不同特征的互补信息,增强模型的特征表达能力。
1.Add:
- 数学层面:FM3 = FM1+FM2,要求CHW相同。
- 距离层面:适合近距离操作,例如残差边+X的融合。
- 融合层面:特征处理:化学反应。
2.Concat:
- 数学层面:FM3 = FM1 Cat FM2, 要求HW相同即可**(HW不等也要cat的话,一般采取上采样)**
- 距离层面:适合module间的远距离融合
- 融合层面:物理反应
