BlessRNG կամ RNG-ի արդարության ստուգում

BlessRNG կամ RNG-ի արդարության ստուգում

Խաղերի մշակման ժամանակ հաճախ պետք է ինչ-որ բան կապել պատահականության հետ. Unity-ն դրա համար ունի իր Random-ը, և դրան զուգահեռ կա System.Random-ը: Ժամանակին նախագծերից մեկի վրա տպավորություն ստեղծվեց, որ երկուսն էլ կարող են տարբեր կերպ աշխատել (թեև պետք է հավասարաչափ բաշխում ունենան):

Այնուհետև նրանք չխորացան, բավական էր, որ անցումը System.Random-ին շտկեց բոլոր խնդիրները: Այժմ մենք որոշեցինք ավելի մանրամասն ուսումնասիրել այն և մի փոքր հետազոտություն անցկացնել՝ որքանով են «կողմնակալ» կամ կանխատեսելի RNG-ները և որն ընտրել: Ավելին, ես մեկ անգամ չէ, որ լսել եմ հակասական կարծիքներ նրանց «ազնվության» մասին. եկեք փորձենք պարզել, թե իրական արդյունքները ինչպես են համեմատվում հայտարարվածների հետ:

Համառոտ կրթական ծրագիր կամ RNG իրականում RNG է

Եթե ​​դուք արդեն ծանոթ եք պատահական թվերի գեներատորներին, ապա կարող եք անմիջապես բաց թողնել «Թեստավորում» բաժինը:

Պատահական թվերը (RN) թվերի հաջորդականություն են, որոնք առաջանում են ինչ-որ պատահական (քաոսային) գործընթացի միջոցով՝ էնտրոպիայի աղբյուր։ Այսինքն՝ սա մի հաջորդականություն է, որի տարրերը փոխկապակցված չեն որևէ մաթեմատիկական օրենքով՝ չունեն պատճառահետևանքային կապ։

Այն, ինչ ստեղծում է պատահական թիվը, կոչվում է պատահական թվերի գեներատոր (RNG): Թվում է, թե ամեն ինչ տարրական է, բայց եթե տեսությունից անցնենք պրակտիկայի, ապա իրականում այդքան էլ պարզ չէ նման հաջորդականություն ստեղծելու ծրագրային ալգորիթմ իրականացնելը։

Պատճառը նույն քաոսի բացակայությունն է ժամանակակից սպառողական էլեկտրոնիկայի մեջ։ Առանց դրա, պատահական թվերը դադարում են պատահական լինել, և դրանց գեներատորը վերածվում է ակնհայտորեն սահմանված արգումենտների սովորական ֆունկցիայի։ ՏՏ ոլորտի մի շարք մասնագիտությունների համար սա լուրջ խնդիր է (օրինակ՝ կրիպտոգրաֆիա), բայց մյուսների համար կա միանգամայն ընդունելի լուծում։

Անհրաժեշտ է գրել ալգորիթմ, որը կվերադարձնի, թեկուզ ոչ իսկապես պատահական թվեր, բայց հնարավորինս մոտ նրանց՝ այսպես կոչված կեղծ պատահական թվեր (PRN): Ալգորիթմն այս դեպքում կոչվում է կեղծ պատահական թվերի գեներատոր (PRNG):

PRNG ստեղծելու մի քանի տարբերակ կա, բայց հետևյալը տեղին կլինի բոլորի համար.

  1. Նախնական սկզբնավորման անհրաժեշտությունը:

    PRNG-ը չունի էնտրոպիայի աղբյուր, ուստի օգտագործելուց առաջ նրան պետք է նախնական վիճակ տրվի: Այն նշվում է որպես թիվ (կամ վեկտոր) և կոչվում է սերմ (պատահական սերմ): Հաճախ որպես սերմ օգտագործվում է պրոցեսորի ժամաչափը կամ համակարգի ժամանակի թվային համարժեքը:

  2. Հերթականության վերարտադրելիություն:

    PRNG-ը լիովին դետերմինիստական ​​է, ուստի սկզբնավորման ժամանակ նշված սերմը եզակիորեն որոշում է թվերի ամբողջ ապագա հաջորդականությունը: Սա նշանակում է, որ նույն սերմով սկզբնավորվող առանձին PRNG (տարբեր ժամանակներում, տարբեր ծրագրերում, տարբեր սարքերում) կստեղծի նույն հաջորդականությունը:

Դուք նաև պետք է իմանաք PRNG-ը բնութագրող հավանականության բաշխումը. ինչ թվեր կստեղծի այն և ինչ հավանականությամբ: Ամենից հաճախ սա կամ նորմալ բաշխում է կամ միասնական բաշխում:
BlessRNG կամ RNG-ի արդարության ստուգում
Նորմալ բաշխում (ձախ) և միասնական բաշխում (աջ)

Ենթադրենք, մենք ունենք արդար մահ 24 կողմերով: Եթե ​​նետեք այն, մեկ ստանալու հավանականությունը հավասար կլինի 1/24-ի (նույնը, ինչ ցանկացած այլ թիվ ստանալու հավանականությունը): Եթե ​​դուք շատ նետումներ կատարեք և գրանցեք արդյունքները, ապա կնկատեք, որ բոլոր եզրերը մոտավորապես նույն հաճախականությամբ են ընկնում։ Ըստ էության, այս դիակը կարելի է համարել միատեսակ բաշխմամբ RNG:

Իսկ եթե այս զառերից միանգամից 10-ը գցես և հաշվես ընդհանուր միավորները: Միատեսակությունը կպահպանվի՞ դրա համար։ Ոչ Ամենից հաճախ գումարը մոտ կլինի 125 միավորին, այսինքն՝ ինչ-որ միջին արժեքի։ Եվ արդյունքում նույնիսկ նետում կատարելուց առաջ կարելի է մոտավորապես գնահատել ապագա արդյունքը։

Պատճառն այն է, որ միջին միավոր ստանալու համար կոմբինացիաների ամենամեծ քանակն է: Որքան հեռու է դրանից, այնքան քիչ համակցություններ, և, համապատասխանաբար, այնքան ցածր է կորստի հավանականությունը: Եթե ​​այս տվյալները պատկերացվեն, ապա դրանք անորոշ կերպով նման կլինեն զանգի ձևին: Հետևաբար, որոշակի ձգման դեպքում 10 զառերի համակարգը կարելի է անվանել նորմալ բաշխմամբ RNG:

Մեկ այլ օրինակ՝ միայն այս անգամ ինքնաթիռում՝ կրակել թիրախի վրա։ Կրակողը կլինի RNG, որը առաջացնում է զույգ թվեր (x, y), որոնք ցուցադրվում են գրաֆիկի վրա:
BlessRNG կամ RNG-ի արդարության ստուգում
Համաձայնեք, որ ձախ կողմում գտնվող տարբերակը ավելի մոտ է իրական կյանքին. սա նորմալ բաշխմամբ RNG է: Բայց եթե ձեզ անհրաժեշտ է աստղեր ցրել մութ երկնքում, ապա ճիշտ տարբերակը, որը ստացվել է RNG-ի միջոցով միասնական բաշխմամբ, ավելի հարմար է: Ընդհանուր առմամբ, ընտրեք գեներատոր՝ կախված առաջադրանքից:

Այժմ խոսենք PNG հաջորդականության էնտրոպիայի մասին։ Օրինակ, կա մի հաջորդականություն, որը սկսվում է այսպես.

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

