Как эффективно проходить функцию через?

Мотивация

Взгляните на следующую картинку.

введите описание изображения здесь

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

Эта общая задача должна быть решена T раза. В этой проблеме есть один последний поворот. Синяя, зеленая и красная кривые следующей итерации строятся через доминирующее решение из предыдущей итерации плюс некоторые различные параметры. В качестве примера на рисунке выше: Решение - черная функция. Эта функция используется для создания новой синей, зеленой и красной кривой. Затем проблема снова начнется, чтобы найти доминирующую для этих новых кривых и т.д.

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

В течение 5 итераций возрастает только оценка функций в одной точке с правой стороны:

Результаты были получены через

test = A(5, 120000, 100000) 

И затем запустите

test.find_all_intersections()

>>> test.find_all_intersections()
iteration 4
to compute function values it took
0.0102479457855
iteration 3
to compute function values it took
0.0134601593018
iteration 2
to compute function values it took
0.0294270515442
iteration 1
to compute function values it took
0.109843969345
iteration 0
to compute function values it took
0.823768854141

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

Подробное описание кода

Я быстро обобщаю наиболее важные функции. Полный код можно найти ниже. Если есть какие-либо другие вопросы относительно кода, я более чем счастлив разработать/уточнить.

  • Метод u: для повторяющейся задачи создания новой партии зеленой, красной и синей кривой выше, нам нужен старый доминирующий. u - это инициализация, которая будет использоваться в самой первой итерации.

  • Метод _function_template: функция генерирует версии зеленой, синей и красной кривой, используя разные параметры. Он возвращает функция одного входа.

  • Метод eval: Это основная функция для генерации синей, зеленой и красной версий каждый раз. Для каждой итерации требуется три различных параметра: vfunction, который является доминирующей функцией предыдущего шага, m и s, которые представляют собой два параметра (флеоты), влияющие на форму результирующей кривой. Остальные параметры одинаковы для каждой итерации. В коде есть значения выборки для m и s для каждой итерации. Для более вызывающих: он должен аппроксимировать интеграл, где m и s - ожидаемое среднее и стандартное отклонение основного нормального распределения. Аппроксимация выполняется через узлы Гаусса-Эрмита/веса.

  • Метод find_all_intersections: это основной метод нахождения в каждая итерация является доминирующей. Он строит доминирующую функции через кусочную мудрую конкатенацию синего, зеленого и красного кривая. Это достигается с помощью функции piecewise.

Вот полный код

