В моих программах машинного обучения у меня есть типичная ситуация, когда мне нужно выполнить определенные операции с данными, что приводит к следующему графику:
- данные —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.) А именно:
Переменные
self._complex_transform1
иself._complex_transform2
Конструкции
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 ответа
Это может быть работа для @functools.cached_property
. Это очень похоже на @property
, за исключением того, что он запоминает значение между вызовами. Затем значение можно очистить с помощью del
в fit
функции или в другом месте, если необходимо.
К сожалению, свойства не могут быть del
ed до тех пор, пока они не будут установлены, а это значит, что мы должны повторно добавить вареную пластину в 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
Один из вариантов — использовать следующий код. Однако я даже не совсем уверен, почему это работает с участником 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
Спасибо! Тем временем я также нашел и альтернативное решение. Не могли бы вы просмотреть это и оставить отзыв? Я также добавил туда вопрос, потому что я даже не совсем уверен, как мне удалось заставить это работать. Я превращаю член класса в член экземпляра — это механизм, которого я действительно не понимаю.
— Make42