เหตุใด Rand ()% 6 จึงเอนเอียง


109

เมื่ออ่านวิธีใช้ std :: rand ฉันพบโค้ดนี้ในcppreference.com

int x = 7;
while(x > 6) 
    x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased

มีอะไรผิดปกติกับนิพจน์ด้านขวา? พยายามแล้วและทำงานได้อย่างสมบูรณ์


24
โปรดทราบว่าการใช้ลูกเต๋าจะดีกว่าstd::uniform_int_distribution
Caleth

1
@Caleth ใช่ก็แค่เข้าใจว่าทำไมรหัสนี้ถึง 'ผิด' ..
yO_

15
เปลี่ยน "ไม่ถูกต้อง" เป็น "ลำเอียง"
Cubbi

3
rand()แย่มากในการใช้งานทั่วไปคุณอาจใช้xkcd RNGเช่นกัน ดังนั้นจึงผิดเพราะใช้rand().
CodesInChaos

3
ฉันเขียนสิ่งนี้ (ไม่ใช่ความคิดเห็น - นั่นคือ @Cubbi) และสิ่งที่ฉันคิดไว้ในตอนนั้นคือคำตอบของ Pete Beckerอธิบาย (FYI นี่เป็นอัลกอริทึมเดียวกับ libstdc ++ uniform_int_distribution)
TC

คำตอบ:


136

มีสองปัญหาrand() % 6( ปัญหา1+ไม่ส่งผลกระทบต่อปัญหาใด ๆ )

ประการแรกเนื่องจากคำตอบหลาย ๆ คำได้ชี้ให้เห็นหากบิตต่ำrand()ไม่สม่ำเสมอกันผลลัพธ์ของตัวดำเนินการส่วนที่เหลือก็ไม่สม่ำเสมอเช่นกัน

ประการที่สองถ้าจำนวนของค่าที่แตกต่างกันที่สร้างขึ้นโดยrand()ไม่ใช่ผลคูณของ 6 ส่วนที่เหลือจะสร้างค่าที่ต่ำกว่าค่าสูง นั่นเป็นความจริงแม้ว่าจะrand()ส่งคืนค่าที่กระจายอย่างสมบูรณ์แบบก็ตาม

ในฐานะที่เป็นตัวอย่างมากแกล้งทำเป็นว่าผลิตค่าการกระจายอย่างสม่ำเสมอในช่วงrand() [0..6]ถ้าคุณดูที่เหลือสำหรับค่านั้นเมื่อrand()ส่งกลับค่าในช่วงที่เหลือผลิตกระจายผลในช่วง[0..5] [0..5]เมื่อrand()คืนค่า 6 ให้rand() % 6ส่งกลับ 0 เหมือนกับว่าrand()ได้ส่งกลับ 0 ดังนั้นคุณจะได้รับการแจกแจงที่มีค่า 0 เป็นสองเท่าของค่าอื่น ๆ

ประการที่สองคือปัญหาที่แท้จริงกับrand() % 6.

