机器学习_第二篇_梯度下降和学习率
前言:
第一次看到"梯度下降"这四个字,下意识觉得:完了,要微积分了。
其实不用慌。梯度下降干的事很朴素:被训练的模型现在答得不太对,看看"往哪个方向改,错得会少一点",然后挪一小步。重复很多次,直到误差小到能接受。(机器学习、数值分析不分家)
它不保证一步到位。它更像摸着黑下山:每走一步,先判断脚下地面朝哪边倾;然后朝下坡方向走一点。步子太大,可能直接跨过山谷;太小,又会走到怀疑人生。这就是学习率。
先把问题说透:梯度下降到底在干什么,就是降低误差。
先回答最核心的问题:梯度下降是什么,它要解决什么问题,它调整的又是什么。
假设我瞎猜了一组参数,算出来的预测值和真实答案差了一大截。我肯定想改参数,但改多少?往哪边改?这就是梯度下降要回答的。
它做的事情很简单:先用当前参数预测,看看总共错了多少;再判断每个参数往哪边挪,误差会降;最后只挪一小步。 然后继续预测、判断、挪步,反复很多轮。
它优化的不是输入数据,也不是训练标签。训练数据是题目,模型参数才是我们反复调整的旋钮。在线性回归里,旋钮是 \(w,b\);在神经网络里,旋钮就是每一层的权重和偏置。好比战报接线员通过旋转按钮,让电报更加清晰。
把名字拆开看会更清楚:
- "梯度"是哪个梯度?是损失函数对模型参数的梯度。不是对 \(x\) 求,也不是对标签 \(y\) 求。它回答的是:当前参数轻轻改一点,损失会往哪里、以多快的速度变;
- "下降"降的是什么?要降的是损失,不是参数本身。参数有的会变大,有的会变小,只要总损失在往下走就对了;
- "调整了什么"?每一步实际改的是参数值,比如把 \(w\) 从 4.0 改成 4.03,把 \(b\) 从 20 改成 20.01。改多少由学习率决定。
所以,梯度下降不是"把梯度降下来"。它是利用梯度当导航,去把损失降下来。
先记住这个总式子,不急着看懂每个符号:
后面讲导数、偏导数和 \(\nabla\),就是在把这个式子里的"梯度"拆开。
先从一个不那么玄的例子说起
假设我在做一个房价预测器。先偷个懒,只看面积:
这里:
- \(x\) 是面积;
- \(\hat y\) 是模型猜的房价;
- \(w\) 是每增加一平方米,预测房价大约加多少;
- \(b\) 是截距,理解成"面积为 0 时模型给出的起点"就行。
手里有一条样本:面积 \(x=50\),真实房价 \(y=300\)。模型目前参数是 \(w=4, b=20\),那它的预测值为:
猜低了,少猜了 80。
问题来了:\(w\) 和 \(b\) 到底该怎么调?往上加多少合适?人眼盯着一两条数据还行,数据有十万条时,总不能靠感觉拧参数。梯度下降就是替我算这件事的办法。
快速大白话导数
在研究怎么调 \(w\) 和 \(b\) 前,得先认识导数。它不是一套专门用来折磨人的符号,问的问题其实很生活化:我把一个量轻轻推一下,结果会变多少?
比如打车计费 \(f(x)=3x+10\),\(x\) 是里程。多开 1 公里,车费多 3 元。这个"每多一点输入,输出大约多多少",就是变化率;对这条直线来说,变化率始终是 3。
写成公式,导数是:
先别被极限吓到。分子是在算"结果变了多少",分母是在算"输入变化了多少";把 \(\Delta x\) 想得非常小,得到的就是当前位置最细的变化趋势。
回到损失函数。先只盯住 \(w\),把 \(b\) 暂时按住不动。不同的 \(w\) 会得到不同的 \(J(w)\),横轴是 \(w\),纵轴是损失。我站在曲线上某个位置,导数告诉我的就是:\(w\) 往右挪一点,损失接下来是涨还是跌,涨跌有多快。
- 导数大于 0:往右走是在上坡,\(w\) 应该减小;
- 导数小于 0:往右走反而在下坡,\(w\) 应该增大;
- 导数接近 0:坡度已经很平,可能在谷底、山顶或鞍点附近。在线性回归的平方损失这类凸问题里,才可以把它看成接近最优。
这就是导数在优化里的作用:它不是直接报出"正确参数是多少",而是给我当前这一步的路况。
有两个旋钮时:偏导数就是"其他先别动"
真实模型很少只有一个参数。这里至少有 \(w\) 和 \(b\):
现在的问题变成:我到底先看 \(w\) 的影响,还是先看 \(b\) 的影响?
偏导数则是将“控制变量”思想数学化。规矩很简单:想研究谁,就只动谁;其他变量先钉住。
- \(\frac{\partial J}{\partial w}\):先把 \(b\) 当常数,只看 \(w\) 动一点会怎样;
- \(\frac{\partial J}{\partial b}\):先把 \(w\) 当常数,只看 \(b\) 动一点会怎样。
符号里的 \(\partial\) 可以先理解成"部分"。它不是新魔法,只是标记:这次只问其中一个旋钮。
拿一个更直白的函数热热身:
对 \(a\) 求偏导时,\(b\) 就像一张写死的便签:
对 \(b\) 求偏导时,\(a^2\) 是常数:
模型参数再多,道理也没变。每个偏导数都在回答:这个参数此刻动一点,对总误差有多大影响?
梯度:把每个旋钮的路况打成一包
有了两个偏导数,梯度就没那么神秘了。它只是把所有参数的偏导数按顺序装进一个向量:
这里左边那个倒三角 \(\nabla\),读作"nabla",中文里通常直接叫它"梯度算子"。可以把它先当成一个打包动作:它看到 \(J(w,b)\),就把"对 \(w\) 的偏导"和"对 \(b\) 的偏导"算出来,按顺序装进一个向量。
它不是一个具体数字,也不是损失值。\(\nabla J\) 的结果是"每个参数各自该怎么调"的一组数。要是模型有 100 万个可训练参数,这个向量通常就有 100 万个分量。框架里常看到的 loss.backward(),干的就是沿计算图把这包梯度算出来并放到各参数的 grad 里;它还没有改参数,真正改参数的是后面的 optimizer.step()。
可以把梯度当成一张导航纸条:第一行写 \(w\) 该往哪边调,第二行写 \(b\) 该往哪边调。数学上,梯度指向损失上升最快的方向;我想把损失压下去,就反着它走。
这就把"梯度下降"拆开了:梯度负责指方向,下降表示朝负梯度方向更新。
先定规则:错多少,怎么算
模型不能只说"我觉得不准",得把"不准"写成一个能计算的数。在线性回归里,最常用的是平方误差。
对单条样本:
先说明一下,\(\hat y\) 上面那个小帽子(^),读作"hat"——就是英语里"hat"的意思,所以 \(\hat y\) 叫"y hat"。它专门用来表示模型的预测值,不是真实值。真实值用的是 \(y\),不加帽子。这个约定在机器学习里很常见:看到带帽子的符号,就知道那是模型猜出来的,不是真实答案。
把 \(\hat y=wx+b\) 塞进去:
\(L\) 就是损失(loss)。预测和真实值差得越远,损失越大。平方有两个实际好处:正负误差不会互相抵消;错得离谱的样本会被罚得更重。
前面的 \(\frac{1}{2}\) 没有什么神秘含义,主要是为了后面求导时把平方产生的 2 抵消掉。没有它也能训练,只是公式会多一个 2。
如果有 \(m\) 条训练数据,常用的整体损失写法是:
注意两个名字:单条样本的损失常记作 \(L\),整批数据的平均损失常记作 \(J\)。叫法不必死记,但要知道训练时真正想压低的是 \(J(w,b)\)。
公式推一遍:更新式不是背出来的
从单条样本的损失开始:
令误差:
那么 \(L=\frac{1}{2}e^2\)。
对 \(w\) 求导
链式法则直接上:
其中:
所以:
对 \(b\) 求导
同样的套路:
因此:
把单条样本推广到 \(m\) 条样本,整体损失的两个偏导为:
现在终于能写更新规则:
\(\eta\) 就是学习率。
减号的含义是:导数指向上坡,我们反着走。
还有容易被忽略的细节:一轮更新里,新的 \(w\) 和新的 \(b\) 必须都基于旧参数计算。不要先更新 \(w\),再拿更新后的 \(w\) 去计算 \(b\) 的梯度。工程上通常先把梯度算好,再一起提交参数更新。
梯度下降的几种走法:SGD、Momentum、AdamW 都在改什么
最朴素的梯度下降,每次都照着当前梯度走:
\(\theta\) 是全部参数,\(\eta\) 是学习率。它简单、好理解,但也有两个老毛病:在狭长山谷里来回摆;遇到小批量数据的噪声时,方向会抖。
所以后面的优化算法,本质上不是把梯度下降推倒重来,而是在"怎么参考过去的方向、每个参数怎么迈步"上加经验。
SGD:小批量给方向,带点噪声往前走
SGD 常被叫作随机梯度下降。今天深度学习里说 SGD,实际往往是 mini-batch SGD:每次抽一小批数据算梯度。
\(B_t\) 就是当前 batch。它比全量数据快得多,但 \(g_t\) 是近似值,曲线会抖。这个抖动不必恐慌,关键看整体趋势和验证集表现。
Momentum:别每次都从零开始起步
推购物车时,不会每秒都忘掉刚才的惯性。Momentum 也一样:把前几步的方向攒起来,再决定这一步怎么走。
\(\beta\) 通常接近 1,比如 0.9。连续几步都指向同一个方向,速度就会累积;左右乱晃的噪声,会被部分抵消。它对"狭长山谷"很有用。
AdaGrad 和 RMSProp:不同参数,别穿同一双鞋
有些参数的梯度一贯很大,有些一贯很小。所有参数共用一个步长,未必合适。AdaGrad 的思路是:过去梯度大的参数,后面走慢点。
问题是 \(r_t\) 一直累加,训练久了步长可能小到走不动。RMSProp 改成只记近期的平方梯度平均:
它让每个参数都有自己的"局部步长",又不至于越训越死。
Adam 与 AdamW:目前常用,但不是免调参卡
Adam 把 Momentum 的一阶动量和 RMSProp 的二阶统计合在一起:
训练开始时,\(m_t\) 和 \(v_t\) 都从 0 起步,早期会偏小,所以要做偏差修正:
最后更新:
不需要背全套公式才会用 Adam。记住它做了两件事就够了:它参考过去的平均方向,也根据各参数近期的波动大小调节步长。
现在训练 Transformer 等模型时,更常见的是 AdamW。它和 Adam 最大的工程差别,是把权重衰减(weight decay)从自适应梯度那一套里拆开:
其中 \(\lambda\) 是权重衰减系数。直白说,它会定期把过大的权重往回拉一点,减少模型死记硬背的倾向。实务里,bias 以及 LayerNorm、BatchNorm 这类归一化层参数通常不做 weight decay。别把它和"把 loss 变小"混为一谈,它更像给参数加了一点刹车。
别急着背优化器选型表
小项目从 SGD 或 AdamW 开始都可以,关键是先把数据、损失函数、学习率和验证方式跑明白。优化器换得再勤,输入特征乱了、标签漏了、验证集切错了,也救不回来。
学习率:每一步到底迈多大
现在看更新式里唯一看起来像旋钮的东西:
这里把所有参数收在向量 \(\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、金额、点击次数可能量级差得更远。
这时损失函数的等高线会被拉得又细又长,像一条狭窄山谷。梯度下降每次容易在两侧来回撞,不敢走快。把学习率调大,它横着飞出去;调小,它纵向挪不动。
常见做法是标准化:
\(\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
这段里有几件事值得盯紧:
X @ w + b是一次前向计算,拿参数做预测;error是预测与真实值的差;grad_w、grad_b是这轮应该往哪调的答案;- 最后一行才真正改参数。
神经网络看起来层数更多,激活函数也更花,但核心循环没变:前向算预测,反向算梯度,用学习率更新参数。反向传播只是帮我高效地把"每个参数对最终误差的影响"算出来。
批量、随机、小批量:每次拿多少数据算梯度
上面公式默认一次把全部 \(m\) 条数据都算一遍,叫批量梯度下降(Batch Gradient Descent)。它的方向稳定,但数据一大,等半天才更新一次。
另一头是随机梯度下降(SGD):每次只拿一条样本更新。它快,更新也频繁,但抖得厉害。
工程里最常用的是小批量梯度下降(Mini-batch SGD):每次取 32、64、128 之类的一小批数据。它能用上 GPU 的并行能力,梯度又没那么吵。
别被"随机"两个字误导。随机不是瞎改参数,而是用一小部分样本近似全量数据给出的方向。近似会带来噪声,也正因为这个噪声,曲线常常是锯齿状往下走。
学习率不会一把梭:常见调法
一个实用起点:先拿对数尺度试几个值,比如 \(10^{-4}\)、\(10^{-3}\)、\(10^{-2}\)。不要一上来只试 0.001,然后把它当成宇宙常数。候选范围要跟着优化器、batch size、模型规模和任务走:SGD 常用的量级未必适合 AdamW,迁移学习的微调学习率也通常比从头训练更小。
观察训练曲线:
- 很慢但稳定:适当增大学习率;
- 大幅震荡、偶发尖峰:先减小学习率,再检查数据有没有异常值;
- 一开始下降,随后长时间不动:可能需要学习率衰减,也可能模型和特征本身有问题;
- 很快出现
nan或inf:先停,别硬跑。查学习率、输入范围、损失函数和混合精度。
常见的学习率衰减写法之一:
其中 \(\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. 上线前先做特征契约和灰度,不要直接全量切流
优化器选得对,训练曲线也漂亮,不代表部署后就安全。上线服务要校验特征名称、类型、顺序、取值范围和默认值;缺了字段、枚举来了新值、单位从"元"变"分",应该明确拒绝、降级或报警,不能默默喂给模型。
发布时先走影子流量或小比例灰度:让新模型只预测不决策,或只影响一小部分请求。把业务指标、延迟、错误率和特征分布一起看,越过阈值就自动回滚。训练阶段在优化参数,发布阶段优化的是风险。
收个尾:先把"方向"和"步长"想明白
梯度下降的核心简而言之:梯度告诉我哪里是上坡,负梯度给出下坡方向;学习率决定我每次走多远。
线性回归把它讲得最清楚:
以后看到神经网络训练、反向传播、AdamW、warmup、cosine decay 这些词,不用急着被吓住。它们大多是在围绕两件事打补丁:梯度算得准不准,步子迈得稳不稳。
把这两件事想透,再往后学就不会只是背名词了。
与君共勉~

浙公网安备 33010602011771号