Лучший способ перетасовать два массива в унисон

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)

Это работает ... но это немного страшно, так как я вижу небольшую гарантию, что оно продолжит работать - это не похоже на то, что гарантированно выживет, например, в простой версии.

Джош Блихер Снайдер
источник
10
Шесть лет спустя я удивлен и удивлен тем, насколько популярным оказался этот вопрос. И в некотором восхитительном совпадении, для Go 1.10 я добавил math / rand.Shuffle в стандартную библиотеку . Конструкция API упрощает одновременное перемешивание двух массивов, и это даже включено в качестве примера в документах.
Джош Блихер Снайдер

Ответы:

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, что приведет к расхождению. Тем не менее, я думаю, что вы правы, что текущее поведение, вероятно, вряд ли изменится, и очень простой doctest делает подтверждение правильного поведения очень легким ...
Джош Блихер Снайдер
Мне нравится ваш предложенный подход, и я могу определенно организовать, чтобы a и b начинали жизнь как единый массив c. Тем не менее, a и b должны быть смежными вскоре после перетасовки (для эффективной передачи в графический процессор), поэтому я думаю, что в моем конкретном случае я бы все равно сделал копии a и b. :(
Джош Блихер Снайдер
@Josh: обратите внимание, что он numpy.random.shuffle()работает с произвольными изменяемыми последовательностями, такими как списки Python или массивы NumPy. Форма массива не имеет значения, только длина последовательности. Это очень вряд ли изменится на мой взгляд.
Свен Марнач
Я этого не знал. Это делает меня намного комфортнее с этим. Спасибо.
Джош Блихер Снайдер
@SvenMarnach: я разместил ответ ниже. Можете ли вы прокомментировать, считаете ли вы, что это имеет смысл / это хороший способ сделать это?
ajfbiw.s
353

Вы можете использовать индексирование массива NumPy :

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

Это приведет к созданию отдельных массивов в случайном порядке.

mtrw
источник
13
Это делает создавать копии, так как она использует усовершенствованную индексацию. Но, конечно, это быстрее, чем оригинал.
Свен Марнах
1
@mtrw: сам факт того, что исходные массивы не затронуты, не исключает того, что возвращаемые массивы являются представлениями тех же данных. Но на самом деле это не так, поскольку представления NumPy недостаточно гибки для поддержки перестановочных представлений (это также нежелательно).
Свен Марнач
1
@ Свен - мне действительно нужно узнать о взглядах. @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,).
Джош Блихер Снайдер
1
Возможно, медлительность связана с тем, что вы не делаете вещи на месте, а вместо этого создаете новые массивы. Или с некоторой медлительностью, связанной с тем, как CPython анализирует индексы массива.
Хор Ме
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
Это решение создает копии ( «Исходные массивы не затрагиваются» ), а «страшное» решение автора - нет.
Бартоло-Отрит
Вы можете выбрать любой стиль, который вам нравится
Джеймс
33

Очень простое решение:

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

два массива x, y теперь оба случайно перемешиваются одинаково

Коннор
источник
5
Это эквивалентно решению mtrw. Ваши первые две строки просто генерируют перестановку, но это можно сделать одной строкой.
Джош Блихер Снайдер
19

Джеймс написал в 2015 году решение sklearn, которое полезно. Но он добавил случайную переменную состояния, которая не нужна. В приведенном ниже коде случайное состояние из 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]
benjaminjsanders
источник
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может быть использован за пределами цикла. См. Ответ
бартоло-отрит
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], а затем переиндексировать строки массивов с перемешанными индексами. Ницца!
Basj
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 точно таким же образом. Это также делается на месте, что всегда является плюсом.

РЕДАКТИРОВАТЬ, не используйте 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 (). Split (X, y)) и приложение для ввода данных в один вызов для разделения (и, возможно, дополнительной выборки) данных в oneliner.

sziraqui
источник
Я не могу поверить, я никогда не думал об этом. Ваш ответ блестящий.
Длинный Нгуен
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]]
монолит
источник
Почему бы не просто [индексы ,:] или б [индексы ,:]?
Кев
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]

Это реализует алгоритм перемешивания Кнута-Фишера-Йейтса.

DaveP
источник
3
codinghorror.com/blog/2007/12/the-danger-of-naivete.html заставил меня опасаться реализации собственных алгоритмов случайного перемешивания; это частично ответственно за мой вопрос. :) Однако вы совершенно правы, отметив, что я должен рассмотреть возможность использования алгоритма Кнута-Фишера-Йейтса.
Джош Блихер Снайдер
Хорошо, я исправил код сейчас. В любом случае, я думаю, что основная идея перетасовки на месте масштабируется до произвольного числа массивов, чтобы избежать копирования.
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)
ajfbiw.s
источник
1
Это более или менее эквивалентно combo = zip(images, labels); shuffle(combo); im, lab = zip(*combo), просто медленнее. Так как вы все равно используете Numpy, гораздо более быстрым решением было бы сжать массивы с помощью Numpy combo = np.c_[images, labels], shuffle и снова разархивировать 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)
szZzr
источник