BlessRNG arba patikrinkite RNG teisingumą

BlessRNG arba patikrinkite RNG teisingumą

Kuriant žaidimą dažnai reikia ką nors susieti su atsitiktinumu: „Unity“ tam turi savo „Random“, o lygiagrečiai su juo yra „System.Random“. Kažkada viename iš projektų susidariau įspūdį, kad abu gali veikti skirtingai (nors turėtų būti tolygiai paskirstyti).

Tada jie nesigilino į detales - pakako, kad perėjimas prie System.Random ištaisė visas problemas. Dabar nusprendėme tai panagrinėti išsamiau ir atlikti nedidelį tyrimą: kiek „šališki“ ar nuspėjami RNG ir kurį pasirinkti. Be to, ne kartą girdėjau prieštaringas nuomones apie jų „sąžiningumą“ - pabandykime išsiaiškinti, kaip tikrieji rezultatai lyginami su deklaruotais.

Trumpa edukacinė programa arba RNG iš tikrųjų yra RNG

Jei jau esate susipažinę su atsitiktinių skaičių generatoriais, galite nedelsdami pereiti į skyrių „Testavimas“.

Atsitiktiniai skaičiai (RN) yra skaičių seka, sugeneruota naudojant kokį nors atsitiktinį (chaotišką) procesą, entropijos šaltinį. Tai yra, tai yra seka, kurios elementai nėra tarpusavyje susiję jokiu matematiniu dėsniu – jie neturi priežasties ir pasekmės ryšio.

Tai, kas sukuria atsitiktinį skaičių, vadinama atsitiktinių skaičių generatoriumi (RNG). Atrodytų, viskas elementaru, bet jei pereitume nuo teorijos prie praktikos, tai iš tikrųjų nėra taip paprasta įdiegti programinį algoritmą tokiai sekai generuoti.

Priežastis yra ta, kad šiuolaikinėje plataus vartojimo elektronikoje nėra to paties chaoso. Be jo atsitiktiniai skaičiai nustoja būti atsitiktiniai, o jų generatorius virsta įprasta akivaizdžiai apibrėžtų argumentų funkcija. Daugeliui IT srities specialybių tai yra rimta problema (pavyzdžiui, kriptografija), tačiau kitoms yra visiškai priimtinas sprendimas.

Būtina parašyti algoritmą, kuris grąžintų, nors ir tikrai ne atsitiktinius skaičius, bet kuo arčiau jų – vadinamuosius pseudoatsitiktinius skaičius (PRN). Algoritmas šiuo atveju vadinamas pseudoatsitiktinių skaičių generatoriumi (PRNG).

Yra keletas PRNG kūrimo parinkčių, tačiau šie dalykai bus svarbūs visiems:

  1. Išankstinės iniciacijos poreikis.

    PRNG neturi entropijos šaltinio, todėl prieš naudojant jam turi būti suteikta pradinė būsena. Jis nurodomas kaip skaičius (arba vektorius) ir vadinamas sėkla (atsitiktinė sėkla). Dažnai procesoriaus laikrodžio skaitiklis arba skaitinis sistemos laiko atitikmuo naudojamas kaip sėkla.

  2. Sekos atkuriamumas.

    PRNG yra visiškai deterministinis, todėl inicijavimo metu nurodyta sėkla vienareikšmiškai nustato visą būsimą skaičių seką. Tai reiškia, kad atskiras PRNG, inicijuotas ta pačia sėkla (skirtingu laiku, skirtingose ​​programose, skirtinguose įrenginiuose), generuos tą pačią seką.

Taip pat reikia žinoti tikimybių skirstinį, apibūdinantį PRNG – kokius skaičius jis generuos ir su kokia tikimybe. Dažniausiai tai yra normalusis arba vienodas pasiskirstymas.
BlessRNG arba patikrinkite RNG teisingumą
Normalus pasiskirstymas (kairėje) ir vienodas (dešinėje)

