LLM Course documentation

Exercițiu Practic: GRPO cu Unsloth

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Ask a Question Open In Colab

Exercițiu Practic: GRPO cu Unsloth

În acest exercițiu, vei ajusta fin un model cu GRPO (Optimizarea Relativă a Politicii de Grup) folosind Unsloth, pentru a îmbunătăți capacitățile de raționament ale unui model. Am acoperit GRPO în Capitolul 3.

Unsloth este o bibliotecă care accelerează ajustarea fină a LLM-urilor, făcând posibilă antrenarea modelelor mai repede și cu mai puține resurse computaționale. Unsloth se conectează la TRL, deci vom construi pe ceea ce am învățat în secțiunile anterioare, și o vom adapta pentru specificațiile Unsloth.

Acest exercițiu poate fi rulat pe un GPU T4 Google Colab gratuit. Pentru cea mai bună experiență, urmărește notebook-ul legat mai sus și încearcă-l singur.

Instalează dependențele

În primul rând, să instalăm bibliotecile necesare. Vom avea nevoie de Unsloth pentru ajustarea fină accelerată și vLLM pentru inferența rapidă.

pip install unsloth vllm
pip install --upgrade pillow

Configurarea Unsloth

Unsloth oferă o clasă (FastLanguageModel) care integrează transformers cu optimizările Unsloth. Să o importăm:

from unsloth import FastLanguageModel

Acum, să încărcăm modelul Google Gemma 3 1B Instruct și să-l configurăm pentru ajustarea fină:

from unsloth import FastLanguageModel
import torch

max_seq_length = 1024  # Poate crește pentru urme de raționament mai lungi
lora_rank = 32  # Rang mai mare = mai inteligent, dar mai lent

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="google/gemma-3-1b-it",
    max_seq_length=max_seq_length,
    load_in_4bit=True,  # False pentru LoRA 16bit
    fast_inference=True,  # Activează inferența rapidă vLLM
    max_lora_rank=lora_rank,
    gpu_memory_utilization=0.6,  # Reduce dacă rămâi fără memorie
)

model = FastLanguageModel.get_peft_model(
    model,
    r=lora_rank,  # Alege orice număr > 0 ! Sugerat 8, 16, 32, 64, 128
    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],  # Elimină QKVO dacă rămâi fără memorie
    lora_alpha=lora_rank,
    use_gradient_checkpointing="unsloth",  # Activează ajustarea fină pentru context lung
    random_state=3407,
)

Acest cod încarcă modelul în cuantizare 4-bit pentru a economisi memoria și aplică LoRA (Adaptarea de Rang Mic) pentru ajustarea fină eficientă. Parametrul target_modules specifică care straturi ale modelului să fie ajustate fin, și use_gradient_checkpointing permite antrenarea cu contexte mai lungi.

Nu vom acoperi detaliile LoRA în acest capitol, dar poți învăța mai multe în Capitolul 11.

Pregătirea Datelor

Pentru acest exercițiu, vom folosi setul de date GSM8K, care conține probleme de matematică de gimnaziu. Vom formata datele pentru a încuraja modelul să-și arate raționamentul înainte de a oferi un răspuns.

În primul rând, vom defini formatul prompt-urilor și răspunsurilor:

# Definește prompt-ul de sistem care instruiește modelul să folosească un format specific
SYSTEM_PROMPT = """
Răspunde în următorul format:
<reasoning>
...
</reasoning>
<answer>
...
</answer>
"""

XML_COT_FORMAT = """\
<reasoning>
{reasoning}
</reasoning>
<answer>
{answer}
</answer>
"""

Acum, să pregătim setul de date:

import re
from datasets import load_dataset, Dataset


# Funcții helper pentru a extrage răspunsuri din formate diferite
def extract_xml_answer(text: str) -> str:
    answer = text.split("<answer>")[-1]
    answer = answer.split("</answer>")[0]
    return answer.strip()


def extract_hash_answer(text: str) -> str | None:
    if "####" not in text:
        return None
    return text.split("####")[1].strip()


# Funcție pentru a pregăti setul de date GSM8K
def get_gsm8k_questions(split="train") -> Dataset:
    data = load_dataset("openai/gsm8k", "main")[split]
    data = data.map(
        lambda x: {
            "prompt": [
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": x["question"]},
            ],
            "answer": extract_hash_answer(x["answer"]),
        }
    )
    return data


