Machine Learning¶
Esta página reúne os fundamentos de aprendizado de máquina supervisionado, com foco em classificação, usando Python e a biblioteca scikit-learn. Para a visão geral de IA (história, subáreas) ver I.A.: conceitos e história; para LLMs, que são um caso particular de modelo treinado em escala, ver LLM.
O que é classificação¶
Definição: Machine Learning (aprendizado de máquina)
Abordagem em que, em vez de escrever regras explícitas, fornecemos exemplos ao programa e deixamos um algoritmo descobrir o padrão. O programa "aprende" com a experiência passada para tomar decisões sobre casos novos.
Definição: Classificação
Tarefa de aprendizado supervisionado em que o modelo atribui cada item a uma categoria (classe) a partir de suas características. Exemplos: e-mail é spam ou não; cliente vai pagar a dívida ou não; vídeo tem conteúdo permitido ou não; funcionário vai pedir demissão ou não.
O computador não sabe o que "spam" significa, mas entende números. Por isso o problema
é reduzido a codificar a resposta como número (ex.: 1 = spam, 0 = não spam) e
ensinar o programa com exemplos já classificados — da mesma forma que uma pessoa
reconhece spam "batendo o olho" porque já viu milhares de e-mails.
Os ingredientes de um problema de classificação¶
| Elemento | O que é | Exemplo (classificar animais) |
|---|---|---|
| Item (amostra) | Cada elemento a ser classificado | Um animal |
| Características (features) | Atributos observáveis do item, em geral codificados como número | [é gordinho?, tem perna curta?, faz "oinc"?] → [1, 1, 0] |
| Marcação (label, rótulo) | A classe correta já conhecida de um item de treino | 1 = porco, -1 = cachorro |
| Dados de treino | Itens com características + marcações | 3 porcos e 3 cachorros já classificados |
| Modelo | O que o algoritmo aprende dos dados de treino | Objeto treinado que sabe prever |
| Item novo | Item sem marcação, a ser previsto | Um animal "misterioso" |
A escolha das características é o que mais importa
O modelo só enxerga o que for representado nas características. Para classificar e-mails poderiam ser: tamanho, palavras que aparecem, horário de envio, se o remetente é conhecido, se é a primeira vez que ele escreve. Escolher características que de fato diferenciam as classes é parte central do trabalho (e a mesma lógica vale para qualquer problema de classificação).
Definição: Marcação binária
Quando só há duas classes, qualquer par de valores serve (0/1, -1/1, A/B).
Usar 1 e -1 é uma convenção que reforça que uma classe é o oposto da outra; o
importante é manter a mesma convenção do treino ao teste.
Treinando e prevendo com scikit-learn¶
O scikit-learn (módulo sklearn) é a biblioteca de ML clássico mais usada em
Python. O ciclo é sempre o mesmo, independente do algoritmo:
flowchart LR
A["Dados + marcações"] --> B["Criar modelo"]
B --> C["fit(dados, marcações)<br/>treinar"]
C --> D["predict(itens novos)<br/>prever"]
D --> E["Comparar com a resposta<br/>esperada"]
Instalação (o pip já vem com o Python 3):
Exemplo completo com o primeiro algoritmo do livro, o Naive Bayes multinomial
(MultinomialNB):
from sklearn.naive_bayes import MultinomialNB
# [é gordinho?, tem perna curta?, faz "auau"?]
porco1 = [1, 1, 0]
porco2 = [1, 1, 0]
porco3 = [1, 1, 0]
cachorro1 = [1, 1, 1]
cachorro2 = [0, 1, 1]
cachorro3 = [0, 1, 1]
dados = [porco1, porco2, porco3, cachorro1, cachorro2, cachorro3]
marcacoes = [1, 1, 1, -1, -1, -1] # 1 = porco, -1 = cachorro
modelo = MultinomialNB() # 1. cria o modelo
modelo.fit(dados, marcacoes) # 2. treina ("adequa-se" aos dados)
misterioso1 = [1, 1, 1]
misterioso2 = [1, 0, 0]
teste = [misterioso1, misterioso2] # lista de itens, mesmo que seja um só
print(modelo.predict(teste)) # [-1 1] -> cachorro, porco
Pontos de atenção:
fit(X, y)treina o modelo;predict(X)devolve uma classe para cada item recebido.predictsempre recebe uma lista de itens (lista de listas / matriz 2D), mesmo quando se quer prever um único item. Passar uma lista simples (1D) está obsoleto e gera erro nas versões atuais da biblioteca.- Criar o modelo não treina nada — só o
fitfaz o modelo aprender. - O mesmo código serve para qualquer problema de duas classes (spam/não spam, adimplente/inadimplente): só mudam as características e as marcações.
Definição: Naive Bayes
Família de algoritmos de classificação baseada no teorema de Bayes: calcula a
probabilidade de cada classe dado o conjunto de características e escolhe a mais
provável. É chamado de naive ("ingênuo") porque assume que as características são
independentes entre si — uma simplificação que raramente é verdadeira, mas funciona
surpreendentemente bem na prática, especialmente com texto. O
MultinomialNB é a variante para contagens/frequências (como ocorrências de
palavras). Os detalhes matemáticos aparecem mais adiante na página.
Avaliando o modelo: taxa de acerto¶
Não basta o código rodar: é preciso medir quão bom o modelo é com dados que ele não
viu no treino. Cria-se um conjunto de teste com itens cuja resposta correta já se
conhece (as marcacoes_teste) e compara-se com o que o modelo previu.
marcacoes_teste = [-1, 1, -1] # respostas corretas conhecidas
resultado = modelo.predict([[1, 1, 1], [1, 0, 0], [0, 0, 1]])
acertos = (resultado == marcacoes_teste).sum()
taxa_de_acerto = 100.0 * acertos / len(marcacoes_teste)
print(taxa_de_acerto) # 100.0
O livro usa um truque equivalente: subtrair resultado - marcacoes_teste — com
marcações 1/-1, o resultado é 0 quando acerta e ±2 quando erra (1-(-1)=2,
-1-1=-2). Comparar diretamente com == é mais claro e funciona com quaisquer rótulos.
Definição: Taxa de acerto (accuracy, acurácia)
Percentual de itens de teste que o modelo classificou corretamente:
acertos / total de itens × 100. É a métrica mais simples de qualidade de um
classificador. Se um dos três itens de teste fosse previsto errado, a taxa seria
66,7%.
Dois recados importantes para a entrevista:
- 100% é raro no mundo real. Um animal atípico (um porco que "late", por exemplo) já é suficiente para o modelo errar. Todo classificador erra; o trabalho é medir esse erro e decidir se ele é aceitável para aquele contexto.
- Erro tem tipos. Classificar um gato como "não gato" e classificar outra coisa como "gato" são erros diferentes, com custos diferentes (ex.: mandar um e-mail legítimo para a caixa de spam é pior do que deixar passar um spam). A taxa de acerto sozinha esconde essa diferença.
Resumo do ciclo de classificação
- Representar cada item como um vetor de características numéricas.
- Marcar cada item de treino com a classe correta.
- Treinar:
modelo.fit(dados, marcacoes). - Prever itens novos:
modelo.predict(teste). - Medir a taxa de acerto contra marcações de teste conhecidas.
Um problema real: prever se o usuário vai comprar¶
O mesmo molde serve para o "mundo real" de uma aplicação web. Cada usuário é um item; as características são os comportamentos observados (visitou a página inicial? visitou a página "como funciona"? visitou a página de contato?); a marcação é o que queremos prever (comprou ou não). Outros problemas com a mesma forma: funcionário vai pedir demissão? aluno vai reprovar? cliente vai cancelar o plano?
Para que serve prever? Se o modelo indica que o usuário não vai comprar, a empresa pode agir — entrar em contato, tirar uma dúvida, descobrir se falta algum produto — em vez de só observar.
Os dados de histórico costumam vir em CSV (comma separated values, "valores separados por vírgula"), formato que qualquer planilha exporta. A primeira linha é o cabeçalho; cada linha seguinte é um usuário do passado:
As três primeiras colunas são as características e a última é a marcação.
Definição: X e Y
Convenção universal em ML supervisionado: X é a matriz de características (o
que conhecemos do item, uma linha por item) e Y é o vetor de marcações (o que
queremos prever). Todo fit recebe (X, Y); todo predict recebe apenas X.
Carregando o CSV em X e Y¶
import csv
def carregar_acessos():
X = []
Y = []
with open("acesso.csv", "r") as arquivo:
leitor = csv.reader(arquivo)
next(leitor) # pula o cabeçalho
for home, como_funciona, contato, comprou in leitor:
dado = [int(home), int(como_funciona), int(contato)]
X.append(dado)
Y.append(int(comprou))
return X, Y
Dois cuidados que aparecem em qualquer carga de dados:
- Descartar o cabeçalho (
next(leitor)), senão o nome das colunas entra como se fosse um dado. - Converter os tipos. O módulo
csvlê tudo como texto ('0','1'); o modelo precisa de números, então converte-se comint()(oufloat()para decimais).
A extração da variável dado é uma refatoração simples (extract variable) que deixa
claro o significado do trecho. Nomes curtos (home, em vez de acessou_home) também
reduzem o ruído quando o contexto já deixa óbvio do que se trata.
Com X e Y carregados, o restante é igual ao exemplo dos animais:
from sklearn.naive_bayes import MultinomialNB
X, Y = carregar_acessos()
modelo = MultinomialNB()
modelo.fit(X, Y)
print(modelo.predict([[1, 0, 1], [0, 1, 0]])) # ex.: [1 0] -> o 1º compra, o 2º não
Treino e teste: nunca avalie com os dados do treino¶
Se o modelo for treinado com todos os 99 registros e depois avaliado com os mesmos 99, a taxa de acerto sai altíssima (93,9% no exemplo do livro) — mas esse número não diz nada sobre o mundo real. É como ensinar 90 animais a uma pessoa e depois pedir para ela classificar exatamente os mesmos animais: ela vai acertar quase tudo porque já os viu. O que importa é como o modelo se comporta diante de itens novos.
Definição: Conjunto de treino e conjunto de teste
Os dados rotulados são divididos em duas partes: treino (usado no fit, para o
modelo aprender) e teste (usado só na avaliação, com itens que o modelo nunca
viu). Uma divisão tradicional é 90% treino / 10% teste (80/20 também é comum).
X, Y = carregar_acessos()
treino_dados = X[:90]
treino_marcacoes = Y[:90]
teste_dados = X[-9:]
teste_marcacoes = Y[-9:]
modelo = MultinomialNB()
modelo.fit(treino_dados, treino_marcacoes)
resultado = modelo.predict(teste_dados)
acertos = sum(1 for previsto, real in zip(resultado, teste_marcacoes) if previsto == real)
taxa_de_acerto = 100.0 * acertos / len(teste_dados)
print(taxa_de_acerto) # 88.89 no exemplo do livro (8 de 9)
Note que o denominador da taxa passa a ser o tamanho do teste, não o do conjunto inteiro. O resultado (≈89%) é menor que os 93,9% anteriores — e é o número honesto.
Vazamento de dados (data leakage)
Usar qualquer informação do conjunto de teste durante o treino, ou testar com os mesmos dados do treino, produz uma estimativa de qualidade irrealisticamente boa. Em entrevista, "separei treino e teste antes de qualquer ajuste" é uma resposta que mostra maturidade.
Um bom hábito é registrar cada experimento como comentário ou em um log (qual estratégia, qual taxa de acerto), para comparar abordagens depois:
Duas limitações ficam em aberto e são tratadas nas seções seguintes: com apenas 9 itens de teste, a taxa varia muito conforme quais itens caíram ali; e a taxa de acerto isolada não diz se 89% é bom ou ruim para aquele problema.
Variáveis categóricas e dummies¶
Até aqui toda característica era binária (0 ou 1). Dados reais têm colunas como "o que o
usuário buscou" (algoritmos, java, ruby, ...). Texto solto o algoritmo não entende;
é preciso convertê-lo em números.
Definição: Variável categórica
Variável que assume um valor dentre um conjunto finito de categorias (ex.: termo buscado, estado onde a pessoa está, tipo de plano). Diferente de uma variável binária (duas opções) ou numérica (quantidade, como horas trabalhadas ou preço).
A técnica é transformar a pergunta "qual foi a busca?" em uma pergunta de sim/não para cada categoria:
| busca original | busca_algoritmos | busca_java | busca_ruby |
|---|---|---|---|
| algoritmos | 1 | 0 | 0 |
| java | 0 | 1 | 0 |
| ruby | 0 | 0 | 1 |
Definição: Dummies (one-hot encoding)
Colunas binárias criadas a partir de uma variável categórica — uma por categoria —,
em que exatamente uma vale 1 por linha. São chamadas "dummies" (variáveis "de
mentira") porque não estavam na coleta original: a informação foi perguntada de um
jeito e é preenchida de outro. A coluna categórica original é descartada depois da
conversão. O nome técnico mais usado em ML é one-hot encoding.
Com isso, tudo vira 0/1 de novo e o mesmo algoritmo já usado serve sem alterações.
Pandas: leitura e preparação de dados¶
Ler CSV "na unha" obriga a escrever uma função por arquivo, informando o tipo de cada coluna. O Pandas (Python Data Analysis Library) resolve isso e é a biblioteca padrão para análise de dados em Python.
Definição: DataFrame
Estrutura de tabela do Pandas (linhas × colunas nomeadas), devolvida por
pd.read_csv. Reconhece o cabeçalho, infere o tipo de cada coluna (int64, object
para texto etc.) e oferece operações de seleção, filtro e transformação. Por convenção,
abrevia-se o Pandas como pd e o DataFrame como df.
Operações essenciais:
| Operação | Código | Observação |
|---|---|---|
| Ler CSV | df = pd.read_csv("buscas.csv") |
Detecta o cabeçalho automaticamente |
| Uma coluna | df["home"] |
Acessa por nome; df[0] dá KeyError |
| Várias colunas | df[["home", "busca", "logado"]] |
Note os colchetes duplos (uma lista de nomes) |
| Gerar dummies | pd.get_dummies(X_df) |
Converte só as colunas de texto; as numéricas ficam como estão |
| DataFrame → array | df.values |
O scikit-learn aceita também DataFrames, mas .values devolve o array NumPy |
Pipeline completo, com uma coluna categórica (busca):
import pandas as pd
from sklearn.naive_bayes import MultinomialNB
df = pd.read_csv("buscas.csv") # colunas: home, busca, logado, comprou
X_df = df[["home", "busca", "logado"]]
Y_df = df["comprou"]
Xdummies_df = pd.get_dummies(X_df) # busca -> busca_algoritmos, busca_java, busca_ruby
X = Xdummies_df.values
Y = Y_df.values # já é binária: não precisa de dummies
porcentagem_treino = 0.9
tamanho_de_treino = int(porcentagem_treino * len(Y)) # fatias exigem inteiro
treino_dados = X[:tamanho_de_treino]
treino_marcacoes = Y[:tamanho_de_treino]
teste_dados = X[tamanho_de_treino:]
teste_marcacoes = Y[tamanho_de_treino:]
modelo = MultinomialNB()
modelo.fit(treino_dados, treino_marcacoes)
resultado = modelo.predict(teste_dados)
taxa_de_acerto = 100.0 * (resultado == teste_marcacoes).mean()
print(taxa_de_acerto) # 82.0 no exemplo do livro (1.000 linhas)
Boas práticas vistas aqui:
- Nomear variáveis pelo que elas são (
X_df,Xdummies_df,X): deixa claro se é DataFrame ou array. - Parametrizar a proporção (
porcentagem_treino = 0.9) e derivar o tamanho do teste do tamanho do treino, para mudar a divisão em um só lugar. - O tamanho de treino precisa ser inteiro para fatiar a lista (
int(0.9 * len(Y))). - Código de treino, predição e cálculo da taxa de acerto não muda quando muda o
problema — só a preparação dos dados (
X,Y) muda. Vale extrair essa parte para funções reutilizáveis.
Com dados reais (900 treino / 100 teste), o modelo acertou 82% usando apenas três características do comportamento do usuário. A pergunta natural é: 82% é bom? Sem um ponto de comparação, não há como dizer — é exatamente o que a próxima seção resolve.
82% é bom? Compare com o algoritmo base¶
Uma taxa de acerto isolada não diz nada: 82% pode ser excelente ou péssimo. É preciso uma referência de comparação, e a mais simples possível é o classificador "burro".
Definição: Algoritmo base (baseline)
Classificador sem nenhuma inteligência, que responde sempre a mesma classe —
a mais frequente — para qualquer item, sem olhar as características. Serve como piso
de qualidade: um modelo que não supera o baseline não está agregando valor e
deveria ser descartado. No scikit-learn existe pronto:
DummyClassifier(strategy="most_frequent").
No exemplo do livro, 832 de 1.000 usuários compraram. Chutar "comprou" para todo mundo acerta 83,2%; chutar "não comprou" acerta só 16,8%. O baseline usa a maior das duas taxas (83,2%) — ou seja, o modelo "inteligente", com 82%, ficou abaixo de quem não faz análise nenhuma.
Isso expõe o perigo de dados desbalanceados: quando 83% dos itens são de uma classe, a taxa de acerto alta é fácil de obter sem aprender nada.
from collections import Counter
# baseline: acerta a classe mais frequente em Y
acerto_base = max(Counter(Y).values())
taxa_de_acerto_base = 100.0 * acerto_base / len(Y)
Counter (do módulo collections) devolve um dicionário com a contagem de cada valor
(Counter({'sim': 832, 'nao': 168})). Como a implementação só pega o maior valor, ela
funciona com quaisquer rótulos (0/1, sim/nao, spam/ham), sem depender de saber
quais são.
Compare sempre com os mesmos dados
O baseline deve ser calculado sobre o mesmo conjunto de teste usado na avaliação do modelo — não sobre todos os dados. Comparar o modelo (avaliado em 10% dos dados) com o baseline (calculado em 100%) não é justo. Se o teste for sorteado aleatoriamente, o baseline usa exatamente o mesmo sorteio.
resultado = modelo.predict(teste_dados)
taxa_de_acerto = 100.0 * (resultado == teste_marcacoes).mean()
acerto_base = max(Counter(teste_marcacoes).values())
taxa_de_acerto_base = 100.0 * acerto_base / len(teste_marcacoes)
print(f"Algoritmo: {taxa_de_acerto:.2f}% | Base: {taxa_de_acerto_base:.2f}%")
# no livro, com o mesmo conjunto de teste: 82,00% x 82,00% — o modelo só empata
Dois critérios para dizer que um modelo é bom¶
- Supera o algoritmo base nos mesmos dados (critério mínimo).
- O número faz sentido para o negócio. Um detector de terremotos com 51% de acerto pode até vencer o baseline, mas metade dos alertas seria inútil. Um modelo para identificar alunos com dificuldade pode acertar bastante e, ainda assim, gastar o tempo dos professores com quem não precisava de ajuda. É preciso decidir qual tipo de erro custa mais (retomado na seção de matriz de confusão e métricas mais adiante).
Marcações em texto (sim/não) e dados genéricos¶
Nem sempre a marcação vem como 0/1; é comum ser sim/nao, true/false etc. O
scikit-learn treina normalmente com marcações em texto, mas o código ao redor precisa
ser ajustado:
| Problema | Solução |
|---|---|
resultado - teste_marcacoes dá TypeError (não existe subtração de texto) |
Comparar com resultado == teste_marcacoes → vetor de True/False |
| Contar acertos | sum(acertos) — em Python True vale 1 e False vale 0 (em NumPy, acertos.mean() já dá a proporção) |
Contar classes sem fixar os valores (len(Y[Y == 'sim'])) |
Counter(Y): conta qualquer valor, sem hardcode |
Filtros booleanos com NumPy/Pandas também ajudam a explorar os dados: Y == 1 gera uma
máscara True/False; Y[Y == 1] devolve só os itens daquela classe; X[Y == 0] devolve
as características de todos os usuários que não compraram.
Naive Bayes por trás dos panos¶
Entender o que o MultinomialNB faz ajuda a explicar, em entrevista, por que ele é
rápido, simples e tão usado com texto.
Probabilidade e regras de decisão¶
Se, numa pesquisa com 100 pessoas, 70 acham que o candidato A ganha e 30 que é o B, as probabilidades são 70% e 30%. Dadas as probabilidades, é preciso uma regra de decisão para responder "quem vai ganhar?":
| Regra | Como decide | Quando faz sentido |
|---|---|---|
| Maior probabilidade (maximum a posteriori, MAP) | Escolhe sempre a classe mais provável | Quando o objetivo é maximizar a taxa de acerto — a mais usada |
| Menor probabilidade | Escolhe a classe menos provável | Não faz sentido: errará quase sempre |
| Sorteio proporcional | Sorteia um número de 1 a 100; cai em 1–70 → A, em 71–100 → B | Quando se quer simular a distribuição real, e não só a classe vencedora |
Definição: Maximum a posteriori (MAP)
Regra de decisão que, dadas as probabilidades de cada classe depois de observar as características (a posteriori), escolhe a classe de maior probabilidade. É o critério padrão dos classificadores Naive Bayes.
O ponto fraco do MAP puro: ele não distingue 70/30 de 99/1 ou 51/49 — sempre escolhe o maior, e se a classe majoritária vencer em todos os cenários, o modelo responderá sempre a mesma coisa (comportamento do algoritmo base, visto acima).
Probabilidade condicional¶
Definição: Probabilidade condicional
Probabilidade de um evento dado que outro ocorreu. Escreve-se
P(Comprar | Rio de Janeiro) ("probabilidade de comprar, dado que o cliente é do Rio
de Janeiro"). A barra | lê-se "dado que".
Exemplo do livro (site de imóveis): contando o histórico de clientes por estado, 68,5% dos clientes do Rio compraram e 27,5% dos de São Paulo. Com o MAP, um cliente do Rio é classificado como "vai comprar" e um de São Paulo como "não vai". Treinar, aqui, é literalmente contar o histórico e montar as tabelas de probabilidade.
Várias características: a suposição "ingênua"¶
Com duas características (estado e faixa de renda, acima/abaixo de R$ 5.000), a pergunta
passa a ser P(Comprar | São Paulo, renda ≤ 5000). O Naive Bayes assume que as
características são independentes entre si, dada a classe — por isso é "ingênuo" — e
por isso consegue tratar cada uma separadamente e multiplicar:
flowchart LR
A["Item novo<br/>(estado, renda)"] --> B["Prob. de cada característica<br/>por classe (tabelas do treino)"]
B --> C["Multiplica as probabilidades<br/>de cada classe"]
C --> D["Regra de decisão<br/>(MAP: maior valor)"]
D --> E["Classe prevista"]
No exemplo simplificado do livro: 0,275 × 0,18 = 4,95% de chance de compra para um
cliente de São Paulo com renda baixa, contra 0,685 × 0,80 = 54,8% para um do Rio com
renda alta — perfis bem diferentes, e o modelo os separa.
Versão rigorosa da fórmula
O livro simplifica para facilitar a intuição. O Naive Bayes formal usa o teorema de Bayes: para cada classe c, calcula-se
ou seja, a probabilidade da classe no histórico (o prior) vezes a probabilidade de cada característica dada a classe; escolhe-se a classe de maior valor (MAP). A ideia é a mesma: treinar = contar frequências no histórico; prever = multiplicar e escolher o maior.
Por que o Naive Bayes é tão popular¶
- Treino extremamente rápido: é só contar ocorrências e montar tabelas — custo linear no número de itens (100 itens → ~100 operações; 1.000 → ~1.000).
- Simples de implementar e fácil de explicar.
- Muito usado em classificação de texto (spam): as características são as palavras e suas frequências.
- Corresponde a um raciocínio humano intuitivo: "a maior parte dos brasileiros gosta de futebol, então respondo que sim" — frequência passada guiando a decisão presente.
- Limitação: a independência entre características raramente é real (ex.: renda e estado podem estar correlacionados), mas na prática o desempenho costuma ser bom o bastante para servir de ponto de partida.
Comparando modelos e escolhendo o vencedor¶
Um resultado só vale para o conjunto de dados em que foi medido. No livro, o mesmo código teve 82% (vs. 82% do baseline) com 1.000 linhas e 75% (vs. 62,5%) com um segundo conjunto de apenas 75 linhas. Conclusões: o desempenho depende da quantidade de dados, da natureza do problema e das características escolhidas — e todo novo conjunto de dados exige uma nova avaliação.
As características também são uma variável do experimento¶
Testar o modelo removendo uma característica por vez mostra o quanto cada uma pesa. No
segundo conjunto, tirar logado ou home não mudou nada; tirar busca derrubou o
resultado ao nível do baseline; usar só busca manteve os 75%. Ou seja: a busca é a
informação valiosa, e mais características nem sempre significam melhor modelo — em
excesso, podem confundir o algoritmo.
# variações testadas (registrar todas!)
X_df = df[["home", "busca", "logado"]] # 75%
X_df = df[["home", "busca"]] # 72,5%
X_df = df[["home", "logado"]] # 62,5% (= baseline)
X_df = df[["busca", "logado"]] # 75%
X_df = df[["busca"]] # 75%
O perigo de tentar demais (e a importância de registrar)¶
Resultado bom por sorte
Cada nova combinação de variáveis é mais um teste. Quanto mais combinações se testa até "acertar", maior a chance de o resultado ótimo ser coincidência. É como pedir a 100 pessoas que joguem uma moeda 10 vezes: alguém vai tirar 10 caras só por acaso. Correlações espúrias famosas (vendas de sorvete × afogamentos; o ano em que a bolsa caiu × filmes de um ator) mostram como se "descobre" padrão onde há só acaso — sorvete não causa afogamento; o verão causa os dois.
A defesa é registrar cada experimento (variáveis, algoritmo, resultado) e apresentar o resultado final junto com todas as tentativas feitas para chegar a ele — inclusive as que falharam. Isso mostra que o número não é fruto de seleção a dedo.
# teste inicial: home, busca, logado => comprou
# home, busca
# home, logado
# busca, logado
# busca: 75% (8 testes)
O algoritmo também é uma variável: AdaBoost¶
Além das características, o algoritmo pode ser trocado. O scikit-learn expõe todos os
classificadores com a mesma interface (fit/predict), então trocar de modelo é mudar
uma linha:
from sklearn.naive_bayes import MultinomialNB
from sklearn.ensemble import AdaBoostClassifier
modelo_a = MultinomialNB()
modelo_b = AdaBoostClassifier()
Definição: AdaBoost (Adaptive Boosting)
Algoritmo de ensemble (conjunto) que combina muitos classificadores simples e
fracos — cada novo "reforça" os itens que o anterior errou — para formar um
classificador forte. O livro só o usa como segundo candidato; o ponto não é o
algoritmo em si, e sim comparar candidatos nos mesmos dados. No mesmo conjunto, o
AdaBoost fez 84–85% contra 82% do MultinomialNB; no conjunto pequeno, os dois
empataram.
Como o código de treino e avaliação é o mesmo para qualquer modelo, extrai-se uma função:
def fit_and_predict(nome, modelo, treino_dados, treino_marcacoes,
teste_dados, teste_marcacoes):
modelo.fit(treino_dados, treino_marcacoes)
resultado = modelo.predict(teste_dados)
taxa_de_acerto = 100.0 * (resultado == teste_marcacoes).mean()
print(f"Taxa de acerto do algoritmo {nome}: {taxa_de_acerto}")
return taxa_de_acerto
Três conjuntos: treino, teste e validação¶
Escolher o vencedor com base no teste contamina o teste: o modelo "melhor" foi selecionado justamente por ir bem nele, então a taxa já não estima o desempenho no mundo real. A solução é separar um terceiro conjunto, que só é usado uma vez, no fim:
| Conjunto | Proporção típica | Uso |
|---|---|---|
| Treino | 80% | Cada candidato aprende (fit) |
| Teste | 10% | Compara os candidatos e escolhe o vencedor |
| Validação ("mundo real") | 10% | Mede o vencedor com dados que ninguém viu — esta é a taxa que se reporta |
porcentagem_de_treino = 0.8
porcentagem_de_teste = 0.1
tamanho_de_treino = int(porcentagem_de_treino * len(Y))
tamanho_de_teste = int(porcentagem_de_teste * len(Y))
fim_de_treino = tamanho_de_treino + tamanho_de_teste
# a validação é o que sobra: len(Y) - tamanho_de_treino - tamanho_de_teste
treino_dados, treino_marcacoes = X[:tamanho_de_treino], Y[:tamanho_de_treino]
teste_dados, teste_marcacoes = X[tamanho_de_treino:fim_de_treino], Y[tamanho_de_treino:fim_de_treino]
validacao_dados, validacao_marcacoes = X[fim_de_treino:], Y[fim_de_treino:]
resultado_a = fit_and_predict("MultinomialNB", modelo_a, treino_dados, treino_marcacoes, teste_dados, teste_marcacoes)
resultado_b = fit_and_predict("AdaBoost", modelo_b, treino_dados, treino_marcacoes, teste_dados, teste_marcacoes)
vencedor = modelo_a if resultado_a > resultado_b else modelo_b
# teste final com dados nunca vistos — e o baseline sobre os MESMOS dados de validação
taxa_real = 100.0 * (vencedor.predict(validacao_dados) == validacao_marcacoes).mean()
O fluxo tem sempre três fases: treinar os candidatos, testar e escolher o melhor, e validar o escolhido com dados novos. Só o número da última fase diz como o modelo deve se comportar na prática — e ele deve ser comparado com o baseline sobre esses mesmos dados de validação. No livro, o AdaBoost venceu com 84% no teste e fez 85% na validação; no conjunto pequeno, igualou o baseline (62,5%), ou seja, não agregou nada.
Definição: Conjunto de validação
Fatia dos dados reservada para a avaliação final, usada uma única vez, depois de todas as escolhas (características, algoritmo, ajustes). Evita que a escolha do "melhor" modelo vaze para a medição de qualidade. Observação de nomenclatura: em outras fontes, o conjunto usado para escolher o modelo é chamado de "validação" e o reservado para a avaliação final de "teste" — o importante é a separação de papéis, não os rótulos.
Classificação com mais de duas categorias (multiclasse)¶
No mundo real nem sempre a resposta é sim/não. Um e-mail pode ser spam, promoção, fórum, atualização, importante, familiar ou normal; um cliente pode estar alegre, neutro ou chateado; um produto novo pode ser sucesso, neutro ou fracasso.
Definição: Classificação multiclasse
Classificação em que cada item pertence a uma entre três ou mais classes (0, 1, 2 … N). Em oposição à classificação binária (duas classes).
Exemplo: situação do cliente (3 classes)¶
Características de cada cliente (todas numéricas, não binárias):
| Característica | Pergunta que responde |
|---|---|
| Recência | Há quanto tempo foi o último acesso? (1 = ontem) |
| Frequência | Em quantos dias distintos acessou depois de inscrito? |
| Tempo de inscrição (semanas) | Há quanto tempo é cliente? |
Marcação: situação do cliente — 2 = alegre, 1 = neutro, 0 = chateado. A cada classe
corresponde uma ação diferente (alegre: sem preocupação; neutro: contato para entender;
chateado: investigar o motivo e tentar recuperar).
Definição: Recência e frequência
Duas características clássicas de comportamento de clientes. Recência: quão recentemente a pessoa interagiu (último acesso/compra). Frequência: com que regularidade interagiu (quantos dias distintos, não quantas vezes). A unidade (dias, horas, semanas) é livre, desde que consistente. Não há regra universal de que mais tempo de cadastro signifique mais ou menos satisfação — por isso a relação é deixada para o algoritmo descobrir.
O fluxo de código é o mesmo dos exemplos anteriores (treino 80% / teste 10% / validação
10%, vários modelos, baseline); só mudam as colunas. Na validação com 23 itens, o
baseline acertou 82,6% — e o MultinomialNB e o AdaBoostClassifier ficaram em
72,7% e 68,2% no teste, com o vencedor empatando com o baseline: previam praticamente
sempre a classe majoritária (1).
Observação sobre as versões atuais
Nas versões atuais do scikit-learn, MultinomialNB e AdaBoostClassifier já
suportam multiclasse nativamente; o que o exemplo do livro mostra é que, naqueles
dados, eles não superaram o baseline. O mecanismo abaixo (One-vs-Rest /
One-vs-One) continua importante porque é a forma de usar com várias classes os
algoritmos que são intrinsecamente binários e porque ilustra a estratégia geral de
"reduzir um problema grande a problemas pequenos que já sabemos resolver".
Estratégia 1: One-vs-Rest ("um contra o resto")¶
Reduz o problema de N classes a N problemas binários: para cada classe, treina-se um classificador "essa classe × todas as outras". Na previsão, roda-se todos e vence a classe cujo classificador der maior confiança.
flowchart TD
D["Dados: classes 0, 1, 2"] --> A["Classificador A<br/>0 vs (1,2)"]
D --> B["Classificador B<br/>1 vs (0,2)"]
D --> C["Classificador C<br/>2 vs (0,1)"]
A --> E["Escolhe a classe de<br/>maior confiança"]
B --> E
C --> E
Com 3 classes são 3 classificadores; com 10 classes, 10 — o custo cresce linearmente com o número de classes.
Definição: One-vs-Rest (OvR, um-contra-todos)
Estratégia multiclasse que treina um classificador binário por classe ("esta classe
contra todas as demais") e escolhe, na previsão, a classe cujo classificador tiver a
maior confiança. No scikit-learn: OneVsRestClassifier.
from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import LinearSVC
modelo_ovr = OneVsRestClassifier(LinearSVC(random_state=0))
modelo_ovr.fit(treino_dados, treino_marcacoes)
O OneVsRestClassifier é só um "embrulho": recebe o classificador binário a ser
repetido (aqui, o LinearSVC, uma máquina de vetores de suporte linear) e o replica por
classe. random_state=0 fixa a semente para o resultado ser reproduzível. No exemplo, o
OvR errou só 2 itens de teste: 90,9%, contra 72,7% e 68,2% dos anteriores.
Estratégia 2: One-vs-One ("um contra um")¶
Treina um classificador para cada par de classes (0×1, 0×2, 1×2…) e escolhe a classe
que mais "vence" nos confrontos (votação). Com N classes são N × (N − 1) / 2
classificadores: o custo cresce quadraticamente.
| Estratégia | Nº de classificadores (N classes) | Custo | Quando preferir |
|---|---|---|---|
| One-vs-Rest | N | Linear | Muitas classes; escolha padrão |
| One-vs-One | N(N−1)/2 | Quadrático | Poucas classes, ou algoritmos que escalam mal com o número de itens (cada par usa só os itens daquelas duas classes) |
from sklearn.multiclass import OneVsOneClassifier
modelo_ovo = OneVsOneClassifier(LinearSVC(random_state=0))
No exemplo do livro, o One-vs-One acertou 100% no teste (e foi o vencedor). Com poucas classes (3) o custo extra é pequeno; com dezenas de classes ficaria caro.
Escolhendo o vencedor entre vários modelos¶
Com mais candidatos, uma cadeia de if/else não escala. Uma forma enxuta é guardar as
taxas por nome e escolher o maior valor com max:
candidatos = {
"OneVsRest": OneVsRestClassifier(LinearSVC(random_state=0)),
"OneVsOne": OneVsOneClassifier(LinearSVC(random_state=0)),
"MultinomialNB": MultinomialNB(),
"AdaBoost": AdaBoostClassifier(),
}
taxas = {}
for nome, modelo in candidatos.items():
taxas[nome] = fit_and_predict(nome, modelo, treino_dados, treino_marcacoes,
teste_dados, teste_marcacoes)
nome_vencedor = max(taxas, key=taxas.get) # chave com a maior taxa
vencedor = candidatos[nome_vencedor]
teste_real(vencedor, validacao_dados, validacao_marcacoes)
O livro usa um dicionário {taxa: modelo} e max sobre a chave; funciona, mas dois
modelos com a mesma taxa se sobrescrevem. Com o nome como chave e key=taxas.get não há
esse problema.
Rode todos os candidatos de uma vez
Não escolha o próximo algoritmo a testar com base no resultado do anterior até "dar certo": isso é a mesma armadilha da sorte vista antes. O processo correto é treinar todos os candidatos, comparar todos no conjunto de teste, eleger o vencedor e só então medi-lo nos dados de validação — compare sempre com o baseline nos mesmos dados.
Validação cruzada (k-fold)¶
O esquema "um treino, um teste, uma validação" tem uma fragilidade: o resultado depende
de quais itens caíram em cada fatia. No exemplo do livro, com 10 itens, treinar com
{1,2,3,4,5,6} e testar com {7,8} deu 82%; trocar apenas um cliente de lugar (treino
{1,2,3,4,6,7}, teste {5,8}) deu 72%. Os dois números são "válidos" — logo nenhum deles
é confiável sozinho. Uma mudança qualquer no mundo real (alguém que acessou um dia
depois, um cliente que ficou doente) altera a ordem e, com ela, o resultado.
A saída é repetir o treino e o teste com várias divisões diferentes e tirar a média. Testar todas as permutações possíveis é inviável; o k-fold faz isso de forma sistemática.
Definição: k-fold (validação cruzada, cross-validation)
Técnica que divide os dados de treino em k pedaços (folds) de tamanho parecido e repete k vezes: em cada rodada, um pedaço é o teste e os k − 1 restantes são o treino. O resultado final é a média das k taxas de acerto. Assim, todo item é usado para treinar e para testar, e a estimativa deixa de depender de uma divisão específica.
Exemplo com 8 itens e k = 3 (pedaços {1,2,3}, {4,5,6}, {7,8}):
| Rodada | Treino | Teste | Acerto |
|---|---|---|---|
| 1 | {4,5,6,7,8} | {1,2,3} | 88% |
| 2 | {1,2,3,7,8} | {4,5,6} | 74% |
| 3 | {1,2,3,4,5,6} | {7,8} | 83% |
| Média | 81,66% |
flowchart LR
subgraph K3["k = 3"]
direction TB
R1["Rodada 1: teste = pedaço 1<br/>treino = pedaços 2 e 3"]
R2["Rodada 2: teste = pedaço 2<br/>treino = pedaços 1 e 3"]
R3["Rodada 3: teste = pedaço 3<br/>treino = pedaços 1 e 2"]
end
K3 --> M["Média das 3 taxas<br/>= estimativa final"]
Escolha do k¶
- k varia de 2 até o número de itens. Em k = 2 cada metade vira treino e teste uma vez; em k = N cada item vira o teste uma vez (leave-one-out, o caso extremo).
- Quanto maior o k, mais rodadas (k treinos por modelo) e mais lento o processo. O livro usa k = 10 como valor típico no restante do exemplo.
- Com k = 3, 4 e 10, o mesmo modelo deu 92,22%, 92,77% e 92,31%: valores próximos, mas diferentes. Decida o k antes e não o ajuste olhando o resultado — escolher o k que dá o melhor número é, de novo, "viciar" a decisão.
Implementação com cross_val_score¶
O scikit-learn faz os cortes, treina e devolve a taxa de cada rodada:
import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import LinearSVC
# 1. reserva apenas validação final (ex.: 20%); o k-fold usa só o treino
tamanho_de_treino = int(0.8 * len(Y))
treino_dados, treino_marcacoes = X[:tamanho_de_treino], Y[:tamanho_de_treino]
validacao_dados, validacao_marcacoes = X[tamanho_de_treino:], Y[tamanho_de_treino:]
# 2. k-fold apenas nos dados de treino
k = 10
modelo = OneVsRestClassifier(LinearSVC(random_state=0))
scores = cross_val_score(modelo, treino_dados, treino_marcacoes, cv=k)
taxa_de_acerto = 100.0 * np.mean(scores) # média das k rodadas
print(taxa_de_acerto)
Observações:
- Agora não há mais conjunto de teste separado: o k-fold usa os mesmos dados de treino, ora como treino, ora como teste. Fica só a validação final, reservada e intocada.
cross_val_scoredevolve um array com a taxa de cada rodada; a média é o resultado e o desvio padrão (scores.std()) indica a estabilidade do modelo.- Para classificação,
cv=kjá faz a divisão estratificada (mantém a proporção das classes em cada pedaço). - A função
fit_and_predictpassa a receber o modelo e os dados de treino e devolver a média do k-fold; o restante do fluxo (comparar candidatos, eleger o vencedor, treinar o vencedor com todo o treino e medir na validação final) permanece igual.
def fit_and_predict(nome, modelo, treino_dados, treino_marcacoes, k=10):
scores = cross_val_score(modelo, treino_dados, treino_marcacoes, cv=k)
taxa_de_acerto = 100.0 * np.mean(scores)
print(f"Taxa de acerto do algoritmo {nome}: {taxa_de_acerto:.2f}")
return taxa_de_acerto
Transparência do processo
Variar o tipo de validação, o k e os modelos até aparecer um bom número é mais uma forma de se enganar por coincidência. Quem apresenta resultados deve apresentar também o processo completo — incluindo o que deu errado. É comum (um "vício humano") publicar só o que funcionou, e isso faz a amostra parecer melhor do que é.
Classificando texto: do texto ao vetor de números¶
Até aqui os algoritmos receberam números. Mas muitos problemas reais envolvem texto: mensagens enviadas pelo formulário de contato de um site, por exemplo, precisam ser direcionadas ao setor certo.
| Mensagem recebida | Categoria |
|---|---|
| "Se eu comprar cinco anos antecipados, eu ganho algum desconto?" | Comercial |
| "O exercício 15 do curso de Java 1 está com a resposta errada. Pode conferir?" | Técnico |
| "Existe algum curso para cuidar do marketing da minha empresa?" | Carreira |
| "Já trabalho como designer e queria aprender mais de UX, quais cursos devo fazer?" | Carreira |
Pedir que o usuário escolha a categoria em uma lista funciona com 3 opções, mas degrada rápido: com Comercial, Financeiro, Técnico, Conteúdo e Carreira, uma mensagem pode caber em duas ou três ao mesmo tempo e a pessoa não sabe qual escolher. É o caso de treinar um classificador com mensagens já categorizadas e deixá-lo decidir — um problema de classificação multiclasse, só que com texto no lugar de números.
O desafio: toda entrada precisa ter o mesmo tamanho¶
Os algoritmos esperam uma tabela de colunas fixas (como recencia, frequencia, semanas).
Textos têm tamanhos diferentes. A solução é reduzir o problema novo a um que já se sabe
resolver: transformar cada texto em um vetor de números de tamanho fixo.
Definição: Bag of words (saco de palavras)
Representação de um texto como um vetor em que cada posição corresponde a uma palavra do vocabulário e o valor é quantas vezes ela aparece no texto. Ignora a ordem das palavras e a gramática — só importa quais palavras ocorrem e quantas vezes. É a representação mais simples de texto para ML clássico.
Definição: Vocabulário (dicionário)
Lista de todas as palavras distintas encontradas nos textos de treino. Define as colunas do vetor: todo texto, curto ou longo, vira um vetor com o mesmo número de posições (o tamanho do vocabulário).
Passo a passo¶
- Montar o vocabulário: juntar as palavras distintas de todos os textos. Em "Se eu comprar cinco anos antecipados, eu ganho algum desconto?" há 10 palavras mas 9 distintas ("eu" repete).
- Contar quantas vezes cada palavra do vocabulário aparece em cada texto.
Com o vocabulário [se, eu, comprar, cinco, anos, antecipados, ganho, algum, desconto]:
| Texto | se | eu | comprar | cinco | anos | antecipados | ganho | algum | desconto |
|---|---|---|---|---|---|---|---|---|---|
| "Se eu comprar cinco anos antecipados, eu ganho algum desconto?" | 1 | 2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| "Eu ganho desconto se comprar cinco anos antecipados?" | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 |
Textos diferentes (e de tamanhos diferentes) viram vetores do mesmo comprimento. Se aparece uma frase com palavras novas ("Ao terminar um curso, eu ganho um certificado?"), acrescentam-se as palavras novas ao vocabulário e todos os vetores passam a ter as novas colunas (com zero onde a palavra não ocorre).
O algoritmo descobre sozinho quais palavras indicam cada categoria: se "preço", "desconto", "valor" e "pagamento" aparecem quase só nas mensagens comerciais, ele aprende a associá-las a essa classe; "curso" e "carreira" puxam para o setor de carreira. Ninguém precisa escrever essas regras.
from sklearn.feature_extraction.text import CountVectorizer
frases = [
"Se eu comprar cinco anos antecipados, eu ganho algum desconto?",
"Eu ganho desconto se comprar cinco anos antecipados?",
"Ao terminar um curso, eu ganho um certificado?",
]
vetorizador = CountVectorizer()
X = vetorizador.fit_transform(frases)
print(vetorizador.get_feature_names_out()) # o vocabulário (ordem alfabética)
print(X.toarray()) # a contagem de cada palavra por frase
CountVectorizer faz o mesmo que o processo manual (montar o vocabulário e contar), e
já converte tudo para minúsculas. A implementação manual — e os cuidados com limpeza do
texto — são vistos adiante.
Resumo da ideia
Para classificar texto: (1) construir o vocabulário com os textos de treino;
(2) transformar cada texto em um vetor de contagens; (3) usar esse vetor como X em
qualquer classificador já visto (MultinomialNB, One-vs-Rest...). Esse é o ponto de
partida de PLN clássico — e o motivo de o MultinomialNB, que trabalha com
contagens, ser a escolha típica para spam e categorização de texto.
Classificação de texto na prática: roteando e-mails¶
Com a ideia do saco de palavras, o fluxo completo para o exemplo do formulário de contato (43 e-mails, cada um já marcado com a categoria 1, 2 ou 3) é:
flowchart LR
A["emails.csv<br/>(texto + categoria)"] --> B["minúsculas +<br/>quebrar em palavras"]
B --> C["vocabulário<br/>(conjunto de palavras)"]
C --> D["palavra → posição<br/>(tradutor)"]
D --> E["cada e-mail vira um vetor<br/>de contagens"]
E --> F["treino / k-fold /<br/>validação"]
1. Ler o CSV e separar texto e marcação¶
import pandas as pd
classificacoes = pd.read_csv("emails.csv") # colunas: email, classificacao
textos_puros = classificacoes["email"]
marcas = classificacoes["classificacao"]
2. Limpar e quebrar em palavras¶
O .str.lower() evita que "Como" e "como" sejam tratadas como palavras diferentes — a
chamada limpeza do texto. Aqui a escolha é minúsculas, mas depende do objetivo: para
detectar spam ou um usuário bravo, texto em CAIXA ALTA pode ser justamente o sinal
relevante, e converter tudo apagaria essa informação.
A divisão por espaço é a abordagem mais simples e ainda é imperfeita
split(" ") deixa pontuação grudada na palavra ("desconto?", "plano,"), de modo que
"plano" e "plano," viram palavras distintas. Tratar isso (remover pontuação,
acentos, stop words como "de", "a", "o") é o próximo passo de qualidade na
preparação de texto.
3. Vocabulário com um conjunto (set)¶
dicionario = set()
for lista in textos_quebrados:
dicionario.update(lista) # só entra a palavra que ainda não existe
total_de_palavras = len(dicionario) # 364 palavras distintas no exemplo
Definição: Conjunto (set)
Estrutura de dados que não admite elementos repetidos (a mesma ideia da teoria dos
conjuntos: {1, 2, 3} não aceita outro 1). Ideal para montar o vocabulário: adicionar
uma palavra já existente não tem efeito.
4. O "tradutor": palavra → posição no vetor¶
Cada palavra do vocabulário recebe um número (0, 1, 2 … 363) com zip e range, e o
par vira um dicionário Python:
tuplas = zip(dicionario, range(total_de_palavras))
tradutor = {palavra: indice for palavra, indice in tuplas}
tradutor["curso"] # devolve a posição da palavra "curso"
tradutor["palavra-inexistente"] # KeyError: a palavra não está no vocabulário
Definição: Dicionário (dict) e tupla
Tupla é um par/sequência ordenada imutável, como ("curso", 16). Dicionário
(dict) é um mapa chave → valor com busca direta pela chave. O zip junta duas
sequências em tuplas, e a compreensão {k: v for k, v in tuplas} as transforma em
dicionário.
5. Vetorizar cada texto¶
def vetorizar_texto(texto, tradutor):
vetor = [0] * len(tradutor) # tamanho = quantidade de palavras do vocabulário
for palavra in texto:
if palavra in tradutor: # ignora palavras desconhecidas
posicao = tradutor[palavra]
vetor[posicao] += 1
return vetor
vetores_de_texto = [vetorizar_texto(texto, tradutor) for texto in textos_quebrados]
Resultado: 43 vetores de 364 posições, todos com o mesmo tamanho, onde cada posição guarda quantas vezes a palavra apareceu naquele e-mail. Um texto cujas palavras são todas desconhecidas vira um vetor só de zeros.
Palavras fora do vocabulário
Em produção, um e-mail novo terá palavras que não estavam no treino. Elas são
ignoradas (como na função acima) — o vocabulário é definido só com os textos
de treino e reaproveitado para novos textos. (O CountVectorizer do scikit-learn
faz exatamente isso: fit constrói o vocabulário e transform aplica.)
6. Treinar, comparar e validar¶
X = vetores_de_texto e Y = marcas. O restante é o fluxo já visto: separar 80%
treino e 20% validação, rodar k-fold apenas no treino para cada candidato, eleger o
vencedor, treiná-lo com todo o treino e medir na validação, comparando com o
baseline.
| Modelo | Acerto (k-fold no treino) |
|---|---|
| OneVsRest (LinearSVC) | 72,33% |
| MultinomialNB | 71,50% |
| OneVsOne (LinearSVC) | 65,67% |
| AdaBoost | 42,33% |
O vencedor, One-vs-Rest, acertou 88,9% na validação (8 de 9 e-mails), enquanto o baseline acertaria só 44,4%: a classificação automática erraria cerca de 11% dos encaminhamentos, contra 56% se se chutasse sempre a mesma seção. (Com apenas 9 e-mails de validação, a margem de confiança é pequena — é uma demonstração, não um resultado de produção.)
Dois aprendizados práticos:
- Teste cada passo ao implementar. Colar muito código de uma vez e depender de
imports esquecidos (
NameError: cross_val_score) torna a depuração um pesadelo; rodar a cada etapa localiza o erro na hora. - Fixe a semente aleatória. O
AdaBoostClassifierusa números aleatórios e dava resultados diferentes a cada execução (47,33%, depois 43,99%…), o que impede comparar e reproduzir. Passarrandom_state=0fixa a seed e torna o resultado repetível:
Definição: Seed / random_state
Valor inicial do gerador de números pseudoaleatórios. Com a mesma seed, a sequência "aleatória" é sempre a mesma, tornando experimentos reproduzíveis. Sem fixar, cada execução pode dar um resultado diferente — e olhar várias execuções até achar a "melhor" é outra forma de viciar a decisão.
Limpeza de texto: stop words, stemming e tokenização¶
O vocabulário "cru" do exemplo dos e-mails (364 palavras) mistura palavras úteis ("recomendam", "carreira", "preço", "certificado", "trocar") com ruído ("com", "o", "uma", "isto") e com variações da mesma palavra. Quanto mais limpo o vocabulário, melhor a precisão (o algoritmo não decide com base em coincidências nas palavras comuns) e melhor o desempenho (menos colunas para processar — com 1 milhão de palavras a diferença é enorme).
A biblioteca padrão para isso em Python é o NLTK (Natural Language Toolkit, pip
install nltk). Seus recursos linguísticos são baixados à parte com nltk.download(...)
— instalar a biblioteca não instala os pacotes de idioma.
1. Stop words¶
Definição: Stop words (palavras de parada)
Palavras muito frequentes de um idioma que servem para construir as frases, mas carregam pouca informação sobre o assunto (em português: "com", "o", "uma", "isto", "de", "que"…). Costumam ser removidas antes de vetorizar o texto. O NLTK traz a lista por idioma.
stopwords = nltk.corpus.stopwords.words("portuguese")
validas = [palavra for palavra in lista if palavra not in stopwords]
No exemplo, a remoção tirou 49 palavras (365 → 315). Atenção: a lista do NLTK pode conter formas que não existem no idioma — e se o seu problema depende de uma dessas palavras (ex.: "não" numa análise de sentimento), é preciso removê-la da lista.
2. Stemming (radical da palavra)¶
Definição: Stemming
Redução de uma palavra ao seu radical (raiz), para que variações de flexão sejam
tratadas como uma só: "amigo", "amigas", "amigos" → amig; "deve", "devem",
"deverão" → dev. Não gera necessariamente uma palavra válida — apenas um
identificador comum. Para o português, o NLTK oferece o RSLPStemmer, baseado na
remoção de sufixos.
Efeito no exemplo: o vocabulário caiu de 315 para 285 radicais.
O mesmo tratamento no treino e na previsão
Se o vocabulário guarda radicais, a vetorização também precisa aplicar o
stemmer.stem() a cada palavra do texto antes de consultar o dicionário — senão
"cinco" nunca será encontrado (o vocabulário só tem "cinc"). Foi exatamente esse
erro que o livro cometeu e depois corrigiu: antes da correção apenas 3 das 7 palavras
da frase de teste eram contadas, e a taxa de acerto dos modelos caiu de ~72% para
~40%. Regra geral: todo pré-processamento aplicado ao treino deve ser aplicado
igual aos dados novos. Observe ainda que o RSLPStemmer quebra com uma string
vazia (IndexError), então é preciso ignorar palavras de tamanho 0.
3. Tokenização¶
Separar o texto só por espaço deixa pontuação colada nas palavras ("eu,", "minutos.", "empresa?"), criando "palavras" distintas por causa de um sinal.
Definição: Tokenização
Divisão do texto em unidades (tokens) — normalmente palavras e sinais de
pontuação. É a primeira etapa de qualquer pipeline de PLN. O word_tokenize do NLTK
separa por espaço e por pontuação, respeitando o idioma.
nltk.tokenize.word_tokenize("Voce vai viajar? Este ano, eu penso que sim!")
# ['Voce', 'vai', 'viajar', '?', 'Este', 'ano', ',', 'eu', 'penso', 'que', 'sim', '!']
Os próprios sinais viram tokens; para descartá-los, usa-se um filtro de tamanho: é comum ignorar tokens com menos de 3 caracteres, o que elimina pontuação e palavras curtas sem significado. No exemplo, o vocabulário final ficou com 230 radicais.
O pipeline de limpeza completo¶
import nltk
import pandas as pd
classificacoes = pd.read_csv("emails.csv")
frases = classificacoes["email"].str.lower()
textos_quebrados = [nltk.tokenize.word_tokenize(frase) for frase in frases]
stopwords = nltk.corpus.stopwords.words("portuguese")
stemmer = nltk.stem.RSLPStemmer()
def limpar(palavras):
return [stemmer.stem(p) for p in palavras if p not in stopwords and len(p) > 2]
dicionario = set()
for palavras in textos_quebrados:
dicionario.update(limpar(palavras))
tradutor = {palavra: i for i, palavra in enumerate(dicionario)}
def vetorizar_texto(palavras, tradutor):
vetor = [0] * len(tradutor)
for radical in limpar(palavras): # mesma limpeza do treino
if radical in tradutor:
vetor[tradutor[radical]] += 1
return vetor
flowchart LR
A["Texto bruto"] --> B["Minúsculas"]
B --> C["Tokenização<br/>(espaços e pontuação)"]
C --> D["Remove stop words<br/>e tokens curtos"]
D --> E["Stemming<br/>(radicais)"]
E --> F["Vocabulário + vetor<br/>de contagens"]
Hoje, no scikit-learn
CountVectorizer e TfidfVectorizer aceitam lowercase, stop_words, tokenizer
e ngram_range, encapsulando boa parte desse pipeline em poucas linhas — e o
Pipeline do scikit-learn garante que o mesmo pré-processamento seja aplicado em
treino e previsão. Para trabalhos recentes de texto, modelos de linguagem e
embeddings (ver LLM) tornam várias dessas etapas manuais
desnecessárias, mas os conceitos de vocabulário, tokenização e limpeza continuam a
base do entendimento.
Evite inflar o número de experimentos
Cada decisão de limpeza (remover stop words? usar stemming? mínimo de 2 ou 3 letras?) é mais um grau de liberdade. Como visto em Comparando modelos, registre as variações testadas e decida o pipeline antes de olhar o resultado final.
Checklist de um projeto de classificação¶
Resumo de tudo o que a página cobre, útil como roteiro de resposta em entrevista:
- Defina o problema e as classes; transforme respostas em números (0/1, 0/1/2…).
- Escolha características que diferenciem as classes; converta categóricas em dummies e texto em vetor de contagens (com limpeza).
- Separe os dados: treino, teste (ou k-fold no treino) e uma validação final
intocada. Fixe
random_statepara reproduzir. - Estabeleça o baseline (classe mais frequente) sobre os mesmos dados de teste.
- Treine vários candidatos de uma vez (Naive Bayes, AdaBoost, One-vs-Rest, One-vs-One…) e compare-os com validação cruzada.
- Escolha o vencedor e meça uma única vez na validação; o número só vale se superar o baseline e fizer sentido para o negócio.
- Registre todos os experimentos, inclusive os que falharam, para não se enganar com resultado obtido por sorte.
Para se aprofundar, o livro sugere praticar com dados reais da própria empresa ou de bases públicas, estudar a matemática por trás dos algoritmos (Naive Bayes, etc.) e explorar bibliotecas em R, Octave e Python — preferindo linguagens com comunidade ativa. Referências citadas: Fundamentals of Machine Learning for Predictive Data Analytics (D'Arcy, Kelleher, Mac Namee) e The Elements of Statistical Learning (Hastie, Tibshirani, Friedman).