web-dev-qa-db-ja.com

Pythonジェネレータ付きのランダムサンプル/反復可能/イテレータ

Pythonのrandom.sampleをジェネレーターオブジェクトで動作させる方法があるかどうかを知っていますか。非常に大きなテキストコーパスからランダムなサンプルを取得しようとしています。問題は、random.sample()が次のエラーを発生させることです。

TypeError: object of type 'generator' has no len()

itertoolsの何かを使ってこれを行う方法があるのではないかと思っていましたが、少し検索しても何も見つかりませんでした。

やや構成された例:

import random
def list_item(ls):
    for item in ls:
        yield item

random.sample( list_item(range(100)), 20 )


[〜#〜]更新[〜#〜]


MartinPietersのリクエストに従って、現在提案されている3つの方法のタイミングを調整しました。結果は以下の通りです。

Sampling 1000 from 10000
Using iterSample 0.0163 s
Using sample_from_iterable 0.0098 s
Using iter_sample_fast 0.0148 s

Sampling 10000 from 100000
Using iterSample 0.1786 s
Using sample_from_iterable 0.1320 s
Using iter_sample_fast 0.1576 s

Sampling 100000 from 1000000
Using iterSample 3.2740 s
Using sample_from_iterable 1.9860 s
Using iter_sample_fast 1.4586 s

Sampling 200000 from 1000000
Using iterSample 7.6115 s
Using sample_from_iterable 3.0663 s
Using iter_sample_fast 1.4101 s

Sampling 500000 from 1000000
Using iterSample 39.2595 s
Using sample_from_iterable 4.9994 s
Using iter_sample_fast 1.2178 s

Sampling 2000000 from 5000000
Using iterSample 798.8016 s
Using sample_from_iterable 28.6618 s
Using iter_sample_fast 6.6482 s

したがって、サンプルサイズが大きい場合、array.insertには重大な欠点があることがわかります。メソッドの時間を計るために使用したコード

from heapq import nlargest
import random
import timeit


def iterSample(iterable, samplesize):
    results = []
    for i, v in enumerate(iterable):
        r = random.randint(0, i)
        if r < samplesize:
            if i < samplesize:
                results.insert(r, v) # add first samplesize items in random order
            else:
                results[r] = v # at a decreasing rate, replace random items

    if len(results) < samplesize:
        raise ValueError("Sample larger than population.")

    return results

def sample_from_iterable(iterable, samplesize):
    return (x for _, x in nlargest(samplesize, ((random.random(), x) for x in iterable)))

def iter_sample_fast(iterable, samplesize):
    results = []
    iterator = iter(iterable)
    # Fill in the first samplesize elements:
    for _ in xrange(samplesize):
        results.append(iterator.next())
    random.shuffle(results)  # Randomize their positions
    for i, v in enumerate(iterator, samplesize):
        r = random.randint(0, i)
        if r < samplesize:
            results[r] = v  # at a decreasing rate, replace random items

    if len(results) < samplesize:
        raise ValueError("Sample larger than population.")
    return results

if __name__ == '__main__':
    pop_sizes = [int(10e+3),int(10e+4),int(10e+5),int(10e+5),int(10e+5),int(10e+5)*5]
    k_sizes = [int(10e+2),int(10e+3),int(10e+4),int(10e+4)*2,int(10e+4)*5,int(10e+5)*2]

    for pop_size, k_size in Zip(pop_sizes, k_sizes):
        pop = xrange(pop_size)
        k = k_size
        t1 = timeit.Timer(stmt='iterSample(pop, %i)'%(k_size), setup='from __main__ import iterSample,pop')
        t2 = timeit.Timer(stmt='sample_from_iterable(pop, %i)'%(k_size), setup='from __main__ import sample_from_iterable,pop')
        t3 = timeit.Timer(stmt='iter_sample_fast(pop, %i)'%(k_size), setup='from __main__ import iter_sample_fast,pop')

        print 'Sampling', k, 'from', pop_size
        print 'Using iterSample', '%1.4f s'%(t1.timeit(number=100) / 100.0)
        print 'Using sample_from_iterable', '%1.4f s'%(t2.timeit(number=100) / 100.0)
        print 'Using iter_sample_fast', '%1.4f s'%(t3.timeit(number=100) / 100.0)
        print ''

また、テストを実行して、すべてのメソッドがジェネレーターの公平なサンプルを実際に取得していることを確認しました。したがって、すべてのメソッドについて、100010000回から100000要素をサンプリングし、母集団内の各項目の平均出現頻度を計算しました。これは、3つのメソッドすべてに期待されるとおり、~.1であることがわかります。

38
Matti Lyra

Martijn Pietersの答えは正しいですが、ループでlist.insertを使用すると2次の複雑さが生じる可能性があるため、samplesizeが大きくなると遅くなります。

私の意見では、パフォーマンスを向上させながら均一性を維持する代替案があります:

def iter_sample_fast(iterable, samplesize):
    results = []
    iterator = iter(iterable)
    # Fill in the first samplesize elements:
    try:
        for _ in xrange(samplesize):
            results.append(iterator.next())
    except StopIteration:
        raise ValueError("Sample larger than population.")
    random.shuffle(results)  # Randomize their positions
    for i, v in enumerate(iterator, samplesize):
        r = random.randint(0, i)
        if r < samplesize:
            results[r] = v  # at a decreasing rate, replace random items
    return results

10000を超えるsamplesize値の違いは徐々に現れ始めます。 (1000000, 100000)での呼び出し時間:

  • iterSample:5.05s
  • iter_sample_fast:2.64s
22
DzinX

できません。

2つのオプションがあります。ジェネレーター全体をリストに読み込み、そのリストからサンプリングするか、ジェネレーターを1つずつ読み取り、そこからサンプルを選択するメソッドを使用します。

import random

def iterSample(iterable, samplesize):
    results = []

    for i, v in enumerate(iterable):
        r = random.randint(0, i)
        if r < samplesize:
            if i < samplesize:
                results.insert(r, v) # add first samplesize items in random order
            else:
                results[r] = v # at a decreasing rate, replace random items

    if len(results) < samplesize:
        raise ValueError("Sample larger than population.")

    return results

このメソッドは、反復可能なso far内のアイテム数に基づいて、次のアイテムがサンプルの一部である可能性を調整します。 samplesize以上のアイテムをメモリに保持する必要はありません。

解決策は私のものではありません。 SOに関する別の回答 の一部として提供されました。

17
Martijn Pieters

簡単に言うと、O -n lgで生成されたnアイテムからの置き換えなしでk要素をサンプリングするワンライナーは次のとおりです。 k)時間:

from heapq import nlargest

def sample_from_iterable(it, k):
    return (x for _, x in nlargest(k, ((random.random(), x) for x in it)))
7
Fred Foo

非常に大きなテキストコーパスからランダムなサンプルを取得しようとしています。

優れた合成の答え は現在、iter_sample_fast(gen, pop)の勝利を示しています。しかし、私はrandom.sample(list(gen), pop)のKatrielの推奨を試しました—それに比べて、それは非常に高速です!

_def iter_sample_easy(iterable, samplesize):
    return random.sample(list(iterable), samplesize)

Sampling 1000 from 10000
Using iter_sample_fast 0.0192 s
Using iter_sample_easy 0.0009 s

Sampling 10000 from 100000
Using iter_sample_fast 0.1807 s
Using iter_sample_easy 0.0103 s

Sampling 100000 from 1000000
Using iter_sample_fast 1.8192 s
Using iter_sample_easy 0.2268 s

Sampling 200000 from 1000000
Using iter_sample_fast 1.7467 s
Using iter_sample_easy 0.3297 s

Sampling 500000 from 1000000
Using iter_sample_easy 0.5628 s

Sampling 2000000 from 5000000
Using iter_sample_easy 2.7147 s
_

コーパスが非常に大きくなると、イテラブル全体をlistに具体化すると、非常に大量のメモリが使用されます。しかし、問題をチャンクアップできれば、Pythonの超高速性を引き続き利用できます(基本的に、CHUNKSIZEを選択します) 「かなり小さい」の場合、そのサイズのチャンクに対して_random.sample_を実行してから、もう一度_random.sample_を使用してそれらをマージします。境界条件を正しく設定する必要があります。

list(iterable)の長さがCHUNKSIZEの正確な倍数であり、_samplesize*CHUNKSIZE_以下である場合の方法を確認します。

_def iter_sample_dist_naive(iterable, samplesize):
    CHUNKSIZE = 10000
    samples = []
    it = iter(iterable)
    try:
        while True:
            first = next(it)
            chunk = itertools.chain([first], itertools.islice(it, CHUNKSIZE-1))
            samples += iter_sample_easy(chunk, samplesize)
    except StopIteration:
        return random.sample(samples, samplesize)
_

ただし、上記のコードはlen(list(iterable)) % CHUNKSIZE != 0の場合に不均一なサンプリングを生成し、len(list(iterable)) * samplesize / CHUNKSIZEが「非常に大きく」なるとメモリ不足になります。これらのバグの修正は私の給与水準を上回っていますが、恐れますが、解決策は このブログ投稿 で説明されており、私にはかなり合理的に聞こえます。 (検索キーワード:「分散ランダムサンプリング」、「分散リザーバーサンプリング」)

_Sampling 1000 from 10000
Using iter_sample_fast 0.0182 s
Using iter_sample_dist_naive 0.0017 s
Using iter_sample_easy 0.0009 s

Sampling 10000 from 100000
Using iter_sample_fast 0.1830 s
Using iter_sample_dist_naive 0.0402 s
Using iter_sample_easy 0.0103 s

Sampling 100000 from 1000000
Using iter_sample_fast 1.7965 s
Using iter_sample_dist_naive 0.6726 s
Using iter_sample_easy 0.2268 s

Sampling 200000 from 1000000
Using iter_sample_fast 1.7467 s
Using iter_sample_dist_naive 0.8209 s
Using iter_sample_easy 0.3297 s
_

本当に勝つのは、samplesizelen(list(iterable))に比べて非常に小さい場合です。

_Sampling 20 from 10000
Using iterSample 0.0202 s
Using sample_from_iterable 0.0047 s
Using iter_sample_fast 0.0196 s
Using iter_sample_easy 0.0001 s
Using iter_sample_dist_naive 0.0004 s

Sampling 20 from 100000
Using iterSample 0.2004 s
Using sample_from_iterable 0.0522 s
Using iter_sample_fast 0.1903 s
Using iter_sample_easy 0.0016 s
Using iter_sample_dist_naive 0.0029 s

Sampling 20 from 1000000
Using iterSample 1.9343 s
Using sample_from_iterable 0.4907 s
Using iter_sample_fast 1.9533 s
Using iter_sample_easy 0.0211 s
Using iter_sample_dist_naive 0.0319 s

Sampling 20 from 10000000
Using iterSample 18.6686 s
Using sample_from_iterable 4.8120 s
Using iter_sample_fast 19.3525 s
Using iter_sample_easy 0.3162 s
Using iter_sample_dist_naive 0.3210 s

Sampling 20 from 100000000
Using iter_sample_easy 2.8248 s
Using iter_sample_dist_naive 3.3817 s
_
2
Quuxplusone

イテレータ内のアイテムの数がわかっている場合(別の場所でアイテムを数えることにより)、別のアプローチは次のとおりです。

