Bias Mitigation: Debiasing, CDA, and Fair Fine-tuning › Debiasing Embeddings: A Worked Example
Language AI HandbookAll chapters Bias Mitigation: Debiasing, CDA, and Fair Fine-tuning › Debiasing Embeddings: A Worked ExampleOne section · Sections in this chapter7 / 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 us walk through a concrete implementation of hard debiasing for GloVe embeddings. We will load 50-dimensional GloVe vectors, identify the gender direction, and neutralize a set of occupation words.
Setup and loading. We start by loading the embeddings and defining our gendered seed pairs.
import numpy as np
# Simulate small GloVe-like embeddings for demonstration
# In practice: load from glove.6B.50d.txt
np.random.seed(42)
vocab_size = 100
embed_dim = 50
# Create synthetic embeddings with planted gender bias
# Gender direction is approximately the first dimension
embeddings = np.random.randn(vocab_size, embed_dim) * 0.5
# Words we will work with
words = [
"he",
"she",
"him",
"her",
"his",
"hers",
"man",
"woman",
"boy",
"girl",
"father",
"mother",
"doctor",
"nurse",
"engineer",
"teacher",
"programmer",
"receptionist",
"mechanic",
"secretary",
"king",
"queen",
"prince",
"princess",
]
word_to_idx = {w: i for i, w in enumerate(words)}
# Plant a gender bias signal
# "male" words get positive first dimension, "female" get negative
male_words = ["he", "him", "his", "man", "boy", "father", "king", "prince"]
female_words = [
"she",
"her",
"hers",
"woman",
"girl",
"mother",
"queen",
"princess",
]
for w in male_words:
if w in word_to_idx:
embeddings[word_to_idx[w], 0] += 1.5
for w in female_words:
if w in word_to_idx:
embeddings[word_to_idx[w], 0] -= 1.5
# Occupations with planted stereotypical bias
male_occupations = ["doctor", "engineer", "programmer", "mechanic"]
female_occupations = ["nurse", "teacher", "receptionist", "secretary"]
for w in male_occupations:
if w in word_to_idx:
embeddings[word_to_idx[w], 0] += 0.8
for w in female_occupations:
if w in word_to_idx:
embeddings[word_to_idx[w], 0] -= 0.8
# Normalize all embeddings to unit length
norms = np.linalg.norm(embeddings[: len(words)], axis=1, keepdims=True)
embeddings[: len(words)] = embeddings[: len(words)] / (norms + 1e-8)Identifying the gender direction. We compute difference vectors for each definitionally gendered pair and run PCA to find the primary direction.
# Definitionally gendered pairs for identifying bias direction
gendered_pairs = [
("he", "she"),
("him", "her"),
("his", "hers"),
("man", "woman"),
("boy", "girl"),
("father", "mother"),
("king", "queen"),
("prince", "princess"),
]
# Compute difference vectors
difference_vectors = []
for male_word, female_word in gendered_pairs:
if male_word in word_to_idx and female_word in word_to_idx:
male_vec = embeddings[word_to_idx[male_word]]
female_vec = embeddings[word_to_idx[female_word]]
diff = male_vec - female_vec
difference_vectors.append(diff)
difference_matrix = np.array(difference_vectors)
# PCA to find the primary gender direction
pca = PCA(n_components=1)
pca.fit(difference_matrix)
gender_direction = pca.components_[0] # Shape: (embed_dim,)
gender_direction = gender_direction / np.linalg.norm(gender_direction)Gender direction identified from 8 pairs First PC explains 25.4% of variance in difference vectors Gender direction vector (first 5 dims): [ 0.116 -0.04 0.166 0.13 -0.034]
The first principal component captures the dominant axis of variation across all gendered pairs. A high explained variance means the pairs agree strongly on a single underlying direction. In practice on real GloVe vectors, the first PC typically explains 60-70% of variance, which confirms that a single gender dimension is a useful approximation even though the full bias structure is higher-dimensional.
Measuring pre-debiasing bias. Before modifying anything, let us quantify the current bias by measuring how strongly occupation words project onto the gender direction.
occupations = [
"doctor",
"nurse",
"engineer",
"teacher",
"programmer",
"receptionist",
"mechanic",
"secretary",
]
# Gender projection: positive = male-associated, negative = female-associated
projections_before = {}
for occ in occupations:
if occ in word_to_idx:
vec = embeddings[word_to_idx[occ]]
proj = np.dot(vec, gender_direction)
projections_before[occ] = projGender projections (before debiasing): Occupation Projection Direction ------------------------------------------ mechanic 0.207 male-associated receptionist 0.176 male-associated doctor 0.168 male-associated nurse 0.144 male-associated teacher 0.086 male-associated secretary -0.034 neutral programmer -0.055 female-associated engineer -0.077 female-associated
Applying hard debiasing. Now we neutralize the occupation words by projecting out their gender component.
# Neutral words: these should not carry gendered associations
neutral_words = [
"doctor",
"nurse",
"engineer",
"teacher",
"programmer",
"receptionist",
"mechanic",
"secretary",
]
# Store debiased embeddings
debiased_embeddings = embeddings.copy()
for word in neutral_words:
if word in word_to_idx:
idx = word_to_idx[word]
vec = debiased_embeddings[idx]
# Project out the gender component
# vec_debiased = vec - (vec · g) * g
gender_projection = np.dot(vec, gender_direction)
debiased_embeddings[idx] = vec - gender_projection * gender_direction
# Re-normalize to unit length
norm = np.linalg.norm(debiased_embeddings[idx])
if norm > 1e-8:
debiased_embeddings[idx] /= norm
# Measure projections after debiasing
projections_after = {}
for occ in occupations:
if occ in word_to_idx:
vec = debiased_embeddings[word_to_idx[occ]]
proj = np.dot(vec, gender_direction)
projections_after[occ] = projGender projections (after debiasing): Occupation Before After Change ------------------------------------------------ mechanic 0.207 0.000 -0.207 receptionist 0.176 0.000 -0.176 doctor 0.168 0.000 -0.168 nurse 0.144 0.000 -0.144 teacher 0.086 0.000 -0.086 secretary -0.034 0.000 +0.034 programmer -0.055 -0.000 +0.055 engineer -0.077 0.000 +0.077 Average |projection| before: 0.118 Average |projection| after: 0.000 Bias reduction: 100.0%
Visualizing the effect. The following plots show the gender projections before and after debiasing, making the shift concrete.


The projections collapse toward zero for all occupation words after debiasing. The key intuition: we have not changed what these word vectors know about the world. "Doctor" still clusters near "hospital", "patient", and "diagnosis". We have only removed the directional pull toward masculine associations.
A second way to visualize this is to project all words into two dimensions and observe how the embedding geometry shifts. In the undebiased space, the first axis of variation separates male- and female-associated words. After debiasing the occupation words, that axis no longer is the primary organizer for those terms, even though the definitionally gendered words (he, she, king, queen) retain their positions.

