Introdução à Otimização de Hiperparâmetros
Em 2019, enfrentei um desafio interessante em um projeto de previsão de churn para uma startup de SaaS. Nosso modelo de Random Forest estava performando razoavelmente bem, mas eu sabia que podíamos melhorar. O problema? Estávamos usando os parâmetros padrão, algo que vejo muitos profissionais fazendo até hoje.
Se você já trabalhou com machine learning, sabe que a diferença entre um modelo bom e um modelo excepcional frequentemente está na escolha dos hiperparâmetros. Não são os dados (assumindo que você já fez uma boa preparação), nem mesmo o algoritmo em si, mas os ajustes finos que fazem toda a diferença.
Neste artigo, vou compartilhar o que aprendi em anos ajustando modelos para empresas de diferentes setores, incluindo técnicas que não são amplamente discutidas em tutoriais básicos.
O que são hiperparâmetros e por que importam?
Antes de mais nada, vamos esclarecer: parâmetros e hiperparâmetros são coisas distintas. Os parâmetros são valores que o modelo aprende durante o treinamento (como os pesos em uma rede neural). Já os hiperparâmetros são configurações que definimos antes do treinamento começar e que controlam como o modelo aprenderá.
Exemplos clássicos de hiperparâmetros incluem:
- Número de árvores em um Random Forest
- Taxa de aprendizado em redes neurais
- Parâmetro C em um SVM
- Valor de k em k-NN
- Profundidade máxima em árvores de decisão
A importância da otimização desses valores é frequentemente subestimada. Em um dos projetos que conduzi para uma empresa de varejo, a simples mudança nos hiperparâmetros de um modelo XGBoost aumentou a precisão de 82% para 89% - uma melhoria que representou milhares de reais em economia por previsões mais precisas de estoque.
As armadilhas comuns da otimização
Antes de entrarmos nas técnicas, preciso alertar sobre algumas armadilhas que já me pegaram no passado:
1. Overfitting nos hiperparâmetros
Esta é sutil e perigosa. Você otimiza exaustivamente seus hiperparâmetros no conjunto de validação e fica empolgado com os resultados... apenas para descobrir que o desempenho no conjunto de teste é muito inferior. Isso aconteceu comigo em 2020, quando trabalhava em um modelo de previsão de vendas e acabei ajustando demais para o conjunto de validação.
2. Ignorar o custo computacional
Algumas técnicas de otimização são extremamente caras computacionalmente. Em um projeto recente para uma fintech, uma busca em grade completa levaria semanas. Precisamos ser estratégicos sobre quais hiperparâmetros otimizar e como fazê-lo.
3. Falta de reprodutibilidade
Sem definir seeds aleatórias, você pode obter resultados diferentes a cada execução. Isso torna impossível determinar se a melhoria veio dos hiperparâmetros ou da aleatoriedade da inicialização do modelo.
Técnicas de otimização que realmente funcionam
Vamos às abordagens que uso regularmente e que têm gerado resultados consistentes:
1. Busca aleatória vs. Busca em grade
A busca em grade (Grid Search) foi por muito tempo o padrão da indústria, mas na maioria das situações práticas, a busca aleatória (Random Search) oferece melhor custo-benefício.
Veja um exemplo de implementação de busca aleatória com Python:
from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import RandomForestClassifier
import numpy as np
# Definindo o espaço de hiperparâmetros
param_dist = {
'n_estimators': np.arange(100, 1000, 100),
'max_depth': [None] + list(np.arange(5, 30, 5)),
'min_samples_split': np.arange(2, 20, 2),
'min_samples_leaf': np.arange(1, 10),
'bootstrap': [True, False]
}
# Criando o modelo
rf = RandomForestClassifier(random_state=42)
# Configurando a busca aleatória
random_search = RandomizedSearchCV(
estimator=rf,
param_distributions=param_dist,
n_iter=100, # Número de combinações a tentar
cv=5, # Validação cruzada 5-fold
random_state=42,
n_jobs=-1, # Usar todos os núcleos disponíveis
scoring='f1' # Métrica a otimizar
)
# Executando a busca
random_search.fit(X_train, y_train)
# Melhores hiperparâmetros encontrados
print(f'Melhores hiperparâmetros: {random_search.best_params_}')
print(f'Melhor pontuação: {random_search.best_score_:.4f}')
Por que isso funciona? Em 2012, Bergstra e Bengio publicaram um artigo mostrando que a busca aleatória é mais eficiente que a busca em grade para a maioria dos problemas de otimização de hiperparâmetros. Isso porque nem todos os hiperparâmetros são igualmente importantes, e a busca aleatória permite explorar mais valores para os hiperparâmetros críticos.
2. Otimização Bayesiana
Esta é minha técnica favorita para modelos computacionalmente caros. Em vez de escolher pontos aleatoriamente, a otimização bayesiana usa os resultados anteriores para selecionar os próximos pontos a avaliar. É como ter uma busca 'inteligente'.
Um exemplo usando a biblioteca Optuna, que tem se tornado meu padrão para projetos complexos:
import optuna
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score
def objective(trial):
# Definindo o espaço de hiperparâmetros
params = {
'n_estimators': trial.suggest_int('n_estimators', 50, 500),
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'max_depth': trial.suggest_int('max_depth', 3, 10),
'min_samples_split': trial.suggest_int('min_samples_split', 2, 20),
'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 10),
'subsample': trial.suggest_float('subsample', 0.5, 1.0)
}
# Criando o modelo com os hiperparâmetros sugeridos
model = GradientBoostingClassifier(**params, random_state=42)
# Avaliando o modelo com validação cruzada
score = cross_val_score(model, X_train, y_train, cv=5, scoring='f1').mean()
return score # Optuna maximiza por padrão
# Criando o estudo de otimização
study = optuna.create_study(direction='maximize')
# Executando a otimização
study.optimize(objective, n_trials=100)
# Melhores hiperparâmetros encontrados
best_params = study.best_params
print(f'Melhores hiperparâmetros: {best_params}')
print(f'Melhor pontuação: {study.best_value:.4f}')
# Visualizando a importância dos hiperparâmetros
optuna.visualization.plot_param_importances(study)
Recentemente, usei esta abordagem para um cliente da área de saúde, e conseguimos reduzir o tempo de otimização de quase uma semana para menos de um dia, encontrando parâmetros ainda melhores que os obtidos por métodos tradicionais.
3. Abordagem em duas etapas
Esta é uma técnica que desenvolvi ao longo dos anos e que tem se mostrado extremamente eficaz:
- Exploração ampla: Comece com uma busca aleatória em um espaço amplo de hiperparâmetros para identificar as regiões promissoras.
- Refinamento: Use otimização bayesiana em um espaço mais restrito, concentrando-se nas regiões identificadas no passo anterior.
Implementei essa abordagem para uma empresa de energia renovável em 2022, e conseguimos um modelo que superou as expectativas iniciais em mais de 15% de acurácia.
Estudo de caso real: Otimização de um modelo XGBoost
Vamos ver um exemplo concreto de um projeto em que trabalhei em 2023, para previsão de demanda em uma empresa de e-commerce. O desafio era prever as vendas diárias com uma janela de 30 dias.
Inicialmente, o modelo XGBoost com parâmetros padrão atingiu um RMSE de 243 unidades. Depois da otimização, chegamos a um RMSE de 187 - uma melhoria de 23%!
Aqui está o código simplificado da abordagem em duas etapas que utilizei:
# Fase 1: Exploração ampla com busca aleatória
from sklearn.model_selection import RandomizedSearchCV
import xgboost as xgb
import numpy as np
# Espaço amplo de hiperparâmetros para exploração inicial
param_space_wide = {
'n_estimators': np.arange(50, 1000, 50),
'learning_rate': np.logspace(-3, 0, 10), # 0.001 a 1.0
'max_depth': np.arange(3, 15),
'min_child_weight': np.arange(1, 10),
'gamma': np.logspace(-3, 1, 10), # 0.001 a 10
'subsample': np.linspace(0.5, 1.0, 6),
'colsample_bytree': np.linspace(0.5, 1.0, 6),
'reg_alpha': np.logspace(-3, 1, 10), # 0.001 a 10
'reg_lambda': np.logspace(-3, 1, 10) # 0.001 a 10
}
model = xgb.XGBRegressor(objective='reg:squarederror', random_state=42)
# Primeira fase: 100 iterações de busca aleatória
random_search = RandomizedSearchCV(
estimator=model,
param_distributions=param_space_wide,
n_iter=100,
cv=5,
scoring='neg_root_mean_squared_error',
random_state=42,
n_jobs=-1
)
random_search.fit(X_train, y_train)
# Melhores parâmetros da primeira fase
best_params_phase1 = random_search.best_params_
print(f'Melhores parâmetros da fase 1: {best_params_phase1}')
print(f'Melhor RMSE da fase 1: {-random_search.best_score_:.2f}')
# Fase 2: Refinamento com otimização bayesiana
import optuna
def create_param_space(trial, best_params):
# Criamos um espaço mais restrito em torno dos melhores valores encontrados
params = {
'n_estimators': trial.suggest_int('n_estimators',
max(50, int(best_params['n_estimators'] * 0.7)),
min(1000, int(best_params['n_estimators'] * 1.3))),
'learning_rate': trial.suggest_float('learning_rate',
best_params['learning_rate'] / 3,
best_params['learning_rate'] * 3, log=True),
'max_depth': trial.suggest_int('max_depth',
max(3, best_params['max_depth'] - 2),
min(15, best_params['max_depth'] + 2)),
'min_child_weight': trial.suggest_int('min_child_weight',
max(1, best_params['min_child_weight'] - 2),
min(10, best_params['min_child_weight'] + 2)),
'gamma': trial.suggest_float('gamma',
best_params['gamma'] / 3,
best_params['gamma'] * 3, log=True),
'subsample': trial.suggest_float('subsample',
max(0.5, best_params['subsample'] - 0.1),
min(1.0, best_params['subsample'] + 0.1)),
'colsample_bytree': trial.suggest_float('colsample_bytree',
max(0.5, best_params['colsample_bytree'] - 0.1),
min(1.0, best_params['colsample_bytree'] + 0.1)),
'reg_alpha': trial.suggest_float('reg_alpha',
best_params['reg_alpha'] / 3,
best_params['reg_alpha'] * 3, log=True),
'reg_lambda': trial.suggest_float('reg_lambda',
best_params['reg_lambda'] / 3,
best_params['reg_lambda'] * 3, log=True)
}
return params
def objective(trial):
params = create_param_space(trial, best_params_phase1)
params['objective'] = 'reg:squarederror'
params['random_state'] = 42
model = xgb.XGBRegressor(**params)
# Usamos validação cruzada para evitar overfitting nos hiperparâmetros
from sklearn.model_selection import cross_val_score
rmse_scores = cross_val_score(model, X_train, y_train,
cv=5, scoring='neg_root_mean_squared_error')
return rmse_scores.mean() # Optuna maximiza por padrão (então usamos valores negativos do RMSE)
# Segunda fase: 50 iterações de otimização bayesiana
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=50)
# Melhores parâmetros da segunda fase
best_params_final = study.best_params
best_params_final['objective'] = 'reg:squarederror'
best_params_final['random_state'] = 42
print(f'Melhores parâmetros finais: {best_params_final}')
print(f'Melhor RMSE final: {-study.best_value:.2f}')
# Treinamento do modelo final com todos os dados de treinamento
final_model = xgb.XGBRegressor(**best_params_final)
final_model.fit(X_train, y_train)
# Avaliação no conjunto de teste
from sklearn.metrics import mean_squared_error
y_pred = final_model.predict(X_test)
final_rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(f'RMSE no conjunto de teste: {final_rmse:.2f}')
Cross-validation aninhada: a estratégia definitiva
Uma técnica avançada que incorporei recentemente em meus projetos é a validação cruzada aninhada (nested cross-validation). Ela resolve de forma elegante o problema de overfitting nos hiperparâmetros.
A ideia é usar um loop externo de validação cruzada para avaliar o desempenho e um loop interno para otimizar os hiperparâmetros, garantindo uma estimativa imparcial da performance do modelo.
from sklearn.model_selection import GridSearchCV, KFold, cross_val_score
# Configuração dos loops de validação cruzada
outer_cv = KFold(n_splits=5, shuffle=True, random_state=42)
inner_cv = KFold(n_splits=3, shuffle=True, random_state=42)
# Espaço de hiperparâmetros
param_grid = {
'n_estimators': [100, 300, 500],
'learning_rate': [0.01, 0.1, 0.3],
'max_depth': [3, 5, 7]
}
# Resultados para armazenar as pontuações
scores = []
# Loop externo de validação cruzada
for train_idx, test_idx in outer_cv.split(X, y):
X_train_outer, X_test_outer = X[train_idx], X[test_idx]
y_train_outer, y_test_outer = y[train_idx], y[test_idx]
# Criação do modelo base
model = xgb.XGBRegressor(objective='reg:squarederror', random_state=42)
# Configuração da busca em grade com o loop interno
grid_search = GridSearchCV(
estimator=model,
param_grid=param_grid,
cv=inner_cv,
scoring='neg_root_mean_squared_error'
)
# Otimização dos hiperparâmetros no conjunto de treinamento externo
grid_search.fit(X_train_outer, y_train_outer)
# Melhor modelo encontrado
best_model = grid_search.best_estimator_
# Avaliação no conjunto de teste externo
y_pred = best_model.predict(X_test_outer)
rmse = np.sqrt(mean_squared_error(y_test_outer, y_pred))
scores.append(rmse)
print(f'Fold RMSE: {rmse:.2f}, Melhores parâmetros: {grid_search.best_params_}')
# Resultado final
print(f'
RMSE médio da validação cruzada aninhada: {np.mean(scores):.2f} ± {np.std(scores):.2f}')
Esta abordagem é mais cara computacionalmente, mas oferece uma estimativa muito mais confiável do desempenho real do modelo, algo crucial para aplicações críticas como detecção de fraudes ou diagnóstico médico.
Dicas práticas que aprendi com a experiência
Além das técnicas formais, há várias lições que aprendi empiricamente:
1. Use escalas logarítmicas para alguns hiperparâmetros
Parâmetros como taxas de aprendizado ou regularização geralmente são melhor explorados em escala logarítmica. Por exemplo, testar valores como [0.001, 0.01, 0.1, 1.0] em vez de [0.1, 0.2, 0.3, 0.4].
2. Otimize hiperparâmetros de forma progressiva
Para modelos com muitos hiperparâmetros (como redes neurais), às vezes é melhor otimizar grupos de parâmetros relacionados sequencialmente. Por exemplo, primeiro o tamanho e arquitetura da rede, depois os parâmetros de otimização.
3. Use diferentes métricas para diferentes fases
Em um projeto recente de classificação de crédito, descobri que otimizar primeiro para recall e depois para pontuação F1 produzia melhores resultados do que otimizar diretamente para F1.
4. Monitore o tempo de execução dos modelos
Às vezes um pequeno sacrifício na performance (por exemplo, 0.5% menos de precisão) vale a pena se o modelo executa significativamente mais rápido. Isso foi crucial em um projeto de recomendação em tempo real.
Ferramentas que recomendo
Ao longo dos anos, experimentei diversas bibliotecas para otimização de hiperparâmetros. Estas são minhas recomendações atuais:
- Scikit-learn: Para projetos simples, RandomizedSearchCV e GridSearchCV são suficientes.
- Optuna: Minha escolha atual para a maioria dos projetos. Interface excelente, visualizações informativas e extremamente flexível.
- Hyperopt: Boa alternativa, especialmente para espaços de parâmetros mais complexos.
- Ray Tune: Quando a escalabilidade é crítica ou quando estou trabalhando com modelos distribuídos.
- Weights & Biases: Para projetos maiores onde o rastreamento de experimentos é crucial.
Conclusão: O equilíbrio entre arte e ciência
A otimização de hiperparâmetros é tanto uma arte quanto uma ciência. As técnicas formais que compartilhei aqui fornecem a estrutura, mas a intuição e experiência são igualmente importantes.
Em meu curso Formação Machine Learning, dedico um módulo inteiro a este tema, explorando estratégias avançadas de otimização para diferentes algoritmos e casos de uso. Se você quer aprofundar seu conhecimento, recomendo também o curso Machine Learning: Classificação com Python, onde cobrimos a otimização de hiperparâmetros para modelos de classificação.
Lembre-se: a otimização de hiperparâmetros não é apenas sobre encontrar o melhor modelo possível, mas sobre encontrar o melhor modelo para seu problema específico, considerando todos os trade-offs envolvidos.
Quais são suas experiências com otimização de hiperparâmetros? Compartilhe nos comentários abaixo - adoro aprender com as experiências da comunidade!