文章目录
一、梯度下降基本原理与学习率
1. 数据背景与最小二乘法求解
2. 一维梯度下降基本流程
2.1 参数移动第一步
2.2 梯度下降的多轮迭代
3. 梯度下降算法特性与学习率
二、梯度下降一般建模流程与多维梯度下降
1. 多维梯度下降与损失函数可视化
2. 梯度下降计算流程
3. 二维梯度下降过程的可视化展示
3.1 等高线图
3.2 损失函数取值变化图
4. 线性回归的梯度下降求解过程
三、梯度下降算法评价
1. 梯度下降算法优势
2. 梯度下降算法局限
2.1 局部最小值点(local minimun)陷阱
2.2 鞍点(saddle point)陷阱
3. 梯度下降算法本质与改善方法
在上文当中,我们已经成功的构建了逻辑回归的损失函数,但由于逻辑回归模型本身的特殊性,我们在构造损失函数时无法采用类似 SSE 的计算思路(此时损失函数不是凸函数),因此最终的损失函数也无法用最小二乘法进行直接求解。
当然,这其实也不仅仅是逻辑回归模型的问题,对于大多数机器学习模型来说,损失函数都无法直接利用最小二乘法进行求解。此时,就需要我们掌握一些可以针对更为一般函数形式进行最小值求解的优化方法。而在机器学习领域,最为基础、同时也最为通用的方法就是梯度下降算法。
# 科学计算模块 import numpy as np import pandas as pd # 绘图模块 import matplotlib as mpl import matplotlib.pyplot as plt # 自定义模块 from ML_basic_function import *
一、梯度下降基本原理与学习率
1. 数据背景与最小二乘法求解
- 梯度下降算法本身是属于数学理论推导相对复杂,但实际应用过程并不难理解的一种方法。
- 因此,我们将先从一个简单的例子入手,通过和最小二乘法计算过程对比,来查看梯度下降的一般过程,然后我们再进行更加严谨的理论推导。
- 例如,有简单数据集表示如下:
x | y |
1 | 2 |
2 | 4 |
3 | 6 |
x = np.linspace(-1, 5, 40) SSELoss = 14 * np.power(x - 2, 2) plt.plot(x, SSELoss)
2. 一维梯度下降基本流程
当然,围绕上述形式非常简单的损失函数,我们也可以尝试使用梯度下降算法进行求解。
首先梯度下降算法的目标仍然是求最小值,但和最小二乘法这种一步到位、通过解方程组直接求得最小值的方式不同,梯度下降是通过一种迭代求解的方式来进行最小值的求解。
其整体求解过程可以粗略描述为,先随机选取一组参数初始值,然后沿着某个方向,一步一步移动到最小值点。例如:
show_trace(gd(lr = 0.01)) plt.plot(2, 0, 'ro')
- 该图像的绘制函数会在稍后的内容中进行讲解。
2.1 参数移动第一步
在确定了梯度之后,接下来参数的移动方向也随之确定,在梯度下降算法中,参数的移动方向是梯度的负方向。
注意,这里有两点需要注意,其一是梯度的负方向,上述计算结果中,224 既是梯度的取值,同时也代表着梯度的方向——正方向。而此时梯度的负方向则是取值减少的方向,和损失函数图像观测结果相同。其二是参数的移动是朝着梯度的负方向移动,也就是朝着数值减少的方向移动,而不是直接在原参数 10 的基础上减去 224,这很明显是不合适的。
对于只包含一个变量的损失函数来说,梯度也就是函数某点的导数。
(3) 确定移动步长并进行移动
在确定移动的方向之后,接下来需要进一步确定移动距离
2.2 梯度下降的多轮迭代
- 迭代其实是一个数学意义上的概念,一般指以下计算流程:某次运算时初始条件是上一次运算的结果,而当前运算的结果则是下一次计算的初始条件。
- 通过带入梯度值进行多轮迭代,最终使得损失函数的取值逐渐下降的算法,就被称为梯度下降。
3. 梯度下降算法特性与学习率
首先,我们简单验证,经过上述梯度下降的一系列计算,最终能否有效降低损失函数的计算结果,即使得 w ww 最终朝向最优值 2 靠拢。我们可以通过定义一个函数来帮助我们进行梯度下降迭代计算
def gd(lr = 0.02, itera_times = 20, w = 10): """ 梯度下降计算函数 :param lr: 学习率 :param itera_times:迭代次数 :param w:参数初始取值 :return results:每一轮迭代的参数计算结果列表 """ results = [w] for i in range(itera_times): w -= lr * 28 * (w - 2) # 梯度计算公式 results.append(w) return results
- 当我们在以 0.02 作为学习率,初始值为 10,迭代 20 轮时参数每一轮迭代后参数的计算结果如下:
res = gd() res #[10, # 5.52, # 3.5488, # 2.681472, # 2.29984768, # 2.1319329792, # 2.058050510848, # 2.02554222477312, # 2.0112385789001728, # 2.004944974716076, # 2.0021757888750735, # 2.0009573471050324, # 2.000421232726214, # 2.000185342399534, # 2.000081550655795, # 2.00003588228855, # 2.000015788206962, # 2.0000069468110633, # 2.000003056596868, # 2.000001344902622, # 2.000000591757154]
- 我们发现,参数从 10 开始朝向 2 移动,并且迭代 20 轮时已经非常接近全域最优解 2,此处也验证梯度下降确实能够帮助我们找到最优解。
- 当然,我们也可以将上述参数移动过程和损失函数的函数图像绘制到一张画布上,具体功能可以通过如下函数实现:
def show_trace(res): """ 梯度下降轨迹绘制函数 """ f_line = np.arange(-6, 10, 0.1) plt.plot(f_line, [14 * np.power(x-2, 2) for x in f_line]) plt.plot(res, [14 * np.power(x-2, 2) for x in res], '-o') plt.xlabel('x') plt.ylabel('Loss(x)')
show_trace(res)
接下来,我们在此基础之上,详细讨论梯度下降算法的基本特性与超参数取值一般规范。
从图像上,我们发现,梯度下降的计算过程中,刚开始 w ww 变化非常快,而随着逐渐接近最小值点 w ww 的变化幅度逐渐减少,也就是说,梯度下降其实并不是一个等步长的移动过程。
这也就等价说明,如果某点的梯度值相对较大,那么该点应该距离全域最小值较远。而梯度下降过程中实际也能够做到在远点迭代移动距离较大、近点迭代移动距离较小。当然,在学习率对移动距离进行修正时,学习率不宜设置过小,也不宜设置过大,例如当学习率设置过小时:
plt.subplot(121) plt.title('lr=0.001') show_trace(gd(lr=0.001)) plt.subplot(122) plt.title('lr=0.01') show_trace(gd(lr=0.01))
- 当学习率取值为 0.001 时,迭代 20 轮仍然距离最小值点差距较远,此时我们考虑进一步增加迭代次数:
plt.subplot(121)plt.subplot(121) plt.title('itera_times=20') show_trace(gd(itera_times=20, lr=0.001)) plt.subplot(122) plt.title('itera_times=40') show_trace(gd(itera_times=40, lr=0.001))
plt.subplot(121) plt.title('itera_times=60') show_trace(gd(itera_times=60, lr=0.001)) plt.subplot(122) plt.title('itera_times=80') show_trace(gd(itera_times=80, lr=0.001))
此处学习率为 0.001 取值的情况下,迭代次数增加了三倍但仍然没有收敛到 2 附近,需要迭代 80 次左右才能收敛至较好效果。而过多次的迭代会增加额外计算量,也就是说如果学习率取值较小,则会耗费更多的计算量。
接下来我们再来看当学习率取值过大时可能出现的状况。当我们将学习率设置为 0.05 时,迭代过程如下所示:
plt.subplot(121) plt.title('lr=0.01') show_trace(gd(lr=0.01)) plt.subplot(122) plt.title('lr=0.05') show_trace(gd(lr=0.05))
- 我们发现当学习率较大时,收敛过程并不平稳,当然小幅度不平稳并不影响最终收敛结果,但学习率继续增加,例如我们将学习率增加至 0.08 时,参数整体迭代过程不仅不会收敛,而且还会逐渐发散:
show_trace(gd(lr=0.08))
- 当学习率为 0.08 时,所得每一次迭代的具体数值如下。
gd(lr=0.08) #[10, # -7.920000000000002, # 14.300800000000006, # -13.25299200000001, # 20.913710080000012, # -21.45300049920002, # 31.081720619008028, # -34.06133356756996, # 46.71605362378676, # -53.447906493495594, # 70.75540405193455, # -83.25670102439885, # 107.71830927025458, # -129.0907034951157, # 164.55247233394346, # -199.56506569408992, # 251.94068146067156, # -307.92644501123283, # 386.30879181392874, # -474.5429018492717, # 592.9131982930971]
- 由此我们不难发现学习率取值是影响结果是否收敛、以及能否在有限次迭代次数中高效收敛的关键参数。
- 学习率过大会导致结果发散,而学习率过小则又会导致模型无法收敛至最优结果。
二、梯度下降一般建模流程与多维梯度下降
1. 多维梯度下降与损失函数可视化
一种更为一般的情况是,一个模型中包含多个参数,而损失函数就是这多个参数共同构成的函数。
例如在 Lesson 1 中我们曾利用带截距项的简单线性回归 y = w x + b y=wx+by=wx+b 对下述数据进行拟合,并得出带有两个参数的损失函数:
- 而此时损失函数图像就是包含两个变量的三维图像
from mpl_toolkits.mplot3d import Axes3D x = np.arange(-1,3,0.05) y = np.arange(-1,3,0.05) w, b = np.meshgrid(x, y) SSE = (2 - w - b) ** 2 + (4 - 3 * w - b) ** 2 ax = plt.axes(projection='3d') ax.plot_surface(w, b, SSE, cmap='rainbow') ax.contour(w, b, SSE, zdir='z', offset=0, cmap="rainbow") #生成z方向投影,投到x-y平面 plt.xlabel('w') plt.ylabel('b') plt.show()
并且,我们通过最小二乘法能够找出全域最小值点为 (1,1),也就是当 w = 1 , b = 1 时,能够让损失函数取值最小。当然,围绕该问题,我们也可以采用梯度下降进行最优解求解。
2. 梯度下降计算流程
此处给出更加严谨的梯度下降实现步骤:
(1) 确定数据及模型
首先还是要明确数据和模型,然后才能进一步确定参数、梯度计算公式等变量。
此处我们以 y = w x + b y=wx+by=wx+b 简单线性回归对上述简单数据集进行拟合,参数向量包含两个分量。
(2) 设置初始参数值
然后我们需要选取一组参数值,一种更加通用的方法,是给初始参数赋予一组随机数作为初始取值。
由于参数包含两个分量,因此参数向量的初始值可以由如下过程确定:
np.random.seed(24) w = np.random.randn(2, 1) # 参数都默认为列向量 w #array([[ 1.32921217], # [-0.77003345]])
- (3) 根据损失函数求出梯度表达式
- 只有确定了梯度表达式,才能在后续每一轮迭代过程中快速计算梯度。
- 线性回归的损失函数是围绕 SSE 及其衍生的指标所构建的损失函数,此处以 MSE 作为损失函数的构建指标,此处可借助已经定义好的 SSELoss 函数执行计算。
- 其中 m 为训练数据总数。
def MSELoss(X, w, y): """ MSE指标计算函数 """ SSE = SSELoss(X, w, y) MSE = SSE / X.shape[0] return MSE
- 此时数据为:
features = np.array([1, 3]).reshape(-1, 1) features = np.concatenate((features, np.ones_like(features)), axis=1) features #array([[1, 1], # [3, 1]]) labels = np.array([2, 4]).reshape(-1, 1) labels #array([[2], # [4]]) # 计算w取值时SSE SSELoss(features, w, labels) #array([[2.68811092]]) # 计算w取值时MSE MSELoss(features, w, labels) #array([[1.34405546]])
而对于线性回归损失函数梯度表达式,则可根据 Lesson 2 中线性回归损失函数梯度求导结果得出:
def lr_gd(X, w, y): """ 线性回归梯度计算公式 """ m = X.shape[0] grad = 2 * X.T.dot((X.dot(w) - y)) / m return grad # 计算w取值时梯度 lr_gd(features, w, labels) #array([[-3.78801208], # [-2.22321821]])
该步骤其实也是在手动实现梯度下降计算过程中至关重要的一步,正如此前所介绍的,只有将方程组转化为矩阵形式,才能更好的利用程序进行计算。
但值得注意的是,并不是所有的损失函数的梯度计算都能简单表示成矩阵运算形式。
(4) 执行梯度下降迭代
在给定梯度计算公式和参数初始值的情况下,我们就能够开始进行梯度下降迭代计算了。在梯度下降过程中,参数更新参照如下公式:
wn=w(n−1)−lr⋅∇wf(w(n−1))
- 则可定义相应函数:
def w_cal(X, w, y, gd_cal, lr = 0.02, itera_times = 20): """ 梯度下降中参数更新函数 :param X: 训练数据特征 :param w: 初始参数取值 :param y: 训练数据标签 :param gd_cal:梯度计算公式 :param lr: 学习率 :param itera_times: 迭代次数 :return w:最终参数计算结果 """ for i in range(itera_times): w -= lr * gd_cal(X, w, y) return w
- 接下来执行梯度下降计算,在学习率为 0.1 的情况下迭代 100 轮:
np.random.seed(24) w = np.random.randn(2, 1) # 参数都默认为列向量 w #array([[ 1.32921217], # [-0.77003345]]) w = w_cal(features, w, labels, gd_cal = lr_gd, lr = 0.1, itera_times = 100) w #array([[1.02052278], # [0.95045363]]) # 计算w取值时SSE SSELoss(features, w, labels) #array([[0.0009869]]) # 计算w取值时MSE MSELoss(features, w, labels) #array([[0.00049345]]) # 计算w取值时梯度 lr_gd(features, w, labels) #array([[ 0.0070423 ], # [-0.01700163]])
- 我们发现,最终参数结果逼近 (1,1) 点,也就是说整体计算过程能够在迭代多轮之后逼近全域最小值点。
- 据此也验证了梯度下降本身的有效性。当然,此处是以二维梯度下降为例进行的展示,更高维度的梯度下降过程也是类似,并且可以借助 NumPy 当中数组的优秀性质来完成相关梯度的计算。
3. 二维梯度下降过程的可视化展示
3.1 等高线图
- 为了方便后续其他梯度下降算法的性质展示,我们通过绘制损失函数的等高线图来观察二维梯度下降过程中参数点移动的过程。首先在上述迭代 100 次的过程中,我们可以记录每一次迭代的计算结果:
def w_cal_rec(X, w, y, gd_cal, lr = 0.02, itera_times = 20): w_res = [np.copy(w)] for i in range(itera_times): w -= lr * gd_cal(X, w, y) w_res.append(np.copy(w)) return w, w_res np.random.seed(24) w = np.random.randn(2, 1) w, w_res = w_cal_rec(features, w, labels, gd_cal = lr_gd, lr = 0.1, itera_times = 100) w_res
- 迭代计算后的结果如下。
# 所有点的横坐标 np.array(w_res)[:, 0]
- 参数移动轨迹图
- 据此我们可以将所有点在迭代过程的移动轨迹绘制在图片上:
plt.plot(np.array(w_res)[:, 0], np.array(w_res)[:, 1], '-o', color='#ff7f0e') • 1
我们发现,参数在参数平面上的移动轨迹其实并不是一条直线。当然,我们也可以通过等高线图来观察参数点逼近 (1,1) 点的移动情况。
# 网格点坐标 x1, x2 = np.meshgrid(np.arange(1, 2, 0.001), np.arange(-1, 1, 0.001))
- 其中 np.meshgrid 生成两个序列,第一个序列根据输入的第一个参数按照列排列,第二个序列根据输入的第二个参数按行排列。该函数输出结果主要用于带网格的图像绘制情况。
np.meshgrid(np.arange(3), np.arange(1, 5)) #[array([[0, 1, 2], # [0, 1, 2], # [0, 1, 2], # [0, 1, 2]]), # array([[1, 1, 1], # [2, 2, 2], # [3, 3, 3], # [4, 4, 4]])] # 绘制等高线图 plt.contour(x1, x2, (2-x1-x2)**2+(4-3*x1-x2)**2) # 绘制参数点移动轨迹图 plt.plot(np.array(w_res)[:, 0], np.array(w_res)[:, 1], '-o', color='#ff7f0e')
3.2 损失函数取值变化图
- 从一个更加严谨的角度,我们可以绘制其损失函数变化曲线:
loss_value = np.array([MSELoss(features, np.array(w), labels) for w in w_res]).flatten() loss_value #array([1.34405546e+00, 5.18623852e-01, 4.63471078e-01, 4.31655465e-01, # 4.02524386e-01, 3.75373031e-01, 3.50053485e-01, 3.26441796e-01, # 3.04422755e-01, 2.83888935e-01, 2.64740155e-01, 2.46882992e-01, # 2.30230325e-01, 2.14700907e-01, 2.00218974e-01, 1.86713872e-01, # 1.74119712e-01, 1.62375048e-01, 1.51422581e-01, 1.41208877e-01, # 1.31684104e-01, 1.22801792e-01, 1.14518608e-01, 1.06794137e-01, # 9.95906956e-02, 9.28731379e-02, 8.66086905e-02, 8.07667906e-02, # 7.53189365e-02, 7.02385492e-02, 6.55008424e-02, 6.10827019e-02, # 5.69625722e-02, 5.31203522e-02, 4.95372963e-02, 4.61959234e-02, # 4.30799317e-02, 4.01741188e-02, 3.74643078e-02, 3.49372780e-02, # 3.25807006e-02, 3.03830783e-02, 2.83336892e-02, 2.64225348e-02, # 2.46402910e-02, 2.29782625e-02, 2.14283405e-02, 1.99829634e-02, # 1.86350793e-02, 1.73781123e-02, 1.62059298e-02, 1.51128129e-02, # 1.40934286e-02, 1.31428034e-02, 1.22562995e-02, 1.14295918e-02, # 1.06586468e-02, 9.93970341e-03, 9.26925391e-03, 8.64402735e-03, # 8.06097336e-03, 7.51724732e-03, 7.01019651e-03, 6.53734712e-03, # 6.09639220e-03, 5.68518043e-03, 5.30170557e-03, 4.94409673e-03, # 4.61060920e-03, 4.29961595e-03, 4.00959972e-03, 3.73914556e-03, # 3.48693399e-03, 3.25173450e-03, 3.03239961e-03, 2.82785922e-03, # 2.63711543e-03, 2.45923761e-03, 2.29335795e-03, 2.13866715e-03, # 1.99441050e-03, 1.85988420e-03, 1.73443193e-03, 1.61744161e-03, # 1.50834249e-03, 1.40660229e-03, 1.31172463e-03, 1.22324662e-03, # 1.14073661e-03, 1.06379203e-03, 9.92037492e-04, 9.25122916e-04, # 8.62721840e-04, 8.04529820e-04, 7.50262948e-04, 6.99656466e-04, # 6.52463476e-04, 6.08453732e-04, 5.67412517e-04, 5.29139601e-04, # 4.93448257e-04]) plt.plot(np.arange(101), loss_value)
- 能够发现,在梯度下降过程中损失值是严格递减的。当然我们也可以将上述过程分装为一个函数:
def loss_vis(X, w_res, y, loss_func): loss_value = np.array([loss_func(X, np.array(w), y) for w in w_res]).flatten() plt.plot(np.arange(len(loss_value)), loss_value) # 验证函数是否可执行 loss_vis(features, w_res, labels, MSELoss)