BlessRNG eða athugaðu RNG fyrir sanngirni

BlessRNG eða athugaðu RNG fyrir sanngirni

Í leikjaþróun þarf oft að binda eitthvað við tilviljun: Unity hefur sitt eigið Random fyrir þetta og samhliða því er System.Random. Einu sinni, í einu af verkefnunum, fékk ég á tilfinninguna að bæði gætu unnið á annan hátt (þó að þau ættu að hafa jafna dreifingu).

Síðan fóru þeir ekki út í smáatriðin - það var nóg að breytingin yfir í System.Random lagaði öll vandamálin. Nú ákváðum við að skoða það nánar og gera smá rannsókn: hversu „hlutdræg“ eða fyrirsjáanleg RNG eru og hvern á að velja. Þar að auki hef ég oftar en einu sinni heyrt misvísandi skoðanir um „heiðarleika“ þeirra - við skulum reyna að reikna út hvernig raunverulegar niðurstöður bera saman við þær sem lýst er yfir.

Stutt fræðsluáætlun eða RNG er í raun RNG

Ef þú ert nú þegar kunnugur slembitöluframleiðendum geturðu strax farið yfir í „Próf“ hlutann.

Handahófskenndar tölur (RN) eru talnaröð sem myndast með því að nota einhverja tilviljunarkennd (óskipuleg) ferli, uppspretta óreiðu. Það er að segja, þetta er röð þar sem þættirnir eru ekki samtengdir með neinu stærðfræðilegu lögmáli - þeir hafa engin orsök og afleiðing tengsl.

Það sem skapar slembitöluna er kallað slembitöluframleiðandi (RNG). Það virðist sem allt sé grunnatriði, en ef við færumst frá kenningu til framkvæmda, þá er í raun ekki svo einfalt að innleiða hugbúnaðaralgrím til að búa til slíka röð.

Ástæðan liggur í fjarveru sama óreiðu í nútíma rafeindatækni. Án þess hætta handahófskenndar tölur að vera tilviljunarkenndar og generator þeirra breytist í venjulegt fall af augljóslega skilgreindum rökum. Fyrir fjölda sérgreina á upplýsingatæknisviðinu er þetta alvarlegt vandamál (til dæmis dulmál), en fyrir aðra er algjörlega ásættanleg lausn.

Það er nauðsynlegt að skrifa reiknirit sem myndi skila, að vísu ekki raunverulegum tilviljunarkenndum tölum, en eins nálægt þeim og hægt er - svokallaðar gervi-slembitölur (PRN). Reikniritið í þessu tilfelli er kallað gervitilviljunarnúmeraframleiðandi (PRNG).

Það eru nokkrir möguleikar til að búa til PRNG, en eftirfarandi mun skipta máli fyrir alla:

  1. Þörfin fyrir bráðabirgðaræsingu.

    PRNG hefur enga uppsprettu óreiðu, svo það verður að fá upphafsstöðu fyrir notkun. Það er tilgreint sem tala (eða vigur) og er kallað fræ (random seed). Oft er klukkuteljari örgjörva eða tölulegt jafngildi kerfistíma notaður sem fræ.

  2. Röð endurgerðanleiki.

    PRNG er algjörlega ákvarðandi, þannig að fræið sem tilgreint er við upphafssetningu ákvarðar á einkvæman hátt alla framtíðarröð talna. Þetta þýðir að sérstakt PRNG frumstillt með sama fræi (á mismunandi tímum, í mismunandi forritum, á mismunandi tækjum) mun búa til sömu röð.

Þú þarft líka að vita líkindadreifinguna sem einkennir PRNG - hvaða tölur það mun mynda og með hvaða líkum. Oftast er þetta annað hvort normaldreifing eða samræmd dreifing.
BlessRNG eða athugaðu RNG fyrir sanngirni
Venjuleg dreifing (vinstri) og samræmd dreifing (hægri)

Segjum að við höfum sanngjarnan tening með 24 hliðum. Ef þú kastar því eru líkurnar á því að fá einn jafnar 1/24 (sama og líkurnar á að fá aðra tölu). Ef þú kastar mörgum köstum og skráir niðurstöðurnar muntu taka eftir því að allar brúnir detta út með um það bil sömu tíðni. Í meginatriðum getur þessi teningur talist RNG með samræmda dreifingu.

Hvað ef þú kastar 10 af þessum teningum í einu og telur heildarstigin? Verður gætt einsleitni í því? Nei. Oftast mun upphæðin vera nálægt 125 stigum, það er að segja að einhverju meðalgildi. Og þar af leiðandi, jafnvel áður en þú kastar, geturðu áætlað framtíðarniðurstöðuna gróflega.

Ástæðan er sú að það er mestur fjöldi samsetninga til að fá meðaleinkunn. Því lengra frá því, því færri samsetningar - og, í samræmi við það, því minni líkur á tapi. Ef þessi gögn eru sýnd, munu þau líkjast óljóst lögun bjöllu. Þess vegna, með smá teygju, er hægt að kalla kerfi með 10 teningum RNG með normaldreifingu.

Annað dæmi, aðeins í þetta skiptið í flugvél - að skjóta á skotmark. Skotinn verður RNG sem myndar par af tölum (x, y) sem birtist á línuritinu.
BlessRNG eða athugaðu RNG fyrir sanngirni
Sammála því að valmöguleikinn til vinstri er nær raunveruleikanum - þetta er RNG með eðlilega dreifingu. En ef þú þarft að dreifa stjörnum á dimmum himni, þá hentar rétti kosturinn, sem fæst með því að nota RNG með samræmdri dreifingu, betur. Almennt skaltu velja rafall eftir því verkefni sem fyrir hendi er.

Nú skulum við tala um óreiðu PNG röðarinnar. Til dæmis er röð sem byrjar svona:

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

Hversu tilviljunarkenndar eru þessar tölur við fyrstu sýn? Byrjum á því að athuga dreifinguna.
BlessRNG eða athugaðu RNG fyrir sanngirni
Það lítur út fyrir að vera einsleitt, en ef þú lest röð tveggja talna og túlkar þær sem hnit á plani færðu þetta:
BlessRNG eða athugaðu RNG fyrir sanngirni
Mynstur verða greinilega sýnileg. Og þar sem gögnunum í röðinni er raðað á ákveðinn hátt (það er að segja að þau hafi litla óreiðu), getur það valdið þessari „hlutdrægni“. Að minnsta kosti er slíkur PRNG ekki mjög hentugur til að búa til hnit á flugvél.

Önnur röð:

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

Allt virðist vera í lagi hérna jafnvel í flugvélinni:
BlessRNG eða athugaðu RNG fyrir sanngirni
Skoðum í rúmmáli (lesum þrjár tölur í einu):
BlessRNG eða athugaðu RNG fyrir sanngirni
Og aftur mynstur. Það er ekki lengur hægt að smíða sjónmynd í fjórvídd. En mynstur geta verið til á þessari vídd og á stærri.

Í dulmáli, þar sem ströngustu kröfur eru gerðar til PRNGs, er slíkt ástand algerlega óviðunandi. Því hafa verið þróuð sérstök reiknirit til að meta gæði þeirra, sem við munum ekki snerta núna. Umfjöllunarefnið er viðamikið og verðskuldar sérstaka grein.

Prófun

Ef við vitum ekki eitthvað fyrir víst, hvernig á að vinna með það? Er það þess virði að fara yfir veginn ef þú veist ekki hvaða umferðarljós leyfa það? Afleiðingarnar geta verið aðrar.

Sama gildir um hið alræmda tilviljun í Unity. Það er gott ef skjölin leiða í ljós nauðsynlegar upplýsingar, en sagan sem nefnd er í upphafi greinarinnar átti sér stað einmitt vegna skorts á tilætluðum sérstöðu.

Og ef þú veist ekki hvernig tólið virkar muntu ekki geta notað það rétt. Almennt séð er kominn tími til að athuga og gera tilraun til að tryggja að minnsta kosti dreifinguna.

Lausnin var einföld og áhrifarík - safna tölfræði, afla hlutlægra gagna og skoða niðurstöðurnar.

Námsefni

Það eru nokkrar leiðir til að búa til handahófskenndar tölur í Unity - við prófuðum fimm.

  1. System.Random.Next(). Myndar heiltölur á tilteknu gildissviði.
  2. System.Random.NextDouble(). Myndar tvöfaldar nákvæmnistölur á bilinu frá [0; 1).
  3. UnityEngine.Random.Range(). Myndar stakar nákvæmnitölur (flot) á tilteknu gildissviði.
  4. UnityEngine.Random.gildi. Myndar stakar nákvæmnitölur (fljótandi) á bilinu frá [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Hluti af nýju Unity.Mathematics bókasafni. Myndar stakar nákvæmnitölur (flot) á tilteknu gildissviði.

Nánast alls staðar í skjölunum var samræmd dreifing tilgreind, að undanskildum UnityEngine.Random.value (þar sem dreifingin var ekki tilgreind, en einnig var gert ráð fyrir hliðstæðum UnityEngine.Random.Range() samræmdu) og Unity.Mathematics.Random .NextFloat() (þar sem í Grunnurinn er xorshift algrímið, sem þýðir að aftur þarf að bíða eftir samræmdri dreifingu).

Sjálfgefið var að væntanlegar niðurstöður voru teknar eins og þær sem tilgreindar eru í skjölunum.

Aðferðafræði

Við skrifuðum lítið forrit sem bjó til raðir af tilviljunarkenndum tölum með því að nota hverja af framkomnum aðferðum og vistuðum niðurstöðurnar til frekari úrvinnslu.

Lengd hverrar röð er 100 tölur.
Bil handahófskenndra talna er [0, 100).

Gögnum var safnað frá nokkrum markkerfum:

  • Windows
    — Unity v2018.3.14f1, ritstjórahamur, mónó, .NET Standard 2.0
  • MacOS
    — Unity v2018.3.14f1, ritstjórahamur, mónó, .NET Standard 2.0
    — Unity v5.6.4p4, ritstjórahamur, mónó, .NET Standard 2.0
  • Android
    — Unity v2018.3.14f1, smíðuð fyrir hvert tæki, Mono, .NET Standard 2.0
  • IOS
    — Unity v2018.3.14f1, smíðuð fyrir hvert tæki, il2cpp, .NET Standard 2.0

Framkvæmd

Við höfum nokkrar mismunandi leiðir til að búa til handahófskenndar tölur. Fyrir hvert þeirra munum við skrifa sérstakan umbúðaflokk sem ætti að veita:

  1. Möguleiki á að stilla gildissvið [mín/max). Verður stillt í gegnum byggingaraðila.
  2. Aðferð sem skilar MF. Við skulum velja flot sem gerð, þar sem það er almennara.
  3. Heiti kynslóðaraðferðar til að merkja niðurstöður. Til hægðarauka munum við skila fullu nafni flokksins sem gildi + nafn aðferðarinnar sem notuð var til að búa til MF.

Í fyrsta lagi skulum við lýsa yfir útdrætti sem verður táknað með IRandomGenerator viðmótinu:

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

        float Generate();
    }
}

Innleiðing á System.Random.Next()

Þessi aðferð gerir þér kleift að stilla gildissvið, en hún skilar heiltölum, en fljóta þarf. Þú getur einfaldlega túlkað heiltölu sem flot, eða þú getur stækkað gildissviðið um nokkrar stærðargráður og bætt þeim upp með hverri kynslóð millibilsins. Niðurstaðan verður eitthvað eins og fastur punktur með ákveðinni nákvæmni. Við munum nota þennan valkost þar sem hann er nær raunverulegu flotgildinu.

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

Innleiðing á System.Random.NextDouble()

Hér er fast gildissvið [0; 1). Til að varpa því á þann sem tilgreindur er í smiðinum notum við einfaldan reikning: X * (hámark − mín) + mín.

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

Innleiðing UnityEngine.Random.Range()

Þessi aðferð UnityEngine.Random static class gerir þér kleift að stilla gildissvið og skilar flotgerð. Þú þarft ekki að gera neinar frekari umbreytingar.

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

Innleiðing UnityEngine.Random.value

Gildi eiginleiki kyrrstöðuflokksins UnityEngine.Random skilar flotgerð úr föstu gildissviði [0; 1). Við skulum varpa því inn á ákveðið svið á sama hátt og þegar System.Random.NextDouble() er útfært.

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

Innleiðing Unity.Mathematics.Random.NextFloat()

NextFloat() aðferðin í Unity.Mathematics.Random bekknum skilar fljótandi punkti af gerðinni floti og gerir þér kleift að tilgreina gildissvið. Eina blæbrigðið er að hvert tilvik af Unity.Mathematics.Random verður að frumstilla með einhverju fræi - þannig munum við forðast að búa til endurteknar raðir.

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

Innleiðing á MainController

Nokkrar útfærslur á IRandomGenerator eru tilbúnar. Næst þarftu að búa til raðir og vista gagnasafnið sem myndast til vinnslu. Til þess munum við búa til senu og lítið MainController handrit í Unity, sem mun vinna alla nauðsynlega vinnu og um leið bera ábyrgð á samskiptum við HÍ.

Stillum stærð gagnasafnsins og svið MF-gilda og fáum líka aðferð sem skilar fjölda rafala sem eru stilltir og tilbúnir til að vinna.

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

        ...
    }
}

Nú skulum við búa til gagnasafn. Í þessu tilviki verður gagnagerð sameinuð með því að skrá niðurstöðurnar í textastraum (á csv sniði). Til að geyma gildi hvers IRandomGenerator er eigin dálki úthlutað og fyrsta línan inniheldur nafn rafallsins.

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

        ...
    }
}

Það eina sem er eftir er að hringja í GenerateCsvDataSet aðferðina og vista niðurstöðuna í skrá, eða flytja gögnin strax yfir netið frá endatækinu yfir á móttökuþjóninn.

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

        ...
    }
}

Heimildir verkefnisins eru kl GitLab.

Niðurstöður

Ekkert kraftaverk gerðist. Það sem þeir bjuggust við er það sem þeir fengu - í öllum tilfellum jafnri dreifingu án votts af samsæri. Ég sé ekki tilganginn með því að setja aðskilin línurit fyrir palla - þau sýna öll um það bil sömu niðurstöður.

Raunveruleikinn er þessi:
BlessRNG eða athugaðu RNG fyrir sanngirni

Sjónræn raðir á flugvél frá öllum fimm kynslóðaraðferðunum:
BlessRNG eða athugaðu RNG fyrir sanngirni

Og sjón í þrívídd. Ég mun aðeins skilja niðurstöðuna af System.Random.Next() eftir til að framleiða ekki fullt af eins efni.
BlessRNG eða athugaðu RNG fyrir sanngirni

Sagan sem sögð var í innganginum um eðlilega dreifingu UnityEngine.Random endurtók sig ekki: annaðhvort var það rangt í upphafi eða eitthvað hefur breyst í vélinni síðan. En nú erum við viss.

Heimild: www.habr.com

Bæta við athugasemd