Tại sao mọi người nói có sự sai lệch modulo khi sử dụng một trình tạo số ngẫu nhiên?


277

Tôi đã thấy câu hỏi này hỏi rất nhiều nhưng chưa bao giờ thấy câu trả lời cụ thể thực sự cho nó. Vì vậy, tôi sẽ đăng một bài ở đây, hy vọng sẽ giúp mọi người hiểu tại sao chính xác có "độ lệch modulo" khi sử dụng trình tạo số ngẫu nhiên, như rand()trong C ++.

Câu trả lời:


394

Vì vậy, rand()một trình tạo số giả ngẫu nhiên chọn một số tự nhiên trong khoảng từ 0 đến RAND_MAXmột hằng số được xác định trong cstdlib(xem bài viết này để biết tổng quan chung về rand()).

Bây giờ điều gì xảy ra nếu bạn muốn tạo một số ngẫu nhiên giữa nói 0 và 2? Để giải thích, giả sử RAND_MAXlà 10 và tôi quyết định tạo một số ngẫu nhiên trong khoảng từ 0 đến 2 bằng cách gọi rand()%3. Tuy nhiên, rand()%3không tạo ra các số từ 0 đến 2 với xác suất bằng nhau!

Khi rand()trả về 0, 3, 6 hoặc 9 , rand()%3 == 0 . Do đó, P (0) = 4/11

Khi rand()trả về 1, 4, 7 hoặc 10 , rand()%3 == 1 . Do đó, P (1) = 4/11

Khi rand()trả về 2, 5 hoặc 8 , rand()%3 == 2 . Do đó, P (2) = 3/11

Điều này không tạo ra các số từ 0 đến 2 với xác suất bằng nhau. Tất nhiên đối với phạm vi nhỏ, điều này có thể không phải là vấn đề lớn nhất nhưng đối với phạm vi lớn hơn, điều này có thể làm lệch phân phối, sai lệch các số nhỏ hơn.

Vậy khi nào rand()%ntrả về một phạm vi số từ 0 đến n-1 với xác suất bằng nhau? Khi RAND_MAX%n == n - 1. Trong trường hợp này, cùng với giả định trước đó của chúng tôi rand()trả về một số từ 0 đến RAND_MAXvới xác suất bằng nhau, các lớp modulo của n cũng sẽ được phân phối bằng nhau.

Vậy làm thế nào để chúng ta giải quyết vấn đề này? Một cách thô sơ là tiếp tục tạo số ngẫu nhiên cho đến khi bạn nhận được một số trong phạm vi mong muốn của mình:

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

nhưng điều đó không hiệu quả đối với các giá trị thấp n, vì bạn chỉ có n/RAND_MAXcơ hội nhận được một giá trị trong phạm vi của mình và do đó, bạn sẽ cần thực hiện RAND_MAX/ncác cuộc gọi rand()trung bình.

Một cách tiếp cận công thức hiệu quả hơn sẽ là lấy một phạm vi lớn với độ dài chia hết cho n, như RAND_MAX - RAND_MAX % n, tiếp tục tạo các số ngẫu nhiên cho đến khi bạn có được một số nằm trong phạm vi, và sau đó lấy mô-đun:

int x;

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

x %= n;

Đối với các giá trị nhỏ của n, điều này sẽ hiếm khi yêu cầu nhiều hơn một cuộc gọi đến rand().


Tác phẩm được trích dẫn và đọc thêm:



6
Một cách nghĩ khác về_ RAND_MAX%n == n - 1_ là (RAND_MAX + 1) % n == 0. Khi đọc mã, tôi có xu hướng hiểu % something == 0là cách chia đều dễ dàng hơn so với các cách tính toán khác. Tất nhiên, nếu stdlib C ++ của bạn có RAND_MAXcùng giá trị như INT_MAX, (RAND_MAX + 1)chắc chắn sẽ không hoạt động; vì vậy tính toán của Mark vẫn là cách thực hiện an toàn nhất.
Slipp D. Thompson

câu trả lời rất hay
Sayali Sonawane

Tôi có thể bị nitpicking, nhưng nếu mục tiêu là giảm các bit bị lãng phí, chúng ta có thể cải thiện điều này một chút cho điều kiện cạnh trong đó RAND_MAX (RM) chỉ bằng 1 so với chia hết cho N. Trong kịch bản này, không có bit nào bị lãng phí làm X> = (RM - RM% N)) ít có giá trị cho các giá trị nhỏ của N, nhưng trở thành giá trị lớn hơn cho các giá trị lớn của N. Như Slipp D. Thompson đã đề cập, có một giải pháp sẽ chỉ hoạt động khi INT_MAX (IM)> RAND_MAX nhưng bị phá vỡ khi chúng bằng nhau. Tuy nhiên, có một giải pháp đơn giản cho việc này, chúng tôi có thể sửa đổi phép tính X> = (RM - RM% N) như sau:
Ben Personick

