Recurrent PPO (LSTM)¶
Intuition¶
Recurrent PPO adds an LSTM to the PPO actor-critic so the policy can integrate information over time -- essential for partially observable tasks where a single observation is not a sufficient state. The recurrent hidden state is carried across timesteps during collection and reset at episode boundaries; PPO updates backpropagate through time (BPTT) over per-episode sequence segments.
Key Equations¶
The standard PPO clipped surrogate, evaluated on recurrent outputs:
with importance ratio \(\rho_t = \dfrac{\pi_\theta(a_t\,|\,s_t, h_t)}{\pi_{\theta_{\text{old}}}(a_t\,|\,s_t, h_t)}\)
and LSTM hidden state \(h_t\); advantages \(\hat{A}_t\) come from GAE. Every loss term
is masked so padded / cross-episode steps contribute exactly zero, and the
hidden state is zeroed at each done so gradients never flow across an episode
boundary.
Pseudocode¶
initialize LSTM actor-critic
for iteration:
collect rollout, carrying (h, c) across steps, resetting (h, c) at done
advantages, returns = compute_gae_batched(...) # reuse Rust GAE
for epoch:
split each env's timeline into per-episode segments (no segment crosses a done)
re-run the LSTM over zero-padded segments (mask = real vs pad)
minimize masked [ PPO clip + 0.5*value MSE - entropy bonus ] (BPTT)
Usage¶
from rlox import Trainer
trainer = Trainer("recurrent_ppo", env="CartPole-v1", config={"lstm_hidden": 64})
trainer.train(total_timesteps=300_000)
Status¶
Experimental. Discrete action spaces (first version). Fully solves CartPole-v1
(reward 500.0). Designed for POMDPs; Trainer.evaluate() resets the recurrent
state between evaluation episodes so they do not leak state into one another.