0%
Machine Learning: os fundamentos

Machine Learning

Como uma máquina ajusta uma regra a partir de exemplos.

10-15 min

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:

TermoSignificadoNo exemplo
Variável explicativa (feature)o que fornecemos como entradaa área
Alvo (target, label)o que queremos prevero aluguel
Parâmetrosos números ajustados pelo aprendizado12,87 e 220
Hiperparâmetrosos ajustes definidos pelo humano antes do aprendizadoaqui 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áximaAcurácia no treinoAcurácia no testeDiferença
10,9250,8950,030
30,9700,9650,005
ilimitada1,0000,9240,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étricaDefiniçãoValorPergunta à qual responde
Acuráciaacertos / total0,978O modelo erra com frequência, considerando todas as classes?
Precisão8 / (8 + 20)0,286Quando ele sinaliza, está certo?
Recall8 / (8 + 2)0,800Quantos casos reais ele encontra?
F1média harmônica0,421Compromisso ú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

Link copiado!