BlessRNG ou RNG verificam a honestidade



No gamedev, você geralmente precisa vincular algo a uma casa aleatória: o Unity possui seu próprio Random para isso, e o System.Random existe paralelamente a ele. Era uma vez em um dos projetos que parecia que ambos podiam funcionar de maneira diferente (embora devessem ter uma distribuição uniforme).

Então eles não entraram em detalhes - bastava que a transição para o System.Random resolvesse todos os problemas. Agora, decidimos entender com mais detalhes e realizar um pouco de pesquisa: o quão RNGs “tendenciosos” ou previsíveis são e qual escolher. Além disso, muitas vezes ouvi opiniões conflitantes sobre sua "honestidade" - vamos tentar descobrir como os resultados reais se relacionam com os declarados.

Um breve programa educacional ou RNG é realmente um PRNG


Se você já conhece os geradores de números aleatórios, pode prosseguir imediatamente para a seção "Teste".

Os números aleatórios (MF) são uma sequência de números gerados usando algum processo aleatório (caótico), uma fonte de entropia. Ou seja, essa é a sequência, cujos elementos não estão conectados por nenhuma lei matemática - eles não têm uma relação causal.

O que cria uma faixa intermediária é chamado de gerador de números aleatórios (RNG). Parece que tudo é elementar, mas se formos da teoria para a prática, não é tão simples implementar um algoritmo de software para gerar essa sequência.

O motivo está na ausência da própria aleatoriedade nos eletrônicos de consumo modernos. Sem ele, os números aleatórios deixam de ser aleatórios, e seu gerador se transforma em uma função comum de argumentos deliberadamente determinados. Para várias especialidades na área de TI, esse é um problema sério (por exemplo, para criptografia); para o restante, existe uma solução perfeitamente aceitável.

Precisamos escrever um algoritmo que retorne, mesmo que não sejam números verdadeiramente aleatórios, mas o mais próximo possível deles - os chamados números pseudo-aleatórios (PSNs). O algoritmo nesse caso é chamado de gerador de números pseudo-aleatórios (PRNG).

Existem várias opções para criar um PRNG, mas, para todos, o seguinte será relevante:

  1. A necessidade de pré-inicialização.

    O PRNG é desprovido de uma fonte de entropia, portanto, antes de usá-lo, é necessário indicar o estado inicial. É especificado como um número (ou vetor) e é chamado de semente (semente, semente aleatória). Freqüentemente, um contador de relógio do processador ou o equivalente numérico da hora do sistema é usado como semente.
  2. Reprodutibilidade da sequência.

    O PRNG é completamente determinístico; portanto, a semente especificada durante a inicialização determina exclusivamente a sequência futura inteira de números. Isso significa que um único PRSP, inicializado com a mesma semente (em momentos diferentes, em programas diferentes, em dispositivos diferentes) gerará a mesma sequência.

Você também precisa conhecer a distribuição de probabilidade que caracteriza o PRNG - quais números ele gerará e com que probabilidade. Na maioria das vezes, essa é uma distribuição normal ou uniforme.

Distribuição normal (esquerda) e distribuição uniforme (direita)

Digamos que temos um dado honesto com 24 rostos. Se você largá-lo, a probabilidade de uma unidade cair será de 1/24 (assim como a probabilidade de qualquer outro número cair). Se você fizer muitos arremessos e registrar os resultados, notará que todos os rostos caem na mesma frequência. De fato, esse dado pode ser considerado um RNG com uma distribuição uniforme.

E se você jogar imediatamente 10 desses ossos e contar a quantidade total de pontos? A uniformidade será mantida para ela? Não. Na maioria das vezes, o valor será próximo a 125 pontos, ou seja, a algum valor médio. E, como resultado - mesmo antes de fazer um arremesso, você pode estimar aproximadamente o resultado futuro.

O motivo é que, para obter a quantidade média de pontos, existe o maior número de combinações. Quanto mais longe, menos combinações - e, consequentemente, menor chance de perda. Se você visualizar esses dados, eles se parecerão remotamente com a forma de um sino. Portanto, com algum alongamento, um sistema de 10 ossos pode ser chamado de RNG com distribuição normal.

