Seminare il generatore di numeri casuali in Javascript


373

È possibile eseguire il seeding del generatore di numeri casuali (Math.random) in Javascript?


non è chiaro se si desidera seminare in modo da ottenere gli stessi risultati ripetutamente per diverse esecuzioni di test o se si desidera seminare con "qualcosa di unico" per utente per una migliore casualità tra utilizzo.
simbo1905,

2
No, purtroppo non è possibile. jsrand è una piccola biblioteca che ho scritto quando avevo bisogno di un PRNG seminabile. Ci sono anche altre librerie più complesse che puoi trovare su Google per questo.
Domenico De Felice,

4
Aggiungendo alla domanda: com'è forse una buona idea offrire un PRNG senza un mezzo per seminarlo ?? C'è qualche buona ragione per questo?
Alan,

Risposte:



160

NOTA: Nonostante (o meglio, a causa di) succinta e apparente eleganza, questo algoritmo non è affatto di alta qualità in termini di casualità. Cerca ad esempio quelli elencati in questa risposta per risultati migliori.

(Originariamente adattato da un'idea intelligente presentata in un commento a un'altra risposta.)

var seed = 1;
function random() {
    var x = Math.sin(seed++) * 10000;
    return x - Math.floor(x);
}

Puoi impostare seedqualsiasi numero, evita semplicemente zero (o qualsiasi multiplo di Math.PI).

L'eleganza di questa soluzione, secondo me, deriva dalla mancanza di numeri "magici" (oltre a 10000, che rappresenta circa la quantità minima di cifre che è necessario eliminare per evitare schemi dispari - vedere i risultati con valori 10 , 100 , 1000 ). Anche la brevità è bella.

È un po 'più lento di Math.random () (di un fattore 2 o 3), ma credo che sia più veloce di qualsiasi altra soluzione scritta in JavaScript.


20
C'è un modo per dimostrare che questo RNG genera numeri distribuiti uniformemente? Sperimentalmente sembra: jsfiddle.net/bhrLT
Nathan Breit,

6
6.000.000 di operazioni / secondo è piuttosto veloce, non ho intenzione di generare più di ~ 3.000.000 per clic. Scherzo, è geniale.
AMK,

59
-1, Questo non è affatto un campionatore uniforme - è piuttosto distorto verso 0 e 1 (vedi jsfiddle.net/bhrLT/17 , che potrebbe richiedere del tempo per il calcolo). I valori consecutivi sono correlati: ogni 355 valori, e ancora di più ogni 710, sono correlati. Si prega di utilizzare qualcosa di più attentamente pensato!
Spencer Nelson,

38
La domanda non riguarda la creazione di un generatore di numeri casuali crittograficamente sicuro, ma qualcosa che funzioni in javascript, utile per dimostrazioni rapide, ecc. Prenderò qualcosa di rapido e semplice che offre una buona distribuzione su un milione di numeri casuali a tale scopo.
Jason Goemaat,

15
Stai attento. Math.sin () può fornire risultati diversi su client e server. Uso Meteor (usa javascript su client e server).
Obiwahn,

145

Ho implementato una serie di funzioni buone, brevi e veloci del generatore di numeri pseudocasuali (PRNG) in JavaScript. Tutti possono essere seminati e fornire numeri di buona qualità.

Prima di tutto, assicurati di inizializzare correttamente i tuoi PRNG. La maggior parte dei generatori di seguito non ha una procedura di generazione dei seed incorporata (per semplicità), ma accetta uno o più valori a 32 bit come stato iniziale del PRNG. Semi simili (ad es. Un seme semplice di 1 e 2) possono causare correlazioni in PRNG più deboli, con conseguente output con proprietà simili (come livelli generati casualmente essere simili). Per evitare ciò, è consigliabile inizializzare i PRNG con un seme ben distribuito.

Per fortuna, le funzioni di hash sono molto buone nel generare semi per PRNG da stringhe brevi. Una buona funzione hash genererà risultati molto diversi anche quando due stringhe sono simili. Ecco un esempio basato sulla funzione di missaggio di MurmurHash3:

function xmur3(str) {
    for(var i = 0, h = 1779033703 ^ str.length; i < str.length; i++)
        h = Math.imul(h ^ str.charCodeAt(i), 3432918353),
        h = h << 13 | h >>> 19;
    return function() {
        h = Math.imul(h ^ h >>> 16, 2246822507);
        h = Math.imul(h ^ h >>> 13, 3266489909);
        return (h ^= h >>> 16) >>> 0;
    }
}

Ogni chiamata successiva alla funzione return di xmur3produce un nuovo valore hash "casuale" a 32 bit da utilizzare come seed in un PRNG. Ecco come potresti usarlo:

// Create xmur3 state:
var seed = xmur3("apples");
// Output four 32-bit hashes to provide the seed for sfc32.
var rand = sfc32(seed(), seed(), seed(), seed());

// Output one 32-bit hash to provide the seed for mulberry32.
var rand = mulberry32(seed());

// Obtain sequential random numbers like so:
rand();
rand();

In alternativa, è sufficiente scegliere alcuni dati fittizi con cui riempire il seme e far avanzare il generatore alcune volte (12-20 iterazioni) per mescolare accuratamente lo stato iniziale. Questo è spesso visto nelle implementazioni di riferimento dei PRNG, ma limita il numero di stati iniziali.

var seed = 1337 ^ 0xDEADBEEF; // 32-bit seed with optional XOR value
// Pad seed with Phi, Pi and E.
// https://en.wikipedia.org/wiki/Nothing-up-my-sleeve_number
var rand = sfc32(0x9E3779B9, 0x243F6A88, 0xB7E15162, seed);
for (var i = 0; i < 15; i++) rand();

L'output di queste funzioni PRNG produce un numero positivo a 32 bit (da 0 a 2 32 -1) che viene quindi convertito in un numero a virgola mobile compreso tra 0-1 (0 incluso, 1 esclusivo) equivalente Math.random(), se si desidera numeri casuali di un intervallo specifico, leggi questo articolo su MDN . Se si desidera solo i bit grezzi, è sufficiente rimuovere l'operazione di divisione finale.

Un'altra cosa da notare sono i limiti di JS. I numeri possono rappresentare solo numeri interi con una risoluzione fino a 53 bit. E quando si usano operazioni bit a bit, questo è ridotto a 32. Ciò rende difficile implementare algoritmi scritti in C o C ++, che usano numeri a 64 bit. Il porting del codice a 64 bit richiede shim che possono ridurre drasticamente le prestazioni. Quindi, per semplicità ed efficienza, ho preso in considerazione solo algoritmi che usano la matematica a 32 bit, poiché è direttamente compatibile con JS.

Ora, in seguito ai generatori. (Mantengo l'elenco completo con riferimenti qui )


sfc32 (contatore veloce semplice)

sfc32 fa parte della suite di test di numeri casuali di PractRand (che passa ovviamente). sfc32 ha uno stato a 128 bit ed è molto veloce in JS.

function sfc32(a, b, c, d) {
    return function() {
      a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0; 
      var t = (a + b) | 0;
      a = b ^ b >>> 9;
      b = c + (c << 3) | 0;
      c = (c << 21 | c >>> 11);
      d = d + 1 | 0;
      t = t + d | 0;
      c = c + t | 0;
      return (t >>> 0) / 4294967296;
    }
}

Mulberry32

Mulberry32 è un semplice generatore con uno stato a 32 bit, ma è estremamente veloce e di buona qualità (l'autore afferma che supera tutti i test della suite di test gjrand e ha un periodo completo di 2 32 , ma non ho verificato).

function mulberry32(a) {
    return function() {
      var t = a += 0x6D2B79F5;
      t = Math.imul(t ^ t >>> 15, t | 1);
      t ^= t + Math.imul(t ^ t >>> 7, t | 61);
      return ((t ^ t >>> 14) >>> 0) / 4294967296;
    }
}

Lo consiglierei se hai solo bisogno di un PRNG semplice ma decente e non hai bisogno di miliardi di numeri casuali (vedi Problema di compleanno ).

xoshiro128 **

A partire da maggio 2018, xoshiro128 ** è il nuovo membro della famiglia Xorshift , di Vigna / Blackman (che ha anche scritto xoroshiro, che viene utilizzato in Chrome). È il generatore più veloce che offre uno stato a 128 bit.

function xoshiro128ss(a, b, c, d) {
    return function() {
        var t = b << 9, r = a * 5; r = (r << 7 | r >>> 25) * 9;
        c ^= a; d ^= b;
        b ^= c; a ^= d; c ^= t;
        d = d << 11 | d >>> 21;
        return (r >>> 0) / 4294967296;
    }
}

Gli autori sostengono che supera bene i test di casualità ( anche se con avvertimenti ). Altri ricercatori hanno sottolineato che falliscono alcuni test in TestU01 (in particolare LinearComp e BinaryRank). In pratica, non dovrebbe causare problemi quando si utilizzano float (come queste implementazioni), ma può causare problemi se si fa affidamento sui bit bassi non elaborati.

JSF (Jenkins 'Small Fast)

Questo è JSF o 'smallprng' di Bob Jenkins (2007), il ragazzo che ha realizzato ISAAC e SpookyHash . Si passa test PractRand e dovrebbe essere abbastanza veloce, anche se non così velocemente come SFC.

function jsf32(a, b, c, d) {
    return function() {
        a |= 0; b |= 0; c |= 0; d |= 0;
        var t = a - (b << 27 | b >>> 5) | 0;
        a = b ^ (c << 17 | c >>> 15);
        b = c + d | 0;
        c = d + t | 0;
        d = a + t | 0;
        return (d >>> 0) / 4294967296;
    }
}

LCG (alias Lehmer / Park-Miller RNG o MCG)

LCG è estremamente veloce e semplice, ma la qualità della sua casualità è così bassa che un uso improprio può effettivamente causare bug nel tuo programma! Tuttavia, è significativamente meglio di alcune risposte che suggeriscono di usare Math.sino Math.PI! È un one-liner però, che è bello :).

var LCG=s=>()=>(2**31-1&(s=Math.imul(48271,s)))/2**31;

Questa implementazione è chiamata RNG standard minima come proposta da Park-Miller nel 1988 e 1993 e implementata in C ++ 11 come minstd_rand. Tieni presente che lo stato è 31 bit (31 bit danno 2 miliardi di stati possibili, 32 bit danno il doppio). Questo è il tipo di PRNG che altri stanno cercando di sostituire!

Funzionerà, ma non lo userei a meno che tu non abbia davvero bisogno di velocità e non ti interessi della qualità della casualità (che cos'è comunque casuale?). Ottimo per un gioco marmellata o una demo o qualcosa del genere. I LCG soffrono di correlazioni con i semi, quindi è meglio scartare il primo risultato di un LCG. E se insisti nell'utilizzare un LCG, l'aggiunta di un valore di incremento può migliorare i risultati, ma è probabilmente un esercizio di futilità quando esistono opzioni molto migliori.

Sembra che ci siano altri moltiplicatori che offrono uno stato a 32 bit (spazio di stato aumentato):

var LCG=s=>()=>(s=Math.imul(741103597,s)>>>0)/2**32;
var LCG=s=>()=>(s=Math.imul(1597334677,s)>>>0)/2**32;

Questi valori LCG provengono da: P. L'Ecuyer: una tabella di generatori congruenziali lineari di diverse dimensioni e buona struttura reticolare, 30 aprile 1997.


5
Questa è una risposta straordinaria. Di sicuro tornerò su questo.
DavidsKanal

1
Credo che i valori che hai citato da "Tabelle dei generatori congruenziali lineari ..." di Pierre L'ecuyer potrebbero superare la dimensione intera massima in Javascript. Il seme massimo di (2 ^ 32-1) * 741103597 ≈ 3e18, che è maggiore della dimensione int massima di JavaScript di ≈ 9e15. Credo che i seguenti valori dal libro di Pierre hanno il periodo di più grande entro limiti nativi: seed = (seed * 185852 + 1) % 34359738337.
Lachmanski,

1
@Lachmanski vero, ma quelli sono associati a 32 bit (e il Park-Miller 31 bit). L'uso Math.imulconsente di overflow come farebbe quando si utilizza la moltiplicazione in C su numeri interi a 32 bit. Quello che stai suggerendo è un LCG che utilizza l'intera gamma dello spazio intero di JS, che è sicuramente un'area interessante da esplorare. :)
bryc,

1
Questo è bellissimo! Posso semplicemente copiare sfc32 in un programma LGPL?
user334639

4
@ blobber2 non so cosa intendi, ma il codice originale è di qui (con altri): github.com/bryc/code/blob/master/jshash/PRNGs.md . più o meno una sintesi all'interno di un repository :-)
bryc

39

No, ma ecco un semplice generatore pseudocasuale, un'implementazione di Moltiplica con carry adattata da Wikipedia (rimossa da allora):

var m_w = 123456789;
var m_z = 987654321;
var mask = 0xffffffff;

// Takes any integer
function seed(i) {
    m_w = (123456789 + i) & mask;
    m_z = (987654321 - i) & mask;
}

// Returns number between 0 (inclusive) and 1.0 (exclusive),
// just like Math.random().
function random()
{
    m_z = (36969 * (m_z & 65535) + (m_z >> 16)) & mask;
    m_w = (18000 * (m_w & 65535) + (m_w >> 16)) & mask;
    var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
    result /= 4294967296;
    return result;
}

EDIT: risolta la funzione seed ripristinandola m_z
EDIT2: sono stati corretti gravi problemi di implementazione


3
Qualcuno ha testato questa funzione per la sua casualità?
Justin

3
Questo è il generatore casuale moltiplicare con carry (MWC) con un periodo piuttosto lungo. Adattato dai generatori di numeri casuali di Wikipedia
Michael_Scharf,

10
La seedfunzione non reimposta il generatore casuale, poiché la mz_zvariabile viene modificata quando random()viene chiamata. Quindi imposta mz_z = 987654321(o qualsiasi altro valore) inseed
Michael_Scharf il

Quando lo uso con il mio generatore di colore casuale (HSL), genera solo colori verde e ciano. Il generatore casuale originale genera tutti i colori. Quindi, non è lo stesso o non funziona.
Tomas Kubes,

@Michael_Scharf 1) Il cambio di seme m_w, no m_z. 2) Entrambi m_we m_zcambiano in base ai loro valori precedenti, quindi modifica il risultato.
ESL,

26

L'algoritmo di Antti Sykäri è bello e breve. Inizialmente ho fatto una variante che ha sostituito Math.random di Javascript quando chiami Math.seed (s), ma poi Jason ha commentato che restituire la funzione sarebbe meglio:

Math.seed = function(s) {
    return function() {
        s = Math.sin(s) * 10000; return s - Math.floor(s);
    };
};

// usage:
var random1 = Math.seed(42);
var random2 = Math.seed(random1());
Math.random = Math.seed(random2());

Questo ti dà un'altra funzionalità che Javascript non ha: più generatori casuali indipendenti. Ciò è particolarmente importante se si desidera avere più simulazioni ripetibili in esecuzione contemporaneamente.


3
Se restituisci la funzione invece di un'impostazione Math.randomche ti consentirebbe di avere più generatori indipendenti, giusto?
Jason Goemaat,

1
Essere sicuri di vedere osservazioni di cui sopra sulla distribuzione di casualità, se la cosa vi interessa: stackoverflow.com/questions/521295/...
jocull

Come si possono ripetere i randoms generati da questo? Continua a dare nuovi numeri ogni volta
SMUsamaShah

ogni volta che si fa Math.seed(42);si resetta la funzione, quindi se lo fai var random = Math.seed(42); random(); random();si ottiene 0.70..., quindi 0.38.... Se lo ripristini chiamando di var random = Math.seed(42);nuovo, la prossima volta che chiami random()riceverai di 0.70...nuovo e la prossima volta che riceverai di 0.38...nuovo.
WOUNDEDStevenJones

1
Per favore non usare questo. Si prega di prendere il tempo invece di utilizzare una variabile locale denominata randomanziché sovrascrivere una funzione javascript nativa. La sovrascrittura Math.randompuò far sì che il compilatore JIST non ottimizzi tutto il codice.
Jack Giffin,

11

Si prega di vedere il lavoro di Pierre L'Ecuyer che risale alla fine degli anni '80 e all'inizio degli anni '90. Ce ne sono anche altri. Creare un generatore di numeri (pseudo) casuale da solo, se non sei un esperto, è piuttosto pericoloso, perché c'è un'alta probabilità che i risultati non siano statisticamente casuali o che abbiano un breve periodo. Pierre (e altri) hanno messo insieme dei buoni (pseudo) generatori di numeri casuali che sono facili da implementare. Uso uno dei suoi generatori LFSR.

https://www.iro.umontreal.ca/~lecuyer/myftp/papers/handstat.pdf

Phil Troy


1
Ottima risposta, ma non correlata a JavaScript :)
Nikolay Fominyh

