Após explorarmos as aplicações práticas da Análise Discriminante, mergulhemos agora nos fundamentos matemáticos que sustentam os classificadores LDA e QDA. Primordialmente, compreender estas formulações nos permitirá aplicar estas técnicas com maior discernimento e interpretar seus resultados de forma mais profunda.
Pressupostos Fundamentais
Conforme mencionamos anteriormente, ambos os métodos assumem que os dados seguem distribuições Gaussianas multivariadas. Analogamente, podemos expressar esta suposição formalmente:
\(P(x | y = k) = \frac{1}{(2\pi)^{d/2} |\Sigma_k|^{1/2}} \exp\left(-\frac{1}{2} (x – \mu_k)^T \Sigma_k^{-1} (x – \mu_k)\right)\)Onde para cada classe k:
- μ_k é o vetor de médias
- Σ_k é a matriz de covariância
- d é a dimensionalidade dos dados
Teorema de Bayes e Função Discriminante
A decisão de classificação baseia-se no teorema de Bayes:
\(P(y = k | x) = \frac{P(x | y = k) P(y = k)}{P(x)}\)Como P(x) é constante para todas as classes, maximizar P(y=k|x) equivale a maximizar o numerador. Portanto, definimos a função discriminante como:
\(\delta_k(x) = \log P(x | y = k) + \log P(y = k)\)Análise Discriminante Linear (LDA)
Pressuposto de Covariância Comum
O LDA assume que todas as classes compartilham a mesma matriz de covariância:
\(\Sigma_k = \Sigma \quad \text{para todo } k\)Desenvolvendo a função discriminante com este pressuposto, obtemos:
\(\delta_k(x) = x^T \Sigma^{-1} \mu_k – \frac{1}{2} \mu_k^T \Sigma^{-1} \mu_k + \log \pi_k\)Onde π_k = P(y=k) é a probabilidade a priori da classe k.
Forma Linear da Fronteira de Decisão
A fronteira entre duas classes i e j é determinada por:
\(\delta_i(x) = \delta_j(x)\)O que resulta em uma equação linear em x:
\(x^T \Sigma^{-1} (\mu_i – \mu_j) – \frac{1}{2} (\mu_i + \mu_j)^T \Sigma^{-1} (\mu_i – \mu_j) + \log(\pi_i/\pi_j) = 0\)Análise Discriminante Quadrática (QDA)
Matrizes de Covariância Distintas
O QDA permite que cada classe tenha sua própria matriz de covariância:
\(\Sigma_k \neq \Sigma_j \quad \text{para } k \neq j\)Desenvolvendo a função discriminante, obtemos:
\(\delta_k(x) = -\frac{1}{2} \log |\Sigma_k| – \frac{1}{2} (x – \mu_k)^T \Sigma_k^{-1} (x – \mu_k) + \log \pi_k\)Forma Quadrática da Fronteira de Decisão
A fronteira entre classes i e j é dada por:
\(\delta_i(x) = \delta_j(x)\)O que resulta em uma equação quadrática em x, criando fronteiras de decisão curvas.
Estimativa dos Parâmetros
Estimativas de Máxima Verossimilhança
Os parâmetros são estimados a partir dos dados de treinamento:
- Médias das classes: \(\hat{\mu}_k = \frac{1}{N_k} \sum_{i: y_i = k} x_i\)
- Probabilidades a priori: \(\hat{\pi}_k = \frac{N_k}{N}\)
- Matriz de covariância (LDA): \(\hat{\Sigma} = \frac{1}{N-K} \sum_{k=1}^K \sum_{i: y_i = k} (x_i – \hat{\mu}_k)(x_i – \hat{\mu}_k)^T\)
- Matrizes de covariância (QDA): \(\hat{\Sigma}_k = \frac{1}{N_k-1} \sum_{i: y_i = k} (x_i – \hat{\mu}_k)(x_i – \hat{\mu}_k)^T\)
Comparação Matemática Detalhada
Complexidade de Parâmetros
O número de parâmetros a estimar difere significativamente:
- LDA: K × d (médias) + d(d+1)/2 (covariância) + K (priors)
- QDA: K × d (médias) + K × d(d+1)/2 (covariâncias) + K (priors)
Inegavelmente, o QDA requer substancialmente mais parâmetros, especialmente quando o número de features é grande.
Propriedades Estatísticas
Ambos os métodos possuem propriedades interessantes:
- São classificadores Bayes ótimos sob os pressupostos Gaussianos
- Produzem estimativas de probabilidade posteriores
- Podem ser regularizados para melhor generalização
- São consistentes sob condições apropriadas
Exemplo Prático em Python
Para ilustrar estas formulações matemáticas, implementemos um exemplo que demonstra os cálculos fundamentais:
|
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 |
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 from sklearn.metrics import accuracy_score, confusion_matrix import scipy.stats as stats ''' Implementação manual dos cálculos LDA e QDA para demonstrar as formulações matemáticas discutidas ''' class ManualLDA: '''Implementação manual do LDA para fins educacionais''' def fit(self, X, y): """ Estima os parâmetros do LDA: médias, covariância comum e priors """ self.classes_ = np.unique(y) self.n_classes = len(self.classes_) self.n_features = X.shape[1] # Calculando médias por classe self.means_ = [] for k in self.classes_: class_mask = (y == k) self.means_.append(np.mean(X[class_mask], axis=0)) self.means_ = np.array(self.means_) # Calculando probabilidades a priori self.priors_ = [] for k in self.classes_: self.priors_.append(np.mean(y == k)) self.priors_ = np.array(self.priors_) # Calculando matriz de covariância comum self.covariance_ = np.zeros((self.n_features, self.n_features)) for k, mean in zip(self.classes_, self.means_): class_mask = (y == k) X_centered = X[class_mask] - mean self.covariance_ += X_centered.T @ X_centered self.covariance_ /= (len(y) - self.n_classes) # Pré-calculando a inversa da covariância self.covariance_inv_ = np.linalg.pinv(self.covariance_) return self def _discriminant_function(self, x, k): """ Calcula a função discriminante para uma observação x e classe k δ_k(x) = x^T Σ^{-1} μ_k - 1/2 μ_k^T Σ^{-1} μ_k + log(π_k) """ mean_k = self.means_[k] prior_k = self.priors_[k] linear_term = x @ self.covariance_inv_ @ mean_k quadratic_term = 0.5 * mean_k @ self.covariance_inv_ @ mean_k prior_term = np.log(prior_k) return linear_term - quadratic_term + prior_term def predict_proba(self, X): """ Calcula probabilidades posteriores usando softmax nas funções discriminantes """ n_samples = X.shape[0] discriminant_scores = np.zeros((n_samples, self.n_classes)) for k in range(self.n_classes): for i in range(n_samples): discriminant_scores[i, k] = self._discriminant_function(X[i], k) # Aplicando softmax para obter probabilidades exp_scores = np.exp(discriminant_scores - np.max(discriminant_scores, axis=1, keepdims=True)) probabilities = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) return probabilities def predict(self, X): """ Prediz classes selecionando a com maior probabilidade posterior """ probabilities = self.predict_proba(X) return np.argmax(probabilities, axis=1) ''' Demonstração comparativa entre implementação manual e scikit-learn ''' print("=== DEMONSTRAÇÃO DAS FORMULAÇÕES MATEMÁTICAS ===") # Gerando dados de exemplo X, y = make_classification( n_samples=300, n_features=2, n_informative=2, n_redundant=0, n_classes=3, n_clusters_per_class=1, random_state=42 ) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) # Aplicando implementação manual print("\n1. IMPLEMENTAÇÃO MANUAL DO LDA") manual_lda = ManualLDA() manual_lda.fit(X_train, y_train) y_pred_manual = manual_lda.predict(X_test) accuracy_manual = accuracy_score(y_test, y_pred_manual) print(f"Parâmetros estimados:") print(f" - Médias das classes: {manual_lda.means_.shape}") print(f" - Matriz de covariância: {manual_lda.covariance_.shape}") print(f" - Priors: {manual_lda.priors_}") print(f" - Acurácia (manual): {accuracy_manual:.3f}") # Comparando com scikit-learn print("\n2. COMPARAÇÃO COM SCIKIT-LEARN") sklearn_lda = LinearDiscriminantAnalysis() sklearn_lda.fit(X_train, y_train) y_pred_sklearn = sklearn_lda.predict(X_test) accuracy_sklearn = accuracy_score(y_test, y_pred_sklearn) print(f" - Acurácia (scikit-learn): {accuracy_sklearn:.3f}") print(f" - Coincidência nas predições: {np.mean(y_pred_manual == y_pred_sklearn):.3f}") ''' Análise das funções discriminantes para uma observação específica ''' print("\n3. ANÁLISE DAS FUNÇÕES DISCRIMINANTES") sample_idx = 0 x_sample = X_test[sample_idx] true_class = y_test[sample_idx] print(f"Observação de teste {sample_idx}:") print(f" - Classe verdadeira: {true_class}") # Calculando scores discriminantes manualmente for k in range(manual_lda.n_classes): score = manual_lda._discriminant_function(x_sample, k) print(f" - Score classe {k}: {score:.3f}") # Probabilidades calculadas manualmente proba_manual = manual_lda.predict_proba(X_test[sample_idx:sample_idx+1])[0] print(f" - Probabilidades (manual): {proba_manual}") # Probabilidades do scikit-learn proba_sklearn = sklearn_lda.predict_proba(X_test[sample_idx:sample_idx+1])[0] print(f" - Probabilidades (scikit-learn): {proba_sklearn}") ''' Visualização das fronteiras de decisão ''' print("\n4. VISUALIZAÇÃO DAS FRONTEIRAS DE DECISÃO") # Criando grid para plotar fronteiras h = 0.02 x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1 y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) # Plot implementação manual Z_manual = manual_lda.predict(np.c_[xx.ravel(), yy.ravel()]) Z_manual = Z_manual.reshape(xx.shape) contour1 = ax1.contourf(xx, yy, Z_manual, alpha=0.3, cmap=plt.cm.Paired) scatter1 = ax1.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=plt.cm.Paired, edgecolors='k') ax1.set_title('Fronteiras de Decisão - Implementação Manual') ax1.set_xlabel('Feature 1') ax1.set_ylabel('Feature 2') # Plot scikit-learn Z_sklearn = sklearn_lda.predict(np.c_[xx.ravel(), yy.ravel()]) Z_sklearn = Z_sklearn.reshape(xx.shape) contour2 = ax2.contourf(xx, yy, Z_sklearn, alpha=0.3, cmap=plt.cm.Paired) scatter2 = ax2.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=plt.cm.Paired, edgecolors='k') ax2.set_title('Fronteiras de Decisão - Scikit-learn') ax2.set_xlabel('Feature 1') ax2.set_ylabel('Feature 2') plt.tight_layout() plt.show() ''' Demonstração do cálculo da matriz de covariância ''' print("\n5. CÁLCULO DETALHADO DA MATRIZ DE COVARIÂNCIA") print("Matriz de covariância comum (LDA):") print(manual_lda.covariance_) # Verificando cálculo manual vs numpy cov_numpy = np.cov(X_train.T, bias=True) print("\nVerificação com numpy.cov (com bias adjustment):") print("Diferença máxima:", np.max(np.abs(manual_lda.covariance_ - cov_numpy))) ''' Implementação simplificada do QDA para comparação ''' print("\n6. COMPARAÇÃO COM QDA") sklearn_qda = QuadraticDiscriminantAnalysis() sklearn_qda.fit(X_train, y_train) y_pred_qda = sklearn_qda.predict(X_test) accuracy_qda = accuracy_score(y_test, y_pred_qda) print(f" - Acurácia QDA: {accuracy_qda:.3f}") print(f" - Acurácia LDA: {accuracy_sklearn:.3f}") # Analisando diferenças nas predições diff_lda_qda = np.mean(y_pred_sklearn != y_pred_qda) print(f" - Diferença nas predições LDA vs QDA: {diff_lda_qda:.3f}") ''' Interpretação dos resultados matemáticos ''' print("\n7. INTERPRETAÇÃO MATEMÁTICA") print("Os resultados demonstram que:") print(" - As formulações matemáticas produzem classificadores consistentes") print(" - A implementação manual coincide com o scikit-learn") print(" - As funções discriminantes capturam a informação de classe") print(" - As fronteiras de decisão refletem a estrutura linear do LDA") |
Interpretação dos Resultados Matemáticos
Analisando a implementação e os resultados, podemos observar que:
- As funções discriminantes realmente capturam a informação de separação entre classes
- A matriz de covariância comum no LDA produz fronteiras lineares
- As probabilidades posteriores são calculadas corretamente via softmax
- A implementação manual coincide com a do scikit-learn, validando as formulações
Implicações Práticas das Formulações
Vantagens do LDA
- Menos parâmetros para estimar → mais robusto com dados limitados
- Computacionalmente mais eficiente
- Fronteiras de decisão lineares são mais interpretáveis
Vantagens do QDA
- Mais flexibilidade para capturar relações complexas
- Melhor performance quando as covariâncias são realmente diferentes
- Fronteiras de decisão curvas podem se ajustar melhor aos dados
Considerações Finais
As formulações matemáticas do LDA e QDA revelam por que estas técnicas são tão eficazes sob os pressupostos Gaussianos. Embora baseadas em conceitos estatísticos clássicos, continuam sendo ferramentas valiosas no machine learning moderno.
Portanto, compreender estas fundamentações não apenas nos permite aplicar os métodos corretamente, mas também nos capacita a interpretar seus resultados, diagnosticar problemas e fazer escolhas informadas entre diferentes abordagens de classificação.