降维
机器学习的问题都会涉及到有着几千甚至数百万维的特征的训练实例,这不仅让训练过程变得非常缓慢,同时还很难找到一个很好的解。
维数灾难(curse of dimentionality
)
我们已经习惯生活在一个三维的世界里,以至于在尝试想象更高维的空间时,我们的直觉不管用了。即使是一个基本的 4D
超正方体也很难在我们的脑中想象出来。
在一个平方单位中随机选取两个点,那么这两个点之间的距离平均约为 0.52。
在单位立方体中选取两个随机点,平均距离将大致为 0.66。
在一个 1,000,000 维超立方体中随机抽取两点呢?那么,平均距离,大概为 408.25!这非常违反直觉:当它们都位于同一单位超立方体内时,两点是怎么距离这么远的?
这一事实意味着高维数据集大概率分布的非常稀疏:大多数训练实例可能彼此远离。
当然,这也意味着一个新实例可能远离任何训练实例,这使得预测的可靠性远低于我们处理较低维度数据的预测,因为它们将基于更大的推测(extrapolations)。
简而言之,训练集的维度越高,过拟合的风险就越大。
维数爆炸的一个解决方案是增加训练集的大小从而达到拥有足够密度的训练集。不幸的是,在实践中,达到给定密度所需的训练实例的数量随着维度的数量呈指数增长。如果只有 100 个特征(比 MNIST
问题要少得多)并且假设它们均匀分布在所有维度上,那么如果想要各个临近的训练实例之间的距离在 0.1 以内,您需要的训练实例比宇宙中的原子还要多。
降维的主要方法
投影(Projection)
投影并不总是降维的最佳方法。在很多情况下,子空间可能会扭曲和转动
简单地将数据集投射到一个平面上(例如,直接丢弃 x 3 x3 x3 )会将不同的层,叠在一起(左图)。我们真正想获取的应该是右图。
流形学习
许多降维算法通过对训练实例所在的流形进行建模从而达到降维目的;这叫做流形学习。它依赖于流形猜想(manifold assumption),也被称为流形假设(manifold hypothesis),它认为大多数现实世界的高维数据集大都靠近一个更低维的流形。这种假设经常在实践中被证实。
如果在训练模型之前降低训练集的维数,那训练速度肯定会加快,但并不总是会得出更好的训练效果;这一切都取决于数据集。
如上图,第一行的训练集在流形的较低维空间中表示,变得很简单,分类边界是一条直线。第二行中就变得复杂了。
主成分分析(PCA
)
主成分分析(Principal Component Analysis
)是目前为止最流行的降维算法。首先它找到接近数据集分布的超平面,然后将所有的数据都投影到这个超平面上。
保留差异性
在将训练集投影到较低维超平面之前,您首先需要选择正确的超平面
将二维平面中的训练数据向一维的不同轴上进行投影:
- 将 ( x 1 , x 2 ) (x_1,x_2) (x1,x2),投影到实线轴、大虚线轴,小虚线轴的结果,如右图,
- 在实线轴上的投影保留了最大的差异性(方差最大)。
- 选择保留最大差异性的轴是比较合理的,因为它比其他两种投影丢失的信息更少。
从另外一个角度说,实线上的投影和原始数据之间的均方距离最小。这正是**PCA
背后的基本思想**。
PCA
寻找训练集中可获得最大方差的轴: C 1 C_1 C1,就是那条实线。- 同时还发现了一个与 C 1 C_1 C1正交的第二个轴: C 2 C_2 C2,选择它可以获得最大残差。
主要成分–PC(Principle Componets)
扩展到多维:
- 定义第
i
i
i 个轴的单位矢量被称为第
i
i
i 个主成分(
PC
), 即:第 i i i 个PC
。
如图:前两个 PC
用平面中的正交箭头表示,第3个 PC
与上述 两个 PC
形成的平面正交(指向为上方或下方)。
主成分的方向不稳定:如果您稍微打乱一下训练集并再次运行 PCA
,则某些新 PC
可能会指向与原始 PC
方向相反。但是,它们通常仍位于同一轴线上。在某些情况下,一对 PC
甚至可能会旋转或交换,但它们定义的平面通常保持不变。
如何找到训练集的PC
?
奇异值分解(SVD
):可以将训练集矩阵
X
X
X 分解为三个矩阵
U
⋅
Σ
⋅
V
T
U·Σ·V^T
U⋅Σ⋅VT 的点积,其中
V
T
V^T
VT 包含我们想要的所有主成分(PC):
V
T
=
(
∣
∣
∣
C
1
C
2
⋯
C
n
∣
∣
∣
)
V^T=\begin{pmatrix} | & | & |\\ C_1 & C_2 & \cdots C_n \\ | & | & |\end{pmatrix}
VT=⎝⎛∣C1∣∣C2∣∣⋯Cn∣⎠⎞
实例代码:使用Numpy.svd()
np.random.seed(4)
m = 60
w1, w2 = 0.1, 0.3
noise = 0.1
angles = np.random.rand(m) * 3 * np.pi / 2 - 0.5
# 60个弧度值的角 rand*3*pi/2-0.5
X = np.empty((m, 3)) # 60*3
X[:, 0] = np.cos(angles) + np.sin(angles)/2 + noise * np.random.randn(m) / 2
# cos(angles)+sin(angles)/2 + 高斯噪声 第0列
X[:, 1] = np.sin(angles) * 0.7 + noise * np.random.randn(m) / 2
# sin(angles)*0.7+高斯噪声/2 第1列
X[:, 2] = X[:, 0] * w1 + X[:, 1] * w2 + noise * np.random.randn(m)
# 第2列, x0*w1 + x1*w2 + 高斯噪声。
X_centered = X - X.mean(axis=0) # PCA假定数据集以原点为中心,所以要对数据集中心化!
U, s, Vt = np.linalg.svd(X_centered)
# 使用numpy的svd()函数,求Vt
c1 = Vt.T[:, 0] #
c2 = Vt.T[:, 1] #只拿c1,c2,降维~
示例代码:使用SKlearn
from sklearn.decomposition import PCA # 分解库 decomposition
pca = PCA(n_components=2) # 使用前两个主要成分PC
X2D = pca.fit_transform(X) # 降维成2维矩阵(使用超平面,保留差异性)
投影到d维空间
一旦确定了所有的主成分(PC
),就可以通过将数据集投影到由前 d
个主成分构成的超平面上,从而将数据集的维数降至d
维。
训练集
X
X
X 投影到
d
d
d维空间 形成降维后新的数据集,公式:
X
d
=
X
⋅
W
d
W
d
:
包
含
前
d
个
主
成
分
的
矩
阵
X_d=X\cdot W_d \\ W_d: 包含前d个主成分的矩阵
Xd=X⋅WdWd:包含前d个主成分的矩阵
W2 = Vt.T[:, :2] #使用前两个主要成分PC
X2D = X_centered.dot(W2) # 降维成2维矩阵(使用超平面,保留差异性)
可解释方差比:
pca.explained_variance_ratio_
可通过explained_variance_ratio_
变量获得。它表示位于每个主成分轴上的数据集方差的比例
pca.explained_variance_ratio_
# array([0.84248607, 0.14631839])
#这表明,84.2% 的数据集方差位于第一轴,14.6% 的方差位于第二轴。第三轴的这一比例不到 1.2%,因此可以认为它可能没有包含什么信息。
选择正确的维度
不能任意选择要降低到的维度数量。通常我们倾向于选择加起来到方差解释率能够达到足够占比(例如 95%)的维度的数量。
除非,要为数据可视化而降低维度 ,在这种情况下,通常将维度降低到 2 或 3。
代码示例:
pca = PCA()
pca.fit(X_train)
cumsum = np.cumsum(pca.explained_variance_ratio_)
d = np.argmax(cumsum >= 0.95) + 1
#在不降维的情况下进行 PCA,然后计算出保留训练集方差 95% 所需的最小维数
#更简洁的方法:
pca = PCA(n_components=0.95)
#设置为 0.0 到 1.0 之间的浮点数,表明您希望保留的方差比率
X_reduced = pca.fit_transform(X_train)
#PCA应用于MNIST数据集, d=154
PCA
压缩
在降维之后,训练集占用的空间要少得多。
如上,将PCA
应用于 MNIST
数据集,同时保留 95% 的方差。你应该发现每个实例只有 154 多个特征,而不是原来的 784 个特征。因此,尽管大部分方差都保留下来,但数据集现在还不到其原始大小的 20%!
通过应用 PCA
投影的逆变换,也可以将缩小的数据集解压缩回 784 维。当然这并不会返回给你最原始的数据,因为投影丢失了一些信息(在 5%的方差内),但它可能非常接近原始数据
PCA
逆变换公式(回退到原始数据维度)
X
r
e
c
o
v
e
r
e
d
=
X
d
⋅
W
d
T
X_{recovered}=X_d \cdot W_d^T
Xrecovered=Xd⋅WdT
示例代码:
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
mnist.target = mnist.target.astype(np.uint8)
X = mnist["data"]
y = mnist["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y)
pca = PCA(n_components=0.95)
X_reduced = pca.fit_transform(X_train)
print(pca.n_components_)
#154
X_recovered = pca.inverse_transform(X_reduced) #逆变换
def plot_digits(instances, images_per_row=5, **options):
size = 28
images_per_row = min(len(instances), images_per_row)
images = [instance.reshape(size,size) for instance in instances]
n_rows = (len(instances) - 1) // images_per_row + 1
row_images = []
n_empty = n_rows * images_per_row - len(instances)
images.append(np.zeros((size, size * n_empty)))
for row in range(n_rows):
rimages = images[row * images_per_row : (row + 1) * images_per_row]
row_images.append(np.concatenate(rimages, axis=1))
image = np.concatenate(row_images, axis=0)
plt.imshow(image, cmap = mpl.cm.binary, **options)
plt.axis("off")
plt.figure(figsize=(7, 4))
plt.subplot(121)
plot_digits(X_train[::2100]) #每隔2100行取1行 52500/2100 =25
plt.title("Original", fontsize=16)
plt.subplot(122)
plot_digits(X_recovered[::2100])
plt.title("Compressed", fontsize=16)
增量PCA
(Incremental PCA)
应用PCA
的一个问题是它需要在内存中处理整个训练集以便SVD
算法运行。幸运的是,我们已经开发了增量 PCA
(IPCA
)算法:
可以将训练集分批,并一次只对一个批量使用IPCA
算法。这对大型训练集非常有用,并且可以在线应用 PCA
(即在新实例到达时即时运行)
示例代码:
from sklearn.decomposition import IncrementalPCA
# 引入增量PCA,即:IPCA
n_batches = 100
# 分成 100 个小批量(使用 NumPy 的array_split()函数)
inc_pca = IncrementalPCA(n_components=154)
for X_batch in np.array_split(X_train, n_batches):
print(".", end="")
inc_pca.partial_fit(X_batch)
# 注意,您必须对每个最小批次调用partial_fit()方法,而不是对整个训练集使用fit()方法
X_reduced = inc_pca.transform(X_train)
X_recovered_inc_pca = inc_pca.inverse_transform(X_reduced) # 逆变换
内核PCA
把核技巧应用于PCA
,从而可以执行复杂的非线性投影来降低维度。这就是所谓的核 PCA(KernelPCA
)。
它通常能够很好地保留投影后的簇,有时甚至可以展开分布近似于扭曲流形的数据集。
如图展示了使用线性核(等同于简单的使用 PCA
类),RBF
核,sigmoid
核将瑞士卷降到 2 维。
代码示例:
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=42)
from sklearn.decomposition import KernelPCA
# 引入KernelPCA
lin_pca = KernelPCA(n_components = 2, kernel="linear", fit_inverse_transform=True)
rbf_pca = KernelPCA(n_components = 2, kernel="rbf", gamma=0.0433, fit_inverse_transform=True)
sig_pca = KernelPCA(n_components = 2, kernel="sigmoid", gamma=0.001, coef0=1, fit_inverse_transform=True)
y = t > 6.9
plt.figure(figsize=(11, 4))
# 遍历元组,PCA后画图
for subplot, pca, title in ((131, lin_pca, "Linear kernel"), (132, rbf_pca, "RBF kernel, $\gamma=0.04$"), (133, sig_pca, "Sigmoid kernel, $\gamma=10^{-3}, r=1$")):
X_reduced = pca.fit_transform(X)
if subplot == 132:
X_reduced_rbf = X_reduced
plt.subplot(subplot)
#plt.plot(X_reduced[y, 0], X_reduced[y, 1], "gs")
#plt.plot(X_reduced[~y, 0], X_reduced[~y, 1], "y^")
plt.title(title, fontsize=14)
plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=t, cmap=plt.cm.hot)
plt.xlabel("$z_1$", fontsize=18)
if subplot == 131:
plt.ylabel("$z_2$", fontsize=18, rotation=0)
plt.grid(True)
plt.show()
选择一种核并调整超参数
由于KernelPCA
是无监督学习算法,因此没有明显的性能指标可以帮助您选择最佳的核方法和超参数值。但是,降维通常是监督学习任务(例如分类)的准备步骤,因此您可以简单地使用网格搜索来选择可以让该任务达到最佳表现的核方法和超参数。
一种方法如代码:
from sklearn.model_selection import GridSearchCV #引入网格搜索
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
clf = Pipeline([
("kpca", KernelPCA(n_components=2)),
("log_reg", LogisticRegression(solver="lbfgs"))
])
param_grid = [{
"kpca__gamma": np.linspace(0.03, 0.05, 10),
"kpca__kernel": ["rbf", "sigmoid"]
}]
grid_search = GridSearchCV(clf, param_grid, cv=3)
grid_search.fit(X, y)
print(grid_search.best_params_)
#{'kpca__gamma': 0.043333333333333335, 'kpca__kernel': 'rbf'}
#创建了一个两步的流水线,首先使用 kPCA 将维度降至两维,然后应用 Logistic 回归进行分类。
#然后它使用Grid SearchCV为 kPCA 找到最佳的核和gamma值,以便在最后获得最佳的分类准确性
另一种方法:非监督的方法,选择产生最低重建误差的核和超参数。但是,重建并不像线性 PCA
那样容易。
局部线性嵌入(LLE)
(Locally Linear Embedding)
是另一种非常有效的非线性降维(NLDR
)方法。这是一种流形学习技术,不依赖投影。
-
LLE
首先测量每个训练实例与其最近邻(c.n.
)之间的线性关系, -
然后寻找能最好地保留这些局部关系的训练集的低维表示 。
-
这使得它特别擅长展开扭曲的流形,尤其是在没有太多噪音的情况下。
示例代码:
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=41)
from sklearn.manifold import LocallyLinearEmbedding
#用流行学习的方法降维:引入LLE,来降维瑞士卷
lle = LocallyLinearEmbedding(n_components=2, n_neighbors=10, random_state=42)
X_reduced = lle.fit_transform(X)
# 绘图
plt.title("Unrolled swiss roll using LLE", fontsize=14)
plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=t, cmap=plt.cm.hot)
plt.xlabel("$z_1$", fontsize=18)
plt.ylabel("$z_2$", fontsize=18)
plt.axis([-0.065, 0.055, -0.1, 0.12])
plt.grid(True)
plt.show()
其他降维方法
有很多其他的降维方法,Scikit-Learn
支持其中的好几种。这里是其中最流行的:
- 随机投影: 将
PCA
的超参数设置为svd_solver="randomized"
,很疯狂。 - 多维缩放(
MDS
):尝试保持实例之间距离的同时降低了维度 Isomap
:通过将每个实例连接到最近的邻居来创建图形,然后在尝试保持实例之间的测地距离时降低维度- t-分布随机邻域嵌入(
t-SNE
):用于降低维度,同时试图保持相似的实例临近并将不相似的实例分开。它主要用于可视化,尤其是用于可视化高维空间中的实例 - 线性判别分析(
LDA
):实际上是一种分类算法,但在训练过程中,它会学习类之间最有区别的轴,然后使用这些轴来定义用于投影数据的超平面。LDA
的好处是投影会尽可能地保持各个类之间距离,所以在运行另一种分类算法(如SVM
分类器)之前,LDA
是很好的降维技术。
===