Word2Vec Training › Data Preprocessing Pipeline

Michael BrenndoerferApril 7, 20257 min read
Language AI HandbookAll chapters
Part 4 of 67Word Embeddings
Chapter 23Word2Vec Training › Data Preprocessing Pipeline
Reading section 2 of 14 · Data Preprocessing Pipeline · 7 minSection 2 of 14 · 7 min

Book contents

Section 2 of 14 · 7 min read

Word2Vec Training › Data Preprocessing PipelineOne section · Sections in this chapter2 / 14 ↓
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

Before training can begin, raw text must be transformed into a form the model can use. This preprocessing pipeline matters more than most practitioners realize. The decisions you make here, about tokenization, vocabulary size, and what to filter, directly affect embedding quality. Getting the preprocessing right is often the difference between embeddings that capture subtle semantic relationships and embeddings that are just noise in 300 dimensions.

Tokenization and Vocabulary Construction

Tokenization converts raw text into a sequence of tokens. For Word2Vec, simple whitespace tokenization often works surprisingly well. The key decision is what constitutes a token: do you lowercase everything, keep punctuation, split on hyphens? Each choice has downstream consequences.

Lowercasing is almost always the right call for Word2Vec. Without lowercasing, "Dog", "dog", and "DOG" become three separate vocabulary entries that receive independent gradient updates, even though they share essentially identical distributional context. On a billion-token corpus, "Dog" might appear rarely enough that it never develops a useful representation, while "dog" has thousands of training examples. Lowercasing merges these into a single entry and concentrates gradient signal.

Punctuation requires a separate decision. Keeping periods and commas as tokens means they become vocabulary entries, but they offer no semantic signal. Every sentence ends with a period regardless of content, so "period" would end up with an embedding that averages everything, similar to "the". Most practitioners strip punctuation entirely for Word2Vec.

Numbers present a different challenge. Replacing all numerals with a special <NUM> token prevents the vocabulary from being fragmented across "1", "2", ..., "10000", but it loses the ability to distinguish years from prices from distances. For most Word2Vec applications the tradeoff favors replacement.

Minimum frequency thresholding is perhaps the most consequential preprocessing decision. Rare words, those appearing fewer than 5 times in a billion-token corpus, cannot receive enough gradient updates to develop meaningful representations. Each training step moves an embedding a small amount; a word that appears only twice simply cannot travel far enough from its random initialization to reach a useful position in embedding space. Worse, rare words hurt other words: they each occupy a slot in the vocabulary, inflating the softmax denominator in naive implementations and adding noise to negative sampling. Removing them improves the embeddings of every word that remains.

In[3]:
Code
import re
from collections import Counter


def preprocess_text(text, min_count=5):
    """Tokenize, lowercase, and build vocabulary with frequency filtering."""
    # Lowercase and remove non-alphabetic characters except spaces
    text = text.lower()
    text = re.sub(r"[^a-z\s]", "", text)
    tokens = text.split()

    # Count word frequencies
    word_counts = Counter(tokens)

    # Build vocabulary: keep only words appearing at least min_count times
    vocab = {
        word: count for word, count in word_counts.items() if count >= min_count
    }

    # Filter tokens to only include vocabulary words
    tokens = [t for t in tokens if t in vocab]

    # Create word-to-index and index-to-word mappings
    word_to_idx = {word: idx for idx, word in enumerate(sorted(vocab.keys()))}
    idx_to_word = {idx: word for word, idx in word_to_idx.items()}

    return tokens, vocab, word_to_idx, idx_to_word


# Example corpus
corpus = """
the quick brown fox jumps over the lazy dog the dog barked at the fox
the fox ran away quickly the quick fox was clever the dog was slow but happy
the brown dog watched the clever fox the lazy dog slept all day long
word embeddings capture semantic meaning in dense vector space representations
the fox and the dog both live in the forest near the river
neural networks learn word embeddings from large text corpora effectively
"""

