1.代码实战:线性回归
1.导包与数据读取:
1
2
3
4
5
6
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split # 分割数据集,分成训练集和测试集
from sklearn.metrics import mean_squared_error # 计算MSE
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
def load_and_clean_data(file_path):
"""
从指定文件中读取数据,清理字符串格式,并返回 NumPy 数组。
数据格式预期为:[[x], [y]]
"""
cleaned_data_list = []
line_number = 0 # 用于跟踪行号
try:
with open(file_path, 'r') as f:
for line in f:
line_number += 1
# 1. 清理行首和行尾的空白字符
line_stripped = line.strip()
# 严格跳过空行或仅包含空白字符的行
if not line_stripped:
# 如果需要调试,可以取消注释下面这行
# print(f"跳过文件中的空行或空白行 (行号: {line_number})")
continue
# 2. 移除所有方括号 '[]' 和空格,只留下数字和逗号
# 示例: "[[5.34...], [30.91...]]" -> "5.34...,30.91..."
clean_value_str = line_stripped.replace('[', '').replace(']', '').replace(' ', '')
# 3. 按逗号分隔,得到 X 和 y 的字符串
x_str, y_str = clean_value_str.split(',')
# 4. 转换为浮点数并存储
x_val = float(x_str)
y_val = float(y_str)
cleaned_data_list.append([x_val, y_val])
except FileNotFoundError:
print(f"❌ 错误:未找到文件 {file_path}。请检查文件路径。")
return None, None
except ValueError as e:
print(f"❌ 错误:数据格式不正确,无法转换为浮点数。请检查数据行 (行号: {line_number}): {line_stripped}")
print(f"具体错误信息: {e}")
return None, None
except Exception as e:
print(f"❌ 发生未知错误: {e}")
return None, None
# 5. 转换为最终的 NumPy 数组
cleaned_data_np = np.array(cleaned_data_list, dtype=np.float64)
# 6. 分离特征 (X) 和目标值 (y)
# X 必须是二维数组 (n, 1)
X = cleaned_data_np[:, 0].reshape(-1, 1)
y = cleaned_data_np[:, 1] # y 是一维数组 (n,)
return X, y
# ----------------- 修改后的使用示例 -----------------
file_name = 'train_data' # 建议使用 .txt 扩展名,以明确文件类型
# **注意:** 请确保您的数据已保存到指定的文件中
X, y = load_and_clean_data(file_name)
if X is not None and y is not None:
print("--- ✅ 数据读取成功 ---")
print(f"特征 X 的形状: {X.shape}")
print(f"目标 y 的形状: {y.shape}")
print("X 的前 5 个样本:\n", X[:5])
print("y 的前 5 个样本:\n", y[:5])
print("\n数据已清理并准备好进行模型训练(未分割数据集)。")
# **********************************************
# 移除的分割代码原本在此处,现在直接对 X 和 y 进行操作
# **********************************************
# 现在您可以直接使用整个 X 和 y 进行模型训练(例如,训练集就是整个数据集)
# from sklearn.linear_model import LinearRegression
# model = LinearRegression()
# model.fit(X, y) # 直接使用所有数据进行训练

2.分割数据集,分为测试集和训练集,比例2:8
1
2
3
4
5
6
7
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42 # 设置随机种子以确保结果可重现
)
print(f"训练集大小 (X_train): {X_train.shape}")
print(f"测试集大小 (X_test): {X_test.shape}")

3.模型训练:
1
2
3
4
5
6
7
model = LinearRegression()
model.fit(X_train, y_train)
# 打印模型参数 (截距和斜率)
# 对于一元线性回归: y = coef * X + intercept
print("\n--- 模型训练结果 ---")
print(f"模型截距 (Intercept): {model.intercept_:.4f}")
print(f"模型系数 (Coefficient/Slope): {model.coef_[0]:.4f}")

4.模型预测,计算MSE
1
2
3
4
5
6
7
8
9
10
11
12
# 1. 对训练集进行预测
y_train_pred = model.predict(X_train)
# 2. 对测试集进行预测
y_test_pred = model.predict(X_test)
# 计算 MSE
mse_train = mean_squared_error(y_train, y_train_pred)
mse_test = mean_squared_error(y_test, y_test_pred)
print("\n--- 模型评估 (MSE) ---")
print(f"训练集 MSE: {mse_train:.4f}")
print(f"测试集 MSE: {mse_test:.4f}")

