Code Architecture — PPO Implementation
This document describes the notebook structure, key functions/classes, data flow, and deliberate simplifications versus the original paper.
Notebook Structure
1. Setup & Imports
Installs required packages (gymnasium, torch) and imports all modules. Uses gymnasium (the maintained successor to OpenAI Gym) with the CartPole-v1 environment — a classic discrete-action control task that trains quickly on GPU and clearly demonstrates PPO’s mechanics.
2. Hyperparameters
All PPO hyperparameters in one cell, matching the paper’s conventions where applicable:
gamma = 0.99(discount factor)epsilon = 0.2(clip parameter — the paper’s ε)epochs = 10(K — number of optimization epochs per update, matching Table 3)gae_lambda = 0.95(GAE parameter λ from Table 3)steps_per_epoch = 2048(T — horizon, matching Table 3)minibatch_size = 64(matching Table 3)lr = 3e-4(Adam stepsize from Table 3)hidden_dim = 64(two hidden layers of 64 units, matching the paper’s MLP)clip_ratio = 0.2
3. ActorCritic Network
A single neural network with two heads:
- Shared trunk: 2-layer MLP with tanh activations (64 hidden units each, matching the paper).
- Policy head (actor): outputs logits over discrete actions → softmax distribution π(a|s).
- Value head (critic): outputs a scalar V(s).
- The paper notes that for the MuJoCo experiments, parameters were NOT shared between policy and value. We use a shared trunk for simplicity, which is the common practical variant and the one used in the Atari experiments (Table 5, where architecture follows A3C with shared layers).
Key shapes:
- Input:
(batch, obs_dim)— e.g.,(2048, 4)for CartPole - Policy logits:
(batch, n_actions)— e.g.,(2048, 2) - Value:
(batch, 1)
4. Memory Buffer
A RolloutBuffer class that collects:
observations,actions,log_probs(from the old policy π_θold)rewards,masks(done flags)values(from the critic)advantages,returns(computed after collection via GAE)
Data flow: The buffer collects T=2048 steps from a single actor (simplified from N parallel actors in the paper). After collection, GAE advantages are computed, the buffer is shuffled into minibatches, and the surrogate loss is optimized for K=10 epochs.
5. GAE Computation (compute_gae)
Implements Generalized Advantage Estimation (Eq. 11-12):
δ_t = r_t + γ·V(s_{t+1})·(1−done) − V(s_t)
Â_t = δ_t + (γλ)·δ_{t+1} + (γλ)²·δ_{t+2} + ...
Computed backwards from the last step. Returns are computed as R_t = Â_t + V(s_t) for the value function loss.
6. PPO Update (ppo_update)
The core algorithm (Algorithm 1 from the paper):
- Compute GAE advantages and returns from the collected buffer.
- Normalize advantages (subtract mean, divide by std) — a standard stabilisation trick not in the original paper but used in virtually all modern implementations.
- For K=10 epochs: a. Shuffle the buffer and split into minibatches of size 64. b. For each minibatch, compute:
r(θ) = exp(log π_θ(a|s) − log π_θold(a|s))— probability ratioL^CPI = r · — unclipped surrogateL^CLIP = min(r·Â, clip(r, 1−ε, 1+ε)·Â)— clipped surrogateL^V = (V_θ(s) − R)²— value function loss (MSE)S = −Σ π·log(π)— entropy bonusTotal = −L^CLIP + c1·L^V − c2·S(minimised, so negated) c. Backpropagate and step with Adam.
- Track clipped vs unclipped objective values for plotting.
7. Clipped vs Unclipped Objective Plotting
During training, we record:
- Mean of
L^CPI = E[r·Â](unclipped) per update - Mean of
L^CLIP = E[min(r·Â, clip(r,1−ε,1+ε)·Â)](clipped) per update - The fraction of samples where clipping was active (|r−1| > ε)
These are plotted after training to show how clipping constrains the objective — when the policy tries to move too far, L^CLIP plateaus while L^CPI keeps increasing (for positive advantages) or keeps decreasing (for negative advantages).
8. Training Loop
for epoch in range(total_epochs):
1. Collect 2048 steps using current policy
2. ppo_update(buffer) # 10 epochs of minibatch SGD
3. Log episode rewards, losses, clip statistics
4. Clear buffer
Plot: episode rewards over time, clipped vs unclipped objectives
Deliberate Simplifications vs Full Paper
| Aspect | Paper | This Notebook |
|---|---|---|
| Environment | MuJoCo continuous control + Atari | CartPole-v1 (discrete, simple) |
| Parallel actors | N parallel actors (8–128) | Single actor (sequential) |
| Policy distribution | Gaussian (continuous actions) | Categorical (discrete actions) |
| Parameter sharing | Separate for MuJoCo, shared for Atari | Shared (Atari-style) |
| Network architecture | 2×64 MLP with tanh | 2×64 MLP with tanh (same) |
| Adaptive KL penalty | Explored as alternative | Not implemented (clip only) |
| Total timesteps | 1M (MuJoCo), 10M (Atari) | ~50K–100K (CartPole converges fast) |
| Entropy coefficient | 0.01 (Atari) | 0.01 (same) |
| Value loss coefficient | 1.0 | 1.0 (same) |
| Advantage normalization | Not mentioned | Yes (standard practice) |
| Observation normalization | Used in Atari | Not used (CartPole obs are small) |
The simplifications make the notebook runnable in under 5 minutes on a Kaggle GPU while faithfully implementing the core algorithm — the clipped surrogate objective, GAE, multiple optimization epochs, and the actor-critic loss combination.