почему мы "собираем" последовательности в pytorch?

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

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

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

Ответы

Ответ 1

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Ответ 4

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

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

Теперь мы хотим передать эти последовательности некоторым рекуррентным архитектурам нейронной сети. Для этого нам нужно добавить все последовательности (обычно с 0) в нашем пакете к максимальной длине последовательности в нашем пакете (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

Ответ 6

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

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

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

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

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

Ответ 7

a = [torch.tensor([5,6,7,8]) ,torch.tensor([1,2,3]), torch.tensor([3,4]) ]
#Out[11]: [tensor([ 5,  6,  7,  8]), tensor([ 1,  2,  3]), tensor([ 3,  4])]
b = torch.nn.utils.rnn.pad_sequence(a, batch_first=True)
# =============================================================================
#The first item in the returned tuple of pack_padded_sequence is a data (tensor)- tensor containing packed sequence.
# torch.nn.utils.rnn.pad_sequence(sequences, batch_first=False, padding_value=0)
# B x T x * if batch_first = True
# B is batch size. It is equal to the number of elements in sequences.
# T is length of the longest sequence
# * is any number of trailing dimensions, including none
#tensor([[ 5,  6,  7,  8],
#        [ 1,  2,  3,  0],
#        [ 3,  4,  0,  0]])
#  
#b.size()
#Out[14]: torch.Size([3, 4])
# =============================================================================
#lengths (Tensor) – list of sequences lengths of each batch element.
torch.nn.utils.rnn.pack_padded_sequence(b, batch_first=True, lengths=[4,3,2])
#Out[15]: PackedSequence(data=tensor([ 5,  1,  3,  6,  2,  4,  7,  3,  8]), batch_sizes=tensor([ 3,  3,  2,  1]))
#If we look into this data columnwise, the batch size is 3,3,2,1 (leave the pad) and the data is in that sequence.
#tensor([[ 5,  6,  7,  8],
#        [ 1,  2,  3,  0],
#        [ 3,  4,  0,  0]])