Fine-tuning de modelos

Cuando necesitas un modelo dedicado a generar descripciones de productos de comercio electrónico, tienes tres opciones principales.

  • La primera es entrenar desde cero (Pre-training from scratch). Esto requiere recolectar enormes cantidades de datos de texto (posiblemente billones de tokens), alquilar decenas de GPUs A100 para entrenar durante meses, con un costo de millones de dólares. Esto es lo que hacen gigantes como OpenAI y Google, no es realista para la gran mayoría.

  • La segunda es la ingeniería de prompts. Se le da al gran modelo general un prompt cuidadosamente diseñado, por ejemplo: “Eres un redactor profesional de textos de comercio electrónico, genera una descripción atractiva según la siguiente información del producto...”. Este método tiene costo cero y está disponible de inmediato, pero el problema es: cada vez hay que escribir prompts largos, el resultado es inestable, es fácil que se vea interferido por entradas irrelevantes, y consume muchos tokens.

  • La tercera es el fine-tuning. Se toma un modelo general ya entrenado, se continúa entrenando un poco con tus datos propios para que “aprenda” tu tarea específica. El modelo resultante conserva las capacidades generales y a la vez se desempeña excelentemente en tu tarea, con un costo controlable.

Comparación de las tres opciones:

MétodoCostoEfectoEscenario de aplicación
Entrenamiento desde ceroExtremadamente alto (a nivel de millones de dólares)Personalización totalCrear nuevos modelos base (solo los gigantes)
Ingeniería de promptsCasi ceroInestable, depende del promptTareas simples, validación rápida
Fine-tuningMedio (de unos cientos a unos miles de dólares)Estable y profesionalTareas de dominio específico, productización

Usando una analogía sencilla:

  • Entrenar desde cero es como formar a un médico desde cero, se necesitan más de diez años.
  • La ingeniería de prompts es como darle a un médico general un manual de operación detallado.
  • El fine-tuning es como hacer que un médico ya cualificado haga una especialización durante unos meses y se convierta en un especialista.

Por qué se necesita el fine-tuning

No todas las tareas necesitan fine-tuning; primero hay que tener claro cuándo usarlo y cuándo no.

Limitaciones de la ingeniería de prompts

La ingeniería de prompts es muy potente, pero tiene varios techos difíciles de superar.

  • La primera limitación es la restricción de la ventana de contexto.Tus prompts y ejemplos deben caber en la ventana de contexto del modelo (por ejemplo, 4K, 8K, 128K, 1024K tokens). Si tu tarea necesita cientos de ejemplos para quedar clara, no caben en el prompt.

  • La segunda limitación es que los resultados son inestables.Con el mismo prompt, si cambias la forma de preguntar, el resultado puede variar mucho. Cuando el modelo está “de buen humor” (aleatoriedad del muestreo) genera buenos resultados; cuando no, puede desviarse por completo del tema.

  • La tercera limitación es el alto costo de inferencia.Cada inferencia requiere enviar el prompt largo, lo que consume muchos tokens y hace lenta la respuesta. En escenarios de alta concurrencia, el costo aumenta drásticamente.

  • La cuarta limitación es el problema del olvido.Aunque el modelo ha visto tu prompt, la enorme cantidad de datos de preentrenamiento previos aún puede arrastrarlo. Por ejemplo, si le pides que genere JSON, es posible que aún añada mucha explicación en lenguaje natural.

Escenarios de aplicación del fine-tuning

Cuando tu tarea cumple con las siguientes características, el fine-tuning merece la pena considerarse.

  • Característica 1: la tarea está claramente definida y el formato de salida es fijo. Por ejemplo: “traducir documentos técnicos a blogs fáciles de entender”, “generar resúmenes de tickets según registros de servicio al cliente”, “convertir lenguaje natural en consultas SQL”.

  • Característica 2: se dispone de unos cientos a unos miles de datos anotados de alta calidad. Con muy pocos datos, el fine-tuning no logra resultados; con demasiados no es necesario; generalmente 500-5000 muestras es un rango ideal.

  • Característica 3: se necesita un desempeño profesional en un dominio específico. Por ejemplo, interpretación de informes médicos, resumen de documentos legales, análisis de noticias financieras. Los modelos generales pueden no entender la terminología y las prácticas del sector; el fine-tuning puede hacer que “entre en el sector”.

  • Característica 4: sensibilidad a la velocidad de respuesta y al costo. Un modelo pequeño afinado puede funcionar mejor que un gran modelo general, con una inferencia 10 veces más rápida y un costo 100 veces menor.

  • Característica 5: se requiere seguir estrictamente el formato de salida. Por ejemplo, se debe generar JSON, usar un tono específico, o incluir ciertos campos. El fine-tuning permite que el modelo “recuerde” estos requisitos, sin necesidad de repetirlos en el prompt cada vez.

Análisis de coste-beneficio del fine-tuning

Antes de hacer fine-tuning, haz primero los cálculos.

El costo proviene principalmente de tres aspectos: preparación de datos, recursos computacionales y depuración manual. La preparación de datos suele ser la mayor parte del trabajo: necesitas recolectar, limpiar y anotar datos. Los recursos computacionales ya son bastante baratos; con LoRA + QLoRA, una GPU de consumo (RTX 3090/4090) puede afinar modelos de 7B/13B.

Los beneficios se reflejan en varios aspectos: mejores resultados, menor costo de inferencia, mayor velocidad de respuesta, salidas más estables. Si tu modelo va a ofrecer servicios al público, estos beneficios se acumulan continuamente.

¿Cuándo no se debe hacer fine-tuning? Si la tarea cambia rápidamente y el formato de datos cambia cada semana, entonces la ingeniería de prompts es más flexible. Si solo es una exploración puntual, o si solo hay unas pocas docenas de datos, el fine-tuning tampoco merece la pena.

FactorMejor para promptsMejor para fine-tuning
Cantidad de datos< 100 muestras> 500 muestras
Estabilidad de la tareaCambia con frecuenciaRelativamente fija
Frecuencia de usoUso ocasionalUso de alta frecuencia
Sensibilidad al costobajoAlto
Requisitos de resultadoCon que funcione es suficienteBuscar lo mejor

Fine-tuning completo (Full Fine-tuning)

Primero entendamos el método de fine-tuning más tradicional, luego veremos por qué hoy en día rara vez lo usamos.

Principio y proceso

La idea del fine-tuning completo es simple: tomar todos los parámetros del modelo preentrenado, continuar entrenando en tu conjunto de datos y actualizar todos los pesos.

