文章目录
- 原理
- 实践
- 示例函数图像
- 代码实现
- 待了解
原理
理解梯度下降关键在于理解导数表示的含义,具体来说,导数表示函数在某一点的变化趋势,导数值的正负表示函数在该点处的趋势是增加还是减少,导数的大小表示这种趋势变化的速率。
梯度下降,就是根据导数所表示的这种变化趋势不断迭代选取自变量的取值,直到函数值的变化范围很小或几乎不再变化,此时的自变量对应了函数的一个局部最小值点。
为了方便理解入门首先以一个简单的二次函数 f ( x ) = x 2 f(x)=x^2 f(x)=x2举例,函数是一条图像开口向上过原点的抛物线,在原点x=0处取得最小值0,导数 f ′ ( x ) = 2 x f^{\prime}(x)=2x f′(x)=2x。开始可以随机化一个点x=10,该点处导数=20,表示x=10这个点处函数值的趋势是增加的,且这种趋势的增长速率是20,因此需要调小x的值才更可能找到较小的函数值。但是怎么调这就涉及到了一个问题,是固定值调还是动态值来调。理想状态下当然是增长的快就多调点,增长的慢就少调点,这样可以更快找到且更不容易错过函数最小值对应的点。导数本身的值可以满足这种需求,但是直接以导数本身来调也不合适,比如在x=10的点处,按照导数值本身来调的话新的x值=10-2x10=-10,此时x位置跑到对称的另一边了,该点x=-10处导数值为-20,表示函数在这个地方是减少的,需要调大x的值,继续迭代下一个新的x值又回到了x=10处,一直会来回震荡,永远找不到最低点。
上面这个场景的原因就在于每次调试x值的时候步子跨大了,所以直接用导数本身来调这个x的话不合适,因此引入了一个超参数学习率的概念。用预先设定的一个学习率*导数值,这个乘积叫做步长,x每次的迭代就以这个步长为准。
学习率是一个超参数,也就是需要在模型训练之前设定,不依赖训练数据学习的参数。一般设置为0.01或其他值,主要目的就是对导数值进行缩放控制每次更新的步长。
这样在一次次迭代中x逐渐趋近于函数的最小值点,最终当满足一定条件的时候,例如达到一定的迭代次数、函数值变化低于某一阈值、自变量x的变化低于某一阈值…,可以认为近似找到了函数的最小值点。
下面以函数值的变化低于一指定阈值作为终止条件,代码对上面的函数利用梯度下降的思想求解函数最小值点。
def f(x):return x ** 2# 定义 f(x) 的梯度(导数)f'(x) = 2x
def f_prime(x):return 2 * x# 梯度下降算法
def gradient_descent(start_point, learn_rate, threshold):curr_fun_result = f(start_point)while True:step = learn_rate * f_prime(start_point)start_point = start_point - stepprev_fun_result = curr_fun_resultcurr_fun_result = f(start_point)if abs(curr_fun_result - prev_fun_result) <= threshold:return start_point, curr_fun_resultif __name__ == '__main__':# 参数设置start_point = 10.0 # 初始点learn_rate = 0.01 # 学习率threshold = 0.00001 # 函数值变化阈值# 执行梯度下降min_x, min_y = gradient_descent(start_point, learn_rate, threshold)print(f"The minimum value of f(x) is approximately {min_y}, at x = {min_x}")# The minimum value of f(x) is approximately 0.00024248699031186326, at x = 0.015571993780883142
约在x=0.015处取得最小值0.0002。
实践
实际业务生产中损失函数往往是关于多个变量的函数关系,因此需要将上面导数的概念延伸到偏导数,偏导数只是将空间图形某一点位置的变化分别分解到不同的度量坐标轴上。所有分解出来的向量依次排列就叫做梯度向量,合起来就叫做梯度,梯度同样指向立体空间中某一点函数值变化最快的方向。
示例函数图像
以函数 f ( x , y ) = x 2 + y 2 + 1 f(x,y)=x^2+y^2+1 f(x,y)=x2+y2+1来理解多变量函数利用梯度下降求解函数最小值的过程。明显最小值的点为(0, 0),最小值1。先直观感受一下函数的图像:
import numpy as np
import matplotlib.pyplot as plt# 创建 x 和 y 的网格
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
x, y = np.meshgrid(x, y)# 计算 z = x^2 + y^2 + 1
z = x ** 2 + y ** 2 + 1# 创建一个新的图形
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')# 绘制曲面
ax.plot_surface(x, y, z, cmap='viridis')# 设置图形标签
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title('Surface plot of f(x, y) = x^2 + y^2 + 1')# 显示图形
plt.show()
代码实现
# 计算函数 f(x) = x^2 + y^2 + 1 的函数值
def f(point):x, y = pointreturn x ** 2 + y ** 2 + 1# 计算函数 f(x) = x^2 + y^2 + 1 对x和y的偏导数
def f_prime(point):x, y = pointreturn 2 * x, 2 * y# 梯度下降算法
def gradient_descent(start_point, learn_rate, threshold):curr_fun_result = f(start_point)while True:x, y = start_pointfx_prime, fy_prime = f_prime(start_point)start_point = (x - learn_rate * fx_prime, y - learn_rate * fy_prime)prev_fun_result = curr_fun_resultcurr_fun_result = f(start_point)if abs(curr_fun_result - prev_fun_result) <= threshold:return start_point, curr_fun_resultif __name__ == '__main__':# 参数设置start_point = (10, 10) # 初始点learn_rate = 0.01 # 学习率threshold = 0.00001 # 函数值变化阈值# 执行梯度下降min_point, min_value = gradient_descent(start_point, learn_rate, threshold)print(f"The minimum value of f(x, y) is approximately {min_value}, at point = {min_point}")# The minimum value of f(x, y) is approximately 1.0002343457942755, at point = (0.010824643048977862, 0.010824643048977862)
看到一些资料说终止条件还可以设置为梯度的范数小于某个阈值时,查了一下L2范数,也叫欧几里得范数,计算方式就是求一个向量所有分向量的平方和。https://worktile.com/kb/p/63296
v = ( v 1 , v 2 , . . . , v n ) v=(v1,v2,...,v_n) v=(v1,v2,...,vn)
∥ v ∥ 2 = v 1 2 + v 2 2 + . . . + v n 2 \|v\|_2=v_1^2+v_2^2+...+v_n^2 ∥v∥2=v12+v22+...+vn2
其实也就是各个偏导数的平方和,L2范数越小,也就代表了各个偏导数越小,合起来的梯度向量也就越小。
关于更多变量的函数利用梯度下降求解时,思路都是一样的,只是增多了偏导数的数量。
待了解
初步了解梯度下降的实现原理之后,可以看到对于这种函数图像表现为“U型”平滑塌陷的,梯度下降可以逐渐逼近全局最小值,但对于类似于下面这种凹凸不平的函数,可能只会逼近其中的一个局部最小值点。
import numpy as np
import matplotlib.pyplot as plt# 定义函数
def f(x, y):return np.sin(x) * np.cos(y) + np.sin(2 * x) * np.cos(2 * y)# 创建x, y值范围
x = np.linspace(-3, 3, 400)
y = np.linspace(-3, 3, 400)
x, y = np.meshgrid(x, y)
z = f(x, y)# 绘制图像
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(x, y, z, cmap='viridis')# 设置标题和标签
ax.set_title('3D Function with Multiple Local Minima')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')plt.show()
可以通过随机选择多个初始点求得多个局部最小值,然后取最优。或者其他方式,待后续深入了解。