BlessRNG atau semak RNG untuk keadilan

BlessRNG atau semak RNG untuk keadilan

Dalam pembangunan permainan, anda sering perlu mengikat sesuatu dengan rawak: Unity mempunyai Rawak sendiri untuk ini, dan selari dengannya terdapat System.Random. Pada suatu masa dahulu, pada salah satu projek, saya mendapat tanggapan bahawa kedua-duanya boleh berfungsi secara berbeza (walaupun mereka sepatutnya mempunyai pengagihan yang sama rata).

Kemudian mereka tidak pergi ke butiran - sudah cukup bahawa peralihan kepada System.Random membetulkan semua masalah. Kini kami memutuskan untuk menelitinya dengan lebih terperinci dan menjalankan sedikit penyelidikan: sejauh manakah RNG "berat sebelah" atau boleh diramal, dan yang mana satu untuk dipilih. Lebih-lebih lagi, saya telah lebih daripada sekali mendengar pendapat yang bercanggah tentang "kejujuran" mereka - mari kita cuba memikirkan bagaimana hasil sebenar dibandingkan dengan yang diisytiharkan.

Program pendidikan ringkas atau RNG sebenarnya adalah RNG

Jika anda sudah biasa dengan penjana nombor rawak, maka anda boleh segera melangkau ke bahagian "Pengujian".

Nombor rawak (RN) ialah urutan nombor yang dijana menggunakan beberapa proses rawak (huru-hara), sumber entropi. Iaitu, ini adalah urutan yang unsur-unsurnya tidak saling berkaitan oleh mana-mana undang-undang matematik - mereka tidak mempunyai hubungan sebab-akibat.

Apa yang mencipta nombor rawak dipanggil penjana nombor rawak (RNG). Nampaknya segala-galanya adalah asas, tetapi jika kita beralih dari teori ke amalan, maka sebenarnya tidak begitu mudah untuk melaksanakan algoritma perisian untuk menghasilkan urutan sedemikian.

Sebabnya terletak pada ketiadaan huru-hara yang sama dalam elektronik pengguna moden. Tanpa itu, nombor rawak tidak lagi menjadi rawak, dan penjananya bertukar menjadi fungsi biasa bagi hujah yang ditakrifkan dengan jelas. Untuk beberapa kepakaran dalam bidang IT, ini adalah masalah yang serius (contohnya, kriptografi), tetapi bagi yang lain terdapat penyelesaian yang boleh diterima sepenuhnya.

Ia adalah perlu untuk menulis algoritma yang akan kembali, walaupun bukan nombor rawak, tetapi sedekat mungkin dengan mereka - apa yang dipanggil nombor rawak semu (PRN). Algoritma dalam kes ini dipanggil penjana nombor pseudorandom (PRNG).

Terdapat beberapa pilihan untuk membuat PRNG, tetapi perkara berikut akan relevan untuk semua orang:

  1. Keperluan untuk permulaan awal.

    PRNG tidak mempunyai sumber entropi, jadi ia mesti diberi keadaan awal sebelum digunakan. Ia dinyatakan sebagai nombor (atau vektor) dan dipanggil benih (benih rawak). Selalunya, pembilang jam pemproses atau setara berangka masa sistem digunakan sebagai benih.

  2. Kebolehulangan urutan.

    PRNG adalah deterministik sepenuhnya, jadi benih yang ditentukan semasa pemulaan secara unik menentukan keseluruhan jujukan nombor masa hadapan. Ini bermakna PRNG berasingan yang dimulakan dengan benih yang sama (pada masa berbeza, dalam program berbeza, pada peranti berbeza) akan menjana jujukan yang sama.

Anda juga perlu mengetahui taburan kebarangkalian yang mencirikan PRNG - nombor yang akan dijana dan dengan kebarangkalian apa. Selalunya ini sama ada taburan normal atau taburan seragam.
BlessRNG atau semak RNG untuk keadilan
Taburan normal (kiri) dan taburan seragam (kanan)

Katakan kita mempunyai die yang adil dengan 24 sisi. Jika anda melambungnya, kebarangkalian untuk mendapat satu adalah sama dengan 1/24 (sama dengan kebarangkalian untuk mendapatkan sebarang nombor lain). Jika anda membuat banyak balingan dan merekodkan keputusan, anda akan perasan bahawa semua tepi jatuh dengan kekerapan yang lebih kurang sama. Pada asasnya, acuan ini boleh dianggap sebagai RNG dengan pengedaran seragam.

Bagaimana jika anda membaling 10 daripada dadu ini sekaligus dan mengira jumlah mata? Adakah keseragaman akan dikekalkan untuknya? Tidak. Selalunya, jumlahnya akan hampir kepada 125 mata, iaitu, kepada beberapa nilai purata. Dan akibatnya, walaupun sebelum membuat balingan, anda boleh menganggarkan secara kasar hasil masa hadapan.

Sebabnya ialah terdapat bilangan kombinasi yang paling banyak untuk mendapatkan skor purata. Semakin jauh daripadanya, semakin sedikit kombinasi - dan, dengan itu, semakin rendah kebarangkalian kerugian. Jika data ini divisualisasikan, ia akan kelihatan menyerupai bentuk loceng. Oleh itu, dengan beberapa regangan, sistem 10 dadu boleh dipanggil RNG dengan taburan normal.

Satu lagi contoh, hanya kali ini di atas kapal terbang - menembak pada sasaran. Penembak akan menjadi RNG yang menjana sepasang nombor (x, y) yang dipaparkan pada graf.
BlessRNG atau semak RNG untuk keadilan
Setuju bahawa pilihan di sebelah kiri lebih dekat dengan kehidupan sebenar - ini ialah RNG dengan taburan normal. Tetapi jika anda perlu menyebarkan bintang di langit yang gelap, maka pilihan yang tepat, diperoleh menggunakan RNG dengan pengedaran seragam, lebih sesuai. Secara umum, pilih penjana bergantung pada tugas yang sedang dijalankan.

Sekarang mari kita bercakap tentang entropi jujukan PNG. Sebagai contoh, terdapat urutan yang bermula seperti ini:

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

Seberapa rawak nombor ini pada pandangan pertama? Mari kita mulakan dengan menyemak pengedaran.
BlessRNG atau semak RNG untuk keadilan
Ia kelihatan hampir dengan seragam, tetapi jika anda membaca urutan dua nombor dan mentafsirkannya sebagai koordinat pada satah, anda mendapat ini:
BlessRNG atau semak RNG untuk keadilan
Corak menjadi jelas kelihatan. Dan oleh kerana data dalam urutan itu disusun mengikut cara tertentu (iaitu, ia mempunyai entropi yang rendah), ini boleh menimbulkan "bias" itu. Sekurang-kurangnya, PRNG sedemikian tidak begitu sesuai untuk menjana koordinat pada satah.

Urutan lain:

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

Segala-galanya nampaknya baik-baik saja di sini walaupun di dalam pesawat:
BlessRNG atau semak RNG untuk keadilan
Mari lihat dalam jumlah (baca tiga nombor pada satu masa):
BlessRNG atau semak RNG untuk keadilan
Dan sekali lagi corak. Tidak mungkin lagi untuk membina visualisasi dalam empat dimensi. Tetapi corak boleh wujud pada dimensi ini dan pada dimensi yang lebih besar.

Dalam kriptografi, di mana keperluan yang paling ketat dikenakan ke atas PRNG, keadaan sedemikian secara kategorinya tidak boleh diterima. Oleh itu, algoritma khas telah dibangunkan untuk menilai kualitinya, yang tidak akan kami sentuh sekarang. Topik ini luas dan patut diberi artikel berasingan.

Ujian

Jika kita tidak mengetahui sesuatu dengan pasti, maka bagaimana untuk bekerja dengannya? Adakah patut menyeberang jalan jika anda tidak tahu lampu isyarat mana yang membenarkannya? Akibatnya mungkin berbeza.

Perkara yang sama berlaku untuk rawak yang terkenal dalam Unity. Adalah baik jika dokumentasi mendedahkan butiran yang diperlukan, tetapi cerita yang disebutkan pada permulaan artikel berlaku dengan tepat kerana kekurangan spesifik yang dikehendaki.

Dan jika anda tidak tahu cara alat itu berfungsi, anda tidak akan dapat menggunakannya dengan betul. Secara umum, sudah tiba masanya untuk menyemak dan menjalankan eksperimen untuk memastikan sekurang-kurangnya tentang pengedaran.

Penyelesaiannya adalah mudah dan berkesan - kumpulkan statistik, dapatkan data objektif dan lihat hasilnya.

Subjek kajian

Terdapat beberapa cara untuk menjana nombor rawak dalam Unity - kami telah menguji lima.

  1. System.Random.Next(). Menghasilkan integer dalam julat nilai tertentu.
  2. System.Random.NextDouble(). Menghasilkan nombor kejituan berganda dalam julat daripada [0; 1).
  3. UnityEngine.Random.Range(). Menghasilkan nombor ketepatan tunggal (terapung) dalam julat nilai tertentu.
  4. UnityEngine.Random.value. Menghasilkan nombor ketepatan tunggal (terapung) dalam julat dari [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Sebahagian daripada perpustakaan Unity.Mathematics baharu. Menghasilkan nombor ketepatan tunggal (terapung) dalam julat nilai tertentu.

Hampir di mana-mana dalam dokumentasi pengedaran seragam telah ditentukan, kecuali UnityEngine.Random.value (di mana pengedaran tidak dinyatakan, tetapi dengan analogi dengan UnityEngine.Random.Range() seragam juga dijangka) dan Unity.Mathematics.Random .NextFloat() (di mana dalam Asas ialah algoritma xorshift, yang bermaksud sekali lagi anda perlu menunggu pengedaran seragam).

Secara lalai, hasil yang dijangkakan telah diambil seperti yang dinyatakan dalam dokumentasi.

Metodologi

Kami menulis aplikasi kecil yang menghasilkan urutan nombor rawak menggunakan setiap kaedah yang dibentangkan dan menyimpan keputusan untuk pemprosesan selanjutnya.

Panjang setiap jujukan ialah 100 nombor.
Julat nombor rawak ialah [0, 100).

Data dikumpul daripada beberapa platform sasaran:

  • Windows
    β€” Unity v2018.3.14f1, Mod Editor, Mono, .NET Standard 2.0
  • MacOS
    β€” Unity v2018.3.14f1, Mod Editor, Mono, .NET Standard 2.0
    β€” Unity v5.6.4p4, Mod Editor, Mono, .NET Standard 2.0
  • Android
    β€” Unity v2018.3.14f1, binaan setiap peranti, Mono, .NET Standard 2.0
  • iOS
    β€” Unity v2018.3.14f1, binaan setiap peranti, il2cpp, .NET Standard 2.0

РСализация

Kami mempunyai beberapa cara berbeza untuk menjana nombor rawak. Untuk setiap daripada mereka, kami akan menulis kelas pembalut yang berasingan, yang sepatutnya menyediakan:

  1. Kemungkinan untuk menetapkan julat nilai [min/maks). Akan ditetapkan melalui pembina.
  2. Kaedah mengembalikan MF. Mari pilih apungan sebagai jenis, kerana ia lebih umum.
  3. Nama kaedah penjanaan untuk menandakan keputusan. Untuk kemudahan, kami akan mengembalikan sebagai nilai nama penuh kelas + nama kaedah yang digunakan untuk menjana MF.

Pertama, mari kita isytiharkan abstraksi yang akan diwakili oleh antara muka IRandomGenerator:

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

        float Generate();
    }
}

Pelaksanaan System.Random.Next()

