Llevas siete capítulos escribiendo redes con numpy. Ahora te voy a enseñar la librería con la que se hacen en el trabajo, y quiero que veas que no te van a enseñar nada nuevo: te van a ahorrar teclas 🧵
Ese orden importa y por eso el capítulo está aquí y no al principio. Quien empieza por la librería aprende a llamar funciones. Quien empieza por la cuenta aprende qué hacen esas funciones, y cuando algo se rompe sabe dónde mirar.
Antes de correr nada, quédate con esta pregunta: ¿qué parte de lo que escribiste a mano crees que PyTorch hace distinto? Al final del capítulo la contestamos con números 🧠
Los mismos datos y el mismo listón
Esto es el arranque del capítulo 7, sin tocar una coma, más las tres líneas que convierten las matrices en tensores.
import numpy as np import pandas as pd import torch from torch import nn from torch.utils.data import DataLoader, TensorDataset from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder, StandardScaler URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv' # Con datos de este tamano los cuatro hilos de torch se estorban entre ellos y # tarda mas que con uno. Con datos de verdad esta linea se quita. torch.set_num_threads(1) def carga_limpia(url): v = pd.read_csv(url).drop_duplicates() v['ciudad'] = (v['ciudad'].str.strip().str.lower() .str.normalize('NFKD') .str.encode('ascii', 'ignore').str.decode('utf-8')) v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.')) for col in ['fecha', 'fecha_ultima_compra']: f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce') falta = f.isna() & v[col].notna() f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce') v[col] = f return v def prepara(v): v = v.sort_values(['cliente_id', 'fecha']).copy() v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int) v['sin_descuento'] = v['descuento'].isna().astype(int) v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int) v['precio_unitario'] = v['monto'] / v['unidades'] v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1 return v NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario', 'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero'] CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria'] datos = prepara(carga_limpia(URL)) X = datos[NUMERICAS + CATEGORICAS] y = datos['compro'] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y) logistica = Pipeline([ ('pre', ColumnTransformer([ ('num', Pipeline([('r', SimpleImputer(strategy='median')), ('e', StandardScaler())]), NUMERICAS), ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')), ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS), ])), ('mod', LogisticRegression(max_iter=1000, random_state=42)), ]).fit(X_tr, y_tr) T_tr = logistica.named_steps['pre'].transform(X_tr) T_te = logistica.named_steps['pre'].transform(X_te) objetivo = y_tr.values.astype(float).reshape(-1, 1) # Las tres lineas nuevas: una matriz de numpy se vuelve tensor y ya esta. E_tr = torch.tensor(T_tr) E_te = torch.tensor(T_te) O_tr = torch.tensor(objetivo) print('entreno con', tuple(E_tr.shape), 'y pruebo con', tuple(E_te.shape)) print('el liston a batir:', round(roc_auc_score( y_te, logistica.predict_proba(X_te)[:, 1]), 4))
entreno con (2250, 28) y pruebo con (750, 28) el liston a batir: 0.7214
Un tensor es una matriz de numpy que además se acuerda de las operaciones que le hicieron. Eso es todo lo que es, y es lo que hace posible lo que viene ahora 📦
Los gradientes que escribiste son los que saca autograd
En el capítulo 6 repartiste la culpa hacia atrás a mano, con la regla de la cadena, y te salieron cuatro matrices de derivadas. Aquí voy a calcular esas mismas cuatro de las dos maneras, con los mismos pesos de partida, y a compararlas.
def sigmoide(z): return 1 / (1 + np.exp(-z)) r = np.random.default_rng(0) W1 = r.normal(0, 0.1, (T_tr.shape[1], 8)); b1 = np.zeros(8) W2 = r.normal(0, 0.1, (8, 1)); b2 = np.zeros(1) # 1) A mano, como en el capitulo de retropropagacion. h = np.tanh(T_tr @ W1 + b1) p = sigmoide(h @ W2 + b2) d2 = (p - objetivo) / len(objetivo) d1 = (d2 @ W2.T) * (1 - h ** 2) mios = {'W1': T_tr.T @ d1, 'b1': d1.sum(axis=0), 'W2': h.T @ d2, 'b2': d2.sum(axis=0)} # 2) Con autograd: los mismos numeros, marcados con requires_grad. sueltos = {n: torch.tensor(v, requires_grad=True) for n, v in [('W1', W1), ('b1', b1), ('W2', W2), ('b2', b2)]} z = (torch.tanh(E_tr @ sueltos['W1'] + sueltos['b1']) @ sueltos['W2'] + sueltos['b2']) nn.functional.binary_cross_entropy_with_logits(z, O_tr).backward() for nombre in ['W1', 'b1', 'W2', 'b2']: suyo = sueltos[nombre].grad.numpy() print('%-3s de %-8s el mio y el de torch son iguales: %s' % (nombre, str(np.shape(mios[nombre])), np.allclose(mios[nombre], suyo)))
W1 de (28, 8) el mio y el de torch son iguales: True b1 de (8,) el mio y el de torch son iguales: True W2 de (8, 1) el mio y el de torch son iguales: True b2 de (1,) el mio y el de torch son iguales: True
Cuatro veces True 🎉
Esa línea de backward() es el capítulo entero de
retropropagación. PyTorch fue apuntando cada operación que hiciste con los
tensores, y al llamar a backward() recorrió esa lista al revés
aplicando la regla de la cadena, que es exactamente lo que hiciste tú con lápiz.
No es otra cuenta: es la misma cuenta, hecha por un programa.
Y fíjate en el detalle que hace que salga igual:
binary_cross_entropy_with_logits. Tu d2 era
(p - objetivo) / n, y esa resta tan limpia es la derivada de la
entropía cruzada con sigmoide. Si le hubiera puesto otra pérdida, las cuatro
habrían dado False, y con razón.
La red entera, en cuatro líneas
Ahora la red completa. Le copio los mismos pesos de partida para que la comparación sea justa, y le pido las mismas cinco fotos del capítulo 7.
red = nn.Sequential(nn.Linear(28, 8), nn.Tanh(), nn.Linear(8, 1)).double() with torch.no_grad(): red[0].weight.copy_(torch.tensor(W1.T)); red[0].bias.copy_(torch.tensor(b1)) red[2].weight.copy_(torch.tensor(W2.T)); red[2].bias.copy_(torch.tensor(b2)) optimizador = torch.optim.SGD(red.parameters(), lr=0.5) perdida = nn.BCEWithLogitsLoss() def mide(modelo): modelo.eval() with torch.no_grad(): return (roc_auc_score(y_tr, modelo(E_tr).numpy().ravel()), roc_auc_score(y_te, modelo(E_te).numpy().ravel())) marcas = (0, 500, 2000, 5000, 10000) for vuelta in range(max(marcas) + 1): if vuelta in marcas: entrena, prueba = mide(red) print(' vuelta %6d entrena %.4f prueba %.4f brecha %+.4f' % (vuelta, entrena, prueba, entrena - prueba)) red.train() optimizador.zero_grad() perdida(red(E_tr), O_tr).backward() optimizador.step()
vuelta 0 entrena 0.4003 prueba 0.4044 brecha -0.0041 vuelta 500 entrena 0.7343 prueba 0.7150 brecha +0.0194 vuelta 2000 entrena 0.7948 prueba 0.6651 brecha +0.1296 vuelta 5000 entrena 0.8245 prueba 0.6290 brecha +0.1955 vuelta 10000 entrena 0.8328 prueba 0.6174 brecha +0.2154
Ve a buscar la tabla del capítulo 7 y ponla al lado 😄
Es la misma, cifra por cifra, en las quince casillas. Y no es que se parezca: es que es la misma cuenta con los mismos pesos, así que tenía que salir igual. Cuando alguien te venda una librería como si fuera inteligencia, acuérdate de esta tabla.
Las tres líneas del medio son el bucle entero:
optimizador.zero_grad()borra los gradientes de la vuelta anterior, porque PyTorch los suma en vez de reemplazarlos.perdida(...).backward()es tu retropropagación.optimizador.step()es tuW -= paso * gradiente, que en el capítulo 5 escribiste a mano.
Y hay dos que no hacen nada aquí y sí harán falta enseguida:
red.train() y red.eval(). Le dicen a la red si está
entrenando o contestando, y las capas que se comportan distinto en cada caso las
miran. Ahora mismo esta red no tiene ninguna de esas capas, así que las dos dan
lo mismo. En cuanto le metamos dropout, dejan de darlo.
Y ahora lo que a mano no ibas a escribir
Hasta aquí PyTorch solo ha empatado. Lo que gana es lo que viene: los tres frenos del capítulo 8 ya están escritos, los lotes también, y hay optimizadores que a mano no te apetece programar.
torch.manual_seed(0) red2 = nn.Sequential(nn.Linear(28, 16), nn.ReLU(), nn.Dropout(0.2), nn.Linear(16, 1)).double() carga = DataLoader(TensorDataset(E_tr, O_tr), batch_size=64, shuffle=True, generator=torch.Generator().manual_seed(0)) opt = torch.optim.Adam(red2.parameters(), lr=0.01, weight_decay=1e-3) for epoca in range(1, 31): red2.train() for filas, etiquetas in carga: opt.zero_grad() perdida(red2(filas), etiquetas).backward() opt.step() if epoca in (1, 2, 3, 5, 10, 20, 30): entrena, prueba = mide(red2) print(' epoca %3d entrena %.4f prueba %.4f brecha %+.4f' % (epoca, entrena, prueba, entrena - prueba))
epoca 1 entrena 0.7134 prueba 0.7233 brecha -0.0098 epoca 2 entrena 0.7223 prueba 0.7214 brecha +0.0009 epoca 3 entrena 0.7278 prueba 0.7199 brecha +0.0079 epoca 5 entrena 0.7344 prueba 0.7161 brecha +0.0184 epoca 10 entrena 0.7506 prueba 0.7076 brecha +0.0430 epoca 20 entrena 0.7688 prueba 0.6852 brecha +0.0836 epoca 30 entrena 0.7799 prueba 0.6876 brecha +0.0923
El mejor resultado del capítulo está en la primera línea 😅
La época 1 da 0,7233 en prueba, que es el mejor número que ha sacado ninguna red en este libro sobre estos datos, y le pasa por delante al listón de la regresión logística, que era 0,7214. A partir de ahí solo empeora. Treinta épocas de entrenamiento para que la buena fuera la primera.
Y antes de que lo cuentes por ahí: esa ventaja es de 19 diezmilésimas, con una semilla y una partición. El capítulo 5 ya dejó escrito que una corrida no es un resultado, y esto es exactamente eso. La conclusión del libro no cambia: en tabla, la red empata con la logística en el mejor de los casos y cuesta cien veces más. Lo que sí es nuevo aquí es que empatar le costó una época en vez de quinientas vueltas 🐢
Eso no es un fallo de PyTorch ni de Adam. Es la misma lección del capítulo 7 con otra ropa: estos datos dan para lo que dan, y una red con 481 números que ajustar se los aprende de memoria en cuanto la dejas. Lo que cambió es la velocidad a la que llega: donde el descenso a mano necesitaba 500 vueltas por todas las filas, Adam con lotes de 64 llegó más alto en una sola pasada.
Los nombres nuevos, uno por uno, y todos son cosas que ya conoces:
| Lo que pone | Qué es, en lo que ya sabes |
|---|---|
nn.Dropout(0.2) | El freno 2 del capítulo 8, apagar neuronas al azar |
weight_decay=1e-3 | El freno 1, encoger los pesos grandes |
DataLoader | Partir las filas en lotes y barajarlas en cada época |
Adam | Descenso de gradiente con un paso distinto para cada peso, ajustado sobre la marcha |
época | Una pasada por todas las filas, o sea 36 lotes de 64 |
Y ahora sí, red2.train() y red2.eval() hacen algo:
el dropout apaga neuronas mientras entrena y no las apaga al medir. Si se te
olvida el eval(), tus números de prueba salen peores de lo que son
y encima cambian en cada corrida.
Los dos errores que salen la primera vez
Estos dos me los sé de memoria porque los he visto en todos los cursos que he dado. El primero sale al mezclar precisiones.
capa = nn.Linear(28, 8) # nace en float32 capa(E_tr) # y mis datos son float64
RuntimeError: mat1 and mat2 must have the same dtype, but got Double and Float
Double es float64 y Float es float32, que es un vocabulario de C asomando por
debajo. PyTorch crea las capas en float32 porque es la mitad de memoria y en GPU
va al doble de rápido, y numpy trabaja en float64 por costumbre. Se arregla
eligiendo un lado: .double() a la red, que es lo que hice arriba
para poder comparar con numpy, o dtype=torch.float32 a los tensores,
que es lo que harías con datos de verdad.
El segundo sale al querer sacar un número de un tensor que todavía está enganchado al grafo.
suelto = torch.tensor([2.0], requires_grad=True) (suelto * 3).numpy()
RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
Este error es un favor. Ese tensor lleva colgando el historial entero de
operaciones que hacen falta para el backward(), y si lo pasaras a
numpy sin avisar, ese historial se quedaría vivo en memoria pegado a algo que ya
no lo usa. Por eso te obliga a decirlo en voz alta con detach(), o
a envolverlo en with torch.no_grad(): como hice en
mide.
Lo de la GPU, que es una línea y no es magia
Todo lo de este capítulo corrió en el procesador de tu computadora y tardó segundos. Cuando los datos crecen, se mueve a la tarjeta gráfica así:
aparato = 'cuda' if torch.cuda.is_available() else 'cpu' red2.to(aparato) E_tr = E_tr.to(aparato)
Y ya. Lo importante es lo que no hace: una GPU no mejora tu modelo ni un decimal. Hace la misma cuenta más rápido, porque puede multiplicar matrices grandes en paralelo. Con 2.250 filas y 28 columnas no vas a notar nada, y muy probablemente vaya más lento, porque mover los datos a la tarjeta cuesta más que la cuenta que ahorras.
La regla que uso: GPU cuando el modelo no entra o el entrenamiento no cabe en una tarde. Antes de eso es un gasto y una fuente de errores nuevos.
La trampa
Una analista entrena su primera red en PyTorch, copia el bucle de un tutorial y lo adapta. La pérdida empieza en 0,69, baja un poco y después se dispara hasta dar nan. Prueba con menos paso, prueba con menos neuronas y sigue igual.
for epoca in range(30): for filas, etiquetas in carga: salida = red(filas) error = perdida(salida, etiquetas) error.backward() opt.step()
Qué está mal
Falta opt.zero_grad() al principio del lote. PyTorch suma los gradientes en vez de reemplazarlos, así que en el lote 10 el gradiente que se aplica es la suma de los diez anteriores, en el lote 100 la de los cien, y el paso efectivo crece sin parar hasta que los pesos se van a nan. La pista está en que empeora con el tiempo en vez de empeorar desde el principio. Y que sume no es un descuido de PyTorch: es lo que permite juntar varios lotes chicos en uno grande cuando no te cabe en memoria. Lo raro es útil, pero hay que saberlo.
Ejercicios
Seis. El 4 es el que hago yo antes de creerme cualquier resultado 💛
1. Cuatro optimizadores con el mismo paso
Entrena la misma red diez épocas con SGD, SGD con momento, RMSprop y Adam, todos con paso 0,01, y compara.
def arma(ancho=16, semilla=0): torch.manual_seed(semilla) return nn.Sequential(nn.Linear(28, ancho), nn.ReLU(), nn.Dropout(0.2), nn.Linear(ancho, 1)).double() def entrena_lotes(modelo, opt, epocas=10, lote=64): datos = DataLoader(TensorDataset(E_tr, O_tr), batch_size=lote, shuffle=True, generator=torch.Generator().manual_seed(0)) for _ in range(epocas): modelo.train() for filas, etiquetas in datos: opt.zero_grad() perdida(modelo(filas), etiquetas).backward() opt.step() return mide(modelo) for nombre, hace in [ ('SGD', lambda p: torch.optim.SGD(p, lr=0.01)), ('SGD momento', lambda p: torch.optim.SGD(p, lr=0.01, momentum=0.9)), ('RMSprop', lambda p: torch.optim.RMSprop(p, lr=0.01)), ('Adam', lambda p: torch.optim.Adam(p, lr=0.01))]: m = arma() a, b = entrena_lotes(m, hace(m.parameters())) print('%-13s entrena %.4f prueba %.4f' % (nombre, a, b))
SGD entrena 0.6042 prueba 0.6136 SGD momento entrena 0.7159 prueba 0.7167 RMSprop entrena 0.7571 prueba 0.6941 Adam entrena 0.7587 prueba 0.7011
Guarda esta tabla, que desmonta dos cosas a la vez 🧨
La primera: el SGD pelado con paso 0,01 casi no aprende, 0,6136. No es que sea mal optimizador, es que ese paso le queda corto. Arriba, con paso 0,5, el mismo SGD llegó a 0,7150.
La segunda es la buena: el que gana en prueba es el SGD con momento, no Adam. Adam memoriza más (0,7587 contra 0,7159 en entrenamiento) y generaliza peor. Adam es el que casi todo el mundo pone por defecto, y en problemas grandes suele merecerlo, pero aquí no. Medir cuesta cuatro líneas y te ahorra creerte una costumbre.
2. El ancho de la capa oculta
Prueba 4, 16, 64 y 256 neuronas ocultas y saca también cuántos números tiene que ajustar cada red.
for ancho in [4, 16, 64, 256]: m = arma(ancho) a, b = entrena_lotes(m, torch.optim.Adam(m.parameters(), lr=0.01)) cuantos = sum(t.numel() for t in m.parameters()) print('%4d neuronas %7d parametros entrena %.4f prueba %.4f' % (ancho, cuantos, a, b))
4 neuronas 121 parametros entrena 0.7288 prueba 0.7147 16 neuronas 481 parametros entrena 0.7587 prueba 0.7011 64 neuronas 1921 parametros entrena 0.8018 prueba 0.6724 256 neuronas 7681 parametros entrena 0.8502 prueba 0.6357
Las cuatro filas van en la misma dirección: cuanto más grande, mejor entrenamiento y peor prueba, sin una sola excepción 📉
La red de 121 parámetros le gana en prueba a la de 7.681 por casi ocho centésimas. Con 2.250 filas, 7.681 números que ajustar son demasiados, y el capítulo 8 explica por qué. La costumbre que te recomiendo: empieza por la red más chica que se te ocurra y crece solo si la prueba mejora.
3. El tamaño del lote
Entrena con lotes de 16, 64, 256 y con las 2.250 filas de golpe.
for lote in [16, 64, 256, 2250]: m = arma() a, b = entrena_lotes(m, torch.optim.Adam(m.parameters(), lr=0.01), lote=lote) print('lote %5d entrena %.4f prueba %.4f' % (lote, a, b))
lote 16 entrena 0.7708 prueba 0.6939 lote 64 entrena 0.7587 prueba 0.7011 lote 256 entrena 0.7399 prueba 0.7165 lote 2250 entrena 0.6906 prueba 0.7099
Al revés de lo que suele contarse 🙃
Se dice que los lotes chicos regularizan porque meten ruido, y aquí el lote más chico es el que peor generaliza. Lo que pasa es que con lotes de 16 hay 141 pasos por época y con lotes de 256 hay 9, así que en las mismas diez épocas la red del lote chico da quince veces más pasos y le da tiempo a memorizar mucho más. Se ve en la columna de entrenamiento: 0,7708 contra 0,7399.
El tamaño del lote no es solo el ruido: es también cuántos pasos das. Cuando compares tamaños de lote, mira si estás comparando épocas o pasos, porque no es lo mismo.
4. Guarda la red y vuelve a cargarla
Entrena tres épocas, guarda los pesos, cárgalos en una red recién hecha y comprueba que contesta exactamente igual.
import io m = arma(16) entrena_lotes(m, torch.optim.Adam(m.parameters(), lr=0.01), epocas=3) m.eval() with torch.no_grad(): antes = m(E_te).numpy().ravel() archivo = io.BytesIO() # en tu maquina, la ruta de un .pt torch.save(m.state_dict(), archivo) archivo.seek(0) otra = nn.Sequential(nn.Linear(28, 16), nn.ReLU(), nn.Dropout(0.2), nn.Linear(16, 1)).double() otra.load_state_dict(torch.load(archivo, weights_only=True)) otra.eval() with torch.no_grad(): despues = otra(E_te).numpy().ravel() print('salen identicas:', np.array_equal(antes, despues)) print('primeras tres antes :', np.round(antes[:3], 6)) print('primeras tres despues:', np.round(despues[:3], 6))
salen identicas: True primeras tres antes : [0.823251 0.833399 0.897478] primeras tres despues: [0.823251 0.833399 0.897478]
Idénticas, no parecidas 🔐
Esto es lo que hago antes de creerme que un modelo está listo, y no por
desconfianza: es que un modelo entrenado que no sabes volver a cargar es un
modelo que se muere cuando se apague el cuaderno. Fíjate en el
otra.eval(), que aquí no es opcional: sin él, el dropout seguiría
apagando neuronas al azar y las dos filas no coincidirían nunca.
5. La matriz que PyTorch guarda al revés
Mira la forma de weight en una capa
Linear y reproduce su salida multiplicando tú.
lineal = m[0] print('lineal.weight.shape:', tuple(lineal.weight.shape)) print('columnas de entrada:', T_tr.shape[1], ' neuronas:', 16) a_mano = T_tr @ lineal.weight.detach().numpy().T + lineal.bias.detach().numpy() with torch.no_grad(): suyo = lineal(E_tr).numpy() print('mi multiplicacion y la suya coinciden:', np.allclose(a_mano, suyo))
lineal.weight.shape: (16, 28) columnas de entrada: 28 neuronas: 16 mi multiplicacion y la suya coinciden: True
Tu W1 era de (28, 8) y el de PyTorch es de (16, 28), o sea al
revés 🔄
Por eso arriba, al copiar los pesos, escribí W1.T y no
W1. Es la fuente de confusión más tonta y más frecuente del
principio, y el remedio es el de este ejercicio: no te fíes de la documentación,
imprime la forma y reproduce el resultado tú.
6. Cuenta los parámetros y comprueba la cuenta
Lista los pesos con nombre y compara el total contra la fórmula escrita a mano.
m = arma(16) for nombre, t in m.named_parameters(): print('%-10s %-12s %6d' % (nombre, str(tuple(t.shape)), t.numel())) print('%-10s %-12s %6d' % ('total', '', sum(t.numel() for t in m.parameters()))) print('a mano: 28*16 + 16 + 16*1 + 1 =', 28 * 16 + 16 + 16 * 1 + 1)
0.weight (16, 28) 448 0.bias (16,) 16 3.weight (1, 16) 16 3.bias (1,) 1 total 481 a mano: 28*16 + 16 + 16*1 + 1 = 481
481 y 481 🎯
Los índices 0 y 3 son las posiciones dentro del Sequential: la 1
es la ReLU y la 2 el Dropout, y ninguna de las dos tiene nada que aprender.
Saber contar parámetros a ojo es de las cosas que más rápido te hacen entender
un modelo ajeno, y es una multiplicación y una suma por capa.
Comprueba que lo tienes
Entrenas una red en PyTorch, la pérdida baja bien pero al medir en prueba los números salen distintos en cada corrida. ¿Qué miras primero?
- Si al medir llamaste a red.eval() antes
- Si el DataLoader está barajando las filas
- Si te falta fijar la semilla del optimizador
- Si el paso es demasiado grande
Lo que te llevas
- 🔦
backward()es el capítulo de retropropagación entero, y con los mismos pesos saca los mismos gradientes que sacaste tú. - 📋 La red en PyTorch reprodujo la tabla del capítulo 7 casilla por casilla, las quince.
- 🔁 El bucle son tres líneas:
zero_grad(),backward()ystep(). Si te falta la primera, los gradientes se suman y acabas en nan. - 🏁 Con Adam y lotes de 64, el mejor resultado fue la época 1 con 0,7233. Más entrenamiento solo empeoró.
- ⚖️ En los ejercicios, el SGD con momento le gana a Adam en prueba, y la red de 121 parámetros le gana a la de 7.681.
- 🖥️ La GPU hace lo mismo más rápido. No mejora el modelo ni un decimal.
Y si de todo el capítulo te llevas una sola frase, que sea esta:
Una librería no sabe más que tú. Escribe más rápido lo que tú ya entiendes.
Si quieres ver el mismo salto en el otro sentido, en el
libro de machine learning desde
cero está scikit-learn desde el primer capítulo, y ahí el pipeline hace el
mismo papel que aquí hace nn.Sequential 🧰. El vocabulario suelto
está definido en el glosario de IA, y si el código
de este capítulo te costó más que la idea, eso es de Python y se arregla en el
libro de Python desde cero 🐍
En el capítulo 10 viene la pieza que hace posible
entrenar redes profundas, y ahora ya sabes qué es ese eval() del
que va a hablar.
Que tengas lindo día! 🌸
Practica este capítulo 📓
Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab y no hay que instalar nada.