Saltar a contenido

Capítulo 1 — Redes neuronales desde cero: la neurona, el perceptrón y el MLP en NumPy puro

Módulo 03 — Deep Learning · AI Master Academy Nivel: intermedio · Requisitos: Módulos 01 (Python/NumPy, álgebra matricial, gradiente descendente) y 02 (ML clásico con sklearn, softmax, métricas) Tiempo estimado: 4–6 horas con código

Bienvenido al módulo de Deep Learning. En los módulos anteriores aprendiste a entrenar modelos que tú alimentabas con features diseñadas a mano: regresión logística sobre columnas que tú elegías, árboles sobre variables que tú transformabas. En este módulo cruzamos la frontera: vamos a construir modelos que aprenden sus propias representaciones de los datos.

Y lo vamos a hacer de la forma más honesta posible: sin frameworks. Ni PyTorch, ni TensorFlow, ni Keras. Solo NumPy. Cuando en el capítulo 3 abras PyTorch por primera vez, cada línea te resultará familiar porque ya habrás implementado a mano todo lo que PyTorch automatiza. Esa es la diferencia entre usar deep learning y entender deep learning.


Índice

  1. ¿Por qué redes neuronales? Los límites del ML clásico
  2. La neurona: de la biología al producto punto
  3. El perceptrón histórico y el problema XOR
  4. Funciones de activación a fondo
  5. El MLP: arquitectura por capas
  6. Forward pass completo en NumPy
  7. Medir el error: MSE y cross-entropy
  8. Entrenamiento con gradiente numérico: la red XOR aprende
  9. Ejemplo integrador: clasificar las dos lunas
  10. Caso empresarial: entrenar, adaptar o alquilar
  11. Buenas prácticas
  12. Malas prácticas
  13. Errores comunes
  14. Preguntas frecuentes (FAQ)
  15. Resumen del capítulo
  16. Bibliografía y recursos

1. ¿Por qué redes neuronales? Los límites del ML clásico

1.1 El cuello de botella: las features hechas a mano

Recuerda el flujo de trabajo del módulo 02. Para clasificar, por ejemplo, si un correo era spam, tú hacías algo así:

  1. Diseñabas features: número de mayúsculas, presencia de la palabra "gratis", longitud del asunto, dominio del remitente...
  2. Entrenabas un modelo (logística, random forest, gradient boosting) sobre esas features.
  3. Si el modelo fallaba, volvías al paso 1 y diseñabas mejores features.

Este proceso se llama ingeniería de características (feature engineering) y tiene un problema estructural: el techo del modelo lo pone tu imaginación, no los datos. El modelo solo puede combinar lo que tú le das. Si la señal relevante está en una interacción que no se te ocurrió, el modelo nunca la verá.

Para datos tabulares (una tabla con columnas con significado: edad, ingresos, número de compras) esto funciona sorprendentemente bien, porque las columnas ya son representaciones de alto nivel. Un humano ya hizo el trabajo de abstracción al definir "ingresos anuales".

Pero ¿qué pasa con una imagen? Una imagen de 224×224 píxeles en color son 150 528 números entre 0 y 255. Ninguno de esos números significa nada por sí solo. El píxel (37, 120) no es una feature: es ruido sin contexto. ¿Qué feature manual detecta "hay un gato"? Durante décadas la visión por computador intentó exactamente eso (detectores de bordes SIFT, HOG, filtros de Gabor diseñados a mano) y se estancó en precisiones mediocres.

1.2 La promesa del deep learning: aprender las representaciones

La idea central del deep learning es esta:

En lugar de diseñar las features a mano, deja que el modelo las aprenda directamente de los datos crudos, por capas sucesivas de abstracción.

Una red neuronal profunda es una pila de transformaciones aprendibles. Cada capa recibe la representación de la capa anterior y la transforma en una representación un poco más abstracta y más útil para la tarea final. En una red que clasifica caras:

  • Las primeras capas aprenden a detectar bordes y gradientes de color.
  • Las capas intermedias combinan bordes en formas: ojos, narices, contornos.
  • Las capas finales combinan formas en conceptos: "cara de frente", "cara de perfil".

Nadie programó eso. Emerge del entrenamiento. Esta jerarquía aprendida es lo que la palabra deep (profundo) significa: profundidad = número de capas de representación.

1.3 Cuándo brilla el deep learning… y cuándo NO

Aquí viene la parte que muchos cursos omiten y que en esta academia consideramos obligatoria: el deep learning no es siempre la mejor herramienta. En 2026, con los LLMs dominando titulares, es fácil olvidar que para una tabla de 50 000 filas con 30 columnas, un gradient boosting (XGBoost/LightGBM, módulo 02) suele ganar a una red neuronal, con menos coste y menos esfuerzo.

Criterio Deep Learning (redes) ML clásico (boosting, logística, RF)
Datos no estructurados (imagen, audio, texto, vídeo) Dominio absoluto: aprende las features Necesita features manuales, techo bajo
Datos tabulares pequeños/medianos (< ~100k filas) Suele perder o empatar Gana la mayoría de las veces (múltiples benchmarks lo confirman)
Datos tabulares enormes con interacciones complejas Puede competir, pocas veces compensa Sigue siendo la apuesta segura
Cantidad de datos necesaria Alta: miles a millones de ejemplos Baja-media: funciona desde cientos
Coste de entrenamiento Alto (GPU, horas/días) Bajo (CPU, segundos/minutos)
Interpretabilidad Baja (caja negra, requiere técnicas extra) Media-alta (importancias, SHAP, coeficientes)
Tiempo de puesta en producción Semanas Días
Mantenimiento Complejo (versiones de modelo, GPU, drift) Sencillo
Transfer learning disponible Enorme ecosistema de preentrenados Casi inexistente

Nota

Estudios comparativos serios (Grinsztajn et al., 2022, "Why do tree-based models still outperform deep learning on tabular data?") muestran que en datasets tabulares de tamaño medio los métodos de árboles ganan de forma consistente. Si un consultor te propone una red neuronal para tu tabla de Excel de 10 000 filas, desconfía.

Consejo profesional

La pregunta correcta antes de elegir deep learning no es "¿es lo más moderno?" sino "¿mis datos son crudos (píxeles, ondas, tokens) o ya son features (columnas con significado)?". Datos crudos → redes. Columnas con significado → empieza por boosting y solo escala a redes si tienes evidencia de que hace falta.

Entonces, ¿por qué dedicamos un módulo entero a redes? Porque todo lo que hoy llamamos IA moderna — visión, voz, LLMs, generación de imágenes, robótica — es deep learning. Y todo ese edificio gigante descansa sobre lo que vas a construir en este capítulo: neuronas, capas, activaciones y un forward pass.


2. La neurona: de la biología al producto punto

2.1 La inspiración biológica (breve y sin mitos)

Una neurona biológica recibe señales eléctricas de otras neuronas a través de sus dendritas. Cada conexión (sinapsis) tiene una "fuerza" distinta: algunas señales excitan a la neurona, otras la inhiben. La neurona integra todas las señales en su cuerpo celular y, si la suma supera un umbral, dispara: envía un impulso por su axón hacia otras neuronas.

De ahí tomamos tres ideas:

  1. Muchas entradas, una salida.
  2. Cada entrada tiene un peso (la fuerza de la sinapsis).
  3. Hay un umbral de activación: la salida no es proporcional a la entrada, hay una no-linealidad.

Advertencia

Aquí termina la analogía. Las neuronas reales son enormemente más complejas (dinámica temporal, neurotransmisores, plasticidad local, tipos celulares...). Una red neuronal artificial NO es un modelo del cerebro, igual que un avión no es un modelo de un pájaro. Ambos vuelan, pero por ingenierías distintas. Desconfía de cualquier material que venda las redes como "cerebros digitales".

2.2 La neurona artificial: es un producto punto

Una neurona artificial con (n) entradas hace exactamente esto:

z = w₁·x₁ + w₂·x₂ + ... + wₙ·xₙ + b        (suma ponderada + sesgo)
a = f(z)                                    (activación: no-linealidad)

Donde:

  • x = (x₁, ..., xₙ): el vector de entradas.
  • w = (w₁, ..., wₙ): el vector de pesos — los parámetros que se aprenden. Un peso grande y positivo significa "esta entrada me excita mucho"; negativo, "me inhibe".
  • b: el sesgo (bias) — desplaza el umbral de disparo. Sin él, la neurona estaría obligada a pasar por el origen (si todas las entradas son 0, z sería 0 siempre), lo que limita gravemente qué puede representar.
  • f: la función de activación, que veremos a fondo en la sección 4.

Y aquí la conexión directa con el módulo 01: la suma ponderada es un producto punto:

[ z = \mathbf{w} \cdot \mathbf{x} + b = \mathbf{w}^T\mathbf{x} + b ]

Todo el deep learning, desde esta neurona hasta GPT, es en el fondo productos punto organizados en matrices, intercalados con no-linealidades. Si dominas np.dot, dominas el 80 % de la mecánica.

2.3 Una neurona en NumPy

import numpy as np

# Fijamos la semilla para que TODOS los resultados del capítulo sean reproducibles.
# Regla de la academia: ningún experimento sin semilla.
rng = np.random.default_rng(42)

def sigmoid(z):
    """Función de activación sigmoide: aplasta cualquier real al rango (0, 1)."""
    return 1.0 / (1.0 + np.exp(-z))   # fórmula clásica 1/(1+e^-z)

# --- Definimos UNA neurona con 3 entradas ---
w = rng.normal(0, 0.5, size=3)   # 3 pesos aleatorios pequeños (media 0, desv 0.5)
b = 0.0                          # sesgo inicial en cero (para el sesgo SÍ vale cero, ver sección 6)

# --- Una entrada de ejemplo: un "cliente" con 3 features normalizadas ---
x = np.array([0.5, -1.2, 3.0])   # vector de entrada, shape (3,)

# --- Cálculo de la neurona, paso a paso ---
z = np.dot(w, x) + b             # producto punto (3,)·(3,) -> escalar, más el sesgo
a = sigmoid(z)                   # no-linealidad: el escalar pasa por la sigmoide

print(f"pesos w = {np.round(w, 3)}")
print(f"suma ponderada z = {z:.4f}")
print(f"activación a = {a:.4f}")

Salida (reproducible con la semilla 42):

pesos w = [ 0.152 -0.52   0.375]
suma ponderada z = 1.8258
activación a = 0.8613

Interpretación: esta neurona, con estos pesos, "se activa bastante" (0.86 sobre un máximo de 1) ante esta entrada. Cambiando los pesos cambia qué patrones de entrada la excitan. Aprender = encontrar los pesos que hacen que la neurona se active con lo que debe activarse.

Nota

¿Te suena esta estructura? Es exactamente la regresión logística del módulo 02: producto punto + sesgo + sigmoide. Una neurona con activación sigmoide es una regresión logística. Una red neuronal es, en esencia, muchas regresiones logísticas apiladas y entrenadas juntas — y esa composición es lo que la hace cualitativamente más potente.

Ejercicio rápido 2.1

Sin ejecutar código: si w = [2, -1], b = -1 y la activación es sigmoide, ¿cuál es la salida de la neurona para x = [1, 1]? ¿Y para x = [0, 0]?

Ver solución Para `x = [1, 1]`: `z = 2·1 + (-1)·1 + (-1) = 0` → `sigmoid(0) = 0.5`. Para `x = [0, 0]`: `z = 0 + 0 - 1 = -1` → `sigmoid(-1) = 1/(1+e¹) ≈ 0.269`. Observa el papel del sesgo: con entrada nula la neurona NO da 0.5 (indiferencia), sino 0.269 (tendencia a "no dispararse"). El sesgo fija el punto de partida.

