Python 应用无监督学习(二)

原文:annas-archive.org/md5/6b15c463e64a9f03f0d968a77b424918

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:自编码器

学习目标

本章结束时,您将能够完成以下内容:

  • 解释自编码器的应用领域及其使用案例

  • 理解人工神经网络的实现与应用

  • 使用 Keras 框架实现一个人工神经网络

  • 解释自编码器在降维和去噪中的应用

  • 使用 Keras 框架实现自编码器

  • 解释并实现一个基于卷积神经网络的自编码器模型

本章将介绍自编码器及其应用。

引言

本章继续讨论降维技术,我们将焦点转向自编码器。自编码器是一个特别有趣的研究领域,因为它们提供了一种基于人工神经网络的有监督学习方法,但又是在无监督的环境下进行的。基于人工神经网络的自编码器是一种极为有效的降维手段,并且提供了额外的好处。随着数据、处理能力和网络连接的可用性不断提升,自编码器自 1980 年代末期以来,正在经历一场复兴,重新被广泛使用和研究。这也与人工神经网络的研究相一致,后者最早在 1960 年代被提出和实现。如今,只需进行简单的互联网搜索,就能发现神经网络的流行性和强大能力。

自编码器可以用于图像去噪和生成人工数据样本,结合其他方法,如递归神经网络或长短期记忆LSTM)架构,用于预测数据序列。人工神经网络的灵活性和强大功能使得自编码器能够形成数据的高效表示,之后这些表示可以直接用于极其高效的搜索方法,或作为特征向量进行后续处理。

考虑在图像去噪应用中使用自编码器,我们展示的是左侧的图像(见图 5.1)。可以看到,图像受到一些随机噪声的影响。我们可以使用经过特殊训练的自编码器去除这些噪声,右侧的图像就是去噪后的结果(见图 5.1)。在学习如何去除噪声的过程中,自编码器也学习到了如何编码构成图像的重要信息,并将这些信息解码(或重构)为原始图像的更清晰版本。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_01.jpg

图 5.1:自编码器去噪
注意

此图像修改自 www.freenzphotos.com/free-photos-of-bay-of-plenty/stormy-fishermen/ 并遵循 CC0 许可协议。

本例演示了自编码器在无监督学习中有用的一个方面(编码阶段),以及在生成新图像时有用的另一个方面(解码阶段)。在本章中,我们将进一步探讨自编码器的这两个有用阶段,并将自编码器的输出应用于 CIFAR-10 数据集的聚类。

这里是编码器和解码器的表示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_02.jpg

图 5.2:编码器/解码器表示

人工神经网络基础

鉴于自编码器是基于人工神经网络的,因此理解神经网络的原理对于理解自编码器至关重要。本章的这一部分将简要回顾人工神经网络的基础知识。需要注意的是,神经网络的许多方面超出了本书的范围。神经网络的主题本身就能填满许多本书,这一部分并不是对该主题的详尽讨论。

如前所述,人工神经网络主要用于监督学习问题,其中我们有一组输入信息,例如一系列图像,我们正在训练一个算法将这些信息映射到所需的输出,例如类别或标签。以 CIFAR-10 数据集(图 5.3)为例,它包含了 10 个不同类别的图像(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车),每个类别有 6,000 张图像。当神经网络用于监督学习时,图像被输入到网络中,而对应的类别标签则是网络的期望输出。

然后,网络经过训练,以最大化推断或预测给定图像正确标签的能力。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_03.jpg

图 5.3:CIFAR-10 数据集
注意

此图来自www.cs.toronto.edu/~kriz/cifar.html,出自《从微小图像中学习多层特征》,Alex Krizhevsky,2009 年。

神经元

人工神经网络得名于生物神经网络,这些神经网络通常存在于大脑中。虽然这一类比的准确性是值得质疑的,但它是帮助理解人工神经网络概念的有用隐喻。与生物神经元类似,神经元是构建所有神经网络的基本单元,通过不同的配置将多个神经元连接起来,从而形成更强大的结构。每个神经元(图 5.4)由四个部分组成:输入值、可调权重(θ)、作用于输入值的激活函数以及最终的输出值:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_04.jpg

图 5.4:神经元的解剖结构

激活函数的选择是根据神经网络的目标来特定选择的,有许多常用的函数,包括tanhsigmoidlinearsigmoidReLU(修正线性单元)。在本章中,我们将同时使用sigmoidReLU激活函数,接下来我们将更详细地讨论它们。

Sigmoid 函数

Sigmoid 激活函数因其能将输入值转化为接近二进制的输出,因此在神经网络分类任务中非常常见。Sigmoid 函数的输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_05.jpg

图 5.5:Sigmoid 函数的输出

我们可以在图 5.5中看到,sigmoid 函数的输出随着x的增大渐近于 1,而当x在负方向远离 0 时,输出渐近于 0。这个函数常用于分类任务,因为它提供接近二进制的输出,表示是否属于类(0)或类(1)。

修正线性单元(ReLU)

修正线性单元是一个非常有用的激活函数,通常在神经网络的中间阶段使用。简单来说,输入小于 0 时输出为 0,大于 0 时输出为输入值本身。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_06.jpg

图 5.6:ReLU 的输出

练习 18:模拟人工神经网络的神经元

在这个练习中,我们将通过使用 sigmoid 函数,实际介绍神经元在 NumPy 中的编程表示。我们将固定输入并调整可调权重,以研究其对神经元的影响。有趣的是,这个模型也非常接近于逻辑回归的监督学习方法。请执行以下步骤:

  1. 导入numpy和 matplotlib 包:

    import numpy as np
    import matplotlib.pyplot as plt
    
  2. 配置 matplotlib 以启用使用 Latex 渲染图像中的数学符号:

    plt.rc('text', usetex=True)
    
  3. sigmoid函数定义为 Python 函数:

    def sigmoid(z):
        return np.exp(z) / (np.exp(z) + 1)
    
    注意

    在这里,我们使用的是 sigmoid 函数。你也可以使用 ReLU 函数。尽管 ReLU 激活函数在人工神经网络中非常强大,但它的定义非常简单。它只需要在输入大于 0 时返回输入值,否则返回 0:

    def relu(x):

    return np.max(0, x)

  4. 定义神经元的输入(x)和可调权重(theta)。在这个示例中,输入(x)将是 100 个在-55之间线性分布的数字。设置theta = 1

    theta = 1
    x = np.linspace(-5, 5, 100)
    x
    

    输出的一部分如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_07.jpg

    图 5.7:打印输入
  5. 计算神经元的输出(y):

    y = sigmoid(x * theta)
    
  6. 绘制神经元的输出与输入的关系图:

    fig = plt.figure(figsize=(10, 7))
    ax = fig.add_subplot(111)
    ax.plot(x, y)
    ax.set_xlabel('$x$', fontsize=22);
    ax.set_ylabel('$h(x\Theta)$', fontsize=22);
    ax.spines['left'].set_position(('data', 0));
    ax.spines['top'].set_visible(False);
    ax.spines['right'].set_visible(False);
    ax.tick_params(axis='both', which='major', labelsize=22)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_08.jpg

    图 5.8:神经元与输入的关系图
  7. 将可调参数 theta 设置为 5,重新计算并存储神经元的输出:

    theta = 5
    y_2 = sigmoid(x * theta)
    
  8. 将可调参数 theta 更改为 0.2,然后重新计算并存储神经元的输出:

    theta = 0.2
    y_3 = sigmoid(x * theta)
    
  9. 在同一图表上绘制神经元的三条不同输出曲线(theta = 1theta = 5theta = 0.2):

    fig = plt.figure(figsize=(10, 7))
    ax = fig.add_subplot(111)
    ax.plot(x, y, label='$\Theta=1$');
    ax.plot(x, y_2, label='$\Theta=5$', linestyle=':');
    ax.plot(x, y_3, label='$\Theta=0.2$', linestyle='--');
    ax.set_xlabel('$x\Theta$', fontsize=22);
    ax.set_ylabel('$h(x\Theta)$', fontsize=22);
    ax.spines['left'].set_position(('data', 0));
    ax.spines['top'].set_visible(False);
    ax.spines['right'].set_visible(False);
    ax.tick_params(axis='both', which='major', labelsize=22);
    ax.legend(fontsize=22);
    

    输出如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_09.jpg

图 5.9: 神经元的输出曲线

在本次练习中,我们使用了一个具有 Sigmoid 激活函数的人工神经网络基本构建模块。我们可以看到,使用 Sigmoid 函数会增加梯度的陡峭度,这意味着只有较小的 x 值才能将输出推向接近 1 或 0。同样,减小 theta 会降低神经元对非零值的敏感度,导致需要更极端的输入值才能将输出推向 0 或 1,从而调节神经元的输出。

活动 8:使用 ReLU 激活函数建模神经元

在本次活动中,我们将研究 ReLU 激活函数以及可调权重对修改 ReLU 单元输出的影响:

  1. 导入 numpy 和 matplotlib。

  2. 将 ReLU 激活函数定义为一个 Python 函数。

  3. 定义神经元的输入(x)和可调权重(theta)。在这个示例中,输入(x)将是 100 个在线性间隔内从-55的数字。设置theta = 1

  4. 计算输出(y)。

  5. 绘制神经元的输出与输入的关系图。

  6. 现在,将 theta 设置为 5,重新计算并存储神经元的输出。

  7. 现在,将 theta 设置为 0.2,然后重新计算并存储神经元的输出。

  8. 在同一图表上绘制神经元的三条不同输出曲线(theta = 1theta = 5,和 theta = 0.2)。

在本活动结束时,您将为 ReLU 激活神经元开发一系列响应曲线。您还将能够描述改变 theta 值对神经元输出的影响。输出将如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_10.jpg

图 5.10: 预期的输出曲线
注意

这个活动的解决方案可以在第 333 页找到。

神经网络:架构定义

单个神经元在孤立状态下并不是特别有用;它提供一个激活函数和调节输出的手段,但单个神经元的学习能力有限。当多个神经元被组合并连接成网络结构时,它们的功能会更强大。通过使用多个不同的神经元并结合各个神经元的输出,可以建立更复杂的关系并构建更强大的学习算法。在本节中,我们将简要讨论神经网络的结构,并使用 Keras 机器学习框架(keras.io/) 实现一个简单的神经网络。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_11.jpg

图 5.11:简化表示的神经网络

图 5.11展示了一个两层全连接神经网络的结构。我们可以做出的第一个观察是,这个结构包含了大量的信息,并且具有高度的连接性,这通过指向每个节点的箭头表示。从图像的左侧开始,我们可以看到神经网络的输入值,表示为(x)值。在这个例子中,每个样本有m个输入值,而且只有第一个样本被输入到网络中,因此,值来自于!凳子的特写

描述自动生成 到 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_Formula_02.png。这些值随后与神经网络第一层的相应权重相乘(https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_Formula_03.png),然后传递到相应神经元的激活函数中。这被称为前馈神经网络。图 5.11中用于标识权重的符号是!标志的特写

描述自动生成,其中i是权重所属的层,j是输入节点的编号(从顶部开始为 1),k是权重传递到的后续层节点。

观察第一层(也称为隐藏层)输出与输出层输入之间的互联性,我们可以看到,有大量的可训练参数(权重)可以用于将输入映射到期望的输出。图 5.11中的网络表示一个n类神经网络分类器,其中每个n节点的输出表示输入属于相应类别的概率。

每一层都可以使用不同的激活函数,如https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_Formula_05.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_Formula_06.png所示,从而允许不同的激活函数混合使用,例如,第一层可以使用 ReLU,第二层可以使用 tanh,第三层可以使用 sigmoid。最终输出通过将前一层输出与相应的权重相乘并求和来计算。

如果我们考虑第一层节点的输出,可以通过将输入值与相应的权重相乘,求和结果,并通过激活函数来计算:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_12.jpg

图 5.12:计算最后一个节点的输出

随着输入和输出之间的层数增加,我们增加了网络的深度。深度的增加也意味着可训练参数的数量增多,同时数据中描述关系的复杂性增加。通常,随着深度的增加,训练网络会变得更加困难,因为选择用于输入的特征变得更加关键。此外,随着我们向每一层添加更多的神经元,我们也增加了神经网络的高度。通过增加更多的神经元,网络描述数据集的能力增强,同时可训练的参数也增多。如果添加了过多的神经元,网络可能会记住数据集中的样本,但无法对新样本进行泛化。构建神经网络的关键在于找到一个平衡点,使得模型既有足够的复杂性来描述数据中的关系,又不会复杂到只会记忆训练样本。

练习 19:定义 Keras 模型

在这个练习中,我们将使用 Keras 机器学习框架定义一个神经网络架构(类似于图 5.11),用于对 CIFAR-10 数据集中的图像进行分类。由于每个输入图像的大小为 32 x 32 像素,输入向量将包含 32*32 = 1,024 个值。CIFAR-10 有 10 个不同的类别,因此神经网络的输出将由 10 个独立的值组成,每个值表示输入数据属于相应类别的概率。

  1. 对于这个练习,我们将需要 Keras 机器学习框架。Keras 是一个高层神经网络 API,通常用于现有库之上,如 TensorFlow 或 Theano。Keras 使得在底层框架之间切换变得更加容易,因为它提供的高层接口在不同的底层库中保持一致。在本书中,我们将使用 TensorFlow 作为底层库。如果你还没有安装 Keras 和 TensorFlow,请使用conda安装:

    !conda install tensforflow keras
    

    或者,你也可以使用pip安装它:

    !pip install tensorflow keras
    
  2. 我们将需要从keras.modelskeras.layers中导入SequentialDense类。导入这些类:

    from keras.models import Sequential
    from keras.layers import Dense
    
  3. 如前所述,输入层将接收 1,024 个值。第二层(层 1)将包含 500 个单元,并且因为网络需要分类 10 个不同的类别,所以输出层将包含 10 个单元。在 Keras 中,模型是通过将有序的层列表传递给Sequential模型类来定义的。此示例使用了Dense层类,它是一个全连接神经网络层。第一层将使用 ReLU 激活函数,而输出层将使用softmax函数来确定每个类别的概率。定义模型:

    model = Sequential([
        Dense(500, input_shape=(1024,), activation='relu'),
        Dense(10, activation='softmax')
    ])
    
  4. 在定义了模型之后,我们可以使用summary方法来确认模型的结构以及其中可训练的参数(或权重)的数量:

    model.summary()
    

    输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_13.jpg

图 5.13:模型中可训练参数的结构与数量

该表总结了神经网络的结构。我们可以看到,模型中有我们指定的两层,第一层包含 500 个单元,第二层包含 10 个输出单元。Param # 列显示了每一层中可训练权重的数量。表格还告诉我们,网络中总共有 517,510 个可训练权重。

在这个练习中,我们在 Keras 中创建了一个神经网络模型,该模型包含超过 500,000 个权重,可以用来对 CIFAR-10 的图像进行分类。在接下来的章节中,我们将训练该模型。

神经网络:训练

定义好神经网络模型后,我们可以开始训练过程;在这个阶段,我们将以监督学习的方式训练模型,以便在继续训练自编码器之前,对 Keras 框架有一定的熟悉度。监督学习模型通过提供输入信息和已知输出信息来训练模型;训练的目标是构建一个网络,使其能够仅通过模型的参数,将输入信息映射到已知输出。

在像 CIFAR-10 这样的监督分类示例中,输入信息是一张图像,而已知输出是该图像所属的类别。在训练过程中,对于每一个样本的预测,使用指定的误差函数计算前馈网络的预测误差。然后,模型中的每个权重都会进行调整,试图减少误差。这一调整过程被称为反向传播,因为误差是从输出开始通过网络向后传播,直到网络的起始位置。

在反向传播过程中,每个可训练的权重都会根据它对整体误差的贡献进行调整,调整幅度与一个被称为学习率的值成正比,该值控制可训练权重变化的速率。看一下图 5.14,我们可以看到,增加学习率的值可以加快误差减少的速度,但也有可能因为步长过大而无法收敛到最小误差。学习率过小可能会导致我们失去耐心,或者根本没有足够的时间找到全局最小值。因此,找到正确的学习率是一个试错过程,尽管从较大的学习率开始并逐步减少通常是一个有效的方法。以下图示展示了学习率的选择:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_14.jpg

图 5.14:选择正确的学习率(一个 epoch 是一次学习步骤)

训练会重复进行,直到预测中的误差不再减少,或者开发者在等待结果时失去耐心。为了完成训练过程,我们首先需要做出一些设计决策,第一个是选择最合适的误差函数。可用的误差函数有很多种,从简单的均方差到更复杂的选项。类别交叉熵(在接下来的练习中使用)是一个非常有用的误差函数,适用于分类多个类别。

在定义了误差函数之后,我们需要选择一种使用误差函数更新可训练参数的方法。最节省内存且有效的更新方法之一是随机梯度下降法(SGD);SGD 有许多变种,所有变种都涉及根据每个权重对计算误差的贡献来调整权重。最后的训练设计决策是模型评估的性能指标,以及选择最佳架构;在分类问题中,这可能是模型的分类准确度,或者在回归问题中,可能是产生最低误差分数的模型。这些比较通常是通过交叉验证方法进行的。

练习 20:训练 Keras 神经网络模型

