拆解指数加权平均:5 分钟看懂机器学习的 “数据平滑神器”

拆解指数加权平均:5 分钟看懂机器学习的 "数据平滑神器"

Ming | 2025.12


什么是指数加权平均?简单来说,它是一种用于将序列数据变得更加平滑的经典方法。在机器学习中,我们经常会遇到来自传感器、实验或训练过程的噪声数据,这些数据波动很大,不容易直接观察趋势。指数加权平均就像一个"数据平滑神器",能够有效过滤短期波动,突出长期趋势,帮助我们更好地理解数据变化。

例如,下面是一个带有噪声的原始序列:

在使用不同平滑参数 β 的指数加权平均后,我们可以得到平滑程度各异的序列:

如果是了解过股票交易的朋友,应该对"移动平均线"(Moving Average)这个概念非常熟悉。它常被用于观察股价的趋势,帮助投资者过滤短期市场的"噪声",把握中长期的走势方向。其实,指数加权平均正是股票技术分析中"指数移动平均线"(Exponential Moving Average, EMA)的核心计算方法。

指数加权平均的实现非常简单,其本质是对历史数据进行加权组合,且权重随时间指数衰减。

假设我们有一个原始序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> a 1 , a 2 , ... , a n a_1, a_2, ..., a_n </math>a1,a2,...,an,我们希望得到一个平滑后的序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 1 , b 2 , ... , b n b_1, b_2, ..., b_n </math>b1,b2,...,bn。

通常我们令初始平滑值 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 0 = a 0 b_0 = a_0 </math>b0=a0,然后从 <math xmlns="http://www.w3.org/1998/Math/MathML"> n = 1 n=1 </math>n=1 开始迭代计算:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> b n = β b n − 1 + ( 1 − β ) a n b_{n} = \beta b_{n-1} +(1-\beta)a_{n} </math>bn=βbn−1+(1−β)an

其中, <math xmlns="http://www.w3.org/1998/Math/MathML"> β \beta </math>β 是一个介于 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 之间的平滑因子。β 越大,曲线越平滑,但也会对最新数据的反应变得"迟钝";β 越小,平滑后的曲线越接近原始数据,保留的细节也越多。公式可以直观理解为:每一步的平滑值 <math xmlns="http://www.w3.org/1998/Math/MathML"> b n b_n </math>bn 是上一时刻平滑值 <math xmlns="http://www.w3.org/1998/Math/MathML"> b n − 1 b_{n-1} </math>bn−1 与当前观测值 <math xmlns="http://www.w3.org/1998/Math/MathML"> a n a_n </math>an 的加权平均。

假设我们有一个长度为 5 的序列:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> a = [ 10 , 8 , 12 , 9 , 11 ] a=[10,8,12,9,11] </math>a=[10,8,12,9,11]

并取 <math xmlns="http://www.w3.org/1998/Math/MathML"> β = 0.8 \beta = 0.8 </math>β=0.8,初始值 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 0 = a 0 = 10 b_0 = a_0 = 10 </math>b0=a0=10。我们一步步来计算 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 1 b_1 </math>b1 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 5 b_5 </math>b5:

  1. <math xmlns="http://www.w3.org/1998/Math/MathML"> b 1 = 0.8 × 10 + 0.2 × 8 = 8 + 1.6 = 9.6 b_1 = 0.8 \times 10 + 0.2 \times 8 = 8 + 1.6 = 9.6 </math>b1=0.8×10+0.2×8=8+1.6=9.6
  2. <math xmlns="http://www.w3.org/1998/Math/MathML"> b 2 = 0.8 × 9.6 + 0.2 × 12 = 7.68 + 2.4 = 10.08 b_2 = 0.8 \times 9.6 + 0.2 \times 12 = 7.68 + 2.4 = 10.08 </math>b2=0.8×9.6+0.2×12=7.68+2.4=10.08
  3. <math xmlns="http://www.w3.org/1998/Math/MathML"> b 3 = 0.8 × 10.08 + 0.2 × 9 = 8.064 + 1.8 = 9.864 b_3 = 0.8 \times 10.08 + 0.2 \times 9 = 8.064 + 1.8 = 9.864 </math>b3=0.8×10.08+0.2×9=8.064+1.8=9.864
  4. <math xmlns="http://www.w3.org/1998/Math/MathML"> b 4 = 0.8 × 9.864 + 0.2 × 11 = 7.8912 + 2.2 = 10.0912 b_4 = 0.8 \times 9.864 + 0.2 \times 11 = 7.8912 + 2.2 = 10.0912 </math>b4=0.8×9.864+0.2×11=7.8912+2.2=10.0912