El proceso es aproximadamente el siguiente:

  • Paso 1: preparar los datos. Organiza tus datos de la tarea en forma de pares “entrada-salida”.

  • Paso 2: cargar el modelo preentrenado. Por ejemplo, descargar modelos de código abierto como LLaMA-2-7B, Mistral-7B.

  • Paso 3: configurar los parámetros de entrenamiento. La tasa de aprendizaje debe ser muy pequeña (por ejemplo, 1e-5 a 5e-5), porque no queremos sobrescribir por completo el conocimiento aprendido en el preentrenamiento.

  • Paso 4: comenzar el entrenamiento. Deja que el modelo recorra tus datos durante algunas épocas (recorridos completos del conjunto de datos varias veces).

  • Paso 5: guardar el modelo. Al finalizar el entrenamiento se obtiene un archivo de pesos completamente nuevo, con el mismo tamaño que el modelo original.

Ejemplo

# ============================================
# Demostración del concepto de fine-tuning completo (pseudocódigo, independiente del marco)
# Muestra la idea central, el código real usará HuggingFace
# ============================================

def full_finetuning_concept():
    """Demostración del concepto central del fine-tuning completo"""
    # 1. Cargar el modelo preentrenado (todos los parámetros son entrenables)
    model = load_pretrained_model("llama-2-7b")
    # El modelo tiene 7 mil millones de parámetros, y cada parámetro se actualiza
    print(f"Número total de parámetros del modelo: {count_parameters(model)}")
    # Salida: Número total de parámetros del modelo: 7000000000

    # 2. Prepara los datos de tu tarea específica
    train_data = [
        {"input": "Producto: termo\nGenerar descripción de e-commerce",
         "output": "Este termo de alta calidad utiliza revestimiento interior de acero inoxidable 304, tecnología de aislamiento al vacío, y puede mantener el calor hasta 12 horas."},
        {"input": "Producto: auriculares inalámbricos\nGenerar descripción de e-commerce",
         "output": "Auriculares inalámbricos Bluetooth verdaderos, conexión Bluetooth 5.3 estable, cancelación activa de ruido, 24 horas de batería, cómodos de llevar."},
        # ... cientos o miles de datos similares
    ]

    # 3. Configurar el optimizador y una tasa de aprendizaje muy pequeña
    optimizer = create_optimizer(model, learning_rate=2e-5)

    # 4. Bucle de entrenamiento
    for epoch in range(3):  # Normalmente se entrenan 2-5 épocas (epochs)
        for batch in create_batches(train_data):
            # Propagación hacia adelante
            loss = model.compute_loss(batch)
            # Retropropagación, ¡actualizar todos los parámetros!
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
        print(f"Época {epoch} completada")

    # 5. Guardar el modelo completo (7 mil millones de parámetros, aproximadamente 13 GB)
    model.save("llama-2-7b-ecommerce-finetuned")
    print("Ajuste fino completo, el nuevo modelo se ha guardado")

# Esto es solo una demostración conceptual; el entrenamiento real requiere mucha memoria de video (VRAM)

Problema del olvido catastrófico

El ajuste fino completo tiene un problema grave: el olvido catastrófico (Catastrophic Forgetting).

¿Qué significa? Cuando entrenas el modelo con tus datos, el modelo gradualmente "olvida" el conocimiento general que aprendió durante el preentrenamiento. Es como una persona que se esfuerza por prepararse para un examen de matemáticas, haciendo muchos ejercicios de matemáticas, y al final olvida la lengua y el inglés que había aprendido antes.

¿Por qué sucede? Porque el ajuste fino completo actualiza todos los parámetros. El conocimiento aprendido en el preentrenamiento está codificado en estos parámetros, y modificarlos en gran medida puede destruir el conocimiento original.

El resultado es: el modelo ajustado se desempeña muy bien en tu tarea, pero su rendimiento en otras tareas generales disminuye. Peor aún, puede perder algunas capacidades básicas, como seguir instrucciones y comprender problemas complejos.

¿Cómo mitigarlo? Se puede entrenar mezclando datos de preentrenamiento, pero eso vuelve al viejo problema de "necesitar datos masivos". O se pueden usar restricciones de regularización para que los parámetros no cambien demasiado, pero el efecto es limitado.

Cálculo de requisitos de VRAM

Otro problema del ajuste fino completo es que la demanda de memoria de video es demasiado grande. Calculemos:

Memoria de video necesaria para entrenar un modelo = parámetros del modelo + gradientes + estado del optimizador + valores de activación.

Para el modelo LLaMA-7B, en precisión completa (FP32), 7 mil millones de parámetros × 4 bytes = 28 GB. Los gradientes también son 28 GB, el estado del optimizador (optimizador Adam) requiere 56 GB (el doble de los gradientes). Los valores de activación varían con el tamaño del lote (batch size) y la longitud de la secuencia, y necesitan al menos unos GB.

En total, el ajuste fino completo de un modelo de 7B requiere aproximadamente más de 100 GB de memoria de video, lo que significa al menos 8 A100 (40 GB) o 2 A100 (80 GB).

Si se usa media precisión (FP16/BF16), se puede reducir a la mitad, pero aún se necesitan unos 50-60 GB de memoria de video. Esto sigue estando fuera del alcance de la mayoría de la gente.

Tamaño del modeloMemoria de video para ajuste fino completo (FP16)GPU necesarias
7B50-60 GB2×A100 (40GB) o 1×A100 (80GB)
13B90-100 GB3×A100 (40GB) o 2×A100 (80GB)
70B400+ GB12×A100 (40GB) o 6×A100 (80GB)

El ajuste fino completo no es imposible de usar, pero es costoso y riesgoso. A menos que tengas una razón de peso (por ejemplo, una cantidad de datos muy grande, o la necesidad de cambiar el comportamiento fundamental del modelo), deberías priorizar los métodos de ajuste fino eficiente en parámetros (PEFT).


Fine-tuning de parámetros eficiente (PEFT)

PEFT (Parameter-Efficient Fine-Tuning) es un término general para una clase de métodos que solo entrenan una pequeña cantidad de parámetros. Preserva la efectividad del ajuste fino y reduce drásticamente los costos.

Idea central de PEFT

La visión central de PEFT es:El modelo preentrenado ya contiene suficiente conocimiento; no necesitamos modificar todos los parámetros, solo necesitamos "ajustar" una pequeña parte de los parámetros para adaptarnos a la nueva tarea。

Es como un piano ya construido: no necesitas fabricar todo el piano de nuevo, solo afinar unas cuantas cuerdas para obtener el tono que deseas.

Los beneficios de PEFT son evidentes:

  • Primero, la demanda de memoria de video se reduce drásticamente. Antes necesitabas 60 GB, ahora puede que solo necesites 6 GB.

  • Segundo, no hay olvido catastrófico. Los parámetros del modelo original no se modifican; solo se añade algo nuevo, y el conocimiento original no se destruye.

  • Tercero, el costo de almacenamiento es bajo. PEFT normalmente solo guarda los pocos parámetros nuevos (de unos MB a unos cientos de MB), sin necesidad de guardar todo el modelo.

  • Cuarto, se puede hacer "combinación de tareas". Entrenar múltiples adaptadores pequeños (Adapters) y cargarlos según sea necesario durante la inferencia; un modelo puede usarse como varios modelos.

