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:
- Cubrir todas las categorías de inputs reales.
- Incluir casos edge (inputs raros pero importantes).
- Tener gold standard (anotación de experto cuando posible).
- Ser estable (mismo set entre runs comparables).
- 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.