X> = RM - (((RM% N) + 1)% N)
Ben Personick

Tôi đã đăng một câu trả lời bổ sung giải thích vấn đề một cách chi tiết và đưa ra giải pháp mã ví dụ.
Ben Personick

36

Tiếp tục chọn ngẫu nhiên là một cách tốt để loại bỏ sự thiên vị.

Cập nhật

Chúng ta có thể làm cho mã nhanh nếu chúng ta tìm kiếm một x trong phạm vi chia hết cho 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;

Vòng lặp trên phải rất nhanh, trung bình 1 lần lặp.


2
Yuck :-P chuyển đổi thành gấp đôi, sau đó nhân với MAX_UPPER_LIMIT / RAND_MAX sạch hơn nhiều và hoạt động tốt hơn.
boycy

22
@boycy: bạn đã bỏ lỡ điểm. Nếu số lượng giá trị rand()có thể trả về không phải là bội số n, thì bất cứ điều gì bạn làm, chắc chắn bạn sẽ nhận được "độ lệch modulo", trừ khi bạn loại bỏ một số giá trị đó. user1413793 giải thích điều đó một cách độc đáo (mặc dù giải pháp được đề xuất trong câu trả lời đó thực sự rất may mắn).
TonyK

4
@TonyK lời xin lỗi của tôi, tôi đã bỏ lỡ điểm. Không nghĩ đủ mạnh và nghĩ rằng sự thiên vị sẽ chỉ áp dụng với các phương thức sử dụng thao tác mô đun rõ ràng. Cảm ơn đã sửa chữa cho tôi :-)
boycy

Ưu tiên toán tử làm cho RAND_MAX+1 - (RAND_MAX+1) % ncông việc chính xác, nhưng tôi vẫn nghĩ rằng nó nên được viết như là RAND_MAX+1 - ((RAND_MAX+1) % n)rõ ràng.
Linus Arver

4
Điều này sẽ không hoạt động nếu RAND_MAX == INT_MAX (như trên hầu hết các hệ thống) . Xem bình luận thứ hai của tôi cho @ user1413793 ở trên.
BlueRaja - Daniel Pflughoeft

19

@ user1413793 là chính xác về vấn đề. Tôi sẽ không thảo luận thêm, ngoại trừ để đưa ra một điểm: có, đối với các giá trị nhỏ nvà giá trị lớn của RAND_MAX, độ lệch modulo có thể rất nhỏ. Nhưng sử dụng một mẫu gây ra sai lệch có nghĩa là bạn phải xem xét độ lệch mỗi khi bạn tính một số ngẫu nhiên và chọn các mẫu khác nhau cho các trường hợp khác nhau. Và nếu bạn chọn sai, các lỗi mà nó giới thiệu là tinh tế và gần như không thể kiểm tra đơn vị. So với việc chỉ sử dụng công cụ thích hợp (nhưarc4random_uniform ), đó là công việc làm thêm chứ không phải công việc ít hơn. Làm nhiều công việc hơn và nhận được một giải pháp tồi tệ hơn là kỹ thuật khủng khiếp, đặc biệt là khi thực hiện đúng mọi lúc mọi nơi đều dễ dàng trên hầu hết các nền tảng.

Thật không may, việc triển khai giải pháp đều không chính xác hoặc kém hiệu quả hơn mức cần thiết. (Mỗi giải pháp có nhiều nhận xét khác nhau giải thích các vấn đề, nhưng không có giải pháp nào được khắc phục để giải quyết chúng.) Điều này có thể gây nhầm lẫn cho người tìm câu trả lời thông thường, vì vậy tôi đang cung cấp một triển khai tốt được biết đến ở đây.

Một lần nữa, giải pháp tốt nhất là chỉ sử dụng arc4random_uniformtrên các nền tảng cung cấp nó hoặc giải pháp có phạm vi tương tự cho nền tảng của bạn (chẳng hạn nhưRandom.nextInt trên Java). Nó sẽ làm điều đúng đắn mà không mất chi phí mã cho bạn. Đây gần như luôn luôn là cuộc gọi chính xác để thực hiện.

