BlessRNG یا بررسی RNG برای انصاف

BlessRNG یا بررسی RNG برای انصاف

در توسعه بازی، شما اغلب نیاز دارید که چیزی را با تصادفی بودن گره بزنید: Unity برای این کار Random خود را دارد و به موازات آن System.Random نیز وجود دارد. روزی روزگاری، در یکی از پروژه ها، این تصور را داشتم که هر دو می توانند متفاوت کار کنند (اگرچه باید توزیع یکنواخت داشته باشند).

سپس آنها وارد جزئیات نشدند - کافی بود که انتقال به System.Random تمام مشکلات را اصلاح کرد. اکنون تصمیم گرفتیم با جزئیات بیشتری به آن بپردازیم و کمی تحقیق کنیم: RNGهای "طرفدار" یا قابل پیش بینی چقدر هستند و کدام یک را انتخاب کنیم. علاوه بر این، من بیش از یک بار نظرات متناقضی در مورد "صداقت" آنها شنیده ام - بیایید سعی کنیم بفهمیم نتایج واقعی چگونه با نتایج اعلام شده مقایسه می شود.

برنامه آموزشی مختصر یا RNG در واقع RNG است

اگر قبلاً با مولدهای اعداد تصادفی آشنا هستید، می توانید بلافاصله به بخش "تست" بروید.

اعداد تصادفی (RN) دنباله ای از اعداد هستند که با استفاده از فرآیندهای تصادفی (آشوب) تولید می شوند که منبع آنتروپی است. یعنی این دنباله ای است که عناصر آن با هیچ قانون ریاضی به هم مرتبط نیستند - آنها هیچ رابطه علت و معلولی ندارند.

چیزی که اعداد تصادفی را ایجاد می کند، مولد اعداد تصادفی (RNG) نامیده می شود. به نظر می رسد که همه چیز ابتدایی است، اما اگر از تئوری به عمل حرکت کنیم، در واقع پیاده سازی یک الگوریتم نرم افزاری برای تولید چنین دنباله ای چندان ساده نیست.

دلیل آن فقدان همان هرج و مرج در لوازم الکترونیکی مصرفی مدرن است. بدون آن، اعداد تصادفی دیگر تصادفی نیستند، و مولد آنها به یک تابع معمولی از آرگومان های واضح و مشخص تبدیل می شود. برای تعدادی از تخصص ها در زمینه IT، این یک مشکل جدی است (مثلاً رمزنگاری)، اما برای برخی دیگر یک راه حل کاملا قابل قبول وجود دارد.

لازم است الگوریتمی بنویسید که البته اعداد تصادفی واقعی نباشد، اما تا حد امکان به آنها نزدیک باشد - به اصطلاح اعداد شبه تصادفی (PRN). الگوریتم در این مورد، مولد عدد شبه تصادفی (PRNG) نامیده می شود.

چندین گزینه برای ایجاد یک PRNG وجود دارد، اما موارد زیر برای همه مرتبط خواهد بود:

  1. نیاز به مقداردهی اولیه اولیه

    PRNG منبع آنتروپی ندارد، بنابراین باید قبل از استفاده به آن حالت اولیه داده شود. به صورت عدد (یا بردار) مشخص می شود و دانه (دانه تصادفی) نامیده می شود. اغلب، شمارشگر ساعت پردازنده یا معادل عددی زمان سیستم به عنوان بذر استفاده می شود.

  2. تکرارپذیری توالی

    PRNG کاملاً قطعی است، بنابراین دانه مشخص شده در هنگام شروع اولیه به طور منحصر به فرد کل دنباله اعداد آینده را تعیین می کند. این بدان معناست که یک PRNG جداگانه که با همان seed مقداردهی شده است (در زمان‌های مختلف، در برنامه‌های مختلف، در دستگاه‌های مختلف) همان دنباله را ایجاد می‌کند.

شما همچنین باید توزیع احتمال مشخص کننده 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) است.

داده ها از چندین پلتفرم هدف جمع آوری شد:

  • ویندوز
    — Unity v2018.3.14f1، حالت ویرایشگر، مونو، دات نت استاندارد 2.0
  • از MacOS
    — Unity v2018.3.14f1، حالت ویرایشگر، مونو، دات نت استاندارد 2.0
    — Unity نسخه 5.6.4p4، حالت ویرایشگر، مونو، دات نت استاندارد 2.0
  • آندروید
    — Unity v2018.3.14f1، ساخت در هر دستگاه، مونو، دات نت استاندارد 2.0
  • IOS
    — Unity v2018.3.14f1، ساخت در هر دستگاه، il2cpp، NET Standard 2.0

اجرا

ما چندین روش مختلف برای تولید اعداد تصادفی داریم. برای هر یک از آنها یک کلاس wrapper جداگانه می نویسیم که باید ارائه دهد:

  1. امکان تنظیم محدوده مقادیر [min/max). از طریق سازنده تنظیم می شود.
  2. روش برگرداندن MF. بیایید شناور را به عنوان نوع انتخاب کنیم، زیرا کلی تر است.
  3. نام روش تولید برای علامت گذاری نتایج. برای راحتی، نام کامل کلاس + نام متد مورد استفاده برای تولید MF را به عنوان یک مقدار برمی گردانیم.

ابتدا، بیایید یک انتزاع را اعلام کنیم که با رابط IRandomGenerator نشان داده می شود:

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

        float Generate();
    }
}

پیاده سازی System.Random.Next()

این روش به شما امکان می دهد محدوده ای از مقادیر را تنظیم کنید، اما اعداد صحیح را برمی گرداند، اما شناورها مورد نیاز هستند. شما به سادگی می توانید عدد صحیح را به عنوان یک شناور تفسیر کنید، یا می توانید محدوده مقادیر را با چندین مرتبه بزرگی گسترش دهید، و آنها را با هر نسل از میدرنج جبران کنید. نتیجه چیزی شبیه یک نقطه ثابت با یک مرتبه دقت مشخص خواهد بود. ما از این گزینه استفاده خواهیم کرد زیرا به مقدار واقعی شناور نزدیکتر است.

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 * (حداکثر - 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 یک نوع شناور را از محدوده ثابتی از مقادیر [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 باید با مقداری seed مقداردهی اولیه شود - به این ترتیب از تولید توالی های تکراری جلوگیری می کنیم.

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

        ...
    }
}

منابع پروژه در گیتلب.

یافته ها

هیچ معجزه ای اتفاق نیفتاد آنچه آنها انتظار داشتند همان چیزی است که به دست آوردند - در همه موارد، توزیع یکنواخت بدون اشاره ای به توطئه. من در قرار دادن نمودارهای جداگانه برای پلتفرم ها فایده ای نمی بینم - همه آنها تقریباً نتایج یکسانی را نشان می دهند.

واقعیت این است:
BlessRNG یا بررسی RNG برای انصاف

تجسم توالی ها در یک صفحه از هر پنج روش نسل:
BlessRNG یا بررسی RNG برای انصاف

و تجسم به صورت سه بعدی من فقط نتیجه System.Random.Next() را می گذارم تا دسته ای از محتوای یکسان تولید نکنم.
BlessRNG یا بررسی RNG برای انصاف

داستانی که در مقدمه در مورد توزیع عادی UnityEngine.Random گفته شد تکرار نشد: یا در ابتدا اشتباه بود یا چیزی از آن زمان در موتور تغییر کرده است. اما حالا مطمئن شدیم.

منبع: www.habr.com

اضافه کردن نظر