import numpy as np
import pandas as pd
from scipy.optimize import brentq
import multiprocessing as mp
import pathos as pt
import timeit
import math
class A(object):
    def u(self, w):
        _w = np.asarray(w).copy()
        _w[_w >= 120000] = 120000
        _p = np.maximum(0, 100000 - _w)
        return _w - 1000*_p**2

    def __init__(self, T, upper_bound, lower_bound):
        self.T = T
        self.upper_bound = upper_bound
        self.lower_bound = lower_bound

    def _function_template(self, *args):
        def _f(x):
            return self.evalv(x, *args)
        return _f

    def evalv(self, w, c, vfunction, g, m, s, gauss_weights, gauss_nodes):
        _A = np.tile(1 + m + math.sqrt(2) * s * gauss_nodes, (np.size(w), 1))
        _W = (_A.T * w).T
        _W = gauss_weights * vfunction(np.ravel(_W)).reshape(np.size(w),
                                                             len(gauss_nodes))
        evalue = g*1/math.sqrt(math.pi)*np.sum(_W, axis=1)
        return c + evalue

    def find_all_intersections(self):

        # the hermite gauss weights and nodes for integration
        # and additional paramters used for function generation

        gauss = np.polynomial.hermite.hermgauss(10)
        gauss_nodes = gauss[0]
        gauss_weights = gauss[1]
        r = np.asarray([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                        1., 1., 1., 1., 1., 1., 1., 1., 1.])
        m = [[0.038063407778193614, 0.08475713587463352, 0.15420895520972322],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.03836174909668277, 0.08543620707856969, 0.15548297423808233],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.038063407778193614, 0.08475713587463352, 0.15420895520972322],
             [0.038063407778193614, 0.08475713587463352, 0.15420895520972322],
             [0.03836174909668277, 0.08543620707856969, 0.15548297423808233],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.038063407778193614, 0.08475713587463352, 0.15420895520972322],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.03836174909668277, 0.08543620707856969, 0.15548297423808233],
             [0.038063407778193614, 0.08475713587463352, 0.15420895520972322],
             [0.038063407778193614, 0.08475713587463352, 0.15420895520972322],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.03836174909668277, 0.08543620707856969, 0.15548297423808233],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624],
             [0.038212567720998125, 0.08509661835487026, 0.15484578903763624]]

        s = [[0.01945441966324046, 0.04690600929081242, 0.200125178687699],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.019529101011406914, 0.04708607140891122, 0.20089341636351565],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.01945441966324046, 0.04690600929081242, 0.200125178687699],
             [0.01945441966324046, 0.04690600929081242, 0.200125178687699],
             [0.019529101011406914, 0.04708607140891122, 0.20089341636351565],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.01945441966324046, 0.04690600929081242, 0.200125178687699],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.019529101011406914, 0.04708607140891122, 0.20089341636351565],
             [0.01945441966324046, 0.04690600929081242, 0.200125178687699],
             [0.01945441966324046, 0.04690600929081242, 0.200125178687699],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.019529101011406914, 0.04708607140891122, 0.20089341636351565],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142],
             [0.019491796104351332, 0.04699612658674578, 0.20050966545654142]]

        self.solution = []

        n_cpu = mp.cpu_count()
        pool = pt.multiprocessing.ProcessPool(n_cpu)

        # this function is used for multiprocessing
        def call_f(f, x):
            return f(x)

        # this function takes differences for getting cross points
        def _diff(f_dom, f_other):
            def h(x):
                return f_dom(x) - f_other(x)
            return h

        # finds the root of two function
        def find_roots(F, u_bound, l_bound):
                try:
                    sol = brentq(F, a=l_bound,
                                 b=u_bound)
                    if np.absolute(sol - u_bound) > 1:
                        return sol
                    else:
                        return l_bound
                except ValueError:
                    return l_bound

        # piecewise function
        def piecewise(l_comp, l_f):
            def f(x):
                _ind_f = np.digitize(x, l_comp) - 1
                if np.isscalar(x):
                    return l_f[_ind_f](x)
                else:
                    return np.asarray([l_f[_ind_f[i]](x[i])
                                       for i in range(0, len(x))]).ravel()
            return f

        _u = self.u

        for t in range(self.T-1, -1, -1):
            print('iteration' + ' ' + str(t))

            l_bound, u_bound = 0.5*self.lower_bound, self.upper_bound
            l_ordered_functions = []
            l_roots = []
            l_solution = []

            # build all function variations

            l_functions = [self._function_template(0, _u, r[t], m[t][i], s[t][i],
                                                   gauss_weights, gauss_nodes)
                           for i in range(0, len(m[t]))]

            # get the best solution for the upper bound on the very
            # right hand side of wealth interval

            array_functions = np.asarray(l_functions)
            start_time = timeit.default_timer()
            functions_values = pool.map(call_f, array_functions.tolist(),
                                        len(m[t]) * [u_bound])
            elapsed = timeit.default_timer() - start_time
            print('to compute function values it took')
            print(elapsed)

            ind = np.argmax(functions_values)
            cross_points = len(m[t]) * [u_bound]
            l_roots.insert(0, u_bound)
            max_m = m[t][ind]
            l_solution.insert(0, max_m)

            # move from the upper bound twoards the lower bound
            # and find the dominating solution by exploring all cross
            # points.

            test = True

            while test:
                l_ordered_functions.insert(0, array_functions[ind])
                current_max = l_ordered_functions[0]

                l_c_max = len(m[t]) * [current_max]
                l_u_cross = len(m[t]) * [cross_points[ind]]

                # Find new cross points on the smaller interval

                diff = pool.map(_diff, l_c_max, array_functions.tolist())
                cross_points = pool.map(find_roots, diff,
                                        l_u_cross, len(m[t]) * [l_bound])

                # update the solution, cross points and current
                # dominating function.

                ind = np.argmax(cross_points)
                l_roots.insert(0, cross_points[ind])
                max_m = m[t][ind]
                l_solution.insert(0, max_m)

                if cross_points[ind] <= l_bound:
                    test = False

            l_ordered_functions.insert(0, l_functions[0])
            l_roots.insert(0, 0)
            l_roots[-1] = np.inf

            l_comp = l_roots[:]
            l_f = l_ordered_functions[:]

            # build piecewise function which is used for next
            # iteration.

            _u = piecewise(l_comp, l_f)
            _sol = pd.DataFrame(data=l_solution,
                                index=np.asarray(l_roots)[0:-1])
            self.solution.insert(0, _sol)
        return self.solution

