Skip to content

Latest commit

 

History

History
442 lines (290 loc) · 21.7 KB

File metadata and controls

442 lines (290 loc) · 21.7 KB

3. TensorFlow 中的聚类

前一章中介绍的线性回归是一种监督学习算法,我们使用数据和输出值(或标签)来构建适合它们的模型。但我们并不总是拥有标记数据,尽管如此,我们也希望以某种方式分析它们。在这种情况下,我们可以使用无监督学习算法,例如聚类。聚类方法被广泛使用,因为它通常是数据分析的初步筛选的好方法。

在本章中,我将介绍名为 K-means 的聚类算法。它肯定是最受欢迎的,广泛用于自动将数据分组到相关的子集中,以便子集中的所有元素彼此更相似。在此算法中,我们没有任何目标或结果变量来预测估计值。

我还将使用本章来介绍 TensorFlow 的知识,并在更详细地介绍名为tensor(张量)的基本数据结构。我将首先解释这种类型的数据是什么样的,并展示可以在其上执行的转换。然后,我将使用张量在案例研究中展示 K-means 算法的使用。

基本数据结构:张量

TensorFlow 程序使用称为张量的基本数据结构来表示其所有数据。张量可以被认为是动态大小的多维数据数组,其具有静态数据类型的属性,可以从布尔值或字符串到各种数字类型。下面是 Python 中的主要类型及其等价物的表格。

TensorFlow 中的类型 Python 中的类型 描述
DT_FLOAT tf.float32 32 位浮点
DT_INT16 tf.int16 16 位整数
DT_INT32 tf.int32 32 位整数
DT_INT64 tf.int64 64 位整数
DT_STRING tf.string 字符串
DT_BOOL tf.bool 布尔值

另外,每个张量拥有阶(Rank),这是其维度的数量。例如,以下张量(在 Python 中定义为列表)的阶为 2:

t = [[1,2,3],[4,5,6],[7,8,9]]

张量可以有任何阶。二阶张量通常被认为是矩阵,一阶张量将是向量。零阶被认为是标量值。

TensorFlow 文档使用三种类型的命名约定来描述张量的维度:形状(Shape),阶(Rank)和维数(Dimension Number)。下表显示了它们之间的关系,以便使跟踪 TensorFlow 文档更容易:

形状 维数
[] 0 0-D
[D0] 1 1-D
[D0, D1] 2 2-D
[D0, D1, D2] 3 3-D
[D0, D1, ... Dn] n n-D

这些张量可以通过一系列 TensorFlow 软件包提供的转换进行操作。 下面,我们将在下表中讨论其中的一些内容。

在本章中,我们将详细介绍其中一些内容。 可以在 TensorFlow 的官方网站 [18] 上找到完整的转换列表和详细信息。

| 操作 | 描述 | | tf.shape | 获取张量的形状 | | tf.size | 获取张量的大小 | | tf.rank | 获取张量的阶 | | tf.reshape | 改变张量的形状,保持包含相同的元素 | | tf.squeeze | 删除大小为 1 的张量维度 | | tf.expand_dims | 将维度插入张量 | | tf.slice | 删除部分张量 | | tf.split | 将张量沿一个维度划分为多个张量 | | tf.tile | 将一个张量多次复制,并创建新的张量 | | tf.concat | 在一个维度上连接张量 | | tf.reverse | 反转张量的特定维度 | | tf.transpose | 转置张量中的维度 | | tf.gather | 根据索引收集部分 |

例如,假设你要将2×2000(2D 张量)的数组扩展为立方体(3D 张量)。 我们可以使用tf.expand_ dims函数,它允许我们向张量插入一个维度:

vectors = tf.constant(conjunto_puntos)
extended_vectors = tf.expand_dims(vectors, 0)

在这种情况下,tf.expand_dims将一个维度插入到由参数给定的一个张量中(维度从零开始)。

从视觉上看,上述转变如下:

如你所见,我们现在有了 3D 张量,但我们无法根据函数参数确定新维度 D0 的大小。

如果我们使用get_shape()操作获得此tensor的形状,我们可以看到没有关联的大小:

print expanded_vectors.get_shape()

它可能会显示:

TensorShape([Dimension(1), Dimension(2000), Dimension(2)])

在本章的后面,我们将看到,由于 TensorFlow 形状广播, 张量的许多数学处理函数(如第一章所示),能够发现大小未指定的维度的大小,,并为其分配这个推导出的值。

TensorFlow 中的数据存储

在介绍 TensorFlow 的软件包之后,从广义上讲,有三种主要方法可以在 TensorFlow 程序上获取数据:

  1. 来自数据文件。
  2. 数据作为常量或变量预加载。
  3. 那些由 Python 代码提供的。

下面,我简要介绍其中的每一个。

  1. 数据文件

通常,从数据文件加载初始数据。这个过程并不复杂,鉴于本书的介绍性质,我邀请读者访问TensorFlow 的网站 [19],了解如何从不同文件类型加载数据。你还可以查看 Python 代码input_data.py [20](可在 Github 上找到),它从文件中加载 MNIST 数据(我将在下面几章使用它)。

  1. 变量和常量

当谈到小集合时,也可以预先将数据加载到内存中;创建它们有两种基本方法,正如我们在前面的例子中看到的那样:

  • constant(…)用于常量
  • Variable(…)用于变量

TensorFlow 包提供可用于生成常量的不同操作。在下表中,你可以找到最重要的操作的摘要:

操作 描述
tf.zeros_like 创建一个张量,所有元素都初始化为 0
tf.ones_like 创建一个张量,所有元素都初始化为 1
tf.fill 创建一个张量,其中所有元素都初始化为由参数给出的标量值
tf.constant 使用参数列出的元素创建常量张量

在 TensorFlow 中,在模型的训练过程中,参数作为变量保存在存储器中。 创建变量时,可以使用由函数参数定义的张量作为初始值,该值可以是常量值或随机值。 TensorFlow 提供了一系列操作,可生成具有不同分布的随机张量:

操作 描述
tf.random_normal 具有正态分布的随机值
tf.truncated_normal 具有正态分布的随机值,但消除那些幅度大于标准差 2 倍的值
tf.random_uniform 具有均匀分布的随机值
tf.random_shuffle 在第一维中随机打乱张量元素
tf.set_random_seed 设置随机种子

一个重要的细节是,所有这些操作都需要特定形状的张量作为函数的参数,并且创建的变量具有相同的形状。 通常,变量具有固定的形状,但TensorFlow提供了在必要时对其进行重塑的机制。

使用变量时,必须在构造图之后,在使用run()函数执行任何操作之前显式初始化这些变量。 正如我们所看到的,为此可以使用tf.initialize_all_variables()。 通过 TensorFlow 的tf.train.Saver()类,可以在训练模型时和之后将变量保存到磁盘上,但是这个类超出了本书的范围。

  1. 由Python代码提供

最后,我们可以使用我们所谓的“符号变量”或占位符来在程序执行期间操作数据。调用是placeholder(),参数为元素类型和张量形状,以及可选的名称。

从 Python 代码调用Session.run()Tensor.eval()的同时,张量由feed_dict参数中指定的数据填充。回想第 1 章中的第一个代码:

import tensorflow as tf
a = tf.placeholder("float")
b = tf.placeholder("float")
y = tf.mul(a, b)
sess = tf.Session()
print sess.run(y, feed_dict={a: 3, b: 3})

在最后一行代码中,调用sess.run()时,我们传递两个张量ab的值到feed_dict参数。

通过张量的简要介绍,我希望从现在起读者可以毫不费力地读懂下面几章的代码。

K-Means 算法

K-Means 是一种无监督算法,可以解决聚类问题。 它的过程遵循一种简单易行的方法,通过一定数量的簇(假设k簇)对给定数据集进行聚类。 簇内的数据点是同构的,不同簇的点是异构的,这意味着子集中的所有元素与其余元素相比更为相似。

算法的结果是一组K个点,称为质心,它们是所得的不同组的焦点,以及点集的标签,这些点分配给其中一个簇。 簇内的所有点与质心的距离都比任何其他质心更近。

如果我们想要直接最小化误差函数(所谓的 NP-hard 问题),那么簇的生成是一个计算上很昂贵的问题。因此,已经创建了一些算法,通过启发式在局部最优中快速收敛。 最常用的算法使用迭代优化技术,它在几次迭代中收敛。

一般来讲,这种技术有三个步骤:

  • 初始步骤(步骤 0):确定K个质心的初始集合。
  • 分配步骤(步骤 1):将每个观测值分配到最近的组。
  • 更新步骤(步骤 2):计算每个新组的新质心。

有几种方法可以确定初始K质心。 其中一个是在数据集中随机选择K个观测值并将它们视为质心;这是我们将在我们的示例中使用的那个。

分配(步骤 1)和更新(步骤 2)的步骤在循环中交替,直到认为算法已经收敛为止,这可以是,例如,当点到组的分配不再改变的时候。

由于这是一种启发式算法,因此无法保证它收敛于全局最优,结果取决于初始组。 因此,由于算法通常非常快,通常使用不同的初始质心值重复执行多次,然后权衡结果。

要在 TensorFlow 中开始编写 K-means 的示例,我建议首先生成一些数据作为测试平台。 我建议做一些简单的事情,比如在 2D 空间中随机生成 2,000 个点,遵循二维正态分布来绘制一个空间,使我们能够更好地理解结果。 例如,我建议使用以下代码:

num_puntos = 2000
conjunto_puntos = []
for i in xrange(num_puntos):
   if np.random.random() > 0.5:
     conjunto_puntos.append([np.random.normal(0.0, 0.9), np.random.normal(0.0, 0.9)])
   else:
     conjunto_puntos.append([np.random.normal(3.0, 0.5), np.random.normal(1.0, 0.5)])

正如我们在前一章中所做的那样,我们可以使用一些 Python 图形库来绘制数据。 我建议像以前一样使用 matplotlib,但这次我们还将使用基于 matplotlib 的可视化包 Seaborn 和数据操作包 pandas,它允许我们使用更复杂的数据结构。

如果未安装这些软件包,则必须先使用pip执行此操作,然后才能运行以下代码。

要显示随机生成的点,我建议使用以下代码:

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

df = pd.DataFrame({"x": [v[0] for v in conjunto_puntos],
        "y": [v[1] for v in conjunto_puntos]})
sns.lmplot("x", "y", data=df, fit_reg=False, size=6)
plt.show()

此代码生成二维空间中的点图,如下面的截图所示:

在 TensorFlow 中实现的 k-means 算法将上述点分组,例如在四个簇中,可能像这样(基于 Shawn Simister 在他的博客中展示的模型 [21]):

import numpy as np
vectors = tf.constant(conjunto_puntos)
k = 4
centroides = tf.Variable(tf.slice(tf.random_shuffle(vectors),[0,0],[k,-1]))

expanded_vectors = tf.expand_dims(vectors, 0)
expanded_centroides = tf.expand_dims(centroides, 1)

assignments = tf.argmin(tf.reduce_sum(tf.square(tf.sub(expanded_vectors, expanded_centroides)), 2), 0)

means = tf.concat(0, [tf.reduce_mean(tf.gather(vectors, tf.reshape(tf.where( tf.equal(assignments, c)),[1,-1])), reduction_indices=[1]) for c in xrange(k)])

update_centroides = tf.assign(centroides, means)

init_op = tf.initialize_all_variables()

sess = tf.Session()
sess.run(init_op)

for step in xrange(100):
   _, centroid_values, assignment_values = sess.run([update_centroides, centroides, assignments])

我建议读者使用以下代码检查assignment_values张量中的结果,该代码生成像上面那样的图:

data = {"x": [], "y": [], "cluster": []}

for i in xrange(len(assignment_values)):
  data["x"].append(conjunto_puntos[i][0])
  data["y"].append(conjunto_puntos[i][1])
  data["cluster"].append(assignment_values[i])

df = pd.DataFrame(data)
sns.lmplot("x", "y", data=df, fit_reg=False, size=6, hue="cluster", legend=False)
plt.show()

截图以及我的代码执行结果如下图所示:

新的组

我假设读者可能会对上一节中介绍的 K-means 代码感到有些不知所措。 好吧,我建议我们一步一步详细分析它,特别是观察涉及的张量以及它们在程序中如何转换。

首先要做的是将所有数据移到张量。 在常数张量中,我们使初始点保持随机生成:

vectors = tf.constant(conjunto_vectors)

按照上一节中介绍的算法,为了开始我们必须确定初始质心。 随着我前进,一个选项可能是,从输入数据中随机选择K个观测值。 一种方法是使用以下代码,它向 TensorFlow 表明,它必须随机地打乱初始点并选择前K个点作为质心:

k = 4
centroides = tf.Variable(tf.slice(tf.random_shuffle(vectors),[0,0],[k,-1]))

K个点存储在 2D 张量中。 要知道这些张量的形状,我们可以使用tf.Tensor.get_shape()

print vectors.get_shape()
print centroides.get_shape()

TensorShape([Dimension(2000), Dimension(2)])
TensorShape([Dimension(4), Dimension(2)])

我们可以看到vectors是一个数组,D0 维包含 2000 个位置,每个位置一个向量,D1 的位置是每个点x, y。 相反,centroids是一个矩阵,维度 D0 有四个位置,每个质心一个位置,D1 和vectors相同。

接下来,算法进入循环。 第一步是为每个点计算其最接近的质心,根据平方欧几里德距离 [22](只能在我们想要比较距离时使用):

为了计算该值,使用tf.sub(vectors, centroides。 我们应该注意到,虽然减法的两个张量都有 2 个维度,但它们在一个维度上大小不同(维度 D0 为 2000 和 4),实际上它们也代表不同的东西。

为了解决这个问题,我们可以使用之前讨论过的一些函数,例如tf.expand_dims,以便在两个张量中插入一个维度。 目的是将两个张量从 2 维扩展到 3 维来使尺寸匹配,以便执行减法:

expanded_vectors = tf.expand_dims(vectors, 0)
expanded_centroides = tf.expand_dims(centroides, 1)

tf.expand_dims在每个张量中插入一个维度;在vectors张量的第一维(D0),以及centroides张量的第二维(D1)。 从图形上看,我们可以看到,在扩展后的张量中,每个维度具有相同的含义:

它似乎得到了解决,但实际上,如果你仔细观察(在插图中概述),在每种情况下都有大小无法确定的些维度。 请记住,使用get_shape()函数我们可以发现:

print expanded_vectors.get_shape()
print expanded_centroides.get_shape()

输出如下:

TensorShape([Dimension(1), Dimension(2000), Dimension(2)])
TensorShape([Dimension(4), Dimension(1), Dimension(2)])

使用 1 表示没有指定大小。

但我已经展示 TensorFlow 允许广播,因此tf.sub函数能够自己发现如何在两个张量之间将元素相减。

直观地,并且观察先前的附图,我们看到两个张量的形状匹配,并且在这些情况下,两个张量在一定维度上具有相同的尺寸。 这些数学,如 D2 维度所示。 相反,在维度 D0 中只有expanded_centroides的定义大小。

在这种情况下,如果我们想要在此维度内对元素执行减法,则 TensorFlow 假定expanded_vectors张量的维度 D0 必须是相同的大小。

对于expended_centroides张量的维度 D1 的大小也是如此,其中 TensorFlow 推导出expanded_vectors张量的尺寸 D1 的大小。

因此,在分配步骤(步骤 1)中,算法可以用 TensorFlow 代码的这四行表示,它计算平方欧几里德距离:

diff=tf.sub(expanded_vectors, expanded_centroides)
sqr= tf.square(diff)
distances = tf.reduce_sum(sqr, 2)
assignments = tf.argmin(distances, 0)

而且,如果我们看一下张量的形状,我们会看到它们分别对应diffsqrdistanceassign,如下所示:

TensorShape([Dimension(4), Dimension(2000), Dimension(2)])
TensorShape([Dimension(4), Dimension(2000), Dimension(2)])
TensorShape([Dimension(4), Dimension(2000)])
TensorShape([Dimension(2000)])

也就是说,tf.sub函数返回了张量dist,其中包含质心和向量的坐标的差(维度 D1 表示数据点,D0 表示质心,每个坐标x, y在维度 D2 中表示)。

sqr张量包含它们的平方。 在dist张量中,我们可以看到它已经减少了一个维度,它在tf.reduce_sum函数中表示为一个参数。

我用这个例子来解释 TensorFlow 提供的几个操作,它们可以用来执行减少张量维数的数学运算,如tf.reduce_sum。在下表中,你可以找到最重要的操作摘要。

操作 描述
tf.reduce_sum 沿一个维度计算元素总和
tf.reduce_prod 沿一个维度计算元素的乘积
tf.reduce_min 沿一个维度计算元素最小值
tf.reduce_max 沿一个维度计算元素最大值
tf.reduce_mean 沿一个维度计算元素平均值

最后,使用tf.argmin实现分配,它返回张量的某个维度的最小值的索引(在我们的例子中是 D0,记得它是质心)。 我们还有tf.argmax操作:

手术 描述
tf.argmin 沿某个维度返回最小值的索引
tf.argmax 沿某个维度返回最大值的索引

事实上,上面提到的 4 条语句可以在一行代码中汇总,正如我们在上一节中看到的那样:

assignments = tf.argmin(tf.reduce_sum(tf.square(tf.sub(expanded_vectors, expanded_centroides)), 2), 0)

但无论如何,内部的tensors,以及它们定义为节点和执行的内部图的操作,就像我们之前描述的那样。

计算新的质心

在那段代码中,我们可以看到means张量是k张量的连接结果,它们对应属于每个簇的每个点的平均值。

接下来,我将评论每个 TensorFlow 操作,这些操作涉及计算属于每个簇的每个点的平均值 [23]。

  • 使用equal,我们可以得到布尔张量(Dimension(2000)),它(使用true)表示assignments张量K个簇匹配的位置,当时我们正在计算点的平均值。
  • 使用where构造一个张量(Dimension(1) x Dimension(2000)),带有布尔张量中值为true的位置,布尔张量作为参数接收的_布尔张量_。
  • reshape构造张量(Dimension(2000) x Dimension(1)),其中vectors张量内的点的索引属于簇c
  • gather构造张量(Dimension(1) x Dimension(2000)),它收集形成簇c的点的坐标。
  • 使用reduce_mean,构造张量_(Dimension(1) x Dimension(2))_,其中包含属于簇c的所有点的平均值。

无论如何,如果读者想要深入研究代码,正如我常说的那样,你可以在 TensorFlow API 页面上找到有关这些操作的更多信息,以及非常具有说明性的示例 [24]。

图表执行

最后,我们必须描述上述代码中,与循环相对应的部分,以及使用means张量的新值更新质心的部分。

为此,我们需要创建一个操作,它将means张量的值分配到质心中,而不是在执行操作run()时,更新的质心的值在循环的下一次迭代中使用:

update_centroides = tf.assign(centroides, means)

在开始运行图之前,我们还必须创建一个操作来初始化所有变量:

init_op = tf.initialize_all_variables()

此时一切准备就绪。 我们可以开始运行图了:

sess = tf.Session()
sess.run(init_op)

for step in xrange(num_steps):
   _, centroid_values, assignment_values = sess.run([update_centroides, centroides, assignments])

在此代码中,每次迭代中,更新每个初始点的质心和新的簇分配。

请注意,代码指定了三个操作,它必须查看run()调用的执行,并按此顺序运行。 由于要搜索三个值,sess.run()会在训练过程中返回元素为三个 numpy 数组的数据结构,内容为相应张量。

由于update_centroides是一个结果是不返回的参数的操作,因此返回元组中的相应项不包含任何内容,因此被排除,用_来表示 [25] 。

对于其他两个值,质心和每个簇的分配点,我们有兴趣在完成所有num_steps次迭代后在屏幕上显示它们。

我们可以使用简单的打印。 输出如下:

print centroid_values

[[ 2.99835277e+00 9.89548564e-01]
[ -8.30736756e-01 4.07433510e-01]
[ 7.49640584e-01 4.99431938e-01]
[ 1.83571398e-03 -9.78474259e-01]]

我希望读者的屏幕上有类似的值,因为这表明他已成功执行了本书这一章中展示的代码。

我建议读者在继续之前尝试更改代码中的任何值。 例如num_points,特别是k的数量,并使用生成图的先前代码查看它如何更改assignment_values张量中的结果。

请记住,为了便于测试本章所述的代码,可以从 Github [26] 下载。 包含此代码的文件名是Kmeans.py

在本章中,我们展示了 TensorFlow 的一些知识,特别是基本数据结构张量,它来自实现 KMeans 聚类算法的 TensorFlow 代码示例。

有了这些知识,我们就可以在下一章中逐步使用 TensorFlow 构建单层神经网络。