梯度下降法的全面讲解及python实现

函数的梯度方向表示了函数值增长速度最快的方向,那么和它相反的方向就可以看作函数值减少速度最快的方向。就机器学习模型优化的问题而言,当目标设定为求解目标函数最小值时,只要朝着梯度下降的方向前进就能不断逼近最优值。

最简单的梯度下降算法—固定学习率的方法,这种梯度下降算法由两个函数和三个变量组成。

    函数1:待优化的函数f(x),它可以根据给定的输入返回函数值

    函数2:待优化函数的导数g(x),它可以根据给定的输入返回函数的导数值

    变量x:保存当前优化过程的参数值,优化开始时该变量将被初始化为某个数值,优化过程中这个变量会被不断变化,直到它找到最小值。

    变量grad:保存变量x点处的梯度值

    变量step:表示沿着梯度下降方向行进的步长,也被称为学习率。

(备注:代码在本文最后有完整代码,此处只给出其中函数部分。所以实现的图和结果想看的直接先到文末把代码copy下来,然后直接执行即可)

python函数举例:


 
 
  1. def gd(x_start, step, g):
  2.     x = np.array(x_start,'float64')
  3.     for i in range(30):
  4.         grad = g(x)
  5.         x -= grad*step
  6.         print("[Epoch{0}] grad={1}, x={2}".format(i, grad, x))
  7.         if isinstance(grad, float):
  8.             if abs(grad) < 1e-6:
  9.                 break
  10.         else:
  11.             if abs( sum( grad)) < 1e-6:
  12.                 break
  13.     return x

假设函数f(x)=x^2-2x+1,则很容易看出最小值是x=1,f(x)=0。其图像就是一个简单的抛物线。

执行gd(5,0.55,g):

如果步长设置过大我们可以看到,参数的梯度不但没有收敛,反而越来越大:

执行gd(5,2.55,g):

 

优化的本意是让目标值朝着梯度下降的方向前进,结果它却走向了另外一个方向。实际上,函数在某一点的梯度指的是它在当前变量处的梯度,对于这一点来说,它的梯度方向指向了函数上升的方向,可以利用泰勒公式证明在一定范围内,沿着负梯度方向前进,函数值是会下降的。但是,公式只能保证在一定范围内是成立的,从函数的实际图像中也可以看出,如果优化的步长太大,就有可能跳出函数值下降的范围,那么函数值是否下降就不好说了 。 当然有可能越变越大,造成优化的悲剧。

 

既然小步长会使目标值的梯度下降,大步长会使梯度发散,那么有没有一个步长会让优化问题原地打转呢?在这个问题中,这样的步长是存在且容易找到的。如果step= 1时,求解会原地打转,梯度下降法就失效了。

执行gd(5,1,g):

 

通过上面的实验可以发现,对于初始值为5这个点,当步长大于1时,梯度下降法会出现求解目标值发散的现象;而步长小于1时,则不会发散,参数会逐渐收敛。所以1就是步长的临界点。那么问题又来了,对于别的初始值,这个规律还适用吗?接下来就把初始值换成4,再进行一次实验:

 

实验发现,步长等于1时,初始值设置成(最优值除外)任何数字,参数都不会收敛到最优值。这个实验揭示了一个道理:对于这个二次函数,如果采用固定步长的梯度下降法进行优化,步长要小于1,否则不论初始值等于多少,问题都会发散或者原地打转!

若换个函数:fx=4x2-4x+1,它的安全步长就不再是1了,而是0.25,这里就不再重复了,感兴趣的可以自己改下函数表达式f和导数表达式g,然后执行gd(5,0.25,g)即可.

 

动量(Momentum)算法:动量代表了前面优化过程积累的“能量”,它将在后面的优化过程中持续发戚,推动目标值前进。拥有了动量,一个已经结束的更新量不会立刻消失,只会以一定的形式衰减,剩下的能量将继续在优化中发挥作用。

python函数举例:


 
 
  1. def momentum(x_start, step, g, discount=0.7):
  2.     x = np.array(x_start, dtype='float64')
  3.     pre_grad = np.zeros_like(x)  # 存储积累的动量
  4.     for i in range(60):
  5.         grad = g(x)
  6.         pre_grad = pre_grad*discount + grad*step
  7.         x -= pre_grad
  8.         print("[Epoch {0}] grad={1},x={2}".format(i, grad, x))
  9.         if abs(sum(grad)) < 1e-6:
  10.             break
  11.     return x

代码中多出了一个新变量pre_grad。这个变量就是用于存储历史积累的动量,每一轮迭代动量都会乘以一个打折量(discount)做能量衰减但是它依然会被用于更新参数。动量算法和前面介绍的梯度下降法相比有什么优点呢?用形象的话来说,它可以帮助目标值穿越“狭窄山谷”形状的优化曲面,从而到达最终的最优点。

假设函数,函数在等高线图上的样子如图(三维图和平面图):

