Miroslav Holec
Premium

Náhodná čísla a třída Random

Miroslav Holec   10. ledna 2015  update 29. března 2016

Tento článek byl napsán v roce 2015. Vývojářské technologie se neustále inovují a článek již nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

Zaujaly mě některé otázky na Stack Overflow, které souvisí s tím, nakolik jsou čísla generovaná pomocí třídy Random v .NET náhodná. Protože je toto téma celkem často opírané, rozhodl jsem se k tomu napsat několik vlastních poznatků, respektive faktů.

Třída Random

Třída Random je tu s náma už od verze .NET frameworku 1.1., nicméně použitelná tak jak ji zhruba známe je od verze 2.0. Technicky se jedná o generátor pseudo-náhodných čísel. Algoritmus napsal prof. D. E. Knuth z Harvardu a je založen na principu lineárního kongruenčního generátoru. Ten tvoří náhodná čísla na základě vzorce s pomocí modulo a dalších zvolených konstant. Obecný předpis takového generátoru je:

xi+1 = axi+c (mod m)

Algoritmus D. E. Knutha používá pro konstanty různá magická čísla. Jedním z nich, které můžeme v implementaci najít je například poměrové číslo označované jako Golden Ratio, česky "Zlatý řez". Algoritmem se nemá smysl dále zabývat, jelikož je důkladně popsán v příručce The Art of Computer Programming, Volume 2: Seminumerical Algorithms

Instance třídy

Třída Random má dva konstruktory. Obvykle vývojář použije ten bezparametrický, který implicitně použije hodnotu Environment.TickCount a předá ji jako hodnotu do druhého konstruktoru Random(int seed). Environment.TickCount vrací čas v milisekundách od spuštění systému. Pokud bychom si nechali v cyklu vypsat hodnotu této property, zjistíme, že cyklus je tak rychlý, že property bude vracet na konzoli často stejná čísla. A to dokonce s použitím uspání vlákna, např.:

for (int i = 0; i < 20; i++)
{
    Thread.Sleep(1);
    Console.WriteLine("{0}: {1}", i, Environment.TickCount);
}

To má za následek fakt, že pokud bychom měli potřebu generovat náhodná čísla bez udání seedu pomocí dvou generátorů, jako nevhodná implementace se jeví:

Random random1 = new Random();
Random random2 = new Random();

S vysokou pravděpodobností takto vytvoříme dva generátory, které mají stejný seed. Správné řešení je použití vlastního seedu, například s pomocí Guid.

Random random = new Random(Guid.NewGuid().GetHashCode());

Seed je defacto nějaký výchozí prvek, od kterého jsou náhodná čísla deterministicky generována. Jinými slovy, pokud vytvoříme instanci třídy Random a předáme jako seed například číslo 99, vždy se bude generovat stejná číselná řada (za předpokladu stejného intervalu při opakovaném volání metody Next(int, int)).

Pro zajímavost (a z logiky věci) - pokud bychom se stejným seed generovali čísla o vyšším rozsahu ale s minimálním rozdílem horní hodnoty, získali bychom dvě velmi podobné řady náhodných čísel. Např.:

Random random1 = new Random(99);
Random random2 = new Random(99);

for (int i = 0; i < 10; i++)
{
    Console.WriteLine("R1: {0}", random1.Next(1,500));
    Console.WriteLine("R2: {0}", random2.Next(1,501));

    Console.WriteLine();
}

Nakonec ještě poznamenám, že vložené číslo seed není konečné, právě toto číslo je dále dopočítáno s pomocí Golden Ratio.

Next(int, int)

Volání metody Next(int minValue, int maxValue) vrátí další náhodné číslo ve stanoveném rozsahu. Zajímavé je, že hodnota maxValue udává ve skutečnosti horní hranici o jednotku menší, tedy maxValue - 1.

Náhodné nebo ne?

Stále jsme se nedostali k tomu, zda jsou generované řady náhodné. Mohl bych napsat ano, stejně jako je to uvedeno v MSDN nebo v práci prof. D. E. Knutha ale pro zajímavost můžeme udělat pár testů. Aby byl test objektivní, je ideální pracovat s 1000 náhodnými čísly a vyzkoušet různé hodnoty pro seed. Tu práci jsem si již dal, proto jen demonstruji výsledky například na seed = 99 a pro interval <0;50>. Testy nebudu popisovat a opět jen odkážu na zdroje.

Kolmogorov-Smirnov test

