ตัวเลขสุ่มถ่วงน้ำหนัก


104

ฉันกำลังพยายามใช้ตัวเลขสุ่มแบบถ่วงน้ำหนัก ตอนนี้ฉันแค่เอาหัวโขกกำแพงและคิดไม่ออก

ในโครงการของฉัน (Hold'em hand-range, subjective all-in equity analysis) ฉันใช้ฟังก์ชันสุ่มของ Boost สมมติว่าฉันต้องการเลือกตัวเลขสุ่มระหว่าง 1 ถึง 3 (เช่น 1, 2 หรือ 3) เครื่องกำเนิดไฟฟ้า Twister mersenne ของ Boost ทำงานได้อย่างมีเสน่ห์สำหรับสิ่งนี้ อย่างไรก็ตามฉันต้องการให้ตัวเลือกถูกถ่วงน้ำหนักเช่นนี้:

1 (weight: 90)
2 (weight: 56)
3 (weight:  4)

Boost มีฟังก์ชันบางอย่างสำหรับสิ่งนี้หรือไม่?

คำตอบ:


181

มีอัลกอริทึมที่ตรงไปตรงมาสำหรับการเลือกไอเท็มโดยการสุ่มโดยที่ไอเท็มจะมีน้ำหนักเดี่ยว:

1) คำนวณผลรวมของน้ำหนักทั้งหมด

2) เลือกตัวเลขสุ่มที่มีค่า 0 หรือมากกว่าและน้อยกว่าผลรวมของน้ำหนัก

3) ผ่านรายการทีละรายการโดยลบน้ำหนักของพวกเขาออกจากหมายเลขสุ่มของคุณจนกว่าคุณจะได้รับไอเท็มที่จำนวนสุ่มน้อยกว่าน้ำหนักของรายการนั้น

รหัสเทียมที่แสดงสิ่งนี้:

int sum_of_weight = 0;
for(int i=0; i<num_choices; i++) {
   sum_of_weight += choice_weight[i];
}
int rnd = random(sum_of_weight);
for(int i=0; i<num_choices; i++) {
  if(rnd < choice_weight[i])
    return i;
  rnd -= choice_weight[i];
}
assert(!"should never get here");

สิ่งนี้ควรตรงไปตรงมาเพื่อปรับให้เข้ากับคอนเทนเนอร์เพิ่มของคุณและอื่น ๆ


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

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


หากคุณไม่ทราบจำนวนรายการในรายการแสดงว่ามีอัลกอริทึมที่เป็นระเบียบมากที่เรียกว่าการสุ่มตัวอย่างอ่างเก็บน้ำที่สามารถปรับให้มีน้ำหนักได้


3
ในการเพิ่มประสิทธิภาพคุณสามารถใช้น้ำหนักสะสมและใช้การค้นหาแบบไบนารี แต่สำหรับค่าที่แตกต่างกันเพียงสามค่านี้อาจจะมากเกินไป
sellibitze

2
ฉันถือว่าเมื่อคุณพูดว่า "ตามลำดับ" คุณตั้งใจที่จะข้ามขั้นตอนการจัดเรียงล่วงหน้าในอาร์เรย์ choice_weight ใช่หรือไม่?
SilentDirge

2
@Aureis ไม่จำเป็นต้องเรียงอาร์เรย์ ฉันพยายามชี้แจงภาษาของฉันแล้ว
จะ

1
@ จะ: ใช่ แต่มีอัลกอริทึมที่มีชื่อเดียวกัน sirkan.iit.bme.hu/~szirmay/c29.pdfและen.wikipedia.org/wiki/Photon_mapping A Monte Carlo method called Russian roulette is used to choose one of these actionsมันจะปรากฏขึ้นในถังเมื่อ googling มัน "อัลกอริทึมรูเล็ตรัสเซีย" คุณสามารถโต้แย้งได้ว่าคนเหล่านี้ทั้งหมดมีชื่อผิด
v.oddou

3
หมายเหตุสำหรับผู้อ่านในอนาคต: ส่วนที่หักน้ำหนักของพวกเขาออกจากจำนวนสุ่มของคุณนั้นง่ายต่อการมองข้าม แต่สำคัญสำหรับอัลกอริทึม (ฉันตกหลุมพรางเดียวกับ @kobik ในความคิดเห็นของพวกเขา)
Frank Schmitt

48

อัปเดตคำตอบสำหรับคำถามเก่า คุณสามารถทำได้อย่างง่ายดายใน C ++ 11 ด้วย std :: lib:

#include <iostream>
#include <random>
#include <iterator>
#include <ctime>
#include <type_traits>
#include <cassert>

