← Voltar pra playlist

Engenharia de Features e Regressão Polinomial

Meme "Flex Tape": em cima, um homem animado rotulado "EU" ao lado de um pote vazando água com a legenda "MODELO FUNCIONANDO CADA VEZ MENOS PRECISO". Embaixo, uma mão colando fita Flex Tape num vidro rachado que ainda vaza um pouco de água por baixo, rotulada "ENGENHARIA DE FEATURES"

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

fw,b(x)=w0x0+w1x1++wn1xn1+bf_{\mathbf{w},b}(\mathbf{x}) = w_0x_0 + w_1x_1 + \ldots + w_{n-1}x_{n-1} + b

O que torna isso "linear" é a relação entre a saída e os parâmetros w,b\mathbf{w}, b, não a relação entre a saída e a variável original. Nada impede que x1x_1 seja, ele próprio, x2x^2, ou logx\log x, ou o produto de duas outras colunas. Por mais que eu ajuste ww e bb, 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: y=1+x2y = 1 + x^2, pra xx de 0 a 19. Primeiro, a tentativa ingênua: jogar o xx 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 ww e bb 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 xx, entrei com x2x^2 (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 x2x^2. Na prática, você não sabe. Uma estratégia é jogar vários candidatos e deixar o ajuste decidir: testei y=w0x+w1x2+w2x3+by = w_0x + w_1x^2 + w_2x^3 + b contra um alvo puro x2x^2, e o solver zerou os pesos de xx e x3x^3 sozinho, deixando praticamente só o de x2x^2 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:

FeatureCorrelação com y=x2y=x^2
xx0.965
x2x^21.000
x3x^30.986

x2x^2 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: xx vai até 19, x2x^2 até 361, x3x^3 até 6859, uma razão de 361 vezes só entre xx e x2x^2. 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 y=cos(x/2)y = \cos(x/2) com um polinômio de grau até 13:

grau 1 · RMSE de treino ≈ 0.7078

Arrasta até o grau 13: RMSE cai pra 1.1×1051.1\times10^{-5}, um ajuste praticamente perfeito nos 20 pontos de treino. E aqui eu troquei de ferramenta de propósito: em vez de gradiente descendente com α\alpha 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 α\alpha 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:

GrauRMSE treinoRMSE testerazão teste/treino
10.7080.7091.0x
30.3230.4211.3x
50.0430.1413.3x
70.0020.03921.4x
90.0000.00924529x

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 (xx 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 x=22x=22, três unidades depois da borda, o grau 3 já erra por 5.42-5.42 (o valor real é 0.0040.004) enquanto o grau 13 acerta em 0.030.03, quase perfeito. Só que em x=30x=30, o grau 3 errou "só" por 31-31 (ruim, mas crescendo devagar), e o grau 13 já está em 8686, um valor absurdo pra uma função que nunca sai de [1,1][-1,1]. 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á sabiaO que esse post resolveu
Regressão linear só ajusta retaCriando features novas (potências, logs, razões), a mesma regressão ajusta qualquer curva
Mais parâmetros ajustam melhor o que já foi vistoIsso é quase um teorema, e é exatamente por isso que erro de treino não serve pra escolher modelo
Vale de custo é convexoConvexo não impede overfitting: o problema não é a otimização, é o modelo decorar o ruído

Três ideias pra levar:

  1. 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.
  2. 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.
  3. 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.