3. El perceptrón histórico y el problema XOR

3.1 El perceptrón de Rosenblatt (1958)

El perceptrón fue la primera neurona artificial que aprendía sola. Frank Rosenblatt lo implementó en hardware (¡la Mark I Perceptron ocupaba un armario!) y la prensa de 1958 anunció que pronto las máquinas "caminarían, hablarían y serían conscientes". Spoiler: faltaban unos 60 años y varios inviernos de la IA.

El perceptrón es una neurona con activación escalón (step): dispara 1 si z ≥ 0, dispara 0 si no. Y su algoritmo de aprendizaje es de una simplicidad hermosa:

Para cada ejemplo (x, y):
    ŷ = escalon(w·x + b)          # predicción actual
    error = y - ŷ                  # -1, 0 o +1
    w = w + lr * error * x         # si falló, mueve los pesos hacia el ejemplo
    b = b + lr * error             # y ajusta el umbral

Si acierta, no toca nada. Si predice 0 y era 1, empuja los pesos hacia el ejemplo. Si predice 1 y era 0, los empuja en contra. Rosenblatt demostró un teorema notable: si los datos son linealmente separables, el perceptrón converge en un número finito de pasos. Garantizado.

3.2 Qué puede aprender: separación lineal

El perceptrón dibuja una recta (en 2D; un hiperplano en general) y clasifica según de qué lado caes. Puede aprender AND y OR sin problema:

        AND (separable)                  OR (separable)
  x₂                               x₂
   1 |  ○         ●                 1 |  ●         ●
     |        \                       |   \
     |         \  <- recta            |    \  <- recta
     |          \                     |     \
   0 |  ○         ○                 0 |  ○    \    ●
     +---------------- x₁             +---------------- x₁
        0         1                      0         1

   ● = clase 1   ○ = clase 0     Una sola recta basta para separar.

3.3 El problema XOR: la recta imposible

XOR (o-exclusivo) devuelve 1 cuando las entradas difieren:

x₁ x₂ XOR
0 0 0
0 1 1
1 0 1
1 1 0

Dibújalo:

        XOR (NO separable)
  x₂
   1 |  ●         ○
     |
     |     ¿¿recta??       Los ● están en esquinas opuestas.
     |                     NINGUNA recta única deja los dos ●
   0 |  ○         ●        a un lado y los dos ○ al otro.
     +---------------- x₁
        0         1

No existe recta que separe las clases. Minsky y Papert lo demostraron formalmente en su libro Perceptrons (1969), y esa demostración (mal interpretada como "las redes neuronales son un callejón sin salida") contribuyó al primer invierno de la IA: la financiación se evaporó durante más de una década.

3.4 Demostración en código: el perceptrón NO converge en XOR

No te lo creas: compruébalo.

import numpy as np

rng = np.random.default_rng(0)             # semilla fija

# Datos XOR: 4 ejemplos, 2 features
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)  # shape (4, 2)
y = np.array([0, 1, 1, 0])                                    # etiquetas XOR

def entrenar_perceptron(X, y, epocas=1000, lr=0.1):
    """Algoritmo original de Rosenblatt. Devuelve pesos y nº de errores por época."""
    w = rng.normal(0, 0.5, size=X.shape[1])   # pesos iniciales pequeños
    b = 0.0                                    # sesgo inicial
    historial_errores = []                     # para ver si converge
    for _ in range(epocas):
        errores = 0
        for xi, yi in zip(X, y):               # recorremos ejemplo a ejemplo
            y_hat = 1 if np.dot(w, xi) + b >= 0 else 0   # activación escalón
            error = yi - y_hat                 # -1, 0 o +1
            if error != 0:                     # solo actualiza si falló
                w += lr * error * xi           # regla de Rosenblatt
                b += lr * error
                errores += 1
        historial_errores.append(errores)
        if errores == 0:                       # convergencia: época perfecta
            break
    return w, b, historial_errores

w, b, hist = entrenar_perceptron(X, y)
print(f"Épocas ejecutadas: {len(hist)} (pedimos máx. 1000)")
print(f"Errores en las últimas 5 épocas: {hist[-5:]}")

Salida:

Épocas ejecutadas: 1000 (pedimos máx. 1000)
Errores en las últimas 5 épocas: [4, 2, 4, 2, 4]

Mil épocas después, sigue fallando 2–4 de los 4 ejemplos, oscilando eternamente. Con AND u OR converge en menos de 10 épocas (pruébalo cambiando y). El teorema de convergencia no miente: si no hay separabilidad lineal, no hay convergencia.

3.5 La solución: una capa oculta

La salida del bloqueo XOR es conceptualmente simple y históricamente enorme: apilar neuronas. XOR se puede descomponer en operaciones lineales:

XOR(x₁, x₂) = OR(x₁, x₂)  AND  NOT-AND(x₁, x₂)

OR es separable. NAND es separable. AND (de esas dos salidas) es separable. Tres perceptrones organizados en dos capas resuelven lo que uno solo no puede:

   x₁ ──┬──► [neurona OR]  ──► h₁ ──┐
        │                            ├──► [neurona AND] ──► XOR ✅
   x₂ ──┴──► [neurona NAND] ──► h₂ ──┘

   Capa oculta: transforma el espacio.     Capa de salida: separa linealmente
   En el espacio (h₁, h₂) los puntos       lo que en el espacio original
   XOR SÍ son linealmente separables.      era inseparable.

Esta es LA idea del deep learning en una frase: cada capa transforma el espacio de representación hasta que el problema se vuelve linealmente separable. La red no "dobla la recta"; dobla el espacio para que una recta baste.

El problema en 1969 era: la regla de Rosenblatt solo sabe entrenar UNA capa. ¿Cómo entrenas los pesos de la capa oculta, si no sabes qué "debería" producir? Ese es el credit assignment problem, y su solución (backpropagation, popularizada en 1986 por Rumelhart, Hinton y Williams) es el tema del capítulo 2. En este capítulo lo esquivaremos con un truco de fuerza bruta (sección 8) que funciona pero es lentísimo — y así apreciarás backprop cuando llegue.

Ejercicio rápido 3.1

Encuentra a mano pesos y sesgos para una neurona escalón que compute NAND(x₁, x₂) (= 0 solo cuando ambas entradas son 1).

Ver solución Una solución clásica: `w = [-1, -1]`, `b = 1.5`. Verificación con escalón (z ≥ 0 → 1): - (0,0): z = 1.5 → 1 - (0,1): z = 0.5 → 1 - (1,0): z = 0.5 → 1 - (1,1): z = -0.5 → 0 Hay infinitas soluciones (cualquier recta que deje (1,1) solo a un lado). Eso también es una lección: los pesos de una red no son únicos.

4. Funciones de activación a fondo

4.1 Por qué la no-linealidad es innegociable

Afirmación fuerte: sin funciones de activación no lineales, una red de 100 capas es exactamente equivalente a una de 1 capa. Demostración con dos capas lineales:

[ h = W_1 x + b_1 \qquad \hat{y} = W_2 h + b_2 ]

Sustituyendo:

[ \hat{y} = W_2(W_1 x + b_1) + b_2 = \underbrace{(W_2 W_1)}{W'} x + \underbrace{(W_2 b_1 + b_2)} = W'x + b' ]

La composición de dos transformaciones lineales es otra transformación lineal. Por inducción, cien capas lineales colapsan a una sola matriz. Toda la "profundidad" es decorativa. Compruébalo:

import numpy as np

rng = np.random.default_rng(7)

# Dos capas "profundas" SIN activación
W1 = rng.normal(size=(3, 2)); b1 = rng.normal(size=3)   # capa 1: 2 -> 3
W2 = rng.normal(size=(1, 3)); b2 = rng.normal(size=1)   # capa 2: 3 -> 1

x = np.array([0.5, -1.0])                                # una entrada cualquiera

# Camino A: pasar por las dos capas
salida_dos_capas = W2 @ (W1 @ x + b1) + b2

# Camino B: colapsar a UNA capa equivalente
W_eq = W2 @ W1                    # matriz combinada (1,3)@(3,2) = (1,2)
b_eq = W2 @ b1 + b2               # sesgo combinado
salida_una_capa = W_eq @ x + b_eq

print(salida_dos_capas)   # [1.5806...]
print(salida_una_capa)    # [1.5806...]  <- idénticas: la profundidad no aportó NADA

La función de activación rompe esta trampa: al intercalar una operación no lineal entre capas, la composición ya no colapsa, y cada capa añade capacidad expresiva real. El teorema de aproximación universal (Cybenko 1989, Hornik 1991) formaliza el premio: un MLP con una sola capa oculta suficientemente ancha y activación no lineal puede aproximar cualquier función continua con precisión arbitraria. (Ojo: el teorema dice que los pesos existen, no que sepas encontrarlos ni que la red generalice — no lo uses como argumento de venta.)

Veamos ahora el catálogo. Para cada activación: fórmula, gráfica ASCII, por qué existe, y por qué dejó de usarse (si dejó).

4.2 Sigmoide: la veterana que satura

[ \sigma(z) = \frac{1}{1 + e^{-z}} \qquad \text{rango: } (0, 1) ]

  1.0 ┤                                  ●●●●●●●●●●●
      │                            ●●●●
      │                         ●●
  0.5 ┤························●···························
      │                     ●●
      │                 ●●●●
  0.0 ┤ ●●●●●●●●●●●●●●
      └──────────┬──────────────┬──────────────┬────────
                -4              0              +4        z

Por qué existe: convierte cualquier real en algo interpretable como probabilidad. Suave y derivable en todo punto (clave para el capítulo 2: su derivada es la elegante σ(z)·(1−σ(z))).

Su gran problema: la saturación. Mira los extremos de la gráfica: para |z| > 4 la curva es prácticamente plana. Plana significa derivada casi cero (la derivada máxima de la sigmoide es 0.25, en z=0, y decae exponencialmente hacia los lados). Cuando en el capítulo 2 propaguemos gradientes hacia atrás multiplicando derivadas capa a capa, multiplicar muchos números ≤ 0.25 hace que el gradiente se desvanezca (vanishing gradient): las primeras capas de una red profunda dejan de aprender. Este fue el muro que frenó al deep learning en los 90–2000.

Problema secundario: su salida no está centrada en cero (siempre positiva), lo que sesga las actualizaciones de la capa siguiente y ralentiza el entrenamiento.

Dónde sigue viva: en la neurona de salida de clasificación binaria (ahí queremos exactamente una probabilidad) y dentro de las puertas de las LSTM. Como activación oculta, está jubilada.

4.3 Tanh: la sigmoide centrada

[ \tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} = 2\sigma(2z) - 1 \qquad \text{rango: } (-1, 1) ]

  +1.0 ┤                             ●●●●●●●●●●●●
       │                        ●●●
       │                     ●●
   0.0 ┤····················●······························
       │                 ●●
       │             ●●●
  -1.0 ┤ ●●●●●●●●●●●●
       └──────────┬─────────────┬─────────────┬──────────
                 -3             0             +3         z

Mejora sobre la sigmoide: salida centrada en cero → las activaciones tienen media ≈ 0 → el aprendizaje de la capa siguiente es más estable. Su derivada máxima es 1 (vs 0.25), así que el desvanecimiento es menos brutal.

Pero sigue saturando en ambos extremos. En redes profundas, mismo destino que la sigmoide, solo que un poco más tarde. Fue la activación estándar de los 90 y sobrevive en RNNs clásicas (el estado de una LSTM pasa por tanh). En redes feedforward modernas, superada.

4.4 ReLU: la que ganó la revolución

[ \text{ReLU}(z) = \max(0, z) \qquad \text{rango: } [0, +\infty) ]

   4 ┤                                          ●
     │                                       ●
   3 ┤                                    ●
     │                                 ●
   2 ┤                              ●
     │                           ●
   1 ┤                        ●
     │                     ●
   0 ┤●●●●●●●●●●●●●●●●●●●●
     └──────────┬─────────┬─────────┬─────────┬─────
               -4         0        +2        +4     z

Sí, es eso. Si es negativo, cero; si es positivo, déjalo pasar. La activación que impulsó la revolución del deep learning (AlexNet, 2012, la usó y entrenó 6 veces más rápido que con tanh) es la función más tonta de esta lista. Por qué ganó:

  1. No satura en el lado positivo. Para z > 0 la derivada es exactamente 1, siempre. Los gradientes atraviesan decenas de capas sin desvanecerse (multiplicar por 1 no encoge nada). Este es EL motivo.
  2. Baratísima. Un max. Nada de exponenciales. En redes de miles de millones de activaciones por segundo, importa.
  3. Induce esparsidad: en cada pasada, ~50 % de las neuronas dan salida exactamente 0. Representaciones dispersas que resultan beneficiosas en la práctica.

Su patología: la ReLU moribunda (dying ReLU). Si durante el entrenamiento los pesos de una neurona la llevan a producir z < 0 para todos los ejemplos del dataset, su salida es siempre 0, su derivada es siempre 0, el gradiente que le llega es siempre 0… y nunca más se actualizará. La neurona ha muerto. Con learning rates altos o mala inicialización puedes acabar con un 20–40 % de la red muerta: pagaste por neuronas que no computan nada.

4.5 Leaky ReLU: la reparación de la fuga

[ \text{LeakyReLU}(z) = \begin{cases} z & z > 0 \ \alpha z & z \le 0 \end{cases} \quad (\alpha \approx 0.01) \qquad \text{rango: } (-\infty, +\infty) ]

   3 ┤                                    ●
     │                                 ●
   2 ┤                              ●
     │                           ●
   1 ┤                        ●
     │                     ●
   0 ┤●●●●●●●●●●●●●●●●●●●●            <- pendiente 0.01: pequeña
     │(pendiente suave α,                pero NO nula. La neurona
  -0.04┤ casi horizontal)                 herida puede recuperarse.
     └──────────┬─────────┬─────────┬─────
               -4         0        +2      z

En el lado negativo, en lugar de cero, deja pasar una fracción pequeña (α·z, típicamente α = 0.01). La derivada en la zona negativa ya no es 0 sino α: una neurona "apagada" recibe un goteo de gradiente y puede volver a la vida. Cura la dying ReLU con coste casi nulo. Variantes: PReLU (α se aprende), ELU (curva exponencial suave en la zona negativa). En la práctica, la mejora sobre ReLU es real pero modesta; muchos equipos siguen con ReLU simple por costumbre y simplicidad.

4.6 GELU: la activación de los Transformers

[ \text{GELU}(z) = z \cdot \Phi(z) \approx 0.5\,z\left(1 + \tanh!\left[\sqrt{2/\pi}\,(z + 0.044715\,z^3)\right]\right) ]

donde Φ(z) es la función de distribución acumulada de la normal estándar.

   3 ┤                                      ●
     │                                   ●
   2 ┤                                ●
     │                             ●
   1 ┤                          ●
     │                       ●
   0 ┤●●●●●●●●●●●●●●●●●●●●●●          <- como ReLU, pero...
     │              ●●●●●                 con un pequeño "valle" suave
 -0.17┤               (mínimo ≈ -0.17 cerca de z ≈ -0.75, luego sube a 0)
     └──────────┬─────────┬─────────┬─────
               -4         0        +2      z

La intuición: ReLU decide de forma determinista y abrupta ("¿z > 0? pasa : muere"). GELU pondera la entrada por la probabilidad de que una normal estándar sea menor que z: es como si cada neurona aplicara un "dropout probabilístico suave" proporcional a cuán grande es su entrada. Valores muy negativos → casi siempre bloqueados; muy positivos → casi siempre pasan; cerca de cero → transición suave y derivable, sin la esquina de la ReLU. Esa suavidad ayuda a la optimización en redes muy grandes.

Dónde se usa: es la activación de BERT, GPT-2/3/4 y prácticamente todos los Transformers que estudiarás en módulos posteriores. Cuando en el módulo de NLP veas activation="gelu", ya sabrás qué hay dentro. (Los LLMs más recientes usan variantes con puertas como SwiGLU, pero la idea de "ReLU suavizada probabilísticamente" es la misma familia.)

4.7 Softmax: la activación de salida multiclase (repaso)

La conociste en el módulo 02. No es una activación por-neurona: opera sobre el vector completo de salidas y lo convierte en una distribución de probabilidad:

[ \text{softmax}(z)_i = \frac{e^{z_i}}{\sum_j e^{z_j}} ]

Salidas positivas que suman 1. Se usa exclusivamente en la capa de salida de clasificación multiclase, emparejada con cross-entropy (sección 7). Nunca como activación oculta: destruiría información al normalizar.

def softmax(z):
    """Softmax numéricamente estable (fila a fila si z es matriz)."""
    z_estable = z - np.max(z, axis=-1, keepdims=True)  # restar el máximo NO cambia el resultado
    exp_z = np.exp(z_estable)                          # ...pero evita overflow con z grandes
    return exp_z / np.sum(exp_z, axis=-1, keepdims=True)  # normalizar para que sume 1

Advertencia

implementa SIEMPRE softmax con la resta del máximo. np.exp(1000) desborda a inf y toda tu red devuelve nan. Es el bug numérico más frecuente en implementaciones caseras, y restar una constante a todos los logits no altera matemáticamente el resultado (se cancela en el cociente).

4.8 Tabla comparativa completa

Activación Fórmula Rango Pros Contras Dónde se usa (2026)
Escalón 1 si z≥0, si no 0 {0, 1} Interpretable, histórica Derivada nula en todas partes: inentrenable con gradientes Solo en libros de historia
Sigmoide 1/(1+e⁻ᶻ) (0, 1) Salida = probabilidad; suave Satura en ambos lados (vanishing); no centrada en 0; exp cara Salida binaria; puertas LSTM
Tanh (eᶻ−e⁻ᶻ)/(eᶻ+e⁻ᶻ) (−1, 1) Centrada en 0; derivada máx = 1 Sigue saturando en ambos lados RNN/LSTM clásicas
ReLU max(0, z) [0, ∞) Sin saturación positiva; baratísima; esparsidad Dying ReLU; no centrada; esquina no derivable en 0 Estándar en CNNs y MLPs
Leaky ReLU z si z>0; αz si no (−∞, ∞) Cura dying ReLU; casi gratis Un hiperparámetro más (α); mejora modesta Alternativa a ReLU, GANs
GELU z·Φ(z) ≈(−0.17, ∞) Suave; mejor optimización en redes grandes Más cara de computar; intuición menos directa Transformers (BERT, GPT)
Softmax eᶻⁱ/Σeᶻʲ (0,1), suma=1 Distribución de probabilidad multiclase Solo tiene sentido en la salida Capa de salida multiclase

Consejo profesional

receta por defecto en 2026 — ReLU en capas ocultas de MLPs y CNNs (o GELU si trabajas con Transformers), sigmoide en la salida si la clasificación es binaria, softmax en la salida si es multiclase, ninguna activación en la salida si es regresión (quieres poder predecir cualquier real). Desviarte de esto requiere un motivo que puedas defender en una code review.

Ejercicio rápido 4.1

Una red tiene 3 capas ocultas con activación identidad f(z) = z en las dos primeras y ReLU en la tercera. ¿Cuántas "capas efectivas" de transformación no lineal tiene? ¿A qué red equivale?

Ver solución Las dos primeras capas son lineales y colapsan entre sí (y con la matriz de la tercera capa) en UNA sola transformación lineal, seguida de la ReLU. La red equivale a un MLP de **una sola capa oculta** con ReLU. Profundidad nominal: 3. Profundidad real: 1. Pagaste memoria y cómputo por parámetros redundantes: por eso cada capa oculta necesita su no-linealidad.

5. El MLP: arquitectura por capas

5.1 De la neurona a la capa, de la capa a la red

Un perceptrón multicapa (MLP, multilayer perceptron) — también llamado red feedforward densa o fully connected — organiza neuronas así:

  • Capa de entrada: no computa nada; son tus features. Si cada ejemplo tiene 2 features, la "capa de entrada" tiene tamaño 2.
  • Capas ocultas: cada una contiene varias neuronas; cada neurona recibe TODAS las salidas de la capa anterior (por eso "densa"). Aplican transformación lineal + activación.
  • Capa de salida: produce la predicción; su tamaño y activación dependen de la tarea (1 + sigmoide para binaria; K + softmax para K clases; 1 + identidad para regresión).
graph LR
    subgraph Entrada
        x1((x₁))
        x2((x₂))
    end
    subgraph "Capa oculta (3 neuronas, ReLU)"
        h1((h₁))
        h2((h₂))
        h3((h₃))
    end
    subgraph "Salida (sigmoide)"
        o1((ŷ))
    end
    x1 --> h1
    x1 --> h2
    x1 --> h3
    x2 --> h1
    x2 --> h2
    x2 --> h3
    h1 --> o1
    h2 --> o1
    h3 --> o1

Esta es exactamente la red 2-3-1 que construiremos para XOR: 2 entradas, 3 neuronas ocultas, 1 salida. Cuenta las flechas: 2×3 = 6 pesos hacia la capa oculta, 3×1 = 3 pesos hacia la salida, más 3+1 sesgos. Total: 13 parámetros entrenables. (GPT-4 tiene del orden de 10¹² — la mecánica es la misma, cambia la escala.)

5.2 Notación matricial: W y b por capa

Con muchas neuronas, escribir pesos uno a uno es inviable. Agrupamos: todos los pesos que entran a la capa (l) forman la matriz (W^{[l]}), y los sesgos el vector (b^{[l]}). El forward de la capa (l) es:

[ Z^{[l]} = A^{[l-1]} W^{[l]} + b^{[l]} \qquad A^{[l]} = f^{[l]}(Z^{[l]}) ]

donde (A^{[0]} = X) (los datos). Convención de esta academia (la misma de sklearn y PyTorch para los datos): cada fila de X es un ejemplo, cada columna una feature. Entonces (W^{[l]}) tiene shape (n_entradas_capa, n_neuronas_capa): cada columna de W son los pesos de una neurona.

Nota

en muchos libros (p. ej. el curso de Andrew Ng) verás la convención transpuesta (Z = W X + b) con ejemplos en columnas. Ninguna es "la correcta"; lo importante es elegir una y ser consistente. Usamos filas-como-ejemplos porque es lo que encontrarás en el 95 % del código profesional en Python.

5.3 Qué representa cada capa: la jerarquía de features

¿Qué "hace" cada capa? Aprende detectores de patrones sobre la salida de la capa anterior. El ejemplo canónico (verificado empíricamente al visualizar CNNs entrenadas en caras, Zeiler & Fergus 2014, Olah et al. 2017):

graph TD
    A["Píxeles crudos<br/>(150 528 números sin significado)"] --> B["Capa 1: detectores de BORDES<br/>líneas, esquinas, gradientes de color"]
    B --> C["Capas intermedias: FORMAS<br/>círculos, texturas, ojos, narices"]
    C --> D["Capas profundas: PARTES y OBJETOS<br/>rostros completos, poses"]
    D --> E["Capa de salida: DECISIÓN<br/>¿es la persona X? probabilidad"]

Cada capa opera a un nivel de abstracción mayor porque compone los detectores de la anterior: un "ojo" es una combinación de bordes curvos; una "cara" es una combinación de ojos, nariz y boca en cierta disposición. Esta composición jerárquica es lo que las features manuales del ML clásico no podían dar: nadie sabe escribir a mano el detector de "nariz vista desde 30 grados".

Nuestro MLP para XOR hace la versión mínima de esto: su capa oculta aprende detectores tipo OR/NAND (representaciones intermedias), y la salida los combina.

5.4 ¿Cuántas capas? ¿Cuántas neuronas? Heurísticas honestas

La respuesta incómoda: no hay fórmula. Es un hiperparámetro que se ajusta con validación (módulo 02). Pero hay heurísticas defendibles:

Situación Punto de partida razonable
Datos tabulares, problema no lineal simple 1–2 capas ocultas de 16–128 neuronas
El baseline lineal ya funciona casi perfecto No uses red: quédate con el lineal
La red no logra ni sobreajustar el train Aumenta capacidad (más neuronas/capas) — primero anchura, luego profundidad
La red sobreajusta (train ≫ validación) Reduce capacidad o regulariza (capítulo 4 del módulo)
Imágenes / secuencias No uses MLP plano: CNNs/Transformers (capítulos siguientes)

Principios que sí son sólidos:

  1. Empieza pequeño y crece. Una red diminuta que funciona regular es mejor punto de partida que un monstruo que no puedes depurar.
  2. Comprueba que puedes sobreajustar un subconjunto pequeño. Si tu red no puede memorizar 100 ejemplos, hay un bug, no un problema de capacidad. Es el test de humo estándar de la profesión.
  3. La profundidad ayuda cuando el problema es composicional (features de features). Para una frontera de decisión suave en 2D, más de 2 capas no aporta.
  4. Potencias de 2 (32, 64, 128) por neuronas: no hay magia matemática, es una convención por eficiencia de hardware y comparabilidad.

Advertencia

desconfía de reglas tipo "la capa oculta debe tener la media geométrica de entrada y salida" que circulan por internet. No tienen respaldo empírico serio. La única regla validada es: prueba con validación honesta.

5.5 Contar parámetros: el ejercicio que todo ingeniero hace de cabeza

Saber cuántos parámetros tiene una arquitectura es una habilidad profesional básica (dimensiona memoria, coste de entrenamiento y riesgo de sobreajuste). La fórmula por capa densa es siempre la misma:

[ \text{parámetros de la capa } l = \underbrace{n_{l-1} \times n_l}{W^{[l]}} + \underbrace{n_l} ]}

