BlessRNG of kontroleer die RNG vir regverdigheid

BlessRNG of kontroleer die RNG vir regverdigheid

In speletjie-ontwikkeling moet jy dikwels iets met ewekansigheid verbind: Unity het sy eie Random hiervoor, en parallel daarmee is daar System.Random. Eens op 'n tyd, op een van die projekte, het ek die indruk gekry dat albei anders kan werk (hoewel hulle 'n eweredige verspreiding moet hê).

Toe het hulle nie in besonderhede ingegaan nie - dit was genoeg dat die oorgang na System.Random al die probleme reggestel het. Nou het ons besluit om dit in meer besonderhede te ondersoek en 'n bietjie navorsing te doen: hoe "bevooroordeeld" of voorspelbaar RNG's is, en watter een om te kies. Boonop het ek meer as een keer teenstrydige menings oor hul "eerlikheid" gehoor - kom ons probeer uitvind hoe die werklike resultate vergelyk met die verklaarde.

Kort opvoedkundige program of RNG is eintlik RNG

As jy reeds vertroud is met ewekansige getalgenerators, dan kan jy dadelik na die "Toets"-afdeling oorslaan.

Ewekansige getalle (RN) is 'n reeks getalle wat gegenereer word deur gebruik te maak van een of ander ewekansige (chaotiese) proses, 'n bron van entropie. Dit wil sê, dit is 'n volgorde waarvan die elemente nie deur enige wiskundige wet verbind word nie - hulle het geen oorsaak-en-gevolg-verwantskap nie.

Wat die ewekansige getal skep, word 'n ewekansige getalgenerator (RNG) genoem. Dit wil voorkom asof alles elementêr is, maar as ons van teorie na praktyk beweeg, dan is dit in werklikheid nie so eenvoudig om 'n sagteware-algoritme te implementeer om so 'n volgorde te genereer nie.

Die rede lê in die afwesigheid van dieselfde chaos in moderne verbruikerselektronika. Daarsonder hou ewekansige getalle op om ewekansig te wees, en hul generator verander in 'n gewone funksie van duidelik gedefinieerde argumente. Vir 'n aantal spesialiteite in die IT-veld is dit 'n ernstige probleem (byvoorbeeld kriptografie), maar vir ander is daar 'n heeltemal aanvaarbare oplossing.

Dit is nodig om 'n algoritme te skryf wat sal terugkeer, al is dit nie werklik ewekansige getalle nie, maar so na as moontlik aan hulle - die sogenaamde pseudo-ewekansige getalle (PRN). Die algoritme word in hierdie geval 'n pseudorandom number generator (PRNG) genoem.

Daar is verskeie opsies om 'n PRNG te skep, maar die volgende sal vir almal relevant wees:

  1. Die behoefte aan voorlopige inisialisering.

    Die PRNG het geen bron van entropie nie, so dit moet 'n aanvanklike toestand gegee word voor gebruik. Dit word gespesifiseer as 'n getal (of vektor) en word 'n saad (willekeurige saad) genoem. Dikwels word die verwerker-klokteller of die numeriese ekwivalent van stelseltyd as 'n saad gebruik.

  2. Volgorde reproduceerbaarheid.

    Die PRNG is heeltemal deterministies, so die saad wat tydens inisialisering gespesifiseer word, bepaal die hele toekomstige reeks getalle uniek. Dit beteken dat 'n aparte PRNG geïnisialiseer met dieselfde saad (op verskillende tye, in verskillende programme, op verskillende toestelle) dieselfde volgorde sal genereer.

Jy moet ook die waarskynlikheidsverdeling ken wat die PRNG kenmerk - watter getalle dit sal genereer en met watter waarskynlikheid. Meestal is dit óf 'n normale verspreiding óf 'n eenvormige verspreiding.
BlessRNG of kontroleer die RNG vir regverdigheid
Normale verspreiding (links) en eenvormige verspreiding (regs)

Kom ons sê ons het 'n regverdige dobbelsteen met 24 kante. As jy dit gooi, sal die waarskynlikheid om 'n een te kry gelyk aan 1/24 wees (dieselfde as die waarskynlikheid om enige ander getal te kry). As jy baie gooie maak en die resultate aanteken, sal jy sien dat al die rande met ongeveer dieselfde frekwensie uitval. In wese kan hierdie dobbelsteen as 'n RNG met 'n eenvormige verspreiding beskou word.

Wat as jy 10 van hierdie dobbelsteen gelyktydig gooi en die totale punte tel? Sal eenvormigheid daarvoor gehandhaaf word? Geen. Meestal sal die bedrag naby aan 125 punte wees, dit wil sê tot 'n gemiddelde waarde. En as gevolg hiervan, selfs voordat jy 'n gooi maak, kan jy die toekomstige resultaat rofweg skat.

Die rede is dat daar die grootste aantal kombinasies is om die gemiddelde telling te verkry. Hoe verder daarvan, hoe minder kombinasies - en, dienooreenkomstig, hoe laer is die waarskynlikheid van 'n verlies. As hierdie data gevisualiseer word, sal dit vaagweg soos die vorm van 'n klok lyk. Daarom, met 'n mate van strek, kan 'n stelsel van 10 dobbelstene 'n RNG met 'n normale verspreiding genoem word.

Nog 'n voorbeeld, net hierdie keer op 'n vliegtuig - skiet op 'n teiken. Die skut sal 'n RNG wees wat 'n paar getalle (x, y) genereer wat op die grafiek vertoon word.
BlessRNG of kontroleer die RNG vir regverdigheid
Stem saam dat die opsie aan die linkerkant nader aan die werklike lewe is - dit is 'n RNG met 'n normale verspreiding. Maar as jy sterre in 'n donker lug moet strooi, dan is die regte opsie, verkry met RNG met 'n eenvormige verspreiding, beter geskik. In die algemeen, kies 'n kragopwekker afhangende van die taak op hande.

Kom ons praat nou oor die entropie van die PNG-reeks. Daar is byvoorbeeld 'n reeks wat so begin:

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

Hoe ewekansig is hierdie getalle met die eerste oogopslag? Kom ons begin deur die verspreiding na te gaan.
BlessRNG of kontroleer die RNG vir regverdigheid
Dit lyk na aan uniform, maar as jy 'n ry van twee getalle lees en dit as koördinate op 'n vliegtuig interpreteer, kry jy dit:
BlessRNG of kontroleer die RNG vir regverdigheid
Patrone word duidelik sigbaar. En aangesien die data in die ry op 'n sekere manier georden is (dit wil sê, dit het 'n lae entropie), kan dit aanleiding gee tot daardie einste "vooroordeel". Op 'n minimum is so 'n PRNG nie baie geskik om koördinate op 'n vliegtuig te genereer nie.

Nog 'n volgorde:

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

Alles blyk reg te wees hier, selfs op die vliegtuig:
BlessRNG of kontroleer die RNG vir regverdigheid
Kom ons kyk in volume (lees drie nommers op 'n slag):
BlessRNG of kontroleer die RNG vir regverdigheid
En weer die patrone. Dit is nie meer moontlik om 'n visualisering in vier dimensies te konstrueer nie. Maar patrone kan op hierdie dimensie en op groteres bestaan.

In kriptografie, waar die strengste vereistes aan PRNG'e gestel word, is so 'n situasie kategories onaanvaarbaar. Daarom is spesiale algoritmes ontwikkel om hul kwaliteit te assesseer, wat ons nie nou sal aanraak nie. Die onderwerp is omvangryk en verdien 'n aparte artikel.

toets

As ons iets nie vir seker weet nie, hoe om dan daarmee te werk? Is dit die moeite werd om die pad oor te steek as jy nie weet watter verkeerslig dit toelaat nie? Die gevolge kan anders wees.

Dieselfde geld vir die berugte willekeurigheid in Unity. Dit is goed as die dokumentasie die nodige besonderhede openbaar, maar die storie wat aan die begin van die artikel genoem word, het juis gebeur as gevolg van die gebrek aan die verlangde besonderhede.

En as jy nie weet hoe die instrument werk nie, sal jy dit nie korrek kan gebruik nie. Oor die algemeen het die tyd aangebreek om 'n eksperiment na te gaan en uit te voer om ten minste seker te maak van die verspreiding.

Die oplossing was eenvoudig en effektief – versamel statistieke, verkry objektiewe data en kyk na die resultate.

Onderwerp van studie

Daar is verskeie maniere om ewekansige getalle in Unity te genereer - ons het vyf getoets.

  1. System.Random.Next(). Genereer heelgetalle in 'n gegewe reeks waardes.
  2. System.Random.NextDouble(). Genereer dubbele presisiegetalle in die reeks van [0; 1).
  3. UnityEngine.Random.Range(). Genereer enkele presisiegetalle (floats) in 'n gegewe reeks waardes.
  4. UnityEngine.Random.value. Genereer enkele presisiegetalle (floats) in die reeks van [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Deel van die nuwe Unity.Mathematics-biblioteek. Genereer enkele presisiegetalle (floats) in 'n gegewe reeks waardes.

Byna oral in die dokumentasie is 'n eenvormige verspreiding gespesifiseer, met die uitsondering van UnityEngine.Random.value (waar die verspreiding nie gespesifiseer is nie, maar na analogie van UnityEngine.Random.Range() uniform is ook verwag) en Unity.Mathematics.Random .NextFloat() (waar in Die basis die xorshift-algoritme is, wat beteken dat jy weer moet wag vir 'n eenvormige verspreiding).

By verstek is die verwagte resultate geneem soos dié wat in die dokumentasie gespesifiseer is.

tegniek

Ons het 'n klein toepassing geskryf wat rye van ewekansige getalle gegenereer het deur elk van die voorgestelde metodes te gebruik en die resultate gestoor het vir verdere verwerking.

Die lengte van elke ry is 100 000 nommers.
Die reeks ewekansige getalle is [0, 100).

Data is van verskeie teikenplatforms ingesamel:

  • Windows
    — Unity v2018.3.14f1, Redigeermodus, Mono, .NET Standard 2.0
  • MacOS
    — Unity v2018.3.14f1, Redigeermodus, Mono, .NET Standard 2.0
    — Unity v5.6.4p4, Redigeermodus, Mono, .NET Standard 2.0
  • Android
    — Unity v2018.3.14f1, gebou per toestel, Mono, .NET Standard 2.0
  • IOS
    — Unity v2018.3.14f1, gebou per toestel, il2cpp, .NET Standard 2.0

Implementering

Ons het verskeie maniere om ewekansige getalle te genereer. Vir elkeen van hulle sal ons 'n aparte omhulklas skryf, wat moet voorsien:

  1. Moontlikheid om die reeks waardes [min/maks] in te stel). Sal deur die konstruktor ingestel word.
  2. Metode wat MF terugstuur. Kom ons kies float as die tipe, aangesien dit meer algemeen is.
  3. Die naam van die generasiemetode om die resultate te merk. Gerieflikheidshalwe sal ons die volle naam van die klas + die naam van die metode wat gebruik word om die MF te genereer as 'n waarde terugstuur.

Kom ons verklaar eers 'n abstraksie wat deur die IRandomGenerator-koppelvlak verteenwoordig sal word:

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

        float Generate();
    }
}

Implementering van System.Random.Next()

Hierdie metode laat jou toe om 'n reeks waardes te stel, maar dit gee heelgetalle terug, maar dryf is nodig. U kan eenvoudig 'n heelgetal as 'n vlot interpreteer, of u kan die reeks waardes met verskeie grootteordes uitbrei, wat hulle met elke generasie van die middelreeks vergoed. Die resultaat sal iets soos 'n vaste punt wees met 'n gegewe volgorde van akkuraatheid. Ons sal hierdie opsie gebruik aangesien dit nader aan die werklike dryfwaarde is.

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

Implementering van System.Random.NextDouble()

Hier is die vaste reeks waardes [0; 1). Om dit te projekteer op die een wat in die konstruktor gespesifiseer is, gebruik ons ​​eenvoudige rekenkunde: X * (maks − 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;
    }
}

Implementering van UnityEngine.Random.Range()

Hierdie metode van die UnityEngine.Random statiese klas laat jou toe om 'n reeks waardes te stel en gee 'n dryftipe terug. Jy hoef nie enige bykomende transformasies te doen nie.

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

Implementering van UnityEngine.Random.value

Die waarde-eienskap van die statiese klas UnityEngine.Random gee 'n dryftipe terug uit 'n vaste reeks waardes [0; 1). Kom ons projekteer dit op 'n gegewe reeks op dieselfde manier as wanneer System.Random.NextDouble() geïmplementeer word.

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

Implementering van Unity.Mathematics.Random.NextFloat()

Die NextFloat()-metode van die Unity.Mathematics.Random-klas gee 'n drywende punt van tipe float terug en laat jou toe om 'n reeks waardes te spesifiseer. Die enigste nuanse is dat elke instansie van Unity.Mathematics.Random met 'n mate van saad geïnisialiseer sal moet word - op hierdie manier sal ons vermy om herhalende rye te genereer.

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

Implementering van MainController

Verskeie implementerings van IRandomGenerator is gereed. Vervolgens moet jy rye genereer en die resulterende datastel stoor vir verwerking. Om dit te doen, sal ons 'n toneel en 'n klein MainController-skrif in Unity skep, wat al die nodige werk sal doen en terselfdertyd verantwoordelik sal wees vir interaksie met die UI.

Kom ons stel die grootte van die datastel en die reeks MF-waardes, en kry ook 'n metode wat 'n verskeidenheid kragopwekkers wat gekonfigureer en gereed is om te werk, terugstuur.

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

        ...
    }
}

Kom ons skep nou 'n datastel. In hierdie geval sal die generering van data gekombineer word met die opname van die resultate in 'n teksstroom (in csv-formaat). Om die waardes van elke IRandomGenerator te stoor, word sy eie aparte kolom toegeken, en die eerste reël bevat die naam van die kragopwekker.

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

        ...
    }
}

Al wat oorbly, is om die GenerateCsvDataSet-metode te roep en die resultaat in 'n lêer te stoor, of die data onmiddellik oor die netwerk van die eindtoestel na die ontvangende bediener oor te dra.

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

        ...
    }
}

Die projekbronne is by GitLab.

Bevindinge

Geen wonderwerk het gebeur nie. Wat hulle verwag het, is wat hulle gekry het – in alle gevalle ’n eweredige verspreiding sonder ’n sweempie sameswerings. Ek sien nie die sin daarin om aparte grafieke vir platforms te plaas nie - hulle toon almal ongeveer dieselfde resultate.

Die realiteit is dit:
BlessRNG of kontroleer die RNG vir regverdigheid

Visualisering van rye op 'n vlak vanaf al vyf generasiemetodes:
BlessRNG of kontroleer die RNG vir regverdigheid

En visualisering in 3D. Ek sal slegs die resultaat van System.Random.Next() laat om nie 'n klomp identiese inhoud te produseer nie.
BlessRNG of kontroleer die RNG vir regverdigheid

Die storie wat in die inleiding vertel is oor die normale verspreiding van UnityEngine.Random het homself nie herhaal nie: óf dit was aanvanklik foutief, óf iets het sedertdien in die enjin verander. Maar nou is ons seker.

Bron: will.com

Voeg 'n opmerking