Inicio / Artículos

De la predicción de token siguiente a la ejecución de instrucciones: ajuste fino de un modelo GPT

Cómo un GPT preentrenado aprende a seguir instrucciones: formato de los registros, cambio de etiquetas, máscaras de relleno, el bucle de entrenamiento, límites de evaluación y el avance hacia las preferencias de DPO.

Editor: REACTAPP.TOP9 min de lectura2112 palabras

Fuentes: From Next-Token Prediction to ChatGPT <> Build an LLM From Scratch [6]

El preentrenamiento ya le otorga a un modelo de lenguaje la capacidad de escribir. Lo que le falta es cualquier noción de lo que el usuario desea que se haga. El ajuste por instrucciones proporciona eso. Si se le presenta a dos modelos la misma solicitud, por ejemplo explicar la recursión a un niño de diez años, la diferencia es evidente. El modelo base tiende a repetir la solicitud y desviarse hacia una introducción al estilo de los libros de texto sobre cursos de informática. En realidad no está respondiendo, solo prolongando el texto. El modelo ajustado, en cambio, ofrece una imagen: una caja que contiene otra caja más pequeña que a su vez contiene otra aún más pequeña, describiendo la recursión como un problema que se sigue entregando a sí mismo una versión más pequeña hasta que la más pequeña es fácil de resolver.

A nivel fundamental, ambos modelos hacen lo mismo: predecir el siguiente token, pero se comportan de manera muy diferente. Los productos de chat hacen que seguir instrucciones parezca algo natural, lo que oculta esa diferencia. Las preguntas reciben respuesta, las solicitudes de código generan código y se redactan resúmenes. Nada de esto se obtiene gratuitamente al leer una gran cantidad de texto. El preentrenamiento inculca algo más básico: dado todo lo anterior, ¿qué token es probable que aparezca a continuación? En ello no se indica en absoluto que una solicitud del usuario deba ser atendida. Superar esa brecha es el tema de este texto.

Acto 1: el preentrenamiento enseña a completar, no a obedecer

La tarea del preentrenamiento es simplemente la predicción del siguiente token. Eso por sí solo es lo suficientemente poderoso como para aprender gramática, hechos, sintaxis de código, estilos de escritura y algunos patrones sorprendentemente complejos, pero obedecer instrucciones no está entre ellos. Proporcione a un modelo base este prompt:

Explain why the sky appears blue.

Y podría continuar con:

Explain why the sky appears blue.
The following questions are commonly asked in introductory physics...

Eso no necesariamente significa que sea un modelo defectuoso. Está haciendo exactamente lo que le pidió el preentrenamiento, que es continuar el texto. El comportamiento deseado se parece más a esto:

The sky appears blue because Earth's atmosphere scatters
shorter wavelengths of sunlight more strongly than longer
wavelengths...

El modelo subyacente es el mismo, pero el comportamiento esperado difiere. La pregunta cambia a la siguiente:

What text probably comes next?

a esto:

When the text looks like an instruction,
what kind of continuation should I produce?

Enseñar al modelo a responder a esa segunda pregunta es precisamente para lo que sirve el ajuste fino de instrucciones.

Acto 2: formateo de los datos de instrucciones

La forma más directa de enseñar al modelo a seguir instrucciones es mostrarle muchas instrucciones acompañadas de buenas respuestas, y realmente es así de sencillo. Un registro de entrenamiento podría verse así:

example = {
    "instruction": "Convert the following sentence to passive voice.",
    "input": "The developer fixed the bug.",
    "output": "The bug was fixed by the developer."
}

Un transformador no puede procesar diccionarios de Python, por lo que el registro se serializa primero en texto plano, de la siguiente manera:

Below is an instruction that describes a task.

### Instruction:
Convert the following sentence to passive voice.

### Input:
The developer fixed the bug.

### Response:
The bug was fixed by the developer.

El formato es importante porque el modelo ve repetidamente el mismo patrón:

instruction
    ↓
optional context
    ↓
response

En resumen, cada registro se convierte en texto formateado, luego en tokens, y después en lotes rellenos; estos lotes realizan un ajuste fino del modelo GPT preentrenado.

La vista de tokens

Una vez tokenizado, el ejemplo se transforma en una lista de IDs, más o menos así:

[21106, 318, 281, 12064, ... 4435, 257, 3126, ...]

Esos IDs de token forman la secuencia de entrenamiento. En el caso de un modelo autoregresivo, las etiquetas son simplemente la secuencia desplazada una posición:

Tokens:
[A, B, C, D, E]

Input:
[A, B, C, D]

Labels:
[B, C, D, E]

Por lo tanto, el ajuste de instrucciones no introduce ningún objetivo de aprendizaje completamente nuevo. El modelo sigue prediciendo el siguiente token. Lo que cambia es la distribución de texto que se le pide que prediga. En lugar de documentos en su mayoría ordinarios, ahora ve esta asignación una y otra vez:

instruction → useful response

Esa es la idea central.

El relleno requiere un manejo especial

Los ejemplos de entrenamiento varían en longitud, pero los lotes necesitan tensores con formas compatibles. Tomemos dos ejemplos:

Example 1:
[10, 20, 30, 40]

Example 2:
[11, 21]

Pueden tener relleno:

Example 1:
[10, 20, 30, 40]

Example 2:
[11, 21, PAD, PAD]

Los tokens de relleno no son objetivos significativos. El modelo no debería ser recompensado por aprender esto:

PAD → PAD → PAD → PAD

Por lo tanto, esas posiciones suelen reemplazarse con un valor de ignorar:

ignore_index = -100

La función de pérdida de entropía cruzada de PyTorch las omite. Conceptualmente:

loss = cross_entropy(
    logits.reshape(-1, vocab_size),
    targets.reshape(-1),
    ignore_index=-100,
)

Los detalles son importantes. Sin enmascarado, parte de la señal de entrenamiento se desperdicia en artefactos generados por el agrupamiento en lotes en lugar de en el lenguaje.

Acto 3: ajustar el comportamiento

El propio bucle de entrenamiento no es algo exótico aquí, ya que la optimización de un modelo de token siguiente ya es algo conocido. Lo importante es qué ejemplos se utilizan para alimentar el bucle. De forma simplificada:

for batch in train_loader:
    optimizer.zero_grad()

    input_ids = batch[:, :-1]
    targets = batch[:, 1:]

    logits = model(input_ids)

    loss = cross_entropy(
        logits.flatten(0, 1),
        targets.flatten(),
        ignore_index=-100,
    )

    loss.backward()
    optimizer.step()

Nada en ese código dice algo similar a esto:

model.learn_to_be_helpful()

El comportamiento proviene de los datos. Supongamos que miles de ejemplos se ven así:

### Instruction:
Summarize this paragraph.

### Response:
<clear concise summary>

Las actualizaciones de gradiente siguen empujando al modelo hacia respuestas de esa estructura tras recibir prompts de ese tipo. Supongamos que otros ejemplos se ven así:

### Instruction:
Write Python code that reverses a string.

### Response:
def reverse_string(s):
    return s[::-1]

Apprende otra correspondencia entre un patrón de instrucción y una continuación adecuada. Visto a nivel de tokens, el ajuste fino de instrucciones no es tan mágico: el contexto alrededor de las predicciones se ha modificado intencionadamente. Antes:

The CPU cache is...

Después del formato de instrucciones:

### Instruction:
Explain CPU caching to a beginner.

### Response:
A CPU cache is...

No se introdujo un objetivo como “entender a los usuarios”. En su lugar, la distribución del texto de entrenamiento se modificó de modo que las respuestas útiles se convirtieran en las continuaciones estadísticamente más probables después de una instrucción. Ese cambio es el que genera el nuevo comportamiento. En el ejemplo presentado, un modelo GPT-2-medium preentrenado se ajusta mediante fine-tuning basado en instrucciones, genera respuestas y guarda el modelo resultante.

Acto 4: la evaluación se vuelve complicada

La clasificación ofrecía un enfoque sencillo para la evaluación. En el caso de la detección de spam:

Prediction: SPAM
Label:      SPAM

Correct.

El seguimiento de instrucciones no permite tal lujo. Considere la solicitud de explicar la recursión de manera sencilla. Una respuesta posible:

Recursion is when a function solves a problem by calling
itself on a smaller version of the same problem.

Otro:

Recursion is a technique where a problem is reduced into
smaller instances of itself until a stopping condition is reached.

Puede ser correcto cualquiera de los dos, y ninguno necesita coincidir con la referencia palabra por palabra. Eso hace que la precisión de coincidencia exacta sea en su mayor parte inútil para respuestas abiertas.

Comparación con una referencia

Una opción es proporcionar una referencia:

{
    "instruction": "Explain recursion simply.",
    "reference": "Recursion solves a problem by repeatedly reducing it...",
    "model_response": "A recursive function calls itself..."
}

Y comparar la respuesta generada con ella. Pero la similitud no es sinónimo de calidad, y un modelo podría dar una respuesta excelente con palabras completamente diferentes. Eso conduce a otro enfoque.

Pedir a otro LLM que juzgue

El evaluador recibe la instrucción, la respuesta de referencia y la respuesta a calificar:

Instruction:
Explain recursion simply.

Reference answer:
...

Model answer:
...

Rate the model answer from 0 to 100 based on correctness,
relevance, and clarity.

Un juez local puede utilizar Ollama, lo cual es práctico, pero plantea un nuevo problema: el propio juez es un modelo. Su puntuación puede depender del prompt de evaluación, de la elección del evaluador, del orden de las respuestas, del nivel de detalle, del gusto estilístico y de los errores del juez. Algunos ejemplos muestran a un juez local otorgando puntuaciones sorprendentemente altas a respuestas de modelos base de calidad deficiente, lo que recuerda que las cifras de los LLM como jueces no constituyen la verdad objetiva.

Un proyecto pequeño puede combinar tres capas:

1. Inspect representative outputs manually
2. Compare against reference answers where useful
3. Use an LLM judge for aggregate comparison

Ninguno de estos es perfecto por sí solo. Juntos ofrecen una imagen más clara que un único número que se presenta como verdad.

