Cosa c'è di sbagliato in questo algoritmo di shuffling "ingenuo"?


23

Questo è il seguito di una domanda StackOverflow sul mescolamento casuale di un array .

Esistono algoritmi consolidati (come il Knuth-Fisher-Yates Shuffle ) che si dovrebbe usare per mescolare un array, piuttosto che fare affidamento su implementazioni ad hoc "ingenue".

Ora sono interessato a provare (o smentire) che il mio ingenuo algoritmo è rotto (come in: non genera tutte le possibili permutazioni con uguale probabilità).

Ecco l'algoritmo:

Ripeti un paio di volte (la lunghezza dell'array dovrebbe fare) e, in ogni iterazione, ottieni due indici di array casuali e scambia i due elementi lì.

Ovviamente, questo ha bisogno di più numeri casuali rispetto a KFY (il doppio), ma a parte questo funziona correttamente? E quale sarebbe il numero appropriato di iterazioni (è sufficiente "lunghezza dell'array")?


4
Non riesco proprio a capire perché la gente pensi che questo scambio sia "più semplice" o "più ingenuo" di FY ... Quando stavo risolvendo questo problema per la prima volta ho appena implementato FY (non sapendo che ha anche un nome) , solo perché mi è sembrato il modo più semplice di farlo per me.

1
@mbq: personalmente li trovo ugualmente facili, anche se concordo sul fatto che FY mi sembra più "naturale".
nico,

3
Quando ho fatto ricerche sugli algoritmi di mescolamento dopo aver scritto il mio (una pratica che ho abbandonato da allora), ero tutto "merda santa, è stato fatto e ha un nome !!"
JM non è uno statistico il

Risposte:


12

È rotto, anche se se esegui abbastanza shuffle, può essere un'approssimazione eccellente (come indicato dalle risposte precedenti).

Solo per avere un'idea di cosa sta succedendo, considera la frequenza con cui l'algoritmo genererà shuffle di una matrice di elementi in cui è fissato il primo elemento, k 2 . Quando le permutazioni sono generate con uguale probabilità, ciò dovrebbe avvenire 1 / k del tempo. Sia p n la frequenza relativa di questa occorrenza dopo n shuffle con il tuo algoritmo. Siamo generosi anche noi e supponiamo che tu stia effettivamente selezionando coppie distinte di indici uniformemente casuali per i tuoi shuffle, in modo che ogni coppia sia selezionata con probabilità =kk21/kpnn 2/(k(k-1))1/(k2)2/(k(k1)). (Ciò significa che non ci sono shuffle "banali" sprecati. D'altra parte, rompe totalmente l'algoritmo per un array a due elementi, perché si alterna tra il fissaggio dei due elementi e lo scambio, quindi se ci si ferma dopo un numero predeterminato di passi, non c'è casualità nei risultati!)

Questa frequenza soddisfa una semplice ricorrenza, perché il primo elemento si trova nella sua posizione originale dopo mescola in due modi disgiunti. Uno è che è stato corretto dopo shuffle e il shuffle successivo non sposta il primo elemento. L'altro è che è stato spostato dopo shuffle, ma shuffle lo sposta indietro. La possibilità di non spostare il primo elemento è uguale a = , mentre la possibilità di spostare indietro il primo elemento è uguale a = . Da dove:n n n + 1 s t ( k - 1n+1nnn+1st (k-2)/k1/ ( k(k12)/(k2)(k2)/k 2/(k(k-1))1/(k2)2/(k(k1))

p0=1
perché il primo elemento inizia al posto giusto;

pn+1=k2kpn+2k(k1)(1pn).

La soluzione è

pn=1/k+(k3k1)nk1k.

Sottraendo , vediamo che la frequenza è errata di . Per e grandi , una buona approssimazione è . Ciò dimostra che l'errore in questa particolare frequenza diminuirà in modo esponenziale con il numero di swap rispetto alla dimensione dell'array ( ), indicando che sarà difficile rilevare con array di grandi dimensioni se è stato effettuato un numero relativamente elevato di swap --ma l'errore è sempre lì.( k - 31/k knk-1(k3k1)nk1kknn/kk1kexp(2nk1)n/k

È difficile fornire un'analisi completa degli errori in tutte le frequenze. È probabile che si comporteranno come questo, tuttavia, il che dimostra che almeno avresti bisogno di (il numero di swap) per essere abbastanza grande da rendere l'errore accettabilmente piccolo. Una soluzione approssimativa èn

n>12(1(k1)log(ϵ))

dove dovrebbe essere molto piccolo rispetto a . Ciò implica che dovrebbe essere più volte per approssimazioni anche grossolane ( ovvero , dove è nell'ordine di volte circa).1 / k n k ϵ 0,01 1 / kϵ1/knkϵ0.011/k

Tutto ciò fa sorgere la domanda: perché dovresti scegliere di utilizzare un algoritmo che non è del tutto (ma solo approssimativamente) corretto, impiega esattamente le stesse tecniche di un altro algoritmo che è dimostrabilmente corretto, e che tuttavia richiede più calcoli?

modificare

Il commento di Thilo è appropriato (e speravo che nessuno lo segnalasse, quindi potrei essere risparmiato questo lavoro extra!). Lasciami spiegare la logica.

  • Se ti assicuri di generare degli swap effettivi ogni volta, sei completamente fregato. Il problema che ho sottolineato per il caso estende a tutti gli array. Solo la metà di tutte le possibili permutazioni può essere ottenuta applicando un numero pari di swap; l'altra metà è ottenuta applicando un numero dispari di swap. Pertanto, in questa situazione, non è mai possibile generare da nessuna parte vicino a una distribuzione uniforme di permutazioni (ma ce ne sono così tante possibili che uno studio di simulazione per qualsiasi considerevole non sarà in grado di rilevare il problema). È davvero brutto.kk=2K

  • Pertanto è consigliabile generare swap a caso generando le due posizioni in modo indipendente a caso. Questo significa che c'è una possibilità ogni volta di scambiare un elemento con se stesso; cioè di non fare nulla. Questo processo rallenta effettivamente un po 'l'algoritmo: dopo passaggi, ci aspettiamo che si siano verificati solo swap reali sun k - 11/KnK-1KN<N

  • Si noti che la dimensione dell'errore diminuisce monotonicamente con il numero di swap distinti. Pertanto, condurre in media meno swap aumenta anche l'errore, in media. Ma questo è un prezzo che dovresti essere disposto a pagare per superare il problema descritto nel primo punto. Di conseguenza, la mia stima dell'errore è prudentemente bassa, approssimativamente di un fattore di .(K-1)/K

Volevo anche segnalare un'interessante eccezione apparente: una stretta cerca nella formula di errore indica che non v'è alcun errore nel caso . Questo non è un errore: è corretto. Tuttavia, qui ho esaminato solo una statistica relativa alla distribuzione uniforme delle permutazioni. Il fatto che l'algoritmo sia in grado di riprodurre questa statistica quando (ovvero ottenere la giusta frequenza di permutazioni che fissano una determinata posizione) non garantisce che le permutazioni siano state effettivamente distribuite uniformemente. In effetti, dopo effettivi swap, le uniche permutazioni possibili che possono essere generate sono ,k = 3 2 n ( 123 ) ( 321 ) 2 n + 1 ( 12 ) ( 23 ) ( 13 )K=3K=32n(123)(321)e l'identità. Solo quest'ultima fissa una determinata posizione, quindi in effetti esattamente un terzo delle permutazioni fissa una posizione. Ma manca la metà delle permutazioni! Nell'altro caso, dopo scambi effettivi, le uniche permutazioni possibili sono , e . Ancora una volta, esattamente uno di questi risolverà una determinata posizione, quindi di nuovo otteniamo la frequenza corretta di permutazioni fissando quella posizione, ma di nuovo otteniamo solo la metà delle possibili permutazioni.2n+1(12)(23)(13)

Questo piccolo esempio aiuta a rivelare i principali elementi dell'argomento: essendo "generosi" sottovalutiamo prudentemente il tasso di errore per una statistica in particolare. Poiché il tasso di errore è diverso da zero per tutto , vediamo che l'algoritmo è rotto. Inoltre, analizzando il decadimento del tasso di errore per questa statistica stabiliamo un limite inferiore al numero di iterazioni dell'algoritmo necessarie per avere qualche speranza di approssimare una distribuzione uniforme delle permutazioni.K4


1
"Cerchiamo di essere generosi e supponiamo che tu stia effettivamente selezionando coppie distinte di indici uniformemente casuali per i tuoi shuffles". Non capisco perché questa ipotesi possa essere fatta e come sia generosa. Sembra scartare possibili permutazioni, risultando in una distribuzione ancora meno casuale.
Thilo,

1
@Thilo: grazie. Il tuo commento merita una risposta estesa, quindi l'ho inserito nella risposta stessa. Vorrei sottolineare qui che essere "generosi" in realtà non elimina alcuna permutazione: elimina semplicemente i passaggi dell'algoritmo che altrimenti non farebbero nulla.
whuber

2
Questo problema può essere analizzato completamente come una catena di Markov sul grafico Cayley del gruppo di permutazione. I calcoli numerici per k = 1 a 7 (una matrice 5040 per 5040!) Confermano che gli autovalori più grandi in termini di dimensioni (dopo 1 e -1) sono esattamente . Ciò implica che una volta affrontato il problema di alternare il segno della permutazione (corrispondente all'autovalore di -1), gli errori in tutte le probabilità decadono al ritmo oppure Più veloce. Sospetto che questo continui a valere per tutti i più grandi . ( 1 - 2 / ( k - 1 ) ) n k(K-3)/(K-1)=1-2/(K-1)(1-2/(K-1))nK
whuber

1
Puoi fare molto meglio di poiché le probabilità sono invarianti sulle classi di coniugazione e ci sono solo partizioni di modo da poter invece analizzare una matrice . 15 7 15 × 155040×504015715×15
Douglas Zare,

8

Penso che il tuo semplice algoritmo mescolerà le carte correttamente mentre il numero di caselle tende all'infinito.

Supponiamo di avere tre carte: {A, B, C}. Supponi che le tue carte inizino nel seguente ordine: A, B, C. Quindi dopo uno shuffle hai le seguenti combinazioni:

{A,B,C}, {A,B,C}, {A,B,C} #You get this if choose the same RN twice.
{A,C,B}, {A,C,B}
{C,B,A}, {C,B,A}
{B,A,C}, {B,A,C}

Quindi, la probabilità che la carta A sia in posizione {1,2,3} è {5/9, 2/9, 2/9}.

Se mescoliamo le carte una seconda volta, allora:

Pr(A in position 1 after 2 shuffles) = 5/9*Pr(A in position 1 after 1 shuffle) 
                                     + 2/9*Pr(A in position 2 after 1 shuffle) 
                                     + 2/9*Pr(A in position 3 after 1 shuffle) 

Questo dà 0,407.

Usando la stessa idea, possiamo formare una relazione di ricorrenza, ovvero:

Pr(A in position 1 after n shuffles) = 5/9*Pr(A in position 1 after (n-1) shuffles) 
                                     + 2/9*Pr(A in position 2 after (n-1) shuffles) 
                                     + 2/9*Pr(A in position 3 after (n-1) shuffles).

Codificandolo in R (vedi il codice sotto), si ha la probabilità che la carta A sia in posizione {1,2,3} come {0,33334, 0,33333, 0,33333} dopo dieci mischiature.

Codice R.

## m is the probability matrix of card position
## Row is position
## Col is card A, B, C
m = matrix(0, nrow=3, ncol=3)
m[1,1] = 1; m[2,2] = 1; m[3,3] = 1

## Transition matrix
m_trans = matrix(2/9, nrow=3, ncol=3)
m_trans[1,1] = 5/9; m_trans[2,2] = 5/9; m_trans[3,3] = 5/9

for(i in 1:10){
  old_m = m
  m[1,1] = sum(m_trans[,1]*old_m[,1])
  m[2,1] = sum(m_trans[,2]*old_m[,1])
  m[3,1] = sum(m_trans[,3]*old_m[,1])

  m[1,2] = sum(m_trans[,1]*old_m[,2])
  m[2,2] = sum(m_trans[,2]*old_m[,2])
  m[3,2] = sum(m_trans[,3]*old_m[,2])

  m[1,3] = sum(m_trans[,1]*old_m[,3])
  m[2,3] = sum(m_trans[,2]*old_m[,3])
  m[3,3] = sum(m_trans[,3]*old_m[,3])
}  
m

1
+1. Ciò dimostra che la probabilità che una data carta finisca in una data posizione si avvicina al rapporto previsto all'aumentare del numero di mescolanze. Tuttavia, lo stesso vale anche per un algoritmo che ruota l'array una sola volta di una quantità casuale: tutte le carte hanno la stessa probabilità di finire in tutte le posizioni, ma non c'è ancora alcuna casualità (l'array rimane ordinato).
Thilo,

@Thilo: mi dispiace non seguire il tuo commento. Un "algoritmo ruota di una quantità casuale" ma c'è ancora "nessuna casualità"? Potresti spiegare ulteriormente?
csgillespie,

Se "mescoli" un array di elementi N ruotandolo tra le posizioni 0 e N-1 (in modo casuale), allora ogni carta ha esattamente la stessa probabilità di finire in una delle posizioni N, ma 2 si trova sempre tra 1 e 3.
Thilo,

1
@Tio: Ah, capisco. Bene, puoi calcolare la probabilità (usando esattamente la stessa idea di cui sopra), per Pr (A in posizione 2) e Pr (A in posizione 3) - dito per le carte B e C. Vedrai che tutte le probabilità tendono a 1/3. Nota: la mia risposta fornisce solo un caso particolare, mentre @whuber bella risposta indica il caso generale.
csgillespie,

4

Un modo per vedere che non otterrai una distribuzione perfettamente uniforme è la divisibilità. Nella distribuzione uniforme, la probabilità di ogni permutazione è. Quando si genera una sequenza di trasposizioni casuali, e sequenze poi raccogliere da loro prodotto, le probabilità che si ottengono sono della forma per qualche intero . Se , quindi . Secondo il postulato di Bertrand (un teorema), per ci sono numeri primi che si presentano nel denominatore e che non dividono , quindinon è un numero intero e non esiste un modo per dividere uniformemente le trasposizioni int A / n 2 t A 1 / n ! = A / n 2 t n 2 t / n ! = A n 3 n n 2 t / n ! n ! n = 52 1 / 52 ! 3 , 5 , 7 , . . . , 47 1 /1/n!tUN/n2tUN1/n!=UN/n2tn2t/n!=UNn3nn2t/n!n!permutazioni. Ad esempio, se , allora il denominatore diè divisibile per mentre il denominatore di non lo è, quindi non può ridurre a.n=521/52!3,5,7,...,47 A / 52 2 t 1 / 52 !1/522tUN/522t1/52!

Di quanti ne hai bisogno per approssimare bene una permutazione casuale? La generazione di una permutazione casuale mediante trasposizioni casuali è stata analizzata da Diaconis e Shahshahani usando la teoria della rappresentazione del gruppo simmetrico in

Diaconis, P., Shahshahani, M. (1981): "Generazione di una permutazione casuale con trasposizioni casuali". Z. Wahrsch. Verw. Geb. 57, 159–179.

Una conclusione è stata che ci vogliono trasposizioni nel senso che dopo le permutazioni sono tutt'altro che casuali, ma dopo il risultato è quasi casuale, sia nel senso della variazione totale che della distanza . Questo tipo di fenomeno di cutoff è comune nelle passeggiate casuali su gruppi ed è legato al famoso risultato che sono necessari shuffle di riffle prima che un mazzo diventi quasi casuale.(1-ϵ)112nlogn(1+ϵ)1(1-ε)12nlognL27(1+ε)12nlognL27


2

Ricorda che non sono uno statistico, ma metterò i miei 2 centesimi.

Ho fatto un piccolo test in R (attenzione, è molto lento in alto numTrials, probabilmente il codice può essere ottimizzato):

numElements <- 1000
numTrials <- 5000

swapVec <- function()
    {
    vec.swp <- vec

    for (i in 1:numElements)
        {
        i <- sample(1:numElements)
        j <- sample(1:numElements)

        tmp <- vec.swp[i]
        vec.swp[i] <- vec.swp[j]
        vec.swp[j] <- tmp
        }

    return (vec.swp)
    }

# Create a normally distributed array of numElements length
vec <- rnorm(numElements)

# Do several "swapping trials" so we can make some stats on them
swaps <- vec
prog <- txtProgressBar(0, numTrials, style=3)

for (t in 1:numTrials)
    {
    swaps <- rbind(swaps, swapVec())
    setTxtProgressBar(prog, t)
    }

Questo genererà una matrice swapscon numTrials+1righe (una per prova + l'originale) e numElementscolonne (una per ogni elemento vettoriale). Se il metodo è corretto, la distribuzione di ciascuna colonna (cioè dei valori per ciascun elemento durante le prove) non dovrebbe essere diversa dalla distribuzione dei dati originali.

Poiché i nostri dati originali erano normalmente distribuiti, ci aspetteremmo che tutte le colonne non si discostino da quello.

Se corriamo

par(mfrow= c(2,2))
# Our original data
hist(swaps[1,], 100, col="black", freq=FALSE, main="Original")
# Three "randomly" chosen columns
hist(swaps[,1], 100, col="black", freq=FALSE, main="Trial # 1") 
hist(swaps[,257], 100, col="black", freq=FALSE, main="Trial # 257")
hist(swaps[,844], 100, col="black", freq=FALSE, main="Trial # 844")

Noi abbiamo:

Istogrammi di prove casuali

che sembra molto promettente. Ora, se vogliamo confermare statisticamente che le distribuzioni non si discostano dall'originale, penso che potremmo usare un test di Kolmogorov-Smirnov (per favore, qualche statistico può confermare che è giusto?) E fare, per esempio

ks.test(swaps[1, ], swaps[, 234])

Il che ci dà p = 0.9926

Se controlliamo tutte le colonne:

ks.results <- apply(swaps, 2, function(col){ks.test(swaps[1,], col)})
p.values <- unlist(lapply(ks.results, function(x){x$p.value})

E noi corriamo

hist(p.values, 100, col="black")

noi abbiamo:

Istogramma dei valori p di Kolmogorov-Smirnov

Quindi, per la stragrande maggioranza degli elementi dell'array, il tuo metodo di scambio ha dato un buon risultato, come puoi vedere guardando anche i quartili.

1> quantile(p.values)
       0%       25%       50%       75%      100% 
0.6819832 0.9963731 0.9999188 0.9999996 1.0000000

Si noti che, ovviamente, con un numero inferiore di prove la situazione non è così buona:

50 prove

1> quantile(p.values)
          0%          25%          50%          75%         100% 
0.0003399635 0.2920976389 0.5583204486 0.8103852744 0.9999165730

100 prove

          0%         25%         50%         75%        100% 
 0.001434198 0.327553996 0.596603804 0.828037097 0.999999591 

500 prove

         0%         25%         50%         75%        100% 
0.007834701 0.504698404 0.764231550 0.934223503 0.999995887 

0

Ecco come sto interpretando il tuo algoritmo, in pseudo codice:

void shuffle(array, length, num_passes)
  for (pass = 0; pass < num_passes; ++pass) 
    for (n = 0; n < length; ++)
      i = random_in(0, length-1)
      j = random_in(0, lenght-1)
      swap(array[i], array[j]

2×length×num_passes[0,length1]length

length2×length×num_passes

length!length!<length2×length×num_passeS

length!|length2×length×num_pun'SSeS

pp<lengthplengthlength>2p|length!length2×length×num_pun'SSeS length>2length!length2×length×num_pun'SSeSlength>2

Esiste un tale primo? Sì. Se la fosse divisibile per tutti i numeri primi , allora deve essere primo, ma poip < l e n g t h l e n g t h - 1lengthp<lengthlength-1length-1length

lengthlength-1length!length!|length!. Non è difficile dimostrare che ogni traccia si traduce in una permutazione diversa, e da lì è facile vedere che Fisher-Yates genera ogni permutazione con uguale probabilità.

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.