Módulo 11 · Rigor

Reproducibilidad y seguridad

Un experimento que no se puede reconstruir no es un resultado: es una anécdota. El módulo de cierre gira alrededor de dos exigencias que separan un juguete de un sistema serio. Primero, todo run debe poder rehacerse — misma seed, mismos datos, mismo código, misma config — hasta el punto de reproducir los números bit a bit. Segundo, la seguridad no vive dentro del fine-tuning: entrenar bien no basta, hace falta validar el modelo y envolverlo en políticas. Aquí fijamos cómo registrar cada experimento de forma declarativa y cómo probar adversarialmente el modelo antes de exponerlo a audiencias sensibles.

Al terminar sabrás

  • Registrar un experimento como una config declarativa (YAML/JSON) que lo describe por completo.
  • Fijar la seed global y las banderas de determinismo para reproducir un run bit a bit.
  • Reportar resultados con media ± std sobre varias seeds y distinguir una mejora real del ruido.
  • Enumerar la superficie de seguridad para audiencias sensibles: alcance, datos personales, temas delicados, respuesta segura.
  • Medir la robustez con refusal-rate y defenderte de la inyección de prompt directa e indirecta.

1Un experimento es una función, no un ritual

La causa número uno de resultados que "ya no salen" es que faltaba anotar algo. Un run de entrenamiento es determinista solo si se fija todo lo que lo determina. Conviene pensarlo como una función pura: dadas las mismas entradas, produce la misma salida.

$$ \text{resultado} = f(\text{seed}, \text{data\_hash}, \text{code\_commit}, \text{config}, \text{flags de determinismo/hardware}) $$

La seed global fija los streams de números aleatorios de Python, NumPy, PyTorch y CUDA — la inicialización de pesos, el orden del shuffle, el dropout, todo lo que consume aleatoriedad. Pero la seed sola no alcanza: dos GPUs distintas, o el mismo hardware con kernels no deterministas, pueden divergir. Por eso el registro incluye también las banderas de determinismo y una huella del hardware.

Se registraPor qué importa
Modelo base exacto + revisiónEl mismo nombre en otro commit del hub ya no es el mismo modelo.
Versión de cada libreríaUn cambio de minor en el trainer altera el resultado en silencio.
Dataset + versión/hashIdentifica exactamente qué datos entraron (ver M2/M3).
SeedFija todos los streams RNG del run.
Config LoRARango, alfa, módulos objetivo, dropout (M4/M5).
Config del trainerLearning rate, batch, épocas, scheduler, warmup (M6).
Chat templateEl formato de turnos con el que se entrenó (M1).
Hardware + banderas de determinismoGPU, precisión, kernels deterministas.
Métricas + checkpointQué se midió y qué artefacto se produjo.
Config de generacióntemperature/top_p con que se evaluó (M0).

2Config declarativa: el run vive en un archivo

La disciplina práctica es simple: ningún hiperparámetro vive en la línea de comandos ni en el cuaderno. Cada valor de M4/M5/M6 se escribe en un archivo declarativo, y ese archivo se hashea para producir el identificador del run. Si el hash coincide, el run es el mismo; si difiere, algo cambió y el archivo dice exactamente qué.

$$ \text{run\_id} = \mathrm{hash}\big(\text{config.yaml}\big), \qquad \text{config.yaml} \;\longrightarrow\; \text{run determinista} $$

Esto vuelve el experimento revisable (se lee como texto), versionable (entra en git junto al código) y reejecutable (un solo comando lo consume). La diferencia entre dos experimentos se convierte en un diff de dos archivos.

3Reportar varianza, no un número suelto

Con la seed fija, un run es reproducible; pero un solo run no dice si una mejora es real. La aleatoriedad de la seed induce dispersión entre corridas. La forma honesta de reportar es correr $n$ seeds y dar la media con su desviación estándar, además del error estándar de la media:

$$ \text{media} \pm \text{std}, \qquad \mathrm{SE} = \frac{\text{std}}{\sqrt{n}} $$

La regla de oro: una mejora afirmada debe superar el ruido entre corridas. Si tu cambio sube la métrica en menos que la desviación entre seeds del baseline, no has demostrado nada — has observado ruido. Fijar la seed es para reproducir; barrer varias seeds es para concluir.

Nota

Reproducibilidad y varianza no se contradicen. Cada corrida individual es determinista (seed fija ⇒ mismos bits). Lo que varía es qué seed elegiste. Reportas la distribución sobre seeds precisamente porque cada punto de esa distribución es, en sí mismo, reproducible.

4La seguridad no está dentro del modelo

Un modelo fine-tuned no es seguro por haber sido entrenado con datos limpios. La seguridad es una propiedad del sistema alrededor del modelo: validaciones a la entrada, políticas de alcance y una respuesta segura cuando el modelo no sabe. Para audiencias sensibles la superficie a cubrir es amplia y concreta:

  • Límites por edad y alcance: qué temas están dentro del dominio permitido y cuáles no.
  • Contenido fuera de dominio: reconocer y declinar peticiones que caen fuera del propósito.
  • Datos personales: no solicitar, no filtrar y no retener información identificable del usuario final.
  • Prompt injection directa e indirecta: instrucciones hostiles inyectadas por el usuario o por datos externos.
  • Temas sensibles y control parental: rutas de escalado y supervisión para audiencias vulnerables.
  • Respuesta segura ante la ignorancia: decir "no lo sé" es preferible a inventar (evitar la alucinación de M9).
  • Pruebas adversariales: probar el sistema como lo probaría un atacante, no como lo usaría un usuario amable.
Cuidado

Mover un caso de seguridad "al fine-tuning" es tentador pero frágil: el modelo generaliza de forma imperfecta y un prompt algo distinto puede sortear lo aprendido. Las barreras robustas se ponen fuera del modelo (validación de entrada/salida, límites de alcance) y se miden, no se asumen.

5Ataques adversariales y la métrica que los mide

Un ataque de sufijo adversarial tipo GCG optimiza un texto añadido al prompt para forzar que el modelo empiece a cooperar con una petición dañina — típicamente maximizando la probabilidad de que la respuesta arranque con una frase afirmativa:

$$ \max_{\text{sufijo}}\; p_\theta\big(\text{"Sure, here…"} \mid \text{prompt} \oplus \text{sufijo}\big) $$

Que esto sea posible motiva medir la robustez con una métrica concreta sobre un conjunto de red-team. La refusal-rate es la fracción de casos dañinos que el modelo efectivamente rechaza:

$$ \text{refusal-rate} = \frac{1}{N}\sum_{i=1}^{N} \mathbb{1}\big[\text{el modelo rechaza el caso dañino}_i\big] $$

No es un número que se reporte una vez y se olvide: se mide antes y después de cada mitigación (por ejemplo un guardrail en el system-prompt) para saber si la defensa realmente subió la tasa de rechazo o solo lo pareció.

6Inyección indirecta: el dato también instruye

La inyección de prompt directa la escribe el usuario. La indirecta es más insidiosa: instrucciones hostiles vienen incrustadas en los datos recuperados — el contexto de RAG del M8 — y entran al prompt con el mismo nivel de confianza que las órdenes legítimas del usuario. El modelo no distingue "instrucción del usuario" de "texto que encontré en un documento".

Nota · fronteras de confianza

La defensa es arquitectónica: establecer fronteras de confianza y aislar la entrada, de modo que el contenido recuperado se trate como datos no confiables y no como instrucciones. La eficacia se mide con la tasa de éxito del ataque antes vs. después de la mitigación — si baja de forma sostenida sobre un set adversarial, la frontera funciona.

usuario ──▶ [ validación de entrada ] ──▶ prompt ──▶ modelo ──▶ [ validación de salida ] ──▶ respuesta
documento recuperado (M8) ──▶ [ frontera de confianza · datos ≠ instrucciones ] ──┘
La seguridad envuelve al modelo por ambos lados; el dato recuperado nunca cruza como orden.
En el teléfono

La seguridad no termina en el modelo: en una app on-device los guardrails corren localmente — validación de entrada, filtros y límites por edad/alcance se aplican en el dispositivo, sin red. La inyección indirecta (instrucciones escondidas en datos recuperados por el RAG local) entra con el mismo nivel de confianza que el usuario: aísla ese contenido en la frontera de la app.

Cómo practicar
  1. Escribe un experiment.yaml con seed, hash del dataset, modelo e hiperparámetros.
  2. Prepara un comando que reproduzca el run y verifica que dos corridas con la misma seed coinciden.
  3. Reporta media±std sobre 3 seeds.
  4. Arma una suite red-team: jailbreak directo + un payload de inyección indirecta escondido en un “documento recuperado”.
  5. Mide refusal-rate / tasa de éxito de inyección antes y después de un guardrail en el system-prompt.
Herramientas: Python · PyYAML · garak / promptfoo
Rust · on-device

La capa de seguridad de una app on-device suele vivir en Rust, justo en la frontera FFI: validas y saneas el prompt del usuario y el contenido recuperado antes de pasarlos al runtime, y aplicas los filtros de salida antes de mostrarlos.

Rust hace esa frontera robusta y sin sorpresas de memoria. Para el red-teaming del modelo, herramientas como garak o promptfoo automatizan los ataques.

Lecturas y recursos

La config declarativa en concreto

