Machine Learning · Insper

Text Representation

From strings to vectors

Tokens, the vocabulary, four representations with their vectors, similarity measures, and sparse against dense on the same data

The problem: text is not a table

  • PCA (principal component analysis) rotates the columns of a table; k-means measures distances between its rows.
  • A review, a support ticket, a forum post is a string of any length.
Predict. How would you measure the distance between two customer reviews, one with 12 words and one with 300?

This lesson builds the bridge from strings to vectors, so that distances, projections and clusters apply to text.

Two families of answers

sparse

Bag-of-words, TF-IDF (term frequency × inverse document frequency)

  • one column per word of the vocabulary
  • a document is its word counts, suitably weighted
  • built by counting, in a second, with no model

dense

Word and sentence embeddings

  • a few hundred numbers per word or document
  • produced by a model trained on large amounts of text
  • similar meaning lands close, even with no word in common

The path of this lesson

  1. Text → tokenstokenization, normalization, vocabulary
  2. Tokens → vectorsparse: bag-of-words, TF-IDF · dense: embeddings
  3. Compare vectorscosine, Euclidean distance, dot product, Jaccard
  4. Use and choosesearch and classification: sparse or dense?
Each stage answers one question: what counts as a word? → how does a word become numbers? → how alike are two texts? → which representation should I use?

Cut the text into tokens


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))
  
Predict. What happens to "O", to the comma and to "Gato" in the first and third sentences?

Three silent decisions


['gato', 'comeu', 'peixe']
['cachorro', 'comeu', 'ração', 'do', 'gato']
['gato', 'dorme', 'gato', 'sonha']
  

lowercase

"Gato" and "gato" are the same token (lowercase=True)

punctuation dropped

the default token_pattern keeps runs of two or more letters or digits

one-letter words vanish

the articles o, a are gone; do is kept. Accents stay ("ração")

Each one is a decision you can change.

Three tokenizers, one sentence

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!)

Predict. Splitting on spaces, what happens to "cachorro," and "comeu!"?
tokenizeroutput
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
Whitespace glues punctuation to words and keeps "Os" ≠ "os"; a word regex gives 12 clean words; scikit-learn also drops the one-letter ones: 10 tokens.

Stop words and n-grams

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)

stop words

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".

n-grams

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.

Applies to: bag-of-words and TF-IDF, where these choices define the columns. Dense embeddings keep the stop words: "não" changes the meaning, and the model reads the whole sentence.

Stemming and lemmatization

Merging the variants of a word into one token. On the same sentence:

what it doesexamplestool
stemmingchops suffixes by rules; the result need not be a wordgatos, gato → gat
comeram, comeu → com
sábados → sáb
NLTK RSLPStemmer
lemmatizationthe dictionary form, using a vocabulary and the part of speechgatos → gato
comeram → comer
sábados → sábado
comeu → comeu (missed)
spaCy
Applies to: bag-of-words and TF-IDF, where merging variants merges columns. GloVe has one vector per form ("cat" and "cats"), and sentence embeddings take the text as it is. Neither tool is in scikit-learn.

The same sentence, normalized

Predict. A stemmer cuts "comeu" (ate) to "com". Which other, unrelated words could land in the same column?
stepoutput
stop words outgatos · 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
bigramsos gatos · gatos não · não comeram · comeram ração · ração do · do cachorro · cachorro mas · mas gato · gato comeu
  • "com" (with) and "comida" (food) also stem to "com": four unrelated words share one column.
  • The small lemmatizer left "comeu" as it is: it is a statistical model, and it makes mistakes.
  • "não" survives this stop list; scikit-learn's English list removes "not", "no", "never".

Subwords: the tokenizers of neural models

Frequent words stay whole, rare words split into frequent pieces: no word is ever unknown.

Predict. An English tokenizer meets "não" and "cachorro". What does it make of them?
tokenizeroutput
WordPiece, all-MiniLM-L6-v2
30,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.

Where each stage-1 decision applies

