优化理论
优化理论 (Optimization Theory) 是一门研究“如何从所有可能的方案中,找到最优方案”的数学学科。在计算机科学和机器学习的语境下,它研究的是如何找到一组参数 \(\theta\),使得目标函数 \(f(\theta)\)(通常是损失函数)达到 最大值或最小值 。
深度学习的目标是让模型在未知数据集上表现良好,因此深度学习的“优化目标”相比于优化理论,多了泛化要求。因此在优化笔记本中,我把优化和正则放在了一起。具体原因可以参见本笔记的“风险函数和经验风险函数”小节。
计算机中的数值计算
更多的关于数值计算的内容参见数学/数值计算笔记。
数值解
深度学习的优化过程,本质上就是在寻找一个能让损失函数最小化的神经网络参数集合,这个过程几乎完全依赖于数值计算方法,尤其是基于梯度的迭代优化算法。具体来说,一个经典的神经网络学习优化过程如下:
- 前向传播:输入数据,通过网络计算得到一个预测值。这个过程是大量的矩阵乘法和激活函数计算,是纯粹的数值计算。
- 计算损失:用损失函数来衡量预测值和真实值之间的差距,这同样是数值计算。
- 反向传播:为了调整参数来减少损失,我们需要计算损失函数对每一个参数的梯度(偏导数),常用的比如反向传播算法,这也是一种数值方法。
- 参数更新:使用一种优化算法,比如Adam,根据计算出来的梯度来微调网络中的所有参数。这是一个反复迭代的数值逼近过程。
上面所说的具体流程在后面的章节中一一学习,这里提出的原因是想说明:数值计算是机器学习以及深度学习的核心,必须要学习掌握,否则无法展开后面内容的学习。
绝大多数机器学习问题(比如训练一个复杂的神经网络)都极其复杂,根本不可能存在一个直接算出最优参数的完美公式。换句话说,机器学习算法要通过迭代过程(Iterative Process)来不断更新解的估计值,而不是通过解析过程(Analytical Solution)直接推导出一个公式。
常见的数值计算的应用场景:
- 优化:在深度学习中,寻找一组能让损失函数最小化的参数
- 线性方程组的求解:比如经典的线性回归模型中会遇到
上溢和下溢
我们已经知道机器学习和深度学习的核心就是一系列的数值计算。在理想的情况下,我们将通过成千上万次的迭代,让参数平稳地、逐步地向着损失函数的最低点收敛。然而,这个完美的数学流程并不能完美地在计算机上实现。首当其冲的问题就是上溢(Overflow)和下溢(Underflow):
- Overflow 上溢:当一个数字太大,超过了计算机能表示的最大范围时,他就会变成一个无穷大(inf)或者一个无意义(NaN, Not a Number)的数字。
- Underflow 下溢:当一个数字变得太接近于零,超出了计算机能表示的最小精度时,它就会被“四舍五入”成零。
举例来说:(先不要管具体方法是怎么实现的,只是帮助理解一下上溢和下溢)
- 在反向传播中,梯度的计算是多个导数的连乘。如果网络很深,且激活函数的导数持续小于1,那么梯度在从后向前传播时会越乘越小,最终变得无限接近于零,从而被计算机当做零来处理。
- 反过来讲,如果在反向传播计算中梯度持续大于1,那么连乘之后就会变得异常巨大,超出计算机能表示的最大值,最终变成inf或NaN。一旦某次计算更新中让算出来一个NaN,那么整个模型瞬间就被摧毁了,训练过程直接失败。
那这里肯定有人会问:python已经支持大整数运算,那么假设我们支持小数点后10万位的小数,计算时把他转换为整数不就好了吗?理论上是可以的,但是计算效率会非常之低。对于一个相当规模的深度学习训练,计算次数是上万亿次的。因此,数值计算速度非常重要。
现在一般的方法是直接把支持32位浮点数和64位浮点数计算的电路单元直接焊死在电路单元或者说电路板上。这个小东西被称为FPU(Floating-Point Unit)。这些电路在一个时钟周期内就能完成一次浮点数加法或乘法,速度快到极致。我们可以把FPU理解为厨师,他只做一件事:进行浮点数计算(加减乘除)。他做的飞快,是物理上真实存在的实体。在一个GPU中,分布着成千上万个这样的厨师,并通过CUDA架构来实现高效的协同工作。
FPU的物理限制是显而易见的,Overflow和Underflow的问题非常突出。举例来说,如果某一次计算中的输入向量是:\(x=[1000,990,980]\)
我们的FPU使用的标准是32位浮点数,一个float32能表示的最大数值是\(3.4 \times 10^{38}\),任何超过这个数的计算,FPU都会报告上溢(Overflow),并返回一个特殊值 inf。
在这里,我们先不讨论为什么要用下面这个公式,这里只是展示overflow的情况下我们该怎么办:
好了,现在第一步就超出FPU的限制了,第一个计算就没能得到结果。程序继续计算e的990次方、980次方,结果都是 inf,整个计算过程全面崩坏,我们不可能得到想要的结果。
为了解决这个问题,我们必然需要把输入向量x进行某种调整。只要我们能把数值调整为计算结果不会溢出就可以了,这种调整一般被称为“数值稳定”。
还是以上面的向量x为示例,举一个数值稳定的方法:
- 首先找到向量x中的最大值,max(x)=1000
- 对x中的每一个元素,减去这个最大值,得到新的向量\(z=[0,-10,-20]\)
- 计算新的softmax函数,上述变换不会改变softmax的最终概率值(因为贡献比例没有改变)
- 由于max(x)的巧妙设置,分母中永远都至少有一个exp(0)=1,而其他下溢的内容被记为0,这样就可以总能得到softmax的结果值而不会出错。换句话说,经过上述变换,上溢问题被解决了,而下溢在softmax计算中又是无害的,因此不会造成计算的崩坏。
当然,现在的深度学习框架如PyTorch和TensorFlow都已经把上述数值计算过程进行了封装,用户在调用计算时不会出现上下溢出问题。例如,调用 torch.nn.functional.softmax时,上面的数值稳定方法(框架中甚至使用了更加优异的数值稳定方法!)已经内嵌在函数中,我们直接调用就可以了。
病态条件
病态条件(Ill-conditioned)的意思是:对于一个函数,其输入的微小变化,将会显著地影响到其输出结果。病态条件在计算机的数值计算中是一个大麻烦,因为计算机在存储数字的时候总会有微小的舍入误差,比如把1/3存储为0.3333333,当这个微小的、不可避免的误差发生在一个病态条件的输入上时,会导致输出产生巨大的偏差,导致计算结果和真实值有天壤之别。
由于深度学习的核心人任务之一就是优化,即寻找一个能让损失函数最小化的参数集。我们可以把损失函数想象成一个非常复杂的、高低不平的地形表面,我们的目标是走到这个地形的最低点。在深度学习任务中,这个地形往往是病态的,他并不光滑,而是更像一个峡谷,在一些地方极其陡峭,而在另外一些地方又极其平缓,如下图所示:

这个病态的损失函数表面有如下特征:
- 充满陡峭的悬崖,在这里损失函数在某个方向上会变化极快
- 平缓的谷底,在这里损失函数在某个方向上的变化又会极慢
这种特点会导致我们的优化算法(比如梯度下降)在进入到这样的峡谷时,梯度在陡峭方向会非常大,在平缓方向又会非常小,从而导致在优化过程在峡谷两侧来回震荡,难以向真正的最低点前进。为了防止震荡,我们必须使用很小的学习率,但是这又会导致在平缓的方向上的前进速度变得极其缓慢,从而导致训练卡住或者不收敛。
这里要注意,上面的3D图只是一种比喻。真实的神经网络的损失函数,其参数量是一个维度极高的向量,远远不止三维。一个中等大小的模型都可能有数百万(百万维)甚至数十亿(十亿维)的参数。这里以及大多数教材中画3D峡谷图,是为了方便我们理解,是一种降维的可视化类比,帮助我们建立直观的理解。换句话说,3D可视图无论如何构建,都是极度简化的切片,有着严重的失真,但足够我们用来理解病态条件(峡谷)、局部最小值(小坑)等重要概念。
既然知道了病态的损失函数表面的这些特点,那么我们就可以针对性地来采用这样一种策略:
- 当我们在平缓的谷底时,用较快的速度前进
- 当我们在陡峭的悬崖时,用较慢的速度移动
上述策略其实就是如今人们常用的优化器(Optimizer) 的核心思想。这里要注意,并非每种优化器都有这种策略,传统的GD(梯度下降)、SGD(随机梯度下降)中没有这种动态变化能力,而Adam、带有Polyak动量的SGD则具备了这种能力。这种能力一般由动量(Momentum) 和自适应学习率(Adaptive Learning Rate) 组成,在后面我们会展开来详细讲解。在学习具备动态功能的高级优化器之前,我们还是要从最为传统的优化器开始,了解梯度下降等基本概念。
深度学习中的优化
风险函数f和经验风险函数g
在深度学习中,我们的最终目标是模型在没有见过的数据上也表现很好,我们想找到的是期望风险(Expected Risk)或真实风险(True Risk),其数学公式是:
其中:
- \(w\) 代表你当前深度学习模型的权重参数。
- \(l(w; x, y)\) 代表单个样本的损失函数。它衡量的是:给定参数 \(w\) 时,模型对输入 \(x\) 的预测结果与真实标签 \(y\) 之间的误差有多大。
- \(P\) 代表真实世界中数据的 完整概率分布 。它包含了世界上所有可能的输入和输出。
- \(E\) 代表期望(Expectation),可以理解为加权平均。
风险函数 \(f\) 评估的是:如果用你现在的模型去应对真实世界中所有可能出现的数据,平均下来你会犯多大的错。它是最完美、最能反映模型真实能力的评估标准(即泛化能力的绝对体现)。
我们永远无法获得真实分布 \(P\)。例如在做人脸识别时,你不可能收集到全人类、所有光照条件、所有角度下的人脸照片。既然找不到所有的输入和输出,我们就根本无法在计算机里算出 \(f(w)\) 的确切数值。
我们定义另外一个函数:经验风险函数(Empirical Risk),来代表通过训练集得到的模型表现,其数学公式为:
其中:
- \(n\) 代表你手里实际拥有的训练集样本总数。
- \(x_i, y_i\) 代表你收集到的第 \(i\) 个具体数据样本和它的真实标签。
- \(\Sigma\) 代表将这 \(n\) 个样本的损失全部加起来。
既然算不出真实的风险 \(f\),我们只能退而求其次:算一算模型在手头现有的这 \(n\) 个训练数据上,平均犯了多大的错。这就是经验风险 \(g\),它也就是我们在编写 PyTorch 或 TensorFlow 代码时,实际计算出来并让模型去优化的那个 Loss 值。
由于你只是从庞大的真实世界里抽取了一小撮数据来训练,这批数据一定带有偶然性、偏差甚至标注错误(噪声)。经验风险 \(g\) 仅仅是真实风险 \(f\) 的一个“替身”,替身永远无法 100% 还原真身。

上图是D2L中的f和g的关系示意图。上图其实反映了一个现实:g总是没有f平滑。
- \(f\) 是绝对平滑的: 因为 \(f\) 涵盖了无限多的真实数据。在统计学中,当数据量趋于无穷大时,个别极端数据(噪声)的影响会被彻底稀释和抹平。因此,\(f\) 随参数 \(w\) 变化的函数曲面是非常平滑的,它的最低点就是绝对的真理。
- \(g\) 是坑洼不平的: 你的训练集只有有限的 \(n\) 个样本。如果这批样本里刚好有几个罕见的特例,或者带有明显噪声的数据,\(g\) 的计算结果就会被这些少数派严重干扰。反映在函数曲面上,\(g\) 会布满局部的凹陷(局部最优解)、毛刺和陡坡。
在理解了\(f\) 和 \(g\) 的区别后,深度学习和优化算法之间的“分工与矛盾”就十分清晰了:
优化的唯一目标是最小化经验风险g。
优化算法(如 SGD、Adam)本身没有“大局观”。它看不到外面的真实世界,只能看到你喂给它的那几万张训练图片。它的任务就是单纯地玩数学游戏:通过调整参数,把训练集上的误差数值降到最低。
然而,深度学习关注的是泛化能力(Generalization)。我们训练模型,不是为了让它完美背诵已经做过的题(训练集),而是希望它在面对未来从没见过的新题(测试集或实际部署场景)时,依然能给出准确的答案。因此:
深度学习的终极目标是最小化风险f。
在理想状态下,如果训练数据无限多,\(g\) 就会等同于 \(f\)。但在现实中,数据总是有限的,这就导致经验风险 \(g\) 只是风险 \(f\) 的一个粗糙且带有噪声的近似(即上文中提到的“不如 \(f\) 平滑”)。当我们让优化算法过度努力,把 \(g\) 压榨到了极致时,模型就会开始死记硬背训练集里的特例和噪声。此时,虽然经验风险 \(g\) 降得很低,但真实世界中的风险 \(f\) 反而会飙升。这就解释了为什么 降低训练误差并不绝对等于降低泛化误差 。
为了防止优化算法把 \(g\) 降得太低而损害 \(f\),深度学习引入了许多专门的手段。比如正则化(Weight Decay)、提前停止(Early Stopping)和 Dropout。这些技术在传统纯数学优化中可能看起来是“反直觉”的,因为它们实际上在阻碍优化算法把 \(g\) 降到最低,但它们却能有效保护模型,使其更接近真实的 \(f\)。
从这个角度来看:
深度学习 = 优化 + 正则
当然,深度学习还受到数据和架构的影响,因此完整的深度学习 = 数据 + 架构 + 优化 + 正则。
不过我们仅从风险函数f和经验风险函数g的角度来看,优化与正则实际上就是让结构风险最小化(Structural Risk Minimization, SRM):
其中
即是我们上面提到的经验风险函数(训练集上的平均误差),衡量模型对当前训练数据的拟合程度。优化算法(Adam、SGD等)的目标就是把这个部分想办法降到0。我们可以把这个看做引擎的输出(经验风险)。
\(R(w)\)是刹车的力度(正则化项 / 惩罚项),这是数学上用来衡量模型复杂度的函数。最常见的例子是 \(L_2\) 正则化(权重衰减 Weight Decay),它的公式是 \(R(w) = \|w\|^2\)。它衡量了模型的 复杂程度或“野心” 。参数 \(w\) 的值越大,说明模型在拼命扭曲自己去迎合数据(哪怕是噪声)。\(R(w)\) 的存在,就是为了惩罚那些过于复杂、权重过大的模型。它告诉优化器:“你可以去拟合数据,但动作幅度不能太大,必须保持平滑和简单。”
\(\lambda\)(Lambda)是一个大于 0 的常数,由算法工程师来设定。它决定了 引擎和刹车谁说了算 。如果 \(\lambda = 0\),刹车完全失灵,退化为纯粹的经验风险最小化(疯狂死记硬背,必定过拟合)。如果 \(\lambda\) 非常大,刹车踩死,模型动都不敢动(权重全被压成 0,什么都学不到,欠拟合)。
把上面的内容整合起来,我们就触及到了机器学习理论的灵魂—— 泛化误差上界(Generalization Bound) 。在统计学习理论中,数学家们证明了一个伟大的不等式。对于真实风险 \(f(w)\) 和经验风险 \(g(w)\),它们之间存在这样的关系:
这个不等式告诉你:真实世界的误差 \(f(w)\),被“训练集误差”和“模型复杂度”共同限制着。
- 如果模型太复杂,虽然 \(g(w)\) 变成了 0,但后面的“复杂度惩罚项”会爆炸,导致真实的泛化误差 \(f(w)\) 依然很高(这就是过拟合的数学本质)。
- 为了让真实的 \(f(w)\) 降下来,我们不能只看 \(g(w)\),必须把右边这两项加在一起同时最小化!
所以,深度学习真正在优化的目标 \(J(w) = g(w) + \lambda R(w)\),其实就是在 直接优化真实风险 \(f(w)\) 的上界 。通过把 \(R(w)\) 强行塞给优化算法,我们用数学手段“欺骗”了只会低头干活的优化器:它以为自己还在单纯地找最低点,但实际上,它已经被我们引导着在“拟合数据”和“保持简单”之间找到了完美的平衡,从而安全地抵达了真实世界 \(f(w)\) 的低谷。
在PyTorch中,这个深刻的数学概念被封装得极其优雅:
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
或者使用Adam优化器:
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
在这行代码里,数学公式 \(J(w) = g(w) + \lambda R(w)\) 被拆解成了几个具体的控制旋钮:
model.parameters()(目标) :告诉优化器引擎,你要调整哪些权重参数 \(w\)。lr=0.01(油门) :Learning Rate(学习率),决定了引擎每次朝着降低 \(g(w)\) 的方向迈多大步子。weight_decay=1e-4(刹车) :这就是我们的数学参数 \(\lambda\)!在 PyTorch 中,\(L_2\) 正则化被称为 Weight Decay(权重衰减) 。这里设定的1e-4(0.0001),就是你踩刹车的力度。
值得注意的是,PyTorch把L2_regularization叫做weight_decay,是因为这里隐藏了一个非常巧妙的工程实现细节:在纯数学理论中,我们是把正则项加到损失函数里的:
当你对这个总目标 \(J(w)\) 求导(计算梯度)时,原本的损失函数求导得到 \(\nabla g\),而后面那一坨正则项对 \(w\) 求导,刚好等于 \(\lambda w\)。
所以,在每次更新参数的时候,实际发生的数学计算是:
把它稍微变换一下形式:
看明白了吗?在 PyTorch 的底层实现中,它根本没有费劲去算什么平方和(\(\|w\|^2\))。它只是在每次更新权重之前, 悄悄地把现有的权重 \(w\) 乘以一个略小于 1 的数 (比如 \(0.9999\)),让权重“衰减”一点点,然后再减去正常的梯度。
这就好像优化器在前面拼命拉着权重往上走(拟合数据),而 weight_decay 在后面像一根弹簧一样,始终把权重往 \(0\) 的方向拉扯(保持简单)。这种直接在梯度更新那一步动手脚的做法,既高效又完美地实现了 \(L_2\) 正则化的数学效果。
关于weight decay和dropout等的详细内容,参见Regularization笔记。
.
偏差与方差
在机器学习(深度学习)中,我们总是在追求“总误差”最小。我们可以把总误差拆分为偏差(Bias)和方差(Variance):
- 偏差 (Bias) \(\approx\) 训练集上的误差。偏差衡量的是模型预测值的期望与真实值之间的差距。高偏差说明模型在训练集上的表现就很差(误差大)。这通常意味着模型欠拟合(Underfitting),即模型太简单了,没能抓住数据中的规律。
- 方差 (Variance) \(\approx\) 测试集与训练集误差的差值。方差衡量的是模型预测值本身的离散程度(稳定性),高方差说明模型在训练集表现很好,但在测试集上表现很差(误差大)。这通常意味着模型过拟合(Overfitting),即记住了训练集的噪声而非普遍的规律。
数学上来看,假设我们要预测的真实函数是 \(y = f(x) + \epsilon\),其中 \(\epsilon\) 是随机噪声(均值为 0,方差为 \(\sigma^2\))。我们训练出的模型为 \(\hat{f}(x)\)。对于一个具体的点 \(x\),其期望预测误差可以分解为:
偏差 (Bias) 的计算:
偏差衡量的是模型预测值的期望与真实值之间的差距。如果你用不同的训练数据训练 100 次模型,这 100 个模型的平均预测值偏离真相有多远。
方差 (Variance) 的计算:
方差衡量的是模型预测值本身的离散程度(稳定性)。当训练集发生细微变化时,预测结果的波动大小。
优化与正则
在深度学习中,优化(Optimization) 就是寻找一组模型参数,使得损失函数(Loss Function)的值达到最小的过程。简单来说,每一个机器学习模型本质上都是寻找一组可以使得损失函数最小化的参数集合,优化的过程就是不断调整参数,来让损失函数最小化的过程。
换句话说,大多数模型都可以拆解为:
- 模型结构:决定了你如何从输入数据来得到预测值
- 损失函数:定义了结果的评断标准
- 优化算法:决定了如何寻找损失最小化的参数集合
对于一次完整的深度学习模型(神经网络)训练,其完整过程可以理解为:
* 将数据集划分为若干份,分若干轮次(epoch)进行;这里需要进行数据归一化处理(**Input Normalization**)
* 在循环开始前,给神经网络的参数$\theta$设置初始值(**Initialization**)
* 在每一轮epoch开始前调整学习率(**Learning Rate Scheduling**),然后进入epoch(注:实际代码一般在epoch结束后):
1. 在每一个epoch中,分批次导入数据(**Batching**):
1. 前向传播,即让数据进入模型,得到预测结果
* 这里可以进行**Batch Normalization**
* 这里可以进行**Regularization**正则化,如Dropout,在传播过程中随机关闭神经元,防止模型死记硬背
2. 根据前向传播的预测结果和真实值,算出损失(loss)
* 这里可以进行**Regularization**正则化,如L2权重衰减
3. 使用优化手段更新参数:
1. 抹掉上一个batch的残余梯度
2. 反向传播:通过链式法则算出当前loss对每个参数的梯度
3. 优化器(**Optimizer**)根据梯度和学习率修改参数$\theta$(即优化)
* epoch结束后,评估验证集,如果表现不错,就将参数保存下来,如果后面过拟合或者断电,可以回退到表现最好的版本上
上面我标记出的Initialization, Learning Rate Scheduling, Batching, Normalization, Optimizer等都旨在于帮助损失函数更快、更稳、更好地找到损失函数的最小值,因而都算是Optimization的范畴。
虽然优化大部分内容都旨在寻找损失函数的最小值,但Regularization较为特殊,它旨在干扰优化。正则的这种行为是为了换取模型在未见过的测试集上损失更低,从而防止模型在训练中过拟合。
虽然正则并非优化的内容,但优化和正则一起组成了模型训练的核心内容,所以我们把他们放在一起学习。
如果把把优化比作下山,找到最低的山谷,那么上述优化手段和正则分别对应了下述作用:
- Initialization:优化的起点(决定了从山的哪个地方开始下坡)
- Batching:优化的采样(决定了你每次看多少路面来决定下一步)
- Regularization:优化的约束(防止你为了走近路而掉进训练数据的死胡同)
- Optimizer:优化的执行(你的双脚如何根据地形反馈移动)
- Learning Rate Scheduling:优化的节奏控制(决定随着距离终点的远近,步幅应当如何变化)
- Normalization:优化的路面修整(将崎岖不平的山路修整得平缓均匀,防止在陡坡上打滑,或平地上原地踏步)
DNN的数学表示
深度神经网络(DNN)可以用层级递推的数学形式来精确描述(参考 Tufts CS130 Topic2)。一个 \(L\) 层的DNN,其第 \(\ell\) 层的计算为:
其中:
- \(h^{(\ell)} \in \mathbb{R}^{n_\ell}\):第 \(\ell\) 层的输出(隐藏表示),\(h^{(0)} = x\) 为输入
- \(W^{(\ell)} \in \mathbb{R}^{n_{\ell-1} \times n_\ell}\):第 \(\ell\) 层的权重矩阵
- \(b^{(\ell)} \in \mathbb{R}^{n_\ell}\):第 \(\ell\) 层的偏置向量
- \(g^{(\ell)}(\cdot)\):第 \(\ell\) 层的激活函数(如 ReLU、Sigmoid、Tanh)
整个网络可以表示为函数的复合:
其中 \(\theta = \{W^{(1)}, b^{(1)}, \ldots, W^{(L)}, b^{(L)}\}\) 是所有可训练参数的集合。
DNN训练作为优化问题:给定数据集 \(\mathcal{D} = \{(x_i, y_i)\}_{i=1}^{N}\),训练DNN就是求解:
其中 \(L(\cdot, \cdot)\) 是损失函数。这个优化问题有以下特点:
- 高维:参数 \(\theta\) 的维度可达数百万甚至数十亿
- 非凸:由于激活函数的非线性和层的复合,目标函数几乎总是非凸的
- 随机性:实际中使用mini-batch近似全量梯度,引入了随机性
代理损失函数(Surrogate Loss)
在分类任务中,我们真正关心的是0-1损失(分类错误率):
但0-1损失是不可微的(阶梯函数),无法直接用梯度下降优化。因此我们使用代理损失函数——一个可微的替代品,其最小化能间接最小化真实的0-1损失。
交叉熵损失(Cross-Entropy Loss)是最常用的分类代理损失。它来源于KL散度的最小化:
其中 \(p\) 是真实分布(one-hot标签),\(q\) 是模型预测分布(softmax输出)。由于 \(H(p)\) 对于one-hot标签是常数(为0),最小化KL散度等价于最小化交叉熵:
对于二分类,简化为:\(L_{BCE} = -[y \log \hat{y} + (1-y) \log(1-\hat{y})]\)
回归任务的损失函数:
- 均方误差(MSE):\(L_{MSE} = \frac{1}{N}\sum_{i=1}^{N}(f(x_i;\theta) - y_i)^2\),对大误差惩罚更重,对异常值敏感
- 平均绝对误差(MAE):\(L_{MAE} = \frac{1}{N}\sum_{i=1}^{N}|f(x_i;\theta) - y_i|\),对异常值更鲁棒,但在零点不可微
梯度下降基础
Ian经典教材第四章中除了计算机的数值计算、梯度下降、Hessian外,还介绍了一些其他概念,比如最优步长、牛顿法、Lipschitz连续、凸优化等,如果后续遇到,我再回来补充。概念太多,没办法一下子全都记住,因此我们暂时先记住最重要的概念:
- 梯度是一个向量,永远指向函数值上升最快的方向
- 梯度下降就是朝着梯度的反方向前进
- Hessian矩阵描述了地形的曲率(弯曲程度),通过分析Hessian,我们可以知道当前位置是U型山谷(曲率小)、V型峡谷(曲率大),还是更复杂的鞍点
- 当Hessian矩阵的最大曲率和最小曲率相差悬殊时,我们就遇到了病态地形。这种地形就好比一个两侧陡峭、谷底平缓的峡谷,会导致梯度下降收敛缓慢,让算法在峡谷两侧来回震荡等,时所有优化算法都要面对的头号敌人。
- 泰勒一阶展开证明了为什么负梯度方向是有效的,二阶展开则揭示了曲率是如何影响优化效果的,并揭示了线性预测和真实情况之间的差距
- 在深度学习这种高维空间中,我们遇到的梯度为零的点,绝大多数都是鞍点,而不是局部最小值。简单的梯度下降会在这里卡柱,而我们之后要学习的现代优化器则有逃离鞍点的机制。
- 既然Hessian描述了地形,那么最理想的优化方法就是在每一步都同时利用梯度和Hessian来计算出完美的下一步。牛顿法虽然完美,但是计算成本过高,不适合深度学习,但这也是所有高级优化器试图模仿和近似的目标。
在Ian书中的第四章还有最后一部分“约束优化”,这一部分暂时先跳过。现在正在学的内容主要都是无约束优化,其目标是在一个没有任何限制的广阔空间里,找到函数的最低点;这是绝大多数深度神经网络如CNN,RNN,Transformer训练的标准场景。
而另一条重要分支:约束优化,也就是书中的这一节,目标是在一系列规则或限制条件下,找到函数的最低点。这个技术路线是KKT方法等,在一些特定的机器学习方法如支持向量机SVM,以及一些特定的深度学习课题中较为重要。
由于现在我们的关注点在无约束优化上,因此这一小节暂先跳过,等到需要学习的时候我们再返回来看。
接下来我将依次介绍各种梯度下降优化器,首先依据一阶或二阶,优化器可以大体分为两类:一阶优化器 First-Order Optimizers和二阶优化器。
一阶优化器(First-Order Optimizers) 按照发展的先后顺序,主要有以下内容:
- 普通梯度下降:Gradient Descent (1847), SGD (1951)
- 结合动量法:Polyak Momentum (1964), Nesterov Momentum (1983)
- 结合Adaptive LR: AdaGrad (2011), RMSProp (2012), AdaDelta (2012)
- 同时结合动量和自适应梯度: Adam (2014), AdamW (2017)
二阶优化器(Second-Order Optimizers) 按照发展的先后顺序,主要有以下内容:
- 纯粹二阶:牛顿法 (17世纪),准牛顿法L-BFGS (1989)
- 近似二阶:KFAC (2015),AdaHessian (2020)
优化器的具体内容请参见优化器笔记(Optimizer)。
梯度
梯度是机器学习中最为核心的数学概念,也是优化算法的基石。在机器学习中,我们的目标通常是找到一个数学模型,使其在处理数据时的“误差”最小。这个寻找最小值的过程就是 优化问题 。想象你在山顶,想要最快下到山脚。如果你知道在每一个位置往哪个方向走是“下降”的,你就可以不断迈步,直到周围所有方向都是“上升”的,这时你就找到了一个局部最低点。梯度就是那个告诉你在当前位置,函数值变化最快的方向和程度的工具。
对于一个输入为多维向量 \(\boldsymbol{x} = (x_1, x_2, \dots, x_n)^\top\) 的标量函数 \(f(\boldsymbol{x})\),它在 \(\boldsymbol{x}\) 处的梯度定义为:
梯度的每一维都是函数对该维度变量的偏导数。梯度本身也是一个向量,它的维度和自变量 \(\boldsymbol{x}\) 是一致的。
梯度向量 \(\nabla f\) 所指的方向,是函数在该点增加最快的方向。在几何上,梯度向量总是垂直于函数的等值线(或等值面)。既然梯度指向增长最快的方向,那么梯度的 反方向 (负梯度方向)就是函数值减小最快的方向。这就是为什么“梯度下降法”要沿着负梯度方向更新参数。

