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() (каде во The base е алгоритмот 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
  • Андроид
    — Unity v2018.3.14f1, изградба по уред, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, изградба по уред, il2cpp, .NET Standard 2.0

Реализация

Имаме неколку различни начини за генерирање случајни броеви. За секој од нив, ќе напишеме посебна класа на обвивка, која треба да обезбеди:

  1. Можност за поставување на опсегот на вредности [мин/макс). Ќе се постави преку конструкторот.
  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()

Методот NextFloat() од класата Unity.Mathematics.Random враќа подвижна точка од типот 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. Следно, треба да генерирате секвенци и да ја зачувате добиената база на податоци за обработка. За да го направите ова, ќе создадеме сцена и мала скрипта за MainController во Unity, која ќе ја изврши целата потребна работа и во исто време ќе биде одговорна за интеракција со 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();
            }
        }

        ...
    }
}

Изворите на проектот се на GitLab.

Наоди

Не се случи чудо. Она што го очекуваа е она што го добија - во сите случаи, рамномерна распределба без навестување за заговори. Не ја гледам поентата во поставувањето посебни графикони за платформи - сите тие покажуваат приближно исти резултати.

Реалноста е:
BlessRNG или проверка на RNG за правичност

Визуелизација на секвенци на рамнина од сите пет методи на генерирање:
BlessRNG или проверка на RNG за правичност

И визуелизација во 3Д. Ќе го оставам само резултатот од System.Random.Next() за да не произведам куп идентична содржина.
BlessRNG или проверка на RNG за правичност

Приказната раскажана во воведот за нормалната дистрибуција на UnityEngine.Random не се повтори: или првично беше погрешна, или оттогаш нешто се промени во моторот. Но, сега сме сигурни.

Извор: www.habr.com

Додадете коментар