Numpy: ERROR: las funciones np.vectorize () operan dos veces en el primer elemento (como se ve cuando la función modifica un objeto mutable)

Creado en 8 mar. 2017  ·  7Comentarios  ·  Fuente: numpy/numpy

Tengo una lista de diccionarios. Estoy tratando de usar np.vectorize para aplicar una función que modifique los elementos del diccionario para cada diccionario en la lista. Los resultados parecen mostrar que vectorizar actúa dos veces sobre el primer elemento. ¿Es este un error que se puede corregir? (¿Quizás relacionado con el hecho de que vectorizar el tipo de verificación en el primer elemento?) A continuación se muestran algunos casos de ejemplo y resultados:

Un caso de prueba simple sin modificaciones de diccionario:

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

salida:

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

Ahora modifique el diccionario y observe que la función se aplica dos veces al primer elemento:

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')

salida:

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

Pruebe una modificación diferente para comprobar la coherencia del error:

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')

salida:

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

Puede hacer lo mismo sin proporcionar un valor de retorno (que es como intento usarlo en mi caso de uso):

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

salida:

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

Y, por cierto, no hay nada especial en una lista de longitud 3, puede cambiar eso y ver el mismo comportamiento de solo el primer elemento modificado doblemente.

Confirmé el comportamiento usando Numpy versión 1.11.3 y 1.12.0

EDITAR:
Encontré una solución que también confirma que es un problema de "probar el tipo en el primer elemento". Si especifica el argumento otypes , el primer elemento no se golpea dos veces:

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'

salida:

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

Comentario más útil

"Si no se especifica otypes, se usará una llamada a la función con el primer argumento para determinar el número de salidas. Los resultados de esta llamada se almacenarán en caché si la caché es True para evitar llamar a la función dos veces. Sin embargo, implementar la caché, la función original debe estar envuelta, lo que ralentizará las llamadas posteriores, así que solo haz esto si tu función es cara ".
https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html

Así que es un comportamiento bien documentado, pero parece contradictorio. Entonces, tal vez sea más una mejora que una corrección de errores.

Todos 7 comentarios

Acabo de editar mi publicación con una nueva prueba que parece confirmar que la verificación del primer tipo de elemento ES lo que causa el problema

Caso de prueba más simple:

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

Da:

got 1
got 1
got 2
got 3

"Si no se especifica otypes, se usará una llamada a la función con el primer argumento para determinar el número de salidas. Los resultados de esta llamada se almacenarán en caché si la caché es True para evitar llamar a la función dos veces. Sin embargo, implementar la caché, la función original debe estar envuelta, lo que ralentizará las llamadas posteriores, así que solo haz esto si tu función es cara ".
https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html

Así que es un comportamiento bien documentado, pero parece contradictorio. Entonces, tal vez sea más una mejora que una corrección de errores.

ah, claramente no leí todos los documentos lo suficientemente bien. Eso soluciona el problema de las llamadas dobles, pero a costa de una ejecución más lenta. Entonces, ¿supongo que no hay forma de evitar esta doble llamada mientras se mantiene el rendimiento?

por ejemplo, en numpy/lib/function_base.py en la función de clase vectorize _get_ufunc_and_otypes() , ingenuamente pensaría que podrías modificar estas líneas:

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

a:

#earlier
import copy

...

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

Y luego no tendría que usar el almacenamiento en caché o especificar otros tipos, pero creo que evita presionar los elementos mutables reales dos veces. Pero ignoro cuánto impacto de rendimiento daría en comparación con el almacenamiento en caché.

Simplemente tengo la sensación de que el comportamiento de almacenamiento en caché se diseñó teniendo en cuenta el costoso tiempo de ejecución de la función, sin pensar en el caso de una función que modifica un objeto mutable. Creo que es potencialmente posible acomodar la modificación de objetos mutables sin el impacto en el rendimiento del almacenamiento en caché que tenía en mente un largo tiempo de ejecución de la función.

Creo que para operaciones vectorizadas en matrices de objetos muy grandes, en realidad podría ser más intensivo desde el punto de vista computacional hacer una copia profunda del primer elemento de lo que costaría aplicar la función. Por lo tanto, podría ser una buena idea tener esa funcionalidad si el usuario no especifica otype , pero luego dice en la documentación que el rendimiento puede verse afectado a menos que se especifique otype para matrices de grandes objetos. @ eric-wieser ¿qué piensas?

copy.deepcopy() no es seguro en muchas entradas, por lo que, lamentablemente, no es una opción viable.

La única forma de solucionar esto sería reescribir el núcleo de vectorize , que actualmente usa numpy.frompyfunc para crear una ufunc realmente numerosa. Debería crearse un bucle interno alternativo, similar al que usamos para np.apply_along_axis . Desafortunadamente, para evitar la degradación del rendimiento, creo que necesitaríamos hacer el ciclo en C (como lo hace frompyfunc en la ufunc que construye).

¿Fue útil esta página
0 / 5 - 0 calificaciones