English · Español
Lab 01 — Autograd a mano para nn.Linear(64, 600)¶
Derivas a mano los gradientes de un
Linear(64, 600)— los tres:∂L/∂x,∂L/∂W,∂L/∂b. Luego ejecutasloss.backward()en PyTorch y comparas. El umbral es 1e-7 a fp32. Si no cuadra, lo arreglas. Esta es la práctica que cierra la convicción "PyTorch autograd es exactamente lo que construimos en Fase ⅞ — más grande, no diferente".
Objetivo¶
Deriva a mano las fórmulas de backward para y = linear(x, W, b) = x @ W.T + b seguidas de una pérdida escalar L = (y - target).pow(2).sum() / 2. Computa ∂L/∂x, ∂L/∂W, ∂L/∂b analíticamente, después verifica contra el autograd de PyTorch a fp32 dentro de un 1e-7 elemento a elemento.
Setup¶
- Fase ⅞ (autograd escalar/tensorial) y Fase 04 (teoría de cálculo).
- Las formas del forward:
x ∈ R^(2 × 64),W ∈ R^(600 × 64),b ∈ R^(600),y ∈ R^(2 × 600),target ∈ R^(2 × 600),L ∈ R.
Las matemáticas¶
Forward:
Pérdida:
Regla de la cadena:
- \(\dfrac{\partial L}{\partial x} = \dfrac{\partial L}{\partial y} \cdot \dfrac{\partial y}{\partial x} = (y - t) W\) (forma
(2, 64)) - \(\dfrac{\partial L}{\partial W} = (y - t)^T x\) (forma
(600, 64)) - \(\dfrac{\partial L}{\partial b} = \sum_i (y_i - t_i)\) (forma
(600,))
Éstas son las fórmulas que el nodo AddmmBackward0 computa internamente. El laboratorio lo verifica.
Tareas¶
Parte A — Forward + backward analítico¶
import torch
torch.manual_seed(42)
x = torch.randn(2, 64, requires_grad=True)
W = torch.randn(600, 64, requires_grad=True)
b = torch.randn(600, requires_grad=True)
target = torch.randn(2, 600)
y = torch.nn.functional.linear(x, W, b)
loss = 0.5 * ((y - target) ** 2).sum()
# Analytical gradients (no autograd):
with torch.no_grad():
dy = y - target # (2, 600)
dx_manual = dy @ W # (2, 64)
dW_manual = dy.T @ x # (600, 64)
db_manual = dy.sum(dim=0) # (600,)
Parte B — Autograd de PyTorch¶
loss.backward()
print("dx max-err:", (x.grad - dx_manual).abs().max().item())
print("dW max-err:", (W.grad - dW_manual).abs().max().item())
print("db max-err:", (b.grad - db_manual).abs().max().item())
Los tres errores máximos deberían ser < 1e-5 a fp32 (y típicamente < 1e-7).
Parte C — Recorre la cadena grad_fn¶
node = loss.grad_fn
while node is not None:
print(type(node).__name__, [t for t in node.next_functions])
nexts = [t[0] for t in node.next_functions if t[0] is not None]
node = nexts[0] if nexts else None
Salida esperada (aproximada; los nombres varían según versión):
DivBackward0 [(SumBackward0, 0)] # the 0.5 *
SumBackward0 [(PowBackward0, 0)]
PowBackward0 [(SubBackward0, 0)]
SubBackward0 [(AddmmBackward0, 0), (None, 0)]
AddmmBackward0 [(AccumulateGrad, 0), (AccumulateGrad, 0), (TBackward0, 0)]
Identifica:
- AddmmBackward0 — el nodo de matmul-y-suma-de-sesgo.
- AccumulateGrad — nodos hoja que acumulan gradientes en .grad.
- TBackward0 — la transposición implícita W → W^T que linear insertó.
Parte D — Verifica la afirmación 1e-7 a fp32¶
torch.manual_seed(123)
errors = []
for _ in range(20):
x = torch.randn(2, 64, requires_grad=True)
W = torch.randn(600, 64, requires_grad=True)
b = torch.randn(600, requires_grad=True)
target = torch.randn(2, 600)
y = torch.nn.functional.linear(x, W, b)
loss = 0.5 * ((y - target) ** 2).sum()
with torch.no_grad():
dy = y - target
dx_m, dW_m, db_m = dy @ W, dy.T @ x, dy.sum(dim=0)
loss.backward()
errors.append((
(x.grad - dx_m).abs().max().item(),
(W.grad - dW_m).abs().max().item(),
(b.grad - db_m).abs().max().item(),
))
import numpy as np
e = np.array(errors)
print("dx: max", e[:, 0].max(), " median", float(np.median(e[:, 0])))
print("dW: max", e[:, 1].max(), " median", float(np.median(e[:, 1])))
print("db: max", e[:, 2].max(), " median", float(np.median(e[:, 2])))
Esperado: las medianas son ~ 1e-7, los máximos son < 1e-5. La razón por la que no es 0.0 aunque la fórmula es idéntica: el orden de la suma en coma flotante difiere entre addmm de PyTorch y tu @. Documenta esto.
Parte E — Repite a fp16, observa la degradación¶
Esperado: los errores son ahora 1e-3 o peores. El problema del orden de la suma se amplifica en baja precisión. Ésta es la razón canónica por la que el entrenamiento fp16 necesita loss-scaling y precisión mixta (la Fase 18 lo mencionó; la Fase 26 se zambulle en ello).
Parte F — Escribe el informe¶
experiments/25-autograd-by-hand/REPORT.md:
- Las matemáticas (renderizadas en LaTeX, tres fórmulas).
- La tabla de errores de la Parte D (20 ejecuciones, mediana + máximo por gradiente).
- El resultado fp16 de la Parte E con una explicación de 2 frases sobre por qué la precisión importa.
- La impresión de la cadena
grad_fnde la Parte C. - Un párrafo: "El autograd de PyTorch calculó las mismas fórmulas que escribí a mano. La desviación a fp32 es
< 1e-5, dominada por diferencias en el orden de suma. A fp16 la desviación es1e-3, lo bastante grande como para afectar a la convergencia — éste es el modo de fallo que el entrenamiento en precisión mixta aborda."
Entregable¶
experiments/25-autograd-by-hand/:
- REPORT.md — los puntos anteriores.
- errors.csv — la tabla de errores 20-ejecuciones × 3-gradientes.
- manifest.json.
Aceptación¶
- fp32: las 20 ejecuciones × 3 gradientes tienen un error máximo
< 1e-5. - fp16: al menos un gradiente tiene un error máximo
> 1e-3(prueba la sensibilidad de precisión). - La impresión de la cadena grad_fn identifica
AddmmBackward0,AccumulateGrad, y la transposición. - El párrafo de interpretación atribuye correctamente la discrepancia fp32 al orden de suma.
Pitfalls¶
- Dirección de transposición errónea.
linear(x, W, b) = x @ W.T + b. Si escribesx @ W + b, las formas no encajarán (x: (2,64),W: (600,64), no se puede hacer matmul). - Olvidarse del
0.5 *en la pérdida. Entonces∂L/∂y = 2(y - t), no(y - t). Casa la constante con la pérdida. - Recomputar
loss.backward()sin poner a cero.grad. Los gradientes se acumulan; la segunda llamada duplica la respuesta. Usax.grad.zero_()entre ejecuciones, o reconstruye los tensores frescos. - fp16 produciendo
nan. Magnitudes mayores detargetdesbordan al cuadrado. Escalatargetpor 0.1 si vesinf. - Comparar
dW.TcondW. PyTorch guardaW.graden el mismo layout queW. Si tudW_manualse calcula comox.T @ dy(forma(64, 600)), necesitarás.T. Comprueba formas antes de comparar.
Stretch¶
- Añade una segunda capa lineal
y2 = linear(y, W2, b2)y deriva el backward completo de dos capas. Compara contra autograd. - Reemplaza la pérdida cuadrada por cross-entropy + softmax sobre las 600 clases de gramática. Deriva
∂L/∂y = softmax(y) - one_hot(target). Ésta es la fórmula contra la que la Fase 18 entrena realmente. - Usa
torch.autograd.gradchecken lugar de diferencias finitas para verificación.
Siguiente lab: lab/02-custom-op.md.