Saltar al contenido

Ajuste fino de modelos de lenguaje para tareas específicas de dominio

Guía práctica del ajuste fino de modelos de lenguaje con datos de dominio: datasets, estrategias de entrenamiento, evaluación y despliegue.

6 min de lectura
Diagrama que muestra un modelo de lenguaje preentrenado siendo ajustado con datos específicos de dominio para producir salidas especializadas para una tarea objetivo

Los modelos de lenguaje preentrenados capturan una comprensión general del lenguaje a partir de corpus masivos, pero carecen de la especificidad que tu dominio requiere. Un modelo entrenado con texto de internet no entiende la terminología interna de tu empresa, el lenguaje regulatorio de tu industria ni la nomenclatura particular de tu producto. El ajuste fino cierra esta brecha adaptando un modelo general para que rinda bien en tu tarea específica con tus datos específicos.

La cuestión no es si ajustar—sino cómo hacerlo eficientemente sin sobreajuste, olvido catastrófico o quemar el presupuesto de cómputo.

Preparación y calidad del dataset

La calidad de tu dataset de ajuste fino determina el techo de rendimiento del modelo. Aquí el principio de "basura entra, basura sale" aplica con fuerza excepcional.

pypython
# ❌ Throwing raw data at the model without cleaning
training_data = [
    {"text": doc["content"], "label": doc["category"]}
    for doc in raw_documents
]
# No deduplication, no quality filtering, no validation
pypython
# ✅ Structured dataset preparation pipeline
import hashlib
from dataclasses import dataclass
from typing import Optional
 
 
@dataclass
class TrainingExample:
    input_text: str
    output_text: str
    source: str
    quality_score: float
 
 
class DatasetPipeline:
    def __init__(self, min_quality: float = 0.7,
                 min_length: int = 50, max_length: int = 2048):
        self.min_quality = min_quality
        self.min_length = min_length
        self.max_length = max_length
        self._seen_hashes: set[str] = set()
 
    def process(
        self, raw_examples: list[dict]
    ) -> list[TrainingExample]:
        processed = []
 
        for raw in raw_examples:
            example = self._clean(raw)
            if example is None:
                continue
 
            if not self._passes_quality(example):
                continue
 
            if self._is_duplicate(example):
                continue
 
            processed.append(example)
 
        return processed
 
    def _clean(self, raw: dict) -> Optional[TrainingExample]:
        text = raw.get("input", "").strip()
        output = raw.get("output", "").strip()
 
        if not text or not output:
            return None
 
        # Normalize whitespace
        text = " ".join(text.split())
        output = " ".join(output.split())
 
        return TrainingExample(
            input_text=text,
            output_text=output,
            source=raw.get("source", "unknown"),
            quality_score=raw.get("quality", 0.5),
        )
 
    def _passes_quality(self, example: TrainingExample) -> bool:
        if example.quality_score < self.min_quality:
            return False
        if len(example.input_text) < self.min_length:
            return False
        if len(example.input_text) > self.max_length:
            return False
        return True
 
    def _is_duplicate(self, example: TrainingExample) -> bool:
        content_hash = hashlib.sha256(
            example.input_text.encode()
        ).hexdigest()
 
        if content_hash in self._seen_hashes:
            return True
 
        self._seen_hashes.add(content_hash)
        return False

La deduplicación importa más de lo que esperas. Los datasets reales suelen contener casi-duplicados que sesgan al modelo hacia memorizar patrones específicos en lugar de aprender comportamientos generalizables.

Configuración de entrenamiento e hiperparámetros

El ajuste fino requiere hiperparámetros distintos a los del preentrenamiento. La tasa de aprendizaje debe ser lo suficientemente baja para preservar el conocimiento preentrenado y lo suficientemente alta para aprender patrones nuevos.

