Numpy: BUG: les fonctions np.vectorize () opèrent deux fois sur le premier élément (comme on le voit quand la fonction modifie un objet mutable)

Créé le 8 mars 2017  ·  7Commentaires  ·  Source: numpy/numpy

J'ai une liste de dictionnaires. J'essaie d'utiliser np.vectorize pour appliquer une fonction qui modifie les éléments du dictionnaire pour chaque dictionnaire de la liste. Les résultats semblent montrer que vectoriser agit deux fois sur le premier élément. Est-ce un bug qui peut être corrigé? (Peut-être lié au fait que vectoriser les vérifications de type sur le premier élément?) Voici quelques exemples de cas et de sortie:

Un cas de test simple sans modifications du dictionnaire:

def fcn1(x):
    return x['b']
a = [{'b': 1} for _ in range(3) ]
print(a)
print(np.vectorize(fcn1)(a))
print(a, '\n\n')

production:

[{'b': 1}, {'b': 1}, {'b': 1}]
[1 1 1]
[{'b': 1}, {'b': 1}, {'b': 1}]

Modifiez maintenant le dictionnaire et voyez que la fonction est appliquée deux fois au premier élément:

def fcn2(x):
    x['b'] += 1
    return x['b']
a = [{'b': 1} for _ in range(3) ]
print(a)
print(np.vectorize(fcn2)(a))
print(a, '\n\n')

production:

[{'b': 1}, {'b': 1}, {'b': 1}]
[3 2 2]
[{'b': 3}, {'b': 2}, {'b': 2}]  

Essayez une modification différente pour vérifier la cohérence du bogue:

def fcn3(x):
    x['b'] *= 2
    return x['b']
a = [{'b': 1} for _ in range(3) ]
print(a)
print(np.vectorize(fcn3)(a))
print(a, '\n\n')

production:

[{'b': 1}, {'b': 1}, {'b': 1}]
[4 2 2]
[{'b': 4}, {'b': 2}, {'b': 2}]    

Vous pouvez faire la même chose sans fournir réellement de valeur de retour (c'est ainsi que j'essaye de l'utiliser dans mon cas d'utilisation):

def fcn4(x):
    x['b'] += 1
a = [{'b': 1} for _ in range(3) ]
print(a)
np.vectorize(fcn4)(a)
print(a, '\n\n')

production:

[{'b': 1}, {'b': 1}, {'b': 1}]
[{'b': 3}, {'b': 2}, {'b': 2}]

Et au fait, il n'y a rien de spécial à propos d'une liste de longueur 3, vous pouvez changer cela et voir le même comportement du seul premier élément étant modifié deux fois.

J'ai confirmé le comportement en utilisant les versions 1.11.3 et 1.12.0 de Numpy

ÉDITER:
J'ai trouvé une solution de contournement qui confirme également que c'est un problème de "test du type sur le premier élément". Si vous spécifiez l'argument otypes , le premier élément n'est pas touché deux fois:

def fcn(x):
    x['b'] += 1
    return x['b']
a = [{'b': 1} for _ in range(3)]
print a
print np.vectorize(fcn, otypes=[dict])(a)
print a, '\n\n'

production:

[{'b': 1}, {'b': 1}, {'b': 1}]
[2 2 2]
[{'b': 2}, {'b': 2}, {'b': 2}]
00 - Bug

Commentaire le plus utile

"Si otypes n'est pas spécifié, un appel à la fonction avec le premier argument sera utilisé pour déterminer le nombre de sorties. Les résultats de cet appel seront mis en cache si cache a la valeur True pour éviter d'appeler la fonction deux fois. Cependant, pour implémenter le cache, la fonction d'origine doit être encapsulée, ce qui ralentira les appels ultérieurs, alors ne le faites que si votre fonction est coûteuse. "
https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html

C'est donc un comportement bien documenté, mais qui semble contre-intuitif. Alors peut-être que c'est plus une amélioration qu'une correction de bogue.

Tous les 7 commentaires

Je viens de modifier mon article avec un nouveau test qui semble confirmer la vérification du premier type d'élément est la cause du problème

Cas de test plus simple:

a = np.array([1, 2, 3])
def f(x):
    print('got', x)
    return x
fv = np.vectorize(f)
y = fv(a)

Donne:

got 1
got 1
got 2
got 3

"Si otypes n'est pas spécifié, un appel à la fonction avec le premier argument sera utilisé pour déterminer le nombre de sorties. Les résultats de cet appel seront mis en cache si cache a la valeur True pour éviter d'appeler la fonction deux fois. Cependant, pour implémenter le cache, la fonction d'origine doit être encapsulée, ce qui ralentira les appels ultérieurs, alors ne le faites que si votre fonction est coûteuse. "
https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html

C'est donc un comportement bien documenté, mais qui semble contre-intuitif. Alors peut-être que c'est plus une amélioration qu'une correction de bogue.

ah, il est clair que je n'ai pas assez bien lu tous les documents. Cela résout le problème des doubles appels, mais au prix d'une exécution plus lente. Donc je suppose qu'il n'y a aucun moyen d'empêcher ce double appel tout en maintenant les performances?

par exemple, dans numpy/lib/function_base.py dans la fonction de classe vectorize _get_ufunc_and_otypes() , je penserais naïvement que vous pourriez modifier ces lignes:

inputs = [arg.flat[0] for arg in args]
outputs = func(*inputs)

à:

#earlier
import copy

...

inputs = copy.deepcopy([arg.flat[0] for arg in args])
outputs = func(*inputs)

Et puis vous n'auriez pas à utiliser la mise en cache ou à spécifier des otypes, mais je pense que vous évitez de frapper deux fois les éléments mutables réels. Mais je ne sais pas combien de performances cela donnerait par rapport au cache.

J'ai juste l'impression que le comportement de mise en cache a été conçu avec un temps d'exécution de fonction coûteux à l'esprit, sans penser au cas d'une fonction modifiant un objet mutable. Je pense qu'il est potentiellement possible de s'adapter à la modification d'objet mutable sans le coup de performance de la mise en cache qui avait un long temps d'exécution de fonction à l'esprit.

Je pense que pour les opérations vectorisées sur des tableaux d'objets très volumineux, il pourrait en fait être plus gourmand en calcul de faire une copie complète du premier élément qu'il n'en coûterait pour appliquer la fonction. Donc, cela peut être une bonne idée d'avoir cette fonctionnalité si l'utilisateur ne spécifie pas otype , mais dites ensuite dans la documentation que les performances peuvent prendre un coup à moins que otype soit spécifié pour des tableaux de grande taille objets. @ eric-wieser qu'en pensez-vous?

copy.deepcopy() n'est pas sûr sur de nombreuses entrées, donc ce n'est malheureusement pas une option viable.

La seule façon de résoudre ce problème serait de réécrire le noyau de vectorize , qui utilise actuellement numpy.frompyfunc pour créer un ufunc réellement numpy. Une autre boucle interne devrait être créée, similaire à celle que nous utilisons pour np.apply_along_axis . Malheureusement, pour éviter une dégradation des performances, je pense que nous aurions besoin de faire la boucle en C (comme frompyfunc fait dans ufunc qu'il construit).

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

qualiaa picture qualiaa  ·  3Commentaires

astrofrog picture astrofrog  ·  4Commentaires

marcocaccin picture marcocaccin  ·  4Commentaires

thouis picture thouis  ·  4Commentaires

inducer picture inducer  ·  3Commentaires