09_降维、特征提取与流行学习
描述
利用无监督学习进行数据变换可能有很多种目的。最常见的目的就是可视化、压缩数据,以及寻找信息量更大的数据表示用于进一步的处理。为了实现这些目的,最简单的也是最常用的一种算法就是主成分分析。另外两种算法:非负矩阵分解(NMF)和t-SNE,前者通常用于特征提取,后者通常用于二维散点图的可视化。
PCA
主成分分析(降维)
主成分分析(principal component analysis,PCA)是一种旋转数据集的方法,旋转后的特征在统计上不相关。
在做完这种旋转之后,通常是根据新特征对解释数据的重要性来选择它的一个子集。
import mglearn
mglearn.plots.plot_pca_illustration()
执行上例,得到4个子图:
- 第一张图(左上)显示的是原始数据点,用不同颜色加以区分。算法首先找到方差最大的方向,将其标记为“成分 1”(Component 1)。这是数据中包含最多信息的方向(或向量),换句话说,沿着这个方向的特征之间最为相关。利用这一过程找到的方向被称为主成分(principal component),因为它们是数据方差的主要方向。一般来说,主成分的个数与原始特征相同。
- 第二张图(右上)显示的是同样的数据,但现在将其旋转,使得第一主成分与 x 轴平行且第二主成分与 y 轴平行。在旋转之前,从数据中减去平均值,使得变换后的数据以零为中心。在 PCA 找到的旋转表示中,两个坐标轴是不相关的,也就是说,对于这种数据表示,除了对角线,相关矩阵全部为零。
- 可以通过仅保留一部分主成分来使用 PCA 进行降维。正如第三张图所示(左下)。这将数据从二维数据集降为一维数据集。但要注意,我们没有保留原始特征之一,而是找到了最有趣的方向
- 可以反向旋转并将平均值重新加到数据中。正如第四张图所示(右下)这些数据点位于原始特征空间中,但我们仅保留了第一主成分中包含的信息。
PCA 最常见的应用之一就是将高维数据集可视化。对于 Iris(鸢尾花)数据集,可以创建散点图矩阵,因为特征比较少。如果想要查看乳腺癌数据集,即便用散点图矩阵也很困难。这个数据集包含 30 个特征,至少要绘制30*14=420张散点图,不利于观察特征之间得关系。
不过可以用一种更简单得可视化方法:对每个特征分别计算两个类别得直方图
from sklearn.datasets import load_breast_cancer
import matplotlib.pyplot as plt
import numpy as npcancer = load_breast_cancer()
fig, axes = plt.subplots(15, 2, figsize=(10, 20))
malignant = cancer.data[cancer.target == 0]
benign = cancer.data[cancer.target == 1]
ax = axes.ravel()
for i in range(30):_, bins = np.histogram(cancer.data[:, i], bins=50)ax[i].hist(malignant[:, i], bins=bins, color=mglearn.cm3(0), alpha=.5)ax[i].hist(benign[:, i], bins=bins, color=mglearn.cm3(2), alpha=.5)ax[i].set_title(cancer.feature_names[i])ax[i].set_yticks(())
ax[0].set_xlabel("Feature magnitude")
ax[0].set_ylabel("Frequency")
ax[0].legend(["malignant", "benign"], loc="best")
fig.tight_layout()
为每个特征创建一个直方图,计算具有某一特征的数据点在特定范围内(叫作bin)的出现频率。每张图都包含两个直方图,一个是良性类别的所有点(蓝色),一个是恶性类别的所有点(红色)。这样可以了解每个特征在两个类别中的分布情况,也可以猜测哪些特征能够更好地区分良性样本和恶性样本。
但是,这种图无法向我们展示变量之间的相互作用以及这种相互作用与类别之间的关系。利用 PCA,可以获取到主要的相互作用,并得到稍为完整的图像。可以找到前两个主成分,并在这个新的二维空间中用散点图将数据可视化。
下面是通过StandardScaler缩放特征数据,再通过PCA选出前两个主成分
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCAscaler = StandardScaler()
scaler.fit(cancer.data)
X_scaled = scaler.transform(cancer.data)# 分析主成分,只取前两个主成分
pca = PCA(n_components=2)
pca.fit(X_scaled)
X_pca = pca.transform(X_scaled)
print(X_scaled.shape)
print(X_pca.shape)
# 可视化
plt.figure(figsize=(8,8))
mglearn.discrete_scatter(X_pca[:,0],X_pca[:,1],cancer.target)
plt.legend(cancer.target_names,loc="best")
plt.gca().set_aspect('equal')
plt.xlabel('first principal componet')
plt.ylabel('second principal comonent')
学习并应用 PCA 变换与应用预处理变换一样简单。将 PCA 对象实例化,调用 fit 方法找到主成分,然后调用 transform 来旋转并降维。默认情况下,PCA 仅旋转(并移动)数据,但保留所有的主成分。为了降低数据的维度,需要在创建 PCA 对象时指定想要保留的主成分个数:n_components。
PCA 是一种无监督方法,在寻找旋转方向时没有用到任何类别信息。它只是观察数据中的相关性。对于上例的散点图,绘制了第一主成分与第二主成分的关系,然后利用类别信息对数据点进行着色。
可以用热图将系数可视化PCA主成分(components_ 中的每一行对应于一个主成分,它们按重要性排序(第一主成分排在首位,以此类推)。)
plt.matshow(pca.components_,cmap='viridis')
plt.yticks([0,1],["First component", "Second component"])
plt.colorbar()
plt.xticks(range(len(cancer.feature_names)),cancer.feature_names,rotation=60,ha='left')
plt.xlabel('Feature')
plt.ylabel('Principal components')
白化
PCA白化旨在通过线性变换将数据的特征向量归一化,并在此过程中保留数据的有用信息,从而消除相关性和冗余性。
假设训练数据是图像,由于图像中相邻像素之间具有很强的相关性,所以用于训练时输入是冗余的;白化的目的就是降低输入的冗余性。
输入数据集X,经过白化处理后,新的数据X’满足两个性质:
- 特征之间相关性较低
- 所有特征具有相同的方差
sklearn中的PCA开启白化是通过参数 whiten控制,whiten=True表示开启白化处理.。
用 PCA 对图像做特征提取的一个简单应用,处理 Labeled Faces(标记人脸)中的人脸图像。
注意:这里是演示PCA白化处理的效果,不是真正的人脸识别;fetch_lfw_people函数会自动下载数据,但是由于提供数据的网站已关闭,需要手动下载数据。
from sklearn.datasets import fetch_lfw_people
people = fetch_lfw_people(data_home='D:/WebDownload', download_if_missing=False,min_faces_per_person=20,resize=0.7)image_shape = people.images[0].shape
fix,axes = plt.subplots(2,5,figsize=(15,8),subplot_kw={'xticks':(),'yticks':()})
for target,image,ax in zip(people.target,people.images,axes.ravel()):ax.imshow(image)ax.set_title(people.target_names[target])print("people.images.shape: {}".format(people.images.shape))
print("Number of classes: {}".format(len(people.target_names)))
为了降低数据偏斜,对每个人最多只取 50 张图像。
mask = np.zeros(people.target.shape,dtype=np.bool_)
for target in np.unique(people.target):mask[np.where(people.target==target)[0][:50]]=1X_people=people.data[mask]
Y_people=people.target[mask]
# 将灰度值缩放到0到1之间,而不是在0到255之间
# 以得到更好的数据稳定性
X_people = X_people / 255.
通过K近邻算法来识别人脸,测试得分只有0.22
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
# 将数据分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X_people, Y_people, stratify=Y_people, random_state=0)
# 使用一个邻居构建KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=1)
knn.fit(X_train, y_train)
print("Test set score of 1-nn: {:.2f}".format(knn.score(X_test, y_test)))
启用 PCA 的白化(whitening)选项,它将主成分缩放到相同的尺度,再进行K近邻算法识别人脸,这次的测试得分0.30,比上例提高了。
# 对数据进行白化处理
pca = PCA(n_components=100, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
print("X_train_pca.shape: {}".format(X_train_pca.shape))knn = KNeighborsClassifier(n_neighbors=1)
knn.fit(X_train_pca, y_train)
print("Test set accuracy: {:.2f}".format(knn.score(X_test_pca, y_test)))
对于图像数据,还可以很容易地将找到的主成分可视化。
fix, axes = plt.subplots(3, 5, figsize=(15, 12),subplot_kw={'xticks': (), 'yticks': ()})
for i, (component, ax) in enumerate(zip(pca.components_, axes.ravel())):ax.imshow(component.reshape(image_shape), cmap='viridis')ax.set_title("{}. component".format((i + 1)))
虽然肯定无法理解这些成分的所有内容,但可以猜测一些主成分捕捉到了人脸图像的哪些方面。第一个主成分似乎主要编码的是人脸与背景的对比,第二个主成分编码的是人脸左半部分和右半部分的明暗程度差异,如此等等。
重要的是要记住,算法对数据(特别是视觉数据,比如人们非常熟悉的图像)的解释通常与人类的解释方式大不相同。
非负矩阵分解
非负矩阵分解(non-negative matrix factorization,NMF)目的在于提取有用的特征。它的工作原理类似于 PCA,也可以用于降维。NMF只能应用于每个特征都是非负的数据。
将数据分解成非负加权求和的这个过程,对由多个独立源相加(或叠加)创建而成的数据特别有用,比如多人说话的音轨或包含多种乐器的音乐。在这种情况下,NMF 可以识别出组成合成数据的原始分量。总的来说,与 PCA 相比,NMF 得到的分量更容易解释,因为负的分量和系数可能会导致难以解释的抵消效应(cancellation effect)。
mglearn.plots.plot_nmf_illustration()
执行上例可以得到两个NMF的图。
对于两个分量的 NMF(如左图所示),显然所有数据点都可以写成这两个分量的正数组合。如果有足够多的分量能够完美地重建数据(分量个数与特征个数相同),那么算法会选择指向数据极值的方向。
如果仅使用一个分量(如右图所示),那么 NMF 会创建一个指向平均值的分量,因为指向这里可以对数据做出最好的解释。
与 PCA 不同,减少分量个数不仅会删除一些方向,而且会创建一组完全不同的分量! NMF 的分量也没有按任何特定方法排序,所以不存在“第一非负分量”:所有分量的地位平等。
NMF 使用了随机初始化,根据随机种子的不同可能会产生不同的结果。在相对简单的情况下(比如两个分量的模拟数据),所有数据都可以被完美地解释,那么随机性的影响很小。
将 NMF 应用于之前用过的 Wild 数据集中的 Labeled Faces。NMF 的主要参数是想要提取的分量个数。通常来说,这个数字要小于输入特征的个数。
from sklearn.decomposition import NMF
nmf = NMF(n_components=15, random_state=0)
nmf.fit(X_train)
X_train_nmf = nmf.transform(X_train)
X_test_nmf = nmf.transform(X_test)# knn = KNeighborsClassifier(n_neighbors=1)
# knn.fit(X_train_nmf, y_train)
# print("Test set accuracy: {:.2f}".format(knn.score(X_test_nmf, y_test)))fix, axes = plt.subplots(3, 5, figsize=(15, 12), subplot_kw={'xticks': (), 'yticks': ()})
for i, (component, ax) in enumerate(zip(nmf.components_, axes.ravel())):ax.imshow(component.reshape(image_shape))ax.set_title("{}. component".format(i))
这些分量都是正的,通过上例,可以清楚地看到,分量 7(component 7)则显示了稍微向左转动的人脸,分量 12(component 12)则显示了人脸右明显的张嘴动作。
compn = 7
# 按第7个分量排序,绘制前10张图像
inds = np.argsort(X_train_nmf[:, compn])[::-1]
fig, axes = plt.subplots(2, 5, figsize=(15, 8),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (ind, ax) in enumerate(zip(inds, axes.ravel())):ax.imshow(X_train[ind].reshape(image_shape))compn = 12
# 按第12个分量排序,绘制前10张图像
inds = np.argsort(X_train_nmf[:, compn])[::-1]
fig, axes = plt.subplots(2, 5, figsize=(15, 8),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (ind, ax) in enumerate(zip(inds, axes.ravel())):ax.imshow(X_train[ind].reshape(image_shape))
用t-SNE进行流行学习
虽然 PCA 通常是用于变换数据的首选方法,能够用散点图将其可视化,但这一方法的性质(先旋转然后减少方向)限制了其有效性。有一类用于可视化的算法叫作流形学习算法(manifold learningalgorithm),它允许进行更复杂的映射,通常也可以给出更好的可视化。其中特别有用的一个就是 t-SNE 算法。
流形学习算法主要用于可视化,因此很少用来生成两个以上的新特征。其中一些算法(包括 t-SNE)计算训练数据的一种新表示,但不允许变换新数据。这意味着这些算法不能用于测试集:更确切地说,它们只能变换用于训练的数据。流形学习对探索性数据分析是很有用的,但如果最终目标是监督学习的话,则很少使用。
t-SNE 背后的思想是找到数据的一个二维表示,尽可能地保持数据点之间的距离。t-SNE 首先给出每个数据点的随机二维表示,然后尝试让在原始特征空间中距离较近的点更加靠近,原始特征空间中相距较远的点更加远离。t-SNE 重点关注距离较近的点,而不是保持距离较远的点之间的距离。换句话说,它试图保存那些表示哪些点比较靠近的信息。
对 scikit-learn 包含的一个手写数字数据集。应用 t-SNE 流形学习算法。在这个数据集中,每个数据点都是 0 到 9 之间手写数字的一张 8×8 灰度图像。
from sklearn.datasets import load_digits
digits = load_digits()fig,axes = plt.subplots(2,5,figsize=(10,5))
for ax,img in zip(axes.ravel(),digits.images):ax.imshow(img)
用 PCA 将降到二维的数据可视化。对前两个主成分作图,并按类别对数据点着色。用每个类别对应的数字作为符号来显示每个类别的位置。利用前两个主成分可以将数字 0、6 和 4 相对较好地分开,尽管仍有重叠。大部分其他数字都大量重叠在一起。
pca = PCA(n_components=2)
pca.fit(digits.data)digits_pca = pca.transform(digits.data)
colors = ["#476A2A", "#7851B8", "#BD3430", "#4A2D4E", "#875525","#A83683", "#4E655E", "#853541", "#3A3120", "#535D8E"]plt.figure(figsize=(10,10))
plt.xlim(digits_pca[:,0].min(),digits_pca[:,0].max())
plt.ylim(digits_pca[:,0].min(),digits_pca[:,1].max())
for i in range(len(digits.data)):plt.text(digits_pca[i,0],digits_pca[i,1],str(digits.target[i]),color=colors[digits.target[i]],fontdict={'weight':'bold','size':9})
plt.xlabel('First principal component')
plt.ylabel('Second principal component')
将 t-SNE 应用于同一个数据集,并对结果进行比较。由于 t-SNE 不支持变换新数据,所以 TSNE 类没有 transform 方法。可以调用 fit_transform 方法来代替,它会构建模无监督学习与预处理型并立刻返回变换后的数据。
from sklearn.manifold import TSNEtsne = TSNE(random_state=42)
digits_tsne = tsne.fit_transform(digits.data)
plt.figure(figsize=(10,10))
plt.xlim(digits_tsne[:,0].min(),digits_tsne[:,0].max())
plt.ylim(digits_tsne[:,0].min(),digits_tsne[:,1].max())
for i in range(len(digits.data)):plt.text(digits_tsne[i,0],digits_tsne[i,1],str(digits.target[i]),color=colors[digits.target[i]],fontdict={'weight':'bold','size':9})
plt.xlabel('First principal component')
plt.ylabel('Second principal component')
t-SNE 的结果非常好。所有类别都被明确分开。数字 1 和 9 被分成几块,但大多数类别都形成一个密集的组。这种方法并不知道类别标签:它完全是无监督的。但它能够找到数据的一种二维表示,仅根据原始空间中数据点之间的靠近程度就能够将各个类别明确分开。