Percursos
/
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:

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

Gerar valores verdadeiramente aleatórios com um computador é um desafio técnico surpreendentemente difícil, por isso podes ver estes resultados referidos como "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 concebidas como o módulo Random da biblioteca padrão do Julia são rápidas, flexíveis e produzem resultados suficientemente bons para a maioria das aplicações em modelação, simulação e jogos.

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

  • 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 de ser instalados antes de serem usados (e que não estão disponíveis no Exercism).

O Random faz parte da biblioteca padrão e provavelmente já vem pré-instalado, mas vais precisar de adicionar using Random no topo do teu programa para trazer o seu conteúdo para o espaço de nomes.

A função rand()

O que esta função faz depende dos argumentos que lhe dás. Há muitas opções.

Sem argumentos, gera um número de vírgula flutuante entre 0 (inclusive) e 1. Esta é uma distribuição uniform com todos os valores igualmente prováveis, como discutido na secção Trabalhar com distribuições, 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 para a subtração, abordado no Conceito Operações com Vetores. O .- simplesmente aplica esta 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

Com um tipo como único argumento, rand usa typemin e typemax como limites. Provavelmente não é o que queres!

Para inteiros aleatórios, podemos fornecer 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, repara que há valores repetidos (por coincidência), porque cada escolha é independente. Isto é "amostragem com reposição", discutida em mais detalhe abaixo.

Para valores de vírgula flutuante num intervalo, normalmente vais precisar de especificar um passo. Caso contrário, o passo será 1,0 por omissã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

Em alternativa, fornece um array ou uma tupla, e rand devolve 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

Imagina que temos um saco com 3 bolas vermelhas e 4 bolas verdes, e retiramos aleatoriamente uma bola do saco. Para obter uma segunda bola, há duas possibilidades:

  1. Voltas a colocar a primeira bola no saco e agitas bem tudo antes de retirar outra. O número de bolas é agora o mesmo de antes (7), e a proporção de vermelhas para verdes também é a mesma.
  2. Pões a primeira bola na mesa antes de retirar uma segunda. Agora há apenas 6 bolas no saco, e a proporção de vermelhas para verdes depende da cor da primeira bola.

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

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

A mais simples (e, no Exercism, a única opção), usa Random.shuffle() para colocar as entradas numa ordem aleatória e depois usa os primeiros n elementos. Isto é adequado para problemas pequenos, mas pode não escalar bem para coleções grandes: shuffle precisa de gerar o array completo, mesmo que só queiras uma pequena fração dele.

Para fazer amostragem com reposição "em condições", instala o pacote StatsBase.jl. O pacote fornece a função sample() com uma gama completa de opções.

Podemos razoavelmente esperar que funcionalidades semelhantes sejam adicionadas ao Random numa versão futura, para fazerem parte da biblioteca padrão (os exemplos de código neste documento foram testados com o Julia 1.11).

Trabalhar com distribuições

Até agora, concentrámo-nos em casos em que todos os resultados são igualmente prováveis. Por exemplo, rand(1:100) tem a mesma probabilidade de dar qualquer inteiro de 1 a 100.

Muitas situações do mundo real são muito menos simples do que isto. Como resultado, 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. Isto chama-se uma distribuição [uniform][uniform-distribution].

Distribuição gaussiana

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

Por exemplo, supõe que a fábrica onde trabalhas acabou de comprar 10 000 parafusos que deveriam ser idênticos. Queres preparar o robô da fábrica para os manusear, por isso pesas uma amostra de 100 e descobres que têm um peso médio (ou mean) de 4,731 g. É extremamente improvável que isto signifique que todos pesam exatamente 4,731 g. Talvez descubras que os valores variam entre 4,627 e 4,794 g, mas se agrupam em torno de 4,731 g.

Esta é a Gaussian distribution, para a qual as probabilidades atingem o máximo na média e diminuem simetricamente de ambos os lados (daí "em forma de sino"). Para simular isto em software, precisamos de alguma forma de especificar a largura da curva (normalmente, parafusos caros agrupam-se mais estreitamente em torno da média do que parafusos baratos!).

Por convenção, isto faz-se com o standard deviation: valores pequenos para uma curva estreita e afiada, grandes para uma curva baixa e larga. Os matemáticos adoram letras gregas, por isso usamos μ ('mu') para representar a média e σ ('sigma') para representar o desvio padrão. Assim, se leres que "95% dos valores estão dentro de 2σ de μ" ou "o bosão de Higgs foi detetado com uma confiança de 5 sigma", tais comentários dizem respeito ao desvio padrão.

Haverá mais a dizer sobre isto no Conceito Statistics.

A função randn()

Abreviatura de "random normal", é semelhante à variante de vírgula flutuante de rand(), exceto que os valores são distribuídos como uma gaussiana com média 0 e desvio padrão 1.

Novamente, podes querer escalar a saída em bruto de randn para o desvio padrão e deslocá-la para 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 para a saída, que a saída em bruto se agrupa mais perto de zero do que no caso de uma distribuição uniforme. Se duvidares, gera 1000 ou mais e traça-os num gráfico para tornar isso mais óbvio.

O módulo Random

Este módulo contém o nível seguinte de funcionalidades, omitido do Base para ajudar a minimizar o tamanho da configuração predefinida do Julia.

O Random complementa rand e randn no Base com versões mutáveis, rand! e randn!.

Uma adição útil é randstring, que gera uma string de um determinado comprimento. Por omissão, usa letras maiúsculas e minúsculas mais os algarismos 0 a 9, mas outras escolhas podem ser especificadas.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

Além disso, há uma função bitrand para gerar um BitArray aleatório de um comprimento especificado.

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

Baralhamentos e permutações

Para baralhar aleatoriamente entradas num Vector, temos shuffle; também shuffle! para modificar o vetor de entrada no local.

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)

Por vezes, é útil ter antes os índices baralhados. Para isso, usa 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. Estas requerem conhecimentos especializados, por isso consulta a documentação se tiveres interesse nelas.

Sementes e algoritmos

Vários algoritmos de geração de números aleatórios (RNG) estão incorporados no Random como padrão, e qualquer pessoa com competências matemáticas adequadas pode adicionar mais. Estas coisas estão muito além do âmbito deste documento!

Uma razão mais comum para trabalhar com RNGs é especificar uma seed, que tem o efeito de tornar a sequência de saídas "aleatórias" reproduzível de uma execução para a seguinte.

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 com aleatoriedade, probabilidade e estatística. Para mais informações, consulta o Conceito Statistics.

O pacote StatsBase.jl

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

A exceção é StatsBase.sample, que fornece uma implementação completa de amostragem com ou sem reposição (ver uma secçã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 com a (muito comum) Distribuição de Poisson.

Para uma gama muito mais ampla de opções, existe o pacote Distributions.jl para quem tem formação adequada em estatística.

Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Aleatoriedade

A prática está bloqueada

Desbloqueia mais 1 exercício para praticares Aleatoriedade