Nếu bạn không có arc4random_uniform, thì bạn có thể sử dụng sức mạnh của mã nguồn mở để xem chính xác cách thức triển khai trên RNG phạm vi rộng hơn (ar4random trong trường hợp này, nhưng cách tiếp cận tương tự cũng có thể hoạt động trên khác).

Đây là cách triển khai 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;
}

Điều đáng chú ý là nhận xét cam kết mới nhất về mã này cho những người cần thực hiện những điều tương tự:

Thay đổi arc4random_uniform () để tính 2**32 % upper_boundnhư -upper_bound % upper_bound. Đơn giản hóa mã và làm cho nó giống nhau trên cả kiến ​​trúc ILP32 và LP64, và cũng nhanh hơn một chút trên kiến ​​trúc LP64 bằng cách sử dụng phần còn lại 32 bit thay vì phần còn lại 64 bit.

Được chỉ ra bởi Jorden Verwer trên tech @ ok deraadt; không phản đối từ djm hoặc otto

Việc triển khai Java cũng dễ dàng tìm thấy (xem liên kết trước):

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;
 }

Lưu ý rằng nếu arcfour_random() thực sự sử dụng thuật toán RC4 thực trong quá trình thực hiện, đầu ra chắc chắn sẽ có một số sai lệch. Hy vọng rằng các tác giả thư viện của bạn đã chuyển sang sử dụng CSPRNG tốt hơn đằng sau cùng một giao diện. Tôi nhớ lại một trong những BSD hiện đang thực sự sử dụng thuật toán ChaCha20 để thực hiện arcfour_random(). Thông tin thêm về các xu hướng đầu ra RC4 khiến nó trở nên vô dụng đối với bảo mật hoặc các ứng dụng quan trọng khác như video poker: blog.cryptographyengineering.com/2013/03/ Kẻ
rmalayter

2
@rmalayter Trên iOS và OS X, arc4random đọc từ / dev / ngẫu nhiên, đó là entropy chất lượng cao nhất trong hệ thống. ("Arc4" trong tên là lịch sử và được bảo tồn để tương thích.)
Rob Napier

@Rob_Napier nên biết, nhưng /dev/randomcũng đã sử dụng RC4 trên một số nền tảng trong quá khứ (Linux sử dụng SHA-1 ở chế độ truy cập). Thật không may, các trang man tôi tìm thấy qua tìm kiếm cho thấy RC4 vẫn đang được sử dụng trên các nền tảng khác nhau cung cấp arc4random(mặc dù mã thực tế có thể khác nhau).
rmalayter

1
Tôi bối rối. Có phải không -upper_bound % upper_bound == 0??
Jon McClung

1
@JonMcClung -upper_bound % upper_boundthực sự sẽ là 0 nếu intrộng hơn 32 bit. Nó phải là (u_int32_t)-upper_bound % upper_bound)(giả sử u_int32_tlà một BSD-ism cho uint32_t).
Ian Abbott

14

Định nghĩa

Modulo Bias là xu hướng cố hữu trong việc sử dụng số học modulo để giảm tập đầu ra thành tập con của tập đầu vào. Nói chung, sai lệch tồn tại bất cứ khi nào ánh xạ giữa bộ đầu vào và đầu ra không được phân phối bằng nhau, như trong trường hợp sử dụng số học modulo khi kích thước của bộ đầu ra không phải là ước của kích thước của bộ đầu vào.

Sự thiên vị này đặc biệt khó tránh trong điện toán, trong đó các số được biểu diễn dưới dạng các chuỗi bit: 0s và 1s. Tìm kiếm nguồn ngẫu nhiên thực sự ngẫu nhiên cũng vô cùng khó khăn, nhưng nằm ngoài phạm vi của cuộc thảo luận này.Trong phần còn lại của câu trả lời này, giả sử rằng tồn tại một nguồn không giới hạn các bit thực sự ngẫu nhiên.

Ví dụ vấn đề

Chúng ta hãy xem xét mô phỏng một cuộn chết (0 đến 5) bằng cách sử dụng các bit ngẫu nhiên này. Có 6 khả năng, vì vậy chúng ta cần đủ bit để đại diện cho số 6, là 3 bit. Thật không may, 3 bit ngẫu nhiên mang lại 8 kết quả có thể xảy ra:

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

Chúng ta có thể giảm kích thước của kết quả được đặt thành chính xác 6 bằng cách lấy giá trị modulo 6, tuy nhiên điều này thể hiện vấn đề sai lệch modulo : 110mang lại 0 và 111mang lại 1. Cái chết này được tải.

