Python实战:用sklearn轻松实现LDA分类与可视化(附完整代码)

如果你刚开始接触机器学习,面对一堆听起来高大上的算法名词,可能会觉得无从下手。线性判别分析(LDA)就是这样一个名字听起来很“学术”,但实际用起来却异常顺手的工具。我第一次在项目里用它处理一个客户分群问题,当时数据维度有十几个,直接用逻辑回归效果总是不稳定。后来尝试了LDA,不仅分类准确率上去了,降维后的可视化图还让业务方一眼就看懂了不同客户群体的差异,那次经历让我彻底记住了这个算法的实用性。

简单来说,LDA是一个“一箭双雕”的算法。它既是一个经典的分类器,也是一个强大的降维工具。与大家更熟悉的PCA(主成分分析)不同,LDA在寻找投影方向时,会聪明地利用数据的类别标签信息,目标是让不同类别的数据点分得尽可能开,同时让同一类别的数据点尽可能聚拢。这就好比老师排座位,不仅要让男生女生分开坐(类间距离大),还要让每个小组内部的同学坐得近一些(类内距离小),这样课堂秩序(分类效果)自然就好了。

这篇文章就是为你准备的,无论你是想快速上手一个可靠的分类模型,还是希望为高维数据找到一个清晰的可视化视角,LDA都能派上用场。我们会完全跳过枯燥的公式推导,直接进入实战环节,手把手教你如何用Python的scikit-learn库,在几分钟内搭建起一个LDA模型,并生成直观的图表来解读结果。你会发现,那些看似复杂的数学概念,落实到代码上,其实就几行而已。

1. 环境准备与数据理解

在开始写代码之前,我们需要确保手头的“工具箱”是齐全的。对于今天的任务,核心就是scikit-learn(简称sklearn)和matplotlib。如果你用的是Anaconda,那么这些库通常已经预装了。如果没有,一条简单的pip命令就能搞定。

pip install scikit-learn matplotlib numpy pandas

提示:建议使用Jupyter Notebook或类似交互式环境进行后续的代码实践,这样可以即时看到每一步的输出和图形,学习体验更佳。

接下来,我们得找点数据来“练手”。为了聚焦于LDA本身的应用,我们避开复杂的数据清洗和预处理,直接使用sklearn内置的经典数据集。这里我选择了鸢尾花(Iris)数据集,它几乎是机器学习入门者的“Hello World”。这个数据集包含了150个样本,每个样本有4个特征(萼片长度、萼片宽度、花瓣长度、花瓣宽度),并属于3个不同的鸢尾花品种。

为什么选它?首先,它的规模适中,便于理解和可视化;其次,它是一个多分类问题(3类),正好可以展示LDA在多类场景下的降维能力;最后,它足够经典,任何异常或问题都容易排查。

让我们先加载数据,并对其有一个直观的认识:

import numpy as np
import pandas as pd
from sklearn import datasets
import matplotlib.pyplot as plt

# 加载鸢尾花数据集
iris = datasets.load_iris()
X = iris.data  # 特征矩阵,形状为 (150, 4)
y = iris.target  # 标签向量,形状为 (150,)
target_names = iris.target_names  # 类别名称:['setosa', 'versicolor', 'virginica']

# 快速查看数据结构和基本信息
print(f"特征数据形状: {X.shape}")
print(f"标签数据形状: {y.shape}")
print(f"特征名称: {iris.feature_names}")
print(f"类别名称: {target_names}")
print(f"各类别样本数量: {np.bincount(y)}")

运行这段代码,你会看到类似下面的输出:

特征数据形状: (150, 4)
标签数据形状: (150,)
特征名称: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
类别名称: ['setosa' 'versicolor' 'virginica']
各类别样本数量: [50 50 50]

数据是平衡的,每类50个样本。现在,我们直接跳到用LDA进行降维和可视化,看看在算法“眼中”,这三类花到底有什么区别。

2. 核心实战:三步实现LDA降维与可视化

LDA在sklearn中的实现非常简洁,主要位于sklearn.discriminant_analysis模块。我们重点关注LinearDiscriminantAnalysis这个类。它的使用流程和大多数sklearn模型一致:导入、实例化、拟合、转换/预测。

2.1 第一步:实例化与拟合模型

