zl程序教程

您现在的位置是:首页 >  其它

当前栏目

被骗了,原来随机梯度下降这么简单

简单 随机 这么 原来 下降 梯度
2023-06-13 09:11:31 时间

作者 | 梁唐

大家好,我是梁唐。

今天是梯度下降的最后一篇,我们来聊聊梯度下降算法的两个优化——随机梯度下降和批量梯度下降。

优化分析

在我们介绍具体的做法之前,我们先来分析一下问题。在我们之前的文章当中也提到过,梯度下降的一个比较明显的问题是随着样本数量的增大,计算梯度会带来大量的计算。而训练的时候大量用到梯度的计算,所以如果梯度的计算耗时很大的话,是无法接受的。

这也很容易明白,我们自己本地实验可能只有几百或者是几千条数据。但是在实际工业的应用场景当中,数据量都是以十万、百万甚至是亿为单位的。而训练一个模型会需要计算成千上万次梯度,所以如果梯度计算的耗时过长,会导致训练模型需要大量的时间才能收敛。

我们来分析一下问题,复杂度的计算来源于数据和算法。在这个问题当中,梯度计算的算法暂时没有优化的可能。除非线性代数或者是底层计算拥有巨大的突破,否则梯度计算的算法基本没有优化的空间,所以我们只能从数据入手。

思路非常朴素,既然我们每次计算全体的样本会导致耗时过大难以承受,那么我们可不可以只选择其中一部分数据代替整体来计算梯度呢?

当然是可以的,不仅是可以,而且也是这么做的。根据我们随机选择计算梯度样本数量的不同,算法进一步划分为随机梯度下降和批量梯度下降。

随机梯度下降

随机梯度下降的原理非常简单,就是每次我们在需要计算梯度的时候,只从样本当中选择一条来计算梯度

只选择一条样本来计算梯度的好处很明显,就是一个字:快。但是缺点也很明显,我们可以很容易想出来,一条样本意味着非常大的随机性。很有可能这条样本的梯度和整体的梯度并不一致,甚至有很大的偏差。这样的话,如果我们朝着它梯度方向学习,很有可能结果并不好。

另一个问题是,由于我们每次只选择一条样本计算梯度,所以模型最后很难收敛到极值点。因为我们到了极值点附近的时候无法确定选到的样本的梯度一定是指向极值点的,实际上更有可能发生偏移。而在极值点附近一直震荡。

好在,这两个问题并不是不可以解决的。对于第一个问题而言,对于单次采样的随机性确实很大,以至于我们完全无法保证它的效果。然而当我们采样的次数足够多了之后,产生的误差会彼此之间冲抵,就好像我们无限次重复实验,最终的结果会趋近于一个稳定值,所以从效果上来说还是会朝着极值点的方向收敛的。

另一个问题也容易解决,解决方法是我们来调整学习率。一开始的时候我们的学习率设置得稍微大一些。随着模型的收敛,学习率也逐渐减小。这样当模型到了极值点附近震荡之后,由于学习率减小,所以可以控制偏移量在一个很小的范围内。

我们来看下面这张图来直观地感受一下:

图上中间一团乱麻的红色区域就是模型收敛之后在极值点震荡的体现,我们也可以看到模型在等高线的表现也一直是蜿蜒着前进的,这也体现了随机性。

搞清楚了原理,就到了硬核coding的时候了。我们先来搞定学习率,我们想要一个一开始很大,逐渐减小的学习率。利用反比例函数,可以很轻松地实现这一点。

我们设置成:

\displaystyle\eta=\frac{t_0}{t+\alpha} 这里的t是迭代次数,t_0\alpha 是常数,这样就可以实现随着迭代次数的增加学习率逐渐减小,最后趋向于0.

我们再来复习一下梯度的公式:

\nabla_\theta MSE(\theta)=\begin{pmatrix} \frac {\partial}{\partial \theta_0}MSE(\theta) \\ \frac {\partial}{\partial \theta_1}MSE(\theta) \\ \vdots\\ \frac {\partial}{\partial \theta_n}MSE(\theta) \\ \end{pmatrix}=\frac {1}{m}X^T\cdot(X \cdot\theta-Y)

我们只需要将公式当中表示整个样本集合的矩阵X 替换成单条样本的矩阵即可。

我们来实现代码:

# 设置参数,来实现学习率递减
n_epochs = 50
t0, t1 = 5, 50

# 学习率迭代函数
def learning_schedule(t):
    return t0 / (t + t1)

theta = np.random.randn(2,1)

for epoch in range(n_epochs):
    for i in range(m):
        # 随机选择一条样本
        random_index = np.random.randint(m)
        xi = X[random_index:random_index+1]
        yi = y[random_index:random_index+1]
        gradients = xi.T.dot(xi.dot(theta)-yi)
        eta = learning_schedule(epoch * m + i)
        theta = theta - eta * gradients

我们来看下最后的结果:

和我们预期一致,单纯从数值上来看和梯度下降相差并不大。但是如果你亲自跑一下代码比较一下两者的话,你会发现运行随机梯度下降的运行速度要比梯度下降快得多。

这也是预料之中的,毕竟相同的迭代次数,我们要比梯度下降快了m 倍,这里的m 是样本的数量。也就是说样本的数量越大,我们的效果越好。当然,这只是理论上的情况,因为使用随机梯度下降会存在误差,所以通常我们迭代的次数会多一些。

批量梯度下降

批量梯度下降和随机梯度下降原理是一样的,都是随机选取出样本来代替整体,从而加快计算梯度的速度。

不过不同的是批量梯度下降选取的是一小批样本,而不是单条样本。所以和随机梯度下降比起来,批量梯度下降由于每次选择一小批样本来计算梯度,所以它的偏差要比随机梯度下降小一些。但是相对的复杂度也就要大一些,算是随机梯度下降和梯度下降的折中方案。

也可以说随机梯度下降是批量梯度下降的一种特殊情况,因此代码的改动量也很小,只有两行,我们来看:

# 设置参数,来实现学习率递减
n_epochs = 50
t0, t1 = 5, 50

# 学习率迭代函数
def learning_schedule(t):
    return t0 / (t + t1)

theta = np.random.randn(2,1)

# 批量大小
batch_size = 16

for epoch in range(n_epochs):
    for i in range(m):
        # 随机选择一批样本
        random_index = np.random.choice(m, size=batch_size)
        xi = X[random_index]
        yi = y[random_index]
        gradients = xi.T.dot(xi.dot(theta)-yi)
        eta = learning_schedule(epoch * m + i)
        theta = theta - eta * gradients

对比一下这两段代码会发现,改动的地方只有增多了一个batch_size的参数,表示一个批量的大小。以及我们将np.random.randint换成了np.random.choice。后者传入两个参数,第一个参数表示随机的范围,第二个参数表示选取的数量。因此使用这个方法,我们可以很轻松地实现批量样本的选取。

最后,我们来看一下训练时候模型的渐近线。

一开始的时候,每根线的间隙很大,说明模型一开始学习的效率很高。到了后来,多根线聚集在一起,说明模型已经收敛,由于随机取样的梯度存在误差,所以一直在极值点附近震荡。

从这张图看出,批量梯度下降的效果还是很好的。实际上我们在实际工作当中几乎清一色都是使用的批量梯度下降的方法来进行模型的训练,在模型的效果和运行效率之间取得了一个比较不错的平衡。