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,第二天真实温度θ_{t+1}=20,那v_{t+1} = 0.9*100 + 0.1*20 = 92。你会看到,新值20只让平均线挪动了8个点。原因很简单,β=0.9意味着上一轮积累的信息占了90%的权重,单日噪声被大幅压制了。反过来,如果β=0.5,那么v_{t+1} = 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.9,0.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 = 30:v_1 = 0.9*0 + 0.1*30 = 3第一天算出的平均值是3,而真实温度是30,差了快10倍。此时如果直接拿3去当“平均温度”用,误差极大。原因是:初始的v_0=0是人为设定的虚拟值,它把前面所有真实值都“稀释”了。这就像你运动手环第一天戴,系统不知道你过去水平,用0作为起点,算出来的平均值自然被拉低。随着迭代继续,v_t会逐渐逼近真实平均水平,但“起步阶段偏低”这个偏差确实存在。在训练神经网络时,这个偏差会影响早期的梯度更新方向,严重时让前几百步的训练变得不够稳定。4.2 修正公式与实操示例标准做法是在计算v_t后进行修正:v_t_corrected = v_t / (1 - β^t)看上面β=0.9, t=1的情况:v_1_corrected = 3 / (1 - 0.9^1) = 3 / 0.1 = 30正好修正回真实温度30。t越大,β^t越接近0,v_t_corrected就越接近v_t。也就是说,偏差修正只在前期起作用,后期影响可忽略。实际操作中需要注意:修正用t,不是用t+1,很多照抄公式的代码会用错。如果不修正,训练初期动量项会有偏向0的错误信号,影响收敛速度。PyTorch的SGD源码里momentum版本其实没有刻意分出一套完全独立的修正逻辑,但Adam里做了类似处理,可见这个细节在不同优化器里处理方式并不完全一样。5. 从EWA到Momentum:优化器的第一块拼图5.1 动量法到底改了什么标准梯度下降每次更新参数是:w = w - learning_rate * gradient问题在于,梯度本身是有噪声的。一个batch的梯度可能指向A方向,下个batch又偏向B方向,更新路径非常抖,收敛慢。动量法(Momentum)引入了一个“速度项”,把每步梯度也做一次EWA:v_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与Adam:EWA的更高级玩法6.1 RMSProp如何用上EWARMSProp解决的问题是“不同参数维度的尺度差异”。有的权重参数梯度大,有的梯度常年很小,统一用一个学习率就容易顾此失彼。RMSProp的思路是:给每个参数单独维护一个梯度平方的EWA:s_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结合,同时维护两个EWA:m_t:梯度的指数加权平均,对应动量项,默认β1=0.9v_t:梯度平方的指数加权平均,对应自适应学习率,默认β2=0.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) + ε)注意β2=0.999意味着它要累计近千步的梯度平方信息,所以二阶矩的估计非常稳定,但也意味着早期v_t严重偏小(偏差最明显),必须靠偏差修正扳回来。如果你在实现Adam时漏掉这一步,前期参数更新会异常大,模型容易直接飞掉。所以你看,Adam并不是什么神秘算法,它就是一个“梯度一阶矩的EWA”加上一个“梯度二阶矩的EWA”,再配一个偏差修正机制。理解EWA,Adam源码里的大部分逻辑就已经清楚了。7. 手写EWA:Python代码实现与调参实验7.1 基础版EWA实现下面这份代码实现了基础EWA,并对比了不同β的平滑效果。我平时做演示时会生成一个带噪声的合成时间序列,模拟温度漂移。import numpy as np import matplotlib.pyplot as plt def exponential_weighted_average(series, beta=0.9): v = 0.0 result = [] for t, value in enumerate(series, start=1): 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, size=t.shape) data = true_temp + noise plt.figure(figsize=(10, 5)) plt.plot(t, data, alpha=0.3, label="raw data") for beta in [0.5, 0.9, 0.99]: ewa = exponential_weighted_average(data, beta=beta) plt.plot(t, ewa, label=f"beta={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, beta=0.9): v = 0.0 result = [] for t, value in enumerate(series, start=1): 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], dtype=torch.float32) + 0.1 model = nn.Linear(5, 1, bias=False) optimizer = torch.optim.SGD(model.parameters(), lr=0.02, momentum=0.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(f"epoch {epoch}, loss {loss.item():.4f}")这里的momentum=0.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。但t=1时的差异可能达数十倍,不能省。RMSProp里的ε是干什么的?加在除根号上防止除以0,一般取1e-8。太小,早期梯度平方接近0时数值不稳;太大,会把自适应调节能力削弱。能不能用EWA做数据预处理?当然可以。很多时序平滑、滑动统计都可以用EWA替代,只需选好β。8.4 别踩的坑最普遍的坑,是把EWA值直接当原始信号用。EWA的设计目标是体现“当前趋势”,不是每个时刻的精确值。评估模型时,如果需要精确值,该用原始数据就用原始数据,EWA只适合展示趋势,不适合做最后的标准答案。还有人在实现Adam时,对一阶动量做偏差修正、对二阶动量不做修正。这样早期二阶矩严重偏小,有效学习率虚高,实践里经常导致第一步参数猛跳。修正要么都做,要么都别做,偏工减料是最危险的。最后建议你把上面那几段Python代码跑一遍,改成自己的时间序列,比如股票收盘价、传感器读数,直观感受一下β从0.5调到0.999时曲线的形态变化。这里面积累的手感,比记住任何公式都重要。
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,第二天真实温度θ_{t+1}=20,那v_{t+1} = 0.9*100 + 0.1*20 = 92。你会看到,新值20只让平均线挪动了8个点。原因很简单,β=0.9意味着上一轮积累的信息占了90%的权重,单日噪声被大幅压制了。反过来,如果β=0.5,那么v_{t+1} = 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.9,0.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 = 30:v_1 = 0.9*0 + 0.1*30 = 3第一天算出的平均值是3,而真实温度是30,差了快10倍。此时如果直接拿3去当“平均温度”用,误差极大。原因是:初始的v_0=0是人为设定的虚拟值,它把前面所有真实值都“稀释”了。这就像你运动手环第一天戴,系统不知道你过去水平,用0作为起点,算出来的平均值自然被拉低。随着迭代继续,v_t会逐渐逼近真实平均水平,但“起步阶段偏低”这个偏差确实存在。在训练神经网络时,这个偏差会影响早期的梯度更新方向,严重时让前几百步的训练变得不够稳定。4.2 修正公式与实操示例标准做法是在计算v_t后进行修正:v_t_corrected = v_t / (1 - β^t)看上面β=0.9, t=1的情况:v_1_corrected = 3 / (1 - 0.9^1) = 3 / 0.1 = 30正好修正回真实温度30。t越大,β^t越接近0,v_t_corrected就越接近v_t。也就是说,偏差修正只在前期起作用,后期影响可忽略。实际操作中需要注意:修正用t,不是用t+1,很多照抄公式的代码会用错。如果不修正,训练初期动量项会有偏向0的错误信号,影响收敛速度。PyTorch的SGD源码里momentum版本其实没有刻意分出一套完全独立的修正逻辑,但Adam里做了类似处理,可见这个细节在不同优化器里处理方式并不完全一样。5. 从EWA到Momentum:优化器的第一块拼图5.1 动量法到底改了什么标准梯度下降每次更新参数是:w = w - learning_rate * gradient问题在于,梯度本身是有噪声的。一个batch的梯度可能指向A方向,下个batch又偏向B方向,更新路径非常抖,收敛慢。动量法(Momentum)引入了一个“速度项”,把每步梯度也做一次EWA:v_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与Adam:EWA的更高级玩法6.1 RMSProp如何用上EWARMSProp解决的问题是“不同参数维度的尺度差异”。有的权重参数梯度大,有的梯度常年很小,统一用一个学习率就容易顾此失彼。RMSProp的思路是:给每个参数单独维护一个梯度平方的EWA:s_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结合,同时维护两个EWA:m_t:梯度的指数加权平均,对应动量项,默认β1=0.9v_t:梯度平方的指数加权平均,对应自适应学习率,默认β2=0.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) + ε)注意β2=0.999意味着它要累计近千步的梯度平方信息,所以二阶矩的估计非常稳定,但也意味着早期v_t严重偏小(偏差最明显),必须靠偏差修正扳回来。如果你在实现Adam时漏掉这一步,前期参数更新会异常大,模型容易直接飞掉。所以你看,Adam并不是什么神秘算法,它就是一个“梯度一阶矩的EWA”加上一个“梯度二阶矩的EWA”,再配一个偏差修正机制。理解EWA,Adam源码里的大部分逻辑就已经清楚了。7. 手写EWA:Python代码实现与调参实验7.1 基础版EWA实现下面这份代码实现了基础EWA,并对比了不同β的平滑效果。我平时做演示时会生成一个带噪声的合成时间序列,模拟温度漂移。import numpy as np import matplotlib.pyplot as plt def exponential_weighted_average(series, beta=0.9): v = 0.0 result = [] for t, value in enumerate(series, start=1): 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, size=t.shape) data = true_temp + noise plt.figure(figsize=(10, 5)) plt.plot(t, data, alpha=0.3, label="raw data") for beta in [0.5, 0.9, 0.99]: ewa = exponential_weighted_average(data, beta=beta) plt.plot(t, ewa, label=f"beta={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, beta=0.9): v = 0.0 result = [] for t, value in enumerate(series, start=1): 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], dtype=torch.float32) + 0.1 model = nn.Linear(5, 1, bias=False) optimizer = torch.optim.SGD(model.parameters(), lr=0.02, momentum=0.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(f"epoch {epoch}, loss {loss.item():.4f}")这里的momentum=0.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。但t=1时的差异可能达数十倍,不能省。RMSProp里的ε是干什么的?加在除根号上防止除以0,一般取1e-8。太小,早期梯度平方接近0时数值不稳;太大,会把自适应调节能力削弱。能不能用EWA做数据预处理?当然可以。很多时序平滑、滑动统计都可以用EWA替代,只需选好β。8.4 别踩的坑最普遍的坑,是把EWA值直接当原始信号用。EWA的设计目标是体现“当前趋势”,不是每个时刻的精确值。评估模型时,如果需要精确值,该用原始数据就用原始数据,EWA只适合展示趋势,不适合做最后的标准答案。还有人在实现Adam时,对一阶动量做偏差修正、对二阶动量不做修正。这样早期二阶矩严重偏小,有效学习率虚高,实践里经常导致第一步参数猛跳。修正要么都做,要么都别做,偏工减料是最危险的。最后建议你把上面那几段Python代码跑一遍,改成自己的时间序列,比如股票收盘价、传感器读数,直观感受一下β从0.5调到0.999时曲线的形态变化。这里面积累的手感,比记住任何公式都重要。