线性判别分析(Linear Discriminant Analysis, LDA)详解
1. 引言
线性判别分析(LDA)是一种经典的降维方法和分类方法,它在模式识别、机器学习等领域有着广泛应用。LDA的核心思想是:将高维数据投影到低维空间中,使得同类数据尽可能紧凑,不同类数据尽可能分开。
2. 数学原理
2.1 基本思想
LDA的目标是找到一个投影方向 w w w,使得:
- 同类样本投影后尽可能近(类内方差最小)
- 不同类样本投影后尽可能远(类间方差最大)
2.2 数学推导
对于二分类问题:
-
类内散度矩阵 S w S_w Sw:
S w = ∑ i = 1 c ∑ x ∈ X i ( x − μ i ) ( x − μ i ) T S_w = \sum_{i=1}^c \sum_{x \in X_i} (x - \mu_i)(x - \mu_i)^T Sw=∑i=1c∑x∈Xi(x−μi)(x−μi)T -
类间散度矩阵 S b S_b Sb:
S b = ∑ i = 1 c N i ( μ i − μ ) ( μ i − μ ) T S_b = \sum_{i=1}^c N_i(\mu_i - \mu)(\mu_i - \mu)^T Sb=∑i=1cNi(μi−μ)(μi−μ)T -
目标函数(Fisher准则):
J ( w ) = w T S b w w T S w w J(w) = \frac{w^T S_b w}{w^T S_w w} J(w)=wTSwwwTSbw -
最优解:
S w − 1 S b w = λ w S_w^{-1}S_b w = \lambda w Sw−1Sbw=λw
3. 算法实现
import numpy as np
from sklearn.datasets import make_classification
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix, classification_report
from sklearn.model_selection import train_test_split
class LDA:
def __init__(self, n_components=1):
self.n_components = n_components
self.w = None
self.explained_variance_ratio_ = None
def fit(self, X, y):
n_samples, n_features = X.shape
classes = np.unique(y)
# 计算类内散度矩阵
S_w = np.zeros((n_features, n_features))
for cls in classes:
X_cls = X[y == cls]
mean_cls = X_cls.mean(axis=0)
X_centered = X_cls - mean_cls
S_w += X_centered.T @ X_centered
# 计算类间散度矩阵
mean_total = X.mean(axis=0)
S_b = np.zeros((n_features, n_features))
for cls in classes:
X_cls = X[y == cls]
mean_cls = X_cls.mean(axis=0)
n_cls = X_cls.shape[0]
mean_diff = (mean_cls - mean_total).reshape(-1, 1)
S_b += n_cls * mean_diff @ mean_diff.T
# 求解特征值和特征向量
eigvals, eigvecs = np.linalg.eig(np.linalg.inv(S_w) @ S_b)
# 选择最大的n_components个特征值对应的特征向量
idx = eigvals.argsort()[::-1]
self.w = eigvecs[:, idx[:self.n_components]]
# 计算解释方差比
self.explained_variance_ratio_ = np.real(eigvals[idx[:self.n_components]] / np.sum(eigvals))
def transform(self, X):
return X @ self.w
def predict(self, X):
# 添加预测方法
X_transformed = self.transform(X)
# 使用简单的阈值进行分类(针对二分类问题)
return (X_transformed[:, 0] > 0).astype(int)
4. 实验示例
# 数据准备
X, y = make_classification(n_samples=1000, n_features=20, n_classes=2,
n_informative=2, n_redundant=10, random_state=42)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 创建并训练LDA模型
lda = LDA(n_components=2)
lda.fit(X_train, y_train)
# 转换数据
X_train_transformed = lda.transform(X_train)
X_test_transformed = lda.transform(X_test)
# 1. 绘制训练集和测试集的投影结果
plt.figure(figsize=(15, 5))
plt.subplot(121)
plt.scatter(X_train_transformed[y_train==0, 0], X_train_transformed[y_train==0, 1],
c='red', label='Class 0', alpha=0.7)
plt.scatter(X_train_transformed[y_train==1, 0], X_train_transformed[y_train==1, 1],
c='blue', label='Class 1', alpha=0.7)
plt.xlabel('First discriminant')
plt.ylabel('Second discriminant')
plt.title('LDA Projection (Training Set)')
plt.legend()
plt.subplot(122)
plt.scatter(X_test_transformed[y_test==0, 0], X_test_transformed[y_test==0, 1],
c='red', label='Class 0', alpha=0.7)
plt.scatter(X_test_transformed[y_test==1, 0], X_test_transformed[y_test==1, 1],
c='blue', label='Class 1', alpha=0.7)
plt.xlabel('First discriminant')
plt.ylabel('Second discriminant')
plt.title('LDA Projection (Test Set)')
plt.legend()
plt.tight_layout()
plt.show()
# 2. 打印解释方差比
print("\n解释方差比:")
for i, ratio in enumerate(lda.explained_variance_ratio_):
print(f"判别式 {i+1}: {ratio:.4f}")
# 3. 绘制第一判别式的分布
plt.figure(figsize=(10, 6))
for label in [0, 1]:
plt.hist(X_train_transformed[y_train==label, 0],
bins=30, alpha=0.5,
label=f'Class {label}')
plt.xlabel('First Discriminant')
plt.ylabel('Frequency')
plt.title('Distribution of First Discriminant')
plt.legend()
plt.show()
# 4. 进行预测和评估
y_pred = lda.predict(X_test)
# 5. 绘制混淆矩阵
plt.figure(figsize=(8, 6))
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.title('Confusion Matrix')
plt.xlabel('Predicted')
plt.ylabel('True')
plt.show()
# 6. 打印分类报告
print("\n分类报告:")
print(classification_report(y_test, y_pred))
# 7. 计算并显示类间距离
class_0_mean = X_train_transformed[y_train==0].mean(axis=0)
class_1_mean = X_train_transformed[y_train==1].mean(axis=0)
distance = np.linalg.norm(class_0_mean - class_1_mean)
print(f"\n类间欧氏距离: {distance:.4f}")
# 8. 计算每个特征的判别能力
feature_importance = np.abs(lda.w)
plt.figure(figsize=(10, 6))
plt.bar(range(lda.w.shape[0]), feature_importance[:, 0])
plt.title('Feature Importance (First Discriminant)')
plt.xlabel('Feature Index')
plt.ylabel('Absolute Weight')
plt.show()
4.3 结果分析
4.3.1 判别式分布分析
从图"Distribution of First Discriminant"可以观察到:
- 两个类别在第一判别式上有明显的分离
- Class 0(蓝色)主要分布在负值区域,峰值在-0.05附近
- Class 1(红色)主要分布在正值区域,峰值在0.1附近
- 两类存在一定重叠区域,说明分类不是完全线性可分的
4.3.2 投影效果分析
从LDA投影散点图(训练集和测试集)可以看出:
- 训练集和测试集表现出相似的分布模式,说明模型具有良好的泛化能力
- 第一判别式(x轴)提供了主要的分类信息,这与解释方差比(2.3878)相对应
- 第二判别式(y轴)提供了补充信息,但贡献较小(解释方差比0.2540)
- 两个类别呈现出明显的对角线分布趋势,说明两个判别式都对分类有贡献
4.3.3 混淆矩阵分析
混淆矩阵显示:
- 真正例(TP):133个样本
- 真负例(TN):101个样本
- 假正例(FP):29个样本
- 假负例(FN):37个样本
模型性能指标:
- 准确率 = (133 + 101)/(133 + 101 + 29 + 37) = 77.67%
- 精确率 = 133/(133 + 29) = 82.10%
- 召回率 = 133/(133 + 37) = 78.24%
- F1分数 = 2 * (精确率 * 召回率)/(精确率 + 召回率) = 80.12%
4.3.4 总体评估
-
模型表现:
- 整体分类效果良好,准确率达到77.67%
- 两个类别的分类性能相对平衡
- 第一判别式贡献了大部分分类信息(约90.4%)
-
优势:
- 数据降维效果明显
- 类别分离度好
- 训练集和测试集表现一致
-
局限性:
- 存在一定的分类错误
- 类别重叠区域可能需要非线性方法进一步优化
-
改进建议:
- 考虑增加特征工程
- 尝试非线性核LDA
- 结合其他分类器形成集成模型
5. LDA与PCA的比较
5.1 相同点
- 都是常用的降维方法
- 都是线性降维方法
- 都可以用于特征提取
5.2 不同点
-
监督与否:
- LDA是监督学习方法,需要标签信息
- PCA是非监督学习方法,不需要标签信息
-
优化目标:
- LDA最大化类间方差,最小化类内方差
- PCA最大化投影方差
-
应用场景:
- LDA更适合分类问题
- PCA更适合数据压缩和可视化
6. LDA的优缺点
6.1 优点
- 可以用于降维和分类
- 计算简单,易于实现
- 对数据分布有较好的解释性
- 在小样本情况下表现良好
6.2 缺点
- 假设数据服从高斯分布
- 对非线性问题效果不好
- 类内散度矩阵可能奇异
- 降维维数受限于类别数-1
7. 实际应用场景
- 人脸识别
- 文本分类
- 医学诊断
- 语音识别
- 生物特征识别
8. 总结
LDA是一种经典且实用的算法,特别适合处理有监督的降维问题。虽然有一些限制,但在实际应用中仍然是一个很好的选择,尤其是在需要降维且数据大致符合高斯分布的场景下。
9. 参考文献
- Fisher, R. A. (1936). The use of multiple measurements in taxonomic problems
- Duda, R. O., Hart, P. E., & Stork, D. G. (2012). Pattern classification