AIGC标识 机器学习_第二篇_梯度下降和学习率

前言:

第一次看到"梯度下降"这四个字,下意识觉得:完了,要微积分了。

其实不用慌。梯度下降干的事很朴素:被训练的模型现在答得不太对,看看"往哪个方向改,错得会少一点",然后挪一小步。重复很多次,直到误差小到能接受。(机器学习、数值分析不分家)

它不保证一步到位。它更像摸着黑下山:每走一步,先判断脚下地面朝哪边倾;然后朝下坡方向走一点。步子太大,可能直接跨过山谷;太小,又会走到怀疑人生。这就是学习率。


先把问题说透:梯度下降到底在干什么,就是降低误差。

先回答最核心的问题:梯度下降是什么,它要解决什么问题,它调整的又是什么。

假设我瞎猜了一组参数,算出来的预测值和真实答案差了一大截。我肯定想改参数,但改多少?往哪边改?这就是梯度下降要回答的。

它做的事情很简单:先用当前参数预测,看看总共错了多少;再判断每个参数往哪边挪,误差会降;最后只挪一小步。 然后继续预测、判断、挪步,反复很多轮。

它优化的不是输入数据,也不是训练标签。训练数据是题目,模型参数才是我们反复调整的旋钮。在线性回归里,旋钮是 \(w,b\);在神经网络里,旋钮就是每一层的权重和偏置。好比战报接线员通过旋转按钮,让电报更加清晰。

把名字拆开看会更清楚:

  • "梯度"是哪个梯度?是损失函数对模型参数的梯度。不是对 \(x\) 求,也不是对标签 \(y\) 求。它回答的是:当前参数轻轻改一点,损失会往哪里、以多快的速度变;
  • "下降"降的是什么?要降的是损失,不是参数本身。参数有的会变大,有的会变小,只要总损失在往下走就对了;
  • "调整了什么"?每一步实际改的是参数值,比如把 \(w\) 从 4.0 改成 4.03,把 \(b\) 从 20 改成 20.01。改多少由学习率决定。

所以,梯度下降不是"把梯度降下来"。它是利用梯度当导航,去把损失降下来。

先记住这个总式子,不急着看懂每个符号:

\[\text{新参数}=\text{旧参数}-\text{学习率}\times\text{损失对参数的梯度} \]

后面讲导数、偏导数和 \(\nabla\),就是在把这个式子里的"梯度"拆开。


先从一个不那么玄的例子说起

假设我在做一个房价预测器。先偷个懒,只看面积:

\[\hat y = wx + b \]

这里:

  • \(x\) 是面积;
  • \(\hat y\) 是模型猜的房价;
  • \(w\) 是每增加一平方米,预测房价大约加多少;
  • \(b\) 是截距,理解成"面积为 0 时模型给出的起点"就行。

手里有一条样本:面积 \(x=50\),真实房价 \(y=300\)。模型目前参数是 \(w=4, b=20\),那它的预测值为:

\[\hat y = 4 \times 50 + 20 = 220 \]

猜低了,少猜了 80。

问题来了:\(w\)\(b\) 到底该怎么调?往上加多少合适?人眼盯着一两条数据还行,数据有十万条时,总不能靠感觉拧参数。梯度下降就是替我算这件事的办法。


快速大白话导数

在研究怎么调 \(w\)\(b\) 前,得先认识导数。它不是一套专门用来折磨人的符号,问的问题其实很生活化:我把一个量轻轻推一下,结果会变多少?

比如打车计费 \(f(x)=3x+10\)\(x\) 是里程。多开 1 公里,车费多 3 元。这个"每多一点输入,输出大约多多少",就是变化率;对这条直线来说,变化率始终是 3。

写成公式,导数是:

