Temporal Difference Learning • Part 1 of 3
📝Draft

n-step TD

Looking further ahead before bootstrapping

In TD(0), the target for updating V(St)V(S_t) was one real reward plus a bootstrap: Rt+1+γV(St+1)R_{t+1} + \gamma V(S_{t+1}). In Monte Carlo, the target was the complete return—every reward until the episode ends. Nobody said you have to choose one of those two. You can collect two real rewards before bootstrapping. Or three. Or ten.

That single observation gives us n-step TD, and with it, a tunable dial between TD and MC:

1-step (TD(0))
R₁ + γV
2-step
R₁ + γR₂ + γ²V
n-step
R₁ + ⋯ + γⁿ⁻¹Rₙ + γⁿV
∞-step (MC)
R₁ + γR₂ + ⋯ + γᵀ⁻¹Rₜ

Each card uses more real reward and less estimated value. By the end of this page you’ll know how to implement any point on this dial—and you’ll see measured evidence that the middle of the dial learns fastest.

The n-step Return

Think of the value target as a relay race between experience and estimation. Experience runs the first leg: it contributes nn real, observed rewards. Then estimation takes the baton: the value estimate of the state you reached summarizes everything after that.

  • With n=1n = 1, estimation carries almost the whole race—fast to compute, but you inherit whatever errors your estimates have.
  • With nn large, experience carries almost everything—no estimation errors, but you’re summing many noisy rewards and waiting many steps.

The n-step return is simply “n legs of experience, then hand off to the estimate.”

📖n-step Return

The n-step return Gt:t+nG_{t:t+n} is the sum of the next nn discounted rewards, plus the discounted value estimate of the state reached after those nn steps. It becomes the update target for V(St)V(S_t) in n-step TD.

Mathematical Details

Recall the book’s return convention: Gt=Rt+1+γRt+2+γ2Rt+3+G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \cdots, where Rt+1R_{t+1} is the reward for the transition out of StS_t. Truncating after nn rewards and correcting with a value estimate gives:

Gt:t+n=Rt+1+γRt+2++γn1Rt+n+γnV(St+n)G_{t:t+n} = R_{t+1} + \gamma R_{t+2} + \cdots + \gamma^{n-1} R_{t+n} + \gamma^n V(S_{t+n})

Boundary conventions:

  • If the episode terminates before nn steps are available (t+nTt + n \geq T), the n-step return is just the ordinary full return: Gt:t+n=GtG_{t:t+n} = G_t. There’s no state left to bootstrap from.
  • n=1n = 1 recovers the TD(0) target: Gt:t+1=Rt+1+γV(St+1)G_{t:t+1} = R_{t+1} + \gamma V(S_{t+1}).
  • nTtn \geq T - t for every tt recovers Monte Carlo: every target is a full return.

The n-step TD update then looks exactly like every update you’ve seen:

V(St)V(St)+α[Gt:t+nV(St)]V(S_t) \leftarrow V(S_t) + \alpha \left[ G_{t:t+n} - V(S_t) \right]

One theoretical reassurance, stated without proof: n-step returns satisfy an error-reduction property. The worst-case error of the expected n-step return, relative to the true VπV^\pi, is at most γn\gamma^n times the worst-case error of the current estimates. Because γnγ<1\gamma^n \leq \gamma < 1 (and shrinks as nn grows), updating toward n-step returns pulls estimates toward the truth—this is the core of the convergence argument for all n-step methods.

📌Example

A 3-step return, by hand

An agent experiences: S0S1S2S3S_0 \to S_1 \to S_2 \to S_3 with rewards R1=1R_1 = 1, R2=0R_2 = 0, R3=2R_3 = 2. Suppose γ=0.9\gamma = 0.9 and the current estimate is V(S3)=10V(S_3) = 10.

The 3-step return for S0S_0:

  • Real rewards: 1+0.9×0+0.81×2=2.621 + 0.9 \times 0 + 0.81 \times 2 = 2.62
  • Bootstrap: 0.93×V(S3)=0.729×10=7.290.9^3 \times V(S_3) = 0.729 \times 10 = 7.29
  • Total: G0:3=9.91G_{0:3} = 9.91

Compare the 1-step return from the same position: R1+γV(S1)R_1 + \gamma V(S_1)—one real reward and heavy reliance on the estimate V(S1)V(S_1). The 3-step return replaced two steps of estimation with two steps of reality.

The Algorithm: Learning n Steps Behind

There’s one wrinkle in turning this into an algorithm: at time tt, the n-step return for StS_t isn’t computable yet—you have to wait until time t+nt + n to have seen the nn rewards. So n-step TD always updates the state visited nn steps ago. When the episode ends, the last few states get their (shortened) updates in a final flush.

