SGD

由于本人知识水平不高,所以下列讲解可能比较详细或者简单,如有错误,还望斧正 ——20170729
文章部分内容属于CS231n课程笔记翻译:最优化笔记(下)知乎发布,其相应文段权利属于该作者
An overview of gradient descent optimization algorithms及译文,Sebastian Ruder,zhiyong_will

梯度下降是如今最好的优化算法之一,也是至今在神经网络里运用最广的一种优化算法。同时,每种最先进的深度学习库都包含了各种不同的算法应用于优化梯度下降,如caffe,tf,keras等。然而这些算法经常被用作一个黑匣子优化算子,这也导致了对它的优缺点解释起来比较麻烦。下面我们将从最底层开始剖析梯度下降法。

目的

$$\theta = \theta - \eta \cdot \nabla_\theta J(\theta) $$

1.梯度下降概念

梯度下降就是一种用来最小化一个客观方程 J ( θ ) J(\theta) J(θ)的方式,其中参数 θ ∈ R d θ∈R^d θRd,这些参数的更新原则是根据对于函数 J ( θ ) J(\theta) J(θ)的梯度 ∇ θ J ( θ ) \nabla_\theta J(\theta) θJ(θ)的反方向更新。学习率 η \eta η决定了我们向(局部)最小值步进的步长大小。换而言之,我们顺着该 J ( θ ) J(\theta) J(θ)函数的空间面坡度下滑直到我们到达一个谷底。

2.GD(gradient descent)

GD即是大家都比较熟悉的梯度下降法,计算梯度有两种方法:一个是缓慢的近似方法(数值梯度法),但实现相对简单。另一个方法(分析梯度法)计算迅速,结果精确,但是实现时容易出错,且需要使用微分。

数值梯度法(the numeric gradient)

梯度的计算为:
基础梯度公式

$$ \frac{df(x)}{dx}=\lim_{x\to 0}\frac{f(x+h)-f(x)}{h} $$

中心差值公式

$$ \frac{df(x)}{dx}=\lim_{x\to 0}\frac{f(x+h)-f(x-h)}{2h} $$

Python 示例:
@ Standford CS231n [Optimization Note](http://cs231n.github.io/optimization-1/)

/**
* 以下代码以基础梯度公式为例
*/
def eval_numerical_gradient(f, x):
  """  
  一个f在x处的数值梯度法的简单实现
  - f是只有一个参数的函数
  - x是计算梯度的点
  """ 

  fx = f(x) # 在原点计算函数值
  grad = np.zeros(x.shape)
  h = 0.00001

  # 对x中所有的索引进行迭代
  it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
  while not it.finished:

    # 计算x+h处的函数值
    ix = it.multi_index
    old_value = x[ix]
    x[ix] = old_value + h # 增加h
    fxh = f(x) # 计算f(x + h)
    x[ix] = old_value # 存到前一个值中 (非常重要)

    # 计算偏导数
    grad[ix] = (fxh - fx) / h # 坡度
    it.iternext() # 到下个维度

  return grad

该代码中h根据微分原理取值应该越小越好,建议值为 1 e − 5 1e^{-5} 1e5左右。而且中心差值梯度效果也肯定比基础梯度效果好,但是相应的计算量也会增大。

##微分分析梯度法(the analytic gradients)
用SVM的损失函数在某个数据点上的计算来举例:

$$L_i=\sum_{j\neq y_i}[max(0,w_j^Tx_i-w_{y_i}^Tx_i+\Delta)] $$

对函数进行微分,比如,对 w y i w_{y_i} wyi进行微分可得到:

$$\nabla_{w_{y_i}} L_i=-(\sum_{j\neq y_i}1(w_j^Tx_i-w_{y_i}^Tx_i+\Delta>0))x_i $$

其中1是一个示性函数,如果括号中的条件为真,那么函数值为1,如果为假,则函数值为0。虽然上述公式看起来复杂,但在代码实现的时候比较简单:只需要计算没有满足边界值的分类的数量(因此对损失函数产生了贡献),然后乘以 x i x_i xi就是梯度了。注意,这个梯度只是对应正确分类的W的行向量的梯度,那些 j ≠ y i j\neq y_i j̸=yi行的梯度是:

$$\nabla_{w_j} L_i=1(w_j^Tx_i-w_{y_i}^Tx_i+\Delta>0)x_i $$

利用微分来分析,能得到计算梯度的公式(不是近似),用公式计算梯度速度很快,唯一不好的就是实现的时候容易出错。为了解决这个问题,在实际操作时常常将分析梯度法的结果和数值梯度法的结果作比较,以此来检查其实现的正确性,这个步骤叫做梯度检查

看完梯度的计算我们来举例实践下微分分析梯度法:

# import the necessary packages
import matplotlib.pyplot as plt
from sklearn.datasets.samples_generator import make_blobs
import numpy as np
import argparse


def sigmoid_activation(x):
    # compute and return the sigmoid activation value for a
    # given input value
    return 1.0 / (1 + np.exp(-x))

# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-e", "--epochs", type=float, default=100,
	help="# of epochs")
ap.add_argument("-a", "--alpha", type=float, default=0.01,
	help="learning rate")
args = vars(ap.parse_args())

# generate a 2-class classification problem with 250 data points,
# where each data point is a 2D feature vector
(X, y) = make_blobs(n_samples=250, n_features=2, centers=2,
                    cluster_std=1.05, random_state=20)

# insert a column of 1's as the first entry in the feature
# vector -- this is a little trick that allows us to treat
# the bias as a trainable parameter *within* the weight matrix
# rather than an entirely separate variable
X = np.c_[np.ones((X.shape[0])), X]

# initialize our weight matrix such it has the same number of
# columns as our input features
print("[INFO] starting training...")
W = np.random.uniform(size=(X.shape[1],))

# initialize a list to store the loss value for each epoch
lossHistory = []