Որքանո՞վ են պատահական այս թվերն առաջին հայացքից: Սկսենք բաշխումը ստուգելուց:
BlessRNG կամ RNG-ի արդարության ստուգում
Այն կարծես թե մոտ է համազգեստին, բայց եթե դուք կարդաք երկու թվերի հաջորդականություն և մեկնաբանեք դրանք որպես կոորդինատներ հարթության վրա, կստանաք հետևյալը.
BlessRNG կամ RNG-ի արդարության ստուգում
Նախշերը դառնում են հստակ տեսանելի: Եվ քանի որ հաջորդականության տվյալները դասավորված են որոշակի ձևով (այսինքն՝ այն ունի ցածր էնտրոպիա), դա կարող է հենց այդ «կողմնակալության» պատճառ դառնալ։ Նվազագույնը, նման PRNG-ն այնքան էլ հարմար չէ ինքնաթիռում կոորդինատներ ստեղծելու համար:

Մեկ այլ հաջորդականություն.

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

Այստեղ ամեն ինչ լավ է թվում նույնիսկ ինքնաթիռում.
BlessRNG կամ RNG-ի արդարության ստուգում
Եկեք նայենք ծավալով (կարդացեք միանգամից երեք թիվ).
BlessRNG կամ RNG-ի արդարության ստուգում
Եվ կրկին նախշերը. Այլևս անհնար է պատկերացում կազմել չորս հարթություններում: Բայց նախշերը կարող են գոյություն ունենալ այս հարթության վրա և ավելի մեծերի վրա:

Կրիպտոգրաֆիայում, որտեղ ամենախիստ պահանջները դրված են PRNG-ների նկատմամբ, նման իրավիճակը կտրականապես անընդունելի է։ Ուստի դրանց որակը գնահատելու համար մշակվել են հատուկ ալգորիթմներ, որոնց այժմ չենք անդրադառնա։ Թեման ծավալուն է և արժանի է առանձին հոդվածի։

Փորձարկում

Եթե ​​մենք ինչ-որ բան հաստատ չգիտենք, ապա ինչպե՞ս աշխատել դրա հետ: Արժե՞ արդյոք անցնել ճանապարհը, եթե չգիտես, թե որ լուսացույցն է դա թույլ տալիս: Հետևանքները կարող են տարբեր լինել։

Նույնը վերաբերում է Unity-ի տխրահռչակ պատահականությանը: Լավ է, եթե փաստաթղթերը բացահայտեն անհրաժեշտ մանրամասները, բայց հոդվածի սկզբում նշված պատմությունը տեղի է ունեցել հենց ցանկալի առանձնահատկությունների բացակայության պատճառով:

Եվ եթե չգիտեք, թե ինչպես է աշխատում գործիքը, չեք կարողանա այն ճիշտ օգտագործել: Ընդհանրապես, եկել է ժամանակը ստուգելու և փորձարկում անցկացնելու, որպեսզի վերջապես համոզվենք գոնե բաշխման հարցում։

Լուծումը պարզ էր և արդյունավետ՝ հավաքել վիճակագրություն, ստանալ օբյեկտիվ տվյալներ և դիտել արդյունքները:

Ուսումնասիրության առարկա

Unity-ում պատահական թվեր ստեղծելու մի քանի եղանակ կա. մենք փորձարկեցինք հինգը:

  1. System.Random.Next(): Առաջացնում է ամբողջ թվեր արժեքների տրված միջակայքում:
  2. System.Random.NextDouble(): Ստեղծում է կրկնակի ճշգրիտ թվեր [0; 1).
  3. UnityEngine.Random.Range(): Առաջացնում է մեկ ճշգրիտ թվեր (լողացողներ) արժեքների տրված միջակայքում:
  4. UnityEngine.Random.value. Առաջացնում է մեկ ճշգրիտ թվեր (լողացողներ) [0; 1).
  5. Unity.Mathematics.Random.NextFloat(): Unity.Mathematics նոր գրադարանի մի մասը։ Առաջացնում է մեկ ճշգրիտ թվեր (լողացողներ) արժեքների տրված միջակայքում:

Փաստաթղթում գրեթե ամենուր նշվում էր միատեսակ բաշխում, բացառությամբ UnityEngine.Random.value-ի (որտեղ բաշխումը նշված չէր, բայց UnityEngine.Random.Range()-ի անալոգիայով նույնպես սպասվում էր համազգեստ) և Unity.Mathematics.Random: .NextFloat() (որտեղ հիմքում գտնվում է xorshift ալգորիթմը, ինչը նշանակում է, որ նորից պետք է սպասել միասնական բաշխման):

Լռելյայնորեն, ակնկալվող արդյունքները ընդունվել են այնպես, ինչպես նշված են փաստաթղթերում:

Մեթոդաբանություն

Մենք գրեցինք մի փոքրիկ հավելված, որը ստեղծեց պատահական թվերի հաջորդականություններ՝ օգտագործելով ներկայացված մեթոդներից յուրաքանչյուրը և պահպանեց արդյունքները հետագա մշակման համար։

Յուրաքանչյուր հաջորդականության երկարությունը 100 թիվ է։
Պատահական թվերի միջակայքը [0, 100):

Տվյալները հավաքագրվել են մի քանի թիրախային հարթակներից.

  • Windows
    — Unity v2018.3.14f1, Խմբագրի ռեժիմ, Մոնո, .NET Standard 2.0
  • MacOS
    — Unity v2018.3.14f1, Խմբագրի ռեժիմ, Մոնո, .NET Standard 2.0
    — Unity v5.6.4p4, Խմբագրի ռեժիմ, Մոնո, .NET Standard 2.0
  • Android
    — Unity v2018.3.14f1, կառուցում մեկ սարքի համար, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, կառուցում մեկ սարքի համար, il2cpp, .NET Standard 2.0

Իրականացման

Մենք ունենք պատահական թվեր ստեղծելու մի քանի տարբեր եղանակներ: Նրանցից յուրաքանչյուրի համար մենք կգրենք առանձին փաթաթման դաս, որը պետք է ապահովի.

  1. Արժեքների միջակայքը սահմանելու հնարավորություն [min/max): Կսահմանվի կոնստրուկտորի միջոցով:
  2. MF վերադարձնելու մեթոդ. Որպես տեսակ ընտրենք float, քանի որ այն ավելի ընդհանուր է։
  3. Արդյունքները նշելու գեներացիայի մեթոդի անվանումը. Հարմարության համար մենք որպես արժեք կվերադարձնենք դասի լրիվ անվանումը + MF-ի ստեղծման համար օգտագործվող մեթոդի անվանումը։

Նախ, եկեք հայտարարենք աբստրակցիա, որը կներկայացվի IRandomGenerator ինտերֆեյսով.

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

        float Generate();
    }
}

System.Random.Next()-ի ներդրում

Այս մեթոդը թույլ է տալիս սահմանել արժեքների մի շարք, սակայն այն վերադարձնում է ամբողջ թվեր, սակայն անհրաժեշտ են լողացողներ: Դուք կարող եք պարզապես մեկնաբանել ամբողջ թիվը որպես լողացող, կամ կարող եք ընդլայնել արժեքների տիրույթը մեծության մի քանի կարգով՝ դրանք փոխհատուցելով միջին միջակայքի յուրաքանչյուր սերնդի հետ: Արդյունքը կլինի ֆիքսված կետի նման մի բան՝ որոշակի ճշգրտության կարգով: Մենք կօգտագործենք այս տարբերակը, քանի որ այն ավելի մոտ է իրական 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 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()-ի ներդրում

Այստեղ արժեքների ֆիքսված միջակայքը [0; 1). Այն կոնստրուկտորում նշվածի վրա նախագծելու համար մենք օգտագործում ենք պարզ թվաբանություն՝ 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()-ի իրականացում

UnityEngine.Random ստատիկ դասի այս մեթոդը թույլ է տալիս սահմանել մի շարք արժեքներ և վերադարձնում է float տեսակը: Դուք չունեք լրացուցիչ փոխակերպումներ:

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-ի իրականացում

UnityEngine.Random ստատիկ դասի արժեքային հատկությունը վերադարձնում է float տեսակ արժեքների ֆիքսված միջակայքից [0; 1). Եկեք այն նախագծենք տրված տիրույթի վրա այնպես, ինչպես 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 դասի NextFloat() մեթոդը վերադարձնում է float տեսակի լողացող կետ և թույլ է տալիս նշել արժեքների մի շարք: Միակ նրբերանգն այն է, որ Unity.Mathematics.Random-ի յուրաքանչյուր օրինակ պետք է նախաստորագրվի որոշակի սերմով. այս կերպ մենք կխուսափենք կրկնվող հաջորդականությունների գեներացումից:

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

MainController-ի ներդրում

IRandomGenerator-ի մի քանի ներդրում պատրաստ են։ Հաջորդը, դուք պետք է ստեղծեք հաջորդականություններ և պահպանեք ստացված տվյալների բազան մշակման համար: Դա անելու համար մենք Unity-ում կստեղծենք տեսարան և փոքրիկ MainController սկրիպտ, որը կկատարի բոլոր անհրաժեշտ աշխատանքները և միևնույն ժամանակ պատասխանատու կլինի UI-ի հետ փոխգործակցության համար։

Եկեք սահմանենք տվյալների հավաքածուի չափը և MF արժեքների տիրույթը, ինչպես նաև ստանանք մեթոդ, որը վերադարձնում է կազմաձևված և աշխատանքի պատրաստ գեներատորների զանգված:

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

        ...
    }
}

Հիմա եկեք ստեղծենք տվյալների բազա: Այս դեպքում տվյալների ստեղծումը կզուգակցվի արդյունքների գրանցման հետ տեքստային հոսքի մեջ (csv ձևաչափով): Յուրաքանչյուր IRandomGenerator-ի արժեքները պահելու համար հատկացվում է իր առանձին սյունակը, և առաջին տողը պարունակում է գեներատորի անունը:

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

        ...
    }
}

Մնում է միայն զանգահարել GenerateCsvDataSet մեթոդը և արդյունքը պահել ֆայլում կամ անմիջապես ցանցի միջոցով փոխանցել տվյալները վերջնական սարքից ստացող սերվեր:

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

        ...
    }
}

Ծրագրի աղբյուրները գտնվում են Գիտլաբը.

Արդյունքները

Ոչ մի հրաշք տեղի չի ունեցել. Այն, ինչ նրանք ակնկալում էին, ստացան. բոլոր դեպքերում հավասարաչափ բաշխում, առանց դավադրության նշույլի: Ես հարթակների համար առանձին գծապատկերներ դնելու իմաստ չեմ տեսնում, դրանք բոլորը ցույց են տալիս մոտավորապես նույն արդյունքները:

Իրականությունը սա է.
BlessRNG կամ RNG-ի արդարության ստուգում

Հարթության վրա հաջորդականությունների պատկերացում բոլոր հինգ սերնդի մեթոդներից.
BlessRNG կամ RNG-ի արդարության ստուգում

Եվ վիզուալիզացիա 3D-ում: Ես կթողնեմ միայն System.Random.Next()-ի արդյունքը, որպեսզի չստեղծեմ միանման բովանդակություն:
BlessRNG կամ RNG-ի արդարության ստուգում

UnityEngine.Random-ի նորմալ բաշխման մասին ներածության մեջ պատմված պատմությունը չկրկնվեց. կամ ի սկզբանե սխալ էր, կամ դրանից հետո ինչ-որ բան փոխվեց շարժիչում: Բայց հիմա մենք համոզված ենք.

Source: www.habr.com

Добавить комментарий