无需替换的内存高效随机数迭代器

Posted

技术标签:

【中文标题】无需替换的内存高效随机数迭代器【英文标题】:memory efficient random number iterator without replacement 【发布时间】:2012-05-23 19:25:42 【问题描述】:

我觉得这应该很容易,但经过多次搜索和尝试,我无法找到答案。基本上我有大量的物品,我想以随机顺序抽样而不更换。在这种情况下,它们是二维数组中的单元。我将用于较小数组的解决方案无法转换,因为它需要改组内存数组。如果我必须抽样的数量很小,我也可以随机抽样项目并保留我尝试过的值的列表。不幸的是,我经常不得不对所有细胞中的很大一部分进行采样,就像所有细胞一样多。

我想要创建的是一个迭代器,它使用 itertools、numpy 和/或 random 的某种组合来产生下一个随机单元格(x 和 y 索引)。另一种可能的解决方案是创建一个迭代器,该迭代器将产生 0 和 (x_count * y_count) 之间的下一个随机数(无需替换),我可以将其映射回一个单元格位置。这两件事似乎都不容易完成。

感谢您的任何建议!

这是我目前的解决方案。

import numpy as np
import itertools as itr
import random as rdm

#works great
x_count = 10
y_count = 5

#good luck!
#x_count = 10000
#y_count = 20000

x_indices = np.arange(x_count)
y_indices = np.arange(y_count)

cell_indices = itr.product(x_indices, y_indices)
list_cell_indices = list(cell_indices)
rdm.shuffle(list_cell_indices)

for i in range(25):
    print list_cell_indices[i]

因此,根据当前的响应以及我对 perl 的翻译我一无所知的尝试,我知道我能做的最好的事情如下:

import numpy as np
import itertools as itr
import random as rdm

x_count = 10000
y_count = 5000

sample_count = 10000
keep_probability = 0.01


tried_cells = set()
kept_cells = set()

while len(kept_cells) < sample_count:
    x = rdm.randint(0, x_count)
    y = rdm.randint(0, y_count)

    if (x, y) in tried_cells:
        pass
    else:
        tried_cells.add((x, y))
        keep = rdm.random() < keep_probability
        if keep:
            kept_cells.add((x,y))


print "worked"

在大多数情况下,使用的处理时间和内存并没有那么糟糕。也许我可以检查平均细胞 keep_probability 和 sample_count 并在困难的情况下抛出错误。

【问题讨论】:

如果您必须对所有这些样品进行采样,您为什么要问这个问题?指“尽可能多”。 如果您有 10K x 20K 的单元格,并且您必须对所有单元格进行随机抽样且无需替换,我认为没有办法绕过需要 O(x_count * y_count) 内存。 随机性必须有多好?统计随机?加密安全?您可以创建一个从range(x_count * y_count)range(x_count * y_count) 的随机排列,然后根据需要对它进行评估。见***.com/questions/3131193/… 在大多数情况下,我不必全部采样,甚至不需要采样那么多。 100,000,000 人中有 10,000 人说。我正在做的是检查每个单元格是否满足条件。如果没有,那么我把它扔掉并得到下一个。在某些情况下,这种情况是不可能的,我最终会扔掉很多细胞。最坏的情况是这种情况太不可能了,以至于我最终扔掉了所有的细胞并抛出了一个错误。 随机性必须在统计上合理。我看了你的交叉引用标记,但老实说,这有点超出我的想象。 【参考方案1】:

我相信,如果不使用大量辅助存储来处理接近R * C 的样本量,就根本无法对序列进行替换。虽然有一些巧妙的方法可以减少小样本量的存储量,但如果您希望对超过三分之一的数据集进行采样,最好只创建一个单独的列表。 random.sample 是为此目的的自然选择;坦率地说,我只是将你的二维 numpy 数组的扁平版本直接传递给它。 (除非您也需要索引,在这种情况下,随机采样整数并将它们转换为坐标,hexparrot 的解决方案是一种合理的方法。)

>>> a = numpy.arange(25).reshape((5, 5))
>>> random.sample(a.ravel(), 5)
[0, 13, 8, 18, 4]

如果您查看random.sample 的实现,您会发现对于较小的样本量,它已经大致完成了上面 perl 代码所做的工作——跟踪集合中先前选择的项目并丢弃已经在集合中的选择集。对于较大的样本量,它会创建输入的副本——这比用于较大值的集合更节省内存,因为集合比每个存储项目的列表占用更多空间——并且稍微修改了Fisher-Yates shuffle,当它有sample_size 项(即它不会打乱整个列表,所以它比自己打乱整个列表更有效。)

基本上,我敢打赌,你不会比random.sample 做得更好,滚动你自己的,除非你用c 编写代码。

然而——我确实找到了这个,你可能会觉得很有趣:numpy.random.choice。这似乎以 c 速度进行随机抽样,有或没有替换。捕获?这是 Numpy 1.7 的新功能!

【讨论】:

【参考方案2】:

这种方法怎么样。我首先创建 x*y 数组并将其重塑为二维。然后,知道每个单元格可以由一个整数唯一标识,得到一个从 0 到 (x*y) 的样本。

import numpy

x_count = 10000
y_count = 20000

x_indices = numpy.arange(x_count)
y_indices = numpy.arange(y_count)

large_table = numpy.arange(y_count * x_count).reshape(y_count, x_count)
print large_table

def get_random_item(sample_size):
    from random import sample
    for i in sample(xrange(y_count * x_count), sample_size):
        y,x = divmod(i, y_count)
        yield (x,y)

for x,y in get_random_item(10):
    print '%12i   x: %5i y: %5i' % (large_table[x][y],  x,y)

返回:

(首先模拟您通过产品创建的现有二维数组)

[[        0         1         2 ...,      9997      9998      9999]
 [    10000     10001     10002 ...,     19997     19998     19999]
 [    20000     20001     20002 ...,     29997     29998     29999]
 ..., 
 [199970000 199970001 199970002 ..., 199979997 199979998 199979999]
 [199980000 199980001 199980002 ..., 199989997 199989998 199989999]
 [199990000 199990001 199990002 ..., 199999997 199999998 199999999]]

然后,它返回 2-dim 坐标,可以简单地通过 array[x][y] 将其转换为您的单元格内容

   154080675   x: 15408 y:   675
   186978188   x: 18697 y:  8188
   157506087   x: 15750 y:  6087
   168859259   x: 16885 y:  9259
    29775768   x:  2977 y:  5768
    94167866   x:  9416 y:  7866
    15978144   x:  1597 y:  8144
    91964007   x:  9196 y:  4007
   163462830   x: 16346 y:  2830
    62613129   x:  6261 y:  3129

sample() 声明它“用于不带替换的随机抽样”,并且这种方法遵循建议“这对于从大量人口中抽样特别快速且节省空间:sample(xrange(10000000), 60)。”在 python random 页面上找到。

我注意到,虽然我使用 get_random_item() 作为生成器,但底层的 sample() 仍在生成一个完整的列表,因此内存使用量仍然是 y*x + sample_size,但它运行得非常快。

【讨论】:

仅供参考,一旦样本量增加到超过一定大小(由 n 项列表与 k 项集的相对大小决定),random.sample 将要抽样的整个人口复制到一个列表中。因此,对于较大的 n 和 k,这可能是低效的。 感谢您的信息;我不知道那个实现细节。 但是,+1 -- 没有比 random.sample 更好的答案了,imo -- 除了 numpy.random.choice,Numpy 1.7 中的新功能【参考方案3】:

您已经在使用 O(N=R*C) 内存,因此您不妨为迭代器使用 O(N) 内存。复制所有元素并对它们进行随机排序,就像对一维情况所做的那样。如果您要访问每个元素,这只是一个合理的做法,您说这是您的情况。

(为了记录:否则,内存不是一个糟糕的问题,因为您只需要“记住”您以前的位置。因此,您可以保留您已经访问过的索引列表。这个如果您确实计划访问每个元素,那就不好了,因为如果使用不断增长的黑名单来实施拒绝抽样可能会花费很长时间。(您也可以使用减少的白名单来实施它,这相当于第一个解决方案。)

【讨论】:

【参考方案4】:

您曾说过,您的网格中大多数单元格的测试可能会失败。如果是这种情况,随机抽样单元格可能不是一个好主意,因为如果不使用大量内存,您将很难跟踪已检查过的单元格。

相反,您最好将测试应用于整个网格并从通过它的元素中随机选择一个元素。

此函数返回一个通过测试的随机元素(如果都失败,则返回 None)。它使用的内存非常少。

def findRandomElementThatPasses(iterable, testFunc):
    value = None
    passed = 0

    for element in iterable:
        if testFunc(element):
            passed += 1
            if random.random() > 1.0/passed:
                value = element

    return value

你可以这样称呼它:

e = findRandomElementThatPasses((x,y) for x in xrange(X_SIZE)
                                      for y in xrange(Y_SIZE),
                                someFunctionTakingAnXYTuple)

如果您使用的是 Python 3,请使用 range 而不是 xrange。

【讨论】:

【参考方案5】:

在 perl 中你可以这样做:

# how many you got and need
$xmax = 10000000;
$ymax = 10000000;
$sampleSize = 10000;

# build hash, dictionary in python
$cells = ;
$count = 0;
while($count < $sampleSize) 
  $x = rand($xmax);
  $y = rand($ymax);
  if(! $cells->$x->$y) 
    $cells->$x->$y++;
    $count++;
  

# now grab the stuff
foreach ($x keys %$cells) 
  foreach ($y keys %$cells->$x) 
    getSample($x, $y);
  

没有重复,相当随机,内存也不太难。

【讨论】:

引用 OP:不幸的是,我经常需要对所有单元格中的很大一部分进行采样。 “相当随机”是一个非常危险的说法。你有任何衡量你的抽样分布是否确实平坦的指标吗? @Zhenya:只要rand()是统一的,我认为这个解决方案的分布属性没有什么问题。你有什么理由不这么认为吗? -0。当它是一个 python 问题时,不确定 perl 答案的意义是什么。 @aix:是的,我有。我无法评论 Perl 代码,我根本不知道。这就是为什么它是一个问题:你确定吗?有许多简单的黑客或技巧的例子,它们甚至以微妙的方式完全破坏了一个好的随机数生成器。因此我提倡一个经验法则:如果用 rand() 做任何不重要的事情,至少看看分布。是的,默认的 rand()-s 通常非常糟糕。

以上是关于无需替换的内存高效随机数迭代器的主要内容,如果未能解决你的问题,请参考以下文章

R中的内存高效分类器,用于极宽且不太长的训练集

itertools---------迭代器函数

python 高效的 itertools 模块

OpenCV 迭代随机森林训练

python itertools 模块讲解

STL 1.概论