\[f'(x)=\lim_{\Delta x\to0}\frac{f(x+\Delta x)-f(x)}{\Delta x} \]

先别被极限吓到。分子是在算"结果变了多少",分母是在算"输入变化了多少";把 \(\Delta x\) 想得非常小,得到的就是当前位置最细的变化趋势。

回到损失函数。先只盯住 \(w\),把 \(b\) 暂时按住不动。不同的 \(w\) 会得到不同的 \(J(w)\),横轴是 \(w\),纵轴是损失。我站在曲线上某个位置,导数告诉我的就是:\(w\) 往右挪一点,损失接下来是涨还是跌,涨跌有多快。

\[\frac{dJ}{dw} \]

  • 导数大于 0:往右走是在上坡,\(w\) 应该减小;
  • 导数小于 0:往右走反而在下坡,\(w\) 应该增大;
  • 导数接近 0:坡度已经很平,可能在谷底、山顶或鞍点附近。在线性回归的平方损失这类凸问题里,才可以把它看成接近最优。

这就是导数在优化里的作用:它不是直接报出"正确参数是多少",而是给我当前这一步的路况。


有两个旋钮时:偏导数就是"其他先别动"

真实模型很少只有一个参数。这里至少有 \(w\)\(b\)

\[J(w,b)=\frac{1}{2m}\sum_{i=1}^{m}(wx_i+b-y_i)^2 \]

现在的问题变成:我到底先看 \(w\) 的影响,还是先看 \(b\) 的影响?

偏导数则是将“控制变量”思想数学化。规矩很简单:想研究谁,就只动谁;其他变量先钉住。

  • \(\frac{\partial J}{\partial w}\):先把 \(b\) 当常数,只看 \(w\) 动一点会怎样;
  • \(\frac{\partial J}{\partial b}\):先把 \(w\) 当常数,只看 \(b\) 动一点会怎样。

符号里的 \(\partial\) 可以先理解成"部分"。它不是新魔法,只是标记:这次只问其中一个旋钮。

拿一个更直白的函数热热身:

\[f(a,b)=a^2+3b \]

\(a\) 求偏导时,\(b\) 就像一张写死的便签:

\[\frac{\partial f}{\partial a}=2a \]

\(b\) 求偏导时,\(a^2\) 是常数:

\[\frac{\partial f}{\partial b}=3 \]

模型参数再多,道理也没变。每个偏导数都在回答:这个参数此刻动一点,对总误差有多大影响?


梯度:把每个旋钮的路况打成一包

有了两个偏导数,梯度就没那么神秘了。它只是把所有参数的偏导数按顺序装进一个向量:

\[\nabla J(w,b)= \begin{bmatrix} \frac{\partial J}{\partial w}\\ \frac{\partial J}{\partial b} \end{bmatrix} \]

这里左边那个倒三角 \(\nabla\),读作"nabla",中文里通常直接叫它"梯度算子"。可以把它先当成一个打包动作:它看到 \(J(w,b)\),就把"对 \(w\) 的偏导"和"对 \(b\) 的偏导"算出来,按顺序装进一个向量。

它不是一个具体数字,也不是损失值。\(\nabla J\) 的结果是"每个参数各自该怎么调"的一组数。要是模型有 100 万个可训练参数,这个向量通常就有 100 万个分量。框架里常看到的 loss.backward(),干的就是沿计算图把这包梯度算出来并放到各参数的 grad 里;它还没有改参数,真正改参数的是后面的 optimizer.step()

可以把梯度当成一张导航纸条:第一行写 \(w\) 该往哪边调,第二行写 \(b\) 该往哪边调。数学上,梯度指向损失上升最快的方向;我想把损失压下去,就反着它走。

这就把"梯度下降"拆开了:梯度负责指方向,下降表示朝负梯度方向更新。


先定规则:错多少,怎么算

模型不能只说"我觉得不准",得把"不准"写成一个能计算的数。在线性回归里,最常用的是平方误差。

对单条样本:

\[L(w,b)=\frac{1}{2}(\hat y-y)^2 \]

先说明一下,\(\hat y\) 上面那个小帽子(^),读作"hat"——就是英语里"hat"的意思,所以 \(\hat y\) 叫"y hat"。它专门用来表示模型的预测值,不是真实值。真实值用的是 \(y\),不加帽子。这个约定在机器学习里很常见:看到带帽子的符号,就知道那是模型猜出来的,不是真实答案。

\(\hat y=wx+b\) 塞进去:

\[L(w,b)=\frac{1}{2}(wx+b-y)^2 \]

\(L\) 就是损失(loss)。预测和真实值差得越远,损失越大。平方有两个实际好处:正负误差不会互相抵消;错得离谱的样本会被罚得更重。

前面的 \(\frac{1}{2}\) 没有什么神秘含义,主要是为了后面求导时把平方产生的 2 抵消掉。没有它也能训练,只是公式会多一个 2。

如果有 \(m\) 条训练数据,常用的整体损失写法是:

\[J(w,b)=\frac{1}{2m}\sum_{i=1}^{m}(wx_i+b-y_i)^2 \]

注意两个名字:单条样本的损失常记作 \(L\),整批数据的平均损失常记作 \(J\)。叫法不必死记,但要知道训练时真正想压低的是 \(J(w,b)\)


公式推一遍:更新式不是背出来的

从单条样本的损失开始:

\[L(w,b)=\frac{1}{2}(wx+b-y)^2 \]

令误差:

\[e = wx+b-y = \hat y-y \]

那么 \(L=\frac{1}{2}e^2\)

\(w\) 求导

链式法则直接上:

\[\frac{\partial L}{\partial w} =\frac{\partial L}{\partial e}\cdot\frac{\partial e}{\partial w} \]

其中:

\[\frac{\partial L}{\partial e}=e, \qquad \frac{\partial e}{\partial w}=x \]

所以:

\[\frac{\partial L}{\partial w}=(\hat y-y)x \]

\(b\) 求导

同样的套路:

\[\frac{\partial L}{\partial b} =\frac{\partial L}{\partial e}\cdot\frac{\partial e}{\partial b} =e\cdot 1 \]

因此:

\[\frac{\partial L}{\partial b}=\hat y-y \]

把单条样本推广到 \(m\) 条样本,整体损失的两个偏导为:

\[\frac{\partial J}{\partial w} =\frac{1}{m}\sum_{i=1}^{m}(\hat y_i-y_i)x_i \]

\[\frac{\partial J}{\partial b} =\frac{1}{m}\sum_{i=1}^{m}(\hat y_i-y_i) \]

现在终于能写更新规则:

\[w \leftarrow w-\eta\frac{\partial J}{\partial w} \]

\[b \leftarrow b-\eta\frac{\partial J}{\partial b} \]

\(\eta\) 就是学习率。

减号的含义是:导数指向上坡,我们反着走。

还有容易被忽略的细节:一轮更新里,新的 \(w\) 和新的 \(b\) 必须都基于旧参数计算。不要先更新 \(w\),再拿更新后的 \(w\) 去计算 \(b\) 的梯度。工程上通常先把梯度算好,再一起提交参数更新。


梯度下降的几种走法:SGD、Momentum、AdamW 都在改什么

最朴素的梯度下降,每次都照着当前梯度走:

\[\theta_{t+1}=\theta_t-\eta\nabla J(\theta_t) \]

\(\theta\) 是全部参数,\(\eta\) 是学习率。它简单、好理解,但也有两个老毛病:在狭长山谷里来回摆;遇到小批量数据的噪声时,方向会抖。

所以后面的优化算法,本质上不是把梯度下降推倒重来,而是在"怎么参考过去的方向、每个参数怎么迈步"上加经验。

SGD:小批量给方向,带点噪声往前走

SGD 常被叫作随机梯度下降。今天深度学习里说 SGD,实际往往是 mini-batch SGD:每次抽一小批数据算梯度。

\[g_t=\nabla J_{B_t}(\theta_t),\qquad \theta_{t+1}=\theta_t-\eta g_t \]

\(B_t\) 就是当前 batch。它比全量数据快得多,但 \(g_t\) 是近似值,曲线会抖。这个抖动不必恐慌,关键看整体趋势和验证集表现。

Momentum:别每次都从零开始起步

推购物车时,不会每秒都忘掉刚才的惯性。Momentum 也一样:把前几步的方向攒起来,再决定这一步怎么走。

\[v_t=\beta v_{t-1}+g_t \]

\[\theta_{t+1}=\theta_t-\eta v_t \]

\(\beta\) 通常接近 1,比如 0.9。连续几步都指向同一个方向,速度就会累积;左右乱晃的噪声,会被部分抵消。它对"狭长山谷"很有用。

AdaGrad 和 RMSProp:不同参数,别穿同一双鞋

有些参数的梯度一贯很大,有些一贯很小。所有参数共用一个步长,未必合适。AdaGrad 的思路是:过去梯度大的参数,后面走慢点。

\[r_t=r_{t-1}+g_t^2 \]

\[\theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{r_t}+\epsilon}g_t \]

