วิธีการ“ ดู” ทำงานอย่างไรใน PyTorch


208

ฉันสับสนเกี่ยวกับวิธีการview()ในข้อมูลโค้ดต่อไปนี้

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool  = nn.MaxPool2d(2,2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1   = nn.Linear(16*5*5, 120)
        self.fc2   = nn.Linear(120, 84)
        self.fc3   = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16*5*5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net()

ความสับสนของฉันเกี่ยวกับบรรทัดต่อไปนี้

x = x.view(-1, 16*5*5)

อะไรtensor.view()ฟังก์ชั่นทำอย่างไร ฉันเห็นการใช้งานหลายแห่ง แต่ฉันไม่เข้าใจว่ามันตีความพารามิเตอร์ของมันอย่างไร

จะเกิดอะไรขึ้นถ้าฉันให้ค่าลบเป็นพารามิเตอร์ของview()ฟังก์ชัน ตัวอย่างเช่นสิ่งที่เกิดขึ้นถ้าผมเรียกtensor_variable.view(1, 1, -1)?

ทุกคนสามารถอธิบายหลักการหลักของview()ฟังก์ชั่นด้วยตัวอย่างได้หรือไม่?

คำตอบ:


288

ฟังก์ชั่นมุมมองมีวัตถุประสงค์เพื่อปรับรูปร่างเมตริกซ์

สมมติว่าคุณมีเมตริกซ์

import torch
a = torch.range(1, 16)

aเป็นเมตริกซ์ที่มี 16 องค์ประกอบตั้งแต่ 1 ถึง 16 (รวมอยู่ด้วย) หากคุณต้องการที่จะก่อร่างใหม่ของเมตริกซ์นี้เพื่อให้มันเป็น4 x 4เมตริกซ์แล้วคุณสามารถใช้

a = a.view(4, 4)

ตอนนี้aจะเป็น4 x 4เทนเซอร์ โปรดทราบว่าหลังจากปรับรูปร่างองค์ประกอบทั้งหมดจะต้องยังคงเหมือนเดิม reshaping เทนเซอร์aกับ3 x 5เทนเซอร์จะไม่เหมาะสม

ความหมายของพารามิเตอร์ -1 คืออะไร

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

สามารถดูได้ในรหัสเครือข่ายประสาทที่คุณให้ไว้ข้างต้น หลังจากบรรทัดx = self.pool(F.relu(self.conv2(x)))ในฟังก์ชั่นไปข้างหน้าคุณจะมีแผนที่คุณลักษณะความลึก 16 คุณต้องแบนสิ่งนี้เพื่อมอบให้กับเลเยอร์ที่เชื่อมต่ออย่างสมบูรณ์ ดังนั้นคุณบอก pytorch เพื่อปรับแต่งเมตริกซ์ที่คุณได้รับมาให้มีจำนวนคอลัมน์ที่เฉพาะเจาะจงและบอกให้ตัดสินใจจำนวนแถวด้วยตัวเอง

วาดความคล้ายคลึงกันระหว่าง numpy และ pytorch ที่viewคล้ายกับของ numpy Reshapeฟังก์ชั่น


93
"มุมมองคล้ายกับการปรับรูปร่างของนัม" - ทำไมพวกเขาไม่เรียกมันว่าreshapeใน PyTorch!
MaxB

54
@ MaxB ซึ่งแตกต่างจากการปรับรูปร่างใหม่เมตริกซ์ใหม่ที่ส่งคืนโดย "view" ใช้ข้อมูลพื้นฐานร่วมกับเมตริกซ์ดั้งเดิมดังนั้นจึงเป็นมุมมองที่แท้จริงเกี่ยวกับเมตริกซ์เก่าแทนการสร้างใหม่
qihqi

39
@blckbird "เปลี่ยนรูปร่างหน่วยความจำเสมอดูไม่ต้องคัดลอกหน่วยความจำ" github.com/torch/cutorch/issues/98
devinbost

3
@devinbost Torch reshape คัดลอกหน่วยความจำเสมอ NumPyปรับรูปร่างไม่ได้
Tavian Barnes

32

ลองทำตัวอย่างจากง่ายกว่าไปยากกว่า

  1. viewวิธีการส่งกลับเมตริกซ์ที่มีข้อมูลเช่นเดียวกับselfเมตริกซ์ (ซึ่งหมายความว่าเมตริกซ์กลับมีหมายเลขเดียวกันขององค์ประกอบ) แต่มีรูปร่างที่แตกต่างกัน ตัวอย่างเช่น:

    a = torch.arange(1, 17)  # a's shape is (16,)
    
    a.view(4, 4) # output below
      1   2   3   4
      5   6   7   8
      9  10  11  12
     13  14  15  16
    [torch.FloatTensor of size 4x4]
    
    a.view(2, 2, 4) # output below
    (0 ,.,.) = 
    1   2   3   4
    5   6   7   8
    
    (1 ,.,.) = 
     9  10  11  12
    13  14  15  16
    [torch.FloatTensor of size 2x2x4]
  2. สมมติว่า-1ไม่ใช่พารามิเตอร์อย่างใดอย่างหนึ่งเมื่อคุณคูณพวกเขาเข้าด้วยกันผลลัพธ์จะต้องเท่ากับจำนวนขององค์ประกอบในเมตริกซ์ หากคุณทำเช่นa.view(3, 3)นั้นจะเป็นการเพิ่มRuntimeErrorรูปร่างเนื่องจาก (3 x 3) ไม่ถูกต้องสำหรับอินพุตที่มีอิลิเมนต์ 16 องค์ประกอบ กล่าวอีกนัยหนึ่ง: 3 x 3 ไม่เท่ากับ 16 แต่ 9

  3. คุณสามารถใช้-1เป็นหนึ่งในพารามิเตอร์ที่คุณส่งผ่านไปยังฟังก์ชั่น แต่เพียงครั้งเดียว สิ่งที่เกิดขึ้นคือวิธีการจะทำคณิตศาสตร์ให้คุณในการเติมมิตินั้น ยกตัวอย่างเช่นเทียบเท่ากับa.view(2, -1, 4) a.view(2, 2, 4)[16 / (2 x 4) = 2]

  4. ขอให้สังเกตว่าเมตริกซ์กลับหุ้นข้อมูลเดียวกัน หากคุณทำการเปลี่ยนแปลงใน "มุมมอง" คุณกำลังเปลี่ยนข้อมูลของเมตริกซ์เดิม:

    b = a.view(4, 4)
    b[0, 2] = 2
    a[2] == 3.0
    False
  5. ตอนนี้สำหรับกรณีการใช้งานที่ซับซ้อนมากขึ้น เอกสารระบุว่ามิติข้อมูลมุมมองใหม่แต่ละรายการต้องเป็นพื้นที่ย่อยของมิติข้อมูลดั้งเดิมหรือขยายได้เฉพาะd, d + 1, ... , d + kที่ตรงตามเงื่อนไขต่อไปนี้สำหรับi = 0, .. , k - 1, กางเกง [ผม] = ก้าว [i + 1] x ขนาด [i + 1] มิฉะนั้นcontiguous()จะต้องมีการเรียกก่อนที่จะสามารถดูเมตริกซ์ได้ ตัวอย่างเช่น:

    a = torch.rand(5, 4, 3, 2) # size (5, 4, 3, 2)
    a_t = a.permute(0, 2, 3, 1) # size (5, 3, 2, 4)
    
    # The commented line below will raise a RuntimeError, because one dimension
    # spans across two contiguous subspaces
    # a_t.view(-1, 4)
    
    # instead do:
    a_t.contiguous().view(-1, 4)
    
    # To see why the first one does not work and the second does,
    # compare a.stride() and a_t.stride()
    a.stride() # (24, 6, 2, 1)
    a_t.stride() # (24, 2, 1, 6)

    ขอให้สังเกตว่าสำหรับa_t, กางเกง [0]! = ก้าว [1] x ขนาด [1]ตั้งแต่24! = 2 x 3


8

torch.Tensor.view()

ใส่ง่ายๆtorch.Tensor.view()ซึ่งได้แรงบันดาลใจจากnumpy.ndarray.reshape()หรือnumpy.reshape()สร้างมุมมองใหม่ของเทนเซอร์ตราบใดที่รูปร่างใหม่เข้ากันได้กับรูปร่างของเทนเซอร์เดิม

มาทำความเข้าใจในรายละเอียดโดยใช้ตัวอย่างที่เป็นรูปธรรม

In [43]: t = torch.arange(18) 

In [44]: t 
Out[44]: 
tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17])

ด้วยเมตริกซ์นี้tของรูปร่าง(18,)ใหม่มุมมองที่สามารถเพียงถูกสร้างขึ้นสำหรับรูปร่างต่อไปนี้:

(1, 18)หรือเท่ากัน (1, -1)หรือ หรือเท่ากัน หรือ หรือเท่ากัน หรือ หรือเท่ากัน หรือ หรือเท่ากัน หรือหรือเทียบเท่า หรือ(-1, 18)
(2, 9)(2, -1)(-1, 9)
(3, 6)(3, -1)(-1, 6)
(6, 3)(6, -1)(-1, 3)
(9, 2)(9, -1)(-1, 2)
(18, 1)(18, -1)(-1, 1)

ในฐานะที่เรามีอยู่แล้วสามารถสังเกตจากรูปร่าง tuples ข้างต้นคูณขององค์ประกอบของ tuple รูปร่าง (เช่น2*9, 3*6ฯลฯ )มักจะต้องเท่ากับจำนวนขององค์ประกอบในเมตริกซ์เดิม (18ในตัวอย่างของเรา)

สิ่งที่ต้องสังเกตอีกอย่างก็คือเราใช้-1หนึ่งในสถานที่ในแต่ละสิ่งอันดับ tuples โดยใช้-1เราจะขี้เกียจในการทำคำนวณตัวเองและค่อนข้างมอบหมายงานให้ PyTorch จะทำคำนวณมูลค่าที่สำหรับรูปร่างเมื่อมันสร้างใหม่มุมมอง สิ่งหนึ่งที่สำคัญที่จะต้องทราบคือการที่เราสามารถเพียงใช้ครั้งเดียว-1ใน tuple รูปร่าง ค่าที่เหลือควรให้เราอย่างชัดเจน PyTorch อื่นจะบ่นโดยการขว้างปาRuntimeError:

RuntimeError: สามารถอนุมานได้หนึ่งมิติเท่านั้น

ดังนั้นด้วยรูปร่างที่กล่าวมาทั้งหมด PyTorch จะกลับมามีมุมมองใหม่ของเมตริกซ์เดิมtเสมอ โดยทั่วไปหมายความว่าเพียงเปลี่ยนข้อมูลกางเกงของเทนเซอร์สำหรับแต่ละมุมมองใหม่ที่ร้องขอ

ด้านล่างนี้คือตัวอย่างบางส่วนแสดงวิธีก้าวหน้าของเทนเซอร์ที่มีการเปลี่ยนแปลงกับใหม่แต่ละมุมมอง

# stride of our original tensor `t`
In [53]: t.stride() 
Out[53]: (1,)

ตอนนี้เราจะเห็นความก้าวหน้าสำหรับมุมมองใหม่:

# shape (1, 18)
In [54]: t1 = t.view(1, -1)
# stride tensor `t1` with shape (1, 18)
In [55]: t1.stride() 
Out[55]: (18, 1)

# shape (2, 9)
In [56]: t2 = t.view(2, -1)
# stride of tensor `t2` with shape (2, 9)
In [57]: t2.stride()       
Out[57]: (9, 1)

# shape (3, 6)
In [59]: t3 = t.view(3, -1) 
# stride of tensor `t3` with shape (3, 6)
In [60]: t3.stride() 
Out[60]: (6, 1)

# shape (6, 3)
In [62]: t4 = t.view(6,-1)
# stride of tensor `t4` with shape (6, 3)
In [63]: t4.stride() 
Out[63]: (3, 1)

# shape (9, 2)
In [65]: t5 = t.view(9, -1) 
# stride of tensor `t5` with shape (9, 2)
In [66]: t5.stride()
Out[66]: (2, 1)

# shape (18, 1)
In [68]: t6 = t.view(18, -1)
# stride of tensor `t6` with shape (18, 1)
In [69]: t6.stride()
Out[69]: (1, 1)

นั่นคือความมหัศจรรย์ของview()ฟังก์ชั่น เพียงแค่เปลี่ยนความก้าวหน้าของเทนเซอร์ (ต้นฉบับ) สำหรับแต่ละมุมมองใหม่ตราบใดที่รูปร่างของมุมมองใหม่เข้ากันได้กับรูปร่างเดิม

อีกสิ่งหนึ่งที่น่าสนใจอาจสังเกตจากความก้าวหน้าอย่างเป็นอันดับที่มูลค่าขององค์ประกอบใน 0 ที่ตำแหน่งเท่ากับมูลค่าขององค์ประกอบใน 1 เซนต์ตำแหน่งของ tuple รูปร่าง

In [74]: t3.shape 
Out[74]: torch.Size([3, 6])
                        |
In [75]: t3.stride()    |
Out[75]: (6, 1)         |
          |_____________|

นี้เป็นเพราะ:

In [76]: t3 
Out[76]: 
tensor([[ 0,  1,  2,  3,  4,  5],
        [ 6,  7,  8,  9, 10, 11],
        [12, 13, 14, 15, 16, 17]])

กางเกง(6, 1)บอกว่าจะไปจากองค์ประกอบหนึ่งไปยังองค์ประกอบถัดไป 0 THมิติเราจะต้องกระโดดหรือใช้ขั้นตอนที่ 6 (คือจะไปจาก0ที่จะ6มีใครที่จะใช้ขั้นตอนที่ 6.) แต่จะไปจากองค์ประกอบหนึ่งไปยังองค์ประกอบถัดไปใน 1 เซนต์มิติเราเพียงแค่ต้องการเพียงขั้นตอนเดียว (สำหรับเช่นจะไปจาก2การ3 )

ดังนั้นข้อมูลความก้าวหน้าจึงเป็นหัวใจสำคัญของการเข้าถึงองค์ประกอบจากหน่วยความจำสำหรับการคำนวณ


torch.reshape ()

ฟังก์ชั่นนี้จะคืนค่ามุมมองและเหมือนกับการใช้torch.Tensor.view()งานตราบใดที่รูปร่างใหม่เข้ากันได้กับรูปร่างของเทนเซอร์ดั้งเดิม มิฉะนั้นจะส่งคืนสำเนา

อย่างไรก็ตามการบันทึกของ torch.reshape()เตือนว่า:

อินพุตที่ต่อเนื่องและอินพุตที่มี strides ที่รองรับสามารถถูก reshaped โดยไม่ต้องคัดลอก แต่ไม่ควรขึ้นอยู่กับพฤติกรรมการคัดลอกและการดู


1

ฉันคิดออกว่าx.view(-1, 16 * 5 * 5)เทียบเท่ากับx.flatten(1)ที่พารามิเตอร์ 1 บ่งชี้ว่ากระบวนการแบนเริ่มจากมิติที่ 1 (ไม่แบนขนาด 'ตัวอย่าง') อย่างที่คุณเห็นการใช้งานครั้งหลังนั้นชัดเจนและใช้งานได้ง่ายขึ้นดังนั้นฉันจึง flatten()ชอบ


1

ความหมายของพารามิเตอร์ -1 คืออะไร

คุณสามารถอ่าน-1เป็นจำนวนพารามิเตอร์แบบไดนามิกหรือ "อะไรก็ได้" เพราะการที่สามารถมีได้เพียงหนึ่งพารามิเตอร์ใน-1view()

หากคุณถามx.view(-1,1)นี้รูปร่างออกจะเมตริกซ์ขึ้นอยู่กับจำนวนขององค์ประกอบใน[anything, 1] xตัวอย่างเช่น:

import torch
x = torch.tensor([1, 2, 3, 4])
print(x,x.shape)
print("...")
print(x.view(-1,1), x.view(-1,1).shape)
print(x.view(1,-1), x.view(1,-1).shape)

จะส่งออก:

tensor([1, 2, 3, 4]) torch.Size([4])
...
tensor([[1],
        [2],
        [3],
        [4]]) torch.Size([4, 1])
tensor([[1, 2, 3, 4]]) torch.Size([1, 4])

1

weights.reshape(a, b) จะส่งคืนเมตริกซ์ใหม่ที่มีข้อมูลเดียวกับน้ำหนักที่มีขนาด (a, b) เหมือนกับที่มันคัดลอกข้อมูลไปยังหน่วยความจำส่วนอื่น

weights.resize_(a, b)ส่งคืนเมตริกซ์เดียวกันกับรูปร่างที่แตกต่าง อย่างไรก็ตามหากรูปร่างใหม่ส่งผลให้มีองค์ประกอบน้อยกว่าเมตริกซ์ดั้งเดิมองค์ประกอบบางส่วนจะถูกลบออกจากเมตริกซ์ (แต่ไม่ใช่จากหน่วยความจำ) ถ้ารูปร่างใหม่ส่งผลให้องค์ประกอบมากกว่าเมตริกซ์เดิมองค์ประกอบใหม่จะถูกกำหนดค่าเริ่มต้นในหน่วยความจำ

weights.view(a, b) จะส่งคืนเมตริกซ์ใหม่พร้อมข้อมูลเดียวกับน้ำหนักที่มีขนาด (a, b)


1

ลองทำความเข้าใจมุมมองด้วยตัวอย่างต่อไปนี้:

    a=torch.range(1,16)

print(a)

    tensor([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12., 13., 14.,
            15., 16.])

print(a.view(-1,2))

    tensor([[ 1.,  2.],
            [ 3.,  4.],
            [ 5.,  6.],
            [ 7.,  8.],
            [ 9., 10.],
            [11., 12.],
            [13., 14.],
            [15., 16.]])

print(a.view(2,-1,4))   #3d tensor

    tensor([[[ 1.,  2.,  3.,  4.],
             [ 5.,  6.,  7.,  8.]],

            [[ 9., 10., 11., 12.],
             [13., 14., 15., 16.]]])
print(a.view(2,-1,2))

    tensor([[[ 1.,  2.],
             [ 3.,  4.],
             [ 5.,  6.],
             [ 7.,  8.]],

            [[ 9., 10.],
             [11., 12.],
             [13., 14.],
             [15., 16.]]])

print(a.view(4,-1,2))

    tensor([[[ 1.,  2.],
             [ 3.,  4.]],

            [[ 5.,  6.],
             [ 7.,  8.]],

            [[ 9., 10.],
             [11., 12.]],

            [[13., 14.],
             [15., 16.]]])

-1 เนื่องจากค่าอาร์กิวเมนต์เป็นวิธีที่ง่ายในการคำนวณค่าของ say x หากเราทราบค่าของ y, z หรือวิธีอื่นในกรณีของ 3d และสำหรับ 2d อีกครั้งวิธีง่ายๆในการคำนวณค่าของ say x ที่เราได้รับ รู้ค่าของ y หรือในทางกลับกัน ..


0

ฉันชอบ @Jadiel de Armas จริงๆ

ฉันต้องการเพิ่มความเข้าใจเล็กน้อยเกี่ยวกับวิธีการจัดเรียงองค์ประกอบสำหรับ. view (... )

  • สำหรับเทนเซอร์ที่มีรูปร่าง(A, B, C)การสั่งซื้อขององค์ประกอบของมันจะถูกกำหนดโดยระบบเลข A: ที่หลักแรกมี ตัวเลขสองหลักมีตัวเลขและหลักสามมีหมายเลข
  • การแมปองค์ประกอบใน Tensor ใหม่ที่ส่งคืนโดย. view (... ) จะเก็บลำดับของ Tensor ดั้งเดิมไว้
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.