English · Español
Lab 01 — Jacobiana de una MLP minúscula, analítica vs numérica¶
🇪🇸 Una MLP de dos capas. Calculas
∂y/∂W1de dos maneras: con regla de la cadena (matmul de Jacobianas) y con diferencias finitas. Las dos deben coincidir dentro de1e-4. Si no, lo arreglas — porque cuando Fase 7 implemente autograd, esta misma derivación tiene que producir el mismo número.
Objetivo¶
Para una MLP de 2 capas y = W₂ · relu(W₁ x + b₁) + b₂, calcula la Jacobiana ∂y/∂W₁ analíticamente (usando la regla de la cadena multivariable de teoría 02) y numéricamente (diferencias finitas). Verifica que coinciden dentro de 1e-4. Esta es la prueba de concepto para el autograd escalar de la Fase 7.
Planteamiento¶
- Formas:
x ∈ R³,W₁ ∈ R^{4×3},b₁ ∈ R⁴,W₂ ∈ R^{2×4},b₂ ∈ R². Asíy ∈ R². - Parámetros totales en
W₁:4 × 3 = 12. Calcularemos la Jacobiana2 × 12∂y/∂vec(W₁). - Usa init aleatoria determinista:
np.random.default_rng(seed=42).
Tareas¶
Parte A — La MLP y el pase hacia adelante¶
import numpy as np
rng = np.random.default_rng(42)
x = rng.standard_normal(3)
W1 = rng.standard_normal((4, 3))
b1 = rng.standard_normal(4)
W2 = rng.standard_normal((2, 4))
b2 = rng.standard_normal(2)
def mlp(x, W1, b1, W2, b2):
h_pre = W1 @ x + b1 # (4,)
h = np.maximum(0, h_pre) # ReLU, (4,)
y = W2 @ h + b2 # (2,)
return y, h_pre, h # devuelve intermedios para la Jacobiana analítica
Parte B — Jacobiana analítica¶
Aplica la regla de la cadena paso a paso.
∂y/∂h = W₂(forma(2, 4)).∂h/∂h_pre = diag(h_pre > 0)— ReLU es1dondeh_pre > 0y0en otro caso. (Forma(4, 4).)∂h_pre/∂W₁: aquí hay que ser cuidadoso con las formas.h_pre = W₁ x. El elementoideh_preesΣ_j W₁_{ij} x_j. Así que∂h_pre_i / ∂W₁_{ab} = δ_{ia} x_b. AplanaW₁avec(W₁) ∈ R^{12}con orden row-major: índice(a, b)→3a + b. Entonces∂h_pre/∂vec(W₁)es una matriz(4, 12)donde la filaitienex_ben la columna3i + by cero en otro caso.
def dh_pre_dW1(x):
J = np.zeros((4, 12))
for i in range(4):
for b in range(3):
J[i, 3*i + b] = x[b]
return J
- Encadénalo todo:
def analytical_jacobian(x, W1, b1, W2, b2):
y, h_pre, h = mlp(x, W1, b1, W2, b2)
dy_dh = W2 # (2, 4)
dh_dh_pre = np.diag((h_pre > 0).astype(float)) # (4, 4)
dh_pre_dW1_mat = dh_pre_dW1(x) # (4, 12)
return dy_dh @ dh_dh_pre @ dh_pre_dW1_mat # (2, 12)
Parte C — Jacobiana numérica (diferencias finitas)¶
def numerical_jacobian(x, W1, b1, W2, b2, h=1e-5):
y_base, _, _ = mlp(x, W1, b1, W2, b2)
J = np.zeros((2, 12))
for k in range(12):
a, b = divmod(k, 3)
W1_plus = W1.copy(); W1_plus[a, b] += h
W1_minus = W1.copy(); W1_minus[a, b] -= h
y_plus, _, _ = mlp(x, W1_plus, b1, W2, b2)
y_minus, _, _ = mlp(x, W1_minus, b1, W2, b2)
J[:, k] = (y_plus - y_minus) / (2 * h)
return J
Parte D — Compara¶
J_an = analytical_jacobian(x, W1, b1, W2, b2)
J_num = numerical_jacobian(x, W1, b1, W2, b2)
max_err = np.max(np.abs(J_an - J_num))
print(f"Max element-wise error: {max_err:.2e}")
assert max_err < 1e-4, f"Jacobians disagree: {max_err}"
Si max_err < 1e-7, genial — ambos métodos coinciden a precisión fp64 (módulo truncamiento por diferencias finitas).
Parte E — Caso límite: ReLU en cero¶
La derivada de ReLU es 1 para entrada positiva, 0 para negativa. En cero exacto, no está definida — la convención es 0 (ya que np.maximum(0, x) da 0). Prueba la comparación con una entrada deliberadamente en el cruce por cero de ReLU:
# Forzar h_pre[0] a ser exactamente 0 vía un W1, b1 cuidadosamente construidos
b1_zeroed = b1.copy()
b1_zeroed[0] = -W1[0] @ x # hace h_pre[0] = 0 exactamente
# Vuelve a correr la comparación
Documenta el resultado: en h_pre = 0, el método analítico dice ∂h/∂h_pre = 0; las diferencias finitas dan 0 por la derecha y 1 por la izquierda, promediando a ~0.5. FD centrada dará ~0.5 mientras que la analítica da 0. El desacuerdo en un conjunto de medida cero es esperado y OK.
Entregable¶
learners/borja/phase-04/lab-01-jacobian.md:
- Listado de código.
- Salida de max_err.
- Reflexión de 3 frases sobre el caso límite de ReLU en cero.
Aceptación¶
- Las Jacobianas analítica y numérica coinciden dentro de
1e-4para la entrada con semilla aleatoria. - Coinciden dentro de
1e-7para entradas típicas (donde ReLU no está en cero). - El caso de ReLU en cero está documentado pero no "arreglado" — es una no-diferenciabilidad genuina.
Escollos¶
- Aplanado row-major vs column-major.
.flatten()de NumPy por defecto es row-major (orden C). Sé explícito. Si tudh_pre/dW1usa una convención y tuvec(W1)la otra, obtendrás basura. - Olvidar copiar
W1antes de perturbar.W1 += h_vecmodifica el original; las perturbaciones posteriores se acumulan. SiempreW1_plus = W1.copy(); W1_plus[a, b] += h. - Usar diferencias hacia adelante. Error O(h); para
h = 1e-5, da ~1e-5de error — falla el umbral1e-7para entradas "típicas" incluso cuando las matemáticas son correctas. - Confusión con la derivada de ReLU. En
h_pre < 0, la derivada es0y la entrada no influencia la salida. Algunas personas escriben∂relu/∂x = (x > 0).astype(float)(correcto); algunas escriben(x >= 0)(off-by-one en cero). Mantén el>estricto. hminúsculo causando cancelación.h = 1e-12da basura por cancelación de punto flotante enf(x+h) - f(x-h). Usah = 1e-5.
Ampliación¶
- Extiende para calcular
∂y/∂b₁y∂y/∂W₂analítica y numéricamente. También deberían coincidir dentro de1e-4. - Implementa el gradiente completo
∂L/∂W₁dondeL = ‖y − target‖²/2para algún target aleatorio. Esto es lo que el autograd de la Fase 7 producirá automáticamente.
Siguiente: 02-optimizers-on-rosenbrock.md