Numpy

numpy ist vielleicht die wichtigste Python-Bibliothek welche nicht Teil der Python-Standardbibliothek. Viele weitere Python-Bibliothek basieren entweder auf numpy oder bieten eine numpy-Schnittstelle. Beispiele sind scipy, matplotlib, pandas, sklearn, pytorch oder python-control.

Geschichte von Numpy

Die Geschichte von numpy ist eng mit dem Namen Travis Oliphant verbunden. Wer die Geschichte von numpy lernen will, sollte die Folge-224 vom Lex Friedman Podcast hören.

Installation

Per Konvention wird numpy mit dem Alias Namen np versehen. Daran sollten wir uns halten. numpy wird ständig weiterentwickelt. Mit __version__ kann die Version ausgelesen werden.

import numpy as np
np.__version__
'1.21.2'

Numpy Arrays - Vektoren, Matrizen, 3D Tensoren

Der Hauptvorteil den numpy bring, sind die n-dimensionalen Arrays. In diesem Buch beschränken wir uns jedoch auf Vektoren, Matrizen und 3D Tensoren. Sind diese Objekte gut verstanden, fällt eine Verallgemeinerung auf n-dimensionale Objekte meist nicht schwer.

Skalar

Als Skalar wird eine mathematische Größe verstanden, die mit einem Zahlenwert charakterisiert wird. Skalare Größen besitzen keine Achsen bzw. Dimensionen.

Die Kreiszahl \(\pi\) ist ein solche Skalare Größe. Diese ist als Konstante in numpy enthalten.

np.pi
3.141592653589793
# (np.pi).shape # error

Warnung

(np.pi).shape führt auf einen Fehler, weil skalare Größen keine Dimensionen besitzen. Das unterscheidet skalare Größen fundamental von Arrays. Es sei aber sofort darauf hingewiesen, dass auch Arrays skalare Werte abbilden können. [np.pi] ist ein Vektor, [[np.pi]] ist eine Matrix, und [[[np.pi]]] ein 3D Tensor.

Vektoren

Wir wollen hier Vektoren in mathematische Vektoren und technische/physikalische Vektoren unterscheiden.

Als mathematische Vektoren

\[ v=(v_1,v_2,\dots,v_n) \]

wollen wir Objekte bezeichnen welche nur eine Achse bzw. Dimension besitzen. Diese Art von Vektoren wird in der Mathematik verwendet.

v = np.array([1,2,3]) # (mathematical) vector
print(v)
print(v.shape) # a vector has only 1-dimension
[1 2 3]
(3,)

Als technische Vektoren wollen wir Objekte bezeichnen welche zwei Achsen bzw. Dimension besitzen, wobei eine der beiden Dimensionen immer nur \(1\) sein darf.

Als Zeilenvektoren

\[ v = \begin{pmatrix} v_1 & v_2 & \cdots & v_n \end{pmatrix} \]

werden \(1 \times n\) Matrizen bezeichnet.

Als Spaltenvektoren

\[\begin{split} v = \begin{pmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{pmatrix} \end{split}\]

werden \(n \times 1\) Matrizen bezeichnet.

Viele technische Bücher benutzen technische Vektoren. Manche Bücher erlauben nur das Definieren von Spaltenvektoren \(v\) und Zeilenvektoren dürfen nur als Transponierte Spaltenvektoren \(v^T\) auftreten.

v = np.array([[1,2,3]]) # row vector
print(v)
print(v.shape) # a row vector has 2-dimensions
[[1 2 3]]
(1, 3)
v =np.array([[1],[2],[3]]) # column vector
print(v)
print(v.shape) # a column vector has 2-dimensions
[[1]
 [2]
 [3]]
(3, 1)

Matrizen

Objekte mit 2 Achsen bzw. Dimensionen werden als Matrizen bezeichnet.

Unter einer Matrix vom Typ \(m \times n\) (man spricht auch von einer \(m \times n\) Matrix) versteht man

\[\begin{split} X = x_{ij} = \begin{bmatrix} x_{11} & x_{12} & \cdots & x_{1n} \\ x_{21} & x_{22} & \cdots & x_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ x_{m1} & x_{m2} & \cdots & x_{mn} \end{bmatrix} \end{split}\]
X_matrix = np.array(
    [[1, 2],
     [2, 3]])
print(X_matrix)
print(X_matrix.shape)
[[1 2]
 [2 3]]
(2, 2)

3D Tensoren

Arrays und Tensoren können als Verallgemeinerungen von Matrizen verstanden werden. Arrays können beliebige Werte beinhalten und Tensoren normalerweise nur Zahlenwerte.

Unter einem Array/Tensor vom Typ \(l \times m \times n\) versteht man

\[\begin{split} X = x_{ijk} = \begin{bmatrix} \begin{bmatrix} x_{11} & x_{12} & \cdots & x_{1n} \\ x_{21} & x_{22} & \cdots & x_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ x_{m1} & x_{m2} & \cdots & x_{mn} \end{bmatrix}_0 \\ \vdots\\ \begin{bmatrix} x_{11} & x_{12} & \cdots & x_{1n} \\ x_{21} & x_{22} & \cdots & x_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ x_{m1} & x_{m2} & \cdots & x_{mn} \end{bmatrix}_l \end{bmatrix} \end{split}\]

Mehrdimensionale Arrays lassen sich nur eingeschränkt in einer 2D Form darstellen.

X_tensor = np.array(
    [[[11, 12],
      [13, 14]],
     [[21, 22],
      [23, 24]],
     [[31, 32],
      [33, 34]]])

print(X_tensor)
print(X_tensor.shape)
[[[11 12]
  [13 14]]

 [[21 22]
  [23 24]]

 [[31 32]
  [33 34]]]
(3, 2, 2)

Die neue Dimension wird einer Matrix vorangestellt.

print(X_tensor[0,0,0])
print(X_tensor[1,0,0])
print(X_tensor[2,0,0])
11
21
31

Konstanten

numpy beinhaltet einige vordefinierte Konstanten. Eine vollständige Liste kann unter https://numpy.org/doc/stable/reference/constants.html gefunden werden.

Wichtig für viele Felder ist die Kreiszahl \(\pi\).

np.pi
3.141592653589793

Auch die Eulerkonstante \(e\) ist vorhanden.

np.e
2.718281828459045

Unendlichkeit.

np.inf
inf

Wert ist keine Nummer (nan = not a number).

np.nan
nan

Negative Null.

np.NZERO
-0.0

Positive Null.

np.PZERO
0.0

Arrays

numpy bringt moderne N-Arrays und Matrizen in die Programmiersprache Python. Zwar besitzt Python mit list N-Dimensionale Listen, aber diesen sind für mathematischen Operation nicht gut geeignet.

l = [[1., 2.], [3., 4.]]
l
[[1.0, 2.0], [3.0, 4.0]]
type(l)
list
A = np.array(l)
A
array([[1., 2.],
       [3., 4.]])
type(A)
numpy.ndarray

Numpy Arrays - Typen

Numpy Arrays können aus verschiedenen Typen bestehen welche unter https://numpy.org/devdocs/user/basics.types.html aufgelistet sind.

Wir wollen hier nur ein paar wichtige Datentypen kennen lernen.

v = np.zeros(3)
print(v)
print(type(v))
print(type(v[0]))
[0. 0. 0.]
<class 'numpy.ndarray'>
<class 'numpy.float64'>
v = np.zeros(3,dtype=np.float32)
print(v)
print(type(v))
print(type(v[0]))
[0. 0. 0.]
<class 'numpy.ndarray'>
<class 'numpy.float32'>
v = np.zeros(3,dtype=int)
print(v)
print(type(v))
print(type(v[0]))
[0 0 0]
<class 'numpy.ndarray'>
<class 'numpy.int64'>
v = np.zeros(3,dtype=complex)
print(v)
print(type(v))
print(type(v[0]))
[0.+0.j 0.+0.j 0.+0.j]
<class 'numpy.ndarray'>
<class 'numpy.complex128'>
v = np.zeros(3,dtype=bool)
print(v)
print(type(v))
print(type(v[0]))
[False False False]
<class 'numpy.ndarray'>
<class 'numpy.bool_'>

Arrays - Anlegen

Vektoren - Anlegen

np.zeros(2)
array([0., 0.])
np.ones(2)
array([1., 1.])
np.array([0., 1.])
array([0., 1.])
np.array((0., 1.))
array([0., 1.])
np.arange(2)
array([0, 1])
np.empty(2)
array([4.63848754e-310, 0.00000000e+000])
np.linspace(0, 5, num=3)
array([0. , 2.5, 5. ])

Wenn wir gezielt einen Zeilenvektor der Form \(\mathbb{R}^{1 \times n}\) anlegen wollen können wir das mit dem Hinzufügen von [] um den Vektor. Es handelt sich hier aber eigentlich schon um eine Matrix.

np.array([[1,2,3]])
array([[1, 2, 3]])

Oder wir verwenden den Befehl np.expand_dims.

np.expand_dims(np.array([1,2,3]), axis=0)
array([[1, 2, 3]])

Wenn wir gezielt einen Spaltenvektor der Form \(\mathbb{R}^{n\times1}\) anlegen wollen können wir das mit dem Hinzufügen von [] um jeden Vektorwert. Es handelt sich hier aber eigentlich schon um eine Matrix.

np.array([[1],[2],[3]])
array([[1],
       [2],
       [3]])

Oder wir verwenden den Befehl np.expand_dims.

np.expand_dims(np.array([1,2,3]), axis=1)
array([[1],
       [2],
       [3]])

Matrizen - Anlegen

np.empty((3,3))
array([[4.63848756e-310, 0.00000000e+000, 8.48798317e-313],
       [2.56761491e-312, 2.14321575e-312, 2.50395503e-312],
       [8.70018274e-313, 4.63848623e-310, 3.95252517e-322]])
np.zeros((3,2))
array([[0., 0.],
       [0., 0.],
       [0., 0.]])
np.ones((3,2))
array([[1., 1.],
       [1., 1.],
       [1., 1.]])
np.eye(3)
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
np.eye(3, k=1)
array([[0., 1., 0.],
       [0., 0., 1.],
       [0., 0., 0.]])
np.eye(3, k=-1)
array([[0., 0., 0.],
       [1., 0., 0.],
       [0., 1., 0.]])
np.diag([1,2,3])
array([[1, 0, 0],
       [0, 2, 0],
       [0, 0, 3]])
np.diag([1,2,3], k=1)
array([[0, 1, 0, 0],
       [0, 0, 2, 0],
       [0, 0, 0, 3],
       [0, 0, 0, 0]])

Array - Dimension

x = np.array([0.,.1])
x.shape
(2,)
x = np.array([[0., .1]])
x.shape
(1, 2)
x = np.array([[0.],[.1]])
x.shape
(2, 1)
x = np.array((1, 2, 3, 4))
x.shape
(4,)
x.shape = (2,2)
x
array([[1, 2],
       [3, 4]])

Array - Indexierung

Der Zugriff auf Element eines Arrays wird als Indexierung bezeichnet.

z = np.linspace(1, 2, 5)
z
array([1.  , 1.25, 1.5 , 1.75, 2.  ])
len(z)
5

Numpy Arrays beginnen mit dem Index \(0\) und enden mit Index \(N-1\).

z[0]
1.0
z[4]
2.0

Wollen wir den Datentyp behalten können wir : benutzen.

z[:1]
array([1.])
z[4:]
array([2.])

Wir können auch in die umgekehrte Richtung gehen. Dazu benutzen wir einen negativen Index. Der Index beginnt hier mit \(-1\) und endet mit \(-N\).

z[-1]
2.0
z[-5]
1.0

Wollen wir den Datentyp behalten können wir : benutzen.

z[-1:]
array([2.])
z[:-4]
array([1.])

Wir können auch mehrere Elemente selektieren, indem wir \([start]:[end]\) benutzen. Dabei werden nur die Elemente bis \(end-1\) selektiert.

z[0:3] # select elements with the index 0,1,2
array([1.  , 1.25, 1.5 ])
z[1:5] # select elements with the index 1,2,3,4
array([1.25, 1.5 , 1.75, 2.  ])

Starten wir mit dem Index \(0\) oder Enden wir mit dem Index \(end\) können wir diese auch weglassen.

z[:3] # select elements with the index 0,1,2
array([1.  , 1.25, 1.5 ])
z[1:] # select elements with the index 1,2,3,4
array([1.25, 1.5 , 1.75, 2.  ])

Auch ein gezieltes zugreifen auf mehrere Elemente ist möglich.

idx = [0,2,4] # idx as list
z[idx]
array([1. , 1.5, 2. ])
idx = np.array([0,2,4]) # idx as numpy array
z[idx]
array([1. , 1.5, 2. ])

Die Indexierung mit einem gleichgroßen Booleanarray als Maske kann sehr hilfreich sein.

idx_bool = np.array([1, 0, 0, 0, 1],  dtype=bool)
print(idx_bool)
z[idx_bool]
[ True False False False  True]
array([1., 2.])
zz = np.zeros(3)
print(zz)
zz[:] = 21
print(zz)
[0. 0. 0.]
[21. 21. 21.]

Array - Operationen

x = np.array((5, 4, 3, 2, 1))
x
array([5, 4, 3, 2, 1])
x.sort()
x
array([1, 2, 3, 4, 5])
x.sum()
15
x.mean()
3.0
x.max()
5
x.argmax()
4
x.cumsum()
array([ 1,  3,  6, 10, 15])
x.cumprod()
array([  1,   2,   6,  24, 120])
x.var()
2.0
x.std()
1.4142135623730951

Elementweise Operationen

In NumPy sind im Gegensatz von z.B “Matlab” alle Operationen elementweise Operationen.

Die Operatoren +,,,/ und ∗∗ wirken also alle elementweise auf das Array.

x = np.array([1, 2, 3, 4])
y = np.array([5, 6, 7, 8])
x+10
array([11, 12, 13, 14])
x-10
array([-9, -8, -7, -6])
x*10
array([10, 20, 30, 40])
x/10
array([0.1, 0.2, 0.3, 0.4])
x**10
array([      1,    1024,   59049, 1048576])
x+y
array([ 6,  8, 10, 12])
x-y
array([-4, -4, -4, -4])
x*y
array([ 5, 12, 21, 32])
x/y
array([0.2       , 0.33333333, 0.42857143, 0.5       ])
x**y
array([    1,    64,  2187, 65536])

Array Muliplikationen

Für Array Multiplikationen stehen verschieden Befehle zur Verfügung. Diese Multiplikation kann np.dot durchgeführt werden. Seit Python 3.5 steht auch noch der @ Operator zur Verfügung.

Warnung

Wichtige zu verstehen sind die Dimensionen der Objekte und wie sie das Ergebnis beeinflussen.

Vektor Multiplikationen

Wir starten mit den mathematischen Vektoren \(x \in \mathbb{R}^{n}\) und \(y \in \mathbb{R}^{n}\).

x = np.array([1, 2])
y = np.array([10, 20])
print(x.shape)
print(y.shape)
(2,)
(2,)

Das Skalarprodukt oder inneres Produkt kann mit np.dot oder @ berechnet werden.

\[ \left\langle x,y \right\rangle = \sum_{i=1}^n x_i y_i = x_1x_2+x_2y_2+\dots+x_ny_n \]

Das Resultat reduziert sich auf einen Skalar.

np.dot(x,y)
50
x.dot(y)
50
x@y
50

Warnung

Fast immer kann der @ Operator np.dot ersetzen. Jedoch erlaubt np.dot eine Multiplikation mit einem Skalar-Datentyp, der @ Operator jedoch nicht. Dieser Unterschied kann zu Fehlern führen, wenn er nicht bedacht wird.

np.dot(x,1) # x @ 1 liefert dagegen einen Fehler
array([1, 2])
np.dot(1,x) # 1 @ x liefert dagegen einen Fehler
array([1, 2])

Wir starten mit technischen Spaltenvektoren (Matrizen) \(x \in \mathbb{R}^{n \times 1}\) und \(y \in \mathbb{R}^{n \times 1}\).

x2 = np.array([[1, 2]]).T
y2 = np.array([[10, 20]]).T
print(x2.shape)
print(y2.shape)
(2, 1)
(2, 1)

Das Skalarprodukt oder inneres Produkt kann mit np.dot oder @ berechnet werden. Es handelt sich hier aber eigentlich um ein Matrizenprodukt und die Dimensionen müssen nun übereinstimmen. Im reellen Fall gilt

\[ \left\langle x,y \right\rangle = x^Ty = y^Tx \]

Das Resultat ist zwar auch hier notwendigerweise ein Skalar, aber die Dimension des Ergebnisses ist mit (1, 1) geben.

np.dot(x2.T,y2)
array([[50]])
x2.T.dot(y2)
array([[50]])
x2.T@y2
array([[50]])
(x2.T@y2).shape
(1, 1)

Matrix Multiplikationen

Kopieren von Arrays

Tiefes Kopieren spielt auch bei numpy-Arrays eine wichtige Rolle.

Referenzen

Wir wollen einen Vektor anlegen.

x = np.array([1, 2, 3])
x
array([1, 2, 3])

Durch eine Zuweisung entsteht eine neue Referenz.

y=x
y
array([1, 2, 3])

Durch eine Zuweisung entsteht eine neue Referenz, jedoch kein neuer Speicher, was sich durch id schnell überprüfen lässt.

print(id(x))
print(id(y))
140590591083600
140590591083600

Wird nun ein Element des neuen Vektors verändert, verändert sich auch der ursprüngliche Vektor.

y[0] = 0

print(x)
print(y)
[0 2 3]
[0 2 3]

Tiefes Kopieren

Eine echte Kopie eines Arrays erhalten wir durch die Verwendung der Methode np.copy().

x = np.array([1, 2, 3])
x
array([1, 2, 3])
y = np.copy(x)
x
array([1, 2, 3])

Durch copy ist ein neuer Speicherplatz geschaffen worden.

print(id(x))
print(id(y))
140590591028560
140590591085904

Wird nun ein Element des neuen Vektors verändert, verändert sich der ursprüngliche Vektor nicht.

y[0] = 0

print(x)
print(y)
[1 2 3]
[0 2 3]

Weitere Funktionalität

Vektorisierte Funktionen

x = np.array([1, 2, 3])
n = len(x)
y = np.empty(n)
for i in range(n):
    y[i] = np.sin(x[i])
    
y
array([0.84147098, 0.90929743, 0.14112001])
np.sin(x)
array([0.84147098, 0.90929743, 0.14112001])
np.cos(x)
array([ 0.54030231, -0.41614684, -0.9899925 ])
np.sqrt(x)
array([1.        , 1.41421356, 1.73205081])

Ebenen (Vektorisieren)

Vektorisieren

Die Methode flatten() kann für viele Algorithmen sehr sinnvoll sein. Am Einfachsten zu verstehen ist es am Beispiel der Matrix

\[\begin{split} A = \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \in \mathbb{R}^{2\times2} \end{split}\]

welche durch die Methode flatten in einen Vektor

\[ v = \begin{pmatrix} 1 & 2 & 3 & 4 \end{pmatrix} \in \mathbb{R}^{4} \]

umgeformt werden kann.

a = np.array([[1, 2],
              [3, 4]])
print(a)
print(a.shape)
[[1 2]
 [3 4]]
(2, 2)

Besonders wichtig ist hier, dass die Matrix eine Dimension verliert. Also aus \(\mathbb{R}^{2\times2}\) wird also wirklich ein mathematischer Vektor \(\mathbb{R}^{4}\).

f = a.flatten()
print(f)
print(f.shape)
[1 2 3 4]
(4,)

Wird ein Zeilenvektor der Form \(\mathbb{R}^{1\times4}\) benötigt, kann das mit der Methode expand_dims erreicht werden.

f = np.expand_dims(a.flatten(), axis=0)
print(f)
print(f.shape)
[[1 2 3 4]]
(1, 4)

Wird ein Spaltenvektor der Form \(\mathbb{R}^{1\times4}\) benötigt, kann das mit der Methode expand_dims erreicht werden.

f = np.expand_dims(a.flatten(), axis=1)
print(f)
print(f.shape)
[[1]
 [2]
 [3]
 [4]]
(4, 1)

Tipp

Zeilen und Spaltenvektoren sind eigentlich zwei dimensionale Matrizen, wo ein der beiden Achsen die Dimension 1 besitzt. Mathematische Vektoren besitzen nur eine Dimension.

Wir wollen nun ein Beispiel besprechen, wo die Methode flatten angewendet werden kann.

Gegeben sei ein quadratische Lyapunov Funktion

\[ V(x) = x^TPx. \]

Diese kann nun mit \(V=vec(xx^T)vec(P)\) in die lineare Parameterform \(V=\phi(x)^Tp\) umgeschrieben werden. Für vec können wir flatten nutzen.

P = np.array([[2,1],[1,2]])
P
array([[2, 1],
       [1, 2]])
x = np.array([[5,10]]).T
V1 = x.T@P@x
V1
array([[350]])
p = np.expand_dims(P.flatten(), axis=1) # row vector
phi = np.expand_dims((x@x.T).flatten(), axis=0) # column vector
V2 = phi@p
V2
array([[350]])

Stapeln

Das Stapeln von Vektoren, Matrizen und Tensoren ist für viele Algorithmen sehr hilfreich. Dazu stehen einige Numpy-Methoden wie stack, vstack, hstack, concatente, append zur Verfügung.

Vektoren

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(a.shape)
print(b.shape)
(3,)
(3,)

Als erstes wollen wir aus den 2 Vektoren a und b einen einzelnen Vektor machen.

np.append(a,b)
array([1, 2, 3, 4, 5, 6])
np.concatenate((a, b), axis=0)
array([1, 2, 3, 4, 5, 6])
np.hstack((a, b))
array([1, 2, 3, 4, 5, 6])

Nun wollen wir aus den 2 Vektoren a und b eine Matrix erstellen. Die Vektoren werden als Zeilenvektoren aufgefasst.

np.stack((a, b))
array([[1, 2, 3],
       [4, 5, 6]])
np.vstack((a, b))
array([[1, 2, 3],
       [4, 5, 6]])
np.row_stack((a,b))
array([[1, 2, 3],
       [4, 5, 6]])

Die Vektoren werden als Spaltenvektoren aufgefasst.

np.stack((a, b), axis=1)
array([[1, 4],
       [2, 5],
       [3, 6]])
np.column_stack((a, b))
array([[1, 4],
       [2, 5],
       [3, 6]])

Matrizen

a = np.array([[1, 2],
              [3, 4]])

b = np.array([[5, 6],
             [7, 8]])
np.vstack((a, b))
array([[1, 2],
       [3, 4],
       [5, 6],
       [7, 8]])
np.concatenate((a, b), axis=0)
array([[1, 2],
       [3, 4],
       [5, 6],
       [7, 8]])
np.hstack((a, b))
array([[1, 2, 5, 6],
       [3, 4, 7, 8]])
np.concatenate((a, b), axis=1)
array([[1, 2, 5, 6],
       [3, 4, 7, 8]])

Aufteilen

Arrays können mit den Methoden split und vsplit einfach aufgeteilt werden.

a = np.array([[1, 3, 5, 7, 9, 11], 
              [2, 4, 6, 8, 10, 12]])
a.shape
(2, 6)

Mit hsplit kann dieses Array in n Arrays aufgespaltet werden.

np.hsplit(a, 2)
[array([[1, 3, 5],
        [2, 4, 6]]),
 array([[ 7,  9, 11],
        [ 8, 10, 12]])]
a1, a2 = np.hsplit(a, 2)
print(a1.shape)
print(a2.shape)
(2, 3)
(2, 3)

Mit vsplit kann dieses Array in n Arrays aufgespaltet werden.

np.vsplit(a, 2)
[array([[ 1,  3,  5,  7,  9, 11]]), array([[ 2,  4,  6,  8, 10, 12]])]
a1, a2 = np.vsplit(a, 2)
print(a1.shape)
print(a2.shape)
(1, 6)
(1, 6)

Fazit

Wir haben hier wichtige Methoden der numpy Bibliothek kennen gelernt. numpy ist mit Sicherheit jene Python-Bibliothek welche am meisten studiert werden sollte. Kaum ein Feld kommt ohne diese Bibliothek aus.