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_sequence
requiere 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_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.
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_sizes
que le dirá cómo los elementos se relacionan entre sí por los pasos.
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 requiere 5 + 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())
"""
>>> 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 output
nuevo 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
De forma estándar, solo necesitamos pasar el módulo padded_seq_batch
to lstm
. 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.
- 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:
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 que output
a partir de dos maneras dar lugar a diferentes valores para los elementos de relleno.