Tensorflow基础API与使用技巧总结(最快学会使用TF)

前言

硕士阶段tensorflow、pytorch双修。开始使用tensorflow基础api复现过各种神经网络网络算法,包括:各种网络结构如DenseNet,基础api撸出来的反响传播,非常规训练算法BinaryNet, FTPROP, 网络修剪Taylor Pruning。之后实习的公司使用Pytorch,所以转了pytorch。也有快半年没使用tensorflow了,复习总结下tensorflow的技巧,供以后参考。

一提到Tensorflow,可能都会想到深度学习。其实,tensorflow只是一个可以调用GPU并行计算的库,还有很多其它用途(比如计算大量数据的协方差矩阵等,MATLAB+cpu花费一天,而用tensorflow+gpu只要一小时)。因此,本文虽然描述了tensorflow如何应用在深度学习实验上,但是会从tensorflow与计算图的角度阐述。

本文完全不涉及keras、slim等高层封装,不使用Optimizer等训练器封装,完成使用最底层API。这样才能更好体会深度学习框架下计算图的概念。

用到的API大致包括:

矩阵定义
tf.placeholder输入源
tf.get_variable源
tf.ones等 源

矩阵运算
tf.reshape, tf.matmul等
tf.nn.conv2d等

求导
tf.gradients

赋值操作
tf.global_variables_initializer
tf.assign

命名域(我主要它用来实现根据name获得variable指针)
tf.variable_scope

会话
tf.Session()

嗯,差不多了,会这些就可以用tensorflow搭建任何模型复现任何算法了。

建立一个计算图

计算图听上去很酷,其实我认为就是指一个比较固定的计算过程。计算图中有几个概念需要非常清楚,才能较好地使用tensorflow:源(Variable, placeholder)、非源(Tensor)与操作(op)

这里我先随便给出一些运算

y = x + b*b
z = 2*y + 10

如果我想将x=1, x=3等等进行上述运算得到对应的z,那么就可以建立一个运算图,其中:1) x是个placeholder,用来放入不同的值,是个源;2)b是个常数,是个源;3)z、y是代表运算结果的Tensor,非源;

建立完计算图,通过session会话去运行它。这里要注意用tf.global_variables_initializer()来真正初始化b。定义b时用的initializer=tf.zeros_initializer只是表明初始化时赋为0,没有真正初始化b。

import tensorflow as tf
	
# 建图
x = tf.placeholder(dtype=tf.float32, shape=[], name="x_name")
b = tf.get_variable("b_name", shape=[], dtype=tf.float32, initializer=tf.ones_initializer)

y = x + b*b
z = 2*y*y + 10

# 会话
sess = tf.Session()

# 初始化
sess.run(tf.global_variables_initializer())

# 计算z。feed_dict是个字典,可以给各个placeholder赋值
#(技巧!)其实也可以给variable赋值。但是给variable赋值,不更新varibale参数,只是参与这次的运算。
out_z = sess.run(z, feed_dict={x: 2})
print(out_z)

# b是源,不需要输入数据就可以取值
out_b = sess.run(b)
print(out_b)

out_z = sess.run(z, feed_dict={x: 2, b: 100})
print(out_z)

# b没变吧
out_b = sess.run(b)
print(out_b)

可以发现,如果源没有值(如placeholder),非源的Tensor将也没有值。在神经网络应用中,placeholder一般就是输入数据(一般还有标签),Variable就是可训练参数与可变参数(如bn中的移动平均值),Tensor就是各层的运算结果(如各层feature maps)。

算个梯度呗

好了,现在会建图,并通过会话输入x得到z了。现在来计算z对b的梯度。
接着上面的程序写:

# 建图
# 一般y为标量,x为矩阵,那么求得的导数grad_x=tf.gradients(y, x)是个和x shape一样的Tensor。
#
# (!技巧) 好奇宝宝会问,y是矩阵呢??你得到的grad_x还是和x的shape一样!
# 此时你应该会需要设置grad_ys,tf.gradients(y, x, grad_ys=a_grad_ys)
# grad_ys与y的shape一致
# 
# 让我们这样理解。有某个标量z。
# a_grad_ys = tf.gradients(z, y)
# grad_x = tf.gradients(y, x, grad_ys=a_grad_ys)
# 得到的grad_x与tf.gradients(z, x)是等价的

grad_b = tf.gradients(z, b)

# 会话计算
out_grad_b = sess.run(grad_b, feed_dict={x: 2})
print(out_grad_b)

求导算下,x=2时dz/db=dz/dydy/db=43*2=24。运行结果24.0。

来点(骚)操作(OP)

求导一般都是为了根据梯度更新参数。现在,根据梯度对b进行参数更新。假设x所有可能的取值是[1,2,3,4,5],希望改变b,使得得到的z变小。
继续接着上面的代码写:

learning_rate = 0.0005
x_val = [1,2,3,4,5]

# 建图
op_update_b = tf.assign(b, b - learning_rate*out_grad_b[0])

# 看看更新前的z取值
for a_x_val in x_val:
	print(sess.run(z, feed_dict={x: a_x_val}))

# 跑10个epoch
for _ in range(10):
	for a_x_val in x_val:
		sess.run(op_update_b, feed_dict={x: a_x_val})

# 看看更新后的z取值
for a_x_val in x_val:
	print(sess.run(z, feed_dict={x: a_x_val}))

# 看看b从1.0变成几了
print(sess.run(b))

可以看到,更新前z是[18.0, 28.0, 42.0, 60.0, 82.0],更新b后z是[12.691196, 19.331192, 29.97119, 44.61119, 63.251186],如预期那样变小了。此时b从1变成了0.39。理论推导我们知道,b=0时z变得最小。

这里,在建图的时候多了tf.assign(ref, value)。然后通过会话run这个操作即可实现更新。因为这里tf.assign()的第二个参数用到非源Tensor: out_grad_b,因此会话运行时需要通过feed_dict送入参数。如果操作里没有非源变量,可以直接run:

op_ones_b = tf.assign(b, 1)

sess.run(op_ones_b)
print(sess.run(b))

关于tf.Session()

session会话,在运行网络的时候,需要注意:

  1. 第一个参数是要获取的Tensor、Variable或Op,可以是个list。第二个参数是feed_dict,如果第一个参数里不涉及非源变量,feed_dict可以不用赋值。
  2. session的运行逻辑是:由于所有的源变量的值已经确定了(placeholder输入的值与Variables或constants的值都是确定的),那么所有Tensor的值也确定的,所以session只会“算一遍”。

为了更好理解,运行下面的代码

op_plus_b = tf.assign(b, b+1)

sess.run([op_plus_b, op_plus_b])
print(sess.run(b))

可以发现,b的值无论run了多少个op_plus_b,依然等于2。下面的情况一样得到的是2。

sess.run(op_ones_b)

op_plus_b_1 = tf.assign(b, b+1)
op_plus_b_2= tf.assign(b, b+1)

sess.run([op_plus_b_1, op_plus_b_2])
print(sess.run(b))

也就是说,run多个相同的OP,只会执行一次。

如果一个+1一个+10呢?这就是两个不同的OP对同一个Variable进行操作

sess.run(op_ones_b)

op_plus_b_1 = tf.assign(b, b+1)
op_plus_b_2= tf.assign(b, b+10)

sess.run([op_plus_b_1, op_plus_b_2])
print(sess.run(b))

结果是,有时候是2,有时候是11,有时是12

因为tensorflow是个并行框架,所以会出现只加了1或只加了10或两个都加了。
所以,不同OP对同一个Variable进行更新时,最好分两个sess.run,或者使用tf.control_dependencies

零碎的技巧:tf.variable_scope

好了,掌握以上的API,足够使用Tensorflow实现大部分算法与网络了,完全可以不用tensorflow高层封装的layer、Optimizer。对于比较复杂的算法逻辑可能还需要tf.cond条件判断,这里不对其进行描述。

这里再详细描述一个API:tf.variable_scope。一般是为了在tensorboard可视化是,分块用的。但是我用它来根据Variable的name获取Variable的指针:

with tf.variable_scope('', reuse=True):
	bbb = tf.get_variable('b_name')
print(id(b), id(bbb))

可以看到,bbb和b的地址是一致的。

后记

虽然不得不承认,pytorch的开发效率要比tensorflow高上很多,而且tensorflow的高层封装API混乱。但是,tensorflow计算图的概念更加清晰,我在之后的博文里会描述,pytorch的计算图概念其实非常的薄弱,不利于开发者实现一些新颖的算法。

你可能感兴趣的:(算法,tensorflow,深度学习,神经网络)