N-gram Language Models › Generating Text from N-gram Models

Michael BrenndoerferMarch 23, 20257 min read
Language AI HandbookAll chapters
Part 2 of 67Classical Text Representations
Chapter 8N-gram Language Models › Generating Text from N-gram Models
Reading section 9 of 15 · Generating Text from N-gram Models · 7 minSection 9 of 15 · 7 min

Book contents

Section 9 of 15 · 7 min read

N-gram Language Models › Generating Text from N-gram ModelsOne section · Sections in this chapter9 / 15 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

N-gram models can generate new text by sampling words according to their conditional probabilities. Starting from the start token, we repeatedly sample the next word given the current context, then advance the context window by one position.

The generation algorithm is simple: maintain a sliding window of the last n−1n-1 words (the context), look up all words that have appeared after that context in training, sample from the distribution, add the sampled word to the output and to the context window, and repeat until </s> is generated or a maximum length is reached.

In[28]:
Code
import random


def generate_sentence(model, max_length=20, seed=None):
    """Generate a sentence from an n-gram model by sampling."""
    if seed is not None:
        random.seed(seed)

    toks = ["<s>"] * (model.n - 1) if model.n > 1 else []

    for _ in range(max_length):
        context = tuple(toks[-(model.n - 1) :]) if model.n > 1 else ()

        candidates = []
        for ngram, count in model.ngram_counts.items():
            if ngram[:-1] == context:
                word = ngram[-1]
                prob = model.probability(word, context)
                candidates.append((word, prob))

        if not candidates:
            break

        words, probs = zip(*candidates)
        next_word = random.choices(words, weights=probs, k=1)[0]

        if next_word == "</s>":
            break

        toks.append(next_word)

    start = model.n - 1 if model.n > 1 else 0
    return " ".join(toks[start:])


generated_bigram = [generate_sentence(bigram_model, seed=i) for i in range(5)]
generated_trigram = [generate_sentence(trigram_model, seed=i) for i in range(5)]
Out[29]:
Console
Generated sentences (bigram model):
--------------------------------------------------
  1. the bird sat on the fish
  2. the bird
  3. the tree
  4. the dog chased the dog sat on the cat ate the dog watched the dog sat on the dog ate
  5. the cat

Generated sentences (trigram model):
--------------------------------------------------
  1. the dog
  2. the bird
  3. the bird flew over the tree
  4. the dog chased the cat sat on the mat
  5. the cat

The bigram model generates sentences that reflect local word patterns from the training data. The trigram model maintains coherence over slightly longer distances since it sees two words of context at each step. Both models tend to produce sentences similar to those in the training corpus: this is a feature, not a bug, since the models have learned the statistical patterns of the training text. With a larger and more diverse training corpus, the generated text would reflect the full variety of patterns the model has seen.

Temperature Sampling

We can control the randomness of generation with a temperature parameter TT. Temperature scaling adjusts the probability distribution over next words before sampling, sharpening it (more deterministic) or flattening it (more exploratory).

The idea is to scale log probabilities by dividing by TT before re-normalizing:

PT(wi)=exp⁡ ⁣(log⁡P(wi)/T)∑jexp⁡ ⁣(log⁡P(wj)/T)P_T(w_i) = \frac{\exp\!\left(\log P(w_i) / T\right)}{\sum_j \exp\!\left(\log P(w_j) / T\right)}

where:

  • P(wi)P(w_i): the original probability of word wiw_i from the language model
  • TT: the temperature parameter, a positive real number typically between 0.1 and 2.0
  • log⁡P(wi)/T\log P(w_i) / T: dividing the log probability by TT amplifies or compresses differences between words
  • exp⁡(⋅)\exp(\cdot): converts the scaled log probability back to a positive number for normalization
  • The denominator normalizes the result so all probabilities sum to 1

The temperature controls the shape of the distribution:

  • T<1T < 1 (low temperature): Differences in log probability are amplified, sharpening the distribution and concentrating probability on the most likely words.
  • T=1T = 1: Uses the original probabilities unchanged.
  • T>1T > 1 (high temperature): Differences are compressed, flattening the distribution and giving rare words more chance.
In[30]:
Code
def generate_with_temperature(model, temperature=1.0, max_length=20, seed=None):
    """Generate with temperature-scaled probabilities."""
    if seed is not None:
        random.seed(seed)

    toks = ["<s>"] * (model.n - 1) if model.n > 1 else []

    for _ in range(max_length):
        context = tuple(toks[-(model.n - 1) :]) if model.n > 1 else ()

        candidates = []
        for ngram, count in model.ngram_counts.items():
            if ngram[:-1] == context:
                word = ngram[-1]
                prob = model.probability(word, context)
                candidates.append((word, prob))

        if not candidates:
            break

        words, probs = zip(*candidates)

        if temperature != 1.0:
            log_probs = [math.log(p + 1e-10) / temperature for p in probs]
            max_log = max(log_probs)
            scaled_probs = [math.exp(lp - max_log) for lp in log_probs]
            total = sum(scaled_probs)
            probs = tuple(p / total for p in scaled_probs)

        next_word = random.choices(words, weights=probs, k=1)[0]

        if next_word == "</s>":
            break

        toks.append(next_word)

    start = model.n - 1 if model.n > 1 else 0
    return " ".join(toks[start:])


temperatures = [0.5, 1.0, 1.5]
temp_samples = {
    t: [
        generate_with_temperature(bigram_model, temperature=t, seed=i)
        for i in range(3)
    ]
    for t in temperatures
}
Out[31]:
Console
Effect of Temperature on Generation:
============================================================

Temperature = 0.5:
  1. the dog
  2. the food
  3. the bird flew over the dog ate the dog ate the cat

Temperature = 1.0:
  1. the bird sat on the fish
  2. the bird
  3. the tree

Temperature = 1.5:
  1. the bird sat on the fish
  2. the bird
  3. the branch

At low temperatures, the model gravitates toward the most common patterns, often producing the same output regardless of the random seed because the probability mass is concentrated on a single continuation. At high temperatures, it explores less likely paths and sometimes produces more creative but less coherent output. This temperature parameter appears throughout modern language modeling, including GPT-style systems. The same mathematical operation, scaling log probabilities before softmax, is used identically in transformer decoders.

Out[32]:
Visualization
Bar chart of word probabilities at temperature 0.5, with the top word dominating.
Word probability distribution after ''the'' at temperature T=0.5. The distribution is sharp: the top-probability word receives substantially more than its base probability, making generation more deterministic and repetitive.
Bar chart of word probabilities at the original temperature 1.0.
Original word probability distribution after 'the' at temperature T=1.0. This is the unmodified MLE distribution learned from training data, which reflects exactly how often each word followed 'the' in the corpus.
Bar chart of word probabilities at temperature 1.5, showing a more uniform distribution.
Word probability distribution after ''the'' at temperature T=1.5. The distribution is flattened: differences between words are compressed, giving less common continuations a better chance of being selected during generation.

The plots illustrate how temperature reshapes probability distributions. At T=0.5, the highest-probability word dominates the distribution. At T=1.5, the distribution flattens and rare words have a much better chance of being selected. This trade-off between coherence and diversity reappears in transformer-based language models, where it is controlled by exactly the same mechanism.