news 2026/9/8 19:42:51

指数移动平均与一阶低通滤波:数学等价、参数换算与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
指数移动平均与一阶低通滤波:数学等价、参数换算与工程实践

指数移动平均(EMA)和一阶低通滤波,这俩名字听起来一个像统计学概念,一个像信号处理术语,八竿子打不着。但我在实际做数据处理、传感器降噪、控制系统反馈平滑这些活儿的时候,越来越发现一个有意思的规律——一旦把它们的公式拆开、把各自的系数换算清楚,你会发现它们本质上是同一个东西,只是穿了不同的马甲。

今天这篇就专门把这个“马甲”扒干净。我会从数学本质、参数换算、实际编码、工程调参四个维度,把这两个概念彻底打通。不管是做量化交易、写单片机滤波,还是处理传感器数据,这篇都能帮你少走弯路,直接照着用。

1. 内容整体设计与思路拆解:两个名字,同一个递推公式

1.1 各自领域的叫法为什么不一样

指数移动平均的经典公式长这样:EMA_t = α * x_t + (1 - α) * EMA_{t-1}

一阶低通滤波的经典公式长这样:y[n] = a * x[n] + (1 - a) * y[n-1]

如果你把这两个公式并排放在一起看,应该立刻就能发现:它们长得一模一样。α和a扮演的角色完全相同,EMA_t和y[n]都是上一时刻的输出值,x_t和x[n]都是当前时刻的输入值。数学结构上,这俩公式根本没有任何区别。

那为什么在金融领域大家管它叫“指数移动平均”,在信号处理领域大家管它叫“一阶低通滤波”?本质上是两个领域的人用了两套话语体系,描述的是同一个数学对象。金融的人关注的是“哪个时间段的均值”,所以强调“窗口”和“移动”;信号处理的人关注的是“频率成分”,所以强调“低通”和“截止频率”。称呼不同,底层是一回事。

1.2 把离散公式还原成连续系统的思路

在信号处理里,一阶低通滤波是有明确“物理意义”的——它对应一个一阶RC电路,或者一个一阶惯性环节。它的连续传递函数是:H(s) = 1 / (τs + 1),其中τ是时间常数。

这个连续系统经过离散化之后,会得到一个带参数a的差分方程。离散化的方法不同,a和τ的换算关系也不同,最常见的是“一阶后向差分”近似:y[n] = (T/(τ+T)) * x[n] + (τ/(τ+T)) * y[n-1]。

把这个式子跟y[n] = a*x[n] + (1-a)*y[n-1]对照,你就会发现:a = T / (τ + T)。这是一个非常重要的换算关系——你只要知道采样周期T和时间常数τ,就能精确算出滤波系数a;反过来,你只要定了a和采样周期T,就能反推出时间常数τ。

同样地,在EMA体系里,α和时间窗口N也有一个近似关系:α ≈ 2/(N+1)。这个公式在技术分析领域被广为使用,用来把EMA的“半衰期”对应到一个等效的简单移动平均窗口上。但严格讲,这个换算只是近似,更精确的对应关系要通过半衰期来定义。

1.3 为什么这个递推结构在无数场景里反复出现

只要一个系统里有“惯性”——输出不能瞬间跟上输入的变化,就会自然涌现出这种一阶递推结构。物理世界的RC电路、热传导系统、机械阻尼,生物世界里的体温调节、神经元的膜电位变化,经济数据里的消费平滑、库存调整,都有这种惯性。所以指数移动平均和一阶低通滤波其实是同一种“惯性思想”在不同场景里的具体表现。

理解了这层关系,真正的价值在于:你可以把两个领域的成熟经验互相迁移。比如金融里对EMA初始值的处理技巧,可以直接用到传感器滤波的初始化上;信号处理里对截止频率和相位延迟的分析方法,可以直接用来解释移动平均为什么“慢半拍”。跨领域的灵感碰撞,往往是从这种基础概念的“去伪存真”开始的。

2. 核心参数解析与选型逻辑:α、时间常数和截止频率的换算

2.1 α和采样周期T是任何实现都无法绕开的两个关键值

不管叫EMA还是一阶低通,你都要面对同一个问题:系数取多少?这个系数不是拍脑袋定的,它背后连着两个物理量——采样周期T和时间常数τ。

举个例子。假设你在做一个体温检测手环,体温信号本身的生理变化很慢,但传感器会有高频噪声,采样周期T=0.1秒。你希望滤波后的信号能跟上体温的真实变化,同时把高频毛刺滤掉。体温变化一个周期大概要几十秒甚至几分钟,所以时间常数τ取5秒比较合适。那么系数:a = T/(τ+T) = 0.1/(5+0.1) ≈ 0.0196。

也就是说,每次新数据对输出的修正权重只有约2%,剩下98%都来自历史积累。这是“平滑”的代价——输出变化会很慢。如果你嫌慢,把τ降到1秒,a≈0.0909,修正力度立刻大了4.6倍。

在EMA场景,如果你在用日线数据做均线策略,希望等效窗口大约20天,用α=2/(20+1)≈0.0952。这个系数每天给新数据的权重不到10%,所以均线会很平滑,但也会明显滞后于价格拐点。

2.2 截止频率:信号处理领域如何反推滤波行为

在信号处理语境下,大家更关心“这个滤波器截止频率是多少”。一阶低通滤波器的截止频率定义为增益下降到-3dB(约0.707倍)时的频率。对于连续系统,ω_c = 1/τ,所以f_c = 1/(2πτ)。

结合离散化的系数a = T/(τ+T),可以推导出截止频率和系数a的关系:

f_c ≈ a / (2πT),当a远小于1时成立。

验证一下:T=0.1秒,τ=1秒,a≈0.0909,那么f_c≈0.0909/(2π×0.1)≈0.145Hz。这个频率意味着,高于0.145Hz的信号会被明显衰减,低于这个频率的信号基本保留。一个体温信号如果变化周期是10秒(0.1Hz),就在截止频率附近,会有一部分衰减;如果变化周期是100秒(0.01Hz),就能完好通过。

这就是“低通”的含义——低频信号畅通无阻,高频信号被压下去。而在EMA语境里,很少有人提截止频率,但如果你知道这个换算关系,就可以把一个20日均线的EMA放到频域里去理解:它等价于一个截止频率约为0.0075Hz(以日线为采样周期)的低通滤波器。

2.3 从高频噪声抑制到参考信号跟踪:两类典型需求怎么选参数

实际工程里,选参数通常分两个方向。

第一种需求是“抑制噪声”。这时候你关心的是信噪比,噪声频率和信号频率差得越远越好办。如果噪声主要是50Hz工频干扰,而你的信号只有0.1Hz,那截止频率设在0.5Hz就非常稳妥。此时a = 2πf_cT = 2π×0.5×0.01 ≈ 0.0314(T=10ms采样),信号会被平滑得很厉害,但没关系,因为你要的就是平滑。

第二种需求是“跟踪参考信号”。比如无人机飞控里要用滤波后的传感器数据做姿态反馈,滤波太强会导致控制滞后,飞机容易震荡。这时候截止频率就不能太低,一般要设到控制系统带宽的5到10倍。假设控制频率50Hz,控制带宽5Hz,那滤波截止频率可以设在25Hz以上,a = 2π×25×0.001≈0.157(T=1ms采样),这样既能滤掉一部分高频毛刺,又不至于让反馈信号滞后太多。

这两种需求是此消彼长的关系:滤波越强,平滑度越高,延迟越大;滤波越弱,响应越快,噪声残留越多。没有万能参数,只有针对场景的权衡。

3. 实操过程与核心环节实现:从Python原型到嵌入式定点数

3.1 Python快速验证:三种等价写法对比

先用Python把EMA/一阶低通滤波跑通,方便观察参数效果。这里给出三种写法,代码上完全等价,但适用场景不同。

import numpy as np import matplotlib.pyplot as plt def ema_loop(x, alpha): """写法一:标准递推循环,最直观""" y = np.zeros_like(x) y[0] = x[0] # 常见初始化:第一个值等于第一个输入 for i in range(1, len(x)): y[i] = alpha * x[i] + (1 - alpha) * y[i-1] return y def ema_pandas(x, alpha): """写法二:pandas的ewm,一行搞定,适合DataFrame场景""" import pandas as pd s = pd.Series(x) return s.ewm(alpha=alpha, adjust=False).mean().values def ema_lfilter(x, alpha): """写法三:scipy.signal.lfilter,信号处理标准方式""" from scipy import signal b = [alpha] # 分子系数,对应a*x[n] a = [1, -(1-alpha)] # 分母系数,对应y[n] - (1-alpha)*y[n-1] return signal.lfilter(b, a, x)

