Skip to content
intermediate

Compare Memoryless and Recurrent Policies in a Partially Observed Task

A memoryless agent can learn perfectly and still lose. It extracts every drop of signal from the observation it receives, then plateaus at a score a coin…

Published 2026-10-02Updated 2026-10-0410 min read
Front view of a classic blue sports car in an open field under a dramatic cloudy sky.
Front view of a classic blue sports car in an open field under a dramatic cloudy sky. Photo by Mike van Schoonderwalt on Pexels.

A memoryless agent can learn perfectly and still lose. It extracts every drop of signal from the observation it receives, then plateaus at a score a coin flip could match — not because training failed, but because the input never contained the answer.

This is a controlled experiment. One small task, two policies, identical training budget, one changed variable: whether the policy can see history. By the end you will have a runnable comparison, a visible performance gap, and a diagnostic habit for telling "memory would fix this" apart from "the observation design is broken."

If partial observability and recurrent policies are still fuzzy, the short version is enough to proceed: an observation that does not identify the hidden state makes the task non-Markovian over observations, so the optimal action can depend on history the agent cannot currently see. A memoryless policy is a function of the current observation alone. A history-dependent policy is a function of the current observation plus some summary of what came before. That is the entire variable we are testing.

Designing a Task Where History Actually Matters

A cue bit is shown at the first step, followed by neutral observations and a final decision. The memoryless path receives only the same final observation for either cue and cannot distinguish the correct action. The recurrent path carries a hidden state containing the cue and selects the matching action.
The task isolates memory: only a policy that carries the earlier cue can use it when the final observation is identical.

We need a task small enough to reason about by hand, where the current observation is deliberately insufficient and the required memory is short and interpretable.

Use a cue-then-act structure:

  1. At step 0, the environment reveals a hidden context — call it a cue bit, either 0 or 1.
  2. Steps 1 through delay emit a neutral observation. Nothing informative happens.
  3. At the final step, the agent must choose action 0 or action 1. It gets reward +1 only if the action matches the cue bit from step 0.

The observation at decision time is identical in both cases. The reward is sparse but deterministic. A human reading the episode description knows exactly what to do: remember the cue, act on it. The agent sees a blank wall.

Why this is a fair test:

  • The hidden variable is decision-relevant. The cue bit changes which action is correct.
  • The observation is lossy by construction. The cue is visible once and then gone.
  • The memory horizon is short. A recurrent state or a small history window can plausibly hold one bit across a handful of steps.

Now add the control. Build a fully observed version of the same task where the cue bit is included in every observation. Same reward structure, same delay, same action space. In that version, a memoryless policy should be sufficient — the current observation already contains the answer. If the memoryless policy still fails there, the problem is task difficulty, not observability. The control is what proves the gap comes from the missing information.

Note: Keep the state space tiny. Two cue values, a fixed delay, two actions. You want to be able to enumerate the correct policy in your head before you run anything.

Knowledge check

Check your understanding

Answer this question before you continue.

If the memoryless policy succeeds on the fully observed control but plateaus on the partially observed task, what does that comparison support?
Comparison Reasoning

Focus: Explain how the fully observed control distinguishes an observability problem from a policy-learning or capacity problem.

Building the Environment and the Memoryless Baseline

Dependencies are minimal: Python, NumPy, and PyTorch for the policy network. No external services, no credentials, no GPU required.

The environment is a fixed-length episode. The observation vector has two slots: a cue slot and a step counter. In the partially observed version, the cue slot is zeroed after step 0. In the control version, it stays populated.

import numpy as np
import torch
import torch.nn as nn

class CueTask:
    def __init__(self, delay=4, fully_observed=False):
        self.delay = delay
        self.fully_observed = fully_observed
        self.obs_dim = 2
        self.n_actions = 2

    def reset(self):
        self.cue = np.random.randint(2)
        self.t = 0
        return self._obs()

    def _obs(self):
        cue_slot = self.cue if (self.fully_observed or self.t == 0) else 0
        return np.array([cue_slot, self.t / self.delay], dtype=np.float32)

    def step(self, action):
        done = self.t == self.delay
        reward = 1.0 if (done and action == self.cue) else 0.0
        self.t += 1
        return self._obs(), reward, done

The memoryless policy maps the current observation to action logits — deliberately no history, no recurrence:

class MemorylessPolicy(nn.Module):
    def __init__(self, obs_dim, n_actions):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_dim, 32),
            nn.ReLU(),
            nn.Linear(32, n_actions),
        )

    def forward(self, obs):
        return self.net(obs)

