TensorFlow变量共享和数据读取

1、变量共享

  前面已经说过如何进行变量的生成和初始化内容,也用到了命名空间的概念,这里说一下什么是变量共享。当我们有一个非常庞大的模型的时候免不了需要进行大量的变量共享,而且有时候还希望能够在一个地方初始化所有的变量,这就需要tf.variable_scope() 和 tf.get_variable()。 
  当只有两层的卷积的时候,前面的程序都是定义了两个卷积变量W1和W2(忽略b),而且简单的命名了。如下面:

def my_image_filter(input_images):
    conv1_weights = tf.Variable(tf.random_normal([5, 5, 32, 32]),
        name="conv1_weights")
    conv1_biases = tf.Variable(tf.zeros([32]), name="conv1_biases")
    conv1 = tf.nn.conv2d(input_images, conv1_weights,
        strides=[1, 1, 1, 1], padding='SAME')
    relu1 = tf.nn.relu(conv1 + conv1_biases)

    conv2_weights = tf.Variable(tf.random_normal([5, 5, 32, 32]),
        name="conv2_weights")
    conv2_biases = tf.Variable(tf.zeros([32]), name="conv2_biases")
    conv2 = tf.nn.conv2d(relu1, conv2_weights,
        strides=[1, 1, 1, 1], padding='SAME')
    return tf.nn.relu(conv2 + conv2_biases)

  但是假设你在reuse这个模型的时候如果有两个图像都调用my_image_filter就会有4*2个变量产生。用全局变量进行参数表示当然可以,但是却破坏了代码封装性,于是用tensorflow的变量作用域(Variable Scope)概念可以解决这个问题。这不得不用到两个函数:

  • tf.get_variable(name, shape, initializer): Creates or returns a variable with a given name.建立并返回一个给定名称的变量
  • tf.variable_scope( scope_name): Manages namespaces for names passed to tf.get_variable(). 管理变量命名空间忽略get_variable定义的变量,而name_scope是所有的都会放到其命名空间中见这里

      如何用这两个方式实现变量共享呢?方法如下假设被调用的函数为:

  def my_image_filter(input_images):
    with tf.variable_scope("conv1"):
        # Variables created here will be named "conv1/weights", "conv1/biases".
        relu1 = conv_relu(input_images, [5, 5, 32, 32], [32])
    with tf.variable_scope("conv2"):
        # Variables created here will be named "conv2/weights", "conv2/biases".
        return conv_relu(relu1, [5, 5, 32, 32], [32])

只要用到reuse_variables()进行注释即可,方法如下:

with tf.variable_scope("image_filters") as scope:
    result1 = my_image_filter(image1)
    scope.reuse_variables()
    result2 = my_image_filter(image2)

至于为什么会这样,建议参考官方文档,因为后面还会用到所以这里只是大概了解一下。


2、线程(Threading)和队列(queues)

  这个是一个基本概念,也是后续数据读取的基础。这里会对相关概念进行一下详细的分析,包括队列、协调器(Coordinator)、QueueRunner。

(1)线程和队列使用的总览

  在TensorFlow中使这么描写队列的:Queues are a powerful mechanism for asynchronous(异步) computation using TensorFlow。队列其实也是一个节点,是个有点类似于variable的状态节点,通过入队(enqueue)和出队(dequeue)来维持一种状态。其中异步是一个很重要的内容,以数据读取为例:在输入的pipeline上,多线程可以有效地增加数据读取的效率,当我们用队列来准备用于训练的数据时可以: 
  * 多线程的准备训练数据,然后把它们push到队列中 
  * 训练线程生成一个一个训练operation来从数据队列中去除mini-batch数量的数据。 
  其实Session就是多线程的,所以TensorFlow可以并行运行,但是多线程有三个条件需要满足: 
  - 所有线程都需要能够同时停止 
  - 必须能够捕捉和报告异常 
  - 队列必须能在停止的时候适当关闭 
  TensorFlow给了我们两个类用于帮助我们协调线程:tf.Coordinatortf.train.QueueRunner。这两个类实际上是一起使用的,其中tf.Coordinator用于多线程停止和异常捕捉,tf.train.QueueRunner用于操作Tensor入队。

(2)主要的队列类

上面对线程和队列的作用进行了一些简单的介绍,我们常用的队列在TensorFlow中已经定义好了:tf.FIFOQueue和tf.RandomShuffleQueue,从名字就可以了解到两个队列的工作方式一个按顺序一个随机。这两个队列支持enqueue、enqueue_many和dequeue操作,这些操作内容也能直接理解出来。最长见的操作就是一次性读入数据,然后一个一个的出队,这里注意是没有dequeue_many操作的,如果想一次性得到多个输出需要使用tf.train.batch或是tf.train.shuffle_batch。 
  这里还有一个在sequence-to-sequence上比较常用的tf.PaddingFIFOQueue也就是带变量填充的队列,而且它支持dequeue_many。 
  我们可以自己定义一个队列,其常用的参数有min_after_dequeue(在dequeue后队列中剩余的最小数目的元素)、bounded capacity(队列中的最大元素数)、shape of the elements in the queue(如果shape为None则元素可以是任何shape)。但是在实际中我们通常不会直接使用队列本身,而是使用string_input_producer(后面读取数据时会用到)。

(2)Coordinator

  前面说过协调器帮助多线程共同停止。其关键的方法有:

  • tf.train.Coordinator.should_stop:如果线程可以停止返回返回True
  • tf.train.Coordinator.request_stop : 请求多线程停止
  • tf.train.Coordinator.join:直到指定线程停止前进行等待

我们可以初始化一个Coordinator对象,然后制造一些线程进行测试。教程给的代码如下,一个线程申请申请停止,其他线程的should_stop就会变为True:

# Thread body: loop until the coordinator indicates a stop was requested.
# If some condition becomes true, ask the coordinator to stop.
def MyLoop(coord):
  while not coord.should_stop():
    ...do something...
    if ...some condition...:
      coord.request_stop()

# Main thread: create a coordinator.
coord = tf.train.Coordinator()

# Create 10 threads that run 'MyLoop()'这个是Python package创造的线程
threads = [threading.Thread(target=MyLoop, args=(coord,)) for i in xrange(10)]

# Start the threads and wait for all of them to stop.
for t in threads:
  t.start()#启动线程
coord.join(threads)

  显然coordinator可以管理许多线程做不同的事情,而不用像上面的例子那样都做同一件事。而且关于异常的捕获和抛出可以看tf.train.Coordinator的文档。

(4)QueueRunner

  QueueRunne类会生成一些线程反复的执行enqueue操作。然后这些线程可以利用Coordinator来停止这些线程。而且如果有异常报道至Coordinator时一个QueueRunner会自动运行一个closer thread来关闭线程。 
  下面可以自己使用queue runner来执行上面的结构: 
  1. 首先建立一个使用TensorFlow队列(如tf.RandomShuffleQueue)的图,接着增加一个Op用于处理数据并将其入队。 
  2. 建立一个queuerunner并行的运行4个线程进行入队,然后建立一个Coordinator让QueueRunner在这个Coordinator上开始线程,并在运行期间不断使用Coordinator监控停止情况。 
  这里没有用官方文档,因为里面有些东西有一些错误。用的是这里的程序。

N_SAMPLES = 1000#样本数
NUM_THREADS = 4#线程数
# Generating some simple data生成数据和标签
# create 1000 random samples, each is a 1D array from the normal distribution (10, 1)
data = 10 * np.random.randn(N_SAMPLES, 4)+1
# create 1000 random labels of 0 and 1
target = np.random.randint(0, 2, size=N_SAMPLES)
#建立一个队列
queue = tf.FIFOQueue(capacity = 50, dtypes=[tf.float32, tf.int32 ], shapes =[[4], []])
#增加Op用于数据入队出队
enqueue_op = queue.enqueue_many([data, target])
dequeue_op = queue.dequeue()
# create NUM_THREADS to do enqueue创建一个QueueRunner用于管理4个线程
qr = tf.train.QueueRunner(queue, [enqueue_op]*NUM_THREADS)
with tf.Session() as sess:
    # Create a coordinator, launch the queue runner threads.生成一个Coordinator启动线程
    coord = tf.train.Coordinator()
    enqueue_threads = qr.create_threads(sess, coord=coord, start=True)
    for step in xrange (100): #do to 100 iterations,执行100次循环进行出队操作,得到需要的数据
        if coord.should_stop():
            break
            data_batch,label_batch=sess.run(dequeue_op)#这里是一条一条的数据
    coord.request_stop()
    coord.join (enqueue_threads)

3、数据读取

  我们前面读取数据都是封装好的内容了,下面就打开封装看看tensorflow是如何读取数据的。上面的队列和线程这一部分内容是数据读取的基础内容,可以两部分对照着看。数据读取有三个来源:Python代码提供的(feeding)、从文件中读出来的、预加载数据。我们的内容主要集中在文件读取上。

(1)feeding

  这个我们遇到了几次,前面一直用的这种方法,先用python读数据再给tensorflow,需要配合Placeholder。python读数据可以用pickle结合os读取数据(这个看看Python就可以了)。但是这种方式读取数据却会出现一个问题就是数据是先读到我们的client手中然后再传入需要数据的地方(workers),当client和worker不在同一个机器上时就会非常慢,这种需要两次传导肯定会比较慢,可能单机的时候没那么明显而已。

(1)从文件读入

  从文件读入可以避免上面的问题,可以直接从数据存储的地方读到worker中。 
  这里有三个blog1(这个对数据读取的概念讲的很好)、blog2blog3讲的比较详细,我根据官方文档进行了一些补充。 
细,我根据官方文档进行了一些补充。

  1. 首先建立一个待读入的文件名列表。 
    有下面几种定义形式:[‘file0’, ‘file1’]、[(“file%d” % i) for i in range(2)],或者直接用python的os生成。
  2. 创建文件名队列 
    用tf.train.string_input_producer创建一个文件名队列,其有两个参数shffle(是否打乱文件名)和num_epoch(epoch需要的数量)。
  3. 定义一个reader 
    需要定义一个reader以便在文件名队列中读取数据。这时候由于有不同的数据格式存在,可以选择不同的reader,其中TextLineReader用的相对较多。而可以把Reader想象成一个每次返回不同值的Op(有点类似于Python的生成器),当我们使用Reader.read()的时候返回键值对,键用于鉴定文件名,值为对应的数据值。 
    • csv或txt格式:使用tf.TextLineReader,配合tf.decode_csv进行读取。每次读取一行,其中tf.decode_csv为一个op,把读出来的数据放入一个tensor列表中(其record_defaults参数用于填充缺失值)
    • bin格式:对于二进制格式可以用tf.FixedLengthRecordReader读取,在所有的文件都是固定长度的时候读取整个文件,可以配合tf.decode_raw操作(将一个string转为uint8的tensor),比如图片数据就可以这么读取,见这里
    • 标准tensorflow格式:不论什么数据都转为tensorflow的支持格式,推荐的格式是TFRecords file,然后再用tf.TFRecordReader进行读取,用 tf.parse_single_example进行解码,后面会提到。
    • 一次读取全部数据:tf.WholeFileReader,其read(queue, name=None)方法返回的(key, value)。key为文件名,value为文件对应的内容。有一个例子可以参加这里和这里。
    • 这里还有一个tf . ReaderBase帮助我们自定义reader。

这里以一个csv读取为例,我们命名为例1,完整程序见这里(源程序有一些函数已经过时如pack已经变为stack):

# 例1
filename_queue=tf.train.string_input_producer(["heart.csv"])
reader=tf.TextLineReader(skip_header_lines=1)
# it means you choose to skip the first line for every file in the queue
# 跳过第一行,比如第一行是一些特征名。
key,value=reader.read(filename_queue)

读出来的数据形式可能为:

key = data/heart.csv:2
value = 144, 0.01, 4.41, 28.61, Absent, 55, 28.87, 2.06, 63,1

  train.string_input_producer所创建的队列是一个封装好的FIFOQueue用于读取文件名,这部分的流程最好参考一下上面提到的blog1。然后产生一个Reader这相当于数据队列,所以为了运行这个队列需要tf.Coordinator 和 tf.QueueRunner。程序如下:

# 例1 续
filename_queue =tf.train.string_input_producer(filenames)
reader = tf.TextLineReader(skip_header_lines=1) # skip the first line in the file
key, value = reader.read(filename_queue)
with tf.Session() as sess:
    coord = tf.train.Coordinator ()
    threads = tf.train.start_queue_runners(coord=coord,sess=sess)#这个相当于前面的定义线程并开启
    print sess.run(key) # data/heart.csv:2
    print sess.run(value) # 144, 0.01, 4.41, 28.61, Absent, 55, 28.87, 2.06, 63,1
    coord.request_stop ()
    coord.join(threads)

  上面程序得到的value其实是一个string类型的,为了能得到可用的数据,比如上面的value包含9个特征和1个lable,我们需要使用decoder,decoder通常有两个参数,第一个是数据,第二个是默认数据。其中默认数据有两个作用,一个是指定value中的数据类型,还有一个就是在value中某一列为空时进行填充:

content=tf.decode_csv(value,record_defaults=record_defaults)

现在继续上面的例子:

# 例1 续
record_defaults = [[1.0] for _ in range(N_FEATURES)] # define all features to be floats
record_defaults[4] = [''] # make the fifth feature string,第四个特征为string类型
record_defaults.append([1])# 让标签变为整数
content=tf.decode_csv(value,record_defaults=record_defaults)

(3)预处理

  可以对上面产生的数据进行一下预处理。比如数据增强,列表切分等等。这里继续对上面的例子进行预处理,比如对某些数据进行处理并切割数据。

# 例1续
# convert the 5th column (present/absent) to the binary value 0 and 1
# 转换第5列中的string为一个0.0或1.0的常数值
condition=tf.equal(content[4],tf.constant('Present')) #判断是否出席返回falsetrue
content[4] = tf.select(condition,tf.constant(1.0),tf.constant(0.0))#这个的意思是对应位置为true就变为1.0
# pack all 9 features into a tensor
features=tf.stack(content[:N_FEATURES]) #这个函数用于打包tf.stack([x, y, z]) = np.asarray([x, y, z])
# assign the last column to label
label=content[-1]

(4)Batching

在结束pipeline之前需要另一个队列来把数据整合起来,可以利用tf.train.batch或tf.train.shuffle_batch。程序如下:

# 例1
# minimum number elements in the queue after a dequeue, used to ensure
# that the samples are sufficiently mixed
# I think 10 times the BATCH_SIZE is sufficient
# 10倍的batch_size作为出队后,队中最少的数据量,越大shuffle约充分但是速度慢占用空间大

min_after_dequeue=10*BATCH_SIZE

# the maximum number of elements in the queue队列中的最大的数据量,建议
# 值为min_after_dequeue + (num_threads + a small safety margin) * batch_size

capacity=20*BATCH_SIZE

# shuffle the data to generate BATCH_SIZE sample pairs
data_batch,label_batch=tf.train.shuffle_batch([features,label],batch_size=BATCH_SIZE,
        capacity=capacity,min_after_dequeue=min_after_dequeue)

这样就能使用data_batch和label_batch进行训练了。 
  其实还有一种读取方式速度更快一点,就是使用read_up_to的方式,这个在后面的补充部分会提到。

3、数据读取

例1、前面已经提到

例2、

  这里首先再增加一个读取二进制文件的例子。而这里说的二进制文件是TensorFlow自己的二进制文件格式TFRecords。TFRecords是一个序列化的tf.train.Example Protobuf对象,可以用几行代码就将一个图像转为一个二进制文档。大致流程是首先读取数据并把数据填充到一个Example protocol buffer中,然后序列化protocol buffer为一个string,并用tf.python_io.TFRecordWriter将这个string写入TFRecords 中。 
  这里用一个例子2来介绍:

#例2
#首先读取图像并转为byte字符串
def get_image_binary (filename):
    image=Image.open(filename)
    image=np.asarray(image,np.uint8)
    shape=np.array(image.shape,np.int32)#保存形状信息帮助复原
    # convert image to raw data bytes in the array.
    # 转换图像为byte
    return shape.tobytes(),image.tobytes() 

为了把上面生成的byte strings写入TFRecords需要使用tf.python_io.TFRecordWriter和tf.train.Features。

#例2
def write_to_tfrecord(label,shape,binary_image, tfrecord_file):
""" This example is to write a sample to TFRecord file. If you want to write more samples , just use a loop.
这个例子只是写入一个采样,想写入多组数据只需要用个循环
"""
    # 定义写入方法
    writer=tf.python_io.TFRecordWriter(tfrecord_file)
    # write label, shape, and image content to the TFRecord file
    # 正如所说的把数据填充到一个Example protocol buffer
    example=tf.train.Example(features=tf.train.Features(feature ={
        'label':tf.train.Feature(bytes_list=tf.train.BytesList(value=[label])),
        'shape':tf.train.Feature(bytes_list=tf.train.BytesList(value=[shape])),
        'image':tf.train.Feature(bytes_list=tf.train.BytesList(value=[binary_image]))
        }))
    #然后序列化protocol buffer为一个string,并写入
    writer.write(example.SerializeToString())
    writer.close()

  这样就已经将数据写入了TFRecord中了,当要读取数据的时候,需要TFRecordReader 和tf.parse_single_example,但是注意这里读取的数据是一个tensor,想得到值还需要eval。

#例2
def read_from_tfrecord(filenames):
    tfrecord_file_queue=tf.train.string_input_producer(filenames, name='queue')
    reader=tf.TFRecordReader()
    _ , tfrecord_serialized=reader.read(tfrecord_file_queue )
    # label and image are stored as bytes but could be stored as
    # int64 or float64 values in a serialized tf.Example protobuf.
    tfrecord_features=tf.parse_single_example(tfrecord_serialized,
            features ={
                'label' : tf.FixedLenFeature([],tf.string ),
                'shape' : tf.FixedLenFeature([],tf.string ),
                'image' : tf.FixedLenFeature([],tf.string ),
                }, name='features')
    # image was saved as uint8, so we have to decode as uint8.
    image = tf.decode_raw(tfrecord_features['image'], tf.uint8 )
    shape = tf.decode_raw(tfrecord_features['shape'], tf.int32 )
    # the image tensor is flattened out, so we have to reconstruct the shape
    image=tf.reshape(image,shape)
    label=tf.cast(tfrecord_features['label'],tf.string)#类型转换,因为只有一个值
    return label, shape, image

例3

程序和讲解在我的github上。

4.进阶补充

这里对上面用到的一些函数进行更详细的解释: 
  我们在线程和队列部分知道了需要一个QueueRunner生成一些线程反复的执行enqueue操作,并且显示的定义了一个queue和QueueRunner进行了一些操作,但是后面我们就直接用到了Reader和start_queue_runners这些内容有什么关系呢  

1. tf.train.start_queue_runners

  用于启动图中所有的queue runners,我们的图中包含两个queue runners一个是文件名队列,一个是内存中的数据的队列。其构造函数为:

start_queue_runners(
    sess=None,这个是执行队列操作Op的对话,默认是default session
    coord=None,可选:协调器用于协调起始线程
    daemon=True,是否线程标记为守护线程
    start=True,如果是FALSE就只创造线程而不启动他们
    collection=tf.GraphKeys.QUEUE_RUNNERS#指定queue runners的来源默认是GraphKeys.QUEUE_RUNNERS
)

2.tf.ReaderBase

  这个是所有Reader的基类,只要这个懂了就差不多都明白了。这个类简单的说是提供了将string(通常为filename)内容提取出来的功能,每次只能提取一条内容,但是一个文件有很多条内容而且我们又有很多的文件存在,所以为了更方便需要用一个队列来进行,队列中保存文件名然后在需要读取的时候用Reader出队。

3. batch用到的函数:

  在batch的时候遇到了tf.train.batch等函数,这里对这些函数的原理进行分析。这些函数在图中加入了一个queue用于聚合a batch of examples,它们还加了一个QueueRunner用于填充队列。

  • 其中tf.train.batch和tf.train.shuffle_batch是单线程的产生数据,或是在有一个单一子图用于生成数据的时候用N个线程来运行(N是能够保证队列满的数量)
  • tf.train.batch_join和tf.train.shuffle_batch_join用于多子图多线程的生成数据。

这些看上去不好分辨,看一下构造函数就明白了。 
 首先是tf.train.batch的构造函数:

batch(
    tensors,可以是列表或字典,这个函数的返回值和这个tensor中的值是一样
    batch_size,
    num_threads=1,入队所需的线程数
    capacity=32,队列中最大的element数量
    enqueue_many=False,这个表示上面输入的tensor是不是只是一个example
    shapes=None,
    dynamic_pad=False,是否对于不同shape的例子进行补充
    allow_smaller_final_batch=False,
    shared_name=None,
    name=None
)

接着是tf.train.batch_join:

batch_join(
    tensors_list,这个是tensor的元组或字典列表,列表中的一个元素和上面batch中的tensors是一样的,而启动的线程数量等于len(tensors_list)
    batch_size,
    capacity=32,
    enqueue_many=False,
    shapes=None,
    dynamic_pad=False,
    allow_smaller_final_batch=False,
    shared_name=None,
    name=None
)

4.一种更高效的数据读取方法read_up_to

  我们前面的模式都是Reader.read+train_batch的形式,但是这样还是和理想速度差很多。其实我们可以用read_up_to的方式提高读取效率。这是Reader中的一个方法,其构造函数为:

read_up_to(
    queue,文件队列
    num_records,读取数据的数量
    name=None
)
返回值是一个Tensor(keys,values)元组

其大概使用流程:

构造文件队列->构造reader对象->读取n条数据->返回数据

而read+train_batch流程为:

构造文件队列->构造reader对象->读取1条数据->将改数据加入队列->若数据队列长度大于n,从队列中返回结果


猜你喜欢

转载自blog.csdn.net/qq_21033779/article/details/80400754