无监督学习实用指南-二-

150 阅读59分钟

无监督学习实用指南(二)

原文:annas-archive.org/md5/5d48074db68aa41a4c5eb547fcbf1a69

译者:飞龙

协议:CC BY-NC-SA 4.0

第二部分:使用 Scikit-Learn 进行无监督学习

在接下来的几章中,我们将介绍两个重要的无监督学习概念——降维和聚类——并使用它们进行异常检测和群组分割。

异常检测和群组分割在许多不同行业中都有重要的实际应用。

异常检测用于高效发现罕见事件,如欺诈、网络安全漏洞、恐怖主义、人类、武器和毒品走私、洗钱、异常交易活动、疾病爆发以及关键设备的维护故障。

群组分割使我们能够理解用户在市场营销、在线购物、音乐听取、视频观看、在线约会和社交媒体活动等领域的行为。

第三章:降维

在本章中,我们将关注构建成功应用机器学习解决方案的一个主要挑战:维度灾难。无监督学习有一个很好的对策——降维。在本章中,我们将介绍这个概念,并从那里开始,帮助你培养对其工作原理的直觉。

在 第四章中,我们将基于降维构建我们自己的无监督学习解决方案——具体来说,是一个基于无监督学习的信用卡欺诈检测系统(与我们在第二章中构建的基于有监督的系统不同)。这种无监督的欺诈检测被称为异常检测,是应用无监督学习领域一个迅速发展的领域。

但在构建异常检测系统之前,让我们在本章中介绍降维。

降维的动机

正如第一章中所提到的,降维有助于克服机器学习中最常见的问题之一——维度灾难,其中算法由于特征空间的巨大规模,无法有效和高效地在数据上训练。

降维算法将高维数据投影到低维空间,同时尽可能保留重要信息,去除冗余信息。一旦数据进入低维空间,机器学习算法能够更有效、更高效地识别有趣的模式,因为噪声已经被大大减少。

有时,降维本身就是目标——例如,构建异常检测系统,我们将在下一章中展示。

其他时候,降维不是一个终点,而是达到另一个终点的手段。例如,降维通常是机器学习管道的一部分,帮助解决涉及图像、视频、语音和文本的大规模、计算密集型问题。

MNIST 手写数字数据库

在介绍降维算法之前,让我们先探索一下本章将使用的数据集。我们将使用一个简单的计算机视觉数据集:MNIST(美国国家标准与技术研究院)手写数字数据库,这是机器学习中最为人知的数据集之一。我们将使用 Yann LeCun 网站上公开的 MNIST 数据集版本。¹ 为了方便起见,我们将使用deeplearning.net提供的 pickle 版本。²

这个数据集已被分为三个部分——一个包含 50,000 个例子的训练集,一个包含 10,000 个例子的验证集和一个包含 10,000 个例子的测试集。我们为所有例子都有标签。

该数据集由手写数字的 28x28 像素图像组成。每个数据点(即每个图像)可以表示为一组数字的数组,其中每个数字描述每个像素的暗度。换句话说,一个 28x28 的数字数组对应于一个 28x28 像素的图像。

为了简化起见,我们可以将每个数组展平为一个 28x28 或 784 维度的向量。向量的每个分量是介于零和一之间的浮点数——表示图像中每个像素的强度。零表示黑色,一表示白色。标签是介于零和九之间的数字,指示图像表示的数字。

数据获取和探索

在我们使用降维算法之前,让我们加载将要使用的库:

# Import libraries
'''Main'''
import numpy as np
import pandas as pd
import os, time
import pickle, gzip

'''Data Viz'''
import matplotlib.pyplot as plt
import seaborn as sns
color = sns.color_palette()
import matplotlib as mpl

%matplotlib inline

'''Data Prep and Model Evaluation'''
from sklearn import preprocessing as pp
from scipy.stats import pearsonr
from numpy.testing import assert_array_almost_equal
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import log_loss
from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import roc_curve, auc, roc_auc_score
from sklearn.metrics import confusion_matrix, classification_report

'''Algos'''
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
import xgboost as xgb
import lightgbm as lgb

加载 MNIST 数据集

现在让我们加载 MNIST 数据集:

# Load the datasets
current_path = os.getcwd()
file = '\\datasets\\mnist_data\\mnist.pkl.gz'

f = gzip.open(current_path+file, 'rb')
train_set, validation_set, test_set = pickle.load(f, encoding='latin1')
f.close()

X_train, y_train = train_set[0], train_set[1]
X_validation, y_validation = validation_set[0], validation_set[1]
X_test, y_test = test_set[0], test_set[1]

验证数据集的形状

让我们验证数据集的形状,以确保它们已正确加载:

# Verify shape of datasets
print("Shape of X_train: ", X_train.shape)
print("Shape of y_train: ", y_train.shape)
print("Shape of X_validation: ", X_validation.shape)
print("Shape of y_validation: ", y_validation.shape)
print("Shape of X_test: ", X_test.shape)
print("Shape of y_test: ", y_test.shape)

以下代码确认了数据集的形状与预期相符:

Shape of X_train:       (50000, 784)
Shape of y_train:       (50000,)
Shape of X_validation:  (10000, 784)
Shape of y_validation:  (10000,)
Shape of X_test:        (10000, 784)
Shape of y_test:        (10000,)

从数据集创建 Pandas DataFrames

让我们将 numpy 数组转换为 Pandas DataFrames,以便更容易进行探索和处理:

# Create Pandas DataFrames from the datasets
train_index = range(0,len(X_train))
validation_index = range(len(X_train), /
                         len(X_train)+len(X_validation))
test_index = range(len(X_train)+len(X_validation), /
                   len(X_train)+len(X_validation)+len(X_test))

X_train = pd.DataFrame(data=X_train,index=train_index)
y_train = pd.Series(data=y_train,index=train_index)

X_validation = pd.DataFrame(data=X_validation,index=validation_index)
y_validation = pd.Series(data=y_validation,index=validation_index)

X_test = pd.DataFrame(data=X_test,index=test_index)
y_test = pd.Series(data=y_test,index=test_index)

探索数据

让我们生成数据的摘要视图:

# Describe the training matrix
X_train.describe()

表格 3-1 显示了图像数据的摘要视图。许多数值为零——换句话说,图像中的大多数像素是黑色的。这是有道理的,因为数字是白色的,显示在黑色背景的中央。

表格 3-1. 数据探索

0123456
计数50000.050000.050000.050000.050000.050000.050000.0
平均值0.00.00.00.00.00.00.0
标准差0.00.00.00.00.00.00.0
最小值0.00.00.00.00.00.00.0
25%0.00.00.00.00.00.00.0
50%0.00.00.00.00.00.00.0
75%0.00.00.00.00.00.00.0
最大0.00.00.00.00.00.00.0
8 行 x 784 列

标签数据是一个表示图像中实际内容的一维向量。前几个图像的标签如下:

# Show the labels
y_train.head()
  0   5
  1   0
  2   4
  3   1
  4   9
  dtype: int64

显示图像

让我们定义一个函数来查看图像及其标签:

def view_digit(example):
    label = y_train.loc[0]
    image = X_train.loc[example,:].values.reshape([28,28])
    plt.title('Example: %d Label: %d' % (example, label))
    plt.imshow(image, cmap=plt.get_cmap('gray'))
    plt.show()

第一个图像的视图——一旦将 784 维向量重塑为 28 x 28 像素图像——显示数字五(图 3-1)。

查看第一个数字

图 3-1. 第一个数字的视图

降维算法

现在我们已经加载并探索了 MNIST 数字数据集,让我们转向降维算法。对于每个算法,我们将首先介绍概念,然后通过将算法应用于 MNIST 数字数据集来深入理解。

线性投影 vs 流形学习

降维有两大主要分支。第一种被称为线性投影,它涉及将数据从高维空间线性投影到低维空间。这包括主成分分析,奇异值分解随机投影等技术。

第二种被称为流形学习,也被称为非线性降维。这涉及技术,如isomap,它学习点之间的曲线距离(也称为测地距离),而不是欧氏距离。其他技术包括多维缩放(MDS),局部线性嵌入(LLE),t 分布随机近邻嵌入(t-SNE),字典学习,随机树嵌入独立成分分析

主成分分析

我们将探讨几个 PCA 版本,包括标准 PCA,增量 PCA,稀疏 PCA 和核 PCA。

PCA,概念

让我们从标准 PCA 开始,这是最常见的线性降维技术之一。在 PCA 中,算法找到数据的低维表示,同时尽可能保留尽可能多的变化(即显著信息)。

PCA 通过处理特征之间的相关性来实现这一点。如果一组特征之间的相关性非常高,PCA 将尝试合并高度相关的特征,并用较少数量的线性不相关特征表示这些数据。该算法持续执行这种相关性减少,找到原始高维数据中方差最大的方向,并将它们投影到较小维度的空间中。这些新导出的成分称为主成分。

有了这些成分,可以重构原始特征,虽然不完全准确但一般足够接近。PCA 算法在寻找最优成分期间积极尝试最小化重构误差。

在我们的 MNIST 示例中,原始特征空间有 784 维,称为d维。PCA 将数据投影到较小的k维子空间(其中k < d),同时尽可能保留关键信息。这k个维度称为主成分。

我们留下的有意义的主成分数量远远小于原始数据集中的维数。通过转移到这个低维空间,我们会失去一些方差(即信息),但数据的基本结构更容易识别,使我们能够更有效地执行异常检测和聚类等任务。

此外,通过减少数据的维数,PCA 将减少数据的大小,进一步提高机器学习管道中后续阶段(例如图像分类等任务)的性能。

注意

在运行 PCA 之前执行特征缩放非常重要。PCA 对原始特征的相对范围非常敏感。通常,我们必须缩放数据以确保特征处于相同的相对范围。然而,对于我们的 MNIST 数字数据集,特征已经缩放到 0 到 1 的范围,因此我们可以跳过这一步。

PCA 实践

现在您对 PCA 工作原理有了更好的掌握,让我们将 PCA 应用于 MNIST 数字数据集,并看看 PCA 如何在将数据从原始的 784 维空间投影到较低维空间时捕获数字的最显著信息。

设置超参数

让我们为 PCA 算法设置超参数:

from sklearn.decomposition import PCA

n_components = 784
whiten = False
random_state = 2018

pca = PCA(n_components=n_components, whiten=whiten, \
          random_state=random_state)

应用 PCA

我们将主成分的数量设置为原始维数(即 784)。然后,PCA 将从原始维度捕获显著信息并开始生成主成分。生成这些组件后,我们将确定需要多少个主成分才能有效地捕获原始特征集中大部分的方差/信息。

让我们拟合并转换我们的训练数据,生成这些主成分:

X_train_PCA = pca.fit_transform(X_train)
X_train_PCA = pd.DataFrame(data=X_train_PCA, index=train_index)

评估 PCA

因为我们完全没有降低维度(只是转换了数据),所以由 784 个主成分捕获的原始数据的方差/信息应为 100%:

# Percentage of Variance Captured by 784 principal components
print("Variance Explained by all 784 principal components: ", \
      sum(pca.explained_variance_ratio_))
Variance Explained by all 784 principal components: 0.9999999999999997

然而,需要注意的是 784 个主成分的重要性差异相当大。这里总结了前 X 个主成分的重要性:

# Percentage of Variance Captured by X principal components
importanceOfPrincipalComponents = \
    pd.DataFrame(data=pca.explained_variance_ratio_)
importanceOfPrincipalComponents = importanceOfPrincipalComponents.T

print('Variance Captured by First 10 Principal Components: ',
      importanceOfPrincipalComponents.loc[:,0:9].sum(axis=1).values)
print('Variance Captured by First 20 Principal Components: ',
      importanceOfPrincipalComponents.loc[:,0:19].sum(axis=1).values)
print('Variance Captured by First 50 Principal Components: ',
      importanceOfPrincipalComponents.loc[:,0:49].sum(axis=1).values)
print('Variance Captured by First 100 Principal Components: ',
      importanceOfPrincipalComponents.loc[:,0:99].sum(axis=1).values)
print('Variance Captured by First 200 Principal Components: ',
      importanceOfPrincipalComponents.loc[:,0:199].sum(axis=1).values)
print('Variance Captured by First 300 Principal Components: ',
      importanceOfPrincipalComponents.loc[:,0:299].sum(axis=1).values)
Variance Captured by First 10 Principal Components: [0.48876238]
Variance Captured by First 20 Principal Components: [0.64398025]
Variance Captured by First 50 Principal Components: [0.8248609]
Variance Captured by First 100 Principal Components: [0.91465857]
Variance Captured by First 200 Principal Components: [0.96650076]
Variance Captured by First 300 Principal Components: [0.9862489]

