当前位置: 首页 > news >正文

别再死记公式了!用Python手撸一个LDA分类器,从鸢尾花数据集开始

从零实现LDA分类器:用Python代码解锁鸢尾花数据集的分类奥秘

当我在第一次接触线性判别分析(LDA)时,那些复杂的数学公式让我望而生畏。直到有一天,我决定抛开教科书,直接用Python代码一步步实现这个算法,才真正理解了它的精妙之处。本文将带你体验这段从理论到实践的旅程,我们不仅会用NumPy手写LDA的核心计算,还会在经典的鸢尾花数据集上进行可视化展示,让你获得"亲手实现"的成就感。

1. 环境准备与数据探索

在开始编码前,我们需要准备好Python环境和必要的数据集。推荐使用Anaconda创建虚拟环境,这能避免包依赖冲突:

conda create -n lda_demo python=3.8 conda activate lda_demo pip install numpy matplotlib scikit-learn

鸢尾花数据集是机器学习领域的"Hello World",它包含三种鸢尾花的四个特征:萼片长度、萼片宽度、花瓣长度和花瓣宽度。我们先加载并观察数据:

from sklearn.datasets import load_iris import numpy as np iris = load_iris() X = iris.data y = iris.target feature_names = iris.feature_names print(f"特征矩阵形状: {X.shape}") print(f"类别分布: {np.bincount(y)}") print("特征名称:", feature_names)

输出结果会显示我们有150个样本,均匀分布在三个类别中。为了简化问题,我们暂时只考虑二分类场景,选择前两个类别:

# 提取前两类数据 X = X[y != 2] y = y[y != 2]

2. LDA核心算法实现

LDA的核心思想是找到最佳投影方向,使得同类样本尽可能聚集,不同类样本尽可能分离。这需要计算两个关键矩阵:

2.1 计算类内散度矩阵

类内散度矩阵(S_w)衡量同类样本的离散程度。我们需要先计算每个类别的协方差矩阵,然后求和:

def compute_within_class_scatter(X, y): classes = np.unique(y) n_features = X.shape[1] S_w = np.zeros((n_features, n_features)) for c in classes: X_c = X[y == c] # 计算类内协方差矩阵,注意ddof=1表示无偏估计 cov_matrix = np.cov(X_c, rowvar=False, ddof=1) S_w += cov_matrix return S_w S_w = compute_within_class_scatter(X, y) print("类内散度矩阵:\n", S_w)

2.2 计算类间散度矩阵

类间散度矩阵(S_b)衡量不同类别中心点的离散程度:

def compute_between_class_scatter(X, y): overall_mean = np.mean(X, axis=0) classes = np.unique(y) n_features = X.shape[1] S_b = np.zeros((n_features, n_features)) for c in classes: n_c = np.sum(y == c) mean_c = np.mean(X[y == c], axis=0) # 计算每个类别的贡献 diff = (mean_c - overall_mean).reshape(-1, 1) S_b += n_c * np.dot(diff, diff.T) return S_b S_b = compute_between_class_scatter(X, y) print("类间散度矩阵:\n", S_b)

2.3 求解最优投影方向

LDA的最优投影方向是最大化广义瑞利商的方向,这可以通过求解广义特征值问题得到:

def lda(X, y): S_w = compute_within_class_scatter(X, y) S_b = compute_between_class_scatter(X, y) # 数值稳定的求逆方法 eigenvalues, eigenvectors = np.linalg.eig(np.linalg.pinv(S_w) @ S_b) # 选择最大特征值对应的特征向量 idx = eigenvalues.argsort()[::-1] eigenvectors = eigenvectors[:, idx] w = eigenvectors[:, 0].real # 归一化投影向量 w = w / np.linalg.norm(w) return w w = lda(X, y) print("最优投影方向:", w)

在实际项目中,我们可能会遇到矩阵奇异的问题。这时可以添加一个小的正则化项来保证数值稳定性:

S_w_reg = S_w + 1e-6 * np.eye(S_w.shape[0])

3. 结果可视化与分析

理解算法最好的方式就是看到它的实际效果。让我们将原始数据和投影后的结果可视化:

