English · Español
02 — Derivadas de las operaciones¶
🇪🇸 La derivada local de cada operación que
Valuesoporta — sumar, multiplicar, dividir, potenciar,exp,log,ReLU,tanh. Estas son las únicas matemáticas nuevas de la fase. Cada derivada cabe en una línea; juntas son la materia prima de toda la retropropagación.
Lo que derivarás aquí¶
Para cada operación c = f(a, b) (o c = f(a) para unarias), la derivada local es ∂c/∂a (y ∂c/∂b para binarias). El closure _backward de c las usará para contribuir a los padres:
Esta es la regla de la cadena, un nodo a la vez.
Suma: c = a + b¶
∂c/∂a = 1∂c/∂b = 1
_backward:
Comprobación de cordura: c = a + b = 2 + 3 = 5. Si incrementamos a en ε, c aumenta en ε (luego ∂c/∂a = 1). Si incrementamos b en ε, c aumenta en ε (luego ∂c/∂b = 1). ✓
Resta: c = a - b¶
Reutilizar: c = a + (-b). Por tanto:
∂c/∂a = 1∂c/∂b = -1
_backward:
Implementación equivalente: define __neg__ (que fija _backward para la negación: ∂(-a)/∂a = -1), luego implementa __sub__ como __add__(a, -b). Ambas funcionan; la última es más limpia y reutiliza código.
Multiplicación: c = a * b¶
∂c/∂a = b∂c/∂b = a
_backward:
Comprobación de cordura: c = 2 * 3 = 6. Incrementa a en ε: c → (2+ε)·3 = 6 + 3ε. Luego ∂c/∂a = 3 = b. ✓ Lo mismo para b.
Nota: leemos .data de los padres, no los padres en sí. La derivada local es un número (un float), no un Value. (La Fase 8 lo revisitará cuando los gradientes pasen a ser tensores.)
División: c = a / b¶
Tratar como c = a · b^(-1). Por regla del producto + regla de la cadena:
∂c/∂a = b^(-1) = 1/b∂c/∂b = a · (-1) · b^(-2) = -a / b²
_backward:
Comprobación de cordura: c = 6/3 = 2. Incrementa a en ε: c → (6+ε)/3 = 2 + ε/3. Luego ∂c/∂a = 1/3 = 1/b. ✓ Incrementa b en ε: c → 6/(3+ε) ≈ 6/3 · (1 - ε/3) = 2 - 2ε/3. Luego ∂c/∂b = -2/3 = -a/b². ✓
Cuidado: división por cero. Forward producirá inf o lanzará ZeroDivisionError. Backward propagará inf/nan. Decide: clamp, o lanzar. El BLUEPRINT elegirá una convención.
Potencia: c = a ** n (con n constante)¶
Para un exponente constante n (no un Value):
∂c/∂a = n · a^(n-1)
_backward:
Por qué n debe ser una constante (int o float de Python), no un Value: si n también fuera diferenciable, necesitarías ∂(a^n)/∂n = a^n · ln(a), que requiere a > 0 y es más frágil. En minigrad.scalar solo soportamos exponentes constantes. Documenta esta restricción en el BLUEPRINT. El pow de PyTorch soporta ambas formas; nosotros no, porque el rédito educativo es bajo y el foot-gun es real.
Comprobación de cordura: c = 2³ = 8. ∂c/∂a = 3·2² = 12. Incrementa a a 2.01: c ≈ 2.01³ = 8.12. Δc/Δa ≈ 12. ✓
Exponencial: c = exp(a)¶
∂c/∂a = exp(a) = c
_backward:
(Usamos c.data, no math.exp(a.data) — son iguales pero c.data ya está computado.)
Logaritmo: c = log(a) (logaritmo natural)¶
∂c/∂a = 1/a
_backward:
Cuidado: a ≤ 0 hace que el forward devuelva nan o -inf. Decide: clampar a a un ε pequeño, lanzar, o confiar en el llamador. El patrón combinado cross_entropy(softmax(...)) en la Fase 8 evita esto por completo al no materializar nunca log(0).
ReLU: c = max(0, a)¶
∂c/∂a = 1 si a > 0 si no 0
_backward:
La cuestión del sub-gradiente: ¿cuál es ∂c/∂a en a = 0? Matemáticamente, ReLU no es diferenciable en 0 — la derivada por la izquierda es 0, por la derecha es 1. El conjunto sub-gradiente es [0, 1]. Los frameworks eligen una convención; opciones comunes son 0 (la mayoría), 0.5 (algunos), o 1 (raros).
Elegimos 0, coincidiendo con PyTorch. Documenta esto en el BLUEPRINT y haz un test unitario explícito:
La elección rara vez importa en la práctica (los floats casi nunca son exactamente cero), pero los tests deben fijarla.
Tanh: c = tanh(a)¶
Recuerda tanh(a) = (e^a - e^(-a)) / (e^a + e^(-a)). Derivar:
_backward:
(Usamos c.data, no una recomputación, por la misma razón que exp.)
Por qué tanh está en el conjunto básico de operaciones: es la "no-linealidad suave acotada" más sencilla útil para redes diminutas. El XOR-MLP en el experimento de esta fase usa tanh como su activación oculta. Sigmoid y softmax no añaden poder expresivo para autograd escalar y abarrotarían la API; los añadiremos en la Fase 8 donde se combinan con cross-entropy.
Una nota lateral: tanh vía exp, ¿o nativo?¶
Dos elecciones de implementación:
Opción A (nativa): tanh es una operación primitiva con su propio _backward usando 1 - c². Un nodo en el grafo.
Opción B (vía exp): descomponer tanh(a) = (exp(2a) - 1) / (exp(2a) + 1). Construir desde operaciones existentes. ~5 nodos en el grafo; la visualización muestra la estructura; los tests comprueban la misma respuesta.
Ambas son correctas. La opción A es más rápida, menos asignaciones. La opción B es más transparente pedagógicamente (sin operación especial, solo composición).
Default para minigrad.scalar: Opción A (nativa). Razón: sigue siendo pedagógicamente clara (un closure corto) y coincide con la estructura de PyTorch. La opción B es un buen ejercicio; hazla una vez para entender, luego ve con A.
El BLUEPRINT registra la elección. Si Borja prefiere B en la apertura de fase, BLUEPRINT cambia.
Negación unaria: c = -a¶
∂c/∂a = -1
_backward:
Implementar como __neg__ en Value, luego reutilizar en __sub__.
Tabla resumen¶
| Op | Forward | Derivada local |
|---|---|---|
+ |
c = a + b |
∂c/∂a = 1, ∂c/∂b = 1 |
- (binaria) |
c = a - b |
∂c/∂a = 1, ∂c/∂b = -1 |
- (unaria) |
c = -a |
∂c/∂a = -1 |
* |
c = a · b |
∂c/∂a = b, ∂c/∂b = a |
/ |
c = a / b |
∂c/∂a = 1/b, ∂c/∂b = -a/b² |
** n |
c = aⁿ |
∂c/∂a = n · aⁿ⁻¹ |
exp |
c = e^a |
∂c/∂a = c |
log |
c = ln(a) |
∂c/∂a = 1/a |
relu |
c = max(0, a) |
∂c/∂a = 1 si a > 0 si no 0 |
tanh |
c = tanh(a) |
∂c/∂a = 1 - c² |
Imprime esta tabla. Pégala en la pared. Para el final de la Fase 7 no deberías necesitar mirarla.
Escollos (morderán en el lab)¶
- Leer
a.datadespués de que cambió. El closure capturaapor referencia; si mutasa.dataentre forward y backward, el closure ve el nuevo valor. No mutes parámetros en mitad de un forward. - Olvidar
c.graden la contribución. La derivada local se multiplica por el gradiente upstream.a.grad += b.dataestá mal; debe sera.grad += b.data * c.grad. - Usar
c.datacuando la regla necesitaa.data. Ten cuidado con qué.dataagarras. Backward de mul:a.grad += b.data * c.grad—b.data, noa.data. La sustitución descuidada es un bug común. - Sub-gradiente de
ReLUen 0. Elige una convención, documéntala, testéala. powcon exponente no constante. No lo soportes. LanzaTypeErrorsines unValue.
Recapitulación en un párrafo¶
Las diez operaciones de minigrad.scalar tienen cada una una derivada local que cabe en una línea. La retropropagación las usa vía la regla de la cadena: cada nodo contribuye derivada_local · gradiente_upstream al gradiente de cada padre. Las decisiones de diseño más delicadas son convenciones (ReLU en 0 → 0; el exponente de pow debe ser constante; log(x≤0) es responsabilidad del llamador). Memoriza la tabla. A partir de aquí, el lab de la Fase 7 es solo implementación — las matemáticas están zanjadas.
Siguiente: 03-worked-backprop.md