问题是 \(r_t\) 一直累加,训练久了步长可能小到走不动。RMSProp 改成只记近期的平方梯度平均:

\[r_t=\rho r_{t-1}+(1-\rho)g_t^2 \]

它让每个参数都有自己的"局部步长",又不至于越训越死。

Adam 与 AdamW:目前常用,但不是免调参卡

Adam 把 Momentum 的一阶动量和 RMSProp 的二阶统计合在一起:

\[m_t=\beta_1m_{t-1}+(1-\beta_1)g_t \]

\[v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2 \]

训练开始时,\(m_t\)\(v_t\) 都从 0 起步,早期会偏小,所以要做偏差修正:

\[\hat m_t=\frac{m_t}{1-\beta_1^t},\qquad \hat v_t=\frac{v_t}{1-\beta_2^t} \]

最后更新:

\[\theta_{t+1}=\theta_t-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon} \]

不需要背全套公式才会用 Adam。记住它做了两件事就够了:它参考过去的平均方向,也根据各参数近期的波动大小调节步长。

现在训练 Transformer 等模型时,更常见的是 AdamW。它和 Adam 最大的工程差别,是把权重衰减(weight decay)从自适应梯度那一套里拆开:

\[\theta_{t+1}=(1-\eta\lambda)\theta_t-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon} \]

