Saltar al contenido
Lección 9 de 12

Evaluacion y Benchmarks

7 min read

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:

  1. Define 5-10 categorias de capacidad en las que tu modelo debe sobresalir
  2. Escribe 10-20 prompts de prueba por categoria cubriendo dificultad facil, media y dificil
  3. Crea salidas de referencia (respuestas ideales) para comparacion automatizada
  4. Incluye ejemplos adversariales que prueben casos extremos y modos de fallo
  5. 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.