3
Il codice per l'implementazione del lavoro del Professor L'Ecuyer è pubblicamente disponibile per java e facilmente traducibile dalla maggior parte dei programmatori in Javascript.
user2383235

6

Combinando alcune delle risposte precedenti, questa è la funzione casuale seminabile che stai cercando:

Math.seed = function(s) {
    var mask = 0xffffffff;
    var m_w  = (123456789 + s) & mask;
    var m_z  = (987654321 - s) & mask;

    return function() {
      m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
      m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;

      var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
      result /= 4294967296;
      return result;
    }
}

var myRandomFunction = Math.seed(1234);
var randomNumber = myRandomFunction();

4
Ciò produce risultati molto simili all'inizio della sequenza con semi diversi. Ad esempio, Math.seed(0)()restituisce 0.2322845458984375e Math.seed(1)()restituisce 0.23228873685002327. Cambiare entrambi m_we in m_zbase al seme sembra aiutare. var m_w = 987654321 + s; var m_z = 123456789 - s;produce una buona distribuzione dei primi valori con semi diversi.
non definito il

1
@undefined il problema che hai descritto è stato risolto dall'ultima modifica, era un bug nell'implementazione di MWC.
bryc,

Funziona bene ora, a partire da gennaio 2020. Semina con 0, ottieni 0.7322976540308446. Seme con 1, 0.16818441334180534, con 2: 0.6040864314418286, con 3: 0.03998844954185188. Grazie ad entrambi!
Eureka,