最终平滑后的序列约为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> b = [ 9.6 , 10.08 , 9.864 , 10.0912 ] b=[9.6,10.08,9.864,10.0912] </math>b=[9.6,10.08,9.864,10.0912]

可以看出,原本波动较大的 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 8 , 12 , 9 , 11 ] [8, 12, 9, 11] </math>[8,12,9,11] 被平滑为一个变化更缓和的序列。

为什么这个公式能让一个曲线变得更加平滑呢?其实很好理解,从"惯性"的角度来看。当 <math xmlns="http://www.w3.org/1998/Math/MathML"> β \beta </math>β 很大(比如 0.9)时, <math xmlns="http://www.w3.org/1998/Math/MathML"> b n − 1 b_{n-1} </math>bn−1 在更新中占据很大比重,当前的新值 <math xmlns="http://www.w3.org/1998/Math/MathML"> a n a_n </math>an 只占很小的比例,这意味着平滑序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 具有强烈的"记忆"能力,不会因为某一点的突变而产生剧烈波动。反之,如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> β \beta </math>β 很小(比如 0.1),则平滑序列会更紧密地跟踪原始数据的变化,适应更快,但平滑效果减弱。

不知道到这里你发现没有,上面的式子虽然能很好的平滑序列,但是有一个缺点,就是在序列刚开始的时候容易"失真"。从上面的展开式也可以看出,在序列开始时,我们令 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 0 = a 0 b_0 = a_0 </math>b0=a0,由于历史数据较少, <math xmlns="http://www.w3.org/1998/Math/MathML"> b 0 b_0 </math>b0 的权重 <math xmlns="http://www.w3.org/1998/Math/MathML"> β \beta </math>β 较大,若 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 0 b_0 </math>b0 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> a 1 , a 2 , ... a_1, a_2, ... </math>a1,a2,... 差异明显,就会导致平滑序列初期出现"失真"。下面这张图就清晰展示了这个问题:

蓝色是原始曲线,红色是指数加权平均后的曲线。明显可见,红色曲线在开始阶段比蓝色高出不少,这是因为我们初始设置 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 0 = a 0 b_0 = a_0 </math>b0=a0,而 <math xmlns="http://www.w3.org/1998/Math/MathML"> a 0 a_0 </math>a0 可能恰是一个较大或较小的异常值,影响了后续若干点的平滑结果。

那么如何解决这种失真呢,方法有很多,常用的解决方法之一是使用序列前若干点的平均值作为初始值 ,而不是直接使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> a 0 a_0 </math>a0。

具体做法是:

  1. 选取一个合适的初始窗口长度 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k(例如 <math xmlns="http://www.w3.org/1998/Math/MathML"> k = 5 k=5 </math>k=5 或 <math xmlns="http://www.w3.org/1998/Math/MathML"> k = 10 k=10 </math>k=10,根据序列总体长度和噪声情况决定)。
  2. 计算前 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k 个原始数据的平均值: <math xmlns="http://www.w3.org/1998/Math/MathML"> b 0 = 1 k ∑ i = 1 k a i b_{0} = \frac{1}{k}\sum_{i=1}^{k}a_{i} </math>b0=k1∑i=1kai

这样做的优点是能够抵消个别异常点对平滑初期的影响,使平滑曲线从一开始就更贴合数据的整体趋势。一般来说, <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k 不宜过大,否则会引入不必要的滞后;如果数据在开始阶段变化剧烈,也可适当缩小 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k,让平滑过程更快适应当前趋势。


接下来,我们用Python来实际操作演示一下指数加权平均的效果。这个例子会用到NumPy和Matplotlib库,我们将一步步生成带噪声的序列,并用不同参数进行平滑处理。

首先,引入必要的库,并定义一个用于生成平滑随机序列的辅助类SmoothRandomSequence

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
import random

