指数移动平均与一阶低通滤波:同一公式的两种视角 1. 为什么两个领域的人守着同一个公式吵了五十年先讲个我早年间经历过的场景。那时候我在做一套传感器数据采集系统前端硬件工程师负责把压力传感器的模拟信号送进来我负责后面的算法处理。硬件那边为了抑制工频干扰在电路板上加了一组电阻电容构成一个经典的一阶RC低通滤波器。我这边为了让曲线更好看、更稳在软件里跑了一段递归平均的代码。然后问题来了我们俩盯着示波器上几乎一模一样的输出波形却用了完全不同的术语去描述它。硬件工程师管它叫“一阶低通滤波”我管它叫“指数移动平均”。更离谱的是我们俩都为这个“自己领域里再基础不过的公式”争论过——他说他的是模拟电路我说我的是数字信号处理两个东西怎么可能是同一个直到后来我把两个公式摆在一起展开一推导才发现这不仅仅是数学上的“相似”而是严格意义上的同一个递推表达式。这件事给我的冲击很大。其实不止是硬件和算法量化交易里用指数移动平均EMA做趋势跟踪的人和控制系统里用一阶惯性环节做平滑的人也经常陷在各自的术语体系里。角落里的公式明明长得一模一样互相却不知道对方的存在。今天这篇文章我就想把这个“同一枚硬币的两面”彻底掰开揉碎。我会从数学推导、物理含义、参数选择、工程实现到常见坑位把指数移动平均和一阶低通滤波从头到尾讲清楚。适合谁看呢如果你是做嵌入式、写传感器滤波、搞控制系统、做量化策略或者仅仅是被“平滑数据”这件事困住过的开发者这篇文章都能给你一个完整的视角。2. 数学本质同一个递推式的三层展开2.1 从两个公式的对比开始先看两个式子。指数移动平均的离散递推形式最常见的写法是s[t] α · x[t] (1 − α) · s[t−1]其中 s[t] 是 t 时刻的平滑值x[t] 是 t 时刻的原始采样值α 是平滑系数取值在 0 到 1 之间。一阶低通滤波器的数字实现如果从模拟域的双线性变换或者最简单的后向差分法推过来常见的递推式长这样y[n] y[n−1] a · (x[n] − y[n−1])其中 a 是滤波系数一般远小于 1。把第二个式子做个简单的代数变形y[n] a · x[n] (1 − a) · y[n−1]。你发现什么了它和第一个式子完全一致只要把 α 和 a 对应上就行了。这没什么好神秘的这就是同一个线性递推方程。2.2 展开后的“指数记忆”为什么叫这个名字若不信两者等价可以把递推式反复展开看它到底在算什么。从最开始的初始值 s[0] 开始经过 t 步之后s[t] α · x[t] α(1−α) · x[t−1] α(1−α)² · x[t−2] … (1−α)ᵗ · s[0]每一项的权重是 α(1−α)ᵏ。这是一个等比数列。如果 α 0.2那么最新样本的权重是 0.2前一个样本的权重是 0.16再前一个是 0.128依此类推。权重随着时间往前推移按照指数的速度衰减永远到不了零但会趋向于零。这就是“指数移动平均”这个名字的由来——它给所有历史数据都分配了一个指数衰减的权重。也正是这个特性让它在不引入滞后感的同时起到了平滑作用。你一阶低通滤波器的“电容记忆效应”在离散域里被这条递推式精确复刻了逻辑是完全一致的。2.3 频域视角为什么它叫“低通”平滑的另一面是“滤掉高频”。如果你把递推式做Z变换能得到系统的传递函数H(z) α / (1 − (1−α) · z⁻¹)这个系统在 z 1直流也就是频率为 0 的分量处增益是 1相位是 0。随着数字频率升高增益开始下降到了奈奎斯特频率即采样频率的一半附近增益会衰减得非常厉害。所以它把低频分量完整放过去把高频分量压制住这就是低通的含义。不过有一点必须说清楚一阶低通的频率响应并不陡峭从通带到阻带是缓慢过渡的。如果你想要一个“砖墙式”的截止特性对不起一阶做不到。它只适合做温和的平滑不适合做严苛的频带选择。3. 工程实现离散化细节与参数换算3.1 从模拟到数字那个关键的 a 是怎么来的很多做嵌入式或者电路的朋友第一反应是“我硬件上用了一个一阶RC滤波器截止频率是 f_c对应的时间常数 τ RC 1 / (2πf_c)那我在软件里应该怎么写”好问题。这里的关键是离散化的方法。如果直接采用最简单、也是最常见的前向欧拉或后向差分近似再做一个近似处理会得到一个很实用的简化公式a Δt / (RC Δt)其中 Δt 是采样周期即两次采样之间的时间间隔。这个公式不是精确解但它思路清晰、实现简单、参数物理意义明确。如果你的系统对相位特性要求不是极端苛刻这个近似在绝大多数工程场景里都够用。举个例子。假设你用单片机每毫秒采样一次Δt 0.001 秒。你希望抑制 50 Hz 的工频干扰选一个截止频率 f_c 5 Hz 的滤波则 τ 1 / (2π × 5) ≈ 0.0318 秒RC Δt ≈ 0.0328所以 a ≈ 0.001 / 0.0328 ≈ 0.0305。把这个 a 代进递推式一个软低通就写完了。再提一句如果你用的是双线性变换法得到的系数会略有不同因为双线性变换做了频率预畸变能够精确地把模拟截止频率映射到数字频率上。但它的算式会稍微复杂一些。我的建议是做传感器平滑用简化公式就够了做严格滤波器设计再用双线性变换也不迟。3.2 写代码一份可以直接抄走的实现我直接用 Python 写一个最小实现完整可运行。假设你有一组带噪声的传感器数据想拿它做平滑。import numpy as np import matplotlib.pyplot as plt def ema_lowpass(data, alpha): out np.empty_like(data) out[0] data[0] for i in range(1, len(data)): out[i] alpha * data[i] (1 - alpha) * out[i-1] return out # 模拟一组带噪声的信号 np.random.seed(42) fs 1000 # 采样率 1000 Hz t np.arange(0, 1.0, 1/fs) true_signal np.sin(2 * np.pi * 2 * t) # 2 Hz 有效信号 noise 0.3 * np.random.randn(len(t)) raw true_signal noise alpha 0.04 smoothed ema_lowpass(raw, alpha) plt.figure(figsize(10, 4)) plt.plot(t, raw, alpha0.4, labelraw) plt.plot(t, smoothed, linewidth2, labelsmoothed) plt.legend() plt.title(fEMA / 一阶低通滤波, alpha{alpha}) plt.show()这段代码很直白无非是把递推公式翻译成循环。但有几个实现层面的细节我踩过坑之后必须叮嘱一下初始值初始化。我这里直接拿 data[0] 作为初始平滑值。如果数据前几个点有明显异常建议先扔几个点再启动滤波或者把初始值设为前 N 个点的均值不然开头会有一段明显的收敛过程。数值精度。α 很小的时候1−α 很接近 1在单精度浮点里反复乘会有精度损失长时间运行可能出问题。嵌入式环境下尽量用 double 或者用定点 Q 格式小心处理。只用一个状态变量。这个滤波器是“无记忆”的恰恰相反它的记忆就藏在那个单状态量 s[t−1] 里。你不需要存整段历史只需要一个全局变量。这既是它内存友好的原因也是它响应滞后的根源。3.3 alpha 到底应该取多少一组经过实测的经验尺度很多初学者喜欢问“α 设多少合适”。这个问题没有标准答案但可以给一组经验尺度省得你盲猜α 0.5 ~ 1.0响应极快几乎没怎么平滑适合只需要轻微去毛刺的场合α 0.1 ~ 0.3常规平滑适合大多数传感器数据能明显降低抖动滞后感还能接受α 0.01 ~ 0.05强平滑曲线会很干净但滞后明显适合对实时性要求不高的指标监控比如温度、湿度这种慢变量α 0.001 以下极强平滑基本就是取长时均值了只适合非常稳定且无突发变化的场景。有个小的对照公式也可以记住滤波器的截止频率 f_c 与 α 的关系在采样频率 f_s 下大致可以写成α ≈ (2π · f_c / f_s) / (1 2π · f_c / f_s)这个公式是从后向差分近似推导出来的。反过来如果你想按截止频率来选 α可以先算好上面那个比值再代公式。我平时调参的时候比较习惯的做法是先估算时间常数 τ然后用 α 1 − exp(−Δt / τ) 去设置这个取值在离散采样率不高的时候更精确。4. 为什么能滤噪从“权重分配”理解滤波的本质4.1 频域之外的第二条理解路径低通滤波可以理解成“给不同频率不同的放行比例”这是频域路径。但对很大一部分只写业务代码的开发者来说“频率”这个抽象概念并不直观。我想提供第二条理解路径把滤波看作一次加权平均。当你计算 s[t] α · x[t] (1−α) · s[t−1] 的时候你其实是在说这一时刻的平滑值有 20% 来自新样本80% 来自过去的全部记忆以指数衰减的权重分配。如果 α 很小新样本的发言权很小过去长时间积累的信息主导着输出所以曲线表现为“拉不动的厚重感”——噪声被压住了但真实信号的变化也会被延迟感知。这就是为什么滤波和滞后是一对分不开的双胞胎。任何试图用一阶低通去平滑信号的人都必须接受“平滑程度越好滞后越大”这个现实。试图用单个一阶环节既完全滤掉噪声又不损失动态响应是不可能的。这属于物理世界的等价交换不讲价。4.2 跟滑动平均比哪个更好经常会有人把滑动平均SMA拎出来跟 EMA 对比。滑动平均是“过去 N 个点的平均”权重是均匀分配EMA 是“全部历史的指数衰减加权”权重越近越高。两者差异我直接列个表特性滑动平均 SMA指数移动平均 EMA / 一阶低通需要存储的样本数需要维护长度为 N 的窗口数据只需一个状态变量权重分配窗口内均匀窗口外为零全部历史指数衰减对近期变化的敏感度相对适中可调α 大则敏感滞后感固定为 (N−1)/2 个采样周期由 α 决定α 小则滞后大内存占用高窗口越长越大极低频率响应有零点会出现波纹平滑单调衰减无波纹如果你的系统内存极其紧张比如单片机只有几百字节的 RAM那 EMA 显然更划算。如果你的信号中存在规律性噪声并且你精确知道其周期用滑动平均往往能通过窗口大小把对应频率的零点对准噪声频点得到很好的抑制。两种工具没有高下之分关键看你的约束条件。5. 利用等价性跨界调参一套吃遍两个领域的参数视角5.1 从强化学习看到一阶低通有意思的迁移来了。一旦你认识到 EMA 和一阶低通是同一个公式很多别处的经验就能直接搬过来用。在强化学习和深度学习优化器比如 Adam里梯度的一阶矩估计和二阶矩估计用的就是指数移动平均。PyTorch 源码里直接用beta1、beta2控制动量。比如betas(0.9, 0.999)这个 0.9 就是 EMA 的衰减系数。你换一个角度看这实际上是给梯度信号做了一阶低通滤波把高频的、随机涨落的梯度分量滤掉只让稳定的、低频的趋势分量去指导更新方向。把深度学习里的经验搬回传感器领域会是什么效果我做过一个实验用 β 0.99 的 EMA 来处理惯性测量单元的加速度计数据效果和我手工设计一个截止频率约 1.6 Hz 的一阶低通几乎完全重合。为什么因为 β 和 α 之间就有一个精确的换算关系α 1 − β。你不需要重新发明参数你只需要一个视角迁移。5.2 从控制系统角度理解“抗扰性”控制系统里有个概念叫系统带宽。一阶低通的带宽就是 −3 dB 截止频率。低于带宽的干扰信号会被系统跟踪高于带宽的会被抑制。这在传感器反馈回路里的含义很直接如果闭环控制的带宽是 1 Hz而你传感器上叠加了 10 Hz 的机械振动噪声那么只要把滤波截止频率设在 0.5 Hz 左右就能把噪声隔离在控制回路之外。这个“带宽匹配”的思维是很多刚接触滤波的人容易忽略的——只知道滤得越狠越干净却不知道滤波本身也是系统的一部分它会改变系统的相位裕度削弱稳定性。我见过有人在 PID 控制里把滤波截止频率设得很低结果噪声确实没了但系统开始抖动。为什么因为滤波器给反馈信号引入了额外滞后等效于给系统增加了相位延迟把原本充裕的相位裕度吃掉了系统自然变得不稳定。这个坑我用一句话总结滤波别只顾着看平滑效果一定要放在整个控制回路里评估联动影响。6. 实操经验从调参到边界情况把坑都替你踩一遍6.1 失真的源头初始化、突发脉冲与截止频率选择即使公式这么简单实际使用的坑依然不少。我先列常见问题及对策都是我们实际联调中一行行调出来的经验。现象原因处理建议输出曲线开头有一段缓慢爬升初始值设置不当滤波还在收敛中预热期先采 N 个点取均值作为 s[0] 再启动输出尖峰脉冲无法被有效抑制α 太大单点权重过高适当减小 α或者先做一次“离群点剔除”预处理突发掉电或者传感器卡死输出被带偏很久EMA 对异常值的“记忆”很长加异常检测输出与真实信号相比有肉眼可见的延迟α 设置太小滞后严重折中调参或者考虑自适应滤波根据信号变化率动态调整 α数值长时间运行后出现偏差单精度浮点误差累积改用 double或定期用原始均值重置状态6.2 一个自适应变体信号平坦就多滤信号突变就少滤实际项目里固定 α 的 EMA 经常面临的尴尬是静态场景下觉得滤得不够干净动态场景下又觉得滤得太钝。既然 α 可以是一个变数自然可以设计出“看人下菜碟”的自适应策略。一个比较实用的思路是依据当前样本与上一个平滑值的偏差大小动态调节 α。偏差大说明可能处于快速变化期应当让新样本权重变大减小滞后偏差小说明当前比较平稳应当加大滤波强度把残余噪声磨平。def adaptive_ema(data, alpha_base, k): out np.empty_like(data) out[0] data[0] for i in range(1, len(data)): diff abs(data[i] - out[i-1]) # diff 越大alpha 越接近 1 alpha alpha_base (1 - alpha_base) * (diff / (diff k)) out[i] alpha * data[i] (1 - alpha) * out[i-1] return out这里的 k 是一个缩放参数决定了偏差多大时开始让 α 快速提高。这种自适应变体在机器人或者机械臂控制里非常有用机械臂静止的时候期望输出是一根直线启动和刹车的瞬间又希望滤波不要吃掉运动的加速度信息。固定 α 根本做不到两全自适应变体可以直接解决问题。6.3 和卡尔曼滤波摆在一起怎么选遇到有人问“既然你说得这么热闹那卡尔曼滤波是不是更好”我觉得得把这个关系也理清楚。卡尔曼滤波的预测-更新框架在某些条件下会退化成和 EMA 类似的递归结构尤其是当系统模型是匀速、过程噪声和测量噪声都是定值的时候。但卡尔曼滤波的核心优势在于它能融合多传感器、能提供状态协方差估计、能显式建模过程模型。代价是实现更复杂、需要对噪声统计特性有比较靠谱的先验。我的建议很直白如果你只有一个传感器、模型简单、想要快速见效用 EMA/一阶低通就够了没必要杀鸡用牛刀如果你需要融合 GPS、惯性测量单元、轮速计等多路数据同时需要输出位置和速度估计的置信度那值得上卡尔曼如果系统是非线性的、场景又恶劣可能还得考虑扩展卡尔曼或无迹卡尔曼。但所有更复杂的方案底层都会递归地使用类似于一阶低通的平滑思想。把这个基础公式吃透学卡尔曼的时候也会顺利很多。6.4 一个生产环境的小细节批量数据的离线滤波不仅在线滤波可以用这个公式离线数据分析也可以。处理批量历史数据时常见操作是正向跑一遍 EMA再反向跑一遍 EMA然后把两次结果取平均这样可以消除相位滞后。GitHub 上很多数据处理库里的filtfilt函数就是这种思路。两个方向各跑一遍滞后的地方互相抵消得到的就是零相位失真的平滑曲线。它适合离线分析不适合实时系统因为实时系统无法预知未来数据。from scipy.signal import butter, filtfilt def zero_phase_filter(data, cutoff_hz, fs): b, a butter(1, cutoff_hz, btypelow, fsfs) return filtfilt(b, a, data)这种零相位滤波在数据分析和画图时非常好用但它不是在线可用的算法挺多刚接触的人容易混淆“离线”和“在线”的边界。7. 最后分享一个我在项目里常用的调参心法前面讲了很多原理和代码最后聊点实在的调参手感。我每次拿到一条需要做平滑的新数据一般不做复杂分析直接用一条经验路径先跑起来先看信号的采样频率和主要噪声频段定一个大致的时间常数转成 α用 α 跑一遍出来肉眼对比原始曲线和滤波曲线重点观察两个地方噪声还有多大残留、信号的尖峰延迟了多少如果噪声残留明显则减小 α如果尖峰延迟不能接受则增大 α来回调两三次之后基本就能定位在一个“既不嫌脏也不迟钝”的中间区域。最后我一直把一句话记在本子上滤波永远是一个噪声抑制和动态响应之间的妥协不存在免费的午餐。一个参数摆平一切需求的幻觉最好趁早放弃。理解了这个公式的双重身份你就不会再纠结于“这到底是一阶低通还是指数移动平均”——公式本身没有门派所有术语的隔阂都只是观察角度不同罢了。