Eu estava tentando replicar como usar o empacotamento para entradas de sequência de comprimento variável para rnn, mas acho que primeiro preciso entender por que precisamos "empacotar" a sequência.
Eu entendo por que precisamos "preenchê-los", mas por que "empacotar" (através pack_padded_sequence
) é necessário?
Qualquer explicação de alto nível seria apreciada!
Respostas:
Também me deparei com esse problema e abaixo está o que descobri.
Ao treinar RNN (LSTM ou GRU ou vanilla-RNN), é difícil agrupar as sequências de comprimento variável. Por exemplo: se o comprimento das sequências em um lote de tamanho 8 for [4,6,8,5,4,3,7,8], você preencherá todas as sequências e isso resultará em 8 sequências de comprimento 8. Você acabaria fazendo 64 cálculos (8x8), mas você precisava fazer apenas 45 cálculos. Além disso, se você quisesse fazer algo sofisticado como usar um RNN bidirecional, seria mais difícil fazer cálculos em lote apenas preenchendo e você poderia acabar fazendo mais cálculos do que o necessário.
Em vez disso, o PyTorch nos permite empacotar a seqüência, a seqüência empacotada internamente é uma tupla de duas listas. Um contém os elementos das sequências. Os elementos são intercalados por etapas de tempo (veja o exemplo abaixo) e outro contém o
tamanho de cada sequência eo tamanho do lote em cada etapa. Isso é útil para recuperar as sequências reais e também para informar ao RNN qual é o tamanho do lote em cada etapa de tempo. Isso foi apontado por @Aerin. Isso pode ser passado para RNN e otimizará internamente os cálculos.Posso não ter sido claro em alguns pontos, então me avise e posso adicionar mais explicações.
Aqui está um exemplo de código:
fonte
Aqui estão algumas explicações visuais 1 que podem ajudar a desenvolver uma melhor intuição para a funcionalidade do
pack_padded_sequence()
Vamos supor que temos
6
sequências (de comprimentos variáveis) no total. Você também pode considerar esse número6
como umbatch_size
hiperparâmetro. (Obatch_size
irá variar dependendo do comprimento da sequência (cf. Fig.2 abaixo))Agora, queremos passar essas sequências para algumas arquiteturas de rede neural recorrente. Para fazer isso, temos que preencher todas as sequências (normalmente com
0
s) em nosso lote até o comprimento máximo de sequência em nosso lote (max(sequence_lengths)
), que na figura abaixo é9
.Então, o trabalho de preparação de dados deve estar concluído agora, certo? Na verdade não .. Porque ainda há um problema urgente, principalmente em termos de quanta computação temos que fazer em comparação com os cálculos realmente necessários.
Para fins de compreensão, também vamos supor que vamos multiplicar a matriz acima
padded_batch_of_sequences
da forma(6, 9)
por uma matrizW
de peso da forma(9, 3)
.Assim, teremos que realizar operações de
6x9 = 54
multiplicação e6x8 = 48
adição (nrows x (n-1)_cols
), apenas para descartar a maioria dos resultados calculados, pois eles seriam0
s (onde temos pads). O cálculo real necessário neste caso é o seguinte:Isso é muito mais economia, mesmo para este exemplo muito simples ( brinquedo ). Agora você pode imaginar quanta computação (eventualmente: custo, energia, tempo, emissão de carbono etc.) pode ser economizada usando
pack_padded_sequence()
para grandes tensores com milhões de entradas, e mais de milhões de sistemas em todo o mundo fazendo isso, novamente e novamente.A funcionalidade do
pack_padded_sequence()
pode ser compreendida na figura abaixo, com a ajuda da codificação por cores utilizada:Como resultado do uso
pack_padded_sequence()
, obteremos uma tupla de tensores contendo (i) o achatado (ao longo do eixo 1, na figura acima)sequences
, (ii) os tamanhos de lote correspondentes,tensor([6,6,5,4,3,3,2,2,1])
para o exemplo acima.O tensor de dados (ou seja, as sequências achatadas) pode então ser passado para funções objetivo, como CrossEntropy para cálculos de perda.
1 crédito de imagem para @sgrvinod
fonte
As respostas acima abordaram muito bem a questão do porquê . Eu só quero adicionar um exemplo para entender melhor o uso de
pack_padded_sequence
.Vamos dar um exemplo
Primeiro, criamos um lote de 2 sequências de comprimentos de sequência diferentes, conforme abaixo. Temos 7 elementos no lote totalmente.
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]
Nós preenchemos
seq_batch
para obter o lote de sequências com comprimento igual a 5 (o comprimento máximo no lote). Agora, o novo lote conta com 10 elementos no total.# 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]]]) """
Em seguida, embalamos o
padded_seq_batch
. Ele retorna uma tupla de dois tensores:batch_sizes
que dirá como os elementos se relacionam entre si pelas etapas.# 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])) """
Agora, passamos a tupla
packed_seq_batch
para os módulos recorrentes no Pytorch, como RNN, LSTM. Isso requer apenas5 + 2=7
cálculos no módulo recorrente.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]) """
Compare este esforço com a forma padrão
Na forma padrão, só precisamos passar o módulo
padded_seq_batch
paralstm
. No entanto, requer 10 cálculos. Envolve vários cálculos mais em elementos de preenchimento que seriam computacionalmente ineficientes.Observe que isso não leva a representações imprecisas , mas precisa de muito mais lógica para extrair representações corretas.
Vamos ver a diferença:
# 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 >)) """
Os resultados acima mostram que
hn
,cn
são diferentes de duas maneiras, enquanto asoutput
duas maneiras levam a valores diferentes para elementos de preenchimento.fonte
Somando-se à resposta de Umang, achei importante observar isso.
O primeiro item na tupla retornada de
pack_padded_sequence
é um tensor de dados (tensor) contendo a sequência compactada. O segundo item é um tensor de inteiros contendo informações sobre o tamanho do lote em cada etapa da sequência.O que é importante aqui, porém, é que o segundo item (tamanhos de lote) representa o número de elementos em cada etapa da sequência no lote, não os comprimentos de sequência variados passados
pack_padded_sequence
.Por exemplo, dados fornecidos
abc
ex
: class:PackedSequence
conteria dadosaxbc
combatch_sizes=[2,1,1]
.fonte
Usei a sequência preenchida do pacote da seguinte forma.
onde text_lengths são o comprimento da sequência individual antes do preenchimento e da sequência serem classificados de acordo com a ordem decrescente de comprimento em um determinado lote.
você pode verificar um exemplo aqui .
E fazemos o empacotamento para que o RNN não veja o índice preenchido indesejado enquanto processa a sequência que afetaria o desempenho geral.
fonte