很多机器学习的问题都会涉及到有着几千甚至数百万维的特征的训练实例。这不仅让训练过程变得非常缓慢,同时还很难找到一个很好的解,我们接下来就会遇到这种情况。这种问题通常被称为维数灾难
(curse of dimentionality
)。
幸运的是,在现实生活中我们经常可以极大的降低特征维度,将一个十分棘手的问题转变成一个可以较为容易解决的问题。例如,对于 MNIST 图片集(第 3 章中提到):图片四周边缘部分的像素几乎总是白的,因此你完全可以将这些像素从你的训练集中扔掉而不会丢失太多信息
。图 7-6 向我们证实了这些像素的确对我们的分类任务是完全不重要的。同时,两个相邻的像素往往是高度相关的:如果你想要将他们合并成一个像素(比如取这两个像素点的平均值)你并不会丢失很多信息
。
警告:降维肯定会丢失一些信息(这就好比将一个图片压缩成 JPEG 的格式会降低图像的质量),因此即使这种方法可以加快训练的速度,同时也会让你的系统表现的稍微差一点。
降维会让你的工作流水线更复杂因而更难维护
。所有你应该先尝试使用原始的数据来训练,如果训练速度太慢的话再考虑使用降维
。在某些情况下,降低训练集数据的维度可能会筛选掉一些噪音和不必要的细节,这可能会让你的结果比降维之前更好(这种情况通常不会发生;它只会加快你训练的速度)
。
降维除了可以加快训练速度外,在数据可视化方面(或者 DataViz
)也十分有用。降低特征维度到 2
(或者 3
)维从而可以在图中画出一个高维度的训练集,让我们可以通过视觉直观的发现一些非常重要的信息,比如聚类
。
在这一章里,我们将会讨论维数灾难问题并且了解在高维空间的数据
。然后,我们将会展示
两种主要的降维方法:投影(projection)和流形学习(Manifold Learning)
.
三种流行的降维技术:主成分分析(PCA)
,核主成分分析(Kernel PCA)
和局部线性嵌入(LLE)
。
1 维数灾难
我们已经习惯生活在一个三维的世界里,以至于当我们尝试想象更高维的空间时,我们的直觉不管用了。即使是一个基本的 4D 超正方体也很难在我们的脑中想象出来(见图 8-1),更不用说一个 200 维的椭球弯曲在一个 1000 维的空间里了。
图 8-1 点,线,方形,立方体和超正方体(0D 到 4D 超正方体)这表明很多物体在高维空间表现的十分不同。比如,如果你在一个正方形单元中随机取一个点(一个1×1
的正方形),那么随机选的点离所有边界大于 0.001
(靠近中间位置)的概率为 0.4%
(1 - 0.998^2
)(换句话说,一个随机产生的点不大可能严格落在某一个维度上。但是在一个 1,0000 维的单位超正方体(一个1×1×...×1
的立方体,有 10,000
个 1
),这种可能性超过了 99.999999%
。在高维超正方体中,大多数点都分布在边界处。
还有一个更麻烦的区别:如果你在一个平方单位中随机选取两个点,那么这两个点之间的距离平均约为 0.52
。如果您在单位 3D
立方体中选取两个随机点,平均距离将大致为 0.66
。但是,在一个 1,000,000
维超立方体中随机抽取两点呢?那么,平均距离,信不信由你,大概为 408.25
=
这非常违反直觉:当它们都位于同一单元超立方体内时,两点是怎么距离这么远的?这一事实意味着高维数据集有很大风险分布的非常稀疏
:大多数训练实例可能彼此远离
。当然,这也意味着一个新实例可能远离任何训练实例,这使得预测的可靠性远低于我们处理较低维度数据的预测
,因为它们将基于更大的推测(extrapolations
)。简而言之,训练集的维度越高,过拟合的风险就越大
。
理论上来说,维数爆炸的一个解决方案是增加训练集的大小从而达到拥有足够密度的训练集
。不幸的是,在实践中,达到给定密度所需的训练实例的数量随着维度的数量呈指数增长
。如果只有 100
个特征(比 MNIST
问题要少得多)并且假设它们均匀分布在所有维度上,那么如果想要各个临近的训练实例之间的距离在 0.1
以内,您需要比宇宙中的原子还要多的训练实例。
2 降维的主要方法
在我们深入研究具体的降维算法之前,我们来看看降低维度的两种主要方法:投影和流形学习。
2.1 投影(Projection
)
在大多数现实生活的问题中,训练实例并不是在所有维度上均匀分布的
。许多特征几乎是常数,而其他特征则高度相关(如前面讨论的 MNIST)
。结果,所有训练实例实际上位于(或接近)高维空间的低维子空间内
。这听起来有些抽象,所以我们不妨来看一个例子。在图 8-2 中,您可以看到由圆圈表示的 3D 数据集。
# 建立3D数据集
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
X = np.empty((m, 3))
X[:, 0] = np.cos(angles) + np.sin(angles)/2 + noise * np.random.randn(m) / 2
X[:, 1] = np.sin(angles) * 0.7 + noise * np.random.randn(m) / 2
X[:, 2] = X[:, 0] * w1 + X[:, 1] * w2 + noise * np.random.randn(m)
# PCA using SVD decomposition
X_centered = X - X.mean(axis=0)
U, s, Vt = np.linalg.svd(X_centered)
c1 = Vt.T[:, 0]
c2 = Vt.T[:, 1]
m, n = X.shape
S = np.zeros(X_centered.shape)
S[:n, :n] = np.diag(s)
np.allclose(X_centered, U.dot(S).dot(Vt))
W2 = Vt.T[:, :2]
X2D = X_centered.dot(W2)
X2D_using_svd = X2D
# PCA using Scikit-Learn:使用Scikit-Learn,PCA非常简单
from sklearn.decomposition import PCA
pca = PCA(n_components = 2)
X2D = pca.fit_transform(X)
X2D[:5]
X2D_using_svd[:5]
np.allclose(X2D, -X2D_using_svd)
X3D_inv = pca.inverse_transform(X2D)
np.allclose(X3D_inv, X)
np.mean(np.sum(np.square(X3D_inv - X), axis=1))
X3D_inv_using_svd = X2D_using_svd.dot(Vt[:2, :])
np.allclose(X3D_inv_using_svd, X3D_inv - pca.mean_)
pca.components_
Vt[:2]
pca.explained_variance_ratio_
1 - pca.explained_variance_ratio_.sum()
np.square(s) / np.square(s).sum()
# 用于绘制3D箭头的实用程序类
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import proj3d
class Arrow3D(FancyArrowPatch):
def __init__(self, xs, ys, zs, *args, **kwargs):
FancyArrowPatch.__init__(self, (0,0), (0,0), *args, **kwargs)
self._verts3d = xs, ys, zs
def draw(self, renderer):
xs3d, ys3d, zs3d = self._verts3d
xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M)
self.set_positions((xs[0],ys[0]),(xs[1],ys[1]))
FancyArrowPatch.draw(self, renderer)
# dataset
axes = [-1.8, 1.8, -1.3, 1.3, -1.0, 1.0]
x1s = np.linspace(axes[0], axes[1], 10)
x2s = np.linspace(axes[2], axes[3], 10)
x1, x2 = np.meshgrid(x1s, x2s)
C = pca.components_
R = C.T.dot(C)
z = (R[0, 2] * x1 + R[1, 2] * x2) / (1 - R[2, 2])
# dataset_3d_plot
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(6, 3.8))
ax = fig.add_subplot(111, projection='3d')
X3D_above = X[X[:, 2] > X3D_inv[:, 2]]
X3D_below = X[X[:, 2] <= X3D_inv[:, 2]]
ax.plot(X3D_below[:, 0], X3D_below[:, 1], X3D_below[:, 2], "bo", alpha=0.5)
ax.plot_surface(x1, x2, z, alpha=0.2, color="k")
np.linalg.norm(C, axis=0)
ax.add_artist(Arrow3D([0, C[0, 0]],[0, C[0, 1]],[0, C[0, 2]], mutation_scale=15, lw=1, arrowstyle="-|>", color="k"))
ax.add_artist(Arrow3D([0, C[1, 0]],[0, C[1, 1]],[0, C[1, 2]], mutation_scale=15, lw=1, arrowstyle="-|>", color="k"))
ax.plot([0], [0], [0], "k.")
for i in range(m):
if X[i, 2] > X3D_inv[i, 2]:
ax.plot([X[i][0], X3D_inv[i][0]], [X[i][1], X3D_inv[i][1]], [X[i][2], X3D_inv[i][2]], "k-")
else:
ax.plot([X[i][0], X3D_inv[i][0]], [X[i][1], X3D_inv[i][1]], [X[i][2], X3D_inv[i][2]], "k-", color="#505050")
ax.plot(X3D_inv[:, 0], X3D_inv[:, 1], X3D_inv[:, 2], "k+")
ax.plot(X3D_inv[:, 0], X3D_inv[:, 1], X3D_inv[:, 2], "k.")
ax.plot(X3D_above[:, 0], X3D_above[:, 1], X3D_above[:, 2], "bo")
ax.set_xlabel("$x_1$", fontsize=18)
ax.set_ylabel("$x_2$", fontsize=18)
ax.set_zlabel("$x_3$", fontsize=18)
ax.set_xlim(axes[0:2])
ax.set_ylim(axes[2:4])
ax.set_zlim(axes[4:6])
plt.show()
图 8-2 一个分布接近于2D子空间的3D数据集
注意到所有训练实例的分布都贴近一个平面:这是高维(3D)空间的较低维(2D)子空间
。现在,如果我们将每个训练实例垂直投影到这个子空间上
(就像将短线连接到平面的点所表示的那样
),我们就可以得到如图8-3所示的新2D数据集
。铛铛铛!我们刚刚将数据集的维度从 3D 降低到了 2D。请注意,坐标轴对应于新的特征z1
和z2
(平面上投影的坐标)。
# dataset_2d_plot
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
ax.plot(X2D[:, 0], X2D[:, 1], "k+")
ax.plot(X2D[:, 0], X2D[:, 1], "k.")
ax.plot([0], [0], "ko")
ax.arrow(0, 0, 0, 1, head_width=0.05, length_includes_head=True, head_length=0.1, fc='k', ec='k')
ax.arrow(0, 0, 1, 0, head_width=0.05, length_includes_head=True, head_length=0.1, fc='k', ec='k')
ax.set_xlabel("$z_1$", fontsize=18)
ax.set_ylabel("$z_2$", fontsize=18, rotation=0)
ax.axis([-1.5, 1.3, -1.2, 1.2])
ax.grid(True)
图 8-3 一个经过投影后的新的 2D 数据集
但是,投影并不总是降维的最佳方法。在很多情况下,子空间可能会扭曲和转动
,比如图 8-4 所示的着名瑞士滚动玩具数据集。
# 空间可能会扭曲和转动
from sklearn.datasets import make_swiss_roll
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=42)
# swiss_roll_plot
axes = [-11.5, 14, -2, 23, -12, 15]
fig = plt.figure(figsize=(6, 5))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=t, cmap=plt.cm.hot)
ax.view_init(10, -70)
ax.set_xlabel("$x_1$", fontsize=18)
ax.set_ylabel("$x_2$", fontsize=18)
ax.set_zlabel("$x_3$", fontsize=18)
ax.set_xlim(axes[0:2])
ax.set_ylim(axes[2:4])
ax.set_zlim(axes[4:6])
plt.show()
图 8-4 瑞士滚动数玩具数据集
简单地将数据集投射到一个平面上(例如,直接丢弃
x3)会将瑞士卷的不同层叠在一起
,如图 8-5 左侧所示。但是,你真正想要的是展开瑞士卷所获取到的类似图 8-5 右侧的 2D 数据集。
# squished_swiss_roll_plot--不同层叠在一起
plt.figure(figsize=(11, 4))
plt.subplot(121)
plt.scatter(X[:, 0], X[:, 1], c=t, cmap=plt.cm.hot)
plt.axis(axes[:4])
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$x_2$", fontsize=18, rotation=0)
plt.grid(True)
plt.subplot(122)
plt.scatter(t, X[:, 1], c=t, cmap=plt.cm.hot)
plt.axis([4, 15, axes[2], axes[3]])
plt.xlabel("$z_1$", fontsize=18)
plt.grid(True)
plt.show()
图 8-5 投射到平面的压缩(左)vs 展开瑞士卷(右)
2.2 流形学习
瑞士卷一个是二维流形的例子。简而言之,二维流形是一种二维形状,它可以在更高维空间中弯曲或扭曲。更一般地,一个d
维流形是类似于d
维超平面的n
维空间(其中d < n
)的一部分。在我们瑞士卷这个例子中,d = 2
,n = 3
:它有些像 2D 平面,但是它实际上是在第三维中卷曲
。
许多降维算法通过对训练实例所在的流形进行建模从而达到降维目的
;这叫做流形学习
。它依赖于流形猜想(manifold assumption),也被称为流形假设(manifold hypothesis)
,它认为大多数现实世界的高维数据集大都靠近一个更低维的流形
。这种假设经常在实践中被证实。
让我们再回到 MNIST 数据集:所有手写数字图像都有一些相似之处。它们由连线组成,边界是白色的,大多是在图片中中间的,等等
。如果你随机生成图像,只有一小部分看起来像手写数字。换句话说,如果您尝试创建数字图像,那么您的自由度远低于您生成任何随便一个图像时的自由度
。这些约束往往会将数据集压缩到较低维流形中
。
流形假设(manifold hypothesis)
通常包含着另一个隐含的假设:你现在的手上的工作(例如分类或回归)如果在流形的较低维空间中表示,那么它们会变得更简单
。例如,在图 8-6 的第一行中,瑞士卷被分为两类:在三维空间中(图左上),分类边界会相当复杂,但在二维展开的流形空间中(图右上),分类边界是一条简单的直线。
但是,这个假设并不总是成立。例如,在图 8-6 的最下面一行,决策边界位于x1 = 5
(图左下)。这个决策边界在原始三维空间(一个垂直平面)看起来非常简单,但在展开的流形中却变得更复杂了(四个独立线段的集合)(图右下)。
简而言之,如果在训练模型之前降低训练集的维数,那训练速度肯定会加快,但并不总是会得出更好的训练效果;这一切都取决于数据集
。
希望你现在对于维数爆炸以及降维算法如何解决这个问题有了一定的理解,特别是对流形假设提出的内容。本章的其余部分将介绍一些最流行的降维算法。
图 8-6 决策边界并不总是会在低维空间中变的简单
3 主成分分析(PCA)
主成分分析(Principal Component Analysis)是目前为止最流行的降维算法。首先它找到接近数据集分布的超平面,然后将所有的数据都投影到这个超平面上。
3.1 保留(最大)方差
在将训练集投影到较低维超平面之前,您首先需要选择正确的超平面
。例如图 8-7 左侧是一个简单的二维数据集,以及三个不同的轴(即一维超平面)。图右边是将数据集投影到每个轴上的结果。正如你所看到的,投影到实线上保留了最大方差,而在点线上的投影只保留了非常小的方差,投影到虚线上保留的方差则处于上述两者之间
。
图 8-7 选择投射到哪一个子空间
选择保持最大方差的轴看起来是合理的,因为它很可能比其他投影损失更少的信息
。证明这种选择的另一种方法是,选择这个轴使得将原始数据集投影到该轴上的均方距离最小
。这是就 PCA 背后的思想,相当简单。
3.2 主成分(Principle Componets)
PCA 寻找训练集中可获得最大方差的轴。在图 8-7 中,它是一条实线。它还发现了一个与第一个轴正交的第二个轴,选择它可以获得最大的残差。在这个 2D 例子中,没有选择:就只有这条点线。但如果在一个更高维的数据集中,PCA 也可以找到与前两个轴正交的第三个轴,以及与数据集中维数相同的第四个轴,第五个轴等。
定义第i
个轴的单位矢量被称为第i
个主成分(PC)。在图 8-7 中,第一个 PC 是c1
,第二个 PC 是c2
。在图 8-2 中,前两个 PC 用平面中的正交箭头表示,第三个 PC 与上述 PC 形成的平面正交(指向上或下)。
概述: 主成分的方向不稳定:如果您稍微打乱一下训练集并再次运行
PCA
,则某些新PC
可能会指向与原始PC
方向相反。但是,它们通常仍位于同一轴线上。在某些情况下,一对 PC 甚至可能会旋转或交换,但它们定义的平面通常保持不变。
那么如何找到训练集的主成分呢?
幸运的是,有一种称为奇异值分解(SVD)的标准矩阵分解技术
,可以将训练集矩阵X
分解为三个矩阵U·Σ·V^T
的点积,其中V^T
包含我们想要的所有主成分,如公式 8-1 所示。
公式 8-1 主成分矩阵
下面的 Python
代码使用了 Numpy
提供的svd()
函数获得训练集的所有主成分,然后提取前两个 PC
:
X_centered=X-X.mean(axis=0)
U,s,V=np.linalg.svd(X_centered)
c1=V.T[:,0]
c2=V.T[:,1]
array([0.93636116, 0.29854881, 0.18465208])
array([0.93636116, 0.29854881, 0.18465208])
警告:
PCA 假定数据集以原点为中心
。正如我们将看到的,Scikit-Learn
的PCA
类负责为您的数据集中心化处理
。但是,如果您自己实现PCA
(如前面的示例所示),或者如果您使用其他库,不要忘记首先要先对数据做中心化处理。
3.3 投影到d
维空间
一旦确定了所有的主成分,你就可以通过将数据集投影到由前d
个主成分构成的超平面上,从而将数据集的维数降至d
维。选择这个超平面可以确保投影将保留尽可能多的方差
。例如,在图 8-2 中,3D 数据集被投影到由前两个主成分定义的 2D 平面,保留了大部分数据集的方差
。因此,2D 投影看起来非常像原始 3D 数据集
。
为了将训练集投影到超平面上,可以简单地通过计算训练集矩阵
X和
Wd的点积
,Wd
定义为包含前d
个主成分的矩阵(即由V^T
的前d
列组成的矩阵),如公式 8-2 所示。
公式 8-2 将训练集投影到d
维空间
下面的 Python
代码将训练集投影到由前两个主成分定义的超平面上
:
W2=V.T[:,:2]
X2D=X_centered.dot(W2)
好了你已经知道这个东西了!你现在已经知道如何给任何一个数据集降维而又能尽可能的保留原数据集的方差了。
3.4 使用 Scikit-Learn
Scikit-Learn
的 PCA
类使用 SVD
分解来实现,就像我们之前做的那样。以下代码应用 PCA
将数据集的维度降至两维(请注意,它会自动处理数据的中心化
):
from sklearn.decomposition import PCA
pca=PCA(n_components=2)
X2D=pca.fit_transform(X)
将 PCA
转化器应用于数据集后,可以使用components_
访问每一个主成分(注意,它返回以 PC 作为水平向量的矩阵,因此,如果我们想要获得第一个主成分则可以写成)
pca.components_.T[:,0]
array([-0.93636116, -0.29854881, -0.18465208])
3.5 方差解释率(Explained Variance Ratio)
另一个非常有用的信息是每个主成分的方差解释率,可通过explained_variance_ratio_
变量获得。它表示位于每个主成分轴上的数据集方差的比例
。例如,让我们看一下图 8-2 中表示的三维数据集前两个分量的方差解释率
:
print(pca.explained_variance_ratio_)
[0.84248607 0.14631839]
这表明,84.2%
的数据集方差位于第一轴,14.6%
的方差位于第二轴。第三轴的这一比例不到1.2%
,因此可以认为它可能没有包含什么信息。
3.6 选择正确的维度
通常我们倾向于选择加起来到方差解释率能够达到足够占比(例如
95%)的维度的数量,而不是任意选择要降低到的维度数量
。当然,除非您正在为数据可视化而降低维度 -- 在这种情况下,您通常希望将维度降低到 2 或 3。
下面的代码在不降维的情况下进行 PCA,然后计算出保留训练集方差 95%
所需的最小维数
:
from scipy.io import loadmat
mnist = loadmat('./datasets/mnist-original.mat')
X,y = mnist['data'].T,mnist['label'].T
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y)
pca = PCA()
pca.fit(X_train)
cumsum = np.cumsum(pca.explained_variance_ratio_)
d = np.argmax(cumsum >= 0.95) + 1
d
154
你可以设置n_components = d
并再次运行 PCA
。但是,有一个更好的选择:不指定你想要保留的主成分个数,而是将
n_components设置为 0.0 到 1.0 之间的浮点数,表明您希望保留的方差比率
:
pca = PCA(n_components=0.95)
X_reduced = pca.fit_transform(X_train)
pca.n_components_
154
另一种选择是画出方差解释率关于维数的函数
(简单地绘制cumsum
;参见图 8-8)。曲线中通常会有一个肘部,方差解释率停止快速增长。您可以将其视为数据集的真正的维度。在这种情况下,您可以看到将维度降低到大约100个维度不会失去太多的可解释方差。
(肘部分析法类似于聚类中确定维数的方法)
图 8-8 可解释方差关于维数的函数
3.7 PCA 压缩
显然,在降维之后,训练集占用的空间要少得多
。例如,尝试将PCA
应用于 MNIST
数据集,同时保留 95%
的方差。你应该发现每个实例只有 150
多个特征,而不是原来的 784
个特征。因此,尽管大部分方差都保留下来,但数据集现在还不到其原始大小的 20%
!这是一个合理的压缩比率,您可以看到这可以如何极大地加快分类算法(如 SVM
分类器)的速度。
通过应用 PCA
投影的逆变换,也可以将缩小的数据集解压缩回 784
维。当然这并不会返回给你最原始的数据,因为投影丢失了一些信息(在5%
的方差内),但它可能非常接近原始数据。原始数据和重构数据之间的均方距离(压缩然后解压缩)被称为重构误差(reconstruction error)
。例如,下面的代码将 MNIST
数据集压缩到 154
维,然后使用inverse_transform()
方法将其解压缩回 784
维。图 8-9 显示了原始训练集(左侧)的几位数字在压缩并解压缩后(右侧)的对应数字。您可以看到有轻微的图像质量降低,但数字仍然大部分完好无损。
pca=PCA(n_components=154)
X_mnist_reduced=pca.fit_transform(X_mnist)
X_mnist_recovered=pca.inverse_transform(X_mnist_reduced)
图 8-9 MNIST 保留 95 方差的压缩
逆变换的公式如公式 8-3 所示
公式 8-3 PCA逆变换,回退到原来的数据维度
3.8 增量 PCA(Incremental PCA)
先前 PCA
实现的一个问题是它需要在内存中处理整个训练集以便 SVD 算法运行
。幸运的是,我们已经开发了增量 PCA
(IPCA
)算法:您可以将训练集分批,并一次只对一个批量使用IPCA
算法。这对大型训练集非常有用,并且可以在线应用 PCA
(即在新实例到达时即时运行)。
下面的代码将 MNIST
数据集分成 100
个小批量(使用 NumPy 的array_split()
函数),并将它们提供给 Scikit-Learn 的IncrementalPCA
类,以将 MNIST
数据集的维度降低到 154
维(就像以前一样)。请注意,您必须对每个最小批次调用partial_fit()
方法,而不是对整个训练集使用fit()
方法:
from sklearn.decomposition import IncrementalPCA
n_batches = 100
inc_pca = IncrementalPCA(n_components=154)
for X_batch in np.array_split(X_train, n_batches):
print(".", end="") # not shown in the book
inc_pca.partial_fit(X_batch)
X_reduced = inc_pca.transform(X_train)
X_recovered_inc_pca = inc_pca.inverse_transform(X_reduced)
plt.figure(figsize=(7, 4))
plt.subplot(121)
plot_digits(X_train[::2100])
plt.subplot(122)
plot_digits(X_recovered_inc_pca[::2100])
plt.tight_layout()
或者,您可以使用 NumPy 的memmap
类,它允许您操作存储在磁盘上二进制文件中的大型数组,就好像它完全在内存中;该类仅在需要时加载内存中所需的数据。由于增量 PCA
类在任何时间内仅使用数组的一小部分,因此内存使用量仍受到控制。这可以调用通常的fit()
方法,如下面的代码所示:
X_mm=np.memmap(filename,dtype='float32',mode='readonly',shape=(m,n))
batch_size=m//n_batches
inc_pca=IncrementalPCA(n_components=154,batch_size=batch_size)
inc_pca.fit(X_mm)
3.9 随机 PCA(Randomized PCA)
Scikit-Learn 提供了另一种执行 PCA 的选择,称为随机 PCA。这是一种随机算法,可以快速找到前d个主成分的近似值。它的计算复杂度是O(m × d^2) + O(d^3)
,而不是O(m × n^2) + O(n^3)
,所以当d
远小于n
时,它比之前的算法快得多。
rnd_pca=PCA(n_components=154,svd_solver='randomized')
X_reduced=rnd_pca.fit_transform(X_mnist)
4 核 PCA(Kernel PCA)
在第 5 章中,我们讨论了核技巧,一种将实例隐式映射到非常高维空间(称为特征空间)的数学技术,让支持向量机可以应用于非线性分类和回归
。回想一下,高维特征空间中的线性决策边界对应于原始空间中的复杂非线性决策边界
。
事实证明,同样的技巧可以应用于 PCA
,从而可以执行复杂的非线性投影来降低维度
。这就是所谓的核 PCA(kPCA)
。它通常能够很好地保留投影后的簇
,有时甚至可以展开分布近似于扭曲流形的数据集
。
例如,下面的代码使用 Scikit-Learn 的KernelPCA
类来执行带有 RBF
核的 kPCA
(有关 RBF
核和其他核的更多详细信息,请参阅第 5 章):
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=42)
from sklearn.decomposition import KernelPCA
rbf_pca = KernelPCA(n_components = 2, kernel="rbf", gamma=0.04)
X_reduced = rbf_pca.fit_transform(X)
# kernel_pca_plot
from sklearn.decomposition import 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))
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()
图 8-10 展示了使用线性核(等同于简单的使用 PCA
类),RBF
核,sigmoid
核(Logistic)将瑞士卷降到 2 维。
图 8-10 使用不同核的 kPCA 将瑞士卷降到 2 维
4.1 选择一种核并调整超参数
由于 kPCA
是无监督学习算法,因此没有明显的性能指标可以帮助您选择最佳的核方法和超参数值
。但是,降维通常是监督学习任务(例如分类)的准备步骤,因此您可以简单地使用网格搜索
来选择可以让该任务达到最佳表现的核方法和超参数
。例如,下面的代码创建了一个两步的流水线,首先使用 kPCA
将维度降至两维,然后应用 Logistic
回归进行分类。然后它使用Grid SearchCV
为 kPCA
找到最佳的核和gamma
值,以便在最后获得最佳的分类准确性:
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())
])
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)
GridSearchCV(cv=3, error_score='raise',
estimator=Pipeline(memory=None,
steps=[('kpca', KernelPCA(alpha=1.0, coef0=1, copy_X=True, degree=3, eigen_solver='auto',
fit_inverse_transform=False, gamma=None, kernel='linear',
kernel_params=None, max_iter=None, n_components=2, n_jobs=1,
random_state=None, remove_zero_eig=False, tol=0)), ('log_reg', LogisticRegre...ty='l2', random_state=None, solver='liblinear', tol=0.0001,
verbose=0, warm_start=False))]),
fit_params=None, iid=True, n_jobs=1,
param_grid=[{'kpca__kernel': ['rbf', 'sigmoid'], 'kpca__gamma': array([0.03 , 0.03222, 0.03444, 0.03667, 0.03889, 0.04111, 0.04333,
0.04556, 0.04778, 0.05 ])}],
pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
scoring=None, verbose=0)
你可以通过调用best_params_
变量来查看使模型效果最好的核和超参数:
print(grid_search.best_params_)
{'kpca__kernel': 'rbf', 'kpca__gamma': 0.043333333333333335}
另一种完全为非监督的方法,是选择产生最低重建误差的核和超参数
。但是,重建并不像线性 PCA
那样容易。这里是原因:图 8-11 显示了原始瑞士卷 3D 数据集(左上角),并且使用 RBF
核应用 kPCA
后生成的二维数据集(右上角)。由于核技巧,这在数学上等同于使用特征映射φ
将训练集映射到无限维特征空间(右下),然后使用线性 PCA
将变换的训练集投影到 2D。请注意,如果我们可以在缩减空间中对给定实例实现反向线性 PCA
步骤,则重构点将位于特征空间中,而不是位于原始空间中(例如,如图中由
x表示的那样)
。由于特征空间是无限维的,我们不能找出重建点,因此我们无法计算真实的重建误差
。幸运的是,可以在原始空间中找到一个贴近重建点的点
。这被称为重建前图像(reconstruction pre-image)
。一旦你有这个前图像,你就可以测量其与原始实例的平方距离
。然后,您可以选择最小化重建前图像错误的核和超参数
。
图 8-11 核 PCA 和重建前图像误差
您可能想知道如何进行这种重建。一种解决方案是训练一个监督回归模型,将预计实例作为训练集,并将原始实例作为训练目标。如果您设置了fit_inverse_transform = True
,Scikit-Learn 将自动执行此操作,代码如下所示:
rbf_pca = KernelPCA(n_components = 2, kernel="rbf", gamma=0.0433,fit_inverse_transform=True)
X_reduced = rbf_pca.fit_transform(X)
X_preimage = rbf_pca.inverse_transform(X_reduced)
from sklearn.metrics import mean_squared_error
mean_squared_error(X, X_preimage)
32.78630879576611
概述:默认条件下,
fit_inverse_transform = False
并且KernelPCA
没有inverse_tranfrom()
方法。这种方法仅仅当fit_inverse_transform = True
的情况下才会创建。
你可以计算重建前图像误差:
现在你可以使用交叉验证的方格搜索来寻找可以最小化重建前图像误差的核方法和超参数。
4.2 LLE
局部线性嵌入(Locally Linear Embedding
)是另一种非常有效的非线性降维(NLDR
)方法。这是一种流形学习技术,不依赖于像以前算法那样的投影。简而言之,LLE 首先测量每个训练实例与其最近邻(c.n.)之间的线性关系,然后寻找能最好地保留这些局部关系的训练集的低维表示(稍后会详细介绍) 。这使得它特别擅长展开扭曲的流形,尤其是在没有太多噪音的情况下。
例如,以下代码使用 Scikit-Learn 的LocallyLinearEmbedding
类来展开瑞士卷。得到的二维数据集如图 8-12 所示。正如您所看到的,瑞士卷被完全展开,实例之间的距离保存得很好。但是,距离不能在较大范围内保留的很好:展开的瑞士卷的左侧被挤压,而右侧的部分被拉长。尽管如此,LLE 在对流形建模方面做得非常好。
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=41)
from sklearn.manifold import LocallyLinearEmbedding
lle = LocallyLinearEmbedding(n_components=2, n_neighbors=10, random_state=42)
X_reduced = lle.fit_transform(X)
# lle_unrolling_plot
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()
图 8-12 使用 LLE 展开瑞士卷
5 其他降维方法
还有很多其他的降维方法,Scikit-Learn 支持其中的好几种。这里是其中最流行的:
-
多维缩放(MDS)在尝试保持实例之间距离的同时降低了维度
(参见图 8-13) -
Isomap 通过将每个实例连接到最近的邻居来创建图形,然后在尝试保持实例之间的测地距离时降低维度。
-
t-分布随机邻域嵌入(t-Distributed Stochastic Neighbor Embedding,t-SNE)可以用于降低维度,同时试图保持相似的实例临近并将不相似的实例分开
。它主要用于可视化,尤其是用于可视化高维空间中的实例(例如,可以将MNIST图像降维到 2D 可视化)。 -
线性判别分析(Linear Discriminant Analysis,LDA)实际上是一种分类算法
,但在训练过程中,它会学习类之间最有区别的轴,然后使用这些轴来定义用于投影数据的超平面。LDA 的好处是投影会尽可能地保持各个类之间距离,所以在运行另一种分类算法(如 SVM 分类器)之前,LDA 是很好的降维技术。
图 8-13 使用不同的技术将瑞士卷降维至 2D
6 练习
- 减少数据集维度的主要动机是什么?主要缺点是什么?
- 什么是维度爆炸?
- 一旦对某数据集降维,我们可能恢复它吗?如果可以,怎样做才能恢复?如果不可以,为什么?
- PCA 可以用于降低一个高度非线性对数据集吗?
- 假设你对一个 1000 维的数据集应用 PCA,同时设置方差解释率为 95%,你的最终数据集将会有多少维?
- 在什么情况下你会使用普通的 PCA,增量 PCA,随机 PCA 和核 PCA?
- 你该如何评价你的降维算法在你数据集上的表现?
- 将两个不同的降维算法串联使用有意义吗?
- 加载 MNIST 数据集(在第 3 章中介绍),并将其分成一个训练集和一个测试集(将前 60,000 个实例用于训练,其余 10,000 个用于测试)。在数据集上训练一个随机森林分类器,并记录了花费多长时间,然后在测试集上评估模型。接下来,使用 PCA 降低数据集的维度,设置方差解释率为 95%。在降维后的数据集上训练一个新的随机森林分类器,并查看需要多长时间。训练速度更快?接下来评估测试集上的分类器:它与以前的分类器比较起来如何?
- 使用 t-SNE 将 MNIST 数据集缩减到二维,并使用 Matplotlib 绘制结果图。您可以使用 10 种不同颜色的散点图来表示每个图像的目标类别。或者,您可以在每个实例的位置写入彩色数字,甚至可以绘制数字图像本身的降维版本(如果绘制所有数字,则可视化可能会过于混乱,因此您应该绘制随机样本或只在周围没有其他实例被绘制的情况下绘制)。你将会得到一个分隔良好的的可视化数字集群。尝试使用其他降维算法,如 PCA,LLE 或 MDS,并比较可视化结果。
练习答案请见附录 A。
网友评论