Подтвердить что ты не робот

.NET Framework: генератор случайных чисел создает повторяющийся шаблон

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

Кажется, я обнаружил повторяющийся шаблон в числах, сгенерированных классом System.Random. Я использую "мастер" случайный экземпляр для генерации семени для второго "основного" случайного экземпляра. Значения, полученные в этом основном случайном экземпляре, имеют повторяющийся шаблон. В частности, произведенное 3-е число очень предсказуемо.

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

using System;

class Program
{
    static void Main(string[] args)
    {
            // repeat experiment with different master RNGs
        for (int iMaster = 0; iMaster < 30; ++iMaster)
        {
                // create master RNG
            var rngMaster = new Random(iMaster + OFFSET);

                // obtain seed from master RNG
            var seed = rngMaster.Next();

                // create main RNG from seed
            var rngMain = new Random(seed);

                // print 3rd number generated by main RNG
            var ignore0 = rngMain.Next(LIMIT);
            var ignore1 = rngMain.Next(LIMIT);
            var randomNumber = rngMain.Next(LIMIT);
            Console.WriteLine(randomNumber);
        }
    }

    const int OFFSET = 0;
    const int LIMIT = 200;
}

Я думаю, что это должно произвести произвольный вывод, но фактический вывод на моем ящике:

84
84
84
84
84
84
84
84
84
84
84
...

Может ли кто-нибудь объяснить, что здесь происходит? Изменение констант OFFSET и LIMIT изменяет выходное значение, но оно всегда повторяется.

4b9b3361

Ответ 1

Добро пожаловать в мир не криптографически сильных ГСЧ. По-видимому, встроенный RNG.NET имеет тенденцию делать 3-й номер, который он выдает 84, если вы ограничиваете его на 0 до 200 для своих выходов. Взгляните на следующую версию программы, она показывает больше того, что происходит на выходе.

class Program
{
    static void Main(string[] args)
    {
        Console.WindowWidth = 44;
        Console.WindowHeight = 33;
        Console.BufferWidth = Console.WindowWidth;
        Console.BufferHeight = Console.WindowHeight;

        string template = "|{0,-5}|{1,-11}|{2,-5}|{3,-5}|{4,-5}|{5,-5}|";
        Console.WriteLine(template, "s1", "s2", "out1", "out2", "out3", "out4");
        Console.WriteLine(template, new String('-', 5), new String('-', 11), new String('-', 5), new String('-', 5), new String('-', 5), new String('-', 5));

        // repeat experiment with different master RNGs
        for (int iMaster = 0; iMaster < 30; ++iMaster)
        {
            int s1 = iMaster + OFFSET;
            // create master RNG
            var rngMaster = new Random(s1);

            // obtain seed from master RNG
            var s2 = rngMaster.Next();

            // create main RNG from seed
            var rngMain = new Random(s2);

            var out1 = rngMain.Next(LIMIT);
            var out2 = rngMain.Next(LIMIT);
            var out3 = rngMain.Next(LIMIT);
            var out4 = rngMain.Next(LIMIT);
            Console.WriteLine(template, s1, s2, out1, out2, out3, out4);
        }

        Console.ReadLine();
    }

    const int OFFSET = 0;
    const int LIMIT = 200;
}

Вот вывод

|s1   |s2         |out1 |out2 |out3 |out4 |
|-----|-----------|-----|-----|-----|-----|
|0    |1559595546 |170  |184  |84   |84   |
|1    |534011718  |56   |177  |84   |123  |
|2    |1655911537 |142  |171  |84   |161  |
|3    |630327709  |28   |164  |84   |199  |
|4    |1752227528 |114  |157  |84   |37   |
|5    |726643700  |0    |150  |84   |75   |
|6    |1848543519 |86   |143  |84   |113  |
|7    |822959691  |172  |136  |84   |151  |
|8    |1944859510 |58   |129  |84   |189  |
|9    |919275682  |144  |122  |84   |28   |
|10   |2041175501 |30   |115  |84   |66   |
|11   |1015591673 |116  |108  |84   |104  |
|12   |2137491492 |2    |102  |84   |142  |
|13   |1111907664 |88   |95   |84   |180  |
|14   |86323836   |174  |88   |84   |18   |
|15   |1208223655 |60   |81   |84   |56   |
|16   |182639827  |146  |74   |84   |94   |
|17   |1304539646 |31   |67   |84   |133  |
|18   |278955818  |117  |60   |84   |171  |
|19   |1400855637 |3    |53   |84   |9    |
|20   |375271809  |89   |46   |84   |47   |
|21   |1497171628 |175  |40   |84   |85   |
|22   |471587800  |61   |33   |84   |123  |
|23   |1593487619 |147  |26   |84   |161  |
|24   |567903791  |33   |19   |84   |199  |
|25   |1689803610 |119  |12   |84   |38   |
|26   |664219782  |5    |5    |84   |76   |
|27   |1786119601 |91   |198  |84   |114  |
|28   |760535773  |177  |191  |84   |152  |
|29   |1882435592 |63   |184  |84   |190  |

