BlessRNG atau periksa keadilan RNG

BlessRNG atau periksa keadilan RNG

Dalam pengembangan game, Anda sering kali perlu menghubungkan sesuatu dengan keacakan: Unity memiliki Randomnya sendiri untuk ini, dan bersamaan dengan itu ada System.Random. Suatu ketika, di salah satu proyek, saya mendapat kesan bahwa keduanya bisa bekerja secara berbeda (walaupun distribusinya harus merata).

Kemudian mereka tidak menjelaskan secara detail - transisi ke System.Random sudah cukup untuk memperbaiki semua masalah. Sekarang kami memutuskan untuk melihatnya lebih detail dan melakukan sedikit riset: seberapa “bias” atau dapat diprediksinya RNG, dan mana yang harus dipilih. Selain itu, saya telah mendengar pendapat yang bertentangan tentang "kejujuran" mereka lebih dari sekali - mari kita coba mencari tahu bagaimana hasil sebenarnya dibandingkan dengan hasil yang dinyatakan.

Program pendidikan singkat atau RNG sebenarnya adalah RNG

Jika Anda sudah familiar dengan generator nomor acak, maka Anda dapat langsung melompat ke bagian “Pengujian”.

Bilangan acak (RN) adalah rangkaian bilangan yang dihasilkan dengan menggunakan proses acak (kacau), yang merupakan sumber entropi. Artinya, ini adalah barisan yang elemen-elemennya tidak saling berhubungan oleh hukum matematika apa pun - mereka tidak memiliki hubungan sebab-akibat.

Apa yang menghasilkan angka acak disebut generator angka acak (RNG). Tampaknya semuanya mendasar, tetapi jika kita beralih dari teori ke praktik, maka sebenarnya tidak mudah menerapkan algoritma perangkat lunak untuk menghasilkan urutan seperti itu.

Alasannya terletak pada tidak adanya kekacauan yang sama dalam perangkat elektronik konsumen modern. Tanpanya, bilangan acak tidak lagi acak, dan generatornya berubah menjadi fungsi biasa dari argumen yang ditentukan dengan jelas. Untuk sejumlah spesialisasi di bidang TI, ini adalah masalah serius (misalnya, kriptografi), tetapi bagi yang lain, ada solusi yang sepenuhnya dapat diterima.

Penting untuk menulis algoritma yang akan mengembalikan, meskipun bukan bilangan acak, tetapi sedekat mungkin dengan bilangan tersebut - yang disebut bilangan acak semu (PRN). Algoritma dalam hal ini disebut pseudorandom number generator (PRNG).

Ada beberapa opsi untuk membuat PRNG, tetapi berikut ini relevan untuk semua orang:

  1. Kebutuhan untuk inisialisasi awal.

    PRNG tidak memiliki sumber entropi, sehingga harus diberikan keadaan awal sebelum digunakan. Ini ditentukan sebagai angka (atau vektor) dan disebut benih (benih acak). Seringkali, penghitung jam prosesor atau setara numerik dengan waktu sistem digunakan sebagai benih.

  2. Reproduksibilitas urutan.

    PRNG sepenuhnya deterministik, sehingga seed yang ditentukan selama inisialisasi secara unik menentukan seluruh rangkaian angka di masa depan. Ini berarti bahwa PRNG terpisah yang diinisialisasi dengan seed yang sama (pada waktu berbeda, dalam program berbeda, pada perangkat berbeda) akan menghasilkan urutan yang sama.

Anda juga perlu mengetahui distribusi probabilitas yang menjadi ciri PRNG - angka apa yang akan dihasilkannya dan berapa probabilitasnya. Paling sering ini adalah distribusi normal atau distribusi seragam.
BlessRNG atau periksa keadilan RNG
Distribusi normal (kiri) dan distribusi seragam (kanan)

Misalkan kita mempunyai sebuah dadu dengan 24 sisi. Jika dilempar, peluang terambilnya angka satu sama dengan 1/24 (sama dengan peluang terambilnya angka lainnya). Jika Anda melakukan banyak lemparan dan mencatat hasilnya, Anda akan melihat bahwa semua sisinya rontok dengan frekuensi yang kira-kira sama. Intinya, dadu ini dapat dianggap sebagai RNG dengan distribusi seragam.

Bagaimana jika Anda melempar 10 dadu ini sekaligus dan menghitung total poinnya? Apakah keseragaman akan dipertahankan? TIDAK. Seringkali, jumlahnya mendekati 125 poin, yaitu nilai rata-rata tertentu. Dan sebagai hasilnya, bahkan sebelum melakukan lemparan, Anda dapat memperkirakan secara kasar hasil di masa depan.

Pasalnya, terdapat jumlah kombinasi terbanyak untuk memperoleh skor rata-rata. Semakin jauh darinya, semakin sedikit kombinasinya - dan, karenanya, semakin rendah kemungkinan kerugiannya. Jika data ini divisualisasikan, secara samar-samar akan menyerupai bentuk lonceng. Oleh karena itu, dengan sedikit peregangan, sistem 10 dadu dapat disebut RNG dengan distribusi normal.

Contoh lain, hanya kali ini di pesawat - menembak sasaran. Penembaknya adalah RNG yang menghasilkan sepasang angka (x, y) yang ditampilkan pada grafik.
BlessRNG atau periksa keadilan RNG
Setuju bahwa opsi di sebelah kiri lebih dekat dengan kehidupan nyata - ini adalah RNG dengan distribusi normal. Namun jika Anda perlu menyebarkan bintang di langit yang gelap, maka opsi yang tepat, diperoleh menggunakan RNG dengan distribusi seragam, lebih cocok. Secara umum, pilih generator tergantung pada tugas yang ada.

Sekarang mari kita bicara tentang entropi barisan PNG. Misalnya, ada urutan yang dimulai 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, ...

Sekilas seberapa acak angka-angka ini? Mari kita mulai dengan memeriksa distribusinya.
BlessRNG atau periksa keadilan RNG
Kelihatannya hampir seragam, tetapi jika Anda membaca barisan dua angka dan menafsirkannya sebagai koordinat pada bidang, Anda mendapatkan ini:
BlessRNG atau periksa keadilan RNG
Pola menjadi terlihat jelas. Dan karena data dalam urutan tersebut diurutkan dengan cara tertentu (yaitu, memiliki entropi rendah), hal ini dapat menimbulkan “bias” tersebut. Minimal, PRNG seperti itu tidak cocok untuk menghasilkan koordinat di pesawat.

Urutan lainnya:

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

Semuanya tampak baik-baik saja di sini bahkan di pesawat:
BlessRNG atau periksa keadilan RNG
Mari kita lihat volumenya (baca tiga angka sekaligus):
BlessRNG atau periksa keadilan RNG
Dan lagi polanya. Visualisasi empat dimensi sudah tidak mungkin lagi dikonstruksi. Namun pola bisa ada pada dimensi ini dan pada dimensi yang lebih besar.

Dalam kriptografi, yang memberlakukan persyaratan paling ketat pada PRNG, situasi seperti ini sangat tidak dapat diterima. Oleh karena itu, algoritma khusus telah dikembangkan untuk menilai kualitasnya, yang tidak akan kami bahas sekarang. Topiknya sangat luas dan layak mendapat artikel terpisah.

Pengujian

Jika kita tidak mengetahui sesuatu secara pasti, lalu bagaimana cara mengatasinya? Apakah layak menyeberang jalan jika Anda tidak tahu lampu lalu lintas mana yang mengizinkannya? Konsekuensinya mungkin berbeda.

Hal yang sama berlaku untuk keacakan yang terkenal di Unity. Ada baiknya jika dokumentasi mengungkapkan detail yang diperlukan, tetapi cerita yang disebutkan di awal artikel terjadi justru karena kurangnya detail yang diinginkan.

Dan jika Anda tidak mengetahui cara kerja alat tersebut, Anda tidak akan dapat menggunakannya dengan benar. Secara umum, waktunya telah tiba untuk memeriksa dan melakukan percobaan untuk akhirnya memastikan setidaknya distribusinya.

Solusinya sederhana dan efektif - kumpulkan statistik, dapatkan data objektif, dan lihat hasilnya.

Subjek studi

Ada beberapa cara untuk menghasilkan angka acak di Unity - kami menguji lima cara.

  1. Sistem.Acak.Berikutnya(). Menghasilkan bilangan bulat dalam rentang nilai tertentu.
  2. Sistem.Acak.BerikutnyaDouble(). Menghasilkan angka presisi ganda dalam rentang [0; 1).
  3. UnityEngine.Acak.Rentang(). Menghasilkan angka presisi tunggal (mengambang) dalam rentang nilai tertentu.
  4. UnityEngine.Random.nilai. Menghasilkan angka presisi tunggal (float) dalam rentang [0; 1).
  5. Persatuan.Matematika.Acak.NextFloat(). Bagian dari perpustakaan Unity.Mathematics yang baru. Menghasilkan angka presisi tunggal (mengambang) dalam rentang nilai tertentu.

Hampir di semua tempat dalam dokumentasi, distribusi seragam ditentukan, dengan pengecualian UnityEngine.Random.value (di mana distribusi tidak ditentukan, tetapi dengan analogi dengan seragam UnityEngine.Random.Range() juga diharapkan) dan Unity.Mathematics.Random .NextFloat() (di mana basisnya adalah algoritma xorshift, yang berarti Anda harus menunggu distribusi seragam lagi).

Secara default, hasil yang diharapkan diambil seperti yang ditentukan dalam dokumentasi.

Metodologi

Kami menulis sebuah aplikasi kecil yang menghasilkan urutan angka acak menggunakan masing-masing metode yang disajikan dan menyimpan hasilnya untuk diproses lebih lanjut.

Panjang tiap barisan adalah 100 angka.
Kisaran angka acak adalah [0, 100).

Data dikumpulkan dari beberapa platform target:

  • Windows
    — Unity v2018.3.14f1, mode Editor, Mono, .NET Standar 2.0
  • MacOS
    — Unity v2018.3.14f1, mode Editor, Mono, .NET Standar 2.0
    — Unity v5.6.4p4, mode Editor, Mono, .NET Standar 2.0
  • Android
    — Unity v2018.3.14f1, dibuat per perangkat, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, dibuat per perangkat, il2cpp, .NET Standard 2.0

Implementasi

Kami memiliki beberapa cara berbeda untuk menghasilkan angka acak. Untuk masing-masingnya, kami akan menulis kelas pembungkus terpisah, yang seharusnya menyediakan:

  1. Kemungkinan untuk mengatur rentang nilai [min/maks). Akan diatur melalui konstruktor.
  2. Metode mengembalikan MF. Mari kita pilih float sebagai tipenya, karena lebih umum.
  3. Nama metode pembangkitan untuk menandai hasil. Untuk kenyamanan, kami akan mengembalikan nama lengkap kelas + nama metode yang digunakan untuk menghasilkan MF sebagai nilai.

Pertama, mari kita deklarasikan abstraksi yang akan diwakili oleh antarmuka IRandomGenerator:

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

        float Generate();
    }
}

Implementasi Sistem.Acak.Berikutnya()

Metode ini memungkinkan Anda untuk menetapkan rentang nilai, tetapi mengembalikan bilangan bulat, tetapi float diperlukan. Anda cukup menafsirkan bilangan bulat sebagai float, atau Anda dapat memperluas rentang nilai beberapa kali lipat, mengkompensasinya dengan setiap generasi rentang menengah. Hasilnya akan berupa titik tetap dengan tingkat akurasi tertentu. Kami akan menggunakan opsi ini karena lebih dekat dengan nilai float sebenarnya.

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

Implementasi Sistem.Acak.NextDouble()

Di sini rentang nilai tetap [0; 1). Untuk memproyeksikannya ke yang ditentukan dalam konstruktor, kita menggunakan aritmatika sederhana: 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;
    }
}

Implementasi UnityEngine.Random.Range()

Metode kelas statis UnityEngine.Random ini memungkinkan Anda mengatur rentang nilai dan mengembalikan tipe float. Anda tidak perlu melakukan transformasi tambahan apa pun.

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

Implementasi UnityEngine.Random.value

Properti value dari kelas statis UnityEngine.Random mengembalikan tipe float dari rentang nilai tetap [0; 1). Mari kita proyeksikan ke rentang tertentu dengan cara yang sama seperti saat mengimplementasikan 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;
    }
}

Implementasi Unity.Mathematics.Random.NextFloat()

Metode NextFloat() dari kelas Unity.Mathematics.Random mengembalikan floating point bertipe float dan memungkinkan Anda menentukan rentang nilai. Satu-satunya nuansa adalah bahwa setiap instance Unity.Mathematics.Random harus diinisialisasi dengan beberapa seed - dengan cara ini kita akan menghindari pembuatan 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);
    }
}

Implementasi Pengendali Utama

Beberapa implementasi IRandomGenerator sudah siap. Selanjutnya, Anda perlu membuat urutan dan menyimpan kumpulan data yang dihasilkan untuk diproses. Untuk melakukan ini, kita akan membuat adegan dan skrip MainController kecil di Unity, yang akan melakukan semua pekerjaan yang diperlukan dan pada saat yang sama bertanggung jawab untuk interaksi dengan UI.

Mari kita atur ukuran kumpulan data dan rentang nilai MF, dan dapatkan juga metode yang mengembalikan array generator yang dikonfigurasi dan siap bekerja.

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 kita membuat kumpulan data. Dalam hal ini, pembuatan data akan digabungkan dengan pencatatan hasilnya ke dalam aliran teks (dalam format csv). Untuk menyimpan nilai setiap IRandomGenerator, kolom terpisahnya dialokasikan, dan baris pertama berisi Nama generator.

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

        ...
    }
}

Yang tersisa hanyalah memanggil metode GenerateCsvDataSet dan menyimpan hasilnya ke file, atau segera mentransfer data melalui jaringan dari perangkat akhir ke server 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 proyek ada di GitLab.

Temuan

Tidak ada keajaiban yang terjadi. Apa yang mereka harapkan adalah apa yang mereka dapatkan - dalam semua kasus, pemerataan tanpa sedikit pun konspirasi. Saya tidak melihat gunanya menempatkan grafik terpisah untuk platform - semuanya menunjukkan hasil yang kurang lebih sama.

Kenyataannya adalah ini:
BlessRNG atau periksa keadilan RNG

Visualisasi urutan pada bidang dari kelima metode pembangkitan:
BlessRNG atau periksa keadilan RNG

Dan visualisasi dalam 3D. Saya hanya akan menyisakan hasil System.Random.Next() agar tidak menghasilkan banyak konten yang identik.
BlessRNG atau periksa keadilan RNG

Kisah yang diceritakan dalam pendahuluan tentang distribusi normal UnityEngine.Random tidak terulang kembali: entah awalnya salah, atau ada sesuatu yang berubah di mesinnya. Tapi sekarang kami yakin.

Sumber: www.habr.com

Tambah komentar