Saat membaca cara menggunakan std :: rand, saya menemukan kode ini di cppreference.com
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
Apa yang salah dengan ekspresi di sebelah kanan? Sudah mencobanya dan bekerja dengan sempurna.
std::uniform_int_distribution
untuk dadurand()
sangat buruk dalam implementasi umum, Anda mungkin juga menggunakan xkcd RNG . Jadi salah karena menggunakanrand()
.uniform_int_distribution
.)Jawaban:
Ada dua masalah dengan
rand() % 6
(1+
tidak memengaruhi masalah mana pun).Pertama, seperti yang ditunjukkan beberapa jawaban, jika bit rendah
rand()
tidak seragam, hasil operator sisanya juga tidak seragam.Kedua, jika jumlah nilai berbeda yang dihasilkan
rand()
bukan merupakan kelipatan 6, maka sisanya akan menghasilkan nilai yang lebih rendah daripada nilai tinggi. Itu benar bahkan jikarand()
mengembalikan nilai yang didistribusikan dengan sempurna.Sebagai contoh ekstrem, anggaplah yang
rand()
menghasilkan nilai terdistribusi seragam dalam rentang tersebut[0..6]
. Jika Anda melihat sisa untuk nilai tersebut, saatrand()
mengembalikan nilai dalam rentang[0..5]
, sisanya menghasilkan hasil yang terdistribusi secara seragam dalam rentang tersebut[0..5]
. Ketikarand()
mengembalikan 6,rand() % 6
mengembalikan 0, sama seperti jikarand()
mengembalikan 0. Jadi Anda mendapatkan distribusi dengan dua kali lebih banyak 0 dari nilai lainnya.Yang kedua adalah masalah sebenarnya dengan
rand() % 6
.Cara untuk menghindari masalah itu adalah dengan membuang nilai yang akan menghasilkan duplikat yang tidak seragam. Anda menghitung kelipatan 6 terbesar yang kurang dari atau sama dengan
RAND_MAX
, dan setiap kalirand()
mengembalikan nilai yang lebih besar dari atau sama dengan kelipatan itu Anda menolaknya dan memanggil `rand () lagi, sebanyak yang diperlukan.Begitu:
Itu adalah implementasi berbeda dari kode yang dipermasalahkan, yang dimaksudkan untuk menunjukkan dengan lebih jelas apa yang sedang terjadi.
sumber
Ada kedalaman tersembunyi di sini:
Penggunaan small
u
inRAND_MAX + 1u
.RAND_MAX
didefinisikan sebagaiint
tipe, dan seringkali yang terbesarint
. PerilakuRAND_MAX + 1
tidak akan ditentukan dalam contoh seperti Anda akan meluapsigned
jenis. Penulisan1u
memaksa jenis konversiRAND_MAX
menjadiunsigned
, sehingga menghindari luapan.Penggunaan
% 6
can (tetapi pada setiap implementasi yangstd::rand
pernah saya lihat tidak ) menimbulkan bias statistik tambahan di atas dan di luar alternatif yang disajikan. Contoh di mana% 6
berbahaya adalah kasus di mana penghasil angka memiliki dataran korelasi dalam bit orde rendah, seperti implementasi IBM yang agak terkenal (dalam C)rand
pada, saya pikir, tahun 1970-an yang membalik bit tinggi dan rendah sebagai "final berkembang". Pertimbangan lebih lanjut adalah bahwa 6 sangat kecil lih.RAND_MAX
, jadi akan ada efek minimal jikaRAND_MAX
bukan kelipatan 6, yang mungkin juga bukan.Sebagai kesimpulan, akhir-akhir ini, karena mudah diatur, saya akan menggunakan
% 6
. Ini tidak mungkin untuk memperkenalkan anomali statistik selain yang diperkenalkan oleh generator itu sendiri. Jika Anda masih ragu, uji generator Anda untuk melihat apakah generator tersebut memiliki properti statistik yang sesuai untuk kasus penggunaan Anda.sumber
% 6
menghasilkan hasil yang bias jika jumlah nilai berbeda yang dihasilkanrand()
bukan merupakan kelipatan 6. Prinsip lubang merpati. Memang, biasnya kecil bilaRAND_MAX
jauh lebih besar dari 6, tapi itu ada. Dan untuk rentang target yang lebih besar, efeknya tentu saja lebih besar.x==7
. Secara bsically, Anda membagi rentang[0, RAND_MAX]
dalam 7 subrentang, 6 dengan ukuran yang sama dan satu subrentang yang lebih kecil di akhir. Hasil dari subrange terakhir akan dibuang. Cukup jelas bahwa Anda tidak dapat memiliki dua sub-rentang yang lebih kecil pada akhirnya dengan cara ini.Kode contoh ini menggambarkan bahwa
std::rand
kasus bualan kultus kargo warisan yang harus membuat alis Anda terangkat setiap kali Anda melihatnya.Ada beberapa masalah di sini:
Kontrak yang biasanya diasumsikan orang — bahkan jiwa malang yang tidak tahu apa-apa dan tidak akan memikirkannya dengan tepat dalam istilah-istilah ini — adalah
rand
sampel dari distribusi seragam pada bilangan bulat di 0, 1, 2,…RAND_MAX
,, dan setiap panggilan menghasilkan sampel independen .Masalah pertama adalah bahwa kontrak yang diasumsikan, sampel acak seragam independen di setiap panggilan, sebenarnya tidak seperti yang dikatakan dokumentasi — dan dalam praktiknya, implementasi secara historis gagal untuk memberikan simulacrum kemandirian yang paling sederhana sekalipun. Misalnya, C99 §7.20.2.1 'The
rand
function' mengatakan, tanpa elaborasi:Ini adalah kalimat yang tidak berarti, karena pseudorandomness adalah properti dari suatu fungsi (atau kelompok fungsi ), bukan dari bilangan bulat, tetapi itu tidak menghentikan birokrat ISO untuk menyalahgunakan bahasa tersebut. Toh, satu-satunya pembaca yang akan kecewa dengan itu tahu lebih baik daripada membaca dokumentasi
rand
karena takut sel otak mereka membusuk.Implementasi historis yang khas di C bekerja seperti ini:
Ini memiliki sifat yang tidak menguntungkan bahwa meskipun satu sampel dapat didistribusikan secara seragam di bawah benih acak yang seragam (yang bergantung pada nilai spesifik
RAND_MAX
), itu bergantian antara bilangan bulat genap dan ganjil dalam panggilan berturut-turut — setelahekspresi tersebut
(a & 1) ^ (b & 1)
menghasilkan 1 dengan probabilitas 100%, yang tidak berlaku untuk sampel acak independen pada distribusi apa pun yang didukung pada bilangan bulat genap dan ganjil. Dengan demikian, sebuah kultus kargo muncul bahwa seseorang harus membuang bit orde rendah untuk mengejar binatang buas yang sulit dipahami dengan 'keacakan yang lebih baik'. (Peringatan spoiler: Ini bukan istilah teknis. Ini adalah tanda bahwa prosa siapa pun yang Anda baca tidak tahu apa yang mereka bicarakan, atau berpikir Anda tidak mengerti dan harus direndahkan.)Masalah kedua adalah bahwa bahkan jika setiap panggilan melakukan sampel secara independen dari distribusi acak seragam pada 0, 1, 2,…,,
RAND_MAX
hasilrand() % 6
tidak akan didistribusikan secara seragam dalam 0, 1, 2, 3, 4, 5 seperti dadu roll, kecualiRAND_MAX
kongruen dengan -1 modulo 6. Counterexample sederhana: JikaRAND_MAX
= 6, maka darirand()
, semua hasil memiliki probabilitas yang sama 1/7, tetapi darirand() % 6
, hasil 0 memiliki probabilitas 2/7 sedangkan semua hasil lainnya memiliki probabilitas 1/7 .Cara yang benar untuk melakukan ini adalah dengan pengambilan sampel penolakan: menggambar berulang kali sampel acak seragam independen
s
dari 0, 1, 2,…RAND_MAX
,, dan menolak (misalnya) hasil 0, 1, 2,…,((RAND_MAX + 1) % 6) - 1
—jika Anda mendapatkan salah satu dari mereka, mulai lagi; jika tidak, hasils % 6
.Dengan cara ini, himpunan hasil dari
rand()
yang kita terima dibagi rata oleh 6, dan setiap kemungkinan hasils % 6
diperoleh dengan jumlah hasil yang diterima yang samarand()
, jadi jikarand()
didistribusikan secara seragam maka begitu jugas
. Tidak ada batasan pada jumlah percobaan, tetapi jumlah yang diharapkan kurang dari 2, dan probabilitas keberhasilan tumbuh secara eksponensial dengan jumlah percobaan.Pilihan yang hasil-hasil dari
rand()
Anda menolak tidaklah penting, asalkan Anda memetakan jumlah yang sama dari mereka untuk setiap bilangan bulat di bawah 6. Kode di cppreference.com membuat yang berbeda pilihan, karena masalah pertama di atas-yang tidak dijamin tentang distribusi atau kemandirian keluaranrand()
, dan dalam praktiknya bit orde rendah menunjukkan pola yang tidak 'terlihat cukup acak' (tidak peduli bahwa keluaran berikutnya adalah fungsi deterministik dari yang sebelumnya).Latihan untuk pembaca: Buktikan bahwa kode di cppreference.com menghasilkan distribusi yang seragam pada gulungan cetakan jika
rand()
menghasilkan distribusi seragam pada 0, 1, 2,…RAND_MAX
,.Latihan untuk pembaca: Mengapa Anda lebih memilih salah satu atau subkumpulan lainnya untuk ditolak? Perhitungan apa yang diperlukan untuk setiap percobaan dalam dua kasus?
Masalah ketiga adalah bahwa ruang benih sangat kecil sehingga meskipun benih didistribusikan secara seragam, musuh yang dipersenjatai dengan pengetahuan tentang program Anda dan satu hasil tetapi bukan benih dapat dengan mudah memprediksi benih dan hasil selanjutnya, yang membuatnya tampak tidak begitu acak setelah semua. Jadi jangan pernah berpikir untuk menggunakan ini untuk kriptografi.
Anda dapat menggunakan rute rekayasa berlebihan yang mewah dan kelas C ++ 11
std::uniform_int_distribution
dengan perangkat acak yang sesuai dan mesin acak favorit Anda seperti angin puyuh Mersenne yang selalu populerstd::mt19937
untuk bermain dadu dengan sepupu Anda yang berusia empat tahun, tetapi bahkan itu pun tidak akan berhasil. cocok untuk menghasilkan materi kunci kriptografik — dan twister Mersenne juga merupakan babi ruang yang mengerikan dengan status multi-kilobyte yang mendatangkan malapetaka pada cache CPU Anda dengan waktu penyiapan yang tidak senonoh, sehingga buruk bahkan untuk, misalnya , simulasi Monte Carlo paralel dengan pohon subkomputasi yang dapat direproduksi; popularitasnya kemungkinan besar muncul terutama dari namanya yang menarik. Tapi Anda bisa menggunakannya untuk mainan dadu yang bergulir seperti contoh ini!Pendekatan lain adalah dengan menggunakan generator nomor pseudorandom kriptografi sederhana dengan keadaan kecil, seperti PRNG penghapusan kunci cepat sederhana , atau hanya stream cipher seperti AES-CTR atau ChaCha20 jika Anda yakin ( misalnya , dalam simulasi Monte Carlo untuk penelitian dalam ilmu alam) bahwa tidak ada konsekuensi yang merugikan untuk memprediksi hasil masa lalu jika negara pernah dikompromikan.
sumber
(RAND_MAX + 1 )% 6
nilai yang persis sama . Tidak peduli bagaimana Anda membagi hasil yang mungkin. Anda dapat menolaknya dari mana saja dalam kisaran tersebut[0, RAND_MAX)
, selama ukuran kisaran yang diterima adalah kelipatan 6. Sial, Anda dapat menolak hasil apa punx>6
, dan Anda tidak akan membutuhkannya%6
lagi.Saya bukan pengguna C ++ berpengalaman dengan cara apa pun, tetapi tertarik untuk melihat apakah jawaban lain tentang
std::rand()/((RAND_MAX + 1u)/6)
kurang bias daripada yang1+std::rand()%6
sebenarnya berlaku. Jadi saya menulis program tes untuk mentabulasi hasil untuk kedua metode (saya belum menulis C ++ dalam usia, silakan periksa). Tautan untuk menjalankan kode ditemukan di sini . Ini juga direproduksi sebagai berikut:Saya kemudian mengambil output ini dan menggunakan
chisq.test
fungsi di R untuk menjalankan uji Chi-square untuk melihat apakah hasilnya berbeda secara signifikan dari yang diharapkan. Pertanyaan stackexchange ini menjelaskan lebih rinci tentang penggunaan uji chi-square untuk menguji keadilan dadu: Bagaimana saya bisa menguji apakah dadu adil? . Berikut adalah hasil untuk beberapa kali lari:Dalam tiga proses yang saya lakukan, nilai p untuk kedua metode selalu lebih besar dari nilai alfa tipikal yang digunakan untuk menguji signifikansi (0,05). Ini berarti bahwa kami tidak akan menganggap salah satu dari mereka bias. Menariknya, metode yang seharusnya tidak bias memiliki nilai p yang lebih rendah secara konsisten, yang menunjukkan bahwa metode tersebut sebenarnya mungkin lebih bias. Peringatannya adalah bahwa saya hanya melakukan 3 kali lari.
PEMBARUAN: Saat saya menulis jawaban saya, Konrad Rudolph memposting jawaban yang mengambil pendekatan yang sama, tetapi mendapatkan hasil yang sangat berbeda. Saya tidak memiliki reputasi untuk mengomentari jawabannya, jadi saya akan membahasnya di sini. Pertama, hal utama adalah bahwa kode yang dia gunakan menggunakan seed yang sama untuk generator nomor acak setiap kali dijalankan. Jika Anda mengganti bibitnya, Anda justru mendapatkan hasil yang beragam. Kedua, jika Anda tidak mengganti benih, tetapi mengubah jumlah percobaan, Anda juga mendapatkan hasil yang beragam. Cobalah menambah atau mengurangi urutan besarnya untuk melihat apa yang saya maksud. Ketiga, ada beberapa pemotongan atau pembulatan integer yang terjadi di mana nilai yang diharapkan tidak cukup akurat. Mungkin tidak cukup untuk membuat perbedaan, tetapi itu ada.
Pada dasarnya, secara ringkas, dia kebetulan mendapatkan benih yang tepat dan jumlah percobaan yang mungkin dia dapatkan hasil yang salah.
sumber
rand()%6
denganrand()/(1+RAND_MAX)/6
. Sebaliknya, ini membandingkan pengambilan langsung sisa dengan pengambilan sampel penolakan (lihat jawaban lain untuk penjelasan). Akibatnya, kode kedua Anda salah (while
loop tidak melakukan apa pun). Pengujian statistik Anda juga memiliki masalah (Anda tidak bisa hanya menjalankan pengulangan pengujian Anda untuk ketahanan, Anda tidak melakukan koreksi,…).std::srand
(dan tidak ada penggunaan<random>
) cukup sulit dilakukan dengan cara yang sesuai standar dan saya tidak ingin kerumitannya mengurangi kode yang tersisa. Ini juga tidak relevan untuk kalkulasi: mengulangi urutan yang sama dalam simulasi sepenuhnya dapat diterima. Tentu saja benih yang berbeda akan memberikan hasil yang berbeda, dan beberapa tidak akan signifikan. Itu sepenuhnya diharapkan berdasarkan bagaimana nilai-p didefinisikan.std::rand
menghasilkan simulasi lemparan koin yang sangat bagus untuk d6, di seluruh kisaran benih acak.RAND_MAX
, yang menentukan ukuran efek dari bias modulo. Signifikansi statistik adalah probabilitas di bawah hipotesis nol bahwa Anda menolaknya secara salah. Apa kekuatan statistik - probabilitas di bawah hipotesis alternatif bahwa pengujian Anda dengan benar menolak hipotesis nol? Apakah Anda akan mendeteksirand() % 6
seperti ini ketika RAND_MAX = 2 ^ 31 - 1?Seseorang dapat menganggap generator bilangan acak bekerja pada aliran digit biner. Generator mengubah aliran menjadi angka dengan mengirisnya menjadi beberapa bagian. Jika
std:rand
fungsinya bekerja dengan aRAND_MAX
32767, maka itu menggunakan 15 bit di setiap irisan.Ketika seseorang mengambil modul angka antara 0 dan 32767 inklusif, orang menemukan bahwa 5462 '0 dan' 1 tetapi hanya 5461 '2,' 3, '4, dan' 5. Oleh karena itu, hasilnya bias. Semakin besar nilai RAND_MAX, semakin sedikit bias, tetapi itu tidak bisa dihindari.
Yang tidak bias adalah angka dalam rentang [0 .. (2 ^ n) -1]. Anda dapat menghasilkan angka (secara teoritis) yang lebih baik dalam kisaran 0..5 dengan mengekstrak 3 bit, mengubahnya menjadi integer dalam kisaran 0..7 dan menolak 6 dan 7.
Satu harapan bahwa setiap bit dalam aliran bit memiliki peluang yang sama untuk menjadi '0' atau '1' terlepas dari di mana ia berada dalam aliran atau nilai bit lainnya. Ini sangat sulit dalam praktiknya. Berbagai implementasi perangkat lunak PRNG menawarkan kompromi yang berbeda antara kecepatan dan kualitas. Generator kongruensial linier seperti
std::rand
menawarkan kecepatan tercepat untuk kualitas terendah. Generator kriptografi menawarkan kualitas tertinggi untuk kecepatan terendah.sumber