Intellectual and Cognitive History › A Small Experiment: What a Learned Model Buys You

Michael BrenndoerferJune 5, 202610 min read
World Models HandbookAll chapters
Part 1 of 12The World-Model Idea
Chapter 5Intellectual and Cognitive History › A Small Experiment: What a Learned Model Buys You
Reading section 8 of 13 · A Small Experiment: What a Learned Model Buys You · 10 minSection 8 of 13 · 10 min

Book contents

Section 8 of 13 · 10 min read

Intellectual and Cognitive History › A Small Experiment: What a Learned Model Buys YouOne section · Sections in this chapter8 / 13 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

History makes claims; let's test one of them. Dyna argues that a learned model can extract more updates from each piece of real experience. We test that claim in a small NumPy gridworld by comparing Q-learning with Dyna under the same learning rule and hyperparameters. This is a Dyna experiment, not a miniature DreamerV3: Dyna interleaves replayed model transitions with direct updates, whereas DreamerV3 trains policies through imagined trajectories inside a learned latent model. The loose lineage is their shared use of learned dynamics to turn real experience into additional learning signal. Planning consumes extra random draws, so paired seeds do not produce identical trajectories; averaging twenty independent seeds reduces that sampling noise while leaving planning steps as the systematic treatment difference.

The environment is an obstacle-free 5×55 \times 5 grid. The agent starts in the top-left corner and must reach the goal in the bottom-right. Each non-goal step costs −1-1 reward; the transition entering the goal receives +20+20 instead of the step cost and ends the episode. With probability 0.10.1 the environment replaces the commanded action with one sampled uniformly from all four actions. The commanded action therefore still executes with probability 0.9+0.1/4=0.9250.9 + 0.1/4 = 0.925. The optimal no-slip episode return is 20−7=1320 - 7 = 13, since the shortest path takes eight actions and only the first seven receive −1-1. The executed display below is the canonical record of the twenty-seed outcomes.

First, the environment:

In[7]:
Code
import numpy as np

GRID = 5
START = (0, 0)
GOAL = (4, 4)
ACTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]  # up, down, left, right
SLIP = 0.1


def env_step(state, action, rng):
    """One step of the true environment. Returns (next_state, reward)."""
    if rng.random() < SLIP:
        action = ACTIONS[rng.integers(len(ACTIONS))]
    r = int(np.clip(state[0] + action[0], 0, GRID - 1))
    c = int(np.clip(state[1] + action[1], 0, GRID - 1))
    next_state = (r, c)
    reward = 20.0 if next_state == GOAL else -1.0
    return next_state, reward

Note the structure of env_step: it is a forward model of a tiny world, written by hand. The agent below does not inspect its code or constants; by experimental convention it receives only the sampled (next_state, reward) outputs and learns a separate empirical model from them. The clipping at the grid edges plays the role of boundary conditions in a physical model. The slip branch is a stochastic disturbance relative to a naive deterministic model, but repeated observations let the empirical model below recover its outcome frequencies.

Next, the agent itself. It is one function with a single switch: planning_steps controls how many Dyna-style simulated updates run between real steps. Setting it to 00 gives pure model-free Q-learning; setting it to 1010 gives Dyna. Both conditions use α=0.3\alpha=0.3, γ=0.95\gamma=0.95, ϵ=0.05\epsilon=0.05, 3,000 real interactions, and a 60-step episode limit. Returns plotted below are undiscounted episode sums even though Q-learning discounts future rewards. A timeout resets the episode but is bootstrapped as a time-limit truncation rather than treated as a terminal transition; goal transitions alone use zero bootstrap.

