English · Español
01 — Embeddings y bi-encoders¶
Un bi-encoder es un modelo que mapea texto → vector. Si dos textos son similares semánticamente, sus vectores deben tener un producto-punto alto. Esta sección deriva cómo se entrena (contrastive loss), por qué los cross-encoders son más precisos pero más caros, y cuándo elegir cada uno.
Bi-encoders: la idea básica¶
Un bi-encoder es una red neuronal E: text → ℝ^d que mapea cualquier texto (una frase, un párrafo, un chunk) a un vector de dimensión fija. La promesa:
||E(text)||₂está acotado (típicamente normalizado a la unidad).- Similitud semántica ≈ similitud vectorial. Si
text_Aytext_Bsignifican cosas similares, entoncesE(text_A) · E(text_B) ≈ 1. Si significan cosas no relacionadas,E(text_A) · E(text_B) ≈ 0.
El pipeline de recuperación (retrieval) queda:
- Offline: para cada documento
dde la KB, calcularE(d)y guardarlo. - Online: para la consulta
q, calcularE(q)y encontrar los top-k documentos porE(q) · E(d_i).
El "bi-" en bi-encoder se refiere a dos codificaciones independientes: E(q) y E(d) se calculan independientemente, y luego se comparan. Compara con los cross-encoders más abajo.
Cómo se entrenan los bi-encoders: contrastive loss¶
Un bi-encoder es típicamente un transformer. El objetivo de entrenamiento: contrastar pares positivos frente a pares negativos.
Dado:
- Par positivo (q, d^+): consulta q y un documento conocido relevante d^+.
- Pares negativos (q, d^-_1), …, (q, d^-_k): la misma consulta con documentos irrelevantes.
Pérdida: InfoNCE (contrastiva):
τ es una temperatura, típicamente 0.05–0.1. La pérdida empuja el producto-punto del par positivo hacia arriba y los productos-punto de los pares negativos hacia abajo.
Mecánicamente idéntico a la clasificación softmax: el par positivo es la "clase correcta" y los pares negativos son "clases erróneas". El gradiente fluye por E(q) y E(d) — el mismo encoder, aplicado dos veces.
In-batch negatives¶
Por eficiencia, el entrenamiento usa in-batch negatives: en un batch de B pares (consulta, positivo), los otros B-1 positivos sirven como negativos. Esto evita necesitar un paso separado de muestreo de negativos.
Para nosotros (Fase 29) esto es conceptual — usamos un sentence-transformers/all-MiniLM-L6-v2 preentrenado (22M params, en inglés) y no entrenamos nuestro propio bi-encoder. Pero entender la pérdida explica por qué el espacio de embeddings se comporta como lo hace.
Por qué funcionan los bi-encoders para recuperación¶
Dos razones:
- Vectorizable. Una vez entrenado,
E(d)para todos los documentos de la KB puede precalcularse una vez. En tiempo de consulta: una pasada de embedding + un producto-punto sobre la KB. Escala a miles de millones de docs. - Distillation. Los bi-encoders modernos se destilan desde cross-encoders o jueces LLM. El profesor proporciona triplas (consulta, doc, label) de alta calidad; el bi-encoder aprende a aproximar las puntuaciones de similitud del profesor en el espacio de embeddings.
El all-MiniLM-L6-v2 de sentence-transformers es el ejemplo canónico: 22M params, destilado de MS-MARCO y varios datasets de QA. Dimensión de embedding 384. Rápido en CPU (~1000 frases/seg).
Cross-encoders: la alternativa de peso pesado¶
Un cross-encoder toma un par (query, doc) y produce una única puntuación de relevancia:
Mecánicamente: concatenas q y d con un token [SEP], lo pasas por un transformer, tomas la salida [CLS], proyectas a escalar.
- Más preciso que los bi-encoders. Los tokens de consulta y doc pueden atenderse mutuamente; el modelo puede resolver "qué doc realmente responde esta consulta" con atención completa.
- Mucho más caro. Por consulta: una pasada del transformer por cada candidato. Para top-1000 candidatos: 1000 pasadas. No es vectorizable a través de docs.
Así que no puedes usar un cross-encoder directamente sobre un corpus grande. Lo usas como reranker — primero recuperas los top-k con un bi-encoder, luego rerankeas los top-k con un cross-encoder.
El pipeline de dos etapas¶
RAG estándar de producción:
- Etapa de recall: el bi-encoder recupera los top-100 de la KB completa. Barato.
- Etapa de precisión: el cross-encoder rerankea los top-100. Caro pero acotado.
El bi-encoder optimiza para recall: queremos el doc gold en los top-100. El cross-encoder optimiza para precisión: queremos que el top-1 sea el doc gold.
Para la KB minúscula de la Fase 29 (~50 docs), el bi-encoder solo cubre la KB completa en top-50. El reranker cross-encoder aún ayuda a la precisión en el top — pero el valor marginal es menor. El Lab 03 lo mide.
Embedding de la consulta vs embedding del doc¶
Un punto sutil: la mayoría de bi-encoders usan el mismo encoder para consulta y doc (uso asimétrico del mismo modelo). El all-MiniLM-L6-v2 de sentence-transformers es uno de estos.
Algunos bi-encoders usan dos encoders separados (llamados bi-encoders "asimétricos"): uno afinado para consultas cortas, otro para documentos más largos. ColBERT (Khattab & Zaharia, 2020) usa un encoder pero con representaciones multi-vector (por token en vez de por frase). Estos son más precisos pero más intensivos en memoria.
Para la Fase 29 usamos el enfoque estándar de mismo encoder. Los documentos de la KB son cortos (1-3 frases cada uno), comparables en longitud a las consultas.
Mean-pooling vs [CLS] vs otro¶
El encoder de un bi-encoder produce una secuencia de embeddings por token h_1, …, h_n. El vector final de dimensión fija es algún pooling de estos:
[CLS]pooling: usar el embedding del token[CLS]. Estándar en modelos derivados de BERT.- Mean pooling: promediar todos los embeddings por token (típicamente excluyendo el padding). Los modelos preentrenados de sentence-transformers usan esto.
- Max pooling, attention pooling, etc.: menos comunes.
La familia sentence-transformers usa mean-pooling. Nosotros lo seguimos.
Alternativa from-scratch: mean-pool de MiniGPT¶
Por motivos pedagógicos, el Lab 01 implementa un bi-encoder from-scratch usando la tabla de embeddings de MiniGPT:
Esto es mucho peor que all-MiniLM-L6-v2:
- La tabla de embeddings de MiniGPT no se entrenó para similitud de frases. Se entrenó para next-token prediction.
- El mean-pool ignora la posición y el contexto.
- La dimensión de embedding es minúscula (64).
Pero es gratis, no requiere modelo externo, e ilustra el concepto abstracto de "texto → vector". El Lab 01 compara los dos; sentence-transformers gana por ~30 puntos porcentuales de hit-rate@5.
Normalización¶
Para la similitud coseno, los embeddings deben estar normalizados a la unidad: ||E(x)||₂ = 1. Sentence-transformers lo hace por defecto.
Para similitud de producto-punto crudo, la normalización a la unidad hace que el producto-punto sea igual al coseno — útil para índices estilo FAISS que operan sobre producto-punto. Normaliza siempre tras el pooling; es barato.
La geometría del espacio de embedding¶
Los bi-encoders entrenados producen espacios de embedding con la propiedad:
- Clústeres temáticos: los docs sobre el mismo tema se agrupan.
- Geometría intra-clúster: docs con estructura de frase similar están cerca; docs de estructura diferente están lejos.
- Anisotropía: los transformers preentrenados tienden a producir embeddings que ocupan un cono estrecho en
ℝ^d. El entrenamiento de sentence-transformers arregla esto parcialmente con contrastive loss + whitening.
Para nuestra KB de la Fase 29, consultas como "past participle of eat?" deberían agruparse cerca de los chunks de reglas que contienen "eat" y "participle". La confusión inter-clúster es el modo de fallo — se observa en el Lab 01 cuando el corpus contiene reglas casi duplicadas (p. ej., para write y wrote).
Consideraciones de cache¶
Los embeddings de los docs de la KB se calculan una vez y se cachean. El embedding de la consulta se calcula por consulta — pero embedding de consulta es rápido (una pasada, ~50ms en CPU). La búsqueda FAISS-flat sobre 50 docs es ~1ms. Latencia total de recuperación: <100ms. El reranker añade otros ~200ms para 10 candidatos.
Para la KB minúscula de la Fase 29 esto está bien. Para corpora de 10M docs, FAISS-HNSW te lleva a <50ms de latencia.
Por qué usamos un modelo preentrenado¶
¿Podríamos entrenar un bi-encoder from-scratch sobre el corpus de gramática verbal? Sí. ¿Deberíamos? No:
- El corpus es minúsculo (~500 frases). No es suficiente para entrenar un bi-encoder from-scratch.
- El entrenamiento de contrastive loss requiere ejemplos negativos; construir negativos significativos para un dominio pequeño es difícil.
all-MiniLM-L6-v2se destiló de datasets enormes. La ganancia downstream del fine-tuning in-domain es pequeña para nuestra tarea.
Usamos el modelo preentrenado tal cual. Mencionado en §A8 (la lista de frameworks / tooling del addendum).
Problemas de drill¶
Soluciones al abrir la fase en solutions/01-embeddings-and-biencoders-ref.md.
- El bi-encoder produce un embedding de 384 dimensiones por chunk. Para 50 chunks, ¿cuántos bytes ocupa el store de embeddings de la KB (fp32)? ¿En MiB?
- Argumenta por qué el mean-pooling es mejor que el pooling
[CLS]para tareas a nivel de frase. (Pista: piensa en qué tokens cargan significado.) - Construye un par de frases de reglas gramaticales que sean semánticamente equivalentes pero léxicamente diferentes. ¿Qué similitud de frase debería ser alta; qué haría un bi-encoder pobre?
- Los cross-encoders puntúan pares
(q, d)por atención. Los bi-encoders embebenqydindependientemente. Argumenta qué puede "ver" el cross-encoder que un bi-encoder no puede. (Pista: interacciones a nivel de token.) - La pérdida InfoNCE tiene una temperatura
τ. Cuandoτ → 0, la pérdida se vuelve casi uniforme entre positivos y negativos. Cuandoτ → ∞, la pérdida se satura. ¿Cuál es el efecto cualitativo deτ = 0.05vsτ = 0.5?
Resumen en un párrafo¶
Un bi-encoder es un transformer que mapea texto a un vector de dimensión fija con la propiedad de que textos semánticamente similares producen vectores con producto-punto alto. El entrenamiento usa pérdida contrastiva InfoNCE con in-batch negatives. Una vez entrenado, el bi-encoder permite recuperación vectorizada: precalcula todos los embeddings de los docs de la KB una vez, haz producto-punto contra un embedding de consulta fresco por consulta. Los cross-encoders son más precisos pero requieren una pasada por par (q, d), así que se usan solo para rerankear los top-k de un bi-encoder. La Fase 29 usa un all-MiniLM-L6-v2 preentrenado para producción y un mean-pool de MiniGPT from-scratch para contraste pedagógico. El último es mucho peor pero ilustra el concepto.
Siguiente: theory/02-chunking-and-indexes.md.