# experiment.yaml — describe el run por completo; su hash es el run_id
run:
  seed: 1337              # fija RNG de Python/NumPy/PyTorch/CUDA
  deterministic: true       # kernels deterministas + cudnn.benchmark=false

model:
  base: "Qwen/Qwen3-0.6B"
  revision: "a1b2c3d"       # commit exacto del hub, no solo el nombre

dataset:
  path: "data/sft_v3.jsonl"
  sha256: "9f2c…e71a"       # huella del split exacto (M2/M3)

lora:                     # M4/M5
  r: 16
  alpha: 32
  dropout: 0.05
  target_modules: ["q_proj", "k_proj", "v_proj", "o_proj"]

trainer:                  # M6
  learning_rate: 2.0e-4
  batch_size: 8
  grad_accum: 4
  epochs: 3
  scheduler: "cosine"
  warmup_ratio: 0.03

chat_template: "qwen3"      # M1

generation:               # M0 — con qué se evaluó
  temperature: 0.7
  top_p: 0.9
Un solo comando consume este archivo; su hash identifica el run bit a bit.

Ejercicios

De menor a mayor complejidad. El último cierra el curso: reproducibilidad demostrada y seguridad medida.

Ejercicio 1 · calentamiento

Fija la seed global

Escribe una función set_seed(seed) que fije los streams de Python, NumPy, PyTorch y CUDA, y active las banderas de determinismo. Genera dos tensores aleatorios tras llamarla dos veces con la misma seed y verifica que son idénticos.

Entrega: la función + un assert de igualdad bit a bit.   Pista: random.seed, np.random.seed, torch.manual_seed, torch.use_deterministic_algorithms(True).

Ejercicio 2

Hashea tu dataset y tu config

Calcula el sha256 del archivo de datos y de tu experiment.yaml. Deriva un run_id a partir del hash de la config e imprímelo. Cambia un solo hiperparámetro y muestra que el run_id cambia.

Entrega: los dos hashes + el run_id antes y después del cambio.   Pista: hashlib.sha256(path.read_bytes()).

Ejercicio 3

Reporta media ± std sobre seeds

Entrena (o evalúa) el mismo setup con 3 seeds distintas y recoge una métrica. Reporta la media, la desviación estándar y el error estándar $\text{std}/\sqrt{n}$. Decide, con esos números, si una mejora de $+0.4$ puntos sería creíble.

Entrega: tabla seed → métrica + la fila resumen media ± std.   Pista: $\mathrm{SE} = \text{std}/\sqrt{3}$; compara la mejora contra la dispersión.

Ejercicio 4

Mide la refusal-rate

Construye un set pequeño de casos dañinos y clasifica, para cada uno, si el modelo lo rechaza. Calcula la refusal-rate. Luego añade un guardrail en el system-prompt y vuelve a medir sobre el mismo set.

Entrega: refusal-rate antes y después + el system-prompt usado.   Pista: $\text{refusal-rate}=\frac{1}{N}\sum_i \mathbb{1}[\text{rechaza}_i]$; fija la config de generación para que la comparación sea justa.

Ejercicio 5 · ejercicio top

Reproducibilidad demostrada + scorecard de seguridad

Escribe un único experiment.yaml que capture seed, hash del dataset, modelo y todos los hiperparámetros de M4–M6, y conéctalo para que un comando reproduzca una corrida de entrenamiento bit-a-bit (verifica que dos runs con la misma seed coinciden, y reporta media±std sobre 3 seeds); luego construye una pequeña suite de red-team — prompts de jailbreak directo + un payload de inyección indirecta escondido en un "documento recuperado" — y mide la refusal-rate / tasa de éxito de inyección sobre tu modelo fine-tuned antes y después de añadir un guardrail en el system-prompt.

Entrega: la prueba de reproducibilidad (runs con seed emparejada + tabla de varianza a 3 seeds) y una scorecard de seguridad adversarial antes/después.   Pista: hashea la config para el run_id; para la inyección indirecta, mide la tasa de éxito del ataque con el payload dentro del contexto recuperado vs. tras aislar los datos del canal de instrucciones.

Puntos clave
  • Un experimento es una función: fija seed, data_hash, code_commit, config y hardware y se reconstruye bit a bit.
  • Cada hiperparámetro de M4/M5/M6 vive en una config declarativa cuyo hash es el run_id.
  • Reproducir usa una seed; concluir exige varias: reporta media ± std y supera el ruido entre corridas.
  • La seguridad envuelve al modelo — validaciones y políticas — no se resuelve solo dentro del fine-tuning.
  • Mide la robustez: refusal-rate y tasa de éxito de inyección, antes y después de cada mitigación; la inyección indirecta entra por los datos recuperados.

volver al índice