Graph 和 Session

TensorFlow用数据流图来表示你的计算中的不同操作的依赖关系。这也导致在使用底层API进行编程的时候,你首先定义一个数据流图,然后创建一个session在一些本地或者远程的设备上运行定义的graph的各个部分。

如果你计划使用底层API进行编程,那么这个文档很有用。像Estimator还有Keras隐藏了很多关于graph和session的细节。但是如果你想了解这些高层API是如何工作的,这篇文章对你也是有帮助的。

为什么用数据流图?


对于并行计算来说,数据流是一个通用的编程模型。在数据流图里,节点表示一些计算,边表示被一个计算消费或者生产的数据。比如,在一个tensorflow的图里,tf.matmul 操作会映射到一个节点,它有两个输入边,代表两个矩阵,一个输出的边,代表两个矩阵相乘的输出。

TensorFlow利用数据流的以下优点来执行你的程序:
– 并行化。用显式的边来代表操作之间的依赖,这样让系统很容易找到可以并行运行的操作。
– 分布式执行。通过显式的边来代表操作之间的数据流动。让tensorflow可以把你的程序分到不同机器的多个设备上执行(GPU,CPU,TPU)。TensorFlow 在设备之间加入必要的通讯和协调。
– 编译。TensorFlow的 XLA compiler 可以利用你数据流图的信息生成运行更快的代码,比如把相邻的操作合并。
– 迁移。数据流图是和语言无关的。你可以用python创建一个数据流图,然后保存它,为了低延迟,你可以用c++程序打开并执行它

tf.Graph是什么


一个tf.Graph包含两种相关的信息:
– 图的结构。 图的边和节点,表示单个的操作是怎么合并到一起。但是并没有说明怎么使用它们。图的结构就像汇编代码,它有一些信息,但是没有包含所有源代码的信息。
– 图里的集合。TensorFlow提供了一种机制可以在tf.Graph里存储元信息集合。tf.add_to_collection方法使你可以把一组对象和一个key关联起来。(tf.GraphKeys定义了一些标准的key),tf.get_collection使你可以根据一个key查询它所关联的所有对象。TensorFlow的很多地方的库都用这个功能:比如当你创建一个tf.Variable,这个变量会默认的被加入到”global variables”和”trainable variables”. 后边当你创建一个tf.train.Saver 或者一个tf.train.Optimizer,在这些集合里的变量会被当做默认参数。

构建一个 tf.Graph


大多数的TensorFlow程序是从构建一个数据流图开始的。在这个阶段,你调用tensorflow API的function来构建新的tf.Operation(节点)和tf.Tensor(边),然后把它们加入一个tf.Graph实例。TensorFlow提供了default graph,这是一个在同一个上下文下对所有API 方法的隐式参数。比如:
– 调用 tf.constant(42.0)会创建一个单一的tf.Operation,它可以产生42这个值。并把它加入到default graph,并且返回一个tf.Tensor它表示这个常量值。
– 调用 tf.matmul(x,y)会创建一个单独的tf.Operation,它会对输入的tensor x,y 相乘,把这个operation加入到default graph,并返回一个tf.Tensor来表示乘积的结果。
– 执行 v = tf.Variable(0) 会给graph上添加一个tf.Operation。这个op会存储一个可写入的tensor value。这个值可以在多次tf.Session.run之间保存。 tf.Variable对象封装了这个opration。并且可以像调用一个tensor一样调用这个variable 对象。它会读取当前存储的值。tf.Variable对象同时拥有向assign和assign_add 这样的方法来创建tf.Operation对象。当执行返回的op对象,会更新存储的值。
– 调用tf.train.Optimizer.minimize会在default graph里增加用来计算梯度的operations和tensors。并且返回一个tf.Operation。运行时,将会对一组variable应用梯度下降。

大多数的程序只依赖于default graph。然而你可以参考Dealing with multiple graphs文档来看更高级的例子。像 Estimator这样的高级接口,会自动帮你管理好default graph。并且可能创建不同的graph用来训练和验证你的模型。

注意,调用大多数的TensorFlow API里的方法只会给default graph里增加opertion和tensor。但是不会执行实际的操作。你把这些tensor和opertion组装起来,直到你有一个tensor或者operation来表示总体的计算。比如执行一步梯度下降。然后把它传给tf.Session去执行计算。

给Opertion命名


一个Graph对象给它所包含的Operation定义了一个namespace。TensorFlow给graph里的每一个opertiaon都起了一个独一无二的名字。但是指定一个有意义的名字让你的程序更易读和易于调试。TensorFlow提供了两种方式让你来给operation命名。
– 每个可以创建一个新的Operation或者tensor的方法都接受一个可选的参数 name。比如 tf.constant(42.0,name=”answer”) 会创建一个新的Operation,名字叫做answer。并且返回一个tensor,叫做answer:0 如果default graph里已经存在一个operation叫做answer,那么TensorFlow会在名字后边加上_1,_2来让名字不重复。
– tf.name_scope方法让你可以在一定的上下文内给创建的operation都加上name space的前缀。当前的name scope前缀是一个以/分隔由所有可用的tf.name_scope管理的命名。如果一个name scope在当前的上下文里已经使用了。那么tensorflow会加上_1,_2。比如:


c_0 = tf.constant(0,name="c") # 名字是c
c_1 = tf.constant(2,name="c") # 名字是c_1
with tf.name_scope("outer"):
    c_2 = tf.constant(2,name="c") # 名字是outer/c
    with tf.name_scope("inner"):
        c_3 = tf.constant(3,name="c") # 名字是outer/inner/c
    with tf.name_scope("inner"):
        c_4 = tf.constant(4,name="c") # 名字是outer/inner_1/c
    c_5 = tf.constant(5,name="c") # 名字是outer/c_1

graph的可视化用了name scope来对操作分组,减少了图形的复杂性。

注意,tensor的名字隐式的是以产生它的operation的名字命名的。一个tensor的名字是


"<OP_NAME>:<i>":

- OP_NAME 是产生这个tensor的operation的名字。
- i,是这个tensor在这个operation产生的output里的序号。

把Operation放到不同的设备


如果你想你的tensorflow程序使用多个不同的设备。tf.device方法提供了一个很方便的功能让所有创建在特定上下文里的所有operation都运行在同样的设备上(或者同类型的设备)。
一个设备的描述是下边这样的形式:


/job:<JOB_NAME>/task:<TASK_INDEX>/device:<DEVICE_TYPE>:<DEVICE_INDEX>

- JOB_NAME, 是一个以字母开头的字母数字组合的名字。
- DEVICE_TYPE 是一个注册的设备类型比如GPU或者CPU
- TASK_INDEX 是一个非负的整数,它表示在JOB里的task的index
- DEVICE_INDEX 非负整数,表示设备index,比如一个进程可以使用多个显卡。

你不需要指定设备描述的全部。如果你的程序运行在一个单独的机器上,只有一个GPU,你可能用tf.device去把operation放到CPU或者GPU上。


# 在外边的context里创建的operation会自动寻找最佳的device去执行,比如CPU和GPU都可用,
# 并且这个operation有GPU实现,就会调用GPU。
weights = tf.random_normal(...)
with tf.device("/device:CPU:0"):
    #这里创建的opertaion会放在CPU上执行。
    img = tf.decode_jpeg(tf.read_file("img.jpg"))
with tf.device("/device:GPU:0"):
    # 这里创建的operation会放在GPU上运行。
    result = tf.matmul(weights,img)

如果你的tensorflow程序是一个分布式的程序,你需要指定job name和task id,这样把variables放到parameter server job(“/job:ps”),而其他的操作放到worker job(“/job:worker”):


with tf.device("/job:ps/task:0"):
    weights_1 = tf.Variable(tf.truncated_normal([784,100]))
    biases_1 = tf.Variable(tf.zeroes([100]))
with tf.device("/job:ps/taks:1"):
    weights_2 = tf.Variable(tf.truncated_normal([100,10]))
    biases_2 = tf.Variable(tf.zeroes([10]))
with tf.device("/job:worker"):
    layer_1 = tf.matmul(train_batch,weights_1)+biases_1
    layer_2 = tf.matmul(train_batch,weights_2)+biases_2

tf.device给了你很大的灵活性去选择哪一个operation,或者图的一块,放在哪里去运行。有很多简单的已有的设置也可以很好的工作,比如tf.train.replica_device_setter 可以被tf.device用来在数据并行的分布式训练下分配operation。下边的例子显示tf.train.replica_device_setter是如何对variable和operation应用不同的分配策略的。


with tf.device(tf.train.replica_device_setter(ps_tasks=3)):
    #variable会被循环的放置在"/job:ps"的3个task上.
    w_0 = tf.Variable(...) #被分配在"/job:ps/task:0"
    b_0 = tf.Variable(...) #被分配在"/job:ps/task:1"
    w_1 = tf.Variable(...) #被分配在"/job:ps/task:2"
    b_1 = tf.Variable(...) #被分配在"/job:ps/task:0"
    input_data = tf.placeholder(tf.float32) # 分配在 "/job:worker"
    layer_0 = tf.matmul(input_data,w_0)+b_0 # 分配在 "/job:worker"
    layer_1 = tf.matmul(layer_0,w_1)+b_1 # 分配在 "/job:worker"

类Tensor对象


很多tensorflow的operation都需要一个或者多个tensor对象作为参数。tf.matmul需要两个tensor对象。tf.add_n需要n 个tensor元素的list作为参数。为了方便,这些方法接收可以接收tensor-like对象来作为参数。并隐式的将类tensor对象通过tf.convert_to_tensor方法转化为tesnor。类Tesnor对象包括:
– tf.Tensor
– tf.Variable
– numpy.ndarray
– list(和类tensor对象的列表)
– Pthon里的标量类型:bool,float,int,str

你也可以用tf.register_tensor_conversion_function去增加额外的类tensor类型。

注意:默认情况下,tensorflow每次会创建新的tensor对象.如果你的类tensor对象比较大,比如是numpy.ndarray的对象,它里边存着所有的训练数据。你多次调用的话,会内存溢出。所以你应该调用tf.convert_to_tensor 把类tensor对象转化为tensor对象。

在tf.Session里执行一个graph


TensorFlow用tf.Session来带便一个和client程序的连接。通常是Python程序,也可以是C++。一个tf.Session对象提供了对本地设备的访问。如果是分布式TensorFlow的运行环境,提供了对远程设备的访问。它同时缓存了关于Graph的信息,让你可以高效的多次运行同样的计算。

创建一个session


# 创建一个默认的进程内的session
with tf.Session() as sess:
    #...
# 创建一个remote session
with tf.Session("grpc://example.org:2222"):

因为session里拥有物理资源比如GPU和网络连接,所以用一个context manager(在with block里),这样当离开这个block的时候就会自动关闭session。你也可以不用with block,那你就要用Session.close自己关闭session。

高层API(Estimator)也接受以下我们说的这些session参数,可以通过直接设置或者通过类似tf.estimator.RunConfig这样的对象来设置。

tf.Session.init接受下边3个可选的参数:
– target。默认是空,表示本地资源。你可以用grpc://URL来指定一个tensorflow server。那样你就可以访问server控制的所有硬件了。你可以通过tf.train.Server去详细看如何创建一个tensorflow server。比如,通用的between-graph replication配置,tf.Session和client在同一个进程上连接到tf.tarin.Server。你可以去查看distributed TensorFlow 分布式部署文档看一下其他常用的场景。
– graph。默认情况下Session会绑定到default graph,也指定运行这个graph的operations。如果你的程序里有多个graph。你可以这明确的指定一个graph。
– config。这个参数让你可以设置一个tf.ConfigProto来控制session的行为。
allow_soft_placement:如果你错误的把一个只能在cpu下运行的操作设置成在GPU下运行,这个配置为True会将那个操作放到CPU下运行。
cluster_def:分布式环境下让你可以控制计算在哪些机器上进行。
graph_options.optimizer_options:tensorflow在运行你的graph之前会进行优化,这个参数让你可以控制这些优化。
gpu_options.allow_growth:控制显存的分配方式,True是渐进分配,False是启动一次分配。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: