De la neurona a la red: perceptrón y MLP
Los 12 bloques de código de la teoría, en el mismo orden. Cópialos y córrelos en tu Python o en Colab; 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 sesgo, 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 sesgo
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 sesgos de un MLP con capas de esos tamaños."""
total = 0
for a, m in zip(tamanos[:-1], tamanos[1:]):
total += a * m + m # matriz (a, m) y un sesgo por neurona
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()