原文:
annas-archive.org/md5/59ef285f4ffb779ed4c411e356902e16
译者:飞龙
第六章:5. 自编码器
概述
在本章中,我们将讨论自编码器及其应用。我们将了解自编码器如何用于降维和去噪。我们将使用 Keras 框架实现一个人工神经网络和自编码器。到本章结束时,你将能够使用卷积神经网络实现一个自编码器模型。
引言
当我们将注意力转向自编码器时,我们将继续讨论降维技术。自编码器是一个特别有趣的研究领域,因为它提供了一种基于人工神经网络的监督学习方法,但在无监督的环境下使用。自编码器基于人工神经网络,是执行降维的极为有效的手段,同时也提供了额外的好处。随着数据、处理能力和网络连接的不断增加,自编码器在使用和研究上迎来了复兴,这种现象自 1980 年代末自编码器起源以来未曾见过。这与人工神经网络的研究是一致的,后者最早在 1960 年代被描述和实现为一种概念。目前,你只需进行简单的互联网搜索,就能发现神经网络的普及和强大功能。
自编码器可以与其他方法结合使用,如递归神经网络或长短期记忆网络(LSTM)架构,用于去噪图像和生成人工数据样本,以预测数据序列。使用人工神经网络所带来的灵活性和强大功能,使得自编码器能够形成非常高效的数据表示,这些表示可以直接作为极其高效的搜索方法,或作为后续处理的特征向量使用。
考虑在图像去噪应用中使用自编码器,我们看到左边的图像(图 5.1),它受到了某些随机噪声的影响。我们可以使用专门训练的自编码器去除这些噪声,如下图右侧所示。通过学习如何去除噪声,自编码器还学会了如何编码组成图像的关键信息,并如何将这些信息解码(或重构)为更清晰的原始图像版本:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_01.jpg
图 5.1:自编码器去噪
注意
这张图片已从www.freenzphotos.com/free-photos-of-bay-of-plenty/stormy-fishermen/
在 CC0 授权下修改。
这个例子展示了自编码器的一个方面,使其在无监督学习(编码阶段)中非常有用,并且另一个方面使其在生成新图像时(解码阶段)也很有用。我们将进一步探讨自编码器的这两个有用阶段,并将自编码器的输出应用于 CIFAR-10 数据集的聚类(www.cs.toronto.edu/~kriz/cifar.html
)。
下面是编码器和解码器的表示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_02.jpg
图 5.2:编码器/解码器表示
人工神经网络基础
由于自编码器基于人工神经网络,因此理解神经网络对理解自编码器也至关重要。本章的这一部分将简要回顾人工神经网络的基础知识。需要注意的是,神经网络有许多方面超出了本书的范围。神经网络的主题很容易填满,并且已经填满了许多书籍,这一部分并不打算成为该主题的详尽讨论。
如前所述,人工神经网络主要用于监督学习问题,在这些问题中,我们有一组输入信息,比如一系列图像,我们正在训练一个算法,将这些信息映射到期望的输出,比如类别或分类。以图 5.3中的 CIFAR-10 数据集为例,它包含 10 个不同类别(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车)的图像,每个类别有 6000 张图像。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_03.jpg
图 5.3:CIFAR-10 数据集
当神经网络用于监督学习时,图像被输入到网络中,网络的期望输出是对应类别标签的表示。
然后,网络将被训练以最大化其推断或预测给定图像的正确标签的能力。
注意
这张图来自www.cs.toronto.edu/~kriz/cifar.html
,出自*《从微小图像中学习多个特征层》*(www.cs.toronto.edu/~kriz/learning-features-2009-TR.pdf
),Alex Krizhevsky,2009 年。
神经元
人工神经网络得名于大脑中常见的生物神经网络。虽然这种类比的准确性确实值得商榷,但它是一个有用的隐喻,可以帮助我们理解人工神经网络的概念。与生物神经网络一样,神经元是所有神经网络的构建块,通过不同的配置连接多个神经元,形成更强大的结构。在图 5.4中,每个神经元由四个部分组成:一个输入值、一个可调权重(theta)、一个作用于权重与输入值乘积的激活函数,以及由此产生的输出值:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_04.jpg
图 5.4:神经元的解剖结构
激活函数的选择取决于神经网络设计的目标,常见的函数包括tanh、sigmoid、linear和ReLU(修正线性单元)。在本章中,我们将使用sigmoid和ReLU激活函数,因此我们可以稍微深入了解它们。
Sigmoid 函数
由于 sigmoid 激活函数能够将输入值转换为接近二进制的输出,因此它在神经网络分类中的输出中被广泛使用。Sigmoid 函数产生以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_05.jpg
图 5.5:Sigmoid 函数的输出
我们可以在前面的图中看到,随着x的增加,sigmoid 函数的输出渐近于 1(趋近但永远无法达到),而当x向负方向远离 0 时,输出渐近于 0。该函数常用于分类任务,因为它提供接近二进制的输出。
我们可以看到,sigmoid 具有渐近性质。由于这一特性,当输入值接近极限时,训练过程会变得缓慢(称为梯度消失)。这是训练中的瓶颈。因此,为了加速训练过程,神经网络的中间阶段使用修正线性单元(ReLU)。然而,ReLU 也有一定的局限性,因为它存在死神经元和偏置问题。
修正线性单元(ReLU)
修正线性单元(ReLU)是一种非常有用的激活函数,通常在神经网络的中间阶段使用。简而言之,对于小于 0 的输入值,ReLU 将其输出为 0,而对于大于 0 的输入值,则返回实际值。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_06.jpg
图 5.6:ReLU 的输出
练习 5.01:建模人工神经网络中的神经元
在本练习中,我们将实际介绍如何在NumPy
中以编程方式表示神经元,并使用sigmoid
函数。我们将固定输入,调整可调权重,以研究其对神经元的影响。为了将这一框架与监督学习中的常见模型关联起来,我们在本练习中的方法与逻辑回归相同。执行以下步骤:
-
导入
numpy
和matplotlib
包:import numpy as np import matplotlib.pyplot as plt
-
将
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)
-
定义神经元的输入(
x
)和可调权重(theta
)。在本例中,输入(x
)将是-5
到5
之间线性间隔的100
个数字。设置theta = 1
:theta = 1 x = np.linspace(-5, 5, 100) x
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_07.jpg
图 5.7:打印输入
-
计算神经元的输出(
y
):y = sigmoid(x * theta)
-
绘制神经元输出与输入的关系图:
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) plt.show()
在以下输出中,您可以看到绘制的
sigmoid
函数——请注意,它通过原点并在0.5
处交叉。https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_08.jpg
图 5.8:神经元与输入的关系图
-
将可调参数
theta
设置为5
,然后重新计算并存储神经元的输出:theta = 5 y_2 = sigmoid(x * theta)
-
将可调参数
theta
改为0.2
,然后重新计算并存储神经元的输出:theta = 0.2 y_3 = sigmoid(x * theta)
-
在一个图表中绘制三条不同的神经元输出曲线(
theta = 1
、theta = 5
和theta = 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) plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_09.jpg
图 5.9:神经元的输出曲线
在本练习中,我们用sigmoid
激活函数模拟了人工神经网络的基本构建块。我们可以看到,使用sigmoid
函数增加了梯度的陡峭度,这意味着只有小的x值才会将输出推向接近 1 或 0。同样,减小theta
会降低神经元对非零值的敏感度,导致需要极端的输入值才能将输出结果推向 0 或 1,从而调节神经元的输出。
注意
要访问此特定部分的源代码,请参考packt.live/2AE9Kwc
。
您还可以在packt.live/3e59UdK
在线运行此示例。
练习 5.02:使用 ReLU 激活函数建模神经元
类似于 练习 5.01,人工神经网络神经元建模,我们将再次建模一个网络,这次使用 ReLU 激活函数。在这个练习中,我们将为 ReLU 激活的神经元开发一系列响应曲线,并描述改变 theta 值对神经元输出的影响:
-
导入
numpy
和matplotlib
:import numpy as np import matplotlib.pyplot as plt
-
将 ReLU 激活函数定义为 Python 函数:
def relu(x): return np.max((0, x))
-
定义神经元的输入(
x
)和可调权重(theta
)。在这个示例中,输入(x
)将是线性间隔在-5
和5
之间的 100 个数字。设置theta = 1
:theta = 1 x = np.linspace(-5, 5, 100) x
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_10.jpg
图 5.10:打印输入
-
计算输出(
y
):y = [relu(_x * theta) for _x in x]
-
绘制神经元输出与输入的关系图:
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) plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_11.jpg
图 5.11:神经元与输入的关系图
-
现在,设置
theta = 5
,重新计算并保存神经元的输出:theta = 5 y_2 = [relu(_x * theta) for _x in x]
-
现在,设置
theta = 0.2
,重新计算并保存神经元的输出:theta = 0.2 y_3 = [relu(_x * theta) for _x in x]
-
在同一张图表上绘制神经元的三条不同输出曲线(
theta = 1
,theta = 5
,和theta = 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) plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_12.jpg
图 5.12:神经元的三条输出曲线
在这个练习中,我们创建了一个基于 ReLU 的人工神经网络神经元模型。我们可以看到,这个神经元的输出与 sigmoid 激活函数的输出有很大不同。对于大于 0 的值,没有饱和区域,因为它仅仅返回函数的输入值。在负方向上,当输入小于 0 时,存在饱和区域,只有 0 会被返回。ReLU 函数是一种非常强大且常用的激活函数,在某些情况下,它比 sigmoid 函数更强大。ReLU 经常是首选的激活函数。
注意
要访问此特定章节的源代码,请参考 packt.live/2O5rnIn
。
你也可以在 packt.live/3iJ2Kzu
上在线运行此示例。
神经网络:架构定义
单个神经元在孤立状态下并不是特别有用;它提供了激活函数和调节输出的手段,但单个神经元的学习能力是有限的。当多个神经元结合并在网络结构中连接在一起时,神经元的功能就变得更加强大。通过使用多个不同的神经元并结合各个神经元的输出,可以建立更复杂的关系,并构建更强大的学习算法。在本节中,我们将简要讨论神经网络的结构,并使用 Keras 机器学习框架实现一个简单的神经网络(keras.io/
)。Keras 是一个高层次的神经网络 API,基于现有的库(如 TensorFlow 或 Theano)之上。Keras 使得在低层框架之间切换变得容易,因为它提供的高层接口在不同的底层库之间保持不变。在本书中,我们将使用 TensorFlow 作为底层库。
以下是一个具有隐藏层的神经网络的简化表示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_13.jpg
图 5.13:神经网络的简化表示
上图展示了一个两层完全连接的神经网络结构。我们可以做出的第一个观察是,这个结构包含了大量的信息,并且具有高度的连接性,箭头表示了每个节点之间的连接。我们从图像的左侧开始,可以看到神经网络的输入值,由(x)值表示。在这个示例中,每个样本有m个输入值,因此,从x11 到x1m 的值代表这些输入值。每个样本的这些值被称为数据的属性或特征,并且每次仅输入一个样本到网络中。然后,这些值会与神经网络第一层对应的权重相乘 (https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_Formula_01.png),然后传入对应神经元的激活函数。这被称为前馈神经网络。在上图中,用来标识权重的符号是https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_Formula_02.png,其中i是权重所属的层,j是输入节点的编号(从顶部开始为 1),而k是后续层中该权重连接的节点。
观察第一层(也叫做隐藏层)输出与输出层输入之间的互联关系,我们可以看到有大量可调节的参数(权重),这些参数可以用来将输入映射到期望的输出。前图的网络代表了一个 n 类神经网络分类器,其中每个 n 个节点的输出表示输入属于相应类别的概率。
每一层都可以使用不同的激活函数,如 h1 和 h2 所示,因此允许不同的激活函数混合使用,例如第一层可以使用 ReLU,第二层可以使用 tanh,第三层可以使用 sigmoid。最终输出是通过将前一层输出与相应的权重相乘,并通过激活函数计算结果来获得的。
如果我们考虑第一层第一节点的输出,它可以通过将输入与相应的权重相乘,求和结果并通过激活函数来计算:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_14.jpg
图 5.14:计算最后一个节点的输出
随着输入和输出之间的层数增加,我们增加了网络的深度。深度的增加意味着可训练参数的增加,以及网络描述数据内在关系的复杂度增加。此外,当我们在每一层添加更多神经元时,我们增加了神经网络的高度。通过增加神经元,网络对数据集的描述能力增强,同时可训练参数也增多。如果增加了过多的神经元,网络可能会记住数据集的内容,但无法对新样本进行泛化。构建神经网络的诀窍在于找到一个平衡点,既能充分描述数据内在关系,又不会过于复杂以至于记住训练样本。
练习 5.03:定义一个 Keras 模型
在本练习中,我们将使用 Keras 机器学习框架定义一个神经网络架构(类似于图 5.13),用于分类 CIFAR-10 数据集的图像。由于每个输入图像的大小为 32 x 32 像素,输入向量将由 32*32 = 1,024 个值组成。CIFAR-10 有 10 个类别,神经网络的输出将由 10 个值组成,每个值表示输入数据属于相应类别的概率。
注意
CIFAR-10 数据集 (www.cs.toronto.edu/~kriz/cifar.html
) 由 60,000 张图像组成,涵盖 10 个类别。这 10 个类别包括飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车,每个类别有 6,000 张图像。通过前面的链接了解更多关于这个数据集的信息。
-
对于本练习,我们将需要 Keras 机器学习框架。如果您还没有安装 Keras 和 TensorFlow,请在 Jupyter 笔记本中使用
conda
进行安装:!conda install tensorflow keras
或者,您也可以通过
pip
安装:!pip install tensorflow keras
-
我们将需要分别从
keras.models
和keras.layers
导入Sequential
和Dense
类。导入这些类:from keras.models import Sequential from keras.layers import Dense
如前所述,输入层将接收 1,024 个值。第二层(层 1)将包含 500 个单元,并且由于该网络需要分类 10 个不同的类别,输出层将有 10 个单元。在 Keras 中,通过将有序的层列表传递给
Sequential
模型类来定义模型。 -
本示例使用了
Dense
层类,这是一个全连接神经网络层。第一层将使用 ReLU 激活函数,而输出层将使用softmax
函数来确定每个类别的概率。定义模型:model = Sequential\ ([Dense(500, input_shape=(1024,), activation='relu'),\ Dense(10, activation='softmax')])
-
定义好模型后,我们可以使用
summary
方法确认模型的结构以及模型中的可训练参数(或权重)数量:model.summary()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_15.jpg
图 5.15:模型中可训练参数的结构和数量
该表总结了神经网络的结构。我们可以看到,我们指定的两个层,其中第一个层有 500 个单元,第二个层有 10 个输出单元。Param #
列告诉我们该特定层中有多少个可训练的权重。该表还告诉我们,网络中总共有 517,510 个可训练的权重。
注意
要访问本节的源代码,请参考packt.live/31WaTdR
。
您还可以在packt.live/3gGEtbA
上在线运行此示例。
在本练习中,我们创建了一个 Keras 神经网络模型,包含超过 500,000 个权重,可用于分类 CIFAR-10 图像。在接下来的章节中,我们将训练这个模型。
神经网络:训练
定义好神经网络模型后,我们可以开始训练过程;在此阶段,我们将以监督方式训练模型,以便在开始训练自编码器之前对 Keras 框架有所了解。监督学习模型通过提供输入信息和已知输出进行训练;训练的目标是构建一个网络,使其仅使用模型的参数,接受输入信息并返回已知的输出。
在像 CIFAR-10 这样的有监督分类示例中,输入信息是图像,而已知的输出是该图像所属的类别。在训练过程中,对于每个样本的预测,使用指定的误差函数计算前馈网络预测中的误差。然后,模型中的每个权重都会被调整,试图减少误差。这个调整过程被称为反向传播,因为误差从输出反向传播通过网络,直到网络的起始部分。
在反向传播过程中,每个可训练的权重都会根据其对总误差的贡献进行调整,调整的幅度与一个被称为学习率的值成比例,学习率控制着可训练权重变化的速度。观察下图,我们可以看到,增大学习率的值可以加快误差减少的速度,但也存在不能收敛到最小误差的风险,因为我们可能会越过最小值。学习率过小可能导致我们失去耐心,或者根本没有足够的时间找到全局最小值。在神经网络训练中,我们的目标是找到误差的全局最小值——基本上就是训练过程中,权重调节到一个无法再进一步减少错误的点。因此,找到合适的学习率是一个试错的过程,虽然从较大的学习率开始并逐渐减小它通常是一个有效的方法。下图表示选择学习率对成本函数优化的影响。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_16.jpg
图 5.16:选择正确的学习率
在前面的图中,您可以看到一个周期内的学习误差,在这种情况下是随着时间的推移变化的。一个周期对应着训练数据集中的完整循环。训练会反复进行,直到预测误差不再减少,或者开发者等待结果时耐心耗尽。为了完成训练过程,我们首先需要做出一些设计决策,其中最重要的是选择最合适的误差函数。可供使用的误差函数种类繁多,从简单的均方差到更复杂的选项都有。分类交叉熵(在接下来的练习中使用)是一个非常有用的误差函数,尤其适用于多类分类问题。
定义了误差函数后,我们需要选择更新可训练参数的方法。最节省内存且有效的更新方法之一是随机梯度下降(SGD)。SGD 有多种变体,所有变体都涉及根据每个权重对计算误差的贡献来调整权重。最终的训练设计决策是选择评估模型的性能指标,并选择最佳架构;在分类问题中,这可能是模型的分类准确率,或者在回归问题中,可能是产生最低误差得分的模型。这些比较通常使用交叉验证方法进行。
练习 5.04:训练一个 Keras 神经网络模型
感谢我们不需要手动编程神经网络的组件,如反向传播,因为 Keras 框架会为我们管理这些。在本次练习中,我们将使用 Keras 训练一个神经网络,使用前一练习中定义的模型架构对 CIFAR-10 数据集的一个小子集进行分类。与所有机器学习问题一样,第一步也是最重要的一步是尽可能多地了解数据集,这将是本次练习的初步重点:
注意
你可以从packt.live/3eexo1s
下载data_batch_1
和batches.meta
文件。
-
导入
pickle
、numpy
、matplotlib
以及keras.models
中的Sequential
类,和keras.layers
中的Dense
。我们将在本练习中使用pickle
来序列化 Python 对象,以便传输或存储:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Sequential from keras.layers import Dense import tensorflow.python.util.deprecation as deprecation deprecation._PRINT_DEPRECATION_WARNINGS = False
-
加载随附源代码提供的 CIFAR-10 数据集样本,该样本位于
data_batch_1
文件中:with open('data_batch_1', 'rb') as f: batch_1 = pickle.load(f, encoding='bytes')
-
数据以字典形式加载。显示字典的键:
batch_1.keys()
输出如下:
dict_keys([b'batch_label', b'labels', b'data', b'filenames'])
-
请注意,键是以二进制字符串形式存储的,表示为
b'
。我们关注的是数据和标签的内容。首先查看标签:labels = batch_1[b'labels'] labels
一部分输出如下,每个类别号对应一个文本标签(飞机、汽车等):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_17.jpg
图 5.17:显示标签
-
我们可以看到,标签是一个值为 0-9 的列表,表示每个样本所属的类别。现在,查看
data
键的内容:batch_1[b'data']
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_18.jpg
图 5.18:数据键的内容
-
数据键提供了一个 NumPy 数组,其中存储了所有图像数据。图像数据的形状是什么?
batch_1[b'data'].shape
输出如下:
(10000, 3072)
-
我们可以看到我们有 1,000 个样本,但每个样本是一个维度为 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') """ Breaking the 3,072 samples of each single image into thirds, which correspond to Red, Green, Blue channels """ 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
-
显示前 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')
以下输出显示了我们数据集中低分辨率图像的一个样本——这是由于我们最初收到的 32 x 32 分辨率图像所导致的:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_19.jpg
图 5.19:前 12 张图片
标签的实际含义是什么?我们将在下一步中找到答案。
-
使用以下代码加载
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/unspr-lrn-ws/img/B15923_05_20.jpg
图 5.20:标签的含义
-
解码二进制字符串以获得实际标签:
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/unspr-lrn-ws/img/B15923_05_21.jpg
图 5.21:打印实际标签
-
打印前 12 张图片的标签:
for lab in labels[:12]: print(actual_labels[lab], end=', ')
输出如下:
frog, truck, truck, deer, automobile, automobile, bird, horse, ship, cat, deer, horse,
-
现在我们需要准备数据来训练模型。第一步是准备输出。目前,输出是一个包含数字 0-9 的列表,但我们需要每个样本表示为一个包含 10 个单元的向量,按照之前的模型来处理。
one_hot_labels = np.zeros((images.shape[0], 10)) for idx, lab in enumerate(labels): one_hot_labels[idx, lab] = 1
-
显示前 12 个样本的 one-hot 编码值:
one_hot_labels[:12]
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_22.jpg
图 5.22:前 12 个样本的 one-hot 编码值
-
该模型有 1,024 个输入,因为它期望一个 32 x 32 的灰度图像。对于每张图像,取三个通道的平均值将其转换为 RGB:
images = images.mean(axis=-1)
-
再次显示前 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/unspr-lrn-ws/img/B15923_05_23.jpg
图 5.23:再次显示前 12 张图片。
-
最后,将图像缩放到 0 到 1 之间,这是神经网络输入所要求的。由于图像中的最大值是 255,我们将其直接除以 255:
images /= 255.
-
我们还需要将图像调整为 10,000 x 1,024 的形状。我们将选择前 7,000 个样本进行训练,最后 3,000 个样本用于评估模型:
images = images.reshape((-1, 32 ** 2)) x_train = images[:7000] y_train = one_hot_labels[:7000] x_test = images[7000:] y_test = one_hot_labels[7000:]
-
使用与 练习 5.03、定义一个 Keras 模型 相同的架构重新定义模型:
model = Sequential\ ([Dense(500, input_shape=(1024,), activation='relu'),\ Dense(10, activation='softmax')])
-
现在我们可以在 Keras 中训练模型。我们首先需要编译方法来指定训练参数。我们将使用类别交叉熵、Adam 优化器和分类准确度作为性能度量:
model.compile(loss='categorical_crossentropy',\ optimizer='adam',\ metrics=['accuracy'])
-
使用反向传播训练模型 100 个周期,并使用模型的
fit
方法:model.fit(x_train, y_train, epochs=100, \ validation_data=(x_test, y_test), \ shuffle = False)
输出结果如下。请注意,由于神经网络训练的随机性,你的结果可能会略有不同:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_24.jpg
图 5.24:训练模型
注意
这里,我们使用 Keras 来训练我们的神经网络模型。Keras 层中的权重初始化是随机进行的,无法通过任何随机种子来控制。因此,每次运行代码时,结果可能会有所不同。
-
使用这个网络,我们在训练数据上达到了大约 75.67%的分类准确率,在验证数据上达到了 32.47%的分类准确率(在图 5.24中显示为
acc: 0.7567
和val_acc: 0.3247
),该网络处理了 10,000 个样本。再次检查前 12 个样本的预测结果:predictions = model.predict(images[:12]) predictions
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_25.jpg
图 5.25:打印预测结果
-
我们可以使用
argmax
方法来确定每个样本的最可能类别:np.argmax(predictions, axis=1)
输出结果如下:
array([6, 9, 9, 4, 1, 1, 2, 7, 8, 3, 4, 7], dtype=int64)
-
与标签进行比较:
labels[:12]
输出结果如下:
[6, 9, 9, 4, 1, 1, 2, 7, 8, 3, 4, 7]
注意
要访问此特定部分的源代码,请参考
packt.live/2CgH25b
。你也可以在
packt.live/38CKwuD
在线运行这个示例。
我们现在已经在 Keras 中训练了一个神经网络模型。完成下一个活动以进一步强化你在训练神经网络方面的技能。
活动 5.01:MNIST 神经网络
在这个活动中,你将训练一个神经网络来识别 MNIST 数据集中的图像,并强化你在训练神经网络方面的技能。这个活动是许多神经网络架构的基础,尤其是在计算机视觉中的分类问题。从物体检测与识别到分类,这种通用结构在各种应用中得到了使用。
这些步骤将帮助你完成该活动:
-
导入
pickle
、numpy
、matplotlib
以及 Keras 中的Sequential
和Dense
类。 -
加载
mnist.pkl
文件,它包含来自 MNIST 数据集的前 10,000 张图像及其对应的标签,源代码中提供了这些数据。MNIST 数据集是一个包含 0 到 9 手写数字的 28x28 灰度图像系列。提取图像和标签。注意
你可以在
packt.live/2JOLAQB
找到mnist.pkl
文件。 -
绘制前 10 个样本及其对应的标签。
-
使用独热编码对标签进行编码。
-
准备将图像输入到神经网络中。作为提示,这个过程包含两个独立的步骤。
-
在 Keras 中构建一个神经网络模型,该模型接受准备好的图像,并具有 600 个单元的隐藏层,使用 ReLU 激活函数,输出层的单元数与类别数相同。输出层使用
softmax
激活函数。 -
使用多类交叉熵、随机梯度下降和准确性度量来编译模型。
-
训练模型。需要多少个周期才能在训练数据上达到至少 95%的分类准确率?
完成这个任务后,你将训练一个简单的神经网络来识别手写数字 0 到 9。你还将开发一个通用框架,用于构建分类问题的神经网络。借助这个框架,你可以扩展和修改网络来处理其他各种任务。你将要分类的数字的预览图像如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_26.jpg
图 5.26:待分类数字的预览
注意
该活动的解决方案可以在第 449 页找到。
自编码器
自编码器是一种专门设计的神经网络架构,旨在以高效且描述性强的方式将输入信息压缩到较低的维度空间中。自编码器网络可以分解为两个独立的子网络或阶段:编码阶段和解码阶段。
以下是一个简化的自编码器模型,使用 CIFAR-10 数据集:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_27.jpg
图 5.27:简单的自编码器网络架构
第一个阶段,即编码阶段,将输入信息压缩到一个比输入样本大小更小的后续层中。后续的解码阶段则会扩展压缩后的图像数据,并尝试将其恢复为原始形式。因此,网络的输入和期望输出是相同的;网络输入,例如 CIFAR-10 数据集中的一张图像,并试图恢复成相同的图像。这个网络架构如上图所示;在这张图中,我们可以看到自编码器的编码阶段减少了表示信息的神经元数量,而解码阶段则将压缩格式恢复为原始状态。使用解码阶段有助于确保编码器正确表示了信息,因为恢复图像所需的仅仅是压缩后的表示。
练习 5.05:简单自编码器
在本次练习中,我们将为 CIFAR-10 数据集样本构建一个简单的自编码器,压缩图像中的信息以供后续使用。
注意
在本次练习中,我们将使用data_batch_1
文件,它是 CIFAR-10 数据集的一个样本。该文件可以从packt.live/3bYi5I8
下载。
-
导入
pickle
、numpy
和matplotlib
,以及从keras.models
导入Model
类,从keras.layers
导入Input
和Dense
:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Model from keras.layers import Input, Dense import tensorflow.python.util.deprecation as deprecation deprecation._PRINT_DEPRECATION_WARNINGS = False
-
加载数据:
with open('data_batch_1', 'rb') as f: batch_1 = pickle.load(f, encoding='bytes')
-
由于这是一个无监督学习方法,我们只关注图像数据。加载图像数据:
images = np.zeros((10000, 32, 32, 3), dtype='uint8') for idx, img in enumerate(batch_1[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
-
将图像转换为灰度图,缩放到 0 到 1 之间,并将每张图像展平为一个长度为 1,024 的向量:
images = images.mean(axis=-1) images = images / 255.0 images = images.reshape((-1, 32 ** 2)) images
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_28.jpg
图 5.28:缩放后的图像
-
定义自编码器模型。由于我们需要访问编码器阶段的输出,因此我们将采用一种与之前略有不同的方法来定义模型。定义一个包含
1024
个单元的输入层:input_layer = Input(shape=(1024,))
-
定义一个后续的
Dense
层,包含256
个单元(压缩比为 1024/256 = 4),并使用 ReLU 激活函数作为编码阶段。注意,我们已将该层分配给一个变量,并通过call
方法将前一层传递给该类:encoding_stage = Dense(256, activation='relu')(input_layer)
-
使用 sigmoid 函数作为激活函数,并与输入层相同的形状定义一个后续的解码器层。选择 sigmoid 函数是因为输入值仅介于 0 和 1 之间:
decoding_stage = Dense(1024, activation='sigmoid')\ (encoding_stage)
-
通过将网络的第一层和最后一层传递给
Model
类来构建模型:autoencoder = Model(input_layer, decoding_stage)
-
使用二元交叉熵损失函数和
adadelta
梯度下降编译自编码器:autoencoder.compile(loss='binary_crossentropy',\ optimizer='adadelta')
注意
adadelta
是一种更为复杂的随机梯度下降版本,其中学习率基于最近的梯度更新窗口进行调整。与其他调整学习率的方法相比,它可以防止非常旧的周期梯度影响学习率。 -
现在,让我们开始训练模型;同样,我们将图像作为训练数据并作为期望的输出。训练 100 个周期:
autoencoder.fit(images, images, epochs=100)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_29.jpg
图 5.29:训练模型
-
计算并存储前五个样本的编码阶段输出:
encoder_output = Model(input_layer, encoding_stage)\ .predict(images[:5])
-
将编码器输出重新调整为 16 x 16(16 x 16 = 256)像素,并乘以 255:
encoder_output = encoder_output.reshape((-1, 16, 16)) * 255
-
计算并存储前五个样本的解码阶段输出:
decoder_output = autoencoder.predict(images[:5])
-
将解码器的输出重新调整为 32 x 32 并乘以 255:
decoder_output = decoder_output.reshape((-1, 32,32)) * 255
-
重新调整原始图像:
images = images.reshape((-1, 32, 32)) plt.figure(figsize=(10, 7)) for i in range(5): # Plot the original images plt.subplot(3, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.axis('off') # Plot the encoder output plt.subplot(3, 5, i + 6) plt.imshow(encoder_output[i], cmap='gray') plt.axis('off') # Plot the decoder output 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/unspr-lrn-ws/img/B15923_05_30.jpg
图 5.30:简单自编码器的输出
在前面的图中,我们可以看到三行图像。第一行是原始的灰度图像,第二行是对应的自编码器输出,第三行是从编码输入中重构的原始图像。我们可以看到第三行解码后的图像包含了图像基本形状的信息;我们可以看到青蛙和鹿的主体,以及样本中卡车和汽车的轮廓。由于我们只训练了 100 个样本,因此增加训练周期的数量将有助于进一步提升编码器和解码器的性能。现在,我们已经得到了训练好的自编码器阶段的输出,可以将其作为其他无监督算法(如 K 均值或 K 近邻)的特征向量。
注意
要访问此特定部分的源代码,请参考packt.live/2BQH03R
。
你也可以在packt.live/2Z9CMgI
在线运行此示例。
活动 5.02:简单的 MNIST 自动编码器
在本活动中,您将为随附源代码中的 MNIST 数据集创建一个自动编码器网络。像本活动中构建的自动编码器网络在无监督学习的预处理阶段非常有用。网络生成的编码信息可以用于聚类或分割分析,例如基于图像的网页搜索:
-
导入
pickle
、numpy
和matplotlib
,以及 Keras 中的Model
、Input
和Dense
类。 -
从随附源代码中提供的 MNIST 数据集样本中加载图像(
mnist.pkl
)。注意
你可以从
packt.live/2wmpyl5
下载mnist.pkl
文件。 -
为神经网络准备图像。作为提示,整个过程有两个独立的步骤。
-
构建一个简单的自动编码器网络,将图像大小减少到编码阶段后的 10 x 10。
-
使用二元交叉熵损失函数和
adadelta
梯度下降法编译自动编码器。 -
适配编码器模型。
-
计算并存储前五个样本的编码阶段输出。
-
将编码器输出重塑为 10 x 10(10 x 10 = 100)像素,并乘以 255。
-
计算并存储解码阶段前五个样本的输出。
-
将解码器的输出重塑为 28 x 28 并乘以 255。
-
绘制原始图像、编码器输出和解码器的图像。
完成此活动后,您将成功训练一个自动编码器网络,从数据集中提取关键信息,为后续处理做好准备。输出将类似于以下内容:
![图 5.31:原始图像、编码器输出和解码器的预期图]
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_31.jpg)
图 5.31:原始图像、编码器输出和解码器的预期图
注意
该活动的解决方案可以在第 452 页找到。
练习 5.06:多层自动编码器
在本练习中,我们将为 CIFAR-10 数据集样本构建一个多层自动编码器,将图像中存储的信息压缩,以便后续使用:
注意
你可以从packt.live/2VcY0a9
下载data_batch_1
文件。
-
导入
pickle
、numpy
和matplotlib
,以及keras.models
中的Model
类,导入keras.layers
中的Input
和Dense
:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Model from keras.layers import Input, Dense import tensorflow.python.util.deprecation as deprecation deprecation._PRINT_DEPRECATION_WARNINGS = False
-
加载数据:
with open('data_batch_1', 'rb') as f: dat = pickle.load(f, encoding='bytes')
-
由于这是一个无监督学习方法,我们只关心图像数据。请按照前面的练习加载图像数据:
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
-
将图像转换为灰度图,缩放到 0 和 1 之间,并将每个图像展平为一个长度为 1,024 的向量:
images = images.mean(axis=-1) images = images / 255.0 images = images.reshape((-1, 32 ** 2)) images
输出如下:
![图 5.32:缩放后的图像]
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_32.jpg)
图 5.32:缩放图像
-
定义多层自动编码器模型。我们将使用与简单自动编码器模型相同的输入形状:
input_layer = Input(shape=(1024,))
-
我们将在 256 自动编码器阶段之前添加另一个层——这次使用 512 个神经元:
hidden_encoding = Dense(512, activation='relu')(input_layer)
-
我们使用与练习 5.05、简单自动编码器相同大小的自动编码器,但这次层的输入是
hidden_encoding
层:encoding_stage = Dense(256, activation='relu')(hidden_encoding)
-
添加解码隐藏层:
hidden_decoding = Dense(512, activation='relu')(encoding_stage)
-
使用与上一练习相同的输出阶段,这次连接到隐藏解码阶段:
decoding_stage = Dense(1024, activation='sigmoid')\ (hidden_decoding)
-
通过将网络的第一个和最后一个层传递给
Model
类来构建模型:autoencoder = Model(input_layer, decoding_stage)
-
使用二进制交叉熵损失函数和
adadelta
梯度下降编译自动编码器:autoencoder.compile(loss='binary_crossentropy',\ optimizer='adadelta')
-
现在,让我们拟合模型;再次将图像作为训练数据和期望的输出。训练 100 epochs:
autoencoder.fit(images, images, epochs=100)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_33.jpg
图 5.33:训练模型
-
计算并存储编码阶段前五个样本的输出:
encoder_output = Model(input_stage, encoding_stage)\ .predict(images[:5])
-
将编码器的输出调整为 16 x 16(16 x 16 = 256)像素并乘以 255:
encoder_output = encoder_output.reshape((-1, 16, 16)) * 255
-
计算并存储解码阶段前五个样本的输出:
decoder_output = autoencoder.predict(images[:5])
-
将解码器的输出调整为 32 x 32 并乘以 255:
decoder_output = decoder_output.reshape((-1, 32, 32)) * 255
-
绘制原始图像、编码器输出和解码器:
images = images.reshape((-1, 32, 32)) plt.figure(figsize=(10, 7)) for i in range(5): # Plot original images plt.subplot(3, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.axis('off') # Plot encoder output plt.subplot(3, 5, i + 6) plt.imshow(encoder_output[i], cmap='gray') plt.axis('off') # Plot decoder output 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/unspr-lrn-ws/img/B15923_05_34.jpg
图 5.34:多层自动编码器的输出
通过查看简单自动编码器和多层自动编码器产生的误差得分,并比较图 5.30和图 5.34,我们可以看到两种编码器结构的输出几乎没有差别。两张图的中间行显示出这两种模型学到的特征实际上是不同的。我们可以使用许多方法来改善这两种模型,例如训练更多的 epochs、使用不同数量的单元或神经元,或使用不同数量的层。本练习的目的是展示如何构建和使用自动编码器,但优化通常是一个系统性的试错过程。我们鼓励你调整一些模型参数,并自己探索不同的结果。
注意
若要访问此特定部分的源代码,请参阅packt.live/2ZbaT81
。
你也可以在packt.live/2ZHvOyo
在线运行此示例。
卷积神经网络
在构建所有以前的神经网络模型时,您可能已经注意到,在将图像转换为灰度图像并将每个图像展平为长度为 1,024 的单一向量时,我们移除了所有颜色信息。这样做实质上丢失了很多可能对我们有用的信息。图像中的颜色可能与图像中的类或对象特定相关;此外,我们还丢失了关于图像的空间信息,例如卡车图像中拖车相对驾驶室的位置或鹿的腿相对头部的位置。卷积神经网络不会遭受这种信息丢失。这是因为它们不是使用可训练参数的平面结构,而是将权重存储在网格或矩阵中,这意味着每组参数可以在其结构中有多层。通过将权重组织在网格中,我们可以防止空间信息的丢失,因为权重是以滑动方式应用于图像的。此外,通过具有多个层,我们可以保留与图像相关的颜色通道。
在开发基于卷积神经网络的自编码器时,MaxPooling2D 和 Upsampling2D 层非常重要。MaxPooling 2D 层通过在输入的窗口内选择最大值来在两个维度上减少或缩小输入矩阵的大小。假设我们有一个 2 x 2 的矩阵,其中三个单元格的值为 1,一个单元格的值为 2:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_35.jpg
图 5.35:示例矩阵演示
如果提供给 MaxPooling2D 层,则该矩阵将返回一个值为 2 的单个值,从而在两个方向上将输入的大小减少一半。
UpSampling2D 层的作用与 MaxPooling2D 层相反,它增加输入的大小而不是减小它。上采样过程重复数据的行和列,从而使输入矩阵的大小加倍。对于前面的例子,您将把一个 2 x 2 的矩阵转换成一个 4 x 4 的矩阵,其中右下角的 4 个像素值为 2,其余为 1。
练习 5.07:卷积自编码器
在这个练习中,我们将开发基于卷积神经网络的自编码器,并与之前的全连接神经网络自编码器性能进行比较:
注意
您可以从 packt.live/2x31ww3
下载 data_batch_1
文件。
-
导入
pickle
、numpy
和matplotlib
,以及从keras.models
导入Model
类,从keras.layers
导入Input
、Conv2D
、MaxPooling2D
和UpSampling2D
:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Model from keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D import tensorflow.python.util.deprecation as deprecation deprecation._PRINT_DEPRECATION_WARNINGS = False
-
加载数据:
with open('data_batch_1', 'rb') as f: batch_1 = pickle.load(f, encoding='bytes')
-
由于这是一种无监督学习方法,我们只对图像数据感兴趣。按照前面的练习加载图像数据:
images = np.zeros((10000, 32, 32, 3), dtype='uint8') for idx, img in enumerate(batch_1[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
-
由于我们使用卷积网络,我们可以仅对图像进行重新缩放使用:
images = images / 255.
-
定义卷积自编码器模型。我们将使用与图像相同的形状输入:
input_layer = Input(shape=(32, 32, 3,))
-
添加一个包含 32 层或滤波器的卷积阶段,使用 3 x 3 的权重矩阵,ReLU 激活函数,并使用相同的填充,这意味着输出的长度与输入图像相同。
hidden_encoding = Conv2D\ (32, # Number of filters in the weight matrix (3, 3), # Shape of the weight matrix activation='relu', padding='same', \ # Retaining dimensions between input and output \ )(input_layer)
-
向编码器中添加一个 2 x 2 核的最大池化层。
MaxPooling
会查看图像中的所有值,使用 2 x 2 矩阵进行扫描。在每个 2 x 2 区域中返回最大值,从而将编码层的大小减少一半:encoded = MaxPooling2D((2, 2))(hidden_encoding)
-
添加一个解码卷积层(该层应该与之前的卷积层相同):
hidden_decoding = \ Conv2D(32, # Number of filters in the weight matrix \ (3, 3), # Shape of the weight matrix \ activation='relu', \ # Retaining dimensions between input and output \ padding='same', \ )(encoded)
-
现在我们需要将图像恢复到原始大小,方法是将上采样设置为与
MaxPooling2D
相同的大小:upsample_decoding = UpSampling2D((2, 2))(hidden_decoding)
-
添加最后的卷积阶段,使用三层来处理图像的 RGB 通道:
decoded = \ Conv2D(3, # Number of filters in the weight matrix \ (3, 3), # Shape of the weight matrix \ activation='sigmoid', \ # Retaining dimensions between input and output \ padding='same', \ )(upsample_decoding)
-
通过将网络的第一层和最后一层传递给
Model
类来构建模型:autoencoder = Model(input_layer, decoded)
-
显示模型的结构:
autoencoder.summary()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_36.jpg
图 5.36:模型结构
注意
与之前的自编码器示例相比,我们的可训练参数要少得多。这是一个特定的设计决策,旨在确保该示例能在各种硬件上运行。卷积网络通常需要更多的处理能力,并且常常需要像图形处理单元(GPU)这样的特殊硬件。
-
使用二元交叉熵损失函数和
adadelta
梯度下降编译自编码器:autoencoder.compile(loss='binary_crossentropy',\ optimizer='adadelta')
-
现在,让我们拟合模型;再次地,我们将图像作为训练数据和期望输出传递。与之前训练 100 个周期不同,这次我们将使用 20 个周期,因为卷积网络的计算时间要长得多:
autoencoder.fit(images, images, epochs=20)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_37.jpg
图 5.37:训练模型
注意,在第二个周期后,误差已经比之前的自编码器练习更小,这表明编码/解码模型更好。这个误差减少主要归功于卷积神经网络没有丢弃大量数据,且编码后的图像为 16 x 16 x 32,比之前的 16 x 16 尺寸要大得多。此外,我们没有压缩图像本身,因为它们现在包含的像素较少(16 x 16 x 32 = 8,192),但比之前有更多的深度(32 x 32 x 3 = 3,072)。这些信息已经重新排列,以便进行更有效的编码/解码处理。
-
计算并存储前五个样本的编码阶段输出:
encoder_output = Model(input_layer, encoded).predict(images[:5])
-
每个编码后的图像的形状为 16 x 16 x 32,这是由于为卷积阶段选择的滤波器数量。因此,在没有修改的情况下,我们无法对其进行可视化。我们将其重塑为 256 x 32 的大小,以便进行可视化:
encoder_output = encoder_output.reshape((-1, 256, 32))
-
获取前五张图像的解码器输出:
decoder_output = autoencoder.predict(images[:5])
-
绘制原始图像、平均编码器输出和解码器:
plt.figure(figsize=(10, 7)) for i in range(5): # Plot original images plt.subplot(3, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.axis('off') # Plot encoder output plt.subplot(3, 5, i + 6) plt.imshow(encoder_output[i], cmap='gray') plt.axis('off') # Plot decoder output 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/unspr-lrn-ws/img/B15923_05_38.jpg
图 5.38:原始图像、编码器输出和解码器输出
注意
要获取此特定部分的源代码,请参考packt.live/2VYprpq
。
你也可以在线运行此示例,网址为packt.live/38EDgic
。
活动 5.03:MNIST 卷积自编码器
在这个活动中,我们将通过 MNIST 数据集加强卷积自编码器的知识。卷积自编码器通常在处理大小适中的基于图像的数据集时能够显著提高性能。这在使用自编码器生成人工图像样本时特别有用:
-
导入
pickle
、numpy
和matplotlib
,以及从keras.models
导入Model
类,并从keras.layers
导入Input
、Conv2D
、MaxPooling2D
和UpSampling2D
。 -
加载包含前 10,000 个图像及其对应标签的
mnist.pkl
文件,这些数据可以在附带的源代码中找到。注意
你可以从
packt.live/3e4HOR1
下载mnist.pkl
文件。 -
将图像重新缩放,使其值介于 0 和 1 之间。
-
我们需要重塑图像,增加一个单一的深度通道以供卷积阶段使用。将图像重塑为 28 x 28 x 1 的形状。
-
定义一个输入层。我们将使用与图像相同的输入形状。
-
添加一个卷积阶段,包含 16 层或滤波器,一个 3 x 3 的权重矩阵,一个 ReLU 激活函数,并使用相同的填充方式,这意味着输出图像的尺寸与输入图像相同。
-
向编码器添加一个最大池化层,使用 2 x 2 的核。
-
添加一个解码卷积层。
-
添加一个上采样层。
-
根据初始图像深度,添加最终的卷积阶段,使用一层。
-
通过将网络的第一层和最后一层传递给
Model
类来构建模型。 -
显示模型的结构。
-
使用二进制交叉熵损失函数和
adadelta
梯度下降来编译自编码器。 -
现在,让我们开始拟合模型;我们再次将图像作为训练数据并作为期望的输出。训练 20 个周期,因为卷积神经网络需要更长的计算时间。
-
计算并存储前五个样本的编码阶段输出。
-
为了可视化,重新调整编码器输出的形状,使每个图像为
X*Y
大小。 -
获取前五个图像的解码器输出。
-
将解码器输出重塑为
28 x 28
的大小。 -
将原始图像重塑为
28 x 28
的大小。 -
绘制原始图像、平均编码器输出和解码器输出。
在本次活动结束时,你将开发出一个包含卷积层的自编码器神经网络。请注意,解码器表示中所做的改进。输出将类似于以下内容:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_39.jpg
图 5.39:预期的原始图像、编码器输出和解码器
注释
本活动的解决方案可以在第 455 页找到。
总结
在本章中,我们首先介绍了人工神经网络的基本概念,讲解了它们的结构以及它们是如何学习完成特定任务的。以一个有监督学习的例子为起点,我们构建了一个人工神经网络分类器来识别 CIFAR-10 数据集中的物体。接着,我们探讨了神经网络的自编码器架构,并学习了如何使用这些网络来准备数据集,以便在无监督学习问题中使用。最后,我们通过自编码器的研究,进一步了解了卷积神经网络,并探讨了这些额外层能够带来的好处。本章为我们最终探讨降维问题做好了准备,我们将在降维过程中学习如何使用和可视化编码后的数据,使用 t 分布最近邻(t-SNE)算法。t-SNE 提供了一种极其有效的可视化高维数据的方法,即便在应用了诸如 PCA 等降维技术之后。t-SNE 在无监督学习中尤其有用。在下一章中,我们将进一步探讨嵌入技术,它们是处理高维数据的重要工具。正如你在本章中的 CIFAR-10 数据集中看到的那样,彩色图像文件的大小可能会迅速增大,从而减慢任何神经网络算法的性能。通过使用降维技术,我们可以最小化高维数据的影响。
第七章:6. t-分布随机邻域嵌入
概述
在本章中,我们将讨论随机邻域嵌入(SNE)和t-分布随机邻域嵌入(t-SNE)作为可视化高维数据集的一种方法。我们将实现 t-SNE 模型并解释 t-SNE 的局限性。能够将高维信息提取到低维空间将有助于可视化和探索性分析,同时也能与我们在前几章中探讨的聚类算法相结合。到本章结束时,我们将能够在低维空间中找到高维数据的聚类,例如用户级别信息或图像。
介绍
到目前为止,我们已经描述了多种不同的方法来减少数据集的维度,作为清洗数据、减少计算效率所需的大小或提取数据集中最重要信息的手段。虽然我们已经展示了许多减少高维数据集的方法,但在许多情况下,我们无法将维度的数量减少到可以可视化的大小,也就是二维或三维,而不会过度降低数据质量。考虑我们之前在本书中使用的 MNIST 数据集,这是一个包含数字 0 到 9 的手写数字图像的集合。每个图像的大小为 28 x 28 像素,提供 784 个独立的维度或特征。如果我们将这 784 个维度减少到 2 或 3 个以便进行可视化,我们几乎会失去所有可用的信息。
在本章中,我们将讨论 SNE 和 t-SNE 作为可视化高维数据集的一种手段。这些技术在无监督学习和机器学习系统设计中非常有用,因为能够可视化数据是一件强大的事情。能够可视化数据可以探索关系、识别群体并验证结果。t-SNE 技术已被用于可视化癌细胞核,这些细胞核具有超过 30 个特征,而文档中的数据可能具有上千维,有时即使在应用了像 PCA 这样的技术后也是如此。
MNIST 数据集
现在,我们将使用附带源代码提供的 MNIST 数据集作为实际示例,探索 SNE 和 t-SNE。在继续之前,我们将快速回顾一下 MNIST 及其中的数据。完整的 MNIST 数据集包含 60,000 个训练样本和 10,000 个测试样本,这些样本是手写数字 0 到 9,表示为黑白(或灰度)图像,大小为 28 x 28 像素(即 784 个维度或特征),每个数字类别的样本数量相等。由于数据集的大小和数据质量,MNIST 已经成为机器学习中最具代表性的数据集之一,通常被作为许多机器学习研究论文中的参考数据集。与其他数据集相比,使用 MNIST 探索 SNE 和 t-SNE 的一个优势是,虽然样本包含大量维度,但即使在降维后,仍然可以将其可视化,因为它们可以表示为图像。图 6.1展示了 MNIST 数据集的一个样本:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_01.jpg
图 6.1:MNIST 数据样本
下图展示了通过 PCA 将相同样本降至 30 个主成分:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_02.jpg
图 6.2:通过 PCA 将 MNIST 数据集降至 30 个主成分
随机邻居嵌入(SNE)
SNE 是多种流形学习方法中的一种,旨在描述低维流形或有界区域中的高维空间。乍一看,这似乎是一个不可能完成的任务;如果我们有一个至少包含 30 个特征的数据集,如何合理地在二维空间中表示数据呢?随着我们逐步推导 SNE 的过程,希望你能够看到这是如何可能的。别担心——我们不会在这一章中深入探讨这个过程的数学细节,因为那超出了本章的范围。构建 SNE 可以分为以下几个步骤:
-
将高维空间中数据点之间的距离转换为条件概率。假设我们有两个点,xi 和xj,位于高维空间中,并且我们想要确定xj 作为xi 邻居的概率(pi|j)。为了定义这个概率,我们使用高斯曲线。这样,我们可以看到,对于附近的点,概率较高,而对于远离的点,概率非常低。
-
我们需要确定高斯曲线的宽度,因为它控制着概率选择的速率。宽曲线意味着许多邻近点相距较远,而窄曲线则意味着它们紧密地聚集在一起。
-
一旦我们将数据投影到低维空间,我们还可以确定相应低维数据之间的概率(qi|j),即yi 和yj 之间的概率。
-
SNE 的目标是通过使用成本函数©最小化所有数据点之间的pi|j 和qi|j 之间的差异,将数据放置到低维空间中。这被称为Kullback-Leibler (KL)散度:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_03.jpg
图 6.3:KL 散度
注
要构建高斯分布的 Python 代码,请参考GaussianDist.ipynb
Jupyter 笔记本,链接为packt.live/2UMVubU
。
当在 SNE 中使用高斯分布时,它通过保持局部模式来减少数据的维度。为了实现这一点,SNE 使用梯度下降过程来最小化 C,使用学习率和训练周期等标准参数,正如我们在前一章中讨论神经网络和自编码器时所提到的那样。SNE 在训练过程中实现了一个额外的项——困惑度。困惑度是在比较中选择有效邻居数量的一个参数,对于困惑度值在 5 到 50 之间时,它相对稳定。实际上,建议在这一范围内使用困惑度值进行反复试验。
注
本章后面将详细讨论困惑度。
SNE 提供了一种有效的方式,将高维数据可视化到低维空间,尽管它仍然存在一个被称为拥挤问题的问题。拥挤问题可能出现在我们有一些点大致等距地分布在一个点周围的区域内,i。当这些点在低维空间中被可视化时,它们会相互拥挤,导致可视化困难。如果我们试图在这些拥挤的点之间留出更多空间,问题会加剧,因为任何距离更远的点会在低维空间中被放置得非常远。实质上,我们是在努力平衡既能可视化近距离点,又不失去远离点所提供的信息。
t-分布 SNE
t-SNE 旨在通过修改后的 KL 散度成本函数,使用学生 t 分布替代低维空间中的高斯分布,从而解决拥挤问题。学生 t 分布是一种概率分布,类似于高斯分布,通常用于样本量较小且总体标准差未知的情况。它常用于学生 t 检验中。
修改后的 KL 成本函数在低维空间中对每对数据点的距离给予相等的权重,而学生分布在低维空间中采用较重的尾部以避免拥挤问题。在高维概率计算中,仍然使用高斯分布,以确保在高维空间中适度的距离在低维空间中也能得到忠实的表示。不同分布在各自空间中的组合,允许忠实地表示由小距离和适度距离分开的数据点。
注意
若需要一些关于如何在 Python 中重现学生 t 分布的示例代码,请参考 packt.live/2UMVubU
中的 Jupyter notebook。
幸运的是,我们不需要手动实现 t-SNE,因为 scikit-learn 提供了一个非常有效的实现,且其 API 非常简洁。我们需要记住的是,SNE 和 t-SNE 都是通过计算两个点在高维空间和低维空间中作为邻居的概率,并尽量最小化两个空间之间概率的差异。
练习 6.01:t-SNE MNIST
在本练习中,我们将使用 MNIST 数据集(随附源代码提供)来探索 scikit-learn 中 t-SNE 的实现。正如我们之前描述的那样,使用 MNIST 让我们能够以其他数据集(如波士顿房价数据集或鸢尾花数据集)无法实现的方式来可视化高维空间。请执行以下步骤:
-
对于本练习,导入
pickle
、numpy
、PCA
和TSNE
(来自 scikit-learn),以及matplotlib
:import pickle import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.manifold import TSNE np.random.seed(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/unspr-lrn-ws/img/B15923_06_04.jpg
图 6.4:加载数据集后的输出
这表明 MNIST 数据集已成功加载。
-
在本练习中,我们将对数据集应用 PCA,提取前 30 个主成分。
model_pca = PCA(n_components=30) mnist_pca = model_pca.fit(mnist['images'].reshape((-1, 28 ** 2)))
-
可视化将数据集降至 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/unspr-lrn-ws/img/B15923_06_05.jpg
图 6.5:可视化数据集降维的效果
请注意,尽管图像清晰度有所下降,但由于降维过程,大部分数字仍然清晰可见。值得注意的是,数字 4 似乎受此过程的影响最大。也许 PCA 过程中丢弃的大部分信息都包含了与数字 4 特有样本相关的信息。
-
现在,我们将应用 t-SNE 算法对 PCA 变换后的数据进行处理,以在二维空间中可视化 30 个主成分。我们可以通过 scikit-learn 中的标准模型 API 接口来构建一个 t-SNE 模型。我们将从使用默认值开始,这些值指定了我们将在二维空间中嵌入 30 个维度进行可视化,使用的困惑度为 30,学习率为 200,迭代次数为 1,000。我们将设置
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/unspr-lrn-ws/img/B15923_06_06.jpg
图 6.6:应用 t-SNE 到 PCA 变换后的数据
在上述截图中,我们可以看到 t-SNE 模型提供了多个配置选项,其中一些比其他选项更为重要。我们将重点关注
learning_rate
、n_components
、n_iter
、perplexity
、random_state
和verbose
的值。对于learning_rate
,正如我们之前所讨论的,t-SNE 使用随机梯度下降将高维数据投影到低维空间。学习率控制该过程执行的速度。如果学习率太高,模型可能无法收敛到一个解;如果太低,可能需要很长时间才能得到结果(如果能得到的话)。一个好的经验法则是从默认值开始;如果你发现模型产生了 NaN(非数值)结果,可能需要降低学习率。一旦对模型的结果满意,最好降低学习率并让其运行更长时间(增加n_iter
),这样可能会得到稍微更好的结果。n_components
是嵌入空间(或可视化空间)的维度数。通常情况下,你会希望数据的可视化是二维图,所以只需要使用默认值2
。n_iter
是梯度下降的最大迭代次数。perplexity
是可视化数据时使用的邻居数量。通常,5 到 50 之间的值是合适的,考虑到较大的数据集通常需要比较小的数据集更多的困惑度。
random_state
是任何模型或算法中的一个重要变量,它会在训练开始时初始化其值。计算机硬件和软件工具中提供的随机数生成器实际上并不是真正的随机数生成器;它们实际上是伪随机数生成器。它们提供了接近随机性的良好近似,但并不是真正的随机。计算机中的随机数从一个称为种子的值开始,然后以复杂的方式生成。通过在过程开始时提供相同的种子,每次运行该过程时都会生成相同的“随机数”。虽然这听起来违反直觉,但它对于再现机器学习实验非常有用,因为你不会看到仅由于训练开始时参数初始化的不同而导致的性能差异。这可以提供更多信心,表明性能的变化是由于对模型或训练的有意改变;例如,神经网络的架构。注意
生成真正的随机序列实际上是用计算机完成的最困难的任务之一。计算机软件和硬件的设计方式是,提供的指令每次执行时都完全相同,以便你获得相同的结果。执行中的随机差异,虽然在生成随机数字序列时理想,但在自动化任务和调试问题时将是噩梦。
verbose
是模型的详细程度,描述了在模型拟合过程中打印到屏幕上的信息量。值为 0 表示没有输出,而值为 1 或更大表示输出中详细程度的增加。 -
使用 t-SNE 转换 MNIST 的分解数据集:
mnist_tsne = model_tsne.fit_transform(mnist_30comp)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_07.jpg
图 6.7:转换分解后的数据集
在拟合过程中提供的输出提供了对 scikit-learn 所完成计算的洞察。我们可以看到它正在为所有样本建立索引并计算邻居,然后以批次为 10 的数据来确定邻居的条件概率。在过程结束时,它提供了一个均值标准差值
304.9988
,并且在 250 次和 1,000 次梯度下降迭代后,给出了 KL 散度。 -
现在,视觉化返回数据集中的维度数量:
mnist_tsne.shape
输出如下:
10000,2
我们已经成功地将 784 个维度降到 2 维以便可视化,那么它看起来怎么样呢?
-
创建由模型生成的二维数据的散点图:
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/unspr-lrn-ws/img/B15923_06_08.jpg
图 6.8:MNIST 的二维表示(无标签)
在上面的图中,我们可以看到已经将 MNIST 数据表示为二维形式,但也可以看到它似乎被聚集在一起。数据集中有许多不同的簇或数据块聚集在一起,并且通过一些空白区域与其他簇分开。似乎有大约九个不同的数据组。所有这些观察结果表明,在各个簇内部和它们之间存在某种关系。
-
绘制按相应图像标签分组的二维数据,并使用标记将各个标签分开。
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/unspr-lrn-ws/img/B15923_06_09.jpg
图 6.9:带标签的 MNIST 二维表示
上面的图非常有趣。在这里,我们可以看到各个簇与数据集中的不同图像类别(从零到九)对应。通过无监督的方式,也就是不提前提供标签,结合 PCA 和 t-SNE,已经能够将 MNIST 数据集中的各个类别分离并归类。特别有趣的是,数据中似乎存在一些混淆,尤其是数字四与数字九的图像,五和三的图像之间也有一定重叠;这两个簇部分重合。如果我们查看在步骤 4中的数字九和数字四的 PCA 图像,t-SNE MNIST,这一点就更有意义了:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_10.jpg
图 6.10:九的 PCA 图像
它们确实看起来非常相似;也许这与数字四的形状的不确定性有关。观察接下来的图像,我们可以从左侧的四中看到,两条垂直线几乎相交,而右侧的四则是两条线平行:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_11.jpg
图 6.11:数字四的形状
在图 6.9中另一个有趣的特征是边缘情况,这些在 Jupyter 笔记本中以不同颜色显示。在每个簇的边缘,我们可以看到一些样本在传统的监督学习中会被错误分类,但它们代表的是与其他簇更相似的样本,而不是它们自己的簇。我们来看一个例子;有一些数字三的样本距离正确的簇非常远。
-
获取数据集中所有数字三的索引:
threes = np.where(mnist['labels'] == 3)[0] threes
输出如下:
array([ 7, 10, 12, ..., 9974, 9977, 9991], dtype=int64)
-
查找 x 值小于 0 的数字三:
tsne_threes = mnist_tsne[threes] far_threes = np.where(tsne_threes[:,0]< -30)[0] far_threes
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_12.jpg
图 6.12:x 值小于零的三
-
显示坐标,找到一个与三类簇相距较远的点:
tsne_threes[far_threes]
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_13.jpg
图 6.13:远离三类簇的坐标
-
选择一个具有合理高负值作为
x
坐标的样本。在本例中,我们将选择第二个样本,即样本11
。plt.imshow(mnist['images'][11], cmap='gray') plt.axis('off'); plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_14.jpg
图 6.14:样本 11 的图像
查看这个示例图像及其对应的 t-SNE 坐标,即大约 (-33, 26),并不奇怪,因为这个样本位于数字 8 和 5 的群集附近,这些数字在这幅图像中有许多共同特征。在这个例子中,我们应用了简化的 SNE,展示了它的一些效率以及可能的混淆来源和无监督学习的输出。
注意
要访问此特定部分的源代码,请参阅 packt.live/3iDsCNf
您还可以在 packt.live/3gBdrSK
上在线运行此示例。
活动 6.01:葡萄酒 t-SNE
在这个活动中,我们将通过使用葡萄酒数据集加强我们对 t-SNE 的了解。通过完成此活动,您将能够为自己的定制应用程序构建 t-SNE 模型。葡萄酒数据集(archive.ics.uci.edu/ml/datasets/Wine
)收集了关于意大利葡萄酒化学分析的属性,来自三个不同生产商,但每个生产商都是同一种类型的葡萄酒。这些信息可以用作验证特定意大利地区葡萄酒制成的瓶子的有效性的示例。13 个属性包括酒精、苹果酸、灰分、灰的碱性、镁、总酚、类黄酮、非黄烷类酚、前花青素、颜色强度、色调、稀释酒的 OD280/OD315 和 脯氨酸。
每个样本包含一个类别标识符(1 - 3)。
注意
此数据集来源于 archive.ics.uci.edu/ml/machine-learning-databases/wine/
(UCI 机器学习库 [archive.ics.uci.edu/ml
]。尔湾,加利福尼亚:加利福尼亚大学信息与计算机科学学院)。也可以从 packt.live/3e1JOcY
下载。
这些步骤将帮助您完成此活动:
-
导入
pandas
、numpy
和matplotlib
,以及从 scikit-learn 导入的t-SNE
和PCA
模型。 -
使用附带源代码中包含的
wine.data
文件加载 Wine 数据集,并显示前五行数据。注意
您可以使用
del
关键字在 pandas DataFrames 中删除列。只需将del
关键字传递给数据帧和在平方根内选择的列即可。 -
第一列包含标签;提取此列并从数据集中删除。
-
执行 PCA 将数据集减少到前六个组件。
-
确定描述这六个组件的数据中的方差量。
-
使用指定的随机状态创建 t-SNE 模型,并将
verbose
值设置为 1。 -
将 PCA 数据拟合到 t-SNE 模型。
-
确认 t-SNE 拟合数据的形状是二维的。
-
创建二维数据的散点图。
-
创建一个带有类标签的二维数据散点图,以可视化可能存在的任何聚类。
到本活动结束时,你将使用 Wine 数据集的六个成分构建一个 t-SNE 可视化图,并在图中的数据位置识别一些关系。最终的图形将类似于以下内容:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_15.jpg
图 6.15:预期的绘图
注意
该活动的解决方案可以在第 460 页找到。
解释 t-SNE 图
现在我们可以使用 t 分布的 SNE 可视化高维数据,重要的是要理解此类图表的局限性以及在解读和生成这些图表时需要关注哪些方面。在本节中,我们将重点介绍 t-SNE 的一些重要特性,并演示在使用这种可视化技术时需要小心的地方。
困惑度
正如我们在 t-SNE 的介绍中所描述的,困惑度值指定了计算条件概率时要使用的最近邻居数量。选择该值会对最终结果产生重大影响;当困惑度值较低时,数据中的局部变化主导,因为计算中只使用了少量样本。相反,困惑度值较大时,会考虑更多的全局变化,因为计算中使用了更多的样本。通常,尝试一系列不同的值来研究困惑度的效果是值得的。再次强调,困惑度值在 5 到 50 之间通常效果不错。
练习 6.02:t-SNE MNIST 和困惑度
在这个练习中,我们将尝试不同的困惑度值,并观察其在可视化图中的效果:
-
导入
pickle
、numpy
和matplotlib
,以及从 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 np.random.seed(2)
-
加载 MNIST 数据集。
with open('mnist.pkl', 'rb') as f: mnist = pickle.load(f)
-
使用 PCA,只选择图像数据的前 30 个方差成分:
model_pca = PCA(n_components=30) mnist_pca = model_pca.fit_transform\ (mnist['images'].reshape((-1, 28 ** 2)))
-
在本练习中,我们正在研究困惑度对 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}) plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_16.jpg
图 6.16:迭代模型
注意
前面的输出已被截断以便展示。像这样的标准输出通常会更长。不过,还是将其包含在内,因为在模型训练时,保持关注此类输出非常重要。
注意在三种不同困惑度值下的 KL 散度,以及平均标准差(方差)的增加。通过查看以下带有类别标签的 t-SNE 图,我们可以看到,在较低困惑度值下,聚类被很好地包含,重叠较少。然而,聚类之间几乎没有空间。随着困惑度的增加,聚类之间的空间得到改善,并且在困惑度为 30 时,区分相对清晰。随着困惑度增加到 300,我们可以看到,8 和 5 号聚类以及 9、4 和 7 号聚类开始趋于合并。
让我们从一个较低的困惑度值开始:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_17.jpg
图 6.17:低困惑度值的图表
注意
第 4 步中的绘图函数将生成此图。接下来的输出是不同困惑度值下的图表。
将困惑度增加 10 倍后,聚类变得更加清晰:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_18.jpg
图 6.18:将困惑度增加 10 倍后的图表
通过将困惑度增加到 300,我们开始将更多标签合并在一起:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_19.jpg
图 6.19:将困惑度值增加到 300
在这个练习中,我们加深了对困惑度影响的理解,并了解了该值对整体结果的敏感性。较小的困惑度值可能导致位置之间空间非常小的更均匀混合。增加困惑度可以更有效地分离聚类,但过高的值会导致聚类重叠。
注意
要访问此特定部分的源代码,请参阅 packt.live/3gI0zdp
你还可以在网上运行这个示例,访问 packt.live/3gDcjxR
活动 6.02:t-SNE 葡萄酒与困惑度
在本活动中,我们将使用 Wine 数据集进一步强化困惑度对 t-SNE 可视化过程的影响。在本活动中,我们将尝试根据葡萄酒的化学成分来判断其来源。t-SNE 过程提供了一种有效的手段来表示并可能识别来源。
注意
这个数据集来源于 archive.ics.uci.edu/ml/machine-learning-databases/wine/
(UCI 机器学习库 [archive.ics.uci.edu/ml
])。它可以从 packt.live/3aPOmRJ
下载。
-
导入
pandas
、numpy
和matplotlib
,以及来自 scikit-learn 的t-SNE
和PCA
模型。 -
加载 Wine 数据集并检查前五行数据。
-
第一列提供标签;从 DataFrame 中提取这些标签,并将它们存储在一个单独的变量中。确保从 DataFrame 中删除该列。
-
对数据集执行 PCA,并提取前六个成分。
-
构建一个循环,遍历困惑度值(1、5、20、30、80、160、320)。在每次循环中,生成一个具有相应困惑度的 t-SNE 模型,并绘制标记的葡萄酒类别的散点图。注意不同困惑度值的影响。
在本活动结束时,你将生成 Wine 数据集的二维表示,并检查生成的图形,以查找数据的簇或分组。困惑度值 320 的图形如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_20.jpg
图 6.20:预期输出
注意
该活动的解决方案可以在第 464 页找到。
迭代次数
我们将要实验的最后一个参数是迭代次数,正如我们在自编码器中的研究所示,它实际上就是应用于梯度下降的训练周期数。幸运的是,迭代次数是一个相对简单的参数,通常只需要一定的耐心,因为低维空间中点的位置会稳定在最终位置。
练习 6.03:t-SNE MNIST 与迭代次数
在本练习中,我们将观察不同迭代参数对 t-SNE 模型的影响,并突出显示一些指标,表明可能需要更多的训练。再次强调,这些参数的值高度依赖于数据集和可用于训练的数据量。在这个例子中,我们将使用 MNIST:
-
导入
pickle
、numpy
和matplotlib
,以及从 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 np.random.seed(2)
-
加载 MNIST 数据集:
with open('mnist.pkl', 'rb') as f: mnist = pickle.load(f)
-
使用 PCA,从图像数据中选择前 30 个方差成分:
model_pca = PCA(n_components=30) mnist_pca = model_pca.fit_transform(mnist['images']\ .reshape((-1, 28 ** 2)))
-
在本练习中,我们正在研究迭代对 t-SNE 流形的影响。通过迭代模型/绘图循环,使用迭代进度值
250
、500
和1000
: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)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_21.jpg
图 6.21:遍历模型
-
绘制结果:
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}) plt.show()
迭代次数减少会限制算法找到相关邻居的能力,导致簇的定义不清:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_22.jpg
图 6.22:250 次迭代后的图形
增加迭代次数为算法提供了足够的时间来充分投影数据:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_23.jpg
图 6.23:将迭代次数增加到 500 后的图形
一旦簇稳定下来,增加迭代次数的影响极小,实际上只是增加了训练时间:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_24.jpg
图 6.24:1,000 次迭代后的图形
从之前的图表来看,我们可以看到迭代值为 500 和 1,000 的聚类位置是稳定的,并且在不同的图表之间几乎没有变化。最有趣的图表是迭代值为 250 的那一张,似乎聚类仍处于运动过程中,正朝着最终位置移动。因此,有足够的证据表明迭代值 500 已经足够。
注意
若要访问此特定部分的源代码,请参见packt.live/2Zaw1uZ
你还可以在线运行这个示例,网址为packt.live/3gCOiHf
活动 6.03:t-SNE 酒类数据与迭代
在这个活动中,我们将研究迭代次数对酒类数据集可视化效果的影响。这个过程在数据处理、清理和理解数据关系的探索阶段中非常常见。根据数据集和分析类型,我们可能需要尝试多种不同的迭代次数,就像我们在本活动中将要看的那样。
正如我们之前提到的,这个过程对于将高维数据降维到更低且更易理解的维度非常有帮助。在这个例子中,我们的数据集有 13 个特征;然而,在现实世界中,你可能会遇到具有数百甚至数千个特征的数据集。一个常见的例子是个人级别的数据,它可能包含任何数量的与人口统计或行为相关的特征,这使得常规的现成分析变得不可能。t-SNE 是一个有助于将高维数据转化为更直观状态的工具。
注意
该数据集来源于archive.ics.uci.edu/ml/machine-learning-databases/wine/
(UCI 机器学习库[archive.ics.uci.edu/ml
]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院)。它可以从packt.live/2xXgHXo
下载。
这些步骤将帮助你完成此活动:
-
导入
pandas
、numpy
、matplotlib
,以及来自 scikit-learn 的t-SNE
和PCA
模型。 -
加载酒类数据集并检查前五行。
-
第一列提供了标签;从 DataFrame 中提取这些标签,并将它们存储在一个单独的变量中。确保该列已从 DataFrame 中移除。
-
在数据集上执行 PCA 并提取前六个主成分。
-
构建一个循环,迭代不同的迭代值(
250
,500
,1000
)。对于每次循环,生成一个具有相应迭代次数的 t-SNE 模型,以及一个没有进度值的相同迭代次数。 -
构建一个带有标签的酒类散点图。注意不同迭代值的效果。
完成本活动后,我们将看到修改模型迭代参数的效果。这是一个重要的参数,确保数据在低维空间中已经稳定到一个相对最终的位置。
注意
本活动的解答可以在第 473 页找到。
关于可视化的最终思考
在本章结束时,有几个关于可视化的重要方面需要注意。第一个是聚类的大小或聚类之间的相对空间,可能并不能真正反映它们的接近程度。正如本章前面所讨论的,SNE 通过组合高斯分布和 Student’s t 分布来将高维数据表示在低维空间中。因此,由于 t-SNE 平衡了局部数据结构和全局数据结构的位置,距离之间没有线性关系的保证。在局部结构中的点之间的实际距离,可能在可视化中看起来非常接近,但在高维空间中可能仍然有一定的距离。
这一特性还有其他后果,即有时随机数据可能看起来像是具有某种结构,通常需要使用不同的困惑度、学习率、迭代次数和随机种子值来生成多个可视化图。
总结
本章介绍了 t-分布 SNE(t-distributed SNE)作为一种可视化高维信息的方法,这些信息可能来自于先前的处理过程,例如 PCA 或自动编码器。我们讨论了 t-SNE 如何生成这种表示,并使用 MNIST 和 Wine 数据集以及 scikit-learn 生成了多个可视化结果。在本章中,我们能够看到无监督学习的一些强大之处,因为 PCA 和 t-SNE 能够在不知道真实标签的情况下,将每个图像的类别进行聚类。在下一章中,我们将通过研究无监督学习的应用(包括篮子分析和主题建模)来基于这一实践经验进行进一步探讨。
第八章:7. 主题建模
概述
在这一章中,我们将对文本数据进行基本的清洗技术,然后对清洗后的数据进行建模,以推导出相关主题。你将评估潜在狄利克雷分配(LDA)模型,并执行非负矩阵分解(NMF)模型。最后,你将解释主题模型的结果,并为给定场景识别最佳的主题模型。我们将看到主题建模如何为文档的潜在结构提供洞察力。在本章结束时,你将能够构建完整的主题模型,为你的业务提供价值和洞察。
介绍
在上一章中,讨论重点是使用降维和自编码技术为建模准备数据。大规模特征集在建模时可能会带来问题,因为多重共线性和大量计算可能会阻碍实时预测。使用主成分分析的降维方法是解决这一问题的一种方法。同样,自编码器旨在找到最佳的特征编码。你可以把自编码器看作是识别数据集中优质交互项的一种手段。现在,让我们超越降维,看看一些实际的建模技术。
主题建模是自然语言处理(NLP)的一部分,NLP 是计算机科学领域,研究自然语言的句法和语义分析,随着文本数据集的增加而越来越受欢迎。NLP 几乎可以处理任何形式的语言,包括文本、语音和图像。除了主题建模之外,情感分析、实体识别和对象字符识别也是值得注意的 NLP 应用。
目前,收集和分析的数据不再是以标准表格形式出现,而是更多以不太结构化的形式,如文档、图像和音频文件。因此,成功的数据科学从业者需要精通处理这些多样化数据集的方法。
下面是一个识别文本中的单词并将其分配到主题的示范:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_01.jpg
图 7.1:在文本中识别单词并将其分配到主题的示例
你立刻想到的问题可能是什么是主题? 让我们通过一个例子来回答这个问题。你可以想象,或者可能已经注意到,在发生重大事件的日子里(例如国家选举、自然灾害或体育赛事冠军),社交媒体网站上的帖子通常会集中讨论这些事件。帖子通常以某种方式反映当天的事件,并且会以不同的方式呈现。这些帖子可以并且会有多个不同的观点,这些观点可以被归类为高层次的主题。如果我们有关于世界杯决赛的推文,这些推文的主题可能涵盖从裁判质量到球迷行为的不同观点。在美国,总统会在每年 1 月中下旬发表一次国情咨文演讲。通过足够数量的社交媒体帖子,我们可以通过使用帖子中的特定关键词来对社交媒体社区对演讲的高层次反应(主题)进行推断或预测。主题模型之所以重要,是因为它们在文本数据中起到的作用类似于经典的统计汇总在数值数据中的作用。也就是说,它们提供了数据的有意义总结。让我们回到国情咨文的例子。快速浏览的目标是确认演讲中的主要观点,这些观点要么引起了观众的共鸣,要么被观众忽视。
主题模型
主题模型属于无监督学习范畴,因为几乎总是,所识别的主题在事先是未知的。因此,没有目标可以进行回归或分类建模。从无监督学习的角度来看,主题模型与聚类算法最为相似,特别是 k 均值聚类。你可能还记得,在 k 均值聚类中,首先确定聚类的数量,然后模型将每个数据点分配到预定数量的聚类中。主题模型通常也是如此。我们在开始时选择主题的数量,然后模型会隔离出形成这些主题的词汇。这是进行高层次主题建模概述的一个很好的起点。
在此之前,让我们检查一下是否已安装并准备好使用正确的环境和库。下表列出了所需的库及其主要用途:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_02.jpg
图 7.2:显示不同库及其用途的表格
如果当前未安装这些库中的任何一个或全部,请通过命令行使用pip
安装所需的包;例如,pip install langdetect
。
即将进行的练习的第 3 步涵盖了从nltk
包安装词典。词典只是为特定用途整理的单词集合。下面安装的停用词词典包含了英语中常见的词,这些词无法澄清上下文、含义或意图。这些常见词可能包括the、an、a和in等。WordNet 词典提供了帮助词形还原过程的单词映射——如下所述。这些单词映射将诸如run、running和ran等词汇联系在一起,所有这些词基本上意味着相同的意思。从高层次来看,词典为数据科学家提供了一种准备文本数据以供分析的方式,无需深入了解语言学或花费大量时间定义单词列表或单词映射。
注意
在下面的练习和活动中,由于支持拉普拉斯·狄利希雷分配(Latent Dirichlet Allocation)和非负矩阵分解(Non-negative Matrix Factorization)的优化算法,结果可能会与显示的略有不同。许多函数没有设置种子功能。
练习 7.01:环境设置
为了检查环境是否准备好进行主题建模,我们将执行几个步骤。其中第一步是加载本章所需的所有库:
-
打开一个新的 Jupyter 笔记本。
-
导入所需的库:
import langdetect import matplotlib.pyplot import nltk import numpy import pandas import pyLDAvis import pyLDAvis.sklearn import regex import sklearn
请注意,并非所有这些包都是用于清理数据的;其中一些包是在实际建模过程中使用的。但一次性导入所有需要的库是很有用的,因此我们现在就来处理所有库的导入。
尚未安装的库将返回以下错误:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_03.jpg
图 7.3:未安装库错误
如果返回此错误,请按之前讨论的方式通过命令行安装相关库。安装成功后,使用
import
重新执行库导入过程。 -
某些文本数据清理和预处理过程需要词典。在这里,我们将安装两个这样的词典。如果已导入
nltk
库,请执行以下代码:nltk.download('wordnet') nltk.download('stopwords')
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_04.jpg
图 7.4:导入库和下载词典
-
运行
matplotlib
并指定内联,以便图形可以显示在笔记本中:%matplotlib inline
笔记本和环境现在已经设置好,可以开始加载数据了。
注意
要访问此部分的源代码,请参考packt.live/34gLGKa
。
你也可以在packt.live/3fbWQES
上在线运行这个示例。
必须执行整个笔记本才能获得预期的结果。
主题模型的高层概述
在分析大量潜在相关的文本数据时,主题模型是一个常用的方法。这里所说的“相关”是指文档描述的是相似的主题。运行任何主题模型所需的唯一数据就是文档本身。不需要额外的数据(无论是元数据还是其他数据)。
简单来说,主题模型通过使用文档中的单词,识别出文档集合(称为语料库)中的抽象主题(也称为主题)。也就是说,如果一个句子包含单词薪水、员工和会议,可以合理推测该句子的主题是工作。值得注意的是,构成语料库的文档不必是传统意义上的文档——可以是信件或合同。文档可以是任何包含文本的内容,包括推文、新闻标题或转录的语音。
主题模型假设同一文档中的单词是相关的,并利用这一假设通过寻找反复出现在相近位置的单词群体来定义抽象主题。通过这种方式,这些模型是经典的模式识别算法,其中检测到的模式由单词组成。通用的主题建模算法有四个主要步骤:
-
确定主题的数量。
-
扫描文档并识别共现的单词或短语。
-
自动学习描述文档的单词群体(或聚类)。
-
输出描述语料库的抽象主题,作为单词群体。
正如步骤 1所述,主题的数量需要在拟合模型之前选择。选择合适的主题数量可能有点棘手,但就像大多数机器学习模型一样,可以通过使用不同主题数量拟合多个模型并基于性能指标选择最佳模型来优化此参数。我们稍后会再次深入探讨这个过程。
以下是通用主题建模的工作流:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_05.jpg
图 7.5:通用主题建模工作流
优化主题数量的参数非常重要,因为这个参数会极大影响主题的连贯性。因为模型在预定义的主题数量约束下,找到最适合语料库的单词群体。如果主题数量过高,主题就会变得不适当的狭窄。过于具体的主题称为过度处理。同样,如果主题数量过低,主题就会变得泛化和模糊。这些类型的主题被认为是欠处理。过度处理和欠处理的主题有时可以通过分别减少或增加主题数量来解决。实际上,主题模型的一个常见且不可避免的结果是,通常至少会有一个主题存在问题。
主题模型的一个关键方面是,它们不会生成具体的单词或短语作为主题,而是生成一组词,每个词代表一个抽象主题。回想之前关于工作的假想句子。构建的主题模型旨在识别该句子所属的假设语料库中的主题时,并不会返回工作这个词作为主题。它会返回一组词,例如薪水单、员工和老板——这些词描述了该主题,可以从中推断出单词或短语主题。这是因为主题模型理解词的接近性,而不是上下文。模型并不知道薪水单、员工和老板的含义,它只知道这些词通常在出现时,彼此之间会出现在接近的位置:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_06.jpg
图 7.6:从词组推断主题
主题模型可以用来预测未知文档所属的主题,但如果你打算进行预测,重要的是要认识到,主题模型只知道用来训练它们的词汇。也就是说,如果未知文档中有训练数据中没有的词,模型将无法处理这些词,即使它们与训练数据中已识别的某个主题相关联。由于这一事实,主题模型往往更多地用于探索性分析和推理,而非预测。
每个主题模型会输出两个矩阵。第一个矩阵包含了词和主题的关系。它列出了与每个主题相关的每个词,并对关系进行了量化。鉴于模型所考虑的词汇数量,每个主题只会用相对较少的词来描述。
词语可以被分配到一个主题,也可以分配到多个主题,并赋予不同的量化值。词语是否被分配到一个或多个主题取决于算法。类似地,第二个矩阵包含了文档和主题的关系。它通过量化文档与主题组合的关系,将每个文档映射到每个主题。
在讨论主题建模时,必须不断强调这样一个事实:代表主题的词组在概念上并不相关,它们仅仅是通过接近性相关。文档中某些词的频繁接近足以定义主题,因为之前提到的假设——同一文档中的所有词都是相关的。
然而,这个假设可能并不成立,或者这些词可能过于通用,无法形成连贯的主题。解释抽象主题涉及平衡文本数据的内在特性与生成的词组。文本数据和语言通常具有高度的变异性、复杂性和上下文性,这意味着任何泛化的结果都需要谨慎对待。
这并不是贬低或无效化模型的结果。在彻底清洗过的文档和适当数量的主题下,词汇组(正如我们将看到的)可以很好地指引我们理解语料库中的内容,并能有效地纳入更大的数据系统。
我们已经讨论了一些主题模型的局限性,但仍有一些额外的要点需要考虑。文本数据的噪声特性可能导致主题模型将与某个主题无关的词语错误地分配到该主题。
再次考虑之前关于工作的句子。词语meeting可能会出现在表示工作主题的词汇组中。也有可能词语long出现在这个组中,但long并不直接与工作相关。Long之所以出现在该组中,可能是因为它经常与词语meeting相近。因此,long可能被错误地(或虚假地)认为与工作相关,并且应该尽可能从主题词汇组中移除。词汇组中的虚假相关词语可能会在分析数据时造成重大问题。
这不一定是模型的缺陷。相反,这是一个特性,考虑到数据中的噪声,模型可能会从数据中提取出一些特殊性,这可能会对结果产生负面影响。虚假的相关性可能是由于数据的收集方式、地点或时间所导致的。如果文档仅在某个特定的地理区域收集,那么与该区域相关的词语可能会不正确地(尽管是偶然的)与模型输出的一个或多个词汇组关联起来。
请注意,随着词汇组中词语的增加,我们可能会将更多文档错误地附加到该主题。应该很容易理解的是,如果我们减少属于某个主题的词语数量,那么该主题将会被分配到更少的文档中。请记住,这并不是坏事。我们希望每个词汇组只包含那些合适的词语,以便将适当的主题分配给适当的文档。
有许多主题建模算法,但也许最著名的两种是潜在狄利克雷分配(LDA)和非负矩阵分解(NMF)。我们稍后会详细讨论这两种方法。
商业应用
尽管存在一些局限性,主题建模仍然可以提供有助于推动商业价值的可操作性洞察,如果正确使用并在合适的情境下应用。现在,让我们回顾一下主题模型的一些最大应用。
其中一个使用场景是在处理新文本数据时进行探索性数据分析,这些数据集的潜在结构尚不清楚。这相当于为一个未见过的数据集绘制图表并计算摘要统计量,其中包括需要理解其特征的数值和分类变量,在进一步的复杂分析能够合理进行之前。这些主题建模的结果可以帮助我们评估该数据集在未来建模工作中的可用性。例如,如果主题模型返回清晰且明确的主题,那么该数据集将是进一步进行聚类类分析的理想候选者。
确定主题会创建一个额外的变量,可以用来对数据进行排序、分类和/或分块。如果我们的主题模型返回“汽车”、“农业”和“电子产品”作为抽象主题,我们可以将大规模文本数据集筛选至仅包含“农业”作为主题的文档。筛选后,我们可以进行进一步的分析,包括情感分析、再一次的主题建模,或任何我们能想到的其他分析。除了定义语料库中存在的主题外,主题建模还间接返回了许多其他信息,这些信息可以用来进一步分解大型数据集并理解其特征。
其中一个特征是主题的普遍性。想象一下,在一个旨在衡量对某个产品反应的开放式问卷调查中进行分析。我们可以设想,主题模型返回的主题形式为情感。一组词可能是好、优秀、推荐和质量,而另一组则可能是垃圾、坏掉、差劲和失望。
鉴于这种调查方式,主题本身可能并不令人惊讶,但有趣的是,我们可以统计包含每个主题的文档数量,并从中获取有用的见解。通过这些统计数据,我们可以得出这样的结论:例如,x% 的调查参与者对产品持积极反应,而只有 y% 的参与者持消极反应。实际上,我们所做的就是创建了一个粗略版本的情感分析。
当前,主题模型最常见的用途是作为推荐引擎的一部分。今天的重点是个性化——向消费者提供专门为他们设计和策划的产品。以网站为例,无论是新闻网站还是其他,致力于传播文章的公司,例如雅虎和 Medium,需要让客户继续阅读才能保持运营。保持客户阅读的一种方式是向他们推送他们更有可能阅读的文章。这就是主题建模的作用所在。通过使用由个体之前阅读的文章组成的语料库,主题模型基本上可以告诉我们该订阅者喜欢阅读哪些类型的文章。然后,公司可以访问其库存,找到具有相似主题的文章,并通过该用户的账户页面或电子邮件将它们发送给该用户。这是为了简化使用并保持用户参与的定制策划。
在我们开始准备数据以供模型使用之前,让我们快速加载并探索数据。
练习 7.02: 数据加载
在本练习中,我们将加载并格式化数据。我们将在与练习 7.01,设置环境相同的笔记本中执行此练习。尽可能彻底地理解我们将要处理的数据集非常重要。理解的过程从了解数据的大致样貌、数据的大小、存在的列以及识别哪些数据集的方面可能对解决我们要解决的问题有帮助开始。我们将在下面回答这些基本问题。
注意
该数据集来源于archive.ics.uci.edu/ml/datasets/News+Popularity+in+Multiple+Social+Media+Platforms
(UCI 机器学习库[archive.ics.uci.edu/ml
]。加利福尼亚大学欧文分校信息与计算机科学学院)。
引用:Nuno Moniz 和 Luís Torgo. “在线新闻源的多源社交反馈”.CoRR [arXiv:1801.07055 [cs.SI]] (2018)。
数据集也可以从packt.live/2Xin2HC
下载。
这是本练习所需的唯一文件。下载并保存到本地后,数据可以加载到笔记本中。
-
定义数据路径并使用
pandas
加载数据:path = "News_Final.csv" df = pandas.read_csv(path, header=0)
注意
将文件添加到与您打开笔记本的相同文件夹中。
-
执行以下代码简要检查数据:
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/unspr-lrn-ws/img/B15923_07_07.jpg
图 7.7: 原始数据
从特征上讲,这是一个比运行主题模型所需的更大的数据集。
-
请注意,其中一列名为
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
-
现在,提取标题数据并将提取的数据转换为列表对象。打印列表的前五个元素以及列表的长度,以确认提取是否成功:
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/unspr-lrn-ws/img/B15923_07_08.jpg
图 7.8:标题列表
现在数据已加载并正确格式化,我们来谈谈文本数据清洗,然后进行一些实际的清洗和预处理。出于教学目的,清洗过程最初将在单个标题上进行构建和执行。一旦我们建立了清洗过程并在示例标题上进行了测试,我们将返回并对每个标题运行该过程。
注意
要访问此特定部分的源代码,请参见packt.live/34gLGKa
。
你也可以在网上运行这个示例,访问packt.live/3fbWQES
。
你必须执行整个 Notebook 才能得到期望的结果。
清洗文本数据
所有成功建模练习的一个关键组成部分是一个经过适当和充分预处理的干净数据集,专门为特定的数据类型和分析任务进行预处理。文本数据也不例外,因为它在原始形式下几乎无法使用。无论运行什么算法:如果数据没有经过适当准备,结果最好的情况下是没有意义的,最坏的情况下是误导性的。正如谚语所说,垃圾进,垃圾出。对于主题建模,数据清洗的目标是通过去除所有可能干扰的内容,来孤立每个文档中可能相关的词汇。
数据清洗和预处理几乎总是特定于数据集的,这意味着每个数据集都需要一组独特的清洗和预处理步骤,专门用于处理其中的问题。对于文本数据,清洗和预处理步骤可能包括语言过滤、移除网址和屏幕名称、词形还原以及停用词移除等。我们将在接下来的章节中详细探讨这些步骤,并在即将进行的练习中实施这些思想,届时一个包含新闻标题的数据集将被清理用于主题建模。
数据清洗技术
重申一下之前的观点,清洗文本以进行主题建模的目标是从每个文档中提取可能与发现语料库抽象主题相关的单词。这意味着需要去除常见词、短词(通常更常见)、数字和标点符号。清洗数据没有固定的流程,因此理解所清洗数据类型中的典型问题点并进行广泛的探索性工作非常重要。
现在,让我们讨论一些我们将在数据清洗中使用的文本清洗技巧。进行任何涉及文本的建模任务时,首先需要做的事情之一是确定文本的语言。在这个数据集中,大多数标题是英文的,因此为了简便起见,我们将删除非英文的标题。构建非英文文本数据的模型需要额外的技能,其中最基本的是对所建模语言的流利掌握。
数据清洗的下一个关键步骤是移除文档中所有与基于单词的模型无关的元素,或者可能成为噪声来源、掩盖结果的元素。需要移除的元素可能包括网站地址、标点符号、数字和停用词。停用词基本上是一些简单的、常用的词(包括we、are和the)。需要注意的是,并没有一个权威的停用词词典;每个词典略有不同。尽管如此,每个词典都包含一些常见词,这些词被认为与主题无关。主题模型试图识别那些既频繁又不那么频繁的词,这些词足以描述一个抽象的主题。
移除网站地址有类似的动机。特定的网站地址出现的频率非常低,但即使某个特定网站地址足够多次出现在文档中并且能与某个主题相关联,网站地址的解释方式却不同于单词。去除文档中无关的信息,可以减少那些可能妨碍模型收敛或掩盖结果的噪声。
词形还原,像语言检测一样,是所有涉及文本的建模活动中的一个重要组成部分。它是将单词还原为其基本形式的过程,目的是将应该相同但因时态或词性变化而不同的单词归为一类。考虑单词running、runs和ran。这三个单词的基本形式是run。词形还原的一个很好的方面是,它会查看句子中的所有单词(换句话说,它会考虑上下文),然后决定如何改变每个单词。词形还原,像大多数前述的清洗技巧一样,简单地减少了数据中的噪声,使我们能够识别出干净且易于解释的主题。
现在,拥有了基本的文本清洗技巧知识,让我们将这些技巧应用于实际数据中。
练习 7.03:逐步清洗数据
在本练习中,我们将学习如何实现一些清理文本数据的关键技术。每个技术将在我们进行练习时进行解释。每一步清理后,都会使用print
输出示例标题,以便我们观察从原始数据到模型数据的演变:
-
选择第六个标题作为我们构建并测试清理过程的示例。第六个标题并不是随机选择的,它是因为包含了在清理过程中将要处理的特定问题:
example = raw[5] print(example)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_09.jpg
图 7.9:第六个标题
-
使用
langdetect
库来检测每个标题的语言。如果语言不是英语(en
),则从数据集中删除该标题。detect
函数仅仅是检测传入文本的语言。当该函数无法检测出语言时(偶尔会发生),只需将语言设置为none
,以便稍后删除: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)))
输出如下:
DETECTED LANGUAGE: en
-
使用空格将包含标题的字符串拆分成片段,称为标记。返回的对象是由构成标题的单词和数字组成的列表。将标题字符串拆分成标记,使清理和预处理过程更加简单。市场上有多种类型的标记器。请注意,NLTK 本身提供了多种类型的标记器。每个标记器考虑了将句子拆分成标记的不同方式。最简单的一种是基于空格拆分文本。
example = example.split(" ") print(example)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_10.jpg
图 7.10:使用空格拆分字符串
-
使用正则表达式搜索包含
http://
或https://
的标记来识别所有 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/unspr-lrn-ws/img/B15923_07_11.jpg
图 7.11:将 URL 替换为 URL 字符串
-
使用正则表达式将所有标点符号和换行符号(
\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/unspr-lrn-ws/img/B15923_07_12.jpg
图 7.12:使用正则表达式将标点符号替换为空字符串
-
使用正则表达式将所有数字替换为空字符串:
example = [regex.sub("^[0-9]*$", "", i) for i in example] print(example)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_13.jpg
图 7.13:将数字替换为空字符串
-
将所有大写字母转换为小写字母。虽然将所有内容转换为小写字母不是强制步骤,但它有助于简化复杂性。将所有内容转换为小写字母后,跟踪的内容较少,因此出错的机会也较小:
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/unspr-lrn-ws/img/B15923_07_14.jpg
图 7.14:将大写字母转换为小写字母
-
移除在 步骤 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/unspr-lrn-ws/img/B15923_07_15.jpg
图 7.15:字符串 URL 已移除
-
从
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/unspr-lrn-ws/img/B15923_07_16.jpg
图 7.16:停用词列表
在使用字典之前,重要的是要重新格式化单词,使其与我们标题的格式匹配。这包括确认所有内容都是小写且没有标点符号。
-
现在我们已经正确格式化了
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/unspr-lrn-ws/img/B15923_07_17.jpg
图 7.17:从标题中移除停用词
-
通过定义一个可以应用于每个标题的函数来执行词形还原。词形还原需要加载
wordnet
字典。morphy
函数会处理文本中的每个单词,并返回其标准形式(如果识别到的话)。例如,如果输入的单词是 running 或 ran,morphy
函数将返回 run: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/unspr-lrn-ws/img/B15923_07_18.jpg
图 7.18:执行词形还原后的输出
-
从词元列表中移除所有长度为四个字符或更少的单词。这个步骤的假设是,短单词通常更为常见,因此不会为我们从主题模型中提取的洞察提供帮助。请注意,移除某些长度的单词并不是一种适用于所有情况的技巧;它仅适用于特定情况。例如,短单词有时可能非常指示某些主题,如识别动物(例如:dog,cat,bird)。
example = [i for i in example if len(i) >= 5] print(example)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_19.jpg
图 7.19:第六个标题清理后的结果
现在我们已经逐一完成了清理和预处理步骤,接下来需要将这些步骤应用到接近 100,000 个标题上。最有效的方法是编写一个包含上述所有步骤的函数,并以某种迭代方式将该函数应用于语料库中的每个文档。这个过程将在下一个练习中进行。
注意
要访问此特定部分的源代码,请参考 packt.live/34gLGKa
。
您还可以在packt.live/3fbWQES
在线运行此示例。
您必须执行整个 Notebook 才能获得所需的结果。
练习 7.04:完整数据清洗
在本次练习中,我们将把步骤 2到步骤 12从练习 7.03《逐步清洗数据》整合为一个函数,应用于每个标题。该函数将以字符串格式的标题作为输入,输出将是一个清洗后的标题列表(tokens)。主题模型要求文档格式为字符串,而不是 tokens 的列表,因此在步骤 4中,tokens 列表将被转换回字符串:
-
定义一个函数,包含练习 7.03《逐步清洗数据》中的所有独立步骤:
Exercise7.01-Exercise7.12.ipynb 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] The complete code for this step can be found at https://packt.live/34gLGKa.
-
在每个标题上执行该函数。Python 中的
map
函数是一种很好的方式,可以将用户定义的函数应用于列表中的每个元素。将map
对象转换为列表,并将其分配给clean
变量。clean
变量是一个列表的列表:tick = time() clean = list(map(do_headline_cleaning, raw)) print(time()-tick)
-
在
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/unspr-lrn-ws/img/B15923_07_20.jpg
图 7.20:示例标题及标题列表的长度
-
对于每个单独的标题,使用空格分隔符连接 tokens。现在这些标题将变成一个无结构的单词集合,对于人类阅读者来说没有意义,但对于主题建模来说是理想的:
clean_sentences = [" ".join(i) for i in clean] print(clean_sentences[0:10])
清洗后的标题应类似于以下内容:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_21.jpg
图 7.21:为建模清洗后的标题
注意
要访问此特定部分的源代码,请参考packt.live/34gLGKa
。
您还可以在packt.live/3fbWQES
在线运行此示例。
您必须执行整个 Notebook 才能获得所需的结果。
总结一下,清洗和预处理的工作实际上是剔除数据中的噪音,以便模型能够专注于数据中可能推动洞察的元素。例如,任何与主题无关的词语不应影响主题,但如果不小心留下这些词,它们可能会干扰。
为了避免我们可以称之为假信号的内容,我们会移除这些词语。同样,由于主题模型无法辨别上下文,标点符号是无关的,因此会被移除。即便模型可以在不清洗数据的情况下找到主题,未经清洗的数据可能包含成千上万甚至百万个多余的单词和随机字符(取决于语料库中的文档数量),这可能显著增加计算需求。因此,数据清洗是主题建模的一个重要部分。你将在接下来的活动中练习这个过程。
活动 7.01:加载和清洗 Twitter 数据
在本活动中,我们将加载并清洗 Twitter 数据,以便在后续活动中进行建模。我们对头条数据的使用是持续进行的,因此让我们在一个单独的 Jupyter 笔记本中完成此活动,但所有的要求和导入的库保持一致。
目标是处理原始推文数据,清洗它,并生成与前一个练习中第 4 步相同的输出。输出应该是一个列表,其长度应该与原始数据文件中的行数相似,但可能不完全相同。这是因为推文在清洗过程中可能会被丢弃,原因可能有很多,比如推文使用了非英语语言。列表中的每个元素应该代表一条推文,并且只包含可能与主题形成相关的推文内容。
以下是完成此活动的步骤:
-
导入必要的库。
-
从
packt.live/2Xje5xF
加载 LA Times 健康 Twitter 数据(latimeshealth.txt
)。注意
数据集来源于
archive.ics.uci.edu/ml/datasets/Health+News+in+Twitter
(UCI 机器学习库[archive.ics.uci.edu/ml
]。加利福尼亚大学欧文分校信息与计算机科学学院)。引用:Karami, A., Gangopadhyay, A., Zhou, B., & Kharrazi, H.(2017)。健康和医学语料库中的模糊方法主题发现。《国际模糊系统杂志》,1-12。
它也可以在 GitHub 上找到,
packt.live/2Xje5xF
。 -
进行快速的探索性分析,确定数据的大小和结构。
-
提取推文文本并将其转换为列表对象。
-
编写一个函数,执行语言检测和基于空格的分词,然后分别用
SCREENNAME
和URL
替换屏幕名称和网址。该函数还应移除标点符号、数字以及SCREENNAME
和URL
的替换内容。将所有内容转换为小写字母,除了SCREENNAME
和URL
。它应移除所有停用词,执行词形还原,并且只保留长度为五个字母或以上的单词。 -
将第 5 步中定义的函数应用于每一条推文。
-
移除输出列表中值为
None
的元素。 -
将每条推文的元素重新转换为字符串。使用空格进行连接。
-
保持笔记本打开,以便进行未来的活动。
注意
本章中的所有活动需要在同一个笔记本中执行。
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_22.jpg
图 7.22:已清洗的推文,用于建模
注意
本活动的解决方案可以在第 478 页找到。
潜在狄利克雷分配(Latent Dirichlet Allocation)
2003 年,David Blei、Andrew Ng 和 Michael Jordan 发表了他们关于主题建模算法潜在狄利克雷分配(LDA)的文章。LDA 是一种生成概率模型。这意味着建模过程从文本开始,反向工作,通过假设生成它的过程,以识别感兴趣的参数。在这种情况下,感兴趣的是生成数据的主题。这里讨论的过程是 LDA 的最基本形式,但对于学习来说,它也是最容易理解的。
语料库中有 M 个可用于主题建模的文档。每个文档可以视为N个单词的序列,即序列(w1,w2… wN)。
对于语料库中的每个文档,假设的生成过程是:
-
选择https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_01.png,其中N是单词的数量,λ是控制泊松分布的参数。
-
选择https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_02.png,其中https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_03.png是主题的分布。
-
对于每个N个单词,Wn,选择主题https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_04.png,并从中选择单词Wn,来自https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_05.png。
让我们更详细地了解一下生成过程。前面提到的三个步骤会对语料库中的每个文档重复。初始步骤是通过从大多数情况下的泊松分布中采样来选择文档中的单词数。需要注意的是,由于 N 与其他变量是独立的,因此与其生成相关的随机性在算法推导中大多被忽略。
在选择N之后,接下来是生成主题混合或主题分布,这对每个文档来说是独特的。可以将其视为每个文档的主题列表,概率表示每个主题所代表的文档部分。考虑三个主题:A、B 和 C。一个示例文档可能是 100%的主题 A,75%的主题 B 和 25%的主题 C,或者是无数其他的组合。
最后,文档中的特定单词是通过概率语句从所选主题及该主题的单词分布中选择的。请注意,文档并不真正以这种方式生成,但它是一个合理的代理方法。
这个过程可以被看作是一个分布上的分布。一个文档从文档集合(分布)中选择出来,然后从该文档的主题概率分布中选择一个主题(通过多项式分布),该分布由 Dirichlet 分布生成。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_23.jpg
图 7.23:LDA 的图形表示
构建表示 LDA 解法的公式最直接的方法是通过图形表示。这个特定的表示方法被称为板符号图形模型,因为它使用板块来表示过程中的两个迭代步骤。
你会记得生成过程是针对语料库中的每个文档执行的,因此最外层的板块(标记为M)表示对每个文档的迭代。类似地,步骤 3中对词汇的迭代通过图中的最内层板块表示,标记为N。
圆圈代表参数、分布和结果。阴影部分的圆圈,标记为w,是选定的词汇,这是唯一已知的数据,因此用于反向推导生成过程。除了w,图中的其他四个变量定义如下:
-
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_06.png:主题-文档 Dirichlet 分布的超参数。
-
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_09.png:这是每个文档主题分布的潜在变量。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_10.png 和https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_11.png 控制文档中主题的频率和主题中词汇的频率。如果https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_12.png 增加时,文档变得越来越相似,因为每个文档中的主题数量增加。另一方面,如果https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_13.png 减少时,文档之间的相似度逐渐降低,因为每个文档中的主题数量减少。 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_14.png 参数表现类似。如果https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_15.png 增加时,文档中使用的词汇更多,用来建模一个主题,而较低的https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_16.png导致每个主题所使用的词汇数量较少。鉴于 LDA 中分布的复杂性,没有直接的解决方案,因此需要某种近似算法来生成结果。LDA 的标准近似算法将在下一节中讨论。
变分推断
LDA 的一个主要问题是条件概率(分布)的评估难以管理,因此,概率不是直接计算,而是通过近似来得到。变分推断是其中一种较为简单的近似算法,但它有一个广泛的推导过程,需要对概率有深入的理解。为了更多地关注 LDA 的应用,本节将简要介绍变分推断在该背景下的应用,但不会深入探讨该算法。
让我们花点时间直观地理解变分推断算法。从随机地将语料库中每篇文档中的每个单词分配到一个主题开始。然后,分别为每个文档和每个文档中的每个单词计算两个比例。这些比例分别是当前分配给该主题的文档中单词的比例,P(Topic|Document),以及特定单词在所有文档中分配到该主题的比例,P(Word|Topic)。将这两个比例相乘,使用得到的比例将该单词分配到一个新的主题。重复这个过程,直到达到一个稳定状态,在这个状态下,主题分配不会发生显著变化。然后,使用这些分配来估计文档内部的主题混合和主题内的单词混合。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_24.jpg
图 7.24:变分推断过程
变分推断的思路是,如果实际分布是不可处理的,那么应找到一个更简单的分布,称为变分分布,它非常接近真实分布且是可处理的,从而使得推断成为可能。换句话说,由于由于实际分布的复杂性,推断实际分布是不可能的,我们试图找到一个更简单的分布,它能够很好地近似实际分布。
让我们暂时从理论中休息一下,来看一个例子。变分推断就像是在拥挤的动物园中观察动物。动物园里的动物处于一个封闭的栖息地,在这个例子中,栖息地就是后验分布。游客无法实际进入栖息地,因此他们必须尽可能靠近栖息地观察,这就是后验近似(即栖息地的最佳近似)。如果动物园里有很多人,可能很难找到那个最佳的观察点。人们通常从人群的后面开始,逐步朝着最佳观察点移动。游客从人群后面移动到最佳观察点的路径就是优化路径。变分推断实际上就是在知道无法真正到达期望点的情况下,尽可能接近期望点的过程。
首先,选择一个分布族(即二项分布、高斯分布、指数分布等),q,并根据新的变分参数进行条件化。这些参数经过优化,使得原始分布(实际上是后验分布,对于熟悉贝叶斯统计的人来说)和变分分布尽可能接近。变分分布会足够接近原始的后验分布,因此可以作为代理,基于它进行的任何推断都适用于原始后验分布。分布族 q 的通用公式如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_25.jpg
图 7.25:分布族的公式,q
有一大堆潜在的变分分布可以用作后验分布的近似。从这些分布中选择一个初始的变分分布,作为优化过程的起点,该过程会不断接近最佳分布。最佳参数是指最能近似后验分布的分布参数。使用Kullback-Leibler(KL)散度来衡量这两个分布的相似性。KL 散度表示如果我们用一个分布来近似另一个分布时,所产生的预期误差量。具有最佳参数的分布将具有最小的 KL 散度,并且与真实分布相比。
一旦确定了最佳分布,也就意味着确定了最佳参数,可以利用它来生成输出矩阵并执行任何需要的推断。
词袋模型
文本不能直接传递给任何机器学习算法;它首先需要被数值编码。在机器学习中处理文本的一个直接方法是使用词袋模型,它移除了关于单词顺序的所有信息,专注于每个单词的出现程度(即计数或频率)。
Python 的sklearn
库可以用来将前一个练习中创建的清洗后的向量转换为 LDA 模型所需的结构。由于 LDA 是一个概率模型,我们不希望对单词出现频率进行任何缩放或加权;相反,我们选择仅输入原始计数。
词袋模型的输入将是练习 7.04中返回的清洗字符串列表,即完整数据清理。输出将是文档编号、单词的数值编码以及该单词在文档中出现的次数。这三个项目将以元组和整数的形式呈现。
元组将类似于(0, 325),其中 0 是文档编号,325 是数值编码的单词。请注意,325 将是该单词在所有文档中的编码。整数部分将是计数。我们将在本章中运行的词袋模型来自sklearn
,分别称为CountVectorizer
和TfIdfVectorizer
。第一个模型返回原始计数,第二个返回一个缩放值,我们稍后将讨论这一点。
一个重要的注意事项是,本章涵盖的两种主题模型的结果可能会因运行而异,即使数据相同,这也是由于随机性所导致。LDA 中的概率和优化算法都不是确定性的,因此不要惊讶于你的结果与接下来展示的结果有所不同。在下一个练习中,我们将运行计数向量化器,以数值方式编码我们的文档,以便能够继续使用 LDA 进行主题建模。
练习 7.05:使用计数向量化器创建词袋模型
在这个练习中,我们将运行sklearn
中的CountVectorizer
,将之前创建的清洗后的标题向量转换为词袋数据结构。此外,我们还将定义一些将在建模过程中使用的变量:
-
定义
number_words
、number_docs
和number_features
。前两个变量控制 LDA 结果的可视化。number_features
变量控制将在特征空间中保留的词汇数量:number_words = 10 number_docs = 10 number_features = 1000
-
运行计数向量化器并打印输出。这里有三个关键输入参数:
max_df
、min_df
和max_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/unspr-lrn-ws/img/B15923_07_26.jpg
图 7.26:词袋数据结构
-
从向量化器中提取特征名称和单词。模型只接收单词的数值编码,因此将特征名称向量与结果合并,将使得解释过程更加容易:
feature_names_vec1 = vectorizer1.get_feature_names()
这个练习涉及文档的枚举,用于 LDA 模型。所需的格式是词袋模型。也就是说,词袋模型仅仅是列出每个文档中出现的所有词汇,并计算每个词在每个文档中出现的次数。通过使用sklearn
完成这一任务后,接下来是探索 LDA 模型评估过程。
注意
要访问此特定部分的源代码,请参考packt.live/34gLGKa
。
你还可以在网上运行此示例,访问packt.live/3fbWQES
。
你必须执行整个 Notebook 才能获得所需的结果。
困惑度
模型通常具有可用于评估其性能的指标。主题模型也不例外,尽管在这种情况下,性能的定义稍有不同。在回归和分类中,预测值可以与实际值进行比较,从中可以计算出明确的性能度量。
对于主题模型,预测的可靠性较低,因为模型仅了解它所训练过的词汇,而新文档可能并未包含这些词汇,尽管它们可能包含相同的主题。由于这一差异,主题模型的评估使用了专门针对语言模型的度量指标,称为困惑度。
困惑度(Perplexity,缩写为 PP)衡量的是在任何给定词语后平均可以跟随的不同且同样最可能的词汇数量。我们以两个词为例:the和announce。词the可以引出大量同样最可能的词汇,而词announce后面可以跟随的同样最可能的词汇数量则明显较少——尽管仍然是一个很大的数字。
其思想是,平均而言,后面能够跟随更少数量的同样最可能出现的单词的单词,越具体,越能够紧密地与主题联系。因此,较低的困惑度分数意味着更好的语言模型。困惑度与熵非常相似,但通常使用困惑度,因为它更容易解释。正如我们稍后将看到的,它可以用于选择最佳的主题数量。假设m是单词序列中的单词数,困惑度定义为:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_27.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_27.jpg)
图 7.27:困惑度公式
在这个公式中,w1*,…,wm 是构成测试数据集中某文档的单词。这些单词的联合概率,P(w1,…,wm),衡量了测试文档与现有模型的契合度。较高的概率意味着模型更强。概率会被提升到*-1/m*的幂,以根据每个文档中的单词数量对分数进行标准化,并使较低的值更优。两者的变化都增加了分数的可解释性。困惑度分数,类似于均方根误差,作为单独的指标意义不大。它通常作为一个比较指标使用。即,构建几个模型,计算它们的困惑度分数并进行比较,以确定最佳的模型,从而继续前进。
如前所述,LDA 有两个必需的输入。第一个是文档本身,第二个是主题数量。选择合适的主题数量可能非常棘手。找到最佳主题数量的一种方法是对多个主题数量进行搜索,并选择与最小困惑度分数对应的主题数量。在机器学习中,这种方法被称为网格搜索。接下来的练习中,我们将使用网格搜索来找到最佳主题数量。
练习 7.06:选择主题数量
在本练习中,我们使用适配不同主题数量的 LDA 模型的困惑度分数来确定应该继续使用的主题数量。请记住,原始数据集中的标题已经被分成了四个主题。让我们看看这种方法是否能得到四个主题:
-
定义一个函数,适配不同主题数量的 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)
-
执行在步骤 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/unspr-lrn-ws/img/B15923_07_28.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_28.jpg)
图 7.28:包含主题数量和困惑度分数的数据框
-
绘制困惑度分数作为主题数的函数。这只是查看 步骤 2 中 DataFrame 中结果的另一种方式:
df_perplexity.plot.line("Number Of Topics", "Perplexity Score")
绘图结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_29.jpg
图 7.29:主题数与困惑度的线图视图
正如 DataFrame 和绘图所示,使用困惑度得出的最佳主题数为三。将主题数设为四产生了第二低的困惑度。因此,虽然结果与原始数据集中包含的信息并不完全匹配,但这些结果足以让我们对网格搜索方法识别最佳主题数感到满意。关于为何网格搜索返回三个而不是四个结果,我们将在即将进行的练习中深入探讨。
注
要访问本节的源代码,请参阅 packt.live/34gLGKa
。
您也可以在 packt.live/3fbWQES
上线运行此示例。
您必须执行整个笔记本才能获得所需的结果。
现在我们已经选择了最佳主题数,将使用该主题数构建我们的官方 LDA 模型。然后,该模型将用于创建可视化效果,并定义语料库中存在的主题列表。
练习 7.07:运行 LDA
在本练习中,我们将实施 LDA 并检查结果。LDA 输出两个矩阵。第一个是主题-文档矩阵,第二个是词-主题矩阵。我们将查看这些矩阵,这些矩阵是模型返回的,并且格式化为更易于理解的表格:
-
使用在 练习 7.06,选择主题数 中找到的最佳主题数拟合 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/unspr-lrn-ws/img/B15923_07_30.jpg
图 7.30:LDA 模型
-
输出主题-文档矩阵及其形状,以确认其与主题数和文档数的对齐情况。矩阵的每一行是主题的文档分布:
lda_transform = lda.transform(clean_vec1) print(lda_transform.shape) print(lda_transform)
输出如下:
(92946, 3) [[0.04761958 0.90419577 0.04818465] [0.04258906 0.04751535 0.90989559] [0.16656181 0.04309434 0.79034385] ... [0.0399815 0.51492894 0.44508955] [0.06918206 0.86099065 0.06982729] [0.48210053 0.30502833 0.21287114]]
-
输出词-主题矩阵及其形状,以确认其与 练习 7.05,使用计数向量化器创建词袋模型 中指定的特征数(词)和输入的主题数的对齐情况。每一行基本上是每个单词分配给该主题的流行度。流行度分数可以转换为每个主题的词分布:
lda_components = lda.components_ print(lda_components.shape) print(lda_components)
输出如下:
(3, 1000) [[3.35570079e-01 1.98879573e+02 9.82489014e+00 ... 3.35388004e-01 2.04173562e+02 4.03130268e-01] [2.74824227e+02 3.94662558e-01 3.63412044e-01 ... 3.45944379e-01 1.77517291e+02 4.61625408e+02] [3.37041234e-01 7.36749100e+01 2.05707096e+02 ... 2.31714093e+02 1.21765267e+02 7.71397922e-01]]
-
定义一个函数,将两个输出矩阵格式化为易于阅读的表格:
Exercise7.01-Exercise7.12.ipynb 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 = {} The complete code for this step can be found at https://packt.live/34gLGKa.
该函数可能有些难以操作,所以让我们一起逐步分析。首先创建W和H矩阵,包括将W的分配计数转换为每个主题的词汇分布。然后,遍历每个主题。在每次遍历中,识别与每个主题相关的前几个词汇和文档。最后,将结果转换为两个数据框。
-
执行在步骤 4中定义的函数:
W_df, H_df = get_topics(mod=lda, \ vec=clean_vec1, \ names=feature_names_vec1, \ docs=raw, \ ndocs=number_docs, \ nwords=number_words)
-
打印出词汇-主题数据框。它展示了与每个主题相关的前 10 个词汇(按分布值排序)。通过这个数据框,我们可以识别出词汇分组所代表的抽象主题。关于抽象主题的更多内容将在后续介绍:
print(W_df)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_31.jpg
图 7.31:词汇-主题表
-
打印出主题-文档数据框。这显示了与每个主题最密切相关的 10 篇文档。其值来自每篇文档的主题分布:
print(H_df)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_32.jpg
图 7.32:主题-文档表
词汇-主题数据框的结果显示,抽象主题包括巴拉克·奥巴马、经济和微软。有趣的是,描述经济的词汇分组中包含了对巴勒斯坦的提及。原始数据集中的四个主题都在词汇-主题数据框的输出中得到了体现,但并没有以预期的完全独立的方式展现出来。我们可能面临两种问题。
首先,引用经济和巴勒斯坦的主题可能还不够成熟,这意味着增加主题的数量可能会解决这个问题。另一个潜在的问题是 LDA 在处理相关主题时效果不佳。在练习 7.09中,尝试四个主题,我们将尝试扩展主题的数量,这将帮助我们更好地理解为什么其中一个词汇分组似乎是多个主题的混合。
注意
要访问此特定部分的源代码,请参考packt.live/34gLGKa
。
你也可以在线运行这个示例,访问packt.live/3fbWQES
。
你必须执行整个 Notebook 才能得到预期的结果。
可视化
使用sklearn
的 LDA 模型在 Python 中的输出可能难以直接解读。与大多数建模工作一样,数据可视化在解读和传达模型结果时有很大帮助。一种 Python 库pyLDAvis
直接与sklearn
模型对象集成,生成直观的图形。这个可视化工具返回一个直方图,展示与每个主题最紧密相关的词汇,以及一个双变量图(PCA 中常用),每个圆圈代表一个主题。通过双变量图,我们可以了解每个主题在整个语料库中的普遍性,这通过圆圈的面积来反映,以及主题之间的相似性,这通过圆圈的接近程度来体现。
理想的情况是图中的圆圈应均匀分布,且大小合理一致。也就是说,我们希望主题清晰区分,并在语料库中均匀分布。除了 pyLDAvis
图形外,我们还将利用前一章节讨论的 t-SNE 模型,生成主题-文档矩阵的二维表示,这个矩阵的每一行表示一个文档,每一列表示该主题描述该文档的概率。
在完成 LDA 模型拟合后,让我们创建一些图形,帮助我们深入理解结果。
练习 7.08:可视化 LDA
可视化是探索主题模型结果的有力工具。在本练习中,我们将观察三种不同的可视化方式。它们分别是基本的直方图和使用 t-SNE 及 PCA 的专业可视化:
-
运行并显示
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/unspr-lrn-ws/img/B15923_07_33.jpg
图 7.33:LDA 模型的直方图和双变量图
-
定义一个拟合 t-SNE 模型并绘制结果的函数。定义完该函数后,将详细描述函数的各个部分,以便步骤清晰:
Exercise7.01-Exercise7.12.ipynb 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 = [] The complete code for this step can be found at https://packt.live/34gLGKa.
步骤 1:该函数首先通过输入的阈值过滤主题-文档矩阵。由于有成千上万的标题,包含所有标题的图形会难以阅读,因此不具有帮助性。因此,只有当分布值大于或等于输入阈值时,函数才会绘制该文档:
index_meet_threshold = numpy.amax(data, axis=1) >= threshold lda_transform_filt = data[index_meet_threshold]
步骤 2:数据过滤完成后,运行 t-SNE,其中组件数量为 2,以便我们能够在二维中绘制结果:
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)
步骤 3:创建一个向量,用来标示每个文档最相关的主题。该向量将用于根据主题为绘图着色:
most_prob_topic = [] for i in range(tsne_fit.shape[0]): most_prob_topic.append(lda_transform_filt[i].argmax())
步骤 4:为了了解主题在语料库中的分布以及阈值筛选的影响,该函数返回主题向量的长度,并给出每个主题及其分布值最大文档数:
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))
步骤 5:创建并返回绘图:
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)
-
执行函数:
plot_tsne(data=lda_transform, threshold=0.75)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_34.jpg
图 7.34:t-SNE 绘图,展示了主题在语料库中的分布指标
可视化结果显示,使用三个主题的 LDA 模型整体产生了良好的结果。在双图中,圆圈的大小适中,表明这些主题在语料库中呈现一致性,且圆圈之间的间隔较好。t-SNE 图显示出明显的聚类,支持双图中圆圈之间的分离。唯一的明显问题,之前已经讨论过,就是其中一个主题包含了似乎与该主题不太相关的词汇。
注意
要访问此特定部分的源代码,请参阅 packt.live/34gLGKa
。
你也可以在线运行此示例,网址:packt.live/3fbWQES
。
必须执行整个 Notebook 才能获得预期的结果。
在下一个练习中,让我们使用四个主题重新运行 LDA 模型。
练习 7.09:尝试四个主题
在这个练习中,LDA 模型的主题数量设置为四。这样做的动机是尝试解决三主题 LDA 模型中可能存在的一个问题,该主题包含与巴勒斯坦和经济相关的词汇。我们首先会执行这些步骤,然后在最后查看结果:
-
运行一个主题数量为四的 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/unspr-lrn-ws/img/B15923_07_35.jpg
图 7.35:LDA 模型
-
执行之前定义的
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)
-
打印词汇-主题表:
print(W_df4)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_36.jpg
图 7.36:使用四主题 LDA 模型的词汇-主题表
-
打印文档-主题表:
print(H_df4)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_37.jpg
图 7.37:使用四主题 LDA 模型的文档-主题表
-
使用
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/unspr-lrn-ws/img/B15923_07_38.jpg
图 7.38:描述四主题 LDA 模型的直方图和双图
查看词汇-主题表,我们可以看到这个模型找到的四个主题与原始数据集中的四个主题一致。这些主题分别是巴拉克·奥巴马、巴勒斯坦、微软和经济。现在的问题是,为什么使用四个主题构建的模型具有比使用三个主题的模型更高的困惑度得分?这个答案可以从步骤 5 生成的可视化结果中找到。
双变量图有合理大小的圆圈,但其中两个圆圈相距非常近,这表明这两个主题(微软和经济)非常相似。在这种情况下,相似性实际上是直观上有道理的。微软是一家全球性的大公司,影响并受经济的影响。如果我们要进行下一步,那就是运行 t-SNE 图,以检查 t-SNE 图中的簇是否有重叠。
注意
要访问此特定部分的源代码,请参见 packt.live/34gLGKa
。
你也可以在网上运行这个例子,访问 packt.live/3fbWQES
。
必须执行整个笔记本才能获得期望的结果。
现在让我们将 LDA 的知识应用于另一个数据集。
活动 7.02:LDA 和健康推文
在本活动中,我们将应用 LDA 于 活动 7.01 中加载和清理过的健康推文数据,加载并清理 Twitter 数据。记得使用该活动中使用的同一笔记本。一旦步骤执行完毕,讨论模型的结果。这些单词分组有意义吗?
对于本活动,让我们假设我们有兴趣获得对主要公共卫生话题的高层次理解。也就是说,了解人们在健康领域谈论的内容。我们已经收集了一些数据,可能会揭示这一问题的答案。正如我们所讨论的,识别数据集中主要话题的最简单方法是主题建模。
以下是完成该活动的步骤:
-
指定
number_words
、number_docs
和number_features
变量。 -
创建一个词袋模型,并将特征名称分配给另一个变量,以便以后使用。
-
确定最佳的主题数量。
-
使用最佳的主题数量来拟合 LDA 模型。
-
创建并打印出词-主题表。
-
打印出文档-主题表。
-
创建一个双变量图可视化。
-
保持笔记本打开,以便以后进行建模。
输出将如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_39.jpg
图 7.39:在健康推文上训练的 LDA 模型的直方图和双变量图
注意
本活动的解决方案可以在第 482 页找到。
练习 7.10:使用 TF-IDF 创建词袋模型
在本练习中,我们将使用 TF-IDF 创建一个词袋模型:
-
运行 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/unspr-lrn-ws/img/B15923_07_40.jpg
图 7.40:TF-IDF 向量化器的输出
-
返回用于分析输出的特征名称(语料库字典中的实际单词)。你会记得我们在执行
CountVectorizer
时也做过同样的事情,出现在 练习 7.05,使用 Count Vectorizer 创建词袋模型: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',
在这个练习中,我们以词袋模型的形式总结了语料库。为每个文档-词组合计算了权重。这个词袋输出将在我们下一步的主题模型拟合中再次使用。下一节将介绍 NMF。
注意
要访问此特定部分的源代码,请参阅packt.live/34gLGKa
。
你也可以在packt.live/3fbWQES
上在线运行此示例。
你必须执行整个笔记本才能获得预期结果。
非负矩阵分解
与 LDA 不同,非负矩阵分解(NMF)不是一个概率模型。相反,正如其名称所示,它是一种涉及线性代数的方法。将矩阵分解作为主题建模的方法由 Daniel D. Lee 和 H. Sebastian Seung 于 1999 年提出。该方法属于模型的分解类,包括 PCA,这是一种在第四章中介绍的建模技术,降维与 PCA 简介。
PCA 和 NMF 之间的主要区别在于,PCA 要求组件是垂直的,但允许它们是正数或负数。而 NMF 要求矩阵组件是非负的,如果你从数据的角度思考这一要求,这应该是有道理的。主题与文档之间不能是负相关的,词汇与主题之间也不能是负相关的。
如果你还不信服,试着解释将一个负权重与主题和文档关联起来。这就像,主题 T 占文档 D 的-30%;但这是什么意思呢?这毫无意义,因此 NMF 对矩阵分解的每个部分都有非负的要求。
让我们定义要分解的矩阵X为术语-文档矩阵,其中行是词汇,列是文档。矩阵X的每个元素要么是词* i*(行)在文档j(列)中的出现次数,要么是词* i与文档j之间关系的其他量化。矩阵X*自然是一个稀疏矩阵,因为术语-文档矩阵中的大多数元素将为零,因为每个文档只包含有限数量的词汇。稍后会讲到如何创建这个矩阵并推导量化方法。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_41.jpg
图 7.41: 矩阵分解
矩阵分解的形式为 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_17.png,其中两个组件矩阵,W和H,分别表示主题词集合和每个文档的主题权重。更具体地说,Wnxk 是一个词对主题的矩阵,而Hkxm 是一个主题对文档的矩阵,正如前面所述,Xnxm 是一个词对文档的矩阵。
思考这个因式分解的一种好方式是将其看作是定义抽象主题的加权词组的总和。矩阵因式分解公式中的等价符号表明,因式分解WH是一个近似值,因此这两个矩阵的乘积不会完全重现原始的术语-文档矩阵。
目标和 LDA 一样,是找到最接近原始矩阵的近似值。像X一样,W和H也是稀疏矩阵,因为每个主题只与少数几个词相关,每个文档仅由少数几个主题组成——在许多情况下是一个主题。
Frobenius 范数
解决 NMF 的目标与 LDA 相同:找到最佳近似值。为了衡量输入矩阵与近似值之间的距离,NMF 可以使用几乎任何距离度量,但标准是 Frobenius 范数,也称为欧几里得范数。Frobenius 范数是元素平方和的总和。
选择好距离度量后,下一步是定义目标函数。最小化 Frobenius 范数将返回最好的原始术语-文档矩阵的近似值,从而得到最合理的主题。请注意,目标函数是相对于W和H最小化的,以使两个矩阵都
是非负的。它的表达式为 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_Formula_19.png。
乘法更新算法
1999 年 Lee 和 Seung 在他们的论文中用于解决 NMF 的优化算法是乘法更新算法,它仍然是最常用的解决方案之一。在本章后面的练习和活动中将会实现该算法。
W和H的更新规则是通过展开目标函数并对W和H分别取偏导数得到的。导数并不难,但需要相当广泛的线性代数知识,而且时间较长,所以我们跳过导数,直接给出更新规则。请注意,在更新规则中,i是当前的迭代次数,T表示矩阵的转置。第一个更新规则如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_42.jpg
图 7.42:第一个更新规则
第二个更新规则如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_43.jpg
图 7.43:第二个更新规则
W和H会迭代更新,直到算法收敛。目标函数也可以被证明是非递减的;即,在每次迭代更新W和H时,目标函数会更接近最小值。请注意,乘法更新优化器,如果更新规则重新组织,是一种重新缩放的梯度下降算法。
构建成功的 NMF 算法的最后一个组成部分是初始化W和H矩阵,以确保乘法更新能够快速工作。一种流行的初始化矩阵的方法是奇异值分解(SVD),它是特征分解的推广。
在接下来的练习中实现的 NMF 方法中,矩阵通过非负双奇异值分解进行初始化,基本上这是 SVD 的一个更高级版本,严格要求非负。关于这些初始化算法的详细信息,对于理解 NMF 并不重要。只需注意,初始化算法是优化算法的起点,能够显著加速收敛过程。
练习 7.11:非负矩阵分解
在本练习中,我们将拟合 NMF 算法,并输出与之前使用 LDA 时相同的两个结果表。这些表是词-主题表,显示与每个主题相关的前 10 个词,和文档-主题表,显示与每个主题相关的前 10 个文档。
NMF 算法函数中有两个我们之前没有讨论过的额外参数,分别是alpha
和l1_ratio
。如果存在过拟合模型的风险,这些参数控制正则化在目标函数中的应用方式(l1_ratio
)和程度(alpha
):
注意
更多细节可以在 scikit-learn 库的文档中找到(scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html
)。
-
定义 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/unspr-lrn-ws/img/B15923_07_44.jpg
图 7.44:定义 NMF 模型
-
运行
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)
-
打印
W
表:print(W_df)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_45.jpg
图 7.45:包含概率的词-主题表
-
打印
H
表:print(H_df)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_46.jpg
图 7.46:包含概率的文档-主题表
词-主题表包含词语分组,表明与四主题 LDA 模型在练习 7.09《尝试四个主题》中生成的抽象主题相同。然而,比较中有趣的是,这些分组中包含的一些个别词语是新的,或者它们在分组中的位置发生了变化。考虑到这两种方法学是不同的,这并不令人惊讶。鉴于与原始数据集中指定的主题一致性,我们已经证明这两种方法都是提取语料库潜在主题结构的有效工具。
就像我们之前对 LDA 模型的拟合一样,我们将可视化我们的 NMF 模型的结果。
注意
若要访问该部分的源代码,请参考 packt.live/34gLGKa
。
你也可以在线运行这个示例,访问链接 packt.live/3fbWQES
。
你必须执行整个 Notebook 才能获得期望的结果。
练习 7.12:可视化 NMF
这个练习的目的是可视化 NMF 的结果。通过可视化结果,可以深入了解主题的独特性以及每个主题在语料库中的流行度。在这个练习中,我们将使用 t-SNE 来进行可视化,t-SNE 在第六章中有详细讨论,t-分布随机邻域嵌入:
-
在清理后的数据上运行
transform
,以获取主题-文档分配。打印数据的形状和一个示例:nmf_transform = nmf.transform(clean_vec2) print(nmf_transform.shape) print(nmf_transform)
输出结果如下:
(92946, 4) [[5.12653315e-02 3.60582233e-15 3.19729419e-34 8.17267206e-16] [7.43734737e-04 2.04138105e-02 6.85552731e-15 2.11679327e-03] [2.92397552e-15 1.94083984e-02 4.76691813e-21 1.24269313e-18] ... [9.83404082e-06 3.41225477e-03 6.14009658e-04 3.23919592e-02] [6.51294966e-07 1.32359509e-07 3.32509174e-08 6.14671536e-02] [4.53925928e-05 1.16401194e-04 1.84755839e-02 2.00616344e-03]]
-
运行
plot_tsne
函数来拟合 t-SNE 模型并绘制结果:plot_tsne(data=nmf_transform, threshold=0)
图形显示如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_47.jpg
图 7.47:带有度量的 t-SNE 图,汇总了语料库中的主题分布
注意
结果可能略有不同,因为支持 LDA 和 NMF 的优化算法有所不同。许多函数没有设定种子值的功能。
若要访问该部分的源代码,请参考 packt.live/34gLGKa
。
你也可以在线运行这个示例,访问链接 packt.live/3fbWQES
。
你必须执行整个 Notebook 才能获得期望的结果。
t-SNE 图没有指定阈值,显示了一些主题重叠,并且语料库中的主题频率存在明显差异。这两点解释了为何在使用困惑度时,最佳的主题数量为三个。似乎存在某些主题之间的关联,模型无法完全处理。即使存在主题之间的关联,当主题数设置为四时,模型仍能找到正确的主题。
总结一下,NMF 是一种非概率主题模型,旨在回答与 LDA 相同的问题。它使用线性代数中的一种常用概念——矩阵分解,即将一个庞大且难以处理的矩阵分解为较小、更易解释的矩阵,从而帮助回答许多与数据相关的问题。请记住,非负性要求并不是数学上的要求,而是数据本身的要求。任何文档的组件不可能为负数。
在许多情况下,NMF 的表现不如 LDA,因为 LDA 包含先验分布,这为主题词组提供了额外的信息层。然而,我们知道在某些情况下,尤其是当主题高度相关时,NMF 的表现更好。正是这种情况发生在所有练习所依据的标题数据上。
现在让我们尝试将新学到的 NMF 知识应用到前面活动中使用的 Twitter 数据集。
活动 7.03:非负矩阵分解
本活动总结了在活动 7.01,加载与清理 Twitter 数据中加载并清理的健康 Twitter 数据上的主题建模分析,以及在活动 7.02,LDA 与健康推文中进行的 LDA 分析。执行 NMF 非常简单,所需代码有限。我们可以借此机会在思考 NMF 的局限性和优势时调整模型参数。
以下是完成本活动的步骤:
-
创建适当的词袋模型,并将特征名称输出为另一个变量。
-
使用活动 7.02,LDA 与健康推文中的主题数量(
n_components
)值,定义并拟合 NMF 算法。 -
获取主题-文档和词-主题表格。花几分钟探索词组,并尝试定义抽象的主题。你能量化这些词组的含义吗?这些词组合理吗?与使用 LDA 产生的结果是否相似?
-
调整模型参数并重新运行步骤 3和步骤 4。结果如何变化?
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_48.jpg
图 7.48:带有概率的词-主题表格
注意
本活动的解决方案可以在第 487 页找到。
总结
当面对从尚未看到的大量文档中提取信息的任务时,主题建模是一个很好的方法,因为它可以提供有关文档潜在结构的洞察。也就是说,主题模型通过接近性而非语境来寻找词组。
在本章中,我们学习了如何应用两种最常见且最有效的主题建模算法:潜在 Dirichlet 分配(LDA)和非负矩阵分解(NMF)。现在你应该能够熟练使用几种不同的技术清理原始文本文档,这些技术可以在许多其他建模场景中使用。接着,我们学习了如何通过应用词袋模型,将清理过的语料库转换为适当的数据结构,即每个文档的原始词频或词权重。
本章的主要内容是拟合这两种主题模型,包括优化主题数量、将输出转换为易于理解的表格,并可视化结果。有了这些信息,你应该能够应用完全功能的主题模型,为你的业务提取价值和洞察。
在下一章,我们将完全改变方向。我们将深入探讨市场篮子分析。