LLM Course documentation
Exercițiu Practic: GRPO cu Unsloth
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 vLLMlearning_rate
: Controlează cât de repede învață modelulnum_generations
: Numărul de completări de generat pentru fiecare promptmax_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ă:
- Configurezi Unsloth pentru ajustarea fină accelerată
- Pregătești datele pentru antrenarea GRPO
- Definești funcții de recompensă personalizate pentru a ghida învățarea modelului
- Antrenezi un model folosind GRPO
- Testezi modelul ajustat fin
- 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