Numpy и код генератора имеет более низкую производительность, чем простой метод добавления. Как улучшить? - PullRequest
0 голосов
/ 02 мая 2020

Я написал random_walk симуляцию, используя numpy для выделения данных и генераторы для выполнения шагов симуляции. Это random_walk - это просто MWE из исходного кода (которое вообще не связано со случайными блужданиями, а математическая модель стохасти c, слишком большая и сложная, чтобы использовать ее в качестве примера. Тем не менее random_walk MWE моделирует основные компоненты .

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

Для сравнения я также написал аналогичный наивный подход, использующий обычный append для списков для хранения данных симуляции, куда я добавляю только когда происходят угловые случаи.

Важно знать, что угловые случаи будут происходить миллиард раз (займет огромную часть ry), но финальная симуляция будет выполняться в течение «бесконечного времени», то есть ОЧЕНЬ большого количества шагов. Угловые случаи похожи на случай 1e-10.

И в конечном коде есть условие остановки, которое я эмулировал здесь, используя distance и классический simulation time.

К моему удивлению Я заметил, что append подход имеет лучшую производительность, чем numpy+generators. Как мы можем видеть в выходных данных ниже

Для небольших наборов данных:

%timeit random_walk_naive(max_distance=1e5, simul_time=1e4)
5.35 ms ± 190 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit random_walk_simul(max_distance=1e5, simul_time=1e4)
16.3 ms ± 567 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Для больших наборов данных

%timeit random_walk_naive(max_distance=1e12, simul_time=1e7)
12.2 s ± 760 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit random_walk_simul(max_distance=1e12, simul_time=1e7)
36 s ± 102 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Выполнение cProfile по вызовам, я заметил генератор Время выполнения вызовов аналогично наивному подходу, и все дополнительное время было потрачено на random_walk_simul самостоятельно. Оценивая np.empty и операцию нарезки, я заметил, что время создания пустого набора данных и его нарезки в ответ минимально. Не влияет на время, потраченное на операции. Кроме того, код почти такой же, за исключением того, что в подходе generator я сначала выделяю данные локальным переменным, а затем "flu sh" их в numpy.array, что было показано быстрее, чем flu sh напрямую, так как я будет использовать значения в while l oop для оценки условия остановки.

Мне нужно понять, почему это поведение проявляется и ожидается ли оно; если не как это исправить?

Полный исходный код вставлен ниже

import numpy as np
from random import random

def clip(value, lower, upper):
    return lower if value < lower else upper if value > upper else value


def random_walk(s_0, a_0, pa, pb):
    """Initial position (often 0), acceleration, 0 < pa < pb < 1"""
    # Time, x-position, Velocity, Acceleration
    t, x, v, a = 0, s_0, 0, a_0
    yield (t, x, v, a)

    while True:        
        # Roll the dices
        god_wishes = random()

        if god_wishes <= pa:
                # Increase acceleration
                a += .005
        elif god_wishes <= pb:
                # Reduce acceleration
                a -= .005

        # Lets avoid too much acceleration
        a = clip(a, -.2, .2)

        # How much time has passed, since last update?
        dt = random()
        v += dt*a
        x += dt*v
        t += dt

        yield (t, x, v, a)


def random_walk_simul(initial_position = 0, acceleration = 0, 
                      prob_increase=0.005, prob_decrease=0.005, 
                      max_distance=10000, simul_time=1000):
    """Runs a random walk simulation given parameters

    Particle initial state (initial position and acceleration)
    State change probability (prob_increase, prob_decrease)
    Stop criteria (max_distance, simul_time)

    Returns a random_walk particle data
    """
    assert (0 < prob_increase+prob_decrease < 1), "Total probability should be in range [0, 1]"

    rw = random_walk(initial_position, acceleration, prob_increase, prob_decrease+prob_increase)

    # Over estimated given by law of large numbers expected value of a
    # uniform distribution
    estimated_N = int(simul_time * 2.2)

    data = np.empty((estimated_N, 4))

    # Runs the first iteraction
    n = 0
    (t, x, v, a) = rw.__next__()
    data[n] = (t, x, v, a)

    # While there is simulation time or not too far away
    while (t < simul_time) and (np.abs(x) < max_distance):
        n += 1
        (t, x, v, a) = rw.__next__()
        data[n] = (t, x, v, a)

    return data[:n]


def random_walk_naive(initial_position = 0, acceleration = 0, 
                      prob_increase=0.005, prob_decrease=0.005, 
                      max_distance=10000, simul_time=1000):
    """Emulates same behavior as random_walk_simul, but use append instead numpy and generators"""
    T = []
    X = []
    V = []
    A = []

    T.append(0)
    X.append(initial_position)
    V.append(0)
    A.append(acceleration)

    a = A[-1]
    t = T[-1]
    v = V[-1]
    x = X[-1]

    while (T[-1] < simul_time) and (abs(X[-1]) < max_distance):       
        god_wishes = random()
        if god_wishes <= prob_increase:
            # Increase acceleration
            a += .005
        elif god_wishes <= prob_increase+prob_decrease:
            # Reduce acceleration
            a -= .005

        # Lets avoid too much acceleration
        a = clip(a, -.2, .2)

        dt = random()
        t += dt
        v += dt*a
        x += dt*v

        # Storing next simulation step
        T.append(t)
        X.append(x)
        V.append(v)
        A.append(a)

    return np.array((T, X, V, A)).transpose()


def main():
    random_walk_naive(max_distance=1e9, simul_time=1e6)
    random_walk_simul(max_distance=1e9, simul_time=1e6)


if __name__ == '__main__':
    main()

1 Ответ

1 голос
/ 02 мая 2020

Это может быть хорошая ситуация для использования numba :

import numpy as np
from random import random
from numba import njit

# Baseline
%timeit random_walk_naive(max_distance=1e9, simul_time=1e6)
1.28 s ± 277 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Few adjustments for numba

@njit
def random_walk_numba(initial_position = 0, acceleration = 0, 
                      prob_increase=0.005, prob_decrease=0.005, 
                      max_distance=10000, simul_time=1000):

    T, X, V, A = [0], [initial_position], [0], [acceleration]

    t, x, v, a = T[-1], X[-1], V[-1], A[-1]

    while (T[-1] < simul_time) and (abs(X[-1]) < max_distance):       
        god_wishes = random()
        if god_wishes <= prob_increase:
            # Increase acceleration
            a += .005
        elif god_wishes <= prob_increase+prob_decrease:
            # Reduce acceleration
            a -= .005

        # Lets avoid too much acceleration
        lower, upper = -0.2, 0.2

        a = lower if a < lower else upper if a > upper else a

        dt = random()
        t += dt
        v += dt*a
        x += dt*v

        # Storing next simulation step
        T.append(t)
        X.append(x)
        V.append(v)
        A.append(a)

    return np.array((T, X, V, A)).transpose()

%timeit random_walk_numba(max_distance=1e9, simul_time=1e6)
172 ms ± 32.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Обратите внимание, что вы не можете позвонить clip, но, к счастью, это легко реализовать внутри.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...