Table of Contents
Open Table of Contents
Policy as NN
Policy based reinforcement learning aims to learn the “policy” itself. It parameterizes policy as a neural network and directly optimizes it unlike value-based reinforcement learning that usually learn a Q-value function and extract a deterministic policy afterwards (Q-learning, SARSA, DQN).
Some notations :
| Meaning | Notation | Explanation |
|---|---|---|
| policy | the action distribution under a certain state | |
| reward | the reward of taking an action at a state | |
| trajectory | also known as an episode, | |
| value | ||
| Q-value |
Policy Gradient Theorem & REINFORCE
A policy is optimal when the mean total reward of all trajectories sampled from the policy is the largest, so we define our target function as follows:
here the likelihood of trajectory under policy is:
Now we want to find the that maximize , we can calculate and do gradient ascent to find the argmax. Policy Gradient Theorem has that:
Intuitively, this means that for a high-reward trajectory, we want the action in this trajectory to appear more frequently.
Expand to see the proof
And this leads to REINFORCE, the simplest policy gradient algorithm — use Monte Carlo to estimate and use gradient ascent on it:
- Sample from policy
Baselines and Advantages
Standard PG usually has very large variance. This is because we update action over the overall reward of the whole sampled trajectory, the super-long rollout adds to stochasticity and variance. One way to reduce the variance is to shorten the reward horizon. Notice that there is causality within the PG theorem that action at time cannot affect reward at , so reward in the part won’t affect current policy.
Notice that , which is expected discounted reward-to-go. So we have:
We can do a little reformulation for this equation. Averaging over trajectory samples and timestep is equal to taking expectation over the state-action visitation distribution, where we can introduce a state visitation distribution . So that we can have a neater expression for PG theorem:
Expand to see the proof
Now we introduce the baseline method, subtracting an action-independent baseline does not affect the policy gradient as long as this bias term is not a function of :
Expand to see the proof
Actor Critic and GAE
Now that we have the baseline theorem, we can move on to the actor-critic algorithm. in the baseline theorem can be any function of state. The most common function of state in RL theory is the value function .
The intuition of the equation is also clear. The vanilla PG theorem uses the reward of entire trajectory to weigh every action. Actor-critic does this in a more fine-grained way. It weighs each action by how much more reward it can get than the average of all actions. This can be seen as an advantage of this action over the mean, so this term is also known as an advantage term.
Typically this is another network (the critic in Actor-Critic), so we use as its notation. We now need to know to calculate the advantage. There are two simple ways.
The first is to use Monte Carlo method. Since is the expectation of Reward-to-Go, we then simply let . This method has low bias but huge variance since we introduce the entire trajectory. The second way is temporal difference. Instead of writing out the entire future trajectory, TD methods bootstrap from the critic: . So the advantage can be written as . It has lower variance because it replaces most future rewards with a bootstrapped value estimate. Its error or bias comes from inaccuracies in the learned critic and, more generally, from function approximation and bootstrapping. The TD-error measures the gap between the critic’s current prediction and a one-step improved estimate.
GAE (Generalized Advantage Estimation) aims to strike a balance between the two methods. The core concept of GAE is to use a weighted sum of TD-errors to construct a low variance estimation for the advantage. The final expression is:
where is the TD-error, is the trajectory length and controls between Monte Carlo estimation (low bias, high variance) and one-step TD (high bias, low variance). If , becomes one-step TD. If , it reduces to the Monte Carlo advantage estimate .
Expand to see the proof
Define n-step bootstrapped Q-value as.
The corresponding -step advantage estimate is
For ,
For ,
Similarly, in general, Now we want to weighted sum all n-step advantages
Interchange the order of summation, we have
For finite trajectory, we have:
The above derivation proves the equivalence between GAE and the exponentially weighted average of n-step advantages under an infinite horizon. In practice, GAE is usually implemented recursively:
Expanding the recursion gives:
For infinite trajectories, this formulation is equivalent to the weighted n-step formulation. For finite trajectories, however, truncating the trajectory introduces different boundary effects depending on the formulation.
In the recursive implementation, the boundary effect appears through the last TD error:
If the trajectory terminates naturally, . If the trajectory is truncated due to a time limit, is retained for bootstrapping.
Natural Policy Gradient
Now let’s move on to the next stage. Both PG and Actor-Critic update parameter with gradient ascend , where is the learning rate. This update rule works well in supervised learning because the training labels are fixed, independent of the model being optimized. However, reinforcement learning is different: the learning signal itself depends on the current policy, since the collected trajectories and future rewards are generated by the policy being optimized. If learning rate is too big, the policy may “overshoot” to a region with worse performance. Since future training data are collected by this degraded policy, the learning signal can become less informative or even misleading, making recovery difficult. Using a very small learning rate makes training stable but inefficient. This can be more easily understood by formulating the gradient ascent as a optimization problem.
This means “Among all possible new parameters within a small neighborhood of the current policy, which direction gives the largest improvement in the objective?” This formulation is equivalent to the gradient ascending expression and the here is proportional to learning rate .
The problem arises in the 2-norm constraint. Proximity in parameter space does not necessarily mean proximity in policy space. Also, different directions in parameter space can have different effects on the resulting policy distribution. For the same numerical change, some dimension introduces greater changes than others. The norm only measures the magnitude of parameter changes and assumes that changes in all directions have the same importance. Therefore, we need a metric that measures the distance between policies rather than parameters. We can use the KL-divergence between old and new policy as the constraint.
KL distance can be expressed in Fisher information matrix when is around zero. , where , so the optimization problem becomes,
Solve this constrain problem and we can have the gradient update formula: . This method is called Natural Gradient.
Although natural gradient provides a more principled update direction, it still has several limitations. First, the derivation relies on a local quadratic approximation of the KL divergence. For large updates, this approximation can become inaccurate, causing the actual policy change to exceed the desired trust region. Second, the Fisher information matrix is expensive to compute and store for large neural networks, making direct matrix inversion impractical. Finally, natural gradient does not explicitly verify whether the update improves the policy, since the theoretical guarantee only holds under local assumptions, namely is not necessarily greater than .
TRPO
This is where TRPO comes into play. We first need a view shift. Let’s first review the basics. Both vanilla PG and natural gradient tries to solve the following optimization problem.
There are two different perspectives to understand this optimization problem.
The first perspective is the gradient optimization view: starting from the current policy , we iteratively update its parameters according to the estimated gradient:
This is the perspective vanilla policy gradient and natural gradient takes.
TRPO and PPO take the policy improvement perspective: given a current policy , we search for a new policy that improves upon it:
In this new perspective, we can directly perform to improve the policy. We first write out the detailed formulation of this target term
Expand to see full derivation
First, we expand
The first equation is because and the expectation averages over all initial states; the second equation is because is only dependent on instead of the whole trajectory, and the marginal distribution of from is still from .
Then we use a trick called telescoping sum,
Now get in the original target and rearrange the terms,
Although the derivation is somewhat involved, the resulting identity has a simple interpretation. It means that “the new policy is better than the old policy if it increases the probability of actions that are better than what the old policy expected.” The RHS can be viewed as how much the new policy improves over the old policy by accumulating the advantages of the actions taken by the new policy.
But we can’t directly optimize this target since the expectation is taken over the new policy distribution. We can’t sample from a policy if we haven’t known the policy. So the core idea of TRPO (and also PPO) is that can we use to optimize by controlling the distance between and . (i.e. can we collect data from old policy to get a new policy).
We first use importance sampling to reformulate the term
here is the discounted state visitation distribution. In the fourth equation, the discounting factor before the advantage term disappear because we absorb it into the state visitation distribution, which is a common hack.
TRPO proves that if is small, then
Therefore, we now have the constraint optimization problem for TRPO:
Now we can use gradient ascent method to solve this optimization similar to Natural Gradient.
PPO
TRPO successfully solves the key problem of policy gradient: how to prevent excessively large policy updates. However, this improvement comes at the cost of solving a constrained second-order optimization problem, which is computationally expensive for large neural networks.
PPO keeps the same intuition — staying close to the old policy — but replaces the complicated constraint with a simple first-order objective. There are two variants of PPO, PPO-Penalty and PPO-Clip.
PPO-Penalty places the KL constraint into the objective by adding a penalty/relaxation term. (like the Lagrangian primal-dual optimization).
There is no constraint anymore, so it can be optimized with standard first-order stochastic gradient methods, and use modern optimizer like ADAM. But there is still one hyperparameter .
PPO-Clip further relieves this burden by simply clipping the surrogate advantage when the likelihood ratio for a sampled action moves beyond the clipping interval. Define , clip the term when is too far from one. (which means new policy is too different from the old one).
When clipping happens, the target becomes which is independent of , hence zero gradient. We can have a more intuitive understanding by plotting a figure.