Các giải pháp tiềm năng

Cách tiếp cận 0:

Thay vì dựa vào các bit ngẫu nhiên, theo lý thuyết, người ta có thể thuê một đội quân nhỏ để gieo xúc xắc cả ngày và ghi lại kết quả vào cơ sở dữ liệu, sau đó chỉ sử dụng mỗi kết quả một lần. Điều này là thực tế như nó có vẻ, và nhiều khả năng sẽ không mang lại kết quả thực sự ngẫu nhiên nào (ý định chơi chữ).

Cách tiếp cận 1:

Thay vì sử dụng các module, một giải pháp ngây thơ nhưng về mặt toán học chính xác là kết quả loại bỏ mà năng suất 110111và chỉ cần cố gắng một lần nữa với 3 bit mới. Thật không may, điều này có nghĩa là có 25% cơ hội cho mỗi lần cuộn mà một cuộn lại sẽ được yêu cầu, bao gồm cả mỗi lần cuộn lại. Điều này rõ ràng là không thực tế cho tất cả nhưng sử dụng tầm thường nhất.

Cách tiếp cận 2:

Sử dụng nhiều bit hơn: thay vì 3 bit, sử dụng 4. Điều này mang lại 16 kết quả có thể xảy ra. Tất nhiên, quay lại bất cứ lúc nào kết quả lớn hơn 5 làm cho mọi thứ tồi tệ hơn (10/16 = 62,5%) để một mình sẽ không giúp đỡ.

Lưu ý rằng 2 * 6 = 12 <16, vì vậy chúng tôi có thể nhận bất kỳ kết quả nào nhỏ hơn 12 và giảm modulo 6 đó để phân phối đều các kết quả. 4 kết quả khác phải được loại bỏ, và sau đó cuộn lại như trong phương pháp trước.

Thoạt nghe có vẻ hay, nhưng hãy kiểm tra toán:

4 discarded results / 16 possibilities = 25%

Trong trường hợp này, thêm 1 bit không giúp được gì cả!

Kết quả đó thật đáng tiếc, nhưng hãy thử lại với 5 bit:

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

Một cải tiến rõ ràng, nhưng không đủ tốt trong nhiều trường hợp thực tế. Tin tốt là, thêm nhiều bit sẽ không bao giờ tăng cơ hội cần phải loại bỏ và cuộn lại . Điều này giữ không chỉ cho súc sắc, mà trong mọi trường hợp.

Như đã trình bày , việc thêm 1 bit có thể không thay đổi bất cứ điều gì. Trong thực tế nếu chúng ta tăng cuộn lên 6 bit, xác suất vẫn là 6,25%.

Điều này đặt ra 2 câu hỏi bổ sung:

  1. Nếu chúng ta thêm đủ bit, có đảm bảo rằng xác suất loại bỏ sẽ giảm đi không?
  2. Có bao nhiêu bit là đủ trong trường hợp chung?

Giải pháp chung

Rất may câu trả lời cho câu hỏi đầu tiên là có. Vấn đề với 6 là 2 ^ x mod 6 lật giữa 2 và 4, trùng hợp là bội số của 2 với nhau, do đó, cho một x> 1 chẵn

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

Do đó, 6 là một ngoại lệ chứ không phải là quy tắc. Có thể tìm thấy các mô đun lớn hơn mang lại sức mạnh liên tiếp bằng 2 theo cùng một cách, nhưng cuối cùng điều này phải bao bọc và xác suất loại bỏ sẽ bị giảm.

Nếu không cung cấp thêm bằng chứng, nói chung, sử dụng gấp đôi số bit cần thiết sẽ cung cấp cơ hội loại bỏ nhỏ hơn, thường không đáng kể.

Bằng chứng của khái niệm

Đây là một chương trình ví dụ sử dụng libcrypo của OpenSSL để cung cấp các byte ngẫu nhiên. Khi biên dịch, hãy chắc chắn liên kết đến thư viện -lcryptomà hầu hết mọi người nên có sẵn.

#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;
}

Tôi khuyến khích chơi với MODULUSROLLScác giá trị để xem có bao nhiêu cuộn lại thực sự xảy ra trong hầu hết các điều kiện. Một người hay hoài nghi cũng có thể muốn lưu các giá trị được tính vào tệp và xác minh phân phối có vẻ bình thường.


Tôi thực sự hy vọng không ai đã sao chép một cách mù quáng việc thực hiện ngẫu nhiên đồng phục của bạn. Các randomPool = RAND_bytes(...)dòng sẽ luôn luôn dẫn đến randomPool == 1do sự khẳng định. Điều này luôn dẫn đến việc loại bỏ và cuộn lại. Tôi nghĩ rằng bạn muốn tuyên bố trên một dòng riêng biệt. Do đó, điều này khiến RNG trở lại với 1mỗi lần lặp.
Qix - MONICA ĐƯỢC PHÂN PHỐI

Để rõ ràng, randomPoolsẽ luôn luôn đánh giá 1theo tài liệuRAND_bytes() OpenSSL vì nó sẽ luôn thành công nhờ vào sự RAND_status()khẳng định.
Qix - MONICA ĐƯỢC PHÂN PHỐI

9

Có hai khiếu nại thông thường với việc sử dụng modulo.

  • một là hợp lệ cho tất cả các máy phát điện. Nó là dễ dàng hơn để xem trong một trường hợp giới hạn. Nếu trình tạo của bạn có RAND_MAX là 2 (không tuân thủ tiêu chuẩn C) và bạn chỉ muốn 0 hoặc 1 làm giá trị, sử dụng modulo sẽ tạo ra 0 lần thường xuyên (khi trình tạo tạo 0 và 2) vì nó sẽ tạo 1 (khi trình tạo tạo 1). Lưu ý rằng điều này đúng ngay khi bạn không bỏ các giá trị, bất kể ánh xạ bạn đang sử dụng từ các giá trị của trình tạo sang giá trị mong muốn, một sẽ xảy ra thường xuyên gấp đôi so với giá trị khác.

  • một số loại trình tạo có các bit ít quan trọng hơn ít ngẫu nhiên hơn các loại khác, ít nhất là đối với một số tham số của chúng, nhưng đáng buồn là các tham số đó có đặc tính thú vị khác (như vậy có thể có RAND_MAX ít hơn một công suất 2). Vấn đề đã được biết đến và trong một thời gian dài triển khai thư viện có thể tránh được vấn đề (ví dụ như việc triển khai rand () mẫu trong tiêu chuẩn C sử dụng loại trình tạo này, nhưng bỏ 16 bit ít quan trọng hơn), nhưng một số người muốn phàn nàn về điều đó và bạn có thể gặp xui xẻo

Sử dụng một cái gì đó như

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; 
}

để tạo một số ngẫu nhiên trong khoảng từ 0 đến n sẽ tránh được cả hai vấn đề (và nó tránh được tràn với RAND_MAX == INT_MAX)

BTW, C ++ 11 đã giới thiệu các cách tiêu chuẩn để giảm và các trình tạo khác ngoài rand ().


n == RAND_MAX? 1: (RAND_MAX-1) / (n + 1): Tôi hiểu ý tưởng ở đây là trước tiên chia RAND_MAX thành kích thước trang bằng N, sau đó trả lại độ lệch trong N, nhưng tôi không thể ánh xạ chính xác mã này.
nhấp nháy

1
Phiên bản ngây thơ phải là (RAND_MAX + 1) / (n + 1) vì có các giá trị RAND_MAX + 1 để chia trong n + 1 xô. Nếu để tránh tràn khi tính toán RAND_MAX + 1, nó có thể được chuyển đổi thành 1+ (RAND_MAX-n) / (n + 1). Để tránh tràn khi tính toán n + 1, trường hợp n == RAND_MAX được kiểm tra trước tiên.
Lập trình viên

+ cộng với, thực hiện chia có vẻ tốn kém hơn thậm chí so với số tái tạo.
nhấp nháy

4
Lấy modulo và chia có cùng chi phí. Một số ISA thậm chí chỉ cung cấp một lệnh cung cấp luôn cả hai. Chi phí tái tạo số sẽ phụ thuộc vào n và RAND_MAX. Nếu n nhỏ so với RAND_MAX, nó có thể có giá rất cao. Và rõ ràng bạn có thể quyết định những thành kiến ​​không quan trọng đối với ứng dụng của bạn; Tôi chỉ đưa ra một cách để tránh chúng.
Lập trình viên

9

Giải pháp của Mark (Giải pháp được chấp nhận) gần như hoàn hảo.

int x;

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

x %= n;

chỉnh sửa ngày 25 tháng 3 năm 16 lúc 23:16

Đánh dấu Amery 39k21170211

Tuy nhiên, nó có một cảnh báo loại bỏ 1 tập kết quả hợp lệ trong bất kỳ kịch bản nào trong đó RAND_MAX( RM) nhỏ hơn 1 so với bội số của N(Trong đóN = Số lượng kết quả hợp lệ có thể có).

