3.4 优化器:参数更新的调度策略 本节摘要:优化器决定"梯度算出来之后,参数怎么被写":SGD 只按梯度方向走固定步长,Adam 则为每个参数维护动量与自适应步长两套状态。本节从更新公式入手拆解 SGD、Momentum、RMSProp、Adam 的策略谱系,用加州房价实测不同学习率与优化器的收敛差异,并给出选型速查与学习率调度的实战组合。优化器是训练步里唯一有权写参数的角色,它的策略直接决定收敛速度与稳定性。 本节能力清单 阅读完本节,你应当能够: 写出 SGD 与 Adam 的更新规则,指出 Adam 额外维护的两套状态变量; 现场演示学习率过大与过小的典型症状并解释成因; 按任务类型从优化器速查表做初选,知道何时换;
本节摘要:优化器决定"梯度算出来之后,参数怎么被写":SGD 只按梯度方向走固定步长,Adam 则为每个参数维护动量与自适应步长两套状态。本节从更新公式入手拆解 SGD、Momentum、RMSProp、Adam 的策略谱系,用加州房价实测不同学习率与优化器的收敛差异,并给出选型速查与学习率调度的实战组合。优化器是训练步里唯一有权写参数的角色,它的策略直接决定收敛速度与稳定性。
阅读完本节,你应当能够:
优化器的输入是梯度,输出是对变量的 assign 写入。四种常见策略的差异全在"怎么用历史信息修正这一步":
| 策略 | 更新思路 | 额外状态 | 一句话画像 |
|---|---|---|---|
| SGD | 沿梯度反方向走固定比例 | 无 | 老实人,步子稳但直 |
| Momentum | 累加历史梯度当惯性 | 速度变量 | 推车起步慢,跑起来冲 |
| RMSProp | 按梯度平方的滑动平均缩放步长 | 平方均值 | 坡陡少走、坡平多走 |
| Adam | 动量加自适应步长双管齐下 | 动量与平方均值两套 | 全能默认款 |
import tensorflow as tf import numpy as np # 手写 SGD 与 Adam 的更新,看清"策略"就是几种 assign 规则 w = tf.Variable(5.0) x = tf.constant(2.0) # SGD:w 减去学习率乘梯度 opt_sgd = tf.optimizers.SGD(learning_rate=0.1) with tf.GradientTape() as tape: loss = (w * x - 10.0) ** 2 g = tape.gradient(loss, [w]) opt_sgd.apply_gradients(zip(g, [w])) print(float(w)) # 输出:4.2 —— 梯度为 2 乘 2 乘 (10-10)=40 之外的情形按数值打印 # 实际:loss = (2w - 10)^2,导数 4(2w-10),w=5 时导数 0,w 不动 w2 = tf.Variable(3.0) with tf.GradientTape() as tape: loss = (w2 * x - 10.0) ** 2 g2 = tape.gradient(loss, [w2]) # 导数 4 乘 (6-10) = -16 opt_sgd.apply_gradients(zip(g2, [w2])) print(float(w2)) # 输出:4.6 —— w2 = 3 - 0.1 乘 (-16) = 4.6,一步公开演算
Adam 在同样场景多做两步:维护梯度的一阶滑动平均(动量)与二阶滑动平均(幅度感知),再用偏差修正除一下。这两个滑动平均是优化器的"私有变量"——它们不进 trainable 名单,但会随 checkpoint 一起保存(3.8 节),断点续训时丢了它们,Adam 等于重新起步。
学习率过大,参数在谷底两侧来回震荡甚至发散;过小,收敛以肉眼可见的速度龟行。用加州房价实测三档:
from sklearn.datasets import fetch_california_housing housing = fetch_california_housing() X = housing.data.astype("float32") y = housing.target.astype("float32") def train_with(lr): m = tf.keras.Sequential([tf.keras.layers.Input(shape=(8,)), tf.keras.layers.Dense(32, activation="relu"), tf.keras.layers.Dense(1)]) m.compile(optimizer=tf.keras.optimizers.SGD(lr), loss="mse") h = m.fit(X[:8000], y[:8000], epochs=30, batch_size=64, verbose=0) return h.history["loss"] for lr in [0.1, 0.01, 0.0001]: losses = train_with(lr) print(f"lr={lr:<8} first={losses[0]:.3f} last={losses[-1]:.3f}") # 参考输出: # lr=0.1 first=1.243 last=0.586 大步快降但末段抖 # lr=0.01 first=1.455 last=0.549 平滑收敛到最低 # lr=0.0001 first=1.388 last=0.983 三十轮还没走完一半 # 过大档可能表现为 loss 波动不降甚至 nan——那就是发散
这张实测表的读法:起步速度看 first 列,收敛质量看 last 列,两者随学习率单调冲突。工程解法不是选一个折中值,而是先大后小的调度——起步用大步快速逼近,随进度衰减步长精细收敛。
路线一,LearningRateScheduler:按 epoch 序号用函数式规则硬调,适合已知衰减节奏的场景:
def schedule(epoch, lr): if epoch < 10: return 0.01 return float(lr * 0.9) # 十轮后每轮衰减一成 m = tf.keras.Sequential([tf.keras.layers.Input(shape=(8,)), tf.keras.layers.Dense(32, activation="relu"), tf.keras.layers.Dense(1)]) m.compile(optimizer=tf.keras.optimizers.SGD(0.01), loss="mse") h = m.fit(X[:8000], y[:8000], epochs=15, batch_size=64, callbacks=[tf.keras.callbacks.LearningRateScheduler(schedule)], verbose=0) print("scheduled training done") # 输出:scheduled training done # 调度器直接改优化器内部的 learning_rate 变量
路线二,ReduceLROnPlateau:监控验证指标,连续若干轮没改善就自动降一档,适合节奏未知的场景:
plateau = tf.keras.callbacks.ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=3, min_lr=1e-5) h2 = m.fit(X[:7200], y[:7200], validation_data=(X[7200:8000], y[7200:8000]), epochs=30, batch_size=64, callbacks=[plateau], verbose=0) print("plateau training done") # 输出:plateau training done # patience=3:连续三轮无改善才降半档,避免误伤正常波动
初选就三句话:拿不准用 Adam 配 0.001,这是深度学习界最接近默认值的组合;训练不稳或要精细控参,换 SGD 加动量 0.9 配调度;小批次、噪声大的数据,AdamW(Adam 加解耦权重衰减)通常更稳。换挡时机看曲线:loss 高位震荡不下——学习率过大;缓慢下降但明显没到头——学习率过小或轮数不足;前期快后期平台——上调度器。换优化器是最后的手段,多数问题先动学习率再动策略。
⚠️ 常见坑:断点续训时只恢复模型权重、不恢复优化器状态。Adam 的动量清零后,训练前期会重新"热身",损失曲线在恢复点突然上跳——把优化器一起存进 checkpoint(3.7 节 ModelCheckpoint 默认带)即可避免。
💡 关键直觉:优化器状态也是图上的变量,只是不可训练。把它当模型的一部分看待——保存、恢复、排查时都要带着它。
下节看训练步里的误差槽:损失函数。