BlessRNG o tingnan ang RNG para sa pagiging patas

BlessRNG o tingnan ang RNG para sa pagiging patas

Sa pagbuo ng laro, madalas mong kailangang itali ang isang bagay na may randomness: Ang Unity ay may sariling Random para dito, at kasabay nito ay mayroong System.Random. Noong unang panahon, sa isa sa mga proyekto, nakuha ko ang impresyon na pareho silang maaaring gumana nang magkaiba (bagaman dapat silang magkaroon ng pantay na pamamahagi).

Pagkatapos ay hindi sila nagpunta sa mga detalye - sapat na na ang paglipat sa System.Random ay naitama ang lahat ng mga problema. Ngayon ay nagpasya kaming tingnan ito nang mas detalyado at magsagawa ng kaunting pananaliksik: kung gaano ka "biased" o predictable ang mga RNG, at kung alin ang pipiliin. Bukod dito, higit sa isang beses akong nakarinig ng magkasalungat na opinyon tungkol sa kanilang "katapatan" - subukan nating malaman kung paano maihahambing ang mga tunay na resulta sa mga ipinahayag.

Ang maikling programang pang-edukasyon o RNG ay talagang RNG

Kung pamilyar ka na sa mga generator ng random na numero, maaari kang agad na lumaktaw sa seksyong "Pagsubok".

Ang mga random na numero (RN) ay isang pagkakasunud-sunod ng mga numero na nabuo gamit ang ilang random (magulong) proseso, isang pinagmulan ng entropy. Iyon ay, ito ay isang pagkakasunud-sunod na ang mga elemento ay hindi magkakaugnay ng anumang batas sa matematika - wala silang sanhi-at-epekto na relasyon.

Ang lumilikha ng random na numero ay tinatawag na random number generator (RNG). Tila ang lahat ay elementarya, ngunit kung lilipat tayo mula sa teorya hanggang sa pagsasanay, kung gayon sa katunayan ay hindi gaanong simple na ipatupad ang isang algorithm ng software para sa pagbuo ng gayong pagkakasunud-sunod.

Ang dahilan ay nakasalalay sa kawalan ng parehong kaguluhan sa modernong consumer electronics. Kung wala ito, ang mga random na numero ay titigil sa pagiging random, at ang kanilang generator ay nagiging isang ordinaryong function ng malinaw na tinukoy na mga argumento. Para sa isang bilang ng mga specialty sa larangan ng IT, ito ay isang seryosong problema (halimbawa, cryptography), ngunit para sa iba ay may ganap na katanggap-tanggap na solusyon.

Kinakailangang magsulat ng isang algorithm na babalik, kahit na hindi tunay na random na mga numero, ngunit mas malapit hangga't maaari sa kanila - ang tinatawag na pseudo-random na mga numero (PRN). Ang algorithm sa kasong ito ay tinatawag na pseudorandom number generator (PRNG).

