PyTorch03-PyTorch的主要组成模块
三、PyTorch的主要组成模块
3.1 深度学习流程及与机器学习差异
一项机器学习任务的整体流程如下:
- 数据准备与划分:对原始数据进行清洗、格式统一与特征变换,并划分为训练集、验证集和测试集(如采用随机切分或 K 折交叉,可以使用sklearn带的test_train_split函数、kfold来实现)。
- 模型与算法配置:选择合适的网络架构,指定衡量误差的损失函数、更新参数的优化器以及相关超参数(可以使用sklearn这样的机器学习库中模型自带的损失函数和优化器)。
- 模型训练(拟合):将训练集输入模型,通过反向传播算法迭代更新权重以拟合数据。
- 性能评估与调优:在验证集和测试集上评估模型的泛化能力,并根据表现进行超参数微调或最终效果验证。
深度学习与机器学习的差异如下:
| 对比维度 | 传统机器学习 | 深度学习 |
|---|---|---|
| 本质定义 | 数据驱动让程序从经验中提升性能,包含多种算法范式 | 机器学习的一个分支,特指用多层神经网络自动提取特征 |
| 特征提取方式 | 人工设计特征。依赖专家经验从原始数据中提取有用特征(如TF-IDF、HOG、SIFT等),特征的好坏直接影响模型上限 | 自动学习特征。网络底层自动学习边缘、纹理等低级特征,高层组合成语义特征,实现端到端学习,无需人工设计 |
| 数据需求量 | 较小。几百到几万样本即可取得不错效果,在小数据集上表现稳定,不易过拟合 | 较大。因参数量巨大,通常需要十万级以上样本才能充分训练,否则容易过拟合;可通过迁移学习/数据增强缓解 |
| 硬件依赖 | CPU即可胜任。计算量相对较小,普通笔记本电脑即可运行大部分算法 | 重度依赖GPU。矩阵运算密集,需要GPU加速才能在大规模数据和深层网络上合理时间内完成训练 |
| 数据加载方式 | 通常可全量加载。数据量小,可一次性读入内存,训练过程简单直接 | 需mini-batch分批加载。数据量和模型参数量远超显存容量,必须分批次送入GPU训练,配合DataLoader、预读取等机制;批大小(batch size)本身也是影响模型收敛的重要超参数 |
| 模型构建方式 | 调用现成算法库。通常直接调用sklearn等库中的完整算法类,传入参数即可,无需逐层搭建网络结构 | 逐层搭建或模块组装。需手动定义网络各层(卷积层、池化层、批正则化层、LSTM层等),或预定义功能模块再组装,灵活性强但代码实现要求更高 |
| 损失函数与优化器 | 与深度学习类似,均需设定损失函数和优化器;但传统模型多为凸优化,调参相对简单 | 损失函数和优化器需保证反向传播能在用户自定义的复杂网络结构上正确实现;非凸优化对学习率、动量等超参数更敏感 |
| GPU配置与操作 | 通常不需要。大部分传统ML算法在CPU上运行即可满足需求 | 必须显式管理。需将模型和数据手动“放到”GPU(.cuda()或.to(device)),损失函数和优化器也需在GPU上工作;多卡训练还需考虑模型并行/数据并行、梯度同步、结果汇总等问题;验证/评测时常需将数据“放回”CPU |
| 训练与验证流程 | 全量数据一次性训练。通常一轮训练即可收敛(如线性回归闭式解),或少量迭代(如随机森林建树) | 按批循环训练。每个epoch遍历所有batch,每批数据前向传播→计算损失→反向传播→优化器更新参数;需协调DataLoader、模型、损失函数、优化器、GPU之间的配合;验证时同样按批计算指标 |
| 可解释性 | 较强。树模型可直接输出特征重要性排序,线性模型系数有明确含义,易于理解和调试 | 较弱。黑盒特性显著,难以直观解释内部决策逻辑,需借助Grad-CAM、SHAP、注意力可视化等事后工具辅助理解 |
| 适用数据类型 | 结构化表格数据为主。在金融风控、电商推荐等表格数据场景中表现优异 | 非结构化数据为主。图像、语音、文本、视频等原始高维数据是深度学习的主场 |
| 表格数据表现 | 常胜。XGBoost、LightGBM等梯度提升树在表格数据上至今是工业界首选 | 未必占优。纯深度模型在表格数据上通常打不过调优后的GBDT,需结合TabNet等专门设计才有竞争力 |
| 图像/语音/NLP | 基本退场。传统方法在这些领域已被深度学习全面超越 | 绝对主场。CNN统治图像,RNN/Transformer统治序列数据,大语言模型引领NLP前沿 |
| 典型算法举例 | 线性回归、逻辑回归、支持向量机(SVM)、决策树、随机森林、XGBoost、K近邻(KNN)、K均值聚类 | 卷积神经网络(CNN)、循环神经网络(RNN/LSTM)、Transformer、生成对抗网络(GAN)、扩散模型(Diffusion)、BERT/GPT |
其中需要特别提到的是,深度学习所需的样本量一般更大,所以相关数据(训练集/验证集/测试集)一般无法一次性被加载到内存或显存中,此时一般分批输入数据。因此深度学习在数据加载上需要有专门的设计。
3.2 基本配置
一些常用的包
在使用PyTorch中,需要导入一些常用的包来使用其提供的特定功能。常见的包有如下:os、sys、torch、torch.nn、torch.utils.data.Dataset、torch.utils.data.DataLoader、torch.optimizer、numpy、matplotlib、seaborn,如果涉及到表格读取与处理一般会用到pandas,下游分析和指标计算一般会用到sklearn。
1 | |
一些常用的超参数:
可以通过变量的形式设置超参数,然后在代码中使用:
1 | |
也可以使用yaml、json,dict等文件来存储超参数,这样可以方便后续的调试和修改,常见的深度学习库(mmdetection,Paddledetection,detectron2)和一些AI
Lab里面比较常用这种方式。
指定GPU
如果未指明要使用GPU,程序中默认使用的是CPU。如果指明了使用GPU但未指定GPU编号,那么默认就一直使用第0个GPU,容易造成此GPU资源如显存耗尽,所以最好明确要使用的GPU的编号。一般有如下两个方法
1 | |
3.3 数据读入
此小节学习如下内容:PyTorch常见的数据读取方式、构建自己的数据读取流程。
在PyTorch中,通过Dataset与DataLoader的配合使用,完成对的数据集的加载与批量读入。其中Dataset告诉 PyTorch 数据在哪里、怎么读取某一条具体的数据(比如读取一张图片、一段文本或一行表格数据)、读取出来后要做什么预处理(数据变换/Augmentation),DataLoader本身就是一个可迭代对象(Iterable)、以迭代的方式不断按批次(就是一次读入固定的N个单数据,最后一个批次可能小于N个单数据)读入数据。
总结起来这二者的作用如下:
Dataset:解决“单条数据的提取与转换”问题(个体逻辑)。
DataLoader:解决“批量化、打乱、多线程加速与内存流式供给”问题(工程效率)。
不管处理的是图像、语音还是文本,DataLoader 的批处理逻辑是通用的,只需要编写对应数据类型的 Dataset 即可。PyTorch官方也提供了一些现成的具体Dataset类实现。
可以自定义类来实现灵活的数据读取,自定义类需要继承PyTorch自身的torch.utils.data.Dataset类。一般只需要重写几个核心方法:
__init__:初始化,传入外部参数、配置数据集运行所需的基础资源与参数。__getitem__:用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据。比如输入一个索引idx,返回第idx条处理好的数据(比如:读取第 50 张图,裁剪放缩后,转换为 Tensor 并与标签一起返回)。__len__:用于返回数据集的样本数。即告诉 PyTorch 一共有多少条数据(比如数据集一共 10000 张图)。
PyTorch 官方提供的一些现成 Dataset
具体实现类,可以直接使用。比如下面的datasets.ImageFolder及大致使用:
1 | |
此示例代码使用了PyTorch中自带的ImageFolder类读取按一定结构存储的图片数据。ImageFolder
会自动扫描 train_path
下的所有子文件夹名称作为类别(Class),并自动为每个类别分配一个整数标签(0, 1, 2...)。
1 | |
其中data_transform可以对图像进行一定的变换,如翻转、裁剪、缩放等操作,可自定义。
如下是一个自定义Dataset类的示例:
1 | |
构建好Dataset后,就可以使用DataLoader来按批次读入数据了,实现代码如下:
1 | |
其中相关参数的作用:
train_data/val_data:输入的数据集实例(即继承并实现了Dataset的对象),告诉 DataLoader 从哪里提取数据。batch_size:样本是按批次(Batch)读入的,指定每个批次包含的样本数量。num_workers:指定用于数据加载的多进程 CPU 线程/进程数。数值越大,后台并行读取数据的速度越快,可有效防止 GPU 等待数据(0 表示仅用主进程)。shuffle:指定是否在每个 Epoch 开始时打乱数据顺序。训练集设置为True可提高模型泛化能力;验证集通常设置为False以保持评估指标的可比性。drop_last:指定当总样本数不能被batch_size整除时,是否丢弃最后一个样本量不足的 Batch(设置为True可避免最后一个小批次影响 Batch Normalization 或计算图的形状)。
可以通过如下方式查看加载的数据(又用到了matplotlib):
1 | |
1 | |
3.4 模型构建
本节学习内容:PyTorch中神经网络的构造方法、PyTorch中特殊层的构建、LeNet的PyTorch实现。
人工智能的第三次浪潮主要受益于深度学习的突破,其成功得益于海量数据、GPU 算力飞跃以及深度神经网络算法(如结合了 BP 算法和 GPU 加速的卷积神经网络 AlexNet)的融合应用。
PyTorch中,自定义模型一般是基于继承nn.Module类来完成的,我们只需要重写其中一些方法(如__init__、forward)即可。这给灵活构造自定义模型提供方便捷。
3.4.1 神经网络的构造
PyTorch
中,torch.nn.Module是一个基本构造类,所有神经网络层(如
nn.Linear、nn.Conv2d)以及自定义的大模型,本质上都是它的子类。
以下示例通过继承 Module 类构造多层感知机。这里定义的 MLP 类重载了
Module 类的 __init__ 函数和
forward
函数。它们分别用于创建模型参数和定义前向计算(正向传播)。下面的 MLP
类定义了一个具有两个隐藏层的多层感知机。
1 | |
上述执行过程大致如下:
- 这个MLP的__init__方法中定义了两个线性函数,一个激活函数。然后在forward方法中使用了它们
self.hidden(x):输入特征数是 784(例如将 \(28 \times 28\) 的图像展平后的向量),通过矩阵乘法 \(xW^T + b\) 转换后,输出 256 个特征数。self.act(...):ReLU 是一个激活函数,计算公式为 \(\max(0, x)\)。它不会改变张量的形状或特征数量,只是把这 256 个特征值中小于 0 的部分变成 0,大于 0 的保持不变,因此最终输出是 256 个激活后的特征值。self.output(o):将这 256 个特征值传递到输出层,最终映射转换为 10 个特征值(比如对应 10 个类别的预测 logits)。
以上的 MLP 类中⽆须定义反向传播函数。系统将通过⾃动求梯度⽽自动⽣成反向传播所需的 backward 函数。
nn.Module类中包含__call__属性且是一个可执行方法,按照Python中的
__call__
协议,nn.Module类实例就可以当成一个方法来调用,且调用时实际上执行的就是__call__属性对应的方法。以下代码首先将MLP实例化为net,通过调用net(x)最终将导致调用MLP类中重写的forward方法(而这也反过来要求我们在继承nn.Module类自定义子类时,一定要重写实现forward方法)。
1 | |
1 | |
注意,此示例中nn.Module的命名特征,翻译过来叫“模块”,而不是叫Layer层或Model模型,因为该类被定义成一个可供⾃由组建的部件。Module
里可以包含 Module,任意多个 Module 可以组合成更大、更复杂的 Module。
继承nn.Module的子类即可以是某某层(如池化层、线性化层等),也可以是一个模型(如上述MLP),甚至一个模型的一部分(在中大型复杂模型如
ResNet、Transformer、YOLO 等中,“模型的一个部分”被经常单独封装为一个
nn.Module )。
3.4.2 神经网络中常见的层
深度学习中提供了或可实现神经网络中各式各样的层,如全连接层、卷积层、池化层与循环层等等。虽然PyTorch中已经预置了大量的层,但我们在实际开发中仍可能需要自定义层,此时就是一个继承
nn.Module类实现子类的过程。
3.4.2.1 不含模型参数的层
先介绍如何定义一个不含模型参数的自定义层。以下示例中定义了一个层:将输入减掉均值后输出。
1 | |
__init__
只负责“搭建舞台”(初始化网络结构与权重),forward
才是真正的“演出过程”(数据流转与计算)。
3.4.2.2 含模型参数的层
还可以自定义包含模型参数的层。其中的模型参数可以通过多轮训练反向传播更新学习。
torch.nn.Parameter类是 torch.Tensor
的子类。当我们在自定义层或模型时,如果某个张量是实际上是torch.nn.Parameter实例,那么它将被自动添加到模型的参数列表中,以便于后续跟踪、求导。所以在⾃定义含模型参数的层时,应该将这些模型参数定义成
Parameter类型。除了直接定义成 Parameter
类外,还可以使⽤ ParameterList 和
ParameterDict 分别定义参数的列表和字典。
1 | |
1 | |
3.4.2.3 二维卷积层
虽然在深度学习中我们习惯叫“卷积”运算,但严格从数学定义上讲,神经网络里的卷积层实际上执行的是“互相关运算(Cross-correlation)”
- 互相关运算:用一个较小的矩阵(卷积核 \(K\))在较大的输入矩阵(输入 \(X\))上从左到右、从上到下按步长滑动。每移动到一个位置,就将卷积核与输入对应的子区域按元素相乘再求和,得到输出矩阵 \(Y\) 中的一个点。
- 标量偏差(Bias \(b\)):一个可学习的标量,加到互相关运算结果的每一个元素上。
- 参数更新:卷积核权重 \(W\) 和偏置 \(b\) 就是模型要学习的“参数”。它们初始时是随机给出的,在训练过程中通过反向传播(Backpropagation)和梯度下降逐步优化。
卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常先对卷积核随机初始化,然后不断迭代更新卷积核和偏差。
1 | |
卷积窗口形状为\(p×q\)的卷积层称为\(p×q\)卷积层。同样,\(p×q\)卷积或\(p×q\)卷积核说明卷积核的高和宽分别为p和q。一般情况下,p与q相等。
如果不手动指定初始化方法,PyTorch 会使用默认的 Kaiming 均匀分布(Kaiming Uniform Initialization) 来随机填充这个 \(p \times q\) 矩阵中的每个数值,每个元素有取值均匀分布在区间 \([-k, k]\) 内,其中 \(k = \frac{1}{\sqrt{\text{in\_channels} \times \text{p} \times \text{q}}}\)。
为了防止图像尺寸过快收缩(如果不加 Padding,每经过一层 \(3 \times 3\) 卷积,图像高宽就会减小 \(2\)。经过十几层卷积后,图像就会缩小为 \(1 \times 1\),无法构建很深的网络结构。)、保护边缘信息(位于输入图像边缘和角落的像素点,如果不加 Padding,被卷积核扫描到的次数远远少于中心区域的像素;加了 Padding 之后,边缘像素也能被充分提取特征),我们在实际应用中会对输入数据上下左右加上填充(padding)。
填充的元素值一般都是 0,PyTorch 中 nn.Conv2d 的
padding 参数默认采用的就是
零填充。加上填充后,我们可以使输入数据与输出数据有相同高度、宽度。
1 | |
还可以为卷积核窗口设置不同的高与宽。如果卷积核的高和宽不同时,可以通过在高和宽上设置不同的填充数,最后使得输出和输入具有相同的高和宽。
1 | |
填充可以增加输出的高和宽。这常用来使输出与输入具有相同的高和宽。
做卷积过程中,卷积窗口在输入数据上按照从上到下、从左到右的规则滑动,每次滑动的行数或列数称为步幅(stride),行步幅与列步幅可以不同。
1 | |
步幅可以减小输出的高和宽,例如将输出的高和宽变为指定的大小。
3.4.2.4 池化层
池化层每次对输入数据的一个固定形状窗口(⼜称池化窗口)中的元素计算输出。池化跟卷积操作顺序上有些类似,比如在二维池化中,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动计算池化窗口中满足计算规则的值。但此处的计算规则跟卷积中使用的计划规则不同,卷积中是计算卷积窗口中的输入与卷积核的互相关性,池化中是直接计算池化窗口中满足特定条件的元素值(如均值、最大值等)。
以下代码示例展示了最大池化层的前向计算过程:
1 | |
3.4.2.5 全连接层
全连接层(Fully Connected Layer,简称 FC
层)也常被称为线性层(Linear
Layer)或稠密层(Dense
Layer)。它的特点是:前一层的每一个输入特征都与当前层的每一个输出神经元相连,因此称为“全连接”。在
PyTorch 中通常使用 nn.Linear 来定义。
对于单个样本,若输入特征向量为 \(\boldsymbol{x}\in\mathbb{R}^{d_{in}}\),输出为 \(\boldsymbol{y}\in\mathbb{R}^{d_{out}}\),全连接层计算的是:
\[ \boldsymbol{y}=\boldsymbol{x}\boldsymbol{W}^{T}+\boldsymbol{b} \]
其中,\(\boldsymbol{W}\in\mathbb{R}^{d_{out}\times d_{in}}\) 是可学习的权重矩阵,\(\boldsymbol{b}\in\mathbb{R}^{d_{out}}\) 是可学习的偏置向量。第 \(j\) 个输出元素可写成:
\[ y_j=\sum_{i=1}^{d_{in}}x_iW_{j,i}+b_j \]
- 输入与输出形状:
nn.Linear(in_features, out_features)通常接收形状为(batch_size, in_features)的张量,输出形状为(batch_size, out_features)。如果输入维度多于两维,它会把最后一维看作特征维度,其余维度保持不变。 - 参数数量:当
bias=True(默认值)时,参数数量为out_features × in_features + out_features。因此当输入维度很大时,全连接层的参数量和计算量都可能较大。 - 线性变换:全连接层本身只进行仿射变换,并不引入非线性。实际网络通常在其后接
ReLU、GELU等激活函数,从而拟合更复杂的非线性关系。 - 主要作用:它能够融合所有输入特征,完成特征维度变换。在分类网络末端,常将卷积网络提取到的特征映射为类别数对应的 logits;在回归任务中,则可映射为一个或多个连续预测值。
在图像分类网络中,卷积层输出常为
(batch_size, channels, height, width)。传统做法会先用
Flatten 将每个样本展平为长度
channels × height × width
的向量,再输入全连接层。为了减少参数量,现代网络也常先使用全局平均池化(Global
Average Pooling)把空间维度压缩为
1 × 1,然后只对通道特征使用全连接层。
全连接层与 \(1\times1\) 卷积层的关系:两者都可以在“通道维度”上做线性组合,但连接方式和适用场景不同。
- 对形状为
(N, C, H, W)的特征图,nn.Conv2d(C, C_out, kernel_size=1)会在每一个空间位置(h, w)上,将该位置的C个通道映射为C_out个通道;所有空间位置共享同一组权重。因此,它等价于对每个像素位置独立地应用同一个nn.Linear(C, C_out)。 - 若先将整个特征图展平,再使用
nn.Linear(C × H × W, out_features),则每个输出神经元会连接到所有通道和所有空间位置,可以直接建模不同位置之间的关系;它的参数量也通常远大于 \(1\times1\) 卷积层。 - 当特征图的空间尺寸为
1 × 1时,\(1\times1\) 卷积层与全连接层在计算上等价(权重形状的排列不同);但当H、W大于 1 时,\(1\times1\) 卷积并不等价于“展平后”的全连接层。
以下示例固定设置权重和偏置,展示 nn.Linear
的逐项计算过程,并验证其结果与手工矩阵计算一致:
1 | |
输出如下:
1 | |
3.4.2.6 批正则处理层
批正则处理层通常指 批量归一化层(Batch Normalization,简称 BatchNorm 或 BN)。它经常被放在卷积层或全连接层之后、激活函数之前,例如:
1 | |
名称中的 Normalization 是“归一化”,不是“正则化”。BN 的直接工作是调整中间特征的数值分布;它有时会带来一定的正则化效果,但它与在损失函数中加入 L1/L2 正则项、或随机丢弃神经元的 Dropout 不是同一件事。
在训练深层网络时,前面层的参数一更新,后面层接收到的特征分布也会随之改变。例如某一特征在这次迭代中的数值可能集中在 0 附近,而下一次迭代中可能整体变得很大或很小。BatchNorm 会根据当前小批量(mini-batch)的统计量,把各个特征先调整到较稳定的尺度,再交给后续层处理。这通常能让训练更稳定,并允许使用相对更大的学习率;但它不是保证模型一定收敛或一定提高准确率的“万能层”。
一维特征的计算过程:假设全连接层的输入形状为 (N, C),其中 N 是 batch_size、C 是特征数。对每一个特征通道 c,BN 会在当前 batch 的 N 个样本上分别计算均值和方差:
\[ \mu_{\mathcal{B},c}=\frac{1}{N}\sum_{i=1}^{N}x_{i,c} \]
\[ \sigma_{\mathcal{B},c}^{2}=\frac{1}{N}\sum_{i=1}^{N}(x_{i,c}-\mu_{\mathcal{B},c})^{2} \]
接着进行标准化:
\[ \hat{x}_{i,c}=\frac{x_{i,c}-\mu_{\mathcal{B},c}}{\sqrt{\sigma_{\mathcal{B},c}^{2}+\epsilon}} \]
其中 \(\epsilon\) 是一个很小的常数,用于防止方差为 0 时除以 0。若只到这一步,每个特征在当前 batch 上的均值约为 0、方差约为 1。BN 最后还会进行可学习的仿射变换:
\[ y_{i,c}=\gamma_c\hat{x}_{i,c}+\beta_c \]
这里的 \(\gamma_c\)(缩放)和 \(\beta_c\)(平移)是每个通道各自拥有的可学习参数。它们使网络不被限制为“输出必须均值为 0、方差为 1”:模型可以在训练中学会保留、放大、缩小或平移某个特征。
卷积特征图的计算过程:卷积层通常输出形状 (N, C, H, W)。nn.BatchNorm2d(C) 仍然是“每个通道一组统计量”,但第 c 个通道的均值和方差会同时在 N、H、W 三个维度上计算,也就是用该通道内的全部 N × H × W 个数值计算,而不会把不同通道混在一起。输出形状保持为 (N, C, H, W) 不变。三种常见 BN 层的输入形式如下:
| 层 | 常见输入形状 | 统计范围 |
|---|---|---|
| nn.BatchNorm1d(num_features) | (N, C),或序列特征 (N, C, L) | 每个通道分别在 batch 维(以及存在时的序列长度维)统计 |
| nn.BatchNorm2d(num_features) | 图像特征图 (N, C, H, W) | 每个通道分别在 N、H、W 维统计 |
| nn.BatchNorm3d(num_features) | 三维体数据 (N, C, D, H, W) | 每个通道分别在 N、D、H、W 维统计 |
训练模式与评估模式的区别:这是使用 BN 时很重要的一点。
- 在 model.train() 的训练模式下,BN 使用当前 batch的均值和方差完成前向计算;同时会把这些统计量以滑动平均的方式更新到 running_mean 和 running_var 中。
- 在 model.eval() 的评估/推理模式下,BN 不再依赖当前输入 batch 的统计量,而是使用训练期间累计得到的 running_mean 和 running_var;这使得同一张图片单独推理或与其他图片一起推理时,结果不会因为同批的其他样本不同而变化。
- running_mean、running_var 是模型的 buffer(缓冲区),会随 state_dict() 一起保存和加载,但不会由优化器通过反向传播更新;可学习的则是 weight(即 \(\gamma\))和 bias(即 \(\beta\))。
PyTorch 中 momentum 的默认值为 0.1。每次训练前向传播后,运行均值大致按下面的方式更新:
\[ \text{running\_mean}_{new}=(1-\text{momentum})\times\text{running\_mean}_{old}+\text{momentum}\times\mu_\mathcal{B} \]
这里的 momentum 是“当前 batch 统计量在运行统计量中所占的更新比例”,含义不要与 SGD 等优化器中的动量混淆。通常不需要手动修改它。
有一个用于理解数值的实现细节:PyTorch 在训练前向传播中用“除以 N”的有偏方差(也就是示例中 x.var(..., unbiased=False) 的结果)完成标准化;但更新 running_var 时使用无偏方差(除以 N-1)。因此示例第一个特征的前向方差为 1.25,而首次更新后的运行方差为 0.9 × 1 + 0.1 × 1.6667,约为 1.0667。初学时只需知道:应让 PyTorch 自动维护 running_var,而不要手动替换它。
BatchNorm 的常用构造参数包括:
- num_features:通道数/特征数。例如卷积输出通道为 64 时,应写 nn.BatchNorm2d(64),不是写 batch_size。
- eps=1e-5:公式中的 \(\epsilon\),通常使用默认值即可。
- momentum=0.1:更新运行均值、运行方差的比例,通常使用默认值即可。
- affine=True:是否保留可学习的 \(\gamma\)、\(\beta\)。默认值为 True;若为 False,BN 只做标准化。
- track_running_stats=True:是否维护供评估模式使用的运行统计量。默认值为 True,一般保持默认值。
下面的完整示例使用 BatchNorm1d 展示“当前 batch 的统计量”“可学习的仿射参数”和“训练/评估模式差异”。为方便核对,示例手动设置了 \(\gamma\) 与 \(\beta\):
1 | |
运行时可观察到:
- batch_mean 分别为 [2.5, 25.0, 250.0],说明 BN 是对每一个特征列独立计算,不会把三列数值混在一起。
- torch.allclose(y_train, y_manual) 为 True,说明训练模式下的输出符合“标准化后乘以 \(\gamma\)、再加 \(\beta\)”的公式。
- 由于这里手动设置了不同的 \(\gamma\)、\(\beta\),y_train 的各列最终均值并不一定是 0、方差也不一定是 1;均值为 0、方差为 1 的是中间量 x_normalized。
- 训练后 running_mean 和 running_var 不等于当前 batch 的统计量,因为它们是从初始值按 momentum=0.1 做一次滑动更新得到的。
- 对同一个 x,训练模式和刚切换到评估模式后的输出通常不同:前者使用当前 batch 的统计量,后者使用尚未完全稳定的运行统计量。这是正常现象,并不是 BN 出错。
BN 依赖一个 batch 内的统计量。当训练 batch 很小(尤其每个通道的有效元素数很少)时,均值和方差的估计可能不稳定;对于小 batch 的检测、分割或生成任务,常会根据具体网络与任务考虑 GroupNorm、LayerNorm 等替代方案。无论使用哪一种归一化层,都应正确切换 model.train() 与 model.eval(),不要只切换部分 BN 层。
3.4.3 模型示例
前面提到过,可以使用torch.nn包来构建神经网络。nn包则依赖于autograd包来定义模型并对它们求导。一个nn.Module包含各个层和一个forward(input)方法,由forward(input)方法返回模型或层的输出output。
3.4.3.1 LeNet
上图是LeNet的网络结构,图来自杨立昆等著的论文《Gradient-based learning applied to document recognition》。
LeNet是一个简单的前馈神经网络,它接受一个输入,然后一层层不断向前传递,直到最后的输出。
一个神经网络的典型训练过程如下:
- 定义包含可学习参数(如权重 Weight 和偏置 Bias)的神经网络
- 在输入数据集上迭代
- 通过网络处理输入
- 计算 loss (输出和正确答案的距离)
- 将梯度反向传播给网络的参数
- 更新网络的参数(Parameters),一般使用简单的规则:\(\text{parameter} = \text{parameter} - \text{learning\_rate} \times \text{gradient}\)
3.4.3.1.1 LeNet-5重点讲解
以下是LeNet-5的现代 PyTorch 适配版本代码:
1 | |
可以看到,其中定义了 forward
函数但没有显式定义backward函数。backward函数会在使用autograd时自动定义(即自动在那些requires_grad=True的张量上启动追踪),backward函数用来计算导数。可以在
forward 函数中使用任何针对张量的操作和计算。
前面提到过,“模型”或“层”或“模型中的某个子模块/部件”中可学习参数会⾃动被添加到模型的参数列表里。这个参数列表可以通过net.parameters()返回。
1 | |
在 PyTorch 中,所有的 2D
卷积层(nn.Conv2d)都强制要求输入数据必须是四维(4D)张量,其维度顺序固定为:
\[\text{(Batch\_Size, Channels, Height, Width)}\]
- Batch_Size(批量大小):一次性喂给模型的样本数量。
- Channels(通道数):图像的颜色通道(如 RGB 为 3 通道,单通道灰度图为 1 通道)。
- Height(高度) & Width(宽度):图像像素的平面尺寸。
1 | |
1 | |
注意:torch.nn只支持小批量处理 (mini-batches)。整个
torch.nn
包只支持小批量样本的输入,不支持单个样本的输入。比如,nn.Conv2d
接受一个4维的张量,即nSamples x nChannels x Height x Width如果是一个单独的样本,只需要使用input.unsqueeze(0)
来添加一个“假的”批大小维度。
torch.Tensor- 一个多维数组,支持诸如backward()等的自动求导操作,同时也保存了张量的梯度。nn.Module- 神经网络模块。是一种方便封装参数的方式,具有将参数移动到GPU、导出、加载等功能。nn.Parameter- 张量的一种,当它作为一个属性分配给一个Module时,它会被自动注册为一个参数。autograd.Function- 实现了自动求导前向和反向传播的定义,每个Tensor至少创建一个Function节点,该节点连接到创建Tensor的函数并对其历史进行编码。
上述LeNet-5的现代 PyTorch 适配版本代码实现了一个经典的
LeNet-5 卷积神经网络架构,展示了在 PyTorch
中从网络结构定义、前向传播计算、参数查看到伪数据模拟训练推理的完整流程。详细拆解如下。
3.4.3.1.2
网络结构定义(class Net)
这个网络包含了 2 个卷积层(CNN) 和 3 个全连接层(FC / Linear)。
1. 构造函数
__init__():实例化各网络层
self.conv1 = nn.Conv2d(1, 6, 5)- 输入通道:
1(如单通道灰度图) - 输出通道:
6(使用 6 个不同的卷积核,提取 6 种不同的特征) - 卷积核大小:
5x5(省略了 padding,默认为 0) - 参数量:包含 \(6 \times 1 \times 5 \times 5 = 150\) 个权重及 \(6\) 个偏置。
- 输入通道:
self.conv2 = nn.Conv2d(6, 16, 5)- 输入通道:
6(接收上一层输出的 6 个特征图) - 输出通道:
16(生成 16 个特征图) - 卷积核大小:
5x5 - 参数量:包含 \(16 \times 6 \times 5 \times 5 = 2400\) 个权重及 \(16\) 个偏置。
- 输入通道:
self.fc1 = nn.Linear(16 * 5 * 5, 120)- 输入特征数:
16 * 5 * 5 = 400(卷积和池化后展平的特征维数) - 输出特征数:
120
- 输入特征数:
self.fc2 = nn.Linear(120, 84)- 输入特征数:
120 - 输出特征数:
84
- 输入特征数:
self.fc3 = nn.Linear(84, 10)- 输入特征数:
84 - 输出特征数:
10(通常对应 10 分类任务,如 MNIST 手写数字识别 0~9)
- 输入特征数:
2. 前向传播
forward(self, x):数据流向与形状变化
假设输入的图像张量形状为
[1, 1, 32, 32](即
[Batch_Size, Channel, Height, Width]):
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))conv1卷积:尺寸计算 \(32 - 5 + 1 = 28\),输出形状变为[1, 6, 28, 28]。ReLU激活:非线性映射,形状不变。max_pool2d池化:采用 \(2 \times 2\) 窗口,高宽减半,输出形状变为[1, 6, 14, 14]。
x = F.max_pool2d(F.relu(self.conv2(x)), 2)conv2卷积:尺寸计算 \(14 - 5 + 1 = 10\),输出形状变为[1, 16, 10, 10]。ReLU激活:形状不变。max_pool2d池化:高宽减半((2, 2)简写为2),输出形状变为[1, 16, 5, 5]。
x = x.view(-1, self.num_flat_features(x))- 展平(Flatten):利用
view将多维特征图打平成一维向量,准备喂给全连接层。 self.num_flat_features(x)计算出单样本特征总数为 \(16 \times 5 \times 5 = 400\)。-1表示自动计算 Batch 维度,形状变为[1, 400]。
- 展平(Flatten):利用
- 全连接层计算:
x = F.relu(self.fc1(x)):矩阵乘法,形状变为[1, 120]。x = F.relu(self.fc2(x)):矩阵乘法,形状变为[1, 84]。x = self.fc3(x):最后一层不加 ReLU,直接输出原始 logits,形状变为[1, 10]。
3. 辅助函数
num_flat_features(self, x)
Python
1 | |
- 作用:动态计算单个样本在进入全连接层之前的所有特征点总数(即 \(C \times H \times W\))。
3.4.3.1.3 主程序执行流程
1. 模型实例化与网络结构打印
Python
1 | |
- 实例化网络并打印出网络的层级架构结构图。
2.
查看可学习参数(net.parameters())
Python
1 | |
len(params)为什么是 10?网络一共有 5 层(
conv1,conv2,fc1,fc2,fc3),每一层包含 1 个 Weight(权重) 和 1 个 Bias(偏置),因此共有 \(5 \times 2 = 10\) 组参数张量。params[0].size():即conv1.weight的形状,打印结果为torch.Size([6, 1, 5, 5])。
3. 模拟数据前向传播
Python
1 | |
- 输出
out是一个形状为torch.Size([1, 10])的 Tensor,表示该输入图像在 10 个类别上的未归一化得分(Logits)。
3.4.3.1.4 极简总结与前向传播特征图变化图解
| 阶段 / 网络层 | 输入形状 (B, C, H, W) | 输出形状 (B, C, H, W) | 说明 |
|---|---|---|---|
| Input | [1, 1, 32, 32] |
[1, 1, 32, 32] |
伪造的输入数据 |
| conv1 + Pool | [1, 1, 32, 32] |
[1, 6, 14, 14] |
经过 5x5 卷积 + 2x2 最大池化 |
| conv2 + Pool | [1, 6, 14, 14] |
[1, 16, 5, 5] |
经过 5x5 卷积 + 2x2 最大池化 |
| view (展平) | [1, 16, 5, 5] |
[1, 400] |
\(16 \times 5 \times 5 = 400\) 铺平为向量 |
| fc1 + fc2 | [1, 400] |
[1, 84] |
连续经过两次带 ReLU 的线性变换 |
| fc3 (Output) | [1, 84] |
[1, 10] |
输出 10 个类别的预测得分 |
3.4.3.2 AlexNet
上图是AlexNet的网络结构,图来自Alex Krizhevsky等著的论文《ImageNet Classification with Deep Convolutional Neural Networks》
1 | |
1 | |
这段代码是 AlexNet 卷积神经网络在 PyTorch 中的经典实现。
AlexNet 是 2012 年 ImageNet
竞赛的冠军模型,也是深度学习复兴的标志性架构。这段代码对原始论文的
AlexNet 进行了微调(例如将输入通道数设为 1,输出类别数设为
10),使其能够直接运行在 Fashion-MNIST 或
MNIST 手写数字数据集上。
以下是对这段代码的逐模块详细拆解。
3.4.3.2.1 整体结构概述
代码通过继承 nn.Module 定义了两个核心部分(使用
nn.Sequential 串联层结构):
self.conv(特征提取器):由 5 个卷积层 和 3 个最大池化层 组成,负责将原始图像转化为高维抽象特征。self.fc(分类器):由 3 个全连接层 和 Dropout 层 组成,负责将提取出的特征映射到 10 个类别上。
3.4.3.2.2
特征提取层(self.conv)维度的详细演变
假设输入的图像尺寸为 Fashion-MNIST 标准的
(Batch, 1, 224, 224)(通常会将 \(28 \times 28\) 图像 Resize 到 \(224 \times 224\) 以适配 AlexNet
的大卷积核):
| 序号 | 层结构 | 参数配置 | 输出特征图尺寸 (Shape) | 作用与核心思想 |
|---|---|---|---|---|
| 1 | nn.Conv2d(1, 96, 11, 4) |
单通道输入, 96核, 11x11核, 步长4 | [B, 96, 54, 54] |
用大卷积核+大步长快速降低特征图分辨率,捕捉宏观纹理。 |
| 2 | nn.ReLU() |
- | [B, 96, 54, 54] |
引入非线性激活。 |
| 3 | nn.MaxPool2d(3, 2) |
3x3核, 步长2 | [B, 96, 26, 26] |
重叠池化(Kernel 3 > Stride 2),防止信息过快丢失,缓解过拟合。 |
| 4 | nn.Conv2d(96, 256, 5, 1, 2) |
256核, 5x5核, 步长1, Padding 2 | [B, 256, 26, 26] |
保持高宽不变,进一步增加通道数提取丰富特征。 |
| 5 | nn.ReLU() |
- | [B, 256, 26, 26] |
非线性激活。 |
| 6 | nn.MaxPool2d(3, 2) |
3x3核, 步长2 | [B, 256, 12, 12] |
再次重叠池化降低尺寸。 |
| 7 | nn.Conv2d(256, 384, 3, 1, 1) |
384核, 3x3核, 步长1, Padding 1 | [B, 384, 12, 12] |
连续卷积操作的第一层,不接池化,加深网络深度。 |
| 8 | nn.ReLU() |
- | [B, 384, 12, 12] |
非线性激活。 |
| 9 | nn.Conv2d(384, 384, 3, 1, 1) |
384核, 3x3核, 步长1, Padding 1 | [B, 384, 12, 12] |
连续卷积操作的第二层。 |
| 10 | nn.ReLU() |
- | [B, 384, 12, 12] |
非线性激活。 |
| 11 | nn.Conv2d(384, 256, 3, 1, 1) |
256核, 3x3核, 步长1, Padding 1 | [B, 256, 12, 12] |
压缩通道数,准备进入池化。 |
| 12 | nn.ReLU() |
- | [B, 256, 12, 12] |
非线性激活。 |
| 13 | nn.MaxPool2d(3, 2) |
3x3核, 步长2 | [B, 256, 5, 5] |
最终卷积输出:每个样本拥有 \(256 \times 5 \times 5\) 个特征点。 |
关键推导:
(1)在深度学习中,二维卷积输出尺寸的通用计算公式为:
\[H_{out} = \left\lfloor \frac{H_{in} + 2 \times \text{padding} - \text{kernel\_size}}{\text{stride}} \right\rfloor + 1\]
(2)最后的输出尺寸为
[B, 256, 5, 5],因此展平(Flatten)后的特征向量长度为 \(256 \times 5 \times 5 = 6400\)。
3.4.3.2.3
分类层(self.fc)与关键机制
特征提取完毕后,进入全连接分类层:
nn.Linear(256 * 5 * 5, 4096):- 输入维度是 \(256 \times 5 \times 5 = 6400\),映射到隐藏层 4096 维。
- 比起的 LeNet(120维),AlexNet 采用了巨大的隐层节点数(4096),极大地增强了模型的特征表达能力。
nn.Dropout(0.5):- 训练时以 50% 的概率随机断开神经元连接。
- 作用:全连接层参数极多(如第一层全连接就有 \(6400 \times 4096 \approx 2600 万\) 个参数),极其容易过拟合。Dropout 是 AlexNet 能成功训练如此大网络的核心技术之一。
nn.Linear(4096, 4096)\(\to\)nn.Linear(4096, 10):- 再次经过 4096 维隐层后,最终输出 10 维分类logits(对应 Fashion-MNIST 的 10 个类别)。
3.4.3.2.4 前向传播(forward)
1 | |
3.4.3.2.5 总结:AlexNet 相比 LeNet 的三大核心改进
- 结构更深更宽:从 LeNet 的 5 层增加到 8 层(5 卷积 + 3 全连接),通道数大幅提升(高达 384)。
- 激活函数改进:使用 ReLU 代替了 Sigmoid/Tanh,极大地加速了网络收敛并防止梯度消失。
- 防过拟合技术:引入了 Dropout 和 重叠池化(Overlapping Pooling),使得大参数量模型的泛化能力大幅提升。
3.5 模型初始化
本小节学习如下内容:常见的初始化函数、初始化函数的使用。
深度学习模型的训练中,权重初始化成符合某种规律的值比较重要。一个恰当的初始值会使模型收敛速度提高、准确率更高 ;不当的初始化值增加梯度消失或梯度爆炸的可能性。
一般情况下,不使用全0初始值训练网络。为了利于训练和减少收敛时间,需要对模型进行合理的初始化。
PyTorch的torch.nn.init包中提供了一些常用的初始化方法。
3.5.1 torch.nn.init内容
如PyTorch官网关于init的页面:https://docs.pytorch.org/docs/2.6/nn.init.html 所示,PyTorch中预置了一些常用的初始化方法:
- torch.nn.init.uniform_(tensor, a=0.0, b=1.0)
- torch.nn.init.normal_(tensor, mean=0.0, std=1.0)
- torch.nn.init.constant_(tensor, val)
- torch.nn.init.ones_(tensor)
- torch.nn.init.zeros_(tensor)
- torch.nn.init.eye_(tensor)
- torch.nn.init.dirac_(tensor, groups=1)
- torch.nn.init.xavier_uniform_(tensor, gain=1.0)
- torch.nn.init.xavier_normal_(tensor, gain=1.0)
- torch.nn.init.kaiming_uniform_(tensor, a=0, mode='fan__in', nonlinearity='leaky_relu')
- torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
- torch.nn.init.orthogonal_(tensor, gain=1)
- torch.nn.init.sparse_(tensor, sparsity, std=0.01)
- torch.nn.init.calculate_gain(nonlinearity, param=None)
可以发现这些函数除了calculate_gain,所有函数的后缀都带有下划线,意味着这些函数将会直接原地更改输入张量的值。
下表展示了不同类型的激活函数(nonlinearity)与其对应的计算增益(gain)的映射关系:
| nonlinearity | gain |
|---|---|
| Linear / Identity | 1 |
| Conv{1,2,3}D | 1 |
| Sigmoid | 1 |
| Tanh | 5/3 |
| ReLU | sqrt(2) |
| Leaky Relu | sqrt[2/(1+neg_slop^2)] |
| SELU | 3/4 |
“计算增益”(Gain),是一个缩放系数(Scaling Factor),用来抵消特定的非线性激活函数(Non-linearity)对数据方差(Variance)的改变,从而保持网络在前向传播时信号的方差稳定,防止梯度消失或梯度爆炸。
思考:线性激活函数的计算增益为什么为1?
3.5.2 torch.nn.init使用
在搭建或训练复杂的神经网络时,我们需要根据不同类型的网络层(如卷积层、全连接层、批归一化层等),为其选择最合适的权重初始化方法;而在代码实现中,我们通过
Python 的 isinstance()
函数来识别当前层到底属于哪种类型,从而精准地应用不同的
torch.nn.init 初始化策略。
3.5.2.1 为什么不同类型的层需要不同的初始化?
不同的网络层在数学逻辑和激活函数搭配上存在差异,混用同一种初始化方式会导致训练效果变差甚至无法收敛:
卷积层(
nn.Conv2d) & 全连接层(nn.Linear):它们包含权重(
weight)和偏置(bias)。如果后续接的是 ReLU 激活函数,通常建议使用 Kaiming (He) 初始化(如nn.init.kaiming_normal_);如果接的是 Tanh 或没有激活函数,通常建议使用 Xavier (Glorot) 初始化。批归一化层(
nn.BatchNorm2d):它的缩放参数
weight(即 \(\gamma\))通常初始化为 1(保持方差不变),平移参数bias(即 \(\beta\))初始化为 0(保持均值不变),通常不能使用基于随机分布(如正态分布normal_或均匀分布uniform_)的初始化函数,而是直接使用 全 1 初始化 和 全 0 初始化。偏置项(
bias):不管是卷积层还是全连接层,偏置通常直接清零(
nn.init.zeros_)。
3.5.2.2 为不同类型的层使用不同的权重初始化方法
1 | |
3.5.3 初始化函数的封装
在 PyTorch
中,一个网络(nn.Module)通常包含几十甚至几百个嵌套的子模块(Sub-modules)。我们不需要手动一个一个去找这些层,而是封装一个考虑了上述网络中出现的所有类型的层权重初始化方法如init_weights,利用
PyTorch 的 net.apply(init_weights)
方法结合 isinstance()
进行自动化判断与初始化。
1 | |
关于上述代码段的解释:
isinstance(m, nn.Conv2d):
m是当前遍历到的每一个子层模块(Module)。isinstance(m, Class)用于检查m是不是Class类的实例或其子类。- 如果
m是nn.Conv2d,表达式返回True,程序就会执行内部的卷积层初始化逻辑。
isinstance(m, (nn.Conv2d, nn.Conv1d)):
isinstance支持传入一个元组(Tuple),代表“只要是 1D 卷积或 2D 卷积中的任意一种”,就命中该条件。
net.apply(init_weights):
- 这是
nn.Module自带的一个递归函数。它会深度遍历net里的所有子模块(self.conv1、self.fc1、self.bn1等),并把每一个子模块作为参数传递给init_weights(m)执行一次。
3.6 损失函数
损失函数(loss
function,也常称代价函数)用一个可微的数值衡量模型预测与目标之间的差异。训练时,模型先前向计算得到预测,再计算损失,随后调用
loss.backward()
自动求导;优化器据此更新模型参数,使训练集上的损失趋于减小。
1 | |
本节以 PyTorch 2.6 的 API 为准。旧教程中的 size_average
与 reduce 已废弃,实际使用时统一通过 reduction
控制聚合方式:
reduction='mean':默认值,返回所有损失元素的平均值;reduction='sum':返回所有损失元素之和;reduction='none':不聚合,保留每个样本(或元素)的损失,适合自行加权或排查异常样本。
选择损失函数时,首先要看任务和模型输出的含义:分类模型通常直接输出
logits(未经过 sigmoid 或
softmax
的实数);回归模型输出连续数值;序列、排序和度量学习有各自专用的损失函数。
3.6.1
二分类:BCEWithLogitsLoss 与 BCELoss
二分类中每个样本只有“是/否”两种结果,标签通常为 0.0 或
1.0。BCEWithLogitsLoss 接收
logits,BCELoss 接收已经转换到 [0, 1]
的概率。新训练代码通常优先使用前者,以获得更好的数值稳定性。
BCEWithLogitsLoss
1 | |
功能:计算二分类或多标签分类的二元交叉熵,并在内部完成 sigmoid,适合直接接收模型 logits。
主要参数:
weight:按元素或按样本缩放损失;size_average:已废弃。旧代码中True表示平均、False表示求和;请改用reduction;reduce:已废弃。旧代码中False表示不聚合;请改用reduction='none';reduction:'none'、'mean'或'sum';pos_weight:按类别增加正样本权重,常用于正样本稀少的情况。
计算公式与说明:对一个元素,设 \(x\) 为 logits、\(y\in\{0,1\}\) 为目标、\(p=\sigma(x)\) 为预测为正类的概率,则:
\[ \ell(x,y)=-[y\log p+(1-y)\log(1-p)] \]
BCEWithLogitsLoss 在数值上以稳定方式直接由 \(x\) 计算上式,不需要先显式求 \(p\)。设置 pos_weight
后,正样本项会乘以该权重;批量损失再按 reduction
做逐元素保留、平均或求和。
1 | |
代码逐行解释:
logits = torch.tensor([...], requires_grad=True):创建形状为[4]的浮点张量,可理解为一个批次中的 4 个样本各自输出 1 个二分类原始得分。它们不是概率,因此可以小于 0 或大于 1。requires_grad=True会要求 Autograd 从此张量开始记录后续运算;在真实训练中,logits 通常来自模型最后一层,梯度会继续传回模型参数。targets = torch.tensor([1.0, 0.0, 1.0, 0.0]):创建与 logits 完全同形状的真实标签。BCE 系列要求标签是浮点张量而不是long,1.0表示正类、0.0表示负类。形状不一致会导致错误或不符合预期的广播计算。nn.BCEWithLogitsLoss(...):实例化损失函数。这里的pos_weight=torch.tensor([2.0])表示发生正样本错误时,正类损失项按 2 倍计入;它适用于正样本比负样本少得多的场景。未显式传入reduction,所以使用默认的'mean'。...(logits, targets):调用损失模块完成前向传播。内部不会先暴露一个实际的 sigmoid 张量再计算对数,而是采用更稳定的等价公式直接从 logits 得到损失;因此模型最后一层不应再手动加sigmoid。loss.backward():loss是一个标量,调用它会从损失节点沿计算图反向求导。此例的梯度保存在logits.grad;真实训练循环通常接着调用optimizer.step(),用这些梯度更新模型权重。print(...):用.item()把单元素损失张量转换成普通 Python 浮点数,便于打印或记录。不要对非标量损失直接backward(),除非提供对应的外部梯度。
内部计算过程:
示例数据:
logits = [1.2, -0.7, 0.3, -1.5];targets = [1, 0, 1, 0];pos_weight = 2,reduction='mean'。
计算步骤:
将 logits 转为概率(仅用于理解;实际函数内部以稳定方式完成):
\[p_i=\sigma(x_i)=\frac{1}{1+e^{-x_i}}\]
\[p\approx[0.7685,\ 0.3318,\ 0.5744,\ 0.1824]\]
逐样本计算加权二元交叉熵:正类项乘以
pos_weight=2。\[l_i=-[2y_i\log(p_i)+(1-y_i)\log(1-p_i)]\]
- \(l_1=-2\log(0.7685)\approx0.5266\);
- \(l_2=-\log(1-0.3318)\approx0.4032\);
- \(l_3=-2\log(0.5744)\approx1.1090\);
- \(l_4=-\log(1-0.1824)\approx0.2014\)。
得到最终输出:默认取平均值。
\[loss=\frac{l_1+l_2+l_3+l_4}{4}\approx0.5601\]
梯度含义:未加权时梯度方向与 \(\sigma(x_i)-y_i\) 一致;正样本的梯度会被
pos_weight 放大,使模型更重视漏判的正样本。
BCELoss
1 | |
功能:计算二分类的二元交叉熵。与
BCEWithLogitsLoss 不同,输入必须已经是范围在
[0, 1] 内的概率。
主要参数:
weight:为各损失元素设置手动缩放权重;size_average:已废弃。旧代码中的平均/求和开关,应改为reduction='mean'或'sum';reduce:已废弃。旧代码中关闭它相当于使用reduction='none';reduction:指定不聚合、平均或求和。
计算公式与说明:公式与二元交叉熵相同: \[ \ell(p,y)=-[y\log p+(1-y)\log(1-p)] \]
此公式中取对数时的底数是自然数e。此处输入 \(p\) 已经是概率,必须满足 \(0\leq p\leq1\);而
BCEWithLogitsLoss 的输入是
logits。两者的数学目标相同,区别主要在输入形式和数值稳定性。
1 | |
代码逐行解释:
logits = ... requires_grad=True:这 3 个值仍是模拟模型输出的原始得分。即使后面先经过 sigmoid,梯度链路仍会保留,因为 sigmoid 的输出依赖于logits。probabilities = torch.sigmoid(logits):逐元素应用 \(\sigma(x)=1/(1+e^{-x})\),将任意实数映射到(0,1)。这一步是BCELoss的前置条件;若误把 logits 直接传给BCELoss,输入不一定是合法概率,训练结果会不正确。targets = ...:标签与probabilities同为[3],且必须是 0/1 浮点数。BCELoss会将每个位置视作彼此独立的一次伯努利事件。loss = nn.BCELoss()(probabilities, targets):先创建默认reduction='mean'的 BCELoss,再立即调用。它逐元素计算交叉熵,最后返回一个标量平均损失。loss.backward():梯度先从平均损失传给probabilities,再通过 sigmoid 的导数 \(p(1-p)\) 传回logits。因此logits.grad可用,而probabilities.grad默认不保存,因为它不是叶子张量。print(...):打印当前批次的平均交叉熵;训练中该数值可用于观察模型是否在学习,但不能单独代表准确率。
内部计算过程:
示例数据:
logits = [1.2, -0.7, 0.3];targets = [1, 0, 1];reduction='mean'。
计算步骤:
Sigmoid 将 logits 转为 BCELoss 所需概率:
\[p=\sigma(logits)\approx[0.7685,\ 0.3318,\ 0.5744]\]
逐样本计算交叉熵:
\[l_i=-[y_i\log(p_i)+(1-y_i)\log(1-p_i)]\]
- \(l_1=-\log(0.7685)\approx0.2633\);
- \(l_2=-\log(1-0.3318)\approx0.4032\);
- \(l_3=-\log(0.5744)\approx0.5544\)。
得到最终输出:
\[loss=\frac{0.2633+0.4032+0.5544}{3}\approx0.4070\]
梯度含义:backward()
先计算损失对概率的导数,再乘 sigmoid 导数 \(p(1-p)\) 回传到
logits;预测正类概率不足时,梯度下降会增大对应 logits。
3.6.2
单标签多分类:CrossEntropyLoss
单标签多分类中,每个样本只属于一个类别。模型输出每个类别的 logits,目标是对应的类别索引。
CrossEntropyLoss
1 | |
功能:计算单标签多分类交叉熵,内部等价于
LogSoftmax + NLLLoss。输入为 [N, C] 的
logits,目标通常为 [N] 的类别索引。
主要参数:
weight:长度为类别数C的权重,用于类别不平衡;size_average:已废弃,请用reduction;ignore_index:忽略指定类别索引的损失,常用于序列填充;reduce:已废弃,请用reduction;reduction:损失聚合方式;label_smoothing:标签平滑系数,默认0.0。
计算公式与说明:设第 \(n\) 个样本的 logits 为 \(x_n\in\mathbb{R}^{C}\),正确类别索引为 \(y_n\),则无标签平滑时:
\[ \ell_n=-\log\frac{\exp(x_{n,y_n})}{\sum_{c=1}^{C}\exp(x_{n,c})} \]
它会提高正确类别的得分、压低其他类别的相对得分。label_smoothing>0
时,不再把全部目标概率放在正确类别上,而会向其他类别分配少量概率,从而减轻模型过度自信。
1 | |
代码逐行解释:
logits = torch.tensor([[...], ...], requires_grad=True):创建形状[3,4]的张量:第 0 维是 3 个样本,第 1 维是 4 个类别。每行的值只是相对得分,不是概率,因此每行不要求和为 1。targets = torch.tensor([0, 1, 3], dtype=torch.long):为每个样本保存一个正确类别的索引。CrossEntropyLoss的类别索引目标必须是long;这里不应传入[1,0,0,0]形式的 one-hot 向量。nn.CrossEntropyLoss(label_smoothing=0.1):实例化交叉熵。默认reduction='mean';label_smoothing=0.1让训练目标不再是绝对 one-hot,从而避免模型被过度鼓励给正确类输出概率 1。loss = criterion(logits, targets):前向计算时,对每一行 logits 独立做log_softmax,再取对应目标类别(或平滑后目标分布)的负对数似然;不需要、也不应该在此之前手工softmax。loss.backward():将 3 个样本平均后的标量损失反向传播。对每一行,正确类别的梯度倾向于使其得分上升,其他类别的梯度倾向于使其得分下降;标签平滑会减弱这个推动力度。argmax(dim=1):若用于推理,取每行最大 logits 的索引即可得到预测类。由于 softmax 是单调变换,先 softmax 再 argmax 得到的索引相同。
内部计算过程:
示例数据:第一行 logits 为
[2.0, 0.1, -1.0, 0.3],正确类别为 0;共有
\(C=4\)
类,label_smoothing=0.1。
计算步骤:
对第一行执行 softmax:
\[p_c=\frac{e^{x_c}}{\sum_j e^{x_j}}\approx[0.7239,\ 0.1082,\ 0.0360,\ 0.1320]\]
构造平滑后的目标分布:
\[q_0=1-0.1+\frac{0.1}{4}=0.925,\qquad q_1=q_2=q_3=\frac{0.1}{4}=0.025\]
计算该样本交叉熵:
\[l_1=-\sum_{c=0}^{3}q_c\log p_c\]
正确类 \(p_0\) 贡献最大,其他类的少量目标概率用于抑制过度自信。第二、三行分别使用标签 1、3 重复此过程。
得到最终输出:将三个样本损失按默认方式平均。
\[loss=\frac{l_1+l_2+l_3}{3}\]
梯度含义:每类的梯度近似为 \((p_c-q_c)/3\);正确类概率不足时提高正确类 logits,预测过高的错误类 logits 被压低。
3.6.3
回归:L1Loss、MSELoss 与
SmoothL1Loss
回归任务预测连续数值;三种损失函数的主要差异在于对大误差和异常值的惩罚方式。
L1Loss
1 | |
功能:计算预测值和目标值的平均绝对误差(MAE),即 \(|x-y|\),对异常值比 MSE 更不敏感。
主要参数:
size_average:已废弃。旧代码中控制平均或求和,请改用reduction;reduce:已废弃。旧代码中关闭聚合,请改用reduction='none';reduction:逐元素返回、平均或求和。
计算公式与说明:对第 \(n\) 个元素:
\[ \ell_n=|x_n-y_n| \]
绝对值使得误差增大时损失线性增长,因此相比 MSE,个别很大的误差不会被额外平方放大。
1 | |
代码逐行解释:
prediction = ... requires_grad=True:创建 3 个连续预测值,形状为[3]。在实际回归模型中它来自最后一层的输出,不需要 sigmoid 或 softmax。target = ...:给出每个预测位置对应的真实连续值,必须能与prediction一一对应;两者形状最好完全相同,避免无意的广播。nn.L1Loss(reduction='mean'):明确指定按平均值返回标量。若改为'none',会得到[0.5,0.5,0]这类逐元素损失,便于检查哪个样本误差最大。loss.backward():将平均绝对误差对每个预测值求导。由于绝对值在零点不可导,PyTorch 在该点采用子梯度规则;学习时一般不影响使用。print(...):打印平均 MAE,其单位与目标值相同,因而通常比 MSE 更直观。
内部计算过程:
示例数据:prediction = [2.5, 0.0, 2.0],target = [3.0, -0.5, 2.0],reduction='mean'。
计算步骤:
计算逐元素误差:
\[d=prediction-target=[-0.5,\ 0.5,\ 0]\]
取绝对值得到逐元素 L1 损失:
\[l=|d|=[0.5,\ 0.5,\ 0]\]
得到最终输出:
\[loss=\frac{0.5+0.5+0}{3}=\frac{1}{3}\approx0.3333\]
梯度含义:三个 prediction 的梯度方向分别为 \([-1/3,1/3,0]\)(零点处取子梯度)。梯度下降会增加 2.5、降低 0.0,而第三项无需调整。
MSELoss
1 | |
功能:计算均方误差(MSE),即 \((x-y)^2\)。大误差会被平方后放大,因此对离群点更敏感。
主要参数:
size_average:已废弃,请用reduction表示平均或求和;reduce:已废弃,请用reduction='none'保留各元素损失;reduction:逐元素返回、平均或求和。
计算公式与说明:对第 \(n\) 个元素:
\[ \ell_n=(x_n-y_n)^2 \]
平方会使大误差的贡献急剧变大;因此它会更积极地修正大误差,但也更容易受离群点影响。
1 | |
代码逐行解释:
prediction:三个回归预测中最后一个值为 8,特意设置成明显偏离目标,用于观察平方惩罚的效果;它是需要求梯度的叶子张量。target:对应真实值[3.0,-0.5,3.0],与预测值逐元素配对。nn.MSELoss():默认reduction='mean'。前向传播先计算每个位置的误差,再平方,最后将 3 个平方误差平均成一个标量。loss.backward():计算 \(L\) 关于每个 prediction 的梯度并写入prediction.grad。平方使远离目标的预测得到更强的梯度修正。print(...):输出的 8.5 是均方误差,不是原始单位下的平均偏差;因此它通常比 MAE 更难直接解释。
内部计算过程:
示例数据:prediction = [2.5, 0.0, 8.0],target = [3.0, -0.5, 3.0],reduction='mean'。
计算步骤:
计算误差:
\[d=[-0.5,\ 0.5,\ 5]\]
平方得到逐元素损失:
\[l=d^2=[0.25,\ 0.25,\ 25]\]
得到最终输出:
\[loss=\frac{0.25+0.25+25}{3}=8.5\]
梯度含义:
\[\frac{\partial loss}{\partial prediction_i}=\frac{2(prediction_i-target_i)}{3}\approx[-0.333,\ 0.333,\ 3.333]\]
第三项大误差的梯度远大于前两项,说明 MSE 会强烈惩罚离群误差。
SmoothL1Loss
1 | |
功能:小误差区域使用平滑的二次函数,大误差区域近似 L1 损失,兼顾 MSE 的平滑性与 L1 对异常值的鲁棒性。
主要参数:
size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:逐元素返回、平均或求和;beta:L1 与二次区域的分界阈值,默认1.0。
计算公式与说明:令 \(d=x-y\),则:
\[ \ell(d)= \begin{cases} \dfrac{0.5d^2}{\beta}, & |d|<\beta\\ |d|-0.5\beta, & |d|\geq\beta \end{cases} \]
误差较小时使用二次函数,梯度在零点附近更平滑;误差较大时使用线性增长,避免 MSE 对异常值的过度惩罚。
1 | |
代码逐行解释:
prediction与target:使用和 MSE 示例相同的数据,使三种回归损失的数值差异只来自损失函数本身。nn.SmoothL1Loss(beta=1.0):创建 Huber 风格的平滑 L1 损失。beta是从二次区域切换到线性区域的阈值,不是优化器的学习率。- 前向调用:函数逐元素判断 \(|prediction-target|\) 是否小于 1,然后选用对应的分段公式;默认对所有元素平均。
backward():小误差区域的梯度随误差线性变化且在 0 处平滑;大误差区域的梯度幅度被限制,类似 L1,因此不会像 MSE 一样被异常值主导。print(...):输出是三种分段损失的平均值,适合和 MSE/L1 对照。
内部计算过程:
示例数据:prediction = [2.5, 0.0, 8.0],target = [3.0, -0.5, 3.0],beta=1.0。
计算步骤:
计算误差:
\[d=[-0.5,\ 0.5,\ 5]\]
按 \(|d|\) 与
beta的关系选择分段公式:- 前两项 \(|d|=0.5<1\),\(l=0.5d^2=0.125\);
- 第三项 \(|d|=5\ge1\),\(l=|d|-0.5=4.5\)。
得到最终输出:
\[loss=\frac{0.125+0.125+4.5}{3}\approx1.5833\]
梯度含义:小误差的梯度与误差成正比;第三项进入 L1 区域,平均后的梯度幅度只有 \(1/3\),不会像 MSE 那样被大误差放大。
3.6.4
计数和概率分布:PoissonNLLLoss 与
KLDivLoss
这类损失分别用于非负计数建模,以及两个概率分布之间的匹配。
PoissonNLLLoss
1 | |
功能:计算目标服从泊松分布时的负对数似然损失,适合预测非负计数,例如单位时间内的事件数量。
主要参数:
log_input:默认True,表示输入是预测率的对数;此时模型可输出任意实数;full:是否加入 Stirling 近似项以得到更完整的泊松负对数似然;size_average:已废弃,请用reduction;eps:当log_input=False时用于避免计算log(0);reduce:已废弃,请用reduction;reduction:损失聚合方式。
计算公式与说明:设目标计数为 \(y\)。当 log_input=True
时,输入 \(x=\log\lambda\),其中 \(\lambda\) 是泊松分布的预测率:
\[ \ell(x,y)=\exp(x)-yx \]
当 log_input=False 时,输入直接是 \(\lambda\),公式为 \(\ell(\lambda,y)=\lambda-y\log(\lambda+\varepsilon)\)。full=True
还会加入与 \(\log(y!)\)
有关的近似项,使损失更接近完整负对数似然。
1 | |
代码逐行解释:
log_rate = ... requires_grad=True:三个值表示模型输出的 \(\log\lambda\),不是直接的计数,也不是概率。这样模型可以输出任意实数,内部指数运算会保证预测率 \(\lambda\) 始终为正。counts = ...:真实观察计数为[1,3,2]。虽然逻辑上是整数计数,PyTorch 损失计算时使用浮点张量以参与连续的自动求导。nn.PoissonNLLLoss(log_input=True, full=True):log_input=True告诉函数调用 \(\exp(log\_rate)\) 恢复率;full=True加入关于阶乘项的近似,使不同计数的损失比较更接近完整的泊松负对数似然。默认仍按平均返回。loss.backward():梯度会作用于log_rate。若当前预测率低于实际计数,梯度下降会倾向增大 log-rate;反之则降低它。print(...):输出为三个计数样本的平均负对数似然,而非“预测计数的平均误差”。
内部计算过程:
示例数据:log_rate = [0.0, 0.7, 1.2],counts = [1,3,2];log_input=True、full=True。
计算步骤:
恢复每个样本的泊松预测率:
\[\lambda=e^{log\_rate}\approx[1.0000,\ 2.0138,\ 3.3201]\]
计算核心负对数似然项:
\[l_i^{core}=\lambda_i-y_i\log\lambda_i=\lambda_i-y_i\cdot log\_rate_i\]
得到 \([1.0000,\ -0.0862,\ 0.9201]\)。由于
full=True,对计数大于 1 的后两项还会加入 \(\log(y_i!)\) 的 Stirling 近似项。得到最终输出:对完整的三个逐元素 NLL 按默认
mean求平均。
梯度含义:核心梯度为 \(\lambda_i-y_i\)。第二项约为 \(-0.9862\),梯度下降会增大其
log_rate,从而把预测率从 2.01 推向真实计数 3。
KLDivLoss
1 | |
功能:计算两个概率分布之间的 KL 散度。默认时第一个输入是对数概率,第二个输入是普通概率,常用于知识蒸馏和分布匹配。
主要参数:
size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:可取'none'、'sum'、'mean'或'batchmean';常用'batchmean',与 KL 散度的标准批量定义一致;log_target:为False(默认)时target是普通概率;为True时target也必须是对数概率。
计算公式与说明:令目标分布为 \(P\),模型分布为 \(Q\),则:
\[ D_{\mathrm{KL}}(P\|Q)=\sum_i P(i)\log\frac{P(i)}{Q(i)} \]
默认 log_target=False 时,input 传入 \(\log Q(i)\),target 传入 \(P(i)\)。KL 散度不对称,衡量的是“用 \(Q\) 近似 \(P\)”时的差异;batchmean
会先对每个样本求和,再除以批量大小。
1 | |
代码逐行解释:
student_logits:形状为[2,3],两行是两个样本,三列是类别得分。它模拟待训练模型的输出,因此设置requires_grad=True。target_probs:也是[2,3],每行都是合法概率分布,必须非负且和为 1;它可以来自教师模型、软标签或人工设定的目标分布。F.log_softmax(student_logits, dim=1):沿类别维计算对数 softmax,直接得到 \(\log Q\)。不能传普通 logits 给默认的KLDivLoss,也不要传softmax后的普通概率作为 input。nn.KLDivLoss(reduction='batchmean'):默认log_target=False,因此第二个参数被视为普通概率 \(P\)。batchmean是把所有类别损失求和后只除以批量大小 2,而不是再除以类别数。backward():让每行学生分布 \(Q\) 接近目标 \(P\);目标分布默认不要求梯度,只有 student logits 会收到梯度。
内部计算过程:
示例数据:第一行学生 logits 为
[1.0,0.2,-0.5],目标分布为
[0.7,0.2,0.1];reduction='batchmean'。
计算步骤:
将 logits 归一化为学生分布:
\[Q=softmax([1.0,0.2,-0.5])\approx[0.5979,\ 0.2687,\ 0.1334]\]
计算每个类别的 KL 项:
\[t_i=P_i(\log P_i-\log Q_i)=P_i\log\frac{P_i}{Q_i}\]
分别代入 \(P=[0.7,0.2,0.1]\) 后再求和,得到第一样本的 \(KL_1\)。
得到最终输出:第二行同样得到 \(KL_2\),随后:
\[loss=\frac{KL_1+KL_2}{2}\]
梯度含义:目标概率高于学生概率的类别会被提高 logits,反之被压低;KL 是有方向的 \(D_{KL}(P\|Q)\),交换两者不会得到同一数值。
3.6.5 排序、边界与度量学习损失
这类损失约束样本之间的排序关系、类别间隔、距离或嵌入向量的相似度,而非直接进行常规单标签分类。
MarginRankingLoss
1 | |
功能:比较两个输入得分的相对大小。目标为
1 时希望 input1 > input2,目标为
-1 时希望
input1 < input2,常用于排序任务。
主要参数:
margin:两个得分之间要求满足的最小间隔;size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:损失聚合方式。
计算公式与说明:设 \(y\in\{-1,1\}\),则单个样本的损失为:
\[ \ell(x_1,x_2,y)=\max(0,-y(x_1-x_2)+\text{margin}) \]
当 \(y=1\) 时,只有在 \(x_1\) 没有比 \(x_2\) 至少大 margin
时才产生损失;当 \(y=-1\)
时,约束方向相反。
1 | |
代码逐行解释:
input1与input2:各有两个元素,索引相同的位置构成一对待排序得分;两者均开启梯度,以便观察违反约束时两个分数如何被同时调整。targets = [1.0,-1.0]:这是排序方向标签而不是类别编号。第一个 1 要求 \(input1[0]>input2[0]\);第二个 -1 要求 $input1[1]<input2[1]`。MarginRankingLoss(margin=0.5):要求满足方向还不够,两个得分的间隔至少为 0.5。默认将两个样本损失取平均。- 前向与
backward():函数逐位置计算 hinge 损失。只有某对未达到指定间隔时才产生梯度;已经正确且间隔足够的对不再被继续拉开。
内部计算过程:
示例数据:input1=[2.0,0.2],input2=[1.0,0.8],targets=[1,-1],margin=0.5。
计算步骤:
第一对(目标为 1):要求 \(input1-input2\ge0.5\)。
\[l_1=\max(0,-1(2.0-1.0)+0.5)=0\]
第二对(目标为 -1):要求 \(input2-input1\ge0.5\)。
\[l_2=\max(0,-(-1)(0.2-0.8)+0.5)=0\]
得到最终输出:
\[loss=(l_1+l_2)/2=0\]
梯度含义:两对都已满足间隔,因此梯度均为
0。若第一对的 input1 低于 1.5,就会产生正损失,并推动
input1 上升、input2 下降。
MultiLabelMarginLoss
1 | |
功能:计算多标签分类的边界损失。目标中列出一个样本所属的多个类别索引,其余位置用
-1 填充。对于现代多标签分类通常更推荐
BCEWithLogitsLoss。
主要参数:
size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:损失聚合方式。
计算公式与说明:对一个样本,设正标签集合为 \(Y\),所有类别得分为 \(x_i\),该损失对每个正标签 \(j\in Y\) 与每个非正标签 \(i\notin Y\) 计算:
\[ \ell=\frac{1}{C}\sum_{j\in Y}\sum_{i\notin Y}\max(0,1-x_j+x_i) \]
它要求每个正标签得分至少比非正标签高 1。目标张量用 -1
表示“正标签列表到此结束”。
1 | |
代码逐行解释:
scores:形状为[1,4],第一个维度表示一个样本,4 列是该样本对 4 个类别的分数。它不是 softmax 概率,各类别分数可以独立增减。targets:与 scores 同形状且类型为long。前两个位置列出正标签类别 1、3;第一个-1表示标签列表结束,后面的值不再参与。MultiLabelMarginLoss():对“每个正类 × 每个负类”计算间隔项,要求每个正类得分都至少高出负类 1;它默认对批量求平均。backward():违反约束的正类会获得提升分数的梯度,相关负类会获得降低分数的梯度;已满足间隔的配对梯度为 0。
内部计算过程:
示例数据:scores=[0.2,0.8,0.1,0.9];targets=[1,3,-1,-1],所以正类为
1、3,非正类为 0、2。
计算步骤:
取出正、负类别得分:正类得分为 \(0.8,0.9\),负类得分为 \(0.2,0.1\)。
计算所有正负配对的间隔项:
\[\max(0,1-x_j+x_i)\]
得到 \(0.4,0.3,0.3,0.2\),分别对应正类 1/3 与负类 0/2 的 4 个组合。
得到最终输出:函数将这些正损失项按类别数进行内部归一化,再按默认方式聚合为单个样本损失。
梯度含义:每个正项都会提升对应正类得分并降低对应负类得分,直到所有正类至少比负类高 1。
SoftMarginLoss
1 | |
功能:计算以 -1 和 1
为标签的逐元素二分类 logistic 损失。若数据集标签为
0/1,一般应使用 BCEWithLogitsLoss。
主要参数:
size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:损失聚合方式。
计算公式与说明:标签 \(y\) 必须为 \(-1\) 或 \(1\),单个元素损失为:
\[ \ell(x,y)=\log(1+\exp(-yx)) \]
当 \(y=1\) 时,增大 \(x\) 能减小损失;当 \(y=-1\) 时,减小 \(x\) 能减小损失。这与
BCEWithLogitsLoss 的标签编码不同。
1 | |
代码逐行解释:
scores:形状[3]的任意实数得分,分别代表三次独立二元判别的 raw score;不需要预先做 sigmoid。targets:必须和 scores 同形状,但标签编码是 \(+1/-1\),这与 BCE 使用 \(1/0\) 的编码不同。nn.SoftMarginLoss():内部把每一项写为 \(\log(1+e^{-yx})\) 并使用默认平均聚合;当 \(yx\) 越大,表示预测方向越正确且越自信,损失越小。backward():对正标签推动分数向正无穷方向移动,对负标签推动分数向负无穷方向移动;梯度会随间隔变大而逐渐减小。
内部计算过程:
示例数据:scores=[0.8,-0.4,1.2],targets=[1,-1,1],默认
reduction='mean'。
计算步骤:
先计算分类方向乘积:
\[y\cdot x=[0.8,\ 0.4,\ 1.2]\]
第二项虽然 score 为负,但目标也是 -1,因此方向正确。
逐元素计算 logistic 损失:
\[l_i=\log(1+e^{-y_ix_i})\approx[0.3711,\ 0.5130,\ 0.2633]\]
得到最终输出:
\[loss=(0.3711+0.5130+0.2633)/3\approx0.3825\]
梯度含义:\(y_ix_i\) 越大,损失越接近 0;梯度下降会使正样本 score 变大、负样本 score 变小。
MultiMarginLoss
1 | |
功能:计算单标签多分类的 hinge
损失。实际多分类任务中,CrossEntropyLoss 往往更常用。
主要参数:
p:间隔惩罚的指数,只能取1或2;margin:正确类别得分相对其他类别要求满足的最小间隔;weight:按类别设置的权重;size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:损失聚合方式。
计算公式与说明:设正确类别为 \(y\),类别数为 \(C\),则:
\[ \ell(x,y)=\frac{1}{C}\sum_{i\ne y}\max(0,\text{margin}-x_y+x_i)^p \]
若正确类别得分 \(x_y\)
已比每个其他类别至少高 margin,对应项为 0;p
决定对违反间隔的惩罚是线性还是平方。
1 | |
代码逐行解释:
scores:形状[2,3],第一维是两个样本,第二维是三个类别的得分。每行可看作一个多分类模型的输出,但此处训练目标采用 hinge 间隔而非 softmax 交叉熵。targets:[1,0]是两个正确类别索引,类型为long。第一行要让第 1 类得分最高,第二行要让第 0 类得分最高。MultiMarginLoss(p=1, margin=1.0):对正确类与每个错误类构造 \(\max(0,1-x_y+x_i)\)。p=1是线性 hinge;若设为 2,违反间隔的项会被平方。- 前向调用与
backward():所有违反间隔的错误类都会推高损失。反向传播提升正确类得分,并降低发生违反的错误类得分;已经满足间隔的类别不产生梯度。
内部计算过程:
示例数据:两行 scores 为
[[0.2,1.0,0.1],[0.9,0.3,0.5]],正确类别
targets=[1,0],margin=1、p=1。
计算步骤:
第一样本,正确类为 1:
\[\max(0,1-1.0+0.2)=0.2,\qquad\max(0,1-1.0+0.1)=0.1\]
第二样本,正确类为 0:
\[\max(0,1-0.9+0.3)=0.4,\qquad\max(0,1-0.9+0.5)=0.6\]
得到最终输出:函数将每行错误类别对应的项按类别数归一化,再对两个样本按默认
mean聚合。
梯度含义:4 个项都违反间隔,因此各正确类 logits 会被提高,4 个对应错误类 logits 会被压低。
TripletMarginLoss
1 | |
功能:计算三元组损失。它让锚点 anchor
与同类样本 positive 的距离,小于锚点与异类样本
negative 的距离,并至少相差一个 margin。
主要参数:
margin:正负样本距离之间要求的最小间隔;p:距离使用的 \(p\) 范数,默认2.0即欧氏距离;eps:数值稳定项;swap:为True时,额外比较正样本与负样本的距离,取更难的负例距离;size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:损失聚合方式。
计算公式与说明:令 \(a\)、\(p\)、\(n\) 分别表示 anchor、positive、negative,距离函数为 \(d(\cdot,\cdot)\),则:
\[ \ell(a,p,n)=\max(0,d(a,p)-d(a,n)+\text{margin}) \]
只有当锚点离正样本不够近,或离负样本不够远时,损失才大于 0。默认使用 \(p=2\) 的欧氏距离。
1 | |
代码逐行解释:
anchor:形状[2,2],两个锚点样本,每个样本是 2 维嵌入向量;设置requires_grad=True,代表其通常由待训练模型产生。positive和negative:形状必须与 anchor 一致。相同行组成一个三元组:positive 应与 anchor 同类,negative 应与 anchor 异类。本例将 positive 设在 anchor 附近、negative 设得很远。TripletMarginLoss(margin=0.5,p=2):使用 \(L_2\) 欧氏距离。它并不是直接最小化正样本距离,而是要求“负距离 - 正距离”至少为 0.5。- 前向调用:逐行分别计算 \(d(a,p)\)、\(d(a,n)\) 和 hinge 项,默认把两个三元组的损失平均。
backward():只有损失为正的困难三元组会产生梯度。此例已经满足约束,loss 可能为 0,梯度也为 0;这不是出错,而是 margin 已被满足。
内部计算过程:
示例数据:第一组三元组为 \(a=[0,0]\)、\(p=[0.1,0]\)、\(n=[2,2]\),margin=0.5、\(p=2\)(欧氏距离)。
计算步骤:
计算 anchor 与正样本距离:
\[d(a,p)=\sqrt{(0-0.1)^2+(0-0)^2}=0.1\]
计算 anchor 与负样本距离:
\[d(a,n)=\sqrt{(0-2)^2+(0-2)^2}=\sqrt8\approx2.828\]
计算三元组损失:
\[l_1=\max(0,0.1-2.828+0.5)=0\]
第二组三元组也满足相同间隔,因此 \(l_2=0\);最终平均
loss=0。
梯度含义:本例没有困难三元组,梯度为 0。若负样本改为
[0.2,0],则损失为 \(0.4\),反向传播会拉近 anchor/positive
并推远 negative。
HingeEmbeddingLoss
1 | |
功能:对距离或相异度进行约束。目标为 1
时希望输入距离小;目标为 -1 时希望输入距离至少达到
margin。
主要参数:
margin:负样本(目标为-1)要求达到的最小距离;size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:损失聚合方式。
计算公式与说明:设输入 \(x\) 是距离或相异度,标签 \(y\in\{-1,1\}\),则:
\[ \ell(x,y)= \begin{cases} x, & y=1\\ \max(0,\text{margin}-x), & y=-1 \end{cases} \]
相似样本对(\(y=1\))的距离越小越好;不相似样本对(\(y=-1\))只要距离达到
margin,便不再产生损失。
1 | |
代码逐行解释:
distances:输入不是两组向量,而是已经计算好的三对样本距离或相异度,形状为[3]。这里开启梯度只是为了观察损失怎样对距离施加优化信号。targets=[1,-1,-1]:第一个是相似对,目标 1 要求距离尽可能小;后两个是不相似对,目标 -1 要求距离至少达到 margin。HingeEmbeddingLoss(margin=1.0):对正对直接使用距离作为损失;对负对只惩罚那些距离小于 1 的样本。默认按 3 个元素平均。backward():相似对总会有把距离变小的驱动力;负对仅在距离不足 margin 时产生把距离增大的驱动力,达到 margin 后梯度为 0。
内部计算过程:
示例数据:distances=[0.2,1.5,0.8],targets=[1,-1,-1],margin=1。
计算步骤:
相似对(标签 1):直接使用距离作为损失:
\[l_1=0.2\]
第一个不相似对(标签 -1):
\[l_2=\max(0,1-1.5)=0\]
第二个不相似对(标签 -1):
\[l_3=\max(0,1-0.8)=0.2\]
得到最终输出:
\[loss=(0.2+0+0.2)/3\approx0.1333\]
梯度含义:第一个距离被推动变小,第三个距离被推动变大,第二个距离已超过 margin 因而没有梯度。
CosineEmbeddingLoss
1 | |
功能:根据余弦相似度约束两组向量。目标为
1 时鼓励向量方向相同;目标为 -1
时要求余弦相似度不超过 margin。
主要参数:
margin:负样本允许的最大余弦相似度,通常取[0, 0.5];size_average:已废弃,请用reduction;reduce:已废弃,请用reduction;reduction:损失聚合方式。
计算公式与说明:设余弦相似度为:
\[ \cos(x_1,x_2)=\frac{x_1\cdot x_2}{\|x_1\|_2\|x_2\|_2} \]
标签 \(y=1\) 时,损失为 \(1-\cos(x_1,x_2)\);标签 \(y=-1\) 时,损失为 \(\max(0,\cos(x_1,x_2)-\text{margin})\)。因此它主要约束向量方向,而不是向量长度。
1 | |
代码逐行解释:
vector1:形状[2,2],两行分别是两对样本中的第一个嵌入向量;它开启梯度追踪,代表模型输出的待学习表示。vector2:与 vector1 同形状,是每对的比较对象。这里未开启梯度,仅作为固定参照;真实的双塔网络中,两个分支都可以要求梯度。targets=[1,-1]:第一对标签 1 表示相似,目标是余弦相似度接近 1;第二对标签 -1 表示不相似,目标是相似度不高于 margin。CosineEmbeddingLoss(margin=0.0):逐行计算点积除以两向量 L2 范数的乘积。margin=0表示负对的余弦相似度只要不大于 0 就不再受惩罚。backward():将梯度写入vector1.grad,主要调整向量方向。余弦相似度已对长度归一化,所以它不像 L2 距离那样直接鼓励缩短向量长度。
内部计算过程:
示例数据:第一对为 \([1,0]\) 与 \([0.9,0.1]\)、标签 1;第二对为 \([1,0]\) 与 \([-1,0]\)、标签
-1;margin=0。
计算步骤:
第一对计算余弦相似度:
\[cos=\frac{0.9}{1\times\sqrt{0.9^2+0.1^2}}\approx0.9939\]
正对损失为 \(l_1=1-0.9939\approx0.0061\)。
第二对计算余弦相似度:
\[cos=-1,\qquad l_2=\max(0,-1-0)=0\]
得到最终输出:
\[loss=(0.0061+0)/2\approx0.00305\]
梯度含义:第一对仍会被轻微推动到完全同向;第二对已满足“相似度不大于 0”的负对约束,梯度为 0。
3.6.6 序列对齐:CTCLoss
CTC 用于输入序列与目标序列长度不同、且逐时间步对齐关系未知的任务,例如语音识别和手写文字识别。
CTCLoss
1 | |
功能:计算连接时序分类(CTC)损失,适合输入序列和目标序列长度不同、且不知道逐时间步对齐方式的任务,例如语音识别与手写文字识别。
主要参数:
blank:blank 类别的索引,默认0;目标序列中不能出现该索引;reduction:'none'、'mean'或'sum';zero_infinity:为True时,将无法对齐导致的无穷损失及梯度置零,避免无穷值传播。
计算公式与说明:设模型在 \(T\) 个时间步上输出对数概率,目标标签序列为 \(y\)。CTC 会枚举所有“压缩后等于 \(y\)”的合法路径 \(\pi\):先合并连续重复标签,再删除 blank 标签。损失是这些路径总概率的负对数:
\[ \ell_{\mathrm{CTC}}=-\log\sum_{\pi\in\mathcal{B}^{-1}(y)}\prod_{t=1}^{T}P(\pi_t\mid t) \]
其中 \(\mathcal{B}\) 表示“合并重复并删除 blank”的映射。CTC 不要求事先知道每个目标字符对应哪个时间步,但要求输入序列有足够长度容纳目标及必要的 blank/重复标签。
1 | |
代码逐行解释:
T, N, C = 6, 2, 5:定义输入时间步数、批量大小和类别数。类别索引 0 被保留为 blank,真正可输出的目标标签是 1 到 4;因此 targets 中不能出现 0。logits = torch.randn(T,N,C,requires_grad=True):模拟序列模型(如 RNN/Transformer)在每个时间步为每个样本输出 5 类原始得分,形状严格为[时间步, 批量, 类别]。注意它与很多batch_first=True的模型输出不同,需要留意维度顺序。log_probs = F.log_softmax(logits, dim=2):只在类别维C上归一化,使每个(t,n)位置的类别概率和为 1,再取对数。CTCLoss 接收对数概率,用它可以将一条路径上的概率连乘转为对数域的稳定求和。targets = [1,2,3,1,2]:采用“拼接的一维目标”表示两个不等长标签序列。它不含 blank:前 2 个属于第一个样本[1,2],后 3 个属于第二个样本[3,1,2]。input_lengths与target_lengths:前者指出两个输入都有效到第 6 个时间步;后者是[2,3],让 CTCLoss 能从拼接 targets 中正确切分目标。真实的变长批次中,input_lengths 常常小于最大的 T。nn.CTCLoss(blank=0, zero_infinity=True):指定 blank 类,默认reduction='mean'。zero_infinity=True在输入长度无法容纳目标序列等异常对齐时把无穷损失/梯度置零,但不替代对数据长度的检查。- 调用、
backward()和打印:函数用动态规划累加所有合法路径的概率,得到单个标量损失。反向传播为[T,N,C]的每个 logits 给出梯度,训练模型在正确时间步上增加目标字符或 blank 的概率。
内部计算过程:
示例数据:T=6、N=2、C=5;blank
类为 0。第一个目标为 [1,2],第二个目标为
[3,1,2]。logits
是随机生成的,因此不假设其具体类别概率。
计算步骤:
每个时间步归一化:对每个
(t,n)的 5 个 logits 执行log_softmax,得到 \(\log P(c\mid t,n)\);每个时间步的普通概率在类别维相加为 1。枚举第一个目标的合法路径:例如路径
[0,1,1,0,2,0]先合并重复项得到[0,1,0,2,0],再删除 blank 得到[1,2],因此合法;所有压缩结果不为[1,2]的路径不计入。聚合路径概率:每条合法路径的概率是各时间步对应类别概率的连乘,例如:
\[P(0\mid t_1)P(1\mid t_2)P(1\mid t_3)P(0\mid t_4)P(2\mid t_5)P(0\mid t_6)\]
将所有合法路径相加后取负对数得到第一个样本损失;第二个目标
[3,1,2]同样处理。得到最终输出:默认
reduction='mean'会先按每个目标序列长度归一化两个样本损失,再取批量平均。
梯度含义:梯度并不强制某个固定时间步输出某个标签,而是提高所有能组成正确目标的路径总概率;这正是 CTC 能处理未知对齐的原因。
3.6.7 小结:常见选择与易错点
| 任务 | 推荐损失函数 | 模型输出 | 目标格式 |
|---|---|---|---|
| 单标签二分类 | BCEWithLogitsLoss |
[N] 或 [N, 1] logits |
同形状的 float,值为 0/1 |
| 单标签多分类 | CrossEntropyLoss |
[N, C] logits |
[N] 的 long 类别索引 |
| 多标签分类 | BCEWithLogitsLoss |
[N, C] logits |
[N, C] 的 float 0/1 矩阵 |
| 连续值回归 | MSELoss / L1Loss /
SmoothL1Loss |
连续预测值 | 与输出形状一致的 float |
| 序列无对齐标注 | CTCLoss |
[T, N, C] 对数概率 |
标签序列与各自长度 |
最常见的错误是“对 logits 重复做激活”:CrossEntropyLoss
前不要手动 softmax,BCEWithLogitsLoss
前不要手动 sigmoid。只有使用 BCELoss
时才应传入 sigmoid
后的概率。调用损失函数前,也应先检查输出形状、目标形状、目标数据类型和标签取值范围是否匹配。
3.7 训练与评估
本节学习的内容饮包含:PyTorch中模型训练/评估模型的开启、完整的训练/评估流程。
3.7.1 泛化能力强与不拟拿区别与联系
为了更清晰、直观地厘清两者之间的界限,以下从概念定义、评判标准、逻辑关系以及典型表现等维度进行集中对比:
泛化能力强 vs 不过拟合
| 比较维度 | 泛化能力强(Good Generalization) | 不过拟合(No Overfitting) |
|---|---|---|
| 本质定义 | 模型在未见过的全新数据(真实场景/测试集)上取得优异预测效果的能力。 | 模型没有过度依赖训练集的特定噪声或局部特例,保持了适度泛化空间的状态。 |
| 核心视角 | 积极/结果导向(追求上限:模型在未知数据上到底有多强)。 | 防御/状态导向(排除病态:模型是否出现了死记硬背的倾向)。 |
| 评估标准 | 依赖测试集/验证集的绝对指标(如测试集准确率 \(95\%\)、测试集 Loss 极低)。 | 依赖训练集与测试集之间的相对差距(即 \(\text{Loss}_{\text{test}} - \text{Loss}_{\text{train}}\) 保持较小或合理)。 |
| 出现欠拟合时 | ❌ 表现极差(对未知数据预测不准)。 | ✅ 依然成立(训练集和测试集表现都很差,但两者几乎无 Gap)。 |
| 出现轻微 Gap 时 | ✅ 依然成立(若训练集 \(99.9\%\)、测试集 \(95\%\),尽管有 \(4.9\%\) 的 Gap,泛化依然极强)。 | ⚠️ 存在微弱过拟合(从严格统计学定义看,存在表现落差)。 |
| 核心优化目标 | 提升模型学习通用特征规律的能力(如通过更好的架构、优质数据、预训练等)。 | 约束模型容量,防止对训练集的过分拟合(如使用 Dropout、L2 正则化、早停等)。 |
两者之间的核心联系
1 | |
- “不过拟合”是“泛化能力强”的必要非充分条件:
- 必要性:如果一个模型严重过拟合,它在测试集上的表现必定崩溃,绝不可能拥有强泛化能力。
- 非充分性:如果模型处于欠拟合状态,它虽然完全没有过拟合,但泛化能力依然极差。
- “泛化能力强”是“不过拟合”在有效拟合前提下的自然结果:
- 当模型在训练集上已经拟合得很好(排除了欠拟合),此时只要成功控制住过拟合,模型就会顺理成章地获得强泛化能力。
3.7.2 训练与评估的大致过程
在完成模型的训练后,需要在验证集/测试集分别完成模型的验证与测试,以确保最终得到的模型是具有泛化能力的/不会出现严重过拟合现象的。
3.7.2.1 训练的流程
PyTorch中,模型训练与评估/测试的最显著区别在于:模型训练需要将模型设置为训练状态,模型的参数要支持反向传播与优化器更新; 而模型验证/评估时,需要将模型设置为验证/测试状态,不要进行损失的反向传播与/更新模型参数。
一个完整的图像分类的训练过程如下所示:
1 | |
3.7.2.2 验证/评估的流程
3.7.2.2.1 PyTorch 原生评估 API
验证/测试的流程基本与训练过程一致,不同点在于:
- 需要预先设置torch.no_grad,以及将model调至eval模式
- 不需要将优化器的梯度置零
- 不需要将loss反向回传到网络
- 不需要更新optimizer
一个完整图像分类的验证过程如下所示:
1 | |
3.7.2.2.2 sklearn.metrics
对于图像分类任务,还可以使用sklearn.metrics中的classification_report函数来计算模型的准确率、召回率、F1值等指标,如下所示:
1 | |
3.7.2.2.3
torcheval或torchmetric
此外,还可以使用torcheval或torchmetric来对模型进行评估。
(1)两大 PyTorch 评估库核心区别
| 库名 | 核心特点 | 适用场景 |
|---|---|---|
| TorchMetrics | 社区最主流(PyTorch Lightning 官方维护),支持 GPU 累加计算、分布式通信与可视化,指标最全面。 | 绝大多数 PyTorch 项目首选 |
| TorchEval | Meta 官方维护,极简轻量,语法契合 PyTorch 原生生态。 | 追求轻量化或 Meta 生态集成项目 |
(2)核心示例代码
两者均采用 update()(逐 Batch 收集)->
compute()(跨 Batch 整体计算)
的流式评估模式,无需像 sklearn
那样把所有数据 .cpu().numpy()
拼接成大数组,更加节省显存。
① 使用 TorchMetrics(最常用)
1 | |
② 使用 TorchEval(Meta 官方)
1 | |
3.8 可视化
在 PyTorch 中,可视化贯穿于模型开发的全生命周期(数据准备、训练监控、效果评估与可解释性分析)。它既包含训练过程中对损失和指标变化的实时监控,也包含对不同阶段、不同模型(如蒸馏/微调前后)效果的对比分析,以及对模型内部特征与决策依据的可解释性展示。
3.9 PyTorch优化器
本小节学习的内容饮包含:了解PyTorch的优化器、学会使用PyTorch提供的优化器进行优化、优化器的属性和构造、优化器的对比。
深度学习的目标是通过多轮的迭代不断改变模型的网络参数,使得模型能够对输入做各种非线性变换、使得输出尽可能的拟合标签。这个过程本质上就是一个复杂函数求最优解的过程,在深度学习模型中这个最优解是一个大矩阵,如何快速求得这个最优解是深度学习中的一个研究重点与难点。比如以经典的resnet-50模型为例,其中大约有2000万个系数需要进行计算,那么如何计算出这么多系数,有以下两种方法:
- 第一种是直接暴力穷举一遍参数,这种方法从理论上行得通,但是实施上可能性基本为0,因为参数量过于庞大。
- 第二种方法:为了使求解参数过程更快,人们提出了BP+优化器逼近求解。
在这个第二种方法中,优化器就是根据网络反向传播的梯度信息来不断更新模型的网络参数,降低损失函数计算值,最终使得模型输出更加接近真实标签。
3.9.1 PyTorch提供的优化器
PyTorch中有一个优化器包torch.optim,其下预置了多个优化器实现:
1 | |
上述这些优化器都继承于torch.optim.Optimizer,以下是torch.optim.Optimizer的__init__方法中几个关键属性,PyTorch2.6.0中针对此类的此方法中还有一些其他属性,但不是此次解释的重点。
1 | |
3.9.1.1 Optimizer类中三个核心属性详解
1. self.defaults —
超参数默认值字典
存储优化器的全局默认超参数,当某个参数组没有单独指定某个超参数时,就使用这里的默认值。
1 | |
输出: 1
{'lr': 0.01, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0.0001, 'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False, 'fused': None}
optimizer.defaults完整解释如下:
| 参数 | 默认值 | 含义 |
|---|---|---|
lr |
0.01 |
学习率(Learning Rate)。每次更新时参数沿梯度方向移动的步长大小。 |
momentum |
0.9 |
动量系数。引入历史梯度的指数移动平均,加速收敛并减少震荡。公式:v = momentum * v + grad |
dampening |
0 |
动量阻尼。用于抑制动量项,防止动量过大。公式:v = momentum * v + (1 - dampening) * grad。设为
0 表示不抑制。 |
weight_decay |
0.0001 |
权重衰减(L2 正则化)。在梯度上加上
weight_decay * param,等效于损失函数加 L2
正则项,防止过拟合。 |
nesterov |
False |
Nesterov 动量。若为 True,使用
Nesterov 加速梯度(NAG),比普通动量"看得更远",收敛更平滑。要求
momentum > 0 且 dampening == 0。 |
maximize |
False |
是否最大化目标。若为
True,则沿梯度上升方向更新(用于最大化问题),而非默认的梯度下降。 |
foreach |
None |
是否使用 foreach 内核。None
表示自动决定;True 强制使用批量 foreach
内核(对多参数并行处理更快);False 强制不使用。 |
differentiable |
False |
是否可微分。若为
True,step()
操作本身保留计算图,允许对优化器步骤求导(用于元学习等场景)。 |
fused |
None |
是否使用 fused 内核。None
表示自动决定;True 强制使用融合 CUDA
内核(将所有参数的更新合并为一次 kernel
launch,减少显存带宽压力,速度更快)。要求 GPU 且不支持
differentiable=True。 |
2. self.state —
每个参数的优化器状态
这是一个
defaultdict(dict),键是参数张量本身(tensor
对象),值是一个字典,存储该参数对应的优化过程中累积的状态(如动量缓冲、步数计数等)。
以 SGD + momentum 为例,每次 step()
后会为每个参数记录动量缓冲:
1 | |
以 Adam 为例,状态更丰富(包含一阶矩、二阶矩、步数):
1 | |
3.
self.param_groups — 参数分组列表
存储参数组列表,每个参数组是一个字典,包含该组的参数张量和对应的超参数。这样不同参数组可以使用不同的学习率等超参数。
1 | |
输出: 1
2
3
4
5
6
7
8
9
10
11
12参数组数量: 2
--- 参数组 0 ---
params: 2 个参数张量 ← model[0] 的 weight 和 bias
lr: 0.01
momentum: 0.9
--- 参数组 1 ---
params: 2 个参数张量 ← model[1] 的 weight 和 bias
lr: 0.001
momentum: 0.9 ← 来自 defaults
三者的关系总结
1 | |
简单来说: -
defaults:优化器的"全局配置" -
param_groups:把参数分组,每组可以有"个性化配置"
-
state:记录每个参数在训练过程中累积的"历史记忆"(动量、步数等)
3.9.1.2
torch.optim.Optimizer 重要方法一览
核心方法
| 方法 | 作用 |
|---|---|
__init__(params, defaults) |
初始化优化器,设置默认超参数、参数分组和状态容器 |
step(closure=None) |
执行一次参数更新。各子类(SGD、Adam 等)各自实现具体更新逻辑 |
zero_grad(set_to_none=True) |
清零所有参数的梯度。set_to_none=True
时将梯度设为 None(省内存),False 时设为
0 |
state_dict() |
返回优化器当前状态的字典,包含
state(各参数的累积状态)和
param_groups(参数分组及超参数) |
load_state_dict(state_dict) |
从字典中加载优化器状态,恢复训练时的优化器状态 |
add_param_group(param_group) |
动态添加一个新的参数组。常用于微调时把冻结层加入训练 |
钩子注册方法
| 方法 | 作用 |
|---|---|
register_step_pre_hook(hook) |
注册一个在 step()
执行前调用的钩子 |
register_step_post_hook(hook) |
注册一个在 step()
执行后调用的钩子 |
register_state_dict_pre_hook(hook) |
注册一个在 state_dict()
执行前调用的钩子 |
register_state_dict_post_hook(hook) |
注册一个在 state_dict()
执行后调用的钩子 |
register_load_state_dict_pre_hook(hook) |
注册一个在 load_state_dict()
执行前调用的钩子 |
register_load_state_dict_post_hook(hook) |
注册一个在 load_state_dict()
执行后调用的钩子 |
内部辅助方法
| 方法 | 作用 |
|---|---|
_patch_step_function() |
将 step() 包装上 profiler 和钩子调用逻辑 |
_cuda_graph_capture_health_check() |
检查 CUDA 图捕获是否安全可用 |
_group_tensors_by_device_and_dtype() |
按设备和数据类型对张量列表分组,用于批量内核优化 |
典型使用流程
1 | |
3.9.2 实际操作
1 | |
3.9.3 输出结果
zero_grad() 把梯度设为 None 后,后续 50 次
step() 中 weight 的梯度为 None,SGD
会跳过该参数(不更新动量缓冲和权重)。具体解释如下:
1 | |
梯度为None后不再更新权重
50 次 step() 前后 state_dict
完全相同,不是 bug,原因是:
1 | |
SGD 的 _init_group
方法中(第 86 行)有判断 if p.grad is not None:,梯度为
None 时该参数不会被加入更新列表,动量缓冲也不会被修改。
梯度更新前一定要置零
optimizer在一个神经网络的epoch中需要实现下两个步骤:梯度置零、梯度更新。
1 | |
为网络中不同的层单独定制优化器参数
1 | |
3.9.4 实验
1 | |
附图2展示了多种常用优化器在同一拟合任务上的收敛速度对比效果。
所有 9 种优化器在 200 个 Step 内都展现出了清晰的收敛特征。结合图中的实际数据曲线,详细解读如下:
1. 第一梯队:自适应学习率优化器(Adam, AdamW, Adamax, Adagrad)
- 曲线表现:曲线在最左侧(前 0~25 个 Steps)几乎呈垂直断崖式下跌,迅速将 Loss 从高位压制到 0.025 以下,并在 25 个 Steps 左右就基本达到了全局最佳收敛点(Loss 近似 0.01 左右)。
- 细节特征:在下跌初期有轻微的齿状震荡(例如粉色的 AdamW 和灰色的 Adamax),这是自适应一阶/二阶矩估计在快速调整梯度步伐时的正常波动,但收敛速度在所有优化器中是最快的。
2. 震荡收敛型:动量优化器(Momentum)
- 曲线表现:绿色曲线在前 50 个 Steps 表现出非常明显的锯齿状强烈震荡,随后震荡幅值逐渐减小,在 75 个 Steps 后趋于平缓并稳定在低 Loss 区域。
- 现象成因:由于加入了动量(
momentum=0.9),参数更新时继承了上一次的惯性。当学习率较高(lr=0.05)时,容易在损失函数的极小值点两侧“来回冲过头(Overshoot)”,形成剧烈抖动,直到惯性积累与反向梯度达到平衡后才稳定下来。
3. 平缓渐进型:基础与衰减系优化器(SGD, ASGD, Adadelta, RMSprop)
- SGD / ASGD:蓝色与橘色曲线几乎重合,下降趋势平缓顺滑,没有任何剧烈震荡。但由于没有加速机制,在 200 个 Steps 结束时 Loss 仍处于约 0.015~0.020 相对较高的位置,尚未完全收敛。
- Adadelta:红色曲线前期下降较慢,在 50~125 个 Steps 阶段才出现明显的加速下探,最终在 125 Steps 左右趋于稳定。
- RMSprop:黄色曲线呈现非常平滑的指数衰减形态,没有像 Adam 那样的剧烈抖动,最终在 75 Steps 左右平稳降至低点。
2. 核心结论与经验法则(“不存在绝对的好坏”)
- 收敛效率:在本次的二次函数回归任务中,Adam / AdamW / Adamax / Adagrad 展现出了极高的求解效率;而 SGD / ASGD 在相同 Step 下收敛最慢。
- 稳定性对比:RMSprop 兼顾了较快的收敛速度与极佳的曲线平滑度;而带大动量的 Momentum 则表现出很强的冲动性(抖动大)。
- 选择策略:
- 追求快速收敛与省心调参:优先选用 AdamW 或 Adam。
- 需要平滑轨迹与防止剧烈震荡:选择带有自适应均方根控制的 RMSprop。
- 传统模型精细调优:使用 Momentum 时,如果发现如上图绿线的剧烈震荡,通常需要适当降低学习率或配合学习率衰减(Learning Rate Decay)策略。