Numpy: ERRO: as funções np.vectorize () operam duas vezes no primeiro elemento (como visto quando a função modifica um objeto mutável)

Criado em 8 mar. 2017  ·  7Comentários  ·  Fonte: numpy/numpy

Eu tenho uma lista de dicionários. Estou tentando usar np.vectorize para aplicar uma função que modifica os elementos do dicionário para cada dicionário da lista. Os resultados parecem mostrar que vetorizar está agindo duas vezes no primeiro elemento. Este é um bug que pode ser corrigido? (Talvez relacionado ao fato de que vetorizar o tipo de verificação no primeiro elemento?) Abaixo estão alguns casos de exemplo e saída:

Um caso de teste simples sem modificações no dicionário:

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

resultado:

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

Agora modifique o dicionário e veja se a função é aplicada duas vezes ao primeiro 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')

resultado:

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

Tente uma modificação diferente para verificar a consistência do bug:

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

resultado:

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

Você pode fazer a mesma coisa sem realmente fornecer um valor de retorno (que é como estou tentando usá-lo no meu 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')

resultado:

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

E, a propósito, não há nada de especial em uma lista de comprimento 3, você pode mudar isso e ver o mesmo comportamento de apenas o primeiro elemento sendo modificado duas vezes.

Confirmei o comportamento usando as versões 1.11.3 e 1.12.0 do Numpy

EDITAR:
Eu encontrei uma solução que também confirma o problema de "testar o tipo no primeiro elemento" Se você especificar o argumento otypes , o primeiro elemento não será atingido duas vezes:

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'

resultado:

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

Comentários muito úteis

"Se otypes não for especificado, uma chamada para a função com o primeiro argumento será usada para determinar o número de saídas. Os resultados desta chamada serão armazenados em cache se o cache for True para evitar a chamada da função duas vezes. No entanto, para implementar o cache, a função original deve ser agrupada, o que tornará as chamadas subsequentes mais lentas, portanto, faça isso apenas se sua função for cara. "
https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html

Portanto, é um comportamento bem documentado, mas parece contra-intuitivo. Então, talvez seja mais uma melhoria do que uma correção de bug.

Todos 7 comentários

Acabei de editar minha postagem com um novo teste que parece confirmar que a verificação do primeiro tipo de item É o que causa o problema

Caso de teste mais simples:

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

Dá:

got 1
got 1
got 2
got 3

"Se otypes não for especificado, uma chamada para a função com o primeiro argumento será usada para determinar o número de saídas. Os resultados desta chamada serão armazenados em cache se o cache for True para evitar a chamada da função duas vezes. No entanto, para implementar o cache, a função original deve ser agrupada, o que tornará as chamadas subsequentes mais lentas, portanto, faça isso apenas se sua função for cara. "
https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html

Portanto, é um comportamento bem documentado, mas parece contra-intuitivo. Então, talvez seja mais uma melhoria do que uma correção de bug.

ah, claramente não li todos os documentos bem o suficiente. Isso corrige o problema das chamadas duplas, mas ao custo de uma execução mais lenta. Então eu acho que não há como evitar essa dupla chamada e ainda manter o desempenho?

por exemplo, em numpy/lib/function_base.py na função vectorize class _get_ufunc_and_otypes() , eu ingenuamente pensaria que você poderia modificar estas linhas:

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

para:

#earlier
import copy

...

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

E então você não teria que usar o cache ou especificar otypes, mas acho que você evita atingir os elementos mutáveis ​​reais duas vezes. Mas não sei quanto de impacto no desempenho isso daria em comparação com o cache.

Estou apenas começando a sentir que o comportamento do cache foi projetado com um caro tempo de execução de função em mente, sem pensar no caso de uma função modificando um objeto mutável. Eu acho que é potencialmente possível acomodar a modificação de objeto mutável sem o impacto no desempenho do armazenamento em cache que tinha um longo tempo de execução de função em mente.

Acho que, para operações vetorizadas em matrizes de objetos muito grandes, pode realmente ser mais intensivo em termos computacionais fazer uma cópia profunda do primeiro elemento do que custaria para aplicar a função. Portanto, pode ser uma boa idéia ter essa funcionalidade se o usuário não especificar otype , mas então dizer na documentação que o desempenho pode ser prejudicado, a menos que otype seja especificado para matrizes de grandes objetos. @ eric-wieser o que você acha?

copy.deepcopy() não é seguro em muitas entradas, então infelizmente essa não é uma opção viável.

A única maneira de corrigir isso seria reescrever o núcleo de vectorize , que atualmente usa numpy.frompyfunc para criar um ufunc realmente numpy. Um loop interno alternativo precisaria ser criado, semelhante ao que usamos para np.apply_along_axis . Infelizmente, para evitar a degradação do desempenho, acho que precisaríamos fazer o loop em C (como frompyfunc faz no ufunc que ele constrói).

Esta página foi útil?
0 / 5 - 0 avaliações