dataset = get_gsm8k_questions()

Setul de date este pregătit prin extragerea răspunsului din setul de date și formatarea acestuia ca șir de caractere.

Definirea Funcțiilor de Recompensă

După cum am discutat într-o pagină anterioară, GRPO poate folosi funcții de recompensă pentru a ghida învățarea modelului bazată pe criterii verificabile precum lungimea și formatarea.

În acest exercițiu, vom defini mai multe funcții de recompensă care încurajează diferite aspecte ale unui raționament bun. De exemplu, vom recompensa modelul pentru oferirea unui răspuns întreg, și pentru urmarea formatului strict.

# Funcția de recompensă care verifică dacă răspunsul este corect
def correctness_reward_func(prompts, completions, answer, **kwargs) -> list[float]:
    responses = [completion[0]["content"] for completion in completions]
    q = prompts[0][-1]["content"]
    extracted_responses = [extract_xml_answer(r) for r in responses]
    print(
        "-" * 20,
        f"Întrebare:\n{q}",
        f"\nRăspuns:\n{answer[0]}",
        f"\nRăspuns model:\n{responses[0]}",
        f"\nExtras:\n{extracted_responses[0]}",
    )
    return [2.0 if r == a else 0.0 for r, a in zip(extracted_responses, answer)]


# Funcția de recompensă care verifică dacă răspunsul este un întreg
def int_reward_func(completions, **kwargs) -> list[float]:
    responses = [completion[0]["content"] for completion in completions]
    extracted_responses = [extract_xml_answer(r) for r in responses]
    return [0.5 if r.isdigit() else 0.0 for r in extracted_responses]


# Funcția de recompensă care verifică dacă completarea urmează formatul strict
def strict_format_reward_func(completions, **kwargs) -> list[float]:
    pattern = r"^<reasoning>\n.*?\n</reasoning>\n<answer>\n.*?\n</answer>\n$"
    responses = [completion[0]["content"] for completion in completions]
    matches = [re.match(pattern, r) for r in responses]
    return [0.5 if match else 0.0 for match in matches]


# Funcția de recompensă care verifică dacă completarea urmează un format mai relaxat
def soft_format_reward_func(completions, **kwargs) -> list[float]:
    pattern = r"<reasoning>.*?</reasoning>\s*<answer>.*?</answer>"
    responses = [completion[0]["content"] for completion in completions]
    matches = [re.match(pattern, r) for r in responses]
    return [0.5 if match else 0.0 for match in matches]


# Funcția de recompensă care numără tag-urile XML și penalizează conținutul extra
def count_xml(text) -> float:
    count = 0.0
    if text.count("<reasoning>\n") == 1:
        count += 0.125
    if text.count("\n</reasoning>\n") == 1:
        count += 0.125
    if text.count("\n<answer>\n") == 1:
        count += 0.125
        count -= len(text.split("\n</answer>\n")[-1]) * 0.001
    if text.count("\n</answer>") == 1:
        count += 0.125
        count -= (len(text.split("\n</answer>")[-1]) - 1) * 0.001
    return count


def xmlcount_reward_func(completions, **kwargs) -> list[float]:
    contents = [completion[0]["content"] for completion in completions]
    return [count_xml(c) for c in contents]

Aceste funcții de recompensă servesc diferite scopuri:

Funcția de Recompensă Scopul
correctness_reward_func Recompensează modelul când răspunsul său se potrivește cu răspunsul corect
int_reward_func Recompensează modelul pentru oferirea unui răspuns numeric
strict_format_reward_func și soft_format_reward_func Recompensează modelul pentru urmarea formatului specificat
xmlcount_reward_func Recompensează utilizarea corectă a tag-urilor XML și penalizează conținutul extra după tag-urile de închidere

Antrenarea cu GRPO

Acum vom configura antrenorul GRPO cu modelul nostru, tokenizer-ul și funcțiile de recompensă. Această parte urmează aceeași abordare ca exercițiul anterior.

from trl import GRPOConfig, GRPOTrainer

max_prompt_length = 256

