Loading...
首页专栏正文

【深度学习】新的深度学习优化器探索(协同优化)

 
1人已赏
Fizz 发布于 2021-06-17 17:33:50 浏览 2617 点赞 96 收藏 5

【深度学习】新的深度学习优化器探索(协同优化)

在这里插入图片描述

文章目录
1 RAdam VS Adam
2 自适应优化
3 LookAhead
    3.1 “侵入式”优化器
    3.2 LookAhead 中的参数:
4 RAdam 加 LookAhead 的一个实现:Ranger

1 RAdam VS Adam

1,目的 想找到一个比较好的优化器,能够在收敛速度和收敛的效果上都比较好。 目前sgd收敛较好,但是慢。 adam收敛快,但是容易收敛到局部解。 常用解决adam收敛问题的方法是,自适应启动方法。

2,adam方法的问题 adam在训练的初期,学习率的方差较大。

根本原因是因为缺少数据,导致方差大。

学习率的方差大,本质上自适应率的方差大。

可以控制自适应率的方差来改变效果。

3,Radam,控制自适应率的方差 一堆数学公式估计出自适应率的最大值和变化过程。

提出了Radam的优化过程

它兼有Adam和SGD两者的优点,既能保证收敛速度快,也不容易掉入局部最优解,而且收敛结果对学习率的初始值非常不敏感。在较大学习率的情况下,RAdam效果甚至还优于SGD。 在这里插入图片描述 RAdam意思是“整流版的Adam”(Rectified Adam),它能根据方差分散度,动态地打开或者关闭自适应学习率,并且提供了一种不需要可调参数学习率预热的方法。

一位Medium网友Less Wright在测试完RAdam算法后,给予了很高的评价:

RAdam可以说是最先进的AI优化器,可以永远取代原来的Adam算法了。

目前论文作者已将RAdam开源,FastAI现在已经集成了RAdam,只需几行代码即可直接调用。

总结:RAdam可以说是AI最新state of the art优化器

正如你所看到的,RAdam提供了一个动态启发式方法来提供自动化的方差衰减,从而消除了在训练期间warmup所涉及手动调优的需要。

此外,RAdam对学习速率变化(最重要的超参数)具有更强的鲁棒性,并在各种数据集和各种AI体系结构中提供更好的训练精度和泛化。

简而言之,我强烈建议您将RAdam放到你的AI架构中,看看你有没有立即获得好处。我愿意提供退款保证,但因为它的成本是0.00美元…

PyTorch的官方github提供了RAdam的实现:https://github.com/LiyuanLucasLiu/RAdam

FastAI用户可以很容易地使用RAdam如下:

在这里插入图片描述

导入RAdam,声明为一个局部函数(根据需要添加参数)

在这里插入图片描述

2 自适应优化

针对简单的SGD及Momentum存在的问题,2011年John Duchi等发布了AdaGrad优化算法(Adaptive Gradient,自适应梯度),它能够对每个不同的参数调整不同的学习率,对频繁变化的参数以更小的步长进行更新,而稀疏的参数以更大的步长进行更新。 在这里插入图片描述 2014年12月,Kingma和Lei Ba两位学者提出了Adam优化器,结合AdaGrad和RMSProp两种优化算法的优点。对梯度的一阶矩估计(First Moment Estimation,即梯度的均值)和二阶矩估计(Second

Moment Estimation,即梯度的未中心化的方差)进行综合考虑,计算出更新步长。

主要包含以下几个显著的优点:

  1. 实现简单,计算高效,对内存需求少

  2. 参数的更新不受梯度的伸缩变换影响

  3. 超参数具有很好的解释性,且通常无需调整或仅需很少的微调

  4. 更新的步长能够被限制在大致的范围内(初始学习率)

  5. 能自然地实现步长退火过程(自动调整学习率)

  6. 很适合应用于大规模的数据及参数的场景

  7. 适用于不稳定目标函数

  8. 适用于梯度稀疏或梯度存在很大噪声的问题

普通更新 最简单的沿着负梯度方向改变参数(梯度指向的是上升方向,但要最小化损失函数)。假设有一个参数向量x及其梯度dx,那么最简单的更新的形式是:# 普通更新

x += - learning_rate * dx

其中,learning_rate是一个超参数,它是一个固定常量。当在整个数据集上进行计算时,只要学习率足够低,总是能在损失函数上得到非负的进展。

动量(Momentum)更新该方法从物理角度上对于最优化问题得到的启发。损失值是山的高度(因此高度势能是 ,所以有 )。最优化过程可以看做是模拟参数向量(即质点)在地形上滚动的过程。因为作用于质点的力与梯度潜在能量( )有关,质点所受的力就是损失函数的(负)梯度。

在这里插入图片描述

v = mu * v - learning_rate * dx # 与速度融合
x += v # 与位置融合

