Policy Gradient Methods: REINFORCE Algorithm & Theory

Michael BrenndoerferDecember 26, 202559 min read

Part of Language AI Handbook

Covers policy gradient theory for language model alignment. Topics include REINFORCE algorithm, variance reduction with baselines, and foundations for PPO.

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Policy Gradient Methods

With a trained reward model that captures human preferences, we face a basic challenge: how do we use this reward signal to improve our language model? The reward model tells us whether a complete generated response is good or bad, but our language model makes decisions token by token, one word at a time. We need a way to propagate that final reward signal back through the entire generation process to influence all of the individual token choices that collectively produced the outcome.

This challenge has a name in machine learning: the credit assignment problem. When a student gets a high score on an exam, you want to reinforce the study habits that led to that success, but determining which specific habits mattered most is hard. Language model alignment faces the same difficulty. A response might consist of hundreds of tokens, and only some of those tokens made the response good. The reward model scores the whole response, not individual tokens. We need a principled way to distribute credit across all the decisions that contributed to the outcome.

This is precisely where reinforcement learning enters the picture. Unlike supervised learning, where we have explicit targets for each prediction (we know the correct next token at every position), reinforcement learning can work with sparse feedback that arrives after an entire trajectory rather than at each step. Policy gradient methods provide a mathematically principled framework for optimizing a model's behavior based on exactly this kind of reward signal.

The core idea behind policy gradients is elegant in its simplicity. Think of it as a trial-and-error learning system operating at the level of complete responses. The model generates a response, receives a reward signal, and then adjusts its parameters to make similar responses more (or less) likely in the future depending on whether the reward was positive or negative. Over thousands of such updates, the model learns to generate the kinds of responses that tend to receive high rewards. The mathematical machinery we develop in this chapter makes this intuition precise and computationally tractable.

In this chapter, we develop the theory behind policy gradients from first principles, derive the key mathematical results that make training possible, and implement the REINFORCE algorithm. Along the way, we encounter the variance problem that makes naive REINFORCE impractical at scale, and we develop variance reduction techniques including baselines, advantages, and reward-to-go. This foundation is needed for understanding PPO and the complete RLHF pipeline that follows in subsequent chapters.

Historical Context

Policy gradient methods have a long history in reinforcement learning, predating their application to language models by decades. Ronald Williams introduced the REINFORCE algorithm in 1992, recognizing that the gradient of expected reward could be estimated from Monte Carlo samples. The key mathematical tool he used, the log-derivative trick (also called the likelihood ratio trick), had been known in statistics for years under different names.

Throughout the 1990s and 2000s, policy gradients were applied primarily to robotics and game playing, but their high variance made them impractical for many tasks. Actor-critic methods, which combine policy gradients with value function estimation, helped control variance. Trust region methods like TRPO (2015) and PPO (2017) further tamed the instability by constraining how much the policy could change per update.

The application to language model alignment came much later. InstructGPT (2022) was one of the first published large-scale systems to use PPO for aligning language models with human preferences, building directly on the theoretical foundations Williams established thirty years earlier. The underlying policy gradient theorem is the same; what changed was the scale, the reward model, and the engineering required to make it work with billion-parameter models.

Language Models as Policies

In reinforcement learning terminology, a policy is a function that maps states to actions. For language models, this mapping has a natural interpretation that becomes clear once we examine how text generation works. When you ask a language model to complete a sentence, it does not produce the entire response instantaneously. Instead, it generates one token at a time, each choice depending on everything that came before. This sequential decision-making process is precisely what reinforcement learning was designed to handle.

Think of a language model as an author writing a letter one word at a time. At each moment, the author has a specific context in mind (the prompt and everything written so far), and from that context, must choose the next word. Some words are clearly more appropriate given the context; others would seem out of place. The author's internal sense of which words fit a context is exactly what a language model encodes as a probability distribution. That probability distribution is the policy.

Policy

A policy πθ(a∣s)\pi_\theta(a|s) is a probability distribution over actions aa given state ss, parameterized by θ\theta. In language models, the policy is the model itself: given a context (state), it produces a probability distribution over the next token (action).

To understand why this framing is so powerful, consider what happens during text generation. The model receives a prompt, which establishes the initial context. Based on this context, the model must decide which token to produce first. This decision changes the context, as the newly generated token becomes part of the history. The model then faces a new decision: given the original prompt plus the first generated token, what should the second token be? This process continues, with each token choice altering the state and presenting a fresh decision problem.

The reinforcement learning vocabulary for this process is precise and useful. At each timestep tt, the system occupies a state sts_t, the agent takes an action ata_t, the state transitions to st+1s_{t+1}, and eventually the agent receives a reward RR. For language generation, this maps directly:

  • State sts_t: The current context, consisting of the prompt plus all tokens generated so far
  • Action ata_t: The next token to generate
  • Policy πθ(at∣st)\pi_\theta(a_t|s_t): The probability the model assigns to token ata_t given context sts_t

This framing reveals why language model alignment is fundamentally a reinforcement learning problem. At each timestep, the model takes an action (selects a token), the state changes (the context grows), and eventually the complete sequence receives a reward from our reward model. The challenge is learning which tokens led to that reward. Unlike a game where individual moves might receive immediate feedback, language generation only reveals whether the response was good or bad after the entire sequence is complete. This delayed reward structure is exactly the scenario where reinforcement learning excels.

One important property of this MDP (Markov Decision Process) formulation is that the state transition is deterministic given the action. Once we pick token yty_t, the new state is simply the old state with yty_t appended. There is no external randomness in the transition. All the stochasticity lives in the policy: the model samples tokens from its probability distribution. This simplification makes the mathematics more tractable than general RL settings where transitions are random.

In[3]:
Code
import torch.nn as nn
import torch.nn.functional as F


# A simple language model acting as a policy
class LanguageModelPolicy(nn.Module):
    def __init__(self, vocab_size, hidden_size=256, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, hidden_size)
        self.lstm = nn.LSTM(
            hidden_size, hidden_size, num_layers, batch_first=True
        )
        self.output = nn.Linear(hidden_size, vocab_size)

    def forward(self, input_ids):
        # State: all tokens seen so far
        embeds = self.embedding(input_ids)
        lstm_out, _ = self.lstm(embeds)
        # Action distribution: probability over next token
        logits = self.output(lstm_out)
        return logits

    def get_action_probs(self, state):
        """Get probability distribution over actions (next tokens)"""
        logits = self.forward(state)
        # Policy: pi_theta(a|s) - probability of each action given state
        return F.softmax(logits[:, -1, :], dim=-1)
Out[4]:
Visualization
Diagram showing states expanding as tokens are added, with actions connecting consecutive states and reward at the end.
Sequential token generation modeled as a Markov Decision Process. Each state $s_t$ encompasses the full context (prompt and generated history), while actions $a_t$ correspond to next-token selections. The visualization highlights the temporal gap between individual actions and the final sparse reward $R$, illustrating the credit assignment challenge that makes language model alignment a reinforcement learning problem.

The model's parameters θ\theta define the policy. Our goal is to adjust these parameters so that the model generates responses that receive high rewards. This means we need to find parameter values that make high-reward sequences more probable and low-reward sequences less probable. The question is: how do we know which direction to adjust the parameters? This is the central technical question that policy gradient theory answers.

The Objective Function

To optimize our policy, we need a clear mathematical objective. In RLHF, we want to maximize the expected reward over all possible responses the model might generate. This expectation is important because language generation is inherently stochastic. The same prompt can lead to many different responses depending on which tokens get sampled at each step. Some responses will be excellent, others will be mediocre, and still others will be poor. We want to tune the model so that, on average, it produces responses that receive high rewards.

Think of the objective function as a report card for a policy, but one computed by averaging over all possible exams the policy might face. A policy scores well on this report card if, when we sample random responses from it, those responses tend to be good. The objective does not ask that every response be perfect; it asks that the distribution over responses be skewed toward high-quality outcomes.

Given a prompt xx, the model generates a response y=(y1,y2,…,yT)y = (y_1, y_2, \ldots, y_T) by sampling tokens according to its policy. The reward model then scores the complete response, giving us R(x,y)R(x, y). Our objective is to find parameters θ\theta that maximize the expected reward:

J(θ)=Ey∼πθ(⋅∣x)[R(x,y)]J(\theta) = \mathbb{E}_{y \sim \pi_\theta(\cdot|x)}[R(x, y)]

where:

  • J(θ)J(\theta): the objective function we want to maximize
  • θ\theta: the parameters of the language model
  • E\mathbb{E}: the expectation over sequences sampled from the policy
  • πθ(⋅∣x)\pi_\theta(\cdot|x): the policy (language model) distribution conditioned on prompt xx
  • R(x,y)R(x, y): the reward scalar for the generated response yy

This expectation is taken over all possible responses the model could generate. Some responses will be excellent (high reward), others mediocre, others poor. The expectation weights each by its probability under the current policy. This weighting matters: if the model rarely generates a particular response, that response contributes little to the expected value, even if it happens to have high reward. A policy that occasionally produces brilliant responses but usually produces mediocre ones will score lower than a policy that consistently produces good responses.

We can expand this expectation using the probability of generating the complete sequence:

J(θ)=∑yπθ(y∣x)R(x,y)J(\theta) = \sum_{y} \pi_\theta(y|x) R(x, y)

where:

  • ∑y\sum_{y}: summation over all possible output sequences in the vocabulary
  • πθ(y∣x)\pi_\theta(y|x): probability of generating sequence yy given prompt xx
  • R(x,y)R(x, y): reward for the specific sequence yy

This summation form makes the objective's structure transparent, but it also reveals a computational challenge. The number of possible sequences is astronomically large: for a vocabulary of 50,000 tokens and sequences of length 100, there are 5000010050000^{100} possible sequences. We cannot enumerate them all. We need a way to optimize this objective without explicitly summing over all sequences.

The probability πθ(y∣x)\pi_\theta(y|x) represents the likelihood of generating the entire response yy. It is computed as the product of individual token probabilities:

πθ(y∣x)=∏t=1Tπθ(yt∣x,y<t)\pi_\theta(y|x) = \prod_{t=1}^{T} \pi_\theta(y_t | x, y_{<t})

where:

  • TT: the length of the generated sequence
  • yty_t: the token generated at timestep tt
  • y<ty_{<t}: the history of tokens generated before step tt, that is (y1,y2,…,yt−1)(y_1, y_2, \ldots, y_{t-1})
  • πθ(yt∣x,y<t)\pi_\theta(y_t | x, y_{<t}): the probability the model assigns to token yty_t given the full context

This autoregressive factorization, which we have seen throughout our discussion of language models in earlier chapters, connects the policy formulation directly to how transformers and LSTMs generate text. Each factor in the product corresponds to one step of the generation process, one call to the model's forward function, one decision about which token to emit next. The full sequence probability is simply the product of all these individual decisions.

Why Direct Optimization Is Hard

You might wonder why we cannot just directly maximize J(θ)J(\theta) by computing its gradient. The obstacle is that J(θ)J(\theta) involves an expectation over sequences sampled from πθ\pi_\theta, and sampling is not a differentiable operation. When we sample a token from a probability distribution, we make a discrete choice, and discrete choices are not differentiable with respect to the parameters that shape that distribution.

Think of it this way: if you tweak the parameters of the model slightly so that the probability of token "excellent" increases by 0.001, you cannot directly measure how that change affects the expected reward. The reward depends on which specific tokens get sampled, and sampling is random. We need a mathematical trick to convert the gradient of an expectation into an expectation of gradients that we can estimate from samples.

The Policy Gradient Theorem

Here is the core challenge stated precisely: we need to compute ∇θJ(θ)\nabla_\theta J(\theta) so we can update our parameters via gradient ascent. But the expectation involves sampling from the policy itself, which depends on θ\theta. This creates a circular dependency that seems difficult to resolve. The gradient depends on how changing θ\theta affects which sequences get sampled, but sampling is a discrete, non-differentiable operation. We cannot simply backpropagate through the sampling process the way we would through a continuous neural network layer.

The policy gradient theorem provides an elegant solution to this problem. Rather than trying to differentiate through sampling directly, it rewrites the gradient in a form that we can estimate using samples from the current policy. The key insight is that we can express the gradient of an expectation in terms of an expectation of gradients that we can compute.

Deriving the Policy Gradient

Let's derive the policy gradient step by step. Starting with our objective written as an explicit sum:

J(θ)=∑yπθ(y∣x)R(x,y)J(\theta) = \sum_{y} \pi_\theta(y|x) R(x, y)

We take the gradient with respect to θ\theta. Since R(x,y)R(x,y) is produced by a frozen reward model and does not depend on θ\theta, only the policy probability is differentiated:

∇θJ(θ)=∑y∇θπθ(y∣x)⋅R(x,y)\nabla_\theta J(\theta) = \sum_{y} \nabla_\theta \pi_\theta(y|x) \cdot R(x, y)

This is still a sum over all sequences, which we cannot compute directly. We need to transform it into an expectation. The trick is to multiply and divide by πθ(y∣x)\pi_\theta(y|x), introducing a factor of 1 without changing the value:

∇θJ(θ)=∑yπθ(y∣x)⋅∇θπθ(y∣x)πθ(y∣x)⋅R(x,y)\nabla_\theta J(\theta) = \sum_{y} \pi_\theta(y|x) \cdot \frac{\nabla_\theta \pi_\theta(y|x)}{\pi_\theta(y|x)} \cdot R(x, y)

Now we apply the log-derivative trick (also called the likelihood ratio trick or REINFORCE trick). This technique appears throughout machine learning and statistics whenever we need to compute gradients of expectations. The core identity is:

∇θπθ(y∣x)πθ(y∣x)=∇θlog⁡πθ(y∣x)\frac{\nabla_\theta \pi_\theta(y|x)}{\pi_\theta(y|x)} = \nabla_\theta \log \pi_\theta(y|x)

This follows directly from the chain rule of differentiation. Recall that ddθlog⁡f(θ)=1f(θ)dfdθ\frac{d}{d\theta} \log f(\theta) = \frac{1}{f(\theta)} \frac{df}{d\theta}, which rearranges to df/dθf(θ)=ddθlog⁡f(θ)\frac{df/d\theta}{f(\theta)} = \frac{d}{d\theta} \log f(\theta). The logarithm converts a multiplicative relationship into an additive one, which proves extremely useful when we deal with sequence probabilities that are products of many terms.

Substituting back:

∇θJ(θ)=∑yπθ(y∣x)⋅∇θlog⁡πθ(y∣x)⋅R(x,y)\nabla_\theta J(\theta) = \sum_{y} \pi_\theta(y|x) \cdot \nabla_\theta \log \pi_\theta(y|x) \cdot R(x, y)

A sum over all possible sequences weighted by their probabilities is exactly an expectation under the policy:

∇θJ(θ)=Ey∼πθ[∇θlog⁡πθ(y∣x)⋅R(x,y)]\nabla_\theta J(\theta) = \mathbb{E}_{y \sim \pi_\theta}[\nabla_\theta \log \pi_\theta(y|x) \cdot R(x, y)]

where:

  • ∇θJ(θ)\nabla_\theta J(\theta): gradient of the objective function with respect to parameters
  • ∇θlog⁡πθ(y∣x)\nabla_\theta \log \pi_\theta(y|x): gradient of the log-probability of the sequence, called the "score function"
  • R(x,y)R(x, y): the reward, acting as a scalar weight on the gradient

This is the policy gradient theorem for our setting. It tells us something remarkable: to compute the gradient of expected reward, we can sample sequences from our policy, compute the gradient of their log-probabilities, and multiply by their rewards. The score function ∇θlog⁡πθ(y∣x)\nabla_\theta \log \pi_\theta(y|x) points in the direction in parameter space that would increase the probability of sequence yy. Multiplying by the reward scales this direction: high rewards give large positive contributions, low rewards give small or negative contributions.

Policy Gradient Theorem

The gradient of expected reward can be written as an expectation:

∇θJ(θ)=Ey∼πθ[∇θlog⁡πθ(y∣x)⋅R(x,y)]\nabla_\theta J(\theta) = \mathbb{E}_{y \sim \pi_\theta}[\nabla_\theta \log \pi_\theta(y|x) \cdot R(x, y)]