前 10 个组件总共捕获了大约 50% 的方差,前一百个组件超过了 90%,前三百个组件几乎捕获了 99% 的方差;其余主成分中的信息几乎可以忽略不计。

我们还可以绘制每个主成分的重要性,从第一个主成分到最后一个主成分进行排名。为了便于阅读,只显示了前 10 个组件在 图 3-2 中。

现在 PCA 的力量应该更加明显了。仅使用前两百个主成分(远少于原始的 784 维度),我们就捕获了超过 96% 的方差/信息。

PCA 允许我们大幅减少原始数据的维度,同时保留大部分显著信息。在 PCA 减少的特征集上,其他机器学习算法——在机器学习流水线中的下游——将更容易在空间中分离数据点(执行异常检测和聚类等任务),并且需要更少的计算资源。

PCA 组件的重要性

图 3-2. PCA 组件的重要性

可视化空间中点的分离

为了展示 PCA 高效、简洁地捕捉数据中的方差/信息的能力,让我们在二维空间中绘制这些观察结果。具体来说,我们将展示第一和第二主成分的散点图,并用真实标签标记这些观察结果。我们将称这个函数为scatterPlot,因为接下来我们还需要为其他维度算法呈现可视化效果。

def scatterPlot(xDF, yDF, algoName):
    tempDF = pd.DataFrame(data=xDF.loc[:,0:1], index=xDF.index)
    tempDF = pd.concat((tempDF,yDF), axis=1, join="inner")
    tempDF.columns = ["First Vector", "Second Vector", "Label"]
    sns.lmplot(x="First Vector", y="Second Vector", hue="Label", \
               data=tempDF, fit_reg=False)
    ax = plt.gca()
    ax.set_title("Separation of Observations using "+algoName)

scatterPlot(X_train_PCA, y_train, "PCA")

如图 3-3 所示,仅使用前两个主成分,PCA 能够有效地将空间中的点分离开来,使相似的点通常比其他不相似的点更靠近。换句话说,相同数字的图像彼此之间比与其他数字的图像更接近。

PCA 可以在完全不使用标签的情况下完成这一任务。这展示了无监督学习捕捉数据潜在结构的能力,帮助在没有标签的情况下发现隐藏的模式。

使用 PCA 分离观察结果

图 3-3. 使用 PCA 分离观察结果

如果我们使用原始的 784 个特征集中最重要的两个特征(通过训练监督学习模型确定),运行相同的二维散点图,最多也只能得到很差的分离效果(见图 3-4)。

不使用 PCA 分离观察结果

图 3-4. 不使用 PCA 分离观察结果

比较图 3-3 和图 3-4,可以看出 PCA 在学习数据集潜在结构方面的强大能力,完全不使用任何标签——即使仅使用两个维度,我们也可以开始有意义地通过显示的数字分离图像。

注意

PCA 不仅帮助分离数据以便更容易发现隐藏模式,还有助于减少特征集的大小,从而在训练机器学习模型时节省时间和计算资源。

对于 MNIST 数据集来说,由于数据集非常小——仅有 784 个特征和 50,000 个观察结果,因此减少训练时间的效果可能很有限。但如果数据集拥有数百万个特征和数十亿个观察结果,降维将大大减少后续机器学习管道中机器学习算法的训练时间。

最后,PCA 通常会丢弃原始特征集中的一些信息,但它会明智地捕获最重要的元素并丢弃不太有价值的元素。基于 PCA 减少特征集训练的模型在准确性上可能不如基于完整特征集训练的模型表现得好,但训练和预测时间会快得多。这是在选择是否在机器学习产品中使用降维时必须考虑的重要权衡之一。

增量 PCA

对于无法全部存入内存的大型数据集,我们可以逐批次增量地执行 PCA,其中每个批次都能放入内存中。批处理大小可以手动设置或自动确定。这种基于批处理的 PCA 形式称为增量 PCA。PCA 和增量 PCA 的生成主成分通常非常相似(图 3-5)。以下是增量 PCA 的代码:

# Incremental PCA
from sklearn.decomposition import IncrementalPCA

n_components = 784
batch_size = None

incrementalPCA = IncrementalPCA(n_components=n_components, \
                                batch_size=batch_size)

X_train_incrementalPCA = incrementalPCA.fit_transform(X_train)
X_train_incrementalPCA = \
    pd.DataFrame(data=X_train_incrementalPCA, index=train_index)

X_validation_incrementalPCA = incrementalPCA.transform(X_validation)
X_validation_incrementalPCA = \
    pd.DataFrame(data=X_validation_incrementalPCA, index=validation_index)

scatterPlot(X_train_incrementalPCA, y_train, "Incremental PCA")

使用增量 PCA 分离观测结果

图 3-5. 使用增量 PCA 分离观测结果

稀疏 PCA

普通 PCA 算法在所有输入变量中搜索线性组合,尽可能紧凑地减少原始特征空间。但对于某些机器学习问题,可能更倾向于一定程度的稀疏性。保留一定程度稀疏性的 PCA 版本,由名为alpha的超参数控制,称为稀疏 PCA。稀疏 PCA 算法仅在部分输入变量中搜索线性组合,将原始特征空间减少到一定程度,但不像普通 PCA 那样紧凑。

因为这种算法的训练速度比普通 PCA 稍慢,所以我们将仅在训练集中的前 10,000 个示例上进行训练(总共有 50,000 个示例)。当算法的训练时间较慢时,我们会继续采用在少于总观测数的情况下进行训练的做法。

对于我们的目的(即开发这些降维算法如何工作的直觉),减少训练过程是可以接受的。为了获得更好的解决方案,建议在完整的训练集上进行训练:

# Sparse PCA
from sklearn.decomposition import SparsePCA

n_components = 100
alpha = 0.0001
random_state = 2018
n_jobs = -1

sparsePCA = SparsePCA(n_components=n_components, \
                alpha=alpha, random_state=random_state, n_jobs=n_jobs)

sparsePCA.fit(X_train.loc[:10000,:])
X_train_sparsePCA = sparsePCA.transform(X_train)
X_train_sparsePCA = pd.DataFrame(data=X_train_sparsePCA, index=train_index)

X_validation_sparsePCA = sparsePCA.transform(X_validation)
X_validation_sparsePCA = \
    pd.DataFrame(data=X_validation_sparsePCA, index=validation_index)

scatterPlot(X_train_sparsePCA, y_train, "Sparse PCA")

图 3-6 展示了使用稀疏 PCA 的前两个主成分的二维散点图。

使用稀疏 PCA 分离观测结果

图 3-6. 使用稀疏 PCA 分离观测结果

注意,这个散点图看起来与普通 PCA 的不同,正如预期的那样。普通和稀疏 PCA 生成主成分的方式不同,点的分离也有所不同。

核 PCA

正常 PCA、增量 PCA 和稀疏 PCA 将原始数据线性投影到较低维度空间,但也有一种非线性形式的 PCA 称为核 PCA,它在原始数据点对上运行相似度函数以执行非线性降维。

通过学习这个相似度函数(称为核方法),核 PCA 映射了大部分数据点所在的隐式特征空间,并在比原始特征集中的维度小得多的空间中创建了这个隐式特征空间。当原始特征集不是线性可分时,这种方法特别有效。

对于核 PCA 算法,我们需要设置所需的组件数、核类型和核系数,称为gamma。最流行的核是径向基函数核,更常被称为RBF 核。这是我们将在这里使用的核心:

# Kernel PCA
from sklearn.decomposition import KernelPCA

n_components = 100
kernel = 'rbf'
gamma = None
random_state = 2018
n_jobs = 1

kernelPCA = KernelPCA(n_components=n_components, kernel=kernel, \
                      gamma=gamma, n_jobs=n_jobs, random_state=random_state)

kernelPCA.fit(X_train.loc[:10000,:])
X_train_kernelPCA = kernelPCA.transform(X_train)
X_train_kernelPCA = pd.DataFrame(data=X_train_kernelPCA,index=train_index)

X_validation_kernelPCA = kernelPCA.transform(X_validation)
X_validation_kernelPCA = \
    pd.DataFrame(data=X_validation_kernelPCA, index=validation_index)

scatterPlot(X_train_kernelPCA, y_train, "Kernel PCA")

核 PCA 的二维散点图与我们的 MNIST 数字数据集的线性 PCA 几乎相同(图 3-7)。学习 RBF 核并不改善降维。

使用核 PCA 进行观测分离

图 3-7. 使用核 PCA 进行观测分离

奇异值分解

学习数据的潜在结构的另一种方法是将特征的原始矩阵的秩降低到一个较小的秩,以便可以使用较小秩矩阵中某些向量的线性组合重新创建原始矩阵。这被称为奇异值分解(SVD)

为了生成较小秩矩阵,SVD 保留原始矩阵中具有最多信息的向量(即最高的奇异值)。较小秩矩阵捕获了原始特征空间的最重要元素。

这与 PCA 非常相似。PCA 使用协方差矩阵的特征值分解来进行降维。奇异值分解(SVD)使用奇异值分解,正如其名称所示。事实上,PCA 在其计算中使用了 SVD,但本书的大部分讨论超出了此范围。

这是 SVD 的工作原理:

# Singular Value Decomposition
from sklearn.decomposition import TruncatedSVD

n_components = 200
algorithm = 'randomized'
n_iter = 5
random_state = 2018

svd = TruncatedSVD(n_components=n_components, algorithm=algorithm, \
                   n_iter=n_iter, random_state=random_state)

X_train_svd = svd.fit_transform(X_train)
X_train_svd = pd.DataFrame(data=X_train_svd, index=train_index)

X_validation_svd = svd.transform(X_validation)
X_validation_svd = pd.DataFrame(data=X_validation_svd, index=validation_index)

scatterPlot(X_train_svd, y_train, "Singular Value Decomposition")

图 3-8 显示了我们使用 SVD 的两个最重要向量实现的点的分离。

使用 SVD 进行观测分离

图 3-8. 使用 SVD 进行观测分离

随机投影

另一种线性降维技术是随机投影,它依赖于Johnson–Lindenstrauss 引理。根据 Johnson–Lindenstrauss 引理,高维空间中的点可以嵌入到一个远低于其维度的空间中,以便点之间的距离几乎保持不变。换句话说,即使从高维空间移动到低维空间,原始特征集的相关结构也得到保留。

高斯随机投影

随机投影有两个版本——标准版本称为高斯随机投影,稀疏版本称为稀疏随机投影

对于高斯随机投影,我们可以指定我们希望在降维特征空间中拥有的组件数量,或者我们可以设置超参数eps。eps 控制嵌入的质量,根据 Johnson–Lindenstrauss 引理,较小的值会生成更多的维度。在我们的情况下,我们将设置这个超参数:

# Gaussian Random Projection
from sklearn.random_projection import GaussianRandomProjection

n_components = 'auto'
eps = 0.5
random_state = 2018

GRP = GaussianRandomProjection(n_components=n_components, eps=eps, \
                               random_state=random_state)

X_train_GRP = GRP.fit_transform(X_train)
X_train_GRP = pd.DataFrame(data=X_train_GRP, index=train_index)

X_validation_GRP = GRP.transform(X_validation)
X_validation_GRP = pd.DataFrame(data=X_validation_GRP, index=validation_index)

scatterPlot(X_train_GRP, y_train, "Gaussian Random Projection")

图 3-9 显示了使用高斯随机投影的二维散点图。

使用高斯随机投影分离观测

图 3-9. 使用高斯随机投影分离观测

尽管随机投影与 PCA 一样都是一种线性投影形式,但随机投影是一种完全不同的降维方法家族。因此,随机投影的散点图看起来与普通 PCA、增量 PCA、稀疏 PCA 和核 PCA 的散点图非常不同。

稀疏随机投影

正如 PCA 有稀疏版本一样,随机投影也有稀疏版本,称为稀疏随机投影。它在转换后的特征集中保留了一定程度的稀疏性,并且通常比普通的高斯随机投影更高效,能够更快地将原始数据转换为降维空间:

# Sparse Random Projection
from sklearn.random_projection import SparseRandomProjection

n_components = 'auto'
density = 'auto'
eps = 0.5
dense_output = False
random_state = 2018

SRP = SparseRandomProjection(n_components=n_components, \
        density=density, eps=eps, dense_output=dense_output, \
        random_state=random_state)

X_train_SRP = SRP.fit_transform(X_train)
X_train_SRP = pd.DataFrame(data=X_train_SRP, index=train_index)

X_validation_SRP = SRP.transform(X_validation)
X_validation_SRP = pd.DataFrame(data=X_validation_SRP, index=validation_index)

scatterPlot(X_train_SRP, y_train, "Sparse Random Projection")

图 3-10 显示了使用稀疏随机投影的二维散点图。

使用稀疏随机投影分离观测

图 3-10. 使用稀疏随机投影分离观测

Isomap

与其线性投影高维空间到低维空间的数据,我们可以使用非线性降维方法。这些方法统称为流形学习。

流形学习最基本的形式被称为等距映射,简称Isomap。与核 PCA 类似,Isomap 通过计算所有点的成对距离来学习原始特征集的新的低维嵌入,其中距离是曲线测地距离,而不是欧氏距离。换句话说,它基于每个点相对于流形上邻近点的位置学习原始数据的内在几何结构:

# Isomap

from sklearn.manifold import Isomap

n_neighbors = 5
n_components = 10
n_jobs = 4

isomap = Isomap(n_neighbors=n_neighbors, \
                n_components=n_components, n_jobs=n_jobs)

isomap.fit(X_train.loc[0:5000,:])
X_train_isomap = isomap.transform(X_train)
X_train_isomap = pd.DataFrame(data=X_train_isomap, index=train_index)

X_validation_isomap = isomap.transform(X_validation)
X_validation_isomap = pd.DataFrame(data=X_validation_isomap, \
                                   index=validation_index)

scatterPlot(X_train_isomap, y_train, "Isomap")

图 3-11 显示了使用 Isomap 的二维散点图。

使用 Isomap 分离观察结果

图 3-11. 使用 isomap 分离观察结果

多维缩放

*多维缩放(MDS)*是一种非线性降维形式,它学习原始数据集中点的相似性,并利用这种相似性在较低维度空间中进行建模:

# Multidimensional Scaling
from sklearn.manifold import MDS

n_components = 2
n_init = 12
max_iter = 1200
metric = True
n_jobs = 4
random_state = 2018

mds = MDS(n_components=n_components, n_init=n_init, max_iter=max_iter, \
          metric=metric, n_jobs=n_jobs, random_state=random_state)

X_train_mds = mds.fit_transform(X_train.loc[0:1000,:])
X_train_mds = pd.DataFrame(data=X_train_mds, index=train_index[0:1001])

scatterPlot(X_train_mds, y_train, "Multidimensional Scaling")

图 3-12 显示了使用 MDS 的二维散点图。

使用 MDS 分离观察结果

图 3-12. 使用 MDS 分离观察结果

局部线性嵌入

另一种流行的非线性降维方法称为局部线性嵌入(LLE)。该方法在将数据从原始特征空间投影到降维空间时保持了局部邻域内的距离。LLE 通过将数据分段成较小的组件(即点的邻域)并将每个组件建模为线性嵌入,发现了原始高维数据中的非线性结构。

对于该算法,我们设置我们期望的组件数量和在给定邻域中考虑的点数:

# Locally Linear Embedding (LLE)
from sklearn.manifold import LocallyLinearEmbedding

n_neighbors = 10
n_components = 2
method = 'modified'
n_jobs = 4
random_state = 2018

lle = LocallyLinearEmbedding(n_neighbors=n_neighbors, \
        n_components=n_components, method=method, \
        random_state=random_state, n_jobs=n_jobs)

lle.fit(X_train.loc[0:5000,:])
X_train_lle = lle.transform(X_train)
X_train_lle = pd.DataFrame(data=X_train_lle, index=train_index)

X_validation_lle = lle.transform(X_validation)
X_validation_lle = pd.DataFrame(data=X_validation_lle, index=validation_index)

scatterPlot(X_train_lle, y_train, "Locally Linear Embedding")

图 3-13 显示了使用 LLE 的二维散点图。

使用 LLE 分离观察结果

图 3-13. 使用 LLE 分离观察结果

t-分布随机邻域嵌入

t-分布随机邻域嵌入(t-SNE)是一种非线性降维技术,用于可视化高维数据。t-SNE 通过将每个高维点建模到二维或三维空间中来实现这一目标,使得相似的点模型接近,而不相似的点则模型远离。它通过构建两个概率分布实现此目标,一个是在高维空间中点对的概率分布,另一个是在低维空间中点对的概率分布,使得相似的点具有较高的概率,而不相似的点具有较低的概率。具体来说,t-SNE 最小化了两个概率分布之间的Kullback–Leibler 散度

在 t-SNE 的实际应用中,最好在应用 t-SNE 之前使用另一种降维技术(例如 PCA,正如我们在此处所做的那样)来减少维数。通过先应用另一种降维方法,我们可以减少馈入 t-SNE 的特征中的噪音,并加快算法的计算速度:

# t-SNE
from sklearn.manifold import TSNE

n_components = 2
learning_rate = 300
perplexity = 30
early_exaggeration = 12
init = 'random'
random_state = 2018

tSNE = TSNE(n_components=n_components, learning_rate=learning_rate, \
            perplexity=perplexity, early_exaggeration=early_exaggeration, \
            init=init, random_state=random_state)

X_train_tSNE = tSNE.fit_transform(X_train_PCA.loc[:5000,:9])
X_train_tSNE = pd.DataFrame(data=X_train_tSNE, index=train_index[:5001])

scatterPlot(X_train_tSNE, y_train, "t-SNE")
注意

t-SNE 具有非凸成本函数,这意味着算法的不同初始化会生成不同的结果。不存在稳定的解决方案。

图 3-14 显示了 t-SNE 的二维散点图。

使用 t-SNE 分离观察结果

图 3-14. 使用 t-SNE 进行观察分离

其他降维方法

我们已经涵盖了线性和非线性形式的降维。现在我们将转向不依赖任何几何或距离度量的方法。

字典学习

其中一种方法是字典学习,它学习原始数据的稀疏表示。生成的矩阵称为字典,字典中的向量称为原子。这些原子是简单的二进制向量,由零和一填充。原始数据中的每个实例可以被重构为这些原子的加权和。

假设原始数据中有d个特征和n个字典原子,我们可以有一个欠完备字典,其中n < d,或过完备字典,其中n > d。欠完备字典实现了降维,用较少的向量表示原始数据,这是我们将专注的内容。³

我们将在我们的数字数据集上应用字典学习的小批量版本。与其他降维方法一样,我们将设置成分的数量。我们还将设置批量大小和执行训练的迭代次数。

由于我们想要使用二维散点图来可视化图像,我们将学习一个非常密集的字典,但实际上,我们会使用一个更稀疏的版本:

# Mini-batch dictionary learning

from sklearn.decomposition import MiniBatchDictionaryLearning

n_components = 50
alpha = 1
batch_size = 200
n_iter = 25
random_state = 2018

miniBatchDictLearning = MiniBatchDictionaryLearning( \
                        n_components=n_components, alpha=alpha, \
                        batch_size=batch_size, n_iter=n_iter, \
                        random_state=random_state)

miniBatchDictLearning.fit(X_train.loc[:,:10000])
X_train_miniBatchDictLearning = miniBatchDictLearning.fit_transform(X_train)
X_train_miniBatchDictLearning = pd.DataFrame( \
    data=X_train_miniBatchDictLearning, index=train_index)

X_validation_miniBatchDictLearning = \
    miniBatchDictLearning.transform(X_validation)
X_validation_miniBatchDictLearning = \
    pd.DataFrame(data=X_validation_miniBatchDictLearning, \
    index=validation_index)

scatterPlot(X_train_miniBatchDictLearning, y_train, \
            "Mini-batch Dictionary Learning")

图 3-15 展示了使用字典学习的二维散点图。

使用字典学习进行观察分离

图 3-15. 使用字典学习进行观察分离

独立成分分析

无标签数据的一个常见问题是,有许多独立信号嵌入到我们给定的特征中。使用独立成分分析(ICA),我们可以将这些混合信号分离成它们的各个组成部分。分离完成后,我们可以通过组合生成的各个个体成分的某些组合来重建任何原始特征。ICA 在信号处理任务中广泛应用(例如,在繁忙咖啡馆音频剪辑中识别各个声音)。

以下展示了 ICA 的工作原理:

# Independent Component Analysis
from sklearn.decomposition import FastICA

n_components = 25
algorithm = 'parallel'
whiten = True
max_iter = 100
random_state = 2018

fastICA = FastICA(n_components=n_components, algorithm=algorithm, \
                  whiten=whiten, max_iter=max_iter, random_state=random_state)

X_train_fastICA = fastICA.fit_transform(X_train)
X_train_fastICA = pd.DataFrame(data=X_train_fastICA, index=train_index)

X_validation_fastICA = fastICA.transform(X_validation)
X_validation_fastICA = pd.DataFrame(data=X_validation_fastICA, \
                                    index=validation_index)

scatterPlot(X_train_fastICA, y_train, "Independent Component Analysis")

图 3-16 展示了使用 ICA 的二维散点图。

使用独立成分分析进行观察分离

图 3-16. 使用独立成分分析进行观察分离

结论

在本章中,我们介绍并探讨了多种降维算法,从线性方法如 PCA 和随机投影开始。然后,我们转向非线性方法——也称为流形学习,例如 Isomap、多维尺度分析、LLE 和 t-SNE。我们还涵盖了非基于距离的方法,如字典学习和 ICA。

降维捕捉数据集中最显著的信息,并通过学习数据的潜在结构将其压缩到少量维度,而无需使用任何标签。通过将这些算法应用于 MNIST 数字数据集,我们能够仅使用前两个维度基于其所代表的数字有效地分离图像。

这突显了降维的强大能力。

在 第四章,我们将使用这些降维算法构建一个应用型的无监督学习解决方案。具体来说,我们将重新审视在 第二章 中介绍的欺诈检测问题,并尝试在不使用标签的情况下将欺诈交易与正常交易分离开来。

¹ 手写数字 MNIST 数据库,由 Yann Lecun 提供。

² MNIST 数据集的 Pickled 版本,由 deeplearning.net 提供。

³ 过完备字典有不同的用途,例如图像压缩。

第四章:异常检测

在 第三章 中,我们介绍了核心的降维算法,并探讨了它们在 MNIST 数字数据库中以显著较少的维度捕获最显著信息的能力。即使在仅两个维度下,这些算法也能有意义地分离数字,而无需使用标签。这就是无监督学习算法的力量 — 它们能够学习数据的潜在结构,并在缺乏标签的情况下帮助发现隐藏的模式。

让我们使用这些降维方法构建一个应用的机器学习解决方案。我们将回顾在 第二章 中介绍的问题,并构建一个无需使用标签的信用卡欺诈检测系统。

在现实世界中,欺诈通常不会被发现,只有被捕获的欺诈行为提供了数据集的标签。此外,欺诈模式随时间变化,因此使用欺诈标签构建的监督系统(如我们在 第二章 中构建的系统)变得过时,捕捉到的是历史上的欺诈模式,而不能适应新出现的欺诈模式。

出于这些原因(标签不足和尽快适应新出现的欺诈模式的需求),无监督学习欺诈检测系统备受青睐。

在本章中,我们将使用前一章探索的一些降维算法来构建这样一个解决方案。

信用卡欺诈检测

让我们重新访问来自 第二章 的信用卡交易问题。

准备数据

就像我们在 第二章 中所做的那样,让我们加载信用卡交易数据集,生成特征矩阵和标签数组,并将数据拆分为训练集和测试集。我们不会使用标签来执行异常检测,但我们将使用标签来帮助评估我们构建的欺诈检测系统。

提醒一下,总共有 284,807 笔信用卡交易,其中 492 笔是欺诈交易,具有正面(欺诈)标签为一。其余的是正常交易,具有负面(非欺诈)标签为零。

我们有 30 个特征用于异常检测 — 时间、金额和 28 个主成分。然后,我们将数据集分为一个训练集(包含 190,820 笔交易和 330 笔欺诈案例)和一个测试集(剩余 93,987 笔交易和 162 笔欺诈案例):

# Load datasets
current_path = os.getcwd()
file = '\\datasets\\credit_card_data\\credit_card.csv'
data = pd.read_csv(current_path + file)

dataX = data.copy().drop(['Class'],axis=1)
dataY = data['Class'].copy()

featuresToScale = dataX.columns
sX = pp.StandardScaler(copy=True)
dataX.loc[:,featuresToScale] = sX.fit_transform(dataX[featuresToScale])

X_train, X_test, y_train, y_test = \
    train_test_split(dataX, dataY, test_size=0.33, \
                    random_state=2018, stratify=dataY)

定义异常分数函数

接下来,我们需要定义一个函数来计算每笔交易的异常程度。交易越异常,它被识别为欺诈的可能性就越大,假设欺诈很少且看起来与大多数正常交易不同。

正如我们在前一章讨论的那样,降维算法在试图最小化重建误差的同时减少数据的维度。换句话说,这些算法试图以尽可能好的方式捕捉原始特征的最显著信息,以便从降维特征集重建原始特征集。然而,这些降维算法不能捕捉所有原始特征的信息,因为它们移动到较低维空间;因此,在将这些算法从降维特征集重建回原始维数时会存在一些误差。

在我们的信用卡交易数据集的背景下,算法将在最难建模的交易中产生最大的重建误差——换句话说,那些发生最少且最异常的交易。由于欺诈很少且可能与正常交易不同,欺诈交易应该表现出最大的重建误差。因此,让我们将异常分数定义为重建误差。每笔交易的重建误差是原始特征矩阵与使用降维算法重建的矩阵之间差异平方和。我们将通过整个数据集的差异平方和的最大-最小范围来缩放差异平方和的总和,以使所有重建误差都在零到一的范围内。

具有最大差异平方和的交易将具有接近一的误差,而具有最小差异平方和的交易将具有接近零的误差。

这应该是熟悉的。就像我们在第二章中构建的监督式欺诈检测解决方案一样,降维算法将有效地为每笔交易分配一个介于零和一之间的异常分数。零表示正常,一表示异常(最有可能是欺诈)。

这里是函数:

def anomalyScores(originalDF, reducedDF):
    loss = np.sum((np.array(originalDF)-np.array(reducedDF))**2, axis=1)
    loss = pd.Series(data=loss,index=originalDF.index)
    loss = (loss-np.min(loss))/(np.max(loss)-np.min(loss))
    return loss

定义评估指标

尽管我们不会使用欺诈标签来构建无监督的欺诈检测解决方案,但我们将使用这些标签来评估我们开发的无监督解决方案。这些标签将帮助我们了解这些解决方案捕捉已知欺诈模式的效果如何。

就像我们在第二章中所做的那样,我们将使用精确-召回曲线、平均精度和 auROC 作为我们的评估指标。

这里是将绘制这些结果的函数:

def plotResults(trueLabels, anomalyScores, returnPreds = False):
    preds = pd.concat([trueLabels, anomalyScores], axis=1)
    preds.columns = ['trueLabel', 'anomalyScore']
    precision, recall, thresholds = \
        precision_recall_curve(preds['trueLabel'],preds['anomalyScore'])
    average_precision = \
        average_precision_score(preds['trueLabel'],preds['anomalyScore'])

    plt.step(recall, precision, color='k', alpha=0.7, where='post')
    plt.fill_between(recall, precision, step='post', alpha=0.3, color='k')

    plt.xlabel('Recall')
    plt.ylabel('Precision')
    plt.ylim([0.0, 1.05])
    plt.xlim([0.0, 1.0])

    plt.title('Precision-Recall curve: Average Precision = \
 {0:0.2f}'.format(average_precision))

    fpr, tpr, thresholds = roc_curve(preds['trueLabel'], \
                                     preds['anomalyScore'])
    areaUnderROC = auc(fpr, tpr)

    plt.figure()
    plt.plot(fpr, tpr, color='r', lw=2, label='ROC curve')
    plt.plot([0, 1], [0, 1], color='k', lw=2, linestyle='--')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.0, 1.05])
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('Receiver operating characteristic: \
 Area under the curve = {0:0.2f}'.format(areaUnderROC))
    plt.legend(loc="lower right")
    plt.show()

    if returnPreds==True:
        return preds
注意

欺诈标签和评估指标将帮助我们评估无监督欺诈检测系统在捕捉已知欺诈模式(我们过去已经捕捉到并有标签的欺诈)方面的表现如何。

然而,我们将无法评估无监督欺诈检测系统在捕捉未知欺诈模式方面的表现如何。换句话说,数据集中可能存在被错误标记为非欺诈的欺诈行为,因为金融公司从未发现它们。

正如你可能已经看到的那样,无监督学习系统比监督学习系统更难评估。通常,无监督学习系统是通过其捕捉已知欺诈模式的能力来评判的。这是一个不完整的评估;更好的评估指标应该是评估它们在识别未知欺诈模式方面的能力,无论是在过去还是在未来。

由于我们无法返回金融公司,并要求他们评估我们识别出的任何未知欺诈模式,因此我们将仅基于它们如何检测已知欺诈模式来评估这些无监督系统。在评估结果时,牢记这一限制非常重要。

定义绘图函数

我们将重用第三章中的散点图函数来展示降维算法在前两个维度上实现的点的分离情况:

def scatterPlot(xDF, yDF, algoName):
    tempDF = pd.DataFrame(data=xDF.loc[:,0:1], index=xDF.index)
    tempDF = pd.concat((tempDF,yDF), axis=1, join="inner")
    tempDF.columns = ["First Vector", "Second Vector", "Label"]
    sns.lmplot(x="First Vector", y="Second Vector", hue="Label", \
               data=tempDF, fit_reg=False)
    ax = plt.gca()
    ax.set_title("Separation of Observations using "+algoName)

普通 PCA 异常检测

在第三章中,我们演示了 PCA 如何仅通过少数几个主成分就捕获了 MNIST 数字数据集中的大部分信息,远少于原始维度。事实上,仅通过两个维度,就可以根据它们展示的数字将图像明显地分成不同的组。

基于这一概念,我们现在将使用 PCA 来学习信用卡交易数据集的潜在结构。一旦学习了这种结构,我们将使用学习模型来重构信用卡交易,然后计算重构交易与原始交易的差异。那些 PCA 重构效果最差的交易是最异常的(也最可能是欺诈性的)。

注意

请记住,我们拥有的信用卡交易数据集中的特征已经是 PCA 的输出结果 — 这是金融公司提供给我们的。然而,对于已经降维的数据集进行 PCA 异常检测并没有什么特别的。我们只需将给定的原始主成分视为原始特征即可。

今后,我们将称我们得到的原始主成分为原始特征。未来任何对主成分的提及都将指的是 PCA 过程中的主成分,而不是我们最初获得的原始特征。

让我们从更深入地理解 PCA 及其在异常检测中的作用开始。正如我们所定义的,异常检测依赖于重构误差。我们希望罕见交易(最有可能是欺诈的交易)的重构误差尽可能高,而其余交易的重构误差尽可能低。

对于 PCA,重构误差主要取决于我们保留和用于重构原始交易的主要成分数量。我们保留的主要成分越多,PCA 在学习原始交易的潜在结构方面表现越好。

然而,需要平衡。如果我们保留太多主要成分,PCA 可能会过于容易地重构原始交易,以至于所有交易的重构误差都将最小化。如果我们保留的主要成分太少,PCA 可能无法充分重构任何原始交易,甚至是正常的非欺诈性交易。

让我们寻找保留以构建良好的欺诈检测系统的正确主成分数。

PCA 成分等于原始维度数

首先,让我们考虑一些事情。如果我们使用 PCA 生成与原始特征数相同数量的主要成分,我们能执行异常检测吗?

如果你仔细思考,答案应该是显而易见的。回顾我们在前一章对 MNIST 数字数据集的 PCA 示例。

当主要成分的数量等于原始维度的数量时,PCA 会捕获数据中近乎 100%的方差/信息,因为它生成主要成分。因此,当 PCA 从主要成分重构交易时,所有交易(无论是欺诈还是正常的)的重构误差都将太小。我们将无法区分罕见交易和正常交易,换句话说,异常检测将效果不佳。

为了突出这一点,让我们应用 PCA 生成与原始特征数相同数量的主要成分(对于我们的信用卡交易数据集为 30 个)。这是通过 Scikit-Learn 中的fit_transform函数实现的。

为了从我们生成的主要成分中重构原始交易,我们将使用 Scikit-Learn 中的inverse_transform函数:

# 30 principal components
from sklearn.decomposition import PCA

n_components = 30
whiten = False
random_state = 2018

pca = PCA(n_components=n_components, whiten=whiten, \
          random_state=random_state)

X_train_PCA = pca.fit_transform(X_train)
X_train_PCA = pd.DataFrame(data=X_train_PCA, index=X_train.index)

X_train_PCA_inverse = pca.inverse_transform(X_train_PCA)
X_train_PCA_inverse = pd.DataFrame(data=X_train_PCA_inverse, \
                                   index=X_train.index)

scatterPlot(X_train_PCA, y_train, "PCA")

图 4-1 展示了使用 PCA 的前两个主要成分对交易进行分离的图表。

使用正常 PCA 和 30 个主要成分分离观察

图 4-1. 使用正常 PCA 和 30 个主要成分分离观察

让我们计算精确率-召回率曲线和 ROC 曲线:

anomalyScoresPCA = anomalyScores(X_train, X_train_PCA_inverse)
preds = plotResults(y_train, anomalyScoresPCA, True)

具有平均精度为 0.11,这是一个较差的欺诈检测解决方案(参见图 4-2)。它几乎无法捕捉到欺诈行为。

使用正常 PCA 和 30 个主成分的结果

图 4-2. 使用 30 个主成分的结果

搜索最佳主成分数量

现在,让我们通过减少 PCA 生成的主成分数量来执行一些实验,并评估欺诈检测结果。我们需要基于 PCA 的欺诈检测解决方案在罕见情况下有足够的误差,以便能够有效地区分欺诈案例和正常案例。但是误差不能对所有交易的罕见和正常交易都过低或过高,以至于它们几乎无法区分。

经过一些实验(可以使用GitHub 代码执行),我们发现 27 个主成分是此信用卡交易数据集的最佳数量。

图 4-3 展示了使用 PCA 的前两个主成分分离交易的图表。

使用正常 PCA 和 27 个主成分分离观察

图 4-3. 使用正常 PCA 和 27 个主成分分离观察

图 4-4 展示了精度-召回曲线、平均精度和 auROC 曲线。

使用正常 PCA 和 27 个主成分的结果

图 4-4. 使用正常 PCA 和 27 个主成分的结果

正如你所看到的,我们能够以 75%的精度捕捉到 80%的欺诈行为。考虑到训练集中有 190,820 笔交易,其中只有 330 笔是欺诈交易,这是非常令人印象深刻的结果。

使用 PCA,我们计算了这 190,820 笔交易中每笔交易的重建误差。如果我们按照重建误差(也称为异常分数)的降序对这些交易进行排序,并从列表中提取前 350 笔交易,我们可以看到其中有 264 笔交易是欺诈的。

这是 75%的精度。此外,我们从我们选择的 350 笔交易中捕捉到的 264 笔交易代表了训练集中 80%的总欺诈行为(330 笔欺诈案例中的 264 笔)。而且,请记住,这是一个真正的无监督欺诈检测解决方案,没有使用标签。

下面是突出显示此问题的代码:

preds.sort_values(by="anomalyScore",ascending=False,inplace=True)
cutoff = 350
predsTop = preds[:cutoff]
print("Precision: ",np.round(predsTop. \
            anomalyScore[predsTop.trueLabel==1].count()/cutoff,2))
print("Recall: ",np.round(predsTop. \
            anomalyScore[predsTop.trueLabel==1].count()/y_train.sum(),2))

下面的代码总结了结果:

Precision: 0.75
Recall: 0.8
Fraud Caught out of 330 Cases: 264

尽管这已经是一个相当好的解决方案,但让我们尝试使用其他降维方法开发欺诈检测系统。

稀疏 PCA 异常检测

让我们尝试使用稀疏 PCA 设计一个欺诈检测解决方案。回想一下,稀疏 PCA 类似于普通 PCA,但提供一个更稀疏的版本;换句话说,稀疏 PCA 提供了主成分的稀疏表示。

我们仍然需要指定所需的主成分数量,但我们还必须设置控制稀疏程度的 alpha 参数。在搜索最佳稀疏 PCA 欺诈检测解决方案时,我们将尝试不同的主成分值和 alpha 参数值。

请注意,对于普通 PCA,Scikit-Learn 使用 fit_transform 函数生成主成分,并使用 inverse_transform 函数从主成分重构原始维度。利用这两个函数,我们能够计算原始特征集和从 PCA 派生的重构特征集之间的重构误差。

不幸的是,Scikit-Learn 并未为稀疏 PCA 提供 inverse_transform 函数。因此,在执行稀疏 PCA 后,我们必须自行重构原始维度。

让我们首先生成具有 27 个主成分和默认 alpha 参数 0.0001 的稀疏 PCA 矩阵:

# Sparse PCA
from sklearn.decomposition import SparsePCA

n_components = 27
alpha = 0.0001
random_state = 2018
n_jobs = -1

sparsePCA = SparsePCA(n_components=n_components, \
                alpha=alpha, random_state=random_state, n_jobs=n_jobs)

sparsePCA.fit(X_train.loc[:,:])
X_train_sparsePCA = sparsePCA.transform(X_train)
X_train_sparsePCA = pd.DataFrame(data=X_train_sparsePCA, index=X_train.index)

scatterPlot(X_train_sparsePCA, y_train, "Sparse PCA")

图 4-5 显示了稀疏 PCA 的散点图。

使用稀疏 PCA 和 27 个主成分的观察分离

图 4-5. 使用稀疏 PCA 和 27 个主成分的观察分离

现在,让我们通过稀疏 PCA 矩阵的简单矩阵乘法(包含 190,820 个样本和 27 个维度)和 Scikit-Learn 库提供的稀疏 PCA 成分(一个 27 x 30 矩阵)生成稀疏 PCA 矩阵的原始维度。这样可以创建一个原始尺寸的矩阵(一个 190,820 x 30 矩阵)。我们还需要将每个原始特征的均值添加到这个新矩阵中,然后就完成了。

利用这个新推导出的逆矩阵,我们可以像对待普通 PCA 那样计算重构误差(异常分数):

X_train_sparsePCA_inverse = np.array(X_train_sparsePCA). \
    dot(sparsePCA.components_) + np.array(X_train.mean(axis=0))
X_train_sparsePCA_inverse = \
    pd.DataFrame(data=X_train_sparsePCA_inverse, index=X_train.index)

anomalyScoresSparsePCA = anomalyScores(X_train, X_train_sparsePCA_inverse)
preds = plotResults(y_train, anomalyScoresSparsePCA, True)

现在,让我们生成精确率-召回率曲线和 ROC 曲线。

使用稀疏 PCA 和 27 个主成分的结果

图 4-6. 使用稀疏 PCA 和 27 个主成分的结果

如图 4-6 所示,结果与普通 PCA 的结果完全相同。这是预期的,因为普通 PCA 和稀疏 PCA 非常相似——后者只是前者的稀疏表示。

使用 GitHub 代码,您可以通过更改生成的主成分数量和 alpha 参数来进行实验,但根据我们的实验,这是最佳的基于稀疏 PCA 的欺诈检测解决方案。

Kernel PCA 异常检测

现在让我们设计一个欺诈检测解决方案,使用核 PCA,这是 PCA 的非线性形式,如果欺诈交易与非欺诈交易不是线性可分的,它将非常有用。

我们需要指定要生成的组件数量,内核(我们将使用 RBF 内核,就像我们在上一章中做的那样),以及 gamma(默认情况下设置为 1/n_features,因此在我们的情况下为 1/30)。我们还需要将fit_inverse_transform设置为true,以应用 Scikit-Learn 提供的内置inverse_transform函数。

最后,由于核 PCA 在训练中非常昂贵,我们将仅在交易数据集的前两千个样本上进行训练。这并非理想选择,但为了快速进行实验,这是必要的。

我们将使用这个训练来转换整个训练集并生成主成分。然后,我们将使用inverse_transform函数从由核 PCA 导出的主成分重新创建原始维度:

# Kernel PCA
from sklearn.decomposition import KernelPCA

n_components = 27
kernel = 'rbf'
gamma = None
fit_inverse_transform = True
random_state = 2018
n_jobs = 1

kernelPCA = KernelPCA(n_components=n_components, kernel=kernel, \
                gamma=gamma, fit_inverse_transform= \
                fit_inverse_transform, n_jobs=n_jobs, \
                random_state=random_state)

kernelPCA.fit(X_train.iloc[:2000])
X_train_kernelPCA = kernelPCA.transform(X_train)
X_train_kernelPCA = pd.DataFrame(data=X_train_kernelPCA, \
                                 index=X_train.index)

X_train_kernelPCA_inverse = kernelPCA.inverse_transform(X_train_kernelPCA)
X_train_kernelPCA_inverse = pd.DataFrame(data=X_train_kernelPCA_inverse, \
                                         index=X_train.index)

scatterPlot(X_train_kernelPCA, y_train, "Kernel PCA")

图 4-7 显示了核 PCA 的散点图。

使用核 PCA 和 27 个主成分分离观察

图 4-7. 使用核 PCA 和 27 个主成分分离观察

现在,让我们计算异常分数并打印结果。

使用核 PCA 和 27 个主成分的结果

图 4-8. 使用核 PCA 和 27 个主成分的结果

如图 4-8 所示,其结果远不如普通 PCA 和稀疏 PCA。虽然进行核 PCA 实验是值得的,但考虑到我们有更好的性能解决方案,我们不会将其用于欺诈检测。

我们不会使用 SVD 构建异常检测解决方案,因为其解决方案与普通 PCA 非常相似。这是预期的——PCA 和 SVD 密切相关。

反而,让我们转向基于随机投影的异常检测。

高斯随机投影异常检测

现在,让我们尝试使用高斯随机投影开发欺诈检测解决方案。请记住,我们可以设置我们想要的组件数量或eps参数,后者控制基于 Johnson-Lindenstrauss 引理导出的嵌入质量。

我们将选择显式设置组件的数量。高斯随机投影训练非常快,因此我们可以在整个训练集上进行训练。

与稀疏 PCA 一样,我们需要推导出自己的inverse_transform函数,因为 Scikit-Learn 没有提供这样的函数:

# Gaussian Random Projection
from sklearn.random_projection import GaussianRandomProjection

n_components = 27
eps = None
random_state = 2018

GRP = GaussianRandomProjection(n_components=n_components, \
                               eps=eps, random_state=random_state)

X_train_GRP = GRP.fit_transform(X_train)
X_train_GRP = pd.DataFrame(data=X_train_GRP, index=X_train.index)

scatterPlot(X_train_GRP, y_train, "Gaussian Random Projection")

图 4-9 显示了高斯随机投影的散点图。图 4-10 显示了高斯随机投影的结果。

使用高斯随机投影和 27 个分量分离观察结果

图 4-9. 使用高斯随机投影和 27 个分量分离观察结果

使用高斯随机投影和 27 个分量的结果

图 4-10. 使用高斯随机投影和 27 个分量的结果

这些结果很差,因此我们不会使用高斯随机投影进行欺诈检测。

稀疏随机投影异常检测

让我们尝试使用稀疏随机投影设计一个欺诈检测解决方案。

我们将指定我们需要的分量数量(而不是设置 eps 参数)。而且,就像使用高斯随机投影一样,我们将使用我们自己的 inverse_transform 函数从稀疏随机投影派生的分量中创建原始维度:

# Sparse Random Projection

from sklearn.random_projection import SparseRandomProjection

n_components = 27
density = 'auto'
eps = .01
dense_output = True
random_state = 2018

SRP = SparseRandomProjection(n_components=n_components, \
        density=density, eps=eps, dense_output=dense_output, \
                                random_state=random_state)

X_train_SRP = SRP.fit_transform(X_train)
X_train_SRP = pd.DataFrame(data=X_train_SRP, index=X_train.index)

scatterPlot(X_train_SRP, y_train, "Sparse Random Projection")

图 4-11 显示了稀疏随机投影的散点图。 图 4-12 展示了稀疏随机投影的结果。

使用稀疏随机投影和 27 个分量分离观察结果

图 4-11. 使用稀疏随机投影和 27 个分量分离观察结果

使用稀疏随机投影和 27 个分量的结果

图 4-12. 使用稀疏随机投影和 27 个分量的结果

和高斯随机投影一样,这些结果很差。让我们继续使用其他降维方法构建异常检测系统。

非线性异常检测

到目前为止,我们已经使用了线性降维方法开发了欺诈检测解决方案,如常规 PCA、稀疏 PCA、高斯随机投影和稀疏随机投影。我们还使用了非线性版本的 PCA——核 PCA。

到目前为止,PCA 是迄今为止最好的解决方案。

我们可以转向非线性降维算法,但这些算法的开源版本运行非常缓慢,不适合快速欺诈检测。因此,我们将跳过这一步,直接转向非距离度量的降维方法:字典学习和独立分量分析。

字典学习异常检测

让我们使用字典学习来开发一个欺诈检测解决方案。回想一下,在字典学习中,算法学习原始数据的稀疏表示。使用学习字典中的向量,可以将原始数据中的每个实例重构为这些学习向量的加权和。

对于异常检测,我们希望学习一个欠完备字典,使得字典中的向量数量少于原始维度。在这个约束条件下,更容易重构出频繁发生的正常交易,但更难构建出罕见的欺诈交易。

在我们的情况下,我们将生成 28 个向量(或成分)。为了学习字典,我们将提供 10 个批次,每个批次包含 200 个样本。

我们也需要使用我们自己的inverse_transform函数:

# Mini-batch dictionary learning
from sklearn.decomposition import MiniBatchDictionaryLearning

n_components = 28
alpha = 1
batch_size = 200
n_iter = 10
random_state = 2018

miniBatchDictLearning = MiniBatchDictionaryLearning( \
    n_components=n_components, alpha=alpha, batch_size=batch_size, \
    n_iter=n_iter, random_state=random_state)

miniBatchDictLearning.fit(X_train)
X_train_miniBatchDictLearning = \
    miniBatchDictLearning.fit_transform(X_train)
X_train_miniBatchDictLearning = \
    pd.DataFrame(data=X_train_miniBatchDictLearning, index=X_train.index)

scatterPlot(X_train_miniBatchDictLearning, y_train, \
            "Mini-batch Dictionary Learning")

图 4-13 展示了字典学习的散点图。图 4-14 展示了字典学习的结果。

使用字典学习和 28 个成分的观测分离

图 4-13. 使用字典学习和 28 个成分的观测分离

使用字典学习和 28 个成分的结果

图 4-14. 使用字典学习和 28 个成分的结果

这些结果远优于核 PCA、高斯随机投影和稀疏随机投影的结果,但与普通 PCA 的结果不相上下。

您可以在 GitHub 上尝试代码,看看是否能改进这个解决方案,但目前来看,PCA 仍然是这个信用卡交易数据集的最佳欺诈检测解决方案。

ICA 异常检测

让我们使用 ICA 设计我们的最后一个欺诈检测解决方案。

我们需要指定成分的数量,我们将设置为 27。Scikit-Learn 提供了一个inverse_transform函数,因此我们不需要使用自己的函数:

# Independent Component Analysis

from sklearn.decomposition import FastICA

n_components = 27
algorithm = 'parallel'
whiten = True
max_iter = 200
random_state = 2018

fastICA = FastICA(n_components=n_components, \
    algorithm=algorithm, whiten=whiten, max_iter=max_iter, \
    random_state=random_state)

X_train_fastICA = fastICA.fit_transform(X_train)
X_train_fastICA = pd.DataFrame(data=X_train_fastICA, index=X_train.index)

X_train_fastICA_inverse = fastICA.inverse_transform(X_train_fastICA)
X_train_fastICA_inverse = pd.DataFrame(data=X_train_fastICA_inverse, \
                                       index=X_train.index)

scatterPlot(X_train_fastICA, y_train, "Independent Component Analysis")

图 4-15 展示了 ICA 的散点图。图 4-16 展示了 ICA 的结果。

使用字典学习和 28 个成分的观测分离

图 4-15. 使用 ICA 和 27 个成分的观测分离

独立成分分析和 27 个成分的结果

图 4-16. 使用 ICA 和 27 个成分的结果

这些结果与普通 PCA 的结果相同。ICA 的欺诈检测解决方案与我们迄今为止开发的最佳解决方案相匹配。

测试集上的欺诈检测

现在,为了评估我们的欺诈检测解决方案,让我们将其应用于前所未见的测试集。我们将对我们开发的前三种解决方案进行评估:普通 PCA、ICA 和字典学习。我们不会使用稀疏 PCA,因为它与普通 PCA 解决方案非常相似。

普通 PCA 在测试集上的异常检测

让我们从普通 PCA 开始。我们将使用 PCA 算法从训练集学习到的 PCA 嵌入,并用此转换测试集。然后,我们将使用 Scikit-Learn 的inverse_transform函数从测试集的主成分矩阵重新创建原始维度。

通过比较原始测试集矩阵和新重建的矩阵,我们可以计算异常分数(正如我们在本章中多次做过的):

# PCA on Test Set
X_test_PCA = pca.transform(X_test)
X_test_PCA = pd.DataFrame(data=X_test_PCA, index=X_test.index)

X_test_PCA_inverse = pca.inverse_transform(X_test_PCA)
X_test_PCA_inverse = pd.DataFrame(data=X_test_PCA_inverse, \
                                  index=X_test.index)

scatterPlot(X_test_PCA, y_test, "PCA")

图 4-17 显示了在测试集上使用 PCA 的散点图。图 4-18 显示了在测试集上使用 PCA 的结果。

在测试集上使用 PCA 和 27 个分量的观察结果分离

图 4-17. 在测试集上使用 PCA 和 27 个分量进行观察结果分离

在测试集上使用 PCA 和 27 个分量的结果

图 4-18. 在测试集上使用 PCA 和 27 个分量的结果

这些是令人印象深刻的结果。我们能够在测试集中捕捉到 80% 的已知欺诈,精度为 80%——而且全部不使用任何标签。