training_args = GRPOConfig(
    learning_rate=5e-6,
    adam_beta1=0.9,
    adam_beta2=0.99,
    weight_decay=0.1,
    warmup_ratio=0.1,
    lr_scheduler_type="cosine",
    optim="paged_adamw_8bit",
    logging_steps=1,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=1,  # Crește la 4 pentru antrenare mai lină
    num_generations=6,  # Scade dacă rămâi fără memorie
    max_prompt_length=max_prompt_length,
    max_completion_length=max_seq_length - max_prompt_length,
    # num_train_epochs = 1, # Setează la 1 pentru o rulare completă de antrenare
    max_steps=250,
    save_steps=250,
    max_grad_norm=0.1,
    report_to="none",  # Poate folosi Weights & Biases
    output_dir="outputs",
)

trainer = GRPOTrainer(
    model=model,
    processing_class=tokenizer,
    reward_funcs=[
        xmlcount_reward_func,
        soft_format_reward_func,
        strict_format_reward_func,
        int_reward_func,
        correctness_reward_func,
    ],
    args=training_args,
    train_dataset=dataset,
)

GRPOConfig setează diferiți hiperparametri pentru antrenare:

  • use_vllm: Activează inferența rapidă cu vLLM
  • learning_rate: Controlează cât de repede învață modelul
  • num_generations: Numărul de completări de generat pentru fiecare prompt
  • max_steps: Numărul total de pași de antrenare de efectuat

Acum să începem antrenarea:

trainer.train()

Antrenarea poate dura ceva timp. S-ar putea să nu vezi recompensele crescând imediat - poate dura 150-200 de pași înainte să începi să vezi îmbunătățiri. Fii răbdător!

Testarea Modelului

După antrenare, să testăm modelul nostru pentru a vedea cum performează. În primul rând, vom salva greutățile LoRA:

model.save_lora("grpo_saved_lora")

Acum, să testăm modelul cu o întrebare nouă:

from vllm import SamplingParams

text = tokenizer.apply_chat_template(
    [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": "Calculează pi."},
    ],
    tokenize=False,
    add_generation_prompt=True,
)

sampling_params = SamplingParams(
    temperature=0.8,
    top_p=0.95,
    max_tokens=1024,
)
output = (
    model.fast_generate(
        text,
        sampling_params=sampling_params,
        lora_request=model.load_lora("grpo_saved_lora"),
    )[0]
    .outputs[0]
    .text
)

print(output)

Ar trebui să vezi că modelul urmează acum formatul specificat, arătându-și raționamentul înainte de a oferi un răspuns.

Salvarea Modelului

Unsloth oferă mai multe opțiuni pentru salvarea modelului tău ajustat fin, dar ne vom concentra pe cea mai comună.

# Salvează cu precizie de 16 biți
model.save_pretrained_merged("model", tokenizer, save_method="merged_16bit")

Încărcarea pe Hugging Face Hub

Vom încărca modelul pe Hugging Face Hub folosind metoda push_to_hub_merged. Această metodă ne permite să încărcăm modelul în multiple formate de cuantizare.

# Încarcă pe Hugging Face Hub (necesită un token)
model.push_to_hub_merged(
    "numele-tau/numele-modelului",
    tokenizer,
    save_method="merged_16bit",
    token="token-ul-tau",
)

Unsloth suportă de asemenea salvarea în format GGUF pentru utilizare cu llama.cpp:

model.push_to_hub_gguf(
    "numele-tau/numele-modelului",
    tokenizer,
    quantization_method=["q4_k_m", "q8_0", "q5_k_m"],
    token="token-ul-tau",
)

Fișierele GGUF pot fi folosite cu llama.cpp sau sisteme bazate pe UI precum Jan sau Open WebUI.

Concluzie

În acest exercițiu, ai învățat cum să:

  1. Configurezi Unsloth pentru ajustarea fină accelerată
  2. Pregătești datele pentru antrenarea GRPO
  3. Definești funcții de recompensă personalizate pentru a ghida învățarea modelului
  4. Antrenezi un model folosind GRPO
  5. Testezi modelul ajustat fin
  6. Salvezi modelul în diverse formate

GRPO este o tehnică puternică pentru alinierea modelelor de limbaj cu comportamente specifice, iar Unsloth o face accesibilă chiar și pe hardware limitat. Prin combinarea mai multor funcții de recompensă, poți ghida modelul să urmeze un format specific în timp ce îi îmbunătățești și capacitățile de raționament.

Pentru mai multe informații și resurse, verifică:

Update on GitHub