Saltar a contenido

Datasets format para fine-tuning Gemma 4

Format JSONL esperado, cómo recolectar, balancear, validar.


Format estándar — OpenAI messages

{"messages": [{"role": "system", "content": "Eres un asistente legal..."}, {"role": "user", "content": "Revisa este contrato..."}, {"role": "assistant", "content": "He encontrado 3 cláusulas..."}]}
{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}

Reglas: - 1 línea = 1 ejemplo de entrenamiento. - messages es lista; debe terminar con role=assistant (lo que el modelo aprende a producir). - system opcional (recomendado para tareas con instrucciones consistentes). - Multi-turn permitido (>=3 messages alternando user/assistant). - UTF-8 sin BOM.


Ejemplo: revisor de contratos (LegalTech)

{"messages": [{"role": "system", "content": "Eres un asistente legal especializado en revisión de contratos de arrendamiento en España. Identificas cláusulas problemáticas según LAU (Ley de Arrendamientos Urbanos) y sugieres alternativas."}, {"role": "user", "content": "Analiza esta cláusula: 'El arrendatario no podrá subarrendar ni ceder el contrato bajo ningún concepto, siendo causa de resolución inmediata.'"}, {"role": "assistant", "content": "## Análisis\n\n**Cláusula identificada**: Prohibición absoluta de subarriendo y cesión.\n\n**Severidad**: 🟡 Media\n\n**Problema**: Excesivamente restrictiva. El art. 8 LAU permite subarriendo PARCIAL con consentimiento expreso del arrendador. Una prohibición ABSOLUTA es válida pero limita flexibilidad del arrendatario.\n\n**Sugerencia de redacción**:\n'El arrendatario podrá subarrendar parcialmente la vivienda con consentimiento expreso y escrito del arrendador. El precio del subarriendo no podrá exceder el del arrendamiento principal.'\n\n**Referencias**: Art. 8 LAU."}]}

Ejemplo: extracción estructurada (Doc Extraction API)

{"messages": [{"role": "system", "content": "Extraes datos de facturas españolas a JSON. NUNCA inventas datos; si un campo no aparece, usa null."}, {"role": "user", "content": "FACTURA F-2026-00123\nFecha emisión: 15/04/2026\nEmisor: Acme SL - B12345678\n...\nTotal: 605,00 €"}, {"role": "assistant", "content": "{\"numero\": \"F-2026-00123\", \"fecha_emision\": \"2026-04-15\", \"emisor\": {\"nombre\": \"Acme SL\", \"cif\": \"B12345678\"}, \"total\": 605.00, \"moneda\": \"EUR\"}"}]}

Tamaño recomendado

Tarea Min ejemplos Sweet spot Diminishing returns
Clasificación simple 200 1.000 5.000
Extracción estructurada 500 3.000 10.000
Generación con estilo específico 1.000 5.000 20.000
Q&A vertical (legal, médico) 2.000 10.000 50.000
Multi-step agentic 5.000 30.000 100.000

Cómo recolectar dataset

Opción 1: Synthetic data con modelo más grande

# Usa Gemini 3 / Claude / GPT-5 para generar ejemplos sintéticos
# que luego pulirás con humanos
from litellm import completion

prompts = [...]  # 500 prompts variados
synthetic_dataset = []

for p in prompts:
    response = completion(
        model="anthropic/claude-opus-4-7",
        messages=[
            {"role": "system", "content": "Eres un experto en {dominio}. Genera respuestas de alta calidad."},
            {"role": "user", "content": p}
        ]
    )
    synthetic_dataset.append({
        "messages": [
            {"role": "user", "content": p},
            {"role": "assistant", "content": response.choices[0].message.content}
        ]
    })

# Save
import json
with open("data/synthetic_train.jsonl", "w") as f:
    for ex in synthetic_dataset:
        f.write(json.dumps(ex, ensure_ascii=False) + "\n")

Pros: rápido (500 ejemplos en 1h). Cons: hereda biases del modelo "teacher". Mitigación: 80% sintético + 20% humano-anotado.

Opción 2: Anotación humana

Para tareas críticas (legal, médico), necesitas anotación de expertos.