Kritická hodnota pro test při hladině významnosti 0,05 pro 1000 prvků je 0,0430. Generovaná řada byla seskupena dle četností a testové kritérium Dn = 0,024. Protože testové kritérium nedosáhlo kritické hodnoty, nezamítáme nulovou hypotézu o shodě prvků a lze říct, že generovaná řada náhodných čísel splňuje podmínky pro statistickou náhodnost na hladině významnosti 0,05.

Empirická a teoretická kumulativní četnost

Na dalším obrázku je vidět rozložení odchylek pro všechny hodnoty v generovaném intervalu (jako absolutní číslo).

Odchylky pro generovaný rozsah

Pro zajímavost, podobně dopadly i testy pro další zvolené hodnoty pro seed.

					D  0.043
seed    5			Dn 0.025
seed    99			Dn 0.024
seed    256			Dn 0.021
seed    314			Dn 0.028
seed    1024		Dn 0.017
seed    102254		Dn 0.030
seed    256345		Dn 0.028
seed    256346		Dn 0.018
seed    256347		Dn 0.018
seed    999999		Dn 0.018

Chí-kvadrát test (shoda rovnoměrného rozd.)

Opět je nalezena kritická hodnota testu pro k-1 prvků (četnosti 0-50). D = 46,214 a dopočteno je testové kritérium Dn = 37,034. Opět testové kritérium nedosáhlo kritické hodnoty a můžeme tedy učinit stejný závěr jako v předchozím případě (stejná hladina významnosti). Pro zajímavost jen výsledky pro další seed hodnoty:

					D  37,034
seed	5			Dn 52,538
seed	99			Dn 37,034
seed	256			Dn 40,910
seed	314			Dn 56,720
seed	1024		Dn 44,378
seed	102254		Dn 50,192
seed	256345		Dn 48,254
seed	256346		Dn 53,558
seed	256347		Dn 48,050
seed	999999		Dn 46,214

Další testy

Vynechal jsem lokální testy, které by měly ještě potvrdit náhodnost v případě rozdělení generované řady na menší části. Udělal jsem ale nad celou řadu i testy bodů zvratu a znamének diferencí. Výsledky dopadly dle očekávání:

Test bodů zvratu
n = 1000
P = 644 (počet diferencí)
U = 1,96
Un = -1,6015
U > |Un| (potvrzena náhodnost)

Test znamének diferencí
n = 1000
C = 496 (počet bodů růstu)
U = 1,96
Un = -0,38321
U > |Un| (potvrzena náhodnost)

Pár hodů kostkou

Nakonec jsem se rozhodl ještě vyzkoušet, jak třída Random obstojí pro simulaci hodu kostkou. Kdybychom měli teoreticky 100% vyváženou kostku a učinili bychom dostatečné množství pokusů (konečné číslo blízké ∞), prakticky by počet hodů každého čísla měl být velmi blízko teoretické hodnotě pravděpodobnosti. Pravděpodobnost, že padne jakékoliv číslo na kostce v rámci jednoho hodu je samozřejmě 1/6 (cca 0,166666...).

Generované rozsahy čísel odpovídaly předpokladu. Diference minimální a maximální hodnoty se pohybovala mezi 0,022 a 0,028 pro různě zvolené seed. Průměrná odchylka od očekávané hodnoty byla v rozmezí 0,0052222 a 0,010889. To všechno při 1000 iteracích (náhodných pokusech).

Následně jsem test provedl na 1 milionu iterací se zajímavými výsledky. Diference min a max hodnoty byla menší než 0,0014 a průměrná odchylka se od očekávané hodnoty zmenšila na cca 0,00024 až 0,00039 pro různé seed hodnoty. Se zvyšováním počtu iterací se tedy průměrná odychylka zmenšuje. Na obrázku níže můžete vidět závislost lineárně se zvyšujícího počtu iterací a chování průměrné odchylky pro počet iterací od 1000 do 1 mil.

Závislost průměrné odchylky na počtu iterací

Když vybereme větší kroky v počtu iterací, je zpřesnění zřejmé:

iterations	avg d.
10			0,116666667
100			0,015
1000		0,005055556
10000		0,002883333
100000		0,001390556
1000000		0,000240611
10000000	8,18E-05
100000000	2,34E-05

Závěry a shrnutí

V článku jsem nastínil jakým způsobem funguje třída Random. Splňuje základní předpoklady statistické náhodnosti, což jsme pro zajímavost otestovali několika testy na zvolené hladině významnosti. Pokud chceme třídu Random použít, je potřeba hlídat si horní hodnotu v metodě Next(int, int), která ve skutečnosti odpovídá max-1 (což je jeden z mála code smell ve třídě). Pro generování náhodných řad doporučuji používat vlastní seed, buď vytvořený náhodně pomocí hashe z Guid.NewGuid() nebo s vlastní hodnotou.