Skip to content

English · Español

Break — Trampa silenciosa de broadcasting con un vector columna

La trampa más sutil de NumPy: sumar un vector de shape (3,) a una matriz (3, 4) no hace lo que crees. La forma compatible es (3, 1). Sin el [:, None], broadcasting elige el otro eje y los números salen mal — pero no hay error.

Objetivo: cualquier suma de logits batch + bias del §A13. Setup típico: logits tiene shape (3 personas, 4 features); un bias por persona tiene shape (3,). Quieres sumar el bias a cada fila.

Hipótesis

El aprendiz predice: "logits + bias donde logits.shape = (3, 4) y bias.shape = (3,) no sumará el bias por filas. NumPy alineará (3,) al eje final (longitud 4), que es incompatible — así que broadcastea contra el eje columna (longitud 3) en su lugar, produciendo la suma incorrecta o, según los shapes, un crash."

El break

En una función que debería sumar un bias por fila:

 def add_row_bias(logits: np.ndarray, bias: np.ndarray) -> np.ndarray:
-    # logits: (P, F); bias: (P,) -> reshape a (P, 1) para que el broadcasting funcione
-    return logits + bias[:, None]
+    return logits + bias    # /break: depende de un alineamiento accidental de shapes

Procedimiento de ejecución

uv run python -c "
import numpy as np

# Caso A: P=3 personas, F=4 features. bias por persona.
logits_A = np.array([[1., 2., 3., 4.],
                     [5., 6., 7., 8.],
                     [9.,10.,11.,12.]])
bias_A = np.array([100., 200., 300.])  # uno por persona

# Caso B: P=3, F=3 (cuadrado). Mismo bias.
logits_B = np.array([[1., 2., 3.],
                     [4., 5., 6.],
                     [7., 8., 9.]])
bias_B = np.array([100., 200., 300.])

print('--- Caso A (3x4) + (3,) ---')
try:
    print(logits_A + bias_A)
except Exception as e:
    print('CRASH:', e)

print('--- Caso B (3x3) + (3,) ---')
print(logits_B + bias_B)
print('--- correcto: suma por filas ---')
print(logits_B + bias_B[:, None])
"

Modo de fallo esperado

--- Caso A (3x4) + (3,) ---
CRASH: operands could not be broadcast together with shapes (3,4) (3,)

--- Caso B (3x3) + (3,) ---
[[101. 202. 303.]
 [104. 205. 306.]
 [107. 208. 309.]]      <-- ¡biases aplicados POR COLUMNAS, no por filas!

--- correcto: suma por filas ---
[[101. 102. 103.]
 [204. 205. 206.]
 [307. 308. 309.]]      <-- bias 100 a la fila 0, 200 a la fila 1, 300 a la fila 2

El Caso A crashea sonoramente (bien — fácil de cazar). El Caso B es la trampa: los shapes accidentalmente cuadran porque la matriz es cuadrada, pero el bias se aplica por columnas en lugar de por filas. Sin error, respuesta incorrecta. Este es el bug que se va silenciosamente a producción.

Diagnóstico

Solo desde los logs:

  1. Imprime los shapes de los operandos antes de cada op por elemento al menos en desarrollo. print(f'{logits.shape=} {bias.shape=}'). Caza la trampa en 5 segundos.
  2. Escribe un test de respuesta conocida con un shape no cuadrado. Las matrices cuadradas ocultan muchos bugs de broadcasting; las rectangulares los exponen.
  3. Usa np.broadcast_shapes(a.shape, b.shape) para ver qué producirá NumPy. Si no coincide con tu modelo mental, arregla el alineamiento.
  4. Añade un property test: para (P, F) aleatorios con P != F, add_row_bias(logits, bias).shape == (P, F) y (add_row_bias(logits, bias) - logits)[i, :] == bias[i] para cada fila i.

Lección

El broadcasting de NumPy alinea shapes desde el eje más a la derecha. Un (3,) se alinea al último eje del otro operando. Si el último eje tiene longitud 3 (matriz cuadrada), las matemáticas "funcionan" pero por columnas, no por filas.

El arreglo es un único carácter: bias[:, None] (o bias.reshape(-1, 1), o bias[:, np.newaxis]). Reshapea (3,) a (3, 1), que broadcastea inambiguamente contra (3, 4)(3, 4) por filas.

Esta es la misma trampa que la del broadcasting de tensores de la Fase 8 (donde los gradientes backward tienen que sumarse a lo largo del eje broadcasteado). Apréndela aquí al coste de una sesión de depuración; en la Fase 8 costaría un fallo de gradcheck de 4 horas para diagnosticar.

Referencias