Inhalt des Skriptums seit dem Sommersemester 2025
- Startseite
- Allgemeine Informationen zur Lehrveranstaltung
- Einfaches Python Setup und Wichtige Informationen
- 01 Einführung in algorithmisches Denken & LLMs
- 02 Algorithmenanalyse und Komplexitätstheorie
- 03 Vektoren, Matrizen und Vektorisierung in Python
- 04 Differenzieren und Integrieren in Python: Analytische und Numerische Methoden
- 05 Arbeiten mit Daten: Vorbereitung, Visualisierung, Analyse
- 06 Graphalgorithmen: Darstellungen, Traversierungen, Kürzeste Wege und Spannbäume
Hier finden Sie die Kapitel aus den Vergangenen Jahren (legacy):
- 0. Python Einführung und Checkup
- 1. Einführung und einfache Algorithmen
- 2. Differenzieren und Integrieren
- 3. Vektoren, Matrizen und Vektorisierung in Python
- 4. Datenanalyse bzw. Datenauswertung
- 5. Grundlagen der Optimierung und Gradient Descent
- 6. Stochastische Optimierung und Genetische Algorithmen
- 7. Monte-Carlo-Methoden – Simulation und Integration
- 8. Monte-Carlo-Methoden, Teil 2 – Monte-Carlo-Integration, Teil 2 und Random Walk
- 9. Unsupervised Machine Learning: Clustering von Daten
- 10. Supervised Machine Learning: Grundlagen
- 11. Einführung in künstliche neuronale Netzwerke
Die Jupyter-Notebooks zur Lehrveranstaltung finden Sie im zugehörigen GitHub-Repository.
3 Vektoren, Matrizen und Vektorisierung in Python¶
In dieser Einheit beschäftigen wir uns mit einem der fundamentalen Werkzeuge der modernen Wissenschaft und Technik: der linearen Algebra. Die lineare Algebra bildet das mathematische Grundgerüst für zahlreiche Anwendungen in den Bereichen Datenanalyse, Machine Learning, Computergrafik, Physik und vielen weiteren Disziplinen. Python bietet uns mit der Bibliothek NumPy eine leistungsstarke Umgebung, um mit Vektoren und Matrizen effizient und direkt zu arbeiten.
Nach dem Studium dieser Einheit sollten Sie zumindest folgende drei der wichtigsten Punkte hier verstehen:
- Wie man Vektoren und Matrizen sowie deren Operationen in Python mit NumPy umsetzt
- Was Vektorisierung bedeutet und wie und warum wir dabei Schleifen durch effizientere vektorisierte Operationen ersetzen können
- Wie Arrays in der Bildbearbeitung ganz einfach zu Einsatz kommen
3.1 Grundlagen der linearen Algebra in Python mit NumPy¶
3.1.1 Vektoren: Definition und Implementierung als 1D-NumPy-Array¶
Ein Vektor ist in der linearen Algebra eine geordnete Liste von Zahlen. Geometrisch können wir uns einen Vektor als eine Pfeildarstellung im Raum vorstellen, die eine Richtung und eine Länge besitzt. In Python repräsentieren wir Vektoren am besten mithilfe von NumPy-Arrays. Entgegen einem ersten Instinkt sind nämlich Listen in Python nicht ideal, weil sich Listen-Elemente grundsätzlich im Datentyp unterscheiden können.
Das bedeutet, dass man Strings, Integers, Tupel, und sogar weitere Listen als Elemente in die gleiche Liste schreiben darf. Das widerspricht allerdings dem Grundkonzept eines Vektors, dessen Elemente bei bestimmten Transformationen des Vektors (z.B. einer Rotation im Raum) ineinander übergehen und miteinander vermischt werden, sodass ein gleicher Datentyp für alle Elemente vorzugeben ist. Genau diese Vorgabe erfüllen Arrays in NumPy. Insbesondere werden die Datentypen von verschiedenen Listen-Elementen bei der Konversion in ein NumPy-Array vereinheitlicht, was zu unerwarteten Artefakten führen kann.
NumPy (Numerical Python) ist eine der wichtigsten Bibliotheken für wissenschaftliches Rechnen in Python. Sie bietet effiziente Datenstrukturen für mehrdimensionale Arrays und eine Vielzahl von mathematischen Funktionen, um mit diesen zu arbeiten. Diese Funktionen und Strukturen sind dabei bereits hochoptimiert und dadurch grundsätzlich auch sehr schnell in der Ausführung.
Beginnen wir mit der Erstellung von Vektoren. Die Code snippets hier und im Folgenden kommen wieder von Claude 3.7 Sonnet:
import numpy as np
# Einen Vektor aus einer Liste erstellen
v1 = np.array([1, 2, 3])
# Einen Vektor mit Nullen erstellen
v2 = np.zeros(5) # [0. 0. 0. 0. 0.]
# Einen Vektor mit Einsen erstellen
v3 = np.ones(3) # [1. 1. 1.]
# Einen Vektor mit gleichmäßig verteilten Werten
v4 = np.linspace(0, 10, 5) # [0. 2.5 5. 7.5 10.]
# Einen Vektor mit zufälligen Werten
v5 = np.random.rand(4) # Zufällige Werte zwischen 0 und 1
print(f"v1: {v1}, Typ: {type(v1)}, Shape (Dimensionen): {v1.shape}")
v1: [1 2 3], Typ: <class 'numpy.ndarray'>, Shape (Dimensionen): (3,)
Wichtige Eigenschaften eines NumPy-Array-Vektors:
shape
: Gibt die Dimensionen des Arrays zurück, für einen Vektor ist das ein Tupel mit einem Element, z.B. (3,)dtype
: Gibt den Datentyp der Elemente an (z.B. int64, float64)size
: Gibt die Gesamtanzahl der Elemente zurück
3.1.2 Erstellen und Manipulieren von Vektoren: Addition, Skalarmultiplikation, Skalarprodukt¶
Mit NumPy können wir verschiedene Vektoroperationen einfach und effizient durchführen:
Vektoraddition: Die Addition zweier Vektoren erfolgt elementweise.
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
c = a + b # [5, 7, 9]
Skalarmultiplikation: Ein Vektor kann mit einem Skalar (einer einzelnen Zahl) multipliziert werden.
a = np.array([1, 2, 3])
s = 2
b = s * a # [2, 4, 6]
Skalarprodukt (auch Punktprodukt oder Inneres Produkt genannt): Die Summe der Produkte der entsprechenden Komponenten.
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
dot_product = np.dot(a, b) # 1*4 + 2*5 + 3*6 = 32
# Alternative Schreibweise
dot_product = a.dot(b)
# Oder bei neueren NumPy-Versionen
dot_product = a @ b
Weitere wichtige Vektoroperationen:
v = np.array([1, 2, 3])
# Betrag (Länge) eines Vektors
magnitude = np.linalg.norm(v) # sqrt(1^2 + 2^2 + 3^2) = sqrt(14)
# Elementweise Multiplikation
product = a * b # [1*4, 2*5, 3*6] = [4, 10, 18]
# Elementweise Division
division = a / b # [1/4, 2/5, 3/6] = [0.25, 0.4, 0.5]
# Elementweise Potenzierung
squared = v**2 # [1, 4, 9]
Hier haben wier gerade eine sehr wichtige Eigenschaft von NumPy-Arrays gesehen: Die einfachen mathematischen Operationen aber auch viele andere Funktionen werden standardmäßig elementweise ausgeführt. Diese einfache Handhabung wird uns noch gute Dientse leisten und macht NumPy-Code grundsätzlich um einiges lesbarer als vergleichsweise denselben Programminhalt, wenn er mit Listen implementiert wird.
3.1.3 Matrizen: Definition und Implementierung als 2D-NumPy-Array¶
Eine Matrix ist in der linearen Algebra eine rechteckige Anordnung von Zahlen in Zeilen und Spalten. In Python werden Matrizen als 2D-NumPy-Arrays repräsentiert.
# Eine 2x3 Matrix erstellen (2 Zeilen, 3 Spalten)
A = np.array([[1, 2, 3],
[4, 5, 6]])
print(f"Matrix A:\n{A}")
print(f"Form: {A.shape}") # (2, 3)
# Eine Matrix mit Nullen erstellen
B = np.zeros((3, 4)) # 3x4 Matrix mit Nullen
# Eine Matrix mit Einsen erstellen
C = np.ones((2, 2)) # 2x2 Matrix mit Einsen
# Eine Einheitsmatrix erstellen
I = np.eye(3) # 3x3 Einheitsmatrix
# Eine Matrix mit zufälligen Werten
D = np.random.rand(2, 3) # 2x3 Matrix mit zufälligen Werten zwischen 0 und 1
3.1.4 Erstellen und Manipulieren von Matrizen: Addition, Multiplikation, Transposition¶
Matrixaddition: Wie bei Vektoren erfolgt die Addition zweier Matrizen elementweise. Die Matrizen müssen die gleiche Form haben.
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = A + B # [[6, 8], [10, 12]]
Matrixmultiplikation: Anders als bei der Addition werden bei der Multiplikation zweier Matrizen die Zeilen der ersten Matrix mit den Spalten der zweiten Matrix multipliziert und aufsummiert.
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# Matrixmultiplikation
C = np.dot(A, B)
# C = A @ B # Alternative Syntax in neueren Python-Versionen
print(f"A × B =\n{C}")
A × B = [[19 22] [43 50]]
Die Matrixmultiplikation ist nur definiert, wenn die Anzahl der Spalten in der ersten Matrix gleich der Anzahl der Zeilen in der zweiten Matrix ist. Das Ergebnis hat dann die Anzahl der Zeilen der ersten Matrix und die Anzahl der Spalten der zweiten Matrix.
Transposition: Bei der Transposition einer Matrix werden die Zeilen zu Spalten und umgekehrt.
A = np.array([[1, 2, 3], [4, 5, 6]])
A_transposed = A.T
print(f"A =\n{A}")
print(f"A^T =\n{A_transposed}")
A = [[1 2 3] [4 5 6]] A^T = [[1 4] [2 5] [3 6]]
Weitere wichtige Matrixoperationen:
A = np.array([[1, 2], [3, 4]])
# Determinante einer quadratischen Matrix
det_A = np.linalg.det(A) # 1*4 - 2*3 = -2
# Inverse einer Matrix (falls sie existiert)
A_inv = np.linalg.inv(A)
print(f"A^(-1) =\n{A_inv}")
# Überprüfen: A * A^(-1) = Einheitsmatrix
print(f"A × A^(-1) =\n{np.dot(A, A_inv)}")
# Matrixrang
rank = np.linalg.matrix_rank(A)
# Eigenwerte und Eigenvektoren (nur für quadratische Matrizen)
eigenvalues, eigenvectors = np.linalg.eig(A)
In den nächsten Abschnitten werden wir sehen, wie diese grundlegenden Operationen in Python vektorisiert werden können, um code effizienter und lesbarer zu gestalten, sowie praktische Anwendungen dieser Konzepte kennenlernen.
3.2 Vektorisierung in Python¶
Nachdem wir die grundlegenden Konzepte der linearen Algebra und deren Implementierung in NumPy kennengelernt haben, wenden wir uns nun kurz einem wichtigen Konzept für effizientes wissenschaftliches Rechnen in Python zu: der Vektorisierung.
3.2.1 Das Konzept der Vektorisierung¶
Vektorisierung bezeichnet die Umwandlung von iterativen Operationen (wie Schleifen) in äquivalente Operationen auf ganzen Arrays. Statt Elemente einzeln nacheinander zu verarbeiten, werden bei der Vektorisierung Operationen auf ganze Datenblöcke gleichzeitig angewendet.
Lassen Sie uns dies an einem einfachen Beispiel verdeutlichen. Angenommen, wir möchten jeden Wert in einem Array quadrieren:
Iterativer Ansatz (mit Schleife):
import numpy as np
import time
# Erstelle ein großes Array
n = 1000000
arr = np.random.rand(n)
# Iterativer Ansatz mit einer Schleife
def square_loop(arr):
result = np.zeros_like(arr)
for i in range(len(arr)):
result[i] = arr[i] ** 2
return result
# Zeitmessung
start_time = time.time()
result_loop = square_loop(arr)
loop_time = time.time() - start_time
print(f"Zeit mit Schleife: {loop_time:.6f} Sekunden")
Zeit mit Schleife: 0.245403 Sekunden
Vektorisierter Ansatz:
# Vektorisierter Ansatz
def square_vectorized(arr):
return arr ** 2
# Zeitmessung
start_time = time.time()
result_vectorized = square_vectorized(arr)
vectorized_time = time.time() - start_time
print(f"Zeit mit Vektorisierung: {vectorized_time:.6f} Sekunden")
# Vergleich der Geschwindigkeit
speedup = loop_time / vectorized_time
print(f"Beschleunigung durch Vektorisierung: {speedup:.2f}x")
# Überprüfung der Ergebnisse
print(f"Ergebnisse identisch: {np.allclose(result_loop, result_vectorized)}")
Zeit mit Vektorisierung: 0.002263 Sekunden Beschleunigung durch Vektorisierung: 108.44x Ergebnisse identisch: True
Wie wir sehen können, führt die Vektorisierung zu einer erheblichen Beschleunigung. Der Grund dafür liegt in mehreren Faktoren:
- Optimierte C-Implementierungen: NumPy-Operationen sind in optimiertem C-Code implementiert.
- Parallelisierung: Viele vektorisierte Operationen können auf modernen Prozessoren parallelisiert werden.
- Weniger Python-Overhead: Python-Schleifen haben einen höheren Overhead als native C-Implementierungen.
- Speichereffizienz: Vektorisierte Operationen können effizienter auf den Speicher zugreifen.
3.2.2 Vorteile gegenüber Schleifen: Performance und Lesbarkeit¶
Neben der deutlich besseren Performance bieten vektorisierte Operationen auch Vorteile hinsichtlich der Lesbarkeit und Wartbarkeit des Codes. Das haben wir im Beispiel gerade eben bereits relativ deutlich gesehen. Listen wir hier die wichtigsten Vorteile explizit:
- Kürzerer und prägnanterer Code: Vektorisierte Operationen reduzieren die Codekomplexität.
- Ausdrucksstarke mathematische Notation: Die Syntax von NumPy-Operationen ähnelt der mathematischen Notation.
- Weniger Fehleranfälligkeit: Indexierungsfehler und Off-by-one-Fehler werden vermieden.
- Bessere Optimierbarkeit: NumPy kann interne Optimierungen durchführen, die in handgeschriebenen Schleifen nicht möglich wären.
Und nun betrachten wir ein weiteres etwas umfangreicheres Beispiel zur Vektorisierung: Die Berechnung des euklidischen Abstands zwischen zwei Punktmengen.
import numpy as np
import time
# Erstelle zwei Mengen von Punkten
n_points = 1000
dim = 3
points_a = np.random.rand(n_points, dim)
points_b = np.random.rand(n_points, dim)
# Iterativer Ansatz
def pairwise_distances_loop(a, b):
n = a.shape[0]
distances = np.zeros(n)
for i in range(n):
# Euklidischer Abstand zwischen a[i] und b[i]
distances[i] = np.sqrt(np.sum((a[i] - b[i])**2))
return distances
# Vektorisierter Ansatz
def pairwise_distances_vectorized(a, b):
return np.sqrt(np.sum((a - b)**2, axis=1))
# Zeitmessung
start_time = time.time()
dist_loop = pairwise_distances_loop(points_a, points_b)
loop_time = time.time() - start_time
print(f"Zeit mit Schleife: {loop_time:.6f} Sekunden")
start_time = time.time()
dist_vec = pairwise_distances_vectorized(points_a, points_b)
vec_time = time.time() - start_time
print(f"Zeit mit Vektorisierung: {vec_time:.6f} Sekunden")
speedup = loop_time / vec_time
print(f"Beschleunigung: {speedup:.2f}x")
Zeit mit Schleife: 0.005228 Sekunden Zeit mit Vektorisierung: 0.000091 Sekunden Beschleunigung: 57.55x
Der vektorisierte Code ist also nicht nur schneller, sondern auch viel kürzer und leichter zu verstehen, wenn man mit der NumPy-Syntax vertraut ist. Die einzige nicht-intuitive Sache in diesem konkreten Fall ist die Summierung über axis=1
, bei der über die räumlichen Dimensionen (das ist der Spaltenindex in den originalen Arrays, der Zeilenindex ist Index 0) der Differenzen summiert wird. Hier wird also sehr elegant genau das hingeschrieben, was zu tun ist.
3.2.3 Wann Vektorisierung sinnvoll ist und wann nicht¶
Obwohl Vektorisierung in vielen Fällen zu erheblichen Leistungsverbesserungen führt, gibt es Situationen, in denen sie möglicherweise nicht die beste Wahl ist:
Wann ist Vektorisierung besonders sinnvoll?
- Bei großen Datenmengen: Je größer die Datenmenge (nicht die Dimension der damit verbundenen Arrays, denn das kann zu Speicherproblemen führen), desto größer der Geschwindigkeitsvorteil
- Bei einfachen, gleichförmigen Operationen: Elementweise Operationen, Matrixmultiplikationen, statistische Berechnungen
- Wenn Speicherverbrauch kein Problem ist: Vektorisierte Operationen erstellen oft interne temporäre Arrays
Wann sollte man vorsichtig sein oder Alternativen in Betracht ziehen?
- Bei komplexen, bedingten Operationen: Wenn für jedes Element unterschiedliche Entscheidungen getroffen werden müssen. Das einleuchtendste einfach Beispiel hierfür ist ein loop, der bei der Erfüllung einer index-abhängigen if-Bedingung abgebrochen wird
- Bei sehr großen Datenmengen mit begrenztem Speicher: Vektorisierung kann zu höherem Speicherverbrauch führen, siehe oben
- Bei sequentiellen Abhängigkeiten: Wenn jede Berechnung vom Ergebnis der vorherigen abhängt
- Wenn Lesbarkeit in komplexen Situationen wichtiger ist als Performance: In manchen Fällen ist eine explizite Schleife ausnahmsweise verständlicher als die kompakte Notation mit Arrays
In solchen Fällen können Alternativen wie NumPy’s np.vectorize
, Cython, Numba oder spezielle Funktionen wie np.apply_along_axis
in Betracht gezogen werden.
Hier ein Beispiel für eine Situation, in der Vektorisierung schwierig ist:
# Eine Funktion mit komplexer Bedingungslogik
def complex_function(x):
if x < 0:
return x**2
elif x < 1:
return np.sin(x)
else:
return np.log(x)
# Anwenden auf ein Array
arr = np.array([-2, -1, 0, 0.5, 1, 2])
# Mit einer Schleife
result_loop = np.zeros_like(arr)
for i, val in enumerate(arr):
result_loop[i] = complex_function(val)
# Mit np.vectorize (eine "Pseudovektorisierung")
vectorized_func = np.vectorize(complex_function)
result_vectorized = vectorized_func(arr)
print("Mit Schleife:", result_loop)
print("Mit np.vectorize:", result_vectorized)
Hinweis: np.vectorize
bietet keine echte Vektorisierung im Sinne der Performanceoptimierung – es ist im Wesentlichen eine elegante Möglichkeit, eine Schleife zu schreiben. Für echte Performanceverbesserungen bei komplexen Funktionen sollten Bibliotheken wie Numba in Betracht gezogen werden.
Fazit zur Vektorisierung:
Vektorisierung ist ein mächtiges Konzept in Python, das die Geschwindigkeit numerischer Berechnungen erheblich verbessern und gleichzeitig zu eleganterem, lesbarerem Code führen kann. Als moderne Datenwissenschaftler und Programmierer sollten wir immer nach Möglichkeiten suchen, unseren Code zu vektorisieren, wenn es sinnvoll ist.
3.3 Beispiel-Anwendung von NumPy in der Linearen Algebra¶
Nach der Einführung in die Grundlagen der linearen Algebra und Vektorisierung in Python wenden wir uns nun zwei konkreten einfachen Anwendungen zu.
3.3.1 Numerisches Lösen linearer Gleichungssysteme¶
Ein lineares Gleichungssystem besteht aus einer Menge von linearen Gleichungen mit mehreren Unbekannten. In der Matrixschreibweise kann ein solches System als $A\mathbf{x} = \mathbf{b}$ dargestellt werden, wobei $A$ die Koeffizientenmatrix, $\mathbf{x}$ der Vektor der Unbekannten und $\mathbf{b}$ der Ergebnisvektor ist.
Beispiel eines linearen Gleichungssystems mit drei Unbekannten:
$$\begin{align*} 2x + 3y – z &= 5 \\ x – y + 2z &= -1 \\ 3x + y + z &= 2 \end{align*}$$
In Matrixform:
$$\begin{pmatrix} 2 & 3 & -1 \\ 1 & -1 & 2 \\ 3 & 1 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ z \end{pmatrix} = \begin{pmatrix} 5 \\ -1 \\ 2 \end{pmatrix}$$
Mit NumPy können wir dieses System auf verschiedene Weise lösen:
Methode 1: Direkte Lösung mit np.linalg.solve
¶
Die Funktion np.linalg.solve
ist für das Lösen linearer Gleichungssysteme optimiert und sollte bevorzugt verwendet werden, wenn die Matrix $A$ quadratisch und invertierbar ist.
import numpy as np
# Koeffizientenmatrix A
A = np.array([
[2, 3, -1],
[1, -1, 2],
[3, 1, 1]
])
# Rechte Seite b
b = np.array([5, -1, 2])
# Lösen des Systems Ax = b
x = np.linalg.solve(A, b)
print("Lösung des Systems:")
print(f"x = {x[0]:.4f}")
print(f"y = {x[1]:.4f}")
print(f"z = {x[2]:.4f}")
# Überprüfen der Lösung
tolerance = 1e-10
residual = np.linalg.norm(A @ x - b)
print(f"\nResiduum ||Ax - b|| = {residual:.10f}")
print(f"Lösung korrekt: {residual < tolerance}")
Lösung des Systems: x = -0.2000 y = 2.0000 z = 0.6000 Residuum ||Ax - b|| = 0.0000000000 Lösung korrekt: True
Methode 2: Lösung über die Inverse der Matrix¶
Eine alternative Methode ist, die Inverse von $A$ zu berechnen und mit $\mathbf{b}$ zu multiplizieren: $\mathbf{x} = A^{-1}\mathbf{b}$. Diese Methode ist jedoch numerisch weniger stabil und ineffizienter als np.linalg.solve
.
# Berechnen der Inversen von A
A_inv = np.linalg.inv(A)
# Lösung berechnen: x = A^(-1) * b
x_inv = A_inv @ b
print("\nLösung mit der Inversen:")
print(f"x = {x_inv[0]:.4f}")
print(f"y = {x_inv[1]:.4f}")
print(f"z = {x_inv[2]:.4f}")
Lösung mit der Inversen: x = -0.2000 y = 2.0000 z = 0.6000
3.3.2 Berechnung von Matrix-Eigensystemen (Eigenwerte und Eigenvektoren) mit NumPy¶
Eigenwerte und Eigenvektoren sind fundamentale Konzepte in der linearen Algebra mit zahlreichen Anwendungen in der Physik, Statistik und im maschinellen Lernen. Ein Eigenvektor einer quadratischen Matrix $A$ ist ein Vektor $\mathbf{v}$, der bei Multiplikation mit $A$ nur seine Länge, nicht aber seine Richtung ändert. Der Faktor, um den sich die Länge ändert, ist der Eigenwert $\lambda$:
$$A\mathbf{v} = \lambda\mathbf{v}$$
NumPy bietet effiziente Funktionen zur Berechnung von Eigenwerten und Eigenvektoren:
import numpy as np
import matplotlib.pyplot as plt
# Beispielmatrix
A = np.array([
[4, 2],
[1, 3]
])
print("Matrix A:")
print(A)
# Berechnung von Eigenwerten und Eigenvektoren
eigenvalues, eigenvectors = np.linalg.eig(A)
print("\nEigenwerte:")
for i, eigenvalue in enumerate(eigenvalues):
print(f"λ_{i+1} = {eigenvalue:.4f}")
print("\nEigenvektoren (spaltenweise):")
print(eigenvectors)
# Verifizierung der Eigenwert-Gleichung A·v = λ·v
for i in range(len(eigenvalues)):
eigval = eigenvalues[i]
eigvec = eigenvectors[:, i]
# Berechne A·v
Av = A @ eigvec
# Berechne λ·v
lambda_v = eigval * eigvec
print(f"\nÜberprüfung für Eigenwert λ_{i+1} = {eigval:.4f}:")
print(f"A·v_{i+1} = {Av}")
print(f"λ_{i+1}·v_{i+1} = {lambda_v}")
print(f"Differenz: {np.linalg.norm(Av - lambda_v):.10f}")
Matrix A: [[4 2] [1 3]] Eigenwerte: λ_1 = 5.0000 λ_2 = 2.0000 Eigenvektoren (spaltenweise): [[ 0.89442719 -0.70710678] [ 0.4472136 0.70710678]] Überprüfung für Eigenwert λ_1 = 5.0000: A·v_1 = [4.47213595 2.23606798] λ_1·v_1 = [4.47213595 2.23606798] Differenz: 0.0000000000 Überprüfung für Eigenwert λ_2 = 2.0000: A·v_2 = [-1.41421356 1.41421356] λ_2·v_2 = [-1.41421356 1.41421356] Differenz: 0.0000000000
Visualisierung der Eigenvektoren einer 2×2-Matrix¶
Eine anschauliche Weise, Eigenvektoren zu verstehen, ist die Visualisierung ihrer Wirkung im 2D-Raum. Wir können zeigen, wie eine Matrix $A$ verschiedene Vektoren transformiert, und insbesondere, wie Eigenvektoren ihre Richtung beibehalten.
# Visualisierung von Eigenvektoren im 2D-Raum
def plot_eigenvectors(A, eigvals, eigvecs):
plt.figure(figsize=(10, 10))
# Gitterpunkte erzeugen
x = np.linspace(-3, 3, 7)
y = np.linspace(-3, 3, 7)
X, Y = np.meshgrid(x, y)
grid_points = np.column_stack([X.flatten(), Y.flatten()])
# Transformierte Gitterpunkte berechnen
transformed_points = grid_points @ A.T
# Gitter vor und nach der Transformation zeichnen
plt.scatter(grid_points[:, 0], grid_points[:, 1], c='k', s=30, alpha=0.5, label='Originales Gitter')
plt.scatter(transformed_points[:, 0], transformed_points[:, 1], c='darkgray', s=30, alpha=0.5, label='Transformiertes Gitter')
# Eigenvektoren skalieren
scaled_eigvecs = eigvecs * 2
# Eigenvektoren zeichnen
colors = ['red', 'blue']
for i in range(len(eigvals)):
v = scaled_eigvecs[:, i]
plt.arrow(0, 0, v[0], v[1], head_width=0.2, head_length=0.3, fc=colors[i], ec=colors[i],
label=f'Eigenvektor zu λ={eigvals[i]:.2f}')
# Transformierte Eigenvektoren
transformed_v = A @ v
plt.arrow(0, 0, transformed_v[0], transformed_v[1], head_width=0.2, head_length=0.3,
fc=colors[i], ec=colors[i], linestyle='dashed', alpha=0.5)
plt.axhline(y=0, color='k', linestyle='-', alpha=0.3)
plt.axvline(x=0, color='k', linestyle='-', alpha=0.3)
plt.grid(alpha=0.3)
plt.xlim(-5, 5)
plt.ylim(-5, 5)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Visualisierung der Eigenvektoren und Transformation')
plt.legend()
plt.axis('equal')
plt.show()
# Visualisierung aufrufen
plot_eigenvectors(A, eigenvalues, eigenvectors)

- Eigenwerte und Eigenvektoren charakterisieren das Verhalten linearer Transformationen (solche werden durch Matrizen dargestellt)
- Eigenvektoren sind Vektoren, die bei der Multiplikation mit einer Matrix nur skaliert, aber nicht in ihrer Richtung verändert werden
- Der Skalierungsfaktor ist der zugehörige Eigenwert
- NumPy bietet mit
np.linalg.eig
eine effiziente Funktion zur Berechnung von Eigenwerten und Eigenvektoren - Eigenwerte und Eigenvektoren haben zahlreiche praktische Anwendungen in verschiedenen Bereichen der Wissenschaft und Technik
3.4 Praktische Anwendungen von NumPy-Arrays¶
Nachdem wir die Grundlagen der linearen Algebra, Vektorisierung und einige theoretische Anwendungen kennengelernt haben, wollen wir nun praktischere Anwendungen von NumPy-Arrays untersuchen. NumPy-Arrays sind nicht nur für mathematische Berechnungen nützlich, sondern finden Anwendung in verschiedenen Bereichen wie Bildverarbeitung, Graphentheorie, Signalverarbeitung und wissenschaftlichen Simulationen.
3.4.1 Bildverarbeitung: Transformationen und Filter¶
Bilder können in Python als mehrdimensionale NumPy-Arrays repräsentiert werden:
- Graustufenbilder: 2D-Arrays, wobei jeder Wert die Intensität eines Pixels darstellt
- Farbbilder (RGB): 3D-Arrays mit der Größe (Höhe, Breite, 3), wobei die dritte Dimension die Rot-, Grün- und Blau-Kanäle repräsentiert
Diese Array-Darstellung ermöglicht es uns, verschiedene Bildverarbeitungstechniken durch einfache NumPy-Operationen zu implementieren.
Grundlegende Bildoperationen¶
Laden und Anzeigen eines Bildes:
import numpy as np
import os
import matplotlib.pyplot as plt
from PIL import Image # Python Imaging Library (Pillow)
# Bild laden und in NumPy-Array konvertieren
image_path = os.path.join('data', 'pinky.jpg')
img = np.array(Image.open(image_path))
# Überprüfen der Form des Arrays
print(f"Bildform: {img.shape}") # z.B. (height, width, 3) für ein RGB-Bild
# Bild anzeigen
plt.figure(figsize=(8, 6))
plt.imshow(img)
plt.axis('off')
plt.title('Originalbild')
plt.show()
Bildform: (1512, 2016, 3)

