De la neurona a la red: perceptrón y MLP
Cuaderno del tema. La teoría y los ejercicios con todo su código, listos para ejecutar en Colab sin instalar nada.
Los 12 bloques de código de la teoría, en el mismo orden. Si prefieres armar tu propio cuaderno, cópialos y córrelos en orden; en la ficha Teoría está la explicación de cada uno.
Una neurona
import numpy as np
import matplotlib.pyplot as plt
def escalon(z):
"""1 si z >= 0, y 0 si no."""
return np.where(z >= 0, 1, 0)
def sigmoide(z):
"""La del tema 8: aplasta cualquier número a un valor entre 0 y 1."""
return 1 / (1 + np.exp(-z))
def neurona(x, w, b, activacion):
"""Suma ponderada más b, pasada por una activación."""
z = np.dot(w, x) + b
return activacion(z)
x = np.array([2.0, -1.0]) # dos entradas
w = np.array([0.5, 1.5]) # un peso por entrada
b = 0.25 # el término independiente
print("z =", np.dot(w, x) + b)
print("Con escalón: ", neurona(x, w, b, escalon))
print("Con sigmoide:", neurona(x, w, b, sigmoide).round(3))El perceptrón
rng = np.random.default_rng(42)
# 50 puntos alrededor de cada centro (datos sintéticos)
X_sep = np.vstack([rng.normal([-1, -1], 0.6, size=(50, 2)),
rng.normal([1.2, 1.0], 0.6, size=(50, 2))])
y_sep = np.array([0] * 50 + [1] * 50)
orden = rng.permutation(100) # mezclamos las dos clases
X_sep, y_sep = X_sep[orden], y_sep[orden]
def entrenar_perceptron(X, y, epocas=15, tasa=0.1):
"""La regla del perceptrón. Devuelve w, b y los errores por época."""
w = np.zeros(X.shape[1])
b = 0.0
errores = []
for epoca in range(epocas):
fallos = 0
for xi, yi in zip(X, y):
pred = escalon(np.dot(w, xi) + b)
# Si acierta, yi - pred vale 0 y nada cambia
w = w + tasa * (yi - pred) * xi
b = b + tasa * (yi - pred)
fallos += int(pred != yi)
errores.append(fallos)
return w, b, errores
w_sep, b_sep, errores_sep = entrenar_perceptron(X_sep, y_sep)
print("Errores en cada época:", errores_sep)
print("w =", w_sep.round(3), " b =", round(b_sep, 3))esquinas = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
clase_xor = np.array([0, 1, 1, 0]) # 1 si los interruptores difieren
cual = rng.integers(0, 4, size=100) # a qué esquina va cada punto
X_xor = esquinas[cual] + rng.normal(0, 0.15, size=(100, 2))
y_xor = clase_xor[cual]
w_x, b_x, errores_xor = entrenar_perceptron(X_xor, y_xor)
print("Errores en cada época:", errores_xor)def dibujar_clases(X, y, ax=None, s=30):
"""Clase 0: círculos de color C0. Clase 1: cuadrados de color C1."""
ax = ax or plt.gca()
ax.scatter(X[y == 0, 0], X[y == 0, 1], s=s, color="C0",
marker="o", label="Clase 0")
ax.scatter(X[y == 1, 0], X[y == 1, 1], s=s, color="C1",
marker="s", label="Clase 1")
def regiones(ax, predecir, x_lim, y_lim):
"""Pinta de C0 o C1, muy claro, la clase que predice cada lugar."""
A, B = np.meshgrid(np.linspace(*x_lim, 200), np.linspace(*y_lim, 200))
zona = predecir(np.column_stack([A.ravel(), B.ravel()]))
ax.contourf(A, B, zona.reshape(A.shape), levels=[-0.5, 0.5, 1.5],
colors=["C0", "C1"], alpha=0.2)
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8))
casos = [(X_sep, y_sep, w_sep, b_sep, errores_sep, (-3, 3.2),
"Separables"),
(X_xor, y_xor, w_x, b_x, errores_xor, (-0.6, 1.6), "XOR")]
for col, (X_c, y_c, w_c, b_c, err, lim, nombre) in enumerate(casos):
arriba, abajo = ejes[0, col], ejes[1, col]
regiones(arriba, lambda P: escalon(P @ w_c + b_c), lim, lim)
dibujar_clases(X_c, y_c, ax=arriba, s=8)
arriba.set_title(nombre)
abajo.plot(range(1, 16), err, color="C0", marker="o", markersize=4)
abajo.set_ylim(-3, 60)
abajo.set_xlabel("Época")
ejes[1, 0].set_ylabel("Errores")
plt.show()Funciones de activación
def tanh(z):
return np.tanh(z)
def relu(z):
"""Los positivos pasan igual; los negativos se vuelven 0."""
return np.maximum(0, z)
def d_sigmoide(z):
return sigmoide(z) * (1 - sigmoide(z))
def d_tanh(z):
return 1 - np.tanh(z) ** 2
def d_relu(z):
# En z = 0 la derivada no existe; por convención se usa 0
return np.where(z > 0, 1.0, 0.0)
activaciones = [("sigmoide", sigmoide, d_sigmoide),
("tanh", tanh, d_tanh),
("ReLU", relu, d_relu)]
for nombre, f, df in activaciones:
print(f"{nombre:8s} f(-5) = {f(-5.0):5.2f} f(0) = {f(0.0):.2f}"
f" f(5) = {f(5.0):.2f} f'(0) = {df(0.0):.2f}"
f" f'(5) = {df(5.0):.4f}")zs = np.linspace(-5, 5, 400)
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8))
for eje, (nombre, f, df) in zip(ejes.flat, activaciones):
eje.plot(zs, f(zs), color="C0", label="f(z)")
eje.plot(zs, df(zs), color="C2", linestyle="--", label="derivada")
eje.set_title(nombre)
eje.set_ylim(-1.2, 2.2)
ejes[0, 0].scatter([0], [0.25], color="C1", s=60, zorder=3)
ejes[0, 0].legend(loc="upper left", fontsize=10)
# La derivada de la sigmoide vale como mucho 0,25: tras n capas,
# el gradiente puede quedar multiplicado por 0,25 ** n
capas = np.arange(1, 11)
ejes[1, 1].semilogy(capas, 0.25 ** capas, color="C0", marker="o",
markersize=4)
ejes[1, 1].set_title("0,25 elevado a n")
ejes[1, 1].set_xlabel("Capas n")
plt.show()
print(f"0.25 ** 10 = {0.25 ** 10:.1e}")De la neurona a la red
X4 = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y4 = np.array([0, 1, 1, 0]) # XOR
# Cada columna de W1 son los pesos de una neurona oculta
W1 = np.array([[6.0, 6.0],
[6.0, 6.0]])
b1 = np.array([-3.0, -9.0]) # neurona 1: «O»; neurona 2: «Y»
W2 = np.array([[8.0],
[-8.0]]) # «h1 y no h2»
b2 = np.array([-4.0])
H = sigmoide(X4 @ W1 + b1) # (4, 2) @ (2, 2) -> (4, 2)
salida = sigmoide(H @ W2 + b2) # (4, 2) @ (2, 1) -> (4, 1)
print("x1 x2 | h1 h2 | salida | XOR")
for xi, hi, si, yi in zip(X4, H, salida[:, 0], y4):
print(f" {xi[0]} {xi[1]} | {hi[0]:.3f} {hi[1]:.3f} |"
f" {si:.3f} | {yi}")def pintar_salida(ax, A, B, P):
"""Bandas coral si P >= 0.5 y tinta si no, más fuertes lejos de 0.5."""
cortes = [0, 0.1, 0.25, 0.4, 0.5, 0.6, 0.75, 0.9, 1]
for bajo, alto in zip(cortes[:-1], cortes[1:]):
color = "C1" if bajo >= 0.5 else "C0"
lejos = max(abs(bajo - 0.5), abs(alto - 0.5))
ax.contourf(A, B, P, levels=[bajo, alto], colors=color,
alpha=0.6 * lejos)
A, B = np.meshgrid(np.linspace(-0.5, 1.5, 200), np.linspace(-0.5, 1.5, 200))
plano = np.column_stack([A.ravel(), B.ravel()])
H_plano = sigmoide(plano @ W1 + b1)
sal_plano = sigmoide(H_plano @ W2 + b2)
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8))
paneles = [(ejes[0, 0], H_plano[:, 0], "Oculta 1: O"),
(ejes[0, 1], H_plano[:, 1], "Oculta 2: Y"),
(ejes[1, 1], sal_plano[:, 0], "Salida: XOR")]
for eje, valores, titulo in paneles:
pintar_salida(eje, A, B, valores.reshape(A.shape))
dibujar_clases(X4, y4, ax=eje, s=50)
eje.set_title(titulo)
# Lo que ve la neurona de salida: los cuatro casos en el espacio (h1, h2)
dibujar_clases(H, y4, ax=ejes[1, 0], s=50)
h = np.linspace(-0.1, 1.1, 2)
# Frontera de la salida: 8·h1 - 8·h2 - 4 = 0, o sea h2 = h1 - 0.5
ejes[1, 0].plot(h, h - 0.5, color="C0")
ejes[1, 0].set_xlim(-0.1, 1.1)
ejes[1, 0].set_ylim(-0.1, 1.1)
ejes[1, 0].set_title("Espacio oculto")
ejes[1, 0].set_xlabel("$h_1$")
ejes[1, 0].set_ylabel("$h_2$")
plt.show()def adelante(X, capas, activacion=relu, ver=False):
"""Paso hacia adelante de un MLP. capas: una lista de pares (W, b)."""
A = X
for i, (W, b) in enumerate(capas):
Z = A @ W + b
if ver:
print(f"Capa {i + 1}: {A.shape} @ {W.shape} + {b.shape}"
f" -> {Z.shape}")
ultima = i == len(capas) - 1
# Las capas ocultas llevan activación; la última sale tal cual
A = Z if ultima else activacion(Z)
return A
rng = np.random.default_rng(0)
# Una red 2-4-1 con pesos al azar: de 2 entradas a 4 ocultas, y a 1
red = [(rng.normal(size=(2, 4)), np.zeros(4)),
(rng.normal(size=(4, 1)), np.zeros(1))]
X5 = rng.normal(size=(5, 2)) # cinco ejemplos
salida5 = adelante(X5, red, ver=True)
# La misma función, con la red de XOR y sigmoide en la salida
z_xor = adelante(X4, [(W1, b1), (W2, b2)], activacion=sigmoide)
print("XOR:", sigmoide(z_xor).ravel().round(3))def contar_parametros(tamanos):
"""Pesos y términos independientes de un MLP con capas de esos tamaños."""
total = 0
for a, m in zip(tamanos[:-1], tamanos[1:]):
# matriz (a, m) y un término independiente por neurona
total += a * m + m
return total
for tamanos in [[2, 2, 1], [2, 4, 1], [784, 128, 10]]:
print(tamanos, "->", contar_parametros(tamanos), "parámetros")rng = np.random.default_rng(1)
V1, c1 = rng.normal(size=(2, 4)), rng.normal(size=4)
V2, c2 = rng.normal(size=(4, 1)), rng.normal(size=1)
dos_capas = (X5 @ V1 + c1) @ V2 + c2 # dos capas, sin activación
V, c = V1 @ V2, c1 @ V2 + c2 # una sola capa equivalente
una_capa = X5 @ V + c
print("Forma de V:", V.shape)
print("¿Dan lo mismo?", np.allclose(dos_capas, una_capa))Un adelanto: una red que aprende sola
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
# Las mismas lunas y la misma división del tema 8
X, y = make_moons(n_samples=200, noise=0.3, random_state=0)
X_entr, X_val, y_entr, y_val = train_test_split(
X, y, test_size=0.3, random_state=0)
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8),
sharex=True, sharey=True)
for eje, ocultas in zip(ejes.flat, [1, 3, 10, 100]):
red_mlp = MLPClassifier(hidden_layer_sizes=(ocultas,),
solver="lbfgs", max_iter=2000,
random_state=0)
red_mlp.fit(X_entr, y_entr)
regiones(eje, red_mlp.predict, (-2, 3), (-1.5, 2))
dibujar_clases(X_entr, y_entr, ax=eje, s=8)
eje.set_title(f"Ancho {ocultas}")
print(f"Ancho {ocultas:3d}: aciertos al entrenar "
f"{red_mlp.score(X_entr, y_entr):.2f}"
f" en validación {red_mlp.score(X_val, y_val):.2f}")
plt.show()¿Terminaste este tema?
Márcalo y en la ruta verás cuánto llevas. Se guarda solo en este navegador.