Assignment 4: Inverse RL & GRPO
Inverse RL
Given a set of expert demonstrations, find the underlying reward function that the expert was secretly trying to maximize. Essentially, instead of telling the AI what to do and letting it figure out how, we show the AI how it’s done, and it figures out why (the reward).
Formally, we want to find a reward function such that the expert outperforms other policies:
where is the expected cumulative discounted sum of features values, or “feature expectations”. Then the objective becomes:
[2]: for a policy to be guaranteed to perform as well as the expert , it suffices that the feature expectations match, that is:
💡 Features are measurable characteristics of the environment at any given moment. For example, in driving, the features might be: (1). Distance to the center of the lane. (2). Current speed (3). Distance to the car in front.
Feature expectations is the average of features experienced by the policy:
Maximum Entropy IRL
There are infinitely many reward functions that could explain the expert’s behavior. To solve this, Maximum Entropy IRL additionally incorporates the principle of maximum entropy:
Out of all the reward functions that match the expert’s features, we want to pick the one that results in the most random, highest-entropy trajectory distribution. This is a mathematical way of saying, “We shouldn’t make any extra assumptions about the expert’s behavior beyond what we actually saw them do.”
The entropy is calculated over the distribution of entire trajectories:
It can be shown that:
- is the total reward of the trajectory. Trajectories with higher total rewards are exponentially more likely.
- accounts for the environment’s dynamics. We can’t control physics, so we just multiply by the probability of those physical transitions happening.
- is the partition function (normalizing constant).
Proof:
Our objective: maximize entropy with two constraints:
- features matching: → expected feature counts of our expectation must equal the expert’s empirical features counts
Using Lagrangian:To find maximum:
is just a constant that ensures the probabilities sum to 1, we can replace it with the partition function . We also multiply by the fixed probability of the transitions, .This leaves us with the exact distribution:
To learn , the gradient of the log-likelihood is:
where:
- is the log-likelihood of the expert’s trajectories under our reward function:
- : the “target” — the empirical feature expectation, i.e. feature counts the expert actually accumulated.
def feature_expectation_from_trajectories(features: np.ndarray, demos: Iterable[Trajectory]) -> np.ndarray:
totals = np.zeros(features.shape[1], dtype=np.float64)
n_trajectories =
for traj in demos:
visited = traj.state_sequence()
if visited.size == 0:
continue
totals += features[visited].sum(axis=0)
n_trajectories += 1
return totals / n_trajectories
- : the “Prediction” — the time-aggregated expected state-visitation frequency, i.e. feature counts our current weights expect the agent to accumulate.
- is the expected state visitation frequency, i.e. the expected number of times the agent visit state .
In practice, is calculated with dynamic programming, involving a backward and a forward pass:
Backward pass - finding the policy
We work backward from the end of the game to the beginning to calculate partition functions . represents the total sum of exponentiated reward we can get from this state onward. represents how much exponentiated reward can get in state and take action ?
Then we calculate this recursively:
The exponential comes from the trajectory distributions defined before: . Then, for a trajectory: . The recursion starts from the terminal states by setting their values to 1.0 while other states to 0. We recursively sweep across all states over until their values converge. Finally, our policy is:
Forward pass - counting the visits
Once we have the policy, we push “probability mass” through the grid, step by step, to see what states the policy spend time on. Let be the probability of being in state at time . We initialize initial state probability (this is done by simply calculating initial state distributions in expert trajectories). Then “push the probability mass” recursively:
note that if reaching a terminal state, we simply skip it in summation since we cannot push probability mass out of a terminal state. Then the final visitation frequency is:
def compute_expected_svf(p_transition: np.ndarray, p_initial: np.ndarray, terminal: Iterable[int], reward: np.ndarray, eps: float = 1e-6) -> np.ndarray:
n_states, _, n_actions = p_transition.shape
terminal = set(int(t) for t in terminal)
Zs = np.zeros(n_states, dtype=np.float64)
Zs[list(terminal)] = 1.0
exp_reward = np.exp(reward)
for _ in range(2 * n_states):
Za = np.zeros((n_states, n_actions), dtype=np.float64)
for s in range(n_states):
for a in range(n_actions):
# compute the state-action partition value using the backward recursion
Za[s, a] = exp_reward[s] * (p_transition[s, :, a] * Zs).sum()
new_Zs = Za.sum(axis=1)
if np.max(np.abs(new_Zs - Zs)) < eps:
Zs = new_Zs
break
Zs = new_Zs
with np.errstate(divide="ignore", invalid="ignore"):
policy = np.divide(Za, Zs[:, None], out=np.zeros_like(Za), where=Zs[:, None] > 0)
horizon = 2 * n_states
d = np.zeros((n_states, horizon), dtype=np.float64)
d[:, 0] = p_initial
for t in range(1, horizon):
for s_prev in range(n_states):
if s_prev in terminal:
continue
for a in range(n_actions):
prob = d[s_prev, t - 1] * policy[s_prev, a]
if prob == 0:
continue
# push visitation probability mass forward through the transition model
d[:, t] += prob * p_transition[s_prev, :, a]
return d.sum(axis=1)
Group Relative Policy Optimization (GRPO)
GRPO extends standard PPO specifically for aligning Large Language Models. Standard PPO requires training a separate, complex “Critic” (Value Model) that is often the same size as the main model. This essentially doubles the memory requirements. GRPO skips the Critic entirely. Instead of using a separate neural network to estimate how good a state is (the baseline), GRPO samples multiple responses for the same prompt and uses their mean reward as a prompt-level control variate. This stabilizes policy updates while saving massive amounts of VRAM.
The Group Advantage (Control Variate)
For each prompt , we draw a group of completions from the policy. We calculate a reward for each completion. To determine if a completion was good/bad relative to the model’s current capability, we calculate the group mean and the group standard deviation .
The centered and normalized advantage is:
By normalizing with the standard deviation, we ensure the advantages have a mean of 0 and a variance of 1, which keeps the gradients at a stable scale regardless of the absolute scale of the reward function.
def compute_group_advantages(rewards: torch.Tensor, group_size: int) -> torch.Tensor:
reshaped = rewards.view(-1, group_size)
mean = reshaped.mean(dim=1, keepdim=True)
std = reshaped.std(dim=1, keepdim=True, unbiased=False).clamp_min(1e-6)
normalized = (reshaped - mean) / std
return normalized.view(-1)
Sequence-Level Probability Ratio
In standard token-level RL, the model tries to assign credit to every single word. However, for reasoning and math tasks, the reward is sparse and sequence-level (e.g. you don’t know if the math is correct until the very end of the <answer> tag). Since the reward function generally works on the whole sequence, we correspondingly calculate the log probability of generating an entire sequence as the sum of the log probabilities of its individual tokens:
The probability ratio then compares how likely the current model is to generate the entire sequence compared to the frozen reference model:
The Clipped Surrogate Objective
The policy update maximizes a PPO-style clipped surrogate objective to prevent destructively large updates from a single lucky (or unlucky) batch:
- When : The policy wants to increase (make this action more likely). However, the min function ensures that once hits the ceiling of , the gradient drops to zero. This prevents the model from taking overly large updates
- When : The policy wants to decrease (make this action less likely). The min function ensures that once drops to the floor of , the gradient drops to zero. This prevents the model from completely destroying the probability of a specific sequence just because it happened to perform worse than average in this one specific group.
KL Divergence Penalty
To prevent the model from drifting too far from the reference model (which causes mode collapse or gibberish), we subtract a KL divergence penalty scaled by a coefficient .
Formal KL Divergence requires an expectation over all possible sequences: . Since we cannot compute that, we use Monte Carlo Estimation: we estimate the divergence using only the specific sequences we just generated in our batch.
DeepSeekMath Robust Estimator
Instead of log-prob difference (), which can sometimes be negative and accidentally reward drift, [3] introduces a strictly non-negative robust estimator for KL divergence (strictly punishing the policy whenever deviating from the reference model):
The Final Loss Function & Code
The objective we want to maximize is . Because PyTorch optimizers Note that taking the .mean() across the batch of sequences effectively computes the expected value () required by the formal KL and RL formulas.
def grpo_sequence_loss(logp_new: torch.Tensor, logp_ref: torch.Tensor, advantages: torch.Tensor, beta: float, epsilon: float) -> torch.Tensor:
# 1. PPO Clipped Surrogate
r_t = torch.exp(logp_new - logp_ref)
clipped_r_t = torch.clamp(r_t, 1 - epsilon, 1 + epsilon)
l_clip = torch.min(r_t * advantages, clipped_r_t * advantages)
# 2. Robust KL Penalty (DeepSeekMath)
log_ratio = logp_ref - logp_new
kl = torch.exp(log_ratio) - log_ratio - 1.0
# 3. Final Loss (Maximize objective = Minimize negative objective)
# The .mean() acts as the Monte Carlo approximation of the expected value
loss = -(l_clip - beta * kl).mean()
return loss
Reward Shaping (Sparse Signals)
Format rewards are dense, but answer-correctness rewards are sparse (only evaluated at the end if the final extraction matches the ground truth). If the model only receives a reward for perfection, it might randomly generate tokens for thousands of steps without ever stumbling upon a correct answer, resulting in 0 gradients and no learning.
Tiered shaping is one way to keep the correctness learning signal meaningful, we use t:
- Format Breadcrumbs (+0.1 to +0.5): Give small, dense rewards for correctly opening a
<think>tag, closing it</think>, and opening an<answer>tag. - The Correctness Jackpot (+2.0): Give a massive reward only if the final extracted string mathematically matches the ground truth.
This bootstraps the policy. The dense rewards guide the model to learn the structural syntax first. Once the model reliably outputs the <answer> tags, it creates the structural “container” required for the verifier to check the math, allowing it to finally experience and learn from the sparse, high-value correctness signal.
References
[1] Brian D. Ziebart, Andrew Maas, J. Andrew Bagnell, and Anind K. Dey. 2008. Maximum entropy inverse reinforcement learning. In Proceedings of the 23rd national conference on Artificial intelligence - Volume 3 (AAAI’08). AAAI Press, 1433–1438.
[2] Pieter Abbeel and Andrew Y. Ng. 2004. Apprenticeship learning via inverse reinforcement learning. In Proceedings of the twenty-first international conference on Machine learning (ICML ‘04). Association for Computing Machinery, New York, NY, USA, 1. https://doi.org/10.1145/1015330.1015430
[3] Shao, Zhihong, Peiyi Wang, Qihao Zhu, Runxin Xu, Jun-Mei Song, Mingchuan Zhang, Y. K. Li, Yu Wu and Daya Guo. “DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models.” ArXiv abs/2402.03300 (2024): n. pag.