5.绘图
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
# -----------------------------------------------------------
# 步骤 3: 绘图
# -----------------------------------------------------------
# Matplotlib 绘图设置
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False # 解决负号显示的问题
# **关键修正步骤:排序**
# 1. 对完整的 X 数组进行排序,以确保拟合线是一条平滑的直线
# np.argsort 返回排序后的索引
sort_index = np.argsort(X.flatten()) # 注意:X是二维数组,需要先展平
X_sorted = X[sort_index]
# 2. 对模型在 X 上的预测值也使用相同的索引进行排序
y_full_pred_sorted = model.predict(X_sorted)
# --- 绘图要求 1: 所有点和预测线一起展示 (修正) ---
plt.figure(figsize=(10, 6))
# 绘制所有原始数据点 (训练集和测试集)
plt.scatter(X_train, y_train, color='blue', label='训练集数据点', alpha=0.6)
plt.scatter(X_test, y_test, color='green', label='测试集数据点', alpha=0.8)
# 绘制预测线 (拟合线) - 使用排序后的预测数据
# 这将确保红色线是一条平滑的直线
plt.plot(X_sorted, y_full_pred_sorted, color='red', linestyle='-',
label='线性回归拟合线 (y = {:.4f}*X + {:.4f})'.format(model.coef_[0], model.intercept_),
linewidth=2)
plt.title('所有数据点与线性回归拟合线 (总览)')
plt.xlabel('特征 X')
plt.ylabel('目标 Y')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()
# --- 绘图要求 2 & 3: 训练集和测试集分开展示(无需排序,因为点本身就是散点图) ---
fig, axes = plt.subplots(1, 2, figsize=(15, 6), sharey=True)
# 2. 训练集 (Training Set) - 绘制训练集数据和预测线
axes[0].scatter(X_train, y_train, color='blue', label='训练集数据点', alpha=0.6)
# **注意:** 这里的 X_train 依然可能未排序,因此也需要排序以防折叠
sort_index_train = np.argsort(X_train.flatten())
X_train_sorted = X_train[sort_index_train]
y_train_pred_sorted = model.predict(X_train_sorted)
axes[0].plot(X_train_sorted, y_train_pred_sorted, color='red', linestyle='-', label='模型预测线', linewidth=2)
axes[0].set_title('训练集数据与模型拟合')
axes[0].set_xlabel('特征 X')
axes[0].set_ylabel('目标 Y')
axes[0].legend()
axes[0].grid(True, linestyle='--', alpha=0.6)
# 3. 测试集 (Testing Set) - 绘制测试集数据和预测线
axes[1].scatter(X_test, y_test, color='green', label='测试集数据点', alpha=0.8)
# **注意:** 这里的 X_test 也需要排序以防折叠
sort_index_test = np.argsort(X_test.flatten())
X_test_sorted = X_test[sort_index_test]
y_test_pred_sorted = model.predict(X_test_sorted)
axes[1].plot(X_test_sorted, y_test_pred_sorted, color='red', linestyle='-', label='模型预测线', linewidth=2)
axes[1].set_title('测试集数据与模型预测')
axes[1].set_xlabel('特征 X')
axes[1].legend()
axes[1].grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()


2.MSE与w,b
画图理解
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
# 使用 sklearn 找到最优 w* 和 b*
optimal_model = LinearRegression().fit(X, y)
w_star = optimal_model.coef_[0]
b_star = optimal_model.intercept_
print(f"最优参数 w*: {w_star:.4f}, b*: {b_star:.4f}")
# 定义 MSE 计算函数
def calculate_mse(w, b, X_data, y_data):
"""计算给定 w 和 b 下的均方误差"""
# 预测值: y_pred = w * X + b
y_pred = w * X_data + b
# MSE: mean((y - y_pred)^2)
return np.mean((y_data - y_pred.flatten())**2)
# -----------------------------------------------------------
# 步骤 2: 绘制 MSE vs W (固定 b = b*)
# -----------------------------------------------------------
# 选择 w 的变化范围 (以最优 w* 为中心)
w_values = np.linspace(w_star - 5, w_star + 5, 100)
# 固定 b 为最优值
b_fixed = b_star
# 计算 MSE 数组
mse_w = [calculate_mse(w, b_fixed, X, y) for w in w_values]
plt.figure(figsize=(8, 6))
plt.plot(w_values, mse_w, color='blue', linewidth=3)
plt.scatter(w_star, calculate_mse(w_star, b_star, X, y), color='red', s=100, label=f'最小MSE点 (w*={w_star:.2f})', zorder=5)
plt.title(f'MSE 随 W (斜率) 的变化 (固定 b = {b_fixed:.2f})')
plt.xlabel('权重 W (斜率)')
plt.ylabel('均方误差 MSE')
plt.grid(True, linestyle='--', alpha=0.6)
plt.legend()
plt.show()
# -----------------------------------------------------------
# 步骤 3: 绘制 MSE vs B (固定 w = w*)
# -----------------------------------------------------------
# 选择 b 的变化范围 (以最优 b* 为中心)
b_values = np.linspace(b_star - 10, b_star + 10, 100)
# 固定 w 为最优值
w_fixed = w_star
# 计算 MSE 数组
mse_b = [calculate_mse(w_fixed, b, X, y) for b in b_values]
plt.figure(figsize=(8, 6))
plt.plot(b_values, mse_b, color='green', linewidth=3)
plt.scatter(b_star, calculate_mse(w_star, b_star, X, y), color='red', s=100, label=f'最小MSE点 (b*={b_star:.2f})', zorder=5)
plt.title(f'MSE 随 B (截距) 的变化 (固定 w = {w_fixed:.2f})')
plt.xlabel('偏置 B (截距)')
plt.ylabel('均方误差 MSE')
plt.grid(True, linestyle='--', alpha=0.6)
plt.legend()
plt.show()


如果以全量数据进行预测,可以从上图看到MSE与w,b的关系。要使得MSE最小,一定存在这样的w和b
这两张图都展示了 MSE 与单个参数(w或 b)之间的关系是 凸的二次函数(抛物线)。曲线只有一个最低点。这个最低点对应的w值(在第一张图上)或b值(在第二张图上)就是使 MSE 最小化的最优参数值。这是梯度下降等优化算法能够有效工作的基础,因为它们总是沿着曲线下降,最终能够收敛到全局最低点。
数学分析(梯度分析)
从数学分析的角度来看,线性回归的均方误差(MSE)损失函数是一个完美的凸函数(Convex Function)。
对于一元线性回归模型 \(y = wX + b\),给定数据集 \(\{(X_i, y_i)\}_{i=1}^n\),其均方误差(MSE)损失函数 \(J(w, b)\) 定义为:
\[J(w, b) = \frac{1}{n} \sum_{i=1}^{n} (y_i - (wX_i + b))^2\]为了证明 \(J(w, b)\) 是一个凸函数,我们可以使用二阶导数判别法(即 Hessian 矩阵)。
【A. 凸函数定义】
如果一个函数 \(f(\mathbf{x})\) 的 Hessian 矩阵 \(\mathbf{H}\) 在其定义域上是 半正定(Positive Semi-definite) 的,那么\(f(\mathbf{x})\) 是一个凸函数。
【B. 计算 Hessian 矩阵】
\(J(w, b)\) 是关于参数向量 \(\mathbf{\theta} = \begin{pmatrix} w \\ b \end{pmatrix}\) 的函数。
1.计算一阶偏导数(梯度 \(\nabla J\)):
\[\frac{\partial J}{\partial w} = \frac{2}{n} \sum_{i=1}^{n} (wX_i + b - y_i) X_i\] \[\frac{\partial J}{\partial b} = \frac{2}{n} \sum_{i=1}^{n} (wX_i + b - y_i)\]2.计算二阶偏导数:
\[\frac{\partial^2 J}{\partial w^2} = \frac{\partial}{\partial w} \left[ \frac{2}{n} \sum_{i=1}^{n} (wX_i + b - y_i) X_i \right] = \frac{2}{n} \sum_{i=1}^{n} X_i^2\] \[\frac{\partial^2 J}{\partial b^2} = \frac{\partial}{\partial b} \left[ \frac{2}{n} \sum_{i=1}^{n} (wX_i + b - y_i) \right] = \frac{2}{n} \sum_{i=1}^{n} 1 = 2\] \[\frac{\partial^2 J}{\partial w \partial b} = \frac{\partial}{\partial b} \left[ \frac{2}{n} \sum_{i=1}^{n} (wX_i + b - y_i) X_i \right] = \frac{2}{n} \sum_{i=1}^{n} X_i\]3.构建 Hessian 矩阵 \(\mathbf{H}\):
\[\mathbf{H} = \begin{pmatrix} \frac{\partial^2 J}{\partial w^2} & \frac{\partial^2 J}{\partial w \partial b} \\ \frac{\partial^2 J}{\partial w \partial b} & \frac{\partial^2 J}{\partial b^2} \end{pmatrix} = \frac{2}{n} \begin{pmatrix} \sum_{i=1}^{n} X_i^2 & \sum_{i=1}^{n} X_i \\ \sum_{i=1}^{n} X_i & n \end{pmatrix}\]【C. 判别 Hessian 矩阵的半正定性】
一个 \(2 \times 2\) 矩阵是半正定的,当且仅当它的主子式(Principal Minors)是非负的:
1.一阶主子式(对角线元素):
- $H_{11} = \frac{2}{n} \sum X_i^2$. 由于 $X_i^2 \ge 0$,因此 $H_{11} \ge 0$.
- $H_{22} = 2 > 0$.
2.二阶主子式(行列式 \(\det(\mathbf{H})\)):
\[\det(\mathbf{H}) = \left(\frac{2}{n}\right)^2 \left[ n \sum_{i=1}^{n} X_i^2 - \left( \sum_{i=1}^{n} X_i \right)^2 \right]\]根据 柯西-施瓦茨不等式(Cauchy–Schwarz inequality) 的离散形式:
\[\left( \sum_{i=1}^{n} a_i^2 \right) \left( \sum_{i=1}^{n} b_i^2 \right) \ge \left( \sum_{i=1}^{n} a_i b_i \right)^2\]在我们的行列式表达式中,令 $a_i = X_i$ 且 $b_i = 1$:
\[\left( \sum_{i=1}^{n} X_i^2 \right) \left( \sum_{i=1}^{n} 1^2 \right) \ge \left( \sum_{i=1}^{n} X_i \cdot 1 \right)^2\] \[n \sum_{i=1}^{n} X_i^2 \ge \left( \sum_{i=1}^{n} X_i \right)^2\]因此,行列式内部的项 \([ n \sum X_i^2 - (\sum X_i)^2 ] \ge 0\)。
这意味着 \(\det(\mathbf{H}) \ge 0\)。
由于所有主子式都非负,Hessian 矩阵 \(\mathbf{H}\) 是半正定的,故 $J(w, b)$ 是一个凸函数。
为什么只有全局最小值,而不是局部最小值?
任何凸函数 \(f(\mathbf{x})\) 的任何局部最小值同时也是全局最小值。
如果一个凸函数是严格凸的(即 \(\det(\mathbf{H}) > 0\)),那么它至多只有一个全局最小值。
在大多数实际数据场景中(非病态数据):只有当所有 \(X_i\) 都相同时(即特征没有变化),行列式 \(\det(\mathbf{H})\) 才会等于零。在这种情况下,MSE 损失函数会沿着某个方向形成一个“平底谷”(即多个参数组合都能达到最小 MSE),此时存在一个最小值的集合。
只要数据集中的 \(X_i\) 不全相同(即 \(\sum X_i^2 \ne \frac{1}{n}(\sum X_i)^2\)),那么 \(\det(\mathbf{H}) > 0\),函数就是严格凸的。
对于一个严格凸函数,它只能有一个局部最小值,而这个局部最小值就是唯一的全局最小值。
这意味着,无论从 \(w\) 和 \(b\) 的哪个初始值开始(比如使用梯度下降法),最终都将收敛到同一个使 MSE 最小化的参数组合。
线性回归的 MSE 损失函数是一个凸函数,其碗状的损失曲面保证了它没有山峰或局部凹陷(局部最小值),因此任何优化算法找到的最低点都是全局最优解。
参数 \(w\) 和 \(b\) 如何更新以逼近这个最小值?
梯度下降法的核心思想是:沿着损失函数曲面当前位置坡度最陡峭的下降方向(即负梯度方向)迈出一步。
梯度 \(\nabla J(w, b)\) 是一个向量,包含了损失函数 \(J\) 对所有参数的一阶偏导数:
\[\nabla J(w, b) = \begin{pmatrix} \frac{\partial J}{\partial w} \\ \frac{\partial J}{\partial b} \end{pmatrix}\]其中,偏导数(即梯度分量)上面计算过:
\[\frac{\partial J}{\partial w} = \frac{2}{n} \sum_{i=1}^{n} (wX_i + b - y_i) X_i\] \[\frac{\partial J}{\partial b} = \frac{2}{n} \sum_{i=1}^{n} (wX_i + b - y_i)\]梯度下降法的参数更新迭代公式如下:
\[\begin{aligned} w_{\text{new}} &= w_{\text{old}} - \alpha \frac{\partial J}{\partial w} \\ b_{\text{new}} &= b_{\text{old}} - \alpha \frac{\partial J}{\partial b} \end{aligned}\]其中,\(\alpha > 0\) 是学习率(Learning Rate),它控制了每一步更新的幅度。
我们以参数 \(w\) 的更新为例进行分析,参数 \(b\) 的分析同理。
我们的目标是让 \(J(w, b)\) 减小,即找到曲线的最低点。
情况一:\(\frac{\partial J}{\partial w} > 0\) (当前点在最低点右侧)
损失函数曲线在当前点 \(w\) 处的斜率为正(向上倾斜)。这意味着当前 \(w\)的值大于最优值 \(w^*\)。
\[\Delta w = - \alpha \underbrace{\frac{\partial J}{\partial w}}_{> 0}\]由于 \(\alpha > 0\),所以 \(\Delta w\) 必须小于 0。参数 \(w\) 将减小 (\(w_{\text{new}} < w_{\text{old}}\))。\(w\) 向左(较小的值)移动,即向全局最小值 \(w^*\)靠近。
情况二:\(\frac{\partial J}{\partial w} < 0\) (当前点在最低点左侧)
损失函数曲线在当前点 \(w\) 处的斜率为负(向下倾斜)。这意味着当前 \(w\)的值小于最优值 \(w^*\)。
\[\Delta w = - \alpha \underbrace{\frac{\partial J}{\partial w}}_{< 0}\]由于 \(\alpha > 0\),所以 \(\Delta w\) 必须大于 0。参数 \(w\) 将增大 (\(w_{\text{new}} > w_{\text{old}}\))。\(w\) 向右(较大的值)移动,即向全局最小值 \(w^*\) 靠近。
所以,无论是梯度大于 0 还是小于 0,参数更新公式 \(w_{\text{new}} = w_{\text{old}} - \alpha \frac{\partial J}{\partial w}\) 都保证了参数总是在向坡度下降最快的方向移动,从而不断减小损失 \(J(w, b)\)。
由于 MSE 损失函数是一个凸函数,这种持续下降的行为最终将不可避免地导向唯一的全局最小值。梯度下降法之所以高效且强大,正是因为它能够利用损失函数的凸性,保证收敛到最优解。
学习率的选择
在应用梯度下降法时至关重要的超参数:学习率(Learning Rate),通常用 \(\alpha\) 表示。
学习率 $\alpha$ 控制了参数在每次迭代中沿着梯度方向移动的步长。在更新公式中体现为:
\[\theta_{\text{new}} = \theta_{\text{old}} - \alpha \cdot \nabla J(\theta_{\text{old}})\]选择 \(\alpha\) 时,需要找到一个平衡点:既要足够大以快速收敛,又要足够小以避免震荡和发散。
| 情况 | 学习率 (α) 特征 | 训练效果 | 数学/几何分析 | 结论 |
|---|---|---|---|---|
| 过大 | Large \(\alpha\) | 发散 (Divergence) 或剧烈震荡 | 步长太大,每次更新都会跳过损失函数的最低点,甚至跳到更高、更陡峭的位置,导致损失函数 \(J(\theta)\) 不断增大。 | ❌ 模型无法收敛 |
| 过小 | Small \(\alpha\) | 收敛缓慢 (Slow Convergence) | 步长太小,需要极多的迭代次数才能到达最低点。模型训练时间过长,计算资源消耗大。 | ⚠️ 训练效率低下 |
| 适中 | Optimal \(\alpha\) | 平稳且高效收敛 | 步长恰到好处,每次更新都能显著减小损失 \(J(\theta)\),并平稳地向全局最小值收敛。 | ✅ 最优选择 |
在实际应用中,没有一个“万能”的学习率,最佳的学习率取决于具体的数据集和损失函数曲面的形状。
可以从一个合理范围的数值开始尝试:常用起始值: \(0.1, 0.01, 0.001, 0.0001\)。
学习率退火 (Learning Rate Scheduling / Decay),在训练过程中动态调整学习率是一种高级策略:在训练初期,使用较大的学习率快速接近最优区域;在训练后期,逐渐减小学习率(“退火”)以实现更精细的搜索,避免在最低点附近来回震荡,确保稳定收敛。每隔一定数量的 Epoch(轮次)或迭代次数,将 $\alpha$ 乘以一个固定因子(如 0.5)。或者指数衰减。
解析解的存在性与计算可行性
在线性回归中,直接求解最优参数 \(w\) 和 \(b\) 的方法是利用正规方程(Normal Equation),这是一种解析解法。
对于线性回归的 MSE 损失函数,最优参数 \(\mathbf{\theta}^*\) 的正规方程解为:
\[\mathbf{\theta}^* = (\mathbf{X}^{\text{T}}\mathbf{X})^{-1}\mathbf{X}^{\text{T}}\mathbf{y}\]我们之前已经证明,MSE 损失函数是一个凸函数,其全局最小值是唯一(或构成一个平面)。这个解析解正是这个全局最小值。所以,解是存在的。
数学上不可能无解,但是指在计算上不可行或效率极低(\(n\) 过大导致“解不出”的计算瓶颈)
矩阵求逆的计算量与特征维度的三次方成正比。
对于数百万、数十亿样本(\(n\) 很大)或数万特征(\(d\) 很大)的现代大数据集,正规方程法因为 \(O(d^3)\) 的复杂度,几乎无法在合理时间内完成计算。
度下降法(以及其变种 SGD/Mini-Batch GD)通过将复杂问题分解为多次高效的 \(O(nd)\) 或更低复杂度的迭代更新,有效规避了求逆的瓶颈,成为大数据和高维场景下求解最优参数的首选方法。
3.梯度下降的分类
- 批量梯度下降 (Batch Gradient Descent, BGD):在计算梯度 \(\nabla J(\mathbf{\theta})\) 时,使用全部训练数据(即整个批次,或 Batch)。每次更新都需要遍历整个数据集。当数据集 \(n\) 非常大时,计算成本高,速度慢。此外,大批量计算可能占用大量内存。
- 随机梯度下降 (Stochastic Gradient Descent, SGD):在计算梯度 \(\nabla J(\mathbf{\theta})\) 时,每次迭代只随机抽取一个样本来估计梯度。梯度估计具有较大的随机性/噪声,导致收敛路径剧烈震荡。虽然最终会收敛到最小值附近,但通常不会精确停在最小值点,而是在附近徘徊。
- 小批量梯度下降 (Mini-Batch Gradient Descent, MBGD):在计算梯度 \(\nabla J(\mathbf{\theta})\) 时,每次迭代使用一个小批量(Mini-Batch)的样本子集(通常大小在 32 到 512 之间,一般为2的幂次)。需要调整超参数“批量大小”(Batch Size)。
其中 \(m\) 是小批次的大小,且 \(1 < m < n\)
机器学习实践中做决策的核心——权衡(Trade-off)。
如果数据量大且资源有限,选择 MBGD 或 SGD,这意味着权衡了收敛的微小不稳定性和噪声,来换取更高的训练速度和内存效率。
如果数据量小且对最终参数精度要求极高,可能会选择 BGD,这意味着权衡了训练速度较慢的劣势,来换取最高的稳定性。
下面就是利用实际的例子说明不同的梯度下降策略,对于最优值w的影响:
实现一个通用的梯度下降函数,并通过调整 batch_size 来模拟 BGD、SGD 和 MBGD
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
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression # 用于找到理论最优解
# Matplotlib 绘图设置 (用于显示中文)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# -----------------------------------------------------------
# 步骤 1: 模拟数据和最优参数 (请用您的实际 X 和 y 替换)
# -----------------------------------------------------------
np.random.seed(42)
# 500 个样本
X = np.linspace(0, 10, 500).reshape(-1, 1)
# 真实模型: y = 5 + 3*X + 噪声
y = 5 + 3 * X.flatten() + np.random.normal(0, 5, 500)
n_samples = X.shape[0]
# 使用 sklearn 找到理论最优解
optimal_model = LinearRegression().fit(X, y)
w_star = optimal_model.coef_[0]
b_star = optimal_model.intercept_
print(f"理论最优参数 w*: {w_star:.4f}, b*: {b_star:.4f}")
# -----------------------------------------------------------
# 步骤 2: 通用梯度下降实现
# -----------------------------------------------------------
def custom_gradient_descent(X, y, learning_rate, n_epochs, batch_size):
"""
运行梯度下降并记录参数 w 的变化路径。
batch_size = n_samples -> BGD
batch_size = 1 -> SGD
batch_size = 32 -> MBGD
"""
w = 0.0 # 初始参数 w
b = 0.0 # 初始参数 b
w_history = []
for epoch in range(n_epochs):
# 每次 Epoch 重新打乱数据,对 SGD/MBGD 尤其重要
if batch_size != n_samples:
indices = np.random.permutation(n_samples)
X_shuffled = X[indices]
y_shuffled = y[indices]
else:
X_shuffled = X
y_shuffled = y
for i in range(0, n_samples, batch_size):
# 提取当前批次数据
X_batch = X_shuffled[i:i + batch_size]
y_batch = y_shuffled[i:i + batch_size]
m_batch = len(X_batch)
# 预测值
y_pred = w * X_batch.flatten() + b
# 计算梯度 (仅针对当前批次)
error = y_pred - y_batch
dw = (1 / m_batch) * np.sum(error * X_batch.flatten())
db = (1 / m_batch) * np.sum(error)
# 更新参数
w = w - learning_rate * dw
b = b - learning_rate * db
w_history.append(w)
return w_history
# -----------------------------------------------------------
# 步骤 3: 运行三种策略
# -----------------------------------------------------------
# 统一参数设置 (不同的学习率和迭代次数可能需要调整以获得最佳展示效果)
N_EPOCHS = 20
LEARNING_RATE = 0.005 # 适用于 BGD/MBGD
LR_SGD = 0.0005 # SGD 通常需要较小的学习率来减缓震荡
# 1. 批量梯度下降 (BGD)
w_history_bgd = custom_gradient_descent(X, y, learning_rate=LEARNING_RATE, n_epochs=N_EPOCHS, batch_size=n_samples)
# 2. 小批量梯度下降 (MBGD) - 常用 Batch Size = 32
w_history_mbgd = custom_gradient_descent(X, y, learning_rate=LEARNING_RATE, n_epochs=N_EPOCHS, batch_size=32)
# 3. 随机梯度下降 (SGD) - Batch Size = 1
w_history_sgd = custom_gradient_descent(X, y, learning_rate=LR_SGD, n_epochs=N_EPOCHS, batch_size=1)
# -----------------------------------------------------------
# 步骤 4: 画图展示 w 的收敛路径
# -----------------------------------------------------------
plt.figure(figsize=(10, 6))
# 绘制三种策略的 w 历史路径
plt.plot(w_history_bgd, label='BGD (Batch Gradient Descent)', color='blue', linewidth=2)
plt.plot(w_history_mbgd, label='MBGD (Mini-Batch Gradient Descent, Batch=32)', color='orange', alpha=0.7)
plt.plot(w_history_sgd, label='SGD (Stochastic Gradient Descent)', color='red', alpha=0.5)
# 绘制理论最优值 w*
plt.axhline(w_star, color='green', linestyle='--', label=f'理论最优解 $w^*$={w_star:.4f}', linewidth=2)
plt.title(f'不同梯度下降策略下参数 $w$ 的寻优路径 (迭代次数={N_EPOCHS} Epochs)')
plt.xlabel('参数更新次数 (Iterations)')
plt.ylabel('权重参数 $w$ 的值')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.ylim(w_star - 1, w_star + 1) # 聚焦于收敛区域
plt.show()

牺牲梯度准确性(减小 Batch Size)可以显著增加参数更新次数(SGD 的红线迭代次数远多于 BGD 的蓝线),从而加快训练速度,但代价是路径的剧烈震荡。在实际工程中,MBGD 提供了最佳的速度与稳定性的权衡。
4.防止过拟合-有效集与提前停止
数据集划分: 训练数据(Train)通常被分成三部分。(一般是随机抽取分为3类)
- 训练集(Train): 用于模型参数的拟合。
- 验证集(Valid): 用于模型调优和选择(有效率)。
- 测试集(Test): 用于最终评估模型性能。
在训练过程中,如果验证集(Valid)的mse突然变大了,而训练集mse仍在下降,这表明模型开始过拟合训练集数据。此时应采用提前停止(Early Stopping)策略,停止训练。
面试题1:小批量梯度下降m取值小,性价比更高?
小批量梯度下降(MBGD)中,批量m取值小(但大于 1),往往具有更高的“性价比”。这里的性价比是指在单位计算资源消耗下,获得最高的参数优化效率。
当计算梯度时,使用的样本 m越多,梯度估计的随机性就越小,越接近全部数据的真实梯度方向。
梯度方差 \(\text{Var}(\nabla J_m)\) 的下降速度与 \(\frac{1}{\sqrt{m}}\) 相关。希望方差尽可能小,使参数w的寻优路径稳定。
| 批量 m 变化 | 迭代成本支出变化 (∝m) | 标准误差 SE 收益变化 (∝1/m) |
|---|---|---|
| \(m=1 \to m=10\) | 成本 \(\times 10\) | SE 降低 \(1/\sqrt{10} \approx 0.316\) 倍 (约 68.4%) |
| \(m=100 \to m=200\) | 成本 \(\times 2\) | SE 降低 \(1/\sqrt{2} \approx 0.707\) 倍 (约 29.3%) |
| \(m=1000 \to m=2000\) | 成本 \(\times 2\) | SE 降低 \(1/\sqrt{2} \approx 0.707\) 倍 (约 29.3%) |
当m从 1增加到10,需要付出 10 倍的计算成本,换来 3.16 倍的 SE 改善(即 $10 / 3.16 \approx 3.16$ 的效率比)。
但是,当m从1000增加到2000时,你付出的计算成本增加了 2 倍,而 SE 只改善了 $\approx 1.414$ 倍(即 $2 / 0.707 \approx 1.414$ 的效率比)。
边际收益递减: 随着m的增加,虽然 SE 仍在下降,但 SE 的下降速度(收益)是 \(\mathbf{1/\sqrt{m}}\),而计算成本(支出)是 \(\mathbf{m}\),成本的增加速度远快于收益的增加速度。
在优化过程中,我们最关心的是单次迭代的计算时间和最终收敛所需的迭代次数。
小m例如32)的性价比高:
- 在较小的m范围内,SE 的改善是非常显著且高效的。只需付出很小的计算代价,就能将梯度噪声降低到一个可接受的水平,避免 SGD 那样剧烈的震荡。在实际训练中,MBGD 的计算时间(\(\propto m\))远小于 BGD,最终达到相同精度所需的总训练时间最短。
- 大m的性价比低,计算成本增大了,但总的收敛速度提升不明显。这是典型的低效率投入。
因此,根据 \(\mathbf{1/\sqrt{m}}\) 的关系,选择一个能最大化硬件利用率、且 SE 足够小的较小批量m,是获得最高优化性价比的策略。
5.书籍推荐
1.统计学习方法 第二版 李航
2.模式分类第2版

3.深度学习 Ian Goodfellow等人
