BlessRNG o RNG verifique la honestidad



En gamedev, a menudo necesitas vincular algo en una casa aleatoria: Unity tiene su propio Random para esto, y System.Random existe en paralelo con él. Había una vez en uno de los proyectos que parecía que ambos podrían funcionar de manera diferente (aunque deberían tener una distribución uniforme).

Luego no entraron en detalles: fue suficiente que la transición a System.Random solucionó todos los problemas. Ahora decidimos comprender con más detalle y realizar un poco de investigación: qué tan "sesgados" o predecibles son los RNG, y cuál elegir. Además, a menudo he escuchado opiniones contradictorias sobre su "honestidad". Tratemos de descubrir cómo se relacionan los resultados reales con los declarados.

Un breve programa educativo o RNG es en realidad un PRNG


Si ya está familiarizado con los generadores de números aleatorios, puede pasar inmediatamente a la sección "Pruebas".

Los números aleatorios (MF) son una secuencia de números generados mediante algún proceso aleatorio (caótico), una fuente de entropía. Es decir, esta es una secuencia de este tipo, cuyos elementos no están conectados por ninguna ley matemática, no tienen una relación causal.

Lo que crea un rango medio se llama generador de números aleatorios (RNG). Parece que todo es elemental, pero si pasamos de la teoría a la práctica, implementar un algoritmo de software para generar tal secuencia no es tan simple.

La razón radica en la ausencia de aleatoriedad en la electrónica de consumo moderna. Sin él, los números aleatorios dejan de ser aleatorios, y su generador se convierte en una función ordinaria de argumentos determinados deliberadamente. Para una serie de especialidades en el campo de TI, este es un problema grave (por ejemplo, para la criptografía), para el resto hay una solución perfectamente aceptable.

Necesitamos escribir un algoritmo que regrese, incluso si no son números verdaderamente aleatorios, pero lo más cerca posible de ellos: los llamados números pseudoaleatorios (PSN). El algoritmo en este caso se llama generador de números pseudoaleatorios (PRNG).

Existen varias opciones para crear un PRNG, pero para todo lo siguiente será relevante:

  1. La necesidad de preinicialización.

    El PRNG carece de una fuente de entropía, por lo tanto, antes de usarlo, es necesario indicar el estado inicial. Se especifica como un número (o vector) y se llama semilla (semilla, semilla aleatoria). A menudo, un contador de reloj del procesador o el equivalente numérico del tiempo del sistema se utiliza como semilla.
  2. Reproducibilidad de la secuencia.

    El PRNG es completamente determinista, por lo que la semilla especificada durante la inicialización determina de forma única la secuencia futura completa de números. Esto significa que un solo PRSP, inicializado con la misma semilla (en diferentes momentos, en diferentes programas, en diferentes dispositivos) generará la misma secuencia.

También necesita saber la distribución de probabilidad que caracteriza al PRNG: qué números generará y con qué probabilidad. Muy a menudo, esta es una distribución normal o una distribución uniforme.

Distribución normal (izquierda) y distribución uniforme (derecha)

Digamos que tenemos un dado honesto con 24 caras. Si lo suelta, la probabilidad de que una unidad se caiga será 1/24 (así como la probabilidad de que se caiga cualquier otro número). Si realiza muchos lanzamientos y registra los resultados, notará que todas las caras se caen aproximadamente a la misma frecuencia. De hecho, este dado puede considerarse un RNG con una distribución uniforme.

¿Y si inmediatamente arrojas 10 de esos huesos y cuentas la cantidad total de puntos? ¿Se mantendrá la uniformidad para ella? No Muy a menudo, la cantidad estará cerca de 125 puntos, es decir, a algún valor promedio. Y como resultado, incluso antes de hacer un lanzamiento, puede estimar aproximadamente el resultado futuro.

La razón es que para obtener la cantidad promedio de puntos hay la mayor cantidad de combinaciones. Cuanto más lejos, menos combinaciones, y, en consecuencia, menos posibilidades de pérdida. Si visualiza estos datos, se parecerá remotamente a la forma de una campana. Por lo tanto, con algo de estiramiento, un sistema de 10 huesos puede llamarse RNG con una distribución normal.

Otro ejemplo, solo ya en el avión: tiro al blanco. El tirador será el RNG que genera un par de números (x, y), que se muestran en el gráfico.

Acuerde que la opción de la izquierda está más cerca de la vida real: este es un RNG con una distribución normal. Pero si necesita dispersar estrellas en un cielo oscuro, entonces la opción correcta, obtenida con la ayuda de un RNG con una distribución uniforme, es mejor. En general, elija un generador según la tarea.

Ahora hablemos de la entropía de la secuencia de PSP. Por ejemplo, hay una secuencia que comienza así:

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

¿Qué tan aleatorios son estos números a primera vista? Comencemos por verificar la distribución.

Parece casi uniforme, pero si lees la secuencia de dos números y los interpretas como coordenadas en el avión, obtienes esto:

Los patrones son claramente visibles. Y dado que los datos en la secuencia están ordenados de cierta manera (es decir, tienen una baja entropía), esto puede conducir al mismo "sesgo". Como mínimo, tal PRNG no es muy adecuado para generar coordenadas en un plano.

Otra secuencia:

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

Todo parece estar bien aquí, incluso en el avión:

Veamos en volumen (leemos tres números cada uno):

Y de nuevo los patrones. La visualización de compilación en cuatro dimensiones no funcionará. Pero los patrones pueden existir tanto en esta dimensión como en las grandes.

En la misma criptografía, donde los requisitos más estrictos se imponen al PRNG, tal situación es categóricamente inaceptable. Por lo tanto, para evaluar su calidad, se han desarrollado algoritmos especiales, que no tocaremos ahora. El tema es extenso y se basa en un artículo separado.

Prueba


Si no sabemos algo con certeza, ¿cómo trabajar con eso? ¿Vale la pena cruzar la calle si no sabe qué señal de tráfico permite? Las consecuencias pueden ser diferentes.

Lo mismo ocurre con la notoria aleatoriedad en Unity. Bueno, si la documentación revela los detalles necesarios, pero la historia mencionada al principio del artículo sucedió solo por la falta de detalles deseados.

Y sin saber cómo funciona la herramienta, no puede aplicarla correctamente. En general, ha llegado el momento de verificar y llevar a cabo un experimento para finalmente asegurarse al menos a expensas de la distribución.

La solución fue simple y efectiva: recopilar estadísticas, obtener datos objetivos y observar los resultados.

Sujeto de investigación


Hay varias formas de generar números aleatorios en Unity: probamos cinco.

  1. System.Random.Next (). Genera enteros en un rango de valores dado.
  2. System.Random.NextDouble (). Genera números de doble precisión (doble) en el rango de [0; 1)
  3. UnityEngine.Random.Range (). Genera números de precisión únicos (flotantes) en un rango de valores dado.
  4. UnityEngine.Random.value. Genera números de precisión únicos (flotante) en el rango de [0; 1)
  5. Unity.Mathematics.Random.NextFloat (). Parte de la nueva biblioteca de Unity.Mathematics. Genera números de precisión únicos (flotantes) en un rango de valores dado.

Casi en todas partes de la documentación, se indicó una distribución uniforme, con la excepción de UnityEngine.Random.value (donde la distribución no se especifica, pero de manera similar a UnityEngine.Random.Range () también se esperaba que fuera uniforme) y Unity.Mathematics.Random.NextFloat () (donde en la base es el algoritmo xorshift, lo que significa que nuevamente debe esperar una distribución uniforme).

Por defecto, los esperados en la documentación fueron tomados para los resultados esperados.

Metodología


Escribimos una pequeña aplicación que generaba secuencias de números aleatorios en cada uno de los métodos presentados y guardaba los resultados para su posterior procesamiento.

La longitud de cada secuencia es de 100,000 números.
El rango de números aleatorios es [0, 100).

Los datos se recopilaron de varias plataformas de destino:

  • Ventanas
    - Unity v2018.3.14f1, modo Editor, Mono, .NET Standard 2.0
  • macOS
    - Unity v2018.3.14f1, modo Editor, Mono, .NET Standard 2.0
    - Unity v5.6.4p4, modo Editor, Mono, .NET Standard 2.0
  • Android
    - Unity v2018.3.14f1, ensamblaje en el dispositivo, Mono, .NET Standard 2.0
  • iOS
    - Unity v2018.3.14f1, compilación en dispositivo, il2cpp, .NET Standard 2.0

Implementación


Tenemos varias formas diferentes de generar números aleatorios. Para cada uno de ellos, escribiremos una clase de contenedor separada, que debería proporcionar:

  1. Posibilidad de establecer el rango de valores [min / max). Se establecerá a través del constructor.
  2. Método de retorno de rango medio. Elegiremos float como el tipo, como uno más general.
  3. El nombre del método de generación para marcar los resultados. Por conveniencia, devolveremos el nombre completo de la clase + el nombre del método utilizado para generar el rango medio como valor.

Primero, declare una abstracción, que estará representada por la interfaz IRandomGenerator:

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

Implementación de System.Random.Next ()


Este método le permite especificar un rango de valores, pero devuelve enteros y se necesita un flotante. Simplemente puede interpretar el número entero como flotante, o puede expandir el rango de valores en varios órdenes de magnitud, compensándolos cada vez que se genera el rango medio. Resultará algo así como un punto fijo con la precisión especificada. Utilizaremos esta opción, ya que está más cerca del valor flotante real.

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

Implementación de System.Random.NextDouble ()


Aquí un rango fijo de valores [0; 1) Para proyectarlo sobre el especificado en el constructor, usamos aritmética simple: 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; } } 

Implementación de UnityEngine.Random.Range ()


Este método de la clase estática UnityEngine.Random le permite especificar un rango de valores y devuelve un rango medio de tipo flotante. No se necesitan transformaciones adicionales.

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

Implementación de UnityEngine.Random.value


La propiedad de valor de la clase estática UnityEngine.Random devuelve un rango medio de tipo flotante de un rango fijo de valores [0; 1) Lo proyectamos en un rango determinado de la misma manera que cuando implementamos 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; } } 

Implementación de Unity.Mathematics.Random.NextFloat ()


El método NextFloat () de la clase Unity.Mathematics.Random devuelve un rango medio de tipo flotante y le permite especificar un rango de valores. El único matiz es que cada instancia de Unity.Mathematics.Random tendrá que inicializarse con algo de semilla, de esta manera evitaremos generar secuencias repetidas.

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

Implementación de MainController


Varias implementaciones de IRandomGenerator están listas. A continuación, debe generar secuencias y guardar el conjunto de datos resultante para su procesamiento. Para hacer esto, cree una escena en Unity y un pequeño script MainController, que realizará todo el trabajo necesario y simultáneamente será responsable de interactuar con la interfaz de usuario.

Establecemos el tamaño del conjunto de datos y el rango de valores de rango medio, y también obtenemos un método que devuelve una matriz de generadores sintonizados y listos para usar.

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

Y ahora estamos formando un conjunto de datos. En este caso, la generación de datos se combinará con el registro de los resultados en una secuencia de texto (en formato csv). Para almacenar los valores de cada IRandomGenerator, se asigna una columna separada y la primera línea contiene el nombre del generador.

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

Queda por llamar al método GenerateCsvDataSet y guardar el resultado en un archivo, o transferir inmediatamente los datos a través de la red desde el dispositivo final al servidor receptor.

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

Las fuentes del proyecto están en GitLab .

Resultados


No sucedió ningún milagro. Lo que esperaban, lo obtuvieron: en todos los casos, una distribución uniforme sin una pizca de conspiraciones. No veo el punto de aplicar gráficos separados en las plataformas: todos muestran aproximadamente los mismos resultados.

La realidad es:


Visualización de secuencias en un plano a partir de los cinco métodos de generación:


Y visualización en 3D. Dejaré solo el resultado de System.Random.Next (), para no producir un montón del mismo contenido.


La historia contada en la introducción sobre la distribución normal de UnityEngine.Random no se repitió: al principio era errónea o algo ha cambiado en el motor desde entonces. Pero ahora estamos seguros.

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


All Articles