上图展示的是函数 \(f(x, y) = -x - 2y\) 在点 \((1, 1)\) 处的梯度示意图。图中那些平行的斜线是 等值线 ,每一条线代表函数值相等的一系列点。从图中可以看到,蓝色的箭头跨过的线最多,这表明在相同的步长(箭头长度)下,蓝色箭头(梯度方向)由于跨过了最多的等值线,意味着沿着这个方向走,函数值增加得最快。
“梯度指向增长最快方向”这一结论在非线性函数中同样成立。如果函数不是线性的,利用一元函数的泰勒展开思想,多元函数在某一点 \(\boldsymbol{x}_0\) 附近也可以进行 线性近似 。
这意味着,只要我们观察的范围足够小(局部),任何平滑的复杂函数看起来都像是一块“平板”(线性函数)。因此,在这一点上,梯度依然是指向值上升最快的方向。
抛物面 (\(f(x, y) = x^2 + y^2\)):像一个碗,梯度会指向远离圆心的放射方向。

马鞍面 (\(f(x, y) = x^2 - y^2\)):形状像马鞍,梯度在不同区域的指向会发生剧烈变化。

正弦乘积面 (\(f(x, y) = \sin(x)\sin(y)\)):像起伏的蛋盒,梯度会指向各个“山峰”。

