¿Por qué “empaquetamos” las secuencias en pytorch?

93

Estaba tratando de replicar Cómo usar el empaquetado para entradas de secuencia de longitud variable para rnn, pero supongo que primero necesito entender por qué necesitamos "empaquetar" la secuencia.

Entiendo por qué tenemos que "rellenarlos", pero ¿por qué es necesario "empacar" pack_padded_sequence?

¡Cualquier explicación de alto nivel será apreciada!

Aerin
fuente
todas las preguntas sobre el embalaje en pytorch: discus.pytorch.org/t/…
Charlie Parker

Respuestas:

88

También me he encontrado con este problema y a continuación se muestra lo que descubrí.

Cuando se entrena RNN (LSTM o GRU o vanilla-RNN), es difícil agrupar las secuencias de longitud variable. Por ejemplo: si la longitud de las secuencias en un lote de tamaño 8 es [4,6,8,5,4,3,7,8], rellenará todas las secuencias y eso dará como resultado 8 secuencias de longitud 8. Usted terminaría haciendo 64 cálculos (8x8), pero solo necesita hacer 45 cálculos. Además, si quisiera hacer algo elegante como usar un RNN bidireccional, sería más difícil hacer cálculos por lotes simplemente rellenando y podría terminar haciendo más cálculos de los necesarios.

En cambio, PyTorch nos permite empaquetar la secuencia, la secuencia empaquetada internamente es una tupla de dos listas. Uno contiene los elementos de las secuencias. Los elementos están intercalados por pasos de tiempo (ver ejemplo a continuación) y otros contienen el tamaño de cada secuencia y el tamaño del lote en cada paso. Esto es útil para recuperar las secuencias reales y para indicarle a RNN cuál es el tamaño del lote en cada paso de tiempo. Esto ha sido señalado por @Aerin. Esto se puede pasar a RNN y optimizará internamente los cálculos.

Es posible que no haya sido claro en algunos puntos, así que avíseme y puedo agregar más explicaciones.

Aquí hay un ejemplo de código:

 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]))
Umang Gupta
fuente
4
¿Puede explicar por qué la salida del ejemplo dado es PackedSequence (data = tensor ([1, 3, 2, 4, 3]), batch_sizes = tensor ([2, 2, 1]))?
ascetic652
3
La parte de datos son solo todos los tensores concatenados a lo largo del eje del tiempo. Batch_size es en realidad la matriz de tamaños de lote en cada paso de tiempo.
Umang Gupta
2
Batch_sizes = [2, 2, 1] representa la agrupación [1, 3] [2, 4] y [3] respectivamente.
Chaitanya Shivade
@ChaitanyaShivade ¿por qué el tamaño del lote es [2,2,1]? ¿no puede ser [1,2,2]? ¿Cuál es la lógica detrás de esto?
Programador anónimo
1
Debido a que en el paso t, solo puede procesar vectores en el paso t, si mantiene los vectores ordenados como [1,2,2], probablemente esté poniendo cada entrada como un lote, pero eso no se puede paralelizar y, por lo tanto, no se puede procesar por lotes
Umang Gupta
51

Aquí hay algunas explicaciones visuales 1 que pueden ayudar a desarrollar una mejor intuición para la funcionalidad depack_padded_sequence()

Supongamos que tenemos 6secuencias (de longitudes variables) en total. También puede considerar este número 6como el batch_sizehiperparámetro.

Ahora, queremos pasar estas secuencias a algunas arquitecturas de redes neuronales recurrentes. Para hacerlo, tenemos que rellenar todas las secuencias (generalmente con 0s) en nuestro lote hasta la longitud máxima de secuencia en nuestro lote ( max(sequence_lengths)), que en la figura siguiente es 9.

seqs acolchados

Entonces, el trabajo de preparación de datos debería estar completo ahora, ¿verdad? En realidad no ... Porque todavía hay un problema urgente, principalmente en términos de cuánto cálculo tenemos que hacer en comparación con los cálculos realmente necesarios.

En aras de la comprensión, supongamos también que multiplicaremos por matriz lo anterior padded_batch_of_sequencesde forma (6, 9)con una matriz Wde peso de forma (9, 3).

Por lo tanto, tendremos que realizar operaciones de 6x9 = 54multiplicación y 6x8 = 48suma                     ( nrows x (n-1)_cols), solo para descartar la mayoría de los resultados calculados ya que serían 0s (donde tenemos pads). El cálculo real requerido en este caso es el siguiente:

 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) 

Eso es MUCHO más ahorro incluso para este ejemplo muy simple ( juguete ). Ahora puede imaginar la cantidad de cómputo (eventualmente: costo, energía, tiempo, emisión de carbono, etc.) que se puede ahorrar usando pack_padded_sequence()grandes tensores con millones de entradas, y más de un millón de sistemas en todo el mundo haciendo eso, una y otra vez.

La funcionalidad de pack_padded_sequence()puede entenderse en la siguiente figura, con la ayuda de la codificación de colores utilizada:

pack-padded-seqs

Como resultado del uso pack_padded_sequence(), obtendremos una tupla de tensores que contienen (i) el aplanado (a lo largo del eje 1, en la figura anterior) sequences, (ii) los tamaños de lote correspondientes, tensor([6,6,5,4,3,3,2,2,1])para el ejemplo anterior.

El tensor de datos (es decir, las secuencias aplanadas) se podría pasar a funciones objetivas como CrossEntropy para los cálculos de pérdidas.


1 créditos de imagen para @sgrvinod

kmario23
fuente
2
Excelentes diagramas!
David Waterworth
1
Editar: creo que stackoverflow.com/a/55805785/6167850 (abajo) responde a mi pregunta, que dejaré aquí de todos modos: ~ ¿Esto significa esencialmente que los gradientes no se propagan a las entradas acolchadas? ¿Qué pasa si mi función de pérdida solo se calcula en el estado / salida oculta final del RNN? Entonces, ¿deben desecharse las ganancias de eficiencia? ¿O se calculará la pérdida desde el paso anterior a donde comienza el relleno, que es diferente para cada elemento de lote en este ejemplo? ~
nlml
25

Las respuestas anteriores abordaron muy bien la pregunta por qué . Solo quiero agregar un ejemplo para comprender mejor el uso de pack_padded_sequence.

Tomemos un ejemplo

Nota: pack_padded_sequencerequiere secuencias ordenadas en el lote (en orden descendente de longitudes de secuencia). En el siguiente ejemplo, el lote de secuencia ya estaba ordenado para reducir el desorden. Visite este enlace esencial para la implementación completa.

Primero, creamos un lote de 2 secuencias de diferentes longitudes de secuencia como se muestra a continuación. Tenemos 7 elementos en el lote en total.

  • Cada secuencia tiene un tamaño de incrustación de 2.
  • La primera secuencia tiene la longitud: 5
  • La segunda secuencia tiene la longitud: 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]

Rellenamos seq_batchpara obtener el lote de secuencias con la misma longitud de 5 (la longitud máxima en el lote). Ahora, el nuevo lote tiene 10 elementos en 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]]])
"""

Luego, empacamos el padded_seq_batch. Devuelve una tupla de dos tensores:

  • El primero son los datos que incluyen todos los elementos del lote de secuencia.
  • El segundo es el batch_sizesque le dirá cómo los elementos se relacionan entre sí por los pasos.
# 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]))
"""

Ahora, pasamos la tupla packed_seq_batcha los módulos recurrentes en Pytorch, como RNN, LSTM. Esto solo requiere 5 + 2=7cálculos en el módulo recurrente.

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>)))
"""

Necesitamos convertir de outputnuevo al lote de salida acolchado:

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 esfuerzo con la forma estándar

  1. De forma estándar, solo necesitamos pasar el módulo padded_seq_batchto lstm. Sin embargo, requiere 10 cálculos. Implica varios cálculos más sobre elementos de relleno que serían computacionalmente ineficientes.

  2. Tenga en cuenta que no conduce a representaciones inexactas , pero necesita mucha más lógica para extraer representaciones correctas.

    • Para LSTM (o cualquier módulo recurrente) con solo dirección de avance, si quisiéramos extraer el vector oculto del último paso como una representación de una secuencia, tendríamos que recoger los vectores ocultos de T (th) paso, donde T es la longitud de la entrada. Recoger la última representación será incorrecto. Tenga en cuenta que T será diferente para diferentes entradas en lote.
    • Para LSTM bidireccionales (o cualquier módulo recurrente), es aún más engorroso, ya que habría que mantener dos módulos RNN, uno que funciona con relleno al comienzo de la entrada y otro con relleno al final de la entrada, y finalmente extrayendo y concatenando los vectores ocultos como se explicó anteriormente.

Veamos la diferencia:

# 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 >))
"""

Los resultados anteriores muestran que hn, cnson diferentes de dos maneras mientras que outputa partir de dos maneras dar lugar a diferentes valores para los elementos de relleno.

David Ng
fuente
2
¡Buena respuesta! Solo una corrección, si hace relleno, no debe usar la última h en lugar de h en el índice igual a la longitud de la entrada. Además, para hacer RNN bidireccional, querrá utilizar dos RNN diferentes, uno con relleno en la parte delantera y otro con relleno en la parte posterior para obtener resultados correctos. El relleno y la selección de la última salida son "incorrectos". Entonces, sus argumentos de que conduce a una representación inexacta son incorrectos. El problema con el relleno es que es correcto pero ineficiente (si la opción de secuencias empaquetadas está ahí) y puede ser engorroso (por ejemplo: bi-dir RNN)
Umang Gupta
18

Agregando a la respuesta de Umang, encontré esto importante de notar.

El primer elemento en la tupla devuelta de pack_padded_sequencees un tensor de datos (tensor) que contiene una secuencia empaquetada. El segundo elemento es un tensor de números enteros que contiene información sobre el tamaño del lote en cada paso de la secuencia.

Sin embargo, lo importante aquí es que el segundo elemento (Tamaños de lote) representa el número de elementos en cada paso de la secuencia en el lote, no las diferentes longitudes de secuencia que se pasan pack_padded_sequence.

Por ejemplo, los datos dados abcy x : class: PackedSequencecontendrían datos axbccon batch_sizes=[2,1,1].

Aerin
fuente
1
Gracias, lo olvidé por completo. y cometí un error en mi respuesta al actualizar eso. Sin embargo, miré la segunda secuencia como algunos datos necesarios para recuperar las secuencias y es por eso que
arruiné
2

Usé la secuencia de paquete acolchado de la siguiente manera.

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

donde text_lengths son la longitud de la secuencia individual antes de que el relleno y la secuencia se clasifiquen de acuerdo con un orden decreciente de longitud dentro de un lote determinado.

puedes ver un ejemplo aquí .

Y empaquetamos para que el RNN no vea el índice acolchado no deseado mientras procesa la secuencia que afectaría el rendimiento general.

Jibin Mathew
fuente