梯度下降(Gradient Descent)

梯度下降是训练几乎所有机器学习模型的核心优化算法。它做的事很单纯:不断微调模型参数,让损失函数(模型的”错误程度”)一点点变小,直到找到(近似)最优参数。 从线性回归到神经网络到大语言模型,训练本质都是在跑梯度下降。

一句话类比:梯度下降就像在浓雾中下山。你看不到山谷在哪(不知道最优解),但能感觉到脚下哪个方向最陡(梯度)。于是你朝最陡的下坡方向迈一小步,到新位置再感受一次,反复迈步,最终走到谷底(损失最小处)。步子的大小就是学习率。

关联:训练模型 = “定义一个衡量错误的损失函数(见 cross-entropy-loss.md)→ 用梯度下降最小化它”。single-layer-perceptron.md 里的更新规则,其实就是梯度下降的一个特例(见第 8 节)。

1. 为什么需要它:训练 = 最小化损失

模型里有一堆参数(权重 w、偏置 b)。训练的目标是找到一组参数,让模型预测得尽量准,也就是让损失函数 L(参数) 尽量小

问题是:参数动辄成千上万乃至上亿,损失函数是个超高维的复杂曲面,没法直接解出最小值。于是改用”迭代逼近”:从一个随机起点出发,每一步都往”损失更小”的方向挪一点,慢慢逼近谷底。

关键认知:梯度下降不保证找到全局最优,它找的是”从当前起点出发,一路下坡能到的最低点”。 但实践中,对高维神经网络这通常已经足够好。

2. 核心直觉:梯度指向”最陡上升”,所以要往反方向走

  • 梯度 (Gradient) ∇L:损失函数对每个参数的偏导数组成的向量。它指向函数值上升最快的方向。
  • 我们要让损失下降,所以往**梯度的反方向(负梯度)**走。
  • 梯度的大小反映坡度陡不陡:越陡(离最优越远)步子迈得越大,越平(接近谷底)步子自然越小。
损失 L
  │      ╲                    ╱
  │       ╲     负梯度方向    ╱
  │        ╲   ◀────────    ╱
  │         ╲___          _╱
  │            ╲___    ___╱   ← 谷底:梯度≈0,停下
  │                ╲__╱
  └──────────────────────────▶ 参数 θ
         当前位置,坡度向右上,
         所以往左(负梯度)走

一维直觉:dL/dθ > 0 说明”θ 增大损失增大”,那就减小 θdL/dθ < 0 说明”θ 增大损失减小”,那就增大 θ。无论哪种,θ 都是朝导数的反方向动——这正是下面公式里那个减号的来历。

3. 参数更新公式(整个算法就这一行)

符号含义
参数(权重、偏置)
损失对参数的梯度(最陡上升方向)
(eta)学习率 (learning rate),步长
减号往梯度反方向走 = 下坡

算法流程:

1. 随机初始化参数 θ
2. 重复直到收敛:
     a. 用当前 θ 算损失 L 和梯度 ∇L
     b. θ ← θ − η · ∇L      # 朝负梯度走一步
3. 得到(近似)最优 θ

4. 学习率 η:最关键的超参数

学习率决定每步迈多大,直接影响训不训得好:

学习率现象后果
太小步子太小收敛极慢,训练半天不动
合适稳步下降平滑收敛到谷底
太大步子太大在谷底两侧来回震荡,甚至越走越高发散
太小:  ·→·→·→·  慢慢挪,半天到不了底
合适:  ·→ · → ·  ↘ 稳稳到底
太大:  · → · ← · → · ← 左右横跳,下不去

呼应 single-layer-perceptron.md 第 8 节的网格搜索:那里试 lr=0.003/0.01/0.03,太小学不动、太大有波动、0.01 刚好——就是这里”学习率取舍”的实例。实践中还常用学习率调度(先大后小)和自适应优化器(见第 7 节)。

5. 三大变体:一次用多少数据算梯度

区别在于每次更新用多少样本来估计梯度:

变体每步用的数据优点缺点
批量梯度下降 (BGD)全部训练数据梯度准、下降平稳慢、吃内存,大数据集跑不动
随机梯度下降 (SGD)每次 1 个样本快、能跳出局部最优抖动大、不稳定
小批量 (Mini-batch)一小批(如 32/64/128)兼顾速度与稳定,能用 GPU 并行需选 batch size

实践里 99% 用 Mini-batch(虽然代码里的优化器常叫 SGD,但一般喂的是小批量)。相关概念:把整个训练集过一遍叫一个 epoch;每次更新用的那一小批叫一个 batch;一个 epoch 里更新的次数 = 样本数 / batch size。

