Parâmetros o modelo aprende sozinho durante o treino. Hiperparâmetros você define antes: a profundidade da árvore, a taxa de aprendizado, a força da regularização. A escolha deles pode ser a diferença entre um modelo medíocre e um excelente — e é aqui que mora uma das armadilhas metodológicas mais comuns em ML: otimizar hiperparâmetros e reportar desempenho no mesmo conjunto, produzindo um número otimista que não se sustenta em produção.

Dominar as estratégias de busca de hiperparâmetros (grid, random, bayesiana, Hyperband), entender por que a busca aleatória frequentemente vence a exaustiva, e — o ponto central — usar validação cruzada aninhada (nested CV) para estimar o desempenho sem viés de seleção.

O quanto os hiperparâmetros decidem

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=3000, n_features=20, random_state=42)

for d in [2, 5, 10, None]:
    auc = cross_val_score(
        RandomForestClassifier(max_depth=d, n_estimators=200, random_state=42),
        X, y, cv=5, scoring="roc_auc").mean()
    print(f"max_depth={d}: AUC={auc:.3f}")

Um único hiperparâmetro pode mover a métrica de forma significativa. Multiplique isso por dezenas de combinações e o espaço de busca fica grande — daí a necessidade de estratégias eficientes.

Grid Search — busca exaustiva

Testa todas as combinações de uma grade pré-definida. Simples e reprodutível, mas o custo explode combinatorialmente: 4 hiperparâmetros com 5 valores cada = 625 combinações × k folds.

from sklearn.model_selection import GridSearchCV

grade = {
    "n_estimators": [100, 300, 500],
    "max_depth": [5, 10, None],
    "min_samples_leaf": [1, 5, 10],
}

busca = GridSearchCV(
    RandomForestClassifier(random_state=42),
    grade, cv=5, scoring="roc_auc", n_jobs=-1,
)
busca.fit(X, y)
print(f"Melhores params: {busca.best_params_}")
print(f"Melhor AUC (CV): {busca.best_score_:.3f}")

Use grid search quando o espaço é pequeno e você tem intuição sobre os valores plausíveis.

Random Search — busca aleatória (e por que costuma vencer)

Amostra combinações aleatoriamente de distribuições. Contraintuitivamente, para o mesmo orçamento, geralmente encontra melhores configurações que o grid. Razão: em quase todo problema, poucos hiperparâmetros importam de verdade; o random search explora mais valores distintos desses poucos importantes, enquanto o grid desperdiça orçamento variando os irrelevantes (resultado clássico de Bergstra & Bengio, 2012).

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform

dist = {
    "n_estimators": randint(100, 800),
    "max_depth": randint(3, 30),
    "min_samples_leaf": randint(1, 20),
    "max_features": uniform(0.1, 0.9),
}

busca = RandomizedSearchCV(
    RandomForestClassifier(random_state=42),
    dist, n_iter=60, cv=5, scoring="roc_auc",
    random_state=42, n_jobs=-1,
)
busca.fit(X, y)
print(f"Melhor AUC: {busca.best_score_:.3f}")

Defina distribuições (não listas): contínuas (uniform, loguniform) para taxas e regularização, inteiras (randint) para contagens. Para taxa de aprendizado e C, use escala log (loguniform), pois esses parâmetros atuam em ordens de magnitude.

Otimização bayesiana — busca inteligente

Grid e random são "sem memória": cada teste ignora os anteriores. A otimização bayesiana constrói um modelo substituto (surrogate) do desempenho em função dos hiperparâmetros e usa uma função de aquisição para decidir onde testar em seguida, equilibrando exploração (regiões incertas) e explotação (regiões promissoras). Encontra bons valores com muito menos avaliações — ideal quando cada treino é caro.

# pip install optuna
import optuna
from sklearn.model_selection import cross_val_score
from lightgbm import LGBMClassifier

def objetivo(trial):
    params = {
        "n_estimators": trial.suggest_int("n_estimators", 100, 1000),
        "learning_rate": trial.suggest_float("learning_rate", 1e-3, 0.3, log=True),
        "num_leaves": trial.suggest_int("num_leaves", 15, 255),
        "subsample": trial.suggest_float("subsample", 0.5, 1.0),
        "reg_lambda": trial.suggest_float("reg_lambda", 1e-8, 10.0, log=True),
    }
    modelo = LGBMClassifier(**params, random_state=42, verbose=-1)
    return cross_val_score(modelo, X, y, cv=5, scoring="roc_auc").mean()

estudo = optuna.create_study(direction="maximize")
estudo.optimize(objetivo, n_trials=50, show_progress_bar=False)
print(f"Melhor AUC: {estudo.best_value:.3f}")
print(f"Melhores params: {estudo.best_params}")

Optuna também oferece pruning (interromper trials ruins cedo) e amostradores como TPE. É hoje o padrão de fato para tuning em Python.

Hyperband e Successive Halving — orçamento adaptativo

Ideias baseadas em multi-armed bandit: em vez de treinar toda configuração até o fim, comece muitas com pouco orçamento (poucas árvores, poucas épocas, subamostra dos dados) e vá eliminando as piores, concentrando recursos nas promissoras.

from sklearn.experimental import enable_halving_search_cv  # noqa
from sklearn.model_selection import HalvingRandomSearchCV

busca = HalvingRandomSearchCV(
    RandomForestClassifier(random_state=42),
    {"n_estimators": randint(50, 800), "max_depth": randint(3, 30)},
    resource="n_estimators", factor=3, cv=5,
    scoring="roc_auc", random_state=42, n_jobs=-1,
)
busca.fit(X, y)

factor=3 mantém 1/3 das configurações a cada rodada, triplicando o orçamento das sobreviventes. Excelente para deep learning e modelos caros, onde o orçamento é "épocas" ou "tamanho do dataset".

O erro fatal: otimizar e avaliar no mesmo conjunto

Quando você roda GridSearchCV, o best_score_ é otimista. Por quê? Você testou dezenas de configurações e escolheu a de maior score na CV — mas o máximo de muitas estimativas ruidosas é, em média, maior que o verdadeiro (viés de seleção do vencedor). Reportar esse best_score_ como desempenho esperado é enganoso.

A solução é a validação cruzada aninhada.

Nested Cross-Validation — a forma correta

Duas camadas de CV:

  • Loop interno: busca de hiperparâmetros (escolhe a melhor config em cada fold externo).
  • Loop externo: estima o desempenho da config escolhida em dados que o tuning nunca viu.
Loop externo (5 folds) ── para cada fold:
   ├── separa treino_ext / teste_ext
   ├── Loop interno (CV no treino_ext) → escolhe melhores hiperparâmetros
   ├── treina com esses params em todo o treino_ext
   └── avalia no teste_ext  ← estimativa imparcial
resultado = média das 5 avaliações externas
from sklearn.model_selection import GridSearchCV, cross_val_score, StratifiedKFold

interno = StratifiedKFold(5, shuffle=True, random_state=1)
externo = StratifiedKFold(5, shuffle=True, random_state=2)

busca = GridSearchCV(
    RandomForestClassifier(random_state=42),
    {"max_depth": [5, 10, None], "n_estimators": [200, 400]},
    cv=interno, scoring="roc_auc", n_jobs=-1,
)

# O loop externo avalia a busca inteira como se fosse um "modelo"
scores = cross_val_score(busca, X, y, cv=externo, scoring="roc_auc")
print(f"AUC nested (imparcial): {scores.mean():.3f} ± {scores.std():.3f}")

Ponto-chave: o busca inteiro (tuning incluído) é tratado como um estimador dentro do loop externo, então cada avaliação externa usa hiperparâmetros escolhidos sem ver aquele fold. Essa é a estimativa que você reporta. Depois, para o modelo final de produção, você roda a busca em todos os dados e usa a config vencedora — mas o número que comunica é o do nested CV.

Boas práticas de tuning

  • Comece largo, depois refine: random/bayesiana amplo para localizar a região boa, depois busca fina em torno dela.
  • Escala log para taxas de aprendizado, C, regularização.
  • Fixe o que não importa: não sintonize tudo; identifique os 2–4 hiperparâmetros de maior impacto.
  • Orçamento consciente: use pruning/Hyperband para não desperdiçar em configs ruins.
  • Registre os experimentos: semente, versões, espaço de busca, para reprodutibilidade.

Em R (tidymodels e mlr3)

library(tidymodels)

modelo <- rand_forest(
  trees = tune(), min_n = tune(), mtry = tune()
) |> set_engine("ranger") |> set_mode("classification")

wf <- workflow() |> add_recipe(receita) |> add_model(modelo)

set.seed(42)
folds <- vfold_cv(treino, v = 5, strata = alvo)

# Busca bayesiana
resultado <- tune_bayes(
  wf, resamples = folds, initial = 10, iter = 30,
  metrics = metric_set(roc_auc)
)

melhores <- select_best(resultado, metric = "roc_auc")
wf_final <- finalize_workflow(wf, melhores)

# Nested CV com nested_cv() + last_fit para estimativa imparcial

O mlr3tuning oferece o AutoTuner, que encapsula um learner + tuning como um único objeto — perfeito para colocar dentro de um resample() externo e obter nested CV de forma limpa.

Hiperparâmetros são definidos antes do treino e pesam muito no resultado. O grid search é exaustivo e explode combinatorialmente; para o mesmo orçamento, a busca aleatória costuma vencer, porque poucos hiperparâmetros realmente importam e ela cobre mais valores dos que importam. A otimização bayesiana, com Optuna, usa as avaliações passadas para buscar com critério, enquanto Hyperband e Successive Halving alocam orçamento de forma adaptativa, eliminando cedo as configurações ruins. Um ponto que se erra com frequência: o best_score_ de uma busca é otimista pelo viés do vencedor, e não deve ser reportado como desempenho esperado. Quem dá a estimativa imparcial é a validação cruzada aninhada — laço interno para o ajuste, externo para a avaliação —, e é esse número que se reporta; o modelo que vai para produção usa a configuração vencedora treinada em todos os dados.

Exercícios

  1. Por que a busca aleatória (random search) frequentemente supera a busca em grade (grid search) para o mesmo número de avaliações?

    ✓ Resposta:

    Na maioria dos problemas, apenas alguns poucos hiperparâmetros afetam de fato o desempenho; os demais têm efeito marginal. O grid search gasta seu orçamento testando todas as combinações, o que significa repetir muitos valores dos hiperparâmetros importantes enquanto varia os irrelevantes. O random search, ao amostrar combinações aleatórias, explora um número muito maior de valores distintos dos hiperparâmetros importantes com o mesmo orçamento, aumentando a chance de encontrar uma boa configuração. Esse é o resultado clássico de Bergstra & Bengio (2012).

  2. Explique por que o best_score_ retornado por um GridSearchCV é uma estimativa otimista do desempenho do modelo.

    ✓ Resposta:

    O best_score_ é o máximo entre os scores de CV de todas as configurações testadas. Cada score é uma estimativa ruidosa do desempenho real; ao escolher o maior entre muitos, você captura não só a configuração genuinamente boa mas também o ruído favorável (algumas configs tiveram sorte no split). Esse é o "viés do vencedor" (winner's curse): o máximo de várias estimativas ruidosas é, em expectativa, maior que o valor verdadeiro. Portanto, o best_score_ superestima o desempenho esperado em dados novos. A correção é estimar o desempenho com nested CV.

  3. Descreva a estrutura da validação cruzada aninhada e o papel de cada loop.

    ✓ Resposta:

    Há dois loops. O loop externo divide os dados em folds; para cada fold externo, o modelo é avaliado no seu conjunto de teste — essa avaliação estima o desempenho de generalização. O loop interno roda dentro do treino de cada fold externo e faz a busca de hiperparâmetros (por CV), escolhendo a melhor configuração usando apenas dados de treino daquele fold externo. Assim, os hiperparâmetros são selecionados sem jamais ver o teste externo correspondente, e a média das avaliações externas é uma estimativa imparcial do desempenho. O loop interno cuida da seleção; o externo, da avaliação.

  4. Quando a otimização bayesiana é preferível ao random search, e como ela decide onde testar em seguida?

    ✓ Resposta:

    É preferível quando cada avaliação é cara (modelos grandes, deep learning, datasets enormes) e você quer bons hiperparâmetros com poucas avaliações. Ela constrói um modelo substituto (surrogate) — tipicamente um processo gaussiano ou um TPE — que estima o desempenho esperado e a incerteza em função dos hiperparâmetros. Uma função de aquisição (ex.: Expected Improvement) usa essas estimativas para escolher o próximo ponto a testar, equilibrando explotação (testar onde o desempenho previsto é alto) e exploração (testar onde há muita incerteza). A cada resultado, o surrogate é atualizado, tornando a busca progressivamente mais informada — ao contrário do random search, que não tem memória.

  5. O que é Successive Halving / Hyperband e em que tipo de problema ele brilha?

    ✓ Resposta:

    São estratégias de alocação adaptativa de orçamento baseadas em bandits. Em vez de treinar cada configuração até o fim, iniciam muitas configurações com pouco recurso (poucas épocas/árvores ou uma fração dos dados), avaliam, descartam as piores (mantendo, digamos, 1/factor) e realocam mais recurso às sobreviventes, repetindo. Isso evita desperdiçar computação em configurações claramente ruins. Hyperband roda vários "brackets" de Successive Halving com trade-offs diferentes entre número de configs e orçamento inicial. Brilha em deep learning e modelos caros, onde o recurso ("épocas", "tamanho do dataset") pode ser aumentado gradualmente e há muitas configurações candidatas.

Fontes e leituras recomendadas