In[8]:
Code
def run_agent(rng, planning_steps=0, n_steps=3000, timeout=60):
    """Q-learning, optionally interleaved with Dyna planning updates."""
    Q = np.zeros((GRID, GRID, len(ACTIONS)))
    model = {}  # (state, action) -> list of observed (next_state, reward)
    alpha, gamma, eps = 0.3, 0.95, 0.05
    state, ep_return, ep_len = START, 0.0, 0
    curve, total_steps = [], 0

    while total_steps < n_steps:
        if rng.random() < eps:
            a = int(rng.integers(len(ACTIONS)))
        else:
            a = int(np.argmax(Q[state]))

        ns, r = env_step(state, ACTIONS[a], rng)
        boot = 0.0 if ns == GOAL else gamma * Q[ns].max()
        Q[state][a] += alpha * (r + boot - Q[state][a])

        # Model learning: store the real transition
        model.setdefault((state, a), []).append((ns, r))
        ep_return += r
        ep_len += 1
        total_steps += 1

        # Planning: replay sampled transitions from the learned model
        for _ in range(planning_steps):
            keys = list(model.keys())
            ps, pa = keys[int(rng.integers(len(keys)))]
            outcomes = model[(ps, pa)]
            pns, pr = outcomes[int(rng.integers(len(outcomes)))]
            pboot = 0.0 if pns == GOAL else gamma * Q[pns].max()
            Q[ps][pa] += alpha * (pr + pboot - Q[ps][pa])

        state = START if (ns == GOAL or ep_len >= timeout) else ns
        if ns == GOAL or ep_len >= timeout:
            curve.append((total_steps, ep_return))
            ep_return, ep_len = 0.0, 0
    # Include a final time-limit truncation so interpolation never has to
    # extrapolate or flat-clamp at the right edge of the reporting grid.
    if not curve or curve[-1][0] < total_steps:
        curve.append((total_steps, ep_return))
    return curve

Three details deserve your attention, because each is a miniature version of a theme from this chapter:

  • The model is an empirical one: for each state-action pair it stores a list of observed outcomes and samples from that list during planning. With enough visits, those frequencies converge toward the environment's slip distribution. This is system identification in its crudest form, a topic Part II develops properly. The model cannot invent an outcome absent from its observations, which prevents one kind of optimistic hallucination but leaves it ignorant about unseen state-action pairs.
  • Planning updates are identical in form to direct updates. In the code, the planning loop uses the same Q-learning equation but draws the transition from model instead of env_step. The update rule does not encode the source; the caller determines whether a sample is real or simulated.
  • The agent never executes a long open-loop plan. Its "plan" is implicit in the value function that planning has improved. At every step it acts epsilon-greedily with respect to current values, then incorporates the newest observation. This resembles the closed-loop humility of receding-horizon control in spirit, but it is not MPC: there is no online trajectory optimization or finite-horizon action sequence. The model shapes the values, the values shape the action, and reality gets a vote at every step.

Now we run the experiment: pure Q-learning versus Dyna with 10 planning steps per real step, averaged over 20 random seeds. This is a closed-loop learning comparison measured in real environment interactions, not an open-loop rollout-accuracy test. The plotted returns come from each agent's online epsilon-greedy behavior policy, including its 0.05 exploration probability; they are not separate greedy-policy evaluations. To compare agents that finish episodes at different times, we interpolate each seed's episode returns onto a common grid spaced every 50 environment steps. Threshold crossings are therefore quantized to that grid. If a run ends midway through an episode at step 3,000, the right-edge record is that time-limit-truncated partial return rather than a completed episode. We report means without uncertainty bands because this is a compact demonstration rather than a statistical benchmark.

In[9]:
Code
N_SEEDS = 20
EXPERIMENT_SEEDS = tuple(range(N_SEEDS))
STEP_GRID = np.arange(100, 3000, 50)


def average_curve(planning_steps):
    curves = []
    for seed in EXPERIMENT_SEEDS:
        rng = np.random.default_rng(seed)
        xs, ys = zip(*run_agent(rng, planning_steps=planning_steps))
        assert xs[0] <= STEP_GRID[0] and xs[-1] >= STEP_GRID[-1], (
            "The reporting grid must remain inside the recorded data range"
        )
        curves.append(np.interp(STEP_GRID, xs, ys))
    return np.mean(curves, axis=0)


