梯度下降

320 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

上一篇了解了线性回归、标准方程。我们知道Scikit-Learning的LinearRegression()就是一个线性回归。但是他是基于SVD算出伪逆,最后伪逆点乘预测值(标签)求出我们需要的θ\theta

但是不管是标准方程(时间复杂度为O(n2.4n^{2.4})到O(n3n^{3})还是SVD(时间复杂度是O(n2n^{2})的计算量都很大,因此我现在要讲的是一种新的找θ\theta的方式-梯度下降

梯度下降

梯度下降是很常用的优化算法,能够在大范围的问题中找到最优解。梯度下降做的就是,不断迭代地调整参数,从而使得成本函数最小化。

图1 梯度下降(Gradient Descent)

让我们看图1,我们看到得每一个黑点就是一个θ\theta,第一个θ\theta值是随机的。我们的算法每一次迭代调整参数后,θ\theta就会向着能使得成本函数最小的θ^\hat{\theta}走去。

θ\theta每次怎么走多远(步长),就需要根据一个超参数-学习率。在设置学习率时,如果设置太小,下降得就会越慢,虽然最终我们还是能够得到最优解,但是需要时间会很长。

但是我们也要注意设置学习率不能太大,否则会遇见如图2的现象:

图2 学习率过高

迭代得到的θ\theta不仅没有朝着最小值去,反而还在远离,也就是在发散。这样我们无论怎么迭代,最终都不会得到最优解了。

陷阱

除了学习率可能存在问题之外,还有就是我们的成本函数也会存在陷阱,如图3所示:

图3 陷阱

如果我们的成本函数不是那么完美的像一个碗的凸函数,这里的凸函数不要和高数的凸函数搞混了,他们的方向刚好是相反的,数学分析中以下凸为凸函数,这个问题之前还把我搞愣住了,后来我去查了一下才知道原来方向是不一样的- -!。

像如图三一样的成本函数存在局部最小值,也会导致我们的算法最终找错,或者如果随机点在右边开始,那么我们看到他有一段比较平坦的,这个会导致需要多次迭代,如果时间不够,也会导致我们找不到全局最小值。

幸运的是,我们用的MSE是一个凸函数(类似图2),只有全局最小值。

同时我们需要注意,例如现在又两个特征θ1\theta_{1}θ2\theta_{2},如果他们的取值范围比较接近(如进行标准化后的特征),那么我们能够更快的找到最优解,但是如果两个特征取值范围相差很大,会导致最终消耗的时间增加,这是为什么呢?我们先看一张图:

图4 有(左图)和没有(右图)特征缩放的梯度下降

我们这里以两个特征当坐标,中间的圆形就是我们的成本函数,圆心为最小值。左图的梯度下降中,我们发现它能够很快找到最小值,但是右边的似乎就不那么顺利,一开始也挺快的,但是到了相对平坦的后面部分时,却变得很慢。所以我们需要做好特征缩放。

批量梯度下降

让我们开始实现一下梯度下降吧。首先我们需要计算出每个关于θj\theta_{j}的梯度,也就是他是怎么变小的。这里就需要成本函数对θj\theta_{j}求偏导数。还记得我们的成本函数MSE吗?忘记了也没事,我重新再写一遍:

MSE=(X,hθ)=1mi=1m(θTx(i)y(i))2MSE = (X,h_{\theta}) = \frac{1}{m} \sum_{i=1}^{m}(\theta^{T}x^{(i)}-y^{(i)} )^{2}

OK,接下来我们求出的偏导数(MSE(X,hθ)(MSE(X,h_{\theta})一般会简写成MSE(θ)MSE(\theta) ):

MSE(θ)θj=2mi=1m(θTx(i)y(i))xj(i)\frac{\partial MSE(\theta) }{\partial \theta_{j}} = \frac{2}{m}\sum_{i=1}^{m}(\theta^{T}x^{(i)}-y^{(i)} )x^{(i)}_{j}

成本函数的梯度向量(记作θMSE(θ)\triangledown_{\theta}MSE(\theta)):

θMSE(θ)=(MSE(θ)θ0MSE(θ)θn)=2mXT(Xθy)\triangledown_{\theta}MSE(\theta) = \begin{pmatrix} \frac{\partial MSE(\theta) }{\partial \theta_{0}} \\ \vdots \\ \frac{\partial MSE(\theta) }{\partial \theta_{n}} \\ \end{pmatrix} = \frac{2}{m}X^{T}(X\theta - y)

当我们得到了梯度向量后,我们就需要将其乘上学习率η\eta,最后θ\theta减去前面的结果,就是下一步的θ\theta

θ=θηθMSE(θ)\theta = \theta - \eta \triangledown _{\theta}MSE(\theta)

^^!终于把烦人的公式了解完了,我们赶紧看看怎么用代码实现:

# 借用了上一篇的X和y,原谅我懒人一个
X = 2 * np.random.rand(100,1)
y = 4 + 3 * X + np.random.randn(100,1)
X_b = np.c_[np.ones((100,1)),X]

# 学习率
eta = 0.1 
# 迭代次数
n_iterations = 1000
# 特征数
m=100
# 初始化随机的theta
theta = np.random.randn(2,1)

# 不断循环迭代
for iteration in range(n_iterations):
    gradients = 2/m * X_b.T.dot(X_b.dot(theta)-y)
    theta = theta - eta * gradients

theta # 输出 array([[3.81554623], [2.94310113]])

最后我们求出的结果还是不错的,这里需要注意的是,我们用了全部的训练集的数据,来计算每一步,这个就是我们实现的-批量梯度下降

我们再看看不同学习度的表现,如下图:

图5 各种学习度的梯度下降

这次我们能够很清楚的看到,如果学习度很小(左图),那么我们要找到最优解就会很慢,但是如果过大(右图),我们就会在训练中错过最优解。

随机梯度下降

我们实现了了批量梯度下降,也会发现一个速度慢的原因:每次计算梯度都需要全部的训练集。那有没有不需要全部数据集的实现方法呢,那必须有:就是接下来我们要看到的-随机梯度下降。它在每次计算梯度时,只需要随机取一个实例。

看到这个随机取一个计算,我们自然也能想到会有一个问题:那就是每次计算出的梯度会很不规则,大大小小,导致整个θ\theta窜上窜下的。不过也正因为如此,我们的θ\theta不容易被局部最小值困住,但他也永远不会走到全局最小值,而是不断在全局最小值附近旋转跳跃。因此最后算法的得出的值不是最优的,但也是足够好的。

我们知道,因为梯度计算的不规则性,最后我们需要让θ\theta不要太随意窜来窜去,我们就需要降低学习度,让他能够一步一步走向最优解,我们把这个降低学习度的方法叫学习率调度。ok,上代码来:

m = len(X_b)
np.random.seed(42)
# 迭代次数
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):
    # 因为我们有m个实例,每次随机获取一个实例
    # 其实在这边我们也发现了,如果按照这样随机来取,有些实例可能会一直取不到,有些实例被取到多次
    # 所以尽可能先将数据集混洗一次
    for i in range(m):
        random_index = np.random.randint(m)
        xi = X_b[random_index:random_index+1]
        yi = y[random_index:random_index+1]
        gradients = 2 * xi.T.dot(xi.dot(theta) - yi)
        # 学习度下降
        eta = learning_schedule(epoch * m + i)
        theta = theta - eta * gradients

看到之后,是不是读者有了更加清晰的了解了。 当然我们也可以使用Scikit-learn提供的SGDRegressor()实现随机梯度下降的线性回归:

from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(max_iter=1000,tol=1e-3,penalty=None, eta0=0.1)
# 注意这里需要通过ravel函数将y转成一维数组
sgd_reg.fit(X, y.ravel())

SGDRegressor的超参数含义:

  • max_iter 迭代次数
  • tol 当theta低于设置的该值时,也可以停止训练
  • penalty 是否设置正则
  • eta0 开始的学习率

小批量梯度下降

我们发现了批量梯度下降和随机梯度下降的都有一丢丢的小问题,但是也有不少优点。我们可以集合他们的优点,削弱点他们的问题。小批量梯度下降就是不需要全部的训练集计算,也不仅仅基于某一个实例。这样的算法更加的稳定:

# 迭代次数
n_iterations = 50
# 批量大小,也就是每次取多小个实例
minibatch_size = 20

np.random.seed(42)
theta = np.random.randn(2,1)  # random initialization

# 学习率调度
t0, t1 = 200, 1000
def learning_schedule(t):
    return t0 / (t + t1)

t = 0
# 循环迭代
for epoch in range(n_iterations):
    # 每次迭代,代将m个实例打乱,避免无法取到某些实例
    shuffled_indices = np.random.permutation(m)
    X_b_shuffled = X_b[shuffled_indices]
    y_shuffled = y[shuffled_indices]
    # 根据批量大小计算每次theta
    for i in range(0, m, minibatch_size):
        t += 1
        # 从混洗的数据集中取值
        xi = X_b_shuffled[i:i+minibatch_size]
        yi = y_shuffled[i:i+minibatch_size]
        gradients = 2/minibatch_size * xi.T.dot(xi.dot(theta) - yi)
        # 学习度下降
        eta = learning_schedule(t)
        theta = theta - eta * gradients

最后我放一张图看看三个梯度下降的训练过程:

图6 训练过程

这下我们能够更加清楚的明白:

  • 批量梯度下降很稳,就是看上去慢慢悠悠的
  • 随机梯度下降很快,但是就是有点调皮,一直在最优解附近乱逛
  • 小批量梯度下降结合了两者优点,弱化缺点