Weit zurück im Konzept Vektoren haben wir festgehalten, dass „Arrays beliebiger Größe sein können (nur begrenzt durch die Speicherbeschränkungen deiner Hardware) und beliebig viele Dimensionen haben können.“
Seitdem haben wir Arrays mit mehr als einer Dimension weitgehend ignoriert, um die Dinge einfach zu halten. Diese Entscheidung ergibt mehr Sinn, wenn du versuchst, die Julia-Referenzdokumente in ihrer vollen Komplexität zu lesen.
Arrays mit höheren Dimensionen sind im wissenschaftlichen Rechnen allerdings sehr, sehr wichtig, also müssen wir sie verstehen.
Nomenklatur: Nach jahrhundertelanger mathematischer Tradition bezeichnen wir 1-D-Arrays als Vectors und 2-D-Arrays als Matrices.
Die Beispiele in diesem Dokument sind größtenteils Matrizen. Die Arbeit mit 3 oder mehr Dimensionen ist syntaktisch nahezu identisch, aber die Ausgabe ist (auf einem 2-D-Bildschirm) schwierig und verwirrend zu lesen.
Der type eines N-dimensionalen Arrays mit eltype T ist Array{T, N}.
Der Einfachheit halber und um mit der mathematischen Nomenklatur konsistent zu bleiben, definiert Julia einige Typ-Aliase: Vector{T} für Array{T, 1} und Matrix{T} für Array{T, 2}.
# 3-D array
julia> m3 = ones(2, 3, 4);
julia> typeof(m3)
Array{Float64, 3}
# Vector
julia> v = [1, 2]
2-element Vector{Int64}:
1
2
julia> typeof(v) == Array{Int64, 1}
true
# Matrix
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> typeof(m)
Matrix{Int64} (alias for Array{Int64, 2})
julia> typeof(m) == Array{Int64, 2}
true
Wir haben schon viele Vektoren erzeugt, indem wir eine kommagetrennte Liste in eckige Klammern gesetzt haben. Semikolons können ebenfalls als Trennzeichen verwendet werden.
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> w = [1; 2; 3]
3-element Vector{Int64}:
1
2
3
julia> v == w
true
Wenn wir ein Leerzeichen (oder einen Tabulator) als Trennzeichen verwenden, ist das Ergebnis anders.
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
Julia definiert das jetzt als 1×3-Matrix (in anderen Zusammenhängen würden wir sie row vector nennen).
Im Allgemeinen verbinden Leerzeichen Dinge horizontal, Semikolons (oder Zeilenumbrüche) verbinden sie vertikal.
Der Verweis auf „Dinge“ ist absichtlich vage, denn Julia versucht, mit dem zu arbeiten, was du ihr gibst.
julia> [v 2v]
3×2 Matrix{Int64}:
1 2
2 4
3 6
julia> [v; 2v]
6-element Vector{Int64}:
1
2
3
2
4
6
Es gibt die Funktionen hcat() und vcat(), die dasselbe tun und deutlicher machen, dass es sich um horizontale bzw. vertikale Verkettungen handelt.
Die Verallgemeinerung auf höhere Dimensionen ist die Funktion cat().
jjulia> hcat(v, 2v)
3×2 Matrix{Int64}:
1 2
2 4
3 6
julia> vcat(v, 2v)
6-element Vector{Int64}:
1
2
3
2
4
6
Explizite Matrizen tippt man praktischerweise in zeilenweiser Reihenfolge ein, weil das der menschlichen Intuition entspricht (für Kulturen mit horizontalem Text ist das leichter zu lesen):
julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
Beachte jedoch, dass Julia (wie Fortran, R und Matlab, aber anders als C/C++ oder NumPy) N-dimensionale Arrays in spaltenweiser Reihenfolge speichert, und das kann einen enormen Leistungsunterschied machen, wenn du über die Elemente iterierst. Hilf deinem CPU-Cache, damit er dir hilft!
# put these integers in 2 rows and 3 columns
julia> reshape(collect(1:6), 2, 3)
2×3 Matrix{Int64}:
1 3 5
2 4 6
Das obige Beispiel nimmt die ganzen Zahlen 1 bis 6 und füllt damit spaltenweise eine 2×3-Matrix.
Es gibt verschiedene Hilfsfunktionen, um gängige Array-Typen zu erzeugen (gleichförmig oder zufällig).
julia> zeros(2, 3) # see also ones()
2×3 Matrix{Float64}:
0.0 0.0 0.0
0.0 0.0 0.0
julia> falses(2, 2) # booleans, see also trues()
2×2 BitMatrix:
0 0
0 0
julia> rand(Float32, 2, 3) # random numbers in the interval [0, 1)
2×3 Matrix{Float32}:
0.768823 0.169633 0.632565
0.388451 0.109176 0.850381
NumPy-Fans wissen, dass np.linspace eine weit verbreitete Funktion ist, um ein Array mit festgelegter Länge, festgelegten Endpunkten und gleichmäßig verteilten Werten zu erzeugen.
Sie wird typischerweise als x-Achse eines Diagramms oder als unabhängige Variable in linearen Modellen verwendet.
Julia hat kein exaktes Äquivalent, aber die sehr flexible Funktion range() kann es nachahmen, indem du die untere und obere Grenze sowie das Schlüsselwort-Argument length angibst.
Wer lokal arbeitet und Zugriff auf das Paket Plots hat, kann diesen Code ausführen:
julia> using Plots
julia> x = range(0, 2π; length=100)
0.0:0.06346651825433926:6.283185307179586
julia> typeof(x)
StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
julia> vals = [x sin.(x) cos.(x)]
100×3 Matrix{Float64}:
0.0 0.0 1.0
0.0634665 0.0634239 0.997987
0.126933 0.126592 0.991955
0.1904 0.189251 0.981929
(...truncated)
julia> plot(vals[:, 1], vals[:, 2:3])
Der StepRangeLen kann wie ein Vektor verwendet werden, einschließlich der Umwandlung in eine Matrixspalte.
Sieh dir auch die entsprechende Funktion logrange() an, um Werte zu erzeugen, die auf einer logarithmischen Achse gleichmäßig verteilt sind.
Für ein 2-D-Array verwenden wir im Allgemeinen zwei Indizes in der Reihenfolge [row, col].
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[1, 2] # row 1, col 2
2
# Stay within bounds! There is no row 3.
julia> m[3, 1]
ERROR: BoundsError: attempt to access 2×3 Matrix{Int64} at index [3, 1]
julia> m[3]
2
Das letzte Beispiel ist vielleicht überraschend: Ein einzelner Index ist kein Fehler und gibt ein einzelnes Element zurück.
Die Erklärung führt zurück zum Kommentar über die spaltenweise Reihenfolge: Julia geht Spalte 1 hinunter, dann Spalte 2, bis sie das 3. Element im Speicher findet.
Sei vorsichtig: Das hat einige Anwendungen beim Schreiben von Allzweckbibliotheken, ist aber eher verwirrend!
Ein ähnliches Problem tritt auf, wenn du die Größe eines Arrays abfragst.
length() liefert die Gesamtzahl der Elemente, size() liefert ein Tupel aus ndims() Elementen mit der Länge jeder Dimension.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> length(m)
6
julia> size(m) # 2 rows, 3 cols
(2, 3)
julia> ndims(m) # how many dimensions? Like `m |> size |> length`
2
Wir können leicht eine Untermatrix kopieren.
Im folgenden Beispiel zwingt die Funktion reshape() ein Array in die angegebenen Dimensionen, indem sie spaltenweise füllt, und dann schneiden wir es.
julia> m12 = reshape(collect(1:12), 4, 3)
4×3 Matrix{Int64}:
1 5 9
2 6 10
3 7 11
4 8 12
julia> m12[2:4, 1:2]
3×2 Matrix{Int64}:
2 6
3 7
4 8
# some rows, all columns
julia> m12[2:4, :]
3×3 Matrix{Int64}:
2 6 10
3 7 11
4 8 12
Ein : für sich allein bedeutet „alles in dieser Dimension kopieren“.
Verwende Vektoren für nicht aufeinanderfolgende Zeilen/Spalten:
julia> m12[[1, 3], :] # rows 1 and 3
2×3 Matrix{Int64}:
1 5 9
3 7 11
Ein : für sich allein flacht jedes Array spaltenweise zu einem Vector ab.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[:]
6-element Vector{Int64}:
1
4
2
5
3
6
Wir haben zuvor Aggregationsfunktionen wie sum() und maximum() auf 1-D-Sammlungen gesehen, wo sie auf alle Elemente wirken und ein skalares Ergebnis zurückgeben.
Das funktioniert auch in höheren Dimensionen.
Manchmal möchten wir die Funktion jedoch nur auf eine Dimension anwenden, zum Beispiel indem wir nach unten oder quer summieren, um ein Array mit einer singleton dimension der Größe 1 zurückzugeben.
Dafür gibt es ein optionales Schlüsselwort-Argument dims.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> sum(m) # sum everything
21
julia> sum(m; dims=1) # sum down
1×3 Matrix{Int64}:
5 7 9
julia> sum(m; dims=2) # sum across
2×1 Matrix{Int64}:
6
15
# other aggregation functions are similar
julia> maximum(m; dims=2)
2×1 Matrix{Int64}:
3
6
Der Wert von dims ist die Dimension, die zu singleton wird, also dims=1 => 1×3 und dims=2 => 2×1 in den obigen Beispielen.
Erweiternd können Arrays mit höheren Dimensionen mehrere Dimensionen reduzieren, wenn dims auf ein Array oder einen Bereich gesetzt wird (auch hier kann das Verständnis der Ausgabe etwas Nachdenken erfordern!).
Das Schlüsselwort-Argument dims ist in eingebauten Funktionen wie sum() üblich, aber wie schreiben wir etwas Entsprechendes in unserem eigenen Code?
Eine gute Antwort ist die Verwendung von Funktionen höherer Ordnung wie reduce(), was in einem späteren Konzept ausführlicher behandelt wird.
Alternativ bietet Julia mehrere Funktionen, mit denen wir N-dimensionale Arrays behandeln können, als wären sie verschachtelte Vektoren von Vektoren.
Für Matrizen sind eachrow() und eachcol() praktisch, aber die allgemeinere Funktion ist eachslice(), die beliebige Dimensionen verarbeiten kann.
# m is as in the previous examples
julia> eachrow(m)
2-element RowSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
[1, 2, 3]
[4, 5, 6]
julia> eachcol(m)
3-element ColumnSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
[1, 4]
[2, 5]
[3, 6]
Der Typ ist etwas beunruhigend, aber nur, weil dies eine Sicht auf das ursprüngliche Array ist, die es uns erlaubt, damit zu arbeiten, ohne zu kopieren. Julia-Arrays können potenziell Terabytes groß sein, also ist Kopieren potenziell ein Performance-Albtraum!
Eine solche Sicht kann zum Schleifen, Broadcasten oder für all die anderen Operationen verwendet werden, die wir bisher im Lehrplan gesehen haben.
Außerdem können Comprehensions mit Array-Eingabe mächtig und vielseitig sein. Einfache Fälle wurden im Konzept Schleifen erwähnt, aber es wird eine erweiterte Diskussion in einem späteren Konzept geben.
Den 1-D-Fall haben wir im Konzept Vektoroperationen besprochen.
julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
1.2
1.5
1.7
julia> v .- 0.5
3-element Vector{Float64}:
0.7
1.0
1.2
Der gepunktete Operator .- behandelt das Singleton 0.5 als äquivalent zu [0.5, 0.5, 0.5], indem er es per broadcasting an die Dimensionen anpasst, und führt dann die Subtraktion elementweise aus.
Die Erweiterung auf höhere Dimensionen ist wirklich nur mehr vom Selben.
Zum Beispiel entspricht beim Broadcasten der Multiplikation eines 2x1-Zeilenvektors [1.0 1.5 2.0] mit der 2x3-Matrix [1 2 3; 4 5 6] das Broadcasten von [1.0 1.5 2.0] zu [1.0 1.5 2.0; 1.0 1.5 2.0], wobei die Multiplikation dann elementweise erfolgt.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m .* [1.0 1.5 2.0] # broadcast a 1x3 row vector down
2×3 Matrix{Float64}:
1.0 3.0 6.0
4.0 7.5 12.0
julia> m .* [1.0 1.5 2.0; 1.0 1.5 2.0] # 2x3 elementwise multiplication
2×3 Matrix{Float64}:
1.0 3.0 6.0
4.0 7.5 12.0
julia> m ./ [1, 2] # broadcast a 2x1 column vector across
2×3 Matrix{Float64}:
1.0 2.0 3.0
2.0 2.5 3.0
julia> m ./ [1 1 1; 2 2 2] # 2x3 elementwise division
2×3 Matrix{Float64}:
1.0 2.0 3.0
2.0 2.5 3.0
Hinweis: Für das Broadcasting müssen die nicht-Singleton-Dimensionen in der Größe übereinstimmen (z. B. 2x3 Matrix .* 2x1 Matrix).
Darüber hinaus kann eine Funktion genau wie bei einem Vector auf jedes Element einer Matrix gebroadcastet werden.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> (x -> x^2).(m) # broadcast a function to all elements
2×3 Matrix{Int64}:
1 4 9
16 25 36
Anfangs magst du das Gefühl haben, dass das Potenzial für Verwirrung bei Programmierern exponentiell mit der Anzahl der Dimensionen wächst, aber Übung hilft sehr. Außerdem lässt sich Vertrautheit mit NumPy-Arrays trotz der anderen Syntax recht gut auf Julia übertragen.
Viele gängige Operationen auf Vektoren und Matrizen fallen unter die allgemeine Überschrift Lineare Algebra.
Auf der Exercism-Website gibt es (derzeit) zwei zusätzliche Konzepte, die nur aus Dokumentation bestehen, bis wir Übungen entwerfen können, die dazu passen.
Möglicherweise wird es in Zukunft ein drittes Konzept zu Matrixfaktorisierungen geben.
Bitte beachte jedoch, dass Grundlagen der linearen Algebra ziemlich mathematisch ist und nicht für alle Lernenden geeignet sein könnte.
Die späteren Konzepte (Gleichungslösen und Faktorisierungen) sind fortgeschrittener und richten sich wirklich nur an Personen, die sich mit Mathematik auf Hochschulniveau wohlfühlen.
Entschuldigung, aber das liegt in der Natur des Themas und ist ein wichtiger Anwendungsfall für Julia.