tại sao chúng ta "đóng gói" các trình tự trong pytorch?


94

Tôi đang cố gắng sao chép Cách sử dụng đóng gói cho các đầu vào chuỗi có độ dài thay đổi cho rnn nhưng tôi đoán trước tiên tôi cần hiểu tại sao chúng ta cần "đóng gói" chuỗi.

Tôi hiểu tại sao chúng ta cần "độn" chúng nhưng tại sao "đóng gói" (thông qua pack_padded_sequence) lại cần thiết?

Mọi lời giải thích cấp cao sẽ được đánh giá cao!


tất cả các câu hỏi về đóng gói trong pytorch: thảo luận.pytorch.org/t/
Charlie Parker,

Câu trả lời:


89

Tôi cũng đã từng vấp phải vấn đề này và dưới đây là những gì tôi đã tìm ra.

Khi huấn luyện RNN (LSTM hoặc GRU hoặc vani-RNN), rất khó để phân lô các chuỗi có độ dài thay đổi. Ví dụ: nếu độ dài của chuỗi trong lô kích thước 8 là [4,6,8,5,4,3,7,8], bạn sẽ đệm tất cả các chuỗi và điều đó sẽ dẫn đến 8 chuỗi có độ dài 8. Bạn cuối cùng sẽ thực hiện 64 phép tính (8x8), nhưng bạn chỉ cần thực hiện 45 phép tính. Hơn nữa, nếu bạn muốn làm điều gì đó lạ mắt như sử dụng RNN hai chiều, sẽ khó hơn để thực hiện tính toán hàng loạt chỉ bằng cách đệm và bạn có thể phải thực hiện nhiều tính toán hơn yêu cầu.

Thay vào đó, PyTorch cho phép chúng ta đóng gói chuỗi, chuỗi được đóng gói bên trong là một bộ gồm hai danh sách. Một chứa các phần tử của chuỗi. Các phần tử được xen kẽ theo các bước thời gian (xem ví dụ bên dưới) và các phần tử khác chứa kích thước của từng chuỗi kích thước lô ở mỗi bước. Điều này rất hữu ích trong việc khôi phục các trình tự thực tế cũng như cho RNN biết kích thước lô ở mỗi bước thời gian là gì. Điều này đã được chỉ ra bởi @Aerin. Điều này có thể được chuyển cho RNN và nó sẽ tối ưu hóa nội bộ các tính toán.

Tôi có thể đã không rõ ràng ở một số điểm, vì vậy hãy cho tôi biết và tôi có thể giải thích thêm.

Đây là một ví dụ về mã:

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

4
Bạn có thể giải thích tại sao đầu ra của ví dụ đã cho là PackedSequence (data = tensor ([1, 3, 2, 4, 3]), batch_sizes = tensor ([2, 2, 1]))?
ascetic652

3
Phần dữ liệu chỉ là tất cả các tenxơ được nối dọc theo trục thời gian. Batch_size thực chất là mảng kích thước hàng loạt tại mỗi bước thời gian.
Umang Gupta

2
Batch_sizes = [2, 2, 1] đại diện cho nhóm [1, 3] [2, 4] và [3] tương ứng.
Chaitanya Shivade

@ChaitanyaShivade tại sao kích thước lô là [2,2,1]? không thể là [1,2,2]? logic đằng sau nó là gì?
Lập trình viên ẩn danh

1
Bởi vì ở bước t, bạn chỉ có thể xử lý vectơ ở bước t, nếu bạn giữ các vectơ có thứ tự là [1,2,2], có thể bạn đang đặt mỗi đầu vào dưới dạng một lô, nhưng điều đó không thể song song và do đó không thể xử lý được
Umang Gupta

53

Dưới đây là một số giải thích trực quan 1 có thể giúp phát triển trực giác tốt hơn cho chức năng củapack_padded_sequence()

Giả sử chúng ta có 6tổng số các chuỗi (có độ dài thay đổi). Bạn cũng có thể coi con số này 6batch_sizesiêu tham số. (Giá trị batch_sizesẽ thay đổi tùy thuộc vào độ dài của chuỗi (xem Hình 2 bên dưới))

Bây giờ, chúng ta muốn chuyển các trình tự này tới một số (các) kiến ​​trúc mạng nơ-ron tuần hoàn. Để làm như vậy, chúng ta phải đệm tất cả các trình tự (thường có 0s) trong lô của chúng tôi thành độ dài trình tự tối đa trong lô ( max(sequence_lengths)) của chúng tôi , trong hình dưới đây là 9.

padded-seqs

Vì vậy, công việc chuẩn bị dữ liệu chắc đã hoàn thành ngay bây giờ, phải không? Không hẳn .. Bởi vì vẫn còn một vấn đề cấp bách, chủ yếu là chúng ta phải thực hiện bao nhiêu tính toán khi so sánh với các tính toán thực sự cần thiết.

Để dễ hiểu, chúng ta cũng giả sử rằng chúng ta sẽ nhân ma trận ở trên padded_batch_of_sequencescủa hình dạng (6, 9)với ma trận trọng lượng Wcủa hình dạng (9, 3).

Do đó, chúng ta sẽ phải thực hiện các 6x9 = 54phép toán nhân6x8 = 48cộng                     ( nrows x (n-1)_cols), chỉ để loại bỏ hầu hết các kết quả được tính toán vì chúng sẽ là 0s (nơi chúng ta có các miếng đệm). Tính toán thực tế cần thiết trong trường hợp này như sau:

 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) 

Đó là một khoản tiết kiệm hơn RẤT NHIỀU ngay cả đối với ví dụ ( đồ chơi ) rất đơn giản này . Bây giờ bạn có thể tưởng tượng lượng máy tính (cuối cùng: chi phí, năng lượng, thời gian, phát thải carbon, v.v.) có thể được tiết kiệm bằng cách sử dụng pack_padded_sequence()cho hàng chục lớn với hàng triệu mục nhập và hàng triệu + hệ thống trên khắp thế giới đang làm điều đó lặp đi lặp lại.

Chức năng của pack_padded_sequence()có thể được hiểu từ hình bên dưới, với sự trợ giúp của mã màu đã sử dụng:

pack-padded-seqs

Kết quả của việc sử dụng pack_padded_sequence(), chúng ta sẽ nhận được một bộ tensors chứa (i) phẳng (dọc theo trục-1, trong hình trên) sequences, (ii) kích thước lô tương ứng, tensor([6,6,5,4,3,3,2,2,1])cho ví dụ trên.

Sau đó, bộ căng dữ liệu (tức là các chuỗi được làm phẳng) sau đó có thể được chuyển đến các hàm mục tiêu như CrossEntropy để tính toán tổn thất.


1 hình ảnh tín dụng cho @sgrvinod


2
Sơ đồ tuyệt vời!
David Waterworth

1
Chỉnh sửa: Tôi nghĩ stackoverflow.com/a/55805785/6167850 (bên dưới) trả lời câu hỏi của tôi, câu hỏi mà tôi sẽ để lại ở đây: ~ Điều này về cơ bản có nghĩa là các gradient không được truyền đến các đầu vào được đệm? Điều gì sẽ xảy ra nếu hàm mất mát của tôi chỉ được tính trên trạng thái / đầu ra ẩn cuối cùng của RNN? Sau đó phải vứt bỏ hiệu suất thu được chứ? Hay khoản lỗ sẽ được tính từ bước trước khi vùng đệm bắt đầu, điều này khác nhau đối với mỗi phần tử lô trong ví dụ này? ~
nlml

26

Các câu trả lời trên đã giải quyết rất tốt câu hỏi tại sao . Tôi chỉ muốn thêm một ví dụ để hiểu rõ hơn về việc sử dụng pack_padded_sequence.

Hãy lấy một ví dụ

Lưu ý: pack_padded_sequenceyêu cầu các chuỗi được sắp xếp trong lô (theo thứ tự giảm dần độ dài chuỗi). Trong ví dụ dưới đây, lô trình tự đã được sắp xếp để bớt lộn xộn hơn. Truy cập liên kết ý chính này để triển khai đầy đủ.

Đầu tiên, chúng ta tạo một lô gồm 2 chuỗi có độ dài trình tự khác nhau như bên dưới. Chúng tôi có 7 yếu tố trong lô hoàn toàn.

  • Mỗi chuỗi có kích thước nhúng là 2.
  • Dãy thứ nhất có độ dài: 5
  • Dãy thứ hai có độ dài: 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]

Chúng tôi pad seq_batchđể nhận được lô các chuỗi có độ dài bằng 5 (Độ dài tối đa trong lô). Bây giờ, lô mới có 10 phần tử hoàn toàn.

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

Sau đó, chúng tôi đóng gói padded_seq_batch. Nó trả về một bộ hai tenxơ:

  • Đầu tiên là dữ liệu bao gồm tất cả các phần tử trong lô trình tự.
  • Thứ hai là batch_sizessẽ cho biết các yếu tố liên quan với nhau như thế nào theo các bước.
# 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]))
"""

Bây giờ, chúng tôi chuyển bộ tuple packed_seq_batchđến các mô-đun lặp lại trong Pytorch, chẳng hạn như RNN, LSTM. Điều này chỉ yêu cầu 5 + 2=7tính toán trong mô-đun định kỳ.

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

Chúng tôi cần chuyển đổi outputtrở lại lô đầu ra được đệm:

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

So sánh nỗ lực này với cách tiêu chuẩn

  1. Theo cách tiêu chuẩn, chúng ta chỉ cần chuyển padded_seq_batchđến lstmmô-đun. Tuy nhiên, nó yêu cầu 10 lần tính toán. Nó liên quan đến một số tính toán nhiều hơn trên các phần tử đệm sẽ không hiệu quả về mặt tính toán .

  2. Lưu ý rằng nó không dẫn đến các biểu diễn không chính xác , nhưng cần nhiều logic hơn để trích xuất các biểu diễn chính xác.

    • Đối với LSTM (hoặc bất kỳ mô-đun lặp lại nào) chỉ có hướng về phía trước, nếu chúng ta muốn trích xuất vectơ ẩn của bước cuối cùng làm đại diện cho một chuỗi, chúng ta sẽ phải lấy vectơ ẩn từ bước T (thứ), trong đó T là độ dài của đầu vào. Chọn đại diện cuối cùng sẽ không chính xác. Lưu ý rằng T sẽ khác nhau đối với các đầu vào khác nhau trong lô.
    • Đối với LSTM hai hướng (hoặc bất kỳ mô-đun lặp lại nào), nó thậm chí còn cồng kềnh hơn, vì người ta sẽ phải duy trì hai mô-đun RNN, một mô-đun hoạt động với đệm ở đầu đầu vào và một có đệm ở cuối đầu vào, và cuối cùng là giải nén và nối các vectơ ẩn như đã giải thích ở trên.

Hãy xem sự khác biệt:

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

Kết quả trên cho thấy rằng hn, cnkhác nhau theo hai cách trong khi outputtừ hai cách dẫn đến các giá trị khác nhau cho các phần tử đệm.


2
Câu trả lời rất hay! Chỉ là một sự điều chỉnh nếu bạn thực hiện đệm, bạn không nên sử dụng h cuối cùng thay vì h ở chỉ mục bằng độ dài của đầu vào. Ngoài ra, để thực hiện RNN hai chiều, bạn sẽ muốn sử dụng hai RNN khác nhau --- một có phần đệm ở phía trước và một phần khác có phần đệm ở phía sau để có được kết quả chính xác. Đệm và chọn đầu ra cuối cùng là "sai". Vì vậy, lập luận của bạn rằng nó dẫn đến đại diện không chính xác là sai. Vấn đề với phần đệm là nó đúng nhưng không hiệu quả (nếu có tùy chọn chuỗi đóng gói) và có thể cồng kềnh (ví dụ: bi-dir RNN)
Umang Gupta

18

Thêm vào câu trả lời của Umang, tôi thấy điều này quan trọng cần lưu ý.

Mục đầu tiên trong bộ pack_padded_sequencedữ liệu được trả về là một dữ liệu (tensor) - tensor chứa chuỗi được đóng gói. Mục thứ hai là hàng chục số nguyên chứa thông tin về kích thước lô ở mỗi bước trình tự.

Tuy nhiên, điều quan trọng ở đây là mục thứ hai (Kích thước lô) đại diện cho số phần tử ở mỗi bước trình tự trong lô, không phải độ dài trình tự khác nhau được chuyển đến pack_padded_sequence.

Ví dụ: dữ liệu đã cho abcx : class: PackedSequencesẽ chứa dữ liệu axbcvới batch_sizes=[2,1,1].


1
Cảm ơn, tôi hoàn toàn quên điều đó. và đã mắc lỗi trong câu trả lời của tôi sẽ cập nhật điều đó. Tuy nhiên, tôi nhìn vào dãy thứ hai như một số dữ liệu cần thiết để khôi phục lại các trình tự và đó là lý do tại sao điều sai lầm mô tả của tôi
Umang Gupta

2

Tôi đã sử dụng trình tự độn gói như sau.

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

trong đó text_lengths là độ dài của chuỗi riêng lẻ trước khi đệm và chuỗi được sắp xếp theo thứ tự độ dài giảm dần trong một lô nhất định.

bạn có thể xem một ví dụ ở đây .

Và chúng tôi đóng gói để RNN không nhìn thấy chỉ số được đệm không mong muốn trong khi xử lý trình tự sẽ ảnh hưởng đến hiệu suất tổng thể.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.