Continuando nossa exploração detalhada das Máquinas de Vetores de Suporte, abordaremos agora o desafio crucial dos problemas desbalanceados. Primordialmente, datasets com distribuição desigual de classes representam um cenário comum em aplicações do mundo real, onde estratégias específicas são necessárias para garantir performance adequada.
O Desafio do Desbalanceamento
Analogamente aos problemas balanceados que discutimos anteriormente, os datasets desbalanceados apresentam desafios únicos para classificadores SVM. Conforme observamos na implementação padrão, as SVM buscam maximizar a margem global, o que pode levar a viés em favor da classe majoritária quando as distribuições são assimétricas.
Impacto no Hiperplano de Decisão
Em problemas desbalanceados, o hiperplano ótimo tende a ser deslocado em direção à classe minoritária:
\(w^T x + b = 0\)Onde o viés b é influenciado pela distribuição desbalanceada das classes.
Estratégias para Lidar com Desbalanceamento
Parâmetro class_weight
O scikit-learn oferece o parâmetro class_weight que permite atribuir pesos diferentes às classes:
class_weight='balanced': Pesos inversamente proporcionais às frequências das classesclass_weight={0: w0, 1: w1}: Pesos customizados para cada classe
Cálculo dos Pesos Automáticos
Quando class_weight='balanced', os pesos são calculados como:
Onde n é o número total de amostras, k o número de classes, e n_j o número de amostras na classe j.
Modificação da Função Objetivo
Com pesos de classe, a função objetivo do SVM torna-se:
\(\min_{w, b, \xi} \frac{1}{2} \|w\|^2 + C \sum_{i=1}^n w_{y_i} \xi_i\)Sujeito a:
\(y_i(w^T x_i + b) \geq 1 – \xi_i \quad \text{e} \quad \xi_i \geq 0\)Onde w_{y_i} é o peso associado à classe da amostra i.
Técnicas Complementares
Ampliação de Dados (Oversampling)
- SMOTE: Synthetic Minority Over-sampling Technique
- ADASYN: Adaptive Synthetic Sampling
- Random Oversampling: Duplicação de amostras da classe minoritária
Redução de Dados (Undersampling)
- Random Undersampling: Remoção aleatória de amostras da classe majoritária
- Cluster Centroids: Baseado em agrupamento
- Tomek Links: Remoção de pares de amostras próximas de classes diferentes
Métricas de Avaliação para Desbalanceamento
Similarmente às métricas padrão que exploramos anteriormente, problemas desbalanceados requerem avaliação mais sofisticada:
- Precision: TP / (TP + FP)
- Recall: TP / (TP + FN)
- F1-Score: Média harmônica entre precision e recall
- ROC-AUC: Área sob a curva ROC
- Precision-Recall AUC: Área sob a curva Precision-Recall
Implementação no scikit-learn
Parâmetros Relevantes
class_weight: Controle de pesos por classeC: Parâmetro de regularização (pode ser ajustado)kernel: Escolha do kernel (RBF geralmente mais robusto)
Integração com Técnicas de Amostragem
O scikit-learn oferece classes para amostragem através do módulo imbalanced-learn (não nativo):
RandomOverSamplerSMOTERandomUnderSampler
Conexões com Tópicos Anteriores
Analogamente aos conceitos que exploramos em classificação balanceada, o tratamento de desbalanceamento:
- Mantém os princípios fundamentais de maximização de margem
- Estende a formulação matemática através de pesos
- Preserva a interpretação dos vetores de suporte
- Requer avaliação mais cuidadosa similar a problemas complexos
Exemplo Prático em Python
Para ilustrar as estratégias de tratamento de problemas desbalanceados com SVM, implementemos um estudo 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 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
import numpy as np import matplotlib.pyplot as plt from sklearn.svm import SVC from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.metrics import precision_score, recall_score, f1_score, roc_auc_score from sklearn.datasets import make_classification from sklearn.preprocessing import StandardScaler import warnings warnings.filterwarnings('ignore') ''' Estudo comparativo de estratégias para problemas desbalanceados com SVM ''' print("=== PROBLEMAS DESBALANCEADOS COM SVM ===") # Criando datasets com diferentes níveis de desbalanceamento print("\n1. CONFIGURAÇÃO DOS CENÁRIOS DESBALANCEADOS") # Cenário 1: Desbalanceamento moderado (80-20) X_moderado, y_moderado = make_classification( n_samples=1000, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=1, weights=[0.8, 0.2], random_state=42 ) # Cenário 2: Desbalanceamento severo (90-10) X_severo, y_severo = make_classification( n_samples=1000, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=1, weights=[0.9, 0.1], random_state=42 ) # Cenário 3: Desbalanceamento extremo (95-5) X_extremo, y_extremo = make_classification( n_samples=1000, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=1, weights=[0.95, 0.05], random_state=42 ) cenarios = [ ('Moderado (80-20)', X_moderado, y_moderado), ('Severo (90-10)', X_severo, y_severo), ('Extremo (95-5)', X_extremo, y_extremo) ] ''' Configuração das estratégias de tratamento ''' print("\n2. CONFIGURAÇÃO DAS ESTRATÉGIAS") # Diferentes abordagens para comparação estrategias = { 'SVC Padrão': SVC(kernel='rbf', random_state=42), 'SVC Class Weight Balanced': SVC(kernel='rbf', class_weight='balanced', random_state=42), 'SVC Class Weight Custom': SVC(kernel='rbf', class_weight={0: 1, 1: 5}, random_state=42), 'SVC Ajustado C': SVC(kernel='rbf', C=0.1, class_weight='balanced', random_state=42) } ''' Avaliação sistemática por cenário ''' print("\n3. AVALIAÇÃO DAS ESTRATÉGIAS POR CENÁRIO") resultados_completos = {} for cenario_nome, X, y in cenarios: print(f"\n--- {cenario_nome} ---") # Estatísticas do dataset unique, counts = np.unique(y, return_counts=True) n_total = len(y) ratios = {cls: count/n_total for cls, count in zip(unique, counts)} print(f"Distribuição: Classe 0: {counts[0]} ({ratios[0]:.1%}), " f"Classe 1: {counts[1]} ({ratios[1]:.1%})") # Padronização scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Divisão treino-teste estratificada X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.3, random_state=42, stratify=y ) cenario_resultados = {} for estrategia_nome, estrategia_model in estrategias.items(): print(f" Avaliando {estrategia_nome}...") try: # Treinamento estrategia_model.fit(X_train, y_train) # Previsões y_pred = estrategia_model.predict(X_test) # Métricas básicas accuracy = accuracy_score(y_test, y_pred) precision = precision_score(y_test, y_pred) recall = recall_score(y_test, y_pred) f1 = f1_score(y_test, y_pred) # Probabilidades para AUC (se disponível) if hasattr(estrategia_model, 'predict_proba'): y_proba = estrategia_model.predict_proba(X_test)[:, 1] roc_auc = roc_auc_score(y_test, y_proba) else: # Usar decision function como proxy decision_scores = estrategia_model.decision_function(X_test) roc_auc = roc_auc_score(y_test, decision_scores) # Coletar informações model_info = { 'model': estrategia_model, 'accuracy': accuracy, 'precision': precision, 'recall': recall, 'f1_score': f1, 'roc_auc': roc_auc, 'predictions': y_pred } # Vetores de suporte por classe if hasattr(estrategia_model, 'support_vectors_'): support_indices = estrategia_model.support_ support_by_class = [] for cls in unique: class_support = np.sum(y_train[support_indices] == cls) support_by_class.append(class_support) model_info['support_by_class'] = support_by_class cenario_resultados[estrategia_nome] = model_info print(f" Accuracy: {accuracy:.3f}, Precision: {precision:.3f}, " f"Recall: {recall:.3f}, F1: {f1:.3f}, AUC: {roc_auc:.3f}") except Exception as e: print(f" ERRO: {e}") cenario_resultados[estrategia_nome] = {'error': str(e)} resultados_completos[cenario_nome] = cenario_resultados ''' Visualização das fronteiras de decisão ''' print("\n4. VISUALIZAÇÃO DAS FRONTEIRAS DE DECISÃO") for cenario_nome, X, y in cenarios: print(f"\nVisualizando {cenario_nome}...") scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Criar mesh h = 0.02 x_min, x_max = X_scaled[:, 0].min() - 1, X_scaled[:, 0].max() + 1 y_min, y_max = X_scaled[:, 1].min() - 1, X_scaled[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) # Configurar subplots estrategias_visuais = list(estrategias.keys()) n_strategies = len(estrategias_visuais) fig, axes = plt.subplots(2, 2, figsize=(15, 12)) axes = axes.flatten() # Dados originais scatter = axes[0].scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap='coolwarm', alpha=0.8) axes[0].set_title(f'{cenario_nome}\nDados Originais') axes[0].set_xlabel('Feature 1') axes[0].set_ylabel('Feature 2') plt.colorbar(scatter, ax=axes[0]) # Estratégias for idx, estrategia_nome in enumerate(estrategias_visuais, 1): if idx >= len(axes): break if estrategia_nome in resultados_completos[cenario_nome]: resultados = resultados_completos[cenario_nome][estrategia_nome] model = resultados['model'] # Prever no mesh Z = model.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) # Plot contour = axes[idx].contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm') axes[idx].scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap='coolwarm', alpha=0.8) # Adicionar métricas no título f1 = resultados['f1_score'] recall = resultados['recall'] title = f'{estrategia_nome}\nF1: {f1:.3f}, Recall: {recall:.3f}' axes[idx].set_title(title) axes[idx].set_xlabel('Feature 1') axes[idx].set_ylabel('Feature 2') # Remover subplots não utilizados for i in range(len(estrategias_visuais) + 1, len(axes)): fig.delaxes(axes[i]) plt.tight_layout() plt.show() ''' Análise detalhada do recall por classe - VERSÃO CORRIGIDA ''' print("\n5. ANÁLISE DETALHADA DO RECALL") for cenario_nome, X, y in cenarios: print(f"\n{cenario_nome}:") print("="*80) print(f"{'Estratégia':<25} {'Recall Classe 0':<15} {'Recall Classe 1':<15} {'Diferença':<12}") print("="*80) if cenario_nome in resultados_completos: # CORREÇÃO: Recalcular y_test para cada cenário scaler = StandardScaler() X_scaled = scaler.fit_transform(X) X_train, X_test_correto, y_train, y_test_correto = train_test_split( X_scaled, y, test_size=0.3, random_state=42, stratify=y ) for estrategia_nome, resultados in resultados_completos[cenario_nome].items(): if 'error' not in resultados: # CORREÇÃO: Usar y_test_correto em vez de tentar recuperar do dicionário y_pred = resultados['predictions'] # Verificar se os tamanhos são compatíveis if len(y_pred) == len(y_test_correto): recall_0 = recall_score(y_test_correto, y_pred, pos_label=0) recall_1 = recall_score(y_test_correto, y_pred, pos_label=1) diferenca = abs(recall_0 - recall_1) print(f"{estrategia_nome:<25} {recall_0:<15.3f} {recall_1:<15.3f} {diferenca:<12.3f}") else: print(f"{estrategia_nome:<25} {'Tamanho incompatível':<45}") ''' Análise dos vetores de suporte - VERSÃO CORRIGIDA ''' print("\n6. ANÁLISE DOS VETORES DE SUPORTE") for cenario_nome, X, y in cenarios: print(f"\n{cenario_nome} - Distribuição dos Vetores de Suporte:") print("="*60) if cenario_nome in resultados_completos: for estrategia_nome, resultados in resultados_completos[cenario_nome].items(): if 'error' not in resultados and 'support_by_class' in resultados: support_by_class = resultados['support_by_class'] total_support = sum(support_by_class) if total_support > 0: ratios = [s/total_support for s in support_by_class] print(f"{estrategia_nome:<25}: Classe 0: {support_by_class[0]} ({ratios[0]:.1%}), " f"Classe 1: {support_by_class[1]} ({ratios[1]:.1%})") ''' Comparação de performance geral - VERSÃO CORRIGIDA ''' print("\n7. COMPARAÇÃO DE PERFORMANCE GERAL") print("\nTabela Comparativa - F1-Score por Cenário e Estratégia:") print("\n" + "="*90) header = f"{'Cenário':<20} {'SVC Padrão':<12} {'SVC Balanced':<12} {'SVC Custom':<12} {'SVC Ajustado':<12}" print(header) print("="*90) for cenario_nome, X, y in cenarios: if cenario_nome in resultados_completos: row = f"{cenario_nome:<20}" for estrategia in ['SVC Padrão', 'SVC Class Weight Balanced', 'SVC Class Weight Custom', 'SVC Ajustado C']: if (estrategia in resultados_completos[cenario_nome] and 'error' not in resultados_completos[cenario_nome][estrategia]): f1 = resultados_completos[cenario_nome][estrategia]['f1_score'] row += f" {f1:<11.3f}" else: row += f" {'N/A':<11}" print(row) ''' Análise do trade-off precision-recall - VERSÃO CORRIGIDA ''' print("\n8. ANÁLISE DO TRADE-OFF PRECISION-RECALL") for cenario_nome, X, y in cenarios: print(f"\n{cenario_nome} - Trade-off Precision vs Recall:") if cenario_nome in resultados_completos: for estrategia_nome, resultados in resultados_completos[cenario_nome].items(): if 'error' not in resultados: precision = resultados['precision'] recall = resultados['recall'] f1 = resultados['f1_score'] print(f" {estrategia_nome:<25}: Precision={precision:.3f}, " f"Recall={recall:.3f}, F1={f1:.3f}") # CORREÇÃO: Adicionar análise de matriz de confusão print("\n9. ANÁLISE DAS MATRIZES DE CONFUSÃO") for cenario_nome, X, y in cenarios: print(f"\n{cenario_nome} - Matrizes de Confusão:") print("="*50) if cenario_nome in resultados_completos: # Recalcular split para este cenário scaler = StandardScaler() X_scaled = scaler.fit_transform(X) X_train, X_test_conf, y_train, y_test_conf = train_test_split( X_scaled, y, test_size=0.3, random_state=42, stratify=y ) for estrategia_nome, resultados in resultados_completos[cenario_nome].items(): if 'error' not in resultados: y_pred = resultados['predictions'] if len(y_pred) == len(y_test_conf): cm = confusion_matrix(y_test_conf, y_pred) print(f"\n{estrategia_nome}:") print(cm) # Adicionar interpretação tn, fp, fn, tp = cm.ravel() print(f" TN: {tn}, FP: {fp}, FN: {fn}, TP: {tp}") else: print(f"\n{estrategia_nome}: Tamanhos incompatíveis para matriz de confusão") ''' Recomendações práticas para problemas desbalanceados - VERSÃO ATUALIZADA ''' print("\n10. RECOMENDAÇÕES PRÁTICAS") print("\nBaseado na análise experimental:") print("\nEscolha da Estratégia por Nível de Desbalanceamento:") print(" - Desbalanceamento leve (< 70-30): SVC padrão pode ser suficiente") print(" - Desbalanceamento moderado (70-30 a 85-15): class_weight='balanced'") print(" - Desbalanceamento severo (> 85-15): Pesos customizados + ajuste de C") print("\nConfiguração de Pesos:") print(" - 'balanced': Boa opção padrão para maioria dos casos") print(" - Customizado: Quando se conhece os custos de erro por classe") print(" - Baseado em dados: Calcular pesos baseados na distribuição real") print("\nAjuste de Hiperparâmetros:") print(" - Reduzir C para aumentar regularização em dados desbalanceados") print(" - Kernel RBF geralmente mais robusto que linear") print(" - Validar com validação cruzada estratificada") print("\nMétricas de Avaliação:") print(" - F1-Score: Métrica balanceada para comparação geral") print(" - Recall: Crítico para detecção da classe minoritária") print(" - Precision-Recall AUC: Melhor que ROC-AUC para desbalanceamento") print(" - Matriz de confusão: Para análise detalhada dos erros") print("\nTécnicas Complementares:") print(" - Amostragem: SMOTE para oversampling inteligente") print(" - Ensemble: Combinar múltiplas estratégias") print(" - Threshold moving: Ajustar limiar de decisão pós-treinamento") print("\nConsiderações por Domínio:") print(" - Médico: Alta recall para detecção de doenças raras") print(" - Fraude: Balancear recall e precision baseado em custos") print(" - Manutenção: Recall crítico para detecção de falhas") print(" - Marketing: Precision importante para custo de aquisição") print("\nBoas Práticas:") print(" - Sempre usar validação estratificada") print(" - Reportar múltiplas métricas, não apenas acurácia") print(" - Considerar custos de negócio na escolha da estratégia") print(" - Documentar a distribuição das classes no dataset") print(" - Testar diferentes abordagens antes de decidir") # CORREÇÃO: Adicionar resumo dos resultados principais print("\n11. RESUMO DOS RESULTADOS PRINCIPAIS") print("\nPrincipais Insights Obtidos:") for cenario_nome, X, y in cenarios: if cenario_nome in resultados_completos: print(f"\n{cenario_nome}:") # Encontrar melhor estratégia por F1-Score melhor_estrategia = None melhor_f1 = 0 for estrategia_nome, resultados in resultados_completos[cenario_nome].items(): if 'error' not in resultados and resultados['f1_score'] > melhor_f1: melhor_f1 = resultados['f1_score'] melhor_estrategia = estrategia_nome if melhor_estrategia: melhor_resultado = resultados_completos[cenario_nome][melhor_estrategia] print(f" Melhor estratégia: {melhor_estrategia} (F1: {melhor_f1:.3f})") print(f" Precision: {melhor_resultado['precision']:.3f}, " f"Recall: {melhor_resultado['recall']:.3f}, " f"AUC: {melhor_resultado['roc_auc']:.3f}") |
Interpretação dos Resultados
Analisando os experimentos com problemas desbalanceados, podemos observar padrões importantes:
- O SVM padrão tende a favorecer a classe majoritária, resultando em alto recall para a classe 0 mas baixo recall para a classe 1
- O uso de
class_weight='balanced'equilibra significativamente o recall entre classes - Estratégias customizadas permitem ajuste fino baseado em custos específicos de erro
- O ajuste do parâmetro C em conjunto com pesos de classe pode melhorar ainda mais a performance
Considerações para Aplicações Práticas
Seleção da Estratégia Ótima
Inegavelmente, a escolha da estratégia depende do contexto específico do problema:
- Acurácia balanceada: Quando todas as classes são igualmente importantes
- Recall da classe minoritária: Para detecção de eventos raros ou críticos
- Precision da classe minoritária: Quando falsos positivos são custosos
- F1-Score: Para balanceamento geral entre precision e recall
Avaliação de Custo-Benefício
Para aplicações reais, a escolha deve considerar os custos associados a diferentes tipos de erro:
- Custo de falsos negativos (não detectar a classe minoritária)
- Custo de falsos positivos (alarmes falsos)
- Benefício de verdadeiros positivos
- Impacto operacional das decisões erradas
Conclusão
O tratamento adequado de problemas desbalanceados representa uma habilidade essencial para praticantes de machine learning. Embora as SVM sejam classificadores poderosos, seu desempenho em datasets desbalanceados depende criticamente da escolha apropriada de estratégias de ponderação e configuração de parâmetros.
Portanto, o domínio dessas técnicas permite extrair o máximo valor dos classificadores SVM em cenários do mundo real, onde distribuições desbalanceadas são a regra rather than a exceção, garantindo modelos robustos e alinhados com os objetivos de negócio específicos.
Referência
Este post explora o item 1.4.1.3. Problemas desbalanceados da documentação do scikit-learn:
https://scikit-learn.org/0.21/modules/svm.html#unbalanced-problems