c++ - randomize - random() c
¿Por qué está sesgado rand()% 6? (5)
Aquí hay profundidades ocultas:
-
El uso de la pequeña
u
enRAND_MAX + 1u
.RAND_MAX
se define como un tipoint
y, a menudo, es elint
más grande posible. El comportamiento deRAND_MAX + 1
sería indefinido en casos en los que desbordaría un tiposigned
. Escribir1u
fuerza la conversión de tipo deRAND_MAX
aunsigned
, evitando así el desbordamiento. -
El uso de
% 6
puede (pero en cada implementación destd::rand
que he visto no lo hace ) introducir ningún sesgo estadístico adicional más allá de la alternativa presentada. Tales casos en los que% 6
es peligroso son aquellos en los que el generador de números tiene correlaciones simples en los bits de orden bajo, como una implementación de IBM bastante famosa (en C) derand
en, creo, en la década de 1970 que volcó los bits altos y bajos como "Un florecimiento final". Otra consideración es que 6 es muy pequeño cf.RAND_MAX
, por lo que habrá un efecto mínimo siRAND_MAX
no es un múltiplo de 6, lo que probablemente no sea.
En conclusión, en estos días, debido a su facilidad de uso, usaría
% 6
.
No es probable que introduzca ninguna anomalía estadística más allá de las introducidas por el propio generador.
Si aún tiene dudas,
pruebe
su generador para ver si tiene las propiedades estadísticas apropiadas para su caso de uso.
Al leer cómo usar std :: rand, encontré este código en cppreference.com
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
¿Qué hay de malo con la expresión de la derecha? Lo probé y funciona perfectamente.
Hay dos problemas con
rand() % 6
(el
1+
no afecta a ninguno de los problemas).
Primero, como han señalado varias respuestas, si los bits bajos de
rand()
no son apropiadamente uniformes, el resultado del operador restante tampoco es uniforme.
Segundo, si el número de valores distintos producidos por
rand()
no es un múltiplo de 6, entonces el resto producirá más valores bajos que valores altos.
Eso es cierto incluso si
rand()
devuelve valores perfectamente distribuidos.
Como ejemplo extremo, imagine que
rand()
produce valores distribuidos uniformemente en el rango
[0..6]
.
Si observa los restos para esos valores, cuando
rand()
devuelve un valor en el rango
[0..5]
, el resto produce resultados distribuidos uniformemente en el rango
[0..5]
.
Cuando
rand()
devuelve 6,
rand() % 6
devuelve 0, como si
rand()
hubiera devuelto 0. Entonces obtienes una distribución con el doble de 0 que cualquier otro valor.
El segundo es el
verdadero
problema con
rand() % 6
.
La forma de evitar ese problema es
descartar
valores que producirían duplicados no uniformes.
Calcula el múltiplo más grande de 6 que es menor o igual a
RAND_MAX
, y cada vez que
rand()
devuelve un valor mayor o igual a ese múltiplo, lo rechaza y llama a `rand () nuevamente, tantas veces como sea necesario.
Asi que:
int max = 6 * ((RAND_MAX + 1u) / 6)
int value = rand();
while (value >= max)
value = rand();
Esa es una implementación diferente del código en cuestión, con la intención de mostrar más claramente lo que está sucediendo.
No soy un usuario experimentado de C ++ de ninguna manera, pero estaba interesado en ver si las otras respuestas con respecto a
std::rand()/((RAND_MAX + 1u)/6)
son menos parciales que
1+std::rand()%6
realidad es cierto.
Así que escribí un programa de prueba para tabular los resultados de ambos métodos (no he escrito C ++ en años, por favor verifíquelo).
Un enlace para ejecutar el código se encuentra
here
.
También se reproduce de la siguiente manera:
// Example program
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <string>
int main()
{
std::srand(std::time(nullptr)); // use current time as seed for random generator
// Roll the die 6000000 times using the supposedly unbiased method and keep track of the results
int results[6] = {0,0,0,0,0,0};
// roll a 6-sided die 20 times
for (int n=0; n != 6000000; ++n) {
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
results[x-1]++;
}
for (int n=0; n !=6; n++) {
std::cout << results[n] << '' '';
}
std::cout << "/n";
// Roll the die 6000000 times using the supposedly biased method and keep track of the results
int results_bias[6] = {0,0,0,0,0,0};
// roll a 6-sided die 20 times
for (int n=0; n != 6000000; ++n) {
int x = 7;
while(x > 6)
x = 1 + std::rand()%6;
results_bias[x-1]++;
}
for (int n=0; n !=6; n++) {
std::cout << results_bias[n] << '' '';
}
}
Luego tomé el resultado de esto y usé la función
chisq.test
en R para ejecutar una prueba de Chi-cuadrado para ver si los resultados son significativamente diferentes de lo esperado.
Esta pregunta de intercambio de pila entra en más detalles sobre el uso de la prueba de chi-cuadrado para evaluar la imparcialidad del dado:
¿Cómo puedo probar si un dado es justo?
.
Aquí están los resultados para algunas ejecuciones:
> ?chisq.test
> unbias <- c(100150, 99658, 100319, 99342, 100418, 100113)
> bias <- c(100049, 100040, 100091, 99966, 100188, 99666 )
> chisq.test(unbias)
Chi-squared test for given probabilities
data: unbias
X-squared = 8.6168, df = 5, p-value = 0.1254
> chisq.test(bias)
Chi-squared test for given probabilities
data: bias
X-squared = 1.6034, df = 5, p-value = 0.9008
> unbias <- c(998630, 1001188, 998932, 1001048, 1000968, 999234 )
> bias <- c(1000071, 1000910, 999078, 1000080, 998786, 1001075 )
> chisq.test(unbias)
Chi-squared test for given probabilities
data: unbias
X-squared = 7.051, df = 5, p-value = 0.2169
> chisq.test(bias)
Chi-squared test for given probabilities
data: bias
X-squared = 4.319, df = 5, p-value = 0.5045
> unbias <- c(998630, 999010, 1000736, 999142, 1000631, 1001851)
> bias <- c(999803, 998651, 1000639, 1000735, 1000064,1000108)
> chisq.test(unbias)
Chi-squared test for given probabilities
data: unbias
X-squared = 7.9592, df = 5, p-value = 0.1585
> chisq.test(bias)
Chi-squared test for given probabilities
data: bias
X-squared = 2.8229, df = 5, p-value = 0.7273
En las tres ejecuciones que hice, el valor p para ambos métodos siempre fue mayor que los valores alfa típicos utilizados para evaluar la significancia (0.05). Esto significa que no consideraríamos que ninguno de ellos sea parcial. Curiosamente, el método supuestamente imparcial tiene valores p consistentemente más bajos, lo que indica que en realidad podría estar más sesgado. La advertencia es que solo hice 3 carreras.
ACTUALIZACIÓN: Mientras escribía mi respuesta, Konrad Rudolph publicó una respuesta que toma el mismo enfoque, pero obtiene un resultado muy diferente. No tengo la reputación de comentar su respuesta, así que voy a abordarla aquí. Primero, lo principal es que el código que usa usa la misma semilla para el generador de números aleatorios cada vez que se ejecuta. Si cambia la semilla, en realidad obtendrá una variedad de resultados. En segundo lugar, si no cambia la semilla, pero cambia el número de ensayos, también obtiene una variedad de resultados. Intenta aumentar o disminuir en un orden de magnitud para ver a qué me refiero. En tercer lugar, hay algún truncamiento o redondeo de enteros donde los valores esperados no son del todo precisos. Probablemente no sea suficiente para marcar la diferencia, pero está ahí.
Básicamente, en resumen, obtuvo la semilla correcta y la cantidad de pruebas que podría estar obteniendo un resultado falso.
Uno puede pensar en un generador de números aleatorios como trabajando en una secuencia de dígitos binarios.
El generador convierte el flujo en números al dividirlo en trozos.
Si la función
std:rand
funciona con un
RAND_MAX
de 32767, entonces está utilizando 15 bits en cada segmento.
Cuando uno toma los módulos de un número entre 0 y 32767 inclusive, encuentra que 5462 ''0''s y'' 1''s pero solo 5461 ''2''s,'' 3''s, ''4''s y'' 5''s. Por lo tanto, el resultado es parcial. Cuanto mayor sea el valor de RAND_MAX, menor será el sesgo, pero es inevitable.
Lo que no está sesgado es un número en el rango [0 .. (2 ^ n) -1]. Puede generar un número (teóricamente) mejor en el rango 0..5 extrayendo 3 bits, convirtiéndolos en un número entero en el rango 0..7 y rechazando 6 y 7.
Uno espera que cada bit en el flujo de bits tenga la misma posibilidad de ser un ''0'' o un ''1'' independientemente de dónde esté en el flujo o los valores de otros bits.
Esto es excepcionalmente difícil en la práctica.
Las diferentes implementaciones de PRNG de software ofrecen diferentes compromisos entre velocidad y calidad.
Un generador congruencial lineal como
std::rand
ofrece la velocidad más rápida para la calidad más baja.
Un generador criptográfico ofrece la más alta calidad a la velocidad más baja.
Este código de ejemplo ilustra que
std::rand
es un caso de balderdash de culto de carga heredado que debería hacer que sus cejas se alcen cada vez que lo vea.
Hay varios problemas aqui:
El contrato que la gente suele asumir, incluso las pobres almas desafortunadas que no conocen mejor y no pensarán en eso precisamente en estos términos, es que las muestras
rand
de la
distribución uniforme
en los enteros en 0, 1, 2, ...,
RAND_MAX
, y cada llamada produce una muestra
independiente
.
El primer problema es que el contrato asumido, muestras aleatorias uniformes independientes en cada llamada, en realidad no es lo que dice la documentación, y en la práctica, las implementaciones históricamente no lograron proporcionar el más mínimo simulacro de independencia.
Por ejemplo, C99 §7.20.2.1 ''La función
rand
'' dice, sin más detalles:
La función
rand
calcula una secuencia de enteros pseudoaleatorios en el rango de 0 aRAND_MAX
.
Esta es una oración sin sentido, porque la pseudoaleatoriedad es una propiedad de una
función
(o
familia de funciones
), no de un número entero, pero eso no impide que incluso los burócratas ISO abusen del lenguaje.
Después de todo, los únicos lectores que estarían molestos por eso saben mejor que leer la documentación de
rand
por temor a que sus células cerebrales se descompongan.
Una implementación histórica típica en C funciona así:
static unsigned int seed = 1;
static void
srand(unsigned int s)
{
seed = s;
}
static unsigned int
rand(void)
{
seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
return (int)seed;
}
Esto tiene la desafortunada propiedad de que
, aunque una sola muestra puede distribuirse uniformemente
bajo una semilla aleatoria uniforme (que depende del valor específico de
RAND_MAX
), alterna entre enteros pares e impares en llamadas consecutivas, después
int a = rand();
int b = rand();
la expresión
(a & 1) ^ (b & 1)
produce 1 con 100% de probabilidad, que no es el caso para muestras aleatorias
independientes
en cualquier distribución compatible con enteros pares e impares.
Por lo tanto, surgió un culto a la carga de que uno debería descartar los bits de bajo orden para perseguir a la bestia esquiva de ''mejor aleatoriedad''.
(Alerta de spoiler: este no es un término técnico. Es una señal de que la prosa que estás leyendo no sabe de qué están hablando o piensa
que
no
tienes
ni idea y que debes ser condescendiente).
El segundo problema es que
incluso si cada llamada muestreara independientemente de una distribución aleatoria uniforme
en 0, 1, 2, ...,
RAND_MAX
, el resultado de
rand() % 6
no se distribuiría uniformemente en 0, 1, 2, 3, 4, 5 como un dado, a menos que
RAND_MAX
sea congruente con -1 módulo 6.
Contraejemplo simple: si
RAND_MAX
= 6, entonces de
rand()
, todos los resultados tienen la misma probabilidad 1/7, pero de
rand() % 6
, el el resultado 0 tiene una probabilidad de 2/7 mientras que todos los demás resultados tienen una probabilidad de 1/7.
La forma correcta de hacerlo es con el muestreo de rechazo:
extraiga
repetidamente
una muestra aleatoria uniforme e independiente de 0, 1, 2, ...,
RAND_MAX
y
rechace
(por ejemplo) los resultados 0, 1, 2, ...,
((RAND_MAX + 1) % 6) - 1
si obtiene uno de esos, comience de nuevo;
de lo contrario, rendimiento
s % 6
.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
De esta manera, el conjunto de resultados de
rand()
que aceptamos es igualmente divisible por 6, y cada posible resultado de
s % 6
se obtiene por el mismo número de resultados
aceptados
de
rand()
, por lo que si
rand()
se distribuye uniformemente entonces también lo es el
s
.
No hay
límite
en el número de intentos, pero el
número esperado
es menor que 2, y la probabilidad de éxito crece exponencialmente con el número de intentos.
La elección de los resultados de
rand()
que rechaza es irrelevante, siempre que asigne un número igual de ellos a cada número entero a continuación 6. El código en cppreference.com toma una decisión
diferente
, debido al primer problema anterior: que nada es garantizado sobre la distribución o independencia de las salidas de
rand()
, y en la práctica, los bits de orden inferior exhiben patrones que no ''parecen lo suficientemente aleatorios'' (no importa que la próxima salida sea una función determinista de la anterior).
Ejercicio para el lector: demuestre que el código en cppreference.com produce una distribución uniforme en los dados si
rand()
produce una distribución uniforme en 0, 1, 2, ...,
RAND_MAX
.
Ejercicio para el lector: ¿Por qué preferiría que uno u otro subconjunto rechace? ¿Qué cálculo se necesita para cada ensayo en los dos casos?
Un tercer problema es que el espacio de semillas es tan pequeño que incluso si la semilla se distribuye uniformemente, un adversario armado con el conocimiento de su programa y un resultado, pero no la semilla, puede predecir fácilmente la semilla y los resultados posteriores, lo que hace que parezca que no es así. al azar después de todo. Así que ni siquiera pienses en usar esto para la criptografía.
Puede ir a la ruta elegante de
std::uniform_int_distribution
clase
std::uniform_int_distribution
C ++ 11 con un dispositivo aleatorio apropiado y su motor aleatorio favorito como el siempre popular Mersenne twister
std::mt19937
para jugar a los dados con su primo de cuatro años , pero incluso eso no va a ser adecuado para generar material de clave criptográfica, y el tornado Mersenne también es un terrible cerdo espacial con un estado de varios kilobytes que causa estragos en el caché de su CPU con un tiempo de configuración obsceno, por lo que es malo incluso para
por ejemplo
, simulaciones paralelas de Monte Carlo con árboles reproducibles de subcomputaciones;
su popularidad probablemente surge principalmente de su nombre pegadizo.
¡Pero puedes usarlo para lanzar dados de juguete como este ejemplo!
Otro enfoque es utilizar un generador de números pseudoaleatorio criptográfico simple con un estado pequeño, como un simple borrado de clave rápida PRNG , o simplemente un cifrado de flujo como AES-CTR o ChaCha20 si está seguro ( por ejemplo , en una simulación de Monte Carlo para investigación en ciencias naturales) que no hay consecuencias adversas para predecir resultados pasados si el estado se ve comprometido alguna vez.