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 en la expresión de la derecha? Lo probé y funciona perfectamente.
std::uniform_int_distribution
para los dadosrand()
es tan malo en implementaciones típicas, también podría usar el xkcd RNG . Entonces está mal porque usarand()
.uniform_int_distribution
.)Respuestas:
Hay dos problemas con
rand() % 6
(1+
no afecta a ninguno de los dos).Primero, como se ha señalado en varias respuestas, si los bits bajos de
rand()
no son adecuadamente uniformes, el resultado del operador restante tampoco es uniforme.En segundo lugar, si el número de valores distintos producidos por
rand()
no es un múltiplo de 6, el resto producirá más valores bajos que valores altos. Eso es cierto incluso sirand()
devuelve valores perfectamente distribuidos.Como ejemplo extremo, imagine que
rand()
produce valores distribuidos uniformemente en el rango[0..6]
. Si observa los restos de esos valores, cuandorand()
devuelve un valor en el rango[0..5]
, el resto produce resultados distribuidos uniformemente en el rango[0..5]
. Cuandorand()
devuelve 6,rand() % 6
devuelve 0, como sirand()
hubiera devuelto 0. De modo que obtiene una distribución con el doble de ceros que cualquier otro valor.El segundo es el verdadero problema con
rand() % 6
.La forma de evitar ese problema es descartar los 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 siempre querand()
devuelve un valor que es mayor o igual a ese múltiplo, lo rechaza y llama a `rand () nuevamente, tantas veces como sea necesario.Entonces:
Esa es una implementación diferente del código en cuestión, destinada a mostrar más claramente lo que está sucediendo.
fuente
Aquí hay profundidades ocultas:
El uso de lo pequeño
u
enRAND_MAX + 1u
.RAND_MAX
se define como unint
tipo y suele ser el más grande posibleint
. El comportamiento deRAND_MAX + 1
sería indefinido en casos en los que desbordaría unsigned
tipo. La escritura1u
fuerza la conversión de tipos deRAND_MAX
aunsigned
, evitando así el desbordamiento.El uso de
% 6
can (pero en todas las implementacionesstd::rand
que he visto no lo hace ) introduce un sesgo estadístico adicional más allá de la alternativa presentada. Tales casos en los que% 6
es peligroso son casos en los que el generador de números tiene llanuras de correlación en los bits de orden inferior, como una implementación de IBM bastante famosa (en C) derand
, creo, en la década de 1970, que cambió los bits altos y bajos como "un final florecer". Una consideración adicional 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, que probablemente no lo sea.En conclusión, en estos días, por su manejabilidad, usaría
% 6
. No es probable que introduzca anomalías estadísticas 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 adecuadas para su caso de uso.fuente
% 6
produce un resultado sesgado cuando el número de valores distintos generados porrand()
no es un múltiplo de 6. Principio de casillero. Por supuesto, el sesgo es pequeño cuandoRAND_MAX
es mucho mayor que 6, pero está ahí. Y para rangos de objetivos más grandes, el efecto es, por supuesto, mayor.x==7
. Básicamente, divide el rango[0, RAND_MAX]
en 7 subrangos, 6 del mismo tamaño y un subrango más pequeño al final. Los resultados del último subrango se descartan. Es bastante obvio que no puede tener dos subrangos más pequeños al final de esta manera.Este código de ejemplo ilustra que
std::rand
es un caso de legado culto al cargo que debería hacer que sus cejas se eleven cada vez que lo vea.Hay varios problemas aqui:
El contrato que la gente suele asumir, incluso las pobres almas desventuradas que no saben nada mejor y no pensarán en ello precisamente en estos términos, es que las
rand
muestras de la distribución uniforme de los números 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 e independientes en cada llamada, no es realmente lo que dice la documentación y, en la práctica, históricamente las implementaciones no lograron proporcionar ni el más mínimo simulacro de independencia. Por ejemplo, C99 §7.20.2.1 'La
rand
función' dice, sin más detalles: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 de ISO abusen del lenguaje. Después de todo, los únicos lectores a los que les molestaría saber que no deben leer la documentación
rand
por temor a que sus células cerebrales se descompongan.Una implementación histórica típica en C funciona así:
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 dela expresión
(a & 1) ^ (b & 1)
da 1 con 100% de probabilidad, lo que no es el caso de muestras aleatorias independientes en cualquier distribución compatible con enteros pares e impares. Por lo tanto, surgió un culto de carga en el que uno debería descartar los bits de bajo orden para perseguir a la escurridiza bestia de la "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 lo que están hablando o piensa que no tienes ni idea y debes ser condescendiente).El segundo problema es que incluso si cada llamada muestreó independientemente de una distribución aleatoria uniforme en 0, 1, 2,…
RAND_MAX
, el resultado derand() % 6
no se distribuiría uniformemente en 0, 1, 2, 3, 4, 5 como un dado. tirar, a menos queRAND_MAX
sea congruente con -1 módulo 6. Contraejemplo simple: siRAND_MAX
= 6, entonces desderand()
, todos los resultados tienen la misma probabilidad 1/7, pero desderand() % 6
, el resultado 0 tiene probabilidad 2/7 mientras que todos los demás resultados tienen probabilidad 1/7 .La forma correcta de hacer esto es con muestreo de rechazo: extraer repetidamente una muestra aleatoria uniforme e independiente
s
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, empezar de nuevo; de lo contrario, cedes % 6
.De esta manera, el conjunto de resultados
rand()
que aceptamos es divisible por 6, y cada resultado posible des % 6
se obtiene por el mismo número de resultados aceptados derand()
, por lo que sirand()
se distribuye uniformemente, entonces también lo ess
. No hay límite en el número de ensayos, pero el número esperado es menor que 2 y la probabilidad de éxito aumenta exponencialmente con el número de ensayos.La elección de cuál los resultados de la
rand()
rechaza es irrelevante, siempre y cuando se asigna el mismo número de ellos a cada número entero inferior a 6. El código en cppreference.com hace una diferente opción, debido al primer problema por encima de la que no se garantiza nada acerca de la distribución o independencia de salidas derand()
, y en la práctica, los bits de bajo orden exhibieron patrones que no "parecen lo suficientemente aleatorios" (no importa que la siguiente salida sea una función determinista de la anterior).Ejercicio para el lector: Demostrar que el código en cppreference.com produce una distribución uniforme sobre rodillos de matriz si
rand()
los rendimientos de una distribución uniforme en 0, 1, 2, ...,RAND_MAX
.Ejercicio para el lector: ¿Por qué preferiría rechazar uno u otro subconjunto? ¿Qué cálculo se necesita para cada ensayo en los dos casos?
Un tercer problema es que el espacio semilla 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 subsiguientes, lo que hace que no parezcan tan al azar después de todo. Así que ni siquiera pienses en usar esto para criptografía.
Puede seguir la elegante ruta de ingeniería excesiva y la
std::uniform_int_distribution
clase de C ++ 11 con un dispositivo aleatorio apropiado y su motor aleatorio favorito, como el siempre popular tornado Mersenne,std::mt19937
para jugar a los dados con su primo de cuatro años, pero incluso eso no va a funcionar. estar en forma para generar material de claves criptográficas, y el tornado de Mersenne también es un terrible acaparador de espacio con un estado de varios kilobytes que causa estragos en la memoria caché de su CPU con un tiempo de configuración obsceno, por lo que es malo incluso para, por ejemplo , simulaciones de Monte Carlo en paralelo con árboles reproducibles de subcomputaciones; su popularidad probablemente se deba principalmente a su pegadizo nombre. ¡Pero puedes usarlo para tirar dados de juguete como este ejemplo!Otro enfoque es usar un generador de números pseudoaleatorios criptográfico simple con un estado pequeño, como un simple borrado rápido de clave 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 alguna vez se ve comprometido.
fuente
(RAND_MAX + 1 )% 6
valores. No importa cómo subdivida los posibles resultados. Puede rechazarlos desde cualquier lugar del rango[0, RAND_MAX)
, siempre que el tamaño del rango aceptado sea un múltiplo de 6. Demonios, puede rechazar cualquier resultadox>6
y ya no lo necesitará%6
.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)
ser menos sesgadas de lo que1+std::rand()%6
realmente son ciertas. Así que escribí un programa de prueba para tabular los resultados de ambos métodos (no he escrito C ++ en años, verifíquelo). Aquí se encuentra un enlace para ejecutar el código . También se reproduce de la siguiente manera:Luego tomé el resultado de esto y usé la
chisq.test
función 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 explica con más detalle el uso de la prueba de chi-cuadrado para probar la equidad del dado: ¿Cómo puedo probar si un dado es justo? . Estos son los resultados de algunas ejecuciones:En las tres carreras que hice, el valor p para ambos métodos fue siempre mayor que los valores alfa típicos utilizados para probar la significancia (0.05). Esto significa que no consideraríamos a ninguno de ellos como 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 adopta el mismo enfoque, pero obtiene un resultado muy diferente. No tengo la reputación de comentar su respuesta, así que lo abordaré 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 obtiene una variedad de resultados. En segundo lugar, si no cambia la semilla, pero cambia el número de ensayos, también obtendrá una variedad de resultados. Intente aumentar o disminuir en un orden de magnitud para ver a qué me refiero. En tercer lugar, hay un truncamiento o redondeo de enteros en los que los valores esperados no son del todo precisos. Probablemente no sea suficiente para marcar la diferencia, pero está ahí.
Básicamente, en resumen, simplemente obtuvo la semilla correcta y el número de pruebas que podría estar obteniendo un resultado falso.
fuente
rand()%6
conrand()/(1+RAND_MAX)/6
. Más bien, está comparando la toma directa del resto con el muestreo de rechazo (consulte otras respuestas para obtener una explicación). En consecuencia, su segundo código es incorrecto (elwhile
ciclo no hace nada). Su prueba estadística también tiene problemas (no puede simplemente ejecutar repeticiones de su prueba para comprobar la solidez, no realizó la corrección,…).std::srand
(y sin usar<random>
) es bastante difícil de hacer de una manera que cumpla con los estándares y no quería que su complejidad reste valor al código restante. También es irrelevante para el cálculo: repetir la misma secuencia en una simulación es totalmente aceptable. Por supuesto diferentes semillas se producen diferentes resultados, y algunos serán no significativa. Eso es completamente esperado en función de cómo se define el valor p.std::rand
produce simulaciones de lanzamiento de moneda notablemente buenas para un d6, en todo el rango de semillas aleatorias.RAND_MAX
, que determina el tamaño del efecto del sesgo de módulo. La significancia estadística es la probabilidad bajo la hipótesis nula de que la rechaces falsamente. ¿Cuál es el poder estadístico , la probabilidad bajo una hipótesis alternativa de que su prueba rechace correctamente la hipótesis nula? ¿Detectaría derand() % 6
esta manera cuando RAND_MAX = 2 ^ 31 - 1?Se puede pensar en un generador de números aleatorios como si trabajara en una secuencia de dígitos binarios. El generador convierte la transmisión en números dividiéndola en trozos. Si la
std:rand
función está trabajando con unRAND_MAX
32767, entonces está usando 15 bits en cada segmento.Cuando uno toma los módulos de un número entre 0 y 32767 inclusive, se encuentra que 5462 '0's y' 1's pero solo 5461 '2's,' 3's, '4's y' 5's. Por tanto, el resultado está sesgado. Cuanto mayor sea el valor RAND_MAX, menos sesgo habrá, pero es ineludible.
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.
Se espera que cada bit del flujo de bits tenga la misma probabilidad de ser un '0' o un '1' independientemente de dónde se encuentre en el flujo o de los valores de otros bits. Esto es excepcionalmente difícil en la práctica. Las diferentes implementaciones de software PRNG ofrecen diferentes compromisos entre velocidad y calidad. Un generador congruencial lineal como
std::rand
ofrece la velocidad más rápida con la calidad más baja. Un generador criptográfico ofrece la máxima calidad a la menor velocidad.fuente