Anhang E: PyTorch - Basics

PyTorch ist - wie TensorFlow - eine Open-Source-Bibliothek für Maschinelles Lernen in Python. PyTorch wurde ursprünglich von der KI-Gruppe bei Facebook, genannt FAIR - Facebook AI Research (oder oft nur “Facebook AI”) - entwickelt. PyTorch tritt in direkte Konkurrenz zu Keras (Google) und soll daher in zwei Anhängen vorgestellt werden.

Zentrales Werkzeug in PyTorch sind natürlich Tensoren. Im Gegensatz zu Keras, das sich stark auf die Bibliothek NumPy stützt, hat PyTorch eigene Mechanismen, um effizient mit Tensoren zu arbeiten.

Dieses Kapitel erläutert daher die Handhabe von Daten, insbesondere von Tensoren, in PyTorch. Das zweite Kapitel geht dann auf das Thema Neuronale Netze ein.

E.1 Installation

Damit Sie PyTorch nutzen können, müssen Sie die Bibliothek auf Ihrem Rechner und in Ihrer entsprechenden Umgebung (falls Sie Umgebungen nutzen) installieren.

Anweisungen dazu finden Sie unter: https://pytorch.org/get-started/locally

Wenn Sie z.B. Anaconda nutzen, reicht dieses Kommando:

conda install pytorch torchvision -c pytorch

Eventuell müssen Sie andere Bibliotheken, wie z.B. NumPy updaten, damit PyTorch läuft. Das machen Sie einfach mit

conda install numpy

E.2 Tensoren

PyTorch hat seine eigenen Versionen von vielen NumPy-Datenformaten und Operationen - hier konzentrieren wir uns auf Tensoren. Diese Ausführungen gehen davon aus, dass Sie vertraut sind mit Tensoren, NumPy-Arrays und entsprechenden Operationen in NumPy.

Eine schöne Überblicksseite zu PyTorch-Tensoren ist https://jhui.github.io/2018/02/09/PyTorch-Basic-operations/

API: https://pytorch.org/docs/stable/tensors.html

E.2.1 Tensor-Objekte

Statt eines NumPy-Arrays nutzen wir in PyTorch die dortigen Tensor-Objekte. Dazu muss man das Paket torch importieren.

Hier sehen wir einfache 1-dimensionale Arrays.

import torch

a = torch.Tensor([4,2])
b = torch.Tensor([2,-1])

print(a)
print(b)
tensor([4., 2.])
tensor([ 2., -1.])

Es gilt die Notation mit eckigen Klammern:

a[0]
tensor(4.)

Man beachte, dass man durch den obigen Zugriff wieder einen Tensor erhält, nicht einen Skalar. Will man den eigentlichen Skalar-Wert erhalten, benutzt man item.

a[0].item()
4.0

Shape

Es gibt auch die Shape-Eigenschaft. Hier haben wir 1-dimensionale Tensoren der Länge 2.

print(a.shape)
print(b.shape)
torch.Size([2])
torch.Size([2])

Für Einzelwerte bekommt man einen “Null-Tensor”:

a[0].shape
torch.Size([])

Zufallswerte

Man kann schnell Tensoren mit Zufallswerten erzeugen. Hier übergibt man die gewünschte Shape.

API: https://pytorch.org/docs/stable/generated/torch.rand.html

c = torch.rand(2,3)
c
tensor([[0.3769, 0.4982, 0.9946],
        [0.8694, 0.2776, 0.9615]])

Standardmatrizen

Mit den Befehlen ones, zeros und eye kann man schnell Standardmatrizen erzeugen.

Mit ones erzeugt man eine mit Einsen gefüllte Matrix.

torch.ones(2, 3)
tensor([[1., 1., 1.],
        [1., 1., 1.]])

Analog mit Nullen:

torch.zeros(2, 3)
tensor([[0., 0., 0.],
        [0., 0., 0.]])

Mit eye (wegen des Gleichlauts mit dem “I” in “Identity”) erzeugt man eine Identitätsmatrix (muss quadratisch sein, d.h. Zeilenzahl = Spaltenzahl).

torch.eye(4)
tensor([[1., 0., 0., 0.],
        [0., 1., 0., 0.],
        [0., 0., 1., 0.],
        [0., 0., 0., 1.]])

Aufzählungen

Mit arange kann man einen Tensor mit den Werten 1, 2, 3, etc. erzeugen.

Wichtig: Der standardmäßig verwendete Datentyp der Werte ist Integer, nicht Float (s.u.).

torch.arange(9)
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8])

Möchte man Float-Werte erzwingen, kann man dies mit einem Paramter tun (Möglichkeit 1):

torch.arange(9, dtype=torch.float32)
tensor([0., 1., 2., 3., 4., 5., 6., 7., 8.])

Sie können den Grenzwert auch als Float kennzeichnen (Möglichkeit 2):

torch.arange(9.0)
tensor([0., 1., 2., 3., 4., 5., 6., 7., 8.])

Bei der Aufzählung können Sie auch mit Start- und Endwert (letzterer ist exklusiv) arbeiten.

torch.arange(5, 10)
tensor([5, 6, 7, 8, 9])

Boolesche Operationen/Filter sind möglich:

d = torch.Tensor([0.7, 0, 0.3, 0.6])
e = d > 0.5
e
tensor([ True, False, False,  True])

Man kann das benutzen, um einen Tensor zu filtern:

d[d > 0.5]
tensor([0.7000, 0.6000])

Jetzt kann man die booleschen Werte in dem Tensor wieder in Zahlen umwandeln:

e.int()
tensor([1, 0, 0, 1], dtype=torch.int32)

E.2.2 Reshape eines Tensors

Man kann sich Tensoren so vorstellen, dass alle Werte intern in einer langen, 1-dimensionalen Liste gespeichert sind (bei einer Matrix: alle Zeilen hintereinander gereiht). Die tatsächliche Shape wird sozusagen “oben drauf” gepackt. Eine Liste von 6 Zahlen kann ein Vektor der Länge 6 sein, eine 2x3-Matrix oder eine 3x2-Matrix. So kann man sich das Konzept des “Reshape” vorstellen. Man ändert einfach die Shape der gleichen linearen Liste von Werten.

Das Verändern der Shape (in NumPy die Methode reshape) funktioniert in PyTorch mit der Tensor-Methode view, die im Gegensatz zu reshape nicht-destruktiv ist.

Als Beispiel nehmen wir eine 3x2-Matrix.

a = torch.Tensor([[1, 2], [3, 4], [5, 6]])
a
tensor([[1., 2.],
        [3., 4.],
        [5., 6.]])

Diese möchten wir in einen Vektor der Länge 6 umwandeln.

a.view(6)
tensor([1., 2., 3., 4., 5., 6.])

Alternativ können wir eine 3x2-Matrix erzeugen.

a.view(2, 3)
tensor([[1., 2., 3.],
        [4., 5., 6.]])

Allerdings ist view nicht-destruktiv, so dass der ursprüngliche Tensor unverändert bleibt.

a
tensor([[1., 2.],
        [3., 4.],
        [5., 6.]])

Unspezifizierte Dimension

Eine -1 bedeutet, dass es egal ist, wie viele Elemente die Dimension enthält.

a.view(-1)
tensor([1., 2., 3., 4., 5., 6.])

Bei mehreren Dimension, wird die unspezifizierte Dimension ausgerechnet.

a.view(-1, 3)
tensor([[1., 2., 3.],
        [4., 5., 6.]])
a.view(3, -1)
tensor([[1., 2.],
        [3., 4.],
        [5., 6.]])

Im Extremfall kann es sein, dass die unspezifizierte Dimension die Länge 1 hat, d.h. man fügt einfach äußere Klammern hinzu. Das kommt tatsächlich in der Praxis häufiger vor, dass man z.B. nur einen (Test-)Tensor hat, aber eine Liste von Tensoren als Input benötigt. Dann fügt man einfach mit view in Kombination mit -1 eine Dimension hinzu (welche die Liste repräsentiert).

a.view(-1, 6)
tensor([[1., 2., 3., 4., 5., 6.]])
a.view(-1, 6).shape
torch.Size([1, 6])

Schnelles Erzeugen von Tensoren

In Kombination mit arange kann man schnell Tensoren erzeugen.

torch.arange(12).view(2,6)
tensor([[ 0,  1,  2,  3,  4,  5],
        [ 6,  7,  8,  9, 10, 11]])
torch.arange(12).view(3,4)
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]])

E.2.3 Kombinieren von Tensoren

Stapeln

Beim Stapeln können wir mehrere Tensoren der gleichen Form zu einem neuen Tensor zusammenfügen. Der neue Tensor hat (im Gegensatz zur Konkatenation) hat eine Dimension mehr. Die neue Dimension steht an erster Stelle in der resultierenden Shape.

Siehe API: https://pytorch.org/docs/stable/generated/torch.stack.html

a = torch.arange(5)
b = torch.arange(5, 10)
c = torch.arange(10, 15)
a, b, c
(tensor([0, 1, 2, 3, 4]),
 tensor([5, 6, 7, 8, 9]),
 tensor([10, 11, 12, 13, 14]))

Beim Stapeln werden eine Reihe von \(n\) Tensoren zusammengefügt zu einem neuen Tensor. Der neue Tensor hat eine neue Dimension der Größe \(n\). Dabei müssen alle Tensoren die gleiche Form (shape) haben.

Im Beispiel werden 3 Vektoren der Länge 5 zusammengefügt zu einer 3x5-Matrix.