Now the rollout and training loop. This is the piece the earlier draft left implicit: rollout must actually construct episodes, sample actions, and return the log-probabilities and rewards the update needs.

def rollout(policy, env, recurrent=False):
    obs = env.reset()
    h = None
    logps, rewards = [], []
    done = False
    while not done:
        obs_t = torch.tensor(obs).unsqueeze(0)
        if recurrent:
            logits, h = policy(obs_t, h)
        else:
            logits = policy(obs_t)
        dist = torch.distributions.Categorical(logits=logits)
        action = dist.sample()
        logps.append(dist.log_prob(action))
        obs, reward, done = env.step(action.item())
        rewards.append(reward)
    return logps, rewards

def train(policy, env, episodes=2000, lr=1e-2, recurrent=False):
    opt = torch.optim.Adam(policy.parameters(), lr=lr)
    returns = []
    for ep in range(episodes):
        logps, rewards = rollout(policy, env, recurrent=recurrent)
        G = sum(rewards)
        returns.append(G)
        loss = -sum(lp * G for lp in logps)
        opt.zero_grad()
        loss.backward()
        opt.step()
    return returns

Run it on the partially observed task. Expect a learning curve that rises quickly, then flattens at roughly 50% success — the return a random policy achieves. The agent is not failing to learn. It is learning the best it can from an input that does not contain the answer.

Run the same code on the fully observed control (fully_observed=True). The curve should climb toward near-perfect return. That contrast is your first piece of evidence: the plateau is caused by observability, not by the policy's capacity to learn.

Common mistake: Treating the plateau as a bug and tuning the learning rate. The agent has already extracted everything the observation contains. No hyperparameter fixes missing information.

Knowledge check

Check your understanding

Answer this question before you continue.

The memoryless policy's return plateaus near 50% in the partially observed task. Which explanation best matches the task design?
Misconception Check

Focus: Diagnose why a memoryless policy plateaus near chance on the partially observed cue task despite training.

Adding Memory: Recurrent State or Explicit History

Two ways to give the policy access to history. Both are worth understanding, but for the main runnable comparison, pick one and carry it through the same training loop.

Option A — explicit history. Concatenate the last k observations into the policy input. The memory horizon is a hyperparameter you can see and count.

class HistoryPolicy(nn.Module):
    def __init__(self, obs_dim, n_actions, k=4):
        super().__init__()
        self.k = k
        self.net = nn.Sequential(
            nn.Linear(obs_dim * k, 64),
            nn.ReLU(),
            nn.Linear(64, n_actions),
        )

    def forward(self, history):
        return self.net(history.flatten())

Simple, transparent, easy to debug. If k is at least as long as the delay, the cue bit is still in the input window at decision time. The cost is that you must maintain a rolling buffer and pad it at episode start.

Option B — recurrent state. Carry a hidden state across timesteps and feed it alongside the current observation. The policy summarizes history rather than storing it verbatim.

class RecurrentPolicy(nn.Module):
    def __init__(self, obs_dim, n_actions, hidden=32):
        super().__init__()
        self.rnn = nn.GRUCell(obs_dim, hidden)
        self.head = nn.Linear(hidden, n_actions)

    def forward(self, obs, h):
        if h is None:
            h = torch.zeros(obs.shape[0], self.rnn.hidden_size)
        h = self.rnn(obs, h)
        return self.head(h), h

The recurrent state is a compressed running summary. The policy is now a function of that summary plus the current observation. It decides what to keep and what to discard — which is exactly what you want when the relevant history is short and the irrelevant history is long.

The rollout function above already handles the recurrent path: it passes h forward across steps and resets it to None at the start of each episode. That reset is the whole reason the recurrent branch works.

Warning: The recurrent state must be reset at episode boundaries and carried correctly through the training loop. If you forget to zero h at the start of each episode, or if you let gradients flow across unrelated episodes, the memory is silently corrupted. The policy will look like it is learning and then behave inconsistently. This is the single most common implementation trap in recurrent policy training.

Knowledge check

Check your understanding

Answer this question before you continue.

A recurrent policy behaves inconsistently across episodes. Which change follows the article's recommended recurrent-state handling?
Debugging

Focus: Identify the episode-boundary and timestep handling required for recurrent policy memory.

Reading the Comparison: What the Numbers Do and Do Not Prove

Compare three things across at least five seeds: final return, learning speed, and variance.

MetricMemoryless (partial)Memory-capable (partial)Memoryless (control)
Final return~0.5~1.0~1.0
Learning speedFast plateauSlower climb, higher ceilingFast climb
Seed varianceLowModerateLow

