Cerramos el módulo 4 con una pregunta de Marta y Diego: el predictor de devoluciones ya está validado (AUC 0,84 con la regresión logística), pero ¿puede un modelo leer las reseñas de resenas.csv o mirar las fotos de las incidencias? Los algoritmos clásicos necesitan que alguien convierta antes el texto o la imagen en columnas numéricas con significado; las redes neuronales, en cambio, aprenden ellas mismas esa conversión. Este módulo explica cómo. En esta primera lección empezamos por la pieza básica: la neurona artificial, que resultará ser algo que ya conoces (la regresión logística de 04-04, vista con otros ojos); el perceptrón de Rosenblatt de 1958, que implementaremos desde cero en numpy y entrenaremos sobre las puertas lógicas y sobre un problema de NovaMarket; el famoso problema XOR que una sola neurona no puede resolver y que ayudó a provocar el primer invierno de la IA (01-01); y la solución, apilar neuronas en capas, que es lo que llamamos red neuronal. Terminaremos viendo, sin entrenarla aún, cómo se escribe esa misma red en PyTorch. Es importante porque todo el deep learning es esta idea repetida a escala: entender bien una neurona y por qué hacen falta capas es entender el 80 % de lo que viene después.

Contenido

  1. De la neurona biológica a la neurona artificial
  2. Anatomía de una neurona artificial: entradas, pesos, sesgo, suma y activación
  3. La regresión logística de 04-04 era una neurona
  4. El perceptrón de Rosenblatt y su regla de aprendizaje
  5. El perceptrón en numpy: puertas lógicas y pedidos urgentes de NovaMarket
  6. El problema XOR: el límite de una neurona y la solución con dos capas
  7. Qué es una red neuronal: capas de entrada, ocultas y salida
  8. El aproximador universal y la comparación modelo lineal vs red
  9. Un vistazo al destino: la misma red en PyTorch
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. De la neurona biológica a la neurona artificial

Una neurona biológica recibe señales de otras neuronas a través de las dendritas, las integra en el cuerpo celular y, si la señal acumulada supera un umbral, emite un impulso por el axón hacia las siguientes. Las conexiones (sinapsis) no son todas iguales: unas excitan y otras inhiben, y su fuerza cambia con la experiencia. El cerebro humano tiene unos 86.000 millones de neuronas conectadas de forma masivamente paralela.

En 1943, McCulloch y Pitts propusieron un modelo matemático mínimo de esa neurona: entradas binarias, una suma y un umbral. Es la inspiración de todo lo que sigue, pero conviene ser honesto con la analogía:

Neurona biológica Neurona artificial Comentario
Dendritas reciben señales Vector de entradas x Análogo razonable
Fuerza de la sinapsis Peso w Análogo razonable: es lo que se "aprende"
Umbral de disparo Sesgo b y función de activación Análogo razonable
Impulsos eléctricos en el tiempo, química, plasticidad Un número real calculado de golpe No hay analogía: la neurona artificial es infinitamente más simple
Aprendizaje local, sin supervisor Descenso del gradiente con etiquetas (05-03) El cerebro no hace retropropagación

Así que "red neuronal" es un nombre histórico, no una afirmación de que estemos imitando el cerebro. Lo que de verdad importa es la matemática: una función muy flexible construida apilando piezas muy simples.

  1. Anatomía de una neurona artificial: entradas, pesos, sesgo, suma y activación

Una neurona artificial recibe n entradas numéricas x₁, ..., xₙ y produce una salida en tres pasos:

  1. Suma ponderada: multiplica cada entrada por su peso y suma un sesgo: z = w₁·x₁ + w₂·x₂ + ... + wₙ·xₙ + b. En notación vectorial, z = w · x + b. Los pesos dicen cuánto y en qué sentido influye cada entrada; el sesgo desplaza el punto en que la neurona "se activa" (sin él, la frontera de decisión pasaría obligatoriamente por el origen).
  2. Función de activación: transforma z en la salida a = f(z). Las dos históricas son:
    • Escalón (Heaviside): f(z) = 1 si z > 0, 0 en caso contrario. La neurona "dispara" o no. Es la de McCulloch-Pitts y del perceptrón.
    • Sigmoide: σ(z) = 1 / (1 + e^(−z)). Versión suave del escalón: da un número entre 0 y 1 interpretable como probabilidad, y como no tiene saltos permitirá calcular pendientes (05-03).
  3. Salida: a se usa como predicción o se pasa a otras neuronas.
