Как избежать пересчета нескольких переменных без полного рефакторинга кода

У меня есть категория функций «детекторы», которые, учитывая видео, параметр пространственной шкалы и параметр шкалы времени, обнаруживают некоторые точки интереса. Я написал функцию «MultiscaleDetector», которая в основном принимает список параметров пространственного масштаба и функцию «детектор» в качестве параметров и выполняет заданный коэффициент детектора для каждого масштаба.

Вот как они выглядят:

def GaborDetector(v, sigma, tau, threshold, num_points):
    """
    Gabor Detector
    
    Keyword arguments:
    video -- input video (y_len, x_len, frames)
    sigma -- Gaussian kernel space standard deviation
    tau -- Gaussian kernel time standard deviation
    kappa -- Gabor response threshold
    """
    # setup video
    video = v.copy()
    video = video.astype(float)/video.max()
    video = video_smoothen_space(video, sigma)
    # first define a linspace of width -2tau to 2tau
    time = np.linspace(-2*tau, 2*tau, int(4*tau+1))
    omega = 4/tau
    # define the gabor filters
    h_ev = np.exp(-time**2/(2*tau**2)) * np.cos(2*np.pi*omega*time)
    h_od = np.exp(-time**2/(2*tau**2)) * np.sin(2*np.pi*omega*time)
    # normalize the L1 norm
    h_ev /= np.linalg.norm(h_ev, ord=1)
    h_od /= np.linalg.norm(h_od, ord=1)
    # compute the response
    response = (scp.convolve1d(video, h_ev, axis=2) ** 2) + (scp.convolve1d(video, h_od, axis=2) ** 2)
    points = interest_points(response, num=num_points, threshold=threshold, scale=sigma)
    return points
def MultiscaleDetector(detector, video, sigmas, tau, num_points):
    """
    Multiscale Detector

    Executes a detector at multiple scales. Detector has to be a function that
    takes a video as input, along with other parameters, and returns a list of interest points.

    
    Keyword arguments:
    detector -- function that returns interest points
    video -- input video (y_len, x_len, frames)
    sigmas -- list of scales
    """
    
    # for every scale, compute the response
    points = []
    for sigm in sigmas:
        found = detector(video, sigm, tau)
        points.append(found)

    # filter the points, currently irrelevant

Как вы, возможно, уже заметили, внутри функции "GaborDetector" выполняются некоторые тяжелые вычисления, которые зависят только от параметра времени. Поэтому функция «MultiscaleDetector» излишне пересчитывает эти переменные при каждом вызове.

Пытаясь избежать рефакторинга кода, я придумал то, что хотел, чтобы было хорошим трюком, но считал, что это будет напрасно:

def MultiscaleDetector(detector, video, sigmas, tau, num_points):
    """
    Multiscale Detector

    Executes a detector at multiple scales. Detector has to be a function that
    takes a video as input, along with other parameters, and returns a list of interest points.

    
    Keyword arguments:
    detector -- function that returns interest points
    video -- input video (y_len, x_len, frames)
    sigmas -- list of scales
    """
    
    optimization_trick = lambda s_param: detector(video, s_param, tau)
    # for every scale, compute the response
    points = []
    for sigm in sigmas:
        found = optimization_trick(sigm)
        points.append(found)

    # filter the points, currently irrelevant

Я надеялся, что переменные, зависящие только от тау, останутся каким-то образом «сохраненными» в «оптимизации_трюке» и не будут пересчитаны. Однако, когда я измерял время для разных реализаций, разница составляла около 0,02 секунды, причем «оптимизированная» функция работала быстрее.

Обновлено:

Фактический вызов, который имеет место, выглядит следующим образом:

# read the video
video = read_video(video_name, num_frames, 0)
# get the interest points
detector = lambda v, s, t: GaborDetector(v, s, t, 0.3, 500)
scales = [3*(1.1)**i for i in range(6)]
start = time.time()
points = MultiscaleDetector(detector, video, scales, 1.5, 500)
end = time.time()
print("Time: {}".format(end-start))

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

def GaborDetectorTrial(v, sigma, tau, threshold, num_points):
    """
    Gabor Detector
    
    Keyword arguments:
    video -- input video (y_len, x_len, frames)
    sigma -- Gaussian kernel space standard deviation
    tau -- Gaussian kernel time standard deviation
    kappa -- Gabor response threshold
    """
    @lru_cache(maxsize=None)
    def time_function(tau):
        time = np.linspace(-2*tau, 2*tau, int(4*tau+1))
        omega = 4/tau
        # define the gabor filters
        h_ev = np.exp(-time**2/(2*tau**2)) * np.cos(2*np.pi*omega*time)
        h_od = np.exp(-time**2/(2*tau**2)) * np.sin(2*np.pi*omega*time)
        # normalize the L1 norm
        h_ev /= np.linalg.norm(h_ev, ord=1)
        h_od /= np.linalg.norm(h_od, ord=1)
        return h_ev, h_od
    # setup video
    video = v.copy()
    video = video.astype(float)/video.max()
    video = video_smoothen_space(video, sigma)
    # compute the response
    h_ev, h_od = time_function(tau)
    response = (scp.convolve1d(video, h_ev, axis=2) ** 2) + (scp.convolve1d(video, h_od, axis=2) ** 2)
    points = interest_points(response, num=num_points, threshold=threshold, scale=sigma)
    return points

Переменные, которые я хочу избежать пересчета, это h_ev и h_od. Время выполнения было в основном таким же (на самом деле, на несколько мс медленнее).

Рассматривали ли вы использование декоратора кеша, чтобы уже вычисленное значение не вычислялось снова, а бралось непосредственно из кеша?

Sparkling Marcel 15.06.2023 15:06

@SparklingMarcel звучит как хорошая идея, я не знал, что такие вещи существуют. Я это попробую.

Stefanos Anagnostou 15.06.2023 15:37

@SparklingMarcel Я немного изучил это, и кажется, что эти декораторы предназначены для кеширования результатов, если я правильно понимаю. В моем примере меня интересует кэширование переменных «время», «омега» и «h_ev», «h_od». Я не перестану изучать это, потому что это кажется интересным, но сейчас у меня мало времени. Спасибо за предложение в любом случае!

Stefanos Anagnostou 15.06.2023 15:52

@StefanosAnagnostou Разве извлечение вычисления в отдельную функцию, которая принимает параметр времени, не сделает так, чтобы вы могли использовать декоратор кеша?

Sylwester 15.06.2023 18:22

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

Stefanos Anagnostou 15.06.2023 19:57

Что-то, чего я не понимаю в вашем коде, это то, что вы никогда не вызываете GaborDetector в своем примере. В любом случае, если некоторые из ваших переменных всегда одинаковы и их нужно вычислить только один раз, поместите их в функцию, верните их, а затем используйте их в качестве параметра для другой функции. Если вы можете обновить свой код с помощью соответствующего вызова функции (GarborDetector), а затем указать нам, какую именно переменную вы не хотите «пересчитывать», я смогу дать удовлетворительный ответ :)

Sparkling Marcel 16.06.2023 08:37

@SparklingMarcel Извините, я думал, что объяснил это, но, возможно, это требует пояснений. Функция "GaborDetector" передается в качестве параметра "MultiscaleDetector". "MultiscaleDetector" предназначен для использования в качестве параметра любой существующей "детекторной" функции (например, "GaborDetector" или других детекторов, которые я не показывал, поскольку они не имеют значения). Я думаю, что ваше последнее предложение - это то, что также предложил Сильвестр. Звучит очень разумно, и я действительно пробовал, но, к сожалению, это не окупается. Я обновлю вопрос.

Stefanos Anagnostou 16.06.2023 10:10

Как можно передать GaborDetector как функцию, если она принимает 5 параметров?

quamrana 16.06.2023 10:12

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

Stefanos Anagnostou 16.06.2023 10:26

Хорошо, теперь я это вижу. Но какой именно бит вы хотите избежать пересчета?

quamrana 16.06.2023 10:45

В вашем примере вы вызываете GaborDetectorTrial только один раз, поэтому вы ничего не пересчитываете, вы имеете в виду, что в реальном случае вы вызываете GaborDetectorTrial несколько раз, но «Тау» всегда одно и то же?

Sparkling Marcel 16.06.2023 10:46

@Stefanos Anagnostou — Вы пишете: «На самом деле происходит следующий звонок: … detector = lambda v, s, t: GaborDetector(v, s, t, 0.3, 500)»; по этому вы вообще не звоните GaborDetectorTrial, а скорее GaborDetector.

Armali 16.06.2023 11:23

@SparklingMarcel В моем примере GaborDetector вызывается несколько раз внутри MultiscaleDetector. Внутри MultiscaleDetector есть цикл, который вызывает GaborDetector несколько раз для разных сигм, но для одного и того же тау. Да, "тау" всегда одно и то же.

Stefanos Anagnostou 16.06.2023 11:50

@Armali Я отсчитываю исходную версию, а затем меняю код на пробную версию. Я не хотел публиковать слишком много кода. Возможно, я должен был быть более ясным. Извините за путаницу.

Stefanos Anagnostou 16.06.2023 11:51
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
14
53
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Разве вы не можете просто разделить несколько функций?

Пример :

def time_function(tau): #To compute hev and hod
        time = np.linspace(-2*tau, 2*tau, int(4*tau+1))
        omega = 4/tau
        # define the gabor filters
        h_ev = np.exp(-time**2/(2*tau**2)) * np.cos(2*np.pi*omega*time)
        h_od = np.exp(-time**2/(2*tau**2)) * np.sin(2*np.pi*omega*time)
        # normalize the L1 norm
        h_ev /= np.linalg.norm(h_ev, ord=1)
        h_od /= np.linalg.norm(h_od, ord=1)
        return h_ev, h_od

а потом

# read the video
video = read_video(video_name, num_frames, 0)
# get the interest points
custom_tau = {}

scales = [3*(1.1)**i for i in range(6)]
start = time.time()
for i in range(0,10): # for loop cause otherwise you call GaborDetector only once so the question is not revelant
    #Insert logic here because for your question to be revelant Tau must not always be the same
    if tau not in custom_tau.keys(): # Logic to "cache" hev and hod for the same tau value
        hev, hod = time_function(tau)
        custom_tau[tau] = (hev, hod)
    else:
        hev, hod = custom_tau[tau]
    gabor = GaborDetector(v, s, t, hev, hod, 0.3, 500)
    points = MultiscaleDetector(gabor, video, scales, 1.5, 500)
end = time.time()
print("Time: {}".format(end-start))

и, наконец, вы меняете GarborCollector, чтобы не пересчитывать время

def GaborDetectorTrial(v, sigma, tau,hev, hod, threshold, num_points):

    video = v.copy()
    video = video.astype(float)/video.max()
    video = video_smoothen_space(video, sigma)
    response = (scp.convolve1d(video, h_ev, axis=2) ** 2) + (scp.convolve1d(video, h_od, axis=2) ** 2)
    points = interest_points(response, num=num_points, threshold=threshold, scale=sigma)
    return points

Что вы делаете в этом коде, так это «кэширование» (через словарь) hev и hod, так что, если вы уже вычислили его для данного «tau», вы вместо этого ищете результат в словаре, в противном случае вы вычисляете его и помещаете его в словаре

Мне пришлось экстраполировать и угадать, как будет работать ваш код, потому что в приведенном вами случае he_v и ho_d никогда не вычисляются повторно, поскольку вы вызываете garbor только один раз.

Я переформатировал код так, чтобы функция GaborDetector принимала «h_ev» и «h_od» в качестве параметров, предварительно вычисленных «time_function», близко к тому, что вы предлагаете. Есть небольшое, но постоянное ускорение около 200-500 мс. Лично я не нахожу это идеальным (из соображений дизайна, хотя, возможно, это легко исправить), но ускорение есть! Я отмечу ответ как принятый.

Stefanos Anagnostou 16.06.2023 12:14

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

Я изменил область, переместив «time_function» за пределы определения «GaborDetector», но, к сожалению, я все еще не вижу ускорения. Я нахожу это действительно странным, потому что то, что вы предлагаете, кажется мне логичным.

Stefanos Anagnostou 16.06.2023 12:06

Хм - мне тоже это кажется странным, мне кажется, это сработало. (Я проверил, вставив print("time_function(%d)"%tau) в time_function.)

Armali 16.06.2023 12:38

Другие вопросы по теме