class SmoothRandomSequence:
    def __init__(self, low, high, initial_value=None, max_step=None):
        self.low = low
        self.high = high
        self.range_width = high - low
        if initial_value is None:
            self.current = random.uniform(low, high)
        else:
            self.current = min(high, max(low, initial_value)) 
        if max_step is None:
            self.max_step = self.range_width * 0.1  
        else:
            self.max_step = max_step
    def next(self):
        step = random.uniform(-self.max_step, self.max_step)
        next_value = self.current + step
        if next_value < self.low:
            next_value = self.low + (self.low - next_value) 
        elif next_value > self.high:
            next_value = self.high - (next_value - self.high)  
        next_value = max(self.low, min(self.high, next_value))
        self.current = next_value
        return next_value
    def generate_sequence(self, n):
        sequence = [self.current]
        for _ in range(n - 1):
            sequence.append(self.next())
        return sequence

这个SmoothRandomSequence类的具体实现细节不重要,你只需要知道它能生成一个在指定范围内波动、变化相对平滑的随机序列,模拟真实场景中带有噪声的数据。

现在,让我们使用这个类来生成一个包含300个数据点的原始序列:

python 复制代码
# 创建平滑随机数生成器实例
# 设置范围0到60,初始值20,最大步长3
generator = SmoothRandomSequence(low=0, high=60, initial_value=20, max_step=3)

# 生成300个随机数的原始序列
noise_array = np.array(generator.generate_sequence(300))

# 绘图
fig, ax = plt.subplots(figsize=(10, 3))
ax.plot(noise_array)

然后写一个名为exponential_weighted_average的函数,这个函数需要传入一个原始numpy序列,beta值和初始窗口大小 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k就可以返回平滑后的序列,返回的序列类型也是numpy序列

python 复制代码
def exponential_weighted_average(sequence, beta, initial_window=None):
    """
    参数:
    sequence: numpy数组,原始序列
    beta: 平滑因子,取值范围0~1
    initial_window: 用于计算初始平均值的窗口大小

    返回:
    平滑后的序列,长度与原始序列相同
    """
    n = len(sequence)
    smoothed = np.zeros(n)

    # 计算初始值
    if initial_window is not None and initial_window > 0:
        k = min(initial_window, n)
        smoothed[0] = np.mean(sequence[:k])
    else:
        smoothed[0] = sequence[0]

    # 迭代计算指数加权平均
    for i in range(1, n):
        smoothed[i] = beta * smoothed[i - 1] + (1 - beta) * sequence[i]

    return smoothed

最后,我们使用上面定义的exponential_weighted_average函数,来计算并且绘制三种不同 <math xmlns="http://www.w3.org/1998/Math/MathML"> β \beta </math>β下的平滑后的曲线

python 复制代码
fig, ax = plt.subplots(figsize=(12, 3))

# 计算三种不同beta下的平滑结果
array1 = exponential_weighted_average(noise_array,beta = 0.95,initial_window=4)
array2 = exponential_weighted_average(noise_array,beta = 0.9,initial_window=4)
array3 = exponential_weighted_average(noise_array,beta = 0.8,initial_window=4)

# 绘制
ax.plot(noise_array)
ax.plot(array1, label=r"$\beta$=0.95")
ax.plot(array2, label=r"$\beta$=0.9")
ax.plot(array3, label=r"$\beta$=0.8")
ax.legend()
相关推荐
SelectDB1 小时前
慢 SQL 诊断准确率 99.99%,天翼云基于 Apache Doris MCP 的 AI 智能运维实践
数据库·人工智能·apache
王中阳Go2 小时前
05 Go Eino AI应用开发实战 | Docker 部署指南
人工智能·后端·go
腾讯云开发者2 小时前
当10年架构师拿起AI:不是写不动了,是写得太快了
人工智能
小马过河R2 小时前
RAG检索增强生成:通过重排序提升AI信息检索精准度
人工智能·语言模型
不惑_2 小时前
通俗理解卷积神经网络
人工智能·windows·python·深度学习·机器学习
rayufo2 小时前
自定义数据在深度学习中的应用方法
人工智能·深度学习
梦帮科技2 小时前
量子计算+AI:下一代智能的终极形态?(第一部分)
人工智能·python·神经网络·深度优先·量子计算·模拟退火算法
小兔崽子去哪了2 小时前
机器学习 线性回归
后端·python·机器学习
山海青风2 小时前
藏文TTS介绍:6 MMS 项目的多语言 TTS
人工智能·python·神经网络·音视频