机器学习(六):梯度下降

本文深入探讨了逻辑回归中的梯度下降方法,包括批量梯度下降、随机梯度下降和小批量梯度下降。文章指出,梯度下降用于寻找损失函数最小值,但在非凸函数中可能陷入局部最小值。通过对比最小二乘法和梯度下降,解释了为什么逻辑回归通常不使用最小二乘法。文章还通过实例展示了梯度下降的计算过程,强调了学习率选择的重要性,并讨论了随机性如何帮助避免局部最优解。最后,介绍了小批量梯度下降作为平衡稳定性和效率的优化策略。
摘要由CSDN通过智能技术生成

全文共30000余字,预计阅读时间约1.5~3小时 | 满满干货,建议收藏!

在这里插入图片描述

一、前言

在之前的文章中,我们深入探讨了逻辑回归的理论基础和计算过程,包括广义线性模型、对数几率、Sigmoid函数等基础知识,以及逻辑回归中的极大似然函数和交叉熵损失函数的应用和理解。

在逻辑回归的学习过程中,理解这些理论基础和概念是十分重要的。 然而,理解了逻辑回归模型的基本形式和损失函数后,我们还需要解决一个问题,那就是如何找到最优的参数,使得损失函数最小。这就引出了我们这篇文章要讨论的主题——梯度下降方法。

梯度下降是一种常用的优化算法,广泛应用于机器学习和深度学习中的参数优化。在逻辑回归中,我们通常使用梯度下降方法来优化模型的参数,从而得到最小的损失函数值。 在本文中,我们将详细介绍梯度下降的原理,包括梯度的理解、学习率的选择、批量梯度下降、随机梯度下降、小批量梯度下降等不同形式,以及梯度下降的收敛性问题和优化方法。希望通过本文,大家能够对梯度下降有更深入的理解,更好地应用在逻辑回归和其他机器学习算法中。

二、为什么逻辑回归的交叉熵损失函数不能使用最小二乘法求解?

理论上,我们可以使用任何损失函数来训练模型,然而,在实践中,我们选择损失函数时会考虑一些因素,比如我们希望模型具有的性质和我们试图优化的目标是一致的。最小二乘法在很多场合下都是一个有效的参数估计方法,尤其是当我们的目标是预测一个连续的输出,如在线性回归中。但在逻辑回归这样的分类问题中,直接使用最小二乘法可能会带来一些问题。我们需要先了解一下什么是凸函数。

2.1 什么是凸函数?

对于一个函数 f f f,如果在其定义域中的所有 x x x y y y以及所有满足 0 ≤ t ≤ 1 0 \leq t \leq 1 0t1的实数 t t t,都满足以下不等式:

f ( t x + ( 1 − t ) y ) ≤ t f ( x ) + ( 1 − t ) f ( y ) (1) f(tx + (1 - t)y) \leq tf(x) + (1 - t)f(y) \tag{1} f(tx+(1t)y)tf(x)+(1t)f(y)(1)

那么,我们就称这个函数为凸函数。这意味着在函数的图像上,任意两点之间的连线(线段)上的任意点都在函数图像之上,或者等于函数图像。这是凸函数的一个核心特性,使得我们在优化问题中能找到全局最优解。

我们使用matplotlib和numpy库生成凸函数和非凸函数的图像。下面是一个简单的例子,你可以在你的Python环境下运行这段代码:

import matplotlib.pyplot as plt
import numpy as np

# 定义一个凸函数:f(x) = x^2
def f1(x):
    return x ** 2

# 定义一个非凸函数:f(x) = x^3
def f2(x):
    return x ** 3

x = np.linspace(-2, 2, 400)
y1 = f1(x)
y2 = f2(x)

# 选择两个点并计算连线
x1, x2 = -1.5, 1.5
y1_1, y2_1 = f1(x1), f1(x2)
y1_2, y2_2 = f2(x1), f2(x2)

# 计算连线上的点
line_x = np.linspace(x1, x2, 400)
line_y1 = ((y2_1 - y1_1) / (x2 - x1)) * (line_x - x1) + y1_1
line_y2 = ((y2_2 - y1_2) / (x2 - x1)) * (line_x - x1) + y1_2

# 绘制函数和连线
plt.figure(figsize=(12, 6))

plt.subplot(1, 2, 1)
plt.plot(x, y1, label='Convex function: f(x) = x^2')
plt.plot(line_x, line_y1, label='Line between two points')
plt.scatter([x1, x2], [y1_1, y2_1], color='red')  # 绘制选中的两个点
plt.legend()
plt.title('Convex function')

plt.subplot(1, 2, 2)
plt.plot(x, y2, label='Non-convex function: f(x) = x^3')
plt.plot(line_x, line_y2, label='Line between two points')
plt.scatter([x1, x2], [y1_2, y2_2], color='red')  # 绘制选中的两个点
plt.legend()
plt.title('Non-convex function')

plt.show()

image-20230613105609266

在这个示例中,我们选择了 f ( x ) = x 2 f(x) = x^2 f(x)=x2函数上的两个点,并绘制了这两点之间的连线。可以看到,这条连线完全在函数的图像之上,满足凸函数的定义。这就是凸函数的一个关键特性,它在许多优化问题,比如梯度下降法中能保证找到全局最优解。

同时,我们选择了 f ( x ) = x 3 f(x) = x^3 f(x)=x3函数上的两个点,并绘制了这两点之间的连线。你可以观察到,这条连线的部分区域在函数的图像之下,这就不满足凸函数的定义。这种情况下,优化问题可能存在多个局部最优解,使得梯度下降法可能无法找到全局最优解。

2.2 最小二乘法和逻辑回归

最小二乘法是一种优化策略,它常常和平方和误差(SSE)损失函数一起使用。在回归问题中,我们的目标通常是最小化预测和实际目标值之间的平方误差,这就是所说的平方和误差(SSE)损失函数。

但是在逻辑回归中,我们通常不会使用平方误差损失函数(Sum of Squared Errors, SSE)。原因是,逻辑回归的输出是一个概率(通过sigmoid函数得到),sigmoid函数将线性函数的输出映射到0和1之间。这个过程是非线性的。它和线性回归的实数输出有本质的区别。如果我们仍然使用平方误差损失函数,那么损失函数就会是预测概率和实际标签(0或1)的差的平方,这个函数在逻辑回归的上下文中不是凸的,可能会有多个局部最优解。所以可能存在的问题如下:

  1. 非凸性:当我们用平方误差损失函数时,逻辑回归的损失函数将变成非凸的。这意味着优化过程可能会陷入局部最优,而不是全局最优。
  2. 敏感性:在分类问题中,平方误差损失函数对于异常值和边界值非常敏感。例如,如果我们的模型预测一个实际为1的样本的输出为0,平方误差损失函数会将这个错误放大,这可能会导致模型过拟合。
  3. 概率解释:逻辑回归的输出是一个概率,这就要求我们的损失函数能够对概率进行良好的度量。交叉熵损失函数在这方面做得很好,因为它直接衡量了模型输出的概率分布与真实标签的概率分布之间的差异。

综上,逻辑回归通常使用交叉熵作为损失函数。它对应的是两个概率分布(实际的标签分布和预测的概率分布)之间的差异。更重要的是,当使用交叉熵作为损失函数时,逻辑回归的优化问题是凸的,这保证了我们可以找到全局最优解。因此,当我们说"逻辑回归的交叉熵损失不能用最小二乘法求解"时,我们实际上是指,我们不能用SSE(和最小二乘法优化策略紧密相连的损失函数)替代交叉熵损失函数。

三、梯度下降

还是我们一贯的风格,先不去管什么是梯度下降,我们先通过数据来模拟线性回归参数估计的过程,采用最小二乘法(Least Squares Method,LSM)和平方和误差(Sum of Squared Error,SSE)找出最优参数,然后再引出梯度下降的概念,直观的感受两种优化方法的异同。

3.1 最小二乘法求解线性回归参数过程

我们构造如下数据集:

xy
12
24
36
3.1.1. 定义模型和损失函数

我们首先定义线性模型和损失函数。在这个例子中,模型是 y = w x + b y = wx + b y=wx+b,其中 w w w 是斜率, b b b 是截距。我损失函数就使用残差平方和(SSE),即 ∑ i = 1 n ( y i − ( w x i + b ) ) 2 \sum_{i=1}^{n}(y_i - (wx_i + b))^2 i=1n(yi(wxi+b))2 ,其中 i i i 是数据点的索引。

