April 2, 2026
Recurrent Neural Networks (RNNs) are the backbone of sequence modeling
Adds knowledge to the NN architecture
If the order of the input matters, then we should preserve that information!
We can process a sequence of vectors, \(\mathbf{x}\), by applying a recurrence formula at every time step:
\[\mathbf{h}_t = f_w(\mathbf{h}_{t-1}, \mathbf{x}_t)\]
The hidden state vector contains latent information about the current state of the model
This information should persist throughout the sequence of predictions!
The vanilla update:
\[\underset{(m \times 1)}{\mathbf{h}_t} = \text{tanh}\left(\underset{(m \times m)}{\mathbf{W}_{hh}} \underset{(m \times 1)}{\mathbf{h}_{t-1}} + \underset{(m \times P)}{\mathbf{W}_{xh}} \underset{(P \times 1)}{\mathbf{x}_t} + \underset{(m \times 1)}{\mathbf{b}_h}\right)\]
The RNN is said to have infinite memory
Today, we’re going to look at the most popular RNN framework — Seq2Seq
Any sequence of inputs to any sequence of outputs
Seq2Seq is a generic approach to the sequence-to-sequence prediction problem!
For each output, we look at:
Problem: The input sequence is bottlenecked through the final encoder state
For large language models, Seq2Seq would need to summarize all of the inputs that it saw in the training stage in terms of a single vector of length 512
Attention is a mechanism that allows the decoder to leverage all parts of the original inputs
At different steps of the decoder, let the model “focus” on different parts of the input!
Input: “The agreement on the European Economic Area was signed in August 1992”
Output: “L’accord sur la zone economique europeenne a ete signe en aout 1992.”
Input: “The agreement on the European Economic Area was signed in August 1992”
Output: “L’accord sur la zone economique europeenne a ete signe en aout 1992.”
The attention pattern roughly follows the diagonal — but not exactly. French and English don’t have the same word order. Attention handles reordering naturally.
The decoder doesn’t use the fact that the encoder hidden states form an ordered sequence!
Order is baked into the hidden states via the encoder
How do we compute the attention weights?
\[e_{ti} = f(\mathbf{s}_{t-1}, \mathbf{h}_i)\]
\[a_{ti} = \text{softmax}(e_{ti})\]
A quick note:
Without loss, we can assume that the hidden state vectors of the encoder \(\mathbf{h}_i\) and the decoder \(\mathbf{s}_t\) are normalized to have norm 1
Additionally, lets just assume that the length of each \(\mathbf{h}_i\) and \(\mathbf{s}_t\) are the same!
Important Review:
An inner product is equivalent to computing the cosine similarity between the two vectors:
\[ \mathbf x^T \mathbf y = \|\mathbf x\| \|\mathbf y\| \cos(\theta) \]
where \(\theta\) is the angle between the two vectors.
When the angle between vectors is 0 (i.e., they point in the same direction), the cosine similarity is 1. When the angle is 90 degrees (i.e., they are orthogonal), the cosine similarity is 0.
Give more weight to hidden vector pairs that have the same direction/highest similarity!!!
\[\mathbf{h}_i \in \mathbb{R}^m \quad ; \quad \mathbf{s}_{t-1} \in \mathbb{R}^m\]
Learnable Scaled Dot Product:
\[e_{ti} = \frac{\mathbf{h}_i^T \mathbf{W}_g \mathbf{s}_{t-1}}{\sqrt{m}}\]
Why scale by \(\sqrt{m}\)?
In \(m = 256\) dimensions, random unit vectors have dot products with standard deviation \(\sqrt{256} = 16\).
Dividing by \(\sqrt{m}\) normalizes the variance back to 1, keeping softmax in its useful regime.
Putting this all together, we get a model for each output of the decoder!
\[\mathbf{y}_t = f(\mathbf{s}_t) \quad ; \quad \mathbf{s}_t = f(\mathbf{y}_{t-1}, \mathbf{c}_t)\]
\[\mathbf{c}_t = \sum_{i=1}^{T_e} a_{ti} \mathbf{h}_i \quad ; \quad a_{ti} = \text{softmax}(e_{ti})\]
\[e_{ti} = \frac{\mathbf{h}_i^T \mathbf{W}_g \mathbf{s}_{t-1}}{\sqrt{m}}\]
where \(\mathbf{h}_i\) is learned via a RNN setup!
This basis admits a more generic approach that will be really important
We’re going to switch to this language
Attention Layers will require us to change our terminology a little
Query: vector from which the attention is looking. “What am I looking for?” In the seq2seq model, this is the decoder hidden state \(\mathbf{s}_{t-1}\)
Key: vector at which the query looks to compute weights. “What do I represent?” In seq2seq, these are the encoder hidden states \(\mathbf{h}_i\)
Value: the vector to be weighted by the similarity between the query and the key. “What information do I carry?” Also the encoder hidden states.
Analogy: a soft dictionary lookup.
The query is what you’d type into a search engine.
The keys are the index of stored documents.
The values are the document contents.
Instead of returning the single best match, return a weighted average of all documents based on relevance.
In our Seq2Seq attention, we computed dot products between decoder and encoder hidden states.
\[e_{ti} = \frac{\mathbf{s}_{t-1}^T \mathbf{h}_i}{\sqrt{m}}\]
This only measures similarity in the original hidden state space. But the hidden states were learned for reconstruction, not for computing relevance.
What if “similarity for the purpose of attention” is different from “similarity in hidden state space”?
Example: when generating the Southern word “cyat,” the decoder state encodes “I need to produce a noun, animal-related.”
The encoder state at “cat” encodes “common English noun, subject of the sentence.” These are related, but their raw hidden states might not have high dot product because they encode different types of information.
We need to project both into a shared “relevance space” where the right things are similar.
Learn three separate projections:
\[\mathbf{Q} = \mathbf{S}\mathbf{W}_Q \quad ; \quad \mathbf{K} = \mathbf{H}\mathbf{W}_K \quad ; \quad \mathbf{V} = \mathbf{H}\mathbf{W}_V\]
\(\mathbf{W}_Q\) \((m \times d_k)\): projects the decoder states into a query space. Transforms “what I need” into a form optimized for matching.
\(\mathbf{W}_K\) \((D \times d_k)\): projects the encoder states into a key space. Transforms “what I represent” into the same matching space as the queries.
\(\mathbf{W}_V\) \((D \times d_v)\): projects the encoder states into a value space. Transforms “what information I carry” into the form most useful for the decoder’s output.
\(d_k\) (the key/query dimension) controls the matching capacity — how rich the similarity computation is.
\(d_v\) (the value dimension) controls the information capacity — how much content each position can carry.
Key insight: what makes two things similar is not the same as what information you want to extract.
Consider translating “The big red dog ran quickly.”
When generating “derg” (Southern for dog), the decoder queries for “animal noun.”
The key for “dog” should encode: “I’m a noun, I’m an animal” — features useful for matching
The value for “dog” should encode: “I’m canine, I’m the subject, I’m modified by big and red” — features useful for generating the translation
If K and V were the same, the model would have to use one representation for both matching AND information retrieval. Separating them lets each do its job.
\[\mathbf{Q}: (N_Q \times d_k) \quad \mathbf{K}: (N_K \times d_k) \quad \mathbf{V}: (N_K \times d_v)\]
\(N_Q\) = number of queries (decoder positions — how many outputs we’re generating)
\(N_K\) = number of keys/values (encoder positions — how many inputs we’re attending to)
\(d_k\) = dimension of the matching space (typically 64)
\(d_v\) = dimension of the information space (typically 64)
Note: \(N_Q\) and \(N_K\) can be different lengths. This is what makes attention so flexible — a 10-word Southern sentence can attend to a 15-word English sentence with no architectural changes.
\[\text{Attention}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) = \text{softmax}\left(\frac{\mathbf{Q}\mathbf{K}^T}{\sqrt{d_k}}\right)\mathbf{V}\]
Step by step:
Three matrix multiplications and a softmax. That’s it.
IMPORTANT Computing recurrence relations is incredibly slow
The attention setup can do everything in parallel
This is really going to matter in a second.
Everything we just derived — the Q/K/V projections, the scaled dot product, the softmax, the weighted sum — is implemented in a single PyTorch layer:
import torch.nn as nn
attn_layer = nn.MultiheadAttention(
embed_dim=256, # dimension of the input embeddings
num_heads=8, # number of attention heads (more next lecture)
dropout=0.1, # dropout on attention weights
)This handles everything: the \(\mathbf{W}_Q\), \(\mathbf{W}_K\), \(\mathbf{W}_V\) projections, the scaling by \(\sqrt{d_k}\), the softmax, the weighted combination, and an output projection \(\mathbf{W}_O\).
# Cross-attention: decoder queries encoder
# query: (seq_len_decoder, batch, embed_dim)
# key: (seq_len_encoder, batch, embed_dim)
# value: (seq_len_encoder, batch, embed_dim)
output, attn_weights = attn_layer(
query=decoder_states,
key=encoder_states,
value=encoder_states,
)
# output: (seq_len_decoder, batch, embed_dim)
# attn_weights: (batch, seq_len_decoder, seq_len_encoder)Under the hood, nn.MultiheadAttention does the following:
We could have just said nn.MultiheadAttention and moved on. So why the math?
Because the formula tells you what the model can and can’t do.
Understanding the mechanism lets you diagnose failures, design new architectures, and know when attention is the right tool.
You’ll use nn.MultiheadAttention in practice. But you’ll understand why it works.
Given the attention setup, do we need our encoder to be recurrent?
Yes: context of the input is sequential — different words make sense in the context of other input words. “Little” in “Bless Your Little Heart” is pejorative, not a descriptor!
No: the decoder doesn’t really care about the encoder’s internal ordering.
Recurrence is only needed to build context within the encoder hidden states!
Suppose that we have an input sentence:
I arrived at the bank after crossing the ____
Two completely viable completions:
There is no hope for a sequential model to learn what should come next!
What we need is the ability to look ahead when we encode the input — and then correct the prediction afterwards.
Allow the encoder to develop context of the input by looking at all other words in the input
“The animal didn’t cross the street because it was too wide.”
What does “it” refer to? Self-attention lets position 8 (“it”) directly attend to position 6 (“street”).
For each word in the input, compute query-key-value sets (linear transformations of the input embeddings).
For each word \(i\):
The output for each position is a weighted combination of all other positions — context-aware, computed in parallel.
The Q, K, V all come from the same sequence:
\[\mathbf{Q} = \mathbf{X}\mathbf{W}_Q \quad ; \quad \mathbf{K} = \mathbf{X}\mathbf{W}_K \quad ; \quad \mathbf{V} = \mathbf{X}\mathbf{W}_V\]
Each word gets a contextualized representation that incorporates information from every other word — no sequential processing needed.
“Bank” in “river bank” gets a different representation than “bank” in “bank account” because self-attention pulls in different context.
This replaces the RNN encoder entirely. No recurrence. Fully parallel.
Does this work?
Kinda…
Easyish fix - add learned positional embeddings to the input.
Not that hard to implement at all in Pytorch!!!!
Since our inputs are always of fixed length (through padding), we can use learned positional embeddings to give the model information about the position of each word in the sequence.
We’ve completely eliminated the wasteful recurrence calculations and replaced them with a set of fully parallelizable computations
More compute resources = better models = faster training!
This is almost a transformer!
Really. The transformer isn’t as complicated as it seems to be at first.
Amazing how powerful the architecture is given its relative simplicity!
But we’re missing a few pieces:
Masked Self-Attention — The decoder is different from the encoder since we need to train it to be able to predict the next token! We want to eliminate recurrence from the decoder, but self-attention + positional embeddings would allow the decoder to cheat in training and do a poor job in inference.
Multi-head attention — one attention head computes one type of relevance. “It” needs to attend to “street” for semantics AND to “was” for grammar. Multiple heads let the model attend for multiple reasons simultaneously.
But we’re missing a few pieces:
Feed-forward layers, residual connections, LayerNorm — the transformer block wraps attention with processing and gradient highways.
Next lecture: these final pieces, the full transformer, and what happens when you use only one side.