BlessRNG ose kontrollojmë RNG-në për ndershmëri

BlessRNG ose kontrollojmë RNG-në për ndershmëri

Në zhvillimin e lojërave shpesh nevojitet të lidhet diçka me rastësinë: për këtë, Unity ka Random-in e tij, ndërsa paralelisht ekziston edhe System.Random. Disa kohë më parë, në një nga projektet, u krijua përshtypja se të dyja mund të funksiononin ndryshe (ndonëse duhet të kishin shpërndarje të barabartë).

Atëherë nuk u thelluam në detaje — mjaftonte që kalimi te System.Random zgjidhi të gjitha problemet. Tani vendosëm të shpërfaqim më parë dhe të kryejmë një kërkim të vogël: sa "të preferuar" ose të parashikueshëm janë gjeneratorët e numrave të rastësishëm, dhe cilin duhet të zgjedhim. Veçanërisht, kam dëgjuar disa mendime kontradiktore mbi "ndershmërinë" e tyre — do të provojmë të kuptojmë se si rezultatet reale përputhen me ato të shpallura.

Një e shkurtër mbi gjeneratorët e numrave të rastësishëm ose çfarë janë në të vërtetë GSH.

Nëse tashmë jeni të njohur me gjeneratorët e numrave të rastësishëm, mund të kaloni menjëherë në seksionin "Testimi".

Numrat e rastësishëm (NR) janë një seri numrash të gjeneruar përmes një procesi të caktuar rastësor (kaotik), një burimi entropie. Kjo do të thotë se kjo është një seri, elementet e së cilës nuk janë të lidhur mes tyre nga ndonjë ligj matematikor — ato nuk kanë lidhje shkaku-pasojë.

Ajo që krijon NR-në quhet gjenerator i numrave të rastësishëm (GNR). Duket e thjeshtë, por kur kalon nga teoria në praktikë, realizimi i një algoritmi programor për gjenerimin e një të tillë nuk është aq i lehtë.

Arsyeja ndodhet në mungesën e asaj kaoticiteti në elektronikën moderne të konsumit. Pa të, numrat e rastësishëm ndalojnë së qeni të tillë, dhe gjeneratori i tyre kthehet në një funksion të zakonshëm të argumenteve të njohura. Për një sërë specialitetesh në fushën e IT-së, kjo është një problem serioz (për shembull, për kriptografinë), por për të tjerët ka një zgjidhje të pranueshme.

Duhet të shkruhet një algoritëm që do të kthejë, ndoshta jo numra të vërtetë të rastësishëm, por sa më afër tyre — të ashtuquajturit, numra pseudorastësorë (PR). Algoritmi në këtë rast quhet gjenerator i numrave pseudorastësorë (GPR).

Ka disa opcione për krijimin e GPR, por për të gjitha do të jetë relevante kjo e dhënë:

  1. Nevoja për inicializim të përparshëm.

    GPCH është pa një burim entropie, kështu që para përdorimit duhet t'i jepet një gjendje fillestare. Kjo caktim jepet si një numër (ose si një vektor) dhe quhet farë (seed, random seed). Shpesh, si farë përdoret numri i cikleve të procesorit ose ekuivalenti numerik i kohës sistemike.

  2. Riprodhueshmëria e sekuencës.

    GPCH është plotësisht determinuese, kështu që farë e caktuar gjatë inicializimit përcakton në mënyrë unike tërë sekuencën e numrave të ardhshëm. Kjo do të thotë se një GPCH i vetëm, i inicializuar me të njëjtën farë (në kohë të ndryshme, në programe të ndryshme, në pajisje të ndryshme) do të gjenerojë të njëjtën sekuencë.

Duhet gjithashtu të dihet shpërndarja e probabiliteteve që karakterizon GPCH – cilat numra do të gjenerojë ai dhe me çfarë probabiliteti. Më shpesh kjo është ose shpërndarja normale (normal distribution), ose shpërndarja e barabartë (uniform distribution).
BlessRNG ose kontrollojmë RNG-në për ndershmëri
Shpërndarja normale (majtas) dhe shpërndarja e barabartë (djathtas)

Supozoni se kemi një zarf të ndershëm me 24 faqe. Nëse e hedhim, probabiliteti i rënies së numrit një do të jetë 1/24 (ashtu si probabiliteti i rënies së ndonjë numri tjetër). Nëse bëjmë shumë hedhje dhe regjistrojmë rezultatet, do të vëmë re se të gjitha faqet bien afërsisht me të njëjtën frekuencë. Në thelb, kjo zarf mund të konsiderohet si GPÇ me shpërndarje të barabartë.

Dhe nëse hedhim menjëherë 10 të tillë dhe llogarisim shumën totale të pikëve? A do të ruajë kjo barazpeshën? Jo. Më shpesh, shuma do të jetë afër 125 pikëve, që do të thotë një mesatare të caktuar. Si pasojë – edhe para hedhjes, mund të vlerësojmë përafërsisht rezultatin e ardhshëm.

Arsyeja është se për të marrë shumën mesatare të pikëve ekziston numri më i madh i kombinimeve. Sa më larg të jetë nga ajo, aq më pak kombinime do të jenë – dhe për rrjedhojë, probabiliteti i rënies do të jetë më i vogël. Nëse këto të dhëna vizualizohen, ato do të duken disi si forma e një zilje. Prandaj, me një minimum të tërheqjes, sistemi i 10 zarfëve mund të quhet GPÇ me shpërndarje normale.

Një shembull tjetër, por tashmë në një plan – qëllimi në një qëllim. Shigjetari do të jetë GPÇ, duke gjeneruar një çift numrash (x, y), që shfaqet në grafik.
BlessRNG ose kontrollojmë RNG-në për ndershmëri
Pranon dhe se varianti në të majtë është më afër jetës reale — ky është një gjenerator numrash të rastësishëm me shpërndarje normale. Por nëse ka nevojë të shpërndajmë yjet në qiellin e errët, atëherë varianti i djathtë, i marrë me një gjenerator numrash të rastësishëm me shpërndarje uniforme, është më i përshtatshëm. Në përgjithësi, zgjidhni gjeneratorin sipas detyrës së vendosur.

Tani do të flasim për entropinë e sekuencës së NRR-së. Për shembull, egziston një sekuencë që fillon kështu:

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

Sa të rastësishme duken këto numra me të parën që shikojmë? Le të fillojmë me kontrollin e shpërndarjes.
BlessRNG ose kontrollojmë RNG-në për ndershmëri
Duket si afër uniformes, por nëse lexojmë sekuencën me dy numra dhe i interpretojmë ata si koordinata në një dyanshëm, atëherë do të rezultojë kjo:
BlessRNG ose kontrollojmë RNG-në për ndershmëri
Patternet bëhen të dukshme. Dhe pasi të dhënat në sekuencë janë të rregulluara në një mënyrë të caktuar (domethënë kanë entropi të ulët), kjo mund të shkaktojë atë që quhet 'preferencë'. Të paktën, ky NRR nuk është shumë i përshtatshëm për gjenerimin e koordinatave në një dyanshëm.

Një sekuencë tjetë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, …

Duket se gjithçka është në rregull edhe në planin dyanshëm:
BlessRNG ose kontrollojmë RNG-në për ndershmëri
Le të shikojmë në volum (lexojmë tre numra):
BlessRNG ose kontrollojmë RNG-në për ndershmëri
Dhe përsëri patternet. Të ndërtojmë një vizualizim në katër dimensione nuk do të funksionojë më. Por patternet mund të ekzistojnë edhe në këtë dimension dhe në më të mëdhenj.

Në të njëjtën fushë të kriptografisë, ku kërkesat për NRR janë më të rrepta, një situatë e tillë është kategorikisht e papranueshme. Prandaj, për të vlerësuar cilësinë e tyre, janë zhvilluar algoritme të veçanta, për të cilët nuk do të flasim tani. Tema është e gjerë dhe kërkon një artikull të veçantë.

Testimi

Nëse nuk dimë diçka me siguri, si të punojmë me të? A duhet të kalojmë rrugën nëse nuk e dimë se cili sinjal i semaforit e lejon këtë? Pasojat mund të jenë të ndryshme.

E njëjta gjë vlen për rastësinë e njohur në Unity. Është mirë nëse dokumentacioni zbulohet me detajet e nevojshme, por historia e përmendur në fillim të artikullit ndodhi pikërisht për shkak të mungesës së saktësive të dëshiruara.

Dhe pa e ditur si funksionon mjeti, nuk mund ta aplikoni atë siç duhet. Në përgjithësi, ka ardhur koha për të kontrolluar dhe për të kryer një eksperiment, për t'u siguruar së paku për shpërndarjen.

Zgjidhja ishte e thjeshtë dhe efektive - të mbledhim statistikë, të marrim të dhëna objektive dhe të shohim rezultatet.