tức là, khi 'số lượng giá trị bị loại bỏ' ( D) bằng N, thì chúng thực sự là một tập hợp lệ ( V)không phải là tập hợp không hợp lệ ( I).

Điều gì gây ra điều này là tại một số điểm Mark đánh mất sự khác biệt giữa NRand_Max.

Nlà một tập hợp những thành viên hợp lệ chỉ bao gồm các số nguyên dương, vì nó chứa một số phản hồi sẽ hợp lệ. (ví dụ: Đặt N= {1, 2, 3, ... n })

Rand_max Tuy nhiên, một tập hợp (như được xác định cho mục đích của chúng tôi) bao gồm bất kỳ số nguyên không âm nào.

Ở dạng chung nhất, những gì được định nghĩa ở đây là Rand Max là Tập hợp tất cả các kết quả hợp lệ, về mặt lý thuyết có thể bao gồm số âm hoặc giá trị không phải là số.

Do đó, Rand_Maxđược định nghĩa tốt hơn là tập hợp các "Phản hồi có thể".

Tuy nhiên, Nhoạt động dựa trên số lượng giá trị trong tập hợp các phản hồi hợp lệ, do đó, ngay cả khi được xác định trong trường hợp cụ thể của chúng tôi, Rand_Maxsẽ là một giá trị nhỏ hơn tổng số mà nó chứa.

Sử dụng Giải pháp của Mark, các giá trị bị loại bỏ khi: 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

Như bạn có thể thấy trong ví dụ trên, khi giá trị của X (số ngẫu nhiên chúng ta nhận được từ hàm ban đầu) là 252, 253, 254 hoặc 255, chúng ta sẽ loại bỏ nó mặc dù bốn giá trị này bao gồm một tập hợp các giá trị được trả về hợp lệ .

IE: Khi số lượng giá trị bị loại bỏ (I) = N (Số lượng kết quả hợp lệ) thì một bộ giá trị trả về hợp lệ sẽ bị loại bỏ bởi chức năng ban đầu.

Nếu chúng tôi mô tả sự khác biệt giữa các giá trị N và RM là D, nghĩa là:

D = (RM - N)

Sau đó, khi giá trị của D trở nên nhỏ hơn, Tỷ lệ cuộn lại không cần thiết do phương pháp này tăng lên ở mỗi phép nhân tự nhiên. (Khi RAND_MAX KHÔNG bằng Số nguyên tố, đây là mối quan tâm hợp lệ)

VÍ DỤ:

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%

Vì tỷ lệ phần trăm Reroll cần thiết tăng N càng gần với RM, nên điều này có thể là mối quan tâm hợp lệ ở nhiều giá trị khác nhau tùy thuộc vào các ràng buộc của hệ thống đang chạy mã và các giá trị được tìm kiếm.

Để phủ nhận điều này, chúng ta có thể thực hiện một sửa đổi đơn giản Như được hiển thị ở đây:

 int x;

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

 x %= n;

Điều này cung cấp một phiên bản tổng quát hơn của công thức tính đến các đặc thù bổ sung của việc sử dụng mô đun để xác định các giá trị tối đa của bạn.

Ví dụ về việc sử dụng một giá trị nhỏ cho RAND_MAX là một số nhân của N.

Phiên bản gốc của Mark'origen:

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.

Phiên bản tổng quát 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.

Ngoài ra, trong trường hợp N phải là số lượng giá trị trong RAND_MAX; trong trường hợp này, bạn có thể đặt N = RAND_MAX +1, trừ khi RAND_MAX = INT_MAX.

Tuy nhiên, bạn có thể sử dụng N = 1 và bất kỳ giá trị nào của X sẽ được chấp nhận và đưa ra câu lệnh IF cho hệ số nhân cuối cùng của bạn. Nhưng có lẽ bạn có mã có thể có lý do hợp lệ để trả về 1 khi hàm được gọi với n = 1 ...

Vì vậy, có thể tốt hơn khi sử dụng 0, thông thường sẽ cung cấp Lỗi Div 0, khi bạn muốn có n = RAND_MAX + 1

Phiên bản tổng quát 2:

int x;

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

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

Cả hai giải pháp này đều giải quyết vấn đề với các kết quả hợp lệ bị loại bỏ một cách không cần thiết sẽ xảy ra khi RM + 1 là sản phẩm của n.

Phiên bản thứ hai cũng bao gồm kịch bản trường hợp cạnh khi bạn cần n bằng tổng số giá trị có thể có trong RAND_MAX.

Cách tiếp cận được sửa đổi trong cả hai đều giống nhau và cho phép giải pháp tổng quát hơn cho nhu cầu cung cấp số ngẫu nhiên hợp lệ và giảm thiểu các giá trị bị loại bỏ.

Để nhắc lại:

Giải pháp chung cơ bản mở rộng ví dụ về nhãn hiệu:

// 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;

Giải pháp chung mở rộng cho phép thêm một kịch bản 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();
}

Trong một số ngôn ngữ (ngôn ngữ được giải thích đặc biệt) thực hiện các tính toán của hoạt động so sánh bên ngoài điều kiện while có thể dẫn đến kết quả nhanh hơn vì đây là phép tính một lần cho dù có bao nhiêu lần thử lại. 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();
}

Có an toàn không khi nói rằng vấn đề với giải pháp của Mark là anh ta coi RAND_MAX và n là cùng một "đơn vị đo lường" trong khi thực tế chúng có nghĩa là hai điều khác nhau? Trong khi n đại diện cho "số khả năng" kết quả, RAND_MAX chỉ đại diện cho giá trị tối đa của khả năng ban đầu, trong đó RAND_MAX + 1 sẽ là số khả năng ban đầu. Tôi ngạc nhiên khi anh ta không đi đến kết luận của bạn vì dường như anh ta đã thừa nhận n và RAND_MAX không giống với phương trình:RAND_MAX%n = n - 1
Danilo Souza Morães

@ DaniloSouzaMorães Cảm ơn Danilo, Bạn đã đặt vấn đề rất ngắn gọn. Tôi đã đi chứng minh những gì anh ấy đã làm cùng với Tại sao và như thế nào, nhưng đừng nghĩ rằng tôi đã có thể nói rõ anh ấy đã làm gì sai một cách hùng hồn, khi tôi hiểu rất chi tiết về logic về cách thức và Tại sao có một vấn đề, mà tôi không nói rõ là vấn đề gì. Bạn có phiền nếu tôi sửa đổi Câu trả lời của mình để sử dụng một số nội dung bạn đã viết ở đây làm bản tóm tắt của riêng tôi về vấn đề gì và giải pháp được chấp nhận đang làm gì cần được giải quyết gần đầu trang không?
Ben Personick

Điêu đo thật tuyệt vơi. Đi cho nó
Danilo Souza Morães

1

Với RAND_MAXgiá trị 3(trong thực tế, nó sẽ cao hơn thế nhiều nhưng sự thiên vị vẫn tồn tại) có ý nghĩa từ những tính toán này rằng có sự thiên vị:

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

Trong trường hợp này, đó % 2là những gì bạn không nên làm khi bạn muốn một số ngẫu nhiên giữa 01. Bạn có thể nhận được một số ngẫu nhiên giữa 02bằng cách thực hiện % 3, bởi vì trong trường hợp này: RAND_MAXlà bội số của 3.

Một phương pháp khác

Có nhiều cách đơn giản hơn nhưng để thêm vào các câu trả lời khác, đây là giải pháp của tôi để có được một số ngẫu nhiên giữa 0n - 1, vì vậy ncác khả năng khác nhau, không có sai lệch.

  • số lượng bit (không phải byte) cần thiết để mã hóa số lượng khả năng là số bit của dữ liệu ngẫu nhiên bạn sẽ cần
  • mã hóa số từ các bit ngẫu nhiên
  • nếu số này là >= n, khởi động lại (không có modulo).

Dữ liệu thực sự ngẫu nhiên không dễ dàng có được, vậy tại sao sử dụng nhiều bit hơn mức cần thiết.

Dưới đây là một ví dụ trong Smalltalk, sử dụng bộ đệm của các bit từ trình tạo số giả ngẫu nhiên. Tôi không có chuyên gia bảo mật nên sử dụng rủi ro của riêng bạn.

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

-1

Như câu trả lời được chấp nhận chỉ ra, "độ lệch modulo" có nguồn gốc từ giá trị thấp của RAND_MAX. Anh ta sử dụng một giá trị cực kỳ nhỏ RAND_MAX(10) để chỉ ra rằng nếu RAND_MAX là 10, thì bạn đã cố tạo một số từ 0 đến 2 bằng%, kết quả sau đây sẽ dẫn đến:

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