import matplotlib.pyplot as plt # 原始数据散点图 plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) for c in np.unique(y): plt.scatter(X[y == c, 0], X[y == c, 1], label=f'Class {c}') plt.xlabel('Sepal length') plt.ylabel('Sepal width') plt.title('Original Data') plt.legend() # 计算投影后的数据 projected = X @ w.reshape(-1, 1) # 投影结果可视化 plt.subplot(1, 2, 2) for c in np.unique(y): plt.hist(projected[y == c], alpha=0.7, label=f'Class {c}', bins=20) plt.xlabel('Projection Value') plt.ylabel('Frequency') plt.title('LDA Projection Results') plt.legend() plt.tight_layout() plt.show()

这段代码会生成两个子图:左边显示原始特征空间中的数据分布,右边显示投影到最佳方向后的分布情况。你会看到投影后的两类数据明显更易区分。

4. 性能评估与边界绘制

为了量化我们的LDA实现效果,我们可以计算分类准确率,并绘制决策边界:

from sklearn.metrics import accuracy_score # 计算投影后的类别中心 mean_0 = np.mean(projected[y == 0]) mean_1 = np.mean(projected[y == 1]) # 计算决策阈值 threshold = (mean_0 + mean_1) / 2 # 预测类别 y_pred = (projected > threshold).astype(int).flatten() # 计算准确率 acc = accuracy_score(y, y_pred) print(f"分类准确率: {acc:.2%}") # 绘制决策边界 plt.figure(figsize=(8, 6)) for c in np.unique(y): plt.scatter(X[y == c, 0], X[y == c, 1], label=f'Class {c}') # 生成网格点用于绘制决策边界 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, 0.1), np.arange(y_min, y_max, 0.1)) # 计算网格点的投影值 Z = np.c_[xx.ravel(), yy.ravel()] @ w.reshape(-1, 1) Z = (Z > threshold).astype(int).reshape(xx.shape) # 绘制决策边界 plt.contourf(xx, yy, Z, alpha=0.3, levels=[-0.5, 0.5, 1.5]) plt.xlabel('Sepal length') plt.ylabel('Sepal width') plt.title('LDA Decision Boundary') plt.legend() plt.show()

5. 扩展到多类别场景

虽然我们演示的是二分类场景,但LDA天然支持多类别分类。关键在于计算投影矩阵时选择多个特征向量:

def multiclass_lda(X, y, n_components=None): S_w = compute_within_class_scatter(X, y) S_b = compute_between_class_scatter(X, y) # 解决数值稳定性问题 S_w_reg = S_w + 1e-6 * np.eye(S_w.shape[0]) eigenvalues, eigenvectors = np.linalg.eig(np.linalg.pinv(S_w_reg) @ S_b) # 按特征值降序排序 idx = eigenvalues.argsort()[::-1] eigenvectors = eigenvectors[:, idx] eigenvalues = eigenvalues[idx] # 选择前n_components个特征向量 if n_components is not None: eigenvectors = eigenvectors[:, :n_components] return eigenvectors.real # 使用全部三类数据 X_full = iris.data y_full = iris.target W = multiclass_lda(X_full, y_full, n_components=2) print("投影矩阵:\n", W) # 投影数据 X_lda = X_full @ W # 可视化投影结果 plt.figure(figsize=(8, 6)) for c in np.unique(y_full): plt.scatter(X_lda[y_full == c, 0], X_lda[y_full == c, 1], label=f'Class {c}') plt.xlabel('LD1') plt.ylabel('LD2') plt.title('Multiclass LDA Projection') plt.legend() plt.show()

在多类别场景中,LDA实际上是将数据投影到一个最多有C-1维的子空间(C是类别数)。对于三分类问题,我们最多可以得到两个有意义的线性判别方向。

6. 实际应用中的注意事项

在真实项目中应用LDA时,有几个关键点需要特别注意:

  • 特征缩放:虽然LDA不受特征尺度影响(因为涉及协方差计算),但为了数值稳定性,建议标准化数据:
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X)
  • 维度灾难:当特征维度很高而样本数较少时,S_w可能奇异。解决方法包括:

    • 增加正则化项
    • 先使用PCA降维
    • 使用伪逆代替常规逆
  • 类别不平衡:原始LDA假设各类别分布均匀。对于不平衡数据,可以调整类间散度矩阵的计算方式:

# 考虑类别权重的S_b计算 def weighted_between_class_scatter(X, y): overall_mean = np.mean(X, axis=0) classes, counts = np.unique(y, return_counts=True) n_features = X.shape[1] S_b = np.zeros((n_features, n_features)) total_samples = len(y) for c, n_c in zip(classes, counts): mean_c = np.mean(X[y == c], axis=0) diff = (mean_c - overall_mean).reshape(-1, 1) weight = n_c / total_samples # 使用类别比例作为权重 S_b += weight * np.dot(diff, diff.T) return S_b
  • 与PCA的区别:虽然PCA和LDA都是线性降维方法,但它们的优化目标不同:

    特性PCALDA
    优化目标最大化方差最大化类间分离度
    监督/无监督无监督有监督
    投影方向数由用户指定最多C-1个(C为类别数)
    适用场景探索性分析、特征提取分类任务、可解释性降维

在完成这个项目后,我最大的收获是理解了数学公式背后的实际意义。比如,类内散度矩阵实际上是在量化每个类别的"紧凑程度",而类间散度矩阵则衡量不同类别的"分离程度"。这种从代码实现反推理论理解的方式,比单纯学习数学推导要直观得多。

http://www.gsyq.cn/news/1437723.html

相关文章:

  • Argo浮标数据怎么用?手把手教你用Python替代Matlab计算海洋热容与盐容贡献
  • 昆山名酒回收电话评测:上海附近上门回收名酒/昆山五粮液回收/昆山八大回收/从核心维度选靠谱服务商 - 优质品牌商家
  • 保姆级教程:在Ubuntu 22.04上,用RTX 40系显卡从零搞定DeepStream 6.4(含CUDA 12.2和TensorRT 8.6.1.6)
  • SEED数据集实战:用Python+MNE批量读取脑电数据,附完整代码与通道映射表
  • AI副业月入6000?我扒了数据,真相扎心了
  • 2026年重庆闲置名表名包回收可靠机构排行盘点 - 优质品牌商家
  • Xshell 7免费版连接VMware Linux保姆级教程:从密钥对登录到文件传输全搞定
  • 告别iSaver!用Wallpaper Engine免费搞定Win10动态锁屏(附保姆级设置流程)
  • Codex 子代理:串行 vs 并行,快多少
  • 2026年白色硅灰厂家选型技术推荐:纳米级微硅粉/超细微硅粉/四川微硅粉厂家/四川硅灰/核心指标解析 - 优质品牌商家
  • AI写论文的宝藏工具!4款AI论文写作助手,让你的写作过程更顺畅
  • 如何用VinXiangQi打造你的智能象棋AI助手:从零开始到专业级分析
  • 深入xv6内核:为每个进程创建独立内核页表到底解决了什么问题?
  • 保姆级教程:在Linux上从零配置TongLINKQ 8.1.15.2客户端,实现与服务端通信
  • Beyond Compare 5逆向工程:RSA非对称加密授权机制深度解析与密钥生成器实战
  • 2026年台州税务代理公司选对=合规高效 企赢税务智能财税推荐(含联系方式) - 本地品牌推荐
  • 2026年Trae与Claude Code优缺点对比:深度横评解析
  • Cora和Citeseer数据集上可直接运行的GCN链路预测代码包(含预处理、训练与评估)
  • 2026 年郑州化妆品柜展柜厂家技术与服务分析报告
  • STM32F103扫地机器人实战工程:FreeRTOS多任务调度+IAP远程升级+电池与传感器全链路管理
  • 告别系统升级焦虑:Ubuntu 22.04 LTS 到 24.04 LTS 保姆级升级指南(含 do-release-upgrade 详解)
  • 告别Ubuntu 22.04默认Dock:这几个gsettings命令和Gnome扩展让你效率翻倍
  • 十年 PM 走心总结:职场管理者的底层逻辑
  • C++如何与C语言混合编程_在C++项目中调用C库函数的extern “C“方法
  • MATLAB版LMS自适应滤波实操包:带运行录像、可调参数源码与收敛效果可视化
  • 从零开始搭建知识问答系统
  • 【Redis】 五大基础数据类型 底层原理深度解析
  • 2026年5月更新:武汉优秀船闸防撞装置生产厂家的选择策略与深度解析 - 2026年企业资讯
  • 从‘宋体.ttf’到屏幕显示:一个汉字在Windows/Linux系统里经历了什么?
  • Spring AI企业级RAG优化|Redis会话记忆持久化+混合检索权重调优(大幅提升问答准确率)