Antes de empezar
Varios ejercicios usan las funciones y variables de la teoría: escalon, sigmoide, tanh, relu, d_sigmoide, neurona, entrenar_perceptron, adelante, contar_parametros, X4 (las cuatro esquinas) y las dos lunas (X_entr, X_val, y_entr, y_val). Abre un cuaderno nuevo en Colab, pega y ejecuta en orden los bloques de la ficha Código y después trabaja debajo. Haz primero a mano, con lápiz, lo que se pide a mano: la computadora es para comprobar.
Ejercicios
1. Una neurona a mano
Una neurona tiene pesos w = (2, −1) y sesgo b = 0,5. Llega la entrada x = (1, 3).
- Calcula la suma ponderada z.
- Calcula la salida con el escalón y con la ReLU. Sin calculadora, di si la sigmoide da más o menos que 0,5 y si la tanh da un número positivo o negativo.
- ¿Cuánto tendría que valer el sesgo para que el escalón diga 1? Da el valor más chico posible.
Respuesta
- z = 2 · 1 + (−1) · 3 + 0,5 = 2 − 3 + 0,5 = −0,5.
- Como z es negativo, el escalón da 0 y la ReLU también da 0. La sigmoide de un número negativo es menor que 0,5, y la tanh de un número negativo es negativa.
- El escalón dice 1 cuando z ≥ 0. Sin el sesgo, la suma vale 2 − 3 = −1, así que hace falta b ≥ 1: el valor más chico es b = 1, que deja z justo en 0.
x = np.array([1.0, 3.0])
w = np.array([2.0, -1.0])
for nombre, f in [("escalón", escalon), ("sigmoide", sigmoide),
("tanh", tanh), ("ReLU", relu)]:
print(f"{nombre:8s} b = 0.5: {neurona(x, w, 0.5, f):6.3f}"
f" b = 1: {neurona(x, w, 1.0, f):6.3f}")
escalón b = 0.5: 0.000 b = 1: 1.000
sigmoide b = 0.5: 0.378 b = 1: 0.500
tanh b = 0.5: -0.462 b = 1: 0.000
ReLU b = 0.5: 0.000 b = 1: 0.000
2. Contar parámetros
Calcula a mano cuántos parámetros (pesos más sesgos) tiene cada red. Después compruébalo con contar_parametros.
- 3-1: tres entradas y una neurona de salida, sin capas ocultas.
- 4-8-3.
- 64-32-10: para las imágenes de dígitos de 8 × 8 del tema 8.
- 64-32-32-10: la misma, con una capa oculta más.
- 784-256-128-10.
¿Qué es la red 1, si su salida usa la sigmoide? En la red 5, ¿qué parte de los parámetros está en la primera capa?
Respuesta
Cada capa de a entradas y m neuronas aporta a · m + m.
- 3 · 1 + 1 = 4. Con sigmoide a la salida es una regresión logística con tres características: tres pesos y un sesgo.
- (4 · 8 + 8) + (8 · 3 + 3) = 40 + 27 = 67.
- (64 · 32 + 32) + (32 · 10 + 10) = 2080 + 330 = 2410.
- La de arriba más una capa de 32 a 32: 2410 + (32 · 32 + 32) = 2410 + 1056 = 3466.
- (784 · 256 + 256) + (256 · 128 + 128) + (128 · 10 + 10) = 200 960 + 32 896 + 1290 = 235 146. La primera capa tiene 200 960, cerca del 85 % del total.
for tamanos in [[3, 1], [4, 8, 3], [64, 32, 10], [64, 32, 32, 10],
[784, 256, 128, 10]]:
print(tamanos, "->", contar_parametros(tamanos))
primera = 784 * 256 + 256
print(f"Primera capa de la red 5: {primera}, el "
f"{primera / contar_parametros([784, 256, 128, 10]):.0%}")
[3, 1] -> 4
[4, 8, 3] -> 67
[64, 32, 10] -> 2410
[64, 32, 32, 10] -> 3466
[784, 256, 128, 10] -> 235146
Primera capa de la red 5: 200960, el 85%
3. Un paso hacia adelante a mano
Una red 2-2-1 tiene ReLU en la capa oculta y ninguna activación a la salida (es una red de regresión). Sus pesos son estos; recuerda que cada columna de W₁ son los pesos de una neurona oculta:
- W₁ = [[1, −1], [2, 1]] y b₁ = (0, −2);
- W₂ = [[0,5], [3]] y b₂ = (1).
Calcula a mano, para la entrada x = (1, 2): las sumas z de las dos neuronas ocultas, sus salidas después de la ReLU y la salida de la red. Comprueba con adelante.
Respuesta
- Neurona oculta 1 (columna (1, 2)): z = 1 · 1 + 2 · 2 + 0 = 5, y ReLU(5) = 5.
- Neurona oculta 2 (columna (−1, 1)): z = 1 · (−1) + 2 · 1 − 2 = −1, y ReLU(−1) = 0.
- Salida: 0,5 · 5 + 3 · 0 + 1 = 3,5.
La neurona 2 quedó «apagada»: su peso de salida, 3, no influye en esta respuesta, aunque sea el más grande.
W1_e = np.array([[1.0, -1.0],
[2.0, 1.0]])
b1_e = np.array([0.0, -2.0])
W2_e = np.array([[0.5],
[3.0]])
b2_e = np.array([1.0])
x_e = np.array([[1.0, 2.0]]) # una fila: un ejemplo
print("Ocultas, z:", x_e @ W1_e + b1_e)
print("Ocultas, ReLU:", relu(x_e @ W1_e + b1_e))
print("Salida:", adelante(x_e, [(W1_e, b1_e), (W2_e, b2_e)]))
Ocultas, z: [[ 5. -1.]]
Ocultas, ReLU: [[5. 0.]]
Salida: [[3.5]]
4. Predecir los shapes
Tienes 8 ejemplos con 3 características cada uno, y una red 3-5-2 (sin preocuparte por la activación).
- ¿Qué forma tienen
X,W1,b1, la salida de la capa oculta,W2,b2y la salida de la red? - Un compañero creó
W1con forma (5, 3), «porque la capa tiene 5 neuronas y 3 entradas». ¿Qué pasa al calcularX @ W1?
Respuesta
X: (8, 3).W1: (3, 5).b1: (5,). La capa oculta: (8, 3) @ (3, 5) da (8, 5).W2: (5, 2).b2: (2,). La salida: (8, 5) @ (5, 2) da (8, 2): una fila por ejemplo y una columna por neurona de salida.
rng = np.random.default_rng(3)
X8 = rng.normal(size=(8, 3))
capas_e = [(rng.normal(size=(3, 5)), np.zeros(5)),
(rng.normal(size=(5, 2)), np.zeros(2))]
salida8 = adelante(X8, capas_e, ver=True)
Capa 1: (8, 3) @ (3, 5) + (5,) -> (8, 5)
Capa 2: (8, 5) @ (5, 2) + (2,) -> (8, 2)
- Falla: en (8, 3) @ (5, 3) los números del medio, 3 y 5, no coinciden. NumPy lo dice con un mensaje largo; la parte importante está al final, entre paréntesis.
W1_mal = rng.normal(size=(5, 3))
H8 = X8 @ W1_mal
Traceback (most recent call last):
...
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 5 is different from 3)
Su forma (5, 3) es la que usa PyTorch por dentro (una fila por neurona), que entonces multiplica por la transpuesta. No hay una convención «correcta»: lo que importa es que los números del medio coincidan. Cuando aparezca este error, imprime el shape de cada lado.
5. El perceptrón con Y, O y XOR
Entrena entrenar_perceptron con las cuatro esquinas X4 y tres reglas distintas: Y (la luz se prende solo si los dos interruptores están en 1), O (si al menos uno está en 1) y XOR. Usa 20 épocas y tasa 1. ¿En cuáles llega a cero errores? Para las que sí, escribe la recta que encontró y comprueba que deja cada esquina del lado correcto.
Respuesta
reglas = {"Y": np.array([0, 0, 0, 1]),
"O": np.array([0, 1, 1, 1]),
"XOR": np.array([0, 1, 1, 0])}
for nombre, y_regla in reglas.items():
# Con tasa 1 y entradas 0 o 1, todas las cuentas dan enteros
w_r, b_r, err = entrenar_perceptron(X4, y_regla, epocas=20,
tasa=1.0)
print(f"{nombre:3s} errores: {err}")
print(f" w = {w_r} b = {b_r}"
f" predice {escalon(X4 @ w_r + b_r)}")
Y errores: [2, 3, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
w = [2. 1.] b = -3.0 predice [0 0 0 1]
O errores: [2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
w = [1. 1.] b = -1.0 predice [0 1 1 1]
XOR errores: [3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
w = [-1. 0.] b = 0.0 predice [1 1 0 0]
- Con Y llega a cero errores en la sexta época, con la recta 2·x₁ + x₂ − 3 = 0. Comprobación: en (0, 0), z = −3; en (0, 1), −2; en (1, 0), −1: las tres dan 0. En (1, 1), z = 0, y el escalón la cuenta como 1 porque pide z ≥ 0.
- Con O llega a cero en la cuarta época, con x₁ + x₂ − 1 = 0. En (0, 0), z = −1 y da 0; en (0, 1) y (1, 0), z = 0 y da 1; en (1, 1), z = 1 y da 1.
- Con XOR nunca baja de 3 errores por época, y desde la tercera se equivoca en las cuatro esquinas en cada pasada: cada corrección deshace la anterior y los pesos dan vueltas en un ciclo. Al final predice [1 1 0 0], que acierta solo dos de cuatro. Es lo que demostramos en la teoría: no existe una recta que sirva.
Fíjate en que Y y O quedaron con esquinas justo sobre la recta. El perceptrón se detiene apenas acierta todo, aunque sea por poco; no busca la recta «más segura». (Esa idea, dejar el mayor margen posible, es la de las máquinas de vectores de soporte del tema 12.)
6. Activaciones que se saturan
- La derivada de la sigmoide es σ(z) · (1 − σ(z)). Sin calcular, explica por qué nunca pasa de 0,25. (Pista: ¿cuándo es más grande el producto de dos números entre 0 y 1 que suman 1?)
- En una red con 5 capas sigmoide, imagina que el gradiente se multiplica por la derivada de cada capa. Calcula el producto si todas las neuronas están en z = 0 y si todas están en z = 3. Haz lo mismo con la ReLU en z = 3.
- ¿Por qué una tanh en z = 0 es mejor que una sigmoide en z = 0 para este problema?
Respuesta
- σ y 1 − σ son dos números entre 0 y 1 que suman 1. Su producto es máximo cuando son iguales, 0,5 y 0,5, y entonces vale 0,25. Eso pasa en z = 0; lejos del 0, uno de los dos se acerca a 0 y el producto también.
- y 3. Con el código:
for z in [0.0, 3.0]:
print(f"sigmoide, z = {z}: derivada {d_sigmoide(z):.4f},"
f" a la quinta {d_sigmoide(z) ** 5:.1e}")
print(f"ReLU, z = 3.0: derivada {d_relu(3.0):.0f},"
f" a la quinta {d_relu(3.0) ** 5:.0f}")
print(f"tanh, z = 0.0: derivada {d_tanh(0.0):.0f}")
sigmoide, z = 0.0: derivada 0.2500, a la quinta 9.8e-04
sigmoide, z = 3.0: derivada 0.0452, a la quinta 1.9e-07
ReLU, z = 3.0: derivada 1, a la quinta 1
tanh, z = 0.0: derivada 1
Con z = 0, cada capa sigmoide multiplica por 0,25 y cinco capas dejan menos de una milésima (9,8 · 10⁻⁴). Si las neuronas están saturadas, en z = 3, la derivada baja a 0,045 y el producto queda en dos diezmillonésimas (1,9 · 10⁻⁷): las primeras capas casi no se enteran de nada. Con la ReLU en z = 3 la derivada es 1 y el producto sigue en 1.
La tanh tiene derivada 1 en z = 0, cuatro veces la de la sigmoide: cerca del 0 no achica el gradiente. Pero también se satura lejos del 0, así que no resuelve el problema del todo; la ReLU, en los positivos, nunca se satura.
7. Capas sin activación, en la práctica
MLPClassifier acepta activation="identity": neuronas ocultas que no aplican ninguna activación. Entrena en las dos lunas un MLP con dos capas ocultas de 10 neuronas con activation="identity" y otro igual con la ReLU ("relu"). Usa solver="lbfgs", max_iter=2000 y random_state=0. Compara los aciertos en validación con el 85 % de la logística del tema 8. ¿Qué forma tiene la frontera del primero, sin dibujarla?
Respuesta
for activacion in ["identity", "relu"]:
red_e = MLPClassifier(hidden_layer_sizes=(10, 10),
activation=activacion, solver="lbfgs",
max_iter=2000, random_state=0)
red_e.fit(X_entr, y_entr)
print(f"{activacion:8s} entrenamiento "
f"{red_e.score(X_entr, y_entr):.2f}"
f" validación {red_e.score(X_val, y_val):.2f}")
identity entrenamiento 0.83 validación 0.85
relu entrenamiento 0.91 validación 0.97
Sin activación, la red de dos capas ocultas acierta exactamente lo mismo que la logística del tema 8 (85 % en validación) y lo mismo que el MLP de una sola neurona oculta de la teoría: su frontera es una recta. Las dos capas de 10 neuronas, con sus 151 parámetros, se juntan en una sola capa lineal. Con la ReLU, la misma arquitectura sube a 97 %.
8. XOR con ReLU
Busca pesos para una red 2-2-1 con ReLU en la capa oculta y sin activación a la salida, de modo que la salida sea 0 en (0, 0) y en (1, 1), y 1 en (0, 1) y en (1, 0). Pista: ReLU(x₁ − x₂) vale 1 solo en una de las cuatro esquinas. ¿Qué función de x₁ y x₂ calcula tu red en todo el plano?
Respuesta
Una neurona oculta calcula ReLU(x₁ − x₂), que vale 1 solo en (1, 0); la otra, ReLU(x₂ − x₁), que vale 1 solo en (0, 1). La salida las suma, con pesos 1 y 1 y sesgo 0:
W1_r = np.array([[1.0, -1.0],
[-1.0, 1.0]]) # columnas: x1 - x2 y x2 - x1
b1_r = np.array([0.0, 0.0])
W2_r = np.array([[1.0],
[1.0]])
b2_r = np.array([0.0])
print(adelante(X4, [(W1_r, b1_r), (W2_r, b2_r)]).ravel())
[0. 1. 1. 0.]
En todo el plano, la red calcula ReLU(x₁ − x₂) + ReLU(x₂ − x₁) = |x₁ − x₂|, la distancia entre las dos entradas: 0 cuando son iguales y 1 cuando son distintas. Con dos neuronas ReLU y una suma resolviste XOR, sin ninguna sigmoide. Hay muchas otras soluciones; cualquiera que dé esas cuatro salidas está bien.
Mini-proyecto: el ancho y la profundidad
Vas a entrenar redes MLPClassifier de distintos tamaños en los dígitos escritos a mano del tema 8 y a medir qué cambia al hacerlas más anchas o más profundas.
Los datos. load_digits (sin descarga), con los píxeles divididos por 16 y la misma división del tema 8: train_test_split(..., test_size=0.3, random_state=0).
Pasos.
- Una referencia. Entrena
LogisticRegression(max_iter=1000)y anota los aciertos en validación. Es una red sin capas ocultas: ¿cuántos parámetros tiene? (64 entradas y 10 clases.) - El ancho. Entrena MLP con una capa oculta de 4, 16, 64 y 256 neuronas (
hidden_layer_sizes=(64,), etc.), conmax_iter=2000yrandom_state=0. Para cada uno, anota los aciertos al entrenar y en validación, y el número de parámetros (concontar_parametroso sumando los.sizedered.coefs_yred.intercepts_). - La profundidad. Con 64 neuronas por capa, prueba una, dos y tres capas ocultas:
(64,),(64, 64)y(64, 64, 64). - Gráfico. Dibuja los aciertos en validación frente al número de parámetros, con el eje horizontal en escala logarítmica (
plt.xscale("log")). Resalta con"C1"la red que elegirías. - Conclusión. En tres o cuatro frases: ¿cuánto ganaste respecto de la logística? ¿A partir de qué tamaño dejó de valer la pena crecer? ¿Qué preferirías, si dos redes empatan en validación?
Resultados de referencia, para comprobar los tuyos
Este código hace los pasos 1 a 3 sin gráfico; tus números deberían coincidir.
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
digitos = load_digits()
Xd_entr, Xd_val, yd_entr, yd_val = train_test_split(
digitos.data / 16, digitos.target, test_size=0.3, random_state=0)
logistica = LogisticRegression(max_iter=1000).fit(Xd_entr, yd_entr)
print(f"Logística {contar_parametros([64, 10]):6d} parámetros"
f" valid. {logistica.score(Xd_val, yd_val):.3f}")
for ocultas in [(4,), (16,), (64,), (256,), (64, 64), (64, 64, 64)]:
red_d = MLPClassifier(hidden_layer_sizes=ocultas, max_iter=2000,
random_state=0).fit(Xd_entr, yd_entr)
n_par = contar_parametros([64, *ocultas, 10])
print(f"MLP {str(ocultas):14s} {n_par:6d} parámetros"
f" entren. {red_d.score(Xd_entr, yd_entr):.3f}"
f" valid. {red_d.score(Xd_val, yd_val):.3f}")
Logística 650 parámetros valid. 0.961
MLP (4,) 310 parámetros entren. 0.967 valid. 0.907
MLP (16,) 1210 parámetros entren. 0.998 valid. 0.967
MLP (64,) 4810 parámetros entren. 1.000 valid. 0.980
MLP (256,) 19210 parámetros entren. 1.000 valid. 0.978
MLP (64, 64) 8970 parámetros entren. 1.000 valid. 0.974
MLP (64, 64, 64) 13130 parámetros entren. 1.000 valid. 0.972
Con 4 neuronas ocultas puede aparecer un aviso ConvergenceWarning: el optimizador llegó a max_iter sin estabilizarse del todo. No es un error, y aquí no cambia la conclusión.
- La logística ya acierta el 96 % con 650 parámetros. Una red demasiado angosta, con 4 neuronas ocultas, tiene menos parámetros (310) y le va peor (91 %): la capa oculta es un cuello de botella que obliga a resumir 64 píxeles en 4 números.
- Al ensanchar, la validación sube hasta 98 % con 64 neuronas. Con 256 ya no mejora (97,8 %), aunque tiene cuatro veces más parámetros.
- Al profundizar con 64 neuronas por capa, la validación no mejora (97,4 % y 97,2 %). Para un problema así de chico, una capa oculta alcanza. Las diferencias de unas décimas son unos pocos dígitos de 540: no hay que leerles demasiado.
- Todas las redes de 16 neuronas o más aciertan casi el 100 % al entrenar: la diferencia está solo en validación.
- Si dos redes empatan en validación, conviene la más chica: es más rápida, ocupa menos y tiene menos capacidad para memorizar.
- La red se entrena con descenso de gradiente sobre la entropía cruzada (
MLPClassifierusa una versión mejorada, Adam, que verás en el tema 19). Cómo calcula los gradientes de cada capa es el tema 17.
Errores comunes
- Olvidar la activación entre capas. Sin ella, todas las capas se juntan en una sola capa lineal y la red no puede trazar más que rectas (ejercicio 7).
- Confundir filas y columnas de la matriz de pesos. Con
X @ W,Wtiene una fila por entrada y una columna por neurona. Si el producto falla, imprime losshapes: los números del medio tienen que coincidir (ejercicio 4). - Olvidar el sesgo. Sin sesgo, todas las rectas de una neurona pasan por el origen; con la regla Y del ejercicio 5, por ejemplo, no habría cómo separar (1, 1) de las demás esquinas.
- Usar la sigmoide en todas las capas ocultas de una red profunda. Su derivada vale como mucho 0,25 y el gradiente se desvanece. En las capas ocultas, empieza con la ReLU.
- Poner una activación que no corresponde a la salida. Una ReLU o una sigmoide en la salida de una regresión limita los valores que puede predecir; para regresión, la última capa va sin activación.
- Creer que más neuronas siempre es mejor. Con 100 neuronas ocultas, la red de las lunas memorizó el entrenamiento y bajó en validación. El tamaño se elige mirando la validación.
- Tomarse al pie de la letra la analogía con el cerebro. Una neurona artificial es una suma ponderada y una función; las neuronas biológicas son mucho más complejas.
¿Lo lograste?
Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.
- Calcular a mano la salida de una neurona (ejercicios 1 y 3).
- Entrenar un perceptrón con su regla y explicar por qué no resuelve XOR (ejercicio 5 y la actividad).
- Comparar las activaciones sigmoide, tanh y ReLU por su forma, rango y derivada (ejercicio 6).
- Escribir el paso hacia adelante de un MLP en NumPy y predecir los
shapes (ejercicios 3, 4 y 8). - Contar los parámetros de una red (ejercicio 2 y paso 2 del mini-proyecto).
- Explicar por qué apilar capas lineales sin activación no sirve (ejercicio 7).