博客TensorFlow基础知识:计算图中的Op,边,和张量中有一句话,对于我来说是醍醐灌顶,一下子明白了TensorFlow的计算模式

计算图的定义和图的运算是分开的.tensorflow是一个符号主义的库.编程模式分为两类,命令式(imperative style)和符号式(symbolic style).命令式的程序很容易理解和调试,它按照原有的逻辑运行.符号式则相反,在现有的深度学习框架中,torch是命令式的,Caffe,Mxnet是两种模式的混合,tensorflow完全采用符号式.符号式编程,一般先定义各种变量,然后建立一个计算图(数据流图),计算图指定了各个变量之间的计算关系,此时对计算图进行编译,没有任何输出,计算图还是一个空壳,只有把需要运算的输入放入后,才会在模型中形成数据流.形成输出.

就是说TensorFlow是一个符号式编程模型,图上最关键的两个东西,一个数据相关的概念Tensor(张量),指的是对数据的一种引用,另一个是图上的概念Operation(操作),也就是计算图中的节点,在写TensorFlow程序的时候,我们就是在Graph上布置节点。
如下代码可以分为几个步骤

  1. 刚开始的时候,想象现在graph上一个op都没有;
  2. 然后随着代码从上往下一直到with tf.Session() as sess:这行,我在图上一共放了6个op,不同的op的之间是有联系的,比如matmul这个op需要“吃掉”一个x和一个weights,然后“吐出”一个东西叫mat_res ,这个mat_res又会被喂给tf.add这个op。这里一个隐含的信息是:op是从上到下依次被定义的(通俗点说,就是依次将各个op扔到了graph上)
  3. 但是当打印mat_res的时候,返回的结果告诉我们,mat_res是一个Tensor,也就是说,个人理解,从数据的角度看,mat_res这个Tensor代表着matmul这个op的输出。
import tensorflow as tf
x = [[1.,2.],[-1.,3.]]
y = [1,0]
weights = tf.Variable([[1,-1],[-1,-1]],name='weights',dtype=tf.float32) #虽然是定义了一个Variable类,但是也相当于在设置了一个op
bias = tf.get_variable('bias',shape=[2],initializer=tf.zeros_initializer) #get_variable也是一个op

mat_res = tf.matmul(x,weights) #定义矩阵乘法计算,也是一个op,目前图上有3个op
print(mat_res) #Tensor("MatMul:0", shape=(2, 2), dtype=float32)
add_res = tf.add(mat_res,bias) #定义矩阵相加计算,也是一个op,目前图上有4个op
y_ = tf.nn.sigmoid(add_res) #定义sigmoid运算,也是一个op,目前图上有5个op

loss = tf.losses.sparse_softmax_cross_entropy(labels=y,logits=y_) #定义sigmoid运算,也是一个op,目前图上有6个op

# 以上就是在图上放op,一共6个op
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    print(sess.run(weights))
    print(sess.run([loss]))

再看下面的代码
我定义了两个cross_entropy,获得losses1的时候,此时图上只有一个cross_entropy1;在获得losses2 的时候,此时图上有cross_entropy1和cross_entropy2;说明通过get_total_losses获取的损失,是此时的graph上的所有损失

def test_loss():
    cross_entropy1 = tf.losses.sparse_softmax_cross_entropy(labels = [1,0,1],logits=[[1/2,1/2],[1/2,1/2],[0.9,1/3]])
    losses1 = tf.losses.get_losses()
    #[<tf.Tensor 'sparse_softmax_cross_entropy_loss/value:0' shape=() dtype=float32>]
    total_loss1 = tf.losses.get_total_loss()
    #Tensor("total_loss:0", shape=(), dtype=float32)
    cross_entropy2 = tf.losses.sparse_softmax_cross_entropy(labels = [1,0,1],logits=[[1/2,1/2],[1/2,1/2],[0.9,1/3]])
    losses2 = tf.losses.get_losses()
    #[<tf.Tensor 'sparse_softmax_cross_entropy_loss/value:0' shape=() dtype=float32>, <tf.Tensor 'sparse_softmax_cross_entropy_loss_1/value:0' shape=() dtype=float32>]
    total_loss2=tf.losses.get_total_loss()
    #Tensor("total_loss_1:0", shape=(), dtype=float32)
    with tf.Session() as sess:
        print(losses1)
        print(total_loss1)
        print(losses2)
        print(total_loss2)
        print(sess.run(cross_entropy1))
        print(sess.run(cross_entropy2))

进一步理解Graph和op,下面的代码里我定义了好多好多op

  1. 先看两个add_res,代码里我定义了两个add_res,他们都叫add_res ,但是打印出来的结果告诉我,第一个add_res是一个名字叫做Add:0的张量,这时候graph上只有一个tf.add的op,所以这个Add:0就代表着这个op的输出;而第二个add_res是一个名字叫做Add_1:0的张量,可以看到它的名字后面多了一个_1,说明它和第一个add_res不是一个东西,它是由于我创建了第二个tf.add,因此现在有两个相加的op,它作为第二个相加的op的输出。
  2. 要理解图上op构建的顺序,就是按照代码执行的顺序从前往后来的,举个例子,当我获得loss1的时候,当前的图上又会被创建一个op,这个op就是通过sparse_softmax_cross_entropy来创建的,它的输出就是loss1;而get_total_loss又会创建一个op,这个op的作用是,获取当前图内全部的损失函数的和,而目前的损失只有一个loss1,所以get_total_loss这个op的输出的tensor的值等于loss1的值,但他们也仅仅是值相等,get_total_loss这个op的输出的tensor不等于loss1,他们仍然是两个tensor,你可以打印一下,他们的名字不同。(可以理解为定义了一个op,这个op的作用就是读取并输出loss1的结果)
  3. val_loss的生成是因为我又定义了一个sparse_softmax_cross_entropy这个op,val_loss作为这个op的输出,然后我再次get_total_loss获取全部的loss的和,叫做all_loss2。那么问题来了,现在我有两个loss了,all_loss1的值等于loss1的值,还是loss1的值和val_loss的值的和?all_loss2又是什么?答案是,all_loss1的值还等于loss1的值,all_loss2的值等于loss1的值和val_loss的值的和。这是因为all_loss1是第一个get_total_loss这个op的输出,这个op在被丢到graph上的时候,此时graph上只有一个loss相关的op,因此他只能获取了一个loss,还记得最开始说的么,这个get_total_loss 的op会以这唯一的一个loss为输入,然后输出一个tensor,也就是all_lose1。然后后来又定义了一个loss,图上现在有两个代表loss的op了,那么第二个all_loss2在定义的时候,相当于去找这两个loss的op的输出,然后求和,作为这第二个的get_total_loss的输出。

想这个问题的时候,请想象一个图,然后在上面扔op

tf.reset_default_graph()

x = [[1.,2.],[-1.,3.]]
y = [1,0]
weights = tf.Variable([[1,-1],[-1,-1]],name='weights',dtype=tf.float32) #虽然是定义了一个Variable类,但是也相当于在设置了一个op
bias = tf.get_variable('bias',shape=[2],initializer=tf.zeros_initializer) #get_variable也是一个op

mat_res = tf.matmul(x,weights) #定义矩阵乘法计算,也是一个op,目前图上有3个op
add_res = tf.add(mat_res,bias) #定义矩阵相加计算,也是一个op,目前图上有4个op
print(add_res) #Tensor("Add:0", shape=(2, 2), dtype=float32)
y_ = tf.nn.sigmoid(add_res) #定义sigmoid运算,也是一个op,目前图上有5个op

loss1 = tf.losses.sparse_softmax_cross_entropy(labels=y,logits=y_) #定义sigmoid运算,也是一个op,目前图上有6个op
all_loss1 = tf.losses.get_total_loss() #获取全部损失,也是一个op,目前图上有7个op
print(all_loss1) #Tensor("total_loss:0", shape=(), dtype=float32)

val_x = [[4.,-2.]]
val_y = [0]
mat_res = tf.matmul(val_x,weights) #定义矩阵乘法计算,也是一个op,目前图上有8个op
add_res = tf.add(mat_res,bias) #定义矩阵相加计算,也是一个op,目前图上有9个op
print(add_res) #Tensor("Add_1:0", shape=(1, 2), dtype=float32)
y_ = tf.nn.sigmoid(add_res) #定义sigmoid运算,也是一个op,目前图上有10个op
val_loss = tf.losses.sparse_softmax_cross_entropy(labels=val_y,logits=y_) #定义sigmoid运算,也是一个op,目前图上有11个op
all_loss2 = tf.losses.get_total_loss() #第二次获取全部损失,也是一个op,目前图上有12个op
print(all_loss2) #Tensor("total_loss_1:0", shape=(), dtype=float32)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    print(sess.run([loss1,all_loss1,val_loss,all_loss2]))

补充:张量、op、variable的关系
看下面的代码,通过前三个打印可以看出,lrassign都是Tensor,前面说过,Tensor是对数据的引用,那是对什么数据的引用呢?
lr为例,他是对exponential_decay_learning_rate:0的引用,exponential_decay_learning_rate就是一个op,也就是一个操作,那么lr实际上指的是,exponential_decay_learning_rate这个op的第0个输出
而从print(tf.global_variables())可以看出,lr并不是一个variable,他只是某个操作的输出。
同理看assign,他也是一个Tensor 下面的print(sess.run(assign))执行,做的实际上就是,执行Assign这个op,然后获取它的第0个输出。

def test_lr():
    global_step = tf.train.get_or_create_global_step()
    assign = tf.assign(global_step, 10)
    lr = tf.train.exponential_decay(0.001, global_step, 500, 0.98, staircase=False,
                                    name='exponential_decay_learning_rate')
    saver = tf.train.Saver()

    print(lr)  # Tensor("exponential_decay_learning_rate:0", shape=(), dtype=float32)
    print(assign)  # Tensor("Assign:0", shape=(), dtype=int64_ref)
    print(tf.global_variables())  # [<tf.Variable 'global_step:0' shape=() dtype=int64_ref>]
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        print(sess.run(assign))  # 10
        print(sess.run(global_step))  # 10
        print(sess.run(lr))  # 0.0009995961
        saver.save(sess, 'test/test', global_step=global_step)

如果我们把刚刚存好的ckpt重新读取进来,会发现global_step被设置为10了,这没问题,但lr是0.004997,这是因为lr并不是一个variable,存储的时候只会存储variable,并不会把lr存进去,他只是个tensor。就像刚刚说的,run一个tensor的时候,做的就是执行op然后返回值,执行的时候用到了global_step,然后根据此时的参数计算lr返回(0.005)。
(另,其实tensor与variable的区分并不明确,简单理解的话,variable就是变量,比如系数啊bias之类的,tensor就是某些op的输出)

def load_lr():
    global_step = tf.train.get_or_create_global_step()
    lr = tf.train.exponential_decay(0.005, global_step, 500, 0.98, staircase=False,
                                    name='exponential_decay_learning_rate')
    saver = tf.train.Saver()
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        saver.restore(sess, 'test/test-10')
        print(sess.run(global_step))  # 10
        print(sess.run(lr))  # 0.00499798

总结
所以看到这里可以再深入想想,执行sess.run()的时候,tensorflow到底帮我们干了什么?对,它帮我们做的事情就是让tensor去flow,就比如我在执行sess.run(lr)的时候,我就是想获取lr这个tensor的值,那怎么才能获取他的值呢,tensorflow做了下面几件事,让global_step作为输入,喂给exponential_decay这个op,然后让这个op去执行和计算,并且将真实的值返回出来。