Таким образом, существуют некоторые сильные корреляции между первым выходом основного RND и первыми несколькими выходами второго RNG, который был скован первым. Random RNG не предназначен для "безопасного", он разработан как "быстрый", поэтому такие вещи, как то, что вы видите здесь, являются компромиссами между быстрым и безопасным. Если вам не нужны такие вещи, вам нужно использовать криптографически безопасный генератор случайных чисел.

Однако просто переключение на криптографический генератор случайных чисел (CRNG) недостаточно, вам все равно нужно быть осторожным, как вы используете CRNG. Очень похожая проблема произошла с беспроводной безопасностью WEP. В зависимости от того, что было указано в заголовке, можно было предсказать, какое значение для начального значения (ключ WEP) для генератора случайных чисел использовалось для защиты соединения. Хотя они использовали CRNG (они использовали RC4), они не использовали его правильно (вам нужно выплюнуть несколько 1000 итераций, прежде чем выход станет не предсказуемым).

Ответ 2

После запуска кода он выглядит как возвращаемое значение для третьего элемента - независимо от того, что семя является довольно плохим недостатком. Я изменил ваш код следующим образом, чтобы сделать его немного более гибким:

public static void TestRNG()
{
    const int OFFSET = 0;
    const int LIMIT = 200;
    const int RndCount = 50;
    const int FieldsPerLine = 10;
    int Rnd;
    for (int iMaster = 0; iMaster < RndCount; ++iMaster)
    {
        // create master RNG
        var rngMaster = new Random(iMaster + OFFSET);

        // obtain seed from master RNG
        var seed = rngMaster.Next();

        // create main RNG from seed
        var rngMain = new Random(seed);

        // print 3rd number generated by main RNG
        Console.WriteLine();
        for (int Loop = 0; Loop < FieldsPerLine; Loop++)
        {
            Rnd = rngMain.Next(LIMIT);
            Console.Write(Rnd.ToString().PadLeft(3) + " ");
        }
    }
}

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

170 184  84  84   5 104 164 113 181 147
 56 177  84 123 150 132 149  56 142  88
142 171  84 161  94 160 134 199 102  28
 28 164  84 199  39 189 119 141  62 168
114 157  84  37 184  17 105  84  23 108
  0 150  84  75 129  45  90  27 183  48
 86 143  84 113  74  74  75 169 144 188
172 136  84 151  18 102  60 112 104 129
 58 129  84 189 163 130  46  55  64  69
144 122  84  28 108 159  31 197  25   9
 30 115  84  66  53 187  16 140 185 149
116 108  84 104 198  15   1  82 145  89
  2 102  84 142 142  44 186  25 106  29
 88  95  84 180  87  72 172 168  66 170
174  88  84  18  32 100 157 110  26 110
 60  81  84  56 177 129 142  53 187  50
146  74  84  94 121 157 127 196 147 190
 31  67  84 133  66 185 113 138 108 130
117  60  84 171  11  14  98  81  68  70
  3  53  84   9 156  42  83  24  28  11
 89  46  84  47 101  70  68 166 189 151
175  40  84  85  45  99  54 109 149  91
 61  33  84 123 190 127  39  51 109  31
147  26  84 161 135 155  24 194  70 171
 33  19  84 199  80 184   9 137  30 112
119  12  84  38  25  12 195  79 190  52
  5   5  84  76 169  40 180  22 151 192
 91 198  84 114 114  69 165 165 111 132
177 191  84 152  59  97 150 107  71  72
 63 184  84 190   4 125 136  50  32  12
149 177  84  28 148 154 121 193 192 153
 35 171  84  66  93 182 106 135 153  93
121 164  84 104  38  10  91  78 113  33
  7 157  84 143 183  39  77  20  73 173
 93 150  84 181 128  67  62 163  34 113
179 143  84  19  72  95  47 106 194  53
 65 136  84  57  17 124  32  48 154 194
151 129  84  95 162 152  18 191 115 134
 37 122  84 133 107 180   3 134  75  74
123 115  84 171  52   9 188  76  35  14
  9 108  84  10 196  37 173  19 196 154
 95 102  84  48 141  65 158 162 156  95
181  95  84  86  86  94 144 104 117  35
 66  88  84 124  31 122 129  47  77 175
