Fine-Tuning de LLMs
¿Cuándo Hacer Fine-Tuning?
Fine-tuning es entrenar un modelo base con tus propios datos para especializarlo. No siempre es necesario — evalúa primero prompting y RAG.
┌─────────────────────────────────────────────────┐
│ ÁRBOL DE DECISIÓN │
│ │
│ ¿El modelo base + prompt funciona? │
│ ├─ SÍ → No necesitas fine-tuning │
│ └─ NO → ¿RAG resuelve el problema? │
│ ├─ SÍ → Usa RAG │
│ └─ NO → ¿Necesitas estilo/formato/tono? │
│ ├─ SÍ → Fine-tuning │
│ └─ NO → ¿Necesitas conocimiento? │
│ ├─ SÍ → RAG + fine-tuning │
│ └─ NO → Revisa tu enfoque │
└─────────────────────────────────────────────────┘
Fine-tuning SÍ:
- Estilo de escritura específico
- Formato de salida consistente
- Terminología de dominio
- Reducir latencia (modelo más pequeño + fine-tuned)
- Reducir costos (modelo pequeño que hace lo mismo que uno grande)
Fine-tuning NO:
- Datos que cambian frecuentemente (usa RAG)
- Solo necesitas contexto adicional (usa RAG)
- El prompt engineering ya funciona bien
Preparación de Datos
Formato JSONL para Chat
{"messages": [{"role": "system", "content": "Eres un asistente médico."}, {"role": "user", "content": "¿Qué es la hipertensión?"}, {"role": "assistant", "content": "La hipertensión arterial es una condición donde la presión sanguínea en las arterias es persistentemente elevada, superando los 140/90 mmHg."}]}
{"messages": [{"role": "system", "content": "Eres un asistente médico."}, {"role": "user", "content": "Síntomas de diabetes tipo 2"}, {"role": "assistant", "content": "Los síntomas principales incluyen: sed excesiva, micción frecuente, fatiga, visión borrosa y pérdida de peso inexplicable."}]}
Validación de Datos
import json
def validate_training_data(filepath: str) -> dict:
stats = {"total": 0, "errors": [], "token_counts": []}
with open(filepath) as f:
for i, line in enumerate(f):
stats["total"] += 1
try:
data = json.loads(line)
messages = data["messages"]
# Verificar estructura
roles = [m["role"] for m in messages]
if roles[0] not in ["system", "user"]:
stats["errors"].append(f"L{i+1}: Primer mensaje debe ser system o user")
if "assistant" not in roles:
stats["errors"].append(f"L{i+1}: Falta respuesta del assistant")
# Contar tokens aproximados
total_tokens = sum(len(m["content"].split()) * 1.3 for m in messages)
stats["token_counts"].append(int(total_tokens))
except (json.JSONDecodeError, KeyError) as e:
stats["errors"].append(f"L{i+1}: {e}")
stats["avg_tokens"] = sum(stats["token_counts"]) / len(stats["token_counts"])
stats["total_tokens"] = sum(stats["token_counts"])
return stats
Fine-Tuning con OpenAI
from openai import OpenAI
client = OpenAI()
# 1. Subir archivo de training
file = client.files.create(
file=open("training_data.jsonl", "rb"),
purpose="fine-tune"
)
# 2. Crear job de fine-tuning
job = client.fine_tuning.jobs.create(
training_file=file.id,
model="gpt-4o-mini-2024-07-18",
hyperparameters={
"n_epochs": 3,
"batch_size": "auto",
"learning_rate_multiplier": "auto",
},
suffix="mi-modelo-custom",
)
# 3. Monitorear progreso
while True:
status = client.fine_tuning.jobs.retrieve(job.id)
print(f"Status: {status.status}")
if status.status in ["succeeded", "failed"]:
break
time.sleep(60)
# 4. Usar modelo fine-tuned
response = client.chat.completions.create(
model=status.fine_tuned_model, # "ft:gpt-4o-mini-2024-07-18:org:mi-modelo-custom:abc123"
messages=[{"role": "user", "content": "..."}],
)
LoRA y QLoRA (Open-Source)
LoRA (Low-Rank Adaptation) permite fine-tuning eficiente entrenando solo matrices de bajo rango añadidas al modelo original.
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import BitsAndBytesConfig
import torch
# QLoRA: Cargar modelo en 4-bit para ahorrar VRAM
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Meta-Llama-3-8B",
quantization_config=bnb_config,
device_map="auto",
)
# Configurar LoRA
lora_config = LoraConfig(
r=16, # Rango de las matrices
lora_alpha=32, # Factor de escala
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, lora_config)
# Ver parámetros entrenables
model.print_trainable_parameters()
# "trainable params: 4,194,304 || all params: 8,030,261,248 || trainable%: 0.0522"
Comparación de Recursos
| Método | VRAM necesaria (Llama 3 8B) | Parámetros entrenados |
|---|---|---|
| Full fine-tuning | ~60 GB | 100% (8B) |
| LoRA | ~24 GB | ~0.05% (4M) |
| QLoRA (4-bit) | ~6 GB | ~0.05% (4M) |
Evaluación Post Fine-Tuning
def evaluate_fine_tuned(base_model, fine_tuned_model, test_data):
results = {"base": [], "fine_tuned": []}
for test in test_data:
# Comparar ambos modelos
base_resp = call_model(base_model, test["input"])
ft_resp = call_model(fine_tuned_model, test["input"])
results["base"].append({
"input": test["input"],
"expected": test["expected"],
"actual": base_resp,
"match": evaluate_match(base_resp, test["expected"]),
})
results["fine_tuned"].append({
"input": test["input"],
"expected": test["expected"],
"actual": ft_resp,
"match": evaluate_match(ft_resp, test["expected"]),
})
base_score = sum(r["match"] for r in results["base"]) / len(results["base"])
ft_score = sum(r["match"] for r in results["fine_tuned"]) / len(results["fine_tuned"])
print(f"Base model accuracy: {base_score:.2%}")
print(f"Fine-tuned accuracy: {ft_score:.2%}")
print(f"Improvement: {ft_score - base_score:+.2%}")
return results
Resumen
Fine-tuning es poderoso pero no siempre necesario. Cuando lo es, LoRA/QLoRA permiten hacerlo eficientemente con hardware modesto. La clave está en datos de calidad, evaluación rigurosa y comparar siempre contra el baseline.