Published on

线性回归中的梯度下降法:从理论到实践

Authors
  • avatar
    Name
    Allen Wang
    Twitter

深入探索梯度下降法在线性回归中的应用

欢迎体验这场线性回归的学习之旅!在这篇博客中,我们将一起实现单变量线性回归,探索如何预测一家餐厅连锁店的利润。通过从理论到代码的逐步拆解,你将学会如何利用梯度下降法优化模型参数,并通过图表直观理解整个过程。不管你是机器学习新手还是有一定基础的学习者,这篇文章都会为你提供清晰且实用的指导。让我们一起开始这场探索吧!

目录


1 - 准备工具包

在我们动手实现线性回归之前,需要准备好一些 Python 工具,它们是我们实现算法的基石。以下是我们要用到的工具包:

  • NumPy:Python 的“计算器”,擅长处理数组和数学运算,用于计算模型参数和误差。
  • Matplotlib:一个“画图板”,帮助我们将数据可视化为图表,直观展示模型效果。
  • utils.py:一个自定义的辅助文件,包含加载数据的函数 load_data(),让我们轻松获取数据集。

创建 utils.py

为了让这篇博客的代码完全可运行,我们需要一个简单的 utils.py 文件。以下是它的内容,包含一个 load_data() 函数,生成一个小型模拟数据集:

Python
# utils.py
import numpy as np

def load_data():
    """
    加载模拟的训练数据集。

    返回:
        x_train (ndarray): 城市人口(单位:万人)
        y_train (ndarray): 餐厅利润(单位:万美元)
    """
    # 模拟数据:人口和利润
    x_train = np.array([6.1101, 5.5277, 8.5186, 7.0032, 5.8598, 8.3829, 7.4764, 8.5781, 6.4862, 5.0546 ..])
    y_train = np.array([17.592, 9.1302, 13.662, 11.854, 6.8233, 11.886, 4.3483, 12.0, 6.5987, 3.8166 ..])
    return x_train, y_train

说明:
这个 utils.py 文件模拟了一个包含 97 个样本的数据集:

  • x_train 表示城市人口(单位:万人,例如 6.1101 表示 61,101 人)。
  • y_train 表示餐厅月平均利润(单位:万美元,例如 17.592 表示 175,920 美元)。
    虽然现实中数据可能来自文件,但这里用固定数组保持简单和可运行性。

加载工具包

运行以下代码,加载所有工具并导入 utils.py 中的函数:

Python
import numpy as np
import matplotlib.pyplot as plt
from utils import load_data  # 从 utils.py 导入 load_data
import copy
import math
%matplotlib inline  # 在 Jupyter 环境中显示图表

这些工具是我们实现线性回归的基础。准备好了吗?让我们进入正题!


2 - 单变量线性回归

2.1 问题背景

假设你是一家餐厅连锁店的 CEO,计划在不同城市开设新店。你希望预测哪些城市能带来更高利润。为此,你收集了一些数据:现有餐厅所在城市的人口和月平均利润。现在的问题是:能否用这些数据预测新城市的潜在收益?

这正是线性回归的用武之地!它能帮我们在数据中找到一条直线,用于预测结果。接下来,我们将一步步实现它。

2.2 数据集概览

首先,我们需要数据。以下代码通过 load_data() 函数加载数据集:

Python
# 加载数据集
x_train, y_train = load_data()

查看数据

动手之前,先了解数据是个好习惯。我们用 print 检查 x_trainy_train 的类型和内容:

Python
# 查看 x_train
print("x_train 的类型:", type(x_train))
print("x_train 的前五个元素:\n", x_train[:5])

输出:

JavaScript
x_train 的类型: <class 'numpy.ndarray'>
x_train 的前五个元素:
 [6.1101 5.5277 8.5186 7.0032 5.8598]
  • x_train 是一个 NumPy 数组,表示城市人口(单位:万人)。

再看看 y_train

Python
# 查看 y_train
print("y_train 的类型:", type(y_train))
print("y_train 的前五个元素:\n", y_train[:5])

输出:

JavaScript
y_train 的类型: <class 'numpy.ndarray'>
y_train 的前五个元素:
 [17.592  9.1302 13.662  11.854  6.8233]
  • y_train 表示对应利润(单位:万美元)。

检查数据规模

用以下代码检查数据集大小:

Python
print('x_train 的形状:', x_train.shape)
print('训练样本数:', len(x_train))

输出:

JavaScript
x_train 的形状: (97,)
训练样本数: 97
  • 数据有 97 个样本,每个样本对应一个城市的人口和利润。

数据可视化

用散点图直观展示人口和利润的关系:

Python
# 创建散点图
plt.scatter(x_train, y_train, marker='x', c='r')
plt.title("利润 vs. 城市人口")
plt.ylabel('利润(单位:万美元)')
plt.xlabel('城市人口(单位:万人)')
plt.show()
输出示例:
(运行后会显示一个散点图,红色的“x”表示数据点。) 散点图 观察:
散点图显示人口越多,利润似乎越高,点大致沿直线分布。这表明线性回归可能是个好选择。

2.3 线性回归基础

线性回归的目标是使用一条直线来描述数据之间的关系,公式如下:

fw,b(x(i))=wx(i)+bf_{w, b}(\mathbf{x}^{(i)}) = w \mathbf{x}^{(i)} + b

我们的模型可以简化表达为:

f(x)=wx+bf(x) = w \cdot x + b
  • x:城市人口(输入)。
  • f(x):预测利润(输出)。
  • w:斜率,表示人口对利润的影响。
  • b:截距,表示人口为 0 时的基准利润。

任务:
调整 wb,让直线尽量贴近所有数据点。我们用成本函数衡量误差,用梯度下降法优化参数,使误差最小。


2.4 计算成本函数

成本函数(J(w,b))衡量模型预测与实际值的差距。计算步骤:

  1. 对每个样本,计算预测值 f(x) 与实际值 y 的差。
  2. 将差平方(消除正负抵消)。
  3. 求所有样本平方差的平均值(除以 2m)。

练习 1:实现成本函数

实现 compute_cost 函数:

Python
def compute_cost(x, y, w, b):
    """
    计算线性回归的成本。

    参数:
        x (ndarray): 形状 (m,),输入数据(城市人口)
        y (ndarray): 形状 (m,),实际值(利润)
        w, b (scalar): 模型参数(斜率和截距)

    返回:
        total_cost (float): 当前 w 和 b 下的成本
    """
    m = x.shape[0]
    total_cost = 0

    cost_sum = 0
    for i in range(m):
        f_wb = w * x[i] + b  # 预测值
        cost = (f_wb - y[i]) ** 2  # 单样本平方误差
        cost_sum += cost  # 累加误差
    total_cost = (1 / (2 * m)) * cost_sum  # 平均误差

    return total_cost

测试: 用初始值 w = 2b = 1 测试:

Python
initial_w = 2
initial_b = 1
cost = compute_cost(x_train, y_train, initial_w, initial_b)
print(f'初始 w 和 b 时的成本: {cost:.3f}')

输出:

JavaScript
初始 w 和 b 时的成本: 75.203
  • 输出接近 75.203 说明函数正确,当前模型误差较大,需要优化。

2.5 梯度下降法

梯度下降法通过计算成本函数的梯度,逐步调整参数 wwbb,以减小预测误差。其步骤如下:

计算成本函数 J(w,b)J(w, b)wwbb 的偏导数(梯度):

Jw=1mi=1m(fw,b(x(i))y(i))x(i) \frac{\partial J}{\partial w} = \frac{1}{m} \sum_{i=1}^{m} \left( f_{w, b}(\mathbf{x}^{(i)}) - y^{(i)} \right) x^{(i)} Jb=1mi=1m(fw,b(x(i))y(i)) \frac{\partial J}{\partial b} = \frac{1}{m} \sum_{i=1}^{m} \left( f_{w, b}(\mathbf{x}^{(i)}) - y^{(i)} \right)

用学习率 α\alpha 更新参数 wwbb

w=wαJw w = w - \alpha \frac{\partial J}{\partial w} b=bαJb b = b - \alpha \frac{\partial J}{\partial b}

重复以上步骤,直到成本函数 J(w,b)J(w, b) 收敛。

练习 2:实现梯度计算

实现 compute_gradient 函数:

Python
def compute_gradient(x, y, w, b):
    """
    计算成本对 w 和 b 的梯度。

    参数:
        x (ndarray): 形状 (m,),输入数据(人口)
        y (ndarray): 形状 (m,),实际值(利润)
        w, b (scalar): 模型参数

    返回:
        dj_dw (scalar): 对 w 的梯度
        dj_db (scalar): 对 b 的梯度
    """
    m = x.shape[0]
    dj_dw = 0
    dj_db = 0

    for i in range(m):
        f_wb = w * x[i] + b  # 预测值
        dj_dw_i = (f_wb - y[i]) * x[i]  # w 的单样本梯度
        dj_db_i = f_wb - y[i]  # b 的单样本梯度
        dj_dw += dj_dw_i  # 累加
        dj_db += dj_db_i
    dj_dw = dj_dw / m  # 平均
    dj_db = dj_db / m

    return dj_dw, dj_db

测试:w = 0b = 0 测试:

Python
initial_w = 0
initial_b = 0
dj_dw, dj_db = compute_gradient(x_train, y_train, initial_w, initial_b)
print('初始 w 和 b 时的梯度:', dj_dw, dj_db)

输出:

JavaScript
初始 w 和 b 时的梯度: -65.32884975 -9.811985
  • 负梯度表明 wb 需向正方向调整。

2.6 优化参数

用梯度下降优化 wb

Python
def gradient_descent(x, y, w_in, b_in, cost_function, gradient_function, alpha, num_iters):
    """
    执行梯度下降优化参数。

    参数:
        x, y (ndarray): 数据
        w_in, b_in (scalar): 参数初始值
        cost_function: 计算成本的函数
        gradient_function: 计算梯度的函数
        alpha (float): 学习率
        num_iters (int): 迭代次数

    返回:
        w, b: 优化后的参数
    """
    w = w_in
    b = b_in

    for i in range(num_iters):
        dj_dw, dj_db = gradient_function(x, y, w, b)
        w = w - alpha * dj_dw
        b = b - alpha * dj_db

        if i % 150 == 0:  # 每 150 次打印成本
            cost = cost_function(x, y, w, b)
            print(f"迭代 {i:4}: 成本 {cost:.2f}")

    return w, b

运行优化: 设置初始值 w = 0, b = 0, 学习率 alpha = 0.01, 迭代 1500 次:

Python
initial_w = 0.
initial_b = 0.
iterations = 1500
alpha = 0.01

w, b = gradient_descent(x_train, y_train, initial_w, initial_b,
                        compute_cost, compute_gradient, alpha, iterations)
print("优化后的 w, b:", w, b)

输出:

JavaScript
迭代    0: 成本 48.09
迭代  150: 成本 5.06
迭代  300: 成本 4.71
...
优化后的 w, b: 1.701589843256735 -5.83913505154639
  • 成本逐渐减小,模型逐步优化。

绘制拟合结果

用优化后的参数绘制拟合直线:

Python
m = x_train.shape[0]
predicted = np.zeros(m)
for i in range(m):
    predicted[i] = w * x_train[i] + b

plt.plot(x_train, predicted, c="b")  # 拟合直线
plt.scatter(x_train, y_train, marker='x', c='r')  # 数据点
plt.title("利润 vs. 城市人口")
plt.ylabel('利润(单位:万美元)')
plt.xlabel('城市人口(单位:万人)')
plt.show()
输出示例:
(显示一条蓝色直线穿过红色数据点,拟合效果良好。) 拟合图

利润预测

预测人口 35,000 和 70,000 的利润:

Python
predict1 = 3.5 * w + b  # 人口 3.5 万
print('人口 35,000 预测利润: $%.2f' % (predict1 * 10000))

predict2 = 7.0 * w + b  # 人口 7 万
print('人口 70,000 预测利润: $%.2f' % (predict2 * 10000))

输出:

JavaScript
人口 35,000 预测利润: $1166.43
人口 70,000 预测利润: $6070.98
  • 人口 35,000 的城市预计月利润约 1.17 万美元。
  • 人口 70,000 的城市预计月利润约 6.07 万美元。

3 - 交互式可视化探索