152  81  84 162 176 150 114 189  37 115
 38  74  84   0 120 179  99 132 198  55
124  67  84  38  65   7  85  75 158 195
 10  60  84  76  10  35  70  17 118 136
 96  53  84 115 155  64  55 160  79  76
182  46  84 153  99  92  40 103  39  16

В прошлом я видел примеры кода, которые не используют первые 3 или 4 значения, возвращаемые методом Random.Next. Теперь я знаю, почему. Таким образом, простая работа заключается в том, чтобы выбросить первые 4 значения, возвращенные методом Random.Next.

Если вас интересует очень быстрый генератор случайных чисел, который также производит высококачественные случайные числа, тогда просмотрите проект TinyMT - который я портирован из исходного кода C.

Ответ 3

Это всего лишь комментарий, но ему нужно пространство. Я вручную добавил отступы и комментарии к коду DotLisp только в текстовом поле SO. Код идентичен, за исключением того, использует ли он (.Next (Random. i)) как семя, или просто использует i как само семя и проверяет ли он третий или четвертый случайный случай .Next.

Я не проверял снова прямо сейчас, но я считаю, что каждый .Next x всегда извлекает один новый образец и преобразует результат в нечто между 0 и x-1.

Использование x = (* 15 183) = 2745 произошло потому, что, глядя на меньшие диапазоны и больше похожие на семена 10000, я обнаружил, что третий .Next x был "равномерным" случайным, но с двумя скоростями "равномерности"; меньший диапазон меньших выбранных значений составлял от 177 до 190 смежных чисел. (Вы можете увидеть это, вызвав (print-histo h) в последней строке, но уменьшите количество тестируемых семян:-)) Когда я увеличил количество семян, я увеличил этот диапазон.

Код просто накапливает счетчик для каждого результата .Next x и отображает счетчик этих счетчиков. Для истинного равномерного случайного .Next x это должно привести к хорошей кривой колокола, как видно из последнего случая (четвертый .Next, последовательные семена).

(let (h (Hashtable.))
 (dotimes i 6553600
  (lets
   (s (.Next (Random. i))
    r (Random. s)) ; using random seed
   (dotimes j 2 r.Next) ; skipping 2 results
   (let (x (r.Next (* 15 183)))
    (x h (+ 1 (or (x h) 0))))))
 (print-histo (histo h.Values)))
  1 2368
  3 2369
 11 2370
 20 2371
 11 2372
 12 2373
 17 2374
 15 2375
  8 2376
 13 2377
 20 2378
 11 2379
  3 2380
  8 2382
 94 2383
253 2384
296 2385
240 2386
248 2387
238 2388
233 2389
252 2390
236 2391
321 2392
163 2393
 18 2394

Скошенная кривая колокола и еще одна небольшая плоская кривая колокола или просто очень неравномерный хвост.

(let (h (Hashtable.))
 (dotimes i 6553600
  (lets
   (s (.Next (Random. i))
    r (Random. s)) ; using random seed
   (dotimes j 3 r.Next) ; skipping 3 results
   (let (x (r.Next (* 15 183)))
    (x h (+ 1 (or (x h) 0))))))
 (print-histo (histo h.Values)))
 11 2377
 43 2378
 90 2379
114 2380
138 2381
133 2382
132 2383
143 2384
127 2385
147 2386
130 2387
136 2388
145 2389
223 2390
430 2391
354 2392
177 2393
 72 2394

Две кривые колокола с одной широкой и одной узкой.

(let (h (Hashtable.))
 (dotimes i 6553600
  (let (r (Random. i)) ; using sequential seed
   (dotimes j 2 r.Next) ; skipping 2 results
   (let (x (r.Next (* 15 183)))
    (x h (+ 1 (or (x h) 0))))))
  (print-histo (histo h.Values)))
 12 2380
104 2381
143 2382
123 2383
106 2384
 55 2385
115 2386
387 2387
511 2388
537 2389
454 2390
194 2391
  4 2392

Эффективно две кривые колокола.

(let (h (Hashtable.))
 (dotimes i 6553600
  (let (r (Random. i)) ; using sequential seed
   (dotimes j 3 r.Next) ; skipping 3 results
   (let (x (r.Next (* 15 183)))
   (x h (+ 1 (or (x h) 0))))))
 (print-histo (histo h.Values)))
 18 2384
154 2385
432 2386
758 2387
798 2388
477 2389
105 2390
  3 2391

Наконец, простая кривая колокола.

В целом, похоже, что существуют два отдельных вопроса: третий образец не очень однородный вообще, а семена, созданные в первом образце, либо подчеркивают проблему, либо показывают отдельную проблему.