Redes neuronales
Qué es una red neuronal, cómo aprende con backpropagation y cómo se implementa: desde cero en NumPy, con PyTorch y con scikit-learn, reconociendo dígitos escritos a mano.
01Qué es
Una red neuronal es una composición de capas: cada capa hace una transformación lineal (multiplica por una matriz de pesos y suma un sesgo) seguida de una función no lineal (la activación). El perceptrón multicapa (MLP) es la forma más básica, con capas totalmente conectadas.
Las redes convolucionales, recurrentes y los Transformers son variaciones de la misma idea con conexiones pensadas para imágenes, secuencias o texto. Todas se entrenan igual: descenso de gradiente con gradientes calculados por backpropagation.
02Intuición
Cada neurona de la primera capa aprende a detectar un patrón sencillo, como un trazo en cierta zona de la imagen. La capa siguiente combina esos patrones en otros más complejos. Sin la activación no lineal, apilar capas no serviría de nada: una composición de funciones lineales sigue siendo lineal.
Con una sola capa oculta suficientemente ancha, una red puede aproximar cualquier función continua en un dominio acotado (teorema de aproximación universal). Más capas permiten hacerlo con muchas menos neuronas.
Mira qué dibuja cada neurona en el laboratorio de frontera de decisión, y sigue los números de una iteración completa en el de backpropagation paso a paso.
03Fundamento matemático
Pasada hacia delante
Pérdida
Backpropagation
La regla de la cadena, aplicada de la salida hacia la entrada. La combinación de softmax y entropía cruzada tiene un gradiente muy simple:
Con esos gradientes, cada peso se actualiza como en cualquier descenso de gradiente: .
04Algoritmo paso a paso
- Inicializa los pesos con valores aleatorios pequeños (inicialización de He para ReLU); los sesgos a cero.
- Baraja los datos y pártelos en mini-lotes.
- Para cada lote: pasada hacia delante, cálculo de la pérdida, backpropagation y actualización de pesos.
- Al terminar cada época (una pasada por todos los datos), mide el error en validación.
- Para cuando la validación deja de mejorar.
05Pseudocódigo
inicializar W₁, b₁, W₂, b₂
para cada época:
para cada lote (X, y):
Z₁ ← X·W₁ + b₁ ; A₁ ← ReLU(Z₁)
P ← softmax(A₁·W₂ + b₂)
δ₂ ← (P − onehot(y)) / n
δ₁ ← (δ₂·W₂ᵀ) ⊙ [Z₁ > 0]
W₂ ← W₂ − η·A₁ᵀ·δ₂ ; b₂ ← b₂ − η·Σδ₂
W₁ ← W₁ − η·Xᵀ·δ₁ ; b₁ ← b₁ − η·Σδ₁06Implementación desde cero en Python
import numpy as np
class MLP:
"""64 → 64 (ReLU) → 10 (softmax), entropía cruzada, SGD por mini-lotes."""
def __init__(self, d_in, d_h, d_out, seed=0):
r = np.random.default_rng(seed)
self.W1 = r.normal(0, np.sqrt(2 / d_in), (d_in, d_h)); self.b1 = np.zeros(d_h) # inicialización de He
self.W2 = r.normal(0, np.sqrt(1 / d_h), (d_h, d_out)); self.b2 = np.zeros(d_out)
def forward(self, X):
self.X = X
self.Z1 = X @ self.W1 + self.b1
self.A1 = np.maximum(0, self.Z1) # ReLU
Z2 = self.A1 @ self.W2 + self.b2
Z2 -= Z2.max(1, keepdims=True) # softmax numéricamente estable
E = np.exp(Z2); self.P = E / E.sum(1, keepdims=True)
return self.P
def backward(self, y, lr):
n = len(y)
dZ2 = self.P.copy(); dZ2[np.arange(n), y] -= 1; dZ2 /= n # softmax + entropía cruzada: p − y
dW2, db2 = self.A1.T @ dZ2, dZ2.sum(0)
dZ1 = (dZ2 @ self.W2.T) * (self.Z1 > 0) # regla de la cadena · ReLU'
dW1, db1 = self.X.T @ dZ1, dZ1.sum(0)
for p, g in ((self.W1, dW1), (self.b1, db1), (self.W2, dW2), (self.b2, db2)):
p -= lr * gred = MLP(64, 64, 10)
rng = np.random.default_rng(1)
for epoca in range(60):
idx = rng.permutation(len(X_tr))
for i in range(0, len(idx), 32): # mini-lotes de 32
lote = idx[i:i + 32]
red.forward(X_tr[lote])
red.backward(y_tr[lote], lr=0.1)
pred = red.forward(X_te).argmax(1) # 97,3 % de aciertoLa forma estándar de saber si un backpropagation escrito a mano es correcto es compararlo con la derivada numérica (diferencias finitas). Aquí coinciden en 10 cifras decimales:
# Comprobación del gradiente: derivada analítica frente a diferencias finitas
eps = 1e-5
red.W1[10, 3] += eps; red.forward(xb); l_mas = red.loss(yb)
red.W1[10, 3] -= 2 * eps; red.forward(xb); l_menos = red.loss(yb)
numerico = (l_mas - l_menos) / (2 * eps)
# analítico: −0.0503071992 numérico: −0.050307199207Implementación con PyTorch y Scikit-learn
import torch, torch.nn as nn
modelo = nn.Sequential(nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, 10)) # 4.810 parámetros
opt = torch.optim.Adam(modelo.parameters(), lr=1e-3)
perdida = nn.CrossEntropyLoss() # incluye el softmax
for epoca in range(60):
for xb, yb in lotes(X_tr, y_tr, 32):
opt.zero_grad()
L = perdida(modelo(xb), yb) # pasada hacia delante
L.backward() # backpropagation automática
opt.step()from sklearn.neural_network import MLPClassifier
mlp = MLPClassifier(hidden_layer_sizes=(64,), max_iter=500, random_state=42).fit(X_tr, y_tr)
mlp.score(X_te, y_te) # 0.982PyTorch calcula los gradientes automáticamente (autograd): escribes solo la pasada hacia delante. scikit-learn es cómodo para redes pequeñas, pero no usa GPU ni permite arquitecturas a medida.
08Dataset de ejemplo
El dataset Digits de scikit-learn es una copia del conjunto de test de «Optical Recognition of Handwritten Digits» del repositorio UCI: 1.797 imágenes de dígitos escritos a mano, reducidas a 8×8 píxeles con 17 niveles de gris (0 a 16), y unas 180 imágenes por dígito. Cada imagen se convierte en un vector de 64 números.
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
X, y = load_digits(return_X_y=True) # 1.797 imágenes de 8×8, píxeles de 0 a 16
X = X / 16.0 # escalar a [0, 1]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, stratify=y, random_state=42)09Entrenamiento
Todo el entrenamiento en NumPy tarda unos 0,3 segundos en un procesador normal: con redes y datos pequeños no hace falta GPU.
10Evaluación
| Modelo (mismos datos de test, 450 imágenes) | Acierto |
|---|---|
| Regresión logística multinomial | 96,2 % |
| MLP desde cero en NumPy (64 ocultas, SGD) | 97,3 % |
| MLP en PyTorch (64 ocultas, Adam) | 97,6 % |
| MLPClassifier de scikit-learn (64 ocultas) | 98,2 % |
Una red con una capa oculta mejora a la regresión logística, pero no por mucho: con imágenes de 8×8, los dígitos ya son casi separables linealmente. La ventaja de las redes crece con el tamaño y la complejidad de los datos; en MNIST (28×28) o en fotos, la distancia es enorme.
11Métricas
En clasificación multiclase se usan el acierto, la matriz de confusión y la precisión y recall por clase. Los 12 errores de la red NumPy en test se concentran en pocos pares:
| Dígito real | Predicho como | Veces |
|---|---|---|
| 8 | 1 | 4 |
| 0 | 4 | 2 |
| 1 | 8 | 2 |
| 5 | 9 | 1 |
| 6 | 8 | 1 |
| 7 | 4 | 1 |
| 8 | 7 | 1 |
El 8 confundido con el 1 es el error más frecuente: a 8×8 píxeles, un 8 estrecho y un 1 con base se parecen mucho.
12Visualización
13Ventajas
- Aprenden las características directamente de datos brutos (píxeles, audio, texto).
- Escalan con los datos: más datos y más capacidad siguen mejorando donde otros modelos se estancan.
- Arquitecturas especializadas para cada tipo de dato.
- Ecosistema maduro (PyTorch, JAX, TensorFlow) y aceleración en GPU.
- Aprendizaje por transferencia: reutilizar modelos preentrenados.
14Limitaciones
- Necesitan muchos datos para brillar; con datos tabulares pequeños, el gradient boosting suele ganar.
- Muchos hiperparámetros y entrenamiento sensible a la inicialización y a la tasa de aprendizaje.
- Difíciles de interpretar.
- Coste computacional alto en modelos grandes.
15Hiperparámetros
| Parámetro | Por defecto | Efecto |
|---|---|---|
hidden_layer_sizes | (100,) | Número de capas ocultas y neuronas en cada una. |
activation | "relu" | Función de activación de las capas ocultas. |
solver | "adam" | Optimizador: adam, sgd o lbfgs (este último, bueno para datasets pequeños). |
learning_rate_init | 0.001 | Tasa de aprendizaje inicial. |
batch_size | "auto" = min(200, n) | Tamaño del mini-lote. |
alpha | 0.0001 | Regularización L2 de los pesos. |
max_iter | 200 | Épocas máximas. |
early_stopping | False | Reserva un 10 % para validación y para cuando no mejora. |
Valores por defecto de MLPClassifier en scikit-learn 1.8. En PyTorch no hay valores por defecto de arquitectura: se definen en el código.
16Casos de uso
- Visión artificial: clasificación, detección y segmentación de imágenes.
- Voz y audio: reconocimiento y síntesis.
- Lenguaje: traducción, resumen, asistentes (Transformers).
- Sistemas de recomendación con embeddings.
17Aplicaciones industriales
- Inspección visual de defectos en líneas de producción.
- Series temporales de sensores para detectar anomalías en maquinaria.
- Telecomunicaciones: clasificación de tráfico y optimización de redes radio.
- Lectura automática de contadores, matrículas y documentos (OCR).
18Ejercicio práctico
Añade una segunda capa oculta y dropout a la red de PyTorch y compara el acierto. ¿Mejora con tan pocos datos, o empeora?
# Ejercicio: añade una segunda capa oculta y dropout en PyTorch.
modelo = nn.Sequential(
nn.Linear(64, 128), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(64, 10))
# ... entrena igual y compara el acierto. Recuerda modelo.eval() antes de evaluar.19Código completo
Las tres implementaciones y todos los experimentos de esta página:
20Referencias y papers
- F. Rosenblatt (1958). «The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain». Psychological Review, 65(6), 386–408.
- D. E. Rumelhart, G. E. Hinton, R. J. Williams (1986). «Learning representations by back-propagating errors». Nature, 323, 533–536. Ficha en la biblioteca.
- K. Hornik, M. Stinchcombe, H. White (1989). «Multilayer feedforward networks are universal approximators». Neural Networks, 2(5), 359–366.
- K. He, X. Zhang, S. Ren, J. Sun (2015). «Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification». ICCV 2015. (Inicialización de He).
- D. P. Kingma, J. Ba (2015). «Adam: A Method for Stochastic Optimization». ICLR 2015. Ficha en la biblioteca.
- I. Goodfellow, Y. Bengio, A. Courville (2016). Deep Learning. MIT Press.