Skip to main content

Cyber AI România

Vineri, 23 octombrie 2026

RLOO cu TRL: rollout-uri, rewards și baseline leave-one-out fără PPO clasic

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.

Surse

Facebook
X
WhatsApp
RLOO cu TRL: rollout-uri, rewards și baseline leave-one-out fără PPO clasic

Te-ar putea interesa si: