Почему модель keras предсказывает медленнее после компиляции?

prediction speed keras

Теоретически прогноз должен быть постоянным, поскольку веса имеют фиксированный размер. Как вернуть скорость после компиляции (без необходимости удалять оптимизатор)?

Смотрите связанный эксперимент: https://nbviewer.jupyter.org/github/off99555/TensorFlowExperiments/blob/master/test-prediction-speed-after-compile.ipynb?flush_cache=true

Ответ 1

КРУПНЫЙ КОНТРОЛЬ: self._experimental_run_tf_function = True. Это экспериментальный. Но это на самом деле не плохо.

Любой читатель TensorFlow, читающий: очистите свой код. Это беспорядок. И это нарушает важные методы кодирования, такие как одна функция выполняет одну вещь; _process_inputs делает гораздо больше, чем "входы процесса", то же самое для _standardize_user_data. "Мне не платят достаточно", но вы платите, тратя дополнительное время на то, чтобы понять свои собственные вещи, а пользователи заполняют страницу "Проблемы" ошибками, которые легче устранить с помощью более четкого кода.


РЕЗЮМЕ: это немного медленнее с compile().

compile() устанавливает внутренний флаг, который назначает другую функцию прогнозирования для predict. Эта функция создает новый граф при каждом вызове, замедляя его по сравнению с некомпилированным. Однако эта разница проявляется только тогда, когда время поезда намного меньше времени обработки данных. Если мы увеличим размер модели по крайней мере до среднего, оба станут равными. Посмотрите код внизу.

Это небольшое увеличение времени обработки данных более чем компенсируется усиленной графикой. Поскольку более эффективно хранить только один граф модели, одна прекомпиляция отбрасывается. Тем не менее: если ваша модель мала по сравнению с данными, вам лучше без compile() для вывода модели. Смотрите мой другой ответ для обходного пути.


ЧТО ДЕЛАТЬ?

Сравните скомпилированную производительность модели с некомпилированной, как в коде внизу.

  • Скомпилировано быстрее: запустите predict на скомпилированной модели.
  • Скомпилировано медленнее: запустите predict на некомпилированной модели.

Да, оба варианта возможны, и это будет зависеть от (1) размера данных; (2) размер модели; (3) аппаратное обеспечение. Код внизу на самом деле показывает, что скомпилированная модель работает быстрее, но 10 итераций - небольшой пример. См. "Обходные пути" в моем другом ответе для "с практическими рекомендациями".


РЕКВИЗИТЫ:

Это заняло некоторое время для отладки, но было весело. Ниже я опишу ключевых виновников, которых я обнаружил, процитирую некоторую соответствующую документацию и покажу результаты профилировщика, которые привели к окончательному узкому месту.

(FLAG == self.experimental_run_tf_function, для краткости)

  1. Model по умолчанию создается с помощью FLAG=False. compile() устанавливает его в True.
  2. predict() включает в себя получение функции прогнозирования, func = self._select_training_loop(x)
  3. Без каких-либо специальных kwargs, переданных predict и compile, все остальные флаги таковы:
    • (A) FLAG==Truefunc = training_v2.Loop()
    • (B) FLAG==Falsefunc = training_arrays.ArrayLikeTrainingLoop()
  4. Из строки документации исходного кода source code docstring, (A) сильно зависит от графика, использует больше стратегии распространения, а операции создаются и создаются & уничтожение элементов графа, которые могут (могут) влиять на производительность.

Истинный виновник: _process_inputs(), что составляет 81% времени выполнения. Его основной компонент? _create_graph_function(), 72% времени выполнения. Этот метод даже не существует для (B). Однако при использовании модели среднего размера _process_inputs составляет менее чем на 1% времени выполнения. Код внизу и результаты профилирования следуют.


ПРОЦЕССОРЫ ДАННЫХ:

(A): <class 'tensorflow.python.keras.engine.data_adapter.TensorLikeDataAdapter'>, используется в _process_inputs(). Соответствующий исходный код

(B): numpy.ndarray, возвращено convert_eager_tensors_to_numpy. Соответствующий исходный код и здесь


ФУНКЦИЯ ИСПОЛНЕНИЯ МОДЕЛИ (например, прогноз)

(A): функция распределения и здесь

(B): функция распределения (другая) и здесь


PROFILER: результаты для кода в моем другом ответе "крошечная модель" и в этом ответе "средняя модель":

Крошечная модель: 1000 итераций, compile()

Крошечная модель: 1000 итераций, нет compile()

Средняя модель: 10 итераций


  ДОКУМЕНТАЦИЯ (косвенно) о влиянии compile(): источника source

В отличие от других операций TensorFlow, мы не конвертируем python   числовые входы в тензоры. Кроме того, новый граф генерируется для каждого   отличное числовое значение питона, например, вызов g(2) и g(3)   создать два новых графика

function создает отдельный граф для каждого уникального набора ввода   формы и типы данных. Например, следующий фрагмент кода приведет   в трех различных графиках, которые отслеживаются, так как каждый вход имеет разные   форма

Один объект tf.function может потребоваться отобразить на несколько графов вычислений.   под капотом. Это должно быть видно только как производительность performance (трассировка графиков имеет    ненулевая вычислительная стоимость и стоимость памяти), но это не должно влиять на правильность   программы


Контрпример:

from tensorflow.keras.layers import Input, Dense, LSTM, Bidirectional, Conv1D
from tensorflow.keras.layers import Flatten, Dropout
from tensorflow.keras.models import Model
import numpy as np
from time import time

def timeit(func, arg, iterations):
    t0 = time()
    for _ in range(iterations):
        func(arg)
    print("%.4f sec" % (time() - t0))

batch_size = 32
batch_shape = (batch_size, 400, 16)
ipt   = Input(batch_shape=batch_shape)
x     = Bidirectional(LSTM(512, activation='relu', return_sequences=True))(ipt)
x     = LSTM(512, activation='relu', return_sequences=True)(ipt)
x     = Conv1D(128, 400, 1, padding='same')(x)
x     = Flatten()(x)
x     = Dense(256, activation='relu')(x)
x     = Dropout(0.5)(x)
x     = Dense(128, activation='relu')(x)
x     = Dense(64,  activation='relu')(x)
out   = Dense(1,  activation='sigmoid')(x)
model = Model(ipt, out)

X = np.random.randn(*batch_shape)
timeit(model.predict, X, 10)
model.compile('adam', loss='binary_crossentropy')
timeit(model.predict, X, 10)

Результаты:

34.8542 sec
34.7435 sec

Ответ 2

ОБНОВЛЕНИЕ: фактический ответ публикуется как отдельный ответ; этот пост содержит дополнительную информацию


.compile() устанавливает большую часть графика TF/Keras, включая потери, метрики, градиенты, а также частично оптимизатор и его веса - что гарантирует заметное замедление.

Что неожиданно, так это степень замедления - в 10 раз в моем собственном эксперименте и для predict(), который не обновляет весовые коэффициенты. Глядя на исходный код TF2, элементы графа кажутся тесно переплетенными, а ресурсы не обязательно распределяются "справедливо".

Возможный упущение разработчиками производительности predict для некомпилированной модели, поскольку модели обычно используются скомпилированными, но на практике это недопустимое различие. Также возможно, что это "необходимое зло", так как существует простой обходной путь (см. ниже).

Это не полный ответ, и я надеюсь, что кто-то может предоставить его здесь - если нет, я бы предложил открыть проблему Github на TensorFlow. (OP имеет; здесь)


Обходной путь: обучить модель, сохранить ее веса, пересобрать модель без компиляции, загрузить веса. Не сохраняйте всю модель (например, model.save()), так как она будет загружена скомпилированной - вместо этого используйте model.save_weights() и model.load_weights().

Обходной путь 2: выше, но используйте load_model(path, compile=False); предложение кредита: D. Мёллер


ОБНОВЛЕНИЕ: для пояснения, оптимизатор не полностью создан с помощью compile, включая его тензоры weights и updates - это делается при первом вызове функции подгонки (fit, train_on_batch и т.д.) через model._make_train_function().

Таким образом, наблюдаемое поведение еще более странно. Хуже того, создание оптимизатора не вызывает дальнейшего замедления (см. ниже) - предположение, что "размер графика" не является основным объяснением здесь.


РЕДАКТИРОВАТЬ: на некоторых моделях замедление в 30 раз. TensorFlow, что ты наделал? Пример ниже:

from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Model
import numpy as np
from time import time

def timeit(func, arg, iterations):
    t0 = time()
    for _ in range(iterations):
        func(arg)
    print("%.4f sec" % (time() - t0))

ipt   = Input(shape=(4,))
x     = Dense(2, activation='relu')(ipt)
out   = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)

X = np.random.randn(32,4)

timeit(model.predict, X, 1000)
model.compile('adam', loss='binary_crossentropy')
timeit(model.predict, X, 1000)
model._make_train_function()  # build optimizer
timeit(model.predict, X, 1000)

устройства Выходы:

0.9891 sec
29.785 sec
29.521 sec