Tensorflow: How to read data 怎样优雅地用tensorflow读取数据
为了让研究者更专注地研究模型本身,Tensorflow为我们提供了tf.estimator这样一个高级封装,它封装了会话管理,计算图持久化等诸多细节,可以使我们专注于编写model_fn这个模型函数中来。 而这时我们怎样向模型中输入数据呢?这是就是我们的中级封装tf.data 与tensorflow特有文件结构TFRecords大显身手的时候了。tf.data将数据读取的过程加入了计算图之中,这样就可以享受谷歌工程师为大家带来的优化。而TFRecords作为二进制流文件,配套的tf.data可以选用文件缓存,这样在数具规模较大时就可以免去爆掉内存的担忧
具体的讲解请详见官网教程https://www.tensorflow.org/tutorials/load_data/tf_records和https://www.tensorflow.org/guide/datasets_for_estimators都有中文版。如果无法登录的朋友请尝试使用境外服务器。这里我结合一些实例讲解一下整个过程,以及一些坑和小技巧。我的tensorflow版本是1.12的CPU版本,使用python3.5作为编程语言,操作系统是Ubuntu 18.04, python环境是Anaconda3。在这里我强烈建议各位博主和开源软件工程师们在发布自己的教程或作品时标注清楚运行环境,并不是每个人都是大神,还是有很多人卡在了版本不同,各种包缺少或冲突或链接有问题等看似不是大问题却的确很杀耐心的“小’'问题上,详见我的Moses安装过程。
实例示范
这段代码是我给自己写的标程,每当设计新模型的时候直接复制粘贴过来,小修小补即可。希望看本教程的朋友能耐心把这段代码看一下
# main.py
import sys
import os
import tensorflow as tf
import extra_capsnets as model
TRAIN_FILE = "data/train.tfrecord"
TEST_FILE = "data/test.tfrecord"
MODEL_DIR = "model_saved"
BATCH_SIZE = 64
PIC_HEIGHT = 28
PIC_WIDTH = 28
PIC_CHANNEL = 1
def parse_element(example):
feature = tf.parse_single_example(example, {
"pic": tf.VarLenFeature(tf.int64),
"label": tf.VarLenFeature(tf.int64)
})
labels = tf.sparse.to_dense(feature["label"])
del feature["label"]
feature["pic"] = tf.to_float((tf.sparse.to_dense(feature["pic"])))
ret_turple = (feature, labels)
return ret_turple
def train_input_fn(batch_size=1):
train_data = tf.data.TFRecordDataset(TRAIN_FILE)
train_data = train_data.map(parse_element).repeat().batch(batch_size)
iterator = train_data.make_one_shot_iterator()
next_batch = iterator.get_next()
return next_batch
def main(_):
#GPU Setting
os.environ["CUDA_VISIBLE_DEVICES"] = sys.argv[1]
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
config.allow_soft_placement = True
run_config = tf.estimator.RunConfig().replace(session_config=config)
#Estimator Setting
feature_columns = list([])
feature_columns.append(tf.feature_column.numeric_column("pic", [784], dtype=tf.float32))
params = {}
params["feature_columns"] = feature_columns
params["pic_size"] = [-1, PIC_HEIGHT, PIC_WIDTH, PIC_CHANNEL]
capsule_classifier = tf.estimator.Estimator(model_fn=model.model_fn, config=run_config,
params=params, model_dir=MODEL_DIR)
#Training
capsule_classifier.train(lambda: train_input_fn(BATCH_SIZE), steps=int(sys.argv[2]))
if __name__ == "__main__":
tf.app.run()
还是先从主函数看起。首先我完成了对GPU的配置,指定了使用的GPU,弹性显存分配以及弹性计算分配。这并不在本篇教程中。然后开始设置estimator。estimator一般由模型函数model_fn和输入参数params为主体。之是训练阶段,这时主要的部分是输入函数input_fn()。与本教程有关的,是输入函数input_fn()和输入参数params中的特征列feature_columns。
先看上面的输入函数。这里实现的是训练用的输入函数train_input_fn()。在当前版本中,它可以有两种返回形式。首先要提的是,在estimator的模型函数中,头两个参数分别是装满了各种特征的feature字典和一个label张量。在调用estimator的train方法时会将输入函数的返回值分成这两个部分传进去。所以显然,输入函数需要返回一个装满特征的字典和一个标签张量。具体怎样操作的呢?这里首先我建立了一个从源TFRecord中提取数据的dataset,接着对其中的二进制数据进行了解析。将源格式转换成estimator能用的形式就在解析这步完成。对应代码中的就是**parse_element()**函数。
参数example代表制作数据时一个最小的封装单位。看不懂这句话的朋友请移步至如何制作TFRecord文件。我封装的形式是**"pic"对应一个整数列表,label对应一个整数列表,用稀疏形式VarLenFeature(tf.int64)把二进制文件格式转化为我们能看懂的整数形式,再将其转换成稠密向量sparse.to_dense**。因为我们要返回一个特征字典+一个标签列表的形式,所以把label从feature字典中删去,单拿出来,然后和剩下的字典扣成一个元组就好了。
特征列在主函数中。其实就是一个列表,其中指明了对feature字典中各个特征的处理方式。我这里使用了最简单的处理方式:numeric_column,直接原封不动把数据送给我就好了。还要好多的处理方式,等待各位朋友去官网自行查阅。
一些小技巧
-
如何解析长度不等的序列?
这是我们就不要使用FixLengthFeature去解析数据了,我们应该使用VarLenFeature。它返回的是一个稀疏张量**(Sparse Tensor),这时候我们就需要使用sparse.to_dense**函数去将其转化为我们正常使用的稠密张量。下面是实例代码
def parse_element(example): feature = tf.parse_single_example(example, { "ori_str": tf.VarLenFeature(tf.int64), "tar_str": tf.VarLenFeature(tf.int64) }) labels = tf.sparse.to_dense(feature["tar_str"]) del feature["tar_str"] feature["ori_str"] = tf.sparse.to_dense(feature["ori_str"]) ret_turple = (feature, labels) return ret_turple
这是一个正常的为tf.data解析example的函数。我传入的example中有两个整数序列,ori_str和tar_str,这里我们先用VarLenFeature将其解析出来,再转化成稠密张量,最后和其他解析函数一样返回**(feature, label)** 元组。
-
如何把长度不等的序列送入特征**(Feature Column)**中
能提出这个问题说明我们都没用pad之后的不定长的一维序列。这里需要提到的一点是,当你在设置dataset的batch时如果你设为1,那么返回的也是一个1维的张量,并不会升维。
解决这个问题就需要提到tensorflow一个很重要的特性,它在不使用TPU时处理batch_size经常是将其处理维动态维度,利用这一点我们将输入数据reshape成**[-1, 1]维的向量就行了,把不确定的长度伪装成batch_size**。下面是示例代码。
def parse_element(example): feature = tf.parse_single_example(example, { "ori_str": tf.VarLenFeature(tf.int64), "tar_str": tf.VarLenFeature(tf.int64) }) labels = tf.sparse.to_dense(feature["tar_str"]) del feature["tar_str"] feature["ori_str"] = tf.reshape(tf.sparse.to_dense(feature["ori_str"]), [-1, 1]) ret_turple = (feature, labels) return ret_turple
借用技巧1中的代码,只不过将其中的feature调整了下形状。相应的,我们的特征列写成这样:
feature_columns = list([]) feature_columns.append(tf.feature_column.numeric_column("ori_str", dtype=tf.int64))
即可。
-
特征列怎么把我的数据类型给改了?
特征列没改,你用来接特征的tf.feature_column.input_layer函数偷偷改了。
在tf.feature_column.input_layer文档的最后几行中,它提到,会将所有的数据都偷偷地转化为tf.float32类型。是不是很坑?但是的确比较数据安全。
未完待续。。。