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

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

Dans le développement de jeux, il est souvent nécessaire de baser certaines choses sur le hasard : Unity dispose de son propre Random, tandis que System.Random existe également. Il y a longtemps, lors d'un projet, j'ai eu l'impression que les deux pouvaient agir différemment (bien qu'ils devraient avoir une distribution uniforme).

À l'Ă©poque, nous ne nous sommes pas attardĂ©s sur les dĂ©tails — il suffisait de constater que le passage Ă  System.Random avait corrigĂ© tous les problĂšmes. Maintenant, nous avons dĂ©cidĂ© d'explorer cela plus en profondeur et de mener une petite investigation : Ă  quel point les gĂ©nĂ©rateurs de nombres alĂ©atoires sont-ils « biaisĂ©s » ou prĂ©visibles, et lequel choisir. D'autant plus que j'ai entendu plusieurs avis contradictoires sur leur « honnĂȘtetĂ© » — essayons de comprendre comment les rĂ©sultats rĂ©els se comparent Ă  ceux annoncĂ©s.

Cours bref ou en réalité, un générateur de nombres aléatoires (GNA) est un générateur de pseudo-nombres aléatoires.

Si vous ĂȘtes dĂ©jĂ  familier avec les gĂ©nĂ©rateurs de nombres alĂ©atoires, vous pouvez passer directement Ă  la section « Test ».

Les nombres alĂ©atoires (NA) sont une sĂ©quence de nombres gĂ©nĂ©rĂ©e par un processus alĂ©atoire (chaotique) ou une source d'entropie. En d'autres termes, il s'agit d'une sĂ©quence dont les Ă©lĂ©ments ne sont pas liĂ©s les uns aux autres par une loi mathĂ©matique — ils n'ont pas de lien de cause Ă  effet.

Ce qui génÚre des NA s'appelle un générateur de nombres aléatoires (GNA). Cela semble élémentaire, mais en passant de la théorie à la pratique, il n'est en réalité pas si simple de réaliser un algorithme de génération de cette séquence.

La raison rĂ©side dans l'absence de cette chaos dans l'Ă©lectronique grand public moderne. Sans cela, les nombres alĂ©atoires cessent d'ĂȘtre alĂ©atoires, et leur gĂ©nĂ©rateur devient une simple fonction de paramĂštres dĂ©terminĂ©s Ă  l'avance. Pour un certain nombre de spĂ©cialitĂ©s dans le domaine de l'informatique, c'est un vĂ©ritable problĂšme (par exemple pour la cryptographie), tandis que pour les autres, il existe des solutions acceptables.

Il est nĂ©cessaire d'Ă©crire un algorithme qui renvoie, mĂȘme si ce n'est pas rĂ©ellement des nombres alĂ©atoires, des valeurs aussi proches que possible — les soi-disant nombres pseudo-alĂ©atoires (NPA). Dans ce cas, l'algorithme s'appelle gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires (GNPA).

Il existe plusieurs façons de créer un GNPA, mais pour tous, le point suivant est pertinent :

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

    Le générateur de nombres pseudo-aléatoires est dépourvu de source d'entropie, il est donc nécessaire de lui indiquer un état initial avant utilisation. Cet état est défini comme un nombre (ou un vecteur) et est appelé graine (seed). Souvent, un compteur de cycles du processeur ou un équivalent numérique de l'heure systÚme est utilisé comme graine.

  2. Reproductibilité de la séquence.

    Le gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires est entiĂšrement dĂ©terministe, donc la graine spĂ©cifiĂ©e lors de l'initialisation dĂ©termine sans ambiguĂŻtĂ© toute la sĂ©quence de nombres futurs. Cela signifie qu'un gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires, initialisĂ© avec la mĂȘme graine (Ă  des moments diffĂ©rents, dans diffĂ©rents programmes, sur diffĂ©rents appareils) va gĂ©nĂ©rer la mĂȘme sĂ©quence.

Il est Ă©galement nĂ©cessaire de connaĂźtre la distribution de probabilitĂ©s caractĂ©ristique du gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires — quels nombres il va gĂ©nĂ©rer et avec quelle probabilitĂ©. Le plus souvent, il s'agit soit d'une distribution normale, soit d'une distribution uniforme.
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG
Distribution normale (Ă  gauche) et distribution uniforme (Ă  droite)

Supposons que nous avons un dĂ© Ă  24 faces Ă©quitable. Si nous le lançons, la probabilitĂ© d'obtenir un un sera de 1/24 (tout comme la probabilitĂ© d'obtenir n'importe quel autre nombre). Si nous effectuons de nombreux lancĂ©s et enregistrons les rĂ©sultats, nous pouvons remarquer que toutes les faces sortent Ă  peu prĂšs avec la mĂȘme frĂ©quence. En essence, ce dĂ© peut ĂȘtre considĂ©rĂ© comme un gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires avec une distribution uniforme.

Que se passe-t-il si nous lançons 10 de ces dĂ©s en mĂȘme temps et que nous comptons la somme totale ? La uniformitĂ© sera-t-elle conservĂ©e ? Non. Le plus souvent, la somme sera proche de 125 points, donc Ă  une certaine valeur moyenne. Et en consĂ©quence, mĂȘme avant de lancer, nous pouvons estimer approximativement le rĂ©sultat futur.

La raison en est qu'il y a le plus grand nombre de combinaisons possibles pour obtenir la somme moyenne des points. Plus nous nous Ă©loignons de celle-ci, moins il y a de combinaisons - et donc, moins la probabilitĂ© d'obtenir ce rĂ©sultat. Si ces donnĂ©es sont visualisĂ©es, elles ressembleront vaguement Ă  la forme d'une cloche. Ainsi, avec un certain effort, le systĂšme de 10 dĂ©s peut ĂȘtre qualifiĂ© de gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires avec une distribution normale.

Un autre exemple, mais cette fois dans un plan - tir sur une cible. Le tireur sera un générateur de nombres pseudo-aléatoires, générant une paire de nombres (x, y) qui est représentée sur un graphique.
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG
Vous conviendrez que l'option de gauche reflÚte mieux la réalité : c'est un GCS avec une distribution normale. Mais si vous devez disperser des étoiles dans un ciel noir, alors l'option de droite, obtenue avec un GCS à distribution uniforme, convient mieux. En général, choisissez le générateur en fonction de la tùche à accomplir.

Parlons maintenant de l'entropie de la séquence des GCS. Par exemple, il y a une séquence qui commence ainsi :

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, 


À quel point ces chiffres semblent alĂ©atoires Ă  premiĂšre vue ? Commençons par vĂ©rifier la distribution.
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG
Cela semble proche de l'uniformité, mais si nous lisons la séquence deux chiffres à la fois et que nous les interprétons comme des coordonnées sur un plan, cela donne ceci :
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG
Des motifs deviennent clairement visibles. Étant donnĂ© que les donnĂ©es de la sĂ©quence sont organisĂ©es d'une certaine maniĂšre (c'est-Ă -dire qu'elles ont une faible entropie), cela peut engendrer ce qu'on appelle le "biais". Au minimum, un tel GCS n'est pas vraiment adaptĂ© 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 bien ici, mĂȘme sur le plan :
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG
Examinons en trois dimensions (en lisant trois chiffres Ă  la fois) :
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG
Et encore des motifs. Il ne sera déjà plus possible de construire une visualisation en quatre dimensions. Mais des motifs peuvent exister dans cette dimension et dans des dimensions supérieures.

Dans la cryptographie, oĂč les exigences envers les GCS sont les plus strictes, une telle situation est catĂ©goriquement inacceptable. C'est pourquoi des algorithmes spĂ©ciaux ont Ă©tĂ© dĂ©veloppĂ©s pour Ă©valuer leur qualitĂ©, dont nous ne parlerons pas ici. Le sujet est vaste et mĂ©riterait un article Ă  part entiĂšre.

Test

Que faire si nous ne savons pas quelque chose avec certitude ? Est-il judicieux de traverser la route si vous ne savez pas quel feu de signalisation permet de le faire ? Les consĂ©quences peuvent ĂȘtre variĂ©es.

Il en va de mĂȘme pour le fameux random dans Unity. C'est bien si la documentation rĂ©vĂšle les dĂ©tails nĂ©cessaires, mais l'histoire mentionnĂ©e au dĂ©but de l'article est survenue justement Ă  cause de l'absence de la prĂ©cision souhaitĂ©e.

Sans comprendre comment fonctionne l'outil, vous ne pourrez pas l'appliquer correctement. Il est temps de vérifier et de mener une expérience pour s'assurer au moins en ce qui concerne la distribution.

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

Objet de l'étude

Dans Unity, il existe plusieurs mĂ©thodes pour gĂ©nĂ©rer des nombres alĂ©atoires — nous en avons testĂ© cinq.

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

