Regularization and Optimization —— 损失函数的完整形式、L1/L2 正则化、梯度下降与 SGD、动量、RMSProp、Adam 与学习率调度
老师首先用几分钟回顾了第 2 讲的主线。图像分类(image classification)是计算机视觉的核心任务:给定一张输入图像,模型要从一组固定标签(如猫、狗、鸟、鹿、卡车)中分配出正确的标签。这个任务之所以困难,源于一系列挑战:人类感知到的"猫"与计算机存储的多维数组(张量)之间存在语义鸿沟(semantic gap);此外还有光照变化(Illumination,不同位置的像素强度不同,部分物体处于阴影中)、形变(deformation)(猫天生柔软易变,会以各种姿势扭曲)、遮挡(occlusion)(猫藏在垫子下,只露出尾巴,人类能推断出是猫)、背景杂乱(background clutter)(物体与背景融为一体)、类内差异(intraclass variation)(同类物体外观差异巨大却必须归为一类)等等。正因为逻辑规则(if-else)无法穷举这些变化,我们才转向数据驱动方法(data-driven approach)。
最简单的数据驱动模型是 k 近邻(k-nearest neighbors, KNN):对一个新的数据点,找训练集中距离它最近的 \(k\) 个点,取多数标签作为预测。超参数 \(k\)(例如 1 或 5)的选择需要把数据划分为训练集、验证集和测试集:在验证集上尝试不同 \(k\) 值、绘制准确率曲线,选最高的那个;测试集则保留到最后,用于评估模型在从未见过的数据上的表现。
针对上讲中同学们对距离度量的困惑,老师再次对比了两种最常用的距离:L1 距离(曼哈顿距离,Manhattan distance)与L2 距离(欧氏距离,Euclidean distance)。L2 就像我们日常理解的几何直线距离;而 L1 只能沿坐标轴方向"横平竖直"地移动,不能走对角线。这带来一个直观差别:在 L1 下,到原点等距的点构成一个菱形——例如从原点出发向上 0.5 再向右 0.5,总距离是 1,与沿正方向直走 1 的距离相同;而在 L2 下,等距点集是一个圆,因为可以直接沿直线走。
随后老师回顾了线性分类器:一张 \(32 \times 32 \times 3\) 的图像被展平成 3072 维的列向量 \(x\)(\(32 \times 32 \times 3 = 3072\),即每个空间位置有红、绿、蓝三个像素值),乘以权重矩阵 \(W\)(若为 10 类,则 \(W\) 的形状为 \(10 \times 3072\)),再加上偏置向量 \(b\)(每类一个,共 10 维),得到 10 个类别的得分。理解这个模型有三种等价的视角:
有了得分之后,如何数学化地刻画"这组得分好不好"?这就是损失函数(loss function)。给定数据集 \(\{(x_i, y_i)\}_{i=1}^{N}\),其中 \(x_i\) 是第 \(i\) 个训练样本、\(y_i\) 是其真实标签,总损失是每个样本损失的平均:
上讲已经详细讲过 Softmax 损失(交叉熵损失,cross-entropy loss),这里老师简要回顾:对每个样本的得分做指数化(使其全部为正),再除以总和归一化成概率分布(所有值相加为 1),最后取正确类别概率的负对数作为该样本的损失。预测正确类别概率越低,损失越高;预测得越自信且正确,损失越低。
但本讲的重点是公式中加粗的那个新成员:正则化项 \(R(W)\)。\(L_i\) 那部分被称为数据损失(data loss),衡量模型预测与训练数据的吻合程度——我们希望它低,因为它低说明模型拟合训练数据好。而正则化项的设计意图恰恰相反:
其中的 \(\lambda\) 是正则化强度(regularization strength),又是一个超参数,同样用训练/验证集来调优。它取 0 到无穷之间的浮点数:\(\lambda = 0\) 表示完全没有正则化,\(\lambda\) 越大则正则化越强,是一个控制"多想阻止模型拟合训练数据"的可调旋钮。
老师用经典的玩具例子建立直觉:假设要拟合平面上一组 \((x, y)\) 数据点,候选模型有两个。\(f_1\) 是一条穿过所有数据点的复杂曲线(比如高阶多项式),训练/数据损失几乎为零;\(f_2\) 是一条不经过每个点、但有更简单形状的曲线。直觉告诉我们,在新数据上 \(f_2\) 很可能是更好的模型——事实也如此。正则化捕捉的正是这种直觉:不要过度拟合(overfit)训练数据,一个拟合得没那么完美但更简单的模型,往往泛化(generalize)更好。
这与哲学和科学发现中的奥卡姆剃刀(Occam's razor)原则一脉相承:当有多个竞争假设时,应先采用最简单的那个;只有确定它错了,才逐步尝试更复杂的。这是"正则化为什么有用"的一个高层次直觉。
两种最简单的正则化都是对权重矩阵 \(W\) 直接施加惩罚:
L2 把 \(W\) 中每个元素平方后求和;L1 则取绝对值后求和。一字之差,行为却大不相同:
有学生追问"L1 为什么偏好稀疏"以及"'推向 0'是什么意思"。老师解释:整个训练就是在最小化总损失,即数据损失与正则化项的加权和。这是一个权衡(trade-off):如果某些权重对模型表现(数据损失)贡献不大,而把它清零又能降低正则化项,那么总损失更低的解就会被优化程序选中。
老师现场做了两道投票题。设 \(x = [1, 1, 1, 1]^T\),两个权重向量都与 \(x\) 点积得分为 1(数据损失完全相同):
L2 更喜欢哪个?——多数同学正确选了 \(W_2\)。\(W_2\) 的 L2 惩罚是 \(4 \times (0.25)^2 = 4 \times 1/16 = \mathbf{1/4}\);而 \(W_1\) 是 \(1^2 = \mathbf{1}\)。正则化损失低了 4 倍,所以 L2 明确偏好更扩散的 \(W_2\)。
L1 更喜欢哪个?——这是一道"陷阱题":两个的 L1 惩罚都是 1(\(W_2\) 是 \(0.25 \times 4 = 1\),\(W_1\) 就是 1 本身),从损失数值上看完全等价。不过在真实训练的动态过程中,由于上述"压零"机制,L1 实际更可能收敛到类似 \(W_1\) 的稀疏解。若把例子中的 1 改成 0.9 之类不对称的数值,L1 就会明确偏向把小权重清零的那个。
老师最后指出:正则化是一个活跃的研究领域,每年都有新论文,还有会改变模型层结构的复杂形式(如 dropout),本课程只覆盖其中一小部分。它们都遵循同一个核心思想——训练上更差,测试上更好。
本讲的损失回顾聚焦在 Softmax 上。之所以 Softmax 成为分类中最常用的损失,是因为它作为一个函数有极好的性质:把任意一组浮点数(得分)转换成一个合法的概率分布——先通过指数化保证全为正,再除以总和保证相加为 1。得分越高,对应概率越大;若某个得分是很高的正数而其余都是很大的负数,Softmax 输出会接近"1 配一串 0"。与上讲介绍的 SVM(合页损失)相比:SVM 只关心正确类别得分是否比其他类别高出安全边际,一旦满足就不再施压;而 Softmax 从不停歇,总是试图把正确类别的概率推向 1,因而对得分的大小更敏感,也给出可解释的概率输出。
课程后续会讲到现代深度学习中更丰富的正则化手段:Dropout(训练时随机丢弃神经元)、数据增强(data augmentation)(随机裁剪、翻转、颜色抖动等扩充训练分布)、标签平滑(label smoothing)、早停(early stopping),以及训练大模型时代的权重衰减(weight decay)。它们的共同点仍然是对训练损失"下手",换取泛化能力的提升。另外,"L2 正则化与贝叶斯先验的高斯对应、L1 对应拉普拉斯先验"这一视角,也在统计学中把正则化解释为最大后验估计(MAP)。
现在我们知道了如何给任意 \(W\) 打分(总损失),接下来的问题是:如何找到最好的 \(W\)?这就是优化(optimization),本讲后半部分的主题。
描述优化时人们常用损失地形(loss landscape)的比喻:把竖直方向(z 轴)看作损失值,把模型参数(此例中是两个参数)看作地形上的 x、y 坐标,我们是在这片山地中寻找最低点的人。老师指出这个比喻有一处不精确——人类可以放眼望去看远处的谷底,而优化算法不能。但只要把这个人想象成蒙着眼睛(blindfolded):看不见任何全局信息,只能用脚感受脚下这一点的坡度——这个比喻就变得极其准确。这种"感受局部坡度、向下坡方向走"的思路,正是本课程乃至整个深度学习训练所有模型的根本方式。
最朴素的想法:写一个 for 循环,随机尝试 1000 个不同的 W,保留损失最小的那个。这显然在数学上不严谨,但确实比随机猜测基线要好——在 CIFAR-10(就是有青蛙、汽车等 10 个类别的数据集)上能得到约 15.5% 的准确率。而现代深度学习在该数据集上已"基本解决"问题,达到 99.7% 的准确率。所以随机搜索不算太差,但离好还差得很远。
一维情形下,斜率就是微积分里的导数(derivative),用极限定义:
在多维情形下,导数推广为梯度(gradient):对每一个维度分别做上述极限,得到一个向量,给出每个方向上的坡度。任意方向上的斜率等于梯度与该方向单位向量的点积;特别地,最速上升方向是梯度本身,最速下降方向是负梯度(negative gradient)。梯度指向上坡,负梯度指向下坡——这就是我们该走的方向。
计算梯度有两种途径:
(1)数值梯度(numerical gradient):直接套用极限定义,取一个很小的 \(h\)(例如 0.0001),计算 \([f(x+h) - f(x)] / h\) 的近似值。对 \(W\) 的每一个元素重复这个过程。它的优点是非常容易写、不易出错;缺点则很致命:慢(要对每个参数各算一次损失,循环整个权重矩阵)而且近似(不是精确导数,浮点运算还会带来不可忽视的误差)。
(2)解析梯度(analytic gradient):损失整体上是 \(W\)、\(x_i\)、\(y_i\) 的函数——含着指数、对数等运算。把 \(x\) 和 \(y\) 看作常数,用微积分(链式法则等)直接推导 \(dW = \nabla_W L\),得到精确且快速的梯度公式。缺点是:如果你从零推导并实现新代码,可能写出 bug。
梯度检查(gradient check)是弥补解析梯度易错这一缺陷的标准做法:用数值方法(小 \(h\) 版本)计算梯度,与解析梯度比对,确认二者在相同数量级上。这是检验实现正确性的利器,也是本课作业中的必备环节。老师还回答了"如果某个更好的损失函数不可微怎么办":一般很难构造出"更好却处处不可微"的损失;如果损失在整个定义域上基本处处不可微、像一堆互不连通的点,那么沿最速下降方向走也未必能到达好解。总结(TLDR):凸(convex)函数与梯度下降配合得很好;非凸、不可微的函数则可能让这类方法步错方向。
基本算法极其简洁:循环地计算当前 \(W\) 处的梯度,然后沿负梯度方向迈出一步:
while True:
dW = compute_gradient(W, X, y) # 梯度
W = W - learning_rate * dW # 沿负梯度走 step_size
公式即 \(W \leftarrow W - \alpha \cdot \nabla_W L\),其中 \(\alpha\) 是步长(step size)/ 学习率(learning rate)——沿下坡方向走多远。可视化时常从山顶俯瞰损失地形(紫色代表高处、红色代表谷底),优化轨迹是一步步折线走向谷底。有学生问:步长固定吗?是固定的,但由于越接近谷底梯度越小,固定学习率乘上更小的梯度,有效步幅会自然变小。又有学生问:什么时候停止?两种常见做法:预设迭代次数;或设定容差——当损失每轮只下降 \(10^{-5}\)、\(10^{-9}\) 这种量级、几乎不再改善时就停止。
完整损失要对整个训练集的 \(N\) 个样本求平均,数据集一大,每走一步都要算完所有样本,代价高昂。随机梯度下降(Stochastic Gradient Descent, SGD)的解决办法是:每步只抽取数据的一个子集,称为小批量(mini-batch / batch),比如每批 256 个样本,只对这 256 个样本估计梯度并更新。"随机"之名正来自每步抽取的都是随机子集。实践中人们通常不纯随机抽取,而是保证轮流用遍数据集所有样本再循环回来,每完整过一遍数据称为一个epoch(轮)。
以等高线图(level sets:同一条曲线上损失处处相等)俯视损失地形时,SGD 有一系列著名的问题。老师逐条讲解并频繁与学生互动:
想象一个狭长山谷:垂直于谷底的方向非常陡峭,沿谷底的方向非常平缓,而我们想沿着谷底前进。此时 SGD 会出问题:过冲(overshoot)——在陡峭方向上来回震荡,若学习率又偏大,甚至可能每一步都弹得更远、直接"震荡出山谷"(学习率太大时确实会发生)。即使学习率没大到发散,也会把大量时间花在上下抖动上,朝谷底中心的实质进展很慢。数学上,这种损失函数被称为具有高条件数(high condition number)——即 Hessian 矩阵(二阶导数矩阵)最大与最小奇异值之比很大:上下方向的二阶导很大,左右方向很小。
如果损失曲面上存在局部极小点——走到那里梯度为 0,没有方向可走,优化就卡死了;或者梯度极小,只能在附近来回小幅震荡。还有鞍点(saddle point):因形似马鞍而得名。在鞍点中心,所有方向的梯度都是 0——它沿一个方向是谷底、沿另一个方向却是山顶顶点——尽管再走远一点就能大幅下降,梯度却给不出任何指引。老师特别强调了一个实证结论:模型维度越高(参数越多),鞍点出现得越频繁,这在深度网络中是比局部极小更普遍的麻烦(Dauphin et al. 的论文专门讨论了鞍点的普遍性)。
SGD 每步只看训练集的一个子集而非全部,因此每一步的方向只是对"真梯度"的带噪估计——轨迹整体朝极小值前进,但每一步都带有随机偏差。
关于局部极小与鞍点,后续研究(如 choromanska et al. 与 LoiLi 等对损失地形的可视化工作)表明:在高维神经网络损失面上,糟糕的局部极小其实相当罕见,大量驻点更接近鞍点;现代大模型的"局部极小"往往在测试性能上与全局结构差别不大。SGD 自带的梯度噪声反而是一种隐式正则化,有助于跳出尖锐极小、偏向平坦极小——平坦极小通常泛化更好(参见 flat minima 相关文献)。
针对上述全部问题,有一个漂亮的技巧:加动量。直觉上,把优化想象成一个沿山坡滚下、会累积动量的球——这与物理上的建模方式非常接近:
# SGD:
dx = compute_gradient(x)
x -= learning_rate * dx
# SGD + Momentum:
v = rho * v + dx # 速度 = 折旧的旧速度 + 当前梯度
x -= learning_rate * v # 用速度代替梯度做更新
更新不再直接用当前梯度,而是用一个速度项 \(v\):每一步的速度由上一步速度(乘以衰减系数 \(\rho\),即动量系数)加上当前梯度构成——本质是历史梯度的滑动平均,\(\rho\) 控制"过去 vs 现在"的权重。\(\rho\) 很大时,新速度严重依赖历史速度,惯性越强。动量如何化解前述三大问题?
针对学生的提问,老师补充了几点诚实的说明:(a)如果初始位置恰好沿鞍点"横向"滚动,理论上仍会卡住,但概率极低,且实践中人们会用多个不同随机种子跑多次训练以防万一,SGD 本身的随机噪声也几乎总能把你带出鞍点;(b)动量可能让收敛变慢(因为过冲后要折返),但平均而言帮你找到更好的极小点——不做动量可能在局部极小收敛,做了动量则越过它继续下探;(c)这些结论多是在神经网络这类具体对象上经验性成立的,实践中也存在无动量 SGD 表现更好的案例——人们最终会用 whatever works best,多试几种挑最优。另外,动量公式存在若干等价写法(不同框架实现可能形式不同),数学上完全相同,课件上可以自行验证。
下一个优化器是 RMSProp(2012 年,出自 Geoffrey Hinton 组,如今看已是"较老"的方法)。它不再只维护速度(历史梯度的平均),而是对梯度的平方维护滑动平均,并在更新时逐元素除以它的平方根:
cache = decay_rate * cache + (1 - decay_rate) * dx**2 # 平方梯度的滑动平均
x -= learning_rate * dx / (sqrt(cache) + eps) # 逐元素缩放的更新
直觉:这是对"能否改变迈步方向"这一学生提问的直接回应。对于梯度一直很大的维度(陡峭方向),分母大,步子被压小;对于梯度一直很小的维度(平坦方向),分母小,步子被放大。于是在窄谷地形中,RMSProp 会自动少走陡峭方向、多走平坦方向,很快把轨迹"扭"向谷底延伸的方向。老师展示了三种方法的对比动画:SGD 始终沿固定局部方向慢吞吞前进;动量版会过冲再折返;RMSProp 则迅速转向中心、沿平坦方向高效推进。
Adam 是当今深度学习中最流行的优化器,它基本上就是SGD+Momentum 与 RMSProp 的合体:一阶矩(first moment,即动量/速度,系数 \(\beta_1\))捕捉梯度的滑动平均,二阶矩(second moment,系数 \(\beta_2\))捕捉梯度平方的滑动平均,更新时用学习率乘一阶矩、除以二阶矩的平方根。命名借用物理力学中"矩"的说法,功能上正是两者优势的叠加:平坦方向加速、陡峭方向减震,再加上动量的惯性。
为什么朴素 Adam 在第一步会出问题?老师让学生猜,随后揭晓:\(\beta_1\)、\(\beta_2\) 通常初始化为非常接近 1 的值(典型 0.9 和 0.999),且一、二阶矩都初始化为 0。于是第一个时间步:二阶矩 = \((1 - \beta_2) \cdot \mathrm{grad}^2\),由于 \(1 - \beta_2 = 0.001\) 极小,只要初始梯度不大,二阶矩就非常接近 0;更新时要除以这个接近 0 的数,导致第一步迈出一个巨大的步子——哪怕梯度本来很小。Adam 的最后一块拼图正是偏差修正(bias correction):给一、二阶矩分别除以 \((1 - \beta^t)\) 形式的、随时间步 \(t\) 变化的修正项,抵消初期估计偏低的问题;随着 \(t\) 增大,修正项趋于 1,不再需要。这将在作业中亲手实现。
AdamW 处理的是"正则化如何与优化器交互"这个不那么直观的问题。默认 Adam 把 L2 正则化项合在损失里一起求梯度,动量和二阶矩的计算都被正则化"污染";AdamW 则只用数据损失计算所有矩和更新,把正则化项(权重衰减,weight decay)直接在最后加到权重更新上。动机是让速度/动量只反映损失地形本身、不受权重数值干扰。很多时候 AdamW 略好——例如 Meta 的 Llama 系列模型全部使用 AdamW。最终哪种更好是经验问题:两种都试,选表现好的。
Adam/AdamW 之后,优化器研究仍在演进:针对大模型训练的内存开销,出现了 Adafactor、8-bit Adam (bitsandbytes) 等低内存变体;LAMB / LARS 支持超大批量训练;近年 Lion(符号更新)、Muon、SOAP/Shampoo 等预条件类优化器也在前沿模型中崭露头角。此外,大模型训练几乎都配合 \(\beta_1 = 0.9\), \(\beta_2 = 0.95\) 左右, \(\epsilon = 10^{-8}\) 这类调校以及精心设计的学习率调度(见下节)。不过经验上,AdamW + warmup + cosine 衰减仍是绝大多数项目的第一选择。
学习率是优化中最重要的超参数之一。老师用损失随训练步数变化的曲线展示了四种典型形态:
老师还提醒:现实中上述几条"看起来不错"的曲线都可能是好学习率,取决于具体情形和所处的训练阶段——由此引出最后一节:学习率不必固定。
几乎所有现代最先进的模型都会在训练中改变学习率。常见策略有:
一条重要的经验法则:若把批量大小(每次更新所用的训练样本数)扩大 \(n\) 倍,学习率也应相应按 \(n\) 同比例放大。其背后数学涉及批量内梯度的方差等因素,也有人给出过证明,但本质上它是在大量问题上被经验反复验证的规则。实用价值是:当你有一份奏效的"配方"、只是想增大批量以加速训练时,把学习率等比例上调即可保持训练动态相似。
最后老师简要提及二阶优化(second-order optimization),回应了早前学生"能不能用 Hessian 决定方向"的提问。目前的梯度下降只利用一阶信息:在当前点对损失做线性(一阶泰勒)近似,沿其下降方向走,再用动量、RMSProp 等技巧修正步幅。二阶方法则利用 Hessian 矩阵(二阶导数)对损失做二次(抛物线型)近似,直接求该二次型的最小值作为下一步。在某些优化问题上效果极佳,但深度学习基本不用,原因有二:
因此对大网络我们几乎从不使用二阶方法;但如果模型小、或者愿意花更多计算换取更高质量的更新步(例如整批数据都能塞进一个 batch 的场景),二阶乃至更高阶的方法值得了解。
老师的实践建议总结:
下一讲进入神经网络(neural networks):在线性分类器的两层权重矩阵之间插入一个非线性函数(最简单的例子是 ReLU)。线性模型无法分开非线性可分的数据(比如蓝红两类点),但通过一层层的变换,网络可以逐步把数据"扭曲"到某个中间空间,使最后一层的线性边界足以将它们分开——这正是深度学习的起点。