Tensorflow简介

接触Tensorflow也有好一段时间了,但总是对这个项目庞大的代码量和复杂的设计结构望而生畏,一直觉得它可以和某些原生的编程语言相提并论,所以大多时候也都是照着一些现成的模板去套用然后再不断调试,对内部的一些基本概念其实并没有太多了解。最近想着好好把官方给的get started和tutorial过一遍,对以后使用应该也更有帮助。

Tensors

张量就是高维数组的另一种表达形式,数组是几维的,张量的rank就是几。比如[1,2,3]是一维向量,rank=1;[[1,2,3], [2,3,4]]是二维矩阵,rank=2;[[[1,2,3], [2,3,4]]]是三维张量,rank=3,就这么简单。

Computational Graph

有了tensor,要形成flow,就必须借助图,张量和图起来就基本构成了Tensorflow的基本框架。图中的每个node可以是tensor,也可以是加减乘除之类的算子。其实回想一下,神经网络本质上就是这种神经元之间互相有向连接的结构,所以在我们定义好每个node后把这些node连接成图,理论上就可以搭建任何模式的结构。但是如果要查看某个图最终node的输出结果,还必须要通过一个session的运行环境来run。下面举个简单的小例子就会一目了然:

1
2
3
4
5
6
7
8
9
10
11
node1 = tf.constant(3.0, tf.float32)
node2 = tf.constant(4.0) # also tf.float32 implicitly
print(node1, node2)
#Output: Tensor("Const:0", shape=(), dtype=float32) Tensor("Const_1:0", shape=(), dtype=float32)
sess = tf.Session()
print(sess.run([node1, node2]))
#Output:[3.0, 4.0]
node3 = tf.add(node1, node2)
print("node3: ", node3)
print("sess.run(node3): ",sess.run(node3))
#Output:7.0

上面的code只是把node1和node2定义为tf.constant()的常量形式,更普遍的我们将输入定义为类似于函数形式的变量,这时就需要利用到tf.placeholder()方法,可以理解为占位符,这类似于python中lambda匿名函数的形式,占位符就是传入匿名函数的输入参数,再看例子:

1
2
3
4
5
6
7
8
9
10
a = tf.placeholder(tf.float32)
b = tf.placeholder(tf.float32)
adder_node = a + b # + provides a shortcut for tf.add(a, b)
print(sess.run(adder_node, feed_dict = {a: 3, b:4.5}))
#Output:7.5
print(sess.run(adder_node, feed_dict = {a: [1,3], b: [2, 4]}))
#Output:[ 3. 7.]
add_and_triple = adder_node * 3.
print(sess.run(add_and_triple, feed_dict = {a: 3, b:4.5}))
#Output:22.5

上面提到的tensor都是不可变,但是在大多machine learning的应用中需要通过不断的迭代来调整参数,比如网络中的weight和bias,这时需要利用tf.Variable()来给定初始值并定义这些需要train的变量。有了trainable的参数和输入数据,最后一步就是定义loss,我们以linear regression为例,定义square loss 为$loss = (Wx + b - \hat y)^2$,大多数常见的loss在Tensorflow中都已经被封装好,直接调用即可。接下来的代码相对来说复杂一点,但逻辑在Tensorflow的框架下十分清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
W = tf.Variable([.3], tf.float32) #Trainable param
b = tf.Variable([-.3], tf.float32) #Trainable param
x = tf.placeholder(tf.float32) #Input placeholder
y = tf.placeholder(tf.float32) #Input placeholder
linear_model = W * x + b # Linear Regression
squared_deltas = tf.square(linear_model - y) #Define square loss
loss = tf.reduce_sum(squared_deltas) #Add loss on all samples
init = tf.global_variables_initializer() # You must explicitly initilize the variables
sess.run(init) # Until we call sess.run, the variables are uninitialized
print(sess.run(loss, {x:[1,2,3,4], y:[0,-1,-2,-3]}))
#Output:23.66

Train

到现在为止,我们还只是简单的手动构建了图,做了几个简单运算,还没有涉及到神经网路的梯度反向传播、参数更新过程。Tensorflow的强大之处在于,在用户构建完成网络结构(图)后,就可以通过简单的函数调用来自动计算梯度。这里我们以最常见的梯度下降为例:

1
2
3
4
5
6
7
8
9
optimizer = tf.train.GradientDescentOptimizer(0.01) # define gradient descent method
train = optimizer.minimize(loss) # minize loss using gradient descent
sess.run(init) # reset values to incorrect defaults.
for i in range(1000): # run gradient descent for 1000 steps
sess.run(train, {x:[1,2,3,4], y:[0,-1,-2,-3]})
print(sess.run([W, b]))
#Output:[array([-0.9999969], dtype=float32), array([ 0.99999082],dtype=float32)]

Reading Data

Tensorflow读取数据的方式大体上说有三种,第一种是和上述例子中一样的方式,先在图中定义好placeholder,再通过feed_dict方式在run session时传入placeholder对应的数据;第二种适用于小数据量,直接将数据定义为constant或variable,个人感觉本质上和第一种没有太大区别;第三种在数据量比较大时候十分常用,就是直接从文件中按照队列的方式进行读取,以目前一般企业级机器学习的数据量来看,前两种方式会受限于内存和图的大小,因此简单介绍下这种比较通用的方式。

首先将要读取的小文件名形成一个列表,然后通过tf.train.string_input_producer函数建立一个关于这些文件名的队列queue。接下来会有若干个reader实体去从这个queue中读取文件名,然后对于不同形式的文件(比如csv、二进制等)再通过不同的decoder对当前文件名下的内容进行解析,这样就可以不断的从文件列表中的每个文件读数,下面是一个涉及到batch read的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def read_my_file_format(filename_queue):
reader = tf.SomeReader() # define reader
key, record_string = reader.read(filename_queue) # read file from file_queue using reader, each execution of read reads a single line from the file
example, label = tf.some_decoder(record_string) # decode file
processed_example = some_processing(example) # do some preprocessing
return processed_example, label
def input_pipeline(filenames, batch_size, num_epochs=None):
filename_queue = tf.train.string_input_producer(
filenames, num_epochs=num_epochs, shuffle=True)
example, label = read_my_file_format(filename_queue)
min_after_dequeue = 10000 # define how big a buffer we will randomly sample from
capacity = min_after_dequeue + 3 * batch_size # larger than min_after_dequeue
example_batch, label_batch = tf.train.shuffle_batch(
[example, label], batch_size=batch_size, capacity=capacity,
min_after_dequeue=min_after_dequeue) # make batch sample
return example_batch, label_batch

一般来说牵扯到了queue,我们就必须要在tf.Session()中在任务run之前利用tf.train.start_queue_runners和tf.train.Coordinator来填充队列和管理线程,不然read进程就会因为从queue中等待读取文件名而发生阻塞。

上面只是对tf做了一个非常非常浅显的介绍,但烂熟于心之后基本上也可以无障碍阅读github上大多数的tensorflow tutorial,而在实际的程序中依然有大量的坑和N多封装好的方法等待被发掘。