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.


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.


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.


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é,


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


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)

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]

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.


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]

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


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.


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]]

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.


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]))

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)

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.


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)
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.