为了更直观地理解线性回归和梯度下降的工作原理,这里准备了三个交互式可视化工具:

3.1 线性回归拟合可视化

这个可视化展示了线性回归如何通过调整参数 wb 来拟合数据点。你可以:

  • 拖动滑块调整 w(斜率)和 b(截距)
  • 实时观察拟合直线的变化
  • 查看当前参数下的成本函数值

使用提示:

  • 尝试将 w 调整到接近 1.7,b 调整到接近 -5.8,观察拟合效果
  • 注意成本函数值如何随参数变化而变化
  • 思考:为什么某些参数组合的成本更低?

3.2 成本函数3D可视化

成本函数 J(w,b)J(w, b) 是一个关于 wb 的二维函数。这个3D可视化帮助你理解:

  • 成本函数的形状(碗状曲面)
  • 最优参数点的位置(最低点)
  • 梯度下降的路径

观察要点:

  • 🔵 蓝色曲面:成本函数的形状
  • 🔴 红点:最优参数位置(成本最低点)
  • 🟢 绿色路径:梯度下降的优化轨迹

3.3 梯度下降动画演示

这个动画展示了梯度下降算法如何一步步找到最优参数:

控制说明:

  • 点击"开始"按钮启动动画
  • 调整学习率观察不同的收敛速度
  • 点击"重置"重新开始

4 - 深入理解梯度下降

4.1 学习率的影响

学习率 α\alpha 是梯度下降中最重要的超参数之一。它决定了每次参数更新的步长。

学习率过小

Python
# 学习率太小:收敛缓慢
alpha_small = 0.0001
w, b = gradient_descent(x_train, y_train, 0, 0,
                        compute_cost, compute_gradient,
                        alpha_small, 10000)  # 需要更多迭代

现象:

  • ✅ 优点:稳定,不会错过最优点
  • ❌ 缺点:收敛极慢,需要大量迭代

学习率过大

Python
# 学习率太大:可能发散
alpha_large = 0.5
w, b = gradient_descent(x_train, y_train, 0, 0,
                        compute_cost, compute_gradient,
                        alpha_large, 100)

现象:

  • ❌ 成本函数值震荡
  • ❌ 可能永远无法收敛
  • ❌ 甚至导致数值溢出

合适的学习率

Python
# 合适的学习率:快速且稳定收敛
alpha_good = 0.01
w, b = gradient_descent(x_train, y_train, 0, 0,
                        compute_cost, compute_gradient,
                        alpha_good, 1500)

特征:

  • ✅ 成本函数单调递减
  • ✅ 收敛速度适中
  • ✅ 最终达到最优解

4.2 收敛判断

如何判断梯度下降是否已经收敛?

方法1:成本变化阈值

Python
def gradient_descent_with_convergence(x, y, w_in, b_in, alpha, max_iters, epsilon=1e-6):
    """
    带收敛判断的梯度下降

    参数:
        epsilon: 收敛阈值,当成本变化小于此值时停止
    """
    w, b = w_in, b_in
    prev_cost = float('inf')

    for i in range(max_iters):
        dj_dw, dj_db = compute_gradient(x, y, w, b)
        w = w - alpha * dj_dw
        b = b - alpha * dj_db

        cost = compute_cost(x, y, w, b)

        # 检查收敛
        if abs(prev_cost - cost) < epsilon:
            print(f"在第 {i} 次迭代时收敛")
            break

        prev_cost = cost

        if i % 100 == 0:
            print(f"迭代 {i}: 成本 {cost:.4f}")

    return w, b

方法2:梯度范数

Python
def check_convergence_by_gradient(x, y, w, b, threshold=1e-5):
    """
    通过梯度范数判断是否收敛
    """
    dj_dw, dj_db = compute_gradient(x, y, w, b)
    gradient_norm = np.sqrt(dj_dw**2 + dj_db**2)

    return gradient_norm < threshold

4.3 特征缩放的重要性

当特征的数值范围差异很大时,梯度下降可能收敛缓慢。

问题示例

Python
# 假设有两个特征:房屋面积(0-5000)和房间数(1-10)
# 数值范围差异巨大,导致成本函数呈现狭长的椭圆形

解决方案:特征归一化

