# 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() &gt; 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 构建单层神经网络。
