尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

指数移动平均与一阶低通滤波等价?α参数与工程落地全解析

指数移动平均与一阶低通滤波等价?α参数与工程落地全解析 在做传感器数据处理那几年我前后在两个技术圈子里被两个词反复轰炸指数移动平均、一阶低通滤波。读信号处理资料看到的是 RC 一阶低通翻量化分析或者深度学习训练曲线平滑工具看到的却是 EMA。一开始我以为它们是两套算法直到把递推式写在纸边上后才发现两者在数字域里就是同一个滤波器只是换了称呼、换了参数习惯。这篇文章想把这个等价关系讲透然后进入最实际的部分α 怎么定、代码怎么写、哪些边界条件一不注意就会翻车。无论你是做嵌入式传感器滤波、量化指标计算还是单纯想把时序数据抹平一点都能从里面找到直接能用的方案。1. 两个名字同一个递推式EMA 和一阶低通到底差在哪1.1 指数移动平均本质是一张“按指数衰减的加权账单”指数移动平均最常见的写法长这样y[n] α * x[n] (1 - α) * y[n-1]这个式子的意思是当前输出y[n]由两部分组成——一部分是当前输入x[n]一部分是上一次的输出y[n-1]。α 是介于 0 到 1 之间的权重系数。α 越接近 1当前输入占的权重越大输出越跟手α 越接近 0保留历史信息越多输出越平滑。把这个递推式一层层展开你会看到它根本不是在“平均最近 N 个点”而是对所有历史样本都保留了权重只是越老的样本权重按(1-α)^k指数衰减。这有点像你每个月记一笔账过去 10 年前的账单当然还在但它的影响已经小到可以忽略。真正起作用的窗口大约在1/α个采样点附近比如 α 0.2 时10 个点以前的数据权重衰减到大约 0.8^10 ≈ 0.107基本上可以当它不存在了。量化分析里经常用到的 EMA比如 12 日 EMA、26 日 EMA就是拿这个公式套到每日收盘价上。它们会把“最近若干天”的价格变化反映到一个平滑曲线上但并不是简单地把所有 K 线收盘价加起来除以天数所以会比 SMA简单移动平均对新鲜行情更敏感。1.2 一阶低通滤波是从模拟电路和微分方程那边走来的一阶低通滤波器的源头是模拟电路里的 RC 低通也可以从控制系统的角度理解它是一个时间常数为 τ 的惯性环节。连续域里的微分方程是τ * dy/dt y x放到数字控制系统里采样周期是 T经过一番离散化处理后最常见的实用递推式变成y[n] y[n-1] α * (x[n] - y[n-1])这个式子和 EMA 的写法完全一样只是 α 在信号处理语境里通常不再是“拍脑袋”定的权重而是由时间常数 τ 和采样周期 T 换算出来的滤波系数。1.3 为什么经常有人觉得它们是两码事问题主要出在两个圈子各自有一套习惯。金融技术指标圈子会用“周期 N”来定义权重常见公式是α 2 / (N 1)比如 N 12 的 EMAα 就是 2 / 13 ≈ 0.154。这个公式来自对“半衰期”或“等效窗口”的一种近似设计目的是让均线和 N 日窗口在反应速度上可比。嵌入式信号处理圈子则喜欢直接讨论截止频率 f_c。一阶低通滤波器的设计目标是低于截止频率的信号保留高于截止频率的噪声被抑制。于是大家一上来就会问“你的采样率多少你想到多少 Hz 截止”然后反推 α。这种语境差异很坑人。同一个迭代公式一个用“多少天窗口”来描述另一个用“多少 Hz 截止”来描述中间隔着一层换算逻辑。只要这层窗户纸没捅破你在两个技术栈之间来回切换时就会觉得它们在讲两种知识。1.4 先记住一个最直接的结论指数移动平均和一阶低通滤波在离散数字信号处理里基本是同一个迭代器。唯一的差别是参数怎么来、初始化怎么处理、以及在连续域里的物理意义。这一点一旦想通你会发现很多库函数只是换了个名字。你用 Python 的ewm函数和用 C 写一个一阶低通类内核其实差不多。后面几节我再把参数处理和落地细节展开这才是真正容易用错的地方。2. α 的三副面孔权重、时间常数、截止频率怎么换算2.1 α 的第一副面孔控制当前输入的分量前面说过α 大一点滤波输出就更相信当前输入α 小一点滤波输出更依赖历史平滑。从最简单直觉来看你可以把 α 理解为“这次采样我信多少”。如果你只是想临时看一个趋势直接试几个 α 就行。但工程项目的麻烦点在于这次项目里你觉得 α0.1 挺好下次换个采样率又得重新试。试来试去没有积累参数没法在不同系统之间迁移这就是为什么不能只停留在“权重”这个层面。2.2 α 的第二副面孔换算成等效时间常数如果把离散公式往连续域方向看输出跟踪输入时存在一个惯性滞后可以用时间常数 τ 来描述。当 α 比较小的时候近似关系是τ ≈ (1 - α) / α * T其中 T 是采样周期。如果 α 0.1T 0.01 秒那么 τ ≈ 0.9 * 0.01 / 0.1 0.09 秒。也就是说这个滤波器大概有 90 毫秒的惯性。实际工程里我更喜欢用另一个更稳定的指数映射α 1 - exp(-T / τ)这个映射的好处是不管 T 和 τ 的比值是大是小α 都严格落在 0 到 1 之间。当你设定目标时间常数 τ然后按实际采样周期 T 计算 α滤波器在不同采样频率下表现出来的时间尺度是一致的。反过来如果只知道 α想要估计等效时间常数在 α 较小的时候直接用τ ≈ T / α就够了α 较大时用τ -T / ln(1-α)更准确。2.3 α 的第三副面孔换算成截止频率一阶低通滤波器对应一个 -3 dB 截止频率。如果采样率是 f_s目标截止频率是 f_c常用的换算公式是α 1 - exp(-2π * f_c / f_s)注意这背后做了连续到离散的映射工程上直接这么用没问题。当 f_c 远小于 f_s 时公式可以近似为α ≈ 2π * f_c / f_s举一个具体例子。你有一个 200 Hz 的 IMU 数据流想滤掉 0.5 Hz 以上的抖动那么代入公式α ≈ 2π * 0.5 / 200 ≈ 0.0157也就是说滤波器的平滑程度会相当弱输出会滞后真实信号大约 1 秒级别的时间尺度。很多人第一次做传感器滤波时总觉得平滑效果不明显回头一查发现只是 α 设得太小了。下面这张表可以直接帮你建立感觉。假设采样率 f_s 100 Hz目标截止频率 f_c (Hz)对应 α指数映射输出达到输入的 63% 大约所需时间0.20.0125约 0.8 秒0.50.031约 0.32 秒10.061约 0.16 秒20.118约 0.08 秒50.270约 0.03 秒100.467约 0.016 秒这张表不是让你死记硬背而是提醒你一个事实在 100 Hz 采样率下想要滤到 0.5 Hz 以下α 大概率只有零点零几远没有很多人第一反应设的 0.5 那么激进。2.4 参数不是越大或越小就好滞后才是代价滤波的本质是拿滞后换平滑。α 越小高频噪声被压得越狠但信号本身的变化也被磨平了。如果你拿滤波后的数据去做闭环控制比如电机转速环或者无人机姿态环过大的滞后会导致控制响应迟钝甚至引起震荡。我的习惯做法是先确定业务上能接受的滞后时间再反推滤波器参数然后看滤波后的频谱确认噪声有没有被压到可以接受的水平。先定滞后再调平滑顺序不能反。否则很容易陷入“越平滑越好”的误区最后控制系统反应慢半拍反而更难调。3. 实战落地滤波函数怎么写得既稳又通用3.1 一个自带初始化保护的实时滤波器很多嵌入式项目里数据不是一个数组整体出现的而是一个包一个包从传感器里读出来的。这种场景下最简单的做法是写一个带状态的对象。class FirstOrderLowPass: def __init__(self, alpha): self.alpha alpha self.y None def update(self, x): if self.y is None: self.y x else: self.y self.alpha * (x - self.y) return self.y这里的self.y is None判断很关键。如果直接拿 0 当初始值而真实信号本身带有直流偏置比如 ADC 读出来是 2048 左右那么滤波输出要经过一段很长的时间才能从 0 爬到真实值。这个启动过程中你的控制逻辑或者状态判断很容易误动作。做一个稍微复杂一点的版本把重置接口也加上class FirstOrderLowPass: def __init__(self, alphaNone): self.alpha alpha self.y None def reset(self, x): self.y x def update(self, x): if self.y is None: self.y x else: self.y self.alpha * (x - self.y) return self.y def set_alpha(self, alpha): self.alpha alpha在单片机 C 语言里也是同样的逻辑只是用一个布尔变量标记首包即可。处理流式数据时状态对象比每次重新从头处理一个数组要自然得多。3.2 批量处理数组时的循环版本如果你手里已经有一段完整数据比如离线分析 CSV 里的几百秒数据可以用一个循环直接处理import numpy as np def lowpass_array(x, alpha): x np.asarray(x, dtypenp.float64) y np.empty_like(x) if x.size 0: return y y[0] x[0] for i in range(1, x.size): y[i] y[i-1] alpha * (x[i] - y[i-1]) return y这段代码最好理解但 Python 的 for 循环在几百万点时会有点慢。numpy 版本没有直接向量化的递归替代但你可以用scipy.signal.lfilterfrom scipy.signal import lfilter def lowpass_lfilter(x, alpha): # y[n] y[n-1] alpha * (x[n] - y[n-1]) # 写成差分方程y[n] - (1-alpha)*y[n-1] alpha*x[n] a [1, -(1 - alpha)] b [alpha] y lfilter(b, a, x) y[0] x[0] # 可选的初值修正 return y注意lfilter默认初值 y[-1] 0所以会有一段启动瞬态。如果只关心稳定后的结果可以忽略如果对开头几个点敏感最好用前面的手动循环版本或者在开头预留一段预热数据。3.3 离线批量处理可以做到零相位偏移如果数据不是实时生成的而是做历史回放或离线的学术分析那就不必忍受一阶低通的滞后。你可以把数据正向滤一遍然后把得到的结果反着再滤一遍相位相互抵消。这就是scipy.signal.filtfilt做的事from scipy.signal import butter, filtfilt # 根据目标截止频率和采样率计算系数 b, a butter(1, fc / (fs / 2), btypelow) y filtfilt(b, a, x)这里截断频率要用奈奎斯特频率归一化filtfilt的代价是不能实时输出。实时控制、量化交易这类需要当下立刻产生结果的场景都不能用这种方法否则就相当于用了未来数据。3.4 在量化指标里的对应实现很多行情软件里你看到的 EMA 均线核心就是指数移动平均的递推。MACD 指标更直接它是快线 EMA12 与慢线 EMA26 的差值用来观察短期动量相对于长期动量的变化。公式里的2 / (N 1)本质上是把一个窗口长度为 N 的均线近似映射成指数衰减权重。如果你把 EMA12 看成一个截止频率约 0.021 倍采样频率的一阶低通把 EMA26 看成截止频率更低的一阶低通那么 MACD 就是在看“两个带宽不同的低通滤波器之间的输出差”。用信号处理的视角去理解技术指标你会发现均线的金叉死叉本质上就是在观察不同频率成分之间的关系和穿越滞后和噪声都对应上了。这个角度对同时做量化和信号处理的工程师挺有启发。4. 最容易翻车的几个边界情况4.1 初值处理一个默认值就能让输出在前面几百点全部作废很多库函数的默认初值是 0。如果真实信号在 100 附近波动你用默认的lfilter滤波会看到输出从 0 开始爬慢慢逼近 100。这个“爬坡过程”不是真实信号而是初始化假象。处理办法有两种实时场景把第一个输入直接赋给输出不用 α 加权。离线场景数据开头多留一段预热数据计算完成之后再截掉前面部分。技术指标里也有类似问题许多行情软件的 EMA 在最初几天的数值并不准确就是因为没有足够历史的启动过程。做策略回测时如果对起始段敏感一定要提前处理初始化。4.2 采样周期不是恒定值时固定 α 会让截止频率漂移假设你在 100 Hz 下把 α 调好但实际运行中由于系统负载、串口延迟等原因某些时刻采样间隔变成 20 ms 甚至 50 ms这时固定 α 会带来一个隐形问题截止频率变了。比如 α 0.1 本来在 10 ms 周期下对应约 1.67 Hz 截止。如果某次采样间隔变成 50 ms还是用 α 0.1实际等效截止频率会明显变化滤波器的“反应速度”跟校准时的预期不一致。更稳的做法是在代码里保存时间常数 τ每次采样时根据实际间隔 T 动态计算 αalpha 1 - math.exp(-dt / tau)这里的 τ 来自你期望的截止频率τ 1 / (2π f_c)。这样即使采样间隔偶尔波动滤波器的时间尺度也保持一致。这个技巧在实时嵌入式系统里特别实在。4.3 数据存在尖峰毛刺时一阶低通压不下去一阶低通擅长压制高频随机噪声但对于单个采样点的尖峰毛刺它只能把尖峰“揉”进输出里无法彻底剔除。比如传感器偶尔冒出一个离谱的野值滤波后你会看到输出有一个明显的小鼓包而不是干干净净。处理尖峰的正确顺序是先用中值滤波或阈值判断把野值剔除再做低通平滑。工业现场很多工程师直接把低通系数调到很小来对付尖峰结果噪声是压下去了有效信号的响应也变成蜗牛速度得不偿失。原始数据 → 野值剔除中值/阈值 → 一阶低通平滑 → 输出这一条对于加速度计、激光测距、超声波传感器都适用。4.4 浮点数据尾数问题大偏置上加小扰动会丢精度如果信号本身带有很大的直流偏置比如 12 位 ADC 输出在 2000 附近而你只对叠加在上面的微小波动感兴趣一阶低通的递推式y α * (x - y)在小 α 时可能出现精度问题。原因是当 y 已经接近 2000而每次 x 的变化只有几个 LSB 时(x - y)很小乘以一个很小的 α 后结果可能低于浮点数能表达的有效位。更糟的是y不断累加极小增量时在单精度浮点下可能根本加不进去。解决办法分两种:一是内部滤波时先把直流偏置减掉只对偏置附近的变化量做平滑二是使用更高精度的浮点类型。特别是在 C 语言嵌入式开发中不要为了省空间把滤波变量设成float后又叠加到一个大偏置上必要时用double或做偏置分离。4.5 双向滤波虽然相位友好但绝对不是实时方案如果你看到有人用filtfilt处理实时到达的数据那大概率是理解有误。filtfilt是先把整个序列正向滤波再把滤波结果翻转继续滤波一次这需要看到全部未来数据。用在一个实时趋势跟踪系统里等于把未来的信号偷渡到了当下这在任何实时业务场景中都是不可接受的。离线数据处理可以大胆用在线控制必须走单方向滤波。5. 一阶不够用时的升级路线别把万能期望压在一个 α 上5.1 遇到大量离群点时先上中值滤波一阶低通的局限性注定了它是一个平滑器不是一个稳健估计器。数据里有明显离群点、尖峰、随机丢包空值时上中值滤波更合适。中值滤波把窗口内的点排序后取中间值能直接把孤立的坏点滤掉。实际组合方式很常见先用宽度 3 或 5 的中值滤波消掉尖峰再用一阶低通抑制高频随机噪声。例如测距传感器偶尔反弹一个错误读数靠α 0.01硬压会出现一个持续时间很长的错误平台但先过中值窗口后这个错误点会被直接丢掉。5.2 噪声频段和信号频段很近时一阶低通不够陡一阶低通的衰减速率只有每倍频程 6 dB也就是说噪声频率如果只是信号频率的 10 倍它只衰减约 20 dB。需要更强抑制能力时可以用二阶低通或有源滤波器比如巴特沃斯低通滚降速率会明显更快。但注意阶数越高相位滞后越严重参数对截止频率越敏感。换成二阶之前一定要先衡量自己的需求是噪声离信号很远只是漏进来一点高频干扰还是噪声和信号频谱混在一起前者往往一阶就够后者才需要上高阶。5.3 需要融合多个传感器时用互补滤波或卡尔曼如果你手上有多路传感器比如加速度计和陀螺仪都要用来估计姿态那就不是单通道低通能解决的问题了。常见做法是互补滤波对低频可信的加速度信号用低通对高频可信的陀螺仪信号用高通然后相加得到融合后的姿态。这个思路本质上还是一阶滤波的组合但比单独对每路信号做 EMA 更合理。更复杂的场景比如位置估计、巡航速度估算需要同时处理不同噪声特性的传感器时适合上卡尔曼滤波家族。卡尔曼拿的不只是当前输入和上一时刻输出还维护了一个“我对当前状态的置信度”根据系统模型和测量噪声动态调节权重效果更强但调试成本也更高。5.4 选型时可以参考这套简单逻辑数据问题推荐方案需要考虑的代价高频随机噪声响应速度要求一般一阶低通 / EMA有相位滞后但实现极简少量野值和离群点中值滤波再做低通窗口宽度影响延迟和边缘效果离线分析不希望看见滞后filtfilt双向滤波必须看到全部数据无法实时噪声带离信号很近需要强衰减二阶或更高阶低通难度和相位滞后同时上升多个传感器有互补特性互补滤波需要手动调融合系数系统有明确运动模型和多传感器卡尔曼滤波状态设计、噪声矩阵调参复杂真正干活的时候我见过的项目 70% 以上用一阶低通就够了。它计算量小、逻辑清晰、嵌入式里也不会让你为内存和浮点性能发愁。剩下 30% 属于信号本身复杂、需要知道当前置信度或者传感器数量多的情况才需要往更高阶方向走。先把一阶吃透把 α、时间常数、截止频率之间那层换算练熟练再往上升级时会少走很多弯路。最后分享一个个人经验处理实时数据时不要在一个大项目里到处散落alpha 0.1这种魔法数。把滤波参数集中放到配置文件或一个初始化结构体里设计时先明确采样率和目标截止频率让代码自己计算 α。这样做的好处是换平台、换传感器、换采样率时你只需要改一个截止频率所有滤波行为都保持一致。踩过几次“换个环境就得重新试参数”的坑之后你会发现这个习惯非常值钱。
返回列表