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!
Respuestas:
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 yel 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:
fuente
Aquí hay algunas explicaciones visuales 1 que pueden ayudar a desarrollar una mejor intuición para la funcionalidad de
pack_padded_sequence()
Supongamos que tenemos
6
secuencias (de longitudes variables) en total. También puede considerar este número6
como elbatch_size
hiperparámetro.Ahora, queremos pasar estas secuencias a algunas arquitecturas de redes neuronales recurrentes. Para hacerlo, tenemos que rellenar todas las secuencias (generalmente con
0
s) en nuestro lote hasta la longitud máxima de secuencia en nuestro lote (max(sequence_lengths)
), que en la figura siguiente es9
.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_sequences
de forma(6, 9)
con una matrizW
de peso de forma(9, 3)
.Por lo tanto, tendremos que realizar operaciones de
6x9 = 54
multiplicación y6x8 = 48
suma (nrows x (n-1)_cols
), solo para descartar la mayoría de los resultados calculados ya que serían0
s (donde tenemos pads). El cálculo real requerido en este caso es el siguiente: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: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
fuente
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
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.
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_batch
para 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:batch_sizes
que 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_batch
a los módulos recurrentes en Pytorch, como RNN, LSTM. Esto solo requiere5 + 2=7
cá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>))) """
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
De forma estándar, solo necesitamos pasar el módulo
padded_seq_batch
tolstm
. Sin embargo, requiere 10 cálculos. Implica varios cálculos más sobre elementos de relleno que serían computacionalmente ineficientes.Tenga en cuenta que no conduce a representaciones inexactas , pero necesita mucha más lógica para extraer representaciones correctas.
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
,cn
son diferentes de dos maneras mientras queoutput
a partir de dos maneras dar lugar a diferentes valores para los elementos de relleno.fuente
Agregando a la respuesta de Umang, encontré esto importante de notar.
El primer elemento en la tupla devuelta de
pack_padded_sequence
es 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
abc
yx
: class:PackedSequence
contendrían datosaxbc
conbatch_sizes=[2,1,1]
.fuente
Usé la secuencia de paquete acolchado de la siguiente manera.
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.
fuente