梯度下降法基础

对于一个一元函数 f ( x ) f(x) f(x),如果在点 x x x附近有微小的变化 Δ x \Delta x Δx,则 f ( x ) f(x) f(x)的变化 f ( x + Δ x ) − f ( x ) f(x+\Delta x)-f(x) f(x+Δx)f(x)可表示成如下微分形式。
f ( x + Δ x ) − f ( x ) ⋍ f ′ ( x ) Δ x f(x+\Delta x)-f(x)\backsimeq f'(x) \Delta x f(x+Δx)f(x)f(x)Δx
也就是说,在点 x x x附近;如果 Δ x \Delta x Δx f ′ ( x ) f'(x) f(x)的符号相同,那么 f ′ ( x ) Δ x f'(x) \Delta x f(x)Δx是正数;如果符号相反,那么 f ′ ( x ) Δ x f'(x) \Delta x f(x)Δx是负数。
例如,取 Δ x = − α f ′ ( x ) \Delta x=-\alpha f'(x) Δx=αf(x) α \alpha α是一个很小的正数),那么 f ( x + Δ x ) − f ( x ) = − α f ′ ( x ) 2 f(x+\Delta x)-f(x)= -\alpha f'(x)^2 f(x+Δx)f(x)=αf(x)2是负数,即 f ( x + Δ x ) f(x+\Delta x) f(x+Δx)的值比 f ( x ) f(x) f(x)小,或者说, x x x沿着 f ′ ( x ) f'(x) f(x)的反方向 − f ′ ( x ) -f'(x) f(x)运动 Δ x \Delta x Δx的距离,其函数值 f ( x + Δ x ) f(x+\Delta x) f(x+Δx)比原来的 f ( x ) f(x) f(x)小。
x x x沿着其导函数 f ′ ( x ) f'(x) f(x)的反方向( − f ′ ( x ) -f'(x) f(x))移动一个微小的距离 − α f ′ ( x ) -\alpha f'(x) αf(x),就能到达 x n e w = x − α f ′ ( x ) x_{new}=x-\alpha f'(x) xnew=xαf(x) x n e w x_{new} xnew的函数值 f ′ ( x n e w ) f'(x_{new}) f(xnew)肯定小于之前的函数值 f ′ ( x ) f'(x) f(x)。随着 x x x不断接近最小值点, f ′ ( x ) f'(x) f(x)将不断接近0(因为在函数极值点 x ∗ x^* x f ′ ( x ∗ ) = 0 f'(x^*)=0 f(x)=0), x x x移动的距离 Δ x \Delta x Δx越来越接近0。
这就是梯度下降法的思想,即从一个初始的 x x x出发,不断用以下公式更新 x x x的值。
x = x − α f ′ ( x ) x=x-\alpha f'(x) x=xαf(x)
对于当前的 x x x,沿着其导数(梯度)的反方向( − f ′ ( x ) -f'(x) f(x))移动,就能使 f ( x ) f(x) f(x)变小。在理想情况下,对于达到最小值 f ( x ) f(x) f(x) x x x,有 f ′ ( x ) = 0 f'(x)=0 f(x)=0。此时,如果迭代更新 x x x x x x的值将不会发生变化。
当然,移动的步伐不能太大。这是因为,根据导数的定义,上述近似公式只在点 x x x附近适用。如果移动的步伐太大,就有可能跳过最优值所对应的点 x x x,导致点 x x x的值来回震荡。梯度下降法就是求一个逼近的最优解。为了避免一直迭代下去,可以用下列方法检查是否足够逼近最优解。

  • f ′ ( x ) f'(x) f(x)的绝对值足够小。
  • 迭代次数已达到预先设定的最大迭代次数。

梯度下降法的示例代码如下。其中,参数df用于计算函数 f ( x ) f(x) f(x)的导数 f ′ ( x ) f'(x) f(x),x是变量的初始值,alpha是学习率,iteration表示迭代次数,epsilon用于检查 f ′ ( x ) f'(x) f(x)的值是否接近0。

def gradient_descent(df, x, alpha=0.01, iterations=100, epsilon=1e-8):
    history = [x]
    for i in range(iterations):
        if abs(df(x)) < epsilon:
            print("梯度足够小!")
            break
        x = x - alpha * df(x)
        history.append(x)
    return history

该函数将迭代过程中所有被更新的 x x x保存在一个Python的列表对象history中,并返回这个对象。
以函数 f ( x ) = x 3 − 3 x 2 − 9 x + 2 f(x)=x^3-3x^2-9x+2 f(x)=x33x29x+2为例,其导函数为 f ′ ( x ) = 3 x 2 − 6 x − 9 f'(x)=3x^2-6x-9 f(x)=3x26x9,要想求 x = 1 x=1 x=1附近函数的极小值,可调用gradient_descent()函数,代码如下。

df = lambda x: 3 * x ** 2 - 6 * x - 9
path = gradient_descent(df, 1., 0.01, 200)
print(path[-1])
###输出###
梯度足够小!
2.999999999256501

执行以上代码,得到了极值点 x = 2.999999999256501 x=2.999999999256501 x=2.999999999256501
以下代码可以将迭代过程中 x x x所对应的曲线绘制出来,如图所示。

import numpy as np
import matplotlib.pyplot as plt
f = lambda x: np.power(x, 3) - 3 * x ** 2 - 9 * x + 2
x= np.arange(-3, 4, 0.01)
y = f(x)
plt.plot(x, y)

path_x = np.asarray(path)
path_y = f(path_x)
plt.quiver(path_x[:-1], path_y[:-1], path_x[1:]-path_x[:-1], path_y[1:]-path_y[:-1], scale_units='xy', angles='xy', scale=1, color='k')
plt.scatter(path[-1], f(path[-1]))
plt.show()

Figure_1.png
对于多变量函数,梯度下降法的原理和单变量函数相同,只不过用梯度代替了导数,公式如下。
f ( x + Δ x ) − f ( x ) ⋍ ∇ f ( x ) Δ x f(x+\Delta x)-f(x)\backsimeq \nabla f(x) \Delta x f(x+Δx)f(x)f(x)Δx
以Beale’s函数公式为例,
f ( x , y ) = ( 1.5 − x + x y ) 2 + ( 2.25 − x + x y 2 ) 2 + ( 2.625 − x + x y 3 ) 2 f(x, y)=(1.5-x+xy)^2+(2.25-x+xy^2)^2+(2.625-x+xy^3)^2 f(x,y)=(1.5x+xy)2+(2.25x+xy2)2+(2.625x+xy3)2
这个函数的全局极小值点是 ( 3 , 0.5 ) (3,0.5) (3,0.5),可以用如下Python代码计算函数值。

f = lambda x, y: (1.5 - x + x * y) ** 2 + (2.25 - x + x * y ** 2) ** 2 + (2.625 - x + x * y ** 3) ** 2

为了绘制这个函数所对应的曲面,可以先取一些均匀分布的坐标值,示例如下。

xmin, xmax, xstep = -4.5, 4.5, .2
ymin, ymax, ystep = -4.5, 4.5, .2
x_list = np.arange(xmin, xmax + xstep, xstep)
y_list = np.arange(ymin, ymax + ystep, ystep)

然后,根据以上代码中的x_list和y_list,用np.meshgrid()函数求出它们的交叉位置的坐标点(x, y),并计算这些坐标点所对应的函数值,示例如下。

x, y = np.meshgrid(x_list, y_list)
z = f(x, y)

最后,调用plot_surface()函数,绘制曲面,示例如下。

ax.plot_surface(x, y, z, norm=LogNorm(), rstride=1, cstride=1, edgecolor='none', alpha=.8, cmap=plt.cm.jet)

完整代码如下所示。

import numpy as np
import random
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.colors import LogNorm

f = lambda x, y: (1.5 - x + x * y) ** 2 + (2.25 - x + x * y ** 2) ** 2 + (2.625 - x + x * y ** 3) ** 2
minima = np.array([3.0, 0.5])
minima_ = minima.reshape(-1, 1)
xmin, xmax, xstep = -4.5, 4.5, .2
ymin, ymax, ystep = -4.5, 4.5, .2
x_list = np.arange(xmin, xmax+xstep, xstep)
y_list = np.arange(ymin, ymax+ystep, ystep)
x, y = np.meshgrid(x_list, y_list)
z = f(x, y)

fig = plt.figure(figsize=(8, 5))
ax = plt.axes(projection='3d', elev=50, azim=-50)
ax.plot_surface(x, y, z, norm=LogNorm(), rstride=1, cstride=1, edgecolor='none', alpha=.8, cmap=plt.cm.jet)
ax.plot(*minima_, f(*minima_), 'r*', markersize=10)
ax.set_xlabel('$x$')
ax.set_ylabel('$y$')
ax.set_zlabel('$z$')
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
plt.show()

Figure_1.png
接着计算 f ( x , y ) f(x, y) f(x,y)关于 ( x , y ) (x,y) (x,y)的偏导数,并使用Matplotlib的quiver()函数在二维坐标平面上绘制这些坐标点的梯度的方向,代码如下。

df_x = lambda x, y: 2 * (1.5 - x + x * y) * (y - 1) + 2 * (2.25 - x + x * y ** 2) * (y ** 2 - 1) + 2 * (2.625 - x + x * y ** 3) * (y ** 3 - 1)
df_y = lambda x, y: 2 * (1.5 - x + x * y) * x + 2 * (2.25 - x + x * y ** 2) * (2 * x * y) + 2 * (2.625 - x + x * y ** 3) * (3 * x * y ** 2)
dz_dx = df_x(x, y)
dz_dy = df_y(x, y)

fig, ax = plt.subplots(figsize=(10, 6))
ax.contour(x, y, z, levels=np.logspace(0, 5, 35), norm=LogNorm(), cmap=plt.cm.jet)
ax.quiver(x, y, x-dz_dx, y-dz_dy, alpha=.5)
ax.plot(*minima_, 'r*', markersize=18)
ax.set_xlabel('$x$')
ax.set_ylabel('$y$')
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
plt.show()

Figure_1.png
为了直接利用之前给出的梯度下降法的代码,可用一个Numpy向量表示x,即将

if abs(df(x)) < epsilon:

修改为:

if np.max(np.abs(df(x))) < epsilon:

将分离的x, y坐标点数组组合起来,示例如下。

x_ = np.vstack((x.reshape(1, -1), y.reshape(1, -1)))

可以定义一个针对这个向量化的坐标点x的梯度函数df,示例如下。在以下代码中,还实现了修改后的向量化梯度下降法。

df = lambda x: np.array([2 * (1.5 - x[0] + x[0]*x[1]) * (x[1] - 1) + 2 * (2.25 - x[0]
                         + x[0]*x[1]**2) * (x[1]**2 - 1) + 2 * (2.625 - x[0]
                        + x[0]*x[1]**3) * (x[1]**3 - 1), 2 * (1.5 - x[0]
                        + x[0]*x[1])*x[0] + 2 * (2.25 - x[0]
                        + x[0]*x[1]**2) * (2 * x[0] * x[1]) + 2 * (2.625 - x[0]
                        + x[0]*x[1]**3) * (3 * x[0] * x[1] ** 2)])

def gradient_descent(df, x, alpha=0.01, iterations=100, epsilon=1e-8):
    history = [x]
    for i in range(iterations):
        if np.max(np.abs(df(x))) < epsilon:
            print("梯度足够小!")
            break
        x = x - alpha * df(x)
        history.append(x)
    return history

执行以下代码,从[3, 4]出发,求这个曲面的极值点。

x0 = np.array([3., 4.])
print("初始点", x0, "的梯度",df(x0))
path = gradient_descent(df, x0, 0.000005, 300000)
print("极值点: ", path[-1])

因为x的初始梯度值很大,所以学习率 α \alpha α必须取很小的值,否则会导致震荡或得到的值很大。最后收敛到点 [ 2.70735828 , 0.41689171 ] [2.70735828,0.41689171] [2.70735828,0.41689171],但它不是最优点。
以下代码将绘制迭代过程中x的变化情况。

def plot_path(path, x, y, z, minima_, xmin, xmax, ymin, ymax):
    fig, ax = plt.subplots(figsize=(10,  6))
    ax.contour(x, y, z, levels=np.logspace(0, 5, 35), norm=LogNorm(), cmap=plt.cm.jet)
    ax.quiver(path[:-1, 0], path[:-1, 1], path[1:, 0]-path[:-1, 0], path[1:, 1]-path[:-1, 1], scale_units='xy', angles='xy', scale=1, color='k')
    ax.plot(*minima_, 'r*', markersize=18)
    ax.set_xlabel('$x$')
    ax.set_ylabel('$y$')
    ax.set_xlim((xmin, xmax))
    ax.set_xlim((ymin, ymax))
    plt.show()

path = np.asarray(path)
plot_path(path, x, y, z, minima_, xmin, xmax, ymin, ymax)

Figure_1.png

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值