Inicio / LLMOps / LLMOps: De Prototipo a Producción / Fine-Tuning de LLMs

Fine-Tuning de LLMs

Cuándo fine-tunear, preparación de datos, LoRA, QLoRA y evaluación.

Intermedio Funciones
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

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.

🔒

Ejercicio práctico disponible

Preparación de datos para fine-tuning

Desbloquear ejercicios
// Preparación de datos para fine-tuning
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes