Engenharia de Features e Regressão Polinomial

Esse é o quarto lab da Semana 2, e o que eu esperava ser só "mais um truque de matemática" virou o post que mais me fez replanejar o que eu ia escrever. A ideia central cabe numa frase, mas as consequências dela (inclusive uma que o próprio lab original nunca menciona) tomaram o post inteiro.
A ideia central: linear nos parâmetros, não na feature
Regressão linear ajusta modelos da forma
O que torna isso "linear" é a relação entre a saída e os parâmetros , não a relação entre a saída e a variável original. Nada impede que seja, ele próprio, , ou , ou o produto de duas outras colunas. Por mais que eu ajuste e , a equação nunca vira uma curva se as features forem retas. Mas se eu criar uma feature curva e jogar ela lá dentro, a mesma maquinaria de sempre ajusta a curva. Isso é engenharia de features.
Features polinomiais na prática
Testei isso com um alvo simples: , pra de 0 a 19. Primeiro, a tentativa ingênua: jogar o cru no modelo.
x = list(range(20))
y = [1 + xi**2 for xi in x]
Arrasta o grau pra 1 no simulador abaixo e repara: nenhum valor de e conserta isso, porque o modelo é uma reta e o alvo é uma parábola. Agora arrasta pra 2:
grau 1 · RMSE de treino ≈ 29.6277
No grau 2 o ajuste vira exato, RMSE zero. O que eu troquei não foi o algoritmo, foi a feature: em vez de , entrei com (e o resto do polinômio) e deixei a mesma regressão linear de sempre encontrar o peso certo.
Escolhendo features, sem saber a resposta de antemão
Ali em cima eu já sabia que o termo certo era . Na prática, você não sabe. Uma estratégia é jogar vários candidatos e deixar o ajuste decidir: testei contra um alvo puro , e o solver zerou os pesos de e sozinho, deixando praticamente só o de de pé. (O notebook original faz essa mesma conta com gradiente descendente e só consegue reduzir os pesos errados, não zerá-los, porque a convergência nunca termina de verdade. Com solução exata, o resultado sai limpo.)
Outra forma de pensar: depois de criar as features, eu continuo fazendo regressão linear. Então a melhor feature é a que tem relação linear com o alvo. Isso vira uma correlação de Pearson fácil de calcular:
| Feature | Correlação com |
|---|---|
| 0.965 | |
| 1.000 | |
| 0.986 |
tem correlação perfeita porque ela é o alvo, a menos de escala. Já falei dessa mesma heurística (plotar feature contra alvo e procurar a reta) no post de normalização, é a mesma ideia, só que agora aplicada pra escolher a forma certa da feature, não só sua escala.
Normalizando, de novo (versão mais extrema)
Features polinomiais são o caso mais extremo de escalas diferentes que eu vi até agora: vai até 19, até 361, até 6859, uma razão de 361 vezes só entre e . Mesmo truque do post anterior resolve: z-score em cada coluna, calculado só com o treino. O componente que eu construí pra esse post já normaliza por baixo dos panos antes de resolver, exatamente como fiz lá.
Uma função de verdade complicada
Com feature engineering dá pra modelar coisas bem mais malucas que uma parábola. Testei com um polinômio de grau até 13:
grau 1 · RMSE de treino ≈ 0.7078
Arrasta até o grau 13: RMSE cai pra , um ajuste praticamente perfeito nos 20 pontos de treino. E aqui eu troquei de ferramenta de propósito: em vez de gradiente descendente com ajustado na mão pra cada grau (o que o notebook original faz, e dá bastante trabalho), esse componente resolve por equação normal, a mesma solução exata que eu já usei pra conferir resultado no post anterior. Isso deixa explorar vários graus instantâneo, sem caçar de novo, e sobra atenção pro que importa de verdade aqui: o que acontece quando o modelo fica flexível demais.
O elefante na sala: overfitting
Acabei de ajustar 14 parâmetros (13 pesos + o viés) a 20 pontos de dado. O ajuste ficou lindo. Isso deveria acender um alerta, não uma comemoração, e é exatamente o que o lab original nunca faz.
Primeira surpresa: sem ruído, flexibilidade não é pecado
Separei os 20 pontos em 10 de treino (índices pares) e 10 de teste (índices ímpares), sem nenhum ruído nos dados:
| Grau | RMSE treino | RMSE teste | razão teste/treino |
|---|---|---|---|
| 1 | 0.708 | 0.709 | 1.0x |
| 3 | 0.323 | 0.421 | 1.3x |
| 5 | 0.043 | 0.141 | 3.3x |
| 7 | 0.002 | 0.039 | 21.4x |
| 9 | 0.000 | 0.009 | 24529x |
Reparei numa coisa: no grau 9 (10 parâmetros pra 10 pontos de treino, o limite exato de solução única), a razão teste/treino parece uma catástrofe (24529 vezes!), mas o erro de teste em número absoluto continua pequeno (0.009). Isso contraria o slogan de "muitos parâmetros sempre causam overfitting". A afirmação certa é mais sutil: overfitting é o modelo ajustar o ruído, não simplesmente ter muitos parâmetros. Sem ruído nenhum pra ajustar, um modelo flexível interpola bem, mesmo no limite extremo.
Com ruído, o fenômeno aparece de verdade
Dado real sempre tem ruído. Adicionei um ruído modesto (desvio-padrão 0.15) e refiz o teste, agora comparando vários graus de uma vez:
O erro de treino só cai (mais parâmetros sempre ajustam melhor o que já foi visto, isso é praticamente um teorema, não uma coincidência). O erro de teste cai, atinge um mínimo por volta do grau 6, e depois sobe forte, chegando a 1.72 no grau 9, pior que um ajuste de grau 1. É esse mínimo que interessa, não o grau que zera o erro de treino.
Testa você mesmo, só com os 10 pontos de treino ruidosos (arrasta o grau e observa o RMSE de teste ao vivo, calculado nos 10 pontos que o ajuste nunca viu):
grau 1 · RMSE de treino ≈ 0.7448 · RMSE de teste ≈ 0.7491
Extrapolação: onde fica realmente perigoso
Tudo até aqui foi dentro da faixa de treino ( de 0 a 19). Fora dela, o comportamento me surpreendeu, e não do jeito que eu esperava. Arrasta o grau pra 13 no simulador abaixo (a faixa verde marca onde os dados de treino realmente estavam) e repara: logo depois da borda, o ajuste de grau 13 continua melhor que um grau baixo por alguns pontos, porque ele aprendeu o formato da curva com muita precisão bem na borda. A armadilha é justamente essa falsa confiança: continua arrastando o eixo pra frente, e o mesmo grau 13 que parecia seguro dispara pro infinito muito mais rápido que qualquer grau baixo.
grau 3 · RMSE de treino ≈ 0.3490
Calculei os números pra confirmar o que o olho vê: em , três unidades depois da borda, o grau 3 já erra por (o valor real é ) enquanto o grau 13 acerta em , quase perfeito. Só que em , o grau 3 errou "só" por (ruim, mas crescendo devagar), e o grau 13 já está em , um valor absurdo pra uma função que nunca sai de . Regra prática que eu levo comigo: nunca confio em previsão polinomial fora do intervalo de treino, principalmente quanto maior o grau, porque o desastre não é imediato, é traiçoeiro.
Fechando
| O que eu já sabia | O que esse post resolveu |
|---|---|
| Regressão linear só ajusta reta | Criando features novas (potências, logs, razões), a mesma regressão ajusta qualquer curva |
| Mais parâmetros ajustam melhor o que já foi visto | Isso é quase um teorema, e é exatamente por isso que erro de treino não serve pra escolher modelo |
| Vale de custo é convexo | Convexo não impede overfitting: o problema não é a otimização, é o modelo decorar o ruído |
Três ideias pra levar:
- Engenharia de features não muda o algoritmo, muda os dados que entram nele. A mesma regressão linear aprende qualquer curva se você der a ela a feature certa.
- Overfitting é sobre ruído, não sobre contagem de parâmetro: um modelo flexível sem ruído pra ajustar generaliza bem, mesmo no limite. O perigo aparece quando existe ruído pra decorar.
- Extrapolação com polinômio de grau alto é traiçoeira: pode parecer melhor que um grau baixo logo depois da borda dos dados, e ainda assim explodir muito mais forte um pouco mais além.
Ainda sobra uma pergunta: dá pra usar toda a flexibilidade de um grau alto sem pagar o preço do overfitting? Existe uma resposta com um botão contínuo em vez da escolha discreta de grau, chamada regularização, e ela é o assunto do próximo post.
Aplicação prática
Mesmo dataset real de 50 casas dos posts anteriores. No post bônus de normalização eu já tinha deixado a promessa: "engenharia de features é o tema do próximo lab". Cumprindo: criei a feature tamanho_por_quarto = square_feet / num_bedrooms e comparei o ajuste com e sem ela.
tamanho_por_quarto = [s / b for s, b in zip(square_feet, num_bedrooms)]
rmse_sem = ajusta_avalia([square_feet, num_bedrooms, nota_localizacao, distancia_centro], preco)
rmse_com = ajusta_avalia([square_feet, num_bedrooms, nota_localizacao, distancia_centro, tamanho_por_quarto], preco)
Saída:
RMSE sem a feature nova: 67.25/RMSE com a feature nova: 65.13(em mil dólares)
Uma melhora real, mas modesta (cerca de 3%). Curioso: a correlação direta entre tamanho_por_quarto e o preço é quase nula (0.0069, bem pertinho de zero), então se eu só olhasse a correlação isolada eu descartaria essa feature. Ela só ajuda quando entra junto com as outras, o mesmo tipo de efeito escondido que eu já vi com o coeficiente de quartos no post anterior.