Evaluacion y Benchmarks
La Brecha de Evaluacion
Entrenaste un modelo, la perdida bajo y genera texto que parece razonable. Pero es realmente mejor que el modelo base para tu caso de uso? Sin evaluacion rigurosa, estas adivinando. La mayoria de los proyectos de fine-tuning fallan no porque el entrenamiento fue malo, sino porque la evaluacion fue insuficiente — o inexistente. Esta leccion te da un kit completo de herramientas de evaluacion.
Metricas Automatizadas Especificas por Tarea
Diferentes tareas demandan diferentes metricas. Elige las que coincidan con tu caso de uso.
Coincidencia Exacta (Exact Match)
Para tareas con una sola respuesta correcta (clasificacion, extraccion de entidades, QA cerrado):
def exact_match(predictions, references):
correct = sum(1 for p, r in zip(predictions, references) if p.strip() == r.strip())
return correct / len(predictions)
# Ejemplo
predictions = ["positivo", "negativo", "neutral"]
references = ["positivo", "negativo", "positivo"]
print(f"Coincidencia Exacta: {exact_match(predictions, references):.2%}") # 66.67%
Puntuacion F1
Para tareas de clasificacion, especialmente con clases desbalanceadas:
from sklearn.metrics import classification_report
predictions = ["positivo", "negativo", "neutral", "positivo", "negativo"]
references = ["positivo", "positivo", "neutral", "positivo", "negativo"]
print(classification_report(references, predictions))
Puntuacion BLEU
Para tareas de traduccion y generacion de texto donde tienes salidas de referencia:
from nltk.translate.bleu_score import sentence_bleu, corpus_bleu
reference = [["el", "gato", "se", "sento", "en", "la", "alfombra"]]
candidate = ["el", "gato", "esta", "en", "la", "alfombra"]
score = sentence_bleu(reference, candidate)
print(f"BLEU: {score:.4f}")
Precaucion: BLEU mide la superposicion de n-gramas con una referencia. Es util para traduccion pero enganoso para generacion abierta donde existen multiples salidas validas.
Puntuacion ROUGE
Para tareas de resumen, midiendo la superposicion entre resumenes generados y de referencia:
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"], use_stemmer=True)
reference = "El modelo fue fine-tuned en documentos legales para analisis de contratos."
generated = "Se realizo fine-tuning en documentos legales para analizar contratos."
scores = scorer.score(reference, generated)
for metric, score in scores.items():
print(f"{metric}: Precision={score.precision:.3f}, Recall={score.recall:.3f}, F1={score.fmeasure:.3f}")
Evaluacion Humana
Las metricas automatizadas no pueden capturar todo. Para calidad subjetiva (utilidad, coherencia, seguridad), la evaluacion humana es esencial.
Protocolo de Comparacion Ciega
El metodo de evaluacion humana mas confiable: muestra a los evaluadores las salidas del modelo base y el fine-tuned lado a lado, sin revelar cual es cual.
import random
import json
def create_blind_evaluation_set(test_prompts, base_outputs, finetuned_outputs):
"""Crear un conjunto de evaluacion ciega aleatorizado."""
evaluation_pairs = []
for i, prompt in enumerate(test_prompts):
# Asignar posiciones A/B aleatoriamente
if random.random() > 0.5:
pair = {
"id": i,
"prompt": prompt,
"response_a": base_outputs[i],
"response_b": finetuned_outputs[i],
"mapping": {"a": "base", "b": "finetuned"}
}
else:
pair = {
"id": i,
"prompt": prompt,
"response_a": finetuned_outputs[i],
"response_b": base_outputs[i],
"mapping": {"a": "finetuned", "b": "base"}
}
evaluation_pairs.append(pair)
return evaluation_pairs
Rubrica de Evaluacion
Define criterios especificos para los evaluadores:
- Precision (1-5): Es correcta la informacion?
- Relevancia (1-5): Aborda la respuesta al prompt?
- Cumplimiento de formato (1-5): Sigue la salida el formato requerido?
- Fluidez (1-5): Es el lenguaje natural y bien escrito?
- Preferencia general: Que respuesta es mejor? (A / B / Empate)
Consejo: Usa al menos 3 evaluadores por ejemplo y mide la concordancia entre anotadores. Si los evaluadores no concuerdan significativamente, tu rubrica necesita aclaracion.
Evaluacion LLM-como-Juez
Usar un LLM poderoso (GPT-4o, Claude) para evaluar salidas es mas rapido y barato que la evaluacion humana, mientras correlaciona bien con las preferencias humanas.
Puntuacion de Salida Unica
import openai
client = openai.OpenAI()
def llm_judge_score(prompt, response, criteria):
"""Puntuar una respuesta del modelo en escala 1-10 usando un juez LLM."""
judge_prompt = f"""Eres un evaluador experto. Puntua la siguiente respuesta de IA
en una escala de 1-10 basandote en estos criterios:
{criteria}
Prompt del Usuario: {prompt}
Respuesta de la IA: {response}
Proporciona tu puntuacion como un objeto JSON con claves "score" (entero 1-10)
y "reasoning" (explicacion breve).
"""
result = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": judge_prompt}],
response_format={"type": "json_object"},
temperature=0,
)
return json.loads(result.choices[0].message.content)
# Ejemplo de uso
score = llm_judge_score(
prompt="Explica el concepto de LoRA en terminos simples",
response="LoRA es una manera de ensenarle a una IA nuevos trucos sin reentrenar todo...",
criteria="Precision, claridad, completitud y simplificacion apropiada para una audiencia general."
)
print(f"Puntuacion: {score['score']}/10 - {score['reasoning']}")
Comparacion por Pares
Mas confiable que la puntuacion absoluta — pide al juez que compare dos salidas:
def llm_judge_compare(prompt, response_a, response_b, criteria):
"""Comparar dos respuestas y seleccionar la mejor."""
judge_prompt = f"""Eres un evaluador experto. Compara estas dos respuestas de IA
y determina cual es mejor basandote en: {criteria}
Prompt del Usuario: {prompt}
Respuesta A: {response_a}
Respuesta B: {response_b}
Responde con JSON: {{"winner": "A" o "B" o "tie", "reasoning": "explicacion breve"}}
"""
result = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": judge_prompt}],
response_format={"type": "json_object"},
temperature=0,
)
return json.loads(result.choices[0].message.content)
Importante: Los jueces LLM tienen sesgos conocidos — tienden a preferir respuestas mas largas, respuestas con viñetas y la primera respuesta en un par (sesgo de posicion). Mitiga el sesgo de posicion ejecutando cada comparacion dos veces con posiciones intercambiadas.
Contaminacion de Benchmarks
Un riesgo critico: si tus datos de entrenamiento contienen ejemplos de benchmarks comunes, tus puntuaciones de evaluacion estaran infladas.
Como sucede:
- Los datos sinteticos generados por GPT-4 pueden contener preguntas de benchmarks parafraseadas
- Los datos raspados de la web pueden incluir datasets de benchmarks
- Incluso la contaminacion indirecta (entrenar con posts de blog que discuten preguntas de benchmarks) puede inflar puntuaciones
Como prevenirlo:
- Usa un conjunto de evaluacion personalizado que no existia antes de tu proyecto
- Ejecuta verificaciones de superposicion de n-gramas entre tus datos de entrenamiento y el conjunto de evaluacion
- Reporta tanto las puntuaciones de benchmarks estandar como las de evaluacion personalizada
def check_contamination(train_data, eval_data, n=8):
"""Verificar superposicion de n-gramas entre conjuntos de entrenamiento y evaluacion."""
from collections import Counter
def get_ngrams(text, n):
words = text.lower().split()
return set(tuple(words[i:i+n]) for i in range(len(words) - n + 1))
train_ngrams = set()
for example in train_data:
text = example.get("instruction", "") + " " + example.get("output", "")
train_ngrams.update(get_ngrams(text, n))
contaminated = []
for i, example in enumerate(eval_data):
text = example.get("instruction", "") + " " + example.get("output", "")
eval_ngrams = get_ngrams(text, n)
overlap = eval_ngrams & train_ngrams
if overlap:
contaminated.append(i)
print(f"Potencialmente contaminados: {len(contaminated)}/{len(eval_data)} ejemplos de evaluacion")
return contaminated
Construyendo Datasets de Evaluacion Personalizados
El mejor conjunto de evaluacion es uno que construyes especificamente para tu caso de uso:
- Define 5-10 categorias de capacidad en las que tu modelo debe sobresalir
- Escribe 10-20 prompts de prueba por categoria cubriendo dificultad facil, media y dificil
- Crea salidas de referencia (respuestas ideales) para comparacion automatizada
- Incluye ejemplos adversariales que prueben casos extremos y modos de fallo
- Manten el conjunto de evaluacion completamente separado de los datos de entrenamiento — nunca lo uses para otro proposito que no sea evaluacion
Benchmarks Estandar
Para comparacion mas amplia con otros modelos:
- MT-Bench: 80 preguntas multi-turno en 8 categorias. Usa GPT-4 como juez. Bueno para modelos conversacionales.
- AlpacaEval: 805 instrucciones evaluadas por GPT-4. Mide la capacidad general de seguimiento de instrucciones.
- MMLU: Preguntas de opcion multiple en 57 temas. Prueba conocimiento factual.
- HumanEval / MBPP: Benchmarks de generacion de codigo. Esenciales si tu modelo genera codigo.
Flujo de Trabajo Practico de Evaluacion
def full_evaluation(model, tokenizer, test_set, base_model=None):
"""Pipeline de evaluacion completo."""
results = {"automated": {}, "llm_judge": {}, "comparison": {}}
# 1. Generar salidas
predictions = []
for example in test_set:
output = generate(model, tokenizer, example["prompt"])
predictions.append(output)
# 2. Metricas automatizadas (si existen salidas de referencia)
if "reference" in test_set[0]:
references = [ex["reference"] for ex in test_set]
results["automated"]["exact_match"] = exact_match(predictions, references)
# 3. Puntuacion LLM-como-juez
scores = []
for example, pred in zip(test_set, predictions):
score = llm_judge_score(example["prompt"], pred, "precision, utilidad, formato")
scores.append(score["score"])
results["llm_judge"]["mean_score"] = sum(scores) / len(scores)
# 4. Comparacion con modelo base (si esta disponible)
if base_model:
base_predictions = [generate(base_model, tokenizer, ex["prompt"]) for ex in test_set]
wins = 0
for example, ft_pred, base_pred in zip(test_set, predictions, base_predictions):
result = llm_judge_compare(example["prompt"], ft_pred, base_pred, "calidad general")
if result["winner"] == "A":
wins += 1
results["comparison"]["win_rate"] = wins / len(test_set)
return results
En la proxima leccion, cubriremos como fusionar tu adaptador LoRA, exportarlo a formatos de produccion y prepararlo para el despliegue.