幸运的是,我们不需要担心手动编写神经网络的各个组件,如反向传播,因为 Keras 框架会为我们管理这些。在本次练习中,我们将使用 Keras 训练一个神经网络,使用前面练习中定义的模型架构对 CIFAR-10 数据集的一个小子集进行分类。与所有机器学习问题一样,第一步也是最重要的一步是尽可能多地了解数据集,这将是本次练习的初步重点:

注意

你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Exercise20下载data_batch_1batches.meta文件。

  1. 导入picklenumpymatplotlib以及从keras.models导入Sequential类,并从keras.layers导入Dense

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Sequential
    from keras.layers import Dense
    
  2. 加载与随附源代码一起提供的 CIFAR-10 数据集样本,该样本位于data_batch_1文件中:

    with open('data_batch_1', 'rb') as f:
        dat = pickle.load(f, encoding='bytes')
    
  3. 数据作为字典加载。显示字典的键:

    dat.keys()
    

    输出结果如下:

    dict_keys([b'batch_label', b'labels', b'data', b'filenames'])
    
  4. 注意,键是作为二进制字符串存储的,表示为b'。我们关心的是数据和标签的内容。我们先来看标签:

    labels = dat[b'labels']
    labels
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_15.jpg

    图 5.15:显示标签
  5. 我们可以看到标签是一个 0 到 9 的值列表,表示每个样本所属的类别。现在,来看一下data键的内容:

    dat[b'data']
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_16.jpg

    图 5.16:数据键的内容
  6. 数据键提供了一个包含所有图像数据的 NumPy 数组。图像数据的形状是什么?

    dat[b'data'].shape
    

    输出结果如下:

    (1000, 3072)
    
  7. 我们可以看到我们有 1000 个样本,但每个样本是一个长度为 3,072 的一维数组。难道图像不应该是 32 x 32 像素吗?是的,它们是,但由于图像是彩色的或 RGB 图像,它们包含三个通道(红、绿、蓝),这意味着图像的尺寸是 32 x 32 x 3。它们还被展开,提供 3,072 长度的向量。所以,我们可以重新调整数组形状,然后可视化一个样本图像。根据 CIFAR-10 文档,前 1,024 个样本是红色,第二个 1,024 个是绿色,第三个 1,024 个是蓝色:

    images = np.zeros((10000, 32, 32, 3), dtype='uint8')
    for idx, img in enumerate(dat[b'data']):
        images[idx, :, :, 0] = img[:1024].reshape((32, 32)) # Red
        images[idx, :, :, 1] = img[1024:2048].reshape((32, 32)) # Green
        images[idx, :, :, 2] = img[2048:].reshape((32, 32)) # Blue
    
  8. 显示前 12 张图片及其标签:

    plt.figure(figsize=(10, 7))
    for i in range(12):
        plt.subplot(3, 4, i + 1)
        plt.imshow(images[i])
        plt.title(labels[i])
        plt.axis('off')
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_17.jpg

    图 5.17:前 12 张图片
  9. 标签的实际意义是什么?要了解这一点,加载 batches.meta 文件:

    with open('batches.meta', 'rb') as f:
        label_strings = pickle.load(f, encoding='bytes')
    
    label_strings
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_18.jpg

    图 5.18:标签的含义
  10. 解码二进制字符串以获取实际标签:

    actual_labels = [label.decode() for label in label_strings[b'label_names']]
    actual_labels
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_19.jpg

    图 5.19:打印实际标签
  11. 打印前 12 张图片的标签:

    for lab in labels[:12]:
        print(actual_labels[lab], end=', ')
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_20.jpg

    图 5.20:前 12 张图片的标签
  12. 现在我们需要准备数据来训练模型。第一步是准备输出。目前,输出是一个 0 到 9 的数字列表,但我们需要每个样本都用一个包含 10 个单位的向量表示,正如前面模型所要求的那样。编码后的输出将是一个形状为 10000 x 10 的 NumPy 数组:

    注意
    one_hot_labels = np.zeros((images.shape[0], 10))
    for idx, lab in enumerate(labels):
        one_hot_labels[idx, lab] = 1
    
  13. 显示前 12 个样本的独热编码值:

    one_hot_labels[:12]
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_21.jpg

    图 5.21:前 12 个样本的独热编码值
  14. 模型有 1,024 个输入,因为它期望输入的是 32 x 32 的灰度图像。将每张图像的三个通道的平均值取出,将其转换为 RGB:

    images = images.mean(axis=-1)
    
  15. 再次显示前 12 张图片:

    plt.figure(figsize=(10, 7))
    for i in range(12):
        plt.subplot(3, 4, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.title(labels[i])
        plt.axis('off')
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_22.jpg

    图 5.22:再次显示前 12 张图片。
  16. 最后,将图片缩放到 0 和 1 之间,这是神经网络所有输入所需要的。由于图像中的最大值为 255,我们将其直接除以 255:

    images /= 255.
    
  17. 我们还需要将图像的形状调整为 10,000 x 1,024:

    images = images.reshape((-1, 32 ** 2))
    
  18. 重新定义模型,使用与练习 19定义 Keras 模型相同的架构:

    model = Sequential([
        Dense(500, input_shape=(1024,), activation='relu'),
        Dense(10, activation='softmax')
    
    ])
    
  19. 现在我们可以在 Keras 中训练模型。我们首先需要编译方法,以指定训练参数。我们将使用分类交叉熵,随机梯度下降,并使用分类准确率作为性能指标:

    model.compile(loss='categorical_crossentropy',
                  optimizer='sgd',
                  metrics=['accuracy'])
    
  20. 使用反向传播方法训练模型,训练 100 个周期,并使用模型的 fit 方法:

    model.fit(images, one_hot_labels, epochs=100)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_23.jpg

    图 5.23:训练模型
  21. 我们使用这个网络对 1,000 个样本进行了分类,并取得了大约 90% 的分类准确率。请再次检查对前 12 个样本的预测结果:

    predictions = model.predict(images[:12])
    predictions
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_24.jpg

    图 5.24:打印预测结果
  22. 我们可以使用 argmax 方法来确定每个样本最可能的类别:

    np.argmax(predictions, axis=1)
    

    输出结果如下:

    array([6, 9, 9, 4, 1, 1, 2, 7, 8, 3, 2, 7])
    
  23. 与标签进行比较:

    labels[:12]
    

    输出结果如下:

    [6, 9, 9, 4, 1, 1, 2, 7, 8, 3, 4, 7]
    

网络在这些样本中犯了一个错误,即它将倒数第二个样本分类为 2(鸟)而不是 4(鹿)。恭喜你!你刚刚成功训练了一个 Keras 神经网络模型。完成下一个活动,进一步巩固你在训练神经网络方面的技能。

活动 9:MNIST 神经网络

在本次活动中,你将训练一个神经网络来识别 MNIST 数据集中的图像,并进一步巩固你在训练神经网络方面的技能。这个活动为许多不同分类问题中的神经网络架构奠定了基础,尤其是在计算机视觉领域。从目标检测和识别到分类,这种通用结构被应用于多种不同的场景。

这些步骤将帮助你完成活动:

  1. 导入 picklenumpymatplotlib,以及来自 Keras 的 SequentialDense 类。

  2. 加载包含前 10,000 张图像及其相应标签的 mnist.pkl 文件,这些数据来自附带源代码中的 MNIST 数据集。MNIST 数据集是一系列 28 x 28 像素的手写数字灰度图像,数字范围从 0 到 9。提取图像和标签。

    注意

    你可以在 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Activity09 找到 mnist.pkl 文件。

  3. 绘制前 10 个样本及其对应标签。

  4. 使用独热编码对标签进行编码。

  5. 准备图像以便输入神经网络。提示:这个过程有 两个 独立的步骤。

  6. 在 Keras 中构建一个神经网络模型,接受已准备好的图像,并具有 600 单元的隐藏层,激活函数为 ReLU,输出层的单元数与类别数相同。输出层使用 softmax 激活函数。

  7. 使用多类交叉熵、随机梯度下降和准确性性能指标编译模型。

  8. 训练模型。要达到训练数据至少 95%的分类准确率,需要多少个训练周期(epoch)?

通过完成此活动,你已经训练了一个简单的神经网络来识别手写数字 0 到 9。你还开发了一个用于构建分类问题神经网络的通用框架。通过这个框架,你可以扩展和修改网络以适应其他任务。

注意

本活动的解答可以在第 335 页找到。

自编码器

既然我们已经习惯在 Keras 中开发监督学习神经网络模型,我们可以将注意力转回到无监督学习及本章的主要主题——自编码器。自编码器是一种专门设计的神经网络架构,旨在以高效且具有描述性的方式将输入信息压缩到较低维度空间。自编码器网络可以分解为两个独立的子网络或阶段:编码阶段和解码阶段。首先,编码阶段将输入信息通过一个后续的层进行压缩,该层的单元数少于输入样本的大小。随后,解码阶段扩展压缩后的图像,并试图将压缩数据恢复为其原始形式。因此,网络的输入和期望输出是相同的;网络输入的是 CIFAR-10 数据集中的一张图像,并试图返回相同的图像。该网络架构如图 5.25所示;在此图中,我们可以看到自编码器的编码阶段将表示信息的神经元数量减少,而解码阶段则将压缩格式的图像恢复到其原始状态。解码阶段的使用有助于确保编码器正确地表示了信息,因为恢复图像到原始状态所需的所有信息都来自于压缩后的表示。接下来,我们将使用 CIFAR-10 数据集来实现一个简化的自编码器模型:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_25.jpg

图 5.25:简单自编码器网络架构

练习 21:简单自编码器

在本练习中,我们将为 CIFAR-10 数据集的样本构建一个简单的自编码器,将图像中存储的信息压缩以供后续使用。

注意

你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Exercise21下载data_batch_1文件。

  1. 导入picklenumpymatplotlib,以及从keras.models中导入Model类,从keras.layers中导入InputDense

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers import Input, Dense
    
  2. 加载数据:

    with open('data_batch_1', 'rb') as f:
        dat = pickle.load(f, encoding='bytes')
    
  3. 由于这是无监督学习方法,我们只关注图像数据。按照前一个练习加载图像数据:

    images = np.zeros((10000, 32, 32, 3), dtype='uint8')
    for idx, img in enumerate(dat[b'data']):
        images[idx, :, :, 0] = img[:1024].reshape((32, 32)) # Red
        images[idx, :, :, 1] = img[1024:2048].reshape((32, 32)) # Green
        images[idx, :, :, 2] = img[2048:].reshape((32, 32)) # Blue
    
  4. 将图像转换为灰度图像,缩放至 0 到 1 之间,并将每个图像展平为一个长度为 1,024 的向量:

    images = images.mean(axis=-1)
    images = images / 255.0
    images = images.reshape((-1, 32 ** 2))
    images
    
  5. 定义自编码器模型。由于我们需要访问编码器阶段的输出,我们将使用一种稍微不同于之前的方法来定义模型。定义一个包含1024个单元的输入层:

    input_layer = Input(shape=(1024,))
    
  6. 定义后续的Dense层,包含256个单元(压缩比为 1024/256 = 4),并使用 ReLU 激活函数作为编码阶段。请注意,我们已将该层分配给一个变量,并将前一层传递给类的调用方法:

    encoding_stage = Dense(256, activation='relu')(input_layer)
    
  7. 定义一个后续解码器层,使用 sigmoid 函数作为激活函数,并与输入层具有相同的形状。选择 sigmoid 函数是因为网络的输入值仅介于 0 和 1 之间:

    decoding_stage = Dense(1024, activation='sigmoid')(encoding_stage)
    
  8. 通过将网络的第一层和最后一层传递给Model类来构建模型:

    autoencoder = Model(input_layer, decoding_stage)
    
  9. 使用二元交叉熵损失函数和 adadelta 梯度下降法编译自编码器:

    autoencoder.compile(loss='binary_crossentropy',
                  optimizer='adadelta')
    
    注意

    adadelta是随机梯度下降法的一个更复杂版本,其中学习率根据最近一段时间内的梯度更新窗口进行调整。与其他学习率调整方法相比,这避免了非常旧的周期的梯度影响学习率。

  10. 现在,让我们拟合模型;同样,我们将图像作为训练数据并作为期望的输出。训练 100 个周期:

    autoencoder.fit(images, images, epochs=100)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_26.jpg

    图 5.26:训练模型
  11. 计算并存储编码阶段前五个样本的输出:

    encoder_output = Model(input_layer, encoding_stage).predict(images[:5])
    
  12. 将编码器输出重塑为 16 x 16(16 x 16 = 256)像素,并乘以 255:

    encoder_output = encoder_output.reshape((-1, 16, 16)) * 255
    
  13. 计算并存储解码阶段前五个样本的输出:

    decoder_output = autoencoder.predict(images[:5])
    
  14. 将解码器的输出重塑为 32 x 32 并乘以 255:

    decoder_output = decoder_output.reshape((-1, 32,32)) * 255
    
  15. 重塑原始图像:

    images = images.reshape((-1, 32, 32))
    plt.figure(figsize=(10, 7))
    for i in range(5):
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
    
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')   
    
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i], cmap='gray')
        plt.axis('off')        
    

    输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_27.jpg

图 5.27:简单自编码器的输出

图 5.27中,我们可以看到三行图像。第一行是原始的灰度图像,第二行是对应于原始图像的自编码器输出,最后,第三行是从编码输入中重建的原始图像。我们可以看到第三行中的解码图像包含了图像的基本形状信息;我们可以看到青蛙和鹿的主体,以及卡车和汽车的轮廓。鉴于我们只训练了 100 个样本,这个练习也可以通过增加训练周期的数量来进一步提高编码器和解码器的性能。现在我们已经获得了训练好的自编码器阶段的输出,我们可以将其作为其他无监督算法的特征向量,如 K-means 或 K 近邻。

活动 10:简单 MNIST 自编码器

在此活动中,您将为包含在附带源代码中的 MNIST 数据集创建一个自编码器网络。像本活动中构建的自编码器网络,可以在无监督学习的预处理阶段非常有用。网络产生的编码信息可以用于聚类或分割分析,例如基于图像的网络搜索:

  1. 导入picklenumpymatplotlib,以及来自 Keras 的ModelInputDense类。

  2. 从随附源代码提供的 MNIST 数据集样本加载图像(mnist.pkl)。

    注意

    你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Activity10下载mnist.pklP-code文件。

  3. 准备输入神经网络的图像。作为提示,这个过程分为两个独立的步骤。

  4. 构建一个简单的自编码器网络,在编码阶段后将图像大小缩小到 10 x 10。

  5. 使用二进制交叉熵损失函数和adadelta梯度下降法编译自编码器。

  6. 拟合编码器模型。

  7. 计算并存储前五个样本的编码阶段输出。

  8. 将编码器输出重塑为 10 x 10(10 x 10 = 100)像素并乘以 255。

  9. 计算并存储前五个样本的解码阶段输出。

  10. 将解码器的输出重塑为 28 x 28 并乘以 255。

  11. 绘制原始图像、编码器输出和解码器输出。

完成此活动后,你将成功训练一个自编码器网络,该网络能够提取数据集中关键信息,并为后续处理做好准备。输出将类似于以下内容:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_28.jpg

图 5.28:原始图像、编码器输出和解码器的预期图示
注意

此活动的解决方案可以在第 338 页找到。

练习 22:多层自编码器

在本练习中,我们将为 CIFAR-10 数据集的样本构建一个多层自编码器,将图像中存储的信息压缩以备后续使用:

注意

你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Exercise22下载data_batch_1文件。

  1. 导入picklenumpymatplotlib,以及来自keras.modelsModel类,并导入来自keras.layersInputDense类:

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers import Input, Dense
    
  2. 加载数据:

    with open('data_batch_1', 'rb') as f:
        dat = pickle.load(f, encoding='bytes')
    
  3. 由于这是无监督学习方法,我们只关心图像数据。按照之前的练习加载图像数据:

    images = np.zeros((10000, 32, 32, 3), dtype='uint8')
    for idx, img in enumerate(dat[b'data']):
        images[idx, :, :, 0] = img[:1024].reshape((32, 32)) # Red
        images[idx, :, :, 1] = img[1024:2048].reshape((32, 32)) # Green
        images[idx, :, :, 2] = img[2048:].reshape((32, 32)) # Blue
    
  4. 将图像转换为灰度,缩放到 0 到 1 之间,并将每个图像展平为一个长度为 1,024 的向量:

    images = images.mean(axis=-1)
    images = images / 255.0
    images = images.reshape((-1, 32 ** 2))
    images
    
  5. 定义多层自编码器模型。我们将使用与简单自编码器模型相同形状的输入:

    input_layer = Input(shape=(1024,))
    
  6. 我们将在 256 自编码器阶段之前添加另一个层,这次使用 512 个神经元:

    hidden_encoding = Dense(512, activation='relu')(input_layer)
    
  7. 使用与之前练习相同大小的自编码器,但这次输入层是hidden_encoding层:

    encoding_stage = Dense(256, activation='relu')(hidden_encoding)
    
  8. 添加解码隐藏层:

    hidden_decoding = Dense(512, activation='relu')(encoding_stage)
    
  9. 使用与之前练习相同的输出阶段,这次连接到隐藏的解码阶段:

    decoding_stage = Dense(1024, activation='sigmoid')(hidden_decoding)
    
  10. 通过将网络的第一层和最后一层传递给 Model 类来构建模型:

    autoencoder = Model(input_layer, decoding_stage)
    
  11. 使用二元交叉熵损失函数和 adadelta 梯度下降法编译自编码器:

    autoencoder.compile(loss='binary_crossentropy',
                  optimizer='adadelta')
    
  12. 现在,让我们拟合模型;再次,我们将图像作为训练数据和期望的输出。训练 100 个 epoch:

    autoencoder.fit(images, images, epochs=100)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_29.jpg

    图 5.29: 训练模型
  13. 计算并存储前五个样本的编码阶段输出:

    encoder_output = Model(input_stage, encoding_stage).predict(images[:5])
    
  14. 将编码器输出调整为 10 x 10(10 x 10 = 100)像素,并乘以 255:

    encoder_output = encoder_output.reshape((-1, 10, 10)) * 255
    
  15. 计算并存储前五个样本的解码阶段输出:

    decoder_output = autoencoder.predict(images[:5])
    
  16. 将解码器的输出调整为 28 x 28,并乘以 255:

    decoder_output = decoder_output.reshape((-1, 28, 28)) * 255
    
  17. 绘制原始图像、编码器输出和解码器:

    images = images.reshape((-1, 28, 28))
    plt.figure(figsize=(10, 7))
    for i in range(5):
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
    
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')   
    
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i], cmap='gray')
        plt.axis('off')      
    

    输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_30.jpg

图 5.30: 多层自编码器的输出

通过查看简单和多层自编码器产生的误差得分,并比较 图 5.27图 5.30,我们可以看到这两种编码器结构的输出几乎没有区别。两幅图的中间行显示,两个模型学习到的特征实际上是不同的。我们可以使用许多选项来改进这两个模型,例如,训练更多的 epochs,使用不同数量的单元或神经元,或使用不同数量的层。这个练习的构建旨在展示如何构建和使用自编码器,但优化通常是一个系统性的试错过程。我们鼓励您调整模型的一些参数,并自己研究不同的结果。

卷积神经网络

在构建我们之前的所有神经网络模型时,您可能会注意到,我们在将图像转换为灰度图像时移除了所有颜色信息,并将每张图像展平为一个长度为 1,024 的单一向量。通过这种方式,我们本质上丢失了可能对我们有用的许多信息。图像中的颜色可能是特定于图像中的类别或物体的;此外,我们还丢失了许多关于图像的空间信息,例如,卡车图像中拖车相对于驾驶室的位置,或鹿的腿部相对于头部的位置。卷积神经网络不会遭遇这种信息丢失的问题。这是因为,卷积神经网络不是使用平坦的可训练参数结构,而是将权重存储在网格或矩阵中,这意味着每组参数可以在其结构中具有多个层。通过将权重组织成网格,我们防止了空间信息的丢失,因为这些权重以滑动的方式应用于图像。此外,通过拥有多个层,我们可以保留与图像相关的颜色通道。

在开发基于卷积神经网络的自编码器时,MaxPooling2D 和 Upsampling2D 层非常重要。MaxPooling2D 层通过在输入的窗口中选择最大值,来对输入矩阵在两个维度上进行降采样或减小尺寸。假设我们有一个 2 x 2 的矩阵,其中三个单元的值为 1,一个单元的值为 2:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_31.jpg

图 5.31:示例矩阵演示

如果提供给 MaxPooling2D 层,这个矩阵会返回一个单一的 2 值,从而在两个方向上都将输入的尺寸减半。

UpSampling2D 层的作用与 MaxPooling2D 层相反,它增加了输入的尺寸,而不是减少它。上采样过程会重复数据的行和列,从而将输入矩阵的大小加倍。

练习 23:卷积自编码器

在本练习中,我们将开发一个基于卷积神经网络的自编码器,并将其性能与之前的全连接神经网络自编码器进行比较:

注意

你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Exercise23下载data_batch_1文件。

  1. 导入picklenumpymatplotlib,以及来自keras.modelsModel类,和来自keras.layersInputConv2DMaxPooling2DUpSampling2D

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D
    
  2. 加载数据:

    with open('data_batch_1', 'rb') as f:
        dat = pickle.load(f, encoding='bytes')
    
  3. 由于这是一个无监督学习方法,我们只对图像数据感兴趣。根据之前的练习加载图像数据:

    images = np.zeros((10000, 32, 32, 3), dtype='uint8')
    for idx, img in enumerate(dat[b'data']):
        images[idx, :, :, 0] = img[:1024].reshape((32, 32)) # Red
        images[idx, :, :, 1] = img[1024:2048].reshape((32, 32)) # Green
        images[idx, :, :, 2] = img[2048:].reshape((32, 32)) # Blue
    
  4. 由于我们使用的是卷积网络,因此可以仅通过重新缩放图像来使用它们:

    images = images / 255.
    
  5. 定义卷积自编码器模型。我们将使用与图像相同形状的输入:

    input_layer = Input(shape=(32, 32, 3,))
    
  6. 添加一个卷积层,包含 32 个层或滤波器,一个 3 x 3 的权重矩阵,ReLU 激活函数,并使用相同的填充方式,这意味着输出的尺寸与输入图像相同:

    注意
    hidden_encoding = Conv2D(
        32, # Number of layers or filters in the weight matrix
        (3, 3), # Shape of the weight matrix
        activation='relu',
        padding='same', # How to apply the weights to the images
    )(input_layer)
    
  7. 向编码器添加一个最大池化层,使用 2 x 2 的卷积核。MaxPooling查看图像中的所有值,使用 2 x 2 的矩阵进行扫描。在每个 2 x 2 区域中,返回最大值,从而将编码层的尺寸减小一半:

    encoded = MaxPooling2D((2, 2))(hidden_encoding)
    
  8. 添加一个解码卷积层(此层应与之前的卷积层相同):

    hidden_decoding = Conv2D(
        32, # Number of layers or filters in the weight matrix
        (3, 3), # Shape of the weight matrix
        activation='relu',
        padding='same', # How to apply the weights to the images
    )(encoded)
    
  9. 现在我们需要将图像恢复到原始尺寸,因此我们将进行与MaxPooling2D相同大小的上采样:

    upsample_decoding = UpSampling2D((2, 2))(hidden_decoding)
    
  10. 添加最后的卷积层,为图像的 RGB 通道使用三个层:

    decoded = Conv2D(
        3, # Number of layers or filters in the weight matrix
        (3, 3), # Shape of the weight matrix
        activation='sigmoid',
        padding='same', # How to apply the weights to the images
    )(upsample_decoding)
    
  11. 通过将网络的第一层和最后一层传递给Model类来构建模型:

    autoencoder = Model(input_layer, decoded)
    
  12. 显示模型的结构:

    autoencoder.summary()
    

    请注意,与之前的自动编码器示例相比,我们的可训练参数要少得多。这是一个特定的设计决策,目的是确保该示例可以在各种硬件上运行。卷积网络通常需要更多的处理能力,并且经常需要特殊硬件,如图形处理单元(GPU)。

  13. 使用二元交叉熵损失函数和adadelta梯度下降法编译自动编码器:

    autoencoder.compile(loss='binary_crossentropy',
                  optimizer='adadelta')
    
  14. 现在,让我们训练模型;再次地,我们将图像作为训练数据,并将其作为期望的输出。训练 20 个 epoch,因为卷积神经网络的计算需要更多时间:

    autoencoder.fit(images, images, epochs=20)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_32.jpg

    图 5.32:训练模型

    请注意,在第二个 epoch 之后,误差已经比之前的自动编码器练习要小,这表明编码/解码模型有所改进。这种误差的减少主要归因于卷积神经网络没有丢弃太多数据,并且编码后的图像是 16 x 16 x 32,这比之前的 16 x 16 尺寸要大得多。此外,我们没有对图像进行压缩,因为它们现在包含的像素较少(16 x 16 x 32 = 8,192),但深度(32 x 32 x 3,072)比之前更多。这些信息已被重新排列,以便更有效地进行编码/解码过程。

  15. 计算并存储前五个样本的编码阶段输出:

    encoder_output = Model(input_layer, encoded).predict(images[:5])
    
  16. 每个编码图像的形状为 16 x 16 x 32,这是由于为卷积阶段选择的滤波器数量。因此,我们不能在不进行修改的情况下直接可视化它们。我们将它们重塑为 256 x 32 大小以进行可视化:

    encoder_output = encoder_output.reshape((-1, 256, 32))
    
  17. 获取前五个图像的解码器输出:

    decoder_output = autoencoder.predict(images[:5])
    
  18. 绘制原始图像、均值编码器输出和解码器:

    plt.figure(figsize=(10, 7))
    for i in range(5):
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
    
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')   
    
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i])
        plt.axis('off')    
    

    输出结果如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_33.jpg

图 5.33:原始图像、编码器输出和解码器

活动 11:MNIST 卷积自动编码器

在本次活动中,我们将通过使用 MNIST 数据集来加深对卷积自动编码器的理解。当使用合理大小的基于图像的数据集时,卷积自动编码器通常能显著提高性能。这在使用自动编码器生成人工图像样本时尤其有用:

  1. 导入picklenumpymatplotlib,以及从keras.models中导入Model类,从keras.layers中导入InputConv2DMaxPooling2DUpSampling2D

  2. 加载mnist.pkl文件,该文件包含来自 MNIST 数据集的前 10,000 张图像及其对应标签,这些文件可在随附的源代码中找到。

    注意

    你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Activity11下载mnist.pkl文件。

  3. 将图像重新缩放为值在 0 和 1 之间。

  4. 我们需要重塑图像,添加一个单一的深度通道,以便与卷积阶段一起使用。将图像重塑为 28 x 28 x 1 的形状。

  5. 定义输入层。我们将使用与图像相同形状的输入。

  6. 添加一个卷积阶段,使用 16 层或过滤器,3 x 3 的权重矩阵,ReLU 激活函数,并使用相同填充,这意味着输出的大小与输入图像相同。

  7. 在编码器中添加一个最大池化层,使用 2 x 2 的卷积核。

  8. 添加一个解码卷积层。

  9. 添加一个上采样层。

  10. 按照初始图像深度,使用 1 层添加最终的卷积阶段。

  11. 通过将网络的第一层和最后一层传递给Model类来构建模型。

  12. 显示模型的结构。

  13. 使用二元交叉熵损失函数和adadelta梯度下降法编译自编码器。

  14. 现在,让我们来拟合模型;再次将图像作为训练数据和期望的输出。训练 20 个周期,因为卷积网络需要较长的计算时间。

  15. 计算并存储编码阶段对前五个样本的输出。

  16. 为了可视化,将编码器的输出重塑为每张图像大小为 X*Y。

  17. 获取解码器对前五个图像的输出。

  18. 将解码器输出重塑为 28 x 28 的大小。

  19. 将原始图像重新调整回 28 x 28 的大小。

  20. 绘制原始图像、平均编码器输出和解码器。

在本次活动结束时,你将开发一个包含卷积层的自编码器神经网络。注意解码器表示的改进。输出将类似于以下内容:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_05_34.jpg

图 5.34:期望的原始图像、编码器输出和解码器
注意

本活动的解决方案可以在第 340 页找到。

总结

在本章中,我们首先介绍了人工神经网络,它们的结构以及它们如何学习完成特定任务。从一个有监督学习的例子开始,我们构建了一个人工神经网络分类器来识别 CIFAR-10 数据集中的物体。然后,我们深入了解了神经网络的自编码器架构,学习了如何利用这些网络为无监督学习问题准备数据集。最后,我们完成了这项调查,查看了卷积神经网络以及这些附加层能带来的好处。通过这章内容,我们为接下来进行降维的最终章节做好了准备,学习如何使用和可视化通过 t-分布近邻(t-SNE)编码的数据。t-SNE 提供了一种非常有效的方法来可视化高维数据,即使在应用 PCA 等降维技术后也是如此。t-SNE 是无监督学习中特别有用的方法。

第六章:t-分布随机邻域嵌入(t-SNE)

学习目标

到本章结束时,你将能够:

  • 描述并理解 t-SNE 背后的动机

  • 描述 SNE 和 t-SNE 的推导过程

  • 在 scikit-learn 中实现 t-SNE 模型

  • 解释 t-SNE 的局限性

在本章中,我们将讨论随机邻域嵌入(SNE)和 t-分布随机邻域嵌入(t-SNE)作为可视化高维数据集的一种手段。

介绍

本章是关于降维技术和变换的微型系列的最后一篇。我们在本系列的前几章中描述了多种不同的降维方法,用于清理数据、提高计算效率或提取数据集中最重要的信息。虽然我们已经展示了许多降维高维数据集的方法,但在许多情况下,我们无法将维度数量减少到可以可视化的规模,即二维或三维,而不严重降低数据的质量。考虑我们在第五章《自编码器》中使用的 MNIST 数据集,它是 0 到 9 的手写数字的数字化集合。每个图像的大小为 28 x 28 像素,提供 784 个独立的维度或特征。如果我们将这 784 个维度降到 2 或 3 个以进行可视化,我们几乎会失去所有可用的信息。

在本章中,我们将讨论随机邻域嵌入(SNE)和 t-分布随机邻域嵌入(t-SNE)作为可视化高维数据集的一种手段。这些技术在无监督学习和机器学习系统设计中非常有用,因为数据的可视化是一种强大的工具。能够可视化数据可以帮助探索关系、识别群体并验证结果。t-SNE 技术已被用于可视化癌细胞核,其中有超过 30 种感兴趣的特征,而来自文档的数据可能有成千上万个维度,有时即使在应用 PCA 等技术后也如此。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_01.jpg

图 6.1:MNIST 数据样本

在本章中,我们将使用 MNIST 数据集,并结合附带的源代码,通过实际示例来探索 SNE 和 t-SNE。在继续之前,我们将快速回顾 MNIST 及其包含的数据。完整的 MNIST 数据集包含 60,000 个训练样本和 10,000 个测试样本,所有样本为手写数字 0 到 9,表示为 28x28 像素大小的黑白(或灰度)图像(总计 784 个维度或特征),每种数字(或类别)的数量相等。由于其数据量大且质量高,MNIST 已成为机器学习中的经典数据集,通常作为许多研究论文中的参考数据集。与其他数据集相比,使用 MNIST 探索 SNE 和 t-SNE 的一个优势是,尽管样本具有较高的维度,但在降维后仍然可以可视化,因为它们可以表示为图像。图 6.1展示了一个 MNIST 数据集的样本,图 6.2展示了同一个样本,使用 PCA 降维到 30 个成分后的效果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_02.jpg

图 6.2:使用 PCA 将 MNIST 数据降维至 30 个成分

随机邻域嵌入(SNE)

随机邻域嵌入(SNE)是属于流形学习类别的众多方法之一,旨在将高维空间描述为低维流形或有界区域。一开始看,这似乎是一项不可能完成的任务;如果我们有一个至少包含 30 个特征的数据集,如何合理地在二维中表示数据呢?在我们推导 SNE 的过程中,期望你能看到这是如何实现的。别担心,我们不会深入探讨这一过程的数学细节,因为这些内容超出了本章的范围。构建 SNE 可以分为以下步骤:

  1. 将高维空间中数据点之间的距离转换为条件概率。假设我们有两个点,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_01.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_02.png,在高维空间中,我们想确定概率(https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_03.png),即https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_04.png将被选为https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_05.png。为了定义这个概率,我们使用高斯曲线,可以看到对于邻近的点,概率较高,而对于远离的点,概率非常低。

  2. 我们需要确定高斯曲线的宽度,因为它控制概率选择的速率。宽曲线表明很多点距离较远,而窄曲线则表明点紧密集中。

  3. 一旦我们将数据投影到低维空间,我们还可以确定相应的概率 (https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_06.png),这与对应的低维数据 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_07.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_08.png 之间的关系有关。

  4. SNE 的目标是将数据定位到低维空间,以通过使用名为 Kullback-Leibler (KL) 散度的代价函数 ©,最小化 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_09.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_Formula_06.png 之间的差异,覆盖所有数据点:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_03.jpg

图 6.3:Kullback-Leibler 散度。
注意

要构建高斯分布的 Python 代码,请参阅 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/blob/master/Lesson06/GaussianDist.ipynb 中的 GaussianDist.ipynb Jupyter 笔记本。

高斯分布将数据映射到低维空间。为此,SNE 使用梯度下降过程来最小化 C,使用我们在上一章中讲解过的标准学习率和迭代次数参数,回顾了神经网络和自编码器的内容。SNE 在训练过程中引入了一个额外的术语——困惑度。困惑度是用来选择有效邻居数量的参数,对于困惑度值在 5 到 50 之间时,效果相对稳定。实际上,建议使用该范围内的困惑度值进行反复试验。

SNE 提供了一种有效的方式将高维数据可视化到低维空间中,尽管它仍然面临一个被称为 拥挤问题 的问题。如果我们有一些点在某个点 i 周围大致等距离地分布,就可能出现拥挤问题。当这些点被可视化到低维空间时,它们会紧密聚集在一起,导致可视化困难。如果我们试图让这些拥挤的点之间保持更大的间距,问题会加剧,因为任何远离这些点的其他点都会被置于低维空间的非常远的位置。实质上,我们在尝试平衡能够可视化接近的点,同时又不丢失远离点提供的信息。

t-分布 SNE

t-SNE 通过修改 KL 散度代价函数,并用学生 t 分布代替低维空间中的高斯分布来解决拥挤问题。学生 t 分布是一种连续分布,通常在样本量较小且未知总体标准差时使用,广泛应用于学生 t 检验中。

修改后的 KL 代价函数将低维空间中的成对距离视为相等,而学生分布在低维空间中采用重尾分布来避免拥挤问题。在高维概率计算中,仍然使用高斯分布,确保高维中的适度距离在低维中也能得到相应的表示。不同分布在各自空间中的组合使得在小距离和中等距离分离的数据点能够得到真实的表示。

注意

有关如何在 Python 中重现学生 t 分布的示例代码,请参考 Jupyter notebook:github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/blob/master/Lesson06/StudentTDist.ipynb

幸运的是,我们不需要手动实现 t-SNE,因为 scikit-learn 提供了一个非常有效的实现,且 API 简单明了。我们需要记住的是,SNE 和 t-SNE 都是通过计算两个点在高维空间和低维空间中作为邻居的概率,旨在最小化这两个空间之间的概率差异。

练习 24:t-SNE MNIST

在本练习中,我们将使用 MNIST 数据集(随附源代码提供)来探索 scikit-learn 对 t-SNE 的实现。如前所述,使用 MNIST 使我们能够以其他数据集(如波士顿住房价格或鸢尾花数据集)无法实现的方式来可视化高维空间:

  1. 对于此练习,导入 picklenumpyPCATSNE 来自 scikit-learn,以及 matplotlib

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 加载并可视化随附源代码提供的 MNIST 数据集:

    注意
    with open('mnist.pkl', 'rb') as f:
        mnist = pickle.load(f)
    plt.figure(figsize=(10, 7))
    for i in range(9):
        plt.subplot(3, 3, i + 1)
        plt.imshow(mnist['images'][i], cmap='gray')
        plt.title(mnist['labels'][i])
        plt.axis('off')
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_04.jpg

    图 6.4:加载数据集后的输出

    这表明 MNIST 数据集已成功加载。

  3. 在本练习中,我们将对数据集使用 PCA 降维,只提取前 30 个成分。

    注意
    model_pca = PCA(n_components=30)
    mnist_pca = model_pca.fit(mnist['images'].reshape((-1, 28 ** 2)))
    
  4. 可视化将数据集降至 30 个成分的效果。为此,我们必须首先将数据集转换到低维空间,然后使用 inverse_transform 方法将数据返回到原始大小,以便进行绘图。当然,我们还需要在转换过程前后调整数据的形状:

    mnist_30comp = model_pca.transform(mnist['images'].reshape((-1, 28 ** 2)))
    mnist_30comp_vis = model_pca.inverse_transform(mnist_30comp)
    mnist_30comp_vis = mnist_30comp_vis.reshape((-1, 28, 28))
    plt.figure(figsize=(10, 7))
    for i in range(9):
        plt.subplot(3, 3, i + 1)
        plt.imshow(mnist_30comp_vis[i], cmap='gray')
        plt.title(mnist['labels'][i])
        plt.axis('off')
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_05.jpg

    图 6.5:可视化减少数据集的效果

    请注意,虽然我们在图像中丧失了一些清晰度,但由于降维过程,大部分数字仍然相当清晰可见。有趣的是,数字四(4)似乎是受此过程影响最大的。也许 PCA 过程丢弃的许多信息包含了特定于数字四(4)样本的信息。

  5. 现在,我们将 t-SNE 应用于 PCA 转换后的数据,以在二维空间中可视化 30 个组件。我们可以使用 scikit-learn 中的标准模型 API 接口来构建 t-SNE 模型。我们将首先使用默认值,这些值指定我们将 30 维数据嵌入到二维空间中进行可视化,使用 30 的困惑度、200 的学习率和 1000 次迭代。我们将指定 random_state 值为 0,并将 verbose 设置为 1:

    model_tsne = TSNE(random_state=0, verbose=1)
    model_tsne
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_06.jpg

    图 6.6:将 t-SNE 应用于 PCA 转换后的数据

    在之前的截图中,我们可以看到 t-SNE 模型的多个配置选项,其中一些比其他选项更为重要。我们将重点关注 learning_raten_componentsn_iterperplexityrandom_stateverbose 的值。对于 learning_rate,如前所述,t-SNE 使用随机梯度下降将高维数据投影到低维空间。学习率控制过程执行的速度。如果学习率过高,模型可能无法收敛到解决方案;如果学习率过低,可能需要很长时间才能收敛(如果能够收敛)。一个好的经验法则是从默认值开始;如果你发现模型产生了 NaN(非数值),则可能需要降低学习率。一旦你对模型满意,也可以降低学习率并让其运行更长时间(增加 n_iter);事实上,这样可能会得到略微更好的结果。n_components 是嵌入(或可视化空间)的维度数量。通常,你希望获得数据的二维图,因此只需使用默认值 2n_iter 是梯度下降的最大迭代次数。perplexity,如前一节所述,是在可视化数据时使用的邻居数量。

    通常,介于 5 到 50 之间的值是合适的,因为较大的数据集通常需要比较小的数据集更多的困惑度(perplexity)。random_state 是任何模型或算法中一个重要的变量,它在训练开始时会随机初始化其值。计算机硬件和软件工具提供的随机数生成器实际上并非真正的随机数生成器;它们实际上是伪随机数生成器。它们提供了一个良好的随机性近似值,但并不是真正的随机。计算机中的随机数是从一个称为种子的值开始的,之后通过复杂的方式生成。通过在过程开始时提供相同的种子,每次运行该过程时都会生成相同的“随机数”。虽然这听起来不直观,但对于复现机器学习实验来说,这非常有用,因为你不会看到仅仅由于参数初始化的不同而导致的性能差异。这可以提供更多的信心,认为性能变化是由于模型或训练的某些改变,例如神经网络的架构。

    注意

    产生真正的随机序列实际上是计算机最难实现的任务之一。计算机的软件和硬件设计是为了每次执行时按完全相同的方式运行指令,从而得到相同的结果。执行中的随机差异,虽然对于生成随机数序列来说理想,但在自动化任务和调试问题时会造成噩梦。

    verbose 是模型的详细程度,描述了在模型拟合过程中打印到屏幕上的信息量。值为 0 表示没有输出,而 1 或更大的值表示输出中详细信息的增加。

  6. 使用 t-SNE 转换 MNIST 的分解数据集:

    mnist_tsne = model_tsne.fit_transform(mnist_30comp)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_07.jpg

    图 6.7:转换分解数据集

    在拟合过程中提供的输出能帮助我们了解 scikit-learn 完成的计算。我们可以看到它正在为所有样本进行索引和计算邻居,然后再批量地计算数据作为邻居的条件概率,每次批次为 10。过程结束时,它提供了一个标准差(方差)均值为 304.9988,且在梯度下降的 250 和 1,000 次迭代后得到了 KL 散度。

  7. 现在,可视化返回数据集中的维度数量:

    mnist_tsne.shape
    

    输出如下:

    1000,2
    

    所以,我们成功地将 784 个维度降到了 2 维进行可视化,那么它看起来是什么样的呢?

  8. 创建模型生成的二维数据的散点图:

    plt.figure(figsize=(10, 7))
    plt.scatter(mnist_tsne[:,0], mnist_tsne[:,1], s=5)
    plt.title('Low Dimensional Representation of MNIST');
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_08.jpg

    图 6.8:MNIST 的二维表示(无标签)。

    图 6*.8* 中,我们可以看到我们已经将 MNIST 数据表示为二维,但我们也可以看到它似乎被分组在一起。这里有很多不同的数据聚类或团块,它们与其他聚类通过一些空白区域分开。似乎大约有九个不同的数据组。所有这些观察结果表明,个别聚类之间以及聚类内可能存在某种关系。

  9. 绘制按图像标签分组的二维数据,并使用标记区分各个标签。结合数据,在图上添加图像标签,以研究嵌入数据的结构:

    MARKER = ['o', 'v', '1', 'p' ,'*', '+', 'x', 'd', '4', '.']
    plt.figure(figsize=(10, 7))
    plt.title('Low Dimensional Representation of MNIST');
    for i in range(10):
        selections = mnist_tsne[mnist['labels'] == i]
        plt.scatter(selections[:,0], selections[:,1], alpha=0.2, marker=MARKER[i], s=5);
        x, y = selections.mean(axis=0)
        plt.text(x, y, str(i), fontdict={'weight': 'bold', 'size': 30})
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_09.jpg

    图 6.9:带标签的 MNIST 二维表示。

    6.9 非常有趣!我们可以看到,数据集中的不同图像类别(从零到九)对应着不同的聚类。在无监督的情况下,即没有提前提供标签,PCA 和 t-SNE 的结合成功地将 MNIST 数据集中的各个类别分开并进行分组。特别有趣的是,数据中似乎存在一些混淆,尤其是数字四和数字九的图像,以及数字五和数字三的图像;这两个聚类有些重叠。如果我们查看从 步骤 4练习 24t-SNE MNIST 提取的数字九和数字四的 PCA 图像,这就可以理解:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_10.jpg

    图 6.10:数字九的 PCA 图像。

    它们实际上看起来非常相似;也许是因为数字四的形状存在不确定性。看一下接下来的图像,我们可以看到左侧的数字四,两条垂直线几乎连接,而右侧的数字四则是两条平行线:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_11.jpg

    图 6.11:数字四的形状

    图 6.9 中需要注意的另一个有趣特征是边缘案例,这在 Jupyter 笔记本中通过颜色显示得更清楚。我们可以看到每个聚类的边缘附近,一些样本在传统的监督学习中会被误分类,但它们实际上可能与其他聚类更为相似。让我们看一个例子;有许多数字三的样本,它们离正确的聚类相当远。

  10. 获取数据集中所有数字三的索引:

    threes = np.where(mnist['labels'] == 3)[0]
    threes
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_12.jpg

    图 6.12:数据集中三的索引。
  11. 查找 x 值小于 0 的三类数据:

    tsne_threes = mnist_tsne[threes]
    far_threes = np.where(tsne_threes[:,0]< 0)[0]
    far_threes
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_13.jpg

    图 6.13:x 值小于零的三类
  12. 显示坐标以找到一个合理远离三的聚类的样本:

    tsne_threes[far_threes]
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_14.jpg

    图 6.14:远离三的聚类的坐标
  13. 选择一个具有较高负值的x坐标的样本。在这个示例中,我们将选择第四个样本,即样本 10。显示该样本的图像:

    plt.imshow(mnist['images'][10], cmap='gray')
    plt.axis('off');
    plt.show()
    

    输出结果如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_15.jpg

图 6.15:样本十的图像

看着这个示例图像及其对应的 t-SNE 坐标,大约是(-8, 47),不难理解这个样本为何会靠近八和五的聚类,因为在这张图像中,八和五这两个数字有很多相似的特征。在这个示例中,我们应用了简化版的 SNE,展示了它的一些高效性以及可能的混淆源和无监督学习的输出结果。

注意

即使提供了随机数种子,t-SNE 也不能保证每次执行时输出完全相同,因为它基于选择概率。因此,您可能会注意到,内容中提供的示例与您的实现之间在细节上有所不同。尽管具体细节可能有所不同,但整体原则和技术依然适用。从实际应用的角度来看,建议多次重复该过程,以从数据中辨别出重要信息。

活动 12:葡萄酒 t-SNE

在本活动中,我们将通过使用葡萄酒数据集来巩固我们对 t-SNE 的理解。完成此活动后,您将能够为自己的自定义应用程序构建 SNE 模型。葡萄酒数据集 (archive.ics.uci.edu/ml/datasets/Wine) 是关于来自意大利三家不同生产商的葡萄酒化学分析的属性集合,但每个生产商的葡萄酒类型相同。此信息可作为示例,用于验证瓶装葡萄酒是否来自意大利特定地区的葡萄。13 个属性包括:酒精、苹果酸、灰分、灰的碱度、镁、总酚、类黄酮、非类黄酮酚、前花青素、颜色强度、色调、稀释酒的 OD280/OD315 比值,以及脯氨酸。

每个样本包含一个类别标识符(1 – 3)。

注意

本数据集来源于 archive.ics.uci.edu/ml/machine-learning-databases/wine/,可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson06/Activity12 下载。

UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。

以下步骤将帮助您完成活动:

  1. 导入pandasnumpymatplotlib以及 scikit-learn 中的t-SNEPCA模型。

  2. 使用随附源代码中的wine.data文件加载 Wine 数据集,并显示前五行数据。

    注意

    您可以通过使用del关键字删除 Pandas DataFrame 中的列。只需将del和所选列放在方括号内。

  3. 第一列包含标签;提取该列并将其从数据集中移除。

  4. 执行 PCA,将数据集降至前六个主成分。

  5. 确定这六个成分所描述的数据中的方差量。

  6. 使用指定的随机状态和verbose值为 1 创建 t-SNE 模型。

  7. 将 PCA 数据拟合到 t-SNE 模型中。

  8. 确认 t-SNE 拟合数据的形状是二维的。

  9. 创建二维数据的散点图。

  10. 创建一个二位数据的散点图,并应用类标签,以可视化可能存在的聚类。

在本活动结束时,您将构建一个基于 Wine 数据集六个成分的 t-SNE 可视化,并识别图中数据位置的一些关系。最终的图将类似于以下内容:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_16.jpg

图 6.16:预期的图示
注意

本活动的解决方案可以在第 345 页找到。

在本节中,我们介绍了生成 SNE 图示的基础知识。在低维空间中表示高维数据的能力至关重要,特别是对于深入理解手头数据至关重要。有时,这些图示的解释可能会有些棘手,因为确切的关系有时会相互矛盾,导致误导性结构。

解释 t-SNE 图示

现在我们可以使用 t-分布 SNE 来可视化高维数据,重要的是理解此类图示的局限性,以及在解释和生成这些图示时需要关注的方面。在本章的这一部分,我们将突出 t-SNE 的一些重要特性,并演示在使用该可视化技术时应注意的事项。

困惑度

如 t-SNE 介绍中所述,困惑度值指定用于计算条件概率的最近邻数量。选择该值对最终结果有显著影响;当困惑度值较低时,数据中的局部变化占主导,因为计算中使用的样本数量较少。相反,较大的困惑度值会考虑更多的全局变化,因为使用了更多的样本进行计算。通常,尝试不同的值以调查困惑度的效果是值得的。通常,困惑度值在 5 到 50 之间的效果较好。

练习 25:t-SNE MNIST 与困惑度

在这个练习中,我们将尝试不同的困惑度值,并查看它在可视化图中的效果:

  1. 导入picklenumpymatplotlib,以及来自 scikit-learn 的PCAt-SNE

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 加载 MNIST 数据集:

    注意
    with open('mnist.pkl', 'rb') as f:
        mnist = pickle.load(f)
    
  3. 使用 PCA,从图像数据中选择前 30 个方差成分:

    model_pca = PCA(n_components=30)
    mnist_pca = model_pca.fit_transform(mnist['images'].reshape((-1, 28 ** 2)))
    
  4. 在这个练习中,我们正在研究困惑度对 t-SNE 流形的影响。通过一个困惑度为 3、30 和 300 的模型/绘图循环进行迭代:

    MARKER = ['o', 'v', '1', 'p' ,'*', '+', 'x', 'd', '4', '.']
    for perp in [3, 30, 300]:
        model_tsne = TSNE(random_state=0, verbose=1, perplexity=perp)
        mnist_tsne = model_tsne.fit_transform(mnist_pca)
        plt.figure(figsize=(10, 7))
        plt.title(f'Low Dimensional Representation of MNIST (perplexity = {perp})');
        for i in range(10):
            selections = mnist_tsne[mnist['labels'] == i]
            plt.scatter(selections[:,0], selections[:,1], alpha=0.2, marker=MARKER[i], s=5);
            x, y = selections.mean(axis=0)
            plt.text(x, y, str(i), fontdict={'weight': 'bold', 'size': 30})    
    

    输出结果如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_17.jpg

图 6.17:通过模型进行迭代

请注意三个不同困惑度值下的 KL 散度,以及平均标准差(方差)的增加。通过查看以下三个带有类标签的 t-SNE 图,我们可以看到,当困惑度值较低时,聚类被很好地分隔,重叠较少。然而,聚类之间几乎没有空间。当我们增加困惑度时,聚类之间的空间改善,在困惑度为 30 时有相对清晰的区分。随着困惑度增加到 300,我们可以看到 8 和 5 的聚类,以及 9、4 和 7 的聚类,开始趋于融合。

从低困惑度值开始:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_18.jpg

图 6.18:低困惑度值的绘图

将困惑度增加 10 倍后,聚类变得更加清晰:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_19.jpg

图 6.19:困惑度增加 10 倍后的绘图

将困惑度增加到 300 后,我们开始将更多的标签合并在一起:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_20.jpg

图 6.20:将困惑度值增加到 300

在这个练习中,我们加深了对困惑度影响及其对整体结果敏感性的理解。较小的困惑度值可能导致位置之间的混合更加均匀,且它们之间的空间非常小。增加困惑度可以更有效地分离聚类,但过大的值会导致聚类重叠。

活动 13:t-SNE 葡萄酒和困惑度

在这个活动中,我们将使用葡萄酒数据集进一步强化困惑度对 t-SNE 可视化过程的影响。在这个活动中,我们尝试确定是否可以根据葡萄酒的化学成分识别其来源。t-SNE 过程提供了一种有效的表示方法,可能帮助识别来源。

注意

该数据集来自于archive.ics.uci.edu/ml/machine-learning-databases/wine/,可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson06/Activity13 下载。

UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。

  1. 导入pandasnumpymatplotlib以及来自 scikit-learn 的t-SNEPCA模型。

  2. 加载 Wine 数据集并检查前五行数据。

  3. 第一列提供了标签;从 DataFrame 中提取这些标签并存储在单独的变量中。确保该列从 DataFrame 中移除。

  4. 对数据集执行 PCA 操作,并提取前六个主成分。

  5. 构建一个循环,遍历不同的困惑度值(1、5、20、30、80、160、320)。对于每次循环,生成一个带有相应困惑度的 t-SNE 模型,并打印带标签的葡萄酒类别的散点图。注意不同困惑度值的效果。

在本活动结束时,你将生成 Wine 数据集的二维表示,并检查生成的图表,寻找数据的聚类或分组。

注意

该活动的解决方案可以在第 348 页找到。

迭代次数

我们将要实验研究的最后一个参数是迭代次数,正如我们在自动编码器中的研究所示,这只是应用于梯度下降的训练轮次数量。幸运的是,迭代次数是一个相对简单的参数,通常只需要一定的耐心,因为低维空间中点的位置会在最终位置上稳定下来。

练习 26:t-SNE MNIST 和迭代次数

在这个练习中,我们将观察一系列不同的迭代参数对 t-SNE 模型的影响,并突出一些可能需要更多训练的指示符。再次强调,这些参数的值在很大程度上依赖于数据集以及可用于训练的数据量。在本示例中,我们仍然使用 MNIST 数据集:

  1. 导入picklenumpymatplotlib以及来自 scikit-learn 的 PCA 和 t-SNE:

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 加载 MNIST 数据集:

    注意
    with open('mnist.pkl', 'rb') as f:
        mnist = pickle.load(f)
    
  3. 使用 PCA,从图像数据中仅选择前 30 个方差成分:

    model_pca = PCA(n_components=30)
    mnist_pca = model_pca.fit_transform(mnist['images'].reshape((-1, 28 ** 2)))
    
  4. 在这个练习中,我们将研究迭代次数对 t-SNE 流形的影响。通过模型/绘图循环,进行迭代,迭代次数分别为2505001000

    MARKER = ['o', 'v', '1', 'p' ,'*', '+', 'x', 'd', '4', '.']
    for iterations in [250, 500, 1000]:
        model_tsne = TSNE(random_state=0, verbose=1, n_iter=iterations, n_iter_without_progress=iterations)
        mnist_tsne = model_tsne.fit_transform(mnist_pca)
    
  5. 绘制结果:

        plt.figure(figsize=(10, 7))
        plt.title(f'Low Dimensional Representation of MNIST (iterations = {iterations})');
        for i in range(10):
            selections = mnist_tsne[mnist['labels'] == i]
            plt.scatter(selections[:,0], selections[:,1], alpha=0.2, marker=MARKER[i], s=5);
            x, y = selections.mean(axis=0)
            plt.text(x, y, str(i), fontdict={'weight': 'bold', 'size': 30})    
    

    迭代次数较少会限制算法找到相关邻居的程度,导致聚类不清晰:

![图 6.21:250 次迭代后的绘图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_21.jpg)

图 6.21:250 次迭代后的绘图

增加迭代次数为算法提供了足够的时间来充分投影数据:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_22.jpg

图 6.22:将迭代次数增加到 500 后的绘图

一旦簇群稳定,增加迭代次数的影响非常小,基本上只是增加了训练时间:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_06_23.jpg

图 6.23:1000 次迭代后的绘图

从前面的绘图来看,我们可以看到,迭代次数为 500 和 1000 时,簇群的位置稳定且在各个图之间几乎没有变化。最有趣的图是迭代次数为 250 的图,其中簇群似乎仍在移动过程中,正在向最终位置靠拢。因此,有充分的证据表明,500 次迭代足以。

活动 14:t-SNE 葡萄酒与迭代次数

在本活动中,我们将研究迭代次数对葡萄酒数据集可视化的影响。这是数据处理、清洗和理解数据关系的探索阶段中常用的一个过程。根据数据集和分析类型,我们可能需要尝试多种不同的迭代次数,就像本次活动中所做的那样。

注意

此数据集来自 archive.ics.uci.edu/ml/machine-learning-databases/wine/。它可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson06/Activity14 下载。

UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。

以下步骤将帮助你完成活动:

  1. 导入pandasnumpymatplotlib,以及从 scikit-learn 导入t-SNEPCA模型。

  2. 加载葡萄酒数据集并检查前五行数据。

  3. 第一列提供了标签;从 DataFrame 中提取这些标签并存储到一个单独的变量中。确保将该列从 DataFrame 中删除。

  4. 对数据集执行 PCA,并提取前六个主成分。

  5. 构建一个循环,遍历迭代值(2505001000)。对于每个循环,生成一个具有相应迭代次数的 t-SNE 模型,并生成一个没有进度值的相同迭代次数的模型。

  6. 构建标记葡萄酒类别的散点图。注意不同迭代值的影响。

通过完成本活动,我们将研究修改模型迭代参数的效果。这是确保数据在低维空间中稳定在某个最终位置的重要参数。

注意

本活动的解决方案可以在第 353 页找到。

关于可视化的最终思考

在我们总结关于 t-分布随机邻域嵌入(t-SNE)这一章节时,有几个关于可视化的重要方面需要注意。首先,聚类的大小或聚类之间的相对空间可能并不能真正反映接近度。正如我们在本章前面讨论的,结合高斯分布和学生 t 分布被用来在低维空间中表示高维数据。因此,距离之间的线性关系并不能得到保证,因为 t-SNE 平衡了局部和全局数据结构的位置。局部结构中点之间的实际距离在可视化表示中可能看起来非常接近,但在高维空间中可能仍然存在一定的距离。

这个特性还有一个附带的后果,那就是有时随机数据看起来像是具有某种结构,并且通常需要生成多个可视化图像,使用不同的困惑度、学习率、迭代次数和随机种子值。

总结

在这一章中,我们介绍了 t-分布随机邻域嵌入(t-SNE)作为可视化高维信息的一种方法,这些信息可能来自先前的过程,如 PCA 或自编码器。我们讨论了 t-SNE 如何生成这种表示,并使用 MNIST 和 Wine 数据集以及 scikit-learn 生成了多个表示。在这一章中,我们能够看到无监督学习的一些强大之处,因为 PCA 和 t-SNE 能够在不知道真实标签的情况下对每张图片的类别进行聚类。在下一章中,我们将基于这次实践经验,探讨无监督学习的应用,包括篮子分析和主题建模。

第七章:主题建模

学习目标

本章结束时,您将能够:

  • 为文本数据执行基本的清理技术

  • 评估隐含狄利克雷分配模型

  • 执行非负矩阵分解模型

  • 解释主题模型的结果

  • 为给定场景识别最佳主题模型

在本章中,我们将看到如何通过主题建模深入了解文档的潜在结构。

引言

主题建模是自然语言处理NLP)的一个方面,这是计算机科学领域探索计算机与人类语言关系的一个领域,随着文本数据集的增加可用性的增加而日益流行。NLP 可以处理几乎任何形式的语言,包括文本、语音和图像。除了主题建模外,情感分析、对象字符识别和词汇语义是值得注意的 NLP 算法。如今,收集和需要分析的数据很少是以标准表格形式出现,而更频繁地以较少结构化的形式出现,包括文档、图像和音频文件。因此,成功的数据科学从业者需要精通处理这些多样化数据集的方法论。

下面是一个示例,演示如何识别文本中的单词并将其分配给主题:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_01.jpg

图 7.1:示例,识别文本中的单词并将它们分配给主题

你可能立即问的问题是什么是主题?让我们通过一个例子来回答这个问题。你可以想象,或者可能已经注意到,在发生重大事件的日子,例如国家选举、自然灾害或体育冠军赛,社交媒体网站上的帖子往往会集中在这些事件上。帖子通常以某种方式反映当天的事件,并且会以不同的方式这样做。帖子可以并且将会有许多不同的观点。如果我们有关于世界杯决赛的推文,这些推文的主题可能涵盖各种不同的观点,从裁判的裁判质量到球迷行为。在美国,总统在每年的一月中旬到晚些时候发表一次称为国情咨文的年度演讲。通过足够数量的社交媒体帖子,我们将能够通过使用其中包含的特定关键词对帖子进行分组来推断或预测社交媒体社区对演讲的高级反应 - 主题。

主题模型

主题模型属于无监督学习类别,因为几乎总是无法事先知道要识别的主题。因此,没有目标可以进行回归或分类建模。在无监督学习中,主题模型最像聚类算法,特别是 k-means 聚类。你会记得,在 k-means 聚类中,首先确定聚类的数量,然后模型将每个数据点分配到预定的某个聚类中。主题模型通常也是如此。我们在开始时选择主题的数量,然后模型会提取出构成这些主题的词汇。这是一个很好的出发点,用于对主题建模进行高层次的概述。

在此之前,我们先检查一下是否安装了正确的环境和库,并准备好使用。下表列出了所需的库及其主要用途:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_02.jpg

图 7.2:显示不同库及其用途的表格

练习 27:设置环境

为了检查环境是否准备好进行主题建模,我们将执行几个步骤。其中第一步是加载本章将需要的所有库:

注意

如果这些库中有任何一个或全部未安装,请通过命令行使用pip安装所需的软件包。例如,若未安装,运行pip install langdetect

  1. 打开一个新的 Jupyter 笔记本。

  2. 导入所需的库:

    import langdetect
    import matplotlib.pyplot
    import nltk
    import numpy
    import pandas
    import pyLDAvis
    import pyLDAvis.sklearn
    import regex
    import sklearn
    

    请注意,并非所有这些库都用于清理数据;其中一些库用于实际建模,但一次性导入所有所需的库是很方便的,所以我们现在就将所有库的导入处理好。

  3. 尚未安装的库将返回以下错误:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_03.jpg

    图 7.3:库未安装错误

    如果返回此错误,请按照之前讨论的方法通过命令行安装相关库。成功安装后,使用import重新运行库导入过程。

  4. 某些文本数据清理和预处理过程需要词典(更多内容稍后介绍)。在此步骤中,我们将安装其中的两个词典。如果导入了nltk库,可以执行以下代码:

    nltk.download('wordnet')
    nltk.download('stopwords')
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_04.jpg

    图 7.4:导入库并下载词典
  5. 运行matplotlib魔法命令并指定 inline,以便图表显示在笔记本中:

    %matplotlib inline
    

笔记本和环境已设置好,准备好加载数据。

主题模型的高层次概述

在分析大量可能相关的文本数据时,主题模型是一个常用方法。所谓相关,是指文档理想情况下来自相同来源。也就是说,调查结果、推文和新闻文章通常不会在同一个模型中同时进行分析。当然,也可以将它们一起分析,但结果可能非常模糊,因此没有意义。运行任何主题模型的唯一数据要求是文档本身。不需要额外的数据,无论是元数据还是其他类型的数据。

简单来说,主题模型通过分析文档中的词汇,识别一组文档(称为语料库)中的抽象主题(也称为主题)。也就是说,如果一句话中包含“薪水”、“员工”和“会议”等词汇,就可以推测这句话的主题是与工作相关的。需要注意的是,构成语料库的文档不一定是传统意义上的文档——可以是信件或合同等。文档可以是任何包含文本的内容,包括推文、新闻标题或转录的演讲。

主题模型假设同一文档中的词汇是相关的,并利用这一假设通过寻找反复出现在相近位置的词汇组来定义抽象主题。通过这种方式,这些模型属于经典的模式识别算法,其中被检测到的模式是由词汇组成的。一般的主题建模算法包含四个主要步骤:

  1. 确定主题的数量。

  2. 扫描文档并识别共现的词汇或短语。

  3. 自动学习表征文档的词汇组(或集群)。

  4. 输出表征语料库的抽象主题,作为词汇组合。

正如步骤 1 所示,在拟合模型之前需要选择主题的数量。选择合适的主题数量可能会有些棘手,但与大多数机器学习模型一样,这个参数可以通过拟合多个使用不同主题数量的模型,并基于某些性能指标选择最佳模型来优化。稍后我们会再深入讨论这个过程。

以下是通用主题建模工作流程:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_05.jpg

图 7.5: 通用主题建模工作流程

选择最佳的主题数非常重要,因为这一参数会对主题的连贯性产生重大影响。这是因为模型会在预定义的主题数限制下,找到最适合语料库的词组。如果主题数过高,主题会变得过于狭窄。过于具体的主题被称为过度加工。同样地,如果主题数过低,主题会变得过于通用和模糊。这些类型的主题被认为是加工不足。过度加工和加工不足的主题有时可以通过分别减少或增加主题数来修正。话题模型的一个常见且不可避免的结果是,通常至少有一个话题会出现问题。

话题模型的一个关键方面是,它们不会生成特定的单词或短语主题,而是生成一组词汇,每个词汇代表一个抽象的主题。回想一下之前关于工作的假设句子。构建出来的主题模型识别该句子所属的假设语料库的主题时,不会返回单词“work”作为主题。它会返回一组词汇,例如“paycheck”(薪水)、“employee”(员工)和“boss”(老板);这些词汇描述了主题,并可以推断出一个单词或短语主题。这是因为话题模型理解的是词汇的接近度,而不是上下文。模型并不理解“paycheck”、“employee”和“boss”的含义;它只知道这些词汇通常在一起出现并且彼此接近:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_06.jpg

图 7.6:从词组中推断主题

话题模型可以用来预测未见文档的主题,但如果你打算做预测,重要的是要认识到,话题模型只知道用来训练它们的词汇。也就是说,如果未见文档中包含训练数据中没有的词汇,模型将无法处理这些词汇,即使它们与训练数据中已识别的某个主题相关。因此,话题模型更多地用于探索性分析和推断,而不是预测。

每个话题模型输出两个矩阵。第一个矩阵包含词汇与主题的关系。它列出了每个词汇与每个主题的关联,并对这种关系进行量化。考虑到模型正在考虑的词汇数量,每个主题只会由相对较少的词汇描述。词汇可以被分配给一个主题,或者分配给多个主题,并在不同的量化下表示。词汇是被分配给一个主题还是多个主题,取决于算法。同样,第二个矩阵包含文档与主题的关系。它列出了与某个主题相关的每个文档,并对每个文档与该主题的关联程度进行量化。

在讨论主题建模时,重要的是要不断强调,代表主题的词汇组之间并不是概念上的关联,而仅仅是通过词汇的接近度来关联的。文档中某些单词的频繁接近足以定义主题,这是因为前面提到的假设——同一文档中的所有单词都是相关的。然而,这个假设可能并不成立,或者这些单词可能过于泛化,无法形成连贯的主题。解释抽象的主题涉及将文本数据的固有特性与生成的词汇组进行平衡。文本数据和语言一般来说是高度可变的、复杂的,并且具有上下文,这意味着任何普遍的结果都需要谨慎对待。这并不是贬低或否定模型的结果。在彻底清理过的文档和适当数量的主题下,词汇组,如我们将看到的,能够很好地指导语料库的内容,并且可以有效地融入到更大的数据系统中。

我们已经讨论了一些主题模型的局限性,但还有一些额外的注意事项。文本数据的噪声特性可能会导致主题模型将与某个主题无关的单词错误地分配给该主题。再考虑之前提到的关于工作的句子。单词 meeting 可能出现在代表“工作”主题的词汇组中。也有可能单词 long 也会出现在该组中,但单词 long 与工作没有直接关系。long 之所以可能出现在该组,是因为它常常出现在 meeting 旁边。因此,long 可能会被认为与工作虚假相关,并且如果可能的话,应该从该主题词汇组中移除。词汇组中的虚假相关词可能会在分析数据时引起严重问题。

这不一定是模型的缺陷,而是模型的一个特性,在面对噪声数据时,可能会从数据中提取出一些特征,这些特征可能会对结果产生负面影响。虚假的相关性可能是由于数据的收集方式、地点或时间造成的。如果文档仅在某些特定的地理区域内收集,那么与该区域相关的词汇可能会不正确地、尽管是偶然地,链接到模型输出的一个或多个词汇组。请注意,随着词汇组中单词数量的增加,我们可能会将更多的文档归类到该主题中,超出应归类的范围。显然,如果我们减少属于某一主题的词汇数量,那么该主题将会被分配给更少的文档。记住,这并不是什么坏事。我们希望每个词汇组只包含有意义的单词,从而将合适的主题分配给合适的文档。

有许多主题建模算法,但也许最为人熟知的两种是潜在狄利克雷分配(Latent Dirichlet Allocation)非负矩阵分解(Non-Negative Matrix Factorization)。我们稍后将详细讨论这两种算法。

商业应用

尽管存在局限性,但如果在正确的上下文中正确使用,主题建模可以提供可操作的洞察,推动商业价值。现在,让我们回顾一下主题模型的一个重要应用。

其中一个使用案例是对新文本数据进行探索性数据分析,其中数据集的底层结构未知。这相当于为一个未见过的数据集绘制图表并计算总结统计量,该数据集包含需要理解其特征的数值和类别变量,才能在进行更复杂的分析之前做出合理的判断。通过主题建模的结果,可以确定此数据集在未来建模工作中的可用性。例如,如果主题模型返回了清晰且 distinct 的主题,那么该数据集将是进一步聚类分析的理想候选对象。

实际上,确定主题的作用是创建一个额外的变量,用于排序、分类和/或分块数据。如果我们的主题模型返回了汽车、农业和电子产品作为抽象主题,我们可以将大量的文本数据集过滤到只有农业作为主题的文档。一旦过滤完毕,我们可以进行进一步的分析,包括情感分析、另一次主题建模,或者任何我们能想到的分析。除了定义语料库中存在的主题外,主题建模还会间接返回许多其他信息,这些信息也可以用于分解大型数据集并理解其特征。

这里显示了文档排序的表示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_07.jpg

图 7.7:文档排序/分类

其中一个特征是主题的普遍性。想象一下对一个开放式问卷调查进行分析,这个问卷调查旨在评估对某个产品的反馈。我们可以设想主题模型返回的主题形式为情感。一组词可能是“好”,“优秀”,“推荐”和“质量”,而另一组则可能是“垃圾”,“损坏”,“差”和“失望”。鉴于这种类型的调查,主题本身可能不会太令人惊讶,但有趣的是,我们可以统计每个主题包含的文档数量。从统计结果中,我们可以得出这样的结论:例如,有 x 百分比的调查参与者对该产品有正面反应,而只有 y 百分比的参与者有负面反应。实质上,我们所创建的是情感分析的粗略版本。

当前,主题模型最常见的用途是作为推荐引擎的一个组件。如今的重点是个性化——为客户提供专门设计和策划的产品。以网站为例,无论是新闻网站还是其他类型的网站,它们的目的是传播文章。像雅虎和 Medium 这样的公司需要客户持续阅读才能维持生意,而让客户继续阅读的一种方法是向他们推送他们更倾向于阅读的文章。这就是主题建模的用武之地。通过使用一个由个体之前阅读的文章组成的语料库,主题模型基本上会告诉我们该订阅者喜欢阅读什么类型的文章。公司然后可以查找其库存中的相关文章,并通过用户的账户页面或电子邮件将其发送给该个体。这就是定制策划,以简化使用并保持用户的持续参与。

在我们开始为模型准备数据之前,让我们快速加载并浏览一下数据。

练习 28:数据加载

在这个练习中,我们将从一个数据集加载数据并进行格式化。本章中的所有练习的数据集均来自加利福尼亚大学尔湾分校(UCI)托管的机器学习库。要找到数据,请访问github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Exercise27-Exercise%2038

注意

该数据下载自archive.ics.uci.edu/ml/datasets/News+Popularity+in+Multiple+Social+Media+Platforms。可以通过github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Exercise27-Exercise%2038访问。Nuno Moniz 和 Luís Torgo(2018),《在线新闻源的多源社交反馈》,CoRR UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚尔湾:加利福尼亚大学信息与计算机科学学院。

这是进行此练习所需的唯一文件。下载并保存在本地后,数据可以加载到笔记本中:

注意

在同一个笔记本中执行本章的练习。

  1. 定义数据的路径并使用pandas加载它:

    path = "News_Final.csv"
    df = pandas.read_csv(path, header=0)
    
    注意

    将文件添加到与您打开的笔记本相同的路径中。

  2. 通过执行以下代码简要检查数据:

    def dataframe_quick_look(df, nrows):
        print("SHAPE:\n{shape}\n".format(shape=df.shape))
        print("COLUMN NAMES:\n{names}\n".format(names=df.columns))
        print("HEAD:\n{head}\n".format(head=df.head(nrows)))
    dataframe_quick_look(df, nrows=2)
    

    这个用户定义的函数返回数据的形状(行数和列数)、列名和数据的前两行。

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_08.jpg

    图 7.8:原始数据

    这是一个在特征上比运行主题模型所需的数据集要大得多的数据集。

  3. 请注意,其中一列名为Topic,实际上包含了主题模型试图确定的信息。让我们简要查看提供的主题数据,以便在最终生成我们自己的主题时,能够进行比较。运行以下代码行,打印唯一的主题值及其出现次数:

    print("TOPICS:\n{topics}\n".format(topics=df["Topic"].value_counts()))
    

    输出如下:

    TOPICS:
    economy      33928
    obama        28610
    microsoft    21858
    palestine     8843
    Name: Topic, dtype: int64
    
  4. 现在,我们提取标题数据并将提取的数据转换为一个列表对象。打印列表中的前五个元素以及列表长度,以确认提取是否成功:

    raw = df["Headline"].tolist()
    print("HEADLINES:\n{lines}\n".format(lines=raw[:5]))
    print("LENGTH:\n{length}\n".format(length=len(raw)))
    

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_09.jpg

图 7.9:标题列表

现在数据已加载并正确格式化,让我们讨论文本数据清洗,然后进行一些实际的清洗和预处理。为了教学目的,清洗过程最初将在一个标题上构建和执行。建立并测试了该过程之后,我们将返回并在每个标题上运行该过程。

清洗文本数据

所有成功建模工作的一个关键组成部分是清洗过的数据集,该数据集已针对特定数据类型和分析方法进行了适当且充分的预处理。文本数据也不例外,因为它以原始形式几乎无法使用。无论运行什么算法:如果数据未经过适当准备,结果充其量是毫无意义,最坏的情况则可能是误导性的。正如俗话所说,“垃圾进,垃圾出。” 对于主题建模来说,数据清洗的目标是通过移除可能妨碍分析的内容,来提取每个文档中可能相关的词汇。

数据清洗和预处理几乎总是特定于数据集的,这意味着每个数据集都需要选择一套独特的清洗和预处理步骤,专门处理该数据集中的问题。对于文本数据,清洗和预处理步骤可能包括语言过滤、移除网址和用户名、词形还原、去除停用词等。在接下来的练习中,我们将清洗一个包含新闻标题的数据集,以进行主题建模。

数据清洗技术

重申一下之前的观点,清洗文本数据以进行主题建模的目标是提取每个文档中可能与发现语料库的抽象主题相关的词汇。这意味着需要去除常见词、短词(通常较为常见)、数字和标点符号。没有固定的清洗流程,因此理解数据类型中的典型问题点并进行广泛的探索性工作是非常重要的。

接下来,我们将讨论一些我们将要使用的文本数据清理技术。进行任何涉及文本的建模任务时,首先需要做的一件事是确定文本的语言。在这个数据集中,大多数标题是英语,因此为了简化处理,我们将删除非英语的标题。构建基于非英语文本数据的模型需要额外的技能,最基本的是流利掌握所处理的语言。

数据清理的下一个关键步骤是删除文档中所有与基于单词的模型无关的元素,或那些可能成为噪声源,进而影响结果的元素。需要删除的元素可能包括网站地址、标点符号、数字和停用词。停用词基本上是一些简单的日常用词,包括 we、are 和 the。需要注意的是,没有一个权威的停用词词典;相反,每个词典都会有所不同。尽管存在差异,每个词典都表示一些常见的单词,这些单词被认为与话题无关。主题模型试图识别那些既频繁又不太频繁的单词,以便能够描述一个抽象的主题。

删除网站地址有类似的动机。特定的网站地址出现的频率非常低,但即使一个特定的网站地址出现足够多,足以与某个话题相关联,网站地址的解释方式也不同于单词。从文档中移除不相关的信息,可以减少可能阻碍模型收敛或模糊结果的噪声。

词形还原,与语言检测一样,是所有涉及文本的建模活动中一个重要的组成部分。它是将单词还原为其基本形式的过程,以便将应该相同但实际不同的单词归为一类。考虑单词 running、runs 和 ran,它们的基本形式都是 run。词形还原的一个好处是,它在决定如何处理每个单词之前,会考虑整个句子的所有单词,换句话说,它会考虑上下文。像大多数前述的清理技术一样,词形还原只是减少数据中的噪声,从而使我们能够识别干净且可解释的主题。

现在,掌握了基本的文本清理技术后,让我们将其应用于实际数据。

练习 29:逐步清理数据

在这个练习中,我们将学习如何实现一些清理文本数据的关键技术。每个技术将在我们进行练习时进行解释。在每一步清理操作后,使用print输出示例标题,便于我们观察从原始数据到适合建模的数据的演变:

  1. 选择第五个标题作为我们构建和测试清理过程的示例。第五个标题不是随意选择的,它被选中是因为它包含了在清理过程中将要解决的特定问题:

    example = raw[5]
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_10.jpg

    图 7.10:第五个标题
  2. 使用langdetect库来检测每个标题的语言。如果语言不是英语(“en”),则从数据集中删除该标题:

    def do_language_identifying(txt):
        try: the_language = langdetect.detect(txt)
        except: the_language = 'none'
        return the_language
    print("DETECTED LANGUAGE:\n{lang}\n".format(lang=do_language_identifying(example)
    ))
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_11.jpg

    图 7.11:检测到的语言
  3. 将包含标题的字符串使用空格分割成片段,称为tokens。返回的对象是构成标题的单词和数字的列表。将标题字符串拆分成 tokens 会使清理和预处理过程更简单:

    example = example.split(" ")
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_12.jpg

    图 7.12:使用空格分割字符串
  4. 使用正则表达式查找包含http://https://的 tokens 来识别所有 URL。将 URL 替换为'URL'字符串:

    example = ['URL' if bool(regex.search("http[s]?://", i))else i for i in example]
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_13.jpg

    图 7.13:将 URLs 替换为 URL 字符串
  5. 使用正则表达式将所有标点符号和换行符(\n)替换为空字符串:

    example = [regex.sub("[^\\w\\s]|\n", "", i) for i in example]
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_14.jpg

    图 7.14:将标点符号替换为换行符
  6. 使用正则表达式将所有数字替换为空字符串:

    example = [regex.sub("^[0-9]*$", "", i) for i in example]
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_15.jpg

    图 7.15:将数字替换为空字符串
  7. 将所有大写字母转换为小写字母。将所有内容转换为小写并不是强制性步骤,但它有助于减少复杂性。将所有内容转换为小写后,跟踪的内容更少,因此出错的机会也更小:

    example = [i.lower() if i not in "URL" else i for i in example]
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_16.jpg

    图 7.16:将大写字母转换为小写字母
  8. 删除步骤 4中作为占位符添加的"URL"字符串。之前添加的"URL"字符串实际上对于建模来说并不需要。如果认为它留在里面无害,考虑到"URL"字符串可能自然出现在标题中,而我们不希望人为地增加它出现的频率。此外,"URL"字符串并非每个标题中都会出现,因此如果将其留在里面,可能会无意中创建"URL"字符串与某一主题之间的关联:

    example = [i for i in example if i not in "URL"]
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_17.jpg

    图 7.17:移除字符串 URL
  9. nltk加载stopwords字典并打印出来:

    list_stop_words = nltk.corpus.stopwords.words("English")
    list_stop_words = [regex.sub("[^\\w\\s]", "", i) for i in list_stop_words]
    print(list_stop_words)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_18.jpg

    图 7.18:停用词列表

    在使用字典之前,重要的是要重新格式化单词,以符合我们标题的格式。这包括确认所有内容都是小写且没有标点符号。

  10. 现在我们已经正确格式化了stopwords字典,使用它来删除标题中的所有停用词:

    example = [i for i in example if i not in list_stop_words]
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_19.jpg

    图 7.19:从标题中移除停用词
  11. 通过定义一个可以单独应用于每个标题的函数来执行词形还原。词形还原需要加载 WordNet 字典:

    def do_lemmatizing(wrd):
        out = nltk.corpus.wordnet.morphy(wrd)
        return (wrd if out is None else out)
    example = [do_lemmatizing(i) for i in example]
    print(example)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_20.jpg

    图 7.20:执行词形还原后的输出
  12. 从标记列表中移除所有长度为四个或更短的单词。该步骤的假设是,短单词通常更为常见,因此不会对我们希望从主题模型中提取的见解产生重要影响。这是数据清理和预处理的最后一步:

    example = [i for i in example if len(i) >= 5]
    print(example)
    

    输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_21.jpg

图 7.21:标题五号清理后的结果

现在,我们已经逐个标题完成了清理和预处理步骤,我们需要将这些步骤应用到近 100,000 个标题上。将这些步骤像对单个标题那样手动应用到每个标题上是不可行的。

练习 30:完整数据清理

在本练习中,我们将把练习 29中的第 2 步到第 12 步(逐步清理数据)合并成一个可以应用于每个标题的函数。该函数将接受一个字符串格式的标题作为输入,并输出一个以标记列表形式呈现的清理后标题。主题模型要求文档以字符串格式而非标记列表的形式呈现,因此,在第 4 步中,标记列表将被转换回字符串:

  1. 定义一个包含练习 29中清理过程所有单独步骤的函数(逐步清理数据):

    def do_headline_cleaning(txt):
          # identify language of tweet
          # return null if language not English
        lg = do_language_identifying(txt)
        if lg != 'en': 
            return None
          # split the string on whitespace
        out = txt.split(" ")
          # identify urls
          # replace with URL
        out = ['URL' if bool(regex.search("http[s]?://", i)) else i for i in out]
          # remove all punctuation
        out = [regex.sub("[^\\w\\s]|\n", "", i) for i in out]
          # remove all numerics
        out = [regex.sub("^[0-9]*$", "", i) for i in out]
          # make all non-keywords lowercase
        out = [i.lower() if i not in "URL" else i for i in out]
          # remove URL
        out = [i for i in out if i not in "URL"]
          # remove stopwords
        list_stop_words = nltk.corpus.stopwords.words("English")
        list_stop_words = [regex.sub("[^\\w\\s]", "", i) for i in list_stop_words]
        out = [i for i in out if i not in list_stop_words]
          # lemmatizing
        out = [do_lemmatizing(i) for i in out]
          # keep words 5 or more characters long
        out = [i for i in out if len(i) >= 5]
        return out
    
  2. 在每个标题上执行该函数。Python 中的map函数是将用户定义的函数应用于列表中每个元素的一个好方法。将map对象转换为列表,并将其赋值给clean变量。clean变量是一个列表的列表:

    clean = list(map(do_headline_cleaning, raw))
    
  3. do_headline_cleaning中,如果检测到标题的语言不是英语,则返回None。最终清理后的列表元素应仅为列表,而不是None,因此需要移除所有None类型。使用print显示前五个清理后的标题和clean变量的长度:

    clean = list(filter(None.__ne__, clean))
    print("HEADLINES:\n{lines}\n".format(lines=clean[:5]))
    print("LENGTH:\n{length}\n".format(length=len(clean)))
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_22.jpg

    图 7.22:标题及其长度
  4. 对每个单独的标题,使用空格分隔符连接标记。此时,标题应为一个非结构化的单词集合,尽管对人类读者来说毫无意义,但对主题建模来说是理想的:

    clean_sentences = [" ".join(i) for i in clean]
    print(clean_sentences[0:10])
    

    清理后的标题应类似于以下内容:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_23.jpg

图 7.23:用于建模的清理后的标题

回顾一下,清理和预处理工作实际做的事情是从数据中剔除噪声,使得模型能够集中于那些实际上可能提供洞察的数据元素。例如,任何与特定主题无关的词(或停用词)不应该影响主题的生成,但如果不小心留下,它们可能会影响主题的生成。为了避免所谓的“假信号”,我们移除了这些词。同样,由于主题模型无法识别上下文,标点符号是无关的,因此也被移除。即使模型在没有清理噪声数据的情况下能够找到主题,未清理的数据可能会有成千上万甚至更多的额外词汇和随机字符需要解析,具体取决于语料库中文档的数量,这可能会显著增加计算需求。因此,数据清理是主题建模的一个重要部分。

活动 15:加载并清理 Twitter 数据

在此活动中,我们将加载并清理 Twitter 数据,以便在后续活动中进行建模。我们对标题数据的使用仍在进行中,因此我们将在一个单独的 Jupyter notebook 中完成此活动,但所有要求和导入的库都保持一致。

目标是处理原始推文数据,清理它,并生成与前一个练习中步骤 4相同的输出。该输出应该是一个列表,列表长度类似于原始数据文件中的行数。长度类似,意味着可能不等于行数,因为在清理过程中可能会丢弃一些推文,原因包括推文可能不是英文的。列表中的每个元素应代表一条推文,且仅包含可能与主题形成相关的词汇。

完成此活动的步骤如下:

  1. 导入必要的库。

  2. github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Activity15-Activity17加载 LA Times 健康推特数据(latimeshealth.txt)。

    注意

    此数据集下载自archive.ics.uci.edu/ml/datasets/Health+News+in+Twitter。我们已将其上传至 GitHub,并可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Activity15-Activity17下载。Karami, A., Gangopadhyay, A., Zhou, B., & Kharrazi, H. (2017). 模糊方法在健康与医学语料库中的主题发现。《国际模糊系统杂志》。UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚大学欧文分校:加利福尼亚大学信息与计算机科学学院。

  3. 运行快速的探索性分析,确定数据的大小和结构。

  4. 提取推文文本并转换为列表对象。

  5. 编写一个函数来执行语言检测、基于空格的分词、将屏幕名称和网址替换为 SCREENNAMEURL。该函数还应删除标点、数字以及屏幕名称和网址的替换。将所有内容转换为小写,但屏幕名称和网址除外。它应删除所有停用词,执行词形还原,并保留五个或更多字母的单词。

  6. 将在 步骤 5 中定义的函数应用于每个推文。

  7. 删除输出列表中等于 None 的元素。

  8. 将每个推文的元素转换回字符串。使用空格连接。

  9. 保持笔记本开放以备将来建模。

    注意

    本章中的所有活动都需要在同一个笔记本中执行。

  10. 输出将如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_24.jpg

图 7.24:用于建模的已清理推文
注意

此活动的解决方案可以在第 357 页找到。

潜在狄利克雷分配

2003 年,David Biel、Andrew Ng 和 Michael Jordan 发表了关于主题建模算法的文章,称为潜在狄利克雷分配LDA)。LDA 是一种生成概率模型。这意味着我们假定已知生成数据的过程,该过程以概率形式表达,并且从数据反推生成数据的参数。在这种情况下,我们感兴趣的是生成数据的主题。这里讨论的过程是 LDA 的最基本形式,但对于学习来说也是最易理解的。

对于语料库中的每个文档,假定生成过程如下:

  1. 选择 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_01.png,其中https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_02.png是单词的数量。

  2. 选择 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_03.png,其中 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_04.png 是主题的分布。

  3. 对于每一个 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_05.png单词 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_06.png,选择主题 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_07.png,并从 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_09.png

让我们更详细地回顾一下生成过程。前面提到的三个步骤会为语料库中的每个文档重复执行。初始步骤是通过从大多数情况下的泊松分布中抽样来选择文档中的单词数。需要注意的是,由于 N 与其他变量无关,因此在推导算法时,生成 N 的随机性大多被忽略。选择了N后,接下来是生成主题混合或每个文档独有的主题分布。可以将其看作是每个文档的主题列表,列表中的概率表示每个主题在文档中所占的比例。考虑三个主题:A、B 和 C。例如,一个文档可能是 100%的主题 A、75%的主题 B 和 25%的主题 C,或者是其他无数的组合。最后,文档中的特定单词是通过基于所选主题和该主题的单词分布的概率条件来选择的。需要注意的是,文档并不是以这种方式生成的,但这种方式是一个合理的代理。

这个过程可以看作是在分布上的分布。从文档集合(分布)中选择一个文档,并从该文档中通过多项分布选择一个主题,这个主题来自由狄利克雷分布生成的该文档的主题概率分布:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_25.jpg

图 7.25:LDA 的图形表示

构建表示 LDA 解的公式最直接的方法是通过图形表示。这种特殊的表示法称为板符号图模型,因为它使用板块来表示过程中的两个迭代步骤。你可能还记得,生成过程会对语料库中的每个文档执行,因此最外层的板块,标记为 M,表示对每个文档进行迭代。同样,第 3 步中的单词迭代则通过图表中最内层的板块表示,标记为N。圆圈表示参数、分布和结果。带阴影的圆圈,标记为w,表示选定的单词,它是唯一已知的数据,因此用来反推生成过程。除了w之外,图中的其他四个变量定义如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_14.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_18.png 参数表现相似。

变分推断

LDA 的一个大问题是条件概率(即分布)的评估是不可管理的,因此不直接计算这些概率,而是进行近似。变分推断是一种简单的近似算法,但它有一个复杂的推导过程,需要对概率有深入的理解。为了能更多地关注 LDA 的应用,本节将简要介绍变分推断在此背景下的应用,但不会全面探讨该算法。

让我们花一点时间直观地讲解变分推断算法。首先,随机地将语料库中每个文档中的每个单词分配给一个主题。然后,针对每个文档中的每个单词,分别计算两个比例。这些比例分别是:文档中当前分配给该主题的单词比例,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_19.png,以及在所有文档中,特定单词分配给该主题的比例,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_20.png。将这两个比例相乘,使用结果比例为该单词分配一个新的主题。重复此过程,直到达到稳态,即主题分配不再发生显著变化为止。然后,这些分配将被用来估算文档内的主题混合和主题内的单词混合。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_26.jpg

图 7.26:变分推断过程

变分推断背后的思维过程是,如果实际分布是不可解的,那么应该找到一个简单的分布,称为变分分布,它非常接近可解的初始分布,以便使推断成为可能。

首先,选择一个分布族 q,并基于新的变分参数进行条件化。优化这些参数,使得原始分布(对于熟悉贝叶斯统计的人来说,实际上是后验分布)与变分分布尽可能接近。变分分布将足够接近原始的后验分布,可以作为代理,从而使得基于该分布进行的任何推断都适用于原始的后验分布。分布族 q 的通用公式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_27.jpg

图 7.27:分布族的公式,q

有大量潜在的变分分布可以用作后验分布的近似。首先从这些分布中选择一个初始变分分布,作为优化过程的起点,该过程会迭代地不断逼近最优分布。最优参数是能够最好地近似后验分布的分布参数。

两个分布的相似度通过 Kullback-Leibler(KL)散度来衡量。KL 散度也被称为相对熵。同样,找到最佳的变分分布 q,以近似原始后验分布 p,需要最小化 KL 散度。找到最小化散度的参数的默认方法是迭代的固定点方法,我们在这里不深入讨论。

一旦识别出最优分布(这意味着最优参数已被确定),就可以利用它来生成输出矩阵并进行任何必要的推断。

词袋模型

文本不能直接传递给任何机器学习算法;首先需要将其进行数值编码。在机器学习中处理文本的一种简单方法是通过词袋模型,该模型去除词语顺序的信息,严格关注每个词的出现程度,即计数或频率。可以利用 Python 的 sklearn 库,将前一个练习中创建的清理向量转换为 LDA 模型所需的结构。由于 LDA 是一个概率模型,我们不希望对词语出现次数进行任何缩放或加权;相反,我们选择输入原始计数。

词袋模型的输入将是从练习 4完整数据清理)返回的清理后的字符串列表。输出将是文档编号、单词的数字编码及该单词在文档中出现的次数。这三项将以元组和整数的形式呈现。元组类似于(0,325),其中 0 是文档编号,325 是数字编码的单词。请注意,325 是该单词在所有文档中的编码。然后,整数表示该单词的出现次数。我们将在本章运行的词袋模型来自sklearn,分别为CountVectorizerTfIdfVectorizer。第一个模型返回原始计数,第二个模型返回一个缩放值,我们稍后会讨论。

一个重要的说明是,本章介绍的两个主题模型的结果可能会有所不同,即使数据相同,也会因为随机性而有所变化。无论是 LDA 中的概率还是优化算法都不是确定性的,因此,如果您的结果与这里展示的结果略有不同,也不必惊讶。

练习 31:使用计数向量化器创建词袋模型

在本练习中,我们将运行CountVectorizer(在sklearn中)将我们之前创建的清洗过的标题向量转换为词袋数据结构。此外,我们还将定义一些变量,这些变量将在建模过程中使用。

  1. 定义number_wordsnumber_docsnumber_features。前两个变量控制 LDA 结果的可视化,稍后会详细介绍。number_features变量控制将在特征空间中保留的单词数:

    number_words = 10
    number_docs = 10
    number_features = 1000
    
  2. 运行计数向量化器并打印输出。有三个关键输入参数,分别是max_dfmin_dfmax_features。这些参数进一步筛选语料库中的单词,保留那些最有可能影响模型的单词。那些只出现在少数文档中的单词太稀有,无法归属到任何一个主题中,因此min_df用于丢弃在指定文档数以下出现的单词。而那些出现在太多文档中的单词则不够具体,无法与特定主题关联,因此max_df用于丢弃在超过指定百分比文档中出现的单词。最后,为了避免过拟合模型,限制用于拟合模型的单词数量为出现频率最高的指定数量单词(max_features):

    vectorizer1 = sklearn.feature_extraction.text.CountVectorizer(
        analyzer="word",
        max_df=0.5, 
        min_df=20, 
        max_features=number_features
    )
    clean_vec1 = vectorizer1.fit_transform(clean_sentences)
    print(clean_vec1[0])
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_28.jpg

    图 7.28:词袋数据结构
  3. 提取特征名称和向量化器中的单词。模型仅接受单词的数字编码,因此将特征名称向量与结果合并将使解释更加容易:

    feature_names_vec1 = vectorizer1.get_feature_names()
    

困惑度

模型通常具有可以用来评估其性能的指标。主题模型也不例外,尽管在这种情况下,性能的定义略有不同。在回归和分类中,预测值可以与实际值进行比较,从中可以计算出明确的性能指标。而在主题模型中,预测的可靠性较差,因为模型只知道它训练时使用的单词,而新文档可能不包含这些单词,尽管它们可能涉及相同的主题。由于这一差异,主题模型使用一种特定于语言模型的指标来评估,即困惑度。困惑度(Perplexity,简称 PP)衡量的是在给定单词后,平均而言,有多少个不同的最可能的单词可以跟随它。我们可以用两个单词作为例子:the 和 announce。单词 the 后可以接大量的同样最可能的单词,而单词 announce 后可以接的同样最可能的单词则要少得多——尽管它们的数量仍然很大。

这个思路是:那些平均可以被更少数量的同样最可能的单词跟随的词语更具特异性,并且可以与主题更紧密地关联。因此,较低的困惑度得分意味着更好的语言模型。困惑度与熵非常相似,但通常使用困惑度,因为它更易于解释。正如我们接下来将看到的,它可以用来选择最佳的主题数。当 m 是单词序列中的单词数时,困惑度定义为:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_29.jpg

图 7.29:困惑度公式

练习 32:选择主题数

如前所述,LDA 有两个必需的输入。第一个是文档本身,第二个是主题数。选择合适的主题数非常具有挑战性。寻找最佳主题数的一种方法是对多个主题数进行搜索,并选择对应于最小困惑度得分的主题数。在机器学习中,这种方法称为网格搜索(grid search)。

在本练习中,我们使用不同主题数下拟合的 LDA 模型的困惑度得分,来确定最终采用的主题数。请记住,原始数据集中的标题已经被分类成四个主题。让我们看看这种方法是否返回了四个主题:

  1. 定义一个函数,该函数根据不同的主题数拟合 LDA 模型并计算困惑度得分。返回两个项目:一个数据框,包含主题数及其困惑度得分;一个整数,表示最小困惑度得分对应的主题数:

    def perplexity_by_ntopic(data, ntopics):
        output_dict = {
            "Number Of Topics": [], 
            "Perplexity Score": []
        }
    
        for t in ntopics:
            lda = sklearn.decomposition.LatentDirichletAllocation(
                n_components=t,
                learning_method="online",
                random_state=0
            )
            lda.fit(data)
    
            output_dict["Number Of Topics"].append(t)
            output_dict["Perplexity Score"].append(lda.perplexity(data))
    
        output_df = pandas.DataFrame(output_dict)
    
        index_min_perplexity = output_df["Perplexity Score"].idxmin()
        output_num_topics = output_df.loc[
            index_min_perplexity,  # index
            "Number Of Topics"  # column
        ]
    
        return (output_df, output_num_topics)
    
  2. 执行在步骤 1 中定义的函数。ntopics 输入是一个包含任何值的主题数列表,可以具有任意长度。打印出数据框:

    df_perplexity, optimal_num_topics = perplexity_by_ntopic(
        clean_vec1, 
        ntopics=[1, 2, 3, 4, 6, 8, 10]
    )
    print(df_perplexity)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_30.jpg

    图 7.30:包含主题数量和困惑度得分的数据框
  3. 将困惑度得分绘制为主题数量的函数。这只是查看步骤 2数据框中结果的另一种方式:

    df_perplexity.plot.line("Number Of Topics", "Perplexity Score")
    

    图表如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_31.jpg

图 7.31:困惑度作为主题数量函数的折线图

正如数据框和图表所示,使用困惑度(perplexity)得到的最佳主题数量是三个。将主题数量设置为四时得到的困惑度为第二低,因此,尽管结果与原始数据集中的信息不完全匹配,但结果足够接近,足以增强对网格搜索方法的信心,用于确定最佳主题数量。网格搜索返回三个而不是四个的原因可能有很多,未来的练习中我们将深入探讨这些原因。

练习 33:运行潜在狄利克雷分配(Latent Dirichlet Allocation)

在本练习中,我们实现 LDA 并检查结果。LDA 输出两个矩阵,第一个是主题-文档矩阵,第二个是词-主题矩阵。我们将查看这些矩阵,分别以模型返回的原始形式以及易于理解的格式化表格呈现:

  1. 使用练习 32中找到的最佳主题数量拟合 LDA 模型,选择主题数量

    lda = sklearn.decomposition.LatentDirichletAllocation(
        n_components=optimal_num_topics,
        learning_method="online",
        random_state=0
    )
    lda.fit(clean_vec1)
    

    输出如下所示:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_32.jpg

    图 7.32:LDA 模型
  2. 输出主题-文档矩阵及其形状,以确认其与主题数量和文档数量一致。矩阵的每一行是每个文档的主题分布:

    lda_transform = lda.transform(clean_vec1)
    print(lda_transform.shape)
    print(lda_transform)
    

    输出如下所示:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_33.jpg

    图 7.33:主题-文档矩阵及其维度
  3. 输出词-主题矩阵及其形状,以确认其与练习 31中指定的特征数量(词语)以及输入的主题数量一致。每一行基本上是每个词分配到该主题的计数(虽然不完全是计数),但这些准计数可以转化为每个主题的词分布:

    lda_components = lda.components_
    print(lda_components.shape)
    print(lda_components)
    

    输出如下所示:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_34.jpg

    图 7.34:词-主题矩阵及其维度
  4. 定义一个函数,将两个输出矩阵格式化为易于阅读的表格:

    def get_topics(mod, vec, names, docs, ndocs, nwords):
        # word to topic matrix
        W = mod.components_
        W_norm = W / W.sum(axis=1)[:, numpy.newaxis]
        # topic to document matrix
        H = mod.transform(vec)
        W_dict = {}
        H_dict = {}
        for tpc_idx, tpc_val in enumerate(W_norm):
            topic = "Topic{}".format(tpc_idx)
            # formatting w
            W_indices = tpc_val.argsort()[::-1][:nwords]
            W_names_values = [
                (round(tpc_val[j], 4), names[j]) 
                for j in W_indices
            ]
            W_dict[topic] = W_names_values
            # formatting h
            H_indices = H[:, tpc_idx].argsort()[::-1][:ndocs]
            H_names_values = [
                (round(H[:, tpc_idx][j], 4), docs[j]) 
                for j in H_indices
            ]
            H_dict[topic] = H_names_values
        W_df = pandas.DataFrame(
            W_dict, 
            index=["Word" + str(i) for i in range(nwords)]
        )
        H_df = pandas.DataFrame(
            H_dict,
            index=["Doc" + str(i) for i in range(ndocs)]
        )
        return (W_df, H_df)
    

    该函数可能比较复杂,因此让我们一步步分析。首先创建WH矩阵,包括将W的分配计数转化为每个主题的词分布。然后,遍历各个主题。在每次迭代中,识别与每个主题相关的顶级词汇和文档。将结果转换为两个数据框。

  5. 执行步骤 4中定义的函数:

    W_df, H_df = get_topics(
        mod=lda,
        vec=clean_vec1,
        names=feature_names_vec1,
        docs=raw,
        ndocs=number_docs, 
        nwords=number_words
    )
    
  6. 打印出单词-主题数据框。它展示了与每个主题相关的前十个单词,按分布值排序。从这个数据框中,我们可以识别出单词分组所代表的抽象主题。更多关于抽象主题的内容请参见后续:

    print(W_df)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_35.jpg

    图 7.35:单词-主题表
  7. 打印出主题-文档数据框。它展示了与每个主题最相关的 10 篇文档。数据来源于每篇文档的主题分布:

    print(H_df)
    

    输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_36.jpg

图 7.36:主题-文档表

单词-主题数据框的结果显示,抽象主题是巴拉克·奥巴马、经济和微软。有趣的是,描述经济的单词分组中包含了关于巴勒斯坦的提及。原始数据集中指定的四个主题在单词-主题数据框输出中都有体现,但并不是以预期的完全独立的方式呈现。我们可能面临两种问题。首先,涉及经济和巴勒斯坦的主题可能还不成熟,这意味着增加主题的数量可能会解决这个问题。另一个潜在的问题是,LDA 对于相关主题的处理较差。在练习 35尝试四个主题中,我们将尝试扩展主题的数量,这将帮助我们更好地理解为什么某个单词分组似乎是多个主题的混合。

练习 34:可视化 LDA

可视化是探索主题模型结果的有用工具。在本练习中,我们将查看三种不同的可视化图表。这些可视化图表包括基本的直方图以及使用 t-SNE 和 PCA 的专业可视化。

为了创建一些可视化图表,我们将使用pyLDAvis库。这个库足够灵活,可以处理使用不同框架构建的主题模型。在这种情况下,我们将使用sklearn框架。这个可视化工具会返回一个直方图,展示与每个主题最相关的单词,以及一个二元图,常用于 PCA,其中每个圆圈代表一个主题。从二元图中,我们可以了解每个主题在整个语料库中的流行度,圆圈的大小反映了这一点;圆圈的接近度反映了主题之间的相似性。理想的情况是,圆圈在图中均匀分布,且大小适中。也就是说,我们希望主题之间是独立的,并且在语料库中一致地出现:

  1. 运行并显示pyLDAvis。这个图表是交互式的。点击每个圆圈会更新直方图,显示与该主题最相关的词汇。以下是这个交互式图表的一个视图:

    lda_plot = pyLDAvis.sklearn.prepare(lda, clean_vec1, vectorizer1, R=10)
    pyLDAvis.display(lda_plot)
    

    图表如下所示:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_37.jpg

    图 7.37:LDA 模型的直方图和二元图
  2. 定义一个函数,拟合 t-SNE 模型并绘制结果:

    def plot_tsne(data, threshold):
        # filter data according to threshold
        index_meet_threshold = numpy.amax(data, axis=1) >= threshold
        lda_transform_filt = data[index_meet_threshold]
        # fit tsne model
        # x-d -> 2-d, x = number of topics
        tsne = sklearn.manifold.TSNE(
            n_components=2, 
            verbose=0, 
            random_state=0, 
            angle=0.5, 
            init='pca'
        )
        tsne_fit = tsne.fit_transform(lda_transform_filt)
        # most probable topic for each headline
        most_prob_topic = []
        for i in range(tsne_fit.shape[0]):
            most_prob_topic.append(lda_transform_filt[i].argmax())
        print("LENGTH:\n{}\n".format(len(most_prob_topic)))
        unique, counts = numpy.unique(
            numpy.array(most_prob_topic), 
            return_counts=True
        )
        print("COUNTS:\n{}\n".format(numpy.asarray((unique, counts)).T))
        # make plot
        color_list = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
        for i in list(set(most_prob_topic)):
            indices = [idx for idx, val in enumerate(most_prob_topic) if val == i]
            matplotlib.pyplot.scatter(
                x=tsne_fit[indices, 0], 
                y=tsne_fit[indices, 1], 
                s=0.5, 
                c=color_list[i], 
                label='Topic' + str(i),
                alpha=0.25
            )
        matplotlib.pyplot.xlabel('x-tsne')
        matplotlib.pyplot.ylabel('y-tsne')
        matplotlib.pyplot.legend(markerscale=10)
    

    该函数首先通过输入阈值过滤主题-文档矩阵。由于有数万个标题,包含所有标题的任何图形都会难以阅读,因此不具备实用性。因此,只有当分布值大于或等于输入阈值时,函数才会绘制文档。一旦数据经过过滤,我们运行 t-SNE,其中组件数为 2,因此可以在二维中绘制结果。接下来,创建一个向量,指示与每个文档最相关的主题。该向量将用于按主题为图形着色。为了理解语料库中主题的分布情况以及阈值过滤的影响,该函数返回主题向量的长度,以及每个主题与最大分布值相关联的文档数。函数的最后一步是创建并返回图形。

  3. 执行该函数:

    plot_tsne(data=lda_transform, threshold=0.75)
    

    输出结果如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_38.jpg

图 7.38:带有关于主题分布度量的 t-SNE 图

可视化结果显示,使用三主题的 LDA 模型总体表现良好。在双变量图中,圆圈的大小适中,表明主题在语料库中出现的一致性较高,并且圆圈之间的间距良好。t-SNE 图展示了清晰的聚类,支持双变量图中圆圈之间的分隔。唯一显著的问题是,之前已讨论过的,某个主题包含了看起来与该主题不相关的词语。在下一个练习中,我们将使用四个主题重新运行 LDA。

练习 35:尝试四个主题

在这个练习中,LDA 模型的主题数被设置为四。这样做的动机是尝试解决三主题 LDA 模型中的一个可能不成熟的主题,该主题包含了与巴勒斯坦和经济相关的词汇。我们将首先按照步骤进行,然后在最后探索结果:

  1. 运行一个 LDA 模型,主题数设置为四:

    lda4 = sklearn.decomposition.LatentDirichletAllocation(
        n_components=4,  # number of topics data suggests
        learning_method="online",
        random_state=0
    )
    lda4.fit(clean_vec1)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_39.jpg

    图 7.39:LDA 模型
  2. 执行前面代码中定义的get_topics函数,生成更易读的词-主题和主题-文档表:

    W_df4, H_df4 = get_topics(
        mod=lda4,
        vec=clean_vec1,
        names=feature_names_vec1,
        docs=raw,
        ndocs=number_docs, 
        nwords=number_words
    )
    
  3. 打印词-主题表:

    print(W_df4)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_40.jpg

    图 7.40:使用四主题 LDA 模型的词-主题表
  4. 打印文档-主题表:

    print(H_df4)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_41.jpg

    图 7.41:使用四主题 LDA 模型的文档-主题表
  5. 使用pyLDAvis显示 LDA 模型结果:

    lda4_plot = pyLDAvis.sklearn.prepare(lda4, clean_vec1, vectorizer1, R=10)
    pyLDAvis.display(lda4_plot)
    

    图形如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_42.jpg

图 7.42:描述四主题 LDA 模型的直方图和双图

看着单词-主题表格,我们发现该模型找到的四个主题与原始数据集中指定的四个主题一致。这些主题分别是巴拉克·奥巴马、巴勒斯坦、微软和经济。现在的问题是,为什么使用四个主题建立的模型比使用三个主题的模型具有更高的困惑度得分?答案可以从步骤 5中生成的可视化中得出。双图中有大小合适的圆圈,但其中两个圆圈非常接近,这表明这两个主题——微软和经济——非常相似。在这种情况下,这种相似性其实是合乎直觉的。微软是一个全球性的大公司,既受到经济的影响,也影响着经济。如果我们继续下一步,可能会运行 t-SNE 图,以检查 t-SNE 图中的聚类是否重叠。现在,让我们将 LDA 的知识应用到另一个数据集上。

活动 16:潜在狄利克雷分配与健康推文

在这个活动中,我们将 LDA 应用于活动 15中加载并清理过的健康推文数据,加载和清理 Twitter 数据。记得使用与活动 15中相同的笔记本。执行完这些步骤后,讨论模型的结果。这些单词组合是否合理?

在这个活动中,让我们假设我们有兴趣获得关于主要公共卫生话题的高层次理解。也就是说,了解人们在健康领域讨论的内容。我们收集了一些数据,可以为这个问题提供一些线索。如我们所讨论的,识别数据集中主要话题的最简单方法是主题建模。

以下是完成此活动的步骤:

  1. 指定number_wordsnumber_docsnumber_features变量。

  2. 创建一个词袋模型,并将特征名称分配给另一个变量以供以后使用。

  3. 确定最优的主题数。

  4. 使用最优的主题数拟合 LDA 模型。

  5. 创建并打印单词-主题表格。

  6. 打印文档-主题表格。

  7. 创建一个双图可视化。

  8. 保持笔记本开启以便进行未来的建模。

    输出将如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_43.jpg

图 7.43:用于健康推文的 LDA 模型训练的直方图和双图
注意

此活动的解决方案可以在第 360 页找到。

词袋模型后续处理

在运行 LDA 模型时,使用了计数向量器词袋模型,但这不是唯一的词袋模型。词频 – 逆文档频率(TF-IDF)类似于 LDA 算法中使用的计数向量器,不同之处在于,TF-IDF 返回的是一个权重,而不是原始计数,反映了给定单词在语料库中文档中的重要性。这种加权方案的关键组成部分是,对于给定单词在整个语料库中出现的频率有一个归一化组件。考虑单词 “have”。

单词 “have” 可能在单个文档中出现多次,表明它可能对区分该文档的主题很重要,但 “have” 会出现在许多文档中,如果不是大多数的话,在语料库中,可能因此使其无法用于区分主题。本质上,这个方案比仅返回文档中单词的原始计数更进一步,旨在初步识别可能有助于识别抽象主题的单词。TF-IDF 向量化器通过 sklearn 使用 TfidfVectorizer 执行。

练习 36:使用 TF-IDF 创建词袋模型

在这个练习中,我们将使用 TF-IDF 创建一个词袋模型:

  1. 运行 TF-IDF 向量化器并打印出前几行:

    vectorizer2 = sklearn.feature_extraction.text.TfidfVectorizer(
        analyzer="word",
        max_df=0.5, 
        min_df=20, 
        max_features=number_features,
        smooth_idf=False
    )
    clean_vec2 = vectorizer2.fit_transform(clean_sentences)
    print(clean_vec2[0])
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_44.jpg

    图 7.44:TF-IDF 向量化器的输出
  2. 返回特征名称,即语料库字典中的实际单词,用于分析输出时使用。你还记得我们在 练习 31 中运行 CountVectorizer 时做的同样事情,使用计数向量器创建词袋模型

    feature_names_vec2 = vectorizer2.get_feature_names()
    feature_names_vec2
    

    输出的一部分如下:

    'abbas',
     'ability',
     'accelerate',
     'accept',
     'access',
     'accord',
     'account',
     'accused',
     'achieve',
     'acknowledge',
     'acquire',
     'acquisition',
     'across',
     'action',
     'activist',
     'activity',
     'actually',
    

非负矩阵分解

与 LDA 不同,非负矩阵分解(NMF)不是一个概率模型。相反,正如其名称所示,它是一种涉及线性代数的方法。使用矩阵分解作为主题建模的方法是由 Daniel D. Lee 和 H. Sebastian Seung 在 1999 年提出的。该方法属于分解类模型,包括 第四章 中介绍的 PCA,降维与 PCA 入门

PCA 和 NMF 之间的主要区别在于,PCA 要求组件是正交的,同时允许它们是正数或负数。而 NMF 要求矩阵组件是非负的,如果你在数据的上下文中思考这个要求,应该能理解这一点。主题与文档之间不能有负相关,单词与主题之间也不能有负相关。如果你不信服,试着解读一个负权重将一个主题与文档关联起来。比如,主题 T 占文档 D 的 -30%;但这意味着什么呢?这是没有意义的,因此 NMF 对矩阵分解的每一部分都有非负的要求。

让我们定义要分解的矩阵,称之为 X,作为术语-文档矩阵,其中行是词,列是文档。矩阵 X 的每个元素是词 ihttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_22.png 列)中出现的次数,或者是词 i 与文档 j 之间关系的其他量化表示。矩阵 X 本质上是一个稀疏矩阵,因为术语-文档矩阵中的大多数元素都是零,因为每个文档只包含有限数量的词。稍后会有更多关于创建此矩阵和推导量化方法的内容:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_45.jpg

图 7.45:矩阵分解

矩阵分解的形式为 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_Formula_24.png 是一个词-话题矩阵,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_46.jpg

图 7.46:第一个更新规则

第二个更新规则如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_47.jpg

图 7.47:第二个更新规则

WH 会通过迭代更新,直到算法收敛。目标函数也可以证明是非递增的。也就是说,每次迭代更新 WH 后,目标函数都会更接近最小值。注意,如果重新组织更新规则,乘法更新优化器实际上是一个缩放版的梯度下降算法。

构建成功的 NMF 算法的最后一个组件是初始化 WH 组件矩阵,以便乘法更新能够快速工作。初始化矩阵的一个常用方法是奇异值分解(SVD),它是特征分解的推广。在接下来的练习中实现的 NMF 中,矩阵是通过非负双重奇异值分解(Double Singular Value Decomposition)进行初始化的,这基本上是 SVD 的一种更高级版本,且严格非负。对于理解 NMF,这些初始化算法的具体细节并不重要。只需要注意,初始化算法作为优化算法的起点,能够显著加速收敛过程。

练习 37:非负矩阵分解

在这个练习中,我们拟合了 NMF 算法,并输出了与之前使用 LDA 得到的相同的两个结果表。这些表格是单词-主题表,显示与每个主题相关的前 10 个单词,以及文档-主题表,显示与每个主题相关的前 10 个文档。NMF 算法函数中有两个我们之前没有讨论的额外参数,分别是 alphal1_ratio。如果担心模型过拟合,这些参数控制正则化应用到目标函数的方式(l1_ratio)和程度(alpha)。更多详细信息可以在 scikit-learn 库的文档中找到(scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html):

  1. 定义 NMF 模型,并使用 TF-IDF 向量化器的输出调用 fit 函数:

    nmf = sklearn.decomposition.NMF(
        n_components=4,
        init="nndsvda",
        solver="mu",
        beta_loss="frobenius",
        random_state=0, 
        alpha=0.1, 
        l1_ratio=0.5
    )
    nmf.fit(clean_vec2)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_48.jpg

    图 7.48:定义 NMF 模型
  2. 运行get_topics函数生成两个输出表:

    W_df, H_df = get_topics(
        mod=nmf,
        vec=clean_vec2,
        names=feature_names_vec2,
        docs=raw,
        ndocs=number_docs, 
        nwords=number_words
    )
    
  3. 打印W表:

    print(W_df)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_49.jpg

    图 7.49:包含概率的词汇-主题表
  4. 打印H表:

    print(H_df)
    

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_50.jpg

图 7.50:包含概率的文档-主题表

词汇-主题表包含的词汇分组表明与练习 35中使用四主题 LDA 模型得到的抽象主题相同。然而,这次比较中有趣的部分是,一些包含在这些分组中的单词是新的,或者在分组中的位置发生了变化。考虑到两种方法论的不同,这是不足为奇的。鉴于与原始数据集中的主题对齐,我们已经证明这两种方法都是提取语料库潜在主题结构的有效工具。

练习 38:可视化 NMF

本练习的目的是可视化 NMF 的结果。通过可视化结果,可以深入了解各主题的独特性以及每个主题在语料库中的普遍性。在本练习中,我们使用 t-SNE 进行可视化,t-SNE 已在第六章中详细讨论过,t-分布随机邻居嵌入(t-SNE)

  1. 对清理后的数据运行transform,以获取主题-文档分配。打印数据的形状和示例:

    nmf_transform = nmf.transform(clean_vec2)
    print(nmf_transform.shape)
    print(nmf_transform)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_51.jpg

    图 7.51:数据的形状和示例
  2. 运行plot_tsne函数以拟合 t-SNE 模型并绘制结果:

    plot_tsne(data=nmf_transform, threshold=0)
    

    图形如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_52.jpg

图 7.52:t-SNE 图,带有总结语料库中主题分布的指标

t-SNE 图,未指定阈值,显示了部分主题重叠,并且跨语料库的主题频率有明显的差异。这两个事实解释了为什么在使用困惑度时,最佳的主题数是三个。似乎有一些主题之间的相关性,而模型未能完全处理这些相关性。即使有这些主题之间的相关性,当主题数设置为四时,模型仍能找出应该有的主题。

回顾一下,NMF(非负矩阵分解)是一个非概率性主题模型,旨在回答与 LDA(潜在狄利克雷分配)相同的问题。它使用了一个在线性代数中非常流行的概念——矩阵分解,这是将一个庞大且难以处理的矩阵分解成较小且更容易解释的矩阵的过程,这些矩阵可以帮助回答有关数据的许多问题。请记住,非负要求并不是源于数学,而是源于数据本身。任何文档的组件不应该是负数。在许多情况下,NMF 的表现不如 LDA,因为 LDA 结合了先验分布,为主题词分组提供了额外的信息层。然而,我们知道在某些情况下,特别是当主题高度相关时,NMF 可能表现得更好。这些情况之一就是所有练习所基于的头条数据。

活动 17:非负矩阵分解

本活动是对健康 Twitter 数据进行主题建模分析的总结,这些数据在第一项活动中被加载并清理过,并且在第二项活动中使用了 LDA。执行 NMF 非常简单,需要的编码很少,因此我建议利用这个机会调整模型参数,同时思考 NMF 的局限性和优点。

完成此活动的步骤如下:

  1. 创建适当的词袋模型,并将特征名称输出为另一个变量。

  2. 使用第二项活动中的主题数量(n_components)值定义并拟合 NMF 算法。

  3. 获取主题-文档和词-主题结果表。花几分钟时间探索词语分组,并尝试定义抽象的主题。你能量化这些词语分组的含义吗?这些词语分组是否合理?结果是否与使用 LDA 时的结果相似?

  4. 调整模型参数并重新运行第 3 步第 4 步。结果如何变化?

    输出将如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-unspr-lrn-py/img/C12626_07_53.jpg

图 7.53:带有概率的词-主题表
注意

这个活动的解决方案可以在第 364 页找到。

总结

当面临从尚未见过的大量文档集合中提取信息的任务时,主题建模是一个很好的方法,因为它提供了对文档底层结构的洞察。也就是说,主题模型通过接近度而非上下文来寻找词语分组。在这一章中,我们学习了如何应用两种最常见且最有效的主题建模算法:潜在狄利克雷分配(LDA)和非负矩阵分解(NMF)。现在我们应该能够通过多种不同的技术来清理原始文本文档;这些技术可以应用于许多其他建模场景。我们继续学习如何通过应用词袋模型将清理后的语料库转换为每篇文档的原始词频或词权重的适当数据结构。本章的主要焦点是拟合这两种主题模型,包括优化主题数、将输出转换为易于解释的表格,并可视化结果。有了这些信息,你应该能够应用功能完备的主题模型,为你的业务提取价值和洞察。

在下一章,我们将完全改变方向。我们将深入探讨市场篮子分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值