Python
def feature_normalize(X):
    """
    特征归一化:将特征缩放到相似的范围

    使用 Z-score 标准化:x_norm = (x - μ) / σ
    """
    mu = np.mean(X, axis=0)      # 均值
    sigma = np.std(X, axis=0)    # 标准差
    X_norm = (X - mu) / sigma

    return X_norm, mu, sigma

# 使用示例
x_norm, mu, sigma = feature_normalize(x_train.reshape(-1, 1))
x_norm = x_norm.flatten()

# 在归一化数据上训练
w_norm, b_norm = gradient_descent(x_norm, y_train, 0, 0,
                                   compute_cost, compute_gradient,
                                   0.01, 1500)

# 预测时需要先归一化输入
def predict_with_normalization(x_new, w, b, mu, sigma):
    x_new_norm = (x_new - mu) / sigma
    return w * x_new_norm + b

5 - 常见问题与解决方案

5.1 成本函数不下降怎么办?

问题: 运行梯度下降后,成本函数值不降反升。

可能原因:

  1. 学习率过大 - 最常见原因
  2. 代码实现错误 - 梯度计算有误
  3. 数值溢出 - 参数值过大导致计算溢出

解决步骤:

Python
# 1. 降低学习率
alpha = 0.001  # 从小值开始尝试

# 2. 检查梯度计算
# 使用数值梯度验证解析梯度
def numerical_gradient(x, y, w, b, epsilon=1e-5):
    """数值方法计算梯度(用于验证)"""
    # 对 w 的数值梯度
    cost_plus = compute_cost(x, y, w + epsilon, b)
    cost_minus = compute_cost(x, y, w - epsilon, b)
    dj_dw_numerical = (cost_plus - cost_minus) / (2 * epsilon)

    # 对 b 的数值梯度
    cost_plus = compute_cost(x, y, w, b + epsilon)
    cost_minus = compute_cost(x, y, w, b - epsilon)
    dj_db_numerical = (cost_plus - cost_minus) / (2 * epsilon)

    return dj_dw_numerical, dj_db_numerical

# 验证梯度
dj_dw_analytical, dj_db_analytical = compute_gradient(x_train, y_train, 1.0, 0.5)
dj_dw_numerical, dj_db_numerical = numerical_gradient(x_train, y_train, 1.0, 0.5)

print(f"解析梯度 dw: {dj_dw_analytical:.6f}, 数值梯度 dw: {dj_dw_numerical:.6f}")
print(f"解析梯度 db: {dj_db_analytical:.6f}, 数值梯度 db: {dj_db_numerical:.6f}")

5.2 如何选择迭代次数?

策略1:固定迭代次数

Python
# 适用于快速实验
iterations = 1500

策略2:早停法(Early Stopping)

Python
def gradient_descent_early_stopping(x, y, w_in, b_in, alpha,
                                     max_iters, patience=50):
    """
    使用早停法的梯度下降

    参数:
        patience: 容忍成本不下降的迭代次数
    """
    w, b = w_in, b_in
    best_cost = float('inf')
    no_improve_count = 0

    for i in range(max_iters):
        dj_dw, dj_db = compute_gradient(x, y, w, b)
        w = w - alpha * dj_dw
        b = b - alpha * dj_db

        cost = compute_cost(x, y, w, b)

        if cost < best_cost:
            best_cost = cost
            no_improve_count = 0
        else:
            no_improve_count += 1

        if no_improve_count >= patience:
            print(f"早停:在第 {i} 次迭代停止")
            break

    return w, b

5.3 向量化实现提升性能

前面的实现使用了 Python 循环,对于大数据集效率较低。使用 NumPy 向量化可以大幅提升速度。

向量化的成本函数

Python
def compute_cost_vectorized(x, y, w, b):
    """
    向量化的成本函数实现
    """
    m = len(x)
    predictions = w * x + b
    errors = predictions - y
    cost = np.sum(errors ** 2) / (2 * m)
    return cost

向量化的梯度计算

Python
def compute_gradient_vectorized(x, y, w, b):
    """
    向量化的梯度计算
    """
    m = len(x)
    predictions = w * x + b
    errors = predictions - y

    dj_dw = np.dot(errors, x) / m
    dj_db = np.sum(errors) / m

    return dj_dw, dj_db