3.1.2. 将损失函数写成参数的函数

为了在给定的数据上找到最优的 w w w b b b,我们需要将损失函数写成 w w w b b b 的函数。对于上述的数据,这个函数是:

S S E L o s s ( w , b ) = ( 2 − ( w ⋅ 1 + b ) ) 2 + ( 4 − ( w ⋅ 2 + b ) ) 2 + ( 6 − ( w ⋅ 3 + b ) ) 2 (2) SSELoss(w, b) = (2 - (w \cdot 1 + b))^2 + (4 - (w \cdot 2 + b))^2 + (6 - (w \cdot 3 + b))^2 \tag{2} SSELoss(w,b)=(2(w1+b))2+(4(w2+b))2+(6(w3+b))2(2)

3.1.3 求解最优参数

我们的目标是找到 w w w b b b 的值,使得 SSE 最小。为了实现这个目标,我们需要对 w w w b b b 分别求偏导,然后令偏导数等于零,解出 w w w b b b 的值。

w w w 求偏导,我们得到:

∂ S S E ∂ w = − 2 ⋅ 1 ⋅ ( 2 − ( w ⋅ 1 + b ) ) − 2 ⋅ 2 ⋅ ( 4 − ( w ⋅ 2 + b ) ) − 2 ⋅ 3 ⋅ ( 6 − ( w ⋅ 3 + b ) ) (3) \frac{\partial SSE}{\partial w} = -2 \cdot 1 \cdot (2 - (w \cdot 1 + b)) - 2 \cdot 2 \cdot (4 - (w \cdot 2 + b)) - 2 \cdot 3 \cdot (6 - (w \cdot 3 + b)) \tag{3} wSSE=21(2(w1+b))22(4(w2+b))23(6(w3+b))(3)

b b b 求偏导,我们得到:

∂ S S E ∂ b = − 2 ⋅ ( 2 − ( w ⋅ 1 + b ) ) − 2 ⋅ ( 4 − ( w ⋅ 2 + b ) ) − 2 ⋅ ( 6 − ( w ⋅ 3 + b ) ) (4) \frac{\partial SSE}{\partial b} = -2 \cdot (2 - (w \cdot 1 + b)) - 2 \cdot (4 - (w \cdot 2 + b)) - 2 \cdot (6 - (w \cdot 3 + b)) \tag{4} bSSE=2(2(w1+b))2(4(w2+b))2(6(w3+b))(4)

然后我们将这两个偏导数分别设为0,解出 w w w b b b 的值。这样我们就找到了最小化SSE的 w w w b b b,也就是最优的模型参数。

这就是在给定数据上使用最小二乘法求解线性回归模型参数的全过程。

上述完成了数学计算过程,接下来我们借助Python来复现上面的过程,计算出 w w w b b b的值

import numpy as np

# 输入数据
X = np.array([1, 2, 3])
Y = np.array([2, 4, 6])

# 添加一列全为1的向量,用于计算截距项
X = np.vstack([X, np.ones(len(X))]).T

# 计算参数w和b(使用最小二乘法)
w, b = np.linalg.lstsq(X, Y, rcond=None)[0]

print("w: ", w)
print("b: ", b)

结果的输出如下:

w:  2.000000000000001
b:  -1.0681905036999146e-15

可以看到,求解出来的最优参数w是2,截距项b是一个无限接近于0的值。我们绘制图像看一下:

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

# 定义数据
x = np.array([1, 2, 3])
y = np.array([2, 4, 6])

# 计算最优参数
X = np.vstack([x, np.ones(len(x))]).T
w_opt, b_opt = np.linalg.lstsq(X, y, rcond=None)[0]

# 定义损失函数
def SSE(w, b):
    return np.sum((y - (w*x + b))**2)

# 创建w, b的值的网格
w_values = np.linspace(w_opt - 1, w_opt + 1, 100)
b_values = np.linspace(b_opt - 1, b_opt + 1, 100)
w_grid, b_grid = np.meshgrid(w_values, b_values)

# 计算损失函数的值
SSE_values = np.array([SSE(w, b) for w, b in zip(np.ravel(w_grid), np.ravel(b_grid))])
SSE_grid = SSE_values.reshape(w_grid.shape)

# 创建w的值的序列
w_values = np.linspace(w_opt - 1, w_opt + 1, 100)

# 计算损失函数的值
SSE_values = np.array([SSE(w, b_opt) for w in w_values])

# 绘制二维图
plt.figure()
plt.plot(w_values, SSE_values, label='SSE-w curve')
plt.scatter(w_opt, SSE(w_opt, b_opt), color='red')  # 标注最优的点
plt.xlabel('w')
plt.ylabel('SSE')
plt.legend()

# 创建3D图
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(w_grid, b_grid, SSE_grid, cmap='YlGnBu')

# 标注最优的点
ax.scatter(w_opt, b_opt, SSE(w_opt, b_opt), color='red', s=100)  
ax.set_xlabel('w')
ax.set_ylabel('b')
ax.set_zlabel('SSE')

plt.show()

这段代码主要是通过实现最小二乘法,在给定的数据集上寻找线性回归模型的最优参数。首先,我们定义了数据点,并利用numpy的最小二乘法函数来计算出最优参数。然后,我们定义了平方和误差损失函数,并在参数的值域上创建了一个网格来计算每个参数组合的损失函数值。最后,我们使用matplotlib生成了3D图和2D图,形象地展示了损失函数随参数变化的趋势,并在图上标注了最小二乘法求解出的最优参数,验证了其为损失函数的全局最小值。这个过程既展示了最小二乘法的实现过程,也直观地展示了损失函数的凸性以及最优解的求解。

91

构造的损失函数是一个平方和误差(SSE),它是关于参数 w w w b b b 的二次函数,形成一个凸函数的形状。凸函数有一个很好的性质,那就是它在整个定义域上只有一个最小值,即全局最小值。在3D图像中,可以看到损失函数形成了一个“碗”形状,这是凸函数的典型特征。"碗"的最低点,就代表了损失函数的最小值,即全局最优解。

最小二乘法的目的就是要找到这个“碗”的最低点,即损失函数的最小值。在我们的图像中,这个最低点就被标记为红色。这个红色的点,就是最小二乘法求解出来的 w w w b b b 的值。从图像中可以直观地看出,这个红色的点确实处在“碗”的最底部,也就验证了最小二乘法求解出的结果是全局最优解。

总的来说,这两个图像清晰地展示了损失函数的凸性质以及最小二乘法求解出的全局最优解。

3.2 什么是梯度

在上面的实验中,我们通过最小二乘法直接求解了损失函数的最小值,这种方法适用于损失函数可以直接求解且问题规模不大的情况。然而,当我们处理的数据量巨大,或者更复杂的模型(如逻辑回归),直接求解往往变得非常困难或者不可行。在这种情况下,我们通常会使用一种称为梯度下降的优化算法。

接下来我们将介绍如何使用梯度下降算法来求解线性回归的最优参数。在实操之前,要先了解一下概念。

当处理优化问题,尤其是机器学习中的参数优化问题时,常常会遇到“梯度”这个概念。那么,什么是梯度呢?

从数学角度看,梯度是一个向量,它指出了函数在某一点上升最快的方向。

我们通过一个简单的例子来理解这个概念。

考虑一个简单的函数 f ( x ) = x 2 f(x) = x^2 f(x)=x2,它的图像是一个开口向上的抛物线。对于这个函数,它的梯度(实际上是导数,因为这是一个一元函数)就是函数f(x)关于x的导数,即 f ′ ( x ) = 2 x f'(x) = 2x f(x)=2x。这个梯度告诉我们函数在某一点x上的切线斜率。

如果我们在 x = 2 x=2 x=2 的地方计算梯度,那么 f ′ ( 2 ) = 2 ∗ 2 = 4 f'(2) = 2*2 = 4 f(2)=22=4。这意味着在 x = 2 x=2 x=2 的位置,函数 f ( x ) = x 2 f(x) = x^2 f(x)=x2 的切线斜率为4。也就是说,如果我们从 x = 2 x=2 x=2 的位置沿着x轴正方向移动一小段距离,函数值会增加4倍于这小段距离。

