Πολλά προγράμματα χρειάζονται (φαινομενικά) τυχαίες τιμές για να προσομοιώσουν συμβάντα του πραγματικού κόσμου.
Συνήθη και οικεία παραδείγματα περιλαμβάνουν:
('H', 'T').Η παραγωγή πραγματικά τυχαίων τιμών με έναν υπολογιστή είναι ένας εκπληκτικά δύσκολος τεχνικός στόχος, οπότε μπορεί να δεις αυτά τα αποτελέσματα να αναφέρονται ως "ψευδοτυχαία".
Σημαντικό: Αυτή η Έννοια δεν καλύπτει κρυπτογραφικά ασφαλείς τυχαίους αριθμούς, που αποτελούν ένα πολύ δυσκολότερο πρόβλημα.
Ωστόσο, καλά σχεδιασμένες βιβλιοθήκες όπως η ενότητα Random στην τυπική βιβλιοθήκη της Julia είναι γρήγορες, ευέλικτες και δίνουν αποτελέσματα που αρκούν απόλυτα για τις περισσότερες εφαρμογές σε μοντελοποίηση, προσομοίωση και παιχνίδια.
Η Julia χωρίζει τη λειτουργικότητα της τυχαιότητας σε πολλαπλά σημεία:
Base, που είναι πάντα διαθέσιμες.Random.Η Random αποτελεί μέρος της τυπικής βιβλιοθήκης και πιθανότατα είναι προεγκατεστημένη, αλλά θα χρειαστεί να προσθέσεις using Random στην αρχή του προγράμματός σου για να φέρεις τα περιεχόμενά της στον χώρο ονομάτων.
rand()
Το τι κάνει αυτή η συνάρτηση εξαρτάται από τα ορίσματα που της δίνεις. Υπάρχουν πολλές επιλογές.
Χωρίς ορίσματα, παράγει έναν αριθμό κινητής υποδιαστολής μεταξύ 0 (συμπεριλαμβανομένου) και 1.
Αυτή είναι μια κατανομή uniform όπου όλες οι τιμές είναι εξίσου πιθανές, όπως συζητείται στην ενότητα Εργασία με Κατανομές, παρακάτω.
Ένα μόνο ακέραιο όρισμα παράγει ένα διάνυσμα αυτού του μήκους.
julia> rand()
0.10261774967264703
julia> rand(5)
5-element Vector{Float64}:
0.24134501977563894
0.5664193284851202
0.9804412082089355
0.6229551330613335
0.47589221741904664
Για διαφορετικό εύρος, απλώς μετατόπισε και κλιμάκωσε κατάλληλα το αποτέλεσμα.
Το παρακάτω παράδειγμα χρησιμοποιεί broadcasting για την αφαίρεση, που καλύπτεται στην Έννοια Πράξεις με Διανύσματα.
Το .- εφαρμόζει απλώς αυτή την αριθμητική πράξη σε κάθε στοιχείο του διανύσματος.
# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
-0.5303906759076336
0.9635682226775855
-0.048823697086981754
0.465842804648374
0.9880834344780736
Με έναν τύπο ως μοναδικό όρισμα, η rand θα χρησιμοποιήσει τις typemin και typemax ως όρια.
Αυτό πιθανότατα δεν είναι αυτό που θέλεις!
Για τυχαίους ακέραιους, μπορούμε να δώσουμε ένα εύρος, και προαιρετικά πόσες τιμές να παραχθούν.
julia> rand(Int64)
-9159538335234594326 # not very useful
julia> rand(1:10, 5)
5-element Vector{Int64}:
1
1
1
4
7
Στο παραπάνω παράδειγμα rand(1:10, 5), πρόσεξε ότι υπάρχουν (κατά σύμπτωση) επαναλαμβανόμενες τιμές, επειδή κάθε επιλογή είναι ανεξάρτητη.
Αυτή είναι η "δειγματοληψία με επανατοποθέτηση", που συζητείται πιο αναλυτικά παρακάτω.
Για τιμές κινητής υποδιαστολής μέσα σε ένα εύρος, συνήθως θα χρειαστεί να ορίσεις ένα βήμα. Διαφορετικά, το βήμα θα πάρει προεπιλεγμένη τιμή 1.0, που σπάνια είναι χρήσιμο.
julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
3.19
2.53
3.14
3.13
Εναλλακτικά, δώσε έναν πίνακα ή μια πλειάδα, και η rand θα επιστρέψει ένα τυχαίο στοιχείο:
julia> rand([4, 9, 16, 25])
16
# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
1
"name"
Φαντάσου ότι έχουμε ένα σακούλι που περιέχει 3 κόκκινες μπάλες και 4 πράσινες, και τραβάμε τυχαία μια μπάλα από το σακούλι. Για να πάρουμε μια δεύτερη μπάλα, υπάρχουν δύο δυνατότητες:
Το σενάριο 1 είναι με επανατοποθέτηση, το σενάριο 2 χωρίς, και δίνουν διαφορετικά αποτελέσματα.
Για να προσομοιώσεις δειγματοληψία χωρίς επανατοποθέτηση στη Julia, υπάρχουν μερικές επιλογές.
Η απλούστερη (και στο Exercism η μόνη επιλογή), χρησιμοποίησε τη Random.shuffle() για να βάλεις τα στοιχεία σε τυχαία σειρά και μετά πάρε τα πρώτα n στοιχεία.
Αυτό είναι εντάξει για μικρά προβλήματα, αλλά μπορεί να μην κλιμακώνεται καλά σε μεγάλες συλλογές: η shuffle χρειάζεται να παράγει ολόκληρο τον πίνακα, ακόμα κι αν θέλεις μόνο ένα μικρό μέρος του.
Για να κάνεις δειγματοληψία με επανατοποθέτηση "σωστά", εγκατέστησε το πακέτο StatsBase.jl.
Αυτό παρέχει τη συνάρτηση sample() με ένα πλήρες φάσμα επιλογών.
Μπορούμε εύλογα να ελπίζουμε ότι παρόμοια λειτουργικότητα θα προστεθεί στη Random σε κάποια μελλοντική έκδοση, ώστε να γίνει μέρος της τυπικής βιβλιοθήκης (τα δείγματα κώδικα σε αυτό το κείμενο δοκιμάστηκαν με Julia 1.11).
Μέχρι τώρα έχουμε επικεντρωθεί σε περιπτώσεις όπου όλα τα αποτελέσματα είναι εξίσου πιθανά.
Για παράδειγμα, η rand(1:100) είναι εξίσου πιθανό να δώσει οποιονδήποτε ακέραιο από το 1 έως το 100.
Πολλές καταστάσεις του πραγματικού κόσμου είναι πολύ λιγότερο απλές από αυτό.
Ως αποτέλεσμα, οι στατιστικολόγοι έχουν δημιουργήσει μια μεγάλη ποικιλία από distributions για να περιγράψουν μαθηματικά τα αποτελέσματα του "πραγματικού κόσμου".
Η συνάρτηση rand() που περιγράφηκε παραπάνω χρησιμοποιείται όταν όλες οι πιθανότητες είναι ίσες.
Αυτή ονομάζεται κατανομή uniform.
Γνωστή και ως "κανονική" κατανομή ή "κωδωνοειδής" καμπύλη, αυτή είναι ένας πολύ συνηθισμένος τρόπος να περιγράψουμε την ανακρίβεια σε μετρούμενες τιμές.
Για παράδειγμα, ας υποθέσουμε ότι το εργοστάσιο όπου δουλεύεις μόλις αγόρασε 10.000 μπουλόνια που υποτίθεται ότι είναι πανομοιότυπα.
Θέλεις να ρυθμίσεις το ρομπότ του εργοστασίου να τα χειρίζεται, οπότε ζυγίζεις ένα δείγμα 100 κομματιών και βρίσκεις ότι έχουν μέσο (ή mean) βάρος 4,731g.
Είναι εξαιρετικά απίθανο αυτό να σημαίνει ότι όλα ζυγίζουν ακριβώς 4,731g.
Ίσως διαπιστώσεις ότι οι τιμές κυμαίνονται από 4,627 έως 4,794g αλλά συγκεντρώνονται γύρω από το 4,731g.
Αυτή είναι η Gaussian distribution, για την οποία οι πιθανότητες κορυφώνονται στον μέσο όρο και μειώνονται συμμετρικά και από τις δύο πλευρές (εξ ου και "κωδωνοειδής").
Για να το προσομοιώσουμε σε λογισμικό, χρειαζόμαστε κάποιον τρόπο να ορίσουμε το πλάτος της καμπύλης (συνήθως, τα ακριβά μπουλόνια συγκεντρώνονται πιο στενά γύρω από τον μέσο όρο από τα φθηνά!).
Κατά σύμβαση, αυτό γίνεται με τη standard deviation: μικρές τιμές για μια απότομη, στενή καμπύλη, μεγάλες για μια χαμηλή, πλατιά καμπύλη.
Οι μαθηματικοί λατρεύουν τα ελληνικά γράμματα, οπότε χρησιμοποιούμε το μ ("μι") για να αναπαραστήσουμε τον μέσο όρο και το σ ("σίγμα") για να αναπαραστήσουμε την τυπική απόκλιση.
Έτσι, αν διαβάσεις ότι "το 95% των τιμών βρίσκεται εντός 2σ από το μ" ή "το μποζόνιο Higgs ανιχνεύτηκε με βεβαιότητα 5 σίγμα", τέτοια σχόλια σχετίζονται με την τυπική απόκλιση.
Θα υπάρχουν περισσότερα να πούμε για αυτό στην Έννοια Statistics.
randn()
Συντομογραφία του "random normal", είναι παρόμοια με την παραλλαγή κινητής υποδιαστολής της rand(), με τη διαφορά ότι οι τιμές κατανέμονται κατά Gauss με μέσο όρο 0 και τυπική απόκλιση 1.
Και πάλι, μπορεί να θέλεις να κλιμακώσεις την ακατέργαστη έξοδο της randn για την τυπική απόκλιση και να τη μετατοπίσεις για τον μέσο όρο.
Το παρακάτω παράδειγμα τη μετατρέπει σε μέσο όρο 30 και τυπική απόκλιση 5.
julia> raw = randn(5)
5-element Vector{Float64}:
3.0762588867281475
1.5101100620253902
-0.5914858221637778
0.684175554069735
-0.8416433926114673
julia> raw * 5 .+ 30
5-element Vector{Float64}:
45.38129443364074
37.55055031012695
27.04257088918111
33.420877770348675
25.791783036942665
Είναι δύσκολο να καταλάβεις κοιτάζοντας την έξοδο ότι η ακατέργαστη έξοδος συγκεντρώνεται πιο κοντά στο μηδέν από ό,τι για μια ομοιόμορφη κατανομή. Αν αμφιβάλλεις, παρήγαγε 1000 ή περισσότερες και σχεδίασέ τις για να γίνει πιο προφανές.
Random
Αυτή η ενότητα περιέχει το επόμενο επίπεδο λειτουργικότητας, που παραλείπεται από το Base για να κρατήσει μικρό το μέγεθος της προεπιλεγμένης διαμόρφωσης της Julia.
Η Random συμπληρώνει τις rand και randn στο Base με παραλλαγές που μεταλλάσσουν, τις rand! και randn!.
Μια χρήσιμη προσθήκη είναι η randstring, που παράγει μια συμβολοσειρά δεδομένου μήκους.
Από προεπιλογή, αυτή χρησιμοποιεί πεζά και κεφαλαία γράμματα συν τα ψηφία 0 έως 9, αλλά μπορούν να οριστούν και άλλες επιλογές.
julia> using Random
julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"
Επιπλέον, υπάρχει μια συνάρτηση bitrand που παράγει έναν τυχαίο BitArray δεδομένου μήκους.
julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
1
1
0
0
1
Για να ανακατέψουμε τυχαία τα στοιχεία ενός Vector, έχουμε τη shuffle, καθώς και τη shuffle! για να μεταλλάξουμε το διάνυσμα εισόδου επιτόπου.
julia> v = ['A', '1', '2', 'J', 'Q', 'K'];
julia> shuffle(v)
6-element Vector{Char}:
'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
Μερικές φορές είναι χρήσιμο να έχουμε αντ' αυτού τις ανακατεμένες θέσεις.
Για αυτό, χρησιμοποίησε τη randperm(n), όπου n είναι το μήκος της ακολουθίας.
julia> randperm(6)
6-element Vector{Int64}:
6
2
4
1
3
5
Στην πράξη, το παραπάνω παράδειγμα δίνει τα ίδια αποτελέσματα με τη shuffle(1:6).
Η Mary είναι μεγάλη θαυμάστρια της τηλεοπτικής σειράς Star Trek: The Next Generation. Παίζει συχνά παιχνίδια ρόλων με χαρτί και μολύβι, όπου αυτή και οι φίλοι της υποδύονται το πλήρωμα του Starship Enterprise. Ο χαρακτήρας της Mary είναι ο Πλοίαρχος Picard, που σημαίνει ότι πρέπει να κρατάει το ημερολόγιο του πλοιάρχου. Λατρεύει το δημιουργικό κομμάτι του παιχνιδιού, αλλά δεν της αρέσει να βγάζει τυχαία δεδομένα επιτόπου.
Βοήθησε τη Mary δημιουργώντας γεννήτορες τυχαίων δεδομένων για δεδομένα που εμφανίζονται συχνά στο ημερολόγιο του πλοιάρχου.
Το Starship Enterprise συναντά πολλούς πλανήτες στα ταξίδια του. Οι πλανήτες στο σύμπαν του Star Trek χωρίζονται σε κατηγορίες με βάση τις ιδιότητές τους. Για παράδειγμα, η Γη είναι πλανήτης κλάσης M. Όλες οι δυνατές κλάσεις πλανητών είναι: D, H, J, K, L, M, N, R, T και Y.
Υλοποίησε τη συνάρτηση random_planet. Θα πρέπει να επιστρέφει τυχαία μία από τις κλάσεις πλανητών.
julia> random_planet_class()
'K'
Το Enterprise (αριθμός νηολογίου NCC-1701) δεν είναι το μόνο διαστημόπλοιο που πετάει εκεί έξω! Όταν συναντιέται με άλλο διαστημόπλοιο, η Mary πρέπει να καταγράψει τον αριθμό νηολογίου αυτού του διαστημοπλοίου.
Οι αριθμοί νηολογίου ξεκινούν με το πρόθεμα "NCC-" και μετά χρησιμοποιούν έναν αριθμό από το 1000 έως το 9999 (συμπεριλαμβανομένων).
Υλοποίησε τη συνάρτηση random_ship_registry_number που επιστρέφει έναν τυχαίο αριθμό νηολογίου διαστημοπλοίου.
julia> random_ship_registry_number()
"NCC-1947"
Τι νόημα έχει ένα ημερολόγιο αν δεν περιλαμβάνει ημερομηνίες;
Ένα stardate είναι ένας αριθμός κινητής υποδιαστολής. Οι περιπέτειες του Starship Enterprise από την πρώτη σεζόν του The Next Generation διαδραματίζονται μεταξύ των stardate 41000.0 και 42000.0. Το "4" αντιστοιχεί στον 24ο αιώνα, το "1" στην πρώτη σεζόν.
Υλοποίησε τη συνάρτηση random_stardate που επιστρέφει έναν αριθμό κινητής υποδιαστολής μεταξύ 41000.0 (συμπεριλαμβανομένου) και 42000.0.
julia> random_stardate()
41458.15721310934
Έχουν υπάρξει παράπονα ότι τα stardate έχουν πάρα πολλά δεκαδικά ψηφία, οπότε η Mary αποφασίζει να στρογγυλοποιεί τις εγγραφές του ημερολογίου στο ένα δεκαδικό ψηφίο.
Αν και αυτό θα μπορούσε να γίνει στρογγυλοποιώντας τιμές από το random_stardate(), συνειδητοποιείς ότι υπάρχει μια πολύ απλούστερη προσέγγιση.
Υλοποίησε τη συνάρτηση random_stardate_v2 που επιστρέφει έναν αριθμό κινητής υποδιαστολής μεταξύ 41000.0 και 42000.0 (συμπεριλαμβανομένων), με ένα μόνο δεκαδικό ψηφίο.
julia> random_stardate_v2()
41732.6
Μια εκδοχή του παιχνιδιού περιλαμβάνει αλληλεπίδραση με άλλα διαστημόπλοια, και η Mary χρειάζεται μια επιλογή.
Υπάρχει μια πλήρης λίστα με πιθανά διαστημόπλοια, αλλά σε κάθε παιχνίδι χρειάζονται μόνο μερικά από αυτά, και θα πρέπει να επιλέγονται τυχαία.
Υλοποίησε τη συνάρτηση pick_starships(starships, number_needed), η οποία παίρνει ένα διάνυσμα με μοναδικούς αριθμούς νηολογίου διαστημοπλοίων και επιστρέφει ένα διάνυσμα μήκους number_needed.
Οι τιμές του αποτελέσματος θα πρέπει να προέρχονται από το starships, χωρίς διπλότυπα.
julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
"NCC-1228"
"NCC-5011"
Περιορίσαμε το όρισμα starships ώστε να είναι διάνυσμα.
Πώς θα υλοποιούσες την pick_starships() ώστε να δουλεύει με άλλα επαναλήψιμα, όπως πλειάδες ή σύνολα;
Δεν υπάρχουν tests για αυτό το μέρος.
Γράψου στο Exercism για να μάθεις και να κατακτήσεις Julia με 35 έννοιες128 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.