Pratiquement 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 une distribution uniforme Ă©tait Ă©galement attendue par analogie avec UnityEngine.Random.Range()) et Unity.Mathematics.Random.NextFloat() (oĂč repose un algorithme xorshift, donc il faut Ă©galement s'attendre Ă  une distribution uniforme).

Par défaut, les résultats attendus étaient ceux indiqués dans la documentation.

Méthodologie

Nous avons écrit une petite application qui générait des séquences de nombres aléatoires par chacun des moyens présentés et sauvegardait les résultats pour un traitement ultérieur.

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

Les données ont été collectées à partir de plusieurs plates-formes 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, version pour appareil, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, version pour appareil, il2cpp, .NET Standard 2.0

Mise en Ɠuvre

Nous avons plusieurs façons différentes de générer des nombres aléatoires. Pour chacune d'elles, nous rédigerons une classe d'encapsulation distincte qui devra fournir :

  1. La possibilité de définir une plage de valeurs [min/max). Cela sera défini par le constructeur.
  2. Une méthode retournant un nombre flottant. Choisissons le type float, étant le plus général.
  3. Le nom de la méthode de génération pour étiqueter les résultats. Pour la commodité, nous retournerons le nom complet de la classe + le nom de la méthode utilisée pour générer le nombre flottant.

D'abord, nous allons déclarer 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 permet de définir une plage de valeurs, mais elle retourne des entiers (integer), alors que nous avons besoin de float. On peut simplement interpréter les entiers comme des float, ou élargir la plage de valeurs de plusieurs ordres, en les compensant lors de chaque génération de nombre aléatoire. Cela donnera quelque chose comme un point fixe avec une précision d'ordre spécifiée. Nous allons utiliser cette option, car elle est plus proche de la valeur float 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;
    }
}

Implémentation de System.Random.NextDouble()

Ici, la plage de valeurs est fixe [0; 1). Pour la projeter sur celle spécifiée dans le constructeur, nous utilisons une simple arithmétique : 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;
    }
}

Implémentation de UnityEngine.Random.Range()

Cette méthode de la classe statique UnityEngine.Random permet de définir une plage de valeurs et retourne un nombre de type float. Aucune transformation supplémentaire ne sera 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 retourne un nombre de type float d'une plage fixe de valeurs [0; 1). Nous allons le projeter sur la plage spĂ©cifiĂ©e de la mĂȘme maniĂšre que pour 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 nombre alĂ©atoire de type float et permet de dĂ©finir une plage de valeurs. La nuance est que chaque instance de Unity.Mathematics.Random doit ĂȘtre initialisĂ©e avec une valeur de dĂ©part (seed) — cela nous permet d'Ă©viter la gĂ©nĂ©ration de sĂ©quences rĂ©pĂ©titives.

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 du MainController

Plusieurs implĂ©mentations de IRandomGenerator sont prĂȘtes. Ensuite, il faut gĂ©nĂ©rer des sĂ©quences et enregistrer le jeu de donnĂ©es rĂ©sultant pour traitement. Pour cela, crĂ©ons une scĂšne dans Unity et un petit script MainController, qui effectuera tout le travail nĂ©cessaire tout en gĂ©rant l'interaction avec l'interface utilisateur.

DĂ©finissons la taille du jeu de donnĂ©es et la plage de valeurs des nombres alĂ©atoires, et Ă©quipons-nous d'une mĂ©thode qui renvoie un tableau de gĂ©nĂ©rateurs configurĂ©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, formons le jeu de données. Dans ce cas, la génération de données sera combinée avec l'enregistrement des résultats dans un flux de texte (au format csv). Chaque IRandomGenerator aura sa propre colonne, et la premiÚre ligne contiendra 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;

            // Ă©crire l'en-tĂȘte
            for (int j = 0; j <= lastIdx; j++)
            {
                writer.Write(generators[j].Name);
                if (j != lastIdx)
                    writer.Write(separator);
            }
            writer.WriteLine();

            // écrire les données
            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 à sauvegarder le résultat dans un fichier, ou à transmettre directement les données sur le réseau depuis l'appareil final vers le récepteur. serveur.

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 se trouvent sur GitLab.

Résultats

Le miracle n'a pas eu lieu. Ce qu'on attendait est exactement ce qu'on a eu : dans tous les cas une distribution uniforme sans aucune allusion Ă  des conspirations. Je ne vois pas l'intĂ©rĂȘt d'annexer des graphiques sĂ©parĂ©s par plateformes - ils montrent tous des rĂ©sultats Ă  peu prĂšs identiques.

La réalité est la suivante :
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG

Visualisation des séquences à plat par toutes les cinq méthodes de génération :
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG

Et une visualisation en 3D. Je ne mettrai que le résultat de System.Random.Next(), pour ne pas créer un tas de contenu identique.
BlessRNG ou vĂ©rifier l'honnĂȘtetĂ© du RNG

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

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster