Skip to content
AI360Xpert

Policy Gradient Methods

Policy gradient methods directly optimize the policy by estimating the gradient of expected reward with respect to policy parameters, making them applicable to continuous action spaces where value-based methods struggle.

Policy gradient methods directly optimize the policy by estimating the gradient of expected reward with respect to policy parameters, making them applicable to continuous action spaces where value-based methods struggle.
Policy gradient methods directly optimize the policy by estimating the gradient of expected reward with respect to policy parameters, making them applicable to continuous action spaces where value-based methods struggle.

Why Does This Exist?

Q-learning selects actions via argmaxaQ(s,a)\arg\max_a Q(s, a). When the action space is continuous — robot joint angles, throttle levels, portfolio weights, drug dosages — this argmax is intractable. Policy gradient methods sidestep this entirely by directly parameterizing the policy πθ(as)\pi_\theta(a \mid s) and optimizing it via gradient ascent.

A second motivation: stochastic policies. Value-based methods produce deterministic greedy policies. But some tasks require mixed strategies — a deterministic policy in rock-paper-scissors is exploitable. Policy gradient naturally represents and learns stochastic policies.

Think of It Like This

A chef adjusting a recipe based on diner feedback

A chef adjusting a recipe doesn't consult a fixed lookup table of optimal spice ratios. They cook a dish, collect feedback ("too salty", "a bit bland"), and nudge the recipe slightly in the direction that diners preferred. Over many meals and feedback cycles, the recipe converges toward something diners love. REINFORCE does exactly this: collect trajectories, observe which actions led to high returns, and nudge policy parameters to make those actions more probable.

How It Actually Works

The policy gradient theorem

We want to maximize expected return J(θ)=Eτπθ[G(τ)]J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}[G(\tau)]. The policy gradient theorem gives:

θJ(θ)=Eτπθ[t=0Tθlogπθ(atst)Gt]\nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}\left[\sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot G_t\right]

Where Gt=k=tTγktrkG_t = \sum_{k=t}^{T} \gamma^{k-t} r_k is the return from timestep tt.

The key term θlogπθ(atst)\nabla_\theta \log \pi_\theta(a_t \mid s_t) is the score function: it points in the direction that increases the probability of action ata_t in state sts_t. Multiplying by GtG_t means: if Gt>0G_t > 0 (good outcome), push toward ata_t; if Gt<0G_t < 0 (bad outcome), push away from it.

REINFORCE (Monte Carlo policy gradient)

The simplest policy gradient algorithm:

  1. Sample trajectory τ=(s0,a0,r0,,sT)\tau = (s_0, a_0, r_0, \ldots, s_T) under current πθ\pi_\theta
  2. Compute returns GtG_t for each timestep tt
  3. Update: θθ+αtθlogπθ(atst)Gt\theta \leftarrow \theta + \alpha \sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot G_t

Variance reduction with a baseline

The REINFORCE gradient estimator has high variance — returns vary dramatically across episodes. Subtracting a baseline b(st)b(s_t) that doesn't depend on ata_t leaves the gradient unbiased while reducing variance:

θJE[tθlogπθ(atst)(Gtb(st))advantage At]\nabla_\theta J \approx \mathbb{E}\left[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot \underbrace{(G_t - b(s_t))}_{\text{advantage } A_t}\right]

The baseline is typically V(st)V(s_t), estimated by a separate value network. The advantage At=GtV(st)A_t = G_t - V(s_t) measures how much better action ata_t was compared to what was expected on average in state sts_t.

Policy parameterization

  • Discrete actions: Softmax over learned logits — πθ(as)=softmax(MLP(s))a\pi_\theta(a \mid s) = \text{softmax}(\text{MLP}(s))_a
  • Continuous actions: Gaussian policy — aN(μθ(s),σθ(s))a \sim \mathcal{N}(\mu_\theta(s), \sigma_\theta(s)), where the network outputs mean and log-std

Code

