Почему пакет TensorFlow "tf.data" замедляет мой код?

Я просто научился использовать 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 и тренировку с полной партией, для выполнения 10 эпох потребовалось в среднем 0.2431719040000644 секунды.

Используя tf.data с размером данных 500 и размером ввода 10 и тренировкой с размером партии 50, для выполнения 10 эпох потребовалось в среднем 0.09512088866661846 секунд.

Используя tf.data, с размером данных 500 и размером ввода 10 и тренировкой с размером партии 500, потребовалось в среднем 0.07286913600000844 секунд для запуска 10 эпох.

Используя tf.data, с размером данных 100000 и размером ввода 10, и тренировкой с размером партии 50, потребовалось в среднем 4,421892363666605 секунд для запуска 10 эпох.

Используя tf.data, с размером данных 100000 и размером ввода 10 и тренировкой с размером партии 100000, потребовалось в среднем 2,2555197536667038 секунд для запуска 10 эпох.

Изменить: Исправлена важная проблема, о которой указал Фред Гут. Однако это не сильно повлияло на результаты.

Ответы

Ответ 1

Это потому, что вы сравниваете яблоки с бананами.

С одной стороны, при использовании заполнителей вы предоставляете монолитный тензор, как есть. С другой стороны, при использовании Dataset вы нарезаете тензор на отдельные образцы. Это совсем другое.

Эквивалент предоставления монотонного тензодателя заполнителя с помощью Dataset - это использование tf.data.Dataset.from_tensors. Когда я использую from_tensors в вашем примере, я получаю аналогичные (фактически меньшие) времена вычислений, чем с заполнителями.

Если вы хотите сравнить более сложный трубопровод, используя from_tensor_slices, вы должны использовать справедливое сравнение с заполнителями. Например, перетасовать данные. Добавьте некоторую предварительную обработку на ваших срезах. Я не сомневаюсь, что вы увидите прирост производительности, который заставляет людей переключаться на этот конвейер.

Ответ 2

Первый:

Вы повторно создаете набор данных.

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

Создание набора данных до начала цикла и изменения regress_with_tfData ввода подписи, чтобы использовать набор данных вместо training_inputs и training_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()

Ответ 3

Я хотел протестировать API-интерфейс набора данных, который, по-видимому, очень удобен для обработки данных. Я много времени тестировал об этом API в режиме CPU, GPU и multi-GPU для небольших и больших NN с различными типами данных.

Во-первых, мне кажется, что ваш код в порядке. Но я должен указать, что ваш NN - всего лишь один простой слой.

Теперь API-интерфейс набора данных не подходит для вашего типа NN, но для NN намного сложнее. Зачем? По нескольким причинам, которые я объясняю ниже (основан на моих поисках понимания API набора данных).

Во-первых, с одной стороны API-интерфейс набора данных обрабатывает данные каждой партии, тогда как в других руках данные предварительно обрабатываются. Поэтому, если он подходит вашей ОЗУ, вы можете сэкономить время, предварительно обработав данные. Здесь ваши данные просто "просты". Если вы хотите проверить, что я говорю, попробуйте найти действительно большой набор данных для обработки. Тем не менее, API набора данных можно настроить с помощью предварительной выборки данных. Вы можете взглянуть на этот учебник, который очень хорошо объясняет, почему хорошо обрабатывать данные с помощью предварительной выборки.

Во-вторых, в моих поисках API набора данных для обучения Multi-GPU я обнаружил, что, насколько я знаю, старый способ предварительной обработки быстрее, чем API-интерфейс набора данных для небольшой нейронной сети. Вы можете проверить это, создав простой стековый RNN, который принимает последовательность во входе. Вы можете попробовать разный размер стека (я тестировал 1, 2, 10 и 20). Вы увидите, что, используя API-интерфейс набора данных, на 1-GPU или на 4-GPU, время не отличалось для небольших RNN-стеков (1, 2 и 5).

Подводя итог, API-интерфейс набора данных подходит для нейронной сети, у которой есть данные, которые не могут быть предварительно обработаны. В зависимости от вашей задачи может быть удобнее предварительно обрабатывать данные, например, если вы хотите настроить свой NN, чтобы улучшить его. Я согласен с тем, что API-интерфейс набора данных действительно крут для партии, дополнения и также удобен для перетасовки большого количества данных, но также не подходит для обучения с несколькими GPU.

Ответ 4

Одна из возможных недостатков - это предварительная выборка. Добавьте предварительную выборку из 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 пакет данных во время тренировки. Таким образом, вы не будете ждать, пока пакет готов, он должен быть готов к работе, как только будет завершена каждая итерация поезда.