浅谈梯度下降法

· · 算法·理论

注:本专栏来源于之前的笔记,我又查了一些更专业的资料整理而成,欢迎质疑和讨论。

\text{Part I.} 概念

梯度下降是迭代法的一种,可以用于求解最小二乘问题,线性和非线性都可以。

在求解机器学习算法的模型参数,即无约束优化问题时,梯度下降(Gradient Descent)是最常采用的方法之一,另一种常用的方法是最小二乘法。

在求解损失函数的最小值时,可以通过梯度下降法来一步步的迭代求解,得到最小化的损失函数和模型参数值。

反过来,如果我们需要求解损失函数的最大值,这时就需要用梯度上升法来迭代了。

在机器学习中,基于基本的梯度下降法发展了两种梯度下降方法,分别为随机梯度下降法和批量梯度下降法。

对于可微分的数量场 f(x,y,z),定义 f 的梯度为 \nabla=(\frac{\partial f}{\partial x},\frac{\partial f}{\partial y},\frac{\partial f}{\partial z})

\text{Part II.} 引入

以线性拟合为例。假设目标变量为 Y_i=\displaystyle\sum^n_{i=1}\theta_i x_i+\Phi,其中 \theta_i 为参量,\Phi 为偏值。

使用的损失函数以线性拟合中 L_2 损失函数为例。

L_2=\dfrac{1}{n}\displaystyle\sum^n_{i=1}(Y_i-f(x_i))^2

需求出一组参量 \{\theta_i\} 使得损失函数最小。

坐标是 xy,函数表达式里的 \theta 已经知道了,所以我们是找到最合适的 (x,y) 使得函数值最小。如果现在已知样本 (x,y),那么变量就是 \Phi\theta_i,并不是 x_i,以 \theta_i\Phi 作为输入变量,x_iy_i 都是已知的固定值。如果作图,则纵坐标就是损失函数的值。

我们的问题是已知样本的坐标,来求解一组 \{\theta_i\} 参数,使得损失函数的值最小。如何找到最低点?最低点对应的横坐标所有维度就是我们想得到的 \Phi\theta_i,而纵坐标就是损失函数的最小值。找到最低点所有答案就全部解出来了。

现在需要一种较慢的调整法让我们得到答案,这就是梯度下降法。

\text{Part III.} 算法概述

表达式如下:

\theta_i=\theta_{i-1}-\alpha\nabla J_{\theta}(\theta_{i-1})

其中 \alpha 为步长,\nabla J_{\theta}(\theta_{i-1}) 为梯度。

我们按照上述表达式一直不停地更新 \theta 的值,一直到 \theta 收敛不变为止,当我们到达山底,此时函数的梯度就是 0 了,\theta 值也就不会再更新了,因为表达式的后半部分一直是 0 了。

整个过程中损失函数单调减,但是也可能会导致在极小值点附近两侧卡着动不了。这时候就是奖励函数的用处——使步长减少,梯度收敛至 0

\text{Part IV.} 梯度下降法的数学计算

\text{Part V.} 梯度下降法的分类

当训练样本数很大时,批量梯度下降的每次更新都会是计算量很大的操作,而随机梯度下降可以利用单个训练样本立即更新,因此随机梯度下降 通常是一个更快的方法。但随机梯度下降也有一个缺点,那就是 \theta 可能不会收敛,而是在最小值附近振荡,但在实际中也都会得到一个足够好的近似。

实际情况下,我们一般不用固定的学习率,而是让它随着算法的运行逐渐减小到零,也就是在接近“山底”的时候慢慢减小下降的“步幅”,换成用“小碎步”走,这样它就更容易收敛于全局最小值而不是围绕它振荡了。

\text{Part VI.} 两个例子

这两段分别是求单变量函数的最小值以及根据给定样本求 \theta 组合的代码实现。

第一组:

import matplotlib.pyplot as plt
import numpy as np
# fx的函数值
def fx(x):
    return x**2