import gymnasium as gymimport torchimport torch.nn as nnimport torch.optim as optim
# ── Policy network (discrete actions) ────────────────────────────────────────class PolicyNet(nn.Module):    def __init__(self, obs_dim, n_actions, hidden=128):        super().__init__()        self.net = nn.Sequential(            nn.Linear(obs_dim, hidden), nn.ReLU(),            nn.Linear(hidden, hidden), nn.ReLU(),            nn.Linear(hidden, n_actions),        )        def forward(self, x):        return torch.softmax(self.net(x), dim=-1)
# ── REINFORCE with baseline ───────────────────────────────────────────────────env = gym.make("CartPole-v1")obs_dim   = env.observation_space.shape[0]n_actions = env.action_space.n
policy    = PolicyNet(obs_dim, n_actions)baseline  = nn.Sequential(nn.Linear(obs_dim, 128), nn.ReLU(), nn.Linear(128, 1))opt_p = optim.Adam(policy.parameters(), lr=3e-4)opt_b = optim.Adam(baseline.parameters(), lr=1e-3)
gamma = 0.99
for episode in range(800):    states, actions, rewards = [], [], []    state, _ = env.reset()    done = False
    while not done:        s_tensor = torch.tensor(state, dtype=torch.float32)        probs  = policy(s_tensor)        action = torch.multinomial(probs, 1).item()        next_state, reward, terminated, truncated, _ = env.step(action)        states.append(state); actions.append(action); rewards.append(reward)        state = next_state        done  = terminated or truncated
    # Compute discounted returns    G, returns = 0.0, []    for r in reversed(rewards):        G = r + gamma * G        returns.insert(0, G)    returns = torch.tensor(returns, dtype=torch.float32)
    states_t  = torch.tensor(states, dtype=torch.float32)    actions_t = torch.tensor(actions, dtype=torch.long)
    # Baseline update (value regression)    values = baseline(states_t).squeeze()    baseline_loss = nn.MSELoss()(values, returns)    opt_b.zero_grad(); baseline_loss.backward(); opt_b.step()
    # Policy update (REINFORCE with advantage)    with torch.no_grad():        advantages = returns - baseline(states_t).squeeze()
    # Normalize advantages for stability    advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
    log_probs = torch.log(policy(states_t).gather(1, actions_t.unsqueeze(1)).squeeze() + 1e-8)    policy_loss = -(log_probs * advantages).mean()    opt_p.zero_grad(); policy_loss.backward(); opt_p.step()
    if episode % 100 == 0:        ep_return = sum(rewards)        print(f"Episode {episode:>4}: return={ep_return:.0f}, steps={len(rewards)}")
env.close()

Watch Out For

High variance of Monte Carlo gradient estimates

Monte Carlo returns are noisy — the same policy produces very different return values due to environment stochasticity and long episode horizons. This makes REINFORCE slow and unstable. Use a baseline (as above), or move to actor-critic methods that use bootstrapped value estimates (r+γV(s)r + \gamma V(s')) instead of Monte Carlo returns for lower variance.

Large policy updates destroying good behavior

A large gradient update can move the policy far from its current position, collapsing it (assigning probability 1 to one action) or making it worse. This is the motivation for PPO (Proximal Policy Optimization), which clips the probability ratio to prevent large updates: J^PPO=clip(πθπθold,1ε,1+ε)At\hat{J}_{\text{PPO}} = \text{clip}\left(\frac{\pi_\theta}{\pi_{\theta_\text{old}}}, 1-\varepsilon, 1+\varepsilon\right) A_t.

The Quick Version

  • Policy gradients directly optimize πθ(as)\pi_\theta(a \mid s) via θJθlogπθGt\nabla_\theta J \propto \nabla_\theta \log \pi_\theta \cdot G_t.
  • REINFORCE: collect Monte Carlo trajectories, compute returns, update policy — simple but high variance.
  • Baselines reduce variance: advantage At=GtV(st)A_t = G_t - V(s_t) keeps the gradient unbiased while reducing noise.
  • Works with continuous actions and naturally represents stochastic policies (unlike Q-learning).
  • PPO and TRPO extend policy gradient with update-size constraints for stability.