BlessRNG ou vérifier l'équité du RNG

BlessRNG ou vérifier l'équité du RNG

En dĂ©veloppement de jeux, on a souvent besoin de recourir Ă  l'alĂ©atoire : Unity propose sa propre fonction Random Ă  cet effet, et System.Random existe en parallĂšle. Il y a longtemps, sur un projet, j'ai eu l'impression que les deux fonctionnaient diffĂ©remment (bien qu'elles soient censĂ©es avoir une distribution uniforme).

Nous n'avions pas approfondi la question Ă  l'Ă©poque ; il nous suffisait de constater que le passage Ă  System.Random rĂ©solvait tous les problĂšmes. Nous avons maintenant dĂ©cidĂ© d'examiner la question de plus prĂšs et de mener une petite Ă©tude : dans quelle mesure les gĂ©nĂ©rateurs de nombres alĂ©atoires sont-ils biaisĂ©s ou prĂ©visibles, et lequel choisir ? Par ailleurs, j'ai entendu Ă  plusieurs reprises des avis contradictoires quant Ă  leur « Ă©quitĂ© Â» ; essayons donc de comparer les rĂ©sultats obtenus aux affirmations.

Une brĂšve introduction au gĂ©nĂ©rateur de nombres alĂ©atoires (GNA), ou est-ce vraiment un GNA ?

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

Les nombres alĂ©atoires (NA) sont une suite de nombres gĂ©nĂ©rĂ©s par un processus alĂ©atoire (chaotique), source d'entropie. Autrement dit, il s'agit d'une suite dont les Ă©lĂ©ments ne sont liĂ©s entre eux par aucune loi mathĂ©matique ; il n'existe aucune relation de cause Ă  effet.

Ce qui crĂ©e le gĂ©nĂ©rateur de nombres alĂ©atoires (GNA) s'appelle un gĂ©nĂ©rateur de nombres alĂ©atoires (GNA). Bien que cela paraisse simple, la mise en pratique de la thĂ©orie rĂ©vĂšle que l'implĂ©mentation d'un algorithme logiciel pour gĂ©nĂ©rer une telle sĂ©quence est loin d'ĂȘtre aussi simple.

La raison tient Ă  l'absence mĂȘme d'alĂ©atoire dans l'Ă©lectronique grand public moderne. Sans lui, les nombres alĂ©atoires cessent d'ĂȘtre alĂ©atoires et leur gĂ©nĂ©rateur se rĂ©duit Ă  une simple fonction d'entrĂ©es prĂ©dĂ©terminĂ©es. Pour certains mĂ©tiers de l'informatique (comme la cryptographie), cela pose un problĂšme majeur, mais pour d'autres, une solution parfaitement acceptable existe.

Nous devons Ă©crire un algorithme qui renvoie des nombres qui ne sont pas vĂ©ritablement alĂ©atoires, mais qui s'en approchent le plus possible : ce que l'on appelle des nombres pseudo-alĂ©atoires (NPA). Cet algorithme est appelĂ© gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires (NPA).

Il existe plusieurs options pour crĂ©er un gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires (PRNG), mais les suivantes seront pertinentes pour tous :

  1. Nécessité d'une pré-initialisation.

    Un gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires (PRNG) ne possĂ©dant pas de source d'entropie, un Ă©tat initial doit ĂȘtre spĂ©cifiĂ© avant utilisation. Cet Ă©tat initial est dĂ©fini par un nombre (ou un vecteur) et est appelĂ© graine (graine alĂ©atoire). Le compteur de cycles du processeur, ou l'Ă©quivalent numĂ©rique du temps systĂšme, est souvent utilisĂ© comme graine.

  2. Reproductibilité de la séquence.

    Le gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires (PRNG) est parfaitement dĂ©terministe ; la graine spĂ©cifiĂ©e lors de l’initialisation dĂ©termine donc de maniĂšre unique la sĂ©quence de nombres gĂ©nĂ©rĂ©e. Autrement dit, un mĂȘme PRNG 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.

Il est Ă©galement nĂ©cessaire de connaĂźtre la distribution de probabilitĂ© qui caractĂ©rise le gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires (PRNG) : quels nombres il gĂ©nĂšre et avec quelle probabilitĂ©. Le plus souvent, il s’agit d’une distribution normale ou d’une distribution uniforme.
BlessRNG ou vérifier l'équité du RNG
Distribution normale (Ă  gauche) et distribution uniforme (Ă  droite)

Imaginons un dé équilibré à 24 faces. Si on le lance, la probabilité d'obtenir un 1 est de 1/24 (comme pour tout autre nombre). En répétant l'opération plusieurs fois et en notant les résultats, on constate que tous les dés à 24 faces donnent des résultats à peu prÚs identiques. On peut donc considérer ce dé comme un générateur de nombres aléatoires à distribution uniforme.

Que se passe-t-il si vous lancez 10 dĂ©s de ce type simultanĂ©ment et calculez le score total ? Sera-t-il uniforme ? Non. Le plus souvent, le score sera proche de 125 points, soit une valeur moyenne. Par consĂ©quent, vous pouvez estimer approximativement le rĂ©sultat futur avant mĂȘme de lancer les dĂ©s.

La raison est simple : il existe un maximum de combinaisons pour atteindre le score moyen. Plus on s'Ă©loigne de la moyenne, moins il y a de combinaisons, et par consĂ©quent, plus la probabilitĂ© d'obtenir un rĂ©sultat positif est faible. Si ces donnĂ©es Ă©taient visualisĂ©es, elles ressembleraient vaguement Ă  une courbe en cloche. Ainsi, en forçant un peu les choses, un systĂšme de 10 dĂ©s pourrait ĂȘtre qualifiĂ© de gĂ©nĂ©rateur de nombres alĂ©atoires Ă  distribution normale.

Un autre exemple, cette fois-ci dans un avion, consiste à tirer sur une cible. Le tireur est un générateur de nombres aléatoires (GNA), qui génÚre une paire de nombres (x, y), lesquels sont affichés sur un graphique.
BlessRNG ou vérifier l'équité du RNG
Vous conviendrez que la variante de gauche est plus proche de la rĂ©alitĂ© : il s’agit d’un gĂ©nĂ©rateur de nombres alĂ©atoires suivant une loi normale. Mais si vous devez disperser des Ă©toiles dans un ciel sombre, la variante de droite, obtenue Ă  l’aide d’un gĂ©nĂ©rateur de nombres alĂ©atoires suivant une loi uniforme, sera plus appropriĂ©e. En rĂ©sumĂ©, choisissez un gĂ©nĂ©rateur en fonction de la tĂąche Ă  accomplir.

Parlons maintenant de l'entropie d'une suite de nombres alĂ©atoires. Par exemple, voici une suite 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, 


À premiĂšre vue, ces nombres semblent-ils alĂ©atoires ? Commençons par examiner leur distribution.
BlessRNG ou vérifier l'équité du RNG
Cela semble presque uniforme, mais si vous lisez la sĂ©quence de deux nombres et les interprĂ©tez comme des coordonnĂ©es dans un plan, vous obtenez ceci :
BlessRNG ou vérifier l'équité du RNG
Des schĂ©mas apparaissent clairement. Et comme 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 gĂ©nĂ©rer le mĂȘme « biais ». À tout le moins, un tel gĂ©nĂ©rateur de nombres pseudo-alĂ©atoires n'est pas trĂšs adaptĂ© Ă  la gĂ©nĂ©ration de coordonnĂ©es dans 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 Ă  plat :
BlessRNG ou vérifier l'équité du RNG
Examinons le volume (lisez trois chiffres) :
BlessRNG ou vérifier l'équité du RNG
Et encore une fois, des motifs. La visualisation en quatre dimensions n'est plus possible. Mais des motifs peuvent exister dans cette dimension et dans des dimensions supérieures.

En cryptographie, oĂč les exigences imposĂ©es aux gĂ©nĂ©rateurs de nombres pseudo-alĂ©atoires sont extrĂȘmement strictes, une telle situation est absolument inacceptable. C'est pourquoi des algorithmes spĂ©cifiques ont Ă©tĂ© dĂ©veloppĂ©s pour Ă©valuer leur qualitĂ©, mais nous n'aborderons pas ce sujet ici. Il est vaste et mĂ©riterait un article Ă  part entiĂšre.

Test

Si nous ignorons quelque chose avec certitude, comment pouvons-nous rĂ©agir ? Devons-nous traverser la rue sans savoir quel feu de circulation l'autorise ? Les consĂ©quences peuvent varier.

Il en va de mĂȘme pour le caractĂšre alĂ©atoire notoire d'Unity. C'est formidable lorsque la documentation fournit les dĂ©tails nĂ©cessaires, mais l'incident mentionnĂ© au dĂ©but de l'article est survenu prĂ©cisĂ©ment en raison de ce manque de prĂ©cision.

Et sans comprendre le fonctionnement de l'outil, impossible de l'utiliser correctement. Il est donc temps de le tester et de mener une expĂ©rience pour en ĂȘtre enfin certain, du moins en ce qui concerne la distribution.

La solution Ă©tait simple et efficace : collecter des statistiques, obtenir des donnĂ©es objectives et analyser 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 compris dans une plage de valeurs donnée.
  2. System.Random.NextDouble(). GénÚre des nombres à double précision dans la plage [0; 1).
  3. UnityEngine.Random.Range(). GénÚre des nombres à simple précision (flottants) dans une plage de valeurs donnée.
  4. UnityEngine.Random.value. GénÚre des nombres à simple précision (float) dans la plage [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Fait partie de la nouvelle bibliothÚque Unity.Mathematics. GénÚre des nombres flottants simple précision dans une plage donnée.

Presque partout dans la documentation, une distribution uniforme Ă©tait spĂ©cifiĂ©e, Ă  l'exception de UnityEngine.Random.value (oĂč la distribution n'est pas spĂ©cifiĂ©e, mais par analogie avec UnityEngine.Random.Range(), une distribution uniforme Ă©tait Ă©galement attendue) et Unity.Mathematics.Random.NextFloat() (qui est basĂ© sur l'algorithme xorshift, ce qui signifie qu'une distribution uniforme devrait Ă©galement ĂȘtre attendue).

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

Méthodologie

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

Chaque séquence comporte 100 000 nombres.
La plage de valeurs numériques 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, version pour appareil, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, version pour appareil, il2cpp, .NET Standard 2.0

exécution

Nous disposons de plusieurs mĂ©thodes pour gĂ©nĂ©rer des nombres alĂ©atoires. Pour chacune d'elles, nous Ă©crirons une classe wrapper distincte qui devra fournir :

  1. Possibilité de spécifier une plage de valeurs (min/max). Sera définie via le constructeur.
  2. Une méthode qui renvoie un nombre. Nous choisirons le type float, car il est plus général.
  3. Nom de la méthode de génération des résultats d'étiquetage. Par commodité, nous renverrons le nom complet de la classe suivi du nom de la méthode utilisée pour générer le SC.

Commençons par 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 spécifier une plage de valeurs, mais elle renvoie des entiers, alors que des nombres à virgule flottante sont requis. Vous pouvez simplement interpréter l'entier comme un nombre à virgule flottante, ou étendre la plage de valeurs sur plusieurs ordres de grandeur, en compensant à chaque génération de la fréquence. On obtient ainsi un résultat proche d'un nombre à virgule fixe avec un ordre de précision spécifié. Nous utiliserons cette option, car elle se rapproche davantage d'une véritable valeur à virgule flottante.

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 fixe est [0; 1). Pour la projeter sur celle spĂ©cifiĂ©e 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;
    }
}

Implémentation de UnityEngine.Random.Range()

Cette méthode de la classe statique UnityEngine.Random permet de spécifier une plage de valeurs et renvoie un nombre aléatoire de type float. Aucune conversion supplémentaire n'est requise.

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 nombre alĂ©atoire de type float Ă  partir d'une plage de valeurs fixe [0; 1). Nous le projetons sur la 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 nombre alĂ©atoire de type float et permet de spĂ©cifier une plage de valeurs. Seule condition : chaque instance de Unity.Mathematics.Random doit ĂȘtre initialisĂ©e avec une graine afin d’éviter la gĂ©nĂ©ration de sĂ©quences identiques.

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 d'IRandomGenerator sont prĂȘtes. Il nous faut maintenant gĂ©nĂ©rer des sĂ©quences et enregistrer les donnĂ©es rĂ©sultantes pour traitement. Pour ce faire, nous allons crĂ©er une scĂšne dans Unity et un petit script MainController qui effectuera toutes les opĂ©rations nĂ©cessaires et gĂ©rera l'interaction avec l'interface utilisateur.

Nous dĂ©finirons la taille de l'ensemble de donnĂ©es et la plage de valeurs de frĂ©quence, et nous crĂ©erons Ă©galement 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)
            };
        }

        ...
    }
}

Nous allons maintenant créer l'ensemble de données. Dans ce cas, la génération des données sera combinée à l'écriture des résultats dans un flux de texte (au format CSV). Chaque générateur 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;

            // 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 ne reste plus qu'à 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 du périphérique final vers le périphérique de réception. 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 sont situées à gitlab ce.

résultats

Il n'y a pas eu de miracle. Nous avons obtenu ce que nous attendions : une distribution uniforme dans tous les cas, sans la moindre trace de complot. Je ne vois pas l'intĂ©rĂȘt de joindre des graphiques distincts pour chaque plateforme ; ils prĂ©sentent tous des rĂ©sultats sensiblement identiques.

La réalité est la suivante :
BlessRNG ou vérifier l'équité du RNG

Visualisation des sĂ©quences sur un plan Ă  partir des cinq mĂ©thodes de gĂ©nĂ©ration :
BlessRNG ou vérifier l'équité du RNG

Et la visualisation 3D. Je ne conserverai que le résultat de System.Random.Next() afin d'éviter de générer une multitude de contenus identiques.
BlessRNG ou vérifier l'équité du RNG

L'histoire racontĂ©e en introduction concernant la distribution normale de UnityEngine.Random ne s'est pas reproduite : soit elle Ă©tait erronĂ©e dĂšs le dĂ©part, soit quelque chose a changĂ© dans le moteur depuis. Mais maintenant, nous sommes confiants.

Source: habr.com

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