чому ми "пакуємо" послідовності в pytorch?


94

Я намагався повторити Як використовувати пакування для входів послідовності змінної довжини для rnn, але, мабуть, спочатку потрібно зрозуміти, чому нам потрібно "упакувати" послідовність.

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

Будь-яке пояснення високого рівня буде вдячне!


усі питання щодо упаковки в pytorch: discuss.pytorch.org/t/…
Чарлі Паркер

Відповіді:


89

Я теж натрапив на цю проблему, і ось що я зрозумів нижче.

Під час тренування RNN (LSTM або GRU або ванільний 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

3
Частина даних - це просто всі тензори, об'єднані вздовж осі часу. Batch_size - це фактично масив розмірів партії на кожному кроці часу.
Уманг Гупта,

2
Розмір_пакету = [2, 2, 1] представляє групування [1, 3] [2, 4] та [3] відповідно.
Чайтанья Шиваде

@ChaitanyaShivade, чому розмір партії [2,2,1]? не може бути [1,2,2]? яка логіка за цим?
Анонімний програміст

1
Оскільки на кроці t ви можете обробляти вектори лише на кроці t, якщо ви тримаєте вектори упорядкованими як [1,2,2], ви, ймовірно, ставите кожен вхід як пакет, але це неможливо паралелізувати і, отже, не піддавати партії
Уманг Гупта

52

Ось деякі наочні пояснення 1, які можуть допомогти розвинути кращу інтуїцію щодо функціональностіpack_padded_sequence()

Припустимо, що 6загалом у нас є послідовності (змінної довжини). Ви також можете розглянути це число 6як batch_sizeгіперпараметр. (Значення batch_sizeваріюватиметься залежно від довжини послідовності (див. Рис. 2 нижче))

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

заповнені сек

Отже, робота з підготовки даних вже має бути завершена, чи не так? Насправді не .. Оскільки є ще одна нагальна проблема, головним чином з точки зору того, скільки обчислень нам потрібно зробити порівняно з фактично необхідними обчисленнями.

Для розуміння, давайте також припустимо, що ми будемо помножувати вищезазначену 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_sequence()ми отримаємо кортеж тензорів, що містить (i) сплющений (вздовж осі-1, на наведеному малюнку) sequences, (ii) відповідні розміри партії, tensor([6,6,5,4,3,3,2,2,1])для наведеного вище прикладу.

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


1 зображення надано @sgrvinod


2
Відмінні схеми!
Девід Вотерворт

1
Редагувати: Я думаю, що stackoverflow.com/a/55805785/6167850 (внизу) відповідає на моє запитання, яке я все одно залишу тут: ~ Чи означає це по суті, що градієнти не розповсюджуються на вкладені введені дані? Що робити, якщо моя функція втрат обчислюється лише для остаточного прихованого стану / виводу RNN? Чи слід тоді викидати підвищення ефективності? Або втрати будуть обчислюватися з кроку до початку заповнення, який відрізняється для кожного елемента партії в цьому прикладі? ~
nlml

26

Наведені вище відповіді стосувалися питання, чому дуже добре. Я просто хочу додати приклад для кращого розуміння використання 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 - один з відступами спереду, а інший з відступами ззаду, щоб отримати правильні результати. Заповнення та вибір останнього виводу є "неправильним". Тож ваші аргументи, що це призводить до неточного подання, є помилковими. Проблема з заповненням полягає в її правильності, але неефективності (якщо є можливість упакованих послідовностей) і може бути громіздкою (наприклад: бі-дир RNN)
Уманг Гупта,

18

Додаючи до відповіді Уманга, я знайшов це важливим для зауваження.

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

Тут важливо лише те, що другий елемент (розміри партії) представляє кількість елементів на кожному кроці послідовності в пакеті, а не різну довжину послідовності, передану pack_padded_sequence.

Наприклад, дані abcта x клас: class: PackedSequenceміститимуть дані axbcз batch_sizes=[2,1,1].


1
Дякую, я це зовсім забув. і помилився у своїй відповіді, збираючись це оновити. Однак я подивився на другу послідовність як на деякі дані, необхідні для відновлення послідовностей, і тому зіпсував мій опис
Уманг Гупта,

2

Я використав упаковану послідовність упаковки наступним чином.

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

де text_lengths - це довжина окремої послідовності перед заповненням, а послідовність сортується відповідно до порядку зменшення довжини в межах даної партії.

Ви можете переглянути приклад тут .

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.