目录
一、梯度下降
1.1 什么是梯度下降?
1.2 梯度下降解决什么问题?
1.3 梯度下降的损失函数
1.4 梯度怎么走损失降的快
1.5 梯度下降线性回归的递推公式
1.6 代码如何实现梯度下降的更新规则
1.7 对数据还要进行特征标准化(Z-score)
1.8 三种梯度下降变体
二、梯度下降常见情形与处理方法
情形一:正常收敛(理想状态)
情形二:学习率过小 —— 收敛过慢
情形三:学习率过大 —— 剧烈震荡
情形四:发散与梯度爆炸
情形五:特征尺度失衡 —— 梯度「偏科」
情形六:梯度消失(了解即可)
情形七:鞍点与局部极小(了解即可)
情形八:SGD 梯度噪声
一、梯度下降
1.1 什么是梯度下降?
梯度下降是一种迭代优化算法。给定一个可微的代价函数J(θ),它沿着负梯度方向一步步更新参数,使 J 越来越小——就像在山坡上「沿最陡的下坡方向走路」,最终走到谷底(局部最小值)。
梯度下降(Gradient Descent)不直接解方程,而是反复微调参数,直到损失足够小。深度学习里几乎所有模型参数,都是靠它(或其变体)训练出来的。
1.2 梯度下降解决什么问题?
我们都知道:线性回归在做什么
给定房屋面积 x,预测房价 y,假设函数为:
其中为权重(斜率),
为偏置(截距),参数向量
。
但是它能干什么?
数据量不大、特征不多时,线性回归甚至可以用正规方程一步求出最优解:
但是换到另一个场景:就不行了
- 特征成千上万(
求逆代价
,太慢)
- 数据量上亿,矩阵放不下内存
- 神经网络等复杂模型,根本没有解析解
所以有了:梯度下降
不再一步求解,而是重复:
其中为学习率(代码中
learning_rate),为梯度。
下文以及配套代码的符号说明:
| 符号 | 含义 | 代码变量 |
|---|---|---|
| 样本数量 | m = len(x) | |
| 第 i 个样本特征(面积) | x[i],列area | |
| 第 i 个样本标签(房价) | y[i],列price | |
| 预测值 | y_pred[i] | |
| 残差 | error[i] | |
| 代价函数(本篇文章取 MSE) | loss | |
| J 对 w 的偏导 | dw | |
| J 对 b 的偏导 | db | |
| 梯度向量 | (dw, db) | |
| 学习率 | learning_rate | |
| 特征均值、标准差 | stats["x_mean"],stats["x_std"] | |
| Z-score 标准化后的特征 | x_n[i] |
1.3 梯度下降的损失函数
损失函数:MSE
残差
均方误差 MSE
MSE(Mean Squared Error)的标准定义:
本教程令代价函数:
优化目标:
- J 越大 → 拟合越差
- J 越小 → 拟合越好
把 J 想象成一座「山」
参数是你在山上的坐标,
是这里的海拔高度:
- 海拔高 = 损失大 = 预测差
- 海拔低 = 损失小 = 预测好
- 我们的目标:从山上某个点出发,走到谷底(J 最小)
代码(compute_loss()):
y_pred = w * x + b loss = np.mean((y_pred - y) ** 2) # J = MSE1.4 梯度怎么走损失降的快
这一节我们分两步:先弄懂「梯度是什么、为什么有用」,再推导线性回归里的梯度公式。
先想一个问题:能往任意方向走吗?
站在山上的某一点,你可以朝任意方向迈一小步:只改、只改
、或两者一起改。
每种走法,损失 J 变化快慢不同。例如:
| 走法 | 含义 |
|---|---|
| 只增大 | 沿 |
| 只增大 | 沿 |
| 斜着走 |
所有梯度下降的核心问题:在无数方向里,选哪一个能让下降最快?
一元情形:偏导数就是「坡度」
先只看,固定
。把
看成
的一元函数
。
偏导数表示:
增加一点点时,
变化多快——即
方向的坡度。
| 含义 | 想让 | |
|---|---|---|
| >0 | 减小 | |
| <0 | 增大 | |
| =0 | 该方向平坦 | 沿 |
规律:沿 的方向改
,
下降最快(至少在不改
的前提下)。
方向同理:应沿
改
。
二元情形:梯度把两个坡度「打包」
同时有和
时,把两个偏导数放进一个向量:
这就是梯度(gradient)。
:综合了「往
走」和「往
走」两个方向的坡度信息
- 它指向函数
增长最快的方向(最陡的上坡)
指向
下降最快的方向(最陡的下坡)
1.5 梯度下降线性回归的递推公式
在1.4的学习后我们了解了如下知识点
| 梯度是什么? | 所有偏导数组成的向量 ∇J∇J,描述当前点各方向的坡度 |
| 为什么用梯度? | 它指出 JJ 增长最快的方向 |
| 为什么更新用 | 负梯度是 J 下降最快的方向 |
那么我们可以写出他的递推公式来计算这个所谓的坡度也就是梯度
对残差,用链式法则
:
:预测偏高,应减小
→ 对
的修正方向与
符号相关
- 全体样本的
累加,得到「
方向综合坡度」
的梯度就是所有残差之和(截距对每个样本影响相同)
1.6 代码如何实现梯度下降的更新规则
前面已经说明:要让损失降得最快,必须沿方向更新参数
从初始参数出发,每轮迭代:
向量形式(把两个式子合成一行):
为什么是减号?因为指向上坡,我们要下坡,所以取负:
。
控制这一步迈多大。
def train(x, y, w_init=0.0, b_init=0.0, learning_rate=0.01, epochs=1000, ...): w, b = w_init, b_init for epoch in range(1, epochs + 1): loss = compute_loss(x, y, w, b) # ① 算当前损失 J dw, db = compute_gradients(x, y, w, b) # ② 算坡度 ∇J if max_grad_norm is not None: dw, db = clip_gradients(dw, db, max_grad_norm) # 可选:梯度裁剪(第 8.5 节) w -= learning_rate * dw # ③ w ← w - η·∂J/∂w b -= learning_rate * db # ④ b ← b - η·∂J/∂b # ⑤ 重复直到 loss 足够小或达到 epochs return w, b, loss_history, ...学习率怎么选?
| 现象 | |
|---|---|
| 过小 | 收敛慢,需要很多轮 |
| 适中 | 损失平稳下降 |
| 过大 | 震荡甚至发散 |
代码中compare_learning_rates()对比=0.001,0.01,0.1 的收敛曲线。
1.7 对数据还要进行特征标准化(Z-score)
面积约 50~150,房价约 200~400,量级不同会导致与
尺度悬殊,同一
难以兼顾。
对训练集做Z-score 标准化:
在上训练得到
,再还原(
denormalize_params()):
代码对应
def standardize(x: np.ndarray, y: np.ndarray): stats = { "x_mean": x.mean(), "x_std": x.std(), "y_mean": y.mean(), "y_std": y.std(), } x_n = (x - stats["x_mean"]) / stats["x_std"] # x̃ = (x - μx) / σx y_n = (y - stats["y_mean"]) / stats["y_std"] # ỹ = (y - μy) / σy return x_n, y_n, stats def denormalize_params(w_n: float, b_n: float, stats: dict): w = w_n * stats["y_std"] / stats["x_std"] b = stats["y_mean"] + stats["y_std"] * b_n - w * stats["x_mean"] return w, b1.8 三种梯度下降变体
| 类型 | 每次更新用多少样本 | 特点 |
|---|---|---|
| BGD批量梯度下降 | 全部 m 个 | 梯度准确、稳定;数据大时慢 |
| SGD随机梯度下降 | 1 个 | 快、有噪声;路径抖动 |
| Mini-batch | 一小批(如 32) | 深度学习最常用 |
Mini-batch已经成为深度学习训练的业界主流选择
二、梯度下降常见情形与处理方法
梯度下降并非每次都能「稳稳下山」。下面列举训练中常见情形、如何识别、如何处理,并与代码中的演示图对应。(图片序号在上方配套代码与数据集中)
| 情形 | 典型表现 | 常见原因 | 处理方法 | 代码/图示 |
|---|---|---|---|---|
| 正常收敛 | 保持当前设置 | 图2、图3 | ||
| 收敛过慢 | 增大 | 图5、图7左 | ||
| 剧烈震荡 | 减小 | 图7 中 | ||
| 发散 / 梯度爆炸 | 减小 | 图7 右 | ||
| 梯度失衡 | 特征/标签量级差悬殊 | Z-score 标准化 | 图8 左 vs 中 | |
| 梯度消失 | 更新几乎为 0,参数不动 | 深层 Sigmoid/Tanh 饱和(本教程线性回归少见) | ReLU;残差连接;合适初始化 | 深度学习阶段学 |
| 鞍点 / 平坦区 | 梯度接近 0,下降极慢 | 高维空间中鞍点众多 | Momentum、Adam 等自适应优化器 | 进阶 |
| SGD 噪声 | 损失曲线毛糙、抖动 | 每步只用 1 个样本估计梯度 | 增大 batch;BGD / Mini-batch | 图6 |
情形一:正常收敛(理想状态)
表现:
- 损失
随 epoch单调下降(后期变缓)
,
- 参数曲线、损失曲线都「顺滑」
条件:≈0.01(标准化后)+ Z-score 标准化 + BGD。
对应图示:图2 参数收敛、图3 损失下降。
情形二:学习率过小 —— 收敛过慢
表现:
太小⇒每步走不动
在大量 epoch 后仍很高
- 训练「没坏,就是慢」
处理:
- 增大学习率
(如 0.001→0.01)
- 学习率搜索:试
,选下降最快且稳定的(图5、图7)
- 学习率衰减:前期大
快速接近,后期小
精细收敛
代码演示:demo_learning_rate_abnormal()左图,=0.0001。
# 梯度下降法.py → demo_learning_rate_abnormal() 调用 train() 对比不同 η _, _, history, _, _ = train(x, y, learning_rate=0.0001, epochs=80, verbose=False)情形三:学习率过大 —— 剧烈震荡
表现:
- 参数更新步长过大,反复跨过谷底
曲线呈锯齿状上下波动,不下降
可能在
两侧来回跳
数学直觉:二次碗形损失在谷底附近,过大使迭代点
被「甩」到山坡另一侧。
处理:
- 减小
(最直接)
- Momentum(动量):积累历史梯度方向,平滑震荡
- AdaGrad / RMSProp / Adam:自适应调整每个参数的有效学习率
代码演示:demo_learning_rate_abnormal()中图,=0.35。
_, _, history, _, _ = train(x, y, learning_rate=0.35, epochs=80, verbose=False)情形四:发散与梯度爆炸
表现:
指数级飙升,出现
甚至
inf/nan绝对值爆炸
- 控制台 loss 打印「越来越大」
原因:
极大,单步更新
失控
- 深度网络中链式法则连乘,梯度指数放大(本教程线性回归只有一层,爆炸多由
过大引起)
处理:
- 大幅减小
- 梯度裁剪(Gradient Clipping):
若
代码中clip_gradients(dw, db, max_norm),train(..., max_grad_norm=1.0)。
def clip_gradients(dw: float, db: float, max_norm: float): """若 ‖∇J‖ > τ,则缩放梯度,防止单步更新过大。""" norm = np.sqrt(dw ** 2 + db ** 2) if norm > max_norm: scale = max_norm / norm dw *= scale db *= scale return dw, db # train() 内:算完梯度后可选裁剪 dw, db = compute_gradients(x, y, w, b) if max_grad_norm is not None: dw, db = clip_gradients(dw, db, max_grad_norm) w -= learning_rate * dw情形五:特征尺度失衡 —— 梯度「偏科」
表现:
与
量级差
倍以上
- 一个参数疯狂更新,另一个几乎不动
- 损失震荡或收敛到错误位置
原因:例如面积用平方毫米(∼)、房价用元(
),未做标准化。
处理:
- Z-score 标准化
- Min-Max 缩放到 [0,1]
- 手工统一单位(不如标准化通用)
数据集:house_price_bad_scale.csv(area_mm2,price_yuan),与主数据集同一线性关系,仅单位极端。
代码演示:demo_scale_and_clipping()—— 左图未标准化(失控)vs 中图标准化(稳定)。
# 梯度下降法.py → demo_scale_and_clipping() x, y = load_data(BAD_SCALE_PATH) # 极端尺度数据 x_n, y_n, stats = standardize(x, y) _, _, hist_raw, _, _ = train(x, y, learning_rate=0.01, epochs=60, verbose=False) # 未标准化 _, _, hist_std, _, _ = train(x_n, y_n, learning_rate=0.01, epochs=60, verbose=False) # 标准化后情形六:梯度消失(了解即可)
表现:梯度,参数几乎不更新。
原因:深层网络 + Sigmoid/Tanh 饱和区,连乘导数 →0。单层线性回归几乎不会梯度消失。
处理:ReLU 激活、残差网络(ResNet)、LSTM 门控、合适初始化、Batch Norm。
情形七:鞍点与局部极小(了解即可)
表现:梯度接近 0,但不在全局最优;高维空间中鞍点比局部极小更常见。
处理:Momentum 帮助「冲过」平坦区;Adam 等自适应方法;多次随机初始化。
情形八:SGD 梯度噪声
表现:损失曲线比 BGD更毛糙,上下抖动明显。
原因:每轮只用 1 个样本,是
的有噪声估计。
处理:
| 方法 | 说明 |
|---|---|
| 用 BGD | 稳定但慢(5000 样本尚可) |
| Mini-batch | 折中,深度学习默认 |
| 减小 SGD 的 | 噪声大时步子小一点 |
| 学习率衰减 | 后期缩小步长 |
代码演示:图6demo_sgd_vs_bgd()。
# 梯度下降法.py → demo_sgd_vs_bgd() 核心对比 # BGD:用全部 m 个样本算梯度(与 train() 相同) dw, db = compute_gradients(x, y, w_bgd, b_bgd) w_bgd -= lr * dw b_bgd -= lr * db # SGD:随机抽 1 个样本,用单样本梯度更新(不再除以 m) idx = rng.integers(0, m) xi, yi = x[idx], y[idx] error = w_sgd * xi + b_sgd - yi w_sgd -= lr * 2.0 * error * xi # ∂J^(i)/∂w = 2·e^(i)·x^(i) b_sgd -= lr * 2.0 * error # ∂J^(i)/∂b = 2·e^(i)