梯度下降优化算法综述与PyTorch实现源码剖析

匿名 (未验证) 提交于 2019-12-02 23:45:01

现代的机器学习系统均利用大量的数据,利用梯度下降算法或者相关的变体进行训练。传统上,最早出现的优化算法是SGD,之后又陆续出现了AdaGrad、RMSprop、ADAM等变体,那么这些算法之间又有哪些区别和联系呢?本文试图对比的介绍目前常用的基于一阶梯度的优化算法,并给出它们的(PyTorch)实现。

SGD

随机梯度下降法(Stochastic Gradient Descent,SGD)是对传统的梯度下降算法(Gradient Descent,GD)进行的一种改进。在应用GD时,我们需要对整个训练集进行一次反向传播计算梯度后再进行参数更新,对系统的计算能力和内存的需求较高,而SGD在计算梯度更新参数时刚好相反,每次只使用整个训练集中的一个样本,因此具有更快地计算速度和较少的内存占用。同时,因为每次只使用一个样本更新参数,使得参数更新更加频繁,更新的参数间具有更高的方差,损失函数会朝不同的方向有较大的波动,这有助于发现新的极值点,避免优化器陷入一个局部极值点。但是也由于这种频繁的震荡,出现了一种折中的方法,即小批量(mini-batch)梯度下降法,每次只取训练集中一个batch的样本进行梯度的计算与参数更新,一般batch的大小为4的倍数。原始SGD的更新法则如下:θ=θηθJ(θ)(1)

传统的SGD在训练的过程中主要存在以下几个问题:

  1. 很难选择一个合适的学习速率,太小的学习速率导致算法收敛很慢,而太大的学习速率会导致在极值点附近震荡甚至错过,因此需要经过多次尝试。
  2. Learning rate schedules往往实现定义一个学习速率衰减表,比如每过多少step对学习速率进行decay,但是这些策略往往没法按照某个数据集的具体参数特性进行定制。
  3. 对于比较稀疏的数据,不同的特征出现的频率差别很大,如果所有的参数均使用一个相同的学习速率进行更新,这样做是不合理的。对于出现频率的特征,我们应该使用一个较大的学习速率。
  4. 深度神经网络之所以难以训练,并不是因为容易陷入局部最小值,而是在学习的过程中陷入到鞍点(saddle point),此时往各个方向的梯度几乎均为0。如果以二维平面为例,y=x3y=x3中x=0处即为一个鞍点。对于传统的SGD而言,一旦优化的过程中进入鞍点,就很难再离开这一位置。

Momentum

针对以上提到的第四点问题,可以通过增加动量(Momentum)的SGD进行缓解,加速优化函数的收敛。vt=γvt1ηθJ(θ)θ=θ+vt(2)γ,ηγ,η分别用来控制上次梯度方向和本次梯度方向对最终更新方向的贡献程度,其中γ(0,1]γ∈(0,1]在开始阶段常常被设置为0.5,当学习趋向稳定后,逐渐增加到0.9甚至更高。 可以把待优化的目标函数想象成一座山,在山顶将一个小球推下,小球在山坡上滚动的位置即系统的loss值,在往下滚动的过程中小球的动量不断增加,由于动量的存在,当小球滚动到山坡中较为平坦的地带时,小球将更容易越过这片地带继续往下滚而不是陷在这一区域停滞不前,并最终到达山谷。

图1 左:原始SGD 右:SGD+Momentum

Nesterov Accelerated Gradient

Its better to correct a mistake after you have made it!

目前我们有了一个带有动量的小球,但是这个小球在滚动的过程中总是随着山势的变化滚动,因此其行进的路径极不稳定。因此我们希望有一个更加“聪明”的小球,它不但拥有动量,而且能够知道自己将要去哪,这样当前面出现上坡小球能够进行减速。比如说,当接近坡底时,小球应该提前减速避免错过坡底。vt=γvt1ηθJ(θ+γvt1)θ=θ+vt(3)θθ计算梯度变为对θ+γvt1

图2 Momentum与NAG更新的区别

当然,在具体实现时,直接计算θ+γvt1

v_prev = v  #备份vt-1项 v = mu*v - lr * g  #这一步和传统的Momentum计算一样 p += -mu*v_prev + (1+mu)*v  #更新时真实的p应该为p-mu*v_prev,更新后为p-mu*v_prev+v,但是为了方便计算加上上次动量项的梯度,这里的p直接保存为p-mu*v_prev+v+mu*v,也就是p(小球)的“未来位置”。 

