收藏 | 从SGD到NadaMax,深度学习十种优化算法原理及实现
点击上方“小白学视觉”,选择加"星标"或“置顶”
重磅干货,第一时间送达
无论是什么优化算法,最后都可以用一个简单的公式抽象:

是参数,而
是参数的增量,而各种优化算法的主要区别在于对
的计算不同,本文总结了下面十个优化算法的公式,以及简单的Python实现:
SGD
Momentum
Nesterov Momentum
AdaGrad
RMSProp
AdaDelta
Adam
AdaMax
Nadam
NadaMax
SGD
虽然有凑数的嫌疑,不过还是把SGD也顺带说一下,就算做一个符号说明了。常规的随机梯度下降公式如下:

其中
是学习率,
是损失关于参数的梯度(有的资料中会写成
等形式),不过相比SGD,用的更多的还是小批量梯度下降(mBGD)算法,不同之处在于一次训练使用多个样本,然后取所有参与训练样本梯度的平均来更新参数,公式如下:

其中
是第
次训练中
个样本损失关于参数梯度的均值,如无特别声明,下文所出现
也遵循该定义
另外
或者
在下面的优化算法中,只是作为一个传入的变量,其具体的计算是由其他模块负责,可以参考下面两个链接:
Numpy实现神经网络框架(3)——线性层反向传播推导及实现
卷积核梯度计算的推导及实现
Momentum

的值,而垂直方向对应为损失。设其质量
,在第
时刻,在单位时间内,该质点受外力而造成的动量改变为:
,所以约去了。另外受到的外力可以分为两个分量:重力沿斜面向下的力
和粘性阻尼力 




,另外
的方向与损失的梯度方向相反,并取系数为
,得到:


足够大时,
趋近于 
import numpy as np
class Momentum(object):
def __init__(self, alpha=0.9, lr=1e-3):
self.alpha = alpha # 动量系数
self.lr = lr # 学习率
self.v = 0 # 初始速度为0
def update(self, g: np.ndarray): # g = J'(w) 为本轮训练参数的梯度
self.v = self.alpha * self.v - self.lr * g # 公式
return self.v # 返回的是参数的增量,下同

,刚开始时
会比期望值要小,需要进行修正,下面的Adam等算法会使用该方式
Nesterov Momentum
更新一遍参数,得到一个临时参数
,然后使用这个临时参数计算本轮训练的梯度。相当于是小球预判了自己下一时刻的位置,并提前使用该位置的梯度更新 :
的更新过程:


,所以可以之前使用Momentum的代码
AdaGrad

是一个极小的正数,用来防止除0,而
,
是矩阵的哈达玛积运算符,另外,本文中矩阵的平方或者两矩阵相乘都是计算哈达玛积,而不是计算矩阵乘法
会越来越大,整体的学习率会越来越小。所以,一般来说AdaGrad算法一开始是激励收敛,到了后面就慢慢变成惩罚收敛,速度越来越慢
展开得到:
,所以在第一次训练时(2.2)式为:
的值是不确定的,所以要防止处0,但是可以令
,这样就可以在(2.2)式中去掉

代入(2.3)式,可以得到:
恒大于0,因此不必在计算
中额外加入
,代码如下:class AdaGrad(object):
def __init__(self, eps=1e-8, lr=1e-3):
self.r = eps # r_0 = epsilon
self.lr = lr
def update(self, g: np.ndarray):
r = r + np.square(g)
return -self.lr * g / np.sqrt(r)
RMSProp
的计算不同,先看公式
展开后的公式:
,从而去掉计算
时的
,实现代码:class RMSProp(object):
def __init__(self, lr=1e-3, beta=0.999, eps=1e-8):
self.r = eps
self.lr = lr
self.beta = beta
def update(self, g: np.ndarray):
r = r * self.beta + (1-self.beta) * np.square(g)
return -self.lr * g / np.sqrt(r)
AdaDelta

