Contextual Embeddings and ELMo
Consider the word "bank" in "I deposited money at the bank" versus "I sat on the river bank." A static word embedding like word2vec or GloVe assigns "bank" exactly one vector, forever. This holds regardless of which sentence it appears in. This is a fundamental mismatch with how language actually works. Word meaning is fluid, and disambiguating it requires looking at surrounding context. Contextual embeddings solve this by computing a word's vector representation as a function of the entire sentence it occurs in. So "bank" gets a different vector in each sentence above. This idea reshaped NLP practice almost overnight. Before contextual embeddings, the standard pipeline was: look up a static vector for each token, feed the sequence into a task-specific recurrent network, and train that network on a labeled dataset. The static vectors carried general distributional knowledge. But all context-sensitivity had to be learned from scratch by the (often small) task-specific model. ELMo (Embeddings from Language Models), introduced in 2018, proposed instead to pretrain a deep bidirectional language model on a huge unlabeled corpus. It then used the model's internal hidden states as rich, contextualized word representations. These representations already encode syntax, semantics, and disambiguation information learned from language modeling itself. This happens before any task-specific training even begins. The big picture is a shift from "one vector per word type" to "one vector per word token in context." It is also a shift from feature engineering to representation transfer. ELMo also introduced a second, equally influential idea. Instead of using only the top layer of a deep network, it learns a task-specific weighted combination of all layers. This works since different layers specialize in different linguistic phenomena. Lower layers capture more syntax, while higher layers capture more semantics. This layer-mixing recipe, along with the pretrain-then-adapt philosophy, became the direct conceptual ancestor of Transformer architectures like BERT and GPT. This article builds ELMo from the ground up. It covers the bidirectional language modeling objective, the character-level convolutional input layer, the stacked bidirectional LSTM, the layer-weighting mechanism, and how the resulting embeddings plug into downstream tasks. We derive the complexity of training and inference. We prove why the bidirectional setup does not "cheat" by seeing the answer. We trace a full worked example. Finally, we place ELMo in its historical context, relative to both static embeddings and the attention-based models that superseded it.
- Static embeddings (word2vec, GloVe) give one fixed vector per word type; contextual embeddings give a different vector per token occurrence, computed from the sentence.
- ELMo pretrains a deep bidirectional LSTM language model (forward LM + backward LM) on unlabeled text, then reuses its internal states as features.
- Input words are represented via a character-level CNN, avoiding a fixed vocabulary and handling unknown/rare words gracefully.
- ELMo's key trick: for a downstream task, learn a softmax-weighted linear combination of all LSTM layers, not just the top layer.
- Complexity is dominated by the LSTM's sequential recurrence, giving time per direction but with poor parallelism across the sequence length .
- ELMo's pretrain-then-finetune-with-frozen-features paradigm directly foreshadows the pretraining recipes used by Transformer architectures.
A word's vector should depend on its sentence, and the best way to get such vectors is to pretrain a deep bidirectional language model and mix its internal layer activations.
The idea
A static embedding maps each word type in a fixed vocabulary to a single vector , learned once from a corpus and then frozen. This is the representation produced by word2vec, GloVe, or fastText. Its core limitation is lexical ambiguity collapse: polysemous words (bank, bat, spring), words whose sense shifts with register or domain, and words entangled in idioms all get squashed into one averaged vector that must serve every context simultaneously. A contextual embedding instead defines a function
that produces the representation of token as a function of the entire input sequence, not just in isolation. The same word type can now receive different vectors depending on its neighbors. ELMo instantiates as the internal hidden states of a deep bidirectional LSTM language model, so representations come "for free" as a byproduct of a well-understood, well-studied pretraining objective: predicting the next word (and the previous word) in a sentence. Two design choices make ELMo work well in practice. First, the model is bidirectional but not naively so: it trains a genuinely separate forward LSTM (reading left-to-right, predicting from ) and a genuinely separate backward LSTM (reading right-to-left, predicting from ), then concatenates their states. This avoids the training-time information leak that would occur if a single recurrent pass could see the target word through the other direction (an issue that motivated masked, rather than autoregressive, objectives in later architectures). Second, ELMo does not simply expose the top LSTM layer as "the" embedding. Instead, it exposes all layers — the character-CNN input layer and every biLSTM layer — and lets each downstream task learn its own softmax-normalized weighted sum. This is justified empirically and theoretically: different layers of a deep LSTM language model specialize in different levels of linguistic structure, so a syntax-heavy task (like part-of-speech tagging) and a semantics-heavy task (like word-sense disambiguation or question answering) should draw on different layers with different emphasis.
How it works
Step 1 — Character-level input representation. Rather than a lookup table indexed by word identity (which requires a closed vocabulary and fails on out-of-vocabulary words), ELMo represents each word by running its character sequence through a convolutional layer, followed by a highway network and a linear projection. This produces a context-independent word vector for word , built purely from its spelling, before any recurrence happens. This is important: it means unknown words, misspellings, and morphologically rich languages are all handled gracefully, since the model never needs an explicit "UNK" fallback.
Step 2 — Forward and backward language models. Given the sequence of context-independent vectors , a forward LSTM language model computes
by running an -layer stacked LSTM forward through the sequence, producing hidden states at layer and position . Symmetrically, a backward LSTM language model computes
producing hidden states . The two directions share the character-CNN input layer and the output softmax vocabulary projection, but maintain separate LSTM parameters, so the forward LSTM never has access to right-context and vice versa. Training jointly maximizes the sum of the forward and backward log-likelihoods.
Step 3 — Assembling per-layer representations. For each token and each layer , concatenate the two directions:
Together with the layer-0 "token representation" (the character-CNN output, duplicated to match dimensionality), this gives candidate vectors per token.
Step 4 — Task-specific layer mixing. For a specific downstream task , ELMo computes
where are learned, task-specific softmax-normalized layer weights, and is a learned scalar that rescales the entire vector (useful because the distribution of the biLM's internal states can differ substantially from what the downstream task's optimizer expects). Both and are trained on the downstream task while the biLM's own weights are typically kept frozen.
Step 5 — Downstream integration. The resulting vector is concatenated with (or substituted for) the static embedding for and fed into the downstream task's architecture (e.g., a BiLSTM tagger, a span-extraction QA model), which is trained normally with backpropagation, updating everything except the frozen biLM weights. ```
# ----- Pretraining phase ----- function TRAIN_BILM(corpus, L, vocab): initialize CharCNN, Highway, forward_LSTM (L layers), backward_LSTM (L layers), softmax_out for each sentence (w_1, ..., w_n) in corpus:
# context-independent token vectors for k in 1..n: x_k = Highway(CharCNN(chars(w_k)))
# forward pass: left-to-right, causal fwd_h[0] = zero_state for k in 1..n: fwd_h[k] = forward_LSTM(x_k, fwd_h[k-1]) # all L layers p_fwd_k = softmax_out(fwd_h[k][L]) loss += -log p_fwd_k[w_{k+1}] # predict NEXT word
# backward pass: right-to-left, causal in reverse bwd_h[n+1] = zero_state for k in n..1: bwd_h[k] = backward_LSTM(x_k, bwd_h[k+1]) p_bwd_k = softmax_out(bwd_h[k][L]) loss += -log p_bwd_k[w_{k-1}] # predict PREVIOUS word backpropagate(loss) update(CharCNN, Highway, forward_LSTM, backward_LSTM, softmax_out) return CharCNN, Highway, forward_LSTM, backward_LSTM
# ----- Feature extraction / fine-tuning phase ----- function ELMO_EMBED(sentence, biLM, s_task, gamma_task): x = [Highway(CharCNN(chars(w))) for w in sentence] fwd_h = run_forward(biLM.forward_LSTM, x) # L layers, per position bwd_h = run_backward(biLM.backward_LSTM, x) # L layers, per position for k in 1..len(sentence): h[k][0] = concat(x[k], x[k]) # layer-0 = token repr, doubled for j in 1..L: h[k][j] = concat(fwd_h[k][j], bwd_h[k][j]) elmo = [] for k in 1..len(sentence): weighted_sum = sum(s_task[j] h[k][j] for j in 0..L) elmo.append(gamma_task weighted_sum) return elmo # one contextual vector per token
function TRAIN_DOWNSTREAM(task_data, biLM, task_model): freeze(biLM) # biLM weights fixed initialize s_task (softmax weights), gamma_task, task_model for (sentence, label) in task_data: elmo_vecs = ELMO_EMBED(sentence, biLM, s_task, gamma_task) input_reprs = [concat(static_embed(w), elmo_vecs[k]) for k, w in enumerate(sentence)] prediction = task_model(input_reprs) loss = task_loss(prediction, label) backpropagate(loss) # updates s_task, gamma_task, update(s_task, gamma_task, task_model) # and task_model only ```
Complexity & correctness
Time complexity. Let be sentence length, the number of stacked LSTM layers, the hidden dimension, and the vocabulary size for the softmax output. Character-CNN encoding of a word of length costs per word (convolution + highway), so total, typically negligible compared to the recurrence. Each LSTM layer processes the sequence with sequential steps, each step costing (matrix multiplications for gates), giving per layer and for the full stack, doubled for the two directions: overall (constant factor 2 absorbed). The softmax output layer costs per direction unless a sampled or hierarchical softmax is used, in which case this drops to or for negative samples. Crucially, the LSTM recurrence is inherently sequential: step 's hidden state depends on step 's, so within one direction the steps cannot be parallelized across time, even though the two directions (forward/backward) and different sentences in a batch can run in parallel. This is the central efficiency contrast with attention-based architectures, where all positions can be processed simultaneously.
Space complexity. Storing all hidden states across all layers and both directions (needed for the downstream layer-mixing step) requires memory per sentence. Training additionally requires storing activations for backpropagation through time, which is also , since BPTT must retain every intermediate gate activation to compute gradients.
Correctness of the bidirectional objective (no information leak). A natural but incorrect design would be to run a single bidirectional LSTM whose forward and backward hidden states are concatenated at every layer and then used to jointly predict the current word . This is invalid, because concatenating (which has seen ) with (which has seen ) and then feeding that combined state through further shared layers before predicting would let the backward path leak information about 's left context into a representation used to predict itself — worse, if layers are shared and re-processed jointly, the network could shortcut and effectively "copy" from the backward path, driving training loss to near-zero without learning anything useful. ELMo avoids this by keeping the forward and backward LSTMs entirely separate stacks of parameters trained on two separate autoregressive objectives:
and only after both models are trained are their hidden states concatenated for use as a feature (never fed back into either LM's own prediction). The invariant maintained throughout training is: the forward LSTM's hidden state at position is a function only of , and the backward LSTM's hidden state at position is a function only of . This invariant is preserved by construction (each recurrence only ever consumes strictly-past, resp. strictly-future, tokens) and it is exactly what makes maximizing a valid proxy for learning the true joint distribution of language, rather than a degenerate shortcut.
Why layer-mixing is justified. Each LSTM layer's hidden state is trained (implicitly, via backpropagation from the language-modeling loss) to be useful for predicting the next/previous word using only what the previous layer computed. Layers do not receive an explicit supervision signal telling them "encode syntax" or "encode semantics" — but empirically (via probing classifiers on ELMo's layers, reported in the original work) lower layers achieve stronger performance on tasks that are more local and syntactic (e.g., part-of-speech tagging), while higher layers do better on tasks requiring longer-range semantic disambiguation (e.g., word-sense disambiguation). Because the ideal mixture is task-dependent and not knowable in advance, learning a softmax-normalized convex combination is a principled way to let each downstream task extract exactly the blend of abstraction levels it needs, while the shared biLM backbone stays frozen and reusable — this is the same idea that later motivates layer-wise probing and layer-selection strategies in Transformer architectures.
Tradeoffs. ELMo's sequential recurrence caps training throughput and cannot exploit the full parallelism available in modern GPU/TPU hardware the way self-attention can, which is one reason Transformer-based models (relying on the Attention Mechanism in NLP rather than recurrence) eventually replaced biLSTM language models as the default pretraining architecture. ELMo also freezes the pretrained biLM and only adapts a lightweight mixing layer, which is cheap and avoids catastrophic forgetting, but it sacrifices the deeper task-specific adaptation that full fine-tuning (as in BERT) can achieve.
Worked example
Take the sentence: "The bank raised interest rates." and contrast it with "They sat by the river bank." We trace how ELMo would represent the token "bank" differently in each.
| Step | Sentence A: "The bank raised interest rates." | Sentence B: "They sat by the river bank." |
|---|---|---|
| Char-CNN input | Identical in both — built purely from the characters b-a-n-k | Identical in both — same spelling, same |
| Forward LSTM context seen at "bank" | "The" (position 1) | "They sat by the river" (positions 1–5) |
| Backward LSTM context seen at "bank" | "raised interest rates" (right context) | "" (end of sentence, minimal right context) |
| Layer-1 hidden state | Reflects a short left context + strong financial right-context cue from "raised interest rates" | Reflects long left context "sat by the river," priming spatial/geographic sense |
| Layer-2 hidden state | Further sharpens toward financial-institution sense; probing studies show later layers disambiguate word sense strongly | Further sharpens toward riverbank/geographic sense |
| Task weights (e.g. for a WSD task) | Learned weights favor higher layers (semantic) | Same fixed weights, but different input hidden states → different output |
| Final | Vector A: near "loan," "financial-institution," "account" in the induced contextual space | Vector B: near "shore," "river," "embankment" in the induced contextual space |
| Cosine similarity | Substantially lower than that a static embedding would give | (same comparison) |
The essential contrast: a static embedding gives by definition (cosine similarity exactly 1, since it's the same lookup vector), while ELMo gives two vectors whose cosine similarity is noticeably lower, reflecting genuinely different senses — this is the direct, measurable payoff of context-dependence, and it is exactly what downstream disambiguation-sensitive tasks (word-sense disambiguation, coreference resolution, semantic role labeling) benefit from.
Common pitfalls & traps
| Pitfall | Why it's wrong | Fix |
|---|---|---|
| Treating ELMo's biLSTM as one interleaved network reading both directions simultaneously in the same layer stack used for prediction | Leaks the target word's identity through the opposite direction, producing a training loss that trivially collapses | Keep forward and backward LSTMs as fully separate parameter stacks, concatenate their states only after both are computed, never inside a shared recurrent step used for LM prediction |
| Using only the top LSTM layer as "the" embedding | Discards useful syntactic information concentrated in lower layers; task performance is measurably worse than with mixing | Learn a softmax-weighted combination of all layers per task, as ELMo specifies |
| Fine-tuning the entire biLM end-to-end on a small downstream dataset | Risks catastrophic forgetting of the broad language-modeling knowledge, and a small dataset cannot re-estimate millions of biLM parameters reliably | Freeze the biLM; only train the lightweight layer-mixing weights and task-specific head |
| Assuming ELMo vectors are as parallelizable as attention-based embeddings | The LSTM recurrence is inherently sequential in time, unlike self-attention; naive benchmarking comparisons mislead about training speed | Account for sequential depth when estimating wall-clock training/inference cost |
| Forgetting the character-CNN's role for rare/unknown words | Falling back to a static "UNK" vector reintroduces the exact problem contextual embeddings were meant to solve | Always route unknown words through the character-level encoder, never through a fixed UNK lookup |
Pioneers & history
Origins · 1 minMatthew E. Peters and how this was discovered
Where this matters
ELMo's release in 2018 triggered an immediate, measurable jump in state-of-the-art results across a wide range of NLP benchmarks — question answering, textual entailment, semantic role labeling, coreference resolution, named-entity recognition, and sentiment analysis — simply by swapping static embeddings for ELMo features in existing task architectures, with no other architectural change. This demonstrated conclusively that large-scale unsupervised pretraining, transferred via contextual representations, was a more powerful lever than continued hand-engineering of task-specific architectures. That lesson directly shaped the design of Transformer architectures such as BERT, GPT, and their successors, all of which retain ELMo's core recipe — pretrain broadly on unlabeled text, transfer to downstream tasks — while replacing the recurrent backbone with the parallelizable Attention Mechanism in NLP and, in BERT's case, replacing autoregressive language modeling with a masked-token objective that lets a single stack see both directions safely. Contextual embeddings from ELMo and its successors are used throughout industrial NLP systems: search engines rely on contextual query and document representations for relevance ranking, virtual assistants use them for intent classification and slot filling, machine translation systems use contextual encoders, and content-moderation and sentiment-analysis pipelines rely on them to disambiguate sarcasm, negation, and domain-specific word senses that static embeddings cannot capture. Understanding ELMo also clarifies, by contrast, exactly what problem attention-based pretraining was designed to solve — sequential bottlenecks and one-directional shortcuts — making it a natural conceptual stepping stone toward modern large language models.
Ask the tutor: How does ELMo's forward/backward separation compare to BERT's masked-language-model approach to achieving safe bidirectionality?
How exactly does ELMo differ from just averaging word2vec vectors over a sentence?
Averaging static vectors still starts from context-independent per-word representations and combines them with a simple, fixed, order-insensitive operation; the word "bank" contributes the identical vector to the average regardless of its neighbors. ELMo instead computes each word's representation using a deep bidirectional LSTM language model that has processed the specific sequential order and content of the whole sentence, so the representation itself is different per occurrence, not just the sentence-level pooling. ELMo also captures nonlinear, long-range dependencies through its recurrent gates, something a simple average cannot express. The two approaches also differ in training objective: averaging reuses embeddings trained via co-occurrence-based objectives (skip-gram, CBOW), while ELMo's vectors emerge from an autoregressive language-modeling objective across full sentences.
Is ELMo still used in practice, or has it been fully replaced?
In modern production systems, Transformer-based pretrained models (BERT, RoBERTa, GPT-family, and their descendants) have almost entirely replaced ELMo, because self-attention parallelizes better on modern hardware, models can be made much deeper and larger, and masked-language-model or autoregressive Transformer objectives scale more effectively with data and compute. ELMo remains valuable pedagogically and historically: it was the first widely adopted demonstration that deep contextual pretraining beats static embeddings across many tasks, and its layer-mixing insight (that different depths encode different linguistic abstractions) persists as a design principle, informing layer-wise probing studies and layer-selection heuristics used with modern Transformer encoders.
Why does ELMo use a character-level CNN instead of a word-lookup table for input?
A word-lookup table requires a fixed, closed vocabulary decided in advance; any word not seen during training (a typo, a rare proper noun, a novel compound) must be mapped to a generic "unknown" token, discarding useful information. A character-level convolutional network instead builds a vector from the sequence of characters in the word, so morphologically related or novel words with recognizable substrings ("un-", "-ing", "-tion") still receive meaningful representations. This also dramatically shrinks the number of parameters needed for the input layer, since the model needs only enough parameters to recognize character n-gram patterns, not one embedding per word type in a huge vocabulary.
What would go wrong if you fine-tuned the entire ELMo biLM on a small downstream dataset instead of freezing it?
The biLM has typically been trained on hundreds of millions of words to learn broad distributional and syntactic regularities; a downstream dataset for, say, named-entity recognition might contain only tens of thousands of sentences. Fully fine-tuning all biLM parameters on such a small dataset risks catastrophic forgetting, where the model's weights shift to fit the narrow downstream signal and lose the general language competence that made the pretrained representations valuable in the first place. The standard ELMo recipe freezes the biLM and only trains the lightweight layer-mixing weights , the scalar , and the task-specific head, which is both cheaper and empirically more robust for small-data regimes.
How does the invariant about forward/backward hidden states relate to correctness of the language-modeling objective?
The correctness argument rests on a strict separation invariant: the forward LSTM's hidden state at position must be a deterministic function only of tokens , and the backward LSTM's hidden state at position only of . As long as this invariant holds throughout training, maximizing the forward and backward log-likelihoods is a valid maximum-likelihood estimation of two genuinely different conditional distributions, both consistent with the true underlying language distribution. If the invariant were violated — for instance, by sharing recurrent state across directions before prediction — the objective would no longer correspond to a meaningful density estimation problem, and the trained hidden states would not reliably transfer to downstream tasks.
Why does layer-mixing help more than using just the final LSTM layer?
In a deep stacked LSTM language model, each layer builds on the representations of the layer below it, and empirically (through probing classifier experiments) lower layers tend to encode more local, syntactic regularities (part-of-speech identity, chunk boundaries) while higher layers encode more global, semantic regularities (word sense, coreference-relevant properties). Different downstream tasks weight these needs differently: a shallow syntactic tagging task benefits more from lower-layer information, while a task requiring deep semantic disambiguation benefits more from higher layers. By learning a softmax-weighted combination per task, ELMo lets each downstream model automatically discover and exploit the mixture of abstraction levels most useful for its objective, rather than being restricted to whatever the final layer happens to encode.
Does ELMo handle long-range dependencies as well as attention-based models?
Not as reliably. An LSTM must propagate information about a distant token through many sequential hidden-state updates, and even with gating mechanisms designed to mitigate vanishing gradients, very long-range dependencies can still degrade over long sequences. Self-attention, used in Transformer architectures, instead lets every position directly attend to every other position in a single computation step, giving a more direct path for long-range information flow and better empirical performance on tasks requiring long-distance reasoning. This is one of the concrete architectural motivations for the transition from recurrent contextual encoders like ELMo to attention-based pretrained encoders.