pytorch에서 시퀀스를 "포장"하는 이유는 무엇입니까?


93

rnn에 대한 가변 길이 시퀀스 입력에 패킹을 사용하는 방법 을 복제하려고 했지만 먼저 시퀀스를 "포장"해야하는 이유를 이해해야합니다.

나는 우리가 그것들을 "패딩"해야하는 이유를 이해하지만 "패킹"(부터 pack_padded_sequence)이 필요한 이유는 무엇입니까?

높은 수준의 설명을 주시면 감사하겠습니다!


: pytorch에 포장에 대한 모든 질문 discuss.pytorch.org/t/...
찰리 파커 (Charlie Parker)을

답변:


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 (data = tensor ([1, 3, 2, 4, 3]), batch_sizes = tensor ([2, 2, 1])) 인 이유를 설명 할 수 있습니까?
ascetic652

3
데이터 부분은 시간 축을 따라 연결된 모든 텐서입니다. Batch_size는 실제로 각 시간 단계에서 배치 크기의 배열입니다.
Umang Gupta 2018

2
batch_sizes = [2, 2, 1]은 각각 [1, 3] [2, 4] 및 [3] 그룹을 나타냅니다.
Chaitanya Shivade

@ChaitanyaShivade 왜 배치 크기 [2,2,1]입니까? [1,2,2] 일 수 없나요? 그 뒤에있는 논리는 무엇입니까?
익명의 프로그래머

1
당신이 벡터 [1,2,2-는, 당신은 아마 배치로 각 입력을 가하고 있습니다,하지만 그 병렬화 할 수 없습니다 따라서 batchable 수 없습니다로 주문할 유지하는 경우 때문에 단계 t에서, 당신은 단지, 단계 t에서 벡터를 처리 할 수
우망 굽타

51

여기에 몇 가지 시각적 인 설명 도 1은 힘의 도움의 기능을보다 직관을 개발하는 것을pack_padded_sequence()

6전체적으로 (가변 길이의) 시퀀스 가 있다고 가정 해 봅시다 . 이 숫자 6batch_size하이퍼 파라미터 로 간주 할 수도 있습니다 .

이제 우리는 이러한 시퀀스를 일부 순환 신경망 아키텍처에 전달하려고합니다. 이렇게하려면 0배치의 모든 시퀀스 (일반적으로 s)를 배치의 최대 시퀀스 길이 ( max(sequence_lengths)아래 그림에서)까지 채워야 9합니다.

패딩 시퀀스

자, 데이터 준비 작업은 지금 쯤 끝났 겠죠? 별로 .. 왜냐하면 실제로 필요한 계산과 비교할 때 우리가 얼마나 많은 계산을해야하는지에 관한 한 가지 시급한 문제가 있기 때문입니다.

이해를 돕기 위해 padded_batch_of_sequencesshape (6, 9)의 가중치 행렬 W과 shape 의 가중치 행렬 을 행렬로 곱한다고 가정하겠습니다 (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와 같은 목적 함수로 전달 될 수 있습니다.


@sgrvinod의 이미지 크레딧 1


2
훌륭한 다이어그램!
David Waterworth

1
편집 : 나는 stackoverflow.com/a/55805785/6167850 (아래)이 내 질문에 대답 한다고 생각 합니다. 어쨌든 여기에 남겨 둘 것입니다 : ~ 이것은 본질적으로 그라디언트가 패딩 된 입력으로 전파되지 않음을 의미합니까? 손실 함수가 RNN의 최종 숨겨진 상태 / 출력에서만 계산되면 어떻게됩니까? 그러면 효율성 향상을 버려야합니까? 아니면 패딩이 시작되기 전 단계에서 손실이 계산됩니까?이 예에서는 배치 요소마다 다릅니다. ~
nlml

25

위의 답변은 아주 좋은지에 대한 질문을 해결했습니다 . 의 사용을 더 잘 이해하기 위해 예제를 추가하고 싶습니다 pack_padded_sequence.

예를 들어 봅시다

참고 : pack_padded_sequence배치에 정렬 된 시퀀스가 ​​필요합니다 (시퀀스 길이의 내림차순). 아래 예에서 시퀀스 배치는 덜 복잡하게 정렬되어 있습니다. 전체 구현을 보려면이 요점 링크 를 방문하십시오 .

먼저, 아래와 같이 서로 다른 시퀀스 길이의 두 시퀀스 배치를 만듭니다. 우리는 배치에 총 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을 RNN, LSTM과 같은 Pytorch의 반복 모듈에 전달합니다 . 이것은 5 + 2=7recurrrent 모듈 에서만 계산을 필요로 합니다.

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_batchto 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를 사용하지 않아야합니다. 또한 양방향 RNN을 수행하려면 두 개의 서로 다른 RNN을 사용할 수 있습니다. 하나는 앞면에 패딩이 있고 다른 하나는 뒷면에 패딩이있어 올바른 결과를 얻습니다. 패딩 및 마지막 출력 선택이 "잘못됨"입니다. 따라서 그것이 부정확 한 표현으로 이어진다는 당신의 주장은 잘못된 것입니다. 패딩의 문제는 정확하지만 비효율적이며 (패킹 된 시퀀스 옵션이있는 경우) 번거로울 수 있습니다 (예 : bi-dir RNN)
Umang Gupta

18

Umang의 대답에 덧붙여서 나는 이것이 중요하다는 것을 알았습니다.

반환 된 튜플의 첫 번째 항목은 pack_padded_sequence패킹 된 시퀀스를 포함하는 데이터 (텐서)-텐서입니다. 두 번째 항목은 각 시퀀스 단계에서 배치 크기에 대한 정보를 보유하는 정수의 텐서입니다.

여기서 중요한 것은 두 번째 항목 (배치 크기)이 .NET에 전달 된 다양한 시퀀스 길이가 아니라 배치의 각 시퀀스 단계에서 요소 수를 나타냅니다 pack_padded_sequence.

예를 들어, 데이터 제공 abcx 클래스 : 다음 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이 원하지 않는 패딩 된 인덱스를 보지 못하도록 패킹합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.