Aula 5, TF-IDF
Esta aula fecha os fundamentos de NLP refinando o Bag of Words. Em vez de contar palavras de forma crua, o TF-IDF dá mais peso aos termos que distinguem um documento dos demais. Com ele, vamos construir o classificador de perguntas de alunos por tema, o projeto que encerra o módulo.
O Bag of Words trata todas as palavras como iguais, mas elas não são. Em uma coleção de perguntas, palavras como pergunta ou como aparecem em quase todos os documentos e ajudam pouco a distinguir o tema. Já palavras como autovetor ou derivada aparecem em poucos documentos e, por isso mesmo, são pistas fortes do assunto.
O TF-IDF captura essa intuição com um peso que combina duas ideias, o quanto uma palavra é frequente em um documento e o quanto ela é rara na coleção. É uma das técnicas mais usadas e mais eficazes do NLP clássico, e ainda hoje serve de linha de base difícil de bater em muitas tarefas de texto. Vamos fechar o módulo usando o TF-IDF para classificar perguntas de alunos por tema, juntando tudo o que construímos.
Objetivos
Ao final desta aula, você deve ser capaz de:
- Explicar as duas componentes do TF-IDF e por que elas se combinam.
- Calcular pesos TF-IDF a partir de uma coleção de documentos.
- Entender por que o TF-IDF melhora a contagem crua do Bag of Words.
- Construir um classificador de texto por tema usando TF-IDF e similaridade.
Teoria
O TF-IDF dá a cada par de palavra e documento um peso, em vez de uma simples contagem. Esse peso é o produto de dois fatores. O primeiro é a frequência do termo, ou TF, que mede o quanto a palavra aparece naquele documento, sob a ideia de que palavras mais frequentes no texto são mais importantes para ele. O segundo é a frequência inversa nos documentos, ou IDF, que mede o quanto a palavra é rara na coleção, sob a ideia de que palavras que aparecem em todo lugar discriminam pouco.
A ideia de pesar termos pela sua especificidade vem de Karen Spärck Jones, e o esquema de pesos que usamos foi consolidado por Salton e Buckley. Multiplicar TF por IDF equilibra os dois efeitos. Uma palavra ganha peso alto quando é frequente em um documento e, ao mesmo tempo, rara na coleção, que é exatamente o perfil de um bom termo de conteúdo.
flowchart LR
TF["TF: frequência no documento"] --> X["peso = TF × IDF"]
IDF["IDF: raridade na coleção"] --> X
X --> V["Vetores TF-IDF<br/>por documento"]
V --> C[Classificação por tema]
Com os pesos TF-IDF, cada documento vira um vetor, como no Bag of Words, mas agora os valores refletem importância, não só contagem. Sobre esses vetores podemos medir similaridade do cosseno e classificar textos novos, que é o que faremos no projeto.
Explicação Intuitiva
Pense em um detetive procurando o que torna cada depoimento único. Palavras que todos falam, como ontem ou pessoa, não ajudam a diferenciar os relatos. Mas se só uma testemunha menciona um carro vermelho, essa expressão vira uma pista valiosa. O TF-IDF é esse detetive, ele rebaixa o que é comum a todos e destaca o que é característico de cada documento.
É por isso que o TF-IDF costuma separar temas melhor que a contagem pura. Numa coleção de perguntas, a palavra derivada, que aparece só nas de cálculo, recebe peso alto e puxa essas perguntas para perto umas das outras, enquanto palavras espalhadas por todos os temas quase não influenciam. O efeito é uma representação mais limpa e mais fácil de classificar.
Explicação Matemática
Para um termo $t$ em um documento $d$, dentro de uma coleção com $N$ documentos, uma formulação comum do TF-IDF é a seguinte. A frequência do termo é a contagem de $t$ em $d$, que podemos chamar de $\text{tf}(t, d)$. A frequência inversa nos documentos usa $\text{df}(t)$, o número de documentos que contêm $t$, e é definida por
\[\text{idf}(t) = \log \frac{N}{\text{df}(t)}.\]O peso final é o produto das duas partes:
\[\text{tfidf}(t, d) = \text{tf}(t, d) \times \text{idf}(t).\]Quando um termo aparece em todos os documentos, $\text{df}(t) = N$, então $\text{idf}(t) = \log 1 = 0$, e o seu peso zera, pois ele não distingue nada. Quando um termo é raro, a razão $N / \text{df}(t)$ é grande, e o IDF eleva o seu peso. Existem variações, como somar um ao denominador para evitar divisão por zero e suavizar o logaritmo, mas a ideia central é sempre essa.
Exemplo Prático
Vamos calcular os pesos TF-IDF, do zero, para a coleção de perguntas de alunos, e inspecionar quais palavras recebem mais peso em cada documento. Em seguida, montamos o classificador de tema que encerra o módulo. A estratégia é simples e eficaz, representamos cada tema por um vetor médio das suas perguntas e classificamos uma pergunta nova pelo tema cujo vetor é mais parecido com o dela, segundo o cosseno.
Esse classificador junta tudo o que vimos, tokenização, remoção de stopwords, normalização e a representação vetorial com pesos inteligentes. O código está no notebook notebooks/modulo-03/05-tf-idf.ipynb, então abra-o ao lado para acompanhar.
Código Comentado
import re
import math
from collections import Counter
# Perguntas de treino, cada uma com o seu tema.
treino = [
("como faço a derivada de uma função", "cálculo"),
("qual a regra da cadeia na derivada", "cálculo"),
("o que é a integral de uma função", "cálculo"),
("como resolvo um sistema linear com matrizes", "álgebra"),
("o que é um autovetor de uma matriz", "álgebra"),
("como multiplico duas matrizes", "álgebra"),
("como declaro uma função em python", "programação"),
("o que é um laço de repetição em python", "programação"),
("como uso uma lista em python", "programação"),
]
documentos = [texto for texto, _ in treino]
def tokenizar(texto):
return re.findall(r"\w+", texto.lower(), re.UNICODE)
# IDF: log(N / df) para cada termo do vocabulário.
N = len(documentos)
df = Counter()
for doc in documentos:
for palavra in set(tokenizar(doc)):
df[palavra] += 1
idf = {palavra: math.log(N / freq) for palavra, freq in df.items()}
def vetor_tfidf(texto):
"""Vetor TF-IDF como dicionário {palavra: peso}."""
tf = Counter(tokenizar(texto))
return {palavra: tf[palavra] * idf.get(palavra, 0.0) for palavra in tf}
def cosseno(a, b):
produto = sum(a[p] * b.get(p, 0.0) for p in a)
na = math.sqrt(sum(v * v for v in a.values()))
nb = math.sqrt(sum(v * v for v in b.values()))
return produto / (na * nb) if na and nb else 0.0
# Representa cada tema pela soma dos vetores TF-IDF das suas perguntas (centróide).
temas = {}
for texto, tema in treino:
v = vetor_tfidf(texto)
alvo = temas.setdefault(tema, {})
for palavra, peso in v.items():
alvo[palavra] = alvo.get(palavra, 0.0) + peso
def classificar(texto):
"""Atribui o tema cujo centróide é mais parecido com a pergunta."""
v = vetor_tfidf(texto)
return max(temas, key=lambda tema: cosseno(v, temas[tema]))
for pergunta in [
"como calculo a derivada de x",
"como inverto uma matriz",
"como crio uma função em python",
]:
print(f"{pergunta!r} -> {classificar(pergunta)}")
Ao rodar, as três perguntas novas costumam cair no tema certo, mesmo usando palavras que não estão idênticas no treino, porque o TF-IDF dá peso aos termos certos, como derivada, matriz e função em python. Esse é o pagamento de tudo o que construímos no módulo, um classificador de texto simples, transparente e que funciona.
Exercícios
1) Conceitual: Explique, em palavras, o que cada parte do TF-IDF mede e por que o
produto das duas é útil.
2) Conceitual: Por que uma palavra que aparece em todos os documentos recebe peso
zero pelo IDF?
3) Prático: Inspecione, para uma pergunta, quais palavras receberam os maiores pesos
TF-IDF. Elas fazem sentido como termos de conteúdo?
4) Prático: Acrescente um novo tema, com algumas perguntas, e teste se o
classificador passa a reconhecê-lo.
5) Extensão: Compare o seu classificador TF-IDF com o TfidfVectorizer do
scikit-learn, conferindo se os pesos e as decisões são parecidos.
Projeto da Aula e Projeto do Módulo
Este é o projeto que fecha o módulo. A entrega é um classificador de perguntas de alunos por tema, construído de ponta a ponta com o que você aprendeu, tokenização, remoção de stopwords, normalização opcional, vetores TF-IDF e classificação por similaridade do cosseno.
O roteiro sugerido é o seguinte. Monte um conjunto de perguntas rotuladas por tema, separando algumas para teste. Calcule o IDF na coleção de treino, represente cada tema por um centróide TF-IDF e classifique as perguntas de teste pelo tema mais parecido. Por fim, meça a acurácia e analise os erros.
Considere o projeto pronto quando você tiver a acurácia nas perguntas de teste, ao menos um exemplo de acerto e um de erro comentados, e um parágrafo discutindo o que o TF-IDF acrescentou em relação à contagem crua do Bag of Words. Com isso, você encerra os fundamentos de NLP e fica pronto para o Módulo 4, em que trocamos essas representações esparsas por embeddings densos, capazes de capturar significado.
Leituras Recomendadas
- Capítulo sobre pesos de termos e o modelo vetorial em Manning e colegas, Introduction to Information Retrieval.
- Seções sobre TF-IDF em Jurafsky e Martin, Speech and Language Processing.
- Documentação do scikit-learn sobre
TfidfVectorizer, para comparar com a implementação feita à mão.
Referências Científicas
As referências abaixo são reais e estão registradas em references/referencias.bib. As chaves entre parênteses são as do BibTeX.
- Spärck Jones, K. (1972). A Statistical Interpretation of Term Specificity and Its
Application in Retrieval. Journal of Documentation, 28(1), 11-21.
(
sparckjones1972idf) - Salton, G., e Buckley, C. (1988). Term-Weighting Approaches in Automatic Text
Retrieval. Information Processing and Management, 24(5), 513-523.
(
salton1988tfidf) - Manning, C. D., Raghavan, P., e Schütze, H. (2008). Introduction to Information
Retrieval. Cambridge University Press. (
manning2008ir)