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

Eri*_*uld 18 python machine-learning python-3.x deep-learning tensorflow

我只是学习使用TensorFlow的tf.dataAPI,而且我发现它使我的代码速度下降了很多,按时间计算.我想,这与它应该做的相反.我写了一个简单的线性回归程序来测试它.

Tl; Dr:tf.data如果您正在使用完整的批次培训,则使用100,000个训练数据,将每个时期的时间减少大约十倍.如果你使用较小的批次更糟糕.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
            ))
Run Code Online (Sandbox Code Playgroud)

这为我输出:

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

不使用tf.data,数据大小为100000,输入维度为10,训练为完整批次,运行10个时期平均花费0.2431719040000644秒.

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

使用tf.data,数据大小为500,输入维度为10,以及批量大小为500的训练,运行10个时期平均花费0.07286913600000844秒.

使用tf.data,数据大小为100000,输入维度为10,批量大小为50的训练,运行10个纪元时平均需要4.421892363666605秒.

使用tf.data,数据大小为100000,输入维度为10,批量大小为100000的训练,运行10个时期平均需要2.2555197536667038秒.

编辑:修正了Fred Guth指出的一个重要问题.但是,它对结果影响不大.

Cle*_*cel 7

我想测试数据集API,它似乎非常方便处理数据.我花了很多时间用CPU,GPU和多GPU方式测试这个API,用于具有不同类型数据的小型和大型NN.

首先,在我看来你的代码还可以.但我需要指出你的NN只是一个简单的层.

现在,数据集API不适合您的NN类型,但适用于复杂性更高的NN.为什么?我在下面解释了几个原因(建立在我对数据集API的理解之上).

首先,一方面,数据集API 每批处理数据,而另一方面,数据被预处理.因此,如果它适合您的RAM,您可以通过预处理数据来节省时间.在这里,您的数据只是"简单".如果你想测试我在说什么,试着找一个非常大的数据集来处理.然而,可以使用预取数据来调整数据集API .您可以查看本教程,它解释了为什么使用预取处理数据是好的.

其次,在我对多GPU培训的数据集API的探索中,我发现据我所知,旧的预处理方式比小型神经网络的数据集API更快.您可以通过创建一个简单的可堆叠RNN来验证这一点,该RNN采用输入序列.您可以尝试不同大小的堆栈(我测试过1,2,10和20).您将看到,使用数据集API,在1-GPU或4-GPU上,小RNN堆栈(1,2和5)的时间没有差异.

总而言之,数据集API适用于具有无法预处理的数据的神经网络.根据您的任务,预处理数据可能更方便,例如,如果您想调整NN以进行改进.我同意数据集API对于批处理,填充非常酷,并且还便于对大量数据进行混洗,但它也不适合多GPU培训.


Fre*_*uth 6

第一:

您正在不必要地重新创建数据集.

data_set = tf.data.Dataset.from_tensor_slices((training_inputs, training_labels))

在循环之前创建数据集并更改regress_with_tfData输入签名以使用数据集而不是 training_inputstraining_labels.

第二:

这里的问题是尺寸为50或甚至500的小型机太小而无法弥补td.data构建延迟的成本.你应该增加小批量的大小.有趣的是你用尺寸为100000的小批量做了这件事,但是也许它太大了(我不确定这个,我认为它需要更多的测试).

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

1)将小批量大小增加到大约10000,看看你是否得到了改进2)更改管道以使用迭代器,例如:

    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)
    iterator = data_set.make_one_shot_iterator()
    ....
    next_element = iterator.get_next()
Run Code Online (Sandbox Code Playgroud)


Bur*_*000 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)
Run Code Online (Sandbox Code Playgroud)

在数据集管道末尾添加1的预取意味着您在训练发生时尝试获取1批数据.这样你就不会在准备好批处理时等待它,它应该准备好在每次列车迭代完成后立即进行.


P-G*_*-Gn 5

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

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

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

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