Phase 10 - Lesson 03
Pipelines de Datos para Preentrenamiento
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
El modelo es un espejo. Refleja cualquier dato que le proporciones. Aliméntalo con basura, y reflejará basura con perfecta fluidez.
Tipo: Build Idiomas: Python Prerrequisitos: Fase 10, Lecciones 01-02 (Tokenizers, Building a Tokenizer) Tiempo: ~90 minutos
Objetivos de Aprendizaje
- Construir un pipeline de datos en streaming que tokeniza, fragmenta (chunks), mezcla (shuffles) y agrupa en lotes (batches) terabytes de texto sin cargar todo en memoria
- Implementar filtros de calidad de datos (deduplicación, detección de idioma, filtrado de contenido) utilizados en pipelines de preentrenamiento reales
- Crear secuencias de entrenamiento de longitud fija con máscaras de atención adecuadas y manejo de límites de documentos
- Analizar el rendimiento (profile) del procesamiento (throughput) del pipeline para garantizar que el dataloader mantenga el ritmo de la velocidad de entrenamiento de la GPU
El Problema
Tienes un tokenizador. Ahora necesitas datos.
No un conjunto de datos. No un archivo CSV. Terabytes de texto: limpios, deduplicados, filtrados por calidad, tokenizados en secuencias de longitud fija y servidos en lotes aleatorios lo suficientemente rápido como para que tu clúster de 8 GPUs nunca espere el siguiente lote.
La mayoría de las personas piensa que entrenar un LLM se trata de la arquitectura del modelo. No es así. Llama 3 utilizó 15.6 billones (trillions) de tokens. GPT-3 utilizó 300 mil millones. DeepSeek-V2 utilizó 8.1 billones. La arquitectura en los tres es aproximadamente la misma: bloques de transformers apilados con capas de atención y feedforward. La diferencia en la calidad del resultado proviene abrumadoramente de los datos.
El artículo de Chinchilla de DeepMind precisó esto. Para un presupuesto de computación (compute budget) dado, existe una proporción óptima de parámetros del modelo con respecto a los tokens de entrenamiento. Chinchilla demostró que la mayoría de los modelos en 2022 estaban drásticamente subentrenados: tenían demasiados parámetros para la cantidad de datos que veían. Un modelo de 70B de parámetros entrenado con 1.4 billones de tokens (óptimo según Chinchilla) superó a un modelo de 280B entrenado con 300 mil millones de tokens (Gopher).
Tu pipeline de datos determina si tu modelo aprende lenguaje o aprende ruido.
El Concepto
De Dónde Provienen los Datos
Cada gran modelo de lenguaje se entrena en una combinación de fuentes. La composición exacta es un secreto muy bien guardado por la mayoría de los laboratorios, pero sabemos lo suficiente como para entender las categorías.
| Fuente | Tamaño | Calidad | Usado Por |
|---|---|---|---|
| Common Crawl | ~250 TB bruto | Baja (requiere filtrado pesado) | GPT-3, Llama, la mayoría de los modelos abiertos |
| Wikipedia | ~20 GB | Alta | Todos los LLM principales |
| Código de GitHub | ~1 TB+ | Media (muchos duplicados, código muerto) | StarCoder, CodeLlama, DeepSeek-Coder |
| Libros (BookCorpus, Pile) | ~100 GB | Alta | GPT-2, GPT-3, modelos iniciales |
| Artículos académicos (arXiv, S2ORC) | ~100 GB | Alta para STEM | Llama, Galactica |
| StackOverflow, Reddit | ~100 GB | Media | Llama, Falcon |
| Web curada (C4, RefinedWeb) | ~5 TB | Media-Alta (prefiltrada) | T5, Falcon |
Llama 3 reveló su mezcla de datos: aproximadamente 50% de datos web, 25% de código, 13% de libros y artículos académicos, 8% de datos de matemáticas y 4% de datos web multilingües. El total fue de 15.6 billones (trillions) de tokens de fuentes que superan los 5 TB de texto bruto.
La proporción importa tanto como el tamaño total. Demasiados datos web y el modelo se convierte en un loro de Reddit. Muy poco código y no puede programar. Muy pocas matemáticas y falla en el razonamiento. Lograr esta mezcla correcta es una de las partes más difíciles del entrenamiento de un LLM, y no existe una fórmula: requiere experimentación y evaluación.
Limpieza de Datos
Los datos web sin procesar son sucios. Un volcado típico de Common Crawl contiene:
- Etiquetas HTML y JavaScript
- Cabeceras, pies de página y menús de navegación predeterminados (boilerplate)
- Páginas duplicadas (exactas y casi duplicadas)
- Spam generado por máquina
- Información de identificación personal (PII)
- Texto de baja calidad (listas de palabras clave, spam de SEO)
- Contenido que no es texto codificado como texto
Limpiar esto no es opcional. Es la diferencia entre un modelo que genera párrafos coherentes y uno que produce etiquetas HTML mezcladas con listas de productos.
graph TD
A[Texto sin Procesar] --> B[Eliminación de HTML]
B --> C[Deducción de Idioma]
B --> C[Detección de Idioma]
C --> D[Filtro de Calidad]
D --> E[Deduplicación]
E --> F[Eliminación de PII]
F --> G[Texto Limpio]
style A fill:#1a1a2e,stroke:#e94560,color:#fff
style B fill:#1a1a2e,stroke:#e94560,color:#fff
style C fill:#1a1a2e,stroke:#e94560,color:#fff
style D fill:#1a1a2e,stroke:#e94560,color:#fff
style E fill:#1a1a2e,stroke:#e94560,color:#fff
style F fill:#1a1a2e,stroke:#e94560,color:#fff
style G fill:#1a1a2e,stroke:#e94560,color:#fff
Cada paso elimina una categoría de ruido:
Eliminación de HTML (HTML stripping): Elimina todo el marcado. Conserva solo el contenido de texto visible. Bibliotecas como trafilatura o readability extraen el contenido del artículo mientras descartan la navegación, los anuncios y el boilerplate.
Detección de idioma: Utiliza el modelo de identificación de idioma de fastText (lid.176.bin) para clasificar cada documento. Filtra según tus idiomas de destino. Un documento clasificado como inglés con menos de 0.8 de confianza probablemente no sea inglés limpio.
Filtrado de calidad: Aquí es donde se pone interesante. RefinedWeb (el conjunto de datos detrás de Falcon) utiliza un filtro basado en perplejidad: entrena un pequeño modelo de lenguaje en Wikipedia, luego califica cada documento. Una perplejidad alta significa que el documento no se parece a Wikipedia; probablemente sea spam, listas de palabras clave o contenido generado por máquina. Los documentos con una perplejidad por encima de un umbral se eliminan.
Deduplicación: El paso de limpieza elogiado como de mayor impacto. Common Crawl contiene una cantidad enorme de páginas duplicadas: descargos de responsabilidad legal, avisos de cookies, términos de servicio. Entrenar con duplicados desperdicia computación y puede hacer que el modelo memorice y repita pasajes específicos palabra por palabra.
Eliminación de PII: Nombres, direcciones de correo electrónico, números de teléfono, números de seguro social/documentos de identidad. Detección basada en expresiones regulares (regex) para PII estructurada, modelos NER (Reconocimiento de Entidades Nombradas) para nombres en contexto.
Deduplicación con MinHash
La deduplicación exacta es fácil: calcular el hash de cada documento y eliminar los duplicados. Pero los casi duplicados son el problema real. Dos copias del mismo artículo de noticias con anuncios ligeramente diferentes a su alrededor son casi duplicados. El contenido es 95% idéntico, pero difieren byte por byte.
MinHash + Locality-Sensitive Hashing (LSH) resuelve esto de manera eficiente.
graph LR
A[Documento] --> B[Shingling]
B --> C[Firma MinHash]
C --> D[Buckets LSH]
D --> E[Pares Candidatos]
E --> F[Similitud de Jaccard]
F --> G[Conjunto Deduplicado]
style A fill:#1a1a2e,stroke:#e94560,color:#fff
style B fill:#1a1a2e,stroke:#e94560,color:#fff
style C fill:#1a1a2e,stroke:#e94560,color:#fff
style D fill:#1a1a2e,stroke:#e94560,color:#fff
style E fill:#1a1a2e,stroke:#e94560,color:#fff
style F fill:#1a1a2e,stroke:#e94560,color:#fff
style G fill:#1a1a2e,stroke:#e94560,color:#fff
La idea:
Shingling: Convertir cada documento en un conjunto de n-gramas (por ejemplo, 5-gramas de palabras o caracteres). "the quick brown fox" con shingles de 3 palabras se convierte en {"the quick brown", "quick brown fox"}.
MinHash: Para el conjunto de shingles de cada documento, calcular k valores de hash. Cada valor de hash es el hash mínimo de todos los shingles bajo una función de hash diferente. Esto crea una "firma" de tamaño fijo que aproxima la similitud de Jaccard entre dos documentos cualesquiera.
LSH: Agrupar documentos en buckets según las bandas de su firma MinHash. Los documentos en el mismo bucket son candidatos a casi duplicados. Esto evita comparar cada par: solo comparas los candidatos.
Verificar: Para cada par candidato, calcular la similitud exacta de Jaccard. Eliminar una copia si la similitud supera un umbral (normalmente 0.8).
El equipo de Llama informó haber eliminado aproximadamente el 38% de sus datos web mediante la deduplicación. Ese no es un número pequeño. Más de un tercio del Common Crawl es contenido duplicado o casi duplicado.
Empaquetado de Secuencias (Sequence Packing)
Tu modelo espera secuencias de entrada de longitud fija. Tus documentos tienen longitud variable. Algunos tienen 50 tokens. Algunos tienen 50,000 tokens.
Enfoque ingenuo: rellenar (pad) cada documento hasta la longitud máxima de la secuencia. Esto desperdicia una enorme cantidad de computación en tokens de relleno (padding) que no contribuyen en nada al aprendizaje.
Mejor enfoque: empaquetar múltiples documentos en una sola secuencia, separados por tokens de fin de secuencia (EOS). Una secuencia de 2048 tokens puede contener tres documentos cortos concatenados con tokens [EOS] entre ellos.
graph TD
subgraph Empaquetado Ingenuo
A1["Doc A (200 tokens)"] --> P1["[PAD] x 1848"]
A2["Doc B (500 tokens)"] --> P2["[PAD] x 1548"]
A3["Doc C (100 tokens)"] --> P3["[PAD] x 1948"]
end
subgraph Empaquetado Eficiente
B1["Doc A (200) | Doc B (500) | Doc C (100) | Doc D (400) | Doc E (848)"]
end
style A1 fill:#1a1a2e,stroke:#e94560,color:#fff
style A2 fill:#1a1a2e,stroke:#e94560,color:#fff
style A3 fill:#1a1a2e,stroke:#e94560,color:#fff
style P1 fill:#333,stroke:#666,color:#999
style P2 fill:#333,stroke:#666,color:#999
style P3 fill:#333,stroke:#666,color:#999
style B1 fill:#1a1a2e,stroke:#16c784,color:#fff
La máscara de atención debe configurarse correctamente. Los tokens del Documento A no deben prestar atención (attend) a los tokens del Documento B dentro de la misma secuencia empaquetada. Esto requiere una máscara de atención diagonal por bloques (block-diagonal attention mask).
Los documentos largos se truncan o se dividen en fragmentos (chunks) en los límites de la secuencia. El punto de división importa: dividir a mitad de una oración obliga al modelo a ver pensamientos incompletos. Algunos pipelines alinean las divisiones con los límites de párrafos u oraciones cuando es posible.
La Ley de Escala de Chinchilla (Chinchilla Scaling Law)
Para un presupuesto de computación fijo C (medido en FLOPs), el tamaño óptimo del modelo N y el tamaño del conjunto de datos D siguen la relación:
N_opt ~ C^0.5
D_opt ~ C^0.5
En la práctica, esto significa que debes escalar el tamaño del modelo y el tamaño del conjunto de datos de manera aproximadamente igual. Un modelo con 10 veces más parámetros necesita aproximadamente 10 veces más tokens de entrenamiento para alcanzar la misma pérdida (loss).
| Modelo | Parámetros | Tokens de Entrenamiento | ¿Óptimo según Chinchilla? |
|---|---|---|---|
| GPT-3 | 175B | 300B | No (subentrenado 3-4x) |
| Chinchilla | 70B | 1.4T | Sí (por diseño) |
| Llama 2 | 70B | 2T | Sobreentrenado (intencionalmente) |
| Llama 3 | 70B | 15T | Fuertemente sobreentrenado |
Llama 3 viola deliberadamente la ley de Chinchilla. Meta descubrió que sobreentrenar (overtraining) con más datos, mucho más allá de la proporción óptima de cómputo, produce mejores modelos para la inferencia. El costo adicional de entrenamiento se paga una vez, pero el modelo más pequeño es más barato de servir para siempre. Esto a veces se conoce como el enfoque de escala "óptimo para inferencia" (inference-optimal) y se ha convertido en el estándar de la industria desde 2024.
Build It
Paso 1: Text Cleaning
Elimina HTML, normaliza espacios en blanco, elimina el contenido que no sea texto. Utilizaremos un texto de dominio público (Project Gutenberg) como nuestro corpus pequeño.
import re
def clean_text(text):
text = re.sub(r"<[^>]+>", "", text)
text = re.sub(r"http\S+", "", text)
text = re.sub(r"[^\x20-\x7E\n]", "", text)
text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r" {2,}", " ", text)
return text.strip()
def quality_filter(text, min_words=50, max_ratio_caps=0.3, max_ratio_special=0.1):
words = text.split()
if len(words) < min_words:
return False
caps_ratio = sum(1 for w in words if w.isupper()) / len(words)
if caps_ratio > max_ratio_caps:
return False
special_chars = sum(1 for c in text if not c.isalnum() and not c.isspace())
if special_chars / max(len(text), 1) > max_ratio_special:
return False
return True
El filtro de calidad detecta spam de SEO (TODO EN MAYÚSCULAS), ruido generado por máquina (alta proporción de caracteres especiales) y páginas de borrador/esbozo (demasiado cortas). Estas tres comprobaciones por sí solas eliminan una cantidad sorprendente de basura de los rastreos web (web crawls).
Paso 2: Deduplicación MinHash
Implementa MinHash desde cero. No se requieren bibliotecas externas, solo hashlib.
import hashlib
from collections import defaultdict
def get_shingles(text, k=5):
words = text.lower().split()
if len(words) < k:
return set()
return {" ".join(words[i:i+k]) for i in range(len(words) - k + 1)}
def minhash_signature(shingles, num_hashes=128):
signature = []
for i in range(num_hashes):
min_hash = float("inf")
for shingle in shingles:
h = int(hashlib.sha256(f"{i}:{shingle}".encode()).hexdigest(), 16)
min_hash = min(min_hash, h)
signature.append(min_hash)
return signature
def lsh_buckets(signature, bands=16):
rows_per_band = len(signature) // bands
buckets = []
for b in range(bands):
start = b * rows_per_band
band_data = tuple(signature[start:start + rows_per_band])
bucket_hash = hashlib.md5(str(band_data).encode()).hexdigest()
buckets.append((b, bucket_hash))
return buckets
def deduplicate(documents, threshold=0.8, num_hashes=128, bands=16):
signatures = []
shingle_sets = []
for doc in documents:
shingles = get_shingles(doc)
shingle_sets.append(shingles)
signatures.append(minhash_signature(shingles, num_hashes))
bucket_map = defaultdict(list)
for doc_idx, sig in enumerate(signatures):
for band_id, bucket_hash in lsh_buckets(sig, bands):
bucket_map[(band_id, bucket_hash)].append(doc_idx)
duplicate_pairs = set()
for bucket_docs in bucket_map.values():
if len(bucket_docs) < 2:
continue
for i in range(len(bucket_docs)):
for j in range(i + 1, len(bucket_docs)):
duplicate_pairs.add((bucket_docs[i], bucket_docs[j]))
removed = set()
for i, j in duplicate_pairs:
if i in removed or j in removed:
continue
s1, s2 = shingle_sets[i], shingle_sets[j]
if not s1 or not s2:
continue
jaccard = len(s1 & s2) / len(s1 | s2)
if jaccard >= threshold:
removed.add(j)
return [doc for idx, doc in enumerate(documents) if idx not in removed], len(removed)
Los parámetros num_hashes=128 y bands=16 controlan el equilibrio entre precisión y exhaustividad (precision-recall tradeoff). Más hashes proporcionan estimaciones de similitud más precisas. Más bandas aumentan la exhaustividad (capturan más duplicados) a costa de más falsos positivos. Estos valores funcionan bien para el texto web típico.
Paso 3: Tokenizar y Empaquetar Secuencias
Toma el texto limpio y deduplicado, tokenízalo y empaquétalo en secuencias de longitud fija para el entrenamiento.
def tokenize_corpus(documents, tokenizer):
all_tokens = []
for doc in documents:
tokens = tokenizer.encode(doc)
all_tokens.extend(tokens)
all_tokens.append(tokenizer.eos_id)
return all_tokens
def pack_sequences(token_ids, seq_length, pad_id=0):
sequences = []
attention_masks = []
for i in range(0, len(token_ids), seq_length):
seq = token_ids[i:i + seq_length]
mask = [1] * len(seq)
if len(seq) < seq_length:
pad_count = seq_length - len(seq)
seq = seq + [pad_id] * pad_count
mask = mask + [0] * pad_count
sequences.append(seq)
attention_masks.append(mask)
return sequences, attention_masks
Paso 4: DataLoader para Entrenamiento
Genera (yield) lotes aleatorios de secuencias empaquetadas. Esto es lo que consume el bucle de entrenamiento.
import random
class PreTrainingDataLoader:
def __init__(self, sequences, attention_masks, batch_size, shuffle=True):
self.sequences = sequences
self.attention_masks = attention_masks
self.batch_size = batch_size
self.shuffle = shuffle
def __len__(self):
return (len(self.sequences) + self.batch_size - 1) // self.batch_size
def __iter__(self):
indices = list(range(len(self.sequences)))
if self.shuffle:
random.shuffle(indices)
for start in range(0, len(indices), self.batch_size):
batch_idx = indices[start:start + self.batch_size]
batch_seqs = [self.sequences[i] for i in batch_idx]
batch_masks = [self.attention_masks[i] for i in batch_idx]
yield batch_seqs, batch_masks
Paso 5: Estadísticas del Dataset
Calcula los números que importan: tokens totales, tokens únicos, tasa de compresión, distribución de la longitud de los documentos.
from collections import Counter
def compute_statistics(documents, token_ids, sequences, tokenizer_vocab_size):
total_chars = sum(len(d) for d in documents)
total_tokens = len(token_ids)
unique_tokens = len(set(token_ids))
compression_ratio = total_chars / total_tokens
doc_lengths = [len(d.split()) for d in documents]
avg_doc_length = sum(doc_lengths) / max(len(doc_lengths), 1)
max_doc_length = max(doc_lengths) if doc_lengths else 0
min_doc_length = min(doc_lengths) if doc_lengths else 0
token_counts = Counter(token_ids)
top_tokens = token_counts.most_common(10)
non_pad_tokens = sum(sum(1 for t in seq if t != 0) for seq in sequences)
total_positions = sum(len(seq) for seq in sequences)
utilization = non_pad_tokens / max(total_positions, 1)
stats = {
"total_documents": len(documents),
"total_characters": total_chars,
"total_tokens": total_tokens,
"unique_tokens": unique_tokens,
"vocab_utilization": unique_tokens / tokenizer_vocab_size,
"compression_ratio": compression_ratio,
"avg_doc_length_words": avg_doc_length,
"max_doc_length_words": max_doc_length,
"min_doc_length_words": min_doc_length,
"num_sequences": len(sequences),
"sequence_utilization": utilization,
"top_10_tokens": top_tokens,
}
return stats
La tasa de compresión te indica qué tan eficiente es el tokenizador en este corpus. El texto en inglés normalmente se comprime a aproximadamente 3-4 caracteres por token. Si ves 1.5 caracteres por token, tu tokenizador se está dividiendo de manera demasiado agresiva. Si ves más de 8, ha aprendido fusiones (merges) muy específicas del dominio.
La utilización de la secuencia te indica qué parte de tus secuencias empaquetadas son datos reales frente a relleno (padding). Menos del 90% significa que tu empaquetado es ineficiente: estás desperdiçando computación en tokens de relleno.
Úsalo
Comparar con HuggingFace Datasets
Carga el mismo corpus a través de la biblioteca datasets de HuggingFace y compara la velocidad del pipeline.
from datasets import load_dataset
from transformers import AutoTokenizer
ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")
import time
start = time.time()
tokenized = ds.map(
lambda x: tokenizer(x["text"], truncation=True, max_length=2048),
batched=True,
num_proc=4,
)
hf_time = time.time() - start
total_tokens = sum(len(t) for t in tokenized["input_ids"])
print(f"HuggingFace: {total_tokens:,} tokens in {hf_time:.2f}s ({total_tokens/hf_time:,.0f} tokens/sec)")
El pipeline de HuggingFace utiliza tokenizadores de Rust bajo el capó y procesamiento paralelo en 4 núcleos. Tu pipeline de Python puro será de 10 a 50 veces más lento. Esa brecha es la razón por la cual los equipos de producción utilizan tokenizadores compilados. El algoritmo es el mismo. La diferencia radica en el lenguaje de implementación.
Entrégalo (Ship It)
Esta lección produce un prompt para validar y depurar la calidad de los datos en pipelines de entrenamiento de LLM. Consulta outputs/prompt-data-quality-checker.md.
Ejercicios
- Fácil: Agrega detección de idioma al pipeline de limpieza utilizando una heurística simple (análisis del conjunto de caracteres). Filtra para incluir solo documentos en inglés y mide cuántos documentos se eliminan.
- Medio: Implementa la deduplicación exacta utilizando hashes SHA-256 junto con la casi duplicación de MinHash. Compara el número de duplicados capturados por cada método en un corpus extraído de la web (web-scraped corpus).
- Difícil: Construye un filtro de calidad basado en la perplejidad. Entrena un pequeño modelo de lenguaje de bigramas con texto de Wikipedia, califica cada documento por perplejidad y elimina el 20% inferior. Compara la calidad de la salida del modelo cuando se entrena con datos filtrados frente a no filtrados.
Términos Clave
| Término | Lo que la gente dice | Lo que realmente significa |
|---|---|---|
| Common Crawl | "El internet" | Una organización sin fines de lucro que rastrea la web mensualmente: ~250 TB brutos, el punto de partida para la mayoría de los datos de entrenamiento de LLM |
| MinHash | "Algún truque de hash" | Una técnica para estimar la similitud de Jaccard entre conjuntos usando firmas de tamaño fijo: permite la detección de casi duplicados a escala |
| LSH | "Locality-Sensitive Hashing" | Un método para agrupar elementos similares en el mismo bucket: reduce las comparaciones de pares de O(n^2) a casi lineal |
| Sequence packing | "Concatenar documentos" | Ajustar múltiples documentos en secuencias de longitud fija con máscaras de atención adecuadas: elimina el desperdicio de relleno (padding) |
| Chinchilla scaling | "Entrenar con más datos" | Para un presupuesto de computación fijo, el rendimiento óptimo requiere escalar el tamaño del modelo y los tokens de entrenamiento de manera aproximadamente igual |
| Fertility | "Tokens por palabra" | Número promedio de tokens por palabra: 1.3 para inglés en GPT-4, mayor para escrituras no latinas |
| Data mixing | "Elegir datos de entrenamiento" | La proporción de código vs texto vs matemáticas vs datos multilingues: no hay fórmula, requiere experimentación |
| Perplexity filter | "Puntuación de calidad" | Usar un modelo de lenguaje pequeño para calificar documentos: una perplejidad alta significa que el texto no se parece a los datos de referencia limpios |
| Deduplication | "Eliminar copias" | Eliminar documentos exactos y casi duplicados: normalmente elimina el 30-40% de los datos web sin procesar |
| Attention mask | "A qué tokens mirar" | Una máscara binaria que evita la atención a través de los límites del documento en secuencias empaquetadas |
Lecturas Adicionales
- Hoffmann et al., 2022 -- Training Compute-Optimal Large Language Models (Chinchilla) — el artículo que cambió nuestra forma de pensar sobre la escala de datos
- Penedo et al., 2023 -- The RefinedWeb Dataset for Falcon LLM — cómo filtrar Common Crawl para obtener alta calidad
- Touvron et al., 2023 -- Llama 2: Open Foundation and Fine-Tuned Chat Models — detalles del pipeline de datos para Llama 2
- Lee et al., 2022 -- Deduplicating Training Data Makes Language Models Better — por que la deduplicación importa más de lo que crees
- Broder, 1997 -- On the Resemblance and Containment of Documents — el artículo original de MinHash
- Meta, 2024 -- Llama 3 Technical Report — 15.6T de tokens, proporciones de mezcla de datos, pipeline de filtrado