opitimizers
1. 优化器算法
1.1 SGD 算法

没有动量的SGD算法:

θ←θ−lr∗g\theta \leftarrow \theta - lr * gθθlrg

其中θ\thetaθ是参数,lrlrlr是学习率,ggg是梯度。

有动量的SGD算法:

v←α∗v+lr∗gv \leftarrow \alpha * v + lr * gvαv+lrg

θ←θ−v\theta \leftarrow \theta - vθθv

其中vvv是速度。

加入动量以后首先学习速率会增加,并且能够在一定程度上缓解随机梯度下降法收敛不稳定的问题,并且有一定的摆脱陷入局部最优解的能力。

1.2 AdaGrad算法

算法:
r←r+g∗gr \leftarrow r + g * grr+gg

Δθ=lr∗geps+r\Delta \theta = \frac {lr * g} {eps + \sqrt {r}}Δθ=eps+rlrg

θ←θ−Δθ\theta \leftarrow \theta - \Delta \thetaθθΔθ

其中epsepseps是一个极小正数值,就是为了避免分母为零。

AdaGrad缩放参数,并且于幅度等于所有梯度历史平方值总和的平方根。所以在损失大偏导出学习速率快,损失偏导大的地方学习速率会小一些。

1.3 RMSProp算法

算法:
r←ρr+(1−ρ)g∗gr \leftarrow \rho r + (1 - \rho)g * grρr+(1ρ)gg

Δθ=lr∗geps+r\Delta \theta = \frac {lr * g} {eps + \sqrt {r}}Δθ=eps+rlrg

θ←θ−Δθ\theta \leftarrow \theta - \Delta \thetaθθΔθ

其中ρ\rhoρ是0-1的一个调节因子。

RMSProp算法比AdaGrad算法好的一点是在积累平方梯度时,可以调整过去积累和当前的比重。

1.4 Adam算法

算法:
t←t+1t \leftarrow t + 1tt+1

s←ρ1s+(1−ρ1)gs \leftarrow \rho_1 s + (1 - \rho_1) gsρ1s+(1ρ1)g

r←ρ2r+(1−ρ2)g2r \leftarrow \rho_2 r + (1 - \rho_2) g^2rρ2r+(1ρ2)g2

s^←s/(1−ρ1t)\hat s \leftarrow s /(1 - \rho_1^t)s^s/(1ρ1t)

r^←r/(1−ρ2t)\hat r \leftarrow r / (1 - \rho_2^t)r^r/(1ρ2t)

Δθ=s^eps+r^\Delta \theta = \frac {\hat s} {eps + \sqrt {\hat r}}Δθ=eps+r^s^

θ←θ−Δθ\theta \leftarrow \theta - \Delta \thetaθθΔθ

其中s,r,ρ1,ρ2s,r,\rho_1,\rho_2s,r,ρ1,ρ2分别是一阶矩,二阶矩,以及一阶矩和二阶矩的调节因子。

Adam是adaptive moments。它可以被看作结合RMAProp和具有动量的变种,此外Adam进行了偏置修正。

2. python实现优化器

具体的逻辑可以看一下代码

import numpy as np


class SGC(object):
    def __init__(self, lr=0.01, momentum=0.0):
        """
        A stochastic gradient descent optimizer.
        :param lr: 学习率,默认0.01
        :param momentum: 动量因子,初始化0.0
        :param cache: 由于下一次计算会用到上一次的动量,这个参数为了保存上一次的结果,也为了区分是具体什么地方使用
        """
        self.lr = lr
        self.momentum = momentum

        self.cache = {}

    def __str__(self):
        return 'SGD(lr={},momentum={})'.format(str(self.lr), str(self.momentum))

    def __call__(self, param, param_grad, param_name):
        self.update(param, param_grad, param_name)

    def update(self, param, param_grad, param_name):
        """
        更新参数
        计算速度更新:v_new = momentum * v_old + lr * param_grad
        计算参数更新:param_new = param_old - v_new
        :param param: 参数的原值,shape(m,n)
        :param param_grad: 一般是损失函数的梯度,shape(m,n)
        :param param_name: 一般是标示在哪个地方用到了,str
        :return: 更新以后的结果,shape(m,n)
        """

        # 如果是第一次执行,没有保存参数对应的动量,则直接计算
        if param_name not in self.cache:
            update = self.lr * param_grad
        else:
            # 在有动量的情况下计算更新值
            update = self.momentum * self.cache[param_name] + self.lr * param_grad
        # 保存更新结果,以便下一次继续计算
        self.cache[param_name] = update
        return param - update


class AdaGrad(object):
    def __init__(self, lr=0.01, eps=1e-7):
        """
        AdaGrad, 随着时间的积累,学习率会不断下降
        :param lr: 学习率,默认值0.01
        :param eps: 一个趋近于0的极小值,为了防止分母为零而报错
        :param cache: 由于下一次计算会用到上一次的动量,这个参数为了保存上一次的结果,也为了区分是具体什么地方使用
        """
        self.lr = lr
        self.eps = eps

        self.cache = {}

    def __str__(self):
        return 'AdaGrad(lr={})'.format(str(self.lr))

    def __call__(self, param, param_grad, param_name):
        return self.update(param, param_grad, param_name)

    def update(self, param, param_grad, param_name):
        """
        更新参数
        积累平方梯度:r_old = r_new + param_grad ** 2
        计算更新: update = lr * param_grad / sqrt(r_old)
        更新参数: param_old = param_old - update
        :param param: 参数的原值,shape(m,n)
        :param param_grad: 一般是损失函数的梯度,shape(m,n)
        :param param_name: 一般是标示在哪个地方用到了,str
        :return: 更新以后的结果,shape(m,n)
        """

        if param_name not in self.cache:
            self.cache[param_name] = np.zeros_like(param_grad)

        # 积累平方梯度:r_old = r_new + param_grad ** 2
        self.cache[param_name] += param_grad ** 2

        # 计算更新: update = lr * param_grad / sqrt(r_old)
        update = self.lr * param_grad / (np.sqrt(self.cache[param_name]) + self.eps)

        return param - update