Visión general de los métodos PEFT más populares

PEFT no es un método, sino un término que agrupa una clase de métodos. Veamos varios métodos principales:

  • El primero es Adapter (adaptador). Se insertan pequeños módulos de red neuronal en el interior de cada capa del Transformer. Durante el entrenamiento, solo se entrenan estos Adapters, y los parámetros del modelo original permanecen congelados. Este fue un método PEFT temprano, ahora se usa menos.

  • El segundo es Prefix Tuning (ajuste de prefijos). Se añaden vectores de "prefijo" aprendibles delante de cada capa del modelo. Estos vectores de prefijo ocupan una parte muy pequeña, pero pueden guiar la salida del modelo.

  • El tercero es Prompt Tuning (ajuste de prompts). Solo se añade un prompt suave (Soft Prompt) aprendible en la capa de entrada. Es el método más simple, pero su efecto es relativamente limitado.

  • El cuarto es LoRA (Low-Rank Adaptation, adaptación de bajo rango). Se añaden matrices de bajo rango junto a las capas de atención del modelo. Es el método PEFT más popular y con mejor rendimiento actualmente.

  • El quinto es QLoRA (LoRA cuantizado). Basado en LoRA, se añade cuantización del modelo para reducir aún más la demanda de memoria de video. Esta es la opción principal en la actualidad.

MétodoPrincipioCantidad de parámetros entrenablesEfecto
Ajuste fino completoActualizar todos los parámetros100%Bueno, pero puede haber olvido
AdapterInsertar redes pequeñas0.1%-1%Medio
Prefix TuningAñadir vectores de prefijo0.1%-1%Medio
Prompt TuningAñadir prompt suave< 0.1%Regular
LoRAAdaptación de matrices de bajo rango0.1%-1%Cercano al ajuste fino completo
QLoRACuantización + LoRA0.1%-1%Cercano al ajuste fino completo

La mejor práctica actual es clara:Prioriza LoRA, y si no tienes suficiente memoria de video, usa QLoRA. Estos dos métodos tienen buenos resultados, bajo costo y un ecosistema maduro; en la mayoría de los casos son la opción óptima.


LoRA(Low-Rank Adaptation)

LoRA es actualmente el método de ajuste fino más utilizado; es importante entender su principio.

Principio de descomposición de rango bajo (comprensión intuitiva)

Primero entendamos qué es "rango" (Rank) y qué es "bajo rango".

Supongamos que tienes una matriz de 100 × 100; sus datos pueden no ser completamente aleatorios. Si estos datos pueden combinarse a partir de muy pocos "patrones", decimos que el rango de esta matriz es bajo.

Por ejemplo: si cada fila es un múltiplo de la primera fila, entonces el rango de esta matriz es 1. Si cada fila es una combinación lineal de las dos primeras filas, entonces el rango es 2. Y así sucesivamente.

La visión central de LoRA es:Cuando un modelo grande se adapta a una nueva tarea, el cambio en los pesos suele ser de "bajo rango", es decir, este cambio puede representarse con muy pocos parámetros.。

Es como si, aunque haya 1000 descripciones distintas de productos de e-commerce, su estilo de escritura pudiera estar determinado por unos pocos factores centrales: tono, longitud, puntos de énfasis. No necesitas 1000 direcciones diferentes para describir el cambio; con 8, 16 o 32 bastaría.

Entonces, el enfoque de LoRA es: congelar todos los parámetros del modelo original y añadir dos matrices pequeñas A y B junto a las capas de atención. A es d × r, B es r × d, donde r es el rango (Rank), que normalmente toma valores pequeños como 8, 16, 32.

Salida del modelo original = propagación hacia adelante del modelo original.

Salida de LoRA = propagación hacia adelante del modelo original + (entrada × A × B) × factor de escala.

Durante el entrenamiento, solo se entrenan A y B; los parámetros del modelo original permanecen completamente intactos.

Derivación matemática de LoRA

No temas, la derivación matemática es muy simple, solo unas pocas líneas.

Supongamos que una capa del modelo original es una transformación lineal: h = W₀x, donde W₀ es la matriz de pesos preentrenada, con forma d × d.

Durante el ajuste fino, queremos actualizar W₀, pero no demasiado. LoRA representa el cambio de W₀ como el producto de dos matrices pequeñas: ΔW = BA.

Así que la salida después del ajuste fino es: h = W₀x + (BA)x.

Aquí, B tiene forma d × r, A tiene forma r × d, y r es el rango (Rank).

Para ser más precisos, la fórmula en el artículo de LoRA es:

h = W₀x + α * (BA)x

donde α es un factor de escala que normalmente se establece como un múltiplo de r, de modo que al cambiar r no sea necesario reajustar los hiperparámetros.

Durante el entrenamiento, W₀ está completamente congelado, solo A y B son entrenables.

En la inicialización, A se inicializa con una distribución gaussiana aleatoria y B se inicializa a cero, de modo que al comienzo del entrenamiento la salida de la parte LoRA sea cero y no afecte el comportamiento del modelo original.

Ejemplo

# ============================================
# Implementación mínima del principio central de LoRA
# Demostración con NumPy puro, sin depender de ningún framework
# ============================================

import numpy as np


class LoRALayer:
    """Implementación mínima de la capa LoRA"""

    def __init__(self, d: int, r: int, alpha: float = 1.0):
        """
d: dimensión de entrada/salida (por ejemplo, 4096)
r: rango (por ejemplo, 8, 16, 32)
alpha: factor de escala
        """

        self.d = d
        self.r = r
        self.alpha = alpha

        # Peso original congelado (simula un modelo preentrenado)
        self.W_0 = np.random.randn(d, d) * 0.01
        self.W_0.flags.writeable = False  # Marcado como no entrenable

        # Las matrices A y B de LoRA, entrenables
        # A: r × d, inicializada con gaussiana aleatoria
        self.A = np.random.randn(r, d) * 0.01
        # B: d × r, inicializada a cero (no afecta al modelo original al inicio del entrenamiento)
        self.B = np.zeros((d, r))

    def forward(self, x: np.ndarray) -> np.ndarray:
        """Propagación hacia adelante: salida del modelo original + salida de LoRA"""
        # Salida del modelo original
        original_output = x @ self.W_0.T
        # Salida de LoRA: (x @ A.T) @ B.T = x @ (B @ A).T
        lora_output = (x @ self.A.T) @ self.B.T
        # Salida combinada
        return original_output + self.alpha * lora_output

    def get_trainable_params(self) -> dict:
        """Obtener parámetros entrenables (solo A y B)"""
        return {"A": self.A, "B": self.B}

    def count_params(self) -> dict:
        """Contar el número de parámetros"""
        total_original = self.d * self.d
        total_lora = self.r * self.d + self.d * self.r
        return {
            "original_params": total_original,
            "lora_params": total_lora,
            "ratio": total_lora / total_original * 100,
        }