วิธีหลีกเลี่ยงปัญหานั้นคือการละทิ้งค่าที่จะทำให้เกิดรายการซ้ำที่ไม่สม่ำเสมอ คุณคำนวณผลคูณที่ใหญ่ที่สุดของ 6 ที่น้อยกว่าหรือเท่ากับRAND_MAXและเมื่อใดก็ตามที่rand()ส่งกลับค่าที่มากกว่าหรือเท่ากับจำนวนเต็มที่คุณปฏิเสธและเรียกว่า `rand () อีกครั้งตามต้องการหลาย ๆ ครั้ง

ดังนั้น:

int max = 6 * ((RAND_MAX + 1u) / 6)
int value = rand();
while (value >= max)
    value = rand();

นั่นเป็นการใช้งานโค้ดที่เป็นปัญหาที่แตกต่างกันโดยมีจุดประสงค์เพื่อแสดงให้เห็นชัดเจนยิ่งขึ้นว่าเกิดอะไรขึ้น


2
ฉันได้สัญญาอย่างน้อยหนึ่งครั้งในไซต์นี้ว่าจะจัดทำเอกสารเกี่ยวกับเรื่องนี้ แต่ฉันคิดว่าการสุ่มตัวอย่างและการปฏิเสธสามารถทำให้ช่วงเวลาสำคัญลดลง เช่นขยายความแปรปรวนมากเกินไป
Bathsheba

30
ฉันสร้างกราฟว่าเทคนิคนี้แนะนำให้มีอคติมากน้อยเพียงใดหาก rand_max เป็น 32768 ซึ่งอยู่ในการนำไปใช้งานบางอย่าง ericlippert.com/2013/12/16/…
Eric Lippert

2
@ บา ธ เชบา: เป็นความจริงที่ฟังก์ชันการปฏิเสธบางอย่างอาจทำให้เกิดสิ่งนี้ได้ แต่การปฏิเสธอย่างง่ายนี้จะเปลี่ยน IID ที่เหมือนกันให้เป็นการกระจาย IID ที่เหมือนกัน ไม่มีชิ้นส่วนใด ๆ ที่เกินดังนั้นจึงเป็นอิสระตัวอย่างทั้งหมดใช้การปฏิเสธแบบเดียวกันเพื่อให้เหมือนกันและไม่สำคัญเพื่อแสดงความสม่ำเสมอ และช่วงเวลาที่สูงขึ้นของตัวแปรสุ่มอินทิกรัลที่เหมือนกันจะถูกกำหนดโดยช่วงของมันอย่างสมบูรณ์
MSalters

4
@MSalters: ประโยคแรกของคุณถูกต้องสำหรับเครื่องกำเนิดไฟฟ้าจริงไม่จำเป็นต้องเป็นจริงสำหรับเครื่องกำเนิดไฟฟ้าหลอก เมื่อฉันเกษียณฉันจะเขียนบทความเกี่ยวกับเรื่องนี้
Bathsheba

2
@ แอนโธนี่คิดในแง่ของลูกเต๋า คุณต้องการตัวเลขสุ่มระหว่าง 1 ถึง 3 และคุณมีเฉพาะแม่พิมพ์ 6 เหลี่ยมมาตรฐานเท่านั้น คุณสามารถทำได้โดยการลบ 3 ถ้าคุณหมุน 4-6 แต่สมมติว่าคุณต้องการตัวเลขระหว่าง 1 ถึง 5 แทนถ้าคุณลบ 5 เมื่อคุณหมุน 6 คุณจะได้ 1 เป็นสองเท่าของจำนวนอื่น ๆ นั่นคือสิ่งที่โค้ด cppreference กำลังทำอยู่ สิ่งที่ถูกต้องที่ต้องทำคือหมุน 6s ใหม่ นั่นคือสิ่งที่พีทกำลังทำอยู่ที่นี่: แบ่งตัวตายออกเพื่อให้มีหลายวิธีในการหมุนแต่ละหมายเลขและหมุนหมายเลขใด ๆ ที่ไม่เข้ากับดิวิชั่นคู่
เรย์

19

มีความลึกที่ซ่อนอยู่ที่นี่:

  1. การใช้ขนาดเล็กในu ถูกกำหนดให้เป็นประเภทและมักจะเป็นไปได้ที่ใหญ่ที่สุด พฤติกรรมของจะไม่ได้กำหนดไว้ในกรณีเช่นนี้เนื่องจากคุณมีประเภทมากเกินไป การเขียนแรงประเภทการแปลงเป็นเพื่อขัดขวางการล้นRAND_MAX + 1uRAND_MAXintintRAND_MAX + 1signed1uRAND_MAXunsigned

  2. การใช้% 6 กระป๋อง ( แต่ในการดำเนินการทุกstd::randที่ผมเคยเห็น ไม่ได้ ) แนะนำอคติทางสถิติใด ๆ เพิ่มเติมเหนือกว่าทางเลือกที่นำเสนอ กรณีดังกล่าวที่% 6เป็นอันตรายคือกรณีที่ตัวสร้างตัวเลขมีที่ราบสหสัมพันธ์ในบิตลำดับต่ำเช่นการใช้งาน IBM ที่มีชื่อเสียง (ใน C) ของrandในฉันคิดว่าในปี 1970 ซึ่งพลิกบิตสูงและต่ำเป็น "a final เจริญรุ่งเรือง ". ข้อควรพิจารณาเพิ่มเติมคือ 6 มีขนาดเล็กมาก cf RAND_MAXดังนั้นจะมีเอฟเฟกต์น้อยที่สุดหากRAND_MAXไม่ใช่ผลคูณของ 6 ซึ่งอาจไม่ใช่

สรุปได้ว่าทุกวันนี้ฉันใช้% 6มัน ไม่น่าจะนำเสนอความผิดปกติทางสถิติใด ๆ นอกเหนือไปจากที่เครื่องกำเนิดไฟฟ้าแนะนำ หากคุณยังสงสัยให้ทดสอบเครื่องกำเนิดไฟฟ้าของคุณเพื่อดูว่ามีคุณสมบัติทางสถิติที่เหมาะสมสำหรับกรณีการใช้งานของคุณหรือไม่


12
% 6ก่อให้เกิดผลลัพธ์ที่เอนเอียงเมื่อใดก็ตามที่จำนวนค่าที่แตกต่างกันที่สร้างขึ้นrand()ไม่ใช่ผลคูณของ 6 หลักการ Pigeon-hole จริงอยู่ที่อคติมีขนาดเล็กเมื่อRAND_MAXมีขนาดใหญ่กว่า 6 มาก แต่ก็มี และสำหรับช่วงเป้าหมายที่ใหญ่ขึ้นแน่นอนว่าเอฟเฟกต์จะใหญ่กว่า
Pete Becker

2
@PeteBecker: อันที่จริงฉันควรทำให้ชัดเจน แต่โปรดทราบว่าคุณจะได้รับนกพิราบเมื่อคุณสุ่มตัวอย่างช่วงเข้าใกล้ RAND_MAX เนื่องจากเอฟเฟกต์การตัดส่วนจำนวนเต็ม
Bathsheba

2
@ บา ธ เชบาไม่ว่าเอฟเฟกต์การตัดทอนจะนำไปสู่ผลลัพธ์ที่ใหญ่กว่า 6 และในการดำเนินการซ้ำ ๆ ของการดำเนินการทั้งหมดหรือไม่?
Gerhardh

1
@Gerhardh: ถูกต้อง ในความเป็นจริงจะนำไปสู่ตรงx==7ไปยังผล โดยทั่วไปคุณแบ่งช่วงออกเป็น[0, RAND_MAX]7 subranges 6 ที่มีขนาดเท่ากันและอีกช่วงย่อยที่เล็กกว่าในตอนท้าย ผลลัพธ์จากช่วงย่อยสุดท้ายจะถูกละทิ้ง ค่อนข้างชัดเจนว่าคุณไม่สามารถมีสองช่วงย่อยที่เล็กกว่าได้ในตอนท้ายด้วยวิธีนี้
MSalters

@MSalters: อันที่จริง แต่โปรดทราบว่าวิธีอื่น ๆ ยังคงได้รับผลกระทบเนื่องจากการตัดทอน สมมติฐานของฉันคือชาวบ้านอวบอ้วนในช่วงหลังเนื่องจากข้อผิดพลาดทางสถิตินั้นยากที่จะเข้าใจ!
Bathsheba

13

โค้ดตัวอย่างนี้แสดงให้เห็นว่าstd::randเป็นกรณีของ Balderdash ลัทธิการขนส่งสินค้าแบบดั้งเดิมที่ควรทำให้คิ้วของคุณยกขึ้นทุกครั้งที่คุณเห็น

มีปัญหาหลายประการที่นี่:

คนทำสัญญามักจะถือว่า - แม้แต่วิญญาณผู้เคราะห์ร้ายที่ไม่รู้จักอะไรดีกว่าและจะไม่คิดในแง่เหล่านี้อย่างชัดเจนนั่นคือrandตัวอย่างจากการแจกแจงสม่ำเสมอบนจำนวนเต็มใน 0, 1, 2, ... RAND_MAX, และการโทรแต่ละครั้งจะให้ตัวอย่างอิสระ

ปัญหาแรกคือสัญญาที่สมมติขึ้นซึ่งเป็นตัวอย่างแบบสุ่มที่เป็นอิสระในการโทรแต่ละครั้งไม่ใช่สิ่งที่เอกสารระบุไว้จริง ๆ และในทางปฏิบัติการนำไปใช้งานในอดีตล้มเหลวในการจัดหาแม้แต่สถานการณ์จำลองของความเป็นอิสระที่ชัดเจนที่สุด ตัวอย่างเช่น C99 §7.20.2.1 'The randfunction' กล่าวโดยไม่ต้องอธิบายรายละเอียด:

randฟังก์ชั่นคำนวณลำดับของจำนวนเต็มสุ่มหลอกในช่วง 0 RAND_MAXถึง

นี่เป็นประโยคที่ไม่มีความหมายเนื่องจาก pseudorandomness เป็นคุณสมบัติของฟังก์ชัน (หรือตระกูลของฟังก์ชัน ) ไม่ใช่จำนวนเต็ม แต่ก็ไม่ได้หยุดแม้แต่เจ้าหน้าที่ ISO จากการใช้ภาษาในทางที่ผิด ท้ายที่สุดผู้อ่านเพียงคนเดียวที่จะไม่พอใจมันรู้ดีกว่าอ่านเอกสารrandเพราะกลัวว่าเซลล์สมองจะสลายตัว

การใช้งานทางประวัติศาสตร์โดยทั่วไปใน C จะทำงานดังนี้:

static unsigned int seed = 1;

static void
srand(unsigned int s)
{
    seed = s;
}

static unsigned int
rand(void)
{
    seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
    return (int)seed;
}

สิ่งนี้มีคุณสมบัติที่น่าเสียดายที่แม้ว่าตัวอย่างเดียวอาจมีการกระจายอย่างสม่ำเสมอภายใต้เมล็ดพันธุ์แบบสุ่มที่เหมือนกัน (ซึ่งขึ้นอยู่กับค่าเฉพาะของRAND_MAX) มันสลับระหว่างจำนวนเต็มคู่และจำนวนคี่ในการโทรติดต่อกัน - หลัง

int a = rand();
int b = rand();

นิพจน์(a & 1) ^ (b & 1)จะให้ 1 พร้อมความน่าจะเป็น 100% ซึ่งไม่ใช่กรณีสำหรับตัวอย่างสุ่มอิสระในการแจกแจงใด ๆ ที่รองรับบนจำนวนเต็มคู่และจำนวนคี่ ดังนั้นลัทธิการขนส่งสินค้าจึงเกิดขึ้นว่าเราควรทิ้งบิตลำดับต่ำเพื่อไล่ล่าสัตว์ร้ายที่เข้าใจยากของ 'การสุ่มที่ดีกว่า' (คำเตือนเกี่ยวกับสปอยเลอร์: นี่ไม่ใช่ศัพท์ทางเทคนิคนี่เป็นสัญญาณว่าใครก็ตามที่คุณอ่านร้อยแก้วไม่ทราบว่าพวกเขากำลังพูดถึงอะไรหรือคิดว่าคุณเป็นคนไร้เดียงสาและต้องถูกมองข้าม)

ปัญหาที่สองคือแม้ว่าการเรียกแต่ละครั้งจะทำตัวอย่างเป็นอิสระจากการแจกแจงแบบสุ่มที่สม่ำเสมอบน 0, 1, 2, …, RAND_MAXผลลัพธ์ของrand() % 6การกระจายจะไม่สม่ำเสมอใน 0, 1, 2, 3, 4, 5 เหมือนการตาย ม้วนเว้นแต่RAND_MAXจะสอดคล้องกับ -1 โมดูโล 6 ตัวอย่าง ง่ายๆตัวอย่าง: ถ้าRAND_MAX= 6 จากนั้นrand()ผลลัพธ์ทั้งหมดมีความน่าจะเป็นเท่ากับ 1/7 แต่จากrand() % 6ผลลัพธ์ 0 มีความน่าจะเป็น 2/7 ในขณะที่ผลลัพธ์อื่น ๆ ทั้งหมดมีความน่าจะเป็น 1/7 .

วิธีที่จะทำเช่นนี้คือมีการสุ่มตัวอย่างปฏิเสธ: ซ้ำ ๆวาดอิสระตัวอย่างสุ่มเครื่องแบบsจาก 0, 1, 2, ... , RAND_MAXและปฏิเสธ (ตัวอย่าง) ผลลัพธ์ที่ 0, 1, 2, ... , ((RAND_MAX + 1) % 6) - 1ถ้าคุณได้รับหนึ่ง เริ่มต้นใหม่ s % 6มิฉะนั้นผลผลิต

unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
    continue;
return s % 6;

วิธีนี้ชุดของผลลัพธ์rand()ที่เรายอมรับจะหารด้วย 6 เท่า ๆ กันและผลลัพธ์ที่เป็นไปได้แต่ละรายการจะได้มาจากs % 6จำนวนผลลัพธ์ที่ยอมรับจากจำนวนเท่ากันrand()ดังนั้นหากrand()มีการกระจายอย่างสม่ำเสมอก็จะเป็นsเช่นนั้น ไม่มีข้อผูกมัดกับจำนวนการทดลอง แต่จำนวนที่คาดหวังน้อยกว่า 2 และความน่าจะเป็นที่จะประสบความสำเร็จเพิ่มขึ้นแบบทวีคูณตามจำนวนการทดลอง

ทางเลือกของซึ่งผลลัพธ์ของrand()คุณปฏิเสธไม่เป็นสาระสำคัญโดยมีเงื่อนไขว่าคุณแมจำนวนที่เท่ากันของพวกเขาไปยังแต่ละจำนวนเต็มด้านล่าง 6. รหัสที่ cppreference.com ที่ทำให้แตกต่างกันทางเลือกเพราะปัญหาแรกข้างต้นว่าไม่มีอะไรที่จะรับประกันเกี่ยวกับ การกระจายหรือความเป็นอิสระของเอาต์พุตของrand()และในทางปฏิบัติบิตลำดับต่ำจะแสดงรูปแบบที่ไม่ 'ดูสุ่มเพียงพอ' (ไม่ต้องสนใจว่าเอาต์พุตถัดไปเป็นฟังก์ชันที่กำหนดไว้ก่อนหน้านี้)

การออกกำลังกายสำหรับผู้อ่าน: พิสูจน์ว่ารหัสที่ cppreference.com ผลผลิตเครื่องแบบกระจายม้วนตายถ้าrand()อัตราผลตอบแทนเครื่องแบบกระจายกับ 0, 1, 2, ... RAND_MAX,

แบบฝึกหัดสำหรับผู้อ่าน: เหตุใดคุณจึงชอบที่จะปฏิเสธหนึ่งชุดหรือชุดย่อยอื่น ๆ การคำนวณแบบใดที่จำเป็นสำหรับการพิจารณาคดีแต่ละครั้งในสองกรณีนี้

ปัญหาที่สามคือพื้นที่เมล็ดมีขนาดเล็กมากแม้ว่าเมล็ดพันธุ์จะกระจายอย่างสม่ำเสมอ แต่ฝ่ายตรงข้ามที่มีความรู้เกี่ยวกับโปรแกรมของคุณและผลลัพธ์เพียงอย่างเดียว แต่ไม่ใช่เมล็ดพันธุ์สามารถทำนายเมล็ดพันธุ์และผลลัพธ์ที่ตามมาได้ทันทีซึ่งทำให้ดูเหมือนว่าไม่เป็นเช่นนั้น สุ่มหลังจากทั้งหมด ดังนั้นอย่าคิดที่จะใช้สิ่งนี้สำหรับการเข้ารหัส

คุณสามารถไปตามเส้นทางที่ใช้งานง่ายและคลาส C ++ 11 std::uniform_int_distributionด้วยอุปกรณ์สุ่มที่เหมาะสมและเอนจินสุ่มที่คุณชื่นชอบเช่น Mersenne twister ยอดนิยมstd::mt19937เพื่อเล่นลูกเต๋ากับลูกพี่ลูกน้องวัยสี่ขวบของคุณ แต่ถึงอย่างนั้นก็จะไม่ไป เหมาะสำหรับการสร้างวัสดุคีย์การเข้ารหัส - และ Mersenne twister ก็เป็นหมูอวกาศที่น่ากลัวเช่นกันด้วยสถานะหลายกิโลไบต์ที่สร้างความหายนะให้กับแคชของ CPU ของคุณด้วยเวลาตั้งค่าที่หยาบคายดังนั้นจึงไม่ดีแม้กระทั่งสำหรับเช่นการจำลองมอนติคาร์โลแบบขนานกับ ต้นไม้ที่ทำซ้ำได้ของการคำนวณย่อย ความนิยมน่าจะเกิดจากชื่อที่ติดปากเป็นหลัก แต่คุณสามารถใช้สำหรับทอยลูกเต๋ากลิ้งได้ดังตัวอย่างนี้!

อีกวิธีหนึ่งคือการใช้ตัวสร้างตัวเลขหลอกรหัสลับแบบธรรมดาที่มีสถานะขนาดเล็กเช่นPRNG การลบคีย์อย่างรวดเร็วอย่างง่ายหรือเพียงแค่การเข้ารหัสแบบสตรีมเช่น AES-CTR หรือ ChaCha20 หากคุณมั่นใจ ( เช่นในการจำลองมอนติคาร์โลสำหรับ การวิจัยทางวิทยาศาสตร์ธรรมชาติ) ว่าไม่มีผลเสียใด ๆ ในการทำนายผลลัพธ์ที่ผ่านมาหากรัฐถูกบุกรุก


4
"เวลาตั้งค่าอนาจาร" คุณไม่ควรใช้ตัวสร้างตัวเลขสุ่มมากกว่าหนึ่งตัว (ต่อเธรด) ดังนั้นเวลาในการตั้งค่าจะถูกตัดจำหน่ายเว้นแต่โปรแกรมของคุณจะทำงานไม่นานนัก
JAB

2
โหวต BTW เพราะไม่เข้าใจว่าลูปในคำถามกำลังทำการสุ่มตัวอย่างการปฏิเสธที่เหมือนกัน(RAND_MAX + 1 )% 6ทุกประการซึ่งมีค่าเท่ากันทุกประการ ไม่สำคัญว่าคุณจะแบ่งย่อยผลลัพธ์ที่เป็นไปได้อย่างไร คุณสามารถปฏิเสธได้จากทุกที่ในช่วง[0, RAND_MAX)ตราบเท่าที่ขนาดของช่วงที่ยอมรับนั้นเป็นผลคูณของ 6 นรกคุณสามารถแบนปฏิเสธผลลัพธ์ใด ๆx>6และคุณไม่ต้องการ%6อีกต่อไป
MSalters

12
ฉันไม่ค่อยพอใจกับคำตอบนี้ การวิ่งเป็นสิ่งที่ดี แต่คุณกำลังไปในทิศทางที่ผิด ตัวอย่างเช่นคุณบ่นว่า“ การสุ่มที่ดีกว่า” ไม่ใช่ศัพท์ทางเทคนิคและไม่มีความหมาย นี่เป็นความจริงครึ่งเดียว ใช่ไม่ใช่คำศัพท์ทางเทคนิค แต่เป็นชวเลขที่มีความหมายอย่างสมบูรณ์ในบริบท การกล่าวเป็นนัยว่าผู้ใช้คำดังกล่าวอาจไม่รู้หรือมุ่งร้ายก็คือหนึ่งในสิ่งเหล่านี้ "การสุ่มที่ดี" อาจยากมากที่จะกำหนดอย่างแม่นยำ แต่ก็ง่ายพอที่จะเข้าใจว่าเมื่อใดที่ฟังก์ชันให้ผลลัพธ์ที่มีคุณสมบัติการสุ่มที่ดีกว่าหรือแย่กว่า
Konrad Rudolph

3
ฉันชอบคำตอบนี้ เป็นเรื่องคุยโว แต่ก็มีข้อมูลพื้นฐานที่ดีมากมาย โปรดทราบว่าผู้เชี่ยวชาญของ REAL เคยใช้เครื่องกำเนิดไฟฟ้าแบบสุ่มฮาร์ดแวร์เท่านั้นปัญหานี้ยาก
Tiger4Hire

10
สำหรับฉันมันกลับกัน แม้ว่าจะมีข้อมูลที่ดี แต่ก็เป็นเรื่องที่คุยโวมากเกินไปที่จะมองว่าเป็นอะไรก็ได้นอกจากความคิดเห็น ประโยชน์กัน
Mr Lister

2

ฉันไม่ใช่ผู้ใช้ C ++ ที่มีประสบการณ์ใด ๆ แต่สนใจที่จะดูว่าคำตอบอื่น ๆ เกี่ยวกับ std::rand()/((RAND_MAX + 1u)/6)การมีอคติน้อยกว่าที่1+std::rand()%6เป็นจริงหรือไม่ ดังนั้นฉันจึงเขียนโปรแกรมทดสอบเพื่อจัดตารางผลลัพธ์สำหรับทั้งสองวิธี (ฉันยังไม่ได้เขียน C ++ มานานแล้วโปรดตรวจสอบ) การเชื่อมโยงสำหรับการเรียกใช้รหัสที่พบที่นี่ นอกจากนี้ยังทำซ้ำดังนี้:

// Example program
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <string>

int main()
{
    std::srand(std::time(nullptr)); // use current time as seed for random generator

    // Roll the die 6000000 times using the supposedly unbiased method and keep track of the results

    int results[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased

        results[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results[n] << ' ';
    }

    std::cout << "\n";


    // Roll the die 6000000 times using the supposedly biased method and keep track of the results

    int results_bias[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::rand()%6;

        results_bias[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results_bias[n] << ' ';
    }
}

จากนั้นฉันก็เอาผลลัพธ์ของสิ่งนี้และใช้chisq.testฟังก์ชันใน R เพื่อรันการทดสอบ Chi-square เพื่อดูว่าผลลัพธ์แตกต่างจากที่คาดไว้อย่างมีนัยสำคัญหรือไม่ คำถาม stackexchange นี้มีรายละเอียดเพิ่มเติมเกี่ยวกับการใช้การทดสอบไคสแควร์เพื่อทดสอบความเป็นธรรมของแม่พิมพ์: ฉันจะทดสอบได้อย่างไรว่าการตายนั้นยุติธรรมหรือไม่ . นี่คือผลลัพธ์สำหรับการวิ่งสองสามครั้ง:

> ?chisq.test
> unbias <- c(100150, 99658, 100319, 99342, 100418, 100113)
> bias <- c(100049, 100040, 100091, 99966, 100188, 99666 )

> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 8.6168, df = 5, p-value = 0.1254

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 1.6034, df = 5, p-value = 0.9008

> unbias <- c(998630, 1001188, 998932, 1001048, 1000968, 999234 )
> bias <- c(1000071, 1000910, 999078, 1000080, 998786, 1001075   )
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.051, df = 5, p-value = 0.2169

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 4.319, df = 5, p-value = 0.5045

> unbias <- c(998630, 999010, 1000736, 999142, 1000631, 1001851)
> bias <- c(999803, 998651, 1000639, 1000735, 1000064,1000108)
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.9592, df = 5, p-value = 0.1585

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 2.8229, df = 5, p-value = 0.7273

ในการรันทั้งสามครั้งที่ฉันทำค่า p สำหรับทั้งสองวิธีจะมากกว่าค่าอัลฟาทั่วไปที่ใช้ในการทดสอบนัยสำคัญเสมอ (0.05) ซึ่งหมายความว่าเราจะไม่ถือว่าทั้งสองคนมีอคติ ที่น่าสนใจคือวิธีการที่ไม่เอนเอียงตามที่คาดการณ์ไว้มีค่า p ที่ต่ำกว่าอย่างต่อเนื่องซึ่งบ่งชี้ว่าจริงๆแล้วมันอาจมีความเอนเอียงมากกว่า ข้อแม้คือฉันวิ่งได้ 3 ครั้งเท่านั้น

UPDATE: ในขณะที่ฉันเขียนคำตอบ Konrad Rudolph ได้โพสต์คำตอบที่ใช้แนวทางเดียวกัน แต่ได้ผลลัพธ์ที่แตกต่างกันมาก ฉันไม่มีชื่อเสียงในการแสดงความคิดเห็นเกี่ยวกับคำตอบของเขาดังนั้นฉันจะพูดถึงที่นี่ ประการแรกสิ่งสำคัญคือรหัสที่เขาใช้ใช้เมล็ดพันธุ์เดียวกันสำหรับเครื่องสร้างตัวเลขสุ่มทุกครั้งที่เรียกใช้ หากคุณเปลี่ยนเมล็ดคุณจะได้ผลลัพธ์ที่หลากหลาย ประการที่สองถ้าคุณไม่เปลี่ยนเมล็ดพันธุ์ แต่เปลี่ยนจำนวนการทดลองคุณก็จะได้ผลลัพธ์ที่หลากหลายเช่นกัน ลองเพิ่มหรือลดตามลำดับขนาดเพื่อดูว่าฉันหมายถึงอะไร ประการที่สามมีการตัดทอนจำนวนเต็มหรือการปัดเศษเกิดขึ้นโดยที่ค่าที่คาดไว้ไม่แม่นยำนัก อาจไม่เพียงพอที่จะสร้างความแตกต่าง แต่มันอยู่ที่นั่น

โดยสรุปแล้วเขาเพิ่งได้เมล็ดพันธุ์ที่ถูกต้องและจำนวนการทดลองที่เขาอาจได้รับผลลัพธ์ที่ผิดพลาด


การดำเนินงานของคุณมีข้อบกพร่องร้ายแรงเนื่องจากความเข้าใจผิดในส่วนของคุณ: ทางเดินยกมาจะไม่เปรียบเทียบกับrand()%6 rand()/(1+RAND_MAX)/6แต่เป็นการเปรียบเทียบการใช้ส่วนที่เหลืออย่างตรงไปตรงมากับการสุ่มตัวอย่างการปฏิเสธ (ดูคำตอบอื่นสำหรับคำอธิบาย) ดังนั้นรหัสที่สองของคุณผิด ( whileลูปไม่ทำอะไรเลย) การทดสอบทางสถิติของคุณก็มีปัญหาเช่นกัน (คุณไม่สามารถเรียกใช้การทดสอบซ้ำ ๆ เพื่อความทนทานคุณไม่ได้ทำการแก้ไข ... )
Konrad Rudolph

1
@KonradRudolph ฉันไม่มีตัวแทนที่จะแสดงความคิดเห็นในคำตอบของคุณดังนั้นฉันจึงเพิ่มเป็นข้อมูลอัปเดตให้กับฉัน นอกจากนี้คุณยังมีข้อบกพร่องร้ายแรงที่เกิดขึ้นกับการใช้ set seed และจำนวนการทดลองทุกครั้งที่ให้ผลลัพธ์ที่ผิดพลาด หากคุณวิ่งซ้ำกับเมล็ดพืชที่แตกต่างกันคุณอาจจับได้ แต่ใช่คุณถูกต้องในขณะที่ลูปไม่ทำอะไรเลย แต่ก็ไม่ได้เปลี่ยนผลลัพธ์ของการบล็อกโค้ดนั้นด้วย
anjama

ฉันวิ่งซ้ำจริง ไม่ได้ตั้งค่าเมล็ดพันธุ์โดยเจตนาเนื่องจากการตั้งค่าเมล็ดพันธุ์แบบสุ่มด้วยstd::srand(และไม่มีการใช้งาน<random>) นั้นค่อนข้างยากที่จะทำในรูปแบบที่เป็นไปตามมาตรฐานและฉันไม่ต้องการให้ความซับซ้อนของมันลดลงจากรหัสที่เหลือ นอกจากนี้ยังไม่เกี่ยวข้องกับการคำนวณ: การทำซ้ำลำดับเดียวกันในการจำลองเป็นสิ่งที่ยอมรับได้ทั้งหมด แน่นอนว่าเมล็ดพันธุ์ที่แตกต่างกันจะให้ผลลัพธ์ที่แตกต่างกันและบางส่วนก็ไม่มีนัยสำคัญ คาดว่าทั้งหมดจะขึ้นอยู่กับวิธีกำหนดค่า p
Konrad Rudolph

1
หนูฉันทำผิดพลาดในการทำซ้ำ และคุณพูดถูกควอนไทล์ที่ 95 ของการรันซ้ำค่อนข้างใกล้เคียงกับ p = 0.05 นั่นคือสิ่งที่เราคาดหวังภายใต้ค่าว่าง โดยสรุปแล้วการใช้ไลบรารีมาตรฐานของฉันstd::randทำให้เกิดการจำลองการโยนเหรียญที่ดีอย่างน่าทึ่งสำหรับ d6 ในช่วงของเมล็ดพันธุ์แบบสุ่ม
Konrad Rudolph

1
นัยสำคัญทางสถิติเป็นเพียงส่วนหนึ่งของเรื่องราว คุณมีสมมติฐานว่าง (กระจายอย่างสม่ำเสมอ) และสมมติฐานทางเลือก (อคติแบบมอดูโล) - จริงๆแล้วคือกลุ่มของสมมติฐานทางเลือกที่จัดทำดัชนีโดยตัวเลือกRAND_MAXซึ่งกำหนดขนาดผลของอคติโมดูโล นัยสำคัญทางสถิติคือความน่าจะเป็นภายใต้สมมติฐานว่างที่คุณปฏิเสธมันอย่างผิด ๆ อะไรคือพลังทางสถิติ - ความน่าจะเป็นภายใต้สมมติฐานทางเลือกที่การทดสอบของคุณปฏิเสธสมมติฐานว่างอย่างถูกต้อง ? คุณจะตรวจพบrand() % 6วิธีนี้หรือไม่เมื่อ RAND_MAX = 2 ^ 31 - 1
Squeamish Ossifrage

2

เราสามารถคิดว่าตัวสร้างตัวเลขสุ่มทำงานบนกระแสของเลขฐานสอง เครื่องกำเนิดไฟฟ้าเปลี่ยนสตรีมเป็นตัวเลขโดยหั่นเป็นชิ้น ๆ หากstd:randฟังก์ชันทำงานร่วมกับRAND_MAX32767 แสดงว่ากำลังใช้ 15 บิตในแต่ละชิ้น

เมื่อเรานำโมดูลที่มีตัวเลขระหว่าง 0 ถึง 32767 มารวมกันพบว่า 5462 '0 และ' 1 แต่มีเพียง 5461 '2,' 3, '4 และ' 5 ดังนั้นผลลัพธ์จึงเอนเอียง ยิ่งค่า RAND_MAX มีค่ามากเท่าใดก็จะมีอคติน้อยลง แต่ก็หลีกเลี่ยงไม่ได้

สิ่งที่ไม่เอนเอียงคือตัวเลขในช่วง [0 .. (2 ^ n) -1] คุณสามารถสร้างจำนวนที่ดีกว่า (ในทางทฤษฎี) ในช่วง 0..5 โดยการแยก 3 บิตแปลงเป็นจำนวนเต็มในช่วง 0..7 และปฏิเสธ 6 และ 7

เราหวังว่าทุกบิตในสตรีมบิตจะมีโอกาสเท่ากันที่จะเป็น '0' หรือ '1' ไม่ว่าจะอยู่ที่ใดในสตรีมหรือค่าของบิตอื่น ๆ นี่เป็นเรื่องยากมากในทางปฏิบัติ การใช้งานซอฟต์แวร์ PRNG ที่แตกต่างกันทำให้เกิดการประนีประนอมที่แตกต่างกันระหว่างความเร็วและคุณภาพ เครื่องกำเนิดไฟฟ้าที่สอดคล้องกันเชิงเส้นเช่นให้std::randความเร็วที่เร็วที่สุดสำหรับคุณภาพต่ำสุด ตัวสร้างการเข้ารหัสมีคุณภาพสูงสุดสำหรับความเร็วต่ำสุด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.