Mengapa orang mengatakan ada bias modulo ketika menggunakan generator nomor acak?

277

Saya telah melihat pertanyaan ini banyak ditanyakan tetapi tidak pernah melihat jawaban nyata yang konkret untuk itu. Jadi saya akan memposting satu di sini yang diharapkan akan membantu orang memahami mengapa sebenarnya ada "modulo bias" saat menggunakan generator angka acak, seperti rand()di C ++.

pengguna1413793
sumber

Jawaban:

394

Jadi rand()adalah generator bilangan pseudo-acak yang memilih bilangan alami antara 0 dan RAND_MAX, yang merupakan konstanta yang didefinisikan dalam cstdlib(lihat artikel ini untuk gambaran umum umum tentang rand()).

Sekarang apa yang terjadi jika Anda ingin menghasilkan angka acak antara katakan 0 dan 2? Demi penjelasan, katakanlah RAND_MAXadalah 10 dan saya memutuskan untuk menghasilkan angka acak antara 0 dan 2 dengan menelepon rand()%3. Namun, rand()%3tidak menghasilkan angka antara 0 dan 2 dengan probabilitas yang sama!

Ketika rand()mengembalikan 0, 3, 6, atau 9 rand()%3 == 0 ,. Oleh karena itu, P (0) = 4/11

Ketika rand()mengembalikan 1, 4, 7, atau 10 rand()%3 == 1 ,. Oleh karena itu, P (1) = 4/11

Ketika rand()mengembalikan 2, 5, atau 8 rand()%3 == 2 ,. Oleh karena itu, P (2) = 3/11

Ini tidak menghasilkan angka antara 0 dan 2 dengan probabilitas yang sama. Tentu saja untuk rentang kecil, ini mungkin bukan masalah terbesar tetapi untuk rentang yang lebih besar ini dapat membuat distribusinya bias, sehingga bias jumlahnya lebih kecil.

Jadi kapan rand()%nmengembalikan rentang angka dari 0 hingga n-1 dengan probabilitas yang sama? Ketika RAND_MAX%n == n - 1. Dalam kasus ini, bersama dengan asumsi kami sebelumnya rand()mengembalikan angka antara 0 dan RAND_MAXdengan probabilitas yang sama, kelas modulo dari n juga akan terdistribusi secara merata.

Jadi bagaimana kita mengatasi masalah ini? Cara kasar adalah terus menghasilkan angka acak hingga Anda mendapatkan nomor dalam rentang yang Anda inginkan:

int x; 
do {
    x = rand();
} while (x >= n);

tapi itu tidak efisien untuk nilai rendah n, karena Anda hanya memiliki n/RAND_MAXpeluang untuk mendapatkan nilai dalam rentang Anda, dan karenanya Anda harus melakukan RAND_MAX/npanggilan ke rand()rata-rata.

Pendekatan rumus yang lebih efisien adalah mengambil rentang besar dengan panjang dapat dibagi dengan n, seperti RAND_MAX - RAND_MAX % n, terus menghasilkan angka acak hingga Anda mendapatkan nomor yang terletak di kisaran, dan kemudian mengambil modulus:

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

Untuk nilai kecil n, ini jarang membutuhkan lebih dari satu panggilan rand().


Karya dikutip dan bacaan lebih lanjut:


pengguna1413793
sumber
6
Cara berpikir lain tentang RAND_MAX%n == n - 1_ adalah (RAND_MAX + 1) % n == 0. Saat membaca kode, saya cenderung memahami % something == 0"terbagi rata" lebih mudah daripada cara lain untuk menghitungnya. Tentu saja, jika stdlib C ++ Anda memiliki RAND_MAXnilai yang sama INT_MAX, (RAND_MAX + 1)tentu tidak akan berfungsi; jadi perhitungan Markus tetap merupakan implementasi teraman.
Slipp D. Thompson
jawaban yang sangat bagus!
Sayali Sonawane
Saya mungkin melakukan nitpicking, tetapi jika tujuannya adalah untuk mengurangi bit yang terbuang, kami dapat meningkatkan sedikit ini untuk kondisi tepi di mana RAND_MAX (RM) hanya 1 kurang dari yang sama-sama dapat dibagi oleh N. Dalam skenario ini, tidak ada bit yang perlu terbuang oleh melakukan X> = (RM - RM% N)) yang bernilai kecil untuk nilai kecil N, tetapi menjadi lebih besar untuk nilai besar N. Seperti disebutkan oleh Slipp D. Thompson, ada solusi yang hanya akan berfungsi ketika INT_MAX (IM)> RAND_MAX tetapi rusak ketika mereka sama. Namun, ada solusi sederhana untuk ini kita dapat mengubah perhitungan X> = (RM - RM% N) sebagai berikut:
Ben Personick
X> = RM - (((RM% N) + 1)% N)
Ben Personick
Saya memposting jawaban tambahan yang menjelaskan masalah secara rinci dan memberikan solusi kode contoh.
Ben Personick
36

Tetap memilih secara acak adalah cara yang baik untuk menghapus bias.

Memperbarui

Kami dapat membuat kode dengan cepat jika kami mencari rentang x yang dapat dibagi oleh n.

// Assumptions
// rand() in [0, RAND_MAX]
// n in (0, RAND_MAX]

int x; 

// Keep searching for an x in a range divisible by n 
do {
    x = rand();
} while (x >= RAND_MAX - (RAND_MAX % n)) 

x %= n;

Loop di atas harus sangat cepat, katakanlah 1 iterasi rata-rata.

Nick Dandoulakis
sumber
2
Yuck :-P mengonversi menjadi ganda, lalu mengalikannya dengan MAX_UPPER_LIMIT / RAND_MAX jauh lebih bersih dan berkinerja lebih baik.
boycy
22
@boycy: Anda sudah melewatkan intinya. Jika jumlah nilai yang rand()dapat kembali bukan kelipatan n, maka apa pun yang Anda lakukan, Anda pasti akan mendapatkan 'modulo bias', kecuali jika Anda membuang beberapa nilai tersebut. user1413793 menjelaskan hal itu dengan baik (meskipun solusi yang diajukan dalam jawaban itu benar-benar sial).
TonyK
4
@TonyK permintaan maaf saya, saya tidak mengerti intinya. Tidak berpikir cukup keras, dan berpikir bias hanya akan berlaku dengan metode menggunakan operasi modulus eksplisit. Terima kasih telah memperbaiki saya :-)
boycy
Diutamakan operator membuat RAND_MAX+1 - (RAND_MAX+1) % npekerjaan dengan benar, tapi saya masih berpikir itu harus ditulis RAND_MAX+1 - ((RAND_MAX+1) % n)untuk kejelasan.
Linus Arver
4
Ini tidak akan berfungsi jika RAND_MAX == INT_MAX (seperti halnya pada kebanyakan sistem) . Lihat komentar kedua saya ke @ user1413793 di atas.
BlueRaja - Danny Pflughoeft
19

@ user1413793 benar tentang masalah ini. Saya tidak akan membahas lebih lanjut, kecuali untuk membuat satu poin: ya, untuk nilai kecil ndan nilai besar RAND_MAX, bias modulo bisa sangat kecil. Tetapi menggunakan pola yang menginduksi bias berarti bahwa Anda harus mempertimbangkan bias setiap kali Anda menghitung angka acak dan memilih pola yang berbeda untuk kasus yang berbeda. Dan jika Anda membuat pilihan yang salah, bug yang diperkenalkannya halus dan hampir tidak mungkin untuk diuji unit. Dibandingkan dengan hanya menggunakan alat yang tepat (seperti arc4random_uniform), itu pekerjaan ekstra, bukan kerja lebih sedikit. Melakukan lebih banyak pekerjaan dan mendapatkan solusi yang lebih buruk adalah rekayasa yang buruk, terutama bila melakukannya dengan benar setiap saat itu mudah di sebagian besar platform.

Sayangnya, implementasi dari solusi semuanya salah atau kurang efisien dari yang seharusnya. (Setiap solusi memiliki berbagai komentar yang menjelaskan masalah, tetapi tidak ada solusi yang diperbaiki untuk mengatasinya.) Ini mungkin membingungkan pencari jawaban biasa, jadi saya memberikan implementasi yang dikenal baik di sini.

Sekali lagi, solusi terbaik hanya digunakan arc4random_uniformpada platform yang menyediakannya, atau solusi jarak yang serupa untuk platform Anda (seperti Random.nextIntdi Jawa). Ini akan melakukan hal yang benar tanpa biaya kode untuk Anda. Ini hampir selalu merupakan panggilan yang tepat untuk dilakukan.

Jika Anda tidak memilikinya arc4random_uniform, maka Anda dapat menggunakan kekuatan opensource untuk melihat bagaimana penerapannya di atas RNG dengan rentang yang lebih luas ( ar4randomdalam hal ini, tetapi pendekatan yang serupa juga dapat bekerja di atas RNG lain).

Berikut ini adalah implementasi OpenBSD :

/*
 * Calculate a uniformly distributed random number less than upper_bound
 * avoiding "modulo bias".
 *
 * Uniformity is achieved by generating new random numbers until the one
 * returned is outside the range [0, 2**32 % upper_bound).  This
 * guarantees the selected random number will be inside
 * [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
 * after reduction modulo upper_bound.
 */
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
    u_int32_t r, min;

    if (upper_bound < 2)
        return 0;

    /* 2**32 % x == (2**32 - x) % x */
    min = -upper_bound % upper_bound;

    /*
     * This could theoretically loop forever but each retry has
     * p > 0.5 (worst case, usually far better) of selecting a
     * number inside the range we need, so it should rarely need
     * to re-roll.
     */
    for (;;) {
        r = arc4random();
        if (r >= min)
            break;
    }

    return r % upper_bound;
}

Patut dicatat komentar komit terbaru tentang kode ini untuk mereka yang perlu menerapkan hal-hal serupa:

Ubah arc4random_uniform () untuk menghitung 2**32 % upper_boundsebagai -upper_bound % upper_bound. Menyederhanakan kode dan membuatnya sama di kedua arsitektur ILP32 dan LP64, dan juga sedikit lebih cepat pada arsitektur LP64 dengan menggunakan sisa 32-bit alih-alih sisa 64-bit.

Ditunjukkan oleh Jorden Verwer di tech @ ok deraadt; tidak ada keberatan dari djm atau otto

Implementasi Java juga mudah ditemukan (lihat tautan sebelumnya):

public int nextInt(int n) {
   if (n <= 0)
     throw new IllegalArgumentException("n must be positive");

   if ((n & -n) == n)  // i.e., n is a power of 2
     return (int)((n * (long)next(31)) >> 31);

   int bits, val;
   do {
       bits = next(31);
       val = bits % n;
   } while (bits - val + (n-1) < 0);
   return val;
 }
Rob Napier
sumber
Perhatikan bahwa jika arcfour_random() benar-benar menggunakan algoritma RC4 nyata dalam implementasinya, output pasti akan memiliki beberapa bias. Semoga penulis perpustakaan Anda telah beralih menggunakan CSPRNG yang lebih baik di belakang antarmuka yang sama. Saya ingat salah satu BSD sekarang benar-benar menggunakan algoritma ChaCha20 untuk diimplementasikan arcfour_random(). Lebih lanjut tentang bias keluaran RC4 yang menjadikannya tidak berguna untuk keamanan atau aplikasi penting lainnya seperti video poker: blog.cryptographyengineering.com/2013/03/…
rmalayter
2
@rmalayter Di iOS dan OS X, arc4random membaca dari / dev / random yang merupakan entropi kualitas tertinggi dalam sistem. ("Arc4" dalam nama itu bersejarah dan dipertahankan untuk kompatibilitas.)
Rob Napier
@Rob_Napier senang mengetahui, tetapi /dev/randomjuga telah menggunakan RC4 pada beberapa platform di masa lalu (Linux menggunakan SHA-1 dalam mode penghitung). Sayangnya halaman manual yang saya temukan melalui pencarian menunjukkan bahwa RC4 masih digunakan pada berbagai platform yang menawarkan arc4random(meskipun kode sebenarnya mungkin berbeda).
rmalayter
1
Saya bingung. Bukan -upper_bound % upper_bound == 0??
Jon McClung
1
@JonMcClung -upper_bound % upper_boundmemang akan 0 jika intlebih lebar dari 32-bit. Seharusnya (u_int32_t)-upper_bound % upper_bound)(dengan asumsi u_int32_tBSD-isme for uint32_t).
Ian Abbott
14

Definisi

Modulo Bias adalah bias inheren dalam menggunakan modulo aritmatika untuk mengurangi set output ke subset dari set input. Secara umum, ada bias setiap kali pemetaan antara input dan output set tidak terdistribusi secara merata, seperti dalam kasus menggunakan modulo aritmatika ketika ukuran set output bukan merupakan pembagi dari ukuran set input.

Bias ini sangat sulit untuk dihindari dalam komputasi, di mana angka direpresentasikan sebagai string bit: 0s dan 1s. Menemukan sumber acak yang benar-benar acak juga sangat sulit, tetapi berada di luar cakupan diskusi ini. Untuk sisa jawaban ini, asumsikan bahwa ada sumber tak terbatas bit benar-benar acak.

Contoh soal

Mari pertimbangkan untuk mensimulasi die roll (0 hingga 5) menggunakan bit acak ini. Ada 6 kemungkinan, jadi kita perlu bit yang cukup untuk mewakili angka 6, yaitu 3 bit. Sayangnya, 3 bit acak menghasilkan 8 hasil yang mungkin:

000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7

Kita dapat mengurangi ukuran hasil yang ditetapkan menjadi tepat 6 dengan mengambil nilai modulo 6, namun ini menyajikan masalah bias modulo : 110menghasilkan 0, dan 111menghasilkan 1. Dadu ini dimuat.

Solusi Potensial

Mendekati 0:

Daripada mengandalkan bit acak, secara teori seseorang bisa menyewa pasukan kecil untuk melempar dadu sepanjang hari dan mencatat hasilnya dalam database, dan kemudian menggunakan setiap hasil hanya sekali. Ini praktis seperti kedengarannya, dan kemungkinan besar tidak akan menghasilkan hasil yang benar-benar acak (pun intended).

Pendekatan 1:

Alih-alih menggunakan modulus, solusi yang naif tetapi benar secara matematis adalah membuang hasil yang menghasilkan 110dan 111dan hanya coba lagi dengan 3 bit baru. Sayangnya, ini berarti ada kemungkinan 25% pada setiap roll yang diperlukan untuk re-roll, termasuk masing-masing roll-re itu sendiri. Ini jelas tidak praktis untuk semua kecuali penggunaan yang paling sepele.

Pendekatan 2:

Gunakan lebih banyak bit: alih-alih 3 bit, gunakan 4. Ini menghasilkan 16 hasil yang mungkin. Tentu saja, bergulir kembali kapan saja hasilnya lebih besar dari 5 membuat segalanya lebih buruk (10/16 = 62,5%) sehingga itu saja tidak akan membantu.

Perhatikan bahwa 2 * 6 = 12 <16, sehingga kita dapat dengan aman mengambil hasil apa pun yang kurang dari 12 dan mengurangi modulo 6 tersebut untuk mendistribusikan hasil secara merata. 4 hasil lainnya harus dibuang, dan kemudian digulung kembali seperti pada pendekatan sebelumnya.

Kedengarannya bagus pada awalnya, tapi mari kita periksa matematika:

4 discarded results / 16 possibilities = 25%

Dalam hal ini, 1 bit ekstra tidak membantu sama sekali!

Hasil itu sangat disayangkan, tetapi mari kita coba lagi dengan 5 bit:

32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%

Perbaikan yang pasti, tetapi tidak cukup baik dalam banyak kasus praktis. Berita baiknya adalah, menambahkan lebih banyak bit tidak akan meningkatkan peluang untuk membuang dan memutar kembali . Ini berlaku tidak hanya untuk dadu, tetapi dalam semua kasus.

Namun seperti yang ditunjukkan , menambahkan 1 bit ekstra mungkin tidak mengubah apa pun. Bahkan jika kita meningkatkan roll kita menjadi 6 bit, probabilitasnya tetap 6,25%.

Ini menimbulkan 2 pertanyaan tambahan:

  1. Jika kami menambahkan bit yang cukup, apakah ada jaminan bahwa kemungkinan sebuah pembuangan akan berkurang?
  2. Berapa banyak bit yang cukup dalam kasus umum?

Solusi Umum

Untungnya jawaban untuk pertanyaan pertama adalah ya. Masalah dengan 6 adalah bahwa 2 ^ x mod 6 membalik antara 2 dan 4 yang kebetulan merupakan kelipatan dari 2 satu sama lain, sehingga untuk genap x> 1,

[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)

Jadi 6 adalah pengecualian daripada aturan. Dimungkinkan untuk menemukan moduli yang lebih besar yang menghasilkan kekuatan 2 berurutan dengan cara yang sama, tetapi pada akhirnya ini harus membungkus, dan kemungkinan pembuangan akan berkurang.

Tanpa menawarkan bukti lebih lanjut, secara umum menggunakan dua kali lipat jumlah bit yang diperlukan akan memberikan peluang yang lebih kecil, biasanya tidak signifikan, untuk dibuang.

Bukti dari konsep

Berikut adalah contoh program yang menggunakan libcrypo OpenSSL untuk memasok byte acak. Saat mengkompilasi, pastikan untuk menautkan ke perpustakaan -lcryptoyang tersedia bagi kebanyakan orang.

#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/rand.h>

volatile uint32_t dummy;
uint64_t discardCount;

uint32_t uniformRandomUint32(uint32_t upperBound)
{
    assert(RAND_status() == 1);
    uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
    uint64_t randomPool = RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));

    while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
        RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
        ++discardCount;
    }

    return randomPool % upperBound;
}

int main() {
    discardCount = 0;

    const uint32_t MODULUS = (1ul << 31)-1;
    const uint32_t ROLLS = 10000000;

    for(uint32_t i = 0; i < ROLLS; ++i) {
        dummy = uniformRandomUint32(MODULUS);
    }
    std::cout << "Discard count = " << discardCount << std::endl;
}

Saya mendorong bermain dengan MODULUSdan ROLLSmenghargai untuk melihat berapa banyak re-roll sebenarnya terjadi di sebagian besar kondisi. Orang yang skeptis juga mungkin ingin menyimpan nilai yang dihitung untuk mengajukan dan memverifikasi distribusi tampak normal.

Jim Wood
sumber
Saya benar-benar berharap tidak ada yang membabi buta meniru implementasi acak seragam Anda. The randomPool = RAND_bytes(...)line akan selalu menghasilkan randomPool == 1akibat pernyataan. Ini selalu menghasilkan discard dan roll ulang. Saya pikir Anda ingin mendeklarasikan pada jalur yang berbeda. Akibatnya, ini menyebabkan RNG kembali dengan 1untuk setiap iterasi.
Qix - MONICA DISALAHKAN
Supaya jelas, randomPoolakan selalu dievaluasi 1sesuai dengan dokumentasiRAND_bytes() OpenSSL untuk karena itu akan selalu berhasil berkat RAND_status()pernyataan itu.
Qix - MONICA DISALAHKAN
9

Ada dua keluhan biasa dengan penggunaan modulo.

  • satu berlaku untuk semua generator. Lebih mudah dilihat dalam batasan kasus. Jika generator Anda memiliki RAND_MAX yang 2 (yang tidak sesuai dengan standar C) dan Anda hanya menginginkan 0 atau 1 sebagai nilai, menggunakan modulo akan menghasilkan 0 dua kali lebih sering (ketika generator menghasilkan 0 dan 2) karena akan menghasilkan 1 (ketika generator menghasilkan 1). Perhatikan bahwa ini benar begitu Anda tidak menjatuhkan nilai, apa pun pemetaan yang Anda gunakan dari nilai generator ke yang diinginkan, yang satu akan terjadi dua kali lebih sering daripada yang lain.

  • beberapa jenis generator memiliki bit kurang signifikan kurang acak daripada yang lain, setidaknya untuk beberapa parameter mereka, tetapi sayangnya parameter tersebut memiliki karakteristik menarik lainnya (seperti telah mampu memiliki RAND_MAX satu kurang dari kekuatan 2). Masalahnya sudah diketahui dan untuk waktu yang lama implementasi perpustakaan mungkin menghindari masalah (misalnya implementasi sampel rand () dalam standar C menggunakan generator jenis ini, tetapi menjatuhkan 16 bit yang kurang signifikan), tetapi beberapa suka mengeluh tentang itu dan Anda mungkin memiliki nasib buruk

Menggunakan sesuatu seperti

int alea(int n){ 
 assert (0 < n && n <= RAND_MAX); 
 int partSize = 
      n == RAND_MAX ? 1 : 1 + (RAND_MAX-n)/(n+1); 
 int maxUsefull = partSize * n + (partSize-1); 
 int draw; 
 do { 
   draw = rand(); 
 } while (draw > maxUsefull); 
 return draw/partSize; 
}

untuk menghasilkan angka acak antara 0 dan n akan menghindari kedua masalah (dan itu menghindari overflow dengan RAND_MAX == INT_MAX)

BTW, C ++ 11 memperkenalkan cara standar untuk reduksi dan generator selain rand ().

Pemrogram
sumber
n == RAND_MAX? 1: (RAND_MAX-1) / (n + 1): Saya mengerti idenya di sini adalah untuk pertama-tama membagi RAND_MAX menjadi ukuran halaman N yang sama, kemudian mengembalikan penyimpangan dalam N, tetapi saya tidak dapat memetakan kode ini dengan tepat.
zinking
1
Versi naif harus (RAND_MAX + 1) / (n + 1) karena ada nilai RAND_MAX + 1 untuk dibagi dalam ember n + 1. Jika ingin menghindari overflow saat menghitung RAND_MAX + 1, itu dapat diubah dalam 1+ (RAND_MAX-n) / (n +1). Untuk menghindari overflow saat menghitung n + 1, case n == RAND_MAX pertama kali diperiksa.
Pemrogram
+ plus, melakukan pembagian tampaknya lebih mahal bahkan dibandingkan dengan angka regenerasi.
zinking
4
Mengambil modulo dan membagi memiliki biaya yang sama. Beberapa ISA bahkan hanya menyediakan satu instruksi yang menyediakan keduanya. Biaya pembuatan ulang nomor akan tergantung pada n dan RAND_MAX. Jika n kecil sehubungan dengan RAND_MAX, mungkin harganya banyak. Dan jelas Anda dapat memutuskan bahwa bias tidak penting untuk aplikasi Anda; Saya hanya memberi cara untuk menghindarinya.
Pemrogram
9

Solusi Mark (Solusi yang diterima) Hampir Sempurna.

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

diedit 25 Maret 16 jam 23:16

Mark Amery 39k21170211

Namun, ia memiliki peringatan yang membuang 1 set hasil yang valid dalam setiap skenario di mana RAND_MAX( RM) adalah 1 kurang dari kelipatan N(Di mana N= Jumlah hasil yang mungkin valid).

yaitu, ketika 'jumlah nilai yang dibuang' ( D) sama dengan N, maka mereka sebenarnya adalah set yang valid ( V), bukan set yang tidak valid ( I).

Apa yang menyebabkan ini pada beberapa titik Mark kehilangan pandangan tentang perbedaan antara Ndan Rand_Max.

Nadalah himpunan yang anggotanya valid hanya terdiri dari Bilangan Bulat Positif, karena berisi hitungan tanggapan yang akan valid. (mis .: Set N= {1, 2, 3, ... n })

