Trilhas
/
Julia
Julia
/
Programa
/
Aleatoriedade
Al

Aleatoriedade em Julia

2 exercícios

Sobre Aleatoriedade

Muitos programas precisam de valores (aparentemente) aleatórios para simular eventos do mundo real.

Exemplos comuns e familiares incluem:

  • Um lançamento de moeda: um valor aleatório de ('H', 'T').
  • O lançamento de um dado: um número inteiro aleatório de 1 a 6.
  • Embaralhar um baralho de cartas: uma ordenação aleatória de uma lista de cartas.

Gerar valores verdadeiramente aleatórios com um computador é um desafio técnico surpreendentemente difícil, então você pode ver esses resultados serem chamados de “pseudoaleatórios”.

Importante: este Conceito não aborda números aleatórios criptograficamente seguros, que são um desafio muito mais difícil.

No entanto, bibliotecas bem projetadas, como o módulo Random da biblioteca padrão do Julia, são rápidas, flexíveis e dão resultados amplamente bons o suficiente para a maioria das aplicações em modelagem, simulação e jogos.

O Julia divide a funcionalidade de aleatoriedade em vários lugares:

  • Apenas algumas funções básicas, mas muito versáteis, em Base, que estão sempre disponíveis.
  • Uma gama mais ampla de opções no módulo Random.
  • Funcionalidades mais especializadas em pacotes que precisam ser instalados antes do uso (e que não estão disponíveis no Exercism).

Random faz parte da biblioteca padrão e provavelmente já vem instalado, mas você vai precisar adicionar using Random no topo do seu programa para trazer o conteúdo dele para o seu namespace.

A função rand()

O que essa função faz depende dos argumentos que você passa para ela. Existem muitas opções.

Sem argumentos, ela gera um número de ponto flutuante entre 0 (inclusive) e 1. Essa é uma distribuição uniform, em que todos os valores têm a mesma probabilidade, como veremos na seção Trabalhando com distribuições, mais abaixo.

Um único argumento inteiro gera um vetor desse comprimento.

julia> rand()
0.10261774967264703

julia> rand(5)
5-element Vector{Float64}:
 0.24134501977563894
 0.5664193284851202
 0.9804412082089355
 0.6229551330613335
 0.47589221741904664

Para um intervalo diferente, basta deslocar e escalar o resultado de forma adequada.

O exemplo abaixo usa broadcasting na subtração, assunto abordado no Conceito de Operações com Vetores. O .- simplesmente aplica essa aritmética a cada elemento do vetor.

# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
 -0.5303906759076336
  0.9635682226775855
 -0.048823697086981754
  0.465842804648374
  0.9880834344780736

Quando o único argumento é um tipo, rand usa typemin e typemax como limites. Provavelmente não é isso que você quer!

Para números inteiros aleatórios, podemos informar um intervalo e, opcionalmente, quantos valores gerar.

julia> rand(Int64)
-9159538335234594326 # not very useful

julia> rand(1:10, 5)
5-element Vector{Int64}:
 1
 1
 1
 4
 7

No exemplo rand(1:10, 5) acima, repare que há valores repetidos (por coincidência), porque cada escolha é independente. Isso é a “amostragem com reposição”, que veremos em mais detalhes abaixo.

Para valores de ponto flutuante em um intervalo, normalmente você precisa especificar o tamanho do passo. Caso contrário, o passo será 1.0 por padrão, o que raramente é útil.

julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
 3.19
 2.53
 3.14
 3.13

Como alternativa, informe um array ou uma tupla, e rand retornará uma entrada aleatória:

julia> rand([4, 9, 16, 25])
16

# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
 1
  "name"

Amostragem com ou sem reposição

Imagine que temos um saco com 3 bolas vermelhas e 4 bolas verdes e tiramos uma bola do saco ao acaso. Para tirar uma segunda bola, há duas possibilidades:

  1. Devolver a primeira bola ao saco e sacudir bem tudo antes de tirar outra. O número de bolas agora é o mesmo de antes (7), e a proporção entre vermelhas e verdes também é a mesma.
  2. Colocar a primeira bola sobre a mesa antes de tirar a segunda. Agora há apenas 6 bolas no saco, e a proporção vermelho:verde depende da cor da primeira bola.

O cenário 1 é com reposição, o cenário 2 é sem reposição, e eles dão resultados diferentes.

Para simular amostragem sem reposição no Julia, há algumas opções.

A mais simples (e, dentro do Exercism, a única opção): use Random.shuffle() para colocar as entradas em ordem aleatória e depois use os primeiros n elementos. Isso funciona bem para problemas pequenos, mas pode não escalar bem para coleções grandes: shuffle precisa gerar o array inteiro, mesmo que você queira apenas uma pequena fração dele.

Para fazer a amostragem com reposição “do jeito certo”, instale o pacote StatsBase.jl. Ele fornece a função sample() com uma gama completa de opções.

É razoável esperar que funcionalidades semelhantes sejam adicionadas ao Random em uma versão futura, para que façam parte da biblioteca padrão (os exemplos de código deste documento foram testados com o Julia 1.11).

Trabalhando com distribuições

Até agora, concentramos nossa atenção nos casos em que todos os resultados têm a mesma probabilidade. Por exemplo, rand(1:100) tem a mesma probabilidade de dar qualquer número inteiro de 1 a 100.

Muitas situações do mundo real são bem menos simples do que isso. Por isso, os estatísticos criaram uma grande variedade de distributions para descrever matematicamente resultados “do mundo real”.

Distribuições uniformes