Outro exemplo, apenas já no avião - tiro ao alvo. O atirador será o RNG gerando um par de números (x, y), que é exibido no gráfico.

Concorde que a opção à esquerda está mais próxima da vida real - este é um RNG com uma distribuição normal. Mas se você precisar espalhar estrelas em um céu escuro, a opção certa, obtida com a ajuda de um RNG com uma distribuição uniforme, é melhor. Em geral, escolha um gerador dependendo da tarefa.

Agora vamos falar sobre a entropia da sequência PSP. Por exemplo, há uma sequência que começa assim:

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ão aleatórios são esses números à primeira vista? Vamos começar verificando a distribuição.

Parece quase uniforme, mas se você ler a sequência de dois números e interpretá-los como coordenadas no plano, terá o seguinte:

Os padrões são claramente visíveis. E como os dados na sequência são ordenados de uma certa maneira (ou seja, eles têm baixa entropia), isso pode levar ao próprio "viés". No mínimo, esse PRNG não é muito adequado para gerar coordenadas em um plano.

Outra sequência:

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

Tudo parece estar bem aqui, mesmo no avião:

Vamos ver no volume (lemos três números cada):

E novamente os padrões. Criar visualização em quatro dimensões não funcionará. Mas os padrões podem existir tanto nessa dimensão quanto nas grandes.

Na mesma criptografia, onde os requisitos mais rigorosos são impostos ao PRNG, essa situação é categoricamente inaceitável. Portanto, para avaliar sua qualidade, algoritmos especiais foram desenvolvidos, sobre os quais não abordaremos agora. O tópico é extenso e baseia-se em um artigo separado.

Teste


Se não sabemos algo com certeza, como trabalhar com isso? Vale a pena atravessar a rua se você não sabe qual sinal de trânsito ele permite? As consequências podem ser diferentes.

O mesmo vale para a notória aleatoriedade na Unity. Bem, se a documentação revelar os detalhes necessários, mas a história mencionada no início do artigo aconteceu apenas por causa da falta de detalhes desejados.

E sem saber como a ferramenta funciona, você não pode aplicá-la corretamente. Em geral, chegou a hora de verificar e conduzir um experimento para finalmente garantir pelo menos às custas da distribuição.

A solução foi simples e eficaz - coletar estatísticas, obter dados objetivos e analisar os resultados.

Assunto da pesquisa


Existem várias maneiras de gerar números aleatórios no Unity - testamos cinco.

  1. System.Random.Next (). Gera números inteiros em um determinado intervalo de valores.
  2. System.Random.NextDouble (). Gera números de precisão duplos (duplo) no intervalo de [0; 1)
  3. UnityEngine.Random.Range (). Gera números de precisão únicos (flutuante) em um determinado intervalo de valores.
  4. UnityEngine.Random.value. Gera números únicos de precisão (flutuante) no intervalo de [0; 1)
  5. Unity.Mathematics.Random.NextFloat (). Parte da nova biblioteca Unity.Mathematics. Gera números de precisão únicos (flutuante) em um determinado intervalo de valores.

Em quase toda parte da documentação, foi indicada uma distribuição uniforme, com exceção de UnityEngine.Random.value (onde a distribuição não está especificada, mas de forma semelhante a UnityEngine.Random.Range (), também era esperado que fosse uniforme) e Unity.Mathematics.Random.NextFloat () (em que a base é o algoritmo xorshift, o que significa que novamente você precisa esperar por uma distribuição uniforme).

Por padrão, os esperados na documentação foram obtidos para os resultados esperados.

Metodologia


Escrevemos um pequeno aplicativo que gerou seqüências de números aleatórios em cada um dos métodos apresentados e salvou os resultados para processamento adicional.

O comprimento de cada sequência é 100.000 números.
O intervalo de números aleatórios é [0, 100).

Os dados foram coletados de várias plataformas de destino:

  • Windows
    - 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, montagem no dispositivo, Mono, .NET Standard 2.0
  • iOS
    - Unity v2018.3.14f1, compilação para dispositivo, il2cpp, .NET Standard 2.0

Implementação


Temos várias maneiras diferentes de gerar números aleatórios. Para cada um deles, escreveremos uma classe de wrapper separada, que deve fornecer:

  1. Capacidade de definir o intervalo de valores [min / max). Será definido através do construtor.
  2. Método retornando midrange. Vamos escolher float como o tipo, como um mais geral.
  3. O nome do método de geração para marcar os resultados. Por conveniência, retornaremos o nome completo da classe + o nome do método usado para gerar o intervalo médio como valor.

Primeiro, declare uma abstração, que será representada pela interface IRandomGenerator:

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

Implementação de System.Random.Next ()


Este método permite especificar um intervalo de valores, mas retorna números inteiros, e é necessário um float. Você pode simplesmente interpretar o número inteiro como um ponto flutuante ou expandir o intervalo de valores em várias ordens de magnitude, compensando-os sempre que o intervalo médio for gerado. Acontecerá algo como ponto fixo com a precisão especificada. Usaremos essa opção, pois ela está mais próxima do valor real da flutuação.

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

Implementação de System.Random.NextDouble ()


Aqui um intervalo fixo de valores [0; 1) Para projetá-lo no especificado no construtor, usamos aritmética simples: 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; } } 

Implementação de UnityEngine.Random.Range ()


Este método da classe estática UnityEngine.Random permite especificar um intervalo de valores e retornar um intervalo médio do tipo float. Nenhuma transformação adicional é necessária.

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

Implementação UnityEngine.Random.value


A propriedade value da classe estática UnityEngine.Random retorna uma faixa intermediária do tipo float a partir de um intervalo fixo de valores [0; 1) Nós o projetamos em um determinado intervalo da mesma maneira que ao implementar 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; } } 

Implementação Unity.Mathematics.Random.NextFloat ()


O método NextFloat () da classe Unity.Mathematics.Random retorna um intervalo médio do tipo float e permite especificar um intervalo de valores. A única nuance é que cada instância do Unity.Mathematics.Random terá que ser inicializada com algumas sementes - dessa forma, evitaremos gerar sequências 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); } } 

Implementação do MainController


Várias implementações do IRandomGenerator estão prontas. Em seguida, você precisa gerar sequências e salvar o conjunto de dados resultante para processamento. Para fazer isso, crie uma cena no Unity e um pequeno script MainController, que executará todo o trabalho necessário e será simultaneamente responsável por interagir com a interface do usuário.

Definimos o tamanho do conjunto de dados e o intervalo dos valores de médio porte e também obtemos um método que retorna uma matriz de geradores ajustados e prontos para uso.

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

E agora estamos formando um conjunto de dados. Nesse caso, a geração de dados será combinada com a gravação dos resultados em um fluxo de texto (no formato csv). Para armazenar os valores de cada IRandomGenerator, uma coluna separada é alocada e a primeira linha contém o nome do gerador.

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

Resta chamar o método GenerateCsvDataSet e salvar o resultado em um arquivo ou transferir imediatamente os dados pela rede do dispositivo final para o servidor de recebimento.

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

As fontes do projeto estão no GitLab .

Resultados


Nenhum milagre aconteceu. O que eles esperavam, eles conseguiram - em todos os casos, uma distribuição uniforme, sem uma pitada de conspiração. Não vejo o ponto de aplicar gráficos separados nas plataformas - todos mostram aproximadamente os mesmos resultados.

A realidade é:


Visualização de seqüências em um plano a partir dos cinco métodos de geração:


E visualização em 3D. Deixarei apenas o resultado de System.Random.Next (), para não produzir um monte do mesmo conteúdo.


A história contada na introdução sobre a distribuição normal do UnityEngine.Random não se repetiu: ou foi inicialmente incorreta ou algo mudou no mecanismo desde então. Mas agora temos certeza.

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


All Articles