BlessRNG atau RNG memeriksa kejujuran



Di gamedev, Anda sering harus mengikat sesuatu di rumah acak: Unity memiliki Acak sendiri untuk ini, dan System.Random ada secara paralel dengannya. Sekali waktu di salah satu proyek itu tampak bahwa keduanya dapat bekerja secara berbeda (walaupun mereka harus memiliki distribusi yang seragam).

Lalu mereka tidak masuk ke detail - itu cukup bahwa transisi ke System.Random memperbaiki semua masalah. Sekarang kami memutuskan untuk memahami lebih detail dan melakukan sedikit riset: bagaimana "bias" atau RNG yang dapat diprediksi, dan mana yang harus dipilih. Selain itu, saya sering mendengar pendapat yang bertentangan tentang "kejujuran" mereka - mari kita coba mencari tahu bagaimana hasil nyata berhubungan dengan yang dinyatakan.

Program pendidikan singkat atau RNG sebenarnya adalah PRNG


Jika Anda sudah terbiasa dengan generator nomor acak, Anda dapat langsung melanjutkan ke bagian "Pengujian".

Angka acak (MF) adalah urutan angka yang dihasilkan menggunakan beberapa proses acak (kacau), sumber entropi. Artinya, ini adalah urutan seperti itu, unsur-unsur yang tidak terhubung oleh hukum matematika - mereka tidak memiliki hubungan sebab akibat.

Apa yang menciptakan midrange disebut generator angka acak (RNG). Tampaknya semuanya adalah dasar, tetapi jika kita beralih dari teori ke praktek, maka sebenarnya mengimplementasikan algoritma perangkat lunak untuk menghasilkan urutan seperti itu tidaklah begitu sederhana.

Alasannya terletak pada tidak adanya keacakan dalam elektronik konsumen modern. Tanpa itu, angka acak berhenti menjadi acak, dan generatornya berubah menjadi fungsi biasa dari argumen yang ditentukan dengan sengaja. Untuk sejumlah spesialisasi di bidang IT, ini adalah masalah serius (misalnya, untuk kriptografi), untuk sisanya ada solusi yang dapat diterima.

Kita perlu menulis sebuah algoritma yang akan mengembalikan, bahkan jika tidak benar-benar angka acak, tetapi sedekat mungkin dengan mereka - yang disebut angka pseudorandom (PSN). Algoritma dalam hal ini disebut pseudo random number generator (PRNG).

Ada beberapa opsi untuk membuat PRNG, tetapi untuk semua yang berikut akan relevan:

  1. Kebutuhan akan pra-inisialisasi.

    PRNG tidak memiliki sumber entropi, oleh karena itu, sebelum digunakan, ia harus menunjukkan keadaan awal. Ini ditentukan sebagai angka (atau vektor) dan disebut benih (seed, seed random). Seringkali, penghitung jam prosesor atau angka yang setara dengan waktu sistem digunakan sebagai seed.
  2. Reproduksibilitas urutan.

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

Anda juga perlu mengetahui distribusi probabilitas yang menjadi ciri PRNG - angka apa yang akan dihasilkan dan dengan probabilitas apa. Paling sering, ini adalah distribusi normal atau distribusi seragam.

Distribusi normal (kiri) dan distribusi seragam (kanan)

Katakanlah kita memiliki dadu yang jujur ​​dengan 24 wajah. Jika Anda menjatuhkannya, maka probabilitas sebuah unit jatuh adalah 1/24 (dan juga probabilitas nomor lainnya yang jatuh). Jika Anda melakukan banyak lemparan dan mencatat hasilnya, Anda akan melihat bahwa semua wajah rontok pada frekuensi yang sama. Bahkan, dadu ini dapat dianggap sebagai RNG dengan distribusi seragam.

Dan jika Anda segera melempar 10 tulang seperti itu dan menghitung jumlah total poin? Apakah keseragaman akan dipertahankan untuknya? Tidak. Paling sering, jumlahnya akan mendekati 125 poin, yaitu beberapa nilai rata-rata. Dan sebagai hasilnya - bahkan sebelum melakukan lemparan, Anda dapat memperkirakan secara kasar hasil di masa depan.

Alasannya adalah untuk mendapatkan jumlah poin rata-rata ada jumlah kombinasi terbesar. Semakin jauh darinya, semakin sedikit kombinasi - dan, karenanya, semakin sedikit kemungkinan kehilangan. Jika Anda memvisualisasikan data ini, itu akan jauh menyerupai bentuk bel. Oleh karena itu, dengan beberapa peregangan, sistem 10 tulang dapat disebut RNG dengan distribusi normal.

Contoh lain, baru saja di pesawat - menembak sasaran. Penembak akan menjadi RNG yang menghasilkan sepasang angka (x, y), yang ditampilkan pada grafik.

Setuju bahwa opsi di sebelah kiri lebih dekat dengan kehidupan nyata - ini adalah RNG dengan distribusi normal. Tetapi jika Anda perlu menyebarkan bintang di langit gelap, maka opsi yang tepat, diperoleh dengan bantuan RNG dengan distribusi seragam, lebih baik. Secara umum, pilih generator tergantung pada tugasnya.

Sekarang mari kita bicara tentang entropi urutan PSP. 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, ...

Seberapa acak angka-angka ini pada pandangan pertama? Mari kita mulai dengan memeriksa distribusinya.

Sepertinya dekat dengan seragam, tetapi jika Anda membaca urutan dua angka dan menafsirkannya sebagai koordinat di pesawat, Anda mendapatkan ini:

Pola terlihat jelas. Dan karena data dalam urutan diurutkan dengan cara tertentu (yaitu, mereka memiliki entropi rendah), ini dapat menyebabkan "bias". Paling tidak, PRNG semacam itu sangat tidak cocok untuk menghasilkan koordinat di pesawat.

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

Semuanya tampak baik-baik saja di sini bahkan di pesawat:

Mari kita lihat dalam volume (masing-masing kita membaca tiga angka):

Dan lagi polanya. Membangun visualisasi dalam empat dimensi tidak akan berhasil. Tetapi pola bisa ada baik pada dimensi ini maupun pada yang besar.

Dalam kriptografi yang sama, di mana persyaratan yang paling ketat diterapkan pada PRNG, situasi seperti itu secara kategoris tidak dapat diterima. Oleh karena itu, untuk mengevaluasi kualitasnya, algoritma khusus telah dikembangkan, yang tidak akan kami sentuh sekarang. Topiknya luas dan mengacu pada artikel terpisah.

Pengujian


Jika kami tidak mengetahui sesuatu dengan pasti, lalu bagaimana cara mengatasinya? Apakah layak untuk menyeberang jalan jika Anda tidak tahu sinyal lalu lintas apa yang dibolehkan? Konsekuensinya mungkin berbeda.

Hal yang sama berlaku untuk keacakan yang terkenal di Unity. Nah, jika dokumentasi mengungkapkan rincian yang diperlukan, tetapi cerita yang disebutkan di awal artikel terjadi hanya karena kurangnya spesifik yang diinginkan.

Dan tanpa mengetahui bagaimana alat ini bekerja, Anda tidak dapat menerapkannya dengan benar. Secara umum, waktunya telah tiba untuk memeriksa dan melakukan percobaan untuk akhirnya memastikan setidaknya dengan mengorbankan distribusi.

Solusinya sederhana dan efektif - untuk mengumpulkan statistik, memperoleh data objektif dan melihat hasilnya.

Subjek penelitian


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

  1. System.Random.Next (). Menghasilkan bilangan bulat dalam rentang nilai yang diberikan.
  2. System.Random.NextDouble (). Menghasilkan angka presisi ganda (ganda) dalam kisaran dari [0; 1).
  3. UnityEngine.Random.Range (). Menghasilkan angka presisi tunggal (float) dalam rentang nilai yang diberikan.
  4. UnityEngine.Random.value. Menghasilkan angka presisi tunggal (float) dalam kisaran dari [0; 1).
  5. Unity.Mathematics.Random.NextFloat (). Bagian dari perpustakaan Unity.Mathematics baru. Menghasilkan angka presisi tunggal (float) dalam rentang nilai yang diberikan.

Hampir di mana-mana dalam dokumentasi, distribusi seragam ditunjukkan, dengan pengecualian UnityEngine.Random.value (di mana distribusi tidak ditentukan, tetapi mirip dengan UnityEngine.Random.Range () itu juga diharapkan seragam) dan Unity.Mathematics.Random.NextFloat () (di mana dalam dasarnya adalah algoritma xorshift, yang artinya Anda harus menunggu distribusi yang seragam).

Secara default, yang diharapkan dalam dokumentasi diambil untuk hasil yang diharapkan.

Metodologi


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

Panjang setiap urutan adalah 100.000 angka.
Kisaran angka acak adalah [0, 100).

