Continuando nossa análise detalhada da Análise Discriminante, chegamos a um conceito crucial para aplicações práticas: o encolhimento (shrinkage). Conforme discutimos anteriormente nas formulações matemáticas do LDA e QDA, a estimação precisa das matrizes de covariância é fundamental para o desempenho destes classificadores.
O Problema da Estimação de Covariância
Analogamente ao que observamos nas implementações anteriores, quando trabalhamos com dados de alta dimensionalidade ou tamanhos de amostra limitados, a estimação das matrizes de covariância torna-se desafiadora. Primordialmente, o problema surge porque:
- O número de parâmetros a estimar cresce quadraticamente com o número de features
- Matrizes de covariância podem tornar-se singular ou mal-condicionada
- Estimativas de máxima verossimilhança podem ter alta variância
O Conceito de Encolhimento
O encolhimento é uma técnica de regularização que combina a matriz de covariância estimada dos dados com uma estrutura mais simples e estável. Similarmente a métodos de regularização que encontramos em regressão Ridge e Lasso, o shrinkage visa melhorar a generalização do modelo.
Formulação Matemática
Para o LDA, a matriz de covariância regularizada é dada por:
\(\Sigma_{shrunk} = (1 – \gamma) \Sigma_{empírico} + \gamma \Sigma_{estruturado}\)Onde γ é o parâmetro de encolhimento que varia entre 0 e 1.
Tipos de Estruturas de Encolhimento
Encolhimento para a Matriz Identidade
Uma abordagem comum é encolher em direção à matriz identidade:
\(\Sigma_{shrunk} = (1 – \gamma) \Sigma + \gamma \frac{\text{tr}(\Sigma)}{d} I\)Onde d é a dimensionalidade e tr(Σ) é o traço da matriz de covariância.
Encolhimento para Covariância Diagonal
Outra abordagem utiliza uma matriz diagonal como alvo:
\(\Sigma_{shrunk} = (1 – \gamma) \Sigma + \gamma \text{diag}(\Sigma)\)Vantagens do Encolhimento
Conforme demonstramos nas implementações anteriores, o uso de shrinkage oferece benefícios significativos:
- Melhora a condição numérica das matrizes de covariância
- Reduz a variância das estimativas
- Permite aplicação em dados de alta dimensionalidade
- Melhora a performance de generalização
Seleção do Parâmetro de Encolhimento
O parâmetro γ controla o trade-off entre viés e variância:
- γ = 0: Usa apenas a covariância empírica (alto risco de overfitting)
- γ = 1: Usa apenas a estrutura alvo (alto viés, baixa variância)
- Valores intermediários: Balanceiam viés e variância
Implementação no scikit-learn
No scikit-learn, o encolhimento está disponível através do parâmetro shrinkage nas classes LinearDiscriminantAnalysis e QuadraticDiscriminantAnalysis. Ademais, o parâmetro shrinkage_factor pode ser estimado automaticamente usando validação cruzada.
Exemplo Prático em Python
Para ilustrar os efeitos do encolhimento, implementemos um exemplo comparativo detalhado:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
import numpy as np import matplotlib.pyplot as plt from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV from sklearn.metrics import accuracy_score, classification_report from sklearn.preprocessing import StandardScaler import warnings warnings.filterwarnings('ignore') ''' Demonstração dos efeitos do encolhimento (shrinkage) em classificadores LDA e QDA ''' print("=== EFEITOS DO ENCOLHIMENTO EM LDA E QDA ===") # Criando dataset com alta dimensionalidade e amostras limitadas print("\n1. CONFIGURAÇÃO DO EXPERIMENTO") X, y = make_classification( n_samples=100, # Poucas amostras para destacar o efeito do shrinkage n_features=20, # Alta dimensionalidade n_informative=5, n_redundant=10, n_classes=3, random_state=42 ) print(f"Dimensões do dataset: {X.shape}") print(f"Número de classes: {len(np.unique(y))}") # Padronizando os dados scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Dividindo em treino e teste X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.3, random_state=42 ) ''' Comparação de LDA com diferentes valores de shrinkage ''' print("\n2. LDA COM DIFERENTES VALORES DE SHRINKAGE") shrinkage_values = [None, 'auto', 0.1, 0.5, 0.9] lda_results = {} for shrinkage in shrinkage_values: if shrinkage == 'auto': lda = LinearDiscriminantAnalysis(solver='lsqr', shrinkage='auto') else: lda = LinearDiscriminantAnalysis(solver='lsqr', shrinkage=shrinkage) # Treinando o modelo lda.fit(X_train, y_train) # Fazendo previsões y_pred = lda.predict(X_test) accuracy = accuracy_score(y_test, y_pred) # Validação cruzada para avaliação robusta cv_scores = cross_val_score(lda, X_scaled, y, cv=5) lda_results[shrinkage] = { 'model': lda, 'accuracy': accuracy, 'cv_mean': np.mean(cv_scores), 'cv_std': np.std(cv_scores) } print(f"Shrinkage {shrinkage}:") print(f" Acurácia teste: {accuracy:.3f}") print(f" CV Score: {np.mean(cv_scores):.3f} ± {np.std(cv_scores):.3f}") ''' Comparação de QDA com regularização ''' print("\n3. QDA COM REGULARIZAÇÃO") # QDA não tem parâmetro de shrinkage direto, mas usa reg_param reg_param_values = [0.0, 0.1, 0.5, 0.9] qda_results = {} for reg_param in reg_param_values: qda = QuadraticDiscriminantAnalysis(reg_param=reg_param) # Treinando o modelo qda.fit(X_train, y_train) # Fazendo previsões y_pred = qda.predict(X_test) accuracy = accuracy_score(y_test, y_pred) # Validação cruzada cv_scores = cross_val_score(qda, X_scaled, y, cv=5) qda_results[reg_param] = { 'model': qda, 'accuracy': accuracy, 'cv_mean': np.mean(cv_scores), 'cv_std': np.std(cv_scores) } print(f"Reg_param {reg_param}:") print(f" Acurácia teste: {accuracy:.3f}") print(f" CV Score: {np.mean(cv_scores):.3f} ± {np.std(cv_scores):.3f}") ''' Busca automática do melhor parâmetro de shrinkage ''' print("\n4. BUSCA AUTOMÁTICA DO MELHOR SHRINKAGE") # Usando GridSearchCV para encontrar o melhor shrinkage param_grid = { 'shrinkage': [None, 'auto', 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] } lda_gs = LinearDiscriminantAnalysis(solver='lsqr') grid_search = GridSearchCV(lda_gs, param_grid, cv=5, scoring='accuracy') grid_search.fit(X_scaled, y) print(f"Melhor parâmetro de shrinkage: {grid_search.best_params_}") print(f"Melhor score de validação cruzada: {grid_search.best_score_:.3f}") ''' Visualização dos resultados - VERSÃO CORRIGIDA ''' print("\n5. VISUALIZAÇÃO DOS RESULTADOS") # Preparando dados para plotagem - CORREÇÃO: garantir tamanhos consistentes shrinkage_labels = [str(s) for s in shrinkage_values] lda_accuracies = [lda_results[s]['accuracy'] for s in shrinkage_values] lda_cv_means = [lda_results[s]['cv_mean'] for s in shrinkage_values] lda_cv_stds = [lda_results[s]['cv_std'] for s in shrinkage_values] qda_reg_labels = [str(r) for r in reg_param_values] qda_accuracies = [qda_results[r]['accuracy'] for r in reg_param_values] qda_cv_means = [qda_results[r]['cv_mean'] for r in reg_param_values] qda_cv_stds = [qda_results[r]['cv_std'] for r in reg_param_values] fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 12)) # Plot 1: LDA - Acurácia vs Shrinkage ax1.errorbar(range(len(shrinkage_labels)), lda_accuracies, yerr=lda_cv_stds, fmt='o-', capsize=5, label='Teste ± CV std') ax1.set_xticks(range(len(shrinkage_labels))) ax1.set_xticklabels(shrinkage_labels) ax1.set_xlabel('Valor de Shrinkage') ax1.set_ylabel('Acurácia') ax1.set_title('LDA: Acurácia vs Shrinkage') ax1.legend() ax1.grid(True, alpha=0.3) # Plot 2: QDA - Acurácia vs Regularização ax2.errorbar(range(len(qda_reg_labels)), qda_accuracies, yerr=qda_cv_stds, fmt='o-', capsize=5, label='Teste ± CV std', color='orange') ax2.set_xticks(range(len(qda_reg_labels))) ax2.set_xticklabels(qda_reg_labels) ax2.set_xlabel('Valor de reg_param') ax2.set_ylabel('Acurácia') ax2.set_title('QDA: Acurácia vs Regularização') ax2.legend() ax2.grid(True, alpha=0.3) # Plot 3: Comparação LDA vs QDA no melhor parâmetro best_lda_shrinkage = grid_search.best_params_['shrinkage'] best_lda_score = grid_search.best_score_ best_qda_reg = max(qda_results.items(), key=lambda x: x[1]['cv_mean'])[0] best_qda_score = qda_results[best_qda_reg]['cv_mean'] models_comparison = ['LDA (melhor)', 'QDA (melhor)'] scores_comparison = [best_lda_score, best_qda_score] bars = ax3.bar(models_comparison, scores_comparison, color=['blue', 'orange']) ax3.set_ylabel('Acurácia (Validação Cruzada)') ax3.set_title('Comparação: Melhor LDA vs Melhor QDA') ax3.grid(True, alpha=0.3) # Adicionando valores nas barras for bar, score in zip(bars, scores_comparison): ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01, f'{score:.3f}', ha='center', va='bottom') # Plot 4: Análise da estabilidade dos modelos - CORREÇÃO: garantir tamanhos iguais shrinkage_stability = [lda_results[s]['cv_std'] for s in shrinkage_values] # CORREÇÃO: Usar apenas os valores de reg_param que temos para QDA reg_stability = [qda_results[r]['cv_std'] for r in reg_param_values] # CORREÇÃO: Criar arrays com o mesmo tamanho para comparação min_length = min(len(shrinkage_labels), len(qda_reg_labels)) x_pos = np.arange(min_length) width = 0.35 ax4.bar(x_pos - width/2, shrinkage_stability[:min_length], width, label='LDA', alpha=0.7) ax4.bar(x_pos + width/2, reg_stability[:min_length], width, label='QDA', alpha=0.7) ax4.set_xticks(x_pos) ax4.set_xticklabels(shrinkage_labels[:min_length]) ax4.set_xlabel('Parâmetro de Regularização') ax4.set_ylabel('Desvio Padrão (CV)') ax4.set_title('Estabilidade: Desvio Padrão na Validação Cruzada') ax4.legend() ax4.grid(True, alpha=0.3) plt.tight_layout() plt.show() ''' Análise de casos extremos - VERSÃO CORRIGIDA ''' print("\n6. ANÁLISE DE CASOS EXTREMOS") # Caso com dimensionalidade ainda mais alta print("Caso com alta dimensionalidade:") X_high, y_high = make_classification( n_samples=50, # Muito poucas amostras n_features=30, # Dimensionalidade muito alta n_informative=5, n_classes=2, random_state=42 ) X_high_scaled = StandardScaler().fit_transform(X_high) # Testando LDA sem e com shrinkage - CORREÇÃO: usar solver apropriado try: lda_no_shrinkage = LinearDiscriminantAnalysis(solver='lsqr', shrinkage=None) lda_no_shrinkage.fit(X_high_scaled, y_high) score_no_shrink = lda_no_shrinkage.score(X_high_scaled, y_high) print(f" LDA sem shrinkage: OK (score: {score_no_shrink:.3f})") except Exception as e: print(f" LDA sem shrinkage falhou: {e}") lda_with_shrinkage = LinearDiscriminantAnalysis(solver='lsqr', shrinkage='auto') lda_with_shrinkage.fit(X_high_scaled, y_high) score_with_shrink = lda_with_shrinkage.score(X_high_scaled, y_high) print(f" LDA com shrinkage automático: OK (score: {score_with_shrink:.3f})") ''' Interpretação prática dos resultados - VERSÃO ATUALIZADA ''' print("\n7. INTERPRETAÇÃO PRÁTICA") print("Os resultados demonstram que:") print(f" - Melhor shrinkage para LDA: {best_lda_shrinkage}") print(f" - Melhor reg_param para QDA: {best_qda_reg}") print(f" - Melhor score LDA: {best_lda_score:.3f}") print(f" - Melhor score QDA: {best_qda_score:.3f}") print(" - O shrinkage melhora a estabilidade numérica do LDA") print(" - Valores moderados de shrinkage geralmente oferecem o melhor trade-off") print("\nRecomendações práticas:") print(" - Use shrinkage quando tiver muitas features ou poucas amostras") print(" - Experimente com 'auto' primeiro para uma configuração rápida") print(" - Use validação cruzada para tuning fino do parâmetro") print(" - Considere QDA com reg_param quando as covariâncias forem diferentes") |
Interpretação dos Resultados
Analisando os experimentos, podemos observar padrões importantes:
- O encolhimento melhora significativamente a estabilidade dos classificadores
- Valores intermediários de shrinkage geralmente produzem os melhores resultados
- O parâmetro ‘auto’ oferece uma solução prática e eficiente
- Em casos de alta dimensionalidade, o shrinkage torna-se essencial
Conexões com Tópicos Anteriores
Similarmente aos conceitos de regularização que discutimos em regressão linear, o encolhimento no LDA e QDA:
- Balanceia o trade-off entre viés e variância
- Melhora a generalização em dados limitados
- Lida com problemas de multicolinearidade
- Oferece robustez contra overfitting
Considerações Práticas
Quando Usar Encolhimento
- Número de features maior que número de amostras
- Matrizes de covariância singular ou mal-condicionada
- Dados com alta correlação entre features
- Aplicações onde robustez é mais importante que precisão máxima
Escolha do Parâmetro
Inegavelmente, a seleção do parâmetro de encolhimento deve considerar:
- Usar validação cruzada para tuning
- Começar com valores padrão ou ‘auto’
- Considerar o trade-off entre performance e complexidade computacional
- Avaliar a sensibilidade do modelo a diferentes valores
Conclusão
O encolhimento representa uma ferramenta essencial para aplicar LDA e QDA em cenários práticos, especialmente quando lidamos com as limitações de dados do mundo real. Embora adicione complexidade ao modelo, os benefícios em termos de robustez e generalização frequentemente justificam seu uso.
Portanto, ao implementar classificadores discriminantes em aplicações reais, o uso criterioso de técnicas de encolhimento pode significar a diferença entre um modelo que funciona bem na prática e um que falha devido a problemas numéricos ou overfitting.
Referência
Este post explora o item 1.2.4. Encolhimento da documentação do scikit-learn:
https://scikit-learn.org/0.21/modules/lda_qda.html