优化器与回调函数:深入理解深度学习训练机制
1. 优化器基础
1.1 随机梯度下降(SGD)实验
首先进行了SGD的实验,以下是训练过程中的相关数据:
| epoch | train_loss | valid_loss | accuracy | time |
| — | — | — | — | — |
| 0 | 2.730918 | 2.009971 | 0.332739 | 00:09 |
| 1 | 2.204893 | 1.747202 | 0.441529 | 00:09 |
| 2 | 1.875621 | 1.684515 | 0.445350 | 00:09 |
从这些数据可以看出,随着训练轮数的增加,训练损失和验证损失都在逐渐降低,准确率也有所提升,说明SGD在一定程度上是有效的。
1.2 动量(Momentum)
1.2.1 动量的概念
SGD可以类比为站在山顶,每次朝着最陡峭的坡度方向走一步下山。而引入动量的概念后,就像是一个球从山上滚下,它不会在每个点都严格遵循梯度的方向,因为它具有动量。动量较大的球(如较重的球)会跳过小的凸起和坑洼,更有可能到达崎岖山脉的底部;而乒乓球则容易陷入每个小裂缝中。
1.2.2 动量的实现
在SGD中引入动量,我们使用移动平均而不是仅使用当前梯度来进行参数更新:
weight.avg = beta * weight.avg + (1-beta) * weight.grad
new_weight = weight - lr * weight.avg
其中,
beta
是我们选择的一个参数,用于定义动量的大小。如果
beta
为 0,则第一个方程变为
weight.avg = weight.grad
,此时就相当于普通的SGD;如果
beta
接近 1,则主要选择的方向是之前步骤的平均值。
1.2.3 动量的效果
动量在损失函数有狭窄峡谷时表现特别好。普通的SGD会让我们在峡谷两侧来回跳动,而带有动量的SGD会对这些跳动进行平均,从而平稳地沿着一侧滚动。
beta
参数决定了我们使用的动量强度:较小的
beta
会使我们更接近实际的梯度值;而较大的
beta
会使我们主要朝着梯度的平均值方向前进,并且在梯度发生变化后,趋势需要一段时间才能改变。
1.2.4 动量的代码实现
为了在优化器中添加动量,我们需要跟踪移动平均梯度,这可以通过一个回调函数来实现:
def average_grad(p, mom, grad_avg=None, **kwargs):
if grad_avg is None: grad_avg = torch.zeros_like(p.grad.data)
return {'grad_avg': grad_avg*mom + p.grad.data}
def momentum_step(p, lr, grad_avg, **kwargs): p.data.add_(-lr, grad_avg)
opt_func = partial(Optimizer, cbs=[average_grad,momentum_step], mom=0.9)
learn = get_learner(opt_func=opt_func)
learn.fit_one_cycle(3, 0.03)
以下是添加动量后训练的相关数据:
| epoch | train_loss | valid_loss | accuracy | time |
| — | — | — | — | — |
| 0 | 2.856000 | 2.493429 | 0.246115 | 00:10 |
| 1 | 2.504205 | 2.463813 | 0.348280 | 00:10 |
| 2 | 2.187387 | 1.755670 | 0.418853 | 00:10 |
从数据来看,虽然训练效果有一定提升,但还不是很理想。
1.3 RMSProp
1.3.1 RMSProp的原理
RMSProp是SGD的另一种变体,由Geoffrey Hinton引入。它与SGD的主要区别在于使用了自适应学习率,即每个参数都有自己特定的学习率,由全局学习率控制。这样,我们可以通过给需要大量改变的权重更高的学习率,而给已经足够好的权重较低的学习率来加速训练。
1.3.2 自适应学习率的确定
为了确定哪些参数应该有高学习率,哪些应该有低学习率,我们可以观察梯度。如果一个参数的梯度在一段时间内接近零,说明损失是平坦的,该参数需要更高的学习率;如果梯度变化很大,我们应该小心选择低学习率以避免发散。我们使用梯度平方的移动平均来确定噪声背后的总体趋势,然后用当前梯度除以这个移动平均的平方根来更新相应的权重:
w.square_avg = alpha * w.square_avg + (1-alpha) * (w.grad ** 2)
new_w = w - lr * w.grad / math.sqrt(w.square_avg + eps)
其中,
eps
是为了数值稳定性而添加的(通常设置为 1e-8),
alpha
的默认值通常为 0.99。
1.3.3 RMSProp的代码实现
def average_sqr_grad(p, sqr_mom, sqr_avg=None, **kwargs):
if sqr_avg is None: sqr_avg = torch.zeros_like(p.grad.data)
return {'sqr_avg': sqr_avg*sqr_mom + p.grad.data**2}
def rms_prop_step(p, lr, sqr_avg, eps, grad_avg=None, **kwargs):
denom = sqr_avg.sqrt().add_(eps)
p.data.addcdiv_(-lr, p.grad, denom)
opt_func = partial(Optimizer, cbs=[average_sqr_grad,rms_prop_step], sqr_mom=0.99, eps=1e-7)
learn = get_learner(opt_func=opt_func)
learn.fit_one_cycle(3, 0.003)
以下是使用RMSProp训练的相关数据:
| epoch | train_loss | valid_loss | accuracy | time |
| — | — | — | — | — |
| 0 | 2.766912 | 1.845900 | 0.402548 | 00:11 |
| 1 | 2.194586 | 1.510269 | 0.504459 | 00:11 |
| 2 | 1.869099 | 1.447939 | 0.544968 | 00:11 |
可以看到,使用RMSProp后,训练效果有了明显的提升。
1.4 Adam
1.4.1 Adam的原理
Adam结合了SGD与动量和RMSProp的思想:它使用梯度的移动平均作为方向,并除以梯度平方的移动平均的平方根,为每个参数提供自适应学习率。
1.4.2 Adam的更新步骤
w.avg = beta1 * w.avg + (1-beta1) * w.grad
unbias_avg = w.avg / (1 - (beta1**(i+1)))
w.sqr_avg = beta2 * w.sqr_avg + (1-beta2) * (w.grad ** 2)
new_w = w - lr * unbias_avg / sqrt(w.sqr_avg + eps)
其中,
eps
通常设置为 1e-8,文献中建议的
(beta1, beta2)
默认值为
(0.9, 0.999)
。在实际应用中,发现
beta2 = 0.99
更适合我们使用的调度类型。
beta1
是动量参数,在调用
fit_one_cycle
时可以通过
moms
参数指定。
eps
不仅用于数值稳定性,较高的
eps
还会限制调整后学习率的最大值。
1.5 解耦权重衰减(Decoupled Weight Decay)
1.5.1 权重衰减的概念
权重衰减在普通SGD中相当于使用以下公式更新参数:
new_weight = weight - lr*weight.grad - lr*wd*weight
其中,最后一部分解释了这种技术的名称:每个权重都会以
lr * wd
的因子进行衰减。权重衰减也被称为L2正则化,即向损失中添加所有权重平方的和(乘以权重衰减因子)。
1.5.2 不同优化器下的权重衰减
对于SGD,这两种公式是等价的。但对于带有动量、RMSProp或Adam的优化器,这种等价性仅适用于标准SGD,因为在这些优化器中,参数更新在梯度周围有一些额外的公式。大多数库使用第二种公式,但有研究指出,对于Adam优化器或带有动量的优化器,第一种方法才是正确的,因此在实际应用中通常采用第一种方法。
1.6 优化器总结
不同优化器的特点总结如下:
| 优化器 | 特点 |
| — | — |
| SGD | 基本的优化算法,简单直接,但在处理复杂问题时可能收敛较慢 |
| 带有动量的SGD | 引入动量概念,有助于跳过局部极小值,加速收敛 |
| RMSProp | 使用自适应学习率,根据参数的梯度情况动态调整学习率 |
| Adam | 结合了动量和自适应学习率的优点,收敛速度快,是常用的优化器 |
1.7 优化器选择的mermaid流程图
graph TD;
A[开始] --> B{数据规模小?};
B -- 是 --> C[SGD];
B -- 否 --> D{损失函数复杂?};
D -- 是 --> E{需要快速收敛?};
E -- 是 --> F[Adam];
E -- 否 --> G[带有动量的SGD];
D -- 否 --> H[RMSProp];
以上就是关于优化器的详细介绍,不同的优化器适用于不同的场景,在实际应用中需要根据具体情况进行选择。同时,优化器只是训练过程的一部分,接下来我们将介绍如何通过回调函数来定制训练过程。
2. 回调函数(Callbacks)
2.1 回调函数的概念与作用
在深度学习训练中,有时需要对训练过程进行一些小的调整,如Mixup、fp16训练、训练RNN时每个epoch后重置模型等。传统的深度学习从业者定制训练循环的方式是复制现有的训练循环,然后插入所需的代码,但这种方式存在很多问题,例如难以满足特定需求,不同的修改之间可能无法协同工作等。
为了解决这些问题,引入了回调函数的概念。回调函数是一段可以在预定义的点插入到另一段代码中的代码。在之前的库中,回调函数的功能有限,只能在少数几个地方插入代码,且不能满足所有需求。而在实际应用中,回调函数需要能够读取训练循环中的所有信息,根据需要修改这些信息,并完全控制批处理、epoch或整个训练循环的终止。
2.2 带有回调函数的训练循环
实际应用中的训练循环经过修改后,在每个步骤之后都会调用回调函数,回调函数可以接收整个训练状态并进行修改。以下是训练循环中每个批次的实际代码:
try:
self._split(b);
self('begin_batch')
self.pred = self.model(*self.xb);
self('after_pred')
self.loss = self.loss_func(self.pred, *self.yb);
self('after_loss')
if not self.training: return
self.loss.backward();
self('after_backward')
self.opt.step();
self('after_step')
self.opt.zero_grad()
except CancelBatchException:
self('after_cancel_batch')
finally:
self('after_batch')
在上述代码中,
self('...')
形式的调用就是调用回调函数的地方。
2.3 可用的回调事件
当编写自己的回调函数时,可用的事件列表如下:
| 事件 | 说明 |
| — | — |
| begin_fit | 在开始训练前调用,适合进行初始设置 |
| begin_epoch | 在每个epoch开始时调用,可用于重置每个epoch所需的行为 |
| begin_train | 在每个epoch的训练阶段开始时调用 |
| begin_batch | 在每个批次开始时调用,可用于进行批次所需的设置或更改输入/目标 |
| after_pred | 在计算模型对批次的输出后调用,可用于在将输出输入到损失函数之前进行更改 |
| after_loss | 在计算损失后但在反向传播之前调用,可用于向损失添加惩罚项 |
| after_backward | 在反向传播后但在参数更新之前调用,可用于在更新之前更改梯度 |
| after_step | 在参数更新后但在梯度清零之前调用 |
| after_batch | 在每个批次结束时调用,用于进行下一批次之前的清理工作 |
| after_train | 在每个epoch的训练阶段结束时调用 |
| begin_validate | 在每个epoch的验证阶段开始时调用,可用于验证所需的设置 |
| after_validate | 在每个epoch的验证阶段结束时调用 |
| after_epoch | 在每个epoch结束时调用,用于进行下一个epoch之前的清理工作 |
| after_fit | 在训练结束时调用,用于最终清理工作 |
2.4 创建回调函数的示例
2.4.1 ModelResetter回调函数
以下是
ModelResetter
回调函数的代码:
class ModelResetter(Callback):
def begin_train(self): self.model.reset()
def begin_validate(self): self.model.reset()
这个回调函数的作用是在每个epoch的训练和验证开始时调用模型的
reset
方法。
2.4.2 RNNRegularizer回调函数
以下是
RNNRegularizer
回调函数的代码:
class RNNRegularizer(Callback):
def __init__(self, alpha=0., beta=0.): self.alpha,self.beta = alpha,beta
def after_pred(self):
self.raw_out,self.out = self.pred[1],self.pred[2]
self.learn.pred = self.pred[0]
def after_loss(self):
if not self.training: return
if self.alpha != 0.:
self.learn.loss += self.alpha * self.out[-1].float().pow(2).mean()
if self.beta != 0.:
h = self.raw_out[-1]
if len(h)>1:
self.learn.loss += self.beta * (h[:,1:] - h[:,:-1]).float().pow(2).mean()
这个回调函数用于添加RNN正则化(AR和TAR)。在
after_pred
事件中,将模型的输出进行处理;在
after_loss
事件中,根据
alpha
和
beta
的值向损失中添加惩罚项。
2.5 回调函数中可访问的Learner属性
在编写回调函数时,可以访问
Learner
的以下属性:
-
模型相关
:
-
model
:用于训练/验证的模型。
-
数据相关
:
-
data
:底层的
DataLoaders
。
-
dl
:当前用于迭代的
DataLoader
。
-
x/xb
:从
self.dl
中获取的最后一个输入(可能被回调函数修改)。
xb
始终是一个元组,
x
是解元组后的结果,只能对
xb
进行赋值。
-
y/yb
:从
self.dl
中获取的最后一个目标(可能被回调函数修改)。
yb
始终是一个元组,
y
是解元组后的结果,只能对
yb
进行赋值。
-
训练相关
:
-
loss_func
:使用的损失函数。
-
opt
:用于更新模型参数的优化器。
-
opt_func
:用于创建优化器的函数。
-
cbs
:包含所有回调函数的列表。
-
pred
:模型的最后一次预测(可能被回调函数修改)。
-
loss
:最后计算的损失(可能被回调函数修改)。
-
n_epoch
:本次训练的epoch数。
-
n_iter
:当前
self.dl
中的迭代次数。
-
epoch
:当前的epoch索引(从0到
n_epoch - 1
)。
-
iter
:当前在
self.dl
中的迭代索引(从0到
n_iter - 1
)。
-
其他
:
-
train_iter
:自本次训练开始以来完成的训练迭代次数。
-
pct_train
:完成的训练迭代百分比(从0到1)。
-
training
:一个标志,指示是否处于训练模式。
-
smooth_loss
:训练损失的指数平均版本。
2.6 回调函数使用流程的mermaid流程图
graph TD;
A[开始编写回调函数] --> B{选择事件};
B --> C[定义回调逻辑];
C --> D[访问Learner属性];
D --> E[修改训练状态];
E --> F[插入到训练循环中];
F --> G[完成回调函数使用];
综上所述,优化器和回调函数在深度学习训练中都起着至关重要的作用。优化器决定了模型参数更新的方式,不同的优化器适用于不同的场景;回调函数则提供了一种灵活的方式来定制训练过程,满足各种特殊需求。在实际应用中,需要根据具体情况选择合适的优化器和编写合适的回调函数,以达到最佳的训练效果。
超级会员免费看

被折叠的 条评论
为什么被折叠?



