BlessRNG aŭ kontrolante la RNG por justeco

BlessRNG aŭ kontrolante la RNG por justeco

En luddisvolviĝo, vi ofte bezonas ligi ion kun hazardo: Unity havas sian propran Hazardon por tio, kaj paralele kun ĝi ekzistas System.Random. Iam, pri unu el la projektoj, mi havis la impreson, ke ambaŭ povus funkcii malsame (kvankam ili havu egalan distribuon).

Tiam ili ne eniris detalojn - sufiĉis, ke la transiro al System.Random korektis ĉiujn problemojn. Nun ni decidis rigardi ĝin pli detale kaj fari iom da esploro: kiom "partiaj" aŭ antaŭvideblaj RNG-oj estas, kaj kiun elekti. Krome, mi pli ol unufoje aŭdis konfliktajn opiniojn pri ilia "honesteco" - ni provu eltrovi kiel la realaj rezultoj komparas kun la deklaritaj.

Mallonga eduka programo aŭ RNG estas fakte RNG

Se vi jam konas hazardajn nombrogenerantojn, tiam vi povas tuj salti al la sekcio "Testado".

Hazardaj nombroj (RN) estas sekvenco de nombroj generitaj uzante iun hazardan (kaosa) procezon, fonton de entropio. Tio estas, ĉi tio estas sinsekvo, kies elementoj ne estas interligitaj per iu matematika leĝo - ili havas neniun kialon-efektan rilaton.

Kio kreas la hazardan nombron nomiĝas hazarda nombrogeneratoro (RNG). Ŝajnus, ke ĉio estas elementa, sed se ni transiras de teorio al praktiko, tiam fakte ne estas tiel simple efektivigi programaran algoritmon por generi tian sinsekvon.

La kialo kuŝas en la foresto de tiu sama kaoso en moderna konsumelektroniko. Sen ĝi, hazardaj nombroj ĉesas esti hazardaj, kaj ilia generatoro fariĝas ordinara funkcio de evidente difinitaj argumentoj. Por kelkaj specialaĵoj en la IT-kampo, tio estas grava problemo (ekzemple, kriptografio), sed por aliaj ekzistas tute akceptebla solvo.

Necesas verki algoritmon, kiu revenus, kvankam ne vere hazardaj nombroj, sed kiel eble plej proksime al ili - la tiel nomataj pseŭdo-hazardaj nombroj (PRN). La algoritmo en ĉi tiu kazo estas nomita pseŭdohazarda nombrogeneratoro (PRNG).

Estas pluraj ebloj por krei PRNG, sed la jenaj estos gravaj por ĉiuj:

  1. La bezono de prepara inicialigo.

    La PRNG havas neniun fonton de entropio, tiel ke ĝi devas ricevi komencan staton antaŭ uzo. Ĝi estas specifita kiel nombro (aŭ vektoro) kaj nomiĝas semo (hazarda semo). Ofte, la procesora horloĝkalkulilo aŭ la nombra ekvivalento de sistemtempo estas utiligita kiel semo.

  2. Sekvenca reproduktebleco.

    La PRNG estas tute determinisma, tiel ke la semo precizigita dum inicialigo unike determinas la tutan estontan sekvencon de nombroj. Ĉi tio signifas, ke aparta PRNG pravigita kun la sama semo (en malsamaj tempoj, en malsamaj programoj, sur malsamaj aparatoj) generos la saman sekvencon.

Vi ankaŭ devas scii la probablan distribuon karakterizantan la PRNG - kiajn nombrojn ĝi generos kaj kun kia probableco. Plej ofte tio estas aŭ normala distribuo aŭ unuforma distribuo.
BlessRNG aŭ kontrolante la RNG por justeco
Normala distribuo (maldekstre) kaj unuforma distribuo (dekstre)

Ni diru, ke ni havas justan ĵetkubon kun 24 flankoj. Se vi ĵetas ĝin, la probableco akiri unu estos egala al 1/24 (la sama kiel la probableco akiri ajnan alian nombron). Se vi faras multajn ĵetojn kaj registras la rezultojn, vi rimarkos, ke ĉiuj randoj elfalas kun proksimume la sama ofteco. Esence, tiu ĵetkubo povas esti konsiderita RNG kun unuforma distribuo.

Kio se vi ĵetas 10 el ĉi tiuj ĵetkuboj samtempe kaj kalkulas la totalajn poentojn? Ĉu unuformeco estos konservita por ĝi? Ne. Plej ofte, la kvanto estos proksima al 125 poentoj, tio estas, al iu averaĝa valoro. Kaj kiel rezulto, eĉ antaŭ ol fari ĵeton, vi povas proksimume taksi la estontan rezulton.

La kialo estas, ke ekzistas la plej granda nombro da kombinaĵoj por akiri la mezan poentaron. Ju pli malproksime de ĝi, des malpli da kombinaĵoj - kaj, sekve, des pli malalta la probablo de perdo. Se ĉi tiuj datumoj estas bildigitaj, ĝi malklare similos al la formo de sonorilo. Tial, kun iom da streĉado, sistemo de 10 ĵetkuboj povas esti nomita RNG kun normala distribuo.

Alia ekzemplo, nur ĉi-foje en aviadilo - pafado al celo. La pafisto estos RNG, kiu generas paron da nombroj (x, y) montrata sur la grafikaĵo.
BlessRNG aŭ kontrolante la RNG por justeco
Konsentu, ke la opcio maldekstre estas pli proksima al la reala vivo - ĉi tio estas RNG kun normala distribuo. Sed se vi bezonas disĵeti stelojn en malhela ĉielo, tiam la ĝusta opcio, akirita per RNG kun unuforma distribuo, pli taŭgas. Ĝenerale, elektu generatoron depende de la tasko.

Nun ni parolu pri la entropio de la PNG-sekvenco. Ekzemple, ekzistas sekvenco kiu komenciĝas tiel:

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

Kiel hazardaj estas ĉi tiuj nombroj unuavide? Ni komencu kontrolante la distribuon.
BlessRNG aŭ kontrolante la RNG por justeco
Ĝi aspektas proksima al uniformo, sed se vi legas sinsekvon de du nombroj kaj interpretas ilin kiel koordinatojn sur ebeno, vi ricevas ĉi tion:
BlessRNG aŭ kontrolante la RNG por justeco
Ŝablonoj fariĝas klare videblaj. Kaj ĉar la datenoj en la sekvenco estas ordigitaj en certa maniero (tio estas, ĝi havas malaltan entropion), tio povas kaŭzi tiun tre "biason". Minimume tia PRNG ne tre taŭgas por generi koordinatojn sur aviadilo.

Alia sekvenco:

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

Ĉio ŝajnas esti bona ĉi tie eĉ en la aviadilo:
BlessRNG aŭ kontrolante la RNG por justeco
Ni rigardu laŭvolume (legu tri nombrojn samtempe):
BlessRNG aŭ kontrolante la RNG por justeco
Kaj denove la ŝablonoj. Ne plu eblas konstrui bildigon en kvar dimensioj. Sed ŝablonoj povas ekzisti sur ĉi tiu dimensio kaj sur pli grandaj.

En kriptografio, kie la plej striktaj postuloj estas truditaj al PRNG-oj, tia situacio estas kategorie neakceptebla. Tial, specialaj algoritmoj estis evoluigitaj por taksi ilian kvaliton, kiun ni ne tuŝos nun. La temo estas ampleksa kaj meritas apartan artikolon.

Testado

Se ni ne scias ion certe, tiam kiel labori kun ĝi? Ĉu indas transiri la vojon, se vi ne scias, kiu semaforo permesas ĝin? La konsekvencoj povas esti malsamaj.

La sama validas por la fifama hazardo en Unity. Estas bone, se la dokumentado malkaŝas la necesajn detalojn, sed la rakonto menciita komence de la artikolo okazis ĝuste pro la manko de la dezirataj specifaĵoj.

Kaj se vi ne scias kiel funkcias la ilo, vi ne povos uzi ĝin ĝuste. Ĝenerale venis la tempo kontroli kaj fari eksperimenton por finfine certigi almenaŭ pri la distribuo.

La solvo estis simpla kaj efika - kolektu statistikojn, akiru objektivajn datumojn kaj rigardu la rezultojn.

Temo de studo

Estas pluraj manieroj generi hazardajn nombrojn en Unity - ni testis kvin.

  1. Sistemo.Hazarda.Sekva (). Generas entjerojn en donita gamo da valoroj.
  2. System.Random.NextDouble (). Generas duoblajn precizecajn nombrojn en la intervalo de [0; 1).
  3. UnityEngine.Random.Range (). Generas ununurajn precizecajn nombrojn (flosiloj) en antaŭfiksita gamo da valoroj.
  4. UnityEngine.Hazarda.valoro. Generas ununurajn precizecajn nombrojn (flosiloj) en la intervalo de [0; 1).
  5. Unity.Mathematics.Random.NextFloat (). Parto de la nova Unity.Mathematics-biblioteko. Generas ununurajn precizecajn nombrojn (flosiloj) en antaŭfiksita gamo da valoroj.

Preskaŭ ĉie en la dokumentado estis precizigita unuforma distribuo, escepte de UnityEngine.Random.value (kie la distribuo ne estis specifita, sed analoge kun UnityEngine.Random.Range() uniformo estis ankaŭ atendita) kaj Unity.Mathematics.Random .NextFloat() (kie en La bazo estas la xorshift-algoritmo, kio signifas, ke denove vi devas atendi uniforman distribuon).

Defaŭlte, la atendataj rezultoj estis prenitaj kiel tiuj specifitaj en la dokumentado.

Metodologio

Ni skribis malgrandan aplikaĵon, kiu generis sekvencojn de hazardaj nombroj uzante ĉiun el la prezentitaj metodoj kaj konservis la rezultojn por plua prilaborado.

La longo de ĉiu sinsekvo estas 100 nombroj.
La gamo de hazardaj nombroj estas [0, 100).

Datenoj estis kolektitaj de pluraj celplatformoj:

  • fenestroj
    — Unity v2018.3.14f1, Redaktila reĝimo, Mono, .NET Standard 2.0
  • MacOS
    — Unity v2018.3.14f1, Redaktila reĝimo, Mono, .NET Standard 2.0
    — Unity v5.6.4p4, Redaktila reĝimo, Mono, .NET Standard 2.0
  • android
    — Unity v2018.3.14f1, konstruo per aparato, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, konstruo per aparato, il2cpp, .NET Standard 2.0

Реализация

Ni havas plurajn malsamajn manierojn por generi hazardajn nombrojn. Por ĉiu el ili, ni skribos apartan envolvaĵklason, kiu devus provizi:

  1. Ebleco agordi la gamon de valoroj [min/maks]. Estos agordita per la konstrukciisto.
  2. Metodo revenanta MF. Ni elektu flosilon kiel la tipon, ĉar ĝi estas pli ĝenerala.
  3. La nomo de la genera metodo por marki la rezultojn. Por komforto, ni resendos kiel valoron la plenan nomon de la klaso + la nomon de la metodo uzata por generi la MF.

Unue, ni deklaru abstraktaĵon, kiu estos reprezentita per la interfaco IRandomGenerator:

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

        float Generate();
    }
}

Efektivigo de System.Random.Next ()

Ĉi tiu metodo permesas al vi agordi gamon da valoroj, sed ĝi resendas entjerojn, sed flosiloj estas necesaj. Vi povas simple interpreti entjeron kiel flosilon, aŭ vi povas vastigi la gamon de valoroj per pluraj grandordoj, kompensante ilin per ĉiu generacio de la meza gamo. La rezulto estos io kiel fiksa punkto kun donita ordo de precizeco. Ni uzos ĉi tiun opcion ĉar ĝi estas pli proksima al la reala flosila valoro.

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

Efektivigo de System.Random.NextDouble ()

Ĉi tie la fiksa gamo de valoroj [0; 1). Por projekcii ĝin sur tiu specifita en la konstruilo, ni uzas simplan aritmetikon: 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;
    }
}

Efektivigo de UnityEngine.Random.Range()

Ĉi tiu metodo de la statika klaso UnityEngine.Random ebligas al vi agordi gamon da valoroj kaj redonas tipon de flosilo. Vi ne devas fari iujn ajn aldonajn transformojn.

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

Efektivigo de UnityEngine.Random.value

La valorposedaĵo de la senmova klaso UnityEngine.Random resendas flosan tipon el fiksa gamo de valoroj [0; 1). Ni projekciu ĝin sur donita gamo en la sama maniero kiel kiam efektiviganta 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;
    }
}

Efektivigo de Unity.Mathematics.Random.NextFloat ()

La metodo NextFloat() de la klaso Unity.Mathematics.Random resendas glitpunkton de tipo float kaj permesas al vi specifi gamon da valoroj. La nura nuanco estas, ke ĉiu okazo de Unity.Mathematics.Random devos esti pravaligita per iom da semo - tiamaniere ni evitos generi ripetantajn sekvencojn.

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

Efektivigo de MainController

Pluraj efektivigoj de IRandomGenerator estas pretaj. Poste, vi devas generi sekvencojn kaj konservi la rezultan datumaron por prilaborado. Por fari tion, ni kreos scenon kaj malgrandan MainController-skripton en Unity, kiu faros la tutan necesan laboron kaj samtempe respondecos pri interago kun la UI.

Ni agordu la grandecon de la datumaro kaj la gamon de MF-valoroj, kaj ankaŭ ricevu metodon, kiu resendas aron da generatoroj agordita kaj preta por funkcii.

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

        ...
    }
}

Nun ni kreu datumaron. En ĉi tiu kazo, datumgenerado estos kombinita kun registrado de la rezultoj en tekstfluon (en csv-formato). Por stoki la valorojn de ĉiu IRandomGenerator, ĝia propra aparta kolumno estas asignita, kaj la unua linio enhavas la Nomon de la generatoro.

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

        ...
    }
}

Restas nur voki la metodon GenerateCsvDataSet kaj konservi la rezulton al dosiero, aŭ tuj transdoni la datumojn tra la reto de la fina aparato al la ricevanta servilo.

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

        ...
    }
}

La fontoj de la projekto estas ĉe GitLab.

Результаты

Neniu miraklo okazis. Kion ili atendis estas tio, kion ili ricevis - en ĉiuj kazoj, egalan distribuon sen sugesto de konspiroj. Mi ne vidas la signifon meti apartajn grafikaĵojn por platformoj - ili ĉiuj montras proksimume la samajn rezultojn.

La realo estas:
BlessRNG aŭ kontrolante la RNG por justeco

Bildigo de sekvencoj sur aviadilo de ĉiuj kvin generaciometodoj:
BlessRNG aŭ kontrolante la RNG por justeco

Kaj bildigo en 3D. Mi lasos nur la rezulton de System.Random.Next () por ne produkti amason da identa enhavo.
BlessRNG aŭ kontrolante la RNG por justeco

La rakonto rakontita en la enkonduko pri la normala distribuo de UnityEngine.Hazarda ne ripetis sin: aŭ ĝi estis komence erara, aŭ io ŝanĝiĝis de tiam en la motoro. Sed nun ni estas certaj.

fonto: www.habr.com

Aldoni komenton