# ============================================
# Demostrar el ahorro de parámetros de LoRA
# ============================================

# Supongamos d = 4096 (similar a la dimensión oculta de LLaMA-7B)
# r = 8 (tamaño de rango común)
d = 4096
r = 8

lora = LoRALayer(d=d, r=r, alpha=r)
params_info = lora.count_params()

print(f"Número de parámetros originales: {params_info['original_params']:,}")
print(f"Número de parámetros LoRA: {params_info['lora_params']:,}")
print(f"Proporción de parámetros: {params_info['ratio']:.4f}%")

# Ejemplo de salida:
# Número de parámetros originales: 16,777,216
# Número de parámetros LoRA: 65,536
# Proporción de parámetros: 0.3906%

Verás, LoRA solo necesita entrenar el 0.4% de los parámetros, y aun así logra un efecto cercano al ajuste fino completo. Ahí está su magia.

Selección de los parámetros r (rango) y α

LoRA tiene dos hiperparámetros clave: el rango r y el factor de escala α.

r determina la capacidad de LoRA. Cuanto mayor es r, más complejas son las variaciones que puede representar, pero también mayor es el número de parámetros.

Los valores comunes de r: 8, 16, 32, 64. Para la mayoría de las tareas, r = 8 o 16 es suficiente. Superar r = 64 suele tener rendimientos decrecientes y aumentar el costo de entrenamiento.

¿Cómo elegir r? Un consejo práctico: prueba primero r = 8; si no funciona bien, auméntalo a 16, 32. No uses un r grande desde el principio.

α es el factor de escala. Una configuración común es α = r, de modo que la escala de ΔW = BA se mantenga aproximadamente estable. O establece α = 1 y deja que el usuario lo ajuste.

Otros parámetros a considerar:

Capas objetivo (target_modules): ¿En qué capas se debe agregar LoRA? Por lo general, solo agregarlo a las proyecciones query y value de la capa de atención (q_proj, v_proj) ya da buenos resultados. También se puede agregar a más capas, pero esto aumenta el número de parámetros.

Dropout: Se puede agregar dropout a LoRA para prevenir el sobreajuste; los valores comunes son 0.05 o 0.1.

ParámetroValor comúnDescripción
r (rango)8, 16, 32Cuanto mayor es r, mayor es la capacidad, pero también mayor es el número de parámetros.
alphar o 1Factor de escala, normalmente se establece como r
target_modules["q_proj", "v_proj"]Las capas donde se agrega LoRA
lora_dropout0.0, 0.05, 0.1Prevenir el sobreajuste

Fusión de pesos LoRA

Después de entrenar LoRA, hay dos opciones para la inferencia.

  • La primera es mantener LoRA separado. Durante la inferencia, se carga el modelo original y luego los pesos de LoRA, y se calcula dinámicamente la salida combinada. La ventaja de este enfoque es que se pueden cambiar fácilmente entre múltiples LoRA; la desventaja es que la inferencia será un poco más lenta (generalmente insignificante).

  • La segunda es fusionar los pesos de LoRA. Se suma ΔW = BA de LoRA al W₀ del modelo original, obteniendo W = W₀ + α×BA. Así se obtiene un único archivo de modelo, y la inferencia es igual que con un modelo normal.

La ventaja de fusionar es que la inferencia es rápida y no requiere soporte especial para LoRA. La desventaja es que una vez fusionado no se puede deshacer, y si hay múltiples LoRA, hay que fusionarlos por separado en diferentes archivos de modelo.

Ejemplo

# ============================================
# Demostración de fusión de pesos de LoRA
# ============================================

import numpy as np


def merge_lora_weights(W_0: np.ndarray, A: np.ndarray, B: np.ndarray,
                      alpha: float = 1.0) -> np.ndarray:
    """
Fusionar los pesos de LoRA en los pesos originales
    W = W_0 + alpha * (B @ A)

Parámetros:
W_0: pesos originales (d, d)
A: matriz A de LoRA (r, d)
B: matriz B de LoRA (d, r)
alpha: factor de escala

Devuelve:
Pesos fusionados
    """

    # Calcular delta_W = alpha * (B @ A)
    delta_W = alpha * (B @ A)
    # Fusionar con los pesos originales
    W_merged = W_0 + delta_W
    return W_merged


# ============================================
# Demostrar el proceso de fusión
# ============================================

d = 4096  # Dimensión de la capa oculta
r = 8     # Rango de LoRA

# Simular pesos originales y pesos de LoRA
W_0 = np.random.randn(d, d) * 0.01
A = np.random.randn(r, d) * 0.01
B = np.zeros((d, r))  # En la práctica, B es el resultado después del entrenamiento

# Fusionar pesos
W_merged = merge_lora_weights(W_0, A, B, alpha=r)

print(f"Forma de los pesos originales: {W_0.shape}")
print(f"Forma de los pesos fusionados: {W_merged.shape}")
print("Fusión de pesos completada, se puede usar como un modelo normal.")

QLoRA (LoRA con cuantificación)

QLoRA lleva el bajo costo de LoRA al extremo: permite ajustar finamente un modelo de 70B en una GPU de consumo.

Cuantización de 4 bits + LoRA

La idea de QLoRA es simple: cuantizar el modelo a 4 bits y luego realizar un ajuste fino con LoRA sobre él.

¿Qué es la cuantización? Es convertir números de punto flotante (FP16/BF16, que ocupan 2 bytes cada uno) a una representación de menor precisión. La cuantización de 4 bits usa 4 bits (0.5 bytes) para representar un peso.

De esta manera, el tamaño del modelo se puede reducir en un 75%: un modelo de 7B pasa de 13 GB a unos 3.5 GB, uno de 13B de 26 GB a unos 6.5 GB, y uno de 70B de 140 GB a unos 35 GB.

Pero la cuantización directa pierde precisión. La innovación de QLoRA es usar un tipo de datos llamado NF4 (Normalized Float 4), que preserva mejor la información de los pesos de la red neuronal.

El enfoque de QLoRA es:

  • 1. Cuantizar el modelo preentrenado a 4 bits (formato NF4).

  • 2. Agregar adaptadores LoRA sobre él.

  • 3. Durante el entrenamiento, el modelo original cuantizado a 4 bits está completamente congelado; solo se entrenan los pesos FP16 de LoRA.

El efecto de esto: el requisito de memoria de video se reduce otro 75%, mientras que el rendimiento apenas se ve afectado.

La biblioteca bitsandbytes

bitsandbytes es la biblioteca central para implementar la cuantización, desarrollada por Tim Dettmers.

Ofrece:

  • 1. Cuantización de 4 bits (NF4).

  • 2. Doble cuantización (Double Quantization): cuantiza también las constantes de cuantización, ahorrando un poco más de memoria de video.

  • 3. Optimizador paginado (Paged Optimizer): cuando la memoria de video no es suficiente, transfiere parte de los datos a la memoria de la CPU.

Los usuarios normales no necesitan interactuar directamente con bitsandbytes; las bibliotecas peft y transformers ya lo han envuelto. Pero es útil saber que existe.

Esquema de fine-tuning en GPU de consumo

Con QLoRA, la gente común también puede permitirse el ajuste fino.

Una RTX 3090/4090 (24GB de VRAM) puede ajustar finamente modelos de 7B/13B con facilidad; incluso se puede intentar con un modelo de 70B (necesita mayor longitud de secuencia o un tamaño de lote más pequeño).

Estimación de requisitos de memoria de video:

Tamaño del modeloRequisitos de memoria de video de QLoRAGPU adecuada
7B5-8 GBRTX 3060/4060 y superiores
13B8-12 GBRTX 3070/4070 y superiores
34B15-20 GBRTX 3090/4090
70B30-40 GBRTX 3090/4090 × 2 o A100

QLoRA cambia las reglas del juego. Convierte el ajuste fino de modelos grandes de «solo las grandes empresas pueden hacerlo» a «la gente común puede hacerlo en su propia laptop de juegos». Es uno de los avances más importantes de la IA de código abierto.


Preparación de datos de entrenamiento

La preparación de datos es la parte más lenta y crítica del proceso de ajuste fino. Basura entra, basura sale: la calidad de los datos determina el límite superior del modelo.

Formatos de datos (formatos Alpaca, ShareGPT)

Hay varios formatos de datos comunes; elige uno y mantente consistente.

La primera es el formato Alpaca. Es uno de los formatos más populares, popularizado por el proyecto Stanford Alpaca.

El formato es: instruction (instrucción), input (entrada, opcional), output (salida).

Ejemplo

# ============================================
# Ejemplo de formato de datos Alpaca
# ============================================

alpaca_data = [
    {
        "instruction": Genera una descripción de producto de comercio electrónico,
        "input": Nombre del producto: reloj deportivo inteligente\nFunciones: monitorización de frecuencia cardíaca, seguimiento del sueño, GPS, resistencia al agua de 50 metros.,
        "output": Este reloj deportivo inteligente es el compañero ideal para los entusiastas del deporte. Cuenta con una monitorización precisa de la frecuencia cardíaca y un seguimiento científico del sueño, lo que te ayuda a comprender tu estado de salud de manera integral. El GPS integrado te permite registrar tus rutas de ejercicio sin llevar el teléfono. Con una resistencia al agua de 50 metros, puedes usarlo con confianza incluso al nadar.
    },
    {
        "instruction": Reescribe el siguiente texto en una expresión más coloquial.,
        "input": Este producto utiliza tecnología avanzada y se caracteriza por su alto rendimiento y bajo consumo energético.,
        "output": ¡Este producto usa tecnología muy avanzada, es potente y ahorra energía!
    },
    # ... más datos
]

print(El formato Alpaca contiene tres campos:)
print(- instruction: instrucción de la tarea (obligatorio))
print(- input: entrada adicional (opcional, si no hay, puede ser una cadena vacía))
print(- output: salida esperada (obligatorio))

El segundo es el formato ShareGPT. Este formato se utiliza para datos de conversación y soporta diálogos multi-turno.

El formato es: conversations (lista de conversación), cada turno tiene from (rol) y value (contenido).

Ejemplo

# ============================================
# Ejemplo de formato de datos ShareGPT
# ============================================

sharegpt_data = [
    {
        "conversations": [
            {
                "from": "human",
                "value": Hola, me gustaría saber sobre su reloj inteligente.
            },
            {
                "from": "gpt",
                "value": ¡Hola! Nuestro reloj inteligente tiene funciones como monitorización de frecuencia cardíaca, seguimiento del sueño, GPS, y además es resistente al agua hasta 50 metros. ¿Hay algo específico que desee saber?
            },
            {
                "from": "human",
                "value": ¿Cómo es la duración de la batería?
            },
            {
                "from": "gpt",
                "value": Con un uso normal puede durar 7 días, y en modo GPS de ejercicio puede durar 12 horas.
            }
        ]
    },
    # ... más conversación
]

print(El formato ShareGPT se utiliza para datos de conversación multi-turno:)
print(- conversations: lista de conversación)
print(- from: rol (normalmente human y gpt))
print(- value: contenido de la conversación)

También hay un formato simple de pares entrada-salida, adecuado para tareas sencillas.

Ejemplo

# ============================================
# Formato simple de par entrada-salida
# ============================================

simple_data = [
    {
        "text": Usuario: Genera una descripción de producto\nProducto: termo\n\nAsistente: Este termo de alta calidad está hecho con...
    },
    {
        "text": Usuario: Genera una descripción de producto\nProducto: auriculares inalámbricos\n\nAsistente: Auriculares inalámbricos Bluetooth 5.3...
    },
    # ... más datos
]

print(Este formato concatena todo el contenido directamente en un campo text, es el más flexible pero también el menos estandarizado.)

¿Qué formato es mejor? El formato Alpaca es el más versátil, la mayoría de los frameworks de entrenamiento lo soportan. Si es una tarea de conversación, ShareGPT es más adecuado.

Puntos clave de la limpieza de datos

La limpieza de datos es mucho más importante de lo que imaginas. El rendimiento de tu modelo depende en un 80% de la calidad de los datos.

  • Primero, deduplicación. Los datos duplicados hacen que el modelo se sobreajuste a los patrones repetidos. Revisa y elimina los datos completamente duplicados o muy similares.

  • Segundo, filtra datos de baja calidad. Elimina datos demasiado cortos, demasiado largos, con formato caótico o con errores evidentes. Por ejemplo, salidas de solo unas pocas palabras o con caracteres corruptos.

  • Tercero, unifica el formato. Por ejemplo, algunas salidas usan comillas chinas, otras usan comillas inglesas; algunas terminan con un punto, otras no. Intenta unificarlo para que el modelo aprenda más rápido.

  • Cuarto, equilibra la distribución. Si un tipo de datos es especialmente abundante y otro es escaso, el modelo se inclinará hacia la clase mayoritaria. Puedes considerar sobremuestrear la clase minoritaria o submuestrear la mayoritaria.

  • Quinto, verifica la fuga de datos. Asegúrate de que los conjuntos de prueba y validación no aparezcan en el conjunto de entrenamiento.

Ejemplo

# ============================================
# Ejemplo simple de limpieza de datos
# ============================================

import json
from typing import List, Dict


def clean_alpaca_data(data: List[Dict]) -> List[Dict]:
    """Función simple de limpieza de datos"""
    cleaned = []
    seen_outputs = set()

    for item in data:
        # 1. Verifica si los campos necesarios existen
        if "instruction" not in item or "output" not in item:
            continue

        instruction = item["instruction"].strip()
        output = item["output"].strip()

        # 2. Filtra las salidas demasiado cortas
        if len(output) < 10:
            continue

        # 3. Filtra las salidas demasiado largas (ajusta según la tarea)
        if len(output) > 2000:
            continue

        # 4. Deduplicación (deduplicación aproximada usando la salida)
        if output in seen_outputs:
            continue
        seen_outputs.add(output)

        # 5. Unificación simple de formato
        # Por ejemplo, cambiar comillas chinas por comillas inglesas
        output = output.replace(""", "\"").replace(""", "\"")
        # Eliminar espacios en blanco al inicio y al final
        output = output.strip()

        cleaned.append({
            "instruction": instruction,
            "input": item.get("input", "").strip(),
            "output": output
        })

    return cleaned


# Cargar datos
with open("data/training_data_raw.json", "r", encoding="utf-8") as f:
    raw_data = json.load(f)

# Limpiar datos
cleaned_data = clean_alpaca_data(raw_data)

print(fCantidad de datos originales: {len(raw_data)})
print(fCantidad de datos después de la limpieza: {len(cleaned_data)})
print(fSe filtraron {len(raw_data) - len(cleaned_data)} datos de baja calidad)

# Guardar datos limpios
with open("data/training_data_cleaned.json", "w", encoding="utf-8") as f:
    json.dump(cleaned_data, f, ensure_ascii=False, indent=2)

print(Los datos limpios se han guardado en data/training_data_cleaned.json)

Reglas empíricas para la cantidad de datos

¿Cuántos datos se necesitan? Esta es una de las preguntas más frecuentes.

La respuesta es: depende de la dificultad de la tarea.

  • Tareas simples (por ejemplo, cambiar un texto a un lenguaje más coloquial): con 100-300 muestras puede ser suficiente.

  • Tareas medianas (por ejemplo, generar resúmenes con un formato específico): 300-1000 muestras es lo adecuado.

  • Tareas complejas (por ejemplo, preguntas y respuestas en un dominio especializado): lo ideal es 1000-5000 muestras.

Por supuesto, cuantos más datos, generalmente mejor, pero los rendimientos marginales son decrecientes. 1000 muestras son mucho mejores que 500, pero 10000 no necesariamente son mucho mejores que 5000.

Un consejo práctico: prepara primero 500 muestras de alta calidad y haz una ejecución de prueba. Si el resultado no es bueno, analiza si es por falta de datos, mala calidad de los datos o problemas de hiperparámetros, y luego mejora en consecuencia.

Calidad de datos >> cantidad de datos. 1000 muestras cuidadosamente elaboradas producen resultados mucho mejores que 10000 muestras mediocres.

Vale la pena invertir tiempo en la fase de preparación de datos. El entrenamiento del modelo es solo cuestión de 'pulsar un botón', pero la calidad de los datos decide si ese botón tiene valor al pulsarlo.


Práctica de fine-tuning supervisado (SFT)

Por fin llegamos a la práctica. Usaremos Unsloth o LLaMA-Factory para implementar el flujo completo de ajuste fino.

Configuración del entorno

Primer paso, instalar las bibliotecas necesarias.

Configuración del entorno

# ============================================
# Instalar dependencias (se recomienda crear un nuevo entorno con conda)
# ============================================

# Crear un nuevo entorno conda
conda create -n finetuning python=3.10 -y
conda activate finetuning

# Instalar PyTorch (elige el comando adecuado según tu versión de CUDA)
# Visita https://pytorch.org/get-started/locally/ para obtener los comandos más recientes
conda install pytorch pytorch-cuda=12.1 -c pytorch -c nvidia

# Instalar el ecosistema de bibliotecas de HuggingFace
pip install transformers datasets accelerate peft bitsandbytes

# Instalar Unsloth (opcional, mayor velocidad de entrenamiento)
pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
pip install --no-deps trl xformers

# O usar LLaMA-Factory (framework de entrenamiento integral)
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e ".[torch,metrics]"

echo ¡Configuración del entorno completada!

Usando Unsloth/LLaMA-Factory

Unsloth es una biblioteca de entrenamiento optimizada, rápida y con bajo consumo de memoria de video. LLaMA-Factory es un framework de entrenamiento integral que soporta múltiples modelos y métodos.

Veamos primero el código de entrenamiento con Unsloth + peft:

Ejemplo

# ============================================
# Ajuste fino LoRA con Unsloth (versión completa)
# Se puede ejecutar en RTX 3090/4090
# ============================================

import torch
from datasets import load_dataset, Dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
import json


# ============================================
# 1. Configurar parámetros
# ============================================

MODEL_NAME = "unsloth/mistral-7b-v0.2-bnb-4bit"  # u otro modelo
DATA_PATH = "data/training_data_cleaned.json"    # tus datos
OUTPUT_DIR = "output/ejemplo-ecommerce-model"     # directorio de salida

# configuración de cuantización de 4 bits
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16
)

# configuración de LoRA
lora_config = LoraConfig(
    r=16,                    # rango (rank), entre 8-64, 16 es un buen punto de partida
    lora_alpha=16,           # coeficiente de escala, normalmente igual a r
    target_modules=[         # capas a las que se añade LoRA, estas son opciones comunes
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],
    lora_dropout=0.05,       # Dropout para evitar el sobreajuste
    bias="none",
    task_type="CAUSAL_LM"    # modelo de lenguaje causal
)

# parámetros de entrenamiento
training_args = TrainingArguments(
    output_dir=OUTPUT_DIR,
    per_device_train_batch_size=4,       # tamaño de lote (batch size) por tarjeta
    per_device_eval_batch_size=4,
    gradient_accumulation_steps=4,       # acumulación de gradientes, equivalente a un lote mayor
    learning_rate=2e-5,                  # tasa de aprendizaje, el rango común es 2e-5 a 5e-5
    num_train_epochs=3,                  # número de épocas, 2-5 épocas es adecuado
    logging_steps=10,                    # registrar cada 10 pasos
    evaluation_strategy="epoch",         # evaluar una vez por época
    save_strategy="epoch",               # guardar una vez por época
    fp16=True,                           # usar entrenamiento de precisión mixta
    bf16=False,                          # Si es una GPU Ampere o superior (series 30/40), puedes configurar bf16=True
    optim="paged_adamw_32bit",           # optimizador paginado, para evitar OOM (out of memory)
    report_to="none",                    # no reportar a plataformas como wandb
    save_total_limit=3,                  # guardar como máximo 3 checkpoints
    load_best_model_at_end=True,         # cargar finalmente el mejor modelo
    metric_for_best_model="eval_loss",   # Usar eval loss para seleccionar el mejor modelo
)


# ============================================
# 2. Cargar datos y formatear
# ============================================

def format_example(example):
    """Formatear datos en formato Alpaca como entrada del modelo"""
    instruction = example["instruction"]
    input_text = example.get("input", "")
    output = example["output"]

    if input_text.strip() == "":
        # Caso sin entrada adicional
        prompt = f"""Below is an instruction that describes a task. Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Response:
"""

    else:
        # Caso con entrada adicional
        prompt = f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Input:
{input_text}

