Skip to content

English · Español

Lab 02 — Métricas de calibración y el slice adversarial

Objetivo: extender el harness para calcular ECE, Brier score, el diagrama de fiabilidad, y los scores del slice adversarial desglosados por categoría de trampa.

Tiempo estimado: 90-120 minutos.

Prerrequisito: Lab 01 hecho (el harness emite results.json con predicciones y confianzas por probe). data/eval/adversarial.jsonl tiene ≥ 20 probes adversariales curados a mano.


Lo que produces

Extensiones:

  • src/eval/calibration.py — helpers de ECE, Brier, diagrama de fiabilidad.
  • src/eval/adversarial.py — agregador del slice adversarial.
  • tests/eval/test_calibration.py — tests sintéticos sobre predicciones de calibración conocida y miscalibración conocida.

Artefactos nuevos (añadidos a experiments/20-eval-report/<checkpoint_name>/):

  • reliability.png — diagrama de fiabilidad (confianza-predicha en eje x, accuracy empírica en eje y, diagonal de referencia).
  • adversarial_by_category.csv — accuracy en cada categoría de trampa.
  • adversarial_by_category.png — gráfico de barras.
  • results.json extendido con ece, brier, adversarial.overall, adversarial.by_category.

TODOs

Bloque A — src/eval/calibration.py

import numpy as np

def ece(confidences: np.ndarray,    # shape (N,), in [0,1]
        correct: np.ndarray,         # shape (N,), 0 or 1
        n_bins: int = 10) -> float:
    """Expected Calibration Error with equal-width bins."""
    bin_edges = np.linspace(0.0, 1.0, n_bins + 1)
    N = len(confidences)
    total = 0.0
    for m in range(n_bins):
        lo, hi = bin_edges[m], bin_edges[m+1]
        # include right edge in the last bin
        if m == n_bins - 1:
            mask = (confidences >= lo) & (confidences <= hi)
        else:
            mask = (confidences >= lo) & (confidences < hi)
        if mask.sum() == 0:
            continue
        bin_acc = correct[mask].mean()
        bin_conf = confidences[mask].mean()
        total += (mask.sum() / N) * abs(bin_acc - bin_conf)
    return float(total)


def brier(confidences: np.ndarray, correct: np.ndarray) -> float:
    """Binary Brier score: mean squared error between confidence and correctness."""
    return float(np.mean((confidences - correct) ** 2))


def brier_multiclass(probs: np.ndarray,   # shape (N, C)
                     labels: np.ndarray,  # shape (N,), integer in [0, C)
                     ) -> float:
    """Multi-class Brier: mean over samples of sum-of-squared-deviations across classes."""
    N, C = probs.shape
    one_hot = np.zeros_like(probs)
    one_hot[np.arange(N), labels] = 1.0
    return float(np.mean(np.sum((probs - one_hot) ** 2, axis=1) / C))


def reliability_diagram(confidences: np.ndarray,
                        correct: np.ndarray,
                        n_bins: int = 10,
                        out_path: str = "reliability.png") -> None:
    """Plot bin-mean-confidence vs bin-mean-accuracy. Diagonal reference."""
    import matplotlib.pyplot as plt
    # ... compute per-bin stats, plot
  • ece devuelve 0 sobre un dataset sintético perfectamente calibrado.
  • brier devuelve 0 sobre predicciones perfectas (conf=1.0 para correct=1, conf=0.0 para correct=0).
  • reliability_diagram guarda un PNG con: gráfico de barras de conteos por bin (fondo), scatter de (conf, acc) por bin, y la diagonal y=x.

Bloque B — src/eval/adversarial.py

def aggregate_adversarial(probes, predictions) -> dict:
    """Returns:
       {
         "overall": {"n": int, "correct": int, "accuracy": float, "wilson": (lo, hi)},
         "by_category": {
            "over_regularization": {...},
            "wrong_person_agreement": {...},
            "wrong_tense_marker": {...},
            "auxiliary_mismatch": {...},
            "en_es_mismatch": {...},
            "plural_or_oos": {...},
         }
       }
    """
    ...
  • Las categorías se derivan de probe.reason_code (la etiqueta de trampa).
  • Las celdas con n < 3 se reportan pero se marcan con low_sample: true.

Bloque C — extender run_eval (del Lab 01)

Tras el bucle de clasificación de probes, también:

adv_probes = load_probes(adversarial_path)
adv_results = classify_all(model, tokenizer, adv_probes)
results["adversarial"] = aggregate_adversarial(adv_probes, adv_results)

confidences = np.array([r.confidence for r in core_results])
correct = np.array([1 if r.predicted == p.expected else 0
                    for r, p in zip(core_results, core_probes)])
results["ece"] = ece(confidences, correct, n_bins=10)
results["brier"] = brier(confidences, correct)
reliability_diagram(confidences, correct, n_bins=10,
                    out_path=str(out_dir / "reliability.png"))

