4000 个样本、10 维:单样本梯度走 8 遍,参数误差从 0.0438 降到 0.0143,中间还反弹过一次。 小批 32 只更新了 1000 次,误差从 0.0203 单调降到 0.0022。 差距不在次数,在步长:小批敢用 0.02,单样本只敢用 0.003。批越大,梯度方差越小, 步长能开得越大。 两条路都在逼近闭式解——因为最小二乘是凸的,只有一个最低点。
3.1 的闭式解要算 A 转置 A,这要扫全部 n 条样本、并且把它们同时放进内存。 n 到千万级、或者数据是流式到达时,这条路就走不通了。 这一节换成迭代法:每次只看一小部分样本,估一个粗糙的下降方向。 代价是要调步长,收益是内存和时间都变成线性。
一、闭式解卡在哪里
两道坎。第一道是内存:A 是 n 乘 b,A 转置 A 是 b 乘 b。b 不大时 A 转置 A 很小, 但算它要遍历整个 A,数据必须能一次读完。 数据在磁盘上、在数据库里、在别的机器的日志流里,就读不完。 第二道是时间:闭式解一次要花 O(n b 平方) 的乘法。梯度法每一步只要 O(b) 或 O(批大小 乘 b), 可以随时停下来看效果。 所以迭代法的真正优势不是「更快到终点」,是「随时可以停」。 数据源源不断进来时,你根本不需要一个终点。参数随着数据慢慢漂移,这本身就是想要的行为。 这也是第 15 章在线学习的前提:模型要能一条一条地更新。最小二乘做得到这件事,闭式解做不到, 因为每来一条新数据就要重算全部。
二、三种批大小
全量(批量)梯度:每步用所有样本算梯度。方向最准,但每步最贵, 而且一步都不能在数据没读完时迈出去。 单样本(随机)梯度:每步只用一个样本。方向噪声极大,但一步极便宜,n 个样本就走 n 步。 这就是「随机梯度下降」里「随机」的来源。 小批:每次用 32 到几百个样本。这是实践中的默认选择,原因有两个: 一次算一批能用上矩阵乘法的并行性;批内的平均把方差压下来了,步长可以开大。 Demo 把差别量成了一个数:同样的 8 遍数据,单样本误差 0.0143,小批 32 误差 0.0022。 而小批的更新次数只有单样本的 32 分之一。 「多走几步」不等于「走得更准」。
三、步长:这一节唯一的真功夫
步长的量纲要和梯度对上。对平方损失,梯度里有个 2 倍的因子,步长要相应缩小。 批大小变大时梯度是平均值、量级不变,但方差小了,所以步长可以开大。 这条规律在 Demo 里写得很直白:单样本 0.003,小批 32 用 0.02,差 6.7 倍。 所以「小批为什么好」的标准答案不是「省时间」,是「稳,所以敢走大步」。 步长还要衰减。固定的步长在最优点附近只能抖:梯度不为零(因为有噪声),参数就一直被推来推去。 衰减到零,抖动幅度才收敛。Demo 用的是 1 除以 (1 加 0.8 倍遍数),简单、够用。 工程上还有几个必做的动作:打乱顺序(不然样本的相关性会让梯度系统性偏斜)、 特征缩放(不然不同方向的条件数差太远,一个步长伺候不了所有方向)。 这两条不做,调步长会调到你怀疑人生。
四、实测:两条曲线
4000 个样本、10 维,闭式解的训练 MSE 是 0.0938。
| 遍历遍数 | 单样本更新 | 参数误差 | 小批 32 更新 | 参数误差 |
|---|---|---|---|---|
| 1 | 4000 | 0.0438 | 125 | 0.0203 |
| 2 | 8000 | 0.0479 | 250 | 0.0144 |
| 4 | 16000 | 0.0196 | 500 | 0.0052 |
| 8 | 32000 | 0.0143 | 1000 | 0.0022 |
| 看第二行:单样本的误差从 0.0438 涨到 0.0479。这不是 bug,是单样本梯度的噪声: | ||||
| 一条样本代表不了整体,它给的方向时准时歪。 | ||||
| 小批那一列从头到尾单调下降,8 遍误差 0.0022,比单样本小 6 倍。而它的更新次数只有 1000 次, | ||||
| 是单样本的 32 分之一。 | ||||
| 两条路都在往 0.0938 那个训练 MSE 靠,但这里比的是「参数误差」不是「损失」。 | ||||
| 参数误差能到 1e-3 量级,损失早就看不出差别了,和 3.1 那个现象一样。 |
五、什么时候不该用迭代法
b 小、n 中等、数据能一次读进内存时,闭式解永远优先。它没有步长要调、没有随机性、 结果完全可复现,跑一次就知道答案。 迭代法要调步长、要调批大小、要定衰减策略、还要设随机种子,这些都是成本。一个粗略的分界: b 在几千以内、n 在几十万以内,闭式解够用。 还有一类情况必须用迭代:模型根本不是线性的。层级模型、逻辑回归、支持向量机,全都没有闭式解, 只能迭代。 所以这一节讲的不只是「一种可选方案」,而是后面所有章节的通用解法。第 4 章往后的每一个算法, 本质都是「定一个损失,然后梯度下降」。
六、动手 Demo:小批梯度下降:单样本 8 遍误差 0.0143,小批 32 是 0.0022
4000 个样本、10 维(第 0 维是常数 1),真实参数随机生成,噪声 0.3。 先用闭式解算出标准答案,再跑两条随机梯度路线:批大小 1(步长 0.003)和批大小 32(步长 0.02), 都按 1 除以 (1 加 0.8 倍遍数) 衰减,每遍结束记录参数与闭式解的距离。
w_star = np.linalg.solve(X.T @ X, X.T @ y) # 闭式解当标准答案
def sgd(bs, epochs, lr0, seed=1):
r2 = np.random.default_rng(seed)
w = np.zeros(b)
for ep in range(epochs):
lr = lr0 / (1.0 + 0.8 * ep) # 步长衰减
order = r2.permutation(n) # 每遍重新打乱
for i in range(0, n, bs):
idx = order[i:i + bs]
g = 2.0 * X[idx].T @ (X[idx] @ w - y[idx]) / len(idx)
w -= lr * g
return w
h1 = sgd(1, 8, 0.003) # 单样本:步长必须小
h32 = sgd(32, 8, 0.02) # 小批:方差小了,步长可以开大
实跑输出:
--------------------------------------------------------------------
小批梯度下降:单样本 8 遍误差 0.0143,小批 32 是 0.0022
--------------------------------------------------------------------
数据 4000 个样本,10 维(第 0 维是常数 1)
闭式解 解一个 10 乘 10 的方程组,训练 MSE 0.0938
全量梯度 每步要扫 4000 个样本,算 X 转置乘 X
小批梯度 每步只看 1 个或 32 个样本,内存 O(b)
初始步长 单样本 0.003,小批 32 是 0.02
遍历遍数 单样本更新次数 参数误差 小批 32 更新次数 参数误差
-----------------------------------------------------------------
1 4000 0.0438 125 0.0203
2 8000 0.0479 250 0.0144
4 16000 0.0196 500 0.0052
8 32000 0.0143 1000 0.0022
**第一行是 1 遍之后:单样本更新了 4000 次,误差 0.0438。**
**一遍之内就更新了 4000 次——方向很糙,但架不住次数多。**
**8 遍之后误差 0.0143,趋势向下,但第二遍时反弹过。**
**反弹是单样本梯度自带的噪声:一条样本代表不了整体,它给的方向时准时歪。**
**所以步长必须小、还要衰减,不然到最优点附近只能抖。**
**小批 32 那边 8 遍只更新了 1000 次,误差 0.0022,一路单调下降。**
**次数少了 32 倍,最后的误差反而小 6 倍——因为每次的方向准得多。**
**更关键的是步长:小批 32 敢用 0.02,单样本只敢用 0.003。**
**批越大,梯度方差越小,步长就能开得越大,末端抖得也越轻。**
**这就是小批在实践中几乎总赢的原因——不只是省时间,是稳。**
**闭式解的 MSE 是 0.0938,两条路都在逼近它。**
**因为目标函数是凸的,最小二乘只有一个最低点,没有局部极小可以卡住。**
**所以「梯度法能不能到全局最优」在这类问题上是「能」,而深度学习里就不是了——那才是它真正难的地方。**
要说明:数据是我按线性模型**造的**;两条曲线的初始步长不同(0.003 与 0.02),都按 1 除以(1 加 0.8 倍遍数)衰减;
**我没测实际耗时**——这个规模下 Python 循环的开销会盖过算法差别,量出来的时间不可信。**这一节比的是「更新次数」这个与机器无关的量。**
注意 2.0 那个系数。平方损失的梯度是 2 乘 (预测减真值),漏掉它,实际步长就只有一半。
这不是理论洁癖:漏掉之后你会觉得「步长怎么调都慢」,然后把步长调到发散。
order = r2.permutation(n) 要在每一遍里重做。如果只打乱一次、后面按同样顺序遍历,
批内样本的构成会固定下来,梯度就带上系统性偏斜。这个小细节在真实项目里影响很明显。
两条曲线的初始步长不同,这是本节最容易被忽略的一点。很多人拿同一个步长比单样本和小批,
然后得出「小批更差」的结论。
那是因为步长是按单样本调的,公平的比较必须让两边各自取到合适的步长。
误差用「与闭式解的距离」而不是损失,是为了看得清。损失早就落在 0.09 附近,两条曲线重在一起;
参数距离还差着 6 倍,这个差才能说明谁更接近正确答案。
要说明:数据是我按线性模型造的,两条曲线的步长不同(0.003 与 0.02)。
我没测实际耗时,这个规模下 Python 循环的开销会盖过算法差别。
这一节比的是「更新次数」这个与机器无关的量。
30 秒实验
- 把单样本的步长从 0.003 改成 0.01,看第二遍那个反弹会不会更夸张,甚至发散。 这就是「步长过大」的形态:误差先降后炸。
- 把小批从 32 改到 256,步长从 0.02 提到 0.1。 8 遍之后误差会更小还是更大?想想每遍只剩 15 次更新这件事。
- 把打乱顺序去掉,再跑单样本 8 遍。 数据本身没有排序,所以差别不大;但如果先把 X 按第一列排序,差别就出来了。
常见误区
- 「随机梯度下降是近似算法,结果不如闭式解」。 对凸问题,它收敛到同一个最优点,只是慢。Demo 里 8 遍就到 1e-3 量级。 「近似」指的是中间过程的抖动,不是最终答案。
- 「批越大越好」。 批大到一定程度,每遍的更新次数就少到不够用; 而且梯度的方差早就降到底了,再大只是浪费算力。实践中几十到几百是常见区间, 超大模型才用几千。
- 「步长可以随便试」。 步长的量纲和问题强相关:特征缩放没做、梯度公式差了系数, 都会让「别人能用的步长」在你这儿发散。正确的做法是先算梯度范数,再按范数定步长。
- 「闭式解已经过时了」。 小规模问题上它仍然是最优选择:无超参、可复现、一次到位。很多论文的「新方法不如基线」, 就是因为基线用了最优的闭式解、而新方法还在调步长。
自测三问(点开对答案)
1. 为什么小批的步长可以比单样本大好几倍? —— **因为批内平均把梯度的方差压小了。**单样本梯度的噪声和真实梯度同量级,步长一大就被噪声带跑; **批平均之后噪声按批大小的平方根缩小,步长就有了余量。**这是小批在实践中几乎总赢的根本原因。 2. 闭式解的复杂度是 b 的三次方,那 b 大就不行了吗? —— **不一定。**b 的三次方在 b 到几千时还能接受,而且 n 那一头是线性的、可以用并行和分块解决。 **真正致命的是「数据读不完」**,那是内存和 I/O 的限制,不是算力的限制。 所以选闭式解还是迭代,先看数据能不能一次读进来,再看 b 有多大。 3. 如果训练损失曲线一直很平,怎么判断是收敛了还是卡住了? —— **看参数的变化量,不要看损失。** **目标函数在最优点附近是平的,损失曲线的分辨率不够。**另一个办法是拿一小批留出数据看误差, 它比训练损失敏感。3.1 那个「200 步损失已到位、参数还差 0.23」就是这个问题。第 3 章结束,最小二乘这条线走完了:有闭式解、有性质、有大规模版本。下一章开始给它加约束: 先限制解只能落在某个子空间里,再限制系数不能太大,最后讨论怎么选约束的强度。
- ☐ (本章完,下一节 4.1 4.1)