Lepszy sposób na potasowanie dwóch tablic numpy zgodnie

239

Mam dwie tablice numpy o różnych kształtach, ale o tej samej długości (wymiar wiodący). Chcę przetasować każdy z nich, tak aby odpowiadające mu elementy nadal korespondowały - tj. Potasować je zgodnie zgodnie z ich wiodącymi indeksami.

Ten kod działa i ilustruje moje cele:

def shuffle_in_unison(a, b):
    assert len(a) == len(b)
    shuffled_a = numpy.empty(a.shape, dtype=a.dtype)
    shuffled_b = numpy.empty(b.shape, dtype=b.dtype)
    permutation = numpy.random.permutation(len(a))
    for old_index, new_index in enumerate(permutation):
        shuffled_a[new_index] = a[old_index]
        shuffled_b[new_index] = b[old_index]
    return shuffled_a, shuffled_b

Na przykład:

>>> a = numpy.asarray([[1, 1], [2, 2], [3, 3]])
>>> b = numpy.asarray([1, 2, 3])
>>> shuffle_in_unison(a, b)
(array([[2, 2],
       [1, 1],
       [3, 3]]), array([2, 1, 3]))

Jednak wydaje się to niezręczne, nieefektywne i powolne, i wymaga wykonania kopii tablic - wolałbym je przetasować w miejscu, ponieważ będą dość duże.

Czy jest lepszy sposób, aby to zrobić? Szybsze wykonanie i mniejsze zużycie pamięci to moje główne cele, ale elegancki kod również byłby miły.

Jeszcze jedna myśl, którą miałem, to:

def shuffle_in_unison_scary(a, b):
    rng_state = numpy.random.get_state()
    numpy.random.shuffle(a)
    numpy.random.set_state(rng_state)
    numpy.random.shuffle(b)

Działa to ... ale jest trochę przerażające, ponieważ widzę niewielką gwarancję, że będzie nadal działać - nie wygląda to na coś, co gwarantuje na przykład przetrwanie w różnych wersjach.

Josh Bleecher Snyder
źródło
9
Sześć lat później jestem rozbawiony i zaskoczony popularnością tego pytania. I przy odrobinie zachwytu zbiegiem okoliczności, dla Go 1.10 włączyłem matematykę / randa. Przetasuj do standardowej biblioteki . Konstrukcja API sprawia, że ​​tasowanie dwóch tablic jednocześnie jest trywialne, a robienie tego jest nawet zawarte jako przykład w dokumentacji.
Josh Bleecher Snyder,

Odpowiedzi:

72

Twoje „przerażające” rozwiązanie nie wydaje mi się przerażające. Wywołanie shuffle()dwóch sekwencji o tej samej długości skutkuje taką samą liczbą wywołań do generatora liczb losowych, a są to jedyne „losowe” elementy w algorytmie odtwarzania losowego. Resetując stan, upewniasz się, że wywołania generatora liczb losowych dadzą takie same wyniki w drugim wywołaniu shuffle(), więc cały algorytm wygeneruje tę samą permutację.

Jeśli ci się to nie podoba, innym rozwiązaniem byłoby przechowywanie danych w jednej tablicy zamiast w dwóch od samego początku i utworzenie dwóch widoków w tej jednej tablicy, symulując dwie macierze, które masz teraz. Możesz użyć pojedynczej tablicy do tasowania i widoków do wszystkich innych celów.

Przykład: Załóżmy tablice ai bwyglądamy następująco:

a = numpy.array([[[  0.,   1.,   2.],
                  [  3.,   4.,   5.]],

                 [[  6.,   7.,   8.],
                  [  9.,  10.,  11.]],

                 [[ 12.,  13.,  14.],
                  [ 15.,  16.,  17.]]])

b = numpy.array([[ 0.,  1.],
                 [ 2.,  3.],
                 [ 4.,  5.]])

Możemy teraz zbudować jedną tablicę zawierającą wszystkie dane:

c = numpy.c_[a.reshape(len(a), -1), b.reshape(len(b), -1)]
# array([[  0.,   1.,   2.,   3.,   4.,   5.,   0.,   1.],
#        [  6.,   7.,   8.,   9.,  10.,  11.,   2.,   3.],
#        [ 12.,  13.,  14.,  15.,  16.,  17.,   4.,   5.]])

Teraz tworzymy widoki symulujące oryginał ai b:

a2 = c[:, :a.size//len(a)].reshape(a.shape)
b2 = c[:, a.size//len(a):].reshape(b.shape)

Dane a2i b2są udostępniane c. Aby przetasować obie tablice jednocześnie, użyj numpy.random.shuffle(c).

W kodzie produkcyjnym oczywiście starałbyś się unikać tworzenia oryginału ai bw ogóle i od razu tworzyć c, a2i b2.

To rozwiązanie można dostosować do przypadku ai bmieć różne typy.

Sven Marnach
źródło
Re: przerażające rozwiązanie: martwię się tylko, że tablice o różnych kształtach mogłyby (prawdopodobnie) wywoływać różną liczbę wywołań do rng, co spowodowałoby rozbieżność. Jednak myślę, że masz rację, że obecne zachowanie jest chyba raczej się nie zmieni, i to bardzo prosty doctest czyni potwierdzający poprawne zachowanie bardzo proste ...
Josh Bleecher Snyder
Podoba mi się twoje sugerowane podejście i zdecydowanie mogę zorganizować rozpoczęcie życia od a do b jako zunifikowaną macierz C. Jednak aib będą musiały być ciągłe krótko po tasowaniu (dla efektywnego przeniesienia do GPU), więc myślę, że w moim szczególnym przypadku i tak skończę. :(
Josh Bleecher Snyder
@Josh: Uwaga, która numpy.random.shuffle()działa na dowolnych sekwencjach, takich jak listy Python lub tablice NumPy. Kształt tablicy nie ma znaczenia, tylko długość sekwencji. Moim zdaniem jest to bardzo mało prawdopodobne.
Sven Marnach,
Nie wiedziałem tego Dzięki temu czuję się o wiele bardziej komfortowo. Dziękuję Ci.
Josh Bleecher Snyder
@ SvenMarnach: poniżej opublikowałem odpowiedź. Czy możesz skomentować, czy uważasz, że ma to sens / czy jest to dobry sposób?
ajfbiw.s
351

Możesz użyć indeksowania tablic NumPy :

def unison_shuffled_copies(a, b):
    assert len(a) == len(b)
    p = numpy.random.permutation(len(a))
    return a[p], b[p]

Spowoduje to utworzenie oddzielnych, potasowanych tablic.

mtrw
źródło
13
To czyni tworzenie kopii, ponieważ wykorzystuje zaawansowane indeksowanie. Ale oczywiście jest szybszy niż oryginał.
Sven Marnach,
1
@mtrw: Sam fakt, że oryginalne tablice są nietknięte, nie wyklucza, że ​​zwrócone tablice są widokami tych samych danych. Ale tak naprawdę nie są, ponieważ widoki NumPy nie są wystarczająco elastyczne, aby obsługiwać widoki permutowane (nie byłoby to również pożądane).
Sven Marnach,
1
@Sven - Naprawdę muszę się dowiedzieć o widokach. @Dat Chu - Właśnie próbowałem >>> t = timeit.Timer(stmt = "<function>(a,b)", setup = "import numpy as np; a,b = np.arange(4), np.arange(4*20).reshape((4,20))")>>> t.timeit()i dostałem 38 sekund dla wersji OP i 27,5 sekundy dla mojej, na 1 milion połączeń każdego.
mtrw
3
Bardzo podoba mi się prostota i czytelność tego, a zaawansowane indeksowanie nadal mnie zaskakuje i zadziwia; za to ta odpowiedź łatwo otrzymuje +1. Co dziwne, w moich (dużych) zestawach danych jest on wolniejszy niż moja oryginalna funkcja: mój oryginał zajmuje ~ 1,8 s na 10 iteracji, a to zajmuje ~ 2,7 s. Obie liczby są dość spójne. Zestaw danych, którego użyłem do testowania, a.shapejest (31925, 405)i b.shapejest (31925,).
Josh Bleecher Snyder
1
Być może powolność ma związek z tym, że nie robisz rzeczy w miejscu, ale tworzysz nowe tablice. Lub z pewnym spowolnieniem związanym z tym, jak CPython analizuje indeksy tablicowe.
Íhor Mé,
174
X = np.array([[1., 0.], [2., 1.], [0., 0.]])
y = np.array([0, 1, 2])
from sklearn.utils import shuffle
X, y = shuffle(X, y, random_state=0)

Aby dowiedzieć się więcej, zobacz http://scikit-learn.org/stable/modules/generated/sklearn.utils.shuffle.html

James
źródło
1
To rozwiązanie tworzy kopie ( „Nie ma to wpływu na oryginalne tablice” ), podczas gdy „przerażające” rozwiązanie autora nie.
bartolo-otrit
Możesz wybrać dowolny styl
James
33

Bardzo proste rozwiązanie:

randomize = np.arange(len(x))
np.random.shuffle(randomize)
x = x[randomize]
y = y[randomize]

obie tablice x, y są teraz losowo tasowane w ten sam sposób

Connor
źródło
5
Jest to równoważne z rozwiązaniem mtrw. Pierwsze dwa wiersze generują permutację, ale można to zrobić w jednym wierszu.
Josh Bleecher Snyder
19

James napisał w 2015 r. Rozwiązanie sklearn , które jest pomocne. Ale dodał losową zmienną stanu, która nie jest potrzebna. W poniższym kodzie automatycznie przyjmuje się losowy stan z numpy.

X = np.array([[1., 0.], [2., 1.], [0., 0.]])
y = np.array([0, 1, 2])
from sklearn.utils import shuffle
X, y = shuffle(X, y)
Daniel
źródło
16
from np.random import permutation
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data #numpy array
y = iris.target #numpy array

# Data is currently unshuffled; we should shuffle 
# each X[i] with its corresponding y[i]
perm = permutation(len(X))
X = X[perm]
y = y[perm]
benjaminjsanders
źródło
12

Przetasuj dowolną liczbę tablic razem, na miejscu, używając tylko NumPy.

import numpy as np


def shuffle_arrays(arrays, set_seed=-1):
    """Shuffles arrays in-place, in the same order, along axis=0

    Parameters:
    -----------
    arrays : List of NumPy arrays.
    set_seed : Seed value if int >= 0, else seed is random.
    """
    assert all(len(arr) == len(arrays[0]) for arr in arrays)
    seed = np.random.randint(0, 2**(32 - 1) - 1) if set_seed < 0 else set_seed

    for arr in arrays:
        rstate = np.random.RandomState(seed)
        rstate.shuffle(arr)

I może być używany w ten sposób

a = np.array([1, 2, 3, 4, 5])
b = np.array([10,20,30,40,50])
c = np.array([[1,10,11], [2,20,22], [3,30,33], [4,40,44], [5,50,55]])

shuffle_arrays([a, b, c])

Kilka rzeczy do zapamiętania:

  • Aser gwarantuje, że wszystkie tablice wejściowe mają tę samą długość wzdłuż pierwszego wymiaru.
  • Tablice przetasowano na miejscu według ich pierwszego wymiaru - nic nie zwróciło.
  • Losowe nasiona w dodatnim zakresie int32.
  • Jeśli konieczne jest powtarzalne odtwarzanie losowe, można ustawić wartość nasion.

Po przetasowaniu dane można podzielić za np.splitpomocą wycinków lub odwołać się do nich za pomocą wycinków - w zależności od aplikacji.

Izaak B.
źródło
2
piękne rozwiązanie, to działało idealnie dla mnie. Nawet z tablicami z osią 3+
wprins
1
To jest poprawna odpowiedź. Nie ma powodu, aby używać globalnego np. Losowego, gdy można ominąć losowe obiekty stanu.
Erotemiczny
Można RandomStatego użyć poza pętlą. Zobacz Adama Snaider za odpowiedź
Bartolo-otrit
1
@ bartolo-otrit, wybór, który musi być dokonany w forpętli, polega na tym, czy ponownie przypisać lub ponownie wybrać stan losowy. Ponieważ liczba tablic przekazywanych do funkcji tasowania powinna być niewielka, nie spodziewałbym się różnicy w wydajności między nimi. Ale tak, rstate można przypisać poza pętlę i ponownie wprowadzić w pętli na każdej iteracji.
Izaak B
9

możesz zrobić tablicę taką jak:

s = np.arange(0, len(a), 1)

następnie potasuj:

np.random.shuffle(s)

teraz użyj tego jako argumentu swoich tablic. te same tasowane argumenty zwracają te same tasowane wektory.

x_data = x_data[s]
x_label = x_label[s]
mohammad hassan bigdeli shamlo
źródło
Naprawdę jest to najlepsze rozwiązanie i powinno być przyjęte! Działa nawet dla wielu (więcej niż 2) tablic jednocześnie. Pomysł jest prosty: wystarczy potasować listę indeksów [0, 1, 2, ..., n-1], a następnie ponownie zindeksować wiersze tablic za pomocą tasowanych indeksów. Miły!
Basj
5

Jednym ze sposobów tasowania w miejscu dla połączonych list jest użycie zarodka (może być losowy) i użycie numpy.random.shuffle do wykonania tasowania.

# Set seed to a random number if you want the shuffling to be non-deterministic.
def shuffle(a, b, seed):
   np.random.seed(seed)
   np.random.shuffle(a)
   np.random.seed(seed)
   np.random.shuffle(b)

Otóż ​​to. Spowoduje to przetasowanie zarówno aib dokładnie w ten sam sposób. Odbywa się to również w miejscu, co zawsze stanowi plus.

EDYCJA, nie używaj np.random.seed () zamiast tego użyj np.random.RandomState

def shuffle(a, b, seed):
   rand_state = np.random.RandomState(seed)
   rand_state.shuffle(a)
   rand_state.seed(seed)
   rand_state.shuffle(b)

Podczas wywoływania wystarczy podać dowolne ziarno, aby podać stan losowy:

a = [1,2,3,4]
b = [11, 22, 33, 44]
shuffle(a, b, 12345)

Wynik:

>>> a
[1, 4, 2, 3]
>>> b
[11, 44, 22, 33]

Edycja: Naprawiono kod do ponownego inicjowania stanu losowego

Adam Snaider
źródło
Ten kod nie działa. RandomStatezmienia stan na pierwsze wezwanie i ai bnie tasuje unisono.
Bruno Klein
@BrunoKlein Masz rację. Naprawiłem post, aby ponownie zainicjować losowy stan. Ponadto, chociaż nie jest to zgodne w tym sensie, że obie listy są tasowane w tym samym czasie, są one zgodne w tym sensie, że obie są tasowane w ten sam sposób, a także nie wymaga więcej pamięci do przechowywania kopia list (o których OP wspomina w swoim pytaniu)
Adam Snaider
4

Istnieje dobrze znana funkcja, która może to obsłużyć:

from sklearn.model_selection import train_test_split
X, _, Y, _ = train_test_split(X,Y, test_size=0.0)

Samo ustawienie test_size na 0 pozwoli uniknąć podziału i da tasowane dane. Chociaż zwykle służy do dzielenia danych pociągu i testowania, tasuje je również.
Z dokumentacji

Podziel tablice lub macierze na losowe podzbiory pociągu i testu

Szybkie narzędzie, które otacza sprawdzanie poprawności danych wejściowych, a następnie (ShuffleSplit (). Split (X, y)) i aplikację do wprowadzania danych w jednym wywołaniu podziału (i opcjonalnie podpróbkowania) danych w oneliner.

sziraqui
źródło
Nie mogę uwierzyć, że nigdy o tym nie myślałem. Twoja odpowiedź jest genialna.
Długi Nguyen,
2

Powiedzmy, że mamy dwie tablice: a i b.

a = np.array([[1,2,3],[4,5,6],[7,8,9]])
b = np.array([[9,1,1],[6,6,6],[4,2,0]]) 

Możemy najpierw uzyskać indeksy wierszy poprzez permutację pierwszego wymiaru

indices = np.random.permutation(a.shape[0])
[1 2 0]

Następnie użyj zaawansowanego indeksowania. W tym przypadku używamy tych samych wskaźników, aby wspólnie tasować obie tablice.

a_shuffled = a[indices[:,np.newaxis], np.arange(a.shape[1])]
b_shuffled = b[indices[:,np.newaxis], np.arange(b.shape[1])]

Jest to równoważne z

np.take(a, indices, axis=0)
[[4 5 6]
 [7 8 9]
 [1 2 3]]

np.take(b, indices, axis=0)
[[6 6 6]
 [4 2 0]
 [9 1 1]]
monolit
źródło
Dlaczego nie tylko [wskaźniki ,:] lub b [wskaźniki ,:]?
Kev
1

Jeśli chcesz uniknąć kopiowania tablic, sugerowałbym, aby zamiast generować listę permutacji, przejrzałeś każdy element w tablicy i losowo zamieniłeś go na inną pozycję w tablicy

for old_index in len(a):
    new_index = numpy.random.randint(old_index+1)
    a[old_index], a[new_index] = a[new_index], a[old_index]
    b[old_index], b[new_index] = b[new_index], b[old_index]

To implementuje algorytm tasowania Knutha-Fishera-Yatesa.

DaveP
źródło
3
codinghorror.com/blog/2007/12/the-danger-of-naivete.html zmusił mnie do ostrożności przed implementacją własnych algorytmów losowych ; częściowo odpowiada za moje pytanie. :) Jednak masz rację, zwracając uwagę, że powinienem rozważyć użycie algorytmu Knuth-Fisher-Yates.
Josh Bleecher Snyder
Dobrze zauważony, teraz poprawiłem kod. W każdym razie myślę, że podstawowa idea tasowania w miejscu jest skalowalna do dowolnej liczby tablic i pozwala uniknąć kopiowania.
DaveP
Kod jest nadal niepoprawny (nawet się nie uruchomi). Aby to działało, zamień len(a)na reversed(range(1, len(a))). Ale i tak nie będzie to bardzo wydajne.
Sven Marnach,
1

To wydaje się bardzo prostym rozwiązaniem:

import numpy as np
def shuffle_in_unison(a,b):

    assert len(a)==len(b)
    c = np.arange(len(a))
    np.random.shuffle(c)

    return a[c],b[c]

a =  np.asarray([[1, 1], [2, 2], [3, 3]])
b =  np.asarray([11, 22, 33])

shuffle_in_unison(a,b)
Out[94]: 
(array([[3, 3],
        [2, 2],
        [1, 1]]),
 array([33, 22, 11]))
andy
źródło
0

Na przykład to, co robię:

combo = []
for i in range(60000):
    combo.append((images[i], labels[i]))

shuffle(combo)

im = []
lab = []
for c in combo:
    im.append(c[0])
    lab.append(c[1])
images = np.asarray(im)
labels = np.asarray(lab)
ajfbiw.s
źródło
1
Jest to mniej więcej odpowiednik combo = zip(images, labels); shuffle(combo); im, lab = zip(*combo), tylko wolniej. Ponieważ i tak używasz Numpy, jeszcze szybszym rozwiązaniem byłoby skompresowanie tablic za pomocą Numpy combo = np.c_[images, labels], losowe i rozpakowanie ponownie images, labels = combo.T. Zakładając, że labelsi imagessą jednowymiarowe tablice numpy o tej samej długości, aby rozpocząć, to będzie łatwo najszybszym rozwiązaniem. Jeśli są wielowymiarowe, zobacz moją odpowiedź powyżej.
Sven Marnach,
Ok, to ma sens. Dzięki! @SvenMarnach
ajfbiw.s
0

Rozszerzyłem random.shuffle () Pythona, aby pobrać drugi argument:

def shuffle_together(x, y):
    assert len(x) == len(y)

    for i in reversed(xrange(1, len(x))):
        # pick an element in x[:i+1] with which to exchange x[i]
        j = int(random.random() * (i+1))
        x[i], x[j] = x[j], x[i]
        y[i], y[j] = y[j], y[i]

W ten sposób mogę mieć pewność, że tasowanie odbywa się w miejscu, a funkcja nie jest zbyt długa ani skomplikowana.

Ivo
źródło
0

Po prostu użyj numpy ...

Najpierw połącz dwie tablice wejściowe Tablica 1D to etykiety (y), a tablica 2D to dane (x) i przetasuj je shufflemetodą NumPy . Na koniec podziel je i wróć.

import numpy as np

def shuffle_2d(a, b):
    rows= a.shape[0]
    if b.shape != (rows,1):
        b = b.reshape((rows,1))
    S = np.hstack((b,a))
    np.random.shuffle(S)
    b, a  = S[:,0], S[:,1:]
    return a,b

features, samples = 2, 5
x, y = np.random.random((samples, features)), np.arange(samples)
x, y = shuffle_2d(train, test)
szZzr
źródło