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

У меня есть два массива numpy разных форм, но с одинаковой длиной (ведущее измерение). Я хочу перемешать каждый из них, чтобы соответствующие элементы продолжали соответствовать, то есть перемешать их в унисон относительно их ведущих индексов.

Этот код работает и иллюстрирует мои цели:

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)

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


person Josh Bleecher Snyder    schedule 05.01.2011    source источник
comment
Шесть лет спустя меня позабавило и удивило, насколько популярным оказался этот вопрос. И, по приятному совпадению, для Go 1.10 я добавил math / rand.Shuffle в стандартную библиотеку . Конструкция API делает тривиальным перемешивание двух массивов в унисон, и это даже включено в качестве примера в документацию.   -  person Josh Bleecher Snyder    schedule 02.12.2017
comment
Однако это другой язык программирования.   -  person Audrius Meskauskas    schedule 15.03.2021


Ответы (15)


Ваше «страшное» решение мне не кажется страшным. Вызов 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 имеют разные типы dtyp.

person Sven Marnach    schedule 05.01.2011
comment
Re: пугающее решение: я просто беспокоюсь, что массивы разных форм могут (предположительно) давать разное количество вызовов rng, что может вызвать расхождение. Однако я думаю, что вы правы в том, что текущее поведение вряд ли изменится, и очень простой документальный тест позволяет очень легко подтвердить правильное поведение ... - person Josh Bleecher Snyder; 05.01.2011
comment
Мне нравится ваш предложенный подход, и я определенно мог бы организовать запуск a и b в виде единого массива c. Тем не менее, a и b должны будут быть смежными вскоре после перетасовки (для эффективной передачи на графический процессор), поэтому я думаю, что в моем конкретном случае я все равно буду делать копии a и b. :( - person Josh Bleecher Snyder; 05.01.2011
comment
@Josh: Обратите внимание, что numpy.random.shuffle() работает с произвольными изменяемыми последовательностями, такими как списки Python или массивы NumPy. Форма массива не имеет значения, важна только длина последовательности. На мой взгляд, это очень вряд ли изменится. - person Sven Marnach; 05.01.2011
comment
Я этого не знал. Это делает меня более комфортным. Спасибо. - person Josh Bleecher Snyder; 05.01.2011
comment
@SvenMarnach: Я опубликовал ответ ниже. Можете ли вы прокомментировать, считаете ли вы, что это имеет смысл / является ли это хорошим способом сделать это? - person ajfbiw.s; 10.02.2016
comment
Есть ли вероятность, что numpy будет обновляться для автоматического изменения состояния RNG всякий раз, когда вызывается случайная функция? - person Abhimanyu Pallavi Sudhir; 23.06.2020
comment
@AbhimanyuPallaviSudhir Я не совсем понимаю, о чем вы говорите. Состояние ГСЧ увеличивается всякий раз, когда вы вызываете функцию с использованием случайных битов - иначе вы бы получали одни и те же биты при каждом вызове. - person Sven Marnach; 23.06.2020
comment
@SvenMarnach Продвигается ли он до или после вызова такой случайной функции? Я догадываюсь после. Я говорю, что если они поменяют это на раньше? (Предположительно следующее состояние не является детерминированной функцией текущего состояния, т.е. оно зависит от текущего времени или чего-то подобного - правильно?) - person Abhimanyu Pallavi Sudhir; 23.06.2020
comment
@AbhimanyuPallaviSudhir Состояние изменяется во время вызова функции, как часть генерации случайного числа. Ни до, ни после. И новое состояние является детерминированной, чистой функцией старого состояния, поэтому хорошие ГПСЧ имеют много энтропии в своем состоянии. Обычно вы засеваете состояние некоторой недетерминированной энтропией, но после этого все дальнейшие шаги детерминированы. - person Sven Marnach; 23.06.2020

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

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

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

person mtrw    schedule 05.01.2011
comment
Этот действительно создает копии, поскольку использует расширенное индексирование. Но конечно быстрее оригинала. - person Sven Marnach; 05.01.2011
comment
@mtrw: Тот факт, что исходные массивы остаются нетронутыми, не отменяет того, что возвращенные массивы являются представлениями одних и тех же данных. Но на самом деле это не так, поскольку представления NumPy недостаточно гибки для поддержки пермутируемых представлений (это тоже нежелательно). - person Sven Marnach; 05.01.2011
comment
Я пробовал эту функцию с модулем времени, и она не быстрее предыдущей. Я сделал что-то не так? - person Dat Chu; 05.01.2011
comment
@Sven - Мне действительно нужно узнать о просмотрах. @ Дат Чу - Я только что попробовал >>> 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 секунд для версии OP и 27,5 секунды для моей, на 1 миллион вызовов каждая. - person mtrw; 05.01.2011
comment
Мне очень нравится простота и удобочитаемость этого, а расширенное индексирование продолжает меня удивлять и удивлять; за это этот ответ легко получает +1. Однако, как ни странно, в моих (больших) наборах данных она работает медленнее, чем моя исходная функция: мой оригинал занимает ~ 1,8 секунды на 10 итераций, а это занимает ~ 2,7 секунды. Оба числа вполне согласуются. Набор данных, который я использовал для тестирования, имеет a.shape (31925, 405) и b.shape (31925,). - person Josh Bleecher Snyder; 05.01.2011
comment
@ Джош - да, ты прав. Это действительно странно. Я пробовал квадратный массив (NxN) и линейный массив (N), и ваша исходная функция работает быстрее, начиная с N = 75. Я в недоумении. Но, как заметил Свен, ваша вторая идея сброса состояния ГСЧ, вероятно, является самым простым способом в любом случае. - person mtrw; 05.01.2011
comment
Возможно, медлительность связана с тем, что вы не делаете что-то на месте, а вместо этого создаете новые массивы. Или с некоторой медлительностью, связанной с тем, как CPython анализирует индексы массивов. - person Íhor Mé; 20.10.2016
comment
Спасибо @mtrw за ваш сервис для Python. Боги благоволят тебе. - person legel; 24.12.2020

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

person James    schedule 04.06.2015
comment
Это решение создает копии (исходный на массивы не влияют), а на пугающее решение автора - нет. - person bartolo-otrit; 14.03.2020
comment
Вы можете выбрать любой стиль по своему вкусу - person James; 16.03.2020

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

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

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

person connor    schedule 08.06.2016
comment
Это эквивалентно решению mtrw. Ваши первые две строки просто генерируют перестановку, но это можно сделать в одной строке. - person Josh Bleecher Snyder; 09.06.2016

Джеймс написал в 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)
person Daniel    schedule 30.05.2018
comment
Безусловно, самый чистый и простой ответ: пальцы вверх: - person Dylan Kerler; 08.02.2021

Перемешайте любое количество массивов на месте, используя только 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 или на них можно ссылаться с помощью фрагментов - в зависимости от приложения.

person Isaac B    schedule 25.07.2018
comment
красивое решение, это сработало идеально для меня. Даже с массивами из 3+ осей - person wprins; 01.11.2018
comment
Это правильный ответ. Нет причин использовать глобальный np.random, когда вы можете передавать случайные объекты состояния. - person Erotemic; 04.02.2020
comment
Один RandomState может использоваться вне цикла. См. ответ Адама Снайдера. - person bartolo-otrit; 14.03.2020
comment
@ bartolo-otrit, выбор, который должен быть сделан в цикле for, состоит в том, переназначить или повторно установить случайное состояние. Учитывая, что количество массивов, передаваемых в функцию перетасовки, должно быть небольшим, я не ожидал бы разницы в производительности между ними. Но да, rstate можно назначить вне цикла и повторно использовать внутри цикла на каждой итерации. - person Isaac B; 15.03.2020

вы можете создать такой массив:

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

затем перемешайте:

np.random.shuffle(s)

теперь используйте этот s как аргумент ваших массивов. те же перетасованные аргументы возвращают одинаковые перетасованные векторы.

x_data = x_data[s]
x_label = x_label[s]
person mohammad hassan bigdeli shamlo    schedule 01.04.2018
comment
На самом деле, это лучшее решение, и оно должно быть принято! Он работает даже для многих (более 2) массивов одновременно. Идея проста: просто перемешайте список индексов [0, 1, 2, ..., n-1], а затем переиндексируйте строки массивов с перемешанными индексами. Отлично! - person Basj; 16.11.2018