,这是该算法的一大优势。除了同样以
来累积梯度的信息之外,该算法还多了一个
以指数衰减的形式来累积
的信息
,得到:
class AdaDelta(object):
def __init__(self, beta=0.999, eps=1e-8):
self.r = eps
self.s = eps
self.beta = beta
def update(self, g: np.ndarray):
g_square = (1-self.beta) * np.square(g) # (1-beta)*g^2
r = r * self.beta + g_square
frac = s / r
res = -np.sqrt(frac) * g
s = s * self.beta + frac * g_squaretmp # 少一次乘法。。。
return res


Adam

计算
却先经过(4.3)和(4.4)式是因为通常会设
,所以此时梯度的一阶矩估计和二阶矩估是有偏的,需要进行修正

,可知当
足够大时修正将不起作用(也不需要修正了):
class Adam(object):
def __init__(self, lr=1e-3, alpha=0.9, beta=0.999, eps=1e-8):
self.s = 0
self.r = eps
self.lr = lr
self.alpha = alpha
self.beta = beta
self.alpha_i = 1
self.beta_i = 1
def update(self, g: np.ndarray):
self.s = self.s * self.alpha + (1-self.alpha) * g
self.r = self.r * self.beta + (1-self.beta) * np.square(g)
self.alpha_i *= self.alpha
self.beta_i *= self.beta_i
lr = -self.lr * (1-self.beta_i)**0.5 / (1-self.alpha_i)
return lr * self.s / np.sqrt(self.r)
AdaMax
的展开式并且令
,得到:
的
范数,也就是说
的各维度的增量是根据该维度上梯度的
范数的累积量进行缩放的。如果用
范数替代就得到了Adam的不同变种,不过其中
范数对应的变种算法简单且稳定
范数,第
轮训练时梯度的累积为:

:

是累积的梯度各个分量的绝对值最大值,所以直接用做分母且不需要修正,代码如下:class AdaMax(object):
def __init__(self, lr=1e-3, alpha=0.9, beta=0.999):
self.s = 0
self.r = 0
self.lr = lr
self.alpha = alpha
self.alpha_i = 1
self.beta = beta
def update(self, g: np.ndarray):
self.s = self.s * self.alpha + (1-self.alpha) * g
self.r = np.maximum(self.r*self.beta, np.abs(g))
self.alpha_i *= self.alpha
lr = -self.lr / (1-self.alpha_i)
return lr * self.s / self.r
Nadam






替换成
,得到:

,消去(5.8)式种的
:
class Nadam(object):
def __init__(self, lr=1e-3, alpha=0.9, beta=0.999, eps=1e-8):
self.s = 0
self.r = eps
self.lr = lr
self.alpha = alpha
self.beta = beta
self.alpha_i = 1
self.beta_i = 1
def update(self, g: np.ndarray):
self.s = self.s * self.alpha + (1-self.alpha) * g
self.r = self.r * self.beta + (1-self.beta) * np.square(g)
self.alpha_i *= self.alpha
self.beta_i *= self.beta_i
lr = -self.lr * (1-self.beta_i)**0.5 / (1-self.alpha_i)
return lr * (self.s * self.alpha + (1-self.alpha) * g) / np.sqrt(self.r)




class NadaMax(object):
def __init__(self, lr=1e-3, alpha=0.9, beta=0.999):
self.s = 0
self.r = 0
self.lr = lr
self.alpha = alpha
self.alpha_i = 1
self.beta = beta
def update(self, g: np.ndarray):
self.s = self.s * self.alpha + (1-self.alpha) * g
self.r = np.maximum(self.r*self.beta, np.abs(g))
self.alpha_i *= self.alpha
lr = -self.lr / (1-self.alpha_i)
return lr * (self.s * self.alpha + (1-self.alpha) * g) / self.r
好消息!
小白学视觉知识星球
开始面向外开放啦👇👇👇
下载1:OpenCV-Contrib扩展模块中文版教程 在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。 下载2:Python视觉实战项目52讲 在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。 下载3:OpenCV实战项目20讲 在「小白学视觉」公众号后台回复:OpenCV实战项目20讲,即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。 交流群
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~
评论

