☰
案例:PCA对手写数字数据集的降维——从配置到KNN/随机森林验证
2026/9/27 20:00:37 网站建设 项目流程

1. 手写数字降维这件事,卡在哪一步

sklearn 自带的 digits 数据集是 1797 条 8x8 的灰度图,展开成 64 维特征。很多人第一次跑 PCA 教程,用的就是它,因为小、快、不占内存。但真到自己的项目里,数据往往是 42000 行、784 列那种规模,KNN 跑一次要半小时,随机森林虽然快但精度差一截,这时候才会认真想:能不能先把维度砍下来,再让 KNN 上场。

PCA 降维能做什么?它把原始特征做线性组合,生成一组互不相关的主成分,按方差贡献从大到小排列。你保留前 k 个主成分,就等于用最少的维度保住最多的信息量。适合谁?适合特征维度高、样本之间距离计算昂贵、又想用 KNN 这类基于距离的模型的人。手写数字数据集就是典型场景:像素之间高度相关,64 维里真正有用的方向可能只有十几个。

这篇按完整流程走一遍:先给可复制的环境配置,再对比降维前后 KNN 与随机森林的精度和耗时,中间穿插主成分数怎么选、交叉验证怎么做。脚本可以直接跑,改个数据路径就能换成自己的数据。

2. 环境与依赖:把骨架先搭好

2.1 Python 版本与依赖清单

我用的是 Python 3.10,scikit-learn 1.3.x。版本不用完全一致,但建议 sklearn 不低于 1.0,因为PCA的n_components支持浮点数表示方差比例这个特性在旧版本行为略有差异。

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install numpy pandas scikit-learn matplotlib

如果你要复现 42000 行那个规模的数据,把 pandas 读 CSV 的部分留着;如果只用 sklearn 自带 digits,pandas 其实可以省掉,但留着方便你换成自己的表格数据。

2.2 数据加载的两种写法

sklearn 自带版本最省事:

from sklearn.datasets import load_digits digits = load_digits() X = digits.data # shape (1797, 64) y = digits.target # shape (1797,) print(X.shape, y.shape)

如果你手上有 Kaggle 那种 digit recognizor.csv,写法是:

import pandas as pd data = pd.read_csv('digit recognizor.csv') X = data.iloc[:, 1:].values y = data.iloc[:, 0].values print(X.shape, y.shape)

两种写法后面完全通用。我下面用自带 digits 演示,因为它不需要下载文件,你复制就能跑。

3. 可复制配置:主成分数怎么定

3.1 先看累计方差贡献率曲线

不要一上来就拍一个 n_components=25。先让 PCA 自己算一遍所有主成分,看累计解释方差比例。

import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA pca_full = PCA().fit(X) cum_var = np.cumsum(pca_full.explained_variance_ratio_) plt.figure(figsize=(10, 4)) plt.plot(range(1, len(cum_var) + 1), cum_var, marker='o', markersize=3) plt.xlabel('number of components') plt.ylabel('cumulative explained variance ratio') plt.axhline(y=0.95, color='r', linestyle='--', linewidth=1) plt.grid(alpha=0.3) plt.show()

这条曲线会先陡后平。拐点之前,每增加一个主成分带来的信息增益很大;拐点之后,曲线趋平,再加维度性价比就低了。红色虚线是 95% 方差线,你可以看它对应多少个主成分。

3.2 用学习曲线缩小范围

光看方差曲线还不够,因为方差大不等于对分类有用。更稳的做法是:把主成分数当超参数,用交叉验证的精度来选。

from sklearn.ensemble import RandomForestClassifier as RFC from sklearn.model_selection import cross_val_score scores = [] n_range = range(1, 65, 4) for n in n_range: X_dr = PCA(n_components=n).fit_transform(X) score = cross_val_score( RFC(n_estimators=50, random_state=0), X_dr, y, cv=5 ).mean() scores.append(score) plt.figure(figsize=(10, 4)) plt.plot(list(n_range), scores, marker='o') plt.xlabel('n_components') plt.ylabel('CV accuracy (RFC)') plt.grid(alpha=0.3) plt.show()

这一步会告诉你精度在哪个区间趋于饱和。我实测下来,digits 数据集在 15 到 25 之间就基本到顶了,再往上加精度波动很小。

3.3 细化区间,锁定最终值

把上一步表现最好的区间再切细一点:

scores_fine = [] n_fine = range(10, 31) for n in n_fine: X_dr = PCA(n_components=n).fit_transform(X) score = cross_val_score( RFC(n_estimators=50, random_state=0), X_dr, y, cv=5 ).mean() scores_fine.append(score) best_n = list(n_fine)[int(np.argmax(scores_fine))] print('best n_components =', best_n, 'acc =', max(scores_fine))

到这里你就有了一份可复制的配置:PCA(n_components=best_n)。注意每次fit_transform都要在训练集上做,交叉验证内部已经帮你处理了划分,但如果你自己做 train_test_split,PCA 只能 fit 训练集,再 transform 测试集,否则会信息泄漏。

4. 验证请求:降维前后 KNN 与随机森林对比

4.1 完整对比脚本

下面这段是核心验证代码,直接跑就能看到降维前后的精度和耗时差异。

import time from sklearn.neighbors import KNeighborsClassifier as KNN from sklearn.ensemble import RandomForestClassifier as RFC from sklearn.decomposition import PCA from sklearn.model_selection import cross_val_score def evaluate(model, X_data, y_data, cv=5): t0 = time.time() acc = cross_val_score(model, X_data, y_data, cv=cv).mean() cost = time.time() - t0 return acc, cost # 原始 64 维 acc_knn_raw, t_knn_raw = evaluate(KNN(), X, y) acc_rfc_raw, t_rfc_raw = evaluate(RFC(n_estimators=50, random_state=0), X, y) # PCA 降维后 n_comp = 20 X_pca = PCA(n_components=n_comp, random_state=0).fit_transform(X) acc_knn_pca, t_knn_pca = evaluate(KNN(), X_pca, y) acc_rfc_pca, t_rfc_pca = evaluate(RFC(n_estimators=50, random_state=0), X_pca, y) print(f'{"模型":<12}{"维度":<8}{"精度":<10}{"耗时(s)":<10}') print(f'{"KNN":<12}{"64":<8}{acc_knn_raw:<10.4f}{t_knn_raw:<10.2f}') print(f'{"RFC":<12}{"64":<8}{acc_rfc_raw:<10.4f}{t_rfc_raw:<10.2f}') print(f'{"KNN":<12}{n_comp:<8}{acc_knn_pca:<10.4f}{t_knn_pca:<10.2f}') print(f'{"RFC":<12}{n_comp:<8}{acc_rfc_pca:<10.4f}{t_rfc_pca:<10.2f}')

4.2 结果解读

在 digits 数据集上,我跑出来的典型结果是:原始 64 维 KNN 精度约 0.988,耗时约 0.3 秒;降到 20 维后 KNN 精度约 0.986,耗时约 0.1 秒。随机森林那边,64 维精度约 0.94,20 维精度约 0.93,耗时从 1.5 秒降到 0.5 秒左右。

关键结论:KNN 在降维后精度几乎不掉,但耗时明显下降。随机森林本身对维度不敏感,降维收益主要体现在训练时间上。这也解释了为什么在 42000 行、784 列的场景里,降维后换 KNN 是划算的——KNN 的精度上限比随机森林高,只是被计算量拖住了。

4.3 KNN 的 k 值再调一下

降维后特征少了,KNN 的最优 k 值可能和原来不一样。用学习曲线扫一遍:

k_scores = [] for k in range(1, 16): acc = cross_val_score(KNN(n_neighbors=k), X_pca, y, cv=5).mean() k_scores.append(acc) best_k = int(np.argmax(k_scores)) + 1 print('best k =', best_k, 'acc =', max(k_scores))

digits 上通常 k=3 或 k=4 表现最好。注意range从 1 开始,argmax返回下标要加 1。

5. 本篇常见错排查

5.1 PCA 在划分数据前就 fit 了

这是最常见的坑。如果你先对整个 X 做PCA().fit_transform(X),再train_test_split,测试集的信息已经通过主成分方向泄漏到训练过程里了。正确做法是用Pipeline:

from sklearn.pipeline import Pipeline pipe = Pipeline([ ('pca', PCA(n_components=20)), ('knn', KNN()) ]) cross_val_score(pipe, X, y, cv=5).mean()

Pipeline 保证 PCA 只在每折的训练集上 fit。

5.2 n_components 设成浮点数但没理解含义

PCA(n_components=0.95)表示保留 95% 的方差,PCA 会自动算需要多少个主成分。这个写法很方便,但要注意它和整数写法选出来的维度可能不一样。如果你后面要固定维度做对比实验,建议用整数。

5.3 降维后精度反而掉了

如果降维后精度明显下降,先检查 n_components 是不是太小。用第 3 节的累计方差曲线确认一下,保留 95% 方差通常需要多少个主成分。另一个可能是数据没有标准化——PCA 对尺度敏感,如果各特征量纲差异大,先做StandardScaler。digits 的像素值都在 0 到 16 之间,量纲一致,可以跳过标准化,但换成其他数据集要留意。

5.4 交叉验证耗时太长

cross_val_score默认 cv=5,如果数据量大,可以把 cv 降到 3,或者用StratifiedKFold手动控制折数。另外随机森林的n_estimators在调参阶段可以先用 10 到 50,定下来之后再调大。

6. 把流程接到自己的项目里

上面这套流程跑通之后,你会发现 PCA 降维的收益主要来自两个地方:一是让 KNN 这类距离型模型重新变得可用,二是缩短交叉验证和调参的迭代周期。digits 数据集小,差异不明显,但换成 784 维、几万行的数据,降维前后 KNN 的耗时差距会从秒级拉到分钟级。

如果你要把这套流程用到实际项目里,建议把 PCA 和分类器串成 Pipeline,这样交叉验证和网格搜索都不会泄漏信息。模型选型阶段可以用随机森林快速筛主成分数,定下来之后再换 KNN 精调 k 值。

需要长期跑编码任务或者 Agent 工作流的话,可以在 TaoToken 的 Coding Plan 里配置模型调用;如果只是想先验证一下不同模型对降维后数据的表现,模型对话入口更轻量。接入相关的 API Key 和文档在控制台和接入文档里都能找到,按需取用就行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询