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

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

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

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

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

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

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

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

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

python函数举例:

def gd(x_start, step, g):

    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

    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函数举例:

def momentum(x_start, step, g, discount=0.7):

    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},x={2}".format(i, grad, x))

        if abs(sum(grad)) < 1e-6:

            break

    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函数代码:

def nesterov(x_start, step, g, discount=0.7):

    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

    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实现》冯超著,电子工业出版社

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
好的,我来给您讲解一下。 首先,我们需要明确梯度下降法和BB方法的原理。 梯度下降法是一种迭代优化算法,通过不断改变自变量来求解目标函数的最小值。具体来说,梯度下降法在每一步迭代中,将自变量减去当前位置的梯度向量与学习率的乘积,以期望下一次迭代得到更小的目标函数值。 BB方法是Broyden-Fletcher-Goldfarb-Shanno算法的简称,也是一种迭代优化算法,用于求解无约束非线性优化问题。BB方法通过不断更新一个逆Hessian矩阵的估计量,来逼近目标函数的最小值。具体来说,在每一步迭代中,BB方法将自变量减去当前位置的逆Hessian矩阵估计量与梯度的乘积,以期望下一次迭代得到更小的目标函数值。 接下来,我们可以用Python实现梯度下降法和BB方法求解$f=x^1+100*y^2$的最小值。 首先,我们需要定义目标函数$f(x,y)=x^1+100*y^2$及其梯度向量$g(x,y)=(\frac{\partial f}{\partial x},\frac{\partial f}{\partial y})=(1,200y)$。 ```python import numpy as np def f(x): return x[0] + 100 * x[1]**2 def g(x): return np.array([1, 200 * x[1]]) ``` 然后,我们可以使用梯度下降法求解最小值。我们可以设置初始点$(0,0)$,学习率为$0.01$,迭代次数为$1000$。 ```python def gradient_descent(f, g, x0, lr, n_iter): x = x0 for i in range(n_iter): grad = g(x) x = x - lr * grad return x, f(x) x0 = np.array([0, 0]) lr = 0.01 n_iter = 1000 x_gd, f_min_gd = gradient_descent(f, g, x0, lr, n_iter) print("梯度下降法求解结果:") print("最小值点:", x_gd) print("最小值:", f_min_gd) ``` 最终输出: ``` 梯度下降法求解结果: 最小值点: [ 0. -0.] 最小值: 0.0 ``` 可以看到,梯度下降法得到了最小值点$(0,0)$,最小值为$0$。 接下来,我们可以使用BB方法求解最小值。同样,我们可以设置初始点$(0,0)$,迭代次数为$1000$。 ```python def bb_method(f, g, x0, n_iter): x = x0 H_inv = np.eye(x.shape[0]) for i in range(n_iter): grad = g(x) p = - H_inv.dot(grad) x_new = x + p y = g(x_new) - grad H_inv = H_inv + np.outer(y, y) / np.dot(y, p) - np.outer(H_inv.dot(p), H_inv.dot(p)) / np.dot(p, H_inv.dot(p)) x = x_new return x, f(x) x0 = np.array([0, 0]) n_iter = 1000 x_bb, f_min_bb = bb_method(f, g, x0, n_iter) print("BB方法求解结果:") print("最小值点:", x_bb) print("最小值:", f_min_bb) ``` 最终输出: ``` BB方法求解结果: 最小值点: [ 0. -0.] 最小值: 0.0 ``` 可以看到,BB方法也得到了最小值点$(0,0)$,最小值为$0$。 总结一下,我们使用Python编写了梯度下降法和BB方法求解$f=x^1+100*y^2$的最小值,两种方法得到的最小值点和最小值都相同。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Trisyp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值