# loop over the desired number of epochs
for epoch in np.arange(0, args["epochs"]):
    # take the dot product between our features `X` and the
    # weight matrix `W`, then pass this value through the
    # sigmoid activation function, thereby giving us our
    # predictions on the dataset
    preds = sigmoid_activation(X.dot(W))

    # now that we have our predictions, we need to determine
    # our `error`, which is the difference between our predictions
    # and the true values
    error = preds - y

    # given our `error`, we can compute the total loss value as
    # the sum of squared loss -- ideally, our loss should
    # decrease as we continue training
    loss = np.sum(error ** 2)
    lossHistory.append(loss)
    print("[INFO] epoch #{}, loss={:.7f}".format(epoch + 1, loss))

    # the gradient update is therefore the dot product between
    # the transpose of `X` and our error, scaled by the total
    # number of data points in `X`
    gradient = X.T.dot(error) / X.shape[0]

    # in the update stage, all we need to do is nudge our weight
    # matrix in the negative direction of the gradient (hence the
    # term "gradient descent" by taking a small step towards a
    # set of "more optimal" parameters
    W += -args["alpha"] * gradient

# to demonstrate how to use our weight matrix as a classifier,
# let's look over our a sample of training examples
for i in np.random.choice(250, 10):
    # compute the prediction by taking the dot product of the
    # current feature vector with the weight matrix W, then
    # passing it through the sigmoid activation function
    activation = sigmoid_activation(X[i].dot(W))

    # the sigmoid function is defined over the range y=[0, 1],
    # so we can use 0.5 as our threshold -- if `activation` is
    # below 0.5, it's class `0`; otherwise it's class `1`
    label = 0 if activation < 0.5 else 1

    # show our output classification
    print("activation={:.4f}; predicted_label={}, true_label={}".format(
        activation, label, y[i]))

# compute the line of best fit by setting the sigmoid function
# to 0 and solving for X2 in terms of X1
Y = (-W[0] - (W[1] * X)) / W[2]

# plot the original data along with our line of best fit
plt.figure()
plt.scatter(X[:, 1], X[:, 2], marker="o", c=y)
plt.plot(X, Y, "r-")

# construct a figure that plots the loss over time
fig = plt.figure()
plt.plot(np.arange(0, args["epochs"]), lossHistory)
fig.suptitle("Training Loss")
plt.xlabel("Epoch #")
plt.ylabel("Loss")
plt.show()

