一、绪论——优化:机器学习的引擎
1.1 优化问题:机器学习的核心任务
机器学习的目标是让模型从数据中学习规律,而实现这一目标的数学引擎正是数值优化。如果把模型比作一个正在学习新技能的学徒,那么优化算法就是指导学徒调整思路、不断进步的老师。
在监督学习中,我们拥有一个包含输入 和对应标签 的数据集。我们想要找到一个函数 (即模型,参数为 ),使得它对输入的预测尽可能接近真实标签。如何衡量"尽可能接近"?这就需要引入损失函数,它量化了模型预测值 与真实值 之间的差距。
将所有训练样本的损失平均起来,就得到了我们的优化目标——经验风险最小化:
- ****:模型的参数,存在于一个 维的空间中()。 可能小至几个(如线性回归),也可能大至数十亿(如深度神经网络)。
- ****:损失函数。例如,均方误差(MSE)用于回归任务,交叉熵损失(Cross-Entropy)用于分类任务。
- ****:我们的目标是找到一组参数 ,使得这个平均损失最小。
形象比喻:损失景观我们可以将参数空间想象成一片起伏不平的山脉,而平均损失函数的值就是这座山在某一点(某组参数)的"海拔高度"。优化的目标,就是在这片山脉中找到最低的那个盆地(全局最优解)。山脉的形状——哪里是陡坡,哪里是缓谷,哪里是鞍部——被称为损失景观(Loss Landscape)。
1.2 精确解与数值迭代:两种求解哲学
既然目标是找到损失函数的最小值,一个很自然的想法是:能不能像解一元二次方程那样,直接用一个公式算出最优参数 ?
1.2.1 为什么需要迭代?
对于少数简单的模型(如线性回归),我们可以通过求解其导数等于零的方程,得到闭式解(Closed-form Solution),也称为解析解。例如,线性回归的普通最小二乘法解:
然而,在绝大多数机器学习(尤其是深度学习)场景中,这条路是走不通的:
非线性的壁垒:深度神经网络是由非线性激活函数堆叠而成的复合函数,其损失函数关于参数 高度非线性,无法直接求逆。
高维的诅咒:现代模型动辄百万、千万甚至上亿参数。求解 这样的矩阵逆运算,其计算复杂度是 ,对于高维数据是完全不可行的。
非凸的困境:深度网络的损失景观是一个极其复杂的、充满高峰和低谷的非凸函数,存在无数个局部最优解。解析方法无法处理这种情况。
因此,我们不得不退而求其次,采用数值迭代的方法。
1.2.2 核心思想:下山之旅
想象你是一个在浓雾中被困在山上的登山者,看不见整座山的地形,但你能够感知脚下土地的坡度。你想要下山到最低点(求最小值),你会怎么做?
你会遵循一个简单的策略:
- 感知方向:感受一下周围地面,哪个方向是向下最陡的?
这就是几乎所有机器学习优化算法的核心思想的直观体现:
- 更新方向:你决定朝哪个方向迈步。在梯度下降法中,这个方向就是负梯度方向(最陡的下坡方向)。
- :学习率(Learning Rate),代表你这一步迈的有多大。这是整个优化过程中最重要的超参数之一。
1.2.3 从下山到算法:核心要素的形式化
将上述下山策略转化为数学形式,我们需要明确三个核心要素:
**初始点 **:下山之旅的起点。在优化中,参数的初始值对最终结果有重要影响,不同的初始化可能导致收敛到不同的局部最优解。
**搜索方向 **:每一步应该朝哪个方向走。这由当前点的梯度信息决定,不同的优化算法本质上是在设计不同的搜索方向策略。
**步长 **:每一步应该迈多大。步长太大可能越过最低点甚至发散,步长太小则收敛过慢。学习率调度策略正是为了解决这一矛盾。
这个简单的"感知-行动-感知"循环,构成了从最基础的梯度下降到最先进的自适应优化算法的共同基础。唯一的区别在于,不同的算法对"更新方向"和"步长"的计算方式有不同的理解和设计。
二、基石——梯度下降法(最速下降路径)
2.1 从方向导数到负梯度:最速下降的数学原理
在第一章中,我们将优化问题形象地比喻为“下山之旅”:每一步都朝着最陡的下坡方向前进。现在,我们需要将这个直观想法转化为严格的数学形式。
2.1.1 方向导数:沿任意方向的变化率
对于一个多元函数 ,我们想知道:站在当前点 ,沿着某个方向向量 迈出一小步,函数值会变化多少?这由方向导数给出:
其中 是梯度 与方向 之间的夹角。
2.1.2 最速下降方向:负梯度的必然性
方向导数的表达式揭示了一个关键事实:当 ,即 时,方向导数取最小值。这意味着与梯度方向完全相反的方向,函数值下降最快。
因此,最速下降方向就是负梯度方向:
这正是梯度下降法的数学本质——每一步都沿着负梯度方向前进,确保在当前点局部意义上获得最快的下降。
2.1.3 梯度下降的迭代公式
将负梯度方向代入第一章的核心公式,得到梯度下降法的完整迭代形式:
其中 是学习率(步长),控制着每一步迈出的距离。
数学洞察:梯度下降法是一阶优化方法的代表,因为它只利用了函数的一阶导数信息。虽然每一步都是局部最优的选择,但将这些局部最优步骤串联起来,就能形成一条通往(局部)最小值的全局路径。
2.2 困境与挑战:梯度下降的局限性
尽管梯度下降法优雅而直观,但在实际应用中面临着诸多挑战。理解这些挑战,是掌握更高级优化算法的前提。
2.2.1 病态曲率:峡谷效应
当损失函数在不同方向上的曲率差异很大时,就出现了病态曲率问题。想象一个狭长的峡谷:垂直于峡谷的方向坡度极陡,而沿峡谷的方向坡度平缓。
在这种情况下,梯度下降会表现出典型的“震荡”行为:
数学解释:这对应于损失函数Hessian矩阵的条件数很大。梯度方向并不直接指向最小值,而是偏向于曲率最大的方向。
可视化理解: 考虑一个二维损失函数 。在 方向曲率是 方向的100倍。梯度下降会在 方向剧烈震荡,而在 方向缓慢爬行,导致整体收敛极慢。
2.2.2 局部最优与鞍点
在高维非凸优化中,真正的局部最小值其实相对罕见,更常见的是鞍点——在某些方向上是极小值,在其他方向上是极大值的点。
在鞍点附近,梯度接近于零,梯度下降会陷入停滞,但实际上存在通往更低值的路径。这解释了为什么随机性和动量对于突破鞍点至关重要。
维度的诅咒:在高维空间中,鞍点的数量远远多于局部极小值。对于一个 维的非凸函数,一个临界点是局部极小值的概率约为 。当 时,这个概率几乎为零。
2.2.3 学习率的敏感性
学习率 是梯度下降中最关键的超参数,也是最大的痛点:
- 固定 的问题:理想的步长应该在陡峭区域小一些,在平缓区域大一些,但固定步长无法适应这种变化
学习率选择的困境: 如果选择一个保守的小学习率,在平坦区域可能需要成千上万次迭代才能取得有意义的进展;如果选择一个激进的大学习率,可能在进入陡峭区域时直接“炸掉”。这种两难处境是梯度下降法最令人头疼的问题之一。
2.3 梯度下降的变体
根据每次迭代使用的数据量,梯度下降有三种主要变体,它们在计算效率和收敛稳定性之间做出不同的权衡。
每次迭代使用全部训练样本计算梯度。
优点: 梯度估计无偏,收敛路径稳定 理论上可以保证线性收敛到局部最优
缺点:当 很大时,每次迭代计算量巨大,无法在线学习(新数据到来需要重新训练),容易陷入鞍点或局部最优,缺乏随机性带来的“逃离”能力
实践洞察:在实际深度学习中,小批量梯度下降是事实上的标准。批量大小通常选择32-512之间,这既保证了计算效率,又提供了足够的随机性来帮助优化。
2.4 从困境到突破:改进的方向
面对这些挑战,研究者们发展出了多种改进策略,这些策略构成了后续章节的基础:
动量法:通过累积历史梯度信息,在震荡方向相互抵消,在一致方向加速前进,有效应对病态曲率。这相当于给下山过程增加了“惯性”,让优化过程更加平滑。
自适应学习率:为每个参数单独调整学习率,在稀疏特征上迈大步,在频繁特征上迈小步。这相当于为每个方向定制了不同的步长策略。
随机梯度下降:引入噪声梯度,既能加速计算,又能帮助逃离鞍点。虽然路径更加曲折,但往往能找到更好的最终解。
这些方法将在后续章节中详细展开。理解梯度下降的基本原理和局限性,就像掌握了下山的基本技巧,为学习更高级的导航策略奠定了坚实的基础。
三、进阶——历史信息的利用(动量法与自适应)
梯度下降法虽然简单优雅,但其局限性促使研究者思考:能否让优化算法具备“记忆”和“自适应”能力?就像一个有经验的登山者,不仅感知脚下的坡度,还能记住之前走过的路径,并根据不同地形的特点调整步伐。本章将介绍两类核心改进方法——动量法与自适应学习率,它们分别从不同角度解决了梯度下降的困境。
3.1 动量法:冲出峡谷的惯性
想象一个小球从山顶滚下:如果遇到平坦区域,凭借积累的动能可以继续前进而不停滞;如果遇到陡峭的震荡坡道,惯性会抵消来回的震荡让路径更加平滑;如果遇到小的坑洼,动能可以帮助小球冲出去。这正是动量法的核心思想——让优化过程具有“惯性”。
动量法引入了一个速度变量 ,用来累积历史梯度的指数衰减平均:,然后参数更新为 。动量系数 通常取0.9或0.99,控制历史信息的衰减速度。当 时,动量法退化为标准梯度下降。
这种设计的精妙之处在于:如果在多个连续步骤中梯度方向一致,速度会不断累积,从而在平坦区域加速前进;如果梯度方向频繁改变,速度会被抵消,从而抑制震荡。这完美解决了病态曲率问题——在陡峭方向震荡相互抵消,在平缓方向速度持续累积。实践中,带动量的SGD几乎总是优于无动量的版本,是深度学习训练的事实标准。
3.2 自适应学习率:为每个参数定制步伐
标准梯度下降对所有参数使用相同的学习率,这在高维问题中显然不合理:有些参数可能频繁更新,需要较小的步长以避免震荡;有些参数更新稀疏,需要较大的步长以获得有意义的进展。自适应学习率方法正是为了解决这一问题——为每个参数单独调整学习率。
AdaGrad是自适应学习率的开创性工作,其核心思想是:对于频繁更新的参数,累积的梯度平方和大,因此降低学习率;对于稀疏更新的参数,累积梯度平方和小,因此保持较大的学习率。具体地,AdaGrad维护每个参数的梯度平方累积和 ,然后参数更新为 。分母中的 起到了归一化作用。
AdaGrad在处理稀疏特征时表现出色,例如在自然语言处理或推荐系统中,某些特征极少出现,AdaGrad能确保这些特征获得有效的更新。但AdaGrad有一个致命缺陷:由于累积平方和单调增长,学习率会持续衰减直至趋近于零,导致训练后期模型停止学习。对于深度神经网络这种需要长期训练的任务,这显然是不可接受的。
为了解决AdaGrad的衰减死亡问题,RMSProp用指数移动平均替代了历史累积和:。参数更新为 。指数移动平均的引入使得RMSProp能够“遗忘”遥远的过去,只关注最近的梯度信息。这样,当损失景观发生变化时,RMSProp可以快速调整学习率,而不会像AdaGrad那样被历史梯度困住。
3.3 集大成者——Adam
如果说动量法解决了方向问题,RMSProp解决了步长问题,那么Adam就是将两者优雅结合的集大成者。Adam同时维护一阶矩和二阶矩的指数移动平均,成为当前最流行、最通用的优化算法之一。
Adam维护两个状态变量:一阶矩估计 是梯度的指数移动平均(动量项),二阶矩估计 是梯度平方的指数移动平均(自适应项)。具体计算过程如下:首先计算当前梯度 ,然后更新带偏置校正的一阶矩和二阶矩 ,。其中 是衰减率,通常取 。
由于 和 初始化为零,在训练初期会偏向于零,因此需要进行偏差校正:,。最后进行参数更新:。
Adam的优势在于:每个参数都有独立调整的学习率,无需手动调节;一阶矩提供了惯性,平滑了更新方向;偏差校正解决了训练初期估计不准的问题,确保快速启动;默认超参数在大多数问题上表现良好,通常不需要精细调参。正因如此,Adam成为研究人员快速验证想法的首选优化器。
Adam的成功也催生了一系列改进版本,如将Nesterov动量融入Adam的Nadam,解决某些情况下不收敛问题的AMSGrad,以及修正权重衰减实现方式的AdamW。其中AdamW由于将权重衰减与梯度更新解耦,显著提升了泛化性能,在很多任务上超越了原始Adam。
四、实战演练——优化器对决
理论终究需要实践的检验。在前三章中,我们从数学原理上理解了不同优化算法的工作机制,本章将通过具体的代码实现和实验对比,直观地观察SGD、SGD+Momentum、RMSProp和Adam四种优化器在同一任务上的表现差异。
4.1 实验设计与实现
本次实验选择月牙形二分类数据集(make_moons),两类样本呈月牙形交错分布,线性模型无法解决,需要神经网络这样的非线性模型。数据集包含1000个样本、2个特征,既保证了训练效率,又能体现出不同优化器的差异,二维特征还便于直接绘制决策边界。
我们使用一个简单的两层神经网络:输入层2个神经元(对应两个特征),隐藏层10个神经元使用tanh激活,输出层1个神经元使用sigmoid激活得到二分类概率。损失函数为交叉熵损失。这个架构足够简单,不会掩盖优化器本身的特性差异。
四种优化器的超参数设置如下:
- SGD+Momentum:学习率0.1,动量系数0.9
所有优化器均使用批量大小为32的mini-batch训练,共300个epoch。每个epoch后记录训练损失,每10个epoch在测试集上计算准确率。
"""优化算法对比实验:从SGD到Adam本代码在一个简单的二分类问题上对比不同优化器的收敛速度和性能"""import numpy as npimport matplotlib.pyplot as pltfrom sklearn.datasets import make_moonsfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import StandardScalerimport warningswarnings.filterwarnings('ignore')# 设置中文字体plt.rcParams['font.sans-serif'] = ['SimHei']plt.rcParams['axes.unicode_minus'] = False# ==================== 数据准备 ====================defgenerate_data():"""生成月牙形二分类数据""" X, y = make_moons(n_samples=1000, noise=0.2, random_state=42) X = StandardScaler().fit_transform(X) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 )return X_train, X_test, y_train, y_test# ==================== 模型定义 ====================classNeuralNetwork:"""两层的神经网络"""def__init__(self, input_size=2, hidden_size=10, output_size=1):# 初始化参数 self.W1 = np.random.randn(input_size, hidden_size) * 0.1 self.b1 = np.zeros((1, hidden_size)) self.W2 = np.random.randn(hidden_size, output_size) * 0.1 self.b2 = np.zeros((1, output_size))defforward(self, X):"""前向传播""" self.z1 = np.dot(X, self.W1) + self.b1 self.a1 = np.tanh(self.z1) # 隐藏层激活 self.z2 = np.dot(self.a1, self.W2) + self.b2 self.a2 = 1 / (1 + np.exp(-self.z2)) # sigmoid输出return self.a2defbackward(self, X, y, output):"""反向传播计算梯度""" m = X.shape[0]# 输出层梯度 dz2 = output - y.reshape(-1, 1) dW2 = np.dot(self.a1.T, dz2) / m db2 = np.sum(dz2, axis=0, keepdims=True) / m# 隐藏层梯度 da1 = np.dot(dz2, self.W2.T) dz1 = da1 * (1 - np.tanh(self.z1) ** 2) dW1 = np.dot(X.T, dz1) / m db1 = np.sum(dz1, axis=0, keepdims=True) / mreturn {'W1': dW1, 'b1': db1, 'W2': dW2, 'b2': db2}defcompute_loss(self, X, y):"""计算二分类交叉熵损失""" output = self.forward(X) loss = -np.mean(y * np.log(output + 1e-8) + (1 - y) * np.log(1 - output + 1e-8))return lossdefpredict(self, X):"""预测类别""" output = self.forward(X)return (output > 0.5).astype(int)# ==================== 优化器实现 ====================classSGD:"""标准随机梯度下降"""def__init__(self, learning_rate=0.01): self.lr = learning_rate self.name = 'SGD'defupdate(self, params, grads):for key in params.keys(): params[key] -= self.lr * grads[key]return paramsclassSGDMomentum:"""带动量的SGD"""def__init__(self, learning_rate=0.01, momentum=0.9): self.lr = learning_rate self.momentum = momentum self.velocities = {} self.name = 'SGD+Momentum'defupdate(self, params, grads):for key in params.keys():if key notin self.velocities: self.velocities[key] = np.zeros_like(params[key]) self.velocities[key] = self.momentum * self.velocities[key] - self.lr * grads[key] params[key] += self.velocities[key]return paramsclassRMSProp:"""RMSProp优化器"""def__init__(self, learning_rate=0.01, decay_rate=0.9, epsilon=1e-8): self.lr = learning_rate self.decay_rate = decay_rate self.epsilon = epsilon self.cache = {} self.name = 'RMSProp'defupdate(self, params, grads):for key in params.keys():if key notin self.cache: self.cache[key] = np.zeros_like(params[key]) self.cache[key] = self.decay_rate * self.cache[key] + (1 - self.decay_rate) * grads[key]**2 params[key] -= self.lr * grads[key] / (np.sqrt(self.cache[key]) + self.epsilon)return paramsclassAdam:"""Adam优化器"""def__init__(self, learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8): self.lr = learning_rate self.beta1 = beta1 self.beta2 = beta2 self.epsilon = epsilon self.m = {} self.v = {} self.t = 0 self.name = 'Adam'defupdate(self, params, grads): self.t += 1for key in params.keys():if key notin self.m: self.m[key] = np.zeros_like(params[key]) self.v[key] = np.zeros_like(params[key])# 更新一阶矩和二阶矩 self.m[key] = self.beta1 * self.m[key] + (1 - self.beta1) * grads[key] self.v[key] = self.beta2 * self.v[key] + (1 - self.beta2) * grads[key]**2# 偏差校正 m_hat = self.m[key] / (1 - self.beta1**self.t) v_hat = self.v[key] / (1 - self.beta2**self.t)# 更新参数 params[key] -= self.lr * m_hat / (np.sqrt(v_hat) + self.epsilon)return params# ==================== 训练函数 ====================deftrain_model(X_train, y_train, X_test, y_test, optimizer, epochs=500, batch_size=32):"""训练一个epoch返回损失和准确率历史""" model = NeuralNetwork() train_losses = [] test_accuracies = [] n_samples = X_train.shape[0]for epoch in range(epochs):# Mini-batch训练 indices = np.random.permutation(n_samples) epoch_loss = 0 n_batches = 0for start in range(0, n_samples, batch_size): end = min(start + batch_size, n_samples) batch_idx = indices[start:end] X_batch = X_train[batch_idx] y_batch = y_train[batch_idx]# 前向传播 output = model.forward(X_batch)# 计算梯度 grads = model.backward(X_batch, y_batch, output)# 更新参数 model.W1, model.b1, model.W2, model.b2 = optimizer.update( {'W1': model.W1, 'b1': model.b1, 'W2': model.W2, 'b2': model.b2}, grads ).values()# 累加损失 batch_loss = -np.mean(y_batch * np.log(output + 1e-8) + (1 - y_batch) * np.log(1 - output + 1e-8)) epoch_loss += batch_loss n_batches += 1# 记录训练损失 train_losses.append(epoch_loss / n_batches)# 每10个epoch计算测试准确率if epoch % 10 == 0: y_pred = model.predict(X_test) accuracy = np.mean(y_pred.flatten() == y_test) test_accuracies.append(accuracy)return train_losses, test_accuracies# ==================== 主程序 ====================defmain():# 生成数据 X_train, X_test, y_train, y_test = generate_data() print(f"训练集大小: {X_train.shape[0]}, 测试集大小: {X_test.shape[0]}")# 定义要对比的优化器 optimizers = [ SGD(learning_rate=0.1), SGDMomentum(learning_rate=0.1, momentum=0.9), RMSProp(learning_rate=0.01, decay_rate=0.9), Adam(learning_rate=0.01, beta1=0.9, beta2=0.999) ]# 存储结果 results = {}# 训练每个优化器for opt in optimizers: print(f"\n训练优化器: {opt.name}") train_losses, test_accuracies = train_model( X_train, y_train, X_test, y_test, opt, epochs=300 ) results[opt.name] = {'loss': train_losses,'accuracy': test_accuracies } print(f"最终损失: {train_losses[-1]:.4f}, 最终准确率: {test_accuracies[-1]*100:.2f}%")# ==================== 可视化对比 ==================== fig, axes = plt.subplots(1, 2, figsize=(14, 5))# 左图:损失下降曲线for name, res in results.items(): axes[0].plot(res['loss'], label=name, linewidth=2) axes[0].set_xlabel('Epoch', fontsize=12) axes[0].set_ylabel('Cross-Entropy Loss', fontsize=12) axes[0].set_title('训练损失下降曲线对比', fontsize=14) axes[0].legend() axes[0].grid(True, alpha=0.3)# 右图:测试准确率 epochs = [i*10for i in range(len(list(results.values())[0]['accuracy']))]for name, res in results.items(): axes[1].plot(epochs, [acc*100for acc in res['accuracy']], marker='o', label=name, linewidth=2, markersize=4) axes[1].set_xlabel('Epoch', fontsize=12) axes[1].set_ylabel('Accuracy (%)', fontsize=12) axes[1].set_title('测试集准确率变化对比', fontsize=14) axes[1].legend() axes[1].grid(True, alpha=0.3) plt.tight_layout() plt.savefig('optimizer_comparison.png', dpi=150, bbox_inches='tight') plt.show()# ==================== 收敛速度分析 ==================== print("\n" + "="*50) print("收敛速度分析(达到指定损失所需的epoch数)") print("="*50) target_loss = 0.3for name, res in results.items(): losses = res['loss'] epochs_to_target = next((i for i, loss in enumerate(losses) if loss < target_loss), None)if epochs_to_target: print(f"{name:15} 达到损失 {target_loss} 需要 {epochs_to_target} 个epoch")else: print(f"{name:15} 未能在300个epoch内达到损失 {target_loss}")# ==================== 决策边界可视化 ====================# 重新训练一个Adam模型用于可视化决策边界 final_model = NeuralNetwork() adam = Adam(learning_rate=0.01)# 完整训练for epoch in range(500): output = final_model.forward(X_train) grads = final_model.backward(X_train, y_train, output) final_model.W1, final_model.b1, final_model.W2, final_model.b2 = adam.update( {'W1': final_model.W1, 'b1': final_model.b1, 'W2': final_model.W2, 'b2': final_model.b2}, grads ).values()# 绘制决策边界 fig, ax = plt.subplots(figsize=(8, 6))# 创建网格 x_min, x_max = X_train[:, 0].min() - 0.5, X_train[:, 0].max() + 0.5 y_min, y_max = X_train[:, 1].min() - 0.5, X_train[:, 1].max() + 0.5 xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200), np.linspace(y_min, y_max, 200))# 预测网格点 grid_points = np.c_[xx.ravel(), yy.ravel()] Z = final_model.predict(grid_points) Z = Z.reshape(xx.shape)# 绘制决策边界和数据点 ax.contourf(xx, yy, Z, alpha=0.3, cmap=plt.cm.RdYlBu) scatter = ax.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=plt.cm.RdYlBu, edgecolor='black', s=30) ax.set_xlabel('Feature 1', fontsize=12) ax.set_ylabel('Feature 2', fontsize=12) ax.set_title('Adam优化器学习到的决策边界', fontsize=14) plt.colorbar(scatter) plt.tight_layout() plt.savefig('decision_boundary.png', dpi=150, bbox_inches='tight') plt.show()if __name__ == "__main__": main()
4.2 实验结果与对比分析
运行代码后,我们得到四种优化器的训练损失下降曲线和测试准确率变化曲线。下面从三个维度进行分析:



4.2.1 收敛速度对比
从训练损失下降曲线可以清晰看到:
- Adam收敛最快,前50个epoch内损失就从初始值急剧下降到0.3以下,展现出动量与自适应学习率结合的优势
- RMSProp紧随其后,虽然初期略慢于Adam,但下降趋势同样迅速
- SGD+Momentum收敛速度中等,由于动量累积需要时间,初期下降较慢,但后期持续稳定下降
- SGD收敛最慢,全程保持缓慢的下降趋势,300个epoch后损失仍高于其他优化器
收敛速度的量化对比:以损失降至0.3为目标,Adam约需40个epoch,RMSProp约需60个epoch,SGD+Momentum约需90个epoch,而SGD在300个epoch内始终未能达到这一目标。
4.2.2 泛化能力对比
测试集准确率曲线揭示了不同优化器的泛化性能:
- SGD+Momentum最终准确率最高,达到92%以上,虽然收敛慢但找到了泛化性更好的解
- Adam和RMSProp准确率相近,约为90-91%,略低于SGD+Momentum
这一现象与机器学习中的经典观察一致:自适应学习率方法(Adam、RMSProp)往往收敛更快,但带动量的SGD经过精细调参后,常常能获得更好的泛化性能。可能的解释是自适应方法倾向于找到尖锐的极小值,而SGD+Momentum更容易收敛到平坦的极小值,后者在测试集上更鲁棒。
4.2.3 稳定性对比
观察损失曲线的平滑程度和震荡幅度:
- Adam曲线最平滑,得益于动量对梯度噪声的抑制和自适应步长的调节
- RMSProp也比较稳定,但在某些区域仍有小幅震荡
- SGD+Momentum初期震荡明显,随着动量累积逐渐平稳
- SGD全程震荡最剧烈,即使接近收敛时参数仍在最优值附近徘徊
4.3 决策边界可视化
训练完成后,我们用Adam优化器训练得到的模型绘制决策边界。可以看到,神经网络成功学出了一个非线性的分类曲面,将月牙形的两类样本有效分开。决策边界在数据密集区域弯曲程度大,在稀疏区域趋于平滑,符合预期。
4.4 洞察与结论
综合实验结果,我们可以得出以下实践指南:
快速原型验证:首选Adam。它在大多数问题上收敛快、超参数鲁棒、无需精细调参,能让研究者快速迭代想法。本次实验中Adam仅用40个epoch就达到可用精度,远快于其他方法。
追求最佳泛化:尝试SGD+Momentum。虽然需要更多调参时间(学习率、动量系数、学习率调度策略),但往往能获得更高的测试准确率。实验中SGD+Momentum最终准确率领先Adam约2个百分点,这在许多实际任务中是显著的提升。
处理稀疏特征:当特征非常稀疏时(如文本数据、推荐系统),AdaGrad或Adam是更好的选择,因为它们能为罕见特征分配较大的学习率。
长期训练任务:避免使用AdaGrad,因为其学习率会持续衰减直至停止。RMSProp和Adam更适合需要长时间训练的任务。
批量大小的选择:较小的批量(32-128)引入适度噪声,有助于逃离鞍点;较大的批量(256以上)梯度估计更准,但可能陷入尖锐极小值。实践中需根据数据规模和硬件条件权衡。
最后需要强调的是,没有绝对最优的优化器,只有最适合特定问题的优化器。理解每种方法的原理和特点,才能针对具体任务做出合理的选择。实验代码附后,读者可以自行调整超参数、更换数据集,探索不同优化器的表现。
最后希望同学们能够用自己的电脑跑一跑python的代码,多上手实战感受一下这种优化方式!
参考资料:
- https://bbs.huaweicloud.com/blogs/462952
- https://zhuanlan.zhihu.com/p/2011206958548547477