Краще один раз перетасувати два масивні масиви


239

У мене два масивні масиви різної форми, але однакової довжини (провідний розмір). Я хочу перетасувати кожну з них так, щоб відповідні елементи продовжували відповідати, тобто змішувати їх в унісон щодо їхніх провідних індексів.

Цей код працює і ілюструє мої цілі:

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

Наприклад:

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

Однак це здається незграбним, неефективним та повільним, і для цього потрібно зробити копію масивів - я б краще перемістити їх на місці, оскільки вони будуть досить великими.

Чи є кращий шлях для цього? Швидше виконання та зменшення використання пам'яті - це мої основні цілі, але елегантний код також буде непоганим.

Ще одна думка, що я мала:

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)

Це працює ... але це трохи страшно, тому що я бачу невелику гарантію, що вона продовжить працювати - це не схоже на те, що гарантовано виживе в нумерованій версії, наприклад.


9
Через шість років я розвеселений і здивований тим, наскільки популярним виявилося це питання. І за трохи приємного збігу обставин, для Go 1.10 я сприяв математиці / rand.Shuffle до стандартної бібліотеки . Конструкція API дозволяє нерівномірно переміщувати два масиви, і це навіть включено як приклад у документи.
Josh Bleecher Snyder

Відповіді:


72

Ваше "страшне" рішення мені не здається страшним. Виклик shuffle()двох послідовностей однакової довжини призводить до однакової кількості викликів до генератора випадкових чисел, і це єдині "випадкові" елементи в алгоритмі перетасовки. Скинувши стан, ви гарантуєте, що виклики до генератора випадкових чисел дадуть однакові результати під час другого виклику shuffle(), тому весь алгоритм генерує однакову перестановку.

Якщо вам це не подобається, іншим рішенням було б зберігати свої дані в одному масиві замість двох прямо з початку та створити два представлення в цьому єдиному масиві, що імітує два масиви, які ви маєте зараз. Ви можете використовувати єдиний масив для переміщення та перегляду для всіх інших цілей.

Приклад: Припустимо масиви aі bвиглядатимемо так:

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

Тепер ми можемо побудувати єдиний масив, що містить усі дані:

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

Тепер ми створюємо подання, що імітують оригінал aта b:

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

Дані a2та b2спільний доступ до них c. Щоб перемістити обидва масиви одночасно, використовуйте numpy.random.shuffle(c).

У виробничому коді ви, звичайно, намагатиметеся уникати створення оригіналу aі bзовсім і відразу створювати c, a2і b2.

Це рішення можна адаптувати до конкретного випадку aта bмати різні типи.


Re: страшне рішення: Я просто переживаю, що масиви різної форми можуть (можливо) спричинити різну кількість викликів до rng, що може спричинити розбіжність. Однак, я думаю, ви праві, що поточна поведінка, мабуть, навряд чи зміниться, і дуже простий докт-тест робить полегшення підтвердження правильної поведінки ...
Josh Bleecher Snyder

Мені подобається ваш запропонований підхід, і я напевно міг би домовитись про те, щоб почати життя як єдиний масив c Однак, a і b потрібно буде бути суміжними незабаром після перемішування (для ефективної передачі в GPU), тому я думаю, що в моєму конкретному випадку я б все-таки створив копії a і b все одно. :(
Josh Bleecher Snyder

@Josh: Зауважте, що numpy.random.shuffle()працює на довільних змінних послідовностях, таких як списки Python або масиви NumPy. Форма масиву не має значення, лише довжина послідовності. Це дуже навряд чи зміниться на мій погляд.
Свен Марнах

Я цього не знав. Це робить мені набагато комфортніше з цим. Дякую.
Josh Bleecher Snyder

@SvenMarnach: Відповідь я розмістив нижче. Чи можете ви прокоментувати, чи вважаєте ви, що це має сенс / це хороший спосіб зробити це?
ajfbiw.s

351

Ви можете використовувати індексацію масиву NumPy :

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

Це призведе до створення окремих масивів, перетасованих унісон.


13
Це робить створювати копії, так як вона використовує вдосконалену індексацію. Але звичайно це швидше, ніж оригінал.
Свен Марнах

1
@mtrw: Сам факт недоторканності оригінальних масивів не виправдовує те, що повернуті масиви є видом одних і тих же даних. Але насправді це не так, оскільки представлення NumPy недостатньо гнучкі, щоб підтримувати перестановлені погляди (це теж не бажано).
Свен Марнах

1
@Sven - мені справді доводиться дізнаватися про погляди. @Dat Chu - Я просто спробував >>> 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()і отримав 38 секунд для версії ОП, і 27,5 секунди для моєї, за 1 мільйон дзвінків кожна.
mtrw

3
Мені дуже подобається простота і зрозумілість цього, а вдосконалена індексація продовжує мене дивувати і дивувати; за це ця відповідь охоче отримує +1. Як не дивно, але на моїх (великих) наборах даних вона повільніше, ніж у моїй оригінальній функції: мій оригінал займає ~ 1,8 секунди за 10 ітерацій, а це займає ~ 2,7 секунди. Обидва числа цілком узгоджуються. Набір даних, який я використовував для тестування, a.shapeє (31925, 405)і b.shapeє (31925,).
Josh Bleecher Snyder

1
Можливо, повільність пов'язана з тим, що ви не робите речі на місці, а натомість створюєте нові масиви. Або з деякою повільністю, пов'язаною з тим, як CPython аналізує масиви-індекси.
Í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)

Щоб дізнатися більше, перегляньте сторінку http://scikit-learn.org/stable/modules/generated/sklearn.utils.shuffle.html


1
Це рішення створює копії ( "Оригінальні масиви не зачіпаються" ), тоді як авторське "страшне" рішення не робить.
bartolo-otrit

Ви можете вибрати будь-який стиль, як вам подобається
Джеймс

33

Дуже просте рішення:

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

два масиви x, y тепер обидва випадково переміщуються однаково


5
Це еквівалентно рішення mtrw. Ваші перші два рядки просто генерують перестановку, але це можна зробити в одному рядку.
Josh Bleecher Snyder

19

У 2015 році Джеймс написав рішення, яке є корисним. Але він додав змінну випадкового стану, яка не потрібна. У наведеному нижче коді автоматично передбачається випадковий стан з 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

Перемішуйте будь-яку кількість масивів разом на місці, використовуючи лише 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)

І можна використовувати так

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

Кілька речей, які слід зазначити:

  • Утвердження гарантують, що всі вхідні масиви мають однакову довжину уздовж першого розміру.
  • Масиви перетасувались на місці за першим виміром - нічого не поверталося.
  • Випадкове насіння в межах позитивного int32.
  • Якщо потрібно повторне переміщення, значення насіння можна встановити.

Після перетасування даних можна розділити дані, np.splitвикористовуючи фрагменти або посилаючись на них - залежно від програми.


2
прекрасне рішення, це спрацювало ідеально для мене. Навіть з масивами осі 3+
wprins

1
Це правильна відповідь. Немає підстав використовувати глобальний np.random, коли ви можете обходити об'єкти випадкових станів.
Erotemic

Один RandomStateможе бути використаний поза петлею. Див Адама Snaider в відповідь
Бартоло-otrit

1
@ bartolo-otrit, вибір, який потрібно зробити в forциклі, полягає в тому, щоб перепризначити чи перезапустити випадковий стан. Очікуючи, що кількість масивів, що передаються у функцію перетасовки, мала, я не очікував би різниці в продуктивності між ними. Але так, Rstate може бути призначений за межами циклу і повторно замінятися всередині циклу на кожній ітерації.
Ісаак Б

9

ви можете створити масив на зразок:

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

потім перетасуйте його:

np.random.shuffle(s)

тепер використовуйте цей s як аргумент ваших масивів. ті ж перетасовані аргументи повертають ті ж перетасовані вектори.

x_data = x_data[s]
x_label = x_label[s]

Дійсно, це найкраще рішення, і воно повинно бути прийнятим! Він навіть працює для багатьох (більше 2) масивів одночасно. Ідея проста: просто перетасуйте список індексів [0, 1, 2, ..., n-1], а потім повторно встановіть ряди масивів із перетасованими індексами. Приємно!
Бась

5

Одним із способів переміщення місця на місці для з'єднаних списків є використання насіння (це може бути випадковим чином) та використання numpy.random.shuffle для переміщення.

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

