滤波,在最广泛的意义上,是从含噪声的观测序列中估计感兴趣信号的过程。这个概念横跨信号处理、控制理论、时间序列分析和统计学,但核心目标高度统一:从杂乱无章的测量值中还原出隐藏其后的真实状态或趋势。
问题设定
考虑一个最简场景:你在一个有噪声的秤上反复称量同一个物体,每次读数都围绕真实值上下浮动。直觉告诉你取几次读数的平均值——这便是最朴素的滤波思想。形式化地描述,每一个离散时刻 $k$ 有一个不可直接观测的系统状态 $x_k$,以及一个受噪声污染的观测 $z_k$,滤波器的任务是根据到时刻 $k$ 为止的全部观测 $z_1, z_2, \dots, z_k$,给出 $x_k$ 的最优估计 $\hat{x}_{k|k}$。
移动平均的定义
移动平均(Moving Average)是最古老也最直观的滤波器。第 $k$ 时刻的估计定义为最近 $W$ 个观测值的算术平均:
$$\hat{x}k = \frac{1}{W} \sum$$}^{W-1} z_{k-j
唯一的参数是窗口宽度 $W$。$W$ 越大,滤波输出越平滑,但对真实变化的响应越迟缓——平滑度与响应速度之间存在着基本权衡,这一权衡贯穿此后所有的滤波方法。
频域特性
移动平均在本质上是有限脉冲响应(FIR)滤波器,其频率响应为
$$H(\omega) = \frac{1}{W} \cdot \frac{\sin(\omega W/2)}{\sin(\omega/2)} \cdot e^{-j\omega (W-1)/2}$$
其中 $\sin(\omega W/2)/\sin(\omega/2)$ 项是 Dirichlet 核,在主瓣之外有逐渐衰减的旁瓣。这意味着移动平均虽能抑制高频噪声,但会引入振铃效应,且对突变信号的响应呈现线性斜坡。
可视化
下图的 GIF 演示了窗口宽度从 1 逐步增加到 30 的过程中,滤波输出如何在平滑与滞后之间变化。

Python 实现
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
os.makedirs('images', exist_ok=True)
np.random.seed(42)
k = np.arange(200)
true = np.sin(2 * np.pi * k / 50) + (k > 100).astype(float)
obs = true + np.random.randn(200) * 0.5
def moving_average(x, W):
kernel = np.ones(W) / W
return np.convolve(x, kernel, mode='same')
W3 = moving_average(obs, 3)
W8 = moving_average(obs, 8)
W20 = moving_average(obs, 20)
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(k, true, 'k-', label='True Signal', linewidth=1.5)
ax.plot(k, obs, '.', color='gray', alpha=0.4, label='Observations')
ax.plot(k, W3, label='MA W=3', linewidth=1.2)
ax.plot(k, W8, label='MA W=8', linewidth=1.2)
ax.plot(k, W20, label='MA W=20', linewidth=1.2)
ax.set_xlabel('Time Step k')
ax.set_ylabel('Value')
ax.set_title('Moving Average Filtering')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('images/moving_average.svg')
fig.savefig('images/moving_average.png', dpi=150)
plt.close(fig)
fig, ax = plt.subplots(figsize=(10, 5))
frames = list(range(1, 31))
line_true, = ax.plot(k, true, 'k-', label='True Signal', linewidth=1.5)
line_obs, = ax.plot(k, obs, '.', color='gray', alpha=0.3, label='Observations')
line_ma, = ax.plot(k, W3, 'r-', label='Moving Average', linewidth=1.5)
title = ax.set_title('Effect of Window Width W=3')
ax.set_xlabel('Time Step k')
ax.set_ylabel('Value')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.3)
ax.set_ylim(-2.5, 3.0)
def update(i):
W = frames[i]
line_ma.set_ydata(moving_average(obs, W))
title.set_text(f'Effect of Window Width W={W}')
return line_ma, title
ani = animation.FuncAnimation(fig, update, frames=len(frames), interval=200, blit=False)
writer = animation.PillowWriter(fps=5)
ani.save('images/滤波01-图1.gif', writer=writer)
plt.close(fig)
import csv
with open('images/moving_average.csv', 'w', newline='') as f:
w = csv.writer(f)
w.writerow(['k', 'true', 'obs', 'W3', 'W8', 'W20'])
for i in range(200):
w.writerow([i, true[i], obs[i], W3[i], W8[i], W20[i]])
演化与局限
移动平均有三个内在局限。其一,它假设信号在窗口内近似恒定,对趋势和周期信号的跟踪天然滞后。其二,它平等对待窗口内的所有观测,不给近期数据更高的权重——而直觉上,越近的观测越能反映当前状态。其三,它需要存储完整的窗口长度历史数据,内存开销随窗口宽度线性增长。
这三个局限中,第二个和第三个可以被一个优雅的递推公式同时解决:让新观测获得更高权重,旧观测的权重指数衰减,且只需存储上一个估计值。这就是指数平滑——下一篇的主题。
参考文献
[1] arXiv:2410.21184 — Shannon-like Interpolation with Spectral Priors and Weighted Hilbert Spaces.
CycleUser