Skip to content
AI360Xpert
Beta

Continuous Action Spaces in RL

Discrete buttons cannot steer a robot arm, so the policy outputs smooth real-valued controls instead. It predicts a Gaussian per joint, samples with a trick that keeps gradients alive, and squashes into motor limits.

A Gaussian policy head predicts mean and spread, samples with the reparameterization trick, and squashes through tanh into bounded motor commands
A Gaussian policy head predicts mean and spread, samples with the reparameterization trick, and squashes through tanh into bounded motor commands

Why Does This Exist?

DQN-style methods pick actions by scoring every option and taking the max:

a∗=arg⁡max⁡aQ(s,a)a^* = \arg\max_a Q(s, a)

That breaks the moment actions turn continuous. A 7-joint arm with 10 bins per joint needs 10710^7 scores per step, and the winning command still jumps in coarse steps that chatter real gears. Robots, cars, and valves need smooth vectors like torques and steering angles, not button presses.

Continuous methods output the vector directly. A stochastic policy predicts a distribution πθ(a∣s)\pi_\theta(a \mid s) and samples it; a deterministic one predicts the vector itself. One forward pass, no search over actions.

This page covers the stochastic Gaussian route with bounded outputs. For the deterministic twin see deterministic policy gradients and DDPG. For the gradient theorem underneath see policy gradient.

You need one setup idea: a policy maps states to actions, and learning adjusts its weights to raise the expected return.

Think of It Like This

A ship wheel versus three gamepad buttons

Steer a yacht through a narrow channel with three buttons: hard left, center, hard right. To hold 4.24.2 degrees you slam left, then right, then left, lurching down the channel and straining the rudder cables.

A real wheel turns to any angle and stays there. Small correction, smooth water, quiet cables.

Map it point for point: buttons are a discretized action grid, the wheel is a continuous policy output, and the lurching is the bang-bang chatter that discretization causes on real hardware.

Where it stops: a wheel is one axle, while a humanoid outputs 18 torques at once with coordination across joints, which is why the policy predicts a whole vector with per-joint spreads.

How It Actually Works

The standard stochastic choice is a diagonal Gaussian. The network predicts a mean head μθ(s)\mu_\theta(s) and a log-spread head log⁡σθ(s)\log \sigma_\theta(s), with spread clamped (often to [−20,2][-20, 2]) so exploration can neither explode nor die. Predicting log-spread keeps σ=exp⁡(log⁡σ)\sigma = \exp(\log \sigma) positive for free.

Two gradient paths

Score-function methods (REINFORCE, PPO, TRPO) use the likelihood ratio:

∇θJ=E[∇θlog⁡πθ(a∣s) A^(s,a)]\nabla_\theta J = E[\nabla_\theta \log \pi_\theta(a \mid s) \, \hat{A}(s, a)]

For one Gaussian dimension the mean score is (a−μ)/σ2(a - \mu)/\sigma^2 and the log-spread score is (a−μ)2/σ2−1(a - \mu)^2/\sigma^2 - 1. Good actions pull the mean toward them; samples beyond one spread widen exploration, nearer ones tighten it.

Pathwise methods (SAC, DDPG-style critics) use the reparameterization trick instead. Sample noise ε∼N(0,1)\varepsilon \sim \mathcal{N}(0, 1) once, then write the action as arithmetic:

u=μθ(s)+σθ(s)εu = \mu_\theta(s) + \sigma_\theta(s) \varepsilon

Gradients now flow through uu into θ\theta by the chain rule through the critic Q(s,a)Q(s, a), with far less variance than the score route.

Squash into limits, then fix the density

Motors have hard bounds but Gaussians run to infinity. Clipping kills gradients outside the range, so modern actors squash smoothly:

a=amax⁡tanh⁡(u)a = a_{\max} \tanh(u)

Squashing compresses probability near the edges, so the logged density must shrink accordingly. The change-of-variables correction for one dimension is:

log⁡π(a∣s)=log⁡p(u∣s)−log⁡(amax⁡(1−tanh⁡2(u)))\log \pi(a \mid s) = \log p(u \mid s) - \log(a_{\max}(1 - \tanh^2(u)))

Skip that subtraction and entropy bonuses in SAC read the wrong density, which pushes pre-squash values to extremes until gradients saturate.

Worked example in 1D

Mean μ=2.0\mu = 2.0, spread σ=0.5\sigma = 0.5, bound amax⁡=1.0a_{\max} = 1.0, latent sample u=2.5u = 2.5.

Base log density:

log⁡p(u)=−12log⁡(2π)−log⁡0.5−(2.5−2.0)22(0.25)=−0.9189+0.6931−0.5000=−0.7258\log p(u) = -\tfrac{1}{2}\log(2\pi) - \log 0.5 - \frac{(2.5-2.0)^2}{2(0.25)} = -0.9189 + 0.6931 - 0.5000 = -0.7258

So p(u)≈0.4839p(u) \approx 0.4839. Scores: mean score (0.5)/(0.25)=2.0(0.5)/(0.25) = 2.0; log-spread score 0.25/0.25−1=0.00.25/0.25 - 1 = 0.0, because the sample sits exactly one spread away.

Squash: a=tanh⁡(2.5)≈0.9866a = \tanh(2.5) \approx 0.9866. Slope da/du=1−0.98662≈0.0266da/du = 1 - 0.9866^2 \approx 0.0266, so log⁡∣J∣≈−3.6271\log |J| \approx -3.6271. Corrected log density:

log⁡π(a∣s)=−0.7258−(−3.6271)=2.9013\log \pi(a \mid s) = -0.7258 - (-3.6271) = 2.9013

That is density ≈18.20\approx 18.20. It looks startling next to 0.480.48, but tanh packs a wide latent range into a sliver near 1.01.0, so density there must be tall. The diagram traces this exact sample.

Code

The snippet below replays the worked sample with only the standard library.

import math

def gaussian_logp(u: float, mu: float, sigma: float) -> float:    """Log density of a 1D Gaussian at u."""    return -0.5 * math.log(2.0 * math.pi) - math.log(sigma) - ((u - mu) ** 2) / (2.0 * sigma ** 2)

mu, sigma, u, a_max = 2.0, 0.5, 2.5, 1.0logp = gaussian_logp(u, mu, sigma)print(round(logp, 4))# -> -0.7258print(round((u - mu) / sigma ** 2, 4))# -> 2.0print(round(((u - mu) ** 2) / sigma ** 2 - 1.0, 4))# -> 0.0
a = math.tanh(u)slope = 1.0 - a ** 2print(round(a, 4))# -> 0.9866print(round(math.log(a_max * slope), 4))# -> -3.6271print(round(logp - math.log(a_max * slope), 4))# -> 2.9013

Watch Out For

Forgetting the tanh Jacobian in entropy code

SAC tempers exploration with an entropy bonus computed from log⁡π(a∣s)\log \pi(a \mid s). Plug in the raw Gaussian log⁡p(u∣s)\log p(u \mid s) and the bonus ignores the squash: near the bounds the policy looks more uncertain than it is, training shoves uu outward, tanh saturates, and gradients vanish.

Symptom: pre-squash magnitudes drift to large values while actions pin at the limits. Fix: always subtract ∑log⁡(1−tanh⁡2(u))\sum \log(1 - \tanh^2(u)) (plus the amax⁡a_{\max} scale) wherever a squashed log-probability is needed.

Letting the spread collapse before exploring

An unconstrained log-spread head can dive early on noisy gradients. Spread hits zero, the policy turns deterministic before seeing the space, and the arm masters one bad motion.

Symptom: actions repeat almost exactly across episodes while returns plateau low. Fix: clamp log-spread (such as to [−20,2][-20, 2]), start near zero spread rather than very negative, and watch entropy per joint, not just the mean return.

The Quick Version

  • Continuous policies emit real-valued control vectors in one forward pass, dodging the KmK^m blowup of discretized grids.
  • The network predicts per-joint Gaussian means and clamped log-spreads, keeping variance positive and bounded.
  • The reparameterization u=μ+σεu = \mu + \sigma \varepsilon routes critic gradients straight through the sample with low variance.
  • Tanh squashing enforces motor bounds, and the log density must subtract the Jacobian term to stay exact.
  • In the worked sample, u=2.5u = 2.5 with mean 2.0 and sigma 0.5 squashes to 0.98660.9866 with corrected log density 2.90132.9013.