在处理损失函数的时候,以下公式经常使用:
- 线性函数求导 :
- \(\nabla_{\boldsymbol{x}} (\boldsymbol{y}^\top \boldsymbol{x}) = \boldsymbol{y}\)
- \(\nabla_{\boldsymbol{x}} (\boldsymbol{x}^\top \boldsymbol{y}) = \boldsymbol{y}\)
- \(\nabla_{\boldsymbol{x}} (\boldsymbol{A}\boldsymbol{x}) = \boldsymbol{A}^\top\)
- 二次型求导 :
- \(\nabla_{\boldsymbol{x}} (\boldsymbol{x}^\top \boldsymbol{x}) = 2\boldsymbol{x}\)
- \(\nabla_{\boldsymbol{x}} (\boldsymbol{x}^\top \boldsymbol{A} \boldsymbol{x}) = (\boldsymbol{A} + \boldsymbol{A}^\top)\boldsymbol{x}\)
- 范数与常数 :
- \(\nabla_{\boldsymbol{x}} \|\boldsymbol{x}\|_2 = \frac{\boldsymbol{x}}{\|\boldsymbol{x}\|_2}\)
- \(\nabla_{\boldsymbol{x}} (a\boldsymbol{x}) = a\boldsymbol{I}\) (其中 \(a\) 为标量,\(\boldsymbol{I}\) 为单位矩阵)
对于saddle point(鞍点)的情况,即便梯度 \(\nabla f(\boldsymbol{x}) = \mathbf{0}\),该点也不一定是极值点(如马鞍面),需要通过黑塞矩阵 \(\boldsymbol{H}_f\) 的性质进一步判定。相关内容详见后面的小节。
梯度下降
在数学上,优化(Optimization) 指的是从所有可能的解决方案中,系统地选择并找出一个或者一组最优解的过程。放到函数上来说,一般指通过改变x,来最小化函数\(f(x)\);最大化本质就是最小化,因为可以通过\(-f(x)\)来采用同样的优化方法。
我们把要最小化的函数称为目标函数(objective function)或准则(criterion)。当我们对其进行最小化时,这个函数有时也被叫做代价函数(cost function)、损失函数(loss function)、误差函数(error function)。在不同情境或论文中作者可能会更倾向于用某一个名称,但是他们的本质都是要最小化的目标函数。
对于使函数能取到最小值的自变量\(x\),我们记作\(x^{*} = \arg\min f(x)\)。最大化的话就是\(x^{*} = \arg\max_{x} f(x)\)。在微积分中,对于函数\(y=f(x)\),它的导数(derivative)**记为\(f'(x)\)或\(\frac{dx}{dy}\)。如果\(f'(x)>0\),说明在x点函数是向上倾斜的,趋势是递增的,也就是说函数在x点处继续增加,函数会变大;反之如果导数小于0,则是递减趋势。
由于我们的目标是找到函数的最小值,因此:
- 在导数大于0时,函数是“上坡”,我们应该减少x,朝上坡的反向走
- 在导数小于0时,函数是“下坡”,我们应该增大x,继续下坡
这个策略就叫做梯度下降(gradient descent)。梯度下降法是最早的基于梯度的算法,由数学家奥古斯丁-路易·柯西 (Augustin-Louis Cauchy) 在 1847 年 提出。随机梯度下降法在梯度下降的基础上进行了实践优化,不再计算所有样本的平均梯度,而是每次只随机抽取 一个 (或一小批)样本来估算梯度并更新参数。这使得它在处理现代大规模数据集(如深度学习)时,收敛速度比传统方法快得多,且能跳出一些局部的陷阱。
在数学上,泰勒一阶展开证明了梯度下降为什么是有效的:
这个式子表明,在x处,如果我们行进一个微小的距离\(\epsilon\),那么行进后的函数大小近似等于原本x处的函数大小,加上原本x处的函数导数乘以这个微小距离。为了让新的函数值 f(x+ϵ) 小于旧的函数值 f(x),我们只需要让增量部分 ϵf′(x) 为负数即可。而保证这一项为负数的最有效方法,就是让我们选择的‘行进方向’ ϵ 与导数 f′(x)的符号相反。换句话说,泰勒一阶展开证明了梯度下降是有效策略。
当\(f'(x)=0\)的时候,导数无法提供往哪个方向移动的信息,因此这个点被称为临界点(critical point)或驻点(stationary point)。

如上图所示,导数为0时可能会有三种情况:
- Local Minimum 局部最小值
- Local Maximum 局部最大值
- Saddle Point 鞍点,或称为Inflection Point 拐点
在上述三种情况中,我们通过二阶导数来判断:
- 如果二阶导数\(f''(x)>0\),那么表明函数是凹的,即山谷,局部最小点
- 如果二阶导数\(f''(x)<0\),那么表明函数是凸的,即山峰,局部最大点
- 如果二阶导数\(f''(x)=0\),那么表面函数是平坦的,像在马鞍的中心
在梯度下降中,局部最小点是理想的停止点,这表明我们找到了一个可能的解。而局部最大值和鞍点都是糟糕的停止点,因为在局部最大点和鞍点,我们无法判断该往哪边走是正确的。标准的梯度下降算法无法区分这三种情况,只要梯度为零他就会停下来。因此现代优化算法针对最大值和鞍点的问题,发展出了动量(Momentum)、随机性(Stochasticity)、二阶方法(Second-Order Methods) 等解决策略。
我们的最终目标是找到全局最小点(global minimum)。一个函数可能有一个全局最小点,也可能有多个,也可能没有全局最小点。想象一个左侧往无穷大、右侧往无穷小的函数图像\(f(x) = -x^3 + 3x\):

上面这个图像在右侧趋向于无穷小,在数学上称为无下界(Unbounded Below)。在梯度下降中,这种函数会导致发散(Divergence),即运行过程中未能收敛到一个稳定的解,而是趋向于一个无效的或者无限的结果。发散在函数值趋向于正无穷时也有可能产生,比如如果学习率(等一下就要讲到)设置的太高,步子迈太大,一步直接跨过了山谷的最低点而到达了对面更高的位置,下一步为了下山又跨的更远,导致越走越高。
现在我们来正式看一下梯度下降在深度学习中是如何应用的。上面我们讨论的都是一维函数,即一条线,但实际操作中我们面对的总是多变量函数。
对于一个多维输入的函数\(f: \mathbb{R}^n \to \mathbb{R}\)(这里的\(\mathbb{R}^n\)指函数的输入是一个n维向量),我们队最小值的概念有一个明确的意义:最小值必须是一维的标量。因为标量可以直接比较大小,而向量无法直接比较大小。这个标量通常也被称为损失(Loss)或成本(Cost)。
在最小值处,有对应着的一个n维向量,这个特定的多维向量就是我们的最优解(Optimal Solution)。换句话说,判断一个向量是否是最优解的标准,就是把他代入函数,然后看计算出来的标量是不是所有可能结果中最小的。
对于一维函数,我们已经知道,对函数求导,当导数为0时,我们就找到了一个特殊的点,这个点可能是极小值、极大值或者鞍点。而对于多维向量,我们用偏导数(partial derivative)对该函数进行求导,这个时候我们可以得到一个由原本输入向量中每个元素的偏导数组成的“包含所有偏导数的向量”,记为\(\nabla_{x} f(x)\),读作“对x的梯度(gradient)”。梯度的的第\(i\)个元素是\(f\)关于\(x_i\)的偏导数。在多维情况下,临界点是梯度中所有元素都为零的点。
举例说明一下,假设我们的多变量函数是\(f(x, y) = x^{2} + 2xy + y^{3}\),那么在这里我们就有输入向量:
我们的目标是计算这个函数在任意点\((x,y)\)的梯度。根据定义,对于我们这个二维函数,他的梯度就是包含了所有偏导数的向量,即:
通过计算,我们可以分别得到对x的偏导数:
以及对y的偏导数:
我们把两个偏导数放回第一步的向量结构中,得到:
这就是函数\(f(x,y)\)在任意点\((x,y)\)的梯度公式。用这个公式,只要你把任何一个点的坐标代入进去,你就能很快算出那个点的梯度向量。
比如说,在点\((3,2)\)处:
这个梯度向量的含义是,函数f的值上升最快的方向,是朝着x轴走10个单位,同时朝着y轴走18个单位的那个方向。在二维平面上我们可以可视化地画出来,但是对于10000维的高维向量,我们只要用这个道理去找就行了,不需要把图像画出来找。换句话说,用这个方法,对于任意维度的多维向量,我们不需要画出图来就能判断一个点处的梯度形势。
对于上面这个方向,我们知道他是最f上升最快的方向,也就是最陡的上坡路。那么在梯度下降算法中,为了让函数值下降的最快,我们就要朝着这个梯度的反方向移动,也就是朝着\(\begin{bmatrix}-10 \\-18\end{bmatrix}\)的方向移动一小步。
对单变量函数求出来的导数,和多变量函数求出来的梯度向量,它们的每一个分量都是一“上坡指南”,指明了在各自维度上让函数值增加的方向和强度。这是导数的性质。因此,对于梯度向量,将所有维度的“上坡指南”合并成的合力方向,一定是函数整体上升最陡峭的方向。书中通过基于方向导数来寻找函数值下降最快方向的数学证明确认这了这一点(P76-77),这里不再展开。我们只需要记住:梯度永远指向最陡的上坡,它的反方向就是最陡的下坡。
既然我们知道了下坡方向,那么我们就可以沿着这个方向前进,来寻找最小值。首先我们确定一个固定步长大小的正标量,记为\(\epsilon\),这个值被称为学习率(learning rate)。
对于多维向量\(X\):
这个公式就是最速下降法(method of steepest descent)或者说梯度下降(gradient descent) 的更新规则。
通过最速下降法迭代公式,我们可以不断寻找下坡路,直到找到一个完全平坦的地方。我们可以通过迭代的方式不断逼近该目标,或者直接通过求解方程组=0来直接跳到临界点(这种直接求解的方式叫做解析解)。在现实世界中,深度学习模型的函数往往极其复杂,梯度方程组也极其庞大,根本无法直接求解析解。因此我们才需要梯度下降这种看起来笨拙但却十分有效的迭代方法。
梯度下降算法只能用于连续空间中的优化问题。但是,对于很多参数离散的问题,比如整数规划、旅行商等,虽然无法直接计算梯度,但是可以用类似的思想,不断在当前位置找到一个最好的方向然后移动一小步来找到最优解。这个方法被称为爬山(hill climbing)算法。(之所以叫做爬山,是因为爬山算法经常用于最大化目标函数的问题,比如棋类游戏、遗传算法等;而梯度下降主要用于机器学习任务,在机器学习中我们的目标几乎总是寻找最小化损失函数。)
Jacobian
在上面的梯度下降中,我们提到用标量来衡量一个输入函数(比如包含神经网络所有参数x的多维向量)的最小值(即损失值L),记作\(f: \mathbb{R}^n \to \mathbb{R}\)。
但是,即便我们的神经网络最终得到的是一个标量,在神经网络中间,我们面临的是若干个多维输入到多维输出的中间层,如下图所示:

这个中间过程表示为\(f: \mathbb{R}^n \to \mathbb{R}^m\),即n维输入到m维输出。那么对于这样的中间层,我们如何来了解每一个输入的变化,是如何影响到每一个输出的呢?如果用梯度向量,那么对于每一个输出变量,我们都要计算n个偏导数;那么对于m个输出变量,我们就要计算m * n个偏导数。那么如何组织这m*n个偏导数,系统性地表示和使用这海量的导数信息呢?Jacobian矩阵就是为了解决这个问题而诞生的。
Jacobian矩阵提供了一个完美的、紧凑的数学结构,即使用一个矩阵,将这 m * n 个偏导数整齐地收纳其中。通过收纳,我们就可以进一步利用线性代数中的矩阵乘法,简洁、高效地表达和计算链式法则。
具体来说,假设我们有一个函数 \(f\),它接受一个 \(n\) 维的向量输入
并产生一个 \(m\) 维的向量输出
这里的 \(f_1, f_2, \ldots, f_m\) 是 \(m\) 个独立的函数,它们共同组成了这个多维输出。
Jacobian 矩阵(通常记作 \(\mathbf{J}\))就是将所有输出函数对所有输入变量的一阶偏导数排列成一个矩阵。 它的标准表示如下:
在该矩阵中:
- 每一行代表一个输出函数对所有输入变量的偏导数;你可以把第\(i\)行看作是函数\(f_i\)的梯度(的转置)。
- 每一列代表所有输出函数对一个输入变量的偏导数。
举一个从 \(\mathbb{R}^2\) 映射到 \(\mathbb{R}^3\) 的具体例子,假设输入向量是
输出向量由以下三个函数定义:
根据定义,这个函数的 Jacobian 矩阵 \(\mathbf{J}\) 将是一个 \(3 \times 2\) 的矩阵。我们来逐行计算它的元素:
第一行(对 \(f_1\) 求偏导):
-
\[ \dfrac{\partial f_1}{\partial x_1} = 2x_1 \]
-
\[ \dfrac{\partial f_1}{\partial x_2} = 5 \]
第二行(对 \(f_2\) 求偏导):
-
\[ \dfrac{\partial f_2}{\partial x_1} = 3\sin(x_2) \]
-
\[ \dfrac{\partial f_2}{\partial x_2} = 3x_1\cos(x_2) \]
第三行(对 \(f_3\) 求偏导):
-
\[ \dfrac{\partial f_3}{\partial x_1} = x_2^2 \]
-
\[ \dfrac{\partial f_3}{\partial x_2} = 2x_1x_2 \]
现在,把这些结果填入矩阵中,我们就得到了这个函数的 Jacobian 矩阵的表达式:
这个矩阵就完整地描述了在任意点 \((x_1, x_2)\) 处,输入向量的微小变化是如何传递并影响到输出向量的。
Hessian
在梯度下降中,我们要找到最陡峭的方向,然后反向下山。然后我们设定一个学习率,代表下山的步子有多大。那么接下来,我们面临这样一个问题:我们在下山的时候,下降的高度,和预期的是一样的吗?
想要检查梯度下降步骤是否达到预期,那么我们其实就要关注梯度下降的曲率。

在上图的三种情况中,绿色虚线就是我们之前提到的泰勒一阶展开\(f(x+\epsilon) \approx f(x) + \epsilon f'(x)\),也就是梯度下降算法所依赖的线性预测。根据这个预测,我们可以得到移动后的新位置\(x_{\text{new}} = x - \eta f'(x)\)。
我们可以注意到,一阶泰勒展开用的是约等于,这代表它只是一个近似,而忽略了更高阶的信息。这里忽略掉的,即是梯度下降算法中忽视的曲率,也即造成绿色虚线和蓝色实现存在差距的原因。这个差距的大小和方向,由二阶导数决定。在上图中:
- 左图代表的是\(f''(x)<0\)的情况,实际下降多于线性预期
- 中图代表的是\(f''(x)=0\)的情况
- 右图代表的是\(f''(x)>0\)的情况,实际下降小于线性预期
为了更加精确,我们可以进行泰勒二阶展开:
这个公式清晰地揭示了所有秘密:
- 梯度下降的预期,就是前半部分的线性预测
- 现实与预期的差距,就是后半部分的修正项
可以看到,修正项的大小,由二阶导数来决定。我们把多维函数的二阶导数(second derivative)叫做Hessian矩阵:
Hessian矩阵等价于梯度的Jacobian矩阵。
到这里,我们可以对上面的内容进行一个小结:
- 我们有一个损失函数\(f(x)\),他的类型是多维输入对单一输出
- 我们队\(f(x)\)进行一阶导数求导,得到它的梯度向量\(\nabla f(x)\)
- 我们把梯度本身看做一个新函数\(g(x)\),它的输出是一个多维向量,因为神经网络是多层的,输入的多维向量进行一次求导后我们得到的是一个中间层;对这个中间层\(g(x)\)求导,可以得到Jacobian矩阵。
- Jacobian矩阵\(J_g\)的第\(i\)行、第\(j\)列的元素是\((\mathbf{J}_g)_{i,j} = \frac{\partial g_i}{\partial x_j}\),这里的\(g_i\)就是函数\(g\)输出向量的第\(i\)个分量,换句话说,也就是梯度向量的第\(i\)个分量,即\(\frac{\partial f}{\partial x_i}\),代入到原式中,得到:
可以看到,这就是我们刚才讨论的Hessian矩阵。绝大多数情况下,我们不说“梯度的Jacobian矩阵”,而是直接说“Hessian矩阵”。
现在我们已经了解了梯度下降、Hessian矩阵,以及知道如何衡量下降时是否符合预期。上面我们提到了一维泰勒二阶展开:
我们把上式扩展到多维的泰勒二阶展开:
这个式子就是在当前点\(x^{(0)}\)附近一个精确的地形地图,其中\(g\)时梯度,\(H\)时Hessian矩阵。你可以认为,这个公式用数学方法描述了局部地形的具体细节。
接着,我们把梯度下降的更新规则:
带入到多维泰勒二阶展开中,即可得到:
这个预测公式可以预测出梯度下降的下一步后,损失值变成了多少。其中:
- 当前损失:我们的起点
- 线性预期下降:基于一阶导数的乐观预测
- 二阶曲率修正:由Hessian主导的,决定现实与预期差距的关键项
在实际中,或者说一个常规的、标准的梯度下降训练循环中,我们不需要多此一举地计算这个泰勒预测值。一般来说,程序会用 x_new = x_old - εg 计算出新位置;然后把 x_new 扔进模型里,通过一次完整的前向传播,直接计算出真实、精确的新损失值 f(x_new)。换句话说,我们总是能得到真实值,那么为什么我们在这里要去讨论泰勒二阶展开和近似的预测公式呢?——对于优化器算法设计来说,这些都是非常关键的。
不过对于大多数人来说,并不需要自己去设计优化器,只需要了解大致的原理,知道如何选择优化器、不同的优化器之间有什么区别就够了。故而在现阶段,我们只需要对这些概念留有一个大致的印象。
一般来讲,Hessian的应用包括:
- 判断临界点类型:通过检测临界点Hessian矩阵所有特征值的正负号来了解;如果所有特征值都为正,则为局部最小值;如果所有特征值都为负,则为局部最大值;如果特征值中又有正数又有负数,则处于鞍点。
- 分析梯度下降的性能:利用泰勒二阶展开攻势可以计算出使得损失下降最多的最优步长;在最坏的情况下(梯度方向与最大曲率方向一致),最优步长由Hessian的特征值决定。
优化问题的形式化框架
在深入具体的优化算法之前,我们先建立一个统一的数学框架(参考 Boyd & Vandenberghe Convex Optimization, 2004;Tufts EE110/CS130 课程)。
一般优化问题可以表述为:
其中:
- \(\theta \in \mathbb{R}^n\) 是优化变量(参数),\(n\) 维向量
- \(J: \mathbb{R}^n \to \mathbb{R}\) 是目标函数(objective / cost / loss function),输入向量、输出标量
- 最优值 \(p^* = \min_\theta J(\theta)\)(或 \(p^* = \inf_\theta J(\theta)\))
- 最优点 \(\theta^*\) 满足 \(J(\theta^*) = p^*\);最优点可能不止一个
注意:最优值 \(p^*\) 不一定能被取到。例如 \(J(\theta) = \frac{1}{\theta}\)(\(\theta > 0\))的下确界为 0,但不存在 \(\theta^*\) 使得 \(J(\theta^*) = 0\)。
局部最优与全局最优
- 局部最优点(Locally Optimal):\(\theta_0\) 是局部最优的,如果存在 \(R > 0\),使得 \(J(\theta_0) \leq J(z)\) 对所有 \(\|z - \theta_0\|_2 \leq R\) 成立。即在以 \(\theta_0\) 为圆心、\(R\) 为半径的球内,\(\theta_0\) 是最优的。
- 全局最优点(Globally Optimal):\(\theta_0\) 解决了原始问题,不受任何局部区域限制。
凸性(Convexity)
对于函数 \(J(\theta)\),如果对任意 \(\theta_1, \theta_2 \in \mathbb{R}^n\) 和 \(0 \leq t \leq 1\),均满足:
则 \(J\) 是凸函数。几何直觉:函数图像上任意两点之间的连线,总在函数曲面的上方。
凸函数的核心性质:局部最优 = 全局最优。
反证法证明(来自课程推导):假设 \(\theta_0\) 是凸函数 \(J\) 的局部最优点,但不是全局最优点。则存在 \(w\)(在局部球外),使得 \(J(w) < J(\theta_0)\)。在 \(\theta_0\) 到 \(w\) 的连线上取一点 \(z = t\theta_0 + (1-t)w\)(\(z\) 在局部球内),由凸性:
这与 \(\theta_0\) 在局部球内是最优的矛盾。故凸函数的局部最优点必为全局最优点。\(\blacksquare\)
迭代优化算法的标准形式
我们通常无法直接求解 \(\nabla J(\theta^*) = 0\) 的解析解,因此需要设计迭代算法:产生点序列 \(\theta^{(0)}, \theta^{(1)}, \theta^{(2)}, \ldots\) 使得 \(J(\theta^{(k)}) \to p^*\)(\(k \to \infty\))。
标准迭代格式为:
其中:
- \(\Delta\theta^{(k)}\):搜索方向(search direction),\(\mathbb{R}^n\) 中的向量
- \(\lambda^{(k)} > 0\):步长(step size)或学习率(learning rate),标量
- \(\theta^{(0)}\):初始点,需预先指定
算法终止条件:\(J(\theta^{(k)}) - p^* \leq \varepsilon\),其中 \(\varepsilon\) 为预设容差。
下降方法(Descent Method):如果算法保证每一步 \(J(\theta^{(k+1)}) < J(\theta^{(k)})\)(除非 \(\theta^{(k)}\) 已是最优点),则称为下降方法。对于凸函数,下降方向必须满足:
即搜索方向与梯度方向的夹角大于 90°。梯度下降选择 \(\Delta\theta = -\nabla J(\theta)\),这是最简单的满足条件的方向。
线搜索与回溯法(Line Search & Backtracking)
梯度下降算法的完整流程为:给定初始点 \(\theta^{(0)}\),重复:
- 计算搜索方向 \(\Delta\theta = -\nabla J(\theta)\)
- 通过线搜索确定步长 \(\lambda > 0\)
- 更新 \(\theta := \theta - \lambda \nabla J(\theta)\)
- 直到 \(\|\nabla J(\theta)\|_2 \leq \varepsilon\)
回溯线搜索(Backtracking Line Search)是确定步长的经典方法。给定常数 \(\alpha \in (0, \frac{1}{2})\) 和 \(\beta \in (0, 1)\):
- 从 \(\lambda = 1\) 开始
- 当 \(J(\theta + \lambda\Delta\theta) > J(\theta) + \alpha \lambda \nabla J(\theta)^T \Delta\theta\) 时,令 \(\lambda := \beta\lambda\)
- 重复直到条件满足(Armijo条件)
直觉:不断缩小步长,直到函数值的实际下降量不低于线性预期的 \(\alpha\) 倍。注意线搜索是自适应的——\(\lambda^{(k)}\) 在每步都可能不同。
条件数与收敛速度
考虑一个二次”碗”函数(quadratic bowl):
其梯度和Hessian为:
从初始点 \(\theta^{(0)} = (\gamma, 1)\) 出发,可以证明精确线搜索下:
每一步目标函数值缩减为原来的 \(\left(\frac{\gamma-1}{\gamma+1}\right)^2\) 倍。\(\gamma\) 就是Hessian的条件数 \(\kappa = M/m\)(最大特征值与最小特征值之比)。


收敛速度分析:
- 当 \(\frac{1}{3} \leq \gamma \leq 3\) 时收敛快(条件数接近1,等高线接近圆形)
- 当 \(\gamma \gg 1\) 或 \(\gamma \ll 1\) 时收敛极慢(等高线高度拉伸为细长椭圆)
- GD展现近似线性收敛(每步以常数因子缩减),收敛速度取决于Hessian条件数
- 要将距离缩减 \(e^k\) 倍,GD需要的迭代次数约为 \(t \approx \frac{1}{2}k\kappa\)
GD的关键性质:
- 计算高效:每步只需向量乘法和加法 \(\theta^+ = \theta - \lambda \cdot \nabla J(\theta)\)
- 收敛速度为 \(O(\kappa)\):条件数越大,收敛越慢
- 动量方法可将收敛速度提升至 \(O(\sqrt{\kappa})\),详见优化器笔记
非凸优化中的GD
对于非凸函数 \(J(\theta)\),求解 \(\nabla J(\theta) = 0\) 得到的可能是:
- 局部最小值 \(\theta_1\)
- 局部最大值 \(\theta_2\)
- 全局最小值 \(\theta_3\)
- 鞍点 \(\theta_4\)(某些方向是最大、某些方向是最小)
在所有这些点上 \(\nabla J(\theta) = 0\),但只有最小值是我们想要的。因此在非凸优化中,需要监控 \(J(\theta^{(k)})\) 在迭代过程中是否持续下降。
凸优化
上面我们已经提到了,梯度为零可能是在鞍点,因而不能说明函数已经取到了极值点。但是我们都知道,对于\(y = x^2\) 这样的二次函数,它的导数\(y' = 2x\) 是单调递增的。在导数为 0 的左侧,导数一直小于 0(函数下降);在右侧,导数一直大于 0(函数上升)。换句话说,导数为零的点必然是极小值点。这里要注意,在导数的坐标图上,导数为负的意思是越往右越低。
为了将这种直觉推广到多元函数,凸函数被提了出来。对于函数 \(f(x)\),如果对任意的 \(x_1, x_2\) 以及 \(0 < \alpha < 1\),均满足:
那么 \(f\) 就是 凸函数 。
在优化算法(如梯度下降法)中,如果目标函数是凸函数,我们就能保证:函数图像没有乱七八糟的”起伏”或”鼓包”。局部最优解就是全局最优解 。换句话说,只要一直顺着梯度往下走,最终一定能找到那个唯一的最低点,而不会掉进其他的”小坑”里。
凸优化(Convex Optimization) 研究的核心确实就是如何寻找凸函数在特定约束条件下的 最小值 。在数学和工程界,凸优化被视为一种”金标准”,因为它拥有一个极其迷人的特性: 局部最优解就是全局最优解 。
很多传统工程和金融问题天然具备凸性,或者可以完美简化为凸函数。在这些领域,凸优化是统治级的工具。然而,对于深度学习问题,目标函数几乎全都是非凸(non-convex)的。
虽然神经网络的全局地形一般都是非凸的,但是如果我们把视野聚焦于一个极小的范围时,任何平滑的曲线都将看起来像是一个微型的小碗。这也是后面优化器的一个基本前提,比如说Adam优化器就把局部视为凸函数。
当然,我们要时刻提醒自己鞍点的存在。优化器使用动量(momentum)来跳过这些中心梯度为零的鞍点。具体的内容详见Optimizer这一节。
随机梯度下降
随机梯度下降 (Stochastic Gradient Descent, SGD) 的理论基础源于一篇名为《一种随机逼近方法》(A Stochastic Approximation Method) 的论文,由Herbert Robbins和Sutton Monro于1951年提出。
SGD是一种快速的优化算法,核心思想是“随机看小部分,快速迭代”。其工作方式:不像标准方法看完所有数据再更新,SGD每次只随机抽取一小批(甚至一个)数据来计算梯度,并立即调整模型参数。在GD中,我们需要把所有的训练数据都看一遍,才能计算一次梯度,然后更新一次参数(走一步)。如果数据集有几百万张,那么走一步得等到猴年马月了。SGD是一个聪明的改进,每次随机抓取一小把数据(mini batch),比如64张图片,然后就用这一小把数据来估算一下梯度的大致方向,然后立刻走一步。
SGD的核心更新公式如下:
其中:
- \(\theta\):theta,表示模型的参数(包含所有权重weights和偏置biases的向量),我们的目标就是通过训练找到最优的theta
- \(:=\) 表示赋值(Assignment)操作
- \(\eta\):eta,表示学习率(Learning Rate),控制每次参数更新的步长。如果eta很大,参数更新的幅度就会很大。
- $\nabla_{\theta} $ 表示梯度(Gradient)。梯度是一个向量,指向损失函数值上升最快的方向,因此梯度下降需要在前面加上负号。
- \(J(\theta; x^{(i)}, y^{(i)})\) 表示损失函数(Loss Function)。这个函数用来衡量模型在单个训练样本\((x^{(i)}, y^{(i)})\)上的预测效果有多差。\(x^i\)是第\(i\)个训练样本的特征,\(y^i\)是第\(i\)个训练样本的真实标签。
可以看出,SGD的更新公式和传统GD相比,区别进在于批量的大小。传统的梯度下降发也叫批量梯度下降法(Batch Gradient Descent, BGD),其核心思想是每次更新参数时都要用全部的训练数据来计算梯度:\(\theta := \theta - \eta \cdot \nabla_{\theta} J(\theta)\)。而SGD的损失函数只针对一个样本,因此计算速度更快。
优点:
- 快: 计算量小,特别适合大规模数据集。更新参数的频率极高,训练速度飞快。
- 避免局部最优: 更新方向的随机“噪声”有助于跳出不够好的解决方案,寻找全局最优解。
缺点:
- 不稳定: 下降路径震荡,不平滑。每一步的方向可能不太准,但总体趋势是往下走的。
- 可能不收敛: 最终可能在最优点附近徘徊,而不是精确到达。
实际中常用的是“小批量梯度下降 (Mini-batch GD)”,它是SGD和标准梯度下降的折中,兼顾了速度和稳定性。我们可以从最纯粹的SGD定义中看到,每次更新模型参数只使用一个随机选择的训练样本,比如:
- 一行表格数据
- 一张图片及其标签
- 一篇文章中的一句话,或者一系列文档中的一个文档
由于现代计算机硬件特别是GPU的并行处理数据能力较高,因此一般实践中我们每次使用一个小批量(mini-batch)的样本,比如32、64、或128个来计算梯度。除了计算效率更高外,只用1个训练样本可能会非常嘈杂和随机,导致训练过程震荡很剧烈。而使用一个批次的平均梯度,能让收敛过程更加稳定(更接近真实梯度)。在深度学习相关的议题和论文中,SGD一般指代的就是小批量梯度下降,而不是纯粹的随机梯度下降。
SGD的梯度方差分析
SGD使用mini-batch来估算全量梯度。设mini-batch大小为 \(m\),则SGD的梯度估计为:
这个估计是全量梯度的无偏估计:\(\mathbb{E}[\hat{g}] = \nabla_\theta J(\theta)\)。然而,其方差与batch size成反比:
其中 \(\sigma^2\) 是单个样本梯度的方差。这意味着:
- batch越大:方差越小,梯度估计越准确,但每步计算成本越高
- batch越小:方差越大,引入更多噪声,但更新频率更高
SGD相比全量GD的优势在于:全量GD需要 \(O(N)\) 次计算才更新一步(\(N\) 为总样本数),而SGD只需 \(O(m)\) 次。当 \(m \ll N\) 时,SGD在相同计算预算下可以完成更多次参数更新,往往能更快地降低损失。
泛化误差与SGD
SGD的随机性不仅是计算上的便利,还带来了重要的泛化优势。Hardt等人(2016, Train faster, generalize better)从算法稳定性(algorithmic stability)的角度证明了:
如果SGD在 \(T\) 步内终止,且每步使用大小为 \(m\) 的mini-batch,则SGD的泛化误差满足:
\[\varepsilon_{gen} = |f(\theta) - g(\theta)| = O\!\left(\frac{T}{N}\right)\]
其中 \(f(\theta)\) 是真实风险,\(g(\theta)\) 是经验风险,\(N\) 是训练集大小。
这个结论的重要启示:
- 早停(Early Stopping)有理论依据:训练步数 \(T\) 越少,泛化误差上界越小
- SGD的噪声是隐式正则化:与全量GD相比,SGD倾向于收敛到更"平坦"的最小值(flat minima),这些点的泛化性能更好。直觉上,平坦最小值对参数的微小扰动不敏感,因此在训练集和测试集上表现更一致
- 大batch可能损害泛化:过大的batch size减少了梯度噪声,可能导致模型收敛到"尖锐"的最小值(sharp minima),泛化性能反而下降

梯度爆炸与消失
在训练深层网络(特别是RNN)时,梯度可能随着层数增加而指数级增大或缩小,导致训练失败。这个问题由Pascanu等人(2013, On the difficulty of training recurrent neural networks)系统分析。
RNN中的数学推导
考虑一个简单的RNN,其隐藏状态递推为:
其中 \(W_r\) 是循环权重矩阵,\(g\) 是激活函数。为了计算损失 \(L\) 对早期时间步参数的梯度,需要通过链式法则将梯度从时间步 \(t\) 传播到时间步 \(k\):
其中 \(z_i = W_r^T h_{i-1} + W_x^T x_i + b\) 是激活函数的输入。
关键洞察在于这个连乘积的行为。对矩阵 \(W_r^T\) 进行特征值分解,设其最大特征值的绝对值(谱半径)为 \(\rho = |\lambda_{\max}(W_r^T)|\):
- 当 \(\rho < 1\) 时:\(\left\|\prod_{i=k+1}^{t} \text{diag}(g'(z_i)) W_r^T\right\| \to 0\),梯度指数级衰减→消失
- 当 \(\rho > 1\) 时:\(\left\|\prod_{i=k+1}^{t} \text{diag}(g'(z_i)) W_r^T\right\| \to \infty\),梯度指数级增长→爆炸
更严格地说,Pascanu等人证明了:
其中 \(\gamma = \max_i \|g'(z_i)\|\) 是激活函数导数的上界(对于Sigmoid,\(\gamma \leq \frac{1}{4}\);对于Tanh,\(\gamma \leq 1\))。因此:
- 如果 \(\|W_r\| \cdot \gamma < 1\):梯度必然消失
- 如果 \(\|W_r\| \cdot \gamma > 1\):梯度可能爆炸(不是必然,取决于具体的 \(g'(z_i)\) 值)

梯度裁剪(Gradient Clipping)
梯度裁剪是防止梯度爆炸最直接的方法(Pascanu et al., 2013)。其思想是:当梯度的范数超过阈值 \(c\) 时,将其缩放回阈值范围:
这保持了梯度的方向不变,只限制其幅度。在PyTorch中:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
缓解梯度消失的策略
梯度消失问题更为棘手,因为它不像爆炸那样有明显的数值异常。常见的缓解策略包括:
- ReLU激活函数:\(g'(z) = 1\)(\(z > 0\) 时),避免了Sigmoid/Tanh导数持续小于1的问题
- 残差连接(Residual Connection):\(h_\ell = F(h_{\ell-1}) + h_{\ell-1}\),提供了梯度直通路径
- LSTM/GRU门控机制:通过遗忘门和输入门控制信息流,避免长程依赖中的梯度消失
- 合理的初始化(如Xavier、Kaiming初始化):使各层激活值的方差保持稳定
- 归一化技术(如BatchNorm、LayerNorm):稳定各层的输入分布