Evolution & control · E15 · Implementation

Separate the three clocks in a recurrent DQN

Environment steps, replay updates and target synchronization are different events. A small PyTorch agent makes their roles visible.

PyTorchGym environmentDeque / random sampling
Environment steps, replay updates and target copies run on separate clocks, even inside one training program.
Figure 1. Three clocks, one learner. Environment steps, replay updates and target copies run on separate clocks, even inside one training program. Schematic update cadence. Original vector illustration.

Follow the information

From input to outcome

Interaction collects transitions, replay sampling selects training examples, and target synchronization copies parameters on a separate schedule. The three clocks must not be collapsed into one update.

Interaction collects transitions, replay sampling selects training examples, and target synchronization copies parameters on a separate schedule. The three clocks must not be collapsed into one update.
Figure 2. Information flow. Solid arrows carry observations, tensors or artifacts; other routes are explicitly labelled. Signal shapes, matrices and network icons are schematic, not measured samples or literal neuron counts. Open full-size SVG ↗ On narrow screens, scroll the diagram horizontally.

Read this alongside Figure 1: Environment steps, replay updates and target copies run on separate clocks, even inside one training program. The module map and layer-level figures below expand the operations in this route.

Separate the three clocks in a recurrent DQN: architectureObserved sequence: B × T × features → LSTM × 2: 128 hidden units → Q head: 64 → action_count → Replay batch: Uniform sampled transitions → TD target: Frozen target network → Optimizer step: Online network only. A high-level module map; comparison branches and training details are explained in the article.EVOLUTION & CONTROL / E15 / MODULE MAP01 INPUTObserved sequenceB × T × features02 MODULELSTM × 2128 hidden units03 MODULEQ head64 → action_count04 MODULEReplay batchUniform sampled transitions05 MODULETD targetFrozen target network06 OUTPUTOptimizer stepOnline network only
Source-grounded module map. Boxes summarize operations, not individual neurons; comparison arms and training paths are detailed below. On a small screen, scroll the diagram horizontally.
Observed sequence — B × T × features

The architecture in context

The system we are building

The earlier agent uses two LSTMs, the final temporal state and a small dense head to estimate an action value vector. An epsilon-greedy rule chooses actions. Transitions enter a bounded deque, while training samples them uniformly. The target network is initialized from the online network and synchronized separately.

Who does what in the stack

PyTorch
Two LSTMs, a dense Q head and Adam.
Gym environment
Defines observation, action and episode semantics.
Deque / random sampling
Provides bounded uniform replay.

The notebook builds a custom Gym environment and connects its observation shape to a recurrent PyTorch model. It explicitly handles device placement, replay batching and target computation. The useful abstraction is not “an LSTM trades”; it is a policy-learning loop whose data, update and synchronization events can be inspected independently.

Framework responsibility map. Each row maps a library or custom component to its job; rows are not a sequential inference graph.
Framework responsibility map. Each row maps a library or custom component to its job; rows are not a sequential inference graph. Open full-size SVG ↗

From module map to executable structure

Inside Recurrent Q-network

This results or evaluation article shares the implementation in E14. The architecture below describes that companion, not a newly trained model.

Two single-layer PyTorch LSTMs, hidden 128; configurable observation and action dimensions.

Layer-level implementation. B denotes batch size; parameter and shape conventions are expanded in the table.
Layer-level implementation. B denotes batch size; parameter and shape conventions are expanded in the table. Open full-size SVG ↗
Layer / tensor / operation ledger
Layer or branchOutput shapeImplementation detail
State sequenceB × T × dDo not confuse a sequence axis with the replay batch axis.
LSTM d→128B × T × 128nn.LSTM, batch_first=True.
LSTM128→128B × T × 128A separate recurrent module, not num_layers=2 passed into either constructor.
Last time stepB × 128Slice [:,-1,:]; padding changes its interpretation.
Q headB × ALinear 128→64 → ReLU → Linear 64→A, no softmax.

The recurrent encoder converts a window into one feature vector; the head estimates one action value per action, not action probabilities. The target network supplies a slowly changing bootstrap. Stop-gradient applies to the target, while only the taken action contributes to each sampled TD error.