Tarkime, kad turime 24 pusių teisingą mirtį. Jei mesti jį, tikimybė gauti vieną bus lygi 1/24 (toki pat kaip ir tikimybė gauti bet kurį kitą skaičių). Jei atliksite daug metimų ir fiksuosite rezultatus, pastebėsite, kad visi kraštai iškrenta maždaug vienodu dažniu. Iš esmės šis štampas gali būti laikomas vienodo pasiskirstymo RNG.

Ką daryti, jei iš karto išmesite 10 šių kauliukų ir suskaičiuosite visus taškus? Ar bus išlaikytas vienodumas? Nr. Dažniausiai suma bus artima 125 balams, tai yra iki tam tikros vidutinės vertės. Ir dėl to dar prieš atlikdami metimą galite apytiksliai įvertinti būsimą rezultatą.

Priežastis ta, kad vidutiniam balui gauti yra daugiausia kombinacijų. Kuo toliau nuo jo, tuo mažiau derinių – ir atitinkamai mažesnė nuostolių tikimybė. Jei šie duomenys bus vizualizuoti, jie neaiškiai primins varpo formą. Todėl su tam tikru tempimu 10 kauliukų sistema gali būti vadinama RNG su normaliu pasiskirstymu.

Kitas pavyzdys, tik šį kartą lėktuve – šaudymas į taikinį. Šaulys bus RNG, kuris generuoja skaičių porą (x, y), kuri rodoma diagramoje.
BlessRNG arba patikrinkite RNG teisingumą
Sutikite, kad kairėje esanti parinktis yra arčiau realaus gyvenimo - tai RNG su normaliu pasiskirstymu. Bet jei jums reikia išsklaidyti žvaigždes tamsiame danguje, tada tinkamas variantas, gautas naudojant RNG su vienodu pasiskirstymu, yra tinkamesnis. Apskritai generatorių rinkitės atsižvelgdami į atliekamą užduotį.

Dabar pakalbėkime apie PNG sekos entropiją. Pavyzdžiui, yra seka, kuri prasideda taip:

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

Kiek atsitiktiniai šie skaičiai iš pirmo žvilgsnio? Pradėkime nuo paskirstymo patikrinimo.
BlessRNG arba patikrinkite RNG teisingumą
Tai atrodo beveik vienoda, bet jei perskaitysite dviejų skaičių seką ir interpretuosite jas kaip koordinates plokštumoje, gausite tai:
BlessRNG arba patikrinkite RNG teisingumą
Raštai tampa aiškiai matomi. Ir kadangi sekos duomenys yra išdėstyti tam tikru būdu (ty jie turi mažą entropiją), tai gali sukelti tą patį „šališkumą“. Mažiausiai toks PRNG nėra labai tinkamas koordinatėms generuoti plokštumoje.

Kita seka:

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

Čia net lėktuve viskas atrodo gerai:
BlessRNG arba patikrinkite RNG teisingumą
Pažiūrėkime į garsumą (perskaitykite tris skaičius vienu metu):
BlessRNG arba patikrinkite RNG teisingumą
Ir vėl modeliai. Nebeįmanoma sukurti vizualizacijos keturiose dimensijose. Tačiau modeliai gali egzistuoti šiame ir didesniuose matmenyse.

Kriptografijoje, kur PRNG keliami griežčiausi reikalavimai, tokia situacija kategoriškai nepriimtina. Todėl jų kokybei įvertinti sukurti specialūs algoritmai, kurių dabar neliesime. Tema yra plati ir verta atskiro straipsnio.

Bandymai

Jei ko nors tiksliai nežinome, kaip su tuo dirbti? Ar verta kirsti kelią, jei nežinai, kuris šviesoforas tai leidžia? Pasekmės gali būti skirtingos.

Tas pats pasakytina apie liūdnai pagarsėjusį atsitiktinumą „Unity“. Gerai, jei dokumentacija atskleidžia reikiamas detales, tačiau straipsnio pradžioje minėta istorija įvyko būtent dėl ​​to, kad trūko norimos specifikos.

Ir jei nežinote, kaip įrankis veikia, negalėsite jo tinkamai naudoti. Apskritai atėjo laikas patikrinti ir atlikti eksperimentą, kad pagaliau įsitikintumėte bent jau dėl platinimo.

Sprendimas buvo paprastas ir efektyvus – rinkti statistiką, gauti objektyvius duomenis ir peržiūrėti rezultatus.

Studijų dalykas

„Unity“ yra keletas būdų, kaip generuoti atsitiktinius skaičius – išbandėme penkis.

  1. System.Random.Next(). Sugeneruoja sveikuosius skaičius tam tikrame verčių diapazone.
  2. System.Random.NextDouble(). Sukuria dvigubo tikslumo skaičius diapazone nuo [0; 1).
  3. UnityEngine.Random.Range(). Sugeneruoja atskirus tikslumo skaičius (plaukia) tam tikrame verčių diapazone.
  4. UnityEngine.Random.value. Generuoja pavienius tikslumo skaičius (plaukia) diapazone nuo [0; 1).
  5. Vienybė.Matematika.Random.NextFloat(). Naujos Unity.Mathematics bibliotekos dalis. Sugeneruoja atskirus tikslumo skaičius (plaukia) tam tikrame verčių diapazone.

Beveik visur dokumentacijoje buvo nurodytas vienodas pasiskirstymas, išskyrus UnityEngine.Random.value (kur paskirstymas nebuvo nurodytas, bet pagal analogiją su UnityEngine.Random.Range() uniforma taip pat buvo tikimasi) ir Unity.Mathematics.Random. .NextFloat() (kur Pagrindas yra xorshift algoritmas, o tai reiškia, kad vėl reikia laukti vienodo paskirstymo).

Pagal numatytuosius nustatymus laukiami rezultatai buvo tokie, kaip nurodyta dokumentacijoje.

Metodika

Mes parašėme nedidelę programą, kuri sugeneravo atsitiktinių skaičių sekas, naudodami kiekvieną pateiktą metodą, ir išsaugojome rezultatus tolesniam apdorojimui.

Kiekvienos sekos ilgis yra 100 000 skaičių.
Atsitiktinių skaičių diapazonas yra [0, 100).

Duomenys buvo renkami iš kelių tikslinių platformų:

  • Windows
    — Unity v2018.3.14f1, redaktoriaus režimas, monofoninis, .NET Standard 2.0
  • macOS
    — Unity v2018.3.14f1, redaktoriaus režimas, monofoninis, .NET Standard 2.0
    — Unity v5.6.4p4, redaktoriaus režimas, monofoninis, .NET standartas 2.0
  • Android
    – Unity v2018.3.14f1, kūrimas vienam įrenginiui, monofoninis, .NET standartas 2.0
  • "iOS"
    — Unity v2018.3.14f1, kūrimas vienam įrenginiui, il2cpp, .NET Standard 2.0

Vykdymas

Mes turime keletą skirtingų būdų, kaip generuoti atsitiktinius skaičius. Kiekvienam iš jų parašysime atskirą įvyniojimo klasę, kurioje turėtų būti:

  1. Galimybė nustatyti reikšmių diapazoną [min/max). Bus nustatyta per konstruktorių.
  2. MF grąžinimo būdas. Kaip tipą pasirinkime plūdę, nes ji yra bendresnė.
  3. Rezultatų žymėjimo generavimo metodo pavadinimas. Kad būtų patogiau, kaip reikšmę pateiksime visą klasės pavadinimą + metodo, naudoto generuojant MF, pavadinimą.

Pirmiausia paskelbkime abstrakciją, kurią pateiks IRandomGenerator sąsaja:

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

        float Generate();
    }
}

System.Random.Next() diegimas

