ทำไมเราถึง "แพ็ค" ลำดับใน pytorch?


94

ฉันพยายามทำซ้ำวิธีใช้การบรรจุสำหรับอินพุตลำดับความยาวผันแปรสำหรับ rnnแต่ฉันคิดว่าฉันต้องเข้าใจก่อนว่าทำไมเราต้อง "แพ็ค" ลำดับ

ฉันเข้าใจว่าทำไมเราถึงต้อง "รอง" แต่ทำไม "การบรรจุ" (ผ่านpack_padded_sequence) จึงจำเป็น

คำอธิบายระดับสูงใด ๆ จะได้รับการชื่นชม!


คำถามทั้งหมดเกี่ยวกับการบรรจุใน pytorch: discuss.pytorch.org/t/...
ชาร์ลีปาร์กเกอร์

คำตอบ:


89

ฉันเจอปัญหานี้เหมือนกันและด้านล่างนี้คือสิ่งที่ฉันคิดออก

เมื่อฝึก RNN (LSTM หรือ GRU หรือ vanilla-RNN) การจัดลำดับความยาวผันแปรเป็นชุดได้ยาก ตัวอย่างเช่นถ้าความยาวของลำดับในชุดขนาด 8 คือ [4,6,8,5,4,3,7,8] คุณจะวางลำดับทั้งหมดและจะส่งผลให้มีความยาว 8 ลำดับ 8 คุณ จะลงเอยด้วยการคำนวณ 64 ครั้ง (8x8) แต่คุณต้องทำการคำนวณเพียง 45 ครั้ง ยิ่งไปกว่านั้นหากคุณต้องการทำสิ่งที่แปลกใหม่เช่นการใช้ RNN แบบสองทิศทางการคำนวณแบบแบทช์จะทำได้ยากขึ้นเพียงแค่การเว้นระยะห่างและคุณอาจต้องทำการคำนวณมากกว่าที่ต้องการ

PyTorch ช่วยให้เราสามารถแพ็คลำดับได้ แต่ลำดับที่บรรจุภายในเป็นทูเพิลจากสองรายการ หนึ่งประกอบด้วยองค์ประกอบของลำดับ องค์ประกอบจะแทรกสลับกันตามขั้นตอนเวลา (ดูตัวอย่างด้านล่าง) และองค์ประกอบอื่น ๆ จะมีขนาดของแต่ละลำดับขนาดแบทช์ในแต่ละขั้นตอน สิ่งนี้มีประโยชน์ในการกู้คืนลำดับจริงและบอก RNN ว่าขนาดแบตช์ในแต่ละขั้นตอนเป็นอย่างไร สิ่งนี้ได้รับการชี้แนะโดย @Aerin สามารถส่งผ่านไปยัง RNN และจะเพิ่มประสิทธิภาพการคำนวณภายใน

ฉันอาจจะไม่ชัดเจนในบางประเด็นดังนั้นโปรดแจ้งให้ฉันทราบและฉันสามารถอธิบายเพิ่มเติมได้

นี่คือตัวอย่างโค้ด:

 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
คุณสามารถอธิบายได้ว่าทำไมผลลัพธ์ของตัวอย่างที่ให้มาคือ PackedSequence (data = tensor ([1, 3, 2, 4, 3]), batch_sizes = tensor ([2, 2, 1]))
ascetic652

3
ส่วนข้อมูลเป็นเพียงเทนเซอร์ทั้งหมดที่ต่อกันตามแกนเวลา Batch_size คืออาร์เรย์ของขนาดชุดงานในแต่ละขั้นตอน
Umang Gupta

2
batch_sizes = [2, 2, 1] แสดงถึงการจัดกลุ่ม [1, 3] [2, 4] และ [3] ตามลำดับ
Chaitanya Shivade

@ChaitanyaShivade ทำไมชุดขนาด [2,2,1]? เป็น [1,2,2] ไม่ได้เหรอ ตรรกะเบื้องหลังคืออะไร?
โปรแกรมเมอร์

1
เนื่องจากในขั้นตอน t คุณสามารถประมวลผลเวกเตอร์ได้ที่ขั้นตอน t เท่านั้นหากคุณให้เวกเตอร์เรียงลำดับเป็น [1,2,2] คุณอาจจะใส่แต่ละอินพุตเป็นแบตช์ แต่ไม่สามารถขนานกันได้และด้วยเหตุนี้จึงไม่สามารถแบทช์ได้
Umang Gupta

53

นี่คือคำอธิบายภาพบางส่วน1ที่อาจช่วยในการพัฒนาสัญชาตญาณที่ดีขึ้นสำหรับการทำงานของpack_padded_sequence()

สมมติว่าเรามี6ลำดับ (ของความยาวผันแปร) ทั้งหมด คุณยังสามารถพิจารณาตัวเลขนี้6เป็นbatch_sizeไฮเปอร์พารามิเตอร์ ( batch_sizeจะแตกต่างกันไปขึ้นอยู่กับความยาวของลำดับ (เทียบกับรูปที่ 2 ด้านล่าง))

ตอนนี้เราต้องการส่งต่อลำดับเหล่านี้ไปยังสถาปัตยกรรมเครือข่ายประสาทที่เกิดซ้ำบางส่วน ต้องการทำเช่นนั้นเราจะต้องแผ่นทั้งหมดของลำดับ (โดยปกติจะมี0s) ในชุดของเราเพื่อความยาวลำดับสูงสุดในชุดของเรา ( max(sequence_lengths)) 9ซึ่งในรูปด้านล่างคือ

เบาะ - seqs

ตอนนี้งานเตรียมข้อมูลก็น่าจะเสร็จแล้วใช่ไหม ไม่จริง .. เนื่องจากยังมีปัญหาเร่งด่วนอยู่อย่างหนึ่งโดยหลัก ๆ แล้วในแง่ของการคำนวณที่เราต้องทำเมื่อเทียบกับการคำนวณที่จำเป็นจริงๆ

เพื่อประโยชน์ในการทำความเข้าใจให้ยังคิดว่าเราจะ matrix คูณข้างต้นpadded_batch_of_sequencesของรูปร่าง(6, 9)กับเมทริกซ์น้ำหนักของรูปร่างW(9, 3)

ดังนั้นเราจะต้องดำเนินการ6x9 = 54คูณและการ6x8 = 48บวก                     ( nrows x (n-1)_cols) เพียงเพื่อทิ้งผลลัพธ์ที่คำนวณได้ส่วนใหญ่เนื่องจากพวกมันจะเป็น0s (ที่เรามีแผ่นอิเล็กโทรด) การคำนวณที่จำเป็นจริงในกรณีนี้มีดังนี้:

 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) 

นั่นเป็นการประหยัดมากขึ้นสำหรับตัวอย่าง( ของเล่น ) ที่เรียบง่ายนี้ ตอนนี้คุณสามารถจินตนาการได้ว่าการคำนวณ (ในที่สุด: ต้นทุนพลังงานเวลาการปล่อยคาร์บอน ฯลฯ ) สามารถประหยัดได้โดยใช้pack_padded_sequence()สำหรับเครื่องเทนเซอร์ขนาดใหญ่ที่มีรายการนับล้านรายการและระบบอีกกว่าล้านระบบทั่วโลกที่ทำเช่นนั้นซ้ำแล้วซ้ำอีก

ฟังก์ชั่นการทำงานpack_padded_sequence()สามารถเข้าใจได้จากรูปด้านล่างด้วยความช่วยเหลือของการเข้ารหัสสีที่ใช้:

แพ็คเบาะ - seqs

จากการใช้pack_padded_sequence()เราจะได้รับทูเปิลของเทนเซอร์ที่มี (i) ที่แบน (ตามแกน -1 ในรูปด้านบน) sequences(ii) ขนาดแบตช์ที่สอดคล้องกันtensor([6,6,5,4,3,3,2,2,1])สำหรับตัวอย่างข้างต้น

จากนั้นเทนเซอร์ข้อมูล (เช่นลำดับที่แบน) สามารถส่งผ่านไปยังฟังก์ชันวัตถุประสงค์เช่น CrossEntropy สำหรับการคำนวณการสูญเสีย


เครดิตภาพ1ภาพถึง@sgrvinod


2
แผนภาพที่ยอดเยี่ยม!
David Waterworth

1
แก้ไข: ฉันคิดว่าstackoverflow.com/a/55805785/6167850 (ด้านล่าง) ตอบคำถามของฉันซึ่งฉันจะปล่อยไว้ที่นี่ต่อไป: ~ โดยพื้นฐานแล้วการไล่ระดับสีจะไม่แพร่กระจายไปยังอินพุตที่มีเบาะหรือไม่? จะเกิดอะไรขึ้นถ้าฟังก์ชันการสูญเสียของฉันคำนวณจากสถานะ / เอาต์พุตสุดท้ายที่ซ่อนอยู่ของ RNN เท่านั้น ประสิทธิภาพที่เพิ่มขึ้นจะต้องถูกโยนทิ้งไปหรือไม่? หรือจะคำนวณการสูญเสียจากขั้นตอนก่อนที่การขยายจะเริ่มต้นซึ่งแตกต่างกันไปสำหรับแต่ละองค์ประกอบแบตช์ในตัวอย่างนี้ ~
nlml

26

คำตอบข้างต้นตอบคำถามว่าทำไมดีมาก ฉันต้องการเพิ่มตัวอย่างเพื่อให้เข้าใจการใช้งานpack_padded_sequenceได้ดีขึ้น

ลองมาเป็นตัวอย่าง

หมายเหตุ: pack_padded_sequenceต้องใช้ลำดับการเรียงลำดับในชุดงาน (ตามลำดับจากมากไปหาน้อยของความยาวของลำดับ) ในตัวอย่างด้านล่างชุดลำดับได้ถูกจัดเรียงแล้วเพื่อให้ไม่เกะกะน้อยลง ไปที่ลิงก์ส่วนสำคัญนี้เพื่อใช้งานเต็มรูปแบบ

ขั้นแรกเราสร้างชุดของ 2 ลำดับที่มีความยาวของลำดับที่แตกต่างกันดังต่อไปนี้ เรามี 7 องค์ประกอบในชุดทั้งหมด

  • แต่ละลำดับมีขนาดการฝัง 2
  • ลำดับแรกมีความยาว: 5
  • ลำดับที่สองมีความยาว: 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]

เรารองseq_batchเพื่อให้ได้ชุดของลำดับที่มีความยาวเท่ากัน 5 (ความยาวสูงสุดในชุดงาน) ตอนนี้ชุดใหม่มี 10 องค์ประกอบทั้งหมด

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

จากนั้นเราบรรจุไฟล์padded_seq_batch. ส่งคืนทูเพิลสองเทนเซอร์:

  • ประการแรกคือข้อมูลรวมถึงองค์ประกอบทั้งหมดในชุดลำดับ
  • อย่างที่สองคือสิ่งbatch_sizesที่จะบอกว่าองค์ประกอบต่างๆเกี่ยวข้องกันอย่างไรตามขั้นตอน
# 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]))
"""

ตอนนี้เราส่งทูเพิลpacked_seq_batchไปยังโมดูลที่เกิดซ้ำใน Pytorch เช่น RNN, LSTM สิ่งนี้ต้องการ5 + 2=7การคำนวณในโมดูลที่เกิดซ้ำเท่านั้น

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

เราจำเป็นต้องแปลงoutputกลับเป็นชุดเอาต์พุตที่มีเบาะ:

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

เปรียบเทียบความพยายามนี้กับวิธีมาตรฐาน

  1. ตามวิธีมาตรฐานเราจะต้องส่งผ่านโมดูลpadded_seq_batchไปlstmเท่านั้น อย่างไรก็ตามต้องใช้การคำนวณ 10 ครั้ง มันเกี่ยวข้องกับการคำนวณหลายอย่างเพิ่มเติมเกี่ยวกับองค์ประกอบ padding ซึ่งจะไม่มีประสิทธิภาพในการคำนวณ

  2. โปรดทราบว่ามันไม่ได้นำไปสู่การแทนค่าที่ไม่ถูกต้องแต่ต้องใช้ตรรกะมากกว่านี้เพื่อดึงข้อมูลที่ถูกต้อง

    • สำหรับ LSTM (หรือโมดูลที่เกิดซ้ำ) ที่มีทิศทางไปข้างหน้าเท่านั้นหากเราต้องการแยกเวกเตอร์ที่ซ่อนอยู่ของขั้นตอนสุดท้ายเพื่อเป็นตัวแทนของลำดับเราจะต้องเลือกเวกเตอร์ที่ซ่อนอยู่จากขั้นตอน T (th) โดยที่ T คือความยาวของอินพุต การหยิบการแสดงครั้งสุดท้ายจะไม่ถูกต้อง โปรดทราบว่า T จะแตกต่างกันสำหรับอินพุตที่แตกต่างกันในชุดงาน
    • สำหรับ Bi-directional LSTM (หรือโมดูลที่เกิดขึ้นซ้ำ ๆ ) จะยิ่งยุ่งยากกว่าเดิมเนื่องจากต้องมีโมดูล RNN สองโมดูลซึ่งทำงานร่วมกับช่องว่างที่จุดเริ่มต้นของอินพุตและอีกโมดูลที่มีช่องว่างภายในตอนท้ายของอินพุต ในที่สุดก็แยกและเชื่อมเวกเตอร์ที่ซ่อนอยู่ตามที่อธิบายไว้ข้างต้น

มาดูความแตกต่างกัน:

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

ผลดังกล่าวข้างต้นแสดงให้เห็นว่าhn, cnมีความแตกต่างในสองวิธีในขณะที่outputจากสองวิธีที่นำไปสู่ค่าที่แตกต่างกันสำหรับองค์ประกอบ padding


2
คำตอบที่ดี! เพียงการแก้ไขหากคุณเพิ่มช่องว่างคุณไม่ควรใช้ h สุดท้ายแทนที่จะเป็น h ที่ดัชนีเท่ากับความยาวของอินพุต นอกจากนี้ในการทำ RNN แบบสองทิศทางคุณจะต้องใช้ RNN สองอันที่แตกต่างกันหนึ่งอันมีช่องว่างด้านหน้าและอีกอันโดยมีช่องว่างด้านหลังเพื่อให้ได้ผลลัพธ์ที่ถูกต้อง การเว้นช่องว่างและการเลือกผลลัพธ์สุดท้าย "ผิด" ดังนั้นข้อโต้แย้งของคุณที่นำไปสู่การเป็นตัวแทนที่ไม่ถูกต้องจึงผิด ปัญหาเกี่ยวกับช่องว่างภายในนั้นถูกต้อง แต่ไม่มีประสิทธิภาพ (หากมีตัวเลือกลำดับการบรรจุ) และอาจยุ่งยาก (เช่น bi-dir RNN)
Umang Gupta

18

การเพิ่มคำตอบของ Umang ฉันพบว่าสิ่งนี้สำคัญที่ควรทราบ

รายการแรกในทูเพิลที่ส่งคืนpack_padded_sequenceคือข้อมูล (เทนเซอร์) - เทนเซอร์ที่มีลำดับการบรรจุ รายการที่สองคือเทนเซอร์ของจำนวนเต็มที่เก็บข้อมูลเกี่ยวกับขนาดแบทช์ในแต่ละขั้นตอนของลำดับ

สิ่งที่สำคัญที่นี่ว่าเป็นรายการที่สอง (ขนาด Batch) หมายถึงจำนวนขององค์ประกอบในแต่ละขั้นตอนลำดับในชุดที่ไม่ยาวลำดับ Varying pack_padded_sequenceส่งผ่านไปยัง

ยกตัวอย่างเช่นข้อมูลที่ได้รับ abcและx ที่ระดับ: PackedSequenceจะมีข้อมูลที่มีaxbc batch_sizes=[2,1,1]


1
ขอบคุณฉันลืมไปโดยสิ้นเชิง และทำผิดพลาดในคำตอบของฉันที่จะอัปเดตสิ่งนั้น อย่างไรก็ตามฉันดูลำดับที่สองเนื่องจากข้อมูลบางอย่างที่จำเป็นในการกู้คืนลำดับและนั่นคือสาเหตุที่ทำให้คำอธิบายของฉันสับสน
Umang Gupta

2

ฉันใช้ลำดับการแพ็คเบาะดังนี้

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

โดยที่ text_lengths คือความยาวของแต่ละลำดับก่อนที่การเว้นวรรคและลำดับจะเรียงลำดับตามลำดับความยาวที่ลดลงภายในชุดงานที่กำหนด

คุณสามารถตรวจสอบตัวอย่างที่นี่

และเราทำการบรรจุเพื่อให้ RNN ไม่เห็นดัชนีเบาะที่ไม่ต้องการขณะประมวลผลลำดับซึ่งจะส่งผลต่อประสิทธิภาพโดยรวม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.