Python: Geradores de números aleatórios

igormontagner.blogspot.com.br

Igor dos Santos Montagner - Blog: Geradores de números aleatórios em Python


Diversos algoritmos e sistemas possuem algum comportamento aleatório inerente ao seu funcionamento. O algoritmo Quicksort, por exemplo, seleciona o elemento pivô aleatoriamente. Esta escolha ajuda a manter a complexidade média do algoritmo O(n log n). O uso de algoritmos aleatórios também é bastante comum na Ciência. Frequentemente utilizamos geradores de números aleatórios para dividir conjuntos de dados ou selecionar parâmetros de modelos. Porém, testar estes métodos pode ser difícil, já que cada hora o resultado do método pode retornar valores diferentes. A reprodutibilidade de experimentos científicos também fica prejudicada, pois não é possível reproduzir exatamente o experimento realizado por outro pesquisador.

Felizmente Python possui diversas bibliotecas científicas que podem resolver este problema. Algoritmos geradores de números aleatórios (RNG) tipicamente dependem de um número seed que controla qual sequência de números será gerada. Dado um mesmo seed o algoritmo gerará sempra a mesma sequência de números, exatamente na mesma ordem. Para facilitar a utilização por usuários leigos, RNGs costumam usar automaticamente um seed baseado no horário atual. Desta maneira, ao chamar diretamente o RNG o usuário sempre obterá uma sequência de números diferente.

Na hora de testar um método ou realizar experimentos, porém, é importante fixar manualmente um seed. Assim toda execução do programa gerará o mesmo resultado mesmo que parte do programa se baseie em comportamento aleatório. Existem duas maneiras de se fixar um seed manualmente e isto depende de quais bibliotecas estão sendo usadas.

O módulo random da biblioteca padrão possui um RNG eficiente e várias funções para introduzir comportamento aleatório em programas. A documentação oficial do módulo é excelente, vale a pena dar uma olhada. No exemplo abaixo usamos algumas funções diferentes do módulo random e mostramos os resultados de suas execuções usando diferentes seeds.

Exemplo 1: embaralhamento de uma lista.

import random

# usando seed 0
lista = list(range(1, 20))
random.seed(0)
random.shuffle(lista)
print('Seed 0', lista)

# usando seed 10
lista = list(range(1, 20))
random.seed(10)
random.shuffle(lista)
print('Seed 10', lista)

# usando seed 0 de novo, o resultado é igual à primeira rodada.
lista = list(range(1, 20))
random.seed(0)
random.shuffle(lista)
print('Seed 0', lista, 'resultado igual a primeira rodada')
Seed 0 [10, 12, 15, 19, 1, 17, 11, 3, 4, 6, 18, 5, 7, 8, 16, 9, 2, 14,
13]
Seed 10 [7, 18, 6, 11, 9, 13, 15, 17, 3, 5, 12, 8, 4, 1, 10, 16, 14,
2, 19]
Seed 0 [10, 12, 15, 19, 1, 17, 11, 3, 4, 6, 18, 5, 7, 8, 16, 9, 2, 14,
13] resultado igual a primeira rodada

Exemplo 2: amostra de uma distribuição normal

import random

# usando seed 0
random.seed(0)
print('Seed 0', [random.gauss(0, 1) for i in range(3)])
print('Seed 0', [random.gauss(0, 1) for i in range(3)])

# usando seed 10
random.seed(10)
print('Seed 10', [random.gauss(0, 1) for i in range(3)])
print('Seed 10', [random.gauss(0, 1) for i in range(3)])

# usando seed 0 de novo. Ao resetar o seed entre as
# chamadas de random.gauss selecionamos de novo
# a mesma amostra.
random.seed(0)
print('Seed 0', [random.gauss(0, 1) for i in range(3)])
random.seed(0)
print('Seed 0', [random.gauss(0, 1) for i in range(3)])
Seed 0 [0.9417154046806644, -1.3965781047011498, -0.6797144480784211]
Seed 0 [0.3705035674606598, -1.016348894188071, -0.07212002278507135]
Seed 10 [-0.9537170080633371, -0.45909415683868526,
-0.5992494722090856]
Seed 10 [-0.32014240105647934, 0.7217183074100624, -1.717264917767424]
Seed 0 [0.9417154046806644, -1.3965781047011498, -0.6797144480784211]
Seed 0 [0.9417154046806644, -1.3965781047011498, -0.6797144480784211]

Uma outra maneira de gerar números aleatórios é usando o numpy. Além das funções da biblioteca padrão o numpy possui uma série de distribuições estatísticas (no pacote [scipy.stats]) e é usado em diversas outras biliotecas, como o scikit-learn.

Uma das grandes vantagens do Numpy é a possibilidade de instanciar um RNG e gerar, ao mesmo tempo, números aleatórios usando seeds diferentes. Isto torna possível "congelar" o estado de um RNG e várias funções que usam Numpy para gerar números aleatórios aceitam uma instância de RNG como parâmetro. Um exemplo disto é a função train_test_split do scikit-learn, que recebe um RNG no parâmetro random_state.

Usando dois RNGs simultaneamente
import numpy as np
import numpy.random

# Cria um RNG
rng1 = np.random.RandomState(10)
print('RNG1, seed 10', rng1.randint(30, 40, 5))

# O módulo np.random encapsula uma instância padrão de RandomState.
print('np.random, seed padrão', np.random.randint(30, 40, 5))

#Se usarmos o mesmo seed, np.random gerará os mesmos inteiros que
rng1.
np.random.seed(10)
print('np.random, seed 10', np.random.randint(30, 40, 5))
RNG1, seed 10 [39 34 30 31 39]
np.random, seed padrão [39 35 33 31 35]
np.random, seed 10 [39 34 30 31 39]

Poder guardar o estado do RNG e passá-lo para funções é uma das melhores maneiras de gerar experimentos científicos reprodutíveis. Se usamos o gerador padrão (seja np.random ou o módulo random) qualquer modificação no programa que necessite de números aleatórios pode mudar o resultado final mesmo que a modificação não mexa em nenhuma variável do programa. Veja um exemplo abaixo:

np.random.seed(0) # resultados reprodutíveis
vetor = np.random.normal(5, 200, 100)
amostra = np.random.choice(vetor, 10)
print(np.mean(amostra))

Este programa simples retorna a média de uma amostra de um vetor com 100 componentes. Muito simples e sem segredo algum. Suponha que fizemos a seguinte modificação neste programa.

np.random.seed(0) # resultados reprodutíveis
vetor = np.random.normal(5, 200, 100)

num_aleatorio = np.random.random()
print(num_aleatorio)

amostra = np.random.choice(vetor, 10)
print(np.mean(amostra))
0.4238550485581797
95.7857427627

O resultado final muda apesar de não mexermos nem em vetor nem em amostra. Está implícito em usar um RNG que a ordem das chamadas feitas importa para o resultado final. Este caso é muito claro, mas se chamamos um algoritmo caixa-preta que não conhecemos e ele usar o RNG padrão nossos resultados serão modificados. Este tipo de efeito colateral é muito difícil de descobrir e pode causar muita dor de cabeça. Agora, se seu código for feito como abaixo isto não ocorre a não ser que a variável meu_rng seja passada explicitamente para o código extra.

meu_rng = np.random.RandomState(0) # resultados reprodutíveis mesmo
vetor = meu_rng.normal(5, 200, 100)

# se não usa meu_rng então não afeta nosso código!!!
num_aleatorio = np.random.random()
print(num_aleatorio)

amostra = meu_rng.choice(vetor, 10)
print(np.mean(amostra))
0.878452190276042
71.768673777

Espero ter mostrado algumas coisas úteis sobre como utilizar melhor as funções de geração de números aleatórios tanto para testes de software quanto para criação de experimentoos científicos reprodutíveis. Qualquer dúvida só usar os comentários abaixo.

Comentários