18. REINFORCE Policy-Gradient Loss
Problem
The REINFORCE algorithm is the simplest way to train a stochastic policy directly from reward: increase the log-probability of actions that led to high return, decrease it for actions that led to low return. No value function, no Bellman equation — just the score-function gradient estimator, E[∇ log π(a|s) · G_t], applied episode by episode. It's also the conceptual starting point for every policy-gradient method DeepMind has published on top of it, from AlphaGo's policy network to AlphaStar's league training.
Given a batch of trajectories' per-timestep rewards and the log-probabilities the policy assigned to the actions it actually sampled, this problem chains two things together: turn the raw rewards into discounted returns-to-go via a backward recursion, then combine those returns with the log-probabilities (and an optional baseline, to reduce variance) into the scalar loss whose gradient is the REINFORCE update.
Source: src/18_reinforce_policy_gradient.py
def reinforce_policy_gradient_loss(
rewards: torch.Tensor, log_probs: torch.Tensor, gamma: float,
baseline: torch.Tensor | None = None,
) -> torch.Tensor: ...
>>> rewards = torch.tensor([[1.0, 0.0, 1.0]])
>>> log_probs = torch.tensor([[-0.2, -0.5, -0.1]])
>>> reinforce_policy_gradient_loss(rewards, log_probs, gamma=0.99).shape
torch.Size([])
Step-by-Step Approach
- Recognize the two separate computations chained together: a discounted return-to-go over time, then a policy-gradient loss over those returns.
- Compute the return-to-go with a backward loop over timesteps:
G_t = r_t + gamma * G_{t+1}, starting fromG_{T} = 0past the last timestep. - If a baseline is given, subtract it from the returns to form the advantage; if not, use the raw returns directly — both paths should share the same loss line.
- Detach the baseline (defensively, even if it's already a plain tensor) so no gradient accidentally flows into the loss through a would-be value-function baseline.
- Combine into the loss:
-(log_probs * advantage).mean()— the leading minus sign turns gradient ascent on expected return into gradient descent on the loss, which is what.backward()and an optimizer expect. - Average over every
(batch, timestep)entry, not just over the batch dimension, so trajectory length doesn't skew the effective learning rate.
The entire method is two small, separable pieces: a purely mechanical discounting recursion, and a one-line loss formula — the only real way to get this wrong is mixing up the sign (ascent vs. descent) or forgetting that the baseline must never carry gradient.
Reference solution
import torch
def reinforce_policy_gradient_loss(
rewards: torch.Tensor, log_probs: torch.Tensor, gamma: float,
baseline: torch.Tensor | None = None,
) -> torch.Tensor:
batch, T = rewards.shape
# backward recursion for discounted return-to-go: G_t = r_t + gamma * G_{t+1}
returns = torch.zeros_like(rewards)
running = torch.zeros(batch)
for t in reversed(range(T)):
running = rewards[:, t] + gamma * running
returns[:, t] = running
# baseline is a variance-reduction trick and must not carry gradient into
# the policy loss -- detach defensively even though callers usually pass
# a plain tensor already
advantage = returns if baseline is None else returns - baseline.detach()
# score-function estimator: loss = -E[log pi(a|s) * advantage], mean over
# every (batch, timestep) sample so batch size doesn't skew the gradient
return -(log_probs * advantage).mean()
Key Functions & Tricks
torch.zeros_like(rewards)— allocates the returns tensor with matching shape and dtype before filling it in the backward loopreversed(range(T))— walks timesteps back-to-front so eachG_tcan reuse the already-computedG_{t+1}baseline.detach()— cuts the baseline out of the autograd graph — a variance-reduction term must never receive its own gradient from this losstensor.mean()— reduces the elementwiselog_probs * advantageproduct to the single scalar a training loop calls.backward()on
How to Recognize This Pattern
Recognize this pattern whenever a problem asks you to turn a policy's sampled log-probabilities and observed rewards into a trainable scalar loss, with no explicit value/Q-function forwarded in — that absence is the signal it's vanilla REINFORCE rather than an actor-critic method. A common variation swaps the simple discounted-return baseline for a learned value-function baseline (turning it into the beginning of an actor-critic setup) or normalizes the advantage (subtract mean, divide by std) before weighting the log-probs, which further reduces gradient variance. The most common pitfall is getting the loss sign backwards — forgetting the leading minus turns a policy-improving update into a policy-destroying one that silently trains in the wrong direction.