以更好的方式同时打乱两个numpy数组

341
我有两个不同形状但长度相同(前导维度)的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 版本存活下来的东西。


25
六年后,我很惊讶并感到有趣的是这个问题竟然变得如此受欢迎。令人愉快的巧合的是,在Go 1.10中,我向标准库贡献了math/rand.Shuffle。该API的设计使得同时打乱两个数组变得非常简单,甚至在文档中还包含了一个示例。 - Josh Bleecher Snyder
7
这是一种不同的编程语言。 - Audrius Meškauskas
18个回答

480

你可以使用NumPy的数组索引

def unison_shuffled_copies(a, b):
    assert len(a) == len(b)
    p = numpy.random.permutation(len(a))
    return a[p], b[p]
这将导致创建单独的未排序合并数组。

23
这确实会创建副本,因为它使用了高级索引。但当然它比原来的方法更快。 - Sven Marnach
1
@mtrw:仅仅因为原始数组没有被改变并不排除返回的数组是相同数据的视图。但实际上它们并不是,因为NumPy视图不足以支持置换视图(这也不是期望的)。 - Sven Marnach
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(),对于每个1百万次调用,OP版本用了38秒,而我的版本只用了27.5秒。 - mtrw
4
我非常喜欢这个函数的简单易读性,而且高级索引仍然让我感到惊讶和惊奇;因此我会给这个答案加上+1。但令人奇怪的是,在我的大型数据集上,它比我的原始函数更慢:我的原始函数需要大约1.8秒来完成10次迭代,而这个函数需要大约2.7秒。这两个数字都非常稳定。我用来测试的数据集的形状为 a.shape(31925, 405)b.shape(31925,) - Josh Bleecher Snyder
1
也许,速度慢的原因在于你没有就地执行操作,而是创建了新的数组。或者与CPython解析数组索引的方式有关的某些缓慢。 - Íhor Mé
显示剩余3条评论

216

4
这个解决方案会创建副本("原始数组不受影响"),而作者的“可怕”解决方案则不会。 - bartolo-otrit
1
你可以选择任何你喜欢的风格。 - James

81

你所谓的“可怕”解决方案在我看来并不可怕。调用shuffle()函数对于两个相同长度的序列来说,会导致随机数生成器调用相同次数,而这些都是洗牌算法中唯一的“随机”元素。通过重置状态,您确保第二次调用shuffle()时,对随机数生成器的调用将给出相同的结果,因此整个算法将生成相同的排列。

如果您不喜欢这个解决方案,另一种解决方案是从一开始就将数据存储在一个数组中,并创建两个视图进入这个单一数组,模拟您现在拥有的两个数组。您可以使用单个数组进行洗牌,并使用视图进行所有其他目的。

示例:假设数组ab如下:

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

现在我们创建模拟原始 ab 的视图:

a2 = c[:, :a.size//len(a)].reshape(a.shape)
b2 = c[:, a.size//len(a):].reshape(b.shape)
a2b2的数据与c共享。要同时打乱这两个数组,请使用numpy.random.shuffle(c)
在生产代码中,您当然会尽量避免创建原始的ab,而是直接创建ca2b2
此解决方案可以适用于ab具有不同数据类型的情况。

1
回复:关于可怕的解决方案:我只是担心不同形状的数组可能会产生不同次数的rng调用,这可能会导致发散。然而,我认为你是正确的,目前的行为可能不太可能改变,非常简单的doctest确实可以轻松确认正确的行为... - Josh Bleecher Snyder
我喜欢你提出的方法,肯定可以安排a和b作为一个统一的c数组开始生活。然而,在洗牌后不久,a和b需要是连续的(为了有效地传输到GPU),所以我认为,在我的特定情况下,我最终仍然会复制a和b。 :( - Josh Bleecher Snyder
1
@Josh:请注意,numpy.random.shuffle() 可以操作任意可变序列,例如 Python 列表或 NumPy 数组。数组形状并不重要,只有序列的长度才重要。在我看来,这种情况非常不可能改变。 - Sven Marnach
我不知道这个。这让我更加放心了。谢谢。 - Josh Bleecher Snyder
@SvenMarnach:我在下面发布了一个答案。您能否评论一下您是否认为它有意义/是一个好的方法来做到这一点? - ajfbiw.s
显示剩余4条评论

46

非常简单的解决方案:

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

现在,两个数组x和y以相同的方式被随机打乱。


7
这相当于mtrw的解决方案。你前两行只是生成排列,但这可以一行完成。 - Josh Bleecher Snyder

28

James在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)

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

这基本上与得票最高的答案完全相同。 - Bernhard Barker

21

使用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范围内。
  • 如果需要可重复的shuffle,则可以设置种子值。

在shuffle之后,数据可以使用np.split拆分或使用切片引用,具体取决于应用程序。


3
漂亮的解决方案,对我非常有效。即使在三维以上的数组中也能正常运作。 - wprins
3
这是正确的答案。当你可以传递随机状态对象时,没有理由使用全局的np.random。 - Erotemic
一个 RandomState 可以在循环外使用。请参考亚当·斯奈德的 回答 - bartolo-otrit
1
@bartolo-otrit,在for循环中需要做出的选择是重新分配还是重新设置随机状态。由于传递到洗牌函数中的数组数量预计很少,我不会期望两者之间有性能差异。但是,是的,rstate可以在循环外分配并在每次迭代中重新设置种子。 - Isaac B

13

你可以像这样创建一个数组:

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

7
有一个广为人知的函数可以处理这个问题:
from sklearn.model_selection import train_test_split
X, _, Y, _ = train_test_split(X,Y, test_size=0.0)

将test_size设置为0将避免分割数据并给您洗牌后的数据。虽然它通常用于拆分训练和测试数据,但它也会对它们进行洗牌。
来自文档 将数组或矩阵随机拆分为训练和测试子集的快速实用程序。它包装了输入验证和next(ShuffleSplit().split(X, y))以及应用于输入数据的单个调用,以便在一行代码中拆分(可选地进行子采样)数据。

1
我简直不敢相信我从来没有想到过这个。你的答案真是太棒了。 - curiouscupcake
sklearn 有什么变化吗?这个解决方案对我不起作用,会抛出 ValueError。 - theProcrastinator
我没有在这个函数中看到任何变化。请检查您传递的数据类型是否正确(任何类似数组的类型都可以),并且检查数组是否具有相同的形状。 - sziraqui

7

这似乎是一个非常简单的解决方案:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接