- 机器学习
- 教程
【免费下载链接】python-machine-learning-book
The "Python Machine Learning (1st edition)" book code repository and info resource
本指南以python-machine-learning-book仓库第 5 章《Compressing Data via Dimensionality Reduction(通过降维压缩数据)》为骨架,系统讲解三类降维技术的原理、手工实现与 scikit-learn 调用方式:无监督的主成分分析(PCA)、有监督的线性判别分析(LDA),以及用于非线性映射的核主成分分析(核 PCA / Kernel PCA)。读完本文,你将能够独立复现特征分解求主成分、构造类内/类间散布矩阵求解线性判别式,以及从零实现 RBF 核 PCA 并投影新样本的完整流程,并能在 Wine 数据集与合成数据(半月形、同心圆)上完成可运行的实战验证。
一、章节概览:一条完整的降维技术路线
本章的原版目录(见 code/ch05/README.md)规划了如下内容主线:
- 无监督降维:主成分分析(PCA)
- 总方差与解释方差(Total and explained variance)
- 特征变换(Feature transformation)
- scikit-learn 中的 PCA
- 有监督数据压缩:线性判别分析(LDA)
- 计算散布矩阵(Computing the scatter matrices)
- 为新的特征子空间选择线性判别式(Selecting linear discriminants)
- 将样本投影到新特征空间(Projecting samples onto the new feature space)
- scikit-learn 中的 LDA
- 使用核 PCA 进行非线性映射(kernel PCA)
- 核函数与核技巧(Kernel functions and the kernel trick)
- 用 Python 实现核 PCA
- 例 1:分离半月形数据(half-moon shapes)
- 例 2:分离同心圆数据(concentric circles)
- 投影新数据点(Projecting new data points)
- scikit-learn 中的核 PCA
- 小结
仓库为本章提供了三种可执行载体,内容完全同源、可互相印证:
| 载体 | 路径 | 说明 |
|---|---|---|
| 交互式 Notebook | code/ch05/ch05.ipynb | 112 个 cell(57 个 Markdown + 55 个代码 cell),含图文讲解与逐步输出 |
| 纯脚本 | code/optional-py-scripts/ch05.py | 与 Notebook 等价的顺序执行脚本,适合命令行直接运行 |
| 本地数据集 | code/datasets/wine/wine.data | Wine 数据集的仓库本地副本,避免在线下载失败 |
运行前可先通过 code/check_environment.ipynb 检查环境;依赖包括numpy、pandas、scipy、matplotlib与scikit-learn。从源码结构看,ch05.py 还针对 scikit-learn 0.18 的 API 变更做了兼容处理:低于 0.18 时从sklearn.grid_search与sklearn.lda导入,否则从sklearn.model_selection与sklearn.discriminant_analysis导入——说明该章节代码适用于 0.18 及以上的 scikit-learn 版本。
二、无监督降维:主成分分析(PCA)
2.1 核心思想:沿最大方差方向投影
PCA 是无监督的线性变换:它寻找一组相互正交的轴(主成分),使得数据沿这些轴投影后方差最大。几何上可以理解为把数据点云的主伸展方向(协方差椭圆的长轴方向)找出来,再丢弃方差贡献小的方向,从而用更少的维度保留最多的信息。本章用 Wine 数据集(第 4、5 章共用,规格见 code/datasets/wine/README.md)演示完整流程:该数据集包含178 个样本、13 个特征、3 个类别(三个类别样本数分别为 59、71、48),特征包括 Alcohol、Malic acid、Ash、Alcalinity of ash、Magnesium、Total phenols、Flavanoids、Nonflavanoid phenols、Proanthocyanins、Color intensity、Hue、OD280/OD315 of diluted wines、Proline。
2.2 数据准备:划分训练集与标准化
首先读取数据并划分 70% 训练集 / 30% 测试集,随后用StandardScaler标准化(这是 PCA 前的关键一步,避免量纲较大的特征主导方差)。注意源码中的用法是训练集fit_transform、测试集仅transform(见 ch05.py),避免数据泄漏;Notebook 中作者还专门备注过一处笔误(误将测试集写成fit_transform),提示了正确的做法:
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/' 'machine-learning-databases/wine/wine.data', header=None) df_wine.columns = ['Class label', 'Alcohol', 'Malic acid', 'Ash', 'Alcalinity of ash', 'Magnesium', 'Total phenols', 'Flavanoids', 'Nonflavanoid phenols', 'Proanthocyanins', 'Color intensity', 'Hue', 'OD280/OD315 of diluted wines', 'Proline'] X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values X_train, X_test, y_train, y_test = \ train_test_split(X, y, test_size=0.3, random_state=0) sc = StandardScaler() X_train_std = sc.fit_transform(X_train) X_test_std = sc.transform(X_test)若在线地址不可用,Notebook 中明确指出可以在仓库本地副本 code/datasets/wine/wine.data 找到该数据集(相对 Notebook 的路径写作./../datasets/wine/wine.data)。
2.3 协方差矩阵的特征分解
PCA 的第一步是构造标准化数据的协方差矩阵,并对其进行特征分解(见 ch05.py):
import numpy as np cov_mat = np.cov(X_train_std.T) eigen_vals, eigen_vecs = np.linalg.eig(cov_mat) print('\nEigenvalues \n%s' % eigen_vals)特征值衡量对应特征向量(主成分方向)上解释的方差大小。Notebook 中作者特别备注:此处使用numpy.linalg.eig分解的是对称的协方差矩阵,而对称矩阵更稳妥的做法是用numpy.linalg.eigh(专为对称/厄米矩阵设计,返回排序后的特征值)。这一细节在后面手工实现核 PCA 时会被再次用到——届时由于核矩阵必然对称,源码直接选用scipy.linalg.eigh。
2.4 总方差与解释方差
解释方差比(explained variance ratio)指单个特征值占总特征值之和的比例:$\text{var_exp}i = \lambda_i / \sum{j=1}^{d} \lambda_j$。累积解释方差则用np.cumsum计算。源码(ch05.py)同时绘制柱状图(单个主成分解释方差)与阶梯线(累积解释方差):
tot = sum(eigen_vals) var_exp = [(i / tot) for i in sorted(eigen_vals, reverse=True)] cum_var_exp = np.cumsum(var_exp) plt.bar(range(1, 14), var_exp, alpha=0.5, align='center', label='individual explained variance') plt.step(range(1, 14), cum_var_exp, where='mid', label='cumulative explained variance') plt.ylabel('Explained variance ratio') plt.xlabel('Principal components') plt.legend(loc='best') plt.show()上图是运行结果:前两个主成分即解释了 Wine 数据绝大部分方差,后续主成分贡献迅速衰减——这正是"用少量主成分压缩数据"的直观证据。
2.5 特征变换:构造投影矩阵 W
选定前 k 个特征向量后,将其按列堆叠成投影矩阵 $W \in \mathbb{R}^{d \times k}$,新特征 $X' = X_{std} \cdot W$。源码实现(ch05.py)将(特征值绝对值, 特征向量)组成元组并按特征值降序排序,取前两个特征向量构造 W:
eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:, i]) for i in range(len(eigen_vals))] eigen_pairs.sort(reverse=True) w = np.hstack((eigen_pairs[0][1][:, np.newaxis], eigen_pairs[1][1][:, np.newaxis])) X_train_pca = X_train_std.dot(w)两个实战要点:
- 符号翻转:Notebook 明确提示,不同 NumPy/LAPACK 版本下特征向量的符号可能整体翻转,得到的 W 与书中截图相反,但这不影响降维与分类效果(只是坐标轴镜像);
- 点积验证:源码打印了
X_train_std[0].dot(w),确认单样本投影结果与矩阵乘法一致,便于逐元素核对。
2.6 scikit-learn 中的 PCA
手工实现有助于理解原理,实际工程可直接使用sklearn.decomposition.PCA(见 ch05.py):
from sklearn.decomposition import PCA pca = PCA() X_train_pca = pca.fit_transform(X_train_std) print('Variance explained ratio:\n', pca.explained_variance_ratio_) pca = PCA(n_components=2) X_train_pca = pca.fit_transform(X_train_std) X_test_pca = pca.transform(X_test_std)关键参数与属性:
n_components:保留的主成分数。设为None(默认)时保留全部主成分,可通过explained_variance_ratio_观察累积解释方差后人工决定截断点;explained_variance_ratio_:每个主成分的解释方差比,是选择维度的核心依据;fit_transform/transform:训练集用前者拟合并变换,测试集用后者仅变换,与手工流程一致。
降维后的训练集与测试集均可在 PC1–PC2 平面绘制散点图,并接入LogisticRegression训练分类器,用plot_decision_regions绘制决策边界——三类样本在二维主成分空间中已基本可分,验证了"压缩后仍保留判别信息"。
三、有监督数据压缩:线性判别分析(LDA)
3.1 与 PCA 的本质区别
PCA 不关心类别标签,只最大化整体方差;LDA 则有监督,目标是最大化类间距离、最小化类内距离,找到最能区分类别的方向(线性判别式,Linear Discriminants)。PCA 寻找的是"描述数据"的轴,LDA 寻找的是"区分类别"的轴。
3.2 计算散布矩阵
LDA 的核心是两类散布矩阵(见 ch05.py):
- 各类均值向量:对 3 个类别分别计算 13 维均值向量;
- 类内散布矩阵 $S_W$:先对每个类别计算各样本与其类均值之差的向量外积之和,再对类别求和:
d = 13 # number of features S_W = np.zeros((d, d)) for label, mv in zip(range(1, 4), mean_vecs): class_scatter = np.zeros((d, d)) for row in X_train_std[y_train == label]: row, mv = row.reshape(d, 1), mv.reshape(d, 1) class_scatter += (row - mv).dot((row - mv).T) S_W += class_scatter- 用协方差矩阵修正:由于三个类别样本数不均衡(59 / 71 / 48),直接累加外积会放大样本数多的类别的贡献。源码随即展示更合理的做法——用各类别的协方差矩阵$\text{cov}(X_{train,label}^T)$ 求和替代 $S_W$,本质是按样本数做了归一化(见 ch05.py);
- 类间散布矩阵 $S_B$:以总体均值为基准,用"每类样本数 ×(类均值 − 总体均值)外积"求和:
mean_overall = np.mean(X_train_std, axis=0) d = 13 S_B = np.zeros((d, d)) for i, mean_vec in enumerate(mean_vecs): n = X_train[y_train == i + 1, :].shape[0] S_B += n * (mean_vec - mean_overall).dot((mean_vec - mean_overall).T)3.3 求解广义特征值问题:$S_W^{-1}S_B$
对矩阵 $S_W^{-1}S_B$ 做特征分解,得到的特征值越大,对应判别方向上的"类别可区分性"越强(见 ch05.py):
eigen_vals, eigen_vecs = np.linalg.eig(np.linalg.inv(S_W).dot(S_B)) eigen_pairs = sorted(eigen_pairs, key=lambda k: k[0], reverse=True)与 PCA 的方差图类似,源码绘制了"可区分性"(discriminability)柱状图与累积曲线:LDA 的判别子空间最多只有 c − 1 = 2 个非零判别式(c 为类别数),前两个判别式即承载了全部可区分性。
3.4 投影样本与新特征空间
取前两个判别向量堆叠成 W,投影 $X' = X_{std} \cdot W$(见 ch05.py)。源码在绘图时对坐标取了相反数(乘以 −1),并在 Notebook 中说明这只是可视化方向调整,不影响判别结果。投影后的三类样本在 LD1–LD2 平面上被清晰地分到不同区域。
3.5 scikit-learn 中的 LDA
实际使用中直接调用sklearn.discriminant_analysis.LinearDiscriminantAnalysis(见 ch05.py):
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA lda = LDA(n_components=2) X_train_lda = lda.fit_transform(X_train_std, y_train) lr = LogisticRegression() lr = lr.fit(X_train_lda, y_train) X_test_lda = lda.transform(X_test_std)注意 LDA 与 PCA 的关键差异:fit_transform需要同时传入X_train_std与y_train(监督信息);训练好的lr分类器在训练集与测试集的 LD 平面上绘制的决策边界几乎一致,说明 LDA 压缩出的二维空间对未见数据同样具有良好的泛化判别能力。
四、核 PCA:处理非线性映射
4.1 为什么需要核技巧
PCA 与 LDA 都是线性变换:当数据本身呈非线性结构(如半月形、同心圆)时,线性投影无法有效分离类别。核 PCA 的思路是先用核函数把样本隐式映射到高维特征空间,在该空间中执行标准 PCA,从而让原本线性不可分的数据变得线性可分,同时不必显式计算高维坐标——这就是"核技巧"(kernel trick)。常用的核函数包括 RBF(高斯)核 $k(x, x') = \exp(-\gamma |x - x'|^2)$ 等。
4.2 从零实现 RBF 核 PCA
仓库源码提供了完整的rbf_kernel_pca实现(见 ch05.py),步骤清晰可复现:
from scipy.spatial.distance import pdist, squareform from scipy import exp from scipy.linalg import eigh def rbf_kernel_pca(X, gamma, n_components): # 1) 计算两两样本的平方欧氏距离 sq_dists = pdist(X, 'sqeuclidean') mat_sq_dists = squareform(sq_dists) # 2) 计算对称核矩阵 K(RBF 核) K = exp(-gamma * mat_sq_dists) # 3) 对核矩阵中心化 N = K.shape[0] one_n = np.ones((N, N)) / N K = K - one_n.dot(K) - K.dot(one_n) + one_n.dot(K).dot(one_n) # 4) 特征分解(eigh 针对对称矩阵并返回有序特征值) eigvals, eigvecs = eigh(K) # 5) 取前 k 个特征向量作为投影结果 X_pc = np.column_stack((eigvecs[:, -i] for i in range(1, n_components + 1))) return X_pc实现要点:
pdist(X, 'sqeuclidean')计算两两平方欧氏距离,squareform还原为对称矩阵;- 核矩阵必须中心化(centering),否则结果不是真正的主成分——公式 $K' = K - 1_N K - K 1_N + 1_N K 1_N$ 是核 PCA 正确性的关键;
- 此处特意选用
scipy.linalg.eigh而非eig:核矩阵对称,eigh返回升序特征值,取末尾 k 个(eigvecs[:, -i])即对应最大的 k 个特征值,与 2.3 节的备注呼应。
4.3 例 1:分离半月形数据
用sklearn.datasets.make_moons(n_samples=100, random_state=123)生成半月形数据。对比可见(源码见 ch05.py):
- 线性 PCA 将两类样本投影到 PC1 后完全重叠,无法分离;
- 调用
rbf_kernel_pca(X, gamma=15, n_components=2)后,两类样本在 PC1 轴上被明显分开,且右子图展示的一维投影(PC1 值分布)呈现出清晰的两段分离——非线性结构被成功"拉直"。
gamma=15是 RBF 核的调节参数:gamma 越大,核的"局部影响范围"越小,决策边界越复杂;实际使用时通常需要通过交叉验证调优。
4.4 例 2:分离同心圆数据
make_circles(n_samples=1000, random_state=123, noise=0.1, factor=0.2)生成内外两层同心圆。同样的对比结论成立(见 ch05.py):线性 PCA 投影后两类在 PC1 上几乎完全混叠;而核 PCA 将内圈与外圈样本沿 PC1 分离成两个清晰的区间。这一类"线性方法彻底失效"的数据形态,正是核 PCA 的典型应用场景。
4.5 投影新数据点:project_x
与线性 PCA 不同,核 PCA 的"特征向量"是训练样本的线性组合(以核函数形式出现),无法像 PCA 那样用简单的 W 矩阵直接变换新样本。源码给出了针对 RBF 核的投影函数project_x(见 ch05.py):
def rbf_kernel_pca(X, gamma, n_components): # ... 与 4.2 相同,但额外返回: alphas = np.column_stack((eigvecs[:, -i] for i in range(1, n_components + 1))) lambdas = [eigvals[-i] for i in range(1, n_components + 1)] return alphas, lambdas def project_x(x_new, X, gamma, alphas, lambdas): pair_dist = np.array([np.sum((x_new - row)**2) for row in X]) k = np.exp(-gamma * pair_dist) return k.dot(alphas / lambdas)project_x的数学依据是:新样本的核 PCA 坐标 = 新样本与所有训练样本的核向量 $k$ 点乘(特征向量 / 特征值)。源码以X[25]为"新样本"验证了流程:alphas[25](原始投影)与project_x重投影(remapped point)在图上的位置吻合,说明该函数可正确把未见样本映射进已训练好的核 PCA 子空间——这是把核 PCA 用于真实预测流水线(如分类器推理)的必要环节。
4.6 scikit-learn 中的核 PCA
工程实践直接使用sklearn.decomposition.KernelPCA(见 ch05.py):
from sklearn.decomposition import KernelPCA scikit_kpca = KernelPCA(n_components=2, kernel='rbf', gamma=15) X_skernpca = scikit_kpca.fit_transform(X)KernelPCA支持linear、poly、rbf、sigmoid等多种核,gamma为 RBF/多项式等核的参数,其降维结果与手工实现基本一致(符号方向可能不同,但结构相同),可作为生产环境的首选实现。
五、三种方法如何选型
结合仓库的 FAQ 文档 docs/faq/dimensionality-reduction.md 可以形成清晰的选型原则:
- PCA(无监督):不利用标签,寻找正交的最大方差轴,适合数据可视化、去相关、压缩与无标签场景;
- LDA(有监督):利用标签最大化类别可分性,适合线性可分程度较高的分类任务前置压缩;FAQ 同时提醒,虽然直觉上分类任务 LDA 优于 PCA,但实证并不总是如此,需要按数据验证;
- 核 PCA(非线性):能分离同心圆这类线性不可分数据,但并非万能——例如它无法展开瑞士卷(Swiss Roll)流形,此时应改用局部线性嵌入(LLE)等流形学习方法(相关对比可见 docs/faq/large-num-features.md 与 docs/faq/lda-vs-pca.md)。
即:没有恒优的降维方法,选择取决于数据形态与任务目标(线性 vs 非线性、有监督 vs 无监督)。
六、运行与验证方式
- Notebook 方式:启动 Jupyter 打开 code/ch05/ch05.ipynb,逐 cell 执行即可看到每步输出与图表;
- 脚本方式:直接运行
python code/optional-py-scripts/ch05.py,脚本按章节打印分隔标题并依次执行全部代码段; - 数据源:默认从 UCI 在线地址读取 Wine 数据;离线时改用 code/datasets/wine/wine.data;
- 可运行性验证:仓库在 code/ch01/tests/test_notebooks.py 中提供了基于
nbconvert --execute的 Notebook 执行测试模式,可用于验证 Notebook 端到端可运行(该测试文件当前仅挂接附录 G 的 TensorFlow Notebook,可作为扩展本章验证的参考写法)。
小结
本章以"降维压缩数据"为统一主题,给出了从原理到实现再到工程调用的完整闭环:PCA 用无监督的方差最大化压缩数据并可视化;LDA 引入类别标签、通过 $S_W^{-1}S_B$ 的广义特征分解最大化类别可分性;核 PCA 借助核技巧把非线性结构映射到可线性分离的高维空间,并提供了新样本投影的完整数学方案。仓库中 code/optional-py-scripts/ch05.py 与 code/ch05/ch05.ipynb 的全部代码均可直接复现,是理解并落地这三类降维技术的可靠参照。
- 机器学习
- 教程
【免费下载链接】python-machine-learning-book
The "Python Machine Learning (1st edition)" book code repository and info resource
相关推荐
5分钟上手Dochub:前端开发者必备的离线文档解决方案
5分钟上手Dochub:前端开发者必备的离线文档解决方案 Dochub 是一款专为开发者设计的离线文档搜索工具,让你在没有网络连接的情况下也能快速查阅前端开发相
后端开发工具CANN/Ascend C调试打印API
Print<a name="ZH CN_TOPIC_0000002330927402" </a 产品支持情况<a name="section1550532418
人工智能深度学习算子库CANNAscendp5play快速入门:从零到一的完整游戏制作教程
p5play快速入门:从零到一的完整游戏制作教程 p5play是一款基于p5.js和Box2D物理引擎的JavaScript游戏开发框架,它让游戏开发变得简单直
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考