优化器准则
凸优化基本概念
- 先定义凸集,集合中的两个点连接的线还在集合里面,就是凸集,用数学语言来表示就是:对于集合中的任意两个元素x,y以及任意实数 λ ∈ ( 0 , 1 ) \lambda \in (0,1) λ∈(0,1),有 λ x + ( 1 − λ ) y ∈ C \lambda x + (1 - \lambda) y \in C λx+(1−λ)y∈C,则称为凸集。
- 再定义凸函数: f ( λ x + ( 1 − λ ) y ) ≤ λ f ( x ) + ( 1 − λ ) f ( y ) f(\lambda x + (1 - \lambda) y) \leq \lambda f(x) + (1 - \lambda) f(y) f(λx+(1−λ)y)≤λf(x)+(1−λ)f(y)其中, λ \lambda λ是一个满足 0 ≤ λ ≤ 1 0 \leq \lambda \leq 1 0≤λ≤1的实数参数。
- 可以看出,凸函数的定义域必须是凸集。直观上,凸函数的图像不会在任何地方凹陷,这使得凸函数的局部最小值也是全局最小值,这使得优化问题更容易解决。
现在再定义凸优化问题:
凸优化是数学优化理论中的一个重要分支,它研究的是凸函数的优化问题。用数学语言来表示就是:
minimize f ( x ) subject to g i ( x ) ≤ 0 , i = 1 , … , m and h j ( x ) = 0 , j = 1 , … , p \begin{align*} \text{minimize} \quad & f(x) \\ \text{subject to} \quad & g_i(x) \leq 0, \quad i = 1, \ldots, m \\ \text{and} \quad & h_j(x) = 0, \quad j = 1, \ldots, p \\ \end{align*} minimizesubject toandf(x)gi(x)≤0,i=1,…,mhj(x)=0,j=1,…,p
其中, f ( x ) f(x) f(x) 是目标函数, g i ( x ) g_i(x) gi(x)是不等式约束函数, h j ( x ) h_j(x) hj(x)是等式约束函数, x x x是决策变量。如果目标函数 f ( x ) f(x) f(x) 和所有约束函数 g i ( x ) g_i(x) gi(x)和 h j ( x ) h_j(x) hj(x)都是凸函数,并且可行域(满足所有约束的 x x x的集合)也是凸集,那么这个问题就是一个凸优化问题。
凸优化问题有以下特点:
- 局部最优即全局最优:如果一个点是局部最小点,那么它也是全局最小点。这使得寻找最优解变得更加容易。
- 对偶性:凸优化问题具有良好的对偶性质,即原问题的对偶问题也是一个凸优化问题。
- 存在性:如果目标函数和约束函数都是下闭的,并且可行域非空,那么凸优化问题总是有解的。
- 稳定性:凸优化问题的解对问题的微小变化是稳定的。
研究方法有:
- 梯度下降法:通过迭代地沿着目标函数的负梯度方向移动来找到最小点。
- 牛顿法:利用目标函数的二阶导数(Hessian)来加速梯度下降法。
- 内点法:一种专门用于解决有约束凸优化问题的算法。
- 次梯度法:对于非光滑的凸函数,使用次梯度而不是梯度来优化。
- 对偶方法:通过解决对偶问题来找到原问题的解。
神经网络优化问题定义
现在我们可以开始讨论优化器了:
深度学习模型的训练就是一个优化问题,模型权重就是我们上面提到的决策变量 x x x,目标函数就是我们所设计的损失函数(所以我们将损失函数设计成凸函数,以满足凸优化的条件),模型本身就是一个等式或者不等式约束,输出结果必须满足事先知道的label。优化器,就是解决这个凸优化问题的实现方案。
我们的优化问题用数学来表示就是:
f ( W ) = min w 1 N ∑ i = 1 N L ( y i , F ( x i ) ) + ∑ j = 1 n λ ∥ w j ∥ f(W) = \min_{w} {\frac{1}{N}\sum_{i=1}^{N} L(y_i,F(x_i)) + \sum_{j=1 }^{n}\lambda \left \| w_{j} \right \| } f(W)=wminN1i=1∑NL(yi,F(xi))+j=1∑nλ∥wj∥
W W W是模型的所有参数
前一项是损失,其中 w w w是参数, N N N是样本总数, y i y_i yi是样本标签, F ( x i ) F(x_i) F(xi)是模型结果,L是损失函数,
后一项是正则损失,用于避免过拟合现象的, λ \lambda λ是正则化系数, ∥ w j ∥ \left \| w_{j} \right \| ∥wj∥是参数的范数,常见有L1,L2等
这就是深度学习优化的数学定义,这样我们就可以去使用数学方法来解决这个问题了。
- 使用梯度下降法就是: W t = W t − 1 − α ∗ ▽ f ( W t − 1 ) W_t = W_{t-1}-\alpha *\bigtriangledown f(W_{t-1}) Wt=Wt−1−α∗▽f(Wt−1)其中 ▽ f ( W t − 1 ) \bigtriangledown f(W_{t-1}) ▽f(Wt−1)是函数的梯度向量。
- 使用牛顿法就是: W t = W t − 1 − α ∗ H t − 1 − 1 ∗ ▽ f ( W t − 1 ) W_t = W_{t-1}-\alpha*H_{t-1}^{-1} *\bigtriangledown f(W_{t-1}) Wt=Wt−1−α∗Ht−1−1∗▽f(Wt−1)其中 H t − 1 − 1 H_{t-1}^{-1} Ht−1−1为Hessian矩阵的逆矩阵即二阶偏导矩阵的逆矩阵。
这也是深度学习中随机梯度下降的由来,从最优化的梯度下降借鉴过来的。
优化器可以做的事情,就是对解决问题方法中的:梯度gt,学习率,参数正则项,参数初始化这几个因素进行调整。
不同的优化器,他们的区别就是这四项的不同。
优化器分类与发展
随机梯度
- SGD:梯度计算的变种,主要区别在于gt的计算方式,原始梯度下降算法叫做GD,计算所有梯度然后更新,SGD叫做随机梯度下降,因为它每次只采用一小批训练样本作为梯度更新参数,然后根据这个梯度更新模型参数。这种方法的优点是计算效率高,因为不需要计算整个训练集上的梯度,这在数据量很大时尤其有用。
- 动量SGD:mSGD,gt不光包括计算出的梯度,还包括了部分过去的梯度信息,好处是会加速收敛,并且跳过一些局部最优
RMS等。
自适应梯度
算法的核心思想是根据参数的历史更新信息来调整每个参数的学习率,从而提高收敛速度并减少训练时间。
- Adaptive Gradient:自适应梯度算法,它通过为每个参数维护一个累积的梯度平方和来调整学习率。AdaGrad 的更新规则如下:
θ i = θ i − η G i i + ϵ ⋅ ∇ θ L ( θ ) i \theta_i = \theta_i - \frac{\eta}{\sqrt{G_{ii} + \epsilon}} \cdot \nabla_\theta L(\theta)_i θi=θi−Gii+ϵη⋅∇θL(θ)i
其中, G G G 是一个对角矩阵, G i i G_{ii} Gii是参数 θ i \theta_i θi的累积梯度平方和, ϵ \epsilon ϵ是一个很小的常数,用来保证数值稳定性。这种算法的缺点是因为下面的累计梯度平方和越来越大,越往后训练的效果越弱,如果有出现异常梯度值,那直接后面的训练就约等于无效了。 - RMSProp(均方根传播):
RMSProp 是一种指数加权的移动平均算法,用于计算梯度的平方的指数衰减平均。它与 AdaGrad 类似,但是使用了梯度平方的指数衰减平均而不是累积和,避免了学习率变得过小的问题。更新规则如下:
G i i = γ G i i + ( 1 − γ ) ⋅ ( ∇ θ L ( θ ) i ) 2 G_{ii} = \gamma G_{ii} + (1 - \gamma) \cdot (\nabla_\theta L(\theta)_i)^2 Gii=γGii+(1−γ)⋅(∇θL(θ)i)2
θ i = θ i − η G i i + ϵ ⋅ ∇ θ L ( θ ) i \theta_i = \theta_i - \frac{\eta}{\sqrt{G_{ii} + \epsilon}} \cdot \nabla_\theta L(\theta)_i θi=θi−Gii+ϵη⋅∇θL(θ)i。其中, γ \gamma γ是衰减率。历史的梯度只占一部分,避免了因为历史梯度导致G不断增大,进而出现无法更新的情况。
Adam & AdamW
-
Adam(自适应矩估计):
14年提出,Adam 结合了 AdaGrad 和 RMSProp 的优点,同时计算了梯度的一阶矩(均值)和二阶矩(方差)的指数加权移动平均。Adam 的更新规则较为复杂,涉及两个时刻的估计量:
m t = β 1 m t − 1 + ( 1 − β 1 ) ⋅ ∇ θ L ( θ ) m_t = \beta_1 m_{t-1} + (1 - \beta_1) \cdot \nabla_\theta L(\theta) mt=β1mt−1+(1−β1)⋅∇θL(θ)就是上面提到的一阶动量部分,借鉴Momentum部分
v t = β 2 v t − 1 + ( 1 − β 2 ) ⋅ ( ∇ θ L ( θ ) ) 2 v_t = \beta_2 v_{t-1} + (1 - \beta_2) \cdot (\nabla_\theta L(\theta))^2 vt=β2vt−1+(1−β2)⋅(∇θL(θ))2
二阶动量部分,也就是借鉴RMSProp部分
这样虽然避免了后期无法更新的问题,但是引入了一个新的问题,那就是因为有衰弱因数,导致在刚开始训练的时候梯度信息积累太慢,因此在更新的时候设一个无偏估计,使用该无偏估计来进行更新
m ^ t = m t 1 − β 1 t \hat{m}_t = \frac{m_t}{1-\beta_1^t} m^t=1−β1tmt
v ^ t = v t 1 − β 1 t \hat{v}_t = \frac{v_t}{1-\beta_1^t} v^t=1−β1tvt
θ t + 1 = θ t − η v ^ t + ϵ ⋅ m ^ t \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \cdot \hat{m}_t θt+1=θt−v^t+ϵη⋅m^t
其中, m t m_t mt 和 v t v_t vt 分别是梯度的一阶矩和二阶矩的估计, m ^ t \hat{m}_t m^t 和 v ^ t \hat{v}_t v^t 是它们的无偏估计量,( \beta_1 ) 和 ( \beta_2 ) 是超参数。无偏估计的意思是:在大量数据的时候,估计量(estimator)的期望值(或平均值)等于被估计的参数的真实值。
在transformer模型中常用,因为transformer的Lipschitz常量很大,每一层的Lipschitz常量差异又很大,学习率很难估计,而且学习完表现也比较差。所以mSGD基本不用,都是用Adam。
mSGD在卷积网络的时候效果还是不错的,能够和Adam打个平手
Lipschitz常量是指在Lipschitz连续中的一个量,能够体现凸函数的变化率。Lipschitz常量差异大就表示不同函数间相同的自变量变化导致的因变量变化差异大,简而言之也就是学习率需要设置的不同。
AdamW
AdamW和Adam基本一致,只有对正则项的处理不一致。Adam和前面是其他的一样,都是在损失函数里面加一个正则项,但是当训练时,前期梯度太大,会把正则项淹没掉,后期梯度太小,正则项又会把梯度信息淹没掉,AdamW的目的是为了平衡这两项。
AdamW中W 代表权重衰减(Weight Decay),将原本的正则项改为weight decay,将原本在损失函数中的项,放到了权重更新公式中:
θ t + 1 = θ t − η v ^ t + ϵ ⋅ m ^ t − λ θ t \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \cdot \hat{m}_t - \lambda \theta_{t} θt+1=θt−v^t+ϵη⋅m^t−λθt
在 AdamW 中,权重衰减不是直接从参数更新中减去,而是作为参数更新的一部分。这样做的好处是:
- 保持自适应学习率的一致性:权重衰减与自适应学习率相结合,确保了不同参数的学习率保持一致。
- 提高收敛性和稳定性:调整后的权重衰减有助于算法更快地收敛,并提高了训练过程的稳定性。
优化器内存占用
在进行小模型训练时,对于优化器的内存占用不是很关注,但是在进行大模型训练时,优化器的内存占用非常大,就需要专门考虑了,大模型常用的优化器为AdamW。
AdamW算法的内存占用相对较高,因为它需要同时保存一阶和二阶矩。具体来说,AdamW算法在优化过程中需要存储以下内容:
- 参数的当前值 θ \theta θ。
- 梯度的一阶矩估计(即一阶动量) m \mathbf{m} m。
- 梯度的二阶矩估计(即二阶动量) v \mathbf{v} v。
每个参数 θ \theta θ 都需要额外存储两个与其尺寸相同的向量 m \mathbf{m} m和 v \mathbf{v} v,这导致内存占用大约是原始参数内存的两倍。此外,还需要存储超参数。在大规模训练或参数量非常大的模型中,这种内存占用可能会成为一个问题。例如,在训练具有数百万参数的模型时,使用AdamW可能会导致显著的内存需求增加,这可能限制了模型的大小或训练并行度。
对于参数量为 Φ \Phi Φ的模型,使用混合精度进行训练,模型参数本身使用fp16存储,占用 2 Φ 2\Phi 2Φ个字节,同样模型梯度占用 2 Φ 2\Phi 2Φ个字节,Adam状态(fp32的模型参数备份,fp32的momentum和fp32的variance)一共要占用 12 Φ 12\Phi 12Φ个字节,这两个统称模型状态,共占用 16 Φ 16\Phi 16Φ个字节
混合精度训练时,前向传播和反向传播都是fp16,但是参数更新时使用fp32。
针对显存这个问题,微软提出了ZeRO技术,将模型状态进行分片,对于N个GPU,每个GPU中保存 1 N \frac{1}{N} N1的模型状态量
实现
Pytorch中优化器:官方教程