3.5.6Sequence Models

Bidirectional RNNs

3,424 words16 min readdifficulty · medium3 backlinks

What Problem Does This Solve?

In vanilla RNNs, the hidden state hth_t at time tt only encodes information from inputs x1,x2,,xtx_1, x_2, \ldots, x_t. This is perfect for causal tasks (predicting the next word) but terrible for tasks where the answer depends on the full context:

  • Named Entity Recognition: "Paris" in "I live in Paris" (city) vs "Paris Hilton visited" (person) – you need words after "Paris"
  • Machine Translation: Word order differs across languages; translating word 3 might require knowing word 7
  • Speech Recognition: A phoneme's identity often depends on following phonemes
  • Sentiment Analysis: "This movie is not bad" – "not" changes everything, but appears before "bad"

Standard RNNs are causally constrained – they can't "peek ahead." Bidirectional RNNs lift this constraint when the full sequence is available at inference time.

The Architecture: Two RNNs, One Forward, One Backward

Figure — Bidirectional RNNs

Deriving the Forward Pass from First Principles

Let's build this step-by-step. Start with a standard RNN cell equation:

ht=tanh(Whht1+Wxhxt+bh)h_t = \tanh(W_{h} h_{t-1} + W_{xh} x_t + b_h)

Why this form? The hidden state is a nonlinear combination of (1) the previous hidden state ht1h_{t-1} (memory) and (2) the current input xtx_t (new information). The tanh squashes to [-1, 1] to prevent exploding values.

Now, for a bidirectional architecture:

Forward RNN (processes left-to-right): ht=tanh(Whht1+Wxxt+bh)\overrightarrow{h}_t = \tanh(W_{\overrightarrow{h}} \overrightarrow{h}_{t-1} + W_{\overrightarrow{x}} x_t + b_{\overrightarrow{h}})

  • Starts with h0=0\overrightarrow{h}_0 = \mathbf{0} (or learned initialization)
  • At t=1t=1: h1\overrightarrow{h}_1 depends only on x1x_1
  • At t=2t=2: h2\overrightarrow{h}_2 depends on x1,x2x_1, x_2 (through h1\overrightarrow{h}_1)
  • At t=Tt=T: hT\overrightarrow{h}_T encodes the entire past sequence x1,,xTx_1, \ldots, x_T

Backward RNN (processes right-to-left): ht=tanh(Whht+1+Wxxt+bh)\overleftarrow{h}_t = \tanh(W_{\overleftarrow{h}} \overleftarrow{h}_{t+1} + W_{\overleftarrow{x}} x_t + b_{\overleftarrow{h}})

  • Starts with hT+1=0\overleftarrow{h}_{T+1} = \mathbf{0}
  • At t=Tt=T: hT\overleftarrow{h}_T depends only on xTx_T
  • At t=T1t=T-1: hT1\overleftarrow{h}_{T-1} depends on xT,xT1x_T, x_{T-1} (through hT\overleftarrow{h}_T)
  • At t=1t=1: h1\overleftarrow{h}_1 encodes the entire future sequence x2,,xTx_2, \ldots, x_T

Why separate weight matrices? The forward and backward RNNs are learning different patterns (past vs. future dependencies), so they need independent parameters. Sharing weights would force both directions to learn a compromise representation.

Combining the outputs at each timestep:

The final output layer (for a task like NER or POS tagging): yt=softmax(Wyht+by)=softmax(Wy[ht;ht]+by)y_t = \text{softmax}(W_y h_t + b_y) = \text{softmax}(W_y [\overrightarrow{h}_t; \overleftarrow{h}_t] + b_y)

Why softmax here? We're making a classification decision (e.g., "is this word a PERSON or LOCATION?") at each timestep, so we need a probability distribution over classes.

The Full Forward Pass Algorithm

Given input sequence x=(x1,x2,,xT)\mathbf{x} = (x_1, x_2, \ldots, x_T):

  1. Initialize: h0=0\overrightarrow{h}_0 = \mathbf{0}, hT+1=0\overleftarrow{h}_{T+1} = \mathbf{0}

  2. Forward pass (left-to-right):

    for t = 1 to T:
        h⃗ₜ = tanh(W_h⃗ h⃗ₜ₋₁ + W_x⃗ xₜ + b_h⃗)
    
  3. Backward pass (right-to-left):

    for t = T down to 1:
        h⃖ₜ = tanh(W_h⃖ h⃖ₜ₊₁ + W_x⃖ xₜ + b_h⃖)
    
  4. Combine:

    for t = 1 to T:
        hₜ = [h⃗ₜ; h⃖ₜ]
        yₜ = softmax(Wᵧ hₜ + bᵧ)
    

Why two separate loops? The forward RNN must complete before we can compute backward RNN outputs because ht\overleftarrow{h}_t depends on ht+1\overleftarrow{h}_{t+1}, which requires processing from right-to-left. In practice, frameworks vectorize this efficiently.

Worked Example: Named Entity Recognition

When to Use Bidirectional RNNs (and When Not To)

Use BiRNs when:

  • The entire sequence is available at inference time (offline processing)
  • The task is non-causal: the label/output at position tt can depend on inputs at positions >t> t
  • Examples: NER, POS tagging, machine translation (encoder side), speech recognition (offline), sentiment analysis, question answering (encoding the question or passage)

Do NOT use BiRNNs when:

  • The task is causal/autoregressive: predicting the next token (language modeling, text generation, online speech recognition)
  • Why? At time tt, you don't have access to xt+1,xt+2,x_{t+1}, x_{t+2}, \ldots yet. Using a BiRNN here would be "cheating" – your model can't deploy in a real-time setting.
  • Real-world constraint: If you need to make predictions as the sequence arrives (e.g., live captioning, chatbots), you can't wait for the full sequence to run the backward pass.

Computational Considerations

Time complexity:

  • Unidirectional RNN: O(Tdh2)O(T \cdot d_h^2) for a sequence of length TT with hidden dimension dhd_h
  • Bidirectional RNN: O(2Tdh2)=O(Tdh2)O(2 \cdot T \cdot d_h^2) = O(T \cdot d_h^2) (same order, but2× constants)

Why? You're running two RNNs, each with TT timesteps and O(dh2)O(d_h^2) work per step (matrix multiply Whhht1W_{hh} h_{t-1}).

Space complexity:

  • Must store both h1,,hT\overrightarrow{h}_1, \ldots, \overrightarrow{h}_T and h1,,hT\overleftarrow{h}_1, \ldots, \overleftarrow{h}_T for backpropagation
  • Total: O(2Tdh)O(2 \cdot T \cdot d_h) memory

Paralelization:

  • Forward and backward passes are not paralelizable within themselves (sequential dependency: hth_t depends on ht1h_{t-1})
  • But forward and backward RNNs are independent, so you can compute them in parallel if you have multiple GPUs or batch-process multiple sequences
  • Contrast with Transformers: Transformers process all timesteps in parallel (self-attention), making them much faster on modern hardware

Backpropagation Through Time (BPTT) for BiRNNs

During training, we need gradients with respect to all parameters: Wh,Wx,Wh,Wx,WyW_{\overrightarrow{h}}, W_{\overrightarrow{x}}, W_{\overleftarrow{h}}, W_{\overleftarrow{x}}, W_y.

Loss function (e.g., for sequence tagging): L=t=1TCrossEntropy(yt,y^t)\mathcal{L} = \sum_{t=1}^T \text{CrossEntropy}(y_t, \hat{y}_t)

Gradient flow:

  1. Compute Lht\frac{\partial \mathcal{L}}{\partial h_t} (gradient w.r.t. combined hidden state)
  2. Since ht=[ht;ht]h_t = [\overrightarrow{h}_t; \overleftarrow{h}_t], split into Lht\frac{\partial \mathcal{L}}{\partial \overrightarrow{h}_t} and Lht\frac{\partial \mathcal{L}}{\partial \overleftarrow{h}_t}
  3. Forward RNN gradients flow backward in time (right-to-left through the forward chain): Lht1=Lhththt1=LhtWhTtanh()\frac{\partial \mathcal{L}}{\partial \overrightarrow{h}_{t-1}} = \frac{\partial \mathcal{L}}{\partial \overrightarrow{h}_t} \cdot \frac{\partial \overrightarrow{h}_t}{\partial \overrightarrow{h}_{t-1}} = \frac{\partial \mathcal{L}}{\partial \overrightarrow{h}_t} \cdot W_{\overrightarrow{h}}^T \cdot \text{tanh}'(\cdots)
  4. Backward RNN gradients flow forward in time (left-to-right through the backward chain): Lht+1=Lhththt+1=LhtWhTtanh()\frac{\partial \mathcal{L}}{\partial \overleftarrow{h}_{t+1}} = \frac{\partial \mathcal{L}}{\partial \overleftarrow{h}_t} \cdot \frac{\partial \overleftarrow{h}_t}{\partial \overleftarrow{h}_{t+1}} = \frac{\partial \mathcal{L}}{\partial \overleftarrow{h}_t} \cdot W_{\overleftarrow{h}}^T \cdot \text{tanh}'(\cdots)

Why this is tricky: You have two vanishing/exploding gradient problems (one per RNN). The same solutions apply: use LSTMs/GRUs, gradient clipping, careful initialization.

Bidirectional LSTMs (BiLSTMs)

In practice, vanilla BiRNNs are rarely used because of vanishing gradients. Instead, we use Bidirectional LSTMs (BiLSTMs) or Bidirectional GRUs (BiGRUs).

The architecture is identical, but each RNN cell is replaced with an LSTM/GRU cell:

ht,ct=LSTMforward(xt,ht1,ct1)\overrightarrow{h}_t, \overrightarrow{c}_t = \text{LSTM}_{\text{forward}}(x_t, \overrightarrow{h}_{t-1}, \overrightarrow{c}_{t-1}) ht,ct=LSTMbackward(xt,ht+1,ct+1)\overleftarrow{h}_t, \overleftarrow{c}_t = \text{LSTM}_{\text{backward}}(x_t, \overleftarrow{h}_{t+1}, \overleftarrow{c}_{t+1}) ht=[ht;ht]h_t = [\overrightarrow{h}_t; \overleftarrow{h}_t]

Why LSTMs? The cell state ctc_t provides a "highway" for gradients, mitigating vanishing gradients over long sequences. This is crucial for NLP tasks where dependencies can span20+ words.

Example task: Sentiment analysis on "The movie was not particularly bad, but the acting was terrible."

  • Forward LSTM at "not": encodes "The movie was not"
  • Backward LSTM at "not": encodes "not particularly bad, but the acting was terrible"
  • Combined: the model learns that "not...bad" is a negation, but "acting was terrible" dominates the sentiment

Recall Explain to a 12-Year-Old

Imagine you're playing a game where you have to guess what each word in a sentence does (is it a person's name? a place? an action?).

If you only read the sentence left-to-right, sometimes you get confused. Like if you see "Apple," you don't know if it's the fruit or the company until you read more. But if you're only looking left, you can't see what comes next!

A Bidirectional RNN is like having two robots:

  1. Robot A reads the sentence left-to-right (normal reading). It remembers everything it's seen so far.
  2. Robot B reads the sentence right-to-left (backwards reading). It remembers everything coming up.

At each word, you ask both robots what they think, then combine their answers. Robot A says "I've seen 'Apple' and nothing before it," and Robot B says "After 'Apple' comes 'released iOS,' so it's probably the tech company!" Together, they figure out the right answer.

The trick is: this only works if you have the whole sentence already. If someone is still typing the sentence (like in a chatbot), Robot B can't read ahead because the words don't exist yet. That's why we only use bidirectional RNNs when we have the complete sentence or document to analyze.


Connections

  • Recurrent Neural Networks (RNNs): Bidirectional RNNs are built from two unidirectional RNNs
  • LSTM and GRU Cells: BiLSTMs and BiGRUs solve vanishing gradients in BiRNNs
  • Encoder-Decoder Architecture: BiRNNs are commonly used in the encoder (encode full source sentence) but never the decoder (autoregressive generation)
  • Attention Mechanism: Often applied on top of BiRNN outputs to weigh timesteps differently
  • Transformers: Replace BiRNNs with self-attention for better parallelization, but the intuition (full-sequence context) is similar
  • Named Entity Recognition: Canonical application of BiRNNs/BiLSTMs
  • Part-of-Speech Tagging: Another sequence labeling task where BiRNNs excel
  • Sequence-to-Sequence Models: BiRNNs in the encoder allow decoder to attend over full input context

#flashcards/ai-ml

What is the key difference between a unidirectional RNN and a Bidirectional RNN? :: A unidirectional RNN processes the sequence in one direction (left-to-right), so hth_t only depends on past inputs x1,,xtx_1, \ldots, x_t. A Bidirectional RNN has two RNNs: one processes left-to-right (ht\overrightarrow{h}_t), one processes right-to-left (ht\overleftarrow{h}_t), and at each timestep ht=[ht;ht]h_t = [\overrightarrow{h}_t; \overleftarrow{h}_t], giving access to both past and future context.

Why can't you use a Bidirectional RNN for language modeling (next-word prediction)?
Language modeling is a causal/autoregressive task: at time tt, you're predicting xt+1x_{t+1}, which doesn't exist yet. A BiRNN's backward pass would require access to future tokens xt+1,xt+2,x_{t+1}, x_{t+2}, \ldots, which is data leakage. At inference, those tokens don't exist, so the model would fail.
Write the forward pass equation for a Bidirectional RNN at timestep tt.
Forward: ht=tanh(Whht1+Wxxt+bh)\overrightarrow{h}_t = \tanh(W_{\overrightarrow{h}} \overrightarrow{h}_{t-1} + W_{\overrightarrow{x}} x_t + b_{\overrightarrow{h}}). Backward: ht=tanh(Whht+1+Wxxt+bh)\overleftarrow{h}_t = \tanh(W_{\overleftarrow{h}} \overleftarrow{h}_{t+1} + W_{\overleftarrow{x}} x_t + b_{\overleftarrow{h}}). Combined: ht=[ht;ht]h_t = [\overrightarrow{h}_t; \overleftarrow{h}_t].
Why do we concatenate [ht;ht][\overrightarrow{h}_t; \overleftarrow{h}_t] instead of adding ht+ht\overrightarrow{h}_t + \overleftarrow{h}_t?
Concatenation preserves both the past context (from ht\overrightarrow{h}_t) and the future context (from ht\overleftarrow{h}_t) as separate signals. Addition mixes them, which can lose information. Downstream layers can learn to weight past vs. future appropriately if they're kept separate.
Give two tasks where Bidirectional RNNs are appropriate and one where they are not.
Appropriate: Named Entity Recognition (full sentence available, label depends on surrounding words), Machine Translation encoder (encode full source sentence). Inappropriate: Text generation (decoder side), because future tokens don't exist yet during autoregressive generation.
What is the time complexity of a Bidirectional RNN for a sequence of length TT with hidden dimension dhd_h?
O(Tdh2)O(T \cdot d_h^2), the same as a unidirectional RNN but with a2× constant factor (because you run two RNNs). Each timestep requires an O(dh2)O(d_h^2) matrix multiplication (Whhht1W_{hh} h_{t-1}).
In a BiRNN, what does h1\overrightarrow{h}_1 encode? What does h1\overleftarrow{h}_1 encode?
h1\overrightarrow{h}_1 encodes only the first input x1x_1 (no past context). h1\overleftarrow{h}_1 encodes the entire future sequence x2,x3,xTx_2, x_3, \ldots x_T (processed right-to-left). Together, h1=[h1;h1]h_1 = [\overrightarrow{h}_1; \overleftarrow{h}_1] has both local (just x1x_1) and global (rest of sequence) information.
Why are BiLSTMs more common than vanilla BiRNNs in practice?
Vanilla RNNs suffer from vanishing gradients over long sequences. LSTMs have cell states ctc_t that act as gradient highways, allowing information to flow across many timesteps without degrading. BiLSTMs combine this with bidirectional context, making them powerful for long-sequence tasks like NER or machine translation.

Concept Map

only encodes past

fails on

examples

solved by

contains

contains

reads left to right

reads right to left

combined via concat or sum

combined via concat or sum

requires

Vanilla RNN

Causally Constrained

Full-Context Tasks

NER, Translation, Speech, Sentiment

Bidirectional RNN

Forward RNN

Backward RNN

Past Context

Future Context

Combined Hidden State

Full Sequence at Inference

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Chalo, ek simple si baat samjho. Normal RNN ek aisi cheez hai jo sentence ko sirf left se right padh sakta hai, matlab usko sirf "past" ka pata hota hai. Jaise agar tum ek line padh rahe ho aur apni ungli se aage ke saare words dhak do, tumhe sirf peeche ke words dikhenge. Par kaafi tasks mein, jaise "The cat sat on the ___", tumhe aage ka context bhi chahiye hota hai. Yahi problem Bidirectional RNN solve karta hai — yeh sequence ko dono directions mein ek saath process karta hai. Ek RNN left se right padhta hai (past context deta hai), doosra right se left (future context deta hai), aur phir dono ke outputs ko combine kar dete hain. Bilkul aise jaise do dost ek mystery novel padh rahe hon — ek shuruaat se, doosra end se — aur har page pe apne notes compare karein.

Ab yeh matter kyun karta hai? Kyunki real-world mein bahut saare tasks aise hain jahan meaning poore context pe depend karti hai, sirf past pe nahi. Jaise "Paris" ka matlab city hai ya person, yeh aage ke words se pata chalta hai. Ya sentiment analysis mein "This movie is not bad" — yahan "not" sab kuch badal deta hai, aur woh "bad" se pehle aata hai. Normal RNN yeh cheezein miss kar deta hai kyunki woh aage "peek" nahi kar sakta. BiRNN yeh limitation hata deta hai jab poori sequence available ho.

Technically, dono RNNs ke alag-alag weight matrices hote hain — kyunki forward wala past patterns seekh raha hai aur backward wala future patterns, toh dono ko independent parameters chahiye. Agar weights share karte, toh dono ko ek compromise representation seekhna padta jo achha nahi hota. End mein har timestep pe forward hidden state aur backward hidden state ko concatenate kar dete hain (ya add), jisse har position ka representation dono taraf ka full context capture karta hai. Bas yaad rakhna — yeh tabhi kaam karta hai jab poori input pehle se available ho, real-time next-word prediction jaise causal tasks mein iska use nahi kar sakte.

Go deeper — visual, from zero

Test yourself — Sequence Models

Connections