BlessRNG veya RNG'nin adalet açısından kontrol edilmesi

BlessRNG veya RNG'nin adalet açısından kontrol edilmesi

Oyun geliştirmede sıklıkla bir şeyi rastgelelikle bağlamanız gerekir: Unity'nin bunun için kendi Random'u vardır ve buna paralel olarak System.Random da vardır. Bir zamanlar projelerden birinde her ikisinin de farklı çalışabileceği izlenimini edindim (her ne kadar eşit bir dağılıma sahip olsalar da).

Sonra ayrıntılara girmediler - System.Random'a geçişin tüm sorunları düzeltmesi yeterliydi. Şimdi konuyu daha detaylı incelemeye ve küçük bir araştırma yapmaya karar verdik: RNG'ler ne kadar "önyargılı" veya öngörülebilir ve hangisini seçmeliyiz? Üstelik onların "dürüstlüğü" hakkında çelişkili görüşler defalarca duydum - hadi gerçek sonuçların beyan edilenlerle nasıl karşılaştırıldığını anlamaya çalışalım.

Kısa eğitim programı veya RNG aslında RNG'dir

Rasgele sayı üreteçlerine zaten aşina iseniz, hemen "Test" bölümüne geçebilirsiniz.

Rastgele sayılar (RN), bir entropi kaynağı olan bazı rastgele (kaotik) işlemler kullanılarak oluşturulan bir sayı dizisidir. Yani bu, elemanları herhangi bir matematik yasasıyla birbirine bağlı olmayan bir dizidir - sebep-sonuç ilişkisi yoktur.

Rastgele sayıyı oluşturan şeye rastgele sayı üreteci (RNG) adı verilir. Görünüşe göre her şey temeldir, ancak teoriden pratiğe geçersek, o zaman aslında böyle bir diziyi oluşturmak için bir yazılım algoritması uygulamak o kadar kolay değildir.

Bunun nedeni, modern tüketici elektroniğinde aynı kaosun olmamasında yatmaktadır. Bu olmadan, rastgele sayılar rastgele olmaktan çıkar ve bunların üreteci, açıkça tanımlanmış argümanların sıradan bir fonksiyonuna dönüşür. BT alanındaki bazı uzmanlıklar için bu ciddi bir sorundur (örneğin kriptografi), ancak diğerleri için tamamen kabul edilebilir bir çözüm vardır.

Gerçekten rastgele sayılar olmasa da, onlara mümkün olduğunca yakın olan sözde rastgele sayılar (PRN) olarak geri dönecek bir algoritma yazmak gerekir. Bu durumda algoritmaya sözde rastgele sayı üreteci (PRNG) adı verilir.

PRNG oluşturmak için çeşitli seçenekler vardır, ancak aşağıdakiler herkes için geçerli olacaktır:

  1. Ön başlatma ihtiyacı.

    PRNG'nin entropi kaynağı yoktur, bu nedenle kullanımdan önce ona bir başlangıç ​​durumu verilmelidir. Bir sayı (veya vektör) olarak belirtilir ve tohum (rastgele tohum) olarak adlandırılır. Çoğunlukla işlemci saat sayacı veya sistem saatinin sayısal eşdeğeri tohum olarak kullanılır.

  2. Dizi tekrarlanabilirliği.

    PRNG tamamen deterministiktir, dolayısıyla başlatma sırasında belirtilen çekirdek, gelecekteki tüm sayı dizisini benzersiz bir şekilde belirler. Bu, aynı tohumla (farklı zamanlarda, farklı programlarda, farklı cihazlarda) başlatılan ayrı bir PRNG'nin aynı diziyi oluşturacağı anlamına gelir.

Ayrıca PRNG'yi karakterize eden olasılık dağılımını da bilmeniz gerekir - hangi sayıları ve hangi olasılıkla üreteceğini. Çoğu zaman bu ya normal bir dağılımdır ya da tekdüze bir dağılımdır.
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi
Normal dağılım (solda) ve düzgün dağılım (sağda)

Diyelim ki 24 tarafı olan adil bir zarımız var. Eğer onu atarsanız, bir gelme olasılığı 1/24'e eşit olacaktır (başka bir sayı gelme olasılığıyla aynı). Çok sayıda atış yaparsanız ve sonuçları kaydederseniz, tüm kenarların yaklaşık olarak aynı sıklıkta düştüğünü fark edeceksiniz. Esasen bu kalıp, düzgün dağılıma sahip bir RNG olarak düşünülebilir.

Bu zarlardan 10 tanesini aynı anda atıp toplam puanı sayarsanız ne olur? Bunun için tekdüzelik korunacak mı? HAYIR. Çoğu zaman miktar 125 puana, yani ortalama bir değere yakın olacaktır. Sonuç olarak, atış yapmadan önce bile gelecekteki sonucu kabaca tahmin edebilirsiniz.

Bunun nedeni, ortalama puanı elde etmek için en fazla sayıda kombinasyonun bulunmasıdır. Ondan ne kadar uzak olursa, kombinasyonlar o kadar az olur ve buna bağlı olarak kayıp olasılığı da o kadar düşük olur. Bu veriler görselleştirilirse belli belirsiz bir zil şekline benzeyecektir. Bu nedenle biraz esnetilerek 10 zardan oluşan bir sisteme normal dağılıma sahip bir RNG denilebilir.

Başka bir örnek, sadece bu sefer uçakta - bir hedefe ateş etmek. Atıcı, grafikte görüntülenen bir sayı çiftini (x, y) üreten bir RNG olacaktır.
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi
Soldaki seçeneğin gerçek hayata daha yakın olduğunu kabul edin - bu normal dağılıma sahip bir RNG'dir. Ancak yıldızları karanlık bir gökyüzüne dağıtmanız gerekiyorsa, tek tip dağılıma sahip RNG kullanılarak elde edilen doğru seçenek daha uygundur. Genel olarak, elinizdeki göreve bağlı olarak bir jeneratör seçin.

Şimdi PNG dizisinin entropisinden bahsedelim. Mesela şöyle başlayan bir dizi var:

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

Bu sayılar ilk bakışta ne kadar rastgele? Dağıtımı kontrol ederek başlayalım.
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi
Tekdüzeye yakın görünüyor, ancak iki sayıdan oluşan bir diziyi okuyup bunları bir düzlemdeki koordinatlar olarak yorumlarsanız şunu elde edersiniz:
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi
Desenler açıkça görünür hale gelir. Ve dizideki veriler belirli bir şekilde sıralandığından (yani düşük entropiye sahip olduğundan), bu tam da o "önyargıya" yol açabilir. En azından böyle bir PRNG, bir düzlemde koordinatlar oluşturmak için pek uygun değildir.

Başka bir sıra:

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

Burada uçakta bile her şey yolunda görünüyor:
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi
Hacme bakalım (bir seferde üç rakamı okuyalım):
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi
Ve yine desenler. Dört boyutlu bir görselleştirme artık mümkün değil. Ancak bu boyutta ve daha büyük boyutlarda desenler mevcut olabilir.

PRNG'lere en katı gereksinimlerin uygulandığı kriptografide böyle bir durum kategorik olarak kabul edilemez. Bu nedenle kalitelerini değerlendirmek için şimdi değinmeyeceğimiz özel algoritmalar geliştirilmiştir. Konu oldukça geniş ve ayrı bir makaleyi hak ediyor.

Test

Bir şeyi kesin olarak bilmiyorsak, onunla nasıl çalışılır? Hangi trafik ışığının buna izin verdiğini bilmiyorsanız yolu geçmeye değer mi? Sonuçlar farklı olabilir.

Aynı şey Unity'deki meşhur rastgelelik için de geçerli. Belgelerin gerekli ayrıntıları ortaya çıkarması iyidir, ancak makalenin başında bahsedilen hikaye tam olarak istenen özelliklerin eksikliğinden kaynaklanmıştır.

Aracın nasıl çalıştığını bilmiyorsanız onu doğru şekilde kullanamazsınız. Genel olarak, en azından dağıtımdan emin olmak için kontrol etme ve bir deney yapma zamanı geldi.

Çözüm basit ve etkiliydi; istatistikleri toplayın, objektif veriler elde edin ve sonuçlara bakın.

Çalışma konusu

Unity'de rastgele sayılar üretmenin birkaç yolu vardır; beşini test ettik.

  1. System.Random.Next(). Belirli bir değer aralığında tamsayılar üretir.
  2. System.Random.NextDouble(). [0; 1).
  3. UnityEngine.Random.Range(). Belirli bir değer aralığında tek duyarlıklı sayılar (değişken sayılar) üretir.
  4. UnityEngine.Random.value. [0; 1).
  5. Unity.Mathematics.Random.NextFloat(). Yeni Unity.Mathematics kütüphanesinin bir parçası. Belirli bir değer aralığında tek duyarlıklı sayılar (değişken sayılar) üretir.

UnityEngine.Random.value (dağılımın belirtilmediği ancak UnityEngine.Random.Range() ile benzer şekilde tekdüzeliğin de beklendiği yer) ve Unity.Mathematics.Random haricinde, belgelerin hemen hemen her yerinde tekdüze bir dağılım belirtildi. .NextFloat() (burada Temel xorshift algoritmasıdır, bu da yine tekdüze bir dağılım beklemeniz gerektiği anlamına gelir).

Varsayılan olarak beklenen sonuçlar belgelerde belirtilenlerle aynı şekilde alınmıştır.

Teknik

Sunulan yöntemlerin her birini kullanarak rastgele sayı dizileri üreten ve sonuçları daha sonraki işlemler için kaydeden küçük bir uygulama yazdık.

Her dizinin uzunluğu 100 sayıdır.
Rasgele sayıların aralığı [0, 100)'dir.

Veriler birkaç hedef platformdan toplandı:

  • Windows
    — Unity v2018.3.14f1, Düzenleyici modu, Mono, .NET Standard 2.0
  • macOS
    — Unity v2018.3.14f1, Düzenleyici modu, Mono, .NET Standard 2.0
    — Unity v5.6.4p4, Düzenleyici modu, Mono, .NET Standard 2.0
  • Android
    — Unity v2018.3.14f1, cihaz başına derleme, Mono, .NET Standard 2.0
  • iOS
    — Unity v2018.3.14f1, cihaz başına derleme, il2cpp, .NET Standard 2.0

uygulama

Rastgele sayılar üretmenin birkaç farklı yolu var. Her biri için aşağıdakileri sağlaması gereken ayrı bir sarmalayıcı sınıfı yazacağız:

  1. Değer aralığını [min/maks] ayarlama imkanı. Yapıcı aracılığıyla ayarlanacaktır.
  2. MF'yi döndüren yöntem. Daha genel olduğu için tür olarak float'ı seçelim.
  3. Sonuçları işaretlemek için kullanılan oluşturma yönteminin adı. Kolaylık sağlamak için sınıfın tam adını + MF'yi oluşturmak için kullanılan yöntemin adını bir değer olarak döndüreceğiz.

Öncelikle IRandomGenerator arayüzü tarafından temsil edilecek bir soyutlama tanımlayalım:

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

        float Generate();
    }
}

System.Random.Next()'in uygulanması

Bu yöntem bir değer aralığı belirlemenize olanak tanır, ancak tamsayıları döndürür, ancak kayan değişkenlere ihtiyaç vardır. Tamsayıyı basitçe kayan nokta olarak yorumlayabilir veya değer aralığını birkaç büyüklük sırasına kadar genişleterek bunları orta aralığın her nesliyle telafi edebilirsiniz. Sonuç, belirli bir doğruluk derecesine sahip sabit bir nokta gibi bir şey olacaktır. Gerçek float değerine daha yakın olduğu için bu seçeneği kullanacağız.

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() uygulamasının uygulanması

Burada sabit değer aralığı [0; 1). Bunu yapıcıda belirtilene yansıtmak için basit aritmetik kullanırız: 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() uygulamasının uygulanması

UnityEngine.Random statik sınıfının bu yöntemi, bir değer aralığı ayarlamanıza olanak tanır ve bir kayan nokta türü döndürür. Herhangi bir ek dönüşüm yapmanıza gerek yoktur.

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'nin uygulanması

UnityEngine.Random statik sınıfının value özelliği, sabit bir değer aralığından bir kayan nokta türü döndürür [0; 1). System.Random.NextDouble() uygularken olduğu gibi bunu belirli bir aralığa yansıtalım.

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() uygulamasının uygulanması

Unity.Mathematics.Random sınıfının NextFloat() yöntemi, float türünde bir kayan nokta döndürür ve bir değer aralığı belirtmenize olanak tanır. Tek nüans, Unity.Mathematics.Random'un her örneğinin bir tohumla başlatılması gerekmesidir - bu şekilde tekrar eden diziler oluşturmaktan kaçınacağız.

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'ın Gerçekleştirilmesi

IRandomGenerator'ın çeşitli uygulamaları hazır. Daha sonra, diziler oluşturmanız ve elde edilen veri kümesini işlenmek üzere kaydetmeniz gerekir. Bunu yapmak için Unity'de gerekli tüm işleri yapacak ve aynı zamanda kullanıcı arayüzü ile etkileşimden sorumlu olacak bir sahne ve küçük bir MainController betiği oluşturacağız.

Veri kümesinin boyutunu ve MF değerlerinin aralığını ayarlayalım ve ayrıca yapılandırılmış ve çalışmaya hazır bir dizi oluşturucuyu döndüren bir yöntem elde edelim.

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

        ...
    }
}

Şimdi bir veri seti oluşturalım. Bu durumda veri oluşturma, sonuçların bir metin akışına (csv formatında) kaydedilmesiyle birleştirilecektir. Her IRandomGenerator'ın değerlerini saklamak için kendine ait ayrı bir sütun tahsis edilmiştir ve ilk satırda jeneratörün Adı yer almaktadır.

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

        ...
    }
}

Geriye kalan tek şey GenerateCsvDataSet yöntemini çağırıp sonucu bir dosyaya kaydetmek veya verileri ağ üzerinden uç cihazdan alıcı sunucuya hemen aktarmaktır.

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

        ...
    }
}

Proje kaynakları şu adreste: GitLab.

Bulgular

Hiçbir mucize gerçekleşmedi. Bekledikleri şey, her durumda, en ufak bir komplo belirtisi olmayan eşit bir dağılımdı. Platformlar için ayrı grafikler koymanın bir manasını göremiyorum; hepsi yaklaşık olarak aynı sonuçları gösteriyor.

Gerçek şu:
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi

Beş üretim yönteminin tümünden dizilerin bir düzlemde görselleştirilmesi:
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi

Ve 3 boyutlu görselleştirme. Bir sürü aynı içerik üretmemek için yalnızca System.Random.Next() sonucunu bırakacağım.
BlessRNG veya RNG'nin adalet açısından kontrol edilmesi

UnityEngine.Random'un normal dağılımı hakkında giriş bölümünde anlatılan hikaye kendini tekrar etmedi: ya başlangıçta hatalıydı ya da o zamandan beri motorda bir şeyler değişti. Ama artık eminiz.

Kaynak: habr.com

Yorum ekle