首先,我们导入LDA类并创建一个模型实例。这里有一个关键参数n_components,它决定了我们想要将数据降到几维。对于分类任务,LDA能降到的最大维度是min(特征数, 类别数-1)。在我们的例子中,特征数是4,类别数是3,所以n_components最大可以是2(3-1)。我们通常就设置为这个最大值,以保留最多的判别信息。

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis

# 创建LDA模型实例,指定降维后的维度为2(因为3个类别,最大可降维数为2)
lda = LinearDiscriminantAnalysis(n_components=2)
# 使用特征X和标签y来训练(拟合)模型
X_lda = lda.fit_transform(X, y)  # fit_transform一步完成拟合和转换

print(f"原始数据维度: {X.shape}")
print(f"LDA降维后数据维度: {X_lda.shape}")

执行后,你会发现数据从原来的(150, 4)变成了(150, 2)。这150个样本现在每个都只用两个坐标(我们称之为LD1和LD2,即线性判别式1和2)来描述。这两个新特征包含了原始4个特征中最能区分三类鸢尾花的信息。

2.2 第二步:可视化降维结果

数据降到二维后,我们就可以轻松地用散点图把它画出来了。这是最令人兴奋的一步,因为你能直观地看到算法是否成功地将不同类别区分开来。

plt.figure(figsize=(10, 8))
colors = ['navy', 'turquoise', 'darkorange']
lw = 2

for color, i, target_name in zip(colors, [0, 1, 2], target_names):
    plt.scatter(X_lda[y == i, 0], X_lda[y == i, 1],
                color=color, alpha=.8, lw=lw, label=target_name)

plt.legend(loc='best', shadow=False, scatterpoints=1)
plt.title('LDA of IRIS dataset')
plt.xlabel('Linear Discriminant 1 (LD1)')
plt.ylabel('Linear Discriminant 2 (LD2)')
plt.grid(True, linestyle='--', alpha=0.5)
plt.show()

如果一切顺利,你将得到一张非常漂亮的散点图。图中,三个类别的数据点应该被清晰地分成三簇。特别是setosa(山鸢尾)这一类,很可能与其他两类被LD1完美地分开。而versicolor(杂色鸢尾)和virginica(维吉尼亚鸢尾)在LD2方向上可能会有一些区分度。这张图本身就是LDA能力的最佳证明:它用两个新轴,就几乎完美地再现了原始四维空间中的类别结构。

2.3 第三步:解读模型与判别能力

画出了图,我们还需要知道这两个新维度(LD1和LD2)到底“代表”了什么。LDA模型对象里存储了这些信息。

# 查看模型解释的方差比例(即每个判别式携带的判别信息量)
explained_variance_ratio = lda.explained_variance_ratio_
print(f"每个判别式解释的方差比例: {explained_variance_ratio}")
print(f"总解释方差比例: {sum(explained_variance_ratio):.4f}")

# 查看判别向量(coefficients),这有助于理解原始特征如何组合成新特征
# 注意:在LDA中,更直接的是查看`scalings_`属性,它给出了将数据投影到判别空间的变换矩阵。
print(f"\n变换矩阵(scalings_)的形状: {lda.scalings_.shape}")
print("变换矩阵(前几行):\n", lda.scalings_)

explained_variance_ratio_这个属性非常重要,它告诉我们每个线性判别式(LD)所承载的“判别力”大小。通常,第一个判别式(LD1)会解释绝大部分的类间方差。在我们的例子中,LD1的解释比例可能高达99%以上,这意味着仅凭第一个维度就几乎能完成分类任务。scalings_矩阵则揭示了原始4个特征是如何加权组合成LD1和LD2的,这对于特征重要性分析很有帮助。

为了更深入地理解LDA作为分类器的表现,我们可以快速看一下它在训练集上的分类准确率(虽然这有过度乐观的风险,但作为初步检查很有用):

# 使用训练好的LDA模型对原始数据进行预测
y_pred = lda.predict(X)
# 计算准确率
from sklearn.metrics import accuracy_score
train_accuracy = accuracy_score(y, y_pred)
print(f"模型在训练集上的分类准确率: {train_accuracy:.4f}")

对于像Iris这样线性可分性很好的数据集,LDA的训练准确率很可能达到100%。但这并不意味着模型是过拟合,而是说明在这个问题上,LDA的线性假设非常贴合数据本质。

3. 进阶应用:LDA作为分类器与参数调优

前面我们主要把LDA当作降维工具来用。实际上,它本身就是一个非常优秀的线性分类器。在sklearn中,当我们调用predict方法时,它就是在执行分类任务。现在,我们来更正式地评估其分类性能,并探讨一个关键参数:solver(求解器)。

3.1 划分训练集与测试集

任何严谨的模型评估都需要在未见过的数据上进行。我们使用train_test_split来划分数据。

from sklearn.model_selection import train_test_split

# 将数据划分为训练集和测试集,测试集占比30%
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)

print(f"训练集大小: {X_train.shape}")
print(f"测试集大小: {X_test.shape}")

参数stratify=y确保了训练集和测试集中各类别的比例与原始数据集一致,这对于小数据集或不平衡数据集尤为重要。

3.2 比较不同求解器(Solver)

LDA在求解时,内部有不同的数学计算方法,对应LinearDiscriminantAnalysissolver参数。主要选项有:

  • 'svd':奇异值分解。无需计算协方差矩阵,在特征数很多时更稳定。这是默认选项,也是唯一不支持shrinkage(收缩)的求解器。
  • 'lsqr''eigen':最小二乘和特征分解。这两种方法都支持shrinkage正则化。

shrinkage是一个非常重要的正则化技术,尤其适用于样本数量少于特征数量,或者特征之间存在强相关性的情况。它通过向类内散度矩阵添加一个收缩系数,来估计更稳定的协方差矩阵,防止过拟合。

下面的代码展示了如何比较不同配置的LDA分类器在测试集上的表现:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns

# 配置不同的LDA模型
solvers = ['svd', 'lsqr', 'eigen']
results = {}

for solver in solvers:
    if solver == 'svd':
        # 'svd'求解器不支持shrinkage参数
        lda_clf = LinearDiscriminantAnalysis(solver=solver, n_components=2)
    else:
        # 对于'lsqr'和'eigen',我们可以尝试使用自动收缩
        lda_clf = LinearDiscriminantAnalysis(solver=solver, shrinkage='auto', n_components=2)
    
    # 在训练集上拟合模型
    lda_clf.fit(X_train, y_train)
    # 在测试集上进行预测
    y_pred = lda_clf.predict(X_test)
    # 计算准确率
    acc = accuracy_score(y_test, y_pred)
    results[solver] = {'model': lda_clf, 'accuracy': acc, 'predictions': y_pred}
    print(f"Solver: {solver:10} | 测试集准确率: {acc:.4f}")

# 选择表现最好的模型进行详细分析
best_solver = max(results, key=lambda x: results[x]['accuracy'])
print(f"\n最佳求解器是: '{best_solver}'")
best_model = results[best_solver]['model']

对于Iris这样的小规模、良构数据集,几种求解器的准确率可能相差无几。但在实际工作中,尤其是高维数据中,shrinkage='auto'配合lsqreigen求解器往往能带来更鲁棒的表现。

3.3 模型评估与诊断

让我们对最佳模型进行更细致的评估,看看它到底在哪里分对了,哪里分错了。

# 生成分类报告
print("详细的分类报告:")
print(classification_report(y_test, results[best_solver]['predictions'], target_names=target_names))

# 绘制混淆矩阵
cm = confusion_matrix(y_test, results[best_solver]['predictions'])
plt.figure(figsize=(6,5))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=target_names, yticklabels=target_names)
plt.ylabel('真实标签')
plt.xlabel('预测标签')
plt.title('LDA分类混淆矩阵')
plt.show()

分类报告会给出精确率(Precision)、召回率(Recall)和F1分数,帮你从不同角度评估模型对每一类的分类能力。混淆矩阵则能直观地显示错误分类都发生在哪些类别之间。对于Iris数据,你可能发现versicolorvirginica之间偶尔会有误判,这与我们在二维散点图上看到的它们部分重叠的情况是吻合的。

4. 超越Iris:在复杂数据集上的实践技巧

掌握了基础操作后,我们需要面对更真实的世界。现实中的数据很少像Iris那样干净、线性可分。这一节,我们使用一个稍复杂的数据集——葡萄酒识别数据集(Wine),来探讨LDA在实际应用中可能遇到的问题及其解决方案。

4.1 处理更复杂的数据

