Tokens, the vocabulary, four representations with their vectors, similarity measures, and sparse against dense on the same data
This lesson builds the bridge from strings to vectors, so that distances, projections and clusters apply to text.
Bag-of-words, TF-IDF (term frequency × inverse document frequency)
Word and sentence embeddings
from sklearn.feature_extraction.text import CountVectorizer
docs = ["O gato comeu o peixe.", # the cat ate the fish
"O cachorro comeu a ração do gato.", # the dog ate the cat's food
"Gato dorme, gato sonha."] # cat sleeps, cat dreams
analyzer = CountVectorizer().build_analyzer() # scikit-learn's default tokenizer
for d in docs:
print(analyzer(d))
['gato', 'comeu', 'peixe']
['cachorro', 'comeu', 'ração', 'do', 'gato']
['gato', 'dorme', 'gato', 'sonha']
"Gato" and "gato" are the same token (lowercase=True)
the default token_pattern keeps runs of two or more letters or digits
the articles o, a are gone; do is kept. Accents stay ("ração")
s = "Os gatos não comeram a ração do cachorro, mas o gato comeu!" (the cats did not eat the dog's food, but the cat ate!)
| tokenizer | output |
|---|---|
s.split() | Os · gatos · não · comeram · a · ração · do · cachorro, · mas · o · gato · comeu! |
re.findall(r"\w+", s.lower()) | os · gatos · não · comeram · a · ração · do · cachorro · mas · o · gato · comeu |
scikit-learn's analyzer(s) | os · gatos · não · comeram · ração · do · cachorro · mas · gato · comeu |
The example sentence: "Os gatos não comeram a ração do cachorro, mas o gato comeu!" (the cats did not eat the dog's food, but the cat ate; 12 tokens)
very frequent words with little content (o, a, do, de), removed before counting
Os gatos não comeram a ração do cachorro, mas o gato comeu → 8 tokens
"não" stays, because it is not on this list; scikit-learn's English list removes "not".
runs of \(n\) neighboring tokens; bigrams are the pairs
os gatos · gatos não · não comeram · comeram ração · … · gato comeu
"não comeram" becomes a single feature: some word order comes back.
On the three sentences of the previous slides: 8 columns → 7 with a tiny stop list → 17 with bigrams.
Merging the variants of a word into one token. On the same sentence:
| what it does | examples | tool | |
|---|---|---|---|
| stemming | chops suffixes by rules; the result need not be a word | gatos, gato → gat comeram, comeu → com sábados → sáb | NLTK RSLPStemmer |
| lemmatization | the dictionary form, using a vocabulary and the part of speech | gatos → gato comeram → comer sábados → sábado comeu → comeu (missed) | spaCy |
| step | output |
|---|---|
| stop words out | gatos · não · comeram · ração · cachorro · mas · gato · comeu |
| stems (RSLP) | os · gat · não · com · a · raç · do · cachorr · mas · o · gat · com |
| lemmas (spaCy) | o · gato · não · comer · o · ração · de o · cachorro · mas · o · gato · comeu |
| bigrams | os gatos · gatos não · não comeram · comeram ração · ração do · do cachorro · cachorro mas · mas gato · gato comeu |
Frequent words stay whole, rare words split into frequent pieces: no word is ever unknown.
| tokenizer | output |
|---|---|
WordPiece, all-MiniLM-L6-v230,522 English pieces | tokenization → token ##ization unbelievably → un ##bel ##ie ##va ##bly s → os ga ##tos na ##o come ##ram a ra ##cao do ca ##cho ##rro , mas o ga ##to come ##u ! |
| SentencePiece, multilingual model 250,002 pieces, 50+ languages | s → ▁Os ▁gato s ▁não ▁come ram ▁a ▁ra ção ▁do ▁cachorro , ▁mas ▁o ▁gato ▁come u ! desparafusaram (they unscrewed) → ▁des para fus aram |
##: glued to the piece before; ▁: start of a word. Each piece has its own vector, so a word never seen whole still gets one.
| bag-of-words, TF-IDF | word embeddings (GloVe) | sentence embeddings | |
|---|---|---|---|
| lowercase, punctuation | your choice (scikit-learn's default lowercases and drops punctuation) | GloVe's vocabulary is all lowercase | the model's tokenizer decides |
| stop words | optional: removes columns | optional, before averaging | no: "não" changes the meaning |
| stemming, lemmatization | optional: merges columns | no: one vector per form | no |
| n-grams | optional: columns for pairs | no | no: the model reads the sentence in order |
| subwords | no | no: a word outside the vocabulary gets no vector | yes, the model's tokenizer |
The 20 Newsgroups collection: 11,314 posts from Usenet groups of the early 1990s. Read them one token at a time and count the distinct words seen so far.
Predict. Of the 101,631 distinct words, what share occurs exactly once in the whole corpus: 5%, 25%, or more than half?
Predict. Switch from unigrams to uni + bi, what ngram_range=(1, 2) does: does the vocabulary grow a little, double, or more?
Once the text is cut into tokens, four pages turn the tokens into vectors, from the simplest to the most powerful. Point at a family to follow it.
| page | a document becomes | built from | what it cannot do |
|---|---|---|---|
| U4.1 Bag-of-words | its word counts, one column per word | counting, no model | see word order, synonyms, or words not seen in fit |
| U4.2 TF-IDF | counts weighted by how rare each word is | counting over the corpus | the same: it only reweights the columns |
| U4.3 Word embeddings | a dense vector per word (averaged for a document) | co-occurrence in a large corpus | tell apart the senses of one word |
| U4.4 Sentence embeddings | one dense vector per sentence or short document | a pretrained transformer | be cheap, or explain its dimensions |
cachorro comeu do dorme gato peixe ração sonha
doc 1 0 1 0 0 1 1 0 0
doc 2 1 1 1 0 1 0 1 0
doc 3 0 0 0 1 2 0 0 1
then each row is divided by its length. Predict. In document 1 (gato, comeu, peixe: one each), which word weighs most?
cachorro comeu do dorme gato peixe ração sonha
idf 1.6931 1.2877 1.6931 1.6931 1.0 1.6931 1.6931 1.6931
cachorro comeu do dorme gato peixe ração sonha
doc 1 0.000 0.548 0.000 0.000 0.425 0.72 0.000 0.000
doc 2 0.505 0.384 0.505 0.000 0.298 0.00 0.505 0.000
doc 3 0.000 0.000 0.000 0.543 0.641 0.00 0.000 0.543
"peixe", only in document 1, weighs 0.720; "gato", in all three, 0.425. Same zeros, each row of length 1. Spärck Jones (1972); Salton & Buckley (1988).
compares the directions of the two vectors, ignores their lengths. 1 = same words in the same proportions; 0 = nothing in common.
the straight-line distance between the two points (the one k-means uses). 0 = identical; grows with every difference, including length.
multiply coordinate by coordinate and add. Large when both are large in the same columns: grows with document length.
forget counts, compare the sets of tokens: shared tokens over all distinct tokens.
Columns: cachorro, comeu, do, dorme, gato, peixe, ração, sonha. The query is A.
| A = "O gato comeu o peixe." | [0 1 0 0 1 1 0 0] |
| B = "Gato dorme, gato sonha." (another subject) | [0 0 0 1 2 0 0 1] |
| A3 = A written three times (same subject, longer) | [0 3 0 0 3 3 0 0] |
| pair | cosine | Euclidean | dot | Jaccard |
|---|---|---|---|---|
| A and B | 0.471 | 2.236 | 2 | 0.200 |
| A and A3 | 1.000 | 3.464 | 9 | 1.000 |
| B and A3 | 0.471 | 4.583 | 6 | 0.200 |
normalize_embeddings=True.fit has no column."You shall know a word by the company it keeps" (Firth, 1957)
word2vec (Mikolov et al., 2013) predicts the words around each word; GloVe factorizes co-occurrence counts.
Synonyms end up close. Limit: one vector per word, whatever its sense.
Transformers (BERT) compute each token from the whole sentence: manga next to "camisa" ≠ next to "madura".
Sentence-BERT: one vector per sentence; the cosine tracks closeness of meaning.
GloVe, trained on 6 billion tokens of English text: 400,000 words, 50 numbers each.
glove = api.load('glove-wiki-gigaword-50')
print('cat =', glove['cat'][:8].round(3), '...')
print(glove.most_similar('cat', topn=5))
cat = [ 0.453 -0.501 -0.537 -0.016 0.222 0.546 -0.673 -0.689] ... (50 numbers, none of them zero)
nearest to cat: dog 0.92, rabbit 0.85, monkey 0.80, rat 0.79, cats 0.79
cosine car / automobile: 0.70
cosine car / banana: 0.22
No single number means anything; the neighbors do. "car" and "automobile" never shared a bag-of-words column; here they have cosine 0.70.
paraphrase-multilingual-MiniLM-L12-v2, with normalize_embeddings=True: 384 numbers per text, 50+ languages.
(3, 384)
o carro é rápido = [ 0.016 0.033 -0.08 0.037 -0.014 -0.002 0.039 -0.028] ... (384 numbers)
vs 1: cosine 0.97 · dot 0.97 · Euclidean 0.25
vs 2: cosine 0.23 · dot 0.23 · Euclidean 1.24
At length 1, dot product = cosine, and the Euclidean distance tells the same story in reverse.
"the cat sleeps on the couch": bag-of-words over the newsgroups' vocabulary, the average of its GloVe vectors, its sentence embedding.
| length | non-zero | values | |
|---|---|---|---|
| bag-of-words | 101,631 | 5 | cat (column 26,019) = 1, couch = 1, on = 1, sleeps = 1, the = 2 · all others 0 |
| GloVe average | 50 | 50 | [ 0.271 0.037 -0.335 -0.147 0.541 0.055] ... |
| sentence embedding | 384 | 384 | [ 0.39 -0.348 -0.135 0.145 0.115 0.278] ... |
| sparse (bag-of-words, TF-IDF) | dense (word and sentence embeddings) | |
|---|---|---|
| length | one column per vocabulary word: 101,631 | fixed by the model: 50 (GloVe), 384 (sentence) |
| non-zero values | only the words of the text: 5 | all of them |
| one coordinate means | column 26,019 = "how many times cat appears" | nothing on its own |
| stored as | the non-zero cells (index, value) | a plain array of floats |
| no word in common | no shared column: cosine 0 | can still be close |
Predict. On the paraphrase "the car is fast" / "this automobile goes quickly", which of the four gives cosine 0? Then swap the word order and tick B three times.
TF-IDF fitted on the sentences of the table; a small multilingual sentence-transformers model (384 numbers per text).
| A | B | TF-IDF | embedding |
|---|---|---|---|
| o carro é rápido | o automóvel é veloz | 0.00 | 0.97 |
| comprei um celular novo | adquiri um smartphone novo | 0.43 | 0.91 |
| o gato dorme no sofá | the cat sleeps on the couch | 0.00 | 0.98 |
| a manga da camisa rasgou | o punho da blusa descosturou | 0.21 | 0.87 |
| a manga da camisa rasgou | comi uma manga madura | 0.21 | 0.35 |
| o filme é muito bom | o filme não é bom | 0.60 | 0.46 |
| o cachorro mordeu o homem | o homem mordeu o cachorro | 1.00 | 0.98 |
| a taxa de juros subiu | o time venceu o jogo | 0.00 | 0.14 |
Two help-desk entries that differ only in an error code (0x80070005 vs 0x80004005), and a query for each code.
Four newsgroups, a logistic regression used as a black box; only the representation changes. Accuracy on 1,364 later posts (guessing one class: about 25%).
| labeled posts per class | TF-IDF + clf | embedding + clf |
|---|---|---|
| 5 | 0.557 | 0.819 |
| 10 | 0.601 | 0.851 |
| 25 | 0.725 | 0.886 |
| 100 | 0.850 | 0.897 |
| all | 0.904 | 0.901 |
has seen only a few hundred distinct words; a test post that uses none of them is a zero vector. With all the labels, it learns each class's vocabulary from the data.
arrives already knowing that "sedan" is near "car", from the text it was trained on.
| sparse (bag-of-words, TF-IDF) | dense (sentence embeddings) | |
|---|---|---|
| a document vector | \(\lvert V \rvert\) columns, 99.9% zeros | 384–1,024 numbers |
| synonyms, paraphrases | orthogonal: 0.00 | nearby: 0.97 |
words never seen in fit | dropped | handled (subword pieces) |
| rare exact tokens | high idf, strong signal | blurred into the meaning |
| other languages | separate vocabularies | shared space (multilingual model) |
| 5 labels per class / all | 55.7% / 90.4% | 81.9% / 90.1% |
| interpretability | each column is a word | columns are abstract |
| cost | fractions of a second, no model | a pretrained model; a GPU helps |
!pip install sentence-transformers first.Each of the four sub-pages (U4.1 to U4.4) has its own simulator and lab.