Ответы

Ответ 1

Ваш код действительно слишком сложный, чтобы объяснить вашу проблему. Стремитесь к чему-то более простому. Иногда вам приходится писать код, чтобы продемонстрировать проблему.

Я принимаю удар, основанный исключительно на вашем описании, а не на вашем коде (хотя я запустил код и проверил). Вот ваша проблема:

метод eval: это основная функция генерации синего, зеленого и красные версии каждый раз. Требуется три различных параметра: каждый итерация: vfunction, которая является доминирующей функцией из предыдущий шаг, m и s, которые являются двумя параметрами (флеотами), влияющими на формы результирующей кривой.

Ваш параметр vfunction более сложный на каждой итерации. Вы передаете вложенную функцию, созданную поверх предыдущих итераций, что вызывает рекурсивное выполнение. Каждая итерация увеличивает глубину рекурсивного вызова.

Как вы можете избежать этого? Там нет простого или встроенного способа. Самый простой ответ - при условии, что входные данные для этих функций согласованы - сохранить функциональный результат (т.е. Числа), а не сами функции. Вы можете сделать это, пока у вас есть конечное число известных входов.

Если входы базовых функций не согласованы, то нет ярлыка. Вам нужно многократно оценивать эти основные функции. Я вижу, что вы выполняете кусочно сплайсинг базовых функций - вы можете проверить, стоит ли затратить на это расходы, просто беря max каждой из основных функций.

Тест, который я выполнил (10 итераций), занял несколько секунд. Я не вижу в этом проблемы.

Ответ 2

Давайте начнем с изменения кода для вывода текущей итерации:

_u = self.u
for t in range(0, self.T):
    print(t)
    lparams = np.random.randint(self.a, self.b, 6).reshape(3, 2).tolist()
    functions = [self._function_template(_u, *lparams[i])
                 for i in range(0, 3)]
    # evaluate functions
    pairs = list(itertools.combinations(functions, 2))
    fval = [F(diff(*pairs[i]), self.a, self.b) for i in range(0, 3)]
    ind = np.sort(np.unique(np.random.randint(self.a, self.b, 10)))
    _u = _temp(ind, np.asarray(functions)[ind % 3])

Заглядывая в строку, вызывающую поведение,

fval = [F(diff(*pairs[i]), self.a, self.b) for i in range(0, 3)]

представляющие интерес функции будут F и diff. Последнее простое, первое:

def F(f, a, b):
    try:
        brentq(f, a=a, b=b)
    except ValueError:
        pass

Хмм, проглатывая исключения, посмотрим, что получится, если мы:

def F(f, a, b):
    brentq(f, a=a, b=b)

Сразу же для первой функции и первой итерации возникает ошибка:

ValueError: f (a) и f (b) должны иметь разные знаки

Глядя на docs, это является обязательным условием функции поиска корня brentq. Позвольте изменить определение еще раз, чтобы контролировать это условие на каждой итерации.

def F(f, a, b):
    try:
        brentq(f, a=a, b=b)
    except ValueError as e:
        print(e)

Выходной сигнал

i
f(a) and f(b) must have different signs
f(a) and f(b) must have different signs
f(a) and f(b) must have different signs

для i в диапазоне от 0 до 57. Значение, когда функция F всегда выполняет какую-либо реальную работу для i=58. И он продолжает делать это для более высоких значений i.

Заключение: для этих более высоких значений требуется больше времени, потому что:

  • корень никогда не вычисляется для более низких значений
  • число вычислений растет линейно для i>58