Мартин Холечек, Amp X Машинное обучение

Глубокое обучение окончательно завоевало многие области машинного обучения, такие как распознавание изображений, обработка естественного языка и т. Д. Прогнозирование временных рядов, однако, является одним из последних препятствий, когда, несмотря на то, что глубокие нейронные сети выиграли несколько битв, война еще не окончательно выиграна.

Одним из последних нововведений в этой области является архитектура нейронной сети Temporal Fusion Transformer (TFT), представленная в Lim et al. 2019 с реализацией, рассмотренной здесь .

TFT объединяет несколько интересных идей для моделирования временных рядов. Мы хотели изучить архитектуру и сравнить ее с хорошо зарекомендовавшими себя моделями, такими как архитектура SeriesNet Шена и др. 2018, на основе популярной модели WaveNet от Google DeepMind.

Между TFT и SeriesNet есть большая разница как в архитектуре, так и в реализации.

SeriesNet имеет сверточную архитектуру, которая может работать с целыми последовательностями, давая результаты, когда сверточное окно скользит по набору данных.

Архитектура TFT основана на подходе Seq2Seq (с кодировщиком и декодером), поэтому при работе с последовательностью требуется специально подготовленный (оконный) ввод. Следовательно, время обучения TFT значительно больше, чем у SeriesNet.

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

Мы разработали и делимся здесь нашими решениями в области проектирования моделей TensorFlow. Мы постарались сделать код максимально похожим на исходный, добавляя эти обновления и параметры для модели:

  • Преобразование модели из TensorFlow версии 1 в 2
  • Создание модели с именованными входами (и, таким образом, удаление части, где необходимо декодировать всю информацию из окон).
  • Создание модели, завернутой в процедуры векторизации.
  • Распространение векторизации глубже в модели только там, где это необходимо.

(Все эти шаги имеют соответствующие параметризованные примеры, включенные в предоставленные тесты, мы время от времени будем упоминать здесь имена подпрограмм, чтобы упростить поиск всего в коде.)

Причина всех этих шагов состоит в том, чтобы сделать работу более доступной и сначала продемонстрировать технику векторизации, рассматривая TFT как «черный ящик».

К векторизации

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

Размеры пакета

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

Однако обе операции должны выполняться в TensorFlow, чтобы не вызывать замедления:

def squash_batch_dimensions(seq, batch_dims):
    """Squashes first (batch_dims) dimensions into 1 batch dimension."""
    batch_shape_orig = tf.shape(seq)[:batch_dims]
    retain_shape = tf.shape(seq)[batch_dims:]
    new_shape = tf.concat([[-1], retain_shape], axis=-1)
    seq_squashed = tf.reshape(seq, new_shape)
    return seq_squashed, batch_shape_orig
def unsquash_batch_dimensions(seq: tf.Tensor, batch_shape_orig: Union[list, tf.Tensor]):
    """Restores additional dimensions previously squashed to the first batch dimension"""
    batch_dims = 1
    retain_shape = tf.shape(seq)[batch_dims:]
    new_shape = tf.concat([batch_shape_orig, retain_shape], axis=-1)
    seq = tf.reshape(seq, new_shape)
    return seq

Итак, давайте представим, что мы отделили входные данные в TFT от оконного кодирования.

Теперь в чистом виде TFT должен знать исторические (H) и будущие данные (F) в каждый момент времени (где некоторые исторические особенности фактически включаются в будущее, если они известны заранее или повторяются):

H ##############################
F -----##############################

Обратите внимание, что последовательность F длиннее, так как будущие известные входные данные должны быть известны также для последних исторических входных данных. Символ «-» означает, что этот ввод никогда не будет использоваться, поэтому он опускается.

Оконный подход принимает - на каждом временном шаге - определенное количество входов H, а также необходимые будущие входы от F:

H XXXXXXXXXX####################
F -----#####XXXXX####################
           ^ as source data at each timepoint

Итак, если мы хотим представить параллельный / векторизованный подход для работы с окнами, нам понадобятся наши данные (помеченные для лучшего понимания):

H 123456789012345678901234567890
F -----FGHIJKLMNOPQRSTUVWXYZABCDEFGHI

Изменить форму следующим образом (читать столбцами сверху вниз):

1234...901 ^
2345...012 |
3456...123 |
4567...234 |
5678...345 |  Model's historical inputs
6789...456 |
7890...567 |
8901...678 |
9012...789 |
0123...890 V <- last (H) inputs
KLMN...CDE ^ <- first (F) inputs
LMNO...DEF |
MNOP...EFG |  Model's future inputs
NOPQ...FGH |
OPQR...GHI V

Теперь, если мы согласны с тем, что наша модель должна выполняться для отдельных столбцов данных, мы только что ввели новое пакетное измерение. Обратите внимание, что это всего лишь метод управления окнами, применяемый к двум потокам (H и F) данных.

Реализация окон

Необходимо обсудить технические детали того, как реализовать управление окнами в TensorFlow.
Есть несколько возможностей - если будет какая-либо векторизованная версия операции среза или прокрутки, это будет предпочтительнее. Однако, поскольку эти операции отсутствуют в векторизованной версии, мы также можем представить себе использование умножения матриц, которое, к сожалению, требует слишком большого объема памяти.

В нашем окончательном дизайне используется функция сбора. Сначала мы генерируем целочисленные индексы, а затем просто меняем форму массива, чтобы он соответствовал целевому прямоугольнику. (Целевой прямоугольник с формой заданной длины окна и количества повторов. Количество повторов является исходной длиной последовательности - длина окна +1.)

В целом tf документация полна подобных идей (см. Например здесь).

Функция TensorFlow, которую мы получаем, также должна учитывать существующие размеры пакета:

def windowing_mechanism(seq_data, batch_dims, window_len):
    """Reformats data in timesequence dimension into two dimensions of [number of windows, window length] by windowing.
    :param seq_data: Original data with time dimension at seq_data.shape[batch_dims] position
        and features at seq_data.shape[batch_dims:]
    :param batch_dims: Defines the number of dimensions to regard as batch at the start. Expects non-tensor.
    :param window_len: Window length to produce. Expects non-tensor (?)
    :return: windowed original sequence of shape:
        list(data.shape[:batch_dims])
        + [data.shape[batch_dims] - window_len + 1, window_len]
        + list(data.shape[batch_dims + 1 :])
    """
    rep = (
        get_shape_fixed_if_possible(seq_data)[batch_dims] - window_len + 1
    )  # when windowing, we will repeat that many times
    # as is first nonbatch dimension - window len + 1
    i = tf.range(0, window_len * rep, delta=1, dtype=None, name="range")  # now we will produce indexes for gather
    indices_orig = tf.math.floormod(i, window_len) + tf.math.floordiv(i, window_len)
    # in this manner: [[ 1,  2,  3,  4,  5,  ..  2,  3,  4,  5,  6,  ..  3,  4,  5,  6,  7, .. ]]
    # expand indices for aligning with batch dimension to use tf.gather on batched data:
    indices = expand_to_match_first_dims(indices_orig, seq_data, batch_dims)
    # should match all the batch dims before!
    result = tf.gather(seq_data, indices, batch_dims=batch_dims,)
    # now reshape it in the form of the matrix we want:
    new_shape = tf.concat(
        [tf.shape(seq_data)[:batch_dims], [rep, window_len], tf.shape(seq_data)[batch_dims + 1 :]], axis=-1
    )
    windowed = tf.reshape(result, new_shape)
    return windowed
    # ... now regard the first (batch_dims+1) dimensions as batches for any model to apply

Теперь, если мы рассматриваем TFT только как черный ящик с возможностью пакетной передачи, эту функцию необходимо использовать как для входов F, так и для H. Вместе с сквошем размерностью партии:

# windowing described in the docstring ^
historical_windowed = windowing_mechanism(
    historical_inputs, batch_dims=1, window_len=self.num_encoder_steps
)
# since the insides of the model are not easily used on 4 dim sequence with first two dimesnions being batch
# we need to squash the dimensions into the batch dimension:
historical_windowed_squashed, historical_windowed_orig_size = squash_batch_dimensions(
    historical_windowed, batch_dims=2
)
# now we must repeat the batch dimension for static inputs to match the new batch dimension of "squashed"
static_emb_repeated = repeat_multiply_batch_dimension(static_emb, tf.shape(historical_windowed)[1])
future_windowed = windowing_mechanism(future_emb, batch_dims=1, window_len=self.get_future_fork_size())
future_windowed_squashed, future_windowed_orig_size = squash_batch_dimensions(future_windowed, batch_dims=2)
transformer_layer_squashed, attention_components = self.build_base_tft_graph(
    historical_windowed_squashed, future_windowed_squashed, static_emb_repeated
)
# back to 4D: (from 3D squashed to batch dimension)
transformer_layer = unsquash_batch_dimensions(transformer_layer_squashed, historical_windowed_orig_size)

Прелесть этого подхода заключается в том, что его можно использовать в любой другой модели с еще большей простотой (когда не нужны потоки F и H).

(Эта версия модели протестирована в test_tft_model_vectorized_saving , чтобы правильно сохранить и выполнить график).

Подробнее о предоставленном коде

Углубившись в модель, мы могли бы заставить модель принимать только один поток (маскируя некоторые входные данные из H, когда требовалось F) и используя трюки пакетирования-распаковки только в необходимых местах.
Оказывается, что только LSTM (и, вероятно, внимание) действительно нуждался в трюке с размерным пакетом, все остальное было хорошо векторизовано (с некоторыми незначительными изменениями по исправлению отрицательных размеров).

(Окончательная версия протестирована в test_tft_single_sequence.)

Технические примечания

  • Все тесты разделены на свои собственные процессы, чтобы не искажать графы TF.
  • Объекты TFT сохраняют свой исходный интерфейс, но некоторые параметры теперь не используются в новых подходах. Это своего рода баланс, пытаясь сохранить исходный код как можно более нетронутым и предоставляя новые средства выполнения.
  • Разница между работой в качестве черного ящика и распространением векторизации глубоко внутри модели основана на параметре, позволяющем экспериментировать.

Настройка сравнения

Мы создали экспериментальную установку с искусственным рядом с некоторым шумом и некоторыми скрытыми вычислительными правилами, чтобы протестировать оба варианта модели на графическом процессоре.
Чтобы сделать тест максимально приближенным к реальности, мы разрешили использовать базовую версию размер пакета больше, чем у векторизованного.
Во-первых, с точки зрения эпох, мы можем увидеть колоссальное ускорение:

  • от 18 секунд на эпоху (базовая версия):
Epoch 4/4
901/901 [==============================] - 21s 23ms/step - loss: 0.4995 - val_loss: 0.5315
  • до 5 секунд на эпоху (векторизованная версия):
Epoch 4/13
32/32 [==============================] - 5s 151ms/step - loss: 0.5060 - val_loss: 0.5298

Время одного шага больше в векторизованной версии, поскольку один шаг означает цикл по всей последовательности (в то время как у нас есть 32 последовательности на эпоху).

Хотя инженерная победа в ускорении более чем в 4 раза в эпоху очень обнадеживает, нам также необходимо проверить время, чтобы достичь того же уровня потери валидации.

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

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

Мы построили график времени до эпохи вместе с потерей валидации для обеих настроек (зеленый - векторизованный, синий - базовый):

Если мы снова посмотрим на логи обеих эпох №4, то увидим, что базовая версия приблизилась к уровню 0,5315 за 88 секунд. В то время как векторизованная версия была на уровне 0,5298 до истечения 30 секунд.

Итак, в этом эксперименте векторизованная версия примерно в 3 раза быстрее с точки зрения потерь.

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

Отметим, что наше собственное приложение модели TFT демонстрирует ускорение, соответствующее экспериментальной установке, представленной здесь.

Заключение

Мы перевели модель TFT в TensorFlow 2.0 и продемонстрировали технику векторизации для работы с окнами.
Векторизация повышает производительность как с точки зрения времени, так и времени для достижения тех же потерь при проверке.

В простом тесте (который вы можете воспроизвести путем клонирования из нашего github) мы продемонстрировали сокращение времени на эпоху в ~ 4 раза и время до тех же потерь в ~ 3 раза, что согласуется с нашими собственными реальными данными. -Мировые приложения для обработки данных.

Представленный метод не является прямой заменой какой-либо техники работы с окнами, но должен служить дополнением к библиотеке методов (как, например, новый оптимизатор будет служить в контексте всех существующих оптимизаторов).

Благодарности

Мы хотели бы поблагодарить команду инженеров Google за их первоначальную работу над моделью TFT и, конечно же, наших коллег по AMP и AMPX, а именно Саймона Ондрачека и Роберта Сухада за их вклад в проект.