Savez-vous s'il existe un moyen de faire fonctionner le random.sample
De python avec un objet générateur. J'essaie d'obtenir un échantillon aléatoire d'un très grand corpus de texte. Le problème est que random.sample()
déclenche l'erreur suivante.
TypeError: object of type 'generator' has no len()
Je pensais qu'il y avait peut-être un moyen de le faire avec quelque chose de itertools
mais je ne pouvais rien trouver avec un peu de recherche.
Un exemple quelque peu inventé:
import random
def list_item(ls):
for item in ls:
yield item
random.sample( list_item(range(100)), 20 )
MISE À JOUR
Conformément à la demande de MartinPieters
, j'ai fait un certain timing des trois méthodes actuellement proposées. Les résultats sont les suivants.
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
Il s'avère donc que le array.insert
Présente un sérieux inconvénient en ce qui concerne les grands échantillons. Le code que j'ai utilisé pour chronométrer les méthodes
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 ''
J'ai également effectué un test pour vérifier que toutes les méthodes prennent effectivement un échantillon non biaisé du générateur. Donc, pour toutes les méthodes, j'ai échantillonné 1000
Éléments à partir de 10000
100000
Fois et calculé la fréquence moyenne d'occurrence de chaque élément de la population qui se révèle être ~.1
comme on peut s'y attendre pour les trois méthodes.
Bien que la réponse de Martijn Pieters soit correcte, elle ralentit lorsque samplesize
devient grand, car l'utilisation de list.insert
dans une boucle peut avoir une complexité quadratique.
Voici une alternative qui, à mon avis, préserve l'uniformité tout en augmentant les performances:
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
La différence commence lentement à apparaître pour les valeurs de samplesize
supérieures à 10000
. Heures d'appel avec (1000000, 100000)
:
Tu ne peux pas.
Vous avez deux options: lire l'intégralité du générateur dans une liste, puis échantillonner à partir de cette liste, ou utiliser une méthode qui lit le générateur un par un et sélectionne l'échantillon à partir de cela:
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
Cette méthode ajuste la chance que l'élément suivant fasse partie de l'échantillon en fonction du nombre d'éléments dans l'itérable jusqu'à présent . Il n'a pas besoin de contenir plus de samplesize
éléments en mémoire.
La solution n'est pas la mienne; il a été fourni dans le cadre de ne autre réponse ici sur SO .
Juste pour le plaisir, voici une ligne qui échantillonne k éléments sans remplacement des n éléments générés en O (n lg - k) temps:
from heapq import nlargest
def sample_from_iterable(it, k):
return (x for _, x in nlargest(k, ((random.random(), x) for x in it)))
J'essaie d'obtenir un échantillon aléatoire d'un très grand corpus de texte.
Votre excellente réponse de synthèse montre actuellement la victoire de iter_sample_fast(gen, pop)
. Cependant, j'ai essayé la recommandation de Katriel de random.sample(list(gen), pop)
- et c'est incroyablement rapide en comparaison!
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
Maintenant, à mesure que votre corpus devient très grand , matérialiser tout l'itérable en list
utilisera des quantités de mémoire prohibitives. Mais nous pouvons toujours exploiter la rapidité fulgurante de Python si nous pouvons couper le problème : en gros, nous choisissons un CHUNKSIZE
qui est "raisonnablement petit", faites random.sample
sur des morceaux de cette taille, puis utilisez à nouveau random.sample
pour les fusionner. Nous devons simplement obtenir les bonnes conditions aux limites.
Je vois comment le faire si la longueur de list(iterable)
est un multiple exact de CHUNKSIZE
et pas plus grand que 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)
Toutefois, le code ci-dessus produit un échantillonnage non uniforme lorsque len(list(iterable)) % CHUNKSIZE != 0
, et il manque de mémoire lorsque len(list(iterable)) * samplesize / CHUNKSIZE
devient "très grand". La correction de ces bogues est au-dessus de ma note de rémunération, je le crains, mais une solution est décrite dans ce billet de blog et semble assez raisonnable pour moi. (Termes de recherche: "échantillonnage aléatoire distribué", "échantillonnage de réservoir distribué".)
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
Là où nous gagnons vraiment, c'est quand samplesize
est très petit par rapport à len(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
Si le nombre d'articles dans l'itérateur est connu (en comptant ailleurs les articles), une autre approche est:
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
Je trouve cela plus rapide, surtout quand sampsize est petit par rapport à iterlen. Cependant, lorsque l’ensemble ou presque l’échantillon est demandé, il y a des problèmes.
iter_sample (iterlen = 10000, samplesize = 100) time: (1, 'ms') iter_sample_fast (iterlen = 10000, samplesize = 100) time: (15, 'ms')
iter_sample (iterlen = 1000000, samplesize = 100) time: (65, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 100) time: (1477, 'ms')
iter_sample (iterlen = 1000000, samplesize = 1000) time: (64, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 1000) time: (1459, 'ms')
iter_sample (iterlen = 1000000, samplesize = 10000) time: (86, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 10000) time: (1480, 'ms')
iter_sample (iterlen = 1000000, samplesize = 100000) time: (388, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 100000) time: (1521, 'ms')
iter_sample (iterlen = 1000000, samplesize = 1000000) time: (25359, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 1000000) time: (2178, 'ms')
Si la taille de la population n est connue, voici un code efficace en mémoire qui boucle sur un générateur, en extrayant uniquement les échantillons cibles:
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)
Cela affiche les sélections dans l'ordre où elles sont produites par le générateur de population.
La technique consiste à utiliser la bibliothèque standard random.sample () pour sélectionner au hasard les indices cibles pour les sélections. Le second comme détermine si un indice donné est parmi les cibles et si c'est le cas donne la valeur correspondante du générateur.
Par exemple, compte tenu des cibles de {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
Cette technique convient pour boucler sur un corpus trop grand pour tenir en mémoire (sinon, vous pouvez simplement utiliser sample () directement sur la population).
Méthode la plus rapide jusqu'à preuve du contraire lorsque vous avez une idée de la durée du générateur (et qu'il sera distribué de manière asymptotique uniforme):
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
Il est à la fois le plus rapide sur le petit itérable ainsi que l'énorme itérable (et probablement tous les deux entre-temps)
# Huge
res = gen_sample(xrange(5000000), 200000, 5000000)
timing: 1.22s
# Small
z = gen_sample(xrange(10000), 1000, 10000)
timing: 0.000441