BlessRNG tai tarkista RNG:n oikeudenmukaisuus

BlessRNG tai tarkista RNG:n oikeudenmukaisuus

Pelikehityksessä joutuu usein sidomaan jotain satunnaisuuteen: Unitylla on tätä varten oma Random ja sen rinnalla System.Random. Olipa kerran, yhdessä projektista, sain vaikutelman, että molemmat voisivat toimia eri tavalla (vaikka niillä pitäisi olla tasainen jakautuminen).

Sitten he eivät menneet yksityiskohtiin - riitti, että siirtyminen System.Random korjasi kaikki ongelmat. Nyt päätimme tarkastella sitä tarkemmin ja tehdä vähän tutkimusta: kuinka "puolueisia" tai ennustettavia RNG:t ovat ja mikä niistä valita. Lisäksi olen useammin kuin kerran kuullut ristiriitaisia ​​mielipiteitä heidän "rehellisyydestään" - yritetään selvittää, kuinka todellisia tuloksia verrataan ilmoitettuihin.

Lyhyt koulutusohjelma tai RNG on itse asiassa RNG

Jos olet jo perehtynyt satunnaislukugeneraattoreihin, voit siirtyä välittömästi "Testaus"-osioon.

Satunnaisluvut (RN) ovat jonkin satunnaisen (kaoottisen) prosessin avulla luotuja lukuja, jotka ovat entropian lähde. Tämä on siis sarja, jonka elementtejä ei liity toisiinsa millään matemaattisella lailla - niillä ei ole syy-seuraus-suhdetta.

Sitä, mikä luo satunnaisluvun, kutsutaan satunnaislukugeneraattoriksi (RNG). Näyttää siltä, ​​​​että kaikki on alkeellista, mutta jos siirrymme teoriasta käytäntöön, niin itse asiassa ohjelmistoalgoritmin toteuttaminen tällaisen sekvenssin luomiseksi ei ole niin yksinkertaista.

Syynä on saman kaaoksen puuttuminen nykyaikaisessa kulutuselektroniikassa. Ilman sitä satunnaisluvut lakkaavat olemasta satunnaisia ​​ja niiden generaattori muuttuu tavalliseksi ilmeisesti määriteltyjen argumenttien funktioksi. Useille IT-alan erikoisaloille tämä on vakava ongelma (esimerkiksi kryptografia), mutta muille on täysin hyväksyttävä ratkaisu.

On tarpeen kirjoittaa algoritmi, joka palauttaisi, vaikkakaan ei todella satunnaisia ​​​​lukuja, mutta mahdollisimman lähellä niitä - ns. pseudosatunnaisluvut (PRN). Algoritmia kutsutaan tässä tapauksessa näennäissatunnaislukugeneraattoriksi (PRNG).

PRNG:n luomiseen on useita vaihtoehtoja, mutta seuraavat ovat tärkeitä kaikille:

  1. Alustavan alustuksen tarve.

    PRNG:llä ei ole entropialähdettä, joten sille on annettava alkutila ennen käyttöä. Se määritellään numerona (tai vektorina) ja sitä kutsutaan siemeneksi (satunnainen siemen). Usein siemenenä käytetään prosessorin kellolaskuria tai järjestelmän ajan numeerista vastinetta.

  2. Sekvenssin toistettavuus.

    PRNG on täysin deterministinen, joten alustuksen aikana määritetty siemen määrittää yksiselitteisesti koko tulevan numerosarjan. Tämä tarkoittaa, että erillinen PRNG, joka on alustettu samalla siemenellä (eri aikoina, eri ohjelmissa, eri laitteilla), luo saman sekvenssin.

Sinun on myös tiedettävä PRNG:tä kuvaava todennäköisyysjakauma - mitä lukuja se tuottaa ja millä todennäköisyydellä. Useimmiten tämä on joko normaalijakauma tai tasajakauma.
BlessRNG tai tarkista RNG:n oikeudenmukaisuus
Normaalijakauma (vasemmalla) ja tasajakauma (oikealla)