Kaedah ini membolehkan anda menetapkan julat nilai, tetapi ia mengembalikan integer, tetapi terapung diperlukan. Anda hanya boleh mentafsirkan integer sebagai apungan, atau anda boleh mengembangkan julat nilai dengan beberapa susunan magnitud, mengimbanginya dengan setiap generasi julat pertengahan. Hasilnya akan menjadi sesuatu seperti titik tetap dengan susunan ketepatan yang diberikan. Kami akan menggunakan pilihan ini kerana ia lebih dekat dengan nilai apungan sebenar.

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

Pelaksanaan System.Random.NextDouble()

Di sini julat tetap nilai [0; 1). Untuk menayangkannya pada yang ditentukan dalam pembina, kami menggunakan aritmetik mudah: X * (maks βˆ’ 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;
    }
}

Pelaksanaan UnityEngine.Random.Range()

Kaedah kelas statik UnityEngine.Random ini membolehkan anda menetapkan julat nilai dan mengembalikan jenis apungan. Anda tidak perlu melakukan sebarang transformasi tambahan.

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

Pelaksanaan UnityEngine.Random.value

Sifat nilai kelas statik UnityEngine.Random mengembalikan jenis apungan daripada julat nilai tetap [0; 1). Mari kita unjurkannya ke julat tertentu dengan cara yang sama seperti semasa melaksanakan 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;
    }
}

Pelaksanaan Unity.Mathematics.Random.NextFloat()

Kaedah NextFloat() kelas Unity.Mathematics.Random mengembalikan titik terapung jenis apungan dan membolehkan anda menentukan julat nilai. Satu-satunya nuansa ialah setiap contoh Unity.Mathematics.Random perlu dimulakan dengan beberapa benih - dengan cara ini kita akan mengelak daripada menjana urutan berulang.

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

Pelaksanaan MainController

Beberapa pelaksanaan IRandomGenerator sudah sedia. Seterusnya, anda perlu menjana jujukan dan menyimpan set data yang terhasil untuk diproses. Untuk melakukan ini, kami akan mencipta adegan dan skrip MainController kecil dalam Unity, yang akan melakukan semua kerja yang diperlukan dan pada masa yang sama bertanggungjawab untuk interaksi dengan UI.

Mari kita tetapkan saiz set data dan julat nilai MF, dan juga dapatkan kaedah yang mengembalikan tatasusunan penjana yang dikonfigurasikan dan sedia untuk berfungsi.

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

        ...
    }
}

Sekarang mari buat set data. Dalam kes ini, penjanaan data akan digabungkan dengan merekodkan hasil ke dalam aliran teks (dalam format csv). Untuk menyimpan nilai setiap IRandomGenerator, lajur berasingannya sendiri diperuntukkan, dan baris pertama mengandungi Nama penjana.

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

        ...
    }
}

Apa yang tinggal ialah memanggil kaedah GenerateCsvDataSet dan menyimpan hasilnya ke fail, atau segera memindahkan data melalui rangkaian dari peranti akhir ke pelayan penerima.

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

        ...
    }
}

Sumber projek adalah di GitLab.

Penemuan

Tiada keajaiban berlaku. Apa yang mereka harapkan adalah apa yang mereka dapat - dalam semua kes, pengedaran yang sama rata tanpa sedikit pun konspirasi. Saya tidak nampak gunanya meletakkan graf berasingan untuk platform - semuanya menunjukkan hasil yang lebih kurang sama.

Realitinya begini:
BlessRNG atau semak RNG untuk keadilan

Visualisasi jujukan pada satah dari kesemua lima kaedah generasi:
BlessRNG atau semak RNG untuk keadilan

Dan visualisasi dalam 3D. Saya akan meninggalkan hanya hasil System.Random.Next() supaya tidak menghasilkan sekumpulan kandungan yang sama.
BlessRNG atau semak RNG untuk keadilan

Kisah yang diceritakan dalam pengenalan tentang pengedaran normal UnityEngine.Random tidak berulang: sama ada ia pada mulanya salah, atau sesuatu telah berubah dalam enjin. Tetapi sekarang kita pasti.

Sumber: www.habr.com

Tambah komen