其中 \(\lambda\) 是权重衰减系数。直白说,它会定期把过大的权重往回拉一点,减少模型死记硬背的倾向。实务里,bias 以及 LayerNorm、BatchNorm 这类归一化层参数通常不做 weight decay。别把它和"把 loss 变小"混为一谈,它更像给参数加了一点刹车。

别急着背优化器选型表

小项目从 SGD 或 AdamW 开始都可以,关键是先把数据、损失函数、学习率和验证方式跑明白。优化器换得再勤,输入特征乱了、标签漏了、验证集切错了,也救不回来。


学习率:每一步到底迈多大

现在看更新式里唯一看起来像旋钮的东西:

\[\theta \leftarrow \theta-\eta\nabla J(\theta) \]

这里把所有参数收在向量 \(\theta\) 里。\(\eta\) 控制每次更新的步长。

学习率太小

比如站在半山腰,每次只往下蹭半毫米。方向没错,但训练会很慢。会看到 loss 一点点降,跑很久还没到一个能用的结果。

这类训练日志通常长这样:

step=1000  loss=0.8431
step=2000  loss=0.8417
step=3000  loss=0.8404

没报错,不代表训练健康。它只是慢得离谱。

学习率太大

步子太大就更刺激了。本来朝谷底走,结果一脚跨过去,又从另一边跨回来。损失上下乱跳,甚至越来越大。

step=100  loss=0.92
step=200  loss=8.31
step=300  loss=126.77
step=400  loss=nan

nan 不是模型突然顿悟,它大概率是数值爆了:参数太大,乘法和指数运算溢出,后面的结果全坏。

学习率合适时

loss 不一定每一步都严格下降,尤其是小批量训练时更不可能。但看一段区间,它应该总体往下,而且参数范数不该无缘无故飙升。

别把"选学习率"想成一次考试,存在唯一标准答案。它更像调火候:数据尺度、模型结构、batch size、优化器都会影响它。


为什么先做特征缩放,能让训练好很多

假设一个特征是面积,范围在 20 到 300;另一个特征是房龄,范围在 0 到 30。更极端一点,用户 ID、金额、点击次数可能量级差得更远。

这时损失函数的等高线会被拉得又细又长,像一条狭窄山谷。梯度下降每次容易在两侧来回撞,不敢走快。把学习率调大,它横着飞出去;调小,它纵向挪不动。

常见做法是标准化:

\[x' = \frac{x-\mu}{\sigma} \]

\(\mu\) 是均值,\(\sigma\) 是标准差。处理后,每个特征大致落在相近的尺度上,损失曲面会圆润不少,同一个学习率更容易工作。

这里有条线上事故级别的原则:训练集怎么算的 \(\mu\)\(\sigma\),推理时也必须原样使用。别在每次线上请求里重新算一套;那等于悄悄换了模型输入的坐标系。


一次完整训练到底在循环什么

把过程压成伪代码,大概是这样:

w, b = init_params()

for epoch in range(num_epochs):
    y_hat = X @ w + b
    error = y_hat - y

    grad_w = (X.T @ error) / len(X)
    grad_b = error.mean()

    w = w - learning_rate * grad_w
    b = b - learning_rate * grad_b

这段里有几件事值得盯紧:

  1. X @ w + b 是一次前向计算,拿参数做预测;
  2. error 是预测与真实值的差;
  3. grad_wgrad_b 是这轮应该往哪调的答案;
  4. 最后一行才真正改参数。

神经网络看起来层数更多,激活函数也更花,但核心循环没变:前向算预测,反向算梯度,用学习率更新参数。反向传播只是帮我高效地把"每个参数对最终误差的影响"算出来。


批量、随机、小批量:每次拿多少数据算梯度

上面公式默认一次把全部 \(m\) 条数据都算一遍,叫批量梯度下降(Batch Gradient Descent)。它的方向稳定,但数据一大,等半天才更新一次。

另一头是随机梯度下降(SGD):每次只拿一条样本更新。它快,更新也频繁,但抖得厉害。

工程里最常用的是小批量梯度下降(Mini-batch SGD):每次取 32、64、128 之类的一小批数据。它能用上 GPU 的并行能力,梯度又没那么吵。

别被"随机"两个字误导。随机不是瞎改参数,而是用一小部分样本近似全量数据给出的方向。近似会带来噪声,也正因为这个噪声,曲线常常是锯齿状往下走。


学习率不会一把梭:常见调法

一个实用起点:先拿对数尺度试几个值,比如 \(10^{-4}\)\(10^{-3}\)\(10^{-2}\)。不要一上来只试 0.001,然后把它当成宇宙常数。候选范围要跟着优化器、batch size、模型规模和任务走:SGD 常用的量级未必适合 AdamW,迁移学习的微调学习率也通常比从头训练更小。

观察训练曲线:

  • 很慢但稳定:适当增大学习率;
  • 大幅震荡、偶发尖峰:先减小学习率,再检查数据有没有异常值;
  • 一开始下降,随后长时间不动:可能需要学习率衰减,也可能模型和特征本身有问题;
  • 很快出现 naninf:先停,别硬跑。查学习率、输入范围、损失函数和混合精度。

常见的学习率衰减写法之一:

\[\eta_t = \eta_0\gamma^t \]

其中 \(\eta_0\) 是初始学习率,\(0<\gamma<1\)。前期走快一点,接近谷底时慢下来,免得来回冲。

实际项目里,很多人也会直接用 Adam、AdamW 之类的优化器。它们会根据历史梯度给不同参数调整步长,省心不少。但"有 Adam 就不用管学习率"是误会。学习率仍然是最敏感的超参数之一,调离谱了照样炸。


容易混掉的几个概念

梯度下降不是"自动找最优"

它只能按当前位置的局部信息往下走。在线性回归的平方损失里,目标函数是凸的,谷底基本就一个,所以它很省心。深度网络的损失面没这么老实,可能有鞍点、平坦区域和很多局部结构。

训练集 loss 下降,不等于模型真能用

在训练集上做题越做越熟,可能只是记住了题库。验证集 loss、业务指标、线上分布变化,都得看。

loss 和业务指标不是一回事

点击率模型的交叉熵变小,未必等于最终 GMV 一定涨。损失函数是训练时用的代理目标,业务效果还隔着数据质量、样本选择和决策链路。


补个刀:公式没错,线上照样能翻车

主线公式和更新方向正确;工程里真正高频的事故点是训练/推理特征不一致、恢复训练漏掉优化器或调度器状态、异常数据被静默跳过,以及未经灰度直接切全量。下面这些检查项,至少要在项目上线前过一遍。

写到这里,算法部分已经够用。但把训练代码塞进真实系统,坑往往不在求导,而在"看起来无关紧要"的工程细节。

1. 特征处理要版本化,不然训练和线上在说两种语言

训练时做了标准化、缺失值填充、类别编码,就要把这套规则连同统计量固化下来,并和模型版本绑定。

常见翻车现场:训练用 age = (age - 35.2) / 8.4,线上服务为了方便,按当天请求重新算均值。模型收到的数看上去合法,语义已经歪了,监控还不一定报警。

做法很朴素:把特征 schema、预处理参数、模型权重一起发布;在线上记录特征缺失率、分布分位数和未知类别占比。

2. loss 只是一盏仪表,不是全部仪表

训练 loss、验证 loss、梯度范数、参数范数、学习率、吞吐量、显存占用,这些最好一起打点。只盯一条 loss 曲线,常常是在事故之后才知道出事。

尤其要做两类告警:

  • nan/inf 检测:首次出现就隔离出问题 batch,并保存样本标识、随机种子和模型版本。不要默认"跳过继续跑",那会悄悄改变训练分布;应按预案失败退出,或由人工确认后处理;
  • 分布漂移检测:线上特征均值、分位数、缺失率偏离训练期太多时,不要等业务指标掉完才发现。

3. 重试不能让训练结果悄悄变样

分布式训练里,worker 重试、数据重放、断点续训都很常见。如果随机种子、数据分片顺序、优化器状态没保存好,同一个配置重跑两次可能得到两套模型。

不是说结果必须逐 bit 一致,GPU 并行本来就可能有非确定性;但至少要能解释差异来自哪里。训练任务需要留下代码 commit、数据快照、配置、容器镜像、随机种子、checkpoint、数据采样器状态/读取游标和评估报告。checkpoint 写入最好原子化,别留下一个看似存在、实际没写完的半截文件。

4. 学习率调度器要和恢复训练绑在一起

很多人保存 checkpoint 时只存模型参数 \(w\),恢复后学习率却回到初始值。前面已经收敛得差不多了,恢复那一刻又给了一个大步长,模型就被踢飞。

checkpoint 至少要包含:模型权重、优化器状态、学习率调度器状态、当前 epoch/step、混合精度的 scaler 状态。少存一个,恢复都可能不是原来的训练。

5. "加大 batch 就等于更快"经常是错觉

batch 变大,GPU 利用率可能更好,但梯度更新次数变少,泛化效果也可能变。直接把 batch 从 64 改到 1024,再照抄旧学习率,通常不稳。

如果要扩 batch,明确做对照实验:同一数据切分、同一总训练 token/样本量,记录学习率规则、收敛速度和验证集指标。别只看每秒样本数。

6. 给训练任务留一个"刹车"

一旦 loss 爆炸、梯度异常、数据质量突然恶化,任务不能傻跑几个小时把 GPU 烧成账单。

建议在训练框架里准备:梯度裁剪、异常 batch 隔离、自动保存最近可用 checkpoint、最大步数/成本上限,以及可以暂停、回滚和审计的发布开关。模型训练不是脚本跑完就结束,它也是生产系统的一部分。

7. 上线前先做特征契约和灰度,不要直接全量切流

优化器选得对,训练曲线也漂亮,不代表部署后就安全。上线服务要校验特征名称、类型、顺序、取值范围和默认值;缺了字段、枚举来了新值、单位从"元"变"分",应该明确拒绝、降级或报警,不能默默喂给模型。

发布时先走影子流量或小比例灰度:让新模型只预测不决策,或只影响一小部分请求。把业务指标、延迟、错误率和特征分布一起看,越过阈值就自动回滚。训练阶段在优化参数,发布阶段优化的是风险。


收个尾:先把"方向"和"步长"想明白

梯度下降的核心简而言之:梯度告诉我哪里是上坡,负梯度给出下坡方向;学习率决定我每次走多远。

线性回归把它讲得最清楚:

\[\text{参数新值}=\text{参数旧值}-\text{学习率}\times\text{损失对参数的导数} \]

以后看到神经网络训练、反向传播、AdamW、warmup、cosine decay 这些词,不用急着被吓住。它们大多是在围绕两件事打补丁:梯度算得准不准,步子迈得稳不稳。

把这两件事想透,再往后学就不会只是背名词了。
与君共勉~

posted @ 2026-08-12 18:41  昂流  阅读(30)  评论(0)    收藏  举报
//替换成自己路径的js文件 hhttp(s)://static.tctip.com/tctip-1.0.4.min.js