Só pra complementar: Pegadinhas de Engenharia de Features
Esse post não vem de nenhuma seção obrigatória do lab, é o que sobrou depois que eu separei o essencial de engenharia de features e overfitting no post principal. Curiosamente, essa é a primeira vez que eu consigo mostrar de verdade a solução pro problema que o post anterior só diagnosticou.
Normalizo depois de criar a feature, não antes
A ordem importa. Se eu normalizasse primeiro e só depois elevasse ao quadrado, chegaria em , que não é a mesma feature que normalizar direto. As duas funcionam, mas produzem colunas diferentes, e a segunda ordem (criar a feature, normalizar depois) é a que eu uso desde o post principal.
Já adianto o final feliz: o RMSE das duas ordens acaba praticamente igual, então relaxa, não é um erro grave se você inverter. O motivo é que dá pra abrir em , que é só uma combinação linear de , e uma constante, três coisas que o modelo já tem à disposição (o próprio e o viés ). Ou seja, normalizar antes ou depois não muda o que o modelo consegue representar, só reescreve a mesma função numa base de coordenadas diferente. O que muda de verdade é a escala dos pesos e a velocidade com que o gradiente descendente converge até eles, não o resultado final. (Detalho a conta inteira no Exercício 2, lá embaixo, se quiser ver isso na régua.)
Features polinomiais são extremamente colineares
, e carregam quase a mesma informação num intervalo estreito. Isso deixa a matriz mal condicionada. Calculei a razão entre a amplitude cru da maior e da menor coluna por grau:
| Grau | Amplitude máx/mín (cru) |
|---|---|
| 1 | 1 |
| 3 | 361 |
| 5 | 1.3 × 10⁵ |
| 9 | 1.7 × 10¹⁰ |
| 13 | 2.2 × 10¹⁵ |
No grau 13 essa razão já está perto do limite de precisão do ponto flutuante de 64 bits (~2.2×10⁻¹⁶ de precisão relativa). Não é exagero dizer que a matriz crua nesse grau está no limiar da singularidade numérica.
E "mal condicionada" não é só um adjetivo assustador, tem uma consequência bem concreta pro peso que sai do ajuste. Resolver a equação normal envolve, no fundo, inverter essa matriz, e quando ela está quase singular, essa inversão se parece muito com dividir por um número quase zero: um tremelique pequeno no numerador (um pouquinho de ruído nos dados, um arredondamento de ponto flutuante) vira uma resposta gigante e instável do outro lado. Na prática, isso significa que dois conjuntos de dados quase idênticos podem produzir pesos completamente diferentes, às vezes com sinais opostos se cancelando quase perfeitamente, mesmo que a curva final prevista pareça igualzinha. Os pesos individuais deixam de significar algo confiável, só a combinação deles ainda presta.
Normalizar (o que meu componente do post principal já faz por baixo dos panos) melhora isso em várias ordens de grandeza, mas não resolve a colinearidade intrínseca entre as potências, só a diferença de escala entre elas. Pra colinearidade de verdade, o remédio é regularização, que eu mostro mais embaixo.
Guardo e , sempre
Bati nessa tecla no post de normalização e bato de novo aqui: sem guardar a média e o desvio-padrão do treino, não dá pra normalizar nenhum dado novo do mesmo jeito, e o modelo vira enfeite. Todo componente que eu construí pra esse post guarda isso internamente antes de prever qualquer coisa.
Mais features nunca piora o erro de treino
Isso não é coincidência, é praticamente um teorema: acrescentar uma coluna só amplia o espaço de modelos possíveis, então o mínimo do custo de treino só pode cair ou empatar, nunca subir. Por isso erro de treino sozinho não serve pra escolher a complexidade do modelo, sempre precisa de um conjunto separado (o que eu já mostrei no post principal).
Engenharia de feature não é só potência
Potência é o exemplo didático mais fácil de mostrar. No mundo real, as features mais úteis costumam vir de conhecimento de domínio: raiz quadrada, log, razões, produtos entre colunas. Testei isso com um alvo :
| Features | Nº de parâmetros | RMSE |
|---|---|---|
| 2 | 0.300 | |
| 3 | 0.163 | |
| 4 | 0.104 | |
| 2 | 0.000 |
Uma única feature zera o erro com 2 parâmetros, enquanto três potências gastam 4 parâmetros e ainda erram. Conhecimento de domínio vale mais que força bruta.
Domando o overfitting: regularização
O post principal terminou com uma pergunta em aberto: dá pra manter toda a flexibilidade de um grau alto sem pagar o preço do overfitting? A resposta é regularização L2, ou Ridge. Em vez de escolher um grau discreto, eu adiciono uma penalidade contínua que castiga pesos grandes:
Por que isso funciona, na prática? Um polinômio de grau alto decorando 10 pontos de treino precisa, em algum lugar da curva, subir muito rápido e descer muito rápido pra passar exatamente em cima de cada ponto ruidoso, e isso só é possível se alguns pesos ficarem enormes (frequentemente com sinais opostos se cancelando na maior parte do domínio, e "descancelando" bem na hora de fazer aquela curva colar num ponto específico). Se eu simplesmente proíbo os pesos de ficarem grandes demais, cobrando um preço () proporcional ao tamanho deles ao quadrado, a curva perde a capacidade física de fazer essas curvas fechadas em volta de cada ponto individual. Ela é forçada a "escolher" uma tendência mais suave que passa perto da maioria dos pontos, em vez de exatamente em cima de cada um, o que é outra forma de dizer: ela para de decorar o ruído e passa a acompanhar o sinal.
Implementei isso na minha própria solução exata (somando à diagonal de , excluindo o viés, que é exatamente a forma fechada que o Ridge do scikit-learn resolve por baixo dos panos). Não tenho scikit-learn instalado nesse ambiente, mas aqui está o equivalente calculado de verdade, não só o princípio: mesmo caso ruidoso de 10 pontos, grau 9, do post principal, variando :
| RMSE treino | RMSE teste | |
|---|---|---|
| 0 (sem regularização) | 0.000 | 1.717 |
| 0.01 | 0.281 | 0.296 |
| 0.1 | 0.478 | 0.646 |
| 0.5 | 0.587 | 0.652 |
| 1.0 | 0.631 | 0.650 |
| 2.0 | 0.666 | 0.658 |
| 5.0 | 0.696 | 0.674 |
Repara no padrão: sem regularização o treino vai a zero (10 parâmetros pra 10 pontos, ajuste exato) e o teste é o pior de todos. Conforme cresce, o treino piora e o teste melhora, até um ponto ( aqui) onde o teste passa a piorar de novo porque o modelo já ficou rígido demais. É o mesmo trade-off viés-variância do post principal, só que controlado por um botão contínuo em vez da escolha discreta de grau.
Testa você mesmo, arrastando :
RMSE de treino ≈ 0.0000 · RMSE de teste ≈ 1.7168
Repara como a curva para de decorar cada ponto de treino e vira parecida com o formato verdadeiro assim que sai de zero, sem eu precisar mexer no grau.
Exercícios
Tenta antes de abrir a resposta.
Exercício 1: descubra a transformação
Gere y = 3 * log(x + 1) + 2 pra x de 0 a 19. Ajusta (a) só com x, (b) com x, x², x³ e (c) com log(x+1). Compara os RMSE.
Resposta
A feature log(x+1) acerta praticamente na mosca com um único peso, porque o alvo é linear nela por construção. O polinômio de grau 3 chega perto (todo polinômio aproxima o log num intervalo limitado), mas gasta 3 parâmetros e erra fora da faixa. Só x fica ruim. A moral: quando você conhece a forma da relação, a feature certa vale mais que qualquer quantidade de potências.
Exercício 2: a ordem da normalização importa?
Compara ajustar zscore(x)² contra zscore(x²). Os RMSE finais são iguais? E os pesos?
Resposta
O RMSE final é praticamente igual, mas os pesos e a velocidade de convergência não são. O motivo: , ou seja, é uma combinação linear de , e uma constante. Como o modelo já tem e o viés disponíveis, o espaço de funções alcançáveis é o mesmo, muda só a base em que ele está escrito.
Exercício 3: quantos pontos são necessários?
Mantendo o grau 13, repete a análise treino/teste com 40, 100 e 500 pontos em . O overfitting some?
Resposta
Some em grande medida. Overfitting é uma relação entre capacidade do modelo e quantidade de dados, não uma propriedade do modelo sozinho. Com 500 pontos, 14 parâmetros deixam de ser demais. Isso dá as três saídas clássicas pra overfitting: mais dados, menos features, ou regularização.
Exercício 4: interações entre variáveis
Com duas variáveis de entrada, features polinomiais de grau 2 geram x1, x2, x1², x1x2, x2². O termo x1x2 é uma interação. Constrói um alvo em que a interação seja essencial (y = x1 * x2) e mostra que um modelo sem ela falha.
Resposta
Sem o termo cruzado, o modelo é aditivo e não consegue representar "o efeito de x1 depende de x2". Interações são a forma mais comum e mais útil de engenharia de features em dados tabulares reais, mais até que potências puras.
Exercício 5: critério de parada honesto
Se eu tivesse usado gradiente descendente em vez de solução exata, como eu pararia de forma confiável?
Resposta
Pela norma do gradiente (max(|dj_dw|, |dj_db|) < tol), não pela melhora do custo. Com pequeno demais, o custo também melhora pouco por iteração, e um critério baseado nele faria eu parar achando que convergi sem ter convergido de verdade.