Це воно. Це змістить як a, так і b точно таким же чином. Це також робиться на місці, що завжди є плюсом.

EDIT, не використовуйте np.random.seed (), використовуйте 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)

Під час виклику просто передайте будь-яке насіння, щоб подати випадковий стан:

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

Вихід:

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

Редагувати: виправлений код для повторного виведення випадкового стану


Цей код не працює. RandomStateзмінює своє стан на перший поклик і aі bне перемішується в унісон.
Бруно Кляйн

@BrunoKlein Ви праві. Я зафіксував посаду, щоб повторно засіяти випадковий стан. Крім того, незважаючи на те, що обидва списки не є унісон у тому, що обидва списки змішуються одночасно, вони є в унісон в тому сенсі, що обидва переміщуються однаково, і це також не потребує більше пам'яті, щоб утримати копія списків (про яку згадує ОП у своєму запитанні)
Адам Снайдер

4

Існує відома функція, яка справляється з цим:

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

Якщо встановити test_size на 0, ви не зможете розбиватись та давати перетасувати дані. Хоча він зазвичай використовується для поділу даних поїздів і випробувань, він також переміщує їх.
З документації

Розділити масиви чи матриці на випадкові підмножини поїздів та тестів

Швидка утиліта, яка завершує перевірку вхідних даних та наступне (ShuffleSplit (). Розділ (X, y)) та додаток для введення даних в один виклик для розділення (і, можливо, підсистеми) даних в одному лінійці.


Не можу повірити, що я ніколи про це не думав. Ваша відповідь геніальна.
Довгий Нгуен

2

Скажіть, у нас є два масиви: a і 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]]) 

Спочатку ми можемо отримати індекси рядків, переставляючи перший вимір

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

Потім використовуйте розширену індексацію. Тут ми використовуємо ті самі індекси, щоб одночасно переміщувати обидва масиви.

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

Це еквівалентно

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

Чому б не просто [індекси,:] або b [індекси ,:]?
Кев

1

Якщо ви хочете уникнути копіювання масивів, тоді я б запропонував, щоб замість генерувати перестановочний список, ви переходите через кожен елемент масиву і випадково поміняєте його на інше місце в масиві

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]

Це реалізує алгоритм перемикання Knuth-Fisher-Yates.


3
codinghorror.com/blog/2007/12/the-danger-of-naivete.html змусив мене насторожено застосовувати власні алгоритми перетасовки; це частково відповідає за моє запитання. :) Однак ви дуже правильно зазначаєте, що я повинен розглянути можливість використання алгоритму Knuth-Fisher-Yates.
Josh Bleecher Snyder

Добре помічений, я виправив код зараз. У будь-якому випадку, я думаю, що основна ідея переміщення на місці є масштабованою до довільної кількості масивів, щоб уникнути копіювання.
DaveP

Код все ще неправильний (він навіть не запускається). Щоб він працював, замініть len(a)на reversed(range(1, len(a))). Але все одно це буде не дуже ефективно.
Свен Марнах

1

Це здається дуже простим рішенням:

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

Наприклад, це те, що я роблю:

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
Це більш-менш рівнозначно combo = zip(images, labels); shuffle(combo); im, lab = zip(*combo), просто повільніше. Оскільки ви все одно використовуєте Numpy, ще набагато швидшим рішенням буде зібрати масиви за допомогою Numpy combo = np.c_[images, labels], перетасувати та знову розпакувати images, labels = combo.T. Якщо припустити, що це labelsі imagesє одновимірними масивами Numpy однакової довжини, це буде легко швидким рішенням. Якщо вони багатовимірні, дивіться мою відповідь вище.
Свен Марнах

Добре, що має сенс. Дякую! @SvenMarnach
ajfbiw.s

0

Я розширив python random.shuffle (), щоб взяти другий аргумент:

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]

Таким чином я можу бути впевнений, що перетасування відбувається на місці, а функція не дуже довга або складна.


0

Просто використовуйте numpy...

Спочатку об'єднайте два вхідних масиви 1D масив - це мітки (y), а 2D масив - це дані (x), і перетасуйте їх shuffleметодом NumPy . Нарешті розділіть їх і поверніться.

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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.