Momentum/NAG的实现和原始论文中的实现有些许的不用,具体的,在PyTorch实现中按照如下的公式更新梯度,其中ηη为learning rate,gθθ的梯度。目前尚不清楚为什么要做出这样的改变?vt=γvt1+gθ=θηvt(4)

def step(self, closure=None):     """Performs a single optimization step.      Arguments:         closure (callable, optional): A closure that reevaluates the model             and returns the loss.     """     loss = None     if closure is not None:         loss = closure()      for group in self.param_groups:         weight_decay = group['weight_decay']         momentum = group['momentum']         dampening = group['dampening']         nesterov = group['nesterov']          for p in group['params']:             if p.grad is None:                 continue             d_p = p.grad.data             if weight_decay != 0:                 d_p.add_(weight_decay, p.data)             if momentum != 0:   #动量项添加                 param_state = self.state[p]                 if 'momentum_buffer' not in param_state:                     buf = param_state['momentum_buffer'] = d_p.clone()                 else:                     buf = param_state['momentum_buffer']                     buf.mul_(momentum).add_(1 - dampening, d_p)                 if nesterov:    #如果使用NAG,则为t+1步先保存可能到达的“位置”                     d_p = d_p.add(momentum, buf)                 else:                     d_p = buf              p.data.add_(-group['lr'], d_p)      return loss  

AdaGrad

AdaGrad为的是解决传统的SGD对所有参数使用相同的学习速率的问题(即1.2节中提到的第三点问题)。它使用参数的历史梯度累计和去归一化该参数对应的学习速率。具体的,对于经常出现的参数,那么其梯度累积和较大,归一化的学习速率就较小。而对于不常见的参数,往往包含更多关于特征的信息,累积和较小,归一化后的学习速率较大,也即是学习算法应该更加关注这些罕见的特征的出现。Gt,ii=Gt1,ii+g2t,iθt+1,i=θt,iηGt,ii+gt,i(5)Gt,iiGt,ii这一项会越来越大,导致归一化的学习速率越来越小,这有可能导致优化函数在收敛之前就停止更新。

class torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0)

def step(self, closure=None):         """Performs a single optimization step.          Arguments:             closure (callable, optional): A closure that reevaluates the model                 and returns the loss.         """         loss = None         if closure is not None:             loss = closure()          for group in self.param_groups:             for p in group['params']:                 if p.grad is None:                     continue                  grad = p.grad.data                 state = self.state[p]                  state['step'] += 1                  if group['weight_decay'] != 0:                     if p.grad.data.is_sparse:                         raise RuntimeError("weight_decay option is not compatible with sparse gradients ")                     grad = grad.add(group['weight_decay'], p.data)                  clr = group['lr'] / (1 + (state['step'] - 1) * group['lr_decay'])                  if p.grad.data.is_sparse:                     grad = grad.coalesce()  # the update is non-linear so indices must be unique                     grad_indices = grad._indices()                     grad_values = grad._values()                     size = torch.Size([x for x in grad.size()])                      def make_sparse(values):                         constructor = type(p.grad.data)                         if grad_indices.dim() == 0 or values.dim() == 0:                             return constructor()                         return constructor(grad_indices, values, size)                     state['sum'].add_(make_sparse(grad_values.pow(2)))                     std = state['sum']._sparse_mask(grad)                     std_values = std._values().sqrt_().add_(1e-10)                     p.data.add_(-clr, make_sparse(grad_values / std_values))                 else:                     state['sum'].addcmul_(1, grad, grad)    #更新核心部分                     std = state['sum'].sqrt().add_(1e-10)                     p.data.addcdiv_(-clr, grad, std)          return loss  

Adadelta

为了避免AdaGrad存在的学习过早停止的问题,Adadelta不再保存过去所有时刻的梯度和,而是采用decaying average的方法平滑过去的梯度值和参数值。

图3 Adadelta伪代码描述

其中E[g2]tE[g2]t存储的是历史梯度平方的平滑值,此外,这里还需要对历史的参数值的平方进行decaying average,也就是E[Δx2]t=ρE[Δx2]t1+(1ρ)Δx2tRMS[Δx]t=E[Δx2]t+RMS[g]t=E[g2]t+xt+1=

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!