ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

指数移动平均与一阶低通滤波:公式推导、系数换算与工程实践

指数移动平均与一阶低通滤波:公式推导、系数换算与工程实践 1. 从一份数据平滑需求说起前段时间处理一批振动传感器数据时现场采集的信号毛刺特别多高频噪声几乎把有效波形淹没了。第一反应是上经典的低通滤波但手头没有现成的信号处理库折腾环境又要浪费时间。旁边同事随口说了句直接用指数移动平均不就行了几行代码就平滑了。这句话让我愣了一下。指数移动平均EMA平时在量化分析、指标计算里用得非常多但要说用它来做信号去噪很多人心里会打个问号这玩意儿真能当滤波器用频响特性靠谱吗和一阶低通滤波到底什么关系查了一晚上资料、又在实际数据上跑了几个对比实验之后结论非常明确指数移动平均和一阶低通滤波在数学上本质是同一个东西只是分别来自统计学和信号处理两个不同的方言区。这篇文章就把这个关系彻底讲透顺带给出实际工程里怎么选系数、怎么写代码、怎么避开那些常踩的坑。2. 两个公式同一个递推结构2.1 统计学的视角指数移动平均指数移动平均的递推公式长这样EMA_t α · x_t (1 - α) · EMA_{t-1}其中x_t是当前时刻的观测值EMA_{t-1}是上一时刻的平滑值α是平滑系数取值在0到1之间。α越大当前观测值占的权重越高平滑效果越弱α越小历史信息占的权重越大曲线越平缓。把这个式子展开写就更好理解了EMA_t α·x_t α(1-α)·x_{t-1} α(1-α)²·x_{t-2} ...也就是说EMA本质上是对所有历史数据做加权平均权重按指数衰减。越久远的数据贡献越小但永远不会完全消失——这正是指数移动四个字的含义。2.2 信号处理的视角一阶低通滤波再看一阶低通滤波RC低通滤波器的差分方程。模拟域的RC低通滤波器的传递函数是H(s) 1 / (1 s·τ)其中τ RC是时间常数。用双线性变换或一阶向后差分法离散化之后可以得到如下形式的差分方程y[n] a · x[n] (1 - a) · y[n-1]看到没有结构完全一样。a是滤波系数y[n]是当前输出y[n-1]是上一次输出。把公式里的字母换一下y换成EMAa换成α这就是一模一样的递推式。2.3 为什么说它们是同一个东西这里多说一句。很多文章会讲EMA约等于一阶低通滤波这个约等于其实有点误导。从离散域的角度看它们就是同一个递推算法的两个名字没有任何近似。差别只在于参数的含义不同EMA用α描述平滑程度低通滤波用截止频率fc描述频率阈值推导的起点不同EMA从加权平均的统计思想出发低通滤波从模拟电路和传递函数出发使用场景不同技术分析里习惯用EMA做趋势判断信号处理里习惯用低通滤波做噪声抑制这种一个算法、两套语言的现象在工程里很常见。搞懂它们的关系最大的好处是你可以在两套知识体系里自由切换需要跟量化背景的人沟通就讲EMA需要跟嵌入式工程师沟通就讲低通滤波本质上讨论的是同一个东西沟通成本会大大降低。3. 核心关键如何把平滑系数换算成截止频率3.1 系数换算公式的推导逻辑既然EMA和低通滤波是同一个算法那α和截止频率fc之间必然存在确定的换算关系。一阶低通滤波的截止频率定义是信号功率衰减到原来的一半约-3dB时的频率此时输出幅值约为输入的0.707倍。对离散系统来说我们需要把频响函数算出来找到幅值等于0.707的位置。一阶低通滤波差分方程y[n] a·x[n] (1-a)·y[n-1]的传递函数z变换后为H(z) a / (1 - (1-a)·z⁻¹)将z e^(jω)代入ω 2πf/fs得到系统的频率响应。这里直接给出推导后的幅值公式|H(f)|² α² / (1 α² - 2(1-α)·cos(2πf/fs))令|H(fc)|² 1/2可以解出α cos(2πfc/fs) - 1 √(cos²(2πfc/fs) - 4cos(2πfc/fs) 3)这个精确公式看起来挺复杂但在实际工程中绝大多数场景满足截止频率远低于采样频率fc fs此时cos(2πfc/fs) ≈ 1 - (2πfc/fs)²/2代入化简可以得到一个非常好用的近似公式α ≈ 2π · (fc / fs) 2π · fc · Δt等等这里有个细节值得注意。不同教材对截止频率的定义有差异导致系数公式会有不同的版本某些场景下会看到α 1 - exp(-2π·fc/fs) // 基于阶跃响应时间常数的定义这个定义源自连续时间RC滤波器的时间常数τ RC离散化时要求α 1 - e^(-Δt/τ)。当fc很小、Δt/τ很小时用泰勒展开e^(-x) ≈ 1-x这个式子同样退化为α ≈ 2π·fc/fs。所以两种定义在低频段基本一致在高频段会有偏差。实际工程里按哪个算都行关键是跟你的频响需求对齐。3.2 反向换算已知α求截止频率如果手里有一个α想知道对应的截止频率是多少可以使用逆运算。在近似条件下直接把上面的公式反过来fc ≈ α · fs / (2π)举个例子。采样率fs1000Hz取α0.1那对应的截止频率大约是fc ≈ 0.1 × 1000 / 6.283 ≈ 15.9Hz这意味着频率高于15.9Hz的信号分量会被明显压制低于15.9Hz的分量基本保留。这个先选fc再定α或先定α再看fc的双向换算在实际调试中非常实用。3.3 一个实际换算案例假设我需要从采样率200Hz的IMU加速度数据中滤掉50Hz的电源噪声同时保留5Hz以内的运动信号。第一步确定截止频率。电源噪声在50Hz运动信号在5Hz以内中间隔了约一个十倍频程截止频率选15Hz左右是合理的折中。选太低会衰减运动信号选太高压制不住50Hz噪声。第二步换算系数。α 2π × 15 / 200 ≈ 0.471第三步验证。用α0.47代入频响公式估算在5Hz处幅值衰减很小在50Hz处已有明显衰减。实际测试效果确实不错。提示遇到选多少截止频率合适这类问题可以先算半功率点再根据信号频谱和噪声频谱的距离做调整。一般来说截止频率放在目标信号最高频率的2到3倍处噪声抑制效果和信号保真度能取得较好平衡。4. 代码落地从单行递推到工程可用4.1 标准的采样级递推实现最直接的实现就是按递推公式来。以Python为例class EMALowPass: def __init__(self, alpha, init_value0.0): self.alpha alpha self.y init_value def filter(self, x): self.y self.alpha * x (1 - self.alpha) * self.y return self.yC语言版本适合嵌入式环境typedef struct { float alpha; float y; } one_pole_lowpass_t; float one_pole_lowpass_process(one_pole_lowpass_t* lp, float x) { lp-y lp-alpha * x (1.0f - lp-alpha) * lp-y; return lp-y; }这套代码逻辑极简核心就三条保存上一次的输出、按公式加权求和、更新状态。但实际使用中有一个很容易被忽略的细节初始值init_value的选取。如果对实时性要求高、数据从0开始init_value0没问题但前几十个点会有一个从0爬升到真实电平的过渡过程如果处理的是离线数据最好用第一个采样点做init_value或者用整段数据的均值初始化可以避免启动瞬态如果数据本身有直流偏置且已知直接把偏置值填进去收敛会快很多4.2 工程中更稳健的写法避免浮点累积误差在嵌入式或长期运行的采集系统中还有一个看似不起眼但非常致命的问题浮点累积误差。y[n] (1-α)·y[n-1] α·x[n]当α非常小比如0.001时(1-α) 0.999。在32位浮点下0.999这个系数和y[n-1]相乘的结果被存储时尾数精度会造成微小的截断。单次截断误差在10⁻⁷量级看起来无所谓但如果系统以1000Hz运行一整天累积误差就可能淹没信号幅值。我自己的经验是常规场景用float问题不大最多在1e-5量级但如果需要长时间连续滤波、对精度要求极高建议typedef struct { double alpha; double y; } lowpass_double_t; lowpass_double_t lp {0.01, 0.0}; // 或者使用增量式写法让误差只和x相关避免历史状态反复运算 float y y_prev alpha * (x - y_prev);这个增量式写法y α·(x - y)有个额外的好处直观上它就在做向目标靠近一步调起参数来心智负担更小。但从数学上看如果α和浮点舍入配合不好这种写法会在接近稳态时出现小幅抖动这时反而应该用标准的加权公式。两者各有取舍大家可以按场景选。4.3 批量数据的分组采样优化再分享一个性能优化小技巧。如果数据量很大而滤波系数不变标准递推每来一个点做一次乘法加法开销其实不高。真正耗时的是内存访问和函数调用开销。用Python处理百万级数组时尽量用NumPy的卷积等效实现import numpy as np def ema_filter_batch(data, alpha): out np.empty_like(data) y data[0] for i, x in enumerate(data): y alpha * x (1 - alpha) * y out[i] y return out但这只是主循环Python化依然快不到哪去。更快的思路是用scipy.signal.lfilter配合分子分母系数from scipy.signal import lfilter def lowpass_batch(data, alpha): b [alpha] # 分子系数 a [1, -(1 - alpha)] # 分母系数 return lfilter(b, a, data)这个方法底层是编译好的C代码处理百万级数据的速度比纯Python循环快几十倍而且结果完全等价。对于C/C开发者也可以把采样数据切块放进SIMD优化过的滤波函数里批量处理逻辑是一样的。4.4 实际工程中的完整初始化与在线更新流程我在实际的数据采集系统中通常这样组织滤波模块class SensorFilterChain: def __init__(self, cutoff_hz, sample_rate_hz): self.fs sample_rate_hz self.fc cutoff_hz self.alpha 2 * np.pi * self.fc / self.fs self.y None def process_stream(self, x_new): if self.y is None: # 首次到来直接初始化避免启动瞬态 self.y x_new return self.y self.y self.alpha * x_new (1 - self.alpha) * self.y return self.y def reset(self, init_valueNone): self.y init_value这个结构把采样率截止频率系数换算封装在一起调用方根本不需要关心α怎么算直接传我要15Hz截止频率就行代码可维护性高很多。4.5 向量化、定点化和嵌入式适配如果要在MCU等嵌入式环境里跑往往用不了浮点单元这时就要做定点化。把α用Q格式表示例如用Q15格式15位小数位#define ALPHA_Q15 1287 // 对应浮点alpha 1287 / 32768 ≈ 0.0393 int16_t lowpass_q15(int16_t x, int16_t* y_state) { int32_t temp (int32_t)ALPHA_Q15 * x (32768 - ALPHA_Q15) * (*y_state); *y_state (int16_t)(temp 15); return *y_state; }注意点中间结果必须用32位或更高位宽保存防止乘法溢出右移15位相当于除以32768和浮点版本的系数缩放对应起来如果α很小定点化后精度损失会比浮点更明显建议α 1/128时考虑用Q24甚至Q31格式。4.6 多通道数据如何共享滤波器状态很多传感器系统不止一路信号。拿六轴IMU来说加速度三路加角速度三路如果每一路都new一个滤波器对象代码冗余还容易漏初始化。可以封装一个按通道索引存状态的版本class MultiChannelLowPass: def __init__(self, cutoff_hz, sample_rate_hz, num_channels): self.alpha ... self.states [None] * num_channels def process(self, ch, x): if self.states[ch] is None: self.states[ch] x else: self.states[ch] self.alpha * x (1 - self.alpha) * self.states[ch] return self.states[ch]这样调用时只需要传入通道号和当前采样值状态管理完全内部化避免遗漏。5. 实测效果从频域和时域两个维度看它到底能做什么5.1 频域实测幅频特性曲线用Python对α0.1、fs1000Hz的情况做一次频率扫描可以得到幅频响应频率(Hz) 理论衰减(dB) 1 -0.09 5 -0.54 10 -1.24 15.9 -3.01 30 -6.94 50 -11.58 100 -17.57这里有几个细节值得注意截止频率(15.9Hz处)刚好是-3dB点和理论预期完全对得上对50Hz噪声的衰减约为11.6dB幅值降低到约26%对100Hz噪声衰减约17.6dB幅值降低到约13%低频段衰减很小5Hz信号只损失0.54dB约6%幅值基本可以忽略在大多数只需要粗略去噪的场景里这个衰减量已经非常够用了。但如果希望50Hz以上的压制更狠一些可以把截止频率调低但代价是5Hz的信号也会被削掉更多。5.2 频谱混叠问题采样率与截止频率的匹配还有一个常常被忽视的约束采样率必须远大于截止频率否则会出严重失真。假设信号里有100Hz的有效频率成分采样率只有200Hz此时用截止频率20Hz的滤波并不安全。因为采样定理要求采样率至少是最高信号频率的2倍100Hz信号在200Hz采样率下已经接近Nyquist频率滤波器的频响曲线会产生混叠截止频率附近的过渡带会变得不可预测。经验公式是采样率至少要达到截止频率的10倍以上最好20倍以上这样离散化误差才足够小α ≈ 2πfc/fs这种近似公式也才足够准确。5.3 时域实测对阶跃信号和脉冲噪声的响应从时域看一阶低通滤波的阶跃响应是一条指数上升曲线时间常数τ对应的时间长度约为τ ≈ 1 / (2π·fc)用刚才的例子fc15.9Hzτ ≈ 10ms。这意味着给一个阶跃输入输出大约需要3τ30ms才能上升到终值的95%大约5τ50ms才基本稳定。这个反应延迟在实际控制、实时监测场景中非常关键。比如做心电信号监测时如果滤波截止频率设得太低一个突发的早搏波形会被抹平导致漏检。做电机电流监测时滤波反应太慢会掩盖过流事件。所以选择截止频率时除了看噪声频谱还要评估滤波器引入的群延迟和阶跃响应时间是否在系统可接受范围内。脉冲噪声比如尖刺干扰经过一阶低通滤波之后会被展宽成一个持续约τ的驼峰幅值被压到原来的1/2到1/4。如果尖峰幅值特别大比如10倍于正常信号滤波后残余幅值在τ时间内依然可能超过正常信号范围。这时就要考虑级联多个一阶滤波或者改用中值滤波专门对付脉冲噪声。一阶低通对付随机噪声没问题但对付脉冲毛刺不算最优解。5.4 和后向均值滤波、滑动平均滤波的对比写代码时很多人第一反应是滑动平均滑动窗口均值两者对比一下优缺点一清二楚对比项指数移动平均/一阶低通滑动平均内存占用只需要存一个状态变量需要存整个窗口数据实时性每个点都能立刻输出窗口不满时会延迟或需补零截止频率可调性α连续可调且与fc有明确换算关系窗口长度N只能取整数频响旁瓣单调下降无旁瓣有旁瓣某些高频分量会泄漏回来对阶跃的响应指数逼近响应快但永远有稳态误差的小尾巴线性爬升响应稍慢对周期噪声的抑制高频衰减斜率约-20dB/十倍频程存在周期性凹陷能完全抑制特定频率这些差异在实际选型时挺关键。如果内存紧张、只需要一路信号EMA几乎是零成本如果需要精确压制某个特定频率比如50Hz工频滑动平均的频响凹坑反而更高效如果要处理包含强冲击成分的信号两者都不是首选先做中值滤波再按需平滑更合理。6. 常见误区和那些容易被忽略的坑6.1 α选择的两级误区第一个误区把α直接当平滑程度理解觉得越大越平滑、越小越灵敏。这么说方向没错但缺少量化概念会导致调参全靠猜。真正靠谱的路径是需求梳理 → 确定fc → 换算α → 频域验证 → 实测定标先问两个问题信号里的有效频率最高到多少噪声主要落在什么频段把这两个数定下来fc和α不是一个随机实验问题而是一个计算问题。第二个误区一阶低通滤波是万能平滑器任何信号都直接套α0.1。实际上如果信号本身是缓变的趋势量比如电池SOC一阶低通没问题如果信号里有需要保留的边沿信息比如方波、触发脉冲低通滤波会把边沿钝化严重的会改变事件时序判断。这种情况要改用更高级的滤波策略比如自适应滤波或卡尔曼滤波。6.2 初值选择与启动瞬态的真实影响我见过不止一个同事调试时发现滤波输出前几百个点明显偏小百思不得其解最后才发现是初值设成了0。对于均值为10V的信号从0爬到10V的过程可能长达数百毫秒直接影响开机阶段的判断逻辑。正确的做法是按4.4小节那样第一次采样到来时直接用采样值作为初始状态。这样启动瞬态几乎为零输出从第一拍开始就贴近真实值。6.3 系数边界α1和α过小当α1滤波器退化为无滤波直接输出输入值这在某些逻辑里是合理的选择比如暂态时希望无延迟。但当α稍小于1比如0.999千万不要觉得这肯定很平滑——它的截止频率极低输出会像一个几乎不动的慢吞吞曲线对任何快变化都几乎无响应。反过来α过小低于0.001还会带来数值问题如果使用32位浮点(1-α)可能被舍入到0.999000012之后滤波性能与理论值出现肉眼可见的偏差。此时应当改用double或者改用增量式写法。6.4 相位滞后很多人会忽略的实际感受前面提到阶跃响应有延迟在频域里对应的是相位滞后。一阶低通在截止频率点处的相位滞后约为45°在高频处接近90°。如果对信号的绝对相位有要求比如多个传感器信号做时间同步、或做闭环控制这个滞后可能会引发严重问题。降相位滞后的思路通常有两个一是把截止频率调高减少滞后但噪声变多二是在离线处理时对数据先正向后反向各滤一遍得到的净相位变化为零——这就是scipy的filtfilt做的事情。如果必须在线处理可以考虑级联两个一阶滤波器做成Butterworth二阶滤波相位特性在相同阻带衰减下比一阶更陡但同样伴随更大的相位拐点。6.5 不适用于非均匀采样数据EMA递推公式假设每个采样点的时间间隔是固定的。如果实际数据是事件驱动的时间间隔不规则比如某些传感器数据到达时间抖动直接用标准递推会产生额外误差。这种情况下需要先做时间戳插值重采样或者改用带时间常数的连续形式离散化y[i] y[i-1] (x[i] - y[i-1]) * (1 - exp(-Δt/τ))其中Δt是第i个点与第i-1个点之间的真实时间间隔τ是可调的平滑时间常数。这个公式在不规则采样下依然保持合理的平滑效果代价是每次都要算exp函数实时性要求高的场景需要查表近似。7. 除了去噪它还能干什么7.1 金融时序里的经典用法趋势判断与交叉信号指数移动平均在量化分析里被广泛用于均线系统。典型逻辑是快线上穿慢线时产生买入信号下穿时产生卖出信号。当你把快线EMA(5)和慢线EMA(20)看成截止频率不同的两个低通滤波器时本质上是拿两个不同带宽的低通结果做比较信号滞后程度不同导致交叉点滞后于真实拐点。理解了这一点就能利用前面写的群延迟公式估算信号的滞后时长从而更好地理解为什么均线策略天然滞后以及如何利用其他指标弥补。7.2 传感器校准中的基线估计很多传感器输出含有缓慢漂移的基线。用一个大α低截止频率的EMA去跟踪基线再把原始信号减掉这个基线就能得到去除漂移后的信号。这个思路比直接用高速滤波简单得多而且基线实时更新能适应缓慢变化的环境。7.3 控制系统的参考值平滑在PID控制里如果目标值突然跳变会给执行机构带来很大冲击。这时先对目标值做一阶低通平滑让设定值按指数曲线逼近目标值系统的超调量和执行器磨损都会下降。这里用到的是设定值预滤波技巧alpha的选择通常结合系统时间常数来定。7.4 数据压缩前的预处理在边缘端做数据存储或传输时先把高频噪声滤掉再用较低采样率仅保存滤波后的结果可以有效降低数据量。比如原始1000Hz振动数据直接存储太占空间先降到截止频率20Hz的一阶低通对应约10倍降采样裕量再用100Hz采样率存储数据量直接降到原来的十分之一而有效信号几乎没有失真。8. 如果这一步不够用从一阶扩展到更高阶8.1 为什么一阶滤波不够的时候会不够一阶低通的阻带衰减斜率只有-20dB/十倍频程。如果噪声频率和信号频率靠得比较近比如信号在1Hz以内噪声在3Hz一阶滤波在3Hz处只能衰减约6到8dB噪声残余还是很明显。这时候增大截止频率没意义因为会伤到信号正确思路是提高滤波器阶数让阻带衰减更陡。8.2 级联多个一阶滤波器把两个一阶低通串联就得到二阶低通。离散化实现很简单跑两遍递推。class TwoPoleLowPass: def __init__(self, alpha): self.a alpha self.y1 0.0 self.y2 0.0 def filter(self, x): self.y1 self.a * x (1 - self.a) * self.y1 self.y2 self.a * self.y1 (1 - self.a) * self.y2 return self.y2注意两级串联后的等效截止频率会比单级略低。要获得相同的-3dB点两级滤波器的单级截止频率需要按一定比例调高一些。实际上更常见的做法是直接设计一个巴特沃斯二阶低通滤波器再用双线性变换得到差分方程的分子分母系数比两个一阶串联更灵活但实现起来也多两步。8.3 何时该上卡尔曼滤波一阶低通滤波器本质上是固定系数的状态估计器它对信号统计特性已知且稳定的场景最有效。如果信号模型更复杂比如目标在运动且噪声方差会变化或者需要对多变量状态做联合估计固定系数就不够用了。这时可以考虑卡尔曼滤波器——它本质上是一个自适应增益版本的最优低通可以根据测量噪声和过程噪声动态调整滤波增益对动态系统的估计效果远超一阶低通。但卡尔曼的代价也很明显需要建模、需要调噪声协方差矩阵、计算量大一个数量级、参数不当时容易发散。我的建议是能用一阶低通解决的就别上卡尔曼。工程里90%的平滑需求用一阶低通就够了另外10%再考虑进阶方案。8.4 零相位失真的离线处理方案如果处理的是离线数据比如事后分析和回放想消除全部相位滞后可以用双向滤波from scipy.signal import filtfilt, butter def zero_phase_lowpass(data, cutoff_hz, fs, order2): b, a butter(order, cutoff_hz / (fs / 2), btypelow) return filtfilt(b, a, data)这个方法先正向滤波再把序列反转后反向滤波一遍。正向引入的相位滞后会被反向过程完全抵消净效果为零相位延迟同时阻带衰减加倍二阶变四阶效果。代价是必须有完整数据集无法实时输出。9. 实操总结与个人体会最后把最核心的实践经验列一下都是这几年实际调数据调出来的感受。系数换算表是日常调试的起点。为了方便查阅我把常用场景列个表假设fs1000Hz目标截止频率fcα值近似公式对应时间常数τ阶跃上升时间(95%)1Hz0.00628159ms477ms5Hz0.031431.8ms95.5ms10Hz0.062815.9ms47.7ms20Hz0.12567.96ms23.9ms50Hz0.3143.18ms9.55ms100Hz0.6281.59ms4.77ms记住三个关键数字α 2π·fc/fs用这个换算、τ 1/(2π·fc)用这个理解滞后、阶跃95%响应 ≈ 3τ用这个估算响应时间。这三个公式能解决绝大多数调参现场的拍脑袋问题。再多说一句经验。滤波这件事永远先确认频谱再动手。很多人拿到数据上来就调α调半天也不知道自己在调什么。正确流程是先对信号做一次FFT看看有效信号和噪声各在什么频段再决定要不要滤波、用什么截止频率。甚至有些场景根本不需要滤波只是信号调理或传感器选型的问题。滤波器不是万能的但它做对了的时候确实是性价比最高的信号处理手段。这套把指数移动平均和一阶低通滤波合并理解的思路不仅能帮你少记一个公式更重要的是能让你在统计学和信号处理两个领域的文献之间自由跳转。下次看到有人用EMA处理股价、有人用低通滤波处理振动信号心里清楚他们做的是同一件事。
返回列表