Oletetaan, että meillä on reilu 24 sivua. Jos heität sen, todennäköisyys saada yksi on yhtä suuri kuin 1/24 (sama kuin todennäköisyys saada mikä tahansa muu luku). Jos teet monta heittoa ja kirjaat tulokset, huomaat, että kaikki reunat putoavat suunnilleen samalla taajuudella. Pohjimmiltaan tätä suulaketta voidaan pitää RNG:nä, jolla on tasainen jakautuminen.

Entä jos heität 10 näistä noppaa kerralla ja lasket kokonaispisteet? Säilytetäänkö siinä yhtenäisyys? Ei. Useimmiten summa on lähellä 125 pistettä, eli johonkin keskiarvoon. Ja sen seurauksena, jo ennen heittoa, voit arvioida karkeasti tulevan tuloksen.

Syynä on se, että keskimääräisen pistemäärän saamiseksi yhdistelmiä on eniten. Mitä kauempana siitä, sitä vähemmän yhdistelmiä - ja vastaavasti, sitä pienempi on tappion todennäköisyys. Jos nämä tiedot visualisoidaan, ne muistuttavat epämääräisesti kellon muotoa. Siksi 10 nopan järjestelmää voidaan tietyllä venytyskerralla kutsua RNG:ksi, jolla on normaalijakauma.

Toinen esimerkki, vain tällä kertaa lentokoneessa - ammunta maaliin. Ampuja on RNG, joka luo numeroparin (x, y), joka näkyy kaaviossa.
BlessRNG tai tarkista RNG:n oikeudenmukaisuus
Hyväksy, että vasemmalla oleva vaihtoehto on lähempänä todellista elämää - tämä on RNG, jolla on normaalijakauma. Mutta jos sinun on hajotettava tähtiä tummalla taivaalla, oikea vaihtoehto, joka on saatu käyttämällä RNG:tä, jolla on tasainen jakautuminen, sopii paremmin. Yleensä valitse generaattori käsillä olevan tehtävän mukaan.

Puhutaan nyt PNG-sekvenssin entropiasta. Esimerkiksi on sekvenssi, joka alkaa näin:

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

Kuinka satunnaisia ​​nämä luvut ovat ensi silmäyksellä? Aloitetaan tarkistamalla jakelu.
BlessRNG tai tarkista RNG:n oikeudenmukaisuus
Se näyttää lähes yhtenäiseltä, mutta jos luet kahden luvun sarjan ja tulkitset ne koordinaatteiksi tasossa, saat tämän:
BlessRNG tai tarkista RNG:n oikeudenmukaisuus
Kuviot tulevat selkeästi näkyviin. Ja koska sekvenssin data on järjestetty tietyllä tavalla (eli sillä on alhainen entropia), tämä voi aiheuttaa juuri tuon "harhaisuuden". Ainakin tällainen PRNG ei ole kovin sopiva koordinaattien generoimiseen tasossa.

Toinen sekvenssi:

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

Täällä kaikki näyttää olevan kunnossa myös lentokoneessa:
BlessRNG tai tarkista RNG:n oikeudenmukaisuus
Katsotaanpa äänenvoimakkuutta (lue kolme numeroa kerralla):
BlessRNG tai tarkista RNG:n oikeudenmukaisuus
Ja taas kuviot. Ei ole enää mahdollista rakentaa visualisointia neljässä ulottuvuudessa. Mutta kuvioita voi olla tässä ulottuvuudessa ja suuremmissa.

Salaustekniikassa, jossa PRNG:ille asetetaan tiukimmat vaatimukset, tällaista tilannetta ei voida hyväksyä. Siksi niiden laadun arvioimiseksi on kehitetty erityisiä algoritmeja, joihin emme nyt puutu. Aihe on laaja ja ansaitsee erillisen artikkelin.

Testaus

Jos emme tiedä jotain varmaksi, niin kuinka toimia sen kanssa? Kannattaako ylittää tietä, jos et tiedä mikä liikennevalo sen sallii? Seuraukset voivat olla erilaisia.

Sama pätee Unityn pahamaineiseen satunnaisuuteen. On hyvä, jos dokumentaatio paljastaa tarvittavat yksityiskohdat, mutta artikkelin alussa mainittu tarina tapahtui juuri haluttujen yksityiskohtien puutteen vuoksi.

Ja jos et tiedä kuinka työkalu toimii, et voi käyttää sitä oikein. Yleisesti ottaen on tullut aika tarkistaa ja tehdä kokeilu, jotta viimeinkin varmistutaan ainakin jakelusta.

Ratkaisu oli yksinkertainen ja tehokas – kerää tilastoja, hanki objektiivisia tietoja ja katso tuloksia.

Opintojen aihe

Unityssa on useita tapoja luoda satunnaislukuja – testasimme viisi.

  1. System.Random.Next(). Luo kokonaislukuja tietyllä arvoalueella.
  2. System.Random.NextDouble(). Luo kaksinkertaisen tarkkuuden lukuja alueella [0; 1).
  3. UnityEngine.Random.Range(). Luo yksittäisiä tarkkuuslukuja (kelluu) tietyllä arvoalueella.
  4. UnityEngine.Random.value. Luo yksittäisiä tarkkuuslukuja (kelluu) alueella [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Osa uutta Unity.Mathematics -kirjastoa. Luo yksittäisiä tarkkuuslukuja (kelluu) tietyllä arvoalueella.

Lähes kaikkialla dokumentaatiossa määriteltiin yhtenäinen jakauma, lukuun ottamatta UnityEngine.Random.value (jossa jakaumaa ei määritetty, mutta analogisesti UnityEngine.Random.Range() -yhtenäistä odotettiin myös) ja Unity.Mathematics.Random .NextFloat() (missä Perus on xorshift-algoritmi, mikä tarkoittaa, että jälleen on odotettava tasaista jakautumista).

Oletusarvoisesti odotetut tulokset on otettu dokumentaatiossa määritellyiksi.

tekniikka

Kirjoitimme pienen sovelluksen, joka loi satunnaislukujonoja kullakin esitetyllä menetelmällä ja tallensi tulokset jatkokäsittelyä varten.

Kunkin sekvenssin pituus on 100 000 numeroa.
Satunnaislukujen alue on [0, 100).

Tietoja kerättiin useilta kohdealustoilta:

  • Windows
    — Unity v2018.3.14f1, muokkaustila, mono, .NET Standard 2.0
  • macOS
    — Unity v2018.3.14f1, muokkaustila, mono, .NET Standard 2.0
    — Unity v5.6.4p4, Editor mode, Mono, .NET Standard 2.0
  • Android
    — Unity v2018.3.14f1, koontiversio laitekohtaisesti, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, koontiversio laitekohtaisesti, il2cpp, .NET Standard 2.0

Реализация

Meillä on useita eri tapoja luoda satunnaislukuja. Jokaiselle niistä kirjoitamme erillisen kääreluokan, jonka pitäisi tarjota:

  1. Mahdollisuus asettaa arvoalue [min/max). Asetetaan rakentajan kautta.
  2. MF:n palautusmenetelmä. Valitaan tyypiksi float, koska se on yleisempää.
  3. Tulosten merkitsemismenetelmän nimi. Mukavuuden vuoksi palautamme arvona luokan koko nimen + MF:n luomiseen käytetyn menetelmän nimen.

Ilmoitetaan ensin abstraktio, jota IRandomGenerator-liitäntä edustaa:

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

        float Generate();
    }
}

Järjestelmän System.Random.Next() toteutus

