多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
# 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`函数,它允许我们向张量插入一个维度: ```py vectors = tf.constant(conjunto_puntos) extended_vectors = tf.expand_dims(vectors, 0) ``` 在这种情况下,`tf.expand_dims`将一个维度插入到由参数给定的一个张量中(维度从零开始)。 从视觉上看,上述转变如下: ![](https://jorditorres.org/wp-content/uploads/2016/02/image023.gif) 如你所见,我们现在有了 3D 张量,但我们无法根据函数参数确定新维度 D0 的大小。 如果我们使用`get_shape()`操作获得此`tensor`的形状,我们可以看到没有关联的大小: ```py print expanded_vectors.get_shape() ``` 它可能会显示: ```py TensorShape([Dimension(1), Dimension(2000), Dimension(2)]) ``` 在本章的后面,我们将看到,由于 TensorFlow 形状广播, 张量的许多数学处理函数(如第一章所示),能够发现大小未指定的维度的大小,,并为其分配这个推导出的值。 ### TensorFlow 中的数据存储 在介绍 TensorFlow 的软件包之后,从广义上讲,有三种主要方法可以在 TensorFlow 程序上获取数据: 1. 来自数据文件。 2. 数据作为常量或变量预加载。 3. 那些由 Python 代码提供的。 下面,我简要介绍其中的每一个。 1) **数据文件** 通常,从数据文件加载初始数据。这个过程并不复杂,鉴于本书的介绍性质,我邀请读者访问TensorFlow 的网站 [19],了解如何从不同文件类型加载数据。你还可以查看 Python 代码[`input_data.py`](https://github.com/jorditorresBCN/TutorialTensorFlow/blob/master/input_data.py) [20](可在 Github 上找到),它从文件中加载 MNIST 数据(我将在下面几章使用它)。 2) **变量和常量** 当谈到小集合时,也可以预先将数据加载到内存中;创建它们有两种基本方法,正如我们在前面的例子中看到的那样: * `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()`类,可以在训练模型时和之后将变量保存到磁盘上,但是这个类超出了本书的范围。 3) **由Python代码提供** 最后,我们可以使用我们所谓的“符号变量”或占位符来在程序执行期间操作数据。调用是`placeholder()`,参数为元素类型和张量形状,以及可选的名称。 从 Python 代码调用`Session.run()`或`Tensor.eval()`的同时,张量由`feed_dict`参数中指定的数据填充。回想第 1 章中的第一个代码: ```py 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()`时,我们传递两个张量`a`和`b`的值到`feed_dict`参数。 通过张量的简要介绍,我希望从现在起读者可以毫不费力地读懂下面几章的代码。 ### K-Means 算法 K-Means 是一种无监督算法,可以解决聚类问题。 它的过程遵循一种简单易行的方法,通过一定数量的簇(假设`k`簇)对给定数据集进行聚类。 簇内的数据点是同构的,不同簇的点是异构的,这意味着子集中的所有元素与其余元素相比更为相似。 算法的结果是一组`K`个点,称为质心,它们是所得的不同组的焦点,以及点集的标签,这些点分配给其中一个簇。 簇内的所有点与质心的距离都比任何其他质心更近。 如果我们想要直接最小化误差函数(所谓的 NP-hard 问题),那么簇的生成是一个计算上很昂贵的问题。因此,已经创建了一些算法,通过启发式在局部最优中快速收敛。 最常用的算法使用迭代优化技术,它在几次迭代中收敛。 一般来讲,这种技术有三个步骤: + 初始步骤(步骤 0):确定`K`个质心的初始集合。 + 分配步骤(步骤 1):将每个观测值分配到最近的组。 + 更新步骤(步骤 2):计算每个新组的新质心。 有几种方法可以确定初始`K`质心。 其中一个是在数据集中随机选择`K`个观测值并将它们视为质心;这是我们将在我们的示例中使用的那个。 分配(步骤 1)和更新(步骤 2)的步骤在循环中交替,直到认为算法已经收敛为止,这可以是,例如,当点到组的分配不再改变的时候。 由于这是一种启发式算法,因此无法保证它收敛于全局最优,结果取决于初始组。 因此,由于算法通常非常快,通常使用不同的初始质心值重复执行多次,然后权衡结果。 要在 TensorFlow 中开始编写 K-means 的示例,我建议首先生成一些数据作为测试平台。 我建议做一些简单的事情,比如在 2D 空间中随机生成 2,000 个点,遵循二维正态分布来绘制一个空间,使我们能够更好地理解结果。 例如,我建议使用以下代码: ```py 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`执行此操作,然后才能运行以下代码。 要显示随机生成的点,我建议使用以下代码: ```py 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() ``` 此代码生成二维空间中的点图,如下面的截图所示: ![](https://jorditorres.org/wp-content/uploads/2016/02/image024.png) 在 TensorFlow 中实现的 k-means 算法将上述点分组,例如在四个簇中,可能像这样(基于 Shawn Simister 在他的博客中展示的模型 [21]): ```py 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`张量中的结果,该代码生成像上面那样的图: ```py 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() ``` 截图以及我的代码执行结果如下图所示: ![](https://jorditorres.org/wp-content/uploads/2016/02/image026.png) ### 新的组 我假设读者可能会对上一节中介绍的 K-means 代码感到有些不知所措。 好吧,我建议我们一步一步详细分析它,特别是观察涉及的张量以及它们在程序中如何转换。 首先要做的是将所有数据移到张量。 在常数张量中,我们使初始点保持随机生成: ```py vectors = tf.constant(conjunto_vectors) ``` 按照上一节中介绍的算法,为了开始我们必须确定初始质心。 随着我前进,一个选项可能是,从输入数据中随机选择`K`个观测值。 一种方法是使用以下代码,它向 TensorFlow 表明,它必须随机地打乱初始点并选择前`K`个点作为质心: ```py k = 4 centroides = tf.Variable(tf.slice(tf.random_shuffle(vectors),[0,0],[k,-1])) ``` 这`K`个点存储在 2D 张量中。 要知道这些张量的形状,我们可以使用`tf.Tensor.get_shape()`: ```py 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](只能在我们想要比较距离时使用): ![](https://jorditorres.org/wp-content/uploads/2016/02/image028.jpg) 为了计算该值,使用`tf.sub(vectors, centroides`。 我们应该注意到,虽然减法的两个张量都有 2 个维度,但它们在一个维度上大小不同(维度 D0 为 2000 和 4),实际上它们也代表不同的东西。 为了解决这个问题,我们可以使用之前讨论过的一些函数,例如`tf.expand_dims`,以便在两个张量中插入一个维度。 目的是将两个张量从 2 维扩展到 3 维来使尺寸匹配,以便执行减法: ```py expanded_vectors = tf.expand_dims(vectors, 0) expanded_centroides = tf.expand_dims(centroides, 1) ``` `tf.expand_dims`在每个张量中插入一个维度;在`vectors`张量的第一维(D0),以及`centroides`张量的第二维(D1)。 从图形上看,我们可以看到,在扩展后的张量中,每个维度具有相同的含义: ![](https://jorditorres.org/wp-content/uploads/2016/02/image031.gif) 它似乎得到了解决,但实际上,如果你仔细观察(在插图中概述),在每种情况下都有大小无法确定的些维度。 请记住,使用`get_shape()`函数我们可以发现: ```py print expanded_vectors.get_shape() print expanded_centroides.get_shape() ``` 输出如下: ```py 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 代码的这四行表示,它计算平方欧几里德距离: ```py diff=tf.sub(expanded_vectors, expanded_centroides) sqr= tf.square(diff) distances = tf.reduce_sum(sqr, 2) assignments = tf.argmin(distances, 0) ``` 而且,如果我们看一下张量的形状,我们会看到它们分别对应`diff`,`sqr`,`distance`和`assign`,如下所示: ```py 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 条语句可以在一行代码中汇总,正如我们在上一节中看到的那样: ```py 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()`时,更新的质心的值在循环的下一次迭代中使用: ```py update_centroides = tf.assign(centroides, means) ``` 在开始运行图之前,我们还必须创建一个操作来初始化所有变量: ```py init_op = tf.initialize_all_variables() ``` 此时一切准备就绪。 我们可以开始运行图了: ```py 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`次迭代后在屏幕上显示它们。 我们可以使用简单的打印。 输出如下: ```py 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 构建单层神经网络。