почему мы «упаковываем» последовательности в pytorch? - PullRequest
0 голосов
/ 25 июня 2018

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

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

Любое объяснение на высоком уровне приветствуется!

Ответы [ 5 ]

0 голосов
/ 19 мая 2019

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

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

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

padded-seqs

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

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

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

 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

Это НАМНОГО больше экономии даже для этого игрушечного примера.Теперь вы можете представить, сколько вычислений (затрат, энергии, времени, выбросов углерода и т. Д.) Можно сэкономить, используя 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

0 голосов
/ 23 апреля 2019

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

0 голосов
/ 26 июня 2018

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

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

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

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

0 голосов
/ 07 февраля 2019

Я использовал следующую последовательность:

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

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

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

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

0 голосов
/ 25 июня 2018

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

При обучении 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]))
...