Tämän menetelmän avulla voit asettaa arvoalueen, mutta se palauttaa kokonaislukuja, mutta kellukkeita tarvitaan. Voit yksinkertaisesti tulkita kokonaisluvun floatiksi tai laajentaa arvojen aluetta useilla suuruusluokilla kompensoimalla niitä jokaisella keskialueen sukupolvelle. Tuloksena on jotain kiinteää pistettä tietyllä tarkkuudella. Käytämme tätä vaihtoehtoa, koska se on lähempänä todellista kelluvaa arvoa.

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

Tässä kiinteä arvoalue [0; 1). Projisoidaksesi sen konstruktorissa määritetylle, käytämme yksinkertaista aritmetiikkaa: 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() -sovelluksen toteutus

Tämän UnityEngine.Random-staattisen luokan menetelmän avulla voit asettaa arvoalueen ja palauttaa kelluvan tyypin. Sinun ei tarvitse tehdä ylimääräisiä muunnoksia.

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:n käyttöönotto

Staattisen luokan UnityEngine.Random arvo-ominaisuus palauttaa float-tyypin kiinteältä arvoalueelta [0; 1). Projisoidaan se tietylle alueelle samalla tavalla kuin toteutettaessa 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()

Unity.Mathematics.Random-luokan NextFloat()-metodi palauttaa liukulukutyypin float ja antaa sinun määrittää arvoalueen. Ainoa vivahde on, että jokainen Unity.Mathematics.Random -esiintymä on alustettava jollain siemenellä - näin vältämme toistuvien sekvenssien luomisen.

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

MainControllerin käyttöönotto

Useita IRandomGenerator-toteutuksia on valmiina. Seuraavaksi sinun on luotava sekvenssit ja tallennettava tuloksena oleva tietojoukko käsittelyä varten. Tätä varten luomme Unityssä kohtauksen ja pienen MainController-skriptin, joka tekee kaiken tarvittavan työn ja vastaa samalla vuorovaikutuksesta käyttöliittymän kanssa.

Asetetaan tietojoukon koko ja MF-arvojen alue, ja hankitaan myös menetelmä, joka palauttaa joukon generaattoreita määritettynä ja valmiina toimimaan.

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

        ...
    }
}

Luodaan nyt tietojoukko. Tässä tapauksessa tiedon tuottaminen yhdistetään tulosten tallentamiseen tekstivirtaan (csv-muodossa). Jokaisen IRandomGeneratorin arvojen tallentamiseksi on varattu oma erillinen sarake, ja ensimmäinen rivi sisältää generaattorin nimen.

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

        ...
    }
}

Jäljelle jää vain kutsua GenerateCsvDataSet-metodi ja tallentaa tulos tiedostoon tai siirtää tiedot välittömästi verkon yli päätelaitteelta vastaanottavalle palvelimelle.

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

        ...
    }
}

Hankkeen lähteet ovat osoitteessa GitLab.

Tulokset

Mitään ihmettä ei tapahtunut. He odottivat sitä, mitä he saivat - kaikissa tapauksissa tasaisen jakelun ilman salaliittoja. En näe mitään järkeä lisätä erillisiä kaavioita alustoille - ne kaikki näyttävät suunnilleen samat tulokset.

Todellisuus on:
BlessRNG tai tarkista RNG:n oikeudenmukaisuus

Sekvenssien visualisointi tasolla kaikilla viidellä sukupolvimenetelmällä:
BlessRNG tai tarkista RNG:n oikeudenmukaisuus

Ja visualisointi 3D:ssä. Jätän vain System.Random.Next()-tuloksen, jotta en tuota kasaa identtistä sisältöä.
BlessRNG tai tarkista RNG:n oikeudenmukaisuus

Johdannossa kerrottu tarina UnityEngine.Randomin normaalijakaumasta ei toistanut itseään: joko se oli alun perin virheellinen tai jokin on sittemmin muuttunut moottorissa. Mutta nyt olemme varmoja.

Lähde: will.com

Lisää kommentti