Implementere Mersenne Twister
Introduksjon
Python sin innebygde random-modul bruker generatoren Mersenne Twister (MT19937). Dersom dere er ferdig med den andre oppgaven og ønsker å lage en robust generator kan dere prøve dere på denne. Implementer MersenneTwister-klassen i mersenne.py, og kontroller at den gir de samme tallene som Python. Dere kan bruke plott.py for å sjekke om denne utgaven har de samme egenskapene som LCG-generatoren.
Kort om Mersenne Twister
En LCG har bare ett tall som tilstand. Det gjør den rask, men også lett å se mønstre i. Mersenne Twister har i stedet en tilstand på 624 tall på 32 bit hver. Der perioden til LCG er maksimalt størrelsen på modulusen, har denne en periode på 2^19937 - 1 steg (et Mersenne-primtall, derav navnet). Generatoren fungerer slikt:
- Initialisering: Frøet fyller tilstanden
mt[0..623], hvor hvert tall regnes ut fra det forrige. - Vri (twist): Når alle de 624 tallene er laget opp, regnes hele tilstanden ut på nytt ved å blande hvert tall med to andre i listen.
- Temper: Hvert tall som hentes ut, blandes litt med bitskift og
xorfor å forbedre fordelingen av bitene.
Dere trenger bare disse operasjonene på heltall:
| Operasjon | Python | Forklaring |
|---|---|---|
| Skift til høyre / venstre | x >> n, x << n | Flytter bitene n plasser |
| Bitvis og | x & maske | Beholder bare bitene som er 1 i maske |
| Bitvis xor | x ^ y | 1 der bitene er ulike |
| Begrense til 32 bit | x & 0xFFFFFFFF | Python har uendelig store heltall, så vi må kappe selv |
Konstanter
N = 624
M = 397
A = 0x9908B0DF
F = 1812433253
OVRE_MASKE = 0x80000000 # bit 31
NEDRE_MASKE = 0x7FFFFFFF # bit 0-30
Algoritmen
Initialisering med frø froe:
mt[0] = froe
mt[i] = (F * (mt[i-1] xor (mt[i-1] >> 30)) + i) % 2^32 for i = 1 ... 623
Vri (kjøres når alle 624 tall er brukt):
for i = 0 ... 623:
y = (mt[i] & OVRE_MASKE) | (mt[(i+1) mod 624] & NEDRE_MASKE)
mt[i] = mt[(i+M) mod 624] xor (y >> 1)
hvis y er oddetall: mt[i] = mt[i] xor A
Temper av neste tall y = mt[indeks]:
y = y xor (y >> 11)
y = y xor ((y << 7) & 0x9D2C5680)
y = y xor ((y << 15) & 0xEFC60000)
y = y xor (y >> 18)
Klassegrensesnitt
| Metode | Parametere | Resultat |
|---|---|---|
__init__ | froe | Setter modulus = 1 << 32, lager tilstanden og setter indeks = 624 |
_vri | Ingen | Regner ut hele tilstanden på nytt og setter indeks = 0 |
rand | Ingen | Vrir hvis indeks >= 624, returnerer neste tempererte 32-bits heltall |
random | Ingen | Returnerer et flyttall i [0, 1) |
randint | gulv, tak | Returnerer et heltall i [gulv, tak] |
from mersenne import MersenneTwister
generator = MersenneTwister(5489)
print(generator.rand()) # 3499211612
print(generator.rand()) # 581869302
print(generator.random())
print(generator.randint(1, 6))
Del 1: Tilstand og initialisering
- Implementer
__init__med initialiseringen over. Husk å kappe til 32 bit etter hver utregning. - Sett
self.indeks = N. Det betyr at tilstanden er brukt opp, ograndvil vri den før første tall hentes. - Kontroller at
mt[1]blir1ogmt[2]blir1812433255når frøet er0.
Del 2: Vri og temper
- Implementer
_vri. Merk at tilstanden endres på stedet, så tall tidlig i listen er allerede oppdatert nårikommer nær slutten. - Implementer
rand: vri om nødvendig, hentmt[indeks], temper, økindeksog returner. - Kjør med frø
5489. De tre første tallene skal være3499211612,581869302og3890346734. Tall nummer 10000 skal være4123659995. - Med frø
1skal de to første tallene være1791095845og4282876139.
Del 3: Flyttall og heltall
- Implementer
randomved å dele påself.modulus. - Implementer
randintsom iLCG. Hvilket problem får dere med de laveste bitene her sammenlignet med LCG? - Kjør
plott_histogram_og_par(generator)ogplott_tripler(generator)medMersenneTwisteri stedet forLCG. Ser dere noe struktur?
Del 4: Sammenlign med Python
Python lar dere hente tilstanden til Random-klassen med getstate(). Tilstanden er en tuppel med 625 tall: de 624 første er selve tilstandslisten mt, og det siste er indeksen. Dere kan derfor kopiere tilstanden inn i deres egen generator og sjekke at den er riktig, uten å kjenne Pythons frøalgoritme.
import random
from mersenne import MersenneTwister, N # N = 624, lengden på tilstandslisten
ekte = random.Random(42)
versjon, tilstand, _ = ekte.getstate() # tilstand er 624 tall + indeksen
mt = MersenneTwister(0)
mt.mt = list(tilstand[:N]) # kopier tilstandslisten
mt.indeks = tilstand[N] # kopier indeksen
for _ in range(1000):
assert mt.rand() == ekte.getrandbits(32)
- Kjør koden over. Hvis det ikke fungerer, sammenlign
mt.mtogtilstandetter første vri. - Python bruker en annen algoritme for å sette frøverdien, så vi kan dessverre ikke bruke
MersenneTwister(42)for å få samme tallfølge somrandom.Random(42). Hvordan Python gjør det, ser dere på i den første utvidelsen.
Tips
- Bruk
& 0xFFFFFFFFetter multiplikasjon og skift til venstre. Dette kapper tallene til 32-bit. Skift til høyre trenger det ikke. - Bruk
% Nfor å holde indeksene innenfor listen:mt[(i + 1) % N]. N er størrelsen til mt-listen. - Skriv ut tilstanden med
print(mt.mt[:5])hvis noe ikke stemmer. Sjekk det første tallet som avviker. - Vri ikke hele tilstanden før den er brukt opp. Det er
indeks-sjekken irandsom styrer dette.
Utvidelser
- Python bruker ikke frøalgoritmen over, men
init_by_array, som tar en liste med frø. Implementer den (se Matsumoto og Nishimuras referansekode,mt19937ar.c) og start medinit_genrand(19650218). Da skalMersenneTwister(42)gi de samme tallene somrandom.Random(42).getrandbits(32). - Python lager flyttall med 53 bit på denne måten:
a = rand() >> 5,b = rand() >> 6,(a * 67108864 + b) / 9007199254740992. Implementerrandomslik og sammenlign medrandom.Random(42).random(). - Tilstanden kan gjenskapes fra 624 påfølgende tallene. Hent 624 tall fra en generator, avtemper hvert tall (alle tempersteg kan snus) og lag en ny generator som forutsier alle senere tall. Hva sier dette om bruk av Mersenne Twister til passord og kryptografi?
- Mål hvor lang tid 1 million kall tar for
LCG,MersenneTwisterog Pythonsrandom. Hvorfor er Pythons egen så mye raskere? - Sammenlign med PCG eller xorshift, som er raskere og har mindre tilstand. Hva taper dere?
Tips: For å måle hvor lang tid noe tar i Python bruker man typisk time.perf_counter().
import time # En modul for å håndtere tidsobjekter. Inneholder mange andre nyttige funskjoner.
def mal_tid(funksjon, n=1_000_000):
"""Returnerer hvor mange sekunder n kall til funksjonen tar. Brukes til å sammenligne generatorene."""
start = time.perf_counter() # Lagrer starttidspunktet
for _ in range(n):
funksjon() # Gjør n-kall på en funksjon
return time.perf_counter() - start # Returnerer tidsdeltaet fra start til slutt