Comment créer un tableau numpy à partir d'un générateur?

166

Comment puis-je créer un tableau numpy à partir d'un objet générateur?

Laissez-moi illustrer le problème:

>>> import numpy
>>> def gimme():
...   for x in xrange(10):
...     yield x
...
>>> gimme()
<generator object at 0x28a1758>
>>> list(gimme())
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> numpy.array(xrange(10))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> numpy.array(gimme())
array(<generator object at 0x28a1758>, dtype=object)
>>> numpy.array(list(gimme()))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Dans cet exemple, gimme()est le générateur dont je voudrais transformer la sortie en un tableau. Cependant, le constructeur de tableau n'itère pas sur le générateur, il stocke simplement le générateur lui-même. Le comportement que je désire est celui de numpy.array(list(gimme())), mais je ne veux pas payer la surcharge de mémoire d'avoir la liste intermédiaire et le tableau final en mémoire en même temps. Existe-t-il un moyen plus efficace d'espace?

saffsd
la source
6
C'est une question intéressante. Je suis tombé sur cela par from numpy import *; print any(False for i in range(1))- qui ombres le intégré any()et produit le résultat opposé (comme je le sais maintenant).
moooeeeep
4
@moooeeeep c'est terrible. s'il numpyne peut pas (ou ne veut pas) traiter les générateurs comme le fait Python, au moins il devrait lever une exception lorsqu'il reçoit un générateur comme argument.
max
1
@max J'ai marché exactement sur le mien. Apparemment, cela a été soulevé sur la liste NumPy (et plus tôt ), concluant que cela ne sera pas modifié pour lever une exception et qu'il faut toujours utiliser des espaces de noms.
alexei

Réponses:

128

Les tableaux Numpy nécessitent que leur longueur soit définie explicitement au moment de la création, contrairement aux listes python. Ceci est nécessaire pour que l'espace pour chaque élément puisse être alloué consécutivement en mémoire. L'allocation consécutive est la caractéristique clé des tableaux numpy: cela, combiné à l'implémentation de code natif, permet aux opérations sur eux de s'exécuter beaucoup plus rapidement que les listes régulières.

En gardant cela à l'esprit, il est techniquement impossible de prendre un objet générateur et de le transformer en un tableau sauf si vous:

  1. peut prédire le nombre d'éléments qu'il produira lors de l'exécution:

    my_array = numpy.empty(predict_length())
    for i, el in enumerate(gimme()): my_array[i] = el
  2. sont disposés à stocker ses éléments dans une liste intermédiaire:

    my_array = numpy.array(list(gimme()))
  3. peut créer deux générateurs identiques, parcourir le premier pour trouver la longueur totale, initialiser le tableau, puis parcourir à nouveau le générateur pour trouver chaque élément:

    length = sum(1 for el in gimme())
    my_array = numpy.empty(length)
    for i, el in enumerate(gimme()): my_array[i] = el

1 est probablement ce que vous recherchez. 2 est inefficace dans l'espace et 3 est inefficace dans le temps (vous devez passer deux fois par le générateur).

shsmurfy
la source
11
L'intégrée array.arrayest une liste contiguë non liée, et vous pouvez simplement array.array('f', generator). Dire que c'est impossible est trompeur. C'est juste une allocation dynamique.
Cuadue
1
Pourquoi numpy.array ne fait pas l'allocation de mémoire de la même manière que le array.array intégré, comme le dit Cuadue. Quel est le métier? Je demande parce qu'il y a de la mémoire allouée contiguë dans les deux exemples. Ou pas?
jgomo3
3
numpy suppose que la taille de ses tableaux ne change pas. Il repose fortement sur différentes vues du même bloc de mémoire, donc permettre aux tableaux d'être étendus et réalloués nécessiterait une couche supplémentaire d'indirection pour activer les vues, par exemple.
joeln
2
Utiliser vide est un peu plus rapide. Puisque vous allez initialiser les valeurs de toute façon, inutile de le faire deux fois.
Kaushik Ghose
Voir aussi la réponse de @ dhill ci-dessous, qui est plus rapide que 1.
Projet de loi
206

Un google derrière ce résultat de stackoverflow, j'ai trouvé qu'il y avait un fichier numpy.fromiter(data, dtype, count). La valeur par défaut count=-1prend tous les éléments de l'itérable. Il faut que a dtypesoit défini explicitement. Dans mon cas, cela a fonctionné:

numpy.fromiter(something.generate(from_this_input), float)

dhill
la source
comment appliquez-vous cela à la question? numpy.fromiter(gimme(), float, count=-1)ne marche pas. Que signifie something?
Matthias 009
1
@ Matthias009 numpy.fromiter(gimme(), float, count=-1)fonctionne pour moi.
moooeeeep
14
Un fil expliquant pourquoi fromiterne fonctionne que sur les tableaux 1D: mail.scipy.org/pipermail/numpy-discussion/2007-August/… .
max
2
fwiw, count=-1n'a pas besoin d'être spécifié, car c'est la valeur par défaut.
askewchan
5
Si vous connaissez au préalable la longueur de l'itérable, spécifiez le countpour améliorer les performances. De cette façon, il alloue la mémoire avant de la remplir avec des valeurs plutôt que de le redimensionner à la demande (voir la documentation de numpy.fromiter)
Eddy
15

Bien que vous puissiez créer un tableau 1D à partir d'un générateur avec numpy.fromiter(), vous pouvez créer un tableau ND à partir d'un générateur avec numpy.stack:

>>> mygen = (np.ones((5, 3)) for _ in range(10))
>>> x = numpy.stack(mygen)
>>> x.shape
(10, 5, 3)

Cela fonctionne également pour les baies 1D:

>>> numpy.stack(2*i for i in range(10))
array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])

Notez que cela numpy.stackconsomme en interne le générateur et crée une liste intermédiaire avec arrays = [asanyarray(arr) for arr in arrays]. La mise en œuvre peut être trouvée ici .

mdeff
la source
1
C'est une solution soignée, merci de l'avoir signalé. Mais cela semble être un peu plus lent (dans mon application) que d'utiliser np.array(tuple(mygen)). Voici les résultats des tests: %timeit np.stack(permutations(range(10), 7)) 1 loop, best of 3: 1.9 s per loopcomparés à%timeit np.array(tuple(permutations(range(10), 7))) 1 loop, best of 3: 427 ms per loop
Projet de loi
13
Cela semble génial et fonctionne pour moi. Mais avec Numpy 1.16.1 je reçois cet avertissement:FutureWarning: arrays to stack must be passed as a "sequence" type such as list or tuple. Support for non-sequence iterables such as generators is deprecated as of NumPy 1.16 and will raise an error in the future.
Joseph Sheedy
6

Un peu tangentiel, mais si votre générateur est une compréhension de liste, vous pouvez l'utiliser numpy.wherepour obtenir plus efficacement votre résultat (j'ai découvert cela dans mon propre code après avoir vu cet article)

Benjamin Horstman
la source
0

Les fonctions vstack , hstack et dstack peuvent prendre comme générateurs d'entrée qui produisent des tableaux multidimensionnels.

Mike R
la source
3
Pouvez-vous donner un exemple au cas où les liens changeraient ou quelque chose comme ça? :)
Ari Cooper-Davis
Ces fonctions peuvent prendre un générateur de tableaux, pas un générateur de valeurs
retnikt