Šis metodas leidžia nustatyti reikšmių diapazoną, tačiau jis grąžina sveikuosius skaičius, tačiau reikia slankiųjų. Galite tiesiog interpretuoti sveikąjį skaičių kaip plūduriuojančią reikšmę arba galite išplėsti reikšmių diapazoną keliomis dydžių eilėmis, kompensuodami jas kiekviena vidutinio diapazono karta. Rezultatas bus kažkas panašaus į fiksuotą tašką tam tikra tikslumo tvarka. Naudosime šią parinktį, nes ji yra arčiau tikrosios plaukiojančios vertės.

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

System.Random.NextDouble() diegimas

Čia fiksuotas reikšmių diapazonas [0; 1). Norėdami jį suprojektuoti į nurodytą konstruktoriuje, naudojame paprastą aritmetiką: 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;
    }
}

UnityEngine.Random.Range() diegimas

Šis UnityEngine.Random statinės klasės metodas leidžia nustatyti reikšmių diapazoną ir grąžina plūduriuojantį tipą. Jums nereikia atlikti jokių papildomų transformacijų.

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

UnityEngine.Random.value įgyvendinimas

Statinės klasės UnityEngine.Random vertės ypatybė grąžina slankiojo tipo iš fiksuoto verčių diapazono [0; 1). Suprojektuokime jį į nurodytą diapazoną taip pat, kaip diegdami 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;
    }
}

Unity.Mathematics.Random.NextFloat() įgyvendinimas

Unity.Mathematics.Random klasės metodas NextFloat() grąžina slankiojo kablelio tipą ir leidžia nurodyti reikšmių diapazoną. Vienintelis niuansas, kad kiekvienas Unity.Mathematics.Random egzempliorius turės būti inicijuotas su tam tikra sėkla – taip išvengsime pasikartojančių sekų generavimo.

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

„MainController“ diegimas

Yra paruošti keli IRandomGenerator diegimai. Tada turite sugeneruoti sekas ir išsaugoti gautą duomenų rinkinį apdorojimui. Norėdami tai padaryti, Unity sukursime sceną ir nedidelį MainController scenarijų, kuris atliks visus reikiamus darbus ir tuo pačiu bus atsakingas už sąveiką su vartotojo sąsaja.

Nustatykime duomenų rinkinio dydį ir MF reikšmių diapazoną, taip pat gaukime metodą, kuris grąžina sukonfigūruotų ir paruoštų darbui generatorių masyvą.

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

        ...
    }
}

Dabar sukurkime duomenų rinkinį. Tokiu atveju duomenų generavimas bus derinamas su rezultatų įrašymu į teksto srautą (csv formatu). Norint išsaugoti kiekvieno IRandomGenerator reikšmes, skiriamas atskiras stulpelis, o pirmoje eilutėje yra generatoriaus pavadinimas.

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

        ...
    }
}

Belieka iškviesti GenerateCsvDataSet metodą ir įrašyti rezultatą į failą arba nedelsiant perduoti duomenis per tinklą iš galutinio įrenginio į priimantį serverį.

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

        ...
    }
}

Projekto šaltiniai yra adresu GitLab.

rezultatai

Jokio stebuklo neįvyko. Ko jie tikėjosi, tą ir gavo – visais atvejais tolygaus pasiskirstymo be sąmokslo užuominos. Nematau prasmės dėti atskirus platformų grafikus – jie visi rodo maždaug tuos pačius rezultatus.

Realybė tokia:
BlessRNG arba patikrinkite RNG teisingumą

Visų penkių generavimo metodų sekų vizualizavimas plokštumoje:
BlessRNG arba patikrinkite RNG teisingumą

Ir vizualizacija 3D formatu. Paliksiu tik System.Random.Next() rezultatą, kad nesukurčiau krūvos identiško turinio.
BlessRNG arba patikrinkite RNG teisingumą

Įžangoje pasakojama istorija apie normalų UnityEngine.Random pasiskirstymą nepasikartojo: arba iš pradžių buvo klaidinga, arba nuo to laiko kažkas pasikeitė variklyje. Bet dabar esame tikri.

Šaltinis: www.habr.com

Добавить комментарий