Ejemplo con una red 784-128-32-10 (MNIST clásico con dos capas ocultas):

Capa Transformación Pesos W Sesgos b Subtotal
Oculta 1 784 → 128 100 352 128 100 480
Oculta 2 128 → 32 4 096 32 4 128
Salida 32 → 10 320 10 330
Total 104 938

Observa dónde vive el coste: el 96 % de los parámetros están en la primera capa, porque la entrada es enorme. Esta asimetría es la razón histórica de las CNNs: compartir pesos para no pagar entrada × neuronas con imágenes grandes. Lo verás en el capítulo correspondiente.

Ejercicio rápido 5.1

Una red para clasificar sensores tiene arquitectura 20-64-64-3 (3 clases de fallo). ¿Cuántos parámetros entrenables tiene en total? ¿Qué activación pondrías en cada capa?

Ver solución - Capa 1: 20×64 + 64 = 1 344 - Capa 2: 64×64 + 64 = 4 160 - Salida: 64×3 + 3 = 195 - **Total: 5 699 parámetros.** Activaciones: ReLU en las dos capas ocultas (estándar 2026 para MLPs) y **softmax** en la salida (3 clases mutuamente excluyentes), emparejada con cross-entropy categórica. Bonus de criterio: con 5 699 parámetros necesitarías, como regla de andar por casa, bastantes más de 5 699 ejemplos de entrenamiento para no sobreajustar sin regularización.

6. Forward pass completo en NumPy

Llegó la hora de construir. El forward pass (pasada hacia delante) es el cálculo de la predicción: los datos entran por la capa de entrada y fluyen capa a capa hasta la salida. Es lo único que la red hace en producción; el entrenamiento (secciones 8–9) existe solo para encontrar buenos pesos para este forward.

6.1 Inicialización: por qué NO ceros (la simetría, demostrada)

Primera decisión: ¿con qué valores nacen W y b? La intuición ingenua dice "ceros, neutral". Es un error fatal, y no por magia sino por simetría:

Si todos los pesos de una capa son idénticos (por ejemplo 0), entonces todas las neuronas de esa capa computan exactamente lo mismo: misma entrada × mismos pesos = misma salida. Y (adelantando el capítulo 2) recibirán exactamente el mismo gradiente, así que se actualizarán exactamente igual… y seguirán siendo idénticas para siempre. Tu capa de 128 neuronas es funcionalmente una neurona repetida 128 veces. La simetría nunca se rompe.

Compruébalo:

import numpy as np

X = np.array([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])   # datos XOR

# Capa oculta 2->3 inicializada A CERO
W1 = np.zeros((2, 3))     # ¡mala idea!
b1 = np.zeros(3)

Z1 = X @ W1 + b1          # forward de la capa
print(Z1)
# [[0. 0. 0.]
#  [0. 0. 0.]
#  [0. 0. 0.]
#  [0. 0. 0.]]
# Las 3 neuronas dicen LO MISMO para TODOS los ejemplos.
# Y como computan lo mismo, aprenderán lo mismo: simetría eterna.

La solución: inicialización aleatoria pequeña. Aleatoria → rompe la simetría (cada neurona nace distinta y se especializa). Pequeña → evita saturar activaciones tipo sigmoide/tanh desde el primer paso (recuerda: |z| grande = zona plana = gradiente muerto).

rng = np.random.default_rng(42)
W1 = rng.normal(0, 0.5, size=(2, 3))   # normal(media 0, desviación 0.5): pequeña y aleatoria
b1 = np.zeros(3)                        # el sesgo SÍ puede ser 0: la asimetría de W ya basta

Nota

en redes serias, la escala "pequeña" se calcula con fórmulas que dependen del tamaño de capa: Xavier/Glorot (std = sqrt(1/n_entradas), para tanh/sigmoide) y He (std = sqrt(2/n_entradas), para ReLU). Las estudiaremos al detalle en el capítulo 2, cuando entiendas por qué la varianza del gradiente importa. Para nuestras redes diminutas, normal(0, 0.5) es suficiente.

6.2 La red 2-3-1 para XOR: forward vectorizado

Ahora el forward completo, procesando los 4 ejemplos a la vez como matriz. Nada de bucles sobre ejemplos: álgebra matricial (módulo 01). Primero, la tabla de shapes — apréndela, porque los errores de dimensiones son la fuente nº 1 de bugs en deep learning:

Tensor Shape Significado
X (4, 2) 4 ejemplos (filas) × 2 features (columnas)
W1 (2, 3) 2 entradas → 3 neuronas ocultas (una columna por neurona)
b1 (3,) un sesgo por neurona oculta (se difunde por broadcasting a las 4 filas)
Z1 = X @ W1 + b1 (4, 2)@(2, 3) → (4, 3) pre-activación oculta: 4 ejemplos × 3 neuronas
A1 = tanh(Z1) (4, 3) activación oculta (misma shape: la activación es elemento a elemento)
W2 (3, 1) 3 neuronas ocultas → 1 salida
b2 (1,) sesgo de la salida
Z2 = A1 @ W2 + b2 (4, 3)@(3, 1) → (4, 1) pre-activación de salida
A2 = sigmoid(Z2) (4, 1) probabilidad predicha para cada ejemplo

La regla mnemotécnica para no perderte jamás: en (m, n) @ (n, p) → (m, p), las dimensiones interiores deben coincidir y desaparecen; las exteriores sobreviven. La dimensión de ejemplos (4) atraviesa toda la red intacta; la dimensión de features va mutando: 2 → 3 → 1.

import numpy as np

rng = np.random.default_rng(42)          # semilla fija: resultados reproducibles

# ---------- activaciones ----------
def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))      # aplasta a (0,1): salida binaria

def tanh(z):
    return np.tanh(z)                     # aplasta a (-1,1): capa oculta

# ---------- datos: XOR ----------
X = np.array([[0., 0.],
              [0., 1.],
              [1., 0.],
              [1., 1.]])                  # shape (4, 2): 4 ejemplos, 2 features
y = np.array([[0.], [1.], [1.], [0.]])   # shape (4, 1): etiqueta en columna,
                                          # para que case con la salida A2 (4,1)

# ---------- parámetros de la red 2-3-1 ----------
def inicializar_parametros(rng):
    """Crea todos los W y b de la red y los devuelve en un diccionario."""
    return {
        "W1": rng.normal(0, 0.5, size=(2, 3)),  # capa oculta: 2 entradas -> 3 neuronas
        "b1": np.zeros(3),                       # sesgos ocultos (cero está bien aquí)
        "W2": rng.normal(0, 0.5, size=(3, 1)),  # capa salida: 3 ocultas -> 1 salida
        "b2": np.zeros(1),                       # sesgo de salida
    }

params = inicializar_parametros(rng)

# ---------- forward pass ----------
def forward(X, params):
    """Pasada hacia delante: de las features a la probabilidad predicha.

    Devuelve la predicción. Cada línea comenta su shape:
    esa disciplina te ahorrará horas de depuración.
    """
    Z1 = X @ params["W1"] + params["b1"]   # (4,2)@(2,3)+(3,) -> (4,3)  pre-act oculta
    A1 = tanh(Z1)                          # (4,3)  no-linealidad: aquí se "dobla" el espacio
    Z2 = A1 @ params["W2"] + params["b2"]  # (4,3)@(3,1)+(1,) -> (4,1)  pre-act salida
    A2 = sigmoid(Z2)                       # (4,1)  probabilidad de clase 1 por ejemplo
    return A2

y_prob = forward(X, params)                # predicción con pesos ALEATORIOS
print("Probabilidades iniciales (red sin entrenar):")
print(np.round(y_prob, 3).ravel())         # ravel: aplanar solo para imprimir bonito
print("Etiquetas reales:", y.ravel())

Salida:

Probabilidades iniciales (red sin entrenar):
[0.5   0.594 0.529 0.617]
Etiquetas reales: [0. 1. 1. 0.]

La red sin entrenar predice ~0.5 para todo: básicamente lanza una moneda. Correcto y esperable — los pesos son ruido. La arquitectura ya puede representar XOR (lo demostramos en 3.5); lo que falta es encontrar los pesos, y de eso van las secciones 7 y 8.

Fíjate en el broadcasting: b1 tiene shape (3,) y se suma a una matriz (4,3). NumPy replica automáticamente el vector en las 4 filas — exactamente lo que queremos (el mismo sesgo para todos los ejemplos). Repasa el módulo 01 si el broadcasting aún te resulta mágico: aquí deja de ser opcional.

6.2b Blindaje profesional: asserts de shapes

Un hábito que separa el código de estudiante del código de ingeniero: verificar las shapes en tiempo de ejecución, no en tu cabeza. Un assert cuesta una línea y convierte el error silencioso nº 2 de la tabla final (broadcasting fantasma) en un fallo ruidoso e inmediato con mensaje útil:

def forward_blindado(X, params):
    """Forward con verificación explícita de dimensiones en cada frontera."""
    m, n_features = X.shape                       # m ejemplos, n features
    assert n_features == params["W1"].shape[0], \
        f"X tiene {n_features} features pero W1 espera {params['W1'].shape[0]}"

    Z1 = X @ params["W1"] + params["b1"]          # (m,2)@(2,3) -> (m,3)
    assert Z1.shape == (m, params["W1"].shape[1]), f"Z1 salió {Z1.shape}"

    A1 = np.tanh(Z1)                              # (m,3): elemento a elemento
    A2 = sigmoid(A1 @ params["W2"] + params["b2"])  # (m,3)@(3,1) -> (m,1)
    assert A2.shape == (m, 1), f"la salida debería ser ({m},1), salió {A2.shape}"
    return A2