Mayroong ilang mga opsyon para sa paglikha ng PRNG, ngunit ang mga sumusunod ay magiging may-katuturan para sa lahat:

  1. Ang pangangailangan para sa paunang pagsisimula.

    Ang PRNG ay walang pinagmumulan ng entropy, kaya dapat itong bigyan ng paunang estado bago gamitin. Ito ay tinukoy bilang isang numero (o vector) at tinatawag na isang binhi (random na binhi). Kadalasan, ang processor clock counter o ang numerical na katumbas ng oras ng system ay ginagamit bilang isang binhi.

  2. Pagkakasunod-sunod na muling paggawa.

    Ang PRNG ay ganap na deterministiko, kaya ang seed na tinukoy sa panahon ng pagsisimula ay natatanging tinutukoy ang buong hinaharap na pagkakasunud-sunod ng mga numero. Nangangahulugan ito na ang isang hiwalay na PRNG na sinimulan sa parehong seed (sa iba't ibang oras, sa iba't ibang mga programa, sa iba't ibang mga device) ay bubuo ng parehong pagkakasunud-sunod.

Kailangan mo ring malaman ang probability distribution na nagpapakilala sa PRNG - anong mga numero ang bubuo nito at kung anong probabilidad. Kadalasan ito ay alinman sa isang normal na pamamahagi o isang pare-parehong pamamahagi.
BlessRNG o tingnan ang RNG para sa pagiging patas
Normal na pamamahagi (kaliwa) at pare-parehong pamamahagi (kanan)

Sabihin nating mayroon tayong patas na mamatay na may 24 na panig. Kung ihahagis mo ito, ang posibilidad na makakuha ng isa ay magiging katumbas ng 1/24 (kapareho ng posibilidad na makakuha ng iba pang numero). Kung gumawa ka ng maraming paghagis at itala ang mga resulta, mapapansin mo na ang lahat ng mga gilid ay nahuhulog nang humigit-kumulang sa parehong dalas. Sa esensya, ang die na ito ay maaaring ituring na isang RNG na may pare-parehong pamamahagi.

Paano kung ihagis mo ang 10 sa mga dice na ito nang sabay-sabay at bilangin ang kabuuang puntos? Mapapanatili ba ang pagkakapareho para dito? Hindi. Kadalasan, ang halaga ay malapit sa 125 puntos, iyon ay, sa ilang average na halaga. At bilang isang resulta, kahit na bago gumawa ng isang paghagis, maaari mong halos tantiyahin ang resulta sa hinaharap.

Ang dahilan ay mayroong pinakamaraming bilang ng mga kumbinasyon upang makuha ang average na marka. Ang mas malayo mula dito, ang mas kaunting mga kumbinasyon - at, nang naaayon, mas mababa ang posibilidad ng isang pagkawala. Kung makikita ang data na ito, malabo itong kahawig ng hugis ng isang kampana. Samakatuwid, sa ilang kahabaan, ang isang sistema ng 10 dice ay maaaring tawaging RNG na may normal na distribusyon.

Isa pang halimbawa, sa pagkakataong ito lamang sa isang eroplano - pagbaril sa isang target. Ang tagabaril ay magiging isang RNG na bumubuo ng isang pares ng mga numero (x, y) na ipinapakita sa graph.
BlessRNG o tingnan ang RNG para sa pagiging patas
Sumang-ayon na ang opsyon sa kaliwa ay mas malapit sa totoong buhay - ito ay isang RNG na may normal na distribusyon. Ngunit kung kailangan mong ikalat ang mga bituin sa isang madilim na kalangitan, kung gayon ang tamang pagpipilian, na nakuha gamit ang RNG na may pare-parehong pamamahagi, ay mas angkop. Sa pangkalahatan, pumili ng generator depende sa gawain sa kamay.

Ngayon pag-usapan natin ang tungkol sa entropy ng PNG sequence. Halimbawa, mayroong isang pagkakasunud-sunod na nagsisimula tulad nito:

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

Gaano ka random ang mga numerong ito sa unang tingin? Magsimula tayo sa pamamagitan ng pagsuri sa pamamahagi.
BlessRNG o tingnan ang RNG para sa pagiging patas
Mukhang malapit ito sa uniporme, ngunit kung magbasa ka ng isang pagkakasunud-sunod ng dalawang numero at bigyang-kahulugan ang mga ito bilang mga coordinate sa isang eroplano, makukuha mo ito:
BlessRNG o tingnan ang RNG para sa pagiging patas
Ang mga pattern ay nagiging malinaw na nakikita. At dahil ang data sa pagkakasunud-sunod ay inayos sa isang tiyak na paraan (iyon ay, ito ay may mababang entropy), maaari itong magbunga ng mismong "bias". Sa pinakamababa, ang naturang PRNG ay hindi masyadong angkop para sa pagbuo ng mga coordinate sa isang eroplano.

Isa pang sequence:

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

Mukhang maayos ang lahat dito kahit sa eroplano:
BlessRNG o tingnan ang RNG para sa pagiging patas
Tingnan natin sa volume (basahin ang tatlong numero nang sabay-sabay):
BlessRNG o tingnan ang RNG para sa pagiging patas
At muli ang mga pattern. Hindi na posible na bumuo ng visualization sa apat na dimensyon. Ngunit maaaring umiral ang mga pattern sa dimensyong ito at sa mas malalaking sukat.

Sa cryptography, kung saan ang pinaka mahigpit na mga kinakailangan ay ipinapataw sa mga PRNG, ang ganitong sitwasyon ay tiyak na hindi katanggap-tanggap. Samakatuwid, ang mga espesyal na algorithm ay binuo upang masuri ang kanilang kalidad, na hindi namin hawakan ngayon. Ang paksa ay malawak at nararapat sa isang hiwalay na artikulo.

Pagsubok

Kung hindi natin alam ang isang bagay, kung gayon paano ito gagawin? Sulit ba ang pagtawid sa kalsada kung hindi mo alam kung aling ilaw ng trapiko ang nagpapahintulot dito? Maaaring iba ang mga kahihinatnan.

Ang parehong napupunta para sa kilalang randomness sa Unity. Mabuti kung ang dokumentasyon ay nagpapakita ng mga kinakailangang detalye, ngunit ang kuwento na binanggit sa simula ng artikulo ay nangyari nang eksakto dahil sa kakulangan ng nais na mga detalye.

At kung hindi mo alam kung paano gumagana ang tool, hindi mo ito magagamit ng tama. Sa pangkalahatan, dumating na ang oras upang suriin at magsagawa ng eksperimento upang matiyak sa wakas ang tungkol sa pamamahagi.

Ang solusyon ay simple at epektibo - mangolekta ng mga istatistika, kumuha ng layunin ng data at tingnan ang mga resulta.

Paksa ng pag-aaral

Mayroong ilang mga paraan upang bumuo ng mga random na numero sa Unity - sinubukan namin ang lima.

  1. System.Random.Next(). Bumubuo ng mga integer sa isang ibinigay na hanay ng mga halaga.
  2. System.Random.NextDouble(). Bumubuo ng dobleng katumpakan na mga numero sa hanay mula sa [0; 1).
  3. UnityEngine.Random.Range(). Bumubuo ng mga solong numero ng katumpakan (float) sa isang ibinigay na hanay ng mga halaga.
  4. UnityEngine.Random.value. Bumubuo ng mga solong numero ng katumpakan (mga lumulutang) sa hanay mula sa [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Bahagi ng bagong Unity.Mathematics library. Bumubuo ng mga solong numero ng katumpakan (float) sa isang ibinigay na hanay ng mga halaga.

Halos lahat ng lugar sa dokumentasyon ay may tinukoy na pare-parehong pamamahagi, maliban sa UnityEngine.Random.value (kung saan hindi tinukoy ang distribusyon, ngunit ayon sa pagkakatulad sa UnityEngine.Random.Range() na uniporme ay inaasahan din) at Unity.Mathematics.Random .NextFloat() (kung saan sa Ang batayan ay ang xorshift algorithm, na nangangahulugan na muli kailangan mong maghintay para sa isang pare-parehong pamamahagi).

Bilang default, ang mga inaasahang resulta ay kinuha tulad ng mga tinukoy sa dokumentasyon.

Pamamaraan

Sumulat kami ng isang maliit na application na nakabuo ng mga pagkakasunud-sunod ng mga random na numero gamit ang bawat isa sa ipinakita na mga pamamaraan at nai-save ang mga resulta para sa karagdagang pagproseso.

Ang haba ng bawat sequence ay 100 na numero.
Ang hanay ng mga random na numero ay [0, 100).

Nakolekta ang data mula sa ilang target na platform:

  • Windows
    β€” Unity v2018.3.14f1, Editor mode, Mono, .NET Standard 2.0
  • MacOS
    β€” Unity v2018.3.14f1, Editor mode, Mono, .NET Standard 2.0
    β€” Unity v5.6.4p4, Editor mode, Mono, .NET Standard 2.0
  • Android
    β€” Unity v2018.3.14f1, build bawat device, Mono, .NET Standard 2.0
  • iOS
    β€” Unity v2018.3.14f1, build bawat device, il2cpp, .NET Standard 2.0

Pagpapatupad

Mayroon kaming iba't ibang paraan upang makabuo ng mga random na numero. Para sa bawat isa sa kanila, magsusulat kami ng isang hiwalay na klase ng wrapper, na dapat magbigay ng:

  1. Posibilidad na itakda ang hanay ng mga halaga [min/max). Itatakda sa pamamagitan ng constructor.
  2. Paraan ng pagbabalik ng MF. Piliin natin ang float bilang uri, dahil ito ay mas pangkalahatan.
  3. Ang pangalan ng paraan ng pagbuo para sa pagmamarka ng mga resulta. Para sa kaginhawahan, ibabalik namin bilang isang halaga ang buong pangalan ng klase + ang pangalan ng paraan na ginamit upang bumuo ng MF.

Una, magdeklara tayo ng abstraction na kakatawanin ng interface ng IRandomGenerator:

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

        float Generate();
    }
}