The intuition is clear from the figure. When , the sampled action is better than expected, so PPO encourages the new policy to increase its probability. However, excessively increasing this probability may lead to an overly large policy update. Therefore, when , the objective is clipped at , preventing further updates in this direction.
Conversely, when , the action is worse than expected, and PPO encourages the new policy to decrease its probability. Similarly, if the probability decreases too much (), the objective is clipped at , preventing an excessively large policy change.
In both cases, PPO only clips updates that move the policy too far in the direction encouraged by the advantage. Updates in the opposite direction are not restricted because they do not cause destructive policy changes.
PPO in Practice: A Minimal Continuous-Control Update
The equations above become easier to understand when mapped to a concrete implementation. The following code shows the smallest useful PPO update.
It assumes a continuous action space and a diagonal Gaussian policy. We first define the actor-critic model, then follow one batch of data through action sampling, GAE, and the PPO update.
Each action dimension has its own state-dependent mean and learned standard deviation. The rollout buffer stores the observations, sampled actions, rewards, episode-boundary information, old action log-probabilities, and old value predictions.
1. Define ActorCritic
The actor maps an observation to the parameters of a Gaussian action distribution, while the critic maps the same observation to a scalar state-value estimate. For a -dimensional action, the actor produces
The following minimal model uses separate MLPs for the actor and critic. The mean depends on the current observation. The log standard deviation is a learned parameter with one value per action dimension, but it does not depend on the observation.
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Normal
class ActorCritic(nn.Module):
def __init__(self, obs_dim, action_dim, hidden_dim=64):
super().__init__()
self.actor_mean = nn.Sequential(
nn.Linear(obs_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, action_dim),
)
self.critic = nn.Sequential(
nn.Linear(obs_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, 1),
)
# Learning log(std) is numerically easier than optimizing std directly.
self.log_std = nn.Parameter(torch.zeros(action_dim))
def forward(self, obs):
mean = self.actor_mean(obs)
std = self.log_std.exp().expand_as(mean)
dist = Normal(mean, std)
value = self.critic(obs).squeeze(-1)
return dist, value
The model can be initialized with
actor_critic = ActorCritic(obs_dim, action_dim)
optimizer = torch.optim.Adam(actor_critic.parameters(), lr=3e-4)
In PyTorch, calling
dist, value = actor_critic(obs)
invokes ActorCritic.forward(obs). If obs has shape [batch_size, obs_dim], then:
dist.meanhas shape[batch_size, action_dim]and represents .dist.stddevhas shape[batch_size, action_dim]and contains the broadcasted learned vector .valuehas shape[batch_size]and represents .
The exponential in self.log_std.exp() guarantees a positive standard deviation. A more expressive actor can predict a state-dependent log_std with another network head, but the state-independent version keeps the example focused on PPO itself.
2. Sample an Action with the Old Policy
During rollout collection, an action and its joint log-probability can be obtained as follows:
with torch.no_grad():
dist, value = actor_critic(obs)
action = dist.sample()
old_log_prob = dist.log_prob(action).sum(dim=-1)
For a multi-dimensional action , a diagonal Gaussian assumes conditional independence between action dimensions:
Therefore, its joint log-probability is the sum of the per-dimension log-probabilities:
The action, joint log-probability, and value prediction must all be stored before the optimizer changes the network. During the update, these stored quantities represent and .
For one transition, the rollout buffer conceptually stores
transition = {
"obs": obs,
"action": action,
"reward": reward,
"terminal": terminated,
"episode_end": terminated or truncated,
"old_log_prob": old_log_prob,
"old_value": value,
"next_value": next_value,
}
Here, terminal means that the underlying MDP truly ended, while episode_end also includes time-limit truncation. For a truncation, next_value should be evaluated on the final observation before the environment is reset.
3. Compute GAE and Critic Targets
The first step is to compute GAE and the corresponding value targets. This computation is performed without gradient tracking because the rollout is treated as fixed training data during the PPO update.
@torch.no_grad()
def compute_gae(
rewards,
terminals,
episode_ends,
values,
next_values,
gamma=0.99,
lam=0.95,
):
advantages = torch.zeros_like(rewards)
gae = torch.zeros_like(rewards[-1])
for t in reversed(range(len(rewards))):
bootstrap_mask = 1.0 - terminals[t].float()
trace_mask = 1.0 - episode_ends[t].float()
delta = (
rewards[t]
+ gamma * bootstrap_mask * next_values[t]
- values[t]
)
gae = delta + gamma * lam * trace_mask * gae
advantages[t] = gae
returns = advantages + values
return advantages, returns
The variable delta is the one-step TD error
and the backward recursion
is an efficient implementation of the GAE sum. bootstrap_mask decides whether the TD target may use the next-state value. trace_mask prevents the recursive advantage from leaking into the next reset episode. (Here a trace of reward is a rollout buffer that may contains multiple episodes.) They are the same for a true terminal state, but differ for a time-limit truncation: the truncated transition may bootstrap from its final observation, while the GAE trace must still stop at the episode boundary.
The function returns two different training signals:
advantages[t]tells the actor whether the sampled action was better or worse than the critic expected.returns[t] = advantages[t] + values[t]is the regression target used to train the critic.
Before optimization, a rollout with shape [rollout_steps, num_envs, ...] is usually flattened into a batch with shape [rollout_steps * num_envs, ...]. The observations and actions keep their final feature dimension, while log-probabilities, advantages, and returns become one-dimensional arrays.
4. Re-evaluate Stored Actions and Update the Networks
Once the advantages and returns have been computed, the same rollout is shuffled and reused for a small number of minibatch epochs:
def ppo_update(
actor_critic,
optimizer,
obs,
actions,
old_log_probs,
advantages,
returns,
*,
clip_eps=0.2,
value_coef=0.5,
entropy_coef=0.01,
update_epochs=4,
minibatch_size=64,
max_grad_norm=0.5,
target_kl=0.01,
):
# The rollout targets must remain fixed throughout all update epochs.
old_log_probs = old_log_probs.detach()
advantages = advantages.detach()
returns = returns.detach()
advantages = (advantages - advantages.mean()) / (
advantages.std(unbiased=False) + 1e-8
)
num_samples = obs.shape[0]
last_metrics = {}
for _ in range(update_epochs):
permutation = torch.randperm(num_samples, device=obs.device)
epoch_kls = []
for start in range(0, num_samples, minibatch_size):
idx = permutation[start : start + minibatch_size]
dist, values = actor_critic(obs[idx])
# Sum over action dimensions to obtain one joint log-probability
# and one joint entropy value for each transition.
new_log_probs = dist.log_prob(actions[idx]).sum(dim=-1)
entropy = dist.entropy().sum(dim=-1).mean()
# get r_{\theta'}
log_ratio = new_log_probs - old_log_probs[idx]
ratio = log_ratio.exp()
unclipped = ratio * advantages[idx]
clipped = torch.clamp(ratio, 1.0 - clip_eps, 1.0 + clip_eps)
clipped = clipped * advantages[idx]
# the minus here is because we want gradient ascent
policy_loss = -torch.min(unclipped, clipped).mean()
value_loss = 0.5 * F.mse_loss(values, returns[idx])
loss = policy_loss + value_coef * value_loss - entropy_coef * entropy
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(actor_critic.parameters(), max_grad_norm)
optimizer.step()
with torch.no_grad():
approx_kl = ((ratio - 1.0) - log_ratio).mean()
clip_fraction = ((ratio - 1.0).abs() > clip_eps).float().mean()
epoch_kls.append(approx_kl)
last_metrics = {
"policy_loss": policy_loss.item(),
"value_loss": value_loss.item(),
"entropy": entropy.item(),
"approx_kl": approx_kl.item(),
"clip_fraction": clip_fraction.item(),
}
# Clipping is not a hard trust-region constraint, so KL is still monitored.
if target_kl is not None and torch.stack(epoch_kls).mean() > target_kl:
break
return last_metrics
The inputs to ppo_update have the following roles:
obscontains the states collected by the old policy.actionscontains the exact Gaussian samples executed in those states. The actions are not resampled during optimization.old_log_probscontains the joint action log-probabilities saved during rollout collection.advantagescontains the fixed GAE estimates used by the actor.returnscontains the fixed value targets used by the critic.
At the beginning of each minibatch, actor_critic(obs[idx]) uses the latest parameters to create a new Gaussian distribution. Calling dist.log_prob(actions[idx]) then asks: under the current policy, how likely are the exact actions selected by the old policy? This old-versus-new comparison is what makes the probability ratio possible.
5. Read the Update Line by Line
The implementation contains the full PPO logic in a few operations:
old_log_probsare the log-probabilities recorded when the rollout was collected. They are detached and frozen, so they always represent during the update.new_log_probsare recomputed after every parameter update and represent the changing policy . For a diagonal Gaussian,dist.log_prob(actions)returns one log-probability per action dimension, so these values must be summed before the probability ratio is computed.- Subtracting the two joint log-probabilities and exponentiating gives the importance ratio:
torch.min(unclipped, clipped)implements the pessimistic surrogate objective. The minus sign converts policy-objective maximization into a loss that can be minimized by a standard optimizer.- The value loss trains the critic toward .
- The entropy bonus rewards a less concentrated action distribution and helps prevent exploration from collapsing too early.
- Advantage normalization changes the scale of the sampled gradient but not which samples have positive or negative advantage. It is a common stabilization technique rather than part of the theoretical PPO objective.
The three loss terms update different parts of the model:
policy_lossupdates the Gaussian actor throughmeanandlog_std.value_lossupdates the critic toward the GAE-based returns.- The entropy term discourages the Gaussian standard deviation from collapsing too early.
The negative sign in policy_loss and the negative entropy term come from using a minimization-based optimizer. Mathematically PPO maximizes the clipped objective and entropy, but PyTorch’s optimizer minimizes loss.
The rollout is reused for several epochs because environment interaction is expensive. However, after each optimizer step, the policy moves farther from the policy that generated the data. Too many epochs therefore make the batch increasingly off-policy. approx_kl and clip_fraction are useful diagnostics: a large approximate KL indicates a large overall policy update, while a large clip fraction means that many sampled ratios lie outside . Early stopping on KL provides an additional safeguard because clipping alone does not guarantee that the new policy remains inside a fixed trust region.
Expand to see details on KL approximation
The approx_kl is obtained with a trick from the original author of PPO (see this blog).
We can only use Monte-Calo to estimate this value. The most straight forward way is to use as estimator. This estimator is unbiased (obviously). But it has great variance since half of the sample is negative, whereas KL-divergence is always positive.
So here we introduce a substitute estimator , where . Taking expectation over this form,
So is unbiased estimator for KL-divergence, and you can easily prove that it’s always positive, hence smaller variance
After ppo_update finishes, the rollout should be discarded. A new rollout is then collected using the updated policy, its log-probabilities and value predictions are stored as the new “old” quantities, and the process repeats.
6. Bounded Actions: One Important Omission
One practical detail is omitted from the minimal code above: many continuous-control environments require bounded actions. Directly clipping a Gaussian sample to the environment bounds changes the executed action distribution but not the stored Gaussian log-probability, making the PPO ratio inconsistent. A common solution is to sample an unconstrained Gaussian variable , transform it with , scale it to the environment bounds, and include the transformation’s Jacobian correction when computing . PyTorch’s TransformedDistribution can handle this consistently. The old and new log-probabilities must always be computed for the same transformed action distribution.