【Tensorflow学习笔记 - 1】Tensorflow计算模式与运行原理概述

引言

本文主要从Tensorflow的使用角度,逐步展开对基本功能的学习。由于Tensorflow可以看作是一个封装好的带框架的库,因此,学好它主要就是在于理解它库中的包和函数的正确用法,然后在不断的使用过程中,加深理解,一般都能够根据它包与函数命名特点以及形参的特点总结出一些规律,这样就能够为以后的熟练使用打下基础。
对于我而言,熟练掌握Tensorflow最终为的是能够根据自己的设想DIY想要的网络结构,训练属于自己网络下的模型。至少,目前为止,没有哪一种网络是完全通用的,也就意味着面对不同的问题,需要设计不同的网络结构,以更好的解决特定的问题。
注意:本文不会对Tensorflow的配置和安装进行介绍。

环境介绍

Tensorflow有许多开发环境,本文学习的是python接口下的Tensorflow,其执行是基于python解释器的,因此与操作系统没有直接关系,无论你是在Windows环境还是Linux环境或者mac的环境下,同样的代码都是可以运行的(有其他库依赖的另当别论),因此我们主要关心的是tensorflow的版本和python的版本,本文是在windows环境下配置的gpu版本的tensorflow,python环境基于anaconda3
tensorflow 1.10.0-dev20180722 in python 3.6

初识Tensorflow计算模式

Tensorflow(后面直接用TF替代)的基本计算原理是基于图结构,大概其构想可能是来源于对神经网络基本结构的拓扑抽象。TF的核心思想正如它的名字一样,是Tensor(张量)在已架设好的计算图模型上flow(流动),并在这一过程中实现对数据的处理,因此对TF来说,所关心的主要就是两件事:数据和操作,通过这两件事的多重组合,就可以得到我们想要的结果。在TF中,对于数据的表示就是Tensor,而对Tensor的各种操作官方文档称作operation,简称OP。这些OP从编程的角度来说,就是一系列封装好的具有一定计算、资源分配、数据变换等功能的函数,只不过,这些函数的输入与输出都少不了一个叫做Tensor类型的对象。这也印证了TF环境所进行的计算工作,就是将一个大的计算拆分成许许多多的OP,我们通过按照自己的需要组合这些OP作为我们的算法,而其中两个OP连结的桥梁就是Tensor(即一个OP的输出的Tensor是另一个OP的输入)。
举一个非常简单的栗子来类比TF中的图运算:实现一串运算 a + 6 * b + 1


图1 一个简单运算的计算图例子

如上图所示,蓝色方框为两个输入,绿框为常数,黑圈为操作符。
这样一个系列的操作用TF的语言表示如下:

import tensorflow as tf

a = tf.placeholder('int32')
b = tf.placeholder('int32')

mul_6b = tf.multiply(6, b)
add_a = tf.add(a, mul_6b)
add_1 = tf.add(add_a, 1)

with tf.Session() as sess:
    print(sess.run(add_1, feed_dict={a: 5, b: -10}))

通过上面的程序可以很容易得到结果:-54
其中输入由一个叫做placeholder的操作来产生一个数据的入口,mul_6a、add_a、add_1这几个分别为定义好的乘法操作与加法操作的一个handle,通过这几个handle就可以用类似操作变量的方式很方便的表示每一个操作之间的关系,从而构建出一个计算流程图。一个图由顶点和边构成,在TF的计算图模型中,顶点(或节点)通常为一个OP,边为Tensor数据。通过这些OP产生的对象均为Tensor对象,我们可以在上述代码中加入print函数来查看其类型与摘要信息:

print(a)
print(b)
print(mul_6b)
print(add_a)
print(add_1)

得到结果:

Tensor("Placeholder:0", dtype=int32)
Tensor("Placeholder_1:0", dtype=int32)
Tensor("Mul:0", dtype=int32)
Tensor("Add:0", dtype=int32)
Tensor("Add_1:0", dtype=int32)

其中我们可以看到,它们都属于Tensor对象。由于我们在用tf.OP(…)这样的函数时,没有指定这一操作叫什么名字,所以它会默认的以如下方式组织名称:

[OP类型]_[序号]:[当前计算节点输出索引]

序号和计算节点输出的标号都是从0开始标号的,当序号为0时,则直接显示OP类型名称,没有下划线部分内容,如上面的a、add_a和mul_6b所示。
遗留问题1:哪些计算节点会输出两个或两个以上的Tensor?即什么情况下输出索引会出现1、2…数值?

从编程的角度来看,OP大部分都是以tensorflow.(编程时通常把tensorflow重命名为tf,以方便编程)开头的公共函数,包括产生数据入口的placeholder函数也可以看作是一个OP(虽然它没有产生任何计算,但他仍然是对节点的操作);而用Tensor对象来表示OP操作的返回内容,因此,在TF的图结构声明阶段,Tensor对象从编程意义上来说仅仅是作为一种对OP操作句柄的表示方式,而从形式上来说可以看作是某OP操作返回了一个Tensor对象,该对象可以继续被其他OP当作输入而不断“接力”实现Tensor在它的计算图模型上流动。

初识Tensorflow的运行模式

当我们大致了解了TF是如何计算我们交给它的任务以后,相当于知道了TF这个worker处理问题的基本思路,那么接下来我们就要考虑,如何把任务交付给TF呢?TF又如何与我们实现交互,从而出色的完成任务呢?
上面的计算模式告诉我们两个重要的概念:Tensor(张量)和OP(operation,定义在某计算节点上的操作)
我们把这两个概念经过一定数学逻辑组合成我们神经网络或者其他计算任务的张量流计算图(Computation Graph)后,和TF通过会话(Session)的模式来“交流”这张图纸的实现过程。
因此,可以理解为:

  • 对图的定义,定义的是计算逻辑,即数据(张量)应该通过怎么样的流程被处理,图结构表示了各个OP节点之间的关系,把这样的图交给TF,它就能够知道计算某个节点的顺序和方法;
  • 会话的过程,表示的是对计算的实际执行过程,与TF的会话过程,对某个节点调用的执行任务都是一次性的,调用一次执行任务使用会话对象的run函数。

一次性如何理解?还拿上面的那个简单计算的例子,能否分两次计算来求得a + 6 * b + 1的结果呢?Let’s try. 修改会话里面的内容如下:

with tf.Session() as sess:
    print(sess.run(mul_6b, feed_dict={b: -10}))  # 这里面只计算mul_6b节点,只需要b参数的值
    print(sess.run(add_1, feed_dict={a: 5})) # 这里接着上面的计算,再提供a的值做最后加法

结果显示:

  • 第一句话正常输出了-60,正确!
  • 第二句话直接报错 “You must feed a value for placeholder tensor ‘Placeholder_1’ with dtype int32”

说明我们在执行第二句话的时候,第一句话中的b参数已经不存在了,从这个角度来讲,每次执行会话的run操作,都是进行了一系列的计算,即我们在计算最后一个图节点的OP时,会按照图1中从右向左的顺序递归每一个所依赖的OP,直到遇到没有依赖的OP为止。(当然实际应用中的图结构要比这个复杂的多,图也未必是简单的树结构,这里只是为了举例方便)

TF将计算图的描述和实际执行分开来做,这样同一个图就可以被很方便的重复使用,在实际执行的过程中,通过run函数启动一次计算任务,对于有placeholder的计算,要通过feed_dict字典告诉TF这些placeholder应当被什么数据填充。如果把执行一次计算任务整体看作是一个函数的话,那么定义的这些placeholders就是这个函数的形参表,函数的返回结果将以numpy的ndarray对象格式被当前执行任务的session的run函数返回。

使用Variable实现多次计算间的交互

到目前为止,通过上面的方式计算a + 6 * b + 1是无法分两次执行的。因为每次执行run的计算之间是互相独立的,那么如何保存两次计算中间产生的那个数呢?使用TF提供的Variable类!
这里有一个小知识点,应该每个学过面向对象编程的小伙伴都了解:对类的定义一定是用大写字母开头的
所以在TF中也不例外,当我们使用tf.Variable的来创建一个变量时候,创建的是一个Variable类的对象,通过print这个对象,你就能发现它与一般OP之间的区别:

b = tf.placeholder('int32')
c = tf.Variable(1, 'int32')
print(b)        # 输出 Tensor("Placeholder:0", dtype=int32)
print(c)        # 输出 <tf.Variable 'Variable:0' shape=() dtype=int32_ref>

那么,当我们想给一个变量对象赋值的时候,要怎么操作呢?
注意下面这是一个错误的示范!!!!!

c = tf.Variable(0, 'int32') # 声明变量
c = tf.multiply(6, b)       # 想给c变量赋值,实际上却是重新定义了c为一个乘法的OP,上面定义的c丢失

通过这种方式是无法对变量进行赋值的,因此这时候,我们就需要借助于Variable对象中的assign函数:
修改代码如下,并将乘法+赋值的操作定义为mul_6b

b = tf.placeholder('int32') # 占位符创建
c = tf.Variable(0, 'int32') # 变量声明
mul_6b = c.assign(tf.multiply(6, b)) # 将6*b的结果赋值给c,并将这个OP定义为mul_6b

这样我们就可以通过mul_6b操作将6*b的结果保存到变量c中。这里我们需要注意的是,assign函数是Variable对象特有的OP,也是它为什么叫做Variable(因为在一次会话结束之前,TF会为这个图中Variable对象分配实际的存储空间,这样使得Variable中的内容能够被保留更新)。对Variable更详细的操作后面会再专门讲述,本文只是进行简单的介绍,从概念上有一个简单的认知即可。

接下来利用这个原理实现两次计算得到结果的操作:

import tensorflow as tf

a = tf.placeholder('int32')
b = tf.placeholder('int32')
c = tf.Variable(0, 'int32')  # 声明变量c,并定义初始值的默认值为0
mul_6b = c.assign(tf.multiply(6, b))
add_a = tf.add(a, c)
add_1 = tf.add(add_a, 1)

# 显示出所有的OP及变量
print(a)
print(b)
print(c)
print(mul_6b)
print(add_a)
print(add_1)

# 测试上面隐式声明的multiply是否产生了实际OP
mm_test = tf.multiply(6, 2)
print(mm_test)

init = tf.initialize_all_variables()  # 在进行计算之前,需要先为所有变量赋一个初始值,此为初始化变量的OP
with tf.Session() as sess:
    sess.run(init)  # 初始化所有变量
    print(sess.run(add_1, feed_dict={a: 5}))  # 这里做一个对比,此时变量c中的值为默认值0
    print(sess.run(mul_6b, feed_dict={b: -10}))  # 这里面只计算mul_6b节点,只需要b参数的值
    print(sess.run(add_1, feed_dict={a: 5}))  # 这里接着上面的计算,再提供a的值做最后加法

通过上面这段代码,我们得到结果如下(中间打印的会话信息已省略):

Tensor("Mul_1:0", shape=(), dtype=int32)     # 这一行打印信息说明了上面隐式multiply产生了实际的OP
Tensor("Placeholder:0", dtype=int32)
Tensor("Placeholder_1:0", dtype=int32)
<tf.Variable 'Variable:0' shape=() dtype=int32_ref>
Tensor("Assign:0", shape=(), dtype=int32_ref)
Tensor("Add:0", dtype=int32)
Tensor("Add_1:0", dtype=int32)
6
-60
-54

由这个结果,我们总结以下内容:

  1. 当产生一个OP的时候,这个OP有两种情况,一种是无handle的,一种是有handle的,在图创建过程中,即使没有用python中的某个变量去承载这个OP,它也会实际在图中有自己的位置和独立的名称。对于上面mul_6b = c.assign(tf.multiply(6, b))来说,其中mul_6b的Tensor类型是Assign,而非Mul+Assign,因此我们可以推断,这样相当于是声明了一个隐式的mul。我们通过在代码中加入另一个无关的multiply操作,并将其命名为mm_test。后面输出结果的第一行印证了我们的推测,mm_test的Tensor类型为Mul_1,按照我们在上面一节中对自动命名方式的结论,可以知道在这个OP产生之前,一定还有一个OP叫做Mul(因为同名的OP自动命名方式为Mul、Mul_1、Mul_2…以此类推)。即上面mul_6b中的multiply操作是隐式声明的一个OP,无handle,而mm_test中的操作是显式声明的OP,可用python变量mm_test去调用或传递它给其他的OP或者会话的run函数。
    那么隐式声明的OP就无法被操作了吗?当然不是的!这大概也是为什么TF要自动给它一个独一无二的名字。TF提供了一个函数叫做get_tensor_by_name(tensor_name),感兴趣的可以自行去测试,是可以重新找回隐式声明的那个Tensor的(前提Tensor的名字要写正确,如本实验中隐式的那个tensor叫做Mul:0),由于这不是这个函数主要使用的地方,仅仅是满足一下猎奇心理,所以这里不作实验展示。
  2. 变量的值能够被TF的会话保存下来,并实现多次计算之间的交互。对于神经网络来说,这一点是很重要的,因为训练往往采用的是批送入式训练,而且数据往往需要进行多次迭代来更新其中的参数,所以变量在TF中的地位非常的重要,灵活运用它才能更好的使用TF。当然本实验中的拆解例子只是为了说明变量的作用,实际应用中应该不会有谁闲的蛋疼把一个简单的联合计算分解成好几步去操作。
  3. 第一个结果为6,是因为对于add_1来说,依赖于add_a和常量1,而add_a又依赖于a和c,这里的a为占位符,无依赖,c为一个变量,变量也是无依赖的,故计算add_1不需要知道b,所以我们在feed_dict中只输入了a的值就能够计算。那么这个时候还没有计算出c的值,变量c的值是从哪来的呢?在我们对变量进行初始化的时候,就会让TF自动为所有变量赋上初始值,这个初始值为声明该变量时候的默认值,即c = tf.Variable(0, 'int32')一句中的0,即为该变量的默认值。实际在初始化变量的时候,会将该默认值当作初始值来初始化c,因此上面的计算为a+c+1=5+0+1=6,改变默认值0会使这个结果发生改变,但是不会影响下面的两个结果。
  4. 第二个结果为-60,这个结果表明了Variable.assign()函数和其他OP一样都是能够返回一个值的,不一样的地方是,与此同时还将Variable对象中的值修改为新的值并保存,因此这个assign函数并不会受到变量默认值的影响。在TF官方文档中也说到,其实在执行sess.run(init)的时候,实际上也是使用了assign操作将所有变量声明时的默认值当作初始值进行初始化的。因此可见assign函数是Variable对象的一个基本操作函数。
  5. 第三个结果为-54,也是我们期望的结果,说明中间的变量c被重新用到了第二次的计算中,并得到了正确的结果,实现了两次计算间交互的目的。这是通过将数据保存在内存中来实现的,如果将数据保存在硬盘上,那就可以实现多次实验间的交互了,也就是我们在训练神经网络时候常说的保存模型和重载模型(个人对模型的理解就是特定的网络结构+参数)

小结

本文主要以一个简单的计算例子,引出Tensorflow内部计算模式与运行模式上的一些原理,了解这些内容是认识这个深度学习框架的基础,当把文中的简单计算拓展为多个维度的矩阵或向量后,就可以组成一个神经网络的前向计算网络。计算图用来提供计算方法(网络算法),会话用来管理计算资源和计算节点间的逻辑。后面将会对里面的内容逐渐展开,逐个进行学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值