One LSTM cell opened into its gates. This cell is repeated over time and within each recurrent layer.
One LSTM cell opened into its gates. This cell is repeated over time and within each recurrent layer. Open full-size SVG ↗

The equation and the update

y=r+γ(1−dterminal)max⁡aQθˉ(s+,a)y=r+\gamma(1-d_{\rm terminal})\max_a Q_{\bar\theta}(s^+,a)

Defaults: Adam 1e-3, gamma .99, replay 50,000 and batch 128. E14 adds prioritized replay and soft target updates (tau .005); E15 uses its own replay/target-update logic. They are not interchangeable training protocols.

Learning or solution path. A parameter-update path is different from the forward inference path; see text for target-network, frozen-feature and local-loss boundaries.
Learning or solution path. A parameter-update path is different from the forward inference path; see text for target-network, frozen-feature and local-loss boundaries. Open full-size SVG ↗

Implementation card / no invented benchmarks

Capacity, budget and execution evidence

Parameters / retained state
512(d+130)+512×258+8,256+65A per network, using PyTorch’s two bias vectors. Target network duplicates stored parameters.
Duration and hardware evidence
Archived examples are limited/interrupted runs; no convergence speed is inferred.
Source coordinates
E14 cell 2, lines 315–363; E15 cell 5, lines 39–80
Current reproduction context
Current workstation, supplied by the author: Apple M4, 128 GB unified RAM, 40 GPU cores and 16 CPU cores. This is context for prospective reproduction, not attribution of every archived run. Python and framework versions are not fully locked for these historical sources; declarations, when available, are identified separately.

Counts above are calculated from the stated layer shapes unless identified as saved measurements. They exclude optimizer state and nontrainable buffers. No archived training was rerun for this revision.

What these design choices change

Longer windows enlarge unrolled activation memory without changing the count of recurrent weights. Prioritized replay changes the sample distribution, so importance weights belong inside the per-sample loss before reduction. A priority exponent and an importance exponent serve different purposes.

Reproduction and measurement protocol

Create one terminal and one nonterminal transition and calculate both targets by hand. Test that priority updates use the intended TD-error magnitude. For recurrent replay, declare whether hidden state is reset for each sampled window; state carried across unrelated samples is a different algorithm.

For a new run, save the resolved Python/framework versions, backend, dtype, seed, input shapes, batch size and exact source revision. Start with one batch and one update. Log training steps separately from epochs or environment steps. Do not equate the configured maximum with a completed budget or convergence.

Measure initialization/compilation, data preparation, warmed forward pass, training updates and evaluation separately. Synchronize accelerator work around timed regions using the chosen framework’s supported mechanism. Report peak process memory and framework allocation separately; parameter bytes exclude activations, gradients, optimizer state and input buffers. On a shared machine, begin with a single CPU worker and a small batch rather than claiming all available resources.

A closer look at the implementation

The code that carries the idea

The excerpt selects only the Q value of the action actually taken using gather. The target is computed under no_grad, with the done flag removing the next-state term at episode termination. Gradients therefore update the online prediction without differentiating through the target estimate.

Python · cell 5 · lines 114–126
        states = states.reshape(self.batch_size, -1, self.state_dim)
        next_states = next_states.reshape(self.batch_size, -1, self.state_dim)
        
        current_q_values = self.policy_net(states).gather(1, actions.unsqueeze(1))
        
        with torch.no_grad():
            next_q_values = self.target_net(next_states).max(1)[0]
            target_q_values = rewards + (1 - dones) * self.gamma * next_q_values
        
        loss = nn.MSELoss()(current_q_values.squeeze(), target_q_values)
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

Verbatim archive excerpt from pt_drl.ipynb. Context-dependent historical code, not a standalone runnable program. Comments retain their original wording; the article distinguishes implemented behavior from stale or overbroad comments.

The boundary that matters

Reshaping a vector into a sequence does not create meaningful temporal context. The observation builder must already encode that ordering. Saved episodes demonstrate that the loop ran; they do not establish that the environment’s reward or fill model matches a real system.

Keep building

Other posts of interest