Consejo profesional

en depuración, el reflejo correcto ante cualquier error de álgebra es imprimir tensor.shape de TODOS los implicados antes de tocar nada más. El 90 % de los bugs de redes se resuelven mirando shapes, no fórmulas. Los frameworks modernos añaden herramientas (como los named tensors), pero el hábito del assert es universal y gratis.

6.3 De probabilidad a predicción

def predecir(X, params, umbral=0.5):
    """Convierte probabilidades en clases 0/1 cortando en el umbral."""
    return (forward(X, params) >= umbral).astype(int)   # booleano -> entero

print(predecir(X, params).ravel())   # [0 1 1 1] o similar: aún falla (sin entrenar)

Ejercicio rápido 6.1

Sin ejecutar nada: una red procesa un batch de 64 imágenes aplanadas de 784 píxeles, con dos capas ocultas de 128 y 32 neuronas y salida de 10 clases. Escribe las shapes de X, W1, Z1, W2, Z2, W3, Z3.

Ver solución - `X`: `(64, 784)` — 64 ejemplos, 784 features. - `W1`: `(784, 128)` → `Z1 = X@W1`: `(64, 128)`. - `W2`: `(128, 32)` → `Z2`: `(64, 32)`. - `W3`: `(32, 10)` → `Z3`: `(64, 10)` — 10 logits por ejemplo, listos para softmax. Patrón: el 64 (batch) sobrevive intacto; la segunda dimensión encadena 784→128→32→10. Si en tu código un `@` falla con `shapes not aligned`, imprime `.shape` de ambos operandos y busca dónde se rompió la cadena.

7. Medir el error: MSE y cross-entropy

Para entrenar necesitamos un número que diga cuán mal predice la red: la función de pérdida (loss). Ya la conoces del módulo 02; repasamos las dos esenciales y — más importante — cuál usar cuándo y por qué.

7.1 MSE: el error cuadrático medio (regresión)

[ \text{MSE} = \frac{1}{m}\sum_{i=1}^{m}(\hat{y}_i - y_i)^2 ]

Penaliza la distancia al valor real, al cuadrado (los errores grandes duelen desproporcionadamente). Es la elección natural cuando la salida es un número continuo: precio, temperatura, demanda.

def mse(y_real, y_pred):
    """Error cuadrático medio: pérdida estándar de regresión."""
    return np.mean((y_pred - y_real) ** 2)   # media de los errores al cuadrado

7.2 Cross-entropy: el castigo logarítmico (clasificación)

Para clasificación binaria, la entropía cruzada binaria (binary cross-entropy, BCE):

[ \text{BCE} = -\frac{1}{m}\sum_{i=1}^{m}\Big[y_i \log(\hat{p}_i) + (1-y_i)\log(1-\hat{p}_i)\Big] ]

Léela así: si la etiqueta es 1, la pérdida del ejemplo es (-\log(\hat{p})) — el logaritmo negativo de la probabilidad que asignaste a la clase correcta. Si la etiqueta es 0, es (-\log(1-\hat{p})), lo mismo desde el otro lado.

¿Por qué cross-entropy y no MSE para clasificar? Dos razones, una intuitiva y una técnica:

1. El castigo logarítmico. MSE trata el error de forma "amable": equivocarte con confianza cuesta como mucho 1. Cross-entropy castiga la confianza injustificada de forma explosiva. Números concretos con etiqueta real y = 1:

Probabilidad predicha p̂ Error MSE = (1−p̂)² Error BCE = −log(p̂) Lectura
0.9 (acierto confiado) 0.01 0.105 ambas contentas
0.5 (indeciso) 0.25 0.693 BCE ya duele el doble
0.1 (error confiado) 0.81 2.303 BCE castiga fuerte
0.01 (error MUY confiado) 0.98 4.605 MSE casi no distingue 0.1 de 0.01…
0.001 (error temerario) 0.998 6.908 …BCE sigue escalando sin techo

Con MSE, pasar de "error confiado" (0.1) a "error temerario" (0.001) apenas mueve la pérdida (0.81 → 0.998). Con cross-entropy la pérdida se triplica (2.3 → 6.9). Un clasificador debe aprender que decir "99.9 % seguro" y fallar es gravísimo — cross-entropy codifica exactamente esa ética.

2. La razón técnica (adelanto del capítulo 2): MSE combinado con sigmoide produce gradientes que se desvanecen justo cuando la red está muy equivocada (la sigmoide saturada aplana la pérdida), o sea, aprende más despacio cuanto peor va — absurdo. Cross-entropy + sigmoide se combinan matemáticamente de modo que el gradiente es proporcional al error (p̂ − y): cuanto más te equivocas, más fuerte corriges. Elegancia pura que demostraremos en el capítulo 2.

def binary_cross_entropy(y_real, y_prob, eps=1e-12):
    """Entropía cruzada binaria, la pérdida de clasificación binaria.

    eps evita log(0) = -inf si la red predice exactamente 0.0 o 1.0.
    """
    p = np.clip(y_prob, eps, 1 - eps)          # acotar probabilidades a (eps, 1-eps)
    perdidas = -(y_real * np.log(p) + (1 - y_real) * np.log(1 - p))  # pérdida por ejemplo
    return np.mean(perdidas)                    # promedio sobre el batch

# Sanidad: pérdida de la red XOR sin entrenar
print(f"BCE inicial: {binary_cross_entropy(y, forward(X, params)):.4f}")
# BCE inicial: 0.7280  -> cerca de log(2)=0.693, la pérdida de "responder al azar". Coherente.

Nota

log(2) ≈ 0.693 es tu punto de referencia mental para BCE: es la pérdida de un modelo que siempre dice 0.5. Si tu red entrena y la BCE se queda clavada cerca de 0.693, la red no está aprendiendo nada. Para K clases con softmax, la referencia equivalente es log(K).

Advertencia

el clip con eps no es paranoia. Sin él, una sola predicción exacta de 0.0 con etiqueta 1 produce -inf, que se propaga como nan a toda la media y a todos los gradientes. Una red entera muerta por un logaritmo. Ponlo siempre.

7.2b Cross-entropy categórica: la versión multiclase

Cuando hay K clases (salida softmax), la generalización es directa: la pérdida de cada ejemplo es el logaritmo negativo de la probabilidad asignada a su clase correcta, y las demás probabilidades solo influyen a través de la normalización del softmax:

[ \text{CE} = -\frac{1}{m}\sum_{i=1}^{m} \log\big(\hat{p}_{i,\,y_i}\big) ]

def categorical_cross_entropy(y_indices, y_prob, eps=1e-12):
    """Entropía cruzada multiclase con etiquetas como índices enteros.

    y_indices: shape (m,)  — la clase correcta de cada ejemplo (0..K-1)
    y_prob:    shape (m,K) — salida del softmax, una distribución por fila
    """
    m = y_prob.shape[0]                          # número de ejemplos del batch
    p = np.clip(y_prob, eps, 1.0)                # blindaje contra log(0)
    # Indexado avanzado: para cada fila i, tomamos la columna y_indices[i].
    # Es la probabilidad que la red dio a la clase CORRECTA de cada ejemplo.
    prob_correctas = p[np.arange(m), y_indices]  # shape (m,)
    return np.mean(-np.log(prob_correctas))      # castigo logarítmico promedio

# Mini-verificación: 2 ejemplos, 3 clases
probs = np.array([[0.7, 0.2, 0.1],    # ejemplo 0: la red apuesta por la clase 0
                  [0.1, 0.1, 0.8]])   # ejemplo 1: apuesta por la clase 2
print(categorical_cross_entropy(np.array([0, 2]), probs))   # 0.2899: acierta confiada, pérdida baja
print(categorical_cross_entropy(np.array([1, 0]), probs))   # 1.9560: falla en ambos, pérdida alta

Nota

la línea p[np.arange(m), y_indices] es el patrón NumPy más importante de esta sección: selecciona un elemento por fila usando indexado avanzado, sin bucles ni matrices one-hot gigantes. Los frameworks lo llaman negative log-likelihood con etiquetas "sparse". Memoriza el patrón: lo reutilizarás en el capítulo 2 al derivar el gradiente de softmax + cross-entropy.

7.3 Resumen: qué pérdida con qué salida

Tarea Activación de salida Pérdida Referencia "modelo tonto"
Regresión Ninguna (identidad) MSE (o MAE si hay outliers) varianza de y
Clasificación binaria Sigmoide (1 neurona) Binary cross-entropy log 2 ≈ 0.693
Clasificación K clases Softmax (K neuronas) Categorical cross-entropy log K
Multi-etiqueta (varias a la vez) Sigmoide (K neuronas independientes) BCE por etiqueta log 2 por etiqueta

8. Entrenamiento con gradiente numérico: la red XOR aprende

8.1 La idea: gradiente por diferencias finitas

Del módulo 02 sabes que entrenar = gradiente descendente: calcula cuánto cambia la pérdida al mover cada parámetro (el gradiente) y muévelos en la dirección que la reduce. El problema aquí: nuestra pérdida atraviesa dos capas de matrices y activaciones. ¿Cómo se deriva eso? La respuesta eficiente es backpropagation (capítulo 2). La respuesta de fuerza bruta, que usaremos hoy, es volver a la definición de derivada:

[ \frac{\partial L}{\partial \theta_j} \approx \frac{L(\theta_j + \epsilon) - L(\theta_j - \epsilon)}{2\epsilon} ]

Para CADA parámetro: súbelo un pelín (ε), mide la pérdida; bájalo un pelín, mide la pérdida; la pendiente es (diferencia / 2ε). Es la diferencia finita central (más precisa que la versión de un solo lado, con error O(ε²) en lugar de O(ε)).

El coste escandaloso: cada derivada parcial requiere 2 forwards completos. Nuestra red tiene 13 parámetros → 26 forwards por paso de entrenamiento. Una red pequeña de verdad (100 000 parámetros) necesitaría 200 000 forwards por paso. GPT-4: ~10¹² parámetros → 2×10¹² forwards por paso. Universos de tiempo. Backprop calculará TODO el gradiente al precio de ~2 forwards en total, no 2 por parámetro. Hoy pagamos el precio bruto a cambio de simplicidad conceptual total: no hay ni una derivada simbólica en el código que sigue.

8.2 Implementación completa

import numpy as np

rng = np.random.default_rng(42)

# ---------- activaciones y pérdida (de las secciones anteriores) ----------
def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

def binary_cross_entropy(y_real, y_prob, eps=1e-12):
    p = np.clip(y_prob, eps, 1 - eps)
    return np.mean(-(y_real * np.log(p) + (1 - y_real) * np.log(1 - p)))