Subjekti i hulumtimit

Në Unity ekzistojnë disa mënyra për të gjeneruar numra të rastësishëm - ne testuam pesë.

  1. System.Random.Next(). Gjeneron numra të tërë (integer) brenda një diapazoni të caktuar.
  2. System.Random.NextDouble(). Gjeneron numra me pikë çift (double) brenda diapazonit nga [0; 1).
  3. UnityEngine.Random.Range(). Gjeneron numra me pikë të vetme (float) brenda një diapazoni të caktuar.
  4. UnityEngine.Random.value. Gjeneron numra me pikë të vetme (float) brenda diapazonit nga [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Pjesë e bibliotekës së re Unity.Mathematics. Gjeneron numra me pikë të vetme (float) brenda një diapazoni të caktuar.

Përveç në disa raste, dokumentacioni tregonte shpërndarje të barabartë, përveç UnityEngine.Random.value (ku shpërndarja nuk ishte e specifikuar, por duke pasur parasysh UnityEngine.Random.Range() gjithashtu pritej e barabartë) dhe Unity.Mathematics.Random.NextFloat() (ku bazohet në algoritmin xorshift, kështu që përsëri duhet pritur shpërndarje të barabartë).

Në parim, rezultatet e pritura ishin ato që janë të specifikuara në dokumentacion.

Metodologjia

Ne shkruam një aplikacion të vogël që gjeneronte sekuenca të numrave të rastësishëm me secilën nga mënyrat e paraqitura dhe ruante rezultatet për përpunim të mëtejshëm.

Gjatësia e secilës sekuencë - 100,000 numra.
Diapazoni i vlerave të numrave të rastësishëm - [0, 100).

Të dhënat u mbledhën nga disa platforma të targetuara:

  • Windows
    — Unity v2018.3.14f1, Mënyra editori, Mono, .NET Standard 2.0
  • macOS
    — Unity v2018.3.14f1, Mënyra editori, Mono, .NET Standard 2.0
    — Unity v5.6.4p4, Mënyra editori, Mono, .NET Standard 2.0
  • Android
    — Unity v2018.3.14f1, ndërtim për pajisje, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, ndërtim për pajisje, il2cpp, .NET Standard 2.0

Implementimi

Kemi disa mënyra të ndryshme për të gjeneruar numra të rastësishëm. Për secilën prej tyre do të shkruajmë një klasë mbështjellëse të veçantë, e cila duhet të ofrojë:

  1. Mundësinë për të caktuar diapazonin e vlerave [min/max). Do të caktohet përmes konstruktorit.
  2. Një metodë që kthen numrin e rastësishëm. Si tip do të zgjedhim float, si më të përgjithshëm.
  3. Emri i metodës së gjenerimit për etiketimin e rezultateve. Për lehtësi, do të kthejmë emrin e plotë të klasës + emrin e metodës që përdoret për të gjeneruar numra të rastësishëm.

Fillimisht, do të shpallim një abstraksion që do të paraqitet nga intervista IRandomGenerator:

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

        float Gjenero();
    }
}

Implementimi i System.Random.Next()

Ky metodë lejon të përcaktohet një gamë vlerash, por ajo kthen numra të plotë (integer), ndërsa neve na duhen float. Mund të interpretojmë thjesht integer si float, ose mund të zgjasim gamën e vlerave disa rendesh, duke e kompensuar atë në çdo gjenerim të numrave të rastësishëm. Do të rezultojë diçka si fixed-point me saktësi të caktuar. Do ta përdorim këtë variant, pasi është më afër vlerave reale float.

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 Emri => "System.Random.Next()";


        public SystemIntegerRandomGenerator(float min, float max, int factor = DefaultFactor)
        {
            _min = (int)min * factor;
            _max = (int)max * factor;
            _factor = factor;
        }


        public float Gjenero() => (float)_generator.Next(_min, _max) / _factor;
    }
}

Implementimi i System.Random.NextDouble()

Këtu është një gamë fikse vlerash [0; 1). Për ta projektuar atë në gamën e përcaktuar në konstruktor, përdorim aritmetikë të thjeshtë: 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 Emri => "System.Random.NextDouble()";


        public SystemDoubleRandomGenerator(float min, float max)
        {
            _factor = max - min;
            _min = min;
        }


        public float Gjenero() => (float)(_generator.NextDouble() * _factor) + _min;
    }
}

Implementimi i UnityEngine.Random.Range()

Ky metodë e klasës statike UnityEngine.Random lejon të përcaktohet një gamë vlerash dhe kthen numra të rastësishëm të llojit float. Nuk do të ketë nevojë për konvertime të tjera.

using UnityEngine;

namespace RandomDistribution
{
    public class UnityRandomRangeGenerator : IRandomGenerator
    {
        private readonly float _min;
        private readonly float _max;


        public string Emri => "UnityEngine.Random.Range()";


        public UnityRandomRangeGenerator(float min, float max)
        {
            _min = min;
            _max = max;
        }


        public float Gjenero() => Random.Range(_min, _max);
    }
}

Implementimi i UnityEngine.Random.value

Vlera e pronës së klasës statike UnityEngine.Random kthen një numër të rastësishëm të tipit float nga një gamë e caktuar vlerash [0; 1). Ta projektojmë atë në gamën e caktuar në të njëjtën mënyrë siç është realizuar 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;
    }
}

Implementimi i Unity.Mathematics.Random.NextFloat()

Metoda NextFloat() e klasës Unity.Mathematics.Random kthen një numër të rastësishëm të tipit float dhe lejon caktimin e një gamë vlerash. Detaji është se çdo ekzemplar i Unity.Mathematics.Random duhet të inicializohet me një seed për të shmangur gjenerimin e sekuencave të përsëritura.

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

Implementimi i MainController

Disa implementime të IRandomGenerator janë gati. Tani duhet të gjenerojmë sekuenca dhe të ruajmë datasetin rezultat për përpunim. Për këtë, do të krijojmë një skenë në Unity dhe një skript të vogël MainController, i cili do të kryejë gjithë punën e nevojshme dhe do të përgjigjet gjithashtu për ndërveprimin me UI.

Do të caktosh madhësinë e datasetit dhe gamën e vlerave të numrave të rastësishëm, si dhe do të kem një metodë që kthen një array të gjeneruesve të konfiguruar dhe të gatshëm për punë.

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

        ...
    }
}

Tani tani jemi duke formuar një dataset. Në këtë rast, gjenerimi i të dhënave do të kombinohet me regjistrimin e rezultateve në një rrjedhë teksti (në formatin csv). Për ruajtjen e vlerave të secilit IRandomGenerator i jepet një kolonë e veçantë, ndërsa rreshti i parë përmban emrin e gjeneratorit.

namespace RandomDistribution
{
    public class MainController : MonoBehaviour
    {
        ...
		
        private void GenerateCsvDataSet(TextWriter writer, int dataSetSize, params IRandomGenerator[] generators)
        {
            const char separator = ',';
            int lastIdx = generators.Length - 1;

            // shkruaj titullin
            for (int j = 0; j <= lastIdx; j++)
            {
                writer.Write(generators[j].Name);
                if (j != lastIdx)
                    writer.Write(separator);
            }
            writer.WriteLine();

            // shkruaj të dhënat
            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();
            }
        }

        ...
    }
}

Tani duhet të thërrasim metodën GenerateCsvDataSet dhe të ruajmë rezultatin në një skedar, ose ta dërgojmë menjëherë të dhënat përmes rrjetit nga pajisja përfundimtare në atë pranuese. 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();
            }
        }

        ...
    }
}

Burimet e projektit ndodhen në GitLab.

Rezultatet

Nuk ndodhi ndonjë mrekulli. Ajo që pritëm, ishte ajo që morëm — në të gjitha rastet një shpërndarje e barabartë pa ndonjë aluzion për komplot. Nuk shoh ndonjë kuptim në shtimin e grafikëve të veçantë për platforma — të gjitha tregojnë rezultate përafërsisht të njëjta.

Realiteti është:
BlessRNG ose kontrollojmë RNG-në për ndershmëri

Vizualizimi i sekuencave në planin e të gjitha pesë mënyrave të gjenerimit:
BlessRNG ose kontrollojmë RNG-në për ndershmëri

Dhe vizualizimi në 3D. Do ta lë vetëm rezultatin e System.Random.Next(), për të mos krijuar një mori përmbajtjeje të njëjtë.
BlessRNG ose kontrollojmë RNG-në për ndershmëri

Historia e treguar në hyrje mbi shpërndarjen normale të UnityEngine.Random nuk u përsërit: ndoshta fillimisht ka qenë e gabuar, ose diçka ka ndryshuar që nga atëherë në motor. Por tani jemi të sigurt.

Burimi: habr.com

Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS 🔥 Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS - ProHoster