Phase 10 - Lesson 06
Ajuste Fino Instruccional (SFT)
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Un modelo base predice el siguiente token. Eso es todo. No sigue instrucciones, no responde preguntas ni rechaza solicitudes dañinas. SFT es el puente entre un predictor de tokens y un asistente útil. Cada modelo con el que has hablado -- Claude, GPT, Llama Chat -- pasó por este paso.
Tipo: Constrúyelo -- wait, the original was "Type: Build". In Portuguese I translated to "Tipo: Construir". In Spanish let's translate to "Tipo: Construir" too (or "Construcción", but let's use "Construir" for consistence). Let me check my draft: it had "Tipo: Construir". That's fine. Languages: Python (con numpy) -- wait, "Languages" -> "Idiomas" (Spanish/Portuguese). The original: "Languages: Python (with numpy)". In Portuguese: "Idiomas: Python (com numpy)". In Spanish: "Idiomas: Python (con numpy)". Prerrequisitos: Phase 10, Lesson 04 (Pre-Training a Mini GPT) Tiempo: ~90 minutos
Objetivos de Aprendizaje
- Implementar el ajuste fino supervisado (SFT) que convierte un modelo de lenguaje base en un asistente que sigue instrucciones
- Formatear datos de entrenamiento usando plantillas de chat con roles de sistema, usuario y asistente, y enmascarar la pérdida en los tokens que no pertenecen al asistente
- Explicar por qué es necesario el SFT: los modelos base continúan el texto en lugar de responder preguntas
- Evaluar la calidad del SFT comparando las respuestas del modelo base frente al modelo ajustado en un conjunto de instrucciones reservado
El Problema
Entrenaste un modelo en la Lesson 04. Puede predecir el siguiente token dada una secuencia. Aliméntalo con "The transformer architecture" y podría continuar con "has revolutionized natural language processing." Eso es impresionante para un predictor del siguiente token.
Ahora intenta esto: aliméntalo con "What is the capital of France?" Un modelo base no responde "Paris." Continúa el patrón. Podría producir "What is the capital of Germany? What is the capital of Spain?" porque aprendió de documentos que contienen listas de preguntas. O podría producir "is a question that many people ask" porque esa es una continuación plausible del siguiente token. El modelo no tiene el concepto de responder. Solo sabe continuar.
Esta es la brecha entre GPT-3 (modelo base, lanzado en junio de 2020) y ChatGPT (ajustado para instrucciones, lanzado en noviembre de 2022). Misma arquitectura. Mismo preentrenamiento. La diferencia son de 20,000 a 100,000 pares de (instrucción, respuesta) cuidadosamente elaborados que le enseñaron al modelo el patrón de conversación.
Stanford Alpaca demostró que no se necesitan millones de ejemplos. En marzo de 2023, ajustaron Llama 7B con solo 52,000 pares de instrucción-respuesta generados por GPT-3.5. Costo total: $600. El resultado fue un chatbot que podía seguir instrucciones, responder preguntas y mantener conversaciones. No tan bueno como ChatGPT, pero sorprendentemente cercano para $600 y unas pocas horas de entrenamiento.
Llama 2 Chat de Meta utilizó solo ~27,000 ejemplos de alta calidad para su etapa SFT inicial. La idea clave: la calidad importa más que la cantidad. 27,000 ejemplos escritos por anotadores calificados superan a 1 millón de ejemplos ruidosos extraídos de internet.
El Concepto
Qué Hace Realmente el SFT
El Ajuste Fino Supervisado continúa el mismo bucle de entrenamiento del preentrenamiento -- paso hacia adelante, calcular pérdida, paso hacia atrás, actualizar pesos -- pero con un tipo de datos diferente. En lugar de texto sin procesar, entrenas con conversaciones estructuradas:
{
"system": "You are a helpful assistant.",
"user": "What is the capital of France?",
"assistant": "The capital of France is Paris."
}
El modelo ya sabe que París es la capital de Francia. Aprendió esto durante el preentrenamiento en Wikipedia, libros de texto y páginas web. El SFT no enseña nuevos hechos al modelo. Le enseña al modelo un nuevo comportamiento: cuando veas una pregunta, produce una respuesta. Cuando veas una instrucción, produce una completación. Cuando veas una solicitud dañina, produce un rechazo.
Piénsalo de esta manera. El preentrenamiento le da conocimiento al modelo. El SFT le da modales.
Formatos de Datos
Tres formatos dominan la industria. Cada uno codifica la misma información -- quién dijo qué -- con diferentes delimitadores.
Formato Alpaca (Stanford, marzo de 2023):
{
"instruction": "Summarize the following article in 3 sentences.",
"input": "The European Central Bank raised interest rates...",
"output": "The ECB increased rates by 25 basis points..."
}
Simple y ampliamente utilizado. El campo input es opcional -- muchas instrucciones no necesitan contexto adicional. Stanford lanzó 52,000 ejemplos en este formato, generados por GPT-3.5 por $600. Esto inició el movimiento de ajuste de instrucciones de código abierto.
Formato ShareGPT (comunidad, 2023):
{
"conversations": [
{"from": "system", "value": "You are a helpful assistant."},
{"from": "human", "value": "What causes tides?"},
{"from": "gpt", "value": "Tides are caused by the gravitational pull of the Moon..."},
{"from": "human", "value": "How often do they occur?"},
{"from": "gpt", "value": "Most coastal areas experience two high tides and two low tides per day..."}
]
}
Soporta conversaciones de varios turnos. El campo "from" usa "human" y "gpt" por convención, independientemente del modelo real. Vicuna fue entrenado en 70,000 conversaciones de ShareGPT extraídas de transcripciones de ChatGPT compartidas por usuarios.
Formato ChatML (OpenAI, utilizado por muchos modelos de código abierto):
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
What is the capital of France?<|im_end|>
<|im_start|>assistant
The capital of France is Paris.<|im_end|>
Usa tokens especiales (<|im_start|>, <|im_end|>) para delimitar roles. Estos tokens se agregan al vocabulario del tokenizador durante el ajuste fino. Qwen, Yi y muchos otros modelos usan ChatML.
Los tres formatos logran lo mismo: le dicen al modelo "esta es la instrucción, esta es la respuesta, aprende este patrón."
Por Qué Funciona
El modelo ya conoce el lenguaje gracias al preentrenamiento. Ha visto miles de millones de ejemplos de preguntas seguidas de respuestas, instrucciones seguidas de completaciones y conversaciones entre personas. Los patrones ya están codificados en los pesos.
El SFT concentra esta habilidad latente. En lugar de que el modelo tenga que deducir por el contexto si debe responder una pregunta o continuar un documento, el SFT entrena explícitamente en el patrón de conversación. Después de unos pocos miles de ejemplos, el modelo aprende: cuando veas el marcador de rol de asistente, produce una respuesta útil.
Por esto 27,000 ejemplos son suficientes. No le estás enseñando inglés al modelo. No le estás enseñando hechos sobre el mundo. Le estás enseñando un comportamiento simple: responder a instrucciones. El conocimiento ya estaba allí.
La Pérdida Enmascarada
Este es el detalle técnico más importante en SFT, y la mayoría de los tutoriales lo omiten.
Durante el preentrenamiento, calculas la pérdida en cada token. El modelo aprende a predecir cada siguiente token en la secuencia. Durante el SFT, solo calculas la pérdida en los tokens de la respuesta. Los tokens de la instrucción están allí por contexto, pero el modelo no es penalizado por "predecirlos" incorrectamente.
¿Por qué? Porque no quieres que el modelo aprenda a generar instrucciones. Quieres que aprenda a responder a instrucciones. Si calculas la pérdida en los tokens de instrucción, estarás entrenando al modelo para predecir "What is the capital of France?" como si fuera él quien hace la pregunta. Eso desperdicia la señal del gradiente y puede confundir al modelo sobre su rol.
En la práctica, creas una máscara de pérdida: 1 para los tokens de respuesta, 0 para los tokens de instrucción. Multiplica la pérdida por token por esta máscara antes de promediar.
Tokens: [SYS] You are helpful [USER] What is the capital? [ASST] Paris is the capital [EOS]
Loss mask: 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1
Solo los tokens después de [ASST] contribuyen a la pérdida. El modelo ve la conversación completa durante el paso hacia adelante (necesita la instrucción para producir la respuesta correcta) pero solo actualiza sus pesos en función de qué tan bien predijo la respuesta.
Hiperparámetros de Entrenamiento
El SFT utiliza hiperparámetros drásticamente diferentes a los del preentrenamiento. No estás entrenando desde cero. Estás ajustando un modelo que ya funciona.
| Parámetro | Preentrenamiento (Llama 2 7B) | SFT (Llama 2 Chat) |
|---|---|---|
| Tasa de aprendizaje | 3e-4 (pico) | 2e-5 |
| Épocas | 1 (un solo paso por los datos) | 2 |
| Tamaño de lote | 4M tokens | 64 ejemplos |
| Pasos de calentamiento | 2,000 | 0-100 |
| Decaimiento de peso | 0.1 | 0.0-0.1 |
| Tamaño de datos | 2T tokens | 27,000 ejemplos |
La tasa de aprendizaje es 15 veces menor para SFT. Esto es crítico. Una tasa de aprendizaje alta durante el ajuste fino destruye el conocimiento preentrenado. El modelo "olvida" lo que aprendió y se sobreajusta al pequeño conjunto de datos de ajuste fino. Esto es el olvido catastrófico.
Dos épocas significan que el modelo ve cada ejemplo de entrenamiento dos veces. Más de 3 épocas en un conjunto de datos pequeño conduce a la memorización; el modelo comienza a reproducir los ejemplos de entrenamiento palabra por palabra en lugar de generalizar.
Olvido Catastrófico
El ajuste fino puede destruir capacidades generales. Si entrenas durante demasiado tiempo con datos de seguimiento de instrucciones, el modelo perderá su capacidad para escribir código, resolver matemáticas o producir texto creativo. Se vuelve muy bueno en el formato específico de sus datos de entrenamiento y terrible en todo lo demás.
Tres mitigaciones:
Tasa de aprendizaje baja. De 1e-5 a 5e-5. Actualizaciones más pequeñas significan menos destrucción de las características preentrenadas.
Entrenamiento corto. De 1 a 3 épocas. Detente antes de que el modelo se sobreajuste.
Mezclar datos de preentrenamiento. Llama 2 Chat mezcló un pequeño porcentaje (2-5%) de datos de preentrenamiento sin procesar en el conjunto de datos de SFT. Esto le "recuerda" al modelo sus capacidades generales mientras aprende el nuevo comportamiento de seguimiento de instrucciones.
Números Reales
El ajuste fino de un modelo 7B en 10,000 pares de instrucciones de alta calidad toma aproximadamente 1 hora en una sola GPU NVIDIA A100 de 80GB. Aquí está la matemática:
- 10,000 ejemplos x 512 tokens en promedio = 5.12M tokens
- 2 épocas = 10.24M tokens en total
- Rendimiento de la A100 para el ajuste fino de un modelo 7B: ~3,000 tokens/segundo
- 10.24M / 3,000 = ~3,400 segundos = ~57 minutos
Para nuestro mini GPT (4 capas, 128 dimensiones), el entrenamiento es casi instantáneo. El punto es entender la mecánica, no la escala.
graph TD
subgraph SFT["Pipeline de Ajuste Fino Supervisado"]
direction TB
D["Dataset de Instrucciones\n(10K-100K ejemplos)"] --> F["Formatear en pares de\n(instrucción, respuesta)"]
F --> T["Tokenizar con\nplantilla de chat"]
T --> M["Crear máscara de pérdida\n(1 para respuesta, 0 para instrucción)"]
M --> FW["Paso hacia adelante\n(secuencia completa)"]
FW --> L["Calcular pérdida enmascarada\n(solo tokens de respuesta)"]
L --> BW["Paso hacia atrás"]
BW --> U["Actualizar pesos\n(lr=2e-5, 1-3 épocas)"]
end
subgraph Base["Modelo Base\n(preentrenado)"]
B1["Conoce el lenguaje"]
B2["Conoce hechos"]
B3["Sin patrón de conversación"]
end
subgraph Chat["Modelo de Chat\n(después de SFT)"]
C1["Conoce el lenguaje"]
C2["Conoce hechos"]
C3["Sigue instrucciones"]
end
Base --> SFT --> Chat
style D fill:#1a1a2e,stroke:#e94560,color:#fff
style L fill:#1a1a2e,stroke:#e94560,color:#fff
style B3 fill:#1a1a2e,stroke:#e94560,color:#fff
style C3 fill:#1a1a2e,stroke:#51cf66,color:#fff
Constrúyelo
Paso 1: Dataset de Instrucciones
Crea un dataset de instrucciones sintético. En producción, empresas como Scale AI y Anthropic emplean anotadores humanos para escribir esto. Nosotros los crearemos mediante programación para demostrar el formato.
import numpy as np
INSTRUCTION_DATA = [
{
"instruction": "What is the capital of France?",
"response": "The capital of France is Paris."
},
{
"instruction": "Explain gravity in one sentence.",
"response": "Gravity is the force that attracts objects with mass toward each other."
},
{
"instruction": "Write a haiku about the ocean.",
"response": "Waves crash on the shore, salt and foam beneath the sun, endless blue expanse."
},
{
"instruction": "What is 15 multiplied by 7?",
"response": "15 multiplied by 7 is 105."
},
{
"instruction": "Name three programming languages.",
"response": "Three programming languages are Python, Rust, and TypeScript."
},
{
"instruction": "Summarize photosynthesis.",
"response": "Photosynthesis converts sunlight, water, and carbon dioxide into glucose and oxygen."
},
{
"instruction": "What year did World War II end?",
"response": "World War II ended in 1945."
},
{
"instruction": "Define machine learning.",
"response": "Machine learning is a field where algorithms learn patterns from data to make predictions."
},
]
Ocho ejemplos es una cantidad minúscula. Stanford Alpaca utilizó 52,000. Pero la mecánica es idéntica si tienes 8 o 52,000: tokenizar, enmascarar, calcular la pérdida solo en las respuestas.
Paso 2: Tokenizar con Plantilla de Chat
Convierte los pares de instrucción-respuesta en secuencias de tokens con marcadores de rol especiales. Los marcadores le dicen al modelo dónde termina la instrucción y dónde comienza la respuesta.
SPECIAL_TOKENS = {
"INST_START": 253,
"INST_END": 254,
"RESP_START": 255,
}
def tokenize_instruction_pair(instruction, response, vocab_size=256):
inst_tokens = list(instruction.encode("utf-8"))
resp_tokens = list(response.encode("utf-8"))
inst_tokens = [min(t, vocab_size - 4) for t in inst_tokens]
resp_tokens = [min(t, vocab_size - 4) for t in resp_tokens]
tokens = (
[SPECIAL_TOKENS["INST_START"]]
+ inst_tokens
+ [SPECIAL_TOKENS["INST_END"]]
+ [SPECIAL_TOKENS["RESP_START"]]
+ resp_tokens
)
return tokens
def create_loss_mask(tokens):
mask = np.zeros(len(tokens), dtype=np.float32)
in_response = False
for i, token in enumerate(tokens):
if token == SPECIAL_TOKENS["RESP_START"]:
in_response = True
continue
if in_response:
mask[i] = 1.0
return mask
La máscara de pérdida tiene solo ceros para los tokens de instrucción y solo unos para los tokens de respuesta. El token RESP_START en sí obtiene una máscara de 0 porque es un delimitador, no parte del contenido de la respuesta.
Paso 3: Pérdida de Entropía Cruzada Enmascarada
Entropía cruzada estándar, pero multiplicada por la máscara de pérdida. Solo los tokens de respuesta contribuyen al gradiente.
def masked_cross_entropy_loss(logits, targets, loss_mask):
batch, seq_len, vocab_size = logits.shape
logits_flat = logits.reshape(-1, vocab_size)
targets_flat = targets.reshape(-1)
mask_flat = loss_mask.reshape(-1)
max_logits = logits_flat.max(axis=-1, keepdims=True)
log_softmax = logits_flat - max_logits - np.log(
np.exp(logits_flat - max_logits).sum(axis=-1, keepdims=True)
)
per_token_loss = -log_softmax[np.arange(len(targets_flat)), targets_flat]
masked_loss = per_token_loss * mask_flat
num_response_tokens = mask_flat.sum()
if num_response_tokens == 0:
return 0.0
loss = masked_loss.sum() / num_response_tokens
return loss
El denominador es num_response_tokens, no seq_len. Si divides por la longitud total de la secuencia, las instrucciones más largas diluyen la señal del gradiente. Dividir por el conteo de tokens de respuesta asegura el mismo peso por token de respuesta independientemente de la longitud de la instrucción.
Paso 4: Bucle de Entrenamiento SFT
Reutiliza el MiniGPT de la Lesson 04. El bucle de entrenamiento parece casi idéntico al preentrenamiento, pero con formato de instrucción y pérdida enmascarada.
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "04-pre-training-mini-gpt", "code"))
from main import MiniGPT, LayerNorm, FeedForward, MultiHeadAttention, TransformerBlock, Embedding
def sft_train(model, dataset, num_epochs=2, lr=2e-5, seq_len=64):
formatted_data = []
for example in dataset:
tokens = tokenize_instruction_pair(example["instruction"], example["response"])
mask = create_loss_mask(tokens)
formatted_data.append((tokens, mask))
print(f"SFT Training: {len(formatted_data)} examples, {num_epochs} epochs, lr={lr}")
print(f"Total tokens: {sum(len(t) for t, _ in formatted_data):,}")
print()
losses = []
for epoch in range(num_epochs):
epoch_loss = 0.0
num_batches = 0
indices = np.random.permutation(len(formatted_data))
for idx in indices:
tokens, mask = formatted_data[idx]
if len(tokens) < 3:
continue
if len(tokens) > seq_len:
tokens = tokens[:seq_len]
mask = mask[:seq_len]
input_ids = np.array(tokens[:-1]).reshape(1, -1)
target_ids = np.array(tokens[1:]).reshape(1, -1)
loss_mask = np.array(mask[1:]).reshape(1, -1)
logits = model.forward(input_ids)
loss = masked_cross_entropy_loss(logits, target_ids, loss_mask)
batch_size, s_len, v_size = logits.shape
probs = np.exp(logits - logits.max(axis=-1, keepdims=True))
probs = probs / probs.sum(axis=-1, keepdims=True)
dlogits = probs.copy()
dlogits[np.arange(batch_size)[:, None], np.arange(s_len), target_ids] -= 1.0
mask_expanded = loss_mask[:, :, np.newaxis]
num_resp = loss_mask.sum()
if num_resp > 0:
dlogits = dlogits * mask_expanded / num_resp
for block in model.blocks:
block.ffn.W1 -= lr * np.random.randn(*block.ffn.W1.shape) * 0.01
block.ffn.W2 -= lr * np.random.randn(*block.ffn.W2.shape) * 0.01
block.ffn.b1 -= lr * np.random.randn(*block.ffn.b1.shape) * 0.01
block.ffn.b2 -= lr * np.random.randn(*block.ffn.b2.shape) * 0.01
epoch_loss += loss
num_batches += 1
losses.append(loss)
avg_loss = epoch_loss / max(num_batches, 1)
print(f"Epoch {epoch + 1}/{num_epochs} | Avg Loss: {avg_loss:.4f}")
return model, losses
La tasa de aprendizaje es 2e-5, coincidiendo con Llama 2 Chat. Compara esto con la tasa de 3e-4 utilizada en el preentrenamiento -- 15 veces menor. El gradiente está enmascarado: los tokens de instrucción producen gradiente cero. Solo los tokens de respuesta empujan los pesos.
Paso 5: Comparar Modelo Base vs Modelo SFT
Todo el punto de SFT es el cambio de comportamiento. Midámoslo verificando cómo responde el modelo a entradas formateadas con instrucciones frente a continuaciones de texto sin procesar.
def generate_response(model, prompt_tokens, max_new_tokens=50, temperature=0.8):
tokens = list(prompt_tokens)
seq_len = model.embedding.pos_embed.shape[0]
for _ in range(max_new_tokens):
context = np.array(tokens[-seq_len:]).reshape(1, -1)
logits = model.forward(context)
next_logits = logits[0, -1, :]
next_logits = next_logits / max(temperature, 1e-8)
probs = np.exp(next_logits - next_logits.max())
probs = probs / probs.sum()
probs = np.clip(probs, 1e-10, 1.0)
probs = probs / probs.sum()
next_token = np.random.choice(len(probs), p=probs)
tokens.append(int(next_token))
return tokens
def evaluate_instruction_following(model, instructions):
print("Evaluating instruction following:")
print("-" * 50)
for instruction in instructions:
tokens = (
[SPECIAL_TOKENS["INST_START"]]
+ [min(t, 252) for t in list(instruction.encode("utf-8"))]
+ [SPECIAL_TOKENS["INST_END"]]
+ [SPECIAL_TOKENS["RESP_START"]]
)
output = generate_response(model, tokens, max_new_tokens=30, temperature=0.6)
response_start = len(tokens)
response_tokens = output[response_start:]
response_bytes = bytes([t for t in response_tokens if t < 128])
response_text = response_bytes.decode("utf-8", errors="replace")
print(f" Q: {instruction}")
print(f" A: {response_text[:80]}")
print()
En un modelo de juguete con 8 ejemplos, las respuestas no serán significativas. Eso es de esperarse. Lo importante es la estructura: el modelo aprende a producir una salida después del marcador de respuesta en lugar de continuar generando más instrucciones.
Paso 6: Medir el Olvido Catastrófico
Compara la capacidad de predicción del siguiente token del modelo antes y después de SFT. Si SFT daña las capacidades generales, la pérdida en el texto sin procesar aumentará.
def measure_forgetting(model, test_text, seq_len=64):
tokens = np.array(list(test_text.encode("utf-8")[:512]))
total_loss = 0.0
num_windows = 0
for start in range(0, len(tokens) - seq_len - 1, seq_len):
input_ids = tokens[start:start + seq_len].reshape(1, -1)
target_ids = tokens[start + 1:start + seq_len + 1].reshape(1, -1)
logits = model.forward(input_ids)
batch, s_len, vocab_size = logits.shape
logits_flat = logits.reshape(-1, vocab_size)
targets_flat = targets.reshape(-1) -- wait, in Step 6, the target is target_ids_flat? The code content from en.md had "targets_flat = target_ids.reshape(-1)"? Oh, let's keep it exactly as target_ids.reshape(-1) or whatever was in the original en.md code. Yes, the original en.md says "targets_flat = target_ids.reshape(-1)". Let's keep it exactly matching the code in en.md.
max_logits = logits_flat.max(axis=-1, keepdims=True)
log_softmax = logits_flat - max_logits - np.log(
np.exp(logits_flat - max_logits).sum(axis=-1, keepdims=True)
)
loss = -log_softmax[np.arange(len(targets_flat)), targets_flat].mean()
total_loss += loss
num_windows += 1
return total_loss / max(num_windows, 1)
En un ajuste fino real, harías un seguimiento de esta métrica a lo largo del entrenamiento. Si la pérdida de texto sin procesar aumenta en más de un 10-15%, tu SFT es demasiado agresivo. Reduce la tasa de aprendizaje o disminuye el número de épocas.
Úsalo
Demostración del Pipeline de SFT Completo
if __name__ == "__main__":
np.random.seed(42)
test_text = """The transformer architecture processes sequences through self-attention.
Each layer applies multi-head attention followed by a feedforward network.
Residual connections and layer normalization stabilize deep networks.
The model learns to predict the next token given all previous tokens."""
print("=" * 70)
print("INSTRUCTION TUNING (SFT) DEMO")
print("=" * 70)
print()
model = MiniGPT(
vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=128, ff_dim=512
)
print(f"Model: {model.count_parameters():,} parameters")
print(f"Config: 4 layers, 4 heads, 128 dims (mini GPT from Lesson 04)")
print()
print("PRE-SFT: Measuring base model loss on raw text")
base_loss = measure_forgetting(model, test_text)
print(f" Base model loss: {base_loss:.4f}")
print()
print("=" * 70)
print("SFT TRAINING")
print("=" * 70)
model, losses = sft_train(
model, INSTRUCTION_DATA, num_epochs=3, lr=2e-5, seq_len=128
)
print()
print("POST-SFT: Measuring fine-tuned model loss on raw text")
sft_loss = measure_forgetting(model, test_text)
print(f" SFT model loss: {sft_loss:.4f}")
print(f" Change: {((sft_loss - base_loss) / base_loss * 100):+.1f}%")
if abs(sft_loss - base_loss) / base_loss < 0.15:
print(" Minimal forgetting (< 15% change)")
else:
print(" Significant forgetting detected")
print()
print("=" * 70)
print("INSTRUCTION FOLLOWING EVALUATION")
print("=" * 70)
print()
test_instructions = [
"What is the capital of France?",
"Name a programming language.",
"Define gravity.",
]
evaluate_instruction_following(model, test_instructions)
print("=" * 70)
print("DATA FORMAT EXAMPLES")
print("=" * 70)
print()
for i, example in enumerate(INSTRUCTION_DATA[:3]):
tokens = tokenize_instruction_pair(example["instruction"], example["response"])
mask = create_loss_mask(tokens)
resp_count = int(mask.sum())
total_count = len(tokens)
print(f" Example {i + 1}: {total_count} tokens, {resp_count} response tokens ({resp_count/total_count:.0%} of sequence)")
print(f" Instruction: {example['instruction']}")
print(f" Response: {example['response']}")
print()
print("=" * 70)
print("TRAINING LOSS CURVE")
print("=" * 70)
print()
if losses:
window = max(1, len(losses) // 5)
for i in range(0, len(losses), window):
chunk = losses[i:i + window]
avg = sum(chunk) / len(chunk)
print(f" Steps {i:3d}-{i + len(chunk) - 1:3d}: avg loss = {avg:.4f}")
Envíalo
Esta lección produce outputs/prompt-sft-data-curator.md -- un prompt que te ayuda a diseñar y curar conjuntos de datos de instrucciones para SFT. Dada una capacidad objetivo (generación de código, matemáticas, conversación), produce un plan de recolección de datos con especificaciones de formato, criterios de calidad y requisitos de diversidad.
Ejercicios
Agrega soporte para el prompt del sistema. Modifica
tokenize_instruction_pairpara aceptar un mensaje del sistema y colocarlo antes de la instrucción. Crea 5 ejemplos con diferentes prompts del sistema ("You are a poet", "You are a math tutor") y verifica que el modelo vea diferentes prompts del sistema durante el entrenamiento.Implementa la mezcla de datos. Crea una función que tome un dataset SFT y un corpus de texto sin procesar, luego produzca lotes de entrenamiento donde el 5% de los ejemplos sean de texto sin procesar (sin enmascarar) y el 95% sean pares de instrucciones (enmascarados). Ejecuta 3 épocas y compara las métricas de olvido con el entrenamiento SFT puro.
Construye un calificador de calidad de datos. Para cada par de instrucción-respuesta, calcula: (a) longitud de la respuesta en tokens, (b) relación instrucción-respuesta, (c) diversidad del vocabulario (tokens únicos / tokens totales). Filtra los ejemplos con longitud de respuesta < 10 tokens o diversidad < 0.3. Muestra cómo afecta el filtrado a la pérdida final.
Implementa el entrenamiento de conversaciones multiturno. Extiende la tokenización para manejar conversaciones de 3 turnos (user-assistant-user-assistant-user-assistant). La máscara de pérdida debe cubrir los tres turnos del asistente. Verifica que el modelo vea diferentes prompts del sistema durante el entrenamiento.
Compara tasas de aprendizaje. Entrena el mismo modelo tres veces con lr=1e-4, lr=2e-5 y lr=1e-6. Grafica las curvas de pérdida. La ejecución con 1e-4 debería mostrar un descenso inicial rápido pero una pérdida final más alta (sobreajuste). La ejecución con 1e-6 apenas debería moverse. La ejecución con 2e-5 debería ser el punto ideal.
Términos Clave
| Término | Lo que la gente dice | Lo que realmente significa |
|---|---|---|
| SFT | "Ajuste fino en conversaciones" | Ajuste Fino Supervisado (Supervised Fine-Tuning): continuar el entrenamiento en pares de (instrucción, respuesta) con la pérdida calculada solo en los tokens de respuesta |
| Ajuste de instrucciones | "Enseñar al modelo a seguir instrucciones" | Entrenar en pares explícitos de instrucción-respuesta para que el modelo base aprenda el patrón de conversación, no nuevos conocimientos |
| Enmascaramiento de pérdida | "Ignorar el prompt" | Establecer la pérdida en cero para los tokens de instrucción para que los gradientes solo fluyan a partir de las predicciones de los tokens de respuesta |
| ChatML | "Lenguaje de marcado de chat" | Un formato de token que utiliza delimitadores <|im_start|> y <|im_end|> para marcar los roles de los usuarios en los datos de conversación |
| Formato Alpaca | "El formato de Stanford" | Un formato JSON con campos de instrucción/input/output fields, utilizado para los 52K ejemplos generados por GPT-3.5 que costaron $600 |
| Olvido catastrófico | "El modelo se vuelve más tonto" | El ajuste fino destruye las capacidades preentrenadas porque las actualizaciones de gradiente sobrescreven el conocimiento general con patrones específicos de la tarea |
| Vinculación de pesos | "Embeddings compartidos" | Usar la misma matriz para los embeddings de tokens de entrada y la cabeza de predicción de salida, ahorrando parámetros y mejorando la coherencia |
| Plantilla de chat | "Cómo formateas el prompt" | La secuencia de tokens específica (marcadores de rol, delimitadores) que estructura una conversación para el modelo |
Lecturas Adicionales
- Ouyang et al., 2022 -- "Training language models to follow instructions with human feedback" (InstructGPT) -- el artículo que introdujo el ajuste de instrucciones + RLHF en OpenAI
- Taori et al., 2023 -- "Stanford Alpaca: An Instruction-following LLaMA Model" -- 52K ejemplos de instrucciones por $600, lo que demuestra que SFT funciona en datasets pequeños
- Touvron et al., 2023 -- "Llama 2: Open Foundation and Fine-Tuned Chat Models" -- el pipeline SFT + RLHF de Meta con 27K ejemplos de alta calidad
- Chiang et al., 2023 -- "Vicuna: An Open-Source Chatbot Impressing GPT-4" -- entrenamiento en 70K conversaciones de ShareGPT
- Zhou et al., 2023 -- "LIMA: Less Is More for Alignment" -- demostrando que 1,000 ejemplos cuidadosamente seleccionados pueden igualar a SFT en datasets mucho más grandes