Есть хорошо известная функция, которая может с этим справиться:

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)) и приложение для ввода данных в один вызов для разделения (и, возможно, субдискретизации) данных в единую строку.

person sziraqui    schedule 07.11.2018
comment
Не могу поверить, что никогда не думал об этом. Ваш ответ великолепен. - person Long Nguyen; 19.11.2019
comment
Что-то изменилось в sklearn? Это решение не работает для меня и вызывает ошибку ValueError. - person YashvanderBamel; 02.06.2021
comment
Я не вижу изменений в этой функции. Убедитесь, что вы передаете правильный тип данных (подойдет любой тип, подобный массиву), а также проверьте, имеют ли массивы одинаковую форму. - person sziraqui; 04.06.2021

Один из способов перетасовки на месте для связанных списков - использование начального числа (оно может быть случайным) и использование 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]

Изменить: исправлен код для повторного заполнения случайного состояния

person Adam Snaider    schedule 30.11.2017
comment
Этот код не работает. RandomState изменяет состояние при первом вызове, а a и b не перемешиваются одновременно. - person Bruno Klein; 24.01.2018
comment
@BrunoKlein Вы правы. Я исправил сообщение, чтобы повторно заполнить случайное состояние. Кроме того, даже если это не в унисон в том смысле, что оба списка перетасовываются одновременно, они согласованы в том смысле, что оба перетасовываются одинаково, и также не требуется больше памяти для хранения копия списков (которые OP упоминает в своем вопросе) - person Adam Snaider; 20.02.2018

Это кажется очень простым решением:

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]))
person andy    schedule 17.04.2020

Скажем, у нас есть два массива: 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]]
person monolith    schedule 05.12.2018
comment
Почему не просто a [индексы ,:] или b [индексы ,:]? - person Kev; 31.01.2019

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

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]

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

person DaveP    schedule 05.01.2011
comment
codinghorror.com/blog/2007/12/the- dangerous-of-naivete.html заставило меня опасаться реализации моих собственных алгоритмов перемешивания; отчасти именно поэтому я задаю этот вопрос. :) Однако вы совершенно правы, когда указываете, что я должен рассмотреть возможность использования алгоритма Кнута-Фишера-Йейтса. - person Josh Bleecher Snyder; 05.01.2011
comment
Хорошо замечено, я исправил код сейчас. В любом случае, я думаю, что основная идея перетасовки на месте масштабируется до произвольного числа массивов, избегая создания копий. - person DaveP; 05.01.2011
comment
Код по-прежнему неверный (он даже не запускается). Чтобы он заработал, замените len(a) на reversed(range(1, len(a))). Но все равно это будет не очень эффективно. - person Sven Marnach; 05.01.2011

В качестве примера я делаю следующее:

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)
person ajfbiw.s    schedule 10.02.2016
comment
Это более или менее эквивалентно combo = zip(images, labels); shuffle(combo); im, lab = zip(*combo), только медленнее. Поскольку вы все равно используете Numpy, гораздо более быстрым решением было бы заархивировать массивы с помощью Numpy combo = np.c_[images, labels], перемешать и снова разархивировать images, labels = combo.T. Если исходить из предположения, что labels и images - одномерные массивы Numpy одинаковой длины, это будет самым быстрым решением. Если они многомерные, см. Мой ответ выше. - person Sven Marnach; 10.02.2016
comment
Хорошо, это имеет смысл. Спасибо! @SvenMarnach - person ajfbiw.s; 10.02.2016

Я расширил 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]

Таким образом, я могу быть уверен, что перетасовка происходит на месте, а функция не слишком длинная или сложная.

person Ivo    schedule 30.10.2017

Просто используйте _1 _...

Сначала объедините два входных массива, 1D-массив - это метки (y), а 2D-массив - это данные (x), и перемешайте их с помощью метода NumPy shuffle. Наконец разделите их и вернитесь.

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)
person szZzr    schedule 02.04.2020