De nombreux programmes ont besoin de valeurs (apparemment) aléatoires pour simuler des événements du monde réel.
Parmi les exemples courants et familiers, on trouve :
('H', 'T').Générer de véritables valeurs aléatoires avec un ordinateur est un défi technique étonnamment difficile, c'est pourquoi on désigne souvent ces résultats sous le nom de « pseudo-aléatoires ».
Important : ce concept ne couvre pas les nombres aléatoires cryptographiquement sûrs, qui représentent un défi bien plus difficile.
Toutefois, des bibliothèques bien conçues comme le module Random de la bibliothèque standard de Julia sont rapides, flexibles, et donnent des résultats largement suffisants pour la plupart des applications de modélisation, de simulation et de jeux.
Julia répartit les fonctionnalités aléatoires sur plusieurs endroits :
Base, qui sont toujours disponibles.Random.Random fait partie de la bibliothèque standard et est probablement préinstallé, mais tu devras ajouter using Random en haut de ton programme pour faire entrer son contenu dans l'espace de noms.
rand()
Ce que fait cette fonction dépend des arguments que tu lui donnes. Les options sont nombreuses.
Sans argument, elle génère un nombre décimal entre 0 (inclus) et 1.
Il s'agit d'une distribution uniform où toutes les valeurs ont la même probabilité, comme nous le verrons dans la section « Travailler avec les distributions », ci-dessous.
Un seul argument entier génère un vecteur de cette longueur.
julia> rand()
0.10261774967264703
julia> rand(5)
5-element Vector{Float64}:
0.24134501977563894
0.5664193284851202
0.9804412082089355
0.6229551330613335
0.47589221741904664
Pour obtenir une autre plage, il suffit de décaler et de mettre à l'échelle le résultat de façon appropriée.
L'exemple ci-dessous utilise le broadcasting pour la soustraction, abordé dans le concept Opérations vectorielles.
Le .- applique simplement cette arithmétique à chaque élément du vecteur.
# 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
Avec un type comme seul argument, rand utilise typemin et typemax comme bornes.
Ce n'est probablement pas ce que tu veux !
Pour des entiers aléatoires, on peut fournir une plage, et éventuellement le nombre de valeurs à générer.
julia> rand(Int64)
-9159538335234594326 # not very useful
julia> rand(1:10, 5)
5-element Vector{Int64}:
1
1
1
4
7
Dans l'exemple rand(1:10, 5) ci-dessus, remarque qu'il y a des valeurs répétées (par coïncidence), parce que chaque tirage est indépendant.
Il s'agit d'un « tirage avec remise », dont nous parlerons plus en détail ci-dessous.
Pour des nombres à virgule flottante dans une plage, tu devras généralement préciser un pas. Sinon, le pas vaudra 1.0 par défaut, ce qui est rarement utile.
julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
3.19
2.53
3.14
3.13
Sinon, fournis un tableau ou un tuple, et rand renverra une entrée aléatoire :
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"
Imagine que l'on a un sac contenant 3 boules rouges et 4 boules vertes, et que l'on tire une boule au hasard dans le sac. Pour obtenir une deuxième boule, il y a deux possibilités :
Le scénario 1 se fait avec remise, le scénario 2 sans remise, et ils donnent des résultats différents.
Pour simuler un tirage sans remise en Julia, il y a quelques options.
Le plus simple (et, dans Exercism, la seule option) : utiliser Random.shuffle() pour mettre les entrées dans un ordre aléatoire, puis prendre les n premiers éléments.
C'est très bien pour de petits problèmes, mais cela passe mal à l'échelle sur de grandes collections : shuffle doit générer le tableau entier, même si tu n'en veux qu'une petite fraction.
Pour faire un tirage avec remise « correctement », installe le paquet StatsBase.jl.
Celui-ci fournit la fonction sample() avec tout un éventail d'options.
On peut raisonnablement espérer que des fonctionnalités similaires seront intégrées à Random dans une future version, pour faire partie de la bibliothèque standard (les exemples de code de ce document ont été testés avec Julia 1.11).
Jusqu'ici, on s'est concentré sur les cas où tous les résultats ont la même probabilité.
Par exemple, rand(1:100) a autant de chances de donner n'importe quel entier de 1 à 100.
De nombreuses situations du monde réel sont bien moins simples que cela.
Par conséquent, les statisticiens ont créé une grande variété de distributions pour décrire mathématiquement des résultats « du monde réel ».
La fonction rand() décrite ci-dessus s'utilise quand toutes les probabilités sont égales.
On parle alors d'une distribution [uniform][uniform-distribution].
Appelée aussi distribution « normale » ou « courbe en cloche », c'est une façon très courante de décrire l'imprécision de valeurs mesurées.
Par exemple, suppose que l'usine où tu travailles vient d'acheter 10 000 boulons censés être identiques.
Tu veux programmer le robot de l'usine pour qu'il les manipule, alors tu pèses un échantillon de 100 boulons et tu constates qu'ils ont un poids moyen (ou mean) de 4,731 g.
Il est extrêmement improbable que cela signifie qu'ils pèsent tous exactement 4,731 g.
Peut-être constates-tu que les valeurs vont de 4,627 à 4,794 g mais se regroupent autour de 4,731 g.
Il s'agit de la Gaussian distribution, pour laquelle les probabilités culminent à la moyenne et diminuent symétriquement de part et d'autre (d'où la « courbe en cloche »).
Pour simuler cela dans un logiciel, il faut un moyen de préciser la largeur de la courbe (en général, des boulons chers se regroupent plus étroitement autour de la moyenne que des boulons bon marché !).
Par convention, on utilise pour cela l'standard deviation : des valeurs petites pour une courbe pointue et étroite, des valeurs grandes pour une courbe basse et large.
Les mathématiciens adorent les lettres grecques, donc on utilise μ (« mu ») pour représenter la moyenne et σ (« sigma ») pour représenter l'écart type.
Ainsi, si tu lis que « 95 % des valeurs se situent à moins de 2σ de μ » ou que « le boson de Higgs a été détecté avec une confiance de 5 sigma », ce genre de remarques se rapportent à l'écart type.
Nous en reparlerons dans le concept Statistics.
randn()
Abréviation de « random normal », elle est similaire à la variante à virgule flottante de rand(), sauf que les valeurs suivent une distribution gaussienne de moyenne 0 et d'écart type 1.
Là encore, tu voudras peut-être multiplier la sortie brute de randn pour régler l'écart type, et la décaler pour la moyenne.
L'exemple ci-dessous la convertit en une moyenne de 30 et un écart type de 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
Il est difficile de voir, à la simple lecture de la sortie, que les valeurs brutes se regroupent plus près de zéro que pour une distribution uniforme. Si tu en doutes, génère 1 000 valeurs ou plus et trace-les pour que ce soit plus évident.
Random
Ce module contient le niveau de fonctionnalités suivant, omis de Base pour contribuer à réduire la taille de la configuration par défaut de Julia.
Random complète rand et randn de Base avec des versions qui modifient leur argument, rand! et randn!.
Un ajout utile est randstring, qui génère une string d'une longueur donnée.
Par défaut, elle utilise des lettres majuscules et minuscules plus les chiffres de 0 à 9, mais d'autres choix peuvent être précisés.
julia> using Random
julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"
En outre, il existe une fonction bitrand qui génère un BitArray aléatoire d'une longueur donnée.
julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
1
1
0
0
1
Pour mélanger aléatoirement les entrées d'un Vector, on dispose de shuffle, et aussi de shuffle! pour modifier le vecteur d'entrée sur place.
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)
Parfois, il est utile d'obtenir plutôt les indices mélangés.
Pour cela, utilise randperm(n) où n est la longueur de la séquence.
julia> randperm(6)
6-element Vector{Int64}:
6
2
4
1
3
5
En pratique, l'exemple ci-dessus donne les mêmes résultats que shuffle(1:6).
Parmi les fonctions apparentées, on trouve randsubseq pour extraire des entrées avec une probabilité fixe, et randcycle pour les permutations cycliques.
Ces fonctions demandent des connaissances spécialisées, alors consulte la documentation si elles t'intéressent.
Plusieurs algorithmes de génération de nombres aléatoires (RNG) sont intégrés en standard dans Random, et toute personne dotée des compétences mathématiques adéquates peut en ajouter d'autres.
Ce genre de choses dépasse largement le cadre de ce document !
Une raison plus courante de travailler avec les RNG est de préciser une seed, ce qui a pour effet de rendre la séquence de sorties « aléatoires » reproductible d'une exécution à l'autre.
Une telle reproductibilité n'a pas sa place dans du code de production, mais elle peut faciliter les tests et le débogage.
En dehors d'Exercism, il existe de nombreux paquets installables liés au hasard, aux probabilités et aux statistiques.
Pour en savoir plus, voir le concept Statistics.
StatsBase.jl
La plupart des fonctions de StatsBase sont assez techniques et sans rapport avec ce document.
L'exception est StatsBase.sample, qui fournit une implémentation complète du tirage avec ou sans remise (voir la section précédente, plus haut).
Il existe aussi des fonctions pour le tirage pondéré (non uniforme).
Distributions.jl
Les distributions uniform et normal (ou gaussienne) ont été décrites plus haut.
Le module Random contient aussi randexp pour échantillonner selon la distribution exponentielle, qui est liée à la distribution de Poisson (très courante).
Pour un choix d'options bien plus large, il y a le paquet Distributions.jl, destiné à ceux qui ont les bases appropriées en statistiques.