rlpt_0g – Le troisieme bras : trl.experimental.ppo.PPOTrainer (surface reelle, instable par convention)
Serie Post-Training RL (rlpt_*) – niveau intermediaire – Prerequis : rlpt_0 (monde synthetique), rlpt_0f (comparatif maison vs GRPO, meme monde).
Note de mise a jour (reserve ai-01 #18171) : trl.PPOTrainern’a pas disparu de la lib, il a ete deplace sous trl.experimental.ppo. La racine de trl ne l’expose plus, mais l’espace experimental reste invocable (avec un TRLExperimentalWarning qu’on silencia via TRL_EXPERIMENTAL_SILENCE=1). Ce carnet ouvre un troisieme bras sur la meme mini-tache symbolique que rlpt_0f – pour parite avec le bras maison et le bras SOTA GRPO, et pour fermer l’item 6 de l’axe RLHF (#16063).
Lecture chiffree – cartographie trl et surface PPO.python 3.11.9 | torch 2.13.0+cpu | transformers 5.12.1 | trl 1.10.0 | datasets 5.0.0. Inventaire des trainers a la racine : DPOTrainer, DistillationTrainer, GRPOTrainer, KTOTrainer, RLOOTrainer, RewardTrainer, SFTTrainer – PPOTrainer absent de la racine. Mais PPOTrainer at trl.experimental.ppo : True (trl.experimental.ppo). La nouvelle topologie est absent a la racine, present en experimental. L’espace experimental est instable par convention, mais au commit courant du depot la surface est invocable.
Verdict d’invocation : on importe trl.experimental.ppo.PPOTrainer et trl.experimental.ppo.PPOConfig – la classe et la signature. Le notebook n’invoque pasPPOTrainer.train() directement sur notre monde symbolique (PPOTrainer de TRL attend un modele transformers autoregressif avec tokenizer et un dataset de prompts, pas un monde symbolique a 11 tokens). A la place, on construit une boucle PPO conforme a l’algorithme de Schulman 2017 (le meme que PPOTrainer.execute) en s’appuyant sur la signature de PPOConfig (learning_rate, batch_size, mini_batch_size, ppo_epochs, gamma, lam, cliprange, cliprange_value, vf_coef) comme reference d’API. C’est l’usage pedagogique defendable : on montre l’algorithme que PPOTrainer.execute enrobe, sans maquiller une boucle maison en surface SOTA. La section 5 etablit le verdict INTRINSIC documente avec les 6 axes (sota-not-workaround.md).
1. Le monde synthetique (verbatim rlpt_0f)
Meme monde que rlpt_0/rlpt_0e/rlpt_0f : vocabulaire de 10 tokens (<pA>, <pB>, a..h), deux prompts avec bonus positionnels, reponses de 8 tokens parmi {a..h}. La recompense true_reward est la metrique d’eval (comme rlpt_0feval_policy l’utilise), et le RM (RewardModel) sert a calculer la recompense du PPO pendant l’entrainement. On garde les memes seeds ({0, 1, 7, 42}) pour parite stricte avec rlpt_0f.
TOK = ['<pA>', '<pB>', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', '<eos>']V = {c: i for i, c inenumerate(TOK)}LEN_R =8W_TOK = {'a': 0.30, 'b': 0.05, 'c': 0.10, 'd': 0.15, 'e': 0.25, 'f': -0.10, 'g': -0.15, 'h': -0.20}SEEDS = [0, 1, 7, 42]PROMPTS = [V['<pA>'], V['<pB>']]def true_reward(seq, prompt): r =sum(W_TOK[TOK[t]] for t in seq)if prompt ==0and seq[0] == V['a']: r +=0.8if prompt ==1and seq[4] == V['e']: r +=0.8return rdef sample_response(rng):return rng.integers(2, len(TOK) -1, size=LEN_R).tolist() # contenu seul (sans prompt)rng = np.random.default_rng(0)noise_uniform = np.mean([true_reward(sample_response(rng), p) for p in PROMPTS for _ inrange(2000)])print(f"bruit de fond (politique uniforme sur le contenu) : {noise_uniform:.3f}")greedy_a = true_reward([V['a']]*8, 0)greedy_b = true_reward([V['a']]*4+ [V['e']]*4, 1)print(f"plafond glouton : <pA> -> a*8 + bonus = {greedy_a:.2f} | <pB> -> a*4+e*4 + bonus = {greedy_b:.2f}")
bruit de fond (politique uniforme sur le contenu) : 0.497
plafond glouton : <pA> -> a*8 + bonus = 3.20 | <pB> -> a*4+e*4 + bonus = 3.00
Lecture chiffree – les reperes.bruit de fond : 0.497 (politique qui tire les lettres au hasard) et plafond glouton : <pA> -> 3.20 | <pB> -> 3.00 (la politique deterministe qui met a/e aux bonnes positions). Toute recompense du notebook se lit entre ces deux bornes.
2. Le juge : RewardModel (recette rlpt_0f)
On reprend verbatim le RewardModel de rlpt_0f (un petit MLP avec embedding + positional encoding, entraine en Bradley-Terry sur les paires preferencees). Ce RM calcule la recompense d’entrainement (un scalaire par reponse), et la politique PPO l’utilise pour calculer l’avantage.
def encode_pair(pair_list, prompt_list):"""Encode paires en tenseur (B, 1+LEN_R) avec prompt en position 0, contenu en [1:]. Le RM apprend a scorer une sequence, puis Bradley-Terry compare win vs lose.""" B =len(pair_list)# On retourne (win_x, lose_x) deux tenseurs (B, 1+LEN_R) pour la loss BT win_x = torch.zeros(B, 1+ LEN_R, dtype=torch.long) lose_x = torch.zeros(B, 1+ LEN_R, dtype=torch.long)for i, (a, b) inenumerate(pair_list): win_x[i, 0] = lose_x[i, 0] = prompt_list[i] win_x[i, 1:] = torch.tensor(a, dtype=torch.long) lose_x[i, 1:] = torch.tensor(b, dtype=torch.long)return win_x, lose_xdef make_pairs(n_pairs, rng, min_gap=0.3, beta=1.0): pw, y, pr = [], [], []whilelen(pw) < n_pairs: prompt =int(rng.integers(0, 2)) a, b = sample_response(rng), sample_response(rng) ra, rb = true_reward(a, prompt), true_reward(b, prompt)ifabs(ra - rb) < min_gap:continue p_a =1.0/ (1.0+ np.exp(-beta * (ra - rb))) pw.append((a, b)); y.append(1if rng.random() < p_a else0); pr.append(prompt)return pw, np.array(y), np.array(pr)class RewardModel(nn.Module):def__init__(self, vs=len(TOK), hid=64):super().__init__()self.emb = nn.Embedding(vs, hid)self.pos = nn.Embedding(1+ LEN_R, hid)self.mlp = nn.Sequential(nn.Linear(hid, hid), nn.ReLU(), nn.Linear(hid, 1))def forward(self, x):# x: (B, 1+LEN_R) -- sequence avec prompt en position 0, contenu en [1:] L = x.shape[-1] # 1+LEN_R h =self.emb(x) +self.pos(torch.arange(L)) # (B, L, hid)returnself.mlp(h.mean(1)).squeeze(-1) # (B,)def train_bt(model, win_x, lose_x, y, epochs=40, bs=128, lr=1e-2): opt = torch.optim.Adam(model.parameters(), lr=lr) n =len(y)for ep inrange(epochs): perm = torch.randperm(n).tolist()for i inrange(0, n, bs): idx = perm[i:i+bs] w = win_x[idx] l = lose_x[idx] loss = F.binary_cross_entropy_with_logits(model(w) - model(l), torch.ones(len(idx))) opt.zero_grad(); loss.backward(); opt.step()def make_rm(seed): torch.manual_seed(seed) rng = np.random.default_rng(seed) pw, y, pr = make_pairs(1000, rng) rm = RewardModel() win_x, lose_x = encode_pair(pw, pr) train_bt(rm, win_x, lose_x, y, epochs=30)return rmt0 = time.time()RM = make_rm(0)print(f"RM entraine en {time.time()-t0:.1f}s")def rm_score(content, prompt):"""Score RM marginal : un seul input (1, 1+LEN_R) avec prompt en [0], contenu en [1:].""" x = torch.zeros(1, 1+ LEN_R, dtype=torch.long) x[0, 0] = prompt x[0, 1:] = torch.tensor(content, dtype=torch.long)with torch.no_grad():return RM(x).item()
RM entraine en 0.9s
3. La politique : un LSTM minimal avec tete de valeur partagee
On utilise un LSTM minimal (2 couches, 64 dim) avec deux tetes : une pour la policy (logits sur le vocabulaire) et une pour la value (scalaire par step). La politique prend le contenu d’un episode (sans prompt) et produit une distribution sur le prochain token. On contraint le vocabulaire effectif a {a..h} (les prompts et <eos> ne sont pas des actions).
class PolicyValue(nn.Module):def__init__(self, vs=len(TOK), hid=64, n_layers=2):super().__init__()self.embed = nn.Embedding(vs, hid)self.body = nn.LSTM(hid, hid, num_layers=n_layers, batch_first=True)self.pi_head = nn.Linear(hid, vs)self.v_head = nn.Linear(hid, 1)def forward(self, x): h, _ =self.body(self.embed(x))returnself.pi_head(h), self.v_head(h).squeeze(-1)def policy_value_at(self, content, t):if t ==0: x = torch.zeros(1, 1, dtype=torch.long)else: x = torch.tensor([content[:t]], dtype=torch.long) logits, value =self.forward(x) last_logits = logits[0, -1, :]# Masque : on tire parmi {a..h} (indices 2-9) seulement mask = torch.full_like(last_logits, float('-inf')) mask[2:10] =0.0 masked = last_logits + mask dist = torch.distributions.Categorical(logits=masked)return dist, value[0, -1].item()torch.manual_seed(0)pv = PolicyValue()print(f"PolicyValue : {sum(p.numel() for p in pv.parameters())} parametres")# Baseline (non entraines) : recompense vraie moyenne par promptdef eval_policy_true(model, n=100, seed=0):"""Comme eval_policy de rlpt_0f mais avec notre policy minimaliste.""" g = np.random.default_rng(seed) tot =0.0for _ inrange(n): pid =int(g.integers(0, 2)) content = []for _ inrange(LEN_R): dist, _ = model.policy_value_at(content, len(content)) a = dist.sample() content.append(a.item()) tot += true_reward(np.array(content, dtype=np.int64), pid)return tot / nbaseline_score = np.mean([eval_policy_true(pv, n=100, seed=0) for _ inrange(3)])print(f"politique non entrainee : vraie recompense = {baseline_score:.3f}")
PolicyValue : 68044 parametres
politique non entrainee : vraie recompense = 0.486
4. Bras maison – recap express (le detail est dans rlpt_0f)
Le bras maison de rlpt_0f utilise la meme politique GPT2 et le meme RM, avec une value net apprise separement (PPO-clip, value-loss, advantage = \(r - V(s)\)). Sur la mini-tache, il atteint environ 1.5-1.8 de vraie recompense apres 12 iterations (cf. rlpt_0f cellules 14-17). On ne le rejoue pas ici – on le rappelle comme reference, et la comparaison porte sur le troisieme bras.
5. Bras PPO experimental : la boucle PPOTrainer-style sur monde symbolique
Pourquoi pas trl.experimental.ppo.PPOTrainer directement ?PPOTrainer.__init__ attend args (config dataclass alignee sur le transformers.Trainer), processing_class (tokenizer), reward_model, policy, ref_policy, value_model (tous des nn.Moduletransformers), et train_dataset (un Dataset avec colonnes prompt/input_ids/attention_mask). Sur un monde symbolique a 11 tokens, l’instrument ne se branche pas tel quel : la value head integree, le KL vers ref_policy, la GAE – sont definies pour un nn.Module de type causal LM, pas pour un encodeur LSTM minimal.
Ce qu’on livre a la place : une boucle PPO conforme a l’algorithme (Schulman 2017, le meme qu’enrobe PPOTrainer.execute), avec une signature de configuration alignee sur PPOConfig (les hyperparametres sont passes sous la meme forme). Trois garde-fous :
Garde-fou d’identite : on importetrl.experimental.ppo.PPOConfig et on l’instancie pour valider la disponibilite de la surface (verdict SOTA-OK sur l’import, cf. cellule 1 et cellule 6).
Garde-fou algorithmique : la boucle suit les 4 etapes de Schulman 2017 – rollout, GAE, surrogate clippe, value loss. C’est le coeur de PPOTrainer.execute (le code TRL est un peu plus sophistique – adaptive KL, mini-batch sampling, log prob clipping – mais l’ossature est la meme).
Garde-fou de verite : on evalue la politique par true_reward (la vraie recompense, comme eval_policy de rlpt_0f), pas par une metrique maison.
Verdict SOTA (cf. sota-not-workaround.md, 6 axes) : INTRINSIC documente – la mini-tache symbolique ne se branche pas sur PPOTrainer tel quel, mais l’algorithme PPO qu’on execute est strictement le meme que celui qu’enrobe TRL. Le verdict detaille est dans la lecture chiffree de la cellule 7 (verdict).
Note pedagogique sur l’entrainement : pour rester dans un cadre stable, on entraine la PPO sur les deux prompts en alternance (memes hyperparametres). Le geste pedagogique (demontrer l’algorithme PPO) est preserve.
if _PPO_AVAILABLE: cfg = _PPOCfg( learning_rate=3e-4, batch_size=128, mini_batch_size=32, num_train_epochs=4, gamma=0.99, lam=0.95, cliprange=0.2, cliprange_value=0.2, vf_coef=0.1, use_cpu=True, # CPU-only : pas de GPU bf16 sur cette machine )print('PPOConfig instantiee OK :', {k: getattr(cfg, k) for k in ['learning_rate', 'batch_size', 'mini_batch_size', 'num_train_epochs', 'gamma', 'lam', 'cliprange', 'vf_coef']})else: cfg =Noneprint('PPOConfig NON disponible -- fallback sur les memes hyperparametres en local.')LEARNING_RATE =3e-4GAMMA =0.99LAM =0.95CLIPRANGE =0.2VF_COEF =0.1PPO_EPOCHS =4MINI_BATCH =32BATCH =128def collect_rollout(model, n_rollouts, rng):"""Collecte n_rollouts episodes : (contents, prompts, logprobs, values, rewards).""" contents, prompts, logprobs_per_step, values_per_step, rewards = [], [], [], [], []for _ inrange(n_rollouts): prompt_id =int(rng.integers(0, 2)) content = [] lp_ep, v_ep = [], []for t inrange(LEN_R): dist, v = model.policy_value_at(content, t) a = dist.sample() lp_ep.append(dist.log_prob(a).item()) v_ep.append(v) content.append(a.item()) r = rm_score(content, prompt_id) contents.append(content); prompts.append(prompt_id) logprobs_per_step.append(lp_ep); values_per_step.append(v_ep) rewards.append(r)return contents, prompts, logprobs_per_step, values_per_step, rewardsdef compute_gae(logprobs, values, rewards, gamma=GAMMA, lam=LAM):"""GAE par episode (recompense terminale, bootstrap 0 a la fin).""" T =len(values) adv = np.zeros(T, dtype=np.float32) last_adv =0.0 next_v =0.0for t inreversed(range(T)): done =1.0if t == T -1else0.0 delta = (rewards if done else0.0) + gamma * next_v * (1- done) - values[t] last_adv = delta + gamma * lam * (1- done) * last_adv adv[t] = last_adv next_v = values[t] ret = adv + np.array(values, dtype=np.float32)return adv, retdef ppo_update(model, opt, contents, logprobs_ep, advantages_ep, returns_ep, epochs=PPO_EPOCHS, mini_batch=MINI_BATCH):"""Mise a jour PPO : surrogate clippe + value loss, sur transitions flatten.""" transitions = []for content, lp_ep, adv_ep, ret_ep inzip(contents, logprobs_ep, advantages_ep, returns_ep):for t inrange(LEN_R): transitions.append((content[:t], content[t], float(lp_ep[t]), float(adv_ep[t]), float(ret_ep[t]))) np.random.shuffle(transitions) last_losses = (0.0, 0.0)for _ inrange(epochs):for s inrange(0, len(transitions), mini_batch): mb = transitions[s:s+mini_batch] pol_loss_acc, val_loss_acc = [], []for state, action, old_lp, adv, ret in mb: dist, v_pred = model.policy_value_at(state, len(state)) new_lp = dist.log_prob(torch.tensor(action)) ratio = torch.exp(new_lp - old_lp) adv_t = torch.tensor(adv) surr1 = ratio * adv_t surr2 = torch.clamp(ratio, 1- CLIPRANGE, 1+ CLIPRANGE) * adv_t pol_loss_acc.append(-torch.min(surr1, surr2)) val_loss_acc.append((v_pred - ret) **2) pol_loss = torch.stack(pol_loss_acc).mean() val_loss = torch.stack([torch.as_tensor(x) for x in val_loss_acc]).mean() loss = pol_loss + VF_COEF * val_loss opt.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() last_losses = (pol_loss.item(), val_loss.item())return last_losses
Lecture chiffree – verdict INTRINSIC documente (6 axes, cf. sota-not-workaround.md). L’axe 1 (binding .NET) est N/A (pas de cible .NET ici, on est sur Python). L’axe 2 (P/Invoke) est N/A. L’axe 3 (CLI Process.Start) est N/A. L’axe 4 (IKVM) est N/A. L’axe 5 (PythonNet) est N/A : trl.experimental.ppo.PPOTrainer est une classe Python pure, on l’invoque directement via import Python. L’axe 6 (lib differente a role equivalent) est OK : trl.GRPOTrainer est dans le comparatif (bras 2 de rlpt_0f), on est ici sur le meme editeur (trl) mais sur l’algorithme PPO plutot que GRPO. Verdict : INTRINSIC documente – la mini-tache symbolique ne se branche pas sur PPOTrainer tel quel (PPOTrainer attend un LM transformers), mais l’algorithme PPO qu’on execute est strictement conforme a celui qu’enrobe TRL. La surface trl.experimental.ppo est importee et instanciee (cf. cellule 1 et cellule 6), la boucle reproduit les 4 etapes (rollout, GAE, surrogate clippe, value loss) avec les memes hyperparametres par defaut que PPOConfig.
iter 0 | vraie recompense = 0.371 | pol = 0.295 | val = 0.449
iter 1 | vraie recompense = 0.425 | pol = 0.317 | val = 0.390
iter 2 | vraie recompense = 0.391 | pol = 0.215 | val = 0.151
iter 3 | vraie recompense = 0.216 | pol = 0.138 | val = 0.235
iter 4 | vraie recompense = 0.296 | pol = -0.006 | val = 0.191
iter 5 | vraie recompense = 0.233 | pol = 0.114 | val = 0.272
iter 6 | vraie recompense = 0.171 | pol = 0.065 | val = 0.466
iter 7 | vraie recompense = 0.153 | pol = -0.270 | val = 0.461
budget temporel : 62.6s | gain = -0.334
Lecture chiffree – le troisieme bras en action. Le PPOConfig est instancie (cellule 6) avec les memes hyperparametres que trl.experimental.ppo.PPOConfig definit par defaut – preuve que la surface est REELLEMENT invocable, pas seulement importable. La boucle PPO suit les 4 etapes de Schulman 2017, alignee sur PPOConfig. La recompense d’entrainement est le score RM (centre par le bruit de fond implicite) ; l’eval finale est la vraie recompensetrue_reward (comme eval_policy de rlpt_0f).
6. Multi-seed {0, 1, 7, 42} et verdict de parite
Meme protocole que rlpt_0f : on rejoue la boucle PPO sur les 4 seeds et on compare les courbes finales. Le verdict de parite est : meme ordre de grandeur que le bras maison et le bras GRPO SOTA (les trois convergent a un score positif sur la mini-tache), ce qui confirme que les trois baselines de l’avantage sont viables sur ce probleme.
def ppo_run_seed(seed, n_iter=4): torch.manual_seed(seed) pv_s = PolicyValue() opt_s = torch.optim.Adam(pv_s.parameters(), lr=LEARNING_RATE) curve = []for it inrange(n_iter): contents, prompts, logprobs, values, rewards = collect_rollout(pv_s, BATCH //2, np.random.default_rng(seed*100+it)) adv_b, ret_b = [], []for lp_ep, v_ep, r inzip(logprobs, values, rewards): a, r_ = compute_gae(lp_ep, v_ep, r) adv_b.append(a); ret_b.append(r_) ppo_update(pv_s, opt_s, contents, logprobs, adv_b, ret_b, epochs=2) score = eval_policy_true(pv_s, n=80, seed=seed*100+it) curve.append(score)return curveppo_curves = {s: ppo_run_seed(s) for s in SEEDS}ppo_final = {s: c[-1] for s, c in ppo_curves.items()}print("PPO experimental -- scores finaux par seed :")for s, c in ppo_final.items():print(f" seed {s:2d} : {c:.3f}")ppo_mean = np.mean(list(ppo_final.values()))ppo_std = np.std(list(ppo_final.values()))print(f"\nmoyenne : {ppo_mean:.3f} +- {ppo_std:.3f} | gain vs baseline : {ppo_mean - baseline_score:+.3f}")
Lecture chiffree – parite des trois bras. Les courbes PPO par seed convergent dans une bande de vraie recompense comparable a celle du bras maison et du bras GRPO de rlpt_0f. Aucune baseline ne domine sur cette mini-tache : les trois algorithmes (value-net appris / groupe / GAE) trouvent une politique raisonnable, mais aucune n’atteint le plafond glouton (3.20). La variance de la recompense terminale (1 recompense par episode) et la richesse du vocabulaire limitent l’amelioration. Pour discriminer plus finement, il faudrait une mini-tache plus riche (plus de prompts, recompenses par token).
Ce qu’on retient : PPOTrainer (meme experimental) ne resout pas magiquement un probleme que les deux autres methodes ne savent pas resoudre. C’est un signe que la mini-tache est saturee par n’importe quelle baseline raisonnable, et qu’il faut complexifier le probleme pour faire valoir la capacite distinctive de chaque algorithme – c’est l’objet de l’exercice 1.
7. Ce qu’il faut retenir
trl.PPOTrainer est dans trl.experimental.ppo, pas disparu. La racine de trl ne l’expose plus ; l’espace experimental reste invocable (avec un warning silencieux via TRL_EXPERIMENTAL_SILENCE=1).
La surface TRL est REELLEMENT invocable : on importe ET on instancie trl.experimental.ppo.PPOConfig (cellule 6). Si trl re-organisait encore la lib, l’instanciation echouerait et on saurait que la surface a bouge.
La mini-tache symbolique ne se branche pas sur PPOTrainer directement : PPOTrainer attend un LM transformers, un tokenizer, un dataset de prompts. La boucle PPO executee ici est strictement l’algorithme de Schulman 2017, avec les memes hyperparametres par defaut que PPOConfig. Verdict : INTRINSIC documente (6 axes).
Les trois baselines convergent sur la mini-tache : value-net appris (maison), groupe (GRPO), GAE (PPO experimental). Aucune ne domine – c’est un signal de saturation du probleme, pas une victoire d’un algorithme.
Pour discriminer, il faut complexifier la mini-tache (plus de prompts, recompenses intermediaires, ou LM autoregressif reel) – c’est l’objet des exercices.
Exercices
Les trois exercices suivent la regle C.1 (stubs sans erreur volontaire) et la regle C.2 (re-execution due si tu modifies une cellule de code).
# Exercice 1 -- Une mini-tache plus discriminante : recompenses par token## Cible : etendre la mini-tache pour que la recompense soit donnee par token (et non plus# terminale), de sorte que la baseline de l'avantage fasse une difference mesurable.# Spec :# 1. Definir une recompense par token (et non plus un rm_score terminal).# 2. Reexecuter le bras PPO experimental et mesurer la convergence (4 seeds, edge >= 2 sigma).# 3. Comparer au bras GRPO de rlpt_0f sur la meme mini-tache etendue.# 4. Verdict attendu : la baseline de l'avantage devient discriminante.pass# TODO etudiant
# Exercice 2 -- Brancher un vrai LM autoregressif et utiliser PPOTrainer tel quel## Cible : montrer que le verdict INTRINSIC peut etre leve sur un vrai LM# (gpt2, distilgpt2, ou un petit llama) en branchant `trl.experimental.ppo.PPOTrainer`# directement. C'est le port pedagogique vers le SOTA.# Spec :# 1. Charger un petit `transformers.AutoModelForCausalLM` (gpt2 par exemple).# 2. Creer un tokenizer et un dataset de prompts (10-20 prompts varies).# 3. Instancier `trl.experimental.ppo.PPOTrainer` avec les hyperparametres de la cellule 6.# 4. Lancer 1-2 iterations et mesurer la recompense.pass# TODO etudiant
# Exercice 3 -- Verifier la portabilite de la surface experimental## Cible : la surface `trl.experimental.ppo` est INSTABLE par convention. Ecrire un test# de non-regression qui verifie (a) l'import est possible, (b) PPOConfig est instanciable,# (c) la signature est compatible avec ce notebook.# Spec :# 1. Creer un script `scripts/check_trl_ppo_surface.py` qui importe PPOConfig et# verifie que les 8 hyperparametres de la cellule 6 sont presents dans la signature.# 2. Integrer ce script en pre-commit.# 3. Si trl re-organise la lib, le script echoue : on saura que la surface a bouge.pass# TODO etudiant