Só pra complementar: Pegadinhas do Scikit-Learn
Esse post não vem de nenhuma seção obrigatória do lab, é o que sobrou depois que separei o essencial de scikit-learn e gradiente estocástico no post principal. São as armadilhas que só aparecem quando o código sai do notebook educacional e vai pra um projeto de verdade.
fit_transform e transform não são a mesma coisa
Esse é o ponto onde mais gente escorrega. fit_transform aprende e aplica. transform só aplica o que já foi aprendido. Nos dados de treino, fit_transform. Em qualquer outro dado (validação, teste, produção), transform.
Separei o dataset de 100 casas em 75 treino / 25 teste e comparei a média do tamanho calculada dos dois jeitos:
mu_certo = tamanho[treino].mean() # certo: só o treino
mu_errado = tamanho[teste].mean() # errado: se eu recalculasse no teste
Saída: média do treino = 1449.93, média do teste = 1305.04, diferença de 144.89
Se eu chamasse fit_transform no teste em vez de transform, o scaler passaria a usar 1305.04 em vez de 1449.93, uma diferença grande o bastante pra distorcer qualquer previsão feita depois. E aí a métrica de validação fica otimista demais sem eu perceber, porque o modelo "viu" estatística de um dado que deveria ser desconhecido.
SGD sem normalizar simplesmente explode
Diferente do LinearRegression, aqui normalizar não é opcional. Com features de escalas muito diferentes, o passo padrão vira um passo gigante na direção da feature de maior escala. Rodei gradiente estocástico direto nos dados crus (sem normalizar), no tamanho da casa em pés²:
| Passos | Resultado | |
|---|---|---|
| 43 | diverge, custo explode pra | |
| 200 | estável, mas ainda longe do mínimo (custo ) |
O mesmo fenômeno de escala que eu já vi com gradiente batch nos posts anteriores, só que agora no SGD. A diferença é que aqui normalizar não é só "recomendado", é praticamente obrigatório, e o motivo é que o SGD já chega numa situação mais frágil antes mesmo de a escala entrar em cena: cada passo usa o gradiente de um único exemplo, não a média suavizada de todo o dataset como no batch, então o passo já pula de um jeito barulhento de exemplo pra exemplo. Multiplica esse gradiente barulhento por uma feature de escala gigante (tamanho da casa em pés², na casa dos milhares) e o tamanho do passo passa a variar violentamente de uma iteração pra outra, sem a média do batch pra absorver esse solavanco. É a mesma direção errada de sempre, só que sem o amortecedor que o batch tinha de graça.
Pipeline e validação cruzada: o jeito profissional
Toda métrica que eu mostrei no post principal foi medida no treino, ela diz o quanto o modelo decorou, não o quanto ele generaliza. E tem um segundo problema: se eu normalizar uma vez e depois fizer validação cruzada, o scaler já viu os dados de todos os folds. Vazamento de novo, só que mais sutil.
Pipeline resolve os dois: encadeia normalização e modelo num único estimador, e a cada ajuste (inclusive dentro de cada fold da validação cruzada) o scaler é reajustado só com a porção de treino daquele fold.
Rodei validação cruzada de 5 folds de verdade, comparando LinearRegression e Ridge(alpha=1):
| Modelo | R² por fold | Média ± desvio |
|---|---|---|
LinearRegression | 0.955, 0.952, 0.915, 0.944, 0.96 | 0.9450 ± 0.0160 |
Ridge(alpha=1) | 0.953, 0.954, 0.919, 0.942, 0.955 | 0.9444 ± 0.0137 |
Repara que a média de validação (0.945) é menor que o R² de treino que eu citei no post principal (0.9594), exatamente como esperado: validação sempre é mais honesta que treino. E os dois modelos ficam praticamente empatados, com 100 exemplos e 4 features não sobra capacidade demais pra Ridge precisar segurar.
As outras pegadinhas, em lista
intercept_é um array noSGDRegressore um escalar noLinearRegression. Código que assume um dos dois formatos quebra no outro.max_iteré um teto, não uma meta (já vimos no post principal). Sempre confiran_iter_.penalty='l2'vem ligado por padrão noSGDRegressor(post principal). Pra mínimos quadrados puros, passepenalty=None.random_statenão é opcional se você quer reproduzir resultado.ConvergenceWarningsignifica que o modelo bateu nomax_itersem satisfazer otol. Não ignora: aumentamax_iter, ajusta a taxa de aprendizado, ou confere se normalizou.- Normalizar o alvo
ynão é feito automaticamente por nada disso. Seytiver magnitude extrema, considere normalizar, e lembre de desnormalizar as previsões antes de reportar qualquer métrica. fitreinicia o modelo do zero. Pra treino incremental, com dado chegando aos poucos (em vez de tudo de uma vez), existepartial_fit: o modelo se atualiza sem nunca precisar ver o dataset inteiro junto. É o caso de uso real por trás de "dado que não cabe na memória" que mencionei no post principal, e é literalmente o que o simulador estocástico do post principal já mostra: cada passo enxerga uma casa só, nunca o dataset inteiro de uma vez, e ainda assim o modelo caminha pro mesmo lugar que o batch.
Exercícios
Tenta antes de abrir a resposta.
Exercício 1: reproduza o normalizador
Implementa uma versão sua de normalização por z-score com os métodos fit, transform e fit_transform, seguindo a convenção de atributos com underscore no fim. Confere que bate com o resultado do post de normalização.
Resposta
class MeuScaler:
def fit(self, X):
self.mean_ = X.mean(axis=0)
self.scale_ = X.std(axis=0)
self.scale_[self.scale_ == 0] = 1.0
return self
def transform(self, X):
return (X - self.mean_) / self.scale_
def fit_transform(self, X):
return self.fit(X).transform(X)
Devolver self no fit é o que permite encadear MeuScaler().fit(X).transform(X), o mesmo padrão que todo transformador do scikit-learn segue.
Exercício 2: o efeito da taxa de aprendizado no SGD
Roda o gradiente estocástico com taxa constante em vários valores. O que acontece nos dois extremos?
Resposta
Com taxa pequena demais, o modelo não chega perto do mínimo dentro do número de passos disponível. Com taxa grande demais, diverge, igual eu mostrei nesse post com os dados crus. Uma taxa que decresce ao longo do treino (o padrão do scikit-learn) existe justamente pra não precisar acertar isso na mão: começa maior, dá passo grande no início quando ainda tá longe, e encolhe conforme se aproxima.
Exercício 3: quanto o Ridge escondido está custando?
Compara penalty=None com vários valores de usando validação cruzada. Em que ponto a regularização começa a atrapalhar? E se o dataset tivesse só 20 exemplos em vez de 100?
Resposta
Com 100 exemplos e 4 features não há capacidade sobrando, então qualquer grande só atrapalha. Com 20 exemplos a história muda: a razão exemplos/parâmetros fica apertada, e um moderado passa a ajudar de verdade. A lição de sempre: regularização não é boa nem ruim em si, é resposta a um excesso de capacidade que pode ou não existir no seu dado.
Exercício 4: validação cruzada com e sem Pipeline
Compara o R² de validação cruzada em dois cenários: normalizando uma vez antes e passando o resultado pra validação cruzada, contra usando um Pipeline. Os números batem?
Resposta
Nesse dataset a diferença é pequena, porque os folds são parecidos entre si. Mas o primeiro cenário está conceitualmente errado: o normalizador viu os dados de validação. Em dataset pequeno, com outlier, ou com estrutura temporal, essa diferença deixa de ser pequena. Usa Pipeline sempre, não porque o número muda muito hoje, mas porque ele torna o erro impossível de cometer sem querer.
Exercício 5: o modelo generaliza pra casas fora da faixa?
Separa as 20 casas mais caras como teste e treina nas 80 restantes. O que acontece com o R² de teste?
Resposta
O R² despenca, e pode até ficar negativo. O motivo não é overfitting, é que a divisão não é aleatória: o teste ficou sistematicamente diferente do treino (extrapolação, o mesmo problema que eu vi com polinômio no post de engenharia de features). Divisão aleatória supõe que os dados são intercambiáveis, quando não são (tempo, geografia, faixa de preço), a divisão precisa respeitar isso.