d = torch.stack((a, b, c))
print(d.shape)
d
torch.Size([3, 5])
tensor([[ 0,  1,  2,  3,  4],
        [ 5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14]])

Im nächsten Beispiel werden 3 Matrizen der Form 2x2 gestapelt in einem 3x2x2-Tensor.

a = torch.arange(4).view(2, 2)
b = torch.arange(4, 8).view(2, 2)
c = torch.arange(8, 12).view(2, 2)
a, b, c
(tensor([[0, 1],
         [2, 3]]),
 tensor([[4, 5],
         [6, 7]]),
 tensor([[ 8,  9],
         [10, 11]]))
d = torch.stack((a, b, c))
print(d.shape)
d
torch.Size([3, 2, 2])
tensor([[[ 0,  1],
         [ 2,  3]],

        [[ 4,  5],
         [ 6,  7]],

        [[ 8,  9],
         [10, 11]]])

Konkatenation

Bei der Konkatenation fügen wir mehrere Tensoren entlang einer ihrer Dimensionen aneinander. Die Dimension, entlang welcher zusammengefügt wird, erweitert sich somit. Alle anderen Dimensionen müssen die gleich Größe aufweisen. Der resultierende Tensor hat (im Gegensatz zum Stapeln) die gleiche Anzahl von Dimensionen wie die ursprünglichen Tensoren.

https://pytorch.org/docs/stable/generated/torch.cat.html

a = torch.arange(6).view(3, 2)
b = torch.arange(6, 12).view(3, 2)
print(a)
print(b)
tensor([[0, 1],
        [2, 3],
        [4, 5]])
tensor([[ 6,  7],
        [ 8,  9],
        [10, 11]])

Standardmäßig wird entlang Dimension 0 (= Zeilen) konkateniert. Aus zwei 3x2-Matrizen wird also eine 6x2-Matrix.

torch.cat((a, b))
tensor([[ 0,  1],
        [ 2,  3],
        [ 4,  5],
        [ 6,  7],
        [ 8,  9],
        [10, 11]])

Wenn man entlang Dimension 1 (= Spalten) konkateniert, wird aus zwei 3x2-Matrizen eine 3x4-Matrix.

torch.cat((a, b), dim=1)
tensor([[ 0,  1,  6,  7],
        [ 2,  3,  8,  9],
        [ 4,  5, 10, 11]])
c = torch.arange(8).view(4, 2)
c
tensor([[0, 1],
        [2, 3],
        [4, 5],
        [6, 7]])

Wenn man entlang Dimension d konkateniert, müssen alle anderen Größen der Tensoren gleich sein. Beispiel: Konkateniert man entlang Dimension 0, dann muss Dimension 1 gleich sein.

Hier konkatenieren wir eine 3x2-Matrix mit einer 4x2-Matrix entlang Dimension 0 und erhalten eine 7x2.Matrix.

d = torch.cat((a, c))
print(a.shape)
print(c.shape)
print(d.shape)
d
torch.Size([3, 2])
torch.Size([4, 2])
torch.Size([7, 2])
tensor([[0, 1],
        [2, 3],
        [4, 5],
        [0, 1],
        [2, 3],
        [4, 5],
        [6, 7]])

E.2.4 Multiplikation

Skalarprodukt

Hier das Skalarprodukt zweier Vektoren (engl. dot product):

a = torch.Tensor([1, 2])
b = torch.Tensor([3, 4])

Wir nutzen die Funktion dot:

a.dot(b)
tensor(11.)

Tensor a wird dabei nicht verändert:

a
tensor([1., 2.])

Alternativ auch so:

torch.dot(a, b)
tensor(11.)

Eine weitere Möglichkeit ist die Funktion matmul:

a.matmul(b)
tensor(11.)
torch.matmul(a, b)
tensor(11.)

Matrixmultiplikation

c = torch.arange(4, dtype=torch.float32).view(2,2)
d = torch.arange(5, 9, dtype=torch.float32).view(2,2)
print(c)
print(d)
tensor([[0., 1.],
        [2., 3.]])
tensor([[5., 6.],
        [7., 8.]])

Für die Multiplikation eines Tensoren mit einem Skalar benutzt man Broadcasting:

10 * a
tensor([10., 20.])
10 * c
tensor([[ 0., 10.],
        [20., 30.]])

Zwei Matrizen multipliziert man mit mm (für “matrix-matrix”).

c.mm(d)
tensor([[ 7.,  8.],
        [31., 36.]])

Eine Matrix und ein Vektor multipliziert man mit mv:

print(c.shape)
print(a.shape)

torch.mv(c, a)
torch.Size([2, 2])
torch.Size([2])
tensor([2., 8.])

Mit der Funktion matmul muss man nicht genau darauf achten, wo eine Matrix und wo ein Vektor steht.

c.matmul(d)
tensor([[ 7.,  8.],
        [31., 36.]])
c.matmul(a)
tensor([2., 8.])