Случайное число с вероятностями в C# - PullRequest
0 голосов
/ 01 апреля 2020

Я преобразовал эту Java программу в C# программу.

using System;
using System.Collections.Generic;

namespace RandomNumberWith_Distribution__Test
{
    public class DistributedRandomNumberGenerator
    {

        private Dictionary<Int32, Double> distribution;
        private double distSum;

        public DistributedRandomNumberGenerator()
        {
            distribution = new Dictionary<Int32, Double>();
        }

        public void addNumber(int val, double dist)
        {
            distribution.Add(val, dist);// are these two
            distSum += dist;            // lines correctly translated?
        }

        public int getDistributedRandomNumber()
        {
            double rand = new Random().NextDouble();//generate a double random number
            double ratio = 1.0f / distSum;//why is ratio needed?
            double tempDist = 0;

            foreach (Int32 i in distribution.Keys)
            {
                tempDist += distribution[i];

                if (rand / ratio <= tempDist)//what does "rand/ratio" signify? What does this comparison achieve?
                {
                    return i;
                }
            }
            return 0;
        }
    }

    public class MainClass
    {
        public static void Main(String[] args)
        {
            DistributedRandomNumberGenerator drng = new DistributedRandomNumberGenerator();
            drng.addNumber(1, 0.2d);
            drng.addNumber(2, 0.3d);
            drng.addNumber(3, 0.5d);

            //================= 
            // start simulation
            int testCount = 1000000;
            Dictionary<Int32, Double> test = new Dictionary<Int32, Double>();

            for (int i = 0; i < testCount; i++)
            {
                int random = drng.getDistributedRandomNumber(); 

                if (test.ContainsKey(random)) 
                {
                    double prob = test[random];   // are these
                    prob = prob + 1.0 / testCount;// three lines
                    test[random] = prob;          // correctly translated?
                }
                else
                {
                    test.Add(random, 1.0 / testCount);// is this line correctly translated?
                }
            }

            foreach (var item in test.Keys)
            {
                Console.WriteLine($"{item}, {test[item]}");
            }

            Console.ReadLine();
        }
    }
}

У меня есть несколько вопросов:

  1. Можете ли вы проверить если помеченные комментариями строки верны или нуждаются в объяснении?
  2. Почему getDistributedRandomNumber() не проверяет, равна ли сумма распределения 1, прежде чем переходить к дальнейшим вычислениям?

1 Ответ

1 голос
/ 02 апреля 2020

Метод

public void addNumber(int val, double dist)

неправильно переведен, так как отсутствуют следующие строки:

if (this.distribution.get(value) != null) {
    distSum -= this.distribution.get(value);
}

Эти строки должны охватывать случай, когда вы вызываете следующее (на основе ваш пример кода):

DistributedRandomNumberGenerator drng = new DistributedRandomNumberGenerator();
drng.addNumber(1, 0.2d);
drng.addNumber(1, 0.5d);

Таким образом, дважды вызывая метод addNumber с одним и тем же первым аргументом, отсутствующая часть кода ищет, если первый аргумент уже присутствует в словаре, и если это так, он удалит «старое» значение из словаря для вставки нового значения.

Ваш метод должен выглядеть следующим образом:

public void addNumber(int val, double dist)
{
    if (distribution.TryGetValue(val, out var oldDist)) //get the old "dist" value, based on the "val"
    {
        distribution.Remove(val); //remove the old entry
        distSum -= oldDist; //substract "distSum" with the old "dist" value
    }

    distribution.Add(val, dist); //add the "val" with the current "dist" value to the dictionary
    distSum += dist; //add the current "dist" value to "distSum"
}

Теперь перейдем ко второму методу

public int getDistributedRandomNumber()

Вместо вызова инициализации нового экземпляра Random каждый раз, когда вызывается этот метод, вам следует инициализировать его только один раз, поэтому измените строку

double rand = new Random().NextDouble();

на эту

double rand = _random.NextDouble();

и инициализировать поле _random вне метода внутри объявления класса, как это

public class DistributedRandomNumberGenerator
{
    private Dictionary<Int32, Double> distribution;
    private double distSum;
    private Random _random = new Random();        


    ... rest of your code
}

Это будет препятствовать тому, чтобы new Random().NextDouble() производил одно и то же число снова и снова при вызове в al oop. Вы можете прочитать об этой проблеме здесь: Генератор случайных чисел, генерирующий только одно случайное число

Как я отмечаю, поля в c# названы с префиксом подчеркивания. Вы должны рассмотреть переименование distribution в _distribution, то же самое относится к distSum.


Далее:

double ratio = 1.0f / distSum;//why is ratio needed?

Соотношение необходимо, потому что метод пытается сделать все возможное, чтобы сделать его работа с информацией, которую вы предоставили, представьте, что вы называете это только:

DistributedRandomNumberGenerator drng = new DistributedRandomNumberGenerator();
drng.addNumber(1, 0.2d);
int random = drng.getDistributedRandomNumber(); 

С этими строками вы сказали классу, что вы хотите, чтобы число 1 в 20% дел, но что о другом 80%?

И вот здесь возникает переменная отношения, она вычисляет сопоставимое значение на основе суммы вероятностей, которые вы дали. например.

double ratio = 1.0f / distSum;

Как и в последнем примере, drng.addNumber(1, 0.2d); distSum будет 0.2, что соответствует вероятности 20%.

double ratio = 1.0f / 0.2;

. 5.0, с вероятностью 20% отношение равно 5, потому что 100% / 5 = 20%.

Теперь давайте посмотрим, как код реагирует, когда отношение равно 5

double tempDist = 0;
foreach (Int32 i in distribution.Keys)
{
    tempDist += distribution[i];

    if (rand / ratio <= tempDist)
    {
        return i;
    }
}

rand будет в любое время значение, которое больше или равно 0,0 и меньше 1,0. Вот как работает NextDouble, поэтому давайте предположим, что 0.254557522132321 будет rand.

Теперь давайте посмотрим, что происходит шаг за шагом

double tempDist = 0; //initialize with 0 
foreach (Int32 i in distribution.Keys) //step through the added probabilities
{
    tempDist += distribution[i]; //get the probabilities and add it to a temporary probability sum

    //as a reminder
    //rand = 0.254557522132321
    //ratio = 5
    //rand / ratio = 0,0509115044264642
    //tempDist = 0,2
    // if will result in true
    if (rand / ratio <= tempDist)
    {
        return i;
    }
}

Если бы мы не применяли отношение, то if было бы ложным, но это было бы неправильно, поскольку у нас есть только одно значение внутри нашего словарь, так что независимо от того, какое значение rand может быть, оператор if должен возвращать true, и это природный смысл rand / ratio.

. Чтобы "исправить" случайно сгенерированное число на основе суммы вероятностей, которые мы добавили , rand / ratio будет полезен только в том случае, если вы не предоставили вероятности, которые в сумме составляют 1 = 100%.

например. если ваш пример будет таким

DistributedRandomNumberGenerator drng = new DistributedRandomNumberGenerator();
drng.addNumber(1, 0.2d);
drng.addNumber(2, 0.3d);
drng.addNumber(3, 0.5d);

Вы можете видеть, что предоставленные вероятности равны 1 => 0.2 + 0.3 + 0.5, в этом случае строка

if (rand / ratio <= tempDist)

будет выглядеть так это

if (rand / 1 <= tempDist)

Деление на 1 никогда не изменит значения и rand / 1 = rand, поэтому единственным вариантом использования этого отклонения являются случаи, когда вы не указали идеальную 100% вероятность, может быть и больше или меньше.

В качестве примечания я бы предложил изменить ваш код на этот

//call the dictionary distributions (notice the plural)
//dont use .Keys
//var distribution will be a KeyValuePair
foreach (var distribution in distributions)
{
    //access the .Value member of the KeyValuePair
    tempDist += distribution.Value;

    if (rand / ratio <= tempDist)
    {
        return i;
    }
}

Кажется, что ваша процедура тестирования переведена правильно.

...