A função rand() descrita acima é usada quando todas as probabilidades são iguais. Isso é chamado de distribuição [uniform][uniform-distribution].

Distribuição gaussiana

Também chamada de distribuição “normal” ou curva “em forma de sino”, é uma forma muito comum de descrever a imprecisão em valores medidos.

Por exemplo, suponha que a fábrica onde você trabalha acabou de comprar 10.000 parafusos que deveriam ser idênticos. Você quer configurar o robô da fábrica para lidar com eles, então pesa uma amostra de 100 e descobre que eles têm um peso médio (ou mean) de 4,731 g. É extremamente improvável que isso signifique que todos pesem exatamente 4,731 g. Talvez você descubra que os valores variam de 4,627 a 4,794 g, mas se concentram em torno de 4,731 g.

Essa é a Gaussian distribution, em que as probabilidades atingem o pico na média e caem simetricamente dos dois lados (daí a forma de “sino”). Para simular isso em software, precisamos de alguma forma de especificar a largura da curva (normalmente, parafusos caros se concentram mais em torno da média do que parafusos baratos!).

Por convenção, isso é feito usando standard deviation: valores pequenos para uma curva estreita e pontiaguda, grandes para uma curva baixa e larga. Os matemáticos adoram letras gregas, então usamos μ (‘mu’) para representar a média e σ (‘sigma’) para representar o desvio padrão. Assim, se você ler que “95% dos valores estão dentro de 2σ de μ” ou que “o bóson de Higgs foi detectado com confiança de 5 sigmas”, comentários assim se referem ao desvio padrão.

Haverá mais sobre isso no Conceito Statistics.

A função randn()

Abreviação de “random normal”, é semelhante à variante de ponto flutuante de rand(), exceto que os valores são distribuídos como uma gaussiana com média 0 e desvio padrão 1.

Novamente, talvez você queira escalar a saída bruta de randn para ajustar o desvio padrão, e deslocá-la para ajustar a média. O exemplo abaixo converte para média 30 e desvio padrão 5.

julia> raw = randn(5)
5-element Vector{Float64}:
  3.0762588867281475
  1.5101100620253902
 -0.5914858221637778
  0.684175554069735
 -0.8416433926114673

julia> raw * 5 .+ 30
5-element Vector{Float64}:
 45.38129443364074
 37.55055031012695
 27.04257088918111
 33.420877770348675
 25.791783036942665

É difícil perceber, olhando a saída, que os valores brutos se concentram mais perto de zero do que no caso de uma distribuição uniforme. Se você duvidar, gere 1000 ou mais e faça um gráfico para deixar isso mais evidente.

O módulo Random

Este módulo contém o próximo nível de funcionalidades, deixadas de fora de Base para ajudar a minimizar o tamanho da configuração padrão do Julia.

Random complementa rand e randn de Base com versões que modificam os valores no lugar, rand! e randn!.

Uma adição útil é randstring, que gera uma string de um comprimento qualquer. Por padrão, ela usa letras maiúsculas e minúsculas mais os dígitos de 0 a 9, mas outras opções podem ser especificadas.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

Além disso, há a função bitrand, que gera um BitArray aleatório de comprimento especificado.

julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
 1
 1
 0
 0
 1

Embaralhamentos e permutações

Para embaralhar aleatoriamente as entradas de um Vector, temos shuffle; e também shuffle! para modificar o vetor de entrada no lugar.

julia> v = ['A', '1', '2', 'J', 'Q', 'K'];

julia> shuffle(v)
6-element Vector{Char}:
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)

# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)

Às vezes é útil ter os índices embaralhados em vez disso. Para isso, use randperm(n), em que n é o comprimento da sequência.

julia> randperm(6)
6-element Vector{Int64}:
 6
 2
 4
 1
 3
 5

Na prática, o exemplo acima dá os mesmos resultados que shuffle(1:6).

Funções relacionadas incluem randsubseq, para extrair entradas com probabilidade fixa, e randcycle, para permutações cíclicas. Elas exigem algum conhecimento especializado, então consulte a documentação se tiver interesse nelas.

Sementes e algoritmos

Vários algoritmos de gerador de números aleatórios (RNG) já vêm embutidos no Random como padrão, e qualquer pessoa com as habilidades matemáticas adequadas pode adicionar mais. Essas coisas estão bem além do escopo deste documento!

Um motivo mais comum para trabalhar com RNGs é especificar uma seed, o que tem o efeito de tornar a sequência de saídas “aleatórias” reprodutível de uma execução para a outra.

Essa reprodutibilidade não é apropriada em código de produção, mas pode ajudar em testes e depuração.

Outros pacotes

Fora do Exercism, há muitos pacotes instaláveis relacionados a aleatoriedade, probabilidade e estatística. Para saber um pouco mais, veja o Conceito Statistics.

O pacote StatsBase.jl

A maioria das funções de StatsBase é bastante técnica e não é relevante para este documento.

A exceção é StatsBase.sample, que oferece uma implementação completa de amostragem com ou sem reposição (veja uma seção anterior, acima). Há também funções para amostragem ponderada (não uniforme).

O pacote Distributions.jl

As distribuições uniform e normal (ou gaussiana) foram descritas acima.

O módulo Random também contém randexp, para amostrar da Distribuição Exponencial, que está relacionada à (muito comum) Distribuição de Poisson.

Para uma variedade muito maior de opções, existe o pacote Distributions.jl, para quem tem uma base adequada em estatística.

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Aleatoriedade

A prática está bloqueada

Desbloqueie mais 1 exercício para praticar Aleatoriedade