Policy Gradient Methods › Complete Implementation

Michael BrenndoerferDecember 26, 20255 min read
Language AI HandbookAll chapters
Part 37 of 67Alignment and RLHF
Chapter 277Policy Gradient Methods › Complete Implementation
Reading section 10 of 14 · Complete Implementation · 5 minSection 10 of 14 · 5 min

Book contents

Section 10 of 14 · 5 min read

Policy Gradient Methods › Complete ImplementationOne section · Sections in this chapter10 / 14 ↓
Reading tools Definitions & inline links

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

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.