The tuned DDPG policy landing on LunarLanderContinuous-v3 (deterministic eval: 213 ± 86 mean return, 83% of episodes solved).
DDPG is an off-policy actor-critic method for continuous action spaces that maintains deterministic policies. This implementation mirrors the structure of the other agents in this repository (PPO, SAC, TD3, etc.) with configurable components, shared utilities, and consistent logging.
- Deterministic actor network with a single critic and Polyak-averaged targets.
- Replay buffer warm-up, optional target policy smoothing, and Gaussian exploration noise.
- Integrated tqdm-aware logging, checkpoint helpers, and Weights & Biases tracking.
DDPG (Lillicrap et al., 2016) is an off-policy, actor-critic algorithm for environments with continuous action spaces. It blends two ideas:
-
Deterministic Policy Gradient (DPG): instead of a stochastic policy that outputs a distribution over actions, DDPG learns a deterministic actor
$\mu_\theta(s)$ that outputs one specific action. This lets us push gradients of the value estimate directly through the chosen action - cheap and low-variance in high-dimensional continuous control. - DQN-style stabilization: like DQN, it uses an experience replay buffer (to decorrelate samples and reuse off-policy data) and target networks (slowly tracking copies that stabilize the bootstrapped learning target).
The result is an actor-critic pair:
-
Critic
$Q_\phi(s, a)$ - learns the action-value function (how good is action$a$ in state$s$ ). -
Actor
$\mu_\theta(s)$ - learns to output the action that maximizes the critic.
Because the policy is deterministic, exploration must be injected by hand: at acting time we add noise to the action (this implementation uses Gaussian noise; the original paper used Ornstein-Uhlenbeck).
- Observe
$s$ , act with$a = \mathrm{clip}\big(\mu_\theta(s) + \epsilon,\ a_{\text{low}},\ a_{\text{high}}\big)$ , where$\epsilon \sim \mathcal{N}(0, \sigma^2)$ . - Store the transition
$(s, a, r, s', d)$ in the replay buffer. - Sample a random minibatch and update the critic (regression toward a bootstrapped target), then the actor (gradient ascent on the critic), then softly nudge both target networks.
Objective. We want a policy that maximizes the expected discounted return. For a deterministic policy
where
Critic learning (Bellman / TD target). The critic is trained to satisfy the Bellman equation. Using target networks
where
Actor learning (Deterministic Policy Gradient theorem). The actor is updated to produce actions that the critic rates highly. The DPG theorem gives the gradient of
In practice this is implemented as gradient descent on the actor loss (the negative of the above objective):
Target network soft (Polyak) updates. After each gradient step, the target networks are nudged a tiny fraction
Bounded actions. The actor squashes its output through
Optional target-policy smoothing. This implementation also exposes a TD3-style trick (off by default): adding clipped noise to the target action
- Initialize critic
$Q_\phi$ , actor$\mu_\theta$ , and targets$\phi' \leftarrow \phi$ ,$\theta' \leftarrow \theta$ ; empty replay buffer$\mathcal{D}$ . - For each environment step: select
$a = \mathrm{clip}(\mu_\theta(s) + \epsilon, a_{\text{low}}, a_{\text{high}})$ , execute it, store$(s, a, r, s', d)$ in$\mathcal{D}$ . - Sample a minibatch from
$\mathcal{D}$ ; compute target$y$ ; update the critic by minimizing$L(\phi)$ . - Update the actor by ascending the deterministic policy gradient (minimizing $L(\theta)$).
- Soft-update both target networks with
$\tau$ .
| Symbol | Meaning | Where in code |
|---|---|---|
|
|
actor and target actor |
Actor in ddpg/networks.py; agent.actor, agent.actor_target
|
|
|
critic and target critic |
Critic in ddpg/networks.py; agent.critic, agent.critic_target
|
| Bellman target |
target in DDPGAgent.update
|
|
| critic MSBE loss |
critic_loss (F.mse_loss) |
|
| actor loss |
actor_loss |
|
| discount factor | gamma |
|
| Polyak coefficient |
tau (_soft_update) |
|
| exploration noise std | exploration_noise |
|
|
|
target smoothing std / clip |
target_policy_noise, target_noise_clip
|
|
|
action rescaling |
action_scale, action_bias buffers in Actor
|
The default benchmark is LunarLanderContinuous-v3 - a continuous-control rocket-landing task whose two-dimensional action space (main and side thrusters) and dense shaping reward are a natural fit for DDPG's deterministic actor. It reliably converges to a successful-landing policy, making for a clean visual inference demo. Pendulum-v1 is kept as an alternative config.
# ==================================================
# Recommended: the tuned config (target smoothing) reproduces the demo above
python -m DDPG.main train --config DDPG/configs/lunarlander_continuous_tuned.yaml
python -m DDPG.main demo --config DDPG/configs/lunarlander_continuous_tuned.yaml --model_path DDPG/checkpoints_tuned/best.pt
# ==================================================
# Or, for the pure DDPG config (no target smoothing)
python -m DDPG.main train --config DDPG/configs/lunarlander_continuous.yaml
python -m DDPG.main demo --config DDPG/configs/lunarlander_continuous.yaml --model_path DDPG/checkpoints/best.ptAuthenticate with WandB via --wandb_key YOUR_KEY, or export WANDB_API_KEY in your environment (the CLI flag takes precedence) - matching the PPO / SAC / TD3 / A3C convention. Checkpoints and the moving-average best.pt snapshot are written under the config's checkpoint_dir. Always demo from best.pt, not the last checkpoint - see Training results & analysis for why.
If you manage the project with uv, set up the environment once from the repository root:
uv venv .venv # create the virtual environment
uv sync # install the core dependencies from pyproject.tomlBox2D is required for the default environment. LunarLanderContinuous-v3 runs on Box2D, which is not part of the default pyproject.toml extras (classic-control, toy-text). Add it once:
uv pip install "gymnasium[box2d]"
# if the box2d build fails, install SWIG first, then retry:
# uv pip install swig && uv pip install "gymnasium[box2d]"Then run training and the demo through uv run (no manual activation needed):
uv run python -m DDPG.main train --config DDPG/configs/lunarlander_continuous.yaml
uv run python -m DDPG.main demo --config DDPG/configs/lunarlander_continuous.yaml --model_path DDPG/checkpoints/best.ptThe Pendulum-v1 config needs no extra dependencies (classic-control is already included), so it works right after uv sync.
# from the repo root (conda env rlhero, or any env with the deps installed)
python -m pytest tests/ddpg/
# or with uv
uv run python -m pytest tests/ddpg/The DDPG tests use stubbed environments, so they run without Box2D installed.
YAML files in DDPG/configs/ expose hyper-parameters:
- Environment: Gym id, render mode, and optional kwargs.
- Training: interaction horizon, warm-up steps, replay buffer size, learning rates, Polyak factor, exploration noise, and optional target policy noise.
- Model: shared hidden layer sizes and activation for actor and critic.
- Logging: logging cadence, checkpoint cadence, output paths, and logger behaviour.
- Inference: default checkpoint path and number of evaluation episodes.
Two LunarLanderContinuous configs are provided:
lunarlander_continuous.yaml- pure DDPG (no target smoothing). Useful for seeing the classic over-estimation failure (see below).lunarlander_continuous_tuned.yaml- adds TD3-style target-policy smoothing (target_policy_noise=0.2,target_noise_clip=0.5) and a gentler critic LR (3e-4). This is the config used for the demo above.
Copy either (or pendulum.yaml) to tailor runs for other continuous control benchmarks.
Both configs were trained for 400k environment steps on LunarLanderContinuous-v3 (seed 42). The headline is a textbook illustration of why DDPG is famously unstable - and how target smoothing helps.
| Run | Best 5-ep avg (train) | Deterministic eval (30 ep) | % solved (≥200) | Peak critic Q | Peak critic loss |
|---|---|---|---|---|---|
| Pure DDPG | 187.9 | 55 ± 115 | 20% | 330 | 258 |
| DDPG + target smoothing | 259.7 | 213 ± 86 | 83% | 147 | ~30 |
What happened with pure DDPG. The policy learned to land within the first ~20k steps (its peak), then collapsed: the single critic's Q-value exploded to ~330 while realized returns stayed negative (a ~240-point over-estimation gap). The actor maximizes the critic, so once the critic became delusional the policy chased a fiction and degraded - ending up hovering until the 1000-step timeout rather than committing to a landing. This is exactly the over-estimation pathology that motivated TD3.
What the tuned run fixed. Adding clipped noise to the target action ($\tilde a' = \mathrm{clip}(\mu_{\theta'}(s') + \mathrm{clip}(\epsilon,-c,c),,a_{\text{low}},a_{\text{high}})$) and lowering the critic learning rate kept the Q-estimate calibrated (peak ~147, critic loss ~30 instead of 258). The result is a genuinely solved policy: 213 mean return with 83% of evaluation episodes ≥200.
Even when tuned, DDPG remains high-variance; the return curve still oscillates and the final checkpoint (step 400k) evaluates at only -40. The strong policy lives in
best.pt, which is whysave_best(rolling-average checkpointing) is essential for this algorithm. Always demo frombest.pt, not the last checkpoint.
Charts below are from the tuned run (DDPG/configs/lunarlander_continuous_tuned.yaml).
Critic Q-value (blue) vs. realized return (green). The estimate stays bounded (~147) and tracks real returns - contrast with pure DDPG, where Q ran away to ~330 while returns stayed negative.
Actor loss ($-\mathbb{E}[Q]$) and critic loss (MSBE). The critic loss stays low and spike-free, a sign the bootstrapped target is well-behaved.
Episode return (green) and length (orange). Returns repeatedly reach the solved bar (200), though with the high variance characteristic of DDPG.
- Lillicrap et al., Continuous Control with Deep Reinforcement Learning, ICLR 2016 https://arxiv.org/abs/1509.02971
- OpenAI Spinning Up DDPG: https://spinningup.openai.com/en/latest/algorithms/ddpg.html
- Stable-Baselines3 DDPG: https://stable-baselines3.readthedocs.io/en/master/modules/ddpg.html



