【AI思想启蒙03】线性回归2从傻瓜到智能,梯度下降法学习法

线性回归代码实现数据读取、模型训练、MSE评估与可视化,分析MSE凸性与梯度下降策略,MBGD平衡速度与稳定性,提前停止防过拟合。

Posted by Hilda on October 27, 2025

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) # 直接使用所有数据进行训练

image-20251027135230250

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}")

image-20251027135243118

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}")

image-20251027135317069

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}")

image-20251027135354439

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()

image-20251027135426797

image-20251027135438038

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()

image-20251027135548654

image-20251027135628114

如果以全量数据进行预测,可以从上图看到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\) 非常大时,计算成本高,速度慢。此外,大批量计算可能占用大量内存。
\[\nabla J(\mathbf{\theta}) = \frac{1}{n} \sum_{i=1}^{n} \nabla J_i(\mathbf{\theta})\]
  • 随机梯度下降 (Stochastic Gradient Descent, SGD):在计算梯度 \(\nabla J(\mathbf{\theta})\) 时,每次迭代只随机抽取一个样本来估计梯度。梯度估计具有较大的随机性/噪声,导致收敛路径剧烈震荡。虽然最终会收敛到最小值附近,但通常不会精确停在最小值点,而是在附近徘徊
\[\nabla J(\mathbf{\theta}) \approx \nabla J_i(\mathbf{\theta})\]
  • 小批量梯度下降 (Mini-Batch Gradient Descent, MBGD):在计算梯度 \(\nabla J(\mathbf{\theta})\) 时,每次迭代使用一个小批量(Mini-Batch)的样本子集(通常大小在 32 到 512 之间,一般为2的幂次)。需要调整超参数“批量大小”(Batch Size)。
\[\nabla J(\mathbf{\theta}) = \frac{1}{m} \sum_{i=1}^{m} \nabla J_i(\mathbf{\theta})\]

其中 \(m\) 是小批次的大小,且 \(1 < m < n\)

机器学习实践中做决策的核心——权衡(Trade-off)

如果数据量大且资源有限,选择 MBGDSGD,这意味着权衡了收敛的微小不稳定性和噪声,来换取更高的训练速度和内存效率

如果数据量小且对最终参数精度要求极高,可能会选择 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()

image-20251027155327675

牺牲梯度准确性(减小 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版

image-20251027212533939

3.深度学习 Ian Goodfellow等人

image-20251027212647529