The expected outcome: the memory-capable policy reaches the higher plateau while the memoryless baseline stays flat, and the gap is stable across seeds. A single run is an anecdote. Five seeds with overlapping confidence intervals is a result.

But the score is not the most important check. Inspect a rollout. Pull one episode from the trained memory policy and confirm its action at the final step actually depends on the cue bit from step 0. Flip the cue, re-run the episode, watch the action flip. If the action does not track the cue, the policy may be exploiting some other correlation in the environment — and you have not demonstrated memory at all.

State the limits honestly. A win here shows memory helps on this task. It does not show recurrence is universally better. It does not show the recurrent policy learned the internal representation you intended. It shows that when the observation omits decision-relevant history, a policy with access to that history can do better.

Knowledge check

Check your understanding

Answer this question before you continue.

A trained memory policy scores well. In a rollout check, you flip the initial cue while keeping the rest of the episode setup the same. What result most directly supports that its final action uses the cue?
Scenario Interpretation

Focus: Use a cue-flip rollout check to determine whether a trained memory policy's decision tracks the task-relevant history.

When Memory Does Not Help

Adding recurrence and watching nothing improve is the most common real-world outcome. Three failure modes explain most of it.

Failure mode 1 — the observation is too lossy. If no summary of history can recover the hidden state, memory cannot help. The fix is a better observation, not a bigger memory. Test this by asking whether a human with the full observation history could solve the task. If not, the observation design is the bottleneck.

Failure mode 2 — the memory horizon is too long. If the cue appears at step 0 and the decision happens at step 500, a fixed-size recurrent state may decay before it is needed. The summary loses the bit. Shorten the delay or increase state capacity — but first confirm the horizon is actually the problem.

Failure mode 3 — the training signal is too sparse or too noisy. If reward arrives only at the end of a long episode, the memory parameters may receive weak gradient and stay near initialization. The recurrent state never learns to carry anything useful.

The decision rule: if a memoryless policy already matches the best achievable return, memory is overhead. If memory fails, test whether the observation or the task design is the real bottleneck before scaling the architecture.

One Modification Worth Running Next

Sweep the memory horizon. Vary k for explicit history, or the recurrent state size, and plot return against memory capacity.

Predict the shape before you run it: performance should improve, then saturate once capacity exceeds the task's true memory requirement. That saturation point is a diagnostic. It tells you how much history the task actually needs — information the reward curve alone never gives you.

A second variation worth trying: shorten the delay between cue and action. Watch the memory advantage shrink. When the delay is one step, the memoryless policy may match the memory policy entirely, confirming the gap is driven by observability rather than by policy capacity.

Before you reach for recurrence on your own task, verify two things: that the observation is genuinely insufficient, and that the required memory is short enough to summarize. If both hold, memory is the right tool. If either fails, you are about to add parameters that cannot fix the real problem. Instrument your task to measure how much history the optimal policy truly needs — that measurement is worth more than any architecture choice.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

A recurrent policy succeeds when the cue-to-decision delay is short but fails when the delay is much longer. Which next step best tests the article's memory-horizon diagnosis?
Question 1 of 2Scenario Interpretation

Focus: Choose a targeted diagnostic or intervention when a recurrent policy may be losing a cue over a long delay.

You sweep explicit-history window size or recurrent-state capacity on the cue task. Which performance pattern is the article's expected diagnostic?
Question 2 of 2Output Prediction

Focus: Predict how performance should change as memory capacity increases beyond the task's actual history requirement.

References

  1. Recurrent DQN: Training recurrent policies — PyTorch Tutorials 2.14.0+cu130 documentationpytorch.org
  2. Part 1: Key Concepts in RL — Spinning Up documentationspinningup.openai.com
Practical resource

Build a stronger RL mental model

Use structured resources to connect equations, algorithms, and observable agent behavior.

Browse resources
Related sites

Continue across related AI foundations

Use LearnPyFast for Python foundations and LearnLLMFast for practical language-model and agent application concepts.

Python tutorialstutorial

LearnPyFast

Beginner-friendly Python tutorials, examples, and learning paths for practical programming foundations.

PythonProgrammingBeginners
Visit LearnPyFast
LLM tutorialstutorial

LearnLLMFast

Practical LLM tutorials for builders who want to understand prompting, workflows, agents, and AI applications.

LLMAIBuilders
Visit LearnLLMFast

Keep learning

Related reinforcement learning tutorials

Continue with nearby RL concepts, algorithms, and experiments that build on the same decision process.