从优化问题的角度看,我们通常会沿着梯度的负方向进行参数更新,因为这是函数下降最快的方向。

例如,如果我们在 x = 2 x=2 x=2 的位置,我们的目标是找到函数的最小值,我们就应该沿着x轴负方向移动,这也正是梯度的负方向。

从生活中的角度来看,你可以把梯度想象成你站在山坡上,然后你闭上眼睛,只能通过感觉脚下的坡度来决定你下山的方向。

这个“感觉脚下的坡度”就是梯度,它告诉你哪个方向是下坡最陡的方向。

我们再来看图理解一下,代码如下:

import numpy as np
import matplotlib.pyplot as plt

# 定义函数和它的导数
def f(x):
    return x**2

def df(x):
    return 2*x

# 生成x的值
x = np.linspace(-3, 3, 100)

# 计算函数值和导数值
y = f(x)
dy = df(x)

# 定义在x=2处的切线
x_tan = np.linspace(1, 3, 10)
y_tan = df(2) * (x_tan - 2) + f(2)  # 切线方程

# 绘制函数图像
plt.figure(figsize=(8,6))
plt.plot(x, y, label="f(x) = x^2")

# 在x=2处绘制切线
plt.plot(x_tan, y_tan, label="tangent at x=2")

# 标记x=2处的点
plt.scatter(2, f(2), color='red')  
plt.text(2, f(2), 'x=2', fontsize=12, verticalalignment='bottom')

# 设置图像属性
plt.xlabel('x')
plt.ylabel('f(x)')
plt.title("Function and its derivative")
plt.legend()
plt.grid(True)
plt.show()

这段代码首先定义了函数 f ( x ) = x 2 f(x) = x^2 f(x)=x2 及其导数,然后生成了x的值,并计算了对应的函数值和导数值。然后,我们定义了在 x = 2 x=2 x=2 处的切线,并在图中画出了这条切线。最后,我们用红色的点标记了 x = 2 x=2 x=2 这个点

image-20230614130510177

这幅图很好地说明了梯度的概念。在这个二次函数 f ( x ) = x 2 f(x) = x^2 f(x)=x2 中,我们特意选取了一个点,即 x = 2 x=2 x=2。在这个点上,切线的斜率(也就是导数)就代表了函数在这个点上的梯度。

从图像上看,当 x = 2 x=2 x=2 时,切线的斜率为正,也就是说,如果你沿着 x x x 的正方向走,函数值 f ( x ) f(x) f(x) 会增加,这也正是梯度的方向。反之,如果你沿着 x x x 的负方向走,也就是逆着梯度的方向走,函数值会减少。

3.3 梯度下降的基本思想

梯度下降是一种迭代的优化算法,它的工作原理是通过计算损失函数的梯度(即偏导数),然后按梯度的负方向更新参数,逐步优化参数,使得损失函数的值不断减小,最终达到一个局部最小值(对于凸函数则是全局最小值)。

在优化问题中,我们通常希望找到函数的最小值。由于梯度给出了函数值增加最快的方向,因此,如果我们沿着梯度的反方向走,就可能找到函数的最小值,这就是梯度下降法的基本思想。

还是上面下山的例子,梯度能告诉我们哪个方向是下坡最陡的方向,我们的目标就是找到下山的最快路线,也就是找到函数的最小值点,而梯度下降法就是在每一步都选择下坡最陡的方向进行移动。

3.4 梯度下降求解线性回归参数的过程

梯度下降算法的目标仍然是求最小值,和最小二乘法这种直接解方程组求得最小值的方式不同,梯度下降是通过一种“迭代求解”的方式来进行最小值的求解,其整体求解过程可简单理解为:先随机选取一组参数初始值,然后沿着某个方向,一步一步移动到最小值点

我们通过3.1节中的例子,来解释梯度下降的计算过程:首先数据还是这个:

xy
12
24
36

第一步:确定模型和损失函数

我们仍然使用线性回归作为模型,SSE作为损失函数:
L o s s S S E = ∑ i = 1 n ( y i − y i ^ ) 2 = ∑ i = 1 n ( y i − ( w x i + b ) ) 2 (5) Loss_{SSE} = \sum_{i=1}^{n} (y_i - \hat{y_i})^2 = \sum_{i=1}^{n} (y_i - (wx_i + b))^2 \tag{5} LossSSE=i=1n(yiyi^)2=i=1n(yi(wxi+b))2(5)

目标是通过优化模型参数 w w w b b b,使得损失函数 S S E SSE SSE 的值尽可能小。

第二步:参数初始化

在训练模型时,需要先为参数设置一个初始值。这个初始值可以是任意的,但通常我们会选择一些常见的方法来初始化参数,例如设置为0,或者设置为一个随机数。

对于我们的线性模型,我们有两个参数需要初始化: w w w b b b。因为我们知道了 b b b是一个无限接近于0的值,所以为了方便演示,我们初始化 b b b=0, w w w=10。所以我们后面的计算过程就暂不考虑截距项b

但在某些情况下,我们可能会选择从一个随机分布(如正态分布或均匀分布)中抽取值来初始化参数。

第三步:计算第一次迭代的梯度

为了进行第一步的迭代,我们首先需要计算损失函数关于 w w w 的偏导数。我们已经有了偏导数的通用表达式:(注意:此处我们暂不考虑截距项b)

∂ S S E ∂ w = − 2 ∑ i = 1 n x i ( y i − w x i ) (6) \frac{\partial SSE}{\partial w} = -2\sum_{i=1}^{n}x_i(y_i - wx_i) \tag{6} wSSE=2i=1nxi(yiwxi)(6)

在这个表达式中,我们将初始的 w = 10 w=10 w=10 带入,并计算偏导数的值。这个值可以通过将每个数据点的 x i x_i xi y i y_i yi 带入然后求和得到:

∂ S S E ∂ w = − 2 [ 1 ∗ ( 2 − 10 ∗ 1 ) + 2 ∗ ( 4 − 10 ∗ 2 ) + 3 ∗ ( 6 − 10 ∗ 3 ) ] = 224 (7) \frac{\partial SSE}{\partial w} = -2[1*(2-10*1) + 2*(4-10*2) + 3*(6-10*3)] = 224 \tag{7} wSSE=2[1(2101)+2(4102)+3(6103)]=224(7)

我们把第一次迭代 w 0 w_0 w0=10的时候, 梯度=224

在确定了梯度之后,接下来参数的移动方向也随之确定,在梯度下降算法中,参数的移动方向是梯度的负方向。224既是梯度的取值,同时也代表着梯度的方向——正方向。而此时梯度的负方向则是取值减少的方向

第四步:参数更新

在得到第一次梯度的值后,我们需要使用这个梯度值来更新我们的参数。这个过程叫做参数更新,是梯度下降算法的关键步骤。

我们需要使用学习率(也被称为步长)和梯度的乘积来更新参数。学习率是一个超参数,决定了我们每次迭代时参数更新的幅度。

假设我们设定的学习率为 l r = 0.01 lr = 0.01 lr=0.01。所以从 w 0 w_0 w0进行移动的距离是 0.01 ∗ 224 = 2.24 0.01 * 224 = 2.24 0.01224=2.24,而又是朝向梯度的负方向进行移动,因此 w 0 w_0 w0最终移动到了 w 1 = 10 − 2.24 = 7.76 w_1 = 10-2.24 = 7.76 w1=102.24=7.76,至此,参数 w w w就完成了第一次移动, w 0 → w 1 w_0 \rightarrow w_1 w0w1

第五步:迭代

我们通过3.1节中知道了w的最优参数是2,此时我们第一轮参数 更新后得到的w是7.76,距离最优值还有很长的一段距离,所以我们需要不断重复上面的过程,直至收敛。

当我们进行第二次迭代的时候,即从 w 1 → w 2 w_1 \rightarrow w_2 w1w2时:

w 2 = w 1 − l r ⋅ ∇ w f ( w 1 ) (8) w_2 = w_1 - lr \cdot \nabla _wf(w_1) \tag{8} w2=w1lrwf(w1)(8)

其中 ∇ w f = ∂ S S E ∂ w \nabla _wf = \frac{\partial SSE}{\partial w} wf=wSSE; 表示 w 2 w_2 w2等于 w 1 w_1 w1沿着 w 1 w_1 w1的负梯度方向移动了 l r ⋅ ∇ w f ( w 1 ) lr\cdot\nabla _wf(w_1) lrwf(w1)距离之后得到的结果。即在 w 1 w_1 w1的基础上,减去学习率和 w 1 w_1 w1梯度的乘积。

