Machine Learning: Avaliação e Ajuste de Modelos
Última atualização: 2026-08-26
Se sua metodologia de avaliação está errada, toda a otimização é construída sobre areia — meça com precisão primeiro, depois ajuste.
1. O que você vai aprender
- Metodologia de avaliação: Hold-out / K-Fold / Stratified K-Fold / TimeSeriesSplit
- Avaliação de classificação: matriz de confusão, curvas Precision-Recall, F1-β, métricas multiclasse
- Avaliação de regressão: MAE / MSE / RMSE / MAPE / R², alinhando métricas com objetivos de negócio
- Ajuste de hiperparâmetros: GridSearchCV / RandomizedSearchCV / otimização bayesiana com Optuna
- Diagnóstico de sobreajuste: curvas de aprendizado, curvas de validação
2. A história real de um engenheiro de ML
(1) O problema: 95% de acurácia no conjunto de teste, 75% após a implantação
Bob avaliou seu modelo com train_test_split e obteve uma acurácia de teste de 95%. Mas uma semana após a implantação, a acurácia no mundo real era de apenas 75% — porque com dados de séries temporais, as distribuições de treino e teste se sobrepunham, e o modelo havia "espiado" informações futuras. Uma metodologia de avaliação falha levou a métricas infladas e um lançamento desastroso.
(2) A solução: metodologia de avaliação adequada
Dados de séries temporais devem usar TimeSeriesSplit — o conjunto de treino contém apenas dados passados, e o conjunto de teste é dado futuro, simulando condições do mundo real.
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(X):
# train_idx sempre < test_idx (sem vazamento de futuro)
model.fit(X[train_idx], y[train_idx])
score = model.score(X[test_idx], y[test_idx])
(3) O resultado: métricas que refletem o desempenho real
Bob reavaliou com TimeSeriesSplit, e a acurácia caiu de 95% "inflado" para 82% "realista". O número diminuiu, mas a acurácia pós-implantação também foi de 82% — avaliação e realidade finalmente bateram, sem mais surpresas desagradáveis.
3. Metodologia de Avaliação
(1) Quatro estratégias de validação cruzada
graph TB
DATA[Dataset] --> HO[Hold-out<br/>Divisão Única Treino/Teste]
DATA --> KF[K-Fold CV<br/>Divisões Aleatórias]
DATA --> SKF[Stratified K-Fold<br/>Divisões Balanceadas por Classe]
DATA --> TSS[TimeSeriesSplit<br/>Divisões Cronológicas]
HO --> HO_USE[Baseline rápido<br/>Dataset grande]
KF --> KF_USE[Uso geral<br/>Estimativa estável]
SKF --> SKF_USE[Classificação<br/>Dados desbalanceados]
TSS --> TSS_USE[Série temporal<br/>Sem vazamento de futuro]
▶ Exemplo: Comparando quatro estratégias de CV
from sklearn.model_selection import (train_test_split, KFold, StratifiedKFold,
TimeSeriesSplit, cross_val_score)
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
import numpy as np
X, y = load_iris(return_X_y=True)
model = RandomForestClassifier(n_estimators=50, random_state=42)
# 1. Hold-out
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
model.fit(X_tr, y_tr)
holdout_score = model.score(X_te, y_te)
# 2. K-Fold
kf_scores = cross_val_score(model, X, y, cv=KFold(n_splits=5, shuffle=True, random_state=42))
# 3. Stratified K-Fold
skf_scores = cross_val_score(model, X, y, cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42))
# 4. TimeSeriesSplit (para dados ordenados no tempo)
tss_scores = cross_val_score(model, X, y, cv=TimeSeriesSplit(n_splits=5))
print(f"Hold-out: {holdout_score:.4f}")
print(f"K-Fold: {kf_scores.mean():.4f} +/- {kf_scores.std():.4f}")
print(f"Stratified K-Fold: {skf_scores.mean():.4f} +/- {skf_scores.std():.4f}")
print(f"TimeSeriesSplit: {tss_scores.mean():.4f} +/- {tss_scores.std():.4f}")
Saída:
# Execução bem-sucedida
| Método | Tipo de dado | Vantagens | Desvantagens |
|---|---|---|---|
| Hold-out | Qualquer | Rápido | Resultado depende da divisão |
| K-Fold | Qualquer | Estável | Particionamento aleatório |
| Stratified K-Fold | Classificação | Preserva proporções de classe | Apenas classificação |
| TimeSeriesSplit | Série temporal | Sem vazamento de futuro | Tamanho do treino cresce |
4. Métricas de Classificação e Regressão
(1) Métricas completas de classificação
▶ Exemplo: Avaliação abrangente de classificação
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
f1_score, roc_auc_score, classification_report,
confusion_matrix)
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
pipe = Pipeline([("scaler", StandardScaler()), ("model", LogisticRegression(max_iter=500))])
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
y_prob = pipe.predict_proba(X_test)[:, 1]
print(f"Acurácia: {accuracy_score(y_test, y_pred):.4f}")
print(f"Precisão: {precision_score(y_test, y_pred):.4f}")
print(f"Revocação: {recall_score(y_test, y_pred):.4f}")
print(f"F1: {f1_score(y_test, y_pred):.4f}")
print(f"AUC-ROC: {roc_auc_score(y_test, y_prob):.4f}")
print(f"\nMatriz de Confusão:\n{confusion_matrix(y_test, y_pred)}")
print(f"\nRelatório Detalhado:\n{classification_report(y_test, y_pred)}")
Saída:
# Execução bem-sucedida
(2) Métricas de regressão alinhadas com objetivos de negócio
▶ Exemplo: Métricas de regressão e seu significado de negócio
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np
y_true = np.array([100, 150, 200, 250, 300]) # mil USD
y_pred = np.array([95, 160, 190, 260, 310])
mae = mean_absolute_error(y_true, y_pred)
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
r2 = r2_score(y_true, y_pred)
mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100
print(f"MAE: {mae:.2f} k USD")
print(f"RMSE: {rmse:.2f} k USD")
print(f"R²: {r2:.4f}")
print(f"MAPE: {mape:.1f}%")
# Cálculo de impacto de negócio
avg_monthly_revenue = 1000 # mil USD
mape_pct = mape / 100
inventory_cost_pct = 0.3 # 30% do excesso de estoque é desperdício
annual_loss = avg_monthly_revenue * mape_pct * inventory_cost_pct * 12
print(f"\nPerda anual de estoque por erro de previsão: {annual_loss:.0f} k USD")
Saída:
# Execução bem-sucedida
| Métrica | Fórmula | Significado de negócio |
|---|---|---|
| MAE | média|y-ŷ| | Desvio médio (k USD) |
| RMSE | √(média(y-ŷ)²) | Penaliza mais os erros grandes |
| R² | 1 - SS_res/SS_tot | Proporção de variância explicada |
| MAPE | média|y-ŷ|/y × 100% | Erro relativo (%) |
5. Ajuste de Hiperparâmetros
(1) Três estratégias de busca
▶ Exemplo: GridSearch vs RandomSearch
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
import time
X, y = fetch_california_housing(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
param_grid = {
"n_estimators": [50, 100, 200],
"max_depth": [5, 10, 15, None],
"min_samples_leaf": [1, 5, 10],
}
# GridSearch: busca exaustiva
start = time.time()
grid = GridSearchCV(RandomForestRegressor(random_state=42), param_grid, cv=3, scoring="r2", n_jobs=-1)
grid.fit(X_train, y_train)
grid_time = time.time() - start
# RandomSearch: amostragem aleatória
start = time.time()
random = RandomizedSearchCV(RandomForestRegressor(random_state=42), param_grid,
n_iter=15, cv=3, scoring="r2", n_jobs=-1, random_state=42)
random.fit(X_train, y_train)
random_time = time.time() - start
print(f"GridSearch: R²={grid.best_score_:.4f}, Tempo={grid_time:.1f}s, Tentativas={len(grid.cv_results_['mean_test_score'])}")
print(f"RandomSearch: R²={random.best_score_:.4f}, Tempo={random_time:.1f}s, Tentativas=15")
Saída:
# Execução bem-sucedida
| Método | Estratégia de busca | Cobertura | Velocidade | Melhor para |
|---|---|---|---|---|
| GridSearch | Exaustiva sobre todas as combinações | 100% | Lento | Poucos parâmetros |
| RandomSearch | Amostragem aleatória | Parcial | Rápido | Muitos parâmetros |
| Optuna | Otimização bayesiana | Busca guiada | Eficiente | Espaços de busca complexos |
(2) Otimização bayesiana com Optuna
▶ Exemplo: Ajuste de hiperparâmetros com Optuna
# pip install optuna
import optuna
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
import numpy as np
X, y = fetch_california_housing(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
def objective(trial):
n_estimators = trial.suggest_int("n_estimators", 50, 500)
max_depth = trial.suggest_int("max_depth", 3, 20)
min_samples_leaf = trial.suggest_int("min_samples_leaf", 1, 20)
max_features = trial.suggest_float("max_features", 0.3, 1.0)
model = RandomForestRegressor(
n_estimators=n_estimators, max_depth=max_depth,
min_samples_leaf=min_samples_leaf, max_features=max_features,
random_state=42, n_jobs=-1,
)
scores = cross_val_score(model, X_train, y_train, cv=3, scoring="r2")
return scores.mean()
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=30, show_progress_bar=False)
print(f"Melhor R²: {study.best_value:.4f}")
print(f"Melhores parâmetros: {study.best_params}")
Saída:
# Função definida com sucesso
6. Diagnóstico de sobreajuste
(1) Curvas de aprendizado e curvas de validação
▶ Exemplo: Plotando curvas de aprendizado
from sklearn.model_selection import learning_curve, validation_curve
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import fetch_california_housing
import matplotlib.pyplot as plt
import numpy as np
X, y = fetch_california_housing(return_X_y=True)
# Curva de aprendizado: desempenho vs tamanho do conjunto de treino
train_sizes, train_scores, val_scores = learning_curve(
RandomForestRegressor(n_estimators=50, random_state=42),
X, y, train_sizes=np.linspace(0.1, 1.0, 10),
cv=3, scoring="r2", n_jobs=-1,
)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Curva de aprendizado
axes[0].plot(train_sizes, train_scores.mean(axis=1), "o-", label="Treino")
axes[0].plot(train_sizes, val_scores.mean(axis=1), "o-", label="Validação")
axes[0].fill_between(train_sizes, train_scores.mean(axis=1) - train_scores.std(axis=1),
train_scores.mean(axis=1) + train_scores.std(axis=1), alpha=0.1)
axes[0].fill_between(train_sizes, val_scores.mean(axis=1) - val_scores.std(axis=1),
val_scores.mean(axis=1) + val_scores.std(axis=1), alpha=0.1)
axes[0].set_xlabel("Tamanho do Treino")
axes[0].set_ylabel("R² Score")
axes[0].set_title("Curva de Aprendizado")
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Curva de validação: desempenho vs hiperparâmetro
param_range = [3, 5, 7, 10, 15, 20, None]
train_scores2, val_scores2 = validation_curve(
RandomForestRegressor(n_estimators=50, random_state=42),
X, y, param_name="max_depth", param_range=param_range,
cv=3, scoring="r2", n_jobs=-1,
)
axes[1].plot(range(len(param_range)), train_scores2.mean(axis=1), "o-", label="Treino")
axes[1].plot(range(len(param_range)), val_scores2.mean(axis=1), "o-", label="Validação")
axes[1].set_xticks(range(len(param_range)))
axes[1].set_xticklabels([str(p) for p in param_range])
axes[1].set_xlabel("max_depth")
axes[1].set_ylabel("R² Score")
axes[1].set_title("Curva de Validação")
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("learning_validation_curves.png", dpi=150)
Saída:
# Execução bem-sucedida
| Diagnóstico | Padrão da curva | Significado | Solução |
|---|---|---|---|
| Subajuste | Tanto treino quanto validação têm scores baixos | Modelo simples demais | Aumentar a complexidade / adicionar features |
| Sobreajuste | Treino alto, validação baixa | Modelo complexo demais | Regularizar / simplificar / adicionar dados |
| Bom ajuste | Ambos altos e próximos | Bem equilibrado | Nenhuma mudança necessária |
| Precisa de mais dados | Curva de validação ainda subindo | Dados insuficientes | Coletar mais dados |
❓ Perguntas Frequentes
P: Como escolher o número de folds para validação cruzada? R: 5 ou 10 são as escolhas mais comuns. 5-fold oferece um bom equilíbrio entre velocidade e estabilidade; 10-fold é mais estável, mas duas vezes mais lento. Para datasets grandes (>50k amostras), 3 folds são suficientes; para datasets pequenos (<1k amostras), use 10 folds.
P: Por que não posso usar K-Fold para dados de séries temporais? R: K-Fold embaralha os dados aleatoriamente, o que pode resultar em treino em dados "futuros" para prever o "passado", causando vazamento de dados. TimeSeriesSplit garante que o conjunto de treino sempre precede o conjunto de teste, simulando cenários reais de previsão.
P: E se o GridSearchCV for muito lento? R: Três estratégias — 1) encolher o espaço de busca (busca grosseira primeiro, depois refinada); 2) usar RandomizedSearchCV (n_iter=20); 3) usar a otimização bayesiana do Optuna (busca inteligente).
P: Quais as desvantagens do MAPE? R: O MAPE explode quando y_true está próximo de zero (divisão por um valor quase zero). Alternativas — use SMAPE (MAPE simétrico) ou MASE (erro escalado médio absoluto).
P: E se o gap entre os scores de treino e validação na curva de aprendizado for grande? R: Um gap grande = sobreajuste. Soluções — 1) aumentar a regularização; 2) reduzir a complexidade do modelo; 3) adicionar mais dados de treino; 4) usar Dropout / early stopping.
P: Quanto melhor é o Optuna em relação ao GridSearch? R: O Optuna usa otimização bayesiana para selecionar de forma inteligente o próximo conjunto de parâmetros. Ele tipicamente encontra em 30-50 tentativas o que o GridSearch precisa de 500+ tentativas para encontrar, economizando mais de 80% dos recursos de computação.
📖 Resumo
- A metodologia de avaliação é a base de qualquer projeto de ML: use StratifiedKFold para classificação, TimeSeriesSplit para séries temporais
- A escolha da métrica de classificação depende do negócio: churn → Revocação, detecção de spam → Precisão, recomendações → F1
- Métricas de regressão devem se alinhar com os custos de negócio: um MAPE de 10% em previsões de milhões de dólares significa $100k de erro
- Progressão do ajuste de hiperparâmetros: GridSearch (exaustivo) → RandomSearch (amostragem) → Optuna (inteligente)
- Diagnóstico de sobreajuste: curvas de aprendizado revelam se você tem dados suficientes; curvas de validação revelam se os parâmetros são ótimos
- A avaliação correta importa mais do que scores altos — métricas infladas colapsam após a implantação
📝 Exercícios
- Básico (Dificuldade ⭐): Compare a acurácia média e o desvio padrão do RandomForestClassifier em Iris usando validação cruzada de 5 e 10 folds. Dica: cross_val_score(cv=5/10).
- Intermediário (Dificuldade ⭐⭐): Use GridSearchCV para encontrar os parâmetros ótimos do XGBoost (max_depth / learning_rate / n_estimators) e produza os melhores parâmetros e o R² da CV. Dica: Pipeline + param_grid.
- Desafio (Dificuldade ⭐⭐⭐): Plote uma curva de aprendizado e uma curva de validação para diagnosticar subajuste/sobreajuste em um modelo, proponha um plano concreto de melhoria e verifique a melhora. Dica: learning_curve + validation_curve + comparação antes/depois.
← Anterior: Redes Neurais Convolucionais | Próximo: Introdução ao MLOps →