### Response:
"""


    # El texto completo es prompt + output
    return {
        "prompt": prompt,
        "completion": output,
        "text": prompt + output
    }


# Cargar datos
dataset = load_dataset("json", data_files=DATA_PATH, split="train")

# Dividir en conjunto de entrenamiento y validación (90% entrenamiento, 10% validación)
dataset = dataset.train_test_split(test_size=0.1, seed=42)
train_dataset = dataset["train"]
eval_dataset = dataset["test"]

# Formatear datos
train_dataset = train_dataset.map(format_example)
eval_dataset = eval_dataset.map(format_example)

print(f"Tamaño del conjunto de entrenamiento: {len(train_dataset)}")
print(f"Tamaño del conjunto de validación: {len(eval_dataset)}")


# ============================================
# 3. Cargar modelo y tokenizer
# ============================================

print("Cargando modelo...")

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.pad_token = tokenizer.eos_token  # Configurar pad token
tokenizer.padding_side = "right"           # Padding desde la derecha

model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
    use_auth_token=True,  # Si se necesita iniciar sesión en HuggingFace
)

# Envolver LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()  # Imprimir proporción de parámetros entrenables


# ============================================
# 4. Crear el entrenador y comenzar el entrenamiento
# ============================================

trainer = SFTTrainer(
    model=model,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    args=training_args,
    tokenizer=tokenizer,
    peft_config=lora_config,
    dataset_text_field="text",  # Qué campo contiene el texto completo
    max_seq_length=2048,        # Longitud máxima de secuencia
    packing=False,              # No empaquetar muestras
)

print("Comenzando entrenamiento...")
trainer.train()
print("¡Entrenamiento completado!")


# ============================================
# 5. Guardar el modelo
# ============================================

# Guardar adaptador LoRA (tamaño pequeño, desde unos pocos MB hasta decenas de MB)
model.save_pretrained(f"{OUTPUT_DIR}/lora_adapter")
tokenizer.save_pretrained(f"{OUTPUT_DIR}/lora_adapter")
print(f"Adaptador LoRA guardado en {OUTPUT_DIR}/lora_adapter")

# Si se desea fusionar los pesos de LoRA (opcional)
# merged_model = model.merge_and_unload()
# merged_model.save_pretrained(f"{OUTPUT_DIR}/merged_model")
# tokenizer.save_pretrained(f"{OUTPUT_DIR}/merged_model")
# print(f"El modelo fusionado se ha guardado en {OUTPUT_DIR}/merged_model")

Si quieres un método más simple, puedes usar LLaMA-Factory, que ofrece dos modos: Web UI y línea de comandos.

Ejemplo

# ============================================
# Entrenar con LLaMA-Factory
# ============================================

# Opción 1: Iniciar Web UI (la más intuitiva)
cd LLaMA-Factory
python src/train_web.py

# Opción 2: Entrenamiento por línea de comandos (adecuado para automatización)
python src/train.py \
    --model_name_or_path mistralai/Mistral-7B-v0.2 \
    --dataset ejemplo_ecommerce \
    --dataset_dir ./data \
    --output_dir ./output/ejemplo-model \
    --finetuning_type lora \
    --quantization_bit 4 \
    --lora_rank 16 \
    --lora_alpha 16 \
    --lora_dropout 0.05 \
    --lora_target q_proj,v_proj,k_proj,o_proj,gate_proj,up_proj,down_proj \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --lr_scheduler_type cosine \
    --logging_steps 10 \
    --save_strategy epoch \
    --learning_rate 2e-5 \
    --num_train_epochs 3 \
    --fp16 true \
    --plot_loss true

Ajuste de hiperparámetros de entrenamiento

Elegir hiperparámetros es un arte, pero también hay algunas reglas empíricas.

Tasa de aprendizaje (Learning Rate): Es el hiperparámetro más importante. Para LoRA, 2e-5 a 5e-5 es un punto de partida seguro. Si el entrenamiento es inestable (fluctuación de loss), ajústala a un valor menor. Si el entrenamiento es demasiado lento (el loss disminuye lentamente), ajústala a un valor mayor.

Tamaño de batch (Batch Size): Cuanto más grande, mejor, pero limitado por la memoria de video. per_device_train_batch_size puede ser 2, 4, 8. Si no hay suficiente memoria, usa gradient_accumulation_steps para acumular gradientes. Por ejemplo, batch_size=2, accumulation_steps=4 equivale a batch_size=8.

Épocas (Epochs): 2-5 épocas es adecuado. Muy pocas, el modelo no aprende; demasiadas, puede sobreajustarse. Puedes observar el eval loss; cuando ya no disminuya, puedes detenerte.

Rango LoRA (r): De 8 a 64 está bien. 16 es un buen punto de partida. Para tareas simples usa 8, para tareas complejas usa 32 o 64.

Longitud máxima de secuencia (Max Sequence Length): Depende de tus datos. 512, 1024, 2048, 4096 son comunes. Cuanto más larga, más memoria de video se necesita.

HiperparámetrosValor inicial recomendadoDirección de ajuste
learning_rate2e-5Si el loss fluctúa, redúcelo; si disminuye demasiado lento, auméntalo.
per_device_train_batch_size4Si hay memoria suficiente, auméntalo; si no, redúcelo.
gradient_accumulation_steps4Batch equivalente = batch_size × accumulation
num_train_epochs3Si hay sobreajuste, redúcelo; si hay subajuste, auméntalo.
lora_rank (r)16Tareas simples 8, tareas complejas 32-64
max_seq_length1024 o 2048Ajusta según la longitud de tus datos.

Monitoreo del entrenamiento e interpretación de la pérdida

Durante el entrenamiento hay que prestar atención a varias métricas.

  • Train Loss: pérdida en el conjunto de entrenamiento, debería disminuir constantemente. Si disminuye muy rápido, significa que la tasa de aprendizaje puede ser demasiado grande o hay muy pocos datos. Si disminuye muy lento, significa que la tasa de aprendizaje es demasiado pequeña o la capacidad del modelo es insuficiente.

  • Eval Loss: pérdida en el conjunto de validación, es la métrica más importante. Debería disminuir junto con el train loss. Si el eval loss empieza a subir mientras el train loss sigue bajando, indica sobreajuste, y deberías detener el entrenamiento.

Manifestaciones de sobreajuste: eval loss baja primero y luego sube, mientras train loss sigue bajando. Soluciones: reducir épocas, aumentar dropout, aumentar datos, reducir el rango de LoRA.

Manifestaciones de subajuste: tanto train loss como eval loss son altos y disminuyen muy lento. Soluciones: aumentar épocas, aumentar el rango de LoRA, aumentar datos, aumentar ligeramente la tasa de aprendizaje.


Evaluación del efecto del fine-tuning

Una vez terminado el entrenamiento, ¿cómo saber si el modelo es bueno?

Selección de métricas de evaluación

El método de evaluación depende del tipo de tarea.

La primera es la evaluación manual. Una persona lee la salida del modelo y califica la calidad. Es la forma más confiable, pero también la que más tiempo consume.

Puedes hacer una tabla de puntuación simple:

Ejemplo

# ============================================
# Plantilla simple de evaluación manual
# ============================================

evaluation_prompts = [
    {
        "instruction": "Generar una descripción de producto de comercio electrónico",
        "input": "Nombre del producto: Cafetera portátil\nFunciones: extracción con un botón, diseño portátil, carga USB",
        "reference": "Esta cafetera portátil te permite disfrutar de café recién molido en cualquier lugar. La extracción con un botón es sencilla, el diseño compacto y portátil es ideal para oficina, viajes y exteriores. Con carga USB, no te preocupes por la fuente de alimentación."
    },
    {
        "instruction": "Generar una descripción de producto de comercio electrónico",
        "input": "Nombre del producto: Teclado mecánico\nCaracterísticas: interruptores azules, retroiluminación RGB, 87 teclas, keycaps PBT",
        "reference": "Este teclado mecánico de 87 teclas utiliza interruptores azules nítidos, con una excelente sensación al escribir. Retroiluminación RGB en todas las teclas, clara incluso de noche. Las keycaps PBT son duraderas y no se vuelven grasientas, siguen como nuevas incluso con uso prolongado."
    },
    # ... 10-20 casos de prueba
]


def evaluate_model(model, tokenizer, prompts):
    """Evaluar el modelo con un conjunto de prompts"""
    results = []
    for item in prompts:
        # Generar salida
        inputs = format_example_for_inference(item)
        output = generate_with_model(model, tokenizer, inputs)

        results.append({
            "instruction": item["instruction"],
            "input": item["input"],
            "model_output": output,
            "reference": item["reference"],
            # Dejar en blanco para que una persona puntúe
            "score_relevance": None,  # 1-5, relevancia
            "score_quality": None,    # 1-5, calidad
            "score_format": None,     # 1-5, corrección de formato
        })

    return results


# Ejecutar evaluación
results = evaluate_model(model, tokenizer, evaluation_prompts)

# Guardar resultados, puntuar manualmente más tarde
with open("evaluation_results.json", "w", encoding="utf-8") as f:
    json.dump(results, f, ensure_ascii=False, indent=2)

print("Resultados de la evaluación guardados, analiza después de la puntuación manual")

La segunda es la evaluación automática. Usa métricas como BLEU, ROUGE, BLEURT. Estas métricas no son perfectas, pero permiten iterar rápidamente.

La tercera es la prueba A/B. Mezcla las salidas del modelo anterior y del nuevo para que alguien las evalúe a ciegas, y observa si el nuevo modelo es realmente mejor.

Prevención del sobreajuste

El sobreajuste es el problema más común en el fine-tuning. Se manifiesta como un buen rendimiento en el conjunto de entrenamiento, pero un mal rendimiento en datos nuevos.

Métodos para prevenir el sobreajuste:

1. Detención temprana (Early Stopping): supervisa el eval loss y detén el entrenamiento cuando deje de disminuir.

2. Aumentar datos: más datos y más diversos son el mejor método para prevenir el sobreajuste.

3. Aumento de datos: reescribe los datos existentes para crear variaciones.

4. Reducir la capacidad del modelo: reduce el rango de LoRA y el número de capas de LoRA.

5. Aumentar Dropout: establece lora_dropout en 0.05 o 0.1 en la configuración de LoRA.

6. Decaimiento de pesos (Weight Decay): añade weight_decay=0.01 en los parámetros de entrenamiento.


Despliegue del modelo afinado

Una vez entrenado el modelo, ¿cómo usarlo?

Cargar LoRA para inferencia

La primera forma es cargar el modelo original y luego cargar el adaptador LoRA.

Ejemplo

# ============================================
# Cargar LoRA y realizar inferencia
# ============================================

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel


def load_finetuned_model(base_model_name, lora_adapter_path):
    """Cargar el modelo base y el adaptador LoRA"""
    # Cargar tokenizer
    tokenizer = AutoTokenizer.from_pretrained(base_model_name)
    tokenizer.pad_token = tokenizer.eos_token

    # Cargar modelo base
    model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        device_map="auto",
        torch_dtype=torch.float16,
        trust_remote_code=True,
    )

    # Cargar adaptador LoRA
    model = PeftModel.from_pretrained(model, lora_adapter_path)

    # Fusionar pesos (opcional, así la inferencia es más rápida)
    model = model.merge_and_unload()

    return model, tokenizer


def generate_product_description(model, tokenizer, product_name, features):
    """Generar descripción de producto de comercio electrónico"""
    # Construir el prompt (¡igual que durante el entrenamiento!)
    prompt = f"""Below is an instruction that describes a task. Write a response that appropriately completes the request.

### Instruction:
Generar una descripción de producto de comercio electrónico

### Input:
Nombre del producto: {product_name}
Funciones: {features}

### Response:
"""


    # Generar
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=200,
            temperature=0.7,      # Controlar la aleatoriedad, 0-1, cuanto mayor, más aleatorio
            top_p=0.9,
            do_sample=True,
            pad_token_id=tokenizer.eos_token_id,
        )

    # Decodificar salida
    generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)

    # Tomar solo la parte de Response
    response = generated_text.split("### Response:")[-1].strip()

    return response


# ============================================
# Ejemplo de uso
# ============================================

BASE_MODEL = "mistralai/Mistral-7B-v0.2"
LORA_PATH = "output/ejemplo-ecommerce-model/lora_adapter"

print("Cargando modelo...")
model, tokenizer = load_finetuned_model(BASE_MODEL, LORA_PATH)
print("¡Modelo cargado!")

# Probar generación
product_name = Robot aspirador inteligente
features = Navegación láser, retorno automático a la base de carga, control por APP, succión potente de 2000Pa

description = generate_product_description(
    model, tokenizer, product_name, features
)

print(f"\nNombre del producto: {product_name})
print(fDescripción generada:\n{description}")

Métodos de despliegue

Hay varias opciones comunes para el despliegue.

  • La primera es usar vLLM. Es un motor de inferencia de alto rendimiento que admite LoRA, procesamiento por lotes continuo y otras funciones. Recomendado para entornos de producción.

  • La segunda es usar Text Generation Inference. Es el servicio de inferencia oficial de HuggingFace, con funciones completas.

  • La tercera es usar FastAPI para encapsularlo uno mismo. Es simple y directo, adecuado para despliegues a pequeña escala.

  • La cuarta es usar Ollama. Convierte el modelo al formato GGUF y ejecútalo con Ollama, adecuado para despliegue local.

Otras extensiones