Rand_max Namun adalah himpunan yang (sebagaimana didefinisikan untuk tujuan kami) termasuk sejumlah bilangan bulat non-negatif.

Dalam bentuk yang paling umum, apa yang didefinisikan di sini Rand Maxadalah Himpunan semua hasil yang valid, yang secara teoritis dapat mencakup angka negatif atau nilai-nilai non-numerik.

Oleh karena Rand_Maxitu lebih baik didefinisikan sebagai set "Kemungkinan Tanggapan".

Namun Nberoperasi terhadap penghitungan nilai dalam set tanggapan yang valid, sehingga meskipun seperti yang didefinisikan dalam kasus khusus kami, Rand_Maxakan menjadi nilai yang kurang dari jumlah total yang dikandungnya.

Menggunakan Mark's Solution, Nilai Dihapus ketika: X => RM - RM% N

EG: 

Ran Max Value (RM) = 255
Valid Outcome (N) = 4

When X => 252, Discarded values for X are: 252, 253, 254, 255

So, if Random Value Selected (X) = {252, 253, 254, 255}

Number of discarded Values (I) = RM % N + 1 == N

 IE:

 I = RM % N + 1
 I = 255 % 4 + 1
 I = 3 + 1
 I = 4

   X => ( RM - RM % N )
 255 => (255 - 255 % 4) 
 255 => (255 - 3)
 255 => (252)

 Discard Returns $True

Seperti yang Anda lihat dalam contoh di atas, ketika nilai X (angka acak yang kita dapatkan dari fungsi awal) adalah 252, 253, 254, atau 255 kita akan membuangnya meskipun keempat nilai ini terdiri dari sekumpulan nilai yang dikembalikan. .

IE: Ketika penghitungan nilai Diabaikan (I) = N (Jumlah hasil yang valid) maka seperangkat nilai pengembalian yang valid akan dibuang oleh fungsi asli.

Jika kita menggambarkan perbedaan antara nilai N dan RM sebagai D, yaitu:

D = (RM - N)

Kemudian ketika nilai D menjadi lebih kecil, Persentase roll-ulang yang tidak dibutuhkan karena metode ini meningkat pada setiap multiplikatif alami. (Ketika RAND_MAX TIDAK sama dengan Nomor Perdana, ini menjadi perhatian yang sah)

MISALNYA:

RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%

RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%

Karena persentase Rerolls yang dibutuhkan meningkat semakin dekat N datang ke RM, ini dapat menjadi perhatian yang valid pada banyak nilai yang berbeda tergantung pada kendala sistem yang menjalankan kode dan nilai-nilai yang dicari.

Untuk meniadakan hal ini, kita dapat membuat amandemen sederhana.

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

 x %= n;

Ini memberikan versi formula yang lebih umum yang menjelaskan keanehan tambahan menggunakan modulus untuk menentukan nilai maksimal Anda.

Contoh menggunakan nilai kecil untuk RAND_MAX yang merupakan multiplikasi dari N.