Algorithm: n-step TD for Estimating V^π
────────────────────────────────────────
Input: policy π, step size α, discount γ, steps n
Initialize V(s) arbitrarily

Loop for each episode:
    Store S_0; T ← ∞
    For t = 0, 1, 2, ...:
        If t < T:
            Take action from π, observe and store R_{t+1}, S_{t+1}
            If S_{t+1} is terminal: T ← t + 1
        τ ← t − n + 1        (τ is the time whose state gets updated)
        If τ ≥ 0:
            G ← sum of γ^(i−τ−1) R_i  for i = τ+1 ... min(τ+n, T)
            If τ + n < T:  G ← G + γ^n V(S_{τ+n})
            V(S_τ) ← V(S_τ) + α [G − V(S_τ)]
    Until τ = T − 1
ℹ️Note

The update lag is the price of looking ahead: n-step TD is still fully online and incremental, but each state’s update arrives nn steps late (and requires buffering the last nn rewards and states). TD(0) has zero lag; Monte Carlo’s “lag” is the whole episode. This tradeoff—target quality versus update delay—shows up again when we fix it elegantly with eligibility traces.

</>Implementation

Here is a complete, runnable implementation—the same code used for the experiment below. First the environment: a 19-state random walk, the big sibling of the 5-state walk from TD(0) Prediction.

import numpy as np

# 19-state random walk (Sutton & Barto, Fig 7.2 setup):
# non-terminal states 1..19, terminals at 0 and 20, start at 10.
# Stepping into 0 gives reward -1; into 20 gives +1; else 0. gamma = 1.
N, START, GAMMA = 19, 10, 1.0
TRUE_V = np.arange(-20, 22, 2) / 20.0   # true values: v(s) = s/10 - 1
TRUE_V[0] = TRUE_V[20] = 0.0            # terminals

def gen_episode(rng):
    """Return (states, rewards); rewards[t] is for states[t] -> states[t+1]."""
    states, rewards, s = [START], [], START
    while 0 < s < 20:
        s2 = s + (1 if rng.random() < 0.5 else -1)
        rewards.append(1.0 if s2 == 20 else (-1.0 if s2 == 0 else 0.0))
        states.append(s2)
        s = s2
    return states, rewards

And n-step TD prediction. Because the policy here is fixed (pure chance), we can generate the episode first and then apply the updates in the same order the online algorithm would—time τ order, always bootstrapping from the current values:

def nstep_td_run(n, alpha, episodes, rng):
    """Online n-step TD prediction. Returns RMS error after each episode."""
    V = np.zeros(21)
    errs = []
    for _ in range(episodes):
        states, rewards = gen_episode(rng)
        T = len(rewards)
        for tau in range(T):                    # update time for S_tau
            end = min(tau + n, T)
            G = sum(GAMMA ** (k - tau) * rewards[k] for k in range(tau, end))
            if tau + n < T:                     # bootstrap unless past the end
                G += GAMMA ** n * V[states[tau + n]]
            V[states[tau]] += alpha * (G - V[states[tau]])
        errs.append(np.sqrt(np.mean((V[1:20] - TRUE_V[1:20]) ** 2)))
    return errs

Note the two conventions in code form: the bootstrap is skipped when τ+nT\tau + n \geq T (the return is then the plain full return), and terminal states carry value 0.

The Experiment: Which n Wins?

Time to settle it with data. The task: estimate the value function of the 19-state random walk under the random policy, starting from V=0V = 0 everywhere.

19
States (start at 10)
±1
Terminal rewards
101
Avg episode steps (measured)
100
Runs averaged

Why 19 states instead of 5? Room to maneuver. With longer episodes (about 101 steps on average, measured over 20,000 episodes), there’s space for multi-step returns to show their strength—and for very deep returns to show their weakness.

Protocol (following Sutton & Barto’s Figure 7.2): for each n{1,2,4,8,16,32}n \in \{1, 2, 4, 8, 16, 32\} and each α{0.02,0.04,,1.0}\alpha \in \{0.02, 0.04, \ldots, 1.0\}, run 10 episodes, compute the RMS error between VV and the true values (averaged over the 19 states and the 10 episodes), and average that over 100 independent runs. Identical episode sequences are used for every (n,α)(n, \alpha) pair, so differences are purely algorithmic. The measured results:

nBest αMin avg RMS error
10.820.348
20.580.280
40.380.267
80.240.281
160.140.313
320.080.361
Best achievable RMS error by n (lower is better, measured)
n=1
0.348
n=2
0.280
n=4
0.267 ★
n=8
0.281
n=16
0.313
n=32
0.361
Each bar: minimum over the α sweep of avg RMS error (19 states, first 10 episodes, 100 runs).

The shape is a clean U: error falls from n=1n = 1 to n=4n = 4, then climbs again. The intermediate methods—neither TD(0) nor Monte Carlo—learn fastest. Exactly the result Sutton & Barto report, reproduced here from scratch.

Two further patterns in the measured data are worth noticing:

The best step size shrinks as n grows. Best α drops from 0.82 at n=1n = 1 to 0.08 at n=32n = 32. That’s the variance talking: a 32-step return is a sum of many random rewards, so each individual target is noisy, and the safe response is to take smaller steps and average over more of them. A low-variance 1-step target can be trusted with big steps.

Each n fails differently. Look at the full sweep: at α=0.1\alpha = 0.1, deep returns are fine (n=16n = 16 scores 0.324) while n=1n = 1 lags at 0.506—with small steps, TD(0)‘s bias fades too slowly over just 10 episodes. At α=1.0\alpha = 1.0, the ranking flips: n=1n = 1 scores 0.390 while n=32n = 32 blows up to 0.711—big steps amplify the deep returns’ noise. The intermediate n’s are the only ones that are decent everywhere, which is why they win the overall sweep.

nα = 0.1α = 0.2α = 0.4α = 0.6α = 0.8α = 1.0
10.5060.4710.4130.3690.3480.390
20.4700.4070.3130.2800.3200.442
40.4130.3250.2680.3120.3900.515
80.3490.2830.3220.3980.4790.586
160.3240.3270.4170.4950.5640.643
320.3640.4230.5350.6070.6600.711

n-step SARSA: The Same Dial for Control

Everything above was prediction. To get a control method, do exactly what SARSA did to TD(0): switch from state values to action values and follow an ε-greedy policy. The n-step return simply ends in Q(St+n,At+n)Q(S_{t+n}, A_{t+n}) instead of V(St+n)V(S_{t+n}), and the update targets Q(Sτ,Aτ)Q(S_\tau, A_\tau)—everything else, including the n-step lag and the end-of-episode flush, carries over unchanged. Deeper backups pay off in control for the same reason they did in prediction, and often more visibly: when rewards are sparse, a single successful episode updates the last nn state-action pairs along the path instead of only the final one, so good news spreads nn times faster through the Q-table.

</>Implementation
def nstep_sarsa_episode(env, Q, n, alpha=0.1, gamma=1.0, eps=0.1, rng=None):
    """One episode of on-policy n-step SARSA (tabular)."""
    rng = rng or np.random.default_rng()

    def eps_greedy(s):
        if rng.random() < eps:
            return rng.integers(len(Q[s]))
        return int(np.argmax(Q[s]))

    s = env.reset()
    a = eps_greedy(s)
    S, A, R = [s], [a], [0.0]        # R[0] unused; R[t+1] rewards transition t
    T, t = float('inf'), 0
    while True:
        if t < T:
            s2, r, done = env.step(A[t])
            S.append(s2); R.append(r)
            if done:
                T = t + 1
            else:
                A.append(eps_greedy(s2))
        tau = t - n + 1
        if tau >= 0:
            end = int(min(tau + n, T))
            G = sum(gamma ** (i - tau - 1) * R[i] for i in range(tau + 1, end + 1))
            if tau + n < T:
                G += gamma ** n * Q[S[tau + n]][A[tau + n]]
            Q[S[tau]][A[tau]] += alpha * (G - Q[S[tau]][A[tau]])
        if tau == T - 1:
            break
        t += 1

Try this on CliffWalking from the SARSA vs Q-learning comparison with n{1,4,8}n \in \{1, 4, 8\}—the multi-step versions typically discover the safe path in noticeably fewer episodes.

🔍Deep Dive

What about off-policy n-step control? Q-learning’s trick—bootstrapping from maxaQ\max_a Q—only “corrects” the final step of the target. With an n-step return, the intermediate n1n - 1 actions were chosen by the behavior policy, and if that differs from the target policy, the multi-step return is answering the wrong question. Fixing this properly needs importance sampling ratios (reweighting by how likely the target policy would have been to take those actions) or smarter constructions like tree-backup updates that bootstrap over the un-taken actions at every step. We’ll flag this issue again in the unifying view—modern deep RL systems often just use small n and ignore the mismatch.

Summary

The n-step return Gt:t+nG_{t:t+n} generalizes both of our target constructions: nn real rewards, then a bootstrap. Measured on the 19-state random walk, intermediate depths win—n=4n = 4 achieved RMS error 0.267 versus 0.348 for TD(0) and 0.361 for the deepest setting tested—because moderate n dilutes bootstrap bias without absorbing the full variance of a long return. The same dial converts to control as n-step SARSA.

But n-step methods force a discrete choice: one n for every state, every update, plus an n-step buffer and update lag. The next section asks a better question: why pick one n at all? Averaging all n-step returns—with a clever incremental implementation—gives us TD(λ) and eligibility traces.