flowchart LR
    x1["x₁"] -->|w₁| S(("Σ + b"))
    x2["x₂"] -->|w₂| S
    x3["x₃"] -->|w₃| S
    S -->|z| F["f(z)"]
    F --> a["a = salida"]

Ejemplo numérico con tres entradas: x = (2, 0, 1), pesos w = (0,5; −1; 0,8), sesgo b = −1. Entonces z = 0,5·2 + (−1)·0 + 0,8·1 − 1 = 0,8. Con escalón la salida es 1 (porque 0,8 > 0); con sigmoide, σ(0,8) = 1 / (1 + e^(−0,8)) ≈ 0,69.

Todo lo que una neurona sola puede hacer es trazar una frontera lineal: en dos dimensiones, una recta (w₁x₁ + w₂x₂ + b = 0); en tres, un plano; en 21, un hiperplano. A un lado de la frontera dice "1", al otro "0". Guarda esta idea: es la clave del problema XOR.

  1. La regresión logística de 04-04 era una neurona

Relee la definición de la regresión logística en 04-04: "calcula la suma ponderada z = a + b₁·x₁ + ... + bₙ·xₙ y la pasa por la sigmoide". Es exactamente una neurona con activación sigmoide; solo cambia la notación (los coeficientes se llaman ahora pesos, la ordenada se llama sesgo). Comprobémoslo con los coeficientes que obtuvimos entonces para el predictor de devoluciones con cuatro columnas:

import numpy as np

def escalon(z):
    return np.where(z > 0, 1, 0)

def sigmoide(z):
    return 1 / (1 + np.exp(-z))

# Pedido de 04-04: 250 €, 1 artículo, 5 días de entrega, cliente nuevo
x = np.array([250.0, 1, 5, 1])
# Coeficientes que aprendió LogisticRegression en 04-04 (ahora los llamamos pesos)
w = np.array([0.012, -0.2012, 0.3069, 1.8821])
b = -4.906                                    # la ordenada es el sesgo

z = w @ x + b                                 # suma ponderada (producto escalar + sesgo)
print("z =", round(z, 3), " sigmoide =", round(sigmoide(z), 3), " escalón =", escalon(z))

Salida:

z = 1.309  sigmoide = 0.787  escalón = 1

El mismo 0,79 (con la diferencia de redondeo de los coeficientes) que calculamos entonces con predict_proba. Conclusión importante: ya has entrenado una neurona en el módulo 4, y fit de LogisticRegression hizo lo que hará el entrenamiento de una red: buscar los pesos que minimizan una pérdida. La diferencia entre "una neurona" y "una red" no está en la pieza sino en cuántas piezas y cómo se conectan.

  1. El perceptrón de Rosenblatt y su regla de aprendizaje

En 1958 Frank Rosenblatt construyó el perceptrón: una neurona con activación escalón y, sobre todo, la primera regla de aprendizaje que ajustaba los pesos a partir de ejemplos (01-01). La regla es de una sencillez asombrosa. Para cada ejemplo (x, y) del conjunto de entrenamiento:

  1. Calcula la predicción ŷ = escalón(w · x + b).
  2. Calcula el error e = y − ŷ (vale 0 si acierta, +1 si debía decir 1 y dijo 0, −1 al revés).
  3. Actualiza: w ← w + η · e · x y b ← b + η · e, donde η (eta) es la tasa de aprendizaje, un número pequeño como 0,1.

Léelo con calma: si acierta no toca nada; si debía dar 1 y dio 0, empuja los pesos en la dirección de x (para que la próxima vez z sea mayor); si debía dar 0 y dio 1, los empuja en la dirección contraria. Se recorre el conjunto varias veces (cada pasada completa se llama época) hasta que no quede ningún error. Rosenblatt demostró el teorema de convergencia del perceptrón: si los datos son linealmente separables (existe una recta o hiperplano que separa las clases sin error), la regla encuentra uno en un número finito de pasos. Ese "si" es la trampa que veremos en la sección 6.

  1. El perceptrón en numpy: puertas lógicas y pedidos urgentes de NovaMarket

5.1 Implementación

import numpy as np

def entrenar_perceptron(X, y, tasa=0.1, epocas=10):
    """Regla de Rosenblatt. Devuelve pesos, sesgo y errores cometidos en cada época."""
    w = np.zeros(X.shape[1])          # un peso por columna, empezamos en cero
    b = 0.0
    historial = []
    for ep in range(epocas):
        errores = 0
        for xi, yi in zip(X, y):      # ejemplo a ejemplo
            pred = 1 if xi @ w + b > 0 else 0
            error = yi - pred          # 0, +1 o -1
            if error != 0:
                w = w + tasa * error * xi
                b = b + tasa * error
                errores += 1
        historial.append(errores)
        if errores == 0:               # una época sin errores: hemos convergido
            break
    return w, b, historial

Explicación línea a línea: X es una matriz con un ejemplo por fila; w empieza en ceros; en cada época recorremos los ejemplos, calculamos z = xi @ w + b (@ es el producto escalar de numpy) y aplicamos el escalón (> 0); si hay error, corregimos pesos y sesgo con la regla; guardamos cuántos errores hubo en la época y paramos en cuanto una época termina limpia.

5.2 Puertas lógicas AND y OR

Las puertas lógicas son el "hola mundo" histórico de las neuronas: dos entradas binarias y una salida.

X_puertas = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_and = np.array([0, 0, 0, 1])
y_or  = np.array([0, 1, 1, 1])

for nombre, y in [("AND", y_and), ("OR", y_or)]:
    w, b, hist = entrenar_perceptron(X_puertas, y, tasa=0.1, epocas=20)
    pred = escalon(X_puertas @ w + b)
    print(f"{nombre}: w={w.round(2)} b={b:.2f} errores por época={hist} predicción={pred}")

Salida:

AND: w=[0.2 0.1] b=-0.20 errores por época=[1, 3, 3, 2, 1, 0] predicción=[0 0 0 1]
OR: w=[0.1 0.1] b=0.00 errores por época=[1, 2, 1, 0] predicción=[0 1 1 1]

Sigamos a mano la traza del AND para ver la regla en acción (solo se muestran los pasos en que hubo error; η = 0,1):

Época Entrada x y z = w·x + b Predicción Error w tras actualizar b tras actualizar
1 (1, 1) 1 0,0 0 +1 (0,1; 0,1) 0,1
2 (0, 0) 0 0,1 1 −1 (0,1; 0,1) 0,0
2 (0, 1) 0 0,1 1 −1 (0,1; 0,0) −0,1
2 (1, 1) 1 0,0 0 +1 (0,2; 0,1) 0,0
3 (0, 1) 0 0,1 1 −1 (0,2; 0,0) −0,1
3 (1, 0) 0 0,1 1 −1 (0,1; 0,0) −0,2
3 (1, 1) 1 −0,1 0 +1 (0,2; 0,1) −0,1
4 (1, 0) 0 0,1 1 −1 (0,1; 0,1) −0,2
4 (1, 1) 1 0,0 0 +1 (0,2; 0,2) −0,1
5 (0, 1) 0 0,1 1 −1 (0,2; 0,1) −0,2
6 (todas) 0 (0,2; 0,1) −0,2

Con w = (0,2; 0,1) y b = −0,2, la frontera es la recta 0,2·x₁ + 0,1·x₂ − 0,2 = 0: solo el punto (1, 1) queda en el lado positivo (z = 0,1), los otros tres dan z ≤ 0. Fíjate en que la solución no es única (w = (1, 1), b = −1,5 también vale): el perceptrón encuentra una recta separadora, no la mejor.

