Skip to content

Word Embeddings (Word2Vec, GloVe)

In plain words

BoW and TF-IDF give every word its own slot in a giant, mostly-empty vector — they have no idea that “great” and “fantastic” mean almost the same thing. Embeddings fix that: every word becomes a short, dense vector, learned so that words used in similar contexts end up with similar vectors.

What embeddings are

Embeddings map words to dense vectors:

  • similar words have similar vectors

Example intuition:

  • king and queen vectors are close
  • dog and puppy vectors are close

Why embeddings are better than BoW (sometimes)

BoW/TF-IDF are sparse and don’t capture similarity.

Embeddings capture:

  • semantic relationships
  • analogies (to some extent)
diagram Diagram mermaid

Word2Vec

Word2Vec doesn’t need labeled data at all — the sentence itself provides the “labels.” It comes in two flavors, and both are trained the same way a classifier would be, just on a task invented purely from raw text:

  • CBOW (Continuous Bag of Words) predicts a missing word from its surrounding context: given ["the", ___, "sat", "down"]["the", ___, "sat", "down"], predict "cat""cat".
  • Skip-gram runs the same idea backwards: given "cat""cat", predict the words likely to appear near it ("the""the", "sat""sat", "down""down").

Either direction pushes two words toward similar vectors whenever they show up in similar contexts — which is exactly why “cat” and “kitten” end up close together despite never being told they’re related.

GloVe

Word2Vec learns by sliding a small window over text and looking at local context. GloVe (Global Vectors) instead starts from a big table of global word co-occurrence counts — how often every pair of words appears together across the entire corpus — and fits vectors so that the dot product between two word vectors approximates the log of how often those words co-occur. In practice, both approaches converge on embedding spaces with very similar properties (synonym clustering, the analogy directions below), and both ship as free, pretrained downloads for English.

Using pretrained vectors instead of training your own

Training good embeddings from scratch needs a lot of text. When your dataset is small, it’s usually better to download vectors someone already trained on billions of words and copy them straight into your model. The pattern is always the same three steps: parse the pretrained file into a {word: vector}{word: vector} lookup, build a matrix indexed by your vocabulary, then hand that matrix to an EmbeddingEmbedding layer as its starting weights.

Building an embedding matrix from pretrained GloVe vectors
import numpy as np
 
# a couple of toy vectors, standing in for lines parsed from glove.6B.100d.txt
glove_vectors = {
    "cat": np.array([0.23, -0.15, 0.88]),
    "dog": np.array([0.21, -0.11, 0.91]),
    "car": np.array([-0.60, 0.44, 0.02]),
}
 
vocabulary = {"cat": 1, "dog": 2, "car": 3}   # word -> row index (0 stays reserved for padding)
embedding_dim = 3
embedding_matrix = np.zeros((len(vocabulary) + 1, embedding_dim))
 
for word, i in vocabulary.items():
    vector = glove_vectors.get(word)
    if vector is not None:
        embedding_matrix[i] = vector
 
print(embedding_matrix)
Building an embedding matrix from pretrained GloVe vectors
import numpy as np
 
# a couple of toy vectors, standing in for lines parsed from glove.6B.100d.txt
glove_vectors = {
    "cat": np.array([0.23, -0.15, 0.88]),
    "dog": np.array([0.21, -0.11, 0.91]),
    "car": np.array([-0.60, 0.44, 0.02]),
}
 
vocabulary = {"cat": 1, "dog": 2, "car": 3}   # word -> row index (0 stays reserved for padding)
embedding_dim = 3
embedding_matrix = np.zeros((len(vocabulary) + 1, embedding_dim))
 
for word, i in vocabulary.items():
    vector = glove_vectors.get(word)
    if vector is not None:
        embedding_matrix[i] = vector
 
print(embedding_matrix)
text
[[ 0.    0.    0.  ]
 [ 0.23 -0.15  0.88]
 [ 0.21 -0.11  0.91]
 [-0.6   0.44  0.02]]
text
[[ 0.    0.    0.  ]
 [ 0.23 -0.15  0.88]
 [ 0.21 -0.11  0.91]
 [-0.6   0.44  0.02]]

Then plug that matrix straight into an EmbeddingEmbedding layer as its starting weights, and freeze it so training doesn’t overwrite the pretrained vectors:

Plugging a pretrained embedding matrix into Keras
import tensorflow as tf
 
embedding_layer = tf.keras.layers.Embedding(
    input_dim=embedding_matrix.shape[0],
    output_dim=embedding_matrix.shape[1],
    embeddings_initializer=tf.keras.initializers.Constant(embedding_matrix),
    trainable=False,   # keep GloVe's vectors frozen instead of fine-tuning them
)
Plugging a pretrained embedding matrix into Keras
import tensorflow as tf
 
embedding_layer = tf.keras.layers.Embedding(
    input_dim=embedding_matrix.shape[0],
    output_dim=embedding_matrix.shape[1],
    embeddings_initializer=tf.keras.initializers.Constant(embedding_matrix),
    trainable=False,   # keep GloVe's vectors frozen instead of fine-tuning them
)

Learning embeddings inside a Keras model

You rarely train Word2Vec by hand anymore — most deep models simply start with a keras.layers.Embeddingkeras.layers.Embedding layer and let it learn word vectors as part of training:

An Embedding layer inside a Keras model
import tensorflow as tf
 
vocab_size = 1000   # number of distinct words (+ OOV buckets)
embed_size = 16      # size of each word's dense vector
 
model = tf.keras.Sequential([
    # mask_zero=True tells every downstream layer to ignore padding tokens (ID 0)
    tf.keras.layers.Embedding(vocab_size, embed_size, mask_zero=True, input_shape=[None]),
    tf.keras.layers.GRU(32, return_sequences=True),
    tf.keras.layers.GRU(32),
    tf.keras.layers.Dense(1, activation="sigmoid"),
])
 
model.summary()
An Embedding layer inside a Keras model
import tensorflow as tf
 
vocab_size = 1000   # number of distinct words (+ OOV buckets)
embed_size = 16      # size of each word's dense vector
 
model = tf.keras.Sequential([
    # mask_zero=True tells every downstream layer to ignore padding tokens (ID 0)
    tf.keras.layers.Embedding(vocab_size, embed_size, mask_zero=True, input_shape=[None]),
    tf.keras.layers.GRU(32, return_sequences=True),
    tf.keras.layers.GRU(32),
    tf.keras.layers.Dense(1, activation="sigmoid"),
])
 
model.summary()

The EmbeddingEmbedding layer’s output shape is [batch size, time steps, embed_size][batch size, time steps, embed_size] — it turns each 2D batch of word IDs into a 3D stack of word vectors, one per token.

Visualize it

Word embeddings live in a high-dimensional space, but if you squash them down to 2 dimensions (say, with PCA or t-SNE), similar words cluster together. Click to reshuffle and watch semantically related words stay near each other while unrelated clusters drift apart:

sketch Word embeddings in 2D space p5.js
Each dot is a word's embedding vector projected to 2D, continuously drifting around its cluster. Similar words stay near each other. Click to reshuffle.

Geometric relationships, not just distances

Embeddings aren’t just “similar words are close together” — specific directions in the embedding space can carry meaning too. The classic example: the vector you’d add to go from “man” to “woman” is roughly the same vector that takes you from “king” to “queen” — a reusable “gender” direction. That means simple vector arithmetic can answer analogies:

Vector arithmetic: king - man + woman ≈ queen
import numpy as np
 
king  = np.array([0.90, 0.10, 0.40])
man   = np.array([0.85, 0.05, 0.35])
woman = np.array([0.75, 0.20, 0.45])
queen = np.array([0.85, 0.15, 0.42])   # the embedding the model actually learned
 
predicted_queen = king - man + woman
 
def cosine_sim(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
 
print("Predicted vector:", np.round(predicted_queen, 2))
print("Similarity to real 'queen' vector:", round(cosine_sim(predicted_queen, queen), 2))
Vector arithmetic: king - man + woman ≈ queen
import numpy as np
 
king  = np.array([0.90, 0.10, 0.40])
man   = np.array([0.85, 0.05, 0.35])
woman = np.array([0.75, 0.20, 0.45])
queen = np.array([0.85, 0.15, 0.42])   # the embedding the model actually learned
 
predicted_queen = king - man + woman
 
def cosine_sim(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
 
print("Predicted vector:", np.round(predicted_queen, 2))
print("Similarity to real 'queen' vector:", round(cosine_sim(predicted_queen, queen), 2))
text
Predicted vector: [0.8  0.25 0.5 ]
Similarity to real 'queen' vector: 0.99
text
Predicted vector: [0.8  0.25 0.5 ]
Similarity to real 'queen' vector: 0.99

Nobody tells the model that gender is a concept — a well-trained embedding space just ends up organized this way because “king” and “queen” (and “man” and “woman”) show up in structurally similar contexts.

Practical note

In modern NLP, embeddings are often learned as part of a deep model (Transformers). But Word2Vec/GloVe are great for understanding the concept.

Mini-checkpoint

What does it mean if two words have a high cosine similarity between embeddings?

  • they appear in similar contexts and are semantically related.

🧪 Try It Yourself

Exercise 1 – Cosine Similarity Between Vectors

Exercise 2 – An Embedding Layer’s Output Shape

Exercise 3 – Analogies with Vector Arithmetic

Next

Continue to Sentiment Analysis Tutorial — put embeddings (or TF-IDF) to work on a real classification task.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did