Continuando nossa exploração detalhada das Máquinas de Vetores de Suporte, focaremos agora especificamente na extensão para problemas de classificação multiclasse. Primordialmente, as SVM foram originalmente concebidas para classificação binária, mas estratégias inteligentes permitem sua aplicação eficaz em problemas com múltiplas classes.
O Desafio da Classificação Multiclasse
Analogamente aos problemas binários que discutimos anteriormente, a classificação multiclasse requer abordagens especiais pois as SVM são fundamentalmente classificadores binários. Conforme observamos na implementação original, o algoritmo busca um hiperplano ótimo para separar duas classes, o que precisa ser estendido para K classes.
Estratégias de Extensão para Multiclasse
One-vs-Rest (OvR) – Um contra Todos
Esta estratégia treina K classificadores binários, onde cada classificador é treinado para distinguir uma classe específica contra todas as outras:
\(f_i(x) = \text{sign}(w_i^T x + b_i) \quad \text{para } i = 1, \dots, K\)A decisão final é tomada selecionando a classe com maior valor da função de decisão:
\(\hat{y} = \arg\max_{i=1,\dots,K} (w_i^T x + b_i)\)One-vs-One (OvO) – Um contra Um
Esta abordagem treina classificadores binários para cada par de classes. O número total de classificadores é dado por:
\(\frac{K(K-1)}{2}\)Cada classificador vota na sua classe preferida, e a classe com mais votos é selecionada.
Implementação no scikit-learn
Suporte Nativo
As classes SVC e LinearSVC suportam nativamente classificação multiclasse através do parâmetro:
decision_function_shape: ‘ovr’ ou ‘ovo’
Estratégias Especializadas
O scikit-learn oferece wrappers especializados:
OneVsRestClassifier: Implementa estratégia One-vs-RestOneVsOneClassifier: Implementa estratégia One-vs-One
Vantagens e Desvantagens das Estratégias
One-vs-Rest (OvR)
Vantagens:
- Menos classificadores para treinar (K vs K(K-1)/2)
- Mais eficiente computacionalmente
- Melhor para datasets grandes
Desvantagens:
- Pode sofrer com desbalanceamento de classes
- Problemas com classes sobrepostas
One-vs-One (OvO)
Vantagens:
- Cada classificador lida com problema mais simples
- Mais robusto para classes sobrepostas
- Melhor para kernels não-lineares complexos
Desvantagens:
- Maior número de classificadores
- Mais lento para treinamento e predição
Considerações de Performance
Complexidade Computacional
A complexidade varia significativamente entre as estratégias:
- OvR: O(K × complexidade_binária)
- OvO: O(K² × complexidade_binária)
Seleção de Estratégia
A escolha depende de vários fatores:
- Número de classes (K)
- Tamanho do dataset
- Complexidade do kernel
- Recursos computacionais disponíveis
Matriz de Decisão Multiclasse
Para problemas multiclasse, a função decision_function retorna uma matriz onde cada linha corresponde a uma amostra e cada coluna ao score de uma classe:
Conexões com Tópicos Anteriores
Similarmente aos conceitos que exploramos em classificação binária, a extensão multiclasse:
- Mantém os mesmos princípios de maximização de margem
- Utiliza as mesmas funções kernel
- Preserva a interpretação dos vetores de suporte
- Oferece alternativas aos métodos probabilísticos multiclasse
Exemplo Prático em Python
Para ilustrar as diferentes estratégias de classificação multiclasse 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 |
import numpy as np import matplotlib.pyplot as plt from sklearn.svm import SVC from sklearn.multiclass import OneVsRestClassifier, OneVsOneClassifier from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.datasets import make_classification, load_iris, load_digits from sklearn.preprocessing import StandardScaler import warnings warnings.filterwarnings('ignore') ''' Estudo comparativo de estratégias multiclasse com SVM ''' print("=== CLASSIFICAÇÃO MULTICLASSE COM SVM ===") # Criando cenários multiclasse com diferentes características print("\n1. CONFIGURAÇÃO DOS CENÁRIOS MULTICLASSE") # Cenário 1: 3 classes bem separadas X_3classes, y_3classes = make_classification( n_samples=300, n_features=2, n_informative=2, n_redundant=0, n_classes=3, n_clusters_per_class=1, class_sep=2.0, random_state=42 ) # Cenário 2: 4 classes com sobreposição X_4classes, y_4classes = make_classification( n_samples=400, n_features=4, n_informative=4, n_redundant=0, n_classes=4, n_clusters_per_class=1, class_sep=1.0, random_state=42 ) # Cenário 3: Dataset Iris (3 classes) iris = load_iris() X_iris, y_iris = iris.data, iris.target # Cenário 4: Dataset Digits (10 classes) digits = load_digits() X_digits, y_digits = digits.data, digits.target cenarios = [ ('3 Classes Separadas', X_3classes, y_3classes, 3), ('4 Classes Sobrepostas', X_4classes, y_4classes, 4), ('Iris Dataset', X_iris, y_iris, 3), ('Digits Dataset', X_digits, y_digits, 10) ] ''' Configuração das estratégias multiclasse ''' print("\n2. CONFIGURAÇÃO DAS ESTRATÉGIAS MULTICLASSE") # Diferentes abordagens para comparação estrategias = { 'SVC OvR Nativo': SVC(kernel='rbf', decision_function_shape='ovr', random_state=42), 'SVC OvO Nativo': SVC(kernel='rbf', decision_function_shape='ovo', random_state=42), 'OneVsRestClassifier': OneVsRestClassifier(SVC(kernel='rbf', random_state=42)), 'OneVsOneClassifier': OneVsOneClassifier(SVC(kernel='rbf', random_state=42)) } # Configurações específicas por número de classes param_grids = { '3 Classes Separadas': { 'SVC OvR Nativo': {'C': [0.1, 1.0, 10.0], 'gamma': [0.1, 1.0, 'scale']}, 'SVC OvO Nativo': {'C': [0.1, 1.0, 10.0], 'gamma': [0.1, 1.0, 'scale']} }, '4 Classes Sobrepostas': { 'SVC OvR Nativo': {'C': [0.1, 1.0, 10.0], 'gamma': [0.1, 1.0, 'scale']}, 'SVC OvO Nativo': {'C': [0.1, 1.0, 10.0], 'gamma': [0.1, 1.0, 'scale']} }, 'Iris Dataset': { 'SVC OvR Nativo': {'C': [0.1, 1.0, 10.0, 100.0], 'gamma': [0.01, 0.1, 1.0, 'scale']}, 'SVC OvO Nativo': {'C': [0.1, 1.0, 10.0, 100.0], 'gamma': [0.01, 0.1, 1.0, 'scale']} }, 'Digits Dataset': { 'SVC OvR Nativo': {'C': [0.1, 1.0, 10.0], 'gamma': [0.001, 0.01, 0.1, 'scale']}, 'SVC OvO Nativo': {'C': [0.1, 1.0, 10.0], 'gamma': [0.001, 0.01, 0.1, 'scale']} } } ''' Avaliação comparativa das estratégias ''' print("\n3. AVALIAÇÃO DAS ESTRATÉGIAS MULTICLASSE") resultados_completos = {} for cenario_nome, X, y, n_classes in cenarios: print(f"\n--- {cenario_nome} ({n_classes} classes) ---") print(f"Dimensões: {X.shape}") # Estatísticas das classes unique, counts = np.unique(y, return_counts=True) print(f"Distribuição: {dict(zip(unique, counts))}") # Padronização scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Divisão treino-teste 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: # Grid Search para tuning if (estrategia_nome in ['SVC OvR Nativo', 'SVC OvO Nativo'] and cenario_nome in param_grids): grid_search = GridSearchCV( estrategia_model, param_grids[cenario_nome][estrategia_nome], cv=5, scoring='accuracy', n_jobs=-1 ) grid_search.fit(X_train, y_train) best_model = grid_search.best_estimator_ best_params = grid_search.best_params_ else: # Para wrappers, usar configuração padrão best_model = estrategia_model best_model.fit(X_train, y_train) best_params = "Configuração padrão" # Previsões e métricas y_pred = best_model.predict(X_test) accuracy = accuracy_score(y_test, y_pred) # Validação cruzada cv_scores = cross_val_score(best_model, X_scaled, y, cv=5) cv_mean = np.mean(cv_scores) cv_std = np.std(cv_scores) # Coletar informações específicas info_modelo = { 'model': best_model, 'best_params': best_params, 'accuracy': accuracy, 'cv_mean': cv_mean, 'cv_std': cv_std, 'n_classes': n_classes } # Contar classificadores binários if hasattr(best_model, 'estimators_'): info_modelo['n_classificadores'] = len(best_model.estimators_) elif hasattr(best_model, 'coef_'): if len(best_model.coef_.shape) > 1: info_modelo['n_classificadores'] = best_model.coef_.shape[0] else: info_modelo['n_classificadores'] = 1 cenario_resultados[estrategia_nome] = info_modelo print(f" Acurácia: {accuracy:.3f}, CV: {cv_mean:.3f} ± {cv_std:.3f}") if 'n_classificadores' in info_modelo: print(f" Classificadores binários: {info_modelo['n_classificadores']}") except Exception as e: print(f" ERRO: {e}") cenario_resultados[estrategia_nome] = {'error': str(e)} resultados_completos[cenario_nome] = cenario_resultados ''' Análise das fronteiras de decisão para cenários 2D ''' print("\n4. ANÁLISE DAS FRONTEIRAS DE DECISÃO MULTICLASSE") # Focar em cenários 2D para visualização for cenario_nome, X, y, n_classes in cenarios: if X.shape[1] == 2: # Apenas dados 2D podem ser visualizados 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 = ['SVC OvR Nativo', 'SVC OvO Nativo'] n_strategies = len([e for e in estrategias_visuais if e in resultados_completos[cenario_nome]]) fig, axes = plt.subplots(1, n_strategies + 1, figsize=(5*(n_strategies + 1), 5)) if n_strategies + 1 == 1: axes = [axes] # Dados originais scatter = axes[0].scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap=plt.cm.Set1, 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=plt.cm.Set1) axes[idx].scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap=plt.cm.Set1, alpha=0.8) axes[idx].set_title(f'{estrategia_nome}\nAcurácia: {resultados["accuracy"]:.3f}') axes[idx].set_xlabel('Feature 1') axes[idx].set_ylabel('Feature 2') plt.tight_layout() plt.show() ''' Análise da matriz de decisão ''' print("\n5. ANÁLISE DA MATRIZ DE DECISÃO") cenario_analise = 'Iris Dataset' if cenario_analise in resultados_completos: print(f"\nAnalisando matriz de decisão - {cenario_analise}") for estrategia_nome in ['SVC OvR Nativo', 'SVC OvO Nativo']: if estrategia_nome in resultados_completos[cenario_analise]: resultados = resultados_completos[cenario_analise][estrategia_nome] model = resultados['model'] X, y = X_iris, y_iris scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Obter função de decisão decision_scores = model.decision_function(X_scaled[:10]) # Primeiras 10 amostras print(f"\n{estrategia_nome} - Scores de decisão (primeiras 10 amostras):") print(f"Shape: {decision_scores.shape}") print("Valores:") print(decision_scores.round(3)) ''' Análise de performance comparativa ''' print("\n6. ANÁLISE DE PERFORMANCE COMPARATIVA") print("\nTabela Comparativa - Acurácia por Cenário e Estratégia:") print("\n" + "="*100) header = f"{'Cenário':<20} {'Classes':<8} {'SVC OvR':<10} {'SVC OvO':<10} {'OvR Wrapper':<12} {'OvO Wrapper':<12} {'Classif. Binários':<15}" print(header) print("="*100) for cenario_nome, X, y, n_classes in cenarios: if cenario_nome in resultados_completos: row = f"{cenario_nome:<20} {n_classes:<8}" for estrategia in ['SVC OvR Nativo', 'SVC OvO Nativo', 'OneVsRestClassifier', 'OneVsOneClassifier']: if (estrategia in resultados_completos[cenario_nome] and 'error' not in resultados_completos[cenario_nome][estrategia]): acc = resultados_completos[cenario_nome][estrategia]['accuracy'] row += f" {acc:<9.3f}" # Adicionar número de classificadores se disponível if estrategia == 'OneVsRestClassifier': n_classif = n_classes elif estrategia == 'OneVsOneClassifier': n_classif = n_classes * (n_classes - 1) // 2 else: n_classif = "N/A" else: row += f" {'N/A':<9}" # Adicionar informação teórica sobre número de classificadores row += f" {f'OvR: {n_classes}, OvO: {n_classes*(n_classes-1)//2}':<15}" print(row) ''' Análise de tempo de treinamento e complexidade ''' print("\n7. ANÁLISE DE COMPLEXIDADE COMPUTACIONAL") print("\nComplexidade Teórica por Estratégia:") print("="*60) for n_classes in [3, 4, 5, 10]: n_ovr = n_classes n_ovo = n_classes * (n_classes - 1) // 2 ratio = n_ovo / n_ovr print(f"{n_classes} classes: OvR={n_ovr} classif., OvO={n_ovo} classif. (ratio: {ratio:.1f}x)") ''' Recomendações práticas para classificação multiclasse ''' print("\n8. RECOMENDAÇÕES PRÁTICAS") print("\nBaseado na análise experimental:") print("\nEscolha da Estratégia:") print(" - OvR (One-vs-Rest): Para até ~10 classes ou datasets grandes") print(" - OvO (One-vs-One): Para problemas complexos com poucas classes") print(" - SVC Nativo: Geralmente preferível sobre wrappers") print("\nConsiderações por Número de Classes:") print(" - 2-5 classes: Ambas estratégias são viáveis") print(" - 6-10 classes: Preferir OvR para eficiência") print(" - 10+ classes: OvR é quase sempre melhor") print("\nOtimização de Parâmetros:") print(" - C: Pode precisar ajuste diferente para multiclasse") print(" - gamma: Similar ao caso binário") print(" - Usar validação cruzada estratificada") print("\nAvaliação de Performance:") print(" - Matriz de confusão para análise detalhada") print(" - Métricas por classe (precisão, recall, F1)") print(" - Considerar balanced accuracy para classes desbalanceadas") print("\nCasos de Uso Específicos:") print(" - Reconhecimento de dígitos: OvR geralmente suficiente") print(" - Classificação de texto: OvR para muitas categorias") print(" - Problemas médicos: OvO para diagnósticos complexos") print(" - Visão computacional: Depende do número de objetos") print("\nConsiderações Finais:") print(" - OvR é mais eficiente e geralmente performa similarmente") print(" - OvO pode ser melhor para problemas muito complexos") print(" - Sempre testar ambas estratégias para problemas críticos") print(" - Considerar custo computacional vs ganho de performance") |
Interpretação dos Resultados
Analisando os experimentos de classificação multiclasse, podemos observar padrões importantes:
- As estratégias OvR e OvO geralmente produzem resultados similares em termos de acurácia
- OvR é computacionalmente mais eficiente, especialmente com muitas classes
- OvO pode oferecer vantagens em problemas com classes muito sobrepostas
- A implementação nativa do scikit-learn geralmente é preferível aos wrappers
Considerações para Aplicações Práticas
Seleção de Estratégia
Inegavelmente, a escolha entre OvR e OvO depende de múltiplos fatores:
- Número de classes: OvR escala linearmente, OvO escala quadraticamente
- Complexidade do problema: OvO pode capturar relações mais complexas
- Recursos computacionais: OvR é mais eficiente em memória e tempo
- Balanceamento das classes: OvO é mais robusto a desbalanceamento
Análise da Matriz de Decisão
Para problemas multiclasse, a análise da matriz de decisão oferece insights valiosos:
- Valores altos indicam confiança na classificação
- Valores próximos sugerem ambiguidade entre classes
- Padrões sistemáticos podem indicar problemas no modelo
Conclusão
A classificação multiclasse com SVM representa uma extensão poderosa e bem fundamentada dos princípios de maximização de margem. Embora as SVM tenham sido originalmente concebidas para problemas binários, as estratégias OvR e OvO permitem sua aplicação eficaz em uma ampla gama de problemas do mundo real.
Portanto, o entendimento profundo dessas estratégias é essencial para qualquer praticante de machine learning, permitindo selecionar a abordagem mais adequada para cada problema específico e extrair o máximo potencial dos classificadores SVM em cenários multiclasse.
Referência
Este post explora o item 1.4.1.1. Classificação multiclasse da documentação do scikit-learn:
https://scikit-learn.org/0.21/modules/svm.html#multi-class-classification