Farbbild in Graustufen umwandeln:
# Methode 1: Durchschnitt der RGB-Kanäle
grayscale_avg = img.mean(axis=2).astype(np.uint8)
# Methode 2: Gewichteter Durchschnitt (der menschlichen Wahrnehmung näher)
grayscale_weighted = (0.299 * img[:,:,0] + 0.587 * img[:,:,1] + 0.114 * img[:,:,2]).astype(np.uint8)
# Anzeigen der Graustufenbilder
plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.imshow(grayscale_avg, cmap='gray')
plt.axis('off')
plt.title('Graustufenbild (Durchschnitt)')
plt.subplot(122)
plt.imshow(grayscale_weighted, cmap='gray')
plt.axis('off')
plt.title('Graustufenbild (Gewichtet)')
plt.tight_layout()
plt.show()

Farbkanäle extrahieren:
# Alle drei Farbkanäle nebeneinander
plt.figure(figsize=(10, 5))
# Definiere alle nötigen Colormaps in einer Liste
colormaps = ["Reds", "Greens", "Blues"]
# Loop über den index der drei Farbkanäle
for ind_color in range(3):
ax = plt.subplot(1, 3, ind_color+1)
ax.imshow(img[:,:,ind_color], cmap=colormaps[ind_color])
plt.show()

Bildtransformationen¶
Spiegeln und Drehen:
# Horizontal spiegeln
img_flipped_h = np.fliplr(img)
# Vertikal spiegeln
img_flipped_v = np.flipud(img)
# Um 90 Grad drehen
img_rotated = np.rot90(img)
# Anzeigen der transformierten Bilder
plt.figure(figsize=(15, 10))
plt.subplot(221)
plt.imshow(img)
plt.axis('off')
plt.title('Original')
plt.subplot(222)
plt.imshow(img_flipped_h)
plt.axis('off')
plt.title('Horizontal gespiegelt')
plt.subplot(223)
plt.imshow(img_flipped_v)
plt.axis('off')
plt.title('Vertikal gespiegelt')
plt.subplot(224)
plt.imshow(img_rotated)
plt.axis('off')
plt.title('Um 90° gedreht')
plt.tight_layout()
plt.show()

Skalieren und Zuschneiden:
# Zuschneiden
height, width = img.shape[:2]
top, left = height // 4, width // 4
bottom, right = 3 * height // 4, 3 * width // 4
img_cropped = img[top:bottom, left:right]
# Skalieren (einfache Methode mit NumPy)
new_height, new_width = height // 20, width // 20
img_rescaled = np.array(Image.fromarray(img).resize((new_width, new_height)))
# Anzeigen
plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.imshow(img_cropped)
plt.axis('off')
plt.title('Zugeschnittenes Bild')
plt.subplot(122)
plt.imshow(img_rescaled)
plt.axis('off')
plt.title('Skaliertes Bild')
plt.tight_layout()
plt.show()

Bildfilterung und Faltungsoperationen¶
Eine wichtige Operation in der Bildverarbeitung ist die Faltung (Convolution), bei der ein Filter (oder Kernel) über das Bild geschoben wird, um verschiedene Effekte zu erzielen.
Anwenden von Filtern mit NumPy:
def apply_kernel(image, kernel):
"""Wendet einen Faltungskernel auf ein Graustufenbild an."""
# Kernel-Dimensionen
k_height, k_width = kernel.shape
# Padding für die Bildränder
pad_height = k_height // 2
pad_width = k_width // 2
# Ausgabebild initialisieren
output = np.zeros_like(image)
# Faltung durchführen
for i in range(pad_height, image.shape[0] - pad_height):
for j in range(pad_width, image.shape[1] - pad_width):
# Region des Bildes unter dem Kernel
region = image[i-pad_height:i+pad_height+1, j-pad_width:j+pad_width+1]
# Faltung berechnen (elementweise Multiplikation und Summierung)
output[i, j] = np.sum(region * kernel)
return output
# Beispiel-Kernel
# 1. Weichzeichner (Blur)
blur_kernel = np.ones((3, 3)) / 9
# 2. Schärfen (Sharpen)
sharpen_kernel = np.array([
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]
])
# 3. Kantenerkennung (Edge Detection)
edge_kernel = np.array([
[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]
])
# Anwenden der Filter auf ein Graustufenbild
grayscale = grayscale_weighted
blurred = apply_kernel(grayscale, blur_kernel).clip(0, 255).astype(np.uint8)
sharpened = apply_kernel(grayscale, sharpen_kernel).clip(0, 255).astype(np.uint8)
edges = apply_kernel(grayscale, edge_kernel).clip(0, 255).astype(np.uint8)
# Zoom auf einen Ausschnitt zur Besseren Darstellung der Unterschiede
top, left = height // 3, width // 3
bottom, right = 2 * height // 3, 2 * width // 3
blurred_cropped = blurred[top:bottom, left:right]
sharpened_cropped = sharpened[top:bottom, left:right]
# Anzeigen der Ergebnisse
plt.figure(figsize=(15, 10))
plt.subplot(221)
plt.imshow(grayscale, cmap='gray')
plt.axis('off')
plt.title('Original (Graustufen)')
plt.subplot(222)
plt.imshow(blurred_cropped, cmap='gray')
plt.axis('off')
plt.title('Weichgezeichnet')
plt.subplot(223)
plt.imshow(sharpened_cropped, cmap='gray')
plt.axis('off')
plt.title('Geschärft')
plt.subplot(224)
plt.imshow(edges, cmap='gray')
plt.axis('off')
plt.title('Kantenerkennung')
plt.tight_layout()
plt.show()

Vektorisierte Version der Faltung:
Die oben gezeigte Implementierung der Faltung verwendet Schleifen, was in Python ineffizient ist. Hier ist eine vektorisierte Version mit scipy.signal.convolve2d
, zum Code-Vergleich:
from scipy import signal
# Vektorisierte Faltung
blurred_vec = signal.convolve2d(grayscale, blur_kernel, mode='same', boundary='symm')
sharpened_vec = signal.convolve2d(grayscale, sharpen_kernel, mode='same', boundary='symm')
edges_vec = signal.convolve2d(grayscale, edge_kernel, mode='same', boundary='symm')
# Ergebnisse in gültigen Bereich bringen
blurred_vec = np.clip(blurred_vec, 0, 255).astype(np.uint8)
sharpened_vec = np.clip(sharpened_vec, 0, 255).astype(np.uint8)
edges_vec = np.clip(edges_vec, 0, 255).astype(np.uint8)
3.5 Übungsaufgabe: Anpassen der Visualisierung von Klimadaten in einem Bild¶
Im Rahmen der Aktionswoche “Open your Course 4 Climate Crisis” (OC4CC) von der Österreichischen Hochschüler_innenschaft und Fridays for Future Austria wählen wir unsere Übungsaufgabe diesmal aus diesem Themengebiet. Konkret werden wir einschlägige Daten als Bilder ins Notebook laden und dann einer Bearbeitung unterziehen.
Die Daten stammen von der EU-Mission Copernicus, konkret aus dem Datensatz “Land Surface Temperature (Synthesis and Thermal Condition Index) 2017-2021 (raster 5 km), global, 10-daily – version 1” (login erforderlich). Ich habe Ihnen von mehreren Daten aus dem Zeitraum Jänner 2017 bis Jänner 2021 Snapshots der Visualisierung der gemessenen Temperaturwerte im Datenverzeichnis abgelegt. Diese Bilder sind die Grundlage unserer Übung.
Das Laden der Bilder und Testen der Bild-Anzeige habe ich hier der Einfachheit bereits für Sie vorbereitet. Den Rest der Übung erarbeiten Sie bitte wie gewohnt in Zusammenarbeit mit dem LLM Ihrer Wahl.
import numpy as np
import os
import matplotlib.pyplot as plt
from PIL import Image
import cv2
from scipy import ndimage
import seaborn as sns
# Bilder laden und in NumPy-Arrays konvertieren
path_2017_01 = os.path.join('data', 'satellite-image-temperature-2017-01.jpg')
img_2017_01 = np.array(Image.open(path_2017_01))
path_2017_07 = os.path.join('data', 'satellite-image-temperature-2017-07.jpg')
img_2017_07 = np.array(Image.open(path_2017_07))
path_2018_07 = os.path.join('data', 'satellite-image-temperature-2018-07.jpg')
img_2018_07 = np.array(Image.open(path_2018_07))
path_2019_01 = os.path.join('data', 'satellite-image-temperature-2019-01.jpg')
img_2019_01 = np.array(Image.open(path_2019_01))
path_2019_07 = os.path.join('data', 'satellite-image-temperature-2019-07.jpg')
img_2019_07 = np.array(Image.open(path_2019_07))
path_2020_07 = os.path.join('data', 'satellite-image-temperature-2020-07.jpg')
img_2020_07 = np.array(Image.open(path_2020_07))
path_2021_01 = os.path.join('data', 'satellite-image-temperature-2021-01.jpg')
img_2021_01 = np.array(Image.open(path_2021_01))
# Überprüfen der Form eines dieser Arrays
print(f"Bilddimensionen: {img_2019_01.shape}") # z.B. (height, width, 3) für ein RGB-Bild
# Bild anzeigen
plt.figure(figsize=(12, 9))
plt.imshow(img_2019_01)
plt.axis('off')
plt.title('Temperatur Jänner 2019')
plt.show()
Bilddimensionen: (1168, 3246, 3)

Diese Bilder stellen als Temperaturdatendarstellungen zu verschiedenen Zeiten im Jahr sowie aus den Julimonaten von vier aufeinanderfolgenden Jahren einen interessanten Ausgangspunkt für viele mögliche Analysen und Vergleiche dar. Ich möchte Sie ermutigen, sich einige interessante Fragestellungen zu überlegen und dann zu versuchen, diese zu beantworten.
Als erste Schritte gilt es folgendes zu beachten:
- Wir üben hier den Umgang mit Bilddaten, also Farbwerten, die Pixeln zugeordnet sind.
- Es gibt keinen vorgegebenen Zusammenhang zwischen Farbe und Temperatur. Einen solchen müssen Sie, falls gewünscht, selbst mit einer geeigneten Annahme herstellen. Dazu empfiehlt es sich, die Farbwerte jedes Pixels in einen “Hue”-Wert umzuwandeln.
- Es gibt nicht für alle Pixel immer Farbwerte. Die Bereiche der Bilder ohne Farbwerte sowie die Meere (wir haben hier Oberflächentemperaturen über Land) sind in Graustufen dargestellt. Die Farbbereiche jedes Bildes sind grundätzlich unterschiedlich.
- Für die allfällige Berechnung von Mittelwerten oder irgendwelchen Verteilungen empfiehlt es sich daher, nur die färbigen Bereiche zu verwenden. Sie können zu diesem Zweck eine Pixel-Maske in NumPy erstellen lassen, die an jedes Bild angepasst wird. Auch hier kann Ihnen der Hue-Wert über eine Grau-Schwelle helfen.
Mit so einer oder einer ähnlichen Herangehensweise sind Ihrer Fantasie keine Grenzen gesetzt. Beispiele für diese Hilfsfunktionen finden sie hier, aber Sie können und sollten das natürlich eventuell direkt mit Ihrem LLMs auf konsistente Weise erarbeiten.
import numpy as np
import os
import matplotlib.pyplot as plt
from PIL import Image
import cv2
from scipy import ndimage
import seaborn as sns
# Definieren verschiedener Hilfs-Funktionen
def rgb_to_hsv(rgb_img):
"""Konvertiert ein RGB-Bild in den HSV-Farbraum"""
# Konvertieren Sie das Bild von 0-255 zu 0-1 Bereich für OpenCV
rgb_normalized = rgb_img.astype(np.float32) / 255
hsv_img = cv2.cvtColor(rgb_normalized, cv2.COLOR_RGB2HSV)
return hsv_img
def extract_hue(hsv_img):
"""Extrahiert den Farbton-Kanal als relativen Temperaturindikator"""
return hsv_img[:,:,0] # H-Kanal (Farbton) aus HSV
def create_temperature_mask(hsv_img, saturation_threshold=0.15):
"""
Erstellt eine binäre Maske, die Bereiche mit tatsächlichen Temperaturdaten (farbig)
von Bereichen ohne Daten (grau) unterscheidet.
"""
# Extrahiere den Sättigungskanal (S)
saturation = hsv_img[:,:,1]
# Erstelle eine binäre Maske basierend auf dem Schwellenwert
mask = saturation > saturation_threshold
# Optional: Anwenden von morphologischen Operationen, um die Maske zu verbessern
# (entfernt kleine isolierte Pixel und füllt kleine Löcher)
mask = ndimage.binary_opening(mask, structure=np.ones((3,3)))
return mask
3.6 Zusammenfassung der wichtigsten Punkte dieser Einheit¶
In dieser Einheit haben wir die Grundlagen der linearen Algebra in Python mit NumPy kennengelernt und gesehen, wie diese in verschiedenen durchaus bunten Anwendungen eingesetzt werden können.
Die wichtigsten Konzepte, die wir behandelt haben:¶
Grundlagen der linearen Algebra mit NumPy
- Vektoren und Matrizen als NumPy-Arrays
- Grundlegende Operationen wie Addition, Multiplikation und Transposition
- Fortgeschrittene Operationen wie Matrixinversion, Determinanten und Eigenwertberechnung
Vektorisierung in Python
- Das Konzept der Vektorisierung als Alternative zu Schleifen
- Signifikante Leistungsverbesserungen durch vektorisierte Operationen
- Anwendungsfälle und Grenzen der Vektorisierung
Anwendungen in der linearen Algebra
- Lösen linearer Gleichungssysteme
- Berechnung und Anwendung von Eigenwerten und Eigenvektoren
Praktische Anwendungen von NumPy-Arrays
- Bildverarbeitung
- Farbkanäle und Graustufen
- Transformationen, Filter und Kompression