模型的评价
交叉验证
交叉验证(Cross Validation)是一种常用的在建模应用中评估模型表现和泛化能力的方法,其核心目的是更可靠地评估模型,避免过拟合或欠拟合,选择最优模型和参数。以下为你详细介绍:
-
基本原理与操作流程 :把数据集分割为多个互不重叠的子集(通常称为 "折"),每次选取其中一部分子集作为训练集来训练模型,另一部分作为测试集评估模型,如此重复多次,将每次评估结果综合起来,得到模型的整体评估效果。以最常见的 k 折交叉验证为例,假设 k=5,即把数据集分成 5 个大小相近的子集,每次选择 1 个子集作为验证集,其余 4 个子集作为训练集进行模型训练和评估,这个过程重复 5 次,确保每个子集都作为一次验证集,最终取 5 次评估结果的平均值作为模型性能指标。
-
主要方法
-
k 折交叉验证(k-fold cross validation):将数据集分成 k 个大小相近的子集,依次选取一个子集作为验证集,其余子集作为训练集进行训练和评估,重复 k 次,取平均结果。优点是所有数据都用于训练和验证,数据利用率高;缺点是计算量随 k 值增大而增加 。
-
留一法交叉验证(leave-one-out cross validation,LOOCV):是 k 折交叉验证的极端形式,每次仅用数据集中的一个样本作为验证集,其余样本作为训练集,数据集有 n 个样本,就进行 n 次训练和验证。在数据量较少时很有用,能最大程度利用数据训练模型,但计算开销极大,当数据集较大时耗时很长。
-
分层 k 折交叉验证(stratified k-fold cross validation):用于分类任务,确保每个折中各类别样本的比例与原始数据集一致。对于类别不平衡的数据集,可避免某些折中类别样本过少或不存在的情况,使模型评估更合理。
-
留出法(holdout method):直接把数据集分为训练集和测试集两部分,用训练集训练模型,测试集评估性能。操作简单,但评估结果依赖于数据划分方式,可能无法全面反映模型性能,不是严格意义上的交叉验证方法。
-
应用场景与作用
- 模型选择:在多个候选模型中挑选最合适的模型时,交叉验证可在同一套交叉验证集上评估不同模型的性能,如准确率、召回率、均方误差等指标,进而选择表现最佳的模型。以预测用户是否会点击广告为例,有逻辑回归、决策树、神经网络等多种模型可供选择,通过交叉验证,计算各模型在不同数据集划分下的评估指标并比较,能客观判断哪种模型对该预测任务更有效 。
- 超参数调优:在模型训练过程中,超参数(如学习率、正则化系数、树的深度等)无法通过训练数据直接学习得到,需要人为设定。利用交叉验证,尝试不同的超参数组合,计算每个组合下模型的性能指标,找到使模型性能最优的超参数设置。比如在训练支持向量机(SVM)模型时,通过交叉验证调整惩罚参数 C 和核函数参数,以获得最佳分类效果 。
- 特征选择与评估:在特征工程阶段,判断哪些特征对模型性能提升有显著作用时,可基于不同特征组合进行交叉验证。若添加某个特征后,模型在交叉验证中的性能明显提升,说明该特征重要;反之,若对性能影响不大或使性能下降,则可考虑舍弃。在房价预测任务中,房屋面积、房间数量、房龄等众多特征中,通过交叉验证可确定对房价预测贡献大的关键特征,构建更精简有效的特征集,提高模型计算效率和解释性 。
- 评估模型稳健性:交叉验证多次训练和测试模型,观察模型在不同子集上的表现差异,评估其稳健性。若模型在每次交叉验证中的性能指标波动小,表明它对不同数据子集适应性强,泛化能力好;若波动大,则说明模型可能对特定数据敏感,稳健性欠佳。例如在图像分类任务中,使用交叉验证查看模型在不同批次图像数据上的准确率,判断模型是否稳定 。
- 应对小数据集挑战:当数据量有限时,传统划分训练集和测试集的方法可能因数据划分不合理导致评估结果偏差。交叉验证多次利用数据,能有效提高数据使用效率,在小数据集上也能得到相对可靠的模型评估结果。比如医学研究中疾病样本数量少,采用交叉验证可更充分利用有限数据评估模型性能 。
- 模型融合:在模型融合场景下,交叉验证用于评估不同模型组合的效果。将多个模型进行融合(如投票法、堆叠法等),通过交叉验证选择能使融合模型性能最优的组合方式,提升整体模型的预测能力。在多模态情感分析中,融合文本、语音、图像等多种模态数据的模型时,利用交叉验证确定不同模态模型的最佳融合策略 。
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

常用的交叉验证函数
【sklearn】KFold、StratifiedKFold、GroupKFold的区别-CSDN博客
首先说明

class 为要预测的标签,group为数据中根据某一特征,数据的分组。
(1)kfold
在这种 K 折交叉验证技术中,整个数据集被划分为 K 个相等大小的部分;每个分区称为一个"折叠"。因此,因为我们有 K 个部分,所以我们称之为 K 折叠。其中1折用作验证集,其余 K-1 折用作训练集。该技术重复 K 次,直到每个折叠用作验证集,其余折叠用作训练集。模型的最终精度是通过取 k-models 验证数据的平均精度来计算的。这种方法是最常见的交叉验证方法。
这种分组 ,无视class 或者 group ,完全随机分组

上图就是4折交叉验证,红色代表验证集,蓝色代表训练集;class代表数据的label有三类,group代表不同数据属于第几组。
**特点:**整个数据集既用作训练集又可以用作验证集;但是不适用于数据集不均衡和时间序列问题
>>> import numpy as np
>>> from sklearn.model_selection import KFold
>>> X = ["a", "b", "c", "d"]
>>> kf = KFold(n_splits=2)
>>> for train, test in kf.split(X):
... print("%s %s" % (train, test))
[2 3] [0 1]
[0 1] [2 3]
(2)Stratified KFold
KFold方法不适用于不平衡的数据集,所以Stratified KFold交叉验证来解决这个问题。在分层k倍交叉验证中,数据集被划分为k个组或折叠,但是 不同class 的比例在每一种数据中是一样的,即
整体数据中每种class 比例=训练集中每种class 比例=验证集中每种class 比例

特点:可以解决数据不均衡的问题,不适用于时间序列问题
>>> from sklearn.model_selection import StratifiedKFold, KFold
>>> import numpy as np
>>> X, y = np.ones((50, 1)), np.hstack(([0] * 45, [1] * 5))
>>> skf = StratifiedKFold(n_splits=3)
>>> for train, test in skf.split(X, y):
... print('train - {} | test - {}'.format(
... np.bincount(y[train]), np.bincount(y[test])))
train - [30 3] | test - [15 2]
train - [30 3] | test - [15 2]
train - [30 4] | test - [15 1]
(3)Group KFold
GroupKFold是KFold一个变体,目的在于将group严格分开,以减少过拟合现象。GroupKFold 会保证同一个group的数据不会同时出现在训练集和测试集上。因为如果训练集中包含了每个group的几个样例,可能训练得到的模型能够足够灵活地从这些样例中学习到特征,在测试集上也会表现很好。但一旦遇到一个新的group它就会表现很差

**特点:**可以将数据的group完全分开,避免样本高度相似出现重复
>>> from sklearn.model_selection import GroupKFold
>>> X = [0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 8.8, 9, 10]
>>> y = ["a", "b", "b", "b", "c", "c", "c", "d", "d", "d"]
>>> groups = [1, 1, 1, 2, 2, 2, 3, 3, 3, 3]
>>> gkf = GroupKFold(n_splits=3)
>>> for train, test in gkf.split(X, y, groups=groups):
... print("%s %s" % (train, test))
[0 1 2 3 4 5] [6 7 8 9]
[0 1 2 6 7 8 9] [3 4 5]
[3 4 5 6 7 8 9] [0 1 2]
(4)Stratified Group KFold
可以将数据的group和标签的class完全分层划开,避免出现样本高度相似和标签分布不均的问题

>>> from sklearn.model_selection import StratifiedGroupKFold
>>> X = list(range(18))
>>> y = [1] * 6 + [0] * 12
>>> groups = [1, 2, 3, 3, 4, 4, 1, 1, 2, 2, 3, 4, 5, 5, 5, 6, 6, 6]
>>> sgkf = StratifiedGroupKFold(n_splits=3)
>>> for train, test in sgkf.split(X, y, groups=groups):
... print("%s %s" % (train, test))
[ 0 2 3 4 5 6 7 10 11 15 16 17] [ 1 8 9 12 13 14]
[ 0 1 4 5 6 7 8 9 11 12 13 14] [ 2 3 10 15 16 17]
[ 1 2 3 8 9 10 12 13 14 15 16 17] [ 0 4 5 6 7 11]
(5)Time Series Split
数据的顺序对于与时间序列相关的问题非常重要。 对于与时间相关的数据集,将数据随机拆分或k倍拆分为训练和验证可能不会产生良好的结果。对于时间序列数据集,根据时间将数据分为训练和验证,也称为前向链接方法或滚动交叉验证。

**特点:**专门设计用于解决时间序列问题的数据切分方法。
>>> from sklearn.model_selection import TimeSeriesSplit
>>> X = np.array([[1, 2], [3, 4], [1, 2], [3, 4], [1, 2], [3, 4]])
>>> y = np.array([1, 2, 3, 4, 5, 6])
>>> tscv = TimeSeriesSplit(n_splits=3)
>>> print(tscv)
TimeSeriesSplit(gap=0, max_train_size=None, n_splits=3, test_size=None)
>>> for train, test in tscv.split(X):
... print("%s %s" % (train, test))
[0 1 2] [3]
[0 1 2 3] [4]
[0 1 2 3 4] [5]
混淆矩阵
混淆矩阵(Confusion Matrix)是机器学习和统计分析中用于衡量分类模型性能的重要工具,它以矩阵形式直观展示模型预测结果与真实结果之间的关系,在多种领域有广泛应用。
- 基本概念
- 定义:混淆矩阵是一个二维矩阵,行数和列数等于分类问题中的类别数。矩阵的行表示真实类别,列表示预测类别。通过统计不同类别样本被正确分类和错误分类的情况,全面反映模型在各类别上的预测性能。
- 示例:以一个简单的二分类问题(判断邮件是否为垃圾邮件)为例,混淆矩阵如下: | 预测结果 \ 真实结果 | 垃圾邮件(正类)| 正常邮件(负类)| |----|----|----| | 垃圾邮件(正类)| 真正例(TP):模型正确预测为垃圾邮件的数量 | 假正例(FP):模型错误预测为垃圾邮件的正常邮件数量 | | 正常邮件(负类)| 假负例(FN):模型错误预测为正常邮件的垃圾邮件数量 | 真负例(TN):模型正确预测为正常邮件的数量 |
- 计算指标
- 准确率(Accuracy):指模型正确预测的样本数占总样本数的比例,计算公式为:

。它反映模型整体的预测准确性,但在类别不均衡问题中,可能会掩盖模型在少数类上的表现。
- 精确率(Precision):针对正类而言,是指模型预测为正类且实际也为正类的样本数占模型预测为正类样本数的比例,公式为:

。精确率越高,说明模型预测为正类的样本中,真正属于正类的比例越大。
- 召回率(Recall):又称灵敏度(Sensitivity)或真正例率(True Positive Rate,TPR),同样针对正类,是指模型正确预测为正类的样本数占实际正类样本数的比例,即

。召回率越高,表明模型对正类样本的识别能力越强。
- F1 值(F1-Score):是精确率和召回率的调和平均数,综合考虑了两者的性能,计算公式为:

。F1 值越高,说明模型在正类的预测上综合表现越好。
- 假正例率(False Positive Rate,FPR):是指模型错误预测为正类的样本数占实际负类样本数的比例,

。FPR 越低,模型将负类误判为正类的情况越少。
ROC曲线
二分类问题
ROC曲线(Receiver Operating Characteristic)全称:受试者工作特征曲线
提到ROC曲线就要先说明一下两个概念:FPR(伪正类率),TPR(真正类率)
而FPR(False Positive Rate)= FP /(FP + TN),即负类数据被分为正类的比例
TPR(True Positive Rate)= TP /(TP + FN),即正类数据被分为正类的比例
那什么是ROC曲线呢?
我们看一下ROC曲线的图示:

深入理解ROC曲线的定义以及绘制ROC曲线过程,其与模型性能的关系,以及AUC_roc曲线形成过程-CSDN博客

分类 损失函数
分类真值的表示
分类问题中真值通常embedding为one-hot 或者 multi-hot表示
| 编码类型 | 定义 | 示例(3 类) | 特点 |
|---|---|---|---|
| 单标签多分类One-hot | 仅一个类别为 1,其余为 0 | [0, 1, 0] | 用于单标签互斥分类 |
| 多标签多分类 Multi-hot | 多个类别可为 1 | [1, 0, 1] | 用于多标签非互斥分类 |
上述1 代表属于这个类别,0代表不属于这个类别。单标签采用One-hot编码,编码结果中 只有一个 1 (表示只能属于唯一一个类别)。多标签采用Multi-hot编码,编码结果中可以有多个 1(表示可以属于多个类别)
模型预测的表示
模型预测输出为logits结果,比如[2.0,-1.0,0.5] 是一组实数值,然后经过softmax 或者 sigmod转为为概率结果
| 任务类型 | 激活函数 | 结果 | 输出概率性质 |
|---|---|---|---|
| 单标签多分类 | Softmax | [0.9, 0.001, 0.099] | 求和为 1 |
| 多标签多分类 | Sigmoid | [0.9, 0.269, 0.9] | 每个概率结果∈(0,1) ,彼此独立,求和 ≠ 1 |
单标签的激活函数softmax 强制不同类别互斥,比如模型认为"类别 猫"的概率很高(比如 0.9),那么其他所有类别的概率之和只能是 0.1,Softmax 会把它们的概率"压缩"到加起来不超过 1。它无法表达"这个样本既是猫又是狗"。
多标签的激活函数sigmod则**不要求总和为 1,**因此类别"猫"的概率可以为0.9,同时"狗"的概率也可以为0.9
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

单标签多分类 → 使用交叉熵损失(Cross-Entropy Loss)
对于每个样本损失函数为: ,其中y为one-hot编码,p为softmax结果,因为one-hot编码中只有正确类别为1,所以损失函数为: 只取正确类别位置i处的概率
代码
import torch
import torch.nn as nn
# 模型预测的logits值,3个样本,4个类别
logits = torch.tensor([
[2.0, 1.0, 0.1, 0.5],
[0.1, 1.2, 3.0, 0.8],
[1.5, 0.2, 0.3, 2.1]
], requires_grad=True)
targets = torch.tensor([0, 2, 3]) # 必须是 torch.long(默认就是),3个样本对应的类别id
# 定义损失函数
criterion = nn.CrossEntropyLoss()
# 计算损失
# 直接将logits 和 targets 传入即可
loss = criterion(logits, targets)
print("CrossEntropyLoss =", loss.item())
交叉熵损失函数支持 忽略某个无效类别和手动设置不同类别的权重,
忽略无效类别: 在一张图片中,存在 背景类别+物体类别,但也存在不需要学习的区域: 比如 遮挡区域、标注错误 / 模糊区域、车道线被车挡住、图片边缘、padding 区域,这些区域的真值标签设置为255 或 **-1,**在计算损失时候忽略样本预测到不需要学习区域的样本,不计算损失。
**权重:**给每个类别手动设置一个权重,让模型更关注 "少样本类别"。 比如图片识别中,背景比例95%,物体比例5%,所以模型预测中,95%的预测为预测背景,只有5%的预测为预测物体,设置:背景权重0.1,物体权重1,那么在损失结果中,提升预测为物体的损失权重,减小预测为背景的损失权重。
import torch
import torch.nn as nn
# 模型预测的logits值,3个样本,4个类别
logits = torch.tensor([
[2.0, 1.0, 0.1, 0.5],
[0.1, 1.2, 3.0, 0.8],
[1.5, 0.2, 0.3, 2.1]
], requires_grad=True)
# 1、真值标签(这里忽略标签为 ignore_index=-1,只要这个样本的真值标签为99,这个样本就直接跳过,不算损失!)
targets = torch.tensor([0, -1, 3]) # -1 = 要忽略的标签
# 2、设置4个类别的不同权重
weights = torch.tensor([1.0, 2.0, 3.0, 4.0]) # 4个类别,每个类别的权重
criterion = nn.CrossEntropyLoss(
weight=weights, # 类别权重
ignore_index=-1 # 忽略标签=99的样本
)
# 计算损失
loss = criterion(logits, targets)
print("CrossEntropyLoss =", loss.item())
使用NLLLoss,有时候为了显示控制CrossEntropyLoss计算过程,会使用log_softmax+NLLLoss等价替换CrossEntropyLoss,比如上述例子改为
import torch
import torch.nn as nn
import torch.nn.functional as F
# 模型预测的 logits
logits = torch.tensor([
[2.0, 1.0, 0.1, 0.5],
[0.1, 1.2, 3.0, 0.8],
[1.5, 0.2, 0.3, 2.1]
], requires_grad=True)
# 真值标签(第二个样本设为忽略 -1)
targets = torch.tensor([0, -1, 3])
# 类别权重
weights = torch.tensor([1.0, 2.0, 3.0, 4.0])
# ==========================================
# 【关键】NLLLoss 必须先算 log_softmax,先softmax 然后 -log
# ==========================================
log_probs = F.log_softmax(logits, dim=1)
# ==========================================
# NLLLoss 同样支持 weight 和 ignore_index
# ==========================================
criterion = nn.NLLLoss(
weight=weights,
ignore_index=-1
)
# 计算损失
loss = criterion(log_probs, targets)
print("NLLLoss =", loss.item())
多标签多分类 → 使用二元交叉熵损失(Binary Cross-Entropy, BCE)
多标签中每个类别可以看作一个独立的二分类问题:
- 对类别 c :判断"是否属于该类" → 是/否(1/0)
- 共有 C个这样的二分类器
- 总损失 = 所有类别 BCE 损失之和(或平均)
单个样本的 BCE 损失(对所有类别求和): ,其中y 为1 或者 0,1代表正样本(属于该类别),0代表负样本(不属于该类别),P为正样本概率,1-p为负样本概率,
,
Pt为模型对真实类别(正样本、负样本)的预测概率,因此,BCE就是 真值为正样本就取正样本预测概率,真值为负样本就取负样本预测概率
代码
import torch
import torch.nn as nn
# 模型输出 logits(未经过 sigmoid)一共3个样本,4个类别
logits = torch.tensor([
[2.0, -1.0, 0.5, 1.2], # 样本0
[-0.5, 1.8, 0.0, -0.2], # 样本1
[1.0, 0.9, -0.3, 2.1] # 样本2
], requires_grad=True)
# 真实标签:二值 float 张量,shape (3, 4)
targets = torch.tensor([
[1, 0, 1, 0],
[0, 1, 0, 0],
[1, 1, 0, 1]
], dtype=torch.float32) # ⚠️ 必须是 float!
# 定义损失函数
criterion = nn.BCEWithLogitsLoss()
# 计算损失
loss = criterion(logits, targets)
print("Loss:", loss.item())
广义的BCE
二元交叉熵损失 中,真值标签 可以为0到1之间的任何的小数,而不是非0即1。根据BCE的求导 ,当 时候,损失最小,即预测概率和真值概率越接近,BCE损失越小。
Focal loss
针对多标签多分类中,有多个样本,但大部分属于负样本,造成总的损失函数中 负样本概率结果为主导,正样本概率结果很少,这种正负样本数量不均衡会造成模型学偏,负样本更容易学习出来,正样本很难学出。
因此为了平衡上述关系,Focal loss 在 BCE 基础上引入两个关键因子:
其中:
- :为每一类别概率。区分正负样本概率, 正样本概率负样本概率比如比如
- :focusing parameter (聚焦参数),默认 = 2。
- :平衡因子,用于调节正负样本权重, 正样本平衡因子负样本平衡因子比如比如
👉对于容易学出的负样本 →1, 接近于 0**,负样本损失被大幅抑制;**
👉对于难学出的正样本 →0, 接近于 1**,,损失几乎不变。**
样本权重是 共同调节的结果, 调节是次方力度,解释一下为什么正样本平衡因子取0.25而不是取0.75,这样岂不是难学正样本权重 小于 易学习的负样本?首先, 调节是次方力度,已经将难学正样本的权重远高于负样本了(比如正样本(1-0.1)的平方=0.81 >> 负样本(1-0.8)的平方0.04)如果正样本平衡因子 再大于 负样本平衡因子,那么损失函数中正样本权重就太大,导致损失中仅仅是少量正样本主导,训练不稳定。所以正样本的次方调节力度 大于负样本 ,但是正样本平衡因子 小于 负样本平衡因子,二者共同,使得正负样本的均衡
class FocalLossBinary(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0, ignore_index=255):
super().__init__()
self.alpha = alpha
self.gamma = gamma
self.ignore_index = ignore_index
def forward(self, inputs, targets):
# inputs: [B, H, W] 通常为正样本结果
# targets: [B, H, W] 0/1/255
prob = torch.sigmoid(inputs)#转为概率
targets = targets.float()
mask = (targets != self.ignore_index).float()#找到忽略的标签
targets = torch.where(targets == self.ignore_index, 0.0, targets)#忽略标签暂时设置为负样本
# 正样本
pos_loss = -self.alpha * (1 - prob) ** self.gamma * torch.log(prob + 1e-8)
# 负样本
neg_loss = -(1 - self.alpha) * prob ** self.gamma * torch.log(1 - prob + 1e-8)
#taget=1代表真值为正样本,取正样本结果,target=0 代表真值为负样本,取负样本结果
loss = torch.where(targets == 1, pos_loss, neg_loss)
loss = loss * mask#取不为忽略标签位置的loss结果
return loss.sum() / mask.sum().clamp(min=1)
度量学习(Metric Learning)损失
在分类交叉熵损失中,数据的总类别数量是固定的,logits值经过softMax后输出所有类别中的唯一类别。但是如果数据中总类别数不固定,且数量巨大时候呢?比如人脸识别中,数据库中有非常多的人,且数据库中人的数量还在不断增加,如果按照分类来做,需要输出一个维度巨大的logits结果,且每增加一个人就需要重新训练一次。这时候就需要用到度量学习损失了。
度量学习是让模型学习针对不同类别学习出一个向量,同类别的向量距离更近,不同类别的向量距离更远。比如人脸识别中有很多人的照片,同一个人又有不同角度的多张照片。将照片输入模型,模型输出一个N维向量。不同人的照片获得的向量 距离差别很大,从而区分不同人。而相同人的不同角度照片的向量则距离差别很小,代表了属于同一个人。
一、度量学习核心思想
不直接做分类 ,而是学习一个特征嵌入空间:
- 同类样本:特征距离尽可能近
- 异类样本:特征距离尽可能远
二、分类和度量学习的区别
(1)分类损失学的是:决策边界
- 只能识别训练里出现过的固定类别
- 不关心特征距离、不关心相似度
- 输出直接是「哪一类」
(2)度量损失学的是:特征空间距离
- 同类靠近、异类远离
- 支持从未见过的新类别做匹配、检索
- 输出向量,靠距离 / 余弦相似度判断
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

三、分类和度量学习如何选
(1)什么时候 用 分类交叉熵
- 类别固定、数量少、永远不新增类别
比如:猫狗二分类、10 分类花卉、MNIST 数字。 - 只需要输出类别标签,不需要相似度、不需要检索
任务终点就是:给我一个类别结果就行。 - 测试集和训练集类别完全一样,不需要泛化新类
- 网络最后一层直接输出类别 logits,不需要拿特征做比对
(2)什么时候 用 度量损失
- 类别无限多、随时会新增未知类别
如:人脸、行人、车辆,永远收集不完所有人。 - 需要做 比对、匹配、检索、以图搜图
1:1 人脸验证、1:N 人脸库检索、行人 ReID、商品检索。 - 细粒度分类,类间差异极小
车型、鸟类、花卉、相似菜品 ------CE 分不清微小差异。 - 少样本 / 零样本
每类只有一两张样本,CE 直接过拟合崩掉。
四、常见的度量学习损失
基础定义:
- 锚点 的特征向量: (Anchor)
- 正样本 的特征向量: (和anchor同类别,Positive)
- 负样本 的特征向量: (和anchor不同类别,Negative)
- 锚点和正样本的距离:
- 锚点和负样本的距离:
-- Triplet Loss 三元组损失(最经典)
核心约束:正样本与anchor距离 比 负样本与anchor距离 至少小一个间隔m
损失函数:
当 正样本与anchor距离 比 负样本与anchor距离小的时候,损失为0;当正样本与anchor距离 比 负样本与anchor距离大的时候,损失不为0。
以人脸识别为例,如果有K 个人,每个人选 M 张图,对于每个人找 (1)该类别自身 () 个向量距离中,最远的距离作为锚点和正样本距离;(2)该类别与其他类别的 () 个距离中,最近的距离作为锚点和负样本的距离
--NDPushPullLoss(Normalized Distance Push-Pull Loss)归一化距离推拉损失
让同类的向量之间距离尽可能的小(内聚(Pull)),让不同类别间的向量距离尽可能的大(分离(Push))
损失函数:
1)Pull Loss(拉力损失)
让同一类的所有特征向量向类中心聚拢:
- :第 p类别的第i个样本的特征向量
- : 第 p类别的平均特征向量,类中心(同类别所有特征的均值 )
- : 设定的最小距离
对于同类别的特征向量 如果距离类中心太远,损失不为0,进行惩罚;否则,损失为0,不惩罚。
2)Push Loss(推力损失)
让不同类的类中心相互远离,保持至少 间隔:
- , :为类别 和类别 的类中心
- :为安全间隔。
当不同类的类中心间距大于 安全间隔,损失为0,不惩罚;否则损失不为0,进行惩罚。
Normalization
深度网络希望每层的输入都是在0附近波动的值,这样反向传播传播时候才能使得梯度快速下降,可以想象,如果每一层的结果都很小或者很大,会造成梯度消失或者爆炸。 normalization 就是将数据归一化,防止数据过大或者过小
boom:详解Layer Normalization和Batch Normalization

- Batch Normalization:在训练过程中,对每个小批量数据进行归一化处理。具体来说,就是对每个特征维度计算其在小批量中的均值和方差,然后将该维度的数据进行归一化,使其均值为 0,方差为 1。这样可以加速模型的收敛,减少梯度消失或爆炸的问题,同时也有助于减少模型对初始化的依赖。
- Layer Normalization:与 Batch Normalization 不同,Layer Normalization 是对单个样本的所有特征维度进行归一化。它计算每个样本在所有特征维度上的均值和方差,然后对该样本的所有特征进行归一化。这种方法更适合于处理变长序列数据,如自然语言处理中的文本序列,因为它不依赖于小批量的统计信息,而是针对每个样本独立进行归一化。
- **RMSNormalization:**通常矩阵计算要进行归一化防止不同特征的取值过大,常用的是layernorm,也就是每一项减去样本的均值,再除以样本的方差;而RMSNorm则是去除了减去均值的操作,以便提升效率
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

正则化
正则化(Regularization)是一种用于防止机器学习模型过拟合的技术。过拟合是指模型在训练数据上表现很好,但在新的、未见过的数据(测试数据)上表现不佳的现象。正则化通过在模型的损失函数中添加一个惩罚项,限制模型的复杂度,使得模型在拟合数据的同时,不会过度学习训练数据中的噪声和细节,从而提高模型的泛化能力。
- 常见正则化



Epoch,iteration和batch_size
在模型训练过过程中,每一个样本输入模型可以得到一个损失函数,假设样本为100,那么我们可以得到100个损失函数,在梯度下降中(这里指 批量梯度下降法)需要对所有的样本的损失函数进行求导,但是这样太消耗时间和资源,那么我们每次可以抽取一部分数据进行求导,然后更新参数(这就是小批量梯度下降法),比如我们一次取20个样本,那么需要5次这种操作才能用完100个样本。
所以一个epoch为100个样本全部使用完一次,batch_size就是梯度下降求导一次使用的样本数这里就是20,iteration 就是迭代次数,这里就是5次
过程
假设训练样本一共100个,batch_size=20
-------> epoch i
│ |
│ |
│ 第1️⃣个batch 训练
│ |
│ |
│ 模型参数更新
│ |
│ |
│ 第2️⃣个batch 训练
│ |
│ |
│ 模型参数更新
│ |
│ |
│ 。。。。。。。。。。
│ |
│ |
│ 第5️⃣个batch 训练
│ |
│ |
│ 模型参数更新
│ |
│ |
<-------epoch i+1
softmax分类器
将数值转化为概率

激活函数
SunshineSki:深度学习笔记:如何理解激活函数?(附常用激活函数)
一些激活函数的问题
- 梯度消失:指的是在使用反向传播算法进行训练时,靠近输入层的层(即较早层)的权重更新变得非常小,几乎不更新。这是因为随着网络层数的增加,在链式法则下,误差梯度在向前(实际上是向输入层方向)传播过程中不断乘以小于1的数(特别是当激活函数如Sigmoid或Tanh输出接近于0或1时,其导数值会很小),导致梯度呈指数级衰减。结果就是,前面的层学习速度极慢甚至停止学习,这对模型性能有负面影响。比如:Sigmoid函数的导数,如下图所示:

如果我们初始化神经网络的权值为 [ 0 , 1 ] 之间的随机值,由反向传播算法的数学推导可知,梯度从后向前传播时,每传递一层梯度值都会减小为原来的0.25倍,如果神经网络隐层特别多,那么梯度在穿过多层后将变得非常小接近于0,即出现梯度消失现象
- 梯度爆炸:是指在训练深度神经网络时,误差梯度在反向传播过程中变得非常大。这种情况通常发生在权重初始化不当或者网络层数特别多的情况下。如果网络中的某些权重初始值过大,那么在反向传播过程中计算梯度时,这些权重会导致梯度值迅速增大,可能导致权重更新的幅度非常大,使得网络无法收敛,甚至出现NaN(非数字)值。
- 激活函数输出的均值不为0:当激活函数的输出不是以零为中心时,这意味着它的输出总是在正数或负数区间内。例如,Sigmoid函数的输出范围是(0, 1),而Tanh函数的输出是以零为中心的(-1, 1)。如果激活函数的输出总是正数(如Sigmoid的情况),那么通过该激活函数后的神经元的输入也倾向于全是正值或者负值,这会导致权重更新只在一个方向上进行,从而使得参数更新过程变得低效。因为优化器在试图最小化损失函数时,可能需要更多次迭代才能找到最优解。Sigmoid 的 output 不是0均值(即zero-centered)。这是不可取的,因为这会导致后一层的神经元将得到上一层输出的非0均值的信号作为输入。 产生的一个结果就是:如x>0 , f = w x + b ,那么对w求局部梯度则都为正,这样在反向传播的过程中w要么都往正方向更新,要么都往负方向更新,导致有一种捆绑的效果,使得收敛缓慢。
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

ReLu激活函数
relu函数在负半区的导数为0 ,所以一旦神经元激活值进入负半区,那么梯度就会为0,而正值不变,这种操作被成为单侧抑制。(也就是说:在输入是负值的情况下,它会输出0,那么神经元就不会被激活。这意味着同一时间只有部分神经元会被激活,从而使得网络很稀疏,进而对计算来说是非常有效率的。)正因为有了这单侧抑制,才使得神经网络中的神经元也具有了稀疏激活性。尤其体现在深度神经网络模型(如CNN)中,当模型增加N层之后,理论上ReLU神经元的激活率将降低2的N次方倍。
SiLu激活函数
ReLu激活函数的问题是**神经元死亡问题(Dying ReLU)**当输入始终 ≤ 0 时,梯度为 0,权重无法更新 → 神经元"死亡"
SiLu = x * sigmoid(x)
导数d(SiLu)=sigmoid(x)+x*sigmoid(x )(1−sigmoid(x))
- 正数区域内,SiLU 函数的输出与 ReLU 函数的输出相同,SiLu导数约等于ReLu导数
- 在负数区域内,SiLU 函数的输出与 sigmoid 函数的输出相同,SiLu导数不为0,解决神经元死亡问题
- SiLu导数连续,ReLu导数非连续
- SiLU函数不是单调递增的,而是在x≈−1.28时达到全局最小值−0.28,这可以起到一个隐式正则化的作用,抑制过大的权重。

GELU(Gaussian Error Linear Unit)
GELU(x )=x ⋅Φ(x)
其中 Φ(x)是标准正态分布的累积分布函数。
🔍 特点:
- 平滑、非单调,比 ReLU 更符合神经科学观察。
- 被 BERT、GPT 等原始 Transformer 模型采用 作为 FFN 的激活函数。
📌 应用:
- BERT、GPT-1/2/3 的前馈网络中使用
GELU。
GLU(Gated Linear Unit)
GLU(x)=(W 1x +b 1)⊗σ (W 2x +b2)
其中 σ 是 sigmoid 函数,⊗是逐元素乘法。
🔍 特点:
- 引入门控机制(gate),控制信息流动。
- 常用于序列模型(如 CNN-based NLP 模型)。
ReGLU
GLU(x )=(W 1x )⊗ReLU(W 2x)
- 门控使用 ReLU。
- 计算简单,但 ReLU 的"死亡"问题可能影响性能。
GeGLU
GeGLU(x )=(W 1x )⊗GELU(W 2x)
- 门控使用 GELU,更平滑。
- 被 Google 的 T5、PaLM 等模型采用。
📌 论文《GLU Variants Improve Transformer》(2020)证明: GeGLU 在多项任务上优于 ReLU 和标准 GELU。
SwiGLU
SwiGLU(x )=(W 1x )⊗Swish(W 2x)
其中 Swish(x )=x ⋅σ (x ),也称为 SiLU(Sigmoid Linear Unit)。
- Swish/SiLU 是一种自门控激活函数,兼具平滑性和非单调性。
- 被 LLaMA、LLaMA2、PaLM、Phi 等现代大模型广泛采用。
✅ SwiGLU 是当前最先进的 FFN 激活函数之一。
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

权重初始化
深度学习中神经网络的几种权重初始化方法_神经网络权重初始化方法-CSDN博客
权重初始化(Weight Initialization)是深度学习模型训练成功的关键第一步。好的初始化可以:
- 加速收敛:让模型更快找到最优解。
- 防止梯度消失/爆炸:确保信号在前向传播和反向传播中保持稳定的方差。
- 打破对称性:确保同一层的不同神经元学习到不同的特征。
以下是主流初始化方法
Xavier (Glorot) 初始化
1、核心思想
核心思想:保持方差稳定
在深层网络中,信号需要经过层层传递:
- 前向传播:输入信号 x 经过多层线性变换和激活函数,最终到达输出层。
- 反向传播:损失函数的梯度 ∇L 从输出层逐层传回输入层。
如果每一层的输出方差(Variance)与输入方差相差太大:
- 方差逐渐变大 →→ 信号爆炸(Exploding)。
- 方差逐渐变小 →→ 信号消失(Vanishing),导致深层神经元"死掉",梯度无法传回。
Xavier 初始化的目标 :
设计一种权重分布,使得每一层的输出方差等于输入方差 ,同时反向传播的梯度方差也保持稳定。
2、数学推导
假设第 L 层的线性变换为 z=Wx+b ,其中:
- W 是权重矩阵,维度为 ( , ) 。
- x 是输入向量。
- 是输入节点数, 是输出节点数。
- 假设权重 W 和输入 x 都是独立同分布,且均值为 0。
前向传播的方差
根据统计学性质, z 中每个元素的方差为:
Var(z)= ⋅Var(W)⋅Var(x)
为了保持方差不变(即 Var(z)=Var(x)),我们需要:
⋅Var(W)=1 ⟹ Var(W)=
反向传播的方差
同理,在反向传播时,梯度从后一层传回前一层,涉及的是转置矩阵 WT,其输入节点数变成了 。为了保持梯度方差不变,我们需要:
⋅Var(W)=1 ⟹ Var(W)=
完美的折中方案
我们既要满足前向稳定,又要满足反向稳定。Glorot 提出取两者的调和平均数(或者简单理解为兼顾两者):
这就是 Xavier 初始化的标准差公式
3、两种分布实现
基于上述方差,Xavier 初始化提供了两种具体的采样方式:
A. Xavier Normal (正态分布)
从均值为 0,标准差为 σ 的高斯分布中采样:
W∼N(0, )
B. Xavier Uniform (均匀分布) ------ 更常用( init.xavier_uniform_默认使用的就是这个公式)
从均匀分布 U[−a,a]中采样。均匀分布 U[−a,a] 的方差公式为 。
令 ,解得 a= 。
W∼U[ ∼ ]
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

4、代码
以下为理解代码,手动计算分布的标准差
import torch
import torch.nn.init as init
# 模拟一个全连接层:输入 100,输出 50
fan_in = 100
fan_out = 50
# 1. 计算理论标准差 (Xavier Normal)
std_dev = (2 / (fan_in + fan_out)) ** 0.5
print(f"Xavier Normal Std Dev: {std_dev:.4f}")
# 2. 计算均匀分布边界 (Xavier Uniform)
limit = (6 / (fan_in + fan_out)) ** 0.5
print(f"Xavier Uniform Bound: [-{limit:.4f}, {limit:.4f}]")
# 3. 使用 PyTorch 内置函数
weight_tensor = torch.empty(50, 100) # (out_features, in_features)
# 应用 Xavier 均匀分布
init.xavier_uniform_(weight_tensor)
print(f"实际生成的权重均值: {weight_tensor.mean():.4f}")
print(f"实际生成的权重标准差: {weight_tensor.std():.4f}")
# 输出应接近理论值
实战代码
import torch.nn as nn
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.tanh = nn.Tanh() # Xavier 的最佳搭档
self.fc2 = nn.Linear(256, 10)
# 显式初始化
init.xavier_uniform_(self.fc1.weight)
init.constant_(self.fc1.bias, 0)
init.xavier_uniform_(self.fc2.weight)
init.constant_(self.fc2.bias, 0)
def forward(self, x):
return self.fc2(self.tanh(self.fc1(x)))
He 初始化
1. 为什么需要 He 初始化?(核心痛点)
在 He 初始化之前,大家普遍使用 Xavier 初始化 。但 Xavier 有一个关键假设:激活函数是关于原点对称的线性函数(如 Tanh, Sigmoid)。
然而,ReLU ( f(x)=max(0,x) ) 打破了这个假设:
- 非对称性:ReLU 将所有负值截断为 0。
- 方差减半:如果输入 x是均值为 0 的对称分布,经过 ReLU 后,大约有一半的神经元输出为 0。这意味着输出的方差变成了输入方差的一半:
后果 :
如果在深层网络中使用 Xavier 初始化 + ReLU:
- 第 1 层方差变为 0.5×Var0
- 第 2 层方差变为 0.5×0.5×Var0
- ...
- 第 L 层方差变为 0.5×........×0.5×Var0
随着层数 L 增加,信号方差会指数级衰减,导致深层网络的梯度迅速消失,模型无法收敛。
2. 数学推导
为了补偿 ReLU 带来的"方差减半"效应,我们需要在初始化权重时,将权重的方差放大 2 倍。
假设第 L层的线性输出为 ,经过 ReLU 后为 。
我们希望前向传播时,每一层的输出方差保持一致:
已知 ReLU 的性质(对于均值为 0 的对称分布输入):
而线性部分的方差为:
联立上述公式:
消去 ,解得 :
结论:He 初始化的核心就是将方差设为 ,而不是 Xavier 的 。
- 它只考虑 fan_in(输入节点数),忽略 fan_out。
- 分子是 2,用来抵消 ReLU 的 0.5 系数。
- 注意 :对于LeakyReLU(斜率为a),公式中的系数 2 需要调整为 。但在大多数默认情况下(a很小或直接使用 ReLU),直接使用标准的2效果已经非常好。
3、两种分布实现
A. 从均值为 0,标准差为 σ 的高斯分布中采样:
W∼N(0, )
B. 均匀分布
从均匀分布 U[−a,a]中采样。均匀分布 U[−a,a] 的方差公式为 。 令 ,解得 a= 。
W∼U[ ∼ ]
4、代码
import torch
import torch.nn as nn
import torch.nn.init as init
# 创建一个 Linear 层
linear = nn.Linear(100, 50) # fan_in = 100
# 方法 1: He Normal (推荐)
# mode='fan_in' 表示只考虑输入节点数
# nonlinearity='relu' 告诉函数我们要用 ReLU,它会自动应用系数 2
init.kaiming_normal_(linear.weight, mode='fan_in', nonlinearity='relu')
# 方法 2: He Uniform
init.kaiming_uniform_(linear.weight, mode='fan_in', nonlinearity='relu')
# 偏置通常初始化为 0
if linear.bias is not None:
init.constant_(linear.bias, 0)
print(f"Weight Std Dev: {linear.weight.std().item():.4f}")
# 理论标准差应为 sqrt(2/100) = 0.1414
class ResNetLike(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.fc = nn.Linear(64, 10)
self._init_weights()
def _init_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear):
# 核心代码:Kaiming 初始化
# nonlinearity 必须与你实际使用的激活函数一致!
# 如果是 LeakyReLU(0.01),则填 'leaky_relu'
init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
if m.bias is not None:
init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
# BN 层权重设为 1,偏置设为 0
init.constant_(m.weight, 1)
init.constant_(m.bias, 0)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
# ...
return x
数据增强
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

优化器
梯度下降法(Gradient Descent)
在微积分中,对多元函数的参数求 偏导数 ,把求得的各个参数的导数以向量的形式写出来就是梯度。梯度就是函数变化最快的地方。梯度下降是迭代法的一种,在求解机器学习算法的模型参数 时,即无约束问题时,梯度下降是最常采用的方法之一。顾名思义,梯度下降法的计算过程就是沿梯度下降的方向求解极小值,也可以沿梯度上升方向求解最大值。 假设模型参数为 ,损失函数为 ,损失函数 关于参数 的偏导数,也就是梯度为 ,学习率为 ,则使用梯度下降法更新参数为:

评价:梯度下降法主要有两个缺点:
训练速度慢 :每走一步都要要计算调整下一步的方向,下山的速度变慢。在应用于大型数据集中,每输入一个样本都要更新一次参数,且每次迭代都要遍历所有的样本。会使得训练过程及其缓慢,需要花费很长时间才能得到收敛解。
**容易陷入局部最优解:**由于是在有限视距内寻找下山的反向。当陷入平坦的洼地,会误以为到达了山地的最低点,从而不会继续往下走。所谓的局部最优解就是鞍点。落入鞍点,梯度为0,使得模型参数不在继续更新。
梯度下降法目前主要分为三种方法,区别在于每次参数更新时计算的样本数据量不同:
批量梯度下降法(BGD, Batch Gradient Descent)
随机梯度下降法(SGD, Stochastic Gradient Descent)
小批量梯度下降法(Mini-batch Gradient Descent)
1、批量梯度下降法BGD
假设训练样本总数为n,样本为 ,模型参数为 ,损失函数为 ,在第i对样本 上损失函数关于参数的梯度为 , 学习率为 ,则使用BGD更新参数为:

由上式可以看出,**每进行一次参数更新,需要计算整个数据样本集,因此导致批量梯度下降法的速度会比较慢,**尤其是数据集非常大的情况下,收敛速度就会非常慢,但是由于每次的下降方向为总体平均梯度,它得到的会是一个全局最优解。
评价:
批量梯度下降法比标准梯度下降法训练时间短,且每次下降的方向都很正确。
2、随机梯度下降法SGD
随机梯度下降法,不像BGD每一次参数更新,需要计算整个数据样本集的梯度,而是每次参数更新时,仅仅选取一个样本 计算其梯度,参数更新公式为:

可以看到BGD和SGD是两个极端,SGD由于每次参数更新仅仅需要计算一个样本的梯度,训练速度很快,即使在样本量很大的情况下,可能只需要其中一部分样本就能迭代到最优解,由于每次迭代并不是都向着整体最优化方向,导致梯度下降的波动非常大,更容易从一个局部最优跳到另一个局部最优,准确度下降。
SGD缺点:
- 选择合适的learning rate比较困难 ,学习率太低会收敛缓慢,学习率过高会使收敛时的波动过大
- 所有参数都是用同样的learning rate
- SGD容易收敛到局部最优,并且在某些情况下可能被困在鞍点
3、小批量梯度下降法
小批量梯度下降法就是结合BGD和SGD的折中,对于含有n个训练样本的数据集,每次参数更新,选择一个大小为m 的mini-batch数据样本计算其梯度,其参数更新公式如下:

小批量梯度下降法即保证了训练的速度,又能保证最后收敛的准确率,目前的SGD默认是小批量梯度下降算法。
评价:
虽然 小批量梯度下降法 需要走很多步的样子,但是对梯度的要求很低(计算梯度快)。而对于引入噪声,大量的理论和实践工作证明,只要噪声不是特别大,都能很好地收敛。
应用大型数据集时,训练速度很快。比如每次从百万数据样本中,取几百或者几千个数据点,算一个梯度,更新一下模型参数。相比于 批量梯度下降法BGD 的遍历全部样本,每输入一个样本更新一次参数,要快得多。
小批量梯度下降法在选择小批量样本时,同时会引入噪声,使得权值更新的方向不一定正确。
4、python代码
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
python代码中默认执行 Mini-batch Gradient Descent (小批量梯度下降) 。为了保持梯度的稳定性,增加动量 Momentum (动量) ,在代码中加了 momentum=0.9,这在 Mini-batch GD 中尤为重要:因为 Mini-batch 的梯度带有噪声(因为它只是全局梯度的一个估计),更新方向可能会左右摇摆。Momentum 通过累积过去的梯度方向,帮助优化器:
-
- 抑制震荡:在波动剧烈的方向上相互抵消。
- 加速收敛:在一致的方向上加速积累速度。
公式:
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9,weight_decay=weight_decay)
上述公式增加了L2 正则项的梯度,正则项权重衰减系数 (weight_decay 记为 )。
指数加权平均
指数加权平均的核心特点是:越近期的数据,权重越高;越久远的数据,权重越低,并且权重随时间呈指数级衰减。这种方法能够在保留历史信息的同时,更敏感地反映数据的最新变化趋势,有效减少随机噪声的影响。
在时刻,他的移动平均值公式是: ,其中是时刻的移动平均预测值;为时刻的真实值;是权重;对于t时刻 来说,之前的输入, 呈现指数 , 衰减
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

梯度震荡
十分钟搞明白Adam和AdamW,SGD,Momentum,RMSProp,Adam,AdamW_哔哩哔哩_bilibili

图中不同圆圈代表了损失函数的等高线,横坐标w 和纵坐标 b代表了待优化参数,网络的参数很多,有的参数很大,有的参数很小,导致不同参数的梯度抖动不一样,比如图中,参数b的梯度抖动很大,导致梯度震荡下降(蓝线)
有没有什么方法可以减小震荡,因此就需要上述介绍的指数加权平均
Momentum
十分钟搞明白Adam和AdamW,SGD,Momentum,RMSProp,Adam,AdamW_哔哩哔哩_bilibili
momentum算法思想:参数更新时在一定程度上保留之前更新的方向,同时又利用当前batch的梯度微调最终的更新方向,简言之就是通过积累之前的动量来(previous_sum_of_gradient)加速当前的梯度。

RMSprop(root mean square prop)
为了进一步减小梯度结果的抖动,将梯度结果进行归一化,如下

其中 为梯度平方的指数加权平均, 相当于对梯度 进行了归一化,超参数小值 是为了防止除以0。
Adam
融合了Momentum 和RMSprop两种方法,

当前梯度
梯度的指数加权平均
梯度二阶的指数加权平均
为修正结果(由于初始阶段和都被初始化为0,导致在迭代初期和的值会比真实的均值和方差要小。特别是当和都接近于1时,偏差会很大。因此,需要对偏差进行修正。)
代码
代码中带有L2正则项,梯度变为 ( 为weight_decay)
import torch.optim as optim
# --- Adam 优化器 ---
optimizer_adam = optim.Adam(
model.parameters(), # 需要优化的参数
lr=0.001, # 学习率 (默认 1e-3)
betas=(0.9, 0.999), # 动量参数 (beta1, beta2)
eps=1e-8, # 防止除以零的极小值
weight_decay=0, # L2 正则化系数
)
Adamw

由于 Adam 会对梯度进行自适应缩放(除以 ),正则项 也被缩放了。这导致正则项衰减的实际效果依赖于梯度的大小,使得λ变得难以调节,通常达不到预期的正则化效果。
Adamw提出将正则项从梯度计算中解耦出来。如上图公式中
代码
import torch.optim as optim
# --- AdamW 优化器 ---
optimizer_adamw = optim.AdamW(
model.parameters(),
lr=0.001,
betas=(0.9, 0.999),
eps=1e-8,
weight_decay=0.01, # ✅ 解耦的权重衰减系数 (推荐设置 0.01 ~ 0.1)
)
学习率
学习率(Learning Rate, LR) 是深度学习中最关键的超参数之一,它控制着模型在每次参数更新时"走多远"。正确设置学习率对模型能否快速、稳定地收敛至关重要。
👉 学习率决定了我们沿着梯度反方向移动的步长。
🔍 学习率的影响
| 学习率大小 | 影响 | 表现 |
|---|---|---|
| 太大 | 步子迈得太大,可能跳过最优解 | 损失震荡、不收敛、甚至发散(loss → NaN) |
| 太小 | 步子太小,前进缓慢 | 收敛极慢,可能陷入局部最优或鞍点 |
| 适中 | 平稳高效收敛 | 损失稳步下降,最终达到较好性能 |
✅ 理想的学习率:让损失快速下降且不震荡。
📊 常见学习率取值范围(经验性)
| 优化器 | 常见初始学习率 |
|---|---|
| SGD | 0.1, 0.01, 0.001 |
| SGD + Momentum | 0.01 ~ 0.1(常用 0.9 动量) |
| Adam | 0.001(即 1e-3,默认值) |
| AdamW | 5e-4 ~ 3e-4(常用于 Transformer) |
| RMSprop | 0.001 ~ 0.0001 |
💡 注意:这些是 起点,具体任务需调整。
🧪 如何选择合适的学习率?
✅ 方法 1:从默认值开始
- Adam/AdamW:从
3e-4或5e-4开始 - SGD:从
0.1开始(配合学习率衰减)
✅ 方法 2:学习率范围测试(LR Range Test)
- 从小到大线性增加学习率(如
1e-7→1) - 记录 loss 下降速度
- 选择 loss 下降最快时对应的学习率
工具:
- PyTorch:
torch.optim.lr_scheduler.LRScheduler - fastai 提供
lr_find()自动探测
🔁 学习率调度策略(Learning Rate Scheduling)
固定学习率往往不够好,动态调整更有效。
| 策略 | 说明 | 公式/特点 |
|---|---|---|
| Step Decay | 每隔固定 epoch 衰减 | LR *= 0.1 every 30 epochs |
| Exponential Decay | 指数衰减 | LR = LR0 * exp(-kt) |
| Cosine Annealing | 余弦退火,平滑下降 | LR = LR_min + (LR_max - LR_min)*(1+cos(t))/2 |
| Reduce on Plateau | 验证 loss 不降时衰减 | patience=5, factor=0.5 |
| Warmup(预热) | 初始阶段从小学习率开始 | 第 0~5 epoch 从 0 → LR_max |
⭐ Warmup 的作用:
-
防止初期梯度爆炸或震荡
-
让模型先"稳定"下来再加大步长
-
尤其适用于 Transformer 类大模型
scheduler = CosineAnnealingLR(
optimizer,
T_max=num_epochs,
eta_min=0,
last_epoch=-1
)scheduler = OneCycleLR(
optimizer,
max_lr=0.1, # 峰值学习率
steps_per_epoch=steps_per_epoch,
epochs=num_epochs,
pct_start=0.1, # 前 10% 的时间用于 Warm-up (从 0 升到 0.1)
anneal_strategy='cos', # 降温策略用余弦
cycle_momentum=True # 动量也跟随变化 (通常动量与 LR 反向变化)
)
A.StepLR (阶梯式衰减)
原理 :每隔固定的 epoch 数,将学习率乘以一个衰减系数 (通常为 0.1 或 0.5)。
公式:
import torch.optim as optim
import torch.optim.lr_scheduler as lr_scheduler
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
# 每 30 个 epoch,学习率变为原来的 0.1 倍
scheduler = lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(90):
train(...)
validate(...)
# 【关键】每个 epoch 结束后调用,step()中内部计数器+1,记录当前是第几个epoch,即上述公式中t
scheduler.step()
print(f"Epoch {epoch}: LR = {scheduler.get_last_lr()[0]}")
# 输出趋势: 0.1 -> (30 epochs后) 0.01 -> (60 epochs后) 0.001
为方便大家学习 这里给大家整理了一份学习资料包 需要的同学 根据下图自取即可

B. CosineAnnealingLR (余弦退火)
原理 :让学习率随 epoch 呈余弦曲线 下降,从初始值平滑降到最小值(通常为 0 或很小值)。
公式:
适用场景:ResNet, Transformer, 以及需要平滑收敛的任务。这是目前最流行的策略之一。
optimizer = optim.AdamW(model.parameters(), lr=0.001)
# T_max 通常是总 epoch 数
scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=0)
for epoch in range(100):
train(...)
scheduler.step()
# 输出趋势: 0.001 (平滑曲线下降) -> 0.0
为AdamW中指定的lr=0.001, 为eta_min=0, 为T_max=100
C. OneCycleLR (单周期策略)
原理 :先升后降。
- Warmup (升温):从很小的值线性增加到最大值(甚至超过初始设定值)。
- Cooling (降温):从最大值按余弦曲线降到底。
优势 :允许使用更大的最大学习率,具有正则化效果,能跳出局部最优,加速收敛。
适用场景:快速训练、CIFAR/ImageNet 高分复现、Transformer。
optimizer = optim.SGD(model.parameters(), lr=0.01) # 这里的 lr 只是占位,实际由 scheduler 控制
# total_steps 通常是 len(dataloader) * epochs
scheduler = lr_scheduler.OneCycleLR(
optimizer,
max_lr=0.1, # 峰值学习率
steps_per_epoch=len(train_loader),
epochs=10,
pct_start=0.3, # 前 30% 时间升温,后 70% 降温
anneal_strategy='cos' # 降温策略用余弦
)
for epoch in range(10):
for batch in train_loader:
train(batch)
# 【注意】OneCycleLR 通常在每个 batch 后调用,而不是 epoch 后
scheduler.step()
特殊技术:Warmup (预热)
原理 :在训练开始的几个 epoch(或 steps)内,将学习率从 0(或极小值)线性增加到设定值。
原因:
- 初始阶段参数随机,梯度方向不稳定,大学习率会导致模型发散。
- 对于 Adam/AdamW,初始时刻的一阶矩 和二阶矩 估计不准,Warmup 可以给统计量时间趋于稳定。