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

23

прогноз скорости керас

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

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

off99555
источник
Я думаю, что вам нужно соответствовать модели после компиляции, а затем использовать обученную модель для прогнозирования. См. Здесь
наивный
@naive Fitting не имеет отношения к проблеме. Если вы знаете, как на самом деле работает сеть, вам будет любопытно, почему прогноз медленнее. При прогнозировании для умножения матриц используются только весовые коэффициенты, и весовые коэффициенты должны фиксироваться до и после компиляции, поэтому время прогнозирования должно оставаться постоянным.
off99555
Я знаю, что это не имеет отношения к проблеме . И не нужно знать, как работает сеть, чтобы указать, что поставленные вами задачи и сравнение их точности на самом деле не имеют смысла. Без подгонки модели к некоторым данным, которые вы прогнозируете, вы фактически сравниваете затраченное время. Это не обычный или правильный
наивный
3
@naive Проблема заключается в понимании производительности модели, скомпилированной и некомпилированной, не имеющей ничего общего с точностью или дизайном модели. Это законный вопрос, который может стоить пользователям TF - я, например, понятия не имел, пока не наткнулся на этот вопрос.
OverLordGoldDragon
1
@naive Вы не можете fitбез compile; Оптимизатор даже не существует для обновления весов. predict может использоваться без fitили compileкак описано в моем ответе, но разница в производительности не должна быть такой значительной - отсюда и проблема.
OverLordGoldDragon

Ответы:

22

UPDATE - 1/15/2020 : текущая лучшая практика для небольших объемов партий должны кормить входы модели непосредственно - то есть preds = model(x), и если слои ведут себя по- разному на поезда / вывода, model(x, training=False). Для последнего коммита это теперь задокументировано .

Я не тестировал их, но в обсуждении Git также стоит попробовать predict_on_batch()- особенно с улучшениями в TF 2.1.


ULTIMATE Виновник : 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, все другие флаги таковы, что:
    • (А) FLAG==True ->func = training_v2.Loop()
    • (B) FLAG==False ->func = training_arrays.ArrayLikeTrainingLoop()
  4. Исходя из строки документации исходного кода , (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) : функция распределения , а здесь

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


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

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

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

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


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

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

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

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


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

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
OverLordGoldDragon
источник
1
Какой вывод о том, что мы должны сделать, чтобы получить максимальную скорость прогнозирования для любого размера модели? Это просто не делать compile()?
off99555
3
@ off99555 "для любого размера модели" - такого нет. Прочитайте весь ответ - если бы я потратил часы на его отладку, несколько минут от автора не должны быть необоснованными.
OverLordGoldDragon
Я прочитал все это, но это трудно понять, потому что я не тот, кто отлаживал код. Таким образом, вы должны дать заключение, которое не включает промежуточные переменные, которые вы найдете на этапе отладки. Например, «если ваша модель небольшая, не используйте компиляцию. Если ваша модель среднего размера, вы можете использовать компиляцию». Что-то в этом
роде
1
@ off99555 Достаточно справедливо; обновлено. Новый раздел довольно здравый смысл, но я вижу, что он не реализуется сразу.
OverLordGoldDragon
1
@ off99555 Не то чтобы я тестировал, но очень большие модели (ResNet и т. д.) могут работать заметно быстрее, скомпилированные, особенно. если он распределен по многим устройствам, поскольку (A) является более сложным в плане графики и распределения. Самый верный тест, ну, в общем, тест - как в ответе. Незнаком с TF lite, но это отдельный вопрос
OverLordGoldDragon
15

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


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

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

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

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


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

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


ОБНОВЛЕНИЕ : уточнить, оптимизатор не в полной мере экземпляры с 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
OverLordGoldDragon
источник
1
Это интересно. Я давно хотел протестировать тренировку со статическим графиком в model.fit()сравнении с динамическим циклом с активным выполнением, чтобы увидеть, не слишком ли велика потеря производительности ...
Даниэль Мёллер,
1
В прошлом я мог заметить существенную разницу в скорости между Keras и PyTorch (будучи PyTorch намного быстрее).
Даниэль Мёллер
1
Я открыл проблему здесь: github.com/tensorflow/tensorflow/issues/33340
off99555
2
Да. Это плохой дизайн, когда вы помещаете обучающий код в предсказание. Потому что пользователи будут использовать эту функцию прогнозирования последовательно несколько раз в производстве. Это должно работать быстрее, чтобы вызвать наименьшее удивление. По сравнению с простой реализацией вам нужно только умножить матрицу, добавить смещение, активировать, и все для плотного слоя. Нет необходимости касаться какой-либо функции потери.
off99555
1
Подсказка, вы можете использовать load_model(name, compile=False), это проще, чем сохранение / загрузка весов и воссоздание модели.
Даниэль Мёллер