Acto 5: un problema más difícil, las preferencias

El ajuste fino de las instrucciones le muestra al modelo cómo debe ser una respuesta razonable. Ahora comparemos dos respuestas. Respuesta A:

Recursion is when a function calls itself.

Respuesta B:

Recursion is a technique where a function solves a problem
by calling itself on a smaller version of that problem.
The process stops when it reaches a base case.

Ambos son válidos, pero se puede preferir B. La señal de entrenamiento ya no es esta:

correct vs incorrect

Es esto:

chosen vs rejected

Un registro de preferencias puede verse así:

{
    "prompt": "Explain recursion simply.",
    "chosen": """
    Recursion solves a problem by repeatedly reducing it
    until reaching a base case.
    """,
    "rejected": """
    Recursion is when recursion happens recursively.
    """
}

Ese es un tipo diferente de supervisión. En lugar de decir “he aquí una respuesta para imitar”, dice “dadas estas dos respuestas, prefiera esta”. Aquí entran en juego técnicas como la Optimización Directa de Preferencias (DPO). A un nivel alto:

Prompt
   ↓
Chosen response ─────┐
                     ├── preference objective
Rejected response ───┘
                     ↓
               model update

El DPO merece un trato aparte, por lo que sus mecanismos quedan fuera de esta discusión. El enlace útil es la progresión general:

Pretraining
    ↓
learn language patterns

------------------------------

Instruction fine-tuning
    ↓
learn instruction → response behavior

------------------------------

Preference tuning
    ↓
learn which acceptable responses we prefer

La generación de conjuntos de datos de preferencias con Llama 3.1 a través de Ollama y la implementación de DPO desde cero son temas para otro momento. Hay una advertencia: el hecho de que DPO esté disponible como material adicional no significa que sea el siguiente paso adecuado para todos los modelos o conjuntos de datos. Algunos experimentos muestran limitaciones en sus resultados, lo cual es precisamente el escepticismo que se debe tener al ajustar las preferencias.

Qué cambió realmente

El punto de partida fue un modelo preentrenado que sabía cómo continuar un texto. El arquitectura de transformador no se reemplazó por una arquitectura de asistente especial. Los ejemplos relacionados con el problema del siguiente token cambiaron, lo que generó un patrón de comportamiento diferente. Ahora, cuando el modelo ve:

### Instruction:

Se han presentado suficientes casos en los que la continuación no es otro párrafo de Internet sino una respuesta. Visto de esta manera, es más fácil razonar sobre la progresión:

Pretraining:
What token should come next?

Instruction tuning:
What does a good answer look like after an instruction?

Preference tuning:
Of several reasonable answers, which behavior should we prefer?

La arquitectura, el optimizador y la tasa de aprendizaje son todos importantes, pero en el ajuste por instrucciones los datos desempeñan un papel especial, ya que definen el comportamiento que se debe imitar. Al existir varias respuestas aceptables, el objetivo ya no es obtener una salida válida en sí misma. La pregunta pasa a ser cuál respuesta aceptable merece más importancia, y ahí es donde DPO abre nuevas posibilidades.

El preentrenamiento enseña qué texto tiende a aparecer a continuación. El ajuste por instrucciones determina qué continuaciones se consideran respuestas útiles a las instrucciones. El ajuste por preferencias da el siguiente paso al decidir cuáles de esas respuestas útiles son las que se deben priorizar.

Leer más

  • Cómo un Transformer predice el siguiente token: atención, residuos y softmax — Una explicación en lenguaje sencillo sobre el Transformer de estilo GPT, desde tokens y embeddings posicionales hasta atención causal, capas feed-forward, logits y softmax.
  • De la predicción del siguiente token al código funcional: cómo los LLM generan Python — Explicación ordenada de tokens, softmax, temperatura, atención autónoma y capas feed-forward, con un ejemplo paso a paso de ordenamiento y los límites inherentes a la predicción.
  • Notas prácticas: Cómo funcionan realmente los LLM: tokens, transformadores y el siguiente token — Guía detallada de las Notas prácticas: Cómo funcionan realmente los LLM: tokens, transformadores y el siguiente token: contratos, verificaciones y espacios para código listo para usar para los equipos que implementan este patrón.
  • Estrategias de fragmentación para RAG en producción: de tamaño fijo, semántica y padre-hijo — Recorrido práctico de la fragmentación en RAG, desde la división por tamaño fijo y recursiva hasta los enfoques padre-hijo, proposiciones, fragmentación tardía, RAPTOR, tablas, código y pruebas de rendimiento.
  • Sandbox de modelos de lenguaje recursivos: qué obstaculizó el proceso y qué necesita reconstrucción — Una revisión retrospectiva de una plataforma de modelos de lenguaje recursivos: capas de aislamiento, intermediarios, CDR, límites de seguimiento en el espacio de usuario, límites de un único host y por qué la moderación fue lo más importante.
  • Cómo funciona un LLM: tokens, atención y predicción del siguiente token — Una explicación sencilla de la arquitectura de los LLM, desde tokens e incrustaciones pasando por la atención, el enmascaramiento causal y la generación, además de por qué el entrenamiento y la inferencia difieren.