在测试集上的 ICA 异常检测

现在让我们转向 ICA,并在测试集上进行欺诈检测:

# Independent Component Analysis on Test Set
X_test_fastICA = fastICA.transform(X_test)
X_test_fastICA = pd.DataFrame(data=X_test_fastICA, index=X_test.index)

X_test_fastICA_inverse = fastICA.inverse_transform(X_test_fastICA)
X_test_fastICA_inverse = pd.DataFrame(data=X_test_fastICA_inverse, \
                                      index=X_test.index)

scatterPlot(X_test_fastICA, y_test, "Independent Component Analysis")

图 4-19 显示了在测试集上使用 ICA 的散点图。图 4-20 显示了在测试集上使用 ICA 的结果。

在测试集上使用独立分量分析和 27 个分量的观察结果分离

图 4-19. 在测试集上使用 ICA 和 27 个分量进行观察结果分离

在测试集上使用独立分量分析和 27 个分量的结果

图 4-20. 在测试集上使用 ICA 和 27 个分量的结果

结果与常规 PCA 完全相同,因此令人印象深刻。

在测试集上的字典学习异常检测

现在让我们转向字典学习,虽然它的表现不如常规 PCA 和 ICA,但仍值得最后一看:

X_test_miniBatchDictLearning = miniBatchDictLearning.transform(X_test)
X_test_miniBatchDictLearning = \
    pd.DataFrame(data=X_test_miniBatchDictLearning, index=X_test.index)

scatterPlot(X_test_miniBatchDictLearning, y_test, \
            "Mini-batch Dictionary Learning")

图 4-21 显示了在测试集上使用字典学习的散点图。图 4-22 显示了在测试集上使用字典学习的结果。

在测试集上使用字典学习和 28 个分量的观察结果分离

图 4-21. 在测试集上使用字典学习和 28 个分量进行观察结果分离

在测试集上使用字典学习和 28 个分量的结果

图 4-22. 在测试集上使用字典学习和 28 个分量的结果

尽管结果并不糟糕——我们可以用 20% 的精度捕捉到 80% 的欺诈——但与常规 PCA 和 ICA 的结果相比差距很大。

结论

在本章中,我们使用了上一章的核心降维算法来针对第二章的信用卡交易数据集开发欺诈检测解决方案。

在第二章中,我们使用标签构建了一个欺诈检测解决方案,但是在本章的训练过程中我们没有使用任何标签。换句话说,我们使用无监督学习构建了一个应用型欺诈检测系统。

虽然并非所有降维算法在这个信用卡交易数据集上表现良好,但是有两个表现非常出色——普通 PCA 和 ICA。

普通的 PCA 和 ICA 可以捕捉到 80%以上的已知欺诈,并且精度达到 80%。相比之下,第二章中表现最佳的基于有监督学习的欺诈检测系统几乎可以捕捉到 90%的已知欺诈,并且精度达到 80%。无监督欺诈检测系统在捕捉已知欺诈模式方面只比有监督系统稍微差一点。

请记住,无监督的欺诈检测系统在训练过程中不需要标签,能够很好地适应不断变化的欺诈模式,并且可以发现以前未被发现的欺诈行为。考虑到这些额外的优势,无监督学习的解决方案通常会比有监督学习的解决方案更好地捕捉到未来已知和未知或新出现的欺诈模式,尽管将两者结合使用效果最佳。

现在我们已经涵盖了降维和异常检测,让我们来探讨聚类,这是无监督学习领域的另一个重要概念。

第五章:聚类

在第三章中,我们介绍了无监督学习中最重要的降维算法,并突出它们密集捕捉信息的能力。在第四章中,我们使用了降维算法构建了一个异常检测系统。具体来说,我们应用这些算法来检测信用卡欺诈,而不使用任何标签。这些算法学习了信用卡交易中的潜在结构。然后,我们根据重构误差将正常交易与罕见的、潜在的欺诈交易分开。

在本章中,我们将在无监督学习的概念基础上进一步讨论聚类,它试图根据相似性将对象组合在一起。聚类在不使用任何标签的情况下实现这一点,比较一个观察数据与其他观察数据的相似性并进行分组。

聚类有许多应用。例如,在信用卡欺诈检测中,聚类可以将欺诈交易分组在一起,与正常交易分开。或者,如果我们的数据集中只有少数几个标签的观察结果,我们可以使用聚类首先对观察结果进行分组(而不使用标签)。然后,我们可以将少数标记观察结果的标签转移到同一组内的其余观察结果上。这是迁移学习的一种形式,是机器学习中一个快速发展的领域。

在在线购物、零售、市场营销、社交媒体、电影、音乐、书籍、约会等领域,聚类可以根据用户行为将相似的人群组合在一起。一旦建立了这些群体,业务用户就能更好地洞察他们的用户群体,并为每个独特的群体制定有针对性的业务战略。

就像我们在降维中所做的那样,让我们先在本章中介绍概念,然后在下一章中构建一个应用的无监督学习解决方案。

MNIST 手写数字数据集

为了简化问题,我们将继续使用我们在第三章中介绍的手写数字 MNIST 图像数据集。

数据准备

让我们首先加载必要的库:

# Import libraries
'''Main'''
import numpy as np
import pandas as pd
import os, time
import pickle, gzip

'''Data Viz'''
import matplotlib.pyplot as plt
import seaborn as sns
color = sns.color_palette()
import matplotlib as mpl

%matplotlib inline

'''Data Prep and Model Evaluation'''
from sklearn import preprocessing as pp
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import roc_curve, auc, roc_auc_score

接下来,让我们加载数据集并创建 Pandas 数据框:

# Load the datasets
current_path = os.getcwd()
file = '\\datasets\\mnist_data\\mnist.pkl.gz'

f = gzip.open(current_path+file, 'rb')
train_set, validation_set, test_set = pickle.load(f, encoding='latin1')
f.close()

X_train, y_train = train_set[0], train_set[1]
X_validation, y_validation = validation_set[0], validation_set[1]
X_test, y_test = test_set[0], test_set[1]

# Create Pandas DataFrames from the datasets
train_index = range(0,len(X_train))
validation_index = range(len(X_train), \
                         len(X_train)+len(X_validation))
test_index = range(len(X_train)+len(X_validation), \
                   len(X_train)+len(X_validation)+len(X_test))

X_train = pd.DataFrame(data=X_train,index=train_index)
y_train = pd.Series(data=y_train,index=train_index)

X_validation = pd.DataFrame(data=X_validation,index=validation_index)
y_validation = pd.Series(data=y_validation,index=validation_index)

X_test = pd.DataFrame(data=X_test,index=test_index)
y_test = pd.Series(data=y_test,index=test_index)

聚类算法

在执行聚类之前,我们将使用 PCA 减少数据的维度。正如在第三章中所示,降维算法捕捉了原始数据中的显著信息,同时减少了数据集的大小。

当我们从高维度向低维度移动时,数据集中的噪声会被最小化,因为降维算法(在本例中是 PCA)需要捕捉原始数据的最重要的方面,而不能将注意力放在频繁出现的元素(例如数据集中的噪声)上。

记得降维算法在学习数据中的潜在结构方面非常强大。在 第三章 中,我们展示了仅使用两个维度进行降维后,可以根据 MNIST 图像所显示的数字有意义地分开它们。

现在让我们再次将 PCA 应用于 MNIST 数据集:

# Principal Component Analysis
from sklearn.decomposition import PCA

n_components = 784
whiten = False
random_state = 2018

pca = PCA(n_components=n_components, whiten=whiten, \
          random_state=random_state)

X_train_PCA = pca.fit_transform(X_train)
X_train_PCA = pd.DataFrame(data=X_train_PCA, index=train_index)

尽管我们没有降低维度,但我们将在聚类阶段指定我们将使用的主成分数目,从而有效地降低维度。

现在让我们转向聚类。三个主要的聚类算法是 k-means层次聚类DBSCAN。我们将逐一介绍并探讨每一个。

k-Means

聚类的目标是在数据集中识别出不同的组,使得组内的观察值彼此相似,但与其他组的观察值不同。在 k-means 聚类中,我们指定所需的簇数 k,算法将每个观察值精确分配到这 k 个簇中的一个。该算法通过最小化 簇内变化(也称为 惯性)来优化这些组,从而使得所有 k 个簇内的变化总和尽可能小。

不同的 k-means 运行会导致略有不同的簇分配,因为 k-means 随机地将每个观察值分配给 k 个簇中的一个来启动聚类过程。k-means 通过这种随机初始化来加速聚类过程。在此随机初始化后,k-means 将重新将观察值分配给不同的簇,以尽量减小每个观察值与其簇中心点(或 质心)之间的欧氏距离。这种随机初始化是随机性的来源,导致从一个 k-means 运行到另一个运行略有不同的聚类分配。

典型情况下,k-means 算法会进行多次运行,并选择具有最佳分离效果的运行,这里分离效果定义为所有 k 个簇内部变化总和最低。

k-Means 惯性

让我们介绍算法。我们需要设置我们想要的簇数目 (n_clusters),我们希望执行的初始化次数 (n_init),算法将运行以重新分配观察值以最小化惯性的最大迭代次数 (max_iter),以及声明收敛的容差 (tol)。

我们将保留默认值,即初始化次数(10)、最大迭代次数(300)和容差(0.0001)。此外,目前我们将从 PCA 中选择前 100 个主成分 (cutoff)。为了测试我们指定的簇数目如何影响惯性度量,让我们对簇大小从 2 到 20 运行 k-means,并记录每个簇的惯性。

这是代码:

# k-means - Inertia as the number of clusters varies
from sklearn.cluster import KMeans

n_clusters = 10
n_init = 10
max_iter = 300
tol = 0.0001
random_state = 2018
n_jobs = 2

kMeans_inertia = pd.DataFrame(data=[],index=range(2,21), \
                              columns=['inertia'])
for n_clusters in range(2,21):
    kmeans = KMeans(n_clusters=n_clusters, n_init=n_init, \
                max_iter=max_iter, tol=tol, random_state=random_state, \
                n_jobs=n_jobs)

    cutoff = 99
    kmeans.fit(X_train_PCA.loc[:,0:cutoff])
    kMeans_inertia.loc[n_clusters] = kmeans.inertia_

如图 5-1 所示,随着群集数量的增加,惯性在减少。这是有道理的。群集越多,每个群集内观察结果的同质性就越大。然而,比起更多的群集,较少的群集更容易处理,因此在运行k-means 时找到正确的群集数量是一个重要考虑因素。

群集大小为 2 至 20 的k-means 惯性

图 5-1. 群集大小为 2 至 20 的k-means 惯性

评估聚类结果

为了演示k-means 的工作原理以及增加群集数量如何导致更加同质的群集,让我们定义一个函数来分析我们每次实验的结果。聚类算法生成的群集分配将存储在名为clusterDF的 Pandas DataFrame 中。

让我们统计每个群集中的观察结果数量,并将这些存储在名为countByCluster的 Pandas DataFrame 中:

def analyzeCluster(clusterDF, labelsDF):
    countByCluster = \
        pd.DataFrame(data=clusterDF['cluster'].value_counts())
    countByCluster.reset_index(inplace=True,drop=False)
    countByCluster.columns = ['cluster','clusterCount']

接下来,让我们将clusterDF与称为labelsDF的真实标签数组结合起来:

    preds = pd.concat([labelsDF,clusterDF], axis=1)
    preds.columns = ['trueLabel','cluster']

让我们还统计训练集中每个真实标签的观察结果数量(这不会改变,但我们需要了解):

    countByLabel = pd.DataFrame(data=preds.groupby('trueLabel').count())

现在,对于每个群集,我们将计算每个不同标签在群集内的观察结果数量。例如,如果给定的群集有三千个观察结果,其中两千可能代表数字二,五百可能代表数字一,三百可能代表数字零,其余的两百可能代表数字九。

一旦我们计算这些,我们将为每个群集存储最频繁出现数字的计数。在上述示例中,我们将为此群集存储两千的计数:

    countMostFreq = \
        pd.DataFrame(data=preds.groupby('cluster').agg( \
                        lambda x:x.value_counts().iloc[0]))
    countMostFreq.reset_index(inplace=True,drop=False)
    countMostFreq.columns = ['cluster','countMostFrequent']

最后,我们将根据每次聚类运行中观察结果在每个群集内的紧密程度来评估每次聚类运行的成功程度。例如,在上述示例中,群集中有两千个观察结果具有相同的标签,总共有三千个观察结果在该群集中。

由于我们理想情况下希望将相似的观察结果聚集在同一个群集中并排除不相似的观察结果,因此这个群集并不理想。

让我们定义聚类的总体准确性,即通过总体训练集观察结果中最频繁出现的观察结果的计数之和除以总观察结果数(即 50,000):

    accuracyDF = countMostFreq.merge(countByCluster, \
                        left_on="cluster",right_on="cluster")
    overallAccuracy = accuracyDF.countMostFrequent.sum()/ \
                        accuracyDF.clusterCount.sum()

我们也可以通过群集评估准确性:

    accuracyByLabel = accuracyDF.countMostFrequent/ \
                        accuracyDF.clusterCount

为了简洁起见,我们将所有这些代码放在一个单独的函数中,可以在GitHub上找到。

k-means 准确性

现在,让我们执行之前的实验,但是不计算惯性,而是根据我们为 MNIST 数字数据集定义的准确性度量来计算群集的整体同质性:

# k-means - Accuracy as the number of clusters varies

n_clusters = 5
n_init = 10
max_iter = 300
tol = 0.0001
random_state = 2018
n_jobs = 2

kMeans_inertia = \
    pd.DataFrame(data=[],index=range(2,21),columns=['inertia'])
overallAccuracy_kMeansDF = \
    pd.DataFrame(data=[],index=range(2,21),columns=['overallAccuracy'])

for n_clusters in range(2,21):
    kmeans = KMeans(n_clusters=n_clusters, n_init=n_init, \
                max_iter=max_iter, tol=tol, random_state=random_state, \
                n_jobs=n_jobs)

    cutoff = 99
    kmeans.fit(X_train_PCA.loc[:,0:cutoff])
    kMeans_inertia.loc[n_clusters] = kmeans.inertia_
    X_train_kmeansClustered = kmeans.predict(X_train_PCA.loc[:,0:cutoff])
    X_train_kmeansClustered = \
        pd.DataFrame(data=X_train_kmeansClustered, index=X_train.index, \
                     columns=['cluster'])

    countByCluster_kMeans, countByLabel_kMeans, countMostFreq_kMeans, \
        accuracyDF_kMeans, overallAccuracy_kMeans, accuracyByLabel_kMeans \
        = analyzeCluster(X_train_kmeansClustered, y_train)

    overallAccuracy_kMeansDF.loc[n_clusters] = overallAccuracy_kMeans

图 5-2 显示了不同群集大小的整体准确性的图表。

簇大小为 2 到 20 的 k-Means 准确性

Figure 5-2. 簇大小为 2 到 20 的 k-means 准确性

如 Figure 5-2 所示,随着簇数的增加,准确性也会提高。换句话说,随着簇数的增加,簇变得更加同质化,因为每个簇变得更小且更紧凑。

按簇计算的准确度差异很大,有些簇表现出高度的同质性,而其他簇则较少。例如,某些簇中超过 90% 的图像具有相同的数字;在其他簇中,少于 50% 的图像具有相同的数字:

0    0.636506
1    0.928505
2    0.848714
3    0.521805
4    0.714337
5    0.950980
6    0.893103
7    0.919040
8    0.404707
9    0.500522
10   0.381526
11   0.587680
12   0.463382
13   0.958046
14   0.870888
15   0.942325
16   0.791192
17   0.843972
18   0.455679
19   0.926480
dtype:  float64

k-Means 和主成分数量

让我们进行另一个实验——这次,让我们评估在聚类算法中使用的主成分数量如何影响簇的同质性(定义为 准确性)。

在之前的实验中,我们使用了一百个主成分,从正常的 PCA 中推导出来。回想一下,MNIST 数字数据集的原始维度是 784。如果 PCA 能够很好地捕捉数据中的基础结构并尽可能紧凑地表示,那么聚类算法将更容易将相似的图像分组在一起,无论是在少量主成分上还是在更多主成分上进行聚类。换句话说,聚类在使用 10 或 50 个主成分时应该和使用一百或几百个主成分时一样好。

让我们来验证这个假设。我们将使用 10、50、100、200、300、400、500、600、700 和 784 个主成分,并评估每个聚类实验的准确性。然后我们将绘制这些结果,看看主成分数量的变化如何影响聚类的准确性:

# k-means - Accuracy as the number of components varies

n_clusters = 20
n_init = 10
max_iter = 300
tol = 0.0001
random_state = 2018
n_jobs = 2

kMeans_inertia = pd.DataFrame(data=[],index=[9, 49, 99, 199, \
                    299, 399, 499, 599, 699, 784],columns=['inertia'])

overallAccuracy_kMeansDF = pd.DataFrame(data=[],index=[9, 49, \
                    99, 199, 299, 399, 499, 599, 699, 784], \
                    columns=['overallAccuracy'])

for cutoffNumber in [9, 49, 99, 199, 299, 399, 499, 599, 699, 784]:
    kmeans = KMeans(n_clusters=n_clusters, n_init=n_init, \
                max_iter=max_iter, tol=tol, random_state=random_state, \
                n_jobs=n_jobs)

    cutoff = cutoffNumber
    kmeans.fit(X_train_PCA.loc[:,0:cutoff])
    kMeans_inertia.loc[cutoff] = kmeans.inertia_
    X_train_kmeansClustered = kmeans.predict(X_train_PCA.loc[:,0:cutoff])
    X_train_kmeansClustered = pd.DataFrame(data=X_train_kmeansClustered, \
                                index=X_train.index, columns=['cluster'])

    countByCluster_kMeans, countByLabel_kMeans, countMostFreq_kMeans, \
        accuracyDF_kMeans, overallAccuracy_kMeans, accuracyByLabel_kMeans \
        = analyzeCluster(X_train_kmeansClustered, y_train)

    overallAccuracy_kMeansDF.loc[cutoff] = overallAccuracy_kMeans

Figure 5-3 显示了不同主成分数量下聚类准确性的图表。

k-means 聚类准确性随主成分数量变化

Figure 5-3. 随着主成分数量变化的 k-means 聚类准确性

这个图表支持我们的假设。随着主成分数量从 10 变化到 784,聚类的准确性保持稳定在约 70% 左右。这也是为什么应该在降维后的数据集上执行聚类的一个原因——聚类算法通常在降维后的数据集上表现更好,无论是在时间还是聚类准确性方面。

对于 MNIST 数据集而言,原始的 784 维度对于聚类算法来说是可以管理的,但是想象一下如果原始数据集的维度是成千上万的话。在这种情况下,进行聚类之前降低维度的理由更加强烈。

在原始数据集上的 k-Means

为了更清楚地说明这一点,让我们在原始数据集上执行聚类,并测量我们传递到聚类算法中的维度数量如何影响聚类准确性。

对于前一节中的 PCA 降维数据集,我们传递给聚类算法的主成分数量变化并不影响聚类准确性,其保持稳定且一致,约为 70%。这对原始数据集也适用吗?

# k-means - Accuracy as the number of components varies
# On the original MNIST data (not PCA-reduced)

n_clusters = 20
n_init = 10
max_iter = 300
tol = 0.0001
random_state = 2018
n_jobs = 2

kMeans_inertia = pd.DataFrame(data=[],index=[9, 49, 99, 199, \
                    299, 399, 499, 599, 699, 784],columns=['inertia'])

overallAccuracy_kMeansDF = pd.DataFrame(data=[],index=[9, 49, \
                    99, 199, 299, 399, 499, 599, 699, 784], \
                    columns=['overallAccuracy'])

for cutoffNumber in [9, 49, 99, 199, 299, 399, 499, 599, 699, 784]:
    kmeans = KMeans(n_clusters=n_clusters, n_init=n_init, \
                max_iter=max_iter, tol=tol, random_state=random_state, \
                n_jobs=n_jobs)

    cutoff = cutoffNumber
    kmeans.fit(X_train.loc[:,0:cutoff])
    kMeans_inertia.loc[cutoff] = kmeans.inertia_
    X_train_kmeansClustered = kmeans.predict(X_train.loc[:,0:cutoff])
    X_train_kmeansClustered = pd.DataFrame(data=X_train_kmeansClustered, \
                                index=X_train.index, columns=['cluster'])

    countByCluster_kMeans, countByLabel_kMeans, countMostFreq_kMeans, \
        accuracyDF_kMeans, overallAccuracy_kMeans, accuracyByLabel_kMeans \
        = analyzeCluster(X_train_kmeansClustered, y_train)

    overallAccuracy_kMeansDF.loc[cutoff] = overallAccuracy_kMeans

图 5-4 显示了在不同原始维度下的聚类准确性。

随着原始维度数量变化的 k-means 聚类准确性

图 5-4. 随着原始维度数量变化的 k-means 聚类准确性

正如图表所示,低维度下的聚类准确性非常低,但仅当维度数量提升至六百时,聚类准确性才接近 70%。

在 PCA 案例中,即使在 10 个维度下,聚类准确性也约为 70%,展示了降维在原始数据集中密集捕捉显著信息的能力。

层次聚类

现在我们来介绍一种叫做层次聚类的第二种聚类方法。这种方法不要求我们预先确定特定数量的簇。相反,我们可以在层次聚类运行完成后选择我们想要的簇的数量。

使用我们数据集中的观察结果,层次聚类算法将构建一个树形图,它可以被描绘为一个倒置的树,叶子位于底部,树干位于顶部。

底部的叶子是数据集中的个别实例。随着我们沿着倒置树向上移动,层次聚类会将这些叶子根据它们彼此的相似程度连接在一起。最相似的实例(或实例组)会更早地连接在一起,而不那么相似的实例则会较晚连接。

通过这个迭代过程,所有实例最终都连接在一起,形成树的单一主干。

这种垂直表示非常有帮助。一旦层次聚类算法运行完成,我们可以查看树状图,并确定我们想要切割树的位置——我们切割得越低,我们留下的个别分支(即更多簇)就越多。如果我们想要更少的簇,我们可以在树状图上部更高处切割,接近这个倒置树顶部的单一主干。

这个垂直切割的位置类似于在k-means 聚类算法中选择k个簇的数量。

聚合式层次聚类

我们将探索的层次聚类版本称为聚合聚类。虽然 Scikit-Learn 有一个库可以实现这一点,但执行速度非常慢。相反,我们选择使用另一个名为fastcluster的层次聚类版本。这个包是一个 C++库,有 Python/SciPy 接口。¹

在本包中我们将使用的主要函数是fastcluster.linkage_vector。这需要几个参数,包括训练矩阵Xmethodmetricmethod可以设置为singlecentroidmedianward,指定用于确定树枝图中新节点到其他节点距离的聚类方案。在大多数情况下,metric应设置为euclidean,并且如果methodcentroidmedianward,则必须为euclidean。有关这些参数的更多信息,请参阅 fastcluster 文档。

让我们为我们的数据设置层次聚类算法。与之前一样,我们将在 PCA 降维的 MNIST 图像数据集的前一百个主成分上训练算法。我们将把method设置为ward(在实验中表现得非常好),metric设置为euclidean

Ward 代表Ward 最小方差法。您可以在在线了解更多关于这种方法的信息。在层次聚类中,Ward 是一个很好的默认选择,但是,根据特定数据集的实际情况进行实验是最好的。

import fastcluster
from scipy.cluster.hierarchy import dendrogram, cophenet
from scipy.spatial.distance import pdist

cutoff = 100
Z = fastcluster.linkage_vector(X_train_PCA.loc[:,0:cutoff], \
                               method='ward', metric='euclidean')
Z_dataFrame = pd.DataFrame(data=Z, \
    columns=['clusterOne','clusterTwo','distance','newClusterSize'])

层次聚类算法将返回一个矩阵Z。该算法将我们的 50,000 个 MNIST 数字数据集中的每个观察视为单点聚类,并且在每次训练迭代中,算法将合并距离最小的两个聚类。

初始时,算法仅合并单点聚类,但随着进行,它将单点或多点聚类与单点或多点聚类合并。最终,通过这个迭代过程,所有的聚类被合并在一起,形成了倒置树(树枝图)的主干。

树枝图

表 5-1 展示了聚类算法生成的 Z 矩阵,显示了算法的成就。

表 5-1. 层次聚类的 Z 矩阵的前几行

clusterOneclusterTwodistancenewClusterSize
042194.043025.00.5626822.0
128350.037674.00.5908662.0
226696.044705.00.6215062.0
312634.032823.00.6277622.0
424707.043151.00.6376682.0
520465.024483.00.6625572.0
6466.042098.00.6641892.0
746542.049961.00.6655202.0
82301.05732.00.6712152.0
937564.047668.00.6751212.0
103375.026243.00.6857972.0
1115722.030368.00.6863562.0
1221247.021575.00.6944122.0
1314900.042486.00.6967692.0
1430100.041908.00.6992612.0
1512040.013254.00.7011342.0
1610508.025434.00.7088722.0
1730695.030757.00.7100232.0
1831019.031033.00.7120522.0
1936264.037285.00.7131302.0