Vì vậy, có 4 đầu ra là 0 (cơ hội 4/10) và chỉ có 3 đầu ra là 1 và 2 (3/10 cơ hội mỗi đầu ra).

Vì vậy, nó thiên vị. Những con số thấp hơn có cơ hội tốt hơn để đi ra.

Nhưng điều đó chỉ hiển thị như vậy rõ ràng khi RAND_MAXcòn nhỏ . Hay cụ thể hơn, khi số lượng bạn đang sửa đổi lớn hơn so vớiRAND_MAX .

Một giải pháp tốt hơn nhiều so với lặp (không hoàn toàn không hiệu quả và thậm chí không nên đề xuất) là sử dụng PRNG với phạm vi đầu ra lớn hơn nhiều. Các Mersenne Twister thuật toán có một sản lượng tối đa của 4294967295. Như vậy làm MersenneTwister::genrand_int32() % 10cho tất cả các ý định và mục đích, sẽ được phân phối đều và hiệu ứng thiên vị modulo sẽ biến mất.


3
Của bạn hiệu quả hơn và có lẽ đúng là nếu RAND_MAX lớn hơn đáng kể thì con số bạn đang sửa đổi, tuy nhiên số của bạn vẫn sẽ bị sai lệch. Được cho là tất cả các trình tạo số ngẫu nhiên giả dù sao và bản thân nó là một chủ đề khác nhau nhưng nếu bạn giả sử một trình tạo số ngẫu nhiên hoàn toàn, cách của bạn vẫn thiên vị các giá trị thấp hơn.
dùng1413793

Bởi vì giá trị cao nhất là số lẻ, MT::genrand_int32()%2chọn 0 (50 + 2.3e-8)% thời gian và 1 (50 - 2.3e-8)% thời gian. Trừ khi bạn xây dựng RGN của sòng bạc (mà bạn có thể sẽ sử dụng phạm vi RGN lớn hơn nhiều), bất kỳ người dùng nào cũng sẽ không nhận thấy thêm 2,3e-8% thời gian. Bạn đang nói về những con số quá nhỏ để quan trọng ở đây.
bobobobo

7
Vòng lặp là giải pháp tốt nhất. Nó không phải là "không hiệu quả điên cuồng"; yêu cầu ít hơn hai lần số lần lặp trong trường hợp trung bình tệ nhất. Sử dụng RAND_MAXgiá trị cao sẽ làm giảm độ lệch modulo, nhưng không loại bỏ nó. Vòng lặp ý chí.
Jared Nielsen

5
Nếu RAND_MAXđủ lớn hơn số bạn đang sửa đổi, số lần bạn cần để tạo lại số ngẫu nhiên là rất nhỏ và sẽ không ảnh hưởng đến hiệu quả. Tôi nói hãy giữ vòng lặp, miễn là bạn đang thử nghiệm với bội số lớn nhất nthay vì chỉ nđược đề xuất bởi câu trả lời được chấp nhận.
Đánh dấu tiền chuộc

-3

Tôi vừa viết một mã cho Phương pháp lật xu không thiên vị của Von Neumann, về mặt lý thuyết sẽ loại bỏ bất kỳ sự thiên vị nào trong quy trình tạo số ngẫu nhiên. Thông tin thêm có thể được tìm thấy tại ( 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;
        }
    }
}

Điều này không giải quyết sự thiên vị modulo. Quá trình này có thể được sử dụng để loại bỏ sự thiên vị trong một luồng bit. Tuy nhiên, để có được từ một luồng bit đến phân phối chẵn từ 0 đến n trong đó n không nhỏ hơn một lũy thừa hai yêu cầu giải quyết sai lệch modulo. Do đó, giải pháp này không thể loại bỏ bất kỳ sai lệch nào trong quá trình tạo số ngẫu nhiên.
Rick

2
@Rick hmm. Phần mở rộng hợp lý của phương pháp của Von Neumann để loại bỏ sai lệch modulo khi tạo một số ngẫu nhiên giữa, giả sử, 1 và 100, sẽ là: A) gọi rand() % 100100 lần. B) nếu tất cả các kết quả là khác nhau, lấy cái đầu tiên. C) nếu không, GOTO A. Điều này sẽ hoạt động, nhưng với số lần lặp dự kiến ​​khoảng 10 ^ 42, bạn sẽ phải khá kiên nhẫn. Và bất tử.
Đánh dấu Amery

@MarkAmery Thật vậy nên làm việc. Nhìn qua thuật toán này mặc dù nó không được thực hiện đúng. Cái khác đầu tiên phải là:else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
Rick
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.