序言
无监督学习包括没有已知输出、没有老师指导学习算法的各种机器学习。在无监督学习中,学习算法只有输入数据,并需要从这些数据中提取知识。
1、无监督学习类型
本章将介绍两种类型的无监督学习:数据集变换与聚类。
-
数据集的无监督变换(
unsupervised transformation
)是创建数据新的表示的算法,与数据的原始表示相比,新的表示可能更容易被人或其他机器学习算法所理解。无监督变换是一个常见应用是降维(dimensionality reduction
),它接受包含许多特征的数据的高维表示,并找到表示该数据的一种新方法,用较少的特征就能概括其重要特性。降维的一个常见应用是为了可视化将数据降为二维。无监督变换的另一个应用是找到 “构成” 数据的各个组成部分。这方面的一个例子就是对文本文档集合进行主题提取。这里的任务是找到每个文档中讨论的未知主题,并学习每个文档中出现了哪些主题。这可以追踪社交媒体上的话题讨论,比如选举、枪支管制或流行歌手等话题。
-
与之相反,聚类算法(
clustering algorithm
)将数据划分成不同的组,每组包含相似的物项。思考向社交网站上传照片的例子。为了方便你整理照片,网站可能想要将同一个人的照片分在一组。但网站并不知道每张照片是谁,也不知道你的照片集中出现了多少个人。明智的做法是提取所有的人脸,并将看起来相似的人脸分在一组。但愿这些人脸对应同一个人,这样图片的分组也就完成了。
2、无监督学习的挑战
无监督学习的一个主要挑战就是评估算法是否学到了有用的东西。无监督学习算法一般用于不包含任何标签信息的数据,所以我们不知道正确的输出应该是什么。因此很难判断一个模型是否 “表现很好”。例如,假设我们的聚类算法已经将所有的侧脸照片和所有的正面照片进行分组。这肯定是人脸照片集合的一种可能的划分方法,但并不是我们想要的那种方法。然而,我们没有办法 “告诉” 算法我们要的是什么,通常来说,评估无监督算法结果的唯一方法就是人工检查。
因此,如果数据科学家想要更好地理解数据,那么无监督算法通常可用于探索性的目的,而不是作为大型自动化系统的一部分。无监督算法的另一个常见应用是作为监督算法的预处理步骤。学习数据的一种新表示,有时可以提高监督算法的精度,或者可以减少内存占用和时间开销。
在开始学习 “真正的” 无监督算法之前,我们先简要讨论几种简单又常用的预处理方法。虽然预处理和缩放通常与监督学习算法一起使用,但缩放方法并没有用到与“监督”有关的信息,所以它是无监督的。
3、预处理与缩放
一些算法(如神经网络和 SVM)对数据缩放非常敏感。因此,通常的做法是对特征进行调节,使数据表示更适合于这些算法。通常来说,这是对数据的一种简单的按特征的缩放和移动。下面的代码给出了一个简单的例子:
mglearn.plots.plot_scaling()
3.1、不同类型的预处理
在上图中,第一张图显示的是一个模拟的有两个特征的二分类数据集。第一个特征(x 轴)位于 10 到 15 之间。第二个特征(y 轴)大约位于 1 到 9 之间。
接下来的 4 张图展示了 4 种数据变换方法,都生成了更加标准的范围:
-
scikit-learn
中的StandardScaler
确保每个特征的平均值为 0、方差为 1,使所有特征都位于同一量级。但这种缩放不能保证特征任何特定的最大值和最小值。 -
RobustScaler
的工作原理与StandardScaler
类似,确保每个特征的统计属性都位于同一范围。但RobustScaler
使用的是中位数和四分位数,而不是平均值和方差。这样RobustScaler
会忽略与其他点有很大不同的数据点(比如测量误差)。这些与众不同的数据点也叫异常值(outlier),可能会给其他缩放方法造成麻烦。对于一组数字来说,中位数指的是这样的数值 x:有一半数值小于 x,另一半数值大于 x。较小四分位数指的是这样的数值 x:有四分之一的数值小于 x。较大四分位数指的是这样的数值 x:有四分之一的数值大于 x。
-
与之相反,
MinMaxScaler
移动数据,使所有特征都刚好位于 0 到 1 之间。对于二维数据集来说,所有的数据都包含在 x 轴 0 到 1 与 y 轴 0 到 1 组成的矩形中。 -
最后,
Normalizer
用到一种完全不同的缩放方法。它对每个数据点进行缩放,使得特征向量的欧式长度等于 1。换句话说,它将一个数据点投射到半径为 1 的圆上(对于更高维度的情况,是球面)。这意味着每个数据点的缩放比例都不相同(乘以其长度的倒数)。如果只有数据的方向(或角度)是重要的,而特征向量的长度无关紧要,那么通常会使用这种归一化。
3.2、应用数据变换
前面我们已经看到不同类型的变换的作用,下面利用 scikit-learn
来应用这些变换。我们将使用 cancer
数据集。通常在应用监督学习算法之前使用预处理方法(比如缩放)。举个例子,比如我们想要将核 SVM(SVC
)应用在 cancer
数据集上,并使用 MinMaxScaler
来预处理数据。首先加载数据集并将其分为训练集和测试集(我们需要分开的训练集和数据集来对预处理后构建的监督模型进行评估):
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target,
random_state=1)
print(X_train.shape) # (426, 30)
print(X_test.shape) # (143, 30)
提醒一下,这个数据集包含 569 个数据点,每个数据点由 30 个测量值表示。我们将数据集分成包含 426 个样本的训练集与包含 143 个样本的测试集。
与之前构建的监督模型一样,我们首先导入实现预处理的类,然后将其实例化:
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
然后,使用 fit
方法拟合缩放器(scaler
),并将其应用于训练数据。对于 MinMaxScaler
来说,fit
方法计算训练集中每个特征的最大值和最小值。与分类器和回归器(regressor
)不同,在对缩放器调用 fit
时只提供了 X_train
,而不用 y_train
:
print(scaler.fit(X_train))
# MinMaxScaler(copy=True, feature_range=(0, 1))
为了应用刚刚学习的变换(即对训练数据进行实际缩放),我们使用缩放器的 transform
方法。在 scikit-learn
中,每当模型返回数据的一种新表示时,都可以使用 transform
方法:
# 变换数据
X_train_scaled = scaler.transform(X_train)
# 在缩放之前和之后分别打印数据集属性
print("transformed shape: {}".format(X_train_scaled.shape))
# transformed shape: (426, 30)
print("per-feature minimum before scaling:\n {}".format(X_train.min(axis=0)))
'''
per-feature minimum before scaling:
[6.981e+00 9.710e+00 4.379e+01 1.435e+02 5.263e-02 1.938e-02 0.000e+00
0.000e+00 1.060e-01 5.024e-02 1.153e-01 3.602e-01 7.570e-01 6.802e+00
1.713e-03 2.252e-03 0.000e+00 0.000e+00 9.539e-03 8.948e-04 7.930e+00
1.202e+01 5.041e+01 1.852e+02 7.117e-02 2.729e-02 0.000e+00 0.000e+00
1.566e-01 5.521e-02]
'''
print("per-feature maximum before scaling:\n {}".format(X_train.max(axis=0)))
'''
per-feature maximum before scaling:
[2.811e+01 3.928e+01 1.885e+02 2.501e+03 1.634e-01 2.867e-01 4.268e-01
2.012e-01 3.040e-01 9.575e-02 2.873e+00 4.885e+00 2.198e+01 5.422e+02
3.113e-02 1.354e-01 3.960e-01 5.279e-02 6.146e-02 2.984e-02 3.604e+01
4.954e+01 2.512e+02 4.254e+03 2.226e-01 9.379e-01 1.170e+00 2.910e-01
5.774e-01 1.486e-01]
'''
print("per-feature minimum after scaling:\n {}".format(
X_train_scaled.min(axis=0)))
'''
per-feature minimum after scaling:
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0.]
'''
print("per-feature maximum after scaling:\n {}".format(
X_train_scaled.max(axis=0)))
'''
per-feature maximum after scaling:
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
1. 1. 1. 1. 1. 1.]
'''
变换后的数据形状与原始数据相同,特征只是发生了移动和缩放。你可以看到,现在所有特征都位于 0 到 1 之间,这也符合我们的预期。
为了将 SVM 应用到缩放后的数据上,还需要对测试集进行变换。这可以通过对 X_test
调用 transform
方法来完成:
# 对测试数据进行变换
X_test_scaled = scaler.transform(X_test)
# 在缩放之后打印测试数据的属性
print("per-feature minimum after scaling:\n{}".format(X_test_scaled.min(axis=0)))
'''
per-feature minimum after scaling:
[ 0.0336031 0.0226581 0.03144219 0.01141039 0.14128374 0.04406704
0. 0. 0.1540404 -0.00615249 -0.00137796 0.00594501
0.00430665 0.00079567 0.03919502 0.0112206 0. 0.
-0.03191387 0.00664013 0.02660975 0.05810235 0.02031974 0.00943767
0.1094235 0.02637792 0. 0. -0.00023764 -0.00182032]
'''
print("per-feature maximum after scaling:\n{}".format(X_test_scaled.max(axis=0)))
'''
per-feature maximum after scaling:
[0.9578778 0.81501522 0.95577362 0.89353128 0.81132075 1.21958701
0.87956888 0.9333996 0.93232323 1.0371347 0.42669616 0.49765736
0.44117231 0.28371044 0.48703131 0.73863671 0.76717172 0.62928585
1.33685792 0.39057253 0.89612238 0.79317697 0.84859804 0.74488793
0.9154725 1.13188961 1.07008547 0.92371134 1.20532319 1.63068851]
'''
你可以发现,对测试集缩放后的最大值和最小值不是 1 和 0,这或许有些出乎意料。有些特征甚至在 0~1 的范围之外!对此的解释是,MinMaxScaler
(以及其他所有缩放器)总是对训练集和测试集应用完全相同的变换。也就是说,transform
方法总是减去训练集的最小值,然后除以训练集的范围,而这两个值可能与测试集的最小值和范围并不相同。
3.3、对训练数据和测试数据进行相同的缩放
为了让监督模型能够在测试集上运行,对训练集和测试集应用完全相同的变换是很重要的。如果我们使用测试集的最小值和范围,下面这个例子展示了会发生什么:
from sklearn.datasets import make_blobs
# 构造数据
X, _ = make_blobs(n_samples=50, centers=5, random_state=4, cluster_std=2)
# 将其分为训练集和测试集
X_train, X_test = train_test_split(X, random_state=5, test_size=.1)
# 绘制训练集和测试集
fig, axes = plt.subplots(1, 3, figsize=(13, 4))
axes[0].scatter(X_train[:, 0], X_train[:, 1],
c=mglearn.cm2(0), label="Training set", s=60)
axes[0].scatter(X_test[:, 0], X_test[:, 1], marker='^',
c=mglearn.cm2(1), label="Test set", s=60)
axes[0].legend(loc='upper left')
axes[0].set_title("Original Data")
# 利用MinMaxScaler缩放数据
scaler = MinMaxScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 将正确缩放的数据可视化
axes[1].scatter(X_train_scaled[:, 0], X_train_scaled[:, 1],
c=mglearn.cm2(0), label="Training set", s=60)
axes[1].scatter(X_test_scaled[:, 0], X_test_scaled[:, 1], marker='^',
c=mglearn.cm2(1), label="Test set", s=60)
axes[1].set_title("Scaled Data")
# 单独对测试集进行缩放
# 使得测试集的最小值为0,最大值为1
# 千万不要这么做!这里只是为了举例
test_scaler = MinMaxScaler()
test_scaler.fit(X_test)
X_test_scaled_badly = test_scaler.transform(X_test)
# 将错误缩放的数据可视化
axes[2].scatter(X_train_scaled[:, 0], X_train_scaled[:, 1],
c=mglearn.cm2(0), label="training set", s=60)
axes[2].scatter(X_test_scaled_badly[:, 0], X_test_scaled_badly[:, 1],
marker='^', c=mglearn.cm2(1), label="test set", s=60)
axes[2].set_title("Improperly Scaled Data")
for ax in axes:
ax.set_xlabel("Feature 0")
ax.set_ylabel("Feature 1")
第一张图是未缩放的二维数据集,其中训练集用圆形表示,测试集用三角形表示。
第二张图中是同样的数据,但使用 MinMaxScaler
缩放。这里我们调用 fit
作用在训练集上,然后调用 transform
作用在训练集和测试集上。你可以发现,第二张图中的数据集看起来与第一张图中的完全相同,只是坐标轴刻度发生了变化。现在所有特征都位于 0 到 1 之间。你还可以发现,测试数据(三角形)的特征最大值和最小值并不是 1 和 0。
第三张图展示了如果我们对训练集和测试集分别进行缩放会发生什么。在这种情况下,对训练集和测试集而言,特征的最大值和最小值都是 1 和 0。但现在数据集看起来不一样。测试集相对训练集的移动不一致,因为它们分别做了不同的缩放。我们随意改变了数据的排列。这显然不是我们想要做的事情。
再换一种思考方式,想象你的测试集只有一个点。对于一个点而言,无法将其正确地缩放以满足 MinMaxScaler
的最大值和最小值的要求。但是,测试集的大小不应该对你的处理方式有影响。
快捷方式与高效的替代方法
通常来说,你想要在某个数据集上
fit
一个模型,然后再将其transform
。这是一个非常常见的任务,通常可以用比先调用fit
再调用transform
更高效的方法来计算。对于这种使用场景,所有具有transform
方法的模型也都具有一个fit_transform
方法。下面是使用StandardScaler
的一个例子:from sklearn.preprocessing import StandardScaler scaler = StandardScaler() # 依次调用fit和transform(使用方法链) X_scaled = scaler.fit(X).transform(X) # 结果相同,但计算更加高效 X_scaled_d = scaler.fit_transform(X)
虽然
fit_transform
不一定对所有模型都更加高效,但在尝试变换训练集时,使用这一方法仍然是很好的做法。
3.4、预处理对监督学习的作用
现在我们回到 cancer
数据集,观察使用 MinMaxScaler
对学习 SVC 的作用。首先,为了对比,我们再次在原始数据上拟合 SVC:
from sklearn.svm import SVC
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target,
random_state=0)
svm = SVC(C=100)
svm.fit(X_train, y_train)
print("Test set accuracy: {:.2f}".format(svm.score(X_test, y_test)))
# Test set accuracy: 0.94
下面先用 MinMaxScaler
对数据进行缩放,然后再拟合 SVC
:
# 使用0-1缩放进行预处理
scaler = MinMaxScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 在缩放后的训练数据上学习SVM
svm.fit(X_train_scaled, y_train)
# 在缩放后的测试集上计算分数
print("Scaled test set accuracy: {:.2f}".format(
svm.score(X_test_scaled, y_test)))
# Scaled test set accuracy: 0.97
你也可以通过改变使用的类将一种预处理算法轻松替换成另一种,因为所有的预处理类都具有相同的接口,都包含 fit
和 transform
方法:
# 利用零均值和单位方差的缩放方法进行预处理
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 在缩放后的训练数据上学习SVM
svm.fit(X_train_scaled, y_train)
# 在缩放后的测试集上计算分数
print("SVM test accuracy: {:.2f}".format(svm.score(X_test_scaled, y_test)))
# SVM test accuracy: 0.96
前面我们已经看到了用于预处理的简单数据变换的工作原理,下面继续学习利用无监督学习进行更有趣的变换。