所以我们能推导出一般形式:

w n = w ( n − 1 ) − l r ⋅ ∇ w f ( w ( n − 1 ) ) (9) w_n = w_{(n-1)} - lr \cdot \nabla _wf(w_{(n-1)}) \tag{9} wn=w(n1)lrwf(w(n1))(9)

通过带入梯度值进行多轮迭代,最终使得损失函数的取值逐渐下降的算法,这个过程就是梯度下降的计算过程。

接下来我们 使用Python来手动实现这个过程:

def gradient(w, x, y):
    return -2 * np.sum(x * (y - w * x))

# 参数w的范围
w_values = np.linspace(-8, 12, 400)

# 计算在每个参数w值下的SSE
loss_values = [SSE(w, x, y) for w in w_values]

# 初始化参数w
w_init = 10
lr = 0.01  # 学习率
iterations = 25  # 迭代次数

# 存储每次迭代后的参数w
w_history = [w_init]
# 存储每次迭代后的损失值
loss_history = [SSE(w_init, x, y)]

# 进行梯度下降
for i in range(iterations):
    w_new = w_history[-1] - lr * gradient(w_history[-1], x, y)
    w_history.append(w_new)
    loss_history.append(SSE(w_new, x, y))

# 绘制损失函数的图像
plt.figure(figsize=(10, 6))
plt.plot(w_values, loss_values, label='SSE')
plt.scatter(w_history, loss_history, color='red')  # 绘制参数移动的过程
plt.plot(w_history, loss_history, color='red', alpha=0.6)  # 添加参数更新的连线
plt.title('SSE as a function of w and parameter updates')
plt.xlabel('w')
plt.ylabel('SSE')
plt.legend()
plt.grid(True)
plt.show()

print("最优的参数w值是:", w_history[-1])

这段代码实现了梯度下降法优化线性回归模型的过程,包括定义损失函数及其梯度,初始化参数,进行梯度下降迭代,以及绘制损失函数和参数更新的过程。最终打印出了优化后的最佳参数值。

image-20230614154433910

图像展示了损失函数(SSE)关于参数w的曲线以及参数w的更新过程。整体曲线形状为抛物线,表示当参数w取最优解时,损失函数达到最小值。红点和红线代表了梯度下降过程中参数w的更新轨迹,从一个较大的初始值开始,逐渐向最优解靠近,这个过程体现了梯度下降的核心思想:通过不断沿着梯度的负方向更新参数,以最小化损失函数。

细心的应该会发现,用最小二乘法得到的最优参数w是:2.000000000000001,使用梯度下降得到的最优参数w是:2.002169713532925,这是因为:梯度下降和最小二乘法都是优化问题的求解方法,但它们的结果可能会有微小的差异。最小二乘法直接得到的是解析解,对于线性回归问题,最小二乘法可以直接得到最优解。而梯度下降是一种迭代的优化方法,它依赖于初始参数、学习率和迭代次数等因素。在迭代过程中,它会逐步接近最优解,但不一定能完全达到最优解,特别是在有限的迭代次数下。

3.5 梯度下降的计算过程总结

基于上述过程,我们进行梯度下降计算过程总结:

  1. 模型选择和损失函数定义:首先确定所要使用的模型和相应的损失函数。模型应当符合问题的实际情况,损失函数是用来度量模型预测与实际值之间的偏差,其值越小表示模型的预测越准确。
  2. 初始化模型参数:在模型训练的开始,需要为模型参数设定一个初始值。这个初始值可以是零,随机数,或者是基于某种分布抽取的随机数。正确的初始化可以提高模型训练的效率和最终的性能。
  3. 计算梯度:计算损失函数在当前模型参数下的梯度,这个梯度指向的方向是损失函数增长最快的方向。因此,如果我们想要最小化损失函数,就应当让参数向着梯度的负方向移动。
  4. 更新模型参数:使用学习率(一个预先设定的正数,控制参数更新的步长)和梯度来更新模型参数。这里的参数更新公式为 new_param = old_param - learning_rate * gradient,这样的更新方式会使得模型参数逐渐向着损失函数的最小值移动。
  5. 迭代优化:重复进行梯度计算和参数更新的过程,每一次迭代都会使得模型的预测效果变得更好(即损失函数的值更小)。这个过程会持续进行,直到达到预设的最大迭代次数,或者损失函数的值变化小于某个设定的阈值,此时可以认为模型已经收敛,不再进行迭代。

这就是梯度下降方法的优化流程,它是一种迭代的优化算法,通过不断地向着梯度的负方向更新参数,以此来最小化损失函数。

相信通过对比最小二乘法的参数求解过程,大家能更好的理解梯度下降到底在做什么!

四、如何选择合适的学习率

上面我们也提到了学习率这个概念,学习率(Learning Rate)是在机器学习或深度学习中优化算法(如梯度下降)的一个重要参数。它控制着模型参数在每次迭代中更新的步长。但如何选择学习率,其实是一个值得思考的问题。

具体来说,如果学习率设定得过大,可能会使模型在寻找最优解时跳过最优解点,导致模型无法收敛;反之,如果学习率设定得过小,可能会使模型的训练速度过慢,需要花费过多时间才能收敛到最优解。同样,我们通过代码来看一下这两种情况:

import matplotlib.pyplot as plt
import numpy as np

def SSE(w, x, y):
    return np.sum((y - w * x) ** 2)

def gradient(w, x, y):
    return -2 * np.sum(x * (y - w * x))

# 数据
x = np.array([1, 2, 3])
y = np.array([2, 4, 6])

# 参数w的范围
w_values = np.linspace(-10, 12, 400)

# 计算在每个参数w值下的SSE
loss_values = [SSE(w, x, y) for w in w_values]

# 初始化参数w
w_init = 10

# 设置两个学习率
lr_small = 0.001  # 小的学习率
lr_big = 0.1  # 大的学习率

# 存储每次迭代后的参数w和损失值
w_history_small, loss_history_small = [w_init], [SSE(w_init, x, y)]
w_history_big, loss_history_big = [w_init], [SSE(w_init, x, y)]

# 迭代次数
iterations = 30

# 进行梯度下降
for i in range(iterations):
    # 小学习率
    w_new_small = w_history_small[-1] - lr_small * gradient(w_history_small[-1], x, y)
    w_history_small.append(w_new_small)
    loss_history_small.append(SSE(w_new_small, x, y))

    # 大学习率
    w_new_big = w_history_big[-1] - lr_big * gradient(w_history_big[-1], x, y)
    w_history_big.append(w_new_big)
    loss_history_big.append(SSE(w_new_big, x, y))

# 绘制损失函数的图像
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.plot(w_values, loss_values, label='SSE')
plt.scatter(w_history_small, loss_history_small, color='red')  # 绘制参数移动的过程
plt.plot(w_history_small, loss_history_small, color='red', alpha=0.6)  # 添加参数更新的连线
plt.title('Small Learning Rate')
plt.xlabel('w')
plt.ylabel('SSE')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(w_values, loss_values, label='SSE')
plt.scatter(w_history_big, loss_history_big, color='red')  # 绘制参数移动的过程
plt.plot(w_history_big, loss_history_big, color='red', alpha=0.6)  # 添加参数更新的连线
plt.title('Big Learning Rate')
plt.xlabel('w')
plt.ylabel('SSE')
plt.legend()

plt.tight_layout()
plt.show()

上述代码首先定义了损失函数和梯度的计算,然后生成了一组模型参数的可能值及其对应的损失。接着,它设定了一个较小和一个较大的学习率,进行了30次的参数更新迭代,并存储了每次迭代后的参数值和损失。最后,它将两个学习率下参数更新过程的图像进行了绘制,展示了参数在损失函数上的移动轨迹。

image-20230614160334748

我们可以清楚地看到,当学习率较小时(0.001),模型的参数更新速度较慢,导致需要更多的迭代次数才能收敛到最优解;而当学习率较大时(例如0.1),模型的参数更新速度较快,可能会在最优解附近震荡,甚至可能越过最优解,导致模型无法收敛。

因此,合适的学习率的选择对于模型的训练效果和训练速度都有重要的影响。通常,我们会在训练过程中动态调整学习率,如使用学习率衰减策略,或者使用一些自适应学习率的优化算法(如Adam、RMSprop等)。这些我们都会在后面的内容详细讲解。

五、梯度下降的优势与局限性

5.1 梯度下降的优势

对于大规模数据集和高维度特征,梯度下降法仍然可以工作。只要是可导的目标函数,基本都可以使用梯度下降来寻找最小值,而且梯度下降算法的实现相对简单,易于实现和理解

5.2 梯度下降的局限性

5.2.1 局部最小值问题

如果目标函数有多个最小值,梯度下降可能会陷入局部最小值而不是全局最小值。特别是在处理深度神经网络时,这是一个非常棘手的问题。

局部最小值是指函数在一定范围内的最小值,但可能不是全局的最小值。

假设我们有一个函数f(x),如果存在一个区间[a,b],在这个区间内,对于任意的x属于[a,b],我们都有f(a)≤f(x)≤f(b),那么我们就说f(a)和f(b)是这个函数在区间[a,b]的局部最小值。

以一维函数为例,如果在某一点的左边和右边的值都比这一点的值大,那么我们就说这一点是函数的局部最小值。这种概念在高维度的情况下也是适用的。在多维度的情况下,如果一个点在其所有方向上的值都比其周围的点小,那么这个点就是函数的局部最小值。

这种现象在优化问题中是非常重要的。很多优化算法,如梯度下降法,会试图找到函数的最小值。然而,如果函数有多个最小值,优化算法可能只找到其中的一个最小值,这就是所谓的“局部最优解”,并不一定是全局的最优解。

我们通过编写一个简单的梯度下降算法来寻找函数的最小值,以此来证明梯度下降可能会陷入局部最小值的问题。

import numpy as np
import matplotlib.pyplot as plt

# 定义函数和它的导数
def f(x):
    return x * np.cos(np.pi * x)

def df(x):
    return np.cos(np.pi * x) - x * np.pi * np.sin(np.pi * x)

# 定义梯度下降函数
def gradient_descent(start_x, df, epochs, lr): 
    xs = np.zeros(epochs+1)
    x = start_x
    xs[0] = x
    for i in range(epochs):
        dx = df(x)
        x = x - lr * dx
        xs[i+1] = x
    return xs

# 设定初始值,迭代次数和学习率
start_x = -0.5
epochs = 3000
lr = 0.02

# 定义x的取值范围
x = np.linspace(-1, 2, 400)

# 执行梯度下降
X = gradient_descent(start_x, df, epochs, lr)

# 画图
plt.figure(figsize=[10,5])
plt.scatter(X, f(X), color='red')
plt.plot(x, f(x))
plt.title('Gradient Descent')
plt.xlabel('x')
plt.ylabel('f(x)')

plt.tight_layout()
plt.show()

这段代码执行了梯度下降算法来寻找函数的最小值。它从x=-0.5开始,通过迭代更新参数x,绘制了参数更新的过程。结果显示,梯度下降算法陷入了局部最小值,而未能找到全局最小值,这展示了梯度下降的局限性。

image-20230614172018612

在这个图像中,红色的点表示梯度下降算法在每一步迭代后的位置。我们可以看到,虽然函数在x=1.1附近有一个全局最小值,但是梯度下降算法从x=-0.5开始后,却在x=-0.3左右的位置陷入了一个局部最小值,并没有继续前进到全局最小值的位置。这是因为在局部最小值的位置,函数的导数(即梯度)为零,梯度下降算法以为自己已经找到了最小值,于是停止了进一步的搜索。这个图像很好地展示了梯度下降算法的一个局限性:它可能会陷入局部最小值,而无法找到全局最小值。

5.2.2 选择合适的学习率难

学习率太小,收敛速度会很慢;学习率太大,可能会越过最小值点,导致算法不收敛。

5.2.3 对初始值敏感

不同的初始值可能会导致梯度下降算法收敛到不同的最小值。

5.2.4 可能会受到鞍点影响

在某些情况下,梯度可能会接近0,但并非在最小值点,这种情况称为鞍点,可能会使梯度下降法停止迭代。在多维空间中,一个鞍点是指在某一维度上看是最大值,而在另一维度上看是最小值的点。

严格来说,一个函数在某一点的二阶导数(也就是导数的导数)可以用来判断这个点是否是鞍点。如果二阶导数在所有方向上都大于0,那么这个点就是局部最小值;如果二阶导数在所有方向上都小于0,那么这个点就是局部最大值;如果二阶导数在某些方向上大于0,在其他方向上小于0,那么这个点就是鞍点。

我们可以这样理解:鞍点是那种不是极值点但梯度为0的点。所谓极值,指的是那些连续函数上导数为0、并且所有两边单调性相反的点,极值包括局部最小值、最小值点、局部最大值和最大值点四类。而鞍点和极值点的区别在于导数为0但是左右两边单调性相同,例如:

import numpy as np
import matplotlib.pyplot as plt

# 定义函数和梯度
def f(x):
    return np.power(x, 3)

def df(x):
    return 3*np.power(x, 2)

# 梯度下降函数
def gradient_descent(start_x, df, epochs, lr): 
    xs = np.zeros(epochs+1)
    x = start_x
    xs[0] = x
    for i in range(epochs):
        dx = df(x)
        x = x - lr * dx
        xs[i+1] = x
    return xs

# 参数
start_x = 0.6
epochs = 3000
lr = 0.01
x = np.linspace(-1, 1, 400)

# 梯度下降
X = gradient_descent(start_x, df, epochs, lr)

# 绘制图像
plt.figure(figsize=[10,5])
plt.subplot(1,2,1)
plt.scatter(X, f(X), color='red')
plt.plot(x, f(x))
plt.title('Gradient Descent')
plt.xlabel('x')
plt.ylabel('f(x)')

plt.tight_layout()
plt.show()

这段代码通过Python实现了梯度下降算法,目标函数是 f ( x ) = x 3 f(x) = x^3 f(x)=x3,展示了梯度下降在寻找最小值过程中可能会陷入鞍点的情况。

image-20230614173053980

在这个特定的函数 f ( x ) = x 3 f(x) = x^3 f(x)=x3中,x=0处是一个鞍点,即在这个点上,函数的一阶导数(也就是斜率)等于0。梯度下降算法的工作原理是沿着斜率最陡峭的方向下降,也就是找到斜率(一阶导数)为零的点。当梯度下降算法找到鞍点时,由于在鞍点处斜率也为零,所以算法会认为它找到了一个最小值,从而停止迭代。

然而,在鞍点处,虽然一阶导数为零,但并不意味着这是一个最小值点,因为在这个点的某些方向上,函数值可能会增加,而在另一些方向上,函数值可能会减少。在我们的例子中,鞍点并不是全局最小值,但由于梯度下降算法只考虑了一阶导数,所以它不能区分鞍点和真正的最小值,因此陷入了鞍点陷阱。

5.3.5 如何解决上述问题

梯度下降方法本质上是寻找能使参数移动到梯度为零的位置,即损失函数最小值的地方。但问题在于,如果损失函数不是严格凸函数,那么梯度为零的位置可能是局部最小值或者鞍点。由于梯度下降算法的计算方式依赖于损失函数的斜率,即一阶导数,当一阶导数为零时,如在局部最小值或鞍点,算法可能会陷入这些点,无法找到路径继续向下降。

对此,一种有效的解决办法是利用随机梯度下降或小批量梯度下降。这些方法在每次参数更新时仅使用一部分数据,从而为参数提供跳出陷阱的机会。具体来说,随机梯度下降在每次更新时只使用一个样本,而小批量梯度下降则使用一小批样本。这样的策略可以引入更新方向的变化,增加了逃离局部最小值或鞍点,从而找到全局最小值的机会。

六、批量梯度下降(Batch Gradient Descent)

梯度下降算法是一种寻找函数最小值点的方法,通过多次迭代计算,它能够收敛至一个梯度为0的点。当优化的目标函数为凸函数时,梯度下降能顺利收敛至全局最小值,因为凸函数的全局最小值点是唯一的梯度为0的点。然而,如果目标函数不是凸函数,梯度下降算法可能陷入局部最小值或鞍点。

为了克服这个问题,我们可以在原有算法的基础上进行改进,例如调整每次用于计算的样本数量。这种策略能够通过改变更新的方向和幅度,利用局部规律的不一致性来规避陷阱。具体来说,有两种主要的改进方法:

  1. 随机梯度下降(SGD):每次只使用一个样本进行更新,增加更新方向的随机性,有机会跳出局部最小值。
  2. 小批量梯度下降(Mini-batch SGD):每次使用一小部分样本进行更新,既保证了一定的计算效率,也保持了一定的随机性。

这两种方法通过引入一定的随机性,试图避免梯度下降算法陷入非全局最优的陷阱。但需要注意的是,即使采用了这些改进方法,也不能保证在复杂的非凸优化问题中总能找到全局最优解。

七、随机梯度下降(Stochastic Gradient Descent,SGD)

7.1 定义和原理

7.1.1 什么是随机梯度下降

随机梯度下降(Stochastic Gradient Descent,简称SGD)是一种求解最优化问题的迭代方法,尤其适用于处理大规模的数据集。在机器学习和深度学习中,它被广泛应用于模型参数的优化。

在标准的梯度下降(Gradient Descent)算法中,我们在每一步都使用整个数据集来计算损失函数的梯度。然而,当数据集非常大时,这会导致计算量巨大且速度慢,同时我们上面提到了。标准的梯度下降很容易陷入局部最小值和鞍点,为了解决这些问题,随机梯度下降在每一步只随机选择一个样本(或者一小部分样本)来估计梯度,这种随机性有时候甚至可以帮助算法跳出局部最优解,找到更好的解。

7.1.2 如何运用随机梯度下降进行优化

随机梯度下降与标准的梯度下降过程基本一致,总结如下:

  1. 初始化参数:随机选择一个初始点作为参数的起始值。
  2. 随机抽样:从训练集中随机选取一个样本。
  3. 计算梯度:计算这个样本在当前参数下的损失函数的梯度。
  4. 更新参数:用计算出的梯度对当前参数进行更新。
  5. 重复步骤2-4:持续进行迭代,直到满足停止准则,如达到预设的最大迭代次数,或者连续几次迭代的参数更新幅度都小于一个预设的阈值。

梯度下降(Gradient Descent)和随机梯度下降(Stochastic Gradient Descent)区别在于:梯度下降每次更新时使用全体训练数据计算梯度,消耗大量计算资源,但更新路径稳定。随机梯度下降每次更新时仅随机选取一个样本计算梯度,计算效率高,但更新路径可能波动较大。因此,二者各有利弊,选择哪种方法取决于具体的应用场景和数据规模

7.1.3 SGD的计算流程示例

有以下样本数据:

xy
12
35

使用线性模型 y = w x y=wx y=wx 进行建模。为每一个样本都建立一个损失函数。在这个数据中,有两个样本,所以我们有两个损失函数:

  • 对于样本1: L o s s S S E 1 = ( 2 − w ∗ 1 ) 2 Loss_{SSE_1} = (2 - w * 1)^2 LossSSE1=(2w1)2

  • 对于样本2: L o s s S S E 2 = ( 5 − w ∗ 3 ) 2 Loss_{SSE_2} = (5 - w * 3)^2 LossSSE2=(5w3)2

目标是找到一个 w w w 使得这两个损失函数的和最小。

初始设定参数 w = 0 w = 0 w=0,学习率 α = 0.01 \alpha = 0.01 α=0.01

首次迭代,随机选择样本1,计算梯度:

d L o s s S S E 1 d w = 2 ∗ ( 2 − w ∗ 1 ) ∗ − 1 = − 2 ∗ ( 2 − w ) (10) \frac{dLoss_{SSE_1}}{dw} = 2 * (2 - w * 1) * -1 = -2 * (2 - w) \tag{10} dwdLossSSE1=2(2w1)1=2(2w)(10)

初始的 w = 0 w = 0 w=0,所以初始梯度为 -4。更新 w w w

w n e w = w o l d − α ∗ g r a d i e n t = 0 − 0.01 ∗ ( − 4 ) = 0.04 (11) w_{new} = w_{old} - \alpha * gradient = 0 - 0.01 * (-4) = 0.04 \tag{11} wnew=woldαgradient=00.01(4)=0.04(11)

第一次迭代后, w = 0.04 w = 0.04 w=0.04

再次迭代,假设我们随机选择了样本2,同样计算梯度:

d L o s s S S E 2 d w = 2 ∗ ( 5 − w ∗ 3 ) ∗ − 3 = − 6 ∗ ( 5 − 0.04 ∗ 3 ) (12) \frac{dLoss_{SSE_2}}{dw} = 2 * (5 - w * 3) * -3 = -6 * (5 - 0.04 * 3) \tag{12} dwdLossSSE2=2(5w3)3=6(50.043)(12)

这里的 w w w 为上一轮迭代后的结果,也就是 0.04。计算得到梯度,然后更新 w w w。 持续这个过程,直到 w w w 收敛,或者达到预设的迭代次数。

上述就是随机梯度下降的计算过程,我们用代码验证一下计算是否正确

import numpy as np

# 数据
x = np.array([1, 3])
y = np.array([2, 5])

# 初始化
w = 0
alpha = 0.01

# 迭代
for i in range(2):  # 这里为了演示,只迭代两次
    # 按照指定顺序选择样本
    idx = i % 2  # 当i=0选择样本1,当i=1选择样本2
    
    # 计算梯度
    gradient = -2 * (y[idx] - w * x[idx]) * x[idx]
    
    # 更新w
    w = w - alpha * gradient
    
    print('Iteration {}: w = {}'.format(i + 1, w))

输出结果为,可以看到,与我们的数学计算结果是一致的。

Iteration 1: w = 0.04
Iteration 2: w = 0.3328

在这个示例中,我们仅用两个样本展示了随机梯度下降的计算过程。首先,我们为每个样本建立了一个损失函数,然后从这些样本中随机选择一个,对其计算梯度,并使用此梯度更新模型的参数。这个过程会反复进行,直到模型参数收敛或达到预设的迭代次数。

请注意,虽然我们在这里只使用了两个样本进行说明,但在实际应用中,随机梯度下降法可以处理包含任意多样本的数据集。对于包含多个样本的大数据集,每次迭代我们依旧是从中随机选择一个样本进行计算。因此,我们在两个样本上演示的计算过程可以很自然地推广到多个样本的情况。

7.2 SGD的优缺点

7.2.1 高计算效率但无法收敛到最优值

针对上述过程,我们对比一下标准的梯度下降与随机梯度下降的效果对比,还是直接上代码:

import numpy as np
import matplotlib.pyplot as plt
import time

# 数据
x = np.array([1, 3])
y = np.array([2, 5])

# 初始化
w_sgd = 0
w_bgd = 0
alpha = 0.01
n_epochs = 1000  # 最大迭代轮数
tolerance = 1e-6  # 收敛的判定阈值

# 为了记录迭代过程中的w值和损失值,我们初始化两个列表
ws_sgd = [w_sgd]
losses_sgd = []

ws_bgd = [w_bgd]
losses_bgd = []

# SGD计算
start_time_sgd = time.time()

for epoch in range(n_epochs):
    loss_sgd = 0
    for idx in range(2):
        # 计算梯度
        gradient_sgd = -2 * (y[idx] - w_sgd * x[idx]) * x[idx]
        
        # 更新w
        w_sgd_new = w_sgd - alpha * gradient_sgd
        
        # 计算损失
        loss_sgd += (y[idx] - w_sgd_new * x[idx]) ** 2
    
    # 计算平均损失
    loss_sgd /= 2
    
    # 判断是否收敛
    if np.abs(w_sgd_new - w_sgd) < tolerance:
        print('SGD converged at epoch {}'.format(epoch + 1))
        break
    
    # 更新w
    w_sgd = w_sgd_new
    
    # 记录w值和损失值
    ws_sgd.append(w_sgd)
    losses_sgd.append(loss_sgd)

end_time_sgd = time.time()
elapsed_time_sgd = end_time_sgd - start_time_sgd

# BGD计算
start_time_bgd = time.time()

