Fine-tuning — Módulo 13: entrenar tus propios embeddings y rerankers
Módulo final del Curso de Fine-tuning: el afinado de mayor retorno para quien opera un RAG. Por qué un embedding genérico falla con jerga propia, cómo construir el dataset de pares y minar negativos difíciles, MultipleNegativesRankingLoss y triplet loss, código con sentence-transformers v3, entrenamiento de un cross-encoder reranker y cómo medir la ganancia con recall@k, MRR y nDCG.
☰ Índice del contenido
- La tesis del módulo
- Por qué un embedding genérico falla en tu dominio
- El dataset: pares (query, documento positivo)
- Los negativos difíciles: el paso que decide todo
- Cómo se minan
- Las funciones de pérdida
- MultipleNegativesRankingLoss
- Triplet loss
- Código: entrenar el bi-encoder
- El cross-encoder reranker: más ganancia por hora
- Medir la ganancia
- Errores comunes
- La frase para llevarse
- Qué viene
- Material relacionado
El módulo 12 cerró el ciclo del fine-tuning de modelos generativos: entrenar, evaluar, servir, versionar y sostener en el tiempo. Este módulo es el bonus, y para una parte importante de quienes llegaron hasta acá va a ser el de mayor retorno práctico de los trece. Porque si operas un RAG y tu sistema “no encuentra las cosas”, el fine-tuning que necesitas no es el del LLM que redacta la respuesta: es el del modelo que decide qué documentos llegan al prompt. Entrenar un embedding o un reranker es más rápido, más barato y más medible que cualquier cosa que hicimos en los doce módulos anteriores, y casi nadie lo hace.
La tesis del módulo
El LLM no puede responder con un documento que el retriever nunca trajo, así que el techo de todo tu RAG lo pone el modelo de recuperación. Afinar ese modelo con unos miles de pares de tu dominio cuesta horas y mueve la métrica que define el techo. Afinar el generador, en cambio, pule el techo que ya tenías.
Esa asimetría no es una opinión: es la conclusión aritmética del módulo 10 del Curso de RAG. Si tu recall@10 es 0,65, el mejor generador imaginable responde bien, como máximo, el 65% de las preguntas. Ninguna cantidad de prompt engineering ni de fine-tuning del LLM sube ese número.
Por qué un embedding genérico falla en tu dominio
Un modelo de embedding —bi-encoder, en el vocabulario del módulo 4 del Curso de RAG— aprende a ubicar textos en un espacio donde la cercanía significa “hablan de lo mismo”. Ese “lo mismo” lo aprendió de su corpus de entrenamiento: Wikipedia, foros, noticias, páginas web. Lo que el modelo considera similar es lo que era similar ahí.
Cuatro formas concretas en que eso se rompe con un corpus especializado.
La jerga colisiona con el lenguaje general. En tu dominio, “provisión” y “reserva” pueden significar exactamente lo mismo, o exactamente cosas distintas, y el modelo genérico opina lo contrario. Un modelo entrenado en la web pone “cabo” (accidente geográfico) y “cabo” (grado militar) donde le parece, no donde tu corpus los necesita.
Los códigos y SKU no significan nada. “XR-40” y “XR-41” son casi idénticos para un tokenizador y para un embedding, y en tu catálogo son productos que no tienen nada que ver. Esto es en parte por qué existe BM25, pero un embedding afinado también aprende a separarlos.
La asimetría de registro. El usuario escribe “cuánto me sale arreglar la pantalla” y el documento dice “el valor de la prestación de servicio técnico de módulo de visualización asciende a”. Un modelo genérico ve dos textos que comparten pocas palabras y ninguna estructura. Un modelo afinado con tus propios pares aprende exactamente ese puente.
Los documentos son todos parecidos entre sí. En un corpus de contratos, todos los chunks hablan de cláusulas, partes y obligaciones. El modelo genérico los ubica a todos en la misma vecindad, apretados, y la distinción entre “cláusula de confidencialidad” y “cláusula de no competencia” queda por debajo de su resolución. Afinar estira ese espacio: separa lo que en tu dominio es distinto.
ESPACIO GENÉRICO ESPACIO AFINADO
(todo apretado) (estirado en tu dominio)
· · · ·
·· ·· · ·
query query
····· · · ·
· · · ·
el doc correcto está el doc correcto queda
a la misma distancia claramente más cerca
que otros 40
La ganancia típica que reporta la literatura para un fine-tuning de embeddings sobre un dominio especializado está en el rango de varios puntos a algunas decenas de puntos de recall, con la variación dependiendo sobre todo de cuán especializado es el corpus. Dicho con honestidad: en un corpus de lenguaje general la ganancia es marginal, y en un corpus con jerga fuerte es grande. Antes de invertir, mira tu corpus y pregúntate cuál de los dos es.
El dataset: pares (query, documento positivo)
La buena noticia del módulo: el dataset es mucho más barato que el de un LLM. No necesitas respuestas ideales escritas por expertos. Necesitas pares donde una consulta se asocia con un documento que la responde.
{"query": "¿cuánto me sale arreglar la pantalla?",
"positive": "El servicio técnico de módulo de visualización tiene un valor de 45.000 pesos e incluye repuesto original y 90 días de garantía."}
De dónde salen esos pares, ordenado por valor:
1. Logs de búsqueda con clics. Si tienes un buscador andando, cada clic es un par: la consulta que escribió el usuario y el documento que abrió. Es la fuente de mayor valor y la que casi nadie explota. Filtra los clics de rebote (abrió y volvió en dos segundos) y quédate con los que terminaron en una sesión larga o en una conversión.
2. Tu golden dataset de RAG. Si seguiste el módulo 10 del Curso de RAG, ya tienes preguntas con chunks_relevantes anotados. Ese archivo es un dataset de entrenamiento de embeddings, línea por línea. Con una advertencia crítica, que trato más abajo: si entrenas con él, ya no puedes evaluarte con él.
3. Pares sintéticos. Para cada chunk, le pides a un LLM que escriba las preguntas que ese chunk responde. Es el mismo código de generación sintética del módulo 10 de RAG y sirve igual acá. La diferencia de criterio: para entrenar toleras más ruido que para evaluar, así que la revisión humana puede ser por muestreo en vez de exhaustiva.
4. Pares estructurales del propio corpus. Título y cuerpo de un artículo, pregunta y respuesta de un FAQ, resumen y documento completo, ticket y su resolución. Son pares gratis que ya viven en tus datos y que capturan la relación semántica de tu dominio sin costo de anotación.
Órdenes de magnitud del volumen:
| Pares | Qué esperar |
|---|---|
| < 500 | Poca señal. Riesgo alto de sobreajuste |
| 1.000 – 5.000 | Zona de trabajo. Mejora ya notoria |
| 10.000 – 50.000 | Mejora consolidada, cubre bien el corpus |
| > 100.000 | Rendimientos decrecientes salvo corpus enorme |
Otra vez, mucho menos que un dataset de SFT. Con 2.000 pares y una hora de GPU ya tienes un modelo afinado que puedes comparar contra el genérico.
Los negativos difíciles: el paso que decide todo
Acá está el módulo. Si te llevas una sola cosa, que sea esta.
Un modelo de embedding aprende por contraste: acerca la query a su positivo y la aleja de los negativos. La calidad del aprendizaje depende enteramente de qué negativos le muestres.
Un negativo aleatorio es un documento cualquiera del corpus. La pregunta es sobre garantías y el negativo habla del horario de atención. Separarlos es trivial: el modelo genérico ya lo hacía, y entrenar con eso no le enseña nada nuevo.
Un negativo difícil (hard negative) es un documento que se parece mucho al positivo pero no responde la pregunta. La pregunta es “¿cuánto dura la garantía legal?” y el negativo es “la garantía del fabricante es opcional y la define cada marca”: mismo tema, mismo vocabulario, respuesta equivocada. Ese es el par que le enseña al modelo la distinción fina que tu dominio necesita.
CON NEGATIVOS ALEATORIOS
query ●
\___ positivo ●
● negativo (otro tema)
ya estaban lejos → gradiente ≈ 0 → no aprende
CON NEGATIVOS DIFÍCILES
query ●
\___ positivo ●
negativo ● ← casi encima
están juntos → gradiente grande → APRENDE a separarlos
Cómo se minan
La receta estándar: usa tu retriever actual para buscar el positivo, y toma como negativos los documentos que salieron arriba y no eran el correcto. Son, por construcción, los documentos que hoy te están confundiendo.
"""Minería de hard negatives con el modelo genérico.
La idea: buscar con el modelo actual y quedarse con los que salieron
arriba y NO eran el positivo. Son los errores que quieres corregir.
"""
from sentence_transformers import SentenceTransformer
from sentence_transformers.util import semantic_search
modelo = SentenceTransformer("intfloat/multilingual-e5-large")
corpus_textos = [c["texto"] for c in corpus]
emb_corpus = modelo.encode(
[f"passage: {t}" for t in corpus_textos], # prefijo E5 (RAG m4)
normalize_embeddings=True,
batch_size=64,
show_progress_bar=True,
)
def minar_negativos(pares: list[dict], n_neg: int = 4,
rango_min: int = 10, rango_max: int = 50,
umbral_max: float = 0.95) -> list[dict]:
"""Tres controles que hacen la diferencia:
- `rango_min`: NO tomes los primeros puestos. Los top-5 suelen ser
falsos negativos: documentos que sí responden y que nadie anotó.
Entrenar con ellos le enseña al modelo justo lo contrario.
- `rango_max`: más allá del top-50 los documentos ya no son
difíciles; son ruido y no aportan gradiente.
- `umbral_max`: descarta candidatos casi idénticos al positivo
(duplicados del corpus). Son falsos negativos garantizados.
"""
queries = [f"query: {p['query']}" for p in pares]
emb_q = modelo.encode(queries, normalize_embeddings=True,
batch_size=64)
resultados = semantic_search(emb_q, emb_corpus, top_k=rango_max)
salida = []
for par, hits in zip(pares, resultados):
pos_idx = corpus_textos.index(par["positive"])
negativos = []
for h in hits[rango_min:]:
if h["corpus_id"] == pos_idx:
continue
if h["score"] >= umbral_max: # casi duplicado
continue
negativos.append(corpus_textos[h["corpus_id"]])
if len(negativos) >= n_neg:
break
if negativos:
salida.append({**par, "negatives": negativos})
return salida
Ese rango_min=10 es el parámetro más importante del bloque y el más contraintuitivo. La tentación es tomar como negativos los documentos de las posiciones 2 a 5, que son los más difíciles de todos. El problema es que en la práctica muchos de ellos sí responden la pregunta y simplemente nadie los anotó como relevantes. Se llaman falsos negativos, y entrenar con ellos le enseña al modelo a alejar documentos correctos: es la forma más rápida de que el fine-tuning empeore el sistema. Empezar en la posición 10 es un compromiso razonable; si tienes anotación exhaustiva de relevancia, puedes bajar.
Cuidado: la minería de negativos se hace una vez por ronda de entrenamiento, no una vez y para siempre. Después de entrenar, tu modelo comete errores distintos, así que los negativos difíciles son otros. Volver a minar con el modelo recién entrenado y hacer una segunda ronda es una técnica estándar y suele dar una segunda mejora apreciable. Dos rondas es el punto donde normalmente se aplana.
Las funciones de pérdida
MultipleNegativesRankingLoss
Es la pérdida por defecto y la que hay que usar salvo razón en contra. Su nombre en la librería es largo; su idea es simple y elegante.
Dado un batch de N pares (query, positivo), para cada query trata su positivo como el correcto y los positivos de todas las demás queries del batch como negativos. No necesitas negativos explícitos: el batch los provee. Eso se llama in-batch negatives.
BATCH DE 4 PARES → matriz de similitud 4x4
pos_1 pos_2 pos_3 pos_4
query_1 [ + ] - - -
query_2 - [ + ] - -
query_3 - - [ + ] -
query_4 - - - [ + ]
La diagonal debe ganar. Cada query aprende contra
los otros 3 positivos del batch, gratis.
De ahí sale la consecuencia práctica más importante de esta sección: el batch size importa muchísimo. Con batch de 8, cada query se compara contra 7 negativos; con batch de 128, contra 127. Más negativos por paso es una señal de contraste mucho más rica, y la calidad del modelo entrenado sube de forma marcada con el batch. En esta tarea, a diferencia del SFT de LLM, el batch grande no es una optimización: es un hiperparámetro de calidad.
Y como los embeddings son modelos chicos —cientos de millones de parámetros, no miles de millones—, batches de 64 a 256 caben en una GPU normal. Si la VRAM aprieta, existe CachedMultipleNegativesRankingLoss, que simula un batch grande con menos memoria a costa de tiempo.
Si además le pasas negativos difíciles explícitos, la pérdida los usa junto con los in-batch. Eso es lo mejor de los dos mundos y es la configuración recomendada.
Triplet loss
La formulación clásica: (ancla, positivo, negativo), con el objetivo de que la distancia al positivo sea menor que la distancia al negativo por un margen definido. Es más antigua, requiere que tú provees los tripletes y en la práctica rinde menos que MNRL para retrieval de texto, porque usa un solo negativo por ejemplo en vez de todo el batch. Vale la pena conocerla porque aparece en literatura y en código heredado.
| Pérdida | Dato que necesita | Negativos por paso | Cuándo usarla |
|---|---|---|---|
| MultipleNegativesRankingLoss | (query, positivo) [+ negativos] | N-1 del batch + los explícitos | El default. Casi siempre |
| CachedMNRL | Igual | Igual, con batch simulado | Batch grande sin VRAM |
| TripletLoss | (ancla, positivo, negativo) | 1 | Código heredado, casos con margen definido |
| CoSENTLoss | (texto1, texto2, score continuo) | — | Tienes similitud graduada, no binaria |
Código: entrenar el bi-encoder
sentence-transformers v3 (2024) introdujo SentenceTransformerTrainer, con la misma forma que el Trainer de transformers que ya conoces del módulo 6.
"""Fine-tuning de un bi-encoder con sentence-transformers v3+.
El dataset se pasa como un `datasets.Dataset` cuyas COLUMNAS definen
el formato que espera la pérdida. Con MultipleNegativesRankingLoss:
- dos columnas -> (anchor, positive)
- tres o más -> (anchor, positive, negative_1, negative_2, ...)
Los NOMBRES de las columnas no importan; el ORDEN sí.
"""
from datasets import Dataset
from sentence_transformers import (
SentenceTransformer,
SentenceTransformerTrainer,
SentenceTransformerTrainingArguments,
)
from sentence_transformers.evaluation import InformationRetrievalEvaluator
from sentence_transformers.losses import MultipleNegativesRankingLoss
from sentence_transformers.training_args import BatchSamplers
MODELO_BASE = "intfloat/multilingual-e5-large" # español, 1024 dims
modelo = SentenceTransformer(MODELO_BASE)
# 1) Dataset: anchor, positive y los hard negatives minados.
# E5 espera los prefijos query:/passage: TAMBIÉN al entrenar.
pares = cargar_jsonl("pares_con_negativos.jsonl")
train = Dataset.from_dict({
"anchor": [f"query: {p['query']}" for p in pares],
"positive": [f"passage: {p['positive']}" for p in pares],
"negative_1": [f"passage: {p['negatives'][0]}" for p in pares],
"negative_2": [f"passage: {p['negatives'][1]}" for p in pares],
})
loss = MultipleNegativesRankingLoss(modelo)
args = SentenceTransformerTrainingArguments(
output_dir="./embeddings/soporte-v1",
# EL hiperparámetro de esta tarea: más batch = más in-batch
# negatives = mejor modelo. Súbelo hasta llenar la VRAM.
per_device_train_batch_size=128,
# LR bajo: partes de un modelo ya bueno y quieres ajustarlo,
# no reescribirlo. 2e-5 es el rango habitual.
learning_rate=2e-5,
warmup_ratio=0.1,
num_train_epochs=2, # 1-3. Sobreajusta rápido
# NO_DUPLICATES evita que dos textos casi iguales caigan en el
# mismo batch. Si eso pasa, uno actúa como negativo del otro
# siendo en realidad positivo: señal contradictoria.
batch_sampler=BatchSamplers.NO_DUPLICATES,
bf16=True,
eval_strategy="steps",
eval_steps=100,
save_steps=100,
load_best_model_at_end=True,
metric_for_best_model="eval_dominio_cosine_ndcg@10",
logging_steps=20,
)
# 2) El evaluador de retrieval, integrado al loop. Mide EXACTAMENTE
# lo que te importa (recall@k, MRR, nDCG) en vez de la loss.
evaluador = InformationRetrievalEvaluator(
queries=queries_eval, # {qid: texto}
corpus=corpus_eval, # {did: texto}
relevant_docs=relevantes, # {qid: set(did)}
name="dominio",
score_functions={"cosine": lambda a, b: a @ b.T},
)
trainer = SentenceTransformerTrainer(
model=modelo,
args=args,
train_dataset=train,
loss=loss,
evaluator=evaluador,
)
trainer.train()
modelo.save_pretrained("./embeddings/soporte-v1/final")
Tres detalles que cambian el resultado. per_device_train_batch_size=128 por lo ya explicado: es calidad, no velocidad. BatchSamplers.NO_DUPLICATES evita el escenario en que dos chunks casi idénticos caen en el mismo batch y se penalizan mutuamente como si fueran negativos. Y el InformationRetrievalEvaluator dentro del loop, que mide recall y nDCG durante el entrenamiento: la loss de MNRL baja de forma bonita y no te dice si el retrieval mejoró, así que hay que mirar la métrica que importa.
Ojo — la regla más importante del módulo, y la más violada. Si entrenas con tu golden dataset, no puedes evaluarte con él. Separa antes de empezar: un split de entrenamiento y un split de evaluación que jamás ve el trainer. Si los mezclas, tu recall@10 va a saltar a 0,98 y va a ser completamente ficticio, porque el modelo memorizó los pares exactos. Es la forma más eficiente de convencerte a ti mismo y a tu equipo de una mejora que no existe.
Y la consecuencia operativa que viene del módulo 4 del Curso de RAG: un modelo de embedding nuevo significa reindexar el corpus completo. Los vectores viejos y los nuevos viven en espacios distintos y no se pueden comparar. Presupuesta ese reindexado y planifica el corte antes de anunciar el cambio.
El cross-encoder reranker: más ganancia por hora
Y ahora la recomendación práctica que cierra el curso. Si tienes tiempo para entrenar un solo modelo de recuperación, entrena el reranker, no el bi-encoder.
Las razones son tres y son bastante decisivas:
No hay que reindexar. El bi-encoder afinado obliga a recalcular los vectores de todo el corpus, con el costo y la coordinación que eso implica. El reranker se enchufa al final del pipeline y no toca el índice. Puedes desplegarlo un martes.
Menos datos para más ganancia. Un cross-encoder ve la query y el documento juntos, con atención cruzada (RAG módulo 8), así que aprende la relación de relevancia con menos ejemplos que un bi-encoder que tiene que comprimir cada documento en un vector genérico.
Actúa sobre la métrica que el usuario siente. El bi-encoder sube el recall; el reranker sube el MRR y el nDCG, es decir, pone la respuesta correcta arriba. Si tu diagnóstico es “recall alto, MRR bajo”, el reranker es literalmente el arreglo.
El dataset es el mismo que ya minaste, con otra forma: en vez de agrupar por query, se aplana a pares con etiqueta.
"""Fine-tuning de un cross-encoder reranker (API de sentence-transformers v4, 2025).
El dataset se aplana: cada fila es (query, documento, label), con
label 1 para el positivo y 0 para cada hard negative. Los negativos
difíciles importan aún MÁS acá que en el bi-encoder.
"""
import torch
from datasets import Dataset
from sentence_transformers.cross_encoder import (
CrossEncoder,
CrossEncoderTrainer,
CrossEncoderTrainingArguments,
)
from sentence_transformers.cross_encoder.losses import BinaryCrossEntropyLoss
# num_labels=1: salida escalar de relevancia, no clasificación.
# m3 = multilingüe, sirve en español (ver tabla del módulo 8 de RAG).
modelo = CrossEncoder("BAAI/bge-reranker-v2-m3", num_labels=1)
filas = {"query": [], "documento": [], "label": []}
for p in cargar_jsonl("pares_con_negativos.jsonl"):
filas["query"].append(p["query"])
filas["documento"].append(p["positive"])
filas["label"].append(1.0)
for neg in p["negatives"]:
filas["query"].append(p["query"])
filas["documento"].append(neg)
filas["label"].append(0.0)
train = Dataset.from_dict(filas)
# pos_weight compensa el desbalance: con 4 negativos por positivo,
# la clase 0 es 4x más frecuente y domina el gradiente sin esto.
loss = BinaryCrossEntropyLoss(modelo, pos_weight=torch.tensor(4.0))
args = CrossEncoderTrainingArguments(
output_dir="./rerankers/soporte-v1",
per_device_train_batch_size=32, # acá el batch NO es calidad
learning_rate=2e-5,
num_train_epochs=1, # sobreajusta muy rápido
warmup_ratio=0.1,
bf16=True,
eval_strategy="steps",
eval_steps=100,
logging_steps=20,
)
trainer = CrossEncoderTrainer(
model=modelo,
args=args,
train_dataset=train,
loss=loss,
)
trainer.train()
modelo.save_pretrained("./rerankers/soporte-v1/final")
Dos diferencias con el bi-encoder que conviene notar. El batch size acá no es un hiperparámetro de calidad: no hay in-batch negatives, cada fila es independiente, así que el batch se elige por VRAM y velocidad como en cualquier entrenamiento normal. Y pos_weight compensa el desbalance de clases: con cuatro negativos por cada positivo, sin ese peso el modelo aprende que casi todo es irrelevante y puntúa bajo a todo.
DÓNDE ENTRA CADA MODELO AFINADO
pregunta
│
▼
┌──────────────────┐
│ bi-encoder │ afinarlo sube el RECALL
│ top-50 │ → obliga a REINDEXAR
└────────┬─────────┘
│
▼
┌──────────────────┐
│ cross-encoder │ afinarlo sube MRR y nDCG
│ reranker top-5 │ → NO toca el índice
└────────┬─────────┘
│
▼
┌──────────────────┐
│ LLM (módulos │ afinarlo mejora el FORMATO
│ 1 al 12) │ de la respuesta
└──────────────────┘
Medir la ganancia
Todo esto se evalúa con las métricas de recuperación del módulo 10 del Curso de RAG, sin invento nuevo: recall@k (¿está el documento correcto entre los k?), MRR (¿qué tan arriba salió el primero?) y nDCG@k (¿está bien ordenado, con relevancia graduada?).
El protocolo es el de siempre y no admite atajos: mismo split de evaluación, nunca visto por el entrenamiento, y una variable a la vez.
| Variante | recall@10 | MRR | nDCG@10 | Costo |
|---|---|---|---|---|
| BM25 solo (baseline honesto) | 0,71 | 0,52 | 0,58 | — |
| Embedding genérico | 0,79 | 0,61 | 0,66 | — |
| Híbrido (BM25 + genérico) | 0,86 | 0,67 | 0,72 | — |
| Híbrido + reranker genérico | 0,86 | 0,79 | 0,83 | Latencia |
| Híbrido + reranker AFINADO | 0,86 | 0,86 | 0,89 | 1 día de trabajo |
| Embedding afinado + reranker afinado | 0,92 | 0,89 | 0,92 | + reindexado |
Los números son ilustrativos de la forma de la relación, no un benchmark: los tuyos van a depender de tu corpus. Lo que sí es general y hay que leer con atención son dos patrones.
El reranker no sube el recall. Las filas 3, 4 y 5 tienen el mismo 0,86 de recall@10, porque el reranker solo reordena lo que el retriever ya trajo. Lo que sube es MRR y nDCG. Es exactamente la lección del módulo 8 del Curso de RAG: el techo del reranker es el recall del retriever.
El reranker afinado da la mejor relación esfuerzo/resultado. De la fila 4 a la 5 hay siete puntos de MRR por un día de trabajo y cero reindexado. Por eso es la recomendación por defecto.
Y el diagnóstico que se deduce de la tabla, que es la herramienta más útil que te llevas:
| Síntoma | Diagnóstico | Qué entrenar |
|---|---|---|
| Recall@10 bajo (< 0,8) | El documento no llega | Bi-encoder (o revisar chunking) |
| Recall alto, MRR bajo | Llega pero mal ordenado | Cross-encoder reranker |
| Recall y MRR altos, respuesta mala | El contexto llega bien | El LLM (módulos 1–12) o el prompt |
| Falla solo con códigos y SKU | Problema léxico | BM25 / híbrido, no fine-tuning |
Esa última fila es importante como freno: no todo se arregla entrenando. Si tu falla es que el usuario busca “XR-40” y no aparece, la respuesta es búsqueda léxica, y cuesta una tarde en vez de una semana.
Errores comunes
- Entrenar y evaluar con el mismo golden dataset. El recall se dispara y la mejora es ficticia: el modelo memorizó los pares. Separa los splits antes de escribir la primera línea de código de entrenamiento.
- Usar negativos aleatorios. Ya estaban lejos, así que el gradiente es casi nulo y el modelo no aprende nada útil. Los negativos que enseñan son los documentos que hoy te confunden, minados con tu propio retriever.
- Minar negativos desde las primeras posiciones. Los top-5 suelen contener falsos negativos —documentos que sí responden y que nadie anotó— y entrenar con ellos le enseña al modelo a alejar documentos correctos. Empieza alrededor de la posición 10.
- Batch chico con MultipleNegativesRankingLoss. Los negativos vienen del batch: con batch 8 tienes 7 negativos por query y con 128 tienes 127. Es el hiperparámetro que más mueve la calidad en esta tarea.
- Olvidar los prefijos query/passage al entrenar. Si el modelo base los usa (E5, BGE), tienen que estar en el entrenamiento, en la indexación y en la consulta. Ponerlos en unos sí y otros no es peor que no ponerlos.
- Afinar el bi-encoder y no reindexar. Los vectores viejos y los nuevos viven en espacios distintos. El sistema no falla con error: devuelve resultados sin sentido con puntajes que parecen razonables.
- Entrenar el bi-encoder cuando el síntoma era MRR bajo. Recall alto con MRR bajo es un problema de orden, y el arreglo es el reranker. Diagnostica antes de entrenar.
- Saltarse el baseline. Antes de entrenar nada, mide BM25 solo, el embedding genérico y el reranker genérico. A veces el reranker genérico ya te da lo que buscabas y te ahorras el proyecto completo.
La frase para llevarse
“El LLM redacta la respuesta, pero el retriever decide si esa respuesta es posible. Si tu RAG no encuentra, no afines al que escribe: afina al que busca.”
Qué viene
Nada: este es el final del Curso de Fine-tuning, y conviene mirar el recorrido completo.
Empezaste en el módulo 1 con la pregunta que evita buena parte de los proyectos innecesarios —si prompting, few-shot o RAG resuelven tu problema, no entrenes— y con el criterio para saber cuándo la respuesta era genuinamente entrenar. El módulo 2 instaló la tesis que sostiene todo el curso: el dataset es el modelo, y ninguna configuración salva datos malos. Los módulos 3 al 5 armaron la maquinaria —formato, loss masking, elección de base y hardware, LoRA y QLoRA— y el 6 y el 7 la pusieron a correr con un SFT completo y las curvas para saber leerlo. El módulo 8 fue la bisagra: sin evaluación, todo lo anterior es una corazonada cara.
El bloque avanzado agregó las preferencias y DPO, el camino del adapter al endpoint que atiende tráfico, el catálogo de técnicas para los problemas que el SFT no toca, la disciplina que hace sostenible un modelo en el tiempo, y este cierre sobre el modelo de recuperación.
Si tuviera que dejar tres frases del curso completo: la mayoría de los problemas no se resuelve con fine-tuning; de los que sí, casi todos se resuelven con mejores datos y no con mejores hiperparámetros; y todo lo que no mides, lo estás eligiendo al azar con pasos extra.
Dos lugares para seguir. El glosario de Fine-tuning reúne los términos de los trece módulos para consulta rápida. Y si este módulo te dejó con ganas de trabajar el lado de la recuperación, el Curso de RAG completo es el camino natural: doce módulos desde por qué existe RAG hasta ponerlo en producción, con la evaluación y las métricas que acabas de usar acá.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.