
不需要主标题直接从## 1.开始。自打我开始同时用Python做量化交易策略回测又用C语言在STM32上写传感器滤波算法之后就一直想写点东西把“指数移动平均”EMA和“一阶低通滤波”这两件事儿掰开揉碎了讲清楚。很多人觉得这是两个领域的事一个是金融数据平滑一个是信号处理八竿子打不着。但实际上它们俩根本就是同一个数学公式在不同行业的不同叫法。如果你能打通这个认知等于在数据分析、自动控制、音频处理、传感器融合这些方向上省下了一大笔学习时间。这篇文章我不打算讲那种教科书味儿的推导我会从一个实际的传感器数据平滑例子出发先说清楚EMA和低通滤波的等价关系然后给出各个领域参数的换算方法最后聊一聊我自己用的时候踩过的一些坑以及怎么避免它们。1. 指数移动平均和一阶低通滤波同一枚硬币的两面1.1 从两个看起来很不一样的公式说起指数移动平均在金融和技术分析领域太常见了比如MACD指标、均线系统用的都是它。它的递推公式长这样EMA_today α * price_today (1 - α) * EMA_yesterday而一阶低通滤波在信号处理、控制论、传感器调理领域也特别常见。比如你用单片机读取一个红外测距传感器的值或者读一个IMU惯性测量单元的加速度计数据经常能看到这样的代码filtered_value alpha * (raw_value - filtered_value);稍微整理一下这个式子filtered_value (1 - alpha) * filtered_value alpha * raw_value把这两个公式放在一起看你会发现它们的形式完全一样新值等于“系数乘以新样本”加上“另一个系数乘以上一次的历史值”。唯一的区别就是符号习惯不同——金融里面习惯写成α乘以新数据加(1-α)乘以旧均值而信号处理里面习惯写成(1-α)乘以旧值再加α乘以新采样。本质上没有任何区别。所以我经常跟同事说你根本不用学两遍你只需要把这个递推结构真正理解了金融数据平滑和传感器数据去噪就都会了。1.2 为什么它叫“指数”移动平均很多人跟我一样一开始最困惑的问题是名字里带“指数”到底指什么答案是如果你把EMA递推公式无限展开你会发现每一个历史样本点对当前值的影响权重是随时间间隔呈指数衰减的。我举个例子假设有一个序列从当前的样本x_t回溯到很久以前的样本x_{t-n}那么x_{t-n}对当前EMA值的贡献它的权重是weight_n α * (1 - α)^n这里的权重随着n的增大呈指数衰减。1-α如果小于1那么它越乘越小而且是指数级别的缩小。如果你把权重画出来会看到一条从高到低、平滑往下掉的曲线跟“线性加权移动平均”那种固定的权重降幅完全不同——指数衰减一开始掉得最猛后面越来越平缓。这就解释了为什么EMA能比简单移动平均SMA更快地反映最近的变化趋势又不会像单点原始数据那样毛刺那么多。它在“响应速度”和“平滑程度”之间给出了一个天然的折中方案而且只需要保存一个状态变量上一次的EMA值不需要像SMA那样维护一个长度为N的窗口这在嵌入式系统里面简直是巨大的优势。1.3 低通滤波器视角这个递推公式在做什么从信号处理的角度看一阶低通滤波器做的事情是让低频信号顺利通过抑制高频信号。传感器数据里面的高频抖动、测量噪声本质上就是高频成分而真实的物理量变化通常是相对缓慢的属于低频成分。所以你把递推公式写成一个差分方程的形式y[n] y[n-1] α * (x[n] - y[n-1])这里的x[n]是当前采样输入y[n]是当前滤波输出。当α比较小的时候y[n]会被y[n-1]强烈“拽住”所以输出变化很慢高频抖动被压得厉害但这个滤波器响应也慢。当α接近1时滤波器几乎不进行平滑输出几乎等于原始输入。这就是一阶低通滤波的全部秘密用一个系数α来调节“信任新数据”和“信任历史滤波值”的比例。2. 关键参数α的深度解剖怎么选怎么算2.1 α的物理意义与取值范围不管是叫EMA还是叫一阶低通滤波这个α都是两个领域共同的核心旋钮。它的取值范围必须在0到1之间。极端情况也很值得想一想α 1滤波器输出完全等于当前样本不做任何平滑相当于直通。α 0滤波器输出永远保持初始值完全不跟踪输入数据全被“堵死”了。实际操作中α一般取0.001到0.5之间。如果你在做价格趋势判断可能想用更小的α来得到一条更平滑的曲线如果你是做实时控制可能就需要更大的α来降低延迟。2.2 金融领域怎么把N日EMA换算成α如果你习惯了“20日均线”“5日均线”这种基于窗口N的说法那么EMA的α和N日均线是有一个标准换算公式的α 2 / (N 1)举个例子如果你想构造一个“20日EMA”那么对应的α就是2除以21约等于0.0952。注意EMA并不是真的要开一个长度为20的窗口来缓存数据它只是有一个“等效的窗口长度”的概念。这个等效窗口长度可以帮助你直观感受这个EMA到底覆盖了多少历史信息。另一个更直观的指标是“平均年龄”或者叫“时间常数”。2.3 信号处理领域由截止频率计算α在信号处理和控制领域一阶低通滤波器最常用的设计指标是截止频率也叫-3dB频率记作f_c。高于这个频率的信号会被明显衰减。给定采样周期Δt和截止频率f_cα可以通过下面的近似公式来计算α ≈ 2π * Δt * f_c / (2π * Δt * f_c 1)或者如果使用时间常数τ来表达就是τ -Δt / ln(1 - α)这里的τ表示滤波器的时间常数——它等于输出值上升到输入阶跃值的约63.2%所需的时间。我举个例子。假设你的惯性测量单元IMU采样频率是100Hz采样周期Δt就是0.01秒。你想把加速度计数据的带宽限制到5Hz以滤除机械振动带来的高频干扰那么α ≈ 2π * 0.01 * 5 / (2π * 0.01 * 5 1) ≈ 0.314 / 1.314 ≈ 0.239这个计算公式在数字信号处理的教科书里都找得到。虽然严格来说这个公式是带有一点近似的但在工程上已经足够准确了。2.4 表格式对照N、α、截止频率、时间常数为了让大家看得更直观我把几个常用的α值和它们在不同语境下的“等效参数”列成了一张表。假定采样频率为100Hzα取值等效N日EMAN(2/α)-1时间常数τ单位秒约等效截止频率f_cHz0.53日0.014s~11.3Hz0.119日0.095s~1.68Hz0.0539日0.195s~0.82Hz0.01199日0.995s~0.16Hz这张表非常实用尤其是在做传感器应用时我经常先根据期望的“响应有多快”直接确定时间常数τ再去反推α这样比凭感觉调参数要靠谱得多。3. 我从零实现一个传感器平滑滤波器的完整过程3.1 背景与需求定义之前我做了一个基于超声波测距模块的液位监测小项目单片机每隔50毫秒读一次距离值也就是采样频率20Hz。麻烦的是液位表面会有波动再加上超声波回波本身的不稳定性读出来的距离值跳得厉害。同一个静止液面读数能在上下5厘米之间乱跳。我需要的指标有两个滤波后的数据必须比较平滑不能出现肉眼可见的抖动。响应不能太慢。如果液位真的快速上升或下降我希望在一两秒内能跟踪到变化。这其实就是典型的“低通滤波需求”我决定用一阶低通滤波来解决。3.2 参数计算与实际编程根据需求液位变化如果能在1.5秒左右被跟踪到那么这个时间常数τ大约是0.5秒比较合适。因为一阶系统的阶跃响应大约经过3倍τ的时间就能稳定到最终值的95%左右。我取τ 0.5秒采样周期Δt 0.05秒那么α 1 - exp(-Δt / τ) 1 - exp(-0.05 / 0.5) 1 - exp(-0.1) ≈ 0.0952这个α在0.05到0.1之间属于比较平滑但又不至于迟钝的范围。然后我直接在嵌入式代码里实现float lowpass_filter(float raw_value) { static float filtered_value 0.0f; static uint8_t first_run 1; if (first_run) { filtered_value raw_value; first_run 0; } else { filtered_value alpha * (raw_value - filtered_value); } return filtered_value; }注意我把初始值设置成了第一次采样的原始值这样就不会出现启动阶段滤波器输出从0开始爬升的问题。3.3 实测数据与效果评估下面是同一组超声波传感器数据在滤波前和滤波后的几个典型值都是静止液面环境下采集的采样序号原始读数cm滤波后输出cm145.245.20248.747.19343.546.84447.946.94550.247.241047.147.032048.047.185046.847.02可以看到原始读数一开始因为液面晃动出现了45.2cm到50.2cm的大幅度跳变但滤波输出始终稳定在47cm附近。关键的是从第1次采样到第10次采样约0.5秒滤波器输出已经基本收敛到了真实值附近响应时间完全符合预期。3.4 为什么不用滑动平均SMA这里我要特别说明一下选型的理由因为它直接关系到整个方案的成败。第一滑动平均需要维护一个长度为N的数组每次新数据进来要把整个窗口平移一次在单片机这种RAM资源受限的环境里如果N选大了会占用不少内存。一阶低通滤波只需要一个静态变量RAM开销几乎为零。第二滑动平均的滞后效应是“固定”的。如果窗口N20那么输出始终滞后大约(N-1)/2个采样周期。而一阶低通滤波的时间常数是可以灵活调整的滞后和噪声抑制之间的关系更线性。第三滑动平均对突然出现的离群点反应明显——一个非常大的毛刺会让接下来N个点的平均值都偏高形成一段“台阶”。而一阶低通滤波因为是指数衰减权重同样的离群点只会产生一个小的凸起然后快速回落。3.5 我踩过的初始化坑与循环依赖问题在做这个项目的时候我踩过两个比较有代表性的坑值得展开说一下。第一个坑是初始值的处理。一开始我把滤波器的初始值设成0结果设备一上电输出从0慢慢往上爬爬了整整几秒才到真实液位周边。如果这时候外部控制系统误以为液位真的在快速上升可能就会触发错误的动作。解决方式我在上面的代码里也写了非常简单第一次滤波时直接把原始读数赋给输出值。如果你用的是Python实现也建议初始化时直接用第一个数据点class LowPassFilter: def __init__(self, alpha): self.alpha alpha self.y None def filter(self, x): if self.y is None: self.y x else: self.y self.alpha * (x - self.y) return self.y第二个坑发生在实现卡尔曼滤波的时候我当时在一个稍微复杂的项目里同时使用了加速度计和陀螺仪滤波结果之间存在循环依赖——加速度计的滤波输出要输入给陀螺仪作为参考而陀螺仪的滤波输出又会反过来影响加速度计的滤波输出。这导致系统在启动阶段出现振荡数据互相“纠缠”了一段时间才收敛。解决思路是打破循环把其中一个通道的初值固定为原始测量值或上一个周期计算的结果另一个通道再去依赖它。一阶低通滤波本身很简单但一旦嵌入到嵌套系统里初始化顺序和状态解耦就变得非常重要。4. 频率响应视角为什么噪声被压掉了但有代价4.1 幅频特性曲线背后的一句话如果你把一阶低通滤波器放到频域里去分析它的传输函数是H(s) 1 / (1 sτ)这就是一阶惯性环节也是控制系统里面最经典的传递函数之一。从幅频特性来看低于截止频率的信号基本不被衰减高于截止频率的信号以每十倍频程20dB的速度衰减。这就带来了一个不可避免的后果噪声抑制和信号响应速度是天生矛盾的。你越想压掉高频噪声也就是把截止频率设得越低α就越小信号的有用变化也会被延迟得越厉害。这就是所谓的“滞后”。所以我看到有人拿着传感器原始数据直接上α0.01的滤波器然后抱怨系统反应太慢——这其实不是滤波器的问题而是参数选择没有匹配应用场景。如果既要低延迟又要强平滑你得考虑更高阶的滤波器或者卡尔曼滤波但那是另一个复杂度层级的问题了。4.2 相位滞后是怎么影响控制系统的在控制类项目里一阶低通滤波的相位滞后是必须正视的问题。每一个一阶低通环节都会在截止频率附近带来大约45度的相位滞后在远高于截止频率的地方接近90度。如果你在一个闭环控制回路里随便加一个低通滤波比如对误差信号做滤波那么滤波引入的滞后会减小系统的相位裕度可能导致系统震荡甚至不稳定。我的个人建议是在控制回路里面滤波器应该加在反馈通路上的原始测量值之前而不是加在控制器输出之后。而且α不要取得太小必要的时候要做闭环带宽和滤波器截止频率的匹配计算千万不要拍脑袋。4.3 噪声模型里的“白噪声假设”到底怎么理解很多人问低通滤波对噪声的抑制效果能不能精确预测能但前提是你得知道噪声的统计特性。比如我通过一阶低通滤波器处理一个叠加了白噪声的信号。如果白噪声的方差是σ²那么经过一阶低通滤波之后输出噪声的方差大约是σ_out² ≈ σ_in² * α / (2 - α)这个公式看起来很简练但我提醒一句它假设输入噪声是白噪声且采样周期是均匀的。如果你的传感器噪声是低频漂移或者高频周期性干扰这个公式的误差会很大此时应该考虑带通或陷波滤波。5. 工程实战C、Python、MATLAB三语言实现与要点5.1 C语言实现强调定点数和浮点数的取舍在单片机上如果主控不带FPU浮点运算单元频繁用float做乘法会产生较大的计算开销。尤其在一些老旧的8位单片机上浮点运算是靠软件模拟的速度非常慢。我曾经在一颗主频只有几十MHz的MCU上跑过一个控制算法滤波器运行得没问题但加上浮点运算之后总循环时间超过了控制周期要求导致系统出现了周期性的丢步。解决办法是把浮点运算改成定点运算把系数α放大成一个整数比如放大1024倍然后滤波运算全部用整数乘法和移位来完成。#define ALPHA_Q10 97 // 相当于0.0952 * 1024 int16_t lowpass_filter_q10(int16_t raw) { static int32_t filtered_q16 0; static uint8_t first 1; if (first) { filtered_q16 ((int32_t)raw) 16; first 0; } filtered_q16 ((int32_t)raw 16) - filtered_q16) * ALPHA_Q10 10; return (int16_t)(filtered_q16 16); }注意这里用Q格式来表示小数。将滤波器的中间状态存储在更高位数的变量里保留足够的小数位避免连续多次运算后误差累积导致输出不收敛。5.2 Python实现向量化与pandas的ewm在量化分析和数据处理场景我大多用Python。如果是一次性处理大批量历史数据直接写for循环做指数移动平均会很慢我建议使用pandas内置的ewm方法import pandas as pd df pd.DataFrame({price: [10, 10.5, 11, 10.8, 10.9]}) df[ema_20] df[price].ewm(span20, adjustFalse).mean()这里的span20对应的就是20日EMA而adjustFalse意味着使用递推形式和前面讲的公式完全一致。如果你要自己实现惯用 α 的版本可以这样写def ema_custom(series, alpha): return series.ewm(alphaalpha, adjustFalse).mean()注意pandas的ewm.mean()在adjustFalse的时候第一个值就等于第一个原始值这在很多金融数据场景下是符合预期的。但如果你想更严谨一些可以在初期做一些偏差修正下一节会展开讲。5.3 MATLAB/Simulink实现离散滤波器模块的使用在控制系统仿真中MATLAB的filter函数或者Simulink里的Discrete Transfer Fcn模块都很方便。一个离散一阶低通滤波器的传递函数可以写成Y(z)/X(z) α / (1 - (1-α) * z^(-1))在Simulink中参数Numerator填[α]Denominator填[1, -(1-α)]。这个模块的采样时间一定要设置成和你的系统采样周期一致否则滤波效果和预期会差很远。5.4 三种语言实现经验对照表实现维度C语言PythonMATLAB/Simulink最适合场景嵌入式、实时控制批量数据分析、策略回测控制系统仿真、算法验证核心关注点定点数优化、内存占用向量化性能、数据对齐采样时间设置、仿真步长易踩坑点浮点数模拟运算慢ewm参数含义混淆连续/离散域混淆状态保存静态变量/全局变量类成员变量单位延迟模块5.5 我推荐的学习顺序如果你刚接触这个概念我建议的学习路线是先用Python或者Excel把EMA的递推关系彻底理解把α和窗口N之间的换算关系弄熟——然后再去看单片机的C语言实现最后再看MATLAB里的频域分析和传递函数。不要一上来就盯着传递函数和Z变换啃那样容易劝退。先把时域递推搞明白其他都是水到渠成的事。6. 高级应用与扩展从双EMA到卡尔曼滤波6.1 双EMA和MACD用它做趋势的一次差分金融里面经常用两条不同周期的EMA来做趋势判断。比如MACD指标就是计算12日EMA和26日EMA的差值再对差值做9日EMA得到信号线。本质上这是在做“快慢两条平滑曲线之间的乖离度分析”。这个思想在工业信号处理上也有应用。我曾经用一个类似“双EMA差值”的方法来检测传感器数据的突变一条快EMAα比较大响应快和一条慢EMAα比较小平滑度高两者之间的差值超过阈值就认为出现了异常跳变。这相当于一个最简单的高通滤波器能提取信号中的快速变化成分同时屏蔽掉直流分量。6.2 二阶低通滤波器什么时候值得升级一阶低通滤波对高频噪声的衰减速度是每倍频程6dB如果噪声很强或者离信号频率很近你可能会觉得它压得不够。这时可以考虑用二阶低通滤波在-3dB之后以每倍频程12dB的速度衰减。二阶低通滤波可以用两个一阶低通滤波串联来实现但参数不一样。如果简单地把两个相同α的一阶滤波串联那么系统会过阻尼响应变慢更好的方式是按二阶系统的固有频率和阻尼比来设计参数。在嵌入式里如果你不想写复杂的差分方程代码也可以直接用一个二阶IIR滤波器实现这个以后有机会再单独写。6.3 和一阶低通滤波相比卡尔曼滤波的优势和代价很多搞机器人或者无人机的小伙伴会问我有卡尔曼滤波为什么还要学这个简单的低通滤波我的看法是卡尔曼滤波本质上是一个“时变增益的最优滤波器”它可以根据你对系统模型的信任程度和传感器噪声的统计特性动态调整α。在传感器噪声方差已知且系统模型比较准确的情况下卡尔曼滤波确实比固定α的一阶低通滤波表现好很多。但代价是你的要懂状态空间模型、协方差矩阵更新而且在模型不准的时候卡尔曼滤波会“迷之自信”给出的结果甚至比简单低通滤波还差。所以我的建议是对绝大多数传感器平滑任务一阶低通滤波已经够用如果你要做多传感器融合、需要预测或数据缺失处理再考虑卡尔曼滤波不迟。7. 参数整定经验从拍脑袋到有章法7.1 一个可复制的参数整定流程多年的实操总结下来我调一阶低通滤波参数的时候一般按以下步骤走确定采样周期。这一步是基础如果采样周期都不稳定后面算出来的参数都是废的。估算信号的有用频率范围。比如液位变化最快也就每秒0.2Hz那我就希望截止频率在1Hz左右。确定可接受的滞后时间。比如系统要求从液位突变到检测到变化的时间不超过2秒那么由τ大约等于0.5到0.7秒就是一个合理的出发点。由τ或截止频率反推α。一边运行一边用可视化数据调整。我通常把原始数据和滤波数据画在同一个图上动态微调α直到满足“平滑度”和“跟随性”的平衡。7.2 时间常数和噪声抑制率的快速参考时间常数τ秒经过该时间后阶跃响应的输出值百分比噪声衰减相对比例相对于α0.5时0.5τ39.3%更平滑但滞后更明显1τ63.2%常规选择区间2τ86.5%平滑效果好但响应偏慢3τ95.0%接近稳态适合要求严格平滑的场景这张表可以当成一个参数初选的手册实际工程中大部分滤波器参数调整都发生在这个范围里。7.3 一个反直觉的参数调整教训最后分享一个我实际遇到的教训。之前调一个生产线上的光电传感器信号滤波器一开始我试图把α调小来消除所有干扰。结果α小到0.005以后信号确实变得非常平滑但传感器在快速工件和慢速工件之间切换时滤波输出需要好几秒才能跟上导致后续的分类机构频繁误判。后来我意识到那种“把所有抖动都消除掉”的思路是错的。传感器数据里面的“抖动”不一定全是噪声有一些是真实物理过程的高频成分。滤波的目的是抑制你不关心的频率成分而不是把数据改得面目全非。最终我把α重新调到0.1左右保留了一部分真实的快速波动分类准确率反而提升了。所以我的经验是决定α大小的不是“你觉得数据有多抖”而是“下游算法需要多快响应”。先定滞后预算再定平滑目标顺序不要反。这个一阶低通滤波/指数移动平均的话题说穿了就一个递推公式但在工程里牵扯出的门道确实不少。希望你看到这里脑子里已经把那根从金融均线到单片机滤波器的线彻底打通了。