在这个表格中,前两列clusterOneclusterTwo列出了两个簇——可以是单点簇(即原始观测数据)或多点簇——在彼此之间的距离下被合并。第三列distance显示了由我们传入聚类算法的 Ward 方法和euclidean度量计算出的距离。

如你所见,距离是单调递增的。换句话说,最短距离的簇首先合并,然后算法迭代地合并下一个最短距离的簇,直到所有点都合并为顶部树形图中的单一簇。

起初,算法将单点簇合并在一起,形成大小为两个的新簇,如第四列newClusterSize所示。然而,随着算法的进展,算法将大型多点簇与其他大型多点簇合并,如表格 5-2 所示。在最后一次迭代(49,998),两个大型簇合并在一起,形成单一簇——顶部树干,包含所有 50,000 个原始观测数据。

表格 5-2. 分层聚类 Z 矩阵的最后几行

clusterOneclusterTwodistancenewClusterSize
4998099965.099972.0161.1069985197.0
4998199932.099980.0172.0700036505.0
4998299945.099960.0182.8408603245.0
4998399964.099976.0184.4757613683.0
4998499974.099979.0185.0278477744.0
4998599940.099975.0185.3452075596.0
4998699957.099967.0211.8547145957.0
4998799938.099983.0215.4948574846.0
4998899978.099984.0216.76036511072.0
4998999970.099973.0217.3558714899.0
4999099969.099986.0225.4682988270.0
4999199981.099982.0238.8451359750.0
4999299968.099977.0266.1467825567.0
4999399985.099989.0270.92945310495.0
4999499990.099991.0346.84094818020.0
4999599988.099993.0394.36519421567.0
4999699987.099995.0425.14238726413.0
4999799992.099994.0440.14830123587.0
4999899996.099997.0494.38385550000.0

在这个表格中,你可能对clusterOneclusterTwo的条目感到有些困惑。例如,在最后一行——49,998 行——cluster 99,996 与 cluster 99,997 合并。但是你知道,在 MNIST 数字数据集中只有 50,000 个观测数据。

clusterOneclusterTwo指的是数字 0 至 49,999 的原始观测值。对于超过 49,999 的数字,聚类编号指的是先前聚类的点。例如,50,000 指的是在第 0 行形成的新聚类,50,001 指的是在第 1 行形成的新聚类,依此类推。

在第 49,998 行,clusterOne,99,996 指的是在第 49,996 行形成的聚类,而clusterTwo,99,997 指的是在第 49,997 行形成的聚类。你可以继续使用这个公式来查看聚类是如何被合并的。

评估聚类结果

现在我们已经有了树状图,请确定在哪里切断树状图以获得我们想要的聚类数目。为了更容易地将层次聚类的结果与k-means 的结果进行比较,让我们将树状图切割成恰好 20 个聚类。然后,我们将使用聚类准确度指标——在k-means部分定义——来评估层次聚类的聚类的同质性。

要从树状图中创建我们想要的聚类,让我们从 SciPy 引入fcluster库。我们需要指定树状图的距离阈值,以确定我们剩下多少个不同的聚类。距离阈值越大,我们得到的聚类就越少。在我们设定的距离阈值内的数据点将属于同一个聚类。较大的距离阈值类似于在非常高的垂直点剪切倒置树。因为随着树的高度越来越高,越来越多的点被分组在一起,我们得到的聚类就越少。

要获得确切的 20 个聚类,我们需要尝试不同的距离阈值,就像这里做的一样。fcluster库将使用我们指定的距离阈值对我们的树状图进行切割。MNIST 手写数字数据集中的每一个观测值将获得一个聚类标签,并且我们将这些标签存储在一个 Pandas DataFrame 中:

from scipy.cluster.hierarchy import fcluster

distance_threshold = 160
clusters = fcluster(Z, distance_threshold, criterion='distance')
X_train_hierClustered = \
    pd.DataFrame(data=clusters,index=X_train_PCA.index,columns=['cluster'])

让我们验证确实有恰好 20 个不同的聚类,考虑到我们选择的距离阈值:

print("Number of distinct clusters: ", \
      len(X_train_hierClustered['cluster'].unique()))

正如预期的那样,这证实了 20 个聚类:

Number of distinct clusters: 20

现在,让我们评估结果:

countByCluster_hierClust, countByLabel_hierClust, \
    countMostFreq_hierClust, accuracyDF_hierClust, \
    overallAccuracy_hierClust, accuracyByLabel_hierClust \
    = analyzeCluster(X_train_hierClustered, y_train)

print("Overall accuracy from hierarchical clustering: ", \
      overallAccuracy_hierClust)

我们发现总体准确度约为 77%,甚至比k-means 的约 70%准确度更好:

Overall accuracy from hierarchical clustering: 0.76882

让我们也评估每个聚类的准确度。

如下所示,准确度变化相当大。对于一些聚类,准确度非常高,接近 100%。对于一些聚类,准确度略低于 50%:

0       0.987962
1       0.983727
2       0.988998
3       0.597356
4       0.678642
5       0.442478
6       0.950033
7       0.829060
8       0.976062
9       0.986141
10      0.990183
11      0.992183
12      0.971033
13      0.554273
14      0.553617
15      0.720183
16      0.538891
17      0.484590
18      0.957732
19      0.977310
dtype:  float64

总体而言,层次聚类在 MNIST 手写数字数据集上表现良好。请记住,这是在不使用任何标签的情况下完成的。

在实际示例中,它将如何工作:首先我们会应用降维(如 PCA),然后执行聚类(如层次聚类),最后我们会为每个聚类手动标记几个点。例如,对于 MNIST 数字数据集,如果我们没有任何标签,我们会查看每个聚类中的几幅图像,并基于它们显示的数字对这些图像进行标记。只要聚类足够同质,我们生成的少量手动标签就可以自动应用于聚类中的所有其他图像。

突然之间,我们几乎可以以 77%的准确率对我们 50,000 个数据集中的所有图像进行标记。这令人印象深刻,并突显了无监督学习的力量。

DBSCAN

现在让我们转向第三个也是最后一个主要的聚类算法,DBSCAN,它代表具有噪声的基于密度的空间聚类。正如其名称所示,这种聚类算法基于点的密度进行分组。

DBSCAN 将紧密排列的点分组在一起,其中“紧密”定义为在一定距离内存在最少数量的点。如果点在多个聚类的一定距离内,则将其与其最密集的聚类分组在一起。不在任何其他聚类的一定距离内的任何实例被标记为离群点。

k-means 和层次聚类中,所有点都必须被聚类,而且离群点处理不当。在 DBSCAN 中,我们可以明确将点标记为离群点,避免必须将它们聚类。这非常强大。与其他聚类算法相比,DBSCAN 在数据中通常由离群点引起的失真问题上要少得多。此外,像层次聚类一样,但不像k-means,我们不需要预先指定聚类的数量。

DBSCAN 算法

现在让我们首先使用 Scikit-Learn 中的 DBSCAN 库。我们需要指定两点之间被视为相邻的最大距离(称为eps)和称为min_samples的最小样本数以被称为聚类的组。eps的默认值是 0.5,min_samples的默认值是 5。如果eps设置得太低,可能没有足够的点接近其他点以被视为相邻。因此,所有点将保持未聚类状态。如果eps设置得太高,可能会将许多点聚类在一起,只有少数点会保持未聚类状态,实际上被标记为数据集中的离群点。

我们需要为我们的 MNIST 数字数据集寻找最佳的epsmin_samples指定在eps距离内需要多少点才能称为一个簇。一旦有足够数量的紧密排列的点,任何距离这些所谓的核心点eps距离内的其他点都属于该簇,即使这些其他点周围没有达到eps距离内的min_samples数量的点。如果这些其他点周围没有min_samples数量的点在eps距离内,它们被称为该簇的边界点

一般来说,随着min_samples的增加,簇的数量减少。与eps类似,我们需要为我们的 MNIST 数字数据集寻找最佳的min_samples。正如您所见,这些簇有核心点和边界点,但就所有意图和目的而言,它们都属于同一组。所有未被分组的点——无论是簇的核心点还是边界点——都被标记为离群点。

应用 DBSCAN 到我们的数据集

现在让我们转向我们的具体问题。与以前一样,我们将对经过 PCA 降维的 MNIST 数字数据集的前 100 个主成分应用 DBSCAN 算法:

from sklearn.cluster import DBSCAN

eps = 3
min_samples = 5
leaf_size = 30
n_jobs = 4

db = DBSCAN(eps=eps, min_samples=min_samples, leaf_size=leaf_size,
            n_jobs=n_jobs)

cutoff = 99
X_train_PCA_dbscanClustered = db.fit_predict(X_train_PCA.loc[:,0:cutoff])
X_train_PCA_dbscanClustered = \
    pd.DataFrame(data=X_train_PCA_dbscanClustered, index=X_train.index, \
                 columns=['cluster'])

countByCluster_dbscan, countByLabel_dbscan, countMostFreq_dbscan, \
    accuracyDF_dbscan, overallAccuracy_dbscan, accuracyByLabel_dbscan \
    = analyzeCluster(X_train_PCA_dbscanClustered, y_train)

overallAccuracy_dbscan

我们将保持min_samples的默认值为 5,但我们将调整eps为 3,以避免集群中点数过少。

这里是总体精度:

Overall accuracy from DBSCAN: 0.242

如您所见,与k-means 和层次聚类相比,准确率非常低。我们可以调整参数epsmin_samples来改善结果,但似乎 DBSCAN 不适合为这个特定数据集的观测进行聚类。

为了探索原因,让我们看看簇(表 5-3)。

表 5-3:DBSCAN 的簇结果

clusterclusterCount
0–139575
108885
28720
3592
41851
53838
64122
73922
8416
92016

大多数点都未被聚类。您可以在图中看到这一点。在训练集中的 50,000 个观测中,39,651 个点属于簇-1,这意味着它们不属于任何簇。它们被标记为离群点——即噪声。

8,885 个点属于 0 号簇。然后是一长串较小规模的簇。看起来 DBSCAN 在找到明显的密集点组时有困难,因此在基于 MNIST 图像展示的数字进行聚类方面表现不佳。

HDBSCAN

让我们尝试 DBSCAN 的另一个版本,看看结果是否会改善。这个版本被称为HDBSCAN,或者层次 DBSCAN。它采用我们介绍过的 DBSCAN 算法,并将其转换为层次聚类算法。换句话说,它基于密度进行分组,然后迭代地根据距离链接基于密度的簇,就像我们在前一节介绍的层次聚类算法中做的那样。

该算法的两个主要参数是min_cluster_sizemin_samples,当设置为None时,默认为min_cluster_size。让我们使用开箱即用的参数选择,评估 HDBSCAN 在我们的 MNIST 数字数据集上是否比 DBSCAN 表现更好:

import hdbscan

min_cluster_size = 30
min_samples = None
alpha = 1.0
cluster_selection_method = 'eom'

hdb = hdbscan.HDBSCAN(min_cluster_size=min_cluster_size, \
        min_samples=min_samples, alpha=alpha, \
        cluster_selection_method=cluster_selection_method)

cutoff = 10
X_train_PCA_hdbscanClustered = \
    hdb.fit_predict(X_train_PCA.loc[:,0:cutoff])

X_train_PCA_hdbscanClustered = \
    pd.DataFrame(data=X_train_PCA_hdbscanClustered, \
    index=X_train.index, columns=['cluster'])

countByCluster_hdbscan, countByLabel_hdbscan, \
    countMostFreq_hdbscan, accuracyDF_hdbscan, \
    overallAccuracy_hdbscan, accuracyByLabel_hdbscan \
    = analyzeCluster(X_train_PCA_hdbscanClustered, y_train)

这里是总体准确率:

Overall accuracy from HDBSCAN: 0.24696

在 25%时,这比 DBSCAN 略好一点,但远远低于k-means 和层次聚类超过 70%的表现。表 5-4 展示了各个聚类的准确率。

表 5-4. HDBSCAN 的聚类结果

聚类聚类数量
0–142570
145140
27942
30605
46295
53252
61119
7545
8232

我们看到与 DBSCAN 类似的现象。大多数点未被聚类,然后是一长串小规模的聚类。结果并未有太大改进。

结论

在本章中,我们介绍了三种主要类型的聚类算法——k-means、层次聚类和 DBSCAN,并将它们应用于 MNIST 数字数据集的降维版本。前两种聚类算法在数据集上表现非常好,能够很好地对图像进行分组,使得跨聚类的标签一致性超过 70%。

对于这个数据集,DBSCAN 的表现并不太理想,但它仍然是一个可行的聚类算法。既然我们已经介绍了这些聚类算法,让我们在第六章中构建一个应用的无监督学习解决方案。

¹ 关于fastcluster的更多信息,请访问该项目的网页。