Regressão Linear com Scikit-Learn

Depois de quatro posts implementando gradiente descendente, normalização e engenharia de features inteiramente na mão, chegou a hora de usar a ferramenta de verdade. E o primeiro choque foi descobrir que o modelo "padrão" que o curso usa aqui não é bem o que parece.
A convenção da API, mais importante que o modelo em si
O scikit-learn traz implementação pronta e testada de boa parte do que eu já fiz na mão. Mas o que mais importa aprender aqui não é o modelo específico, é a convenção de API, porque ela se repete idêntica em centenas de modelos e transformadores:
| Método | O que faz | Quem tem |
|---|---|---|
.fit(X, y) | aprende os parâmetros a partir dos dados | todo estimador |
.predict(X) | usa os parâmetros aprendidos pra prever | modelos preditivos |
.transform(X) | aplica uma transformação já aprendida | transformadores |
.fit_transform(X) | atalho pra fit seguido de transform | transformadores |
.score(X, y) | métrica padrão do estimador (R² em regressão) | quase todos |
Atributos aprendidos terminam com underscore: coef_, intercept_, mean_, scale_, n_iter_. Esse underscore no fim é a convenção que distingue "aprendido com os dados" de "configurado por mim".
A regra de ouro que vem junto: fit só pode ver dado de treino. Em dado de teste, só transform e predict. Violar isso é vazamento de dado, e é o erro mais comum de quem tá começando (já bati nessa tecla nos dois posts bônus anteriores).
StandardScaler: o z-score que eu já fiz na mão
StandardScaler faz exatamente a conta que eu implementei no post de normalização: , coluna a coluna. Conferi e bate dígito a dígito com o meu z-score manual, inclusive usando a mesma convenção de desvio-padrão populacional (dividir por , não por ) que eu já usava.
SGDRegressor e o que o "S" significa
O curso usa SGDRegressor sem nunca explicar a sigla. Vale parar nisso, porque é a diferença real entre esse modelo e o gradiente descendente que eu implementei nos posts anteriores.
Gradiente descendente batch (o que eu fiz até aqui): cada passo usa todos os exemplos pra calcular o gradiente.
Gradiente descendente estocástico (SGD): cada passo usa um único exemplo, escolhido aleatoriamente.
Passo baratíssimo, direção ruidosa, trajetória em zigue-zague, mas com atualizações pelo preço de uma do batch. Construí um motor de SGD do zero (o mesmo GradientDescentSimulator de sempre, só que trocando a engrenagem por baixo) pra você ver a diferença ao vivo, no mesmo dataset de 8 casas do post de normalização:
Batch:
iteração 0 · w = 0.00 · b = 0.00 · custo J = 88073.08
Estocástico:
iteração 0 · w = 0.00 · b = 0.00 · custo J = 88073.08
Roda "Rodar 100" nos dois com o mesmo alpha padrão. Depois de 50 passos, o batch já está com custo perto de 922 (bem próximo do mínimo real, 919), enquanto o estocástico, com o mesmo número de passos mas cada um enxergando só uma casa por vez, ainda está balançando em 7102. O caminho do estocástico no gráfico também é visivelmente mais "sujo", vai e volta em vez de descer suave.
Uma passagem completa pelos exemplos se chama época. No scikit-learn, max_iter conta épocas, não atualizações individuais.
| Batch GD | SGD | |
|---|---|---|
| custo por atualização | ||
| atualizações por época | 1 | |
| trajetória | suave | ruidosa |
| determinístico? | sim | não (depende da ordem sorteada) |
| bom quando | pequeno/médio | muito grande |
Com taxa de aprendizado constante, o SGD nunca para de tremer em torno do mínimo de vez. É por isso que o scikit-learn usa, por padrão, uma taxa que decresce ao longo do treino.
Duas coisas que o max_iter esconde
Peço max_iter=1000 e o SGDRegressor normalmente para bem antes disso. Não é bug: existe parada antecipada (tol e n_iter_no_change) que detecta quando a perda parou de melhorar de verdade e encerra sozinho. max_iter é um teto, não uma meta.
E sem fixar random_state, cada fit sorteia uma ordem diferente pros exemplos, e o resultado muda de execução pra execução. A variação costuma ser pequena, mas é real, principalmente em dataset menor ou mais difícil. Fixar a semente é o que torna o resultado reproduzível.
A surpresa: por padrão, isso é Ridge, não mínimos quadrados puros
Essa é a que mais me pegou de surpresa. Os padrões do SGDRegressor são penalty='l2' e alpha=0.0001. Ou seja, por padrão ele não minimiza
e sim
O termo extra é exatamente a regularização L2 que eu implementei do zero no post bônus anterior. Com (o padrão) o efeito é pequeno demais pra notar neste dataset, mas o mecanismo é real. Ajustei com (bem mais forte, só pra deixar visível) no nosso dataset de 100 casas:
| Modelo | RMSE | Norma de |
|---|---|---|
| Sem regularização | 20.96 | 123.25 |
| Ridge () | 21.04 | 120.39 |
O peso encolhe, o erro de treino piora um pouquinho, exatamente o trade-off que eu já vi no post de Ridge. Conheça os padrões da biblioteca que você usa: um SGDRegressor() chamado sem argumento nenhum não é "regressão linear crua", é Ridge com um discreto e pequeno.
Prevendo, e um jeito frágil de comparar
Duas contas matematicamente equivalentes podem diferir no último bit de precisão por causa da ordem das operações de ponto flutuante. Comparar previsões com == é frágil (0.1 + 0.2 == 0.3 é false em qualquer linguagem que usa ponto flutuante de 64 bits, pelo mesmo motivo). O jeito certo é conferir se a diferença fica dentro de uma tolerância pequena, não exigir igualdade exata.
Métricas: quão bom é esse modelo, de verdade
Até aqui eu tinha usado custo pra treinar, mas nunca uma métrica pensada pra ser lida por gente. Ajustei o modelo com as 4 features no dataset completo de 100 casas e calculei três métricas padrão:
| Métrica | Valor | O que mede |
|---|---|---|
| RMSE | 20.96 mil US$ | erro típico, pune erro grande desproporcionalmente |
| MAE | 16.91 mil US$ | erro absoluto médio, mais robusto a outlier |
| R² | 0.9594 | fração da variância do preço explicada pelo modelo |
Pra ter uma referência: um modelo "burro" que sempre chuta a média erra com RMSE de 104.07. O nosso erra 4.96 vezes menos. É esse o ganho real de ter um modelo, não só o número de R² sozinho.
SGDRegressor ou LinearRegression? A escolha de verdade
O curso apresenta o SGDRegressor como "a regressão linear do scikit-learn", mas na prática ela é a escolha menos comum. LinearRegression resolve a equação normal em forma fechada, , sem iteração, sem taxa de aprendizado, sem semente aleatória. O custo cresce com o cubo do número de features, . SGDRegressor itera, custa por época, e nunca precisa carregar tudo na memória de uma vez.
| Critério | LinearRegression | SGDRegressor |
|---|---|---|
| exatidão | solução exata | aproximada |
| determinismo | total | depende da semente |
| precisa normalizar? | não | sim, obrigatoriamente |
| de features enorme | fica caro () | tudo bem |
| de exemplos gigante | precisa caber na memória | escala bem, aceita partial_fit |
| aprendizado incremental | não | sim |
Regra prática: uso LinearRegression (ou Ridge) por padrão. Só vou pro SGDRegressor quando o dado não cabe na memória, chega em fluxo, ou o número de features é gigante.
Fechando
| O que eu já sabia | O que esse post resolveu |
|---|---|
| Gradiente descendente usa todo o dataset a cada passo | Existe uma versão que usa um exemplo por vez, mais barata por passo, mais ruidosa |
| Eu implementei tudo isso na mão até aqui | A biblioteca padrão da área faz a mesma coisa, com a mesma convenção de API em centenas de modelos |
| Ridge é uma escolha explícita que eu fiz | O modelo "padrão" de gradiente estocástico do scikit-learn já vem com Ridge ligado, sem avisar |
Três ideias pra levar:
- A convenção
fit/predict/transform/scorevale mais que decorar um modelo específico, ela se repete em toda a biblioteca. - SGD troca precisão por velocidade por passo: mesma direção geral, caminho mais barato e mais ruidoso.
- Conheça os padrões do que você usa:
penalty='l2'ligado por padrão noSGDRegressoré o tipo de detalhe que muda o que o seu código realmente está fazendo.
Aplicação prática
Mesmo dataset real de imóveis dos posts anteriores. Já sei que o gradiente descendente batch, com square_feet/100 e price/1000, alpha=0.01, precisa de 4000 iterações completas pra chegar em , custo final 5189.72. Cada uma dessas 4000 iterações olha as 50 casas inteiras, então são 200 mil avaliações de exemplo ao todo.
Rodei o estocástico com o mesmo alpha, só que contando passos individuais em vez de iterações completas:
w, b, hist = sgd_gradient_descent(
square_feet_norm, price,
w_in=0, b_in=0,
alpha=0.01, num_steps=4000) # 4000 exemplos vistos, não 4000 passagens completas
print(f"(w, b) encontrados: ({w:.1f}, {b:.1f})")
Saída:
(w, b) encontrados: (112.1, 396.9), custo final 5233.79
Praticamente o mesmo resultado do batch (custo 5233.79 contra 5189.72), só que usando 50 vezes menos avaliações de exemplo (4000 contra 200 mil). Compara os dois ao vivo:
Carregando dados reais...
Carregando dados reais...
Roda "Rodar 2000" em cada um e repara: o estocástico chega numa vizinhança boa bem mais rápido em termos de trabalho total, mesmo com o caminho mais bagunçado no gráfico.