where:

  • ∇θJ(θ)\nabla_\theta J(\theta): gradient of the objective function
  • Ey∼πθ\mathbb{E}_{y \sim \pi_\theta}: expectation over sequences sampled from the current policy
  • ∇θlog⁡πθ(y∣x)\nabla_\theta \log \pi_\theta(y|x): gradient of the log-probability (the score function)
  • R(x,y)R(x, y): reward of the sampled sequence

This turns an intractable sum over all possible sequences into a tractable Monte Carlo estimate using samples from the policy.

The transformation from an intractable sum to a Monte Carlo estimate is the key practical insight. We cannot enumerate all possible sequences; for even modest vocabulary sizes and sequence lengths, this would involve more sequences than atoms in the universe. But we can sample sequences from our policy, and the policy gradient theorem tells us that averaging over these samples gives us an unbiased estimate of the true gradient.

The unbiasedness guarantee is important. It means that if we sample enough sequences, our gradient estimate will converge to the true gradient. We might not get the exact gradient from a single sample, but in expectation across many samples, we get the right answer. This is what allows reinforcement learning to work despite the discrete, non-differentiable nature of sampling.

Out[5]:
Visualization
Scatter plot showing sampled sequences with arrows showing gradient direction based on reward sign.
Policy gradient update directions in the probability-reward plane. Positive rewards (green) generate gradients that push to increase the log-probability of the sampled sequence, while negative rewards (orange) generate gradients that push to decrease it. The magnitude of each update, represented by arrow length, scales proportionally with the absolute value of the reward, creating stronger learning signals from more extreme outcomes.

From Sequences to Tokens

For language models, we need to express the log-probability of an entire sequence in terms of individual token log-probabilities. This connection is needed because our models compute token-level probabilities, not sequence-level probabilities directly. Fortunately, the autoregressive structure of language models makes this conversion straightforward.

Using the autoregressive factorization from the previous section, we can apply the logarithm to convert the product of probabilities into a sum of log-probabilities:

log⁡πθ(y∣x)=log⁡∏t=1Tπθ(yt∣x,y<t)=∑t=1Tlog⁡πθ(yt∣x,y<t)\log \pi_\theta(y|x) = \log \prod_{t=1}^{T} \pi_\theta(y_t | x, y_{<t}) = \sum_{t=1}^{T} \log \pi_\theta(y_t | x, y_{<t})

where:

  • ∑t=1T\sum_{t=1}^{T}: sum over all timesteps in the sequence
  • log⁡πθ(yt∣x,y<t)\log \pi_\theta(y_t | x, y_{<t}): log-probability of the specific token yty_t given context

The logarithm converts the product of probabilities into a sum of log-probabilities. This is where the log-derivative trick pays off: instead of dealing with products of many small numbers (which can underflow numerically when sequence lengths are in the hundreds), we work with sums of log-probabilities, which remain in a numerically stable range.

Each term log⁡πθ(yt∣st)\log \pi_\theta(y_t | s_t) is simply the log-softmax output of our model at position tt, evaluated at the selected token. This is exactly what we compute during the forward pass when training language models with cross-entropy loss. When the model processes a sequence, it produces logits for every position, and applying log-softmax gives us log-probabilities. To compute the log-probability of a specific token choice, we simply index into this log-probability vector at the chosen token's index.

The gradient of the full sequence log-probability then follows from the linearity of differentiation. The gradient of a sum is the sum of gradients:

∇θlog⁡πθ(y∣x)=∑t=1T∇θlog⁡πθ(yt∣st)\nabla_\theta \log \pi_\theta(y|x) = \sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(y_t | s_t)

where:

  • sts_t: the state (context) at timestep tt, equivalent to (x,y<t)(x, y_{<t})
  • ∇θlog⁡πθ(yt∣st)\nabla_\theta \log \pi_\theta(y_t | s_t): gradient of the log-probability for a single token decision

Each term in the sum corresponds to one token's contribution to the overall sequence probability. We compute each term by running a standard backward pass through the model at that timestep.

Putting this together, the policy gradient becomes a sum over token-level contributions, all weighted by the same sequence-level reward:

∇θJ(θ)=Ey∼πθ[∑t=1T∇θlog⁡πθ(yt∣st)⋅R(x,y)]\nabla_\theta J(\theta) = \mathbb{E}_{y \sim \pi_\theta}\left[\sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(y_t | s_t) \cdot R(x, y)\right]

where:

  • ∑t=1T∇θlog⁡πθ(yt∣st)\sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(y_t | s_t): the accumulated gradient from all tokens in the sequence
  • R(x,y)R(x, y): the final reward, which scales the gradient of every token choice equally

The same reward R(x,y)R(x, y) multiplies every token's gradient. This is the basic credit assignment mechanism of REINFORCE: even though we only receive reward at the end, it propagates back to influence all token choices. Every token that contributed to the response receives the same credit or blame for the final outcome. This is both a strength and a weakness of the basic approach. It allows learning from sparse rewards, but it does not distinguish between tokens that were important for the reward and tokens that were irrelevant to it.

Out[6]:
Visualization
Line plot of per-token log-probabilities showing negative values at each timestep position in the sequence.
Per-token log-probabilities for a single generated sequence, showing individual $\log \pi(y_t|s_t)$ values at each position. All values are negative since probabilities lie in (0,1), and more surprising token choices (lower probability) produce more negative values.
Line plot of cumulative log-probability decreasing as more tokens are added, with annotation of the final value.
Cumulative log-probability accumulating across token positions. The final cumulative value equals the log-probability of the entire sequence and determines the magnitude of the gradient update in the REINFORCE algorithm.

The REINFORCE Algorithm

The policy gradient theorem gives us a theoretical result: the gradient of expected reward equals the expected product of the score function and the reward. REINFORCE (also called the Monte Carlo policy gradient) is the algorithm that turns this theory into practice through Monte Carlo estimation. The name comes from the idea that we reinforce behaviors that lead to good outcomes: when a sequence receives high reward, we reinforce the probability of generating that exact sequence.

Think of REINFORCE as a simple loop: try something, evaluate whether it worked, update your approach based on the result. A human learning to write better essays follows essentially the same process. They write an essay, receive feedback (reward), and adjust their writing style to emphasize the patterns that received positive feedback and reduce the patterns that received criticism. REINFORCE formalizes this intuition mathematically.

The algorithm proceeds in four steps for each training iteration:

  1. Sample a complete sequence from the current policy
  2. Compute the reward for that sequence using the reward model
  3. For each token in the sequence, compute the gradient of its log-probability scaled by the reward
  4. Sum these token-level gradients and update parameters via gradient ascent

In practice, we implement gradient ascent by minimizing the negative objective (gradient descent on the negation):

In[7]:
Code
def reinforce_loss(log_probs, reward):
    """
    Compute REINFORCE loss for a single sequence.

    Args:
        log_probs: Log probabilities of each selected token, shape (seq_len,)
        reward: Scalar reward for the complete sequence

    Returns:
        Loss to minimize (negative of policy gradient objective)
    """
    # Sum log probs to get log pi(y|x)
    log_prob_sequence = log_probs.sum()

    # REINFORCE: maximize E[log pi(y|x) * R]
    # For gradient descent, we minimize the negative
    loss = -log_prob_sequence * reward

    return loss

The reinforce_loss function captures the entire algorithm in two lines of logic. We sum the per-token log-probabilities to get the sequence log-probability, then multiply by the reward and negate (to turn maximization into minimization). When PyTorch computes gradients of this loss with respect to model parameters, it produces exactly the policy gradient we derived above.

To use this loss, we first need to generate a sequence and collect the log-probabilities of each chosen token:

In[8]:
Code
import torch
import torch.nn.functional as F


def generate_and_get_log_probs(
    model, prompt_ids, max_length=20, vocab_size=1000
):
    """Generate a sequence and collect log probabilities."""
    model.eval()

    current_ids = prompt_ids.clone()
    log_probs = []

    with torch.no_grad():
        for _ in range(max_length):
            # Get action probabilities
            logits = model(current_ids)
            probs = F.softmax(logits[:, -1, :], dim=-1)

            # Sample action (next token)
            action = torch.multinomial(probs, 1)

            # Store log probability of selected action
            log_prob = torch.log(probs.gather(1, action))
            log_probs.append(log_prob)

            # Update state
            current_ids = torch.cat([current_ids, action], dim=1)

    return current_ids, torch.cat(log_probs, dim=1).squeeze()

Notice the use of torch.no_grad() during generation. We generate the sequence without tracking gradients, then separately compute the loss and allow gradients to flow through that computation. This is a memory optimization: we do not need to store the full computation graph during generation, only during the loss computation.

A Worked Numerical Example

Let's trace through a concrete numerical example to build intuition. Suppose our model generates a three-token response, and at each position the model assigns the following probabilities to the token it chose:

  • Token 1: probability 0.30 (for example, "The")
  • Token 2: probability 0.50 (for example, "answer")
  • Token 3: probability 0.20 (for example, "is")

The reward model gives this short response a reward of R=+2.0R = +2.0.

Step 1: Compute per-token log-probabilities.

log⁡π(y1∣s1)=log⁡(0.30)≈−1.204log⁡π(y2∣s2)=log⁡(0.50)≈−0.693log⁡π(y3∣s3)=log⁡(0.20)≈−1.609\begin{aligned} \log \pi(y_1|s_1) &= \log(0.30) \approx -1.204 \\ \log \pi(y_2|s_2) &= \log(0.50) \approx -0.693 \\ \log \pi(y_3|s_3) &= \log(0.20) \approx -1.609 \end{aligned}

Step 2: Sum to get the sequence log-probability.

log⁡πθ(y∣x)=−1.204+(−0.693)+(−1.609)=−3.506\log \pi_\theta(y|x) = -1.204 + (-0.693) + (-1.609) = -3.506

This corresponds to a sequence probability of e−3.506≈0.030e^{-3.506} \approx 0.030, which matches 0.30×0.50×0.20=0.0300.30 \times 0.50 \times 0.20 = 0.030 directly.

Step 3: Compute the REINFORCE loss.

L=−log⁡πθ(y∣x)⋅R=−(−3.506)×2.0=+7.012\mathcal{L} = -\log \pi_\theta(y|x) \cdot R = -(-3.506) \times 2.0 = +7.012

Step 4: Interpret the gradient.

When we call loss.backward(), the gradient will be computed with respect to model parameters. This gradient points in the direction that would increase the loss, which means the optimizer (doing gradient descent to minimize the loss) will adjust parameters in the direction that decreases the loss. Decreasing the loss means increasing log⁡πθ(y∣x)⋅R\log \pi_\theta(y|x) \cdot R, which means increasing the log-probability of generating this sequence. Since R=+2.0R = +2.0 is positive, this is exactly what we want: after the update, the model is more likely to generate this sequence.

Negative reward case: Suppose instead R=−1.5R = -1.5. Then the loss equals −(−3.506)×(−1.5)=−5.259-(-3.506) \times (-1.5) = -5.259. Minimizing (making more negative) this loss requires making −log⁡π⋅R-\log \pi \cdot R decrease. Since R=−1.5<0R = -1.5 < 0, decreasing −log⁡π⋅(−1.5)-\log \pi \cdot (-1.5) means decreasing log⁡π\log \pi, making the sequence less likely. This is again the correct behavior: negative reward makes the sequence less probable.

In[9]:
Code
import torch

# Demonstration: REINFORCE gradient direction
log_probs = torch.log(torch.tensor([0.3, 0.5, 0.2]))
log_prob_total = log_probs.sum()

# Positive reward: gradient increases sequence probability
reward_positive = 2.0
loss_positive = -log_prob_total * reward_positive

# Negative reward: gradient decreases sequence probability
reward_negative = -1.5
loss_negative = -log_prob_total * reward_negative
Out[10]:
Console
Log probability of sequence: -3.507
Sequence probability (exp): 0.0300

With positive reward (+2.0): loss = 7.013
  -> Minimizing this loss increases sequence probability

With negative reward (-1.5): loss = -5.260
  -> Minimizing this loss decreases sequence probability

The key insight is that REINFORCE increases the probability of sequences that receive high rewards and decreases the probability of sequences that receive low or negative rewards. Over many updates, the policy gradually shifts to favor the kinds of responses that the reward model prefers. This is the essence of learning from rewards: try things, evaluate what works, do more of what works.

The Variance Problem

REINFORCE has a fundamental weakness that prevents it from being directly used for training large language models: extremely high variance in its gradient estimates. This variance explains why every practical RLHF system uses more sophisticated algorithms. It directly motivates PPO, which we cover in the next chapter.

To understand where the variance comes from, recall our Monte Carlo gradient estimate:

∇θJ(θ)≈1N∑i=1N∇θlog⁡πθ(y(i)∣x)⋅R(x,y(i))\nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i=1}^{N} \nabla_\theta \log \pi_\theta(y^{(i)}|x) \cdot R(x, y^{(i)})

where:

  • NN: the batch size (number of sampled sequences)
  • y(i)y^{(i)}: the ii-th sampled sequence in the batch
  • R(x,y(i))R(x, y^{(i)}): the reward for the ii-th sequence

We are estimating the true gradient by averaging over NN samples. If N=1N = 1, our estimate equals the product of a random gradient direction and a random reward. Both quantities vary across samples, and their product varies even more. Several sources contribute to the overall variance:

Sampling variance. We estimate an expectation using a small batch of samples. Different samples from the same policy can have wildly different rewards. Consider a prompt where some responses are brilliant (reward +10) and others are terrible (reward -10), but most are mediocre (reward near 0). If we happen to sample one of the rare brilliant responses, we get a huge positive gradient. If we sample a terrible one, we get a huge negative gradient. The expected gradient might be small and stable, but individual estimates can be orders of magnitude larger.

Reward magnitude. Large rewards amplify gradient noise. A reward of 100 versus 0.1 changes gradient magnitudes by a factor of 1000. If the reward scale varies across prompts or across training, the gradient scale varies too. This makes it hard to choose a stable learning rate that works well across all prompts.

Long sequences. With more tokens, there is more opportunity for randomness in the sampling process. Each token choice is stochastic, and these random choices compound. The same prompt can lead to vastly different sequences purely due to sampling randomness early in generation. A small difference in the first few tokens can cascade into completely different responses.

To see why the variance matters practically, imagine training a model with batch size 1 on a task where rewards range from -10 to +10. One batch might produce a sample with reward +8, causing a large positive gradient update. The next batch produces a sample with reward -7, causing a large negative gradient update. The model is being pushed back and forth, making little net progress. It is like trying to navigate by compass in a room where someone randomly rotates the compass between readings.

In[11]:
Code
import numpy as np

# Simulate variance in REINFORCE estimates
np.random.seed(42)


def simulate_reinforce_variance(
    n_samples_per_estimate, true_gradient=1.0, reward_std=5.0
):
    """Simulate variance in gradient estimates."""
    # Simulate rewards with high variance
    rewards = np.random.normal(
        loc=true_gradient, scale=reward_std, size=n_samples_per_estimate
    )
    # Each estimate is the mean of samples
    estimate = np.mean(rewards)
    return estimate


# Compare variance with different batch sizes
batch_sizes = [1, 4, 16, 64, 256]
n_estimates = 200

variance_by_batch = {}
for batch_size in batch_sizes:
    estimates = [
        simulate_reinforce_variance(batch_size) for _ in range(n_estimates)
    ]
    variance_by_batch[batch_size] = np.var(estimates)
Out[12]:
Visualization
Bar chart showing gradient estimate variance decreasing from about 25 to near 0.1 as batch size increases.
REINFORCE gradient estimate variance as a function of batch size. Variance decreases following a $1/N$ scaling law, requiring exponentially more samples to achieve linear improvements in stability. Even at batch size 256, variance remains substantial compared to supervised learning objectives, showing the basic sample inefficiency of Monte Carlo policy gradient estimation.

The variance decreases at rate O(1/N)O(1/N) with batch size NN, but this means we need exponentially more samples to achieve linear improvements in stability. To reduce variance by a factor of 10, we need 10 times more samples. To reduce it by a factor of 100, we need 100 times more samples. For large language models where each sample requires generating a complete response (potentially hundreds of tokens) through a billion-parameter model, and where each generated response must be scored by a separate reward model, this sample inefficiency becomes a serious practical barrier.

The practical consequence is that naive REINFORCE is essentially unusable for training large language models. The gradient noise dominates the signal, training progress is erratic, and the compute cost is prohibitive. This motivates variance reduction techniques, which we develop in the following sections.

Variance Reduction with Baselines

The main point for reducing variance comes from a useful mathematical property of the policy gradient: subtracting any constant from the reward does not change the expected gradient, but it can substantially reduce variance. This property allows us to center the rewards around zero. This provides clearer learning signals, without introducing any bias into our gradient estimates.

To understand why centering helps, consider the extreme case where all rewards are positive, say ranging from +5 to +15. Without centering, every gradient update pushes to increase the probability of all sampled sequences. The model receives mixed signals: "increase this sequence, increase that sequence too, increase everything." The optimizer cannot easily distinguish which responses are better than others because every response receives a positive gradient push, just of different magnitudes.

With a baseline of 10 (the approximate mean), rewards become -5 to +5. Now the gradient clearly distinguishes: "increase sequences above the baseline, decrease sequences below it." Sequences that scored 14 get positive reinforcement; sequences that scored 6 get negative reinforcement. This clearer signal leads to faster learning and lower variance in gradient estimates.

Consider the gradient estimator with a baseline bb:

∇θJ(θ)=Ey∼πθ[∇θlog⁡πθ(y∣x)⋅(R(x,y)−b)]\nabla_\theta J(\theta) = \mathbb{E}_{y \sim \pi_\theta}[\nabla_\theta \log \pi_\theta(y|x) \cdot (R(x, y) - b)]

where:

  • bb: a baseline value (which must be independent of the action yy, though it can depend on the state xx)
  • (R(x,y)−b)(R(x, y) - b): the "centered" reward, which reduces variance by removing the mean level

To verify this does not change the expected gradient, we need to show that the baseline term contributes zero in expectation. We check this by computing the expected value of the baseline term directly:

Ey∼πθ[∇θlog⁡πθ(y∣x)⋅b]=b⋅Ey∼πθ[∇θlog⁡πθ(y∣x)]\mathbb{E}_{y \sim \pi_\theta}[\nabla_\theta \log \pi_\theta(y|x) \cdot b] = b \cdot \mathbb{E}_{y \sim \pi_\theta}[\nabla_\theta \log \pi_\theta(y|x)]

We pulled bb outside the expectation because it does not depend on yy. Now we need to evaluate the expected value of the score function ∇θlog⁡πθ(y∣x)\nabla_\theta \log \pi_\theta(y|x). This expectation is always zero:

Ey∼πθ[∇θlog⁡πθ(y∣x)]=∑yπθ(y∣x)∇θπθ(y∣x)πθ(y∣x)(expand using log derivative)=∑y∇θπθ(y∣x)(cancel πθ)=∇θ∑yπθ(y∣x)(linearity of gradient)=∇θ1(probabilities sum to 1)=0(derivative of constant)\begin{aligned} \mathbb{E}_{y \sim \pi_\theta}[\nabla_\theta \log \pi_\theta(y|x)] &= \sum_y \pi_\theta(y|x) \frac{\nabla_\theta \pi_\theta(y|x)}{\pi_\theta(y|x)} && \text{(expand using log derivative)} \\ &= \sum_y \nabla_\theta \pi_\theta(y|x) && \text{(cancel } \pi_\theta \text{)} \\ &= \nabla_\theta \sum_y \pi_\theta(y|x) && \text{(linearity of gradient)} \\ &= \nabla_\theta 1 && \text{(probabilities sum to 1)} \\ &= 0 && \text{(derivative of constant)} \end{aligned}

This result shows that the score function always has zero mean under the policy that generates it. The reason is elegant: the score function measures how changing parameters affects the log-probability of different outcomes. Since increasing the probability of some outcomes necessarily decreases the probability of others (they must sum to one), these effects balance out exactly. Mathematically, the sum of all probabilities is fixed at 1, so its gradient must be zero.

This result implies that we can subtract any baseline without introducing bias. The question becomes: what baseline minimizes variance?

The optimal baseline is the expected reward under the current policy:

b∗=Ey∼πθ[R(x,y)]b^* = \mathbb{E}_{y \sim \pi_\theta}[R(x, y)]

where:

  • b∗b^*: the variance-minimizing baseline value
  • E[R(x,y)]\mathbb{E}[R(x, y)]: the expected reward under the current policy

This is called a value baseline, and in practice we often estimate it with a learned value function V(x)V(x) that takes the prompt xx as input and predicts the expected reward. With this baseline, the quantity (R(x,y)−b∗)(R(x, y) - b^*) becomes the advantage: how much better (or worse) this specific sequence is compared to what we expect on average. The advantage is positive when a response exceeds expectations and negative when it falls short.

Advantage

The advantage measures how much better a specific response yy is compared to the expected reward from the current policy:

A(x,y)=R(x,y)−V(x)A(x, y) = R(x, y) - V(x)

where:

  • A(x,y)A(x, y): the advantage of sequence yy given prompt xx
  • R(x,y)R(x, y): the actual reward for this sequence
  • V(x)V(x): the value function estimating the expected reward E[R(x,y)]\mathbb{E}[R(x, y)]

Positive advantage means better than expected; negative means worse than expected. The policy gradient with advantage is ∇θJ(θ)=E[∇θlog⁡πθ(y∣x)⋅A(x,y)]\nabla_\theta J(\theta) = \mathbb{E}[\nabla_\theta \log \pi_\theta(y|x) \cdot A(x, y)].

The advantage has a natural interpretation: it measures how surprising a reward is relative to what we expected. A response with advantage +5 did much better than typical; one with advantage -3 did worse. By training on advantages rather than raw rewards, we give the model clearer information about which responses are good relative to the typical performance level. This is the same intuition a manager uses when giving feedback: rather than saying "you scored 85 on this review," it is more informative to say "you scored 15 points above the team average."

In[13]:
Code
def reinforce_with_baseline(log_probs, reward, baseline):
    """
    REINFORCE with baseline for variance reduction.

    Args:
        log_probs: Log probabilities of selected tokens (shape: seq_len)
        reward: Scalar reward for the complete sequence
        baseline: Expected reward estimate from the value network

    Returns:
        Policy loss and value loss as separate tensors
    """
    log_prob_sequence = log_probs.sum()

    # Advantage: how much better or worse than expected
    advantage = reward - baseline

    # Policy gradient uses advantage instead of raw reward.
    # We detach the advantage so gradients do not flow back
    # through the value network when updating the policy.
    policy_loss = -log_prob_sequence * advantage.detach()

    # Value loss: train baseline to predict expected reward
    value_loss = (baseline - reward) ** 2

    return policy_loss, value_loss
In[14]:
Code
import numpy as np

np.random.seed(42)

n_trials = 1000
rewards_trial = np.random.normal(loc=5.0, scale=3.0, size=n_trials)

baselines = [0, 3, 5, 7, 10]
variances = [float(np.mean((rewards_trial - b) ** 2)) for b in baselines]
optimal_baseline = float(np.mean(rewards_trial))
Out[15]:
Visualization
Line plot showing variance minimized at baseline value of 5, with U-shaped curve around it.
Gradient estimate variance as a function of baseline value for rewards drawn from a normal distribution with mean 5.0. The variance is minimized when the baseline equals the expected reward (approximately 5.0, marked by the dashed line), showing a clear U-shaped relationship. Deviating from the optimal baseline increases gradient variance, showing why accurate value estimation is necessary for stable policy gradient training.
Out[16]:
Visualization
Histogram of raw rewards, all positive, with mean marked by a vertical line showing lack of contrast between good and bad sequences.
Distribution of raw rewards when all responses receive positive rewards (e.g., from a reward model with scale 3-7). All gradient updates push in the same direction, giving weak learning signals since the optimizer cannot distinguish good responses from mediocre ones.
Histogram of advantages centered at zero with a vertical baseline marker, showing clear positive and negative regions.
Distribution of advantages after subtracting the mean baseline, centered at zero. Positive advantages (right of zero) identify above-average responses that should be reinforced, while negative advantages identify below-average responses that should be suppressed, giving the optimizer a clear directional signal.

Reward-to-Go

Another variance reduction technique exploits a basic causal property of sequential decision-making: actions can only affect future rewards, not past ones. A decision made at time tt cannot retroactively change what happened at times 11 through t−1t-1. Yet in basic REINFORCE, we multiply every token's gradient by the full sequence reward, even though early tokens cannot have influenced any "future" rewards from the perspective of later tokens.

Think of a recipe as an analogy. If a chef makes three decisions (which oil to use, which spice to add, whether to reduce the heat), and the final dish receives a good review, it would be wrong to credit the choice of oil for the flavor benefits that the reduced heat provided. The choice of oil could not have influenced whether the sauce reduced properly, since the oil was chosen before the heat decision. Yet basic REINFORCE does exactly this: it credits the oil choice for the reduced-heat flavor improvement.

Instead of multiplying each token's gradient by the total reward, we can use only the rewards that could have been causally influenced by that action. This is called the reward-to-go formulation. The reward-to-go at timestep tt sums all rewards from that step forward:

Rt=∑t′=tTrt′R_t = \sum_{t'=t}^{T} r_{t'}

where:

  • RtR_t: reward-to-go from timestep tt
  • rt′r_{t'}: immediate reward received at timestep t′t'
  • The sum starts at the current timestep tt and runs to the end of the sequence TT

This captures only the rewards that could have been influenced by the action at time tt, ignoring any rewards that occurred before. The policy gradient with reward-to-go is:

∇θJ(θ)=Ey∼πθ[∑t=1T∇θlog⁡πθ(yt∣st)⋅Rt]\nabla_\theta J(\theta) = \mathbb{E}_{y \sim \pi_\theta}\left[\sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(y_t|s_t) \cdot R_t\right]

where:

  • Ey∼πθ\mathbb{E}_{y \sim \pi_\theta}: expectation over sequences sampled from the policy
  • yty_t: the token generated at timestep tt
  • RtR_t: the cumulative reward from time tt onwards (reward-to-go), ignoring past rewards

This does not change the expected gradient (the proof is analogous to the baseline proof: terms involving rewards from before time tt vanish in expectation because they are independent of the action at time tt) but reduces variance by removing terms that add noise without giving useful credit assignment signal.

In[17]:
Code
import torch


def compute_rewards_to_go(rewards, gamma=1.0):
    """
    Compute reward-to-go for each timestep.

    Args:
        rewards: List of rewards at each timestep
        gamma: Discount factor (1.0 = no discounting)

    Returns:
        Tensor of rewards-to-go
    """
    T = len(rewards)
    rewards_to_go = torch.zeros(T)

    # Work backwards from the end of the sequence
    running_sum = 0
    for t in reversed(range(T)):
        running_sum = rewards[t] + gamma * running_sum
        rewards_to_go[t] = running_sum

    return rewards_to_go

The backward pass through the sequence is computationally efficient. We start from the last timestep (where reward-to-go equals just the immediate reward) and accumulate backward. The discount factor γ\gamma down-weights rewards that are further in the future, introducing a form of temporal discounting. In standard RLHF with a single end-of-sequence reward, γ=1.0\gamma = 1.0 is common since all rewards are already at the end.

In[18]:
Code
# Example: sequence with only a final reward
intermediate_rewards = [0, 0, 0, 0, 1.0]  # Only final reward is non-zero
rewards_to_go = compute_rewards_to_go(intermediate_rewards)
Out[19]:
Console
Timestep | Immediate Reward | Reward-to-Go
---------------------------------------------
    0    |       0.0        |     1.0
    1    |       0.0        |     1.0
    2    |       0.0        |     1.0
    3    |       0.0        |     1.0
    4    |       1.0        |     1.0

In this example with only a final reward, reward-to-go equals the final reward at all timesteps. This makes sense: from every position in the sequence, the only future reward available is the one at the end. The real benefit of reward-to-go emerges when we have intermediate rewards, such as the per-token KL divergence penalty that RLHF systems often add.

Out[20]:
Visualization
Bar chart of per-token immediate rewards with a highlighted positive first reward bar and small negative bars elsewhere.
Per-token immediate rewards for a sequence with a large positive reward at the first timestep and small negative KL penalties at subsequent timesteps. The front-loaded positive reward is marked in green.
Bar chart of cumulative reward-to-go at each timestep, with bars decreasing toward the end.
Cumulative reward-to-go $R_t$ at each timestep, showing that early tokens see larger future rewards than late tokens. Using reward-to-go instead of the total sequence reward reduces gradient variance by excluding past rewards that could not have been influenced by each action.

In the typical RLHF setup where reward is only given at the end, reward-to-go equals the final reward at all timesteps. However, this structure becomes important when we add auxiliary rewards or penalties, such as the KL divergence penalty we will introduce in subsequent chapters. That penalty applies at each token, creating a richer reward structure where reward-to-go provides meaningful variance reduction by correctly attributing per-token contributions to the overall gradient.

Combining Baselines and Reward-to-Go

The two techniques can be combined. We can use a per-token value function V(st)V(s_t) as a baseline for each timestep, subtracting it from the reward-to-go at that position:

At=Rt−V(st)A_t = R_t - V(s_t)

where:

  • AtA_t: the per-token advantage at timestep tt
  • RtR_t: the reward-to-go from timestep tt
  • V(st)V(s_t): the value function predicting expected future reward from state sts_t

This per-token advantage is what actor-critic algorithms compute. The actor is the policy network being trained; the critic is the value function giving the baseline. By training the critic to accurately predict expected future rewards, we get better baselines, which gives us lower-variance advantage estimates, which gives us more stable policy updates.

Complete Implementation

Let's put everything together into a complete REINFORCE implementation with baseline and a proper training loop. We will build on the components developed throughout this chapter.

The first component we need is the value network, which learns to predict the expected reward for a given prompt:

In[21]:
Code
import torch.nn as nn


class ValueNetwork(nn.Module):
    """Baseline network that estimates expected reward given a prompt."""

    def __init__(self, vocab_size, hidden_size=256):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, hidden_size)
        self.lstm = nn.LSTM(hidden_size, hidden_size, batch_first=True)
        self.output = nn.Linear(hidden_size, 1)

    def forward(self, input_ids):
        embeds = self.embedding(input_ids)
        lstm_out, (h_n, _) = self.lstm(embeds)
        # Use final hidden state to predict expected reward for this context
        value = self.output(h_n[-1])
        return value.squeeze(-1)

The value network takes the prompt as input and produces a scalar prediction of expected reward. We train it simultaneously with the policy, updating its weights to minimize mean squared error between its predictions and the observed rewards. As training progresses and the value network improves its predictions, the advantage estimates become more accurate, further reducing variance.

In[22]:
Code
import torch
import torch.nn.functional as F


def reinforce_training_step(
    policy,
    value_net,
    prompt_ids,
    reward_fn,
    policy_optimizer,
    value_optimizer,
    max_length=20,
    vocab_size=1000,
):
    """
    Single REINFORCE training step with baseline.

    Args:
        policy: Language model policy (the model being trained)
        value_net: Baseline value network (the critic)
        prompt_ids: Input prompt token IDs
        reward_fn: Function that scores complete sequences
        policy_optimizer: Optimizer for the policy network
        value_optimizer: Optimizer for the value network
        max_length: Maximum number of tokens to generate
        vocab_size: Size of the vocabulary

    Returns:
        Dictionary with loss values, reward, and advantage statistics
    """
    policy.train()
    value_net.train()

    # Step 1: Generate a complete sequence and collect per-token log-probs
    current_ids = prompt_ids.clone()
    log_probs = []

    for _ in range(max_length):
        logits = policy(current_ids)
        probs = F.softmax(logits[:, -1, :], dim=-1)

        # Sample next token from the policy distribution
        action = torch.multinomial(probs, 1)

        # Record the log-probability of the selected token
        log_prob = torch.log(probs.gather(1, action) + 1e-10)
        log_probs.append(log_prob)

        current_ids = torch.cat([current_ids, action], dim=1)

    # Sum log-probs across the sequence to get log pi(y|x)
    log_probs = torch.cat(log_probs, dim=1).sum(dim=1)

    # Step 2: Score the complete sequence with the reward function
    reward = reward_fn(current_ids)

    # Step 3: Compute baseline prediction from the value network
    baseline = value_net(prompt_ids)

    # Step 4: Compute advantage (reward above baseline)
    advantage = reward - baseline.detach()

    # Step 5: Policy loss (REINFORCE with baseline)
    # Detach advantage so policy gradients do not flow through value network
    policy_loss = -(log_probs * advantage).mean()

    # Step 6: Value loss (mean squared error)
    value_loss = F.mse_loss(baseline, reward)

    # Step 7: Update policy parameters
    policy_optimizer.zero_grad()
    policy_loss.backward()
    policy_optimizer.step()

    # Step 8: Update value network parameters
    value_optimizer.zero_grad()
    value_loss.backward()
    value_optimizer.step()

    return {
        "policy_loss": policy_loss.item(),
        "value_loss": value_loss.item(),
        "reward": reward.mean().item(),
        "advantage": advantage.mean().item(),
    }

Now let's test this with a simple reward function that encourages token diversity in the generated responses:

In[23]:
Code
# Setup
import torch

torch.manual_seed(42)
vocab_size = 100
hidden_size = 64

policy = LanguageModelPolicy(vocab_size, hidden_size, num_layers=1)
value_net = ValueNetwork(vocab_size, hidden_size)

policy_optimizer = torch.optim.Adam(policy.parameters(), lr=1e-3)
value_optimizer = torch.optim.Adam(value_net.parameters(), lr=1e-3)


# Simple reward: bonus for token diversity (unique tokens / total tokens) minus a fixed cost
def length_penalty_reward(sequence_ids):
    """Reward function based on token diversity in the generated portion."""
    batch_size = sequence_ids.shape[0]
    rewards = []
    for i in range(batch_size):
        generated = sequence_ids[i, 5:]  # Strip the 5-token prompt
        n_unique = len(generated.unique())
        n_total = len(generated)
        diversity = n_unique / max(n_total, 1)  # 0 to 1
        rewards.append(
            diversity - 0.5
        )  # Centered: positive if diverse, negative if repetitive
    return torch.tensor(rewards, dtype=torch.float32)


# Training loop
prompt = torch.randint(0, vocab_size, (1, 5))  # Batch of 1, prompt length 5
training_history = []

for step in range(100):
    metrics = reinforce_training_step(
        policy,
        value_net,
        prompt,
        length_penalty_reward,
        policy_optimizer,
        value_optimizer,
        max_length=15,
        vocab_size=vocab_size,
    )
    training_history.append(metrics)
In[24]:
Code
import matplotlib.pyplot as plt

use_book_style(fixed_canvas=True)
plt.rcParams["figure.figsize"] = (2.0, 2.4)

steps = range(len(training_history))
policy_losses = [h["policy_loss"] for h in training_history]
value_losses = [h["value_loss"] for h in training_history]
rewards_hist = [h["reward"] for h in training_history]

# First plot
fig = plt.figure()
fig.subplots_adjust(left=0.29, right=0.96, bottom=0.20, top=0.78)
plt.plot(
    steps,
    policy_losses,
    alpha=0.7,
    color=theme_color("steelblue"),
    linewidth=1.1,
)
plt.xlabel("Training Step")
plt.ylabel("Policy Loss")
plt.title("Policy Loss\n(High Variance)")
plt.show()

# Second plot
fig = plt.figure()
fig.subplots_adjust(left=0.29, right=0.96, bottom=0.20, top=0.78)
plt.plot(
    steps, value_losses, alpha=0.7, color=theme_color("coral"), linewidth=1.1
)
plt.xlabel("Training Step")
plt.ylabel("Value Loss")
plt.title("Value Loss\n(Baseline Learning)")
plt.show()

# Third plot
fig = plt.figure()
fig.subplots_adjust(left=0.29, right=0.96, bottom=0.20, top=0.78)
plt.plot(
    steps,
    rewards_hist,
    alpha=0.7,
    color=theme_color("forestgreen"),
    linewidth=1.1,
)
plt.xlabel("Training Step")
plt.ylabel("Reward")
plt.title("Average Reward")
plt.show()
Out[24]:
Visualization
Line plot of policy loss over 100 training steps showing high variance and noisy oscillations.
Policy loss over 100 REINFORCE training steps, exhibiting the high variance characteristic of Monte Carlo gradient estimates. The noisy trajectory reflects the instability that motivates more sophisticated algorithms like PPO.
Line plot of value loss over training steps showing a decreasing trend as the baseline network improves.
Value network loss over training steps, showing the baseline network gradually learning to predict expected rewards. As the value loss decreases, advantage estimates become more accurate and gradient variance is reduced.
Line plot of average reward over training steps showing the reward trajectory.
Average reward trajectory over training steps under REINFORCE optimization with baseline. The trend reveals whether the policy is improving, though the noise makes it difficult to assess progress on a per-step basis.

The plots reveal the characteristic noisiness of REINFORCE training. While the value network learns to predict expected rewards (value loss decreases), the policy loss remains highly variable. This variance is the primary motivation for more sophisticated algorithms like PPO, which we cover in the next chapter.

Additional Variance Reduction Techniques

Beyond baselines and reward-to-go, several additional techniques help control variance in practice. Together, these techniques form a toolkit that practitioners reach for when raw REINFORCE proves too unstable.

Reward Normalization

Reward normalization standardizes rewards across a batch to have zero mean and unit variance:

In[25]:
Code
def normalize_rewards(rewards, eps=1e-8):
    """
    Normalize rewards to zero mean and unit variance within a batch.

    This prevents large reward magnitudes from causing gradient explosions
    while preserving the relative ordering of rewards within the batch.
    The eps term prevents division by zero when all rewards are identical.
    """
    mean = rewards.mean()
    std = rewards.std()
    return (rewards - mean) / (std + eps)

This technique prevents large reward magnitudes from causing gradient explosions while preserving the relative ordering of rewards. It is particularly useful when the reward scale varies across training: perhaps early in training rewards cluster near zero, while later in training they spread across a wider range. Normalization ensures the learning rate remains appropriate throughout.

The key insight is that what matters for policy improvement is not the absolute scale of rewards but their relative ordering. Whether responses score 5, 7, and 9, or 500, 700, and 900, the model should learn the same qualitative lessons. Normalization makes the gradient magnitudes consistent regardless of the reward scale.

Entropy Regularization

Entropy regularization adds a bonus for maintaining diversity in the policy's probability distribution, preventing premature convergence to deterministic behavior:

In[26]:
Code
import torch


def policy_entropy(probs):
    """
    Compute the Shannon entropy of an action distribution.

    Higher entropy means more uniform distribution (more exploration).
    Lower entropy means more concentrated distribution (more exploitation).
    """
    return -(probs * torch.log(probs + 1e-10)).sum(dim=-1)


def entropy_regularized_loss(policy_loss, probs, entropy_coef=0.01):
    """
    Add entropy bonus to encourage continued exploration.

    The entropy coefficient controls the exploration-exploitation tradeoff:
    larger values encourage more diversity in generated responses.
    """
    entropy = policy_entropy(probs).mean()
    return policy_loss - entropy_coef * entropy

High entropy means the policy is exploring diverse actions, generating varied responses. Low entropy means it is becoming deterministic, repeatedly generating the same responses. The entropy bonus counteracts the natural tendency of policy gradient training to become more and more confident (lower entropy) over time.

Without entropy regularization, a policy gradient algorithm can get stuck in a local optimum: it finds a response type that consistently receives decent rewards, concentrates all probability on that response type, and never explores alternative approaches that might receive higher rewards. The entropy bonus keeps the policy somewhat diffuse, maintaining the exploration needed to discover better response strategies.

Out[27]:
Visualization
Bar chart of action probabilities without entropy regularization showing one action dominating at 0.85 probability.
Action probability distribution without entropy regularization, showing a near-deterministic policy collapsed to a single dominant action. The low entropy (H near 0) indicates that the policy has lost the ability to explore alternative token choices, potentially trapping it in a local optimum.
Bar chart of action probabilities with entropy regularization showing a more uniform distribution across actions.
Action probability distribution with entropy regularization, showing a more diverse distribution that maintains meaningful probability mass across multiple actions. The higher entropy preserves the policy's ability to explore and potentially discover better response strategies.

Gradient Clipping

Gradient clipping limits the magnitude of gradient updates by rescaling the gradient vector when its norm exceeds a threshold:

In[28]:
Code
import torch


def clip_gradients(model, max_norm=1.0):
    """
    Clip gradients to prevent excessively large parameter updates.

    When the gradient norm exceeds max_norm, the gradient vector is
    rescaled to have norm exactly max_norm. This preserves the direction
    of the gradient while bounding its magnitude.
    """
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)

As we discussed in the chapter on gradient clipping, this technique prevents individual updates from being too large, which is especially important given REINFORCE's high variance. A single high-reward sample can produce an enormous gradient that catastrophically overwrites learned behavior. Clipping ensures no single update can move parameters too far from their current values.

The choice of max_norm involves a tradeoff: too small and learning slows unnecessarily; too large and you lose the protection against large updates. Values between 0.5 and 5.0 are common in practice, with 1.0 being a typical starting point.

Putting the Techniques Together

In production RLHF systems, these variance reduction techniques are applied together. A typical implementation would:

  1. Generate a batch of responses (multiple responses per prompt for better variance reduction)
  2. Score each response with the reward model
  3. Normalize rewards within the batch to zero mean and unit variance
  4. Compute baseline predictions from the value network
  5. Compute per-sequence advantages (normalized reward minus baseline)
  6. Compute the REINFORCE loss using advantages instead of raw rewards
  7. Backpropagate and clip gradients before applying the update
  8. Add entropy bonus to prevent policy collapse

Each technique addresses a different source of instability: normalization handles reward scale variation, baselines address absolute reward level, clipping prevents catastrophic updates, and entropy regularization prevents premature convergence.

Limitations and Practical Challenges

REINFORCE, while theoretically elegant and mathematically well-grounded, has significant practical limitations that make it challenging to apply directly to large language model alignment. Understanding these limitations is important both for appreciating why more sophisticated algorithms exist and for knowing when simpler approaches might be appropriate.

Sample efficiency. REINFORCE requires many samples to produce reliable gradient estimates. For large language models generating hundreds of tokens, each sample is expensive: it requires a full autoregressive forward pass through a model with billions of parameters, potentially followed by a separate reward model evaluation. Training to convergence can require millions of sampled sequences. At a cost of perhaps several seconds per sample on expensive hardware, the total compute required becomes prohibitive. The O(1/N)O(1/N) variance reduction from batching means you need 100 times more samples just to reduce variance by a factor of 10 compared to having a perfect gradient estimate.

Credit assignment at the token level. The credit assignment problem is only partially solved. REINFORCE attributes equal credit to all tokens in a sequence based on the sequence-level reward. This ignores the reality that different tokens contribute differently to response quality. A response might be mostly excellent with one factually incorrect sentence, yet every token in the response receives the same blame for that error. The model cannot learn to avoid that specific error while keeping the rest of its behavior intact. More sophisticated algorithms like actor-critic methods attempt finer-grained credit assignment by training a value function that estimates the expected future reward from each intermediate state.

Update stability. REINFORCE provides no guarantees about the size of policy updates. A lucky high-reward sample can push the policy dramatically in one direction, only for the next batch to push it back in the opposite direction. This oscillation wastes computation and can destabilize training. More seriously, if the policy changes too much in one update, future samples will be drawn from a different distribution, making old gradient estimates less relevant. This mismatch between the policy at sample time and the policy at update time can compound over multiple steps, leading to divergence. The PPO algorithm, covered in the next chapter, directly addresses this by constraining how much the policy can change per update.

On-policy requirement and sample reuse. Policy gradient methods are on-policy: the gradient estimate is valid only for the policy that generated the samples. Once we update the policy, the samples become stale, and we must generate new ones. This is in contrast to off-policy methods (like Q-learning) that can reuse old samples from a replay buffer. The on-policy requirement means we cannot reuse samples across multiple gradient steps, further reducing sample efficiency. Every parameter update requires generating a fresh batch of sequences.

Despite these limitations, REINFORCE remains important as the foundational algorithm for understanding all more sophisticated policy gradient methods. PPO, TRPO, and other production algorithms all build on the same core gradient estimator derived in this chapter. Understanding REINFORCE's mechanics, failure modes, and the mathematical properties that enable variance reduction is prerequisite knowledge for understanding why those algorithms make the design choices they do. The log-derivative trick, the zero-mean score function, the baseline property, and the causal structure of reward-to-go all appear in more advanced algorithms, just in more sophisticated forms.

Summary

Policy gradient methods provide the mathematical foundation for optimizing language models using reward signals. The key insights from this chapter are:

  • Language models are policies. The autoregressive generation process naturally maps to the reinforcement learning framework: states are contexts, actions are tokens, and the policy πθ(at∣st)\pi_\theta(a_t|s_t) is the model's next-token probability distribution. This framing connects language model alignment directly to decades of reinforcement learning research.

  • The policy gradient theorem enables optimization. By applying the log-derivative trick, we rewrite the gradient of expected reward as E[∇θlog⁡πθ(y∣x)⋅R(y)]\mathbb{E}[\nabla_\theta \log \pi_\theta(y|x) \cdot R(y)]. This turns an intractable sum over all possible sequences into a Monte Carlo estimate we can compute from samples.

  • REINFORCE is simple but high-variance. The basic algorithm samples sequences, computes their rewards, and updates parameters in proportion to log⁡π(y)⋅R\log \pi(y) \cdot R. This works in principle but requires many samples for stable estimates, which makes it impractical at scale for large language models.

  • Baselines reduce variance without introducing bias. Subtracting a baseline bb from rewards yields the same expected gradient but lower variance. The score function has zero mean in expectation, which is the key mathematical fact that makes baselines work. The optimal baseline is the expected reward, leading to the advantage formulation A=R−VA = R - V.

  • Reward-to-go exploits causal structure. Actions cannot affect past rewards, only future ones. Using only future rewards in the gradient estimate removes noise from past outcomes while preserving the unbiasedness of the estimator.

  • Additional techniques help in practice. Reward normalization, entropy regularization, and gradient clipping all contribute to more stable training. These techniques address different failure modes: scale variation, policy collapse, and catastrophic updates respectively.

The variance problem with REINFORCE motivates the development of more sophisticated algorithms. In the next chapter, we will see how Proximal Policy Optimization (PPO) addresses these issues by constraining policy updates and using careful advantage estimation with generalized advantage estimation (GAE), making RL-based alignment practical for production language models.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about policy gradient methods and the REINFORCE algorithm.

Policy Gradient Methods

Question 1 of 80 of 8 completed
In the reinforcement learning formulation of language models, what does the 'state' represent at timestep t?

Comments

No comments yet. Be the first to share your thoughts!

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025policygradient, author = {Michael Brenndoerfer}, title = {Policy Gradient Methods: REINFORCE Algorithm & Theory}, year = {2025}, url = {https://mbrenndoerfer.com/writing/policy-gradient-methods-reinforce-algorithm}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-30} }
APAAcademic
Michael Brenndoerfer (2025). Policy Gradient Methods: REINFORCE Algorithm & Theory. Retrieved from https://mbrenndoerfer.com/writing/policy-gradient-methods-reinforce-algorithm
MLAAcademic
Michael Brenndoerfer. "Policy Gradient Methods: REINFORCE Algorithm & Theory." 2026. Web. September 30, 2026. <https://mbrenndoerfer.com/writing/policy-gradient-methods-reinforce-algorithm>.
CHICAGOAcademic
Michael Brenndoerfer. "Policy Gradient Methods: REINFORCE Algorithm & Theory." Accessed September 30, 2026. https://mbrenndoerfer.com/writing/policy-gradient-methods-reinforce-algorithm.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Policy Gradient Methods: REINFORCE Algorithm & Theory'. Available at: https://mbrenndoerfer.com/writing/policy-gradient-methods-reinforce-algorithm (Accessed: September 30, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Policy Gradient Methods: REINFORCE Algorithm & Theory. https://mbrenndoerfer.com/writing/policy-gradient-methods-reinforce-algorithm

About the author

Continue with the full handbook

This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore Language AI Handbook
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.