为何向0.0应用指数移动平均(EMA)时收敛更慢?
指数移动平均收敛迭代次数差异的原因分析(与非正规数的关联)
先贴出你的测试代码:
#include <math.h> #include <iostream> using namespace std; inline void Task(double start, double target) { double a0 = 0.0101252; double z = start; double value = -1.0; double temp = 0.0; int counter = 0; while (value != temp) { temp = value; // exponential moving average z += a0 * (target - z); value = z; counter++; } cout << "start: " << start << " | target: " << target << " | iterations: " << counter << std::endl; } int main() { Task(0.0, 0.01); Task(0.01, 0.0); Task(0.01, 0.02); Task(0.02, 0.01); }
你的测试现象很典型:
指数移动平均(Exponential Moving Average)在非零值间切换或从0.0向非零值收敛时,迭代次数约3100次;但从0.01向0.0收敛时,迭代次数高达72305次,约为前者25倍。
这个差异确实和**非正规数(Denormals)**直接相关,咱们来拆解具体原因:
核心逻辑:非正规数导致的精度断崖式下降
先把EMA的更新公式变形一下:z = z + α*(target - z) 等价于 z = (1-α)*z + α*target。当目标值是0.0、起始值是0.01时,公式简化为 z = (1-α)*z,其中1-α ≈ 0.9898748。
- 正规数阶段:当z还大于
2^-1022(double类型的正规数最小值)时,每次乘以(1-α)都是按比例缩小,精度保持在double的15-17位有效范围内,收敛速度稳定。 - 非正规数阶段:当z缩小到小于
2^-1022后,就进入了非正规数区域。非正规数没有隐含的最高位1,靠尾数低位表示极小值,精度会随数值减小线性下降——最小的非正规double数只有1位有效精度。
进入这个区域后,z的更新不再是按比例缩放,而是变成了步进式缩小:每次只能减少一个当前精度下的最小单位,而不是乘以固定比例。这就导致收敛到0的过程变得异常缓慢,直到z最终下溢到0.0,才会触发value == temp退出循环。
其他场景为什么正常?
- 从0.0向0.01收敛:z从0开始增长,很快就进入正规数范围(0.01远大于
2^-1022),全程都是稳定的比例增长,迭代次数正常。 - 非零值间切换:比如0.01→0.02或0.02→0.01,z始终处于正规数区间,更新过程的精度没有断崖式损耗,收敛速度保持一致。
验证小技巧
你可以在循环里加个判断,用std::fpclassify(z) == FP_SUBNORMAL检测z是否进入非正规数区域,会发现0.01→0.0的迭代后期,绝大多数次数都消耗在非正规数阶段。另外,开启编译器的**Flush to Zero(FTZ)**优化(比如GCC的-ffast-math),非正规数会被直接置为0,此时这个场景的迭代次数会立刻降到和其他场景一致,直接坐实非正规数的影响。
内容的提问来源于stack exchange,提问作者markzzz
相关产品推荐
相关产品推荐

