Machine Learning
Como uma máquina ajusta uma regra a partir de exemplos.
Machine Learning: os fundamentos
Um modelo que acerta 99% das respostas pode ser totalmente inútil. Um modelo que reconhece perfeitamente todos os exemplos com os quais foi construído pode ser o pior da seleção. Essas duas afirmações parecem absurdas, mas fazem parte do dia a dia do aprendizado de máquina, e ignorá-las é a forma mais comum de entregar um sistema que falha logo ao entrar em produção.
O motivo é que o aprendizado de máquina não busca memorizar exemplos: ele busca generalizar para casos nunca vistos. Só que não podemos medir diretamente essa capacidade — por definição, os casos futuros não estão disponíveis. Toda a arte consiste, portanto, em organizar uma medição indireta honesta e, em seguida, escolher o indicador certo para lê-la.
Esta lição constrói essas salvaguardas na ordem em que elas se tornam necessárias. Veremos primeiro o que significa concretamente “aprender” para uma máquina, o que se resume a ajustar números. Depois, as três grandes famílias de problemas, conforme a informação de que dispomos. Em seguida, o protocolo que permite medir a generalização e o fenômeno que ele revela: o overfitting (sobreajuste). Por fim, a escolha das métricas, onde se concentra a maioria dos mal-entendidos entre equipes técnicas e de negócio.
Todos os exemplos usam scikit-learn e conjuntos de dados que acompanham a biblioteca: podem ser executados como estão, sem download.
Aprender é ajustar parâmetros
Um modelo é uma função parametrizada. Aprender é buscar os valores de parâmetros que minimizam a diferença entre as previsões e as respostas conhecidas. Nada mais misterioso do que isso.
import numpy as np
from sklearn.linear_model import LinearRegression
# Cinco apartamentos: área em m², aluguel em euros.
surface = np.array([[30], [45], [60], [75], [90]])
loyer = np.array([620, 780, 1010, 1150, 1400])
modele = LinearRegression().fit(surface, loyer)
print(round(float(modele.coef_[0]), 2)) # 12.87
print(round(float(modele.intercept_), 2)) # 220.0
print(modele.predict([[50]]).round(1)) # [863.3]
O modelo encontrou dois números. A regra aprendida pode ser lida em linguagem natural: cerca de 12,87 € por metro quadrado, mais 220 € de taxa fixa. Ela não foi escrita, foi deduzida de cinco observações minimizando a soma dos quadrados dos erros.
O que importa aqui é a natureza da previsão para 50 m²: 863,3 €. Nenhum apartamento de 50 m² consta nos dados. O modelo interpolou entre o de 45 m² e o de 60 m². Isso é legítimo. Por outro lado, pedir o aluguel de um apartamento de 400 m² seria uma extrapolação muito além do domínio observado: a fórmula responderia sem hesitar, e não haveria nenhuma razão para acreditar nela.
Três termos de vocabulário a fixar, pois aparecem o tempo todo:
| Termo | Significado | No exemplo |
|---|---|---|
| Variável explicativa (feature) | o que fornecemos como entrada | a área |
| Alvo (target, label) | o que queremos prever | o aluguel |
| Parâmetros | os números ajustados pelo aprendizado | 12,87 e 220 |
| Hiperparâmetros | os ajustes definidos pelo humano antes do aprendizado | aqui nenhum; em outros casos, a profundidade de uma árvore |
As três famílias de aprendizado
A família depende da informação de que dispomos, não do algoritmo.
Aprendizado supervisionado
Temos exemplos rotulados: para cada entrada, a resposta correta. É o caso da regressão acima e de qualquer classificação. É de longe o cenário mais usado em produção, porque é o que se mede melhor — e o mais caro, porque é preciso produzir os rótulos.
Aprendizado não supervisionado
Temos apenas as entradas. O algoritmo busca uma estrutura: grupos, eixos de variação, anomalias. A avaliação se torna delicada, por falta de uma verdade de referência.
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import load_iris
iris = load_iris()
# NÃO usamos iris.target: o algoritmo não vê nenhum rótulo.
groupes = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(iris.data)
print(np.bincount(groupes)) # [62 50 38]
# Comparação a posteriori com as espécies reais, para julgar o resultado.
import collections
print(collections.Counter(zip(groupes.tolist(), iris.target.tolist())))
A contagem final mostra exatamente o que é preciso entender sobre o não supervisionado. Um grupo contém as 50 setosa e nada além disso: essa espécie é nitidamente separada das outras duas. Os dois grupos restantes misturam versicolor e virginica: 14 virginica acabam com as versicolor, e 2 versicolor com as virginica. Dezesseis flores em cento e cinquenta foram agrupadas incorretamente, pouco mais de 10%.
O algoritmo não falhou: ele encontrou os três grupos mais compactos no sentido da distância euclidiana. Esses grupos não coincidem perfeitamente com as espécies, porque nada o obrigava a isso. É o limite intrínseco do não supervisionado: ele revela uma estrutura, não a estrutura que você tinha em mente.
Aprendizado por reforço
Nem entradas rotuladas, nem simples observação: um agente age, recebe uma recompensa numérica e precisa descobrir a estratégia que maximiza o acúmulo dessas recompensas. A dificuldade própria desse cenário é o equilíbrio entre explorar o que parece bom (exploitation) e experimentar o que ainda foi pouco testado (exploration).
import numpy as np
# Três alavancas, cada uma com uma probabilidade de ganho desconhecida do agente.
rng = np.random.default_rng(0)
vraies = np.array([0.2, 0.5, 0.75])
estimations = np.zeros(3)
tirages = np.zeros(3)
for t in range(2000):
if rng.random() < 0.1: # 10 % do tempo: exploramos
bras = rng.integers(3)
else: # caso contrário: exploramos o melhor conhecido
bras = int(np.argmax(estimations))
recompense = 1.0 if rng.random() < vraies[bras] else 0.0
tirages[bras] += 1
# Média atualizada de forma incremental.
estimations[bras] += (recompense - estimations[bras]) / tirages[bras]
print(estimations.round(2)) # [0.19 0.46 0.75]
print(tirages.astype(int)) # [ 81 72 1847]
As estimativas finais se aproximam das probabilidades reais e, sobretudo, a distribuição das tentativas conta a história do aprendizado: 1847 tentativas em 2000 foram dedicadas à melhor alavanca, contra cerca de 75 para cada uma das outras duas. O agente não apenas estimou, ele concentrou seu orçamento. O preço pago se chama arrependimento (regret): as 153 tentativas nas alavancas medíocres eram necessárias para ter certeza de que eram medíocres.
Treino e teste: por que escondemos dados
Avaliar um modelo com os exemplos que serviram para ajustá-lo mede sua memória, não sua capacidade de generalizar. A regra, portanto, é separar uma parte dos dados antes de qualquer aprendizado e consultá-la somente no final.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
donnees = load_breast_cancer() # 569 tumores, 30 medidas, 2 classes
X_train, X_test, y_train, y_test = train_test_split(
donnees.data, donnees.target, test_size=0.3, random_state=42
)
print(X_train.shape, X_test.shape) # (398, 30) (171, 30)
for profondeur in (1, 3, None):
arbre = DecisionTreeClassifier(max_depth=profondeur, random_state=0)
arbre.fit(X_train, y_train)
print(profondeur,
round(arbre.score(X_train, y_train), 3),
round(arbre.score(X_test, y_test), 3))
O parâmetro random_state fixa o sorteio: sem ele, duas execuções produziriam dois particionamentos diferentes e números não comparáveis.
O overfitting, medido
A tabela produzida pelo loop anterior é o resultado mais instrutivo desta lição:
| Profundidade máxima | Acurácia no treino | Acurácia no teste | Diferença |
|---|---|---|---|
| 1 | 0,925 | 0,895 | 0,030 |
| 3 | 0,970 | 0,965 | 0,005 |
| ilimitada | 1,000 | 0,924 | 0,076 |
A árvore de profundidade ilimitada classifica perfeitamente os 398 exemplos de treino. Ainda assim, ela é pior no teste do que a árvore de profundidade 3. Ela construiu ramos que descrevem particularidades desses 398 pacientes específicos — ruído, não uma regularidade. Isso é o overfitting (sobreajuste).
No outro extremo, a árvore de profundidade 1 faz apenas uma pergunta: é rígida demais para captar o que realmente está presente. Isso é o underfitting (subajuste). O ajuste ideal está entre os dois, e ele é encontrado por medição, não por intuição.
erro
│
│╲ ╱ ← erro de teste
│ ╲ ╱
│ ╲ ╱
│ ╲__________________________╱ ← zona útil
│ ╲
│ ╲___________________ ← erro de treino
└──────────────────────────────────────► complexidade do modelo
underfitting overfitting
Uma diferença de 0,076 entre treino e teste é o sinal de alerta a monitorar. Mas um único particionamento continua sendo um sorteio: medir sobre 171 exemplos deixa uma margem de incerteza considerável. A validação cruzada repete a operação em vários particionamentos:
from sklearn.model_selection import cross_val_score
scores = cross_val_score(DecisionTreeClassifier(max_depth=3, random_state=0),
donnees.data, donnees.target, cv=5)
print(scores.round(3)) # [0.912 0.895 0.939 0.947 0.894]
print(scores.mean().round(3)) # 0.917
print(scores.std().round(3)) # 0.022
A média (0,917) é uma estimativa mais sóbria do que os 0,965 obtidos em um particionamento favorável, e o desvio padrão (0,022) quantifica a margem. Anunciar “96,5%” com base em um único particionamento não é mentira, mas é uma medida frágil demais para fundamentar uma decisão.
Escolher a métrica
Eis a armadilha anunciada na abertura. Em uma doença que atinge 1% da população, um modelo que responde “saudável” para todo mundo obtém 99% de acurácia.
import numpy as np
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
# 1000 pacientes, 10 doentes.
y_vrai = np.array([0] * 990 + [1] * 10)
# Modelo preguiçoso: "ninguém está doente".
y_paresseux = np.zeros(1000, dtype=int)
print((y_vrai == y_paresseux).mean()) # 0.99
print(recall_score(y_vrai, y_paresseux, zero_division=0)) # 0.0
99% de acurácia, 0% de recall: o modelo não detecta nenhum doente. Em um rastreamento, é um sistema sem nenhum valor. A acurácia é enganosa assim que as classes são desbalanceadas, o que acontece em quase todos os problemas interessantes — fraude, falha, doença rara, defeito de fabricação.
Vamos comparar com um modelo imperfeito, porém útil:
y_predit = np.zeros(1000, dtype=int)
y_predit[:20] = 1 # 20 pessoas saudáveis sinalizadas por engano
y_predit[990:998] = 1 # 8 doentes em 10 detectados corretamente
print(confusion_matrix(y_vrai, y_predit))
# [[970 20]
# [ 2 8]]
print(round(precision_score(y_vrai, y_predit), 3)) # 0.286
print(round(recall_score(y_vrai, y_predit), 3)) # 0.8
print(round(f1_score(y_vrai, y_predit), 3)) # 0.421
A matriz de confusão se lê assim: 970 saudáveis classificados corretamente, 20 falsos alarmes, 2 doentes não detectados, 8 doentes detectados.
| Métrica | Definição | Valor | Pergunta à qual responde |
|---|---|---|---|
| Acurácia | acertos / total | 0,978 | O modelo erra com frequência, considerando todas as classes? |
| Precisão | 8 / (8 + 20) | 0,286 | Quando ele sinaliza, está certo? |
| Recall | 8 / (8 + 2) | 0,800 | Quantos casos reais ele encontra? |
| F1 | média harmônica | 0,421 | Compromisso único entre as duas anteriores |
Esse modelo tem uma acurácia inferior à do modelo preguiçoso (0,978 contra 0,990) e é incomparavelmente mais útil: ele encontra 8 doentes em 10. O preço são 20 falsos alarmes.
Esse compromisso não tem solução universal. Ele é decidido fora da matemática:
- Rastreamento de uma doença grave e tratável: privilegiar o recall. Um falso alarme custa um exame de controle; um caso não detectado pode custar uma vida.
- Bloqueio automático de uma conta bancária: privilegiar a precisão. Cada falso positivo é um cliente legítimo bloqueado.
É por isso que a métrica deve ser escolhida com a área de negócio antes do treinamento. Otimizar primeiro e discutir depois leva a refazer o trabalho.
Pontos-chave
- Aprender equivale a ajustar parâmetros para minimizar um erro medido sobre exemplos. O que não está nos dados não estará no modelo.
- As três famílias se distinguem pela informação disponível: rótulos (supervisionado), apenas entradas (não supervisionado), recompensas (reforço). O não supervisionado revela uma estrutura, não necessariamente a que você procurava — 16 íris em 150 agrupadas incorretamente servem de lembrete.
- Uma pontuação sobre os dados de treino não mede nada de útil. A árvore com 1,000 no treino e 0,924 no teste é pior do que a de 0,970 / 0,965: isso é overfitting.
- A validação cruzada substitui uma pontuação frágil por uma média e um desvio padrão. É o mínimo antes de anunciar um número a alguém.
- A acurácia é enganosa em classes desbalanceadas: 99% sem detectar nenhum doente. Precisão e recall são equilibrados conforme o custo real de um falso alarme e o de um caso não detectado, decisão que cabe à área de negócio.
Comentários
Os comentários são alimentados pelo GitHub Discussions
Conecte-se com o GitHub para participar da discussão