Versi Asli:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (RAND_MAX - ( RAND_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.

Versi Umum 1:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (RAND_MAX - ( ( RAND_MAX % n  ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set RAND_MAX so there will be no discard.

Selain itu, dalam hal N harus menjadi jumlah nilai dalam RAND_MAX; dalam hal ini, Anda dapat mengatur N = RAND_MAX +1, kecuali RAND_MAX = INT_MAX.

Namun, Anda hanya bisa menggunakan N = 1, dan nilai X apa pun akan diterima, dan masukkan pernyataan IF untuk pengali akhir Anda. Tetapi mungkin Anda memiliki kode yang mungkin memiliki alasan yang valid untuk mengembalikan 1 ketika fungsi dipanggil dengan n = 1 ...

Jadi mungkin lebih baik menggunakan 0, yang biasanya memberikan Div 0 Error, ketika Anda ingin memiliki n = RAND_MAX + 1

Versi Umum 2:

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = rand();
}

Kedua solusi ini menyelesaikan masalah dengan hasil valid yang tidak perlu dibuang yang akan terjadi ketika RM + 1 adalah produk dari n.

Versi kedua juga mencakup skenario tepi ketika Anda perlu n untuk menyamakan total set nilai yang mungkin terkandung dalam RAND_MAX.

Pendekatan yang dimodifikasi pada keduanya sama dan memungkinkan solusi yang lebih umum untuk kebutuhan menyediakan angka acak yang valid dan meminimalkan nilai yang dibuang.

Untuk mengulangi:

Solusi Umum Dasar yang mencakup contoh tanda:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );

 x %= n;

Solusi Umum yang Diperpanjang yang Memungkinkan satu skenario tambahan RAND_MAX + 1 = n:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );

    x %= n;
} else {
    x = rand();
}

Dalam beberapa bahasa (terutama bahasa yang ditafsirkan) melakukan perhitungan operasi perbandingan di luar kondisi sementara dapat menyebabkan hasil yang lebih cepat karena ini adalah perhitungan satu kali tidak peduli berapa banyak percobaan ulang diperlukan. YMMV!

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x; // Resulting random number
int y; // One-time calculation of the compare value for x

if n != 0 {
    y = RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) 
    do {
        x = rand();
    } while (x > y);

    x %= n;
} else {
    x = rand();
}
Ben Personick
sumber
Tidakkah aman untuk mengatakan bahwa masalah dengan solusi Mark adalah bahwa ia memperlakukan RAND_MAX dan n sebagai "satuan ukuran" yang sama padahal sebenarnya artinya dua hal yang berbeda? Sementara n mewakili "jumlah kemungkinan" yang dihasilkan, RAND_MAX hanya mewakili nilai maks dari kemungkinan awal, di mana RAND_MAX + 1 akan menjadi jumlah kemungkinan semula. Saya terkejut dia tidak sampai pada kesimpulan Anda karena ia tampaknya mengakui n dan RAND_MAX tidak sama dengan persamaan:RAND_MAX%n = n - 1
Danilo Souza Morães
@ DaniloSouzaMorães Terima kasih Danilo, Anda telah menjelaskan masalahnya dengan singkat. Saya pergi untuk menunjukkan apa yang dia lakukan bersama dengan Mengapa dan bagaimana, tapi jangan berpikir saya bisa menyatakan APA yang dia lakukan dengan fasih, karena saya begitu larut dalam rincian logika tentang bagaimana dan mengapa ada masalah, bahwa saya tidak menyatakan dengan jelas apa yang dipermasalahkan. Apakah Anda keberatan jika saya mengubah Jawaban saya untuk menggunakan sebagian dari apa yang Anda tulis di sini sebagai ringkasan saya sendiri untuk masalah apa dan di mana solusi yang diterima melakukan apa yang perlu ditangani di dekat bagian atas?
Ben Personick
Itu akan luar biasa. Lakukanlah
Danilo Souza Morães
1

Dengan RAND_MAXnilai 3(pada kenyataannya seharusnya jauh lebih tinggi dari itu tetapi bias masih ada) masuk akal dari perhitungan ini bahwa ada bias:

1 % 2 = 1 2 % 2 = 0 3 % 2 = 1 random_between(1, 3) % 2 = more likely a 1

Dalam hal ini, itu % 2adalah apa yang tidak boleh Anda lakukan ketika Anda ingin nomor acak antara 0dan 1. Anda bisa mendapatkan angka acak antara 0dan 2dengan melakukan % 3, karena dalam kasus ini: RAND_MAXadalah kelipatan 3.

Metode lain

Ada jauh lebih sederhana tetapi untuk menambahkan jawaban lain, berikut adalah solusi saya untuk mendapatkan angka acak antara 0dan n - 1, nkemungkinan yang sangat berbeda, tanpa bias.

  • jumlah bit (bukan byte) yang diperlukan untuk menyandikan jumlah kemungkinan adalah jumlah bit data acak yang Anda perlukan
  • menyandikan nomor dari bit acak
  • jika nomor ini >= n, restart (tidak ada modulo).

Benar-benar data acak tidak mudah diperoleh, jadi mengapa menggunakan bit lebih banyak dari yang dibutuhkan.

Di bawah ini adalah contoh dalam Smalltalk, menggunakan cache bit dari generator nomor pseudo-acak. Saya bukan ahli keamanan jadi gunakan dengan risiko Anda sendiri.

next: n

    | bitSize r from to |
    n < 0 ifTrue: [^0 - (self next: 0 - n)].
    n = 0 ifTrue: [^nil].
    n = 1 ifTrue: [^0].
    cache isNil ifTrue: [cache := OrderedCollection new].
    cache size < (self randmax highBit) ifTrue: [
        Security.DSSRandom default next asByteArray do: [ :byte |
            (1 to: 8) do: [ :i |    cache add: (byte bitAt: i)]
        ]
    ].
    r := 0.
    bitSize := n highBit.
    to := cache size.
    from := to - bitSize + 1.
    (from to: to) do: [ :i |
        r := r bitAt: i - from + 1 put: (cache at: i)
    ].
    cache removeFrom: from to: to.
    r >= n ifTrue: [^self next: n].
    ^r
Rivenfall
sumber
-1

Sebagai jawaban yang diterima menunjukkan, "modulo bias" berakar pada nilai rendah dari RAND_MAX. Dia menggunakan nilai yang sangat kecil RAND_MAX(10) untuk menunjukkan bahwa jika RAND_MAX adalah 10, maka Anda mencoba menghasilkan angka antara 0 dan 2 menggunakan%, hasil berikut akan menghasilkan:

rand() % 3   // if RAND_MAX were only 10, gives
output of rand()   |   rand()%3
0                  |   0
1                  |   1
2                  |   2
3                  |   0
4                  |   1
5                  |   2
6                  |   0
7                  |   1
8                  |   2
9                  |   0

Jadi ada 4 output 0 (peluang 4/10) dan hanya 3 output 1 dan 2 (peluang 3/10).

Jadi itu bias. Angka yang lebih rendah memiliki peluang lebih baik untuk keluar.

Tapi itu hanya muncul begitu jelas ketika RAND_MAXkecil . Atau lebih khusus, ketika jumlah yang Anda modding lebih besar dibandingkanRAND_MAX.

Solusi yang jauh lebih baik daripada perulangan (yang sangat tidak efisien dan bahkan tidak disarankan) adalah menggunakan PRNG dengan rentang keluaran yang jauh lebih besar. The Mersenne Twister algoritma memiliki output maksimum 4294967295. Dengan demikian melakukan MersenneTwister::genrand_int32() % 10semua maksud dan tujuan, akan terdistribusi secara merata dan efek bias modulo akan hilang sama sekali.

bobobobo
sumber
3
Milik Anda lebih efisien dan mungkin benar bahwa jika RAND_MAX secara signifikan lebih besar dari jumlah yang Anda modding, namun Anda masih bias. Memang ini semua generator nomor acak semu dan bahwa itu adalah topik yang berbeda tetapi jika Anda menganggap generator nomor acak sepenuhnya, cara Anda masih bias nilai-nilai yang lebih rendah.
user1413793
Karena nilai tertinggi adalah ganjil, MT::genrand_int32()%2pilih 0 (50 + 2.3e-8)% dari waktu dan 1 (50 - 2.3e-8)% dari waktu. Kecuali jika Anda membangun RGN kasino (yang mungkin akan Anda gunakan dengan rentang RGN yang jauh lebih besar), pengguna mana pun tidak akan melihat 2.3e-8% ekstra waktu. Anda sedang berbicara tentang angka terlalu kecil untuk menjadi masalah di sini.
bobobobo
7
Looping adalah solusi terbaik. Ini bukan "sangat tidak efisien"; membutuhkan kurang dari dua kali iterasi dalam kasus rata-rata terburuk. Menggunakan RAND_MAXnilai tinggi akan mengurangi bias modulo, tetapi tidak menghilangkannya. Kehancuran akan.
Jared Nielsen
5
Jika RAND_MAXcukup besar dari jumlah yang Anda pilih, jumlah waktu yang Anda butuhkan untuk membuat ulang nomor acak semakin kecil dan tidak akan mempengaruhi efisiensi. Saya katakan terus mengulang, selama Anda menguji terhadap kelipatan terbesar ndaripada hanya nseperti yang diusulkan oleh jawaban yang diterima.
Mark Ransom
-3

Saya baru saja menulis kode untuk Metode Unlimited Coin Flip Von Neumann, yang secara teoritis harus menghilangkan bias dalam proses pembuatan angka acak. Info lebih lanjut dapat ditemukan di ( http://en.wikipedia.org/wiki/Fair_coin )

int unbiased_random_bit() {    
    int x1, x2, prev;
    prev = 2;
    x1 = rand() % 2;
    x2 = rand() % 2;

    for (;; x1 = rand() % 2, x2 = rand() % 2)
    {
        if (x1 ^ x2)      // 01 -> 1, or 10 -> 0.
        {
            return x2;        
        }
        else if (x1 & x2)
        {
            if (!prev)    // 0011
                return 1;
            else
                prev = 1; // 1111 -> continue, bias unresolved
        }
        else
        {
            if (prev == 1)// 1100
                return 0;
            else          // 0000 -> continue, bias unresolved
                prev = 0;
        }
    }
}
Yavuz Koroglu
sumber
Ini tidak membahas bias modulo. Proses ini dapat digunakan untuk menghilangkan bias dalam aliran bit. Namun, untuk mendapatkan dari aliran bit ke distribusi genap dari 0 ke n di mana n tidak satu kurang dari kekuatan dua membutuhkan pengalamatan modulo bias. Dengan demikian solusi ini tidak dapat menghilangkan bias dalam proses pembuatan bilangan acak.
Rick
2
@Rick hmm. Perpanjangan logis metode Von Neumann untuk menghilangkan bias modulo ketika menghasilkan angka acak antara, katakanlah, 1 dan 100, akan menjadi: A) panggilan rand() % 100100 kali. B) jika semua hasilnya berbeda, ambil yang pertama. C) kalau tidak, GOTO A. Ini akan berfungsi, tetapi dengan jumlah iterasi yang diharapkan sekitar 10 ^ 42, Anda harus cukup sabar. Dan abadi.
Mark Amery
@MarkAmery Memang itu seharusnya bekerja. Melihat algoritma ini meskipun tidak diimplementasikan dengan benar. Yang pertama harus:else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
Rick