Skip to content

English · Español

Lab 02 — Detección de drift (KL + PSI sobre la distribución de tokens de verbo)

Objetivo: implementar scripts/mlops/drift.py y demostrar que dispara ante un desplazamiento conocido de la distribución de tokens de verbo.

Tiempo estimado: 3–4 horas.

Prerequisito: lab 00 hecho; el corpus de verbos de la Fase 12 + su histograma de tokens de entrenamiento son accesibles vía DVC.


Lo que produces

experiments/38-drift-detection/ conteniendo:

  • inject.py — script que construye el desplazamiento sintético sobre la rejilla de verbos.
  • measure.py — calcula KL y PSI sobre distribuciones baseline y desplazada.
  • results.json — KL/PSI a niveles variables de perturbación, más grid-PSI por bucket.
  • sensitivity.png — KL y PSI vs magnitud de perturbación (matplotlib).
  • manifest.json.
  • README.md.

El escenario

Tienes:

  • \(P\) = el histograma de tokens de entrenamiento de la Fase 12 para el corpus de verbos (alrededor de 2.000 tokens tras BPE; conteos sumando unos pocos millones).
  • \(P_{\text{grid}}\) = la tabla de frecuencia de celdas (tiempo × persona) en tiempo de entrenamiento (5 × 3 = 15 celdas).
  • \(Q_0\) = una muestra limpia de tráfico en vivo desde la misma distribución (1.000 tokens muestreados de \(P\)).
  • \(Q_\rho\) = una muestra desplazada: 1.000 tokens, de los cuales una fracción \(\rho \in \{0, 0.05, 0.10, 0.15, 0.25, 0.50\}\) han sido reemplazados con tokens de un régimen de tiempo desplazado (p. ej., cuando el entrenamiento era pesado en presente simple, la muestra perturbada es pesada en participio pasado).

Calcula KL y PSI (tanto token-PSI como grid-PSI) para \(P\) vs cada \(Q_\rho\). Confirma que las métricas suben monotónicamente con \(\rho\) y cruzan los umbrales documentados (PSI 0.10, 0.25) en valores sensatos de \(\rho\).

TODOs

Bloque A — ensambla \(P\) y \(P_{\text{grid}}\)

  • dvc pull data/processed/train.jsonl.dvc para fetchear el corpus de la Fase 12.
  • Tokeniza con el tokenizer BPE de la Fase 11. Construye un vector de conteos de longitud \(|V|\) P_counts y normaliza a un vector de probabilidad P (con suavizado \(\alpha = 10^{-6}\) per theory/03).
  • Construye P_grid: una tabla de frecuencia de 5 × 3 = 15 celdas clasificando cada frase del corpus por su tiempo de verbo primario y persona. (Usa las etiquetas en el manifest del corpus — gen_corpus.py de la Fase 12 las emite.)
  • Persiste P como baseline_histogram.json y P_grid como baseline_grid.json en el directorio del experimento. Serán reutilizados por la Fase 39.

Bloque B — construye \(Q_\rho\)

  • Samplea 1.000 tokens de \(P\) → ese es \(Q_0\).
  • Construye una distribución OOD R sobre el mismo vocabulario, sesgada hacia tokens asociados con un tiempo desplazado (p. ej., sufijos de participio pasado -ed, -en, más los participios pasados irregulares gone, been, done, written, seen, come, eaten). Si la distribución de entrenamiento era pesada en presente simple, este R simula una afluencia repentina de queries en participio pasado.
  • Para cada \(\rho\), construye \(Q_\rho\): una muestra de 1.000 tokens donde cada posición se muestrea independientemente de \(P\) con probabilidad \(1-\rho\) y de R con probabilidad \(\rho\).
  • Similarmente construye un Q_grid_\rho por nivel de perturbación: un histograma de 15 celdas donde las celdas de participio pasado reciben masa extra \(\rho\).
  • Persiste cada \(Q_\rho\) como q_rho_<rho>.json.

Bloque C — calcula KL y PSI

  • Implementa kl_divergence(P, Q) en scripts/mlops/drift.py. Usa NumPy. Aplica suavizado \(\alpha = 10^{-6}\) a \(P\) y \(Q\).
  • Implementa psi(P, Q) en el mismo fichero. Usa el bucketing estratificado por frecuencia de theory/03: top 20 tokens en 5 buckets de 4, siguientes 200 en 5 buckets de 40, cola restante en 5 buckets de ~360. Eso es un token-PSI de 15 buckets para el corpus de verbos.
  • Implementa grid_psi(P_grid, Q_grid): PSI sobre las celdas (tiempo, persona). 15 celdas.
  • Para cada \(\rho\) en \(\{0, 0.05, 0.10, 0.15, 0.25, 0.50\}\): calcula kl = kl_divergence(P, Q_rho), psi = psi(P, Q_rho), grid_psi = grid_psi(P_grid, Q_grid_rho). Guarda a results.json.

Bloque D — plot e interpreta

  • Plotea KL, token-PSI y grid-PSI vs \(\rho\) en el mismo eje x, con los umbrales PSI (0.10, 0.25) como líneas horizontales.
  • Identifica (y documenta en README.md): ¿a qué \(\rho\) cruza cada métrica 0.10? ¿0.25? ¿Cuál dispara primero? Para un desplazamiento concentrado en un tiempo (participio pasado), grid-PSI debería disparar antes que token-PSI — verifica esto.
  • Confirma que las tres métricas son monotónicamente crecientes en \(\rho\). Si no lo son, el suavizado, el bucketing o la distribución OOD R están mal — debuggéalo antes de continuar.

Bloque E — chequeo de cordura de tamaño de muestra

  • Repite la medición \(\rho = 0.15\) con tamaños de \(Q\) de \(\{100, 1{,}000, 10{,}000, 100{,}000\}\) tokens. Plotea KL, token-PSI y grid-PSI vs tamaño de \(Q\) a \(\rho\) fijo.
  • Confirma que las tres métricas se estabilizan para \(|Q| \geq 1{,}000\) y son ruidosas debajo. Esta es la racional de la cota inferior para la cadencia de drift en producción.

Bloque F — receta Justfile + manifest + README

  • Añade just drift-check para invocar scripts/mlops/run_drift_check.py sobre el requests.log en vivo de una corrida de Fase 33/34, comparando contra baseline_histogram.json + baseline_grid.json. La receta escribe un drift_reports/YYYY-MM-DD.json y sale no-cero si cualquier PSI ≥ 0.25.
  • manifest.json lista: seed, SHA del tokenizer de la Fase 11, hash DVC del corpus de la Fase 12, lista de valores \(\rho\), suavizado \(\alpha\), esquema de buckets PSI.
  • README.md (300–500 palabras):
  • La tabla de umbral para este corpus: \(\rho_{0.10}, \rho_{0.25}\) por métrica.
  • La cota inferior de tamaño de muestra.
  • Un párrafo: por qué grid-PSI disparó antes que token-PSI (o no lo hizo — registra de cualquier modo).
  • Un párrafo: qué harían las próximas 24h de detección de drift en producción con estos umbrales calibrados.

Restricciones

  • Solo NumPy. Sin funciones entropy o psi de SciPy — escribe las fórmulas tú mismo. (Verificar contra SciPy a posteriori está bien.)
  • Muestras determinísticas. Usa un np.random.default_rng(seed) con seed. La misma corrida de lab con la misma semilla debe producir valores idénticos de KL y PSI.
  • Sin mlflow.log_metric para tracking — escribe a manifest.json y results.json solamente. El tracking MLflow entra en el gate de CI (lab 04), no en el análisis de drift offline.
  • Sin nuevo src/<module>/. drift.py vive en scripts/mlops/. La CLI drift_check es un wrapper fino.

Condiciones de parada

Hecho cuando:

  1. KL, token-PSI y grid-PSI suben monotónicamente con \(\rho\).
  2. Los umbrales PSI 0.10 y 0.25 cruzan en valores \(\rho\) razonables (típicamente \(\rho_{0.10} \approx 0.05\)\(0.10\) y \(\rho_{0.25} \approx 0.15\)\(0.25\) para esta forma de corpus, pero el lab calibrará).
  3. El chequeo de cordura de tamaño de muestra muestra estabilización por encima de \(|Q| = 1{,}000\).
  4. just drift-check corre end-to-end sobre un log en vivo sintético y produce un reporte.
  5. manifest.json y README.md están commiteados.

Pitfalls

  • \(\log(0)\). Si algún \(Q(t) = 0\) para \(t\) con \(P(t) > 0\), KL diverge. El suavizado debe aplicarse a ambas distribuciones, cada vez.
  • Dirección equivocada. \(D_{KL}(P \| Q) \neq D_{KL}(Q \| P)\). Usa la convención "entrenamiento primero" de theory/03. Chequea que la fórmula coincide con sum P * log(P / Q), no al revés.
  • Elección de tamaño de bucket. Buckets PSI de ancho-igual sobre una distribución de tokens de verbo en ley de potencias ponen casi toda la masa en los buckets de tokens raros. Usa el esquema de 15 buckets estratificados por frecuencia del Bloque C, no ancho-igual ingenuo.
  • Mismatch de vocabulario. Si los tokens OOD no están en \(V\), no puedes representarlos. Mapea todos los tokens OOD a un id especial [UNK] (o a tokens raros existentes). Chequeo de cordura: cada token en \(Q_\rho\) tiene un slot de conteo en tu vector de longitud \(|V|\).
  • Precisión flotante. Las probabilidades suavizadas son diminutas (\(\sim 10^{-7}\)). Usa float64 todo el tiempo. float32 hace underflow.
  • Confundir celdas de la rejilla. La rejilla (tiempo, persona) tiene 15 celdas exactamente per §A13. No añadas una celda "negación" o "interrogativa" — esas son expansiones de trabajo futuro. Quédate con la forma del corpus.

Cuándo consultar solutions/

Tras los seis bloques. solutions/02-drift-ref.md (apertura de fase) revisa tu esquema de bucketing, el suavizado y los umbrales calibrados.


Siguiente lab: lab/03-finops-table.md.