1.无约束最优化问题 (Unconstrained Optimization Problem)
无约束最优化问题旨在从所有可能的解决方案中,找到一个在特定指标(如函数值)下达到最优(最小或最大)的方案。
从数学角度看,这通常是寻找一个函数在给定集合 S 上的极小值或极大值。
广义上的最优化包括数学规划、图与网络、组合最优化等多个领域。狭义上的最优化则特指数学规划。
“无约束最优化问题”中的“无约束”体现在对变量取值范围没有限制。
梯度下降法求解的许多机器学习模型(如线性回归、逻辑回归)的损失函数,其参数 θ 的取值可以是任意实数。我们没有任何先验知识或强制要求,比如“参数 \(θ_1\) 必须大于0”或“\(θ_2+θ_3\) 必须等于1”。这就是“无约束”的体现。
这使得问题简化,但同时也限制了它在某些需要考虑现实约束的场景中的应用。
梯度下降法就是用来解决这类无约束最优化问题的典型算法。
2.梯度下降算法
梯度下降法是一种通用的优化算法,它被广泛应用于机器学习和深度学习中,用于求解无约束最优化问题的最优解。
2.1梯度下降法的核心思想
为了找到模型参数 θ 使得损失函数(或成本函数 Cost)最小化,梯度下降法不是一次性找到最优解,而是通过多次迭代,逐步逼近最优解。每一次迭代都根据当前的“位置”信息调整下一步的方向和步长,直到收敛到最低点。
可以将损失函数曲线比喻为山谷,梯度下降法就像一个“下山”的过程。从山上的某个随机位置出发,每一步都朝着最陡峭的方向(梯度的反方向)走,最终到达山谷的最低点。
2.2为什么使用梯度下降法?
正规方程(Normal Equation)在损失函数是凸函数时可以得到解,即唯一的最优解。但很多机器学习模型的损失函数并非凸函数,可能存在多个极值点,无法通过正规方程确定唯一解。另外,正规方程求解涉及矩阵求逆运算,其时间复杂度为 \(O(n^3)\),其中 n 是特征维度。当特征数量过多时,计算量会非常巨大,导致运行时间过长,因此不适用于大规模数据集。
梯度下降法通过迭代方式逐步逼近最优解,没有矩阵求逆的计算负担,因此可以更好地应用于大规模数据集和高维特征空间。
2.3梯度下降法的公式
梯度下降的核心在于其参数更新公式:
或者使用更常见的符号:
3.学习率
学习率的设置是门一门学问,一般我们会把它设置成一个比较小的正整数,0.1、0.01、0.001、0.0001,都是常见的设定数值(然后根据情况调整)。一般情况下学习率在整体迭代过程中是不变,但是也可以设置成随着迭代次数增多学习率逐渐变小,因为越靠近山谷我们就可以步子迈小点,可以更精准的走入最低点,同时防止走过。还有一些深度学习的优化算法会自己控制调整学习率这个值
上图显示了梯度下降的两个主要挑战:
若随机初始化,算法从左侧起步,那么会收敛到一个局部最小值,而不是全局最小值;
若随机初始化,算法从右侧起步,那么需要经过很长时间才能越过Plateau(函数停滞带,梯度很小),如果停下得太早,则永远达不到全局最小值;
而线性回归的模型MSE损失函数恰好是个凸函数,凸函数保证了只有一个全局最小值,其次是个连续函数,斜率不会发生陡峭的变化, 因此即便是乱走,梯度下降都可以趋近全局最小值。
上图损失函数是非凸函数,梯度下降法是有可能落到局部最小值的,所以其实步长不能设置的太小太稳健,那样就很容易落入局部最优解,虽说局部最小值也没大问题, 因为模型只要是堪用的就好嘛,但是我们肯定还是尽量要奔着全局最优解去!
4.检验梯度下降是否收敛
可以绘制一个图,横轴是迭代次数,纵轴是损失函数的值。这种图也叫学习曲线。
或者设置一个阈值(比如\(10^{-3}\)),当损失函数的值小于这个阈值的时候,认为基本已经收敛了,
5.梯度下降步骤与代码模拟
- 初始化: 随机选取一个初始参数值 x(或者 \(\theta\)),并设置一个学习率 \(\eta\)。
- 计算梯度: 在当前参数值 x 处,计算损失函数 f(x) 的梯度(导数)\(\frac {\partial f(x)}{ \partial x}\)。
- 更新参数: 根据梯度下降公式更新参数:\(x \leftarrow x− \eta \cdot \frac {\partial f(x)}{ \partial x}\)。
- 判断收敛: 检查新的参数值与上一步的参数值之间的变化是否小于某个预设的阈值(精度)。如果满足,则停止迭代;否则,返回第2步继续执行。
代码模拟:通过梯度下降法求解函数\(f(x)=(x−3.5)^2−4.5x+10\) 的最小值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import numpy as np
import matplotlib.pyplot as plt
# 定义要优化的函数
def func(x):
return (x - 3.5)**2 - 4.5*x + 10
# 定义函数的导数(梯度)
def gradient(x):
return 2*(x - 3.5) - 4.5
def gradient_descent(func, gradient, initial_x, learning_rate, precision, max_iterations=1000):
"""
使用梯度下降法寻找函数的最小值。
Args:
func: 目标函数。
gradient: 目标函数的梯度(导数)。
initial_x: 初始参数值。
learning_rate: 学习率。
precision: 收敛精度。
max_iterations: 最大迭代次数,防止无限循环。
Returns:
包含每次迭代参数值的列表。
"""
history_x = [initial_x]
current_x = initial_x
print("开始梯度下降...")
for i in range(max_iterations):
last_x = current_x
# 核心更新公式
current_x = current_x - learning_rate * gradient(current_x)
history_x.append(current_x)
# 打印每次迭代的日志
print(f"迭代 {i+1}: x = {current_x:.6f}, f(x) = {func(current_x):.6f}")
# 判断收敛
if np.abs(current_x - last_x) < precision:
print(f"\n收敛于 {current_x:.6f},共迭代 {i+1} 次。")
break
else: # 当for循环正常结束(即达到最大迭代次数)时执行
print("\n达到最大迭代次数,可能未收敛。")
return history_x
# ---- 参数设置和运行 ----
learning_rate = 0.1
initial_x = np.random.randint(0, 12, size=1)[0]
precision = 1e-4
history_x = gradient_descent(func, gradient, initial_x, learning_rate, precision)
# ---- 可视化部分 ----
plt.rcParams['font.sans-serif'] = ['SimHei'] # 替换为支持中文的字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
plt.figure(figsize=(9, 6))
# 绘制函数曲线
plot_x = np.linspace(min(history_x) - 1, max(history_x) + 1, 400)
plot_y = func(plot_x)
plt.plot(plot_x, plot_y, color='green', label='函数曲线 $f(x)$')
# 绘制下降轨迹
history_x = np.array(history_x)
history_y = func(history_x)
plt.plot(history_x, history_y, 'ro--', markersize=5, label='下降轨迹')
plt.title('梯度下降过程演示', size=24, pad=15)
plt.xlabel('$x$')
plt.ylabel('$f(x)$')
plt.legend()
plt.grid(True)
plt.savefig('./梯度下降优化演示.png', dpi=200)
plt.show()
下面解释如何将梯度下降的通用公式应用于线性回归,推导出具体的参数更新规则。
6.线性回归中的梯度下降更新公式
首先,我们从线性回归的损失函数(Cost Function)或代价函数开始,它通常采用均方误差(MSE)的形式。
梯度下降的通用更新公式为:
我们的目标就是推导出 \(\frac{∂J(θ)}{∂θ_j}\) 的具体表达式
为了简化推导,我们只关注单个样本的损失函数\((h_θ(x)−y)^2\),并在求导后再将其推广到所有样本的总和。
求导对象为:
根据链式法则,我们对最外层的平方函数求导,然后乘以内层函数的导数。
然后将 \(h_θ(x)\) 展开为 \(∑_{i=0}^{n}θ_ix_i\),并对 \(θ_j\) 求偏导。
将上述推导结果代入到通用梯度下降公式中,并考虑所有 m 个样本,最终得到每个参数\(θ_j\) 的更新公式:
这个推导过程展示了如何从线性回归的损失函数出发,利用微积分推导出每个参数 θj 的梯度表达式。这个表达式告诉我们,每次更新 θj 的方向和大小,取决于当前预测值与真实值之间的误差 \((h_θ(x)−y)\),以及对应于该参数的特征值 \(x_j\)。
7.三种梯度下降算法
三种梯度下降算法核心区别在于每次更新参数时使用的数据量。
7.1批量梯度下降 (Batch Gradient Descent, BGD)
批量梯度下降是最原始的梯度下降形式。它的核心思想是在每次迭代时,都使用全部训练样本来计算梯度,然后更新模型参数。
上面写成\(X^T\)是为了做维度匹配。
- 优点:
- 方向准确: 每次更新的梯度方向都是基于整个数据集的平均值,因此能够更准确地代表整体数据的下降方向。
- 收敛稳定: 损失函数会平滑地朝着全局最优解(线性回归中)收敛。对于凸函数,BGD 一定能收敛到全局最优。
- 利用并行计算: 矩阵运算可以高效地在GPU上并行处理,加快计算速度。
- 缺点:
- 计算量大: 当训练样本数量 n 非常大时,每次迭代都需要遍历所有样本,计算开销巨大,训练速度很慢。
- 内存需求高: 需要将整个数据集加载到内存中进行计算,对于超大规模数据集不现实。
代码模拟:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import numpy as np
import matplotlib.pyplot as plt
# 设置 Matplotlib 字体以支持中文和负号
plt.rcParams['font.sans-serif'] = ['SimHei'] # 替换为支持中文的字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 1. 生成模拟数据
np.random.seed(0)
X = 2 * np.random.rand(100, 1) # 生成100个样本的x值
y = 4 + 3 * X + np.random.randn(100, 1) # y = 4 + 3x + 噪声
# 添加偏置项(bais),即 x0 = 1
X_b = np.c_[np.ones((100, 1)), X]
# 2. 定义损失函数和梯度
def compute_cost(X, y, theta):
m = len(y)
predictions = X.dot(theta)
cost = (1/(2*m)) * np.sum((predictions - y)**2)
return cost
def compute_gradient(X, y, theta):
m = len(y)
predictions = X.dot(theta)
errors = predictions - y
gradient = (1/m) * X.T.dot(errors)
return gradient
# 3. 实现批量梯度下降(BGD)算法
def batch_gradient_descent(X, y, learning_rate, n_iterations):
m = len(y)
theta = np.random.randn(2, 1) # 随机初始化参数 [theta0, theta1]
cost_history = []
theta_history = []
for i in range(n_iterations):
gradient = compute_gradient(X, y, theta)
theta = theta - learning_rate * gradient
cost_history.append(compute_cost(X, y, theta))
theta_history.append(theta.flatten()) # 记录参数历史,便于可视化
return theta, cost_history, np.array(theta_history)
# 4. 运行BGD算法
learning_rate = 0.1
n_iterations = 100
theta_final, cost_history, theta_history = batch_gradient_descent(X_b, y, learning_rate, n_iterations)
print("BGD 最终收敛的参数 (theta0, theta1):", theta_final.flatten())
print("BGD 最终的损失值:", cost_history[-1])
# 5. 可视化 BGD 的收敛轨迹
# 创建等高线图数据
theta0_range = np.linspace(-10, 10, 100)
theta1_range = np.linspace(-10, 10, 100)
T0, T1 = np.meshgrid(theta0_range, theta1_range)
J = np.zeros((theta0_range.size, theta1_range.size))
for i, t0 in enumerate(theta0_range):
for j, t1 in enumerate(theta1_range):
theta_temp = np.array([[t0], [t1]])
J[i, j] = compute_cost(X_b, y, theta_temp)
# 绘制等高线图和BGD轨迹
plt.figure(figsize=(10, 8))
plt.contour(T0, T1, J.T, levels=np.logspace(-2, 3, 20), cmap='viridis')
plt.plot(theta_history[:, 0], theta_history[:, 1], 'r-x', linewidth=2, label='BGD 训练轨迹')
plt.plot(theta_history[0, 0], theta_history[0, 1], 'ro', markersize=10, label='初始参数')
plt.plot(theta_final[0, 0], theta_final[1, 0], 'r*', markersize=12, label='最终参数')
plt.xlabel(r'$\theta_0$', fontsize=16)
plt.ylabel(r'$\theta_1$', fontsize=16)
plt.title('批量梯度下降(BGD)收敛轨迹', fontsize=20)
plt.legend(loc='best')
plt.grid(True)
plt.show()
从图中可以看出,曲线从一个点(初始参数)出发,平稳地、几乎是直线地向着中心(最优解)移动。虽然迭代次数相对较少,但每次迭代的计算量非常大。
7.2随机梯度下降 (Stochastic Gradient Descent, SGD)
与BGD相反,SGD的特点是在每次迭代时,只使用一个随机选取的样本来计算梯度,然后立即更新参数。
与BGD的公式相比,SGD没有求和符号,因为它只计算单个样本的梯度。
优点:
- 计算速度快: 每次迭代只处理一个样本,更新速度非常快,尤其是在处理大规模数据集时,每一轮的训练速度比BGD快得多。
- 在线学习: 适用于流式数据,可以进行在线学习,无需存储所有数据。
- 可能跳出局部最优: 在非凸函数中,梯度的随机性可能帮助算法跳出较浅的局部最优解,找到更好的全局最优解。
缺点:
- 方向不稳定: 梯度的计算基于单个样本,这并不能代表全局下降方向,导致更新路径非常震荡。
- 收敛不精确: 即使算法接近最优解,由于随机性,它也会在最优解周围持续震荡,难以精确收敛到最小值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import numpy as np
import matplotlib.pyplot as plt
# 设置 Matplotlib 字体以支持中文和负号
plt.rcParams['font.sans-serif'] = ['SimHei'] # 替换为支持中文的字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 1. 生成模拟数据(与BGD示例相同)
np.random.seed(0)
X = 2 * np.random.rand(100, 1) # 生成100个样本的x值
y = 4 + 3 * X + np.random.randn(100, 1) # y = 4 + 3x + 噪声
X_b = np.c_[np.ones((100, 1)), X]
# 2. 定义损失函数(与BGD示例相同)
def compute_cost(X, y, theta):
m = len(y)
predictions = X.dot(theta)
cost = (1/(2*m)) * np.sum((predictions - y)**2)
return cost
# 3. 实现随机梯度下降(SGD)算法
def stochastic_gradient_descent(X, y, learning_rate, n_iterations):
m = len(y)
theta = np.random.randn(2, 1) # 随机初始化参数 [theta0, theta1]
theta_history = []
# 迭代次数通常以“epoch”为单位,一个epoch是遍历一次所有样本
# 这里我们简化为总更新次数
for i in range(n_iterations):
# 随机选取一个样本的索引
random_index = np.random.randint(m)
xi = X[random_index:random_index+1]
yi = y[random_index:random_index+1]
# 计算单个样本的梯度(无需除以样本数 m)
errors = xi.dot(theta) - yi
gradient = xi.T.dot(errors)
# 更新参数
theta = theta - learning_rate * gradient
theta_history.append(theta.flatten()) # 记录参数历史
return theta, np.array(theta_history)
# 4. 运行SGD算法
learning_rate = 0.01 # SGD的学习率通常需要比BGD小
n_iterations = 1000 # SGD需要更多次迭代才能接近最优解
theta_final_sgd, theta_history_sgd = stochastic_gradient_descent(X_b, y, learning_rate, n_iterations)
print("SGD 最终收敛的参数 (theta0, theta1):", theta_final_sgd.flatten())
# 5. 可视化 SGD 的收敛轨迹
# 创建等高线图数据(与BGD示例相同)
theta0_range = np.linspace(-1, 8, 100)
theta1_range = np.linspace(-1, 8, 100)
T0, T1 = np.meshgrid(theta0_range, theta1_range)
J = np.zeros((theta0_range.size, theta1_range.size))
for i, t0 in enumerate(theta0_range):
for j, t1 in enumerate(theta1_range):
theta_temp = np.array([[t0], [t1]])
J[i, j] = compute_cost(X_b, y, theta_temp)
# 绘制等高线图和SGD轨迹
plt.figure(figsize=(10, 8))
plt.contour(T0, T1, J.T, levels=np.logspace(-2, 3, 20), cmap='viridis')
plt.plot(theta_history_sgd[:, 0], theta_history_sgd[:, 1], 'purple', alpha=0.5, label='SGD 训练轨迹')
plt.plot(theta_history_sgd[0, 0], theta_history_sgd[0, 1], 'bo', markersize=10, label='初始参数')
plt.plot(theta_final_sgd[0, 0], theta_final_sgd[1, 0], 'b*', markersize=12, label='最终参数')
plt.xlabel(r'$\theta_0$', fontsize=16)
plt.ylabel(r'$\theta_1$', fontsize=16)
plt.title('随机梯度下降(SGD)收敛轨迹', fontsize=20)
plt.legend(loc='best')
plt.grid(True)
plt.show()
收敛速度快但路径震荡,最终在最优解附近徘徊.
所以,SGD的学习率通常需要设置得更小,或者随着训练的进行逐渐减小(称为学习率衰减),以帮助算法更精确地收敛。
7.3小批量梯度下降 (Mini-Batch Gradient Descent, MBGD)
小批量梯度下降是BGD和SGD的折中方案。它在每次迭代时,不使用全部样本,也不只使用一个样本,而是使用一个预先设定大小的样本子集(称为 mini-batch)来计算梯度并更新参数。
优点:
- 速度与稳定的平衡: 结合了BGD和SGD的优点。相比BGD,它减少了每次迭代的计算量;相比SGD,它通过对小批量样本求平均,减少了梯度的随机性,使得收敛路径更加平滑和稳定。
- 高效并行化: 现代计算架构(如GPU)非常擅长处理批量数据,这使得MBGD能够高效利用硬件加速。
缺点:
- 超参数: 引入了一个新的超参数
batch_size
,需要根据具体问题进行调整。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import numpy as np
import matplotlib.pyplot as plt
# 设置 Matplotlib 字体以支持中文和负号
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 生成模拟数据(与之前示例相同)
np.random.seed(0)
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
X_b = np.c_[np.ones((100, 1)), X]
# 2. 定义损失函数(与之前示例相同)
def compute_cost(X, y, theta):
m = len(y)
predictions = X.dot(theta)
cost = (1/(2*m)) * np.sum((predictions - y)**2)
return cost
# 3. 实现三种梯度下降算法(与之前示例相同)
def batch_gradient_descent(X, y, learning_rate, n_iterations):
m = len(y)
theta = np.random.randn(2, 1)
theta_history = []
for _ in range(n_iterations):
gradient = (1/m) * X.T.dot(X.dot(theta) - y)
theta = theta - learning_rate * gradient
theta_history.append(theta.flatten())
return np.array(theta_history)
def stochastic_gradient_descent(X, y, learning_rate, n_iterations):
m = len(y)
theta = np.random.randn(2, 1)
theta_history = []
for _ in range(n_iterations):
random_index = np.random.randint(m)
xi = X[random_index:random_index+1]
yi = y[random_index:random_index+1]
gradient = xi.T.dot(xi.dot(theta) - yi)
theta = theta - learning_rate * gradient
theta_history.append(theta.flatten())
return np.array(theta_history)
def mini_batch_gradient_descent(X, y, learning_rate, n_iterations, batch_size):
m = len(y)
theta = np.random.randn(2, 1)
theta_history = []
for i in range(n_iterations):
shuffled_indices = np.random.permutation(m)
X_shuffled = X[shuffled_indices]
y_shuffled = y[shuffled_indices]
for j in range(0, m, batch_size):
X_batch = X_shuffled[j:j+batch_size]
y_batch = y_shuffled[j:j+batch_size]
errors = X_batch.dot(theta) - y_batch
gradient = (1/batch_size) * X_batch.T.dot(errors)
theta = theta - learning_rate * gradient
theta_history.append(theta.flatten())
return theta, np.array(theta_history)
# 4. 运行三种算法
# BGD参数
theta_history_bgd = batch_gradient_descent(X_b, y, learning_rate=0.1, n_iterations=100)
# SGD参数
theta_history_sgd = stochastic_gradient_descent(X_b, y, learning_rate=0.01, n_iterations=1000)
# MBGD参数,这里进行关键调整!
learning_rate_mbgd = 0.01 # 保持不变,但增加迭代次数
n_iterations_mbgd = 50 # 将epoch数从10增加到50
batch_size = 10
theta_final_mbgd, theta_history_mbgd = mini_batch_gradient_descent(
X_b, y, learning_rate_mbgd, n_iterations_mbgd, batch_size
)
print(f"MBGD (batch_size={batch_size}, epochs={n_iterations_mbgd}) 最终收敛的参数 (theta0, theta1):", theta_final_mbgd.flatten())
# 5. 可视化
# 创建等高线图数据
theta0_range = np.linspace(-1, 8, 100)
theta1_range = np.linspace(-1, 8, 100)
T0, T1 = np.meshgrid(theta0_range, theta1_range)
J = np.zeros((theta0_range.size, theta1_range.size))
for i, t0 in enumerate(theta0_range):
for j, t1 in enumerate(theta1_range):
theta_temp = np.array([[t0], [t1]])
J[i, j] = compute_cost(X_b, y, theta_temp)
# 绘制等高线图和三种算法的轨迹
plt.figure(figsize=(10, 8))
plt.contour(T0, T1, J.T, levels=np.logspace(-2, 3, 20), cmap='viridis')
# 绘制BGD轨迹(蓝色)
plt.plot(theta_history_bgd[:, 0], theta_history_bgd[:, 1], 'b-', label='BGD')
# 绘制SGD轨迹(紫色)
plt.plot(theta_history_sgd[:, 0], theta_history_sgd[:, 1], 'm-', alpha=0.5, label='SGD')
# 绘制MBGD轨迹(绿色)
plt.plot(theta_history_mbgd[:, 0], theta_history_mbgd[:, 1], 'g-', label=f'MBGD (batch={batch_size})')
# 标记最终参数点
plt.plot(theta_history_bgd[-1, 0], theta_history_bgd[-1, 1], 'b*', markersize=12, label='BGD最终参数')
plt.plot(theta_history_sgd[-1, 0], theta_history_sgd[-1, 1], 'm*', markersize=12, label='SGD最终参数')
plt.plot(theta_final_mbgd[0, 0], theta_final_mbgd[1, 0], 'g*', markersize=12, label='MBGD最终参数')
plt.xlabel(r'$\theta_0$', fontsize=16)
plt.ylabel(r'$\theta_1$', fontsize=16)
plt.title('三种梯度下降算法收敛轨迹对比 (优化后)', fontsize=20)
plt.legend(loc='best')
plt.grid(True)
plt.show()
MBGD的随机性主要体现在每次迭代时,从整个数据集中随机抽取一个小批量(mini-batch)样本。
8.梯度下降优化方法
8.1学习率的选择
学习率过小:参数更新的步幅很小,算法需要很多次迭代才能接近最优解,导致收敛速度非常慢。
学习率过大:参数更新的步幅过大,导致算法在最优解附近来回“跳跃”,甚至可能跳过最优解,无法收敛。这被称为震荡收敛或发散。
8.2不同学习率衰减的方法
选择一个合适的学习率很困难,通常需要进行超参数搜索。一个更好的方法是学习率衰减(Learning Rate Decay),即在训练过程中动态地减小学习率。在训练初期使用较大的学习率,以保证收敛速度。当接近最优解时,逐渐减小学习率,以避免震荡并更精确地收敛。这可以确保在训练初期快速收敛,在后期则以更小的步幅精细调整,避免震荡。
对于非凸函数,随机梯度下降(SGD)或其变体(如Adam)的随机性可以在一定程度上帮助算法跳出浅的局部最优解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import numpy as np
import matplotlib.pyplot as plt
# 设置 Matplotlib 字体以支持中文和负号
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 初始学习率和总迭代次数
initial_lr = 1.0
total_iterations = 100
t = np.arange(total_iterations)
# 1. 分段衰减 (Piecewise Constant Decay)
piecewise_lr = np.ones(total_iterations)
piecewise_lr[20:] = 0.5
piecewise_lr[40:] = 0.25
piecewise_lr[60:] = 0.125
piecewise_lr[80:] = 0.05
# 2. 逆时衰减 (Inverse Time Decay)
beta_inverse = 0.01 # 衰减率
inverse_lr = initial_lr / (1 + beta_inverse * t)
# 3. 指数衰减 (Exponential Decay)
beta_exp = 0.96 # 衰减率
exponential_lr = initial_lr * (beta_exp ** t)
# 4. 自然指数衰减 (Natural Exponential Decay)
beta_nat_exp = 0.04
natural_exp_lr = initial_lr * np.exp(-beta_nat_exp * t)
# 5. 余弦衰减 (Cosine Decay)
# 余弦衰减的公式: η_t = η_0 / 2 * (1 + cos(pi * t / T))
cosine_lr = initial_lr / 2 * (1 + np.cos(np.pi * t / total_iterations))
# 绘制不同学习率衰减曲线
plt.figure(figsize=(10, 6))
plt.plot(t, piecewise_lr, 'k-', linewidth=2, label='分段衰减')
plt.plot(t, inverse_lr, 'g-.', linewidth=2, label='逆时衰减 (β = 0.01)')
plt.plot(t, exponential_lr, 'r--', linewidth=2, label='指数衰减 (β = 0.96)')
plt.plot(t, natural_exp_lr, 'b:', linewidth=2, label='自然指数衰减 (β = 0.04)')
plt.plot(t, cosine_lr, 'y-.', linewidth=2, label='余弦衰减')
plt.xlabel('迭代次数', fontsize=14)
plt.ylabel('学习率', fontsize=14)
plt.title('不同学习率衰减方法的比较', fontsize=16)
plt.legend(loc='upper right')
plt.grid(True)
plt.ylim(0, 1.1)
plt.show()
8.3不同的参数使用不同的学习率
如果数据特征是稀疏的,或者每个特征的统计特征和空间分布不同,那么为每个参数(对应于每个特征)使用相同的学习率可能不是最优的。
为了解决这个问题,出现了许多自适应学习率算法,如 Adagrad、RMSprop、Adam 等。它们会根据每个参数的历史梯度信息,动态地调整该参数的学习率。例如,对于更新较少的参数,会分配一个较大的学习率来加速收敛;对于更新频繁的参数,则分配一个较小的学习率来避免震荡。
8.4非凸目标函数
对于非凸目标函数,容易陷入那些次优的局部极值点中,如在神经网路中。那么如何避免呢?
简单的问题,一般使用随机梯度下降即可解决。在深度学习里,对梯度下降进行了很多改进,比如:自适应梯度下降。
8.5Epoch 和 Batch
Epoch: 循环一次意味着将所有训练数据学习一遍。
Batch: 批次是指将整个训练集分成若干个子集,每次只学习一个子集。
关系: 一个epoch包含若干个batch的训练。在机器学习训练中,我们通常需要训练多个epoch,在每个epoch中又会进行若干个batch的训练。
9.代码封装实现梯度下降
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import numpy as np
def run_linear_regression_gd(n_features, gd_type='BGD', epochs=10000, learning_rate=0.01, batch_size=16):
"""
一个通用的线性回归梯度下降实现。
Args:
n_features (int): 特征数量。
gd_type (str): 梯度下降类型 ('BGD', 'SGD', 'MBGD')。
epochs (int): 训练轮次。
learning_rate (float): 固定学习率。
batch_size (int): 小批量大小 (仅对MBGD有效)。
"""
print(f"--- 运行 {gd_type},特征数={n_features} ---")
# 1. 创建数据集X,y
n_samples = 100
X = np.random.rand(n_samples, n_features)
# 真实参数
w_true = np.random.randint(1, 10, size=(n_features, 1))
b_true = np.random.randint(1, 10, size=(1, 1))
y = X.dot(w_true) + b_true + np.random.randn(n_samples, 1)
# 2. 使用偏置项x_0 = 1,更新X
X_with_bias = np.c_[X, np.ones((n_samples, 1))]
# 5. 初始化 W0...Wn...
theta = np.random.randn(n_features + 1, 1)
# 为了简化,我们使用固定学习率。如果需要衰减,可以修改这里
# def learning_rate_schedule(t):
# return t0/(t+t1)
#
# 或者使用更简单的固定学习率
if gd_type == 'BGD':
for i in range(epochs):
# BGD使用所有样本
predictions = X_with_bias.dot(theta)
errors = predictions - y
gradient = X_with_bias.T.dot(errors) / n_samples
theta = theta - learning_rate * gradient
elif gd_type == 'SGD':
for epoch in range(epochs):
# SGD每次使用一个随机样本
shuffled_indices = np.random.permutation(n_samples)
for i in shuffled_indices:
xi = X_with_bias[[i]]
yi = y[[i]]
predictions = xi.dot(theta)
errors = predictions - yi
gradient = xi.T.dot(errors)
theta = theta - learning_rate * gradient
elif gd_type == 'MBGD':
num_batches = n_samples // batch_size
for epoch in range(epochs):
# MBGD使用一个随机的小批量
shuffled_indices = np.random.permutation(n_samples)
X_shuffled = X_with_bias[shuffled_indices]
y_shuffled = y[shuffled_indices]
for i in range(num_batches):
start_idx = i * batch_size
end_idx = start_idx + batch_size
X_batch = X_shuffled[start_idx:end_idx]
y_batch = y_shuffled[start_idx:end_idx]
predictions = X_batch.dot(theta)
errors = predictions - y_batch
gradient = X_batch.T.dot(errors) / batch_size
theta = theta - learning_rate * gradient
print('真实斜率是:', w_true.T, '截距是:', b_true[0, 0])
print('梯度下降计算的参数是:', theta.T)
print("--- 运行结束 ---\n")
# 调用函数进行验证
run_linear_regression_gd(n_features=1, gd_type='BGD')
run_linear_regression_gd(n_features=3, gd_type='BGD')
run_linear_regression_gd(n_features=1, gd_type='SGD', epochs=100) # SGD通常需要更多epochs,这里为了演示简化
run_linear_regression_gd(n_features=5, gd_type='SGD', epochs=100)
run_linear_regression_gd(n_features=1, gd_type='MBGD', epochs=100)
run_linear_regression_gd(n_features=3, gd_type='MBGD', epochs=100)