为什么TensorFlow的`tf.data`包会使我的代码变慢?

时间:2018-07-26 14:42:57

标签: python python-3.x tensorflow machine-learning deep-learning

我只是在学习使用TensorFlow的tf.data API,并且发现它使我的代码变慢了很多,以每个时期的时间来衡量。我想这与应该做的相反。我编写了一个简单的线性回归程序进行测试。

Tl; Dr :有了100,000个训练数据,tf.data可以将每个时期的时间减慢大约十倍,如果您使用的是批量训练。如果使用较小的批次,更糟。对于500个训练数据,情况恰恰相反。

我的问题:怎么回事?我的实现有缺陷吗?我读过的其他资料显示tf.data的速度提高了约30%。

import tensorflow as tf 
import numpy as np
import timeit

import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
tf.logging.set_verbosity(tf.logging.ERROR)

n_epochs = 10
input_dimensions_list = [10]

def function_to_approximate(x):
    return np.dot(x, random_covector).astype(np.float32) + np.float32(.01) * np.random.randn(1,1).astype(np.float32)

def regress_without_tfData(n_epochs, input_dimension, training_inputs, training_labels):
    tf.reset_default_graph()
    weights = tf.get_variable("weights", initializer=np.random.randn(input_dimension, 1).astype(np.float32))

    X = tf.placeholder(tf.float32, shape=(None, input_dimension), name='X')
    Y = tf.placeholder(tf.float32, shape=(None, 1), name='Y')
    prediction = tf.matmul(X,weights)
    loss = tf.reduce_mean(tf.square(tf.subtract(prediction, Y)))
    loss_op = tf.train.AdamOptimizer(.01).minimize(loss)

    init = tf.global_variables_initializer()

    with tf.Session() as sess:
        sess.run(init)
        for _ in range(n_epochs):
            sess.run(loss_op, feed_dict={X: training_inputs, Y:training_labels})

def regress_with_tfData(n_epochs, input_dimension, training_inputs, training_labels, batch_size):
    tf.reset_default_graph()
    weights = tf.get_variable("weights", initializer=np.random.randn(input_dimension, 1).astype(np.float32))

    X,Y = data_set.make_one_shot_iterator().get_next()

    prediction = tf.matmul(X, weights)
    loss = tf.reduce_mean(tf.square(tf.subtract(prediction, Y)))
    loss_op = tf.train.AdamOptimizer(.01).minimize(loss)

    init = tf.global_variables_initializer()

    with tf.Session() as sess:
        sess.run(init)
        while True:
            try: 
                sess.run(loss_op)
            except tf.errors.OutOfRangeError:
                break

for input_dimension in input_dimensions_list:
    for data_size in [500, 100000]:

        training_inputs = np.random.randn(data_size, input_dimension).astype(np.float32)
        random_covector = np.random.randint(-5, 5, size=(input_dimension, 1))
        training_labels = function_to_approximate(training_inputs)

        print("Not using tf.data, with data size "
        "{}, input dimension {} and training with "
        "a full batch, it took an average of "
        "{} seconds to run {} epochs.\n".
            format(
                data_size,
                input_dimension,
                timeit.timeit(
                    lambda: regress_without_tfData(
                        n_epochs, input_dimension, 
                        training_inputs, training_labels
                    ), 
                    number=3
                ),
                n_epochs))

for input_dimension in input_dimensions_list:
    for data_size, batch_size in [(500, 50), (500, 500), (100000, 50), (100000, 100000)]:

        training_inputs = np.random.randn(data_size, input_dimension).astype(np.float32)
        random_covector = np.random.randint(-5, 5, size=(input_dimension, 1))
        training_labels = function_to_approximate(training_inputs)

        data_set = tf.data.Dataset.from_tensor_slices((training_inputs, training_labels))
        data_set = data_set.repeat(n_epochs)
        data_set = data_set.batch(batch_size)

        print("Using tf.data, with data size "
        "{}, and input dimension {}, and training with "
        "batch size {}, it took an average of {} seconds "
        "to run {} epochs.\n".
            format(
                data_size,
                input_dimension,
                batch_size,
                timeit.timeit(
                    lambda: regress_with_tfData(
                        n_epochs, input_dimension, 
                        training_inputs, training_labels, 
                        batch_size
                    ),
                    number=3
                )/3,
                n_epochs
            ))

这为我输出:

  

不使用tf.data,数据大小为500,输入维度为10,训练为   完整批次平均需要0.20243382899980134秒   运行10个时代。

     

不使用tf.data,数据大小为100000,输入尺寸为10,   进行完整的培训,平均花费0.2431719040000644   秒以运行10个纪元。

     

使用tf.data,数据大小为500,输入维度为10,以及   批次大小为50的培训,平均需要0.09512088866661846   秒以运行10个纪元。

     

使用tf.data,数据大小为500,输入维度为10,以及   批量培训500次,平均需要   0.07286913600000844秒以运行10个历元。

     

使用tf.data,数据大小为100000,输入尺寸为10,并且   批次大小为50的培训,平均花费4.421892363666605   秒以运行10个纪元。

     

使用tf.data,数据大小为100000,输入尺寸为10,并且   培训,批量为100000,平均   2.2555197536667038秒以运行10个纪元。

编辑:修复了Fred Guth指出的一个重要问题。不过,对结果影响不大。

5 个答案:

答案 0 :(得分:7)

我想测试数据集API,这似乎对处理数据非常方便。我用大量数据类型的小型和大型NN对CPU,GPU和多GPU方式的API进行了大量测试。

第一件事,在我看来您的代码还可以。但我需要指出的是,您的NN只是一个简单的层。

现在,数据集API不适合您的NN类型,但是更复杂。为什么呢出于以下原因,我在下面进行了解释(这是出于对理解数据集API的追求)。

首先,一方面数据集API 每批处理数据,而另一方面数据已预处理。因此,如果适合您的RAM,则可以通过预处理数据来节省时间。在这里,您的数据只是“简单”的。如果要测试我在说什么,请尝试查找要处理的非常大的数据集。不过,可以使用prefetching数据来调整数据集API。您可以看看这个tutorial,它很好地解释了为什么使用预取来处理数据会很好。

第二,在寻求用于多GPU训练的数据集API时,我发现,旧的预处理方法比小型神经网络的数据集API快。您可以通过创建一个简单的可堆叠RNN进行验证,该RNN在输入中采用一个序列。您可以尝试不同大小的堆栈(我已经测试了1,2,10和20)。您将看到,使用数据集API在1-GPU或4-GPU上,时间对于小型RNN堆栈(1、2和5)没有区别。

总而言之,数据集API适用于包含无法进行预处理的数据的神经网络。根据您的任务,预处理数据可能会更方便,例如,如果您想调整NN以改善数据。我同意数据集API对于批处理,填充和填充大量数据都很方便,但是它也不适合用于多GPU训练。

答案 1 :(得分:6)

第一:

您不必要地重新创建数据集。

library(dplyr) library(magrittr) > z <- x %>% mutate(match = ifelse(( (lead(X1)==X1) & (lead(X2)==X2)),"YES","NO")) > z %>% mutate(X3 = replace(X3, match=="YES", "f")) X1 X2 X3 X4 X5 X6 match 1 1 a f c 3 d YES 2 1 a 2 c 3 g NO 3 1 f 4 g 5 h <NA>

在循环之前创建数据集,并更改data_set = tf.data.Dataset.from_tensor_slices((training_inputs, training_labels))输入签名以使用数据集而不是regress_with_tfDatatraining_inputs

第二:

这里的问题是大小为50甚至500的微型批处理太小,无法补偿td.data构建延迟的成本。您应该增加小批量的大小。有趣的是,您使用的最小批量为100000,但是它可能太大了(我不确定,我认为它需要更多测试)。

您可以尝试以下几种方法:

1)将小批量的大小增加到10000左右,看看是否有所改善 2)更改管道以使用迭代器,例如:

training_labels

答案 2 :(得分:5)

您可能缺少的一件事是预取。在数据管道的末尾添加预取1,如下所示:

data_set = tf.data.Dataset.from_tensor_slices((training_inputs, training_labels))
data_set = data_set.repeat(n_epochs)
data_set = data_set.batch(batch_size).prefetch(1)

在数据集管道的末尾添加预取1表示您在进行训练时尝试获取1批数据。这样,您就不会在批处理准备期间等待,每次火车迭代完成后都应该准备就绪。

答案 3 :(得分:3)

那是因为您正在将苹果与香蕉进行比较。

一方面,使用占位符时,您将按原样提供整体张量。另一方面,使用Dataset时,您将张量切成单个样本。这是非常不同的。

使用Dataset相当于为tf.data.Dataset.from_tensors管道提供单占位符张量。 在您的示例中使用from_tensors时,我得到的计算时间与占位符相似(实际上更短)。

如果要使用from_tensor_slices比较更复杂的管道,则应与占位符进行公平比较。例如,将您的数据混洗。在切片上添加一些预处理。毫无疑问,您会看到使人们切换到该管道的性能提升。

答案 4 :(得分:0)

接受的答案不再有效,因为 TF 行为已经改变。每个文档:

<块引用>

from_tensors 生成仅包含单个元素的数据集。至 将输入张量切成多个元素,使用 from_tensor_slices 相反。

这意味着你不能批量处理

X = np.arange(10)
data = tf.data.Dataset.from_tensors( X )
data = data.batch(2)
for t in data.as_numpy_iterator():
  print(t)
# only one row, whereas expected 5 !!!

文档推荐from_tensor_slices。但是与 numpy 切片相比,这有相当多的开销。慢切片是一个悬而未决的问题 https://github.com/tensorflow/tensorflow/issues/39750

本质上,TF 中的切片速度很慢,并且会影响输入边界或轻型模型,例如小型网络(回归、word2vec)。