从自然现象中理解梯度下降

在大多数文章中,都以“一个人被困在山上,需要迅速下到谷底”来举例,这个人会“寻找当前所处位置最陡峭的地方向下走”。这个例子中忽略了安全因素,这个人不可能沿着最陡峭的方向走,要考虑坡度。

在自然界中,梯度下降的最好例子,就是泉水下山的过程:

  1. 水受重力影响,会在当前位置,沿着最陡峭的方向流动,有时会形成瀑布(梯度下降);
  2. 水流下山的路径不是唯一的,在同一个地点,有可能有多个位置具有同样的陡峭程度,而造成了分流(可以得到多个解);
  3. 遇到坑洼地区,有可能形成湖泊,而终止下山过程(不能得到全局最优解,而是局部最优解)。

梯度下降的数学理解

梯度下降的数学公式:

\theta{n+1} = \theta{n} - \eta \cdot \nabla J(\theta) \tag{1}

其中:

  • $$\theta_{n+1}$$:下一个值;
  • $$\theta_n$$:当前值;
  • $$-$$:减号,梯度的反向;
  • $$\eta$$:学习率或步长,控制每一步走的距离,不要太快以免错过了最佳景点,不要太慢以免时间太长;
  • $$\nabla$$:梯度,函数当前位置的最快上升点;
  • $$J(\theta)$$:函数。

梯度下降的三要素

  1. 当前点;
  2. 方向;
  3. 步长。

为什么说是“梯度下降”?

“梯度下降”包含了两层含义:

  1. 梯度:函数当前位置的最快上升点;
  2. 下降:与导数相反的方向,用数学语言描述就是那个减号。

亦即与上升相反的方向运动,就是下降。

梯度下降 - 图1

图2-9解释了在函数极值点的两侧做梯度下降的计算过程,梯度下降的目的就是使得x值向极值点逼近。

单变量函数的梯度下降

假设一个单变量函数:

J(x) = x ^2

  1. def target_function(x):
  2. '''
  3. 目标函数
  4. :param x:
  5. :return:
  6. '''
  7. y = x * x
  8. return y

我们的目的是找到该函数的最小值,于是计算其微分:

J’(x) = 2x

  1. def derivative_function(x):
  2. '''
  3. 目标函数导数
  4. :param x:
  5. :return:
  6. '''
  7. return 2*x

假设初始位置为:

x_0=1.2

假设学习率:

\eta = 0.3

根据公式(1),迭代公式:

x{n+1} = x{n} - \eta \cdot \nabla J(x)= x_{n} - \eta \cdot 2x\tag{1}

  1. x = x - eta * derivative_function(x)

假设终止条件为J(x)<1e-2,迭代过程是:

  1. x=0.480000, y=0.230400
  2. x=0.192000, y=0.036864
  3. x=0.076800, y=0.005898
  4. x=0.030720, y=0.000944

上面的过程如图2-10所示。

梯度下降 - 图2

双变量的梯度下降

假设一个双变量函数:

J(x,y) = x^2 + \sin^2(y)

  1. def target_function(x, y):
  2. '''
  3. 目标函数
  4. :param x:
  5. :param y:
  6. :return:
  7. '''
  8. J = x ** 2 + np.sin(y) ** 2
  9. return J

我们的目的是找到该函数的最小值,于是计算其微分:

{\partial{J(x,y)} \over \partial{x}} = 2x

{\partial{J(x,y)} \over \partial{y}} = 2 \sin y \cos y

  1. def derivative_function(theta):
  2. '''
  3. 目标函数的两个偏导数
  4. :param theta:
  5. :return:
  6. '''
  7. x = theta[0]
  8. y = theta[1]
  9. return np.array([2 * x, 2 * np.sin(y) * np.cos(y)])

假设初始位置为:

(x_0,y_0)=(3,1)

假设学习率:

\eta = 0.1

根据公式(1),迭代过程是的计算公式:

(x{n+1},y{n+1}) = (x_n,y_n) - \eta \cdot \nabla J(x,y) = (x_n,y_n) - \eta \cdot (2x,2 \cdot \sin y \cdot \cos y) \tag{1}

  1. theta = np.array([3, 1])
  2. theta = theta - eta * d_theta

根据公式(1),假设终止条件为J(x,y)<1e-2,迭代过程如表2-3所示。

表2-3 双变量梯度下降的迭代过程

迭代次数 x y J(x,y)
1 3 1 9.708073
2 2.4 0.909070 6.382415
15 0.105553 0.063481 0.015166
16 0.084442 0.050819 0.009711

迭代16次后,J(x,y)的值为0.009711,满足小于1e-2的条件,停止迭代。

上面的过程如表2-4所示,由于是双变量,所以需要用三维图来解释。请注意看两张图中间那条隐隐的黑色线,表示梯度下降的过程,从红色的高地一直沿着坡度向下走,直到蓝色的洼地。

  1. def show_3d_surface(x, y, z):
  2. fig = plt.figure()
  3. ax = Axes3D(fig)
  4. u = np.linspace(-3, 3, 100)
  5. v = np.linspace(-3, 3, 100)
  6. # 以参数中每个点为中心,生成网格
  7. X, Y = np.meshgrid(u, v)
  8. R = np.zeros((len(u), len(v)))
  9. for i in range(len(u)):
  10. for j in range(len(v)):
  11. R[i, j] = X[i, j] ** 2 + np.sin(Y[i, j]) ** 2
  12. ax.plot_surface(X, Y, R, cmap='rainbow')
  13. plt.plot(x, y, z, c='black')
  14. plt.show()

表2-4 在三维空间内的梯度下降过程

观察角度1 观察角度2
梯度下降 - 图3 梯度下降 - 图4

学习率η的选择

在公式表达时,学习率被表示为\eta。在代码里,我们把学习率定义为learning_rate,或者eta。针对上面的例子,试验不同的学习率对迭代情况的影响,如表2-5所示。

表2-5 不同学习率对迭代情况的影响

学习率 迭代路线图 说明
1.0 梯度下降 - 图5 学习率太大,迭代的情况很糟糕,在一条水平线上跳来跳去,永远也不能下降。
0.8 梯度下降 - 图6 学习率大,会有这种左右跳跃的情况发生,这不利于神经网络的训练。
0.4 梯度下降 - 图7 学习率合适,损失值会从单侧下降,4步以后基本接近了理想值。
0.1 梯度下降 - 图8 学习率较小,损失值会从单侧下降,但下降速度非常慢,10步了还没有到达理想状态。

梯度下降 - 图9

代码位置

原代码位置:ch02, Level3, Level4, Level5

个人代码:

{% tabs %}
{% tab title=”GDSingleVariable” %}

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. def target_function(x):
  4. '''
  5. 目标函数
  6. :param x:
  7. :return:
  8. '''
  9. y = x * x
  10. return y
  11. def derivative_function(x):
  12. '''
  13. 目标函数导数
  14. :param x:
  15. :return:
  16. '''
  17. return 2*x
  18. def draw_function():
  19. x = np.linspace(-1.2, 1.2)
  20. y = target_function(x)
  21. plt.plot(x, y)
  22. def draw_gd(X, Y):
  23. plt.plot(X, Y)
  24. if __name__ == '__main__':
  25. x = 1.2
  26. eta = 0.3
  27. error = 1e-3
  28. X = []
  29. X.append(x)
  30. Y = []
  31. y = target_function(x)
  32. Y.append(y)
  33. while y > error:
  34. x = x - eta * derivative_function(x)
  35. X.append(x)
  36. y = target_function(x)
  37. Y.append(y)
  38. print("x=%f, y=%f" % (x, y))
  39. draw_function()
  40. draw_gd(X,Y)
  41. plt.show()

{% endtab %}

{% tab title=”GDDoubleVariable” %}

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from mpl_toolkits.mplot3d import Axes3D
  4. def target_function(x, y):
  5. '''
  6. 目标函数
  7. :param x:
  8. :param y:
  9. :return:
  10. '''
  11. J = x ** 2 + np.sin(y) ** 2
  12. return J
  13. def derivative_function(theta):
  14. '''
  15. 目标函数的两个偏导数
  16. :param theta:
  17. :return:
  18. '''
  19. x = theta[0]
  20. y = theta[1]
  21. return np.array([2 * x, 2 * np.sin(y) * np.cos(y)])
  22. def show_3d_surface(x, y, z):
  23. fig = plt.figure()
  24. ax = Axes3D(fig)
  25. u = np.linspace(-3, 3, 100)
  26. v = np.linspace(-3, 3, 100)
  27. # 以参数中每个点为中心,生成网格
  28. X, Y = np.meshgrid(u, v)
  29. R = np.zeros((len(u), len(v)))
  30. for i in range(len(u)):
  31. for j in range(len(v)):
  32. R[i, j] = X[i, j] ** 2 + np.sin(Y[i, j]) ** 2
  33. ax.plot_surface(X, Y, R, cmap='rainbow')
  34. plt.plot(x, y, z, c='black')
  35. plt.show()
  36. if __name__ == '__main__':
  37. theta = np.array([3, 1])
  38. eta = 0.1
  39. error = 1e-2
  40. X = []
  41. Y = []
  42. Z = []
  43. for i in range(100):
  44. print(theta)
  45. x = theta[0]
  46. y = theta[1]
  47. z = target_function(x, y)
  48. X.append(x)
  49. Y.append(y)
  50. Z.append(z)
  51. print("%d: x=%f, y=%f, z=%f" % (i, x, y, z))
  52. d_theta = derivative_function(theta)
  53. print(" ", d_theta)
  54. theta = theta - eta * d_theta
  55. if z < error:
  56. break
  57. show_3d_surface(X, Y, Z)

{% endtab %}

{% tab title=”LearningRate” %}

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. def targetFunction(x):
  4. y = (x - 1) ** 2 + 0.1
  5. return y
  6. def derivativeFun(x):
  7. y = 2 * (x - 1)
  8. return y
  9. def create_sample():
  10. x = np.linspace(-1, 3, num=100)
  11. y = targetFunction(x)
  12. return x, y
  13. def draw_base():
  14. x, y = create_sample()
  15. plt.plot(x, y, '.')
  16. plt.show()
  17. return x, y
  18. def gd(eta):
  19. x = -0.8
  20. a = np.zeros((2, 10))
  21. for i in range(10):
  22. a[0, i] = x
  23. a[1, i] = targetFunction(x)
  24. dx = derivativeFun(x)
  25. x = x - eta * dx
  26. plt.plot(a[0, :], a[1, :], 'x')
  27. plt.plot(a[0, :], a[1, :])
  28. plt.title("eta=%f" % eta)
  29. plt.show()
  30. if __name__ == '__main__':
  31. eta = [1.1, 1., 0.8, 0.6, 0.4, 0.2, 0.1]
  32. for e in eta:
  33. X, Y = create_sample()
  34. plt.plot(X, Y, '.')
  35. # plt.show()
  36. gd(e)

{% endtab %}
{% endtabs %}