# ---------- datos y red ----------
X = np.array([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
y = np.array([[0.], [1.], [1.], [0.]])

params = {
    "W1": rng.normal(0, 0.5, size=(2, 3)),  # capa oculta 2->3
    "b1": np.zeros(3),
    "W2": rng.normal(0, 0.5, size=(3, 1)),  # capa salida 3->1
    "b2": np.zeros(1),
}

def forward(X, params):
    A1 = np.tanh(X @ params["W1"] + params["b1"])          # (4,3) capa oculta
    return sigmoid(A1 @ params["W2"] + params["b2"])       # (4,1) probabilidad

def perdida(params):
    """Pérdida total de la red con los parámetros actuales."""
    return binary_cross_entropy(y, forward(X, params))

# ---------- gradiente numérico por diferencias finitas ----------
def gradiente_numerico(params, eps=1e-5):
    """Aproxima dL/dθ para CADA número de CADA parámetro de la red.

    Recorre parámetro a parámetro, valor a valor:
    lo perturba +eps y -eps, mide la pérdida en ambos casos,
    y estima la pendiente central. Exacto hasta O(eps²), pero carísimo.
    """
    grads = {}
    for nombre, matriz in params.items():
        grad = np.zeros_like(matriz)               # gradiente con la misma shape
        # np.nditer nos da un índice para recorrer arrays de cualquier dimensión
        it = np.nditer(matriz, flags=["multi_index"])
        while not it.finished:
            idx = it.multi_index                   # posición (i,) o (i,j) actual
            valor_original = matriz[idx]           # guardamos para restaurar

            matriz[idx] = valor_original + eps     # perturbar hacia arriba
            perdida_mas = perdida(params)          # forward completo nº 1

            matriz[idx] = valor_original - eps     # perturbar hacia abajo
            perdida_menos = perdida(params)        # forward completo nº 2

            matriz[idx] = valor_original           # ¡restaurar! (imprescindible)
            grad[idx] = (perdida_mas - perdida_menos) / (2 * eps)  # pendiente central
            it.iternext()
        grads[nombre] = grad
    return grads

# ---------- bucle de entrenamiento ----------
lr = 0.5                                            # learning rate (paso del descenso)
print(f"{'época':>6} | {'BCE':>8} | {'aciertos':>8}")
for epoca in range(2001):
    grads = gradiente_numerico(params)              # 2 forwards POR PARÁMETRO (13x2=26)
    for nombre in params:                           # paso de gradiente descendente:
        params[nombre] -= lr * grads[nombre]        # θ <- θ - lr * dL/dθ
    if epoca % 400 == 0:                            # log periódico
        y_prob = forward(X, params)
        aciertos = int(np.sum((y_prob >= 0.5) == (y == 1)))
        print(f"{epoca:>6} | {perdida(params):>8.4f} | {aciertos:>7}/4")

print("\nPredicciones finales:")
print(np.round(forward(X, params), 3).ravel(), "  <- objetivo: [0, 1, 1, 0]")

Salida (reproducible con la semilla 42; tus decimales exactos pueden variar mínimamente según versión de NumPy):

 época |      BCE | aciertos
     0 |   0.7196 |      2/4
   400 |   0.3013 |      4/4
   800 |   0.0421 |      4/4
  1200 |   0.0189 |      4/4
  1600 |   0.0118 |      4/4
  2000 |   0.0085 |      4/4

Predicciones finales:
[0.008 0.992 0.99  0.009]   <- objetivo: [0, 1, 1, 0]

Léelo despacio, porque es un momento histórico personal: la pérdida baja de 0.72 (azar) a 0.009, y la red predice XOR casi perfecto: 0.008 donde debía decir 0, 0.992 donde debía decir 1. El problema que enterró al perceptrón en 1969 acaba de ser resuelto por 13 números ajustados con una derivada aproximada. No hay reglas programadas, no hay lógica booleana escrita: solo descenso por gradiente sobre una arquitectura con capa oculta.

8.3 Lo que acabas de comprar y lo que pagaste

Diagrama del ciclo que acabamos de ejecutar (y que es EL ciclo de todo el deep learning, con cualquier método de gradiente):

flowchart LR
    A["Inicializar W, b<br/>(aleatorio pequeño)"] --> B["Forward:<br/>X → capas → ŷ"]
    B --> C["Pérdida:<br/>comparar ŷ con y"]
    C --> D["Gradiente:<br/>∂L/∂θ para cada parámetro<br/>(hoy: dif. finitas; cap. 2: backprop)"]
    D --> E["Actualizar:<br/>θ ← θ − lr·∇L"]
    E --> F{"¿Converge?"}
    F -- no --> B
    F -- sí --> G["Red entrenada "]

Lo que pagaste: 2001 épocas × 26 forwards = 52 026 forwards para entrenar 13 parámetros sobre 4 ejemplos. Ridículamente caro. El capítulo 2 (backpropagation) obtiene el mismo gradiente — exacto, no aproximado — con ~2 pasadas por época: ~4000 pasadas en total, un ahorro de 13×, y la ventaja crece linealmente con el número de parámetros. Para una red de un millón de parámetros, backprop es un millón de veces más barato. Ahora ya sabes exactamente qué problema resuelve backprop y por qué es EL algoritmo del deep learning.

Consejo profesional

el gradiente numérico no es solo un juguete didáctico: es la herramienta profesional estándar para verificar implementaciones de backprop (gradient checking). Cuando en el capítulo 2 implementes tus derivadas a mano, compararás contra diferencias finitas para cazar bugs. Guarda la función gradiente_numerico: la reutilizarás.

Ejercicio rápido 8.1

En gradiente_numerico, ¿qué pasaría si olvidas la línea matriz[idx] = valor_original (restaurar)? ¿La red entrenaría?

Ver solución Cada parámetro quedaría desplazado −eps tras medir su gradiente, así que estarías midiendo cada gradiente sobre una red ya corrompida por las perturbaciones de los parámetros anteriores, y además aplicarías el paso de gradiente sobre parámetros desplazados. Con eps = 1e-5 el efecto es sutil: la red probablemente entrenaría "casi" bien, lo que lo convierte en un bug traicionero — no revienta, degrada. Los peores bugs de deep learning son así: el código corre, la pérdida baja, pero peor de lo que debería. Por eso el gradient checking compara números, no vibraciones.

9. Ejemplo integrador: clasificar las dos lunas

XOR tiene 4 puntos. Subamos a un dataset "real" de juguete: las dos lunas (make_moons de sklearn — usamos sklearn SOLO para generar datos; toda la red sigue siendo NumPy puro). Dos medialunas entrelazadas: imposibles de separar con una recta, perfectas para demostrar que la red dobla el espacio.

      Frontera imposible para un modelo lineal:

  x₂ |        ○ ○ ○ ○ ○
     |      ○ ○       ○ ○           ○ = clase 0 (luna superior)
     |     ○ ○          ○ ○         ● = clase 1 (luna inferior)
     |    ○ ○    ● ● ●    ○ ○
     |    ○ ○   ● ●  ● ●   ○ ○      Las lunas se abrazan:
     |         ● ●    ● ●           cualquier RECTA deja trozos
     |        ● ●      ● ● ●        de una luna al lado equivocado.
     |       ● ●         ● ● ●
     +----------------------------- x₁

9.1 Datos, red y entrenamiento — línea por línea

import numpy as np
from sklearn.datasets import make_moons          # SOLO para generar los datos
from sklearn.linear_model import LogisticRegression  # el rival lineal a batir
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(42)

# ---------- 1. Generar el dataset ----------
X, y = make_moons(n_samples=200, noise=0.20, random_state=42)
# X: (200, 2) coordenadas de los puntos | y: (200,) etiqueta 0/1
y = y.reshape(-1, 1).astype(float)               # (200,1): columna, para casar con la salida

# División train/test honesta (módulo 02: NUNCA evalúes sobre lo que entrenas)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42)

# ---------- 2. La red: 2 -> 8 -> 1 (más ancha que la de XOR: problema más rico) ----------
params = {
    "W1": rng.normal(0, 0.5, size=(2, 8)),       # 2 features -> 8 neuronas ocultas
    "b1": np.zeros(8),
    "W2": rng.normal(0, 0.5, size=(8, 1)),       # 8 ocultas -> 1 salida
    "b2": np.zeros(1),
}
# Parámetros totales: 2*8 + 8 + 8*1 + 1 = 33. Sigue siendo diminuta.

def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

def relu(z):
    return np.maximum(0, z)                       # esta vez usamos ReLU en la oculta

def forward(X, params):
    A1 = relu(X @ params["W1"] + params["b1"])    # (m,2)@(2,8) -> (m,8), ReLU dobla el espacio
    return sigmoid(A1 @ params["W2"] + params["b2"])  # (m,8)@(8,1) -> (m,1), probabilidad

def bce(y_real, y_prob, eps=1e-12):
    p = np.clip(y_prob, eps, 1 - eps)
    return np.mean(-(y_real * np.log(p) + (1 - y_real) * np.log(1 - p)))

def perdida(params):
    return bce(y_tr, forward(X_tr, params))       # pérdida SOLO sobre train

def gradiente_numerico(params, eps=1e-5):
    """Idéntica a la de la sección 8: diferencias finitas centrales."""
    grads = {}
    for nombre, matriz in params.items():
        grad = np.zeros_like(matriz)
        it = np.nditer(matriz, flags=["multi_index"])
        while not it.finished:
            idx = it.multi_index
            orig = matriz[idx]
            matriz[idx] = orig + eps; L_mas = perdida(params)     # forward nº1
            matriz[idx] = orig - eps; L_menos = perdida(params)   # forward nº2
            matriz[idx] = orig                                     # restaurar
            grad[idx] = (L_mas - L_menos) / (2 * eps)
            it.iternext()
        grads[nombre] = grad
    return grads

# ---------- 3. Entrenar ----------
lr = 0.5
print(f"{'época':>6} | {'BCE train':>9} | {'acc train':>9} | {'acc test':>8}")
for epoca in range(801):
    grads = gradiente_numerico(params)            # 33 params x 2 = 66 forwards por época(!)
    for nombre in params:
        params[nombre] -= lr * grads[nombre]      # descenso por gradiente
    if epoca % 160 == 0:
        acc_tr = np.mean((forward(X_tr, params) >= 0.5) == (y_tr == 1))
        acc_te = np.mean((forward(X_te, params) >= 0.5) == (y_te == 1))
        print(f"{epoca:>6} | {perdida(params):>9.4f} | {acc_tr:>9.3f} | {acc_te:>8.3f}")

# ---------- 4. El rival: regresión logística (modelo LINEAL, módulo 02) ----------
logreg = LogisticRegression().fit(X_tr, y_tr.ravel())
acc_logreg = logreg.score(X_te, y_te.ravel())
acc_red = float(np.mean((forward(X_te, params) >= 0.5) == (y_te == 1)))
print(f"\nAccuracy test  — logística (lineal): {acc_logreg:.3f}")
print(f"Accuracy test  — red 2-8-1 (NumPy):  {acc_red:.3f}")

Salida típica (semillas fijadas; pequeñas variaciones de decimales son normales entre versiones):

 época | BCE train | acc train | acc test
     0 |    0.6907 |     0.586 |    0.617
   160 |    0.3121 |     0.864 |    0.850
   320 |    0.2036 |     0.921 |    0.900
   480 |    0.1454 |     0.950 |    0.917
   640 |    0.1094 |     0.971 |    0.933
   800 |    0.0870 |     0.979 |    0.950

Accuracy test  — logística (lineal): 0.850
Accuracy test  — red 2-8-1 (NumPy):  0.950

9.2 Qué acaba de pasar (y la frontera de decisión)

  • La logística se queda en ~85 %: es lo máximo que da una recta en este dataset. Su frontera es literalmente una línea que corta ambas lunas por donde menos daño hace, condenada a equivocarse en las puntas entrelazadas.
  • La red alcanza ~95 %: su frontera de decisión es una curva en forma de S que serpentea entre las dos lunas, abrazando cada una. Descrita en ASCII:
      Frontera de la LOGÍSTICA          Frontera de la RED 2-8-1
      (una recta, ~85%)                 (curva aprendida, ~95%)

  x₂ |      ○ ○ ○ ○ \                x₂ |      ○ ○ ○ ○
     |    ○ ○      ○ \○                 |    ○ ○ ┌────┐○ ○
     |   ○ ○         ○\○                |   ○ ○ /      \ ○ ○
     |   ○ ○   ● ● ●   \○ ❌ mal        |   ○ ○ | ● ● ● \___
     |        ● ●  ● ●  \               |    \__/● ●  ● ●   \
     |       ● ●    ● ●  \              |        ● ●    ● ●  |
     |    ❌ ● ●      ● ● ●\             |       ● ●      ● ●/
     +---------------------\--- x₁      +--------------------- x₁
      La recta corta las puntas          La S serpentea entre lunas:
      de ambas lunas: errores            cada punta queda del lado
      estructurales inevitables.         correcto. El espacio se dobló.

¿De dónde sale esa curva? Cada una de las 8 neuronas ReLU de la capa oculta define un semiplano con bisagra (activa a un lado de una recta, cero al otro). La capa de salida combina esas 8 bisagras con pesos: la frontera resultante es una curva lineal a trozos con hasta 8 quiebros — suficientes para envolver una luna. Más neuronas = más trozos = fronteras más finas (y más riesgo de sobreajuste: equilibrio del módulo 02, que no ha dejado de aplicar).

Nota

para ver la frontera de verdad, evalúa forward sobre una rejilla de puntos (np.meshgrid) y dibuja con plt.contourf. Lo omitimos aquí solo porque este capítulo es texto; hazlo en tu notebook — ver tu primera frontera no lineal aprendida desde cero es de esos momentos que convierten estudiantes en ingenieros.

Advertencia

esta victoria de la red sobre la logística NO contradice la tabla de la sección 1. Las dos lunas son datos geométricos crudos (coordenadas), no features tabulares con significado — exactamente el terreno donde las redes brillan. Si las features fueran "ingresos" y "edad" con relación casi lineal con la etiqueta, la logística habría empatado con 33 parámetros menos y cero GPU.

Ejercicio rápido 9.1

Modifica el código: (a) sube noise=0.35 en make_moons; (b) sube las neuronas ocultas de 8 a 64. ¿Qué esperas que ocurra con acc_train y acc_test en cada caso, antes de ejecutar?

Ver solución (a) Con más ruido las lunas se solapan: el techo alcanzable baja para ambos modelos. La brecha red-vs-logística se estrecha porque parte de la ventaja no lineal queda enterrada en ruido irreducible (concepto de error de Bayes, módulo 02). (b) Con 64 neuronas (2·64+64+64+1 = 257 parámetros) sobre solo 140 ejemplos de train, acc_train subirá hacia 1.0 y acc_test probablemente **empeorará o no mejorará**: la red tiene capacidad de sobra para memorizar el ruido. Es el sobreajuste clásico. Además el gradiente numérico tardará ~8 veces más por época (257 vs 33 parámetros): sentirás en tu CPU por qué necesitamos backprop.

10. Caso empresarial: entrenar, adaptar o alquilar

Caso empresarial

Ya sabes construir una red. La pregunta que un ingeniero de IA responde en el mundo real no es "¿puedo entrenar una red?" sino "¿DEBE mi empresa entrenar una red?". En 2026 hay tres caminos, y elegir mal cuesta entre miles y millones de euros.

10.1 Los tres caminos

  1. Entrenar desde cero: tú diseñas la arquitectura y la entrenas con tus datos, desde pesos aleatorios (lo que hicimos en este capítulo, a escala).
  2. Adaptar un modelo preentrenado (fine-tuning / transfer learning): partes de un modelo ya entrenado por otros (ResNet, Whisper, un LLM abierto) y lo ajustas a tu tarea con relativamente pocos datos.
  3. Consumir una API: pagas por llamada a un modelo alojado por un proveedor (OpenAI, Anthropic, Google, etc.). Cero entrenamiento, cero infraestructura propia.

10.2 Tabla de decisión con costes realistas (órdenes de magnitud, 2026)

Criterio Entrenar desde cero Fine-tuning de preentrenado API de terceros
Coste inicial típico 10⁴–10⁷ € (datos+GPU+equipo); LLMs frontera: 10⁷–10⁹ € 10²–10⁵ € (horas de GPU + ingeniero) ~0 € inicial
Coste recurrente Infraestructura propia: 10³–10⁵ €/mes Servir el modelo: 10²–10⁴ €/mes Por uso: de 10¹ a 10⁵ €/mes según volumen
Datos necesarios 10⁴–10⁹ ejemplos etiquetados 10²–10⁵ ejemplos 0 (o pocos, para prompts/evaluación)
Talento necesario Equipo de ML senior (escaso y caro) 1–2 ingenieros de ML Desarrolladores generalistas
Tiempo hasta producción 6–18 meses 2 semanas–3 meses Días
Control y privacidad de datos Total Alto (el modelo es tuyo) Limitado (datos salen a un tercero, salvo despliegues privados)
Ventaja competitiva Máxima si tus datos son únicos Media-alta Nula (tu competencia usa la misma API)
Riesgo de dependencia Bajo Bajo-medio (licencias del modelo base) Alto (precios, límites y modelos cambian sin que decidas tú)
Cuándo elegirlo Datos propietarios masivos + tarea central del negocio + escala que amortiza Tarea específica con datos propios moderados Prototipos, tareas genéricas (resumen, chat), volumen bajo-medio

Regla de decisión en forma de diagrama:

flowchart TD
    A["¿La tarea es genérica<br/>(chat, resumen, OCR, traducción)?"] -- Sí --> B["¿Volumen bajo/medio y<br/>datos no ultrasensibles?"]
    B -- Sí --> C["API de terceros<br/>empieza HOY, mide costes"]
    B -- No --> D["Modelo abierto preentrenado<br/>desplegado en tu infraestructura"]
    A -- No --> E["¿Tienes datos propios<br/>abundantes y etiquetados?"]
    E -- No --> F["Consíguelos primero:<br/>sin datos no hay proyecto.<br/>Mientras tanto: API + humanos"]
    E -- Sí --> G["¿Existe un preentrenado<br/>cercano a tu dominio?"]
    G -- Sí --> H["Fine-tuning:<br/>el mejor ratio coste/beneficio"]
    G -- No --> I["¿La tarea es núcleo del negocio<br/>y la escala amortiza 10⁵-10⁷ €?"]
    I -- Sí --> J["Entrenar desde cero<br/>(con equipo senior y plan de MLOps)"]
    I -- No --> K["Replantea: features + boosting<br/>(módulo 02) suele bastar"]

10.3 Ejemplo narrado: Aceros del Norte S.A.

Una empresa industrial ficticia pero realista: fabricante de acero con 12 líneas de laminación, cada una con ~200 sensores (temperatura, vibración, presión) muestreando cada segundo. Problema: las paradas no planificadas por fallo de rodamientos cuestan ~80 000 € por incidente, unos 15 incidentes/año → 1.2 M€/año de pérdida.

Fase 1 — La tentación de la moda (evitada). El primer consultor propone "un LLM multimodal para mantenimiento predictivo". El equipo de datos (que hizo este módulo) señala lo obvio: los datos son series de sensores, no lenguaje. Un LLM genérico por API no sabe nada de los rodamientos de la línea 7, y enviar telemetría industrial confidencial a una API externa incomoda a seguridad. API descartada para el problema central (se queda, eso sí, para un asistente interno de documentación técnica: tarea genérica, camino 3, 400 €/mes).

Fase 2 — El baseline clásico. Antes de tocar redes: features manuales (medias móviles, varianza de vibración, picos espectrales) + gradient boosting. Dos semanas de trabajo, corre en CPU. Resultado: detecta el 60 % de los fallos con 24 h de antelación. Ya es dinero: ~700 k€/año ahorrados con un coste de ~30 k€. Lección: el baseline clásico primero, siempre. Es la tabla de la sección 1 aplicada con criterio.

Fase 3 — La red propia, justificada. El 40 % de fallos restante escapa a las features manuales: son patrones sutiles en la forma cruda de la onda de vibración que nadie sabe describir en columnas. Datos crudos + patrón desconocido = territorio de deep learning. La empresa tiene lo que hace falta: millones de horas de sensor propietario (que ningún preentrenado público cubre bien para SU maquinaria concreta), la tarea es núcleo del negocio, y el ahorro esperado (300–400 k€/año adicionales) amortiza el coste: 2 ingenieros de ML durante 8 meses + ~15 k€ de GPU en la nube ≈ 200 k€ de inversión. Entrenan una red 1D sobre ventanas de señal cruda (la verás en el capítulo de CNNs). Detección total sube al 85 %, retorno de la inversión en menos de un año.

El reparto final — y esto es lo normal en una empresa real: los tres caminos conviven:

Sistema Camino elegido Por qué
Asistente de documentación técnica API (camino 3) Tarea genérica de lenguaje, volumen bajo, sin datos sensibles
Clasificación de defectos visuales en chapa Fine-tuning de un modelo de visión preentrenado (camino 2) Existe preentrenado excelente; solo 5 000 fotos propias etiquetadas
Predicción de fallo por vibración Red propia desde cero (camino 1) Datos crudos propietarios masivos, tarea núcleo, sin preentrenado aplicable
Predicción de demanda (tabular) Gradient boosting, sin red Datos tabulares medianos: gana el clásico (sección 1)

Consejo profesional

en las entrevistas de ingeniería de IA, la pregunta "¿entrenarías un modelo para X?" casi nunca busca un sí entusiasta. Busca que preguntes: ¿qué datos hay y de quién son? ¿existe preentrenado? ¿cuál es el baseline clásico? ¿qué cuesta el error? Responder "primero probaría boosting y una API, y solo escalaría a red propia si los datos crudos propietarios lo justifican" te distingue del 90 % de candidatos.


11. Buenas prácticas

  1. Semilla fija en todo experimento (np.random.default_rng(42)). Sin reproducibilidad no hay ciencia ni depuración posible.
  2. Comenta la shape esperada en cada línea de álgebra matricial (# (m,2)@(2,8) -> (m,8)). Es la vacuna contra el error nº 1 del oficio.
  3. Verifica el punto de partida de la pérdida: BCE inicial ≈ log 2 ≈ 0.693 (binaria) o log K (multiclase). Si no, hay bug antes de empezar.
  4. Test de sobreajuste diminuto: antes de entrenar en serio, comprueba que tu red memoriza 10–100 ejemplos hasta pérdida ~0. Si no puede, el bug está en tu código, no en tus hiperparámetros.
  5. Baseline lineal siempre primero. Si la logística ya da el 99 %, la red es coste sin retorno.
  6. Inicialización aleatoria pequeña en W, ceros solo en b. Y en cuanto llegues al capítulo 2, usa He para ReLU y Xavier para tanh.
  7. Estabilidad numérica por defecto: softmax con resta del máximo, log con clip de eps. Los nan no avisan.
  8. Train/test separados desde el minuto uno, también en juguetes. El hábito se entrena en lo pequeño.
  9. Un cambio a la vez al ajustar (lr, arquitectura, activación). Dos cambios simultáneos = no sabes cuál actuó.
  10. Guarda la función de gradiente numérico: es tu herramienta de gradient checking para verificar backprop en el capítulo 2.

12. Malas prácticas

  1. Inicializar pesos a cero (o a cualquier constante): simetría eterna, la capa entera colapsa en una neurona (demostrado en 6.1).
  2. Usar MSE para clasificación: castigo insuficiente a la confianza errónea y gradientes que se apagan con sigmoide (sección 7.2).
  3. Sigmoide/tanh en capas ocultas profundas en 2026: saturación y desvanecimiento conocidos desde hace 20 años. ReLU/GELU.
  4. Softmax en capas ocultas: normaliza y destruye información. Solo en la salida.
  5. Elegir deep learning por moda para datos tabulares medianos: la evidencia favorece a boosting (sección 1.3). Justifica con datos, no con titulares.
  6. Entrenar sin baseline ni referencia de pérdida: "la loss baja" no significa nada si no sabes desde dónde ni hasta dónde debería.
  7. Learning rate a ojo sin observar la curva: demasiado alto → pérdida oscila o explota; demasiado bajo → eternidad. Mira siempre la curva de loss.
  8. Ignorar warnings de broadcasting: una suma (4,1) + (4,) produce silenciosamente una matriz (4,4) y métricas absurdas sin ningún error. (Véase tabla siguiente.)
  9. Evaluar en train y reportarlo como rendimiento: pecado capital heredado del módulo 02 que las redes, con su capacidad de memorizar, castigan el doble.
  10. Copiar arquitecturas gigantes para problemas diminutos: 257 parámetros para 140 ejemplos ya sobreajusta (ejercicio 9.1); imagina ResNet para 500 filas.

13. Errores comunes

# Error Síntoma Causa Solución
1 ValueError: shapes (4,2) and (3,2) not aligned El @ revienta W creada transpuesta respecto a tu convención Decide convención (filas=ejemplos) y comenta shapes en cada línea
2 Broadcasting fantasma: y (4,) + pred (4,1) → matriz (4,4) Pérdida/accuracy absurdas SIN error NumPy difunde vectores fila contra columna y.reshape(-1,1); verifica y.shape == pred.shape con un assert
3 Pérdida = nan a las pocas épocas Todo se vuelve nan log(0) o exp desbordada Clip con eps en el log; softmax estable; baja el learning rate
4 La pérdida no baja de ~0.693 Red clavada en azar Pesos a cero (simetría), lr = 0, o etiquetas desalineadas con X Inicialización aleatoria; imprime lr; verifica el emparejado X↔y
5 La pérdida baja y luego explota Zigzag creciente Learning rate demasiado alto Divide lr entre 10; observa la curva
6 Accuracy perfecta en train, mala en test Sobreajuste Demasiada capacidad para tan pocos datos Menos neuronas, más datos, regularización (capítulo 4)
7 Muchas neuronas ReLU siempre a 0 Capacidad efectiva desplomada Dying ReLU (lr alto o mala init) Leaky ReLU, lr menor, init He
8 Gradiente numérico ≈ 0 en todas partes No aprende nada eps demasiado pequeño (ahogado en error de coma flotante) o activaciones saturadas eps ≈ 1e-5; revisa la escala de la inicialización
9 Resultados distintos en cada ejecución Imposible comparar experimentos Semilla sin fijar (o fijada después de crear pesos) default_rng(semilla) al principio, antes de todo
10 Olvidar restaurar el parámetro en dif. finitas Entrena "raro", peor de lo esperado Perturbaciones acumuladas corrompen la red Restaurar matriz[idx] = orig siempre (ejercicio 8.1)

14. Preguntas frecuentes (FAQ)

1. ¿Una neurona artificial modela de verdad una neurona del cerebro? No. Comparte tres ideas de alto nivel (entradas ponderadas, integración, umbral no lineal) y nada más. Las neuronas biológicas tienen dinámica temporal, química y plasticidad que la artificial ni intenta capturar. La analogía es inspiración histórica, no biología computacional.

2. Si el perceptrón no puede con XOR, ¿por qué lo estudiamos en 2026? Porque contiene el ADN de todo lo demás: pesos aprendibles, regla de actualización por error y el límite (linealidad) cuya superación define al deep learning. Entender por qué falla el perceptrón es entender por qué existen las capas ocultas.

3. ¿Por qué la capa oculta resuelve XOR, exactamente? Porque transforma las coordenadas: en el espacio de salidas de la capa oculta (h₁, h₂, ...), los cuatro puntos de XOR quedan recolocados de forma linealmente separable, y la capa de salida solo tiene que trazar su recta ahí. La red no curva la frontera en el espacio original: cambia de espacio hasta que una recta basta.

4. ¿Puedo usar cualquier función no lineal como activación? Casi cualquiera rompe el colapso lineal, pero no todas entrenan bien: necesitas derivadas útiles (ni nulas en casi todas partes como el escalón, ni desvanecientes como la sigmoide profunda), coste bajo y estabilidad numérica. Por eso el catálogo práctico es corto: ReLU y familia, GELU, tanh en nichos, sigmoide/softmax en salidas.

5. ¿Más capas es siempre mejor? No. Más capas = más capacidad, que solo paga cuando el problema es composicional y hay datos para alimentarla. Para las dos lunas, una capa oculta basta; añadir cinco solo suma coste, riesgo de sobreajuste y (sin las técnicas del capítulo 4) problemas de entrenamiento. La profundidad se gana con evidencia de validación, no se presupone.

6. ¿Por qué mi red da resultados distintos a los del capítulo si copié el código? Comprueba, por orden: (1) la semilla y su posición (debe fijarse ANTES de crear los pesos), (2) la versión de NumPy (los generadores pueden variar entre versiones mayores), (3) el orden de creación de parámetros (consume el generador en secuencia: crear W2 antes que W1 cambia ambos). Diferencias pequeñas de decimales son normales; conclusiones distintas, no.

7. ¿El gradiente numérico se usa en producción? Jamás para entrenar (coste proporcional al nº de parámetros × 2 forwards). Su uso profesional es el gradient checking: verificar contra diferencias finitas que una implementación de backprop es correcta. Los frameworks modernos (PyTorch, JAX) usan diferenciación automática, que es exacta y eficiente — la entenderás al implementar backprop en el capítulo 2.

8. ¿Cuándo debería mi empresa entrenar una red desde cero en lugar de usar una API? Casi nunca como primera opción. El orden racional: (1) baseline clásico/boosting si es tabular, (2) API si la tarea es genérica y el volumen moderado, (3) fine-tuning de preentrenado si hay datos propios y un modelo base cercano, (4) desde cero solo con datos propietarios masivos, tarea núcleo del negocio y escala que amortice 10⁵–10⁷ €. Revisa el diagrama de la sección 10.2.

9. ¿Sirve el MLP de este capítulo para imágenes o texto reales? Conceptualmente sí, prácticamente no: un MLP denso sobre píxeles ignora la estructura espacial (y explota en parámetros), y sobre texto ignora el orden. Por eso existen CNNs (estructura espacial) y Transformers (secuencias), que estudiarás en este módulo — ambos son, por dentro, las mismas piezas de hoy: matrices, sesgos y activaciones.

10. ¿Qué diferencia hay entre "MLP", "red densa", "feedforward" y "fully connected"? En la práctica, ninguna: son nombres del mismo objeto. "Feedforward" subraya que la información solo fluye hacia delante (sin ciclos, a diferencia de las RNN); "densa/fully connected" subraya que cada neurona conecta con todas las de la capa anterior; "MLP" es el nombre histórico.

15. Resumen del capítulo

  • El ML clásico depende de features manuales; el deep learning aprende las representaciones de datos crudos por capas de abstracción. Brilla con imágenes, audio y texto; para tabular mediano, el boosting sigue mandando.
  • Una neurona artificial es un producto punto más sesgo más no-linealidad: a = f(w·x + b). Una neurona sigmoide es una regresión logística; la red es la composición entrenada en conjunto.
  • El perceptrón (1958) aprende cualquier problema linealmente separable — y solo esos. XOR lo derrota (demostrado en código: 1000 épocas oscilando) y una capa oculta lo resuelve transformando el espacio hasta hacerlo separable.
  • Sin activaciones no lineales, cualquier pila de capas colapsa a UNA transformación lineal (demostrado algebraica y numéricamente). Catálogo: sigmoide (satura; solo salidas binarias), tanh (centrada, satura), ReLU (la estándar: derivada 1 en positivo, barata; riesgo dying), Leaky ReLU (la cura), GELU (Transformers), softmax (solo salida multiclase, siempre estabilizada).
  • El MLP organiza neuronas en capas densas: (Z^{[l]} = A^{[l-1]}W^{[l]} + b^{[l]}), (A^{[l]} = f(Z^{[l]})). Cada capa aprende detectores sobre la anterior: bordes → formas → objetos.
  • El forward pass es álgebra matricial disciplinada: la dimensión de ejemplos atraviesa la red intacta y las features mutan capa a capa. Los pesos nacen aleatorios y pequeños (jamás cero: simetría) y los sesgos a cero.
  • Pérdidas: MSE para regresión, cross-entropy para clasificación — su castigo logarítmico a la confianza errónea y sus buenos gradientes con sigmoide/softmax la hacen obligatoria. Referencia mental: BCE de azar ≈ 0.693.
  • El gradiente numérico (diferencias finitas centrales) entrena de verdad — nuestra red 2-3-1 aprendió XOR y la 2-8-1 clasificó las dos lunas al 95 %, batiendo a la logística (85 %) — pero cuesta 2 forwards POR PARÁMETRO. Ese coste escandaloso es exactamente el problema que backpropagation resuelve en el próximo capítulo.
  • Empresarialmente: API → fine-tuning → desde cero, en ese orden de consideración, con el baseline clásico siempre delante. Los tres caminos conviven en una misma empresa.

Próximo capítulo: abrimos la caja del gradiente. Regla de la cadena, backpropagation implementado a mano, verificación con el gradiente numérico de hoy, e inicializaciones He/Xavier con su porqué. La red de las dos lunas pasará de entrenar en minutos a entrenar en milisegundos.

16. Bibliografía y recursos

  • Rosenblatt, F. (1958). The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain. Psychological Review, 65(6). — El artículo fundacional.
  • Minsky, M. & Papert, S. (1969). Perceptrons: An Introduction to Computational Geometry. MIT Press. — La demostración de los límites (XOR incluido).
  • Rumelhart, D., Hinton, G. & Williams, R. (1986). Learning representations by back-propagating errors. Nature, 323. https://www.nature.com/articles/323533a0 — El paper que revivió las redes (capítulo 2).
  • Cybenko, G. (1989). Approximation by superpositions of a sigmoidal function. — Teorema de aproximación universal.
  • Nielsen, M. Neural Networks and Deep Learning (libro online gratuito): http://neuralnetworksanddeeplearning.com/ — La mejor introducción complementaria; su capítulo 1 cubre este mismo terreno.
  • Goodfellow, I., Bengio, Y. & Courville, A. (2016). Deep Learning. MIT Press. Gratis en: https://www.deeplearningbook.org/ — Capítulo 6 (Deep Feedforward Networks) para profundizar en MLPs.
  • Hendrycks, D. & Gimpel, K. (2016). Gaussian Error Linear Units (GELUs): https://arxiv.org/abs/1606.08415 — El paper de GELU.
  • Grinsztajn, L. et al. (2022). Why do tree-based models still outperform deep learning on typical tabular data?: https://arxiv.org/abs/2207.08815 — La evidencia detrás de la tabla de la sección 1.
  • Karpathy, A. Neural Networks: Zero to Hero (vídeos): https://karpathy.ai/zero-to-hero.html — Construcción desde cero en Python, espíritu idéntico al de este módulo.
  • Olah, C. et al. Feature Visualization (Distill, 2017): https://distill.pub/2017/feature-visualization/ — La jerarquía bordes→formas→objetos, visualizada en redes reales.
  • Documentación de NumPy (broadcasting y álgebra lineal): https://numpy.org/doc/stable/user/basics.broadcasting.html
  • 3Blue1Brown, But what is a neural network? (vídeo, subtítulos en español): https://www.youtube.com/watch?v=aircAruvnKk — La mejor intuición visual del forward pass.

Anterior Módulo Siguiente
README del módulo 03-DEEP-LEARNING Capítulo 2: Backpropagation y entrenamiento