почему мы «упаковываем» последовательности в pytorch?

93

Я пытался воспроизвести, как использовать упаковку для входных данных последовательности переменной длины для rnn, но я думаю, что сначала мне нужно понять, почему нам нужно «упаковать» последовательность.

Я понимаю, почему нам нужно их «прокладывать», но почему это pack_padded_sequenceнеобходимо?

Приветствуются любые объяснения высокого уровня!

Aerin
источник
все вопросы по упаковке в pytorch: Discussion.pytorch.org/t/…
Чарли Паркер

Ответы:

88

Я тоже наткнулся на эту проблему, и ниже я понял.

При обучении RNN (LSTM или GRU или vanilla-RNN) сложно группировать последовательности переменной длины. Например: если длина последовательностей в пакете размером 8 составляет [4,6,8,5,4,3,7,8], вы дополните все последовательности, и в результате получится 8 последовательностей длиной 8. Вы закончится 64 вычислениями (8x8), но вам нужно было сделать только 45 вычислений. Более того, если вы хотите сделать что-то необычное, например, использовать двунаправленную RNN, было бы сложнее выполнять пакетные вычисления просто путем заполнения, и вы могли бы выполнить больше вычислений, чем требуется.

Вместо этого PyTorch позволяет нам упаковать последовательность, внутренне упакованная последовательность представляет собой кортеж из двух списков. Один содержит элементы последовательностей. Элементы чередуются по временным шагам (см. Пример ниже), а другие содержат размер каждой последовательности размер пакета на каждом шаге. Это полезно для восстановления фактических последовательностей, а также для сообщения RNN, каков размер пакета на каждом временном шаге. На это указала @Aerin. Это можно передать в RNN, и она оптимизирует вычисления внутри.

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

Вот пример кода:

 a = [torch.tensor([1,2,3]), torch.tensor([3,4])]
 b = torch.nn.utils.rnn.pad_sequence(a, batch_first=True)
 >>>>
 tensor([[ 1,  2,  3],
    [ 3,  4,  0]])
 torch.nn.utils.rnn.pack_padded_sequence(b, batch_first=True, lengths=[3,2])
 >>>>PackedSequence(data=tensor([ 1,  3,  2,  4,  3]), batch_sizes=tensor([ 2,  2,  1]))
Уманг Гупта
источник
4
Можете ли вы объяснить, почему результат данного примера - PackedSequence (данные = тензор ([1, 3, 2, 4, 3]), batch_sizes = тензор ([2, 2, 1]))?
ascetic652 07
3
Часть данных - это просто все тензоры, сцепленные по оси времени. Batch_size - это фактически массив размеров партии на каждом временном шаге.
Уманг Гупта
2
Batch_sizes = [2, 2, 1] представляет группировку [1, 3] [2, 4] и [3] соответственно.
Чайтанья Шиваде
@ChaitanyaShivade, почему размер партии [2,2,1]? не может быть [1,2,2]? какая логика стоит за этим?
Анонимный программист
1
Поскольку на шаге t вы можете обрабатывать векторы только на шаге t, если вы сохраняете векторы, упорядоченные как [1,2,2], вы, вероятно, помещаете каждый вход как пакет, но он не может быть распараллелен и, следовательно, не доступен для пакетной обработки
Уманг Гупта
51

Вот несколько наглядных объяснений 1, которые могут помочь лучше понять функцииpack_padded_sequence()

Предположим, у нас есть всего 6последовательности (переменной длины). Вы также можете рассматривать это число 6как batch_sizeгиперпараметр.

Теперь мы хотим передать эти последовательности некоторой повторяющейся архитектуре (-ам) нейронной сети. Для этого мы должны дополнить все последовательности (обычно с 0s) в нашем пакете до максимальной длины последовательности в нашем batch ( max(sequence_lengths)), которая на рисунке ниже 9.

padded-seqs

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

Для понимания давайте также предположим, что мы будем матрично умножать указанное выше padded_batch_of_sequencesформы (6, 9)на матрицу весов Wформы (9, 3).

Таким образом, нам придется выполнять операции 6x9 = 54умножения и 6x8 = 48сложения                     ( nrows x (n-1)_cols) только для того, чтобы отбросить большую часть вычисленных результатов, поскольку они будут 0s (там, где у нас есть прокладки). Фактические необходимые вычисления в этом случае следующие:

 9-mult  8-add 
 8-mult  7-add 
 6-mult  5-add 
 4-mult  3-add 
 3-mult  2-add 
 2-mult  1-add
---------------
32-mult  26-add
   
------------------------------  
#savings: 22-mult & 22-add ops  
          (32-54)  (26-48) 

Это НАМНОГО больше экономии даже для этого очень простого ( игрушечного ) примера. Теперь вы можете представить, сколько вычислений (в конечном итоге: затраты, энергия, время, выбросы углерода и т. Д.) Можно сэкономить, используя pack_padded_sequence()большие тензоры с миллионами записей, и более миллиона систем по всему миру делают это снова и снова.

Функциональные pack_padded_sequence()возможности можно понять из рисунка ниже с помощью используемой цветовой кодировки:

pack-padded-seqs

В результате использования pack_padded_sequence()мы получим кортеж тензоров, содержащий (i) сглаженные (по оси-1, на приведенном выше рисунке) sequences, (ii) соответствующие размеры пакетов tensor([6,6,5,4,3,3,2,2,1])для приведенного выше примера.

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


1 кредит изображения для @sgrvinod

kmario23
источник
2
Отличные диаграммы!
Дэвид Уотеруорт,
1
Изменить: я думаю, что stackoverflow.com/a/55805785/6167850 (ниже) отвечает на мой вопрос, который я все равно оставлю здесь: ~ Означает ли это, что градиенты не распространяются на заполненные входы? Что, если моя функция потерь вычисляется только по окончательному скрытому состоянию / выходу RNN? Следует ли тогда отбрасывать повышение эффективности? Или потери будут вычисляться из шага, предшествующего тому, где начинается заполнение, которое отличается для каждого элемента пакета в этом примере? ~
nlml
25

Вышеупомянутые ответы очень хорошо отвечали на вопрос, почему . Я просто хочу добавить пример для лучшего понимания использования pack_padded_sequence.

Возьмем пример

Примечание. pack_padded_sequenceТребуются отсортированные последовательности в пакете (в порядке убывания длины последовательности). В приведенном ниже примере партия последовательности уже отсортирована для уменьшения загромождения. Посетите эту главную ссылку для полной реализации.

Сначала мы создаем пакет из 2 последовательностей разной длины, как показано ниже. Всего в партии 7 элементов.

  • Каждая последовательность имеет размер встраивания 2.
  • Первая последовательность имеет длину: 5
  • Вторая последовательность имеет длину: 2
import torch 

seq_batch = [torch.tensor([[1, 1],
                           [2, 2],
                           [3, 3],
                           [4, 4],
                           [5, 5]]),
             torch.tensor([[10, 10],
                           [20, 20]])]

seq_lens = [5, 2]

Мы дополняем, seq_batchчтобы получить пакет последовательностей с равной длиной 5 (максимальная длина в пакете). Теперь в новом пакете всего 10 элементов.

# pad the seq_batch
padded_seq_batch = torch.nn.utils.rnn.pad_sequence(seq_batch, batch_first=True)
"""
>>>padded_seq_batch
tensor([[[ 1,  1],
         [ 2,  2],
         [ 3,  3],
         [ 4,  4],
         [ 5,  5]],

        [[10, 10],
         [20, 20],
         [ 0,  0],
         [ 0,  0],
         [ 0,  0]]])
"""

Затем мы упаковываем padded_seq_batch. Он возвращает кортеж из двух тензоров:

  • Первый - это данные, включающие все элементы в пакете последовательности.
  • Второй - это то, batch_sizesчто расскажет, как элементы связаны друг с другом шагами.
# pack the padded_seq_batch
packed_seq_batch = torch.nn.utils.rnn.pack_padded_sequence(padded_seq_batch, lengths=seq_lens, batch_first=True)
"""
>>> packed_seq_batch
PackedSequence(
   data=tensor([[ 1,  1],
                [10, 10],
                [ 2,  2],
                [20, 20],
                [ 3,  3],
                [ 4,  4],
                [ 5,  5]]), 
   batch_sizes=tensor([2, 2, 1, 1, 1]))
"""

Теперь мы передаем кортеж packed_seq_batchв повторяющиеся модули в Pytorch, такие как RNN, LSTM. Для этого требуются только 5 + 2=7вычисления в повторяющемся модуле.

lstm = nn.LSTM(input_size=2, hidden_size=3, batch_first=True)
output, (hn, cn) = lstm(packed_seq_batch.float()) # pass float tensor instead long tensor.
"""
>>> output # PackedSequence
PackedSequence(data=tensor(
        [[-3.6256e-02,  1.5403e-01,  1.6556e-02],
         [-6.3486e-05,  4.0227e-03,  1.2513e-01],
         [-5.3134e-02,  1.6058e-01,  2.0192e-01],
         [-4.3123e-05,  2.3017e-05,  1.4112e-01],
         [-5.9372e-02,  1.0934e-01,  4.1991e-01],
         [-6.0768e-02,  7.0689e-02,  5.9374e-01],
         [-6.0125e-02,  4.6476e-02,  7.1243e-01]], grad_fn=<CatBackward>), batch_sizes=tensor([2, 2, 1, 1, 1]))

>>>hn
tensor([[[-6.0125e-02,  4.6476e-02,  7.1243e-01],
         [-4.3123e-05,  2.3017e-05,  1.4112e-01]]], grad_fn=<StackBackward>),
>>>cn
tensor([[[-1.8826e-01,  5.8109e-02,  1.2209e+00],
         [-2.2475e-04,  2.3041e-05,  1.4254e-01]]], grad_fn=<StackBackward>)))
"""

Нам нужно преобразовать outputобратно в заполненный пакет вывода:

padded_output, output_lens = torch.nn.utils.rnn.pad_packed_sequence(output, batch_first=True, total_length=5)
"""
>>> padded_output
tensor([[[-3.6256e-02,  1.5403e-01,  1.6556e-02],
         [-5.3134e-02,  1.6058e-01,  2.0192e-01],
         [-5.9372e-02,  1.0934e-01,  4.1991e-01],
         [-6.0768e-02,  7.0689e-02,  5.9374e-01],
         [-6.0125e-02,  4.6476e-02,  7.1243e-01]],

        [[-6.3486e-05,  4.0227e-03,  1.2513e-01],
         [-4.3123e-05,  2.3017e-05,  1.4112e-01],
         [ 0.0000e+00,  0.0000e+00,  0.0000e+00],
         [ 0.0000e+00,  0.0000e+00,  0.0000e+00],
         [ 0.0000e+00,  0.0000e+00,  0.0000e+00]]],
       grad_fn=<TransposeBackward0>)

>>> output_lens
tensor([5, 2])
"""

Сравните это усилие со стандартным способом

  1. Стандартным образом, нам нужно только передать padded_seq_batchв lstmмодуль. Однако для этого требуется 10 вычислений. Он включает в себя несколько дополнительных вычислений для элементов заполнения, что было бы неэффективно с точки зрения вычислений .

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

    • Для LSTM (или любых рекуррентных модулей) только с прямым направлением, если мы хотим извлечь скрытый вектор последнего шага в качестве представления последовательности, нам нужно будет выбрать скрытые векторы с шага T (th), где T - длина ввода. Подбирать последнее представление будет неверным. Обратите внимание, что T будет отличаться для разных входов в партии.
    • Для двунаправленного LSTM (или любых повторяющихся модулей) это еще более громоздко, так как нужно поддерживать два модуля RNN, один из которых работает с заполнением в начале ввода, а другой - с заполнением в конце ввода, и наконец, извлечение и объединение скрытых векторов, как описано выше.

Посмотрим на разницу:

# The standard approach: using padding batch for recurrent modules
output, (hn, cn) = lstm(padded_seq_batch.float())
"""
>>> output
 tensor([[[-3.6256e-02, 1.5403e-01, 1.6556e-02],
          [-5.3134e-02, 1.6058e-01, 2.0192e-01],
          [-5.9372e-02, 1.0934e-01, 4.1991e-01],
          [-6.0768e-02, 7.0689e-02, 5.9374e-01],
          [-6.0125e-02, 4.6476e-02, 7.1243e-01]],

         [[-6.3486e-05, 4.0227e-03, 1.2513e-01],
          [-4.3123e-05, 2.3017e-05, 1.4112e-01],
          [-4.1217e-02, 1.0726e-01, -1.2697e-01],
          [-7.7770e-02, 1.5477e-01, -2.2911e-01],
          [-9.9957e-02, 1.7440e-01, -2.7972e-01]]],
        grad_fn= < TransposeBackward0 >)

>>> hn
tensor([[[-0.0601, 0.0465, 0.7124],
         [-0.1000, 0.1744, -0.2797]]], grad_fn= < StackBackward >),

>>> cn
tensor([[[-0.1883, 0.0581, 1.2209],
         [-0.2531, 0.3600, -0.4141]]], grad_fn= < StackBackward >))
"""

Приведенные выше результаты показывают, что hn, cnразличаются двумя способами, в то время как outputдва способа приводят к различным значениям для элементов заполнения.

Дэвид Нг
источник
2
Хороший ответ! Просто исправление, если вы выполняете заполнение, вы не должны использовать последний h, а не h в индексе, равном длине ввода. Кроме того, для создания двунаправленной RNN вы можете использовать два разных RNN - один с заполнением спереди, а другой с заполнением сзади, чтобы получить правильные результаты. Заполнение и выбор последнего вывода "неправильный". Итак, ваши аргументы в пользу неточного представления неверны. Проблема с заполнением в том, что оно правильное, но неэффективное (если есть опция упакованных последовательностей) и может быть громоздким (например: bi-dir RNN)
Уманг Гупта
18

Добавляя к ответу Уманга, я счел это важным отметить.

Первый элемент в возвращаемом кортеже pack_padded_sequence- это данные (тензор) - тензор, содержащий упакованную последовательность. Второй элемент - тензор целых чисел, содержащий информацию о размере пакета на каждом шаге последовательности.

Но что здесь важно, так это то, что второй элемент (размеры пакета) представляет количество элементов на каждом шаге последовательности в пакете, а не передаваемые длины различных последовательностей pack_padded_sequence.

Например, данные abcи x : class: PackedSequenceбудут содержать данные axbcс расширением batch_sizes=[2,1,1].

Aerin
источник
1
Спасибо, я совсем забыл об этом. и сделал ошибку в своем ответе, собираясь обновить это. Однако я посмотрел на вторую последовательность как на некоторые данные, необходимые для восстановления последовательностей, и поэтому испортил свое описание
Уманг Гупта
2

Я использовал упакованную последовательность следующим образом.

packed_embedded = nn.utils.rnn.pack_padded_sequence(seq, text_lengths)
packed_output, hidden = self.rnn(packed_embedded)

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

вы можете посмотреть пример здесь .

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

Джибин Мэтью
источник