Кэширование, которое сбрасывается при определенных обстоятельствах: Уменьшите код стандартной пластины

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

  • данные —op A -> intermediate_state —op B -> тип конечного результата 1
  • данные —op A -> intermediate_state —op C -> тип окончательного результата 2
  • данные —op A -> intermediate_state —op D -> тип окончательного результата 3

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

Поскольку для операций требуется параметр, я предоставляю его в конструкторе. Мне также нужно передать параметризованный объект с одинаковой параметризацией в разные части программы, где данные операции A изменяются без изменения параметризации. Вот почему fit это собственный метод, а не часть конструктора. Кроме того, из-за этого я не могу просто кэшировать результаты операций, отличных от A, без их сброса после вызова нового fit.

Вот мой результат на Python:

class MyClass:       

    def __init__(self, parameter):
        self.parameter = parameter

    # expensive machine learning fitting
    def fit(self, data) -> MyClass:
        self.intermediate_data_ = data + self.parameter + 2
        self._complex_transform1 = None
        self._complex_transform2 = None
        return self

    # expensive machine learning operation version 1
    @property
    def complex_transform1(self):
        if self._complex_transform1 is None:
            self._complex_transform1 = self.intermediate_data_ / self.parameter / 2
        return self._complex_transform1

    # expensive machine learning operation version 2
    @property
    def complex_transform2(self):
        if self._complex_transform2 is None:
            self._complex_transform2 = self.intermediate_data_ / self.parameter / 5
        return self._complex_transform2

Проблема, с которой я сталкиваюсь с этим подходом, заключается в том, что я повторяю довольно много кода котельной пластины. (Обратите внимание, что у меня есть много других операций, кроме A.) А именно:

  1. Переменные self._complex_transform1 и self._complex_transform2

  2. Конструкции if self._complex_transform1 is None: может быть уменьшено с

         if self._complex_transform1 is None:
             self._complex_transform1 = self.intermediate_data_ / self.parameter / 2
         return self._complex_transform1
    

к

        return self.intermediate_data_ / self.parameter / 2

и я бы сохранил гнездо.

Я думал об определении класса контейнера, который содержит результаты и сбрасывается при подгонке. Но я не вижу, как реализовать это, не вводя в методы преобразования то же самое, что и шаблонный код.

Какой-нибудь декораторский подход может быть отличным, но я не уверен, как к этому подойти.

2 ответа
2

Это может быть работа для @functools.cached_property. Это очень похоже на @property, за исключением того, что он запоминает значение между вызовами. Затем значение можно очистить с помощью del в fit функции или в другом месте, если необходимо.

К сожалению, свойства не могут быть deled до тех пор, пока они не будут установлены, а это значит, что мы должны повторно добавить вареную пластину в fit в виде try: del x; except AttributeError: pass. Возможно, это улучшение, поскольку оно хотя бы централизовано, но все равно не идеально.

Есть обходные пути вроде

def fit(self, data):
    self.intermediate_data_ = ...

    for prop in ["complex_transform1", "complex_transform2"]:
        try:
            delattr(self, prop)
        except AttributeError:
            pass

    return self

Или, возможно, что-то вроде

def fit(self, data):
    self.intermediate_data_ = ...

    for prop in self.reset_on_fit:
        delattr(self, prop)

    self.reset_on_fit = []
    return self

со свойствами, похожими на

@cached_property
def complex_transform1(self):
    self.reset_on_fit.append("complex_transform1")
    return self.intermediate_data_ / self.parameter / 2

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

Что ж, я не тестировал это очень тщательно, но думаю, что мы можем:

from functools import cached_property, wraps

def resets_on_fit(fn):
    @wraps(fn)
    def wrapper(self):
        self.reset_on_fit.append(fn.__name__)
        return fn(self)

    return wrapper

class MyClass:
    # ...

    def fit(self, data):
        self.intermediate_data_ = data + self.parameter + 2
        
        for prop in self.reset_on_fit:
            delattr(self, prop)
        
        self.reset_on_fit = []
        return self

    @cached_property
    @resets_on_fit
    def complex_transform1(self):
        return self.intermediate_data_ / self.parameter / 2

  • Спасибо! Тем временем я также нашел и альтернативное решение. Не могли бы вы просмотреть это и оставить отзыв? Я также добавил туда вопрос, потому что я даже не совсем уверен, как мне удалось заставить это работать. Я превращаю член класса в член экземпляра — это механизм, которого я действительно не понимаю.

    — Make42


Один из вариантов — использовать следующий код. Однако я даже не совсем уверен, почему это работает с участником property_cache = PropertyCache() в MyClass. Сначала он определяется как член класса, что он и должен, потому что синтаксис декоратора не улавливает его (если бы я написал self.property_cache = PropertyCache()). Но потом пишем в конструкторе self.property_cache = PropertyCache() кажется, «перезаписывает» член класса и превращает его в член экземпляра. И не совсем понимаю этот механизм.

class PropertyCache:

    def __init__(self):
        self.cache = {}

    def reset(self):
        self.cache = {}

    def __call__(this, function):

        def wrapper(self, *args, **kwargs):
            if function.__qualname__ not in self.property_cache.cache:
                self.property_cache.cache[function.__qualname__] = function(self, *args, **kwargs)
            return self.property_cache.cache[function.__qualname__]
        return wrapper


class MyClass:

    property_cache = PropertyCache()

    def __init__(self, parameter):
        self.parameter = parameter
        self.property_cache = PropertyCache()

    def fit(self, data):
        print('fit')
        self.intermediate_data_ = data + self.parameter + 2
        self.property_cache.reset()
        return self

    @property
    @property_cache
    def trans1(self):
        print('trans 1')
        return self.intermediate_data_ / self.parameter / 2

    @property
    @property_cache
    def trans2(self):
        print('trans 2')
        return self.intermediate_data_ / self.parameter / 5


myclass = MyClass(2)
myclass.fit(10)
myclass.trans1
myclass.trans1
myclass.trans2
myclass.fit(15)
myclass.trans1
myclass.trans1
myclass.trans2

myclass2 = MyClass(3)
myclass2.fit(15)
myclass2.trans2
myclass2.trans2

myclass.trans2

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *