Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

DL——不可导函数的可导近似

  • 参考链接:
    • 函数光滑化杂谈:不可导函数的可导逼近-科学空间:苏神关于不可导函数逼近可导函数的总结

AUC的近似

  • 参见:《MBA: Mini-Batch AUC Optimization》
  • 详情见:AUC Optimization - Lanzhe Guo的文章 - 知乎
  • 待补充

其他特殊函数-采样

  • 实际上,可以把采样也看做一个不可导函数,采样的可导近似方法一般称为重参数化技巧
  • 采样包含连续型分布采样和离散分布采样,分别有不同的重参数化技巧
  • 离散分布采样的一种重参数化技巧叫做Gumbel softmax trick(其中使用到了argmax函数的可导近似函数softmax)
  • 详情可参考DL——重参数化技巧

DL——为什么Dropout能防止过拟合

  • 参考博客: https://blog.csdn.net/dQCFKyQDXYm3F8rB0/article/details/81976571

关于Dropout

  • 用途是防止过拟合,关于过拟合的讲解可参考
    • DL——深度学习中降低过拟合的方法
    • ML——模型的方差与偏差(机器学习中的正则化与过拟合)
  • 基本思路:
    • 训练时:随机失活部分神经元
    • 推理时:激活全部神经元

定义

  • dropout是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃。注意是暂时 ,对于随机梯度下降来说,由于是随机丢弃,故而每一个mini-batch都在训练不同的网络。(因为每一轮被丢弃的神经元不同)

应用

  • 在CNN中防止过拟合的效果明显
  • 一般选择0.5比较好,因为0.5的时候Dropout随机生成的网络结果最多,但是实际使用中一般需要调节甚至变化
    • 亲测: 在使用VGG16模型迁移学习来分类Dogs2Cats数据集时,先使用0.5,然后再使用0.2略优于一直使用0.5的情况

Why能防止过拟合?

  • 虽然Dropout在实际应用中的确能防止过拟合,但是关于Dropout防止过拟合的原理,大家众说纷纭
  • 下面介绍两个主流的观点

组合派观点

集成学习方法论
  • 传统神经网络的缺点: 费时 , 容易过拟合
  • 过拟合是很多机器学习的通病
  • 一种修改模型的过拟合解决思路是: 采用Ensemble方法的Bagging方法(平均多个模型的结果,从而能够减少模型的方差,同时减轻过拟合)或者Boosting方法(减小模型的偏差,同时能减轻过拟合?[待更新]),即训练多个模型做组合
  • 但是解决了过拟合后, 费时就成为一个大问题,不仅训练起来费时,测试起来多个模型也很费时
  • Dropout能同时解决以上问题:
    • Dropout的示意图如下: 左图是原图结构,右图是加入Dropout层的
    • 从图上可以看出,有了Dropout,训练的模型就可以看成是多个模型的组合,最终预测时丢弃Dropout即可的到所有模型的组合,从而实现类似于Ensemble方法的Bagging方法,实现了多个模型的组合
动机论
  • 虽然直观上看dropout是ensemble在分类性能上的一个近似,然而实际中,dropout毕竟还是在一个神经网络上进行的,只训练出了一套模型参数。那么他到底是因何而有效呢?

  • 首先分析一个小故事

在自然界中,在中大型动物中,一般是有性繁殖,有性繁殖是指后代的基因从父母两方各继承一半。但是从直观上看,似乎无性繁殖更加合理,因为无性繁殖可以保留大段大段的优秀基因。而有性繁殖则将基因随机拆了又拆,破坏了大段基因的联合适应性。但是自然选择中毕竟没有选择无性繁殖,而选择了有性繁殖,须知物竞天择,适者生存。我们先做一个假设,那就是基因的力量在于混合的能力而非单个基因的能力。不管是有性繁殖还是无性繁殖都得遵循这个假设。为了证明有性繁殖的强大,我们先看一个概率学小知识

  • 基本思想: 有性繁殖的方式不仅仅可以将优秀的基因传下来,还可以降低基因之间的联合适应性,使得复杂的大段大段基因联合适应性变成比较小的一个一个小段基因的联合适应性

  • dropout也能达到同样的效果,它强迫一个神经单元,和随机挑选出来的其他神经单元共同工作,达到好的效果。消除减弱了神经元节点间的联合适应性,增强了泛化能力

噪声派观点

  • 对于每一个dropout后的网络,进行训练时,相当于做了Data Augmentation ,因为,总可以找到一个样本,使得在原始的网络上也能达到dropout单元后的效果。 比如,对于某一层,dropout一些单元后,形成的结果是(1.5,0,2.5,0,1,2,0),其中0是被drop的单元,那么总能找到一个样本(新样本),使得结果也是如此。这样,每一次dropout其实都相当于增加了样本
噪声派观点总结
  • 将dropout映射回得样本训练一个完整的网络,可以达到dropout的效果
  • dropout由固定值变为一个区间,可以提高效果
  • 将dropout后的表示映射回输入空间时,并不能找到一个样本 x 使得所有层都能满足dropout的结果,但可以为每一层都找到一个样本,这样,对于每一个dropout,都可以找到一组样本可以模拟结果

Dropout其他需要注意的点

  • 数据量小的时候,dropout效果不好,数据量大了,dropout效果好
  • dropout的缺点就在于训练时间是没有dropout网络的2-3倍

Dropout训练和预测阶段输出分布一致性保证

  • 保持分布一致的必要性 :Dropout操作会改变神经元输出分布,在 Dropout 的训练和预测阶段保证输出均值一致,能使得模型在训练和预测时具有相同的统计特性,从而提高模型的泛化能力
  • 方式 :通过调整训练和预测阶段的神经元输出比例,可以保证确保两者输出分布的一致性实现方式
  • 方法一:训练阶段缩放(Inverted Dropout)
    • 训练阶段:以概率 \(p\) 失活部分神经元,同时对被激活的所有神经元,对输出均乘以 \(\frac{1}{1-p}\)
    • 预测阶段:在预测阶段,不进行神经元的随机丢弃,而是直接使用训练好的模型进行推理。此时,由于在训练阶段已经对神经元的输出进行了缩放,所以在预测阶段不需要额外的操作,模型的输出均值就会与训练阶段保持一致
  • 方法二:预测阶段缩放(Vanilla Dropout)
    • 训练阶段:以概率 \(p\) 失活部分神经元,不调整输出
    • 预测阶段:激活所有神经元,且对被激活的所有神经元,对输出均乘以 \(1-p\)
  • 注:常规的Dropout实现均保证了训练和推理阶段分布一致,测试代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import torch
    import torch.nn as nn

    x = torch.randn(8)
    dropout = nn.Dropout(p=0.5)

    dropout.train()
    train_output = dropout(x)
    print("训练阶段输出:", train_output)

    dropout.eval()
    test_output = dropout(x)
    print("预测阶段输出:", test_output)

    # 训练阶段输出: tensor([ 0.0000, -1.0198, -1.4742, -0.0408, 1.8699, 0.0000, -0.0000, -0.0000])
    # 预测阶段输出: tensor([ 0.1318, -0.5099, -0.7371, -0.0204, 0.9350, 0.9006, -0.2567, -0.7391])

DL——各种梯度下降相关的优化算法

本文从梯度下降(Gradient Descent, GD)开始,讲述深度学习中的各种优化算法(优化器,Optimizer)

参考文章:【干货】深度学习必备:随机梯度下降(SGD)优化算法及可视化


三种梯度下降框架

随机梯度下降(Stochastic Gradient Descent, SGD)

核心思想
  • 每次从随机从训练集中选择一个训练样本来计算误差,进而更新模型参数
  • 单次迭代时参数移动方向可能不太精确甚至相反,但是最终会收敛
  • 单次迭代的波动也带来了一个好处,可以到达一个更好的局部最优点,甚至到达全局最优点
参数更新公式
  • 公式: \(\theta=\theta-\lambda\frac{\partial L(\theta;x_{i};y_{i})}{\partial \theta}\)
  • 其中: \(L(\theta;x_{i};y_{i})=L(f(\theta;x_{i}),y_{i})\)

批量梯度下降(Batch Gradient Descent, BGD)

核心思想
  • 每次使用全量的训练集样本(假设共 m 个)来计算误差,进而更新模型参数
  • 每次参数能够朝着正确的方向移动
  • 每次遍历所有数据,耗费时间较长
参数更新公式
  • 公式:\(\theta=\theta-\lambda\frac{\partial L(\theta;x_{1:m};y_{1:m})}{\partial \theta}\)
  • 一般来说:\(L(\theta;x_{1:m};y_{1:m}) = \frac{1}{m}\sum_{i=1}^{m} L(\theta;x_{i};y_{i})\)

小批量梯度下降(Mini-Batch Gradient Descent, MBGD)

核心思想
  • 每次从随机从训练集中选择k(k < m)个训练样本来计算误差,进而更新模型参数
  • 介于SGD和BGD之间
    • 波动小
    • 内存占用也相对较小
参数更新公式
  • 公式: \(\theta=\theta-\lambda\frac{\partial L(\theta;x_{i:i+k};y_{i:i+k})}{\partial \theta}\)
  • 一般来说: \(L(\theta;x_{1:k};y_{1:k}) = \frac{1}{k}\sum_{i=1}^{k} L(\theta;x_{i};y_{i})\)

总结

优点
  • 梯度下降算法应用广泛,算法效果很好
缺点
学习速率
  • 大小很难确定,太大容易震荡,太小则收敛太慢
  • 学习速率一般为定值,有时候会实现为逐步衰减
  • 但是无论如何,都需要事前固定一个值,因此无法自适应不同的数据集特点
局部最优
  • 对于非凸的目标函数,容易陷入局部极值点中
  • 比局部极值点更严重的问题:有时候会嵌入鞍点?

SGD 算法的优化

Momentum法(动量法)

核心思想
  • 考虑一种情况,在峡谷地区(某些方向比另一些方向陡峭很多)
    • SGD (或者 MBGD,实际上,SGD 是特殊的 MBGD,平时可以认为这两者是相同的东西)会在这些放附近振荡,从而导致收敛速度变慢
    • 这里最好的例子是鞍点,鞍点出的形状像一个马鞍,一个方向两头上翘,一个方向两头下垂,当上翘的方向比下垂的方向陡峭很多时,SDG和MDG等方法容易在上翘方向上震荡
  • 此时动量可以使得
    • 当前梯度方向与上一次梯度方向相同的地方进行加强,从而加快收敛速度
    • 当前梯度方向与上一次梯度方向不同的地方进行削减,从而减少振荡
  • 动量可以理解为一个从山顶滚下的小球,遇到新的力(当前梯度)时,会结合之前的梯度方向决定接下来的运动方向
参数更新公式
  • 公式:\(\theta=\theta-m_{t}\)
    • \(m_{t}\) 表示当前下降方向, \(m_{t-1}\) 表示上一次的下降方向
    • \(m_{t}=\gamma m_{t-1}+\lambda\frac{\partial L(\theta;x_{i};y_{i})}{\partial \theta}\)
    • \(\gamma<1\),值一般取0.9
    • \(\gamma m_{t-1}\) 是动量项
    • \(\gamma\) 是衰减量
    • \(\lambda\) 是学习率
图示
  • 动量相关图示:
小结
  • 学习过程
    • 从训练集中的随机抽取一批容量为m的样本 \({x_{1},…,x_{m}}\),以及相关的输出 \({y_{1},…,y_{m}}\)
    • 计算梯度和误差,更新v和参数 \(\theta\)

NAG,涅斯捷罗夫梯度加速法(Nesterov Accelerated Gradient)

核心思想
  • 继续考虑普通的 SDG 算法,添加了 Momentum,此时从山顶滚下的球会盲目的选择斜坡
  • 更好的方式是在遇到向上的斜坡时减慢速度
  • NAG在计算梯度时首先获取(近似获得)未来的参数而不是当前参数,然后计算未来参数对应的损失函数的梯度
  • NAG在预测了未来的梯度后,根据未来(\(\theta - \gamma m_{t-1}\))梯度方向和之前梯度的方向决定当前的方向, 这样可以保证在遇到下一点为上升斜坡时适当减慢当前点的速度(否则可能由于惯性走上斜坡, 提前知道 \(\theta - \gamma m_{t-1}\) 处的梯度, 从而保证不要走上去), 从而找到了比Momentum超前的更新方向
  • 对比: Momentum是根据当前梯度方向和之前梯度方向决定当前的方向
参数更新公式
  • 公式:\(\theta=\theta-m_{t}\)
    • \(m_{t}=\gamma m_{t-1}+\lambda\frac{\partial L(\theta - \gamma v_{t-1};x_{i};y_{i})}{\partial \theta}\)
    • NAG使用的是未来的梯度方向(Momentum使用的是当前梯度方向)和之前的梯度方向
图示
  • Momentum(动量)法首先计算当前的梯度值(小蓝色向量),然后在更新的积累向量(大蓝色向量)方向前进一大步
  • NAG 法则首先(试探性地)在之前积累的梯度方向(棕色向量)前进一大步,再根据当前地情况修正,以得到最终的前进方向(绿色向量)
  • 这种基于预测的更新方法,使我们避免过快地前进,并提高了算法地响应能力(responsiveness),大大改进了 RNN 在一些任务上的表现
    • 公式中 \(-\gamma m_{t-1}\) 对应BC向量
    • \(\theta-\gamma m_{t-1}\) 就对应C点(参数)
小结
  • Momentum和NAG法可以使得参数更新过程中根据随时函数的斜率自适应的学习,从而加速SGD的收敛
  • 实际应用中,NAG将比Momentum收敛快很多
  • 学习过程
    • 从训练集中的随机抽取一批容量为m的样本 \({x_{1},…,x_{m}}\),以及相关的输出 \({y_{1},…,y_{m}}\)
    • 计算梯度和误差,更新 v 和参数 \(\theta\)

Adagrad

核心思想
  • 对于较少出现的特征,使用较大的学习率更新,即对低频的参数给予更大的更新
  • 对于较多出现的特征,使用较小的学习率更新,即对高频的参数给予更小的更新
  • 很适合处理稀疏数据
参数更新公式
  • 计算梯度
    • 分量形式: \(g_{t,k} = \frac{\partial L(\theta;x_{i};y_{i})}{\theta}|_{\theta = \theta_{t-1,k}}\)
      • \(g_{t,k}\) 是指第t次迭代时第k个参数 \(\theta_{t-1, k}\) 的梯度
      • 有些地方会这样表达: \(g_{t,k} = \frac{\partial L(\theta_{t-1,k};x_{i};y_{i})}{\theta_{t-1,k}}\)
        • 式子中使用 \(\theta_{t-1, k}\) 在梯度中,事实上不够严谨, 容易让人误解分子分母都不是函数,而是一个确定的值, 事实上我们是先求了导数然后再带入 \(\theta = \theta_{t-1}\) 的
    • 向量形式: \(g_{t} = \frac{\partial L(\theta;x_{i};y_{i})}{\partial \theta}|_{\theta=\theta_{t-1}}\)
  • 此时普通的SGD如下更新参数
    • 分量形式: \(\theta_{t,k} = \theta_{t-1,k} - \lambda g_{t,k}\)
    • 向量形式: \(\theta_{t} = \theta_{t-1} - \lambda g_{t}\)
  • 而Adagrad对学习率 \(\lambda\) 根据不同参数进行了修正
    • 分量形式: \(\theta_{t,k} = \theta_{t-1,k} - \frac{\lambda}{\sqrt{G_{t,kk}+\epsilon}} g_{t,k}\)
      • \(G_{t,kk}=\sum_{r=1}^{t}(g_{r,k})^{2}\)
    • 向量形式: \(\theta_{t} = \theta_{t-1} - \frac{\lambda}{\sqrt{G_{t}+\epsilon}}\bigodot g_{t}\)
      • \(G_{t}=\sum_{r=1}^{t}g_{r}\bigodot g_{r}\)
      • \(\bigodot\) 表示按照对角线上的值与对应梯度相乘
      • 进一步可以简化写为: \(G_t = G_{t-1} + g_t^2\)
        • 注意: 这里 \(g_t^2\) 是指向量按照维度分别相乘, 计算后还是原始向量维度
    • G是一个对角矩阵,对角线上的元素(\(G_{k,k}\))是从一开始到k次迭代目标函数对于参数(\(\theta_{k}\))的梯度的平方和
      • G的累计效果保证了出现次数多的参数(\(\theta_{k}\))对应的对角线上的元素(\(G_{k,k}\))大,从而得到更小的更新
    • \(\epsilon\) 是一个平滑项,用于防止分母为0
  • 总结参数更新公式:
    • \(\theta_{t} = \theta_{t-1} - \frac{\lambda}{\sqrt{G_{t}+\epsilon}} g_{t}\)
    • \(g_{t} = \frac{\partial L(\theta;x_{i};y_{i})}{\partial \theta }|_{\theta = \theta_{t-1}}\)
    • \(G_t = G_{t-1} + g_t^2\)
小结
  • 在分母上累计了平方梯度和 ,造成训练过程中G的对角线元素越来越大 ,最终导致学习率非常小 ,甚至是无限小的值,从而学不到东西
  • 学习过程
    • 从训练集中的随机抽取一批容量为m的样本 \({x_{1},…,x_{m}}\),以及相关的输出 \({y_{1},…,y_{m}}\)
    • 计算梯度和误差,更新G的每个元素,再根据G以及梯度计算参数更新量

Adadelta

核心思想
  • 是Adagrad的一个扩展,目标是解决Adagrad学习率单调下降的问题
  • 解决方案:只累计一段时间内的平方梯度值?
  • 实际上实现是累加时给前面的平方梯度和一个衰减值
  • 方法名delta的来源是选取部分
参数更新公式
  • 将矩阵G的每一项变成当前梯度平方加上过去梯度平方的衰减值(指数衰减)即可
    • 指数衰减:前n-1项的系数是衰减率的n-1次方
    • 实现指数衰减
    • 在Adagrad的基础上修改为: \(G_t = \gamma G_{t-1} + (1-\gamma)g_t^2\)
      • 注意: 这里 \(g_t^2\) 是指向量按照维度分别相乘, 计算后还是原始向量维度
    • 我们通常也把 \(G_t\) 表达为 \(E[g^2]_t\)
      • 因为修改后的 \(G_t\) 可以视为于对 \(g_t^2\) 求期望(不同的 \(t\) 概率权重不一样的分布的期望)
      • 进一步表达为: \(E[g^2]_t = \gamma E[g^2]_{t-1} + (1-\gamma)g_t^2\)
小结
  • 经过衰减后,G 的每一项(忽略掉平滑项 \(\epsilon\))相当于有权重的梯度均方差(Root Mean Square, RMS),后面RMSprop算法就用了这个RMS来命名
    • 均方根的定义是:对所有数求平方和,取平均值(每一项的权重根据概率分布可以不同),再开方
  • 学习过程
    • 从训练集中的随机抽取一批容量为m的样本 \({x_{1},…,x_{m}}\),以及相关的输出 \({y_{1},…,y_{m}}\)
    • 计算梯度和误差,更新G的每个元素,再根据G以及梯度计算参数更新量

RMSprop(Root Mean Square prop)

核心思想
  • 一种适应性学习率方法,至今未公开发表
  • 是Adagrad的一个扩展,目标也是解决Adagrad学习率单调下降的问题
  • RMS的来源是由于分母相当于(忽略掉平滑项 \(\epsilon\))是梯度的均方根(Root Mean Squared, RMS)
参数更新公式
  • 参见Adadelta
  • RMSprop的本质是对Adadelta简单的取之前值和当前值的权重为 0.9 和 0.1 实现指数加权平均, 即 \(\gamma = 0.9\)
  • 有些地方也说RMSprop权重取的是0.5和0.5实现指数加权平均即 \(\gamma = 0.5\)
  • 学习率 \(\lambda\) 一般取值为0.001
小结
  • RMSprop是Adadelta的一种特殊形式
  • Adagrad的分母不能算是均方差(即使忽略平滑项 \(\epsilon\)),因为这里没有取平均值的操作
  • 学习过程
    • 从训练集中的随机抽取一批容量为m的样本 \({x_{1},…,x_{m}}\),以及相关的输出 \({y_{1},…,y_{m}}\)
    • 计算梯度和误差,更新 G 的每个元素,再根据G以及梯度计算参数更新量

Adam(Adaptive Moment Estimation)

核心思想
  • 一种适应性学习率方法,相当于 RMSprop + Momentum + Bias Correction
  • 像 Adadelta 和 RMSprop 一样存储了梯度的平方的指数衰减平均值
  • 像 Momentum 一样保持了过去梯度的指数衰减平均值
  • Bias Correction 是为了得到期望的无偏估计
参数更新公式
  • \(\theta_{t} = \theta_{t-1} - \frac{\lambda}{\sqrt{\tilde{v}_t+\epsilon}} \tilde{m}_t\)
  • \(\tilde{v}_t=\frac{v_{t}}{1-\beta_{1}^{t}}\)
  • \(\tilde{m}_t=\frac{m_{t}}{1-\beta_{2}^{t}}\)
  • \(\lambda\) 是外层学习率,实际使用中,常常可以通过指数衰减、固定步长衰减、余弦退火衰减等学习率衰减策略更新
  • 梯度的指数衰减: \(m_{t} = \beta_{2}m_{t-1}+(1-\beta_{2})g_{t}\)
  • 梯度平方的指数衰减: \(v_{t} = \beta_{1}v_{t-1}+(1-\beta_{1})g_{t}^{2}\)
    • \(m_t\) 和 \(v_t\) 也叫作一阶动量和二阶动量,是对梯度一阶矩估计和二阶矩估计
      • 数学定义:随机变量的一阶矩是随机变量的期望 \(E[X]\),二阶矩是随机变量的方差 \(E[X-E[X]]\)
      • 其实梯度平方的期望不是梯度的方差,这只是一种近似,数学上,随机变量 \(X\) 二阶矩等价于方差,是 \(E[(X-E[X])^2] = E[X^2]-E[X]^2\),当 \(E[X]=0\) 时, \(E[X^2]\) 就是方差
      • 这种滑动平均之所以能代表期望,是因为滑动平均的思想是一种折扣平均,确实可以用来作为期望和方差的估计
    • \(m_t\) 和 \(v_t\) 可以看做是对 \(E[g]_t\) 和 \(E[g^2]_t\) 的估计
    • \(\tilde{m}_t\) 和 \(\tilde{v}_t\) 是对 \(m_t\) 和 \(v_t\) 的 Bias Correction , 这样可以近似为对对期望 \(E[g]_t\) 和 \(E[g^2]_t\) 的无偏估计
      • 注意:修正项 \(\tilde{v}_t=\frac{v_{t}}{1-\beta_{1}^{t}}\) 中的 \(\beta_{1}^{t}\) 是 \(\beta_{1}\) 的 \(t\) 次方的意思,基本思路可以理解为在每一步都尽量将梯度修正到 \(t=0\) 大小
      • 进行修正的原因是当 \(t\) 较小时, \(v_t\) 也较小,而 \(\beta\) 一般较大(0.9或者0.999),此时加权平均的结果也会很小,当 \(t\) 很大时,实际上可以不用修正了,个人理解:应该可以不用修正,只是前期训练时更新速度比较慢而已
小结
  • 超参数设定推荐
    • 梯度平方衰减率: \(\beta_{1}=0.999\)
    • 梯度动量衰减率: \(\beta_{2}=0.9\)
    • 平滑项: \(\epsilon=10e^-8=1*10^{-8}\)
    • 一阶动量 \(v\),初始化为0
    • 二阶动量 \(m\),初始化为0
  • 学习过程
    • 从训练集中的随机抽取一批容量为m的样本 \({x_{1},…,x_{m}}\),以及相关的输出 \({y_{1},…,y_{m}}\)
    • 计算梯度和误差,更新 \(v\) 和 \(m\),再根据 \(v\) 和 \(m\) 以及梯度计算参数更新量

AdamW

Adam with Weight decay是Adam的一种优化

Adam中的L2正则
  • 一般的L2正则
    $$
    Loss(w) = f(w) + \frac{1}{2}\eta||w||^2
    $$
  • 权重衰减后的参数更新如下
    $$
    \begin{align}
    w &= w - \alpha\nabla Loss(w) \\
    &= w - \alpha (\nabla f(w) + \eta w) \\
    &= w - \alpha \nabla f(w) - \alpha \eta w \\
    \end{align}
    $$
  • 由于L2正则化项的存在,每次权重更新时都会减去一定比例的权重,即 \(\alpha \eta w \),这种现象叫做权重衰减(L2正则的目标就是让权重往小的方向更新,所以L2正则也叫作权重衰减)
  • L2正则也称为权重衰减,所以Adam优化的损失函数中添加L2正则的目标本应该也是为了权重衰减
  • Adam中的L2正则
    • 在每次求损失函数梯度前都计算 \(\nabla Loss(w) = \nabla f(w) + \eta w\)
    • 由于L2正则项的梯度 \(\eta w\) 也会被累加到一阶动量和二阶动量中,带有L2的Adam不再是简单的权重衰减,L2正则项还会影响到其他值的更新
    • Adam中的L2正则会产生我们不期望的结果,因为此时L2正则项影响了Adam参数的正常更新(我们想要L2做的仅仅是权重衰减,但在Adam中,L2产生了别的影响,这个不是我们想要的)
AdamW——Adam+权重衰减
  • AdamW则不直接将L2添加到损失函数中,而是显示的把权重衰减提出来,主要修改是下面两步
    • 在计算梯度时,将L2正则从损失函数中去除
    • 在更新参数时,显示增加权重衰减项
  • 相当于在更新参数时增加了L2正则,但是计算梯度时没有L2正则
  • 原始论文:Decoupled Weight Decay Regularization
    • 图中紫色是原始Adam+L2实现部分,在AdamW中会被去除;
    • 绿色是AdamW中新增的权重衰减部分(相当于更新参数时增加了L2正则项)
  • 参考链接:Adam和AdamW,从梯度下降到AdamW一文读懂机器学习优化算法
  • 目前大模型常用的就是AdamW

优化器与内存/显存

  • 训练的过程中,需要的内存/显存大小与优化器(Optimizer)有关
    • 需要存储到内存的变量包括以下几个方面
      • 梯度
      • 参数
      • 优化器状态(Optimizer States),普通 SGD 没有这一项,而Adam和AdamW则需要存储一阶动量和二阶动量
  • 优化器、参数量、内存/显存消耗、混合精度训练相关概念可参考ZeRO: Memory Optimizations Toward Training Trillion Parameter Models
    • 有些论文中也会直接将二阶动量叫做方差(Variance)或者二阶矩,因为二阶动量可以近似方差(当期望为0时)
  • ZeRO论文中指出,在混合精度训练 + Adam/AdamW 时,需要存储的变量包括
    • FP16 的参数
    • FP16 的梯度
    • FP32 的参数
    • FP32 的一阶动量
    • FP32 的二阶动量
    • 注意:动量不能使用 FP16 吗?是的,不能,因为为了精度考虑使用时还是要被转换到 FP32

各种优化方法的比较

鞍点

  • SGD optimization on saddle point

等高线表面

  • SGD optimization on loss surface contours
  • 上面两种情况都可以看出,Adagrad, Adadelta, RMSprop 几乎很快就找到了正确的方向并前进,收敛速度也相当快,而其它方法要么很慢,要么走了很多弯路才找到
  • 由图可知自适应学习率方法即 Adagrad, Adadelta, RMSprop, Adam 在这种情景下会更合适而且收敛性更好

如何选择

  • 如果数据是稀疏的,就用自适用方法,即 Adagrad, Adadelta, RMSprop, Adam
    • 因为他们能够为出现更新次数少(确切的说是梯度累计结果小)的特征分配更高的权重
  • RMSprop, Adadelta, Adam 在很多情况下的效果是相似的
  • Adam 可解释为 RMSprop + Momentum + Bias Correction
  • 随着梯度变的稀疏,Adam 比 RMSprop 效果会好
  • 整体来讲,Adam 是最好的选择
  • 很多论文里都会用 SGD,没有 momentum 等, SGD 虽然能达到极小值,但是比其它算法用的时间长,而且可能会被困在鞍点, 在不正确的方向上来回震荡
  • 如果需要更快的收敛,或者是训练更深更复杂的神经网络,需要用一种自适应的算法

epoch 粒度的学习率调整

  • 本文中提到的常规的梯度下降方法中学习率均是在以 batch 为单位变化,即同一个 batch 学习率相同,而不同 batch 学习率可能不同
  • 深度学习中还有一类学习率调整方法,是以 epoch 为单位变化的,即同一个 epoch 学习率相同,而不同 epoch 学习率可能不同
  • 常见的 epoch 为单位变化的学习率有:
    • StepLR:间隔调整学习率
    • MultiStepLR:按设定的间隔调整学习率
    • ExponentialLR︰按指数衰减调整学习率
    • CosineAnnealingLR:以余弦函数为周期,在每个周期最大值时重置学习率
    • CosineAnnealingWarmRestarts:CosineAnnealingLR 加上 warmup
    • ReduceLROnPlateau:当某指标不再变化(下降或升高),调整学习率
  • 如果以 epoch 为单位的学习率(如 StepLR)和以 batch 为单位的学习率(如 Adam)调整同时被设置,则:
    • 两者会同时生效
    • StepLR 负责在每个 epoch开始时调整学习率
    • Adam 负责在每个 batch 开始时调整学习率

附录:关于 Adam 的更多讨论

  • 参考链接:
    • 如何理解Adam算法(Adaptive Moment Estimation)? - Summer Clover的回答 - 知乎
    • 梯度下降法的神经网络容易收敛到局部最优,为什么应用广泛? - Summer Clover的回答 - 知乎

      (1)正因为梯度下降法 容易收敛到局部最优,所以大家几乎从来不用梯度下降做非凸优化,包括 训练神经网络
      (2)正因为随机梯度下降法 容易逃离鞍点 和泛化不好的 minima(主要是 sharp minima),所以 随机梯度下降(SGD)和它的变种(比如 Momentun、Adam)才是训练神经网络最流行的方法

    • Who is Adam? 重新审视大模型 RLVR 阶段的优化器选择

Adam vs SGD 的优缺点简单总结

  • Adam 更容易逃离鞍点
  • Adam 对学习率超参数不敏感,一般在一个范围内的学习率即可实现最优,无需精确对学习率调参;但 SGD 对超参数非常敏感,不同场景下的最优超参数变化剧烈
  • Adam 收敛更快
  • Adam 需要额外存储两倍参数量的优化器状态,这对超大规模模型来说成本非常高

大模型中的 Adam 和 SGD 对比

  • 参考链接:Who is Adam? 重新审视大模型 RLVR 阶段的优化器选择
  • 注意:这里的讨论仅限于 RLVR 这种不需要更新太多参数的场景
  • TLDR:大模型中,在做 RLVR 时,由于 RLVR 任务需要修改变化的参数较少(以 1e-5 变化量为阈值,大概 0.01% 量级参数发生变化),所以使用不带 Momentum 的 SGD 优化基本就够了,能省下来 Adam 下的很多优化器状态显存占用
  • 在 RLVR 下 SGD vs Adam:
    • SGD 审下来很多优化器状态,且效果和 Adam 差不多
    • 使用 SGD 时,需要使用较大的学习率(Adam 1e-6 ~ 1e-5 是,SGD 需要 0.1 左右),且 SDG 对学习率超参敏感

DL——深度学习中参数的初始化策略


整体说明

  • 在深度学习中,权重/参数初始化策略对模型的训练效率和性能有着至关重要的影响
  • 合适的初始化方法能够缓解梯度消失或梯度爆炸问题,加速收敛 ,并提高模型的泛化能力

随机初始化(Random Initialization)

  • 使用小的随机值初始化参数,可以打破 0 初始化带来的对称性
  • torch 生成随机参数的函数有很多,常见的有:
    • torch.randn():从标准正态分布采样
    • torch.rand():从均匀分布采样

Xavier初始化(Glorot初始化)

  • 目标是保持输入和输出的方差一致 ,缓解梯度消失/爆炸
  • 适用于激活函数为 Sigmoid 或 Tanh 的网络(这两种激活函数容易导致梯度消失等问题)
  • Xavier初始化的公式如下:
    • 均匀分布:
      $$w \sim U\left[-\frac{\sqrt{6} }{\sqrt{n_{in} + n_{out} } }, \frac{\sqrt{6} }{\sqrt{n_{in} + n_{out} } }\right]$$
    • 正态分布:
      $$w \sim \mathcal{N}\left(0, \sqrt{\frac{2}{n_{in} + n_{out} } }\right)$$
  • Xavier初始化的PyTorch实现 :
    1
    2
    3
    4
    import torch.nn as nn
    linear_layer = nn.Linear(3, 4)
    nn.init.xavier_uniform_(linear_layer.weight) # 均匀分布
    nn.init.xavier_normal_(linear_layer.weight) # 正态分布

He初始化(Kaiming初始化)

  • He初始化是专为 ReLU 激活函数设计,可保持每一层的方差一致

  • 适用于 ReLU 及其变种(LeakyReLU、PReLU 等)网络

  • He初始化的公式 :

    • 均匀分布公式:
      $$w \sim U\left[-\sqrt{\frac{6}{n_{in} } }, \sqrt{\frac{6}{n_{in} } }\right]$$
    • 正态分布公式:
      $$w \sim \mathcal{N}\left(0, \sqrt{\frac{2}{n_{in} } }\right)$$
  • He初始化的PyTorch实现 :

    1
    2
    3
    4
    import torch.nn as nn
    linear_layer = nn.Linear(3, 4)
    nn.init.kaiming_uniform_(linear_layer.weight, mode='fan_in', nonlinearity='relu') # 均匀分布
    nn.init.kaiming_normal_(linear_layer.weight, mode='fan_in', nonlinearity='relu') # 正态分布
    • mode:’fan_in’(保持输入方差)或’fan_out’(保持输出方差)
    • nonlinearity:激活函数类型(如’relu’或’leaky_relu’)

预训练初始化

  • 在模型越来越大的今天,常常使用在大规模数据集上预训练的模型参数初始化当前模型

  • 预训练初始化适用于迁移学习场景

  • 从远程下载并加载 ResNet-18 模型(17 个卷积层 + 1 个全连接层)的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import torchvision.models as models
    # 加载预训练的ResNet模型(`pretrained=True`时,会从远程下载并加载预训练好的参数)
    resnet = models.resnet18(pretrained=True)

    # 使用预训练参数初始化自定义模型
    class CustomModel(nn.Module):
    def__init__(self):
    super().__init__()
    self.features = nn.Sequential(*list(resnet.children())[:-1])
    self.classifier = nn.Linear(512, 10) # 自定义分类层
    • 当 pretrained=True 时,PyTorch 会自动下载并加载远程预训练权重参数
      • 这里的 ResNet-18 模型是基于 ImageNet 数据集(包含 1000 个类别、1400 万张图像)训练的
    • 这些预训练权重已学习到通用的图像特征,可用于多种下游计算机视觉任务

正交初始化(Orthogonal Initialization)

  • 正交初始化确保权重矩阵正交,有效缓解梯度消失/爆炸

  • 常常适用于循环神经网络(RNN、LSTM、GRU)中,强化学习中(如 PPO)网络的初始化也常用

  • 正交初始化的 PyTorch 实现:

    1
    nn.init.orthogonal_(weight)
  • 对于简单网络(如浅层 MLP),正交初始化的优势可能不明显,有时标准正态分布或 Xavier初始化 也能满足需求


附录:如何选择初始化策略?

  • Xavier初始化(Glorot初始化) :适用于使用 Sigmoid/Tanh 激活函数的网络
  • He初始化(Kaiming初始化) :适用于使用 ReLU 激活函数的网络
  • 正交初始化 :循环神经网络(RNN/LSTM/GRU)或强化学习网络中
  • 预训练初始化 :迁移学习场景,复用之前训练好的参数
  • 最后:常用配置 :一般使用 He初始化 或 Xavier初始化 ,强化学习用 正交初始化

附录:为什么参数不能初始化为全0?

  • 零初始化(Zero Initialization)会导致同一隐藏层的神经元互相对称,可以通过递推法证明,不管迭代多少次,此时所有的神经元都将计算完全相同的函数
  • 并不会因为参数都为0就导致所有神经元死亡!
  • 注:要特别注意,除了一些特殊的场景外,不推荐全初始化为0
    • 比如 LoRA 的两个网络,其中一个初始为0更好(注意,另一个也不能为0)

附录:为什么参数不能初始化为太大的数值?

  • 因为参数太大会导致sigmoid(z)或tanh(z)中的z太大,从而导致梯度太小而更新太慢
  • 如果网络中完全没有sigmoid和tanh等激活函数,那就还好,但是要注意,二分类中使用sigmoid函数于输出层时也不应该将参数初始化太大

DL——深度学习中降低过拟合的方法

添加Dropout

  • 详情可参考: DL——为什么Dropout能防止过拟合

参数范书惩罚

相关参数: Weight decay(权重衰减)
添加L2或L1正则化, 详情可参考: ML——模型的方差与偏差

  • 参考文档:

    • L1正则化与L2正则化的详细讲解(L1具有稀疏性,L2让参数更小):L1正则化和L2正则化
    • L1具有稀疏性的证明:L1正则为什么更容易获得稀疏解
      • 求导后可知,在0点附近,权重大于0和小于0会产生正负不同的梯度值(当原始损失函数关于当前权重在0点的偏导绝对值小于正则化权重时,整体梯度基本由正则化梯度主导),从而使得参数倾向于走到0点
  • L1正则化:

    • L1又称为: Lasso Regularization(稀疏规则算子)
    • 计算公式为: 参数绝对值求和
    • 意义: 趋向于让一些参数为0, 可以起到特征选择的作用
  • L2正则化:

    • L2又称为: Ridge Regression(岭回归)
    • Weight decay 是放在正则项(Regularization)前面的一个系数,正则项一般指模型的复杂度
    • Weight decay 控制模型复杂度对损失函数的影响, 若Weight Decay很大,则模型的损失函数值也就大
    • pytorch中实现了L2正则化,也叫做权重衰减,具体实现是在优化器中,参数是 weight_decay, 默认为0
  • PyTorch中的weight_decay参数说明

weight_decay (float, optional): weight decay (L2 penalty) (default: 0)

  • 我之前的实现代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # zero the parameter gradients
    optimizer.zero_grad()
    # forward
    outputs = model(inputs)
    # _, preds = torch.max(outputs.data, 1)
    loss = loss_criterion(outputs, labels)

    # L1 regularization
    l1_loss = 0
    for w in model.parameters():
    l1_loss += torch.sum(torch.abs(w))
    loss += l1_rate * l1_loss

    # backward + optimize only if in training phase
    if phase == 'train':
    loss.backward()
    optimizer.step()
    • 其中 # L1 regularization后面是添加的L1 正则化
  • 就整体而言,对比加入正则化和未加入正则化的模型,训练输出的loss和Accuracy信息,我们可以发现,加入正则化后,loss下降的速度会变慢,准确率Accuracy的上升速度会变慢,并且未加入正则化模型的loss和Accuracy的浮动比较大(或者方差比较大),而加入正则化的模型训练loss和Accuracy,表现的比较平滑。并且随着正则化的权重lambda越大,表现的更加平滑。这其实就是正则化的对模型的惩罚作用,通过正则化可以使得模型表现的更加平滑,即通过正则化可以有效解决模型过拟合的问题

数据增强

  • 提高模型的泛化能力最好的办法是, 使用更多的训练数据进行训练
  • 创造一些假数据添加到训练集中
  • 实例:
    • AlexNet中使用对图片旋转等方式生成新的图片作为样本加入训练, 误差能降低1%

提前终止训练

  • 当发现数据在验证集上的损失趋于收敛甚至开始增加时,停止训练
  • 即使模型在验证集上的损失还在减小

参数绑定与参数共享

Soft Weight Sharing

  • 类似于CNN中卷积层的权重共享方法
  • RNN中也有权重共享, 整条时间链上的参数共享

Bagging

  • 其实bagging的方法是可以起到正则化的作用,因为正则化就是要减少泛化误差,而bagging的方法可以组合多个模型起到减少泛化误差的作用
  • 在深度学习中同样可以使用此方法,但是其会增加计算和存储的成本
    • 这一点在Kaggle比赛中有用过,的确有很大提高

Batch Normalization

  • 在Google Inception V2中所采用,是一种非常有用的正则化方法,可以让大型的卷积网络训练速度加快很多倍,同事收敛后分类的准确率也可以大幅度的提高.
  • N在训练某层时,会对每一个mini-batch数据进行标准化(normalization)处理,使输出规范到N(0,1)的正太分布,减少了Internal convariate shift(内部神经元分布的改变),传统的深度神经网络在训练是,每一层的输入的分布都在改变,因此训练困难,只能选择用一个很小的学习速率,但是每一层用了BN后,可以有效的解决这个问题,学习速率可以增大很多倍
  • 更多信息参考: DL——BN-LN-IN-GN-LRN-WN

辅助分类节点

(auxiliary classifiers)

  • 在Google Inception V1中,采用了辅助分类节点的策略,即将中间某一层的输出用作分类,并按一个较小的权重加到最终的分类结果中 ,这样相当于做了模型的融合,同时给网络增加了反向传播的梯度信号,提供了额外的正则化的思想.

尝试不同神经网络架构

  • 尝试替换以下方面:
    • 激活函数
    • 层数
    • 权重?
    • 层的参数?

DL——重参数化技巧

  • 参考链接:
    • 漫谈重参数:从正态分布到Gumbel Softmax
    • 重参数化技巧(Gumbel-Softmax)
    • 通俗易懂地理解Gumbel Softmax

重参数化解决的问题

  • 问题 :假设用NN建模一个分布,比如正太分布可以表达为 \(\mathcal{N}(\mu_\theta,\sigma_\theta)\),此时如果直接从NN建模的分布中采样,由于采样动作是离散的,那么这个采样结果不包含NN分布的梯度信息的,NN反向传播时无法传播回去,也无法实现对参数 \(\theta\) 的更新
  • 重参数化技巧 :通过一些技巧设计采样方式,使得采样过程可导,让采样结果包含NN分布的梯度信息(即实现既可按照NN分布采样 ,又可回传梯度信息)

重参数化的基本思想

  • 不能梯度回传的本质原因是因为采样过程是一种选择动作,这种选择动作本身没有梯度信息,把采样过程挪到计算图之外
  • 用形式来表示,将 \(z \sim f(\theta)\) 构建为形如 \(z = g(\theta, \epsilon), \epsilon \sim p\) 的形式(其中p是与参数无关的某个分布,比如高斯分布)

连续变量分布采样的重参数化

  • 以正太分布为例,原始NN分布采样形式:
    $$ z \sim \mathcal{N}(\mu_\theta,\sigma_\theta) $$
  • 重参数技巧采样:
    $$
    \begin{align}
    \epsilon \sim \mathcal{N}(0,1) \\
    z = \mu_\theta + \sigma_\theta \cdot \epsilon
    \end{align}
    $$

离散变量分布采样的重参数化

以下内容主要参考自重参数化技巧(Gumbel-Softmax)以及其中的回复讨论

原版 softmax(原始问题):

1
2
3
logits = model(x)
probs = softmax(logits)
r = torch.multinomial(probs, num_samples)
  • 采到的 r 都是整数 ID,后面可以用 r 去查 embedding table。缺点是采样这一步把计算图弄断了

Gumbel-Max Trick:

1
2
3
4
5
6
7
8
def sample_gumbel(shape, eps=1e-20, tens_type=torch.FloatTensor):
"""Sample from Gumbel(0, 1)"""
U = Variable(tens_type(*shape).uniform_(), requires_grad=False)
return -torch.log(-torch.log(U + eps) + eps)

logits = model(x)
g = sample_gumbel(logits.size())
r = torch.argmax(logits + g)
  • 采到的 r 都是整数 ID,后面可以用 r 去查 embedding table,计算图连起来了,但 argmax 仍不可导
  • 为什么一定要用sample_gumbel分布而不是其他分布?
    • 因为只有使用gumbel分布采样才能保证与原始softmax后的多项式分布采样完全等价,即 argmax(logits + Gumbel随机变量)与多项式分布采样严格等价 ,相关证明见:漫谈重参数:从正态分布到Gumbel Softmax
  • Gumbel分布的具体定义是什么?
    • 一般Gumbel分布的PDF和CDF:
      $$
      \begin{align}
      \text{PDF}: \quad f(x;\mu,\beta) = e^{-(z+e^{-z})},\quad z=\frac{x-\mu}{\beta} \\
      \text{CDF}: \quad F(x;\mu,\beta) = e^{-e^{-z}}, \quad z=\frac{x-\mu}{\beta}
      \end{align}
      $$
      • \(\mu\) 是位置参数(location parameter)
      • \(\beta\) 是尺度参数(scale parameter)
    • 标准Gumbel分布中, \(\mu=0,\ \beta=1\),此时有 \(z=x\)
      $$
      \begin{align}
      \text{PDF}: \quad f(x;\mu,\beta) = e^{-(x+e^{-x})} \\
      \text{CDF}: \quad F(x;\mu,\beta) = e^{-e^{-x}}
      \end{align}
      $$
  • 在这个场景中,我们使用标准Gumbel分布即可
  • 采样标准Gumbel分布时,可以直接使用逆变换采样(Inverse Transform Sampling) :
    • 先按照均匀分布采样: \(u = \mathcal{U}(0,1)\)
    • 对Gumbel分布原始CDF取逆Gumbel分布采样结果: \(z = -ln(-ln(u))\)

Gumbel-Softmax Trick:

1
2
3
logits = model(x)
g = sample_gumbel(logits.size())
r = F.softmax(logits + g)
  • 采到的 r 都是概率分布,后面可以用 r 把 embedding table 里的各个条目加权平均混合起来,假装是一个单词拿去用。虽然计算图可导了,但是训练和推断不一致!训练时模型见到的都是各个 word embedding 的混合,而非独立的 word embedding!推断时则使用的是独立的 word embedding!

Gumbel-Softmax Trick + Straight-Though Estimator:

1
2
3
4
5
logits = model(x)
g = sample_gumbel(logits.size())
r = F.softmax(logits + g)
r_hard = torch.argmax(r)
r = (r_hard - r).detach() + r
  • 采到的 r 都是整数 ID,后面可以用 r 去查 embedding table
  • 前向传播使用 r_hard 获得独立的单词,反向传播使用 r(即 softmax 的结果)的梯度。一切都很完美
  • Straight-Through Estimator 的意思是说,如果你遇到某一层不可导,你就当它的梯度是 identity,直接把梯度漏下去,即假定当前层的梯度为1
  • 实际上此时正向传播和反向传播面对的公式也不一样
    • 正向传播时得到的是r_hard
    • 反向传播时,由于(r_hard - r).detach()使得梯度为0,所以回传的实际是r的反向梯度

argmax动作的梯度回传

  • argmax操作的形式:
    $$
    \begin{align}
    i^* &= \mathop{\arg\max}_i (\vec{x}) \\
    \text{where} \quad \vec{x}=&(x_1, x_2, \cdots, x_n), \quad x_i = f(\theta)_i
    \end{align}
    $$
    • 注:以上argmax的写法不严谨,严谨的是 \(i^* = \mathop{argmax}_i x_i, \ x_i \in \vec{x}\)
  • 近似形式:
    $$
    \begin{equation}
    \mathop{\arg\max}_i (\vec{x}) \approx \sum_{i=1}^n i\times \text{softmax}(\vec{x})_i
    \end{equation}
    $$
  • argmax本质也可以看做一种离散采样,只是没有随机性,该采样选择使得目标值最大的离散变量
  • 详情见:函数光滑化杂谈:不可导函数的可导逼近

DL——迁移学习-元学习-联邦学习

迁移学习-元学习-联邦学习对比

  • 迁移学习侧重于知识从源任务到目标任务的迁移
  • 元学习侧重于快速适应新任务的能力
  • 联邦学习则侧重于在数据隐私保护的前提下进行分布式学习

迁移学习

  • 迁移学习(Transfer Learning)允许模型在一个任务上学习得到的知识应用到另一个不同但相关的任务上。这种方法特别适用于目标任务的数据量不足时。在迁移学习中,通常有一个源域(source domain)和一个目标域(target domain),模型首先在源域上进行训练,然后将学到的特征或参数迁移到目标域以提高学习效率和性能
  • 参考博客: https://blog.csdn.net/dakenz/article/details/85954548

元学习

  • 元学习(Meta-Learning),又称为“学会学习”,是指模型不仅学习如何处理具体的任务,而且学习如何从经验中快速适应和学习新任务的过程。元学习特别关注于当面对新任务时,如何利用已有的知识来加速学习过程。元学习的一个典型应用是通过少量的样本(例如,少样本学习)快速适应新任务
  • 参考链接:【李宏毅-元学习】少样本&元学习Meta Learning_MAML最新机器学习课程!!!
  • 以常见的元学习方法 MAML 的流程为例:
    • 任务采样 :从任务集合中随机选择一批任务,每个任务都包含训练集和验证集
      • 训练集用于更新模型的参数,验证集用于评估模型的泛化性能
    • 任务内优化(内循环) :对于每个采样到的任务,首先使用当前的初始化参数进行梯度更新
      • 通过这些更新,让模型在任务训练集上的表现逐步改善,快速适应当前任务的特定要求
    • 元优化(外循环) :在完成任务内优化后,使用更新后的模型参数在其他任务的验证集上计算损失
      • 然后,基于多个任务的验证集损失,对初始化参数进行更新
      • 外循环的梯度更新是通过任务验证集上的损失进行的,目的是优化整个模型的初始参数 ,使其能在多个任务上快速学习
  • 总结:MAML 的目标是在元训练阶段,通过在多个任务上进行训练和优化 ,找到一个良好的初始参数 ,使得当模型遇到新任务时 ,只需利用新任务的少量数据进行几次快速梯度更新,就能迅速适应新任务,达到较好的性能

联邦学习

  • 联邦学习(Federated Learning)是一种分布式机器学习范式,它允许多个参与者在保持数据隐私和数据本地化的前提下共同构建机器学习模型。在联邦学习中,数据不需要集中存储或处理,而是在各个参与者的本地进行训练,只有模型的更新(如参数)在参与者之间共享。这种方式可以解决数据孤岛问题,同时保护用户隐私。

PyTorch——Accelerate使用总结

  • 参考链接:
    • 官方文档:docs.pytorch.org/docs/stable/fsdp.html

整体说明

  • HuggingFace Accelerate 是一个轻量级库(accelerate),专为简化 PyTorch 模型在各种硬件配置上的训练和推理而设计
  • 它能自动处理分布式训练、混合精度训练等复杂设置,让开发者无需深入了解底层硬件细节,就能轻松将模型部署到单 GPU、多 GPU、TPU 甚至 CPU 集群等环境中(专注于模型逻辑和训练流程即可)
  • 安装简便,仅需一行代码:pip install accelerate
  • Accelerate 的核心功能包括下面几个
    • 自动识别可用硬件(GPU、TPU 等),并根据硬件情况优化训练配置
    • 无缝支持数据并行、模型并行等分布式训练模式 ,适配多 GPU 或集群环境
    • 可选择 FSDP 或 DeepSpeed 等底层框架,仅需简单修改启动命令即可
    • 支持 FP16、BF16 等混合精度训练 ,在减少显存占用的同时,保证模型训练精度
    • 只需对原有 PyTorch 代码进行少量修改 ,即可实现硬件加速和分布式训练
  • 一般来说仅需要两行代码改动,其他都有命令行进行配置
    • accelerator.prepare() :核心函数,用于包装模型、优化器、数据加载器等组件,自动适配分布式和混合精度设置
    • accelerator.backward() :替代传统的 loss.backward(),在分布式环境中确保梯度正确同步

HuggingFace Accelerate 使用代码示例

  • 以下是一个使用 Accelerate 进行模型训练的基础示例,训练代码(train.py)如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.utils.data import Dataset, DataLoader
    from accelerate import Accelerator # 导入 Accelerator

    class DiyModel(nn.Module):
    def __init__(self):
    super().__init__()
    self.fc = nn.Linear(10, 2) # 二分类任务
    def forward(self, x):
    return self.fc(x)

    class DiyDataset(Dataset):
    def __len__(self):
    return 1000
    def __getitem__(self, idx):
    x = torch.randn(10)
    y = torch.randint(0, 2, (1,)).item() # 随机标签(0 或 1)
    return x, y

    # 混合精度配置:可通过 `Accelerator(mixed_precision="fp16")` 启用 FP16 混合精度训练,减少显存占用
    accelerator = Accelerator() # 核心代码,初始化 Accelerator

    # 以下所有定义都不涉及使用 Accelerator
    model = DiyModel()
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    dataset = DiyDataset()
    dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

    # 重点:使用 accelerator 包装多个组件(注:这一行会自动处理分布式和混合精度)
    model, optimizer, dataloader, criterion = accelerator.prepare(
    model, optimizer, dataloader, criterion
    )

    # 特别注意的一点不同是:训练循环时,使用 accelerator.backward()
    model.train()
    for epoch in range(3):
    total_loss = 0.0
    for x, y in dataloader:
    optimizer.zero_grad()
    outputs = model(x)
    loss = criterion(outputs, y)
    accelerator.backward(loss) # 替代 loss.backward()
    optimizer.step()
    total_loss += loss.item()
    avg_loss = total_loss / len(dataloader)
    print(f"Epoch {epoch+1}, Loss: {avg_loss:.4f}")

Accelerate 启动训练

  • 可简单通过 Accelerate 命令行工具配置训练环境并启动:

    1
    accelerate launch --num_processes=2 train.py  # 使用 2 个进程(如 2 个 GPU)
    • --num_processes:指定进程数(通常等于 GPU 数量)
    • 若使用单 GPU 或 CPU,可直接运行 python train.py,Accelerator 会自动适配环境
  • 更多启动命令参见下文


accelerate launch 命令详细说明

  • accelerate launch 是 HuggingFace Accelerate 库的核心命令,用于启动分布式训练脚本,它能自动处理多卡、多机等复杂分布式环境的配置

  • accelerate launch 的核心作用是:

    • 1)初始化分布式环境(进程组、通信后端等)
    • 2)根据参数自动选择分布式策略(数据并行/FSDP/DeepSpeed 等)
    • 3)将环境配置传递给训练脚本中的 Accelerator 实例,使其能正确处理模型、数据的分布式适配
  • accelerate launch 命令基本语法

    1
    accelerate launch [启动参数] your_script.py [脚本参数]
    • [启动参数]:控制分布式训练的配置(如使用的 GPU 数量、分布式策略等)
    • [脚本参数]:传递给你的训练脚本(your_script.py)的自定义参数(如 --epochs 10、--batch_size 32 等)

硬件与进程配置参数

  • --num_processes N:指定总进程数(通常等于参与训练的 GPU 总数)
    • 例如:--num_processes 4 表示使用 4 个 GPU
  • --num_machines N:指定机器数量(多机分布式训练时使用),默认值为 1(单机器)
  • --machine_rank N:当使用多机时,指定当前机器的序号(从 0 开始)
    • 例如:主节点用 --machine_rank 0,从节点用 --machine_rank 1
  • --main_process_ip IP地址:多机训练时,主节点的 IP 地址(供从节点连接)
  • --main_process_port 端口号:主节点的通信端口(默认 29500,需确保端口未被占用)

分布式策略选择参数

  • Accelerate 支持多种分布式策略,通过参数指定:
  • 默认策略(自动选择) :不指定任何策略时,Accelerate 会根据硬件自动选择当前硬件下的最佳策略:
    • 单卡:直接使用单进程训练
    • 多卡:默认使用 PyTorch 的 nn.DataParallel 或 DistributedDataParallel(数据并行)
  • --use_fsdp:
    • 启用 FSDP(完全分片数据并行) ,适合超大规模模型(需 PyTorch ≥ 1.11);
    • 常用搭配参数如下:
      • --fsdp_fully_shard:完全分片参数、梯度和优化器状态(最大程度节省内存)
      • --fsdp_transformer_layer_cls_to_wrap "类名":指定 Transformer 层的类名(如 GPT2 的 GPT2Layer、BERT 的 BertLayer),用于自动分片模型层
      • --fsdp_sharding_strategy 策略:分片策略,可选 FULL_SHARD(完全分片)、SHARD_GRAD_OP(梯度和优化器分片)等
  • --use_deepspeed:
    • 启用 DeepSpeed 分布式框架,支持 ZeRO 优化、混合精度等(需提前安装 deepspeed)
    • 通常需配合 DeepSpeed 配置文件使用,通过 --deepspeed 配置文件路径 指定

混合精度训练参数

  • --mixed_precision [mode]:指定混合精度策略
  • 可选模式 [mode] 为:
    • no:不使用混合精度(默认)
    • fp16:使用 FP16 混合精度
    • bf16:使用 BF16 混合精度(需 GPU 支持,如 A100、RTX 3090 等)
    • fp8:使用 FP8 混合精度(需 PyTorch ≥ 2.0 且 GPU 支持)

其他实用参数

  • --config_file 配置文件路径:通过 YAML 配置文件指定所有参数(推荐复杂场景使用),无需在命令行逐个输入
  • --debug:启用调试模式,打印详细的分布式初始化日志,便于排查问题
  • --gradient_accumulation_steps N:指定梯度累积步数(等价于在代码中设置,但通过命令行更灵活)

附录:在启动命令中使用配置文件用法(推荐)

  • 对于复杂配置(如 FSDP/DeepSpeed 细节),建议使用 YAML 配置文件,步骤如下:

  • 第一步:生成默认配置文件 :

    1
    accelerate launch --config_file accelerate_config.yaml --generate_config
    • --generate_config 表示运行后会交互式提问,自动生成配置文件
    • 注:也可以自己编辑 accelerate_config.yaml 文件
  • 第二步:示例配置文件(FSDP 场景) ::

    1
    2
    3
    4
    5
    6
    7
    8
    compute_environment: LOCAL_MACHINE  # 本地机器环境
    distributed_type: FSDP # 使用 FSDP 策略
    fsdp_config:
    fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP # 自动包装 Transformer 层
    fsdp_transformer_layer_cls_to_wrap: "GPT2Layer" # 模型层类名
    fsdp_sharding_strategy: FULL_SHARD # 完全分片
    mixed_precision: fp16 # 启用 FP16 混合精度
    num_processes: 4 # 4 个进程(4 卡)
  • 第三步:使用配置文件启动 :

    1
    accelerate launch --config_file accelerate_config.yaml your_script.py --epochs 10

附录:启动命令的常见场景示例

  • 示例一:单机器多卡基础数据并行示例

    1
    accelerate launch --num_processes 4 train.py --batch_size 32
  • 示例二:启用 FSDP 训练大模型

    1
    2
    3
    4
    5
    6
    7
    accelerate launch \
    --num_processes 4 \
    --use_fsdp \
    --fsdp_fully_shard \
    --fsdp_transformer_layer_cls_to_wrap "BertLayer" \
    --mixed_precision bf16 \
    train_bert.py
  • 示例三:启用 DeepSpeed 与 ZeRO-3 优化(其中 ds_config.json 为 DeepSpeed 配置文件,定义 ZeRO 阶段、梯度裁剪等)

    1
    2
    3
    4
    5
    accelerate launch \
    --num_processes 8 \
    --use_deepspeed \
    --deepspeed ds_config.json \
    train.py

PyTorch——CrossEntopy-Loss和NLL-Loss的区别


整体说明

  • 交叉熵损失(Cross-Entropy Loss, CE Loss)和负对数似然损失(Negative Log-Likelihood Loss, NLL Loss)的核心区别在于 是否包含 Softmax 激活 以及 适用场景的精细化
  • PyTorch 框架中的交叉熵损失 = Softmax(或 Sigmoid)+ 负对数似然损失
  • 使用原则
    • 分类任务直接用 CE Loss(输入 logits),简单且数值稳定
    • 若已手动添加 Softmax/LogSoftmax 层,或需要自定义概率输出,再用 NLL Loss
  • NLL Loss 是“对概率的负对数”,CE Loss 是“对分布差异的衡量”,但在分类任务中(预测分布由 Softmax 生成)二者结果等价

分类任务场景符号定义

  • 模型原始输出(logits):\(z = [z_1, z_2, …, z_C]\)(\(C\) 为类别数)
  • 真实标签:\(y\)(one-hot 编码为 \(y_{one-hot} = [0, 0, …, 1, …, 0]\),或类别索引 \(y \in {0,1,…,C-1}\))
  • Softmax 激活:将 logits 映射为概率分布 \(p_i = \text{Softmax}(z)i = \frac{e^{z_i}}{\sum{j=1}^C e^{z_j}}\)(满足 \(\sum p_i = 1\))
  • 对数概率:\(\log(p_i)\)(概率越大,对数概率越接近 0;概率越小,对数概率越负)

负对数似然损失(Negative Log-Likelihood Loss, NLL Loss)

  • 负对数似然损失(NLL Loss)是 对 模型输出的概率分布 \(p\) 取对数后,取负,再根据真实标签\(y\)选择对应类别的项
    • 若标签为 one-hot 编码:
      $$ L_{\text{NLL}} = -\sum_{i=1}^C y_i \cdot \log(p_i)$$
      • 此时仅真实类别 \(y_k=1\),所以可进一步简化为
        $$ L_{\text{NLL}} = -\log(p_k) $$
        • 这也是 负对数似然损失(NLL Loss)名字的由来
    • 若标签为类别索引(本质是一个 index,one-hot 转换成向量以后与 one-hot 编码一致):直接取真实类别 \(y\) 对应的概率 \(p_y\) 的对数再取负
      $$ L_{\text{NLL}} = -\log(p_y)$$
  • 特别说明: NLL Loss 本身不包含 Softmax 步骤
    • 输入给 NLL Los 的必须是 已经经过 Softmax 激活的概率分布 \(p\) 或对数概率 \(\log(p)\))

交叉熵损失(Cross-Entropy Loss, CE Loss)

  • 交叉熵损失(CE Loss) 用于衡量 模型预测概率分布 \(p\) 与 真实标签分布 \(q\) (one-hot编码)之间的差异,公式与 NLL Loss形式完全一致 ,但隐含了“先将 logits 转为概率”的逻辑
    • 数学公式:
      $$ L_{\text{CE}} = -\sum_{i=1}^C q_i \cdot \log(p_i)$$
      • 其中: \(q\) 是真实分布
      • one-hot 时简化为
        $$ L_{\text{CE}} = -\log(p_y) $$
    • 关键区别:在深度学习框架中(如 PyTorch、TensorFlow),CE Loss 的输入是 原始 logits ,而非概率
      • 框架会内部先对 logits 做 Softmax,再计算负对数似然

CE Loss 和 NLL Loss 对比

  • 本质上,交叉熵(CE)的数学定义是
    $$ H(q,p) = -\sum q_i \log(p_i)$$
  • 而 负对数似然(NLL)是
    $$ -\sum q_i \log(p_i)$$
    • 仅当 \(p\) 是模型预测的概率分布时,因此当 \(p\) 是 Softmax 输出时,CE Loss = NLL Loss ,但二者的 计算链路 不同:
      $$ \text{CE Loss(logits)} = \text{NLL Loss(Softmax(logits))}$$

附录:为什么框架中的 CE Loss 更常用?

  • 在 PyTorch 中:
    • nn.NLLLoss():输入必须是 对数概率 (通常需先过nn.LogSoftmax(dim=1))
    • nn.CrossEntropyLoss():输入是 logits,内部等价于 nn.LogSoftmax(dim=1) + nn.NLLLoss()
  • 选择 CE Loss 的核心原因是数值稳定性 :
    • 直接计算 \(\log(\text{Softmax}(z_i))\) 时,若 \(z_i\) 很大(如 1000),\(e^{z_i}\) 会溢出(变成无穷大);而框架会用数学变换优化:
      $$ \log\left(\frac{e^{z_i}}{\sum_j e^{z_j}}\right) = z_i - \log\left(\sum_j e^{z_j}\right) $$
    • 通过减去 \(\log(\sum_j e^{z_j})\)(即 LogSumExp),避免了指数爆炸,数值更稳定
  • 补充:一种常用的等价推导(可以优化计算的方法)是
    $$
    \begin{align}
    \text{log_sofmax}(x) &= \log \frac{e^{x_{i}}}{\sum_{j=1}x_{j}} \\
    &= \log e^{x_i} - \log \sum_{j=1}x_{j} \\
    &= x_i - \log \sum_{j=1}x_{j}
    \end{align}
    $$
    • 上面的式子中,只需要计算一次 \(\log \sum_{j=1}x_{j}\) 即可(且不同维度可重用该值), 其他的都是加减法运算

使用代码示例

  • 用 CrossEntropyLoss(直接输入 logits)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import torch
    import torch.nn as nn

    # 模型输出logits(C=3类,batch_size=2)
    logits = torch.tensor([[2.0, 1.0, 0.1], [1.0, 3.0, 0.5]])
    # 真实标签(类别索引)
    labels = torch.tensor([0, 1])

    # 交叉熵损失(内部含Softmax)
    ce_loss = nn.CrossEntropyLoss()
    print(ce_loss(logits, labels)) # 输出:0.4170(数值稳定)
  • 用 NLLLoss(需先过LogSoftmax)

    1
    2
    3
    4
    5
    6
    7
    # 先将logits转为对数概率
    log_softmax = nn.LogSoftmax(dim=1)
    log_probs = log_softmax(logits) # 等价于框架内部优化后的计算

    # 负对数似然损失(输入是对数概率)
    nll_loss = nn.NLLLoss()
    print(nll_loss(log_probs, labels)) # 输出:0.4170(与CE Loss结果一致)

附录:错误示例(NLLLoss 直接输入 logits)

  • 常常犯的错:
    1
    2
    # 直接用logits输入NLLLoss(会报错或结果错误)
    print(nll_loss(logits, labels)) # 错误:logits不是对数概率,数值会异常

附录:BCE Loss

  • 二分类任务时,CE Loss 常被称为 二元交叉熵(BCE Loss) ,与 NLL Loss 的关系同样成立:

    • nn.BCEWithLogitsLoss():输入是 logits(单输出,如 \(s\)),内部先做 Sigmoid(\(\sigma(s) = 1/(1+e^{-s})\)),再计算BCE Loss:
      $$ L = -[y\log(\sigma(s)) + (1-y)\log(1-\sigma(s))]$$
    • 若手动用 Sigmoid + NLLLoss:需将标签转为 float(如 \(y \in {0.0, 1.0}\)),且输出为单通道概率,此时
      $$ L = -[y\log(p) + (1-y)\log(1-p)]$$
      • 与 BCEWithLogitsLoss 结果一致
  • BCELoss 还可以直接使用

    1
    torch.nn.BCELoss()
    • 具体操作就是实现了书上定义的二分类交叉熵定义
    • 普通样本计算公式:
      $$ loss(o,t)=-\frac{1}{n}\sum_i(t_i\log(o_i)+(1-t_i)\log(1-o_i)) $$
    • 带有权重的单个样本计算公式:
      $$ loss(o,t)=-\frac{1}{n}\sum_i w_i (t_i \log(o_i)+(1-t_i)\log(1-o_i)) $$

BCELoss vs CrossEntropyLoss 比较

  • BCELoss 对应的网络只有一个输出值
  • CrossEntropyLoss 对应的网络有两个输出值
  • 可以证明, 二分类时 BCELoss 与 CrossEntropyLoss等价
    • 证明时,将每个 CrossEntropyLoss 的计算公式中的 softmax 函数分子分母同时除以 shift(\(\text{shift} = max (x_i)\)), 即可得到为下面的定义,进一步可得到 BCELoss 的计算公式
      $$f_i(x) = \frac{e^{(x_i - \text{shift})}} { \sum^j e^{(x_j - \text{shift})}} $$

附录:相关损失函数 MultiLabelMarginLoss

  • 使用

    1
    torch.nn.MultiLabelMarginLoss()
  • 用于多标签分类的损失函数


总结

  • 一般来说直接使用 CrossEntropyLoss 即可
    • 二分类时还可以使用 nn.BCELoss
    • 二分类时使用 nn.BCELoss 的话,输入的 input和target维度都为 (n, 1) 的维度
    • 二分类时使用 CrossEntropyLoss 则输入的 input 为 (n x 2) 的维度
  • 如果使用 NLLLoss 则一定记得在输出层最后加一层 log_softmax 层
  • 注意,log 指的是以 e 为底的对数函数,而不是以10为底的
    • Mac自带的计算器中 log 是以 10 为底的,ln 才是以 e 为底的

PyTorch——DataLoader的使用


整体说明

  • PyTorch 中,Global Step 的计算和实现可能会因为不同版本而发生有趣的现象
  • 比如:在一些场景会遇到一些奇怪的现象,相差一个 样本,且不在 Global Batch 的整数倍边界,但是 Global Step 增加了 1

DataLoader 核心使用说明

  • torch.utils.data.DataLoader 用于加载数据集,实现批量读取、多线程加载、数据 Shuffle等功能
  • 用于训练和评估

DataLoader 基本用法

  • 用法简单示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    from torch.utils.data import DataLoader, Dataset

    # 定义数据集(需实现 __len__ 和 __getitem__)
    class MyDataset(Dataset):
    def __init__(self, data):
    self.data = data
    def __len__(self): # 返回总数据量
    return len(self.data)
    def __getitem__(self, idx): # 返回单条数据(idx为索引)
    return self.data[idx]

    # 初始化 DataLoader
    dataset = MyDataset(data=[1,2,3,4,5,6,7])
    dataloader = DataLoader(
    dataset=dataset, # 传入数据集
    batch_size=2, # 每个批次的样本数(默认1)
    shuffle=True, # 每个epoch是否打乱数据(默认False)
    drop_last=False, # 是否丢弃最后不完整批次(默认False)
    num_workers=0, # 加载数据的线程数(默认0,主线程加载)
    pin_memory=False # 是否将数据存入固定内存(加速 GPU 读取,默认False)
    )

    # 迭代使用(返回tensor批次)
    for batch in dataloader:
    print(batch)
  • 关键参数说明:

    • dataset:必须传入的数据集对象,这个类需实现 __len__/__getitem__
    • batch_size:批次大小,控制每次返回的样本数,当存在 Global Batch Size 和 Micro Batch Size,一般是 Micro Batch Size
    • shuffle:训练时建议设为 True,验证/测试时设为 False, 默认为 None
      • 注:设为 True 时,每个 epoch 开始时会重新打乱数据
    • drop_last:数据量无法被batch_size整除时,是否丢弃最后不足一个批次的样本
    • num_workers:多线程加载数据,可根据 CPU 核心数调整
    • pin_memory:若使用 GPU 训练,设为 True 可减少数据拷贝耗时,默认为 False
  • 注意事项

    • 迭代返回的批次默认是 tensor 类型(若数据集返回 numpy 或 列表 类型的数据,会自动转换);
    • shuffle=True 会在每个 epoch 开始时会重新打乱数据,保证训练随机性;
    • len(dataloader) 表示单个 epoch 的批次数量(计算规则:ceil(总数据量/batch_size) 或 floor,取决于 drop_last)

附录:记一次有趣的 Bug 排查

  • 一般的 Global Batch Step 计算实现如下:

    1
    total_global_step = len(dataloader) * epoches // (global_batch_size / micro_batch_size)
    • 整体可以表述为:数据集中包含的 Micro Batch 数量 除以 一个 Global Batch 中包含的 Micro Batch 数量
      • len(dataloader):数据集中包含的 Micro Batch 数量
      • (global_batch_size / micro_batch_size):一个 Global Batch 中包含的 Micro Batch 数量
  • 有趣的问题:假定 drop_last=False, micro_batch_size = 8, global_batch_size = 128, epoch=1

    • 总样本数量为 2040 时,total_global_step = 15
    • 总样本数量为 2041 时,total_global_step = 16
    • 可通过带入上面的代码验证结果
  • 问题出现的原因:

    • 当样本数量为 2040 时,len(dataloader) = 255
    • 当样本数量为 2041 时,len(dataloader) = 256
    • 进一步计算即可发现,虽然只是一个样本,且该数据量并不在 global_batch_size = 128 的整除边界,但是造成了 Global Step 多了 1,这很反直觉切不容易排查
1…535455…61
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

608 posts
49 tags
GitHub E-Mail
© 2026 Joe Zhou
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4