6. 代码:从手写到框架

手写一个线性回归的梯度下降(拟合 y = wx + b,损失用 MSE):

import numpy as np
 
# 造点数据:真实关系 y ≈ 2x + 1
X = np.array([1, 2, 3, 4, 5], dtype=float)
y = np.array([3, 5, 7, 9, 11], dtype=float)
 
w, b = 0.0, 0.0        # 参数初始化
lr = 0.01              # 学习率
n = len(X)
 
for step in range(1000):
    y_pred = w * X + b               # 前向:当前预测
    error = y_pred - y               # 预测 - 真实
 
    # MSE 损失 L = mean(error^2),对 w、b 求偏导得到梯度:
    grad_w = (2/n) * np.sum(error * X)   # ∂L/∂w
    grad_b = (2/n) * np.sum(error)       # ∂L/∂b
 
    w -= lr * grad_w                 # 朝负梯度走一步(就是第 3 节公式)
    b -= lr * grad_b
 
print(w, b)   # → 接近 2 和 1

用 PyTorch(autograd 自动求梯度,不用手推):

import torch
 
X = torch.tensor([1, 2, 3, 4, 5.])
y = torch.tensor([3, 5, 7, 9, 11.])
w = torch.tensor(0.0, requires_grad=True)   # 标记:要对它求梯度
b = torch.tensor(0.0, requires_grad=True)
 
optimizer = torch.optim.SGD([w, b], lr=0.01)  # 优化器负责执行更新
 
for step in range(1000):
    y_pred = w * X + b
    loss = ((y_pred - y) ** 2).mean()   # MSE
 
    optimizer.zero_grad()   # ① 清空上一轮的梯度(不清会累加!常见坑)
    loss.backward()         # ② 自动反向传播算出所有梯度 (.grad)
    optimizer.step()        # ③ 按 θ -= lr*grad 更新参数
 
print(w.item(), b.item())

三步 zero_grad → backward → step 是 PyTorch 训练的固定节奏。backward()反向传播 (backpropagation) 自动算梯度——这是神经网络能高效求梯度的关键,梯度下降负责”怎么走”,反向传播负责”算出往哪走”。详见 ../python/torch.md

7. 进阶优化器(梯度下降的改良版)

朴素梯度下降有缺点(震荡、对学习率敏感、各参数用同一个步长)。实用中常用改良版:

优化器核心思想一句话
Momentum(动量)累积过去的梯度方向,像滚下山的球带惯性加速收敛、抑制震荡
AdaGrad / RMSProp给每个参数自适应不同的学习率稀疏/不同尺度参数更友好
AdamMomentum + 自适应学习率的结合最常用的默认选择,通常无脑用它

记忆锚点:朴素 SGD 是”匀速直线下山”,Momentum 加了”惯性”,Adam 再加”每个方向单独调速”。 不知道用啥时,先上 Adam。

8. 和感知机更新规则的关系(打通已有笔记)

single-layer-perceptron.md 第 6 节的更新规则:

w = w + lr * (y_i - y_pred) * x_i

它和梯度下降是同一个套路(y_i - y_pred) 是误差,* x_i 来自对参数求导的链式法则,lr 是学习率,整体就是”沿着减小误差的方向调整 w”。

换句话说:感知机的”遇错就改”,就是梯度下降在特定损失下的具体形态。 你已经会的东西,其实就是梯度下降。理解了这条主线,从感知机到神经网络的过渡就顺了。

9. 常见坑 / 误区(复习重点)

  • ❌ “梯度下降一定能找到全局最优” → 只保证局部最优;高维里更多是鞍点问题,靠 SGD 的随机性、Momentum 等帮助逃离
  • ❌ “学习率随便设” → 最关键的超参数:太小学不动、太大会震荡甚至发散(见第 4 节)
  • ❌ “不做特征归一化也行” → 各特征尺度差很大时,损失曲面被”拉长”,梯度下降会来回横跳、收敛极慢。训练前务必归一化(呼应感知机笔记第 9 节)
  • ❌ “PyTorch 里忘了 zero_grad()” → 梯度会累加到上一轮,导致更新错乱。每轮 backward 前必须清零
  • ❌ “梯度消失/爆炸不用管” → 深层网络里梯度连乘会趋近 0(学不动)或爆炸(发散);靠合适的激活函数、归一化层、梯度裁剪缓解
  • ❌ “loss 不降就是模型不行” → 先查学习率、归一化、数据/标签、是否 zero_grad,多数是这些工程问题
  • ❌ “batch 越大越好” → 大 batch 稳但泛化可能变差、吃显存;小 batch 抖动大但有正则效果,需权衡

10. 延伸阅读 / 关联概念