Python+Tensorflow机器学习实战
上QQ阅读APP看书,第一时间看更新

2.3 TensorFlow基本概念

在前面章节中,我们介绍了TensorFlow的设计理念,了解到具有自身特点的张量、节点、占位符和会话等概念。在本节中,将对这些基本概念进行详解。

2.3.1 Tensor

Tensor即张量,是最基本的概念,也是TensorFlow中最主要的数据结构。张量用于在计算图中进行数据传递,但是创建了一个张量后,不会立即在计算图中增加该张量,而需要将该张量赋值给一个变量或占位符,之后才会将该张量增加到计算图中。Tensor中存储的数据类型如表2.1所示。

表2.1 Tensor中的数据类型

张量的生成方式有很多种,例如固定张量、相似张量、序列张量和分布函数张量等,具体实现如下:

其中,第10行表示在0和2之间,包括2,等间距地取3个值的张量。

第11行表示在0和5之间,不包括5,间距为1进行取值后组成的张量。

第12行表示从最小值minval(包括)到最大值maxval(不包括)取均匀分布随机数的张量。

第13行表示取的随机数符合指定均值的正态分布张量。

第14行表示取的正态分布随机数位于指定均值到两个标准差之间的张量。

在环境中查看实际输出张量的结果,在代码中增加会话,具体实现如下:

有关会话的相关内容将在后面章节中介绍,在此只查看输出结果,如图2.4所示。

图2.4 Tensor的输出结果

2.3.2 Variable

Variable即变量,一般用来表示图中的各个计算参数,包括矩阵和向量等,它在计算图中有固定的位置。一般我们在TensorFlow中通过调整这些变量的状态来优化机器学习算法。

创建变量应使用tf.Variable()函数,通过输入一个张量,返回一个变量。变量声明后需要进行初始化才能使用。通过打印张量和变量,可对比它们不同之处,具体实现如下:

其中,在第08行第一次打印变量的结果,此时还没有对其进行初始化,会报错。

在第09和第10行,对所有变量进行了初始化,这样能够成功打印结果,如图2.5所示。

图2.5 Variable的输出结果

2.3.3 Placeholder

Placeholder即占位符,用于表示输入输出数据的格式,允许传入指定类型和形状的数据。占位符仅仅声明了数据位置,告诉系统这里有一个值、向量或矩阵等,现在还没法给出具体数值。占位符通过会话的feed_dict参数获取数据,在计算图运行时使用获取的数据进行计算,计算完毕后获取的数据就会消失。

例如,给出一维数组X[1.0,2.0]和Y[10.0,11.0]中对应值相加的计算结果,使用占位符的具体实现如下:

运行上述代码,计算结果如图2.6所示。

图2.6 Placeholder的输出结果

占位符和变量都是TensorFlow计算图的关键工具,请务必理解两者的区别及正确的使用方法。

2.3.4 Session

Session即会话,是TensorFlow中计算图的具体执行者,与图进行实际的交互。一个会话中可以有多个图,会话的主要目的是将训练数据添加到图中进行计算,当然也可以修改图的结构。

对于会话有两种调用方式:一种方式是明确地调用会话的生成函数和关闭函数,具体实现如下。

  
     01 sess = tf.Session()
     02 sess.run(...)
     03 sess.close()

使用这种调用方式时,要明确调用sess.close(),以释放资源。如果程序异常退出,关闭函数就不能被执行,从而导致资源泄漏。

另一种方式是利用上下文管理机制自动释放所有资源,具体实现如下:

  
     01 with tf.Session() as sess:
     02     sess.run(...)

使用这种调用方式时不需要再调用sess.close()来释放资源,在退出with语句时,会话会自动关闭并释放资源。

2.3.5 Operation

Operation即操作,是TensorFlow图中的节点,它的输入和输出都是Tensor。它的作用是完成各种操作,包括运算操作、矩阵操作和神经网络构建操作等。主要操作如表2.2所示。

表2.2 主要操作

TensorFlow不仅提供了常见的数学运算、数组运算、矩阵运算等数学操作,更重要的是提供了针对神经网络的操作,包括激活函数、池化函数、数据标准化函数、分类函数、损失函数、卷积函数和循环神经网络等,具体内容将在后续章节中介绍。

2.3.6 Queue

Queue即队列,也是图中的一个节点,是一种有状态的节点。Queue主要包含入列(enqueue)和出列(dequeue)两个操作。enqueue操作返回计算图中的一个Operation节点,dequeue操作返回一个Tensor值,需要放在Session中运行才能获得真正的数值。

根据实现方式的不同,队列主要实行了两种队列方式:一是按入列顺序出列的队列FIFOQueue;二是按随机顺序出列的队列RandomShuffleQueue。

FIFOQueue方式就是创建一个先进先出的队列,在需要读入的训练样本有序时使用,例如处理语音、文字样本时。

例如,创建一个长度为10的队列,并将一些数字入队,然后逐一出队,具体实现如下:

运行上述代码,就能看到有序的输出队列,结果如下:

  
     q 1.0
     q 2.0
     q 3.0
     q 4.0
     q 5.0
     q 6.0

RandomShuffleQueue方式就是创建一个随机队列,在出队列时以随机的顺序输出元素。随机队列在需要读入的训练样本无序时使用,例如,处理一些图像样本时。

例如,创建一个长度为10的队列,并将一些数字入队,然后逐一出队,具体实现如下:

其中,在第02行声明了一个队列长度为10、出队后最小长度为0、数据类型为float的随机队列。运行上述代码,就能看到输出队列的结果如下:

  
     q 1.0
     q 4.0
     q 6.0
     q 2.0
     q 5.0
     q 3.0

在随机队列中,我们定义了队列长度以及出队后的最小长度。当队列长度等于最小值时,不再执行出队操作,如果此时还要求执行出队操作,就会发生阻断,程序不再执行。同理,当队列长度等于最大值时,还要求执行入队操作也会发生阻断。

对于一个队列长度为10、出队后最小长度为2的队列,仍然入队6个数,出队6次,具体实现如下:

运行上述代码,就能看到输出队列的结果如下:

  
     q 1.0
     q 6.0
     q 2.0
     q 5.0
     timeout
     timeout

2.3.7 QueueRunner

QueueRunner即队列管理器。在TensorFlow运行时,计算所使用的硬件资源,主要包括CPU、GPU、内存以及读取数据时计算机使用的其他硬件资源。因为涉及磁盘操作,所以速度远低于前者。因此,在实际操作中不会像之前那样在主线程中进行入队操作,通常会使用多个线程来读取数据,然后使用一个线程来使用数据。使用队列管理器可管理这些读写队列的线程。

创建QueueRunner需要使用方法tf.train.QueueRunner._init_(queue, enqueue_ops),其中,queue代表指定的队列,enqueue_ops代表指定的操作,一般都是多个操作,每个操作会使用一个线程。

声明了QueueRunner列队管理器后,还需要创建线程来具体执行队列管理操作。使用方法tf.train.QueueRunner.create_threads(sess, coord=None, daemon=False, start=False)来完成线程的创建。

例如,创建一个队列管理器,进行两个操作。一个操作完成计数器的自增,另一个操作将计数器的值入队。我们通过出队观察入队的数值,具体实现如下:

运行上述代码,实际输出如下:

  
     7.0
     12.0
     14.0
     22.0
     27.0
     32.0
     183.0
     185.0
     ERROR:TensorFlow:Exception in QueueRunner: Session has been closed.
     ERROR:TensorFlow:Exception in QueueRunner: Session has been closed.
     Exception in thread QueueRunnerThread-fifo_queue_2-fifo_queue_2_enqueue:

显然,输出的结果并非自然数列。这是因为计数器自增操作和入队操作在不同线程中且不同步,可能计数器自增操作执行了很多次之后,才进行了一个入队操作。

另一方面,最后程序报错了。这是因为主程序的出队操作和线程中的入队操作是异步的。当出队结束后,主程序结束,自动关闭了会话。但是入队线程并没有结束,所以程序报错。如果显式地采用sess.run()和sess.close()方法,则整个程序最后会产生阻断,无法结束。

2.3.8 Coordinator

在前面中使用QueueRunner时,由于入队和出队由各自的线程完成,且未进行同步通信,因此导致程序无法正常结束。为了实现线程间的同步,使用Coordinator(协调器)来处理。

在使用QueueRunner.create_threads()创建线程时,指定一个协调器。当主线程完成操作后,使用Coordinator.request_stop()方法,通知所有的线程停止当前线程,并等待其他线程结束后返回结果。

下面对2.3.7节中的QueueRunner示例进行重构,加入协调器。具体实现如下:

与2.3.7节的代码进行对比,我们注释掉第11行的原有队列管理器创建语句。修改为第12行的队列管理器语句,指定对应的线程协调器。

对于线程的协调,使用coord进行。如第15行,通知其他线程关闭;第16行,等待其他线程结束。

运行上述代码,实际输出如下:

  
     0.0
     0.0
     0.0
     4.0
     8.0
     14.0
     19.0
     27.0

通过这样的实现,在主进程结束后,其他线程也可以相应地结束,不会再出现会话已结束的程序错误。