BlessRNG ou RNG vĂ©rifier l'honnĂȘtetĂ©



Dans gamedev, vous devez souvent attacher quelque chose sur une maison aléatoire: Unity a son propre Random pour cela, et System.Random existe en parallÚle avec lui. Il était une fois un des projets, il semblait que les deux pouvaient fonctionner différemment (bien qu'ils devraient avoir une distribution uniforme).

Ensuite, ils ne sont pas entrĂ©s dans les dĂ©tails - il suffisait que la transition vers System.Random ait rĂ©solu tous les problĂšmes. Maintenant, nous avons dĂ©cidĂ© de comprendre plus en dĂ©tail et de mener une petite recherche: comment les RNG sont «biaisĂ©s» ou prĂ©visibles, et lequel choisir. De plus, j’ai souvent entendu des opinions contradictoires sur leur «honnĂȘteté» - essayons de comprendre comment les rĂ©sultats rĂ©els se rapportent Ă  ceux dĂ©clarĂ©s.

Un bref programme Ă©ducatif ou RNG est en fait un PRNG


Si vous connaissez déjà les générateurs de nombres aléatoires, vous pouvez immédiatement passer à la section "Test".

Les nombres aléatoires (MF) sont une séquence de nombres générés à l'aide d'un processus aléatoire (chaotique), une source d'entropie. Autrement dit, c'est une telle séquence, dont les éléments ne sont liés par aucune loi mathématique - ils n'ont pas de relation causale.

Ce qui crĂ©e un milieu de gamme s'appelle un gĂ©nĂ©rateur de nombres alĂ©atoires (RNG). Il semblerait que tout soit Ă©lĂ©mentaire, mais si nous passons de la thĂ©orie Ă  la pratique, la mise en Ɠuvre d'un algorithme logiciel pour gĂ©nĂ©rer une telle sĂ©quence n'est pas si simple.

La raison rĂ©side en l'absence de l'alĂ©atoire mĂȘme dans l'Ă©lectronique grand public moderne. Sans cela, les nombres alĂ©atoires cessent d'ĂȘtre alĂ©atoires et leur gĂ©nĂ©rateur se transforme en une fonction ordinaire d'arguments dĂ©libĂ©rĂ©ment dĂ©terminĂ©s. Pour un certain nombre de spĂ©cialitĂ©s dans le domaine informatique, il s'agit d'un problĂšme sĂ©rieux (par exemple, pour la cryptographie), pour le reste, il existe une solution parfaitement acceptable.

Nous devons Ă©crire un algorithme qui retournerait, mĂȘme s'il ne s'agit pas vraiment de nombres alĂ©atoires, mais aussi prĂšs que possible d'eux - les soi-disant nombres pseudo-alĂ©atoires (PSN). Dans ce cas, l'algorithme est appelĂ© gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires (PRNG).

Il existe plusieurs options pour créer un PRNG, mais pour tous, les éléments suivants seront pertinents:

  1. La nécessité d'une pré-initialisation.

    Le PRNG est dépourvu de source d'entropie, par conséquent, avant utilisation, il doit indiquer l'état initial. Il est spécifié sous forme de nombre (ou vecteur) et est appelé une graine (graine, graine aléatoire). Souvent, un compteur d'horloge de processeur ou l'équivalent numérique du temps systÚme est utilisé comme valeur de départ.
  2. Reproductibilité de la séquence.

    Le PRNG est complĂštement dĂ©terministe, de sorte que la graine spĂ©cifiĂ©e lors de l'initialisation dĂ©termine de maniĂšre unique la future sĂ©quence entiĂšre de nombres. Cela signifie qu'un seul DSRP, initialisĂ© avec la mĂȘme graine (Ă  diffĂ©rents moments, dans diffĂ©rents programmes, sur diffĂ©rents appareils) gĂ©nĂ©rera la mĂȘme sĂ©quence.

Vous devez également connaßtre la distribution de probabilité caractérisant le PRNG - quels nombres il générera et avec quelle probabilité. Le plus souvent, il s'agit soit d'une distribution normale, soit d'une distribution uniforme.

Distribution normale (gauche) et distribution uniforme (droite)

Disons que nous avons un dĂ© honnĂȘte avec 24 visages. Si vous le laissez tomber, la probabilitĂ© de chute d'une unitĂ© sera de 1/24 (ainsi que la probabilitĂ© de chute d'un autre nombre). Si vous faites beaucoup de lancers et enregistrez les rĂ©sultats, vous remarquerez que tous les visages tombent Ă  peu prĂšs Ă  la mĂȘme frĂ©quence. En fait, ces dĂ©s peuvent ĂȘtre considĂ©rĂ©s comme un RNG avec une distribution uniforme.

Et si vous jetez immĂ©diatement 10 de ces os et comptez le nombre total de points? L'uniformitĂ© sera-t-elle maintenue pour elle? Non. Le plus souvent, le montant sera proche de 125 points, soit une valeur moyenne. Et par consĂ©quent - avant mĂȘme de lancer, vous pouvez approximativement estimer le rĂ©sultat futur.

La raison en est que pour obtenir le nombre moyen de points, il y a le plus grand nombre de combinaisons. Plus on s'en Ă©loigne, moins il y a de combinaisons - et, par consĂ©quent, moins de risques de perte. Si vous visualisez ces donnĂ©es, elles ressembleront Ă  distance Ă  la forme d'une cloche. Par consĂ©quent, avec un peu d'Ă©tirement, un systĂšme de 10 os peut ĂȘtre appelĂ© un RNG avec une distribution normale.

Un autre exemple, seulement déjà dans l'avion - tir à la cible. Le tireur sera le RNG générant une paire de nombres (x, y), qui est affiché sur le graphique.

Convenez que l'option de gauche est plus proche de la vie réelle - il s'agit d'un RNG avec une distribution normale. Mais si vous avez besoin de disperser des étoiles dans un ciel sombre, la bonne option, obtenue à l'aide d'un RNG avec une distribution uniforme, est meilleure. En général, choisissez un générateur en fonction de la tùche.

Parlons maintenant de l'entropie de la séquence PSP. Par exemple, il existe une séquence qui commence comme ceci:

89, 93, 33, 32, 82, 21, 4, 42, 11, 8, 60, 95, 53, 30, 42, 19, 34, 35, 62, 23, 44, 38, 74, 36, 52, 18, 58, 79, 65, 45, 99, 90, 82, 20, 41, 13, 88, 76, 82, 24, 5, 54, 72, 19, 80, 2, 74, 36, 71, 9, ...

Dans quelle mesure ces chiffres sont-ils aléatoires à premiÚre vue? Commençons par vérifier la distribution.

Cela ressemble à de l'uniforme, mais si vous lisez la séquence de deux nombres et les interprétez comme des coordonnées dans le plan, vous obtenez ceci:

Les motifs sont clairement visibles. Et puisque les donnĂ©es de la sĂ©quence sont ordonnĂ©es d'une certaine maniĂšre (c'est-Ă -dire qu'elles ont une faible entropie), cela peut conduire au «biais» mĂȘme. Au minimum, un tel PRNG n'est pas trĂšs appropriĂ© pour gĂ©nĂ©rer des coordonnĂ©es sur un plan.

Une autre séquence:

42, 72, 17, 0, 30, 0, 15, 9, 47, 19, 35, 86, 40, 54, 97, 42, 69, 19, 20, 88, 4, 3, 67, 27, 42, 56, 17, 14, 20, 40, 80, 97, 1, 31, 69, 13, 88, 89, 76, 9, 4, 85, 17, 88, 70, 10, 42, 98, 96, 53, ...

Tout semble aller bien ici mĂȘme dans l'avion:

Voyons en volume (nous lisons trois chiffres chacun):

Et encore une fois les motifs. La visualisation de la construction en quatre dimensions ne fonctionnera pas. Mais des motifs peuvent exister Ă  la fois sur cette dimension et sur les grands.

Dans la mĂȘme cryptographie, oĂč les exigences les plus strictes sont imposĂ©es au PRNG, une telle situation est catĂ©goriquement inacceptable. Par consĂ©quent, pour Ă©valuer leur qualitĂ©, des algorithmes spĂ©ciaux ont Ă©tĂ© dĂ©veloppĂ©s, que nous n'aborderons pas maintenant. Le sujet est vaste et s'inspire d'un article distinct.

Test


Si nous ne savons pas quelque chose avec certitude, alors comment travailler avec cela? Vaut-il la peine de traverser la route si vous ne savez pas quel feu il autorise? Les consĂ©quences peuvent ĂȘtre diffĂ©rentes.

Il en va de mĂȘme pour l'alĂ©atoire notoire dans Unity. Eh bien, si la documentation rĂ©vĂšle les dĂ©tails nĂ©cessaires, mais l'histoire mentionnĂ©e au dĂ©but de l'article s'est produite juste en raison du manque de dĂ©tails souhaitĂ©s.

Et sans savoir comment l'outil fonctionne, vous ne pouvez pas l'appliquer correctement. En général, le moment est venu de vérifier et de mener une expérience pour enfin s'assurer au moins au détriment de la distribution.

La solution était simple et efficace: collecter des statistiques, obtenir des données objectives et consulter les résultats.

Sujet de recherche


Il existe plusieurs façons de générer des nombres aléatoires dans Unity - nous en avons testé cinq.

  1. System.Random.Next (). GénÚre des entiers dans une plage de valeurs donnée.
  2. System.Random.NextDouble (). GénÚre des nombres à double précision (double) dans la plage de [0; 1).
  3. UnityEngine.Random.Range (). GénÚre des nombres à simple précision (float) dans une plage de valeurs donnée.
  4. UnityEngine.Random.value. GénÚre des nombres à simple précision (float) dans la plage de [0; 1).
  5. Unity.Mathematics.Random.NextFloat (). Fait partie de la nouvelle bibliothÚque Unity.Mathematics. GénÚre des nombres à simple précision (float) dans une plage de valeurs donnée.

Presque partout dans la documentation, une distribution uniforme a Ă©tĂ© indiquĂ©e, Ă  l'exception de UnityEngine.Random.value (oĂč la distribution n'est pas spĂ©cifiĂ©e, mais de maniĂšre similaire Ă  UnityEngine.Random.Range (), elle devait Ă©galement ĂȘtre uniforme) et Unity.Mathematics.Random.NextFloat () (oĂč dans la base est l'algorithme xorshift, ce qui signifie que vous devez Ă  nouveau attendre une distribution uniforme).

Par défaut, ceux attendus dans la documentation ont été pris pour les résultats attendus.

MĂ©thodologie


Nous avons écrit une petite application qui a généré des séquences de nombres aléatoires dans chacune des méthodes présentées et enregistré les résultats pour un traitement ultérieur.

La longueur de chaque séquence est de 100 000 numéros.
La plage de nombres aléatoires est [0, 100).

Les données ont été collectées à partir de plusieurs plateformes cibles:

  • Windows
    - Unity v2018.3.14f1, mode Éditeur, Mono, .NET Standard 2.0
  • macOS
    - Unity v2018.3.14f1, mode Éditeur, Mono, .NET Standard 2.0
    - Unity v5.6.4p4, mode Éditeur, Mono, .NET Standard 2.0
  • Android
    - Unity v2018.3.14f1, assemblage sur l'appareil, Mono, .NET Standard 2.0
  • iOS
    - Unity v2018.3.14f1, build to device, il2cpp, .NET Standard 2.0

Implémentation


Nous avons plusieurs façons de générer des nombres aléatoires. Pour chacun d'eux, nous allons écrire une classe wrapper distincte, qui devrait fournir:

  1. Possibilité de définir la plage de valeurs [min / max). Il sera défini par le constructeur.
  2. Méthode de retour de milieu de gamme. Nous choisirons float comme type, comme type plus général.
  3. Nom de la méthode de génération pour marquer les résultats. Pour plus de commodité, nous retournerons le nom complet de la classe + le nom de la méthode utilisée pour générer le milieu de gamme comme valeur.

Tout d'abord, déclarez une abstraction, qui sera représentée par l'interface IRandomGenerator:

namespace RandomDistribution { public interface IRandomGenerator { string Name { get; } float Generate(); } } 

Implémentation de System.Random.Next ()


Cette méthode vous permet de spécifier une plage de valeurs, mais elle renvoie des entiers et un flottant est nécessaire. Vous pouvez simplement interpréter l'entier comme un flottant, ou vous pouvez étendre la plage de valeurs de plusieurs ordres de grandeur, en les compensant chaque fois que le milieu de gamme est généré. Il se révélera quelque chose comme virgule fixe avec la précision spécifiée. Nous utiliserons cette option, car elle est plus proche de la valeur flottante réelle.

 using System; namespace RandomDistribution { public class SystemIntegerRandomGenerator : IRandomGenerator { private const int DefaultFactor = 100000; private readonly Random _generator = new Random(); private readonly int _min; private readonly int _max; private readonly int _factor; public string Name => "System.Random.Next()"; public SystemIntegerRandomGenerator(float min, float max, int factor = DefaultFactor) { _min = (int)min * factor; _max = (int)max * factor; _factor = factor; } public float Generate() => (float)_generator.Next(_min, _max) / _factor; } } l' using System; namespace RandomDistribution { public class SystemIntegerRandomGenerator : IRandomGenerator { private const int DefaultFactor = 100000; private readonly Random _generator = new Random(); private readonly int _min; private readonly int _max; private readonly int _factor; public string Name => "System.Random.Next()"; public SystemIntegerRandomGenerator(float min, float max, int factor = DefaultFactor) { _min = (int)min * factor; _max = (int)max * factor; _factor = factor; } public float Generate() => (float)_generator.Next(_min, _max) / _factor; } } du using System; namespace RandomDistribution { public class SystemIntegerRandomGenerator : IRandomGenerator { private const int DefaultFactor = 100000; private readonly Random _generator = new Random(); private readonly int _min; private readonly int _max; private readonly int _factor; public string Name => "System.Random.Next()"; public SystemIntegerRandomGenerator(float min, float max, int factor = DefaultFactor) { _min = (int)min * factor; _max = (int)max * factor; _factor = factor; } public float Generate() => (float)_generator.Next(_min, _max) / _factor; } } 

Implémentation de System.Random.NextDouble ()


Voici une plage fixe de valeurs [0; 1). Pour le projeter sur celui spécifié dans le constructeur, nous utilisons une arithmétique simple: X * (max - min) + min.

 using System; namespace RandomDistribution { public class SystemDoubleRandomGenerator : IRandomGenerator { private readonly Random _generator = new Random(); private readonly double _factor; private readonly float _min; public string Name => "System.Random.NextDouble()"; public SystemDoubleRandomGenerator(float min, float max) { _factor = max - min; _min = min; } public float Generate() => (float)(_generator.NextDouble() * _factor) + _min; } } l' using System; namespace RandomDistribution { public class SystemDoubleRandomGenerator : IRandomGenerator { private readonly Random _generator = new Random(); private readonly double _factor; private readonly float _min; public string Name => "System.Random.NextDouble()"; public SystemDoubleRandomGenerator(float min, float max) { _factor = max - min; _min = min; } public float Generate() => (float)(_generator.NextDouble() * _factor) + _min; } } du using System; namespace RandomDistribution { public class SystemDoubleRandomGenerator : IRandomGenerator { private readonly Random _generator = new Random(); private readonly double _factor; private readonly float _min; public string Name => "System.Random.NextDouble()"; public SystemDoubleRandomGenerator(float min, float max) { _factor = max - min; _min = min; } public float Generate() => (float)(_generator.NextDouble() * _factor) + _min; } } 

Implémentation de UnityEngine.Random.Range ()


Cette méthode de la classe statique UnityEngine.Random vous permet de spécifier une plage de valeurs et renvoie un milieu de gamme de type float. Aucune transformation supplémentaire n'est nécessaire.

 using UnityEngine; namespace RandomDistribution { public class UnityRandomRangeGenerator : IRandomGenerator { private readonly float _min; private readonly float _max; public string Name => "UnityEngine.Random.Range()"; public UnityRandomRangeGenerator(float min, float max) { _min = min; _max = max; } public float Generate() => Random.Range(_min, _max); } } 

Implémentation de UnityEngine.Random.value


La propriĂ©tĂ© value de la classe statique UnityEngine.Random renvoie un milieu de gamme de type float Ă  partir d'une plage fixe de valeurs [0; 1). Nous le projetons sur une plage donnĂ©e de la mĂȘme maniĂšre que lors de l'implĂ©mentation de System.Random.NextDouble ().

 using UnityEngine; namespace RandomDistribution { public class UnityRandomValueGenerator : IRandomGenerator { private readonly float _factor; private readonly float _min; public string Name => "UnityEngine.Random.value"; public UnityRandomValueGenerator(float min, float max) { _factor = max - min; _min = min; } public float Generate() => (float)(Random.value * _factor) + _min; } } 

Implémentation de Unity.Mathematics.Random.NextFloat ()


La mĂ©thode NextFloat () de la classe Unity.Mathematics.Random renvoie un milieu de gamme de type float et vous permet de spĂ©cifier une plage de valeurs. La seule nuance est que chaque instance de Unity.Mathematics.Random devra ĂȘtre initialisĂ©e avec une graine - de cette façon, nous Ă©viterons de gĂ©nĂ©rer des sĂ©quences rĂ©pĂ©tĂ©es.

 using Unity.Mathematics; namespace RandomDistribution { public class UnityMathematicsRandomValueGenerator : IRandomGenerator { private Random _generator; private readonly float _min; private readonly float _max; public string Name => "Unity.Mathematics.Random.NextFloat()"; public UnityMathematicsRandomValueGenerator(float min, float max) { _min = min; _max = max; _generator = new Random(); _generator.InitState(unchecked((uint)System.DateTime.Now.Ticks)); } public float Generate() => _generator.NextFloat(_min, _max); } } 

Implémentation de MainController


Plusieurs implĂ©mentations IRandomGenerator sont prĂȘtes. Ensuite, vous devez gĂ©nĂ©rer des sĂ©quences et enregistrer l'ensemble de donnĂ©es rĂ©sultant pour le traitement. Pour ce faire, crĂ©ez une scĂšne dans Unity et un petit script MainController, qui effectuera tout le travail nĂ©cessaire et sera simultanĂ©ment responsable de l'interaction avec l'interface utilisateur.

Nous dĂ©finissons la taille de l'ensemble de donnĂ©es et la plage de valeurs de milieu de gamme, et obtenons Ă©galement une mĂ©thode qui renvoie un tableau de gĂ©nĂ©rateurs optimisĂ©s et prĂȘts Ă  l'emploi.

 namespace RandomDistribution { public class MainController : MonoBehaviour { private const int DefaultDatasetSize = 100000; public float MinValue = 0f; public float MaxValue = 100f; ... private IRandomGenerator[] CreateRandomGenerators() { return new IRandomGenerator[] { new SystemIntegerRandomGenerator(MinValue, MaxValue), new SystemDoubleRandomGenerator(MinValue, MaxValue), new UnityRandomRangeGenerator(MinValue, MaxValue), new UnityRandomValueGenerator(MinValue, MaxValue), new UnityMathematicsRandomValueGenerator(MinValue, MaxValue) }; } ... } } 

Et maintenant, nous formons un ensemble de données. Dans ce cas, la génération des données sera combinée avec l'enregistrement des résultats dans un flux de texte (au format csv). Pour stocker les valeurs de chaque IRandomGenerator, une colonne distincte est allouée et la premiÚre ligne contient le nom du générateur.

 namespace RandomDistribution { public class MainController : MonoBehaviour { ... private void GenerateCsvDataSet(TextWriter writer, int dataSetSize, params IRandomGenerator[] generators) { const char separator = ','; int lastIdx = generators.Length - 1; // write header for (int j = 0; j <= lastIdx; j++) { writer.Write(generators[j].Name); if (j != lastIdx) writer.Write(separator); } writer.WriteLine(); // write data for (int i = 0; i <= dataSetSize; i++) { for (int j = 0; j <= lastIdx; j++) { writer.Write(generators[j].Generate()); if (j != lastIdx) writer.Write(separator); } if (i != dataSetSize) writer.WriteLine(); } } ... } } 

Il reste à appeler la méthode GenerateCsvDataSet et à enregistrer le résultat dans un fichier, ou à transférer immédiatement les données sur le réseau de l'appareil final vers le serveur de réception.

 namespace RandomDistribution { public class MainController : MonoBehaviour { ... public void GenerateCsvDataSet(string path, int dataSetSize, params IRandomGenerator[] generators) { using (var writer = File.CreateText(path)) { GenerateCsvDataSet(writer, dataSetSize, generators); } } public string GenerateCsvDataSet(int dataSetSize, params IRandomGenerator[] generators) { using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) { GenerateCsvDataSet(writer, dataSetSize, generators); return writer.ToString(); } } ... } } 

Les sources du projet sont sur GitLab .

RĂ©sultats


Aucun miracle ne s'est produit. Ce Ă  quoi ils s'attendaient, ils l'ont compris - dans tous les cas, une distribution uniforme sans la moindre trace de complot. Je ne vois pas l’intĂ©rĂȘt d’appliquer des graphiques sĂ©parĂ©s sur les plates-formes - ils affichent tous approximativement les mĂȘmes rĂ©sultats.

La réalité est:


Visualisation de séquences sur un plan à partir des cinq méthodes de génération:


Et la visualisation en 3D. Je ne laisserai que le rĂ©sultat de System.Random.Next (), afin de ne pas produire un tas du mĂȘme contenu.


L'histoire racontée dans l'introduction sur la distribution normale d'UnityEngine.Random ne s'est pas répétée: soit elle était initialement erronée, soit quelque chose a changé dans le moteur depuis. Mais maintenant, nous en sommes sûrs.

Source: https://habr.com/ru/post/fr473880/


All Articles