GenerAI / finetune.py
amogaddy's picture
Upload finetune.py with huggingface_hub
c522400 verified
Raw
History Blame Contribute Delete
8.31 kB
"""
finetune.py
===========
Fine-tuning con LoRA/QLoRA di un LLM su dataset.jsonl.
Usa TRL SFTTrainer β€” funziona su GPU consumer (4GB+) o su Google Colab gratuito.
Modello base consigliato (leggero, multilingue, italiano):
Qwen/Qwen2.5-0.5B-Instruct ← 0.5B param, ottimo su CPU/GPU piccola
Qwen/Qwen2.5-1.5B-Instruct ← 1.5B param, qualitΓ  migliore
google/gemma-2-2b-it ← 2B param, ottimo in italiano
Uso:
# Training completo
python finetune.py --dataset dataset.jsonl --hf-repo TUO_USERNAME/generai-model
# Solo dry-run (verifica che funzioni)
python finetune.py --dataset dataset.jsonl --dry-run
"""
import argparse
import json
import os
import sys
from pathlib import Path
# ── Dipendenze opzionali β€” errore chiaro se mancano ───────────────────────────
def _check_deps():
missing = []
for pkg in ["transformers", "trl", "peft", "datasets", "torch"]:
try:
__import__(pkg)
except ImportError:
missing.append(pkg)
if missing:
print("❌ Dipendenze mancanti per il training:")
print(f" pip install {' '.join(missing)}")
print(" (oppure: pip install -r requirements-train.txt)")
sys.exit(1)
_check_deps()
import torch
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer, SFTConfig
DEFAULT_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"
OUTPUT_DIR = "./generai-finetuned"
def load_dataset_jsonl(path: str) -> Dataset:
rows = []
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
rows.append(json.loads(line))
if not rows:
print(f"❌ Dataset vuoto: {path}")
sys.exit(1)
print(f"[dataset] {len(rows)} esempi caricati da {path}")
return Dataset.from_list(rows)
def apply_chat_template(examples, tokenizer):
"""Applica il chat template del modello ai messaggi."""
texts = []
for msgs in examples["messages"]:
text = tokenizer.apply_chat_template(
msgs,
tokenize=False,
add_generation_prompt=False,
)
texts.append(text)
return {"text": texts}
def build_model_and_tokenizer(model_name: str, use_4bit: bool):
print(f"[model] Caricamento: {model_name}")
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
bnb_config = None
if use_4bit:
try:
import bitsandbytes # noqa
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
print("[model] Quantizzazione 4-bit attivata (QLoRA)")
except ImportError:
print("[model] bitsandbytes non disponibile β€” uso fp16 standard")
use_4bit = False
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
torch_dtype=torch.bfloat16 if not use_4bit else None,
device_map="auto",
trust_remote_code=True,
)
return model, tokenizer
def build_lora_config() -> LoraConfig:
return LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # rank β€” piΓΉ alto = piΓΉ parametri addestrati
lora_alpha=32,
lora_dropout=0.05,
bias="none",
target_modules="all-linear",
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dataset", required=True, help="Path al file dataset.jsonl")
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Modello base HuggingFace (default: {DEFAULT_MODEL})")
parser.add_argument("--hf-repo", default="", help="Username/nome-repo su HuggingFace per upload automatico")
parser.add_argument("--epochs", type=int, default=3, help="Numero di epoche (default: 3)")
parser.add_argument("--batch", type=int, default=2, help="Batch size (default: 2)")
parser.add_argument("--lr", type=float, default=2e-4, help="Learning rate (default: 2e-4)")
parser.add_argument("--max-len", type=int, default=512, help="Lunghezza massima token (default: 512)")
parser.add_argument("--4bit", dest="use_4bit", action="store_true", help="Usa QLoRA 4-bit (serve bitsandbytes)")
parser.add_argument("--dry-run", action="store_true", help="Verifica configurazione senza addestrare")
args = parser.parse_args()
if not Path(args.dataset).exists():
print(f"❌ File non trovato: {args.dataset}")
print(" Esegui prima: python export_dataset.py")
sys.exit(1)
# ── Carica dataset ─────────────────────────────────────────────────────────
raw_ds = load_dataset_jsonl(args.dataset)
# ── Carica modello ─────────────────────────────────────────────────────────
model, tokenizer = build_model_and_tokenizer(args.model, args.use_4bit)
# ── Applica chat template ──────────────────────────────────────────────────
ds = raw_ds.map(
lambda x: apply_chat_template(x, tokenizer),
batched=True,
remove_columns=raw_ds.column_names,
)
print(f"[dataset] Esempio tokenizzato:\n{ds[0]['text'][:300]}...\n")
if args.dry_run:
print("βœ… Dry-run completato. Configurazione corretta.")
print(f" Modello : {args.model}")
print(f" Dataset : {len(ds)} esempi")
print(f" Output : {OUTPUT_DIR}")
return
# ── LoRA ───────────────────────────────────────────────────────────────────
lora_cfg = build_lora_config()
model = get_peft_model(model, lora_cfg)
model.print_trainable_parameters()
# ── Training config ────────────────────────────────────────────────────────
sft_cfg = SFTConfig(
output_dir=OUTPUT_DIR,
num_train_epochs=args.epochs,
per_device_train_batch_size=args.batch,
gradient_accumulation_steps=4,
learning_rate=args.lr,
lr_scheduler_type="cosine",
warmup_ratio=0.1,
logging_steps=10,
save_steps=50,
save_total_limit=2,
bf16=torch.cuda.is_available(),
fp16=False,
max_seq_length=args.max_len,
dataset_text_field="text",
report_to="none",
push_to_hub=bool(args.hf_repo),
hub_model_id=args.hf_repo or None,
)
trainer = SFTTrainer(
model=model,
train_dataset=ds,
args=sft_cfg,
tokenizer=tokenizer,
)
print("\nπŸš€ Avvio training...\n")
trainer.train()
# ── Salva ──────────────────────────────────────────────────────────────────
trainer.save_model(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
print(f"\nβœ… Modello salvato in: {OUTPUT_DIR}")
if args.hf_repo:
print(f"⬆️ Upload su HuggingFace: https://huggingface.co/{args.hf_repo}")
trainer.push_to_hub()
else:
print("\nPer caricare su HuggingFace:")
print(f" huggingface-cli upload TUO_USERNAME/generai-model {OUTPUT_DIR}")
print("\nPer usarlo in GenerAI:")
print(f" Imposta HF_MODEL={args.hf_repo or 'TUO_USERNAME/generai-model'} in .env")
if __name__ == "__main__":
main()