Я пытался воспроизвести, как использовать упаковку для входных данных последовательности переменной длины для rnn, но я думаю, что сначала мне нужно понять, почему нам нужно «упаковать» последовательность.
Я понимаю, почему нам нужно их «прокладывать», но почему это pack_padded_sequence
необходимо?
Приветствуются любые объяснения высокого уровня!
Ответы:
Я тоже наткнулся на эту проблему, и ниже я понял.
При обучении RNN (LSTM или GRU или vanilla-RNN) сложно группировать последовательности переменной длины. Например: если длина последовательностей в пакете размером 8 составляет [4,6,8,5,4,3,7,8], вы дополните все последовательности, и в результате получится 8 последовательностей длиной 8. Вы закончится 64 вычислениями (8x8), но вам нужно было сделать только 45 вычислений. Более того, если вы хотите сделать что-то необычное, например, использовать двунаправленную RNN, было бы сложнее выполнять пакетные вычисления просто путем заполнения, и вы могли бы выполнить больше вычислений, чем требуется.
Вместо этого PyTorch позволяет нам упаковать последовательность, внутренне упакованная последовательность представляет собой кортеж из двух списков. Один содержит элементы последовательностей. Элементы чередуются по временным шагам (см. Пример ниже), а другие содержат
размер каждой последовательностиразмер пакета на каждом шаге. Это полезно для восстановления фактических последовательностей, а также для сообщения RNN, каков размер пакета на каждом временном шаге. На это указала @Aerin. Это можно передать в RNN, и она оптимизирует вычисления внутри.Возможно, я был не совсем ясен в некоторых моментах, поэтому дайте мне знать, и я могу добавить больше объяснений.
Вот пример кода:
источник
Вот несколько наглядных объяснений 1, которые могут помочь лучше понять функции
pack_padded_sequence()
Предположим, у нас есть всего
6
последовательности (переменной длины). Вы также можете рассматривать это число6
какbatch_size
гиперпараметр.Теперь мы хотим передать эти последовательности некоторой повторяющейся архитектуре (-ам) нейронной сети. Для этого мы должны дополнить все последовательности (обычно с
0
s) в нашем пакете до максимальной длины последовательности в нашем batch (max(sequence_lengths)
), которая на рисунке ниже9
.Итак, работа по подготовке данных должна быть завершена, верно? Не совсем ... Потому что есть еще одна насущная проблема, в основном с точки зрения того, сколько вычислений нам нужно сделать по сравнению с фактически необходимыми вычислениями.
Для понимания давайте также предположим, что мы будем матрично умножать указанное выше
padded_batch_of_sequences
формы(6, 9)
на матрицу весовW
формы(9, 3)
.Таким образом, нам придется выполнять операции
6x9 = 54
умножения и6x8 = 48
сложения (nrows x (n-1)_cols
) только для того, чтобы отбросить большую часть вычисленных результатов, поскольку они будут0
s (там, где у нас есть прокладки). Фактические необходимые вычисления в этом случае следующие:Это НАМНОГО больше экономии даже для этого очень простого ( игрушечного ) примера. Теперь вы можете представить, сколько вычислений (в конечном итоге: затраты, энергия, время, выбросы углерода и т. Д.) Можно сэкономить, используя
pack_padded_sequence()
большие тензоры с миллионами записей, и более миллиона систем по всему миру делают это снова и снова.Функциональные
pack_padded_sequence()
возможности можно понять из рисунка ниже с помощью используемой цветовой кодировки:В результате использования
pack_padded_sequence()
мы получим кортеж тензоров, содержащий (i) сглаженные (по оси-1, на приведенном выше рисунке)sequences
, (ii) соответствующие размеры пакетовtensor([6,6,5,4,3,3,2,2,1])
для приведенного выше примера.Тензор данных (то есть сглаженные последовательности) можно затем передать в целевые функции, такие как CrossEntropy, для расчета потерь.
1 кредит изображения для @sgrvinod
источник
Вышеупомянутые ответы очень хорошо отвечали на вопрос, почему . Я просто хочу добавить пример для лучшего понимания использования
pack_padded_sequence
.Возьмем пример
Сначала мы создаем пакет из 2 последовательностей разной длины, как показано ниже. Всего в партии 7 элементов.
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>))) """
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]) """
Сравните это усилие со стандартным способом
Стандартным образом, нам нужно только передать
padded_seq_batch
вlstm
модуль. Однако для этого требуется 10 вычислений. Он включает в себя несколько дополнительных вычислений для элементов заполнения, что было бы неэффективно с точки зрения вычислений .Обратите внимание, что это не приводит к неточным представлениям, но для извлечения правильных представлений требуется гораздо больше логики.
Посмотрим на разницу:
# 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
два способа приводят к различным значениям для элементов заполнения.источник
Добавляя к ответу Уманга, я счел это важным отметить.
Первый элемент в возвращаемом кортеже
pack_padded_sequence
- это данные (тензор) - тензор, содержащий упакованную последовательность. Второй элемент - тензор целых чисел, содержащий информацию о размере пакета на каждом шаге последовательности.Но что здесь важно, так это то, что второй элемент (размеры пакета) представляет количество элементов на каждом шаге последовательности в пакете, а не передаваемые длины различных последовательностей
pack_padded_sequence
.Например, данные
abc
иx
: class:PackedSequence
будут содержать данныеaxbc
с расширениемbatch_sizes=[2,1,1]
.источник
Я использовал упакованную последовательность следующим образом.
где text_lengths - длина отдельной последовательности до заполнения, а последовательность сортируется в соответствии с порядком убывания длины в данном пакете.
вы можете посмотреть пример здесь .
И мы делаем упаковку, чтобы RNN не видела нежелательный дополненный индекс во время обработки последовательности, которая могла бы повлиять на общую производительность.
источник