Policy Gradient Methods
Every algorithm so far learns a value function first, and derives a policy from it only indirectly (act greedily with respect to the values). Policy gradient methods skip the middleman entirely — parameterise the policy directly, and take gradients of expected return with respect to those parameters.
You can take gradients of expected return with respect to policy parameters without ever differentiating through the environment, using the log-derivative trick.
Why value-based methods struggle with continuous actions and stochastic optima
Deep Q-Networks's requires enumerating every action to find the best one — trivial for a handful of discrete actions, intractable for continuous action spaces (an infinite set to maximise over). Value-based methods also struggle to represent a genuinely stochastic optimal policy directly, which some problems (games with hidden information, certain safety-critical settings) actually require.
Parameterising a policy directly
Let be a policy directly parameterised by (a neural network's weights) — for discrete actions, typically a softmax over action logits; for continuous actions, typically the parameters of a Gaussian distribution. This sidesteps value-based methods' action-enumeration problem entirely: sampling from works identically regardless of whether the action space is discrete or continuous.
The objective: expected return
Directly Markov Decision Processes's expected-return objective, now written explicitly as a function of the policy's parameters — the quantity policy gradient methods differentiate and ascend.
The policy gradient theorem, derived via the log-derivative trick
The obstacle: is an expectation over trajectories whose probability itself depends on — naively differentiating through that would require differentiating through the environment's dynamics, which is generally impossible (unknown, non-differentiable). The log-derivative trick sidesteps this:
Applied to trajectories, and using the fact that the environment's transition probabilities don't depend on at all (only the policy's action probabilities do), this yields the policy gradient theorem:
The environment's (possibly unknown, non-differentiable) dynamics have vanished from the gradient entirely — only the policy's own log-probability needs to be differentiated, something a neural network handles trivially.
REINFORCE
The direct algorithm implementing this gradient: run an episode, compute the actual return following each action, and take a gradient step in the direction for every timestep — increasing the probability of actions that led to high return, decreasing it for actions that led to low return.
Why the gradient estimate has enormous variance
is a single Monte Carlo sample of the return — exactly Monte Carlo and TD Learning's high-variance estimator, now used directly inside a gradient. A single lucky (or unlucky) episode can produce a wildly noisy gradient estimate, making raw REINFORCE slow and unstable to train in practice.
Baselines, and the proof that subtracting a state-dependent baseline leaves the estimator unbiased
Subtracting any function that does not depend on the action from leaves the gradient's expectation unchanged:
Since for any , its gradient is exactly zero — so subtracting any state-only baseline adds zero in expectation, while (with a well-chosen baseline) substantially reducing variance in practice.
The value function as the natural baseline
Using (an estimate of the average return from that state) as the baseline turns into an estimate of the advantage from Value Functions and Bellman Equations — "was this specific action better than typical for this state," a much lower-variance signal than the raw return, and the direct bridge to Actor-Critic Methods.
Reward-to-go instead of the full episode return
Using the full episode return for every timestep's update credits early actions with rewards that happened before they could possibly have caused them. Using reward-to-go — , only the return from timestep onward — instead removes this non-causal credit assignment, reducing variance further with no change to the estimator's expectation (rewards from before time don't depend on , so including them only adds zero-mean noise).
Entropy regularisation for exploration
Adding a bonus term proportional to the policy's entropy to the objective discourages the policy from collapsing to a single deterministic action too early, encouraging continued exploration during training — a soft, differentiable alternative to ε-greedy's hard random-action switching.
Continuous action spaces via Gaussian policies
For continuous actions, parameterise as a Gaussian , with the network outputting the mean (and optionally the standard deviation) — sampling and computing both have simple closed forms for a Gaussian, making the policy gradient theorem directly applicable with no discretisation needed.
The on-policy sample-efficiency cost
Every gradient in this page requires trajectories sampled from the current policy — once updates, old trajectories are (in principle) no longer valid samples of the new policy's distribution and must be discarded. This on-policy requirement is a genuine sample-efficiency cost relative to off-policy methods like Deep Q-Networks, which can reuse arbitrarily old experience via the replay buffer.
| Symbol | Meaning |
|---|---|
| the directly-parameterised policy | |
| the expected-return objective | |
| a state-dependent baseline |
Code: REINFORCE with and without a baseline, variance measured directly
import torch
import torch.nn as nn
import numpy as np
from deep_q_network_demo import TinyCartPole
class PolicyNetwork(nn.Module):
def __init__(self, state_dim=4, action_dim=2):
super().__init__()
self.net = nn.Sequential(nn.Linear(state_dim, 64), nn.ReLU(), nn.Linear(64, action_dim))
def forward(self, x):
return torch.softmax(self.net(x), dim=-1)
class ValueNetwork(nn.Module):
def __init__(self, state_dim=4):
super().__init__()
self.net = nn.Sequential(nn.Linear(state_dim, 64), nn.ReLU(), nn.Linear(64, 1))
def forward(self, x):
return self.net(x).squeeze(-1)
def run_episode(env, policy):
states, actions, rewards = [], [], []
state = env.reset()
for _ in range(200):
probs = policy(torch.tensor(state, dtype=torch.float32))
action = torch.multinomial(probs, 1).item()
next_state, reward, done = env.step(action)
states.append(state); actions.append(action); rewards.append(reward)
state = next_state
if done:
break
return states, actions, rewards
def reward_to_go(rewards, gamma=0.99):
result, running = [0.0] * len(rewards), 0.0
for t in reversed(range(len(rewards))):
running = rewards[t] + gamma * running
result[t] = running
return result
def train_reinforce(use_baseline, n_episodes=300):
env = TinyCartPole()
policy = PolicyNetwork()
value_net = ValueNetwork() if use_baseline else None
policy_opt = torch.optim.Adam(policy.parameters(), lr=0.01)
value_opt = torch.optim.Adam(value_net.parameters(), lr=0.01) if use_baseline else None
returns_log, grad_variance_log = [], []
for episode in range(n_episodes):
states, actions, rewards = run_episode(env, policy)
returns = torch.tensor(reward_to_go(rewards), dtype=torch.float32)
states_t = torch.tensor(np.array(states), dtype=torch.float32)
actions_t = torch.tensor(actions)
if use_baseline:
values = value_net(states_t)
advantages = returns - values.detach()
value_loss = nn.functional.mse_loss(values, returns)
value_opt.zero_grad(); value_loss.backward(); value_opt.step()
else:
advantages = returns
log_probs = torch.log(policy(states_t).gather(1, actions_t.unsqueeze(1)).squeeze(1))
per_step_terms = -log_probs * advantages
policy_loss = per_step_terms.mean()
policy_opt.zero_grad(); policy_loss.backward(); policy_opt.step()
returns_log.append(sum(rewards))
grad_variance_log.append(per_step_terms.detach().var().item())
return returns_log, grad_variance_log
returns_no_baseline, var_no_baseline = train_reinforce(use_baseline=False)
returns_baseline, var_baseline = train_reinforce(use_baseline=True)
print(f"no baseline: mean last-20 return = {np.mean(returns_no_baseline[-20:]):.1f}, "
f"mean gradient-term variance = {np.mean(var_no_baseline[-20:]):.2f}")
print(f"with baseline: mean last-20 return = {np.mean(returns_baseline[-20:]):.1f}, "
f"mean gradient-term variance = {np.mean(var_baseline[-20:]):.2f} (should be lower)")
See also
- Actor-Critic Methods — replacing the Monte Carlo return with a learned, lower-variance critic.
- Deep Q-Networks — the value-based alternative this page's on-policy sample cost trades against.