Bloque D — tests sintéticos en tests/eval/test_calibration.py

  1. test_ece_perfectly_calibrated:
    # 100 samples where confidence=0.7 and accuracy=0.7 exactly
    conf = np.full(100, 0.7)
    correct = np.array([1]*70 + [0]*30)
    assert ece(conf, correct) < 1e-9
    
  2. test_ece_overconfident:
    # 100 samples: confidence=0.95, accuracy=0.50
    conf = np.full(100, 0.95)
    correct = np.array([1]*50 + [0]*50)
    assert abs(ece(conf, correct) - 0.45) < 1e-9
    
  3. test_brier_perfect:
    conf = np.array([1.0, 1.0, 0.0, 0.0])
    correct = np.array([1, 1, 0, 0])
    assert brier(conf, correct) == 0.0
    
  4. test_brier_uniform_random:
    # Conf=0.5 always; gives Brier=0.25
    conf = np.full(100, 0.5)
    correct = np.random.RandomState(0).randint(0, 2, size=100).astype(float)
    assert abs(brier(conf, correct) - 0.25) < 0.01
    
  5. test_adversarial_category_aggregation — probes fixture con reason_codes conocidos y un patrón de predicción fijo; verifica las accuracies por categoría.

Bloque E — visualizaciones

reliability.png: - eje y: accuracy empírica en el bin. - eje x: confianza media en el bin. - Diagonal y=x. - Overlay de barras (semitransparente) mostrando conteos por bin en un eje y secundario o anotado. - Título: Reliability — checkpoint=<name>, ECE=<val>, N=<count>.

adversarial_by_category.png: - Gráfico de barras horizontal, una barra por categoría, longitud = accuracy, codificado por color según conteo de muestras. - Anota la barra con n=<count> y Wilson CI.

Restricciones

  • Las categorías coinciden con las etiquetas reason_code exactamente. No inventes nuevas categorías en tiempo de agregación; las categorías están definidas en theory/03 y los valores de reason_code de los probes deben encajar.
  • El diagrama de fiabilidad incluye bins vacíos. No los descartes; muéstralos como marcadores de altura cero para que la ausencia sea visible.
  • Los probes adversariales se excluyen del cálculo de ECE/Brier núcleo. De lo contrario los ejemplos trampa envenenan la estimación de calibración. El ECE adversarial se calcula por separado y se reporta en su propia línea.

Condiciones de parada

Hecho cuando:

  1. Los cinco tests de test_calibration.py pasan.
  2. experiments/20-eval-report/<name>/reliability.png existe y muestra una curva reconocible (puede ser cuasi-diagonal, puede ser sobre o subconfianza — eso es comportamiento del modelo).
  3. experiments/20-eval-report/<name>/adversarial_by_category.csv existe con una fila por categoría.
  4. results.json tiene ece, brier, adversarial.overall.accuracy, y adversarial.by_category.* poblados.

Trampas

  • ECE con n_bins > N/3: cada bin tiene ≤ 3 muestras; las estimaciones de ECE se vuelven ruido. Con N=60 probes núcleo y n_bins=10, cada bin tiene ~6 muestras — al filo. Si un bin acaba con 0 o 1 muestras, ese bin contribuye 0 al ECE pero el ruido del siguiente bin aumenta. Reporta n_bins y min_bin_count en el JSON.
  • La confianza es sobre el conjunto de candidatos, no sobre el vocabulario completo. Un probe de 4 candidatos con confidence=0.5 es mucho más débil que un probe de 2 candidatos con confidence=0.5. Si mezclas probes con distinto len(candidates), la calibración queda enturbiada. O bien normalizas (reescalando a "por encima del azar") o reportas la calibración separadamente por tamaño del conjunto de candidatos. Documenta la elección.
  • Las celdas de adversarial_by_category con n=1 o n=2 son casi sin significado individualmente pero contribuyen a la accuracy adversarial general. No las suprimas del overall; sí márcalas como low-sample.
  • Desacuerdo entre Brier y ECE. Miden cosas relacionadas pero distintas. ECE = 0 no implica Brier = 0 (un modelo puede estar perfectamente calibrado a 0.5 de confianza con 0.5 de accuracy — ECE=0 pero Brier=0.25). Reporta ambos.
  • Ejes del diagrama de fiabilidad invertidos. Convención: confianza en x, accuracy en y. Hazlo bien o todo lector leerá mal el gráfico.

Cuándo consultar solutions/

Después de que pasen todos los tests y los PNGs de reliability + adversarial estén producidos. La solución en solutions/02-calibration-ref.md (escrita al abrir la fase) discute cómo leer el diagrama de fiabilidad y qué significa en la práctica un modelo "sobreconfiado en adversariales".


Siguiente lab: lab/03-report-and-checkpoint-compare.md.