其它章节内容请见机器学习之PyTorch和Scikit-Learn
scikit-learn中的主成分分析
虽然上一小节中繁琐的介绍有助于我们了解PCA的内部原理,但我们还是要谈谈scikit-learn中实现的PCA
类。
PCA
类是scikit-learn中又一个变换器类,我们在使用相同模型参数谈的训练数据和测试集前先使用训练数据拟合模型。下面对葡萄酒训练数据集使用scikit-learn中的PCA
类,通过逻辑回归分类变换后的样本,再使用第2章 为分类训练简单机器学习算法中定义的plot_decision_regions
函数可视化决策区域:
from matplotlib.colors import ListedColormap
def plot_decision_regions(X, y, classifier, test_idx=None, resolution=0.02):
# setup marker generator and color map
markers = ('o', 's', '^', 'v', '<')
colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(y))])
# plot the decision surface
x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
np.arange(x2_min, x2_max, resolution))
lab = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
lab = lab.reshape(xx1.shape)
plt.contourf(xx1, xx2, lab, alpha=0.3, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
# plot class examples
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0],
y=X[y == cl, 1],
alpha=0.8,
c=colors[idx],
marker=markers[idx],
label=f'Class {cl}',
edgecolor='black')
为方便起见,可以将前面的plot_decision_regions
代码放到当前工作目录中单独的文件内,例如,plot_decision_regions_script.py
,然后在当前Python会话中导入它:
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.decomposition import PCA
>>> # initializing the PCA transformer and
>>> # logistic regression estimator:
>>> pca = PCA(n_components=2)
>>> lr = LogisticRegression(multi_class='ovr',
... random_state=1,
... solver='lbfgs')
>>> # dimensionality reduction:
>>> X_train_pca = pca.fit_transform(X_train_std)
>>> X_test_pca = pca.transform(X_test_std)
>>> # fitting the logistic regression model on the reduced dataset:
>>> lr.fit(X_train_pca, y_train)
>>> plot_decision_regions(X_train_pca, y_train, classifier=lr)
>>> plt.xlabel('PC 1')
>>> plt.ylabel('PC 2')
>>> plt.legend(loc='lower left')
>>> plt.tight_layout()
>>> plt.show()
通过执行这段代码,应该会看训练数据的决策区域降低到两个主成分轴:
图5.4:使用scikit-learn的PCA降维后的训练样本和逻辑回归决策区域
对比scikit-learn的PCA投射和我们自己实现的PCA,你可能会发现产生的图彼此互为镜像。请注意这不是说两种实现中有什么错误,出现不同的原因是根据特征值求解的不同,特征向量可能会为正值或负值。
到不是说这很重要,但我们可以轻松通过将数据乘以-1来翻转镜像图片,特征向量一般会缩放到单位长度1。为完整起见,我们对变换的测试数据集绘制逻辑回归决策区域,查看其分类状况:
>>> plot_decision_regions(X_test_pca, y_test, classifier=lr)
>>> plt.xlabel('PC 1')
>>> plt.ylabel('PC 2')
>>> plt.legend(loc='lower left')
>>> plt.tight_layout()
>>> plt.show()
执行如上代码绘制好测试集的决策区域后,可以看到逻辑回归对这个小型二维特征子空间的执行效果很好,只对测试集中很少的几个样本分类有误:
图5.5:通过基于PCA的特征子空间中的逻辑回归决策区域测试数据点
如果对不同主成分的可解释方差比感兴趣的话,可以通过将参数n_components
设置为None
来初始化PCA
,这样会保留所有主成分,然后可通过explained_variance_ratio_
属性访问可解释方差比:
>>> pca = PCA(n_components=None)
>>> X_train_pca = pca.fit_transform(X_train_std)
>>> pca.explained_variance_ratio_
array([ 0.36951469, 0.18434927, 0.11815159, 0.07334252,
0.06422108, 0.05051724, 0.03954654, 0.02643918,
0.02389319, 0.01629614, 0.01380021, 0.01172226,
0.00820609])
注意我们在初始化PCA
类时设置了n_components=None
,这样所有主成分按排序返回,不会执行降维操作。
评估特征贡献
本节中我们会简要地学习如何评估原始特征对主成分的贡献。我们已经学习过,通过PCA,我们创建了表示特征线性组合的主成分。有时,我们想知道每个原始特征对某一主成分贡献了几成。这些贡献通常称为载荷(loadings)。
因子载荷可通过以特征值平方根缩放特征向量进行计算。生成的值可解释为原始特征与主成分之间的关联。为进行讲解,我们来绘制第一个主成分的载荷。
首先,通过对特征向量乘以特征值平方根来计算13×13-维载荷矩阵。
>>> loadings = eigen_vecs * np.sqrt(eigen_vals)
然后我们绘制第一个主成分的载荷,loadings[:, 0]
,它也是矩阵中的第一列:
>>> fig, ax = plt.subplots()
>>> ax.bar(range(13), loadings[:, 0], align='center')
>>> ax.set_ylabel('Loadings for PC 1')
>>> ax.set_xticks(range(13))
>>> ax.set_xticklabels(df_wine.columns[1:], rotation=90)
>>> plt.ylim([-1, 1])
>>> plt.tight_layout()
>>> plt.show()
在图5.6中,可以看到,比如酒精度(Alcohol)与第一个主成分负相关(约–0.3),而苹果酸( Malic acid)与其正相关(约0.54)。注意值1表示完全正相关,而–1对应着完全负相关:
图5.6:与第一个主成分的特征相关性
在以下的代码示例中,我们对自己的PCA实现计算了因子载荷。可以用类似的方式获取拟合scikit-learn PCA对象的载荷,其中pca.components
表示特征向量,pca.explained_variance
表示特征值:
>>> sklearn_loadings = pca.components_.T * np.sqrt(pca.explained_variance_)
为比较scikit-learn PCA载荷与些前我们自己所创建的,我们绘制一个类似的柱状图:
>>> fig, ax = plt.subplots()
>>> ax.bar(range(13), sklearn_loadings[:, 0], align='center')
>>> ax.set_ylabel('Loadings for PC 1')
>>> ax.set_xticks(range(13))
>>> ax.set_xticklabels(df_wine.columns[1:], rotation=90)
>>> plt.ylim([-1, 1])
>>> plt.tight_layout()
>>> plt.show()
可以看到这个柱状图基本一样:
图5.7:使用scikit-learn得到的与第一个主成分的特征相关性
在以无监督提取技术探讨了PCA之后,下一节中我们会介绍线性判别分析(linear discriminant analysis (LDA)),这是一种考虑了类标签的线性变换技术。
线性判别分析的监督数据压缩
LDA可用作特征提取技术来提升计算效率和降低因非正则化模型中维度灾难所导致的过拟合程序。LDA背后的基本概念与PCA非常类似,PCA尝试找到数据集中最大方差的正交分量轴,而LDA的目标是找到优化类分离的特征子空间。在下面的小节中,我们会详细讨论LDA和PCA的相似性并逐步实现LDA方法。
主成分分析 vs. 线性判别分析
PCA和LDA都是可降低数据集中维数的线性变换技术,前者是无监督算法,后者是有监督的。因此,我们可以把LDA看做是比PCA更高级的分类特征提取技术。但A.M. Martinez指出通过PCA对某些图像识别任务做预处理会产生更好的分类结果,比如,每类仅包含很少的样本(PCA Versus LDA by A. M. Martinez and A. C. Kak, IEEE Transactions on Pattern Analysis and Machine Intelligence, 23(2): 228-233, 2001)。
Fisher LDA
LDA有时也称Fisher’s LDA。Ronald A. Fisher首先于1936年为二类分类问题制定了Fisher线性判别(The Use of Multiple Measurements in Taxonomic Problems, R. A. Fisher, Annals of Eugenics, 7(2): 179-188, 1936)。1948年,Fisher线性判别由C. Radhakrishna Rao泛化到多类问题上,前提是相等类协方差和正态分布分类,现在称之为LDA(The Utilization of Multiple Measurements in Problems of Biological Classification by C. R. Rao, Journal of the Royal Statistical Society. Series B (Methodological), 10(2): 159-203, 1948)。
图5.8总结了用于二类问题的LDA的概念。类1的样本使用圆圈表示,类2的样本使用十字符号表示:
图5.8:用于二类问题的LDA的概念
在x轴(LD 1)上所示的线性判别,会很好地分出两个正态分布的类。虽然y轴 (LD 2)上示例线性判别项捕获了数据集中的大量方差,它并不能作为一个很好的线性判别,因为没有捕获到任何类判别信息。
LDA中的一个假设是数据是正态分布的。同时我们假定类具有相同的协方差矩阵,并且训练样本在统计层面上彼此独立。但即使一个或多个假设有(此许的)不符合条件,用于降维的LDA仍可以很好地运行(Pattern Classification 2nd Edition by R. O. Duda, P. E. Hart, and D. G. Stork, New York, 2001)。
线性判别分析的内部原理
在进入代码实现前,我们简要地总结下执行LDA所需的主要步骤:
- 标准化d-维数据集(d为特征数)。
- 为每个类计算d-维平均向量。
- 构建类间散度矩阵SB和类内散度矩阵SW。
- 计算矩阵 S W − 1 S B S_W^{-1}S_B SW−1SB的特征向量和对应的特征值。
- 通过特征值降序来排列对应的特征向量。
- 选择对应k最大特征值的k特征向量来构建d×k-维变换矩阵W,特征向量为矩阵的列。
- 使用变换矩阵W将样本投射到新的特征子空间上。
可以看到,LDA与PCA在将矩阵解构为特征值和特征向量层面非常类似,它会形成新的更低维特征空间。但像前面所说的,LDA会考虑类标签信息,这体现在第2步中计算的平均向量。在下一节中,我们会更详细讨论这7个步骤,以及其代码实现。
计算散度矩阵
在本章开始的PCA小节中我们已经对葡萄酒数据集中的特征做过标准化,所以跳过第一步直接计算平均向量,我们会使用它们分别构建类内散度矩阵和类间散度矩阵。每个平均向量mi,存储着类i中样本的平均特征值 μ m \mu_m μm:
这会产生三个平均向量:
这些平均向量可通过如下代码计算,其中计算了三个标签各自的平均向量:
>>> np.set_printoptions(precision=4)
>>> mean_vecs = []
>>> for label in range(1,4):
... mean_vecs.append(np.mean(
... X_train_std[y_train==label], axis=0))
... print(f'MV {label}: {mean_vecs[label - 1]}\n')
MV 1: [ 0.9066 -0.3497 0.3201 -0.7189 0.5056 0.8807 0.9589 -0.5516
0.5416 0.2338 0.5897 0.6563 1.2075]
MV 2: [-0.8749 -0.2848 -0.3735 0.3157 -0.3848 -0.0433 0.0635 -0.0946
0.0703 -0.8286 0.3144 0.3608 -0.7253]
MV 3: [ 0.1992 0.866 0.1682 0.4148 -0.0451 -1.0286 -1.2876 0.8287
-0.7795 0.9649 -1.209 -1.3622 -0.4013]
使用平均向量我们就可以计算出类内散度矩阵SW:
这是通过单个类i的单独散度矩阵Si加和而得:
>>> 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
>>> print('Within-class scatter matrix: '
... f'{S_W.shape[0]}x{S_W.shape[1]}')
Within-class scatter matrix: 13x13
我们在计算散度矩阵时的假设是训练集中的类标签是均匀分布的。但如果打印出类标签数,会发现不符合这一假定:
>>> print('Class label distribution:',
... np.bincount(y_train)[1:])
Class label distribution: [41 50 33]
因此在加总散度矩阵SW前我们会希望缩放各散度矩阵Si。将散度矩阵除以类样本数ni时,可以看到计算散度矩阵实际上和计算协方差矩阵 Σ i \Sigma_i Σi一样-协方差矩阵是散度矩阵的归一化版本:
计算缩放后的类内散度矩阵的代码如下:
>>> d = 13 # number of features
>>> S_W = np.zeros((d, d))
>>> for label,mv in zip(range(1, 4), mean_vecs):
... class_scatter = np.cov(X_train_std[y_train==label].T)
... S_W += class_scatter
>>> print('Scaled within-class scatter matrix: '
... f'{S_W.shape[0]}x{S_W.shape[1]}')
Scaled within-class scatter matrix: 13x13
在计算好缩放后类内散度矩阵(或协方差矩阵)后,我们可以进入下一步计算类间散度矩阵SB:
这里m是所计算的整体均值,包含所有c类中的样本:
>>> mean_overall = np.mean(X_train_std, axis=0)
>>> mean_overall = mean_overall.reshape(d, 1)
>>> d = 13 # number of features
>>> S_B = np.zeros((d, d))
>>> for i, mean_vec in enumerate(mean_vecs):
... n = X_train_std[y_train == i + 1, :].shape[0]
... mean_vec = mean_vec.reshape(d, 1) # make column vector
... S_B += n * (mean_vec - mean_overall).dot(
... (mean_vec - mean_overall).T)
>>> print('Between-class scatter matrix: '
... f'{S_B.shape[0]}x{S_B.shape[1]}')
Between-class scatter matrix: 13x13
选择新特征子空间的线性判别
LDA剩下的步骤类似于PCA的步骤。但这里不执行对协方差矩阵的解构,而是去解矩阵的泛化特征值问题 S W − 1 S B S_W^{-1}S_B SW−1SB:
>>> eigen_vals, eigen_vecs =\
... np.linalg.eig(np.linalg.inv(S_W).dot(S_B))
我们在计算好特征对后,可以按降序排列特征值:
>>> eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:,i])
... for i in range(len(eigen_vals))]
>>> eigen_pairs = sorted(eigen_pairs,
... key=lambda k: k[0], reverse=True)
>>> print('Eigenvalues in descending order:\n')
>>> for eigen_val in eigen_pairs:
... print(eigen_val[0])
Eigenvalues in descending order:
349.617808906
172.76152219
3.78531345125e-14
2.11739844822e-14
1.51646188942e-14
1.51646188942e-14
1.35795671405e-14
1.35795671405e-14
7.58776037165e-15
5.90603998447e-15
5.90603998447e-15
2.25644197857e-15
0.0
在LDA中,线性判别的数量最多为c – 1,其中c是类标签的数量,因为类内类间散度矩阵SB,是秩为1或更低的c矩阵的和。可以看到我们只有两个非零特征值(特征值3-13并不完全等于0,但这是由NumPy中的浮点运算所导致的)。
共线性
注意在极少的共线性场景中(所有样本点落到同一条直线上),协方差的秩会为1,这会导致每个特征向量中只有一个非零特征值。
为度量类线性判别(特征向量)捕获取了多少判别信息,我们来以降序特征值绘制线性判别,类似于我们在PCA一节创建的可解释方差图。为简化起见,我们称线性判别信息的内容为判别度(discriminability):
>>> tot = sum(eigen_vals.real)
>>> discr = [(i / tot) for i in sorted(eigen_vals.real,
... reverse=True)]
>>> cum_discr = np.cumsum(discr)
>>> plt.bar(range(1, 14), discr, align='center',
... label='Individual discriminability')
>>> plt.step(range(1, 14), cum_discr, where='mid',
... label='Cumulative discriminability')
>>> plt.ylabel('"Discriminability" ratio')
>>> plt.xlabel('Linear Discriminants')
>>> plt.ylim([-0.1, 1.1])
>>> plt.legend(loc='best')
>>> plt.tight_layout()
>>> plt.show()
在图5.9中可以看出,前两个线性判别就捕获取了葡萄酒训练集100%的有用信息:
图5.9:头两个判别捕获了100%的有用信息
我们来堆叠最具判别力的两个特征向量列,创建变换矩阵W:
>>> w = np.hstack((eigen_pairs[0][1][:, np.newaxis].real,
... eigen_pairs[1][1][:, np.newaxis].real))
>>> print('Matrix W:\n', w)
Matrix W:
[[-0.1481 -0.4092]
[ 0.0908 -0.1577]
[-0.0168 -0.3537]
[ 0.1484 0.3223]
[-0.0163 -0.0817]
[ 0.1913 0.0842]
[-0.7338 0.2823]
[-0.075 -0.0102]
[ 0.0018 0.0907]
[ 0.294 -0.2152]
[-0.0328 0.2747]
[-0.3547 -0.0124]
[-0.3915 -0.5958]]
将样本投射至新特征空间
使用我们在前一小节中创建的变换矩阵W,现在可以通过矩阵相乘来变换训练集:
X′ = XW
>>> X_train_lda = X_train_std.dot(w)
>>> colors = ['r', 'b', 'g']
>>> markers = ['o', 's', '^']
>>> for l, c, m in zip(np.unique(y_train), colors, markers):
... plt.scatter(X_train_lda[y_train==l, 0],
... X_train_lda[y_train==l, 1] * (-1),
... c=c, label= f'Class {l}', marker=m)
>>> plt.xlabel('LD 1')
>>> plt.ylabel('LD 2')
>>> plt.legend(loc='lower right')
>>> plt.tight_layout()
>>> plt.show()
从图5.10中可以看出,三个葡萄酒类现在可以完美地在新特征子空间上进行线性分割:
图5.10:在数据投射到前两个判别上后葡萄酒类可完美分割
通过scikit-learn实现LDA
前面的分步实现对于理解LDA内部原理并掌握LDA和PCA之间的差别是一个很好的练习。下面我们来学习scikit-learn中LDA
类的实现:
>>> # the following import statement is one line
>>> from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
>>> lda = LDA(n_components=2)
>>> X_train_lda = lda.fit_transform(X_train_std, y_train)
接下来看逻辑回归分类器是如何处理LDA变换后的更低维训练集的:
>>> lr = LogisticRegression(multi_class='ovr', random_state=1,
... solver='lbfgs')
>>> lr = lr.fit(X_train_lda, y_train)
>>> plot_decision_regions(X_train_lda, y_train, classifier=lr)
>>> plt.xlabel('LD 1')
>>> plt.ylabel('LD 2')
>>> plt.legend(loc='lower left')
>>> plt.tight_layout()
>>> plt.show()
在图5.11中可以看出逻辑回归模型对类2中的一个样本分错了类:
图5.11:逻辑回归模型分错了一个类
通过降低正则化强度,我们可以移动决策边界,以使用逻辑回归模型正确分类训练集中的所有样本。但更重要的是,我们来看看它对测试集的结果:
>>> X_test_lda = lda.transform(X_test_std)
>>> plot_decision_regions(X_test_lda, y_test, classifier=lr)
>>> plt.xlabel('LD 1')
>>> plt.ylabel('LD 2')
>>> plt.legend(loc='lower left')
>>> plt.tight_layout()
>>> plt.show()
在图5.12中可以看到,逻辑回归分类器只需使用一个二维特征子空间就可以很精准地对测试集样本分类,根本不需要原来的12个葡萄酒特征:
图5.12:逻辑回归模型可很好地处理测试数据
非线性降维和可视化
在前面一节中,我们讲解了用线性变换技术,比如PCA和LDA进行特征提取。本节中,我们会讨论为什么考虑使用非线性降维技术是有价值的。
尤其值得重点介绍的一种非线性降维技术是t-分布随机近邻嵌套(t-SNE) ,因为文献中经常使用它来以二维或三维可视化高维数据集。我们会学习如何应用t-SNE来以二维特征空间对手写图片绘图。
为什么考虑非线性降维?
很多机器学习算法都假定输入数据是线性可分割的。我们学习过感知机甚至要求完全线性可分割的训练数据收敛。我们至此学习过的其它算法假定无法完全线性分割的原因是噪声:Adaline、逻辑回归和(标准)SVM等皆是如此。
但如果要处理非线性问题,这在现实世界的应用中经常会碰到,进行降维的线性变换技术,比如PCA和LDA可能就不是最佳选择了:
图5.13:线性和非线性问题之间的不同
scikit-learn库实现了很多用于非线性降维的高级技术,这些不在本书的讲解范围内。感举的读者可以在scikit-learn当前实现中进行总览,辅以一些图示:http://scikit-learn.org/stable/modules/manifold.html。
非线性降维技术的开发和应用常被称作流形学习(manifold learning),流形是指低维拓扑空间嵌套在高维空间中。流形学习的算法需要捕获数据的复杂结构,以投射到低维空间上并且保留数据点间的关系。
经典的流形学习示例是图5.14中所示的3维瑞士卷:
图5.14:三维瑞士卷投射到更低的二维空间上
虽然非线性降维和流形学习算法很强大,但需要注意这些技术出了名的难用,并且如果选择了不理想的超参数,它们可能会弊大于利。这种难度背后的原因是我们常常处理尚未能可视化的高维数据集,其中的结果尚不清晰(不同于图5.14中的瑞士卷示例)。此外,除非我们将数据集投射到二维或三维上(通常不足以捕获更复杂的关系),否则会很难甚至是无法评估结果的质量。因此,很多人仍然依靠PCA和LDA这种更简单的技术来实现降维。
通过t-分布随机近邻嵌套可视化数据
在介绍非线性降维并讨论了它的一些挑战后,我们来学习一个涉及t-SNE的实操案例,它通常用于将复杂数据可视化至二维或三维。
简单地说,t-SNE是基于高维(原始)特征空间中成对距离的一些模型数据点。然后,它在新的低维空间中找到接近原始空间成对距概率分布的成对距离概率分布。或者换句话说,t-SNE学习将数据点嵌套至低维空间,使得原始空间的成对距离得以保留。可以原研究论文中找到该方法是多详细的说明:Visualizing data using t-SNE by Maaten and Hinton, Journal of Machine Learning Research, 2018 (https://www.jmlr.org/papers/volume9/vandermaaten08a/vandermaaten08a.pdf)。但就像研究论文标题所说的,t-SNE是一种意在用于可视化的技术,因为它需要整个数据集来进行投射。因其直接投射各点(不同于PCA,它并不涉及到投射矩阵),我们无法对新数据点应用t-SNE。
以下代码快速演示了如何将t-SNE应用于一个64-维数据集。首先,我们通过scikit-learn加载Digits数据集,其中包含一些低分辨率的手写数字(数字0-9):
>>> from sklearn.datasets import load_digits
>>> digits = load_digits()
数字是8×8的灰度图片。以下代码绘制数据集中的前4组图片,其中总共包含1,797个图像:
>>> fig, ax = plt.subplots(1, 4)
>>> for i in range(4):
>>> ax[i].imshow(digits.images[i], cmap='Greys')
>>> plt.show()
从图5.15中可以看出,图像的像素较低,8×8像素(也就是每张图64像素):
图5.15:低分辨率手写数据图像
注意digits.data
属性让我们可以访问该数据集的平坦版本,其中样本表现为行,列对应像素:
>>> digits.data.shape
(1797, 64)
接下来我们对新变量X_digits
赋特征(像素)并将标签赋值新的变量y_digits
:
>>> y_digits = digits.target
>>> X_digits = digits.data
然后,我们从scikit-learn导入t-SNE类并拟合一个新的tsne
对象。使用fit_transform
,我们用一步执行了t-SNE拟合及数据变换:
>>> from sklearn.manifold import TSNE
>>> tsne = TSNE(n_components=2, init='pca',
... random_state=123)
>>> X_digits_tsne = tsne.fit_transform(X_digits)
通过这段代码,我们将64维数据集投射到了一个二维空间上。我们指定了init='pca'
,它使用PCA初始化了t-SNE嵌套,因为这是研究文章中所推荐的:nitialization is critical for preserving global data structure in both t-SNE and UMAP by Kobak and Linderman, Nature Biotechnology Volume 39, pages 156–157, 2021 (https://www.nature.com/articles/s41587-020-00809-z)。
注意t-SNE还包含其它超参数,比如困惑度(perplexity)和学习率(通常称为epsilon),在我们的示例中省略了(使用了scikit-learn的默认值)。在实操中,我们推荐读者也研究下这些参数。更多有关这些参数的信息及其对结果的影响请见这篇高质量的文章:How to Use t-SNE Effectively by Wattenberg, Viegas, and Johnson, Distill, 2016 (https://distill.pub/2016/misread-tsne/)。
最后,我们使用如下代码可视化二维t-SNE嵌套:
>>> import matplotlib.patheffects as PathEffects
>>> def plot_projection(x, colors):
... f = plt.figure(figsize=(8, 8))
... ax = plt.subplot(aspect='equal')
... for i in range(10):
... plt.scatter(x[colors == i, 0],
... x[colors == i, 1])
... for i in range(10):
... xtext, ytext = np.median(x[colors == i, :], axis=0)
... txt = ax.text(xtext, ytext, str(i), fontsize=24)
... txt.set_path_effects([
... PathEffects.Stroke(linewidth=5, foreground="w"),
... PathEffects.Normal()])
>>> plot_projection(X_digits_tsne, y_digits)
>>> plt.show()
同PCA一样,t-SNE是一种无监督方法,在上面的代码中,我们只使用了类标签y_digits
(0-9)是为了通过函数颜色参数进行可视化。Matplotlib的PathEffects
用于可视化,(通过np.median
)使得类标签显示在属于各自数字数据点的中央 。结果图如下:
图5.16:如何用t-SNE将手写数字嵌套到二维特征空间的可视化
可以看到,t-SNE可以很好地分割不同的数字(类),虽然仍不完美。通过调优超参数很可能达到更好的分离。但模糊的手写体可能会使用一定程度的类交叉不可避免。例如,通过查看单独的图片,我们可能发现有些数字3的图片看起来很像9,诸如此类。
统一流形逼近和投影
另一种著名的可视化技术是统一流形逼近和投影(uniform manifold approximation and projection (UMAP))。UMAP可以产生类似t-SNE一样的好结果(比如,参见早前引用的Kobak和Linderman的论文),且通常更快,它也可用于投射新数据,这使用其在机器学习中用作降维技术时更具吸引力,类似PCA。感兴趣的读者可以在原论文中找到UMAP更多的信息:UMAP: Uniform manifold approximation and projection for dimension reduction by McInnes, Healy, and Melville, **2018 **(https://arxiv.org/abs/1802.03426) 。 兼容scikit-learn的UMAP实现请见https://umap-learn.readthedocs.io。
小结
本章中,我们学习了用于特征提取的两个基本降维技术:PCA和LDA。使用PCA,我们将数据投射到更低维的子空间上,以沿正交的特征轴最大化方差,同时忽略类标签。LDA则与PCA不同,是一种监督降维技术,也就是说它会考虑训练集中的类信息,尝试在线性特征空间中最大化类的分割。最后,我们还学习了t-SNE,这是一种非线性特征提取技术,可用于在二维或三维中可视化数据。
配备了基本预处理技术PCA和LDA,我们为在下一章中学习有效结合各种预处理技术最佳实践及评估各种模型表现做好了准备。