指数加权平均:从温度平滑到Adam优化器的核心机制

发布时间:2026/10/19 19:36:28
指数加权平均:从温度平滑到Adam优化器的核心机制
1. 从一个温度曲线说起为什么优化算法需要“加权平均”先说一个我当年刚接触深度学习时的困惑处理时间序列数据比如某地一年的每日温度数据往往带有很大的随机噪声。今天30度明天25度后天突然跳到31度如果直接把原始数据拿来画曲线你会看到一条锯齿状、完全看不出趋势的线。但如果我们把连续几天的温度做个平均呢比如取前后各3天的滑动平均曲线就平滑多了能看出季节变化。这就是最朴素的想法——用历史数据的平均值抵掉单日观测的随机起伏。可是滑动窗口平均有个先天缺陷它要保存最近N天的原始数据窗口越长占的内存就越多。而且窗口内所有天的权重是一样的昨天和10天前的温度对“今天该是多少度”的参考价值显然不同。更麻烦的是窗口滑到边界时数据会突变算出来的平均值会跳动。指数加权平均Exponential Weighted Average, EWA就是来解决这些问题的。它不需要保存整个窗口只需要一个变量每来一天数据就更新一次而且越近的数据权重越大越远的数据按指数速度衰减。这种特性让它成了深度学习优化算法的基础模块——Momentum、RMSProp、Adam底层全都在用这个思想。所以如果你打算认真学深度学习优化器就绕不开EWA。它不是某个特定优化器的附属品而是一套更底层的“平滑工具”理解了它后面看优化器源码会轻松很多。2. 指数加权平均的核心思路公式是怎么来的2.1 一行公式解决三个麻烦EWA的更新公式长这样v_t β * v_{t-1} (1 - β) * θ_t其中θ_t是第t时刻的真实观测值比如第t天的温度v_t是第t时刻的指数加权平均值也是我们维护的那个“变量”β是衰减因子取值范围在0到1之间通常取0.9、0.98、0.999这类接近1的值刚看到这个公式的人第一反应往往是这不就是“新旧数据的加权平均”吗新数据权重(1-β)上一步平均值权重β对吧对但又不完全对。如果只是新旧两个数字做加权平均那这个式子顶多是个平滑滤波器。它的精妙之处在于v_t会一直向下“传递”。你把v_t逐步展开会得到一个累加和v_t (1-β) * (θ_t β * θ_{t-1} β² * θ_{t-2} β³ * θ_{t-3} ...)每一项的系数是β的k次方。β取0.9时第10天前的数据权重已经变成了0.9^10 ≈ 0.349第50天前只剩0.9^50 ≈ 0.005基本可以忽略。看它天然就把“遗忘”这件事写在系数里了。这也顺势解决了三个问题需要存历史数据吗不需要。无论时间线拉多长你手上只有一个v_t变量在跟着迭代走。权重怎么分配越近的数据权重越大而且是指数式递减这符合绝大多数时间序列的直觉——近期信息比远期信息更有参考价值。窗口边界突变怎么办根本不存在窗口概念自然不会因为窗口滑动产生突变。2.2 展开式里的数学直觉来看一个具体例子。假设β0.9某天的v_t 100第二天真实温度θ_{t1}20那v_{t1} 0.9*100 0.1*20 92。你会看到新值20只让平均线挪动了8个点。原因很简单β0.9意味着上一轮积累的信息占了90%的权重单日噪声被大幅压制了。反过来如果β0.5那么v_{t1} 0.5*100 0.5*20 60平均值瞬间掉下来40个点曲线对噪声就非常敏感。所以β本质上就是一个“记忆长度”调节器。β越接近1记忆越长曲线越平滑β越小响应越快但噪声越大。3. β这个超参数到底在控制什么3.1 0.5、0.9、0.999的直观差异常有人在代码里随手填一个β但不知道这个数字背后对应多长的记忆范围。有个近似公式可以帮我们估算“有效记忆长度”大约影响近 1/(1-β) 个时间步的数据这个近似是怎么来的因为β^k衰减到约1/e ≈ 0.367时可认为影响基本消失。如果β0.90.9^k ≤ 0.1需要k≈22而直接算1/(1-0.9)10。其实更标准的是让β^k 1/e得出k ln(1/e) / ln(β) -1 / ln(β)。当β分别取0.9、0.99、0.999时β值有效记忆步数约平滑程度典型用途0.51~2步几乎不平滑极少用过于敏感0.910步左右轻度平滑Momentum默认值0.99100步左右较强平滑RMSProp、Adam默认值0.9991000步左右极强平滑Adam的二阶矩估计3.2 用温度数据比一比假设有30天温度序列第1~10天在30度附近第11~20天在20度附近第21~30天在25度附近中间叠加随机噪声。β0.9的曲线基本贴合真实温度变化滞后大约2~3天噪声被削掉大半。β0.99的曲线非常平滑但变化也慢第11天温度骤降时它要用好几天才能“拐过弯”来滞后明显。β0.5的曲线贴着原始数据抖平滑效果差但几乎没有滞后。这就是EWA的经典“利弊交换”平滑度和响应速度不可兼得。想更平滑就要忍受更大的延迟想快速跟上变化就得接受噪声。这个取舍在优化算法里尤其重要训练初期梯度方向变来变去需要短记忆赶快修正训练后期梯度变小需要长记忆稳定方向。很多进阶优化器之所以效果好正是因为它能在不同阶段动态调整这个“记忆长度”。4. 初学者最常踩的坑偏差修正4.1 为什么一开始的EWA值“偏低”如果你直接照着公式初始化v_0 0然后开始更新会观察到一个尴尬现象前几步的v_t明显偏小和真实值对不上。举例。β0.9真实温度第一天θ_1 30v_1 0.9*0 0.1*30 3第一天算出的平均值是3而真实温度是30差了快10倍。此时如果直接拿3去当“平均温度”用误差极大。原因是初始的v_00是人为设定的虚拟值它把前面所有真实值都“稀释”了。这就像你运动手环第一天戴系统不知道你过去水平用0作为起点算出来的平均值自然被拉低。随着迭代继续v_t会逐渐逼近真实平均水平但“起步阶段偏低”这个偏差确实存在。在训练神经网络时这个偏差会影响早期的梯度更新方向严重时让前几百步的训练变得不够稳定。4.2 修正公式与实操示例标准做法是在计算v_t后进行修正v_t_corrected v_t / (1 - β^t)看上面β0.9, t1的情况v_1_corrected 3 / (1 - 0.9^1) 3 / 0.1 30正好修正回真实温度30。t越大β^t越接近0v_t_corrected就越接近v_t。也就是说偏差修正只在前期起作用后期影响可忽略。实际操作中需要注意修正用t不是用t1很多照抄公式的代码会用错。如果不修正训练初期动量项会有偏向0的错误信号影响收敛速度。PyTorch的SGD源码里momentum版本其实没有刻意分出一套完全独立的修正逻辑但Adam里做了类似处理可见这个细节在不同优化器里处理方式并不完全一样。5. 从EWA到Momentum优化器的第一块拼图5.1 动量法到底改了什么标准梯度下降每次更新参数是w w - learning_rate * gradient问题在于梯度本身是有噪声的。一个batch的梯度可能指向A方向下个batch又偏向B方向更新路径非常抖收敛慢。动量法Momentum引入了一个“速度项”把每步梯度也做一次EWAv_t β * v_{t-1} (1-β) * gradient_t w w - learning_rate * v_t你看这不就是EWA吗v_t在这里扮演的是“梯度的指数加权平均”。因为梯度是向量所以v_t也是向量每个维度独立做EWA。这样改的效果是如果某几个batch的梯度方向大致一致这些梯度的平均向量会朝着一个“相对稳定”的方向前进如果某个batch的梯度方向跑偏了EWA会把它压下去。生活类比你推一个购物车下坡每次有人从侧面轻轻碰一下车购物车不会立刻转向因为之前积累的动量还在推着它向前。动量法就是这个原理——动量越大抗噪声能力越强。5.2 对动量项的直观理解动量法里β通常取0.9相当于最近约10步的梯度在共同决定更新方向。单个batch带来的局部噪声被后面十步的分量平均掉了。每步更新不再听当前gradient的“片面之词”而是看一段时间的“集体意见”。和标准SGD相比Momentum在弯曲的误差面上表现好很多。比如某个维度上梯度方向频繁翻转标准SGD会来回震荡、走得很慢动量法则因为累计速度项不会彻底转向整体向前。实测下来这类优化器在图像分类、目标检测这种标准任务上比普通SGD收敛快损失曲线也更平滑。我最早在某个图像分类模型上试过不加动量loss曲线波动很大加了之后明显稳定。6. RMSProp与AdamEWA的更高级玩法6.1 RMSProp如何用上EWARMSProp解决的问题是“不同参数维度的尺度差异”。有的权重参数梯度大有的梯度常年很小统一用一个学习率就容易顾此失彼。RMSProp的思路是给每个参数单独维护一个梯度平方的EWAs_t β * s_{t-1} (1-β) * (gradient_t)²更新时w w - learning_rate / sqrt(s_t ε) * gradient_t注意这里EWA的对象变成了梯度平方。梯度大的维度s_t就大sqrt(s_t)大除以它之后有效学习率被压低梯度小的维度有效学习率被放大。这就是“自适应学习率”的核心思想。这里面EWA的作用同样是“平滑”。梯度平方值单看一个batch可能非常大但如果它只是偶发大值EWA会把它平摊到历史里不让某一步的异常梯度把学习率短期压得特别低。6.2 Adam双EWA的组合拳Adam把动量法和RMSProp结合同时维护两个EWAm_t梯度的指数加权平均对应动量项默认β10.9v_t梯度平方的指数加权平均对应自适应学习率默认β20.999更新时它对这两项都要做偏差修正m_t_corrected m_t / (1 - β1^t) v_t_corrected v_t / (1 - β2^t) w w - learning_rate * m_t_corrected / (sqrt(v_t_corrected) ε)注意β20.999意味着它要累计近千步的梯度平方信息所以二阶矩的估计非常稳定但也意味着早期v_t严重偏小偏差最明显必须靠偏差修正扳回来。如果你在实现Adam时漏掉这一步前期参数更新会异常大模型容易直接飞掉。所以你看Adam并不是什么神秘算法它就是一个“梯度一阶矩的EWA”加上一个“梯度二阶矩的EWA”再配一个偏差修正机制。理解EWAAdam源码里的大部分逻辑就已经清楚了。7. 手写EWAPython代码实现与调参实验7.1 基础版EWA实现下面这份代码实现了基础EWA并对比了不同β的平滑效果。我平时做演示时会生成一个带噪声的合成时间序列模拟温度漂移。import numpy as np import matplotlib.pyplot as plt def exponential_weighted_average(series, beta0.9): v 0.0 result [] for t, value in enumerate(series, start1): v beta * v (1 - beta) * value # 如果做偏差修正用下面这一行 # v_corrected v / (1 - beta ** t) result.append(v) return np.array(result) # 生成模拟温度数据三段不同均值 噪声 np.random.seed(42) t np.arange(1, 121) true_temp np.where(t 40, 10, np.where(t 80, 30, 20)) noise np.random.normal(0, 2, sizet.shape) data true_temp noise plt.figure(figsize(10, 5)) plt.plot(t, data, alpha0.3, labelraw data) for beta in [0.5, 0.9, 0.99]: ewa exponential_weighted_average(data, betabeta) plt.plot(t, ewa, labelfbeta{beta}) plt.legend() plt.xlabel(day) plt.ylabel(temperature) plt.title(Exponential Weighted Average with different beta) plt.show()这段代码输出三根曲线直接能看出β0.5跟随噪声波动β0.99平滑但明显滞后β0.9是相对均衡的选择。7.2 带偏差修正的版本加了偏差修正的版本变化只在更新的那一行def exponential_weighted_average_corrected(series, beta0.9): v 0.0 result [] for t, value in enumerate(series, start1): v beta * v (1 - beta) * value v_corrected v / (1 - beta ** t) result.append(v_corrected) return np.array(result)用同一份数据跑一遍把修正前后的曲线叠加对比你能看到前10步差异明显——未修正的曲线从0附近爬升修正后的曲线起手就贴合真实均值。训练类模型时建议直接用修正版避免早期更新方向偏掉。7.3 在简单梯度下降里显式使用EWA下面用随机生成的小数据集手写一个仅含一个线性层的模型显式加入EWA动量观察收敛行为。import torch import torch.nn as nn torch.manual_seed(0) X torch.randn(200, 5) y X torch.tensor([1.5, -2.0, 3.0, 0.5, -1.0], dtypetorch.float32) 0.1 model nn.Linear(5, 1, biasFalse) optimizer torch.optim.SGD(model.parameters(), lr0.02, momentum0.9) loss_fn nn.MSELoss() for epoch in range(200): optimizer.zero_grad() loss loss_fn(model(X).squeeze(), y) loss.backward() optimizer.step() if epoch % 20 0: print(fepoch {epoch}, loss {loss.item():.4f})这里的momentum0.9就是EWA的β。你把它改成0或0.99跑一遍对比loss曲线能直观体会到β对收敛速度和平稳性的影响。我个人试下来0.5看不出明显平滑效果0.99起步时loss下降变慢但后期更稳。8. 实操中的建议与常见问题8.1 什么时候需要修β对应的步数实际训练里很少有人只调β一个参数因为β和learning rate是联动的。β增大时EWA记忆更长、速度项更稳定。但速度项的更新幅度也变了需要配合调整学习率。一个不严谨但直观的经验β从0.9提到0.99相当于速度项“惯性”大了约10倍相应学习率一般要适当调小防止更新幅度过大反过来β调小时模型响应变快但要小心loss震荡这种联动关系在Adam上调β2频率不高因为0.999已经是相对稳定的默认值。很多框架里提供的beta参数其实建议先用默认值不要随手改。8.2 网上那些“调参玄学”可靠吗经常看到有人总结“β0.9是动量最优值”“Adam不用调β”之类的话。我个人的体验是这些说法有实践基础但别太迷信。β0.9只是在大量任务中表现稳定不代表它对你的任务最优。如果你的训练数据分布变动很快0.9的步数记忆可能太长可以试0.8甚至0.7验证集上观察两三轮就能看出差异。另外还要注意有些框架对动量实现方式是“乘在梯度上”比如PyTorch的SGD源码里更新公式在实现上与理论公式有细微差别。照抄理论代码去对齐框架实现刚开始会对不上这不奇怪重点看损失曲线是不是稳定下降。8.3 几个常见问题EWA和滑动窗口平均结果接近吗接近但EWA更省内存且能对近期数据倾斜。若要严格的窗口内平均需要存储窗口数据在小批量场景下一般没这个必要。偏差修正只对初始阶段有用对。t越大β^t越接近0修正项接近1。但t1时的差异可能达数十倍不能省。RMSProp里的ε是干什么的加在除根号上防止除以0一般取1e-8。太小早期梯度平方接近0时数值不稳太大会把自适应调节能力削弱。能不能用EWA做数据预处理当然可以。很多时序平滑、滑动统计都可以用EWA替代只需选好β。8.4 别踩的坑最普遍的坑是把EWA值直接当原始信号用。EWA的设计目标是体现“当前趋势”不是每个时刻的精确值。评估模型时如果需要精确值该用原始数据就用原始数据EWA只适合展示趋势不适合做最后的标准答案。还有人在实现Adam时对一阶动量做偏差修正、对二阶动量不做修正。这样早期二阶矩严重偏小有效学习率虚高实践里经常导致第一步参数猛跳。修正要么都做要么都别做偏工减料是最危险的。最后建议你把上面那几段Python代码跑一遍改成自己的时间序列比如股票收盘价、传感器读数直观感受一下β从0.5调到0.999时曲线的形态变化。这里面积累的手感比记住任何公式都重要。