则容易看出其中中心的点表示最优值。把等高线上的图像想象成地形图,从等高线的疏密程度可以看出,这个函数在y轴方向十分陡峭,在x轴方向则相对平缓。也就是说,函数在y轴的方向导数比较大,在x轴的方向导数比较小

将50轮迭代过程中参数的优化过程图画出来,如图:

gd([150,75],0.016,g)

可以看出目标值从某个点出发,整体趋势向着最优点前进,它和最优值的距离不断靠近,说明优化过程在收敛,步长设置是没有问题的,但是前进的速度似乎有点乏力,50轮迭代井没有到达最优值,有可能是步长设置得偏小。有了前面的经验,这次设置步长时很谨慎。我们只将步长稍变大一点,结果如图:

虽然优化效果有了一定的提高,但成效依然不明显,而且优化的过程图中出现了参数值左右抖动的现象,这是怎么回事呢?看上去参数在优化过程中产生了某种“打转儿”的现象,做了很多无用功。如果继续增加步长,优化曲线会变成:

从结果来看,这个步长已经是能设置的最大步长,如果步长再设大些,就要从U形赛道飞出去,优化参数的梯度也将发散出去。在这个问题中,由于两个坐标轴方向的函数的陡峭性质不同,两个方向对最大步伏的限制不同,显然y方向对步长的限制更严格。但满足y方向的更新,x方向就无法获得充分的更新,这样梯度下降法将无法获得很好的效果。

我们会发现每一次的行动只会在以下三个方向进行:沿-x方向滑行;沿+y方向滑行。沿-y方向滑行。

这样看来,要是能把行动的力量集中在往-x方向走而不是沿y方向打转就好了。这个想法可以被动量算法实现。

我们可以想象,使用了动量后,历史的更新量会以衰减的形式不断作用在这些方向上,那么沿-y和+y两个方向的动量就可以相互抵消,而-x方向的力则会一直加强,这样虽然还会沿y方向打转,但是他在-x方向的速度会因为之前的累积变得越来越快。

momentum ( [150, 75],0.016, g)

优化曲线果然没有令人失望,尽管还是有打转现象,但是在50轮迭代后,他进入了最优点的邻域,完成了优化任务,和前面的梯度下降法相比有了很大的进步。

当然,还是暴露了动量优化存在的一点问题,前面几轮迭代过程中目标值在ν轴上的震荡比过去还要大。在发明动量算法后,又有科研人员发明了基于动量算法的改进算法,解决了动量算法没有解决的问题一一更强烈的抖动,干脆停止玩耍,专心赶路。这就是牛顿动量(Nesterov)法

python函数代码:


 
 
  1. def nesterov(x_start, step, g, discount=0.7):
  2.     x = np.array(x_start, dtype='float64')
  3.     pre_grad = np.zeros_like(x)
  4.     for i in range(50):
  5.         x_future = x - step*discount*pre_grad
  6.         grad = g(x_future)  # 计算更新后的优化点的梯度
  7.         pre_grad = pre_grad*discount+grad
  8.         x -= pre_grad*step
  9.         print('[Epoch {0} grad={1}, x={2}'.format(i, grad, x))
  10.         if abs(sum(grad)) < 1e-6:
  11.             break
  12.     return x

算法不再贪玩,放弃在U形赛道上摩擦。那么,Nesterov算法和动量算法相比有什么区别呢?动量算法计算的梯度是在当前的目标点的;而 Nesterov算法计算的梯度是在动量更新后的优化点的。这其中关键的区别在于计算梯度的点。

可以想象一个场景,当优化点已经积累了某个抖动方向的梯度后,对动量算法来说,虽然当前点的梯度指向积累梯度的相反方向,但是量不够大,所以最终的优化方向还会在积累的方向上前进一段,这就是上图所示的效果。对Nesterov方法来说,如果按照积累方法再向前多走一段,则梯度中指向积累梯度相反方向的量会变大许多,最终两个方向的梯度抵消,反而使抖动方向的量迅速减少。Nesterov的衰减速度确实比动量方法的快不少。

最后来讲述动量在数值上的事情。科研人员已经给出了动量打折率的建议配置一一0.9 (刚才的例子全部是0.7)那么0.9的动量打折率能使历史更新量发挥多大作用呢?

如果用G表示每一轮迭代的动量,g表示当前一轮迭代的更新量(方向×步长),t表示迭代轮数,γ表示动量的打折率,那么对于时刻t的梯度更新量就有公式:

这样可以计算出对于第一轮迭代的更新g0来说,从G0到GT,它的总贡献量为。它的贡献和是一个等比数列的和,比值为γ。如果γ=0.9,那么根据公比小于1的等比数列的极限公式,可以知道更新量在极限状态下的贡献值:

那么当γ=0.9时,它一共贡献了相当于自身10倍的能量。如果γ=0.99,那就是100倍能量。在实际应用中,打折率的设置需要分析具体任务中更新量需要多“持久”的动力。

 

python完整代码:

import numpy as np
import sklearn.datasets as d
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D


def plot1():
    reg_data = d.make_regression(100, 1, 1, 1, 1.0)
    plt.plot(reg_data[0], reg_data[1])
    plt.show()

    cls_data = d.make_classification(100, 2, 2, 0, 0, 2)
    plt.plot(cls_data[0], 'o')
    plt.show()

    fig = plt.figure()
    ax = Axes3D(fig)
    X = np.linspace(0.01, 0.99, 101)
    Y = np.linspace(0.01, 0.99, 101)
    X, Y = np.meshgrid(X, Y)
    Z = -X * np.log2(Y) - (1 - X) * np.log2(1 - Y)
    ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='rainbow')
    plt.show()

    xi = np.linspace(-200,200,800)
    yi = np.linspace(-100,100,800)
    X, Y = np.meshgrid(xi, yi)
    Z = X*X+50*Y*Y
    plt.contour(X, Y, Z, 10)
    plt.plot(0,0,"o")
    plt.show()
    fig = plt.figure()
    ax = Axes3D(fig)
    ax.plot_surface(Y, X, Z, rstride=1, cstride=1, cmap='rainbow')
    plt.show()

def gd(x_start, step, g):
    """
    梯度下降
   
:param x_start:
   
:param step:
   
:param g:
   
:return:
    """
   
result = [[], []]
    x = np.array(x_start,'float64')
    for i in range(30):
        grad = g(x)
        x -= grad*step
        print("[Epoch{0}] grad={1}, x={2}".format(i, grad, x))
        if isinstance(grad, float):
            if abs(grad) < 1e-6:
                break
        else
:
            if abs(sum(grad)) < 1e-6:
                break
       
result[0].append(x[0])
        result[1].append(x[1])
    return result

def momentum(x_start, step, g, discount=0.7):
    """
    动量算法
   
:param x_start:
   
:param step:
   
:param g:
   
:param discount:
   
:return:
    """
   
result = [[], []]
    x = np.array(x_start, dtype='float64')
    pre_grad = np.zeros_like(x)  # 存储积累的动量
   
for i in range(60):
        grad = g(x)
        pre_grad = pre_grad*discount + grad*step
        x -= pre_grad
        print("[Epoch {0}] grad={1},pre_grad={2},x={3}".format(i, grad, pre_grad, x))
        if abs(sum(grad)) < 1e-6:
            break
       
result[0].append(x[0])
        result[1].append(x[1])
    return result

def nesterov(x_start, step, g, discount=0.7):
    """
    Nesterov算法
   
:param x_start:
   
:param step:
   
:param g:
   
:param discount:
   
:return:
    """
   
result = [[], []]
    x = np.array(x_start, dtype='float64')
    pre_grad = np.zeros_like(x)
    for i in range(50):
        x_future = x - step*discount*pre_grad
        grad = g(x_future)  # 计算更新后的优化点的梯度
       
pre_grad = pre_grad*discount+grad
        x -= pre_grad*step
        print('[Epoch {0} grad={1}, x={2}'.format(i, grad, x))
        if abs(sum(grad)) < 1e-6:
            break
       
result[0].append(x[0])
        result[1].append(x[1])
    return result

def test1():
    def f(x):
        return x ** 2 - 2 * x + 1
    def g(x):
        return 2 * x - 2
    x = np.linspace(-5,7,100)
    y = f(x)
    plt.plot(x,y)
    plt.show()
    gd(5, 0.55, g)
    gd(5,2.55,g)
    gd(5,1,g)


def test2():
    xi = np.linspace(-200, 200, 800)
    yi = np.linspace(-100, 100, 800)
    X, Y = np.meshgrid(xi, yi)
    Z = X * X + 50 * Y * Y
    def f(x):
        return x[0]**2+50*x[1]**2
    def g(x):
        return np.array([2*x[0], 100*x[1]])
    result = gd([150,75],0.016,g)
    plt.contour(X, Y, Z, 10)
    plt.plot(0, 0, "o")
    plt.plot(result[0], result[1])
    plt.show()
    result = gd([150,75],0.019,g)
    plt.contour(X, Y, Z, 10)
    plt.plot(0, 0, "o")
    plt.plot(result[0], result[1])
    plt.show()
    result = gd([150, 75], 0.02, g)
    plt.contour(X, Y, Z, 10)
    plt.plot(0, 0, "o")
    plt.plot(result[0], result[1])
    plt.show()
    result = momentum([150,75],0.016,g)
    plt.contour(X, Y, Z, 10)
    plt.plot(0, 0, "o")
    plt.plot(result[0], result[1])
    plt.show()
    result = nesterov([150,75],0.012,g)
    plt.contour(X, Y, Z, 10)
    plt.plot(0, 0, "o")
    plt.plot(result[0], result[1])
    plt.show()


if __name__ == '__main__':
    plot1()
    test1()
    test2()

 

参考书籍:《强化学习精要 核心算法与TensorFlow实现》冯超著,电子工业出版社

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值