for epoch in range(n_epochs):
    # 计算梯度
    gradient_bgd = -2 * np.sum((y - w_bgd * x) * x)
    
    # 更新w
    w_bgd_new = w_bgd - alpha * gradient_bgd

    # 计算损失
    loss_bgd = np.sum((y - w_bgd_new * x) ** 2) / 2
    
    # 判断是否收敛
    if np.abs(w_bgd_new - w_bgd) < tolerance:
        print('BGD converged at epoch {}'.format(epoch + 1))
        break
    
    # 更新w
    w_bgd = w_bgd_new

    # 记录w值和损失值
    ws_bgd.append(w_bgd)
    losses_bgd.append(loss_bgd)

end_time_bgd = time.time()
elapsed_time_bgd = end_time_bgd - start_time_bgd

# 打印结果
print("SGD: Optimal w: ", w_sgd, ", Time elapsed: ", elapsed_time_sgd)
print("BGD: Optimal w: ", w_bgd, ", Time elapsed: ", elapsed_time_bgd)

# 绘制损失值的变化
plt.figure(figsize=(10, 6))
plt.plot(losses_sgd, label='SGD')
plt.plot(losses_bgd, label='BGD')
plt.title('Loss Value Over Epochs')
plt.xlabel('Epochs')
plt.ylabel('Loss Value')
plt.grid(True)
plt.legend()
plt.show()

这段代码实现了随机梯度下降 (SGD) 和批量梯度下降 (BGD),并计算了每个算法的耗时和收敛的迭代次数。同时,它也会绘制出两种算法的损失函数随迭代次数的变化情况,以直观地比较两者的效果。

image-20230615102124699

我们从结果来看:SGD 和 BGD 都收敛到了接近真实值的解,这表明这两种梯度下降方法都可以有效地求解优化问题。而 SGD 需要的迭代次数稍多一些,这是因为 SGD 采用随机的方式更新参数,可能会造成在收敛过程中存在一定的波动,导致需要更多的迭代次数。

从时间消耗来看,看到的时间差别不大,这主要是因为样本数量较少,计算量不大,时间差别并不明显。然而在处理大数据集时,SGD 的优势就非常明显了,因为它每次只需计算一个样本的梯度,大大减少了计算量。大家可以自己尝试带入大规模的数据集,查看结果。

在图像中,两种梯度下降方法的损失值都随着迭代次数的增加而逐渐下降,且最终都收敛到了接近最小损失的状态。

总的来说,SGD 的主要优点是计算效率高,特别适合于大规模数据集,而 BGD 由于每次都需要计算所有样本的梯度,所以在大规模数据集上的效率较低。

7.2.2 随机性会避免陷入局部最优解但会导致迭代震荡

我们有两个样本(x=1.5, y=2.25)和(x=2.5, y=6.25),因此可以为每个样本都计算一个损失函数。对于给定的模型参数 w,这两个损失函数为:

  • 对于样本1: L o s s 1 = ( 2.25 − w ∗ 1.5 ) 2 Loss_{1} = (2.25 - w * 1.5)^2 Loss1=(2.25w1.5)2
  • 对于样本2: L o s s 2 = ( 6.25 − w ∗ 2.5 ) 2 Loss_{2} = (6.25 - w * 2.5)^2 Loss2=(6.25w2.5)2

每个损失函数都反映了模型在对应样本上的预测误差。在随机梯度下降过程中,我们每次随机选择一个样本,计算其损失函数并更新模型参数。这样,模型参数的更新方向会受到所选择样本的影响,因此在这两个损失函数的最小值点之间产生震荡。

我们画图看一下:

import matplotlib.pyplot as plt
import numpy as np

# 数据
x1 = 1.5
y1 = 2.25
x2 = 2.5
y2 = 6.25

# 参数范围
ws = np.arange(0, 3, 0.01)

# 计算损失函数值
loss1 = np.power(y1 - ws * x1, 2)
loss2 = np.power(y2 - ws * x2, 2)

# 画图
plt.figure(figsize=(10, 6))
plt.plot(ws, loss1, label='Sample 1')
plt.plot(ws, loss2, label='Sample 2')
plt.xlabel('w')
plt.ylabel('Loss')
plt.legend()
plt.show()

image-20230615110126767

从随机梯度下降的过程中可以看出,每次都是随机选择一个样本来进行梯度的计算和参数更新。这就意味着在每一次迭代中,参数更新的方向并不总是指向全局的最小值点,而是指向了当前选择的样本的最小值点。

在本次示例中,有两个损失函数:

  • L o s s S S E 1 = ( 2 − w ∗ 1 ) 2 Loss_{SSE_1} = (2 - w * 1)^2 LossSSE1=(2w1)2 对应于第一个样本。
  • L o s s S S E 2 = ( 5 − w ∗ 3 ) 2 Loss_{SSE_2} = (5 - w * 3)^2 LossSSE2=(5w3)2 对应于第二个样本。

对于第一个损失函数 L o s s S S E 1 Loss_{SSE_1} LossSSE1,如果当前的 w w w 值小于真实的参数值2,则损失函数的值将会增加,因此参数更新的方向应该是向右(即 w w w 应增大);相反,如果当前的 w w w 值大于2,则损失函数的值将会增加,因此参数更新的方向应该是向左(即 w w w 应减小)。

对于第二个损失函数 L o s s S S E 2 Loss_{SSE_2} LossSSE2,由于 w w w 的系数是3,所以如果当前的 w w w 值小于真实的参数值5/3,则损失函数的值将会增加,因此参数更新的方向应该是向右(即 w w w 应增大);相反,如果当前的 w w w 值大于5/3,则损失函数的值将会增加,因此参数更新的方向应该是向左(即 w w w 应减小)。

所以可以想象到,对于不同的样本,梯度下降的方向可能会有所不同,这就可能导致参数在更新过程中发生震荡。

在这个示例中,样本1和样本2的最小值点并不重合,这就意味着如果在一次迭代中我们选择了样本1来进行参数更新,参数就会向着样本1的最小值点移动;而在下一次迭代中,如果我们选择了样本2,那么参数就会向着样本2的最小值点移动。因此,参数会在这两个点之间不断震荡。

但尽管参数会在这两个点之间震荡,但随着迭代次数的增加,这个震荡的幅度会越来越小,因为参数会逐渐向着这两个最小值点的中间地带收敛。但是这种震荡虽然可能会让算法跳出局部最优。

7.2.3 SGD总结

随机梯度下降(Stochastic Gradient Descent, SGD)其核心思想是使用单个样本或少量样本来近似全体样本的梯度,从而大大提高了在大数据集上的计算效率。然而,SGD的一个缺点是由于使用的样本数量较少,导致计算出的梯度可能并不准确,会导致更新的参数值在最优解附近发生震荡。

SGD在每一步迭代中只使用一个样本来计算梯度,这一策略既具有随机性,有可能帮助跳出局部最优解,也因此可能导致参数更新路径存在较大的震荡。在大规模数据集上,SGD的计算效率高,但可能需要较多的迭代次数才能达到满意的结果。

对于大数据集,批量梯度下降(Batch Gradient Descent,BGD)每次使用全部数据进行梯度计算,虽然更新路径稳定,但计算复杂度高;随机梯度下降(SGD)每次只用一个样本,虽然计算效率高,但更新路径波动大。

那么,有没有一种方法既可以利用更多的样本来计算更精确的梯度,又能保持较高的计算效率呢?答案是肯定的,那就是Mini-Batch Gradient Descent。Mini-Batch Gradient Descent 是 SGD 和 BGD 的折中,它每次使用一个小批量的样本来计算梯度和更新参数。具体来说,我们每次从训练集中随机选择一个小批量的样本,然后用这些样本的平均梯度来更新参数。这种做法既可以降低计算复杂度,又能减小参数更新的波动,是在大规模数据集上常用的优化方法。在下一节中,我们将详细介绍 Mini-Batch Gradient Descent 的原理和实现。

八、小批量随机梯度下降(Mini-batch Stochastic Gradient Descent)

8.1 定义和工作原理

8.1.1 什么是批量随机梯度下降

小批量梯度下降法(Mini-batch Gradient Descent)是结合了批量梯度下降(Batch Gradient Descent)和随机梯度下降(Stochastic Gradient Descent)两者的优点的一种方法。与每次只选择一个样本的随机梯度下降不同,小批量梯度下降每次会随机选择一部分样本进行梯度的计算和参数的更新。

8.1.2 如何运用小批量随机梯度下降进行优化

