将大型培训和测试文件流式传输到Tensorflow的DNNClassifier

时间:2017-08-23 00:01:30

标签: python csv tensorflow

我有一个巨大的培训CSV文件(709M)和一个大型测试CSV文件(125M),我希望在使用高级Tensorflow API的上下文中将其发送到DNNClassifier

input_fnfit接受的evaluate param似乎必须在内存中保存所有功能和标签数据,但我目前希望在本地计算机上运行此功能,因此,如果我将这些文件读入内存然后处理它们,那么它会很快耗尽内存。

我浏览了streamed-reading of data上的文档,但是用于读取CSV的示例代码似乎是针对低级Tensorflow API的。

并且 - 如果你原谅了一些抱怨 - 对于将训练有素的训练和测试数据文件发送到Estimator的琐碎用例来说似乎过于复杂......尽管如此在Tensorflow中训练和测试大量数据实际上需要复杂程度吗?

无论如何,我真的很感激使用高级API的方法,如果它甚至可能,我开始怀疑。

在探索之后,我确实找到了DNNClassifier#partial_fit,并尝试将其用于训练。

如何使用这种方法的例子可以节省一些时间,但希望我会在接下来的几个小时内偶然发现正确的用法。

然而,似乎没有相应的DNNClassifier#partial_evaluate ...虽然我怀疑我可以将测试数据分解成更小的部分并在每个批次上连续运行DNNClassifier#evaluate实际上可能是一个很好的方法,因为我可以将测试数据分成同类群组,从而获得每队列的准确性。

====更新====

简短版本:

  1. DomJack的建议应该是公认的答案。

  2. 然而,我的Mac的16GB RAM足以将整个709Mb训练数据集保存在内存中而不会崩溃。因此,当我最终部署应用程序时,我将使用DataSet功能,但我还没有将它用于本地开发工作。

  3. 更长的版本:

    我开始使用如上所述的partial_fit API,但每次使用时都会发出警告。

    所以,我去查看方法here的源代码,发现它的完整实现如下:

    logging.warning('The current implementation of partial_fit is not optimized'
                    ' for use in a loop. Consider using fit() instead.')
    return self.fit(x=x, y=y, input_fn=input_fn, steps=steps,
                    batch_size=batch_size, monitors=monitors)
    

    ......这让我想起了Hitchhiker's Guide的这个场景:

    Arthur Dent:如果我按下这个按钮会怎么样?

    Ford Prefect:我不会 -

    亚瑟登特:哦。

    Ford Prefect:发生了什么事?

    Arthur Dent:一个标志亮了起来,说'请不要再按此按钮'。

    这就是说:partial_fit似乎只是为了告诉你不要使用它而存在。

    此外,在训练文件块上迭代使用partial_fit生成的模型远小于在整个训练文件中使用fit生成的模型,这强烈表明只有最后{{1}培训块实际上是“拿走”。

2 个答案:

答案 0 :(得分:18)

查看tf.data.Dataset API。有许多方法可以创建数据集。我将概述四个 - 但您只需实施一个。

我假设您的csv个文件的每一行都是n_features个浮点值,后跟一个int值。

创建tf.data.Dataset

Dataset.from_generator

包装python生成器

最简单的入门方法是包装本机python生成器。这可能会出现性能问题,但可能适用于您的目的。

def read_csv(filename):
    with open(filename, 'r') as f:
        for line in f.readlines():
            record = line.rstrip().split(',')
            features = [float(n) for n in record[:-1]]
            label = int(record[-1])
            yield features, label

def get_dataset():
    filename = 'my_train_dataset.csv'
    generator = lambda: read_csv(filename)
    return tf.data.Dataset.from_generator(
        generator, (tf.float32, tf.int32), ((n_features,), ()))

这种方法非常通用,允许您独立于TensorFlow测试您的生成器函数(read_csv)。

使用Tensorflow Datasets API

支持tensorflow版本1.12 +,tensorflow数据集是我最喜欢的创建数据集的方式。它会自动序列化您的数据,收集统计信息,并通过infobuilder对象向您提供其他元数据。它还可以处理自动下载和提取,使协作变得简单。

将tensorflow_datasets导入为tfds

class MyCsvDatasetBuilder(tfds.core.GeneratorBasedBuilder):
  VERSION = tfds.core.Version("0.0.1")

  def _info(self):
    return tfds.core.DatasetInfo(
        builder=self,
        description=(
            "My dataset"),
        features=tfds.features.FeaturesDict({
            "features": tfds.features.Tensor(
              shape=(FEATURE_SIZE,), dtype=tf.float32),
            "label": tfds.features.ClassLabel(
                names=CLASS_NAMES),
            "index": tfds.features.Tensor(shape=(), dtype=tf.float32)
        }),
        supervised_keys=("features", "label"),
    )

  def _split_generators(self, dl_manager):
    paths = dict(
      train='/path/to/train.csv',
      test='/path/to/test.csv',
    )
    # better yet, if the csv files were originally downloaded, use
    # urls = dict(train=train_url, test=test_url)
    # paths = dl_manager.download(urls)
    return [
        tfds.core.SplitGenerator(
            name=tfds.Split.TRAIN,
            num_shards=10,
            gen_kwargs=dict(path=paths['train'])),
        tfds.core.SplitGenerator(
            name=tfds.Split.TEST,
            num_shards=2,
            gen_kwargs=dict(cvs_path=paths['test']))
    ]

  def _generate_examples(self, csv_path):
    with open(csv_path, 'r') as f:
        for i, line in enumerate(f.readlines()):
            record = line.rstrip().split(',')
            features = [float(n) for n in record[:-1]]
            label = int(record[-1])
            yield dict(features=features, label=label, index=i)

用法:

builder = MyCsvDatasetBuilder()
builder.download_and_prepare()  # will only take time to run first time
# as_supervised makes output (features, label) - good for model.fit
datasets = builder.as_dataset(as_supervised=True)

train_ds = datasets['train']
test_ds = datasets['test']

包装基于索引的python函数

上述缺点之一是使用大小为n的shuffle缓冲区对结果数据集进行混洗,需要加载n个示例。这将在您的管道中创建定期暂停(大n)或导致可能较差的混乱(小n)。

def get_record(i):
    # load the ith record using standard python, return numpy arrays
    return features, labels

def get_inputs(batch_size, is_training):

    def tf_map_fn(index):
        features, labels = tf.py_func(
            get_record, (index,), (tf.float32, tf.int32), stateful=False)
        features.set_shape((n_features,))
        labels.set_shape(())
        # do data augmentation here
        return features, labels

    epoch_size = get_epoch_size()
    dataset = tf.data.Dataset.from_tensor_slices((tf.range(epoch_size,))
    if is_training:
        dataset = dataset.repeat().shuffle(epoch_size)
    dataset = dataset.map(tf_map_fn, (tf.float32, tf.int32), num_parallel_calls=8)
    dataset = dataset.batch(batch_size)
    # prefetch data to CPU while GPU processes previous batch
    dataset = dataset.prefetch(1)
    # Also possible
    # dataset = dataset.apply(
    #     tf.contrib.data.prefetch_to_device('/gpu:0'))
    features, labels = dataset.make_one_shot_iterator().get_next()
    return features, labels

简而言之,我们只创建一个记录索引的数据集(或任何我们可以完全加载到内存中的小记录ID)。然后,我们对此最小数据集进行改组/重复操作,然后map通过tf.data.Dataset.maptf.py_func将索引转换为实际数据。有关用法,请参阅下面的Using with EstimatorsTesting in isolation部分。请注意,这需要您的数据可以按行访问,因此您可能需要将csv转换为其他格式。

TextLineDataset

您还可以使用csv直接阅读tf.data.TextLineDataset文件。

def get_record_defaults():
  zf = tf.zeros(shape=(1,), dtype=tf.float32)
  zi = tf.ones(shape=(1,), dtype=tf.int32)
  return [zf]*n_features + [zi]

def parse_row(tf_string):
    data = tf.decode_csv(
        tf.expand_dims(tf_string, axis=0), get_record_defaults())
    features = data[:-1]
    features = tf.stack(features, axis=-1)
    label = data[-1]
    features = tf.squeeze(features, axis=0)
    label = tf.squeeze(label, axis=0)
    return features, label

def get_dataset():
    dataset = tf.data.TextLineDataset(['data.csv'])
    return dataset.map(parse_row, num_parallel_calls=8)

parse_row函数有点复杂,因为tf.decode_csv期望批处理。如果在解析之前批量处理数据集,则可以使其更简单。

def parse_batch(tf_string):
    data = tf.decode_csv(tf_string, get_record_defaults())
    features = data[:-1]
    labels = data[-1]
    features = tf.stack(features, axis=-1)
    return features, labels

def get_batched_dataset(batch_size):
    dataset = tf.data.TextLineDataset(['data.csv'])
    dataset = dataset.batch(batch_size)
    dataset = dataset.map(parse_batch)
    return dataset

TFRecordDataset

或者,您可以将csv文件转换为TFRecord文件并使用TFRecordDataset。这是一个全面的教程here

步骤1:将csv数据转换为TFRecords数据。以下示例代码(请参阅上面read_csv示例中的from_generator)。

with tf.python_io.TFRecordWriter("my_train_dataset.tfrecords") as writer:
    for features, labels in read_csv('my_train_dataset.csv'):
        example = tf.train.Example()
        example.features.feature[
            "features"].float_list.value.extend(features)
        example.features.feature[
            "label"].int64_list.value.append(label)
        writer.write(example.SerializeToString())

这只需要运行一次。

步骤2:编写解码这些记录文件的数据集。

def parse_function(example_proto):
    features = {
        'features': tf.FixedLenFeature((n_features,), tf.float32),
        'label': tf.FixedLenFeature((), tf.int64)
    }
    parsed_features = tf.parse_single_example(example_proto, features)
    return parsed_features['features'], parsed_features['label']

def get_dataset():
    dataset = tf.data.TFRecordDataset(['data.tfrecords'])
    dataset = dataset.map(parse_function)
    return dataset

将数据集与估算器一起使用

def get_inputs(batch_size, shuffle_size):
    dataset = get_dataset()  # one of the above implementations
    dataset = dataset.shuffle(shuffle_size)
    dataset = dataset.repeat()  # repeat indefinitely
    dataset = dataset.batch(batch_size)
            # prefetch data to CPU while GPU processes previous batch
    dataset = dataset.prefetch(1)
    # Also possible
    # dataset = dataset.apply(
    #     tf.contrib.data.prefetch_to_device('/gpu:0'))
    features, label = dataset.make_one_shot_iterator().get_next()

estimator.train(lambda: get_inputs(32, 1000), max_steps=1e7)

单独测试数据集

我强烈建议您独立于估算工具测试数据集。使用上面的get_inputs,它应该像

一样简单
batch_size = 4
shuffle_size = 100
features, labels = get_inputs(batch_size, shuffle_size)
with tf.Session() as sess:
    f_data, l_data = sess.run([features, labels])
print(f_data, l_data)  # or some better visualization function

性能

假设您使用GPU来运行网络,除非csv文件的每一行都很庞大且您的网络很小,否则您可能无法发现性能上的差异。这是因为Estimator实现强制在CPU上执行数据加载/预处理,而prefetch意味着可以在CPU上准备下一批,因为当前批处理是在GPU上进行训练。唯一的例外是,如果您在每个记录有大量数据的数据集上有大量的随机大小,那么在通过GPU运行任何内容之前,最初需要花费一些时间来加载大量示例。

答案 1 :(得分:3)

我同意DomJack关于使用Dataset API,除了需要读取整个csv文件然后转换为TfRecord。我特此建议TextLineDataset - Dataset API的子类,直接将数据加载到TensorFlow程序中。可以找到一个直观的教程here

下面的代码用于说明MNIST分类问题,希望能回答OP的问题。 csv文件有784列,类的数量是10.我在这个例子中使用的分类器是一个具有16个relu单元的1-隐藏层神经网络。

首先,加载库并定义一些常量:

# load libraries
import tensorflow as tf
import os

# some constants
n_x = 784
n_h = 16
n_y = 10

# path to the folder containing the train and test csv files
# You only need to change PATH, rest is platform independent
PATH = os.getcwd() + '/' 

# create a list of feature names
feature_names = ['pixel' + str(i) for i in range(n_x)]

其次,我们使用Dataset API创建一个读取文件的输入函数,然后将结果提供给Estimator API。返回值必须是按如下方式组织的双元素元组:第一个元素必须是一个dict,其中每个输入要素是一个键,然后是训练批次的值列表,第二个元素是标签列表对于培训批次。

def my_input_fn(file_path, batch_size=32, buffer_size=256,\
                perform_shuffle=False, repeat_count=1):
    '''
    Args:
        - file_path: the path of the input file
        - perform_shuffle: whether the data is shuffled or not
        - repeat_count: The number of times to iterate over the records in the dataset.
                    For example, if we specify 1, then each record is read once.
                    If we specify None, iteration will continue forever.
    Output is two-element tuple organized as follows:
        - The first element must be a dict in which each input feature is a key,
        and then a list of values for the training batch.
        - The second element is a list of labels for the training batch.
    '''
    def decode_csv(line):
        record_defaults = [[0.]]*n_x # n_x features
        record_defaults.insert(0, [0]) # the first element is the label (int)
        parsed_line = tf.decode_csv(records=line,\
                                    record_defaults=record_defaults)
        label = parsed_line[0]  # First element is the label
        del parsed_line[0]  # Delete first element
        features = parsed_line  # Everything but first elements are the features
        d = dict(zip(feature_names, features)), label
        return d

    dataset = (tf.data.TextLineDataset(file_path)  # Read text file
               .skip(1)  # Skip header row
               .map(decode_csv))  # Transform each elem by applying decode_csv fn
    if perform_shuffle:
        # Randomizes input using a window of 256 elements (read into memory)
        dataset = dataset.shuffle(buffer_size=buffer_size)
    dataset = dataset.repeat(repeat_count)  # Repeats dataset this # times
    dataset = dataset.batch(batch_size)  # Batch size to use
    iterator = dataset.make_one_shot_iterator()
    batch_features, batch_labels = iterator.get_next()

    return batch_features, batch_labels

然后,小批量可以计算为

next_batch = my_input_fn(file_path=PATH+'train1.csv',\
                         batch_size=batch_size,\
                         perform_shuffle=True) # return 512 random elements

接下来,我们定义要素列是数字

feature_columns = [tf.feature_column.numeric_column(k) for k in feature_names]

第三,我们创建了一个估算工具DNNClassifier

classifier = tf.estimator.DNNClassifier(
    feature_columns=feature_columns,  # The input features to our model
    hidden_units=[n_h],  # One layer
    n_classes=n_y,
    model_dir=None)

最后,使用测试csv文件训练DNN,同时在测试文件上执行评估。请更改repeat_countsteps,以确保培训符合代码中所需的时代数。

# train the DNN
classifier.train(
    input_fn=lambda: my_input_fn(file_path=PATH+'train1.csv',\
                                 perform_shuffle=True,\
                                 repeat_count=1),\
                                 steps=None)    

# evaluate using the test csv file
evaluate_result = classifier.evaluate(
    input_fn=lambda: my_input_fn(file_path=PATH+'test1.csv',\
                                 perform_shuffle=False))
print("Evaluation results")
for key in evaluate_result:
    print("   {}, was: {}".format(key, evaluate_result[key]))