Pagpapatupad ng System.Random.Next()

Binibigyang-daan ka ng pamamaraang ito na magtakda ng isang hanay ng mga halaga, ngunit nagbabalik ito ng mga integer, ngunit kailangan ang mga float. Maaari mo lamang bigyang-kahulugan ang integer bilang isang float, o maaari mong palawakin ang hanay ng mga halaga sa pamamagitan ng ilang mga order ng magnitude, na binabayaran ang mga ito sa bawat henerasyon ng midrange. Ang resulta ay magiging isang bagay na parang fixed-point na may ibinigay na pagkakasunud-sunod ng katumpakan. Gagamitin namin ang opsyong ito dahil mas malapit ito sa totoong float value.

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

Pagpapatupad ng System.Random.NextDouble()

Narito ang nakapirming hanay ng mga halaga [0; 1). Upang i-project ito sa tinukoy sa constructor, gumagamit kami ng simpleng aritmetika: 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;
    }
}

Pagpapatupad ng UnityEngine.Random.Range()

Ang pamamaraang ito ng UnityEngine.Random na static na klase ay nagbibigay-daan sa iyong magtakda ng isang hanay ng mga halaga at nagbabalik ng isang uri ng float. Hindi mo kailangang gumawa ng anumang karagdagang pagbabago.

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

Pagpapatupad ng UnityEngine.Random.value

Ang value property ng static na klase na UnityEngine.Random ay nagbabalik ng float type mula sa isang nakapirming hanay ng mga value [0; 1). I-proyekto natin ito sa isang ibinigay na hanay sa parehong paraan tulad ng kapag nagpapatupad ng 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;
    }
}

Pagpapatupad ng Unity.Mathematics.Random.NextFloat()

Ang NextFloat() na paraan ng Unity.Mathematics.Random na klase ay nagbabalik ng floating point ng uri ng float at nagbibigay-daan sa iyong tumukoy ng hanay ng mga value. Ang tanging nuance ay ang bawat instance ng Unity.Mathematics.Random ay kailangang masimulan gamit ang ilang seed - sa ganitong paraan maiiwasan natin ang pagbuo ng mga paulit-ulit na sequence.

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

Pagpapatupad ng MainController

Handa na ang ilang pagpapatupad ng IRandomGenerator. Susunod, kailangan mong bumuo ng mga sequence at i-save ang nagreresultang dataset para sa pagproseso. Upang gawin ito, gagawa kami ng isang eksena at isang maliit na script ng MainController sa Unity, na gagawa ng lahat ng kinakailangang gawain at sa parehong oras ay magiging responsable para sa pakikipag-ugnayan sa UI.

Itakda natin ang laki ng dataset at ang hanay ng mga halaga ng MF, at kumuha din ng paraan na nagbabalik ng hanay ng mga generator na na-configure at handang gumana.

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

        ...
    }
}

Ngayon, gumawa tayo ng dataset. Sa kasong ito, ang pagbuo ng data ay isasama sa pagtatala ng mga resulta sa isang text stream (sa csv format). Upang maimbak ang mga halaga ng bawat IRandomGenerator, ang sarili nitong hiwalay na column ay inilalaan, at ang unang linya ay naglalaman ng Pangalan ng generator.

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

        ...
    }
}

Ang natitira na lang ay tawagan ang paraan ng GenerateCsvDataSet at i-save ang resulta sa isang file, o agad na ilipat ang data sa network mula sa end device patungo sa tumatanggap na 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();
            }
        }

        ...
    }
}

Ang mga mapagkukunan ng proyekto ay nasa GitLab.

Natuklasan

Walang milagrong nangyari. Ang inaasahan nila ay kung ano ang nakuha nila - sa lahat ng kaso, isang pantay na pamamahagi nang walang pahiwatig ng mga pagsasabwatan. Hindi ko nakikita ang punto sa paglalagay ng magkahiwalay na mga graph para sa mga platform - lahat sila ay nagpapakita ng humigit-kumulang sa parehong mga resulta.

Ang katotohanan ay:
BlessRNG o tingnan ang RNG para sa pagiging patas

Visualization ng mga sequence sa isang eroplano mula sa lahat ng limang henerasyon na pamamaraan:
BlessRNG o tingnan ang RNG para sa pagiging patas

At visualization sa 3D. Mag-iiwan lang ako ng resulta ng System.Random.Next() para hindi makagawa ng isang grupo ng magkaparehong content.
BlessRNG o tingnan ang RNG para sa pagiging patas

Ang kuwento na sinabi sa pagpapakilala tungkol sa normal na pamamahagi ng UnityEngine.Random ay hindi naulit ang sarili nito: alinman sa una ay mali, o may nagbago mula noon sa makina. Pero ngayon sigurado na tayo.

Pinagmulan: www.habr.com

Magdagdag ng komento