葡萄酒数据集包含13种化学成分特征,用于区分3种不同产地的葡萄酒。我们首先加载并探索数据。

# 加载葡萄酒数据集
wine = datasets.load_wine()
X_wine = wine.data
y_wine = wine.target
feature_names_wine = wine.feature_names
target_names_wine = wine.target_names

print(f"葡萄酒数据集特征形状: {X_wine.shape}")
print(f"特征示例:\n{pd.DataFrame(X_wine[:3], columns=feature_names_wine).to_string()}")
print(f"类别分布: {np.bincount(y_wine)}")

这个数据集有13个特征,样本量(178)也不算大,可能存在特征尺度差异大、特征间相关性强等问题。直接应用LDA可能会遇到挑战。

4.2 关键步骤:特征标准化

LDA虽然不像SVM或KNN那样对特征尺度极度敏感,但良好的数据准备习惯总是有益的。特别是当特征的单位和量级差异很大时(比如酒精含量和颜色强度),进行标准化可以提升数值稳定性。

from sklearn.preprocessing import StandardScaler

# 标准化特征(减去均值,除以标准差)
scaler = StandardScaler()
X_wine_scaled = scaler.fit_transform(X_wine)

# 划分训练测试集
X_train_w, X_test_w, y_train_w, y_test_w = train_test_split(
    X_wine_scaled, y_wine, test_size=0.25, random_state=42, stratify=y_wine
)

# 使用带收缩的LDA
lda_wine = LinearDiscriminantAnalysis(solver='lsqr', shrinkage='auto')
lda_wine.fit(X_train_w, y_train_w)

# 降维并可视化
X_wine_lda = lda_wine.transform(X_train_w)

plt.figure(figsize=(10, 8))
for i, target_name in enumerate(target_names_wine):
    plt.scatter(X_wine_lda[y_train_w == i, 0],
                X_wine_lda[y_train_w == i, 1],
                alpha=0.8, label=target_name)
plt.legend()
plt.xlabel('LD1')
plt.ylabel('LD2')
plt.title('葡萄酒数据集LDA降维可视化(训练集)')
plt.grid(True, linestyle='--', alpha=0.3)
plt.show()

# 评估分类性能
y_pred_w = lda_wine.predict(X_test_w)
print(f"葡萄酒数据集测试集准确率: {accuracy_score(y_test_w, y_pred_w):.4f}")

4.3 应对LDA的局限性:当假设不满足时

LDA的核心假设之一是各类别的协方差矩阵相同(即同方差性)。在真实数据中,这个假设常常不成立。此外,当特征数量远大于样本数量时,类内散度矩阵Sw会变得奇异(不可逆),导致计算失败。这时,我们有几个策略:

  1. 使用svd求解器:它不直接计算Sw的逆,因此可以避免矩阵奇异的问题。
  2. 启用收缩(Shrinkage):如前所述,shrinkage='auto'可以自动估计最优的收缩参数,正则化协方差矩阵的估计,对违反同方差假设的情况有一定鲁棒性。
  3. 先进行PCA降维:这是一个经典的两阶段方法。先用PCA将数据降到较低维度(确保样本数大于特征数),消除多重共线性,然后再应用LDA。这在图像、文本等高维数据中很常见。

下面是一个PCA+LDA管道(Pipeline)的示例:

from sklearn.decomposition import PCA
from sklearn.pipeline import make_pipeline

# 创建一个先PCA后LDA的管道
# 假设我们先将13维特征用PCA降到10维(确保满秩),再用LDA降到2维
pca = PCA(n_components=10, whiten=True)  # whiten可以对PCA结果进行标准化
lda = LinearDiscriminantAnalysis(n_components=2)

pipeline = make_pipeline(StandardScaler(), pca, lda)

# 在训练集上拟合管道
pipeline.fit(X_train_w, y_train_w)
# 转换训练集并可视化
X_train_pca_lda = pipeline.transform(X_train_w)

# 评估整个管道在测试集上的分类性能
test_accuracy = pipeline.score(X_test_w, y_test_w)
print(f"PCA+LDA管道在测试集上的准确率: {test_accuracy:.4f}")

通过组合这些技术,我们可以让LDA在更复杂、更具挑战性的数据上依然保持强大的判别和分类能力。记住,没有放之四海而皆准的算法,理解其原理和局限,才能灵活运用工具解决实际问题。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