小批量随机梯度下降与标准的梯度下降过程基本一致,总结如下:

  1. 首先,随机初始化模型参数。
  2. 然后,从训练数据集中随机选取一个小批量样本。
  3. 接着,对于这个小批量样本,计算损失函数的梯度。
  4. 根据梯度更新模型的参数。
  5. 重复步骤2-4,直到达到最大迭代次数,或者模型的损失小于预设的阈值。
8.1.3 Mini-batch SGD的计算流程示例

有以下样本数据:

xy
12
35
24
47

使用线性模型 y = w x y=wx y=wx 进行建模。为每一个样本都建立一个损失函数。在这个数据中,有四个样本,所以我们有四个损失函数:

  • 对于样本1: L o s s S S E 1 = ( 2 − w ∗ 1 ) 2 Loss_{SSE_1} = (2 - w * 1)^2 LossSSE1=(2w1)2
  • 对于样本2: L o s s S S E 2 = ( 5 − w ∗ 3 ) 2 Loss_{SSE_2} = (5 - w * 3)^2 LossSSE2=(5w3)2
  • 对于样本3: L o s s S S E 3 = ( 4 − w ∗ 2 ) 2 Loss_{SSE_3} = (4 - w * 2)^2 LossSSE3=(4w2)2
  • 对于样本4: L o s s S S E 4 = ( 7 − w ∗ 4 ) 2 Loss_{SSE_4} = (7 - w * 4)^2 LossSSE4=(7w4)2

目标是找到一个 w w w 使得这四个损失函数的和最小。

初始设定参数 w = 0 w = 0 w=0,学习率 α = 0.01 \alpha = 0.01 α=0.01,Mini-Batch的大小设为2。

我们将数据集分为两个批次进行小批量梯度下降。第一批包含样本1和样本2,第二批包含样本3和样本4。

首先计算第一批的平均梯度:

  • 对于样本1,梯度 ∇ L ( 1 ) = − 2 x ( 1 ) ( y ( 1 ) − w x ( 1 ) ) = − 2 ∗ 1 ∗ ( 2 − 0 ∗ 1 ) = − 4 \nabla L(1) = -2x(1)(y(1) - wx(1)) = -2*1*(2 - 0*1) = -4 L(1)=2x(1)(y(1)wx(1))=21(201)=4
  • 对于样本2,梯度 ∇ L ( 2 ) = − 2 x ( 2 ) ( y ( 2 ) − w x ( 2 ) ) = − 2 ∗ 3 ∗ ( 5 − 0 ∗ 3 ) = − 30 \nabla L(2) = -2x(2)(y(2) - wx(2)) = -2*3*(5 - 0*3) = -30 L(2)=2x(2)(y(2)wx(2))=23(503)=30
  • 平均梯度 ∇ L batch1 = ( − 4 − 30 ) / 2 = − 17 \nabla L_{\text{batch1}} = (-4 -30) / 2 = -17 Lbatch1=(430)/2=17

然后,根据梯度下降的规则更新权重 w w w
w = w − α ∗ ∇ L batch1 = 0 − 0.02 ∗ − 17 = 0.34 (13) w = w - \alpha * \nabla L_{\text{batch1}} = 0 - 0.02 * -17 = 0.34 \tag{13} w=wαLbatch1=00.0217=0.34(13)

接着计算第二批的平均梯度:

  • 对于样本3,梯度 ∇ L ( 3 ) = − 2 x ( 3 ) ( y ( 3 ) − w x ( 3 ) ) = − 2 ∗ 2 ∗ ( 4 − 0.34 ∗ 2 ) = − 13.28 \nabla L(3) = -2x(3)(y(3) - wx(3)) = -2*2*(4 - 0.34*2) = -13.28 L(3)=2x(3)(y(3)wx(3))=22(40.342)=13.28
  • 对于样本4,梯度 ∇ L ( 4 ) = − 2 x ( 4 ) ( y ( 4 ) − w x ( 4 ) ) = − 2 ∗ 4 ∗ ( 7 − 0.34 ∗ 4 ) = − 45.12 \nabla L(4) = -2x(4)(y(4) - wx(4)) = -2*4*(7 - 0.34*4) = -45.12 L(4)=2x(4)(y(4)wx(4))=24(70.344)=45.12
  • 平均梯度 ∇ L batch2 = ( − 13.28 − 45.12 ) / 2 = − 29.2 \nabla L_{\text{batch2}} = (-13.28 -45.12) / 2 = -29.2 Lbatch2=(13.2845.12)/2=29.2

最后,再次根据梯度下降的规则更新权重 w w w
w = w − α ∗ ∇ L batch2 = 0.34 − 0.02 ∗ ( − 29.2 ) = 0.34 + 0.584 = 0.9328 (14) w = w - \alpha * \nabla L_{\text{batch2}} = 0.34 - 0.02*(-29.2) = 0.34 + 0.584 = 0.9328 \tag{14} w=wαLbatch2=0.340.02(29.2)=0.34+0.584=0.9328(14)

所以在学习率 α = 0.02 \alpha = 0.02 α=0.02 的情况下,一轮epoch迭代完成后,权重 w w w 的数值为 0.924 0.924 0.924

代码验证一下:

import numpy as np

# 定义样本数据
X = np.array([1, 3, 2, 4])
Y = np.array([2, 5, 4, 7])

# 初始化权重和学习率
w = 0
alpha = 0.02

# 批次划分
batch1 = {'X': X[:2], 'Y': Y[:2]}
batch2 = {'X': X[2:], 'Y': Y[2:]}

# 定义梯度和损失函数
def compute_gradient(x, y, w):
    return -2*x*(y - w*x)

# 执行梯度下降
for i, batch in enumerate([batch1, batch2], start=1):
    gradients = compute_gradient(batch['X'], batch['Y'], w)
    print(f"Batch {i} gradients: {gradients}")

    average_gradient = np.mean(gradients)
    print(f"Batch {i} average gradient: {average_gradient}")

    w = w - alpha * average_gradient
    print(f"Updated weight after batch {i}: {w}\n")

print(f"Final weight after one epoch: {w}")

image-20230615131313495

8.2 Mini-batch SGD总结

Mini-batch SGD结合了BGD和SGD的优点,对于大规模的数据集和硬件并行计算设施(如GPU),这种方法可以更充分地利用硬件的并行处理能力,因为可以同时对小批量的多个样本进行计算。由于每次迭代都基于更多的样本,小批量梯度下降比随机梯度下降更稳定,参数更新的波动会小一些,通常会有更平滑的收敛过程。

但同时呢,也存在一些使用上的问题:选择合适的批量大小需要一些经验和实验。如果批量太小,可能会导致更新过于嘈杂、不稳定和收敛慢。如果批量太大,可能会导致计算效率下降,尤其是当内存资源有限时。就像其他的梯度下降算法,小批量梯度下降也需要仔细地选择和调整学习率。使用固定的学习率可能会导致训练过程停滞不前,特别是在训练的后期。因此,通常需要使用一些策略来动态调整学习率,如学习率衰减。虽然小批量梯度下降的噪声有助于逃逸局部最优,但它仍然有可能陷入局部最优,特别是在处理高维、非凸优化问题时。

总的来说,小批量梯度下降结合了批量梯度下降的稳定性和随机梯度下降的效率,是一种在实践中常用的优化方法。

九、总结

本篇文章作为“逻辑回归深度解析”系列的最后一部分,对逻辑回归中的核心优化技术——梯度下降进行了全面深入的讨论。我们重点关注了梯度下降以及它的几种主要变体:批量梯度下降(BGD)、随机梯度下降(SGD)和小批量随机梯度下降(Mini-batch SGD)。

逻辑回归与梯度下降紧密相连。通过这一系列的文章,应当能够全面理解逻辑回归模型的构建过程、参数估计方法,以及如何利用梯度下降等优化方法求解模型参数。

最后,感谢您阅读这篇文章!如果您觉得有所收获,别忘了点赞、收藏并关注我,这是我持续创作的动力。您有任何问题或建议,都可以在评论区留言,我会尽力回答并接受您的反馈。如果您希望了解某个特定主题,也欢迎告诉我,我会乐于创作与之相关的文章。谢谢您的支持,期待与您共同成长!

期待与您在未来的学习中共同成长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

算法小陈

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

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

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

打赏作者

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

抵扣说明:

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

余额充值