class RMSProp(object):
    def __init__(self, lr=0.001, decay=0.9, eps=1e-7):
        """
        RMSProp
        :param lr: 学习率,默认值0.01
        :param eps: 一个趋近于0的极小值,为了防止分母为零而报错
        :param decay: 积累平方梯度是,一个对于当前梯度平方和过去梯度平方积累的调节因子
        """
        self.lr = lr
        self.decay = decay
        self.eps = eps

        self.cache = {}

    def __str__(self):
        return 'RMSProp(lr={},decay={})'.format(str(self.lr), str(self.decay))

    def __call__(self, param, param_grad, param_name):
        self.update(param, param_grad, param_name)

    def update(self, param, param_grad, param_name):
        """
        更新参数
        积累平方梯度:r_old = delay *r_new + (1-delay)param_grad ** 2
        计算更新: update = lr * param_grad / sqrt(r_old)
        更新参数: param_old = param_old - update
        :param param: 参数的原值,shape(m,n)
        :param param_grad: 一般是损失函数的梯度,shape(m,n)
        :param param_name: 一般是标示在哪个地方用到了,str
        :return: 更新以后的结果,shape(m,n)
        """
        if param_name not in self.cache:
            self.cache[param_name] = np.zeros_like(param_grad)

        # 积累平方梯度:r_old = delay *r_new + (1-delay)param_grad ** 2
        self.cache[param_name] = self.decay * self.cache[param_name] + (1 - self.decay) * param_grad ** 2

        # 计算更新: update = lr * param_grad / sqrt(r_old)
        update = self.lr * param_grad / (np.sqrt(self.cache[param_name]) + self.eps)

        return param - update


class Adam(object):
    def __init__(self, lr=0.001, decay1=0.9, decay2=0.999, eps=1e-7):
        """
        Adam
        :param lr: 学习率,默认值0.001
        :param decay1: 一阶矩的过去积累量和当前一阶矩的调整因子
        :param decay2: 二阶矩的过去积累量和当前二阶矩的调整因子
        :param eps: 一个趋近于0的极小值,为了防止分母为零而报错
        """
        self.lr = lr
        self.decay1 = decay1
        self.decay2 = decay2
        self.eps = eps

        self.cache = {}

    def __str__(self):
        return 'Adam(lr={},decay1={},decay2={})'.format(str(self.lr), str(self.decay1), str(self.decay2))

    def __call__(self, param, param_grad, param_name):
        self.update(param, param_grad, param_name)

    def update(self, param, param_grad, param_name):
        """
        更新参数
        时间因子: t_new = t_old + 1
        有偏一阶矩估计:mean_new = delay1 * mean_old + (1 - delay1) * param_grad
        有偏二阶矩估计:var_new = delay2 * var_old + (1 - delay2) * param_grad ** 2
        修正一阶矩偏差:mean_new = mean_new / (1 - delay1 ** t)
        修正二阶矩偏差:var_new = var_new / (1 - delay2 ** t)
        计算更新值:update = lr * mean_new / sqrt(var_new)
        :param param: 参数的原值,shape(m,n)
        :param param_grad: 一般是损失函数的梯度,shape(m,n)
        :param param_name: 一般是标示在哪个地方用到了,str
        :return: 更新以后的结果,shape(m,n)
        """
        if param_name not in self.cache:
            self.cache[param_name] = {
                "t": 0,
                "mean": np.zeros_like(param_grad),
                "var": np.zeros_like(param_grad),
            }
        # 时间因子: t_new = t_old + 1
        t = self.cache[param_name]["t"] + 1
        var = self.cache[param_name]["var"]
        mean = self.cache[param_name]["mean"]


        self.cache[param_name]["t"] = t

        # 有偏二阶矩估计:var_new = delay2 * var_old + (1 - delay2) * param_grad ** 2
        self.cache[param_name]["var"] = self.decay2 * var + (1 - self.decay2) * param_grad ** 2
        # 有偏一阶矩估计:mean_new = delay1 * mean_old + (1 - delay1) * param_grad
        self.cache[param_name]["mean"] = self.decay1 * mean + (1 - self.decay1) * param_grad

        # 修正二阶矩偏差:var_new = var_new / (1 - delay2 ** t)
        v_hat = self.cache[param_name]["var"] / (1 - self.decay2 ** t)
        # 修正二阶矩偏差:var_new = var_new / (1 - delay2 ** t)
        m_hat = self.cache[param_name]["mean"] / (1 - self.decay1 ** t)

        # 计算更新值:update = lr * mean_new / sqrt(var_new)
        update = self.lr * m_hat / (np.sqrt(v_hat) + self.eps)
        return param - update

参考资料:
《深度学习》 伊恩·古德菲洛等著
https://github.com/ddbourgin/numpy-ml/tree/master/numpy_ml/neural_nets/optimizers

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