# Entrenar modelos con tus documentos: qué falla y qué construimos

Investigación de septiembre de 2026 sobre fuentes públicas (papers, documentación
oficial de los entrenadores, repositorios). Cada afirmación fuerte lleva su
fuente. Lo que no pudimos verificar directamente está marcado como tal.

## El problema, en una frase

Para afinar un modelo con tus propios documentos hay que construir un conjunto
de datos, y hoy eso significa o subir los PDF a un servicio en la nube —
justo lo que no puedes hacer con documentos que contienen datos de clientes — o
montar un pipeline de Python pensado para corpus web de terabytes.

Y aunque lo montes, hay cuatro errores que casi nadie evita, cada uno con
literatura que los documenta.

---

## 1. Duplicados: el modelo memoriza en vez de aprender

Lee et al., *Deduplicating Training Data Makes Language Models Better*
(ACL 2022, [arXiv:2107.06499](https://arxiv.org/abs/2107.06499)):

> «over 1% of the unprompted output of language models trained on these datasets
> is copied verbatim from the training data […] Deduplication allows us to train
> models that emit memorized text ten times less frequently and require fewer
> train steps to achieve the same or better accuracy.»

En C4 encontraron **una frase de 61 palabras repetida más de 60 000 veces**.
Carlini et al. ([arXiv:2202.07646](https://arxiv.org/abs/2202.07646)) muestran
además que la memorización crece log-linealmente con el número de veces que un
ejemplo aparece: **deduplicar no es solo calidad, es privacidad.**

Una carpeta de documentos reales es un caso extremo de esto. CCNet
([arXiv:1911.00359](https://ar5iv.labs.arxiv.org/html/1911.00359)) midió que los
párrafos duplicados eran **el 70 % del texto** en su corpus. En documentos de
empresa el porcentaje difícilmente es menor: la misma plantilla de contrato, el
mismo pie de página, el mismo informe en dos versiones.

**Qué parámetros usa la gente.** El consenso es *shingles de 5-gramas de
palabras*; el resto varía:

| Pipeline | Hashes | Umbral |
|---|---|---|
| The Pile | 10 | 0.5 |
| FineWeb | 112 (14 bandas × 8) | 0.75 |
| text-dedup (por defecto) | 240 | 0.7 |
| RefinedWeb / Lee et al. | 9 000 (20 × 450) | 0.8 |

RefinedWeb critica explícitamente los 10 hashes de The Pile: dieron «lower
deduplication rates and worse model performance»
([arXiv:2306.01116](https://arxiv.org/html/2306.01116v1)).

**Lo que hacemos:** `sirdas dedupe`, con MinHash + LSH sobre 5-gramas, 128
firmas y umbral 0.8 por defecto. El bandeado LSH **se deduce del umbral**: con
divisores potencia de dos, ajustar por el punto de cruce de la curva deja
huecos — a umbral 0.9 el bandeado «elegante» solo recupera el 81 % de los pares,
o sea que uno de cada cinco duplicados se pierde en silencio. Elegimos el
bandeado más barato que recupere el 95 % en el umbral pedido. La verificación
final es Jaccard exacto sobre los shingles, no la estimación de la firma.

---

## 2. Contaminación: la métrica deja de medir

Si un fragmento del conjunto de evaluación está en el de entrenamiento, la
puntuación no mide lo que el modelo aprendió sino lo que recordó.

GPT-3 ([arXiv:2005.14165](https://ar5iv.labs.arxiv.org/html/2005.14165),
apéndice C) estableció el método:

> «We attempted to remove text occurring in benchmarks from training data by
> searching for **13-gram overlaps** […] We ignored 13-grams that matched more
> than 10 training documents, as inspection showed the majority of these to
> contain common cultural phrases, legal boilerplate, or similar content.»

Esa segunda frase es la que importa en documentos de empresa, donde el
formulario domina.

Llama 3 ([arXiv:2407.21783](https://arxiv.org/html/2407.21783v3), §5.1.4) usa
hoy **8-gramas con un umbral de fracción de tokens**, y publicó cuánta
contaminación tienen los benchmarks conocidos: AGIEval 98 %, BIG-Bench Hard
95 %, HellaSwag 85 %, GSM8K 41 %. Tulu 3
([arXiv:2411.15124](https://arxiv.org/html/2411.15124v5)) encontró NuminaMath-TIR
contaminado un 11,3 % contra MATH, y Evol CodeAlpaca un 3,5 % contra HumanEval.

Y Lee et al. midieron que **el 4,6 % del conjunto de validación de C4 y el
14,35 % del de RealNews** tenían un casi-duplicado en el entrenamiento.

**Lo que hacemos:** `sirdas contamination`, con 13-gramas por defecto y la regla
de GPT-3 de descartar los n-gramas que salgan en más de 10 documentos de
referencia. Dejamos 13 y no 8 porque no tenemos un corpus web donde calibrar el
umbral de fracción de tokens, y en documentos corporativos un n corto se dispara
con el formulario; `--n 8` está ahí para quien quiera el criterio de Llama 3.
El informe **cita el fragmento literal que coincidió**, para que el hallazgo se
pueda comprobar en vez de creer.

**Límite conocido:** el solapamiento de n-gramas no ve una paráfrasis. Un
modelo entrenado con un benchmark reescrito puntúa alto y pasa el filtro
([arXiv:2311.04850](https://arxiv.org/pdf/2311.04850) — *no verificamos el
cuerpo del paper, solo el resumen*).

---

## 3. Fuga por troceado: el error silencioso

Este es el que más veces se comete y el que menos se nota, porque el resultado
es una métrica **mejor**.

Si troceas los documentos primero y repartes los trozos después, dos fragmentos
del mismo contrato acaban uno en entrenamiento y otro en prueba. El modelo ya
vio la plantilla, el vocabulario y el formato.

El principio está en la documentación de scikit-learn sobre validación cruzada:

> «we need to ensure that all the samples in the validation fold come from
> groups that are not represented at all in the paired training fold»
> — [scikit-learn, cross-validation](https://scikit-learn.org/stable/modules/cross_validation.html)

Kapoor y Narayanan, *Leakage and the Reproducibility Crisis in ML-based Science*
([arXiv:2207.07048](https://ar5iv.labs.arxiv.org/html/2207.07048)) lo catalogan
como fuga tipo L3.2 y contabilizan **329 artículos afectados en 17 campos**.
Un trabajo sobre ventanas deslizantes
([arXiv:2607.16493](https://arxiv.org/html/2607.16493v1)) cuantifica el efecto:
la estrategia de reparto ingenua «can inflate classification accuracy from a
genuine ~20–60 % to an artefactual 99.9 %», con los mismos datos y el mismo
modelo.

Hasta The Pile lo admite de sí mismo: «it is still possible that some documents
are duplicated across the train/validation/test splits»
([arXiv:2101.00027](https://ar5iv.labs.arxiv.org/html/2101.00027)).

**Lo que hacemos:** el reparto es **siempre por documento**, nunca por trozo.
Además es estable: el mismo hash de nombre de archivo y semilla da siempre el
mismo reparto, y **añadir un documento nuevo no mueve de partición a los que ya
estaban** — lo que permite volver a generar el conjunto sin invalidar una
evaluación anterior. Hay una prueba automática que lo comprueba.

El reparto por documento no basta por sí solo: dos documentos *distintos* pueden
ser casi iguales (dos versiones de un contrato). Por eso hay que pasar también
la deduplicación y la contaminación.

---

## 4. Datos personales que el modelo acaba recitando

Carlini et al., *Extracting Training Data from Large Language Models*
(USENIX Security 2021, [arXiv:2012.07805](https://arxiv.org/abs/2012.07805))
extrajeron de GPT-2 «hundreds of verbatim text sequences», incluidos «names,
phone numbers, and email addresses», **de secuencias que aparecían en un solo
documento** del corpus. Y «larger models are more vulnerable than smaller
models».

Qué hacen los pipelines públicos, que es menos de lo que parece:

- **Dolma** ([arXiv:2402.00159](https://arxiv.org/html/2402.00159v2)) se limita
  a «three kinds of PII that are detectable with high precision: email
  addresses, IP addresses and phone numbers», con regex. Política de densidad:
  hasta 5 apariciones sustituye; por encima, **descarta el documento entero**.
- **FineWeb** anonimiza solo correos e IP públicas.
- **Llama 3** elimina dominios enteros con mucha PII, no la PII en sí.
- **StarCoder** ([arXiv:2305.06161](https://ar5iv.labs.arxiv.org/html/2305.06161))
  entrenó un detector con 22 950 entidades anotadas a mano. Resultados por tipo:
  EMAIL 99 % de cobertura, NAME 89 %, y **ID genérico 53 % — tan malo que lo
  excluyeron del entrenamiento**.

Esa última cifra es la lección honesta: **nadie detecta identificadores
genéricos de forma fiable**, ni con anotadores humanos. Nuestra anonimización
es de reglas y también puede fallar; por eso el aviso está en el producto y no
en la letra pequeña.

**Lo que hacemos:** la anonimización va **activada por defecto** al construir el
dataset, y el resumen dice cuántos datos se sustituyeron y en cuántos registros.
Si la desactivas y hay datos personales, la herramienta avisa por stderr. A
diferencia de Dolma o datatrove, que sustituyen por un token fijo e
irreversible, la nuestra es reversible con el mapa.

---

## 5. Los nombres de campo, que es donde se pierde la tarde

Cada entrenador espera un esquema distinto y el error se ve tarde. Verificado
sobre documentación oficial:

**TRL** ([dataset_formats](https://huggingface.co/docs/trl/en/dataset_formats)):

```json
{"text": "The sky is blue."}
{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}
{"prompt": "The sky is", "completion": " blue."}
{"prompt": "...", "chosen": " blue.", "rejected": " green."}
```

Detalles que rompen cosas y no son obvios:

- **PRMTrainer usa plurales**: `completions` y `labels`.
- **OpenAI DPO no usa los nombres de TRL.** Usa
  `input` (un objeto que envuelve `messages`), `preferred_output` y
  `non_preferred_output`, y solo admite un turno
  ([docs](https://developers.openai.com/api/docs/guides/direct-preference-optimization)).
  Un exportador que asuma TRL produce archivos que fallan la validación.
- **OpenAI SFT** admite `weight: 0|1` por mensaje de asistente para no entrenar
  sobre alguno.
- **LLaMA-Factory** necesita además una entrada en `dataset_info.json` con
  `columns` y `tags`.
- **Axolotl**: `sharegpt` está deprecado; hoy es `type: chat_template` con
  `message_property_mappings`.
- **Anthropic** no tiene, que hayamos podido verificar, un formato propio de
  afinado documentado públicamente. El único oficial legible es el de AWS
  Bedrock para Claude 3 Haiku: `{"system": "...", "messages": [...]}`, mínimo 32
  registros.

**Lo que hacemos:** `sirdas dataset --format text|chat|prompt-completion`, con
los nombres de TRL, que es también lo que come Unsloth y —en el caso de
`messages`— OpenAI. No exportamos formatos de preferencias (DPO) porque
**construir un par elegido/rechazado exige generar dos respuestas, y eso
requiere un modelo**: romperíamos la arquitectura. Ver el apartado siguiente
para lo que sí se puede hacer sin modelo.

---

## 6. RL con recompensa verificable: lo que sí se puede hacer sin modelo

DeepSeek-R1 ([arXiv:2501.12948](https://arxiv.org/html/2501.12948v1), §2.2.2)
explica por qué esto importa:

> «for math problems with deterministic results, the model is required to
> provide the final answer in a specified format […] enabling reliable
> rule-based verification of correctness […] We **do not apply the outcome or
> process neural reward model** […] because we find that the neural reward model
> may suffer from **reward hacking** in the large-scale reinforcement learning
> process.»

Es decir: la recompensa comprobable por código no es una limitación nuestra, es
lo que los mejores resultados de razonamiento eligen a propósito.

**Qué forma tienen esos datos.** El conjunto de referencia de AI2,
[`allenai/RLVR-GSM-MATH-IF-Mixed-Constraints`](https://huggingface.co/datasets/allenai/RLVR-GSM-MATH-IF-Mixed-Constraints),
tiene `messages`, `ground_truth` y `dataset`. Y `GRPOTrainer`
([docs](https://huggingface.co/docs/trl/en/grpo_trainer)) solo exige la columna
`prompt`: **cualquier columna extra llega a la función de recompensa como
argumento con nombre**. El ejemplo oficial de la documentación compara
literalmente `c == gt`.

Conclusión: un dataset de RLVR es JSONL con `prompt` y `ground_truth`. No hace
falta ningún modelo para generarlo. La verificación vive en el entrenador.

**El hueco:** no encontramos ninguna herramienta que tome una tabla de un PDF y
emita esos pares. Y una tabla ya es un conjunto de hechos con respuesta única:
«¿cuánto costó el examen de laboratorio?» tiene una sola respuesta, está
impresa en la página, y comprobarla es comparar dos cadenas.

**Lo que hacemos:** `sirdas rl` genera tres tipos de pregunta desde las tablas —
consulta de celda, conteo de filas y extremo de columna — en JSONL con `prompt`
y `ground_truth`. Reglas de calidad:

- Si una clave de fila se repite, esa pregunta **no se emite**: tendría dos
  respuestas.
- Si hay empate en el máximo o el mínimo, tampoco. *Un par ambiguo castiga al
  modelo por acertar.*
- Los números se comparan como números: `1.120.000` y `1120000` son la misma
  respuesta. La notación colombiana y la inglesa se leen ambas.

Y `sirdas rl --reward-fn` imprime **la función de recompensa en Python**, con el
mismo criterio de comparación, lista para pegar en `GRPOTrainer`. Montar RLVR es
mitad datos y mitad verificador; entregamos los dos.

---

## Qué existe ya, y qué no

| Herramienta | Local | Dedup | Contaminación | PII | Exporta a entrenamiento |
|---|---|---|---|---|---|
| unstructured, Docling, marker, nougat | sí | no | no | no | no |
| LlamaParse | **no, nube** | no | no | no | no |
| datatrove (HF) | sí, Python | sí | sí | regex | no |
| dolma (AI2) | sí, Python+Rust | sí | sí | regex | no |
| NeMo Curator | sí, Python+CUDA | sí | sí | Presidio | no |
| text-dedup | sí, Python | sí | no | no | no |
| **Sırdaş** | **navegador / Node** | sí | sí | sí, reversible | sí |

Hay una línea divisoria nítida: los conversores de PDF terminan en Markdown, y
los curadores de corpus empiezan en JSONL. **El puente no existe** — y es
justo donde viven la procedencia, el reparto por documento y el rastro de
anonimización.

**Honestidad sobre la competencia:** [jsonlkit.com](https://jsonlkit.com/) ya
hace deduplicación MinHash/LSH, enmascarado de PII y detección de fugas **en el
navegador**, con validadores para OpenAI, Anthropic, Gemini y DPO. No podemos
decir que seamos los únicos que hacen esto sin subir archivos. Lo que no tiene
es **ingesta de PDF**: la cadena completa documento → trozos con procedencia →
dataset auditado es lo que no está ocupado.

Tampoco es cierto que las técnicas sean nuestras: MinHash, LSH, el
solapamiento de n-gramas y el reparto por grupos llevan años publicados. Lo
nuevo es que corran sobre tus PDF sin instalar Python y sin que el archivo salga
del equipo.

---

## Lo que NO vamos a construir

Generar pares de pregunta y respuesta «inteligentes» a partir de prosa, o pares
de preferencia para DPO. Las dos cosas exigen llamar a un modelo con el
contenido de tus documentos, que es exactamente lo que este producto existe para
evitar. Nos quedamos en lo verificable: tablas, estructura e higiene del corpus.

## Deduplicación de párrafo (CCNet)

CCNet mide que los párrafos duplicados eran **el 70 % del texto** de su corpus, y
su remedio es el más barato de todos: normalizar el párrafo, hashearlo y
quedarse con la primera aparición. La normalización que usan —minúsculas,
dígitos a cero, sin acentos ni puntuación— es deliberadamente agresiva: hace que
«Factura 1042» y «Factura 1043» cuenten como el mismo párrafo de plantilla, que
es exactamente lo que son.

En documentos de oficina el patrón es más nítido que en la web: el membrete, el
pie con «este documento es confidencial», la cláusula de tratamiento de datos
idéntica en cada contrato. En un corpus de prueba de tres documentos cortos, esa
plantilla ocupaba el 48 % del texto.

`sirdas boilerplate` lo **informa**: qué párrafos, cuántas veces, en qué
documentos. Quitarlos es un paso aparte y explícito (`--strip-boilerplate`),
porque descarta contenido, y descartar contenido del corpus de alguien sin que
lo vea es la clase de cosa que este producto no hace. Cuando se quitan, **se
conserva siempre la primera aparición**: el texto no desaparece del corpus, deja
de estar repetido.

Dos salvaguardas: los párrafos de menos de cinco palabras se ignoran, porque los
títulos, los números de página y las firmas se repiten por naturaleza y quitarlos
rompería la estructura; y por defecto solo cuenta lo que se repite **entre**
documentos, porque repetirse dentro de uno solo puede ser legítimo.

Una diferencia con CCNet, declarada: ellos usan los primeros 64 bits de SHA-1 y
aquí son dos FNV-1a de 32 bits con semillas distintas, para que sea síncrono y
sin dependencias en el navegador. A esta escala la probabilidad de colisión es
equivalente.