上面代码就按照微分分析法,通过 ( p r e d i c t − y ) ∗ x i (predict - y)*x_i predictyxi来算得梯度(gradient),再归一化除去 n u m ( X ) num(X) num(X)
结果图为
[外链图片转存失败(img-N6tcngbU-1562988934232)(https://raw.githubusercontent.com/FutureIsM/blog_img/master/SGD/gradient_descent_decision_boundary.jpg)]
[外链图片转存失败(img-Zo4NYWty-1562988934233)(https://raw.githubusercontent.com/FutureIsM/blog_img/master/SGD/gradient_descent_loss.jpg)]
[外链图片转存失败(img-PaMxERNw-1562988934234)(https://raw.githubusercontent.com/FutureIsM/blog_img/master/SGD/gradient_descent_animation.gif)]

3.SGD(Stochastic gradient descent)与BGD(Batch gradient descent)

BGD和SGD区别
BGD:又称Vanilla梯度下降法,用运行整个训练集(一个epoch)来做一次更新
SGD:运行一个或者几个batch(Minibatch Stochastic gradient Descent)时来更新一次更新
python代码
BGD

for i in range(nb_epochs):
  params_grad = evaluate_gradient(loss_function, data, params)
  params = params - learning_rate * params_grad

SGD

for i in range(nb_epochs):
  np.random.shuffle(data)
  for example in data:
    params_grad = evaluate_gradient(loss_function, example, params)
    params = params - learning_rate * params_grad

由于上面的参数更新机制,所以会产生下列情况却别。
1、 BGD在更新一次参数时所需的时间是巨大的;而SGD由于只需要训练一部分数据更新一次参数,所以需求更新参数时间会很短。
2、 由于初始化参数W都是随机化(一般高斯),所以BGD根据整个训练数据计算梯度会下降到一个稳定的(局部)最小值,而这个值对初始化依赖程度很大;而SGD由于是采用根据训练集一部分数据更换着去更新参数W,所以会高方差改变参数,导致参数更新幅度大而loss出现震荡的现象,但是这个好处也在于无论初始化是否在空域找到一个最合适的点,SGD都有可能跳出局部最小,取得更好的最小值。
SGD_loss
sgd_loss

note:这边顺便提一下,这两种梯度下降优化算法的中间量mini-BGD,由于小批量梯度下降法结合了上述两种优化算法的各自有点,所以一般更实用些,一般取 2 n 2^n 2n作为batch_size,根据需求取16-256最佳。这里提下,假如batch_size越小,根据前述原理,这样震荡会越明显,也更加精确(泛化低)。
mini-BGD

for i in range(nb_epochs):
    np.random.shuffle(data)
    for batch in get_batches(data, batch_size=50):
        params_grad = evaluate_gradient(loss_function, batch, params)
        params = params - learning_rate * params_grad

##CHALLENGE
虽然Vanilla小批量梯度下降法并不能保证较好的收敛性,但是需要强调的是,这也给我们留下了如下的一些挑战:

  • 选择一个合适的学习率可能是困难的。学习率太小会导致收敛的速度很慢,学习率太大会妨碍收敛,导致损失函数在最小值附近波动甚至偏离最小值。
  • 学习率调整[17]试图在训练的过程中通过例如退火的方法调整学习率,即根据预定义的策略或者当相邻两代之间的下降值小于某个阈值时减小学习率。然而,策略和阈值需要预先设定好,因此无法适应数据集的特点[4]。
  • 此外,对所有的参数更新使用同样的学习率。如果数据是稀疏的,同时,特征的频率差异很大时,我们也许不想以同样的学习率更新所有的参数,对于出现次数较少的特征,我们对其执行更大的学习率。
  • 高度非凸误差函数普遍出现在神经网络中,在优化这类函数时,另一个关键的挑战是使函数避免陷入无数次优的局部最小值。Dauphin等人[5]指出出现这种困难实际上并不是来自局部最小值,而是来自鞍点,即那些在一个维度上是递增的,而在另一个维度上是递减的。这些鞍点通常被具有相同误差的点包围,因为在任意维度上的梯度都近似为0,所以SGD很难从这些鞍点中逃开。

#4.针对于上述challenge开发出来的梯度优化算法
下面,我们将列举一些算法,这些算法被深度学习社区广泛用来处理前面提到的挑战。我们不会讨论在实际中不适合在高维数据集中计算的算法,例如诸如牛顿法的二阶方法。

##4.1 动量法
GD很难通过陡谷,即在一个维度上的表面弯曲程度远大于其他维度的区域[19],这种情况通常出现在局部最优点附近。在这种情况下,SGD摇摆地通过陡谷的斜坡,同时,沿着底部到局部最优点的路径上只是缓慢地前进,这个过程如图所示:
SGD without Momentum

SGD without Momentum

SGD with Momentum

SGD with Momentum

如图(SGD with Momentum)所示,动量法[16]是一种帮助SGD在相关方向上加速并抑制摇摆的一种方法。动量法将历史步长的更新向量的一个分量γ增加到当前的更新向量中(部分实现中交换了公式中的符号):

$$v_t=\gamma v_{t-1}+\eta \nabla_\theta J(\theta) \\ \theta=\theta -v_t $$

动量项 γ \gamma γ通常设置为0.9或者类似的值。假设这里 γ \gamma γ设置为0.9的话,我们分析得到,假如

$$ v_t \begin{cases} \lt v_{t-1}, & \text{if $v_{t-1} \lt 10 \eta \nabla_\theta J(\theta) $ } \\ \ge v_{t-1}, & \text{if $v_{t-1} \ge 10 \eta \nabla_\theta J(\theta) $} \end{cases} $$

所以根据分析可知,相比于我们的目的函数,我们这边加入了一个更新量为 − γ v t − 1 -\gamma v_{t-1} γvt1,其实可以看做为一个梯度值为 γ v t − 1 \gamma v_{t-1} γvt1的量,而这个更新量的大小又与当前梯度和历史梯度的积累量相关,假如我们到达一个误差函数空间域的陡坡时,我们就会拥有一个很大的动量积累,下次就可以去很快的突破这个陡坡,而处于缓慢下降而陷入局部最优解附近的问题。

从本质上说,动量法,就像我们从山上推下一个球,球在滚下来的过程中累积动量,变得越来越快(这里在过程中越来越快是不一定的,但是这个 v t v_t vt其实是收敛的,即当积累量远远大于10倍梯度时,该函数收敛, v t v_t vt趋于不变,直到达到终极速度,由于有空气阻力的存在,则 γ \gamma γ<1)。同样的事情也发生在参数的更新过程中:对于在梯度点处具有相同的方向的维度,其动量项增大,对于在梯度点处改变方向的维度,其动量项减小。因此,我们可以得到更快的收敛速度,同时可以减少摇摆。

其实我们也会发现,这个参数 γ \gamma γ在物理意义上有点像个摩擦系数,不然 v t v_t vt将会永远增大而不会停止。通过交叉验证,这个参数通常设为[0.5,0.9,0.95,0.99]中的一个。和学习率随着时间退火(下文有讨论)类似,动量随时间变化的设置有时能略微改善最优化的效果,其中动量在学习过程的后阶段会上升。一个典型的设置是刚开始将动量设为0.5而在后面的多个周期(epoch)中慢慢提升到0.99。通过动量更新,参数向量会在任何有持续梯度的方向上增加速度。

##4.2 Nesterov加速梯度下降法
然而,球从山上滚下的时候,盲目地沿着斜率方向,往往并不能令人满意。我们希望有一个智能的球,这个球能够知道它将要去哪,以至于在重新遇到斜率上升时能够知道减速。而Momentum动量法的下降方向还是依然只取决于一路向下的梯度,而这个梯度是否能引导正确的方向这使我们需要思考的地方和解决的,我们不需要未知的,我们需要已知的。

Nesterov加速梯度下降法(Nesterov accelerated gradient,NAG)[13]是一种能够给动量项这样的预知能力的方法。我们知道,我们利用动量项 γ v t − 1 \gamma v_{t-1} γvt1来更新参数 θ \theta θ。通过计算 θ − γ v t − 1 \theta-\gamma v_{t-1} θγvt1能够告诉我们参数未来位置的一个近似值(对于完整的更新,梯度是缺失的),这也就是告诉我们参数大致将变为多少。通过计算关于参数未来的近似位置的梯度,而不是关于当前的参数 θ \theta θ的梯度,我们可以高效的求解 :

$$v_t=\gamma v_{t-1}+\eta \nabla_\theta J(\theta-\gamma v_{t-1}) \\ \theta=\theta -v_t $$

同时,我们设置动量项 γ \gamma γ大约为0.9。动量法首先计算当前的梯度值(图3中的小的蓝色向量),然后在更新的累积梯度(大的蓝色向量)方向上前进一大步,Nesterov加速梯度下降法NAG首先在先前累积梯度(棕色的向量)方向上前进一大步,计算梯度值,然后做一个修正(绿色的向量)。这个具有预见性的更新防止我们前进得太快,同时增强了算法的响应能力,这一点在很多的任务中对于RNN的性能提升有着重要的意义[2]。
[外链图片转存失败(img-1Ncv899e-1562988934236)(https://raw.githubusercontent.com/FutureIsM/blog_img/master/SGD/nesterov_update_vector.png)]

对于NAG的直观理解的另一种解释可以参见http://cs231n.github.io/neural-networks-3/,同时Ilya Sutskever在其博士论文[18]中给出更详细的综述。在cs231n中的解释让我们能更好理解这个修正,通过公式我们知道, − γ v t − 1 -\gamma v_{t-1} γvt1是未来更新参量,这里我们就相当于将未来的近似位置看做是“向前看”,这个点在我们一会儿要停止的位置附近。因此,计算 J ( θ − γ v t − 1 ) J(\theta-\gamma v_{t-1}) J(θγvt1)的梯度而不是“旧”位置 J ( θ ) J(\theta) J(θ)的梯度就有意义了。这也是为啥这个修正更能体现出最终正确的位置,而少走了一些弯路。

既然我们能够使得我们的更新适应误差函数的斜率以相应地加速SGD,我们同样也想要使得我们的更新能够适应每一个单独参数,以根据每个参数的重要性决定大的或者小的更新。

###note:学习率退火

在训练深度网络的时候,让学习率随着时间退火通常是有帮助的。可以这样理解:如果学习率很高,系统的动能就过大,参数向量就会无规律地跳动,不能够稳定到损失函数更深更窄的部分去。知道什么时候开始衰减学习率是有技巧的:慢慢减小它,可能在很长时间内只能是浪费计算资源地看着它混沌地跳动,实际进展很少。但如果快速地减少它,系统可能过快地失去能量,不能到达原本可以到达的最好位置。通常,实现学习率退火有3种方式:

  • 随步数衰减:每进行几个周期就根据一些因素降低学习率。典型的值是每过5个周期就将学习率减少一半,或者每20个周期减少到之前的0.1。这些数值的设定是严重依赖具体问题和模型的选择的。在实践中可能看见这么一种经验做法:使用一个固定的学习率来进行训练的同时观察验证集错误率,每当验证集错误率停止下降,就乘以一个常数(比如0.5)来降低学习率。
  • 指数衰减。数学公式是 α = α 0 e − k t \alpha=\alpha_0e^{-kt} α=α0ekt,其中 α 0 \alpha_0 α0,k是超参数,t是迭代次数(也可以使用周期作为单位)。
  • 1/t衰减的数学公式是 α = α 0 / ( 1 + k t ) \alpha=\alpha_0/(1+kt) α=α0/(1+kt),其中 α 0 \alpha_0 α0,k是超参数,t是迭代次数。
    在实践中,我们发现随步数衰减的随机失活(dropout)更受欢迎,因为它使用的超参数(衰减系数和以周期为时间单位的步数)比k更有解释性。最后,如果你有足够的计算资源,可以

##4.3 Adagrad
Adagrad[7]是这样的一种基于梯度的优化算法:让学习率适应参数,对于出现次数较少的特征,我们对其采用更大的学习率,对于出现次数较多的特征,我们对其采用较小的学习率。因此,Adagrad非常适合处理稀疏数据。Dean等人[6]发现Adagrad能够极大提高了SGD的鲁棒性并将其应用于Google的大规模神经网络的训练,其中包含了YouTube视频中的猫的识别。此外,Pennington等人[15]利用Adagrad训练Glove词向量,因为低频词比高频词需要更大的步长。

前面,我们每次更新所有的参数 θ \theta θ时,每一个参数 θ i \theta_i θi都使用的是相同的学习率 η \eta η。由于Adagrad在t时刻对每一个参数 θ i \theta_i θi使用了不同的学习率,我们首先介绍Adagrad对每一个参数的更新,然后我们对其向量化。为了简洁,令 g t , i g_{t,i} gt,i为在 t t t时刻目标函数关于参数 θ i \theta_i θi的梯度:

$$g_{t,i}=\nabla_\theta J(\theta_i) $$

t t t时刻,对每个参数 θ i \theta_i θi的更新过程变为:

$$\theta_{t+1,i}=\theta_{t,i}-\eta \cdot g_{t,i} $$

对于上述的更新规则,在 t t t时刻,基于对 θ i \theta_i θi计算过的历史梯度,Adagrad修正了对每一个参数 θ i \theta_i θi的学习率:

$$\theta_{t+1,i}=\theta_{t,i}- \frac{\eta}{\sqrt{G_{t,ii}+\epsilon}} \cdot g_{t,i} $$

其中, G t ∈ R d × d G_t\in \Bbb R^{d\times d} GtRd×d是一个对角矩阵,对角线上的元素 i i i, i i i是直到 t t t时刻为止,所有关于 θ i \theta_i θi的梯度的平方和(Duchi等人[7]将该矩阵作为包含所有先前梯度的外积的完整矩阵的替代,因为即使是对于中等数量的参数 d d d,矩阵的均方根的计算都是不切实际的。), ϵ \epsilon ϵϵ是平滑项,用于防止除数为 θ \theta θ(通常大约设置为1e−8)。比较有意思的是,如果没有平方根的操作,算法的效果会变得很差。

由于 G t G_t Gt的对角线上包含了关于所有参数 θ \theta θ的历史梯度的平方和,现在,我们可以通过 G t G_t Gt g t g_t gt之间的元素向量乘法 ⊙ \odot 向量化上述的操作:

$$\theta_{t+1,i}=\theta_{t,i}- \frac{\eta}{\sqrt{G_{t,ii}+\epsilon}} \odot g_{t,i} $$

Adagrad算法的一个主要优点是无需手动调整学习率。在大多数的应用场景中,通常采用常数0.01。

Adagrad的一个主要缺点是它在分母中累加梯度的平方:由于没增加一个正项,在整个训练过程中,累加的和会持续增长。这会导致学习率变小以至于最终变得无限小,在学习率无限小时,Adagrad算法将无法取得额外的信息。接下来的算法旨在解决这个不足。
##4.4 Adadelta
Adadelta[21]是Adagrad的一种扩展算法,以处理Adagrad学习速率单调递减的问题。不是计算所有的梯度平方,Adadelta将计算计算历史梯度的窗口大小限制为一个固定值 w w w
在Adadelta中,无需存储先前的 w w w个平方梯度,而是将梯度的平方递归地表示成所有历史梯度平方的均值。在 t t t时刻的均值 E [ g 2 ] t E[g^2]_t E[g2]t只取决于先前的均值和当前的梯度(分量 γ \gamma γ类似于动量项):

$$E[g^2]=\gamma E[g^2]_{t-1}+(1-\gamma)g_t^2 $$

我们将 γ \gamma γ设置成与动量项相似的值,即0.9左右。为了简单起见,我们利用参数更新向量 Δ θ t \Delta \theta_t Δθt重新表示SGD的更新过程:

$$\Delta \theta_t=-\eta \cdot g_{t,i} \\ \theta_{t+1}=\theta_t+\Delta \theta_t $$

我们先前得到的Adagrad参数更新向量变为:

$$\Delta \theta_t=-\frac{\eta}{\sqrt{G_{t}+\epsilon}} \odot g_{t} $$

现在,我们简单将对角矩阵 G t G_t Gt替换成历史梯度的均值 E [ g 2 ] t E[g^2]_t E[g2]t

$$\Delta \theta_t=-\frac{\eta}{\sqrt{E[g^2]_t+\epsilon}} g_{t} $$

由于分母仅仅是梯度的均方根(root mean squared,RMS),我们可以简写为:

$$\Delta \theta_t=-\frac{\eta}{RMS[g]_t} g_{t} $$

作者指出上述更新公式中的每个部分(与SGD,动量法或者Adagrad)并不一致,即更新规则中必须与参数具有相同的假设单位。为了实现这个要求,作者首次定义了另一个指数衰减均值,这次不是梯度平方,而是参数的平方的更新(在原式中 g t g_{t} gt表示的是梯度,而 Δ θ t \Delta \theta_t Δθt表示的是梯度的改变量):

$$E[\Delta \theta^2]_t=\gamma E[\Delta \theta^2]_{t-1}+(1-\gamma)\Delta \theta^2_t $$

因此,参数更新的均方根误差为:

$$RMS[\Delta \theta]_t = \sqrt{E[\Delta \theta^2]_t+\epsilon} $$

由于 R M S [ Δ θ ] t RMS[\Delta \theta]_t RMS[Δθ]t是未知的,我们利用参数的均方根误差来近似更新。利用 R M S [ Δ θ ] t − 1 RMS[\Delta \theta]_{t-1} RMS[Δθ]t1替换先前的更新规则中的学习率 η \eta η,最终得到Adadelta的更新规则:

$$\Delta \theta_t=-\frac{RMS[\Delta \theta]_{t-1}}{RMS[g]_{t}}g_t \\ \theta_{t+1}=\theta_t+\Delta \theta_t $$

使用Adadelta算法,我们甚至都无需设置默认的学习率,因为更新规则中已经移除了学习率。
##4.5 RMSprop
RMSprop是一个未被发表的自适应学习率的算法,该算法由Geoff Hinton在其Coursera课堂的课程6e中提出。

RMSprop和Adadelta在相同的时间里被独立的提出,都起源于对Adagrad的极速递减的学习率问题的求解。实际上,RMSprop是先前我们得到的Adadelta的第一个更新向量的特例:

$$E[g^2]_t=0.9E[g^2]_{t-1}+0.1g^2_t \\ \theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{E[g^2]_t+\epsilon}}g_t $$

同样,RMSprop将学习率分解成一个平方梯度的指数衰减的平均。Hinton建议将 γ \gamma γ设置为0.9,对于学习率 η \eta η,一个好的固定值为0.001。
##4.6 Adam
自适应矩估计(Adaptive Moment Estimation,Adam)[9]是另一种自适应学习率的算法,Adam对每一个参数都计算自适应的学习率。除了像Adadelta和RMSprop一样存储一个指数衰减的历史平方梯度的平均 v t v_t vt,Adam同时还保存一个历史梯度的指数衰减均值 m t m_t mt,类似于动量:

$$m_t=\beta_1 m_{t-1}+(1-\beta_1)g_t \\ v_t=\beta_2 v_{t-1}+(1-\beta_2)g_t^2 $$

m t m_t mt v t v_t vt分别是对梯度的一阶矩(均值,这就相当于 m t m_t mt区别于动量法中使用梯度累积量,而使用了均值来起一个平滑的作用,其实这点和Adadelta的想法类似)和二阶矩(非确定的方差,类似均值思维)的估计,正如该算法的名称。当 m t m_t mt v t v_t vt初始化为0向量时,Adam的作者发现它们都偏向于0,尤其是在初始化的步骤和当衰减率很小的时候(例如 β 1 \beta_1 β1 β 2 \beta_2 β2趋向于1)。
通过计算偏差校正的一阶矩和二阶矩估计来抵消偏差:

$$\hat m_t=\frac{m_t}{1-\beta^t_1} \\ \hat v_t=\frac{v_t}{1-\beta^t_2} $$

正如我们在Adadelta和RMSprop中看到的那样,他们利用上述的公式更新参数,由此生成了Adam的更新规则:

$$\theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{\hat v_t}+\epsilon} \hat m_t $$

作者建议 β 1 \beta_1 β1取默认值为0.9, β 2 \beta_2 β2为0.999, ϵ \epsilon ϵ 1 e − 8 1e^{-8} 1e8。他们从经验上表明Adam在实际中表现很好,同时,与其他的自适应学习算法相比,其更有优势。

##4.7 AdaMax
在Adam算法更新规则中 v t v_t vt根据以往的梯度积累 v t − 1 v_{t-1} vt1和当前梯度值 ∣ g t ∣ 2 |g_t|^2 gt2 l 2 l_2 l2范数按反比的缩放。

$$v_t=\beta_2 v_{t-1}+(1-\beta_2)g_t^2 $$

我们可以推广一般化使这个更新依赖于 l p l_p lp范数,注意这里参数 β \beta β也要相应改变, β 2 \beta_2 β2为的 β 2 p \beta_2^p β2p

$$v_t=\beta_2^p v_{t-1}+(1-\beta_2^p)|g_t|^p $$

当p取值较大时,范数就变得不稳定,这也是为什么1范数和2范数实际上大部分相像,但是例外的是无穷范数表现的很稳定。由于这个性质,作者提出了AdaMax (Kingma and Ba, 2015) ,并证明展示了 v t v_t vt l ∞ l_\infty l范数收敛得更稳定,为了避免混淆,我们用 u t u_t ut表示 v t v_t vt l ∞ l_\infty l范数。

$$ \begin {align} v_t & =\beta_2^\infty v_{t-1}+(1-\beta_2^\infty)|g_t|^\infty \\& = max(\beta_2 \cdot v_{t-1},|g_t|) \end {align} $$

我们现在可以把这个 u t u_t ut带入到Adam更新等式 v ^ t + ϵ \sqrt{\hat v_t}+ \epsilon v^t +ϵ上获得新的AdaMax的更新规则了:

$$\theta_{t+1}=\theta_t-\frac{\eta}{u_t} \hat m_t $$

值得注意的是, u t u_t ut依赖于 m a x max max运算,它不建议像Adam一样去更正偏移量在0附近的 m t m_t mt v t v_t vt,这也是我们为什么不需要计算 u t u_t ut的偏移修正。作者建议 β 1 \beta_1 β1取默认值为0.9, β 2 \beta_2 β2为0.999, ϵ \epsilon ϵ 0.002 0.002 0.002

4.8 Nadam

根据上述,Adam可以看做是RMSprop和momentum(动量法)的一种结合:RMSprop提出针对于过去的梯度平方积累 v t v_t vt的指数型衰减平均,而momentum(动量法)提出针对于以往梯度积累均值 m t m_t mt的指数型衰减平均。我们也知道了Nesterov加速梯度法也是优于vanilla动量法的。
Nadam (Nesterov-accelerated Adaptive Moment Estimation)包含了Adam和NAG。为了结合Adam和NAG,我们需要定义它的动量项 m t m_t mt
首先,让我们回顾一下动量法的更新规则:

$$g_t= \nabla_{\theta_t} J(\theta_t) \\ m_t = \gamma m_{t-1} + \eta g_t \\ \theta_{t+1}=\theta_t - m_t $$

其中 J J J是我们的目标函数, γ \gamma γ是动量衰减项, η \eta η是我们的步长,根据上面的等式改写我们第三个式子为:

$$\theta_{t+1}=\theta_t -(\gamma m_{t-1} + \eta g_t) $$

这个式子再一次说明了动量包含了一部分前一次动量向量方向和一部分当前动量向量方向的信息。

NAG允许我们在计算梯度前通过更新动量项的参数来使梯度方向表现的更准确。因此我们需要定义梯度 g t g_t gt达到NAG:

$$g_t= \nabla_{\theta_t} J(\theta_t - \gamma m_{t-1}) \\ m_t = \gamma m_{t-1} + \eta g_t \\ \theta_{t+1}=\theta_t - m_t $$

Dozat建议通过下面的方法来修改NAG:不要分两步来更新动量(一步更新梯度 g t g_t gt,一步更新参数 θ t + 1 \theta_{t+1} θt+1),我们现在使用预见性的动量向量直接更新当前参数:

$$\theta_{t+1}=\theta_t - (\gamma m_{t-1} + \eta \nabla_{\theta_t} J(\theta_t - \gamma m_{t-1}) ) $$

注意这个式子不是为了初始化过去动量项 m t − 1 m_{t-1} mt1来扩展动量更新规则等式,我们现在是在用当前动量向量 m t m_t mt来体现预见性。为了增加Nesterov动量到Adam中,我们需要用用当前动量向量来替换以往的动量向量。首先,回顾下Adam更新规则如下(注意我们没有必要修改 v ^ t \hat v_t v^t):

$$ m_t=\beta_1 m_{t-1}+(1-\beta_1)g_t \\ \hat m_t=\frac {m_t}{1-\beta_1^t} \\ \theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{\hat v_t}+\epsilon} \hat m_t $$

根据上面等式展开为:

$$\theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{\hat v_t}+\epsilon} (\frac{\beta_1 m_{t-1}}{1-\beta_1^t}+\frac{(1-\beta_1)g_t}{1-\beta_1^t}) $$

注意 β 1 m t − 1 1 − β 1 t \frac{\beta_1 m_{t-1}}{1-\beta_1^t} 1β1tβ1mt1仅仅只是针对于过去动量向量的偏移修正量。因此,我们可以替换为 m ^ t − 1 \hat m_{t-1} m^t1

$$\theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{\hat v_t}+\epsilon} (\beta_1 \hat m_{t-1}+\frac{(1-\beta_1)g_t}{1-\beta_1^t}) $$

再一次看这个等式就非常像我们上面的展开动量更新规则了。现在我们可以就像我们上面做的一样通过简单的替换过去时间动量向量偏移修正量 m ^ t − 1 \hat m_{t-1} m^t1为当前修正量 m ^ t \hat m_{t} m^t来添加Nesterov动量。也就是Nadam更新规则:

$$\theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{\hat v_t}+\epsilon} (\beta_1 \hat m_{t}+\frac{(1-\beta_1)g_t}{1-\beta_1^t}) $$

##4.9 算法可视化

下面两张图给出了上述优化算法的优化行为的直观理解。(还可以看看这里关于Karpathy对相同的图片的描述以及另一个简明关于算法讨论的概述)。

在下图1中,我们看到不同算法在损失曲面的等高线上走的不同路线。所有的算法都是从同一个点出发并选择不同路径到达最优点。注意:Adagrad,Adadelta和RMSprop能够立即转移到正确的移动方向上并以类似的速度收敛,而动量法和NAG会导致偏离,想像一下球从山上滚下的画面。然而,NAG能够在偏离之后快速修正其路线,因为NAG通过对最优点的预见增强其响应能力。

下图2中展示了不同算法在鞍点出的行为,鞍点即为一个点在一个维度上的斜率为正,而在其他维度上的斜率为负,正如我们前面提及的,鞍点对SGD的训练造成很大困难。这里注意,SGD,动量法和NAG在鞍点处很难打破对称性,尽管后面两个算法最终设法逃离了鞍点。而Adagrad,RMSprop和Adadelta能够快速想着梯度为负的方向移动,其中Adadelta走在最前面。
[外链图片转存失败(img-cFcbfqtC-1562988934237)(https://raw.githubusercontent.com/FutureIsM/blog_img/master/SGD/contours_evaluation_optimizers.gif)]
[外链图片转存失败(img-UhpgL7H0-1562988934237)(https://raw.githubusercontent.com/FutureIsM/blog_img/master/SGD/saddle_point_evaluation_optimizers.gif)]
正如我们所看到的,自适应学习速率的方法,即 Adagrad、 Adadelta、 RMSprop 和Adam,最适合这些场景下最合适,并在这些场景下得到最好的收敛性。

##4.10 选择使用哪种优化算法?
那么,我们应该选择使用哪种优化算法呢?如果输入数据是稀疏的,选择任一自适应学习率算法可能会得到最好的结果。选用这类算法的另一个好处是无需调整学习率,选用默认值就可能达到最好的结果。
总的来说,RMSprop是Adagrad的扩展形式,用于处理在Adagrad中急速递减的学习率。RMSprop与Adadelta相同,所不同的是Adadelta在更新规则中使用参数的均方根进行更新。最后,Adam是将偏差校正和动量加入到RMSprop中。在这样的情况下,RMSprop、Adadelta和Adam是很相似的算法并且在相似的环境中性能都不错。Kingma等人[9]指出在优化后期由于梯度变得越来越稀疏,偏差校正能够帮助Adam微弱地胜过RMSprop。综合看来,Adam可能是最佳的选择。
有趣的是,最近许多论文中采用不带动量的SGD和一种简单的学习率的退火策略。已表明,通常SGD能够找到最小值点,但是比其他优化的SGD花费更多的时间,与其他算法相比,SGD更加依赖鲁棒的初始化和退火策略,同时,SGD可能会陷入鞍点,而不是局部极小值点。因此,如果你关心的是快速收敛和训练一个深层的或者复杂的神经网络,你应该选择一个自适应学习率的方法。

#5 并行和分布式SGD

当存在大量的大规模数据和廉价的集群时,利用分布式SGD来加速是一个显然的选择。SGD本身有固有的顺序:一步一步,我们进一步进展到最小。SGD提供了良好的收敛性,但SGD的运行缓慢,特别是对于大型数据集。相反,SGD异步运行速度更快,但客户端之间非最理想的通信会导致差的收敛。此外,我们也可以在一台机器上并行SGD,这样就无需大的计算集群。以下是已经提出的优化的并行和分布式的SGD的算法和框架。

##5.1 Hogwild!

Niu等人[14]提出称为Hogwild!的更新机制,Hogwild!允许在多个CPU上并行执行SGD更新。在无需对参数加锁的情况下,处理器可以访问共享的内存。这种方法只适用于稀疏的输入数据,因为每一次更新只会修改一部分参数。在这种情况下,该更新策略几乎可以达到一个最优的收敛速率,因为CPU之间不可能重写有用的信息。

##5.2 Downpour SGD

Downpour SGD是SGD的一种异步的变形形式,在Google,Dean等人[6]在他们的DistBelief框架(TensorFlow的前身)中使用了该方法。Downpour SGD在训练集的子集上并行运行多个模型的副本。这些模型将各自的更新发送给一个参数服务器,参数服务器跨越了多台机器。每一台机器负责存储和更新模型的一部分参数。然而,因为副本之间是彼此不互相通信的,即通过共享权重或者更新,因此可能会导致参数发散而不利于收敛。

##5.3 延迟容忍SGD

通过容忍延迟算法的开发,McMahan和Streeter[11]将AdaGraad扩展成并行的模式,该方法不仅适应于历史梯度,同时适应于更新延迟。该方法已经在实践中被证实是有效的。

##5.4 TensorFlow

TensorFlow[1]是Google近期开源的框架,该框架用于实现和部署大规模机器学习模型。TensorFlow是基于DistBelief开发,同时TensorFlow已经在内部用来在大量移动设备和大规模分布式系统的执行计算。在2016年4月发布的分布式版本依赖于图计算,图计算即是对每一个设备将图划分成多个子图,同时,通过发送、接收节点对完成节点之间的通信。

##5.5 弹性平均SGD

Zhang等人[22]提出的弹性平均SGD(Elastic Averaging SGD,EASGD)连接了异步SGD的参数客户端和一个弹性力,即参数服务器存储的一个中心变量。EASGD使得局部变量能够从中心变量震荡得更远,这在理论上使得在参数空间中能够得到更多的探索。经验表明这种增强的探索能力通过发现新的局部最优点,能够提高整体的性能。

#6 优化SGD的其他策略

最后,我们介绍可以与前面提及到的任一算法配合使用的其他的一些策略,以进一步提高SGD的性能。对于其他的一些常用技巧的概述可以参见[10]。

##6.1 数据集的洗牌和课程学习

总的来说,我们希望避免向我们的模型中以一定意义的顺序提供训练数据,因为这样会使得优化算法产生偏差。因此,在每一轮迭代后对训练数据洗牌是一个不错的主意。

另一方面,在很多情况下,我们是逐步解决问题的,而将训练集按照某个有意义的顺序排列会提高模型的性能和SGD的收敛性,如何将训练集建立一个有意义的排列被称为课程学习[3]。

Zaremba and Sutskever[20]只能使用课程学习训练LSTM来评估简单程序,并表明组合或混合策略比单一的策略更好,通过增加难度来排列示例。

##6.2 批量归一化

为了便于学习,我们通常用0均值和单位方差初始化我们的参数的初始值来归一化。 随着不断训练,参数得到不同的程度的更新,我们失去了这种归一化,随着网络变得越来越深,这种现象会降低训练速度,且放大参数变化。

批量归一化[8]在每次小批量数据反向传播之后重新对参数进行0均值单位方差标准化。通过将模型架构的一部分归一化,我们能够使用更高的学习率,更少关注初始化参数。批量归一化还充当正则化的作用,减少(有时甚至消除)Dropout的必要性。

##6.3 Early stopping

如Geoff Hinton所说:“Early Stopping是美丽好免费午餐”(NIPS 2015 Tutorial slides)。你因此必须在训练的过程中时常在验证集上监测误差,在验证集上如果损失函数不再显著地降低,那么应该提前结束训练。

##6.4 梯度噪音

Neelakantan等人[12]在每个梯度更新中增加满足高斯分布 N ( 0 , δ t 2 ) N(0,\delta^2_t) N(0,δt2)的噪音:

$$g_{t,i}=g_{t,i}+N(0,\delta^2_t) $$

高斯分布的方差需要根据如下的策略退火:

$$\delta_t^2=\frac{\eta}{{(1+t)}^\gamma} $$

他们指出增加了噪音,使得网络对不好的初始化更加鲁棒,同时对深层的和复杂的网络的训练特别有益。他们猜测增加的噪音使得模型更优机会逃离当前的局部最优点,以发现新的局部最优点,这在更深层的模型中更加常见。

#7 总结

在这篇博客文章中,我们初步研究了梯度下降的三个变形形式,其中,小批量梯度下降是最受欢迎的。 然后我们研究了最常用于优化SGD的算法:动量法,Nesterov加速梯度,Adagrad,Adadelta,RMSprop,Adam以及不同的优化异步SGD的算法。 最后,我们已经考虑其他一些改善SGD的策略,如洗牌和课程学习,批量归一化和early stopping。

#References
[1] Abadi, M., Agarwal, A., Barham, P., Brevdo, E., Chen, Z., Citro, C., … Zheng, X. (2015). TensorFlow : Large-Scale Machine Learning on Heterogeneous Distributed Systems.
[2] Bengio, Y., Boulanger-Lewandowski, N., & Pascanu, R. (2012). Advances in Optimizing Recurrent Networks. Retrieved from http://arxiv.org/abs/1212.0901
[3] Bengio, Y., Louradour, J., Collobert, R., & Weston, J. (2009). Curriculum learning. Proceedings of the 26th Annual International Conference on Machine Learning, 41–48. http://doi.org/10.1145/1553374.1553380
[4] Darken, C., Chang, J., & Moody, J. (1992). Learning rate schedules for faster stochastic gradient search. Neural Networks for Signal Processing II Proceedings of the 1992 IEEE Workshop, (September), 1–11. http://doi.org/10.1109/NNSP.1992.253713
[5] Dauphin, Y., Pascanu, R., Gulcehre, C., Cho, K., Ganguli, S., & Bengio, Y. (2014). Identifying and attacking the saddle point problem in high-dimensional non-convex optimization. arXiv, 1–14. Retrieved from http://arxiv.org/abs/1406.2572
[6] Dean, J., Corrado, G. S., Monga, R., Chen, K., Devin, M., Le, Q. V, … Ng, A. Y. (2012). Large Scale Distributed Deep Networks. NIPS 2012: Neural Information Processing Systems, 1–11. http://doi.org/10.1109/ICDAR.2011.95
[7] Duchi, J., Hazan, E., & Singer, Y. (2011). Adaptive Subgradient Methods for Online Learning and Stochastic Optimization. Journal of Machine Learning Research, 12, 2121–2159. Retrieved from http://jmlr.org/papers/v12/duchi11a.html
[8] Ioffe, S., & Szegedy, C. (2015). Batch Normalization : Accelerating Deep Network Training by Reducing Internal Covariate Shift. arXiv Preprint arXiv:1502.03167v3
[9] Kingma, D. P., & Ba, J. L. (2015). Adam: a Method for Stochastic Optimization. International Conference on Learning Representations, 1–13.
[10] LeCun, Y., Bottou, L., Orr, G. B., & Müller, K. R. (1998). Efficient BackProp. Neural Networks: Tricks of the Trade, 1524, 9–50. http://doi.org/10.1007/3-540-49430-8_2
[11] Mcmahan, H. B., & Streeter, M. (2014). Delay-Tolerant Algorithms for Asynchronous Distributed Online Learning. Advances in Neural Information Processing Systems (Proceedings of NIPS), 1–9. Retrieved from http://papers.nips.cc/paper/5242-delay-tolerant-algorithms-for-asynchronous-distributed-online-learning.pdf
[12] Neelakantan, A., Vilnis, L., Le, Q. V., Sutskever, I., Kaiser, L., Kurach, K., & Martens, J. (2015). Adding Gradient Noise Improves Learning for Very Deep Networks, 1–11. Retrieved from http://arxiv.org/abs/1511.06807
[13] Nesterov, Y. (1983). A method for unconstrained convex minimization problem with the rate of convergence o(1/k2). Doklady ANSSSR (translated as Soviet.Math.Docl.), vol. 269, pp. 543– 547.
[14] Niu, F., Recht, B., Christopher, R., & Wright, S. J. (2011). Hogwild! : A Lock-Free Approach to Parallelizing Stochastic Gradient Descent, 1–22.
[15] Pennington, J., Socher, R., & Manning, C. D. (2014). Glove: Global Vectors for Word Representation. Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing, 1532–1543. http://doi.org/10.3115/v1/D14-1162
[16] Qian, N. (1999). On the momentum term in gradient descent learning algorithms. Neural Networks : The Official Journal of the International Neural Network Society, 12(1), 145–151. http://doi.org/10.1016/S0893-6080(98)00116-6
[17] H. Robinds and S. Monro, “A stochastic approximation method,” Annals of Mathematical Statistics, vol. 22, pp. 400–407, 1951.
[18] Sutskever, I. (2013). Training Recurrent neural Networks. PhD Thesis.
[19] Sutton, R. S. (1986). Two problems with backpropagation and other steepest-descent learning procedures for networks. Proc. 8th Annual Conf. Cognitive Science Society.
[20] Zaremba, W., & Sutskever, I. (2014). Learning to Execute, 1–25. Retrieved from http://arxiv.org/abs/1410.4615
[21] Zeiler, M. D. (2012). ADADELTA: An Adaptive Learning Rate Method. Retrieved from http://arxiv.org/abs/1212.5701
[22] Zhang, S., Choromanska, A., & LeCun, Y. (2015). Deep learning with Elastic Averaging SGD. Neural Information Processing Systems Conference (NIPS 2015), 1–24. Retrieved from http://arxiv.org/abs/1412.6651

闲在写了个公众号《算故为法》,本公众号其实主打夯实基础,也许后期后做些视野型的文章写些关注发展前言,但是在我和众多同行交流学习过程中我发现很多学者只触及到了应用层,理论层极度欠缺,本公众号将分享和梳理基础理论知识,也许有错误的地方,也请同行多指点。最后,欢迎大家关注,你的关注是我不断更新的动力!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值