int main()
{
    // Set up distribution
    double interval[] = {1,   2,   3,   4};
    double weights[] =  {  .90, .56, .04};
    std::piecewise_constant_distribution<> dist(std::begin(interval),
                                                std::end(interval),
                                                std::begin(weights));
    // Choose generator
    std::mt19937 gen(std::time(0));  // seed as wanted
    // Demonstrate with N randomly generated numbers
    const unsigned N = 1000000;
    // Collect number of times each random number is generated
    double avg[std::extent<decltype(weights)>::value] = {0};
    for (unsigned i = 0; i < N; ++i)
    {
        // Generate random number using gen, distributed according to dist
        unsigned r = static_cast<unsigned>(dist(gen));
        // Sanity check
        assert(interval[0] <= r && r <= *(std::end(interval)-2));
        // Save r for statistical test of distribution
        avg[r - 1]++;
    }
    // Compute averages for distribution
    for (double* i = std::begin(avg); i < std::end(avg); ++i)
        *i /= N;
    // Display distribution
    for (unsigned i = 1; i <= std::extent<decltype(avg)>::value; ++i)
        std::cout << "avg[" << i << "] = " << avg[i-1] << '\n';
}

ผลลัพธ์ในระบบของฉัน:

avg[1] = 0.600115
avg[2] = 0.373341
avg[3] = 0.026544

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


เพียงบันทึกเตือนความจำในการรวบรวมตัวอย่างนี้: ต้องใช้ C ++ 11 ie ใช้แฟล็กคอมไพเลอร์ -std = c ++ 0x พร้อมใช้งานตั้งแต่ gcc 4.6 เป็นต้นไป
Pete855217

3
สนใจแค่เลือกส่วนที่จำเป็นเพื่อแก้ปัญหาหรือไม่?
จอนนี่

2
นี่เป็นคำตอบที่ดีที่สุด แต่ฉันคิดว่าstd::discrete_distributionแทนที่จะstd::piecewise_constant_distributionดีกว่านี้
แดน

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

15

ถ้าน้ำหนักของคุณเปลี่ยนแปลงช้ากว่าที่วาด C ++ 11 discrete_distributionจะง่ายที่สุด:

#include <random>
#include <vector>
std::vector<double> weights{90,56,4};
std::discrete_distribution<int> dist(std::begin(weights), std::end(weights));
std::mt19937 gen;
gen.seed(time(0));//if you want different results from different runs
int N = 100000;
std::vector<int> samples(N);
for(auto & i: samples)
    i = dist(gen);
//do something with your samples...

อย่างไรก็ตามโปรดทราบว่า c ++ 11 discrete_distributionจะคำนวณผลรวมสะสมทั้งหมดของการเริ่มต้น โดยปกติแล้วคุณต้องการสิ่งนั้นเนื่องจากความเร็วในการสุ่มตัวอย่างสำหรับต้นทุน O (N) เพียงครั้งเดียว แต่สำหรับการกระจายที่เปลี่ยนแปลงอย่างรวดเร็วจะต้องเสียค่าใช้จ่ายในการคำนวณ (และหน่วยความจำ) อย่างหนัก ตัวอย่างเช่นหากน้ำหนักแสดงจำนวนรายการที่มีและทุกครั้งที่คุณวาดหนึ่งชิ้นคุณลบออกคุณอาจต้องการอัลกอริทึมที่กำหนดเอง

คำตอบของ Will https://stackoverflow.com/a/1761646/837451หลีกเลี่ยงค่าใช้จ่ายนี้ แต่จะดึงจาก C ++ 11 ได้ช้ากว่าเนื่องจากไม่สามารถใช้การค้นหาแบบไบนารีได้

หากต้องการดูว่าทำได้คุณสามารถดูบรรทัดที่เกี่ยวข้อง ( /usr/include/c++/5/bits/random.tccบน Ubuntu 16.04 + GCC 5.3 ติดตั้ง):

  template<typename _IntType>
    void
    discrete_distribution<_IntType>::param_type::
    _M_initialize()
    {
      if (_M_prob.size() < 2)
        {
          _M_prob.clear();
          return;
        }

      const double __sum = std::accumulate(_M_prob.begin(),
                                           _M_prob.end(), 0.0);
      // Now normalize the probabilites.
      __detail::__normalize(_M_prob.begin(), _M_prob.end(), _M_prob.begin(),
                            __sum);
      // Accumulate partial sums.
      _M_cp.reserve(_M_prob.size());
      std::partial_sum(_M_prob.begin(), _M_prob.end(),
                       std::back_inserter(_M_cp));
      // Make sure the last cumulative probability is one.
      _M_cp[_M_cp.size() - 1] = 1.0;
    }

10

สิ่งที่ต้องทำเมื่อต้องถ่วงน้ำหนักตัวเลขคือการใช้ตัวเลขสุ่มสำหรับน้ำหนัก

ตัวอย่างเช่นฉันต้องการให้สร้างตัวเลขสุ่มตั้งแต่ 1 ถึง 3 ด้วยน้ำหนักต่อไปนี้:

  • 10% ของตัวเลขสุ่มอาจเป็น 1
  • 30% ของตัวเลขสุ่มอาจเป็น 2
  • 60% ของจำนวนสุ่มอาจเป็น 3

จากนั้นฉันใช้:

weight = rand() % 10;

switch( weight ) {

    case 0:
        randomNumber = 1;
        break;
    case 1:
    case 2:
    case 3:
        randomNumber = 2;
        break;
    case 4:
    case 5:
    case 6:
    case 7:
    case 8:
    case 9:
        randomNumber = 3;
        break;
}