pypython
from transformers import (
    AutoModelForSequenceClassification,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
)
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR
 
 
def create_training_config(
    model_name: str,
    output_dir: str,
    num_examples: int,
    batch_size: int = 8,
) -> TrainingArguments:
    # Calculate steps based on dataset size
    steps_per_epoch = num_examples // batch_size
    total_epochs = 3  # Fine-tuning rarely needs more
    warmup_steps = int(steps_per_epoch * 0.1)
 
    return TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=total_epochs,
        per_device_train_batch_size=batch_size,
        per_device_eval_batch_size=batch_size * 2,
        learning_rate=2e-5,  # Much lower than pre-training
        weight_decay=0.01,
        warmup_steps=warmup_steps,
        lr_scheduler_type="cosine",
        evaluation_strategy="steps",
        eval_steps=steps_per_epoch // 2,
        save_strategy="steps",
        save_steps=steps_per_epoch,
        load_best_model_at_end=True,
        metric_for_best_model="eval_loss",
        logging_steps=50,
        fp16=True,
        gradient_accumulation_steps=4,
        dataloader_num_workers=4,
    )
 
 
def fine_tune(
    model_name: str,
    train_dataset,
    eval_dataset,
    output_dir: str,
):
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForSequenceClassification.from_pretrained(
        model_name, num_labels=train_dataset.num_labels
    )
 
    config = create_training_config(
        model_name=model_name,
        output_dir=output_dir,
        num_examples=len(train_dataset),
    )
 
    trainer = Trainer(
        model=model,
        args=config,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        tokenizer=tokenizer,
    )
 
    trainer.train()
    return trainer

Una tasa de aprendizaje de 2e-5 es un punto de partida bien establecido para el ajuste fino de transformadores. Subir mucho más corre el riesgo de olvido catastrófico: el modelo pierde sus capacidades preentrenadas. Bajar mucho más desperdicia cómputo sin una adaptación suficiente.

Ajuste fino eficiente en parámetros con LoRA

El ajuste fino completo actualiza todos los parámetros del modelo, lo cual es costoso y a menudo innecesario. LoRA (Low-Rank Adaptation) congela los pesos originales e inyecta pequeñas matrices entrenables, reduciendo drásticamente los requisitos de memoria.

pypython
from peft import LoraConfig, get_peft_model, TaskType
 
 
def create_lora_model(base_model_name: str, task_type: str):
    """Create a LoRA-adapted model for efficient fine-tuning."""
    from transformers import AutoModelForCausalLM
 
    base_model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        load_in_8bit=True,  # Quantize base model
        device_map="auto",
    )
 
    lora_config = LoraConfig(
        r=16,  # Rank of the low-rank matrices
        lora_alpha=32,  # Scaling factor
        target_modules=[
            "q_proj", "k_proj", "v_proj", "o_proj",
        ],
        lora_dropout=0.05,
        bias="none",
        task_type=TaskType.CAUSAL_LM,
    )
 
    model = get_peft_model(base_model, lora_config)
 
    # Show parameter efficiency
    trainable = sum(
        p.numel() for p in model.parameters() if p.requires_grad
    )
    total = sum(p.numel() for p in model.parameters())
    print(
        f"Trainable: {trainable:,} / {total:,} "
        f"({100 * trainable / total:.2f}%)"
    )
    # Typically: 0.1-1% of total parameters
 
    return model

LoRA suele entrenar menos del 1% del total de parámetros mientras logra un rendimiento comparable al del ajuste fino completo. Esto hace práctico ajustar modelos grandes en GPUs de consumo.

Evaluación más allá de las curvas de pérdida

Que la pérdida de entrenamiento baje no significa que tu modelo sea útil. Necesitas métricas de evaluación específicas de la tarea que reflejen el rendimiento en el mundo real.

pypython
from sklearn.metrics import (
    classification_report,
    confusion_matrix,
)
import numpy as np
 
 
class DomainEvaluator:
    def __init__(self, label_names: list[str]):
        self.label_names = label_names
        self.predictions: list[int] = []
        self.references: list[int] = []
 
    def add_batch(
        self, predictions: np.ndarray, references: np.ndarray
    ):
        self.predictions.extend(predictions.tolist())
        self.references.extend(references.tolist())
 
    def compute_metrics(self) -> dict:
        report = classification_report(
            self.references,
            self.predictions,
            target_names=self.label_names,
            output_dict=True,
        )
 
        cm = confusion_matrix(
            self.references, self.predictions
        )
 
        # Find worst-performing classes
        per_class = {
            name: report[name]["f1-score"]
            for name in self.label_names
        }
        worst_classes = sorted(
            per_class.items(), key=lambda x: x[1]
        )[:3]
 
        return {
            "macro_f1": report["macro avg"]["f1-score"],
            "weighted_f1": report["weighted avg"]["f1-score"],
            "per_class_f1": per_class,
            "worst_classes": worst_classes,
            "confusion_matrix": cm.tolist(),
        }
 
    def detect_regression(
        self,
        baseline_metrics: dict,
        threshold: float = 0.02,
    ) -> list[str]:
        """Flag classes where performance dropped."""
        current = self.compute_metrics()
        regressions = []
 
        for label in self.label_names:
            baseline_f1 = baseline_metrics["per_class_f1"].get(
                label, 0
            )
            current_f1 = current["per_class_f1"].get(label, 0)
 
            if baseline_f1 - current_f1 > threshold:
                regressions.append(
                    f"{label}: {baseline_f1:.3f} → {current_f1:.3f}"
                )
 
        return regressions

Despliegue y versionado de modelos

Los modelos ajustados necesitan la misma rigurosidad que el software en producción: versionado, capacidad de rollback e infraestructura de pruebas A/B.

tstypescript
interface ModelVersion {
  id: string;
  baseModel: string;
  trainingDate: string;
  datasetVersion: string;
  metrics: {
    macroF1: number;
    weightedF1: number;
    evaluationSet: string;
  };
  status: "training" | "evaluating" | "canary" | "production" | "retired";
}
 
class ModelRegistry {
  private versions: Map<string, ModelVersion> = new Map();
 
  async promote(
    modelId: string,
    targetStatus: ModelVersion["status"]
  ): Promise<void> {
    const model = this.versions.get(modelId);
    if (!model) throw new Error(`Model ${modelId} not found`);
 
    // Enforce promotion order
    const validTransitions: Record<string, string[]> = {
      training: ["evaluating"],
      evaluating: ["canary", "retired"],
      canary: ["production", "retired"],
      production: ["retired"],
    };
 
    const allowed = validTransitions[model.status] ?? [];
    if (!allowed.includes(targetStatus)) {
      throw new Error(
        `Cannot transition from ${model.status} to ${targetStatus}`
      );
    }
 
    // If promoting to production, retire current production model
    if (targetStatus === "production") {
      for (const [id, ver] of this.versions) {
        if (ver.status === "production" && id !== modelId) {
          ver.status = "retired";
        }
      }
    }
 
    model.status = targetStatus;
  }
}

Puntos clave

El ajuste fino transforma un modelo de propósito general en un especialista de dominio, pero el proceso exige disciplina. Limpia tus datos agresivamente: la deduplicación, el filtrado de calidad y las restricciones de longitud evitan que el modelo memorice ruido. Empieza con hiperparámetros conservadores: una tasa de aprendizaje de 2e-5, 3 épocas y programación coseno proporcionan una línea base confiable. Usa LoRA u otros métodos eficientes en parámetros cuando el ajuste fino completo supere tu presupuesto de cómputo: entrenar menos del 1% de los parámetros suele dar resultados comparables. Evalúa con métricas específicas del dominio, no solo curvas de pérdida, y rastrea el rendimiento por clase para detectar regresiones en categorías poco representadas. Trata las versiones del modelo como lanzamientos de software: versionalas, controla las promociones mediante etapas de evaluación y mantén capacidad de rollback. El objetivo no es el modelo más poderoso, sino el más confiable para tu tarea específica.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX