rnn에 대한 가변 길이 시퀀스 입력에 패킹을 사용하는 방법 을 복제하려고 했지만 먼저 시퀀스를 “포장”해야하는 이유를 이해해야합니다.
나는 우리가 그것들을 “패딩”해야하는 이유를 이해하지만 “패킹”(부터 pack_padded_sequence
)이 필요한 이유는 무엇입니까?
높은 수준의 설명을 주시면 감사하겠습니다!
답변
나는이 문제도 우연히 발견했으며 아래는 내가 알아 낸 것입니다.
RNN (LSTM 또는 GRU 또는 vanilla-RNN)을 훈련 할 때 가변 길이 시퀀스를 일괄 처리하기가 어렵습니다. 예를 들어, 크기 8 배치의 시퀀스 길이가 [4,6,8,5,4,3,7,8]이면 모든 시퀀스를 채우고 길이가 8 인 시퀀스 8 개가됩니다. 결국 64 개의 계산 (8×8)을 수행하지만 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]))
답변
여기에 몇 가지 시각적 인 설명 도 1은 힘의 도움의 기능을보다 직관을 개발하는 것을pack_padded_sequence()
6
전체적으로 (가변 길이의) 시퀀스 가 있다고 가정 해 봅시다 . 이 숫자 6
를 batch_size
하이퍼 파라미터 로 간주 할 수도 있습니다 .
이제 우리는 이러한 시퀀스를 일부 순환 신경망 아키텍처에 전달하려고합니다. 이렇게하려면 0
배치의 모든 시퀀스 (일반적으로 s)를 배치의 최대 시퀀스 길이 ( max(sequence_lengths)
아래 그림에서)까지 채워야 9
합니다.
자, 데이터 준비 작업은 지금 쯤 끝났 겠죠? 별로 .. 왜냐하면 실제로 필요한 계산과 비교할 때 우리가 얼마나 많은 계산을해야하는지에 관한 한 가지 시급한 문제가 있기 때문입니다.
이해를 돕기 위해 padded_batch_of_sequences
shape (6, 9)
의 가중치 행렬 W
과 shape 의 가중치 행렬 을 행렬로 곱한다고 가정하겠습니다 (9, 3)
.
따라서 6x9 = 54
곱셈 과 6x8 = 48
덧셈
( nrows x (n-1)_cols
) 연산 을 수행해야 하며 계산 된 결과의 대부분은 0
s (패드가있는 곳) 이므로 버려야 합니다. 이 경우 실제 필요한 계산은 다음과 같습니다.
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 개
답변
위의 답변은 왜 아주 좋은지에 대한 질문을 해결했습니다 . 의 사용을 더 잘 이해하기 위해 예제를 추가하고 싶습니다 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=7
recurrrent 모듈 에서만 계산을 필요로 합니다.
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])
"""
이 노력을 표준 방법과 비교
-
표준 방식에서는
padded_seq_batch
tolstm
모듈 만 전달하면 됩니다 . 그러나 10 번의 계산이 필요합니다. 계산적으로 비효율적 인 패딩 요소에 대해 더 많은 계산이 포함됩니다 . -
부정확 한 표현으로 이어지지는 않지만 올바른 표현을 추출하려면 훨씬 더 많은 논리가 필요합니다.
- 순방향 만있는 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
두 가지 방식에서 패딩 요소의 값 이 서로 다르다는 것을 보여줍니다 .
답변
Umang의 대답에 덧붙여서 나는 이것이 중요하다는 것을 알았습니다.
반환 된 튜플의 첫 번째 항목은 pack_padded_sequence
패킹 된 시퀀스를 포함하는 데이터 (텐서)-텐서입니다. 두 번째 항목은 각 시퀀스 단계에서 배치 크기에 대한 정보를 보유하는 정수의 텐서입니다.
여기서 중요한 것은 두 번째 항목 (배치 크기)이 .NET에 전달 된 다양한 시퀀스 길이가 아니라 배치의 각 시퀀스 단계에서 요소 수를 나타냅니다 pack_padded_sequence
.
예를 들어, 데이터 제공 abc
및 x
클래스 : 다음 PackedSequence
데이터를 포함하는 것 axbc
과를
batch_sizes=[2,1,1]
.
답변
다음과 같이 팩 패딩 시퀀스를 사용했습니다.
packed_embedded = nn.utils.rnn.pack_padded_sequence(seq, text_lengths)
packed_output, hidden = self.rnn(packed_embedded)
여기서 text_lengths는 패딩 전 개별 시퀀스의 길이이며 시퀀스는 지정된 배치 내에서 길이가 감소하는 순서에 따라 정렬됩니다.
그리고 전체 성능에 영향을 미칠 시퀀스를 처리하는 동안 RNN이 원하지 않는 패딩 된 인덱스를 보지 못하도록 패킹합니다.