GloVe › Code Implementation

Michael BrenndoerferApril 10, 20259 min read
Language AI HandbookAll chapters
Part 4 of 67Word Embeddings
Chapter 25GloVe › Code Implementation
Reading section 9 of 13 · Code Implementation · 9 minSection 9 of 13 · 9 min

Book contents

Section 9 of 13 · 9 min read

GloVe › Code ImplementationOne section · Sections in this chapter9 / 13 ↓
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

Let's implement GloVe from scratch: build a co-occurrence matrix, define the weighted least squares objective, and train word vectors with PyTorch. We will use a small toy corpus to make the computation tractable, but every component scales directly to large vocabularies.

Building the Co-Occurrence Matrix

We start by tokenizing a corpus and computing the symmetric co-occurrence matrix within a fixed window. A common refinement is inverse-distance weighting: context words farther from the center contribute a fractional count (1/distance1/\text{distance}) rather than a full count. This gives more weight to immediate neighbors and less weight to distant context, capturing syntactic proximity more naturally.

In[8]:
Code
from collections import Counter

# Small toy corpus for illustration
# Two semantic domains: animals/actions and states-of-water
corpus = [
    "the cat sat on the mat",
    "the cat ate the rat",
    "the dog sat on the mat",
    "the dog chased the cat",
    "ice is solid and cold",
    "steam is gas and hot",
    "water is liquid and wet",
    "ice and steam are both water",
    "solid ice melts into water",
    "hot steam comes from boiling water",
    "the cat drinks cold water",
    "the dog sat near the cold ice",
]

# Tokenize and build vocabulary
tokens_list = [sentence.lower().split() for sentence in corpus]
all_tokens = [t for sent in tokens_list for t in sent]
word_counts = Counter(all_tokens)

# Filter to words appearing at least twice
min_count = 2
vocab = sorted([w for w, c in word_counts.items() if c >= min_count])
word_to_idx = {w: i for i, w in enumerate(vocab)}
V = len(vocab)
Out[9]:
Console
Vocabulary size: 14
Words: ['and', 'cat', 'cold', 'dog', 'hot', 'ice', 'is', 'mat', 'on', 'sat', 'solid', 'steam', 'the', 'water']
In[10]:
Code
def build_cooccurrence_matrix(tokens_list, word_to_idx, window=2):
    """Build symmetric co-occurrence matrix with inverse-distance weighting."""
    V = len(word_to_idx)
    X = np.zeros((V, V), dtype=np.float32)

    for tokens in tokens_list:
        # Only keep tokens in vocabulary
        indices = [word_to_idx[t] for t in tokens if t in word_to_idx]
        for pos, center in enumerate(indices):
            # Window: positions within +-window
            start = max(0, pos - window)
            end = min(len(indices), pos + window + 1)
            for ctx_pos in range(start, end):
                if ctx_pos == pos:
                    continue
                ctx = indices[ctx_pos]
                # Weight by inverse distance (common variant)
                dist = abs(ctx_pos - pos)
                X[center, ctx] += 1.0 / dist

    return X


X = build_cooccurrence_matrix(tokens_list, word_to_idx, window=3)
Out[11]:
Console
Co-occurrence matrix shape: (14, 14)
Non-zero entries: 89
Sparsity: 54.6%

The high sparsity is expected: most pairs of words in the vocabulary never appear in the same context window. Real-world vocabularies of 400,000 words would be even sparser, with far less than 1% of entries being nonzero.

Visualizing the Co-Occurrence Matrix

Out[12]:
Visualization
Heatmap of a word co-occurrence matrix with vocabulary words on both axes and color intensity showing count values.
Co-occurrence matrix for the toy corpus (window size 3, inverse-distance weighting). Darker cells indicate higher co-occurrence values. The toy corpus contains two semantic clusters: animal and action words (cat, dog, sat, mat) and state-of-water words (ice, steam, cold, hot). The block structure visible in the matrix reflects these two clusters, showing how co-occurrence counts naturally capture topical groupings even in a tiny corpus.

Implementing the GloVe Model

The model has four learnable components per word: a word vector (when the word is the target), a context vector (when the word is in context), and bias scalars for each role.

In[13]:
Code
class GloVeModel(nn.Module):
    def __init__(self, vocab_size, embed_dim):
        super().__init__()
        # Word vectors (target embeddings)
        self.word_embeddings = nn.Embedding(vocab_size, embed_dim)
        # Context vectors (context embeddings)
        self.context_embeddings = nn.Embedding(vocab_size, embed_dim)
        # Bias terms for each word
        self.word_biases = nn.Embedding(vocab_size, 1)
        self.context_biases = nn.Embedding(vocab_size, 1)

        # Initialize with small random values
        nn.init.uniform_(
            self.word_embeddings.weight, -0.5 / embed_dim, 0.5 / embed_dim
        )
        nn.init.uniform_(
            self.context_embeddings.weight, -0.5 / embed_dim, 0.5 / embed_dim
        )
        nn.init.zeros_(self.word_biases.weight)
        nn.init.zeros_(self.context_biases.weight)

    def forward(self, word_ids, context_ids):
        # Dot product + biases
        w = self.word_embeddings(word_ids)  # (batch, dim)
        c = self.context_embeddings(context_ids)  # (batch, dim)
        bw = self.word_biases(word_ids).squeeze(1)  # (batch,)
        bc = self.context_biases(context_ids).squeeze(1)  # (batch,)
        return (w * c).sum(dim=1) + bw + bc  # (batch,)

The initialization of word and context vectors with small random values scaled by 1/embed_dim1/\text{embed\_dim} follows the original GloVe paper. Initializing biases to zero is natural because the bias should start as a neutral correction that the optimizer adjusts as needed.

Defining the Weighted Loss

In[14]:
Code
def glove_weight_fn(x, x_max=10.0, alpha=0.75):
    """Weighting function for GloVe loss (adapted for small toy corpus)."""
    return torch.clamp((x / x_max) ** alpha, max=1.0)


def glove_loss(predictions, log_counts, weights):
    """Weighted least squares loss."""
    residuals = (predictions - log_counts) ** 2
    return (weights * residuals).sum()

Note that x_max=10.0 is used here (rather than the paper's 100) to account for the tiny corpus. With only 12 sentences, co-occurrence counts rarely exceed 10, so a lower saturation threshold is appropriate.

Preparing Training Data

In[15]:
Code
# Extract nonzero co-occurrence pairs
nonzero = np.argwhere(X > 0)
word_ids = torch.tensor(nonzero[:, 0], dtype=torch.long)
context_ids = torch.tensor(nonzero[:, 1], dtype=torch.long)
counts = torch.tensor(X[nonzero[:, 0], nonzero[:, 1]], dtype=torch.float32)
log_counts = torch.log(counts)
weights = glove_weight_fn(counts, x_max=10.0)
Out[16]:
Console
Training pairs: 89
Sample counts: [1.0, 1.0, 1.3333333730697632, 2.5, 1.0]
Sample log-counts: [0.0, 0.0, 0.28768211603164673, 0.9162907600402832, 0.0]
Sample weights: [0.17782793939113617, 0.17782793939113617, 0.2206500768661499, 0.3535533845424652, 0.17782793939113617]

Each training pair is an (i,j)(i, j) index pair, the log co-occurrence count log⁡Xij\log X_{ij}, and the weight f(Xij)f(X_{ij}). The entire co-occurrence matrix is loaded into memory as a flat list of nonzero pairs, which is the standard efficient representation for sparse training data.

Training the Model

In[17]:
Code
torch.manual_seed(42)
embed_dim = 8
model = GloVeModel(V, embed_dim)
optimizer = torch.optim.Adagrad(model.parameters(), lr=0.05)

losses = []
n_epochs = 200

for epoch in range(n_epochs):
    optimizer.zero_grad()
    preds = model(word_ids, context_ids)
    loss = glove_loss(preds, log_counts, weights)
    loss.backward()
    optimizer.step()
    losses.append(loss.item())
Out[18]:
Console
Initial loss: 13.2993
Final loss (epoch 200): 0.0001
Reduction: 220540.5x
Out[19]:
Visualization
Line plot of GloVe training loss decreasing over 200 epochs on a log scale, showing fast initial drop followed by slower convergence.
GloVe weighted least squares loss over 200 training epochs using AdaGrad, plotted on a log scale. The loss drops sharply in the first 20 to 30 epochs as the word vectors rapidly orient themselves toward the log co-occurrence structure. After this initial phase, the loss continues decreasing more gradually as the model fine-tunes both vectors and biases to minimize residuals on the most informative word pairs.

The log-scale plot reveals a common pattern in GloVe training: a steep initial drop followed by a much slower refinement phase. The initial drop corresponds to the vectors moving from random initialization to a rough approximation of the log co-occurrence structure. The slow refinement phase is the model fine-tuning the dot products to match the data more precisely, with AdaGrad's decreasing learning rates naturally slowing this phase down.

Extracting and Inspecting Embeddings

After training, we combine word and context vectors as the final representation. This is the averaging trick described earlier: summing the two complementary views of each word.

In[20]:
Code
# Final embeddings: sum of word and context vectors
W = model.word_embeddings.weight.detach().numpy()
C = model.context_embeddings.weight.detach().numpy()
embeddings = W + C  # Common practice: sum (equivalent to averaging up to scale)
Out[21]:
Console
Cosine similarities after GloVe training:
  ice        - cold      : 0.205
  cat        - dog       : 0.512
  sat        - mat       : -0.662
  ice        - dog       : -0.645

The cosine similarities reflect the semantic structure in the corpus. Words from the same semantic domain ("ice" and "cold", "cat" and "dog") should show higher similarity than cross-domain pairs ("ice" and "dog"). With only 12 sentences and 8 dimensions, the signal is weak but directionally correct.

Visualizing the Learned Embeddings

With only 8 dimensions and a tiny corpus, the embeddings will not be perfect. But projecting to 2D with PCA lets us check whether semantically similar words cluster together.

In[22]:
Code
from sklearn.decomposition import PCA

pca = PCA(n_components=2)
emb_2d = pca.fit_transform(embeddings)
Out[23]:
Visualization
2D PCA scatter plot of GloVe word embeddings with labeled points showing semantic cluster separation.
PCA projection of GloVe embeddings trained on the toy corpus, showing the first two principal components. Despite using only 8 dimensions and a tiny 12-sentence corpus, the model begins to organize words by their co-occurrence context: state-of-water words (ice, cold, solid, steam, hot, water) occupy different parts of the space from animal and action words (cat, dog, sat, mat). The imperfect separation is expected from such a small training sample.