43、优化器与回调函数:深入理解深度学习训练机制

优化器与回调函数:深入理解深度学习训练机制

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[完成回调函数使用];

综上所述,优化器和回调函数在深度学习训练中都起着至关重要的作用。优化器决定了模型参数更新的方式,不同的优化器适用于不同的场景;回调函数则提供了一种灵活的方式来定制训练过程,满足各种特殊需求。在实际应用中,需要根据具体情况选择合适的优化器和编写合适的回调函数,以达到最佳的训练效果。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值