← Tensor by Tensor
§ Math Fundamentals for AI
Tensor by Tensor · Complete Guide

Moving averages

Fourteen lessons that connect high-school mathematics to AI. Start with the plain-language explanation and a small example. Then read the full formulas, derivations, and Python code at your own pace.

How to study a lesson

Read the opening example. Try the question before revealing its answer. Then work through the detailed notes below it. You do not need to understand every proof on the first reading.

Symbols: a subscript such as xi identifies one entry. Σ means add a collection of terms. ∈ means “belongs to”. The symbol ≈ means “approximately equal”, not exactly equal. A parameter is an adjustable number in a model.

01.14 · Moving Averages (EMA)

Introduction

A noisy measurement sequence can hide its direction. For values 8, 4, 6, and 2, exponential averages with decay 0.5 and 0.9 react at different speeds, making the smoothing trade-off visible.

Learning goal

Calculate simple and exponential moving averages by hand, including effective windows and early-step bias correction.

Before you start

Weighted averages, sequences, multiplication, subtraction, and a simple Python loop.

Lesson plan

  1. Average a fixed window and identify the storage and delay it introduces.
  2. Update an exponential average recursively and compare two decay values.
  3. Explain effective window, bias correction, optimizer statistics, and model-weight averaging.

Calculate one update. With old average 10, new value 14, and decay 0.75, the exponential moving average becomes 0.75×10 + 0.25×14 = 11. Larger decay keeps more history and reacts more slowly.

Smoothing noisy sequences

During training, loss values, gradients, and metrics fluctuate. Moving averages smooth these sequences to reveal underlying trends.

Simple moving average (SMA)

Average the last $k$ values:

$$\text{SMA}_t = \frac{1}{k}\sum_{i=t-k+1}^{t} x_i$$

When a new value arrives, drop the oldest and add the newest.

Exponential moving average (EMA)

The EMA keeps a single running estimate, blending the old estimate with the new observation:

Definition · EMA
$$v_t = \beta v_{t-1} + (1-\beta) x_t, \qquad 0 \leq \beta < 1$$

$\beta$ controls the smoothing: higher $\beta$ = more smoothing, slower reaction to changes.

Example · $\beta = 0.9$ vs $\beta = 0.5$

Starting with $v_0 = 8$, observations $[4, 6, 2]$:

Observation$\beta=0.5$$\beta=0.9$
8 (init)8.0008.000
46.0007.600
66.0007.440
24.0006.896

$\beta=0.9$ reacts more slowly — it retains more of the past.

Effective window size

An EMA with decay $\beta$ behaves roughly like a simple average over the last $\frac{1}{1-\beta}$ observations:

  • $\beta = 0.9 \implies$ effective window $\approx 10$
  • $\beta = 0.99 \implies$ effective window $\approx 100$
  • $\beta = 0.999 \implies$ effective window $\approx 1000$

Bias correction

When initialised at $v_0 = 0$, the first few estimates are biased toward zero. The correction divides by $1 - \beta^t$:

$$\hat{v}_t = \frac{v_t}{1 - \beta^t}$$

At $t=1$ with $\beta=0.9$: $\hat{v}_1 = v_1 / 0.1 = 10 v_1$. As $t \to \infty$, $\beta^t \to 0$ and the correction vanishes.

Initialisation matters

If you initialise with the first observation ($v_0 = x_0$), do not apply bias correction — it's only needed for zero-initialisation.

EMA in optimisers

EMAs are the core mechanism of modern optimisers:

OptimiserWhat it averages
SGD + MomentumEMA of gradients ($m_t$)
RMSPropEMA of squared gradients ($v_t$)
AdamEMA of gradients ($m_t$) and squared gradients ($v_t$), both bias-corrected
AdamWAdam + decoupled weight decay

EMA for model weights

Averaging the model weights themselves over training often improves generalisation:

$$\bar{\theta}_t = \beta \bar{\theta}_{t-1} + (1-\beta)\theta_t$$

EMA gives recent weights more influence. It differs from standard Stochastic Weight Averaging (SWA), which equally averages selected training checkpoints. Evaluate the averaged weights before choosing them for inference; averaging is not a guarantee of better accuracy.

Implementation

class EMA:
    def __init__(self, beta=0.999):
        self.beta = beta
        self.v = None
        self.t = 0
    
    def update(self, x):
        self.t += 1
        if self.v is None:
            self.v = x  # initialise with first observation
        else:
            self.v = self.beta * self.v + (1 - self.beta) * x
        # Bias correction (only if zero-initialised)
        return self.v  # or self.v / (1 - self.beta**self.t)

# Usage: smoothing training loss
ema = EMA(beta=0.99)
for loss in training_losses:
    smoothed = ema.update(loss)