#定义梯度下降算法
def gradient_descent():
    times = 10 # 迭代次数
    alpha = 0.1 # 学习率
    x =10# 设定x的初始值
    x_axis = np.linspace(-10, 10) #设定x轴的坐标系
    fig = plt.figure(1,figsize=(5,5)) #设定画布大小
    ax = fig.add_subplot(1,1,1) #设定画布内只有一个图
    ax.set_xlabel('X', fontsize=14)
    ax.set_ylabel('Y', fontsize=14)
    ax.plot(x_axis,fx(x_axis)) #作图

    for i in range(times):
        x1 = x          
        y1= fx(x)  
        print("第%d次迭代:x=%f,y=%f" % (i + 1, x, y1))
        x = x - alpha * 2 * x
        y = fx(x)
        ax.plot([x1,x], [y1,y], 'ko', lw=1, ls='-', color='coral')
    plt.show()

if __name__ == "__main__":
    gradient_descent()

第二组:

import numpy as np
import matplotlib.pyplot as plt   
#样本数据
x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
y = [3,4,5,5,2,4,7,8,11,8,10,11,13,13,16,17,16,17,18,20]
m = 20 #样本数量
alpha = 0.01#学习率
theta_0 = 1 #初始化theta_0的值
theta_1 = 1 #初始化theta_1的值

#预测目标变量y的值
def predict(theta_0,theta_1, x):
    y_predicted = theta_0 + theta_1*x
    return y_predicted

#遍历整个样本数据,计算偏差,使用批量梯度下降法
def loop(m, theta_0, theta_1, x, y):
    sum1 = 0
    sum2 = 0
    error = 0
    for i in range(m):
        a = predict(theta_0, theta_1, x[i]) - y[i]
        b = (predict(theta_0, theta_1, x[i]) - y[i])* x[i]
        error1 = a*a
        sum1 = sum1 + a
        sum2 = sum2 + b
        error = error + error1
    return sum1,sum2,error

#批量梯度下降法进行更新theta的值
def batch_gradient_descent(x, y, theta_0, theta_1, alpha, m):
    gradient_1 = (loop(m, theta_0, theta_1, x, y)[1]/m)
    while abs(gradient_1) > 0.001:#设定一个阀值,当梯度的绝对值小于0.001时即不再更新了
        gradient_0 = (loop(m, theta_0, theta_1, x, y)[0]/m)
        gradient_1 = (loop(m, theta_0, theta_1, x, y)[1]/m)
        error = (loop(m, theta_0, theta_1, x, y)[2]/m)
        theta_0 = theta_0 - alpha*gradient_0
        theta_1 = theta_1 - alpha*gradient_1
    return(theta_0, theta_1, error)

theta_0 = batch_gradient_descent(x, y, theta_0, theta_1, alpha, m)[0]
theta_1 = batch_gradient_descent(x, y, theta_0, theta_1, alpha, m)[1]
error = batch_gradient_descent(x, y, theta_0, theta_1, alpha, m)[2]
print ("The theta_0 is %f, The theta_1 is %f, The The Mean Squared Error is %f " %(theta_0,theta_1,error))

\text{Part VII.} 梯度下降法的调试

有时候遇到复杂的求导函数,很有可能求解梯度并不容易,在这种情况下,推导出公式以后,通过编程实现,但真正运行的时候,程序并不报错,但求得的梯度是错误的。那如何发现这种问题的原因所在呢?

一种思想就是通过在这个点的左边和右边各找一个临近点,连成的直线的斜率近似地作为改点的斜率,也就是我们所说的导数,其实这也就是导数的定义中的思想。并且将其扩展到高维空间也是一样的。

这种导数的求法从数学的意义上解释是很简单的,但是这样做带来的问题就是时间复杂度将会增加很多,因为需要在一个点进行求二阶导。

调试公式时间复杂度高,每求一次梯度都要花相当多的时间,因此一般这种方式只用于调试。