English · Español
Lab 02 — Pase forward de la RNN a mano¶
Objetivo: mirar una recurrencia el tiempo suficiente para sentirla. Pase forward en NumPy sobre
I work, you work, he ___— sin entrenamiento.Tiempo estimado: 60–90 minutos.
Prerrequisito: lab 00 (corpus tokenizado), lab 01 (línea base de n-gramas) comprometidos.
Lo que produces¶
Un directorio experiments/14-conjugation-completion/ que contiene:
rnn_forward.py— implementación del pase forward de una RNN vainilla (según el blueprint desrc/minimodel/sequence_baselines/rnn.py).gru_forward.py— pase forward de la GRU.walkthrough.py— ejecuta ambos sobre el ejemplo canónico, imprime cada estado oculto.walkthrough.txt— la salida impresa, comprometida.hidden_state_evolution.png— visualización de \(\|h_t\|\) a lo largo del tiempo, opcionalmente un heatmap de los valores de \(h_t\).manifest.json.README.md(2–3 párrafos).
El ejemplo¶
El ejemplo canónico de la Fase 14 es:
Vas a ejecutar una RNN vainilla sin entrenar y una GRU sin entrenar sobre esta secuencia. Sin entrenamiento. El propósito es ver la recurrencia en funcionamiento, no obtener la respuesta correcta.
Tras el pase forward, calculas los logits en la posición final (he) y lees los 5 tokens predichos top. Para un modelo de inicialización aleatoria, esto debería parecer aleatorio — no informativo. Es lo esperado. El lab va de mecanismo, no de precisión.
TODOs¶
Bloque A — implementar el forward de la RNN vainilla¶
Según el blueprint de src/minimodel/sequence_baselines/rnn.py:
class VanillaRNN:
def __init__(self, vocab_size, d_embed, d_hidden, seed=42):
# Inicializa W_xh, W_hh, W_ho, b_h, b_o con valores pequeños aleatorios.
# Además: matriz de embedding E[vocab_size, d_embed].
...
def forward(self, token_ids: list[int]) -> tuple[np.ndarray, list[np.ndarray]]:
# Devuelve (final_logits, all_hidden_states).
# all_hidden_states[t] es h_t tras procesar token_ids[t].
...
- Inicialización aleatoria: \(W_{hh} \sim \mathcal{N}(0, 0.1)\), \(W_{xh} \sim \mathcal{N}(0, 0.1)\), \(W_{ho} \sim \mathcal{N}(0, 0.1)\). Sesgos a cero.
- Inicialización del embedding: misma escala.
- Pase forward: para cada token, calcula \(h_t = \tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h)\). Registra cada \(h_t\).
- En el paso final, calcula \(\hat y = W_{ho} h_T + b_o\).
- Imprime los 5 tokens top por valor de logit.
Usa \(d_\text{embed} = 16\), \(d_\text{hidden} = 32\). Fíjalos para que varias ejecuciones sean comparables.
Bloque B — implementar el forward de la GRU¶
Según el blueprint, escribe class GRU con la misma API. El pase forward usa la recurrencia de la GRU:
Notas de implementación: - \(W_z, W_r, W \in \mathbb{R}^{d_h \times (d_h + d_\text{embed})}\). - \(\sigma(x) = 1 / (1 + e^{-x})\); por estabilidad, usa \(\sigma(x) = e^x / (1 + e^x)\) cuando \(x < 0\). - Inicializa los sesgos \(b_z, b_r\) a cero (evita los trucos de forget-bias aquí; eso es asunto de la Fase 18).
Bloque C — el walkthrough¶
walkthrough.py ejecuta ambos modelos sobre el ejemplo canónico e imprime:
=== Vanilla RNN forward on "I work , you work , he" ===
seed: 42
config: d_embed=16, d_hidden=32
t=0 token='<bos>' x_t=[0.05, -0.12, ...] h_t=[0.00, 0.00, ...] ||h_t||=0.00
t=1 token='I' x_t=[0.18, -0.05, ...] h_t=[0.04, 0.07, ...] ||h_t||=0.39
t=2 token='work' x_t=[0.07, 0.21, ...] h_t=[0.06, -0.02, ...] ||h_t||=0.44
...
t=8 token='he' x_t=[...] h_t=[...] ||h_t||=0.52
final_logits top-5:
rank=1 token='trabajaron' logit=0.31
rank=2 token='/' logit=0.27
...
=== GRU forward on same sequence ===
...
Compromete la salida como walkthrough.txt.
Bloque D — visualizar la evolución del estado¶
hidden_state_evolution.png: una gráfica con el eje x como paso temporal (time step) \(t\), y o bien:
- (A) una sola curva de \(\|h_t\|_2\) a lo largo del tiempo (más sencillo), o
- (B) un heatmap de los valores de \(h_t\) con el tiempo en el eje x y el índice de la dimensión oculta en el eje y (más rico).
Cualquiera de las dos es aceptable. Dibuja la RNN vainilla y la GRU lado a lado.
Bloque E — interpretar¶
En README.md, responde:
- ¿Es semánticamente razonable la predicción top-1 en
he? Para un modelo de inicialización aleatoria: no, es aleatoria. Confírmalo. - ¿Cómo evoluciona \(\|h_t\|\)? ¿Crece, decrece o se estabiliza? Para un \(W_{hh}\) inicializado aleatoriamente con \(\sigma = 0.1\), esperarías que \(\|h_t\|\) se aplane en torno a un valor fijo porque tanh satura las contribuciones. Confírmalo empíricamente.
- ¿Se ve diferente el \(h_t\) de la GRU del de la RNN? Mira los heatmaps a ojo. Las diferencias deberían ser visibles — la apertura de puertas (gating) de la GRU produce una evolución del estado menos ruidosa.
- ¿Qué información sospechas que hay en \(h_8\)? Está sin entrenar, así que probablemente nada útil. Pero conceptualmente: tras ver
I work, you work, he, un \(h_8\) ideal debería codificar "el sujeto es 3ª singular, el tiempo es present-simple, espera concordancia verbal con-s". Anota esta lectura aspiracional.
Restricciones¶
- Sin entrenamiento. Inicialización aleatoria, solo forward. El propósito es mecanismo, no precisión.
- Sin PyTorch. Solo NumPy y biblioteca estándar.
- Misma semilla en ambos modelos. Si no, compararlos no tiene sentido.
- Fija \(d_\text{embed} = 16, d_\text{hidden} = 32\). Otras elecciones están bien para exploración personal, pero el walkthrough comprometido usa estas.
Condiciones de parada¶
Hecho cuando:
walkthrough.txtexiste e imprime cada \(h_t\) para ambos modelos.hidden_state_evolution.pngmuestra la norma del estado o el heatmap a lo largo del tiempo.README.mdresponde a las cuatro preguntas de interpretación.- Puedes leer tu propio
walkthrough.txty explicar qué hizo el modelo en cada paso — aunque las predicciones sean aleatorias.
Trampas¶
- Todos los \(h_t\) son cero. Probablemente \(b_h = 0\), \(h_0 = 0\), y \(W_{hh}\) es lo bastante pequeño como para que \(\tanh(0 + \text{pequeño}) \approx 0\). O subes la escala de inicialización, o revisa tus operaciones de numpy.
- Todos los \(h_t\) están saturados (cerca de \(\pm 1\)). Escala de inicialización demasiado grande. Redúcela a \(\sigma = 0.05\) y vuelve a ejecutar.
- La GRU colapsa a una RNN vainilla. Esto ocurre cuando \(z_t \approx 1\) en todas partes (entonces \(h_t \approx \tilde h_t\), que es la recurrencia vainilla). Comprueba los valores de \(z_t\) — deberían estar en \([0.3, 0.7]\) inicialmente con inicialización aleatoria.
sigmoid(grande)devuelve inf. Usa la forma numéricamente estable del Bloque B.
Cuándo consultar solutions/¶
Tras comprometer los archivos. Solución en solutions/02-rnn-by-hand-ref.md (escrita al abrir la fase) compara tus estados impresos y discute lo que habría hecho el modelo entrenado.
Siguiente lab: lab/03-vanishing-empirical.md.