Numpy векторизация для лямбды с несколькими аргументами - PullRequest
2 голосов
/ 21 апреля 2020

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

Допустим, У меня есть вектор с плавающей точкой (назовем их «точками»),

v = np.array([9. , 1. , 4.2, 5.6, 3. , 4.6])

Я хочу интерполировать между соседними точками, поэтому мне нужно взять эти пары:

def adjacent_pairs(v):
    """
    Given a 1D numpy array `v = np.array([1, ..., n])`, return a 2D numpy array of
    adjacent pairs, `np.array([(1,2), ..., (n-1,n)])`.
    """
    s = v.shape
    d = len(s)
    assert d == 1, ValueError(f"Vector must be 1D - got a {d}D vector: shape = {s})")
    return np.vstack([v[:-1],v[1:]]).T

adjacent_pairs(v) дает:

array([[9. , 1. ],
       [1. , 4.2],
       [4.2, 5.6],
       [5.6, 3. ],
       [3. , 4.6]])

Я хочу интерполировать эти пары (строки матрицы, например, [9., 1.]) с интервалами размера 0,2, но интерполяции могут быть восходящими или нисходящими, поэтому я нормализую разностный вектор, чтобы найти «направление» или знак (+1 при возрастании, -1 при спуске) и умножить его на размер шага, чтобы передать arange в качестве аргумента step.

Это работает:

def interpolate_1d(v, step=0.2):
    v_adj = adjacent_pairs(v)
    d = np.diff(v_adj) / np.abs(np.diff(v_adj))
    interpolated = [np.arange(*r, diff * step) for r, diff in zip(v_adj, d)]
    return interpolated 

Однако я осознаю, что часть zip() не находится "в" numpy, и, возможно, я должен был бы сделать это таким образом.

I начал изучать различные «векторизованные» функции в * 107 7 * (что, как я понимаю, иногда может ускорить ваш код), но у меня возникают проблемы с переформатированием этого кода в абстракции np.fromiter, np.vectorize или np.frompyfunc, и через несколько часов вчера вечером я ' Я надеюсь, что кто-то, более знакомый с ними, может объяснить мне, как я могу использовать один или несколько из них с моим кодом.

Я бы предпочел передать строку и знак разницы отдельно (как lambda row, diff: ... ), но мне не удалось заставить их работать, поэтому я hstack отредактировал массивы v_adj и d, чтобы каждая строка содержала их обоих (и мне понадобился бы только один аргумент для лямбды) .

Вот две версии функции:

def interpolate_1d_vectorised(v, step=0.2):
    """
    Couldn't get this to work: how to expand out the two parts at a time to pass to
    the lambda function?
    """
    v_adj = adjacent_pairs(v)
    d = np.diff(v_adj) / np.abs(np.diff(v_adj))
    # lambda_func = lambda row, diff: np.arange(*row, diff * step)
    lambda_func = lambda row, diff: np.arange(row[0], row[1], diff * step)
    row_arange = np.vectorize(lambda_func, signature="(),()->()")
    interpolated = row_arange(v_adj, d)
    return interpolated


def interpolate_1d_vectorised_triples(v, step=0.2):
    v_adj = adjacent_pairs(v)
    d = np.diff(v_adj) / np.abs(np.diff(v_adj))
    triples = np.hstack([v_adj, d])
    triple_lambda = lambda t: np.arange(t[0], t[1], t[2] * step)
    row_arange_t = np.vectorize(triple_lambda, signature="()->()")
    interpolated = row_arange_t(triples)
    return interpolated

Некоторые примеры ошибок, которые я получил:

  • ValueError: setting an array element with a sequence.
    • из row_arange(v_adj, d) где row_arange = np.vectorize(lambda_func, signature="(),()->()") (как в interpolate_1d_vectorised)
    • также из np.fromiter([np.arange(a,b,c * step) for (a,b,c) in triples])

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

Я использовал np.apply_along_axis и np.apply_over_axes ранее, но я тоже получал различные ошибки, используя их.

Я ожидал, что это сработает:

triple_lambda = lambda t: np.arange(t[0], t[1], t[2] * 0.2)
np.apply_along_axis(triple_lambda, 1, triples)

, но это дало: ValueError: could not broadcast input array from shape (16) into shape (40), что, как я предполагаю, означает, что интерполированные значения делают вектор больше.

np.apply_over_axes(triple_lambda, triples, axes=[0,2]) дал TypeError: <lambda>() takes 1 positional argument but 2 were given (то же самое, когда axes=[0,1]).

(Это было о точке, которую я дал up)

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

1 Ответ

1 голос
/ 21 апреля 2020

Итак, для начала, lambda эквивалентно def, но более ограниченно. Вам действительно не нужно использовать lambda, так как вы можете передать любую функцию по имени, как и любой другой объект.

Во-вторых, np.vectorize в основном прославленный for l oop. Обрабатывает один элемент за раз. У вас нет возможности вернуть значения разных размеров, которые вам нужны здесь. Это объясняет ваши текущие ошибки. Даже без ошибок он действительно не будет намного лучше, чем ваш начальный zip. Из документов:

Функция vectorize предназначена в первую очередь для удобства, а не для повышения производительности. Реализация по существу для l oop.

Давайте начнем с вычисления количества элементов в каждом диапазоне:

ranges = np.diff(v)
sign = np.sign(ranges)
steps = np.ceil(np.abs(ranges) / step).astype(int)
steps[-1] += 1

Теперь вы можете сделать вектор приращений, который имеют тот же размер, что и вывод:

increments = np.repeat(step * sign, steps)

Вы можете запустить cumsum с шагом, если вы установите начальные значения для каждого сегмента. Началом каждого сегмента является соответствующее значение v, за вычетом предыдущего остатка.

range_start = np.cumsum(steps[:-1])
increments[0] = v[0]
increments[range_start] = v[1:-1] - (v[0:-2] + sign[:-1] * (steps[:-1] - 1) * step)

Теперь вы можете просто взять накопленную сумму (и установить последний элемент):

result = np.cumsum(increments)
result[-1] = v[-1]

Вероятно, вы время от времени столкнетесь с некоторыми проблемами с ошибкой округления, поэтому универсальное решение удаления произвольных невязок является хорошей идеей. Он также правильно обрабатывает нецелые кратные шага:

>>> interpolate_1d(v)
array([9. , 8.8, 8.6, 8.4, 8.2, 8. , 7.8, 7.6, 7.4, 7.2, 7. , 6.8, 6.6,
       6.4, 6.2, 6. , 5.8, 5.6, 5.4, 5.2, 5. , 4.8, 4.6, 4.4, 4.2, 4. ,
       3.8, 3.6, 3.4, 3.2, 3. , 2.8, 2.6, 2.4, 2.2, 2. , 1.8, 1.6, 1.4,
       1.2, 1. , 1.2, 1.4, 1.6, 1.8, 2. , 2.2, 2.4, 2.6, 2.8, 3. , 3.2,
       3.4, 3.6, 3.8, 4. , 4.2, 4.4, 4.6, 4.8, 5. , 5.2, 5.4, 5.6, 5.4,
       5.2, 5. , 4.8, 4.6, 4.4, 4.2, 4. , 3.8, 3.6, 3.4, 3.2, 3. , 3.2,
       3.4, 3.6, 3.8, 4. , 4.2, 4.4, 4.6])
>>> interpolate_1d([1., 2.5, 1.])
array([1. , 1.2, 1.4, 1.6, 1.8, 2. , 2.2, 2.4, 2.5, 2.3, 2.1, 1.9, 1.7,
       1.5, 1.3, 1.1, 1. ])

На этой ноте, если вы на 100% уверены, что все ваши диапазоны кратны размеру шага, и вас не волнует небольшая ошибка округления, вы можете просто сложить исходное определение increments без каких-либо дальнейших изменений:

increments = np.repeat(step * sign, steps)
increments[0] = v[0]
result = np.cumsum(increments)

TL; DR

def interpolate_1d(v, step=0.2):
    ranges = np.diff(v)
    sign = np.sign(ranges)
    steps = np.ceil(np.abs(ranges) / step).astype(int)
    steps[-1] += 1
    range_start = np.cumsum(steps[:-1])
    increments = np.repeat(step * sign, steps)
    increments[0] = v[0]
    increments[range_start] = v[1:-1] - (v[0:-2] + sign[:-1] * (steps[:-1] - 1) * step)
    result = np.cumsum(increments)
    result[-1] = v[-1]
    return result
...