tokens, vocab, word_to_idx, idx_to_word = preprocess_text(corpus, min_count=2)
Out[4]:
Console
Total tokens (after filtering): 42
Vocabulary size: 11

Top 10 most frequent words:
  'the': 14 occurrences
  'fox': 6 occurrences
  'dog': 6 occurrences
  'quick': 2 occurrences
  'brown': 2 occurrences
  'lazy': 2 occurrences
  'was': 2 occurrences
  'clever': 2 occurrences
  'word': 2 occurrences
  'embeddings': 2 occurrences

A vocabulary size of 5 is small for this toy corpus, but in practice you would apply a minimum count threshold of 5 to 10 on large corpora. This removes rare words that would otherwise consume model parameters without receiving enough gradient updates to learn meaningful representations. On a web-scale corpus of 100 billion tokens, applying a minimum count of 5 typically reduces vocabulary size from tens of millions of unique strings down to a few million words with enough signal to learn from.

The choice of minimum count interacts with your embedding dimensionality. If you keep 100,000 vocabulary items and use 300-dimensional embeddings, your center embedding matrix alone is 30 million parameters. If you raise the minimum count to 100 and reduce vocabulary size to 50,000, you cut that in half while losing only very rare words that had poor representations anyway. Tighter frequency thresholds on smaller corpora and looser thresholds on larger corpora is a reasonable heuristic.

Generating Training Pairs

Given the processed token sequence, we generate (center, context) pairs using a sliding window. As described in the Skip-gram chapter, each word in the corpus becomes a center word, and all words within a window of size ww on either side become its context words.

The window size is a hyperparameter with meaningful semantic consequences, beyond ordinary parameter tuning. A small window of size 2 forces the model to focus on syntactic relationships: words that appear immediately adjacent tend to have grammatical dependencies, so the resulting embeddings encode part-of-speech and syntactic function. A large window of size 10 captures topical co-occurrence: words that appear anywhere in a ten-word span tend to share semantic domain, so the embeddings encode topic similarity. This is why researchers in the 2010s found that CBOW with a small window produced better syntactic embeddings while Skip-gram with a large window produced better semantic analogies.

There is also an important subtlety in how the original Word2Vec implementation handles window size: it samples a window size uniformly from 1 to ww for each center word, rather than using the same window size throughout. This means close neighbors contribute more often as context (they fall within any sampled window) while distant words contribute less often (they only fall within larger sampled windows). The effective weighting decreases with distance, which is a natural and desirable property: words right next to the center word are stronger context signals than words five positions away.

In[5]:
Code
import numpy as np


def generate_skipgram_pairs(tokens, word_to_idx, window_size=2):
    """Generate (center_idx, context_idx) pairs for skip-gram training."""
    pairs = []
    token_indices = [word_to_idx[t] for t in tokens]

    for center_pos, center_idx in enumerate(token_indices):
        # Define window boundaries
        start = max(0, center_pos - window_size)
        end = min(len(token_indices), center_pos + window_size + 1)

        for context_pos in range(start, end):
            if context_pos != center_pos:
                context_idx = token_indices[context_pos]
                pairs.append((center_idx, context_idx))

    return np.array(pairs)


pairs = generate_skipgram_pairs(tokens, word_to_idx, window_size=2)
Out[6]:
Console
Total training pairs generated: 162

Sample pairs (center_idx, context_idx):
  (the, quick)
  (the, brown)
  (quick, the)
  (quick, brown)
  (quick, fox)

The number of training pairs scales linearly with both corpus size and window size. A corpus of NN tokens with window size ww produces approximately 2wN2wN pairs (minus boundary effects at the start and end of each sentence). Larger windows capture broader topical similarity but reduce the signal for syntactic relationships between adjacent words, and they increase training time proportionally. For most downstream tasks, window sizes between 5 and 10 strike a good balance.