using LinearAlgebra
13 Lineare Algebra in Julia
Das LinearAlgebra
-Paket liefert unter anderem:
zusätzliche Subtypen von
AbstractMatrix
: genauso verwendbar, wie andere Matrizen, z.B.Tridiagonal
SymTridiagonal
Symmetric
UpperTriangular
zusätzliche/erweiterte Funktionen:
norm
,opnorm
,cond
,inv
,det
,exp
,tr
,dot
,cross
, …einen universellen Solver für lineare Gleichungssysteme:
\
x = A \ b
löst \(A \mathbf{x}=\mathbf{b}\) durch geeignete Matrixfaktorisierung und Vorwärts/Rückwärtssubstition
-
LU
QR
Cholesky
SVD
- …
Berechnung von Eigenwerte/-vektoren
eigen
,eigvals
,eigvecs
Zugriff auf BLAS/LAPACK-Funktionen
13.1 Matrixtypen
= SymTridiagonal(fill(1.0, 4), fill(-0.3, 3)) A
4×4 SymTridiagonal{Float64, Vector{Float64}}:
1.0 -0.3 ⋅ ⋅
-0.3 1.0 -0.3 ⋅
⋅ -0.3 1.0 -0.3
⋅ ⋅ -0.3 1.0
= UpperTriangular(A) B
4×4 UpperTriangular{Float64, SymTridiagonal{Float64, Vector{Float64}}}:
1.0 -0.3 0.0 0.0
⋅ 1.0 -0.3 0.0
⋅ ⋅ 1.0 -0.3
⋅ ⋅ ⋅ 1.0
Diese Typen werden platzsparend gespeichert. Die üblichen Rechenoperationen sind implementiert:
+ B A
4×4 Matrix{Float64}:
2.0 -0.6 0.0 0.0
-0.3 2.0 -0.6 0.0
0.0 -0.3 2.0 -0.6
0.0 0.0 -0.3 2.0
Lesende Indexzugriffe sind möglich,
1,4] A[
0.0
schreibende nicht unbedingt:
1,3] = 17 A[
ArgumentError: cannot set off-diagonal entry (1, 3)
Stacktrace:
[1] setindex!(A::SymTridiagonal{Float64, Vector{Float64}}, x::Int64, i::Int64, j::Int64)
@ LinearAlgebra ~/.julia/juliaup/julia-1.11.3+0.x64.linux.gnu/share/julia/stdlib/v1.11/LinearAlgebra/src/tridiag.jl:466
[2] top-level scope
@ ~/Julia/23/Book-ansipatch/chapters/11_LinAlg.qmd:74
Die Umwandlung in eine ‘normale’ Matrix ist z.B. mit collect()
möglich:
= collect(A) A2
4×4 Matrix{Float64}:
1.0 -0.3 0.0 0.0
-0.3 1.0 -0.3 0.0
0.0 -0.3 1.0 -0.3
0.0 0.0 -0.3 1.0
13.1.1 Die Einheitsmatrix I
I
bezeichnet eine Einheitsmatrix (quadratisch, Diagonalelemente = 1, alle anderen = 0) in der jeweils erforderlichen Größe
+ 4I A
4×4 SymTridiagonal{Float64, Vector{Float64}}:
5.0 -0.3 ⋅ ⋅
-0.3 5.0 -0.3 ⋅
⋅ -0.3 5.0 -0.3
⋅ ⋅ -0.3 5.0
13.2 Normen
Um Fragen wie Kondition oder Konvergenz eines Algorithmus studieren zu können, brauchen wir eine Metrik. Für lineare Räume ist es zweckmäßig, die Metrik über eine Norm zu definieren: \[ d(x,y) := ||x-y|| \]
13.2.1 \(p\)-Normen
Eine einfache Klasse von Normen im \(ℝ^n\) sind die \(p\)-Normen \[ ||\mathbf{x}||_p = \left(\sum |x_i|^p\right)^\frac{1}{p}, \] die die die euklidische Norm \(p=2\) verallgemeinern.
Sei \(x_{\text{max}}\) die betragsmäßig größte Komponente von \(\mathbf{x}\in ℝ^n\). Dann gilt stets \[ |x_{\text{max}}| \le ||\mathbf{x}||_p \le n^\frac{1}{p} |x_{\text{max}}| \] (Man betrachte einen Vektor, dessen Komponenten alle gleich \(x_{\text{max}}\) sind bzw. einen Vektor, dessen Komponenten außer \(x_{\text{max}}\) alle gleich Null sind.)
Damit folgt \[ \lim_{p \rightarrow \infty} ||\mathbf{x}||_p = |x_{\text{max}}| =: ||\mathbf{x}||_\infty. \]
In Julia definiert das LinearAlgebra
-Paket eine Funktion norm(v, p)
.
= [3, 4]
v = [-1, 2, 33.2]
w
@show norm(v) norm(v, 2) norm(v, 1) norm(v, 4) norm(w, Inf);
norm(v) = 5.0
norm(v, 2) = 5.0
norm(v, 1) = 7.0
norm(v, 4) = 4.284572294953817
norm(w, Inf) = 33.2
- Wenn das 2. Argument
p
fehlt, wirdp=2
gesetzt. - Das 2. Argument kann auch
Inf
(also \(+\infty\)) sein. - Das 1. Argument kann ein beliebiger Container voller Zahlen sein. Die Summe \(\sum |x_i|^p\) erstreckt sich über alle Elemente des Containers.
- Damit ist für eine Matrix
norm(A)
gleich der Frobenius-Norm der MatrixA
.
= [1 2 3
A 4 5 6
7 8 9]
norm(A) # Frobenius norm
16.881943016134134
Da Normen homogen unter Multiplikation mit Skalaren sind, \(||\lambda \mathbf{x}|| = |\lambda|\cdot||\mathbf{x}||\), sind sie durch die Angabe der Einheitskugel vollständig bestimmt. Subadditivität der Norm (Dreiecksungleichung) ist äquivalent zur Konvexität der Einheitskugel (Code durch anklicken sichtbar).
Code
using Plots
=[:purple, :green, :red, :blue,:aqua, :black]
colors=LinRange(-1, 1, 1000)
x=LinRange(-1, 1, 1000)
y
=plot()
fig1for p ∈ (0.8, 1, 1.5, 2, 3.001, 1000)
contour!(x,y, (x,y) -> p * norm([x, y], p), levels=[p], aspect_ratio=1,
=false, color=[pop!(colors)], contour_labels=true, ylim=[-1.1, 1.1])
cbarend
fig1
Einheitskugeln im \(ℝ^2\) für verschiedene \(p\)-Normen: \(p\)=0.8; 1; 1.5; 2; 3.001 und 1000
Wie man sieht, muß \(p\ge 1\) sein, damit die Einheitskugel konvex und \(||.||_p\) eine Norm ist.
Die Julia-Funktion norm(v, p)
liefert allerdings für beliebige Parameter p
ein Ergebnis.
13.2.2 Induzierte Normen (Operatornormen)
Matrizen \(A\) repräsentieren lineare Abbildungen \(\mathbf{v}\mapsto A\mathbf{v}\). Die von einer Vektornorm Induzierte Matrixnorm beantwortet die Frage:
„Um welchen Faktor kann ein Vektor durch die Transformation \(A\) maximal gestreckt werden?“
Auf Grund der Homogenität der Norm unter Multiplikation mit Skalaren reicht es aus, das Bild der Einheitskugel unter der Transformation \(A\) zu betrachten.
Sei \(V\) ein Vektorraum mit einer Dimension \(0<n<\infty\) und \(A\) eine \(n\times n\)-Matrix. Dann ist \[ ||A||_p = \max_{||\mathbf{v}||_p=1} ||A\mathbf{v}||_p \]
Induzierte Normen lassen sich für allgemeines \(p\) nur schwer berechnen. Ausnahmen sind die Fälle
- \(p=1\): Spaltensummennorm
- \(p=2\): Spektralnorm und
- \(p=\infty\): Zeilensummennorm
Diese 3 Fälle sind in Julia in der Funktion opnorm(A, p)
aus dem LinearAlgebra
-Paket implementiert, wobei wieder opnorm(A) = opnorm(A, 2)
gilt.
= [ 0 1
A 1.2 1.5 ]
@show opnorm(A, 1) opnorm(A, Inf) opnorm(A, 2) opnorm(A);
opnorm(A, 1) = 2.5
opnorm(A, Inf) = 2.7
opnorm(A, 2) = 2.0879899930905124
opnorm(A) = 2.0879899930905124
Das folgende Bild zeigt die Wirkung von \(A\) auf Einheitsvektoren. Vektoren gleicher Farbe werden aufeinander abgebildet. (Code durch anklicken sichtbar):
Code
using CairoMakie
# Makie bug https://github.com/MakieOrg/Makie.jl/issues/3255
# Würgaround https://github.com/MakieOrg/Makie.jl/issues/2607#issuecomment-1385816645
= BezierPath([
tri MoveTo(Point2f(-0.5, -1)), LineTo(0, 0), LineTo(0.5, -1), ClosePath()
])
= [ 0 1
A 1.2 1.5 ]
= LinRange(0, 1, 100)
t = sin.(2π*t)
xs = cos.(2π*t)
ys = A * [xs, ys]
Axys
= [sin(n*π/6) for n=0:11]
u = [cos(n*π/6) for n=0:11]
v = y = zeros(12)
x
= A * [u,v]
Auv
= Figure(size=(800, 400))
fig2 lines(fig2[1, 1], xs, ys, color=t, linewidth=5, colormap=:hsv, axis=(; aspect = 1, limits=(-2,2, -2,2),
=L"$\mathbf{v}$", titlesize=30))
titlearrows!(fig2[1,1], x, y, u, v, arrowsize=10, arrowhead=tri, colormap=:hsv, linecolor=range(0,11), linewidth=3)
Legend(fig2[1,2], MarkerElement[], String[], L"⟹", width=40, height=30, titlesize=30, framevisible=false)
lines(fig2[1,3], Axys[1], Axys[2], color=t, linewidth=5, colormap=:hsv, axis=(; aspect=1, limits=(-2,2, -2,2),
=L"$A\mathbf{v}$", titlesize=30))
titlearrows!(fig2[1,3], x, y, Auv[1], Auv[2], arrowsize=10, arrowhead=tri, colormap=:hsv, linecolor=range(0,11),
=3)
linewidth
fig2
13.2.3 Konditionszahl
Für p = 1, p = 2 (default) oder p = Inf liefert cond(A,p)
die Konditionszahl in der \(p\)-Norm \[
\text{cond}_p(A) = ||A||_p \cdot ||A^{-1}||_p
\]
@show cond(A, 1) cond(A, 2) cond(A) cond(A, Inf);
cond(A, 1) = 5.625
cond(A, 2) = 3.633085176038432
cond(A) = 3.633085176038432
cond(A, Inf) = 5.625000000000001
13.3 Matrixfaktorisierungen
Basisaufgaben der numerischen linearen Algebra:
- Löse ein lineares Gleichungssystem \(A\mathbf{x} = \mathbf{b}\).
- Falls keine Lösung existiert, finde die beste Annäherung, d.h., den Vektor \(\mathbf{x}\), der \(||A\mathbf{x} - \mathbf{b}||\) minimiert.
- Finde Eigenwerte und Eigenvektoren \(A\mathbf{x} = \lambda \mathbf{x}\) von \(A\).
Diese Aufgaben sind mit Matrixfaktorisierungen lösbar. Einige grundlegende Matrixfaktorisierungen:
- LU-Zerlegung \(A=L\cdot U\)
- faktorisiert eine Matrix als Produkt einer lower und einer upper Dreiecksmatrix
- im Deutschen auch LR-Zerlegung (aber die Julia-Funktion heisst
lu()
) - geht (eventuell nach Zeilenvertauschung - Pivoting) immer
- Cholesky-Zerlegung \(A=L\cdot L^*\)
- die obere Dreiecksmatrix ist die konjugierte der unteren,
- halber Aufwand im Vergleich zu LU
- geht nur, wenn \(A\) hermitesch und positiv definit ist
- QR-Zerlegung \(A=Q\cdot R\)
- zerlegt \(A\) als Produkt einer orthogonalen (bzw. unitären im komplexen Fall) Matrix und einer oberen Dreiecksmatrix
- \(Q\) ist längenerhaltend (Drehungen und/oder Spiegelungen); die Stauchungen/Streckungen werden durch \(R\) beschrieben
- geht immer
- SVD (Singular value decomposition): \(A = U\cdot D \cdot V^*\)
- \(U\) und \(V\) sind orthogonal (bzw. unitär), \(D\) ist eine Diagonalmatrix mit Einträgen in der Diagonale \(σ_i\ge 0\), den sogenannten Singulärwerten von \(A\).
- Jede lineare Transformation \(\mathbf{v} \mapsto A\mathbf{v}\) läßt sich somit darstellen als eine Drehung (und/oder Spiegelung) \(V^*\), gefolgt von einer reinen Skalierung \(v_i \mapsto \sigma_i v_i\) und einer weitere Drehung \(U\).
- \(U\) und \(V\) sind orthogonal (bzw. unitär), \(D\) ist eine Diagonalmatrix mit Einträgen in der Diagonale \(σ_i\ge 0\), den sogenannten Singulärwerten von \(A\).
13.3.1 LU-Faktorisierung
LU-Faktorisierung ist Gauß-Elimination. Das Resultat der Gauß-Elimination ist die obere Dreiecksmatrix \(U\). Die untere Dreiecksmatrix \(L\) enthält Einsen auf der Diagonale und die nichtdiagonalen Einträge \(l_{ij}\) sind gleich minus den Koeffizienten, mit denen im Gauß-Algorithmus Zeile \(Z_j\) multipliziert und zu Zeile \(Z_i\) addiert wird. Ein Beispiel: \[ A=\left[ \begin{array}{ccc} 1 &2 &2 \\ 3 &-4& 4 \\ -2 & 1 & 5 \end{array}\right] ~ \begin{array}{c} ~\\ Z_2 \mapsto Z_2 \mathbin{\color{red}-}\textcolor{red}{3} Z_1\\ Z_3 \mapsto Z_3 + \textcolor{red}{2} Z_1 \end{array} \quad \Longrightarrow\ \left[ \begin{array}{ccc} 1 &2 &2 \\ &-10& -2 \\ & 5 & 9 \end{array}\right] ~ \begin{array}{c} ~\\ ~\\ Z_3 \mapsto Z_3 + \textcolor{red}{\frac{1}{2}} Z_2 \end{array} \quad \Longrightarrow\ \left[ \begin{array}{ccc} 1 &2 &2 \\ &-10& -2 \\ & & 8 \end{array}\right] = U \] \[ A = \left[ \begin{array}{ccc} 1 && \\ \mathbin{\color{red}+}\textcolor{red}{3} &1 & \\ \mathbin{\color{red}-}\textcolor{red}{2} & \mathbin{\color{red}-}\textcolor{red}{\frac{1}{2}}& 1 \end{array} \right] \cdot \left[ \begin{array}{ccc} 1 &2 &2 \\ &-10& -2 \\ & & 8 \end{array}\right] \]
- Häufig in der Praxis: \(A\mathbf{x}=\mathbf{b}\) muss für ein \(A\) und viele rechte Seiten \(\mathbf{b}\) gelöst werden.
- Die Faktorisierung, deren Aufwand kubisch \(\sim n^3\) mit der Matrixgröße \(n\) wächst, muss nur einmal gemacht werden.
- Der anschliessende Aufwand der Vorwärts/Rückwärtssubstition für jedes \(\mathbf{b}\) ist nur noch quadratisch \(\sim n^2\).
Das LinearAlgebra
-Paket von Julia enthält zur Berechnung einer LU-Zerlegung die Funktion lu(A, options)
:
= [ 1 2 2
A 3 -4 4
-2 1 5]
= lu(A, NoPivot())
L, U, _ display(L)
display(U)
3×3 Matrix{Float64}:
1.0 0.0 0.0
3.0 1.0 0.0
-2.0 -0.5 1.0
3×3 Matrix{Float64}:
1.0 2.0 2.0
0.0 -10.0 -2.0
0.0 0.0 8.0
Pivoting
Sehen wir uns einen Schritt der Gauß-Elimination an: \[ \left[ \begin{array}{cccccc} * &\cdots & * & * & \cdots & * \\ &\ddots & \vdots &\vdots && \vdots \\ && * & * &\cdots & * \\ && & \textcolor{red}{a_{ij}}&\cdots & a_{in}\\ && & \textcolor{blue}{a_{i+1,j}}&\cdots & a_{i+1,n}\\ &&& \textcolor{blue}{\vdots} &&\vdots \\ && & \textcolor{blue}{a_{mj}}&\cdots & a_{mn} \end{array} \right] \] Ziel ist es, als nächstes den Eintrag \(a_{i+1,j}\) zum Verschwinden zu bringen, indem zur Zeile \(Z_{i+1}\) ein geeignetes Vielfaches von Zeile \(Z_i\) addiert wird. Das geht nur, wenn das Pivotelement \(\textcolor{red}{a_{ij}}\) nicht Null ist. Falls \(\textcolor{red}{a_{ij}}=0\), müssen wir Zeilen vertauschen um dies zu beheben.
Darüber hinaus ist die Kondition des Algorithmus am besten, wenn man bei jedem Schritt die Matrix so anordnet, dass das Pivotelement das betragsmäßig größte in der entsprechenden Spalte der noch zu bearbeitenden Umtermatrix ist. Beim (Zeilen-)Pivoting wird bei jedem Schritt durch Zeilenvertauschung sichergestellt, dass gilt: \[ |\textcolor{red}{a_{ij}}|=\max_{k=i,...,m} |\textcolor{blue}{a_{kj}}| \]
LU in Julia
- Die Faktorisierungen in Julia geben ein spezielles Objekt zurück, das die Matrixfaktoren und weitere Informationen enthält.
- Die Julia-Funktion
lu(A)
führt eine LU-Faktorisierung mit Pivoting durch.
= lu(A)
F typeof(F)
LU{Float64, Matrix{Float64}, Vector{Int64}}
Elemente des Objekts:
@show F.L F.U F.p;
F.L = [1.0 0.0 0.0; 0.3333333333333333 1.0 0.0; -0.6666666666666666 -0.5 1.0]
F.U = [3.0 -4.0 4.0; 0.0 3.333333333333333 0.6666666666666667; 0.0 0.0 8.0]
F.p = [2, 1, 3]
Man kann auch gleich auf der linken Seite ein entsprechendes Tupel verwenden:
= lu(A);
L, U, p p
3-element Vector{Int64}:
2
1
3
Der Permutationsvektor zeigt an, wie die Zeilen der Matrix permutiert wurden. Es gilt: \[ L\cdot U = PA\]. Die Syntax der indirekten Indizierung erlaubt es, die Zeilenpermutation durch die Schreibweise A[p,:]
anzuwenden:
display(A)
display(A[p,:])
display(L*U)
3×3 Matrix{Int64}:
1 2 2
3 -4 4
-2 1 5
3×3 Matrix{Int64}:
3 -4 4
1 2 2
-2 1 5
3×3 Matrix{Float64}:
3.0 -4.0 4.0
1.0 2.0 2.0
-2.0 1.0 5.0
Die Vorwärts/Rückwärtssubstition mit einem LU
- erledigt der Operator \
:
= [1, 2, 3]
b = F \ b x
3-element Vector{Float64}:
-0.10000000000000002
-0.012500000000000006
0.5625
Probe:
* x - b A
3-element Vector{Float64}:
0.0
0.0
0.0
In Julia verbirgt sich hinter dem \
-Operator ein ziemlich universeller “matrix solver” und man kann ihn auch direkt anwenden:
\ b A
3-element Vector{Float64}:
-0.10000000000000002
-0.012500000000000006
0.5625
Dabei wird implizit eine geeignete Faktorisierung durchgeführt, deren Ergebnis allerdings nicht abgespeichert.
13.3.2 QR-Zerlegung
Die Funktion qr()
liefert ein epezielles QR-Objekt zurück, das die Komponenten \(Q\) und \(R\) enthält. Die orthogonale (bzw. unitäre) Matrix \(Q\) ist in einer optimierten Form abgespeichert. Umwandlung in eine “normale” Matrix ist bei Bedarf wie immer mit collect()
möglich.
= qr(A)
F @show typeof(F) typeof(F.Q)
display(collect(F.Q))
display(F.R)
typeof(F) = LinearAlgebra.QRCompactWY{Float64, Matrix{Float64}, Matrix{Float64}}
typeof(F.Q) = LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}, Matrix{Float64}}
3×3 Matrix{Float64}:
-0.267261 0.872872 0.408248
-0.801784 -0.436436 0.408248
0.534522 -0.218218 0.816497
3×3 Matrix{Float64}:
-3.74166 3.20713 -1.06904
0.0 3.27327 -1.09109
0.0 0.0 6.53197
13.3.3 Passende Faktorisierung
Die Funktion factorize()
liefert eine dem Matrixtyp angepasste Form der Faktorisierung, siehe Dokumentation für Details. Wenn man Lösungen zu mehreren rechten Seiten \(\mathbf{b_1}, \mathbf{b_2},...\) benötigt, sollte man die Faktorisierung nur einmal durchführen:
= factorize(A) Af
LU{Float64, Matrix{Float64}, Vector{Int64}}
L factor:
3×3 Matrix{Float64}:
1.0 0.0 0.0
0.333333 1.0 0.0
-0.666667 -0.5 1.0
U factor:
3×3 Matrix{Float64}:
3.0 -4.0 4.0
0.0 3.33333 0.666667
0.0 0.0 8.0
\ [1, 2, 3] Af
3-element Vector{Float64}:
-0.10000000000000002
-0.012500000000000006
0.5625
\ [5, 7, 9] Af
3-element Vector{Float64}:
0.4000000000000001
0.42500000000000004
1.875