Skip to content

English · Español

Lab 02 — La trampa del broadcasting

Objetivo: producir, intencionadamente, el bug (N,) * (N,1) → (N,N). Después arreglarlo. Después catalogar dos sorpresas más de broadcasting para que nunca te vuelvan a sorprender.

Tiempo estimado: 45–60 minutos.

Prerrequisitos: lab 00.


Lo que produces

Un directorio experiments/06-broadcasting-trap/ que contiene:

  • bug.py — script que reproduce el bug e imprime pruebas (shape incorrecto, número incorrecto).
  • fix.py — el mismo cómputo hecho correctamente; imprime pruebas.
  • catalog.py — tres situaciones más de broadcasting; para cada una, imprime los shapes de entrada, el shape de salida esperado y el shape de salida real (o ValueError).
  • manifest.json — esquema estándar.
  • README.md — un párrafo por situación en catalog.py explicando por qué el broadcast se resuelve como lo hace.

TODOs

Bloque A — reproducir el bug clásico

En bug.py:

  • Genera "predicciones" y_pred = np.arange(10, dtype=np.float32) — shape (10,).
  • Genera "objetivos" y_true = (np.arange(10, dtype=np.float32) + 0.1).reshape(10, 1) — shape (10, 1).
  • Calcula err = y_pred - y_true. Imprime err.shape. (Debería imprimir (10, 10), no (10,).)
  • Calcula mse_wrong = (err ** 2).mean(). Imprime el valor.
  • Calcula también el MSE intencionado mse_right (usa un bucle manual o ((y_pred - y_true.squeeze()) ** 2).mean()). Imprime el valor.
  • Asegura mse_wrong != mse_right para hacer el bug innegable.

Bloque B — arreglarlo de tres maneras

En fix.py, muestra tres arreglos idiomáticos para el bug de arriba:

  1. Coincidir shapes explícitamente: y_true_flat = y_true.squeeze(). Luego ((y_pred - y_true_flat) ** 2).mean().
  2. Coincidir al revés: y_pred_col = y_pred[:, None]. Luego ((y_pred_col - y_true) ** 2).mean().
  3. Ser paranoico: assert y_pred.shape == y_true.squeeze().shape. Luego procede.

Imprime los tres resultados. Deberían coincidir dentro de la precisión fp32 (~1e-7).

Bloque C — catalogar tres situaciones más de broadcasting

En catalog.py, para cada una de las siguientes, escribe una sección: shapes de entrada → shape de salida predicho → shape de salida real → explicación de un párrafo citando la regla de broadcast.

Situación 1: a.shape = (3, 4), b.shape = (4,). Calcula a + b.

Situación 2: a.shape = (3, 4), b.shape = (3,). Calcula a + b. (Pista: esta es la trampa de "quería broadcastear sobre filas pero no reshapé".)

Situación 3: a.shape = (B, 1, N, D), b.shape = (1, H, N, D) (forma de attention). Calcula a + b.

Para cada una, tu código debe:

  • Imprimir los shapes de entrada.
  • Imprimir tu predicción del shape de salida antes de calcular (usa un comentario).
  • Imprimir el shape de salida real.
  • Capturar cualquier ValueError e imprimirlo en su lugar.

Bloque D — fijarlo en memoria

  • En README.md, escribe tres reglas mnemotécnicas con tus propias palabras. Deberían mapearse directamente a: (i) alinear a la derecha, (ii) los dims coinciden o son 1, (iii) el resultado es el máximo por pares. Si no puedes escribir la regla, aún no la posees.

Restricciones

  • fp32. Coincide con fases posteriores.
  • Sin try / except Exception. Captura el ValueError específico que esperas; deja que las excepciones inesperadas estallen para que te enteres.
  • Imprime, no logues. Este lab es interactivo; la estructura de logging no es el punto aquí. (Excepción a la regla del lab 00.)

Resultados esperados

  • bug.py imprime err.shape = (10, 10) y dos valores de MSE distintos. El "incorrecto" es aproximadamente una décima parte del "correcto" (porque estás promediando 100 entradas donde 90 son términos cruzados de magnitud ~0–9).
  • fix.py imprime tres valores de MSE coincidentes.
  • catalog.py situación 1: (3, 4). Situación 2: ValueError. Situación 3: (B, H, N, D).

Condiciones de parada

Hecho cuando:

  1. Los tres scripts corren satisfactoriamente (donde el éxito para bug.py incluye que el assertion se dispare — eso es el bug).
  2. La situación 2 de catalog.py lanza ValueError y tu código la captura limpiamente.
  3. README.md tiene las tres reglas mnemotécnicas con tus propias palabras.

Escollos

  • reshape(-1, 1) frente a [:, None]. Funcionalmente idénticos para arrays 1-D. Elige uno y úsalo consistentemente. [:, None] es más idiomático para insertar ejes.
  • squeeze sin argumento de eje. Elimina todos los ejes de tamaño 1. Peligroso si pretendías eliminar un eje específico. Sé explícito: squeeze(axis=-1).
  • (N, 1) - (N, 1) NO broadcastea a (N, N). Ambos shapes son (N, 1), totalmente compatibles por eje, el resultado es (N, 1). El bug solo ocurre cuando uno es (N,) y el otro es (N, 1).
  • Los shapes de mayor dimensión confunden el ojo. Escribe los shapes alineados a la derecha antes de calcular:
    (B, 1, N, D)
    (1, H, N, D)   ← alineados a la derecha
    
    Luego por eje: (B, H, N, D). Cinco segundos de papel ahorran una hora de depuración.
  • El mensaje de error de NumPy. Cuando el broadcasting falla, NumPy imprime los dos shapes — léelos. El shape que te sorprende es el que necesita [:, None] o .squeeze().

Cuándo consultar solutions/

Después de que los tres scripts funcionen y README.md contenga las reglas con tus propias palabras. solutions/02-broadcasting-trap-ref.md (escrito en la apertura de fase) proporciona las explicaciones de referencia.


Siguiente lab: lab/03-vectorization-budget.md.