Continuando nossa exploração do guia do scikit-learn, chegamos a um dos algoritmos mais influentes e amplamente utilizados no machine learning: as Máquinas de Vetores de Suporte (SVM). Primordialmente, as SVM representam uma abordagem elegante baseada em teoria de aprendizado estatístico que combina princípios de maximização de margens com a flexibilidade dos métodos de kernel.
Conceitos Fundamentais
Analogamente aos classificadores lineares que discutimos anteriormente, as SVM buscam encontrar um hiperplano ótimo para separar classes. Contudo, a abordagem das SVM é distintiva pois focaliza na maximização da margem entre as classes, o que frequentemente leva a melhor generalização.
O Hiperplano Ótimo
As SVM buscam o hiperplano que maximiza a margem entre as classes mais próximas:
\(w^T x + b = 0\)Onde w é o vetor normal ao hiperplano e b é o termo de bias. A margem é definida pelas linhas paralelas:
\(w^T x + b = \pm 1\)Formulação Matemática
Problema de Otimização
O problema de otimização das SVM pode ser formulado como:
\(\min_{w, b} \frac{1}{2} \|w\|^2\)Sujeito a:
\(y_i(w^T x_i + b) \geq 1 \quad \text{para } i = 1, \dots, n\)Forma Dual e Vetores de Suporte
Através da formulação dual, obtemos:
\(\max_{\alpha} \sum_{i=1}^n \alpha_i – \frac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j x_i^T x_j\)Sujeito a:
\(\alpha_i \geq 0 \quad \text{e} \quad \sum_{i=1}^n \alpha_i y_i = 0\)Os pontos com α_i > 0 são os vetores de suporte, que definem o hiperplano de decisão.
SVM com Margem Suave
Para dados não linearmente separáveis, introduzimos variáveis de folga (slack variables):
\(\min_{w, b, \xi} \frac{1}{2} \|w\|^2 + C \sum_{i=1}^n \xi_i\)Sujeito a:
\(y_i(w^T x_i + b) \geq 1 – \xi_i \quad \text{e} \quad \xi_i \geq 0\)O parâmetro C controla o trade-off entre maximização da margem e minimização do erro de classificação.
Kernels em SVM
Similarmente à Regressão de Crista do Kernel que exploramos anteriormente, as SVM podem utilizar kernels para lidar com dados não linearmente separáveis:
- linear: \(K(x_i, x_j) = x_i^T x_j\)
- polynomial: \(K(x_i, x_j) = (\gamma x_i^T x_j + r)^d\)
- rbf: \(K(x_i, x_j) = \exp(-\gamma \|x_i – x_j\|^2)\)
- sigmoid: \(K(x_i, x_j) = \tanh(\gamma x_i^T x_j + r)\)
Implementação no scikit-learn
O scikit-learn oferece duas implementações principais de SVM:
SVC (Support Vector Classification)
Baseada na biblioteca libsvm, oferece suporte a múltiplas classes através das estratégias “one-vs-one” ou “one-vs-rest”.
LinearSVC
Implementação otimizada para kernels lineares, baseada na biblioteca liblinear, geralmente mais eficiente para datasets grandes.
Parâmetros Principais
- C: Parâmetro de regularização (trade-off margem/erro)
- kernel: Tipo de função kernel
- gamma: Coeficiente para kernels RBF, polinomial e sigmoid
- degree: Grau do kernel polinomial
- probability: Se deve habilitar estimativas de probabilidade
Conexões com Tópicos Anteriores
Similarmente às técnicas que exploramos anteriormente, as SVM compartilham conceitos fundamentais:
- Utilizam regularização como a Regressão Ridge
- Aproveitam o truque do kernel como a Regressão de Crista do Kernel
- São baseadas em produtos internos como métodos de projeção linear
- Oferecem garantias teóricas de generalização
Vantagens e Desvantagens
Vantagens
- Eficazes em espaços de alta dimensionalidade
- Boa performance com dados não linearmente separáveis
- Robustas a overfitting em configurações apropriadas
- Base teórica sólida em teoria de aprendizado estatístico
Desvantagens
- Computacionalmente intensivas para datasets muito grandes
- Performance sensível à escolha de parâmetros
- Dificuldade de interpretação com kernels não-lineares
- Não fornecem estimativas de probabilidade diretamente
Exemplo Prático em Python
Para ilustrar a aplicação das SVM em diferentes cenários, 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 |
import numpy as np import matplotlib.pyplot as plt from sklearn.svm import SVC, LinearSVC from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.datasets import make_classification, make_circles, make_moons from sklearn.preprocessing import StandardScaler import warnings warnings.filterwarnings('ignore') ''' Estudo comparativo de Máquinas de Vetores de Suporte (SVM) em diferentes cenários de classificação ''' print("=== MÁQUINAS DE VETORES DE SUPORTE (SVM) ===") # Criando diferentes cenários de dados de classificação print("\n1. CONFIGURAÇÃO DOS CENÁRIOS") # Cenário 1: Dados linearmente separáveis X_linear, y_linear = make_classification( n_samples=200, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=1, class_sep=2.0, random_state=42 ) # Cenário 2: Dados não linearmente separáveis (moons) X_moons, y_moons = make_moons(n_samples=200, noise=0.1, random_state=42) # Cenário 3: Dados com estrutura circular complexa X_circles, y_circles = make_circles(n_samples=200, noise=0.1, factor=0.5, random_state=42) cenarios = [ ('Linearmente Separável', X_linear, y_linear), ('Não Linear (Moons)', X_moons, y_moons), ('Estrutura Circular', X_circles, y_circles) ] ''' Configuração dos modelos SVM ''' print("\n2. CONFIGURAÇÃO DOS MODELOS SVM") models = { 'LinearSVC': LinearSVC(random_state=42), 'SVC Linear': SVC(kernel='linear', random_state=42), 'SVC RBF': SVC(kernel='rbf', random_state=42), 'SVC Polinomial': SVC(kernel='poly', random_state=42) } # Espaço de parâmetros para tuning param_grids = { 'LinearSVC': {'C': [0.1, 1.0, 10.0, 100.0]}, 'SVC Linear': {'C': [0.1, 1.0, 10.0, 100.0]}, 'SVC RBF': { 'C': [0.1, 1.0, 10.0, 100.0], 'gamma': [0.1, 1.0, 10.0, 'scale', 'auto'] }, 'SVC Polinomial': { 'C': [0.1, 1.0, 10.0], 'gamma': [0.1, 1.0], 'degree': [2, 3, 4] } } ''' Avaliação sistemática dos modelos SVM ''' print("\n3. AVALIAÇÃO COMPARATIVA DOS MODELOS SVM") resultados_completos = {} for cenario_nome, X, y in cenarios: print(f"\n--- {cenario_nome} ---") print(f"Dimensões: {X.shape}, 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 ) cenario_resultados = {} for model_name in models.keys(): print(f" Treinando {model_name}...") try: # Grid Search com validação cruzada grid_search = GridSearchCV( models[model_name], param_grids[model_name], cv=5, scoring='accuracy', n_jobs=-1 ) grid_search.fit(X_train, y_train) # Melhor modelo best_model = grid_search.best_estimator_ best_params = grid_search.best_params_ # Previsões e métricas y_pred = best_model.predict(X_test) accuracy = accuracy_score(y_test, y_pred) # Validação cruzada para avaliação robusta cv_scores = cross_val_score(best_model, X_scaled, y, cv=5) cv_mean = np.mean(cv_scores) cv_std = np.std(cv_scores) # Número de vetores de suporte (apenas para SVC) n_support_vectors = 0 if hasattr(best_model, 'support_vectors_'): n_support_vectors = len(best_model.support_vectors_) # Armazenando resultados cenario_resultados[model_name] = { 'model': best_model, 'best_params': best_params, 'accuracy': accuracy, 'cv_mean': cv_mean, 'cv_std': cv_std, 'n_support_vectors': n_support_vectors, 'grid_search': grid_search } print(f" Melhores parâmetros: {best_params}") print(f" Acurácia: {accuracy:.3f}, CV: {cv_mean:.3f} ± {cv_std:.3f}") if n_support_vectors > 0: print(f" Vetores de suporte: {n_support_vectors}") except Exception as e: print(f" ERRO: {e}") cenario_resultados[model_name] = {'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}...") # Preparar dados para visualização scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Criar mesh para plotar fronteiras h = 0.02 x_min, x_max = X_scaled[:, 0].min() - 0.5, X_scaled[:, 0].max() + 0.5 y_min, y_max = X_scaled[:, 1].min() - 0.5, X_scaled[:, 1].max() + 0.5 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) # Configurar subplots n_models = len([m for m in resultados_completos[cenario_nome].keys() if 'error' not in resultados_completos[cenario_nome][m]]) n_cols = min(4, n_models + 1) # +1 para dados originais n_rows = (n_models + 1) // n_cols + 1 fig, axes = plt.subplots(n_rows, n_cols, figsize=(5*n_cols, 5*n_rows)) if n_rows == 1 and n_cols == 1: axes = np.array([axes]) elif n_rows == 1 or n_cols == 1: axes = axes.reshape(-1) else: axes = axes.flatten() # Plot dados originais ax_idx = 0 scatter = axes[ax_idx].scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap=plt.cm.Paired, edgecolors='k') axes[ax_idx].set_title(f'{cenario_nome}\nDados Originais') axes[ax_idx].set_xlabel('Feature 1 (padronizada)') axes[ax_idx].set_ylabel('Feature 2 (padronizada)') ax_idx += 1 # Plot modelos for model_name in models.keys(): if ax_idx >= len(axes): break if (model_name in resultados_completos[cenario_nome] and 'error' not in resultados_completos[cenario_nome][model_name]): resultados = resultados_completos[cenario_nome][model_name] model = resultados['model'] # Prever no mesh Z = model.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) # Plot fronteira de decisão axes[ax_idx].contourf(xx, yy, Z, alpha=0.3, cmap=plt.cm.Paired) axes[ax_idx].scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap=plt.cm.Paired, edgecolors='k') # Destacar vetores de suporte se disponível if hasattr(model, 'support_vectors_'): axes[ax_idx].scatter(model.support_vectors_[:, 0], model.support_vectors_[:, 1], s=100, facecolors='none', edgecolors='red', linewidths=1.5, label='Vetores Suporte') axes[ax_idx].legend() title = f"{model_name}\nAcurácia: {resultados['accuracy']:.3f}" if resultados['n_support_vectors'] > 0: title += f"\nVS: {resultados['n_support_vectors']}" axes[ax_idx].set_title(title) axes[ax_idx].set_xlabel('Feature 1 (padronizada)') axes[ax_idx].set_ylabel('Feature 2 (padronizada)') ax_idx += 1 # Remover subplots vazios for i in range(ax_idx, len(axes)): fig.delaxes(axes[i]) plt.tight_layout() plt.show() ''' Análise detalhada dos vetores de suporte ''' print("\n5. ANÁLISE DOS VETORES DE SUPORTE") for cenario_nome, X, y in cenarios: print(f"\n{cenario_nome}:") for model_name in ['SVC Linear', 'SVC RBF', 'SVC Polinomial']: if (model_name in resultados_completos[cenario_nome] and 'error' not in resultados_completos[cenario_nome][model_name]): resultados = resultados_completos[cenario_nome][model_name] n_sv = resultados['n_support_vectors'] n_samples = len(X) sv_ratio = n_sv / n_samples print(f" {model_name}:") print(f" Vetores de suporte: {n_sv}/{n_samples} ({sv_ratio:.1%})") print(f" Parâmetro C: {resultados['best_params'].get('C', 'N/A')}") ''' Análise de sensibilidade ao parâmetro C ''' print("\n6. ANÁLISE DE SENSIBILIDADE AO PARÂMETRO C") # Focando no SVC Linear para análise cenario_nome = 'Linearmente Separável' if cenario_nome in resultados_completos and 'SVC Linear' in resultados_completos[cenario_nome]: resultados = resultados_completos[cenario_nome]['SVC Linear'] grid_search = resultados['grid_search'] # Extrair resultados do grid search cv_results = grid_search.cv_results_ c_values = [params['C'] for params in cv_results['params']] mean_scores = cv_results['mean_test_score'] std_scores = cv_results['std_test_score'] plt.figure(figsize=(10, 6)) plt.errorbar(c_values, mean_scores, yerr=std_scores, fmt='o-', capsize=5) plt.xscale('log') plt.xlabel('Parâmetro C (escala log)') plt.ylabel('Acurácia Média (Validação Cruzada)') plt.title('Sensibilidade do SVC Linear ao Parâmetro C\n(Cenário Linearmente Separável)') plt.grid(True, alpha=0.3) plt.show() print(f"Melhor C: {resultados['best_params']['C']}") print(f"Melhor acurácia: {np.max(mean_scores):.3f}") ''' Comparação de performance geral - VERSÃO CORRIGIDA ''' print("\n7. COMPARAÇÃO DE PERFORMANCE GERAL") # CORREÇÃO: Extrair nomes dos cenários corretamente cenarios_nomes = [nome for nome, X, y in cenarios] # Tabela comparativa print("\nTabela Comparativa - Acurácia por Cenário e Modelo:") print("\n" + "="*80) print(f"{'Cenário':<25} {'LinearSVC':<12} {'SVC Linear':<12} {'SVC RBF':<12} {'SVC Polinomial':<15}") print("="*80) for cenario_nome in cenarios_nomes: row = f"{cenario_nome:<25}" for model_name in ['LinearSVC', 'SVC Linear', 'SVC RBF', 'SVC Polinomial']: if (cenario_nome in resultados_completos and model_name in resultados_completos[cenario_nome] and 'error' not in resultados_completos[cenario_nome][model_name]): acc = resultados_completos[cenario_nome][model_name]['accuracy'] row += f" {acc:<11.3f}" else: row += f" {'N/A':<11}" print(row) # CORREÇÃO: Adicionar análise dos melhores modelos por cenário print("\n\nMelhores Modelos por Cenário:") print("="*50) for cenario_nome in cenarios_nomes: if cenario_nome in resultados_completos: melhor_modelo = None melhor_acuracia = 0 for model_name, resultados in resultados_completos[cenario_nome].items(): if 'error' not in resultados and resultados['accuracy'] > melhor_acuracia: melhor_acuracia = resultados['accuracy'] melhor_modelo = model_name if melhor_modelo: print(f"{cenario_nome:<25}: {melhor_modelo:<15} (Acurácia: {melhor_acuracia:.3f})") ''' Recomendações práticas - VERSÃO ATUALIZADA ''' print("\n8. RECOMENDAÇÕES PRÁTICAS") print("\nBaseado na análise experimental:") print("\nEscolha do Modelo:") print(" - LinearSVC: Para dados linearmente separáveis e eficiência computacional") print(" - SVC Linear: Similar ao LinearSVC mas com mais funcionalidades") print(" - SVC RBF: Para dados não linearmente separáveis (default recomendado)") print(" - SVC Polinomial: Quando se sabe que a relação é polinomial") print("\nOtimização de Hiperparâmetros:") print(" - C: Controla trade-off entre margem e erro (valores típicos: 0.1-100)") print(" - gamma: Controla influência dos exemplos (RBF/polinomial)") print(" - Sempre usar validação cruzada para tuning") print("\nPré-processamento:") print(" - SVM são sensíveis à escala - sempre padronizar os dados") print(" - Considerar balanceamento de classes se necessário") print("\nConsiderações Computacionais:") print(" - LinearSVC é mais eficiente para datasets grandes") print(" - SVC com kernel não-linear pode ser lento para muitos exemplos") print(" - Número de vetores de suporte indica complexidade do modelo") print("\nCasos de Uso:") print(" - Classificação de texto e documentos") print(" - Reconhecimento de imagens") print(" - Bioinformática") print(" - Detecção de anomalias") # CORREÇÃO: Adicionar análise final dos resultados print("\n9. ANÁLISE FINAL DOS RESULTADOS") print("\nPrincipais Insights Obtidos:") for cenario_nome in cenarios_nomes: if cenario_nome in resultados_completos: print(f"\n{cenario_nome}:") # Análise por tipo de kernel kernels_analisados = [] for model_name in ['SVC Linear', 'SVC RBF', 'SVC Polinomial']: if (model_name in resultados_completos[cenario_nome] and 'error' not in resultados_completos[cenario_nome][model_name]): resultados = resultados_completos[cenario_nome][model_name] kernels_analisados.append({ 'kernel': model_name, 'acuracia': resultados['accuracy'], 'vetores_suporte': resultados.get('n_support_vectors', 0) }) # Ordenar por acurácia kernels_analisados.sort(key=lambda x: x['acuracia'], reverse=True) for kernel_info in kernels_analisados: print(f" - {kernel_info['kernel']}: Acurácia {kernel_info['acuracia']:.3f}, " f"Vetores Suporte: {kernel_info['vetores_suporte']}") |
Interpretação dos Resultados
Analisando os experimentos comparativos, podemos observar padrões importantes:
- Modelos lineares performam bem em dados linearmente separáveis
- SVM com kernel RBF são mais flexíveis para dados complexos
- O número de vetores de suporte indica a complexidade da fronteira
- O parâmetro C tem impacto significativo na performance
Considerações Avançadas
SVM para Problemas Multiclasse
O scikit-learn implementa duas estratégias para problemas multiclasse:
- one-vs-rest (OvR): Um classificador por classe vs todas as outras
- one-vs-one (OvO): Um classificador para cada par de classes
Estimativas de Probabilidade
Embora SVM sejam originalmente classificadores determinísticos, o scikit-learn oferece a opção probability=True que utiliza Platt scaling para gerar estimativas probabilísticas.
Boas Práticas
Inegavelmente, para obter os melhores resultados com SVM:
- Sempre padronize os dados antes do treinamento
- Use validação cruzada para tuning de hiperparâmetros
- Comece com kernel RBF como primeira abordagem
- Considere a complexidade computacional para datasets grandes
- Analise os vetores de suporte para entender o modelo
Conclusão
As Máquinas de Vetores de Suporte representam uma ferramenta poderosa e versátil no arsenal de machine learning. Embora tenham sido desenvolvidas décadas atrás, continuam sendo relevantes devido à sua fundamentação teórica sólida e performance prática consistente.
Portanto, o domínio das SVM é essencial para qualquer praticante de machine learning, oferecendo uma abordagem robusta para problemas de classificação que vai desde casos linearmente separáveis até relações complexas não-lineares.
Referência
Este post explora o item 1.4. Máquinas de Vetores de Suporte da documentação do scikit-learn:
https://scikit-learn.org/0.21/modules/svm.html