Làm thế nào để phương thức xem viewv hoạt động trong PyTorch?


205

Tôi bối rối về phương thức view()trong đoạn mã sau.

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

Sự nhầm lẫn của tôi là liên quan đến dòng sau.

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

Không gì tensor.view()chức năng làm gì? Tôi đã thấy việc sử dụng nó ở nhiều nơi, nhưng tôi không thể hiểu làm thế nào nó diễn giải các tham số của nó.

Điều gì xảy ra nếu tôi đưa ra các giá trị âm làm tham số cho view()hàm? Ví dụ, điều gì xảy ra nếu tôi gọi , tensor_variable.view(1, 1, -1)?

Bất cứ ai có thể giải thích nguyên tắc chính của view()chức năng với một số ví dụ?

Câu trả lời:


283

Hàm view có nghĩa là định hình lại tenxơ.

Nói rằng bạn có một tenor

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

alà một tenxơ có 16 phần tử từ 1 đến 16 (bao gồm). Nếu bạn muốn định hình lại tenor này để biến nó thành 4 x 4tenor thì bạn có thể sử dụng

a = a.view(4, 4)

Bây giờ asẽ là một 4 x 4tenor. Lưu ý rằng sau khi định hình lại, tổng số phần tử cần giữ nguyên. Định hình lại tenxơ athành 3 x 5tenxơ sẽ không phù hợp.

Ý nghĩa của tham số -1 là gì?

Nếu có bất kỳ tình huống nào mà bạn không biết có bao nhiêu hàng bạn muốn nhưng chắc chắn về số lượng cột, thì bạn có thể chỉ định điều này với -1. ( Lưu ý rằng bạn có thể mở rộng giá trị này thành các thang đo với nhiều kích thước hơn. Chỉ một trong các giá trị trục có thể là -1 ). Đây là một cách để nói với thư viện: "hãy cho tôi một tenxơ có nhiều cột này và bạn tính toán số lượng hàng thích hợp cần thiết để thực hiện điều này".

Điều này có thể được nhìn thấy trong mã mạng thần kinh mà bạn đã đưa ra ở trên. Sau dòng x = self.pool(F.relu(self.conv2(x)))trong chức năng chuyển tiếp, bạn sẽ có bản đồ tính năng 16 độ sâu. Bạn phải làm phẳng nó để cung cấp cho lớp được kết nối đầy đủ. Vì vậy, bạn bảo pytorch định hình lại tenxơ bạn thu được để có số lượng cột cụ thể và bảo nó tự quyết định số lượng hàng.

Vẽ một sự tương đồng giữa numpy và pytorch, viewtương tự như chức năng định hình lại của numpy .


93
"Chế độ xem tương tự như định hình lại của numpy" - tại sao họ không gọi nó reshapetrong PyTorch?!
MaxB

54
@MaxB Không giống như định hình lại, tenxơ mới được trả về bằng "view" chia sẻ dữ liệu cơ bản với tenor gốc, vì vậy nó thực sự là một cái nhìn vào tenor cũ thay vì tạo ra một thương hiệu mới.
qihqi

37
@blckbird "định hình lại luôn sao chép bộ nhớ. xem không bao giờ sao chép bộ nhớ." github.com/torch/cutorch/issues/98
devinbost

3
@devinbost Định hình lại ngọn đuốc luôn sao chép bộ nhớ. Định hình lại NumPy không.
Tavian Barnes

32

Chúng ta hãy làm một số ví dụ, từ đơn giản đến khó khăn hơn.

  1. Các viewphương thức trả về một tensor với dữ liệu tương tự như các selftensor (có nghĩa là tensor trở có cùng số phần tử), nhưng với một hình dạng khác nhau. Ví dụ:

    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. Giả sử đó -1không phải là một trong các tham số, khi bạn nhân chúng với nhau, kết quả phải bằng số phần tử trong tenxơ. Nếu bạn làm : a.view(3, 3), nó sẽ tăng một RuntimeErrorhình vì 3 (3 x 3) không hợp lệ cho đầu vào có 16 phần tử. Nói cách khác: 3 x 3 không bằng 16 mà là 9.

  3. Bạn có thể sử dụng -1như một trong các tham số mà bạn truyền cho hàm, nhưng chỉ một lần. Tất cả những gì xảy ra là phương pháp sẽ làm toán cho bạn về cách điền vào chiều đó. Ví dụ a.view(2, -1, 4)tương đương với a.view(2, 2, 4). [16 / (2 x 4) = 2]

  4. Lưu ý rằng tenor trả về chia sẻ cùng một dữ liệu . Nếu bạn thực hiện thay đổi trong "chế độ xem", bạn đang thay đổi dữ liệu của tenor gốc:

    b = a.view(4, 4)
    b[0, 2] = 2
    a[2] == 3.0
    False
    
  5. Bây giờ, cho một trường hợp sử dụng phức tạp hơn. Tài liệu nói rằng mỗi thứ nguyên chế độ xem mới phải là không gian con của thứ nguyên ban đầu hoặc chỉ khoảng d, d + 1, ..., d + k thỏa mãn điều kiện giống như tiếp theo sau với tất cả i = 0 ,. .., k - 1, sải chân [i] = sải chân [i + 1] x size [i + 1] . Mặt khác, contiguous()cần phải được gọi trước khi tenor có thể được xem. Ví dụ:

    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)
    

    Lưu ý rằng a_t, sải chân [0]! = Sải bước [1] x size [1] kể từ 24! = 2 x 3


7

torch.Tensor.view()

Nói một cách đơn giản, torch.Tensor.view()được lấy cảm hứng từ numpy.ndarray.reshape()hoặc numpy.reshape(), tạo ra một cái nhìn mới về tenxơ, miễn là hình dạng mới tương thích với hình dạng của tenxơ ban đầu.

Hãy hiểu chi tiết điều này bằng một ví dụ cụ thể.

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

Với tensor này thình dạng (18,), mới xem có thể chỉ được tạo ra cho các hình dạng sau:

(1, 18)hoặc tương đương (1, -1)hoặc hoặc tương đương hoặc hoặc tương đương hoặc hoặc tương đương hoặc hoặc tương đương hoặc hoặc tương đương hoặc(-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)

Như chúng ta đã có thể quan sát từ các bộ hình dạng ở trên, phép nhân của các phần tử của bộ hình dạng (ví dụ 2*9, 3*6v.v.) phải luôn bằng tổng số phần tử trong thang đo ban đầu ( 18trong ví dụ của chúng tôi).

Một điều khác để quan sát là chúng tôi đã sử dụng một -1trong những vị trí trong mỗi bộ hình dạng. Bằng cách sử dụng a -1, chúng ta sẽ lười biếng trong việc tự tính toán và giao nhiệm vụ cho PyTorch để tính toán giá trị đó cho hình dạng khi nó tạo ra chế độ xem mới . Một điều quan trọng cần lưu ý là chúng ta chỉ có thể sử dụng một -1trong hình dạng tuple. Các giá trị còn lại phải được cung cấp rõ ràng bởi chúng tôi. Else PyTorch sẽ khiếu nại bằng cách ném RuntimeError:

RuntimeError: chỉ có thể suy ra một chiều

Vì vậy, với tất cả các hình dạng được đề cập ở trên, PyTorch sẽ luôn trả lại một cái nhìn mới về tenxơ ban đầu t. Điều này về cơ bản có nghĩa là nó chỉ thay đổi thông tin sải chân của tenor cho mỗi khung nhìn mới được yêu cầu.

Dưới đây là một số ví dụ minh họa cách các bước của thang đo được thay đổi với mỗi chế độ xem mới .

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

Bây giờ, chúng ta sẽ thấy những bước tiến cho các quan điểm mới :

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

Vì vậy, đó là sự kỳ diệu của view()chức năng. Nó chỉ thay đổi các bước của tenxơ (ban đầu) cho mỗi chế độ xem mới , miễn là hình dạng của chế độ xem mới tương thích với hình dạng ban đầu.

Một điều thú vị khác mà người ta có thể quan sát từ các bộ sải chân là giá trị của phần tử ở vị trí thứ 0 bằng với giá trị của phần tử ở vị trí thứ 1 của hình dạng.

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

Điều này là do:

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

sải chân (6, 1)nói rằng để đi từ một yếu tố này sang yếu tố tiếp theo dọc theo chiều thứ 0 , chúng ta phải nhảy hoặc thực hiện 6 bước. (tức là để đi từ 0đến 6, người ta phải thực hiện 6 bước.) Nhưng để đi từ yếu tố này sang yếu tố tiếp theo trong chiều thứ 1 , chúng ta chỉ cần một bước (ví dụ: đi từ 2đến 3).

Do đó, thông tin bước tiến là cốt lõi của cách các phần tử được truy cập từ bộ nhớ để thực hiện tính toán.


ngọn đuốc.reshape ()

Hàm này sẽ trả về một khung nhìn và hoàn toàn giống như sử dụng torch.Tensor.view()miễn là hình dạng mới tương thích với hình dạng của tenxơ ban đầu. Nếu không, nó sẽ trả về một bản sao.

Tuy nhiên, các ghi chú torch.reshape()cảnh báo rằng:

đầu vào liền kề và đầu vào với các bước tương thích có thể được định hình lại mà không cần sao chép, nhưng người ta không nên phụ thuộc vào hành vi sao chép so với hành vi xem.


1

Tôi đã tìm ra nó x.view(-1, 16 * 5 * 5)tương đương với x.flatten(1), trong đó tham số 1 chỉ ra quá trình làm phẳng bắt đầu từ chiều thứ 1 (không làm phẳng kích thước 'mẫu') Như bạn có thể thấy, cách sử dụng sau rõ ràng hơn và dễ sử dụng hơn, vì vậy tôi thích flatten().


1

Ý nghĩa của tham số -1 là gì?

Bạn có thể đọc -1dưới dạng số tham số động hoặc "bất cứ thứ gì". Do đó, chỉ có một tham số -1trong đó view().

Nếu bạn hỏi x.view(-1,1)điều này sẽ xuất ra hình dạng tenor [anything, 1]tùy thuộc vào số lượng phần tử trong x. Ví dụ:

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)

Sẽ xuất:

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) sẽ trả về một tenxơ mới có cùng dữ liệu với trọng số với kích thước (a, b) như trong nó sao chép dữ liệu sang một phần khác của bộ nhớ.

weights.resize_(a, b)trả về cùng một tenxơ với một hình dạng khác nhau. Tuy nhiên, nếu hình dạng mới dẫn đến ít thành phần hơn so với tenor ban đầu, một số phần tử sẽ bị xóa khỏi tenor (nhưng không phải từ bộ nhớ). Nếu hình dạng mới dẫn đến nhiều phần tử hơn so với tenor ban đầu, các phần tử mới sẽ không được khởi tạo trong bộ nhớ.

weights.view(a, b) sẽ trả về một tenxơ mới có cùng dữ liệu với trọng số với kích thước (a, b)


0

Tôi thực sự thích các ví dụ @Jadiel de Armas.

Tôi muốn thêm một cái nhìn sâu sắc nhỏ về cách các yếu tố được đặt hàng cho .view (...)

  • Đối với một thang đo có hình dạng (a, b, c) , thứ tự các phần tử của nó được xác định bởi một hệ thống đánh số: trong đó chữ số thứ nhất có một số, chữ số thứ hai có số b và chữ số thứ ba có số c .
  • Ánh xạ của các phần tử trong Tenor mới được trả về bởi .view (...) duy trì thứ tự này của Tenor gốc.

0

Hãy cố gắng hiểu quan điểm bằng các ví dụ sau:

    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 làm giá trị đối số là một cách dễ dàng để tính giá trị của say x với điều kiện chúng ta biết các giá trị của y, z hoặc ngược lại trong trường hợp 3d và trong 2d một cách dễ dàng để tính giá trị của x nói với điều kiện chúng ta biết giá trị của y hoặc ngược lại ..

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.