VKVlad KulikovPublic / lectures
RL for LLM4 / 13

Module 4. Policy gradients: from REINFORCE to PPO

Choose your answers and select “Check”. Numbers accept a decimal point, comma, or simple fraction. Answers and drafts are saved in this browser and shared between RU/EN.

Use the author’s worked answers to review open questions and code tasks yourself. Code does not run on this site.

Checked: 0 / 8 · Correct: 0

Question P01 · The price of the log-derivative identity

single answer

The identity ∇θE[f(X)]=E[f(X)∇θlog⁡pθ(X)]\nabla_\theta \mathbb{E}[f(X)] = \mathbb{E}[f(X)\nabla_\theta\log p_\theta(X)] removes the need to differentiate ff. What is received in exchange?

One correct option

Question P02 · Reward-to-go

coding task

Specification: Gt=∑k=tT−1γk−trk+1G_t = \sum_{k=t}^{T-1}\gamma^{k-t} r_{k+1} — the return from the current step to the end of the episode. Compute it in a backward pass: Gt=rt+1+γGt+1G_t = r_{t+1} + \gamma G_{t+1}, GT=0G_T = 0.

Input format: line 1 — space-separated rewards; line 2 — γ\gamma. Output format: one line — G0,…,GT−1G_0, \ldots, G_{T-1} space-separated (6 decimals).

Starter code:

import numpy as np

def reward_to_go(rewards, gamma=1.0):
    # YOUR CODE HERE
    raise NotImplementedError

if __name__ == "__main__":
    rewards = [float(x) for x in input().split()]
    gamma = float(input())
    print(" ".join(f"{x:.6f}" for x in reward_to_go(rewards, gamma)))

Open tests:

stdinstdout
1 0 2 3 ⏎ 0.94.807000 4.230000 4.700000 3.000000
1 0 2 3 ⏎ 1.06.000000 5.000000 5.000000 3.000000
0 0 1 ⏎ 0.50.250000 0.500000 1.000000

Question P03 · What can be subtracted without a correction

single answer

A baseline is subtracted from Qπ(s,a)Q^\pi(s,a). What condition makes the substitution safe for the expected gradient?

One correct option

Question P04 · The scale of group centering

numeric answer · tolerance ±0.005

Group rewards are centered on the group mean: Ai=Ri−RˉA_i = R_i - \bar R, group size G=8G = 8, responses conditionally independent.

By what factor does the expectation of the resulting gradient estimate differ from E[RS]\mathbb{E}[RS]?

Question P05 · RLOO and the group mean: collinear but rescaled

coding task

Specification: Aimean=Ri−RˉA_i^{\text{mean}} = R_i - \bar R with Rˉ\bar R the full group mean; AiLOO=Ri−1G−1∑j≠iRjA_i^{\mathrm{LOO}} = R_i - \frac{1}{G-1}\sum_{j\ne i} R_j. Requires G≥2G \ge 2.

Input format: line 1 — the group size GG; line 2 — space-separated rewards. Output format: line 1 — group-mean advantages (6 decimals); line 2 — RLOO advantages (6 decimals); line 3 — the ratio A0LOO/A0meanA^{\mathrm{LOO}}_0/A^{\text{mean}}_0 (6 decimals), or undefined if the denominator is zero.

Starter code:

import numpy as np

def group_and_loo(rewards):
    """Return (mean_advantages, loo_advantages)."""
    # YOUR CODE HERE
    raise NotImplementedError

if __name__ == "__main__":
    g = int(input())
    rewards = [float(x) for x in input().split()]
    mean_adv, loo_adv = group_and_loo(rewards)
    print(" ".join(f"{x:.6f}" for x in mean_adv))
    print(" ".join(f"{x:.6f}" for x in loo_adv))
    print(f"{loo_adv[0] / mean_adv[0]:.6f}" if abs(mean_adv[0]) > 1e-12 else "undefined")

Open tests:

stdinstdout
4 ⏎ 1 0 1 00.500000 -0.500000 0.500000 -0.500000 ⏎ 0.666667 -0.666667 0.666667 -0.666667 ⏎ 1.333333
3 ⏎ 2 2 20.000000 0.000000 0.000000 ⏎ 0.000000 0.000000 0.000000 ⏎ undefined

Question P06 · Generalized advantage estimation

coding task

Specification: the episode terminates on its last step. δt=rt+γVt+1−Vt\delta_t = r_t + \gamma V_{t+1} - V_t with VT=0V_{T} = 0; A^t=δt+γλA^t+1\widehat A_t = \delta_t + \gamma\lambda \widehat A_{t+1}, A^T=0\widehat A_T = 0. Critic target: V^t=Vt+A^t\widehat V_t = V_t + \widehat A_t.

Input format: line 1 — rewards; line 2 — value estimates VtV_t; line 3 — γ\gamma and λ\lambda. Output format: line 1 — A^t\widehat A_t space-separated (6 decimals); line 2 — critic targets (6 decimals).

Starter code:

import numpy as np

def gae(rewards, values, gamma, lam):
    """Return (advantages, value_targets)."""
    # YOUR CODE HERE
    raise NotImplementedError

if __name__ == "__main__":
    rewards = [float(x) for x in input().split()]
    values = [float(x) for x in input().split()]
    gamma, lam = (float(x) for x in input().split())
    adv, targets = gae(rewards, values, gamma, lam)
    print(" ".join(f"{x:.6f}" for x in adv))
    print(" ".join(f"{x:.6f}" for x in targets))

Open tests:

stdinstdout
0 0 1 ⏎ 0.4 0.5 0.6 ⏎ 1.0 0.50.250000 0.300000 0.400000 ⏎ 0.650000 0.800000 1.000000
0 0 1 ⏎ 0.4 0.5 0.6 ⏎ 1.0 0.00.100000 0.100000 0.400000 ⏎ 0.500000 0.600000 1.000000

Question P07 · Bootstrapping at the episode boundary

single answer

A batch was collected with a length cap; some episodes were cut off by it and some ended at a real terminal. What happens to the advantage estimate if both groups are treated identically, as terminal?

One correct option

Question P08 · When the clipping plateau is active

multiple answers

For which combinations of advantage sign and ratio movement does the clipped branch actually bound the objective? Select all that apply.

Select all correct options

Question P09 · PPO diagnostics: outside the interval versus actively clipped

coding task

Specification: each sample supplies a ratio rir_i and an advantage A^i\widehat A_i. Compute three quantities at a given ε\varepsilon:

  1. the fraction with ri∉[1−ε,1+ε]r_i \notin [1-\varepsilon, 1+\varepsilon];
  2. the fraction where the pessimistic branch is strictly below the unclipped one, i.e. min⁡(riA^i, clip⁡(ri)A^i)<riA^i\min(r_i\widehat A_i,\ \operatorname{clip}(r_i)\widehat A_i) < r_i\widehat A_i;
  3. the non-negative sample estimate D^=1N∑i[(ri−1)−log⁡ri]\widehat D = \frac1N\sum_i[(r_i-1) - \log r_i].

Input format: line 1 — the number of samples NN and ε\varepsilon; then NN lines of ratio advantage. Output format: three lines — quantities 1, 2 and 3 (6 decimals).

Starter code:

import numpy as np

def ppo_diagnostics(ratio, advantage, clip_epsilon):
    """Return (outside_fraction, active_clip_fraction, approx_kl)."""
    # YOUR CODE HERE
    raise NotImplementedError

if __name__ == "__main__":
    n, eps = input().split()
    rows = [tuple(float(x) for x in input().split()) for _ in range(int(n))]
    r = np.array([row[0] for row in rows])
    a = np.array([row[1] for row in rows])
    for value in ppo_diagnostics(r, a, float(eps)):
        print(f"{value:.6f}")

Open tests:

stdinstdout
5 0.2 ⏎ 0.75 1.0 ⏎ 1.00 1.0 ⏎ 1.30 1.0 ⏎ 1.10 -1.0 ⏎ 0.85 -1.00.400000 ⏎ 0.200000 ⏎ 0.018505

Question P10 · The non-negative sample KL estimate

numeric answer · tolerance ±0.001

Probability ratios on three samples: r=(0.8, 1.0, 1.25)r = (0.8,\ 1.0,\ 1.25).

What is D^=13∑i[(ri−1)−log⁡ri]\widehat D = \frac13\sum_i[(r_i - 1) - \log r_i]?

Question P11 · RLOO and GRPO: the shared idea and the differences

single answer

RLOO and GRPO both generate several responses per prompt and build a relative signal. What separates them in canonical form?

One correct option

Question P12 · What one weight across all tokens does not solve

single answer

A response-level advantage A(y)A(y) is broadcast across every token and multiplied by the mask. What does this construction do correctly, and what does it not?

One correct option