5.3 Pedidos urgentes de NovaMarket

Un caso menos de juguete. En el almacén de Zaragoza, un pedido se considera urgente si hay que sacarlo en el siguiente turno; depende de las horas que faltan hasta el compromiso de entrega y de la distancia al destino (más lejos, antes hay que salir). Simulamos 200 pedidos con la regla oculta "urgente si horas < 12 + distancia/10", que es lineal, y comprobamos que el perceptrón la descubre:

rng = np.random.default_rng(42)
n = 200
horas = rng.uniform(2, 72, n)          # horas hasta el compromiso de entrega
distancia = rng.uniform(5, 400, n)     # km desde el almacén de Zaragoza
urgente = (horas < 12 + distancia / 10).astype(int)   # regla oculta (lineal)
X = np.column_stack([horas, distancia])
print("Proporción de urgentes:", urgente.mean())

X_esc = (X - X.mean(axis=0)) / X.std(axis=0)   # estandarizar, como en 04-03
w, b, hist = entrenar_perceptron(X_esc, urgente, tasa=0.1, epocas=100)
print("w =", w.round(3), " b =", round(b, 3), " épocas:", len(hist), " errores/época:", hist)
print("Acierto:", (escalon(X_esc @ w + b) == urgente).mean())

# ¿Dónde está la frontera en unidades originales? Despejamos horas para tres distancias
mh, md = X.mean(axis=0); sh, sd = X.std(axis=0)
for d in (50, 200, 350):
    h_lim = mh - sh / w[0] * (w[1] * (d - md) / sd + b)
    print(f"distancia {d} km -> urgente si faltan menos de {h_lim:.1f} h (regla real: {12 + d/10:.1f} h)")

Salida:

Proporción de urgentes: 0.475
w = [-0.862  0.479]  b = -0.2  épocas: 11  errores/época: [16, 9, 9, 3, 8, 6, 6, 5, 6, 4, 0]
Acierto: 1.0
distancia 50 km -> urgente si faltan menos de 17.3 h (regla real: 17.0 h)
distancia 200 km -> urgente si faltan menos de 31.8 h (regla real: 32.0 h)
distancia 350 km -> urgente si faltan menos de 46.3 h (regla real: 47.0 h)

Lectura: en 11 épocas el perceptrón clasifica los 200 pedidos sin error, y la recta que ha encontrado coincide casi exactamente con la regla oculta. El peso de horas es negativo (más horas, menos urgente) y el de distancia positivo, como cabía esperar. Dos observaciones prácticas: (1) el número de errores por época no baja de forma monótona (16, 9, 9, 3, 8...): la regla corrige un ejemplo y desajusta otros; solo garantiza que al final converge; (2) hemos estandarizado las entradas: sin escalar, con tasa=0.01, tras 200 épocas seguía cometiendo unos 16 errores por época, porque un paso de 0,1 en el peso de distancia (que va de 5 a 400) mueve la frontera muchísimo más que el mismo paso en horas. La lección de 04-03 (escalar) es todavía más importante en redes.

  1. El problema XOR: el límite de una neurona y la solución con dos capas

Probemos ahora la puerta XOR (o exclusivo: 1 si las entradas son distintas):

y_xor = np.array([0, 1, 1, 0])
w, b, hist = entrenar_perceptron(X_puertas, y_xor, tasa=0.1, epocas=20)
print("XOR: errores por época =", hist)
print("Predicción:", escalon(X_puertas @ w + b))

Salida:

XOR: errores por época = [2, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
Predicción: [1 1 0 0]

Nunca converge. No es un fallo del código: dibuja los cuatro puntos en el plano; los que valen 1, (0,1) y (1,0), están en esquinas opuestas, y los que valen 0, (0,0) y (1,1), en las otras dos. No existe ninguna recta que deje los dos unos a un lado y los dos ceros al otro. Como una neurona solo sabe trazar rectas, XOR queda fuera de su alcance. Esto es lo que Minsky y Papert demostraron con rigor en Perceptrons (1969), y, combinado con la falta de un método para entrenar varias capas, congeló la investigación en redes durante quince años (01-01).

La solución se conocía en teoría: encadenar neuronas. XOR se puede escribir como "(x₁ OR x₂) AND NOT (x₁ AND x₂)". Cada pieza es lineal, así que dos neuronas en una primera capa (una calcula OR, otra AND) y una tercera que las combina lo resuelven:

x₁ x₂ h₁ = escalón(x₁ + x₂ − 0,5) (OR) h₂ = escalón(x₁ + x₂ − 1,5) (AND) y = escalón(h₁ − h₂ − 0,5) XOR real
0 0 escalón(−0,5) = 0 escalón(−1,5) = 0 escalón(−0,5) = 0 0
0 1 escalón(0,5) = 1 escalón(−0,5) = 0 escalón(0,5) = 1 1
1 0 escalón(0,5) = 1 escalón(−0,5) = 0 escalón(0,5) = 1 1
1 1 escalón(1,5) = 1 escalón(0,5) = 1 escalón(−0,5) = 0 0
def red_xor(x1, x2):
    h1 = escalon(x1 + x2 - 0.5)      # neurona OR   (pesos 1,1; sesgo -0,5)
    h2 = escalon(x1 + x2 - 1.5)      # neurona AND  (pesos 1,1; sesgo -1,5)
    return escalon(h1 - h2 - 0.5)    # neurona de salida: OR y no AND (pesos 1,-1; sesgo -0,5)

for x1 in (0, 1):
    for x2 in (0, 1):
        print(x1, x2, "->", red_xor(x1, x2))

Salida: 0 0 -> 0, 0 1 -> 1, 1 0 -> 1, 1 1 -> 0. La clave conceptual: la primera capa transforma las entradas en un nuevo espacio (h₁, h₂) en el que el problema sí es linealmente separable (los puntos pasan a ser (0,0)→0, (1,0)→1, (1,0)→1, (1,1)→0, y una recta los separa). Eso es lo que hará cada capa oculta de una red: reescribir los datos hasta que la última neurona pueda decidir con una recta. Lo que en 1969 no se sabía era cómo encontrar esos pesos automáticamente en lugar de a mano; la respuesta, la retropropagación, llegó en los años 80 y la veremos en 05-03.

  1. Qué es una red neuronal: capas de entrada, ocultas y salida

Una red neuronal (en su forma básica, perceptrón multicapa o MLP) es un conjunto de neuronas organizadas en capas, donde la salida de cada capa es la entrada de la siguiente:

  • Capa de entrada: no calcula nada; son las características (las 21 columnas del predictor de devoluciones, los 256 píxeles de una foto de 16×16, etc.).
  • Capas ocultas: una o más capas de neuronas, cada una conectada a todas las salidas de la capa anterior (por eso se llaman "densas" o "totalmente conectadas"). Aprenden representaciones intermedias, como h₁ y h₂ en el XOR.
  • Capa de salida: produce la predicción; una neurona con sigmoide para clasificación binaria, tantas como clases para multiclase, una lineal para regresión (los detalles en 05-02).
flowchart LR
    subgraph E["Capa de entrada"]
        x1["x₁"]; x2["x₂"]; x3["x₃"]
    end
    subgraph H["Capa oculta"]
        h1["h₁"]; h2["h₂"]; h3["h₃"]; h4["h₄"]
    end
    subgraph S["Capa de salida"]
        y["ŷ"]
    end
    x1 --> h1 & h2 & h3 & h4
    x2 --> h1 & h2 & h3 & h4
    x3 --> h1 & h2 & h3 & h4
    h1 & h2 & h3 & h4 --> y

Los parámetros de la red son todos los pesos y sesgos: en el diagrama, 3×4 + 4 = 16 en la capa oculta y 4×1 + 1 = 5 en la de salida, 21 en total. Los hiperparámetros (04-01) son las decisiones de diseño: cuántas capas, cuántas neuronas, qué activación. Un detalle esencial: si las capas ocultas no tuvieran función de activación (o fuera lineal), apilar capas no serviría de nada, porque una composición de funciones lineales es otra función lineal y volveríamos a una sola recta. La no linealidad de la activación es lo que da poder a las capas.

  1. El aproximador universal y la comparación modelo lineal vs red

Un resultado teórico de finales de los 80 (Cybenko, Hornik) dice que una red con una sola capa oculta suficientemente ancha y activación no lineal puede aproximar cualquier función continua razonable con la precisión que se quiera. Es el teorema de aproximación universal. Intuición: cada neurona oculta con sigmoide es un "escalón suave" colocado en algún sitio del espacio de entrada; sumando muchos escalones de distintas alturas y posiciones se puede dibujar cualquier curva, igual que con suficientes bloques rectangulares se aproxima cualquier silueta. Dos matices importantes que el teorema no dice: no dice cuántas neuronas hacen falta (puede ser un número enorme) ni cómo encontrar los pesos (eso es el entrenamiento). En la práctica, redes más profundas (varias capas) aproximan lo mismo con muchas menos neuronas que una única capa muy ancha, y de ahí el "deep" de deep learning (05-04).

Aspecto Modelo lineal (regresión logística) Red neuronal (MLP)
Frontera de decisión Un hiperplano Cualquier forma (curvas, regiones desconectadas)
Características Las que le des; las interacciones hay que crearlas a mano (04-03) Las capas ocultas aprenden combinaciones e interacciones
Parámetros Uno por columna + sesgo (22 en el predictor de devoluciones) Cientos, miles o millones
Interpretabilidad Alta: un coeficiente por variable Baja: los pesos ocultos no tienen significado directo
Datos necesarios Pocos Muchos (más parámetros → más riesgo de sobreajuste, 04-06)
Entrenamiento Rápido, óptimo único (pérdida convexa) Más lento, muchos óptimos locales, requiere cuidado (05-03)
Dónde brilla Datos tabulares, pocos datos, necesidad de explicar Texto, imágenes, audio, señales; datos abundantes

Diego, leyendo la tabla, hace la pregunta correcta: "¿entonces para las devoluciones seguimos con la logística?". Probablemente sí, y en 05-03 lo comprobaremos con números; la red se justifica en los problemas donde la logística no puede ni empezar: las reseñas y las fotos.

  1. Un vistazo al destino: la misma red en PyTorch

Para que veas adónde vamos, así se define en PyTorch la red de dos capas que resuelve XOR (dos entradas → dos neuronas ocultas → una salida), con sigmoide en lugar de escalón para que en 05-03 se pueda entrenar:

import torch
import torch.nn as nn

torch.manual_seed(0)
red = nn.Sequential(
    nn.Linear(2, 2),      # capa oculta: 2 entradas -> 2 neuronas (pesos 2x2 + 2 sesgos)
    nn.Sigmoid(),         # activación de la capa oculta
    nn.Linear(2, 1),      # capa de salida: 2 -> 1 (pesos 1x2 + 1 sesgo)
    nn.Sigmoid())         # salida entre 0 y 1
print(red)
print("Parámetros:", sum(p.numel() for p in red.parameters()))

# Sin entrenar, PyTorch ha puesto pesos aleatorios. Copiamos a mano la solución de la sección 6,
# multiplicada por 20 para que la sigmoide se comporte casi como un escalón:
with torch.no_grad():
    red[0].weight[:] = 20 * torch.tensor([[1.0, 1.0], [1.0, 1.0]])   # OR y AND
    red[0].bias[:]   = 20 * torch.tensor([-0.5, -1.5])
    red[2].weight[:] = 20 * torch.tensor([[1.0, -1.0]])              # OR y no AND
    red[2].bias[:]   = 20 * torch.tensor([-0.5])

X_t = torch.tensor(X_puertas, dtype=torch.float32)
print(red(X_t).detach().numpy().round(3).ravel())

Salida:

Sequential(
  (0): Linear(in_features=2, out_features=2, bias=True)
  (1): Sigmoid()
  (2): Linear(in_features=2, out_features=1, bias=True)
  (3): Sigmoid()
)
Parámetros: 9
[0. 1. 1. 0.]

nn.Sequential encadena capas; nn.Linear(2, 2) es una capa densa que calcula z = W·x + b para 2 neuronas (4 pesos + 2 sesgos); nn.Sigmoid() aplica la activación; en total 9 parámetros (4 + 2 + 2 + 1). Con los pesos puestos a mano la red reproduce XOR. Lo que no hemos hecho, y es el objetivo del resto del módulo, es que la red encuentre sola esos pesos: eso exige elegir bien la arquitectura y las activaciones (05-02) y entrenar con descenso del gradiente y retropropagación (05-03).

Errores Comunes y Consejos

  • Creer que "red neuronal" implica imitar el cerebro. Es una función matemática flexible con un nombre histórico. Evita explicárselo así a Diego: genera expectativas equivocadas (recuerda la sobreexpectativa de 01-01).
  • Olvidar el sesgo. Sin b, la frontera pasa por el origen y muchos problemas triviales (como AND) no se pueden resolver. En PyTorch nn.Linear lo incluye por defecto (bias=True).
  • Entrenar el perceptrón sin escalar las entradas. Como hemos visto con distancia (5-400) frente a horas (2-72), la convergencia se resiente o no llega. Estandariza siempre.
  • Esperar que el perceptrón converja con datos no separables. Si hay solapamiento entre clases (lo normal en datos reales, como las devoluciones), la regla oscila para siempre. Por eso se abandonó el escalón en favor de activaciones suaves y una pérdida que se minimiza (05-03).
  • Apilar capas sin activación no lineal. Es un error clásico al empezar con PyTorch: nn.Sequential(nn.Linear(2, 2), nn.Linear(2, 1)) es matemáticamente una sola neurona lineal y no resuelve XOR por muchas capas que añadas.
  • Confundir parámetros con hiperparámetros. Los pesos y sesgos los aprende la red; el número de capas y neuronas lo eliges tú (y se ajusta con validación, 04-06).

Ejercicios

Ejercicio 1. Entrena el perceptrón con entrenar_perceptron sobre la puerta NAND (y = [1, 1, 1, 0]). Después construye XOR sin capa AND, usando la identidad XOR = (x₁ OR x₂) AND (x₁ NAND x₂): escribe una función red_xor_2(x1, x2) con las neuronas OR y NAND en la primera capa y una neurona AND en la salida (pesos y sesgos a mano) y comprueba la tabla de verdad.

Ejercicio 2. Cambia la regla oculta de los pedidos urgentes por una no lineal: urgente = ((horas < 24) != (distancia > 200)).astype(int) (un XOR "continuo": urgente si falta poco tiempo o está lejos, pero no ambas cosas). Entrena el perceptrón 100 épocas sobre las entradas estandarizadas. ¿Converge? ¿Qué acierto máximo alcanza? Explica con la sección 6 por qué.

Ejercicio 3. Una red densa tiene 21 entradas, una capa oculta de 8 neuronas y una salida. (a) Calcula a mano cuántos parámetros tiene. (b) Explica por qué, si la capa oculta no tuviera activación, la red equivaldría a una regresión logística de 22 parámetros. (c) Comprueba (a) definiéndola con nn.Sequential y sum(p.numel() for p in red.parameters()).

Soluciones

Solución 1. El perceptrón aprende NAND en 4 épocas (con tasa=0.1 obtiene w = (−0,2; −0,1), b = 0,2, que da z = 0,2; 0,1; 0,0; −0,1 → 1, 1, 1, 0). La red:

def red_xor_2(x1, x2):
    h1 = escalon(x1 + x2 - 0.5)          # OR
    h2 = escalon(-x1 - x2 + 1.5)         # NAND: 1 salvo cuando ambas son 1
    return escalon(h1 + h2 - 1.5)        # AND de las dos ocultas

Tabla: (0,0) → h = (0, 1) → 0; (0,1) → (1, 1) → 1; (1,0) → (1, 1) → 1; (1,1) → (1, 0) → 0. Cualquier descomposición de XOR en funciones lineales encadenadas vale; lo que no vale es ninguna función lineal sola.

Solución 2. No converge: tras 100 épocas sigue cometiendo entre 75 y 90 errores por época (de 200) y el acierto final oscila alrededor del 40-60 % según en qué época pares, es decir, no mejor que tirar una moneda (los urgentes son el 50,5 %). La razón es la de la sección 6: las cuatro "esquinas" (poco tiempo/cerca → 0, poco tiempo/lejos → 1, mucho tiempo/cerca → 1, mucho tiempo/lejos → 0) forman un XOR y ninguna recta separa las dos regiones de urgentes de las dos de no urgentes. Haría falta una capa oculta que, como en el XOR, transforme primero las entradas (por ejemplo, dos neuronas que detecten "poco tiempo" y "lejos") y una salida que las combine.

Solución 3. (a) Capa oculta: 21 × 8 pesos + 8 sesgos = 176; salida: 8 × 1 + 1 = 9; total 185. (b) Sin activación, la salida sería W₂·(W₁·x + b₁) + b₂ = (W₂·W₁)·x + (W₂·b₁ + b₂), es decir, una única suma ponderada de las 21 entradas con un sesgo: 21 pesos efectivos + 1 sesgo, seguida de la sigmoide final. Los 185 parámetros estarían "desperdiciados" en representar una función que solo tiene 22 grados de libertad. (c) nn.Sequential(nn.Linear(21, 8), nn.Sigmoid(), nn.Linear(8, 1), nn.Sigmoid()) da 185. Esta será, con más neuronas y otra activación, la red que construiremos en 05-02 para el predictor de devoluciones.

Conclusión

En esta lección hemos aprendido que la neurona artificial es una suma ponderada más un sesgo pasada por una función de activación (escalón o sigmoide), que solo sabe trazar fronteras lineales, y que la regresión logística de 04-04 era ya una neurona con sigmoide: la diferencia entre el módulo 4 y este no está en la pieza sino en cuántas se apilan. Hemos implementado el perceptrón de Rosenblatt en numpy con su regla w ← w + η·e·x, lo hemos visto converger en AND, OR y en los pedidos urgentes de NovaMarket (recuperando la regla oculta casi exacta), y fracasar en XOR, el límite que Minsky y Papert señalaron en 1969. La salida es apilar neuronas en capas: la capa oculta transforma las entradas en un espacio donde el problema sí es separable, y por eso una red con activaciones no lineales es un aproximador universal. Hemos cerrado con la misma red escrita en PyTorch con nn.Sequential, con los pesos puestos a mano.

Quedan dos preguntas que este módulo va a responder. Primera: ¿cómo se diseña una red real, con cuántas capas y neuronas, qué activaciones y qué salida según el problema? Es el tema de la siguiente lección, Arquitectura de Redes Neuronales, donde construiremos en PyTorch el MLP del predictor de devoluciones sobre las 21 columnas del módulo 4 y contaremos sus parámetros. Segunda: ¿cómo encuentra la red sus pesos sin que nadie se los escriba a mano? Esa es la de 05-03.

Fundamentos de Inteligencia Artificial (IA)

Módulo 1: Introducción a la Inteligencia Artificial

Módulo 2: Principios Básicos de la IA

Módulo 3: Algoritmos en IA

Módulo 4: Aprendizaje Automático (Machine Learning)

Módulo 5: Redes Neuronales y Deep Learning

Módulo 6: Lógica y Sistemas Expertos

Módulo 7: Herramientas y Lenguajes de Programación en IA

Módulo 8: Proyectos y Casos de Estudio

Módulo 9: Ejercicios y Prácticas

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados