转自 [译]与TensorFlow的第一次接触(三)之聚类
前一章节中介绍的线性回归是一种监督学习算法,我们使用数据与输出值(标签)来建立模型拟合它们。但是我们并不总是有已经打标签的数据,却仍然想去分析它们。这种情况下,我们可以使用无监督的算法如聚类。因为聚类算法是一种很好的方法来对数据进行初步分析,所以它被广泛使用。
本章中,会讲解K-means聚类算法。该算法广泛用来自动将数据分类到相关子集合中,每个子集合中的元素都要比其它集合中的元素更相似。此算法中,我们没有任何目标或结果来预测评估。
本章中依然会介绍TensorFlow的使用,并介绍基础数据结构tensor的更多细节。本章开头介绍tensor的数据类型与分析可在该数据结构上执行的运算变换。接下来展示使用tensor来实现的K-means算法。
基础数据结构—tensor
TensorFlow使用基础数据结构---tensor来表示所有数据。一个tensor可以看成是一个拥用静态数据类型动态大小且多维的数组,它可以从布尔或string转换成数值类型。下表是一些主要类型及在Python中对对应的类型:
另外,每个tensor都有一个秩,也是tensor维度的数量。例如,下面的tensor的秩为2:
t = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
tensor可以拥有任意秩。秩为2的tensor经常被看作矩阵,秩为1的tensor会被看作vector,秩为0的被看作标量值。
TensorFlow文档中使用三种不同称谓来描述tensor的维度:Shape,Rank,Dimension。下面的表展示了它们三者之间的关系,
TensorFlow提供的一系列操作来计算这些tensor,接下来我们会讨论下表中的一些操作。
通过本章,我们会继续讨论更多的细节。在Tensorflow的官方网站上能找到更多的操作列表及每一个操作的细节。
举个例子,假如你想扩展一个2*2000(2D tensor)为立方体(3D tensor)。可以使用tf.expand_dims函数,它可以向tensor中插入一个维度。
tf.expand_dims会向tensor中插入一个维度,插入位置就是参数代表的位置(维度从0开始)。
以可视化来展示的话,上面的转换过程如下图所示:
正如你所看到的,我们得到一个3D tensor,但根据函数参数我们无法判断新维度D0的大小。
如果调用get_shape()来获得tensor的shape,可以看到D0没有大小:
print expanded_vectors.get_shape()
显示的结果如下:
TensorShape([Dimension(1),Dimension(2000), Dimension(2)])
本章的稍后,我们可以看到,由于TensorFlow的shape传递特性,很多tensor的数学运算函数(正如第一章中提到的)可以自已发现未确定大小维度的大小,并将该值赋给它。
TensorFlow中的数据存储
Tensorflow程序中主要有三种方式来获取数据:
1.从数据文件
2.以常数与变量预加载
3.Python代码提供的数据
下面简要描述这三种方式:
1.数据文件
通常,原始数据从数据文件中下载。这个过程并不复杂,建议读者去TensorFlow官方网站查看如何从不同类型文件中下载数据的细节。你也可以查看input_data.py代码(可以从github上下载),它会从文件中加载MNIST数据(下一章中使用该数据)。
2.变量与常数
当提到小的数据集时,数据可提前加载到内存中;正如之前例子中看到的,有两种基本方式来创建它们:
通过constant()来创建常数
通过Variable()来创建变量
TensorFlow提供了不同的操作来创建常数。下表中对最重要的几个操作进行了总结:
用TensorFlow训练模型的过程中,参数以变量的形式保存在内存中。当变量创建后,可以将其作为初始值(可能是一个常数或随机值)给一个tensor,该tensor可做为参数传给一个函数。Tensorflow提供了一系列操作来产生不同分布的随机tensor:
一个重要的细节是所有这些操作都需要一个确定shape的tensor作为参数,返回的变量拥有同样的shape。总而言之,变量拥有一个固定的shape,但如有需要,Tensorflow提供了reshape的机制。
当创建变量后,它们必须在图创建完之后且调用run()函数之前显示初始化。可以通过调用tf.initialize_all_variables()来进行初始化。在训练过程中与训练完成后,可通过tf.train.Saver()类来将变量保存到磁盘中,该类的相关细节超过了本书的讨论范围。
3.Python代码提供数据
最后,在程序执行过程中,我们可通过叫做“符号变量”或placeholder来操作数据。Placeholder()的调用,包含了元素类型与tensor的shape为参数,还有一个可选参数name。
在Python代码中调用Session.run()或Tensro.eval()的同时,这个tensor与feed_dict参数中指定的数据相关联。第一章中的代码如下:
代码的最后一行中调用sess.run()时,我们通过feed_dict参数给两个tensor赋值。
通过简短分析tensor,希望从现在开始读者阅读接下来章节的代码时,没有任何困难。
K-means算法
K-means是一种用来解决聚类问题的无监督算法。该算法依据一个简单容易的方式来对数据集分成一定数目(假设K个类别)的类别。一个类别中的数据点是相似的,不同类别中的数据点是各种各样的,也就是说同一子类别中的元素比其它子类别中的元素更相似。
算法的结果是生成K个点集合,叫做centroids,这是不同组的焦点,标签代表了集合中的点,k个聚类都有自己的tag。一个类中的所有点离centroid要比其它任意centroid要近。
如果我们想直接最小化error function,则生成聚类是非常耗计算的(也就是NP问题);一些算法通过启发式方法来达到局部快速收敛。更通用的算法使用迭代优化技术,仅覆盖几次迭代。
一般来说,这种技术主要有三步:
1.初始化(step 0):初始化K个centroid的集合
2.分配(step 1):将每一个对象赋给最近的组
3.更新(step 2):计算每个新组的新centroid
有多种方法来初始化K个centroid。其中之一就是在数据集中随机选取K个对像并将它们看作centroid;接下来的例子中我们会使用这种方法。
分配(step 1)与更新(step 2)在循环中是可选的,循环直到算法开始收敛,举例来说就是分配点到组后,就不再发生变化。
因为这是一个启发式算法,无法保证算法收敛到最佳目标,结果依赖于初始集合。因为算法通常运行很快,可用不同的初始centroid来多次执行该算法,然后评估结果。
开始在Tensorflow中编码实现K-means算法前,建议先生成一些数据用来进行实验。有一种简单的方式,在2D空间中随机生成2000个点,它们服从两个正态分布,我们可画出空间分布来更好的理解结果。示例代码如下:
如我们在之前章节中所做的,我们可使用Python图库来用图表画出这些点。建议使用matplotlib,这次我们使用基于matplotlib的可视化库Seaborn,操作数据用库pandas,该库能运算更复杂的数据结构。
如果你没有安装这些库,在继续下一步前可能过pip来安装它们。
建议使用如下代码来显示我们随机生成的点:
这段代码生成两维空间下的点图如下:
TensorFlow中实现的K-means算法来对上面生成的点进行分组,假如四个类,求例代码如下(基于Shawn Simister在他博客中发表的模型):
建议读者用如下代码来检查assignment_values tensor中的结果,它会生成一张分布图:
上面代码执行后生成的分布图如下:
新组
也许读者会对上面的K-means代码感到困惑。接下来详细分析每一行代码,我们会特别关注相关tensor的变化及它们在程序中如何运算。
首先需要做的是把我们的数据移到tensor中。我们将所有随机生成的点保存到常量tensor中:
vectors=tf.constant(conjunto_vectors)
根据之前讲解的算法,我们在开始就要决定初始centroids。一种方法就是从输入数据中随机选择K个对像。下面的代码就能达到这个目的,随机排列这些点并选择前K个点作为centroids:
这K个点保存在一个2D tensor中。可通过调用tf.Tensor.get_shape()获得这些tensor的shape:
我们可以看到,vectors是一个数组,D0包含了2000个positions,D1包含了每一个点的坐标x,y。centroids是包含四个元素的矩阵,D0代表每一个形心的位置,D1代表点的坐标x,y。
接下来,算法进入一个循环。第一步就是为每一个点,根据平方欧氏距离(只能被用来比较距离)计算最近的centroid。
为计算该值,需要使用tf.sub(vectors, centroides)。虽然这两个相减的tensor都是2维的,但在第1维度上有不同的大小(2000VS 4 D0中),实际上,这也代表了不同的意义。
为解决这个问题,我们需要使用之前提到的函数,如tf.expand_dims用来在两个tensor中插入一个维度。目的是把这两个tensor从2维转换成3维,使得大小匹配可以进行减法:
tf.expand_dims在每一个tensor中插入一个维度,在vector的tensor中第一维度(D0)插入,在centroides tensor中第二维度(D1)插入。从图片来看,扩展后的tensor中各维度有了同样的含义:
看上去这个问题解决了,实际上,如果仔细来看,两个tensor中都有维度不能确定大小。通过调用get_shape()可以看到:
输出如下:
1代表没有赋予大小。
之前就已经说明TensorFlow允许传递,所以tf.sub函数能够自己发现如何在两个tensor间进行减法。
直观地来看上面的图,两个tensor的形状是匹配的,而且在指定维度上也有相同的大小。这些数学运算就像发生在D2维度上那样。然而,D0中只有expanded_centroides有固定大小。
在这种情况下,TensorFlow假设expanded_vectors拥有同样的大小,如果我们想执行元素对元素的减法。
对于expended_centroides的D1同样如此,Tensorflow推断出expanded_vectors的D1大小。
在分配步骤(step 1),算法可实现为如下四行代码,用来计算平方欧氏距离:
如果我们察看tensor的形状,diff, sqr,distances and assignments的大小分别为:
tf.sub返回一个tensor,包含了两个tensor相减的值(vector表明D1的大小,centroid表明D0的大小。D2中表明了x,y)。sqr tensor包含了它们的平方。在distance tensor中,已经减少了一个维度,减少的维度在tf.reduce_sum函数中表明。
通过这个例子来表明TensorFlow提供了一些操作来进行运算,就像tf.reduce_sum来减少tensor的维度。下面的表中总结了一些很重要的操作:
最后,通过tf.argmin来赋值,它返回tensor某一维度中的最小值索引(此处为D0,代表centroid)。同样也有tf.argmax操作:
实际上,上面的四行代码可以总结成一行代码中:
assignments=tf.argmin(tf.reduce_sum(tf.square(tf.sub(expanded_vectors,expanded_centroides)),2),0)
不管如何,定义结点与执行内部图的那些内部tensor与操作都跟我们之前提到一样。
计算新形心
一旦在迭代中创建了新组,需要记住算法的新步骤中包含了计算组的新形心。正如我们之前看到的代码;
means=tf.concat(0,[tf.reduce_mean(tf.gather(vectors,tf.reshape(tf.where(tf.equal(assignments,c)),[1,-1])),reduction_indices=[1])forcinxrange(k)])
这段代码中,means tensor是连接k个tensor的结果,这k个tensor都是由那些平均值属于每一个k类的点组成的。
接下来,会详细分析计算每一个点属于哪个cluster的代码:
A.通过equal获得一个布尔tensor(Dimension(2000)),true代表了assignment tensor与K cluster相匹配的位置,同时我们也计算了点的平均值
B.where根据传进来的布尔tensor中元素值为true的位置来构造一个tensor(Dimension(1) x Dimension(2000))
C.reshape根据vectors tensor内部那些属于c cluster的点的索引来构建一个tensor(Dimension(2000) x Dimension(1))
D.gather从c cluster中收集所有点的坐标并创建tensor(Dimension(1) x Dimension(2000))
E.reduce_mean则是根据c cluster中所有点的平均值来创建tensor(Dimension(1) x Dimension(2))
如果读者想了解代码的更多细节,可访问TensorFlow api页面,通过解说例子来了解所有操作的细节。
图执行
最后,我们来描述循环相关的代码部分与用新计算的平均值tensor来更新centroide的部分。
当run()方法被调用时,我们要在更新的centroids值在下轮迭代使用前,先创建一个赋值操作符用means tensor值更新centroids值:
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])
在这段代码中,每次迭代时,centroids与为每个点新分配的cluster都会被更新。
代码中指定了三个操作,同时需要查看run()的执行状态,并按顺序来运行这三个操作。因为有三个值需要查找,sess.run()返回了三个numpy数组,每个数组分别包含了训练过程中相应的内容。
因为update_centroides这个操作的结果并不需要返回,在返回的turple中相应元素内容为空,用“_”表示不接收该参数。
对于另外两个值,centroids与将点赋给每一个cluster,一旦完成所有迭代计算后,我们可以将这两个变量打印在屏幕上。
使用简单的打印命令,输出如下:
希望读者的电脑上也有接近的值,这说明读者已经成功执行了本章中的相关代码。
建议读者在继续进行下一步之前,先尝试修改某些值。例如num_points,尤其聚类的数量,然后通过生成结果图来查看assignment_values如何变化。
为了测试本章中的代码,可通过github下载本代码。包含本章代码的文件为Kmeans.py,
本章中已经了解了一些TensorFlow的知识,尤其通过TensorFlow中实现一个聚类算法K-means来学习基础数据结构tensor。
了解了tensor后,我们在下章中可以一步步建立一个单层神经网络。