梯度下降:我们知道曲面上方向导数的最大值的方向就代表了梯度的方向,因此我们在做梯度下降的时候,应该是沿着梯度的反方向进行权重的更新,可以有效的找到全局的最优解。

3 LookAhead

它源于论文《Lookahead Optimizer: k steps forward, 1 step back》,是最近才提出来的优化器,有意思的是大牛Hinton和Adam的作者之一Jimmy Ba也出现在了论文作者列表当中,有这两个大神加持,这个优化器的出现便吸引了不少目光。 Lookahead的思路很朴素,准确来说它并不是一个优化器,而是一个使用现有优化器的方案。简单来说它就是下面三个步骤的循环执行:

1、备份模型现有的权重θ;

2、从θ出发,用指定优化器更新k步,得到新权重θ̃ ;

3、更新模型权重为θ←θ+α(θ̃ −θ)。

3.1 “侵入式”优化器

上面实现优化器的方案是标准的,也就是按Keras的设计规范来做的,所以做起来很轻松。然而我曾经想要实现的一个优化器,却不能用这种方式来实现,经过阅读源码,得到了一种“侵入式”的写法,这种写法类似“外挂”的形式,可以实现我需要的功能,但不是标准的写法。

from keras.optimizers import Optimizer
from keras import backend as K

class InjectOptimizer(Optimizer):
    """定义注入式优化器的基类
        需要传入模型,直接修改模型的训练函数,而不按常规流程使用优化器,所以称为“侵入式”
        其实下面的大部分代码,都是直接抄自keras的源码:
        https://github.com/keras-team/keras/blob/master/keras/engine/training.py#L497
        也就是keras中的_make_train_function函数。
    """
    def get_updates(self, loss, params):
        return []
    def get_grouped_updates(self, loss, params):
        raise NotImplementedError
    def inject(self, model):
        """传入模型做注入
        """
        if not hasattr(model, 'train_function'):
            raise RuntimeError('You must compile your model before using it.')
        model._check_trainable_weights_consistency()
        if model.train_function is None:
            inputs = (model._feed_inputs +
                      model._feed_targets +
                      model._feed_sample_weights)
            if model._uses_dynamic_learning_phase():
                inputs += [K.learning_phase()]
            with K.name_scope('training'):
                train_functions = []
                with K.name_scope(model.optimizer.__class__.__name__):
                    grouped_training_updates = self.get_grouped_updates(
                        params=model._collected_trainable_weights,
                        loss=model.total_loss)
                for i, updates in enumerate(grouped_training_updates[1:]):
                    f = K.function(
                        inputs,
                        [model.total_loss],
                        updates=updates,
                        name='train_function_%s' % (i + 1),
                        **model._function_kwargs)
                    train_functions.append(f)
                # Gets loss and metrics. Updates weights at each call.
                first_updates = (model.updates +
                                 grouped_training_updates[0],
                                 model.metrics_updates)
                first_train = K.function(
                    inputs,
                    [model.total_loss] + model.metrics_tensors,
                    updates=first_updates,
                    name='train_function',
                    **model._function_kwargs)
                def F(inputs):
                    R = first_train(inputs)
                    for f in train_functions:
                        f(inputs)
                    return R
                model.train_function = F

class HeunOptimizer(InjectOptimizer):
    """Heun优化器
    ( https://en.wikipedia.org/wiki/Heun%27s_method )
    """
    def __init__(self, lr, **kwargs):
        super(HeunOptimizer, self).__init__(**kwargs)
        with K.name_scope(self.__class__.__name__):
            self.lr = K.variable(lr, name='lr')
    def get_updates_1(self, loss, params, cache_grads):
        updates = []
        grads = self.get_gradients(loss, params)
        for p, g, cg in zip(params, grads, cache_grads):
            updates.append(K.update(cg, g))
            updates.append(K.update(p, p - self.lr * g))
        return updates
    def get_updates_2(self, loss, params, cache_grads):
        updates = []
        grads = self.get_gradients(loss, params)
        for p, g, cg in zip(params, grads, cache_grads):
            updates.append(K.update(p, p - 0.5 * self.lr * (g - cg)))
        return updates
    def get_grouped_updates(self, loss, params):
        cache_grads = [K.zeros(K.int_shape(p)) for p in params]
        return [
            self.get_updates_1(loss, params, cache_grads),
            self.get_updates_2(loss, params, cache_grads)
        ]

用法是:

opt = HeunOptimizer(0.1)
model.compile(loss='mse', optimizer=opt)
opt.inject(model) # 必须执行这步

model.fit(x_train, y_train, epochs=100, batch_size=32)

用法就很简单了:

model.compile(optimizer=Adam(1e-3), loss='mse') # 用你想用的优化器
lookahead = Lookahead(k=5, alpha=0.5) # 初始化Lookahead
lookahead.inject(model) # 插入到模型中

至于效果,原论文中做了不少实验,有些有轻微提高(cifar10和cifar100那两个),有些提升还比较明显(LSTM做语言模型那个),我自己简单实验了一下,结果是没什么变化。我一直觉得优化器是一个很玄乎的存在,有时候非得SGD才能达到最优效果,有时候又非得Adam才能收敛得下去,总之不能指望单靠换一个优化器就能大幅度提升模型效果。Lookahead的出现,也就是让我们多一种选择罢了,训练时间充足的读者,多去尝试一下就好。

该研究表明这种更新机制能够有效地降低方差。研究者发现 Lookahead 对次优超参数没那么敏感,因此它对大规模调参的需求没有那么强。此外,使用 Lookahead 及其内部优化器(如 SGD 或 Adam),还能实现更快的收敛速度,因此计算开销也比较小。

研究者在多个实验中评估 Lookahead 的效果。比如在 CIFAR 和 ImageNet 数据集上训练分类器,并发现使用 Lookahead 后 ResNet-50 和 ResNet-152 架构都实现了更快速的收敛。

研究者还在 Penn Treebank 数据集上训练 LSTM 语言模型,在 WMT 2014 English-to-German 数据集上训练基于 Transformer 的神经机器翻译模型。在所有任务中,使用 Lookahead 算法能够实现更快的收敛、更好的泛化性能,且模型对超参数改变的鲁棒性更强。

这些实验表明 Lookahead 对内部循环优化器、fast weight 更新次数以及 slow weights 学习率的改变具备鲁棒性。

Lookahead Optimizer 怎么做

Lookahead 迭代地更新两组权重:slow weights φ 和 fast weights θ,前者在后者每更新 k 次后更新一次。Lookahead 将任意标准优化算法 A 作为内部优化器来更新 fast weights。

使用优化器 A 经过 k 次内部优化器更新后,Lookahead 通过在权重空间 θ − φ 中执行线性插值的方式更新 slow weights,方向为最后一个 fast weights。

slow weights 每更新一次,fast weights 将被重置为目前的 slow weights 值。Lookahead 的伪代码见下图 Algorithm 1。

在这里插入图片描述 其中最优化器 A 可能是 Adam 或 SGD 等最优化器,内部的 for 循环会用常规方法更新 fast weights θ,且每次更新的起始点都是从当前的 slow weights φ 开始。最终模型使用的参数也是慢更新那一套,因此快更新相当于做了一系列实验,然后慢更新再根据实验结果选一个比较好的方向,这有点类似 Nesterov Momentum 的思想。

看上去这只是一个小技巧?

3.2 LookAhead 中的参数:

k - 它控制快优化器的权重和 LookAhead 中的慢优化器的权重协同更新的间隔。默认值一般是 5 或者 6,不过 LookAhead 论文里最大也用过 20。 alpha - 它控制根据快慢优化器权重之差的多少比例来更新快优化器的权重。默认值是 0.5,LookAhead 论文作者 Hinton 等人在论文里给出了一个强有力的证明,表示 0.5 可能就是理想值。不过大家也可以做自己的尝试。 他们也在论文中指出,未来一个可能的改进方向是根据训练进行到不同的阶段,规划使用不同的 k 和 alpha 的值。

4 RAdam 加 LookAhead 的一个实现:Ranger

在解释过 LookAhead 的工作原理以后我们可以看出来,其中的那个快优化器可以选用任意一个现有的优化器。在 LookAhead 论文中他们使用的是最初的 Adam,毕竟那时候 RAdam 还没有发布呢。 在这里插入图片描述 那么显然,要实现 RAdam 加 LookAhead,只需要把原来的 LookAhead 中的 Adam 优化器替换成 RAdam 就可以了。

在 FastAI 中,合并 RAdam 和 LookAhead 的代码是一件非常容易的事情,他使用的 LookAhead 代码来自 LonePatient,RAdam 则来自论文作者们的官方代码。Less Wright 把合并后的这个新优化器称作 Ranger(其中的前两个字母 RA 来自 RAdam,Ranger 整个单词的意思“突击队员”则很好地体现出了 LookAhead 能出色地探索损失空间的特点)。

Ranger 的代码开源在 https://github.com/lessw2020/Ranger-Deep-Learning-Optimizer?source=post_page-----2dc83f79a48d----------------------

使用方法:

把 ranger.py 拷贝到工作目录下 import ranger

在这里插入图片描述

前端架构师,精通前端技术栈,物联网。了解云原生,容器编排。

*本文仅代表作者观点,不代表易百纳技术社区立场。系作者授权易百纳技术社区发表,未经许可不得转载。

精彩评论

内容存在敏感词
打赏
打赏作者
Fizz
您的支持将鼓励我继续创作!
金额:
¥1 ¥5 ¥10 ¥50 ¥100
支付方式:
微信支付
支付宝支付
微信支付
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

易百纳技术社区
确定要删除此文章、专栏、评论吗?
确定
取消
易百纳技术社区