BlessRNG o comprovant la justícia del RNG

BlessRNG o comprovant la justícia del RNG

En el desenvolupament del joc, sovint necessiteu lligar alguna cosa amb l'atzar: Unity té el seu propi Random per a això, i en paral·lel hi ha System.Random. Hi havia una vegada, en un dels projectes, vaig tenir la impressió que tots dos podien funcionar de manera diferent (tot i que haurien de tenir una distribució uniforme).

Llavors no van entrar en detalls: n'hi havia prou que la transició a System.Random corregís tots els problemes. Ara hem decidit mirar-ho amb més detall i fer una petita investigació: com són els RNG "esbiaixats" o previsibles i quin triar. A més, més d'una vegada he escoltat opinions contradictòries sobre la seva "honestedat": intentem esbrinar com es comparen els resultats reals amb els declarats.

El programa educatiu breu o RNG és en realitat RNG

Si ja esteu familiaritzat amb els generadors de números aleatoris, podeu passar immediatament a la secció "Proves".

Els nombres aleatoris (RN) són una seqüència de nombres generats mitjançant algun procés aleatori (caòtic), una font d'entropia. És a dir, es tracta d'una seqüència els elements de la qual no estan interconnectats per cap llei matemàtica: no tenen cap relació causa-efecte.

El que crea el nombre aleatori s'anomena generador de nombres aleatoris (RNG). Sembla que tot és elemental, però si passem de la teoria a la pràctica, de fet, no és tan senzill implementar un algorisme de programari per generar aquesta seqüència.

El motiu rau en l'absència d'aquest mateix caos en l'electrònica de consum moderna. Sense ell, els nombres aleatoris deixen de ser aleatoris i el seu generador es converteix en una funció ordinària d'arguments òbviament definits. Per a diverses especialitats de l'àmbit informàtic, aquest és un problema greu (per exemple, la criptografia), però per a altres hi ha una solució completament acceptable.

Cal escriure un algorisme que torni, encara que no siguin nombres realment aleatoris, sinó el més proper possible a ells: els anomenats nombres pseudoaleatoris (PRN). L'algorisme en aquest cas s'anomena generador de nombres pseudoaleatoris (PRNG).

Hi ha diverses opcions per crear un PRNG, però les següents seran rellevants per a tothom:

  1. Necessitat d'inicialització prèvia.

    El PRNG no té cap font d'entropia, per la qual cosa s'ha de donar un estat inicial abans d'utilitzar-lo. S'especifica com a nombre (o vector) i s'anomena llavor (llavor aleatòria). Sovint, el comptador del rellotge del processador o l'equivalent numèric del temps del sistema s'utilitza com a llavor.

  2. Reproductibilitat de la seqüència.

    El PRNG és completament determinista, de manera que la llavor especificada durant la inicialització determina de manera única tota la futura seqüència de números. Això vol dir que un PRNG separat inicialitzat amb la mateixa llavor (en moments diferents, en programes diferents, en dispositius diferents) generarà la mateixa seqüència.

També cal conèixer la distribució de probabilitat que caracteritza el PRNG: quins números generarà i amb quina probabilitat. Molt sovint aquesta és una distribució normal o una distribució uniforme.
BlessRNG o comprovant la justícia del RNG
Distribució normal (esquerra) i distribució uniforme (dreta)

Suposem que tenim un dau just amb 24 cares. Si el tires, la probabilitat d'aconseguir-ne un serà igual a 1/24 (la mateixa que la probabilitat d'obtenir qualsevol altre nombre). Si feu molts llançaments i registreu els resultats, notareu que totes les vores cauen amb aproximadament la mateixa freqüència. Essencialment, aquest dau es pot considerar un RNG amb una distribució uniforme.

Què passa si tires 10 d'aquests daus alhora i comptes el total de punts? Es mantindrà la uniformitat? No. Molt sovint, la quantitat s'aproximarà als 125 punts, és a dir, a un valor mitjà. I com a resultat, fins i tot abans de fer un llançament, podeu estimar aproximadament el resultat futur.

El motiu és que hi ha el major nombre de combinacions per obtenir la puntuació mitjana. Com més allunyat, menys combinacions i, en conseqüència, menor és la probabilitat d'una pèrdua. Si es visualitzen aquestes dades, s'assemblarà vagament a la forma d'una campana. Per tant, amb una mica d'estirament, un sistema de 10 daus es pot anomenar RNG amb una distribució normal.

Un altre exemple, només que aquesta vegada en un avió: disparant a un objectiu. El tirador serà un RNG que genera un parell de números (x, y) que es mostra al gràfic.
BlessRNG o comprovant la justícia del RNG
Accepteu que l'opció de l'esquerra s'acosta més a la vida real: aquest és un RNG amb una distribució normal. Però si necessiteu dispersar estrelles en un cel fosc, l'opció correcta, obtinguda mitjançant RNG amb una distribució uniforme, és més adequada. En general, escolliu un generador en funció de la tasca a realitzar.

Ara parlem de l'entropia de la seqüència PNG. Per exemple, hi ha una seqüència que comença així:

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, ...

Què tan aleatoris són aquests números a primera vista? Comencem per comprovar la distribució.
BlessRNG o comprovant la justícia del RNG
Sembla molt uniforme, però si llegiu una seqüència de dos nombres i els interpreteu com a coordenades en un pla, obteniu això:
BlessRNG o comprovant la justícia del RNG
Els patrons es fan clarament visibles. I com que les dades de la seqüència estan ordenades d'una determinada manera (és a dir, tenen una entropia baixa), això pot donar lloc a aquest mateix "biaix". Com a mínim, aquest PRNG no és molt adequat per generar coordenades en un pla.

Una altra seqüència:

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, ...

Sembla que tot va bé aquí fins i tot a l'avió:
BlessRNG o comprovant la justícia del RNG
Mirem el volum (llegiu tres números alhora):
BlessRNG o comprovant la justícia del RNG
I de nou els patrons. Ja no és possible construir una visualització en quatre dimensions. Però poden existir patrons en aquesta dimensió i en altres més grans.

En criptografia, on els requisits més estrictes s'imposen als PRNG, aquesta situació és categòricament inacceptable. Per tant, s'han desenvolupat algorismes especials per avaluar-ne la qualitat, que ara no tocarem. El tema és extens i mereix un article a part.

Proves

Si no sabem alguna cosa del cert, llavors com treballar-hi? Val la pena creuar la carretera si no saps quin semàfor ho permet? Les conseqüències poden ser diferents.

El mateix passa amb la notòria aleatorietat a Unity. Està bé si la documentació revela els detalls necessaris, però la història esmentada al principi de l'article va passar precisament per la manca de detalls desitjats.

I si no saps com funciona l'eina, no la podràs utilitzar correctament. En general, ha arribat el moment de comprovar i realitzar un experiment per assegurar-nos, almenys, de la distribució.

La solució era senzilla i eficaç: recopilar estadístiques, obtenir dades objectives i mirar els resultats.

Tema d'estudi

Hi ha diverses maneres de generar números aleatoris a Unity: n'hem provat cinc.

  1. System.Random.Next(). Genera nombres enters en un rang determinat de valors.
  2. System.Random.NextDouble(). Genera números de doble precisió en el rang de [0; 1).
  3. UnityEngine.Random.Range(). Genera números de precisió únics (floats) en un rang determinat de valors.
  4. UnityEngine.Random.value. Genera números de precisió únics (floats) en el rang de [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Part de la nova biblioteca Unity.Mathematics. Genera números de precisió únics (floats) en un rang determinat de valors.

Gairebé a tot arreu de la documentació s'especificava una distribució uniforme, amb l'excepció de UnityEngine.Random.value (on no s'especificava la distribució, però també s'esperava per analogia amb UnityEngine.Random.Range() uniforme) i Unity.Mathematics.Random .NextFloat() (on La base és l'algorisme xorshift, la qual cosa significa que de nou cal esperar una distribució uniforme).

Per defecte, els resultats esperats es van prendre com els especificats a la documentació.

Metodologia

Vam escriure una petita aplicació que va generar seqüències de nombres aleatoris utilitzant cadascun dels mètodes presentats i va desar els resultats per a un posterior processament.

La longitud de cada seqüència és de 100 números.
El rang de nombres aleatoris és [0, 100).

Les dades es van recollir de diverses plataformes objectiu:

  • Windows
    — Unity v2018.3.14f1, mode Editor, Mono, .NET Standard 2.0
  • macOS
    — Unity v2018.3.14f1, mode Editor, Mono, .NET Standard 2.0
    — Unity v5.6.4p4, mode Editor, Mono, .NET Standard 2.0
  • Android
    — Unity v2018.3.14f1, compilació per dispositiu, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, compilació per dispositiu, il2cpp, .NET Standard 2.0

Implementació

Tenim diverses maneres diferents de generar nombres aleatoris. Per a cadascun d'ells, escriurem una classe d'embolcall independent, que hauria de proporcionar:

  1. Possibilitat d'establir el rang de valors [min/max). S'establirà mitjançant el constructor.
  2. Mètode que retorna MF. Triem el tipus flotant, ja que és més general.
  3. El nom del mètode de generació per marcar els resultats. Per comoditat, retornarem com a valor el nom complet de la classe + el nom del mètode utilitzat per generar l'MF.

Primer, declarem una abstracció que serà representada per la interfície IRandomGenerator:

namespace RandomDistribution
{
    public interface IRandomGenerator
    {
        string Name { get; }

        float Generate();
    }
}

Implementació de System.Random.Next()

Aquest mètode us permet establir un rang de valors, però retorna nombres enters, però calen flotants. Simplement podeu interpretar el nombre enter com a flotant, o podeu ampliar el rang de valors en diversos ordres de magnitud, compensant-los amb cada generació del rang mitjà. El resultat serà com un punt fix amb un ordre de precisió determinat. Utilitzarem aquesta opció ja que s'acosta més al valor flotant real.

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;
    }
}

Implementació de System.Random.NextDouble()

Aquí el rang fix de valors [0; 1). Per projectar-lo sobre l'especificat al constructor, utilitzem aritmètica 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;
    }
}

Implementació de UnityEngine.Random.Range()

Aquest mètode de la classe estàtica UnityEngine.Random us permet establir un rang de valors i retorna un tipus flotant. No cal fer cap transformació addicional.

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);
    }
}

Implementació de UnityEngine.Random.value

La propietat value de la classe estàtica UnityEngine.Random retorna un tipus flotant d'un rang fix de valors [0; 1). Projectem-lo en un rang determinat de la mateixa manera que quan implementem 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;
    }
}

Implementació de Unity.Mathematics.Random.NextFloat()

El mètode NextFloat() de la classe Unity.Mathematics.Random retorna una coma flotant de tipus float i us permet especificar un rang de valors. L'únic matís és que cada instància de Unity.Mathematics.Random s'haurà d'inicialitzar amb alguna llavor; d'aquesta manera evitarem generar seqüències repetides.

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);
    }
}

Implementació de MainController

Diverses implementacions d'IRandomGenerator estan preparades. A continuació, heu de generar seqüències i desar el conjunt de dades resultant per al seu processament. Per fer-ho, crearem una escena i un petit script MainController a Unity, que farà tota la feina necessària i al mateix temps serà responsable de la interacció amb la IU.

Establim la mida del conjunt de dades i l'interval de valors MF, i també obtenim un mètode que retorni una matriu de generadors configurats i preparats per funcionar.

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)
            };
        }

        ...
    }
}

Ara creem un conjunt de dades. En aquest cas, la generació de dades es combinarà amb l'enregistrament dels resultats en un flux de text (en format csv). Per emmagatzemar els valors de cada IRandomGenerator, s'assigna la seva pròpia columna separada i la primera línia conté el nom del generador.

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();
            }
        }

        ...
    }
}

Només queda trucar al mètode GenerateCsvDataSet i desar el resultat en un fitxer, o transferir immediatament les dades a la xarxa des del dispositiu final al servidor receptor.

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 fonts del projecte són a GitLab.

Troballes

No va passar cap miracle. El que esperaven és el que van aconseguir: en tots els casos, una distribució uniforme sense cap indici de conspiracions. No veig el sentit de posar gràfics separats per a plataformes: tots mostren aproximadament els mateixos resultats.

La realitat és aquesta:
BlessRNG o comprovant la justícia del RNG

Visualització de seqüències en un pla a partir dels cinc mètodes de generació:
BlessRNG o comprovant la justícia del RNG

I visualització en 3D. Deixaré només el resultat de System.Random.Next() per no produir un munt de contingut idèntic.
BlessRNG o comprovant la justícia del RNG

La història explicada a la introducció sobre la distribució normal d'UnityEngine.Random no es va repetir: o era inicialment errònia o alguna cosa ha canviat des d'aleshores al motor. Però ara estem segurs.

Font: www.habr.com

Afegeix comentari