Hasta ahora hemos trabajado con MLP para datos tabulares y con CNN para datos con estructura espacial, como las imágenes.
Pero existe otro tipo de dato muy importante en inteligencia artificial: los datos que tienen orden o que llegan como una secuencia.
Ejemplos típicos son palabras en una oración, valores de una serie temporal, eventos en el tiempo o notas musicales. Para este tipo de situaciones aparecieron las redes recurrentes, o RNN (Recurrent Neural Networks).
En muchos problemas, no basta con conocer qué valores aparecen: también importa en qué orden aparecen.
Por ejemplo, en lenguaje natural no significa lo mismo:
Las palabras son casi las mismas, pero el orden cambia completamente el significado.
Las RNN intentan procesar secuencias manteniendo información de pasos anteriores.
La idea básica es que la salida o el estado del paso actual no depende solo del dato actual, sino también de lo que la red “recuerda” de pasos anteriores.
Esa memoria es la característica distintiva de las redes recurrentes.
Un MLP suele recibir toda la entrada como un bloque fijo. Una RNN, en cambio, puede pensarse como una red que avanza elemento por elemento a través de la secuencia.
En cada paso recibe un nuevo dato y actualiza un estado interno.
Ese estado interno funciona como una especie de resumen de lo que la red ha visto hasta ese momento.
En una RNN aparece una variable muy importante llamada estado oculto o hidden state.
Ese estado se actualiza en cada paso de la secuencia y transporta información desde un instante al siguiente.
Gracias a él, la red puede incorporar contexto y no tratar cada elemento como si estuviera aislado.
Podemos pensar una RNN así:
Esto ocurre para cada posición de la secuencia.
Por eso, en lugar de una sola transformación fija, la red va recorriendo el tiempo o el orden de los datos.
Imagina que queremos leer una secuencia de temperaturas de varios días para decidir si la tendencia general es ascendente o descendente.
Una RNN puede procesar un día a la vez y mantener una representación interna de cómo viene evolucionando la serie.
Esa es precisamente la clase de problema donde el concepto de estado oculto resulta natural.
Un MLP no tiene una memoria explícita del orden temporal. Si queremos usar un MLP con secuencias, tendríamos que transformar la secuencia en un vector fijo y esperar que la red aprenda a interpretarlo.
Una RNN, en cambio, fue pensada directamente para trabajar con secuencias y para mantener un estado entre pasos.
Eso la hace conceptualmente más adecuada para problemas donde el contexto importa.
Las RNN pueden aparecer en distintos tipos de tareas:
La forma exacta de la salida depende del problema específico.
Cuando se explica una RNN, muchas veces se dibuja como si la misma red estuviera repetida varias veces a lo largo del tiempo.
Eso se llama desplegarla en el tiempo o unrolling.
En realidad, no son redes distintas: es la misma celda recurrente aplicada sucesivamente a distintos pasos de la secuencia.
Una característica importante de las RNN es que usan los mismos parámetros en todos los pasos de la secuencia.
Es decir, no se aprende un conjunto de pesos distinto para cada posición temporal.
Esto permite tratar secuencias de longitud variable y mantener una lógica coherente a lo largo del tiempo.
La principal ventaja de una RNN es su capacidad para incorporar contexto secuencial.
En problemas donde el orden importa, esto es una diferencia clave frente a arquitecturas pensadas para entradas fijas sin noción temporal.
Por eso fueron muy importantes en tareas de lenguaje, audio y series temporales.
Las RNN básicas también tienen limitaciones. Una de las más conocidas es que pueden tener dificultades para mantener dependencias de muy largo alcance.
En la práctica, esto significa que a veces les cuesta conservar información importante cuando la secuencia es larga.
Más adelante, esta dificultad llevó al desarrollo de variantes como LSTM y GRU.
El entrenamiento de una RNN se basa en una idea relacionada con backpropagation, pero adaptada a la estructura temporal.
Esta técnica se suele llamar backpropagation through time.
La idea general es que el error se propaga hacia atrás no solo a través de capas, sino también a través de los pasos temporales desplegados.
PyTorch ya implementa capas recurrentes listas para usar, como nn.RNN, nn.LSTM y nn.GRU.
En esta introducción nos concentraremos en la forma más simple, usando nn.RNN.
La idea es aprender primero el mecanismo básico antes de pasar a variantes más avanzadas.
Este punto es muy importante: una RNN no recibe simplemente un vector plano, sino una estructura que representa lotes y secuencias.
Cuando usamos batch_first=True, la entrada suele tener forma:
Esto significa: varios ejemplos, cada uno con varios pasos, y cada paso con ciertas características.
Una RNN en PyTorch suele devolver dos cosas:
En muchos problemas de clasificación de secuencias completas, se usa el último estado oculto como resumen final de toda la secuencia.
Una capa recurrente simple podría declararse así:
Aquí estamos diciendo que cada paso de la secuencia tiene 1 característica y que el estado oculto tendrá tamaño 16.
El parámetro hidden_size indica cuánta capacidad tendrá el estado interno de la RNN.
Un estado oculto más grande puede representar patrones más complejos, aunque también implica más parámetros.
Por eso, igual que en otras arquitecturas, aquí también aparecen decisiones de capacidad y de hiperparámetros.
Si usamos una RNN para clasificar secuencias completas, una estrategia habitual es:
Ese último paso transforma el resumen de la secuencia en una predicción concreta.
Para que la idea sea clara y no depender de datasets externos pesados, en la aplicación final construiremos un problema sintético.
La tarea será clasificar pequeñas secuencias numéricas en dos grupos:
Así podremos ver con claridad cómo la RNN utiliza el orden de los datos.
Si usáramos un ejemplo demasiado complejo desde el principio, se mezclarían demasiadas ideas a la vez.
En cambio, con secuencias simples numéricas es mucho más fácil entender que la red está procesando paso a paso y construyendo una representación temporal.
El objetivo aquí es fijar el concepto de recurrencia, no impresionar con un benchmark complicado.
En este tema también conviene trabajar con dos versiones de la aplicación.
La primera versión contiene solo las partes esenciales de una red recurrente: generación de secuencias, definición del modelo, entrenamiento y predicción.
La segunda versión implementa una interfaz visual más atractiva con Tkinter, donde el usuario puede cargar o escribir una secuencia, visualizar su forma y pedir la predicción del modelo.
import torch
import torch.nn as nn
import torch.optim as optim
# Fijamos semilla para repetir el experimento.
torch.manual_seed(25)
def generar_secuencia_ascendente(longitud):
inicio = torch.rand(1).item()
pasos = torch.linspace(0.0, 1.0, steps=longitud)
secuencia = inicio + pasos + 0.10 * torch.randn(longitud)
return secuencia.unsqueeze(1)
def generar_secuencia_descendente(longitud):
inicio = 1.0 + torch.rand(1).item()
pasos = torch.linspace(0.0, 1.0, steps=longitud)
secuencia = inicio - pasos + 0.10 * torch.randn(longitud)
return secuencia.unsqueeze(1)
def generar_dataset(n_por_clase, longitud):
secuencias = []
etiquetas = []
for _ in range(n_por_clase):
secuencias.append(generar_secuencia_ascendente(longitud))
etiquetas.append(1.0)
secuencias.append(generar_secuencia_descendente(longitud))
etiquetas.append(0.0)
X = torch.stack(secuencias) # forma: [N, longitud, 1]
y = torch.tensor(etiquetas, dtype=torch.float32).unsqueeze(1)
return X, y
# Creamos entrenamiento y validacion.
longitud_secuencia = 12
X_train, y_train = generar_dataset(120, longitud_secuencia)
X_val, y_val = generar_dataset(60, longitud_secuencia)
class ClasificadorRNN(nn.Module):
def __init__(self):
super().__init__()
self.rnn = nn.RNN(input_size=1, hidden_size=16, batch_first=True)
self.fc = nn.Linear(16, 1)
def forward(self, x):
# salida_rnn tiene la salida en cada paso temporal.
# h_n contiene el ultimo estado oculto.
salida_rnn, h_n = self.rnn(x)
# Tomamos el ultimo estado oculto y lo pasamos por una capa final.
ultimo_estado = h_n[-1]
salida = self.fc(ultimo_estado)
return salida
def accuracy_desde_logits(logits, y_real):
probs = torch.sigmoid(logits)
pred = (probs >= 0.5).float()
return (pred == y_real).float().mean().item()
modelo = ClasificadorRNN()
criterio = nn.BCEWithLogitsLoss()
optimizador = optim.Adam(modelo.parameters(), lr=0.01)
for epoca in range(150):
modelo.train()
logits_train = modelo(X_train)
loss_train = criterio(logits_train, y_train)
optimizador.zero_grad()
loss_train.backward()
optimizador.step()
if (epoca + 1) % 25 == 0:
modelo.eval()
with torch.no_grad():
logits_train_eval = modelo(X_train)
logits_val = modelo(X_val)
train_loss = criterio(logits_train_eval, y_train).item()
val_loss = criterio(logits_val, y_val).item()
train_acc = accuracy_desde_logits(logits_train_eval, y_train)
val_acc = accuracy_desde_logits(logits_val, y_val)
print(f"Epoca {epoca+1:3d} | train loss={train_loss:.4f} | val loss={val_loss:.4f} | train acc={train_acc:.3f} | val acc={val_acc:.3f}")
modelo.eval()
with torch.no_grad():
logits_train = modelo(X_train)
logits_val = modelo(X_val)
train_loss = criterio(logits_train, y_train).item()
val_loss = criterio(logits_val, y_val).item()
train_acc = accuracy_desde_logits(logits_train, y_train)
val_acc = accuracy_desde_logits(logits_val, y_val)
print()
print("RESUMEN FINAL")
print(f"train loss={train_loss:.4f} | val loss={val_loss:.4f}")
print(f"train acc={train_acc:.3f} | val acc={val_acc:.3f}")
# Probamos con secuencias nuevas.
ejemplos_nuevos = torch.stack([
generar_secuencia_ascendente(longitud_secuencia),
generar_secuencia_descendente(longitud_secuencia),
generar_secuencia_ascendente(longitud_secuencia)
])
print()
print("SECUENCIAS NUEVAS (paso a paso):")
for i, seq in enumerate(ejemplos_nuevos):
print(f"\nSecuencia {i+1}:")
for t, valor in enumerate(seq):
print(f"t={t:2d} -> {valor.item():.3f}")
with torch.no_grad():
probs = torch.sigmoid(modelo(ejemplos_nuevos))
preds = (probs >= 0.5).float()
print()
print("Probabilidades para secuencias nuevas:")
print(probs)
print("Predicciones finales:")
print(preds)
Esta primera versión es la recomendable para concentrarse en el funcionamiento interno de la RNN sin distracciones de interfaz.
Si además queremos una aplicación más atractiva, podemos usar una interfaz gráfica que muestre el entrenamiento, permita ingresar una secuencia manualmente, generar ejemplos automáticos y visualizar la serie en un gráfico.
import tkinter as tk
from tkinter import ttk, messagebox
import torch
import torch.nn as nn
import torch.optim as optim
import random
# =========================================================
# CONFIGURACIÓN GENERAL
# =========================================================
torch.manual_seed(25)
random.seed(25)
torch.set_printoptions(precision=3, sci_mode=False)
LONGITUD_SECUENCIA = 12
N_ENTRENAMIENTO = 120
N_VALIDACION = 60
TOTAL_EPOCAS = 150
COLOR_FONDO = "#0f172a"
COLOR_PANEL = "#1e293b"
COLOR_PANEL2 = "#334155"
COLOR_TEXTO = "#e2e8f0"
COLOR_SUBTEXTO = "#94a3b8"
COLOR_BOTON = "#2563eb"
COLOR_BOTON2 = "#0ea5e9"
COLOR_OK = "#22c55e"
COLOR_ALERTA = "#f59e0b"
COLOR_GRAFICO = "#38bdf8"
COLOR_PUNTO = "#f97316"
# =========================================================
# GENERACIÓN DE DATOS
# =========================================================
def generar_secuencia_ascendente(longitud):
inicio = torch.rand(1).item()
pasos = torch.linspace(0.0, 1.0, steps=longitud)
secuencia = inicio + pasos + 0.10 * torch.randn(longitud)
return secuencia.unsqueeze(1)
def generar_secuencia_descendente(longitud):
inicio = 1.0 + torch.rand(1).item()
pasos = torch.linspace(0.0, 1.0, steps=longitud)
secuencia = inicio - pasos + 0.10 * torch.randn(longitud)
return secuencia.unsqueeze(1)
def generar_dataset(n_por_clase, longitud):
secuencias = []
etiquetas = []
for _ in range(n_por_clase):
secuencias.append(generar_secuencia_ascendente(longitud))
etiquetas.append(1.0)
secuencias.append(generar_secuencia_descendente(longitud))
etiquetas.append(0.0)
X = torch.stack(secuencias) # [N, longitud, 1]
y = torch.tensor(etiquetas, dtype=torch.float32).unsqueeze(1)
return X, y
# =========================================================
# MODELO RNN
# =========================================================
class ClasificadorRNN(nn.Module):
def __init__(self):
super().__init__()
self.rnn = nn.RNN(input_size=1, hidden_size=16, batch_first=True)
self.fc = nn.Linear(16, 1)
def forward(self, x):
salida_rnn, h_n = self.rnn(x)
ultimo_estado = h_n[-1]
salida = self.fc(ultimo_estado)
return salida
def accuracy_desde_logits(logits, y_real):
probs = torch.sigmoid(logits)
pred = (probs >= 0.5).float()
return (pred == y_real).float().mean().item()
# =========================================================
# APLICACIÓN
# =========================================================
class AplicacionRNN:
def __init__(self, root):
self.root = root
self.root.title("Clasificador de Secuencias con RNN")
self.root.geometry("1180x760")
self.root.configure(bg=COLOR_FONDO)
self.X_train = None
self.y_train = None
self.X_val = None
self.y_val = None
self.modelo = None
self.criterio = None
self.optimizador = None
self.epoca_actual = 0
self.valores = [round(0.5 + i * 0.05, 2) for i in range(LONGITUD_SECUENCIA)]
self.entries = []
self.crear_estilos()
self.crear_interfaz()
self.preparar_entrenamiento()
self.root.after(400, self.entrenar_paso)
# -----------------------------------------------------
# ESTILOS
# -----------------------------------------------------
def crear_estilos(self):
estilo = ttk.Style()
estilo.theme_use("clam")
estilo.configure(
"Horizontal.TProgressbar",
troughcolor="#172033",
background=COLOR_OK,
bordercolor="#172033",
lightcolor=COLOR_OK,
darkcolor=COLOR_OK
)
# -----------------------------------------------------
# INTERFAZ
# -----------------------------------------------------
def crear_interfaz(self):
contenedor = tk.Frame(self.root, bg=COLOR_FONDO)
contenedor.pack(fill="both", expand=True, padx=12, pady=12)
# TÍTULO
titulo = tk.Label(
contenedor,
text="Red Recurrente (RNN) para reconocer secuencias",
bg=COLOR_FONDO,
fg=COLOR_TEXTO,
font=("Arial", 24, "bold")
)
titulo.pack(pady=(0, 6))
subtitulo = tk.Label(
contenedor,
text="La red aprenderá a clasificar si una secuencia es ascendente o descendente",
bg=COLOR_FONDO,
fg=COLOR_SUBTEXTO,
font=("Arial", 12)
)
subtitulo.pack(pady=(0, 12))
# ZONA SUPERIOR
zona_superior = tk.Frame(contenedor, bg=COLOR_FONDO)
zona_superior.pack(fill="x", pady=(0, 12))
# PANEL ENTRENAMIENTO
panel_entrenamiento = tk.Frame(
zona_superior,
bg=COLOR_PANEL,
bd=0,
highlightthickness=1,
highlightbackground="#475569"
)
panel_entrenamiento.pack(side="left", fill="both", expand=True, padx=(0, 6))
tk.Label(
panel_entrenamiento,
text="Entrenamiento de la RNN",
bg=COLOR_PANEL,
fg=COLOR_TEXTO,
font=("Arial", 16, "bold")
).pack(anchor="w", padx=14, pady=(12, 6))
self.label_estado = tk.Label(
panel_entrenamiento,
text="Preparando datos...",
bg=COLOR_PANEL,
fg=COLOR_SUBTEXTO,
font=("Arial", 11)
)
self.label_estado.pack(anchor="w", padx=14, pady=(0, 8))
self.barra = ttk.Progressbar(
panel_entrenamiento,
orient="horizontal",
length=420,
mode="determinate",
maximum=TOTAL_EPOCAS
)
self.barra.pack(fill="x", padx=14, pady=(0, 8))
self.label_porcentaje = tk.Label(
panel_entrenamiento,
text="0%",
bg=COLOR_PANEL,
fg=COLOR_OK,
font=("Arial", 12, "bold")
)
self.label_porcentaje.pack(anchor="w", padx=14, pady=(0, 8))
self.label_metricas = tk.Label(
panel_entrenamiento,
text="Métricas aún no disponibles",
justify="left",
bg=COLOR_PANEL,
fg=COLOR_TEXTO,
font=("Consolas", 12)
)
self.label_metricas.pack(anchor="w", padx=14, pady=(0, 14))
# PANEL RESULTADO
panel_resultado = tk.Frame(
zona_superior,
bg=COLOR_PANEL,
bd=0,
highlightthickness=1,
highlightbackground="#475569"
)
panel_resultado.pack(side="left", fill="both", expand=True, padx=(6, 0))
tk.Label(
panel_resultado,
text="Resultado de predicción",
bg=COLOR_PANEL,
fg=COLOR_TEXTO,
font=("Arial", 16, "bold")
).pack(anchor="w", padx=14, pady=(12, 6))
self.label_clase = tk.Label(
panel_resultado,
text="Esperando fin del entrenamiento...",
bg=COLOR_PANEL,
fg=COLOR_ALERTA,
font=("Arial", 18, "bold")
)
self.label_clase.pack(anchor="w", padx=14, pady=(0, 10))
self.label_probs = tk.Label(
panel_resultado,
text="Ascendente: --\nDescendente: --",
justify="left",
bg=COLOR_PANEL,
fg=COLOR_TEXTO,
font=("Consolas", 14)
)
self.label_probs.pack(anchor="w", padx=14, pady=(0, 10))
self.label_explicacion = tk.Label(
panel_resultado,
text="La RNN observa los valores en orden temporal.\nEl último estado oculto resume la secuencia.",
justify="left",
bg=COLOR_PANEL,
fg=COLOR_SUBTEXTO,
font=("Arial", 11)
)
self.label_explicacion.pack(anchor="w", padx=14, pady=(0, 14))
# ZONA CENTRAL
zona_central = tk.Frame(contenedor, bg=COLOR_FONDO)
zona_central.pack(fill="both", expand=True)
# PANEL IZQUIERDO: entradas
panel_izq = tk.Frame(
zona_central,
bg=COLOR_PANEL,
highlightthickness=1,
highlightbackground="#475569"
)
panel_izq.pack(side="left", fill="both", expand=True, padx=(0, 6))
tk.Label(
panel_izq,
text="Ingresar secuencia",
bg=COLOR_PANEL,
fg=COLOR_TEXTO,
font=("Arial", 16, "bold")
).pack(anchor="w", padx=14, pady=(12, 6))
tk.Label(
panel_izq,
text="Podés escribir los 12 valores o generar ejemplos automáticos.",
bg=COLOR_PANEL,
fg=COLOR_SUBTEXTO,
font=("Arial", 11)
).pack(anchor="w", padx=14, pady=(0, 10))
marco_entries = tk.Frame(panel_izq, bg=COLOR_PANEL)
marco_entries.pack(padx=14, pady=(0, 10))
for i in range(LONGITUD_SECUENCIA):
sub = tk.Frame(marco_entries, bg=COLOR_PANEL)
sub.grid(row=i // 4, column=i % 4, padx=6, pady=6)
tk.Label(
sub,
text=f"t{i}",
bg=COLOR_PANEL,
fg=COLOR_SUBTEXTO,
font=("Arial", 10, "bold")
).pack()
entry = tk.Entry(
sub,
width=8,
justify="center",
font=("Arial", 12),
bg="#e2e8f0",
fg="#0f172a"
)
entry.pack(pady=(4, 0))
entry.insert(0, str(self.valores[i]))
self.entries.append(entry)
marco_botones = tk.Frame(panel_izq, bg=COLOR_PANEL)
marco_botones.pack(padx=14, pady=10, anchor="w")
self.boton_predecir = tk.Button(
marco_botones,
text="Predecir secuencia",
command=self.predecir,
bg=COLOR_BOTON,
fg="white",
activebackground="#1d4ed8",
activeforeground="white",
font=("Arial", 11, "bold"),
relief="flat",
padx=16,
pady=8,
state="disabled",
cursor="hand2"
)
self.boton_predecir.grid(row=0, column=0, padx=5, pady=5)
self.boton_limpiar = tk.Button(
marco_botones,
text="Limpiar",
command=self.limpiar,
bg=COLOR_PANEL2,
fg="white",
activebackground="#475569",
activeforeground="white",
font=("Arial", 11, "bold"),
relief="flat",
padx=16,
pady=8,
state="disabled",
cursor="hand2"
)
self.boton_limpiar.grid(row=0, column=1, padx=5, pady=5)
self.boton_asc = tk.Button(
marco_botones,
text="Generar ascendente",
command=self.cargar_ejemplo_ascendente,
bg=COLOR_BOTON2,
fg="white",
activebackground="#0284c7",
activeforeground="white",
font=("Arial", 11, "bold"),
relief="flat",
padx=16,
pady=8,
state="disabled",
cursor="hand2"
)
self.boton_asc.grid(row=1, column=0, padx=5, pady=5)
self.boton_desc = tk.Button(
marco_botones,
text="Generar descendente",
command=self.cargar_ejemplo_descendente,
bg=COLOR_BOTON2,
fg="white",
activebackground="#0284c7",
activeforeground="white",
font=("Arial", 11, "bold"),
relief="flat",
padx=16,
pady=8,
state="disabled",
cursor="hand2"
)
self.boton_desc.grid(row=1, column=1, padx=5, pady=5)
self.boton_ruido = tk.Button(
marco_botones,
text="Generar aleatoria",
command=self.cargar_ejemplo_aleatorio,
bg="#7c3aed",
fg="white",
activebackground="#6d28d9",
activeforeground="white",
font=("Arial", 11, "bold"),
relief="flat",
padx=16,
pady=8,
state="disabled",
cursor="hand2"
)
self.boton_ruido.grid(row=1, column=2, padx=5, pady=5)
# PANEL DERECHO: gráfico
panel_der = tk.Frame(
zona_central,
bg=COLOR_PANEL,
highlightthickness=1,
highlightbackground="#475569"
)
panel_der.pack(side="left", fill="both", expand=True, padx=(6, 0))
tk.Label(
panel_der,
text="Visualización de la secuencia",
bg=COLOR_PANEL,
fg=COLOR_TEXTO,
font=("Arial", 16, "bold")
).pack(anchor="w", padx=14, pady=(12, 6))
self.canvas = tk.Canvas(
panel_der,
width=500,
height=320,
bg="#0b1220",
highlightthickness=1,
highlightbackground="#334155"
)
self.canvas.pack(padx=14, pady=(0, 12))
self.label_detalle = tk.Label(
panel_der,
text="La línea azul representa la evolución temporal de los datos.",
bg=COLOR_PANEL,
fg=COLOR_SUBTEXTO,
font=("Arial", 11)
)
self.label_detalle.pack(anchor="w", padx=14, pady=(0, 10))
self.dibujar_secuencia_en_canvas(self.obtener_valores_entries())
# -----------------------------------------------------
# ENTRENAMIENTO
# -----------------------------------------------------
def preparar_entrenamiento(self):
self.label_estado.config(text="Generando secuencias de entrenamiento y validación...")
self.X_train, self.y_train = generar_dataset(N_ENTRENAMIENTO, LONGITUD_SECUENCIA)
self.X_val, self.y_val = generar_dataset(N_VALIDACION, LONGITUD_SECUENCIA)
self.modelo = ClasificadorRNN()
self.criterio = nn.BCEWithLogitsLoss()
self.optimizador = optim.Adam(self.modelo.parameters(), lr=0.01)
def entrenar_paso(self):
if self.epoca_actual >= TOTAL_EPOCAS:
self.finalizar_entrenamiento()
return
self.modelo.train()
logits_train = self.modelo(self.X_train)
loss_train = self.criterio(logits_train, self.y_train)
self.optimizador.zero_grad()
loss_train.backward()
self.optimizador.step()
self.epoca_actual += 1
self.barra["value"] = self.epoca_actual
porcentaje = int((self.epoca_actual / TOTAL_EPOCAS) * 100)
self.label_porcentaje.config(text=f"{porcentaje}%")
self.label_estado.config(text=f"Entrenando época {self.epoca_actual} de {TOTAL_EPOCAS}")
if self.epoca_actual % 10 == 0 or self.epoca_actual == 1:
self.modelo.eval()
with torch.no_grad():
logits_train_eval = self.modelo(self.X_train)
logits_val = self.modelo(self.X_val)
train_loss = self.criterio(logits_train_eval, self.y_train).item()
val_loss = self.criterio(logits_val, self.y_val).item()
train_acc = accuracy_desde_logits(logits_train_eval, self.y_train)
val_acc = accuracy_desde_logits(logits_val, self.y_val)
self.label_metricas.config(
text=(
f"Época: {self.epoca_actual:3d}/{TOTAL_EPOCAS}\n"
f"Train loss: {train_loss:.4f}\n"
f"Val loss: {val_loss:.4f}\n"
f"Train acc: {train_acc:.3f}\n"
f"Val acc: {val_acc:.3f}"
)
)
self.root.after(25, self.entrenar_paso)
def finalizar_entrenamiento(self):
self.modelo.eval()
with torch.no_grad():
logits_train = self.modelo(self.X_train)
logits_val = self.modelo(self.X_val)
train_loss = self.criterio(logits_train, self.y_train).item()
val_loss = self.criterio(logits_val, self.y_val).item()
train_acc = accuracy_desde_logits(logits_train, self.y_train)
val_acc = accuracy_desde_logits(logits_val, self.y_val)
self.label_estado.config(text="Entrenamiento finalizado")
self.label_porcentaje.config(text="100%")
self.label_metricas.config(
text=(
f"FINAL\n"
f"Train loss: {train_loss:.4f}\n"
f"Val loss: {val_loss:.4f}\n"
f"Train acc: {train_acc:.3f}\n"
f"Val acc: {val_acc:.3f}"
)
)
self.label_clase.config(text="Modelo listo", fg=COLOR_OK)
self.label_probs.config(text="Ascendente: --\nDescendente: --")
self.boton_predecir.config(state="normal")
self.boton_limpiar.config(state="normal")
self.boton_asc.config(state="normal")
self.boton_desc.config(state="normal")
self.boton_ruido.config(state="normal")
# -----------------------------------------------------
# UTILIDADES DE ENTRADA
# -----------------------------------------------------
def obtener_valores_entries(self):
valores = []
for entry in self.entries:
texto = entry.get().strip().replace(",", ".")
if texto == "":
valores.append(0.0)
else:
valores.append(float(texto))
return valores
def setear_valores_entries(self, valores):
for i, v in enumerate(valores):
self.entries[i].delete(0, tk.END)
self.entries[i].insert(0, f"{v:.3f}")
self.dibujar_secuencia_en_canvas(valores)
def limpiar(self):
ceros = [0.0] * LONGITUD_SECUENCIA
self.setear_valores_entries(ceros)
self.label_clase.config(text="Secuencia limpiada", fg=COLOR_ALERTA)
self.label_probs.config(text="Ascendente: --\nDescendente: --")
def cargar_ejemplo_ascendente(self):
seq = generar_secuencia_ascendente(LONGITUD_SECUENCIA).squeeze().tolist()
self.setear_valores_entries(seq)
def cargar_ejemplo_descendente(self):
seq = generar_secuencia_descendente(LONGITUD_SECUENCIA).squeeze().tolist()
self.setear_valores_entries(seq)
def cargar_ejemplo_aleatorio(self):
seq = [random.uniform(0.0, 2.0) for _ in range(LONGITUD_SECUENCIA)]
self.setear_valores_entries(seq)
# -----------------------------------------------------
# PREDICCIÓN
# -----------------------------------------------------
def predecir(self):
try:
valores = self.obtener_valores_entries()
except ValueError:
messagebox.showerror("Error", "Todos los valores deben ser numéricos.")
return
self.dibujar_secuencia_en_canvas(valores)
entrada = torch.tensor(valores, dtype=torch.float32).view(1, LONGITUD_SECUENCIA, 1)
with torch.no_grad():
logit = self.modelo(entrada)
prob_asc = torch.sigmoid(logit).item()
prob_desc = 1.0 - prob_asc
if prob_asc >= 0.5:
clase = "ASCENDENTE"
color = COLOR_OK
else:
clase = "DESCENDENTE"
color = "#f43f5e"
self.label_clase.config(text=f"Predicción: {clase}", fg=color)
self.label_probs.config(
text=(
f"Ascendente : {prob_asc:.3f}\n"
f"Descendente: {prob_desc:.3f}"
)
)
# -----------------------------------------------------
# DIBUJO EN CANVAS
# -----------------------------------------------------
def dibujar_secuencia_en_canvas(self, valores):
self.canvas.delete("all")
ancho = 500
alto = 320
margen_izq = 45
margen_der = 20
margen_sup = 20
margen_inf = 35
# Ejes
self.canvas.create_line(margen_izq, alto - margen_inf, ancho - margen_der, alto - margen_inf, fill="#64748b", width=2)
self.canvas.create_line(margen_izq, margen_sup, margen_izq, alto - margen_inf, fill="#64748b", width=2)
# Si todos son iguales, evitar división por cero
min_v = min(valores)
max_v = max(valores)
if abs(max_v - min_v) < 1e-6:
max_v = min_v + 1.0
# Líneas guía horizontales
for i in range(5):
y = margen_sup + i * ((alto - margen_sup - margen_inf) / 4)
self.canvas.create_line(margen_izq, y, ancho - margen_der, y, fill="#1e293b", dash=(3, 3))
# Etiquetas Y
for i in range(5):
valor_y = max_v - i * ((max_v - min_v) / 4)
y = margen_sup + i * ((alto - margen_sup - margen_inf) / 4)
self.canvas.create_text(20, y, text=f"{valor_y:.2f}", fill="#94a3b8", font=("Arial", 9))
# Etiquetas X
for i in range(LONGITUD_SECUENCIA):
x = margen_izq + i * ((ancho - margen_izq - margen_der) / (LONGITUD_SECUENCIA - 1))
self.canvas.create_text(x, alto - 15, text=str(i), fill="#94a3b8", font=("Arial", 9))
# Convertir puntos
puntos = []
for i, v in enumerate(valores):
x = margen_izq + i * ((ancho - margen_izq - margen_der) / (LONGITUD_SECUENCIA - 1))
y = margen_sup + (max_v - v) * ((alto - margen_sup - margen_inf) / (max_v - min_v))
puntos.append((x, y))
# Línea
for i in range(len(puntos) - 1):
self.canvas.create_line(
puntos[i][0], puntos[i][1],
puntos[i+1][0], puntos[i+1][1],
fill=COLOR_GRAFICO,
width=3,
smooth=True
)
# Puntos
for i, (x, y) in enumerate(puntos):
self.canvas.create_oval(x-5, y-5, x+5, y+5, fill=COLOR_PUNTO, outline="")
self.canvas.create_text(x, y-14, text=f"{valores[i]:.2f}", fill=COLOR_TEXTO, font=("Arial", 8))
# =========================================================
# PROGRAMA PRINCIPAL
# =========================================================
if __name__ == "__main__":
root = tk.Tk()
app = AplicacionRNN(root)
root.mainloop()
De esta forma podemos estudiar primero una versión compacta para entender la red recurrente y, después, trabajar con una versión visual más atractiva que permite cargar secuencias, ver su gráfico y obtener una predicción interactiva.
Si estás aprendiendo RNN, estas recomendaciones suelen ayudar:
nn.RNN.Las redes recurrentes fueron un paso importante en el desarrollo del Deep Learning para datos secuenciales. Aunque después aparecieron arquitecturas más potentes para ciertas tareas, las RNN siguen siendo una excelente puerta de entrada para entender cómo una red puede incorporar memoria y contexto temporal.
Dominar esta introducción significa empezar a pensar no solo en entradas estáticas, sino también en procesos que evolucionan paso a paso.