Data dikumpulkan dari beberapa platform target:

  • Windows
    - Unity v2018.3.14f1, mode Editor, Mono, .NET Standard 2.0
  • macOS
    - Unity v2018.3.14f1, mode Editor, Mono, .NET Standard 2.0
    - Unity v5.6.4p4, mode Editor, Mono, .NET Standard 2.0
  • Android
    - Unity v2018.3.14f1, perakitan pada perangkat, Mono, .NET Standard 2.0
  • iOS
    - Unity v2018.3.14f1, build to device, il2cpp, .NET Standard 2.0

Implementasi


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

  1. Kemampuan untuk mengatur rentang nilai [min / maks). Ini akan ditetapkan melalui konstruktor.
  2. Metode mengembalikan midrange. Kami akan memilih float sebagai tipenya, sebagai yang lebih umum.
  3. Nama metode generasi untuk menandai hasil. Untuk kenyamanan, kami akan mengembalikan nama kelas penuh + nama metode yang digunakan untuk menghasilkan midrange sebagai nilai.

Pertama, nyatakan abstraksi, yang akan diwakili oleh antarmuka IRandomGenerator:

namespace RandomDistribution { public interface IRandomGenerator { string Name { get; } float Generate(); } } 

Implementasi System.Random.Next ()


Metode ini memungkinkan Anda untuk menentukan rentang nilai, tetapi mengembalikan bilangan bulat, dan float diperlukan. Anda cukup menginterpretasikan integer sebagai float, atau Anda dapat memperluas rentang nilai dengan beberapa urutan besarnya, mengkompensasinya setiap kali midrange dihasilkan. Ini akan menghasilkan sesuatu seperti titik tetap dengan akurasi yang ditentukan. Kami akan menggunakan opsi ini, karena lebih dekat dengan nilai float nyata.

 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 System.Random.NextDouble ()


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

Implementasi UnityEngine.Random.Range ()


Metode kelas statis UnityEngine.Random ini memungkinkan Anda menentukan rentang nilai dan mengembalikan kisaran menengah dari tipe float. Tidak diperlukan 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); } } 

Implementasi UnityEngine.Random.value


Properti nilai dari kelas statis UnityEngine.Random mengembalikan kelas menengah dari tipe float dari rentang nilai yang tetap [0; 1). Kami memproyeksikannya ke rentang yang diberikan dengan cara yang sama seperti ketika menerapkan 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 kelas menengah dari tipe float dan memungkinkan Anda untuk menentukan rentang nilai. Satu-satunya nuansa adalah bahwa setiap instance dari Unity.Mathematics.Random harus diinisialisasi dengan beberapa seed - dengan cara ini kita akan menghindari menghasilkan 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 MainController


Beberapa implementasi IRandomGenerator siap. Selanjutnya, Anda perlu membuat urutan dan menyimpan dataset yang dihasilkan untuk diproses. Untuk melakukan ini, buat adegan di Unity dan MainController skrip kecil, yang akan melakukan semua pekerjaan yang diperlukan dan secara bersamaan bertanggung jawab untuk berinteraksi dengan UI.

Kami mengatur ukuran dataset dan kisaran nilai midrange, dan juga mendapatkan metode yang mengembalikan array generator yang disetel dan siap digunakan.

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

Dan sekarang kami sedang membuat dataset. Dalam hal ini, pembuatan data akan digabungkan dengan perekaman hasil dalam aliran teks (dalam format csv). Untuk menyimpan nilai dari setiap IRandomGenerator, kolom terpisah 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(); } } ... } } 

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

Hasil


Tidak ada keajaiban yang terjadi. Apa yang mereka harapkan, mereka mendapatkannya - dalam semua kasus distribusi yang seragam tanpa sedikit pun konspirasi. Saya tidak melihat titik menerapkan grafik terpisah pada platform - semuanya menunjukkan hasil yang kira-kira sama.

Kenyataannya adalah:


Visualisasi urutan di pesawat dari semua metode generasi lima:


Dan visualisasi dalam 3D. Saya hanya akan meninggalkan hasil System.Random.Next (), agar tidak menghasilkan banyak konten yang sama.


Kisah yang diceritakan dalam pengantar tentang distribusi normal UnityEngine.Random tidak mengulangi: entah itu awalnya salah, atau ada sesuatu yang berubah di mesin sejak saat itu. Tapi sekarang kami yakin.

Source: https://habr.com/ru/post/id473880/


All Articles