ด้วยวิธีนี้การสุ่มจะมี 10% ของความน่าจะเป็นที่จะเป็น 1, 30% เป็น 2 และ 60% ที่จะเป็น 3

คุณสามารถเล่นได้ตามความต้องการของคุณ

หวังว่าฉันจะช่วยคุณโชคดี!


สิ่งนี้จะออกกฎการปรับการแจกแจงแบบไดนามิก
Josh C

2
แฮ็ก แต่ฉันชอบมัน เหมาะสำหรับต้นแบบด่วนที่คุณต้องการน้ำหนักหยาบ
วาด

1
ใช้ได้กับน้ำหนักที่มีเหตุผลเท่านั้น คุณจะมีช่วงเวลาที่ยากลำบากในการทำมันด้วยน้ำหนัก 1 / pi;)
Joseph Budin

1
@JosephBudin จากนั้นอีกครั้งคุณจะไม่มีทางมีน้ำหนักที่ไร้เหตุผล สวิตช์เคส ~ 4.3 พันล้านควรจะทำได้ดีสำหรับน้ำหนักลอย : D
Jason C

1
ใช่ @JasonC ตอนนี้ปัญหาเล็กลงอย่างไม่มีที่สิ้นสุด แต่ก็ยังเป็นปัญหาอยู่;)
Joseph Budin

3

สร้างกระเป๋า (หรือ std :: vector) ของสิ่งของทั้งหมดที่สามารถหยิบได้
ตรวจสอบให้แน่ใจว่าจำนวนของแต่ละรายการเป็นสัดส่วนกับน้ำหนักของคุณ

ตัวอย่าง:

  • 1 60%
  • 2 35%
  • 3 5%

ดังนั้นมีกระเป๋าที่มี 100 รายการโดยมี 60 1, 35 2 และ 5 3
ตอนนี้สุ่มจัดเรียงกระเป๋า (std :: random_shuffle)

เลือกองค์ประกอบจากกระเป๋าตามลำดับจนกว่าจะว่างเปล่า
เมื่อว่างเปล่าสุ่มกระเป๋าและเริ่มใหม่อีกครั้ง


6
หากคุณมีกระเป๋าหินอ่อนสีแดงและสีน้ำเงินและคุณเลือกหินอ่อนสีแดงจากมันและไม่ได้แทนที่มันเป็นไปได้ที่จะเลือกหินอ่อนสีแดงอื่นยังคงเหมือนเดิมหรือไม่? ในทำนองเดียวกันข้อความของคุณ "เลือกองค์ประกอบจากกระเป๋าตามลำดับจนกว่าจะว่าง" ทำให้เกิดการกระจายที่แตกต่างจากที่ตั้งใจไว้โดยสิ้นเชิง
ldog

@ldog: ฉันเข้าใจข้อโต้แย้งของคุณ แต่เราไม่ได้มองหาการสุ่มที่แท้จริงเรากำลังมองหาการแจกแจงเฉพาะ เทคนิคนี้รับประกันการกระจายที่ถูกต้อง
Martin York

4
ประเด็นของฉันคือคุณสร้างการกระจายไม่ถูกต้องตามข้อโต้แย้งก่อนหน้าของฉัน ลองพิจารณาตัวอย่างตัวนับง่ายๆสมมติว่าคุณมีอาร์เรย์เป็น 3 โดย1,2,2ผลิต 1 1/3 ของเวลาและ 2 2/3 สุ่มอาร์เรย์เลือกอันแรกสมมติว่าเป็น 2 ตอนนี้องค์ประกอบถัดไปที่คุณเลือกตามการแจกแจงของ 1 1/2 ของเวลาและ 2 1/2 เวลา เข้าใจ?
ldog

0

เลือกตัวเลขสุ่มบน [0,1) ซึ่งควรเป็นตัวดำเนินการเริ่มต้น () สำหรับบูสต์ RNG เลือกรายการที่มีฟังก์ชันความหนาแน่นของความน่าจะเป็นสะสม> = จำนวนนั้น:

template <class It,class P>
It choose_p(It begin,It end,P const& p)
{
    if (begin==end) return end;
    double sum=0.;
    for (It i=begin;i!=end;++i)
        sum+=p(*i);
    double choice=sum*random01();
    for (It i=begin;;) {
        choice -= p(*i);
        It r=i;
        ++i;
        if (choice<0 || i==end) return r;
    }
    return begin; //unreachable
}

โดยที่ random01 () ส่งคืนค่า double> = 0 และ <1 โปรดทราบว่าข้างต้นไม่ต้องการให้ความน่าจะเป็นในการรวมเป็น 1 มันทำให้พวกเขาเป็นปกติสำหรับคุณ

p เป็นเพียงฟังก์ชันกำหนดความน่าจะเป็นให้กับรายการในคอลเลกชัน [เริ่มต้นสิ้นสุด) คุณสามารถละเว้น (หรือใช้ข้อมูลประจำตัว) ได้หากคุณมีลำดับความน่าจะเป็น


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