Word embeddings¶
Static word vectors: the embedding matrix and lookup identity, the geometry of word vectors (cosine similarity, nearest neighbors, the analogy property), and a from-scratch PCA projection for visualization.
dlhub.embeddings.word_embeddings ¶
Word Embeddings -- Representation, Properties, and Projection¶
A from-scratch, teaching reference for static word embeddings: the embedding matrix and the lookup identity, the geometry of word vectors (cosine similarity, nearest neighbors, and the analogy / parallelogram property), and a from-scratch PCA projection to two dimensions for visualization.
This module treats the vectors as given -- a small, hand-planted toy matrix
(see :func:make_fixture) whose every answer is exact and checkable by eye. How
embeddings are learned (skip-gram, CBOW, negative sampling, GloVe, fastText) is
a separate topic, and so are bias geometry and contextual embeddings. This is the
foundational head: what a word vector is and what you can read off it.
Notation follows Andrew Ng's Deep Learning Specialization, Course 5, Week 2. The
embedding matrix E has shape (d, V) -- d features (rows) by V
vocabulary (columns). For a word with index i, its one-hot column
o_w in {0, 1}^V selects a column::
e_w = E o_w = E[:, i] in R^d
which is the module's spine: an embedding layer is a bias-free linear layer
whose input is one-hot, so in practice one gathers the column by index rather
than materializing the matrix product. (PyTorch stores the transpose, weight
of shape (V, d), so there a word vector is a row, weight[i, :]; the
idiom port documents that convention.)
References
- Mikolov, T., Yih, W., & Zweig, G. (2013). Linguistic Regularities in Continuous Space Word Representations. NAACL-HLT, 746-751. (The analogy / parallelogram property.)
- Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. https://arxiv.org/abs/1301.3781
- Pennington, J., Socher, R., & Manning, C. (2014). GloVe: Global Vectors for Word Representation. EMNLP. https://nlp.stanford.edu/pubs/glove.pdf
- Levy, O., Goldberg, Y., & Dagan, I. (2015). Improving Distributional Similarity with Lessons Learned from Word Embeddings. TACL, 3, 211-225. (3CosMul.)
- Ng, A. (2018). Deep Learning Specialization, Course 5 (Sequence Models), Week 2. https://www.coursera.org/specializations/deep-learning
License
MIT
Notes
- Deterministic PCA sign. The SVD fixes each principal direction only up to
sign, so two runs could return mirror-image coordinates. :func:
pca_project_2dremoves the ambiguity with a fixed convention -- flip each component so its largest-magnitude entry is positive -- which makes the projection byte-for-byte reproducible. The reconstruction is invariant to the flip (a component and its coordinate column flip together), so pinning the sign changes nothing else. - The toy matrix is rank-2 by construction. Only two of the four coordinates
carry signal (royalty, gender); the other two are zero. PCA to 2-D is therefore
loss-less -- it recovers the semantic plane exactly -- which is why the
reconstruction and distance-preservation checks hold to
1e-10rather than only approximately. - Cosine zero-guard. Cosine similarity is undefined for a zero vector (its
norm is zero). Rather than return a
nanthat would silently poison a ranking, :func:cosine_similarityreturns0.0-- "no similarity" -- when either argument is the zero vector. The fixture has no zero vectors; the guard is defensive.
PCAProjection ¶
Bases: NamedTuple
The result of projecting an embedding matrix to two dimensions.
Attributes:
| Name | Type | Description |
|---|---|---|
coords |
ndarray
|
The 2-D coordinates, shape |
components |
ndarray
|
The top-2 principal directions, shape |
mean |
ndarray
|
The mean word vector removed before projecting, shape |
singular_values |
ndarray
|
All singular values of the centered data, descending, shape
|
Source code in src/dlhub/embeddings/word_embeddings.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | |
make_fixture ¶
make_fixture() -> dict
Build the planted toy embedding matrix every test and document shares.
The matrix is hand-written, not trained: d = 4 features by V = 6
words, with only two coordinates carrying signal -- coordinate 0 is
royalty, coordinate 1 is gender (+1 masculine, -1 feminine), and
coordinates 2-3 are zero. Every similarity, neighbor, and analogy is
therefore exact and checkable by reading the numbers, and the data lives in a
2-D plane inside R^4 so the PCA projection is loss-less.
The vocabulary is stored sorted, which fixes the column order of E and
the word_to_ix mapping.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/embeddings/word_embeddings.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
one_hot ¶
one_hot(index: int, size: int) -> np.ndarray
Build the one-hot column vector o_w for a word index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The word's position |
required |
size
|
int
|
The vocabulary size |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/embeddings/word_embeddings.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
lookup ¶
lookup(E: ndarray, index: int) -> np.ndarray
Select a word's embedding by index -- the gather form of E o_w.
This returns the same vector as E @ one_hot(index, V) but by indexing the
column directly, which is what every real embedding layer does: the one-hot
matrix product is the definition, the column gather is the implementation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
E
|
ndarray
|
The embedding matrix, shape |
required |
index
|
int
|
The word's column index |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The embedding |
Source code in src/dlhub/embeddings/word_embeddings.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
cosine_similarity ¶
cosine_similarity(u: ndarray, v: ndarray) -> float
Cosine similarity between two vectors.
Computes (u . v) / (||u|| ||v||), the cosine of the angle between u
and v. It is +1 for identical directions, 0 for orthogonal
vectors, and -1 for opposite directions, independent of magnitude.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u
|
ndarray
|
First vector, shape |
required |
v
|
ndarray
|
Second vector, shape |
required |
Returns:
| Type | Description |
|---|---|
float
|
The cosine similarity, or |
Source code in src/dlhub/embeddings/word_embeddings.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
nearest_neighbors ¶
nearest_neighbors(E: ndarray, query: int, k: int = 1) -> list[tuple[int, float]]
Rank the other words by cosine similarity to a query word.
Scores every column against E[:, query] with :func:cosine_similarity,
sorts by similarity descending, and returns the top k. The query itself
is excluded, and ties are broken by ascending index, so the ranking is
deterministic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
E
|
ndarray
|
The embedding matrix, shape |
required |
query
|
int
|
Column index of the query word. |
required |
k
|
int
|
How many neighbors to return. Clipped to |
1
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/embeddings/word_embeddings.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | |
analogy ¶
analogy(E: ndarray, a: int, b: int, c: int) -> int
Solve "a is to b as c is to ?" by 3CosAdd.
Forms the target t = e_b - e_a + e_c and returns the vocabulary index
whose embedding is most cosine-similar to t. The three input words are
excluded from the candidates -- without this, king - man + woman returns
king -- and ties are broken by ascending index.
The multiplicative variant 3CosMul (Levy, Goldberg & Dagan, 2015) is more robust on real corpora but is not implemented here; on this planted fixture the additive form already recovers the exact answer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
E
|
ndarray
|
The embedding matrix, shape |
required |
a
|
int
|
Column index of the first word (subtracted). |
required |
b
|
int
|
Column index of the second word (added). |
required |
c
|
int
|
Column index of the third word (added). |
required |
Returns:
| Type | Description |
|---|---|
int
|
The column index of the analogy's answer. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any of |
Source code in src/dlhub/embeddings/word_embeddings.py
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | |
pca_project_2d ¶
pca_project_2d(E: ndarray) -> PCAProjection
Project word vectors to two dimensions with PCA, from scratch via SVD.
Lays the words out as rows X = E.T (V x d), removes the mean word
vector to get X_c, and takes the SVD X_c = U S W^T. The first two rows
of W^T are the directions of greatest variance; the 2-D coordinates are
the centered data projected onto them, X_c W[:, :2].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
E
|
ndarray
|
The embedding matrix, shape |
required |
Returns:
| Type | Description |
|---|---|
PCAProjection
|
The |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
The SVD fixes each principal direction only up to sign. To make the output reproducible, each component is flipped so its largest-magnitude entry is positive; the corresponding coordinate column flips with it, so the reconstruction is unchanged.
Source code in src/dlhub/embeddings/word_embeddings.py
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | |
main ¶
main() -> None
Run the fixture end to end and print its known answers.
Source code in src/dlhub/embeddings/word_embeddings.py
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 | |
Learned word vectors: skip-gram with negative sampling (SGNS), CBOW, the smoothed-unigram negative sampler, and GloVe's weighted least-squares objective, each loss paired with its exact gradient, trained from scratch on a toy corpus.
dlhub.embeddings.word2vec_and_glove ¶
Word2Vec and GloVe -- Learning Word Vectors from a Corpus¶
A from-scratch, teaching reference for how static word embeddings are
learned. The sibling :mod:dlhub.embeddings.word_embeddings treats the
vectors as given and reads geometry off them; this module produces those
vectors by optimising an objective over a toy corpus. It implements three
objectives, each with its loss and its exact gradient:
- Skip-gram with negative sampling (SGNS) -- predict context from centre,
with
Knegatives drawn from the smoothed unigram sampler. - CBOW -- the same tables and sampler with the roles reversed: average the context, predict the centre.
- GloVe -- a weighted least-squares fit to the log co-occurrence counts.
The gradients are the payload. Each is written term by term in the notation the
reference card uses, and :mod:tests.test_word2vec_and_glove checks every one
against a finite-difference approximation, because a flipped sign in a teaching
module teaches confidently.
Notation follows the dossier, which departs from the head topic deliberately.
The head's embedding matrix E is (d, V) (a word is a column). Here the
two learned tables are stored row-wise, (V, d), because every objective
indexes a word's vector by row and takes inner products -- gathering columns
from (d, V) would be all transpose noise:
W-- the centre (input) table, shape(V, d);W[w]is centre wordw's vector.C-- the context (output) table, shape(V, d);C[c]is context wordc's vector.
The head's matrix is what this topic produces: E is the learned W (the
head's (d, V) is this module's W transposed).
References
- Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. https://arxiv.org/abs/1301.3781
- Mikolov, T., Sutskever, I., Chen, K., Corrado, G., & Dean, J. (2013). Distributed Representations of Words and Phrases and their Compositionality. NeurIPS. https://arxiv.org/abs/1310.4546 (Negative sampling, unigram^0.75.)
- Pennington, J., Socher, R., & Manning, C. (2014). GloVe: Global Vectors for Word Representation. EMNLP. https://nlp.stanford.edu/pubs/glove.pdf
- Levy, O., & Goldberg, Y. (2014). Neural Word Embedding as Implicit Matrix Factorization. NeurIPS. (SGNS factorises shifted PMI.)
- Rong, X. (2014). word2vec Parameter Learning Explained. https://arxiv.org/abs/1411.2738 (The step-by-step gradient derivation.)
License
MIT
Notes
- Row-major tables.
WandCare(V, d)-- a word's vector is a row -- which is the opposite of the head's(d, V)E. The convention is chosen so the objectives read asW[w] @ C[c]rather thanE[:, w] @ E[:, c]; the relationship isE == W.Tafter training. - Stable sigmoid. :func:
sigmoidmultiplies through byexp(-|z|)so no intermediateexpoverflows: forz >= 0it is1 / (1 + e^-z)and forz < 0it ise^z / (1 + e^z), which are equal to the textbook form but never evaluateexpof a large positive number. - Stable log-sigmoid in the loss. The losses need
log sigma(z). Evaluatinglog(sigmoid(z))underflows to-infoncesigmoid(z)rounds to0, so the losses uselog sigma(z) = -softplus(-z)withsoftplus(x) = logaddexp(0, x)-- the same value, computed without the underflow. This is why the loss code calls :func:numpy.logaddexprather than spelling the formula withlogandsigmoiddirectly. - Windows do not cross sentence boundaries. The corpus is a list of
sentences; :func:
training_pairsruns on one sentence at a time, so no context window straddles two sentences. On the planted fixture every sentence is drawn from a single semantic family, so this is exactly what makes the co-occurrence counts within-family and zero across families. - One generator drives the run. :func:
train_sgnsseeds a singlenumpy.random.default_rng(seed)and draws from it in a fixed order --WthenCfor initialisation, then the per-pair negatives in pair order -- so a run is reproducible to the bit. The negatives for each pair are drawn once and reused every epoch, which fixes the objective and makes the training loss a clean descent curve (classic word2vec resamples every epoch; a teaching module that asserts a monotone loss on a toy corpus fixes the sample instead). - GloVe zero-count guard.
log X_ijis defined only forX_ij > 0. :func:glove_lossand :func:glove_gradientssum over observed entries only; a zero count contributes no term rather thanlog 0. GloVe's final vector is the sum of the two tables,W[i] + Wtilde[i].
make_fixture ¶
make_fixture() -> dict
Build the planted toy corpus every test and document shares.
The corpus is a literal list of 12 sentences x 3 tokens = 36 tokens,
three sentences from each of four semantic families (pets, food,
actions, places) of four words each. It is hand-written, never
downloaded, so every frequency and co-occurrence count is known by
construction and the sampler's distribution is closed-form.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/embeddings/word2vec_and_glove.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
build_vocabulary ¶
build_vocabulary(corpus: Sequence[Sequence[str]]) -> tuple[list[str], dict[str, int]]
Build the vocabulary and the word -> index map from a corpus.
The vocabulary is the sorted list of unique tokens, so the index of a word is
fixed by the corpus alone and does not depend on the order words were first
seen. Sorting is what makes word_to_ix reproducible across runs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
corpus
|
Sequence[Sequence[str]]
|
The corpus as a sequence of sentences, each a sequence of word strings. |
required |
Returns:
| Type | Description |
|---|---|
tuple[list[str], dict[str, int]]
|
|
Source code in src/dlhub/embeddings/word2vec_and_glove.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
training_pairs ¶
training_pairs(sentence: Sequence[int], window: int = 2) -> list[tuple[int, int]]
Extract (centre, context) index pairs from one sentence.
For every position i in the sentence, the centre token t_i is paired
with each context token t_{i+j} for offset j in
{-window, ..., -1, 1, ..., window} that lands inside the sentence. Pairs
are emitted by centre position, then by offset ascending, which fixes the
order in which stochastic descent sees them. The window is applied to a
single sentence so it never crosses a sentence boundary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sentence
|
Sequence[int]
|
One sentence as a sequence of token indices. |
required |
window
|
int
|
The half-width |
2
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
The |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
cooccurrence_matrix ¶
cooccurrence_matrix(sentences: Sequence[Sequence[int]], vocab_size: int, window: int = 2) -> np.ndarray
Count co-occurrences into the (V, V) matrix GloVe fits.
X[i, j] is the number of times word j falls inside word i's
context window, summed over the corpus. It reuses :func:training_pairs, so
the window rule is identical to the one SGNS and CBOW see, and because the
window stays inside a sentence the counts never cross a semantic family.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sentences
|
Sequence[Sequence[int]]
|
The corpus as sentences of token indices. |
required |
vocab_size
|
int
|
The vocabulary size |
required |
window
|
int
|
The half-width of the context window. |
2
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | |
unigram_probabilities ¶
unigram_probabilities(sentences: Sequence[Sequence[int]], vocab_size: int, exponent: float = 0.75) -> np.ndarray
Build the smoothed unigram distribution the negative sampler draws from.
Each word's probability is its corpus frequency raised to exponent and
normalised::
P(w) = f(w)^exponent / sum_w' f(w')^exponent
The default exponent = 3/4 is the Word2Vec choice. Raising a sub-unit
power pulls the distribution toward uniform, so rare words are drawn as
negatives more often than their raw frequency -- which is the point:
negatives drawn in proportion to raw frequency would be dominated by
stop-words and carry little signal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sentences
|
Sequence[Sequence[int]]
|
The corpus as sentences of token indices. |
required |
vocab_size
|
int
|
The vocabulary size |
required |
exponent
|
float
|
The smoothing exponent applied to each frequency. |
0.75
|
Returns:
| Type | Description |
|---|---|
ndarray
|
A |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
sample_negatives ¶
sample_negatives(rng: Generator, probabilities: ndarray, k: int) -> np.ndarray
Draw k negative word indices i.i.d. from the unigram sampler.
This is rng.choice(V, size=k, p=probabilities) -- a thin wrapper that
names the operation and keeps every draw on the single generator the training
run threads through, so the whole run stays reproducible.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rng
|
Generator
|
The run's generator; advancing it here is what keeps the draw order deterministic. |
required |
probabilities
|
ndarray
|
The |
required |
k
|
int
|
How many negatives to draw. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
A |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | |
sigmoid ¶
sigmoid(z: float | ndarray) -> float | np.ndarray
The logistic sigmoid 1 / (1 + e^-z), computed without overflow.
The textbook spelling evaluates exp(-z), which overflows for large
negative z. Multiplying through by exp(-|z|) -- which never exceeds
1 -- gives the identical value from a bounded exponential: 1 / (1 +
e^-z) for z >= 0 and e^z / (1 + e^z) for z < 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
float or ndarray
|
The pre-activation score (a scalar inner product, or an array of them). |
required |
Returns:
| Type | Description |
|---|---|
float or ndarray
|
|
Source code in src/dlhub/embeddings/word2vec_and_glove.py
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | |
sgns_loss ¶
sgns_loss(W: ndarray, C: ndarray, centre: int, context: int, negatives: Sequence[int]) -> float
Skip-gram negative-sampling loss for one (centre, context) pair.
With centre vector v = W[centre], positive context u = C[context] and
K negatives u_k = C[n_k]::
l = -log sigma(v . u) - sum_k log sigma(-v . u_k)
The positive term rewards a high score for the true context; each negative
term rewards a low score for a sampled non-context. The sigma(-z)
spelling of the negative term is the numerically stable one (see the module
Notes), and both terms are evaluated through :func:_log_sigmoid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
W
|
ndarray
|
The centre table, shape |
required |
C
|
ndarray
|
The context table, shape |
required |
centre
|
int
|
Index of the centre word. |
required |
context
|
int
|
Index of the true context word. |
required |
negatives
|
Sequence[int]
|
The |
required |
Returns:
| Type | Description |
|---|---|
float
|
The per-pair loss. |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | |
sgns_gradients ¶
sgns_gradients(W: ndarray, C: ndarray, centre: int, context: int, negatives: Sequence[int]) -> dict[str, np.ndarray]
Gradients of :func:sgns_loss with respect to both tables.
Term by term, with v = W[centre], u = C[context] and
u_k = C[n_k]::
dl/dv = (sigma(v . u) - 1) u + sum_k sigma(v . u_k) u_k
dl/du = (sigma(v . u) - 1) v
dl/du_k = sigma(v . u_k) v
Only the rows a pair touches receive a gradient, so the returned tables are
zero except at centre (in W) and at context and the negatives (in
C). Contributions are accumulated with += one index at a time, so a
negative that happens to equal the context -- or a repeated negative -- adds
correctly rather than overwriting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
W
|
ndarray
|
The centre table, shape |
required |
C
|
ndarray
|
The context table, shape |
required |
centre
|
int
|
Index of the centre word. |
required |
context
|
int
|
Index of the true context word. |
required |
negatives
|
Sequence[int]
|
The |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
|
Source code in src/dlhub/embeddings/word2vec_and_glove.py
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 | |
cbow_loss ¶
cbow_loss(W: ndarray, C: ndarray, centre: int, context_words: Sequence[int], negatives: Sequence[int]) -> float
CBOW negative-sampling loss for one (context -> centre) example.
CBOW reverses skip-gram: average the context vectors and predict the centre.
With ubar = mean_j C[c_j] over the context words, centre vector
v = W[centre] and negatives scored against the centre table,
v_k = W[n_k]::
l = -log sigma(ubar . v) - sum_k log sigma(-ubar . v_k)
It is :func:sgns_loss with ubar in the centre's place and the
centre/negative words looked up in W -- the same objective, the scoring
swapped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
W
|
ndarray
|
The centre table, shape |
required |
C
|
ndarray
|
The context table, shape |
required |
centre
|
int
|
Index of the centre word being predicted. |
required |
context_words
|
Sequence[int]
|
The context word indices whose vectors are averaged. |
required |
negatives
|
Sequence[int]
|
The |
required |
Returns:
| Type | Description |
|---|---|
float
|
The per-example loss. |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | |
cbow_gradients ¶
cbow_gradients(W: ndarray, C: ndarray, centre: int, context_words: Sequence[int], negatives: Sequence[int]) -> dict[str, np.ndarray]
Gradients of :func:cbow_loss with respect to both tables.
With ubar = mean_j C[c_j] and M = len(context_words)::
dl/dubar = (sigma(ubar . v) - 1) v + sum_k sigma(ubar . v_k) v_k
dl/dW[w] = (sigma(ubar . v) - 1) ubar
dl/dW[n_k]= sigma(ubar . v_k) ubar
dl/dC[c_j]= (1 / M) dl/dubar for each context word c_j
The averaging splits the gradient on ubar evenly back to each context
row (1 / M); a word that appears twice in the window collects the share
twice, which the one-index-at-a-time += handles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
W
|
ndarray
|
The centre table, shape |
required |
C
|
ndarray
|
The context table, shape |
required |
centre
|
int
|
Index of the centre word being predicted. |
required |
context_words
|
Sequence[int]
|
The context word indices whose vectors are averaged. |
required |
negatives
|
Sequence[int]
|
The |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
|
Source code in src/dlhub/embeddings/word2vec_and_glove.py
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 | |
glove_weight ¶
glove_weight(x: ndarray | float, x_max: float = 100.0, alpha: float = 0.75) -> np.ndarray | float
GloVe's co-occurrence weighting f(x) = min(1, (x / x_max)^alpha).
The weight grows with the count up to x_max and is capped at 1
thereafter, so very frequent pairs do not dominate the fit while rare pairs
are not ignored. It is only ever evaluated at observed counts x > 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray or float
|
A co-occurrence count (or array of them). |
required |
x_max
|
float
|
The count at which the weight saturates to |
100.0
|
alpha
|
float
|
The exponent controlling the ramp below |
0.75
|
Returns:
| Type | Description |
|---|---|
ndarray or float
|
The weight |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 | |
glove_loss ¶
glove_loss(W: ndarray, W_tilde: ndarray, b: ndarray, b_tilde: ndarray, X: ndarray, x_max: float = 100.0, alpha: float = 0.75) -> float
GloVe weighted least-squares loss over the observed co-occurrences.
For every X[i, j] > 0 the residual is
r_ij = W[i] . W_tilde[j] + b[i] + b_tilde[j] - log X[i, j] and::
J = sum_{i,j: X_ij > 0} f(X_ij) * r_ij^2
Zero counts contribute no term -- GloVe fits log X only where X is
observed, which is also what keeps log 0 out of the sum.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
W
|
ndarray
|
The word table, shape |
required |
W_tilde
|
ndarray
|
The context-word table, shape |
required |
b
|
ndarray
|
The word biases, shape |
required |
b_tilde
|
ndarray
|
The context-word biases, shape |
required |
X
|
ndarray
|
The |
required |
x_max
|
float
|
The weighting cap passed to :func: |
100.0
|
alpha
|
float
|
The weighting exponent passed to :func: |
0.75
|
Returns:
| Type | Description |
|---|---|
float
|
The total loss |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 | |
glove_gradients ¶
glove_gradients(W: ndarray, W_tilde: ndarray, b: ndarray, b_tilde: ndarray, X: ndarray, x_max: float = 100.0, alpha: float = 0.75) -> dict[str, np.ndarray]
Gradients of :func:glove_loss with respect to tables and biases.
With r_ij and f(X_ij) as in :func:glove_loss, each observed entry
contributes::
dJ/dW[i] += 2 f(X_ij) r_ij W_tilde[j]
dJ/dW_tilde[j] += 2 f(X_ij) r_ij W[i]
dJ/db[i] += 2 f(X_ij) r_ij
dJ/db_tilde[j] += 2 f(X_ij) r_ij
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
W
|
ndarray
|
The word table, shape |
required |
W_tilde
|
ndarray
|
The context-word table, shape |
required |
b
|
ndarray
|
The word biases, shape |
required |
b_tilde
|
ndarray
|
The context-word biases, shape |
required |
X
|
ndarray
|
The |
required |
x_max
|
float
|
The weighting cap passed to :func: |
100.0
|
alpha
|
float
|
The weighting exponent passed to :func: |
0.75
|
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
|
Source code in src/dlhub/embeddings/word2vec_and_glove.py
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 | |
train_sgns ¶
train_sgns(sentences: Sequence[Sequence[int]], vocab_size: int, *, d: int = 8, window: int = 2, k: int = 2, lr: float = 0.05, epochs: int = 200, seed: int = 0, exponent: float = 0.75) -> dict
Train skip-gram with negative sampling by full-batch gradient descent.
A single seeded generator draws the two tables (W then C) and then
the k negatives for every (centre, context) pair, in pair order. The
negatives are drawn once and reused each epoch, so the objective is fixed
and the loss is a clean descent curve. Each epoch sums the loss and the
gradient over all pairs and takes one plain SGD step -- no optimiser, no
momentum (that lesson lives in optimizer-update-rules).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sentences
|
Sequence[Sequence[int]]
|
The corpus as sentences of token indices. |
required |
vocab_size
|
int
|
The vocabulary size |
required |
d
|
int
|
The embedding dimension. |
8
|
window
|
int
|
The context half-width. |
2
|
k
|
int
|
The number of negatives per pair. |
2
|
lr
|
float
|
The gradient-descent step size. |
0.05
|
epochs
|
int
|
The number of full-batch passes. |
200
|
seed
|
int
|
Seeds the single generator driving init and sampling. |
0
|
exponent
|
float
|
The negative sampler's smoothing exponent. |
0.75
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/embeddings/word2vec_and_glove.py
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 | |
train_glove ¶
train_glove(X: ndarray, vocab_size: int, *, d: int = 8, lr: float = 0.05, epochs: int = 200, seed: int = 0, x_max: float = 100.0, alpha: float = 0.75) -> dict
Fit GloVe to a co-occurrence matrix by full-batch gradient descent.
A single seeded generator draws the two tables and the two bias vectors in
the order W, W_tilde, b, b_tilde. Each epoch sums the loss
and gradient over the observed entries of X and takes one SGD step. The
learned vector for a word is the sum of the two tables,
W[i] + W_tilde[i], returned as vectors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
The |
required |
vocab_size
|
int
|
The vocabulary size |
required |
d
|
int
|
The embedding dimension. |
8
|
lr
|
float
|
The gradient-descent step size. |
0.05
|
epochs
|
int
|
The number of full-batch passes. |
200
|
seed
|
int
|
Seeds the single generator driving initialisation. |
0
|
x_max
|
float
|
The weighting cap passed to :func: |
100.0
|
alpha
|
float
|
The weighting exponent passed to :func: |
0.75
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/embeddings/word2vec_and_glove.py
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 | |
nearest_neighbor ¶
nearest_neighbor(embeddings: ndarray, query: int, k: int = 1) -> list[int]
Rank the k nearest words to a query by cosine similarity.
Scores every row of embeddings against embeddings[query] with the
head topic's :func:~dlhub.embeddings.word_embeddings.cosine_similarity --
this topic produces vectors, it does not re-derive their geometry -- then
returns the top k indices, the query excluded, ties broken by ascending
index so the ranking is deterministic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ndarray
|
A |
required |
query
|
int
|
The row index of the query word. |
required |
k
|
int
|
How many neighbours to return. |
1
|
Returns:
| Type | Description |
|---|---|
list[int]
|
The |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/embeddings/word2vec_and_glove.py
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 | |
main ¶
main() -> None
Train SGNS on the fixture and print the pets family's recovered neighbours.
Source code in src/dlhub/embeddings/word2vec_and_glove.py
927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 | |
Gender bias as a direction in the embedding space: the gender axis recovered as
the top principal component of the definitional-pair differences, hard debiasing
via neutralize and equalize, WEAT's association score with its exact
enumerated permutation p-value, and the neutral-neighbour map that shows the
indirect bias surviving the projection.
dlhub.embeddings.debiasing_word_embeddings ¶
Debiasing Word Embeddings -- Bias Geometry, Hard Debiasing, and WEAT¶
A from-scratch, teaching reference for the geometry of bias in a given static embedding: how gender bias lives in a direction of the vector space, how Bolukbasi et al.'s hard debiasing removes it (identify the subspace, neutralize, equalize), and how WEAT (Caliskan et al.) measures the association that remains -- used here to quantify bias before and after debiasing.
The vectors are treated as given -- a small, hand-planted toy matrix (see
:func:make_fixture) whose every answer is exact and checkable by eye. What a
word vector is belongs to the sibling :mod:dlhub.embeddings.word_embeddings;
how vectors are learned belongs to
:mod:dlhub.embeddings.word2vec_and_glove. Cosine similarity and nearest-
neighbor ranking are imported from the head rather than restated: this module
studies only the bias geometry on top of them.
The module's thesis is cautionary. After :func:hard_debias, every direct
projection on the gender direction is zero and WEAT's effect size d is
exactly 0 -- the metrics say "fixed". Yet the neutral-neighbour map
(:func:neutral_neighbors) is identical before and after: the words still
group by gender through the indirect cluster axis the debiasing never touched.
That is Gonen & Goldberg's "lipstick on a pig" critique, demonstrated in a
four-dimensional space small enough to read by eye: geometric debiasing is
superficial.
Layout is (V, d) -- one word per row, so a word's vector is E[i]. This
is the head's E transposed (E == head_E.T), the convention the dossier
settles because projections, WEAT, and neighbour structure are transpose-free
on rows; where the head's column-convention helpers are reused, the row matrix
is passed transposed (see :func:neutral_neighbors). Every row is
unit-norm -- :func:make_fixture L2-normalizes, which :func:equalize
requires.
References
- Bolukbasi, T., Chang, K.-W., Zou, J., Saligrama, V., & Kalai, A. (2016). Man is to Computer Programmer as Woman is to Homemaker? Debiasing Word Embeddings. NeurIPS. https://arxiv.org/abs/1607.06520
- Caliskan, A., Bryson, J. J., & Narayanan, A. (2017). Semantics derived automatically from language corpora contain human-like biases. Science, 356(6334), 183-186. (WEAT.)
- Gonen, H., & Goldberg, Y. (2019). Lipstick on a Pig: Debiasing Methods Cover up Systematic Gender Biases. NAACL. https://arxiv.org/abs/1903.03862
- Ng, A. (2018). Deep Learning Specialization, Course 5 (Sequence Models), Week 2. https://www.coursera.org/specializations/deep-learning
License
MIT
Notes
- Deterministic male-positive sign. The SVD fixes the gender direction only
up to sign, so :func:
gender_directionflipsgwhen needed to makeg . (e_he - e_she) > 0for the first definitional pair -- the same determinism-by-convention the head'spca_project_2duses. Without it, two runs could report mirrored projections and a WEAT statistic with a flipped sign. - Stacked-deviation SVD = Bolukbasi's covariance PCA. The paper's Step 1
takes the SVD of the covariance
C = sum_i sum_w (w - mu_i)^T (w - mu_i) / |D_i|over the defining sets; this module instead stacks the deviationsw - mu_ithemselves and takes their SVD. The right singular vectors of the stacked deviations are the eigenvectors of its Gram matrix, so the two forms give the same principal directions, and with definitional pairs the1/|D_i|weight is the uniform1/2, so they coincide exactly. The stacked form is the one shown because it displays the data the SVD consumes. - A single direction (k = 1). Bolukbasi generalizes to a k-dimensional
subspace
B(they usek = 10on real corpora), with the projectionv_B = sum_j (v . b_j) b_j. The fixture's deviation matrix is exactly rank-1 -- singular values[sqrt(6), 0, 0, 0]-- so one direction is the whole subspace here, and every function takes a single unit vectorg. - Normalize first. Equalize's
sqrt(1 - ||nu||^2)is real only when the set members are unit vectors (then||nu|| <= 1,nubeing the neutral part of their mean). :func:make_fixtureL2-normalizes every row, and :func:equalizeraises rather than return ananwhen||nu|| > 1. - Neutralize zero-residual guard. A word that lies entirely along
g(like the fixture's definitionalhe) has no neutral part: the residualw - (w.g) gis the zero vector and renormalizing would divide by zero. :func:neutralizereturns the zero vector instead, which the head's cosine zero-guard then keeps out of every ranking. :func:hard_debiasnever calls it on definitional words -- those definegand are left untouched, as in Bolukbasi. - Population standard deviation in WEAT's effect size.
ddivides by the population std (ddof = 0) of the pooled association scores overX u Y; under the sample std (ddof = 1) the fixture's validated answer would be 1.8102397229 instead of 1.9830182615. - Exact permutation p. :func:
weat_pvalueenumerates everyC(|X u Y|, |X|)equal-size partition -- 20 for the fixture's 3-vs-3 targets -- and reports the fraction whose statistic is>=the observed, so the observed partition always counts itself andp >= 1/C. Exact enumeration is feasible only because the toy sets are tiny; Caliskan et al. sample 10,000 random permutations for real-size sets. - The d zero-spread guard. When every pooled score is equal the std is
zero and the effect size is
0/0; :func:weat_effect_sizereturns0.0by definition rather than anan. That is exactly the post-debias case -- every neutralized word scores0against the pure-gender attributes -- and the dossier pinsd_after = 0to this guard.
make_fixture ¶
make_fixture() -> dict
Build the planted bias fixture every test and document shares.
The matrix is hand-written, not trained: V = 14 words by d = 4
coordinates, committed as an integer literal and L2-normalized row-wise so
every row is unit-norm (normalize-first, as Bolukbasi requires). The
coordinates are 0 = gender (the bias axis), 1 = occupation cluster
(the indirect-bias axis, correlated with gender -- this is what survives
neutralize), 2-3 = within-cluster content (tie-free neighbours).
The six definitional words are pure-gender archetypes and coincide within
gender (he = man = boy = +e0, she = woman = girl = -e0): their only
role is to define g and to serve as WEAT's gender attributes, and
nothing computes neighbours among them, so the coincidence keeps every
known answer exact without affecting any result.
The vocabulary is stored sorted, which fixes the row order of E and the
word_to_ix mapping.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | |
gender_direction ¶
gender_direction(E: ndarray, definitional_pairs: Sequence[tuple[int, int]]) -> np.ndarray
Identify the unit gender direction g from definitional pairs.
For each (male, female) pair, take the pair mean mu and the two
deviations w - mu; stack all deviations and take the top principal
component (via SVD) as g (Bolukbasi §5.1, in the stacked-deviation
form -- see the module Notes). The SVD fixes a direction only up to sign,
so the sign is pinned deterministically male-positive: flip g when
g . (e_male - e_female) < 0 for the first pair.
A single direction (k = 1) is returned; the method generalizes to a
k-dimensional subspace B by taking the top k right singular
vectors instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
E
|
ndarray
|
The embedding matrix, shape |
required |
definitional_pairs
|
Sequence[tuple[int, int]]
|
Row indices |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The unit gender direction |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | |
neutralize ¶
neutralize(w: ndarray, g: ndarray) -> np.ndarray
Remove a gender-neutral word's g component and renormalize.
The projection out of the gender direction, spelled term by term::
w' = (w - (w . g) g) / || w - (w . g) g ||
After this, w' . g = 0 (the direct bias is gone) and w' is back on
the unit sphere, so cosines and distances stay comparable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
w
|
ndarray
|
The word's vector, shape |
required |
g
|
ndarray
|
The unit gender direction, shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The neutralized unit vector, shape |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
equalize ¶
equalize(vectors: ndarray, g: ndarray) -> np.ndarray
Equalize one equality set: make its members symmetric about g.
For a gendered pair to keep distinct but unbiased ({grandfather,
grandmother}), Bolukbasi's Step 2a (arXiv:1607.06520) replaces every
member's neutral part with the set's shared neutral part nu and gives
the members equal-and-opposite g components::
mu = mean(E) # the set mean
mu_B = (mu . g) g # its gender component
nu = mu - mu_B # the shared neutral part
w_B = (w . g) g # each member's gender component
w' = nu + sqrt(1 - ||nu||^2) * (w_B - mu_B) / || w_B - mu_B ||
Every output is unit-norm, the members' g projections are +/- the
shared sqrt(1 - ||nu||^2), and all members are equidistant from any
neutralized word -- which is what makes the pair unbiased without merging
the two words into one vector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vectors
|
ndarray
|
The equality set's member vectors, shape |
required |
g
|
ndarray
|
The unit gender direction, shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The equalized vectors, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
hard_debias ¶
hard_debias(E: ndarray, g: ndarray, neutral_words: Sequence[int], equality_sets: Sequence[Sequence[int]]) -> np.ndarray
Bolukbasi's hard debiasing: neutralize the neutral words, equalize the sets.
Copies E and applies :func:neutralize to every gender-neutral word
(the occupations -- their direct bias on g goes to zero) and
:func:equalize to every equality set (the gendered pairs that must stay
distinct but symmetric). The definitional words that defined g are left
untouched, as in Bolukbasi: neutralizing he = +e0 would annihilate it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
E
|
ndarray
|
The embedding matrix, shape |
required |
g
|
ndarray
|
The unit gender direction, shape |
required |
neutral_words
|
Sequence[int]
|
Row indices of the gender-neutral words to neutralize. |
required |
equality_sets
|
Sequence[Sequence[int]]
|
Row indices of each equality set to equalize, disjoint from
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The debiased matrix, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a word appears in both |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | |
weat_association ¶
weat_association(w: ndarray, A: ndarray, B: ndarray) -> float
WEAT's association score s(w, A, B) for one word.
How much more similar w is to attribute set A than to B::
s(w, A, B) = mean_{a in A} cos(w, a) - mean_{b in B} cos(w, b)
Positive means "leans A", negative "leans B", zero "unassociated". Cosines
come from the head's :func:~dlhub.embeddings.word_embeddings.cosine_similarity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
w
|
ndarray
|
The word's vector, shape |
required |
A
|
ndarray
|
The first attribute set's vectors, shape |
required |
B
|
ndarray
|
The second attribute set's vectors, shape |
required |
Returns:
| Type | Description |
|---|---|
float
|
The association score |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 | |
weat_statistic ¶
weat_statistic(X: ndarray, Y: ndarray, A: ndarray, B: ndarray) -> float
WEAT's test statistic: the total association gap between the target sets.
::
statistic = sum_{x in X} s(x, A, B) - sum_{y in Y} s(y, A, B)
with s from :func:weat_association. Large positive means the X
targets lean toward A more than the Y targets do.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
The first target set's vectors, shape |
required |
Y
|
ndarray
|
The second target set's vectors, shape |
required |
A
|
ndarray
|
The first attribute set's vectors, shape |
required |
B
|
ndarray
|
The second attribute set's vectors, shape |
required |
Returns:
| Type | Description |
|---|---|
float
|
The test statistic. |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 | |
weat_effect_size ¶
weat_effect_size(X: ndarray, Y: ndarray, A: ndarray, B: ndarray) -> float
WEAT's effect size d: the association gap in pooled standard deviations.
::
d = (mean_{x in X} s(x) - mean_{y in Y} s(y)) / std_{w in X u Y} s(w)
with s from :func:eat_association and the population std
(ddof = 0; see the module Notes). d is scale-free -- it says how
far apart the two target groups sit relative to their own spread -- and it
is the dossier's hard known answer, before and after debiasing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
The first target set's vectors, shape |
required |
Y
|
ndarray
|
The second target set's vectors, shape |
required |
A
|
ndarray
|
The first attribute set's vectors, shape |
required |
B
|
ndarray
|
The second attribute set's vectors, shape |
required |
Returns:
| Type | Description |
|---|---|
float
|
The effect size |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 | |
weat_pvalue ¶
weat_pvalue(X: ndarray, Y: ndarray, A: ndarray, B: ndarray) -> float
WEAT's one-sided p-value, by exact enumeration of every permutation.
Pools the targets X u Y and enumerates all C(|X u Y|, |X|) ways to
choose a new X' of the same size (the complement is Y'), computing
the statistic of each partition. The p-value is the fraction of partitions
whose statistic is >= the observed one -- the probability that a random
equal-size split associates the targets with the attributes at least as
strongly as the true split does.
Exact enumeration replaces Caliskan et al.'s 10,000 sampled permutations;
it is affordable here because the toy sets give only C(6, 3) = 20
partitions (see the module Notes).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
The first target set's vectors, shape |
required |
Y
|
ndarray
|
The second target set's vectors, shape |
required |
A
|
ndarray
|
The first attribute set's vectors, shape |
required |
B
|
ndarray
|
The second attribute set's vectors, shape |
required |
Returns:
| Type | Description |
|---|---|
float
|
The permutation p-value in |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 | |
neutral_neighbors ¶
neutral_neighbors(E: ndarray, neutral_words: Sequence[int]) -> dict[int, int]
Map every neutral word to its nearest neighbour within the neutral set.
The tested half of the surviving-bias metric: :func:hard_debias zeroes
every direct projection, but if this map is identical before and after,
the words still group by gender through the indirect cluster axis -- the
Gonen & Goldberg climax. Ranking reuses the head's
:func:~dlhub.embeddings.word_embeddings.nearest_neighbors (cosine, self
excluded, ties by ascending index), on the neutral sub-matrix alone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
E
|
ndarray
|
The embedding matrix, shape |
required |
neutral_words
|
Sequence[int]
|
Row indices of the gender-neutral words, at least two. |
required |
Returns:
| Type | Description |
|---|---|
dict[int, int]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If fewer than two neutral words are given (there would be no neighbour to find). |
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 | |
main ¶
main() -> None
Run the debiasing pipeline end to end and print its known answers.
Source code in src/dlhub/embeddings/debiasing_word_embeddings.py
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 | |