Phase 05 - Lesson 02
Bag of Words, TF-IDF e Representacao de Texto
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Conte primeiro, pense depois. O TF-IDF ainda supera embeddings em tarefas bem definidas em 2026.
Tipo: Build Linguagens: Python Pre-requisitos: Fase 5 · 01 (Processamento de Texto), Fase 2 · 02 (Regressao Linear do Zero) Tempo: ~75 minutos
O Problema
O modelo precisa de numeros. Voce tem strings.
Todo pipeline de NLP precisa responder a mesma pergunta. Como transformar um fluxo de tokens de comprimento variavel em um vetor de tamanho fixo que um classificador consiga consumir. A primeira resposta a que a area chegou foi a mais boba que funciona. Conte as palavras. Faca um vetor.
Esse vetor sustentou mais NLP em producao do que qualquer modelo de embedding. Filtros de spam, classificadores de topico, deteccao de anomalias em logs, ranqueamento de busca (antes do BM25), a primeira onda de analise de sentimento, a primeira decada de benchmarks academicos de NLP. Praticantes de 2026 ainda recorrem a ele primeiro em tarefas de classificacao restritas. Ele e rapido, interpretavel e muitas vezes indistinguivel de um modelo de embedding de 400M de parametros em tarefas onde a presenca da palavra e o que importa.
Esta licao constroi o bag of words e depois o TF-IDF, do zero. Em seguida mostra o scikit-learn fazendo o mesmo em tres linhas. Por fim, nomeia o modo de falha que faz voce recorrer aos embeddings.
O Conceito
Bag of Words (BoW) descarta a ordem. Para cada documento, conte quantas vezes cada palavra do vocabulario aparece. O comprimento do vetor e o tamanho do vocabulario. A posicao i e a contagem da palavra i.
TF-IDF reponderada o BoW. Uma palavra que aparece em todos os documentos e pouco informativa, entao reduza seu peso. Uma palavra rara em todo o corpus mas frequente em um unico documento e sinal, entao aumente seu peso.
TF-IDF(w, d) = TF(w, d) * IDF(w)
= count(w in d) / |d| * log(N / df(w))
Onde TF e a frequencia do termo no documento, df e a frequencia de documento (quantos docs contem a palavra), N e o total de documentos. O log mantem o peso limitado para palavras ubiquas.
Propriedade chave: ambos produzem vetores esparsos com eixos interpretaveis. Voce pode olhar os pesos de um classificador treinado e ler quais palavras empurram um documento em direcao a cada classe. Voce nao consegue fazer isso com um embedding BERT de 768 dimensoes.
Construa
Passo 1: construir o vocabulario
def build_vocab(docs):
vocab = {}
for doc in docs:
for token in doc:
if token not in vocab:
vocab[token] = len(vocab)
return vocab
Entrada: lista de documentos tokenizados (qualquer tokenizador a nivel de palavra serve; o code/main.py desta licao usa uma variante simplificada em minusculas). Saida: dict {word: index}. A ordem de insercao estavel significa que o indice de palavra 0 e a primeira palavra vista no primeiro documento. A convencao varia; o scikit-learn ordena alfabeticamente.
Passo 2: bag of words
def bag_of_words(docs, vocab):
matrix = [[0] * len(vocab) for _ in docs]
for i, doc in enumerate(docs):
for token in doc:
if token in vocab:
matrix[i][vocab[token]] += 1
return matrix
>>> docs = [["cat", "sat", "on", "mat"], ["cat", "cat", "ran"]]
>>> vocab = build_vocab(docs)
>>> bag_of_words(docs, vocab)
[[1, 1, 1, 1, 0], [2, 0, 0, 0, 1]]
As linhas sao documentos. As colunas sao indices do vocabulario. A entrada [i][j] e "quantas vezes a palavra j aparece no documento i". O Doc 1 tem cat duas vezes porque foi o caso. O Doc 0 tem ran zero vezes porque nao foi o caso.
Passo 3: frequencia do termo e frequencia de documento
import math
def term_frequency(doc_bow, doc_length):
return [c / doc_length if doc_length else 0 for c in doc_bow]
def document_frequency(bow_matrix):
df = [0] * len(bow_matrix[0])
for row in bow_matrix:
for j, count in enumerate(row):
if count > 0:
df[j] += 1
return df
def inverse_document_frequency(df, n_docs):
return [math.log((n_docs + 1) / (d + 1)) + 1 for d in df]
Dois truques de suavizacao que vale nomear. O (n+1)/(d+1) evita log(x/0). O +1 ao final garante que uma palavra presente em todos os documentos ainda tenha IDF 1 (nao 0), correspondendo ao padrao do scikit-learn. Outras implementacoes usam o log(N/df) puro. Ambos funcionam; a versao suavizada e mais amigavel.
Passo 4: TF-IDF
def tfidf(bow_matrix):
n_docs = len(bow_matrix)
df = document_frequency(bow_matrix)
idf = inverse_document_frequency(df, n_docs)
out = []
for row in bow_matrix:
length = sum(row)
tf = term_frequency(row, length)
out.append([tf_j * idf_j for tf_j, idf_j in zip(tf, idf)])
return out
>>> docs = [
... ["the", "cat", "sat"],
... ["the", "dog", "sat"],
... ["the", "cat", "ran"],
... ]
>>> vocab = build_vocab(docs)
>>> bow = bag_of_words(docs, vocab)
>>> tfidf(bow)
Tres documentos, cinco palavras de vocabulario (the, cat, sat, dog, ran). the aparece nos tres, entao seu IDF e baixo. dog aparece em um, entao seu IDF e alto. Os vetores sao esparsos (a maioria das entradas e pequena) e as palavras discriminativas se destacam.
Passo 5: normalizar as linhas em L2
def l2_normalize(matrix):
out = []
for row in matrix:
norm = math.sqrt(sum(x * x for x in row))
out.append([x / norm if norm else 0 for x in row])
return out
Sem normalizacao, um documento mais longo gera um vetor maior e domina os scores de similaridade. A normalizacao L2 coloca todos os documentos na hiperesfera unitaria. A similaridade de cosseno entre as linhas passa a ser apenas um produto escalar.
Use
O scikit-learn entrega a versao de producao.
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
docs = ["the cat sat on the mat", "the dog sat on the mat", "the cat ran"]
bow_vectorizer = CountVectorizer()
bow = bow_vectorizer.fit_transform(docs)
print(bow_vectorizer.get_feature_names_out())
print(bow.toarray())
tfidf_vectorizer = TfidfVectorizer()
tfidf = tfidf_vectorizer.fit_transform(docs)
print(tfidf.toarray().round(3))
O CountVectorizer faz tokenizacao, vocabulario e BoW em uma unica chamada. O TfidfVectorizer adiciona a ponderacao IDF e a normalizacao L2. Ambos retornam matrizes esparsas. Para 100k documentos, a versao densa nao cabe na memoria; mantenha tudo esparso ate que o classificador exija algo denso.
Parametros que mudam tudo:
| Arg | Efeito |
|---|---|
ngram_range=(1, 2) |
Inclui bigramas. Geralmente melhora a classificacao. |
min_df=2 |
Descarta palavras presentes em menos de 2 docs. Reduz o vocabulario em dados ruidosos. |
max_df=0.95 |
Descarta palavras presentes em mais de 95% dos docs. Aproxima a remocao de stopwords sem uma lista fixa. |
stop_words="english" |
Lista de stopwords embutida do scikit-learn. Depende da tarefa — a analise de sentimento nao deve descartar negacoes. |
sublinear_tf=True |
Usa 1 + log(tf) em vez do tf puro. Ajuda quando um termo se repete muitas vezes em um doc. |
Quando o TF-IDF ainda vence (em 2026)
- Deteccao de spam, rotulagem de topicos, sinalizacao de anomalias em logs. A presenca da palavra e o que importa; a nuance semantica nao.
- Regimes de poucos dados (centenas de exemplos rotulados). TF-IDF mais regressao logistica nao tem custo de pre-treinamento.
- Onde quer que a latencia importe. TF-IDF mais um modelo linear responde em microssegundos. Embeddar um documento por um transformer leva de 10 a 100ms.
- Sistemas que precisam explicar suas previsoes. Inspecione os coeficientes do classificador. As principais palavras positivas sao o motivo.
Quando o TF-IDF falha
A falha da cegueira semantica. Considere estes dois documentos:
- "The movie was not good at all."
- "The movie was excellent."
Um e uma resenha negativa. O outro e positiva. A sobreposicao TF-IDF deles e exatamente {the, movie, was}. Um classificador bag-of-words tem que memorizar que a palavra not perto de good inverte o rotulo. Ele consegue aprender isso com dados suficientes, mas nunca com a elegancia de um modelo que entende a sintaxe.
A outra falha: palavras fora do vocabulario na inferencia. Um modelo BoW treinado em resenhas do IMDb nao faz ideia do que fazer com Zoomer-approved se esse token nunca apareceu no treinamento. Embeddings de subpalavra (licao 04) lidam com isso. O TF-IDF nao consegue.
Hibrido: embeddings ponderados por TF-IDF
O padrao pragmatico de 2026 para classificacao com dados medios: usar os pesos TF-IDF como atencao sobre embeddings de palavras.
def tfidf_weighted_embedding(doc, tfidf_scores, embedding_table, dim):
vec = [0.0] * dim
total_weight = 0.0
for token in doc:
if token not in embedding_table or token not in tfidf_scores:
continue
weight = tfidf_scores[token]
emb = embedding_table[token]
for i in range(dim):
vec[i] += weight * emb[i]
total_weight += weight
if total_weight == 0:
return vec
return [v / total_weight for v in vec]
Voce ganha capacidade semantica dos embeddings e enfase em palavras raras do TF-IDF. O classificador treina sobre o vetor agregado. Isso supera qualquer um dos dois isoladamente para classificacao de sentimento, topico e intencao abaixo de cerca de 50k exemplos rotulados.
Entregue
Salve como outputs/prompt-vectorization-picker.md:
---
name: vectorization-picker
description: Given a text-classification task, recommend BoW, TF-IDF, embeddings, or a hybrid.
phase: 5
lesson: 02
---
You recommend a text-vectorization strategy. Given a task description, output:
1. Representation (BoW, TF-IDF, transformer embeddings, or a hybrid). Explain why in one sentence.
2. Specific vectorizer configuration. Name the library. Quote the arguments (`ngram_range`, `min_df`, `max_df`, `sublinear_tf`, `stop_words`).
3. One failure mode to test before shipping.
Refuse to recommend embeddings when the user has under 500 labeled examples unless they show evidence of semantic failure in a TF-IDF baseline. Refuse to remove stopwords for sentiment analysis (negations carry signal). Flag class imbalance as needing more than a vectorizer change.
Example input: "Classifying 30k customer support tickets into 12 categories. Most tickets are 2-3 sentences. English only. Need explainability for audit logs."
Example output:
- Representation: TF-IDF. 30k examples is not small; explainability requirement rules out dense embeddings.
- Config: `TfidfVectorizer(ngram_range=(1, 2), min_df=3, max_df=0.95, sublinear_tf=True, stop_words=None)`. Keep stopwords because category keywords sometimes are stopwords ("not working" vs "working").
- Failure to test: verify `min_df=3` does not drop rare category keywords. Run `get_feature_names_out` filtered by class and eyeball.
Exercicios
- Facil. Implemente
cosine_similarity(doc_vec_a, doc_vec_b)sobre a saida TF-IDF normalizada em L2. Verifique que documentos identicos pontuam 1.0 e documentos com vocabularios disjuntos pontuam 0.0. - Medio. Adicione suporte a
n-gramaobag_of_words. O parametronproduz contagens sobren-gramas. Teste quen=2em["the", "cat", "sat"]produz contagens de bigramas para["the cat", "cat sat"]. - Dificil. Construa o hibrido de embedding ponderado por TF-IDF acima usando vetores GloVe 100d (baixe uma vez, cacheie). Compare a acuracia de classificacao contra o TF-IDF puro e os embeddings puros com mean-pooling no dataset 20 Newsgroups. Reporte quem vence onde.
Termos Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| BoW | Vetor de frequencia de palavras | Contagens das palavras do vocabulario em um documento. Descarta a ordem. |
| TF | Frequencia do termo | Contagem de uma palavra em um documento, opcionalmente normalizada pelo comprimento do documento. |
| DF | Frequencia de documento | Contagem de documentos que contem a palavra ao menos uma vez. |
| IDF | Frequencia inversa de documento | log(N / df) suavizado. Reduz o peso de palavras que aparecem em todo lugar. |
| Vetor esparso | Quase tudo zeros | O vocabulario costuma ter de 10k a 100k palavras; a maioria esta ausente de qualquer documento dado. |
| Similaridade de cosseno | Angulo entre vetores | Produto escalar de vetores normalizados em L2. 1 e identico, 0 e ortogonal. |
Leitura Adicional
- scikit-learn — feature extraction from text — a referencia canonica da API, alem de notas sobre cada parametro.
- Salton, G., & Buckley, C. (1988). Term-weighting approaches in automatic text retrieval — o artigo que tornou o TF-IDF o padrao por uma decada.
- "Why TF-IDF Still Beats Embeddings" — Ashfaque Thonikkadavan (Medium) — a perspectiva de 2026 sobre quando o metodo antigo vence e por que.