bag-of-words, TF-IDFword embeddings (GloVe)sentence embeddings
lowercase, punctuationyour choice (scikit-learn's default lowercases and drops punctuation)GloVe's vocabulary is all lowercasethe model's tokenizer decides
stop wordsoptional: removes columnsoptional, before averagingno: "não" changes the meaning
stemming, lemmatizationoptional: merges columnsno: one vector per formno
n-gramsoptional: columns for pairsnono: the model reads the sentence in order
subwordsnono: a word outside the vocabulary gets no vectoryes, the model's tokenizer
Normalizing is a decision of the sparse family. In dense embeddings the tokenizer and the vocabulary come with the model: the text goes in as it is.

How big does the vocabulary get?

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. After a few hundred thousand tokens, does the number of distinct words level off, or keep growing?

Heaps' law: the vocabulary never stops growing

Vocabulary size against tokens read on a log-log scale: a straight line through 3,279 words at 11,025 tokens, 18,692 at 100,504 and 63,306 at 1,000,517, reaching 101,631 words at 2,158,589 tokens without flattening

how to read it

  • x axis: tokens read so far; y axis: distinct words seen. Both on a log scale.
  • On a log scale a power law is a straight line; its slope, 0.61, is the exponent.
  • Dotted: if every token were a new word. The curve stays below it, but never lies down.
  • 11 thousand tokens → 3,279 words; 100 thousand → 18,692; 1 million → 63,306.
Ten times more text gives about 4 times more words, with no end: a vocabulary built today meets new words tomorrow, and a sparse representation has no column for them.

Zipf's law: a few words are everywhere

Predict. Of the 101,631 distinct words, what share occurs exactly once in the whole corpus: 5%, 25%, or more than half?

Occurrences of each word against its rank on a log-log scale: the occurs 106,381 times, car 799 and hockey 374; the 10 most frequent words are 21% of all tokens and 55% of the 101,631 words occur exactly once

how to read it

  • Each point is a word. x axis: its rank (1 = most frequent); y axis: how many times it occurs. Both on a log scale.
  • At the top, "the" (106,381 times), "of", "is": the first 10 are 21% of all tokens.
  • In the middle, "car" (rank 271, 799 times) and "hockey" (rank 625, 374 times).
  • Red band: 55% of the words occur exactly once.
Most columns of a bag-of-words describe a single document; the few words at the top are precisely the stop words.

Simulator: tokenize a paragraph

Predict. Switch from unigrams to uni + bi, what ngram_range=(1, 2) does: does the vocabulary grow a little, double, or more?

A map of the approaches

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.

What each one does, and cannot do

pagea document becomesbuilt fromwhat it cannot do
U4.1 Bag-of-wordsits word counts, one column per wordcounting, no modelsee word order, synonyms, or words not seen in fit
U4.2 TF-IDFcounts weighted by how rare each word iscounting over the corpusthe same: it only reweights the columns
U4.3 Word embeddingsa dense vector per word (averaged for a document)co-occurrence in a large corpustell apart the senses of one word
U4.4 Sentence embeddingsone dense vector per sentence or short documenta pretrained transformerbe cheap, or explain its dimensions

Bag-of-words: a document is its counts


       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
  
  • One row per document, one column per word; "gato" is 2 in document 3. Word order is thrown away.
  • 13 of the 24 cells are already zero. The 11,314 posts: \(11{,}314 \times 101{,}631\), 99.90% zeros, stored in a sparse format.
Raw counts are led by function words: "the" is in 9,475 of the 11,314 posts. That is why counts are rarely used unweighted.

TF-IDF: counts weighted by rarity

\[ \text{tf-idf}(t, d) = \text{tf}(t, d) \times \Big(\ln \frac{1 + n}{1 + \text{df}(t)} + 1\Big) \]

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).

Similarity and distance: four measures

cosine similarity

compares the directions of the two vectors, ignores their lengths. 1 = same words in the same proportions; 0 = nothing in common.

Euclidean distance

the straight-line distance between the two points (the one k-means uses). 0 = identical; grows with every difference, including length.

dot product

multiply coordinate by coordinate and add. Large when both are large in the same columns: grows with document length.

Jaccard similarity

forget counts, compare the sets of tokens: shared tokens over all distinct tokens.

A query and two candidates

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]
Predict. By Euclidean distance, which candidate is closer to A: B or A3?
paircosineEuclideandotJaccard
A and B0.4712.23620.200
A and A31.0003.46491.000
B and A30.4714.58360.200

Why the cosine is the default for text

  • Cosine: A3 is identical to A (1.000), B a partial match (0.471); B scores 0.471 against A and A3 alike.
  • Euclidean gets it backwards: A3 is farther from A (3.464) than B (2.236), only because it is longer.
  • Dot product: B against A3 scores 6, three times its 2 against A, for the same content.
  • Jaccard agrees here, but it is blind to how often a word appears.
Documents have every length, and length says little about the subject. Scaled to length 1, the three agree: the dot product is the cosine. TF-IDF rows already have length 1; sentence embeddings use normalize_embeddings=True.

The limits of sparse representations

Predict. "o carro é rápido" and "o automóvel é veloz" (the car is fast / the automobile is quick): what is their TF-IDF cosine?
  • Synonyms score zero: cosine 0.00. Every word is its own axis, orthogonal to the others.
  • Word order is invisible: "the dog bit the man" and "the man bit the dog" have cosine 1.00.
  • One spelling, two meanings: manga (sleeve or mango) makes unrelated sentences look alike, and two sentences with the same meaning look unrelated (0.14 in both cases).
  • New words vanish: a word not seen in fit has no column.
Back to stage 2 · dense family

Dense: know a word by its company

"You shall know a word by the company it keeps" (Firth, 1957)

  • Distributional hypothesis (Harris, 1954): words used in similar contexts have similar meanings.
  • "car" and "automobile" rarely share a sentence, but both appear next to engine, dealer, tires.
  • Each word or text gets a few hundred numbers, learned so that similar contexts give nearby vectors.
No single number means anything on its own; only distances between vectors do.

Word embeddings, sentence embeddings

word embeddings (U4.3)

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.

sentence embeddings (U4.4)

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.

Word embeddings: what a vector looks like

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))
  
Predict. Which words are nearest to "cat"? And what does the first number, 0.453, mean?

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.

Sentence embeddings: one vector per sentence

paraphrase-multilingual-MiniLM-L12-v2, with normalize_embeddings=True: 384 numbers per text, 50+ languages.

Predict. "o carro é rápido" against its paraphrase "o automóvel é veloz" (no shared word) and against "a taxa de juros subiu" (unrelated): which cosines?

(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.

Sparse vs dense: one sentence, three vectors

"the cat sleeps on the couch": bag-of-words over the newsgroups' vocabulary, the average of its GloVe vectors, its sentence embedding.

Predict. How long is each vector, and how many of its numbers are not zero?
lengthnon-zerovalues
bag-of-words101,6315cat (column 26,019) = 1, couch = 1, on = 1, sleeps = 1, the = 2 · all others 0
GloVe average5050[ 0.271 0.037 -0.335 -0.147 0.541 0.055] ...
sentence embedding384384[ 0.39 -0.348 -0.135 0.145 0.115 0.278] ...

What "sparse" and "dense" mean

sparse (bag-of-words, TF-IDF)dense (word and sentence embeddings)
lengthone column per vocabulary word: 101,631fixed by the model: 50 (GloVe), 384 (sentence)
non-zero valuesonly the words of the text: 5all of them
one coordinate meanscolumn 26,019 = "how many times cat appears"nothing on its own
stored asthe non-zero cells (index, value)a plain array of floats
no word in commonno shared column: cosine 0can still be close
Sparse vectors can only match texts that share words; dense vectors can match texts that share meaning.

Simulator: four ways to turn a sentence into a vector

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.

Sparse vs dense: the same pairs, both ways

TF-IDF fitted on the sentences of the table; a small multilingual sentence-transformers model (384 numbers per text).

ABTF-IDFembedding
o carro é rápidoo automóvel é veloz0.000.97
comprei um celular novoadquiri um smartphone novo0.430.91
o gato dorme no sofáthe cat sleeps on the couch0.000.98
a manga da camisa rasgouo punho da blusa descosturou0.210.87
a manga da camisa rasgoucomi uma manga madura0.210.35
o filme é muito bomo filme não é bom0.600.46
o cachorro mordeu o homemo homem mordeu o cachorro1.000.98
a taxa de juros subiuo time venceu o jogo0.000.14

Reading the table, row by row

  • paraphrases: 0.91 against 0.43; TF-IDF only sees "um" and "novo".
  • translation: no token in common, 0.98. A Portuguese query can find English documents.
  • the two "manga" rows: TF-IDF gives both 0.21; the embedding separates sleeve-and-cuff (0.87) from sleeve-and-mango (0.35).
  • negation: lower (0.46), but far from "opposite".
  • word order: 0.98. Embeddings blur who bit whom too.
  • unrelated: 0.14, not 0.
Embedding cosines live on a compressed scale: the ranking carries the information, not the absolute value, and a threshold tuned for one model does not transfer.

Where TF-IDF is stronger: exact rare tokens

Two help-desk entries that differ only in an error code (0x80070005 vs 0x80004005), and a query for each code.

TF-IDF

0.55 vs 0.18
the code is the most distinctive token and decides the match

embedding

0.80 vs 0.77
both are "an error message during installation"
With many similar codes, product names or legal references, exact matching is what you want. Production search often combines both rankings: hybrid search.

Classification: how many labels do you have?

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%).

Predict. With only 5 labeled posts per class, which representation wins? And with all 2,097?
labeled posts per classTF-IDF + clfembedding + clf
50.5570.819
100.6010.851
250.7250.886
1000.8500.897
all0.9040.901

Why: what each one arrives knowing

TF-IDF, 20 training posts

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.

embedding

arrives already knowing that "sedan" is near "car", from the text it was trained on.

What the scores do not show is cost: TF-IDF processes the 2,097 posts in 0.2 s; the embedding model needs about a minute on the same CPU (10 s on a GPU), after a 470 MB download.

Sparse vs dense, side by side

sparse (bag-of-words, TF-IDF)dense (sentence embeddings)
a document vector\(\lvert V \rvert\) columns, 99.9% zeros384–1,024 numbers
synonyms, paraphrasesorthogonal: 0.00nearby: 0.97
words never seen in fitdroppedhandled (subword pieces)
rare exact tokenshigh idf, strong signalblurred into the meaning
other languagesseparate vocabulariesshared space (multilingual model)
5 labels per class / all55.7% / 90.4%81.9% / 90.1%
interpretabilityeach column is a wordcolumns are abstract
costfractions of a second, no modela pretrained model; a GPU helps

Summary: which one, when

  1. Start with TF-IDF: cheap and hard to beat when labels and domain vocabulary are plentiful.
  2. Reach for embeddings when meaning matters more than exact words (search, short texts, paraphrases, several languages) or labels are scarce.
  3. When exact tokens and meaning matter, combine them.
Where this leads: BERTopic (U5) embeds documents, reduces them with UMAP (a dimensionality reduction), groups them with HDBSCAN (a density-based clustering) and describes each group with a TF-IDF computed per cluster.
Lab · about two minutes on a CPU

Sparse vs dense on real posts

  • Four newsgroups (computer graphics, baseball, electronics, Christianity), about 2,100 posts, represented twice: TF-IDF and sentence embeddings.
  • Steps: load the posts · two representations · which one finds posts about the same subject? · semantic vs keyword search · look at the two spaces · the same black-box classifier on both.
  • Download the lab notebook and open it in Jupyter or Colab; in Colab, run !pip install sentence-transformers first.

Each of the four sub-pages (U4.1 to U4.4) has its own simulator and lab.