妈妈,我的炼丹炉子炸啦(不是)
妈妈,我的深度学习模型训练好了!
本文持续更新,如果有什么你知道的深度学习模型训练技巧,可以在评论区提出,我会加进来的。
weight_decay 作为一个超参数,传入 optimizer 的构造器中,所以这个参数显然是与梯度更新有关的。
梯度更新公式:
θ t ← θ t − 1 − λ ⋅ g t \theta_t \leftarrow \theta_{t-1} - \lambda\cdot g_t θt←θt−1−λ⋅gt
其中 θ \theta θ 是参数, λ \lambda λ 是学习率, g t g_t gt 是 t 时刻的梯度
在梯度更新公式中加入一个衰减参数 β \beta β:
θ t ← ( 1 − β ) ⋅ θ t − 1 − λ ⋅ g t \theta_t \leftarrow (1-\beta)\cdot\theta_{t-1} - \lambda\cdot g_t θt←(1−β)⋅θt−1−λ⋅gt
这个 β \beta β 就是 weight decay 的参数,一般取值比较小,比如0.0005
在深度学习模型中,一般将衰减系数设置为
0.0001
到0.001
之 间的值,这是一个比较常用的范围;
当你不确定模型复杂度和数据集大小的时候,最保守就是从1e-4
周围开始尝试;
来源:https://zhuanlan.zhihu.com/p/607909453
来源:权重衰减(weight decay)与学习率衰减(learning rate decay)
L2正则化就是在代价函数后面再加上一个正则化项。
代价函数,损失函数,目标函数区别
- 损失函数(Loss Function )是定义在单个样本上的,算的是一个样本的误差。
- 代价函数(Cost Function )是定义在整个训练集上的,是所有样本误差的平均,也就是损失函数的平均。
- 目标函数(Object Function)定义为:最终需要优化的函数。等于经验风险+结构风险(也就是代价函数 + 正则化项)。(经验风险最小指,在训练集上的预测最准确;结构风险减低是指,模型简单,过于复杂的模型容易过拟合)
C = C 0 + α 2 n ∑ w w 2 C=C_0+\frac{\alpha}{2n}\sum_ww^2 C=C0+2nαw∑w2
C 0 C_0 C0 是原始代价函数,后面那一项是 L2 正则化项:所有参数 w w w 的平方和,除训练集的样本大小 n n n 。 α \alpha α 是正则项系数,分母中的 2 用于求导系数配平。
为什么说 weight decay 在某些情况下就是 L2 正则化?
对 L2 正则化后的代价函数求导:
∂ C ∂ w = ∂ C 0 ∂ w + α n w \frac{\partial C}{\partial w}=\frac{\partial C_0}{\partial w} + \frac{\alpha}{n}w ∂w∂C=∂w∂C0+nαw
w ← w − λ ∂ C 0 ∂ w − λ α n w w \leftarrow w - \lambda \frac{\partial C_0}{\partial w} - \lambda \frac{\alpha}{n}w w←w−λ∂w∂C0−λnαw
w ← ( 1 − λ α n ) w − λ ∂ C 0 ∂ w w \leftarrow \left(1-\lambda \frac{\alpha}{n}\right)w - \lambda \frac{\partial C_0}{\partial w} w←(1−λnα)w−λ∂w∂C0
weight decay:
β = λ α n \beta = \lambda \frac{\alpha}{n} β=λnα
直观上,weight decay 在权重参数上加了一个缩小因子,使得权重整体保持在一个较小在的值,顺便可以避免梯度爆炸。
weight decay 和 L2 正则化项有让 w w w 变小的效果,但是为什么 w w w 变小可以防止过拟合呢?
来源:权重衰减(weight decay)与学习率衰减(learning rate decay)
(1)从模型的复杂度上解释:更小的权值w,从某种意义上说,表示网络的复杂度更低,对数据的拟合更好(这个法则也叫做奥卡姆剃刀),而在实际应用中,也验证了这一点,L2正则化的效果往往好于未经正则化的效果。(2)从数学方面的解释:过拟合的时候,拟合函数的系数往往非常大,为什么?过拟合,就是拟合函数需要顾忌每一个点,最终形成的拟合函数波动很大。在某些很小的区间里,函数值的变化很剧烈。这就意味着函数在某些小区间里的导数值(绝对值)非常大,由于自变量值可大可小,所以只有系数足够大,才能保证导数值很大。而正则化是通过约束参数的范数使其不要太大,所以可以在一定程度上减少过拟合情况。
来源:神经网络中 warmup 策略为什么有效;有什么理论解释么?
使用 SGD 训练神经网络时,在初始使用较大学习率而后期切换为较小学习率是一种广为使用的做法,在实践中效果好且最近也有若干文章尝试对其进行了理论解释。
而 warmup 策略则与上述 scheme 有些矛盾。warmup 需要在训练最初使用较小的学习率来启动,并很快切换到大学习率而后进行常见的 decay。那么最开始的这一步 warmup 为什么有效呢?
刚开始模型对数据的“分布”理解为零,或者是说“均匀分布”(当然这取决于你的初始化);在第一轮训练的时候,每个数据点对模型来说都是新的,模型会很快地进行数据分布修正,如果这时候学习率就很大,极有可能导致开始的时候就对该数据“过拟合”,后面要通过多轮训练才能拉回来,浪费时间。当训练了一段时间(比如两轮、三轮)后,模型已经对每个数据点看过几遍了,或者说对当前的batch而言有了一些正确的先验,较大的学习率就不那么容易会使模型学偏,所以可以适当调大学习率。这个过程就可以看做是warmup。那么为什么之后还要decay呢?当模型训到一定阶段后(比如十个epoch),模型的分布就已经比较固定了,或者说能学到的新东西就比较少了。如果还沿用较大的学习率,就会破坏这种稳定性,用我们通常的话说,就是已经接近loss的local optimal了,为了靠近这个point,我们就要慢慢来。
如果一开始就用0.1,虽然最终会收敛,但之后acc还是不会提高(使用了pateaus schedule);如果用了warmup,在收敛后还能有所提高。也就是说,用warmup和不用warmup达到的收敛点,对之后模型能够达到的suboptimal有影响。这说明什么?这说明不用warmup收敛到的点比用warmup收敛到的点更差。这可以从侧面说明,一开始学偏了的权重后面拉都拉不回来……
DropPath/drop_path 是一种正则化手段,其效果是将深度学习模型中的多分支结构随机”删除“
pytorch 源码,我对其进行注释:
def drop_path(x, drop_prob: float = 0., training: bool = False, scale_by_keep: bool = True):
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
This is the same as the DropConnect impl I created for EfficientNet, etc networks, however,
the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper...
See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for
changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use
'survival rate' as the argument.
"""
# drop_prob 是随机失活的比例,如果=0,则所有都不失活;如果=1,则所有都失活
if drop_prob == 0. or not training:
return x
# keep_prob 是保留的比例,假设 drop_prob=0.1 则 keep_prob=0.9
# 含义是,每个分支都有 0.9 的概率被保留,有 0.1 的概率被失活
keep_prob = 1 - drop_prob
shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets
# 失活的方法是,生成一个随机 tensor,这个 tensor 的每一个值都是 0 或者 1,有 keep_prob 的概率为 1;
# 等下这个随机 tensor 和 x 相乘,于是对应的位置 x 变成 0,实现失活的效果
random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
if keep_prob > 0.0 and scale_by_keep:
random_tensor.div_(keep_prob)
return x * random_tensor
以 ViT 的 block 代码为例,ViT 的模型结构图:
ViT block 的 pytorch 代码:
class Block(nn.Layer):
def __init__(self,
dim,
num_heads,
mlp_ratio=4.,
qkv_bias=False,
qk_scale=None,
drop=0.,
attn_drop=0.,
drop_path=0.,
act_layer=nn.GELU,
norm_layer='nn.LayerNorm',
epsilon=1e-5):
super().__init__()
self.norm1 = eval(norm_layer)(dim, epsilon=epsilon)
# Multi-head Self-attention
self.attn = Attention(
dim,
num_heads=num_heads,
qkv_bias=qkv_bias,
qk_scale=qk_scale,
attn_drop=attn_drop,
proj_drop=drop)
# DropPath
self.drop_path = DropPath(drop_path) if drop_path > 0. else Identity()
self.norm2 = eval(norm_layer)(dim, epsilon=epsilon)
mlp_hidden_dim = int(dim * mlp_ratio)
self.mlp = Mlp(in_features=dim,
hidden_features=mlp_hidden_dim,
act_layer=act_layer,
drop=drop)
def forward(self, x):
# Multi-head Self-attention, Add, LayerNorm
x = x + self.drop_path(self.attn(self.norm1(x)))
# Feed Forward, Add, LayerNorm
x = x + self.drop_path(self.mlp(self.norm2(x)))
return x
好好观察,ViT 中使用 drop path 的时候,返回的是:
return x + self.drop_path(self.mlp(self.norm2(x)))
# 这种写法是错误的
# return self.drop_path(self.mlp(self.norm2(x)))
DropPath 类似于Dropout,不同的是 Drop将深度学习模型中的多分支结构随机 “失效”,而Dropout 是对神经元随机 “失效”。
注:很多文章里说,dropout / drop path 是随机“删除”一些神经元 / 分支结构。这种说法很容易误导,让人疑惑这个“删除”是怎么个删除法?实际上模型结构并不会改变,只是让这个神经元的输出结果置零。
在 dropout 中,比如原始网络的计算结构是:
y = a 1 ⋅ x 1 + a 2 ⋅ x 2 + a 3 ⋅ x 3 + a 4 ⋅ x 4 y=a_1\cdot x_1 + a_2\cdot x_2 + a_3\cdot x_3 + a_4\cdot x_4 y=a1⋅x1+a2⋅x2+a3⋅x3+a4⋅x4
令 dropout = 0.25,即每个神经元有 0.25 的概率,它的输出结果被置零。假设有个被失活的幸运儿是 a 1 a_1 a1,那么:
训练
y = 0 + a 2 ⋅ x 2 + a 3 ⋅ x 3 + a 4 ⋅ x 4 y=0 + a_2\cdot x_2 + a_3\cdot x_3 + a_4\cdot x_4 y=0+a2⋅x2+a3⋅x3+a4⋅x4
测试
y = 0.75 ∗ ( a 1 ⋅ x 1 + a 2 ⋅ x 2 + a 3 ⋅ x 3 + a 4 ⋅ x 4 ) y=0.75 * (a_1\cdot x_1 + a_2\cdot x_2 + a_3\cdot x_3 + a_4\cdot x_4) y=0.75∗(a1⋅x1+a2⋅x2+a3⋅x3+a4⋅x4)
(Drop Path 让部分 multi-head 失活??(待做实验验证)
随机丢失不会趋于一致性(平凡化?或者说趋同进化?)
指数移动平均(Exponential Moving Average)也叫权重移动平均(Weighted Moving Average),是一种给予近期数据更高权重的平均方法。
计算方法白话版本解说:
一个模型在训练的时候,维护两套参数。一套参数叫 θ \theta θ ,用梯度下降正常更新:
θ t = θ t − 1 − g t − 1 \theta_t = \theta_{t-1} - g_{t-1} θt=θt−1−gt−1
这里 g g g 是梯度
另一套参数叫影子参数,在这里记为 ν \nu ν,影子参数不用梯度更新,而是用 θ \theta θ 更新:
ν t = α ⋅ ν t − 1 + ( 1 − α ) ⋅ θ t \nu_t=\alpha\cdot \nu_{t-1} + (1-\alpha)\cdot\theta_t νt=α⋅νt−1+(1−α)⋅θt
然后导出训练结果的时候,不用 θ \theta θ 而是 ν \nu ν 。
原理的囫囵吞枣讲解,省略公式推导过程,直接看公式推导结果:
θ t = θ 1 − ∑ i = 1 t − 1 g i \theta_t=\theta_1-\sum^{t-1}_{i=1}g_i θt=θ1−i=1∑t−1gi
ν t = θ 1 − ∑ i = 1 t − 1 ( 1 − α t − 1 ) g i \nu_t=\theta_1-\sum^{t-1}_{i=1}\left(1-\alpha^{t-1}\right)g_i νt=θ1−i=1∑t−1(1−αt−1)gi
对比上下两个公式,很容易发现,差别就在于:每次通过梯度更新 ν \nu ν 这套参数的时候,加了个系数 ( 1 − α t − 1 ) \left(1-\alpha^{t-1}\right) (1−αt−1),使得:
也就是说,越是刚刚算出来的梯度( t t t 越小),对最终导出的结果影响越大。
基本的假设是,模型权重在最后的n步内,会在实际的最优点处抖动,所以我们取最后n步的平均,能使得模型更加的鲁棒。
想知道更详细的公式推导,请看:【炼丹技巧】指数移动平均(EMA)的原理及PyTorch实现
SGD是随机梯度下降(stochastic gradient descent)的首字母。设模型参数为 θ \theta θ,梯度为 g g g
θ t = θ t − 1 − g t ⋅ l r \theta_t = \theta_{t-1} - g_{t}\cdot lr θt=θt−1−gt⋅lr
momentum 动量值,帮助参数更新离开盆地:
g t ′ = β ⋅ g t − 1 + ( 1 − β ) ⋅ g t g_t'= \beta\cdot g_{t-1} + (1-\beta)\cdot g_t gt′=β⋅gt−1+(1−β)⋅gt
θ t = θ t − 1 − g t ′ ⋅ l r \theta_t =\theta_{t-1} - g_t'\cdot lr θt=θt−1−gt′⋅lr
来源:优化器:Adam与AdamW
Adam: Adam使用了动量来加速梯度下降,它引入了两个动量参数 β 1 \beta_1 β1(用于一阶矩估计)和 b e t a 2 beta_2 beta2 (用于二阶矩估计)。这些动量参数决定了过去梯度的影响程度。
小标题中的 eps (float, 可选) – 为了增加数值计算的稳定性而加到分母里的项(默认:1e-8)即代码中的 epsilon
# Adam 更新规则
m = beta1*m + (1-beta1)*grad
v = beta2*v + (1-beta2)*(grad**2)
theta = theta - learning_rate * m / (sqrt(v) + epsilon)
AdamW: Adam with Weight Decay Fix
# AdamW 更新规则
m = beta1*m + (1-beta1)*(grad + lamb*theta)
v = beta2*v + (1-beta2)*(grad**2)
theta = theta - learning_rate * m / (sqrt(v) + epsilon)
二者的区别在于,AdamW 中加入了 lamb
,即 λ \lambda λ,weight decay 的参数