Lineare Algebra#
The Matrix is everywhere. It is all around us. Even now in this very room
— Morpheus (The Matrix)
Folien#
Erstellung von Arrays für Vektor und Matrixberechnungen#
NumPy ist die führende Bibliothek in Python für Vektor- und Matrixberechnungen. Daten und Berechnungen werden dabei nicht in Python, sondern mit optimierten C-Objekten und Operationen ausgeführt. Dadurch ist es besonders speichereffizient und schnell in der Verarbeitung großer Datensätze.
Die Bibliothek NumPy wird normalerweise in Python mit der Abkürzung np
importiert. Dadurch kann man einfach das kurze np
im Code schreiben, wenn man die Bibliothek referenziert. Das auch Standard in den meisten Dokumentationen online.
# Import von NumPy
import numpy as np
Die wichtigste Datenstruktur in NumPy sind \(n\)-dimensionale Arrays, die sowohl zur Darstellung von Vektoren (\(n=1\)) und Matrizen (\(n=2\)) verwendet werden. Die Arrays können auch höhere Dimensionen haben. Diese Tensoren sind insbesondere für komplexe ML-Modelle wie Tiefe Neuronale Netzwerke wichtig.
Arrays in NumPy werden mit np.array([1, 2, 3, 4, 5])
erstellt. Wir übergeben dabei eine Liste [1, 2, 3, 4, 5]
der Zahlen.
# Erstellen eines einfachen Vektors als 1D-Arrays (mit ganzen Zahlen)
arr_1d = np.array([1, 2, 3, 4, 5])
print("3x1 Vektor:")
print(arr_1d)
3x1 Vektor:
[1 2 3 4 5]
Dabei wird die Liste in ein NumPy ndarray
umgewandelt. Dies sehen wir, wenn wir den Datentyp prüfen.
print("Datentyp des Vektors in Python: ", type(arr_1d))
Datentyp des Vektors in Python: <class 'numpy.ndarray'>
Intern nutzt NumPy keine Python-Datentypen (int
in dem Beispiel), sondern C-Datentypen, die effizienter in der Speichernutzung sind und vor allem deutlich schnellere numerische Berechnungen erlauben. Den Datentyp kann man mit dem dtype
Attribut einsehen:
print("Datentyp der Werte im Vektor in NumPy: ", arr_1d.dtype)
Datentyp der Werte im Vektor in NumPy: int64
Den Datentypen kann man beim Erstellen festlegen. Das ist insbesondere sinnvoll, wenn NumPy den falschen Datentyp erkennt.
# Erstellen eines einfachen Vektors als 1D-Arrays mit Reellen Zahl
arr_1d_f = np.array([6, 7, 8, 9, 10], dtype=np.float32)
print("3x1 Vektor:")
print(arr_1d_f)
print("Datentyp der Werte im Vektor in NumPy: ", arr_1d_f.dtype)
3x1 Vektor:
[ 6. 7. 8. 9. 10.]
Datentyp der Werte im Vektor in NumPy: float32
NumPy unterstützt hierbei viele unterschiedliche Datentypen, die in der folgenden Tabelle dargestellt sind.
Klasse |
Datentyp |
Python |
NumPy |
Beispiel |
---|---|---|---|---|
Numerisch |
Ganze Zahl |
|
|
|
Natürliche Zahl |
|
|
|
|
Reelle Zahl |
|
|
|
|
Komplexe Zahl |
|
|
|
|
Logisch |
Boolean |
|
|
|
Textuell |
Textuell |
|
|
|
Temporal |
Datum und Zeit |
|
|
|
Zeitdifferenz |
|
|
|
|
Komplex |
Python Objekt |
|
|
|
\(^*\) in separaten Packages verfügbar
Die Gestalt des Arrays können wir mit dem shape
Attribut abfragen:
print("Gestalt des Arrays:", arr_1d.shape)
Gestalt des Arrays: (5,)
Wenn wir eine Matrix (zweidimensionales Array) erstellen wollen, können wir an np.array
eine Liste von Listen übergeben, mit je einer Unterliste für jede Zeile der Matrix. Wichtig ist, dass jede Unterliste gleich groß ist:
# Erstellen einer Matrix als 2D-Arrays
arr_2d = np.array([[1, 2, 3], [4, 5, 6]])
print("3x2 Matrix:")
print(arr_2d)
3x2 Matrix:
[[1 2 3]
[4 5 6]]
Die Gestalt der Matrix hat sich entsprechend geändert. In unserem Beispiel ist die Shape gleich (2, 3)
. Das bedeutet, das wir zwei Zeilen und drei Spalten haben.
print("Shape des Arrays:", arr_2d.shape)
Shape des Arrays: (2, 3)
Zur Erzeugung von Standart-Vektoren oder -Matrizen gibt es spezielle Funktionen in NumPy. Um eine Einsmatrix zu erzeugen, nutzt man:
# Erstellen eines 3x3 Einsmatrix
ones_arr = np.ones((3,3))
print("3x3 Einsmatrix:")
print(ones_arr)
3x3 Einsmatrix:
[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
Nicht zu verwechseln mit der Einheitsmatrix, die zu erstellen ist, mit:
# Erstellen eines 3x3 Einheitsmatrix
identiy_arr = np.identity(3)
print("3x3 Einheitsmatrix:")
print(identiy_arr)
3x3 Einheitsmatrix:
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
Genauso lassen sich 0-Matrizen erzeugen, die man oft zur Initialisierung benötigt.
# Erstellen eines 3x3 0-Matrix
zeros_arr = np.zeros((3, 3))
print("3x3 Nullmatrix:")
print(zeros_arr)
3x3 Nullmatrix:
[[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
Zufallszahlen lassen sich mit der Unterbibliothek random
erzeugen. Um eine Zufallsvektor mit gleichverteilten Werten zu erzeugen kann man z.B. die np.random.random()
Funktion nutzen.
# Erstellen eines 3 Zufallsarrays mit gleichverteilten Werten zwischen 0 und 1
rand_arr = np.random.random((3,3))
print("3x3 Zufallsmatrix:")
print(rand_arr)
3x3 Zufallsmatrix:
[[0.304908 0.60697869 0.5516664 ]
[0.08656956 0.9779218 0.47517128]
[0.42421294 0.02211659 0.29950288]]
Für die Normalverteilung gibt es die Funktion:
# Erstellen eines 3 Zufallsarrays mit normalverteilten Werten mit dem Mittelwert 3 und der Standardabweichung 2
rand_arr_norm = np.random.normal(3, 2, size=(3,3))
print("3x3 Zufallsmatrix:")
print(rand_arr_norm)
3x3 Zufallsmatrix:
[[ 3.47618066 -0.69188299 4.48166791]
[ 1.57317047 0.4185061 2.42730269]
[ 0.83127741 0.9394984 5.1247307 ]]
Zugriff und Slicing#
Auf einzelne Elemente im Array kann über den numerischen Index zuzugreifen. Zu beachten ist das der Index bei 0 anfängt zu zählen.
# Zugriff auf Elemente in einem ndarray
print("Element in der zweiten Zeile und dritten Spalte des 2D-Arrays:", arr_2d[1, 2])
Element in der zweiten Zeile und dritten Spalte des 2D-Arrays: 6
Damit lassen sich auch Werte über den Index direkt zuweisen.
# Ändern von Elementen in einem ndarray
arr_2d[1, 2] = 99
print("Matrix nach Änderung:")
print(arr_2d)
Matrix nach Änderung:
[[ 1 2 3]
[ 4 5 99]]
Bei großen Matrizen möchte man diese oft zerlegen. NumPy hat einige komfortable Operationen dafür. Möchte man zum Beispiel auf die zweite Zeile zugreifen, nutzt man in der Spalte den :
-Operator
arr_2d[1, :]
array([ 4, 5, 99])
Hierbei werden auch die aus Python bekannten negative Indizes unterstützt, mit denen man auf die letzten Elemente zugreifen kann. Um auf die letzte Zeile zuzugreifen, kann man schreiben:
arr_2d[-1, :]
array([ 4, 5, 99])
Den Slices kann man auch Werte zuweisen. Um die letzte Zeile zu nullen, schreiben wir:
arr_2d[-1, :] = 99
print("Matrix nach Änderung:")
print(arr_2d)
Matrix nach Änderung:
[[ 1 2 3]
[99 99 99]]
Bei der Analyse von Daten ist es oft wichtig, Daten zu filtern. Wollen wir zum Beispiel alle Werte, die nicht 99
sind erhalten, können wir eine Logische Bedingung auf die gleiche Variable als Index verwenden
print("Werte kleiner als 99:")
arr_2d[arr_2d<99]
Werte kleiner als 99:
array([1, 2, 3])
Vektorisierte Funktionen und Lineare Algebra#
Es ist möglich, Schleifen zu verwenden, um Berechnungen mit Numpy-Objekten durchzuführen wie bei der Arbeit mit Listen in Python. Allerdings sollte man stattdessen immer vektorisierte Operationen von Numpy verwenden, wenn möglich. Diese sind deutlich performanter als Schleifen und meist auch zu programmieren und einfacher zu lesen.
Numpy bietet eine Vielzahl von vektorisierten Funktionen und Operatoren, die als universelle Funktionen bezeichnet werden. Zum Beispiel mathematische Operationen für die effiziente Rechnung mit Vektoren und Matrizen, was unteranderem für die lineare Algebra sehr sinnvoll ist.
So kann man die Vektoraddition und skalare Multiplikation einfach mit den vektoriellen Operatoren +
, -
und *
durchführen. Nehmen wir als Beispiel die Vektoren \(a, b, c\):
a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])
c = np.array([9, 10, 11, 12])
Wir können zum Beispiel \( a\) und \(b\) addieren durch:
a + b
array([ 6, 8, 10, 12])
oder eine skalare Multiplikation ausführen mit
2 * a
array([2, 4, 6, 8])
und das Quadrat aller Elemente berechnen durch
2**a
array([ 2, 4, 8, 16])
Wir können auch zeigen, dass \(a, b, c\) nicht linear unabhängig sind, indem wir zeigen, dass: \(2 b - c - a = 0\)
2*b - c - a
array([0, 0, 0, 0])
Ähnlich wie wir 1-D-Arrays als Vektoren verwendet haben, können wir 2-D-Arrays als Matrizen verwenden. Die Operationen +
, -
und *
funktionieren wie erwartet für zwei Arten von Operationen - elementweise Addition, Subtraktion und Multiplikation - sowie für die Addition, Subtraktion von Konstanten und Multiplikation mit einem Skalar. Hier sind ein paar Beispiele:
A = np.array([[1, 2], [3, 4]])
A + 10 # Addition einer Konstanten
array([[11, 12],
[13, 14]])
A * 10 # Skalare Multiplikation
array([[10, 20],
[30, 40]])
Es gibt noch ein paar andere praktische Möglichkeiten. np.diag
erzeugt entweder eine diagonale Matrix eines gegebenen Vektors oder gibt, falls eine Matrix gegeben ist, deren Diagonale zurück:
np.diag([1, 2, 3]) # Erzeugt eine Diagonal Matrix mit der angegebenen Diagonalen
array([[1, 0, 0],
[0, 2, 0],
[0, 0, 3]])
np.diag(A) # Gibt die Diagonale der Matrix zurück
array([1, 4])
Auch das Transponieren von Vektoren und Matrizen ist einfach mit .T
erreichbar
A.T # Transponierte Matrix A
array([[1, 3],
[2, 4]])
Skalarprodukt und Matrizenmultiplikation#
Das Skalarprodukt (Punktprodukt) zweier Vektoren kann mit dem @
-Symbol als Multiplikationszeichen durchgeführt werden, es gibt auch die Funktion np.dot()
und die Methode .dot()
.
Wir erstellen zum Beispiel Vektoren \(a=(1,2)\) und \(b=(11,12)^T\):
a = np.array([1, 2]) # Zeilenvektor
b = np.array([[11], [12]]) # Spaltenvektor
und berechnen
a @ b # Inneres (Matrix) Produkt
array([35])
Es ist wichtig zu wissen, dass das gewöhnliche Multiplikationszeichen *
kein Matrizenmultiplikation ist, sondern eine elementweise Multiplikation mit bestimmten Regeln zur Behandlung von Dimensionsunterschieden.
Da \(b\) transponiert ist ergibt sich zum Beispiel
a * b # elementweises Produkt (Übertragung, da die Abmessungen nicht übereinstimmen)
array([[11, 22],
[12, 24]])
Dies ist nicht das Matrixprodukt! Beachten Sie auch, dass wir hier keinen Fehler erhalten haben (obwohl das manchmal vorkommen kann), da die Berechnung immer noch gültig ist, nur nicht das, was wir hier erhalten wollen.
Genau wie bei Vektoren kann das Matrizenmultiplikation mit @
und nicht mit *
bei Matrizen durchgeführt werden.
a @ A
array([ 7, 10])
Auch ist zu beachten, dass die Matrizenmultiplikation nicht kommutativ ist, also die Reihenfolge der Matrizen bei der Produktbildung nicht vertauscht werden darf und zu anderen Ergebnissen führt.
A @ a
array([ 5, 11])
Gleichungssysteme mit linearer Algebra lösen#
Gleichungssysteme lassen sich numerisch einfach mit Matrizen und Vektoren lösen. Nehmen wir das folgende Beispiel eines Gleichungssystems mit drei Unbekannte und drei Gleichungen.
Wir können dieses Gleichungssystem auch als Matrizenprodukt darzustellen, in dem wir die bekannten Koeffizienten in der Matrix \(\mathbf{A}\) von den unbekannten Variablen im Vektor \(\mathbf{x}\) trennen und den bekannten Ergebnisvektor \(\mathbf{b}\) zuweisen.
Angewandt auf die obigen Gleichungen (1)-(3) erhalten wir die Matrixform:
Die Koeffizientenmatrix \(\mathbf{A}\) enthält alle Konstanten, die mit Ihren unbekannten Variablen \(x_1, x_2\) und \(x_3\) multipliziert werden. Der Ergebnisvektor \(\mathbf{y}\) enthält alle bekannten Konstanten, die nicht mit Ihren unbekannten Variablen \(x_1, x_2\) und \(x_3\) multipliziert werden. Schließlich enthält der Vektor \(\mathbf{x}=[x_1, x_2, x_3]\) die unbekannten Werte.
Dies können wir mit linearer Algebra lösen indem wir die obige Gleichung (4) mit der inversen Matrix \(\mathbf{A}^{-1}\) von links multiplizieren, um die Gleichung nach \(\mathbf{x}\) umzustellen:
Dieses Gleichungssystem ist immer dann lösbar, wenn eine Lösung für die inverse Matrix \(\mathbf{A}^{-1}\) existiert, was der Fall ist wenn deren Determinante \(\det \mathbf{A} \neq 0\) nicht null ist.
Wenn wir das gelernte auf Numpy ergibt sich folgender Lösungsweg. Zuerst definieren wir unsere Koeffizientenmatrix \(\mathbf{A}\) und den Ergebnisvektor \(\mathbf{b}\).
A = np.array([[5, 3, 0], [1, 2, 3], [1, 1, 1]])
b = np.array([1, 2, 3])
Wir prüfen die Determinante der Matrix. Hierfür nutzen wir die Funktion det()
aus der Bibliothek np.linalg
für Lineare Algebra in Numpy:
np.linalg.det(A) # Gibt die Determinante der Matrix zurück
1.0000000000000002
Die Determinante ist nicht 0, also können wir die Inverse der Matrix berechnen.
Dies geschieht mit der Funktion inv()
wie folgt:
A_inv = np.linalg.inv(A) # Berechnet die Inverse von A
Nicht alle Matrizen sind invertierbar, und wenn Sie versuchen, die Inverse einer Matrix mit Determinante 0 zu berechnen, wird die Funktion inv()
eine LinAlgError
-Ausnahme auslösen.
Letztendlich können wir den gesuchten \(\mathbf{x}\)-Vektor nach der obigen Gleichung (5) bestimmen
x = A_inv @ b
x
array([ 20., -33., 16.])
Gleichungssysteme formal lösen mit SymPy#
Bisher haben wir betrachtet, wie wir Gleichungssysteme numerisch lösen können. In manchen Situationen ist es jedoch notwendig eine formale Lösung für eine Berechnung algebraisch zu finden. Hier hilft das Python Paket SymPy.
Definieren wir formal unser Gleichungssystem aus Gleichung (3) als
from sympy import *
A = MatrixSymbol('A', 3, 3).as_explicit()
x = MatrixSymbol('x', 3, 1).as_explicit()
b = MatrixSymbol('b', 3, 1).as_explicit()
So erhalten wir keine numerischen, sondern formale Matrizen und Vektoren.
A
x
Hierfür können wir jetzt zum Beispiel algebraisch die Lösung der Determinante bestimmen
A.det()
oder die Inverse der Matrix
A.inv()
Auch können wir das Gleichungssystem komplett algebraisch lösen lassen.
A.solve(b)
Prinzipiell können wir die Gleichungen (1)-(3) auch direkt algebraisch lösen, ohne sie vorher in eine Matrizendarstellung zu bringen.
x1, x2, x3 = symbols("x1, x2, x3")
solve([
5 * x1 + 3 * x2 - 1,
x1 + 2 * x2 + 3 * x3 - 2,
x1 + x2 + x3 - 3
], [x1, x2, x3], dict=True)
Weitere Funktionen in SimPy erlauben das Vereinfachen von Formeln. Insbesondere wenn es um komplexe Brüche oder trigonometrische Funktionen geht, hat man nicht alle Ersetzungsmuster im Kopf und kann das von SimPy lösen lassen.
x = symbols("x")
simplify(sin(x)**2 + cos(x)**2)
simplify((x**3 + x**2 - x - 1)/(x**2 + 2*x + 1))
In gleicher Weise lassen sich vereinfachte Formen erweitern
expand((x + 1)**2)
Das funktioniert auch für Differentialgleichungen. Von einfachen Lösungen wie
diff(cos(x), x)
bis zu komplexen Ausdrücken, wie diese Differentialgleichung zweiter Ordnung
f = symbols("f", cls=Function)
diffeq = Eq(f(x).diff(x, x) - 2*f(x).diff(x) + f(x), sin(x))
diffeq
die wir mit der Funktion dsolve
formal lösen können
dsolve(diffeq)
als auch für bestimmte Randbedingungen für \(f(x)\) wie \(f(0) = 1\) und \(f(2) = 3\)
dsolve(diffeq, ics={f(0): 1, f(2): 3})
Das funktioniert auch für Differentialgleichungssysteme
f, g = symbols("f g", cls=Function)
x = symbols("x")
eqs = [Eq(f(x).diff(x), g(x)), Eq(g(x).diff(x), f(x))]
eqs
mit der formalen Lösung
dsolve(eqs, [f(x), g(x)])
und einer spezifischen Lösung mit gegebenen Randbedingungen \(f(0) = 1\) und \(g(2) = 3\)
dsolve(eqs, [f(x), g(x)], ics={f(0): 1, g(2): 3})