q_curve = average_curve(planning_steps=0)
dyna_curve = average_curve(planning_steps=10)

## Sample efficiency: environment steps needed to reach a return of 7.0
THRESHOLD = 7.0


def steps_to_threshold(curve):
    reached = np.nonzero(curve >= THRESHOLD)[0]
    return int(STEP_GRID[reached[0]]) if len(reached) else None


q_steps = steps_to_threshold(q_curve)
dyna_steps = steps_to_threshold(dyna_curve)

## Sweep planning depth to see diminishing returns
SWEEP = [0, 1, 5, 10, 50]
sweep_steps = [steps_to_threshold(average_curve(p)) for p in SWEEP]

## Acceptance criteria test the instructional comparison without freezing
## stochastic outputs into prose. The following display cell remains the
## canonical record of exact values for the locked environment.
assert np.interp(500, STEP_GRID, dyna_curve) > np.interp(
    500, STEP_GRID, q_curve
)
assert dyna_steps is not None and q_steps is not None and dyna_steps < q_steps
assert all(step is not None for step in sweep_steps), (
    "Every planning-depth condition must reach the plotted threshold"
)
assert sweep_steps[2] <= sweep_steps[0]
Out[10]:
Console
Return after 500 real steps   Q-learning:   7.36   Dyna:  11.24
Return after 2950 real steps  Q-learning:  11.55   Dyna:  11.52
Real steps to return >= 7.0  Q-learning: 500   Dyna: 200
Planning sweep (planning steps -> real steps to threshold):
    0 planning steps/real step -> 500
    1 planning steps/real step -> 300
    5 planning steps/real step -> 200
   10 planning steps/real step -> 200
   50 planning steps/real step -> 150
NumPy version used for this execution: 2.4.2

The execution output above makes the comparison concrete without duplicating stochastic decimals in prose. For the declared seeds, Dyna learns faster in real-interaction units and reaches the displayed threshold before pure Q-learning. Ten planning steps mean eleven Q-updates per real step, one direct and ten replayed. The planning-depth sweep shows where measured gains occur in this execution; its printed values and bar labels, rather than a frozen prose pattern, are the canonical record. Additional replay cannot compensate for state-action pairs the empirical model has not yet observed, but this experiment does not isolate coverage from exploration, learning rate, grid quantization, or performance-ceiling effects. The no-slip optimum is 13, while slip, persistent epsilon-greedy exploration, and residual value error help explain the lower online returns. Exact returns, threshold crossings, sweep values, and the active NumPy version are recomputed deterministically from EXPERIMENT_SEEDS during conversion. Executable acceptance criteria require Dyna to lead at 500 steps, cross the threshold earlier, and produce a threshold crossing for every plotted sweep condition.

Now the learning curves:

Out[11]:
Visualization
Learning curves for Q-learning and Dyna, with the Dyna curve reaching the horizontal return threshold earlier.
Mean online epsilon-greedy episode return versus real environment steps for pure Q-learning and Dyna with 10 planning steps per real step, averaged over the 20 declared seeds. In this deterministic execution, Dyna reaches the displayed threshold with fewer real interactions. Seed variability is not shown.

And the planning-depth sweep:

Out[12]:
Visualization
Five labeled bars compare grid-quantized real steps to the return threshold for zero, one, five, ten, and fifty planning updates.
Real environment steps needed to reach a mean online return of 7.0 as a function of planning steps per real step, averaged over the 20 declared seeds and measured on a 50-step grid. Bar labels are the canonical values for this deterministic execution; differences of one bar-label interval are at the reporting resolution.

Read these two figures together as a small illustration of the computation-for-experience trade. Simulated transitions accelerate learning in this gridworld under the declared seeds, while the labeled sweep shows how the measured reduction in real interactions varies with planning depth. Differences of one reporting interval should not be read as a general law because the threshold is sampled only every 50 steps. Planning cannot add observations that the model has never received, but the sweep alone does not prove that coverage is the active bottleneck.