RLOO, Reinforce Leave-One-Out, este o metodă de post-training online pentru modele lingvistice: modelul generează mai multe completări pentru același prompt, fiecare completare primește o recompensă, iar celelalte completări din același grup devin baseline. În practică, păstrezi ideea de rollout-uri și recompense, dar eviți fluxul PPO clasic, cu value model separat și mai multă complexitate operațională.
În acest tutorial construim un proiect complet, local și reproductibil: date IMDB, rollout-uri cu Hugging Face TRL RLOOTrainer, reward de sentiment, baseline leave-one-out, antrenare scurtă, evaluare înainte/după și inferență. Pentru rewarduri mai serioase decât un classifier de sentiment, merită citit separat și ghidul despre Reward Model pentru preference learning. Aici scopul este mecanica RLOO, nu un model final de producție.
Ce construim
Construim un proiect numit rloo-imdb-trl. El ia exemple pozitive din datasetul stanfordnlp/imdb, creează prompturi scurte, generează completări cu sshleifer/tiny-gpt2, calculează recompensa cu un classifier de sentiment și salvează un checkpoint RLOO local în outputs/rloo-imdb-tiny-gpt2.
Rezultatul final verificabil este acesta: un director de model salvat, un script de evaluare care afișează base_mean_reward și trained_mean_reward, plus un script de inferență care încarcă modelul antrenat. Pentru calitatea datelor, logica se leagă natural de pașii din dataset sintetic pentru fine-tuning, dar aici folosim strict IMDB ca să rămânem reproductibili.
Mediul exact
Tutorialul folosește o singură variantă concretă:
Sistem: Ubuntu 24.04 LTS sau WSL2 Ubuntu 24.04
Python: 3.12
RAM: minimum 8 GB
GPU: opțional; varianta de mai jos rulează pe CPU, dar mai lent
Model policy: sshleifer/tiny-gpt2
Dataset: stanfordnlp/imdb
Dataset license pe cardul Hugging Face: other
Dataset size pe cardul Hugging Face: 133190302 bytes
Reward classifier: distilbert/distilbert-base-uncased-finetuned-sst-2-english
torch: 2.14.1
trl: 1.14.2
transformers: 5.19.0
datasets: 5.1.0
accelerate: 1.15.0
sshleifer/tiny-gpt2 este ales pentru viteză și pentru verificarea mecanicii. Nu îl folosi ca dovadă de calitate lingvistică. Dacă vrei să scalezi după ce proiectul merge cap-coadă, verifică separat contaminarea evaluării, ca în Benchmark Contamination Detection.
Ce îți trebuie
Instalezi totul într-un mediu virtual local. Nu rula comenzile direct într-un mediu Python global.
mkdir rloo-imdb-trl
cd rloo-imdb-trl
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Creează fișierul requirements.txt:
torch==2.14.1
trl==1.14.2
transformers==5.19.0
datasets==5.1.0
accelerate==1.15.0
Instalează dependențele:
python -m pip install -r requirements.txt
Ce trebuie să vezi: instalarea se termină fără ERROR. Verifică versiunile:
python - <<'PY'
import torch
import trl
import transformers
import datasets
import accelerate
print("torch", torch.__version__)
print("trl", trl.__version__)
print("transformers", transformers.__version__)
print("datasets", datasets.__version__)
print("accelerate", accelerate.__version__)
PY
Exemplu de output așteptat:
torch 2.14.1
trl 1.14.2
transformers 5.19.0
datasets 5.1.0
accelerate 1.15.0
1. Configurează Accelerate pentru un singur proces
TRL recomandă lansarea antrenării cu accelerate launch. Pentru laborator local folosim un singur proces, fără distribuție.
mkdir -p .cache/accelerate
cat > .cache/accelerate/default_config.yaml <<'EOF'
compute_environment: LOCAL_MACHINE
debug: false
distributed_type: 'NO'
downcast_bf16: 'no'
enable_cpu_affinity: false
machine_rank: 0
main_training_function: main
mixed_precision: 'no'
num_machines: 1
num_processes: 1
rdzv_backend: static
same_network: true
tpu_use_cluster: false
tpu_use_sudo: false
use_cpu: true
EOF
Verifică mediul:
ACCELERATE_CONFIG_FILE=.cache/accelerate/default_config.yaml accelerate env
Ce trebuie să vezi: comanda afișează informații despre Accelerate, iar configurația folosește distributed_type: 'NO'.
2. Creează scriptul de antrenare RLOO
Creează fișierul train_rloo_imdb.py:
import argparse
import os
from pathlib import Path
import torch
from datasets import load_dataset
from transformers import AutoTokenizer, pipeline
from trl import RLOOConfig, RLOOTrainer
MODEL_NAME = "sshleifer/tiny-gpt2"
REWARD_MODEL_NAME = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
OUTPUT_DIR = "outputs/rloo-imdb-tiny-gpt2"
def build_prompt(example):
review = example["text"].replace("n", " ")
review = " ".join(review.split())[:700]
return {
"prompt": (
"Write one short, clearly positive movie comment in English. "
"Use the style and topic hints from this IMDB review excerpt:n"
f"{review}nnPositive comment:"
)
}
def prepare_dataset(limit_train=64, limit_eval=16):
dataset = load_dataset("stanfordnlp/imdb")
train = (
dataset["train"]
.filter(lambda row: row["label"] == 1)
.select(range(limit_train))
.map(build_prompt, remove_columns=dataset["train"].column_names)
)
eval_dataset = (
dataset["test"]
.filter(lambda row: row["label"] == 1)
.select(range(limit_eval))
.map(build_prompt, remove_columns=dataset["test"].column_names)
)
return train, eval_dataset
_reward_pipe = None
def get_reward_pipe():
global _reward_pipe
if _reward_pipe is None:
device = 0 if torch.cuda.is_available() else -1
_reward_pipe = pipeline(
"sentiment-analysis",
model=REWARD_MODEL_NAME,
tokenizer=REWARD_MODEL_NAME,
device=device,
)
return _reward_pipe
def sentiment_reward(prompts, completions, **kwargs):
pipe = get_reward_pipe()
texts = []
for completion in completions:
if isinstance(completion, list):
completion = " ".join(str(item) for item in completion)
texts.append(str(completion))
results = pipe(texts, truncation=True, max_length=256)
rewards = []
for item in results:
score = float(item["score"])
if item["label"].upper() == "POSITIVE":
rewards.append(score)
else:
rewards.append(-score)
return rewards
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--max-steps", type=int, default=10)
parser.add_argument("--train-limit", type=int, default=64)
parser.add_argument("--eval-limit", type=int, default=16)
args = parser.parse_args()
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
train_dataset, eval_dataset = prepare_dataset(
limit_train=args.train_limit,
limit_eval=args.eval_limit,
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.padding_side = "left"
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
rloo_args = RLOOConfig(
output_dir=OUTPUT_DIR,
seed=47,
per_device_train_batch_size=2,
per_device_eval_batch_size=2,
gradient_accumulation_steps=1,
num_generations=2,
max_completion_length=32,
learning_rate=1e-6,
beta=0.05,
max_steps=args.max_steps,
logging_steps=1,
save_steps=args.max_steps,
eval_steps=args.max_steps,
report_to="none",
bf16=False,
fp16=False,
remove_unused_columns=False,
)
trainer = RLOOTrainer(
model=MODEL_NAME,
args=rloo_args,
reward_funcs=sentiment_reward,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
processing_class=tokenizer,
)
trainer.train()
trainer.save_model(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
print(f"Saved RLOO model to {OUTPUT_DIR}")
if __name__ == "__main__":
main()
Aici sunt piesele importante. num_generations=2 produce două completări pentru același prompt. RLOO folosește recompensa fiecărei completări și baseline-ul format din celelalte completări ale aceluiași prompt. beta=0.05 păstrează penalizarea KL față de politica de referință. Diferența față de un environment cu stare, ca în GRPO pentru agenți AI, este că aici recompensa vine din classifierul de sentiment, nu dintr-o interacțiune cu tool-uri.
3. Rulează antrenarea
HF_HOME=.cache/huggingface
ACCELERATE_CONFIG_FILE=.cache/accelerate/default_config.yaml
accelerate launch train_rloo_imdb.py --max-steps 10 --train-limit 64 --eval-limit 16
Ce trebuie să vezi: la prima rulare se descarcă datasetul și modelele de pe Hugging Face Hub. Apoi apar loguri de training. La final trebuie să apară:
Saved RLOO model to outputs/rloo-imdb-tiny-gpt2
Verifică fișierele salvate:
find outputs/rloo-imdb-tiny-gpt2 -maxdepth 1 -type f | sort
Ce trebuie să vezi: fișiere de configurație, tokenizer și greutăți de model. Numele exact al fișierului de greutăți poate diferi în funcție de formatul de salvare folosit de biblioteci.
4. Adaugă evaluarea înainte/după
Creează fișierul evaluate_reward.py:
import argparse
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
BASE_MODEL = "sshleifer/tiny-gpt2"
REWARD_MODEL_NAME = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
def build_prompts(limit):
dataset = load_dataset("stanfordnlp/imdb", split="test")
positives = dataset.filter(lambda row: row["label"] == 1).select(range(limit))
prompts = []
for row in positives:
review = " ".join(row["text"].replace("n", " ").split())[:700]
prompts.append(
"Write one short, clearly positive movie comment in English. "
"Use the style and topic hints from this IMDB review excerpt:n"
f"{review}nnPositive comment:"
)
return prompts
def generate(model_path, prompts):
tokenizer = AutoTokenizer.from_pretrained(model_path)
tokenizer.padding_side = "left"
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(model_path)
model.eval()
outputs = []
for prompt in prompts:
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512)
with torch.no_grad():
generated = model.generate(
**inputs,
max_new_tokens=32,
do_sample=True,
temperature=0.8,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id,
)
decoded = tokenizer.decode(generated[0], skip_special_tokens=True)
outputs.append(decoded[len(prompt):].strip())
return outputs
def score(texts):
device = 0 if torch.cuda.is_available() else -1
reward_pipe = pipeline(
"sentiment-analysis",
model=REWARD_MODEL_NAME,
tokenizer=REWARD_MODEL_NAME,
device=device,
)
results = reward_pipe(texts, truncation=True, max_length=256)
values = []
for item in results:
value = float(item["score"])
if item["label"].upper() != "POSITIVE":
value = -value
values.append(value)
return values
def mean(values):
return sum(values) / len(values)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--trained-model", default="outputs/rloo-imdb-tiny-gpt2")
parser.add_argument("--limit", type=int, default=8)
args = parser.parse_args()
prompts = build_prompts(args.limit)
base_outputs = generate(BASE_MODEL, prompts)
trained_outputs = generate(args.trained_model, prompts)
base_scores = score(base_outputs)
trained_scores = score(trained_outputs)
print(f"base_mean_reward={mean(base_scores):.4f}")
print(f"trained_mean_reward={mean(trained_scores):.4f}")
print("nSample trained completion:")
print(trained_outputs[0])
if __name__ == "__main__":
main()
Rulează evaluarea:
python evaluate_reward.py --limit 8
Ce trebuie să vezi: două linii cu scoruri medii și o completare generată:
base_mean_reward=<valoare numerică>
trained_mean_reward=<valoare numerică>
Sample trained completion:
<text generat>
Nu interpreta automat o diferență mică drept aliniere reală. Cu 10 pași și tiny-gpt2, verifici că rollout-urile, rewardul, baseline-ul și salvarea checkpointului funcționează.
5. Rulează inferența pe modelul salvat
Creează infer.py:
import argparse
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="outputs/rloo-imdb-tiny-gpt2")
parser.add_argument(
"--prompt",
default=(
"Write one short, clearly positive movie comment in English. "
"The film has strong acting and a warm ending.nnPositive comment:"
),
)
args = parser.parse_args()
tokenizer = AutoTokenizer.from_pretrained(args.model)
tokenizer.padding_side = "left"
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(args.model)
model.eval()
inputs = tokenizer(args.prompt, return_tensors="pt")
with torch.no_grad():
output = model.generate(
**inputs,
max_new_tokens=40,
do_sample=True,
temperature=0.8,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id,
)
print(tokenizer.decode(output[0], skip_special_tokens=True))
if __name__ == "__main__":
main()
Rulează:
python infer.py
Ce trebuie să vezi: promptul plus o completare generată. Textul poate fi slab sau repetitiv, pentru că modelul este foarte mic. Verificarea reală este că modelul salvat se încarcă și produce output.
Verificarea finală
Rulează cele trei verificări:
test -d outputs/rloo-imdb-tiny-gpt2
python evaluate_reward.py --limit 8
python infer.py
Rezultatul așteptat:
Prima comandă iese fără mesaj și fără eroare.
A doua comandă afișează base_mean_reward și trained_mean_reward.
A treia comandă afișează un prompt și o completare generată.
Dacă ai aceste trei rezultate, proiectul RLOO este complet: date, rollout-uri, reward, baseline leave-one-out, checkpoint, evaluare și inferență.
Probleme frecvente
Mesaj:
ModuleNotFoundError: No module named 'trl'
Cauză: mediul virtual nu este activ sau dependențele nu au fost instalate. Rezolvare:
source .venv/bin/activate
python -m pip install -r requirements.txt
Mesaj:
ValueError: The effective batch size ... must be evenly divisible by num_generations
Cauză: în RLOO, batch-ul efectiv trebuie să se împartă exact la num_generations. În tutorial, per_device_train_batch_size=2, gradient_accumulation_steps=1, num_processes=1 și num_generations=2, deci regula este respectată. Dacă schimbi batch-ul, păstrează împărțirea exactă.
Mesaj:
RuntimeError: CUDA out of memory
Cauză: modelul, classifierul de reward sau batch-ul nu încap în VRAM. Rezolvare pentru acest tutorial: păstrează sshleifer/tiny-gpt2, rulează pe CPU cu use_cpu: true sau redu max_completion_length. Pentru training distribuit cu noduri instabile, contextul este diferit; vezi separat TorchElastic pentru training distribuit.
Mesaj:
OSError: sshleifer/tiny-gpt2 is not a local folder and is not a valid model identifier
Cauză: nu există conexiune la Hugging Face Hub, numele modelului a fost modificat sau cache-ul local este corupt. Rezolvare: verifică internetul, păstrează exact numele modelului și șterge doar cache-ul proiectului:
rm -rf .cache/huggingface
Revenire și curățenie
Pentru a șterge artefactele de training, dar nu și codul:
rm -rf outputs .cache/huggingface
Pentru a elimina complet mediul local:
deactivate || true
rm -rf .venv outputs .cache
Nu șterge ~/.cache/huggingface dacă este folosit de alte proiecte. Tutorialul a folosit intenționat cache local în .cache/huggingface ca să poți curăța proiectul fără să afectezi alte experimente.
Mai departe
După ce pipeline-ul merge cap-coadă, înlocuiește rewardul de sentiment cu un reward model calibrat pe perechi chosen/rejected, crește modelul policy și rulează mai mulți pași. Păstrează aceeași disciplină: dataset controlat, reward verificabil, evaluare separată, apoi scalare.
RLOO nu elimină auditul datelor, evaluarea umană sau verificarea regresiilor. Îți oferă o buclă online mai simplă decât PPO clasic pentru anumite scenarii RLHF, dar calitatea finală depinde de date, reward, model și evaluare.
Întrebări frecvente
RLOO înlocuiește complet PPO?
Nu. În acest tutorial, RLOO este folosit ca variantă mai simplă pentru post-training online cu rollout-uri, reward, baseline leave-one-out și KL penalty. Alegerea finală depinde de model, reward, buget și evaluare.
De ce folosim num_generations=2?
Pentru că RLOO are nevoie de mai multe completări pentru același prompt ca să calculeze baseline-ul leave-one-out. Cu num_generations=2, fiecare completare este comparată cu cealaltă din grup.
Rewardul de sentiment este suficient pentru producție?
Nu. Rewardul de sentiment din tutorial este doar o metodă reproductibilă pentru laborator. Pentru producție ai nevoie de reward model calibrat, date curate și evaluare separată.
De ce rezultatele modelului tiny pot fi slabe?
sshleifer/tiny-gpt2 este ales pentru viteză și pentru verificarea mecanicii RLOO. Modelul este prea mic pentru a demonstra calitate lingvistică robustă după doar câțiva pași.

















