3

Scrivere il tuo generatore pseudo casuale è abbastanza semplice.

Il suggerimento di Dave Scotese è utile ma, come sottolineato da altri, non è distribuito in modo abbastanza uniforme.

Tuttavia, non è a causa degli argomenti interi del peccato. È semplicemente a causa della gamma del peccato, che sembra essere una proiezione unidimensionale di un cerchio. Se invece prendessi l'angolo del cerchio sarebbe uniforme.

Quindi invece di sin (x) usa arg (exp (i * x)) / (2 * PI).

Se non ti piace l'ordine lineare, mescola un po 'con xor. Anche il fattore reale non conta molto.

Per generare n numeri pseudo casuali è possibile utilizzare il codice:

function psora(k, n) {
  var r = Math.PI * (k ^ n)
  return r - Math.floor(r)
}
n = 42; for(k = 0; k < n; k++) console.log(psora(k, n))

Si noti inoltre che non è possibile utilizzare sequenze pseudo casuali quando è necessaria la vera entropia.


Non sono un esperto, ma i semi sequenziali seguono uno schema costante . I pixel colorati sono> = 0,5. Immagino che stia ripetendo ripetutamente il raggio?
bryc


1

Math.randomno, ma la libreria ran risolve questo. Ha quasi tutte le distribuzioni che puoi immaginare e supporta la generazione di numeri casuali con seeding. Esempio:

ran.core.seed(0)
myDist = new ran.Dist.Uniform(0, 1)
samples = myDist.sample(1000)

-1

Ho scritto una funzione che restituisce un numero casuale con seeding, usa Math.sin per avere un numero casuale lungo e usa il seed per scegliere i numeri da quello.

Uso :

seedRandom("k9]:2@", 15)

restituirà il numero di seeding il primo parametro è qualsiasi valore di stringa; il tuo seme. il secondo parametro è quante cifre restituiranno.

     function seedRandom(inputSeed, lengthOfNumber){

           var output = "";
           var seed = inputSeed.toString();
           var newSeed = 0;
           var characterArray = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','y','x','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','U','R','S','T','U','V','W','X','Y','Z','!','@','#','$','%','^','&','*','(',')',' ','[','{',']','}','|',';',':',"'",',','<','.','>','/','?','`','~','-','_','=','+'];
           var longNum = "";
           var counter = 0;
           var accumulator = 0;

           for(var i = 0; i < seed.length; i++){
                var a = seed.length - (i+1);
                for(var x = 0; x < characterArray.length; x++){
                     var tempX = x.toString();
                     var lastDigit = tempX.charAt(tempX.length-1);
                     var xOutput = parseInt(lastDigit);
                     addToSeed(characterArray[x], xOutput, a, i); 
                }                  
           }

                function addToSeed(character, value, a, i){
                     if(seed.charAt(i) === character){newSeed = newSeed + value * Math.pow(10, a)}
                }
                newSeed = newSeed.toString();

                var copy = newSeed;
           for(var i=0; i<lengthOfNumber*9; i++){
                newSeed = newSeed + copy;
                var x = Math.sin(20982+(i)) * 10000;
                var y = Math.floor((x - Math.floor(x))*10);
                longNum = longNum + y.toString()
           }

           for(var i=0; i<lengthOfNumber; i++){
                output = output + longNum.charAt(accumulator);
                counter++;
                accumulator = accumulator + parseInt(newSeed.charAt(counter));
           }
           return(output)
      }

1
Le sequenze di numeri prodotte da questo non si avvicinano realmente alle proprietà delle sequenze di numeri casuali. Genera 15 numeri con esso e la stringa risultante inizia quasi sempre con un 7 per quasi tutte le chiavi, ad esempio.
Gabriel,

-2

Un approccio semplice per un seme fisso:

function fixedrandom(p){
    const seed = 43758.5453123;
    return (Math.abs(Math.sin(p)) * seed)%1;
}

-6

Per un numero compreso tra 0 e 100.

Number.parseInt(Math.floor(Math.random() * 100))

3
La domanda è sulla semina in modo Math.randomtale che ogni volta che Math.randomviene seminato con lo stesso seme, produrrà la stessa serie successiva di numeri casuali. Questa domanda non è, per dire, sull'uso / dimostrazione effettiva di Math.random.
Jack Giffin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.