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

- Name
- Allen Wang
深入探索梯度下降法在线性回归中的应用
欢迎体验这场线性回归的学习之旅!在这篇博客中,我们将一起实现单变量线性回归,探索如何预测一家餐厅连锁店的利润。通过从理论到代码的逐步拆解,你将学会如何利用梯度下降法优化模型参数,并通过图表直观理解整个过程。不管你是机器学习新手还是有一定基础的学习者,这篇文章都会为你提供清晰且实用的指导。让我们一起开始这场探索吧!
目录
1 - 准备工具包
在我们动手实现线性回归之前,需要准备好一些 Python 工具,它们是我们实现算法的基石。以下是我们要用到的工具包:
- NumPy:Python 的“计算器”,擅长处理数组和数学运算,用于计算模型参数和误差。
- Matplotlib:一个“画图板”,帮助我们将数据可视化为图表,直观展示模型效果。
utils.py:一个自定义的辅助文件,包含加载数据的函数load_data(),让我们轻松获取数据集。
创建 utils.py
为了让这篇博客的代码完全可运行,我们需要一个简单的 utils.py 文件。以下是它的内容,包含一个 load_data() 函数,生成一个小型模拟数据集:
# 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 中的函数:
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() 函数加载数据集:
# 加载数据集
x_train, y_train = load_data()
查看数据
动手之前,先了解数据是个好习惯。我们用 print 检查 x_train 和 y_train 的类型和内容:
# 查看 x_train
print("x_train 的类型:", type(x_train))
print("x_train 的前五个元素:\n", x_train[:5])
输出:
x_train 的类型: <class 'numpy.ndarray'>
x_train 的前五个元素:
[6.1101 5.5277 8.5186 7.0032 5.8598]
x_train是一个 NumPy 数组,表示城市人口(单位:万人)。
再看看 y_train:
# 查看 y_train
print("y_train 的类型:", type(y_train))
print("y_train 的前五个元素:\n", y_train[:5])
输出:
y_train 的类型: <class 'numpy.ndarray'>
y_train 的前五个元素:
[17.592 9.1302 13.662 11.854 6.8233]
y_train表示对应利润(单位:万美元)。
检查数据规模
用以下代码检查数据集大小:
print('x_train 的形状:', x_train.shape)
print('训练样本数:', len(x_train))
输出:
x_train 的形状: (97,)
训练样本数: 97
- 数据有 97 个样本,每个样本对应一个城市的人口和利润。
数据可视化
用散点图直观展示人口和利润的关系:
# 创建散点图
plt.scatter(x_train, y_train, marker='x', c='r')
plt.title("利润 vs. 城市人口")
plt.ylabel('利润(单位:万美元)')
plt.xlabel('城市人口(单位:万人)')
plt.show()
(运行后会显示一个散点图,红色的“x”表示数据点。)
观察:散点图显示人口越多,利润似乎越高,点大致沿直线分布。这表明线性回归可能是个好选择。
2.3 线性回归基础
线性回归的目标是使用一条直线来描述数据之间的关系,公式如下:
我们的模型可以简化表达为:
x:城市人口(输入)。f(x):预测利润(输出)。w:斜率,表示人口对利润的影响。b:截距,表示人口为 0 时的基准利润。
任务:
调整 w 和 b,让直线尽量贴近所有数据点。我们用成本函数衡量误差,用梯度下降法优化参数,使误差最小。
2.4 计算成本函数
成本函数(J(w,b))衡量模型预测与实际值的差距。计算步骤:
- 对每个样本,计算预测值
f(x)与实际值y的差。 - 将差平方(消除正负抵消)。
- 求所有样本平方差的平均值(除以 2m)。
练习 1:实现成本函数
实现 compute_cost 函数:
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 = 2 和 b = 1 测试:
initial_w = 2
initial_b = 1
cost = compute_cost(x_train, y_train, initial_w, initial_b)
print(f'初始 w 和 b 时的成本: {cost:.3f}')
输出:
初始 w 和 b 时的成本: 75.203
- 输出接近 75.203 说明函数正确,当前模型误差较大,需要优化。
2.5 梯度下降法
梯度下降法通过计算成本函数的梯度,逐步调整参数 和 ,以减小预测误差。其步骤如下:
计算成本函数 对 和 的偏导数(梯度):
用学习率 更新参数 和 :
重复以上步骤,直到成本函数 收敛。
练习 2:实现梯度计算
实现 compute_gradient 函数:
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 = 0 和 b = 0 测试:
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)
输出:
初始 w 和 b 时的梯度: -65.32884975 -9.811985
- 负梯度表明
w和b需向正方向调整。
2.6 优化参数
用梯度下降优化 w 和 b:
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 次:
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)
输出:
迭代 0: 成本 48.09
迭代 150: 成本 5.06
迭代 300: 成本 4.71
...
优化后的 w, b: 1.701589843256735 -5.83913505154639
- 成本逐渐减小,模型逐步优化。
绘制拟合结果
用优化后的参数绘制拟合直线:
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 的利润:
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))
输出:
人口 35,000 预测利润: $1166.43
人口 70,000 预测利润: $6070.98
- 人口 35,000 的城市预计月利润约 1.17 万美元。
- 人口 70,000 的城市预计月利润约 6.07 万美元。
3 - 交互式可视化探索
为了更直观地理解线性回归和梯度下降的工作原理,这里准备了三个交互式可视化工具:
3.1 线性回归拟合可视化
这个可视化展示了线性回归如何通过调整参数 w 和 b 来拟合数据点。你可以:
- 拖动滑块调整
w(斜率)和b(截距) - 实时观察拟合直线的变化
- 查看当前参数下的成本函数值
使用提示:
- 尝试将
w调整到接近 1.7,b调整到接近 -5.8,观察拟合效果 - 注意成本函数值如何随参数变化而变化
- 思考:为什么某些参数组合的成本更低?
3.2 成本函数3D可视化
成本函数 是一个关于 w 和 b 的二维函数。这个3D可视化帮助你理解:
- 成本函数的形状(碗状曲面)
- 最优参数点的位置(最低点)
- 梯度下降的路径
观察要点:
- 🔵 蓝色曲面:成本函数的形状
- 🔴 红点:最优参数位置(成本最低点)
- 🟢 绿色路径:梯度下降的优化轨迹
3.3 梯度下降动画演示
这个动画展示了梯度下降算法如何一步步找到最优参数:
控制说明:
- 点击"开始"按钮启动动画
- 调整学习率观察不同的收敛速度
- 点击"重置"重新开始
4 - 深入理解梯度下降
4.1 学习率的影响
学习率 是梯度下降中最重要的超参数之一。它决定了每次参数更新的步长。
学习率过小
# 学习率太小:收敛缓慢
alpha_small = 0.0001
w, b = gradient_descent(x_train, y_train, 0, 0,
compute_cost, compute_gradient,
alpha_small, 10000) # 需要更多迭代
现象:
- ✅ 优点:稳定,不会错过最优点
- ❌ 缺点:收敛极慢,需要大量迭代
学习率过大
# 学习率太大:可能发散
alpha_large = 0.5
w, b = gradient_descent(x_train, y_train, 0, 0,
compute_cost, compute_gradient,
alpha_large, 100)
现象:
- ❌ 成本函数值震荡
- ❌ 可能永远无法收敛
- ❌ 甚至导致数值溢出
合适的学习率
# 合适的学习率:快速且稳定收敛
alpha_good = 0.01
w, b = gradient_descent(x_train, y_train, 0, 0,
compute_cost, compute_gradient,
alpha_good, 1500)
特征:
- ✅ 成本函数单调递减
- ✅ 收敛速度适中
- ✅ 最终达到最优解
4.2 收敛判断
如何判断梯度下降是否已经收敛?
方法1:成本变化阈值
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:梯度范数
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 特征缩放的重要性
当特征的数值范围差异很大时,梯度下降可能收敛缓慢。
问题示例
# 假设有两个特征:房屋面积(0-5000)和房间数(1-10)
# 数值范围差异巨大,导致成本函数呈现狭长的椭圆形
解决方案:特征归一化
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. 降低学习率
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:固定迭代次数
# 适用于快速实验
iterations = 1500
策略2:早停法(Early Stopping)
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 向量化可以大幅提升速度。
向量化的成本函数
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
向量化的梯度计算
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
性能对比
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")
预期输出:
循环版本耗时: 0.8234 ms
向量化版本耗时: 0.0521 ms
加速比: 15.81x
5.4 多项式回归扩展
线性回归只能拟合直线关系。如果数据呈现曲线趋势怎么办?
多项式特征
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
可视化多项式拟合
# 拟合不同阶数的多项式
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 性能优化建议
- 使用向量化操作 - 避免 Python 循环
- 特征缩放 - 加速收敛
- 合适的学习率 - 平衡速度和稳定性
- 早停法 - 避免不必要的迭代
- 批量处理 - 对于大数据集使用小批量梯度下降
6.3 扩展学习方向
掌握了单变量线性回归后,你可以继续学习:
- 多变量线性回归 - 处理多个特征
- 正则化 - 防止过拟合(L1/L2正则化)
- 逻辑回归 - 解决分类问题
- 神经网络 - 更复杂的非线性模型
- 优化算法 - Adam、RMSprop 等高级优化器
总结
恭喜你完成这场线性回归的实践!通过这篇博客,你学会了以下内容:
- 使用线性回归模型拟合数据,探索人口与利润之间的关系。
- 实现了成本函数和梯度下降算法,优化模型参数。
- 通过图表验证模型的效果,并利用模型预测新数据。 这只是机器学习的起点!你可以尝试调整学习率(
alpha)或增加迭代次数,观察模型性能的变化。下一期我们将探索分类问题,敬请期待!
本篇文章的部分内容和思想参考了 吴恩达 (Andrew Ng) 在 Coursera 机器学习课程 中的讲解,感谢他对机器学习领域的卓越贡献。