三种写法输出结果完全一致。第一种适合理解原理,第二种适合做数据分析和回测,第三种适合跟其他滤波器统一处理。实际项目中我大多数时候用scipy的lfilter,因为一旦要切换成二阶滤波、巴特沃斯滤波,代码结构不用改,只换系数就行。

验证一下等价性:

x = np.random.randn(1000) # 模拟白噪声 alpha = 0.1 y1 = ema_loop(x, alpha) y2 = ema_pandas(x, alpha) y3 = ema_lfilter(x, alpha) print(np.max(np.abs(y1 - y2))) # 一般输出1e-15级别 print(np.max(np.abs(y1 - y3))) # 一般输出1e-15级别

计算机浮点误差级别,可以认为完全一致。

3.2 嵌入式C实现:浮点版和定点版

到了单片机或者嵌入式平台,问题就来了:有些低端MCU没有FPU,浮点运算全靠软件模拟,慢得离谱。这时候有两种选择:用浮点但是降低采样率,或者用定点数优化。

先看浮点版本,这个在STM32F4以上(带FPU)很常见:

typedef struct { float alpha; float y_prev; uint8_t is_initialized; } lowpass_t; float lowpass_process(lowpass_t *flt, float x) { if (!flt->is_initialized) { flt->y_prev = x; flt->is_initialized = 1; return x; } flt->y_prev = flt->alpha * x + (1.0f - flt->alpha) * flt->y_prev; return flt->y_prev; }

这段代码逻辑简单,但有一个问题:每次浮点运算都有舍入误差。如果α很小(比如0.001),那么(1-α)=0.999,输出值长期接近满量程,浮点精度会把微小的修正量淹没。在控制系统里,这可能表现为“输出回不到零点”或“稳态误差”。解决办法是把变量归一化到0到1之间,别让数值长期贴近量程边界。

再看定点版本,适合没有FPU的Cortex-M0小芯片。假设ADC是12位,信号范围0到4095,你希望用一个uint32_t来存滤波状态。这里的关键是把浮点系数a转成Q格式定点数:

typedef struct { uint32_t alpha_q; // Q16格式,alpha * 65536 uint32_t y_prev_q; // Q16格式的滤波状态 } lowpass_q16_t; #define Q16_SCALE 65536 uint32_t lowpass_q16_process(lowpass_q16_t *flt, uint32_t x_q) { // y = alpha * x + (1-alpha) * y_prev // 全部在Q16格式下运算,最后右移16位 uint32_t term1 = flt->alpha_q * x_q; uint32_t term2 = (Q16_SCALE - flt->alpha_q) * flt->y_prev_q; flt->y_prev_q = (term1 + term2) >> 16; return flt->y_prev_q; }

注意这里有个大坑:term1和term2都可能达到40亿级别(Q16格式下alpha_q约65536,x_q约4096,乘积约2.68亿,看起来还好;但term2的(1-alpha)约65000,y_prev_q可能约65536×4096≈2.68亿,乘积约1.74e13,远超uint32_t的范围),直接溢出。所以实际工程里要用uint64_t做中间变量,或者把Q格式降低,比如Q12、Q8。

正确做法是这样:

uint32_t lowpass_q16_process_fix(lowpass_q16_t *flt, uint32_t x_q) { uint64_t term1 = (uint64_t)flt->alpha_q * x_q; uint64_t term2 = (uint64_t)(Q16_SCALE - flt->alpha_q) * flt->y_prev_q; flt->y_prev_q = (uint32_t)((term1 + term2) >> 16); return flt->y_prev_q; }

uint64_t中间变量在Cortex-M0上虽然不是一条指令完成,但比软件浮点快太多,实际测试大概快10到20倍。速度完全够用。

3.3 初始值策略:第一个输出到底怎么定

很多人写代码忽略初始值,结果滤波输出在启动阶段有一段明显“爬坡”。原因很简单:如果y[0]初始化为0,但x[0]是100,那么输出会从0慢慢向100靠近,这个过渡过程可能持续好几个τ。

我试过几种初始值策略:

  • y[0] = 0:简单但启动延迟大,不适合对实时性有要求的场景。
  • y[0] = x[0]:最常见,启动曲线平滑,系统出厂即“在线”。
  • 预热期输出原始值:先跑N个周期,期间不做滤波,等状态稳定后再切到滤波输出。适合对启动阶段数据要求极高的场景(比如医疗设备)。

实际项目里我大部分时候用y[0] = x[0],因为它实现最简单,且绝大多数应用对启动阶段不敏感。但在飞控、电机控制这类需要“零冲击启动”的地方,我更喜欢“预热期输出原始值”的策略——上电后先跑几百个采样周期,期间把滤波器状态持续更新,但不把滤波结果输出到控制链路,等稳定后再切换。这样可以彻底避开启动爬坡。

4. 常见问题与排查技巧实录:调参、延迟和数值稳定性

4.1 滤波后信号延迟太大,怎么量化?

这是高频问题:明明用了滤波,数据干净了,但系统“反应变慢”了。这个“慢”不是感觉,而是可以量化的一阶滞后。

对于一阶低通滤波,输出相对输入的相位延迟可以计算。在截止频率f_c处,相位滞后约45°;在远低于截止频率的频段,相位滞后近似为:Δφ ≈ arctan(ω/ω_c) ≈ ω/ω_c(弧度)。

换算成时间延迟:

delay ≈ τ = RC时间常数(秒)

所以在设计时可以用一个经验法则:一阶低通滤波的“信号延迟”大致等于时间常数τ。假如你的系统要求总延迟不超过50ms,那么τ就不能超过50ms,对应a ≥ T/(T+0.05)。

实际上,如果要做更严格的对比,可以给输入一个阶跃信号,测量滤波输出从10%上升到90%的时间,这个时间约等于2.2τ。比如τ=1秒时,输出从阶跃的10%到90%大约需要2.2秒——这个数值会让你对滤波器的“迟钝”建立非常直观的印象。

4.2 噪声滤不干净,是系数问题还是数据问题?

滤波后波形还毛毛躁躁,很多人第一反应是“把系数再调小一点”。但如果α已经调到0.01以下还不行,就要检查噪声的来源是不是超出了简单一阶滤波器的能力范围。

一阶滤波器的衰减斜率固定是-20dB/十倍频。什么意思?频率每升高10倍,衰减增加20dB,也就是幅度衰减到原来的1/10。如果噪声和信号的频率离得很近,或者噪声频带很宽(白噪声、脉冲噪声),一阶滤波器无法有效区分,这时候有两个方向:

方向一:升级滤波器阶数,用二阶巴特沃斯或更高阶的滤波器,衰减斜率更陡。代价是延迟更大,代码复杂度更高。

方向二:先做异常值剔除,再做平滑滤波。比如中值滤波剔除脉冲噪声,再接一阶低通平滑剩余的高斯噪声。这个组合在传感器数据处理里特别管用,计算量也很低,在单片机上一样跑得动。

4.3 α取值很小的时候,输出跟不上真实变化

这个问题的本质是“等效带宽太窄”。用前面的公式:f_c = α / (2πT)。当你把α定在0.001,T=1ms时,截止频率只有约0.16Hz。这意味着变化快于0.16Hz的信号都会被明显衰减。如果你的真实信号变化频率是1Hz,那它也会被压在0.2倍左右,输出自然跟不上。

解决方法不外乎两个:把α调大,或者降低采样周期T(让系统能感知到更快的变化)。如果两者都不能动,就要考虑用互补滤波或者卡尔曼滤波——它们能在保留响应速度的同时兼顾噪声抑制,本质上是多了一个“模型预测”自由度。

4.4 常见问题速查表

现象可能原因解决方案
启动阶段输出爬坡初始值y[0]=0改用y[0]=x[0]或预热期策略
输出滞后明显τ过大,截止频率过低增大α或减小τ;检查f_c是否满足系统带宽要求
噪声滤不净一阶衰减斜率不足,噪声带宽太宽先用中值滤波去脉冲,再加二阶滤波器
α很小但输出漂移浮点舍入误差积累改用定点数Q格式,或归一化变量到0~1范围
定点实现结果乱跳中间变量溢出中间运算用uint64_t或降低Q格式位数
滤波后波形有台阶采样不均匀,时间间隔忽大忽小固定采用周期,或用时间戳修正系数
输出绝对值系统性偏低信号本身在截止频率附近,幅度被衰减检查f_c是否高于信号主频,必要时提高截止频率
突然出现尖峰输入信号本身有脉冲干扰先加中值滤波或防脉冲均值滤波,再进低通

4.5 一个调参的实操案例

我在做一款电池管理系统(BMS)的电流采样时,遇到一个典型的折衷问题。电流信号真实变化范围从0到200A,变化率最快到50A/s,同时传感器叠加了高频噪声。系统要求:电流显示值尽可能平滑,纹波控制在2A以内;同时过流保护响应时间要快,从过流发生到保护触发不超过30ms。

如果只用一阶低通滤波,τ=30ms时,截止频率f_c=1/(2π×0.03)≈5.3Hz,噪声衰减不够;τ=100ms时,噪声够了,但保护延迟太大。最后我采用两级策略:第一级用α=0.05的强滤波,把噪声压下去,用于显示和正常电流监控;第二路不滤波或弱滤波,用于过流保护判定。这样显示很平滑,保护也足够快——其实很多实战系统都是这么做的,一路快通道保安全,一路慢通道看趋势。

这个思路也可以沿用到其他场景:控制用一路滤波,监测用另一路滤波,参数各自独立整定。没有冲突,反而更灵活。

5. 我的一点使用心得:把这两个概念当“惯性”来理解,比记公式更长久

做得越多越觉得,指数移动平均和一阶低通滤波之所以在这么多领域反复出现,就是因为它实现了一个最简单也最重要的物理直觉——任何真实系统都有惯性,输出不能瞬间跃迁到新的输入值。

只要你想明白这个“惯性”从哪来、需要多大、能不能接受它的代价,就不需要死记公式。公式只是惯性的量化表达。

再分享一个我常用的快速测试习惯:拿到一组数据,先不用管滤波系数,直接画出一阶低通在不同τ下的输出曲线,叠加在原始数据上看。这样比看公式直观得多,通常一两轮就能锁定合适的参数范围。另外,千万要在真实数据上测试,别只跑理想正弦波——真实数据里的毛刺、突变、缺失值才是最容易暴露问题的。

指数移动平均和一阶低通滤波的等价关系,不算什么惊天动地的发现,但把两个领域的经验互相迁移,真的能省掉很多重复踩坑的时间。下次你看到一个金融K线图上的EMA,或者一段传感器波形里的平滑曲线,记得它们背后其实是同一个老朋友。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 19:41:49

上下文学习如何重塑机器人示教:从轨迹回放到语义泛化

最近在做人形机器人的任务泛化实验,有个现象让我特别有感触:以前教机器人抓一个透明杯子,得在仿真环境里调半天位姿容差;现在用ICL(In-Context Learning,上下文学习)的思路,把三段人…

作者头像 李华
网站建设 2026/9/8 19:40:33

AI葡萄智能移栽机器人 QT国产信创完整工程

# AI葡萄智能移栽机器人 QT国产信创完整工程 适配**统信UOS、银河麒麟**国产操作系统,Qt5.12/5.15开发,严格匹配葡萄嫁接苗/自根苗大田、温室标准化移栽农艺;双目视觉+深度相机三维重建,AI自动分级筛选一级合格葡萄苗、剔除弱苗/病苗/伤根苗;六轴柔性夹爪无损取苗,集成**…

作者头像 李华
网站建设 2026/9/8 19:40:17

从Copilot到自主编程Agent:AI辅助开发的范式跃迁与实战指南

GitHub Copilot 刚发布技术预览那会儿,我第一时间就申请了内测资格。说实话,第一次看到编辑器里凭空补出一整段函数的时候,我整个人的状态是既兴奋又警惕。几年过去,AI 编程助手这个赛道已经卷出了新物种:从只会补全代…

作者头像 李华
网站建设 2026/9/8 19:40:05

Windows下OpenCV 4.10+CUDA 12.5+cuDNN 9.2+MSVC 2022编译实战指南

简介:这是OpenCV 4.10.0结合CUDA 12.5.0与cuDNN 9.2.0,使用MSVC 2022编译的Windows 64位预编译包,面向需要在Windows上进行GPU加速图像处理、深度学习和计算机视觉开发的工程师,可省去自行编译OpenCV及CUDA模块的繁琐过程。编译时…

作者头像 李华
网站建设 2026/9/8 19:38:55

深入解析移动机器人差分底盘运动学:原理、应用与深度优化

文章导读:差分驱动底盘(差速底盘)是移动机器人最通用的底层方案,凭借结构简单、成本低廉、控制高效、运动灵活的优势,广泛应用于工业AGV、家用扫地机器人、巡检小车、科研教学机器人等设备。不同于阿克曼底盘的物理转向结构,差分底盘依靠左右轮速度差实现全部运动姿态,其…

作者头像 李华