Wie funktioniert die Ansichtsmethode in PyTorch?

204

Ich bin verwirrt über die Methode view()im folgenden Code-Snippet.

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

Meine Verwirrung betrifft die folgende Zeile.

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

Was macht die tensor.view()Funktion? Ich habe seine Verwendung an vielen Stellen gesehen, aber ich kann nicht verstehen, wie es seine Parameter interpretiert.

Was passiert, wenn ich der view()Funktion negative Werte als Parameter gebe ? Was passiert zum Beispiel, wenn ich anrufe tensor_variable.view(1, 1, -1)?

Kann jemand das Hauptfunktionsprinzip anhand view()einiger Beispiele erklären ?

Wasi Ahmad
quelle

Antworten:

283

Die Ansichtsfunktion soll den Tensor umformen.

Angenommen, Sie haben einen Tensor

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

aist ein Tensor mit 16 Elementen von 1 bis 16 (enthalten). Wenn Sie diesen Tensor umformen möchten, um ihn zu einem 4 x 4Tensor zu machen, können Sie ihn verwenden

a = a.view(4, 4)

Jetzt awird ein 4 x 4Tensor sein. Beachten Sie, dass nach der Umformung die Gesamtzahl der Elemente gleich bleiben muss. Eine Umformung des Tensors azu einem 3 x 5Tensor wäre nicht angebracht.

Was bedeutet Parameter -1?

Wenn Sie nicht wissen, wie viele Zeilen Sie möchten, sich aber der Anzahl der Spalten sicher sind, können Sie dies mit -1 angeben. ( Beachten Sie, dass Sie dies auf Tensoren mit mehr Abmessungen erweitern können. Nur einer der Achsenwerte kann -1 sein. ) Dies ist eine Möglichkeit, der Bibliothek zu sagen: "Geben Sie mir einen Tensor mit diesen vielen Spalten, und Sie berechnen die entsprechende Anzahl von Zeilen, die erforderlich sind, um dies zu erreichen."

Dies ist in dem oben angegebenen neuronalen Netzwerkcode zu sehen. Nach der Linie x = self.pool(F.relu(self.conv2(x)))in der Vorwärtsfunktion erhalten Sie eine Feature-Map mit 16 Tiefen. Sie müssen dies reduzieren, um es der vollständig verbundenen Ebene zu geben. Sie weisen pytorch also an, den erhaltenen Tensor so umzuformen, dass er eine bestimmte Anzahl von Spalten aufweist, und die Anzahl der Zeilen selbst zu bestimmen.

Zeichnen einer Ähnlichkeit zwischen numpy und pytorch, viewist ähnlich der numpy reshape Funktion.

Kashyap
quelle
93
"Ansicht ist ähnlich wie Numpys Umformung" - warum haben sie es nicht einfach reshapein PyTorch genannt?!
MaxB
54
@MaxB Im Gegensatz zur Umformung teilt der von "view" zurückgegebene neue Tensor die zugrunde liegenden Daten mit dem ursprünglichen Tensor, sodass es sich tatsächlich um einen Blick in den alten Tensor handelt, anstatt einen brandneuen zu erstellen.
Qihqi
37
@blckbird "Umformen kopiert immer Speicher. Ansicht kopiert nie Speicher." github.com/torch/cutorch/issues/98
devinbost
3
@devinbost Torch Reshape kopiert immer den Speicher. NumPy- Umformung nicht.
Tavian Barnes
32

Lassen Sie uns einige Beispiele machen, von einfacher bis schwieriger.

  1. Die viewMethode gibt einen Tensor mit denselben Daten wie der selfTensor zurück (was bedeutet, dass der zurückgegebene Tensor die gleiche Anzahl von Elementen hat), jedoch mit einer anderen Form. Beispielsweise:

    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. Angenommen, dies -1ist keiner der Parameter. Wenn Sie sie miteinander multiplizieren, muss das Ergebnis der Anzahl der Elemente im Tensor entsprechen. Wenn Sie: tun a.view(3, 3), wird eine RuntimeErrorweil-Form (3 x 3) ausgelöst, die für die Eingabe mit 16 Elementen ungültig ist. Mit anderen Worten: 3 x 3 entspricht nicht 16, sondern 9.

  3. Sie können -1als einen der Parameter verwenden, die Sie an die Funktion übergeben, jedoch nur einmal. Alles, was passiert, ist, dass die Methode die Mathematik für Sie erledigt, wie diese Dimension gefüllt werden soll. Zum Beispiel a.view(2, -1, 4)ist äquivalent zu a.view(2, 2, 4). [16 / (2 x 4) = 2]

  4. Beachten Sie, dass der zurückgegebene Tensor dieselben Daten verwendet . Wenn Sie in der "Ansicht" eine Änderung vornehmen, ändern Sie die Daten des ursprünglichen Tensors:

    b = a.view(4, 4)
    b[0, 2] = 2
    a[2] == 3.0
    False
  5. Nun zu einem komplexeren Anwendungsfall. Die Dokumentation besagt, dass jede neue Ansichtsdimension entweder ein Unterraum einer ursprünglichen Dimension sein muss oder nur d, d + 1, ..., d + k überspannen muss , die die folgende zusammenhängungsähnliche Bedingung erfüllen, dass für alle i = 0 ,. .., k - 1, Schritt [i] = Schritt [i + 1] x Größe [i + 1] . Andernfalls contiguous()muss aufgerufen werden, bevor der Tensor angezeigt werden kann. Beispielsweise:

    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)

    Beachten Sie, dass für a_t, schreiten [0]! = Schritt [1] x Größe [1] seit 24! = 2 x 3

Jadiel de Armas
quelle
6

torch.Tensor.view()

Einfach ausgedrückt, torch.Tensor.view()die von numpy.ndarray.reshape()oder inspiriert ist numpy.reshape(), erzeugt eine neue Ansicht des Tensors, solange die neue Form mit der Form des ursprünglichen Tensors kompatibel ist.

Lassen Sie uns dies anhand eines konkreten Beispiels im Detail verstehen.

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

Mit dieser Tensor tder Form (18,), neue Ansichten können nur für die folgenden Formen erstellt werden:

(1, 18)oder gleichwertig (1, -1)oder oder gleichwertig oder oder gleichwertig oder oder gleichwertig oder oder gleichwertig oder oder gleichwertig oder(-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)

Wie wir bereits aus der obigen Form Tupeln beobachten können, die Multiplikation der Elemente der Form Tupel (zB 2*9, 3*6usw.) muß immer auf die Gesamtzahl der Elemente in der ursprünglichen Tensor (gleich 18in unserem Beispiel).

Eine andere Sache zu beobachten ist, dass wir ein -1an einer der Stellen in jedem der Formtupel verwendet haben. Durch die Verwendung von a -1sind wir bei der Berechnung selbst faul und delegieren die Aufgabe eher an PyTorch, um diesen Wert für die Form zu berechnen, wenn die neue Ansicht erstellt wird . Eine wichtige Sache zu beachten ist, dass wir nur ein einzelnes -1in der Form Tupel verwenden können. Die restlichen Werte sollten ausdrücklich von uns angegeben werden. Andernfalls beschwert sich PyTorch mit einem RuntimeError:

RuntimeError: Es kann nur eine Dimension abgeleitet werden

Bei allen oben genannten Formen gibt PyTorch immer eine neue Ansicht des ursprünglichen Tensors zurück t. Dies bedeutet im Grunde, dass nur die Schrittinformationen des Tensors für jede der neuen angeforderten Ansichten geändert werden.

Im Folgenden finden Sie einige Beispiele, die veranschaulichen, wie sich die Schritte der Tensoren bei jeder neuen Ansicht ändern .

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

Jetzt werden wir die Schritte für die neuen Ansichten sehen :

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

Das ist also die Magie der view()Funktion. Es werden nur die Schritte des (ursprünglichen) Tensors für jede der neuen Ansichten geändert , solange die Form der neuen Ansicht mit der ursprünglichen Form kompatibel ist.

Ein weitere interessante Sache könnte man aus den Schritten Tupeln beobachten ist , dass der Wert des Elements in der 0 - ten Position auf den Wert des Elements in der gleich 1 st Position des Form Tupels.

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

Das ist weil:

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

der Schritt (6, 1)sagt , dass entlang der 0 auf das nächste Element von einem Elemente gehen th Dimension, müssen wir springen 6 Stufen oder nehmen. (dh , um von 0zu 6, hat man 6 Schritte unternehmen.) Aber von einem Element zum nächsten Element in der 1 zu gehen st Dimension, wir müssen nur nur einen Schritt (zum Beispiel , um von 2zu 3).

Somit ist die Schrittinformation das Herzstück dessen, wie auf die Elemente aus dem Speicher zugegriffen wird, um die Berechnung durchzuführen.


torch.reshape ()

Diese Funktion würde eine Ansicht zurückgeben und entspricht genau der Verwendung torch.Tensor.view(), solange die neue Form mit der Form des ursprünglichen Tensors kompatibel ist. Andernfalls wird eine Kopie zurückgegeben.

Die Notizen von torch.reshape()warnen jedoch, dass:

zusammenhängende Eingaben und Eingaben mit kompatiblen Schritten können ohne Kopieren umgeformt werden, aber man sollte nicht vom Kopier- oder Anzeigeverhalten abhängen.

kmario23
quelle
1

Ich habe herausgefunden, dass dies x.view(-1, 16 * 5 * 5)äquivalent zu ist x.flatten(1), wenn der Parameter 1 angibt, dass der Abflachungsprozess ab der 1. Dimension beginnt (ohne die 'Beispiel'-Dimension abzuflachen). Wie Sie sehen können, ist die letztere Verwendung semantisch klarer und einfacher zu verwenden, also ich bevorzugen flatten().

FENGSHI ZHENG
quelle
1

Was bedeutet Parameter -1?

Sie können -1als dynamische Anzahl von Parametern oder "irgendetwas" lesen . Aus diesem Grunde kann es nur ein Parameter sein -1in view().

Wenn Sie danach fragen, x.view(-1,1)wird [anything, 1]abhängig von der Anzahl der Elemente in die Tensorform ausgegeben x. Beispielsweise:

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)

Wird ausgegeben:

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])
Prosti
quelle
1

weights.reshape(a, b) gibt einen neuen Tensor mit denselben Daten wie Gewichte mit der Größe (a, b) zurück, in dem die Daten in einen anderen Teil des Speichers kopiert werden.

weights.resize_(a, b)Gibt den gleichen Tensor mit einer anderen Form zurück. Wenn die neue Form jedoch zu weniger Elementen als der ursprüngliche Tensor führt, werden einige Elemente aus dem Tensor entfernt (jedoch nicht aus dem Speicher). Wenn die neue Form zu mehr Elementen als der ursprüngliche Tensor führt, werden neue Elemente im Speicher nicht initialisiert.

weights.view(a, b) gibt einen neuen Tensor mit denselben Daten zurück wie Gewichte mit Größe (a, b)

Jibin Mathew
quelle
0

Ich mochte @ Jadiel de Armas Beispiele sehr.

Ich möchte einen kleinen Einblick in die Reihenfolge der Elemente für .view (...) geben.

  • Für einen Tensor mit Form (a, b, c) wird die Reihenfolge seiner Elemente durch ein Nummerierungssystem bestimmt: wobei die erste Ziffer eine Zahl hat, die zweite Ziffer b Zahlen und die dritte Ziffer c Zahlen hat.
  • Die von .view (...) zurückgegebene Zuordnung der Elemente im neuen Tensor behält diese Reihenfolge des ursprünglichen Tensors bei.
ychnh
quelle
0

Versuchen wir, die Ansicht anhand der folgenden Beispiele zu verstehen:

    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 als Argumentwert ist eine einfache Methode, um den Wert von say x zu berechnen, vorausgesetzt, wir kennen die Werte von y, z oder umgekehrt, im Fall von 3d, und für 2d wiederum eine einfache Methode, um den Wert von say x zu berechnen, vorausgesetzt, wir Werte von y kennen oder umgekehrt ..

Lija Alex
quelle