Как правильно обобщить функции, работающие с векторами, с функциями, работающими с многомерными массивами? - PullRequest
2 голосов
/ 21 февраля 2020

Я часто заканчиваю тем, что применяю подпрограммы, которые работают с одномерными массивами, а затем обобщаю их так, что, если они вызываются в матрицах, они обрабатывают каждую строку независимо. Например, предположим, что нам нужна функция, которая вычитает из нее среднее значение вектора, а затем делает это в каждой строке матрицы, если ее вход имеет более одного измерения. Это может быть реализовано следующим образом:

import numpy as np

def _sub_mean(a):
    """ Subtract the mean from vector elements """

    assert isinstance(a, np.ndarray) and a.ndim == 1

    return a - np.mean(a)

def sub_mean(a):
    """ Subtract the mean

    If `a` is a 1D array it returns a 1D array (obtained by subtracting the
    mean of `a` from each of its elements).  If `a` has more than one dimension
    each row is treated separately

    """

    a = np.asanyarray(a)

    if a.ndim <= 1:
        a = np.atleast_1d(a)

        return np.squeeze(_sub_mean(a))

    retval = np.empty(a.shape)

    for idx in np.ndindex(*a.shape[:-1]):
        retval[idx] = _sub_mean(a[idx])

    return retval

Вот несколько примеров вывода sub_mean:

>>> sub_mean(5)
array(0.)
>>> sub_mean([1, 2])
array([-0.5,  0.5])
>>> sub_mean([[1, 2], [4, 6]])
array([[-0.5,  0.5],
       [-1. ,  1. ]])
>>> 

Обратите внимание, что вычисление «ядра» происходит в приватная функция _sub_mean. Действительно, тот же код в sub_mean можно использовать для обобщения любой функции, работающей с одномерными массивами, до функции, работающей с произвольным числом измерений, заменив _sub_mean. Также можно подумать о дальнейшем обобщении, например, добавив аргумент axis, который определяет ось, на которой работает функция, и / или возможность работы со сглаженным входным массивом.

Мне было интересно, уже NumPy предоставляет декоратор для обобщения функций, которые работают с векторами, с функциями, которые работают с более чем одномерными массивами? Т.е., если я смогу заменить приведенный выше код на:

import numpy as np

@np.some_decorator
def sub_mean(a):        
    """ Subtract the mean

    If `a` is a 1D array it returns a 1D array (obtained by subtracting the
    mean of `a` from each of its elements).  If `a` has more than one dimension
    each row is treated separately  """


    assert isinstance(a, np.ndarray) and a.ndim == 1

    return a - np.mean(a)

(и, очевидно, получу те же выходные данные.)


ОБНОВЛЕНИЕ: я закончил тем, что написал следующий декоратор:

class _Expand:
    def __init__(self, func1d):
        functools.update_wrapper(self, func1d)
        self._func1d = func1d

    def __call__(self, arr, *args, **kwargs):
        arr = np.asanyarray(arr)

        axis = kwargs.pop('axis', -1)

        return np.apply_along_axis(self._func1d, axis, arr, *args, **kwargs)

, что позволило бы мне написать sub_mean (или любую другую сложную функцию, которая работает с массивами 1D) как:

@_Expand
def sub_mean(a):        
    """ Subtract the mean

    If `a` is a 1D array it returns a 1D array (obtained by subtracting the
    mean of `a` from each of its elements).  If `a` has more than one dimension
    each row is treated separately  """


    assert isinstance(a, np.ndarray) and a.ndim == 1

    return a - np.mean(a)

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

Тем не менее, мне все равно было бы интересно узнать, реализован ли такой декоратор в NumPy * * 1030

1 Ответ

1 голос
/ 31 марта 2020

Это довольно распространенный вопрос, но не имеет идеального ответа. Реальный ответ заключается в том, что в конечном итоге вы должны индивидуально настраивать каждую функцию.

Предполагая, что вы хотите придерживаться чистого numpy (и vanilla python), а не использовать numba или cython, вот некоторые соображения, которые следует придерживаться помните:

  • np.apply_along_axis реализовано в основном как python l oop. Это не дает никакого реального преимущества, кроме более компактной записи.
  • Почти все функции numpy принимают аргумент axis. Начиная с numpy v1.15, вы можете указывать несколько одновременных осей для ufuncs , а также для многих функций, встроенных в них.
  • Ufuncs дополнительно имеет методы , позволяющие применять их в определенных местах и ​​комбинациях.
  • Функции сокращения, такие как np.mean, часто имеют аргумент keepdims. Это позволяет комбинировать операции сокращения с исходным массивом. Функции, которые не поддерживают keepdims, могут быть уменьшены с помощью ручной вставки оси, например, np.expand_dims.
  • Существуют функции, которые выполняют операции специально над индексами; большинство их имен начинаются с arg*: np.nonzero, np.argmin, np.argmax, np.argpartition, np.argsort, et c.

Взятые вместе, вы можете векторизовать практически любую функцию, которая может быть написана для одного измерения. Иногда результат требует дополнительного умения, а иногда это просто невозможно, но обычно это довольно просто. Один пример, который я могу подумать, что нетривиален, это что-либо, включающее кодирование по длине прогона. Проблема в том, что на некоторых этапах вы получаете рваный массив, для которого требуются инструменты, превышающие numpy.

. Ваш конкретный пример можно записать следующим образом:

def sub_mean(a, axis):
    a = np.array(a, copy=False, subok=True)
    return a - np.mean(a, axis=axis, keepdims=True)
...