Πολύ πιο πίσω, στην έννοια Διανύσματα, σημειώσαμε ότι "οι πίνακες μπορούν να έχουν αυθαίρετο μέγεθος (υπόκεινται μόνο στους περιορισμούς μνήμης του υλικού σου), και μπορούν να έχουν αυθαίρετα πολλές διαστάσεις."
Έκτοτε, έχουμε σε μεγάλο βαθμό αγνοήσει τους πίνακες με περισσότερες από μία διαστάσεις, απλώς για να κρατήσουμε τα πράγματα απλά. Αυτή η απόφαση θα βγάλει περισσότερο νόημα αν δοκιμάσεις να διαβάσεις τα έγγραφα αναφοράς της Julia, στην πλήρη πολυπλοκότητά τους.
Ωστόσο, οι πίνακες περισσότερων διαστάσεων είναι πάρα πολύ σημαντικοί στον επιστημονικό υπολογισμό, οπότε πρέπει να τους κατανοήσουμε.
Ονοματολογία: Ακολουθώντας αιώνες μαθηματικού προηγούμενου, αναφερόμαστε στους μονοδιάστατους πίνακες ως Vectors και στους δισδιάστατους ως Matrices.
Τα παραδείγματα σε αυτό το κείμενο θα είναι ως επί το πλείστον πίνακες. Η εργασία με 3 ή περισσότερες διαστάσεις είναι συντακτικά σχεδόν πανομοιότυπη, αλλά η έξοδος είναι δύσκολη και μπερδεμένη στην ανάγνωση (σε μια δισδιάστατη οθόνη).
Ο type ενός N-διάστατου πίνακα με eltype T είναι Array{T, N}.
Για ευκολία, και για συνέπεια με τη μαθηματική ονοματολογία, η Julia ορίζει μερικά ψευδώνυμα τύπων: Vector{T} για Array{T, 1} και Matrix{T} για 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
Έχουμε δημιουργήσει πολλά διανύσματα βάζοντας μια λίστα χωρισμένη με κόμμα μέσα σε αγκύλες. Οι άνω τελείες μπορούν επίσης να χρησιμοποιηθούν ως διαχωριστικό.
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
Αν χρησιμοποιήσουμε το κενό (ή το tab) ως διαχωριστικό, το αποτέλεσμα είναι διαφορετικό.
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
Η Julia τώρα το ορίζει αυτό ως πίνακα 1×3 (σε άλλα πλαίσια, θα το λέγαμε row vector).
Γενικά, τα κενά ενώνουν τα πράγματα οριζόντια, οι άνω τελείες (ή οι αλλαγές γραμμής) τα ενώνουν κάθετα.
Η αναφορά στα "πράγματα" είναι σκόπιμα αόριστη, καθώς η Julia θα προσπαθήσει να δουλέψει με ό,τι της δώσεις.
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
Υπάρχουν οι συναρτήσεις hcat() και vcat() που κάνουν το ίδιο πράγμα, κάνοντάς το πιο σαφές ότι πρόκειται για οριζόντιες και κάθετες συνενώσεις.
Η γενίκευση για περισσότερες διαστάσεις είναι η συνάρτηση 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
Η πληκτρολόγηση ρητών πινάκων γίνεται βολικά με σειρά κατά γραμμές, επειδή αυτό ταιριάζει με τη διαίσθηση του ανθρώπου (πιο εύκολο να το κοιτάς, για πολιτισμούς με οριζόντια γραφή):
julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
Ωστόσο, να ξέρεις ότι η Julia (όπως η Fortran, η R και η Matlab, αλλά σε αντίθεση με τη C/C++ ή το NumPy) αποθηκεύει τους N-διάστατους πίνακες σε σειρά κατά στήλες, και αυτό μπορεί να κάνει τεράστια διαφορά στην απόδοση αν κάνεις βρόχο πάνω στα στοιχεία. Βοήθησε την cache της CPU σου, για να σε βοηθήσει κι αυτή!
# 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
Το παραπάνω παράδειγμα παίρνει τους ακέραιους από το 1 έως το 6 και γεμίζει έναν πίνακα 2×3 με αυτούς, κατά στήλες.
Υπάρχουν διάφορες βοηθητικές συναρτήσεις για την κατασκευή κοινών τύπων πίνακα (ομοιόμορφων ή τυχαίων).
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 θα ξέρουν ότι το np.linspace είναι μια ευρέως χρησιμοποιούμενη συνάρτηση για τη δημιουργία ενός πίνακα συγκεκριμένου μήκους, με καθορισμένα άκρα και ισαπέχουσες τιμές.
Αυτό χρησιμοποιείται συνήθως ως ο άξονας x ενός γραφήματος, ή ως η ανεξάρτητη μεταβλητή σε γραμμικά μοντέλα.
Η Julia δεν έχει ακριβές ισοδύναμο, αλλά η πολύ ευέλικτη συνάρτηση range() μπορεί να το μιμηθεί καθορίζοντας τα κάτω και άνω όρια, συν το όρισμα με λέξη-κλειδί length.
Όποιος δουλεύει τοπικά, με πρόσβαση στο πακέτο Plots, μπορεί να τρέξει αυτόν τον κώδικα:
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])
Το StepRangeLen μπορεί να χρησιμοποιηθεί σαν διάνυσμα, συμπεριλαμβανομένης της μετατροπής του σε στήλη πίνακα.
Δες επίσης την αντίστοιχη συνάρτηση logrange(), για τη δημιουργία τιμών με ίσα διαστήματα σε λογαριθμικό άξονα.
Για έναν δισδιάστατο πίνακα, χρησιμοποιούμε γενικά δύο θέσεις με σειρά [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
Το τελευταίο παράδειγμα ίσως εκπλήσσει: μια μεμονωμένη θέση δεν αποτελεί σφάλμα, και επιστρέφει ένα μόνο στοιχείο.
Η εξήγηση πηγαίνει πίσω στο σχόλιο για τη σειρά κατά στήλες: η Julia κατεβαίνει τη στήλη 1, μετά τη στήλη 2, ώσπου να βρει το 3ο στοιχείο στη μνήμη.
Πρόσεξε: αυτό έχει κάποιες χρήσεις όταν γράφεις βιβλιοθήκες γενικού σκοπού, αλλά πιο πιθανό είναι να σε μπερδέψει!
Ένα παρόμοιο ζήτημα προκύπτει όταν ρωτάς το μέγεθος ενός πίνακα.
Η length() δίνει τον συνολικό αριθμό στοιχείων, η size() δίνει μια πλειάδα τόσων στοιχείων όσα επιστρέφει η ndims(), με το μήκος κάθε διάστασης.
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
Μπορούμε εύκολα να αντιγράψουμε έναν υποπίνακα.
Στο παρακάτω παράδειγμα, η συνάρτηση reshape() εξαναγκάζει έναν πίνακα στις καθορισμένες διαστάσεις γεμίζοντάς τον κατά στήλες, και μετά τον τεμαχίζουμε.
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
Ένα : από μόνο του σημαίνει "αντίγραψε τα πάντα σε αυτή τη διάσταση".
Χρησιμοποίησε διανύσματα για μη συνεχόμενες γραμμές/στήλες:
julia> m12[[1, 3], :] # rows 1 and 3
2×3 Matrix{Int64}:
1 5 9
3 7 11
Ένα : από μόνο του θα ισοπεδώσει οποιονδήποτε πίνακα σε ένα Vector, κατά στήλες.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[:]
6-element Vector{Int64}:
1
4
2
5
3
6
Έχουμε δει προηγουμένως συναρτήσεις συνάθροισης όπως οι sum() και maximum() εφαρμοσμένες σε μονοδιάστατες συλλογές, όπου λειτουργούν σε όλα τα στοιχεία και επιστρέφουν ένα βαθμωτό αποτέλεσμα.
Αυτό λειτουργεί επίσης σε περισσότερες διαστάσεις.
Ωστόσο, μπορεί να θέλουμε να εφαρμόσουμε τη συνάρτηση σε μία μόνο διάσταση, για παράδειγμα αθροίζοντας κάθετα ή οριζόντια, ώστε να επιστραφεί ένας πίνακας με μια singleton dimension μεγέθους 1.
Για αυτό, υπάρχει ένα προαιρετικό όρισμα με λέξη-κλειδί 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
Η τιμή του dims είναι η διάσταση που γίνεται singleton, οπότε στα παραπάνω παραδείγματα dims=1 => 1×3 και dims=2 => 2×1.
Κατ' επέκταση, οι πίνακες περισσότερων διαστάσεων μπορούν να μειώσουν πολλαπλές διαστάσεις, αν το dims οριστεί σε έναν πίνακα ή ένα διάστημα (και πάλι, η κατανόηση της εξόδου μπορεί να χρειαστεί λίγη σκέψη!).
Το όρισμα με λέξη-κλειδί dims είναι συνηθισμένο σε ενσωματωμένες συναρτήσεις όπως η sum(), αλλά πώς γράφουμε κάτι αντίστοιχο στον δικό μας κώδικα;
Μια καλή απάντηση είναι να χρησιμοποιήσεις συναρτήσεις υψηλότερης τάξης, όπως η reduce(), και αυτό θα καλυφθεί με κάποια λεπτομέρεια σε μεταγενέστερη έννοια.
Εναλλακτικά, η Julia παρέχει αρκετές συναρτήσεις που μας επιτρέπουν να χειριστούμε τους N-διάστατους πίνακες σαν να ήταν εμφωλευμένα διανύσματα από διανύσματα.
Για πίνακες, οι eachrow() και eachcol() είναι βολικές, αλλά η πιο γενική συνάρτηση είναι η eachslice(), που μπορεί να χειριστεί αυθαίρετες διαστάσεις.
# 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]
Ο τύπος είναι λίγο τρομακτικός, αλλά μόνο επειδή πρόκειται για μια όψη στον αρχικό πίνακα, που μας επιτρέπει να δουλέψουμε πάνω του χωρίς αντιγραφή. Οι πίνακες της Julia μπορούν ενδεχομένως να έχουν μέγεθος Terabytes, οπότε η αντιγραφή μπορεί να είναι εφιάλτης για την απόδοση!
Μια τέτοια όψη μπορεί να χρησιμοποιηθεί για βρόχους, για broadcasting, ή για όλες τις άλλες πράξεις που έχουμε δει στη διδακτέα ύλη μέχρι τώρα.
Επίσης, οι κατανοήσεις μπορούν να είναι ισχυρές και ευέλικτες με είσοδο πίνακα. Απλές περιπτώσεις αναφέρθηκαν στην έννοια Βρόχοι, αλλά θα υπάρξει εκτενέστερη συζήτηση σε μεταγενέστερη έννοια.
Συζητήσαμε τη μονοδιάστατη περίπτωση στην έννοια Πράξεις με Διανύσματα.
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
Ο τελεστής με τελεία .- αντιμετωπίζει το μεμονωμένο 0.5 ως ισοδύναμο με το [0.5, 0.5, 0.5], κάνοντάς το broadcasting ώστε να ταιριάζουν οι διαστάσεις, και μετά κάνει την αφαίρεση στοιχείο προς στοιχείο.
Η επέκταση αυτού σε περισσότερες διαστάσεις είναι στην πραγματικότητα απλώς το ίδιο πράγμα ξανά.
Για παράδειγμα, όταν κάνουμε broadcasting στον πολλαπλασιασμό ενός διανύσματος γραμμής 2x1 [1.0 1.5 2.0] με τον πίνακα 2x3 [1 2 3; 4 5 6], είναι ισοδύναμο με το να κάνουμε broadcasting το [1.0 1.5 2.0] σε [1.0 1.5 2.0; 1.0 1.5 2.0], με τον πολλαπλασιασμό να γίνεται μετά στοιχείο προς στοιχείο.
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
Σημείωση: Για το broadcasting, οι διαστάσεις που δεν είναι singleton πρέπει να ταιριάζουν σε μέγεθος (π.χ. 2x3 Matrix .* 2x1 Matrix).
Επιπλέον, μια συνάρτηση μπορεί να γίνει broadcast σε κάθε στοιχείο ενός Matrix ακριβώς όπως γίνεται με ένα Vector.
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
Μπορεί στην αρχή να νιώσεις ότι το περιθώριο για σύγχυση του προγραμματιστή κλιμακώνεται εκθετικά με τον αριθμό των διαστάσεων, αλλά η εξάσκηση βοηθάει πολύ. Επίσης, η εξοικείωση με τους πίνακες του NumPy μεταφέρεται αρκετά καλά στη Julia, παρά τη διαφορετική σύνταξη.
Πολλές κοινές πράξεις σε διανύσματα και πίνακες εμπίπτουν στη γενική κατηγορία της Γραμμικής Άλγεβρας.
Υπάρχουν (προς το παρόν) δύο επιπλέον έννοιες στον ιστότοπο του Exercism, οι οποίες υπάρχουν μόνο ως τεκμηρίωση μέχρι να σχεδιάσουμε ασκήσεις που να τις συνοδεύουν.
Ίσως στο μέλλον υπάρξει και μια τρίτη έννοια για τις παραγοντοποιήσεις πινάκων.
Ωστόσο, να ξέρεις ότι τα Βασικά Στοιχεία Γραμμικής Άλγεβρας είναι αρκετά μαθηματικά και μπορεί να μην ταιριάζουν σε όλους τους μαθητές.
Οι μεταγενέστερες έννοιες (επίλυση εξισώσεων και παραγοντοποιήσεις) είναι πιο προχωρημένες και απευθύνονται πραγματικά μόνο σε άτομα που νιώθουν άνετα με μαθηματικά πανεπιστημιακού επιπέδου.
Συγγνώμη, αλλά αυτή είναι η φύση του θέματος, και μια σημαντική περίπτωση χρήσης για τη Julia.