Tools: - Label Studio (open source). - Argilla (open source, HF-friendly). - Doccano (open source, simple).

Costo: €30-100/h de tiempo de experto. 100 ejemplos / hora típico → €0.30-1 por ejemplo.

Opción 3: Logs de producción (con consentimiento)

Una vez tu producto está vivo y los usuarios consintieron: - Captura prompts + outputs reales. - Anota cuáles fueron "buenos" (👍 botón en UI) y "malos" (👎). - Usa solo los 👍 para fine-tune (DPO opcional con pairs 👍/👎).


Split train / val / eval

import json
import random

random.seed(42)

with open("data/all.jsonl") as f:
    examples = [json.loads(line) for line in f]

random.shuffle(examples)

n = len(examples)
train = examples[:int(0.8 * n)]
val = examples[int(0.8 * n):int(0.9 * n)]
eval_ = examples[int(0.9 * n):]

for name, data in [("train", train), ("valid", val), ("test", eval_)]:
    with open(f"data/{name}.jsonl", "w") as f:
        for ex in data:
            f.write(json.dumps(ex, ensure_ascii=False) + "\n")

print(f"Train: {len(train)}, Val: {len(val)}, Eval: {len(eval_)}")

Reglas: - Eval set debe ser idéntico entre runs (compara apples to apples). - Eval set NO se mezcla con train (data contamination = métricas falsas). - Para datasets pequeños (<1K), considera 5-fold cross-validation.


Validación del dataset (pre-training)

Antes de entrenar, valida:

import json

issues = []
with open("data/train.jsonl") as f:
    for i, line in enumerate(f):
        try:
            ex = json.loads(line)
        except json.JSONDecodeError as e:
            issues.append(f"Line {i}: invalid JSON ({e})")
            continue

        if "messages" not in ex:
            issues.append(f"Line {i}: missing 'messages'")
            continue

        msgs = ex["messages"]
        if not msgs or msgs[-1]["role"] != "assistant":
            issues.append(f"Line {i}: must end with assistant message")

        for m in msgs:
            if "role" not in m or "content" not in m:
                issues.append(f"Line {i}: malformed message")
            if not m.get("content", "").strip():
                issues.append(f"Line {i}: empty content")

if issues:
    print(f"❌ {len(issues)} issues found:")
    for issue in issues[:20]:
        print(f"  {issue}")
else:
    print("✅ Dataset valid")

Anti-patterns comunes

Antipattern Síntoma Fix
Dataset desbalanceado Modelo memoriza la clase dominante Stratified sampling o oversampling
Labels inconsistentes (2 anotadores discrepan) Eval F1 inflada Re-anotar; calcular Cohen's kappa
Ejemplos casi idénticos (data leakage) Train acc 99%, eval acc 60% Dedup con embeddings (>0.95 cosine = duplicado)
Sistemas prompts diferentes en train vs prod Modelo confundido en producción Mantén system prompt idéntico
Outputs demasiado largos OOM en entreno Trim a max 2000 tokens output
Multi-idioma mezclado sin balance Calidad pobre en idioma minoritario Stratified per idioma

Eval set diseño (crítico)

Tu eval set debe:

  1. Cubrir todas las categorías de inputs reales.
  2. Incluir casos edge (inputs raros pero importantes).
  3. Tener gold standard (anotación de experto cuando posible).
  4. Ser estable (mismo set entre runs comparables).
  5. Ser secreto (no en train).

Tamaño mínimo: - 50 ejemplos: solo gut-check. - 200 ejemplos: estadísticamente útil para detectar improvements >5%. - 1000+ ejemplos: detecta improvements <2%.


Métricas por tarea

Tarea Métrica primaria Secundarias
Clasificación F1 macro Precision, Recall, Accuracy
Extracción estructurada F1 por field + JSON validity rate Exact match
Generación libre BLEU / ROUGE-L Length, perplexity, human-rated
Q&A Exact match + F1 token-level Citation accuracy
Code generation Pass@1 (tests pass) Compilation rate
Agentic / tool use Task completion rate Steps per task

Implementa eval automatizado: corre eval después de cada fine-tune. Sin esto, no sabes si mejoraste.