性能对比

Python
import time

# 测试循环版本
start = time.time()
cost_loop = compute_cost(x_train, y_train, 1.5, -3.0)
time_loop = time.time() - start

# 测试向量化版本
start = time.time()
cost_vec = compute_cost_vectorized(x_train, y_train, 1.5, -3.0)
time_vec = time.time() - start

print(f"循环版本耗时: {time_loop*1000:.4f} ms")
print(f"向量化版本耗时: {time_vec*1000:.4f} ms")
print(f"加速比: {time_loop/time_vec:.2f}x")

预期输出:

JavaScript
循环版本耗时: 0.8234 ms
向量化版本耗时: 0.0521 ms
加速比: 15.81x

5.4 多项式回归扩展

线性回归只能拟合直线关系。如果数据呈现曲线趋势怎么办?

多项式特征

Python
def create_polynomial_features(x, degree):
    """
    创建多项式特征

    例如:degree=2 时,x -> [x, x^2]
    """
    X_poly = np.zeros((len(x), degree))
    for i in range(degree):
        X_poly[:, i] = x ** (i + 1)
    return X_poly

# 创建二次特征
X_poly = create_polynomial_features(x_train, degree=2)

# 现在可以拟合二次曲线:y = w1*x + w2*x^2 + b

可视化多项式拟合

Python
# 拟合不同阶数的多项式
degrees = [1, 2, 3, 5]
plt.figure(figsize=(12, 8))

for i, degree in enumerate(degrees, 1):
    plt.subplot(2, 2, i)

    # 创建多项式特征并拟合
    X_poly = create_polynomial_features(x_train, degree)
    # ... 训练模型 ...

    plt.scatter(x_train, y_train, marker='x', c='r', label='数据')
    plt.plot(x_train, predictions, 'b-', label=f'{degree}次多项式')
    plt.title(f'阶数 = {degree}')
    plt.legend()

plt.tight_layout()
plt.show()

注意: 阶数过高可能导致过拟合!


6 - 实战技巧总结

6.1 调试检查清单

在实现线性回归时,按以下顺序检查:

  • 数据检查

    • 数据是否正确加载?
    • 是否有缺失值或异常值?
    • 特征和标签的维度是否匹配?
  • 成本函数检查

    • 初始成本值是否合理?
    • 成本是否随迭代单调递减?
    • 最终成本是否接近预期?
  • 梯度检查

    • 使用数值梯度验证解析梯度
    • 梯度的符号是否正确?
    • 梯度的量级是否合理?
  • 学习率调优

    • 从小学习率开始(如 0.001)
    • 逐步增大直到成本开始震荡
    • 选择震荡前的最大值
  • 收敛检查

    • 绘制成本-迭代次数曲线
    • 确认成本已经稳定
    • 检查参数是否还在变化

6.2 性能优化建议

  1. 使用向量化操作 - 避免 Python 循环
  2. 特征缩放 - 加速收敛
  3. 合适的学习率 - 平衡速度和稳定性
  4. 早停法 - 避免不必要的迭代
  5. 批量处理 - 对于大数据集使用小批量梯度下降

6.3 扩展学习方向

掌握了单变量线性回归后,你可以继续学习:

  1. 多变量线性回归 - 处理多个特征
  2. 正则化 - 防止过拟合(L1/L2正则化)
  3. 逻辑回归 - 解决分类问题
  4. 神经网络 - 更复杂的非线性模型
  5. 优化算法 - Adam、RMSprop 等高级优化器

总结

恭喜你完成这场线性回归的实践!通过这篇博客,你学会了以下内容:

  • 使用线性回归模型拟合数据,探索人口与利润之间的关系。
  • 实现了成本函数和梯度下降算法,优化模型参数。
  • 通过图表验证模型的效果,并利用模型预测新数据。 这只是机器学习的起点!你可以尝试调整学习率(alpha)或增加迭代次数,观察模型性能的变化。下一期我们将探索分类问题,敬请期待!

本篇文章的部分内容和思想参考了 吴恩达 (Andrew Ng)Coursera 机器学习课程 中的讲解,感谢他对机器学习领域的卓越贡献。