Условная numpy кумулятивная сумма

Я ищу способ вычислить кумулятивную сумму с numpy, но не хочу продвигать значение (или устанавливать его на ноль) в случае, если кумулятивная сумма очень близка к нулю и отрицательна.

Например

a = np.asarray([0, 4999, -5000, 1000])
np.cumsum(a)

возвращает [0, 4999, -1, 999]

но я бы хотел установить [2]-value (-1) равным нулю во время расчета. Проблема в том, что это решение может быть принято только во время расчета, так как промежуточный результат априори неизвестен.

Ожидаемый массив: [0, 4999, 0, 1000]

Причина этого в том, что я получаю очень маленькие значения (с плавающей запятой, а не целые числа, как в примере), которые связаны с вычислениями с плавающей запятой, которые на самом деле должны быть нулевыми. При вычислении совокупной суммы эти значения складываются, что приводит к ошибкам.


person orange    schedule 10.02.2016    source источник
comment
Насколько велики значения в нужной части массива? Если отрицательные значения в любом случае близки к нулю, сколько ущерба нанесут вашим расчетам, если просто позволить им накапливаться?   -  person mtrw    schedule 10.02.2016
comment
Действительно, см. Мой комментарий stackoverflow.com/questions / 35309237 /. Я тоже пришел к такому выводу.   -  person orange    schedule 10.02.2016


Ответы (3)


Эту проблему может решить алгоритм суммирования Кахана. К сожалению, это не реализовано в numpy. Это означает, что требуется индивидуальная реализация:

def kahan_cumsum(x):
    x = np.asarray(x)
    cumulator = np.zeros_like(x)
    compensation = 0.0

    cumulator[0] = x[0]    
    for i in range(1, len(x)):
        y = x[i] - compensation
        t = cumulator[i - 1] + y
        compensation = (t - cumulator[i - 1]) - y
        cumulator[i] = t
    return cumulator

Я должен признать, что это не совсем то, о чем просили в вопросе. (В примере верно значение -1 на третьем выходе cumsum). Однако я надеюсь, что это решит реальную проблему, стоящую за вопросом, которая связана с точностью с плавающей запятой.

person kazemakase    schedule 10.02.2016
comment
Спасибо. Хорошо знать. Но числовая ошибка не является результатом агрегирования, а уже произошла в предыдущем (более сложном) вычислении. - person orange; 10.02.2016
comment
Понятно. Вы уверены, что это действительно проблема? Сумма обычно очень устойчива к небольшим отклонениям. Если значения достаточно близки к нулю (с точностью до числовой точности), возможно, полученные суммы достаточно близки к желаемым результатам. Просто мысль :) - person kazemakase; 10.02.2016
comment
Вы, наверное, правы. Суммарная ошибка может быть не такой уж большой проблемой, поскольку она в любом случае близка к нулю и не проходит через множество дальнейших итераций в цикле cumsum. Возможно, я слишком усложнил это в надежде на более чистое решение ... - person orange; 10.02.2016

Интересно, будет ли округление делать то, о чем вы просите:

np.cumsum(np.around(a,-1))
# the -1 means it rounds to the nearest 10

дает

array([   0, 5000,    0, 1000])

Это не совсем то, что вы положили в ожидаемый массив из своего ответа, но с использованием _ 3_, возможно, с параметром decimals, установленным на 0, может сработать, если вы примените его к проблеме с числами с плавающей запятой.

person atomh33ls    schedule 10.02.2016
comment
Я не думаю, что это сработает. Промежуточное значение для решения, округлять или нет, создается во время вычисления общей суммы. Его первое появление может быть округлено post cumsum, но оно уже было бы добавлено к следующим элементам в массиве, которых я стараюсь избегать. - person orange; 10.02.2016
comment
@orange coule вы добавляете пример с поплавками к своему вопросу? - person atomh33ls; 10.02.2016

Вероятно, лучший способ - написать этот бит в Cython (назовите файл cumsum_eps.pyx):

cimport numpy as cnp
import numpy as np

cdef inline _cumsum_eps_f4(float *A, int ndim, int dims[], float *out, float eps):
    cdef float sum
    cdef size_t ofs

    N = 1
    for i in xrange(0, ndim - 1):
        N *= dims[i]
    ofs = 0
    for i in xrange(0, N):
        sum = 0
        for k in xrange(0, dims[ndim-1]):
            sum += A[ofs]
            if abs(sum) < eps:
                sum = 0
            out[ofs] = sum
            ofs += 1

def cumsum_eps_f4(cnp.ndarray[cnp.float32_t, mode='c'] A, shape, float eps):
    cdef cnp.ndarray[cnp.float32_t] _out
    cdef cnp.ndarray[cnp.int_t] _shape
    N = np.prod(shape)
    out = np.zeros(N, dtype=np.float32)
    _out = <cnp.ndarray[cnp.float32_t]> out
    _shape = <cnp.ndarray[cnp.int_t]> np.array(shape, dtype=np.int)
    _cumsum_eps_f4(&A[0], len(shape), <int*> &_shape[0], &_out[0], eps)
    return out.reshape(shape)


def cumsum_eps(A, axis=None, eps=np.finfo('float').eps):
    A = np.array(A)
    if axis is None:
        A = np.ravel(A)
    else:
        axes = list(xrange(len(A.shape)))
        axes[axis], axes[-1] = axes[-1], axes[axis]
        A = np.transpose(A, axes)
    if A.dtype == np.float32:
        out = cumsum_eps_f4(np.ravel(np.ascontiguousarray(A)), A.shape, eps)
    else:
        raise ValueError('Unsupported dtype')
    if axis is not None: out = np.transpose(out, axes)
    return out

то вы можете скомпилировать его так (Windows, Visual C ++ 2008 Command Line):

\Python27\Scripts\cython.exe cumsum_eps.pyx
cl /c cumsum_eps.c /IC:\Python27\include /IC:\Python27\Lib\site-packages\numpy\core\include
F:\Users\sadaszew\Downloads>link /dll cumsum_eps.obj C:\Python27\libs\python27.lib /OUT:cumsum_eps.pyd

или так (Linux использует расширение .so / Cygwin использует расширение .dll, gcc):

cython cumsum_eps.pyx
gcc -c cumsum_eps.c -o cumsum_eps.o -I/usr/include/python2.7 -I/usr/lib/python2.7/site-packages/numpy/core/include
gcc -shared cumsum_eps.o -o cumsum_eps.so -lpython2.7

и используйте вот так:

from cumsum_eps import *
import numpy as np
x = np.array([[1,2,3,4], [5,6,7,8]], dtype=np.float32)

>>> print cumsum_eps(x)
[  1.   3.   6.  10.  15.  21.  28.  36.]
>>> print cumsum_eps(x, axis=0)
[[  1.   2.   3.   4.]
 [  6.   8.  10.  12.]]
>>> print cumsum_eps(x, axis=1)
[[  1.   3.   6.  10.]
 [  5.  11.  18.  26.]]
>>> print cumsum_eps(x, axis=0, eps=1)
[[  1.   2.   3.   4.]
 [  6.   8.  10.  12.]]
>>> print cumsum_eps(x, axis=0, eps=2)
[[  0.   2.   3.   4.]
 [  5.   8.  10.  12.]]
>>> print cumsum_eps(x, axis=0, eps=3)
[[  0.   0.   3.   4.]
 [  5.   6.  10.  12.]]
>>> print cumsum_eps(x, axis=0, eps=4)
[[  0.   0.   0.   4.]
 [  5.   6.   7.  12.]]
>>> print cumsum_eps(x, axis=0, eps=8)
[[ 0.  0.  0.  0.]
 [ 0.  0.  0.  8.]]
>>> print cumsum_eps(x, axis=1, eps=3)
[[  0.   0.   3.   7.]
 [  5.  11.  18.  26.]]

и так далее, конечно, обычно eps будет небольшим значением, здесь целые числа используются только для демонстрации / простоты набора.

Если вам это нужно и для double, варианты _f8 легко написать, а другой случай должен быть обработан в cumsum_eps ().

Когда вы довольны реализацией, сделайте ее надлежащей частью вашего setup.py - Cython setup.py

Обновление №1: если у вас есть хорошая поддержка компилятора в среде выполнения, вы можете попробовать [Theano] [3] для реализации либо алгоритма компенсации, либо вашей исходной идеи:

import numpy as np
import theano
import theano.tensor as T
from theano.ifelse import ifelse

A=T.vector('A')

sum=T.as_tensor_variable(np.asarray(0, dtype=np.float64))

res, upd=theano.scan(fn=lambda cur_sum, val: ifelse(T.lt(cur_sum+val, 1.0), np.asarray(0, dtype=np.float64), cur_sum+val), outputs_info=sum, sequences=A)

f=theano.function(inputs=[A], outputs=res)

f([0.9, 2, 3, 4])

даст [0 2 3 4] вывод. В Cython или в этом вы получаете как минимум +/- производительность нативного кода.

person ALGOholic    schedule 10.02.2016
comment
Да, я мог бы использовать свой собственный в cython, так как не похоже, что есть прямое решение для numpy. - person orange; 10.02.2016
comment
Кстати, вам не нужно предварительно компилировать ваш файл pyx. Вы можете просто импортировать его, используя pyximport.install, и он компилируется во время первого импорта. - person orange; 10.02.2016
comment
Я предполагаю, что среда выполнения не является средой сборки (т.е. компилятора C нет). - person ALGOholic; 10.02.2016
comment
Понятно. В моем случае среда выполнения может компилироваться. - person orange; 10.02.2016
comment
Вы также можете попробовать Theano, если у вас есть хорошая поддержка компилятора. import theano import theano.tensor as T from theano.ifelse import ifelse A=T.vector('A') sum=T.as_tensor_variable(np.asarray(0, dtype=np.float64)) res, upd=theano.scan(fn=lambda cur_sum, val: ifelse(T.lt(cur_sum+val, 1.0), np.asarray(0, dtype=np.float64), cur_sum+val), outputs_info=sum, sequence=A) f=theano.function(inputs=[A], outputs=res) f([0.9, 2, 3, 4]) даст [0 2 3 4] вывод - person ALGOholic; 10.02.2016
comment
Я получаю [0. 2. 5. 9.] на моем компьютере с вашим theano кодом вместо [0 2 3 4] из вашего ответа - единственное изменение - это print() вызов: print(f[0.9, 2, 3, 4]), чтобы увидеть результаты. Предполагая, что код находится в theano_algoholic.py файле, я запускаю его, используя: docker run --rm -it -v "$PWD:/prj" kaixhin/theano python /prj/theano_algoholic.py - person jfs; 28.10.2016