def iter_sample(iterable, iterlen, samplesize):
    if iterlen < samplesize:
        raise ValueError("Sample larger than population.")
    indexes = set()
    while len(indexes) < samplesize:
        indexes.add(random.randint(0,iterlen))
    indexesiter = iter(sorted(indexes))
    current = indexesiter.next()
    ret = []
    for i, item in enumerate(iterable):
        if i == current:
            ret.append(item)
            try:
                current = indexesiter.next()
            except StopIteration:
                break
    random.shuffle(ret)
    return ret

特にsampsizeがiterlenに比べて小さい場合、私はこれがより速いことを見つけます。ただし、全体、または全体に近いサンプルを要求された場合は問題があります。

iter_sample(iterlen = 10000、samplesize = 100)時間:(1、 'ms')iter_sample_fast(iterlen = 10000、samplesize = 100)時間:(15、 'ms')

iter_sample(iterlen = 1000000、samplesize = 100)時間:(65、 'ms')iter_sample_fast(iterlen = 1000000、samplesize = 100)時間:(1477、 'ms')

iter_sample(iterlen = 1000000、samplesize = 1000)時間:(64、 'ms')iter_sample_fast(iterlen = 1000000、samplesize = 1000)時間:(1459、 'ms')

iter_sample(iterlen = 1000000、samplesize = 10000)時間:(86、 'ms')iter_sample_fast(iterlen = 1000000、samplesize = 10000)時間:(1480、 'ms')

iter_sample(iterlen = 1000000、samplesize = 100000)時間:(388、 'ms')iter_sample_fast(iterlen = 1000000、samplesize = 100000)時間:(1521、 'ms')

iter_sample(iterlen = 1000000、samplesize = 1000000)時間:(25359、 'ms')iter_sample_fast(iterlen = 1000000、samplesize = 1000000)時間:(2178、 'ms')

0
blitzen

母集団のサイズnがわかっている場合、ターゲットサンプルのみを抽出して、ジェネレーターをループするメモリ効率の良いコードを次に示します。

from random import sample
from itertools import count, compress

targets = set(sample(range(n), k=10))
for selection in compress(pop, map(targets.__contains__, count())):
    print(selection)

これにより、選択は人口生成プログラムによって生成された順序で出力されます。

テクニックは、標準ライブラリrandom.sample()を使用して、選択対象のターゲットインデックスをランダムに選択することです。 2番目は、指定されたインデックスがターゲットの中にあるかどうかを判断し、そうである場合はジェネレーターからの対応する値を提供します。

たとえば、与えられたターゲット{6, 2, 4}

0  1  2  3  4  5  6  7  8  9  10   ...  output of count()
F  F  T  F  T  F  T  F  F  F  F    ...  is the count in targets?
A  B  C  D  E  F  G  H  I  J  K    ...  output of the population generator
-  -  C  -  E  -  G  -  -  -  -    ...  selections emitted by compress

この手法は、コーパスをループしてメモリに収まらない場合にループするのに適しています(そうでない場合は、母集団に対してsample()を直接使用できます)。

0

ジェネレーターの長さ(そして漸近的に均一に分散される)について考えている場合に証明されるまでの最速の方法:

def gen_sample(generator_list, sample_size, iterlen):
    num = 0
    inds = numpy.random.random(iterlen) <= (sample_size * 1.0 / iterlen)
    results = []
    iterator = iter(generator_list)
    gotten = 0
    while gotten < sample_size: 
        try:
            b = iterator.next()
            if inds[num]: 
                results.append(b)
                gotten += 1
            num += 1    
        except: 
            num = 0
            iterator = iter(generator_list)
            inds = numpy.random.random(iterlen) <= ((sample_size - gotten) * 1.0 / iterlen)
    return results

小さなイテラブルと巨大なイテラブルの両方で最も高速です(おそらくその間すべて)

# Huge
res = gen_sample(xrange(5000000), 200000, 5000000)
timing: 1.22s

# Small
z = gen_sample(xrange(10000), 1000, 10000) 
timing: 0.000441    
0
PascalVKooten