Preparando Datos de Entrenamiento
De Datos Limpios a Formato Listo para Entrenamiento
Has recopilado y limpiado tus datos. Ahora necesitas formatearlos para que los frameworks de entrenamiento puedan consumirlos. Diferentes herramientas esperan diferentes esquemas JSONL, diferentes modelos usan diferentes plantillas de conversacion, y pequenos errores de formato pueden degradar silenciosamente la calidad del entrenamiento. Esta leccion te da codigo probado en batalla para formatear datos correctamente a la primera.
Formatos JSONL por Framework
Hugging Face (SFTTrainer)
El SFTTrainer de la biblioteca trl de Hugging Face espera datos en uno de dos formatos:
Formato 1: Conversacional (recomendado)
{"messages": [{"role": "system", "content": "Eres un asistente util."}, {"role": "user", "content": "Que es LoRA?"}, {"role": "assistant", "content": "LoRA (Low-Rank Adaptation) es..."}]}
{"messages": [{"role": "user", "content": "Explica el fine-tuning"}, {"role": "assistant", "content": "El fine-tuning es el proceso de..."}]}
Formato 2: Campo de texto (para texto pre-formateado)
{"text": "<|system|>\nEres un asistente util.\n<|user|>\nQue es LoRA?\n<|assistant|>\nLoRA es..."}
Axolotl
Axolotl soporta multiples formatos a traves de su configuracion YAML. El mas comun:
{"conversations": [{"from": "system", "value": "Eres un asistente util."}, {"from": "human", "value": "Que es LoRA?"}, {"from": "gpt", "value": "LoRA es..."}]}
Nota los diferentes nombres de clave: from/value en lugar de role/content, y human/gpt en lugar de user/assistant.
Unsloth
Unsloth funciona con el formato conversacional estandar de Hugging Face pero tambien tiene su propio camino optimizado:
{"conversations": [{"role": "system", "content": "Eres un asistente util."}, {"role": "user", "content": "Que es LoRA?"}, {"role": "assistant", "content": "LoRA es..."}]}
Plantillas de Conversacion
Diferentes modelos esperan diferentes tokens especiales para envolver los mensajes. Usar la plantilla equivocada es uno de los bugs de fine-tuning mas comunes — el modelo entrena bien pero produce salida confusa porque fue entrenado con tokens no coincidentes.
ChatML (Qwen, Mistral-Instruct)
<|im_start|>system
Eres un asistente util.<|im_end|>
<|im_start|>user
Que es LoRA?<|im_end|>
<|im_start|>assistant
LoRA es...<|im_end|>
Plantilla Llama 3
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
Eres un asistente util.<|eot_id|><|start_header_id|>user<|end_header_id|>
Que es LoRA?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
LoRA es...<|eot_id|>
Plantilla Alpaca (Legacy)
Below is an instruction that describes a task. Write a response that appropriately completes the request.
### Instruction:
Que es LoRA?
### Response:
LoRA es...
Consejo critico: Siempre usa el metodo apply_chat_template integrado del tokenizador cuando sea posible. Maneja todos los tokens especiales automaticamente:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
messages = [
{"role": "system", "content": "Eres un asistente util."},
{"role": "user", "content": "Que es LoRA?"},
{"role": "assistant", "content": "LoRA es un metodo de fine-tuning eficiente en parametros."}
]
# Esto maneja todos los tokens especiales correctamente
formatted = tokenizer.apply_chat_template(messages, tokenize=False)
print(formatted)
Tecnicas de Aumentacion de Datos
Cuando tu dataset es pequeno, la aumentacion puede incrementar la diversidad efectiva sin recopilar nuevos datos.
Parafraseando Instrucciones
Reescribe el lado de la instruccion (entrada) mientras mantienes la salida igual. Esto ensena al modelo que diferentes formulaciones deben producir la misma salida.
import openai
client = openai.OpenAI()
def paraphrase_instruction(original_instruction):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": "Reescribe la siguiente instruccion para decir lo mismo "
"de manera diferente. Mantén el significado identico pero "
"cambia las palabras, estructura y fraseo. Devuelve SOLO "
"la instruccion reescrita."
}, {
"role": "user",
"content": original_instruction
}],
temperature=0.8
)
return response.choices[0].message.content
# Aumentar cada ejemplo con 2-3 parafraseos
augmented_dataset = []
for example in original_dataset:
augmented_dataset.append(example) # Mantener original
for _ in range(2):
new_instruction = paraphrase_instruction(example["instruction"])
augmented_dataset.append({
"instruction": new_instruction,
"output": example["output"]
})
Agregando Variaciones de System Prompt
Si tu modelo se usara con diferentes system prompts en produccion, incluye variaciones en el entrenamiento:
system_prompts = [
"Eres un asistente legal util.",
"Eres un experto en derecho contractual. Proporciona asesoramiento claro y accionable.",
"Como IA legal, analiza lo siguiente con precision y claridad.",
]
augmented = []
for example in dataset:
for sys_prompt in system_prompts:
augmented.append({
"messages": [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": example["user"]},
{"role": "assistant", "content": example["assistant"]}
]
})
Generacion de Datos Sinteticos a Escala
Para datasets sinteticos mas grandes, usa un pipeline estructurado:
import json
import random
from pathlib import Path
def generate_training_batch(seed_examples, num_generate, output_file):
"""Genera un lote de ejemplos de entrenamiento sinteticos."""
generated = []
for i in range(num_generate):
# Elegir un ejemplo semilla aleatorio como referencia de estilo
seed = random.choice(seed_examples)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": (
"Genera un NUEVO ejemplo de entrenamiento similar en estilo y "
"complejidad a la referencia de abajo, pero con contenido "
"completamente diferente. El ejemplo debe ser realista y "
"diverso.\n\n"
f"Referencia:\n"
f"Usuario: {seed['user']}\n"
f"Asistente: {seed['assistant']}\n\n"
"Devuelve JSON con claves 'user' y 'assistant'."
)
}],
response_format={"type": "json_object"},
temperature=1.0
)
try:
example = json.loads(response.choices[0].message.content)
generated.append(example)
except json.JSONDecodeError:
continue # Omitir salidas malformadas
# Escribir a JSONL
with open(output_file, "w") as f:
for ex in generated:
f.write(json.dumps(ex) + "\n")
return generated
Divisiones Train/Validacion/Test
Nunca entrenes con todos tus datos. Necesitas conjuntos reservados para evaluacion.
import random
from collections import defaultdict
def stratified_split(dataset, train_ratio=0.85, val_ratio=0.10, test_ratio=0.05):
"""Divide el dataset manteniendo la distribucion de categorias si existe."""
random.shuffle(dataset)
n = len(dataset)
train_end = int(n * train_ratio)
val_end = train_end + int(n * val_ratio)
train = dataset[:train_end]
val = dataset[train_end:val_end]
test = dataset[val_end:]
print(f"Train: {len(train)}, Validacion: {len(val)}, Test: {len(test)}")
return train, val, test
train_data, val_data, test_data = stratified_split(full_dataset)
# Guardar cada division
for split_name, split_data in [("train", train_data), ("val", val_data), ("test", test_data)]:
with open(f"data/{split_name}.jsonl", "w") as f:
for example in split_data:
f.write(json.dumps(example) + "\n")
Guias para divisiones:
- Entrenamiento (85%): Los datos de los que el modelo aprende
- Validacion (10%): Usado durante entrenamiento para detectar sobreajuste
- Test (5%): Nunca visto durante entrenamiento. Usado solo para evaluacion final
Para datasets pequenos (menos de 500 ejemplos), usa 90/10 train/val y cura manualmente un conjunto de test separado de 20-30 ejemplos de alta calidad.
Consideraciones de Tokenizacion
La tokenizacion afecta al entrenamiento mas de lo que la mayoria de la gente cree. Puntos clave:
Longitud de Secuencia
La mayoria de las ejecuciones de fine-tuning usan una longitud maxima de secuencia (por ejemplo, 2048 o 4096 tokens). Los ejemplos que exceden esta longitud se truncan, potencialmente cortando la respuesta del modelo. Siempre verifica la distribucion de longitud de tokens de tu dataset:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
lengths = []
for example in dataset:
text = tokenizer.apply_chat_template(example["messages"], tokenize=False)
tokens = tokenizer.encode(text)
lengths.append(len(tokens))
print(f"Estadisticas de longitud de tokens:")
print(f" Min: {min(lengths)}, Max: {max(lengths)}, Media: {sum(lengths)/len(lengths):.0f}")
print(f" Ejemplos > 2048 tokens: {sum(1 for l in lengths if l > 2048)}")
print(f" Ejemplos > 4096 tokens: {sum(1 for l in lengths if l > 4096)}")
Padding y Packing
Dos estrategias para manejar secuencias de longitud variable:
- Padding: Rellena secuencias mas cortas hasta la longitud maxima del batch. Simple pero desperdicia computo en tokens de relleno.
- Packing: Concatena multiples ejemplos cortos en una sola secuencia. Mas eficiente pero requiere implementacion cuidadosa para evitar contaminacion cruzada entre ejemplos.
La mayoria de los frameworks de entrenamiento modernos manejan esto automaticamente. La opcion packing=True de SFTTrainer habilita el empaquetado de secuencias.
Pipeline de Formateo Completo
Aqui hay un script completo que toma datos en bruto y produce archivos listos para entrenamiento:
import json
import random
from pathlib import Path
from transformers import AutoTokenizer
def format_dataset(input_file, output_dir, model_name, max_seq_length=2048):
"""Pipeline completo: cargar, formatear, validar, dividir y guardar."""
tokenizer = AutoTokenizer.from_pretrained(model_name)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Cargar datos en bruto
with open(input_file) as f:
raw_data = [json.loads(line) for line in f]
# Formatear y validar
formatted = []
skipped = 0
for example in raw_data:
messages = example["messages"]
text = tokenizer.apply_chat_template(messages, tokenize=False)
token_count = len(tokenizer.encode(text))
if token_count > max_seq_length:
skipped += 1
continue
formatted.append(example)
print(f"Mantenidos {len(formatted)}/{len(raw_data)} ejemplos ({skipped} demasiado largos)")
# Dividir
random.shuffle(formatted)
train_end = int(len(formatted) * 0.9)
train_data = formatted[:train_end]
val_data = formatted[train_end:]
# Guardar
for name, data in [("train", train_data), ("val", val_data)]:
path = output_dir / f"{name}.jsonl"
with open(path, "w") as f:
for ex in data:
f.write(json.dumps(ex) + "\n")
print(f"Guardados {len(data)} ejemplos en {path}")
format_dataset("raw_data.jsonl", "prepared_data/", "meta-llama/Llama-3.1-8B-Instruct")
En la proxima leccion, profundizamos en la mecanica de LoRA y QLoRA — entender exactamente como funcionan te ayudara a configurarlos de manera optima para tu caso de uso.