Διαδρομές
/
Julia
Julia
/
Ύλη
/
Πράξεις με διανύσματα
Πρ

Πράξεις με διανύσματα σε Julia

2 ασκήσεις

Σχετικά με την έννοια Πράξεις με διανύσματα

Στην έννοια Vectors, είπαμε ότι «οι πίνακες βρίσκονται στην καρδιά της γλώσσας Julia» και ότι ένα διάνυσμα είναι ένας μονοδιάστατος πίνακας.

Δεδομένου αυτού, θα μπορούσαμε εύλογα να ελπίζουμε ότι η γλώσσα παρέχει πολλούς ευέλικτους και ισχυρούς τρόπους για να κάνεις πράγματα με διανύσματα, ό,τι κι αν σημαίνει αυτό.

Μια σημείωση για την ορολογία: παρόλο που αυτό το κείμενο μιλάει πολύ για "διανύσματα", μεγάλο μέρος του ισχύει επίσης για οποιονδήποτε τύπο που μπορεί να διατραχθεί: διαστήματα, πλειάδες, σύνολα και διάφορα άλλα.

Συναρτήσεις που περιμένουν διάνυσμα ως είσοδο

Κάποιες πολύ απλές συναρτήσεις δέχονται ένα διάνυσμα ως είσοδο και (για μονοδιάστατη είσοδο) επιστρέφουν μια βαθμωτή τιμή.

v = [2, 3, 4]
length(v)  # => 3
sum(v)  # => 9

Όταν φτάσουμε στην έννοια των πολυδιάστατων πινάκων, θα γίνει πιο ξεκάθαρο ότι αυτό είναι μείωση διάστασης και όχι απαραίτητα επιστροφή μιας βαθμωτής τιμής. Αν αυτό δεν σου βγάζει νόημα, άφησέ το για την ώρα.

Υπάρχουν πολλές ακόμη συναρτήσεις αυτού του τύπου. Δες την έννοια Statistics για μερικά παραδείγματα.

Υπάρχουν επίσης συναρτήσεις που λειτουργούν σε πολλά διανύσματα, όπως η (πολύ χρήσιμη) zip.

julia> z = zip( 1:3, ['a', 'b', 'c'], ["I", "make", "tuples"] )
zip([1, 2, 3], ['a', 'b', 'c'], ["I", "make", "tuples"])

# convert iterator to vector
julia> collect(z)
3-element Vector{Tuple{Int64, Char, String}}:
 (1, 'a', "I")
 (2, 'b', "make")
 (3, 'c', "tuples")

Η zip() δέχεται οποιονδήποτε αριθμό διανυσματικών εισόδων και επιστρέφει έναν επαναλήπτη πλειάδων.

Οι είσοδοι έχουν συνήθως όλες το ίδιο μήκος. Αν μία είναι μικρότερη, οι υπόλοιπες περικόπτονται στο μικρότερο μήκος: ίσως αυτό που είχες στο μυαλό σου, αλλά πιο συχνά ένα bug στον κώδικα που έγραψες.

Αριθμητικές πράξεις

Ας υποθέσουμε ότι έχεις ένα αριθμητικό διάνυσμα και θέλεις να αφαιρέσεις 0.5 από κάθε τιμή.

julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
 1.2
 1.5
 1.7

julia> v - 0.5
ERROR: MethodError: no method matching -(::Vector{Float64}, ::Float64)

Αυτό αποτυγχάνει, οπότε τι γίνεται με την αφαίρεση ενός άλλου διανύσματος;

julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

Πετυχημένο, αλλά αρκετά κουραστικό και απαιτητικό σε μνήμη όσο τα διανύσματα μεγαλώνουν.

Ανάλογα με το πόσο μακριά έχεις φτάσει στην ύλη, μπορείς πιθανότατα να σκεφτείς κι άλλες προσεγγίσεις:

  • Γράψε έναν βρόχο, αν και θα ήταν φλύαρο και άκομψο.
  • Χρησιμοποίησε ένα comprehension: το [x - 0.5 for x in v] δίνει το επιθυμητό αποτέλεσμα (σε στυλ Python).
  • Χρησιμοποίησε μια συνάρτηση ανώτερης τάξης: το map(x -> x - 0.5, v) δουλεύει επίσης (σε στυλ Haskell, αν και συνηθίζεται σε πολλές γλώσσες).

Ευτυχώς, η Julia έχει μια "μαγική" τελεία που λύνει αυτό το πρόβλημα πολύ απλά: το v .- 0.5 είναι το μόνο που χρειάζεσαι.

Η επόμενη ενότητα εξηγεί το γιατί.

Μετάδοση

Λοιπόν, το v - 0.5 αποτυγχάνει αλλά το v .- 0.5 πετυχαίνει, και πρέπει να καταλάβουμε τι κάνει η τελεία.

Δύο πράγματα, που συνδυάζονται για να δώσουν το επιθυμητό αποτέλεσμα.

1) Εφαρμογή στοιχείο προς στοιχείο

Πρώτα, η προσθήκη μιας τελείας πριν από έναν ενθεματικό τελεστή σημαίνει "εφάρμοσε αυτή την πράξη σε κάθε στοιχείο ξεχωριστά".

Παρομοίως, η προσθήκη μιας τελείας μετά από το όνομα μιας συνάρτησης τη "διανυσματικοποιεί", ακόμα κι αν η συνάρτηση έχει γραφτεί για βαθμωτές εισόδους.

julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
 1.0
 2.0
 3.0

Παρεμπιπτόντως, οι ενθεματικοί τελεστές είναι στην πραγματικότητα απλώς συντακτική ζάχαρη για την υποκείμενη συνάρτηση.

Αυτό σημαίνει ότι, για παράδειγμα, το [1, 5, 10] .% 3 μεταφράζεται από τον διερμηνευτή σε mod.([1, 5, 10], 3), και στη συνέχεια εκτελείται η σύνταξη mod. (και οι δύο εκδοχές επιστρέφουν [1, 2, 1]).

2) Επέκταση μονήρων στοιχείων

Είδαμε σε ένα προηγούμενο παράδειγμα ότι μπορούμε να αφαιρέσουμε διανύσματα ίσου μήκους, αν και πρέπει να καταλάβεις ότι το .- είναι ασφαλέστερος τελεστής από το -, καθώς κάνει ξεκάθαρη την πρόθεση της πράξης στοιχείο προς στοιχείο.

julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

Τι γίνεται με διανύσματα άνισου μήκους;

julia> v .- [0.5, 0.5]
ERROR: DimensionMismatch: arrays could not be broadcast to a common size

julia> v .- [0.5,]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

Γενικά, τα άνισα μήκη είναι σφάλμα, εκτός όταν το ένα έχει μήκος 1 (τεχνικά, μια "μονήρης" διάσταση).

Τα μονήρη στοιχεία όπως το [0.5,] ή απλώς το 0.5 επεκτείνονται αυτόματα στο απαραίτητο μήκος με επανάληψη. Αυτό βρίσκεται στην καρδιά του broadcasting.

Όποιος ανησυχεί για τη χρήση μνήμης από αυτή την "επανάληψη" μπορεί να ηρεμήσει: υλοποιείται με πολύ αποδοτικό τρόπο που στην πραγματικότητα δεν αντιγράφει τις τιμές στη μνήμη.

Οι προγραμματιστές που γνωρίζουν τη μετάδοση σε άλλες γλώσσες θα πρέπει να σημειώσουν ότι η προσέγγιση της Julia είναι (ως επί το πλείστον) παρόμοια με του NumPy, αλλά πολύ λιγότερο ανεκτική σε αναντιστοιχίες μεγεθών από την R.

Μετάδοση επιτόπου

Αν η χρήση μνήμης σε απασχολεί, τότε οι πράξεις επιτόπου είναι ένας συνηθισμένος τρόπος να μειώσεις τις δεσμεύσεις μνήμης. Ωστόσο, οι πράξεις μετάδοσης στα παραπάνω παραδείγματα δημιουργούν ένα νέο Vector αντί να τροποποιήσουν το αρχικό.

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v
3-element Vector{Int64}:
 1
 2
 3

Για να τροποποιήσεις το v επιτόπου, η τροποποίηση πρέπει να γίνει μέσω μετάδοσης.

julia> v .= v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v .+= 1  # equivalent operation to above
3-element Vector{Int64}:
 3
 4
 5

julia> v
3-element Vector{Int64}:
 3
 4
 5

Αλλά πρόσεξε! Η τελεία πριν από τον τελεστή ανάθεσης .= είναι σημαντική.

julia> v = [1, 2, 3];

julia> v = v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v
3-element Vector{Int64}:
 2
 3
 4

Φαίνεται ότι δούλεψε με τον ίδιο τρόπο, αλλά εδώ το v .+ 1 δημιούργησε ένα νέο διάνυσμα [2, 3, 4] και στη συνέχεια το ανέθεσε στη μεταβλητή v, αφήνοντας το αρχικό διάνυσμα [1, 2, 3] στη μνήμη για να το μαζέψει ο συλλέκτης απορριμμάτων. Έτσι χρησιμοποιείται διπλάσια μνήμη από ό,τι στο προηγούμενο παράδειγμα, που επαναχρησιμοποιεί τη μνήμη που είχε δεσμευτεί για το αρχικό διάνυσμα.

Παρομοίως, υπάρχουν σχετικές λεπτομέρειες όταν κάνεις μετάδοση με διανύσματα ίδιου μεγέθους. Για παράδειγμα, με δύο διανύσματα v και w ίδιου μεγέθους:

  • Το v .= w παράγει ένα copy του w στη θέση μνήμης του v. Περαιτέρω αλλαγές στο v δεν επηρεάζουν το w και αντίστροφα.
  • Το v = w παράγει έναν ακόμη δείκτη στη θέση μνήμης του w με το όνομα v. Περαιτέρω αλλαγές στο v αντανακλώνται στο w και αντίστροφα.

Τελεστές χωρίς τελεία: μια προειδοποίηση

Αυτή η υποενότητα είναι αρκετά γεμάτη μαθηματικά, οπότε δεν αναμένεται από τους περισσότερους μαθητές να την καταλάβουν πραγματικά. Ωστόσο, είναι μια χρήσιμη προειδοποίηση που μπορεί να σε βοηθήσει στο debugging όταν βλέπεις απροσδόκητα μηνύματα σφάλματος.

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v * v
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})

# look, no commas
julia> u = [1 2 3]
1×3 Matrix{Int64}:
 1  2  3

julia> u * v
1-element Vector{Int64}:
 14

julia> v * u
3×3 Matrix{Int64}:
 1  2  3
 2  4  6
 3  6  9

Αν τυχαίνει να έχεις υπόβαθρο στη γραμμική άλγεβρα, τότε (1) δεν είσαι ένας τυπικός χρήστης του Exercism (αλλά είσαι πολύ ευπρόσδεκτος εδώ!) και (2) μπορεί να αναγνωρίσεις ότι το v είναι διάνυσμα στήλης, το u είναι διάνυσμα γραμμής, το u * v είναι το εσωτερικό γινόμενο και το v * u το εξωτερικό γινόμενο. Η Julia ακολουθεί τους κανόνες των μαθηματικών, εδώ όπως και σε όλα τα άλλα.

Για όλους τους υπόλοιπους: απλώς κατάλαβε γιατί σου συνιστούμε να χρησιμοποιείς πάντα τελεστές με τελεία για υπολογισμούς στοιχείο προς στοιχείο: το v .* v δουλεύει ακριβώς όπως θα περίμενες, δίνοντας [1, 4, 9].

Ευρετηρίαση

Η επιλογή στοιχείων ενός διανύσματος με βάση τη θέση έχει συζητηθεί σε προηγούμενες έννοιες.

a = collect('A':'Z')  # => 26-element Vector{Char}

# index with an integer
a[2]  # => 'B'

# index with a range
 a[12:2:18]  # => ['L', 'N', 'P, 'R']
 
 # index with another vector
 a[ [1, 3, 5] ]  # => ['A', 'C', 'E']

Λογική ευρετηρίαση

Είναι επίσης δυνατό να επιλέξεις στοιχεία που ικανοποιούν κάποια λογική έκφραση (τεχνικά, ένα "κατηγόρημα"). Αυτό συνήθως απαιτεί μετάδοση.

julia> a[a .< 'D']
3-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)

Σε πιο σύνθετες εκφράσεις οι τελείες τείνουν να πολλαπλασιάζονται (αλλά είναι μικρές και εύκολες στην πληκτρολόγηση).

julia> a[a .< 'D' .|| a .> 'W']
6-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
 'X': ASCII/Unicode U+0058 (category Lu: Letter, uppercase)
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'Z': ASCII/Unicode U+005A (category Lu: Letter, uppercase)

Μια υπενθύμιση ότι το "διάνυσμα" μπορεί στην πραγματικότητα να είναι οποιαδήποτε κατάλληλη διατεταγμένη δομή που μπορεί να διατραχθεί, όπως ένα διάστημα:

julia> n = 3:10
3:10

julia> n[isodd.(n)]
4-element Vector{Int64}:
 3
 5
 7
 9

Εσωτερικά, το κατηγόρημα μετατρέπεται σε ένα BitVector που στη συνέχεια χρησιμοποιείται για την επιλογή θέσεων.

julia> condition = a .< 'D